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/src/index.js
CHANGED
|
@@ -11,6 +11,19 @@ import { x402HTTPResourceServer, HTTPFacilitatorClient } from "@x402/core/http";
|
|
|
11
11
|
import { ExactEvmScheme } from "@x402/evm/exact/server";
|
|
12
12
|
import { isAddress } from "viem";
|
|
13
13
|
import { resolveSupportedChain } from "./chain_meta.js";
|
|
14
|
+
import {
|
|
15
|
+
ACCESS_MODE_VALUES,
|
|
16
|
+
DEFAULT_ACCESS_MODE,
|
|
17
|
+
accessModeRequiresDownloadCode,
|
|
18
|
+
accessModeRequiresPayment,
|
|
19
|
+
accessModeSummary,
|
|
20
|
+
isValidAccessMode,
|
|
21
|
+
} from "./access_mode.js";
|
|
22
|
+
import {
|
|
23
|
+
DOWNLOAD_CODE_HEADER,
|
|
24
|
+
isValidDownloadCodeHash,
|
|
25
|
+
verifyDownloadCode,
|
|
26
|
+
} from "./download_code.js";
|
|
14
27
|
|
|
15
28
|
dotenv.config();
|
|
16
29
|
|
|
@@ -87,6 +100,10 @@ const SELLER_PAY_TO = String(
|
|
|
87
100
|
process.env.SELLER_PAY_TO || process.env.PAY_TO || "",
|
|
88
101
|
).trim();
|
|
89
102
|
const PRICE_USD = process.env.PRICE_USD || "1.00";
|
|
103
|
+
const ACCESS_MODE = String(
|
|
104
|
+
process.env.ACCESS_MODE || DEFAULT_ACCESS_MODE,
|
|
105
|
+
).trim().toLowerCase();
|
|
106
|
+
const DOWNLOAD_CODE_HASH = String(process.env.DOWNLOAD_CODE_HASH || "").trim();
|
|
90
107
|
const RAW_CHAIN_ID =
|
|
91
108
|
process.env.CHAIN_ID || process.env.NETWORK || "eip155:84532";
|
|
92
109
|
const ARTIFACT_PATH = process.env.ARTIFACT_PATH || process.env.PROTECTED_FILE;
|
|
@@ -117,7 +134,7 @@ const OG_IMAGE_WIDTH = 1200;
|
|
|
117
134
|
const OG_IMAGE_HEIGHT = 630;
|
|
118
135
|
const SKILL_NAME = "leak-buy";
|
|
119
136
|
const SKILL_DESCRIPTION =
|
|
120
|
-
"Buy and download
|
|
137
|
+
"Buy and download leak content from promo or download links using the leak CLI tool";
|
|
121
138
|
const SKILL_SOURCE = "clawhub";
|
|
122
139
|
const SKILL_INSTALL_COMMAND = "clawhub install leak-buy";
|
|
123
140
|
const WELL_KNOWN_CACHE_CONTROL = "public, max-age=60";
|
|
@@ -134,6 +151,30 @@ const ENDED_WINDOW_SECONDS = parseNonNegativeInt(
|
|
|
134
151
|
0,
|
|
135
152
|
);
|
|
136
153
|
|
|
154
|
+
if (!isValidAccessMode(ACCESS_MODE)) {
|
|
155
|
+
console.error(`Invalid ACCESS_MODE: ${ACCESS_MODE}`);
|
|
156
|
+
console.error(`Supported ACCESS_MODE values: ${ACCESS_MODE_VALUES.join(", ")}`);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const REQUIRES_DOWNLOAD_CODE = accessModeRequiresDownloadCode(ACCESS_MODE);
|
|
161
|
+
const REQUIRES_PAYMENT = accessModeRequiresPayment(ACCESS_MODE);
|
|
162
|
+
|
|
163
|
+
if (REQUIRES_DOWNLOAD_CODE && !DOWNLOAD_CODE_HASH) {
|
|
164
|
+
console.error(`ACCESS_MODE=${ACCESS_MODE} requires DOWNLOAD_CODE_HASH.`);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
if (DOWNLOAD_CODE_HASH && !isValidDownloadCodeHash(DOWNLOAD_CODE_HASH)) {
|
|
168
|
+
console.error("Invalid DOWNLOAD_CODE_HASH format.");
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
if (!REQUIRES_DOWNLOAD_CODE && DOWNLOAD_CODE_HASH) {
|
|
172
|
+
console.error(
|
|
173
|
+
`ACCESS_MODE=${ACCESS_MODE} does not use DOWNLOAD_CODE_HASH. Remove DOWNLOAD_CODE_HASH or choose a download-code access mode.`,
|
|
174
|
+
);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
|
|
137
178
|
let CHAIN_META;
|
|
138
179
|
try {
|
|
139
180
|
CHAIN_META = resolveSupportedChain(RAW_CHAIN_ID);
|
|
@@ -147,39 +188,41 @@ const CHAIN_NAME = CHAIN_META.name;
|
|
|
147
188
|
const CHAIN_NUMERIC_ID = CHAIN_META.id;
|
|
148
189
|
const IS_BASE_MAINNET = CHAIN_NUMERIC_ID === 8453;
|
|
149
190
|
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
191
|
+
if (REQUIRES_PAYMENT) {
|
|
192
|
+
if (!new Set(["testnet", "cdp_mainnet"]).has(FACILITATOR_MODE)) {
|
|
193
|
+
console.error(
|
|
194
|
+
"Invalid FACILITATOR_MODE. Supported values: testnet, cdp_mainnet",
|
|
195
|
+
);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
156
198
|
|
|
157
|
-
if (IS_BASE_MAINNET && FACILITATOR_MODE !== "cdp_mainnet") {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
199
|
+
if (IS_BASE_MAINNET && FACILITATOR_MODE !== "cdp_mainnet") {
|
|
200
|
+
console.error(
|
|
201
|
+
"Invalid config: CHAIN_ID=eip155:8453 requires FACILITATOR_MODE=cdp_mainnet.",
|
|
202
|
+
);
|
|
203
|
+
console.error(
|
|
204
|
+
"Set FACILITATOR_MODE=cdp_mainnet and configure CDP_API_KEY_ID/CDP_API_KEY_SECRET.",
|
|
205
|
+
);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
166
208
|
|
|
167
|
-
if (
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
209
|
+
if (
|
|
210
|
+
FACILITATOR_MODE === "cdp_mainnet" &&
|
|
211
|
+
(!CDP_API_KEY_ID || !CDP_API_KEY_SECRET)
|
|
212
|
+
) {
|
|
213
|
+
console.error("Missing CDP credentials for FACILITATOR_MODE=cdp_mainnet.");
|
|
214
|
+
console.error(
|
|
215
|
+
"Set CDP_API_KEY_ID and CDP_API_KEY_SECRET in your environment.",
|
|
216
|
+
);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
176
219
|
}
|
|
177
220
|
|
|
178
|
-
if (!SELLER_PAY_TO) {
|
|
221
|
+
if (REQUIRES_PAYMENT && !SELLER_PAY_TO) {
|
|
179
222
|
console.error("Missing required env var: SELLER_PAY_TO (or PAY_TO)");
|
|
180
223
|
process.exit(1);
|
|
181
224
|
}
|
|
182
|
-
if (!isAddress(SELLER_PAY_TO)) {
|
|
225
|
+
if (SELLER_PAY_TO && !isAddress(SELLER_PAY_TO)) {
|
|
183
226
|
console.error(`Invalid SELLER_PAY_TO (or PAY_TO): ${SELLER_PAY_TO}`);
|
|
184
227
|
console.error("Expected a valid Ethereum address (0x + 40 hex chars).");
|
|
185
228
|
process.exit(1);
|
|
@@ -405,6 +448,10 @@ function promoModel(req) {
|
|
|
405
448
|
saleEndTs: SALE_END_TS,
|
|
406
449
|
endedWindowSeconds: ENDED_WINDOW_SECONDS,
|
|
407
450
|
endedWindowCutoffTs: endedWindowCutoffTs(),
|
|
451
|
+
accessMode: ACCESS_MODE,
|
|
452
|
+
accessSummary: accessModeSummary(ACCESS_MODE),
|
|
453
|
+
requiresPayment: REQUIRES_PAYMENT,
|
|
454
|
+
requiresDownloadCode: REQUIRES_DOWNLOAD_CODE,
|
|
408
455
|
};
|
|
409
456
|
}
|
|
410
457
|
|
|
@@ -428,11 +475,24 @@ function buildDiscoveryResource(req) {
|
|
|
428
475
|
price_currency: "USDC",
|
|
429
476
|
network: CHAIN_ID,
|
|
430
477
|
sale_end: new Date(SALE_END_TS * 1000).toISOString(),
|
|
478
|
+
access_mode: ACCESS_MODE,
|
|
479
|
+
access_summary: accessModeSummary(ACCESS_MODE),
|
|
480
|
+
payment_required: REQUIRES_PAYMENT,
|
|
481
|
+
download_code_required: REQUIRES_DOWNLOAD_CODE,
|
|
482
|
+
download_code_header: DOWNLOAD_CODE_HEADER,
|
|
431
483
|
};
|
|
432
484
|
}
|
|
433
485
|
|
|
434
486
|
function renderWellKnownSkillMd(req) {
|
|
435
487
|
const resource = buildDiscoveryResource(req);
|
|
488
|
+
const codeNote = resource.download_code_required
|
|
489
|
+
? `- Download code header: ${resource.download_code_header}`
|
|
490
|
+
: "- Download code: not required";
|
|
491
|
+
const paymentFlow = resource.payment_required
|
|
492
|
+
? `2. Use \`download_url\` for x402 purchase and token mint.
|
|
493
|
+
3. Download from \`/download?token=...\` and save locally.`
|
|
494
|
+
: `2. Use \`download_url\` for direct download.
|
|
495
|
+
3. Save the response body locally.`;
|
|
436
496
|
return `# ${SKILL_NAME}
|
|
437
497
|
|
|
438
498
|
${SKILL_DESCRIPTION}
|
|
@@ -442,6 +502,8 @@ ${SKILL_DESCRIPTION}
|
|
|
442
502
|
- Download URL: ${resource.download_url}
|
|
443
503
|
- Status: ${resource.status}
|
|
444
504
|
- Sale ends: ${resource.sale_end}
|
|
505
|
+
- Access mode: ${resource.access_mode}
|
|
506
|
+
${codeNote}
|
|
445
507
|
|
|
446
508
|
## Buyer Fast Path (for unknown agents)
|
|
447
509
|
1. Ask the user for approval to install the skill once.
|
|
@@ -452,12 +514,11 @@ Do not explain x402 internals unless the user explicitly asks.
|
|
|
452
514
|
|
|
453
515
|
## Discovery Fallback
|
|
454
516
|
1. Read resource metadata from \`/.well-known/skills/${SKILL_NAME}/resource.json\`.
|
|
455
|
-
|
|
456
|
-
3. Download from \`/download?token=...\` and save locally.
|
|
517
|
+
${paymentFlow}
|
|
457
518
|
|
|
458
519
|
## CLI
|
|
459
520
|
- Install: \`${SKILL_INSTALL_COMMAND}\`
|
|
460
|
-
- Buy: \`leak buy <promo_or_download_url> --buyer-private-key-file ./buyer.key\`
|
|
521
|
+
- Buy: \`leak buy <promo_or_download_url> [--download-code <code>] [--buyer-private-key-file ./buyer.key]\`
|
|
461
522
|
`;
|
|
462
523
|
}
|
|
463
524
|
|
|
@@ -495,6 +556,9 @@ function urlsForQuickPathFromRequestUrl(requestUrl) {
|
|
|
495
556
|
}
|
|
496
557
|
|
|
497
558
|
function renderAgentQuickPathHtmlBlock(fastPath) {
|
|
559
|
+
const ruleText = REQUIRES_PAYMENT
|
|
560
|
+
? "do not explain x402 internals unless the user explicitly asks."
|
|
561
|
+
: "focus on direct download steps and avoid adding payment instructions.";
|
|
498
562
|
return `<section class="agent-quick-path">
|
|
499
563
|
<h2>Agent Quick Path</h2>
|
|
500
564
|
<ol>
|
|
@@ -502,7 +566,7 @@ function renderAgentQuickPathHtmlBlock(fastPath) {
|
|
|
502
566
|
<li>Install: <code>${escapeHtml(fastPath.installCommand)}</code></li>
|
|
503
567
|
<li>Load the skill and continue with Buyer Fast Path.</li>
|
|
504
568
|
</ol>
|
|
505
|
-
<p><strong>Rule:</strong>
|
|
569
|
+
<p><strong>Rule:</strong> ${escapeHtml(ruleText)}</p>
|
|
506
570
|
</section>`;
|
|
507
571
|
}
|
|
508
572
|
|
|
@@ -514,6 +578,8 @@ function renderPromoSharedContent({ promoUrl, downloadUrl, saleEndTs }) {
|
|
|
514
578
|
return `
|
|
515
579
|
<div class="kv"><strong>Price:</strong> ${escapeHtml(PRICE_USD)} USD equivalent</div>
|
|
516
580
|
<div class="kv"><strong>Network:</strong> ${escapeHtml(CHAIN_NAME)} (${escapeHtml(CHAIN_ID)})</div>
|
|
581
|
+
<div class="kv"><strong>Access mode:</strong> ${escapeHtml(ACCESS_MODE)} (${escapeHtml(accessModeSummary(ACCESS_MODE))})</div>
|
|
582
|
+
<div class="kv"><strong>Download-code header:</strong> <code>${escapeHtml(DOWNLOAD_CODE_HEADER)}</code>${REQUIRES_DOWNLOAD_CODE ? "" : " (not required)"}</div>
|
|
517
583
|
<div class="kv"><strong>Sale end:</strong> <span id="sale-end-local" data-sale-end-iso="${escapeHtml(expiresIso)}">${escapeHtml(expiresIso)}</span></div>
|
|
518
584
|
${renderAgentQuickPathHtmlBlock(fastPath)}
|
|
519
585
|
|
|
@@ -637,6 +703,62 @@ function renderUnpaidDownloadGuidancePage(requestUrl) {
|
|
|
637
703
|
</html>`;
|
|
638
704
|
}
|
|
639
705
|
|
|
706
|
+
function renderDownloadCodeRequiredPage(requestUrl) {
|
|
707
|
+
const urls = urlsForQuickPathFromRequestUrl(requestUrl);
|
|
708
|
+
const sharedContent = renderPromoSharedContent({
|
|
709
|
+
promoUrl: urls.promoUrl,
|
|
710
|
+
downloadUrl: urls.downloadUrl,
|
|
711
|
+
saleEndTs: SALE_END_TS,
|
|
712
|
+
});
|
|
713
|
+
return `<!doctype html>
|
|
714
|
+
<html lang="en">
|
|
715
|
+
<head>
|
|
716
|
+
<meta charset="utf-8" />
|
|
717
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
718
|
+
<title>Download Code Required - leak</title>
|
|
719
|
+
<style>
|
|
720
|
+
body { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; margin: 0; padding: 24px; background: #f7f7f5; color: #1f1f1f; }
|
|
721
|
+
.card { max-width: 760px; margin: 0 auto; border: 1px solid #d8d8d0; background: #fff; border-radius: 10px; padding: 20px; }
|
|
722
|
+
h1 { margin: 0 0 12px; font-size: 24px; }
|
|
723
|
+
p { line-height: 1.5; }
|
|
724
|
+
.kv { margin: 14px 0; font-size: 14px; color: #333; }
|
|
725
|
+
code, pre { background: #f0f0eb; border-radius: 6px; padding: 2px 6px; }
|
|
726
|
+
pre { padding: 10px; overflow-x: auto; }
|
|
727
|
+
.install-note { margin-top: 16px; font-size: 13px; color: #2f2f2f; }
|
|
728
|
+
.install-note a { color: #1f1f1f; }
|
|
729
|
+
.agent-quick-path { margin: 16px 0; padding: 14px; border: 1px solid #d8d8d0; border-radius: 8px; background: #fafaf6; }
|
|
730
|
+
.agent-quick-path h2 { margin: 0 0 8px; font-size: 18px; }
|
|
731
|
+
.agent-quick-path ol { margin: 8px 0 8px 20px; }
|
|
732
|
+
.agent-quick-path p { margin: 8px 0 0; }
|
|
733
|
+
</style>
|
|
734
|
+
</head>
|
|
735
|
+
<body>
|
|
736
|
+
<main class="card">
|
|
737
|
+
<h1>401 Download Code Required</h1>
|
|
738
|
+
<p>This URL requires a download code before access is granted.</p>
|
|
739
|
+
<div class="kv"><strong>Header:</strong> <code>${escapeHtml(DOWNLOAD_CODE_HEADER)}</code></div>
|
|
740
|
+
<div class="kv"><strong>Resource:</strong> ${escapeHtml(ARTIFACT_NAME)}</div>
|
|
741
|
+
${sharedContent}
|
|
742
|
+
</main>
|
|
743
|
+
${renderPromoSharedClientScript(urls.promoUrl)}
|
|
744
|
+
</body>
|
|
745
|
+
</html>`;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function sendDownloadCodeRequired(req, res, requestUrl) {
|
|
749
|
+
res.setHeader("X-LEAK-DOWNLOAD-CODE-REQUIRED", "1");
|
|
750
|
+
const wantsHtml = (req.get("accept") || "").includes("text/html");
|
|
751
|
+
const isBrowser = (req.get("user-agent") || "").includes("Mozilla");
|
|
752
|
+
if (wantsHtml && isBrowser) {
|
|
753
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
754
|
+
return res.status(401).send(renderDownloadCodeRequiredPage(requestUrl));
|
|
755
|
+
}
|
|
756
|
+
return res.status(401).json({
|
|
757
|
+
error: "download code required",
|
|
758
|
+
header: DOWNLOAD_CODE_HEADER,
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
|
|
640
762
|
function sendSkillIndex(req, res) {
|
|
641
763
|
const payload = {
|
|
642
764
|
skills: [
|
|
@@ -874,59 +996,76 @@ function validateAndConsumeToken(token) {
|
|
|
874
996
|
return { ok: true };
|
|
875
997
|
}
|
|
876
998
|
|
|
999
|
+
function sendArtifactStream(res) {
|
|
1000
|
+
const p = absArtifactPath();
|
|
1001
|
+
if (!fs.existsSync(p)) {
|
|
1002
|
+
return res.status(404).json({ error: "artifact not found" });
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
res.setHeader("Content-Type", MIME_TYPE);
|
|
1006
|
+
res.setHeader(
|
|
1007
|
+
"Content-Disposition",
|
|
1008
|
+
`attachment; filename=\"${path.basename(p)}\"`,
|
|
1009
|
+
);
|
|
1010
|
+
return fs.createReadStream(p).pipe(res);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
877
1013
|
const app = express();
|
|
878
1014
|
app.set("trust proxy", true);
|
|
879
1015
|
|
|
880
1016
|
// x402 core server + HTTP wrapper
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
facilitatorConfig
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
new
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
// Route config for x402HTTPResourceServer
|
|
893
|
-
const routes = {
|
|
894
|
-
"GET /download": {
|
|
895
|
-
accepts: [
|
|
896
|
-
{
|
|
897
|
-
scheme: "exact",
|
|
898
|
-
price: `$${PRICE_USD}`,
|
|
899
|
-
network: CHAIN_ID,
|
|
900
|
-
payTo: SELLER_PAY_TO,
|
|
901
|
-
maxTimeoutSeconds: WINDOW_SECONDS,
|
|
902
|
-
},
|
|
903
|
-
],
|
|
904
|
-
description: ARTIFACT_NAME,
|
|
905
|
-
mimeType: MIME_TYPE,
|
|
906
|
-
unpaidResponseBody: async (context) => ({
|
|
907
|
-
contentType: "text/html; charset=utf-8",
|
|
908
|
-
body: renderUnpaidDownloadGuidancePage(context?.adapter?.getUrl?.()),
|
|
909
|
-
}),
|
|
910
|
-
},
|
|
911
|
-
};
|
|
912
|
-
|
|
913
|
-
const httpServer = new x402HTTPResourceServer(coreServer, routes);
|
|
914
|
-
try {
|
|
915
|
-
await httpServer.initialize();
|
|
916
|
-
} catch (err) {
|
|
917
|
-
console.error("[startup] Failed to initialize x402 route configuration.");
|
|
918
|
-
console.error(
|
|
919
|
-
`[startup] facilitator=${FACILITATOR_URL} mode=${FACILITATOR_MODE} network=${CHAIN_ID}`,
|
|
1017
|
+
let httpServer = null;
|
|
1018
|
+
if (REQUIRES_PAYMENT) {
|
|
1019
|
+
await preflightCdpAuth();
|
|
1020
|
+
const facilitatorConfig = { url: FACILITATOR_URL };
|
|
1021
|
+
if (FACILITATOR_MODE === "cdp_mainnet") {
|
|
1022
|
+
facilitatorConfig.createAuthHeaders = createCdpAuthHeadersFactory();
|
|
1023
|
+
}
|
|
1024
|
+
const facilitatorClient = new HTTPFacilitatorClient(facilitatorConfig);
|
|
1025
|
+
const coreServer = new x402ResourceServer(facilitatorClient).register(
|
|
1026
|
+
CHAIN_ID,
|
|
1027
|
+
new ExactEvmScheme(),
|
|
920
1028
|
);
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1029
|
+
|
|
1030
|
+
// Route config for x402HTTPResourceServer
|
|
1031
|
+
const routes = {
|
|
1032
|
+
"GET /download": {
|
|
1033
|
+
accepts: [
|
|
1034
|
+
{
|
|
1035
|
+
scheme: "exact",
|
|
1036
|
+
price: `$${PRICE_USD}`,
|
|
1037
|
+
network: CHAIN_ID,
|
|
1038
|
+
payTo: SELLER_PAY_TO,
|
|
1039
|
+
maxTimeoutSeconds: WINDOW_SECONDS,
|
|
1040
|
+
},
|
|
1041
|
+
],
|
|
1042
|
+
description: ARTIFACT_NAME,
|
|
1043
|
+
mimeType: MIME_TYPE,
|
|
1044
|
+
unpaidResponseBody: async (context) => ({
|
|
1045
|
+
contentType: "text/html; charset=utf-8",
|
|
1046
|
+
body: renderUnpaidDownloadGuidancePage(context?.adapter?.getUrl?.()),
|
|
1047
|
+
}),
|
|
1048
|
+
},
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
httpServer = new x402HTTPResourceServer(coreServer, routes);
|
|
1052
|
+
try {
|
|
1053
|
+
await httpServer.initialize();
|
|
1054
|
+
} catch (err) {
|
|
1055
|
+
console.error("[startup] Failed to initialize x402 route configuration.");
|
|
1056
|
+
console.error(
|
|
1057
|
+
`[startup] facilitator=${FACILITATOR_URL} mode=${FACILITATOR_MODE} network=${CHAIN_ID}`,
|
|
1058
|
+
);
|
|
1059
|
+
if (Array.isArray(err?.errors) && err.errors.length > 0) {
|
|
1060
|
+
for (const e of err.errors) {
|
|
1061
|
+
console.error(`[startup] ${e.message || JSON.stringify(e)}`);
|
|
1062
|
+
}
|
|
1063
|
+
} else {
|
|
1064
|
+
console.error(`[startup] ${err?.message || String(err)}`);
|
|
924
1065
|
}
|
|
925
|
-
|
|
926
|
-
|
|
1066
|
+
printFacilitatorHint(err);
|
|
1067
|
+
process.exit(1);
|
|
927
1068
|
}
|
|
928
|
-
printFacilitatorHint(err);
|
|
929
|
-
process.exit(1);
|
|
930
1069
|
}
|
|
931
1070
|
|
|
932
1071
|
setInterval(() => {
|
|
@@ -947,12 +1086,17 @@ app.get("/info", (req, res) => {
|
|
|
947
1086
|
artifact: path.basename(absArtifactPath()),
|
|
948
1087
|
price_usd: PRICE_USD,
|
|
949
1088
|
network: CHAIN_ID,
|
|
950
|
-
pay_to: SELLER_PAY_TO,
|
|
1089
|
+
pay_to: SELLER_PAY_TO || null,
|
|
951
1090
|
window_seconds: WINDOW_SECONDS,
|
|
952
1091
|
confirmation_policy: CONFIRMATION_POLICY,
|
|
953
1092
|
confirmations_required: CONFIRMATIONS_REQUIRED,
|
|
954
1093
|
facilitator_url: FACILITATOR_URL,
|
|
955
1094
|
facilitator_mode: FACILITATOR_MODE,
|
|
1095
|
+
access_mode: ACCESS_MODE,
|
|
1096
|
+
access_summary: accessModeSummary(ACCESS_MODE),
|
|
1097
|
+
payment_required: REQUIRES_PAYMENT,
|
|
1098
|
+
download_code_required: REQUIRES_DOWNLOAD_CODE,
|
|
1099
|
+
download_code_header: DOWNLOAD_CODE_HEADER,
|
|
956
1100
|
download_url: model.downloadUrl,
|
|
957
1101
|
promo_url: model.promoUrl,
|
|
958
1102
|
});
|
|
@@ -1095,13 +1239,18 @@ app.get("/.well-known/leak", (req, res) => {
|
|
|
1095
1239
|
install_command: SKILL_INSTALL_COMMAND,
|
|
1096
1240
|
},
|
|
1097
1241
|
resource: {
|
|
1098
|
-
type: "x402-gated-download",
|
|
1242
|
+
type: REQUIRES_PAYMENT ? "x402-gated-download" : "direct-download",
|
|
1099
1243
|
download_url: model.downloadUrl,
|
|
1100
1244
|
promo_url: model.promoUrl,
|
|
1101
1245
|
artifact_name: ARTIFACT_NAME,
|
|
1102
1246
|
price_usd: PRICE_USD,
|
|
1103
1247
|
price_currency: "USDC",
|
|
1104
1248
|
network: CHAIN_ID,
|
|
1249
|
+
access_mode: ACCESS_MODE,
|
|
1250
|
+
access_summary: accessModeSummary(ACCESS_MODE),
|
|
1251
|
+
payment_required: REQUIRES_PAYMENT,
|
|
1252
|
+
download_code_required: REQUIRES_DOWNLOAD_CODE,
|
|
1253
|
+
download_code_header: DOWNLOAD_CODE_HEADER,
|
|
1105
1254
|
sale_end: new Date(SALE_END_TS * 1000).toISOString(),
|
|
1106
1255
|
},
|
|
1107
1256
|
deprecation: LEGACY_DISCOVERY_DEPRECATION,
|
|
@@ -1110,7 +1259,7 @@ app.get("/.well-known/leak", (req, res) => {
|
|
|
1110
1259
|
});
|
|
1111
1260
|
});
|
|
1112
1261
|
|
|
1113
|
-
//
|
|
1262
|
+
// Access gate for GET /download (download-code check, then optional x402 payment).
|
|
1114
1263
|
app.use("/download", async (req, res, next) => {
|
|
1115
1264
|
if (saleEnded()) {
|
|
1116
1265
|
return res.status(410).json({ error: "leak ended" });
|
|
@@ -1122,10 +1271,30 @@ app.use("/download", async (req, res, next) => {
|
|
|
1122
1271
|
return next();
|
|
1123
1272
|
}
|
|
1124
1273
|
|
|
1274
|
+
const requestUrl = `${req.protocol}://${req.get("host")}${req.originalUrl}`;
|
|
1275
|
+
|
|
1276
|
+
if (REQUIRES_DOWNLOAD_CODE) {
|
|
1277
|
+
const submittedCode = String(req.get(DOWNLOAD_CODE_HEADER) || "").trim();
|
|
1278
|
+
if (!submittedCode) return sendDownloadCodeRequired(req, res, requestUrl);
|
|
1279
|
+
|
|
1280
|
+
let valid = false;
|
|
1281
|
+
try {
|
|
1282
|
+
valid = await verifyDownloadCode(submittedCode, DOWNLOAD_CODE_HASH);
|
|
1283
|
+
} catch (err) {
|
|
1284
|
+
console.error(
|
|
1285
|
+
`[download-code] verification failed: ${err?.message || String(err)}`,
|
|
1286
|
+
);
|
|
1287
|
+
return res.status(500).json({ error: "download code validation failed" });
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
if (!valid) return sendDownloadCodeRequired(req, res, requestUrl);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
if (!REQUIRES_PAYMENT) return next();
|
|
1294
|
+
|
|
1125
1295
|
// NOTE: because this middleware is mounted at "/download", Express strips the mount
|
|
1126
1296
|
// path and `req.path` becomes "/". x402 route matching needs the *full* path.
|
|
1127
1297
|
const fullPath = `${req.baseUrl || ""}${req.path || ""}`;
|
|
1128
|
-
const requestUrl = `${req.protocol}://${req.get("host")}${req.originalUrl}`;
|
|
1129
1298
|
|
|
1130
1299
|
const adapter = {
|
|
1131
1300
|
getHeader(name) {
|
|
@@ -1216,20 +1385,15 @@ app.get("/download", async (req, res) => {
|
|
|
1216
1385
|
if (token) {
|
|
1217
1386
|
const check = validateAndConsumeToken(token);
|
|
1218
1387
|
if (!check.ok) return res.status(403).json({ error: check.reason });
|
|
1388
|
+
return sendArtifactStream(res);
|
|
1389
|
+
}
|
|
1219
1390
|
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
res.setHeader("Content-Type", MIME_TYPE);
|
|
1225
|
-
res.setHeader(
|
|
1226
|
-
"Content-Disposition",
|
|
1227
|
-
`attachment; filename=\"${path.basename(p)}\"`,
|
|
1228
|
-
);
|
|
1229
|
-
return fs.createReadStream(p).pipe(res);
|
|
1391
|
+
// 2) No token.
|
|
1392
|
+
if (!REQUIRES_PAYMENT) {
|
|
1393
|
+
return sendArtifactStream(res);
|
|
1230
1394
|
}
|
|
1231
1395
|
|
|
1232
|
-
//
|
|
1396
|
+
// If we got here with payment enabled, payment has been verified by middleware.
|
|
1233
1397
|
// If you want immediate UX, just mint token. If you want stronger guarantees, settle.
|
|
1234
1398
|
if (CONFIRMATION_POLICY === "confirmed") {
|
|
1235
1399
|
let settle;
|
|
@@ -1278,13 +1442,25 @@ app.get("/download", async (req, res) => {
|
|
|
1278
1442
|
|
|
1279
1443
|
app.listen(PORT, () => {
|
|
1280
1444
|
console.log(`x402-node listening on http://localhost:${PORT}`);
|
|
1281
|
-
console.log(`
|
|
1282
|
-
|
|
1445
|
+
console.log(`access mode: ${ACCESS_MODE} (${accessModeSummary(ACCESS_MODE)})`);
|
|
1446
|
+
if (REQUIRES_PAYMENT) {
|
|
1447
|
+
console.log(`facilitator mode: ${FACILITATOR_MODE}`);
|
|
1448
|
+
console.log(`facilitator url: ${FACILITATOR_URL}`);
|
|
1449
|
+
}
|
|
1283
1450
|
console.log(`network: ${CHAIN_ID}`);
|
|
1451
|
+
if (REQUIRES_DOWNLOAD_CODE) {
|
|
1452
|
+
console.log(`download-code: required via header ${DOWNLOAD_CODE_HEADER}`);
|
|
1453
|
+
}
|
|
1284
1454
|
console.log(`promo: http://localhost:${PORT}/ (share this)`);
|
|
1285
1455
|
console.log(`info: http://localhost:${PORT}/info`);
|
|
1286
1456
|
console.log(`health: http://localhost:${PORT}/health`);
|
|
1287
|
-
|
|
1457
|
+
const protection = [
|
|
1458
|
+
REQUIRES_DOWNLOAD_CODE ? "download-code" : null,
|
|
1459
|
+
REQUIRES_PAYMENT ? "x402 payment" : null,
|
|
1460
|
+
].filter(Boolean);
|
|
1461
|
+
console.log(
|
|
1462
|
+
`download http://localhost:${PORT}/download (${protection.length > 0 ? protection.join(" + ") : "direct"})`,
|
|
1463
|
+
);
|
|
1288
1464
|
if (endedWindowActive()) {
|
|
1289
1465
|
console.log(
|
|
1290
1466
|
`ended-window active until ${new Date(endedWindowCutoffTs() * 1000).toISOString()} (download endpoints HTTP 410 mode)`,
|