@vonpay/checkout-cli 0.18.0 → 0.19.0

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.
Files changed (3) hide show
  1. package/README.md +4 -2
  2. package/dist/index.js +461 -378
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,7 +1,24 @@
1
1
  #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
2
19
 
3
20
  // src/program.ts
4
- import { Command as Command13 } from "commander";
21
+ import { Command as Command14 } from "commander";
5
22
 
6
23
  // src/commands/login.ts
7
24
  import { Command, Option } from "commander";
@@ -10,7 +27,7 @@ import chalk from "chalk";
10
27
 
11
28
  // src/config.ts
12
29
  import { chmodSync as chmodSync2, existsSync, mkdirSync as mkdirSync2, readFileSync, rmSync, writeFileSync } from "fs";
13
- import { homedir as homedir2 } from "os";
30
+ import { homedir } from "os";
14
31
  import { join as join2 } from "path";
15
32
 
16
33
  // src/utils/json-errors.ts
@@ -31,218 +48,93 @@ function emitCliError(json, text) {
31
48
  }
32
49
 
33
50
  // src/utils/key-mode.ts
34
- import { isLiveKey } from "@vonpay/checkout-node";
35
- var TEST_SECRET_KEY_PREFIX = "vp_sk_test_";
36
- function requiresLiveConfirmation(key) {
37
- if (typeof key !== "string") return true;
38
- if (key.startsWith(TEST_SECRET_KEY_PREFIX)) return false;
39
- if (key.startsWith("vp_pk_")) return false;
40
- return true;
41
- }
51
+ import { keyMode } from "@vonpay/checkout-node";
42
52
  function secretKeyMode(key) {
43
- if (typeof key !== "string") return null;
44
- if (isLiveKey(key)) return "live";
45
- if (key.startsWith(TEST_SECRET_KEY_PREFIX)) return "test";
46
- return null;
53
+ const mode = keyMode(key);
54
+ return mode === "test" || mode === "live" ? mode : null;
47
55
  }
48
56
  var NOT_A_SECRET_KEY_MESSAGE = "This is not a Von Payments secret key: it must start with vp_sk_test_ (sandbox) or vp_sk_live_ (live). Publishable keys (vp_pk_\u2026) and keys from other services cannot be used with this CLI.";
49
57
 
50
58
  // src/utils/audit-log.ts
59
+ var audit_log_exports = {};
60
+ __export(audit_log_exports, {
61
+ AUDIT_FIELDS: () => AUDIT_FIELDS,
62
+ COMMAND_KIND: () => COMMAND_KIND,
63
+ MONEY: () => MONEY,
64
+ UNKNOWN_COMMAND: () => UNKNOWN_COMMAND,
65
+ _resetAuditForTests: () => _resetAuditForTests,
66
+ _resetAuditWarningsForTests: () => _resetAuditWarningsForTests,
67
+ auditError: () => auditError,
68
+ auditFinish: () => auditFinish,
69
+ auditIntent: () => auditIntent,
70
+ auditLine: () => auditLine,
71
+ auditNote: () => auditNote,
72
+ auditSignalHandledByCommand: () => auditSignalHandledByCommand,
73
+ auditStart: () => auditStart,
74
+ auditUsageError: () => auditUsageError,
75
+ fullRecordLine: () => fullRecordLine,
76
+ isMoneyCommand: () => isMoneyCommand,
77
+ pruneAuditFiles: () => pruneAuditFiles,
78
+ writeAuditLine: () => writeAuditLine
79
+ });
80
+ __reExport(audit_log_exports, audit_log_star);
51
81
  import {
52
82
  chmodSync,
53
83
  closeSync,
54
- constants as FS,
55
84
  fchmodSync,
56
85
  fstatSync,
57
86
  lstatSync,
58
87
  mkdirSync,
59
88
  openSync,
60
89
  readdirSync,
90
+ readSync,
61
91
  unlinkSync,
62
92
  writeSync
63
93
  } from "fs";
64
- import { homedir } from "os";
65
- import { join, resolve } from "path";
66
- import { isErrorCode, stripHiddenCharacters } from "@vonpay/checkout-node";
67
- var AUDIT_FIELDS = [
68
- "ts",
69
- "surface",
70
- "version",
71
- "command",
72
- "key_mode",
73
- "key_fingerprint",
74
- "outcome",
75
- "error_code",
76
- "request_id",
77
- "amount",
78
- "currency",
79
- "target"
80
- ];
81
- var AUDIT_OUTCOMES = [
82
- "pending",
83
- // a money command is about to send; the command's final line follows
84
- "sent",
85
- // an API command ran to the end (sent and answered); no person was asked
86
- "local",
87
- // a command that sends nothing to the API (login, logout, init, trigger) ran to the end
88
- "approved",
89
- // a person confirmed it at the terminal, and it ran to the end
90
- "declined",
91
- // the person did not type "confirm"
92
- "budget_refused",
93
- // (the MCP server's spending budget; the CLI has none)
94
- "read_mode_blocked",
95
- // (the MCP server's read-only mode; the CLI has none)
96
- "withheld",
97
- // a live action with no person at an interactive terminal to confirm it
98
- "lookup_refused",
99
- // the check before the confirmation failed (a refund's payment could not be looked up or did not match)
100
- "refused_local",
101
- // refused by the CLI before anything was sent (a flag, a key rule, an unsafe base URL, a command line it cannot parse, …)
102
- "error"
103
- // the API (or the network) answered with an error, the run was interrupted, or the CLI crashed (internal_error)
104
- ];
105
- var LOCAL_ERROR_CODES = [
106
- "auth_failed",
107
- "budget",
108
- "cancelled",
109
- "confirm_live_required",
110
- "currency_mismatch",
111
- "declined",
112
- "dismissed",
113
- "excess_arguments",
114
- "idempotency_key_required",
115
- "internal_error",
116
- "interrupted",
117
- "invalid_argument",
118
- "invalid_input",
119
- "live_loopback_url",
120
- "local_refusal",
121
- "lookup_failed",
122
- "missing_argument",
123
- "network_error",
124
- "no_approval_channel",
125
- "no_terminal",
126
- "not_ticked",
127
- "prompt_failed",
128
- "read_mode",
129
- "unknown_command",
130
- "unknown_option",
131
- "unknown_tool",
132
- "usage_error"
133
- ];
134
- var LOCAL_CODES = new Set(LOCAL_ERROR_CODES);
135
- var AUDIT_DIR_ENV = "VONPAY_AUDIT_DIR";
136
- var AUDIT_SWITCH_ENV = "VONPAY_AUDIT_LOG";
137
- var AUDIT_RETENTION_ENV = "VONPAY_AUDIT_RETENTION_DAYS";
138
- var DEFAULT_RETENTION_DAYS = 30;
139
- var MAX_FILE_BYTES = 50 * 1024 * 1024;
140
- function auditText(s, max) {
141
- const line = stripHiddenCharacters(s).replace(/[\p{Cc}\u{2028}\u{2029}]/gu, " ").trim();
142
- return line.length > max ? line.slice(0, max) : line;
143
- }
144
- function keyFingerprint(key) {
145
- if (key.length >= 24) return `${key.slice(0, 12)}\u2026${key.slice(-4)}`;
146
- const head = Math.min(12, Math.floor(key.length / 3));
147
- const tail = Math.min(4, Math.floor(key.length / 4));
148
- return `${key.slice(0, head)}\u2026${key.slice(key.length - tail)}`;
149
- }
150
- var TARGET_ID = /^(vpi|vp_tx|vsub|vp_cs)_(live|test)_[A-Za-z0-9_-]{16}$/;
151
- var REQUEST_ID = /^[A-Za-z0-9_-]{12}$/;
152
- var CURRENCY = /^[A-Za-z]{3}$/;
153
- var COMMAND = /^[a-z0-9-]+( [a-z0-9-]+){0,3}$/;
94
+ import { join } from "path";
95
+ import {
96
+ AUDIT_DIR_ENV,
97
+ AUDIT_SWITCH_ENV,
98
+ DEFAULT_AUDIT_LIMITS,
99
+ FULL_RECORD_SUFFIX,
100
+ auditAppendFlags,
101
+ auditErrorCodeOf,
102
+ auditFileExpired,
103
+ auditKeyFields,
104
+ auditLineFormat,
105
+ auditSettingsFromEnv,
106
+ auditSpaceWarnings,
107
+ checkAuditDirStats,
108
+ checkAuditFileStats,
109
+ isReservedLine,
110
+ spaceDecision,
111
+ stderrWarn
112
+ } from "@vonpay/checkout-node/internal/audit-log";
113
+ import * as audit_log_star from "@vonpay/checkout-node/internal/audit-log";
154
114
  var UNKNOWN_COMMAND = "[unknown command]";
155
- var UNRECOGNISED_ID = "[unrecognised id]";
156
- var UNRECOGNISED_REQUEST_ID = "[unrecognised request id]";
157
- function knownErrorCode(code) {
158
- return typeof code === "string" && (isErrorCode(code) || LOCAL_CODES.has(code));
159
- }
160
- function auditLine(record) {
161
- const out = {};
162
- for (const field of AUDIT_FIELDS) {
163
- const v = record[field];
164
- if (v === void 0 || v === null) continue;
165
- switch (field) {
166
- case "amount":
167
- if (typeof v === "number" && Number.isSafeInteger(v) && v >= 0) out[field] = v;
168
- break;
169
- case "currency":
170
- if (typeof v === "string" && CURRENCY.test(v)) out[field] = v.toUpperCase();
171
- break;
172
- case "target":
173
- if (typeof v === "string") out[field] = TARGET_ID.test(v) ? v : UNRECOGNISED_ID;
174
- break;
175
- case "error_code":
176
- if (typeof v === "string") out[field] = knownErrorCode(v) ? v : "unrecognised_code";
177
- break;
178
- case "request_id":
179
- if (typeof v === "string") out[field] = REQUEST_ID.test(v) ? v : UNRECOGNISED_REQUEST_ID;
180
- break;
181
- case "outcome":
182
- if (typeof v === "string" && AUDIT_OUTCOMES.includes(v)) out[field] = v;
183
- break;
184
- case "key_mode":
185
- if (v === "test" || v === "live") out[field] = v;
186
- break;
187
- case "command":
188
- if (typeof v === "string") out[field] = COMMAND.test(v) ? v : UNKNOWN_COMMAND;
189
- break;
190
- default:
191
- if (typeof v === "string") out[field] = auditText(v, field === "ts" ? 40 : 80);
192
- }
193
- }
194
- return `${JSON.stringify(out)}
195
- `;
196
- }
197
- function auditSettingsFromEnv(env = process.env, warn = stderrWarn) {
198
- const off = ["off", "0", "false", "no"].includes((env[AUDIT_SWITCH_ENV] ?? "").trim().toLowerCase());
199
- const raw = (env[AUDIT_RETENTION_ENV] ?? "").trim();
200
- let retentionDays = DEFAULT_RETENTION_DAYS;
201
- if (raw !== "") {
202
- const n = Number(raw);
203
- if (/^[0-9]{1,3}$/.test(raw) && n >= 1 && n <= 365) retentionDays = n;
204
- else warn(`[vonpay] ${AUDIT_RETENTION_ENV} must be a whole number from 1 to 365; keeping audit files ${DEFAULT_RETENTION_DAYS} days.`);
205
- }
206
- const dir = env[AUDIT_DIR_ENV]?.trim() ? resolve(env[AUDIT_DIR_ENV].trim()) : join(homedir(), ".vonpay", "audit");
207
- return { dir: off ? null : dir, retentionDays };
208
- }
209
- function stderrWarn(m) {
210
- try {
211
- process.stderr.write(`${m}
212
- `);
213
- } catch (err) {
214
- void err;
215
- }
216
- }
217
- function errorCodeOf(err) {
218
- return err !== null && typeof err === "object" && typeof err.code === "string" ? err.code : "error";
219
- }
220
- var AuditPlaceRefused = class extends Error {
221
- constructor(code) {
222
- super(code);
223
- this.code = code;
224
- }
225
- code;
226
- };
227
- function myUid() {
228
- return typeof process.getuid === "function" ? process.getuid() : void 0;
229
- }
115
+ var FORMAT = auditLineFormat({ nameField: "command", namePattern: /^[a-z0-9-]+( [a-z0-9-]+){0,3}$/, unknownName: UNKNOWN_COMMAND });
116
+ var AUDIT_FIELDS = FORMAT.fields;
117
+ var auditLine = FORMAT.line;
118
+ var fullRecordLine = FORMAT.fullRecordLine;
230
119
  function checkDir(dir, st) {
231
- if (st.isSymbolicLink()) throw new AuditPlaceRefused("directory_is_a_symlink");
232
- if (!st.isDirectory()) throw new AuditPlaceRefused("not_a_directory");
233
- const uid = myUid();
234
- if (uid !== void 0 && st.uid !== uid) throw new AuditPlaceRefused("directory_owned_by_another_user");
235
- if (uid !== void 0 && (st.mode & 63) !== 0) chmodSync(dir, 448);
120
+ if (checkAuditDirStats(st)) chmodSync(dir, 448);
236
121
  }
237
122
  var FILE_NAME = /^cli-(\d{4})-(\d{2})-(\d{2})\.jsonl$/;
238
- var appendFlags = () => FS.O_APPEND | FS.O_CREAT | FS.O_WRONLY | (FS.O_NOFOLLOW ?? 0);
239
123
  var warned = false;
240
124
  var warnedFull = false;
125
+ var warnedReserve = false;
126
+ var warnedTooLong = false;
127
+ function _resetAuditWarningsForTests() {
128
+ warned = false;
129
+ warnedFull = false;
130
+ warnedReserve = false;
131
+ warnedTooLong = false;
132
+ }
241
133
  function warnOnce(dir, err, warn) {
242
134
  if (warned) return;
243
135
  warned = true;
244
136
  warn(
245
- `[vonpay] The local audit log cannot be written (${errorCodeOf(err)}) in ${dir}; the command went on without it. Set ${AUDIT_DIR_ENV} to a writable directory of your own, or ${AUDIT_SWITCH_ENV}=off.`
137
+ `[vonpay] The local audit log cannot be written (${auditErrorCodeOf(err)}) in ${dir}; the command went on without it. Set ${AUDIT_DIR_ENV} to a writable directory of your own, or ${AUDIT_SWITCH_ENV}=off.`
246
138
  );
247
139
  }
248
140
  function pruneAuditFiles(dir, retentionDays, now = /* @__PURE__ */ new Date(), warn = stderrWarn) {
@@ -251,17 +143,15 @@ function pruneAuditFiles(dir, retentionDays, now = /* @__PURE__ */ new Date(), w
251
143
  checkDir(dir, lstatSync(dir));
252
144
  names = readdirSync(dir);
253
145
  } catch (err) {
254
- if (errorCodeOf(err) !== "ENOENT") warnOnce(dir, err, warn);
146
+ if (auditErrorCodeOf(err) !== "ENOENT") warnOnce(dir, err, warn);
255
147
  return 0;
256
148
  }
257
- const today = Date.parse(`${now.toISOString().slice(0, 10)}T00:00:00Z`);
258
149
  let removed = 0;
259
150
  let skipped = 0;
260
151
  for (const name of names) {
261
152
  const m = FILE_NAME.exec(name);
262
153
  if (!m) continue;
263
- const day = Date.parse(`${m[1]}-${m[2]}-${m[3]}T00:00:00Z`);
264
- if (!Number.isFinite(day) || (today - day) / 864e5 <= retentionDays) continue;
154
+ if (!auditFileExpired(`${m[1]}-${m[2]}-${m[3]}`, now, retentionDays)) continue;
265
155
  const path = join(dir, name);
266
156
  try {
267
157
  if (!lstatSync(path).isFile()) {
@@ -274,42 +164,60 @@ function pruneAuditFiles(dir, retentionDays, now = /* @__PURE__ */ new Date(), w
274
164
  skipped++;
275
165
  }
276
166
  }
277
- if (skipped > 0) warn(`[vonpay] ${skipped} old audit file(s) in ${dir} were not deleted (not a regular file, or could not be removed).`);
167
+ if (skipped > 0) warn(auditSpaceWarnings.notPruned(skipped, dir));
278
168
  return removed;
279
169
  }
280
- function appendSecure(file, line, maxBytes, dir, warn) {
281
- const fd = openSync(file, appendFlags(), 384);
170
+ function endsWithFullRecord(fd, size) {
171
+ const n = Buffer.byteLength(FULL_RECORD_SUFFIX);
172
+ if (size < n) return false;
173
+ const buf = Buffer.alloc(n);
174
+ readSync(fd, buf, 0, n, size - n);
175
+ return buf.toString("utf8") === FULL_RECORD_SUFFIX;
176
+ }
177
+ function appendSecure(file, line, reserved, record, now, limits, dir, warn) {
178
+ const fd = openSync(file, auditAppendFlags(), 384);
282
179
  try {
283
180
  const st = fstatSync(fd);
284
- if (!st.isFile()) throw new AuditPlaceRefused("not_a_regular_file");
285
- const uid = myUid();
286
- if (uid !== void 0 && st.uid !== uid) throw new AuditPlaceRefused("file_owned_by_another_user");
287
- if (uid !== void 0 && (st.mode & 63) !== 0) fchmodSync(fd, 384);
288
- if (st.size + Buffer.byteLength(line) > maxBytes) {
181
+ if (checkAuditFileStats(st)) fchmodSync(fd, 384);
182
+ const decision = spaceDecision(st.size, Buffer.byteLength(line), reserved, limits);
183
+ if (decision === "write") {
184
+ writeSync(fd, line);
185
+ } else if (decision === "too_long") {
186
+ if (!warnedTooLong) {
187
+ warnedTooLong = true;
188
+ warn(auditSpaceWarnings.tooLong(limits.maxLineBytes, dir));
189
+ }
190
+ } else if (decision === "drop") {
191
+ if (!warnedReserve) {
192
+ warnedReserve = true;
193
+ warn(auditSpaceWarnings.nearlyFull(dir));
194
+ }
195
+ } else {
196
+ const full = fullRecordLine(record.surface, record.version, now);
197
+ if (!endsWithFullRecord(fd, st.size) && st.size + Buffer.byteLength(full) <= limits.maxFileBytes) writeSync(fd, full);
289
198
  if (!warnedFull) {
290
199
  warnedFull = true;
291
- warn(`[vonpay] Today's audit file in ${dir} is full; no more lines are written to it today.`);
200
+ warn(auditSpaceWarnings.full(dir));
292
201
  }
293
- return;
294
202
  }
295
- writeSync(fd, line);
296
203
  } finally {
297
204
  closeSync(fd);
298
205
  }
299
206
  }
300
- function writeAuditLine(dir, record, now = /* @__PURE__ */ new Date(), warn = stderrWarn, maxBytes = MAX_FILE_BYTES) {
207
+ function writeAuditLine(dir, record, now = /* @__PURE__ */ new Date(), warn = stderrWarn, limits = DEFAULT_AUDIT_LIMITS) {
301
208
  const file = join(dir, `cli-${now.toISOString().slice(0, 10)}.jsonl`);
302
209
  try {
303
210
  const line = auditLine(record);
211
+ const reserved = isReservedLine(record, typeof record.command === "string" && isMoneyCommand(record.command));
304
212
  const attempt = () => {
305
213
  mkdirSync(dir, { recursive: true, mode: 448 });
306
214
  checkDir(dir, lstatSync(dir));
307
- appendSecure(file, line, maxBytes, dir, warn);
215
+ appendSecure(file, line, reserved, record, now, limits, dir, warn);
308
216
  };
309
217
  try {
310
218
  attempt();
311
219
  } catch (err) {
312
- if (errorCodeOf(err) !== "ENOENT") throw err;
220
+ if (auditErrorCodeOf(err) !== "ENOENT") throw err;
313
221
  attempt();
314
222
  }
315
223
  return true;
@@ -335,6 +243,8 @@ var COMMAND_KIND = {
335
243
  "checkout refunds create": "api",
336
244
  "checkout refunds list": "api",
337
245
  "checkout tokens create": "api",
246
+ "checkout subscription-products list": "api",
247
+ "checkout subscription-products terms": "api",
338
248
  "checkout capabilities": "api",
339
249
  "checkout listen": "api",
340
250
  "checkout health": "api",
@@ -347,6 +257,9 @@ var MONEY = {
347
257
  "checkout refunds create": (o) => ({ amount: wholeNumber(o.amount), currency: o.currency, target: o.paymentIntent ?? o.transaction }),
348
258
  "checkout sessions create": (o) => ({ amount: wholeNumber(o.amount), currency: o.currency })
349
259
  };
260
+ function isMoneyCommand(command) {
261
+ return Object.prototype.hasOwnProperty.call(MONEY, command);
262
+ }
350
263
  function wholeNumber(v) {
351
264
  return typeof v === "string" && /^[0-9]{1,15}$/.test(v.trim()) ? Number(v.trim()) : typeof v === "number" ? v : void 0;
352
265
  }
@@ -354,9 +267,6 @@ var pending = null;
354
267
  var hooked = false;
355
268
  var SIGNAL_EXIT = { SIGINT: 130, SIGTERM: 143 };
356
269
  var processHandlers = [];
357
- function keyFields(apiKey) {
358
- return apiKey ? { key_mode: apiKey.startsWith("vp_sk_test_") ? "test" : "live", key_fingerprint: keyFingerprint(apiKey) } : {};
359
- }
360
270
  var signalsHandledByCommand = false;
361
271
  function auditSignalHandledByCommand() {
362
272
  signalsHandledByCommand = true;
@@ -387,13 +297,13 @@ function auditStart(command, version, opts = {}, args = []) {
387
297
  return;
388
298
  }
389
299
  pruneAuditFiles(settings.dir, settings.retentionDays);
390
- let money = {};
300
+ let money2 = {};
391
301
  try {
392
- money = Object.prototype.hasOwnProperty.call(MONEY, command) ? MONEY[command](opts, args) : {};
302
+ money2 = Object.prototype.hasOwnProperty.call(MONEY, command) ? MONEY[command](opts, args) : {};
393
303
  } catch {
394
- money = {};
304
+ money2 = {};
395
305
  }
396
- pending = { command, money, settings, version, intentWritten: false, written: false };
306
+ pending = { command, money: money2, settings, version, intentWritten: false, written: false };
397
307
  if (typeof opts.apiKey === "string") pending.apiKey = opts.apiKey;
398
308
  hookProcess();
399
309
  }
@@ -415,7 +325,7 @@ function auditIntent(now = /* @__PURE__ */ new Date()) {
415
325
  const p = pending;
416
326
  if (!p || p.intentWritten || p.settings.dir === null || !Object.prototype.hasOwnProperty.call(MONEY, p.command)) return;
417
327
  p.intentWritten = true;
418
- writeAuditLine(p.settings.dir, { command: p.command, ts: now.toISOString(), surface: "cli", version: p.version, ...keyFields(p.apiKey), outcome: "pending", ...p.money }, now);
328
+ writeAuditLine(p.settings.dir, { command: p.command, ts: now.toISOString(), surface: "cli", version: p.version, ...auditKeyFields(p.apiKey), outcome: "pending", ...p.money }, now);
419
329
  }
420
330
  function auditFinish(exitCode, now = /* @__PURE__ */ new Date()) {
421
331
  const p = pending;
@@ -438,7 +348,7 @@ function auditFinish(exitCode, now = /* @__PURE__ */ new Date()) {
438
348
  ts: now.toISOString(),
439
349
  surface: "cli",
440
350
  version: p.version,
441
- ...keyFields(p.apiKey),
351
+ ...auditKeyFields(p.apiKey),
442
352
  outcome,
443
353
  ...code ? { error_code: code } : {},
444
354
  ...p.requestId ? { request_id: p.requestId } : {},
@@ -484,9 +394,15 @@ function auditUsageError(command, version, commanderCode, exitCode, now = /* @__
484
394
  void err;
485
395
  }
486
396
  }
397
+ function _resetAuditForTests() {
398
+ pending = null;
399
+ for (const [event, h] of processHandlers.splice(0)) process.removeListener(event, h);
400
+ hooked = false;
401
+ signalsHandledByCommand = false;
402
+ }
487
403
 
488
404
  // src/config.ts
489
- var CONFIG_DIR = join2(homedir2(), ".vonpay");
405
+ var CONFIG_DIR = join2(homedir(), ".vonpay");
490
406
  var CONFIG_FILE = join2(CONFIG_DIR, "config.json");
491
407
  function loadConfig() {
492
408
  if (!existsSync(CONFIG_FILE)) return {};
@@ -582,10 +498,10 @@ function evaluateKeyToSave(key) {
582
498
  }
583
499
  function readLine(prompt) {
584
500
  const rl = createInterface({ input: process.stdin, output: process.stderr });
585
- return new Promise((resolve3) => {
501
+ return new Promise((resolve2) => {
586
502
  rl.question(prompt, (answer) => {
587
503
  rl.close();
588
- resolve3(answer.trim());
504
+ resolve2(answer.trim());
589
505
  });
590
506
  });
591
507
  }
@@ -659,7 +575,7 @@ function logoutCommand() {
659
575
  // src/commands/init.ts
660
576
  import { Command as Command3 } from "commander";
661
577
  import { appendFileSync, chmodSync as chmodSync3, existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
662
- import { resolve as resolve2 } from "path";
578
+ import { resolve } from "path";
663
579
  import chalk3 from "chalk";
664
580
  var LIVE_KEY_NOT_WRITTEN_MESSAGE = "Refusing to write a live key (vp_sk_live_\u2026) to .env: live keys are never written to disk. Run `init` with a sandbox key (vp_sk_test_\u2026), and give your app its live key through your deployment's secret store or the environment when you go live.";
665
581
  function ensureGitignoreBlocksEnv() {
@@ -710,7 +626,7 @@ function initCommand() {
710
626
  process.exit(1);
711
627
  }
712
628
  const envFile = ".env";
713
- const absPath = resolve2(envFile);
629
+ const absPath = resolve(envFile);
714
630
  if (existsSync2(envFile) && !opts.force) {
715
631
  console.error(
716
632
  chalk3.yellow(
@@ -846,13 +762,14 @@ function scrubSecrets(s) {
846
762
  }
847
763
 
848
764
  // src/utils/sanitize.ts
849
- import { stripHiddenCharacters as stripHiddenCharacters2 } from "@vonpay/checkout-node";
765
+ import { stripHiddenCharacters } from "@vonpay/checkout-node";
850
766
  var ANSI_PATTERN = /\x1B\[[0-9;?]*[A-Za-z]/g;
851
767
  var C1_CSI_PATTERN = /\u{9B}[0-9;?]*[A-Za-z]/gu;
852
768
  var CONTROL_PATTERN = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\u{80}-\u{9F}]/gu;
853
769
  var LINE_SEPARATOR_PATTERN = /[\u{2028}\u{2029}]/gu;
770
+ var PLAIN_TEXT_PATTERN = /^[\x20-\x7e]*$/;
854
771
  function stripAnsi(s) {
855
- return stripHiddenCharacters2(s.replace(ANSI_PATTERN, "").replace(C1_CSI_PATTERN, "").replace(CONTROL_PATTERN, ""));
772
+ return stripHiddenCharacters(s.replace(ANSI_PATTERN, "").replace(C1_CSI_PATTERN, "").replace(CONTROL_PATTERN, ""));
856
773
  }
857
774
  function truncate(s, max) {
858
775
  if (s.length <= max) return s;
@@ -864,13 +781,50 @@ function safeDisplay(s, max = 200) {
864
781
  function safeInline(s, max = 200) {
865
782
  return safeDisplay(s.replace(/[\r\n]+/g, " ").replace(LINE_SEPARATOR_PATTERN, " "), max);
866
783
  }
784
+ var READ_SECRET_PATTERNS = SECRET_PATTERNS.map(
785
+ ([re, replacement]) => [new RegExp(`(?<![A-Za-z0-9])(?:${re.source})`, re.flags), replacement]
786
+ );
787
+ function redactSecretShapes(s) {
788
+ let out = s;
789
+ for (const [re, replacement] of READ_SECRET_PATTERNS) out = out.replace(re, replacement);
790
+ return out;
791
+ }
792
+ function uniqueKey(cleaned, taken) {
793
+ let key = cleaned;
794
+ for (let n = 2; taken.has(key); n++) key = `${cleaned}#${n}`;
795
+ taken.add(key);
796
+ return key;
797
+ }
798
+ function cleanJsonValue(value, options = {}) {
799
+ const { lines = "keep", maxChars = Number.MAX_SAFE_INTEGER, redact = redactSecretShapes, exactKeys } = options;
800
+ const text = (s) => {
801
+ if (PLAIN_TEXT_PATTERN.test(s)) return truncate(s, maxChars);
802
+ return lines === "join" ? safeInline(s, maxChars) : truncate(stripAnsi(s.replace(LINE_SEPARATOR_PATTERN, " ")), maxChars);
803
+ };
804
+ const walk = (v, exact) => {
805
+ if (typeof v === "string") return exact ? text(v) : redact(text(v));
806
+ if (v === null || typeof v !== "object") return v;
807
+ const toJSON = v.toJSON;
808
+ if (typeof toJSON === "function") return walk(toJSON.call(v), exact);
809
+ if (Array.isArray(v)) return v.map((item) => walk(item, exact));
810
+ const taken = /* @__PURE__ */ new Set();
811
+ return Object.fromEntries(
812
+ Object.entries(v).map(([k, item]) => [uniqueKey(text(k), taken), walk(item, exact || exactKeys?.has(k) === true)])
813
+ );
814
+ };
815
+ return walk(value, false);
816
+ }
817
+ function jsonOutput(value, options) {
818
+ return JSON.stringify(cleanJsonValue(value, options), null, 2);
819
+ }
867
820
 
868
821
  // src/utils/errors.ts
869
822
  var CLI_READ_BACK = "From this CLI: `vonpay checkout payment-intents get <id>` (charge, capture, void), `vonpay checkout refunds list --payment-intent <id>` or `--transaction <id>` (refund), `sessions get <id>` (checkout session).";
870
823
  var READ_BACK_FIRST = "Check the payment first: read it back (`vonpay checkout payment-intents get <id>` for a charge, capture or void; `vonpay checkout refunds list --payment-intent <id>` (or `--transaction <id>`) for a refund; `sessions get <id>` for a checkout session) and act only on what it shows.";
871
824
  var UNKNOWN_OUTCOME_GUIDANCE = `${READ_BACK_FIRST} If you then retry, reuse the SAME --idempotency-key as the first attempt, never a new one: a new key is a new action and can move money twice.`;
872
825
  var str = (v) => v === void 0 || v === null ? null : String(v);
873
- var oneLine = (s) => safeInline(s, 2e3);
826
+ var ONE_LINE_MAX_CHARS = 2e3;
827
+ var oneLine = (s) => safeInline(s, ONE_LINE_MAX_CHARS);
874
828
  function paramFlagHint(param) {
875
829
  const flag = param.startsWith("metadata") ? "--metadata" : `--${param.replace(/\[(gte|lt)\]$/, "-$1").replace(/_/g, "-")}`;
876
830
  return /^--[a-z-]+$/.test(flag) ? ` (the ${flag} flag)` : "";
@@ -970,16 +924,7 @@ function errorDetailLines(err) {
970
924
  }
971
925
  return lines;
972
926
  }
973
- function scrubDeep(value) {
974
- if (typeof value === "string") return scrubSecrets(oneLine(value));
975
- if (Array.isArray(value)) return value.map(scrubDeep);
976
- if (value !== null && typeof value === "object") {
977
- const out = {};
978
- for (const [k, v] of Object.entries(value)) out[k] = scrubDeep(v);
979
- return out;
980
- }
981
- return value;
982
- }
927
+ var ERROR_JSON_CLEANING = { lines: "join", maxChars: ONE_LINE_MAX_CHARS, redact: scrubSecrets };
983
928
  function errorJson(err) {
984
929
  if (err instanceof VonPayError) {
985
930
  const out = { error: err.message };
@@ -988,14 +933,14 @@ function errorJson(err) {
988
933
  const value = source[field];
989
934
  if (value !== void 0) out[field] = value;
990
935
  }
991
- return scrubDeep(out);
936
+ return cleanJsonValue(out, ERROR_JSON_CLEANING);
992
937
  }
993
938
  const message = err instanceof Error ? err.message : String(err);
994
939
  return { error: scrubSecrets(oneLine(message)), code: null };
995
940
  }
996
941
  function exitWithCliError(message, text) {
997
942
  auditNote({ outcome: "refused_local", code: "local_refusal" }, true);
998
- return emitCliError({ error: scrubSecrets(message), code: null }, text);
943
+ return emitCliError({ error: cleanJsonValue(message, ERROR_JSON_CLEANING), code: null }, text);
999
944
  }
1000
945
  function handleError(err) {
1001
946
  auditError(err);
@@ -1074,7 +1019,7 @@ function makeCheckoutClient() {
1074
1019
  import { createInterface as createInterface2 } from "readline";
1075
1020
  import chalk5 from "chalk";
1076
1021
  import { Option as Option2 } from "commander";
1077
- import { isLiveKey as isLiveKey2 } from "@vonpay/checkout-node";
1022
+ import { heldAsLive, isLiveKey } from "@vonpay/checkout-node";
1078
1023
  var CONFIRM_WORD = "confirm";
1079
1024
  var terminal = {
1080
1025
  /**
@@ -1086,27 +1031,27 @@ var terminal = {
1086
1031
  },
1087
1032
  /** Ask one line on the terminal. End of input or Ctrl-C answers "" (cancel). */
1088
1033
  ask(prompt) {
1089
- return new Promise((resolve3) => {
1034
+ return new Promise((resolve2) => {
1090
1035
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
1091
1036
  let answered = false;
1092
1037
  rl.on("SIGINT", () => rl.close());
1093
1038
  rl.on("close", () => {
1094
- if (!answered) resolve3("");
1039
+ if (!answered) resolve2("");
1095
1040
  });
1096
1041
  rl.question(prompt, (answer) => {
1097
1042
  answered = true;
1098
1043
  rl.close();
1099
- resolve3(answer);
1044
+ resolve2(answer);
1100
1045
  });
1101
1046
  });
1102
1047
  }
1103
1048
  };
1104
- function keyFingerprint2(key) {
1049
+ function keyFingerprint(key) {
1105
1050
  const m = key.match(/^(vp_sk_live_)([A-Za-z0-9_-]{0,4})/);
1106
1051
  return m ? `${m[1]}${m[2]}\u2026` : "a key that is not a sandbox key";
1107
1052
  }
1108
1053
  async function confirmLiveAction(key, summary, opts = {}) {
1109
- if (!requiresLiveConfirmation(key)) return;
1054
+ if (!heldAsLive(key)) return;
1110
1055
  if (!terminal.isInteractive()) {
1111
1056
  const message = `Refusing to ${summary.action} with a live key: no person is at an interactive terminal to confirm it. A live-key action must be confirmed by a person typing at a terminal (stdin and stdout both a terminal); scripts, CI jobs and AI agents cannot confirm it, and --confirm-live is no longer enough. Use a sandbox key (vp_sk_test_\u2026) for automation, or run the command yourself in a terminal. Nothing was sent.`;
1112
1057
  auditNote({ outcome: "withheld", code: "no_terminal" });
@@ -1120,7 +1065,7 @@ async function confirmLiveAction(key, summary, opts = {}) {
1120
1065
  );
1121
1066
  }
1122
1067
  const lines = [
1123
- chalk5.bold.red(`You are about to ${summary.action} with a LIVE key (${keyFingerprint2(key)}).`),
1068
+ chalk5.bold.red(`You are about to ${summary.action} with a LIVE key (${keyFingerprint(key)}).`),
1124
1069
  // At least one space after the label, however long it is.
1125
1070
  ...(summary.details ?? []).map(([label, value]) => ` ${`${label}:`.padEnd(Math.max(17, label.length + 2))}${safeInline(value)}`),
1126
1071
  chalk5.yellow(summary.consequence ?? "This moves real money and cannot be undone from here.")
@@ -1141,7 +1086,7 @@ function retiredConfirmLiveOption() {
1141
1086
  ).hideHelp();
1142
1087
  }
1143
1088
  function warnLiveSecretFlag(key, flag) {
1144
- if (!requiresLiveConfirmation(key)) return;
1089
+ if (!heldAsLive(key)) return;
1145
1090
  console.warn(
1146
1091
  chalk5.yellow(
1147
1092
  `Warning: passing ${flag} as a command argument with a live key exposes it to shell history and process-list (\`ps\`). Consider piping or env-injecting the value instead.`
@@ -1162,6 +1107,11 @@ function parseMinorUnits(raw, flag) {
1162
1107
  }
1163
1108
  return { ok: true, value };
1164
1109
  }
1110
+ function positiveWholeNumber(raw, flag) {
1111
+ const parsed = parseMinorUnits(raw, flag);
1112
+ if (!parsed.ok || parsed.value < 1) throw new Error(`${flag} must be a whole number of at least 1; got ${JSON.stringify(raw)}.`);
1113
+ return parsed.value;
1114
+ }
1165
1115
  function minorUnitsOrExit(raw, flag) {
1166
1116
  const parsed = parseMinorUnits(raw, flag);
1167
1117
  if (!parsed.ok) exitWithCliError(parsed.message, `Error: ${parsed.message}`);
@@ -1172,9 +1122,7 @@ function optionalMinorUnitsOrExit(raw, flag) {
1172
1122
  }
1173
1123
 
1174
1124
  // src/utils/mask-secrets.ts
1175
- function maskSecretsInJson(json) {
1176
- return scrubSecrets(json);
1177
- }
1125
+ var SHOW_SECRETS_CLEANING = { exactKeys: /* @__PURE__ */ new Set(["clientSecret"]) };
1178
1126
  var SHOW_SECRETS_HELP = "With --json, print the session's client secret in full. \u26A0 The output then contains a secret: do not paste it or let it reach a log";
1179
1127
  function withClientSecret(session) {
1180
1128
  if (session === null || typeof session !== "object" || Array.isArray(session)) return session;
@@ -1182,9 +1130,9 @@ function withClientSecret(session) {
1182
1130
  return { ...session, clientSecret: session.clientSecret };
1183
1131
  }
1184
1132
  function sessionJson(session, showSecrets) {
1185
- const json = JSON.stringify(withClientSecret(session), null, 2);
1186
- if (json === void 0) return String(json);
1187
- return showSecrets === true ? json : maskSecretsInJson(json);
1133
+ const body = cleanJsonValue(withClientSecret(session), showSecrets === true ? SHOW_SECRETS_CLEANING : {});
1134
+ const json = JSON.stringify(body, null, 2);
1135
+ return json === void 0 ? String(json) : json;
1188
1136
  }
1189
1137
 
1190
1138
  // src/commands/sessions.ts
@@ -1284,7 +1232,7 @@ function sessionsCommand() {
1284
1232
  const result = await client.sessions.validate(params);
1285
1233
  spinner.stop();
1286
1234
  if (opts.json) {
1287
- console.log(JSON.stringify(result, null, 2));
1235
+ console.log(jsonOutput(result));
1288
1236
  } else if (result.valid) {
1289
1237
  console.log(chalk6.green("\u2713 Valid request"));
1290
1238
  if (result.warnings.length > 0) {
@@ -1342,6 +1290,7 @@ import { formatMinorUnits as formatMinorUnits3 } from "@vonpay/checkout-node";
1342
1290
  // src/utils/idempotency.ts
1343
1291
  import { randomUUID } from "crypto";
1344
1292
  import chalk7 from "chalk";
1293
+ import { heldAsLive as heldAsLive2 } from "@vonpay/checkout-node";
1345
1294
  function resolveIdempotencyKey(flag) {
1346
1295
  const given = flag?.trim();
1347
1296
  if (given) return { key: given, generated: false };
@@ -1349,7 +1298,7 @@ function resolveIdempotencyKey(flag) {
1349
1298
  }
1350
1299
  function idempotencyKeyFor(flag, what, apiKey) {
1351
1300
  const { key, generated } = resolveIdempotencyKey(flag);
1352
- if (generated && apiKey !== void 0 && requiresLiveConfirmation(apiKey)) {
1301
+ if (generated && apiKey !== void 0 && heldAsLive2(apiKey)) {
1353
1302
  const message = `Refusing to send this ${what} with a live key without --idempotency-key. Choose one key for this ${what} (for example your order id plus the action, such as "order_1042:${what}") and pass the SAME key if you ever run the command again: a new key is a new action and can move money twice. Nothing was sent.`;
1354
1303
  auditNote({ outcome: "refused_local", code: "idempotency_key_required" });
1355
1304
  exitWithCliError(message, chalk7.red(message));
@@ -1465,11 +1414,6 @@ function buildFilters(opts) {
1465
1414
  }
1466
1415
  return filters;
1467
1416
  }
1468
- function wholeNumber2(raw, flag) {
1469
- const parsed = parseMinorUnits(raw, flag);
1470
- if (!parsed.ok || parsed.value < 1) throw new Error(`${flag} must be a whole number of at least 1; got ${JSON.stringify(raw)}.`);
1471
- return parsed.value;
1472
- }
1473
1417
  var PAYMENT_LIST_JSON_FIELDS = [
1474
1418
  "id",
1475
1419
  "status",
@@ -1492,17 +1436,20 @@ function listItemJson(pi, includeMetadata) {
1492
1436
  const src = pi;
1493
1437
  return Object.fromEntries(PAYMENT_LIST_JSON_FIELDS.filter((k) => src[k] !== void 0).map((k) => [k, src[k]]));
1494
1438
  }
1495
- var ALL_JSON_DEFAULT_MAX_ITEMS = 1e4;
1439
+ var ALL_DEFAULT_MAX_ITEMS = 1e4;
1440
+ function stoppedAtDefaultCap() {
1441
+ return `Stopped at ${ALL_DEFAULT_MAX_ITEMS} payments (the --all default); there may be more. Pass --max-items to read more, or narrow the window.`;
1442
+ }
1496
1443
  function buildListParams(opts) {
1497
1444
  const params = buildFilters(opts);
1498
1445
  if (opts.status !== void 0) {
1499
1446
  params.status = parseListFlag(opts.status, "--status", { lowercase: true });
1500
1447
  }
1501
- if (opts.limit !== void 0) params.limit = wholeNumber2(opts.limit, "--limit");
1448
+ if (opts.limit !== void 0) params.limit = positiveWholeNumber(opts.limit, "--limit");
1502
1449
  if (opts.startingAfter !== void 0) params.startingAfter = opts.startingAfter;
1503
1450
  if (opts.maxItems !== void 0 && !opts.all) throw new Error("--max-items is used with --all.");
1504
1451
  if (opts.includeMetadata && !opts.json) throw new Error("--include-metadata is used with --json.");
1505
- const maxItems = opts.maxItems !== void 0 ? wholeNumber2(opts.maxItems, "--max-items") : opts.all && opts.json ? ALL_JSON_DEFAULT_MAX_ITEMS : void 0;
1452
+ const maxItems = opts.maxItems !== void 0 ? positiveWholeNumber(opts.maxItems, "--max-items") : opts.all ? ALL_DEFAULT_MAX_ITEMS : void 0;
1506
1453
  return { params, ...maxItems !== void 0 ? { maxItems } : {} };
1507
1454
  }
1508
1455
  function buildSummaryParams(opts) {
@@ -1587,7 +1534,10 @@ function addPaymentReportCommands(intents) {
1587
1534
  ).option("--status <list>", "Only these statuses, comma-separated (e.g. failed,succeeded)").option("--limit <n>", "Page size, up to 100 per page (default 25; larger values are treated as 100)").option(
1588
1535
  "--starting-after <cursor>",
1589
1536
  "The next-page cursor printed by a previous run, unchanged, with exactly the same filters and window (it lasts 24 hours)"
1590
- ).option("--all", "Read every page, one request at a time (text: printed as each payment arrives)").option("--max-items <n>", `With --all: stop after this many payments (with --json, default ${ALL_JSON_DEFAULT_MAX_ITEMS})`).option(
1537
+ ).option(
1538
+ "--all",
1539
+ `Read every page, one request at a time, up to ${ALL_DEFAULT_MAX_ITEMS} payments unless --max-items says otherwise (text: printed as each payment arrives)`
1540
+ ).option("--max-items <n>", `With --all: stop after this many payments (default ${ALL_DEFAULT_MAX_ITEMS})`).option(
1591
1541
  "--json",
1592
1542
  "Output JSON: each payment's status, outcome, amount, card, store and decline fields \u2014 not your metadata (with --all: one JSON array)"
1593
1543
  ).option("--include-metadata", "With --json: print every field of each payment, your metadata included").action(async (opts) => {
@@ -1609,12 +1559,8 @@ function addPaymentReportCommands(intents) {
1609
1559
  const items = [];
1610
1560
  for await (const pi of pager) items.push(listItemJson(pi, opts.includeMetadata));
1611
1561
  spinner.stop();
1612
- console.log(JSON.stringify(items, null, 2));
1613
- if (opts.maxItems === void 0 && items.length >= ALL_JSON_DEFAULT_MAX_ITEMS) {
1614
- console.error(
1615
- `Stopped at ${ALL_JSON_DEFAULT_MAX_ITEMS} payments (the --all --json default). Pass --max-items to read more, or narrow the window.`
1616
- );
1617
- }
1562
+ console.log(jsonOutput(items));
1563
+ if (opts.maxItems === void 0 && items.length >= ALL_DEFAULT_MAX_ITEMS) console.error(stoppedAtDefaultCap());
1618
1564
  return;
1619
1565
  }
1620
1566
  let count = 0;
@@ -1626,12 +1572,13 @@ function addPaymentReportCommands(intents) {
1626
1572
  spinner.stop();
1627
1573
  if (count === 0) console.log(" No payments match.");
1628
1574
  console.log(` ${count} payment(s).`);
1575
+ if (opts.maxItems === void 0 && count >= ALL_DEFAULT_MAX_ITEMS) console.error(stoppedAtDefaultCap());
1629
1576
  return;
1630
1577
  }
1631
1578
  const page = await client.paymentIntents.list(built.params);
1632
1579
  spinner.stop();
1633
1580
  if (opts.json) {
1634
- console.log(JSON.stringify({ ...page, data: page.data.map((pi) => listItemJson(pi, opts.includeMetadata)) }, null, 2));
1581
+ console.log(jsonOutput({ ...page, data: page.data.map((pi) => listItemJson(pi, opts.includeMetadata)) }));
1635
1582
  return;
1636
1583
  }
1637
1584
  for (const line of paymentListLines(page)) console.log(line);
@@ -1660,7 +1607,7 @@ function addPaymentReportCommands(intents) {
1660
1607
  const summary = await client.paymentIntents.summary(params);
1661
1608
  spinner.stop();
1662
1609
  if (opts.json) {
1663
- console.log(JSON.stringify(summary, null, 2));
1610
+ console.log(jsonOutput(summary));
1664
1611
  return;
1665
1612
  }
1666
1613
  for (const line of summaryLines(summary)) console.log(line);
@@ -1849,7 +1796,7 @@ function paymentIntentsCommand() {
1849
1796
  });
1850
1797
  spinner.stop();
1851
1798
  if (opts.json) {
1852
- console.log(JSON.stringify(intent, null, 2));
1799
+ console.log(jsonOutput(intent));
1853
1800
  } else {
1854
1801
  console.log(chalk9.green("\u2713 Payment intent created"));
1855
1802
  printPaymentIntent(intent);
@@ -1868,7 +1815,7 @@ function paymentIntentsCommand() {
1868
1815
  const intent = await client.paymentIntents.retrieve(id);
1869
1816
  spinner.stop();
1870
1817
  if (opts.json) {
1871
- console.log(JSON.stringify(intent, null, 2));
1818
+ console.log(jsonOutput(intent));
1872
1819
  } else {
1873
1820
  printPaymentIntent(intent);
1874
1821
  }
@@ -1918,7 +1865,7 @@ function paymentIntentsCommand() {
1918
1865
  );
1919
1866
  spinner.stop();
1920
1867
  if (opts.json) {
1921
- console.log(JSON.stringify(intent, null, 2));
1868
+ console.log(jsonOutput(intent));
1922
1869
  } else {
1923
1870
  const captureLabel = amountToCapture !== void 0 ? ` (partial: ${formatMinorUnits3(amountToCapture, intent.currency)})` : "";
1924
1871
  console.log(chalk9.green(`\u2713 Payment intent captured${captureLabel}`));
@@ -1957,7 +1904,7 @@ function paymentIntentsCommand() {
1957
1904
  const intent = await client.paymentIntents.void(id, { idempotencyKey });
1958
1905
  spinner.stop();
1959
1906
  if (opts.json) {
1960
- console.log(JSON.stringify(intent, null, 2));
1907
+ console.log(jsonOutput(intent));
1961
1908
  } else {
1962
1909
  console.log(chalk9.green("\u2713 Payment intent voided"));
1963
1910
  printPaymentIntent(intent);
@@ -1975,7 +1922,7 @@ function paymentIntentsCommand() {
1975
1922
  // src/commands/refunds.ts
1976
1923
  import { Command as Command6 } from "commander";
1977
1924
  import chalk10 from "chalk";
1978
- import { formatMinorUnits as formatMinorUnits4, VonPayError as VonPayError2 } from "@vonpay/checkout-node";
1925
+ import { formatMinorUnits as formatMinorUnits4, heldAsLive as heldAsLive3, VonPayError as VonPayError2 } from "@vonpay/checkout-node";
1979
1926
  function present(v) {
1980
1927
  return v !== void 0 && v.trim() !== "";
1981
1928
  }
@@ -2137,11 +2084,11 @@ function refundsCommand() {
2137
2084
  handleError(err);
2138
2085
  }
2139
2086
  const target = resolveRefundTarget(opts);
2140
- const sessionProblem = refundSessionProblem(target, opts.sessionId, requiresLiveConfirmation(apiKey));
2087
+ const sessionProblem = refundSessionProblem(target, opts.sessionId, heldAsLive3(apiKey));
2141
2088
  if (sessionProblem) exitWithCliError(sessionProblem, chalk10.red(sessionProblem));
2142
2089
  refuseUnsafeLiveDestination(apiKey);
2143
2090
  const idempotencyKey = idempotencyKeyFor(opts.idempotencyKey, "refund", apiKey);
2144
- const lookedUp = requiresLiveConfirmation(apiKey) && terminal.isInteractive() ? await refundLookupDetails(makeCheckoutClient(), target, opts.sessionId?.trim(), params.amount, params.currency) : [];
2091
+ const lookedUp = heldAsLive3(apiKey) && terminal.isInteractive() ? await refundLookupDetails(makeCheckoutClient(), target, opts.sessionId?.trim(), params.amount, params.currency) : [];
2145
2092
  await confirmLiveAction(
2146
2093
  apiKey,
2147
2094
  {
@@ -2166,7 +2113,7 @@ function refundsCommand() {
2166
2113
  const refund = await client.refunds.create(params, { idempotencyKey });
2167
2114
  spinner.stop();
2168
2115
  if (opts.json) {
2169
- console.log(JSON.stringify(refund, null, 2));
2116
+ console.log(jsonOutput(refund));
2170
2117
  } else {
2171
2118
  console.log(chalk10.green("\u2713 Refund created"));
2172
2119
  printRefund(refund);
@@ -2178,7 +2125,7 @@ function refundsCommand() {
2178
2125
  });
2179
2126
  refunds.command("list").description(
2180
2127
  "List the refunds of ONE payment and its refund totals \u2014 the way to check a refund whose outcome was unclear"
2181
- ).option("--payment-intent <id>", "The payment's payment intent ID (vpi_*). Use this OR --transaction").option("--transaction <id>", "The payment's transaction ID (vp_tx_*), instead of --payment-intent").option("--limit <n>", "How many refunds to show (default 25, at most 100)").option("--starting-after <cursor>", "The next-page cursor printed by a previous run").option("--json", "Output raw JSON").action(
2128
+ ).option("--payment-intent <id>", "The payment's payment intent ID (vpi_*). Use this OR --transaction").option("--transaction <id>", "The payment's transaction ID (vp_tx_*), instead of --payment-intent").option("--limit <n>", "How many refunds to show (default 25; the API allows at most 100)").option("--starting-after <cursor>", "The next-page cursor printed by a previous run").option("--json", "Output raw JSON").action(
2182
2129
  async (opts) => {
2183
2130
  let target;
2184
2131
  try {
@@ -2189,12 +2136,12 @@ function refundsCommand() {
2189
2136
  }
2190
2137
  let limit;
2191
2138
  if (opts.limit !== void 0) {
2192
- const parsed = parseMinorUnits(opts.limit, "--limit");
2193
- if (!parsed.ok || parsed.value < 1) {
2194
- const message = `--limit must be a whole number from 1 to 100; got ${JSON.stringify(opts.limit)}.`;
2139
+ try {
2140
+ limit = positiveWholeNumber(opts.limit, "--limit");
2141
+ } catch (err) {
2142
+ const message = err.message;
2195
2143
  exitWithCliError(message, chalk10.red(`Error: ${message}`));
2196
2144
  }
2197
- limit = parsed.value;
2198
2145
  }
2199
2146
  const client = makeCheckoutClient();
2200
2147
  const spinner = startSpinner("Listing refunds...");
@@ -2205,7 +2152,7 @@ function refundsCommand() {
2205
2152
  });
2206
2153
  spinner.stop();
2207
2154
  if (opts.json) {
2208
- console.log(JSON.stringify(list, null, 2));
2155
+ console.log(jsonOutput(list));
2209
2156
  return;
2210
2157
  }
2211
2158
  for (const line of refundListLines(list)) console.log(line);
@@ -2219,10 +2166,10 @@ function refundsCommand() {
2219
2166
  }
2220
2167
  function refundListLines(list) {
2221
2168
  const currency = list.data[0]?.currency;
2222
- const money = (n) => currency ? formatMinorUnits4(n, currency) : `${n} in minor units`;
2169
+ const money2 = (n) => currency ? formatMinorUnits4(n, currency) : `${n} in minor units`;
2223
2170
  const lines = [
2224
- ` Refunded so far: ${money(list.amountRefunded)} (succeeded + still in progress)`,
2225
- ` Still refundable: ${money(list.remainingRefundable)}${list.refundable ? "" : " \u2014 this payment cannot be refunded now"}`
2171
+ ` Refunded so far: ${money2(list.amountRefunded)} (succeeded + still in progress)`,
2172
+ ` Still refundable: ${money2(list.remainingRefundable)}${list.refundable ? "" : " \u2014 this payment cannot be refunded now"}`
2226
2173
  ];
2227
2174
  if (list.data.length === 0) {
2228
2175
  lines.push(" No refunds on this payment.");
@@ -2292,7 +2239,7 @@ function tokensCommand() {
2292
2239
  );
2293
2240
  spinner.stop();
2294
2241
  if (opts.json) {
2295
- console.log(JSON.stringify(token, null, 2));
2242
+ console.log(jsonOutput(token));
2296
2243
  } else {
2297
2244
  console.log(chalk11.green("\u2713 Token created"));
2298
2245
  printToken(token);
@@ -2311,35 +2258,166 @@ function formatTokenCard(card) {
2311
2258
  return { card: cardLine, expires };
2312
2259
  }
2313
2260
 
2314
- // src/commands/capabilities.ts
2261
+ // src/commands/subscription-products.ts
2315
2262
  import { Command as Command8 } from "commander";
2316
2263
  import chalk12 from "chalk";
2264
+ import { formatMinorUnits as formatMinorUnits5 } from "@vonpay/checkout-node";
2265
+ var STATUSES = ["active", "archived"];
2266
+ function buildListParams2(opts) {
2267
+ const params = {};
2268
+ if (opts.productRef !== void 0) params.productRef = opts.productRef;
2269
+ if (opts.status !== void 0) {
2270
+ const status = opts.status.trim().toLowerCase();
2271
+ if (!STATUSES.includes(status)) {
2272
+ throw new Error(`--status must be active or archived; got ${JSON.stringify(opts.status)}.`);
2273
+ }
2274
+ params.status = status;
2275
+ }
2276
+ if (opts.limit !== void 0) params.limit = positiveWholeNumber(opts.limit, "--limit");
2277
+ if (opts.startingAfter !== void 0) params.startingAfter = opts.startingAfter;
2278
+ return params;
2279
+ }
2280
+ function buildTermsParams(opts) {
2281
+ if (opts.amount === void 0) return void 0;
2282
+ const parsed = parseMinorUnits(opts.amount, "--amount");
2283
+ if (!parsed.ok) throw new Error(parsed.message);
2284
+ if (parsed.value < 1) throw new Error(`--amount must be at least 1 (minor units); got ${JSON.stringify(opts.amount)}.`);
2285
+ return { amount: parsed.value };
2286
+ }
2287
+ var LEGAL_TEXT_MAX_CHARS = 500;
2288
+ var CUT_MARK = "\u2026 [CUT: not the full text; use --json to read all of it]";
2289
+ function legalText(raw) {
2290
+ const clean = safeInline(raw, Number.MAX_SAFE_INTEGER);
2291
+ return clean.length > LEGAL_TEXT_MAX_CHARS ? { text: `${clean.slice(0, LEGAL_TEXT_MAX_CHARS)}${CUT_MARK}`, cut: true } : { text: clean, cut: false };
2292
+ }
2293
+ function money(amount, currency) {
2294
+ if (typeof amount !== "number") return "?";
2295
+ return currency ? safeInline(formatMinorUnits5(amount, currency)) : `${amount} in minor units`;
2296
+ }
2297
+ function every(count, interval) {
2298
+ if (!interval) return "";
2299
+ const unit = safeInline(interval);
2300
+ return typeof count === "number" && count !== 1 ? `every ${count} ${unit}s` : `every ${unit}`;
2301
+ }
2302
+ function statusTone2(status) {
2303
+ return status === "active" ? chalk12.green(status) : chalk12.yellow(status);
2304
+ }
2305
+ function productLines(p) {
2306
+ const head = ` ${safeInline(p.id)} ${statusTone2(safeInline(p.status))} ${safeInline(p.name, 120)} (ref ${safeInline(p.productRef, 128)})`;
2307
+ const schedule = p.interval ? `first payment ${money(p.firstPrice, p.currency)}, then ${money(p.renewalPrice, p.currency)} ${every(p.intervalCount, p.interval)}` : `${money(p.renewalPrice, p.currency)} \u2014 no renewal schedule yet (it cannot start a subscription)`;
2308
+ const lines = [head, ` ${schedule}`];
2309
+ const terms = p.terms;
2310
+ if (terms && typeof terms.disclosure === "string") lines.push(` Disclosure: ${legalText(terms.disclosure).text}`);
2311
+ return lines;
2312
+ }
2313
+ function productListLines(page) {
2314
+ const lines = [];
2315
+ if (page.data.length === 0) lines.push(" No subscription products match.");
2316
+ for (const p of page.data) lines.push(...productLines(p));
2317
+ if (page.hasMore && page.nextCursor) {
2318
+ lines.push(` More: re-run with the same filters and --starting-after ${safeInline(page.nextCursor)}`);
2319
+ }
2320
+ return lines;
2321
+ }
2322
+ function termsLines(t) {
2323
+ const disclosure = legalText(t.disclosure);
2324
+ const phrase = legalText(t.pricePhrase);
2325
+ return [
2326
+ ` Product: ${safeInline(t.product)} (ref ${safeInline(t.productRef, 128)})`,
2327
+ ` Disclosure: ${disclosure.text}`,
2328
+ ` Price phrase: ${phrase.text}`,
2329
+ ` First payment: ${money(t.firstAmount, t.currency)}`,
2330
+ ` Renewal: ${money(t.amount, t.currency)} ${every(t.intervalCount, t.interval)}`,
2331
+ ` Terms version: ${safeInline(t.termsVersion)}`,
2332
+ disclosure.cut || phrase.cut ? " The text above was cut. Read all of it with --json before you show it next to the order button." : " Show the disclosure, or your own words containing the price phrase, with your business name and how to cancel."
2333
+ ];
2334
+ }
2335
+ function exitOnBadFlag2(err) {
2336
+ const message = err.message;
2337
+ exitWithCliError(message, chalk12.red(`Error: ${message}`));
2338
+ }
2339
+ function subscriptionProductsCommand() {
2340
+ const products = new Command8("subscription-products").description(
2341
+ "Read your subscription products and the renewal terms a sign-up states (read-only; products are created and changed in the dashboard)"
2342
+ );
2343
+ products.command("list").description("List your subscription products, newest first, each with the terms a sign-up on it states").option("--product-ref <ref>", "Only the product with this reference (your store's product number), matched exactly").option("--status <status>", "Only products with this status: active or archived").option("--limit <n>", "Page size, 1 to 100 (default 10)").option("--starting-after <id>", "The id of the last product on the previous page (printed by a previous run)").option("--json", "Output raw JSON").action(async (opts) => {
2344
+ let params;
2345
+ try {
2346
+ params = buildListParams2(opts);
2347
+ } catch (err) {
2348
+ exitOnBadFlag2(err);
2349
+ }
2350
+ const client = makeCheckoutClient();
2351
+ const spinner = startSpinner("Listing subscription products...");
2352
+ try {
2353
+ const page = await client.subscriptionProducts.list(params);
2354
+ spinner.stop();
2355
+ if (opts.json) {
2356
+ console.log(jsonOutput(page));
2357
+ return;
2358
+ }
2359
+ for (const line of productListLines(page)) console.log(line);
2360
+ } catch (err) {
2361
+ spinner.stop();
2362
+ handleError(err);
2363
+ }
2364
+ });
2365
+ products.command("terms <product>").description(
2366
+ "Read one subscription product's renewal terms, to show next to the order button before the sign-up charge. <product> is the product's id (vsprod_*) or its reference"
2367
+ ).option("--amount <minor>", "An intro first payment in minor units (e.g. 100 = 1.00 USD), at or below the product's price").option("--json", "Output raw JSON").action(async (product, opts) => {
2368
+ let params;
2369
+ try {
2370
+ params = buildTermsParams(opts);
2371
+ } catch (err) {
2372
+ exitOnBadFlag2(err);
2373
+ }
2374
+ const client = makeCheckoutClient();
2375
+ const spinner = startSpinner("Reading subscription product terms...");
2376
+ try {
2377
+ const terms = await client.subscriptionProducts.terms(product, params);
2378
+ spinner.stop();
2379
+ if (opts.json) {
2380
+ console.log(jsonOutput(terms));
2381
+ return;
2382
+ }
2383
+ for (const line of termsLines(terms)) console.log(line);
2384
+ } catch (err) {
2385
+ spinner.stop();
2386
+ handleError(err);
2387
+ }
2388
+ });
2389
+ return products;
2390
+ }
2391
+
2392
+ // src/commands/capabilities.ts
2393
+ import { Command as Command9 } from "commander";
2394
+ import chalk13 from "chalk";
2317
2395
  function fmtBool(v) {
2318
- return v ? chalk12.green("yes") : chalk12.dim("no");
2396
+ return v ? chalk13.green("yes") : chalk13.dim("no");
2319
2397
  }
2320
2398
  function fmtVoidPolicy(v) {
2321
2399
  switch (v) {
2322
2400
  case "supported":
2323
- return chalk12.green(v);
2401
+ return chalk13.green(v);
2324
2402
  case "rerouted_to_refund":
2325
- return chalk12.yellow(v);
2403
+ return chalk13.yellow(v);
2326
2404
  case "not_supported":
2327
- return chalk12.dim(v);
2405
+ return chalk13.dim(v);
2328
2406
  default:
2329
- return chalk12.dim(String(v));
2407
+ return chalk13.dim(String(v));
2330
2408
  }
2331
2409
  }
2332
2410
  function fmtDisputeReporting(v) {
2333
- return v === "tracked" ? chalk12.green(v) : chalk12.yellow(v);
2411
+ return v === "tracked" ? chalk13.green(v) : chalk13.yellow(v);
2334
2412
  }
2335
2413
  function fmtSubscriptionBilling(v) {
2336
- if (v === true) return chalk12.yellow("yes");
2337
- if (v === false) return chalk12.green("no");
2338
- return chalk12.yellow("unknown");
2414
+ if (v === true) return chalk13.yellow("yes");
2415
+ if (v === false) return chalk13.green("no");
2416
+ return chalk13.yellow("unknown");
2339
2417
  }
2340
2418
  function printCapabilities(caps) {
2341
2419
  const ops = caps.supportedOperations;
2342
- console.log(chalk12.bold("Supported operations"));
2420
+ console.log(chalk13.bold("Supported operations"));
2343
2421
  console.log(` auth/capture separation: ${fmtBool(ops.authCaptureSeparation)}`);
2344
2422
  console.log(` partial capture: ${fmtBool(ops.partialCapture)}`);
2345
2423
  console.log(` partial refund: ${fmtBool(ops.partialRefund)}`);
@@ -2354,17 +2432,17 @@ function printCapabilities(caps) {
2354
2432
  console.log();
2355
2433
  if (ops.disputeReporting !== "tracked") {
2356
2434
  console.log(
2357
- chalk12.yellow(
2435
+ chalk13.yellow(
2358
2436
  " \u26A0 Chargebacks on this connection do NOT reach Von Payments.\n A `succeeded` payment is not evidence it was never disputed.\n Monitor and answer disputes in your processor's own dashboard \u2014\n they carry a deadline we cannot see or report."
2359
2437
  )
2360
2438
  );
2361
2439
  console.log();
2362
2440
  }
2363
- console.log(chalk12.bold("Subscription billing"));
2441
+ console.log(chalk13.bold("Subscription billing"));
2364
2442
  console.log(` Von Pay renews for you: ${fmtSubscriptionBilling(caps.subscriptionBilling?.enabled)}`);
2365
2443
  if (caps.subscriptionBilling?.enabled !== false) {
2366
2444
  console.log(
2367
- chalk12.yellow(
2445
+ chalk13.yellow(
2368
2446
  ` \u26A0 Do not run renewals from your own system while this is not "no":
2369
2447
  Von Pay may be charging these customers' renewals itself, and both
2370
2448
  would charge them. Check again at the moment you renew.`
@@ -2372,25 +2450,25 @@ function printCapabilities(caps) {
2372
2450
  );
2373
2451
  }
2374
2452
  console.log();
2375
- console.log(chalk12.bold("Settlement currencies"));
2453
+ console.log(chalk13.bold("Settlement currencies"));
2376
2454
  console.log(
2377
- ` ${caps.settlementCurrencies.length ? caps.settlementCurrencies.join(", ") : chalk12.dim("(none reported)")}`
2455
+ ` ${caps.settlementCurrencies.length ? caps.settlementCurrencies.join(", ") : chalk13.dim("(none reported)")}`
2378
2456
  );
2379
2457
  console.log();
2380
- console.log(chalk12.bold("Rate limits"));
2458
+ console.log(chalk13.bold("Rate limits"));
2381
2459
  console.log(
2382
2460
  ` payment intents / minute: ${caps.rateLimits.paymentIntentsPerMinute}`
2383
2461
  );
2384
2462
  }
2385
2463
  function capabilitiesCommand() {
2386
- return new Command8("capabilities").description("Show what this merchant's binder supports").option("--json", "Output raw JSON").action(async (opts) => {
2464
+ return new Command9("capabilities").description("Show what this merchant's binder supports").option("--json", "Output raw JSON").action(async (opts) => {
2387
2465
  const client = makeCheckoutClient();
2388
2466
  const spinner = startSpinner("Fetching capabilities...");
2389
2467
  try {
2390
2468
  const caps = await client.capabilities.get();
2391
2469
  spinner.stop();
2392
2470
  if (opts.json) {
2393
- console.log(JSON.stringify(caps, null, 2));
2471
+ console.log(jsonOutput(caps));
2394
2472
  } else {
2395
2473
  printCapabilities(caps);
2396
2474
  }
@@ -2402,9 +2480,9 @@ function capabilitiesCommand() {
2402
2480
  }
2403
2481
 
2404
2482
  // src/commands/trigger.ts
2405
- import { Command as Command9 } from "commander";
2483
+ import { Command as Command10 } from "commander";
2406
2484
  import { createHmac as createHmac2, randomUUID as randomUUID2 } from "crypto";
2407
- import chalk13 from "chalk";
2485
+ import chalk14 from "chalk";
2408
2486
  import ora2 from "ora";
2409
2487
 
2410
2488
  // src/utils/signing-secret.ts
@@ -2622,7 +2700,7 @@ function warnIfUnsubscribable(event, write = (m) => console.warn(m)) {
2622
2700
  if (!UNSUBSCRIBABLE_EVENTS.includes(event)) return false;
2623
2701
  const suggestion = event === "session.succeeded" ? "charge.succeeded" : "charge.failed";
2624
2702
  write(
2625
- chalk13.yellow(
2703
+ chalk14.yellow(
2626
2704
  `\u26A0 ${event} is emitted internally but is NOT subscribable.
2627
2705
  The subscription API accepts the key, stores nothing and returns success, so a
2628
2706
  real endpoint receives this event NEVER \u2014 a handler tested only against it is
@@ -2817,7 +2895,7 @@ function buildPayload(event, opts) {
2817
2895
  };
2818
2896
  }
2819
2897
  function triggerCommand() {
2820
- return new Command9("trigger").description("Send a test webhook event to a local URL").argument("<event>", `Event type (${SUPPORTED_EVENTS.join(", ")})`).requiredOption("--url <url>", "Local webhook URL (e.g. http://localhost:3000/webhooks)").option("--session-id <id>", "Custom session ID").option("--amount <cents>", "Amount in minor units, a whole number").option("--currency <code>", "Currency code").option(
2898
+ return new Command10("trigger").description("Send a test webhook event to a local URL").argument("<event>", `Event type (${SUPPORTED_EVENTS.join(", ")})`).requiredOption("--url <url>", "Local webhook URL (e.g. http://localhost:3000/webhooks)").option("--session-id <id>", "Custom session ID").option("--amount <cents>", "Amount in minor units, a whole number").option("--currency <code>", "Currency code").option(
2821
2899
  "--url-tunnel",
2822
2900
  "Allow a non-loopback --url (ngrok / Cloudflare Tunnel). Without it, --url must be this machine"
2823
2901
  ).option(
@@ -2826,7 +2904,7 @@ function triggerCommand() {
2826
2904
  ).addOption(retiredConfirmLiveOption()).action(async (event, opts) => {
2827
2905
  if (!SUPPORTED_EVENTS.includes(event)) {
2828
2906
  console.error(
2829
- chalk13.red(
2907
+ chalk14.red(
2830
2908
  `Unknown event: ${event}
2831
2909
  Supported: ${SUPPORTED_EVENTS.join(", ")}`
2832
2910
  )
@@ -2840,10 +2918,10 @@ Supported: ${SUPPORTED_EVENTS.join(", ")}`
2840
2918
  tunnelFlag: "--url-tunnel"
2841
2919
  });
2842
2920
  if (!destination.ok) {
2843
- console.error(chalk13.red(destination.reason));
2921
+ console.error(chalk14.red(destination.reason));
2844
2922
  process.exit(1);
2845
2923
  }
2846
- if (destination.warning) console.warn(chalk13.yellow(destination.warning));
2924
+ if (destination.warning) console.warn(chalk14.yellow(destination.warning));
2847
2925
  const amount = optionalMinorUnitsOrExit(
2848
2926
  opts.amount === void 0 ? void 0 : String(opts.amount),
2849
2927
  "--amount"
@@ -2851,7 +2929,7 @@ Supported: ${SUPPORTED_EVENTS.join(", ")}`
2851
2929
  const apiKey = requireApiKey();
2852
2930
  const signing = resolveSigningSecret(opts.signingSecret, process.env, apiKey);
2853
2931
  if ("error" in signing) {
2854
- console.error(chalk13.red(signing.error));
2932
+ console.error(chalk14.red(signing.error));
2855
2933
  process.exit(1);
2856
2934
  }
2857
2935
  await confirmLiveAction(
@@ -2868,7 +2946,7 @@ Supported: ${SUPPORTED_EVENTS.join(", ")}`
2868
2946
  );
2869
2947
  const payload = buildPayload(event, { ...opts, amount });
2870
2948
  const body = JSON.stringify(payload);
2871
- console.log(chalk13.dim(signingSecretNotice(signing)));
2949
+ console.log(chalk14.dim(signingSecretNotice(signing)));
2872
2950
  const t = Math.floor(Date.now() / 1e3);
2873
2951
  const v1 = createHmac2("sha256", signing.secret).update(`${t}.${body}`).digest("hex");
2874
2952
  const signatureHeader = `t=${t},v1=${v1}`;
@@ -2890,45 +2968,45 @@ Supported: ${SUPPORTED_EVENTS.join(", ")}`
2890
2968
  spinner.stop();
2891
2969
  if (isRedirect(response)) {
2892
2970
  console.error(
2893
- chalk13.red(
2971
+ chalk14.red(
2894
2972
  `\u2717 ${event} not delivered: ${safeDisplay(String(opts.url))} answered with a redirect (HTTP ${response.status}). It was not followed, and the signed event was not sent anywhere else. Point --url at the endpoint itself.`
2895
2973
  )
2896
2974
  );
2897
2975
  process.exit(1);
2898
2976
  }
2899
2977
  if (response.ok) {
2900
- console.log(chalk13.green(`\u2713 ${event} delivered \u2192 ${response.status}`));
2978
+ console.log(chalk14.green(`\u2713 ${event} delivered \u2192 ${response.status}`));
2901
2979
  } else {
2902
2980
  console.log(
2903
- chalk13.yellow(
2981
+ chalk14.yellow(
2904
2982
  `\u26A0 ${event} delivered but got ${response.status} response`
2905
2983
  )
2906
2984
  );
2907
2985
  }
2908
- console.log(chalk13.dim(` Event: ${payload.id} (${payload.type})`));
2986
+ console.log(chalk14.dim(` Event: ${payload.id} (${payload.type})`));
2909
2987
  console.log(
2910
- chalk13.dim(
2988
+ chalk14.dim(
2911
2989
  ` Amount: ${payload.data.amount} ${payload.data.currency}`
2912
2990
  )
2913
2991
  );
2914
2992
  console.log(
2915
- chalk13.dim(
2993
+ chalk14.dim(
2916
2994
  `
2917
2995
  Verify with: vonpay.webhooks.constructEvent(rawBody, signatureHeader, secret)`
2918
2996
  )
2919
2997
  );
2920
2998
  console.log(
2921
- chalk13.dim(
2999
+ chalk14.dim(
2922
3000
  ` ('secret' is this run's signing secret, printed above \u2014 production webhooks are signed with your endpoint's whsec_\u2026 secret, and never with your API key)`
2923
3001
  )
2924
3002
  );
2925
3003
  } catch (err) {
2926
3004
  spinner.stop();
2927
3005
  if (err instanceof Error && err.name === "TimeoutError") {
2928
- console.error(chalk13.red(`Timeout: ${opts.url} did not respond within 10s`));
3006
+ console.error(chalk14.red(`Timeout: ${opts.url} did not respond within 10s`));
2929
3007
  } else {
2930
3008
  console.error(
2931
- chalk13.red(
3009
+ chalk14.red(
2932
3010
  `Failed to deliver: ${err instanceof Error ? err.message : err}`
2933
3011
  )
2934
3012
  );
@@ -2939,7 +3017,7 @@ Supported: ${SUPPORTED_EVENTS.join(", ")}`
2939
3017
  }
2940
3018
 
2941
3019
  // src/commands/listen.ts
2942
- import { Command as Command10 } from "commander";
3020
+ import { Command as Command11 } from "commander";
2943
3021
  import { VonPayCheckout as VonPayCheckout2 } from "@vonpay/checkout-node";
2944
3022
 
2945
3023
  // ../sse-parser/dist/index.js
@@ -3205,14 +3283,14 @@ var SseRedirectError = class extends Error {
3205
3283
  status;
3206
3284
  };
3207
3285
  function sleep(ms, signal) {
3208
- return new Promise((resolve3) => {
3286
+ return new Promise((resolve2) => {
3209
3287
  const timer = setTimeout(() => {
3210
3288
  signal.removeEventListener("abort", onAbort);
3211
- resolve3();
3289
+ resolve2();
3212
3290
  }, ms);
3213
3291
  const onAbort = () => {
3214
3292
  clearTimeout(timer);
3215
- resolve3();
3293
+ resolve2();
3216
3294
  };
3217
3295
  signal.addEventListener("abort", onAbort, { once: true });
3218
3296
  });
@@ -3231,7 +3309,7 @@ function noop() {
3231
3309
  }
3232
3310
 
3233
3311
  // src/utils/listen-output.ts
3234
- import chalk14 from "chalk";
3312
+ import chalk15 from "chalk";
3235
3313
  function parseAttemptPayload(data) {
3236
3314
  let raw;
3237
3315
  try {
@@ -3248,21 +3326,21 @@ function parseAttemptPayload(data) {
3248
3326
  }
3249
3327
  function formatAttemptHuman(payload, forward) {
3250
3328
  const ts = formatTimestamp(payload.timestamp);
3251
- const eventType = chalk14.cyan(safeInline(payload.event_type, 64).padEnd(24));
3252
- const eventId = chalk14.gray(safeInline(payload.event_id, 64).padEnd(28));
3253
- const attempt = chalk14.gray(`attempt ${payload.attempt}`);
3254
- const arrow = chalk14.gray("\u2192");
3329
+ const eventType = chalk15.cyan(safeInline(payload.event_type, 64).padEnd(24));
3330
+ const eventId = chalk15.gray(safeInline(payload.event_id, 64).padEnd(28));
3331
+ const attempt = chalk15.gray(`attempt ${payload.attempt}`);
3332
+ const arrow = chalk15.gray("\u2192");
3255
3333
  const upstream = formatUpstream(payload);
3256
- const latency = chalk14.gray(`${payload.latency_ms}ms`);
3257
- const liveBadge = payload.livemode ? chalk14.red(" [LIVE]") : "";
3334
+ const latency = chalk15.gray(`${payload.latency_ms}ms`);
3335
+ const liveBadge = payload.livemode ? chalk15.red(" [LIVE]") : "";
3258
3336
  let line = `[${ts}] ${eventType} ${eventId} ${attempt} ${arrow} ${upstream} ${latency}${liveBadge}`;
3259
3337
  if (forward && forward.attempted) {
3260
- const fwdArrow = chalk14.gray("\u2192 local");
3338
+ const fwdArrow = chalk15.gray("\u2192 local");
3261
3339
  if (forward.error) {
3262
- line += ` ${fwdArrow} ${chalk14.red(safeInline(forward.error))}`;
3340
+ line += ` ${fwdArrow} ${chalk15.red(safeInline(forward.error))}`;
3263
3341
  } else if (forward.status !== null) {
3264
3342
  const fwdStatus = formatStatus(forward.status);
3265
- const fwdLatency = chalk14.gray(`${forward.latencyMs}ms`);
3343
+ const fwdLatency = chalk15.gray(`${forward.latencyMs}ms`);
3266
3344
  line += ` ${fwdArrow} ${fwdStatus} ${fwdLatency}`;
3267
3345
  }
3268
3346
  }
@@ -3288,24 +3366,27 @@ function formatAttemptJson(payload, forward) {
3288
3366
  error: forward.error === null ? null : safeInline(forward.error)
3289
3367
  };
3290
3368
  }
3291
- return JSON.stringify(body);
3369
+ return JSON.stringify(cleanJsonValue(body));
3292
3370
  }
3371
+ var LIFECYCLE_EXACT_KEYS = /* @__PURE__ */ new Set(["signing_secret"]);
3293
3372
  function formatLifecycleHuman(message, kind) {
3294
- const tag = kind === "info" ? chalk14.cyan("[vonpay]") : kind === "warn" ? chalk14.yellow("[vonpay]") : chalk14.red("[vonpay]");
3373
+ const tag = kind === "info" ? chalk15.cyan("[vonpay]") : kind === "warn" ? chalk15.yellow("[vonpay]") : chalk15.red("[vonpay]");
3295
3374
  return `${tag} ${message}`;
3296
3375
  }
3297
3376
  function formatLifecycleJson(kind, extra = {}) {
3298
- return JSON.stringify({ type: `cli.${kind}`, ...extra, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
3377
+ return JSON.stringify(
3378
+ cleanJsonValue({ type: `cli.${kind}`, ...extra, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, { exactKeys: LIFECYCLE_EXACT_KEYS })
3379
+ );
3299
3380
  }
3300
3381
  function formatStatus(status) {
3301
- if (status >= 200 && status < 300) return chalk14.green(String(status));
3302
- if (status >= 400 && status < 500) return chalk14.yellow(String(status));
3303
- if (status >= 500) return chalk14.red(String(status));
3304
- return chalk14.gray(String(status));
3382
+ if (status >= 200 && status < 300) return chalk15.green(String(status));
3383
+ if (status >= 400 && status < 500) return chalk15.yellow(String(status));
3384
+ if (status >= 500) return chalk15.red(String(status));
3385
+ return chalk15.gray(String(status));
3305
3386
  }
3306
3387
  function formatUpstream(payload) {
3307
3388
  if (payload.response_status === null) {
3308
- return chalk14.red("timeout");
3389
+ return chalk15.red("timeout");
3309
3390
  }
3310
3391
  return formatStatus(payload.response_status);
3311
3392
  }
@@ -3320,7 +3401,7 @@ function formatTimestamp(iso) {
3320
3401
 
3321
3402
  // src/commands/listen.ts
3322
3403
  function listenCommand() {
3323
- return new Command10("listen").description(
3404
+ return new Command11("listen").description(
3324
3405
  "Watch webhook delivery attempts in real time. Optionally re-forward each event to a local URL for development."
3325
3406
  ).option(
3326
3407
  "--forward-to <url>",
@@ -3568,20 +3649,20 @@ function lifecycleSeverity(kind) {
3568
3649
  }
3569
3650
 
3570
3651
  // src/commands/health.ts
3571
- import { Command as Command11 } from "commander";
3572
- import chalk15 from "chalk";
3652
+ import { Command as Command12 } from "commander";
3653
+ import chalk16 from "chalk";
3573
3654
  import { VonPayError as VonPayError3 } from "@vonpay/checkout-node";
3574
3655
  function healthCommand() {
3575
- return new Command11("health").description("Check checkout API health").option("--json", "Output raw JSON").action(async (opts) => {
3656
+ return new Command12("health").description("Check checkout API health").option("--json", "Output raw JSON").action(async (opts) => {
3576
3657
  const client = makeCheckoutClient();
3577
3658
  const spinner = startSpinner("Checking API health...");
3578
3659
  try {
3579
3660
  const health = await client.health();
3580
3661
  spinner.stop();
3581
3662
  if (opts.json) {
3582
- console.log(JSON.stringify(health, null, 2));
3663
+ console.log(jsonOutput(health));
3583
3664
  } else {
3584
- const statusColor = health.status === "ok" ? chalk15.green : health.status === "degraded" ? chalk15.yellow : chalk15.red;
3665
+ const statusColor = health.status === "ok" ? chalk16.green : health.status === "degraded" ? chalk16.yellow : chalk16.red;
3585
3666
  console.log(` Status: ${statusColor(safeInline(String(health.status)))}`);
3586
3667
  console.log(` Latency: ${safeInline(String(health.latencyMs))}ms`);
3587
3668
  if (health.version) {
@@ -3593,11 +3674,11 @@ function healthCommand() {
3593
3674
  auditError(err);
3594
3675
  if (jsonErrorsEnabled()) handleError(err);
3595
3676
  if (err instanceof VonPayError3) {
3596
- console.error(chalk15.red(`API error: ${scrubSecrets(safeInline(err.message, 2e3))}`));
3597
- console.error(chalk15.dim(` Code: ${safeInline(String(err.code))}`));
3677
+ console.error(chalk16.red(`API error: ${scrubSecrets(safeInline(err.message, 2e3))}`));
3678
+ console.error(chalk16.dim(` Code: ${safeInline(String(err.code))}`));
3598
3679
  } else {
3599
3680
  console.error(
3600
- chalk15.red(
3681
+ chalk16.red(
3601
3682
  `Cannot reach API: ${scrubSecrets(safeInline(err instanceof Error ? err.message : String(err), 2e3))}`
3602
3683
  )
3603
3684
  );
@@ -3608,10 +3689,10 @@ function healthCommand() {
3608
3689
  }
3609
3690
 
3610
3691
  // src/commands/doctor.ts
3611
- import { Command as Command12 } from "commander";
3612
- import chalk16 from "chalk";
3692
+ import { Command as Command13 } from "commander";
3693
+ import chalk17 from "chalk";
3613
3694
  import ora3 from "ora";
3614
- import { homedir as homedir3 } from "os";
3695
+ import { homedir as homedir2 } from "os";
3615
3696
  import { existsSync as existsSync3 } from "fs";
3616
3697
  import {
3617
3698
  VonPayCheckout as VonPayCheckout3,
@@ -3627,7 +3708,7 @@ var RELEVANT_ENV_VARS = [
3627
3708
  "NEXT_PUBLIC_BASE_URL"
3628
3709
  ];
3629
3710
  function redactHome(p) {
3630
- const home = homedir3();
3711
+ const home = homedir2();
3631
3712
  if (home && p.startsWith(home)) {
3632
3713
  return "~" + p.slice(home.length);
3633
3714
  }
@@ -3639,7 +3720,8 @@ function maskPrefix(key) {
3639
3720
  if (!m) return void 0;
3640
3721
  return `${m[1]}${m[3]}\u2026`;
3641
3722
  }
3642
- function keyMode(key) {
3723
+ var DOCTOR_JSON_CLEANING = { exactKeys: /* @__PURE__ */ new Set(["apiKeyPrefix"]) };
3724
+ function keyMode2(key) {
3643
3725
  if (!key) return void 0;
3644
3726
  return secretKeyMode(key) ?? "not_a_secret_key";
3645
3727
  }
@@ -3665,10 +3747,10 @@ async function gather() {
3665
3747
  present: existsSync3(
3666
3748
  `${process.env.HOME ?? process.env.USERPROFILE ?? ""}/.vonpay/config.json`
3667
3749
  ),
3668
- keyType: config.checkout?.apiKey ? keyMode(config.checkout?.apiKey) : void 0
3750
+ keyType: config.checkout?.apiKey ? keyMode2(config.checkout?.apiKey) : void 0
3669
3751
  },
3670
3752
  apiKeyPrefix: maskPrefix(apiKey),
3671
- apiKeyMode: keyMode(apiKey),
3753
+ apiKeyMode: keyMode2(apiKey),
3672
3754
  health: { baseUrl, reachable: false },
3673
3755
  hints: []
3674
3756
  };
@@ -3793,47 +3875,47 @@ async function gather() {
3793
3875
  }
3794
3876
  function renderHuman(r) {
3795
3877
  const lines = [];
3796
- lines.push(chalk16.bold.blue("vonpay doctor \u2014 diagnostic report"));
3797
- lines.push(chalk16.dim(`generated ${r.generatedAt}`));
3878
+ lines.push(chalk17.bold.blue("vonpay doctor \u2014 diagnostic report"));
3879
+ lines.push(chalk17.dim(`generated ${r.generatedAt}`));
3798
3880
  lines.push("");
3799
- lines.push(chalk16.bold("Runtime"));
3881
+ lines.push(chalk17.bold("Runtime"));
3800
3882
  lines.push(` CLI: ${r.cli.version}`);
3801
3883
  lines.push(` Node: ${r.runtime.node}`);
3802
3884
  lines.push(` Platform: ${r.runtime.platform} (${r.runtime.arch})`);
3803
3885
  lines.push(` CWD: ${r.runtime.cwd}`);
3804
3886
  lines.push("");
3805
- lines.push(chalk16.bold("API key"));
3887
+ lines.push(chalk17.bold("API key"));
3806
3888
  if (r.apiKeyPrefix) {
3807
3889
  lines.push(` Prefix: ${r.apiKeyPrefix}`);
3808
3890
  lines.push(` Mode: ${r.apiKeyMode}`);
3809
3891
  } else {
3810
- lines.push(chalk16.dim(" (no key found in env or ~/.vonpay/config.json)"));
3892
+ lines.push(chalk17.dim(" (no key found in env or ~/.vonpay/config.json)"));
3811
3893
  }
3812
3894
  lines.push("");
3813
- lines.push(chalk16.bold("Env vars present (names only)"));
3895
+ lines.push(chalk17.bold("Env vars present (names only)"));
3814
3896
  if (r.envVarsPresent.length === 0) {
3815
- lines.push(chalk16.dim(" (none of the expected vonpay env vars are set)"));
3897
+ lines.push(chalk17.dim(" (none of the expected vonpay env vars are set)"));
3816
3898
  } else {
3817
3899
  for (const n of r.envVarsPresent) lines.push(` \u2713 ${n}`);
3818
3900
  }
3819
3901
  lines.push("");
3820
- lines.push(chalk16.bold("Health probe"));
3902
+ lines.push(chalk17.bold("Health probe"));
3821
3903
  if (r.health.reachable) {
3822
- const statusColor = r.health.status === "ok" ? chalk16.green : r.health.status === "degraded" ? chalk16.yellow : chalk16.red;
3823
- lines.push(` Reachable: ${chalk16.green("yes")}`);
3904
+ const statusColor = r.health.status === "ok" ? chalk17.green : r.health.status === "degraded" ? chalk17.yellow : chalk17.red;
3905
+ lines.push(` Reachable: ${chalk17.green("yes")}`);
3824
3906
  lines.push(` Status: ${statusColor(r.health.status ?? "?")}`);
3825
3907
  lines.push(` Latency: ${r.health.latencyMs}ms`);
3826
3908
  } else {
3827
- lines.push(` Reachable: ${chalk16.red("no")}`);
3909
+ lines.push(` Reachable: ${chalk17.red("no")}`);
3828
3910
  lines.push(` Error: ${r.health.error}`);
3829
3911
  }
3830
3912
  lines.push("");
3831
3913
  if (r.validateProbe) {
3832
- lines.push(chalk16.bold("sessions.validate probe ($14.99 USD dry-run)"));
3914
+ lines.push(chalk17.bold("sessions.validate probe ($14.99 USD dry-run)"));
3833
3915
  if (r.validateProbe.ok) {
3834
- lines.push(` Result: ${chalk16.green("valid")}`);
3916
+ lines.push(` Result: ${chalk17.green("valid")}`);
3835
3917
  } else {
3836
- lines.push(` Result: ${chalk16.red("invalid")}`);
3918
+ lines.push(` Result: ${chalk17.red("invalid")}`);
3837
3919
  if (r.validateProbe.error) {
3838
3920
  lines.push(` Code: ${r.validateProbe.error.code}`);
3839
3921
  lines.push(` Action: ${r.validateProbe.error.nextAction}`);
@@ -3846,7 +3928,7 @@ function renderHuman(r) {
3846
3928
  lines.push("");
3847
3929
  }
3848
3930
  if (r.hints.length > 0) {
3849
- lines.push(chalk16.bold("Hints"));
3931
+ lines.push(chalk17.bold("Hints"));
3850
3932
  for (const h of r.hints) lines.push(` \u2022 ${h}`);
3851
3933
  lines.push("");
3852
3934
  }
@@ -3923,7 +4005,7 @@ function renderForLlm(r) {
3923
4005
  return out.join("\n");
3924
4006
  }
3925
4007
  function doctorCommand() {
3926
- return new Command12("doctor").description(
4008
+ return new Command13("doctor").description(
3927
4009
  "Diagnose your Von Payments environment \u2014 runtime, env vars, live API probe; outputs human / JSON / LLM-readable formats"
3928
4010
  ).option("--json", "Output JSON (machine-parseable)").option(
3929
4011
  "--for-llm",
@@ -3938,7 +4020,7 @@ function doctorCommand() {
3938
4020
  const report = await gather();
3939
4021
  spinner.stop();
3940
4022
  if (opts.json) {
3941
- console.log(JSON.stringify(report, null, 2));
4023
+ console.log(jsonOutput(report, DOCTOR_JSON_CLEANING));
3942
4024
  } else if (opts.forLlm) {
3943
4025
  console.log(renderForLlm(report));
3944
4026
  } else {
@@ -3960,6 +4042,7 @@ function registerCheckout(program) {
3960
4042
  checkout.addCommand(paymentIntentsCommand());
3961
4043
  checkout.addCommand(refundsCommand());
3962
4044
  checkout.addCommand(tokensCommand());
4045
+ checkout.addCommand(subscriptionProductsCommand());
3963
4046
  checkout.addCommand(capabilitiesCommand());
3964
4047
  checkout.addCommand(triggerCommand());
3965
4048
  checkout.addCommand(listenCommand());
@@ -3974,7 +4057,7 @@ function commandPath(cmd) {
3974
4057
  return names.join(" ");
3975
4058
  }
3976
4059
  function buildProgram() {
3977
- const program = new Command13();
4060
+ const program = new Command14();
3978
4061
  program.name("vonpay").description("Von Payments developer tools").version(CLI_VERSION).option(
3979
4062
  ALLOW_CUSTOM_BASE_URL_FLAG,
3980
4063
  "With VONPAY_ALLOW_CUSTOM_BASE_URL=1: send a live key to a base URL that is not a Von Payments host (a proxy you run)"