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

chore: add Biome for linting and formatting

- Add @biomejs/biome with 2-space indent, single quotes
- Add npm scripts: format, lint, check
- Auto-fix all lint issues

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

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

+24
biome.json
··· 1 + { 2 + "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json", 3 + "vcs": { 4 + "enabled": true, 5 + "clientKind": "git", 6 + "useIgnoreFile": true 7 + }, 8 + "formatter": { 9 + "enabled": true, 10 + "indentStyle": "space", 11 + "indentWidth": 2 12 + }, 13 + "linter": { 14 + "enabled": true, 15 + "rules": { 16 + "recommended": true 17 + } 18 + }, 19 + "javascript": { 20 + "formatter": { 21 + "quoteStyle": "single" 22 + } 23 + } 24 + }
+164
package-lock.json
··· 8 8 "name": "pds.js", 9 9 "version": "0.1.0", 10 10 "devDependencies": { 11 + "@biomejs/biome": "^2.3.11", 11 12 "wrangler": "^4.54.0" 13 + } 14 + }, 15 + "node_modules/@biomejs/biome": { 16 + "version": "2.3.11", 17 + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.11.tgz", 18 + "integrity": "sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ==", 19 + "dev": true, 20 + "license": "MIT OR Apache-2.0", 21 + "bin": { 22 + "biome": "bin/biome" 23 + }, 24 + "engines": { 25 + "node": ">=14.21.3" 26 + }, 27 + "funding": { 28 + "type": "opencollective", 29 + "url": "https://opencollective.com/biome" 30 + }, 31 + "optionalDependencies": { 32 + "@biomejs/cli-darwin-arm64": "2.3.11", 33 + "@biomejs/cli-darwin-x64": "2.3.11", 34 + "@biomejs/cli-linux-arm64": "2.3.11", 35 + "@biomejs/cli-linux-arm64-musl": "2.3.11", 36 + "@biomejs/cli-linux-x64": "2.3.11", 37 + "@biomejs/cli-linux-x64-musl": "2.3.11", 38 + "@biomejs/cli-win32-arm64": "2.3.11", 39 + "@biomejs/cli-win32-x64": "2.3.11" 40 + } 41 + }, 42 + "node_modules/@biomejs/cli-darwin-arm64": { 43 + "version": "2.3.11", 44 + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.11.tgz", 45 + "integrity": "sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA==", 46 + "cpu": [ 47 + "arm64" 48 + ], 49 + "dev": true, 50 + "license": "MIT OR Apache-2.0", 51 + "optional": true, 52 + "os": [ 53 + "darwin" 54 + ], 55 + "engines": { 56 + "node": ">=14.21.3" 57 + } 58 + }, 59 + "node_modules/@biomejs/cli-darwin-x64": { 60 + "version": "2.3.11", 61 + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.11.tgz", 62 + "integrity": "sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg==", 63 + "cpu": [ 64 + "x64" 65 + ], 66 + "dev": true, 67 + "license": "MIT OR Apache-2.0", 68 + "optional": true, 69 + "os": [ 70 + "darwin" 71 + ], 72 + "engines": { 73 + "node": ">=14.21.3" 74 + } 75 + }, 76 + "node_modules/@biomejs/cli-linux-arm64": { 77 + "version": "2.3.11", 78 + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.11.tgz", 79 + "integrity": "sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g==", 80 + "cpu": [ 81 + "arm64" 82 + ], 83 + "dev": true, 84 + "license": "MIT OR Apache-2.0", 85 + "optional": true, 86 + "os": [ 87 + "linux" 88 + ], 89 + "engines": { 90 + "node": ">=14.21.3" 91 + } 92 + }, 93 + "node_modules/@biomejs/cli-linux-arm64-musl": { 94 + "version": "2.3.11", 95 + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.11.tgz", 96 + "integrity": "sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg==", 97 + "cpu": [ 98 + "arm64" 99 + ], 100 + "dev": true, 101 + "license": "MIT OR Apache-2.0", 102 + "optional": true, 103 + "os": [ 104 + "linux" 105 + ], 106 + "engines": { 107 + "node": ">=14.21.3" 108 + } 109 + }, 110 + "node_modules/@biomejs/cli-linux-x64": { 111 + "version": "2.3.11", 112 + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.11.tgz", 113 + "integrity": "sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg==", 114 + "cpu": [ 115 + "x64" 116 + ], 117 + "dev": true, 118 + "license": "MIT OR Apache-2.0", 119 + "optional": true, 120 + "os": [ 121 + "linux" 122 + ], 123 + "engines": { 124 + "node": ">=14.21.3" 125 + } 126 + }, 127 + "node_modules/@biomejs/cli-linux-x64-musl": { 128 + "version": "2.3.11", 129 + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.11.tgz", 130 + "integrity": "sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw==", 131 + "cpu": [ 132 + "x64" 133 + ], 134 + "dev": true, 135 + "license": "MIT OR Apache-2.0", 136 + "optional": true, 137 + "os": [ 138 + "linux" 139 + ], 140 + "engines": { 141 + "node": ">=14.21.3" 142 + } 143 + }, 144 + "node_modules/@biomejs/cli-win32-arm64": { 145 + "version": "2.3.11", 146 + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.11.tgz", 147 + "integrity": "sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw==", 148 + "cpu": [ 149 + "arm64" 150 + ], 151 + "dev": true, 152 + "license": "MIT OR Apache-2.0", 153 + "optional": true, 154 + "os": [ 155 + "win32" 156 + ], 157 + "engines": { 158 + "node": ">=14.21.3" 159 + } 160 + }, 161 + "node_modules/@biomejs/cli-win32-x64": { 162 + "version": "2.3.11", 163 + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.11.tgz", 164 + "integrity": "sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg==", 165 + "cpu": [ 166 + "x64" 167 + ], 168 + "dev": true, 169 + "license": "MIT OR Apache-2.0", 170 + "optional": true, 171 + "os": [ 172 + "win32" 173 + ], 174 + "engines": { 175 + "node": ">=14.21.3" 12 176 } 13 177 }, 14 178 "node_modules/@cloudflare/kv-asset-handler": {
+5 -1
package.json
··· 8 8 "deploy": "wrangler deploy", 9 9 "test": "node --test test/*.test.js", 10 10 "test:e2e": "./test/e2e.sh", 11 - "setup": "node scripts/setup.js" 11 + "setup": "node scripts/setup.js", 12 + "format": "biome format --write .", 13 + "lint": "biome lint .", 14 + "check": "biome check ." 12 15 }, 13 16 "devDependencies": { 17 + "@biomejs/biome": "^2.3.11", 14 18 "wrangler": "^4.54.0" 15 19 } 16 20 }
+246 -222
scripts/setup.js
··· 9 9 * Usage: node scripts/setup.js --handle alice --pds https://your-pds.workers.dev 10 10 */ 11 11 12 - import { webcrypto } from 'crypto' 13 - import { writeFileSync } from 'fs' 12 + import { webcrypto } from 'node:crypto'; 13 + import { writeFileSync } from 'node:fs'; 14 14 15 15 // === ARGUMENT PARSING === 16 16 17 17 function parseArgs() { 18 - const args = process.argv.slice(2) 18 + const args = process.argv.slice(2); 19 19 const opts = { 20 20 handle: null, 21 21 pds: null, 22 22 plcUrl: 'https://plc.directory', 23 - relayUrl: 'https://bsky.network' 24 - } 23 + relayUrl: 'https://bsky.network', 24 + }; 25 25 26 26 for (let i = 0; i < args.length; i++) { 27 27 if (args[i] === '--handle' && args[i + 1]) { 28 - opts.handle = args[++i] 28 + opts.handle = args[++i]; 29 29 } else if (args[i] === '--pds' && args[i + 1]) { 30 - opts.pds = args[++i] 30 + opts.pds = args[++i]; 31 31 } else if (args[i] === '--plc-url' && args[i + 1]) { 32 - opts.plcUrl = args[++i] 32 + opts.plcUrl = args[++i]; 33 33 } else if (args[i] === '--relay-url' && args[i + 1]) { 34 - opts.relayUrl = args[++i] 34 + opts.relayUrl = args[++i]; 35 35 } 36 36 } 37 37 38 38 if (!opts.pds) { 39 - console.error('Usage: node scripts/setup.js --pds <pds-url> [--handle <subdomain>]') 40 - console.error('') 41 - console.error('Options:') 42 - console.error(' --pds PDS URL (e.g., "https://atproto-pds.chad-53c.workers.dev")') 43 - console.error(' --handle Subdomain handle (e.g., "alice") - optional, uses bare hostname if omitted') 44 - console.error(' --plc-url PLC directory URL (default: https://plc.directory)') 45 - console.error(' --relay-url Relay URL (default: https://bsky.network)') 46 - process.exit(1) 39 + console.error( 40 + 'Usage: node scripts/setup.js --pds <pds-url> [--handle <subdomain>]', 41 + ); 42 + console.error(''); 43 + console.error('Options:'); 44 + console.error( 45 + ' --pds PDS URL (e.g., "https://atproto-pds.chad-53c.workers.dev")', 46 + ); 47 + console.error( 48 + ' --handle Subdomain handle (e.g., "alice") - optional, uses bare hostname if omitted', 49 + ); 50 + console.error( 51 + ' --plc-url PLC directory URL (default: https://plc.directory)', 52 + ); 53 + console.error(' --relay-url Relay URL (default: https://bsky.network)'); 54 + process.exit(1); 47 55 } 48 56 49 - return opts 57 + return opts; 50 58 } 51 59 52 60 // === KEY GENERATION === ··· 55 63 const keyPair = await webcrypto.subtle.generateKey( 56 64 { name: 'ECDSA', namedCurve: 'P-256' }, 57 65 true, 58 - ['sign', 'verify'] 59 - ) 66 + ['sign', 'verify'], 67 + ); 60 68 61 69 // Export private key as raw 32 bytes 62 - const privateJwk = await webcrypto.subtle.exportKey('jwk', keyPair.privateKey) 63 - const privateBytes = base64UrlDecode(privateJwk.d) 70 + const privateJwk = await webcrypto.subtle.exportKey( 71 + 'jwk', 72 + keyPair.privateKey, 73 + ); 74 + const privateBytes = base64UrlDecode(privateJwk.d); 64 75 65 76 // Export public key as uncompressed point (65 bytes) 66 - const publicRaw = await webcrypto.subtle.exportKey('raw', keyPair.publicKey) 67 - const publicBytes = new Uint8Array(publicRaw) 77 + const publicRaw = await webcrypto.subtle.exportKey('raw', keyPair.publicKey); 78 + const publicBytes = new Uint8Array(publicRaw); 68 79 69 80 // Compress public key to 33 bytes 70 - const compressedPublic = compressPublicKey(publicBytes) 81 + const compressedPublic = compressPublicKey(publicBytes); 71 82 72 83 return { 73 84 privateKey: privateBytes, 74 85 publicKey: compressedPublic, 75 - cryptoKey: keyPair.privateKey 76 - } 86 + cryptoKey: keyPair.privateKey, 87 + }; 77 88 } 78 89 79 90 function compressPublicKey(uncompressed) { 80 91 // uncompressed is 65 bytes: 0x04 + x(32) + y(32) 81 - const x = uncompressed.slice(1, 33) 82 - const y = uncompressed.slice(33, 65) 83 - const prefix = (y[31] & 1) === 0 ? 0x02 : 0x03 84 - const compressed = new Uint8Array(33) 85 - compressed[0] = prefix 86 - compressed.set(x, 1) 87 - return compressed 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; 88 99 } 89 100 90 101 function base64UrlDecode(str) { 91 - const base64 = str.replace(/-/g, '+').replace(/_/g, '/') 92 - const binary = atob(base64) 93 - const bytes = new Uint8Array(binary.length) 102 + const base64 = str.replace(/-/g, '+').replace(/_/g, '/'); 103 + const binary = atob(base64); 104 + const bytes = new Uint8Array(binary.length); 94 105 for (let i = 0; i < binary.length; i++) { 95 - bytes[i] = binary.charCodeAt(i) 106 + bytes[i] = binary.charCodeAt(i); 96 107 } 97 - return bytes 108 + return bytes; 98 109 } 99 110 100 111 function bytesToHex(bytes) { 101 - return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('') 112 + return Array.from(bytes) 113 + .map((b) => b.toString(16).padStart(2, '0')) 114 + .join(''); 102 115 } 103 116 104 117 // === DID:KEY ENCODING === 105 118 106 119 // Multicodec prefix for P-256 public key (0x1200) 107 - const P256_MULTICODEC = new Uint8Array([0x80, 0x24]) 120 + const P256_MULTICODEC = new Uint8Array([0x80, 0x24]); 108 121 109 122 function publicKeyToDidKey(compressedPublicKey) { 110 123 // did:key format: "did:key:" + multibase(base58btc) of multicodec + key 111 - const keyWithCodec = new Uint8Array(P256_MULTICODEC.length + compressedPublicKey.length) 112 - keyWithCodec.set(P256_MULTICODEC) 113 - keyWithCodec.set(compressedPublicKey, P256_MULTICODEC.length) 124 + const keyWithCodec = new Uint8Array( 125 + P256_MULTICODEC.length + compressedPublicKey.length, 126 + ); 127 + keyWithCodec.set(P256_MULTICODEC); 128 + keyWithCodec.set(compressedPublicKey, P256_MULTICODEC.length); 114 129 115 - return 'did:key:z' + base58btcEncode(keyWithCodec) 130 + return `did:key:z${base58btcEncode(keyWithCodec)}`; 116 131 } 117 132 118 133 function base58btcEncode(bytes) { 119 - const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' 134 + const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; 120 135 121 136 // Count leading zeros 122 - let zeros = 0 137 + let zeros = 0; 123 138 for (const b of bytes) { 124 - if (b === 0) zeros++ 125 - else break 139 + if (b === 0) zeros++; 140 + else break; 126 141 } 127 142 128 143 // Convert to base58 129 - const digits = [0] 144 + const digits = [0]; 130 145 for (const byte of bytes) { 131 - let carry = byte 146 + let carry = byte; 132 147 for (let i = 0; i < digits.length; i++) { 133 - carry += digits[i] << 8 134 - digits[i] = carry % 58 135 - carry = (carry / 58) | 0 148 + carry += digits[i] << 8; 149 + digits[i] = carry % 58; 150 + carry = (carry / 58) | 0; 136 151 } 137 152 while (carry > 0) { 138 - digits.push(carry % 58) 139 - carry = (carry / 58) | 0 153 + digits.push(carry % 58); 154 + carry = (carry / 58) | 0; 140 155 } 141 156 } 142 157 143 158 // Convert to string 144 - let result = '1'.repeat(zeros) 159 + let result = '1'.repeat(zeros); 145 160 for (let i = digits.length - 1; i >= 0; i--) { 146 - result += ALPHABET[digits[i]] 161 + result += ALPHABET[digits[i]]; 147 162 } 148 163 149 - return result 164 + return result; 150 165 } 151 166 152 167 // === CBOR ENCODING (dag-cbor compliant for PLC operations) === 153 168 154 169 function cborEncodeKey(key) { 155 170 // 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 171 + const bytes = new TextEncoder().encode(key); 172 + const parts = []; 173 + const mt = 3 << 5; // major type 3 = text string 159 174 if (bytes.length < 24) { 160 - parts.push(mt | bytes.length) 175 + parts.push(mt | bytes.length); 161 176 } else if (bytes.length < 256) { 162 - parts.push(mt | 24, bytes.length) 177 + parts.push(mt | 24, bytes.length); 163 178 } else if (bytes.length < 65536) { 164 - parts.push(mt | 25, bytes.length >> 8, bytes.length & 0xff) 179 + parts.push(mt | 25, bytes.length >> 8, bytes.length & 0xff); 165 180 } 166 - parts.push(...bytes) 167 - return new Uint8Array(parts) 181 + parts.push(...bytes); 182 + return new Uint8Array(parts); 168 183 } 169 184 170 185 function compareBytes(a, b) { 171 186 // dag-cbor: bytewise lexicographic order of encoded keys 172 - const minLen = Math.min(a.length, b.length) 187 + const minLen = Math.min(a.length, b.length); 173 188 for (let i = 0; i < minLen; i++) { 174 - if (a[i] !== b[i]) return a[i] - b[i] 189 + if (a[i] !== b[i]) return a[i] - b[i]; 175 190 } 176 - return a.length - b.length 191 + return a.length - b.length; 177 192 } 178 193 179 194 function cborEncode(value) { 180 - const parts = [] 195 + const parts = []; 181 196 182 197 function encode(val) { 183 198 if (val === null) { 184 - parts.push(0xf6) 199 + parts.push(0xf6); 185 200 } else if (typeof val === 'string') { 186 - const bytes = new TextEncoder().encode(val) 187 - encodeHead(3, bytes.length) 188 - parts.push(...bytes) 201 + const bytes = new TextEncoder().encode(val); 202 + encodeHead(3, bytes.length); 203 + parts.push(...bytes); 189 204 } else if (typeof val === 'number') { 190 205 if (Number.isInteger(val) && val >= 0) { 191 - encodeHead(0, val) 206 + encodeHead(0, val); 192 207 } 193 208 } else if (val instanceof Uint8Array) { 194 - encodeHead(2, val.length) 195 - parts.push(...val) 209 + encodeHead(2, val.length); 210 + parts.push(...val); 196 211 } else if (Array.isArray(val)) { 197 - encodeHead(4, val.length) 198 - for (const item of val) encode(item) 212 + encodeHead(4, val.length); 213 + for (const item of val) encode(item); 199 214 } else if (typeof val === 'object') { 200 215 // 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) 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); 204 221 for (const key of keysSorted) { 205 - encode(key) 206 - encode(val[key]) 222 + encode(key); 223 + encode(val[key]); 207 224 } 208 225 } 209 226 } 210 227 211 228 function encodeHead(majorType, length) { 212 - const mt = majorType << 5 229 + const mt = majorType << 5; 213 230 if (length < 24) { 214 - parts.push(mt | length) 231 + parts.push(mt | length); 215 232 } else if (length < 256) { 216 - parts.push(mt | 24, length) 233 + parts.push(mt | 24, length); 217 234 } else if (length < 65536) { 218 - parts.push(mt | 25, length >> 8, length & 0xff) 235 + parts.push(mt | 25, length >> 8, length & 0xff); 219 236 } 220 237 } 221 238 222 - encode(value) 223 - return new Uint8Array(parts) 239 + encode(value); 240 + return new Uint8Array(parts); 224 241 } 225 242 226 243 // === HASHING === 227 244 228 245 async function sha256(data) { 229 - const hash = await webcrypto.subtle.digest('SHA-256', data) 230 - return new Uint8Array(hash) 246 + const hash = await webcrypto.subtle.digest('SHA-256', data); 247 + return new Uint8Array(hash); 231 248 } 232 249 233 250 // === PLC OPERATIONS === 234 251 235 252 async function signPlcOperation(operation, privateKey) { 236 253 // Encode operation without sig field 237 - const { sig, ...opWithoutSig } = operation 238 - const encoded = cborEncode(opWithoutSig) 254 + const { sig, ...opWithoutSig } = operation; 255 + const encoded = cborEncode(opWithoutSig); 239 256 240 257 // Sign with P-256 241 258 const signature = await webcrypto.subtle.sign( 242 259 { name: 'ECDSA', hash: 'SHA-256' }, 243 260 privateKey, 244 - encoded 245 - ) 261 + encoded, 262 + ); 246 263 247 264 // Convert to low-S form and base64url encode 248 - const sigBytes = ensureLowS(new Uint8Array(signature)) 249 - return base64UrlEncode(sigBytes) 265 + const sigBytes = ensureLowS(new Uint8Array(signature)); 266 + return base64UrlEncode(sigBytes); 250 267 } 251 268 252 269 function ensureLowS(sig) { 253 270 // P-256 order N 254 - const N = BigInt('0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551') 255 - const halfN = N / 2n 271 + const N = BigInt( 272 + '0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551', 273 + ); 274 + const halfN = N / 2n; 256 275 257 - const r = sig.slice(0, 32) 258 - const s = sig.slice(32, 64) 276 + const r = sig.slice(0, 32); 277 + const s = sig.slice(32, 64); 259 278 260 279 // Convert s to BigInt 261 - let sInt = BigInt('0x' + bytesToHex(s)) 280 + let sInt = BigInt(`0x${bytesToHex(s)}`); 262 281 263 282 // If s > N/2, replace with N - s 264 283 if (sInt > halfN) { 265 - sInt = N - sInt 266 - const newS = hexToBytes(sInt.toString(16).padStart(64, '0')) 267 - const result = new Uint8Array(64) 268 - result.set(r) 269 - result.set(newS, 32) 270 - return result 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; 271 290 } 272 291 273 - return sig 292 + return sig; 274 293 } 275 294 276 295 function hexToBytes(hex) { 277 - const bytes = new Uint8Array(hex.length / 2) 296 + const bytes = new Uint8Array(hex.length / 2); 278 297 for (let i = 0; i < hex.length; i += 2) { 279 - bytes[i / 2] = parseInt(hex.substr(i, 2), 16) 298 + bytes[i / 2] = parseInt(hex.substr(i, 2), 16); 280 299 } 281 - return bytes 300 + return bytes; 282 301 } 283 302 284 303 function base64UrlEncode(bytes) { 285 - const binary = String.fromCharCode(...bytes) 286 - return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') 304 + const binary = String.fromCharCode(...bytes); 305 + return btoa(binary) 306 + .replace(/\+/g, '-') 307 + .replace(/\//g, '_') 308 + .replace(/=+$/, ''); 287 309 } 288 310 289 311 async function createGenesisOperation(opts) { 290 - const { didKey, handle, pdsUrl, cryptoKey } = opts 312 + const { didKey, handle, pdsUrl, cryptoKey } = opts; 291 313 292 314 // Build full handle: subdomain.pds-hostname, or just pds-hostname if no subdomain 293 - const pdsHost = new URL(pdsUrl).host 294 - const fullHandle = handle ? `${handle}.${pdsHost}` : pdsHost 315 + const pdsHost = new URL(pdsUrl).host; 316 + const fullHandle = handle ? `${handle}.${pdsHost}` : pdsHost; 295 317 296 318 const operation = { 297 319 type: 'plc_operation', 298 320 rotationKeys: [didKey], 299 321 verificationMethods: { 300 - atproto: didKey 322 + atproto: didKey, 301 323 }, 302 324 alsoKnownAs: [`at://${fullHandle}`], 303 325 services: { 304 326 atproto_pds: { 305 327 type: 'AtprotoPersonalDataServer', 306 - endpoint: pdsUrl 307 - } 328 + endpoint: pdsUrl, 329 + }, 308 330 }, 309 - prev: null 310 - } 331 + prev: null, 332 + }; 311 333 312 334 // Sign the operation 313 - operation.sig = await signPlcOperation(operation, cryptoKey) 335 + operation.sig = await signPlcOperation(operation, cryptoKey); 314 336 315 - return { operation, fullHandle } 337 + return { operation, fullHandle }; 316 338 } 317 339 318 340 async function deriveDidFromOperation(operation) { 319 341 // DID is computed from the FULL operation INCLUDING the signature 320 - const encoded = cborEncode(operation) 321 - const hash = await sha256(encoded) 342 + const encoded = cborEncode(operation); 343 + const hash = await sha256(encoded); 322 344 // DID is base32 of first 15 bytes of hash (= 24 base32 chars) 323 - return 'did:plc:' + base32Encode(hash.slice(0, 15)) 345 + return `did:plc:${base32Encode(hash.slice(0, 15))}`; 324 346 } 325 347 326 348 function base32Encode(bytes) { 327 - const alphabet = 'abcdefghijklmnopqrstuvwxyz234567' 328 - let result = '' 329 - let bits = 0 330 - let value = 0 349 + const alphabet = 'abcdefghijklmnopqrstuvwxyz234567'; 350 + let result = ''; 351 + let bits = 0; 352 + let value = 0; 331 353 332 354 for (const byte of bytes) { 333 - value = (value << 8) | byte 334 - bits += 8 355 + value = (value << 8) | byte; 356 + bits += 8; 335 357 while (bits >= 5) { 336 - bits -= 5 337 - result += alphabet[(value >> bits) & 31] 358 + bits -= 5; 359 + result += alphabet[(value >> bits) & 31]; 338 360 } 339 361 } 340 362 341 363 if (bits > 0) { 342 - result += alphabet[(value << (5 - bits)) & 31] 364 + result += alphabet[(value << (5 - bits)) & 31]; 343 365 } 344 366 345 - return result 367 + return result; 346 368 } 347 369 348 370 // === PLC DIRECTORY REGISTRATION === 349 371 350 372 async function registerWithPlc(plcUrl, did, operation) { 351 - const url = `${plcUrl}/${encodeURIComponent(did)}` 373 + const url = `${plcUrl}/${encodeURIComponent(did)}`; 352 374 353 375 const response = await fetch(url, { 354 376 method: 'POST', 355 377 headers: { 356 - 'Content-Type': 'application/json' 378 + 'Content-Type': 'application/json', 357 379 }, 358 - body: JSON.stringify(operation) 359 - }) 380 + body: JSON.stringify(operation), 381 + }); 360 382 361 383 if (!response.ok) { 362 - const text = await response.text() 363 - throw new Error(`PLC registration failed: ${response.status} ${text}`) 384 + const text = await response.text(); 385 + throw new Error(`PLC registration failed: ${response.status} ${text}`); 364 386 } 365 387 366 - return true 388 + return true; 367 389 } 368 390 369 391 // === PDS INITIALIZATION === 370 392 371 393 async function initializePds(pdsUrl, did, privateKeyHex, handle) { 372 - const url = `${pdsUrl}/init?did=${encodeURIComponent(did)}` 394 + const url = `${pdsUrl}/init?did=${encodeURIComponent(did)}`; 373 395 374 396 const response = await fetch(url, { 375 397 method: 'POST', 376 398 headers: { 377 - 'Content-Type': 'application/json' 399 + 'Content-Type': 'application/json', 378 400 }, 379 401 body: JSON.stringify({ 380 402 did, 381 403 privateKey: privateKeyHex, 382 - handle 383 - }) 384 - }) 404 + handle, 405 + }), 406 + }); 385 407 386 408 if (!response.ok) { 387 - const text = await response.text() 388 - throw new Error(`PDS initialization failed: ${response.status} ${text}`) 409 + const text = await response.text(); 410 + throw new Error(`PDS initialization failed: ${response.status} ${text}`); 389 411 } 390 412 391 - return response.json() 413 + return response.json(); 392 414 } 393 415 394 416 // === HANDLE REGISTRATION === 395 417 396 418 async function registerHandle(pdsUrl, handle, did) { 397 - const url = `${pdsUrl}/register-handle` 419 + const url = `${pdsUrl}/register-handle`; 398 420 399 421 const response = await fetch(url, { 400 422 method: 'POST', 401 423 headers: { 402 - 'Content-Type': 'application/json' 424 + 'Content-Type': 'application/json', 403 425 }, 404 - body: JSON.stringify({ handle, did }) 405 - }) 426 + body: JSON.stringify({ handle, did }), 427 + }); 406 428 407 429 if (!response.ok) { 408 - const text = await response.text() 409 - throw new Error(`Handle registration failed: ${response.status} ${text}`) 430 + const text = await response.text(); 431 + throw new Error(`Handle registration failed: ${response.status} ${text}`); 410 432 } 411 433 412 - return true 434 + return true; 413 435 } 414 436 415 437 // === RELAY NOTIFICATION === 416 438 417 439 async function notifyRelay(relayUrl, pdsHostname) { 418 - const url = `${relayUrl}/xrpc/com.atproto.sync.requestCrawl` 440 + const url = `${relayUrl}/xrpc/com.atproto.sync.requestCrawl`; 419 441 420 442 const response = await fetch(url, { 421 443 method: 'POST', 422 444 headers: { 423 - 'Content-Type': 'application/json' 445 + 'Content-Type': 'application/json', 424 446 }, 425 447 body: JSON.stringify({ 426 - hostname: pdsHostname 427 - }) 428 - }) 448 + hostname: pdsHostname, 449 + }), 450 + }); 429 451 430 452 // Relay might return 200 or 202, both are OK 431 453 if (!response.ok && response.status !== 202) { 432 - const text = await response.text() 433 - console.warn(` Warning: Relay notification returned ${response.status}: ${text}`) 434 - return false 454 + const text = await response.text(); 455 + console.warn( 456 + ` Warning: Relay notification returned ${response.status}: ${text}`, 457 + ); 458 + return false; 435 459 } 436 460 437 - return true 461 + return true; 438 462 } 439 463 440 464 // === CREDENTIALS OUTPUT === 441 465 442 466 function saveCredentials(filename, credentials) { 443 - writeFileSync(filename, JSON.stringify(credentials, null, 2)) 467 + writeFileSync(filename, JSON.stringify(credentials, null, 2)); 444 468 } 445 469 446 470 // === MAIN === 447 471 448 472 async function main() { 449 - const opts = parseArgs() 473 + const opts = parseArgs(); 450 474 451 - console.log('PDS Federation Setup') 452 - console.log('====================') 453 - console.log(`PDS: ${opts.pds}`) 454 - console.log('') 475 + console.log('PDS Federation Setup'); 476 + console.log('===================='); 477 + console.log(`PDS: ${opts.pds}`); 478 + console.log(''); 455 479 456 480 // Step 1: Generate keypair 457 - console.log('Generating P-256 keypair...') 458 - const keyPair = await generateP256Keypair() 459 - const didKey = publicKeyToDidKey(keyPair.publicKey) 460 - console.log(` did:key: ${didKey}`) 461 - console.log('') 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(''); 462 486 463 487 // Step 2: Create genesis operation 464 - console.log('Creating PLC genesis operation...') 488 + console.log('Creating PLC genesis operation...'); 465 489 const { operation, fullHandle } = await createGenesisOperation({ 466 490 didKey, 467 491 handle: opts.handle, 468 492 pdsUrl: opts.pds, 469 - cryptoKey: keyPair.cryptoKey 470 - }) 471 - const did = await deriveDidFromOperation(operation) 472 - console.log(` DID: ${did}`) 473 - console.log(` Handle: ${fullHandle}`) 474 - console.log('') 493 + cryptoKey: keyPair.cryptoKey, 494 + }); 495 + const did = await deriveDidFromOperation(operation); 496 + console.log(` DID: ${did}`); 497 + console.log(` Handle: ${fullHandle}`); 498 + console.log(''); 475 499 476 500 // Step 3: Register with PLC directory 477 - console.log(`Registering with ${opts.plcUrl}...`) 478 - await registerWithPlc(opts.plcUrl, did, operation) 479 - console.log(' Registered successfully!') 480 - console.log('') 501 + console.log(`Registering with ${opts.plcUrl}...`); 502 + await registerWithPlc(opts.plcUrl, did, operation); 503 + console.log(' Registered successfully!'); 504 + console.log(''); 481 505 482 506 // Step 4: Initialize PDS 483 - console.log(`Initializing PDS at ${opts.pds}...`) 484 - const privateKeyHex = bytesToHex(keyPair.privateKey) 485 - await initializePds(opts.pds, did, privateKeyHex, fullHandle) 486 - console.log(' PDS initialized!') 487 - console.log('') 507 + console.log(`Initializing PDS at ${opts.pds}...`); 508 + const privateKeyHex = bytesToHex(keyPair.privateKey); 509 + await initializePds(opts.pds, did, privateKeyHex, fullHandle); 510 + console.log(' PDS initialized!'); 511 + console.log(''); 488 512 489 513 // Step 4b: Register handle -> DID mapping (only for subdomain handles) 490 514 if (opts.handle) { 491 - console.log(`Registering handle mapping...`) 492 - await registerHandle(opts.pds, opts.handle, did) 493 - console.log(` Handle ${opts.handle} -> ${did}`) 494 - console.log('') 515 + console.log(`Registering handle mapping...`); 516 + await registerHandle(opts.pds, opts.handle, did); 517 + console.log(` Handle ${opts.handle} -> ${did}`); 518 + console.log(''); 495 519 } 496 520 497 521 // Step 5: Notify relay 498 - const pdsHostname = new URL(opts.pds).host 499 - console.log(`Notifying relay at ${opts.relayUrl}...`) 500 - const relayOk = await notifyRelay(opts.relayUrl, pdsHostname) 522 + const pdsHostname = new URL(opts.pds).host; 523 + console.log(`Notifying relay at ${opts.relayUrl}...`); 524 + const relayOk = await notifyRelay(opts.relayUrl, pdsHostname); 501 525 if (relayOk) { 502 - console.log(' Relay notified!') 526 + console.log(' Relay notified!'); 503 527 } 504 - console.log('') 528 + console.log(''); 505 529 506 530 // Step 6: Save credentials 507 531 const credentials = { ··· 510 534 privateKeyHex: bytesToHex(keyPair.privateKey), 511 535 didKey, 512 536 pdsUrl: opts.pds, 513 - createdAt: new Date().toISOString() 514 - } 537 + createdAt: new Date().toISOString(), 538 + }; 515 539 516 - const credentialsFile = `./credentials-${opts.handle || new URL(opts.pds).host}.json` 517 - saveCredentials(credentialsFile, credentials) 540 + const credentialsFile = `./credentials-${opts.handle || new URL(opts.pds).host}.json`; 541 + saveCredentials(credentialsFile, credentials); 518 542 519 543 // Final output 520 - console.log('Setup Complete!') 521 - console.log('===============') 522 - console.log(`Handle: ${fullHandle}`) 523 - console.log(`DID: ${did}`) 524 - console.log(`PDS: ${opts.pds}`) 525 - console.log('') 526 - console.log(`Credentials saved to: ${credentialsFile}`) 527 - console.log('Keep this file safe - it contains your private key!') 544 + console.log('Setup Complete!'); 545 + console.log('==============='); 546 + console.log(`Handle: ${fullHandle}`); 547 + console.log(`DID: ${did}`); 548 + console.log(`PDS: ${opts.pds}`); 549 + console.log(''); 550 + console.log(`Credentials saved to: ${credentialsFile}`); 551 + console.log('Keep this file safe - it contains your private key!'); 528 552 } 529 553 530 - main().catch(err => { 531 - console.error('Error:', err.message) 532 - process.exit(1) 533 - }) 554 + main().catch((err) => { 555 + console.error('Error:', err.message); 556 + process.exit(1); 557 + });
+128 -117
scripts/update-did.js
··· 6 6 * Usage: node scripts/update-did.js --credentials <file> --new-handle <handle> --new-pds <url> 7 7 */ 8 8 9 - import { webcrypto } from 'crypto' 10 - import { readFileSync, writeFileSync } from 'fs' 9 + import { webcrypto } from 'node:crypto'; 10 + import { readFileSync, writeFileSync } from 'node:fs'; 11 11 12 12 // === ARGUMENT PARSING === 13 13 14 14 function parseArgs() { 15 - const args = process.argv.slice(2) 15 + const args = process.argv.slice(2); 16 16 const opts = { 17 17 credentials: null, 18 18 newHandle: null, 19 19 newPds: null, 20 - plcUrl: 'https://plc.directory' 21 - } 20 + plcUrl: 'https://plc.directory', 21 + }; 22 22 23 23 for (let i = 0; i < args.length; i++) { 24 24 if (args[i] === '--credentials' && args[i + 1]) { 25 - opts.credentials = args[++i] 25 + opts.credentials = args[++i]; 26 26 } else if (args[i] === '--new-handle' && args[i + 1]) { 27 - opts.newHandle = args[++i] 27 + opts.newHandle = args[++i]; 28 28 } else if (args[i] === '--new-pds' && args[i + 1]) { 29 - opts.newPds = args[++i] 29 + opts.newPds = args[++i]; 30 30 } else if (args[i] === '--plc-url' && args[i + 1]) { 31 - opts.plcUrl = args[++i] 31 + opts.plcUrl = args[++i]; 32 32 } 33 33 } 34 34 35 35 if (!opts.credentials || !opts.newHandle || !opts.newPds) { 36 - console.error('Usage: node scripts/update-did.js --credentials <file> --new-handle <handle> --new-pds <url>') 37 - process.exit(1) 36 + console.error( 37 + 'Usage: node scripts/update-did.js --credentials <file> --new-handle <handle> --new-pds <url>', 38 + ); 39 + process.exit(1); 38 40 } 39 41 40 - return opts 42 + return opts; 41 43 } 42 44 43 45 // === CRYPTO HELPERS === 44 46 45 47 function hexToBytes(hex) { 46 - const bytes = new Uint8Array(hex.length / 2) 48 + const bytes = new Uint8Array(hex.length / 2); 47 49 for (let i = 0; i < hex.length; i += 2) { 48 - bytes[i / 2] = parseInt(hex.substr(i, 2), 16) 50 + bytes[i / 2] = parseInt(hex.substr(i, 2), 16); 49 51 } 50 - return bytes 52 + return bytes; 51 53 } 52 54 53 55 function bytesToHex(bytes) { 54 - return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('') 56 + return Array.from(bytes) 57 + .map((b) => b.toString(16).padStart(2, '0')) 58 + .join(''); 55 59 } 56 60 57 61 async function importPrivateKey(privateKeyBytes) { 58 62 const pkcs8Prefix = new Uint8Array([ 59 63 0x30, 0x41, 0x02, 0x01, 0x00, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 60 64 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 61 - 0x01, 0x07, 0x04, 0x27, 0x30, 0x25, 0x02, 0x01, 0x01, 0x04, 0x20 62 - ]) 65 + 0x01, 0x07, 0x04, 0x27, 0x30, 0x25, 0x02, 0x01, 0x01, 0x04, 0x20, 66 + ]); 63 67 64 - const pkcs8 = new Uint8Array(pkcs8Prefix.length + 32) 65 - pkcs8.set(pkcs8Prefix) 66 - pkcs8.set(privateKeyBytes, pkcs8Prefix.length) 68 + const pkcs8 = new Uint8Array(pkcs8Prefix.length + 32); 69 + pkcs8.set(pkcs8Prefix); 70 + pkcs8.set(privateKeyBytes, pkcs8Prefix.length); 67 71 68 72 return webcrypto.subtle.importKey( 69 73 'pkcs8', 70 74 pkcs8, 71 75 { name: 'ECDSA', namedCurve: 'P-256' }, 72 76 false, 73 - ['sign'] 74 - ) 77 + ['sign'], 78 + ); 75 79 } 76 80 77 81 // === CBOR ENCODING === 78 82 79 83 function cborEncodeKey(key) { 80 - const bytes = new TextEncoder().encode(key) 81 - const parts = [] 82 - const mt = 3 << 5 84 + const bytes = new TextEncoder().encode(key); 85 + const parts = []; 86 + const mt = 3 << 5; 83 87 if (bytes.length < 24) { 84 - parts.push(mt | bytes.length) 88 + parts.push(mt | bytes.length); 85 89 } else if (bytes.length < 256) { 86 - parts.push(mt | 24, bytes.length) 90 + parts.push(mt | 24, bytes.length); 87 91 } 88 - parts.push(...bytes) 89 - return new Uint8Array(parts) 92 + parts.push(...bytes); 93 + return new Uint8Array(parts); 90 94 } 91 95 92 96 function compareBytes(a, b) { 93 - const minLen = Math.min(a.length, b.length) 97 + const minLen = Math.min(a.length, b.length); 94 98 for (let i = 0; i < minLen; i++) { 95 - if (a[i] !== b[i]) return a[i] - b[i] 99 + if (a[i] !== b[i]) return a[i] - b[i]; 96 100 } 97 - return a.length - b.length 101 + return a.length - b.length; 98 102 } 99 103 100 104 function cborEncode(value) { 101 - const parts = [] 105 + const parts = []; 102 106 103 107 function encode(val) { 104 108 if (val === null) { 105 - parts.push(0xf6) 109 + parts.push(0xf6); 106 110 } else if (typeof val === 'string') { 107 - const bytes = new TextEncoder().encode(val) 108 - encodeHead(3, bytes.length) 109 - parts.push(...bytes) 111 + const bytes = new TextEncoder().encode(val); 112 + encodeHead(3, bytes.length); 113 + parts.push(...bytes); 110 114 } else if (typeof val === 'number') { 111 115 if (Number.isInteger(val) && val >= 0) { 112 - encodeHead(0, val) 116 + encodeHead(0, val); 113 117 } 114 118 } else if (val instanceof Uint8Array) { 115 - encodeHead(2, val.length) 116 - parts.push(...val) 119 + encodeHead(2, val.length); 120 + parts.push(...val); 117 121 } else if (Array.isArray(val)) { 118 - encodeHead(4, val.length) 119 - for (const item of val) encode(item) 122 + encodeHead(4, val.length); 123 + for (const item of val) encode(item); 120 124 } else if (typeof val === 'object') { 121 - const keys = Object.keys(val) 122 - const keysSorted = keys.sort((a, b) => compareBytes(cborEncodeKey(a), cborEncodeKey(b))) 123 - encodeHead(5, keysSorted.length) 125 + const keys = Object.keys(val); 126 + const keysSorted = keys.sort((a, b) => 127 + compareBytes(cborEncodeKey(a), cborEncodeKey(b)), 128 + ); 129 + encodeHead(5, keysSorted.length); 124 130 for (const key of keysSorted) { 125 - encode(key) 126 - encode(val[key]) 131 + encode(key); 132 + encode(val[key]); 127 133 } 128 134 } 129 135 } 130 136 131 137 function encodeHead(majorType, length) { 132 - const mt = majorType << 5 138 + const mt = majorType << 5; 133 139 if (length < 24) { 134 - parts.push(mt | length) 140 + parts.push(mt | length); 135 141 } else if (length < 256) { 136 - parts.push(mt | 24, length) 142 + parts.push(mt | 24, length); 137 143 } else if (length < 65536) { 138 - parts.push(mt | 25, length >> 8, length & 0xff) 144 + parts.push(mt | 25, length >> 8, length & 0xff); 139 145 } 140 146 } 141 147 142 - encode(value) 143 - return new Uint8Array(parts) 148 + encode(value); 149 + return new Uint8Array(parts); 144 150 } 145 151 146 152 // === SIGNING === 147 153 148 - const P256_N = BigInt('0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551') 154 + const P256_N = BigInt( 155 + '0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551', 156 + ); 149 157 150 158 function ensureLowS(sig) { 151 - const halfN = P256_N / 2n 152 - const r = sig.slice(0, 32) 153 - const s = sig.slice(32, 64) 154 - let sInt = BigInt('0x' + bytesToHex(s)) 159 + const halfN = P256_N / 2n; 160 + const r = sig.slice(0, 32); 161 + const s = sig.slice(32, 64); 162 + let sInt = BigInt(`0x${bytesToHex(s)}`); 155 163 156 164 if (sInt > halfN) { 157 - sInt = P256_N - sInt 158 - const newS = hexToBytes(sInt.toString(16).padStart(64, '0')) 159 - const result = new Uint8Array(64) 160 - result.set(r) 161 - result.set(newS, 32) 162 - return result 165 + sInt = P256_N - sInt; 166 + const newS = hexToBytes(sInt.toString(16).padStart(64, '0')); 167 + const result = new Uint8Array(64); 168 + result.set(r); 169 + result.set(newS, 32); 170 + return result; 163 171 } 164 - return sig 172 + return sig; 165 173 } 166 174 167 175 function base64UrlEncode(bytes) { 168 - const binary = String.fromCharCode(...bytes) 169 - return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') 176 + const binary = String.fromCharCode(...bytes); 177 + return btoa(binary) 178 + .replace(/\+/g, '-') 179 + .replace(/\//g, '_') 180 + .replace(/=+$/, ''); 170 181 } 171 182 172 183 async function signPlcOperation(operation, privateKey) { 173 - const { sig, ...opWithoutSig } = operation 174 - const encoded = cborEncode(opWithoutSig) 184 + const { sig, ...opWithoutSig } = operation; 185 + const encoded = cborEncode(opWithoutSig); 175 186 176 187 const signature = await webcrypto.subtle.sign( 177 188 { name: 'ECDSA', hash: 'SHA-256' }, 178 189 privateKey, 179 - encoded 180 - ) 190 + encoded, 191 + ); 181 192 182 - const sigBytes = ensureLowS(new Uint8Array(signature)) 183 - return base64UrlEncode(sigBytes) 193 + const sigBytes = ensureLowS(new Uint8Array(signature)); 194 + return base64UrlEncode(sigBytes); 184 195 } 185 196 186 197 // === MAIN === 187 198 188 199 async function main() { 189 - const opts = parseArgs() 200 + const opts = parseArgs(); 190 201 191 202 // Load credentials 192 - const creds = JSON.parse(readFileSync(opts.credentials, 'utf-8')) 193 - console.log(`Updating DID: ${creds.did}`) 194 - console.log(` Old handle: ${creds.handle}`) 195 - console.log(` New handle: ${opts.newHandle}`) 196 - console.log(` New PDS: ${opts.newPds}`) 197 - console.log('') 203 + const creds = JSON.parse(readFileSync(opts.credentials, 'utf-8')); 204 + console.log(`Updating DID: ${creds.did}`); 205 + console.log(` Old handle: ${creds.handle}`); 206 + console.log(` New handle: ${opts.newHandle}`); 207 + console.log(` New PDS: ${opts.newPds}`); 208 + console.log(''); 198 209 199 210 // Fetch current operation log 200 - console.log('Fetching current PLC operation log...') 201 - const logRes = await fetch(`${opts.plcUrl}/${creds.did}/log/audit`) 211 + console.log('Fetching current PLC operation log...'); 212 + const logRes = await fetch(`${opts.plcUrl}/${creds.did}/log/audit`); 202 213 if (!logRes.ok) { 203 - throw new Error(`Failed to fetch PLC log: ${logRes.status}`) 214 + throw new Error(`Failed to fetch PLC log: ${logRes.status}`); 204 215 } 205 - const log = await logRes.json() 206 - const lastOp = log[log.length - 1] 207 - console.log(` Found ${log.length} operations`) 208 - console.log(` Last CID: ${lastOp.cid}`) 209 - console.log('') 216 + const log = await logRes.json(); 217 + const lastOp = log[log.length - 1]; 218 + console.log(` Found ${log.length} operations`); 219 + console.log(` Last CID: ${lastOp.cid}`); 220 + console.log(''); 210 221 211 222 // Import private key 212 - const privateKey = await importPrivateKey(hexToBytes(creds.privateKeyHex)) 223 + const privateKey = await importPrivateKey(hexToBytes(creds.privateKeyHex)); 213 224 214 225 // Create new operation 215 226 const newOp = { ··· 220 231 services: { 221 232 atproto_pds: { 222 233 type: 'AtprotoPersonalDataServer', 223 - endpoint: opts.newPds 224 - } 234 + endpoint: opts.newPds, 235 + }, 225 236 }, 226 - prev: lastOp.cid 227 - } 237 + prev: lastOp.cid, 238 + }; 228 239 229 240 // Sign the operation 230 - console.log('Signing new operation...') 231 - newOp.sig = await signPlcOperation(newOp, privateKey) 241 + console.log('Signing new operation...'); 242 + newOp.sig = await signPlcOperation(newOp, privateKey); 232 243 233 244 // Submit to PLC 234 - console.log('Submitting to PLC directory...') 245 + console.log('Submitting to PLC directory...'); 235 246 const submitRes = await fetch(`${opts.plcUrl}/${creds.did}`, { 236 247 method: 'POST', 237 248 headers: { 'Content-Type': 'application/json' }, 238 - body: JSON.stringify(newOp) 239 - }) 249 + body: JSON.stringify(newOp), 250 + }); 240 251 241 252 if (!submitRes.ok) { 242 - const text = await submitRes.text() 243 - throw new Error(`PLC update failed: ${submitRes.status} ${text}`) 253 + const text = await submitRes.text(); 254 + throw new Error(`PLC update failed: ${submitRes.status} ${text}`); 244 255 } 245 256 246 - console.log(' Updated successfully!') 247 - console.log('') 257 + console.log(' Updated successfully!'); 258 + console.log(''); 248 259 249 260 // Update credentials file 250 - creds.handle = opts.newHandle 251 - creds.pdsUrl = opts.newPds 252 - writeFileSync(opts.credentials, JSON.stringify(creds, null, 2)) 253 - console.log(`Updated credentials file: ${opts.credentials}`) 261 + creds.handle = opts.newHandle; 262 + creds.pdsUrl = opts.newPds; 263 + writeFileSync(opts.credentials, JSON.stringify(creds, null, 2)); 264 + console.log(`Updated credentials file: ${opts.credentials}`); 254 265 255 266 // Verify 256 - console.log('') 257 - console.log('Verifying...') 258 - const verifyRes = await fetch(`${opts.plcUrl}/${creds.did}`) 259 - const didDoc = await verifyRes.json() 260 - console.log(` alsoKnownAs: ${didDoc.alsoKnownAs}`) 261 - console.log(` PDS endpoint: ${didDoc.service[0].serviceEndpoint}`) 267 + console.log(''); 268 + console.log('Verifying...'); 269 + const verifyRes = await fetch(`${opts.plcUrl}/${creds.did}`); 270 + const didDoc = await verifyRes.json(); 271 + console.log(` alsoKnownAs: ${didDoc.alsoKnownAs}`); 272 + console.log(` PDS endpoint: ${didDoc.service[0].serviceEndpoint}`); 262 273 } 263 274 264 - main().catch(err => { 265 - console.error('Error:', err.message) 266 - process.exit(1) 267 - }) 275 + main().catch((err) => { 276 + console.error('Error:', err.message); 277 + process.exit(1); 278 + });
+1182 -974
src/pds.js
··· 16 16 17 17 // === CONSTANTS === 18 18 // CBOR primitive markers (RFC 8949) 19 - const CBOR_FALSE = 0xf4 20 - const CBOR_TRUE = 0xf5 21 - const CBOR_NULL = 0xf6 19 + const CBOR_FALSE = 0xf4; 20 + const CBOR_TRUE = 0xf5; 21 + const CBOR_NULL = 0xf6; 22 22 23 23 // DAG-CBOR CID link tag 24 - const CBOR_TAG_CID = 42 24 + const CBOR_TAG_CID = 42; 25 25 26 26 // === ERROR HELPER === 27 27 function errorResponse(error, message, status) { 28 - return Response.json({ error, message }, { status }) 28 + return Response.json({ error, message }, { status }); 29 29 } 30 30 31 31 // === CRAWLER NOTIFICATION === 32 32 // Notify relays to come crawl us after writes (like official PDS) 33 - let lastCrawlNotify = 0 34 - const CRAWL_NOTIFY_THRESHOLD = 20 * 60 * 1000 // 20 minutes (matches official PDS) 33 + let lastCrawlNotify = 0; 34 + const CRAWL_NOTIFY_THRESHOLD = 20 * 60 * 1000; // 20 minutes (matches official PDS) 35 35 36 36 async function notifyCrawlers(env, hostname) { 37 - const now = Date.now() 37 + const now = Date.now(); 38 38 if (now - lastCrawlNotify < CRAWL_NOTIFY_THRESHOLD) { 39 - return // Throttle notifications 39 + return; // Throttle notifications 40 40 } 41 41 42 - const relayHost = env.RELAY_HOST 43 - if (!relayHost) return 42 + const relayHost = env.RELAY_HOST; 43 + if (!relayHost) return; 44 44 45 - lastCrawlNotify = now 45 + lastCrawlNotify = now; 46 46 47 47 // Fire and forget - don't block writes on relay notification 48 48 fetch(`${relayHost}/xrpc/com.atproto.sync.requestCrawl`, { 49 49 method: 'POST', 50 50 headers: { 'Content-Type': 'application/json' }, 51 - body: JSON.stringify({ hostname }) 52 - }).catch(err => { 53 - console.log('Failed to notify relay:', err.message) 54 - }) 51 + body: JSON.stringify({ hostname }), 52 + }).catch((err) => { 53 + console.log('Failed to notify relay:', err.message); 54 + }); 55 55 } 56 56 57 57 // === CID WRAPPER === ··· 60 60 class CID { 61 61 constructor(bytes) { 62 62 if (!(bytes instanceof Uint8Array)) { 63 - throw new Error('CID must be constructed with Uint8Array') 63 + throw new Error('CID must be constructed with Uint8Array'); 64 64 } 65 - this.bytes = bytes 65 + this.bytes = bytes; 66 66 } 67 67 } 68 68 ··· 76 76 * @param {number} length - Value or length to encode 77 77 */ 78 78 function encodeHead(parts, majorType, length) { 79 - const mt = majorType << 5 79 + const mt = majorType << 5; 80 80 if (length < 24) { 81 - parts.push(mt | length) 81 + parts.push(mt | length); 82 82 } else if (length < 256) { 83 - parts.push(mt | 24, length) 83 + parts.push(mt | 24, length); 84 84 } else if (length < 65536) { 85 - parts.push(mt | 25, length >> 8, length & 0xff) 85 + parts.push(mt | 25, length >> 8, length & 0xff); 86 86 } else if (length < 4294967296) { 87 87 // Use Math.floor instead of bitshift to avoid 32-bit signed integer overflow 88 - parts.push(mt | 26, 88 + parts.push( 89 + mt | 26, 89 90 Math.floor(length / 0x1000000) & 0xff, 90 91 Math.floor(length / 0x10000) & 0xff, 91 92 Math.floor(length / 0x100) & 0xff, 92 - length & 0xff) 93 + length & 0xff, 94 + ); 93 95 } 94 96 } 95 97 ··· 99 101 * @returns {Uint8Array} CBOR-encoded bytes 100 102 */ 101 103 export function cborEncode(value) { 102 - const parts = [] 104 + const parts = []; 103 105 104 106 function encode(val) { 105 107 if (val === null) { 106 - parts.push(CBOR_NULL) 108 + parts.push(CBOR_NULL); 107 109 } else if (val === true) { 108 - parts.push(CBOR_TRUE) 110 + parts.push(CBOR_TRUE); 109 111 } else if (val === false) { 110 - parts.push(CBOR_FALSE) 112 + parts.push(CBOR_FALSE); 111 113 } else if (typeof val === 'number') { 112 - encodeInteger(val) 114 + encodeInteger(val); 113 115 } else if (typeof val === 'string') { 114 - const bytes = new TextEncoder().encode(val) 115 - encodeHead(parts, 3, bytes.length) // major type 3 = text string 116 - parts.push(...bytes) 116 + const bytes = new TextEncoder().encode(val); 117 + encodeHead(parts, 3, bytes.length); // major type 3 = text string 118 + parts.push(...bytes); 117 119 } else if (val instanceof Uint8Array) { 118 - encodeHead(parts, 2, val.length) // major type 2 = byte string 119 - parts.push(...val) 120 + encodeHead(parts, 2, val.length); // major type 2 = byte string 121 + parts.push(...val); 120 122 } else if (Array.isArray(val)) { 121 - encodeHead(parts, 4, val.length) // major type 4 = array 122 - for (const item of val) encode(item) 123 + encodeHead(parts, 4, val.length); // major type 4 = array 124 + for (const item of val) encode(item); 123 125 } else if (typeof val === 'object') { 124 126 // Sort keys for deterministic encoding 125 - const keys = Object.keys(val).sort() 126 - encodeHead(parts, 5, keys.length) // major type 5 = map 127 + const keys = Object.keys(val).sort(); 128 + encodeHead(parts, 5, keys.length); // major type 5 = map 127 129 for (const key of keys) { 128 - encode(key) 129 - encode(val[key]) 130 + encode(key); 131 + encode(val[key]); 130 132 } 131 133 } 132 134 } 133 135 134 136 function encodeInteger(n) { 135 137 if (n >= 0) { 136 - encodeHead(parts, 0, n) // major type 0 = unsigned int 138 + encodeHead(parts, 0, n); // major type 0 = unsigned int 137 139 } else { 138 - encodeHead(parts, 1, -n - 1) // major type 1 = negative int 140 + encodeHead(parts, 1, -n - 1); // major type 1 = negative int 139 141 } 140 142 } 141 143 142 - encode(value) 143 - return new Uint8Array(parts) 144 + encode(value); 145 + return new Uint8Array(parts); 144 146 } 145 147 146 148 // DAG-CBOR encoder that handles CIDs with tag 42 147 149 function cborEncodeDagCbor(value) { 148 - const parts = [] 150 + const parts = []; 149 151 150 152 function encode(val) { 151 153 if (val === null) { 152 - parts.push(CBOR_NULL) 154 + parts.push(CBOR_NULL); 153 155 } else if (val === true) { 154 - parts.push(CBOR_TRUE) 156 + parts.push(CBOR_TRUE); 155 157 } else if (val === false) { 156 - parts.push(CBOR_FALSE) 158 + parts.push(CBOR_FALSE); 157 159 } else if (typeof val === 'number') { 158 160 if (Number.isInteger(val) && val >= 0) { 159 - encodeHead(parts, 0, val) 161 + encodeHead(parts, 0, val); 160 162 } else if (Number.isInteger(val) && val < 0) { 161 - encodeHead(parts, 1, -val - 1) 163 + encodeHead(parts, 1, -val - 1); 162 164 } 163 165 } else if (typeof val === 'string') { 164 - const bytes = new TextEncoder().encode(val) 165 - encodeHead(parts, 3, bytes.length) 166 - parts.push(...bytes) 166 + const bytes = new TextEncoder().encode(val); 167 + encodeHead(parts, 3, bytes.length); 168 + parts.push(...bytes); 167 169 } else if (val instanceof CID) { 168 170 // CID links in DAG-CBOR use tag 42 + 0x00 multibase prefix 169 171 // The 0x00 prefix indicates "identity" multibase (raw bytes) 170 - parts.push(0xd8, CBOR_TAG_CID) 171 - encodeHead(parts, 2, val.bytes.length + 1) // +1 for 0x00 prefix 172 - parts.push(0x00) 173 - parts.push(...val.bytes) 172 + parts.push(0xd8, CBOR_TAG_CID); 173 + encodeHead(parts, 2, val.bytes.length + 1); // +1 for 0x00 prefix 174 + parts.push(0x00); 175 + parts.push(...val.bytes); 174 176 } else if (val instanceof Uint8Array) { 175 177 // Regular byte string 176 - encodeHead(parts, 2, val.length) 177 - parts.push(...val) 178 + encodeHead(parts, 2, val.length); 179 + parts.push(...val); 178 180 } else if (Array.isArray(val)) { 179 - encodeHead(parts, 4, val.length) 180 - for (const item of val) encode(item) 181 + encodeHead(parts, 4, val.length); 182 + for (const item of val) encode(item); 181 183 } else if (typeof val === 'object') { 182 184 // DAG-CBOR: sort keys by length first, then lexicographically 183 185 // (differs from standard CBOR which sorts lexicographically only) 184 - const keys = Object.keys(val).filter(k => val[k] !== undefined) 186 + const keys = Object.keys(val).filter((k) => val[k] !== undefined); 185 187 keys.sort((a, b) => { 186 - if (a.length !== b.length) return a.length - b.length 187 - return a < b ? -1 : a > b ? 1 : 0 188 - }) 189 - encodeHead(parts, 5, keys.length) 188 + if (a.length !== b.length) return a.length - b.length; 189 + return a < b ? -1 : a > b ? 1 : 0; 190 + }); 191 + encodeHead(parts, 5, keys.length); 190 192 for (const key of keys) { 191 - const keyBytes = new TextEncoder().encode(key) 192 - encodeHead(parts, 3, keyBytes.length) 193 - parts.push(...keyBytes) 194 - encode(val[key]) 193 + const keyBytes = new TextEncoder().encode(key); 194 + encodeHead(parts, 3, keyBytes.length); 195 + parts.push(...keyBytes); 196 + encode(val[key]); 195 197 } 196 198 } 197 199 } 198 200 199 - encode(value) 200 - return new Uint8Array(parts) 201 + encode(value); 202 + return new Uint8Array(parts); 201 203 } 202 204 203 205 /** ··· 206 208 * @returns {*} Decoded value 207 209 */ 208 210 export function cborDecode(bytes) { 209 - let offset = 0 211 + let offset = 0; 210 212 211 213 function read() { 212 - const initial = bytes[offset++] 213 - const major = initial >> 5 214 - const info = initial & 0x1f 214 + const initial = bytes[offset++]; 215 + const major = initial >> 5; 216 + const info = initial & 0x1f; 215 217 216 - let length = info 217 - if (info === 24) length = bytes[offset++] 218 - else if (info === 25) { length = (bytes[offset++] << 8) | bytes[offset++] } 219 - else if (info === 26) { 218 + let length = info; 219 + if (info === 24) length = bytes[offset++]; 220 + else if (info === 25) { 221 + length = (bytes[offset++] << 8) | bytes[offset++]; 222 + } else if (info === 26) { 220 223 // Use multiplication instead of bitshift to avoid 32-bit signed integer overflow 221 - length = bytes[offset++] * 0x1000000 + bytes[offset++] * 0x10000 + bytes[offset++] * 0x100 + bytes[offset++] 224 + length = 225 + bytes[offset++] * 0x1000000 + 226 + bytes[offset++] * 0x10000 + 227 + bytes[offset++] * 0x100 + 228 + bytes[offset++]; 222 229 } 223 230 224 231 switch (major) { 225 - case 0: return length // unsigned int 226 - case 1: return -1 - length // negative int 227 - case 2: { // byte string 228 - const data = bytes.slice(offset, offset + length) 229 - offset += length 230 - return data 232 + case 0: 233 + return length; // unsigned int 234 + case 1: 235 + return -1 - length; // negative int 236 + case 2: { 237 + // byte string 238 + const data = bytes.slice(offset, offset + length); 239 + offset += length; 240 + return data; 231 241 } 232 - case 3: { // text string 233 - const data = new TextDecoder().decode(bytes.slice(offset, offset + length)) 234 - offset += length 235 - return data 242 + case 3: { 243 + // text string 244 + const data = new TextDecoder().decode( 245 + bytes.slice(offset, offset + length), 246 + ); 247 + offset += length; 248 + return data; 236 249 } 237 - case 4: { // array 238 - const arr = [] 239 - for (let i = 0; i < length; i++) arr.push(read()) 240 - return arr 250 + case 4: { 251 + // array 252 + const arr = []; 253 + for (let i = 0; i < length; i++) arr.push(read()); 254 + return arr; 241 255 } 242 - case 5: { // map 243 - const obj = {} 256 + case 5: { 257 + // map 258 + const obj = {}; 244 259 for (let i = 0; i < length; i++) { 245 - const key = read() 246 - obj[key] = read() 260 + const key = read(); 261 + obj[key] = read(); 247 262 } 248 - return obj 263 + return obj; 249 264 } 250 - case 6: { // tag 265 + case 6: { 266 + // tag 251 267 // length is the tag number 252 - const taggedValue = read() 268 + const taggedValue = read(); 253 269 if (length === CBOR_TAG_CID) { 254 270 // CID link: byte string with 0x00 multibase prefix, return raw CID bytes 255 - return taggedValue.slice(1) // strip 0x00 prefix 271 + return taggedValue.slice(1); // strip 0x00 prefix 256 272 } 257 - return taggedValue 273 + return taggedValue; 258 274 } 259 - case 7: { // special 260 - if (info === 20) return false 261 - if (info === 21) return true 262 - if (info === 22) return null 263 - return undefined 275 + case 7: { 276 + // special 277 + if (info === 20) return false; 278 + if (info === 21) return true; 279 + if (info === 22) return null; 280 + return undefined; 264 281 } 265 282 } 266 283 } 267 284 268 - return read() 285 + return read(); 269 286 } 270 287 271 288 // === CID GENERATION === ··· 277 294 * @returns {Promise<Uint8Array>} CID bytes (36 bytes: version + codec + multihash) 278 295 */ 279 296 export async function createCid(bytes) { 280 - const hash = await crypto.subtle.digest('SHA-256', bytes) 281 - const hashBytes = new Uint8Array(hash) 297 + const hash = await crypto.subtle.digest('SHA-256', bytes); 298 + const hashBytes = new Uint8Array(hash); 282 299 283 300 // CIDv1: version(1) + codec(dag-cbor=0x71) + multihash(sha256) 284 301 // Multihash: hash-type(0x12) + length(0x20=32) + digest 285 - const cid = new Uint8Array(2 + 2 + 32) 286 - cid[0] = 0x01 // CIDv1 287 - cid[1] = 0x71 // dag-cbor codec 288 - cid[2] = 0x12 // sha-256 289 - cid[3] = 0x20 // 32 bytes 290 - cid.set(hashBytes, 4) 302 + const cid = new Uint8Array(2 + 2 + 32); 303 + cid[0] = 0x01; // CIDv1 304 + cid[1] = 0x71; // dag-cbor codec 305 + cid[2] = 0x12; // sha-256 306 + cid[3] = 0x20; // 32 bytes 307 + cid.set(hashBytes, 4); 291 308 292 - return cid 309 + return cid; 293 310 } 294 311 295 312 /** ··· 299 316 */ 300 317 export function cidToString(cid) { 301 318 // base32lower encoding for CIDv1 302 - return 'b' + base32Encode(cid) 319 + return `b${base32Encode(cid)}`; 303 320 } 304 321 305 322 /** ··· 308 325 * @returns {string} Base32lower-encoded string 309 326 */ 310 327 export function base32Encode(bytes) { 311 - const alphabet = 'abcdefghijklmnopqrstuvwxyz234567' 312 - let result = '' 313 - let bits = 0 314 - let value = 0 328 + const alphabet = 'abcdefghijklmnopqrstuvwxyz234567'; 329 + let result = ''; 330 + let bits = 0; 331 + let value = 0; 315 332 316 333 for (const byte of bytes) { 317 - value = (value << 8) | byte 318 - bits += 8 334 + value = (value << 8) | byte; 335 + bits += 8; 319 336 while (bits >= 5) { 320 - bits -= 5 321 - result += alphabet[(value >> bits) & 31] 337 + bits -= 5; 338 + result += alphabet[(value >> bits) & 31]; 322 339 } 323 340 } 324 341 325 342 if (bits > 0) { 326 - result += alphabet[(value << (5 - bits)) & 31] 343 + result += alphabet[(value << (5 - bits)) & 31]; 327 344 } 328 345 329 - return result 346 + return result; 330 347 } 331 348 332 349 // === TID GENERATION === 333 350 // Timestamp-based IDs: base32-sort encoded microseconds + clock ID 334 351 335 - const TID_CHARS = '234567abcdefghijklmnopqrstuvwxyz' 336 - let lastTimestamp = 0 337 - let clockId = Math.floor(Math.random() * 1024) 352 + const TID_CHARS = '234567abcdefghijklmnopqrstuvwxyz'; 353 + let lastTimestamp = 0; 354 + const clockId = Math.floor(Math.random() * 1024); 338 355 339 356 /** 340 357 * Generate a timestamp-based ID (TID) for record keys ··· 342 359 * @returns {string} 13-character base32-sort encoded TID 343 360 */ 344 361 export function createTid() { 345 - let timestamp = Date.now() * 1000 // microseconds 362 + let timestamp = Date.now() * 1000; // microseconds 346 363 347 364 // Ensure monotonic 348 365 if (timestamp <= lastTimestamp) { 349 - timestamp = lastTimestamp + 1 366 + timestamp = lastTimestamp + 1; 350 367 } 351 - lastTimestamp = timestamp 368 + lastTimestamp = timestamp; 352 369 353 370 // 13 chars: 11 for timestamp (64 bits but only ~53 used), 2 for clock ID 354 - let tid = '' 371 + let tid = ''; 355 372 356 373 // Encode timestamp (high bits first for sortability) 357 - let ts = timestamp 374 + let ts = timestamp; 358 375 for (let i = 0; i < 11; i++) { 359 - tid = TID_CHARS[ts & 31] + tid 360 - ts = Math.floor(ts / 32) 376 + tid = TID_CHARS[ts & 31] + tid; 377 + ts = Math.floor(ts / 32); 361 378 } 362 379 363 380 // Append clock ID (2 chars) 364 - tid += TID_CHARS[(clockId >> 5) & 31] 365 - tid += TID_CHARS[clockId & 31] 381 + tid += TID_CHARS[(clockId >> 5) & 31]; 382 + tid += TID_CHARS[clockId & 31]; 366 383 367 - return tid 384 + return tid; 368 385 } 369 386 370 387 // === P-256 SIGNING === ··· 377 394 */ 378 395 export async function importPrivateKey(privateKeyBytes) { 379 396 // Validate private key length (P-256 requires exactly 32 bytes) 380 - if (!(privateKeyBytes instanceof Uint8Array) || privateKeyBytes.length !== 32) { 381 - throw new Error(`Invalid private key: expected 32 bytes, got ${privateKeyBytes?.length ?? 'non-Uint8Array'}`) 397 + if ( 398 + !(privateKeyBytes instanceof Uint8Array) || 399 + privateKeyBytes.length !== 32 400 + ) { 401 + throw new Error( 402 + `Invalid private key: expected 32 bytes, got ${privateKeyBytes?.length ?? 'non-Uint8Array'}`, 403 + ); 382 404 } 383 405 384 406 // PKCS#8 wrapper for raw P-256 private key 385 407 const pkcs8Prefix = new Uint8Array([ 386 408 0x30, 0x41, 0x02, 0x01, 0x00, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 387 409 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 388 - 0x01, 0x07, 0x04, 0x27, 0x30, 0x25, 0x02, 0x01, 0x01, 0x04, 0x20 389 - ]) 410 + 0x01, 0x07, 0x04, 0x27, 0x30, 0x25, 0x02, 0x01, 0x01, 0x04, 0x20, 411 + ]); 390 412 391 - const pkcs8 = new Uint8Array(pkcs8Prefix.length + 32) 392 - pkcs8.set(pkcs8Prefix) 393 - pkcs8.set(privateKeyBytes, pkcs8Prefix.length) 413 + const pkcs8 = new Uint8Array(pkcs8Prefix.length + 32); 414 + pkcs8.set(pkcs8Prefix); 415 + pkcs8.set(privateKeyBytes, pkcs8Prefix.length); 394 416 395 417 return crypto.subtle.importKey( 396 418 'pkcs8', 397 419 pkcs8, 398 420 { name: 'ECDSA', namedCurve: 'P-256' }, 399 421 false, 400 - ['sign'] 401 - ) 422 + ['sign'], 423 + ); 402 424 } 403 425 404 426 // P-256 curve order N 405 - const P256_N = BigInt('0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551') 406 - const P256_N_DIV_2 = P256_N / 2n 427 + const P256_N = BigInt( 428 + '0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551', 429 + ); 430 + const P256_N_DIV_2 = P256_N / 2n; 407 431 408 432 function bytesToBigInt(bytes) { 409 - let result = 0n 433 + let result = 0n; 410 434 for (const byte of bytes) { 411 - result = (result << 8n) | BigInt(byte) 435 + result = (result << 8n) | BigInt(byte); 412 436 } 413 - return result 437 + return result; 414 438 } 415 439 416 440 function bigIntToBytes(n, length) { 417 - const bytes = new Uint8Array(length) 441 + const bytes = new Uint8Array(length); 418 442 for (let i = length - 1; i >= 0; i--) { 419 - bytes[i] = Number(n & 0xffn) 420 - n >>= 8n 443 + bytes[i] = Number(n & 0xffn); 444 + n >>= 8n; 421 445 } 422 - return bytes 446 + return bytes; 423 447 } 424 448 425 449 /** ··· 432 456 const signature = await crypto.subtle.sign( 433 457 { name: 'ECDSA', hash: 'SHA-256' }, 434 458 privateKey, 435 - data 436 - ) 437 - const sig = new Uint8Array(signature) 459 + data, 460 + ); 461 + const sig = new Uint8Array(signature); 438 462 439 - const r = sig.slice(0, 32) 440 - const s = sig.slice(32, 64) 441 - const sBigInt = bytesToBigInt(s) 463 + const r = sig.slice(0, 32); 464 + const s = sig.slice(32, 64); 465 + const sBigInt = bytesToBigInt(s); 442 466 443 467 // Low-S normalization: Bitcoin/ATProto require S <= N/2 to prevent 444 468 // signature malleability (two valid signatures for same message) 445 469 if (sBigInt > P256_N_DIV_2) { 446 - const newS = P256_N - sBigInt 447 - const newSBytes = bigIntToBytes(newS, 32) 448 - const normalized = new Uint8Array(64) 449 - normalized.set(r, 0) 450 - normalized.set(newSBytes, 32) 451 - return normalized 470 + const newS = P256_N - sBigInt; 471 + const newSBytes = bigIntToBytes(newS, 32); 472 + const normalized = new Uint8Array(64); 473 + normalized.set(r, 0); 474 + normalized.set(newSBytes, 32); 475 + return normalized; 452 476 } 453 477 454 - return sig 478 + return sig; 455 479 } 456 480 457 481 /** ··· 462 486 const keyPair = await crypto.subtle.generateKey( 463 487 { name: 'ECDSA', namedCurve: 'P-256' }, 464 488 true, 465 - ['sign', 'verify'] 466 - ) 489 + ['sign', 'verify'], 490 + ); 467 491 468 492 // Export private key as raw bytes 469 - const privateJwk = await crypto.subtle.exportKey('jwk', keyPair.privateKey) 470 - const privateBytes = base64UrlDecode(privateJwk.d) 493 + const privateJwk = await crypto.subtle.exportKey('jwk', keyPair.privateKey); 494 + const privateBytes = base64UrlDecode(privateJwk.d); 471 495 472 496 // Export public key as compressed point 473 - const publicRaw = await crypto.subtle.exportKey('raw', keyPair.publicKey) 474 - const publicBytes = new Uint8Array(publicRaw) 475 - const compressed = compressPublicKey(publicBytes) 497 + const publicRaw = await crypto.subtle.exportKey('raw', keyPair.publicKey); 498 + const publicBytes = new Uint8Array(publicRaw); 499 + const compressed = compressPublicKey(publicBytes); 476 500 477 - return { privateKey: privateBytes, publicKey: compressed } 501 + return { privateKey: privateBytes, publicKey: compressed }; 478 502 } 479 503 480 504 function compressPublicKey(uncompressed) { 481 505 // uncompressed is 65 bytes: 0x04 + x(32) + y(32) 482 506 // compressed is 33 bytes: prefix(02 or 03) + x(32) 483 - const x = uncompressed.slice(1, 33) 484 - const y = uncompressed.slice(33, 65) 485 - const prefix = (y[31] & 1) === 0 ? 0x02 : 0x03 486 - const compressed = new Uint8Array(33) 487 - compressed[0] = prefix 488 - compressed.set(x, 1) 489 - return compressed 507 + const x = uncompressed.slice(1, 33); 508 + const y = uncompressed.slice(33, 65); 509 + const prefix = (y[31] & 1) === 0 ? 0x02 : 0x03; 510 + const compressed = new Uint8Array(33); 511 + compressed[0] = prefix; 512 + compressed.set(x, 1); 513 + return compressed; 490 514 } 491 515 492 516 /** ··· 495 519 * @returns {string} Base64url-encoded string 496 520 */ 497 521 export function base64UrlEncode(bytes) { 498 - let binary = '' 522 + let binary = ''; 499 523 for (const byte of bytes) { 500 - binary += String.fromCharCode(byte) 524 + binary += String.fromCharCode(byte); 501 525 } 502 - const base64 = btoa(binary) 503 - return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') 526 + const base64 = btoa(binary); 527 + return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); 504 528 } 505 529 506 530 /** ··· 509 533 * @returns {Uint8Array} Decoded bytes 510 534 */ 511 535 export function base64UrlDecode(str) { 512 - const base64 = str.replace(/-/g, '+').replace(/_/g, '/') 513 - const pad = base64.length % 4 514 - const padded = pad ? base64 + '='.repeat(4 - pad) : base64 515 - const binary = atob(padded) 516 - const bytes = new Uint8Array(binary.length) 536 + const base64 = str.replace(/-/g, '+').replace(/_/g, '/'); 537 + const pad = base64.length % 4; 538 + const padded = pad ? base64 + '='.repeat(4 - pad) : base64; 539 + const binary = atob(padded); 540 + const bytes = new Uint8Array(binary.length); 517 541 for (let i = 0; i < binary.length; i++) { 518 - bytes[i] = binary.charCodeAt(i) 542 + bytes[i] = binary.charCodeAt(i); 519 543 } 520 - return bytes 544 + return bytes; 521 545 } 522 546 523 547 /** ··· 532 556 new TextEncoder().encode(secret), 533 557 { name: 'HMAC', hash: 'SHA-256' }, 534 558 false, 535 - ['sign'] 536 - ) 537 - const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(data)) 538 - return base64UrlEncode(new Uint8Array(sig)) 559 + ['sign'], 560 + ); 561 + const sig = await crypto.subtle.sign( 562 + 'HMAC', 563 + key, 564 + new TextEncoder().encode(data), 565 + ); 566 + return base64UrlEncode(new Uint8Array(sig)); 539 567 } 540 568 541 569 /** ··· 546 574 * @returns {Promise<string>} Signed JWT 547 575 */ 548 576 export async function createAccessJwt(did, secret, expiresIn = 7200) { 549 - const header = { typ: 'at+jwt', alg: 'HS256' } 550 - const now = Math.floor(Date.now() / 1000) 577 + const header = { typ: 'at+jwt', alg: 'HS256' }; 578 + const now = Math.floor(Date.now() / 1000); 551 579 const payload = { 552 580 scope: 'com.atproto.access', 553 581 sub: did, 554 582 aud: did, 555 583 iat: now, 556 - exp: now + expiresIn 557 - } 584 + exp: now + expiresIn, 585 + }; 558 586 559 - const headerB64 = base64UrlEncode(new TextEncoder().encode(JSON.stringify(header))) 560 - const payloadB64 = base64UrlEncode(new TextEncoder().encode(JSON.stringify(payload))) 561 - const signature = await hmacSign(`${headerB64}.${payloadB64}`, secret) 587 + const headerB64 = base64UrlEncode( 588 + new TextEncoder().encode(JSON.stringify(header)), 589 + ); 590 + const payloadB64 = base64UrlEncode( 591 + new TextEncoder().encode(JSON.stringify(payload)), 592 + ); 593 + const signature = await hmacSign(`${headerB64}.${payloadB64}`, secret); 562 594 563 - return `${headerB64}.${payloadB64}.${signature}` 595 + return `${headerB64}.${payloadB64}.${signature}`; 564 596 } 565 597 566 598 /** ··· 571 603 * @returns {Promise<string>} Signed JWT 572 604 */ 573 605 export async function createRefreshJwt(did, secret, expiresIn = 7776000) { 574 - const header = { typ: 'refresh+jwt', alg: 'HS256' } 575 - const now = Math.floor(Date.now() / 1000) 606 + const header = { typ: 'refresh+jwt', alg: 'HS256' }; 607 + const now = Math.floor(Date.now() / 1000); 576 608 // Generate random jti (token ID) 577 - const jtiBytes = new Uint8Array(32) 578 - crypto.getRandomValues(jtiBytes) 579 - const jti = base64UrlEncode(jtiBytes) 609 + const jtiBytes = new Uint8Array(32); 610 + crypto.getRandomValues(jtiBytes); 611 + const jti = base64UrlEncode(jtiBytes); 580 612 581 613 const payload = { 582 614 scope: 'com.atproto.refresh', ··· 584 616 aud: did, 585 617 jti, 586 618 iat: now, 587 - exp: now + expiresIn 588 - } 619 + exp: now + expiresIn, 620 + }; 589 621 590 - const headerB64 = base64UrlEncode(new TextEncoder().encode(JSON.stringify(header))) 591 - const payloadB64 = base64UrlEncode(new TextEncoder().encode(JSON.stringify(payload))) 592 - const signature = await hmacSign(`${headerB64}.${payloadB64}`, secret) 622 + const headerB64 = base64UrlEncode( 623 + new TextEncoder().encode(JSON.stringify(header)), 624 + ); 625 + const payloadB64 = base64UrlEncode( 626 + new TextEncoder().encode(JSON.stringify(payload)), 627 + ); 628 + const signature = await hmacSign(`${headerB64}.${payloadB64}`, secret); 593 629 594 - return `${headerB64}.${payloadB64}.${signature}` 630 + return `${headerB64}.${payloadB64}.${signature}`; 595 631 } 596 632 597 633 /** ··· 602 638 * @throws {Error} If token is invalid, expired, or wrong type 603 639 */ 604 640 export async function verifyAccessJwt(jwt, secret) { 605 - const parts = jwt.split('.') 641 + const parts = jwt.split('.'); 606 642 if (parts.length !== 3) { 607 - throw new Error('Invalid JWT format') 643 + throw new Error('Invalid JWT format'); 608 644 } 609 645 610 - const [headerB64, payloadB64, signatureB64] = parts 646 + const [headerB64, payloadB64, signatureB64] = parts; 611 647 612 648 // Verify signature 613 - const expectedSig = await hmacSign(`${headerB64}.${payloadB64}`, secret) 649 + const expectedSig = await hmacSign(`${headerB64}.${payloadB64}`, secret); 614 650 if (signatureB64 !== expectedSig) { 615 - throw new Error('Invalid signature') 651 + throw new Error('Invalid signature'); 616 652 } 617 653 618 654 // Decode header and payload 619 - const header = JSON.parse(new TextDecoder().decode(base64UrlDecode(headerB64))) 620 - const payload = JSON.parse(new TextDecoder().decode(base64UrlDecode(payloadB64))) 655 + const header = JSON.parse( 656 + new TextDecoder().decode(base64UrlDecode(headerB64)), 657 + ); 658 + const payload = JSON.parse( 659 + new TextDecoder().decode(base64UrlDecode(payloadB64)), 660 + ); 621 661 622 662 // Check token type 623 663 if (header.typ !== 'at+jwt') { 624 - throw new Error('Invalid token type: expected access token') 664 + throw new Error('Invalid token type: expected access token'); 625 665 } 626 666 627 667 // Check expiration 628 - const now = Math.floor(Date.now() / 1000) 668 + const now = Math.floor(Date.now() / 1000); 629 669 if (payload.exp && payload.exp < now) { 630 - throw new Error('Token expired') 670 + throw new Error('Token expired'); 631 671 } 632 672 633 - return payload 673 + return payload; 634 674 } 635 675 636 676 /** ··· 644 684 * @returns {Promise<string>} Signed JWT 645 685 */ 646 686 export async function createServiceJwt({ iss, aud, lxm, signingKey }) { 647 - const header = { typ: 'JWT', alg: 'ES256' } 648 - const now = Math.floor(Date.now() / 1000) 687 + const header = { typ: 'JWT', alg: 'ES256' }; 688 + const now = Math.floor(Date.now() / 1000); 649 689 650 690 // Generate random jti 651 - const jtiBytes = new Uint8Array(16) 652 - crypto.getRandomValues(jtiBytes) 653 - const jti = bytesToHex(jtiBytes) 691 + const jtiBytes = new Uint8Array(16); 692 + crypto.getRandomValues(jtiBytes); 693 + const jti = bytesToHex(jtiBytes); 654 694 655 695 const payload = { 656 696 iss, 657 697 aud, 658 698 exp: now + 60, // 1 minute expiration 659 699 iat: now, 660 - jti 661 - } 662 - if (lxm) payload.lxm = lxm 700 + jti, 701 + }; 702 + if (lxm) payload.lxm = lxm; 663 703 664 - const headerB64 = base64UrlEncode(new TextEncoder().encode(JSON.stringify(header))) 665 - const payloadB64 = base64UrlEncode(new TextEncoder().encode(JSON.stringify(payload))) 666 - const toSign = new TextEncoder().encode(`${headerB64}.${payloadB64}`) 704 + const headerB64 = base64UrlEncode( 705 + new TextEncoder().encode(JSON.stringify(header)), 706 + ); 707 + const payloadB64 = base64UrlEncode( 708 + new TextEncoder().encode(JSON.stringify(payload)), 709 + ); 710 + const toSign = new TextEncoder().encode(`${headerB64}.${payloadB64}`); 667 711 668 - const sig = await sign(signingKey, toSign) 669 - const sigB64 = base64UrlEncode(sig) 712 + const sig = await sign(signingKey, toSign); 713 + const sigB64 = base64UrlEncode(sig); 670 714 671 - return `${headerB64}.${payloadB64}.${sigB64}` 715 + return `${headerB64}.${payloadB64}.${sigB64}`; 672 716 } 673 717 674 718 /** ··· 677 721 * @returns {string} Hex string 678 722 */ 679 723 export function bytesToHex(bytes) { 680 - return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('') 724 + return Array.from(bytes) 725 + .map((b) => b.toString(16).padStart(2, '0')) 726 + .join(''); 681 727 } 682 728 683 729 /** ··· 686 732 * @returns {Uint8Array} Decoded bytes 687 733 */ 688 734 export function hexToBytes(hex) { 689 - const bytes = new Uint8Array(hex.length / 2) 735 + const bytes = new Uint8Array(hex.length / 2); 690 736 for (let i = 0; i < hex.length; i += 2) { 691 - bytes[i / 2] = parseInt(hex.substr(i, 2), 16) 737 + bytes[i / 2] = parseInt(hex.substr(i, 2), 16); 692 738 } 693 - return bytes 739 + return bytes; 694 740 } 695 741 696 742 // === MERKLE SEARCH TREE === 697 743 // ATProto-compliant MST implementation 698 744 699 745 async function sha256(data) { 700 - const hash = await crypto.subtle.digest('SHA-256', data) 701 - return new Uint8Array(hash) 746 + const hash = await crypto.subtle.digest('SHA-256', data); 747 + return new Uint8Array(hash); 702 748 } 703 749 704 750 // Cache for key depths (SHA-256 is expensive) 705 - const keyDepthCache = new Map() 751 + const keyDepthCache = new Map(); 706 752 707 753 /** 708 754 * Get MST tree depth for a key based on leading zeros in SHA-256 hash ··· 711 757 */ 712 758 export async function getKeyDepth(key) { 713 759 // Count leading zeros in SHA-256 hash, divide by 2 714 - if (keyDepthCache.has(key)) return keyDepthCache.get(key) 760 + if (keyDepthCache.has(key)) return keyDepthCache.get(key); 715 761 716 - const keyBytes = new TextEncoder().encode(key) 717 - const hash = await sha256(keyBytes) 762 + const keyBytes = new TextEncoder().encode(key); 763 + const hash = await sha256(keyBytes); 718 764 719 - let zeros = 0 765 + let zeros = 0; 720 766 for (const byte of hash) { 721 767 if (byte === 0) { 722 - zeros += 8 768 + zeros += 8; 723 769 } else { 724 770 // Count leading zeros in this byte 725 771 for (let i = 7; i >= 0; i--) { 726 - if ((byte >> i) & 1) break 727 - zeros++ 772 + if ((byte >> i) & 1) break; 773 + zeros++; 728 774 } 729 - break 775 + break; 730 776 } 731 777 } 732 778 733 779 // MST depth = leading zeros in SHA-256 hash / 2 734 780 // This creates a probabilistic tree where ~50% of keys are at depth 0, 735 781 // ~25% at depth 1, etc., giving O(log n) lookups 736 - const depth = Math.floor(zeros / 2) 737 - keyDepthCache.set(key, depth) 738 - return depth 782 + const depth = Math.floor(zeros / 2); 783 + keyDepthCache.set(key, depth); 784 + return depth; 739 785 } 740 786 741 787 // Compute common prefix length between two byte arrays 742 788 function commonPrefixLen(a, b) { 743 - const minLen = Math.min(a.length, b.length) 789 + const minLen = Math.min(a.length, b.length); 744 790 for (let i = 0; i < minLen; i++) { 745 - if (a[i] !== b[i]) return i 791 + if (a[i] !== b[i]) return i; 746 792 } 747 - return minLen 793 + return minLen; 748 794 } 749 795 750 796 class MST { 751 797 constructor(sql) { 752 - this.sql = sql 798 + this.sql = sql; 753 799 } 754 800 755 801 async computeRoot() { 756 - const records = this.sql.exec(` 802 + const records = this.sql 803 + .exec(` 757 804 SELECT collection, rkey, cid FROM records ORDER BY collection, rkey 758 - `).toArray() 805 + `) 806 + .toArray(); 759 807 760 808 if (records.length === 0) { 761 - return null 809 + return null; 762 810 } 763 811 764 812 // Build entries with pre-computed depths (heights) 765 813 // In ATProto MST, "height" determines which layer a key belongs to 766 814 // Layer 0 is at the BOTTOM, root is at the highest layer 767 - const entries = [] 768 - let maxDepth = 0 815 + const entries = []; 816 + let maxDepth = 0; 769 817 for (const r of records) { 770 - const key = `${r.collection}/${r.rkey}` 771 - const depth = await getKeyDepth(key) 772 - maxDepth = Math.max(maxDepth, depth) 818 + const key = `${r.collection}/${r.rkey}`; 819 + const depth = await getKeyDepth(key); 820 + maxDepth = Math.max(maxDepth, depth); 773 821 entries.push({ 774 822 key, 775 823 keyBytes: new TextEncoder().encode(key), 776 824 cid: r.cid, 777 - depth 778 - }) 825 + depth, 826 + }); 779 827 } 780 828 781 829 // Start building from the root (highest layer) going down to layer 0 782 - return this.buildTree(entries, maxDepth) 830 + return this.buildTree(entries, maxDepth); 783 831 } 784 832 785 833 async buildTree(entries, layer) { 786 - if (entries.length === 0) return null 834 + if (entries.length === 0) return null; 787 835 788 836 // Separate entries for this layer vs lower layers (subtrees) 789 837 // Keys with depth == layer stay at this node 790 838 // Keys with depth < layer go into subtrees (going down toward layer 0) 791 - const thisLayer = [] 792 - let leftSubtree = [] 839 + const thisLayer = []; 840 + let leftSubtree = []; 793 841 794 842 for (const entry of entries) { 795 843 if (entry.depth < layer) { 796 844 // This entry belongs to a lower layer - accumulate for subtree 797 - leftSubtree.push(entry) 845 + leftSubtree.push(entry); 798 846 } else { 799 847 // This entry belongs at current layer (depth == layer) 800 848 // Process accumulated left subtree first 801 849 if (leftSubtree.length > 0) { 802 - const leftCid = await this.buildTree(leftSubtree, layer - 1) 803 - thisLayer.push({ type: 'subtree', cid: leftCid }) 804 - leftSubtree = [] 850 + const leftCid = await this.buildTree(leftSubtree, layer - 1); 851 + thisLayer.push({ type: 'subtree', cid: leftCid }); 852 + leftSubtree = []; 805 853 } 806 - thisLayer.push({ type: 'entry', entry }) 854 + thisLayer.push({ type: 'entry', entry }); 807 855 } 808 856 } 809 857 810 858 // Handle remaining left subtree 811 859 if (leftSubtree.length > 0) { 812 - const leftCid = await this.buildTree(leftSubtree, layer - 1) 813 - thisLayer.push({ type: 'subtree', cid: leftCid }) 860 + const leftCid = await this.buildTree(leftSubtree, layer - 1); 861 + thisLayer.push({ type: 'subtree', cid: leftCid }); 814 862 } 815 863 816 864 // Build node with proper ATProto format 817 - const node = { e: [] } 818 - let leftCid = null 819 - let prevKeyBytes = new Uint8Array(0) 865 + const node = { e: [] }; 866 + let leftCid = null; 867 + let prevKeyBytes = new Uint8Array(0); 820 868 821 869 for (let i = 0; i < thisLayer.length; i++) { 822 - const item = thisLayer[i] 870 + const item = thisLayer[i]; 823 871 824 872 if (item.type === 'subtree') { 825 873 if (node.e.length === 0) { 826 - leftCid = item.cid 874 + leftCid = item.cid; 827 875 } else { 828 876 // Attach to previous entry's 't' field 829 - node.e[node.e.length - 1].t = new CID(cidToBytes(item.cid)) 877 + node.e[node.e.length - 1].t = new CID(cidToBytes(item.cid)); 830 878 } 831 879 } else { 832 880 // Entry - compute prefix compression 833 - const keyBytes = item.entry.keyBytes 834 - const prefixLen = commonPrefixLen(prevKeyBytes, keyBytes) 835 - const keySuffix = keyBytes.slice(prefixLen) 881 + const keyBytes = item.entry.keyBytes; 882 + const prefixLen = commonPrefixLen(prevKeyBytes, keyBytes); 883 + const keySuffix = keyBytes.slice(prefixLen); 836 884 837 885 // ATProto requires t field to be present (can be null) 838 886 const e = { 839 887 p: prefixLen, 840 888 k: keySuffix, 841 889 v: new CID(cidToBytes(item.entry.cid)), 842 - t: null // Will be updated if there's a subtree 843 - } 890 + t: null, // Will be updated if there's a subtree 891 + }; 844 892 845 - node.e.push(e) 846 - prevKeyBytes = keyBytes 893 + node.e.push(e); 894 + prevKeyBytes = keyBytes; 847 895 } 848 896 } 849 897 850 898 // ATProto requires l field to be present (can be null) 851 - node.l = leftCid ? new CID(cidToBytes(leftCid)) : null 899 + node.l = leftCid ? new CID(cidToBytes(leftCid)) : null; 852 900 853 901 // Encode node with proper MST CBOR format 854 - const nodeBytes = cborEncodeDagCbor(node) 855 - const nodeCid = await createCid(nodeBytes) 856 - const cidStr = cidToString(nodeCid) 902 + const nodeBytes = cborEncodeDagCbor(node); 903 + const nodeCid = await createCid(nodeBytes); 904 + const cidStr = cidToString(nodeCid); 857 905 858 906 this.sql.exec( 859 907 `INSERT OR REPLACE INTO blocks (cid, data) VALUES (?, ?)`, 860 908 cidStr, 861 - nodeBytes 862 - ) 909 + nodeBytes, 910 + ); 863 911 864 - return cidStr 912 + return cidStr; 865 913 } 866 914 } 867 915 ··· 873 921 * @returns {Uint8Array} Varint-encoded bytes 874 922 */ 875 923 export function varint(n) { 876 - const bytes = [] 924 + const bytes = []; 877 925 while (n >= 0x80) { 878 - bytes.push((n & 0x7f) | 0x80) 879 - n >>>= 7 926 + bytes.push((n & 0x7f) | 0x80); 927 + n >>>= 7; 880 928 } 881 - bytes.push(n) 882 - return new Uint8Array(bytes) 929 + bytes.push(n); 930 + return new Uint8Array(bytes); 883 931 } 884 932 885 933 /** ··· 889 937 */ 890 938 export function cidToBytes(cidStr) { 891 939 // Decode base32lower CID string to bytes 892 - if (!cidStr.startsWith('b')) throw new Error('expected base32lower CID') 893 - return base32Decode(cidStr.slice(1)) 940 + if (!cidStr.startsWith('b')) throw new Error('expected base32lower CID'); 941 + return base32Decode(cidStr.slice(1)); 894 942 } 895 943 896 944 /** ··· 899 947 * @returns {Uint8Array} Decoded bytes 900 948 */ 901 949 export function base32Decode(str) { 902 - const alphabet = 'abcdefghijklmnopqrstuvwxyz234567' 903 - let bits = 0 904 - let value = 0 905 - const output = [] 950 + const alphabet = 'abcdefghijklmnopqrstuvwxyz234567'; 951 + let bits = 0; 952 + let value = 0; 953 + const output = []; 906 954 907 955 for (const char of str) { 908 - const idx = alphabet.indexOf(char) 909 - if (idx === -1) continue 910 - value = (value << 5) | idx 911 - bits += 5 956 + const idx = alphabet.indexOf(char); 957 + if (idx === -1) continue; 958 + value = (value << 5) | idx; 959 + bits += 5; 912 960 if (bits >= 8) { 913 - bits -= 8 914 - output.push((value >> bits) & 0xff) 961 + bits -= 8; 962 + output.push((value >> bits) & 0xff); 915 963 } 916 964 } 917 965 918 - return new Uint8Array(output) 966 + return new Uint8Array(output); 919 967 } 920 968 921 969 /** ··· 925 973 * @returns {Uint8Array} CAR file bytes 926 974 */ 927 975 export function buildCarFile(rootCid, blocks) { 928 - const parts = [] 976 + const parts = []; 929 977 930 978 // Header: { version: 1, roots: [rootCid] } 931 - const rootCidBytes = cidToBytes(rootCid) 932 - const header = cborEncodeDagCbor({ version: 1, roots: [new CID(rootCidBytes)] }) 933 - parts.push(varint(header.length)) 934 - parts.push(header) 979 + const rootCidBytes = cidToBytes(rootCid); 980 + const header = cborEncodeDagCbor({ 981 + version: 1, 982 + roots: [new CID(rootCidBytes)], 983 + }); 984 + parts.push(varint(header.length)); 985 + parts.push(header); 935 986 936 987 // Blocks: varint(len) + cid + data 937 988 for (const block of blocks) { 938 - const cidBytes = cidToBytes(block.cid) 939 - const blockLen = cidBytes.length + block.data.length 940 - parts.push(varint(blockLen)) 941 - parts.push(cidBytes) 942 - parts.push(block.data) 989 + const cidBytes = cidToBytes(block.cid); 990 + const blockLen = cidBytes.length + block.data.length; 991 + parts.push(varint(blockLen)); 992 + parts.push(cidBytes); 993 + parts.push(block.data); 943 994 } 944 995 945 996 // Concatenate all parts 946 - const totalLen = parts.reduce((sum, p) => sum + p.length, 0) 947 - const car = new Uint8Array(totalLen) 948 - let offset = 0 997 + const totalLen = parts.reduce((sum, p) => sum + p.length, 0); 998 + const car = new Uint8Array(totalLen); 999 + let offset = 0; 949 1000 for (const part of parts) { 950 - car.set(part, offset) 951 - offset += part.length 1001 + car.set(part, offset); 1002 + offset += part.length; 952 1003 } 953 1004 954 - return car 1005 + return car; 955 1006 } 956 1007 957 1008 /** ··· 972 1023 /** @type {Record<string, Route>} */ 973 1024 const pdsRoutes = { 974 1025 '/.well-known/atproto-did': { 975 - handler: (pds, req, url) => pds.handleAtprotoDid() 1026 + handler: (pds, _req, _url) => pds.handleAtprotoDid(), 976 1027 }, 977 1028 '/init': { 978 1029 method: 'POST', 979 - handler: (pds, req, url) => pds.handleInit(req) 1030 + handler: (pds, req, _url) => pds.handleInit(req), 980 1031 }, 981 1032 '/status': { 982 - handler: (pds, req, url) => pds.handleStatus() 1033 + handler: (pds, _req, _url) => pds.handleStatus(), 983 1034 }, 984 1035 '/reset-repo': { 985 - handler: (pds, req, url) => pds.handleResetRepo() 1036 + handler: (pds, _req, _url) => pds.handleResetRepo(), 986 1037 }, 987 1038 '/forward-event': { 988 - handler: (pds, req, url) => pds.handleForwardEvent(req) 1039 + handler: (pds, req, _url) => pds.handleForwardEvent(req), 989 1040 }, 990 1041 '/register-did': { 991 - handler: (pds, req, url) => pds.handleRegisterDid(req) 1042 + handler: (pds, req, _url) => pds.handleRegisterDid(req), 992 1043 }, 993 1044 '/get-registered-dids': { 994 - handler: (pds, req, url) => pds.handleGetRegisteredDids() 1045 + handler: (pds, _req, _url) => pds.handleGetRegisteredDids(), 995 1046 }, 996 1047 '/register-handle': { 997 1048 method: 'POST', 998 - handler: (pds, req, url) => pds.handleRegisterHandle(req) 1049 + handler: (pds, req, _url) => pds.handleRegisterHandle(req), 999 1050 }, 1000 1051 '/resolve-handle': { 1001 - handler: (pds, req, url) => pds.handleResolveHandle(url) 1052 + handler: (pds, _req, url) => pds.handleResolveHandle(url), 1002 1053 }, 1003 1054 '/repo-info': { 1004 - handler: (pds, req, url) => pds.handleRepoInfo() 1055 + handler: (pds, _req, _url) => pds.handleRepoInfo(), 1005 1056 }, 1006 1057 '/xrpc/com.atproto.server.describeServer': { 1007 - handler: (pds, req, url) => pds.handleDescribeServer(req) 1058 + handler: (pds, req, _url) => pds.handleDescribeServer(req), 1008 1059 }, 1009 1060 '/xrpc/com.atproto.server.createSession': { 1010 1061 method: 'POST', 1011 - handler: (pds, req, url) => pds.handleCreateSession(req) 1062 + handler: (pds, req, _url) => pds.handleCreateSession(req), 1012 1063 }, 1013 1064 '/xrpc/com.atproto.server.getSession': { 1014 - handler: (pds, req, url) => pds.handleGetSession(req) 1065 + handler: (pds, req, _url) => pds.handleGetSession(req), 1015 1066 }, 1016 1067 '/xrpc/app.bsky.actor.getPreferences': { 1017 - handler: (pds, req, url) => pds.handleGetPreferences(req) 1068 + handler: (pds, req, _url) => pds.handleGetPreferences(req), 1018 1069 }, 1019 1070 '/xrpc/app.bsky.actor.putPreferences': { 1020 1071 method: 'POST', 1021 - handler: (pds, req, url) => pds.handlePutPreferences(req) 1072 + handler: (pds, req, _url) => pds.handlePutPreferences(req), 1022 1073 }, 1023 1074 '/xrpc/com.atproto.sync.listRepos': { 1024 - handler: (pds, req, url) => pds.handleListRepos() 1075 + handler: (pds, _req, _url) => pds.handleListRepos(), 1025 1076 }, 1026 1077 '/xrpc/com.atproto.repo.createRecord': { 1027 1078 method: 'POST', 1028 - handler: (pds, req, url) => pds.handleCreateRecord(req) 1079 + handler: (pds, req, _url) => pds.handleCreateRecord(req), 1029 1080 }, 1030 1081 '/xrpc/com.atproto.repo.deleteRecord': { 1031 1082 method: 'POST', 1032 - handler: (pds, req, url) => pds.handleDeleteRecord(req) 1083 + handler: (pds, req, _url) => pds.handleDeleteRecord(req), 1033 1084 }, 1034 1085 '/xrpc/com.atproto.repo.putRecord': { 1035 1086 method: 'POST', 1036 - handler: (pds, req, url) => pds.handlePutRecord(req) 1087 + handler: (pds, req, _url) => pds.handlePutRecord(req), 1037 1088 }, 1038 1089 '/xrpc/com.atproto.repo.applyWrites': { 1039 1090 method: 'POST', 1040 - handler: (pds, req, url) => pds.handleApplyWrites(req) 1091 + handler: (pds, req, _url) => pds.handleApplyWrites(req), 1041 1092 }, 1042 1093 '/xrpc/com.atproto.repo.getRecord': { 1043 - handler: (pds, req, url) => pds.handleGetRecord(url) 1094 + handler: (pds, _req, url) => pds.handleGetRecord(url), 1044 1095 }, 1045 1096 '/xrpc/com.atproto.repo.describeRepo': { 1046 - handler: (pds, req, url) => pds.handleDescribeRepo() 1097 + handler: (pds, _req, _url) => pds.handleDescribeRepo(), 1047 1098 }, 1048 1099 '/xrpc/com.atproto.repo.listRecords': { 1049 - handler: (pds, req, url) => pds.handleListRecords(url) 1100 + handler: (pds, _req, url) => pds.handleListRecords(url), 1050 1101 }, 1051 1102 '/xrpc/com.atproto.sync.getLatestCommit': { 1052 - handler: (pds, req, url) => pds.handleGetLatestCommit() 1103 + handler: (pds, _req, _url) => pds.handleGetLatestCommit(), 1053 1104 }, 1054 1105 '/xrpc/com.atproto.sync.getRepoStatus': { 1055 - handler: (pds, req, url) => pds.handleGetRepoStatus() 1106 + handler: (pds, _req, _url) => pds.handleGetRepoStatus(), 1056 1107 }, 1057 1108 '/xrpc/com.atproto.sync.getRepo': { 1058 - handler: (pds, req, url) => pds.handleGetRepo() 1109 + handler: (pds, _req, _url) => pds.handleGetRepo(), 1059 1110 }, 1060 1111 '/xrpc/com.atproto.sync.getRecord': { 1061 - handler: (pds, req, url) => pds.handleSyncGetRecord(url) 1112 + handler: (pds, _req, url) => pds.handleSyncGetRecord(url), 1062 1113 }, 1063 1114 '/xrpc/com.atproto.sync.subscribeRepos': { 1064 - handler: (pds, req, url) => pds.handleSubscribeRepos(req, url) 1065 - } 1066 - } 1115 + handler: (pds, req, url) => pds.handleSubscribeRepos(req, url), 1116 + }, 1117 + }; 1067 1118 1068 1119 export class PersonalDataServer { 1069 1120 constructor(state, env) { 1070 - this.state = state 1071 - this.sql = state.storage.sql 1072 - this.env = env 1121 + this.state = state; 1122 + this.sql = state.storage.sql; 1123 + this.env = env; 1073 1124 1074 1125 // Initialize schema 1075 1126 this.sql.exec(` ··· 1101 1152 ); 1102 1153 1103 1154 CREATE INDEX IF NOT EXISTS idx_records_collection ON records(collection, rkey); 1104 - `) 1155 + `); 1105 1156 } 1106 1157 1107 1158 async initIdentity(did, privateKeyHex, handle = null) { 1108 - await this.state.storage.put('did', did) 1109 - await this.state.storage.put('privateKey', privateKeyHex) 1159 + await this.state.storage.put('did', did); 1160 + await this.state.storage.put('privateKey', privateKeyHex); 1110 1161 if (handle) { 1111 - await this.state.storage.put('handle', handle) 1162 + await this.state.storage.put('handle', handle); 1112 1163 } 1113 1164 } 1114 1165 1115 1166 async getDid() { 1116 1167 if (!this._did) { 1117 - this._did = await this.state.storage.get('did') 1168 + this._did = await this.state.storage.get('did'); 1118 1169 } 1119 - return this._did 1170 + return this._did; 1120 1171 } 1121 1172 1122 1173 async getHandle() { 1123 - return this.state.storage.get('handle') 1174 + return this.state.storage.get('handle'); 1124 1175 } 1125 1176 1126 1177 async getSigningKey() { 1127 - const hex = await this.state.storage.get('privateKey') 1128 - if (!hex) return null 1129 - return importPrivateKey(hexToBytes(hex)) 1178 + const hex = await this.state.storage.get('privateKey'); 1179 + if (!hex) return null; 1180 + return importPrivateKey(hexToBytes(hex)); 1130 1181 } 1131 1182 1132 1183 // Collect MST node blocks for a given root CID 1133 1184 collectMstBlocks(rootCidStr) { 1134 - const blocks = [] 1135 - const visited = new Set() 1185 + const blocks = []; 1186 + const visited = new Set(); 1136 1187 1137 1188 const collect = (cidStr) => { 1138 - if (visited.has(cidStr)) return 1139 - visited.add(cidStr) 1189 + if (visited.has(cidStr)) return; 1190 + visited.add(cidStr); 1140 1191 1141 - const rows = this.sql.exec( 1142 - `SELECT data FROM blocks WHERE cid = ?`, cidStr 1143 - ).toArray() 1144 - if (rows.length === 0) return 1192 + const rows = this.sql 1193 + .exec(`SELECT data FROM blocks WHERE cid = ?`, cidStr) 1194 + .toArray(); 1195 + if (rows.length === 0) return; 1145 1196 1146 - const data = new Uint8Array(rows[0].data) 1147 - blocks.push({ cid: cidStr, data }) // Keep as string, buildCarFile will convert 1197 + const data = new Uint8Array(rows[0].data); 1198 + blocks.push({ cid: cidStr, data }); // Keep as string, buildCarFile will convert 1148 1199 1149 1200 // Decode and follow child CIDs (MST nodes have 'l' and 'e' with 't' subtrees) 1150 1201 try { 1151 - const node = cborDecode(data) 1152 - if (node.l) collect(cidToString(node.l)) 1202 + const node = cborDecode(data); 1203 + if (node.l) collect(cidToString(node.l)); 1153 1204 if (node.e) { 1154 1205 for (const entry of node.e) { 1155 - if (entry.t) collect(cidToString(entry.t)) 1206 + if (entry.t) collect(cidToString(entry.t)); 1156 1207 } 1157 1208 } 1158 - } catch (e) { 1209 + } catch (_e) { 1159 1210 // Not an MST node, ignore 1160 1211 } 1161 - } 1212 + }; 1162 1213 1163 - collect(rootCidStr) 1164 - return blocks 1214 + collect(rootCidStr); 1215 + return blocks; 1165 1216 } 1166 1217 1167 1218 async createRecord(collection, record, rkey = null) { 1168 - const did = await this.getDid() 1169 - if (!did) throw new Error('PDS not initialized') 1219 + const did = await this.getDid(); 1220 + if (!did) throw new Error('PDS not initialized'); 1170 1221 1171 - rkey = rkey || createTid() 1172 - const uri = `at://${did}/${collection}/${rkey}` 1222 + rkey = rkey || createTid(); 1223 + const uri = `at://${did}/${collection}/${rkey}`; 1173 1224 1174 1225 // Encode and hash record (must use DAG-CBOR for proper key ordering) 1175 - const recordBytes = cborEncodeDagCbor(record) 1176 - const recordCid = await createCid(recordBytes) 1177 - const recordCidStr = cidToString(recordCid) 1226 + const recordBytes = cborEncodeDagCbor(record); 1227 + const recordCid = await createCid(recordBytes); 1228 + const recordCidStr = cidToString(recordCid); 1178 1229 1179 1230 // Store block 1180 1231 this.sql.exec( 1181 1232 `INSERT OR REPLACE INTO blocks (cid, data) VALUES (?, ?)`, 1182 - recordCidStr, recordBytes 1183 - ) 1233 + recordCidStr, 1234 + recordBytes, 1235 + ); 1184 1236 1185 1237 // Store record index 1186 1238 this.sql.exec( 1187 1239 `INSERT OR REPLACE INTO records (uri, cid, collection, rkey, value) VALUES (?, ?, ?, ?, ?)`, 1188 - uri, recordCidStr, collection, rkey, recordBytes 1189 - ) 1240 + uri, 1241 + recordCidStr, 1242 + collection, 1243 + rkey, 1244 + recordBytes, 1245 + ); 1190 1246 1191 1247 // Rebuild MST 1192 - const mst = new MST(this.sql) 1193 - const dataRoot = await mst.computeRoot() 1248 + const mst = new MST(this.sql); 1249 + const dataRoot = await mst.computeRoot(); 1194 1250 1195 1251 // Get previous commit 1196 - const prevCommits = this.sql.exec( 1197 - `SELECT cid, rev FROM commits ORDER BY seq DESC LIMIT 1` 1198 - ).toArray() 1199 - const prevCommit = prevCommits.length > 0 ? prevCommits[0] : null 1252 + const prevCommits = this.sql 1253 + .exec(`SELECT cid, rev FROM commits ORDER BY seq DESC LIMIT 1`) 1254 + .toArray(); 1255 + const prevCommit = prevCommits.length > 0 ? prevCommits[0] : null; 1200 1256 1201 1257 // Create commit 1202 - const rev = createTid() 1258 + const rev = createTid(); 1203 1259 // Build commit with CIDs wrapped in CID class (for dag-cbor tag 42 encoding) 1204 1260 const commit = { 1205 1261 did, 1206 1262 version: 3, 1207 - data: new CID(cidToBytes(dataRoot)), // CID wrapped for explicit encoding 1263 + data: new CID(cidToBytes(dataRoot)), // CID wrapped for explicit encoding 1208 1264 rev, 1209 - prev: prevCommit?.cid ? new CID(cidToBytes(prevCommit.cid)) : null 1210 - } 1265 + prev: prevCommit?.cid ? new CID(cidToBytes(prevCommit.cid)) : null, 1266 + }; 1211 1267 1212 1268 // Sign commit (using dag-cbor encoder for CIDs) 1213 - const commitBytes = cborEncodeDagCbor(commit) 1214 - const signingKey = await this.getSigningKey() 1215 - const sig = await sign(signingKey, commitBytes) 1269 + const commitBytes = cborEncodeDagCbor(commit); 1270 + const signingKey = await this.getSigningKey(); 1271 + const sig = await sign(signingKey, commitBytes); 1216 1272 1217 - const signedCommit = { ...commit, sig } 1218 - const signedBytes = cborEncodeDagCbor(signedCommit) 1219 - const commitCid = await createCid(signedBytes) 1220 - const commitCidStr = cidToString(commitCid) 1273 + const signedCommit = { ...commit, sig }; 1274 + const signedBytes = cborEncodeDagCbor(signedCommit); 1275 + const commitCid = await createCid(signedBytes); 1276 + const commitCidStr = cidToString(commitCid); 1221 1277 1222 1278 // Store commit block 1223 1279 this.sql.exec( 1224 1280 `INSERT OR REPLACE INTO blocks (cid, data) VALUES (?, ?)`, 1225 - commitCidStr, signedBytes 1226 - ) 1281 + commitCidStr, 1282 + signedBytes, 1283 + ); 1227 1284 1228 1285 // Store commit reference 1229 1286 this.sql.exec( 1230 1287 `INSERT INTO commits (cid, rev, prev) VALUES (?, ?, ?)`, 1231 - commitCidStr, rev, prevCommit?.cid || null 1232 - ) 1288 + commitCidStr, 1289 + rev, 1290 + prevCommit?.cid || null, 1291 + ); 1233 1292 1234 1293 // Update head and rev for listRepos 1235 - await this.state.storage.put('head', commitCidStr) 1236 - await this.state.storage.put('rev', rev) 1294 + await this.state.storage.put('head', commitCidStr); 1295 + await this.state.storage.put('rev', rev); 1237 1296 1238 1297 // Collect blocks for the event (record + commit + MST nodes) 1239 1298 // Build a mini CAR with just the new blocks - use string CIDs 1240 - const newBlocks = [] 1299 + const newBlocks = []; 1241 1300 // Add record block 1242 - newBlocks.push({ cid: recordCidStr, data: recordBytes }) 1301 + newBlocks.push({ cid: recordCidStr, data: recordBytes }); 1243 1302 // Add commit block 1244 - newBlocks.push({ cid: commitCidStr, data: signedBytes }) 1303 + newBlocks.push({ cid: commitCidStr, data: signedBytes }); 1245 1304 // Add MST node blocks (get all blocks referenced by commit.data) 1246 - const mstBlocks = this.collectMstBlocks(dataRoot) 1247 - newBlocks.push(...mstBlocks) 1305 + const mstBlocks = this.collectMstBlocks(dataRoot); 1306 + newBlocks.push(...mstBlocks); 1248 1307 1249 1308 // Sequence event with blocks - store complete event data including rev and time 1250 1309 // blocks must be a full CAR file with header (roots = [commitCid]) 1251 - const eventTime = new Date().toISOString() 1310 + const eventTime = new Date().toISOString(); 1252 1311 const evt = cborEncode({ 1253 - ops: [{ action: 'create', path: `${collection}/${rkey}`, cid: recordCidStr }], 1254 - blocks: buildCarFile(commitCidStr, newBlocks), // Full CAR with header 1255 - rev, // Store the actual commit revision 1256 - time: eventTime // Store the actual event time 1257 - }) 1312 + ops: [ 1313 + { action: 'create', path: `${collection}/${rkey}`, cid: recordCidStr }, 1314 + ], 1315 + blocks: buildCarFile(commitCidStr, newBlocks), // Full CAR with header 1316 + rev, // Store the actual commit revision 1317 + time: eventTime, // Store the actual event time 1318 + }); 1258 1319 this.sql.exec( 1259 1320 `INSERT INTO seq_events (did, commit_cid, evt) VALUES (?, ?, ?)`, 1260 - did, commitCidStr, evt 1261 - ) 1321 + did, 1322 + commitCidStr, 1323 + evt, 1324 + ); 1262 1325 1263 1326 // Broadcast to subscribers (both local and via default DO for relay) 1264 - const evtRows = this.sql.exec( 1265 - `SELECT * FROM seq_events ORDER BY seq DESC LIMIT 1` 1266 - ).toArray() 1327 + const evtRows = this.sql 1328 + .exec(`SELECT * FROM seq_events ORDER BY seq DESC LIMIT 1`) 1329 + .toArray(); 1267 1330 if (evtRows.length > 0) { 1268 - this.broadcastEvent(evtRows[0]) 1331 + this.broadcastEvent(evtRows[0]); 1269 1332 // Also forward to default DO for relay subscribers 1270 1333 if (this.env?.PDS) { 1271 - const defaultId = this.env.PDS.idFromName('default') 1272 - const defaultPds = this.env.PDS.get(defaultId) 1334 + const defaultId = this.env.PDS.idFromName('default'); 1335 + const defaultPds = this.env.PDS.get(defaultId); 1273 1336 // Convert ArrayBuffer to array for JSON serialization 1274 - const row = evtRows[0] 1275 - const evtArray = Array.from(new Uint8Array(row.evt)) 1337 + const row = evtRows[0]; 1338 + const evtArray = Array.from(new Uint8Array(row.evt)); 1276 1339 // Fire and forget but log errors 1277 - defaultPds.fetch(new Request('http://internal/forward-event', { 1278 - method: 'POST', 1279 - body: JSON.stringify({ ...row, evt: evtArray }) 1280 - })).then(r => r.json()).then(r => console.log('forward result:', r)).catch(e => console.log('forward error:', e)) 1340 + defaultPds 1341 + .fetch( 1342 + new Request('http://internal/forward-event', { 1343 + method: 'POST', 1344 + body: JSON.stringify({ ...row, evt: evtArray }), 1345 + }), 1346 + ) 1347 + .then((r) => r.json()) 1348 + .then((r) => console.log('forward result:', r)) 1349 + .catch((e) => console.log('forward error:', e)); 1281 1350 } 1282 1351 } 1283 1352 1284 - return { uri, cid: recordCidStr, commit: commitCidStr } 1353 + return { uri, cid: recordCidStr, commit: commitCidStr }; 1285 1354 } 1286 1355 1287 1356 async deleteRecord(collection, rkey) { 1288 - const did = await this.getDid() 1289 - if (!did) throw new Error('PDS not initialized') 1357 + const did = await this.getDid(); 1358 + if (!did) throw new Error('PDS not initialized'); 1290 1359 1291 - const uri = `at://${did}/${collection}/${rkey}` 1360 + const uri = `at://${did}/${collection}/${rkey}`; 1292 1361 1293 1362 // Check if record exists 1294 - const existing = this.sql.exec( 1295 - `SELECT cid FROM records WHERE uri = ?`, uri 1296 - ).toArray() 1363 + const existing = this.sql 1364 + .exec(`SELECT cid FROM records WHERE uri = ?`, uri) 1365 + .toArray(); 1297 1366 if (existing.length === 0) { 1298 - return { error: 'RecordNotFound', message: 'record not found' } 1367 + return { error: 'RecordNotFound', message: 'record not found' }; 1299 1368 } 1300 1369 1301 1370 // Delete from records table 1302 - this.sql.exec(`DELETE FROM records WHERE uri = ?`, uri) 1371 + this.sql.exec(`DELETE FROM records WHERE uri = ?`, uri); 1303 1372 1304 1373 // Rebuild MST 1305 - const mst = new MST(this.sql) 1306 - const dataRoot = await mst.computeRoot() 1374 + const mst = new MST(this.sql); 1375 + const dataRoot = await mst.computeRoot(); 1307 1376 1308 1377 // Get previous commit 1309 - const prevCommits = this.sql.exec( 1310 - `SELECT cid, rev FROM commits ORDER BY seq DESC LIMIT 1` 1311 - ).toArray() 1312 - const prevCommit = prevCommits.length > 0 ? prevCommits[0] : null 1378 + const prevCommits = this.sql 1379 + .exec(`SELECT cid, rev FROM commits ORDER BY seq DESC LIMIT 1`) 1380 + .toArray(); 1381 + const prevCommit = prevCommits.length > 0 ? prevCommits[0] : null; 1313 1382 1314 1383 // Create commit 1315 - const rev = createTid() 1384 + const rev = createTid(); 1316 1385 const commit = { 1317 1386 did, 1318 1387 version: 3, 1319 1388 data: dataRoot ? new CID(cidToBytes(dataRoot)) : null, 1320 1389 rev, 1321 - prev: prevCommit?.cid ? new CID(cidToBytes(prevCommit.cid)) : null 1322 - } 1390 + prev: prevCommit?.cid ? new CID(cidToBytes(prevCommit.cid)) : null, 1391 + }; 1323 1392 1324 1393 // Sign commit 1325 - const commitBytes = cborEncodeDagCbor(commit) 1326 - const signingKey = await this.getSigningKey() 1327 - const sig = await sign(signingKey, commitBytes) 1394 + const commitBytes = cborEncodeDagCbor(commit); 1395 + const signingKey = await this.getSigningKey(); 1396 + const sig = await sign(signingKey, commitBytes); 1328 1397 1329 - const signedCommit = { ...commit, sig } 1330 - const signedBytes = cborEncodeDagCbor(signedCommit) 1331 - const commitCid = await createCid(signedBytes) 1332 - const commitCidStr = cidToString(commitCid) 1398 + const signedCommit = { ...commit, sig }; 1399 + const signedBytes = cborEncodeDagCbor(signedCommit); 1400 + const commitCid = await createCid(signedBytes); 1401 + const commitCidStr = cidToString(commitCid); 1333 1402 1334 1403 // Store commit block 1335 1404 this.sql.exec( 1336 1405 `INSERT OR REPLACE INTO blocks (cid, data) VALUES (?, ?)`, 1337 - commitCidStr, signedBytes 1338 - ) 1406 + commitCidStr, 1407 + signedBytes, 1408 + ); 1339 1409 1340 1410 // Store commit reference 1341 1411 this.sql.exec( 1342 1412 `INSERT INTO commits (cid, rev, prev) VALUES (?, ?, ?)`, 1343 - commitCidStr, rev, prevCommit?.cid || null 1344 - ) 1413 + commitCidStr, 1414 + rev, 1415 + prevCommit?.cid || null, 1416 + ); 1345 1417 1346 1418 // Update head and rev 1347 - await this.state.storage.put('head', commitCidStr) 1348 - await this.state.storage.put('rev', rev) 1419 + await this.state.storage.put('head', commitCidStr); 1420 + await this.state.storage.put('rev', rev); 1349 1421 1350 1422 // Collect blocks for the event (commit + MST nodes, no record block) 1351 - const newBlocks = [] 1352 - newBlocks.push({ cid: commitCidStr, data: signedBytes }) 1423 + const newBlocks = []; 1424 + newBlocks.push({ cid: commitCidStr, data: signedBytes }); 1353 1425 if (dataRoot) { 1354 - const mstBlocks = this.collectMstBlocks(dataRoot) 1355 - newBlocks.push(...mstBlocks) 1426 + const mstBlocks = this.collectMstBlocks(dataRoot); 1427 + newBlocks.push(...mstBlocks); 1356 1428 } 1357 1429 1358 1430 // Sequence event with delete action 1359 - const eventTime = new Date().toISOString() 1431 + const eventTime = new Date().toISOString(); 1360 1432 const evt = cborEncode({ 1361 1433 ops: [{ action: 'delete', path: `${collection}/${rkey}`, cid: null }], 1362 1434 blocks: buildCarFile(commitCidStr, newBlocks), 1363 1435 rev, 1364 - time: eventTime 1365 - }) 1436 + time: eventTime, 1437 + }); 1366 1438 this.sql.exec( 1367 1439 `INSERT INTO seq_events (did, commit_cid, evt) VALUES (?, ?, ?)`, 1368 - did, commitCidStr, evt 1369 - ) 1440 + did, 1441 + commitCidStr, 1442 + evt, 1443 + ); 1370 1444 1371 1445 // Broadcast to subscribers 1372 - const evtRows = this.sql.exec( 1373 - `SELECT * FROM seq_events ORDER BY seq DESC LIMIT 1` 1374 - ).toArray() 1446 + const evtRows = this.sql 1447 + .exec(`SELECT * FROM seq_events ORDER BY seq DESC LIMIT 1`) 1448 + .toArray(); 1375 1449 if (evtRows.length > 0) { 1376 - this.broadcastEvent(evtRows[0]) 1450 + this.broadcastEvent(evtRows[0]); 1377 1451 // Forward to default DO for relay subscribers 1378 1452 if (this.env?.PDS) { 1379 - const defaultId = this.env.PDS.idFromName('default') 1380 - const defaultPds = this.env.PDS.get(defaultId) 1381 - const row = evtRows[0] 1382 - const evtArray = Array.from(new Uint8Array(row.evt)) 1383 - defaultPds.fetch(new Request('http://internal/forward-event', { 1384 - method: 'POST', 1385 - body: JSON.stringify({ ...row, evt: evtArray }) 1386 - })).catch(e => console.log('forward error:', e)) 1453 + const defaultId = this.env.PDS.idFromName('default'); 1454 + const defaultPds = this.env.PDS.get(defaultId); 1455 + const row = evtRows[0]; 1456 + const evtArray = Array.from(new Uint8Array(row.evt)); 1457 + defaultPds 1458 + .fetch( 1459 + new Request('http://internal/forward-event', { 1460 + method: 'POST', 1461 + body: JSON.stringify({ ...row, evt: evtArray }), 1462 + }), 1463 + ) 1464 + .catch((e) => console.log('forward error:', e)); 1387 1465 } 1388 1466 } 1389 1467 1390 - return { ok: true } 1468 + return { ok: true }; 1391 1469 } 1392 1470 1393 1471 formatEvent(evt) { 1394 1472 // AT Protocol frame format: header + body 1395 1473 // Use DAG-CBOR encoding for body (CIDs need tag 42 + 0x00 prefix) 1396 - const header = cborEncode({ op: 1, t: '#commit' }) 1474 + const header = cborEncode({ op: 1, t: '#commit' }); 1397 1475 1398 1476 // Decode stored event to get ops, blocks, rev, and time 1399 - const evtData = cborDecode(new Uint8Array(evt.evt)) 1400 - const ops = evtData.ops.map(op => ({ 1477 + const evtData = cborDecode(new Uint8Array(evt.evt)); 1478 + const ops = evtData.ops.map((op) => ({ 1401 1479 ...op, 1402 - cid: op.cid ? new CID(cidToBytes(op.cid)) : null // Wrap in CID class for tag 42 encoding 1403 - })) 1480 + cid: op.cid ? new CID(cidToBytes(op.cid)) : null, // Wrap in CID class for tag 42 encoding 1481 + })); 1404 1482 // Get blocks from stored event (already in CAR format) 1405 - const blocks = evtData.blocks || new Uint8Array(0) 1483 + const blocks = evtData.blocks || new Uint8Array(0); 1406 1484 1407 1485 const body = cborEncodeDagCbor({ 1408 1486 seq: evt.seq, 1409 1487 rebase: false, 1410 1488 tooBig: false, 1411 1489 repo: evt.did, 1412 - commit: new CID(cidToBytes(evt.commit_cid)), // Wrap in CID class for tag 42 encoding 1413 - rev: evtData.rev, // Use stored rev from commit creation 1490 + commit: new CID(cidToBytes(evt.commit_cid)), // Wrap in CID class for tag 42 encoding 1491 + rev: evtData.rev, // Use stored rev from commit creation 1414 1492 since: null, 1415 1493 blocks: blocks instanceof Uint8Array ? blocks : new Uint8Array(blocks), 1416 1494 ops, 1417 1495 blobs: [], 1418 - time: evtData.time // Use stored time from event creation 1419 - }) 1496 + time: evtData.time, // Use stored time from event creation 1497 + }); 1420 1498 1421 1499 // Concatenate header + body 1422 - const frame = new Uint8Array(header.length + body.length) 1423 - frame.set(header) 1424 - frame.set(body, header.length) 1425 - return frame 1500 + const frame = new Uint8Array(header.length + body.length); 1501 + frame.set(header); 1502 + frame.set(body, header.length); 1503 + return frame; 1426 1504 } 1427 1505 1428 1506 async webSocketMessage(ws, message) { 1429 1507 // Handle ping 1430 - if (message === 'ping') ws.send('pong') 1508 + if (message === 'ping') ws.send('pong'); 1431 1509 } 1432 1510 1433 - async webSocketClose(ws, code, reason) { 1511 + async webSocketClose(_ws, _code, _reason) { 1434 1512 // Durable Object will hibernate when no connections remain 1435 1513 } 1436 1514 1437 1515 broadcastEvent(evt) { 1438 - const frame = this.formatEvent(evt) 1516 + const frame = this.formatEvent(evt); 1439 1517 for (const ws of this.state.getWebSockets()) { 1440 1518 try { 1441 - ws.send(frame) 1442 - } catch (e) { 1519 + ws.send(frame); 1520 + } catch (_e) { 1443 1521 // Client disconnected 1444 1522 } 1445 1523 } 1446 1524 } 1447 1525 1448 1526 async handleAtprotoDid() { 1449 - let did = await this.getDid() 1527 + let did = await this.getDid(); 1450 1528 if (!did) { 1451 - const registeredDids = await this.state.storage.get('registeredDids') || [] 1452 - did = registeredDids[0] 1529 + const registeredDids = 1530 + (await this.state.storage.get('registeredDids')) || []; 1531 + did = registeredDids[0]; 1453 1532 } 1454 1533 if (!did) { 1455 - return new Response('User not found', { status: 404 }) 1534 + return new Response('User not found', { status: 404 }); 1456 1535 } 1457 - return new Response(did, { headers: { 'Content-Type': 'text/plain' } }) 1536 + return new Response(did, { headers: { 'Content-Type': 'text/plain' } }); 1458 1537 } 1459 1538 1460 1539 async handleInit(request) { 1461 - const body = await request.json() 1540 + const body = await request.json(); 1462 1541 if (!body.did || !body.privateKey) { 1463 - return errorResponse('InvalidRequest', 'missing did or privateKey', 400) 1542 + return errorResponse('InvalidRequest', 'missing did or privateKey', 400); 1464 1543 } 1465 - await this.initIdentity(body.did, body.privateKey, body.handle || null) 1466 - return Response.json({ ok: true, did: body.did, handle: body.handle || null }) 1544 + await this.initIdentity(body.did, body.privateKey, body.handle || null); 1545 + return Response.json({ 1546 + ok: true, 1547 + did: body.did, 1548 + handle: body.handle || null, 1549 + }); 1467 1550 } 1468 1551 1469 1552 async handleStatus() { 1470 - const did = await this.getDid() 1471 - return Response.json({ initialized: !!did, did: did || null }) 1553 + const did = await this.getDid(); 1554 + return Response.json({ initialized: !!did, did: did || null }); 1472 1555 } 1473 1556 1474 1557 async handleResetRepo() { 1475 - this.sql.exec(`DELETE FROM blocks`) 1476 - this.sql.exec(`DELETE FROM records`) 1477 - this.sql.exec(`DELETE FROM commits`) 1478 - this.sql.exec(`DELETE FROM seq_events`) 1479 - await this.state.storage.delete('head') 1480 - await this.state.storage.delete('rev') 1481 - return Response.json({ ok: true, message: 'repo data cleared' }) 1558 + this.sql.exec(`DELETE FROM blocks`); 1559 + this.sql.exec(`DELETE FROM records`); 1560 + this.sql.exec(`DELETE FROM commits`); 1561 + this.sql.exec(`DELETE FROM seq_events`); 1562 + await this.state.storage.delete('head'); 1563 + await this.state.storage.delete('rev'); 1564 + return Response.json({ ok: true, message: 'repo data cleared' }); 1482 1565 } 1483 1566 1484 1567 async handleForwardEvent(request) { 1485 - const evt = await request.json() 1486 - const numSockets = [...this.state.getWebSockets()].length 1487 - console.log(`forward-event: received event seq=${evt.seq}, ${numSockets} connected sockets`) 1568 + const evt = await request.json(); 1569 + const numSockets = [...this.state.getWebSockets()].length; 1570 + console.log( 1571 + `forward-event: received event seq=${evt.seq}, ${numSockets} connected sockets`, 1572 + ); 1488 1573 this.broadcastEvent({ 1489 1574 seq: evt.seq, 1490 1575 did: evt.did, 1491 1576 commit_cid: evt.commit_cid, 1492 - evt: new Uint8Array(Object.values(evt.evt)) 1493 - }) 1494 - return Response.json({ ok: true, sockets: numSockets }) 1577 + evt: new Uint8Array(Object.values(evt.evt)), 1578 + }); 1579 + return Response.json({ ok: true, sockets: numSockets }); 1495 1580 } 1496 1581 1497 1582 async handleRegisterDid(request) { 1498 - const body = await request.json() 1499 - const registeredDids = await this.state.storage.get('registeredDids') || [] 1583 + const body = await request.json(); 1584 + const registeredDids = 1585 + (await this.state.storage.get('registeredDids')) || []; 1500 1586 if (!registeredDids.includes(body.did)) { 1501 - registeredDids.push(body.did) 1502 - await this.state.storage.put('registeredDids', registeredDids) 1587 + registeredDids.push(body.did); 1588 + await this.state.storage.put('registeredDids', registeredDids); 1503 1589 } 1504 - return Response.json({ ok: true }) 1590 + return Response.json({ ok: true }); 1505 1591 } 1506 1592 1507 1593 async handleGetRegisteredDids() { 1508 - const registeredDids = await this.state.storage.get('registeredDids') || [] 1509 - return Response.json({ dids: registeredDids }) 1594 + const registeredDids = 1595 + (await this.state.storage.get('registeredDids')) || []; 1596 + return Response.json({ dids: registeredDids }); 1510 1597 } 1511 1598 1512 1599 async handleRegisterHandle(request) { 1513 - const body = await request.json() 1514 - const { handle, did } = body 1600 + const body = await request.json(); 1601 + const { handle, did } = body; 1515 1602 if (!handle || !did) { 1516 - return errorResponse('InvalidRequest', 'missing handle or did', 400) 1603 + return errorResponse('InvalidRequest', 'missing handle or did', 400); 1517 1604 } 1518 - const handleMap = await this.state.storage.get('handleMap') || {} 1519 - handleMap[handle] = did 1520 - await this.state.storage.put('handleMap', handleMap) 1521 - return Response.json({ ok: true }) 1605 + const handleMap = (await this.state.storage.get('handleMap')) || {}; 1606 + handleMap[handle] = did; 1607 + await this.state.storage.put('handleMap', handleMap); 1608 + return Response.json({ ok: true }); 1522 1609 } 1523 1610 1524 1611 async handleResolveHandle(url) { 1525 - const handle = url.searchParams.get('handle') 1612 + const handle = url.searchParams.get('handle'); 1526 1613 if (!handle) { 1527 - return errorResponse('InvalidRequest', 'missing handle', 400) 1614 + return errorResponse('InvalidRequest', 'missing handle', 400); 1528 1615 } 1529 - const handleMap = await this.state.storage.get('handleMap') || {} 1530 - const did = handleMap[handle] 1616 + const handleMap = (await this.state.storage.get('handleMap')) || {}; 1617 + const did = handleMap[handle]; 1531 1618 if (!did) { 1532 - return errorResponse('NotFound', 'handle not found', 404) 1619 + return errorResponse('NotFound', 'handle not found', 404); 1533 1620 } 1534 - return Response.json({ did }) 1621 + return Response.json({ did }); 1535 1622 } 1536 1623 1537 1624 async handleRepoInfo() { 1538 - const head = await this.state.storage.get('head') 1539 - const rev = await this.state.storage.get('rev') 1540 - return Response.json({ head: head || null, rev: rev || null }) 1625 + const head = await this.state.storage.get('head'); 1626 + const rev = await this.state.storage.get('rev'); 1627 + return Response.json({ head: head || null, rev: rev || null }); 1541 1628 } 1542 1629 1543 1630 handleDescribeServer(request) { 1544 - const hostname = request.headers.get('x-hostname') || 'localhost' 1631 + const hostname = request.headers.get('x-hostname') || 'localhost'; 1545 1632 return Response.json({ 1546 1633 did: `did:web:${hostname}`, 1547 1634 availableUserDomains: [`.${hostname}`], 1548 1635 inviteCodeRequired: false, 1549 1636 phoneVerificationRequired: false, 1550 1637 links: {}, 1551 - contact: {} 1552 - }) 1638 + contact: {}, 1639 + }); 1553 1640 } 1554 1641 1555 1642 async handleCreateSession(request) { 1556 - const body = await request.json() 1557 - const { identifier, password } = body 1643 + const body = await request.json(); 1644 + const { identifier, password } = body; 1558 1645 1559 1646 if (!identifier || !password) { 1560 - return errorResponse('InvalidRequest', 'Missing identifier or password', 400) 1647 + return errorResponse( 1648 + 'InvalidRequest', 1649 + 'Missing identifier or password', 1650 + 400, 1651 + ); 1561 1652 } 1562 1653 1563 1654 // Check password against env var 1564 - const expectedPassword = this.env?.PDS_PASSWORD 1655 + const expectedPassword = this.env?.PDS_PASSWORD; 1565 1656 if (!expectedPassword || password !== expectedPassword) { 1566 - return errorResponse('AuthenticationRequired', 'Invalid identifier or password', 401) 1657 + return errorResponse( 1658 + 'AuthenticationRequired', 1659 + 'Invalid identifier or password', 1660 + 401, 1661 + ); 1567 1662 } 1568 1663 1569 1664 // Resolve identifier to DID 1570 - let did = identifier 1665 + let did = identifier; 1571 1666 if (!identifier.startsWith('did:')) { 1572 1667 // Try to resolve handle 1573 - const handleMap = await this.state.storage.get('handleMap') || {} 1574 - did = handleMap[identifier] 1668 + const handleMap = (await this.state.storage.get('handleMap')) || {}; 1669 + did = handleMap[identifier]; 1575 1670 if (!did) { 1576 - return errorResponse('InvalidRequest', 'Unable to resolve handle', 400) 1671 + return errorResponse('InvalidRequest', 'Unable to resolve handle', 400); 1577 1672 } 1578 1673 } 1579 1674 1580 1675 // Get handle for response 1581 - const handle = await this.getHandleForDid(did) 1676 + const handle = await this.getHandleForDid(did); 1582 1677 1583 1678 // Create tokens 1584 - const jwtSecret = this.env?.JWT_SECRET 1679 + const jwtSecret = this.env?.JWT_SECRET; 1585 1680 if (!jwtSecret) { 1586 - return errorResponse('InternalServerError', 'Server not configured for authentication', 500) 1681 + return errorResponse( 1682 + 'InternalServerError', 1683 + 'Server not configured for authentication', 1684 + 500, 1685 + ); 1587 1686 } 1588 1687 1589 - const accessJwt = await createAccessJwt(did, jwtSecret) 1590 - const refreshJwt = await createRefreshJwt(did, jwtSecret) 1688 + const accessJwt = await createAccessJwt(did, jwtSecret); 1689 + const refreshJwt = await createRefreshJwt(did, jwtSecret); 1591 1690 1592 1691 return Response.json({ 1593 1692 accessJwt, 1594 1693 refreshJwt, 1595 1694 handle: handle || did, 1596 1695 did, 1597 - active: true 1598 - }) 1696 + active: true, 1697 + }); 1599 1698 } 1600 1699 1601 1700 async handleGetSession(request) { 1602 - const authHeader = request.headers.get('Authorization') 1701 + const authHeader = request.headers.get('Authorization'); 1603 1702 if (!authHeader || !authHeader.startsWith('Bearer ')) { 1604 - return errorResponse('AuthenticationRequired', 'Missing or invalid authorization header', 401) 1703 + return errorResponse( 1704 + 'AuthenticationRequired', 1705 + 'Missing or invalid authorization header', 1706 + 401, 1707 + ); 1605 1708 } 1606 1709 1607 - const token = authHeader.slice(7) // Remove 'Bearer ' 1608 - const jwtSecret = this.env?.JWT_SECRET 1710 + const token = authHeader.slice(7); // Remove 'Bearer ' 1711 + const jwtSecret = this.env?.JWT_SECRET; 1609 1712 if (!jwtSecret) { 1610 - return errorResponse('InternalServerError', 'Server not configured for authentication', 500) 1713 + return errorResponse( 1714 + 'InternalServerError', 1715 + 'Server not configured for authentication', 1716 + 500, 1717 + ); 1611 1718 } 1612 1719 1613 1720 try { 1614 - const payload = await verifyAccessJwt(token, jwtSecret) 1615 - const did = payload.sub 1616 - const handle = await this.getHandleForDid(did) 1721 + const payload = await verifyAccessJwt(token, jwtSecret); 1722 + const did = payload.sub; 1723 + const handle = await this.getHandleForDid(did); 1617 1724 1618 1725 return Response.json({ 1619 1726 handle: handle || did, 1620 1727 did, 1621 - active: true 1622 - }) 1728 + active: true, 1729 + }); 1623 1730 } catch (err) { 1624 - return errorResponse('InvalidToken', err.message, 401) 1731 + return errorResponse('InvalidToken', err.message, 401); 1625 1732 } 1626 1733 } 1627 1734 1628 - async handleGetPreferences(request) { 1735 + async handleGetPreferences(_request) { 1629 1736 // Preferences are stored per-user in their DO 1630 - const preferences = await this.state.storage.get('preferences') || [] 1631 - return Response.json({ preferences }) 1737 + const preferences = (await this.state.storage.get('preferences')) || []; 1738 + return Response.json({ preferences }); 1632 1739 } 1633 1740 1634 1741 async handlePutPreferences(request) { 1635 - const body = await request.json() 1636 - const { preferences } = body 1742 + const body = await request.json(); 1743 + const { preferences } = body; 1637 1744 if (!Array.isArray(preferences)) { 1638 - return errorResponse('InvalidRequest', 'preferences must be an array', 400) 1745 + return errorResponse( 1746 + 'InvalidRequest', 1747 + 'preferences must be an array', 1748 + 400, 1749 + ); 1639 1750 } 1640 - await this.state.storage.put('preferences', preferences) 1641 - return Response.json({}) 1751 + await this.state.storage.put('preferences', preferences); 1752 + return Response.json({}); 1642 1753 } 1643 1754 1644 1755 async getHandleForDid(did) { 1645 1756 // Check if this DID has a handle registered 1646 - const handleMap = await this.state.storage.get('handleMap') || {} 1757 + const handleMap = (await this.state.storage.get('handleMap')) || {}; 1647 1758 for (const [handle, mappedDid] of Object.entries(handleMap)) { 1648 - if (mappedDid === did) return handle 1759 + if (mappedDid === did) return handle; 1649 1760 } 1650 1761 // Check instance's own handle 1651 - const instanceDid = await this.getDid() 1762 + const instanceDid = await this.getDid(); 1652 1763 if (instanceDid === did) { 1653 - return await this.state.storage.get('handle') 1764 + return await this.state.storage.get('handle'); 1654 1765 } 1655 - return null 1766 + return null; 1656 1767 } 1657 1768 1658 1769 async createServiceAuthForAppView(did, lxm) { 1659 - const signingKey = await this.getSigningKey() 1770 + const signingKey = await this.getSigningKey(); 1660 1771 return createServiceJwt({ 1661 1772 iss: did, 1662 1773 aud: 'did:web:api.bsky.app', 1663 1774 lxm, 1664 - signingKey 1665 - }) 1775 + signingKey, 1776 + }); 1666 1777 } 1667 1778 1668 1779 async handleAppViewProxy(request, userDid) { 1669 - const url = new URL(request.url) 1780 + const url = new URL(request.url); 1670 1781 // Extract lexicon method from path: /xrpc/app.bsky.actor.getPreferences -> app.bsky.actor.getPreferences 1671 - const lxm = url.pathname.replace('/xrpc/', '') 1782 + const lxm = url.pathname.replace('/xrpc/', ''); 1672 1783 1673 1784 // Create service auth JWT 1674 - const serviceJwt = await this.createServiceAuthForAppView(userDid, lxm) 1785 + const serviceJwt = await this.createServiceAuthForAppView(userDid, lxm); 1675 1786 1676 1787 // Build AppView URL 1677 - const appViewUrl = new URL(url.pathname + url.search, 'https://api.bsky.app') 1788 + const appViewUrl = new URL( 1789 + url.pathname + url.search, 1790 + 'https://api.bsky.app', 1791 + ); 1678 1792 1679 1793 // Forward request with service auth 1680 - const headers = new Headers() 1681 - headers.set('Authorization', `Bearer ${serviceJwt}`) 1682 - headers.set('Content-Type', request.headers.get('Content-Type') || 'application/json') 1794 + const headers = new Headers(); 1795 + headers.set('Authorization', `Bearer ${serviceJwt}`); 1796 + headers.set( 1797 + 'Content-Type', 1798 + request.headers.get('Content-Type') || 'application/json', 1799 + ); 1683 1800 if (request.headers.get('Accept')) { 1684 - headers.set('Accept', request.headers.get('Accept')) 1801 + headers.set('Accept', request.headers.get('Accept')); 1685 1802 } 1686 1803 if (request.headers.get('Accept-Language')) { 1687 - headers.set('Accept-Language', request.headers.get('Accept-Language')) 1804 + headers.set('Accept-Language', request.headers.get('Accept-Language')); 1688 1805 } 1689 1806 1690 1807 const proxyReq = new Request(appViewUrl.toString(), { 1691 1808 method: request.method, 1692 1809 headers, 1693 - body: request.method !== 'GET' && request.method !== 'HEAD' ? request.body : undefined, 1694 - }) 1810 + body: 1811 + request.method !== 'GET' && request.method !== 'HEAD' 1812 + ? request.body 1813 + : undefined, 1814 + }); 1695 1815 1696 1816 try { 1697 - const response = await fetch(proxyReq) 1817 + const response = await fetch(proxyReq); 1698 1818 // Return the response with CORS headers 1699 - const responseHeaders = new Headers(response.headers) 1700 - responseHeaders.set('Access-Control-Allow-Origin', '*') 1819 + const responseHeaders = new Headers(response.headers); 1820 + responseHeaders.set('Access-Control-Allow-Origin', '*'); 1701 1821 return new Response(response.body, { 1702 1822 status: response.status, 1703 1823 statusText: response.statusText, 1704 - headers: responseHeaders 1705 - }) 1824 + headers: responseHeaders, 1825 + }); 1706 1826 } catch (err) { 1707 - return errorResponse('UpstreamFailure', 'Failed to reach AppView: ' + err.message, 502) 1827 + return errorResponse( 1828 + 'UpstreamFailure', 1829 + `Failed to reach AppView: ${err.message}`, 1830 + 502, 1831 + ); 1708 1832 } 1709 1833 } 1710 1834 1711 1835 async handleListRepos() { 1712 - const registeredDids = await this.state.storage.get('registeredDids') || [] 1713 - const did = await this.getDid() 1714 - const repos = did ? [{ did, head: null, rev: null }] : 1715 - registeredDids.map(d => ({ did: d, head: null, rev: null })) 1716 - return Response.json({ repos }) 1836 + const registeredDids = 1837 + (await this.state.storage.get('registeredDids')) || []; 1838 + const did = await this.getDid(); 1839 + const repos = did 1840 + ? [{ did, head: null, rev: null }] 1841 + : registeredDids.map((d) => ({ did: d, head: null, rev: null })); 1842 + return Response.json({ repos }); 1717 1843 } 1718 1844 1719 1845 async handleCreateRecord(request) { 1720 - const body = await request.json() 1846 + const body = await request.json(); 1721 1847 if (!body.collection || !body.record) { 1722 - return errorResponse('InvalidRequest', 'missing collection or record', 400) 1848 + return errorResponse( 1849 + 'InvalidRequest', 1850 + 'missing collection or record', 1851 + 400, 1852 + ); 1723 1853 } 1724 1854 try { 1725 - const result = await this.createRecord(body.collection, body.record, body.rkey) 1726 - const head = await this.state.storage.get('head') 1727 - const rev = await this.state.storage.get('rev') 1855 + const result = await this.createRecord( 1856 + body.collection, 1857 + body.record, 1858 + body.rkey, 1859 + ); 1860 + const head = await this.state.storage.get('head'); 1861 + const rev = await this.state.storage.get('rev'); 1728 1862 return Response.json({ 1729 1863 uri: result.uri, 1730 1864 cid: result.cid, 1731 1865 commit: { cid: head, rev }, 1732 - validationStatus: 'valid' 1733 - }) 1866 + validationStatus: 'valid', 1867 + }); 1734 1868 } catch (err) { 1735 - return errorResponse('InternalError', err.message, 500) 1869 + return errorResponse('InternalError', err.message, 500); 1736 1870 } 1737 1871 } 1738 1872 1739 1873 async handleDeleteRecord(request) { 1740 - const body = await request.json() 1874 + const body = await request.json(); 1741 1875 if (!body.collection || !body.rkey) { 1742 - return errorResponse('InvalidRequest', 'missing collection or rkey', 400) 1876 + return errorResponse('InvalidRequest', 'missing collection or rkey', 400); 1743 1877 } 1744 1878 try { 1745 - const result = await this.deleteRecord(body.collection, body.rkey) 1879 + const result = await this.deleteRecord(body.collection, body.rkey); 1746 1880 if (result.error) { 1747 - return Response.json(result, { status: 404 }) 1881 + return Response.json(result, { status: 404 }); 1748 1882 } 1749 - return Response.json({}) 1883 + return Response.json({}); 1750 1884 } catch (err) { 1751 - return errorResponse('InternalError', err.message, 500) 1885 + return errorResponse('InternalError', err.message, 500); 1752 1886 } 1753 1887 } 1754 1888 1755 1889 async handlePutRecord(request) { 1756 - const body = await request.json() 1890 + const body = await request.json(); 1757 1891 if (!body.collection || !body.rkey || !body.record) { 1758 - return errorResponse('InvalidRequest', 'missing collection, rkey, or record', 400) 1892 + return errorResponse( 1893 + 'InvalidRequest', 1894 + 'missing collection, rkey, or record', 1895 + 400, 1896 + ); 1759 1897 } 1760 1898 try { 1761 1899 // putRecord is like createRecord but with a specific rkey (upsert) 1762 - const result = await this.createRecord(body.collection, body.record, body.rkey) 1763 - const head = await this.state.storage.get('head') 1764 - const rev = await this.state.storage.get('rev') 1900 + const result = await this.createRecord( 1901 + body.collection, 1902 + body.record, 1903 + body.rkey, 1904 + ); 1905 + const head = await this.state.storage.get('head'); 1906 + const rev = await this.state.storage.get('rev'); 1765 1907 return Response.json({ 1766 1908 uri: result.uri, 1767 1909 cid: result.cid, 1768 1910 commit: { cid: head, rev }, 1769 - validationStatus: 'valid' 1770 - }) 1911 + validationStatus: 'valid', 1912 + }); 1771 1913 } catch (err) { 1772 - return errorResponse('InternalError', err.message, 500) 1914 + return errorResponse('InternalError', err.message, 500); 1773 1915 } 1774 1916 } 1775 1917 1776 1918 async handleApplyWrites(request) { 1777 - const body = await request.json() 1919 + const body = await request.json(); 1778 1920 if (!body.writes || !Array.isArray(body.writes)) { 1779 - return errorResponse('InvalidRequest', 'missing writes array', 400) 1921 + return errorResponse('InvalidRequest', 'missing writes array', 400); 1780 1922 } 1781 1923 try { 1782 - const results = [] 1924 + const results = []; 1783 1925 for (const write of body.writes) { 1784 - const type = write['$type'] 1926 + const type = write.$type; 1785 1927 if (type === 'com.atproto.repo.applyWrites#create') { 1786 - const result = await this.createRecord(write.collection, write.value, write.rkey) 1928 + const result = await this.createRecord( 1929 + write.collection, 1930 + write.value, 1931 + write.rkey, 1932 + ); 1787 1933 results.push({ 1788 1934 $type: 'com.atproto.repo.applyWrites#createResult', 1789 1935 uri: result.uri, 1790 1936 cid: result.cid, 1791 - validationStatus: 'valid' 1792 - }) 1937 + validationStatus: 'valid', 1938 + }); 1793 1939 } else if (type === 'com.atproto.repo.applyWrites#update') { 1794 - const result = await this.createRecord(write.collection, write.value, write.rkey) 1940 + const result = await this.createRecord( 1941 + write.collection, 1942 + write.value, 1943 + write.rkey, 1944 + ); 1795 1945 results.push({ 1796 1946 $type: 'com.atproto.repo.applyWrites#updateResult', 1797 1947 uri: result.uri, 1798 1948 cid: result.cid, 1799 - validationStatus: 'valid' 1800 - }) 1949 + validationStatus: 'valid', 1950 + }); 1801 1951 } else if (type === 'com.atproto.repo.applyWrites#delete') { 1802 - await this.deleteRecord(write.collection, write.rkey) 1952 + await this.deleteRecord(write.collection, write.rkey); 1803 1953 results.push({ 1804 - $type: 'com.atproto.repo.applyWrites#deleteResult' 1805 - }) 1954 + $type: 'com.atproto.repo.applyWrites#deleteResult', 1955 + }); 1806 1956 } else { 1807 - return errorResponse('InvalidRequest', `Unknown write operation type: ${type}`, 400) 1957 + return errorResponse( 1958 + 'InvalidRequest', 1959 + `Unknown write operation type: ${type}`, 1960 + 400, 1961 + ); 1808 1962 } 1809 1963 } 1810 1964 // Return commit info 1811 - const head = await this.state.storage.get('head') 1812 - const rev = await this.state.storage.get('rev') 1813 - return Response.json({ commit: { cid: head, rev }, results }) 1965 + const head = await this.state.storage.get('head'); 1966 + const rev = await this.state.storage.get('rev'); 1967 + return Response.json({ commit: { cid: head, rev }, results }); 1814 1968 } catch (err) { 1815 - return errorResponse('InternalError', err.message, 500) 1969 + return errorResponse('InternalError', err.message, 500); 1816 1970 } 1817 1971 } 1818 1972 1819 1973 async handleGetRecord(url) { 1820 - const collection = url.searchParams.get('collection') 1821 - const rkey = url.searchParams.get('rkey') 1974 + const collection = url.searchParams.get('collection'); 1975 + const rkey = url.searchParams.get('rkey'); 1822 1976 if (!collection || !rkey) { 1823 - return errorResponse('InvalidRequest', 'missing collection or rkey', 400) 1977 + return errorResponse('InvalidRequest', 'missing collection or rkey', 400); 1824 1978 } 1825 - const did = await this.getDid() 1826 - const uri = `at://${did}/${collection}/${rkey}` 1827 - const rows = this.sql.exec( 1828 - `SELECT cid, value FROM records WHERE uri = ?`, uri 1829 - ).toArray() 1979 + const did = await this.getDid(); 1980 + const uri = `at://${did}/${collection}/${rkey}`; 1981 + const rows = this.sql 1982 + .exec(`SELECT cid, value FROM records WHERE uri = ?`, uri) 1983 + .toArray(); 1830 1984 if (rows.length === 0) { 1831 - return errorResponse('RecordNotFound', 'record not found', 404) 1985 + return errorResponse('RecordNotFound', 'record not found', 404); 1832 1986 } 1833 - const row = rows[0] 1834 - const value = cborDecode(new Uint8Array(row.value)) 1835 - return Response.json({ uri, cid: row.cid, value }) 1987 + const row = rows[0]; 1988 + const value = cborDecode(new Uint8Array(row.value)); 1989 + return Response.json({ uri, cid: row.cid, value }); 1836 1990 } 1837 1991 1838 1992 async handleDescribeRepo() { 1839 - const did = await this.getDid() 1993 + const did = await this.getDid(); 1840 1994 if (!did) { 1841 - return errorResponse('RepoNotFound', 'repo not found', 404) 1995 + return errorResponse('RepoNotFound', 'repo not found', 404); 1842 1996 } 1843 - const handle = await this.state.storage.get('handle') 1997 + const handle = await this.state.storage.get('handle'); 1844 1998 // Get unique collections 1845 - const collections = this.sql.exec( 1846 - `SELECT DISTINCT collection FROM records` 1847 - ).toArray().map(r => r.collection) 1999 + const collections = this.sql 2000 + .exec(`SELECT DISTINCT collection FROM records`) 2001 + .toArray() 2002 + .map((r) => r.collection); 1848 2003 1849 2004 return Response.json({ 1850 2005 handle: handle || did, 1851 2006 did, 1852 2007 didDoc: {}, 1853 2008 collections, 1854 - handleIsCorrect: !!handle 1855 - }) 2009 + handleIsCorrect: !!handle, 2010 + }); 1856 2011 } 1857 2012 1858 2013 async handleListRecords(url) { 1859 - const collection = url.searchParams.get('collection') 2014 + const collection = url.searchParams.get('collection'); 1860 2015 if (!collection) { 1861 - return errorResponse('InvalidRequest', 'missing collection', 400) 2016 + return errorResponse('InvalidRequest', 'missing collection', 400); 1862 2017 } 1863 - const limit = Math.min(parseInt(url.searchParams.get('limit') || '50'), 100) 1864 - const reverse = url.searchParams.get('reverse') === 'true' 1865 - const cursor = url.searchParams.get('cursor') 2018 + const limit = Math.min( 2019 + parseInt(url.searchParams.get('limit') || '50', 10), 2020 + 100, 2021 + ); 2022 + const reverse = url.searchParams.get('reverse') === 'true'; 2023 + const _cursor = url.searchParams.get('cursor'); 1866 2024 1867 - const did = await this.getDid() 1868 - let query = `SELECT uri, cid, value FROM records WHERE collection = ? ORDER BY rkey ${reverse ? 'DESC' : 'ASC'} LIMIT ?` 1869 - const params = [collection, limit + 1] 2025 + const _did = await this.getDid(); 2026 + const query = `SELECT uri, cid, value FROM records WHERE collection = ? ORDER BY rkey ${reverse ? 'DESC' : 'ASC'} LIMIT ?`; 2027 + const params = [collection, limit + 1]; 1870 2028 1871 - const rows = this.sql.exec(query, ...params).toArray() 1872 - const hasMore = rows.length > limit 1873 - const records = rows.slice(0, limit).map(r => ({ 2029 + const rows = this.sql.exec(query, ...params).toArray(); 2030 + const hasMore = rows.length > limit; 2031 + const records = rows.slice(0, limit).map((r) => ({ 1874 2032 uri: r.uri, 1875 2033 cid: r.cid, 1876 - value: cborDecode(new Uint8Array(r.value)) 1877 - })) 2034 + value: cborDecode(new Uint8Array(r.value)), 2035 + })); 1878 2036 1879 2037 return Response.json({ 1880 2038 records, 1881 - cursor: hasMore ? records[records.length - 1]?.uri : undefined 1882 - }) 2039 + cursor: hasMore ? records[records.length - 1]?.uri : undefined, 2040 + }); 1883 2041 } 1884 2042 1885 2043 handleGetLatestCommit() { 1886 - const commits = this.sql.exec( 1887 - `SELECT cid, rev FROM commits ORDER BY seq DESC LIMIT 1` 1888 - ).toArray() 2044 + const commits = this.sql 2045 + .exec(`SELECT cid, rev FROM commits ORDER BY seq DESC LIMIT 1`) 2046 + .toArray(); 1889 2047 if (commits.length === 0) { 1890 - return errorResponse('RepoNotFound', 'repo not found', 404) 2048 + return errorResponse('RepoNotFound', 'repo not found', 404); 1891 2049 } 1892 - return Response.json({ cid: commits[0].cid, rev: commits[0].rev }) 2050 + return Response.json({ cid: commits[0].cid, rev: commits[0].rev }); 1893 2051 } 1894 2052 1895 2053 async handleGetRepoStatus() { 1896 - const did = await this.getDid() 1897 - const commits = this.sql.exec( 1898 - `SELECT cid, rev FROM commits ORDER BY seq DESC LIMIT 1` 1899 - ).toArray() 2054 + const did = await this.getDid(); 2055 + const commits = this.sql 2056 + .exec(`SELECT cid, rev FROM commits ORDER BY seq DESC LIMIT 1`) 2057 + .toArray(); 1900 2058 if (commits.length === 0 || !did) { 1901 - return errorResponse('RepoNotFound', 'repo not found', 404) 2059 + return errorResponse('RepoNotFound', 'repo not found', 404); 1902 2060 } 1903 - return Response.json({ did, active: true, status: 'active', rev: commits[0].rev }) 2061 + return Response.json({ 2062 + did, 2063 + active: true, 2064 + status: 'active', 2065 + rev: commits[0].rev, 2066 + }); 1904 2067 } 1905 2068 1906 2069 handleGetRepo() { 1907 - const commits = this.sql.exec( 1908 - `SELECT cid FROM commits ORDER BY seq DESC LIMIT 1` 1909 - ).toArray() 2070 + const commits = this.sql 2071 + .exec(`SELECT cid FROM commits ORDER BY seq DESC LIMIT 1`) 2072 + .toArray(); 1910 2073 if (commits.length === 0) { 1911 - return errorResponse('RepoNotFound', 'repo not found', 404) 2074 + return errorResponse('RepoNotFound', 'repo not found', 404); 1912 2075 } 1913 2076 1914 2077 // Only include blocks reachable from the current commit 1915 - const commitCid = commits[0].cid 1916 - const neededCids = new Set() 2078 + const commitCid = commits[0].cid; 2079 + const neededCids = new Set(); 1917 2080 1918 2081 // Helper to get block data 1919 2082 const getBlock = (cid) => { 1920 - const rows = this.sql.exec(`SELECT data FROM blocks WHERE cid = ?`, cid).toArray() 1921 - return rows.length > 0 ? new Uint8Array(rows[0].data) : null 1922 - } 2083 + const rows = this.sql 2084 + .exec(`SELECT data FROM blocks WHERE cid = ?`, cid) 2085 + .toArray(); 2086 + return rows.length > 0 ? new Uint8Array(rows[0].data) : null; 2087 + }; 1923 2088 1924 2089 // Collect all reachable blocks starting from commit 1925 2090 const collectBlocks = (cid) => { 1926 - if (neededCids.has(cid)) return 1927 - neededCids.add(cid) 2091 + if (neededCids.has(cid)) return; 2092 + neededCids.add(cid); 1928 2093 1929 - const data = getBlock(cid) 1930 - if (!data) return 2094 + const data = getBlock(cid); 2095 + if (!data) return; 1931 2096 1932 2097 // Decode CBOR to find CID references 1933 2098 try { 1934 - const decoded = cborDecode(data) 2099 + const decoded = cborDecode(data); 1935 2100 if (decoded && typeof decoded === 'object') { 1936 2101 // Commit object - follow 'data' (MST root) 1937 2102 if (decoded.data instanceof Uint8Array) { 1938 - collectBlocks(cidToString(decoded.data)) 2103 + collectBlocks(cidToString(decoded.data)); 1939 2104 } 1940 2105 // MST node - follow 'l' and entries' 'v' and 't' 1941 2106 if (decoded.l instanceof Uint8Array) { 1942 - collectBlocks(cidToString(decoded.l)) 2107 + collectBlocks(cidToString(decoded.l)); 1943 2108 } 1944 2109 if (Array.isArray(decoded.e)) { 1945 2110 for (const entry of decoded.e) { 1946 2111 if (entry.v instanceof Uint8Array) { 1947 - collectBlocks(cidToString(entry.v)) 2112 + collectBlocks(cidToString(entry.v)); 1948 2113 } 1949 2114 if (entry.t instanceof Uint8Array) { 1950 - collectBlocks(cidToString(entry.t)) 2115 + collectBlocks(cidToString(entry.t)); 1951 2116 } 1952 2117 } 1953 2118 } 1954 2119 } 1955 - } catch (e) { 2120 + } catch (_e) { 1956 2121 // Not a structured block, that's fine 1957 2122 } 1958 - } 2123 + }; 1959 2124 1960 - collectBlocks(commitCid) 2125 + collectBlocks(commitCid); 1961 2126 1962 2127 // Build CAR with only needed blocks 1963 - const blocksForCar = [] 2128 + const blocksForCar = []; 1964 2129 for (const cid of neededCids) { 1965 - const data = getBlock(cid) 2130 + const data = getBlock(cid); 1966 2131 if (data) { 1967 - blocksForCar.push({ cid, data }) 2132 + blocksForCar.push({ cid, data }); 1968 2133 } 1969 2134 } 1970 2135 1971 - const car = buildCarFile(commitCid, blocksForCar) 2136 + const car = buildCarFile(commitCid, blocksForCar); 1972 2137 return new Response(car, { 1973 - headers: { 'content-type': 'application/vnd.ipld.car' } 1974 - }) 2138 + headers: { 'content-type': 'application/vnd.ipld.car' }, 2139 + }); 1975 2140 } 1976 2141 1977 2142 async handleSyncGetRecord(url) { 1978 - const collection = url.searchParams.get('collection') 1979 - const rkey = url.searchParams.get('rkey') 2143 + const collection = url.searchParams.get('collection'); 2144 + const rkey = url.searchParams.get('rkey'); 1980 2145 if (!collection || !rkey) { 1981 - return errorResponse('InvalidRequest', 'missing collection or rkey', 400) 2146 + return errorResponse('InvalidRequest', 'missing collection or rkey', 400); 1982 2147 } 1983 - const did = await this.getDid() 1984 - const uri = `at://${did}/${collection}/${rkey}` 1985 - const rows = this.sql.exec( 1986 - `SELECT cid FROM records WHERE uri = ?`, uri 1987 - ).toArray() 2148 + const did = await this.getDid(); 2149 + const uri = `at://${did}/${collection}/${rkey}`; 2150 + const rows = this.sql 2151 + .exec(`SELECT cid FROM records WHERE uri = ?`, uri) 2152 + .toArray(); 1988 2153 if (rows.length === 0) { 1989 - return errorResponse('RecordNotFound', 'record not found', 404) 2154 + return errorResponse('RecordNotFound', 'record not found', 404); 1990 2155 } 1991 - const recordCid = rows[0].cid 2156 + const recordCid = rows[0].cid; 1992 2157 1993 2158 // Get latest commit 1994 - const commits = this.sql.exec( 1995 - `SELECT cid FROM commits ORDER BY seq DESC LIMIT 1` 1996 - ).toArray() 2159 + const commits = this.sql 2160 + .exec(`SELECT cid FROM commits ORDER BY seq DESC LIMIT 1`) 2161 + .toArray(); 1997 2162 if (commits.length === 0) { 1998 - return errorResponse('RepoNotFound', 'no commits', 404) 2163 + return errorResponse('RepoNotFound', 'no commits', 404); 1999 2164 } 2000 - const commitCid = commits[0].cid 2165 + const commitCid = commits[0].cid; 2001 2166 2002 2167 // Build proof chain: commit -> MST path -> record 2003 2168 // Include commit block, all MST nodes on path to record, and record block 2004 - const blocks = [] 2005 - const included = new Set() 2169 + const blocks = []; 2170 + const included = new Set(); 2006 2171 2007 2172 const addBlock = (cidStr) => { 2008 - if (included.has(cidStr)) return 2009 - included.add(cidStr) 2010 - const blockRows = this.sql.exec( 2011 - `SELECT data FROM blocks WHERE cid = ?`, cidStr 2012 - ).toArray() 2173 + if (included.has(cidStr)) return; 2174 + included.add(cidStr); 2175 + const blockRows = this.sql 2176 + .exec(`SELECT data FROM blocks WHERE cid = ?`, cidStr) 2177 + .toArray(); 2013 2178 if (blockRows.length > 0) { 2014 - blocks.push({ cid: cidStr, data: new Uint8Array(blockRows[0].data) }) 2179 + blocks.push({ cid: cidStr, data: new Uint8Array(blockRows[0].data) }); 2015 2180 } 2016 - } 2181 + }; 2017 2182 2018 2183 // Add commit block 2019 - addBlock(commitCid) 2184 + addBlock(commitCid); 2020 2185 2021 2186 // Get commit to find data root 2022 - const commitRows = this.sql.exec( 2023 - `SELECT data FROM blocks WHERE cid = ?`, commitCid 2024 - ).toArray() 2187 + const commitRows = this.sql 2188 + .exec(`SELECT data FROM blocks WHERE cid = ?`, commitCid) 2189 + .toArray(); 2025 2190 if (commitRows.length > 0) { 2026 - const commit = cborDecode(new Uint8Array(commitRows[0].data)) 2191 + const commit = cborDecode(new Uint8Array(commitRows[0].data)); 2027 2192 if (commit.data) { 2028 - const dataRootCid = cidToString(commit.data) 2193 + const dataRootCid = cidToString(commit.data); 2029 2194 // Collect MST path blocks (this includes all MST nodes) 2030 - const mstBlocks = this.collectMstBlocks(dataRootCid) 2195 + const mstBlocks = this.collectMstBlocks(dataRootCid); 2031 2196 for (const block of mstBlocks) { 2032 - addBlock(block.cid) 2197 + addBlock(block.cid); 2033 2198 } 2034 2199 } 2035 2200 } 2036 2201 2037 2202 // Add record block 2038 - addBlock(recordCid) 2203 + addBlock(recordCid); 2039 2204 2040 - const car = buildCarFile(commitCid, blocks) 2205 + const car = buildCarFile(commitCid, blocks); 2041 2206 return new Response(car, { 2042 - headers: { 'content-type': 'application/vnd.ipld.car' } 2043 - }) 2207 + headers: { 'content-type': 'application/vnd.ipld.car' }, 2208 + }); 2044 2209 } 2045 2210 2046 2211 handleSubscribeRepos(request, url) { 2047 - const upgradeHeader = request.headers.get('Upgrade') 2212 + const upgradeHeader = request.headers.get('Upgrade'); 2048 2213 if (upgradeHeader !== 'websocket') { 2049 - return new Response('expected websocket', { status: 426 }) 2214 + return new Response('expected websocket', { status: 426 }); 2050 2215 } 2051 - const { 0: client, 1: server } = new WebSocketPair() 2052 - this.state.acceptWebSocket(server) 2053 - const cursor = url.searchParams.get('cursor') 2216 + const { 0: client, 1: server } = new WebSocketPair(); 2217 + this.state.acceptWebSocket(server); 2218 + const cursor = url.searchParams.get('cursor'); 2054 2219 if (cursor) { 2055 - const events = this.sql.exec( 2056 - `SELECT * FROM seq_events WHERE seq > ? ORDER BY seq`, 2057 - parseInt(cursor) 2058 - ).toArray() 2220 + const events = this.sql 2221 + .exec( 2222 + `SELECT * FROM seq_events WHERE seq > ? ORDER BY seq`, 2223 + parseInt(cursor, 10), 2224 + ) 2225 + .toArray(); 2059 2226 for (const evt of events) { 2060 - server.send(this.formatEvent(evt)) 2227 + server.send(this.formatEvent(evt)); 2061 2228 } 2062 2229 } 2063 - return new Response(null, { status: 101, webSocket: client }) 2230 + return new Response(null, { status: 101, webSocket: client }); 2064 2231 } 2065 2232 2066 2233 async fetch(request) { 2067 - const url = new URL(request.url) 2068 - const route = pdsRoutes[url.pathname] 2234 + const url = new URL(request.url); 2235 + const route = pdsRoutes[url.pathname]; 2069 2236 2070 2237 // Check for local route first 2071 2238 if (route) { 2072 2239 if (route.method && request.method !== route.method) { 2073 - return errorResponse('MethodNotAllowed', 'method not allowed', 405) 2240 + return errorResponse('MethodNotAllowed', 'method not allowed', 405); 2074 2241 } 2075 - return route.handler(this, request, url) 2242 + return route.handler(this, request, url); 2076 2243 } 2077 2244 2078 2245 // Handle app.bsky.* proxy requests (only if no local route) 2079 2246 if (url.pathname.startsWith('/xrpc/app.bsky.')) { 2080 - const userDid = request.headers.get('x-authed-did') 2247 + const userDid = request.headers.get('x-authed-did'); 2081 2248 if (!userDid) { 2082 - return errorResponse('Unauthorized', 'Missing auth context', 401) 2249 + return errorResponse('Unauthorized', 'Missing auth context', 401); 2083 2250 } 2084 - return this.handleAppViewProxy(request, userDid) 2251 + return this.handleAppViewProxy(request, userDid); 2085 2252 } 2086 2253 2087 - return errorResponse('NotFound', 'not found', 404) 2254 + return errorResponse('NotFound', 'not found', 404); 2088 2255 } 2089 2256 } 2090 2257 2091 2258 const corsHeaders = { 2092 2259 'Access-Control-Allow-Origin': '*', 2093 2260 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 2094 - 'Access-Control-Allow-Headers': 'Content-Type, Authorization, atproto-accept-labelers, atproto-proxy, x-bsky-topics', 2095 - } 2261 + 'Access-Control-Allow-Headers': 2262 + 'Content-Type, Authorization, atproto-accept-labelers, atproto-proxy, x-bsky-topics', 2263 + }; 2096 2264 2097 2265 function addCorsHeaders(response) { 2098 - const newHeaders = new Headers(response.headers) 2266 + const newHeaders = new Headers(response.headers); 2099 2267 for (const [key, value] of Object.entries(corsHeaders)) { 2100 - newHeaders.set(key, value) 2268 + newHeaders.set(key, value); 2101 2269 } 2102 2270 return new Response(response.body, { 2103 2271 status: response.status, 2104 2272 statusText: response.statusText, 2105 - headers: newHeaders 2106 - }) 2273 + headers: newHeaders, 2274 + }); 2107 2275 } 2108 2276 2109 2277 export default { 2110 2278 async fetch(request, env) { 2111 2279 // Handle CORS preflight 2112 2280 if (request.method === 'OPTIONS') { 2113 - return new Response(null, { headers: corsHeaders }) 2281 + return new Response(null, { headers: corsHeaders }); 2114 2282 } 2115 2283 2116 - const response = await handleRequest(request, env) 2284 + const response = await handleRequest(request, env); 2117 2285 // Don't wrap WebSocket upgrades - they need the webSocket property preserved 2118 2286 if (response.status === 101) { 2119 - return response 2287 + return response; 2120 2288 } 2121 - return addCorsHeaders(response) 2122 - } 2123 - } 2289 + return addCorsHeaders(response); 2290 + }, 2291 + }; 2124 2292 2125 2293 // Extract subdomain from hostname (e.g., "alice" from "alice.foo.workers.dev") 2126 2294 function getSubdomain(hostname) { 2127 - const parts = hostname.split('.') 2295 + const parts = hostname.split('.'); 2128 2296 // workers.dev domains: [subdomain?].[worker-name].[account].workers.dev 2129 2297 // If more than 4 parts, first part(s) are user subdomain 2130 2298 if (parts.length > 4 && parts.slice(-2).join('.') === 'workers.dev') { 2131 - return parts.slice(0, -4).join('.') 2299 + return parts.slice(0, -4).join('.'); 2132 2300 } 2133 2301 // Custom domains: check if there's a subdomain before the base 2134 2302 // For now, assume no subdomain on custom domains 2135 - return null 2303 + return null; 2136 2304 } 2137 2305 2138 2306 /** ··· 2142 2310 * @returns {Promise<{did: string} | {error: Response}>} DID or error response 2143 2311 */ 2144 2312 async function requireAuth(request, env) { 2145 - const authHeader = request.headers.get('Authorization') 2313 + const authHeader = request.headers.get('Authorization'); 2146 2314 if (!authHeader || !authHeader.startsWith('Bearer ')) { 2147 2315 return { 2148 - error: Response.json({ 2149 - error: 'AuthenticationRequired', 2150 - message: 'Authentication required' 2151 - }, { status: 401 }) 2152 - } 2316 + error: Response.json( 2317 + { 2318 + error: 'AuthenticationRequired', 2319 + message: 'Authentication required', 2320 + }, 2321 + { status: 401 }, 2322 + ), 2323 + }; 2153 2324 } 2154 2325 2155 - const token = authHeader.slice(7) 2156 - const jwtSecret = env?.JWT_SECRET 2326 + const token = authHeader.slice(7); 2327 + const jwtSecret = env?.JWT_SECRET; 2157 2328 if (!jwtSecret) { 2158 2329 return { 2159 - error: Response.json({ 2160 - error: 'InternalServerError', 2161 - message: 'Server not configured for authentication' 2162 - }, { status: 500 }) 2163 - } 2330 + error: Response.json( 2331 + { 2332 + error: 'InternalServerError', 2333 + message: 'Server not configured for authentication', 2334 + }, 2335 + { status: 500 }, 2336 + ), 2337 + }; 2164 2338 } 2165 2339 2166 2340 try { 2167 - const payload = await verifyAccessJwt(token, jwtSecret) 2168 - return { did: payload.sub } 2341 + const payload = await verifyAccessJwt(token, jwtSecret); 2342 + return { did: payload.sub }; 2169 2343 } catch (err) { 2170 2344 return { 2171 - error: Response.json({ 2172 - error: 'InvalidToken', 2173 - message: err.message 2174 - }, { status: 401 }) 2175 - } 2345 + error: Response.json( 2346 + { 2347 + error: 'InvalidToken', 2348 + message: err.message, 2349 + }, 2350 + { status: 401 }, 2351 + ), 2352 + }; 2176 2353 } 2177 2354 } 2178 2355 2179 2356 async function handleAuthenticatedRepoWrite(request, env) { 2180 - const auth = await requireAuth(request, env) 2181 - if (auth.error) return auth.error 2357 + const auth = await requireAuth(request, env); 2358 + if (auth.error) return auth.error; 2182 2359 2183 - const body = await request.json() 2184 - const repo = body.repo 2360 + const body = await request.json(); 2361 + const repo = body.repo; 2185 2362 if (!repo) { 2186 - return errorResponse('InvalidRequest', 'missing repo param', 400) 2363 + return errorResponse('InvalidRequest', 'missing repo param', 400); 2187 2364 } 2188 2365 2189 2366 if (auth.did !== repo) { 2190 - return errorResponse('Forbidden', 'Cannot modify another user\'s repo', 403) 2367 + return errorResponse('Forbidden', "Cannot modify another user's repo", 403); 2191 2368 } 2192 2369 2193 - const id = env.PDS.idFromName(repo) 2194 - const pds = env.PDS.get(id) 2195 - const response = await pds.fetch(new Request(request.url, { 2196 - method: 'POST', 2197 - headers: request.headers, 2198 - body: JSON.stringify(body) 2199 - })) 2370 + const id = env.PDS.idFromName(repo); 2371 + const pds = env.PDS.get(id); 2372 + const response = await pds.fetch( 2373 + new Request(request.url, { 2374 + method: 'POST', 2375 + headers: request.headers, 2376 + body: JSON.stringify(body), 2377 + }), 2378 + ); 2200 2379 2201 2380 // Notify relay of updates on successful writes 2202 2381 if (response.ok) { 2203 - const url = new URL(request.url) 2204 - notifyCrawlers(env, url.hostname) 2382 + const url = new URL(request.url); 2383 + notifyCrawlers(env, url.hostname); 2205 2384 } 2206 2385 2207 - return response 2386 + return response; 2208 2387 } 2209 2388 2210 2389 async function handleRequest(request, env) { 2211 - const url = new URL(request.url) 2212 - const subdomain = getSubdomain(url.hostname) 2390 + const url = new URL(request.url); 2391 + const subdomain = getSubdomain(url.hostname); 2213 2392 2214 2393 // Handle resolution via subdomain or bare domain 2215 2394 if (url.pathname === '/.well-known/atproto-did') { 2216 2395 // Look up handle -> DID in default DO 2217 2396 // Use subdomain if present, otherwise try bare hostname as handle 2218 - const handleToResolve = subdomain || url.hostname 2219 - const defaultId = env.PDS.idFromName('default') 2220 - const defaultPds = env.PDS.get(defaultId) 2397 + const handleToResolve = subdomain || url.hostname; 2398 + const defaultId = env.PDS.idFromName('default'); 2399 + const defaultPds = env.PDS.get(defaultId); 2221 2400 const resolveRes = await defaultPds.fetch( 2222 - new Request(`http://internal/resolve-handle?handle=${encodeURIComponent(handleToResolve)}`) 2223 - ) 2401 + new Request( 2402 + `http://internal/resolve-handle?handle=${encodeURIComponent(handleToResolve)}`, 2403 + ), 2404 + ); 2224 2405 if (!resolveRes.ok) { 2225 - return new Response('Handle not found', { status: 404 }) 2406 + return new Response('Handle not found', { status: 404 }); 2226 2407 } 2227 - const { did } = await resolveRes.json() 2228 - return new Response(did, { headers: { 'Content-Type': 'text/plain' } }) 2408 + const { did } = await resolveRes.json(); 2409 + return new Response(did, { headers: { 'Content-Type': 'text/plain' } }); 2229 2410 } 2230 2411 2231 2412 // describeServer - works on bare domain 2232 2413 if (url.pathname === '/xrpc/com.atproto.server.describeServer') { 2233 - const defaultId = env.PDS.idFromName('default') 2234 - const defaultPds = env.PDS.get(defaultId) 2414 + const defaultId = env.PDS.idFromName('default'); 2415 + const defaultPds = env.PDS.get(defaultId); 2235 2416 const newReq = new Request(request.url, { 2236 2417 method: request.method, 2237 - headers: { ...Object.fromEntries(request.headers), 'x-hostname': url.hostname } 2238 - }) 2239 - return defaultPds.fetch(newReq) 2418 + headers: { 2419 + ...Object.fromEntries(request.headers), 2420 + 'x-hostname': url.hostname, 2421 + }, 2422 + }); 2423 + return defaultPds.fetch(newReq); 2240 2424 } 2241 2425 2242 2426 // createSession - handle on default DO (has handleMap for identifier resolution) 2243 2427 if (url.pathname === '/xrpc/com.atproto.server.createSession') { 2244 - const defaultId = env.PDS.idFromName('default') 2245 - const defaultPds = env.PDS.get(defaultId) 2246 - return defaultPds.fetch(request) 2428 + const defaultId = env.PDS.idFromName('default'); 2429 + const defaultPds = env.PDS.get(defaultId); 2430 + return defaultPds.fetch(request); 2247 2431 } 2248 2432 2249 2433 // getSession - route to default DO 2250 2434 if (url.pathname === '/xrpc/com.atproto.server.getSession') { 2251 - const defaultId = env.PDS.idFromName('default') 2252 - const defaultPds = env.PDS.get(defaultId) 2253 - return defaultPds.fetch(request) 2435 + const defaultId = env.PDS.idFromName('default'); 2436 + const defaultPds = env.PDS.get(defaultId); 2437 + return defaultPds.fetch(request); 2254 2438 } 2255 2439 2256 2440 // Proxy app.bsky.* endpoints to Bluesky AppView 2257 2441 if (url.pathname.startsWith('/xrpc/app.bsky.')) { 2258 2442 // Authenticate the user first 2259 - const auth = await requireAuth(request, env) 2260 - if (auth.error) return auth.error 2443 + const auth = await requireAuth(request, env); 2444 + if (auth.error) return auth.error; 2261 2445 2262 2446 // Route to the user's DO instance to create service auth and proxy 2263 - const id = env.PDS.idFromName(auth.did) 2264 - const pds = env.PDS.get(id) 2265 - return pds.fetch(new Request(request.url, { 2266 - method: request.method, 2267 - headers: { 2268 - ...Object.fromEntries(request.headers), 2269 - 'x-authed-did': auth.did // Pass the authenticated DID 2270 - }, 2271 - body: request.method !== 'GET' && request.method !== 'HEAD' ? request.body : undefined, 2272 - })) 2447 + const id = env.PDS.idFromName(auth.did); 2448 + const pds = env.PDS.get(id); 2449 + return pds.fetch( 2450 + new Request(request.url, { 2451 + method: request.method, 2452 + headers: { 2453 + ...Object.fromEntries(request.headers), 2454 + 'x-authed-did': auth.did, // Pass the authenticated DID 2455 + }, 2456 + body: 2457 + request.method !== 'GET' && request.method !== 'HEAD' 2458 + ? request.body 2459 + : undefined, 2460 + }), 2461 + ); 2273 2462 } 2274 2463 2275 2464 // Handle registration routes - go to default DO 2276 - if (url.pathname === '/register-handle' || url.pathname === '/resolve-handle') { 2277 - const defaultId = env.PDS.idFromName('default') 2278 - const defaultPds = env.PDS.get(defaultId) 2279 - return defaultPds.fetch(request) 2465 + if ( 2466 + url.pathname === '/register-handle' || 2467 + url.pathname === '/resolve-handle' 2468 + ) { 2469 + const defaultId = env.PDS.idFromName('default'); 2470 + const defaultPds = env.PDS.get(defaultId); 2471 + return defaultPds.fetch(request); 2280 2472 } 2281 2473 2282 2474 // resolveHandle XRPC endpoint 2283 2475 if (url.pathname === '/xrpc/com.atproto.identity.resolveHandle') { 2284 - const handle = url.searchParams.get('handle') 2476 + const handle = url.searchParams.get('handle'); 2285 2477 if (!handle) { 2286 - return errorResponse('InvalidRequest', 'missing handle param', 400) 2478 + return errorResponse('InvalidRequest', 'missing handle param', 400); 2287 2479 } 2288 - const defaultId = env.PDS.idFromName('default') 2289 - const defaultPds = env.PDS.get(defaultId) 2480 + const defaultId = env.PDS.idFromName('default'); 2481 + const defaultPds = env.PDS.get(defaultId); 2290 2482 const resolveRes = await defaultPds.fetch( 2291 - new Request(`http://internal/resolve-handle?handle=${encodeURIComponent(handle)}`) 2292 - ) 2483 + new Request( 2484 + `http://internal/resolve-handle?handle=${encodeURIComponent(handle)}`, 2485 + ), 2486 + ); 2293 2487 if (!resolveRes.ok) { 2294 - return errorResponse('InvalidRequest', 'Unable to resolve handle', 400) 2488 + return errorResponse('InvalidRequest', 'Unable to resolve handle', 400); 2295 2489 } 2296 - const { did } = await resolveRes.json() 2297 - return Response.json({ did }) 2490 + const { did } = await resolveRes.json(); 2491 + return Response.json({ did }); 2298 2492 } 2299 2493 2300 2494 // subscribeRepos WebSocket - route to default instance for firehose 2301 2495 if (url.pathname === '/xrpc/com.atproto.sync.subscribeRepos') { 2302 - const defaultId = env.PDS.idFromName('default') 2303 - const defaultPds = env.PDS.get(defaultId) 2304 - return defaultPds.fetch(request) 2496 + const defaultId = env.PDS.idFromName('default'); 2497 + const defaultPds = env.PDS.get(defaultId); 2498 + return defaultPds.fetch(request); 2305 2499 } 2306 2500 2307 2501 // listRepos needs to aggregate from all registered DIDs 2308 2502 if (url.pathname === '/xrpc/com.atproto.sync.listRepos') { 2309 - const defaultId = env.PDS.idFromName('default') 2310 - const defaultPds = env.PDS.get(defaultId) 2311 - const regRes = await defaultPds.fetch(new Request('http://internal/get-registered-dids')) 2312 - const { dids } = await regRes.json() 2503 + const defaultId = env.PDS.idFromName('default'); 2504 + const defaultPds = env.PDS.get(defaultId); 2505 + const regRes = await defaultPds.fetch( 2506 + new Request('http://internal/get-registered-dids'), 2507 + ); 2508 + const { dids } = await regRes.json(); 2313 2509 2314 - const repos = [] 2510 + const repos = []; 2315 2511 for (const did of dids) { 2316 - const id = env.PDS.idFromName(did) 2317 - const pds = env.PDS.get(id) 2318 - const infoRes = await pds.fetch(new Request('http://internal/repo-info')) 2319 - const info = await infoRes.json() 2512 + const id = env.PDS.idFromName(did); 2513 + const pds = env.PDS.get(id); 2514 + const infoRes = await pds.fetch(new Request('http://internal/repo-info')); 2515 + const info = await infoRes.json(); 2320 2516 if (info.head) { 2321 - repos.push({ did, head: info.head, rev: info.rev, active: true }) 2517 + repos.push({ did, head: info.head, rev: info.rev, active: true }); 2322 2518 } 2323 2519 } 2324 - return Response.json({ repos, cursor: undefined }) 2520 + return Response.json({ repos, cursor: undefined }); 2325 2521 } 2326 2522 2327 2523 // Repo endpoints use ?repo= param instead of ?did= 2328 - if (url.pathname === '/xrpc/com.atproto.repo.describeRepo' || 2329 - url.pathname === '/xrpc/com.atproto.repo.listRecords' || 2330 - url.pathname === '/xrpc/com.atproto.repo.getRecord') { 2331 - const repo = url.searchParams.get('repo') 2524 + if ( 2525 + url.pathname === '/xrpc/com.atproto.repo.describeRepo' || 2526 + url.pathname === '/xrpc/com.atproto.repo.listRecords' || 2527 + url.pathname === '/xrpc/com.atproto.repo.getRecord' 2528 + ) { 2529 + const repo = url.searchParams.get('repo'); 2332 2530 if (!repo) { 2333 - return errorResponse('InvalidRequest', 'missing repo param', 400) 2531 + return errorResponse('InvalidRequest', 'missing repo param', 400); 2334 2532 } 2335 - const id = env.PDS.idFromName(repo) 2336 - const pds = env.PDS.get(id) 2337 - return pds.fetch(request) 2533 + const id = env.PDS.idFromName(repo); 2534 + const pds = env.PDS.get(id); 2535 + return pds.fetch(request); 2338 2536 } 2339 2537 2340 2538 // Sync endpoints use ?did= param 2341 - if (url.pathname === '/xrpc/com.atproto.sync.getLatestCommit' || 2342 - url.pathname === '/xrpc/com.atproto.sync.getRepoStatus' || 2343 - url.pathname === '/xrpc/com.atproto.sync.getRepo' || 2344 - url.pathname === '/xrpc/com.atproto.sync.getRecord') { 2345 - const did = url.searchParams.get('did') 2539 + if ( 2540 + url.pathname === '/xrpc/com.atproto.sync.getLatestCommit' || 2541 + url.pathname === '/xrpc/com.atproto.sync.getRepoStatus' || 2542 + url.pathname === '/xrpc/com.atproto.sync.getRepo' || 2543 + url.pathname === '/xrpc/com.atproto.sync.getRecord' 2544 + ) { 2545 + const did = url.searchParams.get('did'); 2346 2546 if (!did) { 2347 - return errorResponse('InvalidRequest', 'missing did param', 400) 2547 + return errorResponse('InvalidRequest', 'missing did param', 400); 2348 2548 } 2349 - const id = env.PDS.idFromName(did) 2350 - const pds = env.PDS.get(id) 2351 - return pds.fetch(request) 2549 + const id = env.PDS.idFromName(did); 2550 + const pds = env.PDS.get(id); 2551 + return pds.fetch(request); 2352 2552 } 2353 2553 2354 2554 // Authenticated repo write endpoints ··· 2356 2556 '/xrpc/com.atproto.repo.createRecord', 2357 2557 '/xrpc/com.atproto.repo.deleteRecord', 2358 2558 '/xrpc/com.atproto.repo.putRecord', 2359 - '/xrpc/com.atproto.repo.applyWrites' 2360 - ] 2559 + '/xrpc/com.atproto.repo.applyWrites', 2560 + ]; 2361 2561 if (repoWriteEndpoints.includes(url.pathname)) { 2362 - return handleAuthenticatedRepoWrite(request, env) 2562 + return handleAuthenticatedRepoWrite(request, env); 2363 2563 } 2364 2564 2365 2565 // Health check endpoint 2366 2566 if (url.pathname === '/xrpc/_health') { 2367 - return Response.json({ version: '0.1.0' }) 2567 + return Response.json({ version: '0.1.0' }); 2368 2568 } 2369 2569 2370 2570 // Root path - ASCII art ··· 2378 2578 ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚════╝ ╚══════╝ 2379 2579 2380 2580 ATProto PDS on Cloudflare Workers 2381 - ` 2382 - return new Response(ascii, { headers: { 'Content-Type': 'text/plain; charset=utf-8' } }) 2581 + `; 2582 + return new Response(ascii, { 2583 + headers: { 'Content-Type': 'text/plain; charset=utf-8' }, 2584 + }); 2383 2585 } 2384 2586 2385 2587 // On init, register this DID with the default instance (requires ?did= param, no auth yet) 2386 2588 if (url.pathname === '/init' && request.method === 'POST') { 2387 - const did = url.searchParams.get('did') 2589 + const did = url.searchParams.get('did'); 2388 2590 if (!did) { 2389 - return errorResponse('InvalidRequest', 'missing did param', 400) 2591 + return errorResponse('InvalidRequest', 'missing did param', 400); 2390 2592 } 2391 - const body = await request.json() 2593 + const body = await request.json(); 2392 2594 2393 2595 // Register with default instance for discovery 2394 - const defaultId = env.PDS.idFromName('default') 2395 - const defaultPds = env.PDS.get(defaultId) 2396 - await defaultPds.fetch(new Request('http://internal/register-did', { 2397 - method: 'POST', 2398 - body: JSON.stringify({ did }) 2399 - })) 2596 + const defaultId = env.PDS.idFromName('default'); 2597 + const defaultPds = env.PDS.get(defaultId); 2598 + await defaultPds.fetch( 2599 + new Request('http://internal/register-did', { 2600 + method: 'POST', 2601 + body: JSON.stringify({ did }), 2602 + }), 2603 + ); 2400 2604 2401 2605 // Register handle if provided 2402 2606 if (body.handle) { 2403 - await defaultPds.fetch(new Request('http://internal/register-handle', { 2404 - method: 'POST', 2405 - body: JSON.stringify({ did, handle: body.handle }) 2406 - })) 2607 + await defaultPds.fetch( 2608 + new Request('http://internal/register-handle', { 2609 + method: 'POST', 2610 + body: JSON.stringify({ did, handle: body.handle }), 2611 + }), 2612 + ); 2407 2613 } 2408 2614 2409 2615 // Forward to the actual PDS instance 2410 - const id = env.PDS.idFromName(did) 2411 - const pds = env.PDS.get(id) 2412 - return pds.fetch(new Request(request.url, { 2413 - method: 'POST', 2414 - headers: request.headers, 2415 - body: JSON.stringify(body) 2416 - })) 2616 + const id = env.PDS.idFromName(did); 2617 + const pds = env.PDS.get(id); 2618 + return pds.fetch( 2619 + new Request(request.url, { 2620 + method: 'POST', 2621 + headers: request.headers, 2622 + body: JSON.stringify(body), 2623 + }), 2624 + ); 2417 2625 } 2418 2626 2419 2627 // Unknown endpoint 2420 - return errorResponse('NotFound', 'Endpoint not found', 404) 2628 + return errorResponse('NotFound', 'Endpoint not found', 404); 2421 2629 }
+312 -292
test/pds.test.js
··· 1 - import { test, describe } from 'node:test' 2 - import assert from 'node:assert' 1 + import assert from 'node:assert'; 2 + import { describe, test } from 'node:test'; 3 3 import { 4 - cborEncode, cborDecode, createCid, cidToString, cidToBytes, base32Encode, createTid, 5 - generateKeyPair, importPrivateKey, sign, bytesToHex, hexToBytes, 6 - getKeyDepth, varint, base32Decode, buildCarFile, 7 - base64UrlEncode, base64UrlDecode, 8 - createAccessJwt, createRefreshJwt, verifyAccessJwt 9 - } from '../src/pds.js' 4 + base32Decode, 5 + base32Encode, 6 + base64UrlDecode, 7 + base64UrlEncode, 8 + buildCarFile, 9 + bytesToHex, 10 + cborDecode, 11 + cborEncode, 12 + cidToString, 13 + createAccessJwt, 14 + createCid, 15 + createRefreshJwt, 16 + createTid, 17 + generateKeyPair, 18 + getKeyDepth, 19 + hexToBytes, 20 + importPrivateKey, 21 + sign, 22 + varint, 23 + verifyAccessJwt, 24 + } from '../src/pds.js'; 10 25 11 26 describe('CBOR Encoding', () => { 12 27 test('encodes simple map', () => { 13 - const encoded = cborEncode({ hello: 'world', num: 42 }) 28 + const encoded = cborEncode({ hello: 'world', num: 42 }); 14 29 // Expected: a2 65 68 65 6c 6c 6f 65 77 6f 72 6c 64 63 6e 75 6d 18 2a 15 30 const expected = new Uint8Array([ 16 - 0xa2, 0x65, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x65, 0x77, 0x6f, 0x72, 0x6c, 0x64, 17 - 0x63, 0x6e, 0x75, 0x6d, 0x18, 0x2a 18 - ]) 19 - assert.deepStrictEqual(encoded, expected) 20 - }) 31 + 0xa2, 0x65, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x65, 0x77, 0x6f, 0x72, 0x6c, 32 + 0x64, 0x63, 0x6e, 0x75, 0x6d, 0x18, 0x2a, 33 + ]); 34 + assert.deepStrictEqual(encoded, expected); 35 + }); 21 36 22 37 test('encodes null', () => { 23 - const encoded = cborEncode(null) 24 - assert.deepStrictEqual(encoded, new Uint8Array([0xf6])) 25 - }) 38 + const encoded = cborEncode(null); 39 + assert.deepStrictEqual(encoded, new Uint8Array([0xf6])); 40 + }); 26 41 27 42 test('encodes booleans', () => { 28 - assert.deepStrictEqual(cborEncode(true), new Uint8Array([0xf5])) 29 - assert.deepStrictEqual(cborEncode(false), new Uint8Array([0xf4])) 30 - }) 43 + assert.deepStrictEqual(cborEncode(true), new Uint8Array([0xf5])); 44 + assert.deepStrictEqual(cborEncode(false), new Uint8Array([0xf4])); 45 + }); 31 46 32 47 test('encodes small integers', () => { 33 - assert.deepStrictEqual(cborEncode(0), new Uint8Array([0x00])) 34 - assert.deepStrictEqual(cborEncode(1), new Uint8Array([0x01])) 35 - assert.deepStrictEqual(cborEncode(23), new Uint8Array([0x17])) 36 - }) 48 + assert.deepStrictEqual(cborEncode(0), new Uint8Array([0x00])); 49 + assert.deepStrictEqual(cborEncode(1), new Uint8Array([0x01])); 50 + assert.deepStrictEqual(cborEncode(23), new Uint8Array([0x17])); 51 + }); 37 52 38 53 test('encodes integers >= 24', () => { 39 - assert.deepStrictEqual(cborEncode(24), new Uint8Array([0x18, 0x18])) 40 - assert.deepStrictEqual(cborEncode(255), new Uint8Array([0x18, 0xff])) 41 - }) 54 + assert.deepStrictEqual(cborEncode(24), new Uint8Array([0x18, 0x18])); 55 + assert.deepStrictEqual(cborEncode(255), new Uint8Array([0x18, 0xff])); 56 + }); 42 57 43 58 test('encodes negative integers', () => { 44 - assert.deepStrictEqual(cborEncode(-1), new Uint8Array([0x20])) 45 - assert.deepStrictEqual(cborEncode(-10), new Uint8Array([0x29])) 46 - }) 59 + assert.deepStrictEqual(cborEncode(-1), new Uint8Array([0x20])); 60 + assert.deepStrictEqual(cborEncode(-10), new Uint8Array([0x29])); 61 + }); 47 62 48 63 test('encodes strings', () => { 49 - const encoded = cborEncode('hello') 64 + const encoded = cborEncode('hello'); 50 65 // 0x65 = text string of length 5 51 - assert.deepStrictEqual(encoded, new Uint8Array([0x65, 0x68, 0x65, 0x6c, 0x6c, 0x6f])) 52 - }) 66 + assert.deepStrictEqual( 67 + encoded, 68 + new Uint8Array([0x65, 0x68, 0x65, 0x6c, 0x6c, 0x6f]), 69 + ); 70 + }); 53 71 54 72 test('encodes byte strings', () => { 55 - const bytes = new Uint8Array([1, 2, 3]) 56 - const encoded = cborEncode(bytes) 73 + const bytes = new Uint8Array([1, 2, 3]); 74 + const encoded = cborEncode(bytes); 57 75 // 0x43 = byte string of length 3 58 - assert.deepStrictEqual(encoded, new Uint8Array([0x43, 1, 2, 3])) 59 - }) 76 + assert.deepStrictEqual(encoded, new Uint8Array([0x43, 1, 2, 3])); 77 + }); 60 78 61 79 test('encodes arrays', () => { 62 - const encoded = cborEncode([1, 2, 3]) 80 + const encoded = cborEncode([1, 2, 3]); 63 81 // 0x83 = array of length 3 64 - assert.deepStrictEqual(encoded, new Uint8Array([0x83, 0x01, 0x02, 0x03])) 65 - }) 82 + assert.deepStrictEqual(encoded, new Uint8Array([0x83, 0x01, 0x02, 0x03])); 83 + }); 66 84 67 85 test('sorts map keys deterministically', () => { 68 - const encoded1 = cborEncode({ z: 1, a: 2 }) 69 - const encoded2 = cborEncode({ a: 2, z: 1 }) 70 - assert.deepStrictEqual(encoded1, encoded2) 86 + const encoded1 = cborEncode({ z: 1, a: 2 }); 87 + const encoded2 = cborEncode({ a: 2, z: 1 }); 88 + assert.deepStrictEqual(encoded1, encoded2); 71 89 // First key should be 'a' (0x61) 72 - assert.strictEqual(encoded1[1], 0x61) 73 - }) 90 + assert.strictEqual(encoded1[1], 0x61); 91 + }); 74 92 75 93 test('encodes large integers >= 2^31 without overflow', () => { 76 94 // 2^31 would overflow with bitshift operators (treated as signed 32-bit) 77 - const twoTo31 = 2147483648 78 - const encoded = cborEncode(twoTo31) 79 - const decoded = cborDecode(encoded) 80 - assert.strictEqual(decoded, twoTo31) 95 + const twoTo31 = 2147483648; 96 + const encoded = cborEncode(twoTo31); 97 + const decoded = cborDecode(encoded); 98 + assert.strictEqual(decoded, twoTo31); 81 99 82 100 // 2^32 - 1 (max unsigned 32-bit) 83 - const maxU32 = 4294967295 84 - const encoded2 = cborEncode(maxU32) 85 - const decoded2 = cborDecode(encoded2) 86 - assert.strictEqual(decoded2, maxU32) 87 - }) 101 + const maxU32 = 4294967295; 102 + const encoded2 = cborEncode(maxU32); 103 + const decoded2 = cborDecode(encoded2); 104 + assert.strictEqual(decoded2, maxU32); 105 + }); 88 106 89 107 test('encodes 2^31 with correct byte format', () => { 90 108 // 2147483648 = 0x80000000 91 109 // CBOR: major type 0 (unsigned int), additional info 26 (4-byte follows) 92 - const encoded = cborEncode(2147483648) 93 - assert.strictEqual(encoded[0], 0x1a) // type 0 | info 26 94 - assert.strictEqual(encoded[1], 0x80) 95 - assert.strictEqual(encoded[2], 0x00) 96 - assert.strictEqual(encoded[3], 0x00) 97 - assert.strictEqual(encoded[4], 0x00) 98 - }) 99 - }) 110 + const encoded = cborEncode(2147483648); 111 + assert.strictEqual(encoded[0], 0x1a); // type 0 | info 26 112 + assert.strictEqual(encoded[1], 0x80); 113 + assert.strictEqual(encoded[2], 0x00); 114 + assert.strictEqual(encoded[3], 0x00); 115 + assert.strictEqual(encoded[4], 0x00); 116 + }); 117 + }); 100 118 101 119 describe('Base32 Encoding', () => { 102 120 test('encodes bytes to base32lower', () => { 103 - const bytes = new Uint8Array([0x01, 0x71, 0x12, 0x20]) 104 - const encoded = base32Encode(bytes) 105 - assert.strictEqual(typeof encoded, 'string') 106 - assert.match(encoded, /^[a-z2-7]+$/) 107 - }) 108 - }) 121 + const bytes = new Uint8Array([0x01, 0x71, 0x12, 0x20]); 122 + const encoded = base32Encode(bytes); 123 + assert.strictEqual(typeof encoded, 'string'); 124 + assert.match(encoded, /^[a-z2-7]+$/); 125 + }); 126 + }); 109 127 110 128 describe('CID Generation', () => { 111 129 test('creates CIDv1 with dag-cbor codec', async () => { 112 - const data = cborEncode({ test: 'data' }) 113 - const cid = await createCid(data) 130 + const data = cborEncode({ test: 'data' }); 131 + const cid = await createCid(data); 114 132 115 - assert.strictEqual(cid.length, 36) // 2 prefix + 2 multihash header + 32 hash 116 - assert.strictEqual(cid[0], 0x01) // CIDv1 117 - assert.strictEqual(cid[1], 0x71) // dag-cbor 118 - assert.strictEqual(cid[2], 0x12) // sha-256 119 - assert.strictEqual(cid[3], 0x20) // 32 bytes 120 - }) 133 + assert.strictEqual(cid.length, 36); // 2 prefix + 2 multihash header + 32 hash 134 + assert.strictEqual(cid[0], 0x01); // CIDv1 135 + assert.strictEqual(cid[1], 0x71); // dag-cbor 136 + assert.strictEqual(cid[2], 0x12); // sha-256 137 + assert.strictEqual(cid[3], 0x20); // 32 bytes 138 + }); 121 139 122 140 test('cidToString returns base32lower with b prefix', async () => { 123 - const data = cborEncode({ test: 'data' }) 124 - const cid = await createCid(data) 125 - const cidStr = cidToString(cid) 141 + const data = cborEncode({ test: 'data' }); 142 + const cid = await createCid(data); 143 + const cidStr = cidToString(cid); 126 144 127 - assert.strictEqual(cidStr[0], 'b') 128 - assert.match(cidStr, /^b[a-z2-7]+$/) 129 - }) 145 + assert.strictEqual(cidStr[0], 'b'); 146 + assert.match(cidStr, /^b[a-z2-7]+$/); 147 + }); 130 148 131 149 test('same input produces same CID', async () => { 132 - const data1 = cborEncode({ test: 'data' }) 133 - const data2 = cborEncode({ test: 'data' }) 134 - const cid1 = cidToString(await createCid(data1)) 135 - const cid2 = cidToString(await createCid(data2)) 150 + const data1 = cborEncode({ test: 'data' }); 151 + const data2 = cborEncode({ test: 'data' }); 152 + const cid1 = cidToString(await createCid(data1)); 153 + const cid2 = cidToString(await createCid(data2)); 136 154 137 - assert.strictEqual(cid1, cid2) 138 - }) 155 + assert.strictEqual(cid1, cid2); 156 + }); 139 157 140 158 test('different input produces different CID', async () => { 141 - const cid1 = cidToString(await createCid(cborEncode({ a: 1 }))) 142 - const cid2 = cidToString(await createCid(cborEncode({ a: 2 }))) 159 + const cid1 = cidToString(await createCid(cborEncode({ a: 1 }))); 160 + const cid2 = cidToString(await createCid(cborEncode({ a: 2 }))); 143 161 144 - assert.notStrictEqual(cid1, cid2) 145 - }) 146 - }) 162 + assert.notStrictEqual(cid1, cid2); 163 + }); 164 + }); 147 165 148 166 describe('TID Generation', () => { 149 167 test('creates 13-character TIDs', () => { 150 - const tid = createTid() 151 - assert.strictEqual(tid.length, 13) 152 - }) 168 + const tid = createTid(); 169 + assert.strictEqual(tid.length, 13); 170 + }); 153 171 154 172 test('uses valid base32-sort characters', () => { 155 - const tid = createTid() 156 - assert.match(tid, /^[234567abcdefghijklmnopqrstuvwxyz]+$/) 157 - }) 173 + const tid = createTid(); 174 + assert.match(tid, /^[234567abcdefghijklmnopqrstuvwxyz]+$/); 175 + }); 158 176 159 177 test('generates monotonically increasing TIDs', () => { 160 - const tid1 = createTid() 161 - const tid2 = createTid() 162 - const tid3 = createTid() 178 + const tid1 = createTid(); 179 + const tid2 = createTid(); 180 + const tid3 = createTid(); 163 181 164 - assert.ok(tid1 < tid2, `${tid1} should be less than ${tid2}`) 165 - assert.ok(tid2 < tid3, `${tid2} should be less than ${tid3}`) 166 - }) 182 + assert.ok(tid1 < tid2, `${tid1} should be less than ${tid2}`); 183 + assert.ok(tid2 < tid3, `${tid2} should be less than ${tid3}`); 184 + }); 167 185 168 186 test('generates unique TIDs', () => { 169 - const tids = new Set() 187 + const tids = new Set(); 170 188 for (let i = 0; i < 100; i++) { 171 - tids.add(createTid()) 189 + tids.add(createTid()); 172 190 } 173 - assert.strictEqual(tids.size, 100) 174 - }) 175 - }) 191 + assert.strictEqual(tids.size, 100); 192 + }); 193 + }); 176 194 177 195 describe('P-256 Signing', () => { 178 196 test('generates key pair with correct sizes', async () => { 179 - const kp = await generateKeyPair() 197 + const kp = await generateKeyPair(); 180 198 181 - assert.strictEqual(kp.privateKey.length, 32) 182 - assert.strictEqual(kp.publicKey.length, 33) // compressed 183 - assert.ok(kp.publicKey[0] === 0x02 || kp.publicKey[0] === 0x03) 184 - }) 199 + assert.strictEqual(kp.privateKey.length, 32); 200 + assert.strictEqual(kp.publicKey.length, 33); // compressed 201 + assert.ok(kp.publicKey[0] === 0x02 || kp.publicKey[0] === 0x03); 202 + }); 185 203 186 204 test('can sign data with generated key', async () => { 187 - const kp = await generateKeyPair() 188 - const key = await importPrivateKey(kp.privateKey) 189 - const data = new TextEncoder().encode('test message') 190 - const sig = await sign(key, data) 205 + const kp = await generateKeyPair(); 206 + const key = await importPrivateKey(kp.privateKey); 207 + const data = new TextEncoder().encode('test message'); 208 + const sig = await sign(key, data); 191 209 192 - assert.strictEqual(sig.length, 64) // r (32) + s (32) 193 - }) 210 + assert.strictEqual(sig.length, 64); // r (32) + s (32) 211 + }); 194 212 195 213 test('different messages produce different signatures', async () => { 196 - const kp = await generateKeyPair() 197 - const key = await importPrivateKey(kp.privateKey) 214 + const kp = await generateKeyPair(); 215 + const key = await importPrivateKey(kp.privateKey); 198 216 199 - const sig1 = await sign(key, new TextEncoder().encode('message 1')) 200 - const sig2 = await sign(key, new TextEncoder().encode('message 2')) 217 + const sig1 = await sign(key, new TextEncoder().encode('message 1')); 218 + const sig2 = await sign(key, new TextEncoder().encode('message 2')); 201 219 202 - assert.notDeepStrictEqual(sig1, sig2) 203 - }) 220 + assert.notDeepStrictEqual(sig1, sig2); 221 + }); 204 222 205 223 test('bytesToHex and hexToBytes roundtrip', () => { 206 - const original = new Uint8Array([0x00, 0x0f, 0xf0, 0xff, 0xab, 0xcd]) 207 - const hex = bytesToHex(original) 208 - const back = hexToBytes(hex) 224 + const original = new Uint8Array([0x00, 0x0f, 0xf0, 0xff, 0xab, 0xcd]); 225 + const hex = bytesToHex(original); 226 + const back = hexToBytes(hex); 209 227 210 - assert.strictEqual(hex, '000ff0ffabcd') 211 - assert.deepStrictEqual(back, original) 212 - }) 228 + assert.strictEqual(hex, '000ff0ffabcd'); 229 + assert.deepStrictEqual(back, original); 230 + }); 213 231 214 232 test('importPrivateKey rejects invalid key lengths', async () => { 215 233 // Too short 216 234 await assert.rejects( 217 235 () => importPrivateKey(new Uint8Array(31)), 218 - /expected 32 bytes, got 31/ 219 - ) 236 + /expected 32 bytes, got 31/, 237 + ); 220 238 221 239 // Too long 222 240 await assert.rejects( 223 241 () => importPrivateKey(new Uint8Array(33)), 224 - /expected 32 bytes, got 33/ 225 - ) 242 + /expected 32 bytes, got 33/, 243 + ); 226 244 227 245 // Empty 228 246 await assert.rejects( 229 247 () => importPrivateKey(new Uint8Array(0)), 230 - /expected 32 bytes, got 0/ 231 - ) 232 - }) 248 + /expected 32 bytes, got 0/, 249 + ); 250 + }); 233 251 234 252 test('importPrivateKey rejects non-Uint8Array input', async () => { 235 253 // Arrays have .length but aren't Uint8Array 236 254 await assert.rejects( 237 255 () => importPrivateKey([1, 2, 3]), 238 - /Invalid private key/ 239 - ) 256 + /Invalid private key/, 257 + ); 240 258 241 259 // Strings don't work either 242 260 await assert.rejects( 243 261 () => importPrivateKey('not bytes'), 244 - /Invalid private key/ 245 - ) 262 + /Invalid private key/, 263 + ); 246 264 247 265 // null/undefined 248 - await assert.rejects( 249 - () => importPrivateKey(null), 250 - /Invalid private key/ 251 - ) 252 - }) 253 - }) 266 + await assert.rejects(() => importPrivateKey(null), /Invalid private key/); 267 + }); 268 + }); 254 269 255 270 describe('MST Key Depth', () => { 256 271 test('returns a non-negative integer', async () => { 257 - const depth = await getKeyDepth('app.bsky.feed.post/abc123') 258 - assert.strictEqual(typeof depth, 'number') 259 - assert.ok(depth >= 0) 260 - }) 272 + const depth = await getKeyDepth('app.bsky.feed.post/abc123'); 273 + assert.strictEqual(typeof depth, 'number'); 274 + assert.ok(depth >= 0); 275 + }); 261 276 262 277 test('is deterministic for same key', async () => { 263 - const key = 'app.bsky.feed.post/test123' 264 - const depth1 = await getKeyDepth(key) 265 - const depth2 = await getKeyDepth(key) 266 - assert.strictEqual(depth1, depth2) 267 - }) 278 + const key = 'app.bsky.feed.post/test123'; 279 + const depth1 = await getKeyDepth(key); 280 + const depth2 = await getKeyDepth(key); 281 + assert.strictEqual(depth1, depth2); 282 + }); 268 283 269 284 test('different keys can have different depths', async () => { 270 285 // Generate many keys and check we get some variation 271 - const depths = new Set() 286 + const depths = new Set(); 272 287 for (let i = 0; i < 100; i++) { 273 - depths.add(await getKeyDepth(`collection/key${i}`)) 288 + depths.add(await getKeyDepth(`collection/key${i}`)); 274 289 } 275 290 // Should have at least 1 unique depth (realistically more) 276 - assert.ok(depths.size >= 1) 277 - }) 291 + assert.ok(depths.size >= 1); 292 + }); 278 293 279 294 test('handles empty string', async () => { 280 - const depth = await getKeyDepth('') 281 - assert.strictEqual(typeof depth, 'number') 282 - assert.ok(depth >= 0) 283 - }) 295 + const depth = await getKeyDepth(''); 296 + assert.strictEqual(typeof depth, 'number'); 297 + assert.ok(depth >= 0); 298 + }); 284 299 285 300 test('handles unicode strings', async () => { 286 - const depth = await getKeyDepth('app.bsky.feed.post/émoji🎉') 287 - assert.strictEqual(typeof depth, 'number') 288 - assert.ok(depth >= 0) 289 - }) 290 - }) 301 + const depth = await getKeyDepth('app.bsky.feed.post/émoji🎉'); 302 + assert.strictEqual(typeof depth, 'number'); 303 + assert.ok(depth >= 0); 304 + }); 305 + }); 291 306 292 307 describe('CBOR Decoding', () => { 293 308 test('decodes what encode produces (roundtrip)', () => { 294 - const original = { hello: 'world', num: 42 } 295 - const encoded = cborEncode(original) 296 - const decoded = cborDecode(encoded) 297 - assert.deepStrictEqual(decoded, original) 298 - }) 309 + const original = { hello: 'world', num: 42 }; 310 + const encoded = cborEncode(original); 311 + const decoded = cborDecode(encoded); 312 + assert.deepStrictEqual(decoded, original); 313 + }); 299 314 300 315 test('decodes null', () => { 301 - const encoded = cborEncode(null) 302 - const decoded = cborDecode(encoded) 303 - assert.strictEqual(decoded, null) 304 - }) 316 + const encoded = cborEncode(null); 317 + const decoded = cborDecode(encoded); 318 + assert.strictEqual(decoded, null); 319 + }); 305 320 306 321 test('decodes booleans', () => { 307 - assert.strictEqual(cborDecode(cborEncode(true)), true) 308 - assert.strictEqual(cborDecode(cborEncode(false)), false) 309 - }) 322 + assert.strictEqual(cborDecode(cborEncode(true)), true); 323 + assert.strictEqual(cborDecode(cborEncode(false)), false); 324 + }); 310 325 311 326 test('decodes integers', () => { 312 - assert.strictEqual(cborDecode(cborEncode(0)), 0) 313 - assert.strictEqual(cborDecode(cborEncode(42)), 42) 314 - assert.strictEqual(cborDecode(cborEncode(255)), 255) 315 - assert.strictEqual(cborDecode(cborEncode(-1)), -1) 316 - assert.strictEqual(cborDecode(cborEncode(-10)), -10) 317 - }) 327 + assert.strictEqual(cborDecode(cborEncode(0)), 0); 328 + assert.strictEqual(cborDecode(cborEncode(42)), 42); 329 + assert.strictEqual(cborDecode(cborEncode(255)), 255); 330 + assert.strictEqual(cborDecode(cborEncode(-1)), -1); 331 + assert.strictEqual(cborDecode(cborEncode(-10)), -10); 332 + }); 318 333 319 334 test('decodes strings', () => { 320 - assert.strictEqual(cborDecode(cborEncode('hello')), 'hello') 321 - assert.strictEqual(cborDecode(cborEncode('')), '') 322 - }) 335 + assert.strictEqual(cborDecode(cborEncode('hello')), 'hello'); 336 + assert.strictEqual(cborDecode(cborEncode('')), ''); 337 + }); 323 338 324 339 test('decodes arrays', () => { 325 - assert.deepStrictEqual(cborDecode(cborEncode([1, 2, 3])), [1, 2, 3]) 326 - assert.deepStrictEqual(cborDecode(cborEncode([])), []) 327 - }) 340 + assert.deepStrictEqual(cborDecode(cborEncode([1, 2, 3])), [1, 2, 3]); 341 + assert.deepStrictEqual(cborDecode(cborEncode([])), []); 342 + }); 328 343 329 344 test('decodes nested structures', () => { 330 - const original = { arr: [1, { nested: true }], str: 'test' } 331 - const decoded = cborDecode(cborEncode(original)) 332 - assert.deepStrictEqual(decoded, original) 333 - }) 334 - }) 345 + const original = { arr: [1, { nested: true }], str: 'test' }; 346 + const decoded = cborDecode(cborEncode(original)); 347 + assert.deepStrictEqual(decoded, original); 348 + }); 349 + }); 335 350 336 351 describe('CAR File Builder', () => { 337 352 test('varint encodes small numbers', () => { 338 - assert.deepStrictEqual(varint(0), new Uint8Array([0])) 339 - assert.deepStrictEqual(varint(1), new Uint8Array([1])) 340 - assert.deepStrictEqual(varint(127), new Uint8Array([127])) 341 - }) 353 + assert.deepStrictEqual(varint(0), new Uint8Array([0])); 354 + assert.deepStrictEqual(varint(1), new Uint8Array([1])); 355 + assert.deepStrictEqual(varint(127), new Uint8Array([127])); 356 + }); 342 357 343 358 test('varint encodes multi-byte numbers', () => { 344 359 // 128 = 0x80 -> [0x80 | 0x00, 0x01] = [0x80, 0x01] 345 - assert.deepStrictEqual(varint(128), new Uint8Array([0x80, 0x01])) 360 + assert.deepStrictEqual(varint(128), new Uint8Array([0x80, 0x01])); 346 361 // 300 = 0x12c -> [0xac, 0x02] 347 - assert.deepStrictEqual(varint(300), new Uint8Array([0xac, 0x02])) 348 - }) 362 + assert.deepStrictEqual(varint(300), new Uint8Array([0xac, 0x02])); 363 + }); 349 364 350 365 test('base32 encode/decode roundtrip', () => { 351 - const original = new Uint8Array([0x01, 0x71, 0x12, 0x20, 0xab, 0xcd]) 352 - const encoded = base32Encode(original) 353 - const decoded = base32Decode(encoded) 354 - assert.deepStrictEqual(decoded, original) 355 - }) 366 + const original = new Uint8Array([0x01, 0x71, 0x12, 0x20, 0xab, 0xcd]); 367 + const encoded = base32Encode(original); 368 + const decoded = base32Decode(encoded); 369 + assert.deepStrictEqual(decoded, original); 370 + }); 356 371 357 372 test('buildCarFile produces valid structure', async () => { 358 - const data = cborEncode({ test: 'data' }) 359 - const cid = await createCid(data) 360 - const cidStr = cidToString(cid) 373 + const data = cborEncode({ test: 'data' }); 374 + const cid = await createCid(data); 375 + const cidStr = cidToString(cid); 361 376 362 - const car = buildCarFile(cidStr, [{ cid: cidStr, data }]) 377 + const car = buildCarFile(cidStr, [{ cid: cidStr, data }]); 363 378 364 - assert.ok(car instanceof Uint8Array) 365 - assert.ok(car.length > 0) 379 + assert.ok(car instanceof Uint8Array); 380 + assert.ok(car.length > 0); 366 381 // First byte should be varint of header length 367 - assert.ok(car[0] > 0) 368 - }) 369 - }) 382 + assert.ok(car[0] > 0); 383 + }); 384 + }); 370 385 371 386 describe('JWT Base64URL', () => { 372 387 test('base64UrlEncode encodes bytes correctly', () => { 373 - const input = new TextEncoder().encode('hello world') 374 - const encoded = base64UrlEncode(input) 375 - assert.strictEqual(encoded, 'aGVsbG8gd29ybGQ') 376 - assert.ok(!encoded.includes('+')) 377 - assert.ok(!encoded.includes('/')) 378 - assert.ok(!encoded.includes('=')) 379 - }) 388 + const input = new TextEncoder().encode('hello world'); 389 + const encoded = base64UrlEncode(input); 390 + assert.strictEqual(encoded, 'aGVsbG8gd29ybGQ'); 391 + assert.ok(!encoded.includes('+')); 392 + assert.ok(!encoded.includes('/')); 393 + assert.ok(!encoded.includes('=')); 394 + }); 380 395 381 396 test('base64UrlDecode decodes string correctly', () => { 382 - const decoded = base64UrlDecode('aGVsbG8gd29ybGQ') 383 - const str = new TextDecoder().decode(decoded) 384 - assert.strictEqual(str, 'hello world') 385 - }) 397 + const decoded = base64UrlDecode('aGVsbG8gd29ybGQ'); 398 + const str = new TextDecoder().decode(decoded); 399 + assert.strictEqual(str, 'hello world'); 400 + }); 386 401 387 402 test('base64url roundtrip', () => { 388 - const original = new Uint8Array([0, 1, 2, 255, 254, 253]) 389 - const encoded = base64UrlEncode(original) 390 - const decoded = base64UrlDecode(encoded) 391 - assert.deepStrictEqual(decoded, original) 392 - }) 393 - }) 403 + const original = new Uint8Array([0, 1, 2, 255, 254, 253]); 404 + const encoded = base64UrlEncode(original); 405 + const decoded = base64UrlDecode(encoded); 406 + assert.deepStrictEqual(decoded, original); 407 + }); 408 + }); 394 409 395 410 describe('JWT Creation', () => { 396 411 test('createAccessJwt creates valid JWT structure', async () => { 397 - const did = 'did:web:test.example' 398 - const secret = 'test-secret-key' 399 - const jwt = await createAccessJwt(did, secret) 412 + const did = 'did:web:test.example'; 413 + const secret = 'test-secret-key'; 414 + const jwt = await createAccessJwt(did, secret); 400 415 401 - const parts = jwt.split('.') 402 - assert.strictEqual(parts.length, 3) 416 + const parts = jwt.split('.'); 417 + assert.strictEqual(parts.length, 3); 403 418 404 419 // Decode header 405 - const header = JSON.parse(new TextDecoder().decode(base64UrlDecode(parts[0]))) 406 - assert.strictEqual(header.typ, 'at+jwt') 407 - assert.strictEqual(header.alg, 'HS256') 420 + const header = JSON.parse( 421 + new TextDecoder().decode(base64UrlDecode(parts[0])), 422 + ); 423 + assert.strictEqual(header.typ, 'at+jwt'); 424 + assert.strictEqual(header.alg, 'HS256'); 408 425 409 426 // Decode payload 410 - const payload = JSON.parse(new TextDecoder().decode(base64UrlDecode(parts[1]))) 411 - assert.strictEqual(payload.scope, 'com.atproto.access') 412 - assert.strictEqual(payload.sub, did) 413 - assert.strictEqual(payload.aud, did) 414 - assert.ok(payload.iat > 0) 415 - assert.ok(payload.exp > payload.iat) 416 - }) 427 + const payload = JSON.parse( 428 + new TextDecoder().decode(base64UrlDecode(parts[1])), 429 + ); 430 + assert.strictEqual(payload.scope, 'com.atproto.access'); 431 + assert.strictEqual(payload.sub, did); 432 + assert.strictEqual(payload.aud, did); 433 + assert.ok(payload.iat > 0); 434 + assert.ok(payload.exp > payload.iat); 435 + }); 417 436 418 437 test('createRefreshJwt creates valid JWT with jti', async () => { 419 - const did = 'did:web:test.example' 420 - const secret = 'test-secret-key' 421 - const jwt = await createRefreshJwt(did, secret) 438 + const did = 'did:web:test.example'; 439 + const secret = 'test-secret-key'; 440 + const jwt = await createRefreshJwt(did, secret); 422 441 423 - const parts = jwt.split('.') 424 - const header = JSON.parse(new TextDecoder().decode(base64UrlDecode(parts[0]))) 425 - assert.strictEqual(header.typ, 'refresh+jwt') 442 + const parts = jwt.split('.'); 443 + const header = JSON.parse( 444 + new TextDecoder().decode(base64UrlDecode(parts[0])), 445 + ); 446 + assert.strictEqual(header.typ, 'refresh+jwt'); 426 447 427 - const payload = JSON.parse(new TextDecoder().decode(base64UrlDecode(parts[1]))) 428 - assert.strictEqual(payload.scope, 'com.atproto.refresh') 429 - assert.ok(payload.jti) // has unique token ID 430 - }) 431 - }) 448 + const payload = JSON.parse( 449 + new TextDecoder().decode(base64UrlDecode(parts[1])), 450 + ); 451 + assert.strictEqual(payload.scope, 'com.atproto.refresh'); 452 + assert.ok(payload.jti); // has unique token ID 453 + }); 454 + }); 432 455 433 456 describe('JWT Verification', () => { 434 457 test('verifyAccessJwt returns payload for valid token', async () => { 435 - const did = 'did:web:test.example' 436 - const secret = 'test-secret-key' 437 - const jwt = await createAccessJwt(did, secret) 458 + const did = 'did:web:test.example'; 459 + const secret = 'test-secret-key'; 460 + const jwt = await createAccessJwt(did, secret); 438 461 439 - const payload = await verifyAccessJwt(jwt, secret) 440 - assert.strictEqual(payload.sub, did) 441 - assert.strictEqual(payload.scope, 'com.atproto.access') 442 - }) 462 + const payload = await verifyAccessJwt(jwt, secret); 463 + assert.strictEqual(payload.sub, did); 464 + assert.strictEqual(payload.scope, 'com.atproto.access'); 465 + }); 443 466 444 467 test('verifyAccessJwt throws for wrong secret', async () => { 445 - const did = 'did:web:test.example' 446 - const jwt = await createAccessJwt(did, 'correct-secret') 468 + const did = 'did:web:test.example'; 469 + const jwt = await createAccessJwt(did, 'correct-secret'); 447 470 448 471 await assert.rejects( 449 472 () => verifyAccessJwt(jwt, 'wrong-secret'), 450 - /invalid signature/i 451 - ) 452 - }) 473 + /invalid signature/i, 474 + ); 475 + }); 453 476 454 477 test('verifyAccessJwt throws for expired token', async () => { 455 - const did = 'did:web:test.example' 456 - const secret = 'test-secret-key' 478 + const did = 'did:web:test.example'; 479 + const secret = 'test-secret-key'; 457 480 // Create token that expired 1 second ago 458 - const jwt = await createAccessJwt(did, secret, -1) 481 + const jwt = await createAccessJwt(did, secret, -1); 459 482 460 - await assert.rejects( 461 - () => verifyAccessJwt(jwt, secret), 462 - /expired/i 463 - ) 464 - }) 483 + await assert.rejects(() => verifyAccessJwt(jwt, secret), /expired/i); 484 + }); 465 485 466 486 test('verifyAccessJwt throws for refresh token', async () => { 467 - const did = 'did:web:test.example' 468 - const secret = 'test-secret-key' 469 - const jwt = await createRefreshJwt(did, secret) 487 + const did = 'did:web:test.example'; 488 + const secret = 'test-secret-key'; 489 + const jwt = await createRefreshJwt(did, secret); 470 490 471 491 await assert.rejects( 472 492 () => verifyAccessJwt(jwt, secret), 473 - /invalid token type/i 474 - ) 475 - }) 476 - }) 493 + /invalid token type/i, 494 + ); 495 + }); 496 + });