/** * Remote registry browser RPC — catalog, tags, manifests, delete. */ import * as validation from '../utils/validation.js' import { fetchCatalog, fetchTags, fetchManifest, deleteManifest, enrichTagsWithDigests, normalizeRegistryEndpoint, } from '../services/registry-client.js' import { resolveRegistryAuth } from './vault.js' /** * Resolve auth + server address for browser ops. * @param {import('../rpc/session.js').PeerSession} session * @param {object} args */ function resolveBrowserAuth(session, args = {}) { const auth = resolveRegistryAuth(session, { credentialId: args.credentialId, auth: args.auth, autoVault: args.autoVault !== false, image: args.repository ? `${args.serveraddress || 'docker.io'}/${args.repository}` : args.image, }) const serveraddress = args.serveraddress || auth?.serveraddress || 'https://index.docker.io/v1/' return { auth: auth ? { username: auth.username, password: auth.password } : null, serveraddress: validation.sanitizeString(serveraddress, 512), } } /** * @param {import('../rpc/session.js').PeerSession} session */ export function registerRegistryHandlers(session) { session.respond('registryCatalog', async (args = {}) => { const { auth, serveraddress } = resolveBrowserAuth(session, args) const result = await fetchCatalog(serveraddress, auth, { n: args.n, last: args.last, }) return { success: true, type: 'registryCatalog', serveraddress: result.endpoint.serveraddress, host: result.endpoint.host, isDockerHub: result.endpoint.isDockerHub, repositories: result.repositories, supported: result.supported, error: result.error, nextLast: result.nextLast, } }) session.respond('registryListTags', async (args = {}) => { const repository = validation.sanitizeString(args.repository || args.name || '', 512) if (!repository) throw new Error('repository required') const { auth, serveraddress } = resolveBrowserAuth(session, { ...args, repository, }) const result = await fetchTags(repository, serveraddress, auth) let tagsDetail = null if (args.enrich && result.tags.length) { tagsDetail = await enrichTagsWithDigests( result.repository, result.tags, serveraddress, auth, { limit: args.enrichLimit || 40 } ) } return { success: true, type: 'registryTags', serveraddress: result.endpoint.serveraddress, host: result.endpoint.host, repository: result.repository, tags: result.tags, tagsDetail, tagCount: result.tags.length, } }) session.respond('registryGetManifest', async (args = {}) => { const repository = validation.sanitizeString(args.repository || args.name || '', 512) const reference = validation.sanitizeString( args.reference || args.tag || args.digest || '', 256 ) if (!repository || !reference) throw new Error('repository and reference required') const { auth, serveraddress } = resolveBrowserAuth(session, { ...args, repository, }) const result = await fetchManifest(repository, reference, serveraddress, auth) // Don't ship full layer blobs — trim manifest for UI let manifestSummary = result.manifest if (manifestSummary && typeof manifestSummary === 'object') { manifestSummary = { schemaVersion: manifestSummary.schemaVersion, mediaType: manifestSummary.mediaType, architecture: manifestSummary.architecture, os: manifestSummary.os, config: manifestSummary.config ? { digest: manifestSummary.config.digest, size: manifestSummary.config.size, mediaType: manifestSummary.config.mediaType } : undefined, layers: Array.isArray(manifestSummary.layers) ? manifestSummary.layers.map((l) => ({ digest: l.digest, size: l.size, mediaType: l.mediaType, })) : undefined, manifests: Array.isArray(manifestSummary.manifests) ? manifestSummary.manifests.map((m) => ({ digest: m.digest, mediaType: m.mediaType, platform: m.platform, size: m.size, })) : undefined, } } return { success: true, type: 'registryManifest', serveraddress: result.endpoint.serveraddress, host: result.endpoint.host, repository: result.repository, reference: result.reference, digest: result.digest, mediaType: result.mediaType, sizeBytes: result.sizeBytes, platformCount: result.platformCount, architecture: result.architecture, os: result.os, childDigests: result.childDigests, manifest: manifestSummary, } }) session.respond('registryDeleteTag', async (args = {}) => { const repository = validation.sanitizeString(args.repository || args.name || '', 512) const reference = validation.sanitizeString( args.reference || args.tag || args.digest || '', 256 ) if (!repository || !reference) throw new Error('repository and reference required') const { auth, serveraddress } = resolveBrowserAuth(session, { ...args, repository, }) if (!auth) { throw Object.assign(new Error('Authenticated credential required to delete remote tags'), { code: 'AUTH_REQUIRED', }) } const result = await deleteManifest(repository, reference, serveraddress, auth) return { success: true, type: 'registryDelete', ...result, host: result.endpoint?.host, serveraddress: result.endpoint?.serveraddress, } }) session.respond('registryDeleteTags', async (args = {}) => { const repository = validation.sanitizeString(args.repository || args.name || '', 512) const tags = Array.isArray(args.tags) ? args.tags.map((t) => String(t || '').trim()).filter(Boolean) : [] if (!repository || !tags.length) throw new Error('repository and tags[] required') if (tags.length > 50) throw new Error('Max 50 tags per bulk delete') const { auth, serveraddress } = resolveBrowserAuth(session, { ...args, repository, }) if (!auth) { throw Object.assign(new Error('Authenticated credential required to delete remote tags'), { code: 'AUTH_REQUIRED', }) } /** @type {Array} */ const results = [] for (const tag of tags) { try { const r = await deleteManifest(repository, tag, serveraddress, auth) results.push({ tag, ok: true, digest: r.digest, deleted: r.deleted }) } catch (err) { results.push({ tag, ok: false, error: err.message }) } } const deleted = results.filter((r) => r.ok && r.deleted).length return { success: true, type: 'registryBulkDelete', repository, deleted, failed: results.filter((r) => !r.ok).length, results, } }) session.respond('registryNormalizeEndpoint', async (args = {}) => { const ep = normalizeRegistryEndpoint(args.serveraddress || args.server || '') return { success: true, type: 'registryEndpoint', ...ep } }) }