Files
smartregistry/ts/classes.smartregistry.ts
T

310 lines
9.2 KiB
TypeScript
Raw Normal View History

2025-11-19 15:32:00 +00:00
import { RegistryStorage } from './core/classes.registrystorage.js';
import { AuthManager } from './core/classes.authmanager.js';
import { BaseRegistry } from './core/classes.baseregistry.js';
import type {
IProtocolConfig,
IRegistryConfig,
IRequestContext,
IResponse,
TRegistryProtocol,
} from './core/interfaces.core.js';
import { toReadableStream } from './core/helpers.stream.js';
2025-11-19 15:32:00 +00:00
import { OciRegistry } from './oci/classes.ociregistry.js';
import { NpmRegistry } from './npm/classes.npmregistry.js';
import { MavenRegistry } from './maven/classes.mavenregistry.js';
import { CargoRegistry } from './cargo/classes.cargoregistry.js';
import { ComposerRegistry } from './composer/classes.composerregistry.js';
import { PypiRegistry } from './pypi/classes.pypiregistry.js';
import { RubyGemsRegistry } from './rubygems/classes.rubygemsregistry.js';
2025-11-19 15:16:20 +00:00
type TRegistryDescriptor = {
protocol: TRegistryProtocol;
getConfig: (config: IRegistryConfig) => IProtocolConfig | undefined;
matchesPath: (config: IRegistryConfig, path: string) => boolean;
create: (args: {
storage: RegistryStorage;
authManager: AuthManager;
config: IRegistryConfig;
protocolConfig: IProtocolConfig;
}) => BaseRegistry;
};
const registryDescriptors: TRegistryDescriptor[] = [
{
protocol: 'oci',
getConfig: (config) => config.oci,
matchesPath: (config, path) => path.startsWith(config.oci?.basePath ?? '/oci'),
create: ({ storage, authManager, config, protocolConfig }) => {
const ociTokens = config.auth.ociTokens?.enabled ? {
realm: config.auth.ociTokens.realm,
service: config.auth.ociTokens.service,
} : undefined;
return new OciRegistry(
storage,
authManager,
protocolConfig.basePath ?? '/oci',
ociTokens,
config.upstreamProvider
);
},
},
{
protocol: 'npm',
getConfig: (config) => config.npm,
matchesPath: (config, path) => path.startsWith(config.npm?.basePath ?? '/npm'),
create: ({ storage, authManager, config, protocolConfig }) => {
const basePath = protocolConfig.basePath ?? '/npm';
return new NpmRegistry(
storage,
authManager,
basePath,
protocolConfig.registryUrl ?? `http://localhost:5000${basePath}`,
config.upstreamProvider
);
},
},
{
protocol: 'maven',
getConfig: (config) => config.maven,
matchesPath: (config, path) => path.startsWith(config.maven?.basePath ?? '/maven'),
create: ({ storage, authManager, config, protocolConfig }) => {
const basePath = protocolConfig.basePath ?? '/maven';
return new MavenRegistry(
storage,
authManager,
basePath,
protocolConfig.registryUrl ?? `http://localhost:5000${basePath}`,
config.upstreamProvider
);
},
},
{
protocol: 'cargo',
getConfig: (config) => config.cargo,
matchesPath: (config, path) => path.startsWith(config.cargo?.basePath ?? '/cargo'),
create: ({ storage, authManager, config, protocolConfig }) => {
const basePath = protocolConfig.basePath ?? '/cargo';
return new CargoRegistry(
storage,
authManager,
basePath,
protocolConfig.registryUrl ?? `http://localhost:5000${basePath}`,
config.upstreamProvider
);
},
},
{
protocol: 'composer',
getConfig: (config) => config.composer,
matchesPath: (config, path) => path.startsWith(config.composer?.basePath ?? '/composer'),
create: ({ storage, authManager, config, protocolConfig }) => {
const basePath = protocolConfig.basePath ?? '/composer';
return new ComposerRegistry(
storage,
authManager,
basePath,
protocolConfig.registryUrl ?? `http://localhost:5000${basePath}`,
config.upstreamProvider
);
},
},
{
protocol: 'pypi',
getConfig: (config) => config.pypi,
matchesPath: (config, path) => {
const basePath = config.pypi?.basePath ?? '/pypi';
return path.startsWith(basePath) || path.startsWith('/simple');
},
create: ({ storage, authManager, config, protocolConfig }) => new PypiRegistry(
storage,
authManager,
protocolConfig.basePath ?? '/pypi',
protocolConfig.registryUrl ?? 'http://localhost:5000',
config.upstreamProvider
),
},
{
protocol: 'rubygems',
getConfig: (config) => config.rubygems,
matchesPath: (config, path) => path.startsWith(config.rubygems?.basePath ?? '/rubygems'),
create: ({ storage, authManager, config, protocolConfig }) => {
const basePath = protocolConfig.basePath ?? '/rubygems';
return new RubyGemsRegistry(
storage,
authManager,
basePath,
protocolConfig.registryUrl ?? `http://localhost:5000${basePath}`,
config.upstreamProvider
);
},
},
];
2025-11-19 15:16:20 +00:00
/**
* Main registry orchestrator.
* Routes requests to appropriate protocol handlers (OCI, NPM, Maven, Cargo, Composer, PyPI, or RubyGems).
*
* Supports pluggable authentication and storage hooks:
*
* @example
* ```typescript
* // Basic usage with default in-memory auth
* const registry = new SmartRegistry(config);
*
* // With custom auth provider (LDAP, OAuth, etc.)
* const registry = new SmartRegistry({
* ...config,
* authProvider: new LdapAuthProvider(ldapClient),
* });
*
* // With storage hooks for quota tracking
* const registry = new SmartRegistry({
* ...config,
* storageHooks: {
* beforePut: async (ctx) => {
* const quota = await getQuota(ctx.actor?.orgId);
* if (ctx.metadata?.size > quota) {
* return { allowed: false, reason: 'Quota exceeded' };
* }
* return { allowed: true };
* },
* afterPut: async (ctx) => {
* await auditLog('storage.put', ctx);
* }
* }
* });
* ```
2025-11-19 15:16:20 +00:00
*/
export class SmartRegistry {
private storage: RegistryStorage;
2025-11-19 15:32:00 +00:00
private authManager: AuthManager;
private registries: Map<TRegistryProtocol, BaseRegistry> = new Map();
2025-11-19 15:16:20 +00:00
private config: IRegistryConfig;
private initialized: boolean = false;
constructor(config: IRegistryConfig) {
this.config = config;
// Create storage with optional hooks
this.storage = new RegistryStorage(config.storage, config.storageHooks);
// Create auth manager with optional custom provider
this.authManager = new AuthManager(config.auth, config.authProvider);
2025-11-19 15:16:20 +00:00
}
/**
2025-11-19 15:32:00 +00:00
* Initialize the registry system
2025-11-19 15:16:20 +00:00
*/
public async init(): Promise<void> {
if (this.initialized) return;
2025-11-19 15:32:00 +00:00
// Initialize storage
await this.storage.init();
2025-11-19 15:16:20 +00:00
2025-11-19 15:32:00 +00:00
// Initialize auth manager
await this.authManager.init();
2025-11-19 15:16:20 +00:00
for (const descriptor of registryDescriptors) {
const protocolConfig = descriptor.getConfig(this.config);
if (!protocolConfig?.enabled) {
continue;
}
const registry = descriptor.create({
storage: this.storage,
authManager: this.authManager,
config: this.config,
protocolConfig,
});
await registry.init();
this.registries.set(descriptor.protocol, registry);
}
2025-11-19 15:32:00 +00:00
this.initialized = true;
2025-11-19 15:16:20 +00:00
}
/**
2025-11-19 15:32:00 +00:00
* Handle an incoming HTTP request
* Routes to the appropriate protocol handler based on path
2025-11-19 15:16:20 +00:00
*/
2025-11-19 15:32:00 +00:00
public async handleRequest(context: IRequestContext): Promise<IResponse> {
const path = context.path;
let response: IResponse | undefined;
2025-11-19 15:16:20 +00:00
for (const descriptor of registryDescriptors) {
if (response) {
break;
2025-11-19 15:16:20 +00:00
}
const protocolConfig = descriptor.getConfig(this.config);
if (!protocolConfig?.enabled || !descriptor.matchesPath(this.config, path)) {
continue;
2025-11-19 15:16:20 +00:00
}
const registry = this.registries.get(descriptor.protocol);
if (registry) {
response = await registry.handleRequest(context);
}
}
2025-11-19 15:32:00 +00:00
// No matching registry
if (!response) {
response = {
status: 404,
headers: { 'Content-Type': 'application/json' },
body: {
error: 'NOT_FOUND',
message: 'No registry handler for this path',
},
};
}
// Normalize body to ReadableStream<Uint8Array> at the API boundary
if (response.body != null && !(response.body instanceof ReadableStream)) {
if (!Buffer.isBuffer(response.body) && typeof response.body === 'object' && !(response.body instanceof Uint8Array)) {
response.headers['Content-Type'] ??= 'application/json';
}
response.body = toReadableStream(response.body);
}
return response;
2025-11-19 15:16:20 +00:00
}
/**
2025-11-19 15:32:00 +00:00
* Get the storage instance (for testing/advanced use)
2025-11-19 15:16:20 +00:00
*/
2025-11-19 15:32:00 +00:00
public getStorage(): RegistryStorage {
return this.storage;
2025-11-19 15:16:20 +00:00
}
/**
2025-11-19 15:32:00 +00:00
* Get the auth manager instance (for testing/advanced use)
2025-11-19 15:16:20 +00:00
*/
2025-11-19 15:32:00 +00:00
public getAuthManager(): AuthManager {
return this.authManager;
2025-11-19 15:16:20 +00:00
}
/**
2025-11-19 15:32:00 +00:00
* Get a specific registry handler
2025-11-19 15:16:20 +00:00
*/
public getRegistry(protocol: 'oci' | 'npm' | 'maven' | 'cargo' | 'composer' | 'pypi' | 'rubygems'): BaseRegistry | undefined {
2025-11-19 15:32:00 +00:00
return this.registries.get(protocol);
2025-11-19 15:16:20 +00:00
}
/**
2025-11-19 15:32:00 +00:00
* Check if the registry is initialized
2025-11-19 15:16:20 +00:00
*/
2025-11-19 15:32:00 +00:00
public isInitialized(): boolean {
return this.initialized;
2025-11-19 15:16:20 +00:00
}
/**
* Clean up resources (timers, connections, etc.)
*/
public destroy(): void {
for (const registry of this.registries.values()) {
registry.destroy();
}
}
2025-11-19 15:16:20 +00:00
}