feat(upstream): Add dynamic per-request upstream provider and integrate into registries
This commit is contained in:
@@ -6,8 +6,8 @@
|
||||
import { BaseRegistry } from '../core/classes.baseregistry.js';
|
||||
import type { RegistryStorage } from '../core/classes.registrystorage.js';
|
||||
import type { AuthManager } from '../core/classes.authmanager.js';
|
||||
import type { IRequestContext, IResponse, IAuthToken } from '../core/interfaces.core.js';
|
||||
import type { IProtocolUpstreamConfig } from '../upstream/interfaces.upstream.js';
|
||||
import type { IRequestContext, IResponse, IAuthToken, IRequestActor } from '../core/interfaces.core.js';
|
||||
import type { IUpstreamProvider } from '../upstream/interfaces.upstream.js';
|
||||
import { isBinaryData, toBuffer } from '../core/helpers.buffer.js';
|
||||
import type {
|
||||
IComposerPackage,
|
||||
@@ -30,34 +30,66 @@ export class ComposerRegistry extends BaseRegistry {
|
||||
private authManager: AuthManager;
|
||||
private basePath: string = '/composer';
|
||||
private registryUrl: string;
|
||||
private upstream: ComposerUpstream | null = null;
|
||||
private upstreamProvider: IUpstreamProvider | null = null;
|
||||
|
||||
constructor(
|
||||
storage: RegistryStorage,
|
||||
authManager: AuthManager,
|
||||
basePath: string = '/composer',
|
||||
registryUrl: string = 'http://localhost:5000/composer',
|
||||
upstreamConfig?: IProtocolUpstreamConfig
|
||||
upstreamProvider?: IUpstreamProvider
|
||||
) {
|
||||
super();
|
||||
this.storage = storage;
|
||||
this.authManager = authManager;
|
||||
this.basePath = basePath;
|
||||
this.registryUrl = registryUrl;
|
||||
this.upstreamProvider = upstreamProvider || null;
|
||||
}
|
||||
|
||||
// Initialize upstream if configured
|
||||
if (upstreamConfig?.enabled) {
|
||||
this.upstream = new ComposerUpstream(upstreamConfig);
|
||||
/**
|
||||
* Extract scope from Composer package name.
|
||||
* For Composer, vendor is the scope.
|
||||
* @example "symfony" from "symfony/console"
|
||||
*/
|
||||
private extractScope(vendorPackage: string): string | null {
|
||||
const slashIndex = vendorPackage.indexOf('/');
|
||||
if (slashIndex > 0) {
|
||||
return vendorPackage.substring(0, slashIndex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get upstream for a specific request.
|
||||
* Calls the provider to resolve upstream config dynamically.
|
||||
*/
|
||||
private async getUpstreamForRequest(
|
||||
resource: string,
|
||||
resourceType: string,
|
||||
method: string,
|
||||
actor?: IRequestActor
|
||||
): Promise<ComposerUpstream | null> {
|
||||
if (!this.upstreamProvider) return null;
|
||||
|
||||
const config = await this.upstreamProvider.resolveUpstreamConfig({
|
||||
protocol: 'composer',
|
||||
resource,
|
||||
scope: this.extractScope(resource),
|
||||
actor,
|
||||
method,
|
||||
resourceType,
|
||||
});
|
||||
|
||||
if (!config?.enabled) return null;
|
||||
return new ComposerUpstream(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources (timers, connections, etc.)
|
||||
*/
|
||||
public destroy(): void {
|
||||
if (this.upstream) {
|
||||
this.upstream.stop();
|
||||
}
|
||||
// No persistent upstream to clean up with dynamic provider
|
||||
}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
@@ -96,6 +128,14 @@ export class ComposerRegistry extends BaseRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
// Build actor from context and validated token
|
||||
const actor: IRequestActor = {
|
||||
...context.actor,
|
||||
userId: token?.userId,
|
||||
ip: context.headers['x-forwarded-for'] || context.headers['X-Forwarded-For'],
|
||||
userAgent: context.headers['user-agent'] || context.headers['User-Agent'],
|
||||
};
|
||||
|
||||
// Root packages.json
|
||||
if (path === '/packages.json' || path === '' || path === '/') {
|
||||
return this.handlePackagesJson();
|
||||
@@ -106,7 +146,7 @@ export class ComposerRegistry extends BaseRegistry {
|
||||
if (metadataMatch) {
|
||||
const [, vendorPackage, devSuffix] = metadataMatch;
|
||||
const includeDev = !!devSuffix;
|
||||
return this.handlePackageMetadata(vendorPackage, includeDev, token);
|
||||
return this.handlePackageMetadata(vendorPackage, includeDev, token, actor);
|
||||
}
|
||||
|
||||
// Package list: /packages/list.json?filter=vendor/*
|
||||
@@ -176,26 +216,30 @@ export class ComposerRegistry extends BaseRegistry {
|
||||
private async handlePackageMetadata(
|
||||
vendorPackage: string,
|
||||
includeDev: boolean,
|
||||
token: IAuthToken | null
|
||||
token: IAuthToken | null,
|
||||
actor?: IRequestActor
|
||||
): Promise<IResponse> {
|
||||
// Read operations are public, no authentication required
|
||||
let metadata = await this.storage.getComposerPackageMetadata(vendorPackage);
|
||||
|
||||
// Try upstream if not found locally
|
||||
if (!metadata && this.upstream) {
|
||||
const [vendor, packageName] = vendorPackage.split('/');
|
||||
if (vendor && packageName) {
|
||||
const upstreamMetadata = includeDev
|
||||
? await this.upstream.fetchPackageDevMetadata(vendor, packageName)
|
||||
: await this.upstream.fetchPackageMetadata(vendor, packageName);
|
||||
if (!metadata) {
|
||||
const upstream = await this.getUpstreamForRequest(vendorPackage, 'metadata', 'GET', actor);
|
||||
if (upstream) {
|
||||
const [vendor, packageName] = vendorPackage.split('/');
|
||||
if (vendor && packageName) {
|
||||
const upstreamMetadata = includeDev
|
||||
? await upstream.fetchPackageDevMetadata(vendor, packageName)
|
||||
: await upstream.fetchPackageMetadata(vendor, packageName);
|
||||
|
||||
if (upstreamMetadata && upstreamMetadata.packages) {
|
||||
// Store upstream metadata locally
|
||||
metadata = {
|
||||
packages: upstreamMetadata.packages,
|
||||
lastModified: new Date().toUTCString(),
|
||||
};
|
||||
await this.storage.putComposerPackageMetadata(vendorPackage, metadata);
|
||||
if (upstreamMetadata && upstreamMetadata.packages) {
|
||||
// Store upstream metadata locally
|
||||
metadata = {
|
||||
packages: upstreamMetadata.packages,
|
||||
lastModified: new Date().toUTCString(),
|
||||
};
|
||||
await this.storage.putComposerPackageMetadata(vendorPackage, metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user