leak-cli 2026.2.11
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 +23 -0
- package/LICENSE +15 -0
- package/README.md +492 -0
- package/package.json +54 -0
- package/scripts/buy.js +195 -0
- package/scripts/cli.js +55 -0
- package/scripts/config.js +322 -0
- package/scripts/config_store.js +198 -0
- package/scripts/leak.js +435 -0
- package/src/index.js +766 -0
package/scripts/buy.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "dotenv/config";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { x402Client } from "@x402/core/client";
|
|
7
|
+
import {
|
|
8
|
+
decodePaymentRequiredHeader,
|
|
9
|
+
decodePaymentResponseHeader,
|
|
10
|
+
encodePaymentSignatureHeader,
|
|
11
|
+
} from "@x402/core/http";
|
|
12
|
+
import { registerExactEvmScheme } from "@x402/evm/exact/client";
|
|
13
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
14
|
+
|
|
15
|
+
function usageAndExit(code = 1) {
|
|
16
|
+
console.log(
|
|
17
|
+
"Usage: leak buy <download_url> --buyer-private-key 0x... [--out <path> | --basename <name>]",
|
|
18
|
+
);
|
|
19
|
+
console.log("Examples:");
|
|
20
|
+
console.log(
|
|
21
|
+
" leak buy https://xxxx.trycloudflare.com/download --buyer-private-key 0x...",
|
|
22
|
+
);
|
|
23
|
+
console.log(
|
|
24
|
+
" leak buy https://xxxx.trycloudflare.com/download --buyer-private-key 0x... --basename myfile",
|
|
25
|
+
);
|
|
26
|
+
process.exit(code);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseArgs(argv) {
|
|
30
|
+
const args = { _: [] };
|
|
31
|
+
for (let i = 0; i < argv.length; i++) {
|
|
32
|
+
const a = argv[i];
|
|
33
|
+
if (a === "--help" || a === "-h") usageAndExit(0);
|
|
34
|
+
if (a.startsWith("--")) {
|
|
35
|
+
const key = a.slice(2);
|
|
36
|
+
const val = argv[i + 1];
|
|
37
|
+
if (val && !val.startsWith("--")) {
|
|
38
|
+
args[key] = val;
|
|
39
|
+
i++;
|
|
40
|
+
} else {
|
|
41
|
+
args[key] = true;
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
args._.push(a);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return args;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getHeaderCaseInsensitive(headers, name) {
|
|
51
|
+
const target = name.toLowerCase();
|
|
52
|
+
for (const [k, v] of headers.entries()) {
|
|
53
|
+
if (k.toLowerCase() === target) return v;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizePrivateKey(pk) {
|
|
59
|
+
const s = String(pk).trim();
|
|
60
|
+
if (s.startsWith("0x")) return s;
|
|
61
|
+
return `0x${s}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function filenameFromContentDisposition(cd) {
|
|
65
|
+
// Very small parser: attachment; filename="foo.ext"
|
|
66
|
+
if (!cd) return null;
|
|
67
|
+
const m = String(cd).match(/filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i);
|
|
68
|
+
const raw = m ? (m[1] || m[2]) : null;
|
|
69
|
+
if (!raw) return null;
|
|
70
|
+
try {
|
|
71
|
+
return decodeURIComponent(raw);
|
|
72
|
+
} catch {
|
|
73
|
+
return raw;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function sanitizeFilename(name) {
|
|
78
|
+
const base = path.basename(String(name || "").trim());
|
|
79
|
+
if (!base || base === "." || base === "..") return "downloaded.bin";
|
|
80
|
+
return base.replace(/[\u0000-\u001f\u007f]/g, "_");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function explorerTxUrl(network, transaction) {
|
|
84
|
+
if (!network || !transaction) return null;
|
|
85
|
+
if (network === "eip155:8453") return `https://basescan.org/tx/${transaction}`;
|
|
86
|
+
if (network === "eip155:84532") return `https://sepolia.basescan.org/tx/${transaction}`;
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function main() {
|
|
91
|
+
const args = parseArgs(process.argv.slice(2));
|
|
92
|
+
const downloadUrl = args._[0];
|
|
93
|
+
if (!downloadUrl) usageAndExit(1);
|
|
94
|
+
|
|
95
|
+
const buyerPk =
|
|
96
|
+
args["buyer-private-key"] || process.env.BUYER_PRIVATE_KEY || process.env.LEAK_BUYER_PRIVATE_KEY;
|
|
97
|
+
if (!buyerPk) {
|
|
98
|
+
console.error(
|
|
99
|
+
"Missing --buyer-private-key (or env BUYER_PRIVATE_KEY / LEAK_BUYER_PRIVATE_KEY)",
|
|
100
|
+
);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const account = privateKeyToAccount(normalizePrivateKey(buyerPk));
|
|
105
|
+
const client = new x402Client();
|
|
106
|
+
registerExactEvmScheme(client, { signer: account });
|
|
107
|
+
|
|
108
|
+
// 1) Request: expect 402 with PAYMENT-REQUIRED header.
|
|
109
|
+
const r1 = await fetch(downloadUrl, { method: "GET" });
|
|
110
|
+
if (r1.status !== 402) {
|
|
111
|
+
const text = await r1.text().catch(() => "");
|
|
112
|
+
throw new Error(`Expected 402, got ${r1.status}: ${text}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const paymentRequiredHeader = getHeaderCaseInsensitive(r1.headers, "PAYMENT-REQUIRED");
|
|
116
|
+
if (!paymentRequiredHeader) throw new Error("Missing PAYMENT-REQUIRED header");
|
|
117
|
+
|
|
118
|
+
const paymentRequired = decodePaymentRequiredHeader(paymentRequiredHeader);
|
|
119
|
+
const payload = await client.createPaymentPayload(paymentRequired);
|
|
120
|
+
const paymentHeader = encodePaymentSignatureHeader(payload);
|
|
121
|
+
|
|
122
|
+
// 2) Retry with payment signature, expect token JSON.
|
|
123
|
+
const r2 = await fetch(downloadUrl, {
|
|
124
|
+
method: "GET",
|
|
125
|
+
headers: {
|
|
126
|
+
"PAYMENT-SIGNATURE": paymentHeader,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (!r2.ok) {
|
|
131
|
+
const text = await r2.text().catch(() => "");
|
|
132
|
+
throw new Error(`Payment failed ${r2.status}: ${text}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const paymentResponseHeader =
|
|
136
|
+
getHeaderCaseInsensitive(r2.headers, "PAYMENT-RESPONSE")
|
|
137
|
+
|| getHeaderCaseInsensitive(r2.headers, "X-PAYMENT-RESPONSE");
|
|
138
|
+
if (paymentResponseHeader) {
|
|
139
|
+
try {
|
|
140
|
+
const receipt = decodePaymentResponseHeader(paymentResponseHeader);
|
|
141
|
+
const explorer = explorerTxUrl(receipt.network, receipt.transaction);
|
|
142
|
+
console.log("Payment receipt:");
|
|
143
|
+
console.log(`- network: ${receipt.network}`);
|
|
144
|
+
if (receipt.payer) console.log(`- payer: ${receipt.payer}`);
|
|
145
|
+
console.log(`- tx: ${receipt.transaction}`);
|
|
146
|
+
if (explorer) console.log(`- explorer: ${explorer}`);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
console.error(`[buy] warning: could not decode PAYMENT-RESPONSE header (${err.message || String(err)})`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const data = await r2.json();
|
|
153
|
+
const token = data?.token;
|
|
154
|
+
if (!token) throw new Error(`Missing token in response: ${JSON.stringify(data)}`);
|
|
155
|
+
|
|
156
|
+
// Determine download URL for the token step.
|
|
157
|
+
const base = new URL(downloadUrl);
|
|
158
|
+
const tokenUrl = new URL(base.toString());
|
|
159
|
+
tokenUrl.searchParams.set("token", token);
|
|
160
|
+
|
|
161
|
+
// 3) Download with token.
|
|
162
|
+
const r3 = await fetch(tokenUrl.toString(), { method: "GET" });
|
|
163
|
+
if (!r3.ok) {
|
|
164
|
+
const text = await r3.text().catch(() => "");
|
|
165
|
+
throw new Error(`Download failed ${r3.status}: ${text}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const serverFilename =
|
|
169
|
+
data?.filename ||
|
|
170
|
+
filenameFromContentDisposition(r3.headers.get("content-disposition")) ||
|
|
171
|
+
"downloaded.bin";
|
|
172
|
+
const safeServerFilename = sanitizeFilename(serverFilename);
|
|
173
|
+
|
|
174
|
+
let outPath;
|
|
175
|
+
if (args.out) {
|
|
176
|
+
outPath = String(args.out);
|
|
177
|
+
} else if (args.basename) {
|
|
178
|
+
const safeBase = sanitizeFilename(args.basename);
|
|
179
|
+
const ext = path.extname(safeServerFilename) || "";
|
|
180
|
+
outPath = `./${safeBase}${ext}`;
|
|
181
|
+
} else {
|
|
182
|
+
outPath = `./${safeServerFilename}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const buf = Buffer.from(await r3.arrayBuffer());
|
|
186
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
187
|
+
fs.writeFileSync(outPath, buf);
|
|
188
|
+
|
|
189
|
+
console.log(`Saved ${buf.length} bytes -> ${outPath}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
main().catch((e) => {
|
|
193
|
+
console.error(e);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
});
|
package/scripts/cli.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
|
|
9
|
+
const sub = process.argv[2];
|
|
10
|
+
|
|
11
|
+
function runSubcommand(scriptName, argv) {
|
|
12
|
+
const scriptPath = path.resolve(__dirname, scriptName);
|
|
13
|
+
const child = spawn(process.execPath, [scriptPath, ...argv], {
|
|
14
|
+
stdio: "inherit",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
child.on("error", (err) => {
|
|
18
|
+
console.error(`Failed to launch ${scriptName}: ${err.message}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
child.on("exit", (code, signal) => {
|
|
23
|
+
if (signal) {
|
|
24
|
+
console.error(`${scriptName} exited via signal ${signal}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
process.exit(code ?? 1);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!sub || sub === "--help" || sub === "-h") {
|
|
32
|
+
console.log("Usage:");
|
|
33
|
+
console.log(" leak --file <path> [--price <usdc>] [--window <duration>] [--pay-to <address>] [--network <caip2>] [--port <port>] [--confirmed] [--public] [--og-title <text>] [--og-description <text>] [--og-image-url <https://...|./image.png>] [--ended-window-seconds <seconds>]");
|
|
34
|
+
console.log(" leak buy <download_url> --buyer-private-key 0x... [--out <path> | --basename <name>]");
|
|
35
|
+
console.log(" leak config");
|
|
36
|
+
console.log(" leak config show");
|
|
37
|
+
console.log(" leak config --write-env");
|
|
38
|
+
console.log("");
|
|
39
|
+
console.log("Notes:");
|
|
40
|
+
console.log(" share / as promo (social card), use /download for agent-assisted purchase.");
|
|
41
|
+
console.log("Backward-compatible:");
|
|
42
|
+
console.log(" leak leak --file <path> ...");
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (sub === "leak") {
|
|
47
|
+
runSubcommand("leak.js", process.argv.slice(3));
|
|
48
|
+
} else if (sub === "buy") {
|
|
49
|
+
runSubcommand("buy.js", process.argv.slice(3));
|
|
50
|
+
} else if (sub === "config") {
|
|
51
|
+
runSubcommand("config.js", process.argv.slice(3));
|
|
52
|
+
} else {
|
|
53
|
+
// Default command: treat all args as leak-server args.
|
|
54
|
+
runSubcommand("leak.js", process.argv.slice(2));
|
|
55
|
+
}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "dotenv/config";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import readline from "node:readline/promises";
|
|
6
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
defaultFacilitatorUrlForMode,
|
|
10
|
+
redactConfig,
|
|
11
|
+
readConfig,
|
|
12
|
+
writeConfig,
|
|
13
|
+
} from "./config_store.js";
|
|
14
|
+
|
|
15
|
+
const ALLOWED_FACILITATOR_MODES = new Set(["testnet", "cdp_mainnet"]);
|
|
16
|
+
const ALLOWED_CONFIRMATION_POLICIES = new Set(["confirmed", "optimistic"]);
|
|
17
|
+
|
|
18
|
+
function usageAndExit(code = 0) {
|
|
19
|
+
console.log("Usage:");
|
|
20
|
+
console.log(" leak config");
|
|
21
|
+
console.log(" leak config show");
|
|
22
|
+
console.log(" leak config --write-env");
|
|
23
|
+
console.log("");
|
|
24
|
+
console.log("Notes:");
|
|
25
|
+
console.log(" - Stores defaults in ~/.leak/config.json");
|
|
26
|
+
console.log(" - `--write-env` writes a project .env scaffold in the current directory");
|
|
27
|
+
process.exit(code);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseArgs(argv) {
|
|
31
|
+
const args = { _: [] };
|
|
32
|
+
for (let i = 0; i < argv.length; i++) {
|
|
33
|
+
const token = argv[i];
|
|
34
|
+
if (token === "--help" || token === "-h") usageAndExit(0);
|
|
35
|
+
if (token.startsWith("--")) {
|
|
36
|
+
const key = token.slice(2);
|
|
37
|
+
const next = argv[i + 1];
|
|
38
|
+
if (next && !next.startsWith("--")) {
|
|
39
|
+
args[key] = next;
|
|
40
|
+
i++;
|
|
41
|
+
} else {
|
|
42
|
+
args[key] = true;
|
|
43
|
+
}
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
args._.push(token);
|
|
47
|
+
}
|
|
48
|
+
return args;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parsePositiveInt(value) {
|
|
52
|
+
const n = Number(value);
|
|
53
|
+
if (!Number.isFinite(n) || n <= 0) return null;
|
|
54
|
+
return Math.floor(n);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseNonNegativeInt(value) {
|
|
58
|
+
const n = Number(value);
|
|
59
|
+
if (!Number.isFinite(n) || n < 0) return null;
|
|
60
|
+
return Math.floor(n);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseDurationToSeconds(s) {
|
|
64
|
+
if (!s) return null;
|
|
65
|
+
const str = String(s).trim().toLowerCase();
|
|
66
|
+
const spaced = str.replace(/\s+/g, "");
|
|
67
|
+
if (/^\d+$/.test(spaced)) return Number(spaced);
|
|
68
|
+
const m = spaced.match(
|
|
69
|
+
/^(\d+(?:\.\d+)?)(s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days)$/,
|
|
70
|
+
);
|
|
71
|
+
if (!m) return null;
|
|
72
|
+
const n = Number(m[1]);
|
|
73
|
+
const unit = m[2];
|
|
74
|
+
if (["s", "sec", "secs", "second", "seconds"].includes(unit)) return Math.round(n);
|
|
75
|
+
if (["m", "min", "mins", "minute", "minutes"].includes(unit)) return Math.round(n * 60);
|
|
76
|
+
if (["h", "hr", "hrs", "hour", "hours"].includes(unit)) return Math.round(n * 3600);
|
|
77
|
+
if (["d", "day", "days"].includes(unit)) return Math.round(n * 86400);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function askWithDefault(rl, label, currentValue = "") {
|
|
82
|
+
const current = String(currentValue ?? "").trim();
|
|
83
|
+
const suffix = current ? ` [${current}]` : "";
|
|
84
|
+
const answer = (await rl.question(`${label}${suffix}: `)).trim();
|
|
85
|
+
return answer || current;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function printShow() {
|
|
89
|
+
const loaded = readConfig();
|
|
90
|
+
if (loaded.error) {
|
|
91
|
+
console.error(`[config] warning: ${loaded.error}`);
|
|
92
|
+
}
|
|
93
|
+
if (!loaded.exists) {
|
|
94
|
+
console.log(`No leak config found at ${loaded.path}`);
|
|
95
|
+
console.log("Run `leak config` to initialize your config file.");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const redacted = redactConfig(loaded.config);
|
|
100
|
+
console.log(`Leak config path: ${loaded.path}`);
|
|
101
|
+
console.log(JSON.stringify(redacted, null, 2));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function buildEnvScaffold(defaults) {
|
|
105
|
+
const facilitatorMode = defaults.facilitatorMode || "testnet";
|
|
106
|
+
const facilitatorUrl = defaults.facilitatorUrl || defaultFacilitatorUrlForMode(facilitatorMode);
|
|
107
|
+
const windowSeconds = parseDurationToSeconds(defaults.window || "");
|
|
108
|
+
|
|
109
|
+
const lines = [
|
|
110
|
+
"# Generated by `leak config --write-env`",
|
|
111
|
+
"",
|
|
112
|
+
"# Server",
|
|
113
|
+
`PORT=${defaults.port || 4021}`,
|
|
114
|
+
"",
|
|
115
|
+
"# x402",
|
|
116
|
+
`FACILITATOR_MODE=${facilitatorMode}`,
|
|
117
|
+
`FACILITATOR_URL=${facilitatorUrl}`,
|
|
118
|
+
`SELLER_PAY_TO=${defaults.sellerPayTo || ""}`,
|
|
119
|
+
`PRICE_USD=${defaults.priceUsd || "0.01"}`,
|
|
120
|
+
`CHAIN_ID=${defaults.chainId || "eip155:84532"}`,
|
|
121
|
+
`WINDOW_SECONDS=${windowSeconds ?? 3600}`,
|
|
122
|
+
"",
|
|
123
|
+
"# Required when FACILITATOR_MODE=cdp_mainnet (Base mainnet path)",
|
|
124
|
+
`CDP_API_KEY_ID=${defaults.cdpApiKeyId || ""}`,
|
|
125
|
+
`CDP_API_KEY_SECRET=${defaults.cdpApiKeySecret || ""}`,
|
|
126
|
+
"",
|
|
127
|
+
"# Settlement / confirmation policy",
|
|
128
|
+
`CONFIRMATION_POLICY=${defaults.confirmationPolicy || "confirmed"}`,
|
|
129
|
+
"CONFIRMATIONS_REQUIRED=1",
|
|
130
|
+
"",
|
|
131
|
+
"# Artifact to serve",
|
|
132
|
+
"ARTIFACT_PATH=./protected/asset.bin",
|
|
133
|
+
"PROTECTED_MIME=application/octet-stream",
|
|
134
|
+
"",
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
return `${lines.join("\n")}\n`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function maybeWriteEnvScaffold(defaults) {
|
|
141
|
+
const envPath = path.resolve(process.cwd(), ".env");
|
|
142
|
+
if (fs.existsSync(envPath)) {
|
|
143
|
+
console.log(`[config] ${envPath} already exists; skipping .env scaffold write.`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const body = buildEnvScaffold(defaults);
|
|
148
|
+
fs.writeFileSync(envPath, body);
|
|
149
|
+
console.log(`[config] wrote ${envPath}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function runWizard({ writeEnv }) {
|
|
153
|
+
const loaded = readConfig();
|
|
154
|
+
if (loaded.error) {
|
|
155
|
+
console.error(`[config] warning: ${loaded.error}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const existing = loaded.config.defaults || {};
|
|
159
|
+
const rl = readline.createInterface({ input, output });
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const sellerPayTo = await askWithDefault(
|
|
163
|
+
rl,
|
|
164
|
+
"SELLER_PAY_TO (seller payout address)",
|
|
165
|
+
existing.sellerPayTo || "",
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const chainId = await askWithDefault(
|
|
169
|
+
rl,
|
|
170
|
+
"CHAIN_ID",
|
|
171
|
+
existing.chainId || "eip155:84532",
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
let facilitatorMode = await askWithDefault(
|
|
175
|
+
rl,
|
|
176
|
+
"FACILITATOR_MODE (testnet|cdp_mainnet)",
|
|
177
|
+
existing.facilitatorMode || "testnet",
|
|
178
|
+
);
|
|
179
|
+
facilitatorMode = facilitatorMode.toLowerCase();
|
|
180
|
+
while (!ALLOWED_FACILITATOR_MODES.has(facilitatorMode)) {
|
|
181
|
+
console.error("Invalid FACILITATOR_MODE. Use: testnet or cdp_mainnet");
|
|
182
|
+
facilitatorMode = (await askWithDefault(rl, "FACILITATOR_MODE", "testnet")).toLowerCase();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const modeDefaultUrl = defaultFacilitatorUrlForMode(facilitatorMode);
|
|
186
|
+
const existingModeUrl =
|
|
187
|
+
existing.facilitatorMode === facilitatorMode
|
|
188
|
+
? existing.facilitatorUrl || ""
|
|
189
|
+
: "";
|
|
190
|
+
const facilitatorUrl = await askWithDefault(
|
|
191
|
+
rl,
|
|
192
|
+
"FACILITATOR_URL",
|
|
193
|
+
existingModeUrl || modeDefaultUrl,
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
let cdpApiKeyId = existing.cdpApiKeyId || "";
|
|
197
|
+
let cdpApiKeySecret = existing.cdpApiKeySecret || "";
|
|
198
|
+
if (facilitatorMode === "cdp_mainnet") {
|
|
199
|
+
cdpApiKeyId = await askWithDefault(
|
|
200
|
+
rl,
|
|
201
|
+
"CDP_API_KEY_ID",
|
|
202
|
+
existing.cdpApiKeyId || "",
|
|
203
|
+
);
|
|
204
|
+
while (!cdpApiKeyId) {
|
|
205
|
+
console.error("CDP_API_KEY_ID is required when FACILITATOR_MODE=cdp_mainnet");
|
|
206
|
+
cdpApiKeyId = await askWithDefault(rl, "CDP_API_KEY_ID", "");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
cdpApiKeySecret = await askWithDefault(
|
|
210
|
+
rl,
|
|
211
|
+
"CDP_API_KEY_SECRET",
|
|
212
|
+
existing.cdpApiKeySecret || "",
|
|
213
|
+
);
|
|
214
|
+
while (!cdpApiKeySecret) {
|
|
215
|
+
console.error("CDP_API_KEY_SECRET is required when FACILITATOR_MODE=cdp_mainnet");
|
|
216
|
+
cdpApiKeySecret = await askWithDefault(rl, "CDP_API_KEY_SECRET", "");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let confirmationPolicy = await askWithDefault(
|
|
221
|
+
rl,
|
|
222
|
+
"CONFIRMATION_POLICY (confirmed|optimistic)",
|
|
223
|
+
existing.confirmationPolicy || "confirmed",
|
|
224
|
+
);
|
|
225
|
+
confirmationPolicy = confirmationPolicy.toLowerCase();
|
|
226
|
+
while (!ALLOWED_CONFIRMATION_POLICIES.has(confirmationPolicy)) {
|
|
227
|
+
console.error("Invalid CONFIRMATION_POLICY. Use: confirmed or optimistic");
|
|
228
|
+
confirmationPolicy = (
|
|
229
|
+
await askWithDefault(rl, "CONFIRMATION_POLICY", "confirmed")
|
|
230
|
+
).toLowerCase();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const priceUsd = await askWithDefault(
|
|
234
|
+
rl,
|
|
235
|
+
"PRICE_USD",
|
|
236
|
+
existing.priceUsd || "0.01",
|
|
237
|
+
);
|
|
238
|
+
const window = await askWithDefault(
|
|
239
|
+
rl,
|
|
240
|
+
"WINDOW (e.g. 15m, 1h, 3600)",
|
|
241
|
+
existing.window || "15m",
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
let portRaw = await askWithDefault(
|
|
245
|
+
rl,
|
|
246
|
+
"PORT",
|
|
247
|
+
String(existing.port || 4021),
|
|
248
|
+
);
|
|
249
|
+
let port = parsePositiveInt(portRaw);
|
|
250
|
+
while (port === null) {
|
|
251
|
+
console.error("PORT must be a positive integer");
|
|
252
|
+
portRaw = await askWithDefault(rl, "PORT", "4021");
|
|
253
|
+
port = parsePositiveInt(portRaw);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let endedWindowRaw = await askWithDefault(
|
|
257
|
+
rl,
|
|
258
|
+
"ENDED_WINDOW_SECONDS",
|
|
259
|
+
String(existing.endedWindowSeconds ?? 0),
|
|
260
|
+
);
|
|
261
|
+
let endedWindowSeconds = parseNonNegativeInt(endedWindowRaw);
|
|
262
|
+
while (endedWindowSeconds === null) {
|
|
263
|
+
console.error("ENDED_WINDOW_SECONDS must be a non-negative integer");
|
|
264
|
+
endedWindowRaw = await askWithDefault(rl, "ENDED_WINDOW_SECONDS", "0");
|
|
265
|
+
endedWindowSeconds = parseNonNegativeInt(endedWindowRaw);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const ogTitle = await askWithDefault(rl, "OG_TITLE (optional)", existing.ogTitle || "");
|
|
269
|
+
const ogDescription = await askWithDefault(
|
|
270
|
+
rl,
|
|
271
|
+
"OG_DESCRIPTION (optional)",
|
|
272
|
+
existing.ogDescription || "",
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const defaults = {
|
|
276
|
+
sellerPayTo,
|
|
277
|
+
chainId,
|
|
278
|
+
facilitatorMode,
|
|
279
|
+
facilitatorUrl,
|
|
280
|
+
cdpApiKeyId,
|
|
281
|
+
cdpApiKeySecret,
|
|
282
|
+
confirmationPolicy,
|
|
283
|
+
priceUsd,
|
|
284
|
+
window,
|
|
285
|
+
port,
|
|
286
|
+
endedWindowSeconds,
|
|
287
|
+
ogTitle,
|
|
288
|
+
ogDescription,
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const written = writeConfig({ version: 1, defaults });
|
|
292
|
+
console.log(`[config] saved ${written.path}`);
|
|
293
|
+
console.log(JSON.stringify(redactConfig(written.config), null, 2));
|
|
294
|
+
|
|
295
|
+
if (writeEnv) {
|
|
296
|
+
maybeWriteEnvScaffold(written.config.defaults);
|
|
297
|
+
}
|
|
298
|
+
} finally {
|
|
299
|
+
rl.close();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function main() {
|
|
304
|
+
const args = parseArgs(process.argv.slice(2));
|
|
305
|
+
const sub = args._[0];
|
|
306
|
+
|
|
307
|
+
if (sub === "show") {
|
|
308
|
+
printShow();
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (sub && sub !== "show") {
|
|
313
|
+
usageAndExit(1);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
await runWizard({ writeEnv: Boolean(args["write-env"]) });
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
main().catch((err) => {
|
|
320
|
+
console.error(err);
|
|
321
|
+
process.exit(1);
|
|
322
|
+
});
|