@indecisivedev/openplan 0.2.0-alpha.2

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.
Files changed (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +120 -0
  3. package/dist/access-policy.d.ts +35 -0
  4. package/dist/access-policy.js +107 -0
  5. package/dist/access-policy.js.map +1 -0
  6. package/dist/access-store.d.ts +33 -0
  7. package/dist/access-store.js +212 -0
  8. package/dist/access-store.js.map +1 -0
  9. package/dist/admin.d.ts +1 -0
  10. package/dist/admin.js +13 -0
  11. package/dist/admin.js.map +1 -0
  12. package/dist/api.d.ts +2 -0
  13. package/dist/api.js +867 -0
  14. package/dist/api.js.map +1 -0
  15. package/dist/bootstrap-admin.d.ts +1 -0
  16. package/dist/bootstrap-admin.js +35 -0
  17. package/dist/bootstrap-admin.js.map +1 -0
  18. package/dist/browser-security.d.ts +5 -0
  19. package/dist/browser-security.js +53 -0
  20. package/dist/browser-security.js.map +1 -0
  21. package/dist/celld-quickstart.d.ts +51 -0
  22. package/dist/celld-quickstart.js +1175 -0
  23. package/dist/celld-quickstart.js.map +1 -0
  24. package/dist/cli-api-client.d.ts +78 -0
  25. package/dist/cli-api-client.js +272 -0
  26. package/dist/cli-api-client.js.map +1 -0
  27. package/dist/cli-credentials.d.ts +47 -0
  28. package/dist/cli-credentials.js +447 -0
  29. package/dist/cli-credentials.js.map +1 -0
  30. package/dist/cli-endpoint.d.ts +19 -0
  31. package/dist/cli-endpoint.js +56 -0
  32. package/dist/cli-endpoint.js.map +1 -0
  33. package/dist/cli-program.d.ts +11 -0
  34. package/dist/cli-program.js +934 -0
  35. package/dist/cli-program.js.map +1 -0
  36. package/dist/cli.d.ts +2 -0
  37. package/dist/cli.js +7 -0
  38. package/dist/cli.js.map +1 -0
  39. package/dist/client-ip.d.ts +4 -0
  40. package/dist/client-ip.js +49 -0
  41. package/dist/client-ip.js.map +1 -0
  42. package/dist/config.d.ts +35 -0
  43. package/dist/config.js +113 -0
  44. package/dist/config.js.map +1 -0
  45. package/dist/crypto.d.ts +3 -0
  46. package/dist/crypto.js +11 -0
  47. package/dist/crypto.js.map +1 -0
  48. package/dist/database.d.ts +15 -0
  49. package/dist/database.js +155 -0
  50. package/dist/database.js.map +1 -0
  51. package/dist/db.d.ts +31 -0
  52. package/dist/db.js +388 -0
  53. package/dist/db.js.map +1 -0
  54. package/dist/drafts.d.ts +43 -0
  55. package/dist/drafts.js +90 -0
  56. package/dist/drafts.js.map +1 -0
  57. package/dist/feature-settings.d.ts +7 -0
  58. package/dist/feature-settings.js +32 -0
  59. package/dist/feature-settings.js.map +1 -0
  60. package/dist/generated/annotation-client.d.ts +1 -0
  61. package/dist/generated/annotation-client.js +2 -0
  62. package/dist/generated/annotation-client.js.map +1 -0
  63. package/dist/html-policy.d.ts +15 -0
  64. package/dist/html-policy.js +172 -0
  65. package/dist/html-policy.js.map +1 -0
  66. package/dist/ids.d.ts +2 -0
  67. package/dist/ids.js +10 -0
  68. package/dist/ids.js.map +1 -0
  69. package/dist/local-auth.d.ts +36 -0
  70. package/dist/local-auth.js +215 -0
  71. package/dist/local-auth.js.map +1 -0
  72. package/dist/maintenance.d.ts +1 -0
  73. package/dist/maintenance.js +18 -0
  74. package/dist/maintenance.js.map +1 -0
  75. package/dist/oidc.d.ts +39 -0
  76. package/dist/oidc.js +194 -0
  77. package/dist/oidc.js.map +1 -0
  78. package/dist/public-url.d.ts +17 -0
  79. package/dist/public-url.js +96 -0
  80. package/dist/public-url.js.map +1 -0
  81. package/dist/rate-limit.d.ts +25 -0
  82. package/dist/rate-limit.js +55 -0
  83. package/dist/rate-limit.js.map +1 -0
  84. package/dist/reconcile.d.ts +5 -0
  85. package/dist/reconcile.js +18 -0
  86. package/dist/reconcile.js.map +1 -0
  87. package/dist/render-annotations.d.ts +10 -0
  88. package/dist/render-annotations.js +73 -0
  89. package/dist/render-annotations.js.map +1 -0
  90. package/dist/render-web.d.ts +74 -0
  91. package/dist/render-web.js +439 -0
  92. package/dist/render-web.js.map +1 -0
  93. package/dist/render.d.ts +4 -0
  94. package/dist/render.js +85 -0
  95. package/dist/render.js.map +1 -0
  96. package/dist/server.d.ts +1 -0
  97. package/dist/server.js +26 -0
  98. package/dist/server.js.map +1 -0
  99. package/dist/shoo.d.ts +26 -0
  100. package/dist/shoo.js +92 -0
  101. package/dist/shoo.js.map +1 -0
  102. package/dist/sqlite-migrations.d.ts +6 -0
  103. package/dist/sqlite-migrations.js +214 -0
  104. package/dist/sqlite-migrations.js.map +1 -0
  105. package/dist/storage.d.ts +14 -0
  106. package/dist/storage.js +86 -0
  107. package/dist/storage.js.map +1 -0
  108. package/dist/types.d.ts +24 -0
  109. package/dist/types.js +2 -0
  110. package/dist/types.js.map +1 -0
  111. package/dist/upload-state.d.ts +19 -0
  112. package/dist/upload-state.js +118 -0
  113. package/dist/upload-state.js.map +1 -0
  114. package/dist/web-auth.d.ts +33 -0
  115. package/dist/web-auth.js +122 -0
  116. package/dist/web-auth.js.map +1 -0
  117. package/dist/web.d.ts +2 -0
  118. package/dist/web.js +550 -0
  119. package/dist/web.js.map +1 -0
  120. package/package.json +70 -0
@@ -0,0 +1,1175 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { spawn } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { createInterface } from "node:readline/promises";
7
+ import { normalizeApiUrl } from "./cli-endpoint.js";
8
+ import { CredentialStore, CredentialError, validateApiKey } from "./cli-credentials.js";
9
+ import { createEnrollmentApiClient } from "./cli-api-client.js";
10
+ export const QUICKSTART_STAGES = [
11
+ "preflight",
12
+ "prepare",
13
+ "deploy",
14
+ "activate",
15
+ "migrate",
16
+ "prove"
17
+ ];
18
+ const DEFAULT_COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
19
+ const DEFAULT_REQUEST_TIMEOUT_MS = 20 * 1000;
20
+ const READINESS_TIMEOUT_MS = 60 * 1000;
21
+ const READINESS_RETRY_DELAY_MS = 1 * 1000;
22
+ const MAX_CAPTURED_OUTPUT = 128 * 1024;
23
+ const MAX_PROBE_RESPONSE_BYTES = 256 * 1024;
24
+ const SECRET_KEYS = [
25
+ "BOOTSTRAP_API_KEY",
26
+ "SESSION_SECRET",
27
+ "OPENPLAN_SHARE_SECRET",
28
+ "OPENPLAN_RECONCILE_SECRET",
29
+ "DRAFTS_S3_ACCESS_KEY_ID",
30
+ "DRAFTS_S3_SECRET_ACCESS_KEY"
31
+ ];
32
+ /** Safe, actionable diagnostics: never include raw transport or child errors. */
33
+ export class QuickstartError extends Error {
34
+ constructor(message) { super(message); this.name = "QuickstartError"; }
35
+ }
36
+ export async function runCelldQuickstart(options) {
37
+ const io = options.io || defaultIo();
38
+ const executor = options.executor || createDefaultExecutor();
39
+ const inputs = await resolveInputs(options, io);
40
+ const dryRun = options.dryRun === true;
41
+ if (inputs.nodeHosts.length > 0 && !inputs.remoteVarsFile) {
42
+ throw new QuickstartError("Remote nodes require --remote-vars-file so the protected Worker variables can be copied without printing them.");
43
+ }
44
+ // Read and validate retained effective credentials even for --reset. A reset
45
+ // is never authority to rotate or rebind a key to a new target.
46
+ const recordedState = readState(inputs.stateFile);
47
+ const previousState = options.reset ? null : recordedState;
48
+ if (previousState && !sameStateInputs(previousState.inputs, inputs)) {
49
+ throw new QuickstartError(`Quickstart state belongs to different inputs: ${inputs.stateFile}. Review the effective bootstrap origin before using --reset.`);
50
+ }
51
+ // A completed resume does not enroll old credentials or rewrite any file.
52
+ if (previousState?.completedStages.includes("prove") && !dryRun) {
53
+ io.log(`Openplan celld quickstart is already complete; state: ${inputs.stateFile}`);
54
+ return;
55
+ }
56
+ const prepared = prepareConfig(inputs, recordedState);
57
+ io.log(`Target: ${inputs.publicUrl}. After the full private-sample proof, enroll this origin's bootstrap credential and replace the CLI default endpoint; other origins' credentials are preserved.`);
58
+ // All target/binding/config validation above is read-only, including reset.
59
+ if (!dryRun) {
60
+ if (options.reset)
61
+ removeStateFile(inputs.stateFile);
62
+ if (!recordedState && prepared.vars.path !== inputs.varsFile && !fs.existsSync(prepared.vars.path)) {
63
+ ensureDirectory(path.dirname(prepared.vars.path), 0o700);
64
+ writeText(prepared.vars.path, serializeVars(prepared.vars.values), 0o600);
65
+ }
66
+ }
67
+ const state = previousState || {
68
+ version: 2,
69
+ inputs: stateInputs(inputs),
70
+ effectiveVarsFile: prepared.vars.path,
71
+ completedStages: [],
72
+ updatedAt: new Date().toISOString()
73
+ };
74
+ if (state.effectiveVarsFile !== prepared.vars.path) {
75
+ throw new QuickstartError(`The protected variables file recorded by quickstart is missing or changed: ${state.effectiveVarsFile}. Re-run with --reset after reviewing it.`);
76
+ }
77
+ // Incomplete retries recheck the fleet and activation before any pending migration.
78
+ if (!dryRun && !state.completedStages.includes("prove")) {
79
+ state.completedStages = state.completedStages.filter(stage => stage !== "preflight" &&
80
+ !(stage === "activate" && !state.completedStages.includes("migrate")));
81
+ }
82
+ const stages = dryRun
83
+ ? QUICKSTART_STAGES
84
+ : QUICKSTART_STAGES.filter((stage) => !state.completedStages.includes(stage));
85
+ if (stages.length === 0) {
86
+ io.log(`Openplan celld quickstart is already complete; state: ${inputs.stateFile}`);
87
+ return;
88
+ }
89
+ for (const stage of stages) {
90
+ io.log(`\n== ${stage} ==`);
91
+ switch (stage) {
92
+ case "preflight": {
93
+ const minor = await preflight(inputs, prepared.vars, executor, io, dryRun, options.fetchImpl || fetch);
94
+ if (state.completedStages.includes("deploy") && state.celldMinor && minor !== state.celldMinor) {
95
+ throw new QuickstartError("The celld version changed after publication. Review the deployed configuration before starting a new run with --reset.");
96
+ }
97
+ state.celldMinor = minor;
98
+ break;
99
+ }
100
+ case "prepare":
101
+ await prepare(inputs, executor, io, dryRun);
102
+ break;
103
+ case "deploy":
104
+ state.deploymentVersion = await deploy(inputs, prepared.vars, executor, io, dryRun, options.yes === true, state);
105
+ break;
106
+ case "activate":
107
+ await activate(inputs, executor, io, dryRun, options.fetchImpl || fetch, state);
108
+ break;
109
+ case "migrate":
110
+ await migrate(inputs, executor, io, dryRun, state);
111
+ break;
112
+ case "prove":
113
+ await prove(inputs, prepared.apiKey, options.homeDir || os.homedir(), options.fetchImpl || fetch, io, dryRun);
114
+ break;
115
+ }
116
+ if (!dryRun) {
117
+ if (stage === "preflight")
118
+ state.detectedFleetMode = "dedicated";
119
+ if (!state.completedStages.includes(stage))
120
+ state.completedStages.push(stage);
121
+ state.updatedAt = new Date().toISOString();
122
+ try {
123
+ writeState(inputs.stateFile, state);
124
+ }
125
+ catch {
126
+ if (stage === "prove") {
127
+ throw new QuickstartError("Proof and CLI credential/default commit succeeded, but the completion checkpoint failed. The connection remains saved; inspect local status before retrying. Retrying may create another private sample.");
128
+ }
129
+ throw new QuickstartError("Quickstart progress checkpoint failed. Inspect the local state file and completed deployment actions before retrying.");
130
+ }
131
+ io.log(`PASS: ${stage}`);
132
+ }
133
+ }
134
+ if (dryRun) {
135
+ io.log("\nDry-run complete; no fleet mutation, CLI credential write, or sample upload was performed.");
136
+ }
137
+ else {
138
+ io.log("\nOpenplan celld quickstart completed successfully.");
139
+ }
140
+ }
141
+ export function createDefaultExecutor() {
142
+ return {
143
+ run(file, args, options = {}) {
144
+ return new Promise((resolve, reject) => {
145
+ const child = spawn(file, args, {
146
+ cwd: options.cwd,
147
+ shell: false,
148
+ stdio: ["ignore", "pipe", "pipe"]
149
+ });
150
+ let stdout = "";
151
+ let stderr = "";
152
+ let settled = false;
153
+ let timer;
154
+ const finish = (result) => {
155
+ if (settled)
156
+ return;
157
+ settled = true;
158
+ if (timer)
159
+ clearTimeout(timer);
160
+ resolve(result);
161
+ };
162
+ child.stdout.setEncoding("utf8");
163
+ child.stderr.setEncoding("utf8");
164
+ child.stdout.on("data", (chunk) => {
165
+ stdout = appendOutput(stdout, chunk);
166
+ });
167
+ child.stderr.on("data", (chunk) => {
168
+ stderr = appendOutput(stderr, chunk);
169
+ });
170
+ child.once("error", (error) => {
171
+ if (settled)
172
+ return;
173
+ settled = true;
174
+ if (timer)
175
+ clearTimeout(timer);
176
+ reject(error);
177
+ });
178
+ child.once("close", (exitCode) => finish({ exitCode, stdout, stderr }));
179
+ if (options.timeoutMs && options.timeoutMs > 0) {
180
+ timer = setTimeout(() => {
181
+ child.kill();
182
+ finish({ exitCode: null, stdout, stderr: `${stderr}\nTimed out after ${options.timeoutMs}ms.` });
183
+ }, options.timeoutMs);
184
+ }
185
+ });
186
+ }
187
+ };
188
+ }
189
+ async function resolveInputs(options, io) {
190
+ const repoRoot = path.resolve(options.repoRoot);
191
+ const homeDir = path.resolve(options.homeDir || os.homedir());
192
+ const stateFile = path.resolve(options.stateFile || path.join(homeDir, ".openplan", "celld-quickstart.json"));
193
+ assertOutsideCheckout(stateFile, repoRoot, "state file");
194
+ let varsFile = options.varsFile || process.env.CELLD_VARS_FILE || "";
195
+ if (!varsFile) {
196
+ varsFile = await promptForValue("protected Worker variables file", "--vars-file", options.nonInteractive === true, io);
197
+ }
198
+ varsFile = path.resolve(varsFile);
199
+ assertOutsideCheckout(varsFile, repoRoot, "variables file");
200
+ const rawVars = readVars(varsFile);
201
+ let fleetBucket = options.fleetBucket || process.env.CELLD_BUCKET || rawVars.values.CELLD_BUCKET || "";
202
+ if (!fleetBucket) {
203
+ fleetBucket = await promptForValue("celld fleet bucket", "--fleet-bucket", options.nonInteractive === true, io);
204
+ }
205
+ let publicUrl = options.publicUrl || process.env.OPENPLAN_PUBLIC_URL || rawVars.values.PUBLIC_BASE_URL || "";
206
+ if (!publicUrl) {
207
+ publicUrl = await promptForValue("public Openplan URL", "--public-url", options.nonInteractive === true, io);
208
+ }
209
+ const nodeHosts = uniqueHosts([...(options.nodes || []), ...(options.remoteHosts || [])]);
210
+ for (const host of nodeHosts)
211
+ validateHost(host);
212
+ const remoteVarsFile = options.remoteVarsFile ? validateRemotePath(options.remoteVarsFile) : null;
213
+ const restartProgram = options.restartProgram || defaultRestartProgram(nodeHosts.length > 0);
214
+ const restartArgs = options.restartArgs && options.restartArgs.length > 0
215
+ ? [...options.restartArgs]
216
+ : defaultRestartArgs(restartProgram);
217
+ const fleetMode = options.fleetMode || "auto";
218
+ if (fleetMode !== "auto" && fleetMode !== "dedicated" && fleetMode !== "shared") {
219
+ throw new QuickstartError("Unknown fleet mode. Use auto, dedicated, or shared.");
220
+ }
221
+ validateFleetBucket(fleetBucket);
222
+ const normalizedPublicUrl = normalizeApiUrl(publicUrl, "Public URL");
223
+ validateProtectedFile(varsFile);
224
+ validateRepository(repoRoot);
225
+ return {
226
+ repoRoot,
227
+ fleetBucket,
228
+ publicUrl: normalizedPublicUrl,
229
+ varsFile,
230
+ stateFile,
231
+ fleetMode,
232
+ nodeHosts,
233
+ remoteVarsFile,
234
+ operatorUrls: (options.operatorUrls || []).map(normalizeOperatorUrl),
235
+ restartProgram,
236
+ restartArgs,
237
+ commandTimeoutMs: positiveNumber(options.commandTimeoutMs, DEFAULT_COMMAND_TIMEOUT_MS),
238
+ requestTimeoutMs: positiveNumber(options.requestTimeoutMs, DEFAULT_REQUEST_TIMEOUT_MS),
239
+ cliVersion: options.cliVersion || "quickstart",
240
+ homeDir
241
+ };
242
+ }
243
+ function prepareConfig(inputs, previousState) {
244
+ if (previousState) {
245
+ if (!fs.existsSync(previousState.effectiveVarsFile)) {
246
+ throw new QuickstartError(`Protected variables file from quickstart state does not exist: ${previousState.effectiveVarsFile}`);
247
+ }
248
+ const vars = readVars(previousState.effectiveVarsFile);
249
+ validateVars(vars, inputs.publicUrl);
250
+ return { vars, apiKey: readApiKey(vars) };
251
+ }
252
+ // A failed preflight can leave generated effective variables before the first
253
+ // checkpoint exists. They are retained credentials too, not disposable output.
254
+ const retainedVarsPath = path.join(path.dirname(inputs.stateFile), `${path.basename(inputs.stateFile)}.vars`);
255
+ if (fs.existsSync(retainedVarsPath)) {
256
+ const vars = readVars(retainedVarsPath);
257
+ validateVars(vars, inputs.publicUrl);
258
+ return { vars, apiKey: readApiKey(vars) };
259
+ }
260
+ const source = readVars(inputs.varsFile);
261
+ validateBootstrapBinding(source, inputs.publicUrl);
262
+ const values = { ...source.values };
263
+ let changed = false;
264
+ const setValue = (key, value) => {
265
+ if (values[key] !== value) {
266
+ values[key] = value;
267
+ changed = true;
268
+ }
269
+ };
270
+ setValue("CELLD_BUCKET", inputs.fleetBucket);
271
+ setValue("PUBLIC_BASE_URL", inputs.publicUrl);
272
+ if (!values.OPENPLAN_DRAFT_ACCESS)
273
+ setValue("OPENPLAN_DRAFT_ACCESS", "link");
274
+ if (!values.OPENPLAN_ALLOW_ANONYMOUS_UPLOADS)
275
+ setValue("OPENPLAN_ALLOW_ANONYMOUS_UPLOADS", "false");
276
+ if (!values.OPENPLAN_PASSWORD_ITERATIONS)
277
+ setValue("OPENPLAN_PASSWORD_ITERATIONS", "210000");
278
+ const bootstrap = values.BOOTSTRAP_API_KEY || values.OPENPLAN_BOOTSTRAP_API_KEY;
279
+ setValue("BOOTSTRAP_API_KEY", bootstrap || `op_${randomBytes(32).toString("base64url")}`);
280
+ if (!bootstrap)
281
+ setValue("OPENPLAN_BOOTSTRAP_API_KEY_ORIGIN", inputs.publicUrl);
282
+ setValue("SESSION_SECRET", values.SESSION_SECRET || randomBytes(32).toString("hex"));
283
+ setValue("OPENPLAN_SHARE_SECRET", values.OPENPLAN_SHARE_SECRET || randomBytes(32).toString("hex"));
284
+ setValue("OPENPLAN_RECONCILE_SECRET", values.OPENPLAN_RECONCILE_SECRET || randomBytes(32).toString("hex"));
285
+ const varsPath = changed
286
+ ? path.join(path.dirname(inputs.stateFile), `${path.basename(inputs.stateFile)}.vars`)
287
+ : inputs.varsFile;
288
+ if (changed) {
289
+ assertOutsideCheckout(varsPath, inputs.repoRoot, "generated variables file");
290
+ }
291
+ const vars = { path: varsPath, values };
292
+ validateVars(vars, inputs.publicUrl, !changed);
293
+ return { vars, apiKey: readApiKey(vars) };
294
+ }
295
+ async function preflight(inputs, vars, executor, io, dryRun, fetchImpl) {
296
+ validateVars(vars, inputs.publicUrl, !dryRun || fs.existsSync(vars.path));
297
+ await requireCommand(executor, executable("npm"), inputs.commandTimeoutMs, "npm");
298
+ const remote = inputs.nodeHosts.length > 0;
299
+ if (remote) {
300
+ await requireCommand(executor, executable("ssh"), inputs.commandTimeoutMs, "ssh");
301
+ await requireCommand(executor, executable("scp"), inputs.commandTimeoutMs, "scp");
302
+ }
303
+ else {
304
+ await requireCommand(executor, executable("celld"), inputs.commandTimeoutMs, "celld");
305
+ }
306
+ const version = await runFleetCommand(inputs, executor, ["--version"], io, "celld version");
307
+ const versionMatch = `${version.stdout}\n${version.stderr}`.match(/celld\s+v?(\d+)\.(\d+)\.(\d+)([^\s]*)/i);
308
+ if (!versionMatch)
309
+ throw new QuickstartError("Preflight could not determine the celld version.");
310
+ if (versionMatch[1] !== "0" || !(versionMatch[2] === "3" || versionMatch[2] === "4" || (versionMatch[2] === "5" && versionMatch[3] === "1" && !versionMatch[4]))) {
311
+ throw new QuickstartError(`This quickstart supports celld 0.3.x and 0.4.x, plus pinned 0.5.1; found ${versionMatch[1]}.${versionMatch[2]}.${versionMatch[3]}.`);
312
+ }
313
+ if (versionMatch[2] === "5" && remote) {
314
+ throw new QuickstartError("For celld 0.5.1, run celld on this CLI machine with fleet credentials and one --operator-url per node. SSH --node publication is only supported on 0.3/0.4.");
315
+ }
316
+ if (versionMatch[2] === "5" && Object.keys(process.env).some(name => name === "CELLD_VARS_FILE" || name.startsWith("CELLD_VAR_"))) {
317
+ throw new QuickstartError("celld 0.5.1 removed CELLD_VARS_FILE and CELLD_VAR_* overrides. Unset them in this shell and node services, then pass the protected input with --vars-file; the quickstart publishes Wrangler bindings.");
318
+ }
319
+ for (const host of inputs.nodeHosts.slice(1)) {
320
+ const peer = await runCommand(executor, executable("ssh"), sshArgs(host, executable("celld"), ["--version"]), inputs, io, "celld node version");
321
+ const match = `${peer.stdout}\n${peer.stderr}`.match(/celld\s+v?(\d+)\.(\d+)\.(\d+)/i);
322
+ if (!match || match[1] !== versionMatch[1] || match[2] !== versionMatch[2]) {
323
+ throw new QuickstartError("Every intended node must use the same supported celld minor version. Do not mix celld 0.3 and 0.4 or 0.5 in one fleet.");
324
+ }
325
+ }
326
+ const diagnose = await runFleetCommand(inputs, executor, ["diagnose", "--bucket", inputs.fleetBucket, "--read-only"], io, "celld fleet diagnose");
327
+ const mode = classifyFleet(`${diagnose.stdout}\n${diagnose.stderr}`);
328
+ if (mode === "shared") {
329
+ throw new QuickstartError("Preflight detected a shared celld fleet. The generic quickstart stops here; use an operator-specific named-service workflow.");
330
+ }
331
+ if (inputs.fleetMode === "shared") {
332
+ throw new QuickstartError("--fleet-mode shared is not supported by the generic quickstart; use a named-service workflow instead.");
333
+ }
334
+ if (inputs.fleetMode === "auto" && mode !== "dedicated") {
335
+ throw new QuickstartError("Preflight could not prove that the fleet is dedicated. Review the read-only diagnose output and re-run with --fleet-mode dedicated.");
336
+ }
337
+ const leaseCount = `${diagnose.stdout}\n${diagnose.stderr}`.match(/\bfleet:?\s+(\d+)\s+node lease(?:\(s\)|s)? enumerated/i);
338
+ if (!leaseCount) {
339
+ throw new QuickstartError("Preflight could not verify active nodes from celld diagnose. Run the displayed read-only diagnosis and confirm a healthy running fleet before retrying.");
340
+ }
341
+ if (Number(leaseCount[1]) === 0) {
342
+ throw new QuickstartError("The fleet has no running node leases. Start a celld node for this dedicated fleet and verify its read-only diagnosis before retrying; D1 migrations require a running node. No deployment was performed.");
343
+ }
344
+ if (versionMatch[2] !== "3") {
345
+ if (inputs.operatorUrls.length !== Number(leaseCount[1]) ||
346
+ new Set(inputs.operatorUrls).size !== inputs.operatorUrls.length) {
347
+ throw new QuickstartError("celld 0.4/0.5.1 requires one distinct --operator-url per node lease, reachable from this CLI through a trusted private connection or local tunnel.");
348
+ }
349
+ for (const url of inputs.operatorUrls)
350
+ await readOperatorState(inputs, url, fetchImpl);
351
+ }
352
+ io.log(`Detected celld ${versionMatch[1]}.${versionMatch[2]}.${versionMatch[3]} on a dedicated fleet with ${leaseCount[1]} node lease(s).`);
353
+ return versionMatch[2];
354
+ }
355
+ async function prepare(inputs, executor, io, dryRun) {
356
+ if (dryRun) {
357
+ describeCommand(executable("npm"), ["ci"], io, "DRY-RUN");
358
+ describeCommand(executable("npm"), ["run", "worker:build"], io, "DRY-RUN");
359
+ }
360
+ else {
361
+ await runLocalCommand(executor, inputs, ["ci"], io, "npm ci");
362
+ await runLocalCommand(executor, inputs, ["run", "worker:build"], io, "npm run worker:build");
363
+ }
364
+ const manifest = fs.readFileSync(path.join(inputs.repoRoot, "worker", "wrangler.jsonc"), "utf8");
365
+ for (const marker of ["\"main\"", "\"d1_databases\"", "\"migrations_dir\""]) {
366
+ if (!manifest.includes(marker)) {
367
+ throw new QuickstartError(`Prepare could not validate worker/wrangler.jsonc; missing ${marker}.`);
368
+ }
369
+ }
370
+ if (dryRun)
371
+ io.log("Prepared local dependencies and Worker build for the deployment preview.");
372
+ }
373
+ async function deploy(inputs, vars, executor, io, dryRun, confirmed, state) {
374
+ const project = state.celldMinor === "5" ? privateWorkerProject(inputs) : "worker";
375
+ const deployArgs = ["deploy", project, "--bucket", inputs.fleetBucket, "--json"];
376
+ if (dryRun) {
377
+ if (state.celldMinor === "5") {
378
+ io.log(`DRY-RUN: create protected Worker project at ${project}; variables become fleet deployment bindings. Every fleet-storage reader can read these secrets.`);
379
+ }
380
+ else
381
+ await distributeVars(inputs, vars.path, executor, io, true);
382
+ describeFleetCommand(inputs, deployArgs, io, "DRY-RUN");
383
+ return "preview";
384
+ }
385
+ if (!confirmed) {
386
+ const approved = await io.confirm(`Deploy, activate the Worker, and apply D1 migrations to this dedicated fleet; then, after full proof, enroll the bootstrap credential for ${inputs.publicUrl} and replace the CLI default endpoint? [y/N] `);
387
+ if (!approved)
388
+ throw new QuickstartError("Deployment cancelled before mutation.");
389
+ }
390
+ if (state.celldMinor === "5") {
391
+ state.workerProjectDigest = preparePrivateWorkerProject(inputs, vars);
392
+ io.log("Protected Worker snapshot prepared outside the checkout. Worker variables are stored in the fleet deployment; restrict fleet-storage readers accordingly.");
393
+ }
394
+ else
395
+ await distributeVars(inputs, vars.path, executor, io, false);
396
+ const result = await runFleetCommand(inputs, executor, deployArgs, io, "celld Worker deploy");
397
+ let output;
398
+ try {
399
+ output = JSON.parse(result.stdout);
400
+ }
401
+ catch { /* reject without echoing child output */ }
402
+ if (!isRecord(output) || typeof output.version !== "string" || !/^[a-f0-9]{16}$/i.test(output.version)) {
403
+ throw new QuickstartError("Worker was published but celld returned no valid deployment version. Inspect deployment diagnostics before retrying.");
404
+ }
405
+ return output.version;
406
+ }
407
+ async function distributeVars(inputs, varsFile, executor, io, dryRun) {
408
+ const hosts = inputs.nodeHosts.length > 0 ? inputs.nodeHosts : [null];
409
+ for (const host of hosts) {
410
+ const label = host || "local node";
411
+ if (host && inputs.remoteVarsFile) {
412
+ const copyArgs = ["-q", varsFile, `${host}:${inputs.remoteVarsFile}`];
413
+ if (dryRun)
414
+ describeCommand(executable("scp"), copyArgs, io, "DRY-RUN");
415
+ else {
416
+ await runCommand(executor, executable("scp"), copyArgs, inputs, io, `copy protected variables to ${label}`);
417
+ }
418
+ const chmodArgs = sshArgs(host, "chmod", ["600", inputs.remoteVarsFile]);
419
+ if (dryRun)
420
+ describeCommand(executable("ssh"), chmodArgs, io, "DRY-RUN");
421
+ else
422
+ await runCommand(executor, executable("ssh"), chmodArgs, inputs, io, `protect variables on ${label}`);
423
+ }
424
+ else if (!host) {
425
+ io.log(`Using protected variables file for local node: ${varsFile}`);
426
+ }
427
+ }
428
+ io.log("Service configuration must already load this effective CELLD_VARS_FILE; the quickstart does not edit or verify service-manager settings.");
429
+ }
430
+ function privateWorkerProject(inputs) {
431
+ const project = `${inputs.stateFile}.worker`;
432
+ assertOutsideCheckout(project, inputs.repoRoot, "protected Worker project");
433
+ return project;
434
+ }
435
+ /** 0.5 removed node-local variable overrides. Keep secret bindings and the
436
+ * exact build/migration inputs in a protected snapshot outside the source tree.
437
+ * Deploy and migration both consume this snapshot; retries never rebuild it. */
438
+ function preparePrivateWorkerProject(inputs, vars) {
439
+ const project = privateWorkerProject(inputs);
440
+ let manifest;
441
+ try {
442
+ const parsed = JSON.parse(fs.readFileSync(path.join(inputs.repoRoot, "worker/wrangler.jsonc"), "utf8"));
443
+ if (!isRecord(parsed) || !Array.isArray(parsed.d1_databases) || parsed.d1_databases.length !== 1 ||
444
+ !isRecord(parsed.d1_databases[0]) || parsed.d1_databases[0].migrations_dir !== "migrations")
445
+ throw new Error();
446
+ manifest = { ...parsed, main: "index.js", no_bundle: true,
447
+ vars: { ...(isRecord(parsed.vars) ? parsed.vars : {}), ...vars.values } };
448
+ }
449
+ catch {
450
+ throw new QuickstartError("celld 0.5.1 quickstart requires the supplied JSON-formatted Worker manifest with one D1 database and migrations_dir=migrations.");
451
+ }
452
+ const sourceBundle = path.join(inputs.repoRoot, "worker/dist/index.js");
453
+ if (!fs.existsSync(sourceBundle))
454
+ throw new QuickstartError("Worker build produced no worker/dist/index.js; no deployment was performed.");
455
+ const migrations = path.join(inputs.repoRoot, "worker/migrations");
456
+ const names = fs.readdirSync(migrations).sort();
457
+ if (!names.length || names.some(name => !/^\d+_[A-Za-z0-9_-]+\.sql$/.test(name) || !fs.lstatSync(path.join(migrations, name)).isFile())) {
458
+ throw new QuickstartError("Worker migrations must be a nonempty directory of ordered SQL files; no deployment was performed.");
459
+ }
460
+ // Refuse redirected paths rather than chmod/copy through a symlink into a
461
+ // checkout or another user's directory. The parent may be an existing home.
462
+ const parent = path.dirname(project);
463
+ fs.mkdirSync(parent, { recursive: true, mode: 0o700 });
464
+ assertOutsideCheckout(fs.realpathSync(parent), fs.realpathSync(inputs.repoRoot), "protected Worker project parent");
465
+ for (const directory of [project, path.join(project, "migrations")]) {
466
+ if (fs.existsSync(directory))
467
+ validatePrivateDirectory(directory);
468
+ else
469
+ fs.mkdirSync(directory, { mode: 0o700 });
470
+ }
471
+ // Refuse stale extra files rather than carrying unknown migration inputs into
472
+ // a new deployment. An interrupted publication may reuse this exact shape.
473
+ const expected = new Set(["index.js", "wrangler.json", ...names.map(name => `migrations/${name}`)]);
474
+ if (privateProjectFiles(project).some(name => !expected.has(name))) {
475
+ throw new QuickstartError("Protected Worker project contains unexpected files. Review and move it aside before retrying with --reset.");
476
+ }
477
+ writeText(path.join(project, "index.js"), fs.readFileSync(sourceBundle, "utf8"), 0o600);
478
+ for (const name of names)
479
+ writeText(path.join(project, "migrations", name), fs.readFileSync(path.join(migrations, name), "utf8"), 0o600);
480
+ writeText(path.join(project, "wrangler.json"), JSON.stringify(manifest, null, 2) + "\n", 0o600);
481
+ return privateProjectDigest(project);
482
+ }
483
+ function validatePrivateDirectory(directory) {
484
+ const stat = fs.lstatSync(directory);
485
+ if (!stat.isDirectory() || (process.platform !== "win32" && (stat.uid !== process.getuid?.() || (stat.mode & 0o077) !== 0))) {
486
+ throw new QuickstartError("Protected Worker project must use owner-only directories without symlinks.");
487
+ }
488
+ }
489
+ function privateProjectFiles(project) {
490
+ validatePrivateDirectory(project);
491
+ const files = [];
492
+ for (const name of fs.readdirSync(project).sort()) {
493
+ const file = path.join(project, name);
494
+ if (name === "migrations") {
495
+ validatePrivateDirectory(file);
496
+ for (const migration of fs.readdirSync(file).sort()) {
497
+ validateProtectedFile(path.join(file, migration));
498
+ files.push(`migrations/${migration}`);
499
+ }
500
+ }
501
+ else {
502
+ validateProtectedFile(file);
503
+ files.push(name);
504
+ }
505
+ }
506
+ return files.sort();
507
+ }
508
+ function privateProjectDigest(project) {
509
+ const digest = createHash("sha256");
510
+ for (const name of privateProjectFiles(project)) {
511
+ const contents = fs.readFileSync(path.join(project, name));
512
+ digest.update(`${name}\0${contents.length}\0`).update(contents);
513
+ }
514
+ return digest.digest("hex");
515
+ }
516
+ function verifyPrivateWorkerProject(inputs, state) {
517
+ try {
518
+ const project = privateWorkerProject(inputs);
519
+ assertOutsideCheckout(fs.realpathSync(project), fs.realpathSync(inputs.repoRoot), "protected Worker project");
520
+ if (!state.workerProjectDigest || privateProjectDigest(project) !== state.workerProjectDigest)
521
+ throw new Error();
522
+ }
523
+ catch {
524
+ throw new QuickstartError("Protected Worker snapshot is missing, changed, or no longer private. Restore the exact published snapshot before resuming; no migrations were started.");
525
+ }
526
+ }
527
+ async function activate(inputs, executor, io, dryRun, fetchImpl, state) {
528
+ const hosts = inputs.nodeHosts.length > 0 ? inputs.nodeHosts : [null];
529
+ if (state.celldMinor === "3" && !inputs.restartProgram) {
530
+ if (dryRun) {
531
+ io.warn("DRY-RUN: no default restart program exists on this platform; pass --restart-program and repeat --restart-arg for activation.");
532
+ }
533
+ else {
534
+ throw new QuickstartError("Activation needs an explicit restart program on this platform. Pass --restart-program and repeat --restart-arg; the local runner does not wrap it in a shell.");
535
+ }
536
+ }
537
+ for (const host of hosts) {
538
+ const label = host || "local node";
539
+ if (state.celldMinor === "3" && inputs.restartProgram) {
540
+ if (host) {
541
+ const args = sshArgs(host, inputs.restartProgram, inputs.restartArgs);
542
+ if (dryRun)
543
+ describeCommand(executable("ssh"), args, io, `DRY-RUN restart ${label}`);
544
+ else
545
+ await runCommand(executor, executable("ssh"), args, inputs, io, `restart ${label}`);
546
+ }
547
+ else if (dryRun) {
548
+ describeCommand(inputs.restartProgram, inputs.restartArgs, io, "DRY-RUN restart local node");
549
+ }
550
+ else {
551
+ await runCommand(executor, inputs.restartProgram, inputs.restartArgs, inputs, io, "restart local node");
552
+ }
553
+ }
554
+ }
555
+ if (state.celldMinor === "4" || state.celldMinor === "5") {
556
+ if (!dryRun && state.celldMinor === "5")
557
+ verifyPrivateWorkerProject(inputs, state);
558
+ for (const url of inputs.operatorUrls) {
559
+ if (dryRun) {
560
+ io.log(`DRY-RUN: POST ${url}/reload; GET ${url}/state must match the published deployment version.`);
561
+ continue;
562
+ }
563
+ const result = await operatorJson(inputs, `${url}/reload`, "POST", fetchImpl);
564
+ if (!isRecord(result) || result.ok !== true ||
565
+ !["adopted", "unchanged"].includes(String(result.outcome)) || result.version !== state.deploymentVersion) {
566
+ throw new QuickstartError("celld reload did not confirm the published version. Inspect the node's logs and Worker variable configuration, then retry; migrations were not started.");
567
+ }
568
+ const loaded = await readOperatorState(inputs, url, fetchImpl);
569
+ if (loaded.version !== state.deploymentVersion) {
570
+ throw new QuickstartError("celld still serves a different deployment after reload. Inspect the node's logs, then retry; migrations were not started.");
571
+ }
572
+ io.log("PASS: node adopted the published Worker version.");
573
+ }
574
+ }
575
+ }
576
+ async function migrate(inputs, executor, io, dryRun, state) {
577
+ const project = state.celldMinor === "5" ? privateWorkerProject(inputs) : "worker";
578
+ if (!dryRun && state.celldMinor === "5")
579
+ verifyPrivateWorkerProject(inputs, state);
580
+ const database = state.celldMinor === "5" && !dryRun
581
+ ? JSON.parse(fs.readFileSync(path.join(project, "wrangler.json"), "utf8")).d1_databases[0].database_name
582
+ : "openplan";
583
+ if (typeof database !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(database)) {
584
+ throw new QuickstartError("The protected Worker snapshot has an unsupported D1 database name.");
585
+ }
586
+ for (const [args, label] of [
587
+ [["d1", "execute", database, "--command", "SELECT 1", project, "--bucket", inputs.fleetBucket], "D1 availability probe"],
588
+ [["d1", "migrations", "apply", database, project, "--bucket", inputs.fleetBucket], "D1 migrations"]
589
+ ]) {
590
+ if (dryRun)
591
+ describeFleetCommand(inputs, [...args], io, "DRY-RUN");
592
+ else
593
+ await runFleetCommand(inputs, executor, [...args], io, label);
594
+ }
595
+ }
596
+ async function operatorJson(inputs, url, method, fetchImpl) {
597
+ try {
598
+ return await fetchJson(fetchImpl, url, { method, redirect: "error" }, inputs.requestTimeoutMs, "celld operator request");
599
+ }
600
+ catch {
601
+ // Operator responses and transport errors can contain configuration values.
602
+ throw new QuickstartError("celld operator request failed. Check the private operator connection and node logs, then retry the same quickstart; no migrations will run until activation succeeds.");
603
+ }
604
+ }
605
+ async function readOperatorState(inputs, url, fetchImpl) {
606
+ const result = await operatorJson(inputs, `${url}/state`, "GET", fetchImpl);
607
+ if (!isRecord(result) || !isRecord(result.deployment) || typeof result.deployment.version !== "string") {
608
+ throw new QuickstartError("celld operator state has no deployment version. Check that --operator-url reaches the celld 0.4/0.5.1 internal listener.");
609
+ }
610
+ return { version: result.deployment.version };
611
+ }
612
+ async function prove(inputs, apiKey, homeDir, fetchImpl, io, dryRun) {
613
+ if (dryRun) {
614
+ io.log(`DRY-RUN: GET ${routeUrl(inputs.publicUrl, "healthz")}`);
615
+ io.log(`DRY-RUN: GET ${routeUrl(inputs.publicUrl, "readyz")}`);
616
+ io.log("DRY-RUN: authenticate the CLI, upload a private sample, verify owner access metadata and signed-out denial (no public sharing mutation).");
617
+ return;
618
+ }
619
+ await waitForReadiness(inputs, io, "proof readiness", fetchImpl);
620
+ const client = createEnrollmentApiClient(inputs.publicUrl, apiKey, {
621
+ fetchImpl, timeoutMs: inputs.requestTimeoutMs, userAgent: `openplan/${inputs.cliVersion}`
622
+ });
623
+ const apiMe = await client.me();
624
+ io.log("CLI authentication verified in memory; local connection is unchanged until full proof succeeds.");
625
+ const sample = "<!doctype html>\n<html lang=\"en\">\n<head><meta charset=\"utf-8\"><title>Openplan ready</title></head>\n<body><main><h1>Openplan is ready</h1><p>This draft was published through the celld quickstart.</p></main></body>\n</html>\n";
626
+ const uploadBody = await client.upload({
627
+ html: sample,
628
+ filename: "openplan-quickstart.html",
629
+ metadata: { cliVersion: inputs.cliVersion, fileSha256: createHash("sha256").update(sample).digest("hex") }
630
+ });
631
+ if (!isRecord(uploadBody) || typeof uploadBody.draftId !== "string" ||
632
+ !/^[a-z0-9]{12}$/.test(uploadBody.draftId) || uploadBody.versionNumber !== 1) {
633
+ throw new QuickstartError("Sample upload returned an invalid new draft identity.");
634
+ }
635
+ // Derive routes from the configured endpoint, never send the owner's key to
636
+ // a server-returned URL. This proof intentionally does not make anything public.
637
+ for (const version of [undefined, 1]) {
638
+ const overview = await client.getAccess(uploadBody.draftId, version ?? "access");
639
+ if (!isRecord(overview) || !isRecord(overview.access) ||
640
+ overview.access.mode !== "private" || overview.access.legacy !== false ||
641
+ typeof overview.access.policyId !== "string" || !overview.access.policyId) {
642
+ throw new QuickstartError("The new sample does not have a persisted private access policy.");
643
+ }
644
+ }
645
+ for (const alias of ["", "/raw", "/v/1", "/v/1/raw"]) {
646
+ for (const method of ["GET", "HEAD"]) {
647
+ const body = await fetchWithTimeout(fetchImpl, routeUrl(inputs.publicUrl, `d/${uploadBody.draftId}${alias}`), { method, redirect: "manual" }, inputs.requestTimeoutMs, "signed-out private sample check", [302, 401, 403, 404]);
648
+ if ((method === "HEAD" && body.length !== 0) ||
649
+ body.includes("Openplan is ready") || body.includes("Openplan ready") ||
650
+ body.includes("This draft was published through the celld quickstart")) {
651
+ throw new QuickstartError("A signed-out response disclosed private sample content or returned a HEAD body.");
652
+ }
653
+ }
654
+ }
655
+ io.log(`Private sample verified: ${routeUrl(inputs.publicUrl, `d/${uploadBody.draftId}`)}`);
656
+ io.log("Owner access metadata and signed-out denial verified; the sample remains private. Sign in and link the CLI account to verify browser access and exact document bytes. This check does not prove browser identity or byte-for-byte readback.");
657
+ // The proof may deploy/create a sample, but must never alter local connection
658
+ // state until every check has passed. Save the origin binding before default.
659
+ try {
660
+ await new CredentialStore(homeDir).save(client.origin, apiKey, apiMe);
661
+ }
662
+ catch (error) {
663
+ if (error instanceof CredentialError && error.code === "PERSISTENCE_UNCERTAIN") {
664
+ throw new QuickstartError("Proof succeeded, but credential persistence is uncertain after replacement. The CLI default was not changed. Inspect local auth status before retrying.");
665
+ }
666
+ throw new QuickstartError("Proof succeeded, but credential commit failed; the CLI default was not changed. Inspect local credential state before retrying.");
667
+ }
668
+ try {
669
+ persistCliDefault(homeDir, client.origin);
670
+ }
671
+ catch {
672
+ throw new QuickstartError("Proof succeeded; partial commit: the origin-bound credential was saved, but updating the CLI default failed. Inspect endpoint and auth status before retrying.");
673
+ }
674
+ io.log("Origin-bound CLI credential saved with owner-only permissions; CLI default endpoint updated.");
675
+ }
676
+ async function checkReadiness(inputs, io, label, fetchImpl, deadline) {
677
+ for (const route of ["healthz", "readyz"]) {
678
+ const remaining = deadline - Date.now();
679
+ if (remaining <= 0)
680
+ throw new QuickstartError(`${label} exhausted its readiness deadline.`);
681
+ const body = await fetchWithTimeout(fetchImpl, routeUrl(inputs.publicUrl, route), undefined, Math.min(inputs.requestTimeoutMs, remaining), `${label} (${route})`);
682
+ if (!body.replace(/\s/g, "").includes('"ok":true')) {
683
+ throw new QuickstartError(`${label} (${route}) returned a non-ready response.`);
684
+ }
685
+ io.log(`PASS: ${route}`);
686
+ }
687
+ }
688
+ async function waitForReadiness(inputs, io, label, fetchImpl) {
689
+ const deadline = Date.now() + READINESS_TIMEOUT_MS;
690
+ let lastError;
691
+ while (Date.now() < deadline) {
692
+ try {
693
+ await checkReadiness(inputs, io, label, fetchImpl, deadline);
694
+ return;
695
+ }
696
+ catch (error) {
697
+ lastError = error;
698
+ }
699
+ const remaining = deadline - Date.now();
700
+ if (remaining <= 0)
701
+ break;
702
+ await new Promise((resolve) => setTimeout(resolve, Math.min(READINESS_RETRY_DELAY_MS, remaining)));
703
+ }
704
+ if (lastError instanceof QuickstartError)
705
+ throw lastError;
706
+ throw new QuickstartError(`${label} did not succeed within ${READINESS_TIMEOUT_MS}ms.`);
707
+ }
708
+ async function fetchJson(fetchImpl, url, init, timeoutMs, label) {
709
+ const body = await fetchWithTimeout(fetchImpl, url, init, timeoutMs, label);
710
+ try {
711
+ return JSON.parse(body);
712
+ }
713
+ catch {
714
+ throw new QuickstartError(`${label} returned invalid JSON.`);
715
+ }
716
+ }
717
+ async function fetchWithTimeout(fetchImpl, url, init, timeoutMs, label, expectedStatuses) {
718
+ const controller = new AbortController();
719
+ let response;
720
+ let reader;
721
+ let complete = false;
722
+ let timer;
723
+ // Race the entire fetch/read operation, not just response headers. This also
724
+ // bounds injected transports that fail to honor cancellation promptly.
725
+ const timeout = new Promise((_resolve, reject) => {
726
+ timer = setTimeout(() => {
727
+ controller.abort();
728
+ reject(new QuickstartError(`${label} exceeded its request deadline.`));
729
+ }, timeoutMs);
730
+ });
731
+ const consume = async () => {
732
+ response = await fetchImpl(url, { ...init, signal: controller.signal });
733
+ if (controller.signal.aborted) {
734
+ void response.body?.cancel().catch(() => { });
735
+ throw new QuickstartError(`${label} exceeded its request deadline.`);
736
+ }
737
+ if (expectedStatuses ? !expectedStatuses.includes(response.status) : !response.ok) {
738
+ throw new QuickstartError(`${label} returned HTTP ${response.status}.`);
739
+ }
740
+ const declaredLength = response.headers.get("Content-Length");
741
+ if (declaredLength !== null && Number(declaredLength) > MAX_PROBE_RESPONSE_BYTES) {
742
+ throw new QuickstartError(`${label} exceeded the response byte limit.`);
743
+ }
744
+ if (!response.body)
745
+ return "";
746
+ reader = response.body.getReader();
747
+ const decoder = new TextDecoder();
748
+ let bytes = 0;
749
+ let body = "";
750
+ while (true) {
751
+ const chunk = await reader.read();
752
+ if (chunk.done)
753
+ break;
754
+ bytes += chunk.value.byteLength;
755
+ if (bytes > MAX_PROBE_RESPONSE_BYTES)
756
+ throw new QuickstartError(`${label} exceeded the response byte limit.`);
757
+ body += decoder.decode(chunk.value, { stream: true });
758
+ }
759
+ return body + decoder.decode();
760
+ };
761
+ try {
762
+ const body = await Promise.race([consume(), timeout]);
763
+ complete = true;
764
+ return body;
765
+ }
766
+ catch (error) {
767
+ if (error instanceof QuickstartError)
768
+ throw error;
769
+ throw new QuickstartError(`${label} failed. Check the connection and service diagnostics locally, then retry.`);
770
+ }
771
+ finally {
772
+ clearTimeout(timer);
773
+ if (!complete)
774
+ controller.abort();
775
+ if (reader) {
776
+ const ownedReader = reader;
777
+ if (!complete) {
778
+ // Do not await an untrusted stream's cancel hook past the deadline.
779
+ void ownedReader.cancel().catch(() => { });
780
+ try {
781
+ ownedReader.releaseLock();
782
+ }
783
+ catch { /* cancellation settles any pending read */ }
784
+ }
785
+ else
786
+ ownedReader.releaseLock();
787
+ }
788
+ else if (!complete)
789
+ void response?.body?.cancel().catch(() => { });
790
+ }
791
+ }
792
+ async function runFleetCommand(inputs, executor, args, io, label) {
793
+ if (inputs.nodeHosts.length > 0) {
794
+ const commandArgs = sshArgs(inputs.nodeHosts[0], executable("celld"), args);
795
+ return runCommand(executor, executable("ssh"), commandArgs, inputs, io, label);
796
+ }
797
+ return runCommand(executor, executable("celld"), args, inputs, io, label);
798
+ }
799
+ async function runLocalCommand(executor, inputs, args, io, label) {
800
+ return runCommand(executor, executable("npm"), args, inputs, io, label);
801
+ }
802
+ async function runCommand(executor, file, args, inputs, io, label) {
803
+ describeCommand(file, args, io, "RUN");
804
+ let result;
805
+ try {
806
+ result = await executor.run(file, args, { cwd: inputs.repoRoot, timeoutMs: inputs.commandTimeoutMs });
807
+ }
808
+ catch (error) {
809
+ throw new QuickstartError(`${label} could not start. Check the executable and its local configuration, then retry.`);
810
+ }
811
+ if (result.exitCode !== 0) {
812
+ const output = `${result.stdout}\n${result.stderr}`;
813
+ // Map known diagnostic classes to fixed messages; child output can include
814
+ // credentials and must not be copied verbatim into shared logs.
815
+ const hint = /no node leases|needs a running fleet/i.test(output)
816
+ ? "Start a celld node for this dedicated fleet, verify the read-only diagnosis, then retry. D1 migrations require a running node."
817
+ : /esbuild.*(?:not found|not installed)|(?:not found|not installed).*esbuild/i.test(output)
818
+ ? "Install project dependencies and ensure the checkout's esbuild executable is available to celld."
819
+ : /AccessDenied|InvalidAccessKeyId|SignatureDoesNotMatch|NoCredentialProviders/i.test(output)
820
+ ? "Check the celld fleet storage credentials and endpoint in the command environment; draft-storage variables are separate."
821
+ : "Run the command displayed above directly to inspect its diagnostics locally, then retry the quickstart to resume.";
822
+ throw new QuickstartError(`${label} failed with exit code ${result.exitCode === null ? "unknown" : result.exitCode}. ${hint}`);
823
+ }
824
+ return result;
825
+ }
826
+ async function requireCommand(executor, command, timeoutMs, label) {
827
+ const probe = process.platform === "win32" ? "where.exe" : "which";
828
+ let result;
829
+ try {
830
+ result = await executor.run(probe, [command], { timeoutMs });
831
+ }
832
+ catch {
833
+ throw new QuickstartError(`Preflight could not find ${label}.`);
834
+ }
835
+ if (result.exitCode !== 0)
836
+ throw new QuickstartError(`Preflight could not find ${label}.`);
837
+ }
838
+ function describeFleetCommand(inputs, args, io, prefix) {
839
+ if (inputs.nodeHosts.length > 0) {
840
+ describeCommand(executable("ssh"), sshArgs(inputs.nodeHosts[0], executable("celld"), args), io, prefix);
841
+ }
842
+ else {
843
+ describeCommand(executable("celld"), args, io, prefix);
844
+ }
845
+ }
846
+ function describeCommand(file, args, io, prefix) {
847
+ io.log(`${prefix}: ${[file, ...args].map(displayArg).join(" ")}`);
848
+ }
849
+ function sshArgs(host, file, args) {
850
+ return ["-o", "BatchMode=yes", "-o", "ConnectTimeout=10", host, file, ...args];
851
+ }
852
+ function classifyFleet(output) {
853
+ const lower = output.toLowerCase();
854
+ if (/(^|[^a-z])(shared|multi[- ]application|multiple applications|existing workload)([^a-z]|$)/u.test(lower)) {
855
+ return "shared";
856
+ }
857
+ if (/(^|[^a-z])dedicated([^a-z]|$)/u.test(lower))
858
+ return "dedicated";
859
+ return "unknown";
860
+ }
861
+ function validateRepository(repoRoot) {
862
+ for (const relative of ["package.json", "package-lock.json", "worker/wrangler.jsonc", "worker/src/index.ts"]) {
863
+ if (!fs.existsSync(path.join(repoRoot, relative))) {
864
+ throw new QuickstartError(`Required repository file is missing: ${relative}.`);
865
+ }
866
+ }
867
+ }
868
+ function validateVars(vars, publicUrl, checkFile = true) {
869
+ if (checkFile)
870
+ validateProtectedFile(vars.path);
871
+ validateBootstrapBinding(vars, publicUrl);
872
+ const required = [
873
+ "DRAFTS_S3_ENDPOINT",
874
+ "DRAFTS_S3_REGION",
875
+ "DRAFTS_S3_BUCKET",
876
+ "DRAFTS_S3_ACCESS_KEY_ID",
877
+ "DRAFTS_S3_SECRET_ACCESS_KEY"
878
+ ];
879
+ for (const key of required) {
880
+ const value = vars.values[key];
881
+ if (!value || isPlaceholder(value))
882
+ throw new QuickstartError(`Protected variables file needs a real ${key} value.`);
883
+ }
884
+ const access = vars.values.OPENPLAN_DRAFT_ACCESS || "link";
885
+ if (!["link", "authenticated", "owner"].includes(access)) {
886
+ throw new QuickstartError("OPENPLAN_DRAFT_ACCESS must be link, authenticated, or owner (legacy fallback only).");
887
+ }
888
+ const anonymous = vars.values.OPENPLAN_ALLOW_ANONYMOUS_UPLOADS || "false";
889
+ if (anonymous !== "false")
890
+ throw new QuickstartError("The quickstart refuses anonymous uploads; set OPENPLAN_ALLOW_ANONYMOUS_UPLOADS=false.");
891
+ if (vars.values.PUBLIC_BASE_URL !== publicUrl)
892
+ throw new QuickstartError("PUBLIC_BASE_URL does not match --public-url.");
893
+ if (!readApiKey(vars))
894
+ throw new QuickstartError("Protected variables file needs BOOTSTRAP_API_KEY or OPENPLAN_BOOTSTRAP_API_KEY.");
895
+ for (const key of SECRET_KEYS) {
896
+ if (!vars.values[key] && key !== "BOOTSTRAP_API_KEY")
897
+ throw new QuickstartError(`Protected variables file needs ${key}.`);
898
+ }
899
+ }
900
+ function validateBootstrapBinding(vars, publicUrl) {
901
+ const primary = vars.values.BOOTSTRAP_API_KEY;
902
+ const alias = vars.values.OPENPLAN_BOOTSTRAP_API_KEY;
903
+ if (primary && alias && primary !== alias) {
904
+ throw new QuickstartError("Conflicting BOOTSTRAP_API_KEY and OPENPLAN_BOOTSTRAP_API_KEY aliases; explicitly configure one bootstrap credential.");
905
+ }
906
+ const origin = vars.values.OPENPLAN_BOOTSTRAP_API_KEY_ORIGIN;
907
+ if (!primary && !alias) {
908
+ if (origin !== undefined)
909
+ throw new QuickstartError("OPENPLAN_BOOTSTRAP_API_KEY_ORIGIN requires a bootstrap key; remove the orphan declaration before generating a new pair.");
910
+ return;
911
+ }
912
+ readApiKey(vars);
913
+ if (!origin)
914
+ throw new QuickstartError("An existing bootstrap key requires independent OPENPLAN_BOOTSTRAP_API_KEY_ORIGIN in the protected variables file; --yes and target selection do not authorize rebinding.");
915
+ if (normalizeApiUrl(origin, "OPENPLAN_BOOTSTRAP_API_KEY_ORIGIN") !== publicUrl) {
916
+ throw new QuickstartError("OPENPLAN_BOOTSTRAP_API_KEY_ORIGIN does not match the selected public origin; no mutation was performed.");
917
+ }
918
+ }
919
+ function readApiKey(vars) {
920
+ const value = vars.values.BOOTSTRAP_API_KEY || vars.values.OPENPLAN_BOOTSTRAP_API_KEY || "";
921
+ if (!value || isPlaceholder(value))
922
+ throw new QuickstartError("Protected variables file needs a real bootstrap API key.");
923
+ validateApiKey(value);
924
+ return value;
925
+ }
926
+ function readVars(file) {
927
+ if (!fs.existsSync(file))
928
+ throw new QuickstartError(`Protected variables file does not exist: ${file}`);
929
+ let content;
930
+ try {
931
+ content = fs.readFileSync(file, "utf8");
932
+ }
933
+ catch (error) {
934
+ throw new QuickstartError("Could not read the protected variables file. Check its ownership and permissions locally.");
935
+ }
936
+ const values = {};
937
+ for (const [index, sourceLine] of content.split(/\r?\n/u).entries()) {
938
+ const line = sourceLine.trim();
939
+ if (!line || line.startsWith("#"))
940
+ continue;
941
+ const assignment = line.startsWith("export ") ? line.slice(7) : line;
942
+ const separator = assignment.indexOf("=");
943
+ if (separator <= 0)
944
+ throw new QuickstartError(`Invalid variables file line ${index + 1}; expected KEY=value.`);
945
+ const key = assignment.slice(0, separator).trim();
946
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key))
947
+ throw new QuickstartError(`Invalid variables file key on line ${index + 1}.`);
948
+ values[key] = unquoteEnvValue(assignment.slice(separator + 1).trim());
949
+ }
950
+ return { path: file, values };
951
+ }
952
+ function serializeVars(values) {
953
+ return Object.entries(values)
954
+ .map(([key, value]) => `${key}=${quoteEnvValue(value)}`)
955
+ .join("\n") + "\n";
956
+ }
957
+ function unquoteEnvValue(value) {
958
+ if (value.length >= 2 && ((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'")))) {
959
+ return value.slice(1, -1);
960
+ }
961
+ return value;
962
+ }
963
+ function quoteEnvValue(value) {
964
+ return /^[A-Za-z0-9_./:@+,-]+$/u.test(value) ? value : JSON.stringify(value);
965
+ }
966
+ function validateProtectedFile(file) {
967
+ const stat = fs.lstatSync(file);
968
+ if (!stat.isFile() || (process.platform !== "win32" && stat.uid !== process.getuid?.()))
969
+ throw new QuickstartError(`Protected variables path is not a regular file: ${file}`);
970
+ if (process.platform !== "win32" && (stat.mode & 0o077) !== 0) {
971
+ throw new QuickstartError(`Protected variables file is readable by other users; chmod 600 ${file} and retry.`);
972
+ }
973
+ }
974
+ function persistCliDefault(homeDir, apiUrl) {
975
+ const directory = path.join(homeDir, ".openplan");
976
+ ensureDirectory(directory, 0o700);
977
+ const configPath = path.join(directory, "config.json");
978
+ let config = {};
979
+ if (fs.existsSync(configPath)) {
980
+ try {
981
+ const parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
982
+ if (!isRecord(parsed))
983
+ throw new Error("not an object");
984
+ config = parsed;
985
+ }
986
+ catch {
987
+ throw new QuickstartError(`Openplan config is not valid JSON: ${configPath}`);
988
+ }
989
+ }
990
+ writeText(configPath, `${JSON.stringify({ ...config, apiUrl }, null, 2)}\n`, 0o600);
991
+ }
992
+ function writeState(file, state) {
993
+ ensureDirectory(path.dirname(file), 0o700);
994
+ writeText(file, `${JSON.stringify(state, null, 2)}\n`, 0o600);
995
+ }
996
+ function readState(file) {
997
+ if (!fs.existsSync(file))
998
+ return null;
999
+ try {
1000
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
1001
+ if (!isRecord(parsed) || (parsed.version !== 1 && parsed.version !== 2) || !isRecord(parsed.inputs) || !Array.isArray(parsed.completedStages)) {
1002
+ throw new Error("invalid state shape");
1003
+ }
1004
+ if (parsed.completedStages.some(stage => !QUICKSTART_STAGES.includes(stage)))
1005
+ throw new Error("invalid stage");
1006
+ if (parsed.version === 1) {
1007
+ // Legacy deploy combined publication and migration, with no version identity.
1008
+ // Preserve credentials/build preparation; replay publication and activation
1009
+ // on interrupted runs so we can establish the new ordering safely.
1010
+ const complete = parsed.completedStages.includes("prove");
1011
+ parsed.completedStages = complete ? [...QUICKSTART_STAGES] :
1012
+ parsed.completedStages.filter(stage => stage === "preflight" || stage === "prepare");
1013
+ parsed.version = 2;
1014
+ }
1015
+ return parsed;
1016
+ }
1017
+ catch {
1018
+ throw new QuickstartError(`Quickstart state is not valid JSON: ${file}`);
1019
+ }
1020
+ }
1021
+ function removeStateFile(file) {
1022
+ if (fs.existsSync(file))
1023
+ fs.rmSync(file, { force: true });
1024
+ }
1025
+ function stateInputs(inputs) {
1026
+ return {
1027
+ fleetBucket: inputs.fleetBucket,
1028
+ publicUrl: inputs.publicUrl,
1029
+ varsFile: inputs.varsFile,
1030
+ fleetMode: inputs.fleetMode,
1031
+ nodeHosts: [...inputs.nodeHosts],
1032
+ remoteVarsFile: inputs.remoteVarsFile,
1033
+ operatorUrls: [...inputs.operatorUrls],
1034
+ restartProgram: inputs.restartProgram,
1035
+ restartArgs: [...inputs.restartArgs]
1036
+ };
1037
+ }
1038
+ function sameStateInputs(previous, inputs) {
1039
+ return JSON.stringify({ ...previous, operatorUrls: undefined }) === JSON.stringify({ ...stateInputs(inputs), operatorUrls: undefined }) &&
1040
+ JSON.stringify(previous.operatorUrls || []) === JSON.stringify(inputs.operatorUrls);
1041
+ }
1042
+ function defaultRestartProgram(remote) {
1043
+ return process.platform === "linux" || remote ? (remote ? "systemctl" : executable("systemctl")) : null;
1044
+ }
1045
+ function defaultRestartArgs(program) {
1046
+ return program && /(?:^|[\\/])systemctl(?:\.exe)?$/u.test(program) ? ["--user", "restart", "celld"] : [];
1047
+ }
1048
+ function validateFleetBucket(value) {
1049
+ if (!/^(?:s3|gs|az):\/\/[^\s/]+(?:\/[^\s]*)?$/u.test(value)) {
1050
+ throw new QuickstartError("Fleet bucket must use an explicit s3://, gs://, or az:// URI.");
1051
+ }
1052
+ }
1053
+ function normalizeListenerUrl(value) {
1054
+ let parsed;
1055
+ try {
1056
+ parsed = new URL(value);
1057
+ }
1058
+ catch {
1059
+ throw new QuickstartError("Public URL must be an absolute http:// or https:// URL.");
1060
+ }
1061
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
1062
+ throw new QuickstartError("Public URL must use http:// or https://.");
1063
+ if (parsed.username || parsed.password || parsed.search || parsed.hash)
1064
+ throw new QuickstartError("Public URL must not contain credentials, a query, or a fragment.");
1065
+ if (!parsed.hostname)
1066
+ throw new QuickstartError("Public URL must contain a hostname.");
1067
+ return parsed.origin + (parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/+$/u, ""));
1068
+ }
1069
+ function normalizeOperatorUrl(value) {
1070
+ const url = normalizeListenerUrl(value);
1071
+ if (new URL(url).pathname !== "/")
1072
+ throw new QuickstartError("--operator-url must be the internal listener origin, without a path.");
1073
+ return url;
1074
+ }
1075
+ function validateHost(host) {
1076
+ if (!host || host.startsWith("-") || !/^[A-Za-z0-9._@:-]+$/u.test(host)) {
1077
+ throw new QuickstartError("Remote node host contains unsupported characters.");
1078
+ }
1079
+ }
1080
+ function validateRemotePath(value) {
1081
+ if (!value || /[\s\0]/u.test(value) || (!value.startsWith("/") && !/^[A-Za-z]:[\\/]/u.test(value))) {
1082
+ throw new QuickstartError("--remote-vars-file must be an absolute path without whitespace.");
1083
+ }
1084
+ return value;
1085
+ }
1086
+ function assertOutsideCheckout(file, repoRoot, label) {
1087
+ const relative = path.relative(repoRoot, file);
1088
+ if (relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative))) {
1089
+ throw new QuickstartError(`The ${label} must be outside the repository checkout: ${file}`);
1090
+ }
1091
+ }
1092
+ function ensureDirectory(directory, mode) {
1093
+ fs.mkdirSync(directory, { recursive: true, mode });
1094
+ if (process.platform !== "win32")
1095
+ fs.chmodSync(directory, mode);
1096
+ }
1097
+ function writeText(file, content, mode) {
1098
+ const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
1099
+ try {
1100
+ fs.writeFileSync(temporary, content, { encoding: "utf8", mode });
1101
+ if (process.platform !== "win32")
1102
+ fs.chmodSync(temporary, mode);
1103
+ fs.renameSync(temporary, file);
1104
+ }
1105
+ finally {
1106
+ fs.rmSync(temporary, { force: true });
1107
+ }
1108
+ }
1109
+ async function promptForValue(label, option, nonInteractive, _io) {
1110
+ if (nonInteractive || !process.stdin.isTTY || !process.stdout.isTTY) {
1111
+ throw new QuickstartError(`Missing ${label}; provide ${option} in non-interactive mode.`);
1112
+ }
1113
+ const readline = createInterface({ input: process.stdin, output: process.stdout });
1114
+ try {
1115
+ const value = (await readline.question(`${label}: `)).trim();
1116
+ if (!value)
1117
+ throw new QuickstartError(`Missing ${label}; provide ${option}.`);
1118
+ return value;
1119
+ }
1120
+ finally {
1121
+ readline.close();
1122
+ }
1123
+ }
1124
+ function uniqueHosts(hosts) {
1125
+ return [...new Set(hosts.map((host) => host.trim()).filter(Boolean))];
1126
+ }
1127
+ function positiveNumber(value, fallback) {
1128
+ return value !== undefined && Number.isFinite(value) && value > 0 ? value : fallback;
1129
+ }
1130
+ function executable(name) {
1131
+ if (process.platform !== "win32")
1132
+ return name;
1133
+ if (name === "npm")
1134
+ return "npm.cmd";
1135
+ if (name === "celld" || name === "ssh" || name === "scp" || name === "systemctl")
1136
+ return `${name}.exe`;
1137
+ return name;
1138
+ }
1139
+ function appendOutput(current, chunk) {
1140
+ if (current.length >= MAX_CAPTURED_OUTPUT)
1141
+ return current;
1142
+ return `${current}${chunk}`.slice(0, MAX_CAPTURED_OUTPUT);
1143
+ }
1144
+ function displayArg(value) {
1145
+ return /^[A-Za-z0-9_./:@+=,-]+$/u.test(value) ? value : JSON.stringify(value);
1146
+ }
1147
+ function isPlaceholder(value) {
1148
+ return /replace-with|example\.invalid|<[^>]+>|your[-_]/iu.test(value);
1149
+ }
1150
+ function isRecord(value) {
1151
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1152
+ }
1153
+ function routeUrl(base, route) {
1154
+ const parsed = new URL(base);
1155
+ const prefix = parsed.pathname === "/" ? "/" : `${parsed.pathname.replace(/\/+$/u, "")}/`;
1156
+ parsed.pathname = `${prefix}${route.replace(/^\/+/, "")}`;
1157
+ return parsed.toString();
1158
+ }
1159
+ function defaultIo() {
1160
+ return {
1161
+ log: (message) => console.log(message),
1162
+ warn: (message) => console.warn(message),
1163
+ confirm: async (message) => {
1164
+ const readline = createInterface({ input: process.stdin, output: process.stdout });
1165
+ try {
1166
+ const answer = await readline.question(message);
1167
+ return /^(?:y|yes)$/iu.test(answer.trim());
1168
+ }
1169
+ finally {
1170
+ readline.close();
1171
+ }
1172
+ }
1173
+ };
1174
+ }
1175
+ //# sourceMappingURL=celld-quickstart.js.map