leak-cli 2026.2.15-beta.0 → 2026.2.15
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/README.md +8 -0
- package/package.json +2 -2
- package/scripts/config.js +12 -1
- package/scripts/leak.js +13 -2
- package/src/chain_meta.js +55 -0
- package/src/index.js +146 -28
package/README.md
CHANGED
|
@@ -105,6 +105,14 @@ Security note: use a dedicated buyer key with limited funds.
|
|
|
105
105
|
|
|
106
106
|
Under the hood, the skill scripts try `leak` on PATH first and fall back to `npx -y leak-cli@2026.2.14` for one-off usage if needed.
|
|
107
107
|
|
|
108
|
+
Recommended first-time agent UX for unknown URLs:
|
|
109
|
+
- ask only for skill-install approval (`clawhub install leak`)
|
|
110
|
+
- ask for an existing buyer key file path (default mode)
|
|
111
|
+
- if user opts in and no key exists, create `./.leak/buyer.key` (hot wallet; fund minimally; user must back it up)
|
|
112
|
+
- if workspace is a git repo, add `./.leak/buyer.key` to `.gitignore` so it is not tracked
|
|
113
|
+
- run: `bash skills/leak/scripts/buy.sh "<promo_or_download_url>" --buyer-private-key-file <buyer_key_file_path>` (or `./.leak/buyer.key` for generated fallback)
|
|
114
|
+
- avoid protocol deep-dives unless the user explicitly asks for x402 internals
|
|
115
|
+
|
|
108
116
|
### Next: Mainnet checklist (optional)
|
|
109
117
|
|
|
110
118
|
Warning: switching only `CHAIN_ID` to mainnet is not sufficient.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leak-cli",
|
|
3
|
-
"version": "2026.2.15
|
|
3
|
+
"version": "2026.2.15",
|
|
4
4
|
"description": "Self-hosted pop-up stores for creators -- with agent-friendly automation built in",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"check:version-sync": "node scripts/check_version_sync.js",
|
|
29
29
|
"check:changelog-version": "node scripts/check_changelog_version.js",
|
|
30
30
|
"check:no-local-paths": "node scripts/check_no_local_paths.js",
|
|
31
|
-
"check:syntax": "node --check src/index.js && node --check scripts/cli.js && node --check scripts/leak.js && node --check scripts/buy.js && node --check scripts/config.js",
|
|
31
|
+
"check:syntax": "node --check src/index.js && node --check src/chain_meta.js && node --check scripts/cli.js && node --check scripts/leak.js && node --check scripts/buy.js && node --check scripts/config.js",
|
|
32
32
|
"check:smoke": "node scripts/cli.js --help && node scripts/leak.js --help && node scripts/buy.js --help && node scripts/config.js --help",
|
|
33
33
|
"check:release": "npm run check:version-sync && npm run check:syntax && npm run check:smoke && npm run check:no-local-paths && npm pack --dry-run --cache ./.npm-cache",
|
|
34
34
|
"release:beta": "npm publish --tag beta --provenance",
|
package/scripts/config.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
readConfig,
|
|
15
15
|
writeConfig,
|
|
16
16
|
} from "./config_store.js";
|
|
17
|
+
import { resolveSupportedChain } from "../src/chain_meta.js";
|
|
17
18
|
|
|
18
19
|
const ALLOWED_FACILITATOR_MODES = new Set(["testnet", "cdp_mainnet"]);
|
|
19
20
|
const ALLOWED_CONFIRMATION_POLICIES = new Set(["confirmed", "optimistic"]);
|
|
@@ -217,11 +218,21 @@ async function runWizard({ writeEnv }) {
|
|
|
217
218
|
sellerPayTo = await askWithDefault(rl, "SELLER_PAY_TO (seller payout address)", sellerPayTo);
|
|
218
219
|
}
|
|
219
220
|
|
|
220
|
-
|
|
221
|
+
let chainIdInput = await askWithDefault(
|
|
221
222
|
rl,
|
|
222
223
|
"CHAIN_ID",
|
|
223
224
|
existing.chainId || "eip155:84532",
|
|
224
225
|
);
|
|
226
|
+
let chainId;
|
|
227
|
+
while (true) {
|
|
228
|
+
try {
|
|
229
|
+
chainId = resolveSupportedChain(chainIdInput).caip2;
|
|
230
|
+
break;
|
|
231
|
+
} catch (err) {
|
|
232
|
+
console.error(err.message || String(err));
|
|
233
|
+
chainIdInput = await askWithDefault(rl, "CHAIN_ID", chainIdInput || "eip155:84532");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
225
236
|
|
|
226
237
|
let facilitatorMode = await askWithDefault(
|
|
227
238
|
rl,
|
package/scripts/leak.js
CHANGED
|
@@ -8,6 +8,7 @@ import { stdin as input, stdout as output } from "node:process";
|
|
|
8
8
|
import { spawn, spawnSync } from "node:child_process";
|
|
9
9
|
import { isAddress } from "viem";
|
|
10
10
|
import { defaultFacilitatorUrlForMode, readConfig } from "./config_store.js";
|
|
11
|
+
import { resolveSupportedChain } from "../src/chain_meta.js";
|
|
11
12
|
|
|
12
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
14
|
const __dirname = path.dirname(__filename);
|
|
@@ -330,7 +331,17 @@ async function main() {
|
|
|
330
331
|
process.exit(1);
|
|
331
332
|
}
|
|
332
333
|
|
|
333
|
-
const
|
|
334
|
+
const networkInput = args.network || process.env.CHAIN_ID || configDefaults.chainId || "eip155:84532";
|
|
335
|
+
let network;
|
|
336
|
+
let networkName;
|
|
337
|
+
try {
|
|
338
|
+
const networkMeta = resolveSupportedChain(networkInput);
|
|
339
|
+
network = networkMeta.caip2;
|
|
340
|
+
networkName = networkMeta.name;
|
|
341
|
+
} catch (err) {
|
|
342
|
+
console.error(err.message || String(err));
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
334
345
|
const port = Number(args.port || process.env.PORT || configDefaults.port || 4021);
|
|
335
346
|
const facilitatorMode = (
|
|
336
347
|
process.env.FACILITATOR_MODE || configDefaults.facilitatorMode || "testnet"
|
|
@@ -419,7 +430,7 @@ async function main() {
|
|
|
419
430
|
console.log(`- price: ${prompted.price} USDC`);
|
|
420
431
|
console.log(`- window: ${prompted.windowSeconds}s`);
|
|
421
432
|
console.log(`- to: ${payTo}`);
|
|
422
|
-
console.log(`- net: ${network}`);
|
|
433
|
+
console.log(`- net: ${network} (${networkName})`);
|
|
423
434
|
console.log(`- mode: ${confirmationPolicy}`);
|
|
424
435
|
console.log(`- facilitator_mode: ${facilitatorMode}`);
|
|
425
436
|
console.log(`- facilitator_url: ${facilitatorUrl}`);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { base, baseSepolia } from "viem/chains";
|
|
2
|
+
import { extractChain } from "viem/chains/utils";
|
|
3
|
+
|
|
4
|
+
const SUPPORTED_CHAINS = [base, baseSepolia];
|
|
5
|
+
|
|
6
|
+
const SUPPORTED_CHAIN_SUMMARY = [
|
|
7
|
+
`${base.id} (${base.name})`,
|
|
8
|
+
`${baseSepolia.id} (${baseSepolia.name})`,
|
|
9
|
+
].join(", ");
|
|
10
|
+
|
|
11
|
+
function invalidFormatMessage(raw) {
|
|
12
|
+
const value = String(raw ?? "").trim() || "<empty>";
|
|
13
|
+
return `Invalid chain identifier '${value}'. Expected eip155:<number>. Supported values: ${SUPPORTED_CHAIN_SUMMARY}.`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function unsupportedChainMessage(caip2) {
|
|
17
|
+
return `Unsupported chain '${caip2}'. Supported values: ${SUPPORTED_CHAIN_SUMMARY}.`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function parseCaip2Eip155(chainIdString) {
|
|
21
|
+
const value = String(chainIdString ?? "").trim();
|
|
22
|
+
const match = value.match(/^eip155:(\d+)$/);
|
|
23
|
+
if (!match) {
|
|
24
|
+
throw new Error(invalidFormatMessage(chainIdString));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const id = Number(match[1]);
|
|
28
|
+
if (!Number.isSafeInteger(id) || id <= 0) {
|
|
29
|
+
throw new Error(invalidFormatMessage(chainIdString));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
id,
|
|
34
|
+
caip2: `eip155:${id}`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function resolveSupportedChain(chainIdString) {
|
|
39
|
+
const parsed = parseCaip2Eip155(chainIdString);
|
|
40
|
+
const chain = extractChain({ chains: SUPPORTED_CHAINS, id: parsed.id });
|
|
41
|
+
if (!chain) {
|
|
42
|
+
throw new Error(unsupportedChainMessage(parsed.caip2));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
caip2: parsed.caip2,
|
|
47
|
+
id: parsed.id,
|
|
48
|
+
name: chain.name,
|
|
49
|
+
testnet: Boolean(chain.testnet),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function formatChainDisplayName(chainIdString) {
|
|
54
|
+
return resolveSupportedChain(chainIdString).name;
|
|
55
|
+
}
|
package/src/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { x402ResourceServer } from "@x402/core/server";
|
|
|
9
9
|
import { x402HTTPResourceServer, HTTPFacilitatorClient } from "@x402/core/http";
|
|
10
10
|
import { ExactEvmScheme } from "@x402/evm/exact/server";
|
|
11
11
|
import { isAddress } from "viem";
|
|
12
|
+
import { resolveSupportedChain } from "./chain_meta.js";
|
|
12
13
|
|
|
13
14
|
dotenv.config();
|
|
14
15
|
|
|
@@ -80,7 +81,7 @@ const FACILITATOR_URL = (
|
|
|
80
81
|
).trim();
|
|
81
82
|
const SELLER_PAY_TO = String(process.env.SELLER_PAY_TO || process.env.PAY_TO || "").trim();
|
|
82
83
|
const PRICE_USD = process.env.PRICE_USD || "1.00";
|
|
83
|
-
const
|
|
84
|
+
const RAW_CHAIN_ID = process.env.CHAIN_ID || process.env.NETWORK || "eip155:84532";
|
|
84
85
|
const ARTIFACT_PATH = process.env.ARTIFACT_PATH || process.env.PROTECTED_FILE;
|
|
85
86
|
const WINDOW_SECONDS = Number(process.env.WINDOW_SECONDS || 3600);
|
|
86
87
|
const MAX_GRANTS = parsePositiveInt(process.env.MAX_GRANTS, 10000);
|
|
@@ -110,7 +111,19 @@ const LEGACY_DISCOVERY_DEPRECATION =
|
|
|
110
111
|
const SALE_START_TS = parsePositiveInt(process.env.SALE_START_TS, now());
|
|
111
112
|
const SALE_END_TS = parsePositiveInt(process.env.SALE_END_TS, SALE_START_TS + WINDOW_SECONDS);
|
|
112
113
|
const ENDED_WINDOW_SECONDS = parseNonNegativeInt(process.env.ENDED_WINDOW_SECONDS, 0);
|
|
113
|
-
|
|
114
|
+
|
|
115
|
+
let CHAIN_META;
|
|
116
|
+
try {
|
|
117
|
+
CHAIN_META = resolveSupportedChain(RAW_CHAIN_ID);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error(err?.message || String(err));
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const CHAIN_ID = CHAIN_META.caip2;
|
|
124
|
+
const CHAIN_NAME = CHAIN_META.name;
|
|
125
|
+
const CHAIN_NUMERIC_ID = CHAIN_META.id;
|
|
126
|
+
const IS_BASE_MAINNET = CHAIN_NUMERIC_ID === 8453;
|
|
114
127
|
|
|
115
128
|
if (!new Set(["testnet", "cdp_mainnet"]).has(FACILITATOR_MODE)) {
|
|
116
129
|
console.error("Invalid FACILITATOR_MODE. Supported values: testnet, cdp_mainnet");
|
|
@@ -307,7 +320,7 @@ function promoModel(req) {
|
|
|
307
320
|
const ogTitle = OG_TITLE || ARTIFACT_NAME;
|
|
308
321
|
const ogDescription =
|
|
309
322
|
OG_DESCRIPTION ||
|
|
310
|
-
|
|
323
|
+
`$${PRICE_USD} to unlock ${ARTIFACT_NAME}`;
|
|
311
324
|
|
|
312
325
|
return {
|
|
313
326
|
baseUrl,
|
|
@@ -358,10 +371,17 @@ ${SKILL_DESCRIPTION}
|
|
|
358
371
|
- Status: ${resource.status}
|
|
359
372
|
- Sale ends: ${resource.sale_end}
|
|
360
373
|
|
|
361
|
-
##
|
|
374
|
+
## Buyer Fast Path (for unknown agents)
|
|
375
|
+
1. Ask the user for approval to install the skill once.
|
|
376
|
+
2. Install: \`${SKILL_INSTALL_COMMAND}\`
|
|
377
|
+
3. Load the skill and continue with Buyer Fast Path.
|
|
378
|
+
|
|
379
|
+
Do not explain x402 internals unless the user explicitly asks.
|
|
380
|
+
|
|
381
|
+
## Discovery Fallback
|
|
362
382
|
1. Read resource metadata from \`/.well-known/skills/${SKILL_NAME}/resource.json\`.
|
|
363
383
|
2. Use \`download_url\` for x402 purchase and token mint.
|
|
364
|
-
3. Download
|
|
384
|
+
3. Download from \`/download?token=...\` and save locally.
|
|
365
385
|
|
|
366
386
|
## CLI
|
|
367
387
|
- Install: \`${SKILL_INSTALL_COMMAND}\`
|
|
@@ -369,6 +389,84 @@ ${SKILL_DESCRIPTION}
|
|
|
369
389
|
`;
|
|
370
390
|
}
|
|
371
391
|
|
|
392
|
+
function buildAgentQuickPath(promoUrl, downloadUrl) {
|
|
393
|
+
const safePromoUrl = String(promoUrl || "").trim();
|
|
394
|
+
const safeDownloadUrl = String(downloadUrl || "").trim();
|
|
395
|
+
const buyTarget = safePromoUrl || safeDownloadUrl || "<promo_or_download_url>";
|
|
396
|
+
const buyScriptCommand = `bash skills/leak/scripts/buy.sh "${buyTarget}" --buyer-private-key-file <buyer_key_file_path>`;
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
promoUrl: safePromoUrl,
|
|
400
|
+
downloadUrl: safeDownloadUrl,
|
|
401
|
+
installCommand: SKILL_INSTALL_COMMAND,
|
|
402
|
+
buyTarget,
|
|
403
|
+
buyScriptCommand,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function urlsForQuickPathFromRequestUrl(requestUrl) {
|
|
408
|
+
try {
|
|
409
|
+
const parsed = new URL(String(requestUrl || ""));
|
|
410
|
+
const origin = parsed.origin;
|
|
411
|
+
return {
|
|
412
|
+
promoUrl: `${origin}/`,
|
|
413
|
+
downloadUrl: `${origin}/download`,
|
|
414
|
+
};
|
|
415
|
+
} catch {
|
|
416
|
+
const fallbackBase = isAbsoluteHttpUrl(PUBLIC_BASE_URL)
|
|
417
|
+
? PUBLIC_BASE_URL.replace(/\/+$/, "")
|
|
418
|
+
: `http://127.0.0.1:${PORT}`;
|
|
419
|
+
return {
|
|
420
|
+
promoUrl: `${fallbackBase}/`,
|
|
421
|
+
downloadUrl: `${fallbackBase}/download`,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function renderAgentQuickPathHtmlBlock(fastPath) {
|
|
427
|
+
return `<section class="agent-quick-path">
|
|
428
|
+
<h2>Agent Quick Path</h2>
|
|
429
|
+
<ol>
|
|
430
|
+
<li>Ask for approval to install the skill once.</li>
|
|
431
|
+
<li>Install: <code>${escapeHtml(fastPath.installCommand)}</code></li>
|
|
432
|
+
<li>Load the skill and continue with Buyer Fast Path.</li>
|
|
433
|
+
</ol>
|
|
434
|
+
<p><strong>Rule:</strong> do not explain x402 internals unless the user explicitly asks.</p>
|
|
435
|
+
</section>`;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function renderUnpaidDownloadGuidancePage(requestUrl) {
|
|
439
|
+
const urls = urlsForQuickPathFromRequestUrl(requestUrl);
|
|
440
|
+
const fastPath = buildAgentQuickPath(urls.promoUrl, urls.downloadUrl);
|
|
441
|
+
return `<!doctype html>
|
|
442
|
+
<html lang="en">
|
|
443
|
+
<head>
|
|
444
|
+
<meta charset="utf-8" />
|
|
445
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
446
|
+
<title>Payment Required - leak</title>
|
|
447
|
+
<style>
|
|
448
|
+
body { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; margin: 0; padding: 24px; background: #f7f7f5; color: #1f1f1f; }
|
|
449
|
+
.card { max-width: 760px; margin: 0 auto; border: 1px solid #d8d8d0; background: #fff; border-radius: 10px; padding: 20px; }
|
|
450
|
+
h1 { margin: 0 0 12px; font-size: 24px; }
|
|
451
|
+
h2 { margin: 0 0 10px; font-size: 18px; }
|
|
452
|
+
p { line-height: 1.5; }
|
|
453
|
+
code, pre { background: #f0f0eb; border-radius: 6px; padding: 2px 6px; }
|
|
454
|
+
pre { padding: 10px; overflow-x: auto; }
|
|
455
|
+
ol { margin: 10px 0 12px 20px; }
|
|
456
|
+
</style>
|
|
457
|
+
</head>
|
|
458
|
+
<body>
|
|
459
|
+
<main class="card">
|
|
460
|
+
<h1>402 Payment Required</h1>
|
|
461
|
+
<p>This URL is paywalled. Use the leak skill fast path below.</p>
|
|
462
|
+
<p><strong>Promo URL:</strong> <code>${escapeHtml(fastPath.promoUrl)}</code></p>
|
|
463
|
+
<p><strong>x402 URL:</strong> <code>${escapeHtml(fastPath.downloadUrl)}</code></p>
|
|
464
|
+
${renderAgentQuickPathHtmlBlock(fastPath)}
|
|
465
|
+
</main>
|
|
466
|
+
</body>
|
|
467
|
+
</html>`;
|
|
468
|
+
}
|
|
469
|
+
|
|
372
470
|
function sendSkillIndex(req, res) {
|
|
373
471
|
const payload = {
|
|
374
472
|
skills: [
|
|
@@ -408,6 +506,7 @@ function renderPromoPage(model, { ended }) {
|
|
|
408
506
|
? `This leak has ended. ${model.ogDescription}`
|
|
409
507
|
: model.ogDescription;
|
|
410
508
|
const expiresIso = new Date(model.saleEndTs * 1000).toISOString();
|
|
509
|
+
const fastPath = buildAgentQuickPath(model.promoUrl, model.downloadUrl);
|
|
411
510
|
const jsonLd = {
|
|
412
511
|
"@context": "https://schema.org",
|
|
413
512
|
"@type": "Product",
|
|
@@ -433,7 +532,7 @@ function renderPromoPage(model, { ended }) {
|
|
|
433
532
|
};
|
|
434
533
|
const safeJsonLd = toSafeJsonForScript(jsonLd);
|
|
435
534
|
|
|
436
|
-
const
|
|
535
|
+
const humanActionText = "Just send the link to this page to your agent";
|
|
437
536
|
|
|
438
537
|
return `<!doctype html>
|
|
439
538
|
<html lang="en">
|
|
@@ -472,53 +571,68 @@ function renderPromoPage(model, { ended }) {
|
|
|
472
571
|
.copy-status { font-size: 12px; color: #3f3f3f; min-height: 1em; }
|
|
473
572
|
.install-note { margin-top: 16px; font-size: 13px; color: #2f2f2f; }
|
|
474
573
|
.install-note a { color: #1f1f1f; }
|
|
574
|
+
.agent-quick-path { margin: 16px 0; padding: 14px; border: 1px solid #d8d8d0; border-radius: 8px; background: #fafaf6; }
|
|
575
|
+
.agent-quick-path h2 { margin: 0 0 8px; font-size: 18px; }
|
|
576
|
+
.agent-quick-path ol { margin: 8px 0 8px 20px; }
|
|
577
|
+
.agent-quick-path p { margin: 8px 0 0; }
|
|
475
578
|
</style>
|
|
476
579
|
</head>
|
|
477
580
|
<body>
|
|
478
581
|
<main class="card">
|
|
479
582
|
<div class="state">${escapeHtml(stateLabel)}</div>
|
|
480
583
|
<h1>${escapeHtml(pageTitle)}</h1>
|
|
481
|
-
<p>${escapeHtml(description)}</p>
|
|
482
|
-
<p><strong>Agent-assisted purchase:</strong> this release is designed to be bought through an agent using the x402 endpoint below.</p>
|
|
483
584
|
|
|
484
585
|
<div class="kv"><strong>Price:</strong> ${escapeHtml(PRICE_USD)} USD equivalent</div>
|
|
485
|
-
<div class="kv"><strong>Network:</strong> ${escapeHtml(CHAIN_ID)}</div>
|
|
486
|
-
<div class="kv"><strong>Sale end:</strong> ${escapeHtml(expiresIso)}</div>
|
|
586
|
+
<div class="kv"><strong>Network:</strong> ${escapeHtml(CHAIN_NAME)} (${escapeHtml(CHAIN_ID)})</div>
|
|
587
|
+
<div class="kv"><strong>Sale end:</strong> <span id="sale-end-local" data-sale-end-iso="${escapeHtml(expiresIso)}">${escapeHtml(expiresIso)}</span></div>
|
|
588
|
+
${renderAgentQuickPathHtmlBlock(fastPath)}
|
|
487
589
|
|
|
488
|
-
<p><strong>x402 URL</strong><br /><code>${escapeHtml(model.downloadUrl)}</code></p>
|
|
489
590
|
<div class="prompt-head">
|
|
490
|
-
<p><strong>
|
|
491
|
-
<button class="copy-btn" id="copy-
|
|
492
|
-
<span class="copy-status" id="copy-
|
|
591
|
+
<p><strong>Human action</strong></p>
|
|
592
|
+
<button class="copy-btn" id="copy-link-btn" type="button" aria-label="Copy page link">Copy link</button>
|
|
593
|
+
<span class="copy-status" id="copy-link-status" aria-live="polite"></span>
|
|
493
594
|
</div>
|
|
494
|
-
<pre id="
|
|
595
|
+
<pre id="human-action-text">${escapeHtml(humanActionText)}</pre>
|
|
495
596
|
<p class="install-note">
|
|
496
|
-
|
|
597
|
+
Want to know more about <code>leak</code>? Visit
|
|
497
598
|
<a href="https://github.com/eucalyptus-viminalis/leak">github.com/eucalyptus-viminalis/leak</a>
|
|
498
599
|
or search for leak on clawhub.
|
|
499
600
|
</p>
|
|
500
601
|
</main>
|
|
501
602
|
<script>
|
|
502
603
|
(() => {
|
|
503
|
-
const button = document.getElementById("copy-
|
|
504
|
-
const
|
|
505
|
-
const
|
|
506
|
-
|
|
604
|
+
const button = document.getElementById("copy-link-btn");
|
|
605
|
+
const status = document.getElementById("copy-link-status");
|
|
606
|
+
const promoUrl = ${toSafeJsonForScript(model.promoUrl)};
|
|
607
|
+
const saleEndLocal = document.getElementById("sale-end-local");
|
|
608
|
+
|
|
609
|
+
if (saleEndLocal) {
|
|
610
|
+
const saleEndIso = saleEndLocal.getAttribute("data-sale-end-iso") || "";
|
|
611
|
+
const saleEndDate = new Date(saleEndIso);
|
|
612
|
+
if (!Number.isNaN(saleEndDate.getTime())) {
|
|
613
|
+
try {
|
|
614
|
+
const formatter = new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "medium" });
|
|
615
|
+
saleEndLocal.textContent = formatter.format(saleEndDate) + " (local time)";
|
|
616
|
+
} catch {
|
|
617
|
+
saleEndLocal.textContent = saleEndDate.toLocaleString() + " (local time)";
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (!button || !promoUrl) return;
|
|
507
623
|
|
|
508
624
|
const setStatus = (text) => {
|
|
509
625
|
if (status) status.textContent = text;
|
|
510
626
|
};
|
|
511
627
|
|
|
512
628
|
button.addEventListener("click", async () => {
|
|
513
|
-
const
|
|
514
|
-
if (!text) return;
|
|
515
|
-
const original = "Copy prompt";
|
|
629
|
+
const original = "Copy link";
|
|
516
630
|
try {
|
|
517
631
|
if (navigator.clipboard?.writeText) {
|
|
518
|
-
await navigator.clipboard.writeText(
|
|
632
|
+
await navigator.clipboard.writeText(promoUrl);
|
|
519
633
|
} else {
|
|
520
634
|
const ta = document.createElement("textarea");
|
|
521
|
-
ta.value =
|
|
635
|
+
ta.value = promoUrl;
|
|
522
636
|
ta.setAttribute("readonly", "");
|
|
523
637
|
ta.style.position = "absolute";
|
|
524
638
|
ta.style.left = "-9999px";
|
|
@@ -546,7 +660,7 @@ function renderPromoPage(model, { ended }) {
|
|
|
546
660
|
function renderOgSvg(req) {
|
|
547
661
|
const model = promoModel(req);
|
|
548
662
|
const title = model.ogTitle;
|
|
549
|
-
const subtitle =
|
|
663
|
+
const subtitle = `$${PRICE_USD} on ${CHAIN_NAME}`;
|
|
550
664
|
const status = saleEnded() ? "ENDED" : "LIVE";
|
|
551
665
|
|
|
552
666
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -559,10 +673,10 @@ function renderOgSvg(req) {
|
|
|
559
673
|
</defs>
|
|
560
674
|
<rect width="1200" height="630" fill="url(#bg)"/>
|
|
561
675
|
<rect x="64" y="64" width="1072" height="502" rx="18" fill="#ffffff" stroke="#bdbdae"/>
|
|
562
|
-
<text x="96" y="170" font-size="32" font-family="monospace" fill="#222">${escapeXml(status)}
|
|
676
|
+
<text x="96" y="170" font-size="32" font-family="monospace" fill="#222">${escapeXml(status)}</text>
|
|
563
677
|
<text x="96" y="250" font-size="52" font-family="monospace" fill="#111">${escapeXml(title)}</text>
|
|
564
678
|
<text x="96" y="330" font-size="30" font-family="monospace" fill="#333">${escapeXml(subtitle)}</text>
|
|
565
|
-
<text x="96" y="404" font-size="22" font-family="monospace" fill="#444">
|
|
679
|
+
<text x="96" y="404" font-size="22" font-family="monospace" fill="#444">Share this link with your OpenClaw agent to download</text>
|
|
566
680
|
</svg>`;
|
|
567
681
|
}
|
|
568
682
|
|
|
@@ -637,6 +751,10 @@ const routes = {
|
|
|
637
751
|
],
|
|
638
752
|
description: ARTIFACT_NAME,
|
|
639
753
|
mimeType: MIME_TYPE,
|
|
754
|
+
unpaidResponseBody: async (context) => ({
|
|
755
|
+
contentType: "text/html; charset=utf-8",
|
|
756
|
+
body: renderUnpaidDownloadGuidancePage(context?.adapter?.getUrl?.()),
|
|
757
|
+
}),
|
|
640
758
|
},
|
|
641
759
|
};
|
|
642
760
|
|