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'; 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'; 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 ); }, }, ]; /** * 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); * } * } * }); * ``` */ export class SmartRegistry { private storage: RegistryStorage; private authManager: AuthManager; private registries: Map = new Map(); 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); } /** * Initialize the registry system */ public async init(): Promise { if (this.initialized) return; // Initialize storage await this.storage.init(); // Initialize auth manager await this.authManager.init(); 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); } this.initialized = true; } /** * Handle an incoming HTTP request * Routes to the appropriate protocol handler based on path */ public async handleRequest(context: IRequestContext): Promise { const path = context.path; let response: IResponse | undefined; for (const descriptor of registryDescriptors) { if (response) { break; } const protocolConfig = descriptor.getConfig(this.config); if (!protocolConfig?.enabled || !descriptor.matchesPath(this.config, path)) { continue; } const registry = this.registries.get(descriptor.protocol); if (registry) { response = await registry.handleRequest(context); } } // 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 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; } /** * Get the storage instance (for testing/advanced use) */ public getStorage(): RegistryStorage { return this.storage; } /** * Get the auth manager instance (for testing/advanced use) */ public getAuthManager(): AuthManager { return this.authManager; } /** * Get a specific registry handler */ public getRegistry(protocol: 'oci' | 'npm' | 'maven' | 'cargo' | 'composer' | 'pypi' | 'rubygems'): BaseRegistry | undefined { return this.registries.get(protocol); } /** * Check if the registry is initialized */ public isInitialized(): boolean { return this.initialized; } /** * Clean up resources (timers, connections, etc.) */ public destroy(): void { for (const registry of this.registries.values()) { registry.destroy(); } } }