/** * The npm registry as a server extension: the platform passes the factory * to core's `extensions` option and core mounts the registry under /npm/ * on its own fetch, and the packages listing the account page reads under * /account/api. The registry reads through the server and publishes * straight to the ports, the way createNpmRegistry is built by hand. */ import { cborDecode, toDisplayValue } from '@pdsjs/core/repo'; import { createNpmRegistry } from './http.js'; import { NPM_PACKAGE_COLLECTION } from './lexicon.js'; /** * @typedef {Object} PackageSummary * @property {string} name * @property {string} uri * @property {Array<{tag: string, version: string}>} distTags * @property {Array<{version: string, size: number, publishedAt: string|null}>} versions * @property {number} size - every tarball and manifest, in bytes * @property {string|null} createdAt * @property {string|null} updatedAt */ /** * One record summarized for the packages page. The registry stores sizes on * the blob references, so the summary reads the record alone. * @param {{uri: string, value: Uint8Array|ArrayBuffer}} record * @returns {PackageSummary} */ function summarizePackage(record) { const value = /** @type {{name?: unknown, distTags?: unknown, versions?: unknown, createdAt?: unknown, updatedAt?: unknown}} */ ( toDisplayValue( cborDecode( record.value instanceof Uint8Array ? record.value : new Uint8Array(record.value), ), ) ); const name = typeof value.name === 'string' ? value.name : record.uri.split('/').pop() || ''; const distTags = /** @type {Array<{tag: string, version: string}>} */ ( /** @type {Array<{tag?: unknown, version?: unknown}>} */ ( Array.isArray(value.distTags) ? value.distTags : [] ).filter((t) => typeof t.tag === 'string' && typeof t.version === 'string') ); const versions = /** @type {Array<{version: string, publishedAt?: unknown, tarball?: {size?: unknown}, manifest?: {size?: unknown}}>} */ ( /** @type {Array<{version?: unknown}>} */ ( Array.isArray(value.versions) ? value.versions : [] ).filter((v) => typeof v.version === 'string') ); let size = 0; for (const entry of versions) { if (typeof entry.tarball?.size === 'number') size += entry.tarball.size; if (typeof entry.manifest?.size === 'number') size += entry.manifest.size; } return { name, uri: record.uri, distTags, versions: versions.map((entry) => ({ version: entry.version, size: typeof entry.tarball?.size === 'number' ? entry.tarball.size : 0, publishedAt: typeof entry.publishedAt === 'string' ? entry.publishedAt : null, })), size, createdAt: typeof value.createdAt === 'string' ? value.createdAt : null, updatedAt: typeof value.updatedAt === 'string' ? value.updatedAt : null, }; } /** * @param {Object} [options] * @param {number} [options.maxTarballSize] - largest accepted tarball, in bytes * @param {string} [options.collection] - record collection, for lexicon forks * @returns {import('@pdsjs/core/ports').ExtensionFactory} */ export function createNpmExtension({ maxTarballSize, collection = NPM_PACKAGE_COLLECTION, } = {}) { return (host) => { const registry = createNpmRegistry({ xrpc: host.xrpc, getDid: host.getDid, actorStorage: host.actorStorage, blobs: host.blobs, createCommit: host.createCommit, hasher: host.hasher, maxTarballSize, collection, }); return { name: 'npm', collections: [collection], prefixes: ['/npm'], handle: (request) => registry.handle(request), features: { npm: true }, stats: (count) => ({ npmPackages: count(collection) }), routes: { // The packages hosted in this account, for the account page. '/account/api/npm/packages': { method: 'GET', handler: (request, url) => host.accountApi(request, url, async () => { const page = await host.actorStorage.listRecords( collection, null, 100, ); return { enabled: true, registryUrl: `${url.protocol}//${url.host}/npm/`, packages: page.records.map(summarizePackage), }; }), }, }, }; }; }