feat(upstream): Add dynamic per-request upstream provider and integrate into registries

This commit is contained in:
2025-12-03 22:16:40 +00:00
parent 351680159b
commit e9af3f8328
14 changed files with 1117 additions and 287 deletions

View File

@@ -2,8 +2,8 @@ import { Smartlog } from '@push.rocks/smartlog';
import { BaseRegistry } from '../core/classes.baseregistry.js';
import { RegistryStorage } from '../core/classes.registrystorage.js';
import { 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 type {
ICargoIndexEntry,
ICargoPublishMetadata,
@@ -27,20 +27,21 @@ export class CargoRegistry extends BaseRegistry {
private basePath: string = '/cargo';
private registryUrl: string;
private logger: Smartlog;
private upstream: CargoUpstream | null = null;
private upstreamProvider: IUpstreamProvider | null = null;
constructor(
storage: RegistryStorage,
authManager: AuthManager,
basePath: string = '/cargo',
registryUrl: string = 'http://localhost:5000/cargo',
upstreamConfig?: IProtocolUpstreamConfig
upstreamProvider?: IUpstreamProvider
) {
super();
this.storage = storage;
this.authManager = authManager;
this.basePath = basePath;
this.registryUrl = registryUrl;
this.upstreamProvider = upstreamProvider || null;
// Initialize logger
this.logger = new Smartlog({
@@ -54,20 +55,38 @@ export class CargoRegistry extends BaseRegistry {
}
});
this.logger.enableConsole();
}
// Initialize upstream if configured
if (upstreamConfig?.enabled) {
this.upstream = new CargoUpstream(upstreamConfig, undefined, this.logger);
}
/**
* 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<CargoUpstream | null> {
if (!this.upstreamProvider) return null;
const config = await this.upstreamProvider.resolveUpstreamConfig({
protocol: 'cargo',
resource,
scope: resource, // For Cargo, crate name is the scope
actor,
method,
resourceType,
});
if (!config?.enabled) return null;
return new CargoUpstream(config, undefined, this.logger);
}
/**
* 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> {
@@ -94,6 +113,14 @@ export class CargoRegistry extends BaseRegistry {
const authHeader = context.headers['authorization'] || context.headers['Authorization'];
const token = authHeader ? await this.authManager.validateToken(authHeader, 'cargo') : null;
// 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'],
};
this.logger.log('debug', `handleRequest: ${context.method} ${path}`, {
method: context.method,
path,
@@ -107,11 +134,11 @@ export class CargoRegistry extends BaseRegistry {
// API endpoints
if (path.startsWith('/api/v1/')) {
return this.handleApiRequest(path, context, token);
return this.handleApiRequest(path, context, token, actor);
}
// Index files (sparse protocol)
return this.handleIndexRequest(path);
return this.handleIndexRequest(path, actor);
}
/**
@@ -132,7 +159,8 @@ export class CargoRegistry extends BaseRegistry {
private async handleApiRequest(
path: string,
context: IRequestContext,
token: IAuthToken | null
token: IAuthToken | null,
actor?: IRequestActor
): Promise<IResponse> {
// Publish: PUT /api/v1/crates/new
if (path === '/api/v1/crates/new' && context.method === 'PUT') {
@@ -142,7 +170,7 @@ export class CargoRegistry extends BaseRegistry {
// Download: GET /api/v1/crates/{crate}/{version}/download
const downloadMatch = path.match(/^\/api\/v1\/crates\/([^\/]+)\/([^\/]+)\/download$/);
if (downloadMatch && context.method === 'GET') {
return this.handleDownload(downloadMatch[1], downloadMatch[2]);
return this.handleDownload(downloadMatch[1], downloadMatch[2], actor);
}
// Yank: DELETE /api/v1/crates/{crate}/{version}/yank
@@ -175,7 +203,7 @@ export class CargoRegistry extends BaseRegistry {
* Handle index file requests
* Paths: /1/{name}, /2/{name}, /3/{c}/{name}, /{p1}/{p2}/{name}
*/
private async handleIndexRequest(path: string): Promise<IResponse> {
private async handleIndexRequest(path: string, actor?: IRequestActor): Promise<IResponse> {
// Parse index paths to extract crate name
const pathParts = path.split('/').filter(p => p);
let crateName: string | null = null;
@@ -202,7 +230,7 @@ export class CargoRegistry extends BaseRegistry {
};
}
return this.handleIndexFile(crateName);
return this.handleIndexFile(crateName, actor);
}
/**
@@ -224,23 +252,26 @@ export class CargoRegistry extends BaseRegistry {
/**
* Serve index file for a crate
*/
private async handleIndexFile(crateName: string): Promise<IResponse> {
private async handleIndexFile(crateName: string, actor?: IRequestActor): Promise<IResponse> {
let index = await this.storage.getCargoIndex(crateName);
// Try upstream if not found locally
if ((!index || index.length === 0) && this.upstream) {
const upstreamIndex = await this.upstream.fetchCrateIndex(crateName);
if (upstreamIndex) {
// Parse the newline-delimited JSON
const parsedIndex: ICargoIndexEntry[] = upstreamIndex
.split('\n')
.filter(line => line.trim())
.map(line => JSON.parse(line));
if (!index || index.length === 0) {
const upstream = await this.getUpstreamForRequest(crateName, 'index', 'GET', actor);
if (upstream) {
const upstreamIndex = await upstream.fetchCrateIndex(crateName);
if (upstreamIndex) {
// Parse the newline-delimited JSON
const parsedIndex: ICargoIndexEntry[] = upstreamIndex
.split('\n')
.filter(line => line.trim())
.map(line => JSON.parse(line));
if (parsedIndex.length > 0) {
// Cache locally
await this.storage.putCargoIndex(crateName, parsedIndex);
index = parsedIndex;
if (parsedIndex.length > 0) {
// Cache locally
await this.storage.putCargoIndex(crateName, parsedIndex);
index = parsedIndex;
}
}
}
}
@@ -431,18 +462,22 @@ export class CargoRegistry extends BaseRegistry {
*/
private async handleDownload(
crateName: string,
version: string
version: string,
actor?: IRequestActor
): Promise<IResponse> {
this.logger.log('debug', 'handleDownload', { crate: crateName, version });
let crateFile = await this.storage.getCargoCrate(crateName, version);
// Try upstream if not found locally
if (!crateFile && this.upstream) {
crateFile = await this.upstream.fetchCrate(crateName, version);
if (crateFile) {
// Cache locally
await this.storage.putCargoCrate(crateName, version, crateFile);
if (!crateFile) {
const upstream = await this.getUpstreamForRequest(crateName, 'crate', 'GET', actor);
if (upstream) {
crateFile = await upstream.fetchCrate(crateName, version);
if (crateFile) {
// Cache locally
await this.storage.putCargoCrate(crateName, version, crateFile);
}
}
}