+126
-72
src/pds.js
+126
-72
src/pds.js
···
544
544
* @returns {Promise<Uint8Array>} CID bytes (36 bytes: version + codec + multihash)
545
545
*/
546
546
async function createCidWithCodec(bytes, codec) {
547
-
const hash = await crypto.subtle.digest('SHA-256', /** @type {BufferSource} */(bytes));
547
+
const hash = await crypto.subtle.digest(
548
+
'SHA-256',
549
+
/** @type {BufferSource} */ (bytes),
550
+
);
548
551
const hashBytes = new Uint8Array(hash);
549
552
550
553
// CIDv1: version(1) + codec + multihash(sha256)
···
672
675
673
676
return crypto.subtle.importKey(
674
677
'pkcs8',
675
-
/** @type {BufferSource} */(pkcs8),
678
+
/** @type {BufferSource} */ (pkcs8),
676
679
{ name: 'ECDSA', namedCurve: 'P-256' },
677
680
false,
678
681
['sign'],
···
689
692
const signature = await crypto.subtle.sign(
690
693
{ name: 'ECDSA', hash: 'SHA-256' },
691
694
privateKey,
692
-
/** @type {BufferSource} */(data),
695
+
/** @type {BufferSource} */ (data),
693
696
);
694
697
const sig = new Uint8Array(signature);
695
698
···
724
727
725
728
// Export private key as raw bytes
726
729
const privateJwk = await crypto.subtle.exportKey('jwk', keyPair.privateKey);
727
-
const privateBytes = base64UrlDecode(/** @type {string} */(privateJwk.d));
730
+
const privateBytes = base64UrlDecode(/** @type {string} */ (privateJwk.d));
728
731
729
732
// Export public key as compressed point
730
733
const publicRaw = await crypto.subtle.exportKey('raw', keyPair.publicKey);
···
764
767
async function hmacSign(data, secret) {
765
768
const key = await crypto.subtle.importKey(
766
769
'raw',
767
-
/** @type {BufferSource} */(new TextEncoder().encode(secret)),
770
+
/** @type {BufferSource} */ (new TextEncoder().encode(secret)),
768
771
{ name: 'HMAC', hash: 'SHA-256' },
769
772
false,
770
773
['sign'],
···
772
775
const sig = await crypto.subtle.sign(
773
776
'HMAC',
774
777
key,
775
-
/** @type {BufferSource} */(new TextEncoder().encode(data)),
778
+
/** @type {BufferSource} */ (new TextEncoder().encode(data)),
776
779
);
777
780
return base64UrlEncode(new Uint8Array(sig));
778
781
}
···
1558
1561
async getSigningKey() {
1559
1562
const hex = await this.state.storage.get('privateKey');
1560
1563
if (!hex) return null;
1561
-
return importPrivateKey(hexToBytes(/** @type {string} */(hex)));
1564
+
return importPrivateKey(hexToBytes(/** @type {string} */ (hex)));
1562
1565
}
1563
1566
1564
1567
/**
···
1576
1579
if (visited.has(cidStr)) return;
1577
1580
visited.add(cidStr);
1578
1581
1579
-
const rows = /** @type {BlockRow[]} */ (this.sql
1580
-
.exec(`SELECT data FROM blocks WHERE cid = ?`, cidStr)
1581
-
.toArray());
1582
+
const rows = /** @type {BlockRow[]} */ (
1583
+
this.sql.exec(`SELECT data FROM blocks WHERE cid = ?`, cidStr).toArray()
1584
+
);
1582
1585
if (rows.length === 0) return;
1583
1586
1584
1587
const data = new Uint8Array(rows[0].data);
···
1675
1678
const commit = {
1676
1679
did,
1677
1680
version: 3,
1678
-
data: new CID(cidToBytes(/** @type {string} */(dataRoot))), // CID wrapped for explicit encoding
1681
+
data: new CID(cidToBytes(/** @type {string} */ (dataRoot))), // CID wrapped for explicit encoding
1679
1682
rev,
1680
-
prev: prevCommit?.cid ? new CID(cidToBytes(/** @type {string} */(prevCommit.cid))) : null,
1683
+
prev: prevCommit?.cid
1684
+
? new CID(cidToBytes(/** @type {string} */ (prevCommit.cid)))
1685
+
: null,
1681
1686
};
1682
1687
1683
1688
// Sign commit (using dag-cbor encoder for CIDs)
···
1718
1723
// Add commit block
1719
1724
newBlocks.push({ cid: commitCidStr, data: signedBytes });
1720
1725
// Add MST node blocks (get all blocks referenced by commit.data)
1721
-
const mstBlocks = this.collectMstBlocks(/** @type {string} */(dataRoot));
1726
+
const mstBlocks = this.collectMstBlocks(/** @type {string} */ (dataRoot));
1722
1727
newBlocks.push(...mstBlocks);
1723
1728
1724
1729
// Sequence event with blocks - store complete event data including rev and time
···
1740
1745
);
1741
1746
1742
1747
// Broadcast to subscribers (both local and via default DO for relay)
1743
-
const evtRows = /** @type {SeqEventRow[]} */ (this.sql
1744
-
.exec(`SELECT * FROM seq_events ORDER BY seq DESC LIMIT 1`)
1745
-
.toArray());
1748
+
const evtRows = /** @type {SeqEventRow[]} */ (
1749
+
this.sql
1750
+
.exec(`SELECT * FROM seq_events ORDER BY seq DESC LIMIT 1`)
1751
+
.toArray()
1752
+
);
1746
1753
if (evtRows.length > 0) {
1747
1754
this.broadcastEvent(evtRows[0]);
1748
1755
// Also forward to default DO for relay subscribers
···
1760
1767
body: JSON.stringify({ ...row, evt: evtArray }),
1761
1768
}),
1762
1769
)
1763
-
.catch(() => { }); // Ignore forward errors
1770
+
.catch(() => {}); // Ignore forward errors
1764
1771
}
1765
1772
}
1766
1773
···
1824
1831
const commit = {
1825
1832
did,
1826
1833
version: 3,
1827
-
data: dataRoot ? new CID(cidToBytes(/** @type {string} */(dataRoot))) : null,
1834
+
data: dataRoot
1835
+
? new CID(cidToBytes(/** @type {string} */ (dataRoot)))
1836
+
: null,
1828
1837
rev,
1829
-
prev: prevCommit?.cid ? new CID(cidToBytes(/** @type {string} */(prevCommit.cid))) : null,
1838
+
prev: prevCommit?.cid
1839
+
? new CID(cidToBytes(/** @type {string} */ (prevCommit.cid)))
1840
+
: null,
1830
1841
};
1831
1842
1832
1843
// Sign commit
···
1863
1874
const newBlocks = [];
1864
1875
newBlocks.push({ cid: commitCidStr, data: signedBytes });
1865
1876
if (dataRoot) {
1866
-
const mstBlocks = this.collectMstBlocks(/** @type {string} */(dataRoot));
1877
+
const mstBlocks = this.collectMstBlocks(/** @type {string} */ (dataRoot));
1867
1878
newBlocks.push(...mstBlocks);
1868
1879
}
1869
1880
···
1883
1894
);
1884
1895
1885
1896
// Broadcast to subscribers
1886
-
const evtRows = /** @type {SeqEventRow[]} */ (this.sql
1887
-
.exec(`SELECT * FROM seq_events ORDER BY seq DESC LIMIT 1`)
1888
-
.toArray());
1897
+
const evtRows = /** @type {SeqEventRow[]} */ (
1898
+
this.sql
1899
+
.exec(`SELECT * FROM seq_events ORDER BY seq DESC LIMIT 1`)
1900
+
.toArray()
1901
+
);
1889
1902
if (evtRows.length > 0) {
1890
1903
this.broadcastEvent(evtRows[0]);
1891
1904
// Forward to default DO for relay subscribers
···
1901
1914
body: JSON.stringify({ ...row, evt: evtArray }),
1902
1915
}),
1903
1916
)
1904
-
.catch(() => { }); // Ignore forward errors
1917
+
.catch(() => {}); // Ignore forward errors
1905
1918
}
1906
1919
}
1907
1920
···
1920
1933
// Decode stored event to get ops, blocks, rev, and time
1921
1934
const evtData = cborDecode(new Uint8Array(evt.evt));
1922
1935
/** @type {Array<{action: string, path: string, cid: CID|null}>} */
1923
-
const ops = evtData.ops.map((/** @type {{action: string, path: string, cid?: string}} */ op) => ({
1924
-
...op,
1925
-
cid: op.cid ? new CID(cidToBytes(op.cid)) : null, // Wrap in CID class for tag 42 encoding
1926
-
}));
1936
+
const ops = evtData.ops.map(
1937
+
(/** @type {{action: string, path: string, cid?: string}} */ op) => ({
1938
+
...op,
1939
+
cid: op.cid ? new CID(cidToBytes(op.cid)) : null, // Wrap in CID class for tag 42 encoding
1940
+
}),
1941
+
);
1927
1942
// Get blocks from stored event (already in CAR format)
1928
1943
const blocks = evtData.blocks || new Uint8Array(0);
1929
1944
···
1991
2006
if (!did) {
1992
2007
return new Response('User not found', { status: 404 });
1993
2008
}
1994
-
return new Response(/** @type {string} */(did), { headers: { 'Content-Type': 'text/plain' } });
2009
+
return new Response(/** @type {string} */ (did), {
2010
+
headers: { 'Content-Type': 'text/plain' },
2011
+
});
1995
2012
}
1996
2013
1997
2014
/** @param {Request} request */
···
2282
2299
// Check instance's own handle
2283
2300
const instanceDid = await this.getDid();
2284
2301
if (instanceDid === did) {
2285
-
return /** @type {string|null} */ (await this.state.storage.get('handle'));
2302
+
return /** @type {string|null} */ (
2303
+
await this.state.storage.get('handle')
2304
+
);
2286
2305
}
2287
2306
return null;
2288
2307
}
···
2372
2391
const did = await this.getDid();
2373
2392
const repos = did
2374
2393
? [{ did, head: null, rev: null }]
2375
-
: registeredDids.map((/** @type {string} */ d) => ({ did: d, head: null, rev: null }));
2394
+
: registeredDids.map((/** @type {string} */ d) => ({
2395
+
did: d,
2396
+
head: null,
2397
+
rev: null,
2398
+
}));
2376
2399
return Response.json({ repos });
2377
2400
}
2378
2401
···
2521
2544
}
2522
2545
const did = await this.getDid();
2523
2546
const uri = `at://${did}/${collection}/${rkey}`;
2524
-
const rows = /** @type {RecordRow[]} */ (this.sql
2525
-
.exec(`SELECT cid, value FROM records WHERE uri = ?`, uri)
2526
-
.toArray());
2547
+
const rows = /** @type {RecordRow[]} */ (
2548
+
this.sql
2549
+
.exec(`SELECT cid, value FROM records WHERE uri = ?`, uri)
2550
+
.toArray()
2551
+
);
2527
2552
if (rows.length === 0) {
2528
2553
return errorResponse('RecordNotFound', 'record not found', 404);
2529
2554
}
···
2570
2595
const query = `SELECT uri, cid, value FROM records WHERE collection = ? ORDER BY rkey ${reverse ? 'DESC' : 'ASC'} LIMIT ?`;
2571
2596
const params = [collection, limit + 1];
2572
2597
2573
-
const rows = /** @type {RecordRow[]} */ (this.sql.exec(query, ...params).toArray());
2598
+
const rows = /** @type {RecordRow[]} */ (
2599
+
this.sql.exec(query, ...params).toArray()
2600
+
);
2574
2601
const hasMore = rows.length > limit;
2575
2602
const records = rows.slice(0, limit).map((r) => ({
2576
2603
uri: r.uri,
···
2585
2612
}
2586
2613
2587
2614
handleGetLatestCommit() {
2588
-
const commits = /** @type {CommitRow[]} */ (this.sql
2589
-
.exec(`SELECT cid, rev FROM commits ORDER BY seq DESC LIMIT 1`)
2590
-
.toArray());
2615
+
const commits = /** @type {CommitRow[]} */ (
2616
+
this.sql
2617
+
.exec(`SELECT cid, rev FROM commits ORDER BY seq DESC LIMIT 1`)
2618
+
.toArray()
2619
+
);
2591
2620
if (commits.length === 0) {
2592
2621
return errorResponse('RepoNotFound', 'repo not found', 404);
2593
2622
}
···
2596
2625
2597
2626
async handleGetRepoStatus() {
2598
2627
const did = await this.getDid();
2599
-
const commits = /** @type {CommitRow[]} */ (this.sql
2600
-
.exec(`SELECT cid, rev FROM commits ORDER BY seq DESC LIMIT 1`)
2601
-
.toArray());
2628
+
const commits = /** @type {CommitRow[]} */ (
2629
+
this.sql
2630
+
.exec(`SELECT cid, rev FROM commits ORDER BY seq DESC LIMIT 1`)
2631
+
.toArray()
2632
+
);
2602
2633
if (commits.length === 0 || !did) {
2603
2634
return errorResponse('RepoNotFound', 'repo not found', 404);
2604
2635
}
···
2611
2642
}
2612
2643
2613
2644
handleGetRepo() {
2614
-
const commits = /** @type {CommitRow[]} */ (this.sql
2615
-
.exec(`SELECT cid FROM commits ORDER BY seq DESC LIMIT 1`)
2616
-
.toArray());
2645
+
const commits = /** @type {CommitRow[]} */ (
2646
+
this.sql
2647
+
.exec(`SELECT cid FROM commits ORDER BY seq DESC LIMIT 1`)
2648
+
.toArray()
2649
+
);
2617
2650
if (commits.length === 0) {
2618
2651
return errorResponse('RepoNotFound', 'repo not found', 404);
2619
2652
}
···
2625
2658
// Helper to get block data
2626
2659
/** @param {string} cid */
2627
2660
const getBlock = (cid) => {
2628
-
const rows = /** @type {BlockRow[]} */ (this.sql
2629
-
.exec(`SELECT data FROM blocks WHERE cid = ?`, cid)
2630
-
.toArray());
2661
+
const rows = /** @type {BlockRow[]} */ (
2662
+
this.sql.exec(`SELECT data FROM blocks WHERE cid = ?`, cid).toArray()
2663
+
);
2631
2664
return rows.length > 0 ? new Uint8Array(rows[0].data) : null;
2632
2665
};
2633
2666
···
2680
2713
}
2681
2714
2682
2715
const car = buildCarFile(commitCid, blocksForCar);
2683
-
return new Response(/** @type {BodyInit} */(car), {
2716
+
return new Response(/** @type {BodyInit} */ (car), {
2684
2717
headers: { 'content-type': 'application/vnd.ipld.car' },
2685
2718
});
2686
2719
}
···
2694
2727
}
2695
2728
const did = await this.getDid();
2696
2729
const uri = `at://${did}/${collection}/${rkey}`;
2697
-
const rows = /** @type {RecordRow[]} */ (this.sql
2698
-
.exec(`SELECT cid FROM records WHERE uri = ?`, uri)
2699
-
.toArray());
2730
+
const rows = /** @type {RecordRow[]} */ (
2731
+
this.sql.exec(`SELECT cid FROM records WHERE uri = ?`, uri).toArray()
2732
+
);
2700
2733
if (rows.length === 0) {
2701
2734
return errorResponse('RecordNotFound', 'record not found', 404);
2702
2735
}
2703
2736
const recordCid = rows[0].cid;
2704
2737
2705
2738
// Get latest commit
2706
-
const commits = /** @type {CommitRow[]} */ (this.sql
2707
-
.exec(`SELECT cid FROM commits ORDER BY seq DESC LIMIT 1`)
2708
-
.toArray());
2739
+
const commits = /** @type {CommitRow[]} */ (
2740
+
this.sql
2741
+
.exec(`SELECT cid FROM commits ORDER BY seq DESC LIMIT 1`)
2742
+
.toArray()
2743
+
);
2709
2744
if (commits.length === 0) {
2710
2745
return errorResponse('RepoNotFound', 'no commits', 404);
2711
2746
}
···
2721
2756
const addBlock = (cidStr) => {
2722
2757
if (included.has(cidStr)) return;
2723
2758
included.add(cidStr);
2724
-
const blockRows = /** @type {BlockRow[]} */ (this.sql
2725
-
.exec(`SELECT data FROM blocks WHERE cid = ?`, cidStr)
2726
-
.toArray());
2759
+
const blockRows = /** @type {BlockRow[]} */ (
2760
+
this.sql.exec(`SELECT data FROM blocks WHERE cid = ?`, cidStr).toArray()
2761
+
);
2727
2762
if (blockRows.length > 0) {
2728
2763
blocks.push({ cid: cidStr, data: new Uint8Array(blockRows[0].data) });
2729
2764
}
···
2733
2768
addBlock(commitCid);
2734
2769
2735
2770
// Get commit to find data root
2736
-
const commitRows = /** @type {BlockRow[]} */ (this.sql
2737
-
.exec(`SELECT data FROM blocks WHERE cid = ?`, commitCid)
2738
-
.toArray());
2771
+
const commitRows = /** @type {BlockRow[]} */ (
2772
+
this.sql
2773
+
.exec(`SELECT data FROM blocks WHERE cid = ?`, commitCid)
2774
+
.toArray()
2775
+
);
2739
2776
if (commitRows.length > 0) {
2740
2777
const commit = cborDecode(new Uint8Array(commitRows[0].data));
2741
2778
if (commit.data) {
···
2752
2789
addBlock(recordCid);
2753
2790
2754
2791
const car = buildCarFile(commitCid, blocks);
2755
-
return new Response(/** @type {BodyInit} */(car), {
2792
+
return new Response(/** @type {BodyInit} */ (car), {
2756
2793
headers: { 'content-type': 'application/vnd.ipld.car' },
2757
2794
});
2758
2795
}
···
2797
2834
2798
2835
// Check size limits
2799
2836
if (size === 0) {
2800
-
return errorResponse('InvalidRequest', 'Empty blobs are not allowed', 400);
2837
+
return errorResponse(
2838
+
'InvalidRequest',
2839
+
'Empty blobs are not allowed',
2840
+
400,
2841
+
);
2801
2842
}
2802
2843
const MAX_BLOB_SIZE = 50 * 1024 * 1024;
2803
2844
if (size > MAX_BLOB_SIZE) {
···
2851
2892
const cid = url.searchParams.get('cid');
2852
2893
2853
2894
if (!did || !cid) {
2854
-
return errorResponse('InvalidRequest', 'missing did or cid parameter', 400);
2895
+
return errorResponse(
2896
+
'InvalidRequest',
2897
+
'missing did or cid parameter',
2898
+
400,
2899
+
);
2855
2900
}
2856
2901
2857
2902
// Validate CID format (CIDv1 base32lower: starts with 'b', 59 chars total)
···
2862
2907
// Verify DID matches this DO
2863
2908
const myDid = await this.getDid();
2864
2909
if (did !== myDid) {
2865
-
return errorResponse('InvalidRequest', 'DID does not match this repo', 400);
2910
+
return errorResponse(
2911
+
'InvalidRequest',
2912
+
'DID does not match this repo',
2913
+
400,
2914
+
);
2866
2915
}
2867
2916
2868
2917
// Look up blob metadata
···
2909
2958
// Verify DID matches this DO
2910
2959
const myDid = await this.getDid();
2911
2960
if (did !== myDid) {
2912
-
return errorResponse('InvalidRequest', 'DID does not match this repo', 400);
2961
+
return errorResponse(
2962
+
'InvalidRequest',
2963
+
'DID does not match this repo',
2964
+
400,
2965
+
);
2913
2966
}
2914
2967
2915
2968
// Query blobs with pagination (cursor is createdAt::cid for uniqueness)
···
2954
3007
this.state.acceptWebSocket(server);
2955
3008
const cursor = url.searchParams.get('cursor');
2956
3009
if (cursor) {
2957
-
const events = /** @type {SeqEventRow[]} */ (this.sql
2958
-
.exec(
2959
-
`SELECT * FROM seq_events WHERE seq > ? ORDER BY seq`,
2960
-
parseInt(cursor, 10),
2961
-
)
2962
-
.toArray());
3010
+
const events = /** @type {SeqEventRow[]} */ (
3011
+
this.sql
3012
+
.exec(
3013
+
`SELECT * FROM seq_events WHERE seq > ? ORDER BY seq`,
3014
+
parseInt(cursor, 10),
3015
+
)
3016
+
.toArray()
3017
+
);
2963
3018
for (const evt of events) {
2964
3019
server.send(this.formatEvent(evt));
2965
3020
}
···
3018
3073
await this.env?.BLOBS?.delete(`${did}/${cid}`);
3019
3074
this.sql.exec('DELETE FROM blob WHERE cid = ?', cid);
3020
3075
}
3021
-
3022
3076
}
3023
3077
}
3024
3078
+5
-5
test/e2e.sh
+5
-5
test/e2e.sh
···
211
211
# Create a minimal valid PNG (1x1 transparent pixel)
212
212
# PNG signature + IHDR + IDAT + IEND
213
213
PNG_FILE=$(mktemp)
214
-
printf '\x89PNG\r\n\x1a\n' > "$PNG_FILE"
215
-
printf '\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89' >> "$PNG_FILE"
216
-
printf '\x00\x00\x00\nIDATx\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4' >> "$PNG_FILE"
217
-
printf '\x00\x00\x00\x00IEND\xaeB`\x82' >> "$PNG_FILE"
214
+
printf '\x89PNG\r\n\x1a\n' >"$PNG_FILE"
215
+
printf '\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89' >>"$PNG_FILE"
216
+
printf '\x00\x00\x00\nIDATx\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4' >>"$PNG_FILE"
217
+
printf '\x00\x00\x00\x00IEND\xaeB`\x82' >>"$PNG_FILE"
218
218
219
219
# uploadBlob requires auth
220
220
STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE/xrpc/com.atproto.repo.uploadBlob" \
···
261
261
BLOB_POST=$(curl -sf -X POST "$BASE/xrpc/com.atproto.repo.createRecord" \
262
262
-H "Authorization: Bearer $TOKEN" \
263
263
-H "Content-Type: application/json" \
264
-
-d "{\"repo\":\"$DID\",\"collection\":\"app.bsky.feed.post\",\"record\":{\"text\":\"post with image\",\"createdAt\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"embed\":{\"\$type\":\"app.bsky.embed.images\",\"images\":[{\"image\":{\"\$type\":\"blob\",\"ref\":{\"\$link\":\"$BLOB_CID\"},\"mimeType\":\"image/png\",\"size\":$(wc -c < "$PNG_FILE")},\"alt\":\"test\"}]}}}")
264
+
-d "{\"repo\":\"$DID\",\"collection\":\"app.bsky.feed.post\",\"record\":{\"text\":\"post with image\",\"createdAt\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"embed\":{\"\$type\":\"app.bsky.embed.images\",\"images\":[{\"image\":{\"\$type\":\"blob\",\"ref\":{\"\$link\":\"$BLOB_CID\"},\"mimeType\":\"image/png\",\"size\":$(wc -c <"$PNG_FILE")},\"alt\":\"test\"}]}}}")
265
265
BLOB_POST_URI=$(echo "$BLOB_POST" | jq -r '.uri')
266
266
BLOB_POST_RKEY=$(echo "$BLOB_POST_URI" | sed 's|.*/||')
267
267
[ "$BLOB_POST_URI" != "null" ] && [ -n "$BLOB_POST_URI" ] && pass "createRecord with blob ref" || fail "createRecord with blob"
+48
-12
test/pds.test.js
+48
-12
test/pds.test.js
···
615
615
616
616
test('detects WebP', () => {
617
617
const bytes = new Uint8Array([
618
-
0x52, 0x49, 0x46, 0x46, // RIFF
619
-
0x00, 0x00, 0x00, 0x00, // size (ignored)
620
-
0x57, 0x45, 0x42, 0x50, // WEBP
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
621
630
]);
622
631
assert.strictEqual(sniffMimeType(bytes), 'image/webp');
623
632
});
624
633
625
634
test('detects MP4', () => {
626
635
const bytes = new Uint8Array([
627
-
0x00, 0x00, 0x00, 0x18, // size
628
-
0x66, 0x74, 0x79, 0x70, // ftyp
629
-
0x69, 0x73, 0x6f, 0x6d, // isom brand
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
630
648
]);
631
649
assert.strictEqual(sniffMimeType(bytes), 'video/mp4');
632
650
});
633
651
634
652
test('detects AVIF', () => {
635
653
const bytes = new Uint8Array([
636
-
0x00, 0x00, 0x00, 0x1c, // size
637
-
0x66, 0x74, 0x79, 0x70, // ftyp
638
-
0x61, 0x76, 0x69, 0x66, // avif brand
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
639
666
]);
640
667
assert.strictEqual(sniffMimeType(bytes), 'image/avif');
641
668
});
642
669
643
670
test('detects HEIC', () => {
644
671
const bytes = new Uint8Array([
645
-
0x00, 0x00, 0x00, 0x18, // size
646
-
0x66, 0x74, 0x79, 0x70, // ftyp
647
-
0x68, 0x65, 0x69, 0x63, // heic brand
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
648
684
]);
649
685
assert.strictEqual(sniffMimeType(bytes), 'image/heic');
650
686
});