leak-cli 2026.2.17-beta.0 → 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 +275 -100
package/scripts/buy.js
CHANGED
|
@@ -11,20 +11,23 @@ import {
|
|
|
11
11
|
} from "@x402/core/http";
|
|
12
12
|
import { registerExactEvmScheme } from "@x402/evm/exact/client";
|
|
13
13
|
import { privateKeyToAccount } from "viem/accounts";
|
|
14
|
+
import { DOWNLOAD_CODE_HEADER } from "../src/download_code.js";
|
|
15
|
+
import { createUi } from "./ui.js";
|
|
14
16
|
|
|
15
|
-
const SKILL_NAME = "leak";
|
|
17
|
+
const SKILL_NAME = "leak-buy";
|
|
18
|
+
const outUi = createUi(process.stdout);
|
|
19
|
+
const errUi = createUi(process.stderr);
|
|
16
20
|
|
|
17
21
|
function usageAndExit(code = 1) {
|
|
18
|
-
console.log(
|
|
19
|
-
|
|
20
|
-
);
|
|
21
|
-
console.log("
|
|
22
|
-
console.log(
|
|
23
|
-
|
|
24
|
-
);
|
|
25
|
-
console.log(
|
|
26
|
-
|
|
27
|
-
);
|
|
22
|
+
console.log(outUi.heading("Leak Buy CLI"));
|
|
23
|
+
console.log("");
|
|
24
|
+
console.log(outUi.section("Usage"));
|
|
25
|
+
console.log(" leak buy <promo_or_download_url> [--download-code <code> | --download-code-stdin] [--buyer-private-key-file <path> | --buyer-private-key-stdin] [--out <path> | --basename <name>]");
|
|
26
|
+
console.log("");
|
|
27
|
+
console.log(outUi.section("Examples"));
|
|
28
|
+
console.log(" leak buy https://xxxx.trycloudflare.com/ --buyer-private-key-file ./buyer.key");
|
|
29
|
+
console.log(" leak buy https://xxxx.trycloudflare.com/download --download-code friends-only --buyer-private-key-file ./buyer.key --basename myfile");
|
|
30
|
+
console.log(" printf '%s\\n' 'friends-only' | leak buy https://xxxx.trycloudflare.com/download --download-code-stdin --out ./downloads/file.bin");
|
|
28
31
|
process.exit(code);
|
|
29
32
|
}
|
|
30
33
|
|
|
@@ -83,20 +86,24 @@ function readPrivateKeyFromFile(filePath) {
|
|
|
83
86
|
return firstLine;
|
|
84
87
|
}
|
|
85
88
|
|
|
86
|
-
function
|
|
89
|
+
function readFirstLineFromStdin(kindLabel) {
|
|
87
90
|
let data = "";
|
|
88
91
|
try {
|
|
89
92
|
data = fs.readFileSync(0, "utf8");
|
|
90
93
|
} catch {
|
|
91
|
-
throw new Error(
|
|
94
|
+
throw new Error(`Failed to read ${kindLabel} from stdin`);
|
|
92
95
|
}
|
|
93
96
|
const firstLine = String(data).split(/\r?\n/, 1)[0]?.trim() || "";
|
|
94
97
|
if (!firstLine) {
|
|
95
|
-
throw new Error(
|
|
98
|
+
throw new Error(`No ${kindLabel} received on stdin`);
|
|
96
99
|
}
|
|
97
100
|
return firstLine;
|
|
98
101
|
}
|
|
99
102
|
|
|
103
|
+
function readPrivateKeyFromStdin() {
|
|
104
|
+
return readFirstLineFromStdin("buyer private key");
|
|
105
|
+
}
|
|
106
|
+
|
|
100
107
|
function resolveBuyerPrivateKey(args) {
|
|
101
108
|
if (typeof args["buyer-private-key"] !== "undefined") {
|
|
102
109
|
throw new Error(
|
|
@@ -121,8 +128,31 @@ function resolveBuyerPrivateKey(args) {
|
|
|
121
128
|
);
|
|
122
129
|
}
|
|
123
130
|
|
|
131
|
+
function resolveDownloadCode(args) {
|
|
132
|
+
const hasInlineCode = typeof args["download-code"] !== "undefined";
|
|
133
|
+
const hasStdinCode = Boolean(args["download-code-stdin"]);
|
|
134
|
+
|
|
135
|
+
if (hasInlineCode && hasStdinCode) {
|
|
136
|
+
throw new Error("Use exactly one download code input: --download-code or --download-code-stdin");
|
|
137
|
+
}
|
|
138
|
+
if (hasInlineCode && args["download-code"] === true) {
|
|
139
|
+
throw new Error("--download-code requires a value");
|
|
140
|
+
}
|
|
141
|
+
if (hasStdinCode && Boolean(args["buyer-private-key-stdin"])) {
|
|
142
|
+
throw new Error("--download-code-stdin cannot be combined with --buyer-private-key-stdin");
|
|
143
|
+
}
|
|
144
|
+
if (hasInlineCode) {
|
|
145
|
+
const code = String(args["download-code"] || "").trim();
|
|
146
|
+
if (!code) throw new Error("--download-code cannot be empty");
|
|
147
|
+
return code;
|
|
148
|
+
}
|
|
149
|
+
if (hasStdinCode) {
|
|
150
|
+
return readFirstLineFromStdin("download code");
|
|
151
|
+
}
|
|
152
|
+
return "";
|
|
153
|
+
}
|
|
154
|
+
|
|
124
155
|
function filenameFromContentDisposition(cd) {
|
|
125
|
-
// Very small parser: attachment; filename="foo.ext"
|
|
126
156
|
if (!cd) return null;
|
|
127
157
|
const m = String(cd).match(/filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i);
|
|
128
158
|
const raw = m ? (m[1] || m[2]) : null;
|
|
@@ -147,11 +177,6 @@ function explorerTxUrl(network, transaction) {
|
|
|
147
177
|
return null;
|
|
148
178
|
}
|
|
149
179
|
|
|
150
|
-
function formatTriedEndpoints(tried) {
|
|
151
|
-
if (!Array.isArray(tried) || tried.length === 0) return "";
|
|
152
|
-
return `\nTried:\n- ${tried.join("\n- ")}`;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
180
|
function normalizeInputUrl(value) {
|
|
156
181
|
let parsed;
|
|
157
182
|
try {
|
|
@@ -180,23 +205,6 @@ function normalizeSameOriginDownloadUrl(candidate, origin, sourceLabel) {
|
|
|
180
205
|
return parsed.toString();
|
|
181
206
|
}
|
|
182
207
|
|
|
183
|
-
async function probeX402Endpoint(url) {
|
|
184
|
-
try {
|
|
185
|
-
const response = await fetch(url, { method: "GET" });
|
|
186
|
-
if (response.status === 402) {
|
|
187
|
-
const paymentRequiredHeader = getHeaderCaseInsensitive(response.headers, "PAYMENT-REQUIRED");
|
|
188
|
-
if (paymentRequiredHeader) return { kind: "x402", response, paymentRequiredHeader };
|
|
189
|
-
return { kind: "not-x402", response };
|
|
190
|
-
}
|
|
191
|
-
if (response.status === 410) {
|
|
192
|
-
return { kind: "ended", response };
|
|
193
|
-
}
|
|
194
|
-
return { kind: "other", response };
|
|
195
|
-
} catch (err) {
|
|
196
|
-
return { kind: "error", error: err };
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
208
|
async function fetchJsonPayload(url) {
|
|
201
209
|
try {
|
|
202
210
|
const response = await fetch(url, {
|
|
@@ -220,221 +228,248 @@ async function fetchJsonPayload(url) {
|
|
|
220
228
|
|
|
221
229
|
async function resolveDownloadUrl(input) {
|
|
222
230
|
const inputUrl = normalizeInputUrl(input);
|
|
223
|
-
const tried = [];
|
|
224
231
|
const isRootPath = inputUrl.pathname === "/" || inputUrl.pathname === "";
|
|
225
232
|
const isDownloadPath = inputUrl.pathname === "/download";
|
|
226
233
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (directProbe.kind === "x402") {
|
|
230
|
-
return {
|
|
231
|
-
downloadUrl: inputUrl.toString(),
|
|
232
|
-
firstProbe: directProbe,
|
|
233
|
-
tried,
|
|
234
|
-
};
|
|
234
|
+
if (isDownloadPath) {
|
|
235
|
+
return { downloadUrl: inputUrl.toString() };
|
|
235
236
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
if (!isRootPath && !isDownloadPath) {
|
|
237
|
+
|
|
238
|
+
if (!isRootPath) {
|
|
240
239
|
throw new Error(
|
|
241
|
-
`Unsupported path for buy flow: ${inputUrl.pathname}. Use a promo URL (/) or /download URL
|
|
240
|
+
`Unsupported path for buy flow: ${inputUrl.pathname}. Use a promo URL (/) or /download URL.`,
|
|
242
241
|
);
|
|
243
242
|
}
|
|
244
243
|
|
|
245
244
|
const rfcResourceUrl = new URL(`/.well-known/skills/${SKILL_NAME}/resource.json`, inputUrl.origin).toString();
|
|
246
|
-
tried.push(`rfc resource ${rfcResourceUrl}`);
|
|
247
245
|
const rfcFetch = await fetchJsonPayload(rfcResourceUrl);
|
|
248
246
|
if (rfcFetch.ok && (rfcFetch.response.status === 200 || rfcFetch.response.status === 410)) {
|
|
249
247
|
const resourceStatus = String(rfcFetch.body?.status || "").toLowerCase();
|
|
250
248
|
if (rfcFetch.response.status === 410 || resourceStatus === "ended") {
|
|
251
|
-
throw new Error(`Sale has ended according to ${rfcResourceUrl}
|
|
249
|
+
throw new Error(`Sale has ended according to ${rfcResourceUrl}`);
|
|
252
250
|
}
|
|
253
251
|
if (typeof rfcFetch.body?.download_url === "string" && rfcFetch.body.download_url) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
return {
|
|
263
|
-
downloadUrl: rfcDownloadUrl,
|
|
264
|
-
firstProbe: rfcProbe,
|
|
265
|
-
tried,
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
if (rfcProbe.kind === "ended") {
|
|
269
|
-
throw new Error(`Sale has ended at ${rfcDownloadUrl}${formatTriedEndpoints(tried)}`);
|
|
270
|
-
}
|
|
252
|
+
return {
|
|
253
|
+
downloadUrl: normalizeSameOriginDownloadUrl(
|
|
254
|
+
rfcFetch.body.download_url,
|
|
255
|
+
inputUrl.origin,
|
|
256
|
+
rfcResourceUrl,
|
|
257
|
+
),
|
|
258
|
+
discovery: rfcFetch.body,
|
|
259
|
+
};
|
|
271
260
|
}
|
|
272
261
|
}
|
|
273
262
|
|
|
274
263
|
const legacyDiscoveryUrl = new URL("/.well-known/leak", inputUrl.origin).toString();
|
|
275
|
-
tried.push(`legacy discovery ${legacyDiscoveryUrl}`);
|
|
276
264
|
const legacyFetch = await fetchJsonPayload(legacyDiscoveryUrl);
|
|
277
265
|
if (legacyFetch.ok && (legacyFetch.response.status === 200 || legacyFetch.response.status === 410)) {
|
|
278
266
|
if (legacyFetch.response.status === 410) {
|
|
279
|
-
throw new Error(`Sale has ended according to ${legacyDiscoveryUrl}
|
|
267
|
+
throw new Error(`Sale has ended according to ${legacyDiscoveryUrl}`);
|
|
280
268
|
}
|
|
281
269
|
if (typeof legacyFetch.body?.resource?.download_url === "string" && legacyFetch.body.resource.download_url) {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
return {
|
|
291
|
-
downloadUrl: legacyDownloadUrl,
|
|
292
|
-
firstProbe: legacyProbe,
|
|
293
|
-
tried,
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
if (legacyProbe.kind === "ended") {
|
|
297
|
-
throw new Error(`Sale has ended at ${legacyDownloadUrl}${formatTriedEndpoints(tried)}`);
|
|
298
|
-
}
|
|
270
|
+
return {
|
|
271
|
+
downloadUrl: normalizeSameOriginDownloadUrl(
|
|
272
|
+
legacyFetch.body.resource.download_url,
|
|
273
|
+
inputUrl.origin,
|
|
274
|
+
legacyDiscoveryUrl,
|
|
275
|
+
),
|
|
276
|
+
discovery: legacyFetch.body.resource,
|
|
277
|
+
};
|
|
299
278
|
}
|
|
300
279
|
}
|
|
301
280
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
281
|
+
return { downloadUrl: new URL("/download", inputUrl.origin).toString() };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function responseBodyText(response) {
|
|
285
|
+
return response.text().catch(() => "");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function resolveOutputPath(args, serverFilename) {
|
|
289
|
+
const safeServerFilename = sanitizeFilename(serverFilename || "downloaded.bin");
|
|
290
|
+
if (args.out) {
|
|
291
|
+
return String(args.out);
|
|
292
|
+
}
|
|
293
|
+
if (args.basename) {
|
|
294
|
+
const safeBase = sanitizeFilename(args.basename);
|
|
295
|
+
const ext = path.extname(safeServerFilename) || "";
|
|
296
|
+
return `./${safeBase}${ext}`;
|
|
297
|
+
}
|
|
298
|
+
return `./${safeServerFilename}`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function saveBinaryResponse(response, args, suggestedFilename) {
|
|
302
|
+
const serverFilename =
|
|
303
|
+
suggestedFilename ||
|
|
304
|
+
filenameFromContentDisposition(response.headers.get("content-disposition")) ||
|
|
305
|
+
"downloaded.bin";
|
|
306
|
+
|
|
307
|
+
const outPath = resolveOutputPath(args, serverFilename);
|
|
308
|
+
const buf = Buffer.from(await response.arrayBuffer());
|
|
309
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
310
|
+
fs.writeFileSync(outPath, buf);
|
|
311
|
+
console.log(outUi.statusLine("ok", `Saved ${buf.length} bytes -> ${outPath}`));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function finalizeDownloadResponse(response, { args, downloadUrl }) {
|
|
315
|
+
const contentType = (response.headers.get("content-type") || "").toLowerCase();
|
|
316
|
+
if (!contentType.includes("application/json")) {
|
|
317
|
+
await saveBinaryResponse(response, args, null);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const data = await response.json().catch(() => null);
|
|
322
|
+
if (!data || typeof data !== "object") {
|
|
323
|
+
throw new Error("Unexpected non-download JSON response");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (typeof data.token === "string" && data.token) {
|
|
327
|
+
const tokenUrl = new URL(downloadUrl);
|
|
328
|
+
tokenUrl.searchParams.set("token", data.token);
|
|
329
|
+
|
|
330
|
+
const r3 = await fetch(tokenUrl.toString(), { method: "GET" });
|
|
331
|
+
if (!r3.ok) {
|
|
332
|
+
const text = await responseBodyText(r3);
|
|
333
|
+
throw new Error(`Download failed ${r3.status}: ${text}`);
|
|
312
334
|
}
|
|
313
|
-
|
|
314
|
-
|
|
335
|
+
|
|
336
|
+
await saveBinaryResponse(r3, args, data.filename || null);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (typeof data.download_url === "string" && data.download_url) {
|
|
341
|
+
const sameOriginDownload = normalizeSameOriginDownloadUrl(
|
|
342
|
+
data.download_url,
|
|
343
|
+
new URL(downloadUrl).origin,
|
|
344
|
+
downloadUrl,
|
|
345
|
+
);
|
|
346
|
+
const r3 = await fetch(sameOriginDownload, { method: "GET" });
|
|
347
|
+
if (!r3.ok) {
|
|
348
|
+
const text = await responseBodyText(r3);
|
|
349
|
+
throw new Error(`Download failed ${r3.status}: ${text}`);
|
|
315
350
|
}
|
|
351
|
+
await saveBinaryResponse(r3, args, data.filename || null);
|
|
352
|
+
return;
|
|
316
353
|
}
|
|
317
354
|
|
|
318
|
-
throw new Error(
|
|
319
|
-
`Could not resolve an x402 download endpoint from ${inputUrl.toString()}${formatTriedEndpoints(tried)}`,
|
|
320
|
-
);
|
|
355
|
+
throw new Error(`Unexpected JSON response from download endpoint: ${JSON.stringify(data)}`);
|
|
321
356
|
}
|
|
322
357
|
|
|
323
|
-
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
358
|
+
function maybePrintPaymentReceipt(response) {
|
|
359
|
+
const paymentResponseHeader =
|
|
360
|
+
getHeaderCaseInsensitive(response.headers, "PAYMENT-RESPONSE") ||
|
|
361
|
+
getHeaderCaseInsensitive(response.headers, "X-PAYMENT-RESPONSE");
|
|
362
|
+
if (!paymentResponseHeader) return;
|
|
327
363
|
|
|
328
|
-
let buyerPk;
|
|
329
364
|
try {
|
|
330
|
-
|
|
365
|
+
const receipt = decodePaymentResponseHeader(paymentResponseHeader);
|
|
366
|
+
const explorer = explorerTxUrl(receipt.network, receipt.transaction);
|
|
367
|
+
console.log("");
|
|
368
|
+
console.log(outUi.section("Payment Receipt"));
|
|
369
|
+
const rows = [
|
|
370
|
+
{ key: "network", value: receipt.network },
|
|
371
|
+
receipt.payer ? { key: "payer", value: receipt.payer } : null,
|
|
372
|
+
{ key: "tx", value: receipt.transaction },
|
|
373
|
+
explorer ? { key: "explorer", value: explorer } : null,
|
|
374
|
+
];
|
|
375
|
+
for (const line of outUi.formatRows(rows)) {
|
|
376
|
+
console.log(line);
|
|
377
|
+
}
|
|
331
378
|
} catch (err) {
|
|
332
|
-
console.error(
|
|
333
|
-
|
|
379
|
+
console.error(
|
|
380
|
+
errUi.statusLine(
|
|
381
|
+
"warn",
|
|
382
|
+
`Could not decode PAYMENT-RESPONSE header (${err.message || String(err)})`,
|
|
383
|
+
),
|
|
384
|
+
);
|
|
334
385
|
}
|
|
386
|
+
}
|
|
335
387
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
const client = new x402Client();
|
|
344
|
-
registerExactEvmScheme(client, { signer: account });
|
|
388
|
+
async function main() {
|
|
389
|
+
const args = parseArgs(process.argv.slice(2));
|
|
390
|
+
const inputUrl = args._[0];
|
|
391
|
+
if (!inputUrl) usageAndExit(1);
|
|
392
|
+
|
|
393
|
+
const downloadCode = resolveDownloadCode(args);
|
|
345
394
|
|
|
346
395
|
const resolved = await resolveDownloadUrl(inputUrl);
|
|
347
396
|
const downloadUrl = resolved.downloadUrl;
|
|
348
397
|
if (downloadUrl !== inputUrl) {
|
|
349
|
-
console.log(`
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// 1) Request: expect 402 with PAYMENT-REQUIRED header.
|
|
353
|
-
const r1 = resolved.firstProbe?.response || await fetch(downloadUrl, { method: "GET" });
|
|
354
|
-
if (r1.status !== 402) {
|
|
355
|
-
const text = await r1.text().catch(() => "");
|
|
356
|
-
throw new Error(`Expected 402, got ${r1.status}: ${text}`);
|
|
398
|
+
console.log(outUi.statusLine("info", `Resolved purchase endpoint: ${downloadUrl}`));
|
|
357
399
|
}
|
|
358
400
|
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
if (!paymentRequiredHeader) throw new Error("Missing PAYMENT-REQUIRED header");
|
|
362
|
-
|
|
363
|
-
const paymentRequired = decodePaymentRequiredHeader(paymentRequiredHeader);
|
|
364
|
-
const payload = await client.createPaymentPayload(paymentRequired);
|
|
365
|
-
const paymentHeader = encodePaymentSignatureHeader(payload);
|
|
401
|
+
const initialHeaders = {};
|
|
402
|
+
if (downloadCode) initialHeaders[DOWNLOAD_CODE_HEADER] = downloadCode;
|
|
366
403
|
|
|
367
|
-
|
|
368
|
-
const r2 = await fetch(downloadUrl, {
|
|
404
|
+
const r1 = await fetch(downloadUrl, {
|
|
369
405
|
method: "GET",
|
|
370
|
-
headers:
|
|
371
|
-
"PAYMENT-SIGNATURE": paymentHeader,
|
|
372
|
-
},
|
|
406
|
+
headers: initialHeaders,
|
|
373
407
|
});
|
|
374
408
|
|
|
375
|
-
if (
|
|
376
|
-
|
|
377
|
-
throw new Error(`Payment failed ${r2.status}: ${text}`);
|
|
409
|
+
if (r1.status === 410) {
|
|
410
|
+
throw new Error(`Sale has ended at ${downloadUrl}`);
|
|
378
411
|
}
|
|
379
412
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
413
|
+
if (r1.status === 401) {
|
|
414
|
+
const text = await responseBodyText(r1);
|
|
415
|
+
throw new Error(
|
|
416
|
+
`Download code required or invalid (send header ${DOWNLOAD_CODE_HEADER}). ${text}`,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (r1.status === 402) {
|
|
421
|
+
const paymentRequiredHeader = getHeaderCaseInsensitive(r1.headers, "PAYMENT-REQUIRED");
|
|
422
|
+
if (!paymentRequiredHeader) throw new Error("Missing PAYMENT-REQUIRED header");
|
|
423
|
+
|
|
424
|
+
let buyerPk;
|
|
384
425
|
try {
|
|
385
|
-
|
|
386
|
-
const explorer = explorerTxUrl(receipt.network, receipt.transaction);
|
|
387
|
-
console.log("Payment receipt:");
|
|
388
|
-
console.log(`- network: ${receipt.network}`);
|
|
389
|
-
if (receipt.payer) console.log(`- payer: ${receipt.payer}`);
|
|
390
|
-
console.log(`- tx: ${receipt.transaction}`);
|
|
391
|
-
if (explorer) console.log(`- explorer: ${explorer}`);
|
|
426
|
+
buyerPk = resolveBuyerPrivateKey(args);
|
|
392
427
|
} catch (err) {
|
|
393
|
-
|
|
428
|
+
throw new Error(`${err.message || String(err)} (payment required by seller)`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
let account;
|
|
432
|
+
try {
|
|
433
|
+
account = privateKeyToAccount(normalizePrivateKey(buyerPk));
|
|
434
|
+
} catch {
|
|
435
|
+
throw new Error("Invalid buyer private key format.");
|
|
394
436
|
}
|
|
395
|
-
}
|
|
396
437
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if (!token) throw new Error(`Missing token in response: ${JSON.stringify(data)}`);
|
|
438
|
+
const client = new x402Client();
|
|
439
|
+
registerExactEvmScheme(client, { signer: account });
|
|
400
440
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
tokenUrl.searchParams.set("token", token);
|
|
441
|
+
const paymentRequired = decodePaymentRequiredHeader(paymentRequiredHeader);
|
|
442
|
+
const payload = await client.createPaymentPayload(paymentRequired);
|
|
443
|
+
const paymentHeader = encodePaymentSignatureHeader(payload);
|
|
405
444
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
445
|
+
const r2 = await fetch(downloadUrl, {
|
|
446
|
+
method: "GET",
|
|
447
|
+
headers: {
|
|
448
|
+
...initialHeaders,
|
|
449
|
+
"PAYMENT-SIGNATURE": paymentHeader,
|
|
450
|
+
},
|
|
451
|
+
});
|
|
412
452
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const safeServerFilename = sanitizeFilename(serverFilename);
|
|
453
|
+
if (!r2.ok) {
|
|
454
|
+
const text = await responseBodyText(r2);
|
|
455
|
+
throw new Error(`Payment failed ${r2.status}: ${text}`);
|
|
456
|
+
}
|
|
418
457
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
} else if (args.basename) {
|
|
423
|
-
const safeBase = sanitizeFilename(args.basename);
|
|
424
|
-
const ext = path.extname(safeServerFilename) || "";
|
|
425
|
-
outPath = `./${safeBase}${ext}`;
|
|
426
|
-
} else {
|
|
427
|
-
outPath = `./${safeServerFilename}`;
|
|
458
|
+
maybePrintPaymentReceipt(r2);
|
|
459
|
+
await finalizeDownloadResponse(r2, { args, downloadUrl });
|
|
460
|
+
return;
|
|
428
461
|
}
|
|
429
462
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
463
|
+
if (!r1.ok) {
|
|
464
|
+
const text = await responseBodyText(r1);
|
|
465
|
+
throw new Error(`Download failed ${r1.status}: ${text}`);
|
|
466
|
+
}
|
|
433
467
|
|
|
434
|
-
|
|
468
|
+
await finalizeDownloadResponse(r1, { args, downloadUrl });
|
|
435
469
|
}
|
|
436
470
|
|
|
437
471
|
main().catch((e) => {
|
|
438
|
-
|
|
472
|
+
const detail = e?.stack || e?.message || String(e);
|
|
473
|
+
console.error(errUi.statusLine("error", detail));
|
|
439
474
|
process.exit(1);
|
|
440
475
|
});
|
package/scripts/cli.js
CHANGED
|
@@ -1,13 +1,81 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { spawn } from "node:child_process";
|
|
6
|
+
import { createUi } from "./ui.js";
|
|
5
7
|
|
|
6
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
9
|
const __dirname = path.dirname(__filename);
|
|
10
|
+
const PACKAGE_JSON_PATH = path.resolve(__dirname, "..", "package.json");
|
|
11
|
+
const outUi = createUi(process.stdout);
|
|
12
|
+
const errUi = createUi(process.stderr);
|
|
8
13
|
|
|
9
14
|
const sub = process.argv[2];
|
|
10
15
|
|
|
16
|
+
function readVersion() {
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, "utf8"));
|
|
19
|
+
const version = String(parsed?.version || "").trim();
|
|
20
|
+
return version || "unknown";
|
|
21
|
+
} catch {
|
|
22
|
+
return "unknown";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function printVersion() {
|
|
27
|
+
console.log(outUi.section(`leak-cli ${readVersion()}`));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function printHelp() {
|
|
31
|
+
console.log(outUi.heading("Leak CLI"));
|
|
32
|
+
console.log(outUi.muted(`version ${readVersion()}`));
|
|
33
|
+
console.log("");
|
|
34
|
+
console.log(outUi.section("Usage"));
|
|
35
|
+
console.log(" leak publish [prefill flags]");
|
|
36
|
+
console.log(" leak --file <path> [publish flags]");
|
|
37
|
+
console.log(" leak buy <promo_or_download_url> [buy flags]");
|
|
38
|
+
console.log(" leak host --config <path> [host flags]");
|
|
39
|
+
console.log(" leak config [show|--write-env]");
|
|
40
|
+
console.log(" leak version");
|
|
41
|
+
console.log("");
|
|
42
|
+
console.log(outUi.section("Publish Flags"));
|
|
43
|
+
console.log(" --access-mode <mode>");
|
|
44
|
+
console.log(" --download-code <code> | --download-code-stdin");
|
|
45
|
+
console.log(" --price <usdc> --window <duration>");
|
|
46
|
+
console.log(" --pay-to <address> --network <caip2> --port <port>");
|
|
47
|
+
console.log(" --confirmed --public --og-title --og-description --og-image-url");
|
|
48
|
+
console.log("");
|
|
49
|
+
console.log(outUi.section("Buy Flags"));
|
|
50
|
+
console.log(" --download-code <code> | --download-code-stdin");
|
|
51
|
+
console.log(" --buyer-private-key-file <path> | --buyer-private-key-stdin");
|
|
52
|
+
console.log(" --out <path> | --basename <name>");
|
|
53
|
+
console.log("");
|
|
54
|
+
console.log(outUi.section("Host Flags"));
|
|
55
|
+
console.log(" --config <path>");
|
|
56
|
+
console.log(" --proxy-host <host> --proxy-port <port>");
|
|
57
|
+
console.log(" --public --public-confirm I_UNDERSTAND_PUBLIC_EXPOSURE");
|
|
58
|
+
console.log(" --dry-run");
|
|
59
|
+
console.log("");
|
|
60
|
+
console.log(outUi.section("Examples"));
|
|
61
|
+
console.log(" leak publish");
|
|
62
|
+
console.log(" leak publish --file ./song.mp3 --access-mode download-code-only-no-payment");
|
|
63
|
+
console.log(" leak --file ./song.mp3 --access-mode payment-only-no-download-code");
|
|
64
|
+
console.log(" leak --file ./song.mp3 --access-mode download-code-only-no-payment --download-code \"friends-only\"");
|
|
65
|
+
console.log(" leak buy https://xxxx.trycloudflare.com/ --download-code \"friends-only\"");
|
|
66
|
+
console.log(" leak host --config ./examples/multi-host.example.json --dry-run");
|
|
67
|
+
console.log(" leak host --config ./examples/multi-host.example.json --public --public-confirm I_UNDERSTAND_PUBLIC_EXPOSURE");
|
|
68
|
+
console.log(" leak config");
|
|
69
|
+
console.log(" leak version");
|
|
70
|
+
console.log("");
|
|
71
|
+
console.log(outUi.section("Notes"));
|
|
72
|
+
console.log(" share / as promo (social card); buy can start from / or /download.");
|
|
73
|
+
console.log(" buyer private key is required only when seller access mode includes payment.");
|
|
74
|
+
console.log("");
|
|
75
|
+
console.log(outUi.section("Backward-compatible"));
|
|
76
|
+
console.log(" leak leak --file <path> ...");
|
|
77
|
+
}
|
|
78
|
+
|
|
11
79
|
function runSubcommand(scriptName, argv) {
|
|
12
80
|
const scriptPath = path.resolve(__dirname, scriptName);
|
|
13
81
|
const child = spawn(process.execPath, [scriptPath, ...argv], {
|
|
@@ -15,38 +83,37 @@ function runSubcommand(scriptName, argv) {
|
|
|
15
83
|
});
|
|
16
84
|
|
|
17
85
|
child.on("error", (err) => {
|
|
18
|
-
console.error(`Failed to launch ${scriptName}: ${err.message}`);
|
|
86
|
+
console.error(errUi.statusLine("error", `Failed to launch ${scriptName}: ${err.message}`));
|
|
19
87
|
process.exit(1);
|
|
20
88
|
});
|
|
21
89
|
|
|
22
90
|
child.on("exit", (code, signal) => {
|
|
23
91
|
if (signal) {
|
|
24
|
-
console.error(`${scriptName} exited via signal ${signal}`);
|
|
92
|
+
console.error(errUi.statusLine("error", `${scriptName} exited via signal ${signal}`));
|
|
25
93
|
process.exit(1);
|
|
26
94
|
}
|
|
27
95
|
process.exit(code ?? 1);
|
|
28
96
|
});
|
|
29
97
|
}
|
|
30
98
|
|
|
31
|
-
if (!sub || sub === "--help" || sub === "-h") {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
console.log("");
|
|
39
|
-
console.log("Notes:");
|
|
40
|
-
console.log(" share / as promo (social card); buy can start from / or /download.");
|
|
41
|
-
console.log("Backward-compatible:");
|
|
42
|
-
console.log(" leak leak --file <path> ...");
|
|
99
|
+
if (!sub || sub === "--help" || sub === "-h" || sub === "help") {
|
|
100
|
+
printHelp();
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (sub === "--version" || sub === "-v" || sub === "version") {
|
|
105
|
+
printVersion();
|
|
43
106
|
process.exit(0);
|
|
44
107
|
}
|
|
45
108
|
|
|
46
109
|
if (sub === "leak") {
|
|
47
110
|
runSubcommand("leak.js", process.argv.slice(3));
|
|
111
|
+
} else if (sub === "publish") {
|
|
112
|
+
runSubcommand("leak.js", ["--wizard", ...process.argv.slice(3)]);
|
|
48
113
|
} else if (sub === "buy") {
|
|
49
114
|
runSubcommand("buy.js", process.argv.slice(3));
|
|
115
|
+
} else if (sub === "host") {
|
|
116
|
+
runSubcommand("host.js", process.argv.slice(3));
|
|
50
117
|
} else if (sub === "config") {
|
|
51
118
|
runSubcommand("config.js", process.argv.slice(3));
|
|
52
119
|
} else {
|