@vonpay/checkout-cli 0.18.0 → 0.20.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 +462 -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)` : "";
@@ -897,6 +851,7 @@ var FIELD_PRINTERS = {
897
851
  currentStatus: (e) => e.currentStatus ? `Current status: ${str(e.currentStatus)}` : null,
898
852
  rejectReason: (e) => e.rejectReason ? `Reject reason: ${str(e.rejectReason)}` : null,
899
853
  paymentIntent: (e) => e.paymentIntent ? `Payment intent: ${str(e.paymentIntent)}` : null,
854
+ paymentIntentStatus: (e) => e.paymentIntentStatus ? `Payment intent status: ${str(e.paymentIntentStatus)}` : null,
900
855
  // refund_target_is_duplicate: the SDK has already checked this is a
901
856
  // transaction id (vp_tx_*), so it is safe to echo as a command.
902
857
  refundInstead: (e) => e.refundInstead ? `Refund instead: vonpay checkout refunds create --transaction ${str(e.refundInstead)}` : null,
@@ -970,16 +925,7 @@ function errorDetailLines(err) {
970
925
  }
971
926
  return lines;
972
927
  }
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
- }
928
+ var ERROR_JSON_CLEANING = { lines: "join", maxChars: ONE_LINE_MAX_CHARS, redact: scrubSecrets };
983
929
  function errorJson(err) {
984
930
  if (err instanceof VonPayError) {
985
931
  const out = { error: err.message };
@@ -988,14 +934,14 @@ function errorJson(err) {
988
934
  const value = source[field];
989
935
  if (value !== void 0) out[field] = value;
990
936
  }
991
- return scrubDeep(out);
937
+ return cleanJsonValue(out, ERROR_JSON_CLEANING);
992
938
  }
993
939
  const message = err instanceof Error ? err.message : String(err);
994
940
  return { error: scrubSecrets(oneLine(message)), code: null };
995
941
  }
996
942
  function exitWithCliError(message, text) {
997
943
  auditNote({ outcome: "refused_local", code: "local_refusal" }, true);
998
- return emitCliError({ error: scrubSecrets(message), code: null }, text);
944
+ return emitCliError({ error: cleanJsonValue(message, ERROR_JSON_CLEANING), code: null }, text);
999
945
  }
1000
946
  function handleError(err) {
1001
947
  auditError(err);
@@ -1074,7 +1020,7 @@ function makeCheckoutClient() {
1074
1020
  import { createInterface as createInterface2 } from "readline";
1075
1021
  import chalk5 from "chalk";
1076
1022
  import { Option as Option2 } from "commander";
1077
- import { isLiveKey as isLiveKey2 } from "@vonpay/checkout-node";
1023
+ import { heldAsLive, isLiveKey } from "@vonpay/checkout-node";
1078
1024
  var CONFIRM_WORD = "confirm";
1079
1025
  var terminal = {
1080
1026
  /**
@@ -1086,27 +1032,27 @@ var terminal = {
1086
1032
  },
1087
1033
  /** Ask one line on the terminal. End of input or Ctrl-C answers "" (cancel). */
1088
1034
  ask(prompt) {
1089
- return new Promise((resolve3) => {
1035
+ return new Promise((resolve2) => {
1090
1036
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
1091
1037
  let answered = false;
1092
1038
  rl.on("SIGINT", () => rl.close());
1093
1039
  rl.on("close", () => {
1094
- if (!answered) resolve3("");
1040
+ if (!answered) resolve2("");
1095
1041
  });
1096
1042
  rl.question(prompt, (answer) => {
1097
1043
  answered = true;
1098
1044
  rl.close();
1099
- resolve3(answer);
1045
+ resolve2(answer);
1100
1046
  });
1101
1047
  });
1102
1048
  }
1103
1049
  };
1104
- function keyFingerprint2(key) {
1050
+ function keyFingerprint(key) {
1105
1051
  const m = key.match(/^(vp_sk_live_)([A-Za-z0-9_-]{0,4})/);
1106
1052
  return m ? `${m[1]}${m[2]}\u2026` : "a key that is not a sandbox key";
1107
1053
  }
1108
1054
  async function confirmLiveAction(key, summary, opts = {}) {
1109
- if (!requiresLiveConfirmation(key)) return;
1055
+ if (!heldAsLive(key)) return;
1110
1056
  if (!terminal.isInteractive()) {
1111
1057
  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
1058
  auditNote({ outcome: "withheld", code: "no_terminal" });
@@ -1120,7 +1066,7 @@ async function confirmLiveAction(key, summary, opts = {}) {
1120
1066
  );
1121
1067
  }
1122
1068
  const lines = [
1123
- chalk5.bold.red(`You are about to ${summary.action} with a LIVE key (${keyFingerprint2(key)}).`),
1069
+ chalk5.bold.red(`You are about to ${summary.action} with a LIVE key (${keyFingerprint(key)}).`),
1124
1070
  // At least one space after the label, however long it is.
1125
1071
  ...(summary.details ?? []).map(([label, value]) => ` ${`${label}:`.padEnd(Math.max(17, label.length + 2))}${safeInline(value)}`),
1126
1072
  chalk5.yellow(summary.consequence ?? "This moves real money and cannot be undone from here.")
@@ -1141,7 +1087,7 @@ function retiredConfirmLiveOption() {
1141
1087
  ).hideHelp();
1142
1088
  }
1143
1089
  function warnLiveSecretFlag(key, flag) {
1144
- if (!requiresLiveConfirmation(key)) return;
1090
+ if (!heldAsLive(key)) return;
1145
1091
  console.warn(
1146
1092
  chalk5.yellow(
1147
1093
  `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 +1108,11 @@ function parseMinorUnits(raw, flag) {
1162
1108
  }
1163
1109
  return { ok: true, value };
1164
1110
  }
1111
+ function positiveWholeNumber(raw, flag) {
1112
+ const parsed = parseMinorUnits(raw, flag);
1113
+ if (!parsed.ok || parsed.value < 1) throw new Error(`${flag} must be a whole number of at least 1; got ${JSON.stringify(raw)}.`);
1114
+ return parsed.value;
1115
+ }
1165
1116
  function minorUnitsOrExit(raw, flag) {
1166
1117
  const parsed = parseMinorUnits(raw, flag);
1167
1118
  if (!parsed.ok) exitWithCliError(parsed.message, `Error: ${parsed.message}`);
@@ -1172,9 +1123,7 @@ function optionalMinorUnitsOrExit(raw, flag) {
1172
1123
  }
1173
1124
 
1174
1125
  // src/utils/mask-secrets.ts
1175
- function maskSecretsInJson(json) {
1176
- return scrubSecrets(json);
1177
- }
1126
+ var SHOW_SECRETS_CLEANING = { exactKeys: /* @__PURE__ */ new Set(["clientSecret"]) };
1178
1127
  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
1128
  function withClientSecret(session) {
1180
1129
  if (session === null || typeof session !== "object" || Array.isArray(session)) return session;
@@ -1182,9 +1131,9 @@ function withClientSecret(session) {
1182
1131
  return { ...session, clientSecret: session.clientSecret };
1183
1132
  }
1184
1133
  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);
1134
+ const body = cleanJsonValue(withClientSecret(session), showSecrets === true ? SHOW_SECRETS_CLEANING : {});
1135
+ const json = JSON.stringify(body, null, 2);
1136
+ return json === void 0 ? String(json) : json;
1188
1137
  }
1189
1138
 
1190
1139
  // src/commands/sessions.ts
@@ -1284,7 +1233,7 @@ function sessionsCommand() {
1284
1233
  const result = await client.sessions.validate(params);
1285
1234
  spinner.stop();
1286
1235
  if (opts.json) {
1287
- console.log(JSON.stringify(result, null, 2));
1236
+ console.log(jsonOutput(result));
1288
1237
  } else if (result.valid) {
1289
1238
  console.log(chalk6.green("\u2713 Valid request"));
1290
1239
  if (result.warnings.length > 0) {
@@ -1342,6 +1291,7 @@ import { formatMinorUnits as formatMinorUnits3 } from "@vonpay/checkout-node";
1342
1291
  // src/utils/idempotency.ts
1343
1292
  import { randomUUID } from "crypto";
1344
1293
  import chalk7 from "chalk";
1294
+ import { heldAsLive as heldAsLive2 } from "@vonpay/checkout-node";
1345
1295
  function resolveIdempotencyKey(flag) {
1346
1296
  const given = flag?.trim();
1347
1297
  if (given) return { key: given, generated: false };
@@ -1349,7 +1299,7 @@ function resolveIdempotencyKey(flag) {
1349
1299
  }
1350
1300
  function idempotencyKeyFor(flag, what, apiKey) {
1351
1301
  const { key, generated } = resolveIdempotencyKey(flag);
1352
- if (generated && apiKey !== void 0 && requiresLiveConfirmation(apiKey)) {
1302
+ if (generated && apiKey !== void 0 && heldAsLive2(apiKey)) {
1353
1303
  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
1304
  auditNote({ outcome: "refused_local", code: "idempotency_key_required" });
1355
1305
  exitWithCliError(message, chalk7.red(message));
@@ -1465,11 +1415,6 @@ function buildFilters(opts) {
1465
1415
  }
1466
1416
  return filters;
1467
1417
  }
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
1418
  var PAYMENT_LIST_JSON_FIELDS = [
1474
1419
  "id",
1475
1420
  "status",
@@ -1492,17 +1437,20 @@ function listItemJson(pi, includeMetadata) {
1492
1437
  const src = pi;
1493
1438
  return Object.fromEntries(PAYMENT_LIST_JSON_FIELDS.filter((k) => src[k] !== void 0).map((k) => [k, src[k]]));
1494
1439
  }
1495
- var ALL_JSON_DEFAULT_MAX_ITEMS = 1e4;
1440
+ var ALL_DEFAULT_MAX_ITEMS = 1e4;
1441
+ function stoppedAtDefaultCap() {
1442
+ 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.`;
1443
+ }
1496
1444
  function buildListParams(opts) {
1497
1445
  const params = buildFilters(opts);
1498
1446
  if (opts.status !== void 0) {
1499
1447
  params.status = parseListFlag(opts.status, "--status", { lowercase: true });
1500
1448
  }
1501
- if (opts.limit !== void 0) params.limit = wholeNumber2(opts.limit, "--limit");
1449
+ if (opts.limit !== void 0) params.limit = positiveWholeNumber(opts.limit, "--limit");
1502
1450
  if (opts.startingAfter !== void 0) params.startingAfter = opts.startingAfter;
1503
1451
  if (opts.maxItems !== void 0 && !opts.all) throw new Error("--max-items is used with --all.");
1504
1452
  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;
1453
+ const maxItems = opts.maxItems !== void 0 ? positiveWholeNumber(opts.maxItems, "--max-items") : opts.all ? ALL_DEFAULT_MAX_ITEMS : void 0;
1506
1454
  return { params, ...maxItems !== void 0 ? { maxItems } : {} };
1507
1455
  }
1508
1456
  function buildSummaryParams(opts) {
@@ -1587,7 +1535,10 @@ function addPaymentReportCommands(intents) {
1587
1535
  ).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
1536
  "--starting-after <cursor>",
1589
1537
  "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(
1538
+ ).option(
1539
+ "--all",
1540
+ `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)`
1541
+ ).option("--max-items <n>", `With --all: stop after this many payments (default ${ALL_DEFAULT_MAX_ITEMS})`).option(
1591
1542
  "--json",
1592
1543
  "Output JSON: each payment's status, outcome, amount, card, store and decline fields \u2014 not your metadata (with --all: one JSON array)"
1593
1544
  ).option("--include-metadata", "With --json: print every field of each payment, your metadata included").action(async (opts) => {
@@ -1609,12 +1560,8 @@ function addPaymentReportCommands(intents) {
1609
1560
  const items = [];
1610
1561
  for await (const pi of pager) items.push(listItemJson(pi, opts.includeMetadata));
1611
1562
  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
- }
1563
+ console.log(jsonOutput(items));
1564
+ if (opts.maxItems === void 0 && items.length >= ALL_DEFAULT_MAX_ITEMS) console.error(stoppedAtDefaultCap());
1618
1565
  return;
1619
1566
  }
1620
1567
  let count = 0;
@@ -1626,12 +1573,13 @@ function addPaymentReportCommands(intents) {
1626
1573
  spinner.stop();
1627
1574
  if (count === 0) console.log(" No payments match.");
1628
1575
  console.log(` ${count} payment(s).`);
1576
+ if (opts.maxItems === void 0 && count >= ALL_DEFAULT_MAX_ITEMS) console.error(stoppedAtDefaultCap());
1629
1577
  return;
1630
1578
  }
1631
1579
  const page = await client.paymentIntents.list(built.params);
1632
1580
  spinner.stop();
1633
1581
  if (opts.json) {
1634
- console.log(JSON.stringify({ ...page, data: page.data.map((pi) => listItemJson(pi, opts.includeMetadata)) }, null, 2));
1582
+ console.log(jsonOutput({ ...page, data: page.data.map((pi) => listItemJson(pi, opts.includeMetadata)) }));
1635
1583
  return;
1636
1584
  }
1637
1585
  for (const line of paymentListLines(page)) console.log(line);
@@ -1660,7 +1608,7 @@ function addPaymentReportCommands(intents) {
1660
1608
  const summary = await client.paymentIntents.summary(params);
1661
1609
  spinner.stop();
1662
1610
  if (opts.json) {
1663
- console.log(JSON.stringify(summary, null, 2));
1611
+ console.log(jsonOutput(summary));
1664
1612
  return;
1665
1613
  }
1666
1614
  for (const line of summaryLines(summary)) console.log(line);
@@ -1849,7 +1797,7 @@ function paymentIntentsCommand() {
1849
1797
  });
1850
1798
  spinner.stop();
1851
1799
  if (opts.json) {
1852
- console.log(JSON.stringify(intent, null, 2));
1800
+ console.log(jsonOutput(intent));
1853
1801
  } else {
1854
1802
  console.log(chalk9.green("\u2713 Payment intent created"));
1855
1803
  printPaymentIntent(intent);
@@ -1868,7 +1816,7 @@ function paymentIntentsCommand() {
1868
1816
  const intent = await client.paymentIntents.retrieve(id);
1869
1817
  spinner.stop();
1870
1818
  if (opts.json) {
1871
- console.log(JSON.stringify(intent, null, 2));
1819
+ console.log(jsonOutput(intent));
1872
1820
  } else {
1873
1821
  printPaymentIntent(intent);
1874
1822
  }
@@ -1918,7 +1866,7 @@ function paymentIntentsCommand() {
1918
1866
  );
1919
1867
  spinner.stop();
1920
1868
  if (opts.json) {
1921
- console.log(JSON.stringify(intent, null, 2));
1869
+ console.log(jsonOutput(intent));
1922
1870
  } else {
1923
1871
  const captureLabel = amountToCapture !== void 0 ? ` (partial: ${formatMinorUnits3(amountToCapture, intent.currency)})` : "";
1924
1872
  console.log(chalk9.green(`\u2713 Payment intent captured${captureLabel}`));
@@ -1957,7 +1905,7 @@ function paymentIntentsCommand() {
1957
1905
  const intent = await client.paymentIntents.void(id, { idempotencyKey });
1958
1906
  spinner.stop();
1959
1907
  if (opts.json) {
1960
- console.log(JSON.stringify(intent, null, 2));
1908
+ console.log(jsonOutput(intent));
1961
1909
  } else {
1962
1910
  console.log(chalk9.green("\u2713 Payment intent voided"));
1963
1911
  printPaymentIntent(intent);
@@ -1975,7 +1923,7 @@ function paymentIntentsCommand() {
1975
1923
  // src/commands/refunds.ts
1976
1924
  import { Command as Command6 } from "commander";
1977
1925
  import chalk10 from "chalk";
1978
- import { formatMinorUnits as formatMinorUnits4, VonPayError as VonPayError2 } from "@vonpay/checkout-node";
1926
+ import { formatMinorUnits as formatMinorUnits4, heldAsLive as heldAsLive3, VonPayError as VonPayError2 } from "@vonpay/checkout-node";
1979
1927
  function present(v) {
1980
1928
  return v !== void 0 && v.trim() !== "";
1981
1929
  }
@@ -2137,11 +2085,11 @@ function refundsCommand() {
2137
2085
  handleError(err);
2138
2086
  }
2139
2087
  const target = resolveRefundTarget(opts);
2140
- const sessionProblem = refundSessionProblem(target, opts.sessionId, requiresLiveConfirmation(apiKey));
2088
+ const sessionProblem = refundSessionProblem(target, opts.sessionId, heldAsLive3(apiKey));
2141
2089
  if (sessionProblem) exitWithCliError(sessionProblem, chalk10.red(sessionProblem));
2142
2090
  refuseUnsafeLiveDestination(apiKey);
2143
2091
  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) : [];
2092
+ const lookedUp = heldAsLive3(apiKey) && terminal.isInteractive() ? await refundLookupDetails(makeCheckoutClient(), target, opts.sessionId?.trim(), params.amount, params.currency) : [];
2145
2093
  await confirmLiveAction(
2146
2094
  apiKey,
2147
2095
  {
@@ -2166,7 +2114,7 @@ function refundsCommand() {
2166
2114
  const refund = await client.refunds.create(params, { idempotencyKey });
2167
2115
  spinner.stop();
2168
2116
  if (opts.json) {
2169
- console.log(JSON.stringify(refund, null, 2));
2117
+ console.log(jsonOutput(refund));
2170
2118
  } else {
2171
2119
  console.log(chalk10.green("\u2713 Refund created"));
2172
2120
  printRefund(refund);
@@ -2178,7 +2126,7 @@ function refundsCommand() {
2178
2126
  });
2179
2127
  refunds.command("list").description(
2180
2128
  "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(
2129
+ ).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
2130
  async (opts) => {
2183
2131
  let target;
2184
2132
  try {
@@ -2189,12 +2137,12 @@ function refundsCommand() {
2189
2137
  }
2190
2138
  let limit;
2191
2139
  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)}.`;
2140
+ try {
2141
+ limit = positiveWholeNumber(opts.limit, "--limit");
2142
+ } catch (err) {
2143
+ const message = err.message;
2195
2144
  exitWithCliError(message, chalk10.red(`Error: ${message}`));
2196
2145
  }
2197
- limit = parsed.value;
2198
2146
  }
2199
2147
  const client = makeCheckoutClient();
2200
2148
  const spinner = startSpinner("Listing refunds...");
@@ -2205,7 +2153,7 @@ function refundsCommand() {
2205
2153
  });
2206
2154
  spinner.stop();
2207
2155
  if (opts.json) {
2208
- console.log(JSON.stringify(list, null, 2));
2156
+ console.log(jsonOutput(list));
2209
2157
  return;
2210
2158
  }
2211
2159
  for (const line of refundListLines(list)) console.log(line);
@@ -2219,10 +2167,10 @@ function refundsCommand() {
2219
2167
  }
2220
2168
  function refundListLines(list) {
2221
2169
  const currency = list.data[0]?.currency;
2222
- const money = (n) => currency ? formatMinorUnits4(n, currency) : `${n} in minor units`;
2170
+ const money2 = (n) => currency ? formatMinorUnits4(n, currency) : `${n} in minor units`;
2223
2171
  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"}`
2172
+ ` Refunded so far: ${money2(list.amountRefunded)} (succeeded + still in progress)`,
2173
+ ` Still refundable: ${money2(list.remainingRefundable)}${list.refundable ? "" : " \u2014 this payment cannot be refunded now"}`
2226
2174
  ];
2227
2175
  if (list.data.length === 0) {
2228
2176
  lines.push(" No refunds on this payment.");
@@ -2292,7 +2240,7 @@ function tokensCommand() {
2292
2240
  );
2293
2241
  spinner.stop();
2294
2242
  if (opts.json) {
2295
- console.log(JSON.stringify(token, null, 2));
2243
+ console.log(jsonOutput(token));
2296
2244
  } else {
2297
2245
  console.log(chalk11.green("\u2713 Token created"));
2298
2246
  printToken(token);
@@ -2311,35 +2259,166 @@ function formatTokenCard(card) {
2311
2259
  return { card: cardLine, expires };
2312
2260
  }
2313
2261
 
2314
- // src/commands/capabilities.ts
2262
+ // src/commands/subscription-products.ts
2315
2263
  import { Command as Command8 } from "commander";
2316
2264
  import chalk12 from "chalk";
2265
+ import { formatMinorUnits as formatMinorUnits5 } from "@vonpay/checkout-node";
2266
+ var STATUSES = ["active", "archived"];
2267
+ function buildListParams2(opts) {
2268
+ const params = {};
2269
+ if (opts.productRef !== void 0) params.productRef = opts.productRef;
2270
+ if (opts.status !== void 0) {
2271
+ const status = opts.status.trim().toLowerCase();
2272
+ if (!STATUSES.includes(status)) {
2273
+ throw new Error(`--status must be active or archived; got ${JSON.stringify(opts.status)}.`);
2274
+ }
2275
+ params.status = status;
2276
+ }
2277
+ if (opts.limit !== void 0) params.limit = positiveWholeNumber(opts.limit, "--limit");
2278
+ if (opts.startingAfter !== void 0) params.startingAfter = opts.startingAfter;
2279
+ return params;
2280
+ }
2281
+ function buildTermsParams(opts) {
2282
+ if (opts.amount === void 0) return void 0;
2283
+ const parsed = parseMinorUnits(opts.amount, "--amount");
2284
+ if (!parsed.ok) throw new Error(parsed.message);
2285
+ if (parsed.value < 1) throw new Error(`--amount must be at least 1 (minor units); got ${JSON.stringify(opts.amount)}.`);
2286
+ return { amount: parsed.value };
2287
+ }
2288
+ var LEGAL_TEXT_MAX_CHARS = 500;
2289
+ var CUT_MARK = "\u2026 [CUT: not the full text; use --json to read all of it]";
2290
+ function legalText(raw) {
2291
+ const clean = safeInline(raw, Number.MAX_SAFE_INTEGER);
2292
+ return clean.length > LEGAL_TEXT_MAX_CHARS ? { text: `${clean.slice(0, LEGAL_TEXT_MAX_CHARS)}${CUT_MARK}`, cut: true } : { text: clean, cut: false };
2293
+ }
2294
+ function money(amount, currency) {
2295
+ if (typeof amount !== "number") return "?";
2296
+ return currency ? safeInline(formatMinorUnits5(amount, currency)) : `${amount} in minor units`;
2297
+ }
2298
+ function every(count, interval) {
2299
+ if (!interval) return "";
2300
+ const unit = safeInline(interval);
2301
+ return typeof count === "number" && count !== 1 ? `every ${count} ${unit}s` : `every ${unit}`;
2302
+ }
2303
+ function statusTone2(status) {
2304
+ return status === "active" ? chalk12.green(status) : chalk12.yellow(status);
2305
+ }
2306
+ function productLines(p) {
2307
+ const head = ` ${safeInline(p.id)} ${statusTone2(safeInline(p.status))} ${safeInline(p.name, 120)} (ref ${safeInline(p.productRef, 128)})`;
2308
+ 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)`;
2309
+ const lines = [head, ` ${schedule}`];
2310
+ const terms = p.terms;
2311
+ if (terms && typeof terms.disclosure === "string") lines.push(` Disclosure: ${legalText(terms.disclosure).text}`);
2312
+ return lines;
2313
+ }
2314
+ function productListLines(page) {
2315
+ const lines = [];
2316
+ if (page.data.length === 0) lines.push(" No subscription products match.");
2317
+ for (const p of page.data) lines.push(...productLines(p));
2318
+ if (page.hasMore && page.nextCursor) {
2319
+ lines.push(` More: re-run with the same filters and --starting-after ${safeInline(page.nextCursor)}`);
2320
+ }
2321
+ return lines;
2322
+ }
2323
+ function termsLines(t) {
2324
+ const disclosure = legalText(t.disclosure);
2325
+ const phrase = legalText(t.pricePhrase);
2326
+ return [
2327
+ ` Product: ${safeInline(t.product)} (ref ${safeInline(t.productRef, 128)})`,
2328
+ ` Disclosure: ${disclosure.text}`,
2329
+ ` Price phrase: ${phrase.text}`,
2330
+ ` First payment: ${money(t.firstAmount, t.currency)}`,
2331
+ ` Renewal: ${money(t.amount, t.currency)} ${every(t.intervalCount, t.interval)}`,
2332
+ ` Terms version: ${safeInline(t.termsVersion)}`,
2333
+ 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."
2334
+ ];
2335
+ }
2336
+ function exitOnBadFlag2(err) {
2337
+ const message = err.message;
2338
+ exitWithCliError(message, chalk12.red(`Error: ${message}`));
2339
+ }
2340
+ function subscriptionProductsCommand() {
2341
+ const products = new Command8("subscription-products").description(
2342
+ "Read your subscription products and the renewal terms a sign-up states (read-only; products are created and changed in the dashboard)"
2343
+ );
2344
+ 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) => {
2345
+ let params;
2346
+ try {
2347
+ params = buildListParams2(opts);
2348
+ } catch (err) {
2349
+ exitOnBadFlag2(err);
2350
+ }
2351
+ const client = makeCheckoutClient();
2352
+ const spinner = startSpinner("Listing subscription products...");
2353
+ try {
2354
+ const page = await client.subscriptionProducts.list(params);
2355
+ spinner.stop();
2356
+ if (opts.json) {
2357
+ console.log(jsonOutput(page));
2358
+ return;
2359
+ }
2360
+ for (const line of productListLines(page)) console.log(line);
2361
+ } catch (err) {
2362
+ spinner.stop();
2363
+ handleError(err);
2364
+ }
2365
+ });
2366
+ products.command("terms <product>").description(
2367
+ "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"
2368
+ ).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) => {
2369
+ let params;
2370
+ try {
2371
+ params = buildTermsParams(opts);
2372
+ } catch (err) {
2373
+ exitOnBadFlag2(err);
2374
+ }
2375
+ const client = makeCheckoutClient();
2376
+ const spinner = startSpinner("Reading subscription product terms...");
2377
+ try {
2378
+ const terms = await client.subscriptionProducts.terms(product, params);
2379
+ spinner.stop();
2380
+ if (opts.json) {
2381
+ console.log(jsonOutput(terms));
2382
+ return;
2383
+ }
2384
+ for (const line of termsLines(terms)) console.log(line);
2385
+ } catch (err) {
2386
+ spinner.stop();
2387
+ handleError(err);
2388
+ }
2389
+ });
2390
+ return products;
2391
+ }
2392
+
2393
+ // src/commands/capabilities.ts
2394
+ import { Command as Command9 } from "commander";
2395
+ import chalk13 from "chalk";
2317
2396
  function fmtBool(v) {
2318
- return v ? chalk12.green("yes") : chalk12.dim("no");
2397
+ return v ? chalk13.green("yes") : chalk13.dim("no");
2319
2398
  }
2320
2399
  function fmtVoidPolicy(v) {
2321
2400
  switch (v) {
2322
2401
  case "supported":
2323
- return chalk12.green(v);
2402
+ return chalk13.green(v);
2324
2403
  case "rerouted_to_refund":
2325
- return chalk12.yellow(v);
2404
+ return chalk13.yellow(v);
2326
2405
  case "not_supported":
2327
- return chalk12.dim(v);
2406
+ return chalk13.dim(v);
2328
2407
  default:
2329
- return chalk12.dim(String(v));
2408
+ return chalk13.dim(String(v));
2330
2409
  }
2331
2410
  }
2332
2411
  function fmtDisputeReporting(v) {
2333
- return v === "tracked" ? chalk12.green(v) : chalk12.yellow(v);
2412
+ return v === "tracked" ? chalk13.green(v) : chalk13.yellow(v);
2334
2413
  }
2335
2414
  function fmtSubscriptionBilling(v) {
2336
- if (v === true) return chalk12.yellow("yes");
2337
- if (v === false) return chalk12.green("no");
2338
- return chalk12.yellow("unknown");
2415
+ if (v === true) return chalk13.yellow("yes");
2416
+ if (v === false) return chalk13.green("no");
2417
+ return chalk13.yellow("unknown");
2339
2418
  }
2340
2419
  function printCapabilities(caps) {
2341
2420
  const ops = caps.supportedOperations;
2342
- console.log(chalk12.bold("Supported operations"));
2421
+ console.log(chalk13.bold("Supported operations"));
2343
2422
  console.log(` auth/capture separation: ${fmtBool(ops.authCaptureSeparation)}`);
2344
2423
  console.log(` partial capture: ${fmtBool(ops.partialCapture)}`);
2345
2424
  console.log(` partial refund: ${fmtBool(ops.partialRefund)}`);
@@ -2354,17 +2433,17 @@ function printCapabilities(caps) {
2354
2433
  console.log();
2355
2434
  if (ops.disputeReporting !== "tracked") {
2356
2435
  console.log(
2357
- chalk12.yellow(
2436
+ chalk13.yellow(
2358
2437
  " \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
2438
  )
2360
2439
  );
2361
2440
  console.log();
2362
2441
  }
2363
- console.log(chalk12.bold("Subscription billing"));
2442
+ console.log(chalk13.bold("Subscription billing"));
2364
2443
  console.log(` Von Pay renews for you: ${fmtSubscriptionBilling(caps.subscriptionBilling?.enabled)}`);
2365
2444
  if (caps.subscriptionBilling?.enabled !== false) {
2366
2445
  console.log(
2367
- chalk12.yellow(
2446
+ chalk13.yellow(
2368
2447
  ` \u26A0 Do not run renewals from your own system while this is not "no":
2369
2448
  Von Pay may be charging these customers' renewals itself, and both
2370
2449
  would charge them. Check again at the moment you renew.`
@@ -2372,25 +2451,25 @@ function printCapabilities(caps) {
2372
2451
  );
2373
2452
  }
2374
2453
  console.log();
2375
- console.log(chalk12.bold("Settlement currencies"));
2454
+ console.log(chalk13.bold("Settlement currencies"));
2376
2455
  console.log(
2377
- ` ${caps.settlementCurrencies.length ? caps.settlementCurrencies.join(", ") : chalk12.dim("(none reported)")}`
2456
+ ` ${caps.settlementCurrencies.length ? caps.settlementCurrencies.join(", ") : chalk13.dim("(none reported)")}`
2378
2457
  );
2379
2458
  console.log();
2380
- console.log(chalk12.bold("Rate limits"));
2459
+ console.log(chalk13.bold("Rate limits"));
2381
2460
  console.log(
2382
2461
  ` payment intents / minute: ${caps.rateLimits.paymentIntentsPerMinute}`
2383
2462
  );
2384
2463
  }
2385
2464
  function capabilitiesCommand() {
2386
- return new Command8("capabilities").description("Show what this merchant's binder supports").option("--json", "Output raw JSON").action(async (opts) => {
2465
+ return new Command9("capabilities").description("Show what this merchant's binder supports").option("--json", "Output raw JSON").action(async (opts) => {
2387
2466
  const client = makeCheckoutClient();
2388
2467
  const spinner = startSpinner("Fetching capabilities...");
2389
2468
  try {
2390
2469
  const caps = await client.capabilities.get();
2391
2470
  spinner.stop();
2392
2471
  if (opts.json) {
2393
- console.log(JSON.stringify(caps, null, 2));
2472
+ console.log(jsonOutput(caps));
2394
2473
  } else {
2395
2474
  printCapabilities(caps);
2396
2475
  }
@@ -2402,9 +2481,9 @@ function capabilitiesCommand() {
2402
2481
  }
2403
2482
 
2404
2483
  // src/commands/trigger.ts
2405
- import { Command as Command9 } from "commander";
2484
+ import { Command as Command10 } from "commander";
2406
2485
  import { createHmac as createHmac2, randomUUID as randomUUID2 } from "crypto";
2407
- import chalk13 from "chalk";
2486
+ import chalk14 from "chalk";
2408
2487
  import ora2 from "ora";
2409
2488
 
2410
2489
  // src/utils/signing-secret.ts
@@ -2622,7 +2701,7 @@ function warnIfUnsubscribable(event, write = (m) => console.warn(m)) {
2622
2701
  if (!UNSUBSCRIBABLE_EVENTS.includes(event)) return false;
2623
2702
  const suggestion = event === "session.succeeded" ? "charge.succeeded" : "charge.failed";
2624
2703
  write(
2625
- chalk13.yellow(
2704
+ chalk14.yellow(
2626
2705
  `\u26A0 ${event} is emitted internally but is NOT subscribable.
2627
2706
  The subscription API accepts the key, stores nothing and returns success, so a
2628
2707
  real endpoint receives this event NEVER \u2014 a handler tested only against it is
@@ -2817,7 +2896,7 @@ function buildPayload(event, opts) {
2817
2896
  };
2818
2897
  }
2819
2898
  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(
2899
+ 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
2900
  "--url-tunnel",
2822
2901
  "Allow a non-loopback --url (ngrok / Cloudflare Tunnel). Without it, --url must be this machine"
2823
2902
  ).option(
@@ -2826,7 +2905,7 @@ function triggerCommand() {
2826
2905
  ).addOption(retiredConfirmLiveOption()).action(async (event, opts) => {
2827
2906
  if (!SUPPORTED_EVENTS.includes(event)) {
2828
2907
  console.error(
2829
- chalk13.red(
2908
+ chalk14.red(
2830
2909
  `Unknown event: ${event}
2831
2910
  Supported: ${SUPPORTED_EVENTS.join(", ")}`
2832
2911
  )
@@ -2840,10 +2919,10 @@ Supported: ${SUPPORTED_EVENTS.join(", ")}`
2840
2919
  tunnelFlag: "--url-tunnel"
2841
2920
  });
2842
2921
  if (!destination.ok) {
2843
- console.error(chalk13.red(destination.reason));
2922
+ console.error(chalk14.red(destination.reason));
2844
2923
  process.exit(1);
2845
2924
  }
2846
- if (destination.warning) console.warn(chalk13.yellow(destination.warning));
2925
+ if (destination.warning) console.warn(chalk14.yellow(destination.warning));
2847
2926
  const amount = optionalMinorUnitsOrExit(
2848
2927
  opts.amount === void 0 ? void 0 : String(opts.amount),
2849
2928
  "--amount"
@@ -2851,7 +2930,7 @@ Supported: ${SUPPORTED_EVENTS.join(", ")}`
2851
2930
  const apiKey = requireApiKey();
2852
2931
  const signing = resolveSigningSecret(opts.signingSecret, process.env, apiKey);
2853
2932
  if ("error" in signing) {
2854
- console.error(chalk13.red(signing.error));
2933
+ console.error(chalk14.red(signing.error));
2855
2934
  process.exit(1);
2856
2935
  }
2857
2936
  await confirmLiveAction(
@@ -2868,7 +2947,7 @@ Supported: ${SUPPORTED_EVENTS.join(", ")}`
2868
2947
  );
2869
2948
  const payload = buildPayload(event, { ...opts, amount });
2870
2949
  const body = JSON.stringify(payload);
2871
- console.log(chalk13.dim(signingSecretNotice(signing)));
2950
+ console.log(chalk14.dim(signingSecretNotice(signing)));
2872
2951
  const t = Math.floor(Date.now() / 1e3);
2873
2952
  const v1 = createHmac2("sha256", signing.secret).update(`${t}.${body}`).digest("hex");
2874
2953
  const signatureHeader = `t=${t},v1=${v1}`;
@@ -2890,45 +2969,45 @@ Supported: ${SUPPORTED_EVENTS.join(", ")}`
2890
2969
  spinner.stop();
2891
2970
  if (isRedirect(response)) {
2892
2971
  console.error(
2893
- chalk13.red(
2972
+ chalk14.red(
2894
2973
  `\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
2974
  )
2896
2975
  );
2897
2976
  process.exit(1);
2898
2977
  }
2899
2978
  if (response.ok) {
2900
- console.log(chalk13.green(`\u2713 ${event} delivered \u2192 ${response.status}`));
2979
+ console.log(chalk14.green(`\u2713 ${event} delivered \u2192 ${response.status}`));
2901
2980
  } else {
2902
2981
  console.log(
2903
- chalk13.yellow(
2982
+ chalk14.yellow(
2904
2983
  `\u26A0 ${event} delivered but got ${response.status} response`
2905
2984
  )
2906
2985
  );
2907
2986
  }
2908
- console.log(chalk13.dim(` Event: ${payload.id} (${payload.type})`));
2987
+ console.log(chalk14.dim(` Event: ${payload.id} (${payload.type})`));
2909
2988
  console.log(
2910
- chalk13.dim(
2989
+ chalk14.dim(
2911
2990
  ` Amount: ${payload.data.amount} ${payload.data.currency}`
2912
2991
  )
2913
2992
  );
2914
2993
  console.log(
2915
- chalk13.dim(
2994
+ chalk14.dim(
2916
2995
  `
2917
2996
  Verify with: vonpay.webhooks.constructEvent(rawBody, signatureHeader, secret)`
2918
2997
  )
2919
2998
  );
2920
2999
  console.log(
2921
- chalk13.dim(
3000
+ chalk14.dim(
2922
3001
  ` ('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
3002
  )
2924
3003
  );
2925
3004
  } catch (err) {
2926
3005
  spinner.stop();
2927
3006
  if (err instanceof Error && err.name === "TimeoutError") {
2928
- console.error(chalk13.red(`Timeout: ${opts.url} did not respond within 10s`));
3007
+ console.error(chalk14.red(`Timeout: ${opts.url} did not respond within 10s`));
2929
3008
  } else {
2930
3009
  console.error(
2931
- chalk13.red(
3010
+ chalk14.red(
2932
3011
  `Failed to deliver: ${err instanceof Error ? err.message : err}`
2933
3012
  )
2934
3013
  );
@@ -2939,7 +3018,7 @@ Supported: ${SUPPORTED_EVENTS.join(", ")}`
2939
3018
  }
2940
3019
 
2941
3020
  // src/commands/listen.ts
2942
- import { Command as Command10 } from "commander";
3021
+ import { Command as Command11 } from "commander";
2943
3022
  import { VonPayCheckout as VonPayCheckout2 } from "@vonpay/checkout-node";
2944
3023
 
2945
3024
  // ../sse-parser/dist/index.js
@@ -3205,14 +3284,14 @@ var SseRedirectError = class extends Error {
3205
3284
  status;
3206
3285
  };
3207
3286
  function sleep(ms, signal) {
3208
- return new Promise((resolve3) => {
3287
+ return new Promise((resolve2) => {
3209
3288
  const timer = setTimeout(() => {
3210
3289
  signal.removeEventListener("abort", onAbort);
3211
- resolve3();
3290
+ resolve2();
3212
3291
  }, ms);
3213
3292
  const onAbort = () => {
3214
3293
  clearTimeout(timer);
3215
- resolve3();
3294
+ resolve2();
3216
3295
  };
3217
3296
  signal.addEventListener("abort", onAbort, { once: true });
3218
3297
  });
@@ -3231,7 +3310,7 @@ function noop() {
3231
3310
  }
3232
3311
 
3233
3312
  // src/utils/listen-output.ts
3234
- import chalk14 from "chalk";
3313
+ import chalk15 from "chalk";
3235
3314
  function parseAttemptPayload(data) {
3236
3315
  let raw;
3237
3316
  try {
@@ -3248,21 +3327,21 @@ function parseAttemptPayload(data) {
3248
3327
  }
3249
3328
  function formatAttemptHuman(payload, forward) {
3250
3329
  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");
3330
+ const eventType = chalk15.cyan(safeInline(payload.event_type, 64).padEnd(24));
3331
+ const eventId = chalk15.gray(safeInline(payload.event_id, 64).padEnd(28));
3332
+ const attempt = chalk15.gray(`attempt ${payload.attempt}`);
3333
+ const arrow = chalk15.gray("\u2192");
3255
3334
  const upstream = formatUpstream(payload);
3256
- const latency = chalk14.gray(`${payload.latency_ms}ms`);
3257
- const liveBadge = payload.livemode ? chalk14.red(" [LIVE]") : "";
3335
+ const latency = chalk15.gray(`${payload.latency_ms}ms`);
3336
+ const liveBadge = payload.livemode ? chalk15.red(" [LIVE]") : "";
3258
3337
  let line = `[${ts}] ${eventType} ${eventId} ${attempt} ${arrow} ${upstream} ${latency}${liveBadge}`;
3259
3338
  if (forward && forward.attempted) {
3260
- const fwdArrow = chalk14.gray("\u2192 local");
3339
+ const fwdArrow = chalk15.gray("\u2192 local");
3261
3340
  if (forward.error) {
3262
- line += ` ${fwdArrow} ${chalk14.red(safeInline(forward.error))}`;
3341
+ line += ` ${fwdArrow} ${chalk15.red(safeInline(forward.error))}`;
3263
3342
  } else if (forward.status !== null) {
3264
3343
  const fwdStatus = formatStatus(forward.status);
3265
- const fwdLatency = chalk14.gray(`${forward.latencyMs}ms`);
3344
+ const fwdLatency = chalk15.gray(`${forward.latencyMs}ms`);
3266
3345
  line += ` ${fwdArrow} ${fwdStatus} ${fwdLatency}`;
3267
3346
  }
3268
3347
  }
@@ -3288,24 +3367,27 @@ function formatAttemptJson(payload, forward) {
3288
3367
  error: forward.error === null ? null : safeInline(forward.error)
3289
3368
  };
3290
3369
  }
3291
- return JSON.stringify(body);
3370
+ return JSON.stringify(cleanJsonValue(body));
3292
3371
  }
3372
+ var LIFECYCLE_EXACT_KEYS = /* @__PURE__ */ new Set(["signing_secret"]);
3293
3373
  function formatLifecycleHuman(message, kind) {
3294
- const tag = kind === "info" ? chalk14.cyan("[vonpay]") : kind === "warn" ? chalk14.yellow("[vonpay]") : chalk14.red("[vonpay]");
3374
+ const tag = kind === "info" ? chalk15.cyan("[vonpay]") : kind === "warn" ? chalk15.yellow("[vonpay]") : chalk15.red("[vonpay]");
3295
3375
  return `${tag} ${message}`;
3296
3376
  }
3297
3377
  function formatLifecycleJson(kind, extra = {}) {
3298
- return JSON.stringify({ type: `cli.${kind}`, ...extra, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
3378
+ return JSON.stringify(
3379
+ cleanJsonValue({ type: `cli.${kind}`, ...extra, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, { exactKeys: LIFECYCLE_EXACT_KEYS })
3380
+ );
3299
3381
  }
3300
3382
  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));
3383
+ if (status >= 200 && status < 300) return chalk15.green(String(status));
3384
+ if (status >= 400 && status < 500) return chalk15.yellow(String(status));
3385
+ if (status >= 500) return chalk15.red(String(status));
3386
+ return chalk15.gray(String(status));
3305
3387
  }
3306
3388
  function formatUpstream(payload) {
3307
3389
  if (payload.response_status === null) {
3308
- return chalk14.red("timeout");
3390
+ return chalk15.red("timeout");
3309
3391
  }
3310
3392
  return formatStatus(payload.response_status);
3311
3393
  }
@@ -3320,7 +3402,7 @@ function formatTimestamp(iso) {
3320
3402
 
3321
3403
  // src/commands/listen.ts
3322
3404
  function listenCommand() {
3323
- return new Command10("listen").description(
3405
+ return new Command11("listen").description(
3324
3406
  "Watch webhook delivery attempts in real time. Optionally re-forward each event to a local URL for development."
3325
3407
  ).option(
3326
3408
  "--forward-to <url>",
@@ -3568,20 +3650,20 @@ function lifecycleSeverity(kind) {
3568
3650
  }
3569
3651
 
3570
3652
  // src/commands/health.ts
3571
- import { Command as Command11 } from "commander";
3572
- import chalk15 from "chalk";
3653
+ import { Command as Command12 } from "commander";
3654
+ import chalk16 from "chalk";
3573
3655
  import { VonPayError as VonPayError3 } from "@vonpay/checkout-node";
3574
3656
  function healthCommand() {
3575
- return new Command11("health").description("Check checkout API health").option("--json", "Output raw JSON").action(async (opts) => {
3657
+ return new Command12("health").description("Check checkout API health").option("--json", "Output raw JSON").action(async (opts) => {
3576
3658
  const client = makeCheckoutClient();
3577
3659
  const spinner = startSpinner("Checking API health...");
3578
3660
  try {
3579
3661
  const health = await client.health();
3580
3662
  spinner.stop();
3581
3663
  if (opts.json) {
3582
- console.log(JSON.stringify(health, null, 2));
3664
+ console.log(jsonOutput(health));
3583
3665
  } else {
3584
- const statusColor = health.status === "ok" ? chalk15.green : health.status === "degraded" ? chalk15.yellow : chalk15.red;
3666
+ const statusColor = health.status === "ok" ? chalk16.green : health.status === "degraded" ? chalk16.yellow : chalk16.red;
3585
3667
  console.log(` Status: ${statusColor(safeInline(String(health.status)))}`);
3586
3668
  console.log(` Latency: ${safeInline(String(health.latencyMs))}ms`);
3587
3669
  if (health.version) {
@@ -3593,11 +3675,11 @@ function healthCommand() {
3593
3675
  auditError(err);
3594
3676
  if (jsonErrorsEnabled()) handleError(err);
3595
3677
  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))}`));
3678
+ console.error(chalk16.red(`API error: ${scrubSecrets(safeInline(err.message, 2e3))}`));
3679
+ console.error(chalk16.dim(` Code: ${safeInline(String(err.code))}`));
3598
3680
  } else {
3599
3681
  console.error(
3600
- chalk15.red(
3682
+ chalk16.red(
3601
3683
  `Cannot reach API: ${scrubSecrets(safeInline(err instanceof Error ? err.message : String(err), 2e3))}`
3602
3684
  )
3603
3685
  );
@@ -3608,10 +3690,10 @@ function healthCommand() {
3608
3690
  }
3609
3691
 
3610
3692
  // src/commands/doctor.ts
3611
- import { Command as Command12 } from "commander";
3612
- import chalk16 from "chalk";
3693
+ import { Command as Command13 } from "commander";
3694
+ import chalk17 from "chalk";
3613
3695
  import ora3 from "ora";
3614
- import { homedir as homedir3 } from "os";
3696
+ import { homedir as homedir2 } from "os";
3615
3697
  import { existsSync as existsSync3 } from "fs";
3616
3698
  import {
3617
3699
  VonPayCheckout as VonPayCheckout3,
@@ -3627,7 +3709,7 @@ var RELEVANT_ENV_VARS = [
3627
3709
  "NEXT_PUBLIC_BASE_URL"
3628
3710
  ];
3629
3711
  function redactHome(p) {
3630
- const home = homedir3();
3712
+ const home = homedir2();
3631
3713
  if (home && p.startsWith(home)) {
3632
3714
  return "~" + p.slice(home.length);
3633
3715
  }
@@ -3639,7 +3721,8 @@ function maskPrefix(key) {
3639
3721
  if (!m) return void 0;
3640
3722
  return `${m[1]}${m[3]}\u2026`;
3641
3723
  }
3642
- function keyMode(key) {
3724
+ var DOCTOR_JSON_CLEANING = { exactKeys: /* @__PURE__ */ new Set(["apiKeyPrefix"]) };
3725
+ function keyMode2(key) {
3643
3726
  if (!key) return void 0;
3644
3727
  return secretKeyMode(key) ?? "not_a_secret_key";
3645
3728
  }
@@ -3665,10 +3748,10 @@ async function gather() {
3665
3748
  present: existsSync3(
3666
3749
  `${process.env.HOME ?? process.env.USERPROFILE ?? ""}/.vonpay/config.json`
3667
3750
  ),
3668
- keyType: config.checkout?.apiKey ? keyMode(config.checkout?.apiKey) : void 0
3751
+ keyType: config.checkout?.apiKey ? keyMode2(config.checkout?.apiKey) : void 0
3669
3752
  },
3670
3753
  apiKeyPrefix: maskPrefix(apiKey),
3671
- apiKeyMode: keyMode(apiKey),
3754
+ apiKeyMode: keyMode2(apiKey),
3672
3755
  health: { baseUrl, reachable: false },
3673
3756
  hints: []
3674
3757
  };
@@ -3793,47 +3876,47 @@ async function gather() {
3793
3876
  }
3794
3877
  function renderHuman(r) {
3795
3878
  const lines = [];
3796
- lines.push(chalk16.bold.blue("vonpay doctor \u2014 diagnostic report"));
3797
- lines.push(chalk16.dim(`generated ${r.generatedAt}`));
3879
+ lines.push(chalk17.bold.blue("vonpay doctor \u2014 diagnostic report"));
3880
+ lines.push(chalk17.dim(`generated ${r.generatedAt}`));
3798
3881
  lines.push("");
3799
- lines.push(chalk16.bold("Runtime"));
3882
+ lines.push(chalk17.bold("Runtime"));
3800
3883
  lines.push(` CLI: ${r.cli.version}`);
3801
3884
  lines.push(` Node: ${r.runtime.node}`);
3802
3885
  lines.push(` Platform: ${r.runtime.platform} (${r.runtime.arch})`);
3803
3886
  lines.push(` CWD: ${r.runtime.cwd}`);
3804
3887
  lines.push("");
3805
- lines.push(chalk16.bold("API key"));
3888
+ lines.push(chalk17.bold("API key"));
3806
3889
  if (r.apiKeyPrefix) {
3807
3890
  lines.push(` Prefix: ${r.apiKeyPrefix}`);
3808
3891
  lines.push(` Mode: ${r.apiKeyMode}`);
3809
3892
  } else {
3810
- lines.push(chalk16.dim(" (no key found in env or ~/.vonpay/config.json)"));
3893
+ lines.push(chalk17.dim(" (no key found in env or ~/.vonpay/config.json)"));
3811
3894
  }
3812
3895
  lines.push("");
3813
- lines.push(chalk16.bold("Env vars present (names only)"));
3896
+ lines.push(chalk17.bold("Env vars present (names only)"));
3814
3897
  if (r.envVarsPresent.length === 0) {
3815
- lines.push(chalk16.dim(" (none of the expected vonpay env vars are set)"));
3898
+ lines.push(chalk17.dim(" (none of the expected vonpay env vars are set)"));
3816
3899
  } else {
3817
3900
  for (const n of r.envVarsPresent) lines.push(` \u2713 ${n}`);
3818
3901
  }
3819
3902
  lines.push("");
3820
- lines.push(chalk16.bold("Health probe"));
3903
+ lines.push(chalk17.bold("Health probe"));
3821
3904
  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")}`);
3905
+ const statusColor = r.health.status === "ok" ? chalk17.green : r.health.status === "degraded" ? chalk17.yellow : chalk17.red;
3906
+ lines.push(` Reachable: ${chalk17.green("yes")}`);
3824
3907
  lines.push(` Status: ${statusColor(r.health.status ?? "?")}`);
3825
3908
  lines.push(` Latency: ${r.health.latencyMs}ms`);
3826
3909
  } else {
3827
- lines.push(` Reachable: ${chalk16.red("no")}`);
3910
+ lines.push(` Reachable: ${chalk17.red("no")}`);
3828
3911
  lines.push(` Error: ${r.health.error}`);
3829
3912
  }
3830
3913
  lines.push("");
3831
3914
  if (r.validateProbe) {
3832
- lines.push(chalk16.bold("sessions.validate probe ($14.99 USD dry-run)"));
3915
+ lines.push(chalk17.bold("sessions.validate probe ($14.99 USD dry-run)"));
3833
3916
  if (r.validateProbe.ok) {
3834
- lines.push(` Result: ${chalk16.green("valid")}`);
3917
+ lines.push(` Result: ${chalk17.green("valid")}`);
3835
3918
  } else {
3836
- lines.push(` Result: ${chalk16.red("invalid")}`);
3919
+ lines.push(` Result: ${chalk17.red("invalid")}`);
3837
3920
  if (r.validateProbe.error) {
3838
3921
  lines.push(` Code: ${r.validateProbe.error.code}`);
3839
3922
  lines.push(` Action: ${r.validateProbe.error.nextAction}`);
@@ -3846,7 +3929,7 @@ function renderHuman(r) {
3846
3929
  lines.push("");
3847
3930
  }
3848
3931
  if (r.hints.length > 0) {
3849
- lines.push(chalk16.bold("Hints"));
3932
+ lines.push(chalk17.bold("Hints"));
3850
3933
  for (const h of r.hints) lines.push(` \u2022 ${h}`);
3851
3934
  lines.push("");
3852
3935
  }
@@ -3923,7 +4006,7 @@ function renderForLlm(r) {
3923
4006
  return out.join("\n");
3924
4007
  }
3925
4008
  function doctorCommand() {
3926
- return new Command12("doctor").description(
4009
+ return new Command13("doctor").description(
3927
4010
  "Diagnose your Von Payments environment \u2014 runtime, env vars, live API probe; outputs human / JSON / LLM-readable formats"
3928
4011
  ).option("--json", "Output JSON (machine-parseable)").option(
3929
4012
  "--for-llm",
@@ -3938,7 +4021,7 @@ function doctorCommand() {
3938
4021
  const report = await gather();
3939
4022
  spinner.stop();
3940
4023
  if (opts.json) {
3941
- console.log(JSON.stringify(report, null, 2));
4024
+ console.log(jsonOutput(report, DOCTOR_JSON_CLEANING));
3942
4025
  } else if (opts.forLlm) {
3943
4026
  console.log(renderForLlm(report));
3944
4027
  } else {
@@ -3960,6 +4043,7 @@ function registerCheckout(program) {
3960
4043
  checkout.addCommand(paymentIntentsCommand());
3961
4044
  checkout.addCommand(refundsCommand());
3962
4045
  checkout.addCommand(tokensCommand());
4046
+ checkout.addCommand(subscriptionProductsCommand());
3963
4047
  checkout.addCommand(capabilitiesCommand());
3964
4048
  checkout.addCommand(triggerCommand());
3965
4049
  checkout.addCommand(listenCommand());
@@ -3974,7 +4058,7 @@ function commandPath(cmd) {
3974
4058
  return names.join(" ");
3975
4059
  }
3976
4060
  function buildProgram() {
3977
- const program = new Command13();
4061
+ const program = new Command14();
3978
4062
  program.name("vonpay").description("Von Payments developer tools").version(CLI_VERSION).option(
3979
4063
  ALLOW_CUSTOM_BASE_URL_FLAG,
3980
4064
  "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)"