chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
/**
* Manifest parsing: what a pushed manifest references, so the record can
* keep those blobs and the server can refuse a manifest whose parts are
* missing. The bytes themselves are stored untouched; a digest covers the
* exact serialization.
*/
export const MEDIA_TYPES = {
dockerManifest: 'application/vnd.docker.distribution.manifest.v2+json',
dockerManifestList:
'application/vnd.docker.distribution.manifest.list.v2+json',
ociManifest: 'application/vnd.oci.image.manifest.v1+json',
ociIndex: 'application/vnd.oci.image.index.v1+json',
};
const INDEX_TYPES = new Set([
MEDIA_TYPES.dockerManifestList,
MEDIA_TYPES.ociIndex,
]);
/** Largest manifest accepted, in bytes. Real manifests are a few KB. */
export const MAX_MANIFEST_SIZE = 4 * 1024 * 1024;
/**
* @typedef {Object} ManifestDescriptor
* @property {string} digest
* @property {string} [mediaType]
* @property {number} [size]
* @property {string[]} [urls] - locations outside this registry; such a
* layer is not stored here and existence checks skip it
*/
/**
* @typedef {Object} ParsedManifest
* @property {string} mediaType - from the body, else the Content-Type header
* @property {boolean} isIndex
* @property {ManifestDescriptor[]} blobs - config and layers an image
* manifest references
* @property {ManifestDescriptor[]} children - manifests an index references
*/
/**
* @param {unknown} value
* @returns {ManifestDescriptor|null}
*/
function asDescriptor(value) {
if (!value || typeof value !== 'object') return null;
const entry = /** @type {Record<string, unknown>} */ (value);
if (typeof entry.digest !== 'string') return null;
return {
digest: entry.digest,
mediaType:
typeof entry.mediaType === 'string' ? entry.mediaType : undefined,
size: typeof entry.size === 'number' ? entry.size : undefined,
urls: Array.isArray(entry.urls)
? entry.urls.filter((u) => typeof u === 'string')
: undefined,
};
}
/**
* @param {Uint8Array} bytes - manifest body, exactly as pushed
* @param {string} contentType - Content-Type header from the push
* @returns {ParsedManifest}
*/
export function parseManifest(bytes, contentType) {
/** @type {Record<string, unknown>} */
let body;
try {
body = JSON.parse(new TextDecoder().decode(bytes));
} catch {
throw new Error('manifest is not valid JSON');
}
if (!body || typeof body !== 'object' || Array.isArray(body)) {
throw new Error('manifest is not a JSON object');
}
const declaredType =
typeof body.mediaType === 'string' ? body.mediaType : null;
const headerType = contentType.split(';')[0].trim();
const mediaType = declaredType ?? headerType;
if (declaredType && headerType && declaredType !== headerType) {
throw new Error(
`manifest mediaType ${declaredType} does not match Content-Type ${headerType}`,
);
}
const isIndex = INDEX_TYPES.has(mediaType) || Array.isArray(body.manifests);
if (isIndex) {
const children = (Array.isArray(body.manifests) ? body.manifests : [])
.map(asDescriptor)
.filter((d) => d !== null);
return { mediaType, isIndex, blobs: [], children };
}
/** @type {ManifestDescriptor[]} */
const blobs = [];
const config = asDescriptor(body.config);
if (config) blobs.push(config);
for (const layer of Array.isArray(body.layers) ? body.layers : []) {
const descriptor = asDescriptor(layer);
if (descriptor) blobs.push(descriptor);
}
return { mediaType, isIndex, blobs, children: [] };
}