leak-cli 2026.2.17-beta.1 → 2026.2.17

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/scripts/leak.js CHANGED
@@ -1,36 +1,85 @@
1
1
  #!/usr/bin/env node
2
2
  import "dotenv/config";
3
3
  import fs from "node:fs";
4
+ import os from "node:os";
4
5
  import path from "node:path";
5
6
  import { fileURLToPath } from "node:url";
6
7
  import readline from "node:readline/promises";
7
8
  import { stdin as input, stdout as output } from "node:process";
8
9
  import { spawn, spawnSync } from "node:child_process";
10
+ import { randomUUID } from "node:crypto";
11
+ import enquirer from "enquirer";
9
12
  import { isAddress } from "viem";
10
- import { defaultFacilitatorUrlForMode, readConfig } from "./config_store.js";
13
+ import {
14
+ defaultFacilitatorUrlForMode,
15
+ readConfig,
16
+ writeConfig,
17
+ } from "./config_store.js";
11
18
  import { resolveSupportedChain } from "../src/chain_meta.js";
19
+ import {
20
+ ACCESS_MODE_VALUES,
21
+ DEFAULT_ACCESS_MODE,
22
+ accessModeRequiresDownloadCode,
23
+ accessModeRequiresPayment,
24
+ isValidAccessMode,
25
+ } from "../src/access_mode.js";
26
+ import {
27
+ hashDownloadCode,
28
+ isValidDownloadCodeHash,
29
+ } from "../src/download_code.js";
30
+ import { createUi } from "./ui.js";
31
+ const { Select, Input } = enquirer;
32
+ const HiddenCodePrompt = enquirer["Pass" + "word"];
12
33
 
13
34
  const __filename = fileURLToPath(import.meta.url);
14
35
  const __dirname = path.dirname(__filename);
15
36
  const SERVER_ENTRY = path.resolve(__dirname, "..", "src", "index.js");
16
37
  const PUBLIC_CONFIRM_PHRASE = "I_UNDERSTAND_PUBLIC_EXPOSURE";
17
38
  const ABSOLUTE_SENSITIVE_PATHS = ["/etc", "/proc", "/sys", "/var/run/secrets"];
39
+ const ALLOWED_CONFIRMATION_POLICIES = new Set(["confirmed", "optimistic"]);
40
+ const ALLOWED_FACILITATOR_MODES = new Set(["testnet", "cdp_mainnet"]);
41
+ const RUNS_DIR = ".leak/runs";
42
+ const outUi = createUi(output);
43
+ const errUi = createUi(process.stderr);
44
+
45
+ function logInfo(message) {
46
+ console.log(outUi.statusLine("info", message));
47
+ }
48
+
49
+ function logOk(message) {
50
+ console.log(outUi.statusLine("ok", message));
51
+ }
52
+
53
+ function logWarn(message) {
54
+ console.error(errUi.statusLine("warn", message));
55
+ }
56
+
57
+ function logError(message) {
58
+ console.error(errUi.statusLine("error", message));
59
+ }
18
60
 
19
61
  function usageAndExit(code = 1, hint = "") {
20
- if (hint) console.error(`Hint: ${hint}\n`);
21
- console.log(`Usage: leak --file <path> [--price <usdc>] [--window <duration>] [--pay-to <address>] [--network <caip2>] [--port <port>] [--confirmed] [--public] [--public-confirm ${PUBLIC_CONFIRM_PHRASE}] [--allow-sensitive-path --acknowledge-sensitive-path-risk] [--og-title <text>] [--og-description <text>] [--og-image-url <https://...|./image.png>] [--ended-window-seconds <seconds>]`);
22
- console.log(` leak leak --file <path> [--price <usdc>] [--window <duration>] [--pay-to <address>] [--network <caip2>] [--port <port>] [--confirmed] [--public] [--public-confirm ${PUBLIC_CONFIRM_PHRASE}] [--allow-sensitive-path --acknowledge-sensitive-path-risk] [--og-title <text>] [--og-description <text>] [--og-image-url <https://...|./image.png>] [--ended-window-seconds <seconds>]`);
23
- console.log(``);
24
- console.log(`Notes:`);
25
- console.log(` --public requires cloudflared (Cloudflare Tunnel) installed.`);
26
- console.log(`Examples:`);
27
- console.log(` leak --file ./vape.jpg`);
28
- console.log(` leak --file ./vape.jpg --price 0.01 --window 1h --confirmed`);
29
- console.log(` leak --file ./vape.jpg --public --og-title "My New Drop" --og-description "Agent-assisted purchase"`);
62
+ if (hint) logWarn(`Hint: ${hint}`);
63
+ console.log(outUi.heading("Leak Publish CLI"));
64
+ console.log("");
65
+ console.log(outUi.section("Usage"));
66
+ console.log(` leak publish [--file <path>] [--access-mode <${ACCESS_MODE_VALUES.join("|")}>]`);
67
+ console.log(` leak --file <path> [--access-mode <${ACCESS_MODE_VALUES.join("|")}>] [--download-code <code> | --download-code-stdin] [--price <usdc>] [--window <duration>] [--pay-to <address>] [--network <caip2>] [--port <port>] [--confirmed] [--public] [--public-confirm ${PUBLIC_CONFIRM_PHRASE}] [--allow-sensitive-path --acknowledge-sensitive-path-risk] [--og-title <text>] [--og-description <text>] [--og-image-url <https://...|./image.png>] [--ended-window-seconds <seconds>]`);
68
+ console.log(` leak leak --file <path> [--access-mode <${ACCESS_MODE_VALUES.join("|")}>] [--download-code <code> | --download-code-stdin] [--price <usdc>] [--window <duration>] [--pay-to <address>] [--network <caip2>] [--port <port>] [--confirmed] [--public] [--public-confirm ${PUBLIC_CONFIRM_PHRASE}] [--allow-sensitive-path --acknowledge-sensitive-path-risk] [--og-title <text>] [--og-description <text>] [--og-image-url <https://...|./image.png>] [--ended-window-seconds <seconds>]`);
69
+ console.log("");
70
+ console.log(outUi.section("Notes"));
71
+ console.log(" --public requires cloudflared (Cloudflare Tunnel) installed.");
72
+ console.log("");
73
+ console.log(outUi.section("Examples"));
74
+ console.log(" leak publish");
75
+ console.log(" leak --file ./vape.jpg");
76
+ console.log(" leak --file ./vape.jpg --price 0.01 --window 1h --confirmed");
77
+ console.log(' leak --file ./vape.jpg --access-mode download-code-only-no-payment --download-code "friends-only"');
78
+ console.log(' leak --file ./vape.jpg --public --og-title "My New Drop" --og-description "Agent-assisted purchase"');
30
79
  console.log(` leak --file ./vape.jpg --public --public-confirm ${PUBLIC_CONFIRM_PHRASE}`);
31
- console.log(` leak --file ./vape.jpg --public --og-image-url ./cover.png`);
32
- console.log(` npm run leak -- --file ./vape.jpg`);
33
- console.log(` npm run leak -- --file ./vape.jpg --price 0.01 --window 1h --confirmed`);
80
+ console.log(" leak --file ./vape.jpg --public --og-image-url ./cover.png");
81
+ console.log(" npm run leak -- --file ./vape.jpg");
82
+ console.log(" npm run leak -- --file ./vape.jpg --price 0.01 --window 1h --confirmed");
34
83
  process.exit(code);
35
84
  }
36
85
 
@@ -71,6 +120,600 @@ function parseNonNegativeInt(value) {
71
120
  return Math.floor(n);
72
121
  }
73
122
 
123
+ function parsePositiveInt(value) {
124
+ if (value === undefined || value === null || value === "") return null;
125
+ const n = Number(value);
126
+ if (!Number.isFinite(n) || n <= 0) return null;
127
+ return Math.floor(n);
128
+ }
129
+
130
+ function trim(value) {
131
+ return String(value || "").trim();
132
+ }
133
+
134
+ function yesNoChoices() {
135
+ return [
136
+ { name: "yes", message: "Yes" },
137
+ { name: "no", message: "No" },
138
+ ];
139
+ }
140
+
141
+ async function promptYesNo(message, initialYes = true) {
142
+ const prompt = new Select({
143
+ name: "choice",
144
+ message,
145
+ choices: yesNoChoices(),
146
+ initial: initialYes ? 0 : 1,
147
+ });
148
+ const choice = await prompt.run();
149
+ return choice === "yes";
150
+ }
151
+
152
+ async function promptSelect(message, options, initialName) {
153
+ const normalizedOptions = options.map((opt) => ({ name: String(opt), message: String(opt) }));
154
+ const initialIndex = Math.max(
155
+ 0,
156
+ normalizedOptions.findIndex((opt) => opt.name === initialName),
157
+ );
158
+ const prompt = new Select({
159
+ name: "choice",
160
+ message,
161
+ choices: normalizedOptions,
162
+ initial: initialIndex,
163
+ });
164
+ return prompt.run();
165
+ }
166
+
167
+ async function promptMaskedDownloadCode(existingHash) {
168
+ if (existingHash) {
169
+ const keepExisting = await promptYesNo(
170
+ "Keep current stored download-code hash from config/env?",
171
+ true,
172
+ );
173
+ if (keepExisting) return { raw: "", hashOverride: existingHash };
174
+ }
175
+
176
+ const prompt = new HiddenCodePrompt({
177
+ name: "downloadCode",
178
+ message: "DOWNLOAD_CODE (hidden input)",
179
+ });
180
+ const raw = trim(await prompt.run());
181
+ if (!raw) throw new Error("DOWNLOAD_CODE cannot be empty");
182
+ return { raw, hashOverride: "" };
183
+ }
184
+
185
+ async function askWithDefaultReadline(rl, label, currentValue = "") {
186
+ const current = trim(currentValue);
187
+ const suffix = current ? ` [${current}]` : "";
188
+ const answer = trim(await rl.question(`${label}${suffix}: `));
189
+ return answer || current;
190
+ }
191
+
192
+ async function askWithDefault(label, currentValue = "") {
193
+ const current = trim(currentValue);
194
+ const prompt = new Input({
195
+ name: "value",
196
+ message: label,
197
+ initial: current,
198
+ });
199
+ const answer = trim(await prompt.run());
200
+ return answer || current;
201
+ }
202
+
203
+ function resolveInputPathForAutocomplete(inputPath) {
204
+ const raw = String(inputPath || "");
205
+ const expanded = expandHomePath(raw);
206
+ if (expanded !== raw) return expanded;
207
+ if (path.isAbsolute(raw)) return raw;
208
+ return path.resolve(process.cwd(), raw);
209
+ }
210
+
211
+ function pathAutocomplete(line) {
212
+ const raw = String(line || "");
213
+ if (raw === "~") return [["~/"], raw];
214
+
215
+ const hasSlash = raw.includes("/");
216
+ const endsWithSlash = raw.endsWith("/");
217
+ const splitIndex = raw.lastIndexOf("/");
218
+ const dirPart = endsWithSlash
219
+ ? raw
220
+ : (hasSlash ? raw.slice(0, splitIndex + 1) : ".");
221
+ const prefix = endsWithSlash ? "" : (hasSlash ? raw.slice(splitIndex + 1) : raw);
222
+
223
+ const fsDir = resolveInputPathForAutocomplete(dirPart);
224
+ let entries = [];
225
+ try {
226
+ entries = fs.readdirSync(fsDir, { withFileTypes: true });
227
+ } catch {
228
+ return [[], raw];
229
+ }
230
+
231
+ const base = raw.slice(0, raw.length - prefix.length);
232
+ const hits = entries
233
+ .filter((entry) => entry.name.startsWith(prefix))
234
+ .sort((a, b) => a.name.localeCompare(b.name))
235
+ .map((entry) => {
236
+ const suffix = entry.isDirectory() ? "/" : "";
237
+ return `${base}${entry.name}${suffix}`;
238
+ });
239
+
240
+ return [hits.length ? hits : [], raw];
241
+ }
242
+
243
+ function readDownloadCodeFromStdin() {
244
+ let data = "";
245
+ try {
246
+ data = fs.readFileSync(0, "utf8");
247
+ } catch {
248
+ throw new Error("Failed to read download code from stdin");
249
+ }
250
+ const firstLine = String(data).split(/\r?\n/, 1)[0]?.trim() || "";
251
+ if (!firstLine) {
252
+ throw new Error("No download code received on stdin");
253
+ }
254
+ return firstLine;
255
+ }
256
+
257
+ async function resolveDownloadCodeHash({
258
+ args,
259
+ configDefaults,
260
+ accessMode,
261
+ persistedHashOverride = undefined,
262
+ }) {
263
+ const requiresDownloadCode = accessModeRequiresDownloadCode(accessMode);
264
+ const hasInlineCode = typeof args["download-code"] !== "undefined";
265
+ const useStdinCode = Boolean(args["download-code-stdin"]);
266
+
267
+ if (hasInlineCode && useStdinCode) {
268
+ throw new Error("Use exactly one download code input: --download-code or --download-code-stdin");
269
+ }
270
+
271
+ let inlineCode = "";
272
+ if (hasInlineCode) {
273
+ if (args["download-code"] === true) {
274
+ throw new Error("--download-code requires a value");
275
+ }
276
+ inlineCode = String(args["download-code"] || "").trim();
277
+ if (!inlineCode) throw new Error("--download-code cannot be empty");
278
+ }
279
+
280
+ let stdinCode = "";
281
+ if (useStdinCode) {
282
+ stdinCode = readDownloadCodeFromStdin();
283
+ }
284
+
285
+ const persistedHash = persistedHashOverride === undefined
286
+ ? trim(process.env.DOWNLOAD_CODE_HASH || configDefaults.downloadCodeHash || "")
287
+ : trim(persistedHashOverride);
288
+
289
+ if (!requiresDownloadCode) {
290
+ if (inlineCode || stdinCode || persistedHash) {
291
+ throw new Error(
292
+ `ACCESS_MODE=${accessMode} does not accept download code input. Remove --download-code/--download-code-stdin and clear DOWNLOAD_CODE_HASH.`,
293
+ );
294
+ }
295
+ return "";
296
+ }
297
+
298
+ if (inlineCode) return hashDownloadCode(inlineCode);
299
+ if (stdinCode) return hashDownloadCode(stdinCode);
300
+ if (!persistedHash) {
301
+ throw new Error(
302
+ `ACCESS_MODE=${accessMode} requires a download code. Provide --download-code, --download-code-stdin, or DOWNLOAD_CODE_HASH.`,
303
+ );
304
+ }
305
+ if (!isValidDownloadCodeHash(persistedHash)) {
306
+ throw new Error("Invalid DOWNLOAD_CODE_HASH format");
307
+ }
308
+ return persistedHash;
309
+ }
310
+
311
+ function resolvePublishPrefill({ args, configDefaults }) {
312
+ const accessModeInput = trim(
313
+ args["access-mode"] ||
314
+ process.env.ACCESS_MODE ||
315
+ configDefaults.accessMode ||
316
+ DEFAULT_ACCESS_MODE,
317
+ ).toLowerCase();
318
+ const accessMode = isValidAccessMode(accessModeInput)
319
+ ? accessModeInput
320
+ : DEFAULT_ACCESS_MODE;
321
+
322
+ const requiresPayment = accessModeRequiresPayment(accessMode);
323
+ const requiresDownloadCode = accessModeRequiresDownloadCode(accessMode);
324
+
325
+ const networkInput = trim(
326
+ args.network || process.env.CHAIN_ID || configDefaults.chainId || "eip155:84532",
327
+ );
328
+
329
+ const facilitatorModeInput = trim(
330
+ args["facilitator-mode"] ||
331
+ process.env.FACILITATOR_MODE ||
332
+ configDefaults.facilitatorMode ||
333
+ "testnet",
334
+ ).toLowerCase();
335
+ const facilitatorMode = ALLOWED_FACILITATOR_MODES.has(facilitatorModeInput)
336
+ ? facilitatorModeInput
337
+ : "testnet";
338
+
339
+ const facilitatorUrl = trim(
340
+ args["facilitator-url"] ||
341
+ process.env.FACILITATOR_URL ||
342
+ configDefaults.facilitatorUrl ||
343
+ defaultFacilitatorUrlForMode(facilitatorMode),
344
+ );
345
+
346
+ const confirmationPolicyInput = trim(
347
+ args["confirmation-policy"] ||
348
+ (args.confirmed
349
+ ? "confirmed"
350
+ : process.env.CONFIRMATION_POLICY || configDefaults.confirmationPolicy || "confirmed"),
351
+ ).toLowerCase();
352
+ const confirmationPolicy = ALLOWED_CONFIRMATION_POLICIES.has(confirmationPolicyInput)
353
+ ? confirmationPolicyInput
354
+ : "confirmed";
355
+
356
+ const publicEnabled = Boolean(args.public);
357
+ const endedWindowArg =
358
+ args["ended-window-seconds"] ??
359
+ process.env.ENDED_WINDOW_SECONDS ??
360
+ configDefaults.endedWindowSeconds;
361
+ const endedWindowExplicit =
362
+ endedWindowArg !== undefined && endedWindowArg !== null && String(endedWindowArg) !== "";
363
+ const parsedEndedWindow = parseNonNegativeInt(endedWindowArg);
364
+ const endedWindowSeconds =
365
+ parsedEndedWindow !== null ? parsedEndedWindow : publicEnabled ? 86400 : 0;
366
+
367
+ const parsedPort = parsePositiveInt(
368
+ args.port || process.env.PORT || configDefaults.port || 4021,
369
+ );
370
+ const port = parsedPort || 4021;
371
+
372
+ return {
373
+ file: trim(args.file || ""),
374
+ accessMode,
375
+ requiresPayment,
376
+ requiresDownloadCode,
377
+ payTo: trim(
378
+ args["pay-to"] || process.env.SELLER_PAY_TO || configDefaults.sellerPayTo || "",
379
+ ),
380
+ price: trim(args.price || process.env.PRICE_USD || configDefaults.priceUsd || "0.01"),
381
+ window: trim(args.window || process.env.WINDOW_SECONDS || configDefaults.window || "1h"),
382
+ networkInput,
383
+ publicEnabled,
384
+ endedWindowSeconds,
385
+ endedWindowExplicit,
386
+ port,
387
+ confirmationPolicy,
388
+ facilitatorMode,
389
+ facilitatorUrl,
390
+ cdpApiKeyId: trim(
391
+ args["cdp-api-key-id"] || process.env.CDP_API_KEY_ID || configDefaults.cdpApiKeyId || "",
392
+ ),
393
+ cdpApiKeySecret: trim(
394
+ args["cdp-api-key-secret"] ||
395
+ process.env.CDP_API_KEY_SECRET ||
396
+ configDefaults.cdpApiKeySecret ||
397
+ "",
398
+ ),
399
+ ogTitle: trim(
400
+ typeof args["og-title"] === "string"
401
+ ? args["og-title"]
402
+ : process.env.OG_TITLE || configDefaults.ogTitle || "",
403
+ ),
404
+ ogDescription: trim(
405
+ typeof args["og-description"] === "string"
406
+ ? args["og-description"]
407
+ : process.env.OG_DESCRIPTION || configDefaults.ogDescription || "",
408
+ ),
409
+ ogImageInput: trim(
410
+ typeof args["og-image-url"] === "string"
411
+ ? args["og-image-url"]
412
+ : process.env.OG_IMAGE_URL || "",
413
+ ),
414
+ downloadCodeHash: trim(
415
+ args["download-code-hash"] ||
416
+ process.env.DOWNLOAD_CODE_HASH ||
417
+ configDefaults.downloadCodeHash ||
418
+ "",
419
+ ),
420
+ rawDownloadCode: trim(
421
+ typeof args["download-code"] === "string" ? args["download-code"] : "",
422
+ ),
423
+ };
424
+ }
425
+
426
+ async function runPublishWizard({ args, configDefaults }) {
427
+ if (!input.isTTY || !output.isTTY) {
428
+ throw new Error("Interactive publish wizard requires a TTY. Use direct flags in non-interactive mode.");
429
+ }
430
+
431
+ const prefill = resolvePublishPrefill({ args, configDefaults });
432
+ const filePathRl = readline.createInterface({
433
+ input,
434
+ output,
435
+ completer: pathAutocomplete,
436
+ });
437
+ let state = { ...prefill };
438
+
439
+ console.log(outUi.heading("Interactive Publish Wizard"));
440
+ console.log(outUi.muted("Press Enter to keep defaults shown in brackets."));
441
+ console.log(outUi.muted("FILE_PATH supports Tab autocomplete."));
442
+ console.log("");
443
+
444
+ try {
445
+ state.file = await askWithDefaultReadline(filePathRl, "FILE_PATH", state.file);
446
+ while (true) {
447
+ state.file = trim(state.file);
448
+ if (!state.file) {
449
+ logError("FILE_PATH is required.");
450
+ } else {
451
+ try {
452
+ resolveAndValidateArtifactPath(state.file, args);
453
+ break;
454
+ } catch (err) {
455
+ logError(err.message || String(err));
456
+ }
457
+ }
458
+ state.file = await askWithDefaultReadline(filePathRl, "FILE_PATH", state.file);
459
+ }
460
+ filePathRl.close();
461
+
462
+ state.accessMode = await promptSelect(
463
+ "ACCESS_MODE",
464
+ ACCESS_MODE_VALUES,
465
+ state.accessMode,
466
+ );
467
+ state.requiresPayment = accessModeRequiresPayment(state.accessMode);
468
+ state.requiresDownloadCode = accessModeRequiresDownloadCode(state.accessMode);
469
+
470
+ state.rawDownloadCode = "";
471
+ state.downloadCodeHash = state.requiresDownloadCode ? state.downloadCodeHash : "";
472
+ if (
473
+ state.requiresDownloadCode &&
474
+ state.downloadCodeHash &&
475
+ !isValidDownloadCodeHash(state.downloadCodeHash)
476
+ ) {
477
+ logWarn("Existing DOWNLOAD_CODE_HASH is invalid; please enter a new download-code.");
478
+ state.downloadCodeHash = "";
479
+ }
480
+ if (state.requiresDownloadCode) {
481
+ const resolved = await promptMaskedDownloadCode(state.downloadCodeHash);
482
+ state.rawDownloadCode = resolved.raw;
483
+ state.downloadCodeHash = resolved.hashOverride;
484
+ }
485
+
486
+ if (state.requiresPayment) {
487
+ state.price = await askWithDefault("PRICE_USD", state.price || "0.01");
488
+ while (!state.price || Number.isNaN(Number(state.price))) {
489
+ logError("PRICE_USD must be numeric.");
490
+ state.price = await askWithDefault("PRICE_USD", state.price || "0.01");
491
+ }
492
+ } else {
493
+ state.price = "0";
494
+ }
495
+
496
+ let windowInput = await askWithDefault("WINDOW (e.g. 15m, 1h, 3600)", state.window || "1h");
497
+ let parsedWindowSeconds = parseDurationToSeconds(windowInput);
498
+ while (!parsedWindowSeconds || parsedWindowSeconds <= 0) {
499
+ logError("Invalid WINDOW. Use formats like 15m, 1h, or 3600.");
500
+ windowInput = await askWithDefault("WINDOW (e.g. 15m, 1h, 3600)", windowInput || "1h");
501
+ parsedWindowSeconds = parseDurationToSeconds(windowInput);
502
+ }
503
+ state.window = `${parsedWindowSeconds}s`;
504
+
505
+ if (state.requiresPayment) {
506
+ state.payTo = await askWithDefault("SELLER_PAY_TO", state.payTo);
507
+ while (!state.payTo || !isAddress(state.payTo)) {
508
+ if (!state.payTo) logError("SELLER_PAY_TO is required for payment modes.");
509
+ else logError("Invalid SELLER_PAY_TO. Expected a valid Ethereum address.");
510
+ state.payTo = await askWithDefault("SELLER_PAY_TO", state.payTo);
511
+ }
512
+ } else {
513
+ state.payTo = "";
514
+ }
515
+
516
+ let networkInput = await askWithDefault("CHAIN_ID", state.networkInput || "eip155:84532");
517
+ while (true) {
518
+ try {
519
+ state.networkInput = resolveSupportedChain(networkInput).caip2;
520
+ break;
521
+ } catch (err) {
522
+ logError(err.message || String(err));
523
+ networkInput = await askWithDefault("CHAIN_ID", networkInput || "eip155:84532");
524
+ }
525
+ }
526
+
527
+ state.publicEnabled = await promptYesNo(
528
+ "Expose this publish run via temporary Cloudflare tunnel (--public)?",
529
+ state.publicEnabled,
530
+ );
531
+
532
+ const useAdvanced = await promptYesNo(
533
+ "Configure advanced options (facilitator, ports, OG metadata, ended-window)?",
534
+ false,
535
+ );
536
+
537
+ if (useAdvanced) {
538
+ if (state.requiresPayment) {
539
+ state.confirmationPolicy = await promptSelect(
540
+ "CONFIRMATION_POLICY",
541
+ ["confirmed", "optimistic"],
542
+ state.confirmationPolicy,
543
+ );
544
+ } else {
545
+ state.confirmationPolicy = "confirmed";
546
+ }
547
+
548
+ let portInput = await askWithDefault("PORT", String(state.port || 4021));
549
+ let parsedPort = parsePositiveInt(portInput);
550
+ while (!parsedPort) {
551
+ logError("PORT must be a positive integer.");
552
+ portInput = await askWithDefault("PORT", String(state.port || 4021));
553
+ parsedPort = parsePositiveInt(portInput);
554
+ }
555
+ state.port = parsedPort;
556
+
557
+ let endedWindowInput = await askWithDefault(
558
+ "ENDED_WINDOW_SECONDS",
559
+ String(state.endedWindowSeconds),
560
+ );
561
+ let parsedEnded = parseNonNegativeInt(endedWindowInput);
562
+ while (parsedEnded === null) {
563
+ logError("ENDED_WINDOW_SECONDS must be a non-negative integer.");
564
+ endedWindowInput = await askWithDefault(
565
+ "ENDED_WINDOW_SECONDS",
566
+ String(state.endedWindowSeconds),
567
+ );
568
+ parsedEnded = parseNonNegativeInt(endedWindowInput);
569
+ }
570
+ state.endedWindowSeconds = parsedEnded;
571
+ state.endedWindowExplicit = true;
572
+
573
+ state.ogTitle = await askWithDefault("OG_TITLE", state.ogTitle);
574
+ state.ogDescription = await askWithDefault("OG_DESCRIPTION", state.ogDescription);
575
+ state.ogImageInput = await askWithDefault(
576
+ "OG_IMAGE_URL (http(s) URL or local file path)",
577
+ state.ogImageInput,
578
+ );
579
+
580
+ state.facilitatorMode = await promptSelect(
581
+ "FACILITATOR_MODE",
582
+ ["testnet", "cdp_mainnet"],
583
+ state.facilitatorMode,
584
+ );
585
+ state.facilitatorUrl = await askWithDefault(
586
+ "FACILITATOR_URL",
587
+ state.facilitatorUrl || defaultFacilitatorUrlForMode(state.facilitatorMode),
588
+ );
589
+
590
+ if (state.facilitatorMode === "cdp_mainnet") {
591
+ state.cdpApiKeyId = await askWithDefault("CDP_API_KEY_ID", state.cdpApiKeyId);
592
+ while (!state.cdpApiKeyId) {
593
+ logError("CDP_API_KEY_ID is required when FACILITATOR_MODE=cdp_mainnet.");
594
+ state.cdpApiKeyId = await askWithDefault("CDP_API_KEY_ID", state.cdpApiKeyId);
595
+ }
596
+ state.cdpApiKeySecret = await askWithDefault(
597
+ "CDP_API_KEY_SECRET",
598
+ state.cdpApiKeySecret,
599
+ );
600
+ while (!state.cdpApiKeySecret) {
601
+ logError("CDP_API_KEY_SECRET is required when FACILITATOR_MODE=cdp_mainnet.");
602
+ state.cdpApiKeySecret = await askWithDefault(
603
+ "CDP_API_KEY_SECRET",
604
+ state.cdpApiKeySecret,
605
+ );
606
+ }
607
+ }
608
+ } else {
609
+ if (!state.endedWindowExplicit) {
610
+ state.endedWindowSeconds = state.publicEnabled ? 86400 : 0;
611
+ }
612
+ }
613
+
614
+ console.log("");
615
+ console.log(outUi.section("Publish Summary"));
616
+ const summaryRows = [
617
+ { key: "file", value: state.file },
618
+ { key: "access_mode", value: state.accessMode },
619
+ { key: "download_code", value: state.requiresDownloadCode ? "required" : "not required" },
620
+ { key: "price", value: `${state.price} USDC` },
621
+ { key: "window", value: state.window },
622
+ { key: "network", value: state.networkInput },
623
+ { key: "public_tunnel", value: state.publicEnabled ? "yes" : "no" },
624
+ state.requiresPayment ? { key: "pay_to", value: state.payTo } : null,
625
+ { key: "facilitator_mode", value: state.facilitatorMode },
626
+ { key: "facilitator_url", value: state.facilitatorUrl },
627
+ {
628
+ key: "confirmation_policy",
629
+ value: state.requiresPayment ? state.confirmationPolicy : "n/a (payment disabled)",
630
+ },
631
+ { key: "port", value: state.port },
632
+ { key: "ended_window_seconds", value: state.endedWindowSeconds },
633
+ state.ogTitle ? { key: "og_title", value: state.ogTitle } : null,
634
+ state.ogDescription ? { key: "og_description", value: state.ogDescription } : null,
635
+ state.ogImageInput ? { key: "og_image_url", value: state.ogImageInput } : null,
636
+ ];
637
+ for (const line of outUi.formatRows(summaryRows)) {
638
+ console.log(line);
639
+ }
640
+
641
+ const confirmedLaunch = await promptYesNo("Launch publish with these settings?", true);
642
+ if (!confirmedLaunch) {
643
+ throw new Error("Publish wizard cancelled before launch.");
644
+ }
645
+
646
+ const saveDefaults = await promptYesNo(
647
+ "Save these values to ~/.leak/config.json as defaults?",
648
+ false,
649
+ );
650
+
651
+ let downloadCodeHashForSave = "";
652
+ if (state.requiresDownloadCode) {
653
+ if (state.rawDownloadCode) {
654
+ downloadCodeHashForSave = await hashDownloadCode(state.rawDownloadCode);
655
+ } else {
656
+ downloadCodeHashForSave = state.downloadCodeHash;
657
+ }
658
+ }
659
+
660
+ if (saveDefaults) {
661
+ const defaults = {
662
+ ...(configDefaults || {}),
663
+ sellerPayTo: state.requiresPayment
664
+ ? state.payTo
665
+ : trim(configDefaults?.sellerPayTo || ""),
666
+ chainId: state.networkInput,
667
+ facilitatorMode: state.facilitatorMode,
668
+ facilitatorUrl: state.facilitatorUrl,
669
+ cdpApiKeyId: state.cdpApiKeyId,
670
+ cdpApiKeySecret: state.cdpApiKeySecret,
671
+ confirmationPolicy: state.confirmationPolicy,
672
+ priceUsd: state.price,
673
+ window: state.window,
674
+ port: state.port,
675
+ endedWindowSeconds: state.endedWindowSeconds,
676
+ ogTitle: state.ogTitle,
677
+ ogDescription: state.ogDescription,
678
+ accessMode: state.accessMode,
679
+ downloadCodeHash: downloadCodeHashForSave,
680
+ };
681
+ const written = writeConfig({ version: 1, defaults });
682
+ logOk(`Saved defaults: ${written.path}`);
683
+ }
684
+
685
+ args.file = state.file;
686
+ args["access-mode"] = state.accessMode;
687
+ args["download-code-hash"] = state.requiresDownloadCode
688
+ ? (state.rawDownloadCode ? "" : state.downloadCodeHash)
689
+ : "";
690
+ if (state.requiresDownloadCode && state.rawDownloadCode) args["download-code"] = state.rawDownloadCode;
691
+ else delete args["download-code"];
692
+ delete args["download-code-stdin"];
693
+
694
+ args.price = state.price;
695
+ args.window = state.window;
696
+ args.network = state.networkInput;
697
+ args.port = String(state.port);
698
+ args["ended-window-seconds"] = String(state.endedWindowSeconds);
699
+ args["pay-to"] = state.payTo;
700
+ args.public = state.publicEnabled;
701
+ args["confirmation-policy"] = state.confirmationPolicy;
702
+ if (state.confirmationPolicy === "confirmed") args.confirmed = true;
703
+ else delete args.confirmed;
704
+
705
+ args["facilitator-mode"] = state.facilitatorMode;
706
+ args["facilitator-url"] = state.facilitatorUrl;
707
+ args["cdp-api-key-id"] = state.cdpApiKeyId;
708
+ args["cdp-api-key-secret"] = state.cdpApiKeySecret;
709
+ args["og-title"] = state.ogTitle;
710
+ args["og-description"] = state.ogDescription;
711
+ args["og-image-url"] = state.ogImageInput;
712
+ } finally {
713
+ filePathRl.close();
714
+ }
715
+ }
716
+
74
717
  function isAbsoluteHttpUrl(value) {
75
718
  try {
76
719
  const u = new URL(String(value));
@@ -130,18 +773,18 @@ function cloudflaredPreflight() {
130
773
  }
131
774
 
132
775
  function printCloudflaredInstallHelp(localOnlyCmd) {
133
- console.error("[leak] --public requested, but cloudflared is unavailable.");
134
- console.error("[leak] cloudflared is required to create a public tunnel URL.");
776
+ logError("--public requested, but cloudflared is unavailable.");
777
+ logWarn("cloudflared is required to create a public tunnel URL.");
135
778
  console.error("");
136
- console.error("[leak] Install cloudflared:");
779
+ console.error(errUi.section("Install cloudflared"));
137
780
  console.error(" macOS (Homebrew): brew install cloudflared");
138
781
  console.error(" Windows (winget): winget install --id Cloudflare.cloudflared");
139
782
  console.error(" Linux packages/docs: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/");
140
783
  console.error("");
141
- console.error("[leak] Retry public mode after install:");
784
+ console.error(errUi.section("Retry"));
142
785
  console.error(" leak --file <path> --pay-to <address> --public");
143
786
  console.error("");
144
- console.error("[leak] Local-only alternative (no tunnel):");
787
+ console.error(errUi.section("Local-only Alternative (No Tunnel)"));
145
788
  console.error(` ${localOnlyCmd}`);
146
789
  }
147
790
 
@@ -169,8 +812,18 @@ function parseDurationToSeconds(s) {
169
812
  return null;
170
813
  }
171
814
 
815
+ function expandHomePath(inputPath) {
816
+ const raw = String(inputPath || "");
817
+ const home = process.env.HOME || "";
818
+ if (!home) return raw;
819
+ if (raw === "~") return home;
820
+ if (raw.startsWith("~/")) return path.join(home, raw.slice(2));
821
+ return raw;
822
+ }
823
+
172
824
  function resolveFile(p) {
173
- const abs = path.isAbsolute(p) ? p : path.resolve(process.cwd(), p);
825
+ const expanded = expandHomePath(p);
826
+ const abs = path.isAbsolute(expanded) ? expanded : path.resolve(process.cwd(), expanded);
174
827
  return abs;
175
828
  }
176
829
 
@@ -258,7 +911,7 @@ async function ensurePublicExposureConfirmed(args) {
258
911
 
259
912
  const rl = readline.createInterface({ input, output });
260
913
  try {
261
- console.log("[leak] You are about to expose a local file to the public internet.");
914
+ logWarn("You are about to expose a local file to the public internet.");
262
915
  const answer = (await rl.question(`[leak] Type ${PUBLIC_CONFIRM_PHRASE} to continue: `)).trim();
263
916
  if (answer !== PUBLIC_CONFIRM_PHRASE) {
264
917
  throw new Error("Public exposure confirmation failed. Aborting.");
@@ -268,16 +921,20 @@ async function ensurePublicExposureConfirmed(args) {
268
921
  }
269
922
  }
270
923
 
271
- async function promptMissing({ price, windowSeconds }) {
924
+ async function promptMissing({ price, windowSeconds, requiresPayment }) {
272
925
  const rl = readline.createInterface({ input, output });
273
926
  try {
274
- let p = price;
275
- if (!p) {
276
- p = (await rl.question("How much (USDC)? e.g. 0.01 or $0.01: ")).trim();
927
+ let p = requiresPayment ? price : (price || "0");
928
+ if (requiresPayment) {
929
+ if (!p) {
930
+ p = (await rl.question("How much (USDC)? e.g. 0.01 or $0.01: ")).trim();
931
+ }
932
+ p = String(p).trim();
933
+ if (p.startsWith("$")) p = p.slice(1).trim();
934
+ if (!p || Number.isNaN(Number(p))) throw new Error("Invalid price");
935
+ } else {
936
+ p = "0";
277
937
  }
278
- p = String(p).trim();
279
- if (p.startsWith("$")) p = p.slice(1).trim();
280
- if (!p || Number.isNaN(Number(p))) throw new Error("Invalid price");
281
938
 
282
939
  let w = windowSeconds;
283
940
  if (!w) {
@@ -292,14 +949,394 @@ async function promptMissing({ price, windowSeconds }) {
292
949
  }
293
950
  }
294
951
 
952
+ function nowSeconds() {
953
+ return Math.floor(Date.now() / 1000);
954
+ }
955
+
956
+ function toIsoSeconds(ts) {
957
+ return new Date(Number(ts) * 1000).toISOString();
958
+ }
959
+
960
+ function bestEffortChmod(targetPath, mode) {
961
+ try {
962
+ fs.chmodSync(targetPath, mode);
963
+ } catch {
964
+ // best effort only
965
+ }
966
+ }
967
+
968
+ function getRunsDirPath() {
969
+ const home = process.env.HOME || os.homedir();
970
+ return path.join(home, RUNS_DIR);
971
+ }
972
+
973
+ function ensureRunsDir() {
974
+ const runsDir = getRunsDirPath();
975
+ fs.mkdirSync(runsDir, { recursive: true });
976
+ bestEffortChmod(runsDir, 0o700);
977
+ return runsDir;
978
+ }
979
+
980
+ function createRunStatePaths(runId) {
981
+ const runsDir = ensureRunsDir();
982
+ return {
983
+ runsDir,
984
+ statePath: path.join(runsDir, `${runId}.json`),
985
+ latestPath: path.join(runsDir, "latest.json"),
986
+ };
987
+ }
988
+
989
+ function persistRunState(paths, runState) {
990
+ const nextState = {
991
+ ...runState,
992
+ updatedAtTs: nowSeconds(),
993
+ };
994
+ const serialized = `${JSON.stringify(nextState, null, 2)}\n`;
995
+ fs.writeFileSync(paths.statePath, serialized, { mode: 0o600 });
996
+ bestEffortChmod(paths.statePath, 0o600);
997
+
998
+ const latest = {
999
+ runId: nextState.runId,
1000
+ statePath: paths.statePath,
1001
+ status: nextState.status,
1002
+ updatedAtTs: nextState.updatedAtTs,
1003
+ };
1004
+ fs.writeFileSync(paths.latestPath, `${JSON.stringify(latest, null, 2)}\n`, { mode: 0o600 });
1005
+ bestEffortChmod(paths.latestPath, 0o600);
1006
+ return nextState;
1007
+ }
1008
+
1009
+ function computeRestartDelayMs(restartCount) {
1010
+ const baseMs = 1000;
1011
+ const capped = Math.min(30000, baseMs * 2 ** Math.max(0, restartCount - 1));
1012
+ const jitter = 0.8 + Math.random() * 0.4;
1013
+ return Math.max(250, Math.floor(capped * jitter));
1014
+ }
1015
+
1016
+ function sleepWithCancel(ms, registerCancel) {
1017
+ const durationMs = Math.max(0, Number(ms) || 0);
1018
+ return new Promise((resolve) => {
1019
+ if (!durationMs) {
1020
+ registerCancel?.(null);
1021
+ resolve();
1022
+ return;
1023
+ }
1024
+ let done = false;
1025
+ const finish = () => {
1026
+ if (done) return;
1027
+ done = true;
1028
+ registerCancel?.(null);
1029
+ resolve();
1030
+ };
1031
+ const timer = setTimeout(finish, durationMs);
1032
+ registerCancel?.(() => {
1033
+ clearTimeout(timer);
1034
+ finish();
1035
+ });
1036
+ });
1037
+ }
1038
+
1039
+ function runWorkerOnce({
1040
+ args,
1041
+ port,
1042
+ env,
1043
+ remainingUntilHardStopSeconds,
1044
+ registerManualStop,
1045
+ onTunnelUrls,
1046
+ }) {
1047
+ return new Promise((resolve) => {
1048
+ let settled = false;
1049
+ let stopReason = "";
1050
+ let tunnelFatalDetail = "";
1051
+ let tunnelProc = null;
1052
+ let stopTimer = null;
1053
+
1054
+ const child = spawn(process.execPath, [SERVER_ENTRY], {
1055
+ env,
1056
+ stdio: "inherit",
1057
+ });
1058
+
1059
+ const finish = (result) => {
1060
+ if (settled) return;
1061
+ settled = true;
1062
+ if (stopTimer) clearTimeout(stopTimer);
1063
+ registerManualStop(null);
1064
+ try {
1065
+ tunnelProc?.kill("SIGTERM");
1066
+ } catch {}
1067
+ resolve(result);
1068
+ };
1069
+
1070
+ const stopAll = (reason) => {
1071
+ if (stopReason) return;
1072
+ stopReason = reason;
1073
+ try {
1074
+ child.kill("SIGTERM");
1075
+ } catch {}
1076
+ try {
1077
+ tunnelProc?.kill("SIGTERM");
1078
+ } catch {}
1079
+ };
1080
+
1081
+ registerManualStop(() => stopAll("manual_stop"));
1082
+
1083
+ child.on("error", (err) => {
1084
+ finish({ reason: "child_crash", detail: `failed to start server process: ${err.message}` });
1085
+ });
1086
+
1087
+ if (args.public) {
1088
+ logInfo("Starting Cloudflare quick tunnel...");
1089
+ tunnelProc = spawn(
1090
+ "cloudflared",
1091
+ ["tunnel", "--url", `http://localhost:${port}`, "--no-autoupdate"],
1092
+ { stdio: ["ignore", "pipe", "pipe"] },
1093
+ );
1094
+
1095
+ tunnelProc.on("error", (err) => {
1096
+ if (err.code === "ENOENT") {
1097
+ tunnelFatalDetail = "cloudflared not found. Install it or re-run without --public.";
1098
+ } else {
1099
+ tunnelFatalDetail = `failed to start tunnel: ${err.message}`;
1100
+ }
1101
+ stopAll("tunnel_fatal");
1102
+ });
1103
+
1104
+ const urlRegex = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/gi;
1105
+ const onData = (chunk) => {
1106
+ const s = chunk.toString("utf8");
1107
+ const m = s.match(urlRegex);
1108
+ if (m && m[0]) {
1109
+ const publicUrl = m[0];
1110
+ const promoUrl = `${publicUrl}/`;
1111
+ const buyUrl = `${publicUrl}/download`;
1112
+ console.log("");
1113
+ console.log(outUi.section("Public Tunnel"));
1114
+ for (const line of outUi.formatRows([
1115
+ { key: "public_url", value: outUi.link(publicUrl) },
1116
+ { key: "promo_link", value: outUi.link(promoUrl) },
1117
+ { key: "buy_link", value: outUi.link(buyUrl) },
1118
+ ])) {
1119
+ console.log(line);
1120
+ }
1121
+ onTunnelUrls?.({ publicUrl, promoUrl, buyUrl });
1122
+ tunnelProc?.stdout?.off("data", onData);
1123
+ tunnelProc?.stderr?.off("data", onData);
1124
+ }
1125
+ };
1126
+
1127
+ tunnelProc.stdout.on("data", onData);
1128
+ tunnelProc.stderr.on("data", onData);
1129
+
1130
+ tunnelProc.on("exit", (code, signal) => {
1131
+ if (stopReason) {
1132
+ if (signal) logWarn(`Tunnel exited (signal ${signal})`);
1133
+ else logInfo(`Tunnel exited (code ${code})`);
1134
+ return;
1135
+ }
1136
+ tunnelFatalDetail = signal
1137
+ ? `tunnel exited unexpectedly (signal ${signal})`
1138
+ : `tunnel exited unexpectedly (code ${code})`;
1139
+ stopAll("tunnel_fatal");
1140
+ });
1141
+ }
1142
+
1143
+ stopTimer = setTimeout(
1144
+ () => stopAll("deadline_stop"),
1145
+ Math.max(0, remainingUntilHardStopSeconds) * 1000,
1146
+ );
1147
+
1148
+ child.on("exit", (code, signal) => {
1149
+ if (stopReason === "manual_stop") {
1150
+ finish({ reason: "manual_stop" });
1151
+ return;
1152
+ }
1153
+ if (stopReason === "deadline_stop") {
1154
+ finish({ reason: "normal_window_stop" });
1155
+ return;
1156
+ }
1157
+ if (stopReason === "tunnel_fatal") {
1158
+ finish({
1159
+ reason: "tunnel_fatal",
1160
+ detail: tunnelFatalDetail || "public tunnel failed unexpectedly",
1161
+ });
1162
+ return;
1163
+ }
1164
+ if (signal) {
1165
+ finish({ reason: "child_crash", detail: `server exited unexpectedly (signal ${signal})` });
1166
+ return;
1167
+ }
1168
+ finish({ reason: "child_crash", detail: `server exited unexpectedly (code ${code ?? "n/a"})` });
1169
+ });
1170
+ });
1171
+ }
1172
+
1173
+ async function supervisorMain({
1174
+ args,
1175
+ port,
1176
+ envBase,
1177
+ saleStartTsFixed,
1178
+ saleEndTsFixed,
1179
+ hardStopTsFixed,
1180
+ effectiveEndedWindowSeconds,
1181
+ runStatePaths,
1182
+ runState,
1183
+ }) {
1184
+ console.log("");
1185
+ console.log(outUi.section("Supervisor"));
1186
+ for (const line of outUi.formatRows([
1187
+ { key: "run_id", value: runState.runId },
1188
+ { key: "state_file", value: runStatePaths.statePath },
1189
+ { key: "sale_end", value: toIsoSeconds(saleEndTsFixed) },
1190
+ { key: "hard_stop", value: toIsoSeconds(hardStopTsFixed) },
1191
+ ])) {
1192
+ console.log(line);
1193
+ }
1194
+
1195
+ let manualStopRequested = false;
1196
+ let activeManualStop = null;
1197
+ let pendingDelayCancel = null;
1198
+
1199
+ const handleSignal = (signalName) => {
1200
+ if (manualStopRequested) return;
1201
+ manualStopRequested = true;
1202
+ logWarn(`Received ${signalName}; stopping supervisor...`);
1203
+ if (typeof activeManualStop === "function") activeManualStop();
1204
+ if (typeof pendingDelayCancel === "function") pendingDelayCancel();
1205
+ };
1206
+ const onSigInt = () => handleSignal("SIGINT");
1207
+ const onSigTerm = () => handleSignal("SIGTERM");
1208
+ process.on("SIGINT", onSigInt);
1209
+ process.on("SIGTERM", onSigTerm);
1210
+
1211
+ try {
1212
+ while (true) {
1213
+ const nowTs = nowSeconds();
1214
+ const remainingSaleSeconds = Math.max(0, saleEndTsFixed - nowTs);
1215
+ const remainingUntilHardStopSeconds = Math.max(0, hardStopTsFixed - nowTs);
1216
+
1217
+ if (remainingUntilHardStopSeconds <= 0) {
1218
+ runState.status = "stopped";
1219
+ runState.lastExitReason = "normal_window_stop";
1220
+ runState = persistRunState(runStatePaths, runState);
1221
+ if (effectiveEndedWindowSeconds > 0) {
1222
+ logInfo(`Ended-window elapsed (${effectiveEndedWindowSeconds}s after sale end). stopping...`);
1223
+ } else {
1224
+ logInfo(`Window expired. stopping...`);
1225
+ }
1226
+ return 0;
1227
+ }
1228
+
1229
+ const env = {
1230
+ ...envBase,
1231
+ WINDOW_SECONDS: String(remainingSaleSeconds),
1232
+ SALE_START_TS: String(saleStartTsFixed),
1233
+ SALE_END_TS: String(saleEndTsFixed),
1234
+ ENDED_WINDOW_SECONDS: String(effectiveEndedWindowSeconds),
1235
+ };
1236
+
1237
+ const result = await runWorkerOnce({
1238
+ args,
1239
+ port,
1240
+ env,
1241
+ remainingUntilHardStopSeconds,
1242
+ registerManualStop: (nextStop) => {
1243
+ activeManualStop = typeof nextStop === "function" ? nextStop : null;
1244
+ },
1245
+ onTunnelUrls: (urls) => {
1246
+ runState.latestPublicUrl = urls.publicUrl;
1247
+ runState.latestPromoUrl = urls.promoUrl;
1248
+ runState.latestBuyUrl = urls.buyUrl;
1249
+ runState = persistRunState(runStatePaths, runState);
1250
+ },
1251
+ });
1252
+ activeManualStop = null;
1253
+ runState.lastExitReason = result.reason;
1254
+ runState = persistRunState(runStatePaths, runState);
1255
+
1256
+ if (manualStopRequested || result.reason === "manual_stop") {
1257
+ runState.status = "stopped";
1258
+ runState.lastExitReason = "manual_stop";
1259
+ runState = persistRunState(runStatePaths, runState);
1260
+ logInfo("Stopped by user request.");
1261
+ return 0;
1262
+ }
1263
+
1264
+ if (result.reason === "normal_window_stop") {
1265
+ runState.status = "stopped";
1266
+ runState = persistRunState(runStatePaths, runState);
1267
+ if (effectiveEndedWindowSeconds > 0) {
1268
+ logInfo(`Ended-window elapsed (${effectiveEndedWindowSeconds}s after sale end). stopping...`);
1269
+ } else {
1270
+ logInfo("Window expired. stopping...");
1271
+ }
1272
+ return 0;
1273
+ }
1274
+
1275
+ if (result.reason === "child_crash" || result.reason === "tunnel_fatal") {
1276
+ runState.restartCount += 1;
1277
+ runState.status = "running";
1278
+ runState = persistRunState(runStatePaths, runState);
1279
+
1280
+ const remainingSeconds = Math.max(0, hardStopTsFixed - nowSeconds());
1281
+ if (remainingSeconds <= 0) {
1282
+ runState.status = "stopped";
1283
+ runState.lastExitReason = "normal_window_stop";
1284
+ runState = persistRunState(runStatePaths, runState);
1285
+ logInfo("Hard-stop deadline reached. stopping...");
1286
+ return 0;
1287
+ }
1288
+
1289
+ const requestedDelayMs = computeRestartDelayMs(runState.restartCount);
1290
+ const delayMs = Math.min(requestedDelayMs, remainingSeconds * 1000);
1291
+ const detailSuffix = result.detail ? `: ${result.detail}` : "";
1292
+ logWarn(
1293
+ `Worker exited (${result.reason}${detailSuffix}). Restarting in ${(delayMs / 1000).toFixed(1)}s...`,
1294
+ );
1295
+ await sleepWithCancel(delayMs, (cancel) => {
1296
+ pendingDelayCancel = cancel;
1297
+ });
1298
+ pendingDelayCancel = null;
1299
+ if (manualStopRequested) {
1300
+ runState.status = "stopped";
1301
+ runState.lastExitReason = "manual_stop";
1302
+ runState = persistRunState(runStatePaths, runState);
1303
+ logInfo("Stopped by user request.");
1304
+ return 0;
1305
+ }
1306
+ continue;
1307
+ }
1308
+
1309
+ runState.status = "failed";
1310
+ runState.lastExitReason = result.reason || "config_fatal";
1311
+ runState = persistRunState(runStatePaths, runState);
1312
+ logError(`Supervisor failed with non-retriable reason: ${runState.lastExitReason}`);
1313
+ return 1;
1314
+ }
1315
+ } finally {
1316
+ process.off("SIGINT", onSigInt);
1317
+ process.off("SIGTERM", onSigTerm);
1318
+ if (typeof activeManualStop === "function") activeManualStop();
1319
+ if (typeof pendingDelayCancel === "function") pendingDelayCancel();
1320
+ }
1321
+ }
1322
+
295
1323
  async function main() {
296
1324
  const args = parseArgs(process.argv.slice(2));
297
1325
  const storedConfig = readConfig();
298
1326
  if (storedConfig.error) {
299
- console.error(`[leak] warning: ${storedConfig.error}`);
1327
+ logWarn(storedConfig.error);
300
1328
  }
301
1329
  const configDefaults = storedConfig.config.defaults || {};
302
1330
 
1331
+ if (args.wizard) {
1332
+ try {
1333
+ await runPublishWizard({ args, configDefaults });
1334
+ } catch (err) {
1335
+ logError(err.message || String(err));
1336
+ process.exit(1);
1337
+ }
1338
+ }
1339
+
303
1340
  const fileArg = args.file;
304
1341
  if (!fileArg) {
305
1342
  const positionalPath = args._?.[0];
@@ -316,18 +1353,43 @@ async function main() {
316
1353
  try {
317
1354
  artifactPath = resolveAndValidateArtifactPath(fileArg, args);
318
1355
  } catch (err) {
319
- console.error(err.message || String(err));
1356
+ logError(err.message || String(err));
1357
+ process.exit(1);
1358
+ }
1359
+
1360
+ const accessModeInput = String(
1361
+ args["access-mode"] || process.env.ACCESS_MODE || configDefaults.accessMode || DEFAULT_ACCESS_MODE,
1362
+ ).trim().toLowerCase();
1363
+ if (!isValidAccessMode(accessModeInput)) {
1364
+ logError(`Invalid --access-mode value: ${accessModeInput}`);
1365
+ logError(`Supported access modes: ${ACCESS_MODE_VALUES.join(", ")}`);
1366
+ process.exit(1);
1367
+ }
1368
+ const accessMode = accessModeInput;
1369
+ const requiresPayment = accessModeRequiresPayment(accessMode);
1370
+ const requiresDownloadCode = accessModeRequiresDownloadCode(accessMode);
1371
+
1372
+ let downloadCodeHash;
1373
+ try {
1374
+ downloadCodeHash = await resolveDownloadCodeHash({
1375
+ args,
1376
+ configDefaults,
1377
+ accessMode,
1378
+ persistedHashOverride: args["download-code-hash"],
1379
+ });
1380
+ } catch (err) {
1381
+ logError(err.message || String(err));
320
1382
  process.exit(1);
321
1383
  }
322
1384
 
323
1385
  const payTo = String(args["pay-to"] || process.env.SELLER_PAY_TO || configDefaults.sellerPayTo || "").trim();
324
- if (!payTo) {
325
- console.error("Missing --pay-to, SELLER_PAY_TO in env, or sellerPayTo in ~/.leak/config.json");
1386
+ if (requiresPayment && !payTo) {
1387
+ logError("Missing --pay-to, SELLER_PAY_TO in env, or sellerPayTo in ~/.leak/config.json");
326
1388
  process.exit(1);
327
1389
  }
328
- if (!isAddress(payTo)) {
329
- console.error(`Invalid seller payout address: ${payTo}`);
330
- console.error("Expected a valid Ethereum address (0x + 40 hex chars).");
1390
+ if (requiresPayment && payTo && !isAddress(payTo)) {
1391
+ logError(`Invalid seller payout address: ${payTo}`);
1392
+ logError("Expected a valid Ethereum address (0x + 40 hex chars).");
331
1393
  process.exit(1);
332
1394
  }
333
1395
 
@@ -339,24 +1401,56 @@ async function main() {
339
1401
  network = networkMeta.caip2;
340
1402
  networkName = networkMeta.name;
341
1403
  } catch (err) {
342
- console.error(err.message || String(err));
1404
+ logError(err.message || String(err));
343
1405
  process.exit(1);
344
1406
  }
345
1407
  const port = Number(args.port || process.env.PORT || configDefaults.port || 4021);
346
- const facilitatorMode = (
347
- process.env.FACILITATOR_MODE || configDefaults.facilitatorMode || "testnet"
348
- ).trim();
1408
+ if (!Number.isFinite(port) || !Number.isInteger(port) || port <= 0) {
1409
+ logError("Invalid --port (must be a positive integer)");
1410
+ process.exit(1);
1411
+ }
1412
+ const facilitatorMode = trim(
1413
+ args["facilitator-mode"] ||
1414
+ process.env.FACILITATOR_MODE ||
1415
+ configDefaults.facilitatorMode ||
1416
+ "testnet",
1417
+ ).toLowerCase();
1418
+ if (requiresPayment && !ALLOWED_FACILITATOR_MODES.has(facilitatorMode)) {
1419
+ logError("Invalid FACILITATOR_MODE. Use: testnet or cdp_mainnet");
1420
+ process.exit(1);
1421
+ }
1422
+ const effectiveFacilitatorMode = ALLOWED_FACILITATOR_MODES.has(facilitatorMode)
1423
+ ? facilitatorMode
1424
+ : "testnet";
349
1425
  const facilitatorUrl = (
350
- process.env.FACILITATOR_URL
351
- || configDefaults.facilitatorUrl
352
- || defaultFacilitatorUrlForMode(facilitatorMode)
1426
+ args["facilitator-url"] ||
1427
+ process.env.FACILITATOR_URL ||
1428
+ configDefaults.facilitatorUrl ||
1429
+ defaultFacilitatorUrlForMode(effectiveFacilitatorMode)
353
1430
  ).trim();
354
- const cdpApiKeyId = process.env.CDP_API_KEY_ID || configDefaults.cdpApiKeyId || "";
355
- const cdpApiKeySecret = process.env.CDP_API_KEY_SECRET || configDefaults.cdpApiKeySecret || "";
1431
+ const cdpApiKeyId = trim(
1432
+ args["cdp-api-key-id"] || process.env.CDP_API_KEY_ID || configDefaults.cdpApiKeyId || "",
1433
+ );
1434
+ const cdpApiKeySecret = trim(
1435
+ args["cdp-api-key-secret"] ||
1436
+ process.env.CDP_API_KEY_SECRET ||
1437
+ configDefaults.cdpApiKeySecret ||
1438
+ "",
1439
+ );
356
1440
 
357
- const confirmationPolicy = args.confirmed
358
- ? "confirmed"
359
- : (process.env.CONFIRMATION_POLICY || configDefaults.confirmationPolicy || "confirmed");
1441
+ const confirmationPolicyInput = trim(
1442
+ args["confirmation-policy"] ||
1443
+ (args.confirmed
1444
+ ? "confirmed"
1445
+ : process.env.CONFIRMATION_POLICY || configDefaults.confirmationPolicy || "confirmed"),
1446
+ ).toLowerCase();
1447
+ if (requiresPayment && !ALLOWED_CONFIRMATION_POLICIES.has(confirmationPolicyInput)) {
1448
+ logError("Invalid confirmation policy. Use: confirmed or optimistic");
1449
+ process.exit(1);
1450
+ }
1451
+ const confirmationPolicy = ALLOWED_CONFIRMATION_POLICIES.has(confirmationPolicyInput)
1452
+ ? confirmationPolicyInput
1453
+ : "confirmed";
360
1454
  const ogTitle = typeof args["og-title"] === "string"
361
1455
  ? args["og-title"]
362
1456
  : (process.env.OG_TITLE || configDefaults.ogTitle);
@@ -370,14 +1464,20 @@ async function main() {
370
1464
  const defaultEndedWindowSeconds = args.public ? 86400 : 0;
371
1465
  const endedWindowSeconds = parseNonNegativeInt(endedWindowArg);
372
1466
 
373
- const price = args.price || process.env.PRICE_USD || configDefaults.priceUsd; // we keep env name for compatibility
1467
+ const price = requiresPayment
1468
+ ? (args.price || process.env.PRICE_USD || configDefaults.priceUsd)
1469
+ : "0";
374
1470
  const windowRaw = args.window || process.env.WINDOW_SECONDS || configDefaults.window;
375
1471
  const windowSeconds = typeof windowRaw === "string" ? parseDurationToSeconds(windowRaw) : Number(windowRaw);
376
1472
 
377
- const prompted = await promptMissing({ price, windowSeconds: windowSeconds || null });
1473
+ const prompted = await promptMissing({
1474
+ price,
1475
+ windowSeconds: windowSeconds || null,
1476
+ requiresPayment,
1477
+ });
378
1478
 
379
1479
  if (endedWindowArg !== undefined && endedWindowSeconds === null) {
380
- console.error("Invalid --ended-window-seconds (must be a non-negative integer)");
1480
+ logError("Invalid --ended-window-seconds (must be a non-negative integer)");
381
1481
  process.exit(1);
382
1482
  }
383
1483
 
@@ -385,30 +1485,32 @@ async function main() {
385
1485
  try {
386
1486
  ogImageResolved = resolveOgImageInput(ogImageInput);
387
1487
  } catch (err) {
388
- console.error(err.message || String(err));
1488
+ logError(err.message || String(err));
389
1489
  process.exit(1);
390
1490
  }
391
1491
 
392
- const saleStartTs = Math.floor(Date.now() / 1000);
393
- const saleEndTs = saleStartTs + prompted.windowSeconds;
1492
+ const saleStartTsFixed = nowSeconds();
1493
+ const saleEndTsFixed = saleStartTsFixed + prompted.windowSeconds;
394
1494
  const effectiveEndedWindowSeconds = endedWindowSeconds ?? defaultEndedWindowSeconds;
395
- const stopAfterSeconds = prompted.windowSeconds + effectiveEndedWindowSeconds;
1495
+ const hardStopTsFixed = saleEndTsFixed + effectiveEndedWindowSeconds;
396
1496
 
397
1497
  try {
398
1498
  await ensurePublicExposureConfirmed(args);
399
1499
  } catch (err) {
400
- console.error(err.message || String(err));
1500
+ logError(err.message || String(err));
401
1501
  process.exit(1);
402
1502
  }
403
1503
 
404
1504
  // Spawn the server with explicit env so there's no confusion.
405
- const env = {
1505
+ const envBase = {
406
1506
  ...process.env,
407
1507
  PORT: String(port),
408
1508
  SELLER_PAY_TO: payTo,
409
1509
  PRICE_USD: String(prompted.price),
1510
+ ACCESS_MODE: accessMode,
1511
+ DOWNLOAD_CODE_HASH: downloadCodeHash,
410
1512
  CHAIN_ID: String(network),
411
- FACILITATOR_MODE: facilitatorMode,
1513
+ FACILITATOR_MODE: effectiveFacilitatorMode,
412
1514
  FACILITATOR_URL: facilitatorUrl,
413
1515
  CDP_API_KEY_ID: cdpApiKeyId,
414
1516
  CDP_API_KEY_SECRET: cdpApiKeySecret,
@@ -419,140 +1521,105 @@ async function main() {
419
1521
  OG_DESCRIPTION: ogDescription || "",
420
1522
  OG_IMAGE_URL: ogImageResolved.ogImageUrl || "",
421
1523
  OG_IMAGE_PATH: ogImageResolved.ogImagePath || "",
422
- SALE_START_TS: String(saleStartTs),
423
- SALE_END_TS: String(saleEndTs),
424
- ENDED_WINDOW_SECONDS: String(effectiveEndedWindowSeconds),
425
1524
  PUBLIC_BASE_URL: process.env.PUBLIC_BASE_URL || "",
426
1525
  };
427
1526
 
428
- console.log("\nLeak config:");
429
- console.log(`- file: ${artifactPath}`);
430
- console.log(`- price: ${prompted.price} USDC`);
431
- console.log(`- window: ${prompted.windowSeconds}s`);
432
- console.log(`- to: ${payTo}`);
433
- console.log(`- net: ${network} (${networkName})`);
434
- console.log(`- mode: ${confirmationPolicy}`);
435
- console.log(`- facilitator_mode: ${facilitatorMode}`);
436
- console.log(`- facilitator_url: ${facilitatorUrl}`);
437
- if (ogTitle) console.log(`- og_title: ${ogTitle}`);
438
- if (ogDescription) console.log(`- og_description: ${ogDescription}`);
439
- if (ogImageResolved.ogImageUrl) console.log(`- og_image_url: ${ogImageResolved.ogImageUrl}`);
440
- if (ogImageResolved.ogImagePath) console.log(`- og_image_path: ${ogImageResolved.ogImagePath}`);
441
- console.log(`- ended_window: ${effectiveEndedWindowSeconds}s`);
1527
+ console.log("");
1528
+ console.log(outUi.section("Leak Config"));
1529
+ const runtimeRows = [
1530
+ { key: "file", value: artifactPath },
1531
+ { key: "price", value: `${prompted.price} USDC` },
1532
+ { key: "window", value: `${prompted.windowSeconds}s` },
1533
+ { key: "access_mode", value: accessMode },
1534
+ { key: "download_code", value: requiresDownloadCode ? "required" : "not required" },
1535
+ requiresPayment
1536
+ ? { key: "to", value: payTo }
1537
+ : (payTo ? { key: "to", value: `${payTo} (ignored: payment disabled by access mode)` } : null),
1538
+ { key: "net", value: `${network} (${networkName})` },
1539
+ {
1540
+ key: "settlement",
1541
+ value: requiresPayment ? confirmationPolicy : "n/a (payment disabled)",
1542
+ },
1543
+ { key: "facilitator_mode", value: effectiveFacilitatorMode },
1544
+ { key: "facilitator_url", value: facilitatorUrl },
1545
+ ogTitle ? { key: "og_title", value: ogTitle } : null,
1546
+ ogDescription ? { key: "og_description", value: ogDescription } : null,
1547
+ ogImageResolved.ogImageUrl ? { key: "og_image_url", value: ogImageResolved.ogImageUrl } : null,
1548
+ ogImageResolved.ogImagePath ? { key: "og_image_path", value: ogImageResolved.ogImagePath } : null,
1549
+ { key: "ended_window", value: `${effectiveEndedWindowSeconds}s` },
1550
+ ];
1551
+ for (const line of outUi.formatRows(runtimeRows)) {
1552
+ console.log(line);
1553
+ }
442
1554
 
443
1555
  if (args.public) {
444
1556
  const preflight = cloudflaredPreflight();
445
1557
  if (!preflight.ok) {
446
- const localOnlyCmd = `leak --file ${JSON.stringify(artifactPath)} --price ${prompted.price} --window ${prompted.windowSeconds}s --pay-to ${payTo} --network ${network}${confirmationPolicy === "confirmed" ? " --confirmed" : ""}${Number.isFinite(port) && port !== 4021 ? ` --port ${port}` : ""}${effectiveEndedWindowSeconds > 0 ? ` --ended-window-seconds ${effectiveEndedWindowSeconds}` : ""}`;
1558
+ const localOnlyCmd = `leak --file ${JSON.stringify(artifactPath)} --access-mode ${accessMode} --price ${prompted.price} --window ${prompted.windowSeconds}s${requiresPayment ? ` --pay-to ${payTo}` : ""} --network ${network}${requiresPayment && confirmationPolicy === "confirmed" ? " --confirmed" : ""}${Number.isFinite(port) && port !== 4021 ? ` --port ${port}` : ""}${effectiveEndedWindowSeconds > 0 ? ` --ended-window-seconds ${effectiveEndedWindowSeconds}` : ""}`;
447
1559
  printCloudflaredInstallHelp(localOnlyCmd);
1560
+ if (requiresDownloadCode) {
1561
+ logWarn("Local mode still requires download-code input or DOWNLOAD_CODE_HASH.");
1562
+ }
448
1563
  if (!preflight.missing) {
449
- console.error(`[leak] detail: ${preflight.reason}`);
1564
+ logWarn(`detail: ${preflight.reason}`);
450
1565
  }
1566
+ const runId = randomUUID();
1567
+ const runStatePaths = createRunStatePaths(runId);
1568
+ let runState = {
1569
+ runId,
1570
+ createdAtTs: nowSeconds(),
1571
+ updatedAtTs: nowSeconds(),
1572
+ saleStartTs: saleStartTsFixed,
1573
+ saleEndTs: saleEndTsFixed,
1574
+ hardStopTs: hardStopTsFixed,
1575
+ endedWindowSeconds: effectiveEndedWindowSeconds,
1576
+ restartCount: 0,
1577
+ latestPublicUrl: null,
1578
+ latestPromoUrl: null,
1579
+ latestBuyUrl: null,
1580
+ status: "failed",
1581
+ lastExitReason: "config_fatal",
1582
+ };
1583
+ runState = persistRunState(runStatePaths, runState);
451
1584
  process.exit(1);
452
1585
  }
453
1586
  }
454
1587
 
455
- const child = spawn(process.execPath, [SERVER_ENTRY], {
456
- env,
457
- stdio: "inherit",
458
- });
459
-
460
- let stoppedByWindow = false;
461
- let tunnelFatal = false;
462
-
463
- child.on("error", (err) => {
464
- console.error(`[leak] failed to start server process: ${err.message}`);
465
- process.exit(1);
466
- });
467
-
468
- let tunnelProc = null;
469
- if (args.public) {
470
- // Cloudflare "quick tunnel" (temporary URL)
471
- // Requires `cloudflared` installed.
472
- console.log("\n[leak] starting Cloudflare quick tunnel...");
473
-
474
- tunnelProc = spawn(
475
- "cloudflared",
476
- ["tunnel", "--url", `http://localhost:${port}`, "--no-autoupdate"],
477
- {
478
- stdio: ["ignore", "pipe", "pipe"],
479
- },
480
- );
481
-
482
- tunnelProc.on("error", (err) => {
483
- tunnelFatal = true;
484
- if (err.code === "ENOENT") {
485
- console.error("[leak] cloudflared not found. Install it or re-run without --public.");
486
- } else {
487
- console.error(`[leak] failed to start tunnel: ${err.message}`);
488
- }
489
- try {
490
- child.kill("SIGTERM");
491
- } catch {}
492
- });
493
-
494
- const urlRegex = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/gi;
495
- const onData = (chunk) => {
496
- const s = chunk.toString("utf8");
497
- const m = s.match(urlRegex);
498
- if (m && m[0]) {
499
- const promoUrl = `${m[0]}/`;
500
- const buyUrl = `${m[0]}/download`;
501
- console.log(`\n[leak] public URL: ${m[0]}`);
502
- console.log(`[leak] promo link: ${promoUrl}`);
503
- console.log(`[leak] buy link: ${buyUrl}`);
504
- // only print once
505
- tunnelProc?.stdout?.off("data", onData);
506
- tunnelProc?.stderr?.off("data", onData);
507
- }
508
- };
509
-
510
- tunnelProc.stdout.on("data", onData);
511
- tunnelProc.stderr.on("data", onData);
512
-
513
- tunnelProc.on("exit", (code, signal) => {
514
- if (signal) console.log(`[leak] tunnel exited (signal ${signal})`);
515
- else console.log(`[leak] tunnel exited (code ${code})`);
516
- });
517
- }
518
-
519
- const stopAll = () => {
520
- stoppedByWindow = true;
521
- if (effectiveEndedWindowSeconds > 0) {
522
- console.log(
523
- `\n[leak] ended-window elapsed (${effectiveEndedWindowSeconds}s after sale end). stopping...`,
524
- );
525
- } else {
526
- console.log(`\n[leak] window expired (${prompted.windowSeconds}s). stopping...`);
527
- }
528
- try {
529
- child.kill("SIGTERM");
530
- } catch {}
531
- try {
532
- tunnelProc?.kill("SIGTERM");
533
- } catch {}
1588
+ const runId = randomUUID();
1589
+ const runStatePaths = createRunStatePaths(runId);
1590
+ let runState = {
1591
+ runId,
1592
+ createdAtTs: nowSeconds(),
1593
+ updatedAtTs: nowSeconds(),
1594
+ saleStartTs: saleStartTsFixed,
1595
+ saleEndTs: saleEndTsFixed,
1596
+ hardStopTs: hardStopTsFixed,
1597
+ endedWindowSeconds: effectiveEndedWindowSeconds,
1598
+ restartCount: 0,
1599
+ latestPublicUrl: null,
1600
+ latestPromoUrl: null,
1601
+ latestBuyUrl: null,
1602
+ status: "running",
1603
+ lastExitReason: "",
534
1604
  };
535
-
536
- const stopTimer = setTimeout(stopAll, stopAfterSeconds * 1000);
537
-
538
- child.on("exit", (code, signal) => {
539
- clearTimeout(stopTimer);
540
- try {
541
- tunnelProc?.kill("SIGTERM");
542
- } catch {}
543
- if (tunnelFatal) process.exit(1);
544
- if (stoppedByWindow && signal === "SIGTERM") process.exit(0);
545
- if (signal) {
546
- console.log(`[leak] server exited (signal ${signal})`);
547
- process.exit(1);
548
- } else {
549
- console.log(`[leak] server exited (code ${code})`);
550
- process.exit(code ?? 1);
551
- }
1605
+ runState = persistRunState(runStatePaths, runState);
1606
+
1607
+ const exitCode = await supervisorMain({
1608
+ args,
1609
+ port,
1610
+ envBase,
1611
+ saleStartTsFixed,
1612
+ saleEndTsFixed,
1613
+ hardStopTsFixed,
1614
+ effectiveEndedWindowSeconds,
1615
+ runStatePaths,
1616
+ runState,
552
1617
  });
1618
+ process.exit(exitCode);
553
1619
  }
554
1620
 
555
1621
  main().catch((e) => {
556
- console.error(e);
1622
+ const detail = e?.stack || e?.message || String(e);
1623
+ logError(detail);
557
1624
  process.exit(1);
558
1625
  });