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});