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/scripts/config.js CHANGED
@@ -15,17 +15,46 @@ import {
15
15
  writeConfig,
16
16
  } from "./config_store.js";
17
17
  import { resolveSupportedChain } from "../src/chain_meta.js";
18
+ import {
19
+ ACCESS_MODE_VALUES,
20
+ DEFAULT_ACCESS_MODE,
21
+ accessModeRequiresDownloadCode,
22
+ accessModeRequiresPayment,
23
+ isValidAccessMode,
24
+ } from "../src/access_mode.js";
25
+ import { hashDownloadCode } from "../src/download_code.js";
26
+ import { createUi } from "./ui.js";
18
27
 
19
28
  const ALLOWED_FACILITATOR_MODES = new Set(["testnet", "cdp_mainnet"]);
20
29
  const ALLOWED_CONFIRMATION_POLICIES = new Set(["confirmed", "optimistic"]);
30
+ const outUi = createUi(output);
31
+ const errUi = createUi(process.stderr);
32
+
33
+ function logInfo(message) {
34
+ console.log(outUi.statusLine("info", message));
35
+ }
36
+
37
+ function logOk(message) {
38
+ console.log(outUi.statusLine("ok", message));
39
+ }
40
+
41
+ function logWarn(message) {
42
+ console.error(errUi.statusLine("warn", message));
43
+ }
44
+
45
+ function logError(message) {
46
+ console.error(errUi.statusLine("error", message));
47
+ }
21
48
 
22
49
  function usageAndExit(code = 0) {
23
- console.log("Usage:");
50
+ console.log(outUi.heading("Leak Config CLI"));
51
+ console.log("");
52
+ console.log(outUi.section("Usage"));
24
53
  console.log(" leak config");
25
54
  console.log(" leak config show");
26
55
  console.log(" leak config --write-env");
27
56
  console.log("");
28
- console.log("Notes:");
57
+ console.log(outUi.section("Notes"));
29
58
  console.log(" - Stores defaults in ~/.leak/config.json");
30
59
  console.log(" - `--write-env` writes a project .env scaffold in the current directory");
31
60
  process.exit(code);
@@ -123,19 +152,48 @@ async function askOgField(rl, label, currentValue, fieldType) {
123
152
  }
124
153
  }
125
154
 
155
+ async function askDownloadCodeHash(rl, existingHash, accessMode) {
156
+ if (!accessModeRequiresDownloadCode(accessMode)) return "";
157
+
158
+ if (existingHash) {
159
+ const prompt = new Select({
160
+ name: "DOWNLOAD_CODE",
161
+ message: "DOWNLOAD_CODE (required by selected ACCESS_MODE)",
162
+ choices: [
163
+ { name: "keep", message: "Keep existing stored download code hash" },
164
+ { name: "replace", message: "Replace with new download code" },
165
+ ],
166
+ initial: 0,
167
+ });
168
+ const choice = await prompt.run();
169
+ if (choice === "keep") return existingHash;
170
+ }
171
+
172
+ let raw = (await rl.question("DOWNLOAD_CODE (input visible): ")).trim();
173
+ while (!raw) {
174
+ logError("DOWNLOAD_CODE is required for the selected ACCESS_MODE.");
175
+ raw = (await rl.question("DOWNLOAD_CODE (input visible): ")).trim();
176
+ }
177
+ return hashDownloadCode(raw);
178
+ }
179
+
126
180
  function printShow() {
127
181
  const loaded = readConfig();
128
182
  if (loaded.error) {
129
- console.error(`[config] warning: ${loaded.error}`);
183
+ logWarn(loaded.error);
130
184
  }
131
185
  if (!loaded.exists) {
132
- console.log(`No leak config found at ${loaded.path}`);
133
- console.log("Run `leak config` to initialize your config file.");
186
+ logInfo(`No leak config found at ${loaded.path}`);
187
+ logInfo("Run `leak config` to initialize your config file.");
134
188
  return;
135
189
  }
136
190
 
137
191
  const redacted = redactConfig(loaded.config);
138
- console.log(`Leak config path: ${loaded.path}`);
192
+ console.log(outUi.section("Leak Config"));
193
+ for (const line of outUi.formatRows([{ key: "path", value: loaded.path }])) {
194
+ console.log(line);
195
+ }
196
+ console.log("");
139
197
  console.log(JSON.stringify(redacted, null, 2));
140
198
  }
141
199
 
@@ -158,6 +216,10 @@ function buildEnvScaffold(defaults) {
158
216
  `CHAIN_ID=${defaults.chainId || "eip155:84532"}`,
159
217
  `WINDOW_SECONDS=${windowSeconds ?? 3600}`,
160
218
  "",
219
+ "# Access control",
220
+ `ACCESS_MODE=${defaults.accessMode || DEFAULT_ACCESS_MODE}`,
221
+ `DOWNLOAD_CODE_HASH=${defaults.downloadCodeHash || ""}`,
222
+ "",
161
223
  "# Required when FACILITATOR_MODE=cdp_mainnet (Base mainnet path)",
162
224
  `CDP_API_KEY_ID=${defaults.cdpApiKeyId || ""}`,
163
225
  `CDP_API_KEY_SECRET=${defaults.cdpApiKeySecret || ""}`,
@@ -182,19 +244,19 @@ function buildEnvScaffold(defaults) {
182
244
  function maybeWriteEnvScaffold(defaults) {
183
245
  const envPath = path.resolve(process.cwd(), ".env");
184
246
  if (fs.existsSync(envPath)) {
185
- console.log(`[config] ${envPath} already exists; skipping .env scaffold write.`);
247
+ logWarn(`${envPath} already exists; skipping .env scaffold write.`);
186
248
  return;
187
249
  }
188
250
 
189
251
  const body = buildEnvScaffold(defaults);
190
252
  fs.writeFileSync(envPath, body);
191
- console.log(`[config] wrote ${envPath}`);
253
+ logOk(`Wrote ${envPath}`);
192
254
  }
193
255
 
194
256
  async function runWizard({ writeEnv }) {
195
257
  const loaded = readConfig();
196
258
  if (loaded.error) {
197
- console.error(`[config] warning: ${loaded.error}`);
259
+ logWarn(loaded.error);
198
260
  }
199
261
 
200
262
  const existing = loaded.config.defaults || {};
@@ -206,16 +268,12 @@ async function runWizard({ writeEnv }) {
206
268
  "SELLER_PAY_TO (seller payout address)",
207
269
  existing.sellerPayTo || "",
208
270
  );
209
- while (true) {
210
- sellerPayTo = String(sellerPayTo || "").trim();
211
- if (!sellerPayTo) {
212
- console.error("SELLER_PAY_TO is required.");
213
- } else if (!isAddress(sellerPayTo)) {
214
- console.error("Invalid SELLER_PAY_TO. Expected a valid Ethereum address (0x + 40 hex chars).");
215
- } else {
216
- break;
217
- }
218
- sellerPayTo = await askWithDefault(rl, "SELLER_PAY_TO (seller payout address)", sellerPayTo);
271
+ sellerPayTo = String(sellerPayTo || "").trim();
272
+ while (sellerPayTo && !isAddress(sellerPayTo)) {
273
+ logError("Invalid SELLER_PAY_TO. Expected a valid Ethereum address (0x + 40 hex chars).");
274
+ sellerPayTo = String(
275
+ await askWithDefault(rl, "SELLER_PAY_TO (seller payout address)", sellerPayTo),
276
+ ).trim();
219
277
  }
220
278
 
221
279
  let chainIdInput = await askWithDefault(
@@ -229,7 +287,7 @@ async function runWizard({ writeEnv }) {
229
287
  chainId = resolveSupportedChain(chainIdInput).caip2;
230
288
  break;
231
289
  } catch (err) {
232
- console.error(err.message || String(err));
290
+ logError(err.message || String(err));
233
291
  chainIdInput = await askWithDefault(rl, "CHAIN_ID", chainIdInput || "eip155:84532");
234
292
  }
235
293
  }
@@ -241,7 +299,7 @@ async function runWizard({ writeEnv }) {
241
299
  );
242
300
  facilitatorMode = facilitatorMode.toLowerCase();
243
301
  while (!ALLOWED_FACILITATOR_MODES.has(facilitatorMode)) {
244
- console.error("Invalid FACILITATOR_MODE. Use: testnet or cdp_mainnet");
302
+ logError("Invalid FACILITATOR_MODE. Use: testnet or cdp_mainnet");
245
303
  facilitatorMode = (await askWithDefault(rl, "FACILITATOR_MODE", "testnet")).toLowerCase();
246
304
  }
247
305
 
@@ -265,7 +323,7 @@ async function runWizard({ writeEnv }) {
265
323
  existing.cdpApiKeyId || "",
266
324
  );
267
325
  while (!cdpApiKeyId) {
268
- console.error("CDP_API_KEY_ID is required when FACILITATOR_MODE=cdp_mainnet");
326
+ logError("CDP_API_KEY_ID is required when FACILITATOR_MODE=cdp_mainnet");
269
327
  cdpApiKeyId = await askWithDefault(rl, "CDP_API_KEY_ID", "");
270
328
  }
271
329
 
@@ -275,7 +333,7 @@ async function runWizard({ writeEnv }) {
275
333
  existing.cdpApiKeySecret || "",
276
334
  );
277
335
  while (!cdpApiKeySecret) {
278
- console.error("CDP_API_KEY_SECRET is required when FACILITATOR_MODE=cdp_mainnet");
336
+ logError("CDP_API_KEY_SECRET is required when FACILITATOR_MODE=cdp_mainnet");
279
337
  cdpApiKeySecret = await askWithDefault(rl, "CDP_API_KEY_SECRET", "");
280
338
  }
281
339
  }
@@ -287,12 +345,50 @@ async function runWizard({ writeEnv }) {
287
345
  );
288
346
  confirmationPolicy = confirmationPolicy.toLowerCase();
289
347
  while (!ALLOWED_CONFIRMATION_POLICIES.has(confirmationPolicy)) {
290
- console.error("Invalid CONFIRMATION_POLICY. Use: confirmed or optimistic");
348
+ logError("Invalid CONFIRMATION_POLICY. Use: confirmed or optimistic");
291
349
  confirmationPolicy = (
292
350
  await askWithDefault(rl, "CONFIRMATION_POLICY", "confirmed")
293
351
  ).toLowerCase();
294
352
  }
295
353
 
354
+ let accessMode = await askWithDefault(
355
+ rl,
356
+ `ACCESS_MODE (${ACCESS_MODE_VALUES.join("|")})`,
357
+ existing.accessMode || DEFAULT_ACCESS_MODE,
358
+ );
359
+ accessMode = accessMode.toLowerCase();
360
+ while (!isValidAccessMode(accessMode)) {
361
+ logError(`Invalid ACCESS_MODE. Use one of: ${ACCESS_MODE_VALUES.join(", ")}`);
362
+ accessMode = (
363
+ await askWithDefault(
364
+ rl,
365
+ "ACCESS_MODE",
366
+ existing.accessMode || DEFAULT_ACCESS_MODE,
367
+ )
368
+ ).toLowerCase();
369
+ }
370
+
371
+ if (accessModeRequiresPayment(accessMode)) {
372
+ while (!sellerPayTo) {
373
+ logError("SELLER_PAY_TO is required for payment access modes.");
374
+ sellerPayTo = String(
375
+ await askWithDefault(rl, "SELLER_PAY_TO (seller payout address)", ""),
376
+ ).trim();
377
+ }
378
+ while (!isAddress(sellerPayTo)) {
379
+ logError("Invalid SELLER_PAY_TO. Expected a valid Ethereum address (0x + 40 hex chars).");
380
+ sellerPayTo = String(
381
+ await askWithDefault(rl, "SELLER_PAY_TO (seller payout address)", sellerPayTo),
382
+ ).trim();
383
+ }
384
+ }
385
+
386
+ const downloadCodeHash = await askDownloadCodeHash(
387
+ rl,
388
+ existing.downloadCodeHash || "",
389
+ accessMode,
390
+ );
391
+
296
392
  const priceUsd = await askWithDefault(
297
393
  rl,
298
394
  "PRICE_USD",
@@ -311,7 +407,7 @@ async function runWizard({ writeEnv }) {
311
407
  );
312
408
  let port = parsePositiveInt(portRaw);
313
409
  while (port === null) {
314
- console.error("PORT must be a positive integer");
410
+ logError("PORT must be a positive integer");
315
411
  portRaw = await askWithDefault(rl, "PORT", "4021");
316
412
  port = parsePositiveInt(portRaw);
317
413
  }
@@ -323,7 +419,7 @@ async function runWizard({ writeEnv }) {
323
419
  );
324
420
  let endedWindowSeconds = parseNonNegativeInt(endedWindowRaw);
325
421
  while (endedWindowSeconds === null) {
326
- console.error("ENDED_WINDOW_SECONDS must be a non-negative integer");
422
+ logError("ENDED_WINDOW_SECONDS must be a non-negative integer");
327
423
  endedWindowRaw = await askWithDefault(rl, "ENDED_WINDOW_SECONDS", "0");
328
424
  endedWindowSeconds = parseNonNegativeInt(endedWindowRaw);
329
425
  }
@@ -345,10 +441,13 @@ async function runWizard({ writeEnv }) {
345
441
  endedWindowSeconds,
346
442
  ogTitle,
347
443
  ogDescription,
444
+ accessMode,
445
+ downloadCodeHash,
348
446
  };
349
447
 
350
448
  const written = writeConfig({ version: 1, defaults });
351
- console.log(`[config] saved ${written.path}`);
449
+ logOk(`Saved ${written.path}`);
450
+ console.log("");
352
451
  console.log(JSON.stringify(redactConfig(written.config), null, 2));
353
452
 
354
453
  if (writeEnv) {
@@ -376,6 +475,7 @@ async function main() {
376
475
  }
377
476
 
378
477
  main().catch((err) => {
379
- console.error(err);
478
+ const detail = err?.stack || err?.message || String(err);
479
+ logError(detail);
380
480
  process.exit(1);
381
481
  });
@@ -1,6 +1,11 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
+ import {
5
+ isValidAccessMode,
6
+ DEFAULT_ACCESS_MODE,
7
+ } from "../src/access_mode.js";
8
+ import { isValidDownloadCodeHash } from "../src/download_code.js";
4
9
 
5
10
  export const CONFIG_VERSION = 1;
6
11
  export const DEFAULT_TESTNET_FACILITATOR_URL = "https://x402.org/facilitator";
@@ -99,10 +104,24 @@ function normalizeDefaults(rawDefaults) {
99
104
  const ogDescription = trimString(rawDefaults.ogDescription);
100
105
  if (ogDescription) defaults.ogDescription = ogDescription;
101
106
 
107
+ const accessMode = trimString(rawDefaults.accessMode).toLowerCase();
108
+ if (isValidAccessMode(accessMode)) {
109
+ defaults.accessMode = accessMode;
110
+ }
111
+
112
+ const downloadCodeHash = trimString(rawDefaults.downloadCodeHash);
113
+ if (downloadCodeHash && isValidDownloadCodeHash(downloadCodeHash)) {
114
+ defaults.downloadCodeHash = downloadCodeHash;
115
+ }
116
+
102
117
  if (defaults.facilitatorMode && !defaults.facilitatorUrl) {
103
118
  defaults.facilitatorUrl = defaultFacilitatorUrlForMode(defaults.facilitatorMode);
104
119
  }
105
120
 
121
+ if (!defaults.accessMode) {
122
+ defaults.accessMode = DEFAULT_ACCESS_MODE;
123
+ }
124
+
106
125
  return defaults;
107
126
  }
108
127
 
@@ -194,5 +213,9 @@ export function redactConfig(config) {
194
213
  copy.defaults.cdpApiKeyId = redactSecret(copy.defaults.cdpApiKeyId);
195
214
  }
196
215
 
216
+ if (copy.defaults.downloadCodeHash) {
217
+ copy.defaults.downloadCodeHash = redactSecret(copy.defaults.downloadCodeHash);
218
+ }
219
+
197
220
  return copy;
198
221
  }