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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
235 lines · 7.3 KB Raw History
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();
  });
});