feat(registry): add declarative protocol routing and request-scoped storage hook context across registries
This commit is contained in:
+154
-162
@@ -1,7 +1,13 @@
|
||||
import { RegistryStorage } from './core/classes.registrystorage.js';
|
||||
import { AuthManager } from './core/classes.authmanager.js';
|
||||
import { BaseRegistry } from './core/classes.baseregistry.js';
|
||||
import type { IRegistryConfig, IRequestContext, IResponse } from './core/interfaces.core.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';
|
||||
@@ -11,6 +17,129 @@ 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).
|
||||
@@ -49,7 +178,7 @@ import { RubyGemsRegistry } from './rubygems/classes.rubygemsregistry.js';
|
||||
export class SmartRegistry {
|
||||
private storage: RegistryStorage;
|
||||
private authManager: AuthManager;
|
||||
private registries: Map<string, BaseRegistry> = new Map();
|
||||
private registries: Map<TRegistryProtocol, BaseRegistry> = new Map();
|
||||
private config: IRegistryConfig;
|
||||
private initialized: boolean = false;
|
||||
|
||||
@@ -75,112 +204,20 @@ export class SmartRegistry {
|
||||
// Initialize auth manager
|
||||
await this.authManager.init();
|
||||
|
||||
// Initialize OCI registry if enabled
|
||||
if (this.config.oci?.enabled) {
|
||||
const ociBasePath = this.config.oci.basePath ?? '/oci';
|
||||
const ociTokens = this.config.auth.ociTokens?.enabled ? {
|
||||
realm: this.config.auth.ociTokens.realm,
|
||||
service: this.config.auth.ociTokens.service,
|
||||
} : undefined;
|
||||
const ociRegistry = new OciRegistry(
|
||||
this.storage,
|
||||
this.authManager,
|
||||
ociBasePath,
|
||||
ociTokens,
|
||||
this.config.upstreamProvider
|
||||
);
|
||||
await ociRegistry.init();
|
||||
this.registries.set('oci', ociRegistry);
|
||||
}
|
||||
for (const descriptor of registryDescriptors) {
|
||||
const protocolConfig = descriptor.getConfig(this.config);
|
||||
if (!protocolConfig?.enabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Initialize NPM registry if enabled
|
||||
if (this.config.npm?.enabled) {
|
||||
const npmBasePath = this.config.npm.basePath ?? '/npm';
|
||||
const registryUrl = this.config.npm.registryUrl ?? `http://localhost:5000${npmBasePath}`;
|
||||
const npmRegistry = new NpmRegistry(
|
||||
this.storage,
|
||||
this.authManager,
|
||||
npmBasePath,
|
||||
registryUrl,
|
||||
this.config.upstreamProvider
|
||||
);
|
||||
await npmRegistry.init();
|
||||
this.registries.set('npm', npmRegistry);
|
||||
}
|
||||
|
||||
// Initialize Maven registry if enabled
|
||||
if (this.config.maven?.enabled) {
|
||||
const mavenBasePath = this.config.maven.basePath ?? '/maven';
|
||||
const registryUrl = this.config.maven.registryUrl ?? `http://localhost:5000${mavenBasePath}`;
|
||||
const mavenRegistry = new MavenRegistry(
|
||||
this.storage,
|
||||
this.authManager,
|
||||
mavenBasePath,
|
||||
registryUrl,
|
||||
this.config.upstreamProvider
|
||||
);
|
||||
await mavenRegistry.init();
|
||||
this.registries.set('maven', mavenRegistry);
|
||||
}
|
||||
|
||||
// Initialize Cargo registry if enabled
|
||||
if (this.config.cargo?.enabled) {
|
||||
const cargoBasePath = this.config.cargo.basePath ?? '/cargo';
|
||||
const registryUrl = this.config.cargo.registryUrl ?? `http://localhost:5000${cargoBasePath}`;
|
||||
const cargoRegistry = new CargoRegistry(
|
||||
this.storage,
|
||||
this.authManager,
|
||||
cargoBasePath,
|
||||
registryUrl,
|
||||
this.config.upstreamProvider
|
||||
);
|
||||
await cargoRegistry.init();
|
||||
this.registries.set('cargo', cargoRegistry);
|
||||
}
|
||||
|
||||
// Initialize Composer registry if enabled
|
||||
if (this.config.composer?.enabled) {
|
||||
const composerBasePath = this.config.composer.basePath ?? '/composer';
|
||||
const registryUrl = this.config.composer.registryUrl ?? `http://localhost:5000${composerBasePath}`;
|
||||
const composerRegistry = new ComposerRegistry(
|
||||
this.storage,
|
||||
this.authManager,
|
||||
composerBasePath,
|
||||
registryUrl,
|
||||
this.config.upstreamProvider
|
||||
);
|
||||
await composerRegistry.init();
|
||||
this.registries.set('composer', composerRegistry);
|
||||
}
|
||||
|
||||
// Initialize PyPI registry if enabled
|
||||
if (this.config.pypi?.enabled) {
|
||||
const pypiBasePath = this.config.pypi.basePath ?? '/pypi';
|
||||
const registryUrl = this.config.pypi.registryUrl ?? `http://localhost:5000`;
|
||||
const pypiRegistry = new PypiRegistry(
|
||||
this.storage,
|
||||
this.authManager,
|
||||
pypiBasePath,
|
||||
registryUrl,
|
||||
this.config.upstreamProvider
|
||||
);
|
||||
await pypiRegistry.init();
|
||||
this.registries.set('pypi', pypiRegistry);
|
||||
}
|
||||
|
||||
// Initialize RubyGems registry if enabled
|
||||
if (this.config.rubygems?.enabled) {
|
||||
const rubygemsBasePath = this.config.rubygems.basePath ?? '/rubygems';
|
||||
const registryUrl = this.config.rubygems.registryUrl ?? `http://localhost:5000${rubygemsBasePath}`;
|
||||
const rubygemsRegistry = new RubyGemsRegistry(
|
||||
this.storage,
|
||||
this.authManager,
|
||||
rubygemsBasePath,
|
||||
registryUrl,
|
||||
this.config.upstreamProvider
|
||||
);
|
||||
await rubygemsRegistry.init();
|
||||
this.registries.set('rubygems', rubygemsRegistry);
|
||||
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;
|
||||
@@ -194,62 +231,19 @@ export class SmartRegistry {
|
||||
const path = context.path;
|
||||
let response: IResponse | undefined;
|
||||
|
||||
// Route to OCI registry
|
||||
if (!response && this.config.oci?.enabled && path.startsWith(this.config.oci.basePath)) {
|
||||
const ociRegistry = this.registries.get('oci');
|
||||
if (ociRegistry) {
|
||||
response = await ociRegistry.handleRequest(context);
|
||||
for (const descriptor of registryDescriptors) {
|
||||
if (response) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Route to NPM registry
|
||||
if (!response && this.config.npm?.enabled && path.startsWith(this.config.npm.basePath)) {
|
||||
const npmRegistry = this.registries.get('npm');
|
||||
if (npmRegistry) {
|
||||
response = await npmRegistry.handleRequest(context);
|
||||
const protocolConfig = descriptor.getConfig(this.config);
|
||||
if (!protocolConfig?.enabled || !descriptor.matchesPath(this.config, path)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Route to Maven registry
|
||||
if (!response && this.config.maven?.enabled && path.startsWith(this.config.maven.basePath)) {
|
||||
const mavenRegistry = this.registries.get('maven');
|
||||
if (mavenRegistry) {
|
||||
response = await mavenRegistry.handleRequest(context);
|
||||
}
|
||||
}
|
||||
|
||||
// Route to Cargo registry
|
||||
if (!response && this.config.cargo?.enabled && path.startsWith(this.config.cargo.basePath)) {
|
||||
const cargoRegistry = this.registries.get('cargo');
|
||||
if (cargoRegistry) {
|
||||
response = await cargoRegistry.handleRequest(context);
|
||||
}
|
||||
}
|
||||
|
||||
// Route to Composer registry
|
||||
if (!response && this.config.composer?.enabled && path.startsWith(this.config.composer.basePath)) {
|
||||
const composerRegistry = this.registries.get('composer');
|
||||
if (composerRegistry) {
|
||||
response = await composerRegistry.handleRequest(context);
|
||||
}
|
||||
}
|
||||
|
||||
// Route to PyPI registry (also handles /simple prefix)
|
||||
if (!response && this.config.pypi?.enabled) {
|
||||
const pypiBasePath = this.config.pypi.basePath ?? '/pypi';
|
||||
if (path.startsWith(pypiBasePath) || path.startsWith('/simple')) {
|
||||
const pypiRegistry = this.registries.get('pypi');
|
||||
if (pypiRegistry) {
|
||||
response = await pypiRegistry.handleRequest(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Route to RubyGems registry
|
||||
if (!response && this.config.rubygems?.enabled && path.startsWith(this.config.rubygems.basePath)) {
|
||||
const rubygemsRegistry = this.registries.get('rubygems');
|
||||
if (rubygemsRegistry) {
|
||||
response = await rubygemsRegistry.handleRequest(context);
|
||||
const registry = this.registries.get(descriptor.protocol);
|
||||
if (registry) {
|
||||
response = await registry.handleRequest(context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,9 +303,7 @@ export class SmartRegistry {
|
||||
*/
|
||||
public destroy(): void {
|
||||
for (const registry of this.registries.values()) {
|
||||
if (typeof (registry as any).destroy === 'function') {
|
||||
(registry as any).destroy();
|
||||
}
|
||||
registry.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user