chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
/**
* 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.',
},
},
},
},
};