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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
145 lines · 4.1 KB Raw History
/**
 * Lexicon for OCI image repositories stored natively in an atproto repo.
 *
 * One record per image repository (rkey = encoded repository name). The
 * record holds the tag list and a blob reference for every manifest, config
 * and layer, which keeps those blobs out of orphan cleanup. Manifest and
 * layer bytes live in PDS blob storage. A blob's CID and its OCI digest
 * carry the same SHA-256 bytes, so content addressing needs no extra index.
 */

export const OCI_REPO_COLLECTION = 'dev.pdsjs.oci.repo';

/**
 * Repository name grammar from the OCI distribution spec: lowercase
 * components joined by `.`, `_`, `__` or runs of `-`, with `/` between
 * path components.
 */
const NAME_COMPONENT = /^[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*$/;
const MAX_NAME_LENGTH = 255;

/**
 * @param {string} name
 * @returns {boolean}
 */
export function isValidName(name) {
  if (!name || name.length > MAX_NAME_LENGTH) return false;
  return name.split('/').every((part) => NAME_COMPONENT.test(part));
}

/** Tag grammar from the OCI distribution spec. */
const TAG_PATTERN = /^[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}$/;

/**
 * @param {string} tag
 * @returns {boolean}
 */
export function isValidTag(tag) {
  return TAG_PATTERN.test(tag);
}

/**
 * A repository name may hold `/`, which a record key may not. `:` is a
 * valid record-key character that no repository name contains, so the two
 * substitute cleanly in both directions.
 * @param {string} name
 * @returns {string}
 */
export function nameToRkey(name) {
  return name.replaceAll('/', ':');
}

/**
 * @param {string} rkey
 * @returns {string}
 */
export function rkeyToName(rkey) {
  return rkey.replaceAll(':', '/');
}

export const ociRepoLexicon = {
  lexicon: 1,
  id: OCI_REPO_COLLECTION,
  defs: {
    main: {
      type: 'record',
      description:
        'An OCI image repository: its tags plus a blob reference for every stored manifest, config and layer. A blob CID and an OCI sha256 digest hold the same hash bytes.',
      key: 'any',
      record: {
        type: 'object',
        required: ['name', 'tags', 'manifests', 'layers', 'createdAt'],
        properties: {
          name: {
            type: 'string',
            maxLength: 512,
            description:
              'Repository name. The record key encodes this name with `/` replaced by `:`.',
          },
          description: { type: 'string', maxLength: 3000 },
          tags: {
            type: 'array',
            items: { type: 'ref', ref: '#tag' },
          },
          manifests: {
            type: 'array',
            description: 'Every stored manifest, tagged or not.',
            items: { type: 'ref', ref: '#manifest' },
          },
          layers: {
            type: 'array',
            description:
              'Config and layer blobs the stored manifests reference.',
            items: { type: 'ref', ref: '#layer' },
          },
          createdAt: { type: 'string', format: 'datetime' },
          updatedAt: { type: 'string', format: 'datetime' },
        },
      },
    },
    tag: {
      type: 'object',
      required: ['tag', 'digest'],
      properties: {
        tag: {
          type: 'string',
          maxLength: 128,
          description: 'Tag name, e.g. latest.',
        },
        digest: {
          type: 'string',
          maxLength: 128,
          description: 'Digest of the manifest the tag points at.',
        },
      },
    },
    manifest: {
      type: 'object',
      required: ['digest', 'mediaType', 'blob'],
      properties: {
        digest: { type: 'string', maxLength: 128 },
        mediaType: {
          type: 'string',
          maxLength: 256,
          description:
            'Exact media type the manifest is served with, e.g. application/vnd.oci.image.manifest.v1+json.',
        },
        blob: {
          type: 'blob',
          description: 'The manifest bytes, exactly as pushed.',
        },
      },
    },
    layer: {
      type: 'object',
      required: ['digest', 'blob'],
      properties: {
        digest: { type: 'string', maxLength: 128 },
        blob: {
          type: 'blob',
          description: 'Config or layer bytes.',
        },
      },
    },
  },
};