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

fix: compute DID from signed operation (include sig in hash)

The PLC spec requires the DID to be derived from the hash of the
complete signed operation, not just the unsigned operation.

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

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

Changed files
+47 -8
scripts
+12
package-lock.json
···
··· 1 + { 2 + "name": "cloudflare-pds", 3 + "version": "0.1.0", 4 + "lockfileVersion": 3, 5 + "requires": true, 6 + "packages": { 7 + "": { 8 + "name": "cloudflare-pds", 9 + "version": "0.1.0" 10 + } 11 + } 12 + }
+35 -8
scripts/setup.js
··· 149 return result 150 } 151 152 - // === CBOR ENCODING (minimal for PLC operations) === 153 154 function cborEncode(value) { 155 const parts = [] ··· 172 encodeHead(4, val.length) 173 for (const item of val) encode(item) 174 } else if (typeof val === 'object') { 175 - const keys = Object.keys(val).sort() 176 - encodeHead(5, keys.length) 177 - for (const key of keys) { 178 encode(key) 179 encode(val[key]) 180 } ··· 289 } 290 291 async function deriveDidFromOperation(operation) { 292 - const { sig, ...opWithoutSig } = operation 293 - const encoded = cborEncode(opWithoutSig) 294 const hash = await sha256(encoded) 295 - // DID is base32 of first 24 bytes of hash 296 - return 'did:plc:' + base32Encode(hash.slice(0, 24)) 297 } 298 299 function base32Encode(bytes) {
··· 149 return result 150 } 151 152 + // === CBOR ENCODING (dag-cbor compliant for PLC operations) === 153 + 154 + function cborEncodeKey(key) { 155 + // Encode a string key to CBOR bytes (for sorting) 156 + const bytes = new TextEncoder().encode(key) 157 + const parts = [] 158 + const mt = 3 << 5 // major type 3 = text string 159 + if (bytes.length < 24) { 160 + parts.push(mt | bytes.length) 161 + } else if (bytes.length < 256) { 162 + parts.push(mt | 24, bytes.length) 163 + } else if (bytes.length < 65536) { 164 + parts.push(mt | 25, bytes.length >> 8, bytes.length & 0xff) 165 + } 166 + parts.push(...bytes) 167 + return new Uint8Array(parts) 168 + } 169 + 170 + function compareBytes(a, b) { 171 + // dag-cbor: bytewise lexicographic order of encoded keys 172 + const minLen = Math.min(a.length, b.length) 173 + for (let i = 0; i < minLen; i++) { 174 + if (a[i] !== b[i]) return a[i] - b[i] 175 + } 176 + return a.length - b.length 177 + } 178 179 function cborEncode(value) { 180 const parts = [] ··· 197 encodeHead(4, val.length) 198 for (const item of val) encode(item) 199 } else if (typeof val === 'object') { 200 + // dag-cbor: sort keys by their CBOR-encoded bytes (length first, then lexicographic) 201 + const keys = Object.keys(val) 202 + const keysSorted = keys.sort((a, b) => compareBytes(cborEncodeKey(a), cborEncodeKey(b))) 203 + encodeHead(5, keysSorted.length) 204 + for (const key of keysSorted) { 205 encode(key) 206 encode(val[key]) 207 } ··· 316 } 317 318 async function deriveDidFromOperation(operation) { 319 + // DID is computed from the FULL operation INCLUDING the signature 320 + const encoded = cborEncode(operation) 321 const hash = await sha256(encoded) 322 + // DID is base32 of first 15 bytes of hash (= 24 base32 chars) 323 + return 'did:plc:' + base32Encode(hash.slice(0, 15)) 324 } 325 326 function base32Encode(bytes) {