chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
Split pds.test.js Into Module-Specific Test Files
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Split the monolithic test/pds.test.js into 7 separate test files in packages/core/test/, each corresponding to its source module.
Architecture: Extract describe blocks from the single test file into module-specific test files. Each new file imports only from its corresponding source module. No source file changes.
Tech Stack: Vitest, ES modules
File Mapping Reference
| New Test File | Describe Blocks |
|---|---|
packages/core/test/crypto.test.js | Base32 Encoding, P-256 Signing, JWT Base64URL, JWK Thumbprint |
packages/core/test/repo.test.js | CBOR Encoding, CBOR Decoding, CID Generation, TID Generation, CAR File Builder, MIME Type Sniffing, Blob Ref Detection |
packages/core/test/auth.test.js | JWT Creation, JWT Verification |
packages/core/test/scope.test.js | Scope Parsing, ScopePermissions, parseScopesForDisplay |
packages/core/test/oauth.test.js | Client Metadata |
packages/core/test/pds.test.js | Proxy Utilities |
packages/core/test/mst.test.js | MST Key Depth |
Task 1: Create test directory and crypto.test.js
Files:
- Create:
packages/core/test/crypto.test.js
Step 1: Create test directory
Run: mkdir -p packages/core/test
Step 2: Create crypto.test.js
import { describe, expect, test } from 'vitest';
import {
base32Decode,
base32Encode,
base64UrlDecode,
base64UrlEncode,
bytesToHex,
computeJwkThumbprint,
generateKeyPair,
hexToBytes,
importPrivateKey,
sign,
} from '@pdsjs/core/crypto';
describe('Base32 Encoding', () => {
test('encodes bytes to base32lower', () => {
const bytes = new Uint8Array([0x01, 0x71, 0x12, 0x20]);
const encoded = base32Encode(bytes);
expect(typeof encoded).toBe('string');
expect(encoded).toMatch(/^[a-z2-7]+$/);
});
test('base32 encode/decode roundtrip', () => {
const original = new Uint8Array([0x01, 0x71, 0x12, 0x20, 0xab, 0xcd]);
const encoded = base32Encode(original);
const decoded = base32Decode(encoded);
expect(decoded).toEqual(original);
});
});
describe('P-256 Signing', () => {
test('generates key pair with correct sizes', async () => {
const kp = await generateKeyPair();
expect(kp.privateKey.length).toBe(32);
expect(kp.publicKey.length).toBe(33); // compressed
expect(kp.publicKey[0] === 0x02 || kp.publicKey[0] === 0x03).toBe(true);
});
test('can sign data with generated key', async () => {
const kp = await generateKeyPair();
const key = await importPrivateKey(kp.privateKey);
const data = new TextEncoder().encode('test message');
const sig = await sign(key, data);
expect(sig.length).toBe(64); // r (32) + s (32)
});
test('different messages produce different signatures', async () => {
const kp = await generateKeyPair();
const key = await importPrivateKey(kp.privateKey);
const sig1 = await sign(key, new TextEncoder().encode('message 1'));
const sig2 = await sign(key, new TextEncoder().encode('message 2'));
expect(sig1).not.toEqual(sig2);
});
test('bytesToHex and hexToBytes roundtrip', () => {
const original = new Uint8Array([0x00, 0x0f, 0xf0, 0xff, 0xab, 0xcd]);
const hex = bytesToHex(original);
const back = hexToBytes(hex);
expect(hex).toBe('000ff0ffabcd');
expect(back).toEqual(original);
});
test('importPrivateKey rejects invalid key lengths', async () => {
// Too short
await expect(() => importPrivateKey(new Uint8Array(31))).rejects.toThrow(
/expected 32 bytes, got 31/,
);
// Too long
await expect(() => importPrivateKey(new Uint8Array(33))).rejects.toThrow(
/expected 32 bytes, got 33/,
);
// Empty
await expect(() => importPrivateKey(new Uint8Array(0))).rejects.toThrow(
/expected 32 bytes, got 0/,
);
});
test('importPrivateKey rejects non-Uint8Array input', async () => {
// Arrays have .length but aren't Uint8Array
// @ts-expect-error - Testing runtime validation with invalid type
await expect(() => importPrivateKey([1, 2, 3])).rejects.toThrow(
/Invalid private key/,
);
// Strings don't work either
// @ts-expect-error - Testing runtime validation with invalid type
await expect(() => importPrivateKey('not bytes')).rejects.toThrow(
/Invalid private key/,
);
// null/undefined
// @ts-expect-error - Testing runtime validation with invalid type
await expect(() => importPrivateKey(null)).rejects.toThrow(
/Invalid private key/,
);
});
});
describe('JWT Base64URL', () => {
test('base64UrlEncode encodes bytes correctly', () => {
const input = new TextEncoder().encode('hello world');
const encoded = base64UrlEncode(input);
expect(encoded).toBe('aGVsbG8gd29ybGQ');
expect(encoded.includes('+')).toBe(false);
expect(encoded.includes('/')).toBe(false);
expect(encoded.includes('=')).toBe(false);
});
test('base64UrlDecode decodes string correctly', () => {
const decoded = base64UrlDecode('aGVsbG8gd29ybGQ');
const str = new TextDecoder().decode(decoded);
expect(str).toBe('hello world');
});
test('base64url roundtrip', () => {
const original = new Uint8Array([0, 1, 2, 255, 254, 253]);
const encoded = base64UrlEncode(original);
const decoded = base64UrlDecode(encoded);
expect(decoded).toEqual(original);
});
});
describe('JWK Thumbprint', () => {
test('computes deterministic thumbprint for EC key', async () => {
// Test vector: known JWK and its expected thumbprint
const jwk = {
kty: 'EC',
crv: 'P-256',
x: 'WbbCfHGZ9QtKsVuMdPZ8hBbP2949N_CSLG3LVV0nnKY',
y: 'eSgPlDj0RVMw8t8u4MvCYG4j_JfDwvrMUUwEEHVLmqQ',
};
const jkt1 = await computeJwkThumbprint(jwk);
const jkt2 = await computeJwkThumbprint(jwk);
// Thumbprint must be deterministic
expect(jkt1).toBe(jkt2);
// Must be base64url-encoded SHA-256 (43 chars)
expect(jkt1.length).toBe(43);
// Must only contain base64url characters
expect(jkt1).toMatch(/^[A-Za-z0-9_-]+$/);
});
test('produces different thumbprints for different keys', async () => {
const jwk1 = {
kty: 'EC',
crv: 'P-256',
x: 'WbbCfHGZ9QtKsVuMdPZ8hBbP2949N_CSLG3LVV0nnKY',
y: 'eSgPlDj0RVMw8t8u4MvCYG4j_JfDwvrMUUwEEHVLmqQ',
};
const jwk2 = {
kty: 'EC',
crv: 'P-256',
x: 'f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU',
y: 'x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0',
};
const jkt1 = await computeJwkThumbprint(jwk1);
const jkt2 = await computeJwkThumbprint(jwk2);
expect(jkt1).not.toBe(jkt2);
});
});
Step 3: Run tests to verify
Run: pnpm test packages/core/test/crypto.test.js
Expected: All tests PASS
Step 4: Commit
git add packages/core/test/crypto.test.js
git commit -m "test: extract crypto tests from pds.test.js"
Task 2: Create repo.test.js
Files:
- Create:
packages/core/test/repo.test.js
Step 1: Create repo.test.js
import { describe, expect, test } from 'vitest';
import {
buildCarFile,
cborDecode,
cborEncode,
cidToString,
createBlobCid,
createCid,
createTid,
findBlobRefs,
sniffMimeType,
varint,
} from '@pdsjs/core/repo';
import { base32Decode, base32Encode } from '@pdsjs/core/crypto';
describe('CBOR Encoding', () => {
test('encodes simple map', () => {
const encoded = cborEncode({ hello: 'world', num: 42 });
// Expected: a2 65 68 65 6c 6c 6f 65 77 6f 72 6c 64 63 6e 75 6d 18 2a
const expected = new Uint8Array([
0xa2, 0x65, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x65, 0x77, 0x6f, 0x72, 0x6c,
0x64, 0x63, 0x6e, 0x75, 0x6d, 0x18, 0x2a,
]);
expect(encoded).toEqual(expected);
});
test('encodes null', () => {
const encoded = cborEncode(null);
expect(encoded).toEqual(new Uint8Array([0xf6]));
});
test('encodes booleans', () => {
expect(cborEncode(true)).toEqual(new Uint8Array([0xf5]));
expect(cborEncode(false)).toEqual(new Uint8Array([0xf4]));
});
test('encodes small integers', () => {
expect(cborEncode(0)).toEqual(new Uint8Array([0x00]));
expect(cborEncode(1)).toEqual(new Uint8Array([0x01]));
expect(cborEncode(23)).toEqual(new Uint8Array([0x17]));
});
test('encodes integers >= 24', () => {
expect(cborEncode(24)).toEqual(new Uint8Array([0x18, 0x18]));
expect(cborEncode(255)).toEqual(new Uint8Array([0x18, 0xff]));
});
test('encodes negative integers', () => {
expect(cborEncode(-1)).toEqual(new Uint8Array([0x20]));
expect(cborEncode(-10)).toEqual(new Uint8Array([0x29]));
});
test('encodes strings', () => {
const encoded = cborEncode('hello');
// 0x65 = text string of length 5
expect(encoded).toEqual(
new Uint8Array([0x65, 0x68, 0x65, 0x6c, 0x6c, 0x6f]),
);
});
test('encodes byte strings', () => {
const bytes = new Uint8Array([1, 2, 3]);
const encoded = cborEncode(bytes);
// 0x43 = byte string of length 3
expect(encoded).toEqual(new Uint8Array([0x43, 1, 2, 3]));
});
test('encodes arrays', () => {
const encoded = cborEncode([1, 2, 3]);
// 0x83 = array of length 3
expect(encoded).toEqual(new Uint8Array([0x83, 0x01, 0x02, 0x03]));
});
test('sorts map keys deterministically', () => {
const encoded1 = cborEncode({ z: 1, a: 2 });
const encoded2 = cborEncode({ a: 2, z: 1 });
expect(encoded1).toEqual(encoded2);
// First key should be 'a' (0x61)
expect(encoded1[1]).toBe(0x61);
});
test('encodes large integers >= 2^31 without overflow', () => {
// 2^31 would overflow with bitshift operators (treated as signed 32-bit)
const twoTo31 = 2147483648;
const encoded = cborEncode(twoTo31);
const decoded = cborDecode(encoded);
expect(decoded).toBe(twoTo31);
// 2^32 - 1 (max unsigned 32-bit)
const maxU32 = 4294967295;
const encoded2 = cborEncode(maxU32);
const decoded2 = cborDecode(encoded2);
expect(decoded2).toBe(maxU32);
});
test('encodes 2^31 with correct byte format', () => {
// 2147483648 = 0x80000000
// CBOR: major type 0 (unsigned int), additional info 26 (4-byte follows)
const encoded = cborEncode(2147483648);
expect(encoded[0]).toBe(0x1a); // type 0 | info 26
expect(encoded[1]).toBe(0x80);
expect(encoded[2]).toBe(0x00);
expect(encoded[3]).toBe(0x00);
expect(encoded[4]).toBe(0x00);
});
});
describe('CBOR Decoding', () => {
test('decodes what encode produces (roundtrip)', () => {
const original = { hello: 'world', num: 42 };
const encoded = cborEncode(original);
const decoded = cborDecode(encoded);
expect(decoded).toEqual(original);
});
test('decodes null', () => {
const encoded = cborEncode(null);
const decoded = cborDecode(encoded);
expect(decoded).toBe(null);
});
test('decodes booleans', () => {
expect(cborDecode(cborEncode(true))).toBe(true);
expect(cborDecode(cborEncode(false))).toBe(false);
});
test('decodes integers', () => {
expect(cborDecode(cborEncode(0))).toBe(0);
expect(cborDecode(cborEncode(42))).toBe(42);
expect(cborDecode(cborEncode(255))).toBe(255);
expect(cborDecode(cborEncode(-1))).toBe(-1);
expect(cborDecode(cborEncode(-10))).toBe(-10);
});
test('decodes strings', () => {
expect(cborDecode(cborEncode('hello'))).toBe('hello');
expect(cborDecode(cborEncode(''))).toBe('');
});
test('decodes arrays', () => {
expect(cborDecode(cborEncode([1, 2, 3]))).toEqual([1, 2, 3]);
expect(cborDecode(cborEncode([]))).toEqual([]);
});
test('decodes nested structures', () => {
const original = { arr: [1, { nested: true }], str: 'test' };
const decoded = cborDecode(cborEncode(original));
expect(decoded).toEqual(original);
});
});
describe('CID Generation', () => {
test('createCid uses dag-cbor codec', async () => {
const data = cborEncode({ test: 'data' });
const cid = await createCid(data);
expect(cid.length).toBe(36); // 2 prefix + 2 multihash header + 32 hash
expect(cid[0]).toBe(0x01); // CIDv1
expect(cid[1]).toBe(0x71); // dag-cbor
expect(cid[2]).toBe(0x12); // sha-256
expect(cid[3]).toBe(0x20); // 32 bytes
});
test('createBlobCid uses raw codec', async () => {
const data = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]); // JPEG magic bytes
const cid = await createBlobCid(data);
expect(cid.length).toBe(36);
expect(cid[0]).toBe(0x01); // CIDv1
expect(cid[1]).toBe(0x55); // raw codec
expect(cid[2]).toBe(0x12); // sha-256
expect(cid[3]).toBe(0x20); // 32 bytes
});
test('same bytes produce different CIDs with different codecs', async () => {
const data = new Uint8Array([1, 2, 3, 4]);
const dagCborCid = cidToString(await createCid(data));
const rawCid = cidToString(await createBlobCid(data));
expect(dagCborCid).not.toBe(rawCid);
});
test('cidToString returns base32lower with b prefix', async () => {
const data = cborEncode({ test: 'data' });
const cid = await createCid(data);
const cidStr = cidToString(cid);
expect(cidStr[0]).toBe('b');
expect(cidStr).toMatch(/^b[a-z2-7]+$/);
});
test('same input produces same CID', async () => {
const data1 = cborEncode({ test: 'data' });
const data2 = cborEncode({ test: 'data' });
const cid1 = cidToString(await createCid(data1));
const cid2 = cidToString(await createCid(data2));
expect(cid1).toBe(cid2);
});
test('different input produces different CID', async () => {
const cid1 = cidToString(await createCid(cborEncode({ a: 1 })));
const cid2 = cidToString(await createCid(cborEncode({ a: 2 })));
expect(cid1).not.toBe(cid2);
});
});
describe('TID Generation', () => {
test('creates 13-character TIDs', () => {
const tid = createTid();
expect(tid.length).toBe(13);
});
test('uses valid base32-sort characters', () => {
const tid = createTid();
expect(tid).toMatch(/^[234567abcdefghijklmnopqrstuvwxyz]+$/);
});
test('generates monotonically increasing TIDs', () => {
const tid1 = createTid();
const tid2 = createTid();
const tid3 = createTid();
expect(tid1 < tid2).toBe(true);
expect(tid2 < tid3).toBe(true);
});
test('generates unique TIDs', () => {
const tids = new Set();
for (let i = 0; i < 100; i++) {
tids.add(createTid());
}
expect(tids.size).toBe(100);
});
});
describe('CAR File Builder', () => {
test('varint encodes small numbers', () => {
expect(varint(0)).toEqual(new Uint8Array([0]));
expect(varint(1)).toEqual(new Uint8Array([1]));
expect(varint(127)).toEqual(new Uint8Array([127]));
});
test('varint encodes multi-byte numbers', () => {
// 128 = 0x80 -> [0x80 | 0x00, 0x01] = [0x80, 0x01]
expect(varint(128)).toEqual(new Uint8Array([0x80, 0x01]));
// 300 = 0x12c -> [0xac, 0x02]
expect(varint(300)).toEqual(new Uint8Array([0xac, 0x02]));
});
test('buildCarFile produces valid structure', async () => {
const data = cborEncode({ test: 'data' });
const cid = await createCid(data);
const cidStr = cidToString(cid);
const car = buildCarFile(cidStr, [{ cid: cidStr, data }]);
expect(car instanceof Uint8Array).toBe(true);
expect(car.length > 0).toBe(true);
// First byte should be varint of header length
expect(car[0] > 0).toBe(true);
});
});
describe('MIME Type Sniffing', () => {
test('detects JPEG', () => {
const bytes = new Uint8Array([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10]);
expect(sniffMimeType(bytes)).toBe('image/jpeg');
});
test('detects PNG', () => {
const bytes = new Uint8Array([
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
]);
expect(sniffMimeType(bytes)).toBe('image/png');
});
test('detects GIF', () => {
const bytes = new Uint8Array([0x47, 0x49, 0x46, 0x38, 0x39, 0x61]);
expect(sniffMimeType(bytes)).toBe('image/gif');
});
test('detects WebP', () => {
const bytes = new Uint8Array([
0x52,
0x49,
0x46,
0x46, // RIFF
0x00,
0x00,
0x00,
0x00, // size (ignored)
0x57,
0x45,
0x42,
0x50, // WEBP
]);
expect(sniffMimeType(bytes)).toBe('image/webp');
});
test('detects MP4', () => {
const bytes = new Uint8Array([
0x00,
0x00,
0x00,
0x18, // size
0x66,
0x74,
0x79,
0x70, // ftyp
0x69,
0x73,
0x6f,
0x6d, // isom brand
]);
expect(sniffMimeType(bytes)).toBe('video/mp4');
});
test('detects AVIF', () => {
const bytes = new Uint8Array([
0x00,
0x00,
0x00,
0x1c, // size
0x66,
0x74,
0x79,
0x70, // ftyp
0x61,
0x76,
0x69,
0x66, // avif brand
]);
expect(sniffMimeType(bytes)).toBe('image/avif');
});
test('detects HEIC', () => {
const bytes = new Uint8Array([
0x00,
0x00,
0x00,
0x18, // size
0x66,
0x74,
0x79,
0x70, // ftyp
0x68,
0x65,
0x69,
0x63, // heic brand
]);
expect(sniffMimeType(bytes)).toBe('image/heic');
});
test('returns null for unknown', () => {
const bytes = new Uint8Array([0x00, 0x01, 0x02, 0x03]);
expect(sniffMimeType(bytes)).toBe(null);
});
});
describe('Blob Ref Detection', () => {
test('finds blob ref in simple object', () => {
const record = {
$type: 'app.bsky.feed.post',
text: 'Hello',
embed: {
$type: 'app.bsky.embed.images',
images: [
{
image: {
$type: 'blob',
ref: { $link: 'bafkreiabc123' },
mimeType: 'image/jpeg',
size: 1234,
},
alt: 'test image',
},
],
},
};
const refs = findBlobRefs(record);
expect(refs).toEqual(['bafkreiabc123']);
});
test('finds multiple blob refs', () => {
const record = {
images: [
{
image: {
$type: 'blob',
ref: { $link: 'cid1' },
mimeType: 'image/png',
size: 100,
},
},
{
image: {
$type: 'blob',
ref: { $link: 'cid2' },
mimeType: 'image/png',
size: 200,
},
},
],
};
const refs = findBlobRefs(record);
expect(refs).toEqual(['cid1', 'cid2']);
});
test('returns empty array when no blobs', () => {
const record = { text: 'Hello world', count: 42 };
const refs = findBlobRefs(record);
expect(refs).toEqual([]);
});
test('handles null and primitives', () => {
expect(findBlobRefs(null)).toEqual([]);
expect(findBlobRefs('string')).toEqual([]);
expect(findBlobRefs(42)).toEqual([]);
});
});
Step 2: Run tests to verify
Run: pnpm test packages/core/test/repo.test.js
Expected: All tests PASS
Step 3: Commit
git add packages/core/test/repo.test.js
git commit -m "test: extract repo tests from pds.test.js"
Task 3: Create auth.test.js
Files:
- Create:
packages/core/test/auth.test.js
Step 1: Create auth.test.js
import { describe, expect, test } from 'vitest';
import {
createAccessJwt,
createRefreshJwt,
verifyAccessJwt,
verifyRefreshJwt,
} from '@pdsjs/core/auth';
import { base64UrlDecode } from '@pdsjs/core/crypto';
describe('JWT Creation', () => {
test('createAccessJwt creates valid JWT structure', async () => {
const did = 'did:web:test.example';
const secret = 'test-secret-key';
const jwt = await createAccessJwt(did, secret);
const parts = jwt.split('.');
expect(parts.length).toBe(3);
// Decode header
const header = JSON.parse(
new TextDecoder().decode(base64UrlDecode(parts[0])),
);
expect(header.typ).toBe('at+jwt');
expect(header.alg).toBe('HS256');
// Decode payload
const payload = JSON.parse(
new TextDecoder().decode(base64UrlDecode(parts[1])),
);
expect(payload.scope).toBe('com.atproto.access');
expect(payload.sub).toBe(did);
expect(payload.aud).toBe(did);
expect(payload.iat > 0).toBe(true);
expect(payload.exp > payload.iat).toBe(true);
});
test('createRefreshJwt creates valid JWT with jti', async () => {
const did = 'did:web:test.example';
const secret = 'test-secret-key';
const jwt = await createRefreshJwt(did, secret);
const parts = jwt.split('.');
const header = JSON.parse(
new TextDecoder().decode(base64UrlDecode(parts[0])),
);
expect(header.typ).toBe('refresh+jwt');
const payload = JSON.parse(
new TextDecoder().decode(base64UrlDecode(parts[1])),
);
expect(payload.scope).toBe('com.atproto.refresh');
expect(payload.jti).toBeTruthy(); // has unique token ID
});
});
describe('JWT Verification', () => {
test('verifyAccessJwt returns payload for valid token', async () => {
const did = 'did:web:test.example';
const secret = 'test-secret-key';
const jwt = await createAccessJwt(did, secret);
const payload = await verifyAccessJwt(jwt, secret);
expect(payload.sub).toBe(did);
expect(payload.scope).toBe('com.atproto.access');
});
test('verifyAccessJwt throws for wrong secret', async () => {
const did = 'did:web:test.example';
const jwt = await createAccessJwt(did, 'correct-secret');
await expect(() => verifyAccessJwt(jwt, 'wrong-secret')).rejects.toThrow(
/invalid signature/i,
);
});
test('verifyAccessJwt throws for expired token', async () => {
const did = 'did:web:test.example';
const secret = 'test-secret-key';
// Create token that expired 1 second ago
const jwt = await createAccessJwt(did, secret, -1);
await expect(() => verifyAccessJwt(jwt, secret)).rejects.toThrow(
/expired/i,
);
});
test('verifyAccessJwt throws for refresh token', async () => {
const did = 'did:web:test.example';
const secret = 'test-secret-key';
const jwt = await createRefreshJwt(did, secret);
await expect(() => verifyAccessJwt(jwt, secret)).rejects.toThrow(
/invalid token type/i,
);
});
test('verifyRefreshJwt returns payload for valid token', async () => {
const did = 'did:web:test.example';
const secret = 'test-secret-key';
const jwt = await createRefreshJwt(did, secret);
const payload = await verifyRefreshJwt(jwt, secret);
expect(payload.sub).toBe(did);
expect(payload.scope).toBe('com.atproto.refresh');
expect(payload.jti).toBeTruthy(); // has token ID
});
test('verifyRefreshJwt throws for wrong secret', async () => {
const did = 'did:web:test.example';
const jwt = await createRefreshJwt(did, 'correct-secret');
await expect(() => verifyRefreshJwt(jwt, 'wrong-secret')).rejects.toThrow(
/invalid signature/i,
);
});
test('verifyRefreshJwt throws for expired token', async () => {
const did = 'did:web:test.example';
const secret = 'test-secret-key';
// Create token that expired 1 second ago
const jwt = await createRefreshJwt(did, secret, -1);
await expect(() => verifyRefreshJwt(jwt, secret)).rejects.toThrow(
/expired/i,
);
});
test('verifyRefreshJwt throws for access token', async () => {
const did = 'did:web:test.example';
const secret = 'test-secret-key';
const jwt = await createAccessJwt(did, secret);
await expect(() => verifyRefreshJwt(jwt, secret)).rejects.toThrow(
/invalid token type/i,
);
});
test('verifyAccessJwt throws for malformed JWT', async () => {
const secret = 'test-secret-key';
// Not a JWT at all
await expect(() => verifyAccessJwt('not-a-jwt', secret)).rejects.toThrow(
/Invalid JWT format/i,
);
// Only two parts
await expect(() => verifyAccessJwt('two.parts', secret)).rejects.toThrow(
/Invalid JWT format/i,
);
// Four parts
await expect(() =>
verifyAccessJwt('one.two.three.four', secret),
).rejects.toThrow(/Invalid JWT format/i);
});
test('verifyRefreshJwt throws for malformed JWT', async () => {
const secret = 'test-secret-key';
await expect(() => verifyRefreshJwt('not-a-jwt', secret)).rejects.toThrow(
/Invalid JWT format/i,
);
await expect(() => verifyRefreshJwt('two.parts', secret)).rejects.toThrow(
/Invalid JWT format/i,
);
});
});
Step 2: Run tests to verify
Run: pnpm test packages/core/test/auth.test.js
Expected: All tests PASS
Step 3: Commit
git add packages/core/test/auth.test.js
git commit -m "test: extract auth tests from pds.test.js"
Task 4: Create mst.test.js
Files:
- Create:
packages/core/test/mst.test.js
Step 1: Create mst.test.js
import { describe, expect, test } from 'vitest';
import { getKeyDepth } from '@pdsjs/core/repo';
describe('MST Key Depth', () => {
test('returns a non-negative integer', async () => {
const depth = await getKeyDepth('app.bsky.feed.post/abc123');
expect(typeof depth).toBe('number');
expect(depth >= 0).toBe(true);
});
test('is deterministic for same key', async () => {
const key = 'app.bsky.feed.post/test123';
const depth1 = await getKeyDepth(key);
const depth2 = await getKeyDepth(key);
expect(depth1).toBe(depth2);
});
test('different keys can have different depths', async () => {
// Generate many keys and check we get some variation
const depths = new Set();
for (let i = 0; i < 100; i++) {
depths.add(await getKeyDepth(`collection/key${i}`));
}
// Should have at least 1 unique depth (realistically more)
expect(depths.size >= 1).toBe(true);
});
test('handles empty string', async () => {
const depth = await getKeyDepth('');
expect(typeof depth).toBe('number');
expect(depth >= 0).toBe(true);
});
test('handles unicode strings', async () => {
const depth = await getKeyDepth('app.bsky.feed.post/émoji🎉');
expect(typeof depth).toBe('number');
expect(depth >= 0).toBe(true);
});
});
Step 2: Run tests to verify
Run: pnpm test packages/core/test/mst.test.js
Expected: All tests PASS
Step 3: Commit
git add packages/core/test/mst.test.js
git commit -m "test: extract mst tests from pds.test.js"
Task 5: Create scope.test.js
Files:
- Create:
packages/core/test/scope.test.js
Step 1: Create scope.test.js
import { describe, expect, test } from 'vitest';
import {
matchesMime,
parseBlobScope,
parseRepoScope,
parseScopesForDisplay,
ScopePermissions,
} from '@pdsjs/core/scope';
describe('Scope Parsing', () => {
describe('parseRepoScope', () => {
test('parses repo scope with query parameter action', () => {
const result = parseRepoScope('repo:app.bsky.feed.post?action=create');
expect(result).toEqual({
collection: 'app.bsky.feed.post',
actions: ['create'],
});
});
test('parses repo scope with multiple query parameter actions', () => {
const result = parseRepoScope(
'repo:app.bsky.feed.post?action=create&action=update',
);
expect(result).toEqual({
collection: 'app.bsky.feed.post',
actions: ['create', 'update'],
});
});
test('parses repo scope without actions as all actions', () => {
const result = parseRepoScope('repo:app.bsky.feed.post');
expect(result).toEqual({
collection: 'app.bsky.feed.post',
actions: ['create', 'update', 'delete'],
});
});
test('parses wildcard collection with action', () => {
const result = parseRepoScope('repo:*?action=create');
expect(result).toEqual({
collection: '*',
actions: ['create'],
});
});
test('parses query-only format', () => {
const result = parseRepoScope(
'repo?collection=app.bsky.feed.post&action=create',
);
expect(result).toEqual({
collection: 'app.bsky.feed.post',
actions: ['create'],
});
});
test('deduplicates repeated actions', () => {
const result = parseRepoScope(
'repo:app.bsky.feed.post?action=create&action=create&action=update',
);
expect(result).toEqual({
collection: 'app.bsky.feed.post',
actions: ['create', 'update'],
});
});
test('returns null for non-repo scope', () => {
expect(parseRepoScope('atproto')).toBe(null);
expect(parseRepoScope('blob:image/*')).toBe(null);
expect(parseRepoScope('transition:generic')).toBe(null);
});
test('returns null for invalid repo scope', () => {
expect(parseRepoScope('repo:')).toBe(null);
expect(parseRepoScope('repo?')).toBe(null);
});
});
describe('parseBlobScope', () => {
test('parses wildcard MIME', () => {
const result = parseBlobScope('blob:*/*');
expect(result).toEqual({ accept: ['*/*'] });
});
test('parses type wildcard', () => {
const result = parseBlobScope('blob:image/*');
expect(result).toEqual({ accept: ['image/*'] });
});
test('parses specific MIME', () => {
const result = parseBlobScope('blob:image/png');
expect(result).toEqual({ accept: ['image/png'] });
});
test('parses multiple MIMEs', () => {
const result = parseBlobScope('blob:image/png,image/jpeg');
expect(result).toEqual({ accept: ['image/png', 'image/jpeg'] });
});
test('returns null for non-blob scope', () => {
expect(parseBlobScope('atproto')).toBe(null);
expect(parseBlobScope('repo:*:create')).toBe(null);
});
});
describe('matchesMime', () => {
test('wildcard matches everything', () => {
expect(matchesMime('*/*', 'image/png')).toBe(true);
expect(matchesMime('*/*', 'video/mp4')).toBe(true);
});
test('type wildcard matches same type', () => {
expect(matchesMime('image/*', 'image/png')).toBe(true);
expect(matchesMime('image/*', 'image/jpeg')).toBe(true);
expect(matchesMime('image/*', 'video/mp4')).toBe(false);
});
test('exact match', () => {
expect(matchesMime('image/png', 'image/png')).toBe(true);
expect(matchesMime('image/png', 'image/jpeg')).toBe(false);
});
test('case insensitive', () => {
expect(matchesMime('image/PNG', 'image/png')).toBe(true);
expect(matchesMime('IMAGE/*', 'image/png')).toBe(true);
});
});
});
describe('ScopePermissions', () => {
describe('static scopes', () => {
test('atproto grants full access', () => {
const perms = new ScopePermissions('atproto');
expect(perms.allowsRepo('app.bsky.feed.post', 'create')).toBe(true);
expect(perms.allowsRepo('any.collection', 'delete')).toBe(true);
expect(perms.allowsBlob('image/png')).toBe(true);
expect(perms.allowsBlob('video/mp4')).toBe(true);
});
test('transition:generic grants full repo/blob access', () => {
const perms = new ScopePermissions('transition:generic');
expect(perms.allowsRepo('app.bsky.feed.post', 'create')).toBe(true);
expect(perms.allowsRepo('any.collection', 'delete')).toBe(true);
expect(perms.allowsBlob('image/png')).toBe(true);
});
});
describe('repo scopes', () => {
test('wildcard collection allows any collection', () => {
const perms = new ScopePermissions('repo:*?action=create');
expect(perms.allowsRepo('app.bsky.feed.post', 'create')).toBe(true);
expect(perms.allowsRepo('app.bsky.feed.like', 'create')).toBe(true);
expect(perms.allowsRepo('app.bsky.feed.post', 'delete')).toBe(false);
});
test('specific collection restricts to that collection', () => {
const perms = new ScopePermissions(
'repo:app.bsky.feed.post?action=create',
);
expect(perms.allowsRepo('app.bsky.feed.post', 'create')).toBe(true);
expect(perms.allowsRepo('app.bsky.feed.like', 'create')).toBe(false);
});
test('multiple actions', () => {
const perms = new ScopePermissions('repo:*?action=create&action=update');
expect(perms.allowsRepo('x', 'create')).toBe(true);
expect(perms.allowsRepo('x', 'update')).toBe(true);
expect(perms.allowsRepo('x', 'delete')).toBe(false);
});
test('multiple scopes combine', () => {
const perms = new ScopePermissions(
'repo:app.bsky.feed.post?action=create repo:app.bsky.feed.like?action=delete',
);
expect(perms.allowsRepo('app.bsky.feed.post', 'create')).toBe(true);
expect(perms.allowsRepo('app.bsky.feed.like', 'delete')).toBe(true);
expect(perms.allowsRepo('app.bsky.feed.post', 'delete')).toBe(false);
});
test('allowsRepo with query param format scopes', () => {
const perms = new ScopePermissions(
'atproto repo:app.bsky.feed.post?action=create',
);
expect(perms.allowsRepo('app.bsky.feed.post', 'create')).toBe(true);
expect(perms.allowsRepo('app.bsky.feed.post', 'delete')).toBe(true); // atproto grants full access
});
});
describe('blob scopes', () => {
test('wildcard allows any MIME', () => {
const perms = new ScopePermissions('blob:*/*');
expect(perms.allowsBlob('image/png')).toBe(true);
expect(perms.allowsBlob('video/mp4')).toBe(true);
});
test('type wildcard restricts to type', () => {
const perms = new ScopePermissions('blob:image/*');
expect(perms.allowsBlob('image/png')).toBe(true);
expect(perms.allowsBlob('image/jpeg')).toBe(true);
expect(perms.allowsBlob('video/mp4')).toBe(false);
});
test('specific MIME restricts exactly', () => {
const perms = new ScopePermissions('blob:image/png');
expect(perms.allowsBlob('image/png')).toBe(true);
expect(perms.allowsBlob('image/jpeg')).toBe(false);
});
});
describe('empty/no scope', () => {
test('no scope denies everything', () => {
const perms = new ScopePermissions('');
expect(perms.allowsRepo('x', 'create')).toBe(false);
expect(perms.allowsBlob('image/png')).toBe(false);
});
test('undefined scope denies everything', () => {
const perms = new ScopePermissions(undefined);
expect(perms.allowsRepo('x', 'create')).toBe(false);
});
});
describe('assertRepo', () => {
test('throws ScopeMissingError when denied', () => {
const perms = new ScopePermissions(
'repo:app.bsky.feed.post?action=create',
);
expect(() => perms.assertRepo('app.bsky.feed.like', 'create')).toThrow(
/Missing required scope/,
);
});
test('does not throw when allowed', () => {
const perms = new ScopePermissions(
'repo:app.bsky.feed.post?action=create',
);
expect(() =>
perms.assertRepo('app.bsky.feed.post', 'create'),
).not.toThrow();
});
});
describe('assertBlob', () => {
test('throws ScopeMissingError when denied', () => {
const perms = new ScopePermissions('blob:image/*');
expect(() => perms.assertBlob('video/mp4')).toThrow(
/Missing required scope/,
);
});
test('does not throw when allowed', () => {
const perms = new ScopePermissions('blob:image/*');
expect(() => perms.assertBlob('image/png')).not.toThrow();
});
});
});
describe('parseScopesForDisplay', () => {
test('parses identity-only scope', () => {
const result = parseScopesForDisplay('atproto');
expect(result.hasAtproto).toBe(true);
expect(result.hasTransitionGeneric).toBe(false);
expect(result.repoPermissions.size).toBe(0);
expect(result.blobPermissions).toEqual([]);
});
test('parses granular repo scopes', () => {
const result = parseScopesForDisplay(
'atproto repo:app.bsky.feed.post?action=create&action=update',
);
expect(result.repoPermissions.size).toBe(1);
const postPerms = result.repoPermissions.get('app.bsky.feed.post');
expect(postPerms).toEqual({
create: true,
update: true,
delete: false,
});
});
test('merges multiple scopes for same collection', () => {
const result = parseScopesForDisplay(
'atproto repo:app.bsky.feed.post?action=create repo:app.bsky.feed.post?action=delete',
);
const postPerms = result.repoPermissions.get('app.bsky.feed.post');
expect(postPerms).toEqual({
create: true,
update: false,
delete: true,
});
});
test('parses blob scopes', () => {
const result = parseScopesForDisplay('atproto blob:image/*');
expect(result.blobPermissions).toEqual(['image/*']);
});
test('detects transition:generic', () => {
const result = parseScopesForDisplay('atproto transition:generic');
expect(result.hasTransitionGeneric).toBe(true);
});
test('handles empty scope string', () => {
const result = parseScopesForDisplay('');
expect(result.hasAtproto).toBe(false);
expect(result.hasTransitionGeneric).toBe(false);
expect(result.repoPermissions.size).toBe(0);
expect(result.blobPermissions).toEqual([]);
});
});
Step 2: Run tests to verify
Run: pnpm test packages/core/test/scope.test.js
Expected: All tests PASS
Step 3: Commit
git add packages/core/test/scope.test.js
git commit -m "test: extract scope tests from pds.test.js"
Task 6: Create oauth.test.js
Files:
- Create:
packages/core/test/oauth.test.js
Step 1: Create oauth.test.js
import { describe, expect, test } from 'vitest';
import {
getLoopbackClientMetadata,
isLoopbackClient,
validateClientMetadata,
} from '@pdsjs/core/oauth';
describe('Client Metadata', () => {
test('isLoopbackClient detects localhost', () => {
expect(isLoopbackClient('http://localhost:8080')).toBe(true);
expect(isLoopbackClient('http://127.0.0.1:3000')).toBe(true);
expect(isLoopbackClient('https://example.com')).toBe(false);
});
test('getLoopbackClientMetadata returns permissive defaults', () => {
const metadata = getLoopbackClientMetadata('http://localhost:8080');
expect(metadata.client_id).toBe('http://localhost:8080');
expect(metadata.grant_types.includes('authorization_code')).toBe(true);
expect(metadata.dpop_bound_access_tokens).toBe(true);
});
test('validateClientMetadata rejects mismatched client_id', () => {
const metadata = {
client_id: 'https://other.com/metadata.json',
redirect_uris: ['https://example.com/callback'],
grant_types: ['authorization_code'],
response_types: ['code'],
};
expect(() =>
validateClientMetadata(metadata, 'https://example.com/metadata.json'),
).toThrow(/client_id mismatch/);
});
});
Step 2: Run tests to verify
Run: pnpm test packages/core/test/oauth.test.js
Expected: All tests PASS
Step 3: Commit
git add packages/core/test/oauth.test.js
git commit -m "test: extract oauth tests from pds.test.js"
Task 7: Create pds.test.js
Files:
- Create:
packages/core/test/pds.test.js
Step 1: Create pds.test.js
import { describe, expect, test } from 'vitest';
import {
getKnownServiceUrl,
parseAtprotoProxyHeader,
} from '@pdsjs/core/pds';
// Internal constant - not exported from pds.js due to Cloudflare Workers limitation
const BSKY_APPVIEW_URL = 'https://api.bsky.app';
describe('Proxy Utilities', () => {
describe('parseAtprotoProxyHeader', () => {
test('parses valid header', () => {
const result = parseAtprotoProxyHeader(
'did:web:api.bsky.app#bsky_appview',
);
expect(result).toEqual({
did: 'did:web:api.bsky.app',
serviceId: 'bsky_appview',
});
});
test('parses header with did:plc', () => {
const result = parseAtprotoProxyHeader(
'did:plc:z72i7hdynmk6r22z27h6tvur#atproto_labeler',
);
expect(result).toEqual({
did: 'did:plc:z72i7hdynmk6r22z27h6tvur',
serviceId: 'atproto_labeler',
});
});
test('returns null for null/undefined', () => {
// @ts-expect-error - Testing runtime handling of null
expect(parseAtprotoProxyHeader(null)).toBe(null);
// @ts-expect-error - Testing runtime handling of undefined
expect(parseAtprotoProxyHeader(undefined)).toBe(null);
expect(parseAtprotoProxyHeader('')).toBe(null);
});
test('returns null for header without fragment', () => {
expect(parseAtprotoProxyHeader('did:web:api.bsky.app')).toBe(null);
});
test('returns null for header with only fragment', () => {
expect(parseAtprotoProxyHeader('#bsky_appview')).toBe(null);
});
test('returns null for header with trailing fragment', () => {
expect(parseAtprotoProxyHeader('did:web:api.bsky.app#')).toBe(null);
});
});
describe('getKnownServiceUrl', () => {
test('returns URL for known Bluesky AppView', () => {
const result = getKnownServiceUrl('did:web:api.bsky.app', 'bsky_appview');
expect(result).toBe(BSKY_APPVIEW_URL);
});
test('returns null for unknown service DID', () => {
const result = getKnownServiceUrl(
'did:web:unknown.service',
'bsky_appview',
);
expect(result).toBe(null);
});
test('returns null for unknown service ID', () => {
const result = getKnownServiceUrl(
'did:web:api.bsky.app',
'unknown_service',
);
expect(result).toBe(null);
});
test('returns null for both unknown', () => {
const result = getKnownServiceUrl('did:web:unknown', 'unknown');
expect(result).toBe(null);
});
});
});
Step 2: Run tests to verify
Run: pnpm test packages/core/test/pds.test.js
Expected: All tests PASS
Step 3: Commit
git add packages/core/test/pds.test.js
git commit -m "test: extract pds tests from pds.test.js"
Task 8: Run all new tests together
Step 1: Run all core tests
Run: pnpm test packages/core/test/
Expected: All tests PASS (same count as original file)
Step 2: Verify test count matches original
The original test/pds.test.js had tests across all describe blocks. Verify the total test count is the same.
Task 9: Delete original test file and verify
Files:
- Delete:
test/pds.test.js
Step 1: Delete original file
Run: rm test/pds.test.js
Step 2: Run full test suite
Run: pnpm test
Expected: All tests PASS (e2e.test.js + all new core tests)
Step 3: Commit
git add -A
git commit -m "refactor: remove original pds.test.js after splitting into modules"
Task 10: Final verification
Step 1: Run full test suite one more time
Run: pnpm test
Expected: All tests PASS
Step 2: Verify no regressions
Check that test count matches expectations and no tests were lost in the split.