#!/usr/bin/env node /** * Update DID handle and PDS endpoint * * Usage: node scripts/update-did.js --credentials --new-handle --new-pds */ import { webcrypto } from 'crypto' import { readFileSync, writeFileSync } from 'fs' // === ARGUMENT PARSING === function parseArgs() { const args = process.argv.slice(2) const opts = { credentials: null, newHandle: null, newPds: null, plcUrl: 'https://plc.directory' } for (let i = 0; i < args.length; i++) { if (args[i] === '--credentials' && args[i + 1]) { opts.credentials = args[++i] } else if (args[i] === '--new-handle' && args[i + 1]) { opts.newHandle = args[++i] } else if (args[i] === '--new-pds' && args[i + 1]) { opts.newPds = args[++i] } else if (args[i] === '--plc-url' && args[i + 1]) { opts.plcUrl = args[++i] } } if (!opts.credentials || !opts.newHandle || !opts.newPds) { console.error('Usage: node scripts/update-did.js --credentials --new-handle --new-pds ') process.exit(1) } return opts } // === CRYPTO HELPERS === function hexToBytes(hex) { const bytes = new Uint8Array(hex.length / 2) for (let i = 0; i < hex.length; i += 2) { bytes[i / 2] = parseInt(hex.substr(i, 2), 16) } return bytes } function bytesToHex(bytes) { return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('') } async function importPrivateKey(privateKeyBytes) { const pkcs8Prefix = new Uint8Array([ 0x30, 0x41, 0x02, 0x01, 0x00, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x04, 0x27, 0x30, 0x25, 0x02, 0x01, 0x01, 0x04, 0x20 ]) const pkcs8 = new Uint8Array(pkcs8Prefix.length + 32) pkcs8.set(pkcs8Prefix) pkcs8.set(privateKeyBytes, pkcs8Prefix.length) return webcrypto.subtle.importKey( 'pkcs8', pkcs8, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign'] ) } // === CBOR ENCODING === function cborEncodeKey(key) { const bytes = new TextEncoder().encode(key) const parts = [] const mt = 3 << 5 if (bytes.length < 24) { parts.push(mt | bytes.length) } else if (bytes.length < 256) { parts.push(mt | 24, bytes.length) } parts.push(...bytes) return new Uint8Array(parts) } function compareBytes(a, b) { const minLen = Math.min(a.length, b.length) for (let i = 0; i < minLen; i++) { if (a[i] !== b[i]) return a[i] - b[i] } return a.length - b.length } function cborEncode(value) { const parts = [] function encode(val) { if (val === null) { parts.push(0xf6) } else if (typeof val === 'string') { const bytes = new TextEncoder().encode(val) encodeHead(3, bytes.length) parts.push(...bytes) } else if (typeof val === 'number') { if (Number.isInteger(val) && val >= 0) { encodeHead(0, val) } } else if (val instanceof Uint8Array) { encodeHead(2, val.length) parts.push(...val) } else if (Array.isArray(val)) { encodeHead(4, val.length) for (const item of val) encode(item) } else if (typeof val === 'object') { const keys = Object.keys(val) const keysSorted = keys.sort((a, b) => compareBytes(cborEncodeKey(a), cborEncodeKey(b))) encodeHead(5, keysSorted.length) for (const key of keysSorted) { encode(key) encode(val[key]) } } } function encodeHead(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) } } encode(value) return new Uint8Array(parts) } // === SIGNING === const P256_N = BigInt('0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551') function ensureLowS(sig) { const halfN = P256_N / 2n const r = sig.slice(0, 32) const s = sig.slice(32, 64) let sInt = BigInt('0x' + bytesToHex(s)) if (sInt > halfN) { sInt = P256_N - sInt const newS = hexToBytes(sInt.toString(16).padStart(64, '0')) const result = new Uint8Array(64) result.set(r) result.set(newS, 32) return result } return sig } function base64UrlEncode(bytes) { const binary = String.fromCharCode(...bytes) return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') } async function signPlcOperation(operation, privateKey) { const { sig, ...opWithoutSig } = operation const encoded = cborEncode(opWithoutSig) const signature = await webcrypto.subtle.sign( { name: 'ECDSA', hash: 'SHA-256' }, privateKey, encoded ) const sigBytes = ensureLowS(new Uint8Array(signature)) return base64UrlEncode(sigBytes) } // === MAIN === async function main() { const opts = parseArgs() // Load credentials const creds = JSON.parse(readFileSync(opts.credentials, 'utf-8')) console.log(`Updating DID: ${creds.did}`) console.log(` Old handle: ${creds.handle}`) console.log(` New handle: ${opts.newHandle}`) console.log(` New PDS: ${opts.newPds}`) console.log('') // Fetch current operation log console.log('Fetching current PLC operation log...') const logRes = await fetch(`${opts.plcUrl}/${creds.did}/log/audit`) if (!logRes.ok) { throw new Error(`Failed to fetch PLC log: ${logRes.status}`) } const log = await logRes.json() const lastOp = log[log.length - 1] console.log(` Found ${log.length} operations`) console.log(` Last CID: ${lastOp.cid}`) console.log('') // Import private key const privateKey = await importPrivateKey(hexToBytes(creds.privateKeyHex)) // Create new operation const newOp = { type: 'plc_operation', rotationKeys: lastOp.operation.rotationKeys, verificationMethods: lastOp.operation.verificationMethods, alsoKnownAs: [`at://${opts.newHandle}`], services: { atproto_pds: { type: 'AtprotoPersonalDataServer', endpoint: opts.newPds } }, prev: lastOp.cid } // Sign the operation console.log('Signing new operation...') newOp.sig = await signPlcOperation(newOp, privateKey) // Submit to PLC console.log('Submitting to PLC directory...') const submitRes = await fetch(`${opts.plcUrl}/${creds.did}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newOp) }) if (!submitRes.ok) { const text = await submitRes.text() throw new Error(`PLC update failed: ${submitRes.status} ${text}`) } console.log(' Updated successfully!') console.log('') // Update credentials file creds.handle = opts.newHandle creds.pdsUrl = opts.newPds writeFileSync(opts.credentials, JSON.stringify(creds, null, 2)) console.log(`Updated credentials file: ${opts.credentials}`) // Verify console.log('') console.log('Verifying...') const verifyRes = await fetch(`${opts.plcUrl}/${creds.did}`) const didDoc = await verifyRes.json() console.log(` alsoKnownAs: ${didDoc.alsoKnownAs}`) console.log(` PDS endpoint: ${didDoc.service[0].serviceEndpoint}`) } main().catch(err => { console.error('Error:', err.message) process.exit(1) })