this repo has no description
1#!/usr/bin/env node
2
3/**
4 * PDS Setup Script
5 *
6 * Registers a did:plc, initializes the PDS, and notifies the relay.
7 * Zero dependencies - uses Node.js built-ins only.
8 *
9 * Usage: node scripts/setup.js --handle alice --pds https://your-pds.workers.dev
10 */
11
12import { webcrypto } from 'crypto'
13
14// === ARGUMENT PARSING ===
15
16function parseArgs() {
17 const args = process.argv.slice(2)
18 const opts = {
19 handle: null,
20 pds: null,
21 plcUrl: 'https://plc.directory',
22 relayUrl: 'https://bsky.network'
23 }
24
25 for (let i = 0; i < args.length; i++) {
26 if (args[i] === '--handle' && args[i + 1]) {
27 opts.handle = args[++i]
28 } else if (args[i] === '--pds' && args[i + 1]) {
29 opts.pds = args[++i]
30 } else if (args[i] === '--plc-url' && args[i + 1]) {
31 opts.plcUrl = args[++i]
32 } else if (args[i] === '--relay-url' && args[i + 1]) {
33 opts.relayUrl = args[++i]
34 }
35 }
36
37 if (!opts.handle || !opts.pds) {
38 console.error('Usage: node scripts/setup.js --handle <handle> --pds <pds-url>')
39 console.error('')
40 console.error('Options:')
41 console.error(' --handle Handle name (e.g., "alice")')
42 console.error(' --pds PDS URL (e.g., "https://atproto-pds.chad-53c.workers.dev")')
43 console.error(' --plc-url PLC directory URL (default: https://plc.directory)')
44 console.error(' --relay-url Relay URL (default: https://bsky.network)')
45 process.exit(1)
46 }
47
48 return opts
49}
50
51// === KEY GENERATION ===
52
53async function generateP256Keypair() {
54 const keyPair = await webcrypto.subtle.generateKey(
55 { name: 'ECDSA', namedCurve: 'P-256' },
56 true,
57 ['sign', 'verify']
58 )
59
60 // Export private key as raw 32 bytes
61 const privateJwk = await webcrypto.subtle.exportKey('jwk', keyPair.privateKey)
62 const privateBytes = base64UrlDecode(privateJwk.d)
63
64 // Export public key as uncompressed point (65 bytes)
65 const publicRaw = await webcrypto.subtle.exportKey('raw', keyPair.publicKey)
66 const publicBytes = new Uint8Array(publicRaw)
67
68 // Compress public key to 33 bytes
69 const compressedPublic = compressPublicKey(publicBytes)
70
71 return {
72 privateKey: privateBytes,
73 publicKey: compressedPublic,
74 cryptoKey: keyPair.privateKey
75 }
76}
77
78function compressPublicKey(uncompressed) {
79 // uncompressed is 65 bytes: 0x04 + x(32) + y(32)
80 const x = uncompressed.slice(1, 33)
81 const y = uncompressed.slice(33, 65)
82 const prefix = (y[31] & 1) === 0 ? 0x02 : 0x03
83 const compressed = new Uint8Array(33)
84 compressed[0] = prefix
85 compressed.set(x, 1)
86 return compressed
87}
88
89function base64UrlDecode(str) {
90 const base64 = str.replace(/-/g, '+').replace(/_/g, '/')
91 const binary = atob(base64)
92 const bytes = new Uint8Array(binary.length)
93 for (let i = 0; i < binary.length; i++) {
94 bytes[i] = binary.charCodeAt(i)
95 }
96 return bytes
97}
98
99function bytesToHex(bytes) {
100 return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')
101}
102
103// === DID:KEY ENCODING ===
104
105// Multicodec prefix for P-256 public key (0x1200)
106const P256_MULTICODEC = new Uint8Array([0x80, 0x24])
107
108function publicKeyToDidKey(compressedPublicKey) {
109 // did:key format: "did:key:" + multibase(base58btc) of multicodec + key
110 const keyWithCodec = new Uint8Array(P256_MULTICODEC.length + compressedPublicKey.length)
111 keyWithCodec.set(P256_MULTICODEC)
112 keyWithCodec.set(compressedPublicKey, P256_MULTICODEC.length)
113
114 return 'did:key:z' + base58btcEncode(keyWithCodec)
115}
116
117function base58btcEncode(bytes) {
118 const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
119
120 // Count leading zeros
121 let zeros = 0
122 for (const b of bytes) {
123 if (b === 0) zeros++
124 else break
125 }
126
127 // Convert to base58
128 const digits = [0]
129 for (const byte of bytes) {
130 let carry = byte
131 for (let i = 0; i < digits.length; i++) {
132 carry += digits[i] << 8
133 digits[i] = carry % 58
134 carry = (carry / 58) | 0
135 }
136 while (carry > 0) {
137 digits.push(carry % 58)
138 carry = (carry / 58) | 0
139 }
140 }
141
142 // Convert to string
143 let result = '1'.repeat(zeros)
144 for (let i = digits.length - 1; i >= 0; i--) {
145 result += ALPHABET[digits[i]]
146 }
147
148 return result
149}
150
151// === CBOR ENCODING (minimal for PLC operations) ===
152
153function cborEncode(value) {
154 const parts = []
155
156 function encode(val) {
157 if (val === null) {
158 parts.push(0xf6)
159 } else if (typeof val === 'string') {
160 const bytes = new TextEncoder().encode(val)
161 encodeHead(3, bytes.length)
162 parts.push(...bytes)
163 } else if (typeof val === 'number') {
164 if (Number.isInteger(val) && val >= 0) {
165 encodeHead(0, val)
166 }
167 } else if (val instanceof Uint8Array) {
168 encodeHead(2, val.length)
169 parts.push(...val)
170 } else if (Array.isArray(val)) {
171 encodeHead(4, val.length)
172 for (const item of val) encode(item)
173 } else if (typeof val === 'object') {
174 const keys = Object.keys(val).sort()
175 encodeHead(5, keys.length)
176 for (const key of keys) {
177 encode(key)
178 encode(val[key])
179 }
180 }
181 }
182
183 function encodeHead(majorType, length) {
184 const mt = majorType << 5
185 if (length < 24) {
186 parts.push(mt | length)
187 } else if (length < 256) {
188 parts.push(mt | 24, length)
189 } else if (length < 65536) {
190 parts.push(mt | 25, length >> 8, length & 0xff)
191 }
192 }
193
194 encode(value)
195 return new Uint8Array(parts)
196}
197
198// === HASHING ===
199
200async function sha256(data) {
201 const hash = await webcrypto.subtle.digest('SHA-256', data)
202 return new Uint8Array(hash)
203}
204
205// === PLC OPERATIONS ===
206
207async function signPlcOperation(operation, privateKey) {
208 // Encode operation without sig field
209 const { sig, ...opWithoutSig } = operation
210 const encoded = cborEncode(opWithoutSig)
211
212 // Sign with P-256
213 const signature = await webcrypto.subtle.sign(
214 { name: 'ECDSA', hash: 'SHA-256' },
215 privateKey,
216 encoded
217 )
218
219 // Convert to low-S form and base64url encode
220 const sigBytes = ensureLowS(new Uint8Array(signature))
221 return base64UrlEncode(sigBytes)
222}
223
224function ensureLowS(sig) {
225 // P-256 order N
226 const N = BigInt('0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551')
227 const halfN = N / 2n
228
229 const r = sig.slice(0, 32)
230 const s = sig.slice(32, 64)
231
232 // Convert s to BigInt
233 let sInt = BigInt('0x' + bytesToHex(s))
234
235 // If s > N/2, replace with N - s
236 if (sInt > halfN) {
237 sInt = N - sInt
238 const newS = hexToBytes(sInt.toString(16).padStart(64, '0'))
239 const result = new Uint8Array(64)
240 result.set(r)
241 result.set(newS, 32)
242 return result
243 }
244
245 return sig
246}
247
248function hexToBytes(hex) {
249 const bytes = new Uint8Array(hex.length / 2)
250 for (let i = 0; i < hex.length; i += 2) {
251 bytes[i / 2] = parseInt(hex.substr(i, 2), 16)
252 }
253 return bytes
254}
255
256function base64UrlEncode(bytes) {
257 const binary = String.fromCharCode(...bytes)
258 return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
259}
260
261async function createGenesisOperation(opts) {
262 const { didKey, handle, pdsUrl, cryptoKey } = opts
263
264 // Build the full handle
265 const pdsHost = new URL(pdsUrl).host
266 const fullHandle = `${handle}.${pdsHost}`
267
268 const operation = {
269 type: 'plc_operation',
270 rotationKeys: [didKey],
271 verificationMethods: {
272 atproto: didKey
273 },
274 alsoKnownAs: [`at://${fullHandle}`],
275 services: {
276 atproto_pds: {
277 type: 'AtprotoPersonalDataServer',
278 endpoint: pdsUrl
279 }
280 },
281 prev: null
282 }
283
284 // Sign the operation
285 operation.sig = await signPlcOperation(operation, cryptoKey)
286
287 return { operation, fullHandle }
288}
289
290async function deriveDidFromOperation(operation) {
291 const { sig, ...opWithoutSig } = operation
292 const encoded = cborEncode(opWithoutSig)
293 const hash = await sha256(encoded)
294 // DID is base32 of first 24 bytes of hash
295 return 'did:plc:' + base32Encode(hash.slice(0, 24))
296}
297
298function base32Encode(bytes) {
299 const alphabet = 'abcdefghijklmnopqrstuvwxyz234567'
300 let result = ''
301 let bits = 0
302 let value = 0
303
304 for (const byte of bytes) {
305 value = (value << 8) | byte
306 bits += 8
307 while (bits >= 5) {
308 bits -= 5
309 result += alphabet[(value >> bits) & 31]
310 }
311 }
312
313 if (bits > 0) {
314 result += alphabet[(value << (5 - bits)) & 31]
315 }
316
317 return result
318}
319
320// === PLC DIRECTORY REGISTRATION ===
321
322async function registerWithPlc(plcUrl, did, operation) {
323 const url = `${plcUrl}/${encodeURIComponent(did)}`
324
325 const response = await fetch(url, {
326 method: 'POST',
327 headers: {
328 'Content-Type': 'application/json'
329 },
330 body: JSON.stringify(operation)
331 })
332
333 if (!response.ok) {
334 const text = await response.text()
335 throw new Error(`PLC registration failed: ${response.status} ${text}`)
336 }
337
338 return true
339}
340
341// === PDS INITIALIZATION ===
342
343async function initializePds(pdsUrl, did, privateKeyHex, handle) {
344 const url = `${pdsUrl}/init?did=${encodeURIComponent(did)}`
345
346 const response = await fetch(url, {
347 method: 'POST',
348 headers: {
349 'Content-Type': 'application/json'
350 },
351 body: JSON.stringify({
352 did,
353 privateKey: privateKeyHex,
354 handle
355 })
356 })
357
358 if (!response.ok) {
359 const text = await response.text()
360 throw new Error(`PDS initialization failed: ${response.status} ${text}`)
361 }
362
363 return response.json()
364}
365
366// === MAIN ===
367
368async function main() {
369 const opts = parseArgs()
370
371 console.log('PDS Federation Setup')
372 console.log('====================')
373 console.log(`Handle: ${opts.handle}`)
374 console.log(`PDS: ${opts.pds}`)
375 console.log('')
376
377 // Step 1: Generate keypair
378 console.log('Generating P-256 keypair...')
379 const keyPair = await generateP256Keypair()
380 const didKey = publicKeyToDidKey(keyPair.publicKey)
381 console.log(` did:key: ${didKey}`)
382 console.log('')
383
384 // Step 2: Create genesis operation
385 console.log('Creating PLC genesis operation...')
386 const { operation, fullHandle } = await createGenesisOperation({
387 didKey,
388 handle: opts.handle,
389 pdsUrl: opts.pds,
390 cryptoKey: keyPair.cryptoKey
391 })
392 const did = await deriveDidFromOperation(operation)
393 console.log(` DID: ${did}`)
394 console.log(` Handle: ${fullHandle}`)
395 console.log('')
396
397 // Step 3: Register with PLC directory
398 console.log(`Registering with ${opts.plcUrl}...`)
399 await registerWithPlc(opts.plcUrl, did, operation)
400 console.log(' Registered successfully!')
401 console.log('')
402
403 // Step 4: Initialize PDS
404 console.log(`Initializing PDS at ${opts.pds}...`)
405 const privateKeyHex = bytesToHex(keyPair.privateKey)
406 await initializePds(opts.pds, did, privateKeyHex, fullHandle)
407 console.log(' PDS initialized!')
408 console.log('')
409
410 // TODO: Notify relay
411}
412
413main().catch(err => {
414 console.error('Error:', err.message)
415 process.exit(1)
416})