diff --git a/src/pds.js b/src/pds.js index 77b3a45..141bff5 100644 --- a/src/pds.js +++ b/src/pds.js @@ -380,6 +380,84 @@ export class PersonalDataServer { return importPrivateKey(hexToBytes(hex)) } + async createRecord(collection, record, rkey = null) { + const did = await this.getDid() + if (!did) throw new Error('PDS not initialized') + + rkey = rkey || createTid() + const uri = `at://${did}/${collection}/${rkey}` + + // Encode and hash record + const recordBytes = cborEncode(record) + const recordCid = await createCid(recordBytes) + const recordCidStr = cidToString(recordCid) + + // Store block + this.sql.exec( + `INSERT OR REPLACE INTO blocks (cid, data) VALUES (?, ?)`, + recordCidStr, recordBytes + ) + + // Store record index + this.sql.exec( + `INSERT OR REPLACE INTO records (uri, cid, collection, rkey, value) VALUES (?, ?, ?, ?, ?)`, + uri, recordCidStr, collection, rkey, recordBytes + ) + + // Rebuild MST + const mst = new MST(this.sql) + const dataRoot = await mst.computeRoot() + + // Get previous commit + const prevCommits = this.sql.exec( + `SELECT cid, rev FROM commits ORDER BY seq DESC LIMIT 1` + ).toArray() + const prevCommit = prevCommits.length > 0 ? prevCommits[0] : null + + // Create commit + const rev = createTid() + const commit = { + did, + version: 3, + data: dataRoot, + rev, + prev: prevCommit?.cid || null + } + + // Sign commit + const commitBytes = cborEncode(commit) + const signingKey = await this.getSigningKey() + const sig = await sign(signingKey, commitBytes) + + const signedCommit = { ...commit, sig } + const signedBytes = cborEncode(signedCommit) + const commitCid = await createCid(signedBytes) + const commitCidStr = cidToString(commitCid) + + // Store commit block + this.sql.exec( + `INSERT OR REPLACE INTO blocks (cid, data) VALUES (?, ?)`, + commitCidStr, signedBytes + ) + + // Store commit reference + this.sql.exec( + `INSERT INTO commits (cid, rev, prev) VALUES (?, ?, ?)`, + commitCidStr, rev, prevCommit?.cid || null + ) + + // Sequence event + const evt = cborEncode({ + ops: [{ action: 'create', path: `${collection}/${rkey}`, cid: recordCidStr }] + }) + this.sql.exec( + `INSERT INTO seq_events (did, commit_cid, evt) VALUES (?, ?, ?)`, + did, commitCidStr, evt + ) + + return { uri, cid: recordCidStr, commit: commitCidStr } + } + async fetch(request) { const url = new URL(request.url) if (url.pathname === '/test/cbor') { @@ -439,6 +517,23 @@ export class PersonalDataServer { did: did || null }) } + if (url.pathname === '/xrpc/com.atproto.repo.createRecord') { + if (request.method !== 'POST') { + return Response.json({ error: 'method not allowed' }, { status: 405 }) + } + + const body = await request.json() + if (!body.collection || !body.record) { + return Response.json({ error: 'missing collection or record' }, { status: 400 }) + } + + try { + const result = await this.createRecord(body.collection, body.record, body.rkey) + return Response.json(result) + } catch (err) { + return Response.json({ error: err.message }, { status: 500 }) + } + } return new Response('pds running', { status: 200 }) } }