310 lines
9.2 KiB
TypeScript
310 lines
9.2 KiB
TypeScript
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<TRegistryProtocol, BaseRegistry> = 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<void> {
|
|
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<IResponse> {
|
|
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<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;
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
}
|
|
}
|
|
}
|