import assert from 'node:assert'; import { describe, test } from 'node:test'; import { base32Decode, base32Encode, base64UrlDecode, base64UrlEncode, buildCarFile, bytesToHex, cborDecode, cborEncode, cidToString, createAccessJwt, createCid, createRefreshJwt, createTid, generateKeyPair, getKeyDepth, hexToBytes, importPrivateKey, sign, varint, verifyAccessJwt, } from '../src/pds.js'; 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, ]); assert.deepStrictEqual(encoded, expected); }); test('encodes null', () => { const encoded = cborEncode(null); assert.deepStrictEqual(encoded, new Uint8Array([0xf6])); }); test('encodes booleans', () => { assert.deepStrictEqual(cborEncode(true), new Uint8Array([0xf5])); assert.deepStrictEqual(cborEncode(false), new Uint8Array([0xf4])); }); test('encodes small integers', () => { assert.deepStrictEqual(cborEncode(0), new Uint8Array([0x00])); assert.deepStrictEqual(cborEncode(1), new Uint8Array([0x01])); assert.deepStrictEqual(cborEncode(23), new Uint8Array([0x17])); }); test('encodes integers >= 24', () => { assert.deepStrictEqual(cborEncode(24), new Uint8Array([0x18, 0x18])); assert.deepStrictEqual(cborEncode(255), new Uint8Array([0x18, 0xff])); }); test('encodes negative integers', () => { assert.deepStrictEqual(cborEncode(-1), new Uint8Array([0x20])); assert.deepStrictEqual(cborEncode(-10), new Uint8Array([0x29])); }); test('encodes strings', () => { const encoded = cborEncode('hello'); // 0x65 = text string of length 5 assert.deepStrictEqual( encoded, 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 assert.deepStrictEqual(encoded, new Uint8Array([0x43, 1, 2, 3])); }); test('encodes arrays', () => { const encoded = cborEncode([1, 2, 3]); // 0x83 = array of length 3 assert.deepStrictEqual(encoded, 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 }); assert.deepStrictEqual(encoded1, encoded2); // First key should be 'a' (0x61) assert.strictEqual(encoded1[1], 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); assert.strictEqual(decoded, twoTo31); // 2^32 - 1 (max unsigned 32-bit) const maxU32 = 4294967295; const encoded2 = cborEncode(maxU32); const decoded2 = cborDecode(encoded2); assert.strictEqual(decoded2, 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); assert.strictEqual(encoded[0], 0x1a); // type 0 | info 26 assert.strictEqual(encoded[1], 0x80); assert.strictEqual(encoded[2], 0x00); assert.strictEqual(encoded[3], 0x00); assert.strictEqual(encoded[4], 0x00); }); }); describe('Base32 Encoding', () => { test('encodes bytes to base32lower', () => { const bytes = new Uint8Array([0x01, 0x71, 0x12, 0x20]); const encoded = base32Encode(bytes); assert.strictEqual(typeof encoded, 'string'); assert.match(encoded, /^[a-z2-7]+$/); }); }); describe('CID Generation', () => { test('creates CIDv1 with dag-cbor codec', async () => { const data = cborEncode({ test: 'data' }); const cid = await createCid(data); assert.strictEqual(cid.length, 36); // 2 prefix + 2 multihash header + 32 hash assert.strictEqual(cid[0], 0x01); // CIDv1 assert.strictEqual(cid[1], 0x71); // dag-cbor assert.strictEqual(cid[2], 0x12); // sha-256 assert.strictEqual(cid[3], 0x20); // 32 bytes }); test('cidToString returns base32lower with b prefix', async () => { const data = cborEncode({ test: 'data' }); const cid = await createCid(data); const cidStr = cidToString(cid); assert.strictEqual(cidStr[0], 'b'); assert.match(cidStr, /^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)); assert.strictEqual(cid1, cid2); }); test('different input produces different CID', async () => { const cid1 = cidToString(await createCid(cborEncode({ a: 1 }))); const cid2 = cidToString(await createCid(cborEncode({ a: 2 }))); assert.notStrictEqual(cid1, cid2); }); }); describe('TID Generation', () => { test('creates 13-character TIDs', () => { const tid = createTid(); assert.strictEqual(tid.length, 13); }); test('uses valid base32-sort characters', () => { const tid = createTid(); assert.match(tid, /^[234567abcdefghijklmnopqrstuvwxyz]+$/); }); test('generates monotonically increasing TIDs', () => { const tid1 = createTid(); const tid2 = createTid(); const tid3 = createTid(); assert.ok(tid1 < tid2, `${tid1} should be less than ${tid2}`); assert.ok(tid2 < tid3, `${tid2} should be less than ${tid3}`); }); test('generates unique TIDs', () => { const tids = new Set(); for (let i = 0; i < 100; i++) { tids.add(createTid()); } assert.strictEqual(tids.size, 100); }); }); describe('P-256 Signing', () => { test('generates key pair with correct sizes', async () => { const kp = await generateKeyPair(); assert.strictEqual(kp.privateKey.length, 32); assert.strictEqual(kp.publicKey.length, 33); // compressed assert.ok(kp.publicKey[0] === 0x02 || kp.publicKey[0] === 0x03); }); 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); assert.strictEqual(sig.length, 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')); assert.notDeepStrictEqual(sig1, sig2); }); test('bytesToHex and hexToBytes roundtrip', () => { const original = new Uint8Array([0x00, 0x0f, 0xf0, 0xff, 0xab, 0xcd]); const hex = bytesToHex(original); const back = hexToBytes(hex); assert.strictEqual(hex, '000ff0ffabcd'); assert.deepStrictEqual(back, original); }); test('importPrivateKey rejects invalid key lengths', async () => { // Too short await assert.rejects( () => importPrivateKey(new Uint8Array(31)), /expected 32 bytes, got 31/, ); // Too long await assert.rejects( () => importPrivateKey(new Uint8Array(33)), /expected 32 bytes, got 33/, ); // Empty await assert.rejects( () => importPrivateKey(new Uint8Array(0)), /expected 32 bytes, got 0/, ); }); test('importPrivateKey rejects non-Uint8Array input', async () => { // Arrays have .length but aren't Uint8Array await assert.rejects( () => importPrivateKey([1, 2, 3]), /Invalid private key/, ); // Strings don't work either await assert.rejects( () => importPrivateKey('not bytes'), /Invalid private key/, ); // null/undefined await assert.rejects(() => importPrivateKey(null), /Invalid private key/); }); }); describe('MST Key Depth', () => { test('returns a non-negative integer', async () => { const depth = await getKeyDepth('app.bsky.feed.post/abc123'); assert.strictEqual(typeof depth, 'number'); assert.ok(depth >= 0); }); test('is deterministic for same key', async () => { const key = 'app.bsky.feed.post/test123'; const depth1 = await getKeyDepth(key); const depth2 = await getKeyDepth(key); assert.strictEqual(depth1, 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) assert.ok(depths.size >= 1); }); test('handles empty string', async () => { const depth = await getKeyDepth(''); assert.strictEqual(typeof depth, 'number'); assert.ok(depth >= 0); }); test('handles unicode strings', async () => { const depth = await getKeyDepth('app.bsky.feed.post/émoji🎉'); assert.strictEqual(typeof depth, 'number'); assert.ok(depth >= 0); }); }); describe('CBOR Decoding', () => { test('decodes what encode produces (roundtrip)', () => { const original = { hello: 'world', num: 42 }; const encoded = cborEncode(original); const decoded = cborDecode(encoded); assert.deepStrictEqual(decoded, original); }); test('decodes null', () => { const encoded = cborEncode(null); const decoded = cborDecode(encoded); assert.strictEqual(decoded, null); }); test('decodes booleans', () => { assert.strictEqual(cborDecode(cborEncode(true)), true); assert.strictEqual(cborDecode(cborEncode(false)), false); }); test('decodes integers', () => { assert.strictEqual(cborDecode(cborEncode(0)), 0); assert.strictEqual(cborDecode(cborEncode(42)), 42); assert.strictEqual(cborDecode(cborEncode(255)), 255); assert.strictEqual(cborDecode(cborEncode(-1)), -1); assert.strictEqual(cborDecode(cborEncode(-10)), -10); }); test('decodes strings', () => { assert.strictEqual(cborDecode(cborEncode('hello')), 'hello'); assert.strictEqual(cborDecode(cborEncode('')), ''); }); test('decodes arrays', () => { assert.deepStrictEqual(cborDecode(cborEncode([1, 2, 3])), [1, 2, 3]); assert.deepStrictEqual(cborDecode(cborEncode([])), []); }); test('decodes nested structures', () => { const original = { arr: [1, { nested: true }], str: 'test' }; const decoded = cborDecode(cborEncode(original)); assert.deepStrictEqual(decoded, original); }); }); describe('CAR File Builder', () => { test('varint encodes small numbers', () => { assert.deepStrictEqual(varint(0), new Uint8Array([0])); assert.deepStrictEqual(varint(1), new Uint8Array([1])); assert.deepStrictEqual(varint(127), new Uint8Array([127])); }); test('varint encodes multi-byte numbers', () => { // 128 = 0x80 -> [0x80 | 0x00, 0x01] = [0x80, 0x01] assert.deepStrictEqual(varint(128), new Uint8Array([0x80, 0x01])); // 300 = 0x12c -> [0xac, 0x02] assert.deepStrictEqual(varint(300), new Uint8Array([0xac, 0x02])); }); test('base32 encode/decode roundtrip', () => { const original = new Uint8Array([0x01, 0x71, 0x12, 0x20, 0xab, 0xcd]); const encoded = base32Encode(original); const decoded = base32Decode(encoded); assert.deepStrictEqual(decoded, original); }); 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 }]); assert.ok(car instanceof Uint8Array); assert.ok(car.length > 0); // First byte should be varint of header length assert.ok(car[0] > 0); }); }); describe('JWT Base64URL', () => { test('base64UrlEncode encodes bytes correctly', () => { const input = new TextEncoder().encode('hello world'); const encoded = base64UrlEncode(input); assert.strictEqual(encoded, 'aGVsbG8gd29ybGQ'); assert.ok(!encoded.includes('+')); assert.ok(!encoded.includes('/')); assert.ok(!encoded.includes('=')); }); test('base64UrlDecode decodes string correctly', () => { const decoded = base64UrlDecode('aGVsbG8gd29ybGQ'); const str = new TextDecoder().decode(decoded); assert.strictEqual(str, 'hello world'); }); test('base64url roundtrip', () => { const original = new Uint8Array([0, 1, 2, 255, 254, 253]); const encoded = base64UrlEncode(original); const decoded = base64UrlDecode(encoded); assert.deepStrictEqual(decoded, original); }); }); 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('.'); assert.strictEqual(parts.length, 3); // Decode header const header = JSON.parse( new TextDecoder().decode(base64UrlDecode(parts[0])), ); assert.strictEqual(header.typ, 'at+jwt'); assert.strictEqual(header.alg, 'HS256'); // Decode payload const payload = JSON.parse( new TextDecoder().decode(base64UrlDecode(parts[1])), ); assert.strictEqual(payload.scope, 'com.atproto.access'); assert.strictEqual(payload.sub, did); assert.strictEqual(payload.aud, did); assert.ok(payload.iat > 0); assert.ok(payload.exp > payload.iat); }); 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])), ); assert.strictEqual(header.typ, 'refresh+jwt'); const payload = JSON.parse( new TextDecoder().decode(base64UrlDecode(parts[1])), ); assert.strictEqual(payload.scope, 'com.atproto.refresh'); assert.ok(payload.jti); // 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); assert.strictEqual(payload.sub, did); assert.strictEqual(payload.scope, 'com.atproto.access'); }); test('verifyAccessJwt throws for wrong secret', async () => { const did = 'did:web:test.example'; const jwt = await createAccessJwt(did, 'correct-secret'); await assert.rejects( () => verifyAccessJwt(jwt, 'wrong-secret'), /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 assert.rejects(() => verifyAccessJwt(jwt, secret), /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 assert.rejects( () => verifyAccessJwt(jwt, secret), /invalid token type/i, ); }); });