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