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}