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/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;
|
|
@@ -115,11 +132,11 @@ const OG_IMAGE_PATH = OG_IMAGE_PATH_RAW
|
|
|
115
132
|
const OG_IMAGE_CACHE_CONTROL = "public, max-age=60";
|
|
116
133
|
const OG_IMAGE_WIDTH = 1200;
|
|
117
134
|
const OG_IMAGE_HEIGHT = 630;
|
|
118
|
-
const SKILL_NAME = "leak";
|
|
135
|
+
const SKILL_NAME = "leak-buy";
|
|
119
136
|
const SKILL_DESCRIPTION =
|
|
120
|
-
"
|
|
137
|
+
"Buy and download leak content from promo or download links using the leak CLI tool";
|
|
121
138
|
const SKILL_SOURCE = "clawhub";
|
|
122
|
-
const SKILL_INSTALL_COMMAND = "clawhub install leak";
|
|
139
|
+
const SKILL_INSTALL_COMMAND = "clawhub install leak-buy";
|
|
123
140
|
const WELL_KNOWN_CACHE_CONTROL = "public, max-age=60";
|
|
124
141
|
const LEGACY_DISCOVERY_DEPRECATION =
|
|
125
142
|
"Deprecated endpoint; use /.well-known/skills/index.json for RFC-compatible discovery.";
|
|
@@ -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
|
|
|
@@ -466,14 +527,12 @@ function buildAgentQuickPath(promoUrl, downloadUrl) {
|
|
|
466
527
|
const safeDownloadUrl = String(downloadUrl || "").trim();
|
|
467
528
|
const buyTarget =
|
|
468
529
|
safePromoUrl || safeDownloadUrl || "<promo_or_download_url>";
|
|
469
|
-
const buyScriptCommand = `bash skills/leak/scripts/buy.sh "${buyTarget}" --buyer-private-key-file <buyer_key_file_path>`;
|
|
470
530
|
|
|
471
531
|
return {
|
|
472
532
|
promoUrl: safePromoUrl,
|
|
473
533
|
downloadUrl: safeDownloadUrl,
|
|
474
534
|
installCommand: SKILL_INSTALL_COMMAND,
|
|
475
535
|
buyTarget,
|
|
476
|
-
buyScriptCommand,
|
|
477
536
|
};
|
|
478
537
|
}
|
|
479
538
|
|
|
@@ -497,6 +556,9 @@ function urlsForQuickPathFromRequestUrl(requestUrl) {
|
|
|
497
556
|
}
|
|
498
557
|
|
|
499
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.";
|
|
500
562
|
return `<section class="agent-quick-path">
|
|
501
563
|
<h2>Agent Quick Path</h2>
|
|
502
564
|
<ol>
|
|
@@ -504,7 +566,7 @@ function renderAgentQuickPathHtmlBlock(fastPath) {
|
|
|
504
566
|
<li>Install: <code>${escapeHtml(fastPath.installCommand)}</code></li>
|
|
505
567
|
<li>Load the skill and continue with Buyer Fast Path.</li>
|
|
506
568
|
</ol>
|
|
507
|
-
<p><strong>Rule:</strong>
|
|
569
|
+
<p><strong>Rule:</strong> ${escapeHtml(ruleText)}</p>
|
|
508
570
|
</section>`;
|
|
509
571
|
}
|
|
510
572
|
|
|
@@ -516,6 +578,8 @@ function renderPromoSharedContent({ promoUrl, downloadUrl, saleEndTs }) {
|
|
|
516
578
|
return `
|
|
517
579
|
<div class="kv"><strong>Price:</strong> ${escapeHtml(PRICE_USD)} USD equivalent</div>
|
|
518
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>
|
|
519
583
|
<div class="kv"><strong>Sale end:</strong> <span id="sale-end-local" data-sale-end-iso="${escapeHtml(expiresIso)}">${escapeHtml(expiresIso)}</span></div>
|
|
520
584
|
${renderAgentQuickPathHtmlBlock(fastPath)}
|
|
521
585
|
|
|
@@ -529,6 +593,7 @@ function renderPromoSharedContent({ promoUrl, downloadUrl, saleEndTs }) {
|
|
|
529
593
|
Want to know more about <code>leak</code>? Visit
|
|
530
594
|
<a href="https://github.com/eucalyptus-viminalis/leak">github.com/eucalyptus-viminalis/leak</a>
|
|
531
595
|
or search for leak on clawhub.
|
|
596
|
+
Want to publish your own content? Install the <code>leak-publish</code> skill.
|
|
532
597
|
</p>
|
|
533
598
|
`;
|
|
534
599
|
}
|
|
@@ -638,6 +703,62 @@ function renderUnpaidDownloadGuidancePage(requestUrl) {
|
|
|
638
703
|
</html>`;
|
|
639
704
|
}
|
|
640
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
|
+
|
|
641
762
|
function sendSkillIndex(req, res) {
|
|
642
763
|
const payload = {
|
|
643
764
|
skills: [
|
|
@@ -875,59 +996,76 @@ function validateAndConsumeToken(token) {
|
|
|
875
996
|
return { ok: true };
|
|
876
997
|
}
|
|
877
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
|
+
|
|
878
1013
|
const app = express();
|
|
879
1014
|
app.set("trust proxy", true);
|
|
880
1015
|
|
|
881
1016
|
// x402 core server + HTTP wrapper
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
facilitatorConfig
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
new
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
// Route config for x402HTTPResourceServer
|
|
894
|
-
const routes = {
|
|
895
|
-
"GET /download": {
|
|
896
|
-
accepts: [
|
|
897
|
-
{
|
|
898
|
-
scheme: "exact",
|
|
899
|
-
price: `$${PRICE_USD}`,
|
|
900
|
-
network: CHAIN_ID,
|
|
901
|
-
payTo: SELLER_PAY_TO,
|
|
902
|
-
maxTimeoutSeconds: WINDOW_SECONDS,
|
|
903
|
-
},
|
|
904
|
-
],
|
|
905
|
-
description: ARTIFACT_NAME,
|
|
906
|
-
mimeType: MIME_TYPE,
|
|
907
|
-
unpaidResponseBody: async (context) => ({
|
|
908
|
-
contentType: "text/html; charset=utf-8",
|
|
909
|
-
body: renderUnpaidDownloadGuidancePage(context?.adapter?.getUrl?.()),
|
|
910
|
-
}),
|
|
911
|
-
},
|
|
912
|
-
};
|
|
913
|
-
|
|
914
|
-
const httpServer = new x402HTTPResourceServer(coreServer, routes);
|
|
915
|
-
try {
|
|
916
|
-
await httpServer.initialize();
|
|
917
|
-
} catch (err) {
|
|
918
|
-
console.error("[startup] Failed to initialize x402 route configuration.");
|
|
919
|
-
console.error(
|
|
920
|
-
`[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(),
|
|
921
1028
|
);
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
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)}`);
|
|
925
1065
|
}
|
|
926
|
-
|
|
927
|
-
|
|
1066
|
+
printFacilitatorHint(err);
|
|
1067
|
+
process.exit(1);
|
|
928
1068
|
}
|
|
929
|
-
printFacilitatorHint(err);
|
|
930
|
-
process.exit(1);
|
|
931
1069
|
}
|
|
932
1070
|
|
|
933
1071
|
setInterval(() => {
|
|
@@ -948,12 +1086,17 @@ app.get("/info", (req, res) => {
|
|
|
948
1086
|
artifact: path.basename(absArtifactPath()),
|
|
949
1087
|
price_usd: PRICE_USD,
|
|
950
1088
|
network: CHAIN_ID,
|
|
951
|
-
pay_to: SELLER_PAY_TO,
|
|
1089
|
+
pay_to: SELLER_PAY_TO || null,
|
|
952
1090
|
window_seconds: WINDOW_SECONDS,
|
|
953
1091
|
confirmation_policy: CONFIRMATION_POLICY,
|
|
954
1092
|
confirmations_required: CONFIRMATIONS_REQUIRED,
|
|
955
1093
|
facilitator_url: FACILITATOR_URL,
|
|
956
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,
|
|
957
1100
|
download_url: model.downloadUrl,
|
|
958
1101
|
promo_url: model.promoUrl,
|
|
959
1102
|
});
|
|
@@ -1081,7 +1224,7 @@ app.get("/.well-known/leak", (req, res) => {
|
|
|
1081
1224
|
install_command: SKILL_INSTALL_COMMAND,
|
|
1082
1225
|
},
|
|
1083
1226
|
message:
|
|
1084
|
-
"This leak has expired, but you can install the leak skill for future purchases",
|
|
1227
|
+
"This leak has expired, but you can install the leak-buy skill for future purchases",
|
|
1085
1228
|
deprecation: LEGACY_DISCOVERY_DEPRECATION,
|
|
1086
1229
|
discovery_index_url: discoveryPath,
|
|
1087
1230
|
rfc_resource_url: rfcResourcePath,
|
|
@@ -1096,13 +1239,18 @@ app.get("/.well-known/leak", (req, res) => {
|
|
|
1096
1239
|
install_command: SKILL_INSTALL_COMMAND,
|
|
1097
1240
|
},
|
|
1098
1241
|
resource: {
|
|
1099
|
-
type: "x402-gated-download",
|
|
1242
|
+
type: REQUIRES_PAYMENT ? "x402-gated-download" : "direct-download",
|
|
1100
1243
|
download_url: model.downloadUrl,
|
|
1101
1244
|
promo_url: model.promoUrl,
|
|
1102
1245
|
artifact_name: ARTIFACT_NAME,
|
|
1103
1246
|
price_usd: PRICE_USD,
|
|
1104
1247
|
price_currency: "USDC",
|
|
1105
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,
|
|
1106
1254
|
sale_end: new Date(SALE_END_TS * 1000).toISOString(),
|
|
1107
1255
|
},
|
|
1108
1256
|
deprecation: LEGACY_DISCOVERY_DEPRECATION,
|
|
@@ -1111,7 +1259,7 @@ app.get("/.well-known/leak", (req, res) => {
|
|
|
1111
1259
|
});
|
|
1112
1260
|
});
|
|
1113
1261
|
|
|
1114
|
-
//
|
|
1262
|
+
// Access gate for GET /download (download-code check, then optional x402 payment).
|
|
1115
1263
|
app.use("/download", async (req, res, next) => {
|
|
1116
1264
|
if (saleEnded()) {
|
|
1117
1265
|
return res.status(410).json({ error: "leak ended" });
|
|
@@ -1123,10 +1271,30 @@ app.use("/download", async (req, res, next) => {
|
|
|
1123
1271
|
return next();
|
|
1124
1272
|
}
|
|
1125
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
|
+
|
|
1126
1295
|
// NOTE: because this middleware is mounted at "/download", Express strips the mount
|
|
1127
1296
|
// path and `req.path` becomes "/". x402 route matching needs the *full* path.
|
|
1128
1297
|
const fullPath = `${req.baseUrl || ""}${req.path || ""}`;
|
|
1129
|
-
const requestUrl = `${req.protocol}://${req.get("host")}${req.originalUrl}`;
|
|
1130
1298
|
|
|
1131
1299
|
const adapter = {
|
|
1132
1300
|
getHeader(name) {
|
|
@@ -1217,20 +1385,15 @@ app.get("/download", async (req, res) => {
|
|
|
1217
1385
|
if (token) {
|
|
1218
1386
|
const check = validateAndConsumeToken(token);
|
|
1219
1387
|
if (!check.ok) return res.status(403).json({ error: check.reason });
|
|
1388
|
+
return sendArtifactStream(res);
|
|
1389
|
+
}
|
|
1220
1390
|
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
res.setHeader("Content-Type", MIME_TYPE);
|
|
1226
|
-
res.setHeader(
|
|
1227
|
-
"Content-Disposition",
|
|
1228
|
-
`attachment; filename=\"${path.basename(p)}\"`,
|
|
1229
|
-
);
|
|
1230
|
-
return fs.createReadStream(p).pipe(res);
|
|
1391
|
+
// 2) No token.
|
|
1392
|
+
if (!REQUIRES_PAYMENT) {
|
|
1393
|
+
return sendArtifactStream(res);
|
|
1231
1394
|
}
|
|
1232
1395
|
|
|
1233
|
-
//
|
|
1396
|
+
// If we got here with payment enabled, payment has been verified by middleware.
|
|
1234
1397
|
// If you want immediate UX, just mint token. If you want stronger guarantees, settle.
|
|
1235
1398
|
if (CONFIRMATION_POLICY === "confirmed") {
|
|
1236
1399
|
let settle;
|
|
@@ -1279,13 +1442,25 @@ app.get("/download", async (req, res) => {
|
|
|
1279
1442
|
|
|
1280
1443
|
app.listen(PORT, () => {
|
|
1281
1444
|
console.log(`x402-node listening on http://localhost:${PORT}`);
|
|
1282
|
-
console.log(`
|
|
1283
|
-
|
|
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
|
+
}
|
|
1284
1450
|
console.log(`network: ${CHAIN_ID}`);
|
|
1451
|
+
if (REQUIRES_DOWNLOAD_CODE) {
|
|
1452
|
+
console.log(`download-code: required via header ${DOWNLOAD_CODE_HEADER}`);
|
|
1453
|
+
}
|
|
1285
1454
|
console.log(`promo: http://localhost:${PORT}/ (share this)`);
|
|
1286
1455
|
console.log(`info: http://localhost:${PORT}/info`);
|
|
1287
1456
|
console.log(`health: http://localhost:${PORT}/health`);
|
|
1288
|
-
|
|
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
|
+
);
|
|
1289
1464
|
if (endedWindowActive()) {
|
|
1290
1465
|
console.log(
|
|
1291
1466
|
`ended-window active until ${new Date(endedWindowCutoffTs() * 1000).toISOString()} (download endpoints HTTP 410 mode)`,
|