/** * Lexicon for npm packages stored natively in an atproto repo. * * One record per package (rkey = encoded package name). The record holds * the dist-tags and, for every published version, blob references for the * tarball and the version manifest, which keeps those blobs out of orphan * cleanup. npm integrity is SHA-512, so unlike an OCI digest it cannot be * read back out of a blob CID; the record carries the integrity strings. */ export const NPM_PACKAGE_COLLECTION = 'dev.pdsjs.npm.package'; const NAME_PART = /^[a-z0-9~-][a-z0-9._~-]*$/; const MAX_NAME_LENGTH = 214; /** * npm package name grammar: an optional `@scope/`, then a name that does * not begin with `.` or `_`. * @param {string} name * @returns {boolean} */ export function isValidName(name) { if (!name || name.length > MAX_NAME_LENGTH) return false; if (name.startsWith('@')) { const parts = name.slice(1).split('/'); if (parts.length !== 2) return false; return parts.every((part) => NAME_PART.test(part)); } return NAME_PART.test(name); } /** * A package name may hold `@` and `/`, which a record key may not. A * scoped name encodes as `scope:name`: `:` is a valid record-key character * that no npm name contains, so the two substitute cleanly. * @param {string} name * @returns {string} */ export function nameToRkey(name) { if (name.startsWith('@')) return name.slice(1).replace('/', ':'); return name; } /** * @param {string} rkey * @returns {string} */ export function rkeyToName(rkey) { if (rkey.includes(':')) return `@${rkey.replace(':', '/')}`; return rkey; } export const npmPackageLexicon = { lexicon: 1, id: NPM_PACKAGE_COLLECTION, defs: { main: { type: 'record', description: 'An npm package: its dist-tags plus a tarball and manifest blob for every published version.', key: 'any', record: { type: 'object', required: ['name', 'distTags', 'versions', 'createdAt'], properties: { name: { type: 'string', maxLength: 512, description: 'Package name. The record key encodes this name with the leading `@` dropped and `/` replaced by `:`.', }, description: { type: 'string', maxLength: 3000 }, distTags: { type: 'array', items: { type: 'ref', ref: '#distTag' }, }, versions: { type: 'array', items: { type: 'ref', ref: '#version' }, }, createdAt: { type: 'string', format: 'datetime' }, updatedAt: { type: 'string', format: 'datetime' }, }, }, }, distTag: { type: 'object', required: ['tag', 'version'], properties: { tag: { type: 'string', maxLength: 128 }, version: { type: 'string', maxLength: 256 }, }, }, version: { type: 'object', required: ['version', 'tarball', 'manifest', 'integrity'], properties: { version: { type: 'string', maxLength: 256 }, tarball: { type: 'blob', description: 'The package tarball, exactly as published.', }, manifest: { type: 'blob', description: 'The version manifest as JSON bytes, exactly as published. The packument is assembled from these.', }, integrity: { type: 'string', maxLength: 512, description: 'SRI string for the tarball, e.g. sha512-....', }, shasum: { type: 'string', maxLength: 64, description: 'SHA-1 hex of the tarball, for older clients.', }, publishedAt: { type: 'string', format: 'datetime' }, }, }, }, };