chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
import { createHash, randomBytes } from 'node:crypto';
import { cidToString, createBlobCid } from '@pdsjs/core/repo';
import { describe, expect, it } from 'vitest';
import {
cidToDigest,
digestFromHashBytes,
digestToCid,
isValidDigest,
} from '../src/digest.js';
import {
isValidName,
isValidTag,
nameToRkey,
rkeyToName,
} from '../src/lexicon.js';
import { MEDIA_TYPES, parseManifest } from '../src/manifest.js';
import {
blobRef,
parseRepoRecord,
recordBlobCids,
removeManifest,
serializeRepoRecord,
} from '../src/record.js';
describe('digest', () => {
it('round-trips digest to CID and back', () => {
const hex = createHash('sha256').update('layer bytes').digest('hex');
const digest = `sha256:${hex}`;
const cid = digestToCid(digest);
expect(cid.startsWith('b')).toBe(true);
expect(cidToDigest(cid)).toBe(digest);
});
it('matches the CID the blob upload path computes', async () => {
const bytes = new Uint8Array(randomBytes(1024));
const uploadCid = cidToString(await createBlobCid(bytes));
const hash = createHash('sha256').update(bytes).digest();
expect(digestToCid(digestFromHashBytes(hash))).toBe(uploadCid);
expect(cidToDigest(uploadCid)).toBe(`sha256:${hash.toString('hex')}`);
});
it('validates digest strings', () => {
expect(isValidDigest(`sha256:${'a'.repeat(64)}`)).toBe(true);
expect(isValidDigest(`sha512:${'a'.repeat(128)}`)).toBe(false);
expect(isValidDigest(`sha256:${'A'.repeat(64)}`)).toBe(false);
expect(isValidDigest('sha256:short')).toBe(false);
expect(() => digestToCid('sha256:nope')).toThrow(/unsupported digest/);
});
it('refuses a CID that is not sha256', () => {
expect(() => cidToDigest('bnope')).toThrow();
});
});
describe('names and tags', () => {
it('accepts spec-shaped repository names', () => {
for (const name of [
'app',
'my-app',
'a.b',
'a_b',
'a__b',
'chad/app',
'a/b/c',
'app2',
]) {
expect(isValidName(name), name).toBe(true);
}
});
it('refuses names outside the grammar', () => {
for (const name of ['', 'UPPER', 'a//b', 'a/', '/a', '-a', 'a-', 'a:b']) {
expect(isValidName(name), name).toBe(false);
}
});
it('round-trips names through record keys', () => {
expect(nameToRkey('chad/app')).toBe('chad:app');
expect(rkeyToName('chad:app')).toBe('chad/app');
expect(rkeyToName(nameToRkey('a/b/c'))).toBe('a/b/c');
expect(nameToRkey('app')).toBe('app');
});
it('validates tags', () => {
expect(isValidTag('latest')).toBe(true);
expect(isValidTag('v1.2.3')).toBe(true);
expect(isValidTag('_hidden')).toBe(true);
expect(isValidTag('.dot')).toBe(false);
expect(isValidTag('a'.repeat(129))).toBe(false);
});
});
describe('manifest parsing', () => {
const layer = {
mediaType: 'application/vnd.oci.image.layer.v1.tar+gzip',
digest: `sha256:${'1'.repeat(64)}`,
size: 123,
};
const config = {
mediaType: 'application/vnd.oci.image.config.v1+json',
digest: `sha256:${'2'.repeat(64)}`,
size: 7,
};
it('reads config and layers from an image manifest', () => {
const body = new TextEncoder().encode(
JSON.stringify({
schemaVersion: 2,
mediaType: MEDIA_TYPES.ociManifest,
config,
layers: [layer],
}),
);
const parsed = parseManifest(body, MEDIA_TYPES.ociManifest);
expect(parsed.isIndex).toBe(false);
expect(parsed.mediaType).toBe(MEDIA_TYPES.ociManifest);
expect(parsed.blobs.map((b) => b.digest)).toEqual([
config.digest,
layer.digest,
]);
expect(parsed.children).toEqual([]);
});
it('reads children from an index', () => {
const body = new TextEncoder().encode(
JSON.stringify({
schemaVersion: 2,
mediaType: MEDIA_TYPES.ociIndex,
manifests: [
{ mediaType: MEDIA_TYPES.ociManifest, digest: layer.digest },
],
}),
);
const parsed = parseManifest(body, MEDIA_TYPES.ociIndex);
expect(parsed.isIndex).toBe(true);
expect(parsed.children.map((c) => c.digest)).toEqual([layer.digest]);
expect(parsed.blobs).toEqual([]);
});
it('takes the media type from the header when the body has none', () => {
const body = new TextEncoder().encode(
JSON.stringify({ schemaVersion: 2, config, layers: [] }),
);
const parsed = parseManifest(body, MEDIA_TYPES.dockerManifest);
expect(parsed.mediaType).toBe(MEDIA_TYPES.dockerManifest);
});
it('refuses a body that contradicts the header', () => {
const body = new TextEncoder().encode(
JSON.stringify({ mediaType: MEDIA_TYPES.ociManifest }),
);
expect(() => parseManifest(body, MEDIA_TYPES.dockerManifest)).toThrow(
/does not match/,
);
});
it('refuses bytes that are not a JSON object', () => {
expect(() =>
parseManifest(new TextEncoder().encode('not json'), ''),
).toThrow(/not valid JSON/);
expect(() => parseManifest(new TextEncoder().encode('[1]'), '')).toThrow(
/not a JSON object/,
);
});
it('keeps urls on a descriptor so foreign layers are recognizable', () => {
const body = new TextEncoder().encode(
JSON.stringify({
schemaVersion: 2,
mediaType: MEDIA_TYPES.ociManifest,
config,
layers: [{ ...layer, urls: ['https://elsewhere.example/layer'] }],
}),
);
const parsed = parseManifest(body, MEDIA_TYPES.ociManifest);
expect(parsed.blobs[1].urls).toEqual(['https://elsewhere.example/layer']);
});
});
describe('record', () => {
const manifestDigest = `sha256:${'3'.repeat(64)}`;
const layerDigest = `sha256:${'4'.repeat(64)}`;
const record = {
name: 'chad/app',
tags: [{ tag: 'latest', digest: manifestDigest }],
manifests: [
{
digest: manifestDigest,
mediaType: MEDIA_TYPES.ociManifest,
blob: blobRef(digestToCid(manifestDigest), MEDIA_TYPES.ociManifest, 7),
},
],
layers: [
{
digest: layerDigest,
blob: blobRef(digestToCid(layerDigest), 'application/octet-stream', 9),
},
],
createdAt: '2026-08-19T00:00:00.000Z',
};
it('round-trips through serialization', () => {
const parsed = parseRepoRecord(serializeRepoRecord(record));
expect(parsed).not.toBeNull();
expect(parsed?.name).toBe('chad/app');
expect(parsed?.tags).toEqual(record.tags);
expect(parsed?.manifests).toEqual(record.manifests);
expect(parsed?.layers).toEqual(record.layers);
});
it('collects every referenced blob CID once', () => {
const cids = recordBlobCids(record);
expect(cids).toContain(digestToCid(manifestDigest));
expect(cids).toContain(digestToCid(layerDigest));
expect(cids).toHaveLength(2);
});
it('drops a manifest, its tags and its unshared layers', () => {
const next = removeManifest(record, manifestDigest, new Set());
expect(next.tags).toEqual([]);
expect(next.manifests).toEqual([]);
expect(next.layers).toEqual([]);
});
it('keeps layers another manifest still references', () => {
const next = removeManifest(record, manifestDigest, new Set([layerDigest]));
expect(next.layers).toEqual(record.layers);
});
it('parses nothing from a malformed value', () => {
expect(parseRepoRecord(null)).toBeNull();
expect(parseRepoRecord({})).toBeNull();
expect(parseRepoRecord({ name: 5 })).toBeNull();
});
});