~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
122 lines · 3.7 KB Raw History
/**
 * 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' },
      },
    },
  },
};