this repo has no description
1// === CBOR ENCODING ===
2// Minimal deterministic CBOR (RFC 8949) - sorted keys, minimal integers
3
4function cborEncode(value) {
5 const parts = []
6
7 function encode(val) {
8 if (val === null) {
9 parts.push(0xf6) // null
10 } else if (val === true) {
11 parts.push(0xf5) // true
12 } else if (val === false) {
13 parts.push(0xf4) // false
14 } else if (typeof val === 'number') {
15 encodeInteger(val)
16 } else if (typeof val === 'string') {
17 const bytes = new TextEncoder().encode(val)
18 encodeHead(3, bytes.length) // major type 3 = text string
19 parts.push(...bytes)
20 } else if (val instanceof Uint8Array) {
21 encodeHead(2, val.length) // major type 2 = byte string
22 parts.push(...val)
23 } else if (Array.isArray(val)) {
24 encodeHead(4, val.length) // major type 4 = array
25 for (const item of val) encode(item)
26 } else if (typeof val === 'object') {
27 // Sort keys for deterministic encoding
28 const keys = Object.keys(val).sort()
29 encodeHead(5, keys.length) // major type 5 = map
30 for (const key of keys) {
31 encode(key)
32 encode(val[key])
33 }
34 }
35 }
36
37 function encodeHead(majorType, length) {
38 const mt = majorType << 5
39 if (length < 24) {
40 parts.push(mt | length)
41 } else if (length < 256) {
42 parts.push(mt | 24, length)
43 } else if (length < 65536) {
44 parts.push(mt | 25, length >> 8, length & 0xff)
45 } else if (length < 4294967296) {
46 parts.push(mt | 26, (length >> 24) & 0xff, (length >> 16) & 0xff, (length >> 8) & 0xff, length & 0xff)
47 }
48 }
49
50 function encodeInteger(n) {
51 if (n >= 0) {
52 encodeHead(0, n) // major type 0 = unsigned int
53 } else {
54 encodeHead(1, -n - 1) // major type 1 = negative int
55 }
56 }
57
58 encode(value)
59 return new Uint8Array(parts)
60}
61
62function cborDecode(bytes) {
63 let offset = 0
64
65 function read() {
66 const initial = bytes[offset++]
67 const major = initial >> 5
68 const info = initial & 0x1f
69
70 let length = info
71 if (info === 24) length = bytes[offset++]
72 else if (info === 25) { length = (bytes[offset++] << 8) | bytes[offset++] }
73 else if (info === 26) {
74 length = (bytes[offset++] << 24) | (bytes[offset++] << 16) | (bytes[offset++] << 8) | bytes[offset++]
75 }
76
77 switch (major) {
78 case 0: return length // unsigned int
79 case 1: return -1 - length // negative int
80 case 2: { // byte string
81 const data = bytes.slice(offset, offset + length)
82 offset += length
83 return data
84 }
85 case 3: { // text string
86 const data = new TextDecoder().decode(bytes.slice(offset, offset + length))
87 offset += length
88 return data
89 }
90 case 4: { // array
91 const arr = []
92 for (let i = 0; i < length; i++) arr.push(read())
93 return arr
94 }
95 case 5: { // map
96 const obj = {}
97 for (let i = 0; i < length; i++) {
98 const key = read()
99 obj[key] = read()
100 }
101 return obj
102 }
103 case 7: { // special
104 if (info === 20) return false
105 if (info === 21) return true
106 if (info === 22) return null
107 return undefined
108 }
109 }
110 }
111
112 return read()
113}
114
115// === CID GENERATION ===
116// dag-cbor (0x71) + sha-256 (0x12) + 32 bytes
117
118async function createCid(bytes) {
119 const hash = await crypto.subtle.digest('SHA-256', bytes)
120 const hashBytes = new Uint8Array(hash)
121
122 // CIDv1: version(1) + codec(dag-cbor=0x71) + multihash(sha256)
123 // Multihash: hash-type(0x12) + length(0x20=32) + digest
124 const cid = new Uint8Array(2 + 2 + 32)
125 cid[0] = 0x01 // CIDv1
126 cid[1] = 0x71 // dag-cbor codec
127 cid[2] = 0x12 // sha-256
128 cid[3] = 0x20 // 32 bytes
129 cid.set(hashBytes, 4)
130
131 return cid
132}
133
134function cidToString(cid) {
135 // base32lower encoding for CIDv1
136 return 'b' + base32Encode(cid)
137}
138
139function base32Encode(bytes) {
140 const alphabet = 'abcdefghijklmnopqrstuvwxyz234567'
141 let result = ''
142 let bits = 0
143 let value = 0
144
145 for (const byte of bytes) {
146 value = (value << 8) | byte
147 bits += 8
148 while (bits >= 5) {
149 bits -= 5
150 result += alphabet[(value >> bits) & 31]
151 }
152 }
153
154 if (bits > 0) {
155 result += alphabet[(value << (5 - bits)) & 31]
156 }
157
158 return result
159}
160
161// === TID GENERATION ===
162// Timestamp-based IDs: base32-sort encoded microseconds + clock ID
163
164const TID_CHARS = '234567abcdefghijklmnopqrstuvwxyz'
165let lastTimestamp = 0
166let clockId = Math.floor(Math.random() * 1024)
167
168function createTid() {
169 let timestamp = Date.now() * 1000 // microseconds
170
171 // Ensure monotonic
172 if (timestamp <= lastTimestamp) {
173 timestamp = lastTimestamp + 1
174 }
175 lastTimestamp = timestamp
176
177 // 13 chars: 11 for timestamp (64 bits but only ~53 used), 2 for clock ID
178 let tid = ''
179
180 // Encode timestamp (high bits first for sortability)
181 let ts = timestamp
182 for (let i = 0; i < 11; i++) {
183 tid = TID_CHARS[ts & 31] + tid
184 ts = Math.floor(ts / 32)
185 }
186
187 // Append clock ID (2 chars)
188 tid += TID_CHARS[(clockId >> 5) & 31]
189 tid += TID_CHARS[clockId & 31]
190
191 return tid
192}
193
194// === P-256 SIGNING ===
195// Web Crypto ECDSA with P-256 curve
196
197async function importPrivateKey(privateKeyBytes) {
198 // PKCS#8 wrapper for raw P-256 private key
199 const pkcs8Prefix = new Uint8Array([
200 0x30, 0x41, 0x02, 0x01, 0x00, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48,
201 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03,
202 0x01, 0x07, 0x04, 0x27, 0x30, 0x25, 0x02, 0x01, 0x01, 0x04, 0x20
203 ])
204
205 const pkcs8 = new Uint8Array(pkcs8Prefix.length + 32)
206 pkcs8.set(pkcs8Prefix)
207 pkcs8.set(privateKeyBytes, pkcs8Prefix.length)
208
209 return crypto.subtle.importKey(
210 'pkcs8',
211 pkcs8,
212 { name: 'ECDSA', namedCurve: 'P-256' },
213 false,
214 ['sign']
215 )
216}
217
218async function sign(privateKey, data) {
219 const signature = await crypto.subtle.sign(
220 { name: 'ECDSA', hash: 'SHA-256' },
221 privateKey,
222 data
223 )
224 return new Uint8Array(signature)
225}
226
227async function generateKeyPair() {
228 const keyPair = await crypto.subtle.generateKey(
229 { name: 'ECDSA', namedCurve: 'P-256' },
230 true,
231 ['sign', 'verify']
232 )
233
234 // Export private key as raw bytes
235 const privateJwk = await crypto.subtle.exportKey('jwk', keyPair.privateKey)
236 const privateBytes = base64UrlDecode(privateJwk.d)
237
238 // Export public key as compressed point
239 const publicRaw = await crypto.subtle.exportKey('raw', keyPair.publicKey)
240 const publicBytes = new Uint8Array(publicRaw)
241 const compressed = compressPublicKey(publicBytes)
242
243 return { privateKey: privateBytes, publicKey: compressed }
244}
245
246function compressPublicKey(uncompressed) {
247 // uncompressed is 65 bytes: 0x04 + x(32) + y(32)
248 // compressed is 33 bytes: prefix(02 or 03) + x(32)
249 const x = uncompressed.slice(1, 33)
250 const y = uncompressed.slice(33, 65)
251 const prefix = (y[31] & 1) === 0 ? 0x02 : 0x03
252 const compressed = new Uint8Array(33)
253 compressed[0] = prefix
254 compressed.set(x, 1)
255 return compressed
256}
257
258function base64UrlDecode(str) {
259 const base64 = str.replace(/-/g, '+').replace(/_/g, '/')
260 const binary = atob(base64)
261 const bytes = new Uint8Array(binary.length)
262 for (let i = 0; i < binary.length; i++) {
263 bytes[i] = binary.charCodeAt(i)
264 }
265 return bytes
266}
267
268function bytesToHex(bytes) {
269 return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')
270}
271
272function hexToBytes(hex) {
273 const bytes = new Uint8Array(hex.length / 2)
274 for (let i = 0; i < hex.length; i += 2) {
275 bytes[i / 2] = parseInt(hex.substr(i, 2), 16)
276 }
277 return bytes
278}
279
280// === MERKLE SEARCH TREE ===
281// Simple rebuild-on-write implementation
282
283async function sha256(data) {
284 const hash = await crypto.subtle.digest('SHA-256', data)
285 return new Uint8Array(hash)
286}
287
288function getKeyDepth(key) {
289 // Count leading zeros in hash to determine tree depth
290 const keyBytes = new TextEncoder().encode(key)
291 // Sync hash for depth calculation (use first bytes of key as proxy)
292 let zeros = 0
293 for (const byte of keyBytes) {
294 if (byte === 0) zeros += 8
295 else {
296 for (let i = 7; i >= 0; i--) {
297 if ((byte >> i) & 1) break
298 zeros++
299 }
300 break
301 }
302 }
303 return Math.floor(zeros / 4)
304}
305
306class MST {
307 constructor(sql) {
308 this.sql = sql
309 }
310
311 async computeRoot() {
312 const records = this.sql.exec(`
313 SELECT collection, rkey, cid FROM records ORDER BY collection, rkey
314 `).toArray()
315
316 if (records.length === 0) {
317 return null
318 }
319
320 const entries = records.map(r => ({
321 key: `${r.collection}/${r.rkey}`,
322 cid: r.cid
323 }))
324
325 return this.buildTree(entries, 0)
326 }
327
328 async buildTree(entries, depth) {
329 if (entries.length === 0) return null
330
331 const node = { l: null, e: [] }
332 let leftEntries = []
333
334 for (const entry of entries) {
335 const keyDepth = getKeyDepth(entry.key)
336
337 if (keyDepth > depth) {
338 leftEntries.push(entry)
339 } else {
340 // Store accumulated left entries
341 if (leftEntries.length > 0) {
342 const leftCid = await this.buildTree(leftEntries, depth + 1)
343 if (node.e.length === 0) {
344 node.l = leftCid
345 } else {
346 node.e[node.e.length - 1].t = leftCid
347 }
348 leftEntries = []
349 }
350 node.e.push({ k: entry.key, v: entry.cid, t: null })
351 }
352 }
353
354 // Handle remaining left entries
355 if (leftEntries.length > 0) {
356 const leftCid = await this.buildTree(leftEntries, depth + 1)
357 if (node.e.length > 0) {
358 node.e[node.e.length - 1].t = leftCid
359 } else {
360 node.l = leftCid
361 }
362 }
363
364 // Encode and store node
365 const nodeBytes = cborEncode(node)
366 const nodeCid = await createCid(nodeBytes)
367 const cidStr = cidToString(nodeCid)
368
369 this.sql.exec(
370 `INSERT OR REPLACE INTO blocks (cid, data) VALUES (?, ?)`,
371 cidStr,
372 nodeBytes
373 )
374
375 return cidStr
376 }
377}
378
379// === CAR FILE BUILDER ===
380
381function varint(n) {
382 const bytes = []
383 while (n >= 0x80) {
384 bytes.push((n & 0x7f) | 0x80)
385 n >>>= 7
386 }
387 bytes.push(n)
388 return new Uint8Array(bytes)
389}
390
391function cidToBytes(cidStr) {
392 // Decode base32lower CID string to bytes
393 if (!cidStr.startsWith('b')) throw new Error('expected base32lower CID')
394 return base32Decode(cidStr.slice(1))
395}
396
397function base32Decode(str) {
398 const alphabet = 'abcdefghijklmnopqrstuvwxyz234567'
399 let bits = 0
400 let value = 0
401 const output = []
402
403 for (const char of str) {
404 const idx = alphabet.indexOf(char)
405 if (idx === -1) continue
406 value = (value << 5) | idx
407 bits += 5
408 if (bits >= 8) {
409 bits -= 8
410 output.push((value >> bits) & 0xff)
411 }
412 }
413
414 return new Uint8Array(output)
415}
416
417function buildCarFile(rootCid, blocks) {
418 const parts = []
419
420 // Header: { version: 1, roots: [rootCid] }
421 const rootCidBytes = cidToBytes(rootCid)
422 const header = cborEncode({ version: 1, roots: [rootCidBytes] })
423 parts.push(varint(header.length))
424 parts.push(header)
425
426 // Blocks: varint(len) + cid + data
427 for (const block of blocks) {
428 const cidBytes = cidToBytes(block.cid)
429 const blockLen = cidBytes.length + block.data.length
430 parts.push(varint(blockLen))
431 parts.push(cidBytes)
432 parts.push(block.data)
433 }
434
435 // Concatenate all parts
436 const totalLen = parts.reduce((sum, p) => sum + p.length, 0)
437 const car = new Uint8Array(totalLen)
438 let offset = 0
439 for (const part of parts) {
440 car.set(part, offset)
441 offset += part.length
442 }
443
444 return car
445}
446
447export class PersonalDataServer {
448 constructor(state, env) {
449 this.state = state
450 this.sql = state.storage.sql
451 this.env = env
452
453 // Initialize schema
454 this.sql.exec(`
455 CREATE TABLE IF NOT EXISTS blocks (
456 cid TEXT PRIMARY KEY,
457 data BLOB NOT NULL
458 );
459
460 CREATE TABLE IF NOT EXISTS records (
461 uri TEXT PRIMARY KEY,
462 cid TEXT NOT NULL,
463 collection TEXT NOT NULL,
464 rkey TEXT NOT NULL,
465 value BLOB NOT NULL
466 );
467
468 CREATE TABLE IF NOT EXISTS commits (
469 seq INTEGER PRIMARY KEY AUTOINCREMENT,
470 cid TEXT NOT NULL,
471 rev TEXT NOT NULL,
472 prev TEXT
473 );
474
475 CREATE TABLE IF NOT EXISTS seq_events (
476 seq INTEGER PRIMARY KEY AUTOINCREMENT,
477 did TEXT NOT NULL,
478 commit_cid TEXT NOT NULL,
479 evt BLOB NOT NULL
480 );
481
482 CREATE INDEX IF NOT EXISTS idx_records_collection ON records(collection, rkey);
483 `)
484 }
485
486 async initIdentity(did, privateKeyHex, handle = null) {
487 await this.state.storage.put('did', did)
488 await this.state.storage.put('privateKey', privateKeyHex)
489 if (handle) {
490 await this.state.storage.put('handle', handle)
491 }
492 }
493
494 async getDid() {
495 if (!this._did) {
496 this._did = await this.state.storage.get('did')
497 }
498 return this._did
499 }
500
501 async getHandle() {
502 return this.state.storage.get('handle')
503 }
504
505 async getSigningKey() {
506 const hex = await this.state.storage.get('privateKey')
507 if (!hex) return null
508 return importPrivateKey(hexToBytes(hex))
509 }
510
511 async createRecord(collection, record, rkey = null) {
512 const did = await this.getDid()
513 if (!did) throw new Error('PDS not initialized')
514
515 rkey = rkey || createTid()
516 const uri = `at://${did}/${collection}/${rkey}`
517
518 // Encode and hash record
519 const recordBytes = cborEncode(record)
520 const recordCid = await createCid(recordBytes)
521 const recordCidStr = cidToString(recordCid)
522
523 // Store block
524 this.sql.exec(
525 `INSERT OR REPLACE INTO blocks (cid, data) VALUES (?, ?)`,
526 recordCidStr, recordBytes
527 )
528
529 // Store record index
530 this.sql.exec(
531 `INSERT OR REPLACE INTO records (uri, cid, collection, rkey, value) VALUES (?, ?, ?, ?, ?)`,
532 uri, recordCidStr, collection, rkey, recordBytes
533 )
534
535 // Rebuild MST
536 const mst = new MST(this.sql)
537 const dataRoot = await mst.computeRoot()
538
539 // Get previous commit
540 const prevCommits = this.sql.exec(
541 `SELECT cid, rev FROM commits ORDER BY seq DESC LIMIT 1`
542 ).toArray()
543 const prevCommit = prevCommits.length > 0 ? prevCommits[0] : null
544
545 // Create commit
546 const rev = createTid()
547 const commit = {
548 did,
549 version: 3,
550 data: dataRoot,
551 rev,
552 prev: prevCommit?.cid || null
553 }
554
555 // Sign commit
556 const commitBytes = cborEncode(commit)
557 const signingKey = await this.getSigningKey()
558 const sig = await sign(signingKey, commitBytes)
559
560 const signedCommit = { ...commit, sig }
561 const signedBytes = cborEncode(signedCommit)
562 const commitCid = await createCid(signedBytes)
563 const commitCidStr = cidToString(commitCid)
564
565 // Store commit block
566 this.sql.exec(
567 `INSERT OR REPLACE INTO blocks (cid, data) VALUES (?, ?)`,
568 commitCidStr, signedBytes
569 )
570
571 // Store commit reference
572 this.sql.exec(
573 `INSERT INTO commits (cid, rev, prev) VALUES (?, ?, ?)`,
574 commitCidStr, rev, prevCommit?.cid || null
575 )
576
577 // Sequence event
578 const evt = cborEncode({
579 ops: [{ action: 'create', path: `${collection}/${rkey}`, cid: recordCidStr }]
580 })
581 this.sql.exec(
582 `INSERT INTO seq_events (did, commit_cid, evt) VALUES (?, ?, ?)`,
583 did, commitCidStr, evt
584 )
585
586 // Broadcast to subscribers
587 const evtRows = this.sql.exec(
588 `SELECT * FROM seq_events ORDER BY seq DESC LIMIT 1`
589 ).toArray()
590 if (evtRows.length > 0) {
591 this.broadcastEvent(evtRows[0])
592 }
593
594 return { uri, cid: recordCidStr, commit: commitCidStr }
595 }
596
597 formatEvent(evt) {
598 // AT Protocol frame format: header + body
599 const header = cborEncode({ op: 1, t: '#commit' })
600 const body = cborEncode({
601 seq: evt.seq,
602 rebase: false,
603 tooBig: false,
604 repo: evt.did,
605 commit: cidToBytes(evt.commit_cid),
606 rev: createTid(),
607 since: null,
608 blocks: new Uint8Array(0), // Simplified - real impl includes CAR slice
609 ops: cborDecode(new Uint8Array(evt.evt)).ops,
610 blobs: [],
611 time: new Date().toISOString()
612 })
613
614 // Concatenate header + body
615 const frame = new Uint8Array(header.length + body.length)
616 frame.set(header)
617 frame.set(body, header.length)
618 return frame
619 }
620
621 async webSocketMessage(ws, message) {
622 // Handle ping
623 if (message === 'ping') ws.send('pong')
624 }
625
626 async webSocketClose(ws, code, reason) {
627 // Durable Object will hibernate when no connections remain
628 }
629
630 broadcastEvent(evt) {
631 const frame = this.formatEvent(evt)
632 for (const ws of this.state.getWebSockets()) {
633 try {
634 ws.send(frame)
635 } catch (e) {
636 // Client disconnected
637 }
638 }
639 }
640
641 async fetch(request) {
642 const url = new URL(request.url)
643
644 // Handle resolution - doesn't require ?did= param
645 if (url.pathname === '/.well-known/atproto-did') {
646 const did = await this.getDid()
647 if (!did) {
648 return new Response('User not found', { status: 404 })
649 }
650 return new Response(did, {
651 headers: { 'Content-Type': 'text/plain' }
652 })
653 }
654
655 if (url.pathname === '/init') {
656 const body = await request.json()
657 if (!body.did || !body.privateKey) {
658 return Response.json({ error: 'missing did or privateKey' }, { status: 400 })
659 }
660 await this.initIdentity(body.did, body.privateKey, body.handle || null)
661 return Response.json({ ok: true, did: body.did, handle: body.handle || null })
662 }
663 if (url.pathname === '/status') {
664 const did = await this.getDid()
665 return Response.json({
666 initialized: !!did,
667 did: did || null
668 })
669 }
670 if (url.pathname === '/xrpc/com.atproto.repo.createRecord') {
671 if (request.method !== 'POST') {
672 return Response.json({ error: 'method not allowed' }, { status: 405 })
673 }
674
675 const body = await request.json()
676 if (!body.collection || !body.record) {
677 return Response.json({ error: 'missing collection or record' }, { status: 400 })
678 }
679
680 try {
681 const result = await this.createRecord(body.collection, body.record, body.rkey)
682 return Response.json(result)
683 } catch (err) {
684 return Response.json({ error: err.message }, { status: 500 })
685 }
686 }
687 if (url.pathname === '/xrpc/com.atproto.repo.getRecord') {
688 const collection = url.searchParams.get('collection')
689 const rkey = url.searchParams.get('rkey')
690
691 if (!collection || !rkey) {
692 return Response.json({ error: 'missing collection or rkey' }, { status: 400 })
693 }
694
695 const did = await this.getDid()
696 const uri = `at://${did}/${collection}/${rkey}`
697
698 const rows = this.sql.exec(
699 `SELECT cid, value FROM records WHERE uri = ?`, uri
700 ).toArray()
701
702 if (rows.length === 0) {
703 return Response.json({ error: 'record not found' }, { status: 404 })
704 }
705
706 const row = rows[0]
707 // Decode CBOR for response (convert ArrayBuffer to Uint8Array)
708 const value = cborDecode(new Uint8Array(row.value))
709
710 return Response.json({ uri, cid: row.cid, value })
711 }
712 if (url.pathname === '/xrpc/com.atproto.sync.getRepo') {
713 const commits = this.sql.exec(
714 `SELECT cid FROM commits ORDER BY seq DESC LIMIT 1`
715 ).toArray()
716
717 if (commits.length === 0) {
718 return Response.json({ error: 'repo not found' }, { status: 404 })
719 }
720
721 const blocks = this.sql.exec(`SELECT cid, data FROM blocks`).toArray()
722 // Convert ArrayBuffer data to Uint8Array
723 const blocksForCar = blocks.map(b => ({
724 cid: b.cid,
725 data: new Uint8Array(b.data)
726 }))
727 const car = buildCarFile(commits[0].cid, blocksForCar)
728
729 return new Response(car, {
730 headers: { 'content-type': 'application/vnd.ipld.car' }
731 })
732 }
733 if (url.pathname === '/xrpc/com.atproto.sync.subscribeRepos') {
734 const upgradeHeader = request.headers.get('Upgrade')
735 if (upgradeHeader !== 'websocket') {
736 return new Response('expected websocket', { status: 426 })
737 }
738
739 const { 0: client, 1: server } = new WebSocketPair()
740 this.state.acceptWebSocket(server)
741
742 // Send backlog if cursor provided
743 const cursor = url.searchParams.get('cursor')
744 if (cursor) {
745 const events = this.sql.exec(
746 `SELECT * FROM seq_events WHERE seq > ? ORDER BY seq`,
747 parseInt(cursor)
748 ).toArray()
749
750 for (const evt of events) {
751 server.send(this.formatEvent(evt))
752 }
753 }
754
755 return new Response(null, { status: 101, webSocket: client })
756 }
757 return Response.json({ error: 'not found' }, { status: 404 })
758 }
759}
760
761export default {
762 async fetch(request, env) {
763 const url = new URL(request.url)
764
765 // For /.well-known/atproto-did, extract DID from subdomain
766 // e.g., alice.atproto-pds.chad-53c.workers.dev -> look up "alice"
767 if (url.pathname === '/.well-known/atproto-did') {
768 const host = request.headers.get('Host') || ''
769 // For now, use the first Durable Object (single-user PDS)
770 // Extract handle from subdomain if present
771 const did = url.searchParams.get('did') || 'default'
772 const id = env.PDS.idFromName(did)
773 const pds = env.PDS.get(id)
774 return pds.fetch(request)
775 }
776
777 const did = url.searchParams.get('did')
778 if (!did) {
779 return new Response('missing did param', { status: 400 })
780 }
781
782 const id = env.PDS.idFromName(did)
783 const pds = env.PDS.get(id)
784 return pds.fetch(request)
785 }
786}
787
788// Export utilities for testing
789export {
790 cborEncode, cborDecode, createCid, cidToString, base32Encode, createTid,
791 generateKeyPair, importPrivateKey, sign, bytesToHex, hexToBytes,
792 getKeyDepth, varint, base32Decode, buildCarFile
793}