A zero-dependency AT Protocol Personal Data Server written in JavaScript
atproto pds
1import assert from 'node:assert'; 2import { describe, test } from 'node:test'; 3import { 4 base32Decode, 5 base32Encode, 6 base64UrlDecode, 7 base64UrlEncode, 8 buildCarFile, 9 bytesToHex, 10 cborDecode, 11 cborEncode, 12 cidToString, 13 createAccessJwt, 14 createCid, 15 createBlobCid, 16 createRefreshJwt, 17 createTid, 18 findBlobRefs, 19 generateKeyPair, 20 getKeyDepth, 21 hexToBytes, 22 importPrivateKey, 23 sign, 24 sniffMimeType, 25 varint, 26 verifyAccessJwt, 27 verifyRefreshJwt, 28} from '../src/pds.js'; 29 30describe('CBOR Encoding', () => { 31 test('encodes simple map', () => { 32 const encoded = cborEncode({ hello: 'world', num: 42 }); 33 // Expected: a2 65 68 65 6c 6c 6f 65 77 6f 72 6c 64 63 6e 75 6d 18 2a 34 const expected = new Uint8Array([ 35 0xa2, 0x65, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x65, 0x77, 0x6f, 0x72, 0x6c, 36 0x64, 0x63, 0x6e, 0x75, 0x6d, 0x18, 0x2a, 37 ]); 38 assert.deepStrictEqual(encoded, expected); 39 }); 40 41 test('encodes null', () => { 42 const encoded = cborEncode(null); 43 assert.deepStrictEqual(encoded, new Uint8Array([0xf6])); 44 }); 45 46 test('encodes booleans', () => { 47 assert.deepStrictEqual(cborEncode(true), new Uint8Array([0xf5])); 48 assert.deepStrictEqual(cborEncode(false), new Uint8Array([0xf4])); 49 }); 50 51 test('encodes small integers', () => { 52 assert.deepStrictEqual(cborEncode(0), new Uint8Array([0x00])); 53 assert.deepStrictEqual(cborEncode(1), new Uint8Array([0x01])); 54 assert.deepStrictEqual(cborEncode(23), new Uint8Array([0x17])); 55 }); 56 57 test('encodes integers >= 24', () => { 58 assert.deepStrictEqual(cborEncode(24), new Uint8Array([0x18, 0x18])); 59 assert.deepStrictEqual(cborEncode(255), new Uint8Array([0x18, 0xff])); 60 }); 61 62 test('encodes negative integers', () => { 63 assert.deepStrictEqual(cborEncode(-1), new Uint8Array([0x20])); 64 assert.deepStrictEqual(cborEncode(-10), new Uint8Array([0x29])); 65 }); 66 67 test('encodes strings', () => { 68 const encoded = cborEncode('hello'); 69 // 0x65 = text string of length 5 70 assert.deepStrictEqual( 71 encoded, 72 new Uint8Array([0x65, 0x68, 0x65, 0x6c, 0x6c, 0x6f]), 73 ); 74 }); 75 76 test('encodes byte strings', () => { 77 const bytes = new Uint8Array([1, 2, 3]); 78 const encoded = cborEncode(bytes); 79 // 0x43 = byte string of length 3 80 assert.deepStrictEqual(encoded, new Uint8Array([0x43, 1, 2, 3])); 81 }); 82 83 test('encodes arrays', () => { 84 const encoded = cborEncode([1, 2, 3]); 85 // 0x83 = array of length 3 86 assert.deepStrictEqual(encoded, new Uint8Array([0x83, 0x01, 0x02, 0x03])); 87 }); 88 89 test('sorts map keys deterministically', () => { 90 const encoded1 = cborEncode({ z: 1, a: 2 }); 91 const encoded2 = cborEncode({ a: 2, z: 1 }); 92 assert.deepStrictEqual(encoded1, encoded2); 93 // First key should be 'a' (0x61) 94 assert.strictEqual(encoded1[1], 0x61); 95 }); 96 97 test('encodes large integers >= 2^31 without overflow', () => { 98 // 2^31 would overflow with bitshift operators (treated as signed 32-bit) 99 const twoTo31 = 2147483648; 100 const encoded = cborEncode(twoTo31); 101 const decoded = cborDecode(encoded); 102 assert.strictEqual(decoded, twoTo31); 103 104 // 2^32 - 1 (max unsigned 32-bit) 105 const maxU32 = 4294967295; 106 const encoded2 = cborEncode(maxU32); 107 const decoded2 = cborDecode(encoded2); 108 assert.strictEqual(decoded2, maxU32); 109 }); 110 111 test('encodes 2^31 with correct byte format', () => { 112 // 2147483648 = 0x80000000 113 // CBOR: major type 0 (unsigned int), additional info 26 (4-byte follows) 114 const encoded = cborEncode(2147483648); 115 assert.strictEqual(encoded[0], 0x1a); // type 0 | info 26 116 assert.strictEqual(encoded[1], 0x80); 117 assert.strictEqual(encoded[2], 0x00); 118 assert.strictEqual(encoded[3], 0x00); 119 assert.strictEqual(encoded[4], 0x00); 120 }); 121}); 122 123describe('Base32 Encoding', () => { 124 test('encodes bytes to base32lower', () => { 125 const bytes = new Uint8Array([0x01, 0x71, 0x12, 0x20]); 126 const encoded = base32Encode(bytes); 127 assert.strictEqual(typeof encoded, 'string'); 128 assert.match(encoded, /^[a-z2-7]+$/); 129 }); 130}); 131 132describe('CID Generation', () => { 133 test('createCid uses dag-cbor codec', async () => { 134 const data = cborEncode({ test: 'data' }); 135 const cid = await createCid(data); 136 137 assert.strictEqual(cid.length, 36); // 2 prefix + 2 multihash header + 32 hash 138 assert.strictEqual(cid[0], 0x01); // CIDv1 139 assert.strictEqual(cid[1], 0x71); // dag-cbor 140 assert.strictEqual(cid[2], 0x12); // sha-256 141 assert.strictEqual(cid[3], 0x20); // 32 bytes 142 }); 143 144 test('createBlobCid uses raw codec', async () => { 145 const data = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]); // JPEG magic bytes 146 const cid = await createBlobCid(data); 147 148 assert.strictEqual(cid.length, 36); 149 assert.strictEqual(cid[0], 0x01); // CIDv1 150 assert.strictEqual(cid[1], 0x55); // raw codec 151 assert.strictEqual(cid[2], 0x12); // sha-256 152 assert.strictEqual(cid[3], 0x20); // 32 bytes 153 }); 154 155 test('same bytes produce different CIDs with different codecs', async () => { 156 const data = new Uint8Array([1, 2, 3, 4]); 157 const dagCborCid = cidToString(await createCid(data)); 158 const rawCid = cidToString(await createBlobCid(data)); 159 160 assert.notStrictEqual(dagCborCid, rawCid); 161 }); 162 163 test('cidToString returns base32lower with b prefix', async () => { 164 const data = cborEncode({ test: 'data' }); 165 const cid = await createCid(data); 166 const cidStr = cidToString(cid); 167 168 assert.strictEqual(cidStr[0], 'b'); 169 assert.match(cidStr, /^b[a-z2-7]+$/); 170 }); 171 172 test('same input produces same CID', async () => { 173 const data1 = cborEncode({ test: 'data' }); 174 const data2 = cborEncode({ test: 'data' }); 175 const cid1 = cidToString(await createCid(data1)); 176 const cid2 = cidToString(await createCid(data2)); 177 178 assert.strictEqual(cid1, cid2); 179 }); 180 181 test('different input produces different CID', async () => { 182 const cid1 = cidToString(await createCid(cborEncode({ a: 1 }))); 183 const cid2 = cidToString(await createCid(cborEncode({ a: 2 }))); 184 185 assert.notStrictEqual(cid1, cid2); 186 }); 187}); 188 189describe('TID Generation', () => { 190 test('creates 13-character TIDs', () => { 191 const tid = createTid(); 192 assert.strictEqual(tid.length, 13); 193 }); 194 195 test('uses valid base32-sort characters', () => { 196 const tid = createTid(); 197 assert.match(tid, /^[234567abcdefghijklmnopqrstuvwxyz]+$/); 198 }); 199 200 test('generates monotonically increasing TIDs', () => { 201 const tid1 = createTid(); 202 const tid2 = createTid(); 203 const tid3 = createTid(); 204 205 assert.ok(tid1 < tid2, `${tid1} should be less than ${tid2}`); 206 assert.ok(tid2 < tid3, `${tid2} should be less than ${tid3}`); 207 }); 208 209 test('generates unique TIDs', () => { 210 const tids = new Set(); 211 for (let i = 0; i < 100; i++) { 212 tids.add(createTid()); 213 } 214 assert.strictEqual(tids.size, 100); 215 }); 216}); 217 218describe('P-256 Signing', () => { 219 test('generates key pair with correct sizes', async () => { 220 const kp = await generateKeyPair(); 221 222 assert.strictEqual(kp.privateKey.length, 32); 223 assert.strictEqual(kp.publicKey.length, 33); // compressed 224 assert.ok(kp.publicKey[0] === 0x02 || kp.publicKey[0] === 0x03); 225 }); 226 227 test('can sign data with generated key', async () => { 228 const kp = await generateKeyPair(); 229 const key = await importPrivateKey(kp.privateKey); 230 const data = new TextEncoder().encode('test message'); 231 const sig = await sign(key, data); 232 233 assert.strictEqual(sig.length, 64); // r (32) + s (32) 234 }); 235 236 test('different messages produce different signatures', async () => { 237 const kp = await generateKeyPair(); 238 const key = await importPrivateKey(kp.privateKey); 239 240 const sig1 = await sign(key, new TextEncoder().encode('message 1')); 241 const sig2 = await sign(key, new TextEncoder().encode('message 2')); 242 243 assert.notDeepStrictEqual(sig1, sig2); 244 }); 245 246 test('bytesToHex and hexToBytes roundtrip', () => { 247 const original = new Uint8Array([0x00, 0x0f, 0xf0, 0xff, 0xab, 0xcd]); 248 const hex = bytesToHex(original); 249 const back = hexToBytes(hex); 250 251 assert.strictEqual(hex, '000ff0ffabcd'); 252 assert.deepStrictEqual(back, original); 253 }); 254 255 test('importPrivateKey rejects invalid key lengths', async () => { 256 // Too short 257 await assert.rejects( 258 () => importPrivateKey(new Uint8Array(31)), 259 /expected 32 bytes, got 31/, 260 ); 261 262 // Too long 263 await assert.rejects( 264 () => importPrivateKey(new Uint8Array(33)), 265 /expected 32 bytes, got 33/, 266 ); 267 268 // Empty 269 await assert.rejects( 270 () => importPrivateKey(new Uint8Array(0)), 271 /expected 32 bytes, got 0/, 272 ); 273 }); 274 275 test('importPrivateKey rejects non-Uint8Array input', async () => { 276 // Arrays have .length but aren't Uint8Array 277 await assert.rejects( 278 () => importPrivateKey([1, 2, 3]), 279 /Invalid private key/, 280 ); 281 282 // Strings don't work either 283 await assert.rejects( 284 () => importPrivateKey('not bytes'), 285 /Invalid private key/, 286 ); 287 288 // null/undefined 289 await assert.rejects(() => importPrivateKey(null), /Invalid private key/); 290 }); 291}); 292 293describe('MST Key Depth', () => { 294 test('returns a non-negative integer', async () => { 295 const depth = await getKeyDepth('app.bsky.feed.post/abc123'); 296 assert.strictEqual(typeof depth, 'number'); 297 assert.ok(depth >= 0); 298 }); 299 300 test('is deterministic for same key', async () => { 301 const key = 'app.bsky.feed.post/test123'; 302 const depth1 = await getKeyDepth(key); 303 const depth2 = await getKeyDepth(key); 304 assert.strictEqual(depth1, depth2); 305 }); 306 307 test('different keys can have different depths', async () => { 308 // Generate many keys and check we get some variation 309 const depths = new Set(); 310 for (let i = 0; i < 100; i++) { 311 depths.add(await getKeyDepth(`collection/key${i}`)); 312 } 313 // Should have at least 1 unique depth (realistically more) 314 assert.ok(depths.size >= 1); 315 }); 316 317 test('handles empty string', async () => { 318 const depth = await getKeyDepth(''); 319 assert.strictEqual(typeof depth, 'number'); 320 assert.ok(depth >= 0); 321 }); 322 323 test('handles unicode strings', async () => { 324 const depth = await getKeyDepth('app.bsky.feed.post/émoji🎉'); 325 assert.strictEqual(typeof depth, 'number'); 326 assert.ok(depth >= 0); 327 }); 328}); 329 330describe('CBOR Decoding', () => { 331 test('decodes what encode produces (roundtrip)', () => { 332 const original = { hello: 'world', num: 42 }; 333 const encoded = cborEncode(original); 334 const decoded = cborDecode(encoded); 335 assert.deepStrictEqual(decoded, original); 336 }); 337 338 test('decodes null', () => { 339 const encoded = cborEncode(null); 340 const decoded = cborDecode(encoded); 341 assert.strictEqual(decoded, null); 342 }); 343 344 test('decodes booleans', () => { 345 assert.strictEqual(cborDecode(cborEncode(true)), true); 346 assert.strictEqual(cborDecode(cborEncode(false)), false); 347 }); 348 349 test('decodes integers', () => { 350 assert.strictEqual(cborDecode(cborEncode(0)), 0); 351 assert.strictEqual(cborDecode(cborEncode(42)), 42); 352 assert.strictEqual(cborDecode(cborEncode(255)), 255); 353 assert.strictEqual(cborDecode(cborEncode(-1)), -1); 354 assert.strictEqual(cborDecode(cborEncode(-10)), -10); 355 }); 356 357 test('decodes strings', () => { 358 assert.strictEqual(cborDecode(cborEncode('hello')), 'hello'); 359 assert.strictEqual(cborDecode(cborEncode('')), ''); 360 }); 361 362 test('decodes arrays', () => { 363 assert.deepStrictEqual(cborDecode(cborEncode([1, 2, 3])), [1, 2, 3]); 364 assert.deepStrictEqual(cborDecode(cborEncode([])), []); 365 }); 366 367 test('decodes nested structures', () => { 368 const original = { arr: [1, { nested: true }], str: 'test' }; 369 const decoded = cborDecode(cborEncode(original)); 370 assert.deepStrictEqual(decoded, original); 371 }); 372}); 373 374describe('CAR File Builder', () => { 375 test('varint encodes small numbers', () => { 376 assert.deepStrictEqual(varint(0), new Uint8Array([0])); 377 assert.deepStrictEqual(varint(1), new Uint8Array([1])); 378 assert.deepStrictEqual(varint(127), new Uint8Array([127])); 379 }); 380 381 test('varint encodes multi-byte numbers', () => { 382 // 128 = 0x80 -> [0x80 | 0x00, 0x01] = [0x80, 0x01] 383 assert.deepStrictEqual(varint(128), new Uint8Array([0x80, 0x01])); 384 // 300 = 0x12c -> [0xac, 0x02] 385 assert.deepStrictEqual(varint(300), new Uint8Array([0xac, 0x02])); 386 }); 387 388 test('base32 encode/decode roundtrip', () => { 389 const original = new Uint8Array([0x01, 0x71, 0x12, 0x20, 0xab, 0xcd]); 390 const encoded = base32Encode(original); 391 const decoded = base32Decode(encoded); 392 assert.deepStrictEqual(decoded, original); 393 }); 394 395 test('buildCarFile produces valid structure', async () => { 396 const data = cborEncode({ test: 'data' }); 397 const cid = await createCid(data); 398 const cidStr = cidToString(cid); 399 400 const car = buildCarFile(cidStr, [{ cid: cidStr, data }]); 401 402 assert.ok(car instanceof Uint8Array); 403 assert.ok(car.length > 0); 404 // First byte should be varint of header length 405 assert.ok(car[0] > 0); 406 }); 407}); 408 409describe('JWT Base64URL', () => { 410 test('base64UrlEncode encodes bytes correctly', () => { 411 const input = new TextEncoder().encode('hello world'); 412 const encoded = base64UrlEncode(input); 413 assert.strictEqual(encoded, 'aGVsbG8gd29ybGQ'); 414 assert.ok(!encoded.includes('+')); 415 assert.ok(!encoded.includes('/')); 416 assert.ok(!encoded.includes('=')); 417 }); 418 419 test('base64UrlDecode decodes string correctly', () => { 420 const decoded = base64UrlDecode('aGVsbG8gd29ybGQ'); 421 const str = new TextDecoder().decode(decoded); 422 assert.strictEqual(str, 'hello world'); 423 }); 424 425 test('base64url roundtrip', () => { 426 const original = new Uint8Array([0, 1, 2, 255, 254, 253]); 427 const encoded = base64UrlEncode(original); 428 const decoded = base64UrlDecode(encoded); 429 assert.deepStrictEqual(decoded, original); 430 }); 431}); 432 433describe('JWT Creation', () => { 434 test('createAccessJwt creates valid JWT structure', async () => { 435 const did = 'did:web:test.example'; 436 const secret = 'test-secret-key'; 437 const jwt = await createAccessJwt(did, secret); 438 439 const parts = jwt.split('.'); 440 assert.strictEqual(parts.length, 3); 441 442 // Decode header 443 const header = JSON.parse( 444 new TextDecoder().decode(base64UrlDecode(parts[0])), 445 ); 446 assert.strictEqual(header.typ, 'at+jwt'); 447 assert.strictEqual(header.alg, 'HS256'); 448 449 // Decode payload 450 const payload = JSON.parse( 451 new TextDecoder().decode(base64UrlDecode(parts[1])), 452 ); 453 assert.strictEqual(payload.scope, 'com.atproto.access'); 454 assert.strictEqual(payload.sub, did); 455 assert.strictEqual(payload.aud, did); 456 assert.ok(payload.iat > 0); 457 assert.ok(payload.exp > payload.iat); 458 }); 459 460 test('createRefreshJwt creates valid JWT with jti', async () => { 461 const did = 'did:web:test.example'; 462 const secret = 'test-secret-key'; 463 const jwt = await createRefreshJwt(did, secret); 464 465 const parts = jwt.split('.'); 466 const header = JSON.parse( 467 new TextDecoder().decode(base64UrlDecode(parts[0])), 468 ); 469 assert.strictEqual(header.typ, 'refresh+jwt'); 470 471 const payload = JSON.parse( 472 new TextDecoder().decode(base64UrlDecode(parts[1])), 473 ); 474 assert.strictEqual(payload.scope, 'com.atproto.refresh'); 475 assert.ok(payload.jti); // has unique token ID 476 }); 477}); 478 479describe('JWT Verification', () => { 480 test('verifyAccessJwt returns payload for valid token', async () => { 481 const did = 'did:web:test.example'; 482 const secret = 'test-secret-key'; 483 const jwt = await createAccessJwt(did, secret); 484 485 const payload = await verifyAccessJwt(jwt, secret); 486 assert.strictEqual(payload.sub, did); 487 assert.strictEqual(payload.scope, 'com.atproto.access'); 488 }); 489 490 test('verifyAccessJwt throws for wrong secret', async () => { 491 const did = 'did:web:test.example'; 492 const jwt = await createAccessJwt(did, 'correct-secret'); 493 494 await assert.rejects( 495 () => verifyAccessJwt(jwt, 'wrong-secret'), 496 /invalid signature/i, 497 ); 498 }); 499 500 test('verifyAccessJwt throws for expired token', async () => { 501 const did = 'did:web:test.example'; 502 const secret = 'test-secret-key'; 503 // Create token that expired 1 second ago 504 const jwt = await createAccessJwt(did, secret, -1); 505 506 await assert.rejects(() => verifyAccessJwt(jwt, secret), /expired/i); 507 }); 508 509 test('verifyAccessJwt throws for refresh token', async () => { 510 const did = 'did:web:test.example'; 511 const secret = 'test-secret-key'; 512 const jwt = await createRefreshJwt(did, secret); 513 514 await assert.rejects( 515 () => verifyAccessJwt(jwt, secret), 516 /invalid token type/i, 517 ); 518 }); 519 520 test('verifyRefreshJwt returns payload for valid token', async () => { 521 const did = 'did:web:test.example'; 522 const secret = 'test-secret-key'; 523 const jwt = await createRefreshJwt(did, secret); 524 525 const payload = await verifyRefreshJwt(jwt, secret); 526 assert.strictEqual(payload.sub, did); 527 assert.strictEqual(payload.scope, 'com.atproto.refresh'); 528 assert.ok(payload.jti); // has token ID 529 }); 530 531 test('verifyRefreshJwt throws for wrong secret', async () => { 532 const did = 'did:web:test.example'; 533 const jwt = await createRefreshJwt(did, 'correct-secret'); 534 535 await assert.rejects( 536 () => verifyRefreshJwt(jwt, 'wrong-secret'), 537 /invalid signature/i, 538 ); 539 }); 540 541 test('verifyRefreshJwt throws for expired token', async () => { 542 const did = 'did:web:test.example'; 543 const secret = 'test-secret-key'; 544 // Create token that expired 1 second ago 545 const jwt = await createRefreshJwt(did, secret, -1); 546 547 await assert.rejects(() => verifyRefreshJwt(jwt, secret), /expired/i); 548 }); 549 550 test('verifyRefreshJwt throws for access token', async () => { 551 const did = 'did:web:test.example'; 552 const secret = 'test-secret-key'; 553 const jwt = await createAccessJwt(did, secret); 554 555 await assert.rejects( 556 () => verifyRefreshJwt(jwt, secret), 557 /invalid token type/i, 558 ); 559 }); 560 561 test('verifyAccessJwt throws for malformed JWT', async () => { 562 const secret = 'test-secret-key'; 563 564 // Not a JWT at all 565 await assert.rejects( 566 () => verifyAccessJwt('not-a-jwt', secret), 567 /Invalid JWT format/i, 568 ); 569 570 // Only two parts 571 await assert.rejects( 572 () => verifyAccessJwt('two.parts', secret), 573 /Invalid JWT format/i, 574 ); 575 576 // Four parts 577 await assert.rejects( 578 () => verifyAccessJwt('one.two.three.four', secret), 579 /Invalid JWT format/i, 580 ); 581 }); 582 583 test('verifyRefreshJwt throws for malformed JWT', async () => { 584 const secret = 'test-secret-key'; 585 586 await assert.rejects( 587 () => verifyRefreshJwt('not-a-jwt', secret), 588 /Invalid JWT format/i, 589 ); 590 591 await assert.rejects( 592 () => verifyRefreshJwt('two.parts', secret), 593 /Invalid JWT format/i, 594 ); 595 }); 596}); 597 598describe('MIME Type Sniffing', () => { 599 test('detects JPEG', () => { 600 const bytes = new Uint8Array([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10]); 601 assert.strictEqual(sniffMimeType(bytes), 'image/jpeg'); 602 }); 603 604 test('detects PNG', () => { 605 const bytes = new Uint8Array([ 606 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 607 ]); 608 assert.strictEqual(sniffMimeType(bytes), 'image/png'); 609 }); 610 611 test('detects GIF', () => { 612 const bytes = new Uint8Array([0x47, 0x49, 0x46, 0x38, 0x39, 0x61]); 613 assert.strictEqual(sniffMimeType(bytes), 'image/gif'); 614 }); 615 616 test('detects WebP', () => { 617 const bytes = new Uint8Array([ 618 0x52, 619 0x49, 620 0x46, 621 0x46, // RIFF 622 0x00, 623 0x00, 624 0x00, 625 0x00, // size (ignored) 626 0x57, 627 0x45, 628 0x42, 629 0x50, // WEBP 630 ]); 631 assert.strictEqual(sniffMimeType(bytes), 'image/webp'); 632 }); 633 634 test('detects MP4', () => { 635 const bytes = new Uint8Array([ 636 0x00, 637 0x00, 638 0x00, 639 0x18, // size 640 0x66, 641 0x74, 642 0x79, 643 0x70, // ftyp 644 0x69, 645 0x73, 646 0x6f, 647 0x6d, // isom brand 648 ]); 649 assert.strictEqual(sniffMimeType(bytes), 'video/mp4'); 650 }); 651 652 test('detects AVIF', () => { 653 const bytes = new Uint8Array([ 654 0x00, 655 0x00, 656 0x00, 657 0x1c, // size 658 0x66, 659 0x74, 660 0x79, 661 0x70, // ftyp 662 0x61, 663 0x76, 664 0x69, 665 0x66, // avif brand 666 ]); 667 assert.strictEqual(sniffMimeType(bytes), 'image/avif'); 668 }); 669 670 test('detects HEIC', () => { 671 const bytes = new Uint8Array([ 672 0x00, 673 0x00, 674 0x00, 675 0x18, // size 676 0x66, 677 0x74, 678 0x79, 679 0x70, // ftyp 680 0x68, 681 0x65, 682 0x69, 683 0x63, // heic brand 684 ]); 685 assert.strictEqual(sniffMimeType(bytes), 'image/heic'); 686 }); 687 688 test('returns null for unknown', () => { 689 const bytes = new Uint8Array([0x00, 0x01, 0x02, 0x03]); 690 assert.strictEqual(sniffMimeType(bytes), null); 691 }); 692}); 693 694describe('Blob Ref Detection', () => { 695 test('finds blob ref in simple object', () => { 696 const record = { 697 $type: 'app.bsky.feed.post', 698 text: 'Hello', 699 embed: { 700 $type: 'app.bsky.embed.images', 701 images: [ 702 { 703 image: { 704 $type: 'blob', 705 ref: { $link: 'bafkreiabc123' }, 706 mimeType: 'image/jpeg', 707 size: 1234, 708 }, 709 alt: 'test image', 710 }, 711 ], 712 }, 713 }; 714 const refs = findBlobRefs(record); 715 assert.deepStrictEqual(refs, ['bafkreiabc123']); 716 }); 717 718 test('finds multiple blob refs', () => { 719 const record = { 720 images: [ 721 { 722 image: { 723 $type: 'blob', 724 ref: { $link: 'cid1' }, 725 mimeType: 'image/png', 726 size: 100, 727 }, 728 }, 729 { 730 image: { 731 $type: 'blob', 732 ref: { $link: 'cid2' }, 733 mimeType: 'image/png', 734 size: 200, 735 }, 736 }, 737 ], 738 }; 739 const refs = findBlobRefs(record); 740 assert.deepStrictEqual(refs, ['cid1', 'cid2']); 741 }); 742 743 test('returns empty array when no blobs', () => { 744 const record = { text: 'Hello world', count: 42 }; 745 const refs = findBlobRefs(record); 746 assert.deepStrictEqual(refs, []); 747 }); 748 749 test('handles null and primitives', () => { 750 assert.deepStrictEqual(findBlobRefs(null), []); 751 assert.deepStrictEqual(findBlobRefs('string'), []); 752 assert.deepStrictEqual(findBlobRefs(42), []); 753 }); 754});