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 *
8 * Usage: node scripts/setup.js --handle alice --pds https://your-pds.workers.dev
9 */
10
11import { writeFileSync } from 'node:fs';
12import {
13 base32Encode,
14 base64UrlEncode,
15 bytesToHex,
16 cborEncodeDagCbor,
17 generateKeyPair,
18 importPrivateKey,
19 sign,
20} from '../src/pds.js';
21
22// === ARGUMENT PARSING ===
23
24function parseArgs() {
25 const args = process.argv.slice(2);
26 const opts = {
27 handle: null,
28 pds: null,
29 plcUrl: 'https://plc.directory',
30 relayUrl: 'https://bsky.network',
31 };
32
33 for (let i = 0; i < args.length; i++) {
34 if (args[i] === '--handle' && args[i + 1]) {
35 opts.handle = args[++i];
36 } else if (args[i] === '--pds' && args[i + 1]) {
37 opts.pds = args[++i];
38 } else if (args[i] === '--plc-url' && args[i + 1]) {
39 opts.plcUrl = args[++i];
40 } else if (args[i] === '--relay-url' && args[i + 1]) {
41 opts.relayUrl = args[++i];
42 }
43 }
44
45 if (!opts.pds) {
46 console.error(
47 'Usage: node scripts/setup.js --pds <pds-url> [--handle <subdomain>]',
48 );
49 console.error('');
50 console.error('Options:');
51 console.error(
52 ' --pds PDS URL (e.g., "https://atproto-pds.chad-53c.workers.dev")',
53 );
54 console.error(
55 ' --handle Subdomain handle (e.g., "alice") - optional, uses bare hostname if omitted',
56 );
57 console.error(
58 ' --plc-url PLC directory URL (default: https://plc.directory)',
59 );
60 console.error(' --relay-url Relay URL (default: https://bsky.network)');
61 process.exit(1);
62 }
63
64 return opts;
65}
66
67// === DID:KEY ENCODING ===
68
69// Multicodec prefix for P-256 public key (0x1200)
70const P256_MULTICODEC = new Uint8Array([0x80, 0x24]);
71
72function publicKeyToDidKey(compressedPublicKey) {
73 // did:key format: "did:key:" + multibase(base58btc) of multicodec + key
74 const keyWithCodec = new Uint8Array(
75 P256_MULTICODEC.length + compressedPublicKey.length,
76 );
77 keyWithCodec.set(P256_MULTICODEC);
78 keyWithCodec.set(compressedPublicKey, P256_MULTICODEC.length);
79
80 return `did:key:z${base58btcEncode(keyWithCodec)}`;
81}
82
83function base58btcEncode(bytes) {
84 const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
85
86 // Count leading zeros
87 let zeros = 0;
88 for (const b of bytes) {
89 if (b === 0) zeros++;
90 else break;
91 }
92
93 // Convert to base58
94 const digits = [0];
95 for (const byte of bytes) {
96 let carry = byte;
97 for (let i = 0; i < digits.length; i++) {
98 carry += digits[i] << 8;
99 digits[i] = carry % 58;
100 carry = (carry / 58) | 0;
101 }
102 while (carry > 0) {
103 digits.push(carry % 58);
104 carry = (carry / 58) | 0;
105 }
106 }
107
108 // Convert to string
109 let result = '1'.repeat(zeros);
110 for (let i = digits.length - 1; i >= 0; i--) {
111 result += ALPHABET[digits[i]];
112 }
113
114 return result;
115}
116
117// === HASHING ===
118
119async function sha256(data) {
120 const hash = await crypto.subtle.digest('SHA-256', data);
121 return new Uint8Array(hash);
122}
123
124// === PLC OPERATIONS ===
125
126async function signPlcOperation(operation, cryptoKey) {
127 // Encode operation without sig field
128 const { sig, ...opWithoutSig } = operation;
129 const encoded = cborEncodeDagCbor(opWithoutSig);
130
131 // Sign with P-256 (sign() handles low-S normalization)
132 const signature = await sign(cryptoKey, encoded);
133 return base64UrlEncode(signature);
134}
135
136async function createGenesisOperation(opts) {
137 const { didKey, handle, pdsUrl, cryptoKey } = opts;
138
139 // Build full handle: subdomain.pds-hostname, or just pds-hostname if no subdomain
140 const pdsHost = new URL(pdsUrl).host;
141 const fullHandle = handle ? `${handle}.${pdsHost}` : pdsHost;
142
143 const operation = {
144 type: 'plc_operation',
145 rotationKeys: [didKey],
146 verificationMethods: {
147 atproto: didKey,
148 },
149 alsoKnownAs: [`at://${fullHandle}`],
150 services: {
151 atproto_pds: {
152 type: 'AtprotoPersonalDataServer',
153 endpoint: pdsUrl,
154 },
155 },
156 prev: null,
157 };
158
159 // Sign the operation
160 operation.sig = await signPlcOperation(operation, cryptoKey);
161
162 return { operation, fullHandle };
163}
164
165async function deriveDidFromOperation(operation) {
166 // DID is computed from the FULL operation INCLUDING the signature
167 const encoded = cborEncodeDagCbor(operation);
168 const hash = await sha256(encoded);
169 // DID is base32 of first 15 bytes of hash (= 24 base32 chars)
170 return `did:plc:${base32Encode(hash.slice(0, 15))}`;
171}
172
173// === PLC DIRECTORY REGISTRATION ===
174
175async function registerWithPlc(plcUrl, did, operation) {
176 const url = `${plcUrl}/${encodeURIComponent(did)}`;
177
178 const response = await fetch(url, {
179 method: 'POST',
180 headers: {
181 'Content-Type': 'application/json',
182 },
183 body: JSON.stringify(operation),
184 });
185
186 if (!response.ok) {
187 const text = await response.text();
188 throw new Error(`PLC registration failed: ${response.status} ${text}`);
189 }
190
191 return true;
192}
193
194// === PDS INITIALIZATION ===
195
196async function initializePds(pdsUrl, did, privateKeyHex, handle) {
197 const url = `${pdsUrl}/init?did=${encodeURIComponent(did)}`;
198
199 const response = await fetch(url, {
200 method: 'POST',
201 headers: {
202 'Content-Type': 'application/json',
203 },
204 body: JSON.stringify({
205 did,
206 privateKey: privateKeyHex,
207 handle,
208 }),
209 });
210
211 if (!response.ok) {
212 const text = await response.text();
213 throw new Error(`PDS initialization failed: ${response.status} ${text}`);
214 }
215
216 return response.json();
217}
218
219// === HANDLE REGISTRATION ===
220
221async function registerHandle(pdsUrl, handle, did) {
222 const url = `${pdsUrl}/register-handle`;
223
224 const response = await fetch(url, {
225 method: 'POST',
226 headers: {
227 'Content-Type': 'application/json',
228 },
229 body: JSON.stringify({ handle, did }),
230 });
231
232 if (!response.ok) {
233 const text = await response.text();
234 throw new Error(`Handle registration failed: ${response.status} ${text}`);
235 }
236
237 return true;
238}
239
240// === RELAY NOTIFICATION ===
241
242async function notifyRelay(relayUrl, pdsHostname) {
243 const url = `${relayUrl}/xrpc/com.atproto.sync.requestCrawl`;
244
245 const response = await fetch(url, {
246 method: 'POST',
247 headers: {
248 'Content-Type': 'application/json',
249 },
250 body: JSON.stringify({
251 hostname: pdsHostname,
252 }),
253 });
254
255 // Relay might return 200 or 202, both are OK
256 if (!response.ok && response.status !== 202) {
257 const text = await response.text();
258 console.warn(
259 ` Warning: Relay notification returned ${response.status}: ${text}`,
260 );
261 return false;
262 }
263
264 return true;
265}
266
267// === CREDENTIALS OUTPUT ===
268
269function saveCredentials(filename, credentials) {
270 writeFileSync(filename, JSON.stringify(credentials, null, 2));
271}
272
273// === MAIN ===
274
275async function main() {
276 const opts = parseArgs();
277
278 console.log('PDS Federation Setup');
279 console.log('====================');
280 console.log(`PDS: ${opts.pds}`);
281 console.log('');
282
283 // Step 1: Generate keypair
284 console.log('Generating P-256 keypair...');
285 const keyPair = await generateKeyPair();
286 const cryptoKey = await importPrivateKey(keyPair.privateKey);
287 const didKey = publicKeyToDidKey(keyPair.publicKey);
288 console.log(` did:key: ${didKey}`);
289 console.log('');
290
291 // Step 2: Create genesis operation
292 console.log('Creating PLC genesis operation...');
293 const { operation, fullHandle } = await createGenesisOperation({
294 didKey,
295 handle: opts.handle,
296 pdsUrl: opts.pds,
297 cryptoKey,
298 });
299 const did = await deriveDidFromOperation(operation);
300 console.log(` DID: ${did}`);
301 console.log(` Handle: ${fullHandle}`);
302 console.log('');
303
304 // Step 3: Register with PLC directory
305 console.log(`Registering with ${opts.plcUrl}...`);
306 await registerWithPlc(opts.plcUrl, did, operation);
307 console.log(' Registered successfully!');
308 console.log('');
309
310 // Step 4: Initialize PDS
311 console.log(`Initializing PDS at ${opts.pds}...`);
312 const privateKeyHex = bytesToHex(keyPair.privateKey);
313 await initializePds(opts.pds, did, privateKeyHex, fullHandle);
314 console.log(' PDS initialized!');
315 console.log('');
316
317 // Step 4b: Register handle -> DID mapping (only for subdomain handles)
318 if (opts.handle) {
319 console.log(`Registering handle mapping...`);
320 await registerHandle(opts.pds, opts.handle, did);
321 console.log(` Handle ${opts.handle} -> ${did}`);
322 console.log('');
323 }
324
325 // Step 5: Notify relay
326 const pdsHostname = new URL(opts.pds).host;
327 console.log(`Notifying relay at ${opts.relayUrl}...`);
328 const relayOk = await notifyRelay(opts.relayUrl, pdsHostname);
329 if (relayOk) {
330 console.log(' Relay notified!');
331 }
332 console.log('');
333
334 // Step 6: Save credentials
335 const credentials = {
336 handle: fullHandle,
337 did,
338 privateKeyHex: bytesToHex(keyPair.privateKey),
339 didKey,
340 pdsUrl: opts.pds,
341 createdAt: new Date().toISOString(),
342 };
343
344 const credentialsFile = `./credentials-${opts.handle || new URL(opts.pds).host}.json`;
345 saveCredentials(credentialsFile, credentials);
346
347 // Final output
348 console.log('Setup Complete!');
349 console.log('===============');
350 console.log(`Handle: ${fullHandle}`);
351 console.log(`DID: ${did}`);
352 console.log(`PDS: ${opts.pds}`);
353 console.log('');
354 console.log(`Credentials saved to: ${credentialsFile}`);
355 console.log('Keep this file safe - it contains your private key!');
356}
357
358main().catch((err) => {
359 console.error('Error:', err.message);
360 process.exit(1);
361});