A zero-dependency AT Protocol Personal Data Server written in JavaScript
atproto pds

feat: add createRecord endpoint

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Changed files
+95
src
+95
src/pds.js
··· 380 380 return importPrivateKey(hexToBytes(hex)) 381 381 } 382 382 383 + async createRecord(collection, record, rkey = null) { 384 + const did = await this.getDid() 385 + if (!did) throw new Error('PDS not initialized') 386 + 387 + rkey = rkey || createTid() 388 + const uri = `at://${did}/${collection}/${rkey}` 389 + 390 + // Encode and hash record 391 + const recordBytes = cborEncode(record) 392 + const recordCid = await createCid(recordBytes) 393 + const recordCidStr = cidToString(recordCid) 394 + 395 + // Store block 396 + this.sql.exec( 397 + `INSERT OR REPLACE INTO blocks (cid, data) VALUES (?, ?)`, 398 + recordCidStr, recordBytes 399 + ) 400 + 401 + // Store record index 402 + this.sql.exec( 403 + `INSERT OR REPLACE INTO records (uri, cid, collection, rkey, value) VALUES (?, ?, ?, ?, ?)`, 404 + uri, recordCidStr, collection, rkey, recordBytes 405 + ) 406 + 407 + // Rebuild MST 408 + const mst = new MST(this.sql) 409 + const dataRoot = await mst.computeRoot() 410 + 411 + // Get previous commit 412 + const prevCommits = this.sql.exec( 413 + `SELECT cid, rev FROM commits ORDER BY seq DESC LIMIT 1` 414 + ).toArray() 415 + const prevCommit = prevCommits.length > 0 ? prevCommits[0] : null 416 + 417 + // Create commit 418 + const rev = createTid() 419 + const commit = { 420 + did, 421 + version: 3, 422 + data: dataRoot, 423 + rev, 424 + prev: prevCommit?.cid || null 425 + } 426 + 427 + // Sign commit 428 + const commitBytes = cborEncode(commit) 429 + const signingKey = await this.getSigningKey() 430 + const sig = await sign(signingKey, commitBytes) 431 + 432 + const signedCommit = { ...commit, sig } 433 + const signedBytes = cborEncode(signedCommit) 434 + const commitCid = await createCid(signedBytes) 435 + const commitCidStr = cidToString(commitCid) 436 + 437 + // Store commit block 438 + this.sql.exec( 439 + `INSERT OR REPLACE INTO blocks (cid, data) VALUES (?, ?)`, 440 + commitCidStr, signedBytes 441 + ) 442 + 443 + // Store commit reference 444 + this.sql.exec( 445 + `INSERT INTO commits (cid, rev, prev) VALUES (?, ?, ?)`, 446 + commitCidStr, rev, prevCommit?.cid || null 447 + ) 448 + 449 + // Sequence event 450 + const evt = cborEncode({ 451 + ops: [{ action: 'create', path: `${collection}/${rkey}`, cid: recordCidStr }] 452 + }) 453 + this.sql.exec( 454 + `INSERT INTO seq_events (did, commit_cid, evt) VALUES (?, ?, ?)`, 455 + did, commitCidStr, evt 456 + ) 457 + 458 + return { uri, cid: recordCidStr, commit: commitCidStr } 459 + } 460 + 383 461 async fetch(request) { 384 462 const url = new URL(request.url) 385 463 if (url.pathname === '/test/cbor') { ··· 438 516 initialized: !!did, 439 517 did: did || null 440 518 }) 519 + } 520 + if (url.pathname === '/xrpc/com.atproto.repo.createRecord') { 521 + if (request.method !== 'POST') { 522 + return Response.json({ error: 'method not allowed' }, { status: 405 }) 523 + } 524 + 525 + const body = await request.json() 526 + if (!body.collection || !body.record) { 527 + return Response.json({ error: 'missing collection or record' }, { status: 400 }) 528 + } 529 + 530 + try { 531 + const result = await this.createRecord(body.collection, body.record, body.rkey) 532 + return Response.json(result) 533 + } catch (err) { 534 + return Response.json({ error: err.message }, { status: 500 }) 535 + } 441 536 } 442 537 return new Response('pds running', { status: 200 }) 443 538 }