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/.env.example +2 -0
- package/README.md +164 -17
- package/examples/multi-host.example.json +50 -0
- package/package.json +9 -4
- package/scripts/buy.js +224 -189
- package/scripts/cli.js +81 -14
- package/scripts/config.js +128 -28
- package/scripts/config_store.js +23 -0
- package/scripts/host.js +1131 -0
- package/scripts/leak.js +1240 -173
- package/scripts/ui.js +106 -0
- package/src/access_mode.js +51 -0
- package/src/download_code.js +91 -0
- package/src/index.js +271 -95
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 {
|
|
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)
|
|
21
|
-
console.log(
|
|
22
|
-
console.log(
|
|
23
|
-
console.log(
|
|
24
|
-
console.log(`
|
|
25
|
-
console.log(` --
|
|
26
|
-
console.log(`
|
|
27
|
-
console.log(
|
|
28
|
-
console.log(
|
|
29
|
-
console.log(
|
|
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(
|
|
32
|
-
console.log(
|
|
33
|
-
console.log(
|
|
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
|
-
|
|
134
|
-
|
|
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("
|
|
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("
|
|
784
|
+
console.error(errUi.section("Retry"));
|
|
142
785
|
console.error(" leak --file <path> --pay-to <address> --public");
|
|
143
786
|
console.error("");
|
|
144
|
-
console.error("
|
|
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
|
|
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
|
-
|
|
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 (
|
|
276
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
330
|
-
|
|
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
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
351
|
-
||
|
|
352
|
-
||
|
|
1426
|
+
args["facilitator-url"] ||
|
|
1427
|
+
process.env.FACILITATOR_URL ||
|
|
1428
|
+
configDefaults.facilitatorUrl ||
|
|
1429
|
+
defaultFacilitatorUrlForMode(effectiveFacilitatorMode)
|
|
353
1430
|
).trim();
|
|
354
|
-
const cdpApiKeyId =
|
|
355
|
-
|
|
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
|
|
358
|
-
|
|
359
|
-
|
|
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 =
|
|
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({
|
|
1473
|
+
const prompted = await promptMissing({
|
|
1474
|
+
price,
|
|
1475
|
+
windowSeconds: windowSeconds || null,
|
|
1476
|
+
requiresPayment,
|
|
1477
|
+
});
|
|
378
1478
|
|
|
379
1479
|
if (endedWindowArg !== undefined && endedWindowSeconds === null) {
|
|
380
|
-
|
|
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
|
-
|
|
1488
|
+
logError(err.message || String(err));
|
|
389
1489
|
process.exit(1);
|
|
390
1490
|
}
|
|
391
1491
|
|
|
392
|
-
const
|
|
393
|
-
const
|
|
1492
|
+
const saleStartTsFixed = nowSeconds();
|
|
1493
|
+
const saleEndTsFixed = saleStartTsFixed + prompted.windowSeconds;
|
|
394
1494
|
const effectiveEndedWindowSeconds = endedWindowSeconds ?? defaultEndedWindowSeconds;
|
|
395
|
-
const
|
|
1495
|
+
const hardStopTsFixed = saleEndTsFixed + effectiveEndedWindowSeconds;
|
|
396
1496
|
|
|
397
1497
|
try {
|
|
398
1498
|
await ensurePublicExposureConfirmed(args);
|
|
399
1499
|
} catch (err) {
|
|
400
|
-
|
|
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
|
|
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:
|
|
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("
|
|
429
|
-
console.log(
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
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
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
-
|
|
1622
|
+
const detail = e?.stack || e?.message || String(e);
|
|
1623
|
+
logError(detail);
|
|
557
1624
|
process.exit(1);
|
|
558
1625
|
});
|