# pds.js Refactor Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Improve pds.js maintainability through consolidated CBOR encoding, JSDoc documentation, route table pattern, and clarifying comments. **Architecture:** Single-file refactor preserving current dependency order. Extract shared `encodeHead` helper for CBOR encoders. Replace `PersonalDataServer.fetch` if/else chain with declarative route table. Add JSDoc to exported functions and "why" comments to protocol-specific logic. **Tech Stack:** JavaScript (ES modules), Cloudflare Workers, JSDoc --- ## Task 1: Add CBOR Constants **Files:** - Modify: `src/pds.js:1-12` **Step 1: Write test for constants usage** No new test needed — existing CBOR tests will verify constants work correctly. **Step 2: Add constants section at top of file** Insert before the CID wrapper class: ```javascript // === CONSTANTS === // CBOR primitive markers (RFC 8949) const CBOR_FALSE = 0xf4 const CBOR_TRUE = 0xf5 const CBOR_NULL = 0xf6 // DAG-CBOR CID link tag const CBOR_TAG_CID = 42 ``` **Step 3: Update cborEncode to use constants** Replace in `cborEncode` function: - `parts.push(0xf6)` → `parts.push(CBOR_NULL)` - `parts.push(0xf5)` → `parts.push(CBOR_TRUE)` - `parts.push(0xf4)` → `parts.push(CBOR_FALSE)` **Step 4: Update cborEncodeDagCbor to use constants** Same replacements, plus: - `parts.push(0xd8, 42)` → `parts.push(0xd8, CBOR_TAG_CID)` **Step 5: Update cborEncodeMstNode to use constants** Same replacements for null/true/false and tag 42. **Step 6: Run tests to verify** Run: `npm test` Expected: All CBOR tests pass **Step 7: Commit** ```bash git add src/pds.js git commit -m "refactor: extract CBOR constants for clarity" ``` --- ## Task 2: Extract Shared encodeHead Helper **Files:** - Modify: `src/pds.js` (CBOR ENCODING section) **Step 1: Write test for large integer encoding** Already exists — `test/pds.test.js` has "encodes large integers >= 2^31 without overflow" **Step 2: Extract shared encodeHead function** Add after constants section, before `cborEncode`: ```javascript /** * Encode CBOR type header (major type + length) * @param {number[]} parts - Array to push bytes to * @param {number} majorType - CBOR major type (0-7) * @param {number} length - Value or length to encode */ function encodeHead(parts, majorType, length) { const mt = majorType << 5 if (length < 24) { parts.push(mt | length) } else if (length < 256) { parts.push(mt | 24, length) } else if (length < 65536) { parts.push(mt | 25, length >> 8, length & 0xff) } else if (length < 4294967296) { // Use Math.floor instead of bitshift to avoid 32-bit signed integer overflow parts.push(mt | 26, Math.floor(length / 0x1000000) & 0xff, Math.floor(length / 0x10000) & 0xff, Math.floor(length / 0x100) & 0xff, length & 0xff) } } ``` **Step 3: Update cborEncode to use shared helper** Remove the local `encodeHead` function. Replace calls: - `encodeHead(3, bytes.length)` → `encodeHead(parts, 3, bytes.length)` - Same pattern for all other calls **Step 4: Update cborEncodeDagCbor to use shared helper** Remove the local `encodeHead` function. Update all calls to pass `parts` as first argument. **Step 5: Update cborEncodeMstNode to use shared helper** Remove the local `encodeHead` function. Update all calls to pass `parts` as first argument. **Step 6: Run tests** Run: `npm test` Expected: All tests pass **Step 7: Commit** ```bash git add src/pds.js git commit -m "refactor: consolidate CBOR encodeHead into shared helper" ``` --- ## Task 3: Add JSDoc to Exported Functions **Files:** - Modify: `src/pds.js` **Step 1: Add JSDoc to cborEncode** ```javascript /** * Encode a value as CBOR bytes (RFC 8949 deterministic encoding) * @param {*} value - Value to encode (null, boolean, number, string, Uint8Array, array, or object) * @returns {Uint8Array} CBOR-encoded bytes */ export function cborEncode(value) { ``` **Step 2: Add JSDoc to cborDecode** ```javascript /** * Decode CBOR bytes to a JavaScript value * @param {Uint8Array} bytes - CBOR-encoded bytes * @returns {*} Decoded value */ export function cborDecode(bytes) { ``` **Step 3: Add JSDoc to CID functions** ```javascript /** * Create a CIDv1 (dag-cbor + sha-256) from raw bytes * @param {Uint8Array} bytes - Content to hash * @returns {Promise} CID bytes (36 bytes: version + codec + multihash) */ export async function createCid(bytes) { /** * Convert CID bytes to base32lower string representation * @param {Uint8Array} cid - CID bytes * @returns {string} Base32lower-encoded CID with 'b' prefix */ export function cidToString(cid) { /** * Encode bytes as base32lower string * @param {Uint8Array} bytes - Bytes to encode * @returns {string} Base32lower-encoded string */ export function base32Encode(bytes) { ``` **Step 4: Add JSDoc to TID function** ```javascript /** * Generate a timestamp-based ID (TID) for record keys * Monotonic within a process, sortable by time * @returns {string} 13-character base32-sort encoded TID */ export function createTid() { ``` **Step 5: Add JSDoc to signing functions** ```javascript /** * Import a raw P-256 private key for signing * @param {Uint8Array} privateKeyBytes - 32-byte raw private key * @returns {Promise} Web Crypto key handle */ export async function importPrivateKey(privateKeyBytes) { /** * Sign data with ECDSA P-256, returning low-S normalized signature * @param {CryptoKey} privateKey - Web Crypto key from importPrivateKey * @param {Uint8Array} data - Data to sign * @returns {Promise} 64-byte signature (r || s) */ export async function sign(privateKey, data) { /** * Generate a new P-256 key pair * @returns {Promise<{privateKey: Uint8Array, publicKey: Uint8Array}>} 32-byte private key, 33-byte compressed public key */ export async function generateKeyPair() { ``` **Step 6: Add JSDoc to utility functions** ```javascript /** * Convert bytes to hexadecimal string * @param {Uint8Array} bytes - Bytes to convert * @returns {string} Hex string */ export function bytesToHex(bytes) { /** * Convert hexadecimal string to bytes * @param {string} hex - Hex string * @returns {Uint8Array} Decoded bytes */ export function hexToBytes(hex) { /** * Get MST tree depth for a key based on leading zeros in SHA-256 hash * @param {string} key - Record key (collection/rkey) * @returns {Promise} Tree depth (leading zeros / 2) */ export async function getKeyDepth(key) { /** * Encode integer as unsigned varint * @param {number} n - Non-negative integer * @returns {Uint8Array} Varint-encoded bytes */ export function varint(n) { /** * Convert base32lower CID string to raw bytes * @param {string} cidStr - CID string with 'b' prefix * @returns {Uint8Array} CID bytes */ export function cidToBytes(cidStr) { /** * Decode base32lower string to bytes * @param {string} str - Base32lower-encoded string * @returns {Uint8Array} Decoded bytes */ export function base32Decode(str) { /** * Build a CAR (Content Addressable aRchive) file * @param {string} rootCid - Root CID string * @param {Array<{cid: string, data: Uint8Array}>} blocks - Blocks to include * @returns {Uint8Array} CAR file bytes */ export function buildCarFile(rootCid, blocks) { ``` **Step 7: Run tests** Run: `npm test` Expected: All tests pass (JSDoc doesn't affect runtime) **Step 8: Commit** ```bash git add src/pds.js git commit -m "docs: add JSDoc to exported functions" ``` --- ## Task 4: Add "Why" Comments to Protocol Logic **Files:** - Modify: `src/pds.js` **Step 1: Add comment to DAG-CBOR key sorting** In `cborEncodeDagCbor`, before the `keys.sort()` call: ```javascript // DAG-CBOR: sort keys by length first, then lexicographically // (differs from standard CBOR which sorts lexicographically only) const keys = Object.keys(val).filter(k => val[k] !== undefined) keys.sort((a, b) => { ``` **Step 2: Add comment to MST depth calculation** In `getKeyDepth`, before the return: ```javascript // MST depth = leading zeros in SHA-256 hash / 2 // This creates a probabilistic tree where ~50% of keys are at depth 0, // ~25% at depth 1, etc., giving O(log n) lookups const depth = Math.floor(zeros / 2) ``` **Step 3: Add comment to low-S normalization** In `sign` function, before the if statement: ```javascript // Low-S normalization: Bitcoin/ATProto require S <= N/2 to prevent // signature malleability (two valid signatures for same message) if (sBigInt > P256_N_DIV_2) { ``` **Step 4: Add comment to CID tag encoding** In `cborEncodeDagCbor`, at the CID encoding: ```javascript } else if (val instanceof CID) { // CID links in DAG-CBOR use tag 42 + 0x00 multibase prefix // The 0x00 prefix indicates "identity" multibase (raw bytes) parts.push(0xd8, CBOR_TAG_CID) ``` **Step 5: Run tests** Run: `npm test` Expected: All tests pass **Step 6: Commit** ```bash git add src/pds.js git commit -m "docs: add 'why' comments to protocol-specific logic" ``` --- ## Task 5: Extract PersonalDataServer Route Table **Files:** - Modify: `src/pds.js` (PERSONAL DATA SERVER section) **Step 1: Define route table before class** Add before `export class PersonalDataServer`: ```javascript /** * Route handler function type * @callback RouteHandler * @param {PersonalDataServer} pds - PDS instance * @param {Request} request - HTTP request * @param {URL} url - Parsed URL * @returns {Promise} HTTP response */ /** * @typedef {Object} Route * @property {string} [method] - Required HTTP method (default: any) * @property {RouteHandler} handler - Handler function */ /** @type {Record} */ const pdsRoutes = { '/.well-known/atproto-did': { handler: (pds, req, url) => pds.handleAtprotoDid() }, '/init': { method: 'POST', handler: (pds, req, url) => pds.handleInit(req) }, '/status': { handler: (pds, req, url) => pds.handleStatus() }, '/reset-repo': { handler: (pds, req, url) => pds.handleResetRepo() }, '/forward-event': { handler: (pds, req, url) => pds.handleForwardEvent(req) }, '/register-did': { handler: (pds, req, url) => pds.handleRegisterDid(req) }, '/get-registered-dids': { handler: (pds, req, url) => pds.handleGetRegisteredDids() }, '/repo-info': { handler: (pds, req, url) => pds.handleRepoInfo() }, '/xrpc/com.atproto.server.describeServer': { handler: (pds, req, url) => pds.handleDescribeServer(req) }, '/xrpc/com.atproto.sync.listRepos': { handler: (pds, req, url) => pds.handleListRepos() }, '/xrpc/com.atproto.repo.createRecord': { method: 'POST', handler: (pds, req, url) => pds.handleCreateRecord(req) }, '/xrpc/com.atproto.repo.getRecord': { handler: (pds, req, url) => pds.handleGetRecord(url) }, '/xrpc/com.atproto.sync.getLatestCommit': { handler: (pds, req, url) => pds.handleGetLatestCommit() }, '/xrpc/com.atproto.sync.getRepoStatus': { handler: (pds, req, url) => pds.handleGetRepoStatus() }, '/xrpc/com.atproto.sync.getRepo': { handler: (pds, req, url) => pds.handleGetRepo() }, '/xrpc/com.atproto.sync.subscribeRepos': { handler: (pds, req, url) => pds.handleSubscribeRepos(req, url) } } ``` **Step 2: Extract handleAtprotoDid method** Add to PersonalDataServer class: ```javascript async handleAtprotoDid() { let did = await this.getDid() if (!did) { const registeredDids = await this.state.storage.get('registeredDids') || [] did = registeredDids[0] } if (!did) { return new Response('User not found', { status: 404 }) } return new Response(did, { headers: { 'Content-Type': 'text/plain' } }) } ``` **Step 3: Extract handleInit method** ```javascript async handleInit(request) { const body = await request.json() if (!body.did || !body.privateKey) { return Response.json({ error: 'missing did or privateKey' }, { status: 400 }) } await this.initIdentity(body.did, body.privateKey, body.handle || null) return Response.json({ ok: true, did: body.did, handle: body.handle || null }) } ``` **Step 4: Extract handleStatus method** ```javascript async handleStatus() { const did = await this.getDid() return Response.json({ initialized: !!did, did: did || null }) } ``` **Step 5: Extract handleResetRepo method** ```javascript async handleResetRepo() { this.sql.exec(`DELETE FROM blocks`) this.sql.exec(`DELETE FROM records`) this.sql.exec(`DELETE FROM commits`) this.sql.exec(`DELETE FROM seq_events`) await this.state.storage.delete('head') await this.state.storage.delete('rev') return Response.json({ ok: true, message: 'repo data cleared' }) } ``` **Step 6: Extract handleForwardEvent method** ```javascript async handleForwardEvent(request) { const evt = await request.json() const numSockets = [...this.state.getWebSockets()].length console.log(`forward-event: received event seq=${evt.seq}, ${numSockets} connected sockets`) this.broadcastEvent({ seq: evt.seq, did: evt.did, commit_cid: evt.commit_cid, evt: new Uint8Array(Object.values(evt.evt)) }) return Response.json({ ok: true, sockets: numSockets }) } ``` **Step 7: Extract handleRegisterDid method** ```javascript async handleRegisterDid(request) { const body = await request.json() const registeredDids = await this.state.storage.get('registeredDids') || [] if (!registeredDids.includes(body.did)) { registeredDids.push(body.did) await this.state.storage.put('registeredDids', registeredDids) } return Response.json({ ok: true }) } ``` **Step 8: Extract handleGetRegisteredDids method** ```javascript async handleGetRegisteredDids() { const registeredDids = await this.state.storage.get('registeredDids') || [] return Response.json({ dids: registeredDids }) } ``` **Step 9: Extract handleRepoInfo method** ```javascript async handleRepoInfo() { const head = await this.state.storage.get('head') const rev = await this.state.storage.get('rev') return Response.json({ head: head || null, rev: rev || null }) } ``` **Step 10: Extract handleDescribeServer method** ```javascript handleDescribeServer(request) { const hostname = request.headers.get('x-hostname') || 'localhost' return Response.json({ did: `did:web:${hostname}`, availableUserDomains: [`.${hostname}`], inviteCodeRequired: false, phoneVerificationRequired: false, links: {}, contact: {} }) } ``` **Step 11: Extract handleListRepos method** ```javascript async handleListRepos() { const registeredDids = await this.state.storage.get('registeredDids') || [] const did = await this.getDid() const repos = did ? [{ did, head: null, rev: null }] : registeredDids.map(d => ({ did: d, head: null, rev: null })) return Response.json({ repos }) } ``` **Step 12: Extract handleCreateRecord method** ```javascript async handleCreateRecord(request) { 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 }) } } ``` **Step 13: Extract handleGetRecord method** ```javascript async handleGetRecord(url) { const collection = url.searchParams.get('collection') const rkey = url.searchParams.get('rkey') if (!collection || !rkey) { return Response.json({ error: 'missing collection or rkey' }, { status: 400 }) } const did = await this.getDid() const uri = `at://${did}/${collection}/${rkey}` const rows = this.sql.exec( `SELECT cid, value FROM records WHERE uri = ?`, uri ).toArray() if (rows.length === 0) { return Response.json({ error: 'record not found' }, { status: 404 }) } const row = rows[0] const value = cborDecode(new Uint8Array(row.value)) return Response.json({ uri, cid: row.cid, value }) } ``` **Step 14: Extract handleGetLatestCommit method** ```javascript handleGetLatestCommit() { const commits = this.sql.exec( `SELECT cid, rev FROM commits ORDER BY seq DESC LIMIT 1` ).toArray() if (commits.length === 0) { return Response.json({ error: 'RepoNotFound', message: 'repo not found' }, { status: 404 }) } return Response.json({ cid: commits[0].cid, rev: commits[0].rev }) } ``` **Step 15: Extract handleGetRepoStatus method** ```javascript async handleGetRepoStatus() { const did = await this.getDid() const commits = this.sql.exec( `SELECT cid, rev FROM commits ORDER BY seq DESC LIMIT 1` ).toArray() if (commits.length === 0 || !did) { return Response.json({ error: 'RepoNotFound', message: 'repo not found' }, { status: 404 }) } return Response.json({ did, active: true, status: 'active', rev: commits[0].rev }) } ``` **Step 16: Extract handleGetRepo method** ```javascript handleGetRepo() { const commits = this.sql.exec( `SELECT cid FROM commits ORDER BY seq DESC LIMIT 1` ).toArray() if (commits.length === 0) { return Response.json({ error: 'repo not found' }, { status: 404 }) } const blocks = this.sql.exec(`SELECT cid, data FROM blocks`).toArray() const blocksForCar = blocks.map(b => ({ cid: b.cid, data: new Uint8Array(b.data) })) const car = buildCarFile(commits[0].cid, blocksForCar) return new Response(car, { headers: { 'content-type': 'application/vnd.ipld.car' } }) } ``` **Step 17: Extract handleSubscribeRepos method** ```javascript handleSubscribeRepos(request, url) { const upgradeHeader = request.headers.get('Upgrade') if (upgradeHeader !== 'websocket') { return new Response('expected websocket', { status: 426 }) } const { 0: client, 1: server } = new WebSocketPair() this.state.acceptWebSocket(server) const cursor = url.searchParams.get('cursor') if (cursor) { const events = this.sql.exec( `SELECT * FROM seq_events WHERE seq > ? ORDER BY seq`, parseInt(cursor) ).toArray() for (const evt of events) { server.send(this.formatEvent(evt)) } } return new Response(null, { status: 101, webSocket: client }) } ``` **Step 18: Replace fetch method with router** ```javascript async fetch(request) { const url = new URL(request.url) const route = pdsRoutes[url.pathname] if (!route) { return Response.json({ error: 'not found' }, { status: 404 }) } if (route.method && request.method !== route.method) { return Response.json({ error: 'method not allowed' }, { status: 405 }) } return route.handler(this, request, url) } ``` **Step 19: Run tests** Run: `npm test` Expected: All tests pass **Step 20: Commit** ```bash git add src/pds.js git commit -m "refactor: extract PersonalDataServer route table" ``` --- ## Summary After completing all tasks, the file will have: - Named constants for CBOR markers and CID tag - Single shared `encodeHead` helper (no duplication) - JSDoc on all 15 exported functions - "Why" comments on 4 protocol-specific code sections - Declarative route table with 16 focused handler methods - Same dependency order, same single file