diff --git a/scripts/setup.js b/scripts/setup.js index 1e44223..edcd981 100644 --- a/scripts/setup.js +++ b/scripts/setup.js @@ -17,16 +17,13 @@ import { writeFileSync } from 'fs' function parseArgs() { const args = process.argv.slice(2) const opts = { - handle: null, pds: null, plcUrl: 'https://plc.directory', relayUrl: 'https://bsky.network' } for (let i = 0; i < args.length; i++) { - if (args[i] === '--handle' && args[i + 1]) { - opts.handle = args[++i] - } else if (args[i] === '--pds' && args[i + 1]) { + if (args[i] === '--pds' && args[i + 1]) { opts.pds = args[++i] } else if (args[i] === '--plc-url' && args[i + 1]) { opts.plcUrl = args[++i] @@ -35,17 +32,19 @@ function parseArgs() { } } - if (!opts.handle || !opts.pds) { - console.error('Usage: node scripts/setup.js --handle --pds ') + if (!opts.pds) { + console.error('Usage: node scripts/setup.js --pds ') console.error('') console.error('Options:') - console.error(' --handle Handle name (e.g., "alice")') console.error(' --pds PDS URL (e.g., "https://atproto-pds.chad-53c.workers.dev")') console.error(' --plc-url PLC directory URL (default: https://plc.directory)') console.error(' --relay-url Relay URL (default: https://bsky.network)') process.exit(1) } + // Handle is just the PDS hostname + opts.handle = new URL(opts.pds).host + return opts } @@ -289,17 +288,14 @@ function base64UrlEncode(bytes) { async function createGenesisOperation(opts) { const { didKey, handle, pdsUrl, cryptoKey } = opts - // Build the full handle - const pdsHost = new URL(pdsUrl).host - const fullHandle = `${handle}.${pdsHost}` - + // Handle is already the full hostname const operation = { type: 'plc_operation', rotationKeys: [didKey], verificationMethods: { atproto: didKey }, - alsoKnownAs: [`at://${fullHandle}`], + alsoKnownAs: [`at://${handle}`], services: { atproto_pds: { type: 'AtprotoPersonalDataServer', @@ -312,7 +308,7 @@ async function createGenesisOperation(opts) { // Sign the operation operation.sig = await signPlcOperation(operation, cryptoKey) - return { operation, fullHandle } + return { operation, handle } } async function deriveDidFromOperation(operation) { @@ -429,7 +425,6 @@ async function main() { console.log('PDS Federation Setup') console.log('====================') - console.log(`Handle: ${opts.handle}`) console.log(`PDS: ${opts.pds}`) console.log('') @@ -442,7 +437,7 @@ async function main() { // Step 2: Create genesis operation console.log('Creating PLC genesis operation...') - const { operation, fullHandle } = await createGenesisOperation({ + const { operation, handle } = await createGenesisOperation({ didKey, handle: opts.handle, pdsUrl: opts.pds, @@ -450,7 +445,7 @@ async function main() { }) const did = await deriveDidFromOperation(operation) console.log(` DID: ${did}`) - console.log(` Handle: ${fullHandle}`) + console.log(` Handle: ${handle}`) console.log('') // Step 3: Register with PLC directory @@ -462,7 +457,7 @@ async function main() { // Step 4: Initialize PDS console.log(`Initializing PDS at ${opts.pds}...`) const privateKeyHex = bytesToHex(keyPair.privateKey) - await initializePds(opts.pds, did, privateKeyHex, fullHandle) + await initializePds(opts.pds, did, privateKeyHex, handle) console.log(' PDS initialized!') console.log('') @@ -477,7 +472,7 @@ async function main() { // Step 6: Save credentials const credentials = { - handle: fullHandle, + handle, did, privateKeyHex: bytesToHex(keyPair.privateKey), didKey, @@ -485,13 +480,13 @@ async function main() { createdAt: new Date().toISOString() } - const credentialsFile = `./credentials-${opts.handle}.json` + const credentialsFile = `./credentials.json` saveCredentials(credentialsFile, credentials) // Final output console.log('Setup Complete!') console.log('===============') - console.log(`Handle: ${fullHandle}`) + console.log(`Handle: ${handle}`) console.log(`DID: ${did}`) console.log(`PDS: ${opts.pds}`) console.log('') diff --git a/src/pds.js b/src/pds.js index 2678150..9cbdc2a 100644 --- a/src/pds.js +++ b/src/pds.js @@ -216,6 +216,15 @@ export function cborDecode(bytes) { } return obj } + case 6: { // tag + // length is the tag number + const taggedValue = read() + if (length === CBOR_TAG_CID) { + // CID link: byte string with 0x00 multibase prefix, return raw CID bytes + return taggedValue.slice(1) // strip 0x00 prefix + } + return taggedValue + } case 7: { // special if (info === 20) return false if (info === 21) return true @@ -1014,8 +1023,8 @@ export class PersonalDataServer { rkey = rkey || createTid() const uri = `at://${did}/${collection}/${rkey}` - // Encode and hash record - const recordBytes = cborEncode(record) + // Encode and hash record (must use DAG-CBOR for proper key ordering) + const recordBytes = cborEncodeDagCbor(record) const recordCid = await createCid(recordBytes) const recordCidStr = cidToString(recordCid) @@ -1404,19 +1413,56 @@ export class PersonalDataServer { if (rows.length === 0) { return Response.json({ error: 'RecordNotFound', message: 'record not found' }, { status: 404 }) } - // Get the record block const recordCid = rows[0].cid - const blockRows = this.sql.exec( - `SELECT cid, data FROM blocks WHERE cid = ?`, recordCid + + // Get latest commit + const commits = this.sql.exec( + `SELECT cid FROM commits ORDER BY seq DESC LIMIT 1` ).toArray() - if (blockRows.length === 0) { - return Response.json({ error: 'RecordNotFound', message: 'block not found' }, { status: 404 }) + if (commits.length === 0) { + return Response.json({ error: 'RepoNotFound', message: 'no commits' }, { status: 404 }) } - const blocks = blockRows.map(b => ({ - cid: b.cid, - data: new Uint8Array(b.data) - })) - const car = buildCarFile(recordCid, blocks) + const commitCid = commits[0].cid + + // Build proof chain: commit -> MST path -> record + // Include commit block, all MST nodes on path to record, and record block + const blocks = [] + const included = new Set() + + const addBlock = (cidStr) => { + if (included.has(cidStr)) return + included.add(cidStr) + const blockRows = this.sql.exec( + `SELECT data FROM blocks WHERE cid = ?`, cidStr + ).toArray() + if (blockRows.length > 0) { + blocks.push({ cid: cidStr, data: new Uint8Array(blockRows[0].data) }) + } + } + + // Add commit block + addBlock(commitCid) + + // Get commit to find data root + const commitRows = this.sql.exec( + `SELECT data FROM blocks WHERE cid = ?`, commitCid + ).toArray() + if (commitRows.length > 0) { + const commit = cborDecode(new Uint8Array(commitRows[0].data)) + if (commit.data) { + const dataRootCid = cidToString(commit.data) + // Collect MST path blocks (this includes all MST nodes) + const mstBlocks = this.collectMstBlocks(dataRootCid) + for (const block of mstBlocks) { + addBlock(block.cid) + } + } + } + + // Add record block + addBlock(recordCid) + + const car = buildCarFile(commitCid, blocks) return new Response(car, { headers: { 'content-type': 'application/vnd.ipld.car' } }) @@ -1482,6 +1528,10 @@ export default { } const response = await handleRequest(request, env) + // Don't wrap WebSocket upgrades - they need the webSocket property preserved + if (response.status === 101) { + return response + } return addCorsHeaders(response) } }