this repo has no description

refactor: deduplicate helpers between setup.js and pds.js

- setup.js now imports shared helpers from pds.js instead of duplicating them
- Export cborEncodeDagCbor from pds.js for proper DAG-CBOR encoding
- Add docker-compose.yml for local PLC directory testing
- Reduces setup.js from 558 to 362 lines

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

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

Changed files
+51 -215
scripts
src
+31
docker-compose.yml
···
··· 1 + services: 2 + plc: 3 + build: 4 + context: https://github.com/did-method-plc/did-method-plc.git 5 + dockerfile: packages/server/Dockerfile 6 + ports: 7 + - "2582:2582" 8 + environment: 9 + - DATABASE_URL=postgres://plc:plc@postgres:5432/plc 10 + - PORT=2582 11 + command: ["dumb-init", "node", "--enable-source-maps", "../dist/bin.js"] 12 + depends_on: 13 + postgres: 14 + condition: service_healthy 15 + 16 + postgres: 17 + image: postgres:16-alpine 18 + environment: 19 + - POSTGRES_USER=plc 20 + - POSTGRES_PASSWORD=plc 21 + - POSTGRES_DB=plc 22 + volumes: 23 + - plc_data:/var/lib/postgresql/data 24 + healthcheck: 25 + test: ["CMD-SHELL", "pg_isready -U plc"] 26 + interval: 2s 27 + timeout: 5s 28 + retries: 10 29 + 30 + volumes: 31 + plc_data:
+19 -214
scripts/setup.js
··· 4 * PDS Setup Script 5 * 6 * Registers a did:plc, initializes the PDS, and notifies the relay. 7 - * Zero dependencies - uses Node.js built-ins only. 8 * 9 * Usage: node scripts/setup.js --handle alice --pds https://your-pds.workers.dev 10 */ 11 12 - import { webcrypto } from 'node:crypto'; 13 import { writeFileSync } from 'node:fs'; 14 15 // === ARGUMENT PARSING === 16 ··· 57 return opts; 58 } 59 60 - // === KEY GENERATION === 61 - 62 - async function generateP256Keypair() { 63 - const keyPair = await webcrypto.subtle.generateKey( 64 - { name: 'ECDSA', namedCurve: 'P-256' }, 65 - true, 66 - ['sign', 'verify'], 67 - ); 68 - 69 - // Export private key as raw 32 bytes 70 - const privateJwk = await webcrypto.subtle.exportKey( 71 - 'jwk', 72 - keyPair.privateKey, 73 - ); 74 - const privateBytes = base64UrlDecode(privateJwk.d); 75 - 76 - // Export public key as uncompressed point (65 bytes) 77 - const publicRaw = await webcrypto.subtle.exportKey('raw', keyPair.publicKey); 78 - const publicBytes = new Uint8Array(publicRaw); 79 - 80 - // Compress public key to 33 bytes 81 - const compressedPublic = compressPublicKey(publicBytes); 82 - 83 - return { 84 - privateKey: privateBytes, 85 - publicKey: compressedPublic, 86 - cryptoKey: keyPair.privateKey, 87 - }; 88 - } 89 - 90 - function compressPublicKey(uncompressed) { 91 - // uncompressed is 65 bytes: 0x04 + x(32) + y(32) 92 - const x = uncompressed.slice(1, 33); 93 - const y = uncompressed.slice(33, 65); 94 - const prefix = (y[31] & 1) === 0 ? 0x02 : 0x03; 95 - const compressed = new Uint8Array(33); 96 - compressed[0] = prefix; 97 - compressed.set(x, 1); 98 - return compressed; 99 - } 100 - 101 - function base64UrlDecode(str) { 102 - const base64 = str.replace(/-/g, '+').replace(/_/g, '/'); 103 - const binary = atob(base64); 104 - const bytes = new Uint8Array(binary.length); 105 - for (let i = 0; i < binary.length; i++) { 106 - bytes[i] = binary.charCodeAt(i); 107 - } 108 - return bytes; 109 - } 110 - 111 - function bytesToHex(bytes) { 112 - return Array.from(bytes) 113 - .map((b) => b.toString(16).padStart(2, '0')) 114 - .join(''); 115 - } 116 117 // === DID:KEY ENCODING === 118 ··· 164 return result; 165 } 166 167 - // === CBOR ENCODING (dag-cbor compliant for PLC operations) === 168 - 169 - function cborEncodeKey(key) { 170 - // Encode a string key to CBOR bytes (for sorting) 171 - const bytes = new TextEncoder().encode(key); 172 - const parts = []; 173 - const mt = 3 << 5; // major type 3 = text string 174 - if (bytes.length < 24) { 175 - parts.push(mt | bytes.length); 176 - } else if (bytes.length < 256) { 177 - parts.push(mt | 24, bytes.length); 178 - } else if (bytes.length < 65536) { 179 - parts.push(mt | 25, bytes.length >> 8, bytes.length & 0xff); 180 - } 181 - parts.push(...bytes); 182 - return new Uint8Array(parts); 183 - } 184 - 185 - function compareBytes(a, b) { 186 - // dag-cbor: bytewise lexicographic order of encoded keys 187 - const minLen = Math.min(a.length, b.length); 188 - for (let i = 0; i < minLen; i++) { 189 - if (a[i] !== b[i]) return a[i] - b[i]; 190 - } 191 - return a.length - b.length; 192 - } 193 - 194 - function cborEncode(value) { 195 - const parts = []; 196 - 197 - function encode(val) { 198 - if (val === null) { 199 - parts.push(0xf6); 200 - } else if (typeof val === 'string') { 201 - const bytes = new TextEncoder().encode(val); 202 - encodeHead(3, bytes.length); 203 - parts.push(...bytes); 204 - } else if (typeof val === 'number') { 205 - if (Number.isInteger(val) && val >= 0) { 206 - encodeHead(0, val); 207 - } 208 - } else if (val instanceof Uint8Array) { 209 - encodeHead(2, val.length); 210 - parts.push(...val); 211 - } else if (Array.isArray(val)) { 212 - encodeHead(4, val.length); 213 - for (const item of val) encode(item); 214 - } else if (typeof val === 'object') { 215 - // dag-cbor: sort keys by their CBOR-encoded bytes (length first, then lexicographic) 216 - const keys = Object.keys(val); 217 - const keysSorted = keys.sort((a, b) => 218 - compareBytes(cborEncodeKey(a), cborEncodeKey(b)), 219 - ); 220 - encodeHead(5, keysSorted.length); 221 - for (const key of keysSorted) { 222 - encode(key); 223 - encode(val[key]); 224 - } 225 - } 226 - } 227 - 228 - function encodeHead(majorType, length) { 229 - const mt = majorType << 5; 230 - if (length < 24) { 231 - parts.push(mt | length); 232 - } else if (length < 256) { 233 - parts.push(mt | 24, length); 234 - } else if (length < 65536) { 235 - parts.push(mt | 25, length >> 8, length & 0xff); 236 - } 237 - } 238 - 239 - encode(value); 240 - return new Uint8Array(parts); 241 - } 242 - 243 // === HASHING === 244 245 async function sha256(data) { 246 - const hash = await webcrypto.subtle.digest('SHA-256', data); 247 return new Uint8Array(hash); 248 } 249 250 // === PLC OPERATIONS === 251 252 - async function signPlcOperation(operation, privateKey) { 253 // Encode operation without sig field 254 const { sig, ...opWithoutSig } = operation; 255 - const encoded = cborEncode(opWithoutSig); 256 - 257 - // Sign with P-256 258 - const signature = await webcrypto.subtle.sign( 259 - { name: 'ECDSA', hash: 'SHA-256' }, 260 - privateKey, 261 - encoded, 262 - ); 263 - 264 - // Convert to low-S form and base64url encode 265 - const sigBytes = ensureLowS(new Uint8Array(signature)); 266 - return base64UrlEncode(sigBytes); 267 - } 268 - 269 - function ensureLowS(sig) { 270 - // P-256 order N 271 - const N = BigInt( 272 - '0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551', 273 - ); 274 - const halfN = N / 2n; 275 - 276 - const r = sig.slice(0, 32); 277 - const s = sig.slice(32, 64); 278 - 279 - // Convert s to BigInt 280 - let sInt = BigInt(`0x${bytesToHex(s)}`); 281 - 282 - // If s > N/2, replace with N - s 283 - if (sInt > halfN) { 284 - sInt = N - sInt; 285 - const newS = hexToBytes(sInt.toString(16).padStart(64, '0')); 286 - const result = new Uint8Array(64); 287 - result.set(r); 288 - result.set(newS, 32); 289 - return result; 290 - } 291 292 - return sig; 293 - } 294 - 295 - function hexToBytes(hex) { 296 - const bytes = new Uint8Array(hex.length / 2); 297 - for (let i = 0; i < hex.length; i += 2) { 298 - bytes[i / 2] = parseInt(hex.substr(i, 2), 16); 299 - } 300 - return bytes; 301 - } 302 - 303 - function base64UrlEncode(bytes) { 304 - const binary = String.fromCharCode(...bytes); 305 - return btoa(binary) 306 - .replace(/\+/g, '-') 307 - .replace(/\//g, '_') 308 - .replace(/=+$/, ''); 309 } 310 311 async function createGenesisOperation(opts) { ··· 339 340 async function deriveDidFromOperation(operation) { 341 // DID is computed from the FULL operation INCLUDING the signature 342 - const encoded = cborEncode(operation); 343 const hash = await sha256(encoded); 344 // DID is base32 of first 15 bytes of hash (= 24 base32 chars) 345 return `did:plc:${base32Encode(hash.slice(0, 15))}`; 346 - } 347 - 348 - function base32Encode(bytes) { 349 - const alphabet = 'abcdefghijklmnopqrstuvwxyz234567'; 350 - let result = ''; 351 - let bits = 0; 352 - let value = 0; 353 - 354 - for (const byte of bytes) { 355 - value = (value << 8) | byte; 356 - bits += 8; 357 - while (bits >= 5) { 358 - bits -= 5; 359 - result += alphabet[(value >> bits) & 31]; 360 - } 361 - } 362 - 363 - if (bits > 0) { 364 - result += alphabet[(value << (5 - bits)) & 31]; 365 - } 366 - 367 - return result; 368 } 369 370 // === PLC DIRECTORY REGISTRATION === ··· 479 480 // Step 1: Generate keypair 481 console.log('Generating P-256 keypair...'); 482 - const keyPair = await generateP256Keypair(); 483 const didKey = publicKeyToDidKey(keyPair.publicKey); 484 console.log(` did:key: ${didKey}`); 485 console.log(''); ··· 490 didKey, 491 handle: opts.handle, 492 pdsUrl: opts.pds, 493 - cryptoKey: keyPair.cryptoKey, 494 }); 495 const did = await deriveDidFromOperation(operation); 496 console.log(` DID: ${did}`);
··· 4 * PDS Setup Script 5 * 6 * Registers a did:plc, initializes the PDS, and notifies the relay. 7 * 8 * Usage: node scripts/setup.js --handle alice --pds https://your-pds.workers.dev 9 */ 10 11 import { writeFileSync } from 'node:fs'; 12 + import { 13 + base32Encode, 14 + base64UrlEncode, 15 + bytesToHex, 16 + cborEncodeDagCbor, 17 + generateKeyPair, 18 + importPrivateKey, 19 + sign, 20 + } from '../src/pds.js'; 21 22 // === ARGUMENT PARSING === 23 ··· 64 return opts; 65 } 66 67 68 // === DID:KEY ENCODING === 69 ··· 115 return result; 116 } 117 118 // === HASHING === 119 120 async function sha256(data) { 121 + const hash = await crypto.subtle.digest('SHA-256', data); 122 return new Uint8Array(hash); 123 } 124 125 // === PLC OPERATIONS === 126 127 + async function signPlcOperation(operation, cryptoKey) { 128 // Encode operation without sig field 129 const { sig, ...opWithoutSig } = operation; 130 + const encoded = cborEncodeDagCbor(opWithoutSig); 131 132 + // Sign with P-256 (sign() handles low-S normalization) 133 + const signature = await sign(cryptoKey, encoded); 134 + return base64UrlEncode(signature); 135 } 136 137 async function createGenesisOperation(opts) { ··· 165 166 async function deriveDidFromOperation(operation) { 167 // DID is computed from the FULL operation INCLUDING the signature 168 + const encoded = cborEncodeDagCbor(operation); 169 const hash = await sha256(encoded); 170 // DID is base32 of first 15 bytes of hash (= 24 base32 chars) 171 return `did:plc:${base32Encode(hash.slice(0, 15))}`; 172 } 173 174 // === PLC DIRECTORY REGISTRATION === ··· 283 284 // Step 1: Generate keypair 285 console.log('Generating P-256 keypair...'); 286 + const keyPair = await generateKeyPair(); 287 + const cryptoKey = await importPrivateKey(keyPair.privateKey); 288 const didKey = publicKeyToDidKey(keyPair.publicKey); 289 console.log(` did:key: ${didKey}`); 290 console.log(''); ··· 295 didKey, 296 handle: opts.handle, 297 pdsUrl: opts.pds, 298 + cryptoKey, 299 }); 300 const did = await deriveDidFromOperation(operation); 301 console.log(` DID: ${did}`);
+1 -1
src/pds.js
··· 795 * @param {*} value 796 * @returns {Uint8Array} 797 */ 798 - function cborEncodeDagCbor(value) { 799 /** @type {number[]} */ 800 const parts = []; 801
··· 795 * @param {*} value 796 * @returns {Uint8Array} 797 */ 798 + export function cborEncodeDagCbor(value) { 799 /** @type {number[]} */ 800 const parts = []; 801