diff --git a/changelog.md b/changelog.md index 2358197..2a5db55 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2025-11-25 - 2.1.0 - feat(oci) +Support configurable OCI token realm/service and centralize unauthorized responses + +- SmartRegistry now forwards optional ociTokens (realm and service) from auth configuration to OciRegistry when OCI is enabled +- OciRegistry constructor accepts an optional ociTokens parameter and stores it for use in auth headers +- Replaced repeated construction of WWW-Authenticate headers with createUnauthorizedResponse and createUnauthorizedHeadResponse helpers that use configured realm/service +- Behavior is backwards-compatible: when ociTokens are not configured the registry falls back to the previous defaults (realm: /v2/token, service: "registry") + ## 2025-11-25 - 2.0.0 - BREAKING CHANGE(pypi,rubygems) Revise PyPI and RubyGems handling: normalize error payloads, fix .gem parsing/packing, adjust PyPI JSON API and tests, and export smartarchive plugin diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 8d94b15..1095f69 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartregistry', - version: '2.0.0', + version: '2.1.0', description: 'A composable TypeScript library implementing OCI, NPM, Maven, Cargo, Composer, PyPI, and RubyGems registries for building unified container and package registries' } diff --git a/ts/classes.smartregistry.ts b/ts/classes.smartregistry.ts index 81be292..4998963 100644 --- a/ts/classes.smartregistry.ts +++ b/ts/classes.smartregistry.ts @@ -42,7 +42,11 @@ export class SmartRegistry { // Initialize OCI registry if enabled if (this.config.oci?.enabled) { const ociBasePath = this.config.oci.basePath ?? '/oci'; - const ociRegistry = new OciRegistry(this.storage, this.authManager, ociBasePath); + const ociTokens = this.config.auth.ociTokens?.enabled ? { + realm: this.config.auth.ociTokens.realm, + service: this.config.auth.ociTokens.service, + } : undefined; + const ociRegistry = new OciRegistry(this.storage, this.authManager, ociBasePath, ociTokens); await ociRegistry.init(); this.registries.set('oci', ociRegistry); } diff --git a/ts/oci/classes.ociregistry.ts b/ts/oci/classes.ociregistry.ts index 8466d46..e7d7e49 100644 --- a/ts/oci/classes.ociregistry.ts +++ b/ts/oci/classes.ociregistry.ts @@ -20,12 +20,19 @@ export class OciRegistry extends BaseRegistry { private uploadSessions: Map = new Map(); private basePath: string = '/oci'; private cleanupInterval?: NodeJS.Timeout; + private ociTokens?: { realm: string; service: string }; - constructor(storage: RegistryStorage, authManager: AuthManager, basePath: string = '/oci') { + constructor( + storage: RegistryStorage, + authManager: AuthManager, + basePath: string = '/oci', + ociTokens?: { realm: string; service: string } + ) { super(); this.storage = storage; this.authManager = authManager; this.basePath = basePath; + this.ociTokens = ociTokens; } public async init(): Promise { @@ -280,13 +287,7 @@ export class OciRegistry extends BaseRegistry { headers?: Record ): Promise { if (!await this.checkPermission(token, repository, 'pull')) { - return { - status: 401, - headers: { - 'WWW-Authenticate': `Bearer realm="${this.basePath}/v2/token",service="registry",scope="repository:${repository}:pull"`, - }, - body: this.createError('DENIED', 'Insufficient permissions'), - }; + return this.createUnauthorizedResponse(repository, 'pull'); } // Resolve tag to digest if needed @@ -367,13 +368,7 @@ export class OciRegistry extends BaseRegistry { headers?: Record ): Promise { if (!await this.checkPermission(token, repository, 'push')) { - return { - status: 401, - headers: { - 'WWW-Authenticate': `Bearer realm="${this.basePath}/v2/token",service="registry",scope="repository:${repository}:push"`, - }, - body: this.createError('DENIED', 'Insufficient permissions'), - }; + return this.createUnauthorizedResponse(repository, 'push'); } if (!body) { @@ -685,10 +680,12 @@ export class OciRegistry extends BaseRegistry { * Per OCI Distribution Spec, 401 responses MUST include WWW-Authenticate header. */ private createUnauthorizedResponse(repository: string, action: string): IResponse { + const realm = this.ociTokens?.realm || `${this.basePath}/v2/token`; + const service = this.ociTokens?.service || 'registry'; return { status: 401, headers: { - 'WWW-Authenticate': `Bearer realm="${this.basePath}/v2/token",service="registry",scope="repository:${repository}:${action}"`, + 'WWW-Authenticate': `Bearer realm="${realm}",service="${service}",scope="repository:${repository}:${action}"`, }, body: this.createError('DENIED', 'Insufficient permissions'), }; @@ -698,10 +695,12 @@ export class OciRegistry extends BaseRegistry { * Create an unauthorized HEAD response (no body per HTTP spec). */ private createUnauthorizedHeadResponse(repository: string, action: string): IResponse { + const realm = this.ociTokens?.realm || `${this.basePath}/v2/token`; + const service = this.ociTokens?.service || 'registry'; return { status: 401, headers: { - 'WWW-Authenticate': `Bearer realm="${this.basePath}/v2/token",service="registry",scope="repository:${repository}:${action}"`, + 'WWW-Authenticate': `Bearer realm="${realm}",service="${service}",scope="repository:${repository}:${action}"`, }, body: null, };