feat(upstream): Add dynamic per-request upstream provider and integrate into registries
This commit is contained in:
@@ -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 { NpmUpstream } from './classes.npmupstream.js';
|
||||
import type {
|
||||
IPackument,
|
||||
@@ -27,20 +27,21 @@ export class NpmRegistry extends BaseRegistry {
|
||||
private basePath: string = '/npm';
|
||||
private registryUrl: string;
|
||||
private logger: Smartlog;
|
||||
private upstream: NpmUpstream | null = null;
|
||||
private upstreamProvider: IUpstreamProvider | null = null;
|
||||
|
||||
constructor(
|
||||
storage: RegistryStorage,
|
||||
authManager: AuthManager,
|
||||
basePath: string = '/npm',
|
||||
registryUrl: string = 'http://localhost:5000/npm',
|
||||
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({
|
||||
@@ -55,15 +56,51 @@ export class NpmRegistry extends BaseRegistry {
|
||||
});
|
||||
this.logger.enableConsole();
|
||||
|
||||
// Initialize upstream if configured
|
||||
if (upstreamConfig?.enabled) {
|
||||
this.upstream = new NpmUpstream(upstreamConfig, registryUrl, this.logger);
|
||||
this.logger.log('info', 'NPM upstream initialized', {
|
||||
upstreams: upstreamConfig.upstreams.map(u => u.name),
|
||||
});
|
||||
if (upstreamProvider) {
|
||||
this.logger.log('info', 'NPM upstream provider configured');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract scope from npm package name.
|
||||
* @example "@company/utils" -> "company"
|
||||
* @example "lodash" -> null
|
||||
*/
|
||||
private extractScope(packageName: string): string | null {
|
||||
if (packageName.startsWith('@')) {
|
||||
const slashIndex = packageName.indexOf('/');
|
||||
if (slashIndex > 1) {
|
||||
return packageName.substring(1, 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<NpmUpstream | null> {
|
||||
if (!this.upstreamProvider) return null;
|
||||
|
||||
const config = await this.upstreamProvider.resolveUpstreamConfig({
|
||||
protocol: 'npm',
|
||||
resource,
|
||||
scope: this.extractScope(resource),
|
||||
actor,
|
||||
method,
|
||||
resourceType,
|
||||
});
|
||||
|
||||
if (!config?.enabled) return null;
|
||||
return new NpmUpstream(config, this.registryUrl, this.logger);
|
||||
}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
// NPM registry initialization
|
||||
}
|
||||
@@ -80,6 +117,14 @@ export class NpmRegistry extends BaseRegistry {
|
||||
const tokenString = authHeader?.replace(/^Bearer\s+/i, '');
|
||||
const token = tokenString ? await this.authManager.validateToken(tokenString, 'npm') : null;
|
||||
|
||||
// Build actor context for upstream resolution
|
||||
const actor: IRequestActor = {
|
||||
userId: token?.userId,
|
||||
ip: context.headers['x-forwarded-for'] || context.headers['x-real-ip'],
|
||||
userAgent: context.headers['user-agent'],
|
||||
...context.actor, // Include any pre-populated actor info
|
||||
};
|
||||
|
||||
this.logger.log('debug', `handleRequest: ${context.method} ${path}`, {
|
||||
method: context.method,
|
||||
path,
|
||||
@@ -118,7 +163,7 @@ export class NpmRegistry extends BaseRegistry {
|
||||
const tarballMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/-\/(.+\.tgz)$/);
|
||||
if (tarballMatch) {
|
||||
const [, packageName, filename] = tarballMatch;
|
||||
return this.handleTarballDownload(packageName, filename, token);
|
||||
return this.handleTarballDownload(packageName, filename, token, actor);
|
||||
}
|
||||
|
||||
// Unpublish specific version: DELETE /{package}/-/{version}
|
||||
@@ -142,7 +187,7 @@ export class NpmRegistry extends BaseRegistry {
|
||||
if (versionMatch) {
|
||||
const [, packageName, version] = versionMatch;
|
||||
this.logger.log('debug', 'versionMatch', { packageName, version });
|
||||
return this.handlePackageVersion(packageName, version, token);
|
||||
return this.handlePackageVersion(packageName, version, token, actor);
|
||||
}
|
||||
|
||||
// Package operations: /{package}
|
||||
@@ -150,7 +195,7 @@ export class NpmRegistry extends BaseRegistry {
|
||||
if (packageMatch) {
|
||||
const packageName = packageMatch[1];
|
||||
this.logger.log('debug', 'packageMatch', { packageName });
|
||||
return this.handlePackage(context.method, packageName, context.body, context.query, token);
|
||||
return this.handlePackage(context.method, packageName, context.body, context.query, token, actor);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -198,11 +243,12 @@ export class NpmRegistry extends BaseRegistry {
|
||||
packageName: string,
|
||||
body: any,
|
||||
query: Record<string, string>,
|
||||
token: IAuthToken | null
|
||||
token: IAuthToken | null,
|
||||
actor?: IRequestActor
|
||||
): Promise<IResponse> {
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
return this.getPackument(packageName, token, query);
|
||||
return this.getPackument(packageName, token, query, actor);
|
||||
case 'PUT':
|
||||
return this.publishPackage(packageName, body, token);
|
||||
case 'DELETE':
|
||||
@@ -219,7 +265,8 @@ export class NpmRegistry extends BaseRegistry {
|
||||
private async getPackument(
|
||||
packageName: string,
|
||||
token: IAuthToken | null,
|
||||
query: Record<string, string>
|
||||
query: Record<string, string>,
|
||||
actor?: IRequestActor
|
||||
): Promise<IResponse> {
|
||||
let packument = await this.storage.getNpmPackument(packageName);
|
||||
this.logger.log('debug', `getPackument: ${packageName}`, {
|
||||
@@ -229,17 +276,20 @@ export class NpmRegistry extends BaseRegistry {
|
||||
});
|
||||
|
||||
// If not found locally, try upstream
|
||||
if (!packument && this.upstream) {
|
||||
this.logger.log('debug', `getPackument: fetching from upstream`, { packageName });
|
||||
const upstreamPackument = await this.upstream.fetchPackument(packageName);
|
||||
if (upstreamPackument) {
|
||||
this.logger.log('debug', `getPackument: found in upstream`, {
|
||||
packageName,
|
||||
versions: Object.keys(upstreamPackument.versions || {}).length
|
||||
});
|
||||
packument = upstreamPackument;
|
||||
// Optionally cache the packument locally (without tarballs)
|
||||
// We don't store tarballs here - they'll be fetched on demand
|
||||
if (!packument) {
|
||||
const upstream = await this.getUpstreamForRequest(packageName, 'packument', 'GET', actor);
|
||||
if (upstream) {
|
||||
this.logger.log('debug', `getPackument: fetching from upstream`, { packageName });
|
||||
const upstreamPackument = await upstream.fetchPackument(packageName);
|
||||
if (upstreamPackument) {
|
||||
this.logger.log('debug', `getPackument: found in upstream`, {
|
||||
packageName,
|
||||
versions: Object.keys(upstreamPackument.versions || {}).length
|
||||
});
|
||||
packument = upstreamPackument;
|
||||
// Optionally cache the packument locally (without tarballs)
|
||||
// We don't store tarballs here - they'll be fetched on demand
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,7 +329,8 @@ export class NpmRegistry extends BaseRegistry {
|
||||
private async handlePackageVersion(
|
||||
packageName: string,
|
||||
version: string,
|
||||
token: IAuthToken | null
|
||||
token: IAuthToken | null,
|
||||
actor?: IRequestActor
|
||||
): Promise<IResponse> {
|
||||
this.logger.log('debug', 'handlePackageVersion', { packageName, version });
|
||||
let packument = await this.storage.getNpmPackument(packageName);
|
||||
@@ -289,11 +340,14 @@ export class NpmRegistry extends BaseRegistry {
|
||||
}
|
||||
|
||||
// If not found locally, try upstream
|
||||
if (!packument && this.upstream) {
|
||||
this.logger.log('debug', 'handlePackageVersion: fetching from upstream', { packageName });
|
||||
const upstreamPackument = await this.upstream.fetchPackument(packageName);
|
||||
if (upstreamPackument) {
|
||||
packument = upstreamPackument;
|
||||
if (!packument) {
|
||||
const upstream = await this.getUpstreamForRequest(packageName, 'packument', 'GET', actor);
|
||||
if (upstream) {
|
||||
this.logger.log('debug', 'handlePackageVersion: fetching from upstream', { packageName });
|
||||
const upstreamPackument = await upstream.fetchPackument(packageName);
|
||||
if (upstreamPackument) {
|
||||
packument = upstreamPackument;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -563,7 +617,8 @@ export class NpmRegistry extends BaseRegistry {
|
||||
private async handleTarballDownload(
|
||||
packageName: string,
|
||||
filename: string,
|
||||
token: IAuthToken | null
|
||||
token: IAuthToken | null,
|
||||
actor?: IRequestActor
|
||||
): Promise<IResponse> {
|
||||
// Extract version from filename: package-name-1.0.0.tgz
|
||||
const versionMatch = filename.match(/-([\d.]+(?:-[a-z0-9.]+)?)\.tgz$/i);
|
||||
@@ -579,21 +634,24 @@ export class NpmRegistry extends BaseRegistry {
|
||||
let tarball = await this.storage.getNpmTarball(packageName, version);
|
||||
|
||||
// If not found locally, try upstream
|
||||
if (!tarball && this.upstream) {
|
||||
this.logger.log('debug', 'handleTarballDownload: fetching from upstream', {
|
||||
packageName,
|
||||
version,
|
||||
});
|
||||
const upstreamTarball = await this.upstream.fetchTarball(packageName, version);
|
||||
if (upstreamTarball) {
|
||||
tarball = upstreamTarball;
|
||||
// Cache the tarball locally for future requests
|
||||
await this.storage.putNpmTarball(packageName, version, tarball);
|
||||
this.logger.log('debug', 'handleTarballDownload: cached tarball locally', {
|
||||
if (!tarball) {
|
||||
const upstream = await this.getUpstreamForRequest(packageName, 'tarball', 'GET', actor);
|
||||
if (upstream) {
|
||||
this.logger.log('debug', 'handleTarballDownload: fetching from upstream', {
|
||||
packageName,
|
||||
version,
|
||||
size: tarball.length,
|
||||
});
|
||||
const upstreamTarball = await upstream.fetchTarball(packageName, version);
|
||||
if (upstreamTarball) {
|
||||
tarball = upstreamTarball;
|
||||
// Cache the tarball locally for future requests
|
||||
await this.storage.putNpmTarball(packageName, version, tarball);
|
||||
this.logger.log('debug', 'handleTarballDownload: cached tarball locally', {
|
||||
packageName,
|
||||
version,
|
||||
size: tarball.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user