@ascorbic/pds 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,528 @@
1
+ #!/usr/bin/env node
2
+ import { defineCommand, runMain } from "citty";
3
+ import { randomBytes } from "node:crypto";
4
+ import * as p from "@clack/prompts";
5
+ import { spawn } from "node:child_process";
6
+ import { experimental_patchConfig, experimental_readRawConfig } from "wrangler";
7
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
8
+ import { resolve } from "node:path";
9
+ import bcrypt from "bcryptjs";
10
+ import { Secp256k1Keypair } from "@atproto/crypto";
11
+
12
+ //#region src/cli/utils/wrangler.ts
13
+ /**
14
+ * Wrangler integration utilities for setting vars and secrets
15
+ */
16
+ /**
17
+ * Set a var in wrangler.jsonc using experimental_patchConfig
18
+ */
19
+ function setVar(name, value) {
20
+ const { configPath } = experimental_readRawConfig({});
21
+ if (!configPath) throw new Error("No wrangler config found");
22
+ experimental_patchConfig(configPath, { vars: { [name]: value } });
23
+ }
24
+ /**
25
+ * Set multiple vars in wrangler.jsonc
26
+ */
27
+ function setVars(vars) {
28
+ const { configPath } = experimental_readRawConfig({});
29
+ if (!configPath) throw new Error("No wrangler config found");
30
+ experimental_patchConfig(configPath, { vars });
31
+ }
32
+ /**
33
+ * Get current vars from wrangler config
34
+ */
35
+ function getVars() {
36
+ const { rawConfig } = experimental_readRawConfig({});
37
+ return rawConfig.vars || {};
38
+ }
39
+ /**
40
+ * Set a secret using wrangler secret put
41
+ */
42
+ async function setSecret(name, value) {
43
+ return new Promise((resolve$1, reject) => {
44
+ const child = spawn("wrangler", [
45
+ "secret",
46
+ "put",
47
+ name
48
+ ], { stdio: [
49
+ "pipe",
50
+ "inherit",
51
+ "inherit"
52
+ ] });
53
+ child.stdin.write(value);
54
+ child.stdin.end();
55
+ child.on("close", (code) => {
56
+ if (code === 0) resolve$1();
57
+ else reject(/* @__PURE__ */ new Error(`wrangler secret put ${name} failed with code ${code}`));
58
+ });
59
+ child.on("error", reject);
60
+ });
61
+ }
62
+
63
+ //#endregion
64
+ //#region src/cli/utils/dotenv.ts
65
+ /**
66
+ * .dev.vars file utilities for local development
67
+ */
68
+ const DEV_VARS_FILE = ".dev.vars";
69
+ /**
70
+ * Parse a .dev.vars file into a record
71
+ */
72
+ function readDevVars(dir = process.cwd()) {
73
+ const filePath = resolve(dir, DEV_VARS_FILE);
74
+ if (!existsSync(filePath)) return {};
75
+ const content = readFileSync(filePath, "utf-8");
76
+ const vars = {};
77
+ for (const line of content.split("\n")) {
78
+ const trimmed = line.trim();
79
+ if (!trimmed || trimmed.startsWith("#")) continue;
80
+ const eqIndex = trimmed.indexOf("=");
81
+ if (eqIndex === -1) continue;
82
+ const key = trimmed.slice(0, eqIndex).trim();
83
+ let value = trimmed.slice(eqIndex + 1).trim();
84
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
85
+ vars[key] = value;
86
+ }
87
+ return vars;
88
+ }
89
+ /**
90
+ * Quote a value if it contains special characters
91
+ */
92
+ function quoteValue(value) {
93
+ if (value.includes(" ") || value.includes("\"") || value.includes("'")) return "\"" + value.replace(/"/g, "\\\"") + "\"";
94
+ return value;
95
+ }
96
+ /**
97
+ * Write vars to .dev.vars file, preserving comments and order
98
+ */
99
+ function writeDevVars(vars, dir = process.cwd()) {
100
+ const filePath = resolve(dir, DEV_VARS_FILE);
101
+ let existingLines = [];
102
+ if (existsSync(filePath)) existingLines = readFileSync(filePath, "utf-8").split("\n");
103
+ const outputLines = [];
104
+ const updatedKeys = /* @__PURE__ */ new Set();
105
+ for (const line of existingLines) {
106
+ const trimmed = line.trim();
107
+ if (!trimmed || trimmed.startsWith("#")) {
108
+ outputLines.push(line);
109
+ continue;
110
+ }
111
+ const eqIndex = trimmed.indexOf("=");
112
+ if (eqIndex === -1) {
113
+ outputLines.push(line);
114
+ continue;
115
+ }
116
+ const key = trimmed.slice(0, eqIndex).trim();
117
+ if (key in vars) {
118
+ outputLines.push(key + "=" + quoteValue(vars[key]));
119
+ updatedKeys.add(key);
120
+ } else outputLines.push(line);
121
+ }
122
+ for (const [key, value] of Object.entries(vars)) if (!updatedKeys.has(key)) outputLines.push(key + "=" + quoteValue(value));
123
+ writeFileSync(filePath, outputLines.join("\n").trimEnd() + "\n");
124
+ }
125
+ /**
126
+ * Set a single var in .dev.vars
127
+ */
128
+ function setDevVar(key, value, dir = process.cwd()) {
129
+ const vars = readDevVars(dir);
130
+ vars[key] = value;
131
+ writeDevVars(vars, dir);
132
+ }
133
+
134
+ //#endregion
135
+ //#region src/cli/commands/secret/jwt.ts
136
+ /**
137
+ * JWT secret generation command
138
+ */
139
+ const jwtCommand = defineCommand({
140
+ meta: {
141
+ name: "jwt",
142
+ description: "Generate and set JWT signing secret"
143
+ },
144
+ args: { local: {
145
+ type: "boolean",
146
+ description: "Write to .dev.vars instead of wrangler secrets",
147
+ default: false
148
+ } },
149
+ async run({ args }) {
150
+ p.intro("Generate JWT Secret");
151
+ const secret = randomBytes(32).toString("base64");
152
+ if (args.local) {
153
+ setDevVar("JWT_SECRET", secret);
154
+ p.outro("JWT_SECRET written to .dev.vars");
155
+ } else {
156
+ const spinner = p.spinner();
157
+ spinner.start("Setting JWT_SECRET via wrangler...");
158
+ try {
159
+ await setSecret("JWT_SECRET", secret);
160
+ spinner.stop("JWT_SECRET set successfully");
161
+ p.outro("Done!");
162
+ } catch (error) {
163
+ spinner.stop("Failed to set JWT_SECRET");
164
+ p.log.error(String(error));
165
+ process.exit(1);
166
+ }
167
+ }
168
+ }
169
+ });
170
+
171
+ //#endregion
172
+ //#region src/cli/commands/secret/password.ts
173
+ /**
174
+ * Password hash generation command
175
+ */
176
+ const passwordCommand = defineCommand({
177
+ meta: {
178
+ name: "password",
179
+ description: "Set account password (stored as bcrypt hash)"
180
+ },
181
+ args: { local: {
182
+ type: "boolean",
183
+ description: "Write to .dev.vars instead of wrangler secrets",
184
+ default: false
185
+ } },
186
+ async run({ args }) {
187
+ p.intro("Set Account Password");
188
+ const password = await p.password({ message: "Enter password:" });
189
+ if (p.isCancel(password)) {
190
+ p.cancel("Cancelled");
191
+ process.exit(0);
192
+ }
193
+ const confirm = await p.password({ message: "Confirm password:" });
194
+ if (p.isCancel(confirm)) {
195
+ p.cancel("Cancelled");
196
+ process.exit(0);
197
+ }
198
+ if (password !== confirm) {
199
+ p.log.error("Passwords do not match");
200
+ process.exit(1);
201
+ }
202
+ const spinner = p.spinner();
203
+ spinner.start("Hashing password...");
204
+ const passwordHash = await bcrypt.hash(password, 10);
205
+ spinner.stop("Password hashed");
206
+ if (args.local) {
207
+ setDevVar("PASSWORD_HASH", passwordHash);
208
+ p.outro("PASSWORD_HASH written to .dev.vars");
209
+ } else {
210
+ spinner.start("Setting PASSWORD_HASH via wrangler...");
211
+ try {
212
+ await setSecret("PASSWORD_HASH", passwordHash);
213
+ spinner.stop("PASSWORD_HASH set successfully");
214
+ p.outro("Done!");
215
+ } catch (error) {
216
+ spinner.stop("Failed to set PASSWORD_HASH");
217
+ p.log.error(String(error));
218
+ process.exit(1);
219
+ }
220
+ }
221
+ }
222
+ });
223
+
224
+ //#endregion
225
+ //#region src/cli/commands/secret/key.ts
226
+ /**
227
+ * Signing key generation command
228
+ */
229
+ const keyCommand = defineCommand({
230
+ meta: {
231
+ name: "key",
232
+ description: "Generate and set signing keypair"
233
+ },
234
+ args: { local: {
235
+ type: "boolean",
236
+ description: "Write to .dev.vars instead of wrangler secrets/config",
237
+ default: false
238
+ } },
239
+ async run({ args }) {
240
+ p.intro("Generate Signing Keypair");
241
+ const spinner = p.spinner();
242
+ spinner.start("Generating secp256k1 keypair...");
243
+ const keypair = await Secp256k1Keypair.create({ exportable: true });
244
+ const privateKeyJwk = await keypair.export();
245
+ const publicKeyMultibase = keypair.did().replace("did:key:", "");
246
+ spinner.stop("Keypair generated");
247
+ const privateKeyJson = JSON.stringify(privateKeyJwk);
248
+ if (args.local) {
249
+ setDevVar("SIGNING_KEY", privateKeyJson);
250
+ setDevVar("SIGNING_KEY_PUBLIC", publicKeyMultibase);
251
+ p.outro("SIGNING_KEY and SIGNING_KEY_PUBLIC written to .dev.vars");
252
+ } else {
253
+ spinner.start("Setting SIGNING_KEY via wrangler secret...");
254
+ try {
255
+ await setSecret("SIGNING_KEY", privateKeyJson);
256
+ spinner.stop("SIGNING_KEY set");
257
+ spinner.start("Setting SIGNING_KEY_PUBLIC in wrangler.jsonc...");
258
+ setVar("SIGNING_KEY_PUBLIC", publicKeyMultibase);
259
+ spinner.stop("SIGNING_KEY_PUBLIC set");
260
+ p.outro("Done!");
261
+ } catch (error) {
262
+ spinner.stop("Failed");
263
+ p.log.error(String(error));
264
+ process.exit(1);
265
+ }
266
+ }
267
+ p.log.info("Public key (for DID document): " + publicKeyMultibase);
268
+ }
269
+ });
270
+
271
+ //#endregion
272
+ //#region src/cli/commands/secret/index.ts
273
+ /**
274
+ * Secret management commands
275
+ */
276
+ const secretCommand = defineCommand({
277
+ meta: {
278
+ name: "secret",
279
+ description: "Manage PDS secrets"
280
+ },
281
+ subCommands: {
282
+ jwt: jwtCommand,
283
+ password: passwordCommand,
284
+ key: keyCommand
285
+ }
286
+ });
287
+
288
+ //#endregion
289
+ //#region src/cli/commands/init.ts
290
+ /**
291
+ * Interactive PDS setup wizard
292
+ */
293
+ /**
294
+ * Run wrangler types to regenerate TypeScript types
295
+ */
296
+ function runWranglerTypes() {
297
+ return new Promise((resolve$1, reject) => {
298
+ const child = spawn("wrangler", ["types"], { stdio: "inherit" });
299
+ child.on("close", (code) => {
300
+ if (code === 0) resolve$1();
301
+ else reject(/* @__PURE__ */ new Error(`wrangler types failed with code ${code}`));
302
+ });
303
+ child.on("error", reject);
304
+ });
305
+ }
306
+ const initCommand = defineCommand({
307
+ meta: {
308
+ name: "init",
309
+ description: "Interactive PDS setup wizard"
310
+ },
311
+ args: { production: {
312
+ type: "boolean",
313
+ description: "Deploy secrets to Cloudflare (prompts to reuse .dev.vars values)",
314
+ default: false
315
+ } },
316
+ async run({ args }) {
317
+ p.intro("PDS Setup Wizard");
318
+ const isProduction = args.production;
319
+ if (isProduction) p.log.info("Production mode: secrets will be deployed via wrangler");
320
+ const wranglerVars = getVars();
321
+ const devVars = readDevVars();
322
+ const currentVars = {
323
+ ...devVars,
324
+ ...wranglerVars
325
+ };
326
+ const hostname = await p.text({
327
+ message: "PDS hostname:",
328
+ placeholder: "pds.example.com",
329
+ initialValue: currentVars.PDS_HOSTNAME || "",
330
+ validate: (v) => !v ? "Hostname is required" : void 0
331
+ });
332
+ if (p.isCancel(hostname)) {
333
+ p.cancel("Cancelled");
334
+ process.exit(0);
335
+ }
336
+ const handle = await p.text({
337
+ message: "Account handle:",
338
+ placeholder: "alice." + hostname,
339
+ initialValue: currentVars.HANDLE || "",
340
+ validate: (v) => !v ? "Handle is required" : void 0
341
+ });
342
+ if (p.isCancel(handle)) {
343
+ p.cancel("Cancelled");
344
+ process.exit(0);
345
+ }
346
+ const didDefault = "did:web:" + hostname;
347
+ const did = await p.text({
348
+ message: "Account DID:",
349
+ placeholder: didDefault,
350
+ initialValue: currentVars.DID || didDefault,
351
+ validate: (v) => {
352
+ if (!v) return "DID is required";
353
+ if (!v.startsWith("did:")) return "DID must start with did:";
354
+ }
355
+ });
356
+ if (p.isCancel(did)) {
357
+ p.cancel("Cancelled");
358
+ process.exit(0);
359
+ }
360
+ const spinner = p.spinner();
361
+ let authToken;
362
+ let signingKey;
363
+ let signingKeyPublic;
364
+ let jwtSecret;
365
+ let passwordHash;
366
+ if (isProduction) {
367
+ authToken = await getOrGenerateSecret("AUTH_TOKEN", devVars, async () => {
368
+ spinner.start("Generating auth token...");
369
+ const token = randomBytes(32).toString("base64url");
370
+ spinner.stop("Auth token generated");
371
+ return token;
372
+ });
373
+ signingKey = await getOrGenerateSecret("SIGNING_KEY", devVars, async () => {
374
+ spinner.start("Generating signing keypair...");
375
+ const keypair = await Secp256k1Keypair.create({ exportable: true });
376
+ const key = JSON.stringify(await keypair.export());
377
+ spinner.stop("Signing keypair generated");
378
+ return key;
379
+ });
380
+ signingKeyPublic = (await Secp256k1Keypair.import(JSON.parse(signingKey))).did().replace("did:key:", "");
381
+ jwtSecret = await getOrGenerateSecret("JWT_SECRET", devVars, async () => {
382
+ spinner.start("Generating JWT secret...");
383
+ const secret = randomBytes(32).toString("base64");
384
+ spinner.stop("JWT secret generated");
385
+ return secret;
386
+ });
387
+ passwordHash = await getOrGenerateSecret("PASSWORD_HASH", devVars, async () => {
388
+ const password = await p.password({ message: "Account password:" });
389
+ if (p.isCancel(password)) {
390
+ p.cancel("Cancelled");
391
+ process.exit(0);
392
+ }
393
+ const confirm = await p.password({ message: "Confirm password:" });
394
+ if (p.isCancel(confirm)) {
395
+ p.cancel("Cancelled");
396
+ process.exit(0);
397
+ }
398
+ if (password !== confirm) {
399
+ p.log.error("Passwords do not match");
400
+ process.exit(1);
401
+ }
402
+ spinner.start("Hashing password...");
403
+ const hash = await bcrypt.hash(password, 10);
404
+ spinner.stop("Password hashed");
405
+ return hash;
406
+ });
407
+ } else {
408
+ const password = await p.password({ message: "Account password:" });
409
+ if (p.isCancel(password)) {
410
+ p.cancel("Cancelled");
411
+ process.exit(0);
412
+ }
413
+ const confirm = await p.password({ message: "Confirm password:" });
414
+ if (p.isCancel(confirm)) {
415
+ p.cancel("Cancelled");
416
+ process.exit(0);
417
+ }
418
+ if (password !== confirm) {
419
+ p.log.error("Passwords do not match");
420
+ process.exit(1);
421
+ }
422
+ spinner.start("Hashing password...");
423
+ passwordHash = await bcrypt.hash(password, 10);
424
+ spinner.stop("Password hashed");
425
+ spinner.start("Generating JWT secret...");
426
+ jwtSecret = randomBytes(32).toString("base64");
427
+ spinner.stop("JWT secret generated");
428
+ spinner.start("Generating auth token...");
429
+ authToken = randomBytes(32).toString("base64url");
430
+ spinner.stop("Auth token generated");
431
+ spinner.start("Generating signing keypair...");
432
+ const keypair = await Secp256k1Keypair.create({ exportable: true });
433
+ signingKey = JSON.stringify(await keypair.export());
434
+ signingKeyPublic = keypair.did().replace("did:key:", "");
435
+ spinner.stop("Signing keypair generated");
436
+ }
437
+ spinner.start("Updating wrangler.jsonc...");
438
+ setVars({
439
+ PDS_HOSTNAME: hostname,
440
+ DID: did,
441
+ HANDLE: handle,
442
+ SIGNING_KEY_PUBLIC: signingKeyPublic
443
+ });
444
+ spinner.stop("wrangler.jsonc updated");
445
+ if (isProduction) {
446
+ spinner.start("Setting AUTH_TOKEN...");
447
+ await setSecret("AUTH_TOKEN", authToken);
448
+ spinner.stop("AUTH_TOKEN set");
449
+ spinner.start("Setting SIGNING_KEY...");
450
+ await setSecret("SIGNING_KEY", signingKey);
451
+ spinner.stop("SIGNING_KEY set");
452
+ spinner.start("Setting JWT_SECRET...");
453
+ await setSecret("JWT_SECRET", jwtSecret);
454
+ spinner.stop("JWT_SECRET set");
455
+ spinner.start("Setting PASSWORD_HASH...");
456
+ await setSecret("PASSWORD_HASH", passwordHash);
457
+ spinner.stop("PASSWORD_HASH set");
458
+ } else {
459
+ spinner.start("Writing secrets to .dev.vars...");
460
+ writeDevVars({
461
+ AUTH_TOKEN: authToken,
462
+ SIGNING_KEY: signingKey,
463
+ JWT_SECRET: jwtSecret,
464
+ PASSWORD_HASH: passwordHash
465
+ });
466
+ spinner.stop("Secrets written to .dev.vars");
467
+ }
468
+ spinner.start("Generating TypeScript types...");
469
+ try {
470
+ await runWranglerTypes();
471
+ spinner.stop("TypeScript types generated");
472
+ } catch {
473
+ spinner.stop("Failed to generate types (wrangler types)");
474
+ }
475
+ p.note([
476
+ "Configuration summary:",
477
+ "",
478
+ " PDS_HOSTNAME: " + hostname,
479
+ " DID: " + did,
480
+ " HANDLE: " + handle,
481
+ " SIGNING_KEY_PUBLIC: " + signingKeyPublic,
482
+ "",
483
+ isProduction ? "Secrets deployed to Cloudflare" : "Secrets saved to .dev.vars",
484
+ "",
485
+ "Auth token (save this!):",
486
+ " " + authToken
487
+ ].join("\n"), "Setup Complete");
488
+ if (isProduction) p.outro("Your PDS is configured! Run 'wrangler deploy' to deploy.");
489
+ else p.outro("Your PDS is configured! Run 'pnpm dev' to start locally.");
490
+ }
491
+ });
492
+ /**
493
+ * Helper to get a secret from .dev.vars or generate a new one
494
+ */
495
+ async function getOrGenerateSecret(name, devVars, generate) {
496
+ if (devVars[name]) {
497
+ const useExisting = await p.confirm({
498
+ message: `Use ${name} from .dev.vars?`,
499
+ initialValue: true
500
+ });
501
+ if (p.isCancel(useExisting)) {
502
+ p.cancel("Cancelled");
503
+ process.exit(0);
504
+ }
505
+ if (useExisting) return devVars[name];
506
+ }
507
+ return generate();
508
+ }
509
+
510
+ //#endregion
511
+ //#region src/cli/index.ts
512
+ /**
513
+ * PDS CLI - Setup and management for AT Protocol PDS on Cloudflare Workers
514
+ */
515
+ runMain(defineCommand({
516
+ meta: {
517
+ name: "pds",
518
+ version: "0.0.0",
519
+ description: "AT Protocol PDS setup and management CLI"
520
+ },
521
+ subCommands: {
522
+ init: initCommand,
523
+ secret: secretCommand
524
+ }
525
+ }));
526
+
527
+ //#endregion
528
+ export { };