import * as plugins from '../plugins.js'; import { BaseUpstream } from '../upstream/classes.baseupstream.js'; import type { IProtocolUpstreamConfig, IUpstreamFetchContext, IUpstreamResult, IUpstreamRegistryConfig, } from '../upstream/interfaces.upstream.js'; import type { IOciManifest, IOciImageIndex, ITagList } from './interfaces.oci.js'; /** * OCI-specific upstream implementation. * * Handles: * - Manifest fetching (image manifests and index manifests) * - Blob proxying (layers, configs) * - Tag list fetching * - Content-addressable caching (blobs are immutable) * - Docker Hub authentication flow */ export class OciUpstream extends BaseUpstream { protected readonly protocolName = 'oci'; /** Local registry base path for URL building */ private readonly localBasePath: string; constructor( config: IProtocolUpstreamConfig, localBasePath: string = '/oci', logger?: plugins.smartlog.Smartlog, ) { super(config, logger); this.localBasePath = localBasePath; } /** * Fetch a manifest from upstream registries. */ public async fetchManifest( repository: string, reference: string, ): Promise<{ manifest: IOciManifest | IOciImageIndex; contentType: string; digest: string } | null> { const context: IUpstreamFetchContext = { protocol: 'oci', resource: repository, resourceType: 'manifest', path: `/v2/${repository}/manifests/${reference}`, method: 'GET', headers: { 'accept': [ 'application/vnd.oci.image.manifest.v1+json', 'application/vnd.oci.image.index.v1+json', 'application/vnd.docker.distribution.manifest.v2+json', 'application/vnd.docker.distribution.manifest.list.v2+json', 'application/vnd.docker.distribution.manifest.v1+json', ].join(', '), }, query: {}, }; const result = await this.fetch(context); if (!result || !result.success) { return null; } let manifest: IOciManifest | IOciImageIndex; if (Buffer.isBuffer(result.body)) { manifest = JSON.parse(result.body.toString('utf8')); } else { manifest = result.body; } const contentType = result.headers['content-type'] || 'application/vnd.oci.image.manifest.v1+json'; const digest = result.headers['docker-content-digest'] || ''; return { manifest, contentType, digest }; } /** * Check if a manifest exists in upstream (HEAD request). */ public async headManifest( repository: string, reference: string, ): Promise<{ exists: boolean; contentType?: string; digest?: string; size?: number } | null> { const context: IUpstreamFetchContext = { protocol: 'oci', resource: repository, resourceType: 'manifest', path: `/v2/${repository}/manifests/${reference}`, method: 'HEAD', headers: { 'accept': [ 'application/vnd.oci.image.manifest.v1+json', 'application/vnd.oci.image.index.v1+json', 'application/vnd.docker.distribution.manifest.v2+json', 'application/vnd.docker.distribution.manifest.list.v2+json', ].join(', '), }, query: {}, }; const result = await this.fetch(context); if (!result) { return null; } if (!result.success) { return { exists: false }; } return { exists: true, contentType: result.headers['content-type'], digest: result.headers['docker-content-digest'], size: result.headers['content-length'] ? parseInt(result.headers['content-length'], 10) : undefined, }; } /** * Fetch a blob from upstream registries. */ public async fetchBlob(repository: string, digest: string): Promise { const context: IUpstreamFetchContext = { protocol: 'oci', resource: repository, resourceType: 'blob', path: `/v2/${repository}/blobs/${digest}`, method: 'GET', headers: { 'accept': 'application/octet-stream', }, query: {}, }; const result = await this.fetch(context); if (!result || !result.success) { return null; } return Buffer.isBuffer(result.body) ? result.body : Buffer.from(result.body); } /** * Check if a blob exists in upstream (HEAD request). */ public async headBlob( repository: string, digest: string, ): Promise<{ exists: boolean; size?: number } | null> { const context: IUpstreamFetchContext = { protocol: 'oci', resource: repository, resourceType: 'blob', path: `/v2/${repository}/blobs/${digest}`, method: 'HEAD', headers: {}, query: {}, }; const result = await this.fetch(context); if (!result) { return null; } if (!result.success) { return { exists: false }; } return { exists: true, size: result.headers['content-length'] ? parseInt(result.headers['content-length'], 10) : undefined, }; } /** * Fetch the tag list for a repository. */ public async fetchTags(repository: string, n?: number, last?: string): Promise { const query: Record = {}; if (n) query.n = n.toString(); if (last) query.last = last; const context: IUpstreamFetchContext = { protocol: 'oci', resource: repository, resourceType: 'tags', path: `/v2/${repository}/tags/list`, method: 'GET', headers: { 'accept': 'application/json', }, query, }; const result = await this.fetch(context); if (!result || !result.success) { return null; } let tagList: ITagList; if (Buffer.isBuffer(result.body)) { tagList = JSON.parse(result.body.toString('utf8')); } else { tagList = result.body; } return tagList; } /** * Override URL building for OCI-specific handling. * OCI registries use /v2/ prefix and may require special handling for Docker Hub. */ protected buildUpstreamUrl( upstream: IUpstreamRegistryConfig, context: IUpstreamFetchContext, ): string { let baseUrl = upstream.url; // Remove trailing slash if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, -1); } // Handle Docker Hub special case // Docker Hub uses registry-1.docker.io but library images need special handling if (baseUrl.includes('docker.io') || baseUrl.includes('registry-1.docker.io')) { // For library images (e.g., "nginx" -> "library/nginx") const pathParts = context.path.match(/^\/v2\/([^\/]+)\/(.+)$/); if (pathParts) { const [, repository, rest] = pathParts; // If repository doesn't contain a slash, it's a library image if (!repository.includes('/')) { return `${baseUrl}/v2/library/${repository}/${rest}`; } } } return `${baseUrl}${context.path}`; } /** * Override header building for OCI-specific authentication. * OCI registries may require token-based auth obtained from a separate endpoint. */ protected buildHeaders( upstream: IUpstreamRegistryConfig, context: IUpstreamFetchContext, ): Record { const headers = super.buildHeaders(upstream, context); // OCI registries typically use Docker-Distribution-API-Version header headers['docker-distribution-api-version'] = 'registry/2.0'; return headers; } }