feat(registry): add declarative protocol routing and request-scoped storage hook context across registries
This commit is contained in:
@@ -1,14 +1,131 @@
|
||||
import type { IRequestContext, IResponse, IAuthToken } from './interfaces.core.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
import type { IRequestContext, IResponse, IAuthToken, IRequestActor } from './interfaces.core.js';
|
||||
|
||||
/**
|
||||
* Abstract base class for all registry protocol implementations
|
||||
*/
|
||||
export abstract class BaseRegistry {
|
||||
protected getHeader(contextOrHeaders: IRequestContext | Record<string, string>, name: string): string | undefined {
|
||||
const headers = 'headers' in contextOrHeaders ? contextOrHeaders.headers : contextOrHeaders;
|
||||
if (headers[name] !== undefined) {
|
||||
return headers[name];
|
||||
}
|
||||
|
||||
const lowerName = name.toLowerCase();
|
||||
for (const [headerName, value] of Object.entries(headers)) {
|
||||
if (headerName.toLowerCase() === lowerName) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected getAuthorizationHeader(context: IRequestContext): string | undefined {
|
||||
return this.getHeader(context, 'authorization');
|
||||
}
|
||||
|
||||
protected getClientIp(context: IRequestContext): string | undefined {
|
||||
const forwardedFor = this.getHeader(context, 'x-forwarded-for');
|
||||
if (forwardedFor) {
|
||||
return forwardedFor.split(',')[0]?.trim();
|
||||
}
|
||||
|
||||
return this.getHeader(context, 'x-real-ip');
|
||||
}
|
||||
|
||||
protected getUserAgent(context: IRequestContext): string | undefined {
|
||||
return this.getHeader(context, 'user-agent');
|
||||
}
|
||||
|
||||
protected extractBearerToken(contextOrHeader: IRequestContext | string | undefined): string | null {
|
||||
const authHeader = typeof contextOrHeader === 'string'
|
||||
? contextOrHeader
|
||||
: contextOrHeader
|
||||
? this.getAuthorizationHeader(contextOrHeader)
|
||||
: undefined;
|
||||
|
||||
if (!authHeader || !/^Bearer\s+/i.test(authHeader)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return authHeader.replace(/^Bearer\s+/i, '');
|
||||
}
|
||||
|
||||
protected parseBasicAuthHeader(authHeader: string | undefined): { username: string; password: string } | null {
|
||||
if (!authHeader || !/^Basic\s+/i.test(authHeader)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const base64 = authHeader.replace(/^Basic\s+/i, '');
|
||||
const decoded = Buffer.from(base64, 'base64').toString('utf-8');
|
||||
const separatorIndex = decoded.indexOf(':');
|
||||
|
||||
if (separatorIndex < 0) {
|
||||
return {
|
||||
username: decoded,
|
||||
password: '',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
username: decoded.substring(0, separatorIndex),
|
||||
password: decoded.substring(separatorIndex + 1),
|
||||
};
|
||||
}
|
||||
|
||||
protected buildRequestActor(context: IRequestContext, token: IAuthToken | null): IRequestActor {
|
||||
const actor: IRequestActor = {
|
||||
...(context.actor ?? {}),
|
||||
};
|
||||
|
||||
if (token?.userId) {
|
||||
actor.userId = token.userId;
|
||||
}
|
||||
|
||||
const ip = this.getClientIp(context);
|
||||
if (ip) {
|
||||
actor.ip = ip;
|
||||
}
|
||||
|
||||
const userAgent = this.getUserAgent(context);
|
||||
if (userAgent) {
|
||||
actor.userAgent = userAgent;
|
||||
}
|
||||
|
||||
return actor;
|
||||
}
|
||||
|
||||
protected createProtocolLogger(
|
||||
containerName: string,
|
||||
zone: string
|
||||
): plugins.smartlog.Smartlog {
|
||||
const logger = new plugins.smartlog.Smartlog({
|
||||
logContext: {
|
||||
company: 'push.rocks',
|
||||
companyunit: 'smartregistry',
|
||||
containerName,
|
||||
environment: (process.env.NODE_ENV as any) || 'development',
|
||||
runtime: 'node',
|
||||
zone,
|
||||
}
|
||||
});
|
||||
logger.enableConsole();
|
||||
return logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the registry
|
||||
*/
|
||||
abstract init(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Clean up timers, connections, and other registry resources.
|
||||
*/
|
||||
public destroy(): void {
|
||||
// Default no-op for registries without persistent resources.
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming HTTP request
|
||||
* @param context - Request context
|
||||
|
||||
+168
-322
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,109 @@
|
||||
function digestToHash(digest: string): string {
|
||||
return digest.split(':')[1];
|
||||
}
|
||||
|
||||
export function getOciBlobPath(digest: string): string {
|
||||
return `oci/blobs/sha256/${digestToHash(digest)}`;
|
||||
}
|
||||
|
||||
export function getOciManifestPath(repository: string, digest: string): string {
|
||||
return `oci/manifests/${repository}/${digestToHash(digest)}`;
|
||||
}
|
||||
|
||||
export function getNpmPackumentPath(packageName: string): string {
|
||||
return `npm/packages/${packageName}/index.json`;
|
||||
}
|
||||
|
||||
export function getNpmTarballPath(packageName: string, version: string): string {
|
||||
const safeName = packageName.replace('@', '').replace('/', '-');
|
||||
return `npm/packages/${packageName}/${safeName}-${version}.tgz`;
|
||||
}
|
||||
|
||||
export function getMavenArtifactPath(
|
||||
groupId: string,
|
||||
artifactId: string,
|
||||
version: string,
|
||||
filename: string
|
||||
): string {
|
||||
const groupPath = groupId.replace(/\./g, '/');
|
||||
return `maven/artifacts/${groupPath}/${artifactId}/${version}/${filename}`;
|
||||
}
|
||||
|
||||
export function getMavenMetadataPath(groupId: string, artifactId: string): string {
|
||||
const groupPath = groupId.replace(/\./g, '/');
|
||||
return `maven/metadata/${groupPath}/${artifactId}/maven-metadata.xml`;
|
||||
}
|
||||
|
||||
export function getCargoConfigPath(): string {
|
||||
return 'cargo/config.json';
|
||||
}
|
||||
|
||||
export function getCargoIndexPath(crateName: string): string {
|
||||
const lower = crateName.toLowerCase();
|
||||
const len = lower.length;
|
||||
|
||||
if (len === 1) {
|
||||
return `cargo/index/1/${lower}`;
|
||||
}
|
||||
|
||||
if (len === 2) {
|
||||
return `cargo/index/2/${lower}`;
|
||||
}
|
||||
|
||||
if (len === 3) {
|
||||
return `cargo/index/3/${lower.charAt(0)}/${lower}`;
|
||||
}
|
||||
|
||||
const prefix1 = lower.substring(0, 2);
|
||||
const prefix2 = lower.substring(2, 4);
|
||||
return `cargo/index/${prefix1}/${prefix2}/${lower}`;
|
||||
}
|
||||
|
||||
export function getCargoCratePath(crateName: string, version: string): string {
|
||||
return `cargo/crates/${crateName}/${crateName}-${version}.crate`;
|
||||
}
|
||||
|
||||
export function getComposerMetadataPath(vendorPackage: string): string {
|
||||
return `composer/packages/${vendorPackage}/metadata.json`;
|
||||
}
|
||||
|
||||
export function getComposerZipPath(vendorPackage: string, reference: string): string {
|
||||
return `composer/packages/${vendorPackage}/${reference}.zip`;
|
||||
}
|
||||
|
||||
export function getPypiMetadataPath(packageName: string): string {
|
||||
return `pypi/metadata/${packageName}/metadata.json`;
|
||||
}
|
||||
|
||||
export function getPypiSimpleIndexPath(packageName: string): string {
|
||||
return `pypi/simple/${packageName}/index.html`;
|
||||
}
|
||||
|
||||
export function getPypiSimpleRootIndexPath(): string {
|
||||
return 'pypi/simple/index.html';
|
||||
}
|
||||
|
||||
export function getPypiPackageFilePath(packageName: string, filename: string): string {
|
||||
return `pypi/packages/${packageName}/${filename}`;
|
||||
}
|
||||
|
||||
export function getRubyGemsVersionsPath(): string {
|
||||
return 'rubygems/versions';
|
||||
}
|
||||
|
||||
export function getRubyGemsInfoPath(gemName: string): string {
|
||||
return `rubygems/info/${gemName}`;
|
||||
}
|
||||
|
||||
export function getRubyGemsNamesPath(): string {
|
||||
return 'rubygems/names';
|
||||
}
|
||||
|
||||
export function getRubyGemsGemPath(gemName: string, version: string, platform?: string): string {
|
||||
const filename = platform ? `${gemName}-${version}-${platform}.gem` : `${gemName}-${version}.gem`;
|
||||
return `rubygems/gems/${filename}`;
|
||||
}
|
||||
|
||||
export function getRubyGemsMetadataPath(gemName: string): string {
|
||||
return `rubygems/metadata/${gemName}/metadata.json`;
|
||||
}
|
||||
Reference in New Issue
Block a user