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

@@ -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 { toBuffer } from '../core/helpers.buffer.js';
import type { IMavenCoordinate, IMavenMetadata, IChecksums } from './interfaces.maven.js';
import {
@@ -33,34 +33,64 @@ export class MavenRegistry extends BaseRegistry {
private authManager: AuthManager;
private basePath: string = '/maven';
private registryUrl: string;
private upstream: MavenUpstream | null = null;
private upstreamProvider: IUpstreamProvider | null = null;
constructor(
storage: RegistryStorage,
authManager: AuthManager,
basePath: string,
registryUrl: string,
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 MavenUpstream(upstreamConfig);
}
/**
* Extract scope from Maven coordinates.
* For Maven, the groupId is the scope.
* @example "com.example" from "com.example:my-lib"
*/
private extractScope(groupId: string): string | null {
return groupId || 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<MavenUpstream | null> {
if (!this.upstreamProvider) return null;
// For Maven, resource is "groupId:artifactId"
const [groupId] = resource.split(':');
const config = await this.upstreamProvider.resolveUpstreamConfig({
protocol: 'maven',
resource,
scope: this.extractScope(groupId),
actor,
method,
resourceType,
});
if (!config?.enabled) return null;
return new MavenUpstream(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> {
@@ -85,13 +115,21 @@ export class MavenRegistry extends BaseRegistry {
token = await this.authManager.validateToken(tokenString, 'maven');
}
// 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'],
};
// Parse path to determine request type
const coordinate = pathToGAV(path);
if (!coordinate) {
// Not a valid artifact path, could be metadata or root
if (path.endsWith('/maven-metadata.xml')) {
return this.handleMetadataRequest(context.method, path, token);
return this.handleMetadataRequest(context.method, path, token, actor);
}
return {
@@ -108,7 +146,7 @@ export class MavenRegistry extends BaseRegistry {
}
// Handle artifact requests (JAR, POM, WAR, etc.)
return this.handleArtifactRequest(context.method, coordinate, token, context.body);
return this.handleArtifactRequest(context.method, coordinate, token, context.body, actor);
}
protected async checkPermission(
@@ -128,7 +166,8 @@ export class MavenRegistry extends BaseRegistry {
method: string,
coordinate: IMavenCoordinate,
token: IAuthToken | null,
body?: Buffer | any
body?: Buffer | any,
actor?: IRequestActor
): Promise<IResponse> {
const { groupId, artifactId, version } = coordinate;
const filename = buildFilename(coordinate);
@@ -139,7 +178,7 @@ export class MavenRegistry extends BaseRegistry {
case 'HEAD':
// Maven repositories typically allow anonymous reads
return method === 'GET'
? this.getArtifact(groupId, artifactId, version, filename)
? this.getArtifact(groupId, artifactId, version, filename, actor)
: this.headArtifact(groupId, artifactId, version, filename);
case 'PUT':
@@ -211,7 +250,8 @@ export class MavenRegistry extends BaseRegistry {
private async handleMetadataRequest(
method: string,
path: string,
token: IAuthToken | null
token: IAuthToken | null,
actor?: IRequestActor
): Promise<IResponse> {
// Parse path to extract groupId and artifactId
// Path format: /com/example/my-lib/maven-metadata.xml
@@ -232,7 +272,7 @@ export class MavenRegistry extends BaseRegistry {
if (method === 'GET') {
// Metadata is usually public (read permission optional)
// Some registries allow anonymous metadata access
return this.getMetadata(groupId, artifactId);
return this.getMetadata(groupId, artifactId, actor);
}
return {
@@ -250,22 +290,27 @@ export class MavenRegistry extends BaseRegistry {
groupId: string,
artifactId: string,
version: string,
filename: string
filename: string,
actor?: IRequestActor
): Promise<IResponse> {
let data = await this.storage.getMavenArtifact(groupId, artifactId, version, filename);
// Try upstream if not found locally
if (!data && this.upstream) {
// Parse the filename to extract extension and classifier
const { extension, classifier } = this.parseFilename(filename, artifactId, version);
if (extension) {
data = await this.upstream.fetchArtifact(groupId, artifactId, version, extension, classifier);
if (data) {
// Cache the artifact locally
await this.storage.putMavenArtifact(groupId, artifactId, version, filename, data);
// Generate and store checksums
const checksums = await calculateChecksums(data);
await this.storeChecksums(groupId, artifactId, version, filename, checksums);
if (!data) {
const resource = `${groupId}:${artifactId}`;
const upstream = await this.getUpstreamForRequest(resource, 'artifact', 'GET', actor);
if (upstream) {
// Parse the filename to extract extension and classifier
const { extension, classifier } = this.parseFilename(filename, artifactId, version);
if (extension) {
data = await upstream.fetchArtifact(groupId, artifactId, version, extension, classifier);
if (data) {
// Cache the artifact locally
await this.storage.putMavenArtifact(groupId, artifactId, version, filename, data);
// Generate and store checksums
const checksums = await calculateChecksums(data);
await this.storeChecksums(groupId, artifactId, version, filename, checksums);
}
}
}
}
@@ -495,16 +540,20 @@ export class MavenRegistry extends BaseRegistry {
// METADATA OPERATIONS
// ========================================================================
private async getMetadata(groupId: string, artifactId: string): Promise<IResponse> {
private async getMetadata(groupId: string, artifactId: string, actor?: IRequestActor): Promise<IResponse> {
let metadataBuffer = await this.storage.getMavenMetadata(groupId, artifactId);
// Try upstream if not found locally
if (!metadataBuffer && this.upstream) {
const upstreamMetadata = await this.upstream.fetchMetadata(groupId, artifactId);
if (upstreamMetadata) {
metadataBuffer = Buffer.from(upstreamMetadata, 'utf-8');
// Cache the metadata locally
await this.storage.putMavenMetadata(groupId, artifactId, metadataBuffer);
if (!metadataBuffer) {
const resource = `${groupId}:${artifactId}`;
const upstream = await this.getUpstreamForRequest(resource, 'metadata', 'GET', actor);
if (upstream) {
const upstreamMetadata = await upstream.fetchMetadata(groupId, artifactId);
if (upstreamMetadata) {
metadataBuffer = Buffer.from(upstreamMetadata, 'utf-8');
// Cache the metadata locally
await this.storage.putMavenMetadata(groupId, artifactId, metadataBuffer);
}
}
}