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
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export const CONFIG_VERSION = 1;
|
|
6
|
+
export const DEFAULT_TESTNET_FACILITATOR_URL = "https://x402.org/facilitator";
|
|
7
|
+
export const DEFAULT_CDP_MAINNET_FACILITATOR_URL = "https://api.cdp.coinbase.com/platform/v2/x402";
|
|
8
|
+
|
|
9
|
+
const CONFIG_DIRNAME = ".leak";
|
|
10
|
+
const CONFIG_FILENAME = "config.json";
|
|
11
|
+
const ALLOWED_FACILITATOR_MODES = new Set(["testnet", "cdp_mainnet"]);
|
|
12
|
+
const ALLOWED_CONFIRMATION_POLICIES = new Set(["confirmed", "optimistic"]);
|
|
13
|
+
|
|
14
|
+
function trimString(value) {
|
|
15
|
+
if (value === undefined || value === null) return "";
|
|
16
|
+
return String(value).trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parsePositiveInt(value) {
|
|
20
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
21
|
+
const n = Number(value);
|
|
22
|
+
if (!Number.isFinite(n) || n <= 0) return undefined;
|
|
23
|
+
return Math.floor(n);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseNonNegativeInt(value) {
|
|
27
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
28
|
+
const n = Number(value);
|
|
29
|
+
if (!Number.isFinite(n) || n < 0) return undefined;
|
|
30
|
+
return Math.floor(n);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function bestEffortChmod(targetPath, mode) {
|
|
34
|
+
try {
|
|
35
|
+
fs.chmodSync(targetPath, mode);
|
|
36
|
+
} catch {
|
|
37
|
+
// best effort only
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function userHomeDir() {
|
|
42
|
+
return process.env.HOME || os.homedir();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getConfigPath() {
|
|
46
|
+
return path.join(userHomeDir(), CONFIG_DIRNAME, CONFIG_FILENAME);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function defaultFacilitatorUrlForMode(mode) {
|
|
50
|
+
return mode === "cdp_mainnet"
|
|
51
|
+
? DEFAULT_CDP_MAINNET_FACILITATOR_URL
|
|
52
|
+
: DEFAULT_TESTNET_FACILITATOR_URL;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeDefaults(rawDefaults) {
|
|
56
|
+
const defaults = {};
|
|
57
|
+
if (!rawDefaults || typeof rawDefaults !== "object") return defaults;
|
|
58
|
+
|
|
59
|
+
const sellerPayTo = trimString(rawDefaults.sellerPayTo);
|
|
60
|
+
if (sellerPayTo) defaults.sellerPayTo = sellerPayTo;
|
|
61
|
+
|
|
62
|
+
const chainId = trimString(rawDefaults.chainId);
|
|
63
|
+
if (chainId) defaults.chainId = chainId;
|
|
64
|
+
|
|
65
|
+
const facilitatorMode = trimString(rawDefaults.facilitatorMode);
|
|
66
|
+
if (ALLOWED_FACILITATOR_MODES.has(facilitatorMode)) {
|
|
67
|
+
defaults.facilitatorMode = facilitatorMode;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const facilitatorUrl = trimString(rawDefaults.facilitatorUrl);
|
|
71
|
+
if (facilitatorUrl) defaults.facilitatorUrl = facilitatorUrl;
|
|
72
|
+
|
|
73
|
+
const cdpApiKeyId = trimString(rawDefaults.cdpApiKeyId);
|
|
74
|
+
if (cdpApiKeyId) defaults.cdpApiKeyId = cdpApiKeyId;
|
|
75
|
+
|
|
76
|
+
const cdpApiKeySecret = trimString(rawDefaults.cdpApiKeySecret);
|
|
77
|
+
if (cdpApiKeySecret) defaults.cdpApiKeySecret = cdpApiKeySecret;
|
|
78
|
+
|
|
79
|
+
const confirmationPolicy = trimString(rawDefaults.confirmationPolicy);
|
|
80
|
+
if (ALLOWED_CONFIRMATION_POLICIES.has(confirmationPolicy)) {
|
|
81
|
+
defaults.confirmationPolicy = confirmationPolicy;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const priceUsd = trimString(rawDefaults.priceUsd);
|
|
85
|
+
if (priceUsd) defaults.priceUsd = priceUsd;
|
|
86
|
+
|
|
87
|
+
const window = trimString(rawDefaults.window);
|
|
88
|
+
if (window) defaults.window = window;
|
|
89
|
+
|
|
90
|
+
const port = parsePositiveInt(rawDefaults.port);
|
|
91
|
+
if (port !== undefined) defaults.port = port;
|
|
92
|
+
|
|
93
|
+
const endedWindowSeconds = parseNonNegativeInt(rawDefaults.endedWindowSeconds);
|
|
94
|
+
if (endedWindowSeconds !== undefined) defaults.endedWindowSeconds = endedWindowSeconds;
|
|
95
|
+
|
|
96
|
+
const ogTitle = trimString(rawDefaults.ogTitle);
|
|
97
|
+
if (ogTitle) defaults.ogTitle = ogTitle;
|
|
98
|
+
|
|
99
|
+
const ogDescription = trimString(rawDefaults.ogDescription);
|
|
100
|
+
if (ogDescription) defaults.ogDescription = ogDescription;
|
|
101
|
+
|
|
102
|
+
if (defaults.facilitatorMode && !defaults.facilitatorUrl) {
|
|
103
|
+
defaults.facilitatorUrl = defaultFacilitatorUrlForMode(defaults.facilitatorMode);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return defaults;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function normalizeConfig(rawConfig) {
|
|
110
|
+
const normalized = {
|
|
111
|
+
version: CONFIG_VERSION,
|
|
112
|
+
defaults: {},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (!rawConfig || typeof rawConfig !== "object") return normalized;
|
|
116
|
+
normalized.defaults = normalizeDefaults(rawConfig.defaults);
|
|
117
|
+
return normalized;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function readConfig() {
|
|
121
|
+
const configPath = getConfigPath();
|
|
122
|
+
if (!fs.existsSync(configPath)) {
|
|
123
|
+
return {
|
|
124
|
+
path: configPath,
|
|
125
|
+
exists: false,
|
|
126
|
+
config: normalizeConfig(null),
|
|
127
|
+
error: null,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let rawText;
|
|
132
|
+
try {
|
|
133
|
+
rawText = fs.readFileSync(configPath, "utf8");
|
|
134
|
+
} catch (err) {
|
|
135
|
+
return {
|
|
136
|
+
path: configPath,
|
|
137
|
+
exists: true,
|
|
138
|
+
config: normalizeConfig(null),
|
|
139
|
+
error: `unable to read config: ${err.message || String(err)}`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const parsed = JSON.parse(rawText);
|
|
145
|
+
return {
|
|
146
|
+
path: configPath,
|
|
147
|
+
exists: true,
|
|
148
|
+
config: normalizeConfig(parsed),
|
|
149
|
+
error: null,
|
|
150
|
+
};
|
|
151
|
+
} catch (err) {
|
|
152
|
+
return {
|
|
153
|
+
path: configPath,
|
|
154
|
+
exists: true,
|
|
155
|
+
config: normalizeConfig(null),
|
|
156
|
+
error: `unable to parse config JSON: ${err.message || String(err)}`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function writeConfig(nextConfig) {
|
|
162
|
+
const configPath = getConfigPath();
|
|
163
|
+
const configDir = path.dirname(configPath);
|
|
164
|
+
const normalized = normalizeConfig(nextConfig);
|
|
165
|
+
|
|
166
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
167
|
+
bestEffortChmod(configDir, 0o700);
|
|
168
|
+
|
|
169
|
+
fs.writeFileSync(configPath, `${JSON.stringify(normalized, null, 2)}\n`, { mode: 0o600 });
|
|
170
|
+
bestEffortChmod(configPath, 0o600);
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
path: configPath,
|
|
174
|
+
config: normalized,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function redactSecret(value) {
|
|
179
|
+
const raw = trimString(value);
|
|
180
|
+
if (!raw) return "";
|
|
181
|
+
if (raw.length <= 4) return "*".repeat(raw.length);
|
|
182
|
+
return `${"*".repeat(raw.length - 4)}${raw.slice(-4)}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function redactConfig(config) {
|
|
186
|
+
const normalized = normalizeConfig(config);
|
|
187
|
+
const copy = JSON.parse(JSON.stringify(normalized));
|
|
188
|
+
|
|
189
|
+
if (copy.defaults.cdpApiKeySecret) {
|
|
190
|
+
copy.defaults.cdpApiKeySecret = redactSecret(copy.defaults.cdpApiKeySecret);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (copy.defaults.cdpApiKeyId) {
|
|
194
|
+
copy.defaults.cdpApiKeyId = redactSecret(copy.defaults.cdpApiKeyId);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return copy;
|
|
198
|
+
}
|
package/scripts/leak.js
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "dotenv/config";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import readline from "node:readline/promises";
|
|
7
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
8
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
9
|
+
import { defaultFacilitatorUrlForMode, readConfig } from "./config_store.js";
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
const SERVER_ENTRY = path.resolve(__dirname, "..", "src", "index.js");
|
|
14
|
+
|
|
15
|
+
function usageAndExit(code = 1, hint = "") {
|
|
16
|
+
if (hint) console.error(`Hint: ${hint}\n`);
|
|
17
|
+
console.log(`Usage: 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>]`);
|
|
18
|
+
console.log(` leak 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>]`);
|
|
19
|
+
console.log(``);
|
|
20
|
+
console.log(`Notes:`);
|
|
21
|
+
console.log(` --public requires cloudflared (Cloudflare Tunnel) installed.`);
|
|
22
|
+
console.log(`Examples:`);
|
|
23
|
+
console.log(` leak --file ./vape.jpg`);
|
|
24
|
+
console.log(` leak --file ./vape.jpg --price 0.01 --window 1h --confirmed`);
|
|
25
|
+
console.log(` leak --file ./vape.jpg --public --og-title "My New Drop" --og-description "Agent-assisted purchase"`);
|
|
26
|
+
console.log(` leak --file ./vape.jpg --public --og-image-url ./cover.png`);
|
|
27
|
+
console.log(` npm run leak -- --file ./vape.jpg`);
|
|
28
|
+
console.log(` npm run leak -- --file ./vape.jpg --price 0.01 --window 1h --confirmed`);
|
|
29
|
+
process.exit(code);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseArgs(argv) {
|
|
33
|
+
const args = { _: [] };
|
|
34
|
+
for (let i = 0; i < argv.length; i++) {
|
|
35
|
+
const a = argv[i];
|
|
36
|
+
if (a === "--help" || a === "-h") usageAndExit(0);
|
|
37
|
+
if (a === "--confirmed") {
|
|
38
|
+
args.confirmed = true;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (a === "--public") {
|
|
42
|
+
args.public = true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (!a.startsWith("--")) continue;
|
|
46
|
+
const key = a.slice(2);
|
|
47
|
+
const val = argv[i + 1];
|
|
48
|
+
if (val && !val.startsWith("--")) {
|
|
49
|
+
args[key] = val;
|
|
50
|
+
i++;
|
|
51
|
+
} else {
|
|
52
|
+
args[key] = true;
|
|
53
|
+
}
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
for (const a of argv) {
|
|
57
|
+
if (!a.startsWith("--")) args._.push(a);
|
|
58
|
+
}
|
|
59
|
+
return args;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseNonNegativeInt(value) {
|
|
63
|
+
if (value === undefined || value === null || value === "") return null;
|
|
64
|
+
const n = Number(value);
|
|
65
|
+
if (!Number.isFinite(n) || n < 0) return null;
|
|
66
|
+
return Math.floor(n);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isAbsoluteHttpUrl(value) {
|
|
70
|
+
try {
|
|
71
|
+
const u = new URL(String(value));
|
|
72
|
+
return u.protocol === "http:" || u.protocol === "https:";
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const SUPPORTED_OG_IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg", ".avif"]);
|
|
79
|
+
|
|
80
|
+
function resolveOgImageInput(value) {
|
|
81
|
+
if (!value) return { ogImageUrl: "", ogImagePath: "" };
|
|
82
|
+
const raw = String(value).trim();
|
|
83
|
+
if (!raw) return { ogImageUrl: "", ogImagePath: "" };
|
|
84
|
+
|
|
85
|
+
if (isAbsoluteHttpUrl(raw)) {
|
|
86
|
+
return { ogImageUrl: raw, ogImagePath: "" };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const localPath = resolveFile(raw);
|
|
90
|
+
if (!fs.existsSync(localPath)) {
|
|
91
|
+
throw new Error("Invalid --og-image-url (must be an absolute http(s) URL or a valid local image file path)");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let stat;
|
|
95
|
+
try {
|
|
96
|
+
stat = fs.statSync(localPath);
|
|
97
|
+
} catch {
|
|
98
|
+
throw new Error("Invalid --og-image-url (must be an absolute http(s) URL or a valid local image file path)");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!stat.isFile()) {
|
|
102
|
+
throw new Error("Invalid --og-image-url (must be an absolute http(s) URL or a valid local image file path)");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const ext = path.extname(localPath).toLowerCase();
|
|
106
|
+
if (!SUPPORTED_OG_IMAGE_EXTENSIONS.has(ext)) {
|
|
107
|
+
throw new Error("Invalid --og-image-url (must be an absolute http(s) URL or a valid local image file path)");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { ogImageUrl: "", ogImagePath: localPath };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function cloudflaredPreflight() {
|
|
114
|
+
const probe = spawnSync("cloudflared", ["--version"], { stdio: "ignore" });
|
|
115
|
+
if (!probe.error && probe.status === 0) return { ok: true };
|
|
116
|
+
|
|
117
|
+
const missing = probe.error?.code === "ENOENT";
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
missing,
|
|
121
|
+
reason: missing
|
|
122
|
+
? "cloudflared is not installed or not on PATH."
|
|
123
|
+
: `cloudflared check failed (status=${probe.status ?? "n/a"}).`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function printCloudflaredInstallHelp(localOnlyCmd) {
|
|
128
|
+
console.error("[leak] --public requested, but cloudflared is unavailable.");
|
|
129
|
+
console.error("[leak] cloudflared is required to create a public tunnel URL.");
|
|
130
|
+
console.error("");
|
|
131
|
+
console.error("[leak] Install cloudflared:");
|
|
132
|
+
console.error(" macOS (Homebrew): brew install cloudflared");
|
|
133
|
+
console.error(" Windows (winget): winget install --id Cloudflare.cloudflared");
|
|
134
|
+
console.error(" Linux packages/docs: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/");
|
|
135
|
+
console.error("");
|
|
136
|
+
console.error("[leak] Retry public mode after install:");
|
|
137
|
+
console.error(" leak --file <path> --pay-to <address> --public");
|
|
138
|
+
console.error("");
|
|
139
|
+
console.error("[leak] Local-only alternative (no tunnel):");
|
|
140
|
+
console.error(` ${localOnlyCmd}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function parseDurationToSeconds(s) {
|
|
144
|
+
if (!s) return null;
|
|
145
|
+
const str = String(s).trim().toLowerCase();
|
|
146
|
+
|
|
147
|
+
// Allow: "1 hour", "60 minutes", etc.
|
|
148
|
+
const spaced = str.replace(/\s+/g, "");
|
|
149
|
+
|
|
150
|
+
// Raw seconds: "3600"
|
|
151
|
+
if (/^\d+$/.test(spaced)) return Number(spaced);
|
|
152
|
+
|
|
153
|
+
const m = spaced.match(
|
|
154
|
+
/^(\d+(?:\.\d+)?)(s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days)$/,
|
|
155
|
+
);
|
|
156
|
+
if (!m) return null;
|
|
157
|
+
|
|
158
|
+
const n = Number(m[1]);
|
|
159
|
+
const unit = m[2];
|
|
160
|
+
if (["s", "sec", "secs", "second", "seconds"].includes(unit)) return Math.round(n);
|
|
161
|
+
if (["m", "min", "mins", "minute", "minutes"].includes(unit)) return Math.round(n * 60);
|
|
162
|
+
if (["h", "hr", "hrs", "hour", "hours"].includes(unit)) return Math.round(n * 3600);
|
|
163
|
+
if (["d", "day", "days"].includes(unit)) return Math.round(n * 86400);
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function resolveFile(p) {
|
|
168
|
+
const abs = path.isAbsolute(p) ? p : path.resolve(process.cwd(), p);
|
|
169
|
+
return abs;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function promptMissing({ price, windowSeconds }) {
|
|
173
|
+
const rl = readline.createInterface({ input, output });
|
|
174
|
+
try {
|
|
175
|
+
let p = price;
|
|
176
|
+
if (!p) {
|
|
177
|
+
p = (await rl.question("How much (USDC)? e.g. 0.01 or $0.01: ")).trim();
|
|
178
|
+
}
|
|
179
|
+
p = String(p).trim();
|
|
180
|
+
if (p.startsWith("$")) p = p.slice(1).trim();
|
|
181
|
+
if (!p || Number.isNaN(Number(p))) throw new Error("Invalid price");
|
|
182
|
+
|
|
183
|
+
let w = windowSeconds;
|
|
184
|
+
if (!w) {
|
|
185
|
+
w = (await rl.question("How long? (e.g. 15m / 1h / 3600): ")).trim();
|
|
186
|
+
}
|
|
187
|
+
const secs = parseDurationToSeconds(w);
|
|
188
|
+
if (!secs || secs <= 0) throw new Error("Invalid duration");
|
|
189
|
+
|
|
190
|
+
return { price: String(p), windowSeconds: secs };
|
|
191
|
+
} finally {
|
|
192
|
+
rl.close();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function main() {
|
|
197
|
+
const args = parseArgs(process.argv.slice(2));
|
|
198
|
+
const storedConfig = readConfig();
|
|
199
|
+
if (storedConfig.error) {
|
|
200
|
+
console.error(`[leak] warning: ${storedConfig.error}`);
|
|
201
|
+
}
|
|
202
|
+
const configDefaults = storedConfig.config.defaults || {};
|
|
203
|
+
|
|
204
|
+
const fileArg = args.file;
|
|
205
|
+
if (!fileArg) {
|
|
206
|
+
const positionalPath = args._?.[0];
|
|
207
|
+
if (positionalPath) {
|
|
208
|
+
usageAndExit(
|
|
209
|
+
1,
|
|
210
|
+
`Expected '--file <path>', but got positional '${positionalPath}'. If using npm scripts, run: npm run leak -- --file ${positionalPath}`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
usageAndExit(1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const artifactPath = resolveFile(fileArg);
|
|
217
|
+
if (!fs.existsSync(artifactPath)) {
|
|
218
|
+
console.error(`File not found: ${artifactPath}`);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const payTo = args["pay-to"] || process.env.SELLER_PAY_TO || configDefaults.sellerPayTo;
|
|
223
|
+
if (!payTo) {
|
|
224
|
+
console.error("Missing --pay-to, SELLER_PAY_TO in env, or sellerPayTo in ~/.leak/config.json");
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const network = args.network || process.env.CHAIN_ID || configDefaults.chainId || "eip155:84532";
|
|
229
|
+
const port = Number(args.port || process.env.PORT || configDefaults.port || 4021);
|
|
230
|
+
const facilitatorMode = (
|
|
231
|
+
process.env.FACILITATOR_MODE || configDefaults.facilitatorMode || "testnet"
|
|
232
|
+
).trim();
|
|
233
|
+
const facilitatorUrl = (
|
|
234
|
+
process.env.FACILITATOR_URL
|
|
235
|
+
|| configDefaults.facilitatorUrl
|
|
236
|
+
|| defaultFacilitatorUrlForMode(facilitatorMode)
|
|
237
|
+
).trim();
|
|
238
|
+
const cdpApiKeyId = process.env.CDP_API_KEY_ID || configDefaults.cdpApiKeyId || "";
|
|
239
|
+
const cdpApiKeySecret = process.env.CDP_API_KEY_SECRET || configDefaults.cdpApiKeySecret || "";
|
|
240
|
+
|
|
241
|
+
const confirmationPolicy = args.confirmed
|
|
242
|
+
? "confirmed"
|
|
243
|
+
: (process.env.CONFIRMATION_POLICY || configDefaults.confirmationPolicy || "confirmed");
|
|
244
|
+
const ogTitle = typeof args["og-title"] === "string"
|
|
245
|
+
? args["og-title"]
|
|
246
|
+
: (process.env.OG_TITLE || configDefaults.ogTitle);
|
|
247
|
+
const ogDescription = typeof args["og-description"] === "string"
|
|
248
|
+
? args["og-description"]
|
|
249
|
+
: (process.env.OG_DESCRIPTION || configDefaults.ogDescription);
|
|
250
|
+
const ogImageInput = typeof args["og-image-url"] === "string"
|
|
251
|
+
? args["og-image-url"]
|
|
252
|
+
: process.env.OG_IMAGE_URL;
|
|
253
|
+
const endedWindowArg = args["ended-window-seconds"] ?? process.env.ENDED_WINDOW_SECONDS ?? configDefaults.endedWindowSeconds;
|
|
254
|
+
const defaultEndedWindowSeconds = args.public ? 86400 : 0;
|
|
255
|
+
const endedWindowSeconds = parseNonNegativeInt(endedWindowArg);
|
|
256
|
+
|
|
257
|
+
const price = args.price || process.env.PRICE_USD || configDefaults.priceUsd; // we keep env name for compatibility
|
|
258
|
+
const windowRaw = args.window || process.env.WINDOW_SECONDS || configDefaults.window;
|
|
259
|
+
const windowSeconds = typeof windowRaw === "string" ? parseDurationToSeconds(windowRaw) : Number(windowRaw);
|
|
260
|
+
|
|
261
|
+
const prompted = await promptMissing({ price, windowSeconds: windowSeconds || null });
|
|
262
|
+
|
|
263
|
+
if (endedWindowArg !== undefined && endedWindowSeconds === null) {
|
|
264
|
+
console.error("Invalid --ended-window-seconds (must be a non-negative integer)");
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
let ogImageResolved;
|
|
269
|
+
try {
|
|
270
|
+
ogImageResolved = resolveOgImageInput(ogImageInput);
|
|
271
|
+
} catch (err) {
|
|
272
|
+
console.error(err.message || String(err));
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const saleStartTs = Math.floor(Date.now() / 1000);
|
|
277
|
+
const saleEndTs = saleStartTs + prompted.windowSeconds;
|
|
278
|
+
const effectiveEndedWindowSeconds = endedWindowSeconds ?? defaultEndedWindowSeconds;
|
|
279
|
+
const stopAfterSeconds = prompted.windowSeconds + effectiveEndedWindowSeconds;
|
|
280
|
+
|
|
281
|
+
// Spawn the server with explicit env so there's no confusion.
|
|
282
|
+
const env = {
|
|
283
|
+
...process.env,
|
|
284
|
+
PORT: String(port),
|
|
285
|
+
SELLER_PAY_TO: payTo,
|
|
286
|
+
PRICE_USD: String(prompted.price),
|
|
287
|
+
CHAIN_ID: String(network),
|
|
288
|
+
FACILITATOR_MODE: facilitatorMode,
|
|
289
|
+
FACILITATOR_URL: facilitatorUrl,
|
|
290
|
+
CDP_API_KEY_ID: cdpApiKeyId,
|
|
291
|
+
CDP_API_KEY_SECRET: cdpApiKeySecret,
|
|
292
|
+
WINDOW_SECONDS: String(prompted.windowSeconds),
|
|
293
|
+
CONFIRMATION_POLICY: confirmationPolicy,
|
|
294
|
+
ARTIFACT_PATH: artifactPath,
|
|
295
|
+
OG_TITLE: ogTitle || "",
|
|
296
|
+
OG_DESCRIPTION: ogDescription || "",
|
|
297
|
+
OG_IMAGE_URL: ogImageResolved.ogImageUrl || "",
|
|
298
|
+
OG_IMAGE_PATH: ogImageResolved.ogImagePath || "",
|
|
299
|
+
SALE_START_TS: String(saleStartTs),
|
|
300
|
+
SALE_END_TS: String(saleEndTs),
|
|
301
|
+
ENDED_WINDOW_SECONDS: String(effectiveEndedWindowSeconds),
|
|
302
|
+
PUBLIC_BASE_URL: process.env.PUBLIC_BASE_URL || "",
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
console.log("\nLeak config:");
|
|
306
|
+
console.log(`- file: ${artifactPath}`);
|
|
307
|
+
console.log(`- price: ${prompted.price} USDC`);
|
|
308
|
+
console.log(`- window: ${prompted.windowSeconds}s`);
|
|
309
|
+
console.log(`- to: ${payTo}`);
|
|
310
|
+
console.log(`- net: ${network}`);
|
|
311
|
+
console.log(`- mode: ${confirmationPolicy}`);
|
|
312
|
+
console.log(`- facilitator_mode: ${facilitatorMode}`);
|
|
313
|
+
console.log(`- facilitator_url: ${facilitatorUrl}`);
|
|
314
|
+
if (ogTitle) console.log(`- og_title: ${ogTitle}`);
|
|
315
|
+
if (ogDescription) console.log(`- og_description: ${ogDescription}`);
|
|
316
|
+
if (ogImageResolved.ogImageUrl) console.log(`- og_image_url: ${ogImageResolved.ogImageUrl}`);
|
|
317
|
+
if (ogImageResolved.ogImagePath) console.log(`- og_image_path: ${ogImageResolved.ogImagePath}`);
|
|
318
|
+
console.log(`- ended_window: ${effectiveEndedWindowSeconds}s`);
|
|
319
|
+
|
|
320
|
+
if (args.public) {
|
|
321
|
+
const preflight = cloudflaredPreflight();
|
|
322
|
+
if (!preflight.ok) {
|
|
323
|
+
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}` : ""}`;
|
|
324
|
+
printCloudflaredInstallHelp(localOnlyCmd);
|
|
325
|
+
if (!preflight.missing) {
|
|
326
|
+
console.error(`[leak] detail: ${preflight.reason}`);
|
|
327
|
+
}
|
|
328
|
+
process.exit(1);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const child = spawn(process.execPath, [SERVER_ENTRY], {
|
|
333
|
+
env,
|
|
334
|
+
stdio: "inherit",
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
let stoppedByWindow = false;
|
|
338
|
+
let tunnelFatal = false;
|
|
339
|
+
|
|
340
|
+
child.on("error", (err) => {
|
|
341
|
+
console.error(`[leak] failed to start server process: ${err.message}`);
|
|
342
|
+
process.exit(1);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
let tunnelProc = null;
|
|
346
|
+
if (args.public) {
|
|
347
|
+
// Cloudflare "quick tunnel" (temporary URL)
|
|
348
|
+
// Requires `cloudflared` installed.
|
|
349
|
+
console.log("\n[leak] starting Cloudflare quick tunnel...");
|
|
350
|
+
|
|
351
|
+
tunnelProc = spawn(
|
|
352
|
+
"cloudflared",
|
|
353
|
+
["tunnel", "--url", `http://localhost:${port}`, "--no-autoupdate"],
|
|
354
|
+
{
|
|
355
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
356
|
+
},
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
tunnelProc.on("error", (err) => {
|
|
360
|
+
tunnelFatal = true;
|
|
361
|
+
if (err.code === "ENOENT") {
|
|
362
|
+
console.error("[leak] cloudflared not found. Install it or re-run without --public.");
|
|
363
|
+
} else {
|
|
364
|
+
console.error(`[leak] failed to start tunnel: ${err.message}`);
|
|
365
|
+
}
|
|
366
|
+
try {
|
|
367
|
+
child.kill("SIGTERM");
|
|
368
|
+
} catch {}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const urlRegex = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/gi;
|
|
372
|
+
const onData = (chunk) => {
|
|
373
|
+
const s = chunk.toString("utf8");
|
|
374
|
+
const m = s.match(urlRegex);
|
|
375
|
+
if (m && m[0]) {
|
|
376
|
+
const promoUrl = `${m[0]}/`;
|
|
377
|
+
const buyUrl = `${m[0]}/download`;
|
|
378
|
+
console.log(`\n[leak] public URL: ${m[0]}`);
|
|
379
|
+
console.log(`[leak] promo link: ${promoUrl}`);
|
|
380
|
+
console.log(`[leak] buy link: ${buyUrl}`);
|
|
381
|
+
// only print once
|
|
382
|
+
tunnelProc?.stdout?.off("data", onData);
|
|
383
|
+
tunnelProc?.stderr?.off("data", onData);
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
tunnelProc.stdout.on("data", onData);
|
|
388
|
+
tunnelProc.stderr.on("data", onData);
|
|
389
|
+
|
|
390
|
+
tunnelProc.on("exit", (code, signal) => {
|
|
391
|
+
if (signal) console.log(`[leak] tunnel exited (signal ${signal})`);
|
|
392
|
+
else console.log(`[leak] tunnel exited (code ${code})`);
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const stopAll = () => {
|
|
397
|
+
stoppedByWindow = true;
|
|
398
|
+
if (effectiveEndedWindowSeconds > 0) {
|
|
399
|
+
console.log(
|
|
400
|
+
`\n[leak] ended-window elapsed (${effectiveEndedWindowSeconds}s after sale end). stopping...`,
|
|
401
|
+
);
|
|
402
|
+
} else {
|
|
403
|
+
console.log(`\n[leak] window expired (${prompted.windowSeconds}s). stopping...`);
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
child.kill("SIGTERM");
|
|
407
|
+
} catch {}
|
|
408
|
+
try {
|
|
409
|
+
tunnelProc?.kill("SIGTERM");
|
|
410
|
+
} catch {}
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const stopTimer = setTimeout(stopAll, stopAfterSeconds * 1000);
|
|
414
|
+
|
|
415
|
+
child.on("exit", (code, signal) => {
|
|
416
|
+
clearTimeout(stopTimer);
|
|
417
|
+
try {
|
|
418
|
+
tunnelProc?.kill("SIGTERM");
|
|
419
|
+
} catch {}
|
|
420
|
+
if (tunnelFatal) process.exit(1);
|
|
421
|
+
if (stoppedByWindow && signal === "SIGTERM") process.exit(0);
|
|
422
|
+
if (signal) {
|
|
423
|
+
console.log(`[leak] server exited (signal ${signal})`);
|
|
424
|
+
process.exit(1);
|
|
425
|
+
} else {
|
|
426
|
+
console.log(`[leak] server exited (code ${code})`);
|
|
427
|
+
process.exit(code ?? 1);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
main().catch((e) => {
|
|
433
|
+
console.error(e);
|
|
434
|
+
process.exit(1);
|
|
435
|
+
});
|