@vonpay/checkout-cli 0.13.0 → 0.15.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 (114) hide show
  1. package/README.md +30 -5
  2. package/dist/index.js +2795 -13
  3. package/package.json +6 -4
  4. package/dist/app-info.d.ts +0 -5
  5. package/dist/app-info.d.ts.map +0 -1
  6. package/dist/app-info.js +0 -11
  7. package/dist/app-info.js.map +0 -1
  8. package/dist/client-factory.d.ts +0 -10
  9. package/dist/client-factory.d.ts.map +0 -1
  10. package/dist/client-factory.js +0 -31
  11. package/dist/client-factory.js.map +0 -1
  12. package/dist/commands/capabilities.d.ts +0 -22
  13. package/dist/commands/capabilities.d.ts.map +0 -1
  14. package/dist/commands/capabilities.js +0 -123
  15. package/dist/commands/capabilities.js.map +0 -1
  16. package/dist/commands/checkout.d.ts +0 -3
  17. package/dist/commands/checkout.d.ts.map +0 -1
  18. package/dist/commands/checkout.js +0 -39
  19. package/dist/commands/checkout.js.map +0 -1
  20. package/dist/commands/doctor.d.ts +0 -59
  21. package/dist/commands/doctor.d.ts.map +0 -1
  22. package/dist/commands/doctor.js +0 -366
  23. package/dist/commands/doctor.js.map +0 -1
  24. package/dist/commands/health.d.ts +0 -3
  25. package/dist/commands/health.d.ts.map +0 -1
  26. package/dist/commands/health.js +0 -52
  27. package/dist/commands/health.js.map +0 -1
  28. package/dist/commands/init.d.ts +0 -10
  29. package/dist/commands/init.d.ts.map +0 -1
  30. package/dist/commands/init.js +0 -112
  31. package/dist/commands/init.js.map +0 -1
  32. package/dist/commands/listen.d.ts +0 -19
  33. package/dist/commands/listen.d.ts.map +0 -1
  34. package/dist/commands/listen.js +0 -249
  35. package/dist/commands/listen.js.map +0 -1
  36. package/dist/commands/login.d.ts +0 -27
  37. package/dist/commands/login.d.ts.map +0 -1
  38. package/dist/commands/login.js +0 -75
  39. package/dist/commands/login.js.map +0 -1
  40. package/dist/commands/logout.d.ts +0 -3
  41. package/dist/commands/logout.d.ts.map +0 -1
  42. package/dist/commands/logout.js +0 -23
  43. package/dist/commands/logout.js.map +0 -1
  44. package/dist/commands/payment-intents.d.ts +0 -62
  45. package/dist/commands/payment-intents.d.ts.map +0 -1
  46. package/dist/commands/payment-intents.js +0 -271
  47. package/dist/commands/payment-intents.js.map +0 -1
  48. package/dist/commands/refunds.d.ts +0 -40
  49. package/dist/commands/refunds.d.ts.map +0 -1
  50. package/dist/commands/refunds.js +0 -142
  51. package/dist/commands/refunds.js.map +0 -1
  52. package/dist/commands/sessions.d.ts +0 -16
  53. package/dist/commands/sessions.d.ts.map +0 -1
  54. package/dist/commands/sessions.js +0 -152
  55. package/dist/commands/sessions.js.map +0 -1
  56. package/dist/commands/tokens.d.ts +0 -3
  57. package/dist/commands/tokens.d.ts.map +0 -1
  58. package/dist/commands/tokens.js +0 -89
  59. package/dist/commands/tokens.js.map +0 -1
  60. package/dist/commands/trigger.d.ts +0 -38
  61. package/dist/commands/trigger.d.ts.map +0 -1
  62. package/dist/commands/trigger.js +0 -246
  63. package/dist/commands/trigger.js.map +0 -1
  64. package/dist/config.d.ts +0 -22
  65. package/dist/config.d.ts.map +0 -1
  66. package/dist/config.js +0 -72
  67. package/dist/config.js.map +0 -1
  68. package/dist/index.d.ts +0 -3
  69. package/dist/index.d.ts.map +0 -1
  70. package/dist/index.js.map +0 -1
  71. package/dist/utils/errors.d.ts +0 -80
  72. package/dist/utils/errors.d.ts.map +0 -1
  73. package/dist/utils/errors.js +0 -179
  74. package/dist/utils/errors.js.map +0 -1
  75. package/dist/utils/forward-to.d.ts +0 -38
  76. package/dist/utils/forward-to.d.ts.map +0 -1
  77. package/dist/utils/forward-to.js +0 -92
  78. package/dist/utils/forward-to.js.map +0 -1
  79. package/dist/utils/json-errors.d.ts +0 -16
  80. package/dist/utils/json-errors.d.ts.map +0 -1
  81. package/dist/utils/json-errors.js +0 -41
  82. package/dist/utils/json-errors.js.map +0 -1
  83. package/dist/utils/listen-forward.d.ts +0 -49
  84. package/dist/utils/listen-forward.d.ts.map +0 -1
  85. package/dist/utils/listen-forward.js +0 -113
  86. package/dist/utils/listen-forward.js.map +0 -1
  87. package/dist/utils/listen-output.d.ts +0 -52
  88. package/dist/utils/listen-output.d.ts.map +0 -1
  89. package/dist/utils/listen-output.js +0 -85
  90. package/dist/utils/listen-output.js.map +0 -1
  91. package/dist/utils/live-key.d.ts +0 -22
  92. package/dist/utils/live-key.d.ts.map +0 -1
  93. package/dist/utils/live-key.js +0 -37
  94. package/dist/utils/live-key.js.map +0 -1
  95. package/dist/utils/metadata.d.ts +0 -2
  96. package/dist/utils/metadata.d.ts.map +0 -1
  97. package/dist/utils/metadata.js +0 -31
  98. package/dist/utils/metadata.js.map +0 -1
  99. package/dist/utils/sanitize.d.ts +0 -4
  100. package/dist/utils/sanitize.d.ts.map +0 -1
  101. package/dist/utils/sanitize.js +0 -18
  102. package/dist/utils/sanitize.js.map +0 -1
  103. package/dist/utils/spinner.d.ts +0 -15
  104. package/dist/utils/spinner.d.ts.map +0 -1
  105. package/dist/utils/spinner.js +0 -18
  106. package/dist/utils/spinner.js.map +0 -1
  107. package/dist/utils/sse-consumer.d.ts +0 -81
  108. package/dist/utils/sse-consumer.d.ts.map +0 -1
  109. package/dist/utils/sse-consumer.js +0 -172
  110. package/dist/utils/sse-consumer.js.map +0 -1
  111. package/dist/utils/sse-parser.d.ts +0 -63
  112. package/dist/utils/sse-parser.d.ts.map +0 -1
  113. package/dist/utils/sse-parser.js +0 -149
  114. package/dist/utils/sse-parser.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,16 +1,2798 @@
1
1
  #!/usr/bin/env node
2
- import { Command } from "commander";
3
- import { readFileSync } from "node:fs";
4
- import { dirname, join } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
- import { registerCheckout } from "./commands/checkout.js";
7
- const __dirname = dirname(fileURLToPath(import.meta.url));
8
- const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
9
- const program = new Command();
10
- program
11
- .name("vonpay")
12
- .description("Von Payments developer tools")
13
- .version(pkg.version);
2
+
3
+ // src/index.ts
4
+ import { Command as Command13 } from "commander";
5
+ import { readFileSync as readFileSync4 } from "fs";
6
+ import { dirname as dirname2, join as join3 } from "path";
7
+ import { fileURLToPath as fileURLToPath2 } from "url";
8
+
9
+ // src/commands/login.ts
10
+ import { Command, Option } from "commander";
11
+ import { createInterface } from "readline";
12
+ import chalk from "chalk";
13
+
14
+ // src/config.ts
15
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
16
+ import { homedir } from "os";
17
+ import { join } from "path";
18
+
19
+ // src/utils/json-errors.ts
20
+ var jsonErrors = false;
21
+ function setJsonErrors(enabled) {
22
+ jsonErrors = enabled;
23
+ }
24
+ function jsonErrorsEnabled() {
25
+ return jsonErrors;
26
+ }
27
+ function printJsonError(obj) {
28
+ console.error(JSON.stringify(obj));
29
+ }
30
+ function emitCliError(json, text) {
31
+ if (jsonErrors) printJsonError(json);
32
+ else console.error(text);
33
+ return process.exit(1);
34
+ }
35
+
36
+ // src/utils/key-mode.ts
37
+ import { isLiveKey } from "@vonpay/checkout-node";
38
+ var TEST_SECRET_KEY_PREFIX = "vp_sk_test_";
39
+ function secretKeyMode(key) {
40
+ if (typeof key !== "string") return null;
41
+ if (isLiveKey(key)) return "live";
42
+ if (key.startsWith(TEST_SECRET_KEY_PREFIX)) return "test";
43
+ return null;
44
+ }
45
+ 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.";
46
+
47
+ // src/config.ts
48
+ var CONFIG_DIR = join(homedir(), ".vonpay");
49
+ var CONFIG_FILE = join(CONFIG_DIR, "config.json");
50
+ function loadConfig() {
51
+ if (!existsSync(CONFIG_FILE)) return {};
52
+ try {
53
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
54
+ } catch {
55
+ console.error(
56
+ "Warning: ~/.vonpay/config.json is malformed. Run `vonpay checkout login` to reset it."
57
+ );
58
+ return {};
59
+ }
60
+ }
61
+ var LiveKeyNotStoredError = class extends Error {
62
+ constructor() {
63
+ super(
64
+ "Refusing to save a live key (vp_sk_live_\u2026) to ~/.vonpay/config.json: live keys are never stored on disk. Supply it for this terminal session only, through the VON_PAY_SECRET_KEY environment variable."
65
+ );
66
+ this.name = "LiveKeyNotStoredError";
67
+ }
68
+ };
69
+ function saveConfig(config) {
70
+ const key = config.checkout?.apiKey;
71
+ if (secretKeyMode(key) === "live") {
72
+ if (key !== loadConfig().checkout?.apiKey) throw new LiveKeyNotStoredError();
73
+ warnStoredLiveKey();
74
+ }
75
+ if (!existsSync(CONFIG_DIR)) {
76
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
77
+ }
78
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", {
79
+ encoding: "utf-8",
80
+ mode: 384
81
+ });
82
+ chmodSync(CONFIG_FILE, 384);
83
+ }
84
+ function clearStoredApiKey() {
85
+ const config = loadConfig();
86
+ if (!config.checkout?.apiKey) return { removed: false };
87
+ delete config.checkout.apiKey;
88
+ if (Object.keys(config.checkout).length === 0) delete config.checkout;
89
+ if (Object.keys(config).length === 0) {
90
+ rmSync(CONFIG_FILE, { force: true });
91
+ } else {
92
+ saveConfig(config);
93
+ }
94
+ return { removed: true };
95
+ }
96
+ function getApiKey() {
97
+ const envKey = process.env.VON_PAY_SECRET_KEY;
98
+ if (envKey) return envKey;
99
+ const config = loadConfig();
100
+ const stored = config.checkout?.apiKey;
101
+ if (secretKeyMode(stored) === "live") warnStoredLiveKey();
102
+ return stored;
103
+ }
104
+ var warnedStoredLiveKey = false;
105
+ function warnStoredLiveKey() {
106
+ if (warnedStoredLiveKey) return;
107
+ warnedStoredLiveKey = true;
108
+ console.warn(
109
+ "Warning: a LIVE key is saved in plaintext in ~/.vonpay/config.json (stored by an older version of this CLI). Live keys are no longer stored. Run `vonpay checkout logout` to delete it, and supply the key through the VON_PAY_SECRET_KEY environment variable for the session instead."
110
+ );
111
+ }
112
+ function requireApiKey() {
113
+ const key = getApiKey();
114
+ if (!key) {
115
+ const message = "No API key found. Run `vonpay checkout login` or set VON_PAY_SECRET_KEY.";
116
+ emitCliError({ error: message, code: null }, message);
117
+ }
118
+ if (secretKeyMode(key) === null) {
119
+ emitCliError({ error: NOT_A_SECRET_KEY_MESSAGE, code: null }, NOT_A_SECRET_KEY_MESSAGE);
120
+ }
121
+ return key;
122
+ }
123
+
124
+ // src/commands/login.ts
125
+ var LIVE_KEY_NOT_SAVED_MESSAGE = "Live keys (vp_sk_live_\u2026) are never saved to ~/.vonpay/config.json: the file is plaintext, and a live key moves real money. Supply it for this terminal session only, through the environment, for example:\n read -rs VON_PAY_SECRET_KEY && export VON_PAY_SECRET_KEY\n(paste the key at the silent prompt; it ends with the session). Use `login` for sandbox keys (vp_sk_test_\u2026).";
126
+ function evaluateCliKeyGate(key, _opts = {}) {
127
+ if (!key) return { action: "prompt" };
128
+ const refusal = evaluateKeyToSave(key);
129
+ if (refusal) return refusal;
130
+ return {
131
+ action: "save",
132
+ warning: "Warning: passing keys as a command argument exposes them in shell history and process-list (`ps`). Use interactive mode (`vonpay checkout login`) instead."
133
+ };
134
+ }
135
+ function evaluateKeyToSave(key) {
136
+ const mode = secretKeyMode(key);
137
+ if (mode === null) return { action: "refuse", reason: NOT_A_SECRET_KEY_MESSAGE };
138
+ if (mode === "live") return { action: "refuse", reason: LIVE_KEY_NOT_SAVED_MESSAGE };
139
+ return null;
140
+ }
141
+ function readLine(prompt) {
142
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
143
+ return new Promise((resolve2) => {
144
+ rl.question(prompt, (answer) => {
145
+ rl.close();
146
+ resolve2(answer.trim());
147
+ });
148
+ });
149
+ }
150
+ function loginCommand() {
151
+ return new Command("login").description("Store a sandbox API key (vp_sk_test_\u2026) for CLI access. Live keys are never stored").argument("[key]", "Sandbox API secret key (vp_sk_test_*)").addOption(
152
+ new Option(
153
+ "--confirm-cli-exposure",
154
+ "No longer has any effect: live keys are never saved"
155
+ ).hideHelp()
156
+ ).action(async (key, opts) => {
157
+ let apiKey = key;
158
+ const verdict = evaluateCliKeyGate(key, opts);
159
+ if (verdict.action === "refuse") {
160
+ console.error(chalk.red(verdict.reason));
161
+ process.exit(1);
162
+ }
163
+ if (verdict.action === "save" && verdict.warning) {
164
+ console.warn(chalk.yellow(verdict.warning));
165
+ }
166
+ if (!apiKey) {
167
+ console.log(
168
+ chalk.dim(
169
+ "Get your API key from the Von Payments dashboard \u2192 Developers"
170
+ )
171
+ );
172
+ apiKey = await readLine("Sandbox API key (vp_sk_test_*): ");
173
+ }
174
+ if (!apiKey) {
175
+ console.error(chalk.red("No key provided."));
176
+ process.exit(1);
177
+ }
178
+ const refusal = evaluateKeyToSave(apiKey);
179
+ if (refusal) {
180
+ console.error(chalk.red(refusal.reason));
181
+ process.exit(1);
182
+ }
183
+ const config = loadConfig();
184
+ config.checkout = { ...config.checkout, apiKey };
185
+ saveConfig(config);
186
+ console.log(
187
+ chalk.green(`\u2713 API key saved (test mode) \u2192 ~/.vonpay/config.json`)
188
+ );
189
+ });
190
+ }
191
+
192
+ // src/commands/logout.ts
193
+ import { Command as Command2 } from "commander";
194
+ import chalk2 from "chalk";
195
+ function logoutCommand() {
196
+ return new Command2("logout").description("Remove the stored API key from ~/.vonpay/config.json").action(() => {
197
+ const { removed } = clearStoredApiKey();
198
+ if (removed) {
199
+ console.log(
200
+ chalk2.green("\u2713 Stored API key removed from ~/.vonpay/config.json")
201
+ );
202
+ } else {
203
+ console.log(
204
+ chalk2.dim("No stored API key found in ~/.vonpay/config.json")
205
+ );
206
+ }
207
+ if (process.env.VON_PAY_SECRET_KEY) {
208
+ console.warn(
209
+ chalk2.yellow(
210
+ "Note: VON_PAY_SECRET_KEY is still set in your environment and takes precedence over the stored config. Run `unset VON_PAY_SECRET_KEY` to fully log out of this shell."
211
+ )
212
+ );
213
+ }
214
+ });
215
+ }
216
+
217
+ // src/commands/init.ts
218
+ import { Command as Command3 } from "commander";
219
+ import { appendFileSync, chmodSync as chmodSync2, existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
220
+ import { resolve } from "path";
221
+ import chalk3 from "chalk";
222
+ 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.";
223
+ function ensureGitignoreBlocksEnv() {
224
+ if (!existsSync2(".gitignore")) return "no-repo";
225
+ if (envAlreadyIgnored(".gitignore")) {
226
+ return "already";
227
+ }
228
+ appendFileSync(".gitignore", "\n# Added by vonpay checkout init\n.env\n");
229
+ return "added";
230
+ }
231
+ var POSITIVE_ENV_PATTERNS = /* @__PURE__ */ new Set([
232
+ ".env",
233
+ "/.env",
234
+ "**/.env",
235
+ "*.env",
236
+ "**/.env*",
237
+ ".env*"
238
+ ]);
239
+ var NEGATION_ENV_PATTERNS = /* @__PURE__ */ new Set([
240
+ "!.env",
241
+ "!/.env",
242
+ "!**/.env",
243
+ "!*.env",
244
+ "!**/.env*",
245
+ "!.env*"
246
+ ]);
247
+ function envAlreadyIgnoredFromText(gitignoreText) {
248
+ const lines = gitignoreText.split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
249
+ let ignored = false;
250
+ for (const p of lines) {
251
+ if (POSITIVE_ENV_PATTERNS.has(p)) ignored = true;
252
+ else if (NEGATION_ENV_PATTERNS.has(p)) ignored = false;
253
+ }
254
+ return ignored;
255
+ }
256
+ function envAlreadyIgnored(rootIgnorePath) {
257
+ return envAlreadyIgnoredFromText(readFileSync2(rootIgnorePath, "utf-8"));
258
+ }
259
+ function initCommand() {
260
+ return new Command3("init").description("Initialize a project with Von Payments checkout").option("-f, --force", "Overwrite existing .env file").action(async (opts) => {
261
+ const apiKey = getApiKey();
262
+ if (!apiKey) {
263
+ console.error(
264
+ chalk3.red(
265
+ "No API key found. Run `vonpay checkout login` first or set VON_PAY_SECRET_KEY."
266
+ )
267
+ );
268
+ process.exit(1);
269
+ }
270
+ const envFile = ".env";
271
+ const absPath = resolve(envFile);
272
+ if (existsSync2(envFile) && !opts.force) {
273
+ console.error(
274
+ chalk3.yellow(
275
+ `.env already exists at ${absPath}. Use --force to overwrite.`
276
+ )
277
+ );
278
+ process.exit(1);
279
+ }
280
+ const mode = secretKeyMode(apiKey);
281
+ if (mode === null) {
282
+ console.error(chalk3.red(NOT_A_SECRET_KEY_MESSAGE));
283
+ process.exit(1);
284
+ }
285
+ if (mode === "live") {
286
+ console.error(chalk3.red(LIVE_KEY_NOT_WRITTEN_MESSAGE));
287
+ process.exit(1);
288
+ }
289
+ const content = [
290
+ "# Von Payments Checkout",
291
+ `# Mode: ${mode}`,
292
+ `VON_PAY_SECRET_KEY=${apiKey}`,
293
+ ""
294
+ ].join("\n");
295
+ writeFileSync2(envFile, content, { encoding: "utf-8", mode: 384 });
296
+ chmodSync2(envFile, 384);
297
+ console.log(chalk3.green(`\u2713 .env created (${mode} mode) \u2192 ${absPath}`));
298
+ const gitignoreState = ensureGitignoreBlocksEnv();
299
+ if (gitignoreState === "added") {
300
+ console.log(chalk3.dim(" Added .env to .gitignore"));
301
+ } else if (gitignoreState === "already") {
302
+ console.log(chalk3.dim(" .env already listed in .gitignore \u2713"));
303
+ } else {
304
+ console.log(
305
+ chalk3.yellow(
306
+ " No .gitignore found. If this is a git repo, add .env to your .gitignore before committing."
307
+ )
308
+ );
309
+ }
310
+ });
311
+ }
312
+
313
+ // src/commands/sessions.ts
314
+ import { Command as Command4 } from "commander";
315
+ import chalk5 from "chalk";
316
+
317
+ // src/utils/spinner.ts
318
+ import ora from "ora";
319
+ function startSpinner(text) {
320
+ return ora({ text, isSilent: jsonErrorsEnabled() }).start();
321
+ }
322
+
323
+ // src/commands/sessions.ts
324
+ import { formatMinorUnits } from "@vonpay/checkout-node";
325
+
326
+ // src/client-factory.ts
327
+ import { VonPayCheckout } from "@vonpay/checkout-node";
328
+
329
+ // src/app-info.ts
330
+ import { readFileSync as readFileSync3 } from "fs";
331
+ import { dirname, join as join2 } from "path";
332
+ import { fileURLToPath } from "url";
333
+ var pkg = JSON.parse(
334
+ readFileSync3(join2(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf-8")
335
+ );
336
+ var CLI_VERSION = pkg.version;
337
+ var CLI_APP_INFO = { name: "vonpay-cli", version: CLI_VERSION };
338
+ var SILENT_ERROR_REPORTER = () => {
339
+ };
340
+
341
+ // src/utils/base-url.ts
342
+ import { keyDestinationProblem as sdkKeyDestinationProblem } from "@vonpay/checkout-node";
343
+ var ALLOW_CUSTOM_BASE_URL_ENV = "VONPAY_ALLOW_CUSTOM_BASE_URL";
344
+ function customBaseUrlAllowed(env = process.env) {
345
+ return env[ALLOW_CUSTOM_BASE_URL_ENV] === "1";
346
+ }
347
+ var UNSAFE_URL_CHARS = /[\s\x00-\x1f\x7f\\]/;
348
+ var WHERE = "It comes from VONPAY_BASE_URL or baseUrl in ~/.vonpay/config.json; fix it, or unset it to use https://checkout.vonpay.com.";
349
+ function keyDestinationProblem(apiKey, baseUrl, env = process.env) {
350
+ if (baseUrl === void 0) return null;
351
+ if (UNSAFE_URL_CHARS.test(baseUrl)) {
352
+ return `Refusing to use the base URL: it contains spaces, control characters or backslashes. ${WHERE}`;
353
+ }
354
+ let url;
355
+ try {
356
+ url = new URL(baseUrl);
357
+ } catch {
358
+ return `Refusing to use the base URL: it is not a valid absolute URL (for example https://checkout.vonpay.com). ${WHERE}`;
359
+ }
360
+ if (url.username !== "" || url.password !== "") {
361
+ return `Refusing to use the base URL: it contains a user name or password, which would be sent with every request. ${WHERE}`;
362
+ }
363
+ const problem = sdkKeyDestinationProblem(apiKey, baseUrl, { allowCustom: customBaseUrlAllowed(env) });
364
+ return problem === null ? null : `${problem} ${WHERE}`;
365
+ }
366
+ function allowCustomBaseUrl(env = process.env) {
367
+ return customBaseUrlAllowed(env);
368
+ }
369
+
370
+ // src/utils/errors.ts
371
+ import chalk4 from "chalk";
372
+ import {
373
+ RETRY_WITHHELD_HELP,
374
+ VonPayError
375
+ } from "@vonpay/checkout-node";
376
+ var SECRET_PATTERNS = [
377
+ [/vp_sk_(?:test|live)_[A-Za-z0-9_-]+/g, "vp_sk_***"],
378
+ [/ss_(?:test|live)_[A-Za-z0-9_-]+/g, "ss_***"],
379
+ [/whsec_[A-Za-z0-9_-]+/g, "whsec_***"],
380
+ [/sk_(?:test|live)_[A-Za-z0-9_-]+/g, "sk_***"],
381
+ [/rk_(?:test|live)_[A-Za-z0-9_-]+/g, "rk_***"]
382
+ ];
383
+ function scrubSecrets(s) {
384
+ let out = s;
385
+ for (const [re, replacement] of SECRET_PATTERNS) out = out.replace(re, replacement);
386
+ return out;
387
+ }
388
+ 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).";
389
+ 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.";
390
+ 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.`;
391
+ var str = (v) => v === void 0 || v === null ? null : String(v);
392
+ var FIELD_PRINTERS = {
393
+ code: (e) => `Code: ${str(e.code)}`,
394
+ fix: (e) => `Fix: ${str(e.fix)}`,
395
+ nextAction: (e) => (
396
+ // A withheld retry carries its own (SDK) guidance on the line below.
397
+ e.nextAction === "reconcile" && e.retryWithheld !== true ? `Next action: reconcile \u2014 ${UNKNOWN_OUTCOME_GUIDANCE}` : e.nextAction ? `Next action: ${str(e.nextAction)}` : null
398
+ ),
399
+ retryable: (e) => typeof e.retryable === "boolean" ? `Retryable: ${e.retryable ? "yes" : "no \u2014 do not re-run this command as-is"}` : null,
400
+ retryWithheld: (e) => e.retryWithheld === true ? (
401
+ // What to do is the SDK's RETRY_WITHHELD_HELP, printed on the Hint line
402
+ // (the SDK puts it in llmHint; quoted here only when the error carries
403
+ // something else). This line adds the CLI commands for the read-back.
404
+ "Retry withheld: the outcome is unknown and the CLI did not retry it. " + (e.llmHint === RETRY_WITHHELD_HELP.llmHint ? "" : `${RETRY_WITHHELD_HELP.llmHint} `) + CLI_READ_BACK
405
+ ) : null,
406
+ captureOutcome: (e) => e.captureOutcome ? `Capture outcome: ${str(e.captureOutcome)} \u2014 the provider accepted the capture but it could not be recorded, so money may have moved. Do NOT capture again; read the payment intent back first.` : null,
407
+ voidOutcome: (e) => e.voidOutcome ? `Void outcome: ${str(e.voidOutcome)} \u2014 the provider accepted the void but it could not be recorded. Do NOT void again or tell the buyer the hold was released until a read-back confirms it.` : null,
408
+ currentStatus: (e) => e.currentStatus ? `Current status: ${str(e.currentStatus)}` : null,
409
+ rejectReason: (e) => e.rejectReason ? `Reject reason: ${str(e.rejectReason)}` : null,
410
+ paymentIntent: (e) => e.paymentIntent ? `Payment intent: ${str(e.paymentIntent)}` : null,
411
+ // refund_target_is_duplicate: the SDK has already checked this is a
412
+ // transaction id (vp_tx_*), so it is safe to echo as a command.
413
+ refundInstead: (e) => e.refundInstead ? `Refund instead: vonpay checkout refunds create --transaction ${str(e.refundInstead)}` : null,
414
+ reason: (e) => e.reason ? `Reason: ${str(e.reason)}` : null,
415
+ // An unrecognised expire reason reads "unknown"; show what the API sent.
416
+ rawReason: (e) => e.rawReason && e.rawReason !== e.reason ? `Reason as sent: ${str(e.rawReason)}` : null,
417
+ retryAfterSeconds: (e) => typeof e.retryAfterSeconds === "number" ? `Retry after: ${e.retryAfterSeconds} seconds \u2014 then ask again` : null,
418
+ session: (e) => e.session ? `Session: ${JSON.stringify(e.session)}` : null,
419
+ // On a withheld retry the SDK sets this to RETRY_WITHHELD_HELP.llmHint — the
420
+ // one wording for every surface — so it is printed as is.
421
+ llmHint: (e) => e.llmHint ? `Hint: ${str(e.llmHint)}` : null,
422
+ docs: (e) => e.docs ? `Docs: ${str(e.docs)}` : null,
423
+ status: (e) => typeof e.status === "number" ? `HTTP status: ${e.status}` : null,
424
+ rateLimit: (e) => e.rateLimit ? `Rate limit: ${JSON.stringify(e.rateLimit)}` : null,
425
+ requestId: (e) => e.requestId ? `Request ID: ${str(e.requestId)}` : null
426
+ };
427
+ var ERROR_FIELDS = Object.keys(FIELD_PRINTERS);
428
+ function errorDetailLines(err) {
429
+ const source = err;
430
+ const lines = [];
431
+ for (const print of Object.values(FIELD_PRINTERS)) {
432
+ const line = print(source);
433
+ if (line !== null) lines.push(` ${scrubSecrets(line)}`);
434
+ }
435
+ return lines;
436
+ }
437
+ function scrubDeep(value) {
438
+ if (typeof value === "string") return scrubSecrets(value);
439
+ if (Array.isArray(value)) return value.map(scrubDeep);
440
+ if (value !== null && typeof value === "object") {
441
+ const out = {};
442
+ for (const [k, v] of Object.entries(value)) out[k] = scrubDeep(v);
443
+ return out;
444
+ }
445
+ return value;
446
+ }
447
+ function errorJson(err) {
448
+ if (err instanceof VonPayError) {
449
+ const out = { error: err.message };
450
+ const source = err;
451
+ for (const field of ERROR_FIELDS) {
452
+ const value = source[field];
453
+ if (value !== void 0) out[field] = value;
454
+ }
455
+ return scrubDeep(out);
456
+ }
457
+ const message = err instanceof Error ? err.message : String(err);
458
+ return { error: scrubSecrets(message), code: null };
459
+ }
460
+ function exitWithCliError(message, text) {
461
+ return emitCliError({ error: scrubSecrets(message), code: null }, text);
462
+ }
463
+ function handleError(err) {
464
+ if (jsonErrorsEnabled()) {
465
+ printJsonError(errorJson(err));
466
+ process.exit(1);
467
+ }
468
+ if (err instanceof VonPayError) {
469
+ console.error(chalk4.red(`Error: ${scrubSecrets(err.message)}`));
470
+ for (const line of errorDetailLines(err)) console.error(chalk4.dim(line));
471
+ process.exit(1);
472
+ }
473
+ const message = err instanceof Error ? err.message : String(err);
474
+ console.error(chalk4.red(`Error: ${scrubSecrets(message)}`));
475
+ process.exit(1);
476
+ }
477
+
478
+ // src/client-factory.ts
479
+ function getBaseUrl() {
480
+ const envBase = process.env.VONPAY_BASE_URL;
481
+ if (envBase) return envBase;
482
+ const config = loadConfig();
483
+ return config.checkout?.baseUrl;
484
+ }
485
+ function refuseUnsafeLiveDestination(apiKey) {
486
+ const problem = keyDestinationProblem(apiKey, getBaseUrl());
487
+ if (problem) exitWithCliError(problem, problem);
488
+ }
489
+ function apiHostDetail() {
490
+ const baseUrl = getBaseUrl();
491
+ if (!baseUrl) return [];
492
+ let origin = baseUrl;
493
+ try {
494
+ origin = new URL(baseUrl).origin;
495
+ } catch {
496
+ }
497
+ return origin === "https://checkout.vonpay.com" ? [] : [["API host", origin]];
498
+ }
499
+ function makeCheckoutClient() {
500
+ const apiKey = requireApiKey();
501
+ const baseUrl = getBaseUrl();
502
+ refuseUnsafeLiveDestination(apiKey);
503
+ return new VonPayCheckout({
504
+ apiKey,
505
+ ...baseUrl ? { baseUrl } : {},
506
+ // The SDK applies the same host rule at construction; the opt-in is the
507
+ // same environment variable (real process environment only).
508
+ allowCustomBaseUrl: allowCustomBaseUrl(),
509
+ appInfo: CLI_APP_INFO,
510
+ // The CLI prints every error itself (handleError / --json). Without this the SDK's fallback
511
+ // reporter adds its own `[vonpay] …` line to stderr on EVERY API error, live key included.
512
+ errorReporter: SILENT_ERROR_REPORTER
513
+ });
514
+ }
515
+
516
+ // src/utils/amount.ts
517
+ var WHOLE_NUMBER = /^\d+$/;
518
+ function parseMinorUnits(raw, flag) {
519
+ const text = raw.trim();
520
+ const value = Number(text);
521
+ if (!WHOLE_NUMBER.test(text) || !Number.isSafeInteger(value)) {
522
+ return {
523
+ ok: false,
524
+ message: `${flag} must be a whole number in minor units (the currency's smallest unit): for example 1499 = 14.99 USD. Got ${JSON.stringify(raw)}.`
525
+ };
526
+ }
527
+ return { ok: true, value };
528
+ }
529
+ function minorUnitsOrExit(raw, flag) {
530
+ const parsed = parseMinorUnits(raw, flag);
531
+ if (!parsed.ok) exitWithCliError(parsed.message, `Error: ${parsed.message}`);
532
+ return parsed.value;
533
+ }
534
+ function optionalMinorUnitsOrExit(raw, flag) {
535
+ return raw === void 0 ? void 0 : minorUnitsOrExit(raw, flag);
536
+ }
537
+
538
+ // src/commands/sessions.ts
539
+ var makeClient = makeCheckoutClient;
540
+ function sessionLines(session) {
541
+ const statusColor = session.status === "succeeded" ? chalk5.green : session.status === "failed" ? chalk5.red : session.status === "expired" ? chalk5.gray : chalk5.yellow;
542
+ const lines = [
543
+ ` ID: ${session.id}`,
544
+ ` Status: ${statusColor(session.status)}`,
545
+ ` Amount: ${formatMinorUnits(session.amount, session.currency)}`
546
+ ];
547
+ if (session.paymentStatus) {
548
+ const paymentColor = session.paymentStatus === "paid" ? chalk5.green : session.paymentStatus === "held" ? chalk5.yellow : chalk5.gray;
549
+ lines.push(` Payment: ${paymentColor(session.paymentStatus)}`);
550
+ if (session.paymentStatus === "held") {
551
+ lines.push(
552
+ chalk5.yellow(
553
+ " Held, not taken: capture it to take the money, or void it. Do not ship on a hold alone."
554
+ )
555
+ );
556
+ }
557
+ }
558
+ if (session.paymentIntentId) {
559
+ lines.push(` Intent: ${session.paymentIntentId}`);
560
+ }
561
+ if (session.transactionId) {
562
+ lines.push(` Transaction: ${session.transactionId}`);
563
+ }
564
+ if (typeof session.vp_tx_id === "string") {
565
+ lines.push(` Refund id: ${session.vp_tx_id}`);
566
+ }
567
+ if (typeof session.vp_tx_count === "number") {
568
+ lines.push(` Payments: ${session.vp_tx_count}`);
569
+ if (session.vp_tx_count >= 2) {
570
+ lines.push(
571
+ chalk5.red(
572
+ ` \u26A0 Charged ${session.vp_tx_count} times: there is no single refund id. Have a person check the payments before refunding or fulfilling.`
573
+ )
574
+ );
575
+ }
576
+ }
577
+ lines.push(` Created: ${session.createdAt}`);
578
+ lines.push(` Expires: ${session.expiresAt}`);
579
+ return lines;
580
+ }
581
+ function sessionsCommand() {
582
+ const sessions = new Command4("sessions").description(
583
+ "Create and manage checkout sessions"
584
+ );
585
+ sessions.command("create").description("Create a new checkout session").requiredOption("--amount <cents>", "Amount in minor units, a whole number (e.g. 1499 = $14.99)").requiredOption("--currency <code>", "Currency code (e.g. USD)").option("--country <code>", "Country code (e.g. US)").option("--description <text>", "Session description").option("--success-url <url>", "Redirect URL on success").option("--cancel-url <url>", "Redirect URL on cancel").option("--idempotency-key <key>", "Idempotency key for safe retries").option("--dry-run", "Validate without creating a session").option("--json", "Output raw JSON").action(async (opts) => {
586
+ const amount = minorUnitsOrExit(String(opts.amount), "--amount");
587
+ const client = makeClient();
588
+ const spinner = startSpinner("Creating session...");
589
+ try {
590
+ const params = {
591
+ amount,
592
+ currency: opts.currency.toUpperCase(),
593
+ country: opts.country?.toUpperCase(),
594
+ description: opts.description,
595
+ successUrl: opts.successUrl,
596
+ cancelUrl: opts.cancelUrl
597
+ };
598
+ if (opts.dryRun) {
599
+ spinner.text = "Validating...";
600
+ const result = await client.sessions.validate(params);
601
+ spinner.stop();
602
+ if (opts.json) {
603
+ console.log(JSON.stringify(result, null, 2));
604
+ } else if (result.valid) {
605
+ console.log(chalk5.green("\u2713 Valid request"));
606
+ if (result.warnings.length > 0) {
607
+ for (const w of result.warnings) {
608
+ console.log(chalk5.yellow(` \u26A0 ${w}`));
609
+ }
610
+ }
611
+ } else {
612
+ console.log(chalk5.red("\u2717 Invalid request"));
613
+ }
614
+ return;
615
+ }
616
+ const session = await client.sessions.create(
617
+ params,
618
+ opts.idempotencyKey ? { idempotencyKey: opts.idempotencyKey } : void 0
619
+ );
620
+ spinner.stop();
621
+ if (opts.json) {
622
+ console.log(JSON.stringify(session, null, 2));
623
+ } else {
624
+ console.log(chalk5.green("\u2713 Session created"));
625
+ console.log(` ID: ${session.id}`);
626
+ console.log(` URL: ${chalk5.cyan(session.checkoutUrl)}`);
627
+ console.log(` Expires: ${session.expiresAt}`);
628
+ }
629
+ } catch (err) {
630
+ spinner.stop();
631
+ handleError(err);
632
+ }
633
+ });
634
+ sessions.command("get").description("Get session status").argument("<id>", "Session ID").option("--json", "Output raw JSON").action(async (id, opts) => {
635
+ const client = makeClient();
636
+ const spinner = startSpinner("Fetching session...");
637
+ try {
638
+ const session = await client.sessions.get(id);
639
+ spinner.stop();
640
+ if (opts.json) {
641
+ console.log(JSON.stringify(session, null, 2));
642
+ } else {
643
+ for (const line of sessionLines(session)) console.log(line);
644
+ }
645
+ } catch (err) {
646
+ spinner.stop();
647
+ handleError(err);
648
+ }
649
+ });
650
+ return sessions;
651
+ }
652
+
653
+ // src/commands/payment-intents.ts
654
+ import { Command as Command5 } from "commander";
655
+ import chalk8 from "chalk";
656
+ import { formatMinorUnits as formatMinorUnits2 } from "@vonpay/checkout-node";
657
+
658
+ // src/utils/idempotency.ts
659
+ import { randomUUID } from "crypto";
660
+ import chalk6 from "chalk";
661
+ function resolveIdempotencyKey(flag) {
662
+ const given = flag?.trim();
663
+ if (given) return { key: given, generated: false };
664
+ return { key: `cli-${randomUUID()}`, generated: true };
665
+ }
666
+ function idempotencyKeyFor(flag, what) {
667
+ const { key, generated } = resolveIdempotencyKey(flag);
668
+ if (generated) {
669
+ console.error(
670
+ chalk6.dim(
671
+ `Idempotency key: ${key} (to retry this same ${what}, re-run with --idempotency-key ${key})`
672
+ )
673
+ );
674
+ }
675
+ return key;
676
+ }
677
+
678
+ // src/utils/metadata.ts
679
+ var MAX_LEN = 256;
680
+ var CONTROL_CHARS = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/;
681
+ function parseMetadata(pairs) {
682
+ if (!pairs || pairs.length === 0) return void 0;
683
+ const out = {};
684
+ for (const pair of pairs) {
685
+ const idx = pair.indexOf("=");
686
+ if (idx === -1) {
687
+ throw new Error(
688
+ `Invalid --metadata value "${pair}". Expected key=value (use multiple --metadata flags for multiple pairs).`
689
+ );
690
+ }
691
+ const key = pair.slice(0, idx);
692
+ const value = pair.slice(idx + 1);
693
+ if (key.length === 0) {
694
+ throw new Error(
695
+ `Invalid --metadata value "${pair}": key is empty. Expected key=value with a non-empty key.`
696
+ );
697
+ }
698
+ if (key.length > MAX_LEN) {
699
+ throw new Error(
700
+ `--metadata key too long (${key.length} > ${MAX_LEN}).`
701
+ );
702
+ }
703
+ if (value.length > MAX_LEN) {
704
+ throw new Error(
705
+ `--metadata value too long (${value.length} > ${MAX_LEN}).`
706
+ );
707
+ }
708
+ if (CONTROL_CHARS.test(key) || CONTROL_CHARS.test(value)) {
709
+ throw new Error(
710
+ `--metadata "${key}" contains control characters; metadata must be printable text.`
711
+ );
712
+ }
713
+ out[key] = value;
714
+ }
715
+ return out;
716
+ }
717
+
718
+ // src/utils/sanitize.ts
719
+ var ANSI_PATTERN = /\x1B\[[0-9;?]*[A-Za-z]/g;
720
+ var C0_CONTROL_PATTERN = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
721
+ function stripAnsi(s) {
722
+ return s.replace(ANSI_PATTERN, "").replace(C0_CONTROL_PATTERN, "");
723
+ }
724
+ function truncate(s, max) {
725
+ if (s.length <= max) return s;
726
+ return s.slice(0, max - 1) + "\u2026";
727
+ }
728
+ function safeDisplay(s, max = 200) {
729
+ return truncate(stripAnsi(s), max);
730
+ }
731
+
732
+ // src/utils/live-key.ts
733
+ import { createInterface as createInterface2 } from "readline";
734
+ import chalk7 from "chalk";
735
+ import { Option as Option2 } from "commander";
736
+ import { isLiveKey as isLiveKey2 } from "@vonpay/checkout-node";
737
+ var CONFIRM_WORD = "confirm";
738
+ var terminal = {
739
+ /**
740
+ * Both stdin and stdout are a terminal. A command run by a script, a CI job or
741
+ * an AI agent's shell tool has at least one of them piped.
742
+ */
743
+ isInteractive() {
744
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
745
+ },
746
+ /** Ask one line on the terminal. End of input or Ctrl-C answers "" (cancel). */
747
+ ask(prompt) {
748
+ return new Promise((resolve2) => {
749
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
750
+ let answered = false;
751
+ rl.on("SIGINT", () => rl.close());
752
+ rl.on("close", () => {
753
+ if (!answered) resolve2("");
754
+ });
755
+ rl.question(prompt, (answer) => {
756
+ answered = true;
757
+ rl.close();
758
+ resolve2(answer);
759
+ });
760
+ });
761
+ }
762
+ };
763
+ function keyFingerprint(key) {
764
+ const m = key.match(/^(vp_sk_live_)([A-Za-z0-9_-]{0,4})/);
765
+ return m ? `${m[1]}${m[2]}\u2026` : "vp_sk_live_\u2026";
766
+ }
767
+ async function confirmLiveAction(key, summary, opts = {}) {
768
+ if (!isLiveKey2(key)) return;
769
+ if (!terminal.isInteractive()) {
770
+ 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.`;
771
+ exitWithCliError(message, chalk7.red(message));
772
+ }
773
+ if (opts.confirmLive) {
774
+ console.warn(
775
+ chalk7.yellow(
776
+ "Note: --confirm-live no longer confirms a live action by itself. A person confirms it at the terminal."
777
+ )
778
+ );
779
+ }
780
+ const lines = [
781
+ chalk7.bold.red(`You are about to ${summary.action} with a LIVE key (${keyFingerprint(key)}).`),
782
+ ...(summary.details ?? []).map(([label, value]) => ` ${`${label}:`.padEnd(17)}${safeDisplay(value)}`),
783
+ chalk7.yellow(summary.consequence ?? "This moves real money and cannot be undone from here.")
784
+ ];
785
+ for (const line of lines) console.log(line);
786
+ const answer = await terminal.ask(`Type "${CONFIRM_WORD}" to proceed, anything else to cancel: `);
787
+ if (answer.trim() !== CONFIRM_WORD) {
788
+ const message = `Cancelled: you did not type "${CONFIRM_WORD}". Nothing was sent.`;
789
+ exitWithCliError(message, chalk7.red(message));
790
+ }
791
+ }
792
+ function retiredConfirmLiveOption() {
793
+ return new Option2(
794
+ "--confirm-live",
795
+ "No longer confirms anything: a live-key action is confirmed by a person at the terminal"
796
+ ).hideHelp();
797
+ }
798
+ function warnLiveSecretFlag(key, flag) {
799
+ if (!isLiveKey2(key)) return;
800
+ console.warn(
801
+ chalk7.yellow(
802
+ `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.`
803
+ )
804
+ );
805
+ }
806
+
807
+ // src/commands/payment-intents.ts
808
+ var MIT_INITIATORS = ["merchant", "customer"];
809
+ var MIT_REASONS = [
810
+ "recurring",
811
+ "unscheduled",
812
+ "installment"
813
+ ];
814
+ function buildCreateParams(opts, resolved) {
815
+ const params = {
816
+ amount: opts.amount,
817
+ currency: opts.currency.toUpperCase()
818
+ };
819
+ if (resolved.captureMethod !== void 0) {
820
+ params.captureMethod = resolved.captureMethod;
821
+ }
822
+ if (resolved.metadata !== void 0) {
823
+ params.metadata = resolved.metadata;
824
+ }
825
+ if (resolved.mit !== void 0) {
826
+ params.mit = resolved.mit;
827
+ }
828
+ const paymentMethodId = opts.paymentMethod?.trim();
829
+ if (paymentMethodId) {
830
+ params.paymentMethod = { id: paymentMethodId };
831
+ }
832
+ const sessionId = opts.sessionId?.trim();
833
+ if (sessionId) {
834
+ params.sessionId = sessionId;
835
+ }
836
+ const buyerId = opts.buyerId?.trim();
837
+ if (buyerId) {
838
+ params.buyerId = buyerId;
839
+ }
840
+ const buyerEmail = opts.buyerEmail?.trim();
841
+ if (buyerEmail) {
842
+ params.buyerEmail = buyerEmail;
843
+ }
844
+ return params;
845
+ }
846
+ function buildMITBlock(opts) {
847
+ const initiator = opts.mitInitiator?.trim().toLowerCase();
848
+ const reason = opts.mitReason?.trim().toLowerCase();
849
+ const originalTransactionId = opts.mitOriginalTransactionId?.trim();
850
+ const provided = [initiator, reason, originalTransactionId].filter(
851
+ (v) => v !== void 0 && v !== ""
852
+ ).length;
853
+ if (provided === 0) return void 0;
854
+ if (provided !== 3) {
855
+ throw new Error(
856
+ "MIT requires all three of --mit-initiator, --mit-reason, --mit-original-transaction-id (non-empty)."
857
+ );
858
+ }
859
+ if (!MIT_INITIATORS.includes(initiator)) {
860
+ throw new Error(
861
+ `Invalid --mit-initiator "${opts.mitInitiator}". Expected: ${MIT_INITIATORS.join(", ")}`
862
+ );
863
+ }
864
+ if (!MIT_REASONS.includes(reason)) {
865
+ throw new Error(
866
+ `Invalid --mit-reason "${opts.mitReason}". Expected: ${MIT_REASONS.join(", ")}`
867
+ );
868
+ }
869
+ return {
870
+ initiator,
871
+ reason,
872
+ originalTransactionId
873
+ };
874
+ }
875
+ function printPaymentIntent(intent) {
876
+ const statusColor = intent.status === "succeeded" ? chalk8.green : intent.status === "authorized" ? chalk8.cyan : intent.status === "requires_action" ? chalk8.yellow : intent.status === "failed" || intent.status === "canceled" ? chalk8.red : chalk8.gray;
877
+ console.log(` ID: ${intent.id}`);
878
+ console.log(` Status: ${statusColor(intent.status)}`);
879
+ console.log(
880
+ ` Amount: ${formatMinorUnits2(intent.amount, intent.currency)}`
881
+ );
882
+ console.log(` Capture: ${intent.captureMethod}`);
883
+ if (intent.nextAction) {
884
+ console.log(
885
+ ` Next action: ${chalk8.yellow(safeDisplay(intent.nextAction.type))}`
886
+ );
887
+ if (intent.nextAction.type === "redirect_to_url") {
888
+ console.log(
889
+ ` Redirect: ${chalk8.yellow(safeDisplay(intent.nextAction.redirectToUrl.url))}`
890
+ );
891
+ }
892
+ }
893
+ if (intent.declineCode) {
894
+ console.log(` Decline code: ${chalk8.red(safeDisplay(intent.declineCode))}`);
895
+ }
896
+ console.log(` Created: ${intent.createdAt}`);
897
+ }
898
+ var AMOUNT_HELP = "Amount in minor units, a whole number (e.g. 1499 = $14.99)";
899
+ function paymentIntentsCommand() {
900
+ const intents = new Command5("payment-intents").description(
901
+ "Create, read, capture, and void payment intents (discrete lifecycle)"
902
+ );
903
+ intents.command("create").description("Create a new payment intent").requiredOption("--amount <cents>", AMOUNT_HELP).requiredOption("--currency <code>", "ISO 4217 currency (e.g. USD)").option(
904
+ "--capture-method <mode>",
905
+ "automatic (default) or manual (parks at authorized)"
906
+ ).option(
907
+ "--metadata <pair...>",
908
+ "Metadata key=value (repeat flag for multiple pairs)"
909
+ ).option(
910
+ "--mit-initiator <initiator>",
911
+ `MIT initiator (${MIT_INITIATORS.join(" | ")})`
912
+ ).option(
913
+ "--mit-reason <reason>",
914
+ `MIT reason (${MIT_REASONS.join(" | ")})`
915
+ ).option(
916
+ "--mit-original-transaction-id <id>",
917
+ "Cardholder-initiated anchor payment intent ID (vpi_*)"
918
+ ).option(
919
+ "--payment-method <id>",
920
+ "Stored card to charge (vp_pmt_*). Required for saved-card / recurring charges \u2014 --mit alone declares the charge merchant-initiated but does not say which card to bill"
921
+ ).option(
922
+ "--session-id <id>",
923
+ "Checkout session this charge belongs to (vp_cs_*). Arms the double-charge guard: refused if that session already charged at submit. Safe to always send"
924
+ ).option(
925
+ "--buyer-id <id>",
926
+ "Your customer reference. Send on EVERY saved-card charge \u2014 it is what makes the server reject a charge against the wrong customer's stored card"
927
+ ).option("--buyer-email <email>", "Buyer email for this charge").option(
928
+ "--idempotency-key <key>",
929
+ "Idempotency key for this charge. Reuse the SAME key to retry the same charge safely. Generated (and printed) when omitted"
930
+ ).addOption(retiredConfirmLiveOption()).option("--json", "Output raw JSON").action(async (raw) => {
931
+ const apiKey = requireApiKey();
932
+ const opts = { ...raw, amount: minorUnitsOrExit(raw.amount, "--amount") };
933
+ const captureMethod = opts.captureMethod?.toLowerCase();
934
+ if (captureMethod !== void 0 && captureMethod !== "automatic" && captureMethod !== "manual") {
935
+ const message = `Invalid --capture-method "${opts.captureMethod}". Expected automatic or manual.`;
936
+ exitWithCliError(message, chalk8.red(message));
937
+ }
938
+ let params;
939
+ try {
940
+ const metadata = parseMetadata(opts.metadata);
941
+ const mit = buildMITBlock(opts);
942
+ params = buildCreateParams(opts, { captureMethod, metadata, mit });
943
+ } catch (err) {
944
+ handleError(err);
945
+ }
946
+ refuseUnsafeLiveDestination(apiKey);
947
+ const idempotencyKey = idempotencyKeyFor(opts.idempotencyKey, "charge");
948
+ await confirmLiveAction(
949
+ apiKey,
950
+ {
951
+ action: "create a payment intent (charge a payment)",
952
+ details: [
953
+ ...apiHostDetail(),
954
+ ["Amount", formatMinorUnits2(params.amount, params.currency)],
955
+ ["Capture", params.captureMethod ?? "automatic"],
956
+ ...params.paymentMethod ? [["Saved card", params.paymentMethod.id]] : [],
957
+ ...params.buyerId ? [["Buyer", params.buyerId]] : [],
958
+ ...params.sessionId ? [["Session", params.sessionId]] : [],
959
+ ["Idempotency key", idempotencyKey]
960
+ ]
961
+ },
962
+ opts
963
+ );
964
+ const client = makeCheckoutClient();
965
+ const spinner = startSpinner("Creating payment intent...");
966
+ try {
967
+ const intent = await client.paymentIntents.create(params, {
968
+ idempotencyKey
969
+ });
970
+ spinner.stop();
971
+ if (opts.json) {
972
+ console.log(JSON.stringify(intent, null, 2));
973
+ } else {
974
+ console.log(chalk8.green("\u2713 Payment intent created"));
975
+ printPaymentIntent(intent);
976
+ }
977
+ } catch (err) {
978
+ spinner.stop();
979
+ handleError(err);
980
+ }
981
+ });
982
+ intents.command("get").description(
983
+ "Read a payment intent back \u2014 the way to check a payment whose outcome was unclear before doing anything else"
984
+ ).argument("<id>", "Payment intent ID (vpi_*)").option("--json", "Output raw JSON").action(async (id, opts) => {
985
+ const client = makeCheckoutClient();
986
+ const spinner = startSpinner("Fetching payment intent...");
987
+ try {
988
+ const intent = await client.paymentIntents.retrieve(id);
989
+ spinner.stop();
990
+ if (opts.json) {
991
+ console.log(JSON.stringify(intent, null, 2));
992
+ } else {
993
+ printPaymentIntent(intent);
994
+ }
995
+ } catch (err) {
996
+ spinner.stop();
997
+ handleError(err);
998
+ }
999
+ });
1000
+ intents.command("capture").description("Capture an authorized payment intent").argument("<id>", "Payment intent ID (vpi_*)").option(
1001
+ "--amount-to-capture <cents>",
1002
+ "Partial capture amount in minor units, a whole number (omit for full)"
1003
+ ).option(
1004
+ "--idempotency-key <key>",
1005
+ "Idempotency key. Reuse the SAME key to retry safely. Generated (and printed) when omitted"
1006
+ ).addOption(retiredConfirmLiveOption()).option("--json", "Output raw JSON").action(
1007
+ async (id, opts) => {
1008
+ const apiKey = requireApiKey();
1009
+ const amountToCapture = optionalMinorUnitsOrExit(
1010
+ opts.amountToCapture,
1011
+ "--amount-to-capture"
1012
+ );
1013
+ refuseUnsafeLiveDestination(apiKey);
1014
+ const idempotencyKey = idempotencyKeyFor(opts.idempotencyKey, "capture");
1015
+ await confirmLiveAction(
1016
+ apiKey,
1017
+ {
1018
+ action: "capture a payment intent (take the held money)",
1019
+ details: [
1020
+ ...apiHostDetail(),
1021
+ ["Payment intent", id],
1022
+ [
1023
+ "Amount",
1024
+ amountToCapture !== void 0 ? `${amountToCapture} in minor units (partial capture)` : "the full authorized amount"
1025
+ ],
1026
+ ["Idempotency key", idempotencyKey]
1027
+ ]
1028
+ },
1029
+ opts
1030
+ );
1031
+ const client = makeCheckoutClient();
1032
+ const spinner = startSpinner("Capturing payment intent...");
1033
+ try {
1034
+ const intent = await client.paymentIntents.capture(
1035
+ id,
1036
+ amountToCapture !== void 0 ? { amountToCapture } : void 0,
1037
+ { idempotencyKey }
1038
+ );
1039
+ spinner.stop();
1040
+ if (opts.json) {
1041
+ console.log(JSON.stringify(intent, null, 2));
1042
+ } else {
1043
+ const captureLabel = amountToCapture !== void 0 ? ` (partial: ${formatMinorUnits2(amountToCapture, intent.currency)})` : "";
1044
+ console.log(chalk8.green(`\u2713 Payment intent captured${captureLabel}`));
1045
+ printPaymentIntent(intent);
1046
+ }
1047
+ } catch (err) {
1048
+ spinner.stop();
1049
+ handleError(err);
1050
+ }
1051
+ }
1052
+ );
1053
+ intents.command("void").description("Void an authorized (uncaptured) payment intent").argument("<id>", "Payment intent ID (vpi_*)").option(
1054
+ "--idempotency-key <key>",
1055
+ "Idempotency key. Reuse the SAME key to retry safely. Generated (and printed) when omitted"
1056
+ ).addOption(retiredConfirmLiveOption()).option("--json", "Output raw JSON").action(
1057
+ async (id, opts) => {
1058
+ const apiKey = requireApiKey();
1059
+ refuseUnsafeLiveDestination(apiKey);
1060
+ const idempotencyKey = idempotencyKeyFor(opts.idempotencyKey, "void");
1061
+ await confirmLiveAction(
1062
+ apiKey,
1063
+ {
1064
+ action: "void a payment intent (release the hold)",
1065
+ details: [
1066
+ ...apiHostDetail(),
1067
+ ["Payment intent", id],
1068
+ ["Idempotency key", idempotencyKey]
1069
+ ],
1070
+ consequence: "A voided payment cannot be captured afterwards; this cannot be undone from here."
1071
+ },
1072
+ opts
1073
+ );
1074
+ const client = makeCheckoutClient();
1075
+ const spinner = startSpinner("Voiding payment intent...");
1076
+ try {
1077
+ const intent = await client.paymentIntents.void(id, { idempotencyKey });
1078
+ spinner.stop();
1079
+ if (opts.json) {
1080
+ console.log(JSON.stringify(intent, null, 2));
1081
+ } else {
1082
+ console.log(chalk8.green("\u2713 Payment intent voided"));
1083
+ printPaymentIntent(intent);
1084
+ }
1085
+ } catch (err) {
1086
+ spinner.stop();
1087
+ handleError(err);
1088
+ }
1089
+ }
1090
+ );
1091
+ return intents;
1092
+ }
1093
+
1094
+ // src/commands/refunds.ts
1095
+ import { Command as Command6 } from "commander";
1096
+ import chalk9 from "chalk";
1097
+ import { formatMinorUnits as formatMinorUnits3 } from "@vonpay/checkout-node";
1098
+ function present(v) {
1099
+ return v !== void 0 && v.trim() !== "";
1100
+ }
1101
+ function resolveRefundTarget(opts) {
1102
+ const hasPi = present(opts.paymentIntent);
1103
+ const hasTx = present(opts.transaction);
1104
+ if (hasPi && hasTx) {
1105
+ throw new Error(
1106
+ "Pass exactly one of --payment-intent or --transaction, not both. A refund is issued against one target."
1107
+ );
1108
+ }
1109
+ if (hasPi) return { paymentIntent: opts.paymentIntent.trim() };
1110
+ if (hasTx) return { transaction: opts.transaction.trim() };
1111
+ throw new Error(
1112
+ "A refund target is required: pass --payment-intent <vpi_*> or --transaction <vp_tx_*>."
1113
+ );
1114
+ }
1115
+ function buildRefundParams(opts, metadata) {
1116
+ const params = { ...resolveRefundTarget(opts) };
1117
+ if (opts.amount !== void 0) params.amount = opts.amount;
1118
+ if (opts.currency !== void 0) {
1119
+ params.currency = opts.currency.toUpperCase();
1120
+ }
1121
+ if (opts.reason !== void 0) params.reason = opts.reason;
1122
+ if (metadata !== void 0) params.metadata = metadata;
1123
+ return params;
1124
+ }
1125
+ function printRefund(refund) {
1126
+ const statusColor = refund.status === "succeeded" ? chalk9.green : refund.status === "failed" ? chalk9.red : chalk9.yellow;
1127
+ console.log(` ID: ${refund.id}`);
1128
+ if (refund.transaction !== null) {
1129
+ console.log(` Transaction: ${refund.transaction}`);
1130
+ } else {
1131
+ console.log(` Payment intent: ${refund.paymentIntent}`);
1132
+ }
1133
+ console.log(` Status: ${statusColor(refund.status)}`);
1134
+ console.log(
1135
+ ` Amount: ${formatMinorUnits3(refund.amount, refund.currency)}`
1136
+ );
1137
+ if (refund.reason) {
1138
+ console.log(` Reason: ${safeDisplay(refund.reason)}`);
1139
+ }
1140
+ }
1141
+ function refundsCommand() {
1142
+ const refunds = new Command6("refunds").description(
1143
+ "Refund a captured payment intent or a settled transaction"
1144
+ );
1145
+ refunds.command("create").description(
1146
+ "Create a refund against a payment intent (--payment-intent) or a transaction (--transaction) \u2014 exactly one"
1147
+ ).option(
1148
+ "--payment-intent <id>",
1149
+ "Payment intent ID to refund (vpi_*). Use this OR --transaction. A payment intent id is also visible to the shopper's browser \u2014 it is not proof a refund is wanted; only refund one you looked up from your own order records"
1150
+ ).option(
1151
+ "--transaction <id>",
1152
+ "Transaction ID to refund (vp_tx_*), instead of --payment-intent (prefer --payment-intent whenever the payment has one). Use it for a payment that has no payment intent, to follow the transaction a refund_target_is_duplicate error names, or as the vp_tx_id read from the session when its payment has no payment intent \u2014 only when vp_tx_count is exactly 1, and only after matching it to the order in your own records. A transaction id is also visible to the shopper's browser \u2014 only refund one you looked up from your own order records"
1153
+ ).option(
1154
+ "--amount <cents>",
1155
+ "Refund amount in minor units, a whole number (omit for full remaining balance)"
1156
+ ).option("--currency <code>", "ISO 4217 currency code").option("--reason <text>", "Free-form reason (mirrored back on the record)").option(
1157
+ "--metadata <pair...>",
1158
+ "Metadata key=value (repeat flag for multiple pairs)"
1159
+ ).option(
1160
+ "--idempotency-key <key>",
1161
+ "Idempotency key. Reuse the SAME key to retry safely. Generated (and printed) when omitted"
1162
+ ).addOption(retiredConfirmLiveOption()).option("--json", "Output raw JSON").action(async (raw) => {
1163
+ try {
1164
+ resolveRefundTarget(raw);
1165
+ } catch (err) {
1166
+ const message = err.message;
1167
+ exitWithCliError(message, chalk9.red(`Error: ${message}`));
1168
+ }
1169
+ const apiKey = requireApiKey();
1170
+ const opts = {
1171
+ ...raw,
1172
+ amount: optionalMinorUnitsOrExit(raw.amount, "--amount")
1173
+ };
1174
+ let params;
1175
+ try {
1176
+ params = buildRefundParams(opts, parseMetadata(opts.metadata));
1177
+ } catch (err) {
1178
+ handleError(err);
1179
+ }
1180
+ const target = resolveRefundTarget(opts);
1181
+ refuseUnsafeLiveDestination(apiKey);
1182
+ const idempotencyKey = idempotencyKeyFor(opts.idempotencyKey, "refund");
1183
+ await confirmLiveAction(
1184
+ apiKey,
1185
+ {
1186
+ action: "create a refund (send money back to the buyer)",
1187
+ details: [
1188
+ ...apiHostDetail(),
1189
+ "transaction" in target ? ["Transaction", target.transaction] : ["Payment intent", target.paymentIntent],
1190
+ [
1191
+ "Amount",
1192
+ params.amount === void 0 ? "the full remaining balance" : params.currency ? formatMinorUnits3(params.amount, params.currency) : `${params.amount} in minor units`
1193
+ ],
1194
+ ["Idempotency key", idempotencyKey]
1195
+ ],
1196
+ consequence: "This pays money back out of the merchant's balance and cannot be reversed once settled."
1197
+ },
1198
+ opts
1199
+ );
1200
+ const client = makeCheckoutClient();
1201
+ const spinner = startSpinner("Creating refund...");
1202
+ try {
1203
+ const refund = await client.refunds.create(params, { idempotencyKey });
1204
+ spinner.stop();
1205
+ if (opts.json) {
1206
+ console.log(JSON.stringify(refund, null, 2));
1207
+ } else {
1208
+ console.log(chalk9.green("\u2713 Refund created"));
1209
+ printRefund(refund);
1210
+ }
1211
+ } catch (err) {
1212
+ spinner.stop();
1213
+ handleError(err);
1214
+ }
1215
+ });
1216
+ refunds.command("list").description(
1217
+ "List the refunds of ONE payment and its refund totals \u2014 the way to check a refund whose outcome was unclear"
1218
+ ).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(
1219
+ async (opts) => {
1220
+ let target;
1221
+ try {
1222
+ target = resolveRefundTarget(opts);
1223
+ } catch (err) {
1224
+ const message = err.message.replace("A refund target is required", "A payment is required");
1225
+ exitWithCliError(message, chalk9.red(`Error: ${message}`));
1226
+ }
1227
+ let limit;
1228
+ if (opts.limit !== void 0) {
1229
+ const parsed = parseMinorUnits(opts.limit, "--limit");
1230
+ if (!parsed.ok || parsed.value < 1) {
1231
+ const message = `--limit must be a whole number from 1 to 100; got ${JSON.stringify(opts.limit)}.`;
1232
+ exitWithCliError(message, chalk9.red(`Error: ${message}`));
1233
+ }
1234
+ limit = parsed.value;
1235
+ }
1236
+ const client = makeCheckoutClient();
1237
+ const spinner = startSpinner("Listing refunds...");
1238
+ try {
1239
+ const list = await client.refunds.list(target, {
1240
+ ...limit !== void 0 ? { limit } : {},
1241
+ ...opts.startingAfter ? { startingAfter: opts.startingAfter } : {}
1242
+ });
1243
+ spinner.stop();
1244
+ if (opts.json) {
1245
+ console.log(JSON.stringify(list, null, 2));
1246
+ return;
1247
+ }
1248
+ for (const line of refundListLines(list)) console.log(line);
1249
+ } catch (err) {
1250
+ spinner.stop();
1251
+ handleError(err);
1252
+ }
1253
+ }
1254
+ );
1255
+ return refunds;
1256
+ }
1257
+ function refundListLines(list) {
1258
+ const currency = list.data[0]?.currency;
1259
+ const money = (n) => currency ? formatMinorUnits3(n, currency) : `${n} in minor units`;
1260
+ const lines = [
1261
+ ` Refunded so far: ${money(list.amountRefunded)} (succeeded + still in progress)`,
1262
+ ` Still refundable: ${money(list.remainingRefundable)}${list.refundable ? "" : " \u2014 this payment cannot be refunded now"}`
1263
+ ];
1264
+ if (list.data.length === 0) {
1265
+ lines.push(" No refunds on this payment.");
1266
+ }
1267
+ for (const r of list.data) {
1268
+ const status = r.status === "succeeded" ? chalk9.green(r.status) : r.status === "failed" ? chalk9.red(r.status) : chalk9.yellow(r.status);
1269
+ lines.push(
1270
+ ` ${r.id} ${status} ${formatMinorUnits3(r.amount, r.currency)}` + (r.createdAt ? ` ${r.createdAt}` : "") + (r.idempotencyKey ? ` key ${safeDisplay(r.idempotencyKey)}` : "")
1271
+ );
1272
+ }
1273
+ if (list.data.some((r) => r.status === "requested")) {
1274
+ lines.push(chalk9.yellow(" A refund marked requested is still in progress and can still fail: do not refund again."));
1275
+ }
1276
+ if (list.hasMore && list.nextCursor) {
1277
+ lines.push(` More: re-run with --starting-after ${safeDisplay(list.nextCursor)}`);
1278
+ }
1279
+ return lines;
1280
+ }
1281
+
1282
+ // src/commands/tokens.ts
1283
+ import { Command as Command7 } from "commander";
1284
+ import chalk10 from "chalk";
1285
+ function printToken(token) {
1286
+ const statusColor = token.status === "active" ? chalk10.green : token.status === "expired" ? chalk10.yellow : chalk10.red;
1287
+ console.log(` ID: ${token.id}`);
1288
+ console.log(` Status: ${statusColor(token.status)}`);
1289
+ const card = formatTokenCard(token.card);
1290
+ console.log(` Card: ${card.card}`);
1291
+ console.log(` Expires: ${card.expires}`);
1292
+ }
1293
+ function tokensCommand() {
1294
+ const tokens = new Command7("tokens").description(
1295
+ "Create payment-method tokens for saved-card / MIT flows"
1296
+ );
1297
+ tokens.command("create").description("Create a payment-method token").option("--buyer-id <id>", "Buyer ID to attach the token to").option(
1298
+ "--provider-reference <ref>",
1299
+ "Provider tokenization handle (required for live keys with iframe-vault providers)"
1300
+ ).option(
1301
+ "--metadata <pair...>",
1302
+ "Metadata key=value (repeat flag for multiple pairs)"
1303
+ ).option("--idempotency-key <key>", "Idempotency key for safe retries").option("--json", "Output raw JSON").action(
1304
+ async (opts) => {
1305
+ const apiKey = requireApiKey();
1306
+ if (!opts.buyerId && !opts.providerReference) {
1307
+ console.warn(
1308
+ chalk10.yellow(
1309
+ "Warning: no --buyer-id or --provider-reference supplied. Providers with an iframe vault (sandbox and live alike) will fail with a 422 validation_error."
1310
+ )
1311
+ );
1312
+ }
1313
+ if (opts.providerReference !== void 0) {
1314
+ warnLiveSecretFlag(apiKey, "--provider-reference");
1315
+ }
1316
+ const client = makeCheckoutClient();
1317
+ const spinner = startSpinner("Creating token...");
1318
+ try {
1319
+ const metadata = parseMetadata(opts.metadata);
1320
+ const params = {};
1321
+ if (opts.buyerId !== void 0) params.buyerId = opts.buyerId;
1322
+ if (opts.providerReference !== void 0) {
1323
+ params.providerReference = opts.providerReference;
1324
+ }
1325
+ if (metadata !== void 0) params.metadata = metadata;
1326
+ const token = await client.tokens.create(
1327
+ params,
1328
+ opts.idempotencyKey ? { idempotencyKey: opts.idempotencyKey } : void 0
1329
+ );
1330
+ spinner.stop();
1331
+ if (opts.json) {
1332
+ console.log(JSON.stringify(token, null, 2));
1333
+ } else {
1334
+ console.log(chalk10.green("\u2713 Token created"));
1335
+ printToken(token);
1336
+ }
1337
+ } catch (err) {
1338
+ spinner.stop();
1339
+ handleError(err);
1340
+ }
1341
+ }
1342
+ );
1343
+ return tokens;
1344
+ }
1345
+ function formatTokenCard(card) {
1346
+ const cardLine = card.brand === null && card.last4 === null ? "unknown" : `${card.brand ?? "unknown brand"} \u2022\u2022\u2022\u2022 ${card.last4 ?? "unknown"}`;
1347
+ const expires = card.expMonth === null && card.expYear === null ? "unknown" : `${card.expMonth === null ? "??" : String(card.expMonth).padStart(2, "0")}/${card.expYear ?? "????"}`;
1348
+ return { card: cardLine, expires };
1349
+ }
1350
+
1351
+ // src/commands/capabilities.ts
1352
+ import { Command as Command8 } from "commander";
1353
+ import chalk11 from "chalk";
1354
+ function fmtBool(v) {
1355
+ return v ? chalk11.green("yes") : chalk11.dim("no");
1356
+ }
1357
+ function fmtVoidPolicy(v) {
1358
+ switch (v) {
1359
+ case "supported":
1360
+ return chalk11.green(v);
1361
+ case "rerouted_to_refund":
1362
+ return chalk11.yellow(v);
1363
+ case "not_supported":
1364
+ return chalk11.dim(v);
1365
+ default:
1366
+ return chalk11.dim(String(v));
1367
+ }
1368
+ }
1369
+ function fmtDisputeReporting(v) {
1370
+ return v === "tracked" ? chalk11.green(v) : chalk11.yellow(v);
1371
+ }
1372
+ function fmtSubscriptionBilling(v) {
1373
+ if (v === true) return chalk11.yellow("yes");
1374
+ if (v === false) return chalk11.green("no");
1375
+ return chalk11.yellow("unknown");
1376
+ }
1377
+ function printCapabilities(caps) {
1378
+ const ops = caps.supportedOperations;
1379
+ console.log(chalk11.bold("Supported operations"));
1380
+ console.log(` auth/capture separation: ${fmtBool(ops.authCaptureSeparation)}`);
1381
+ console.log(` partial capture: ${fmtBool(ops.partialCapture)}`);
1382
+ console.log(` partial refund: ${fmtBool(ops.partialRefund)}`);
1383
+ console.log(` unreferenced refund: ${fmtBool(ops.unreferencedRefund)}`);
1384
+ console.log(` void after capture: ${fmtVoidPolicy(ops.voidAfterCapture)}`);
1385
+ console.log(` MIT: ${fmtBool(ops.mit)}`);
1386
+ console.log(` network tokens: ${fmtBool(ops.networkTokens)}`);
1387
+ console.log(` 3D Secure 2: ${fmtBool(ops.threeDSecure2)}`);
1388
+ console.log(` ACH: ${fmtBool(ops.ach)}`);
1389
+ console.log(` payouts API: ${fmtBool(ops.payoutsApi)}`);
1390
+ console.log(` dispute reporting: ${fmtDisputeReporting(ops.disputeReporting)}`);
1391
+ console.log();
1392
+ if (ops.disputeReporting !== "tracked") {
1393
+ console.log(
1394
+ chalk11.yellow(
1395
+ " \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."
1396
+ )
1397
+ );
1398
+ console.log();
1399
+ }
1400
+ console.log(chalk11.bold("Subscription billing"));
1401
+ console.log(` Von Pay renews for you: ${fmtSubscriptionBilling(caps.subscriptionBilling?.enabled)}`);
1402
+ if (caps.subscriptionBilling?.enabled !== false) {
1403
+ console.log(
1404
+ chalk11.yellow(
1405
+ ` \u26A0 Do not run renewals from your own system while this is not "no":
1406
+ Von Pay may be charging these customers' renewals itself, and both
1407
+ would charge them. Check again at the moment you renew.`
1408
+ )
1409
+ );
1410
+ }
1411
+ console.log();
1412
+ console.log(chalk11.bold("Settlement currencies"));
1413
+ console.log(
1414
+ ` ${caps.settlementCurrencies.length ? caps.settlementCurrencies.join(", ") : chalk11.dim("(none reported)")}`
1415
+ );
1416
+ console.log();
1417
+ console.log(chalk11.bold("Rate limits"));
1418
+ console.log(
1419
+ ` payment intents / minute: ${caps.rateLimits.paymentIntentsPerMinute}`
1420
+ );
1421
+ }
1422
+ function capabilitiesCommand() {
1423
+ return new Command8("capabilities").description("Show what this merchant's binder supports").option("--json", "Output raw JSON").action(async (opts) => {
1424
+ const client = makeCheckoutClient();
1425
+ const spinner = startSpinner("Fetching capabilities...");
1426
+ try {
1427
+ const caps = await client.capabilities.get();
1428
+ spinner.stop();
1429
+ if (opts.json) {
1430
+ console.log(JSON.stringify(caps, null, 2));
1431
+ } else {
1432
+ printCapabilities(caps);
1433
+ }
1434
+ } catch (err) {
1435
+ spinner.stop();
1436
+ handleError(err);
1437
+ }
1438
+ });
1439
+ }
1440
+
1441
+ // src/commands/trigger.ts
1442
+ import { Command as Command9 } from "commander";
1443
+ import { createHmac as createHmac2, randomUUID as randomUUID2 } from "crypto";
1444
+ import chalk12 from "chalk";
1445
+ import ora2 from "ora";
1446
+
1447
+ // src/utils/forward-to.ts
1448
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
1449
+ "localhost",
1450
+ "127.0.0.1",
1451
+ "0.0.0.0",
1452
+ // kernel-wildcard often routes to loopback on Linux / macOS dev hosts
1453
+ "::1",
1454
+ // URL.hostname surrounds IPv6 with brackets, but the bracket-stripping
1455
+ // happens before this set is consulted (see normalizeHost). Both forms
1456
+ // listed for defensiveness.
1457
+ "[::1]",
1458
+ // IPv6-mapped IPv4 loopback. Node's URL parser normalizes
1459
+ // `[::ffff:127.0.0.1]` to either form depending on version — both
1460
+ // are functionally loopback so we list every variant we have
1461
+ // observed in the wild.
1462
+ "::ffff:127.0.0.1",
1463
+ "::ffff:7f00:1"
1464
+ ]);
1465
+ function validateForwardTo(raw, opts) {
1466
+ const flag = opts.flag ?? "--forward-to";
1467
+ const tunnelFlag = opts.tunnelFlag ?? "--forward-to-tunnel";
1468
+ if (typeof raw !== "string" || raw.length === 0) {
1469
+ return { ok: false, reason: `${flag} URL is required` };
1470
+ }
1471
+ let url;
1472
+ try {
1473
+ url = new URL(raw);
1474
+ } catch {
1475
+ return {
1476
+ ok: false,
1477
+ reason: `${flag} is not a parseable URL: ${truncate2(raw)}`
1478
+ };
1479
+ }
1480
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
1481
+ return {
1482
+ ok: false,
1483
+ reason: `${flag} scheme must be http or https; got ${url.protocol}`
1484
+ };
1485
+ }
1486
+ if (url.username !== "" || url.password !== "") {
1487
+ return {
1488
+ ok: false,
1489
+ reason: `${flag} must not contain a user name or password.`
1490
+ };
1491
+ }
1492
+ const host = normalizeHost(url.hostname);
1493
+ const isLoopback = LOOPBACK_HOSTS.has(host);
1494
+ if (!isLoopback && !opts.allowTunnel) {
1495
+ return {
1496
+ ok: false,
1497
+ reason: `${flag} refuses non-loopback hosts by default (got ${url.hostname}). Re-run with ${tunnelFlag} if forwarding through ngrok / Cloudflare Tunnel / etc.`
1498
+ };
1499
+ }
1500
+ const warning = host === "0.0.0.0" ? "forwarding to 0.0.0.0 \u2014 that's a bind-wildcard, not a guaranteed loopback destination (it can route off-host in some container/network setups). Prefer 127.0.0.1 or localhost if you meant your local machine." : void 0;
1501
+ return { ok: true, url, isLoopback, warning };
1502
+ }
1503
+ function normalizeHost(hostname) {
1504
+ if (hostname.startsWith("[") && hostname.endsWith("]")) {
1505
+ return hostname.slice(1, -1);
1506
+ }
1507
+ return hostname.toLowerCase();
1508
+ }
1509
+ function truncate2(s) {
1510
+ return s.length <= 120 ? s : `${s.slice(0, 117)}...`;
1511
+ }
1512
+
1513
+ // src/utils/listen-forward.ts
1514
+ import { createHmac } from "crypto";
1515
+ async function forwardPayload(payload, opts) {
1516
+ const body = JSON.stringify(payload);
1517
+ const t = Math.floor(Date.now() / 1e3);
1518
+ const v1 = createHmac("sha256", opts.signingSecret).update(`${t}.${body}`).digest("hex");
1519
+ const signatureHeader = `t=${t},v1=${v1}`;
1520
+ const timeoutMs = opts.timeoutMs ?? 5e3;
1521
+ const localController = new AbortController();
1522
+ const timeoutId = setTimeout(() => localController.abort(), timeoutMs);
1523
+ const combinedSignal = anySignal([opts.signal, localController.signal]);
1524
+ const start = Date.now();
1525
+ try {
1526
+ const res = await fetch(opts.url.toString(), {
1527
+ method: "POST",
1528
+ headers: {
1529
+ "Content-Type": "application/json",
1530
+ "x-vonpay-signature": signatureHeader,
1531
+ "User-Agent": "vonpay-cli-listen"
1532
+ },
1533
+ body,
1534
+ signal: combinedSignal,
1535
+ // Never follow a redirect: a 307/308 from the local endpoint would make
1536
+ // fetch re-send this body AND the signature header to wherever it points,
1537
+ // including a non-loopback host --forward-to never allowed.
1538
+ redirect: "manual"
1539
+ });
1540
+ try {
1541
+ await res.arrayBuffer();
1542
+ } catch {
1543
+ }
1544
+ if (isRedirect(res)) {
1545
+ return {
1546
+ attempted: true,
1547
+ status: res.status,
1548
+ latencyMs: Date.now() - start,
1549
+ error: `the endpoint answered with a redirect (HTTP ${res.status}); it was not followed and the event was not sent anywhere else`
1550
+ };
1551
+ }
1552
+ return {
1553
+ attempted: true,
1554
+ status: res.status,
1555
+ latencyMs: Date.now() - start,
1556
+ error: null
1557
+ };
1558
+ } catch (err) {
1559
+ if (opts.signal.aborted) {
1560
+ return { attempted: true, status: null, latencyMs: null, error: "aborted" };
1561
+ }
1562
+ if (localController.signal.aborted) {
1563
+ return { attempted: true, status: null, latencyMs: null, error: `timeout after ${timeoutMs}ms` };
1564
+ }
1565
+ const message = err instanceof Error ? err.message : String(err);
1566
+ return { attempted: true, status: null, latencyMs: null, error: message };
1567
+ } finally {
1568
+ clearTimeout(timeoutId);
1569
+ }
1570
+ }
1571
+ function isRedirect(res) {
1572
+ return res.status >= 300 && res.status < 400 || res.type === "opaqueredirect";
1573
+ }
1574
+ function anySignal(signals) {
1575
+ if (typeof AbortSignal.any === "function") {
1576
+ return AbortSignal.any(signals);
1577
+ }
1578
+ const controller = new AbortController();
1579
+ const tracked = [];
1580
+ const onAbort = () => {
1581
+ for (const sig of tracked) {
1582
+ sig.removeEventListener("abort", onAbort);
1583
+ }
1584
+ controller.abort();
1585
+ };
1586
+ for (const signal of signals) {
1587
+ if (signal.aborted) {
1588
+ controller.abort();
1589
+ for (const sig of tracked) {
1590
+ sig.removeEventListener("abort", onAbort);
1591
+ }
1592
+ return controller.signal;
1593
+ }
1594
+ signal.addEventListener("abort", onAbort);
1595
+ tracked.push(signal);
1596
+ }
1597
+ return controller.signal;
1598
+ }
1599
+
1600
+ // src/commands/trigger.ts
1601
+ var SUPPORTED_EVENTS = [
1602
+ "charge.succeeded",
1603
+ "charge.failed",
1604
+ "charge.refunded",
1605
+ "refund.failed",
1606
+ "payment_intent.succeeded",
1607
+ "payment_intent.failed",
1608
+ "payment_intent.cancelled",
1609
+ "mirror.order.created",
1610
+ "session.succeeded",
1611
+ "session.failed"
1612
+ ];
1613
+ var UNSUBSCRIBABLE_EVENTS = [
1614
+ "session.succeeded",
1615
+ "session.failed"
1616
+ ];
1617
+ function warnIfUnsubscribable(event, write = (m) => console.warn(m)) {
1618
+ if (!UNSUBSCRIBABLE_EVENTS.includes(event)) return false;
1619
+ const suggestion = event === "session.succeeded" ? "charge.succeeded" : "charge.failed";
1620
+ write(
1621
+ chalk12.yellow(
1622
+ `\u26A0 ${event} is emitted internally but is NOT subscribable.
1623
+ The subscription API accepts the key, stores nothing and returns success, so a
1624
+ real endpoint receives this event NEVER \u2014 a handler tested only against it is
1625
+ untested. Use ${suggestion} instead.`
1626
+ )
1627
+ );
1628
+ return true;
1629
+ }
1630
+ function buildEventData(event, ids, amount, currency) {
1631
+ const base = {
1632
+ session_id: ids.sessionId,
1633
+ payment_intent_id: ids.paymentIntentId,
1634
+ transaction_id: ids.transactionId,
1635
+ amount,
1636
+ currency
1637
+ };
1638
+ switch (event) {
1639
+ case "session.succeeded":
1640
+ case "payment_intent.succeeded":
1641
+ return base;
1642
+ case "charge.succeeded":
1643
+ return { ...base, vp_tx_id: ids.transactionId, card: null };
1644
+ case "session.failed":
1645
+ case "payment_intent.failed":
1646
+ return {
1647
+ ...base,
1648
+ failure_reason: "Card declined",
1649
+ failure_code: "card_declined",
1650
+ network_decline_code: "05"
1651
+ };
1652
+ case "charge.failed":
1653
+ return {
1654
+ ...base,
1655
+ failure_reason: "Card declined",
1656
+ failure_code: "card_declined",
1657
+ // Non-null ONLY when failure_code is `blocked_by_rule`; this fixture is
1658
+ // an ordinary decline, so a consumer must see the null.
1659
+ rule_code: null,
1660
+ // Always null on a decline: no money moved, no settlement row.
1661
+ vp_tx_id: null,
1662
+ network_decline_code: "05"
1663
+ };
1664
+ case "mirror.order.created":
1665
+ return {
1666
+ payment_intent_id: ids.paymentIntentId,
1667
+ order_number: `TEST-${randomUUID2().slice(0, 6).toUpperCase()}`,
1668
+ order_status_url: null
1669
+ };
1670
+ case "payment_intent.cancelled":
1671
+ return { ...base, cancellation_reason: "buyer_abandoned" };
1672
+ case "charge.refunded":
1673
+ return {
1674
+ ...base,
1675
+ refund_id: `vpr_test_${randomUUID2().slice(0, 8)}`,
1676
+ vp_tx_id: ids.transactionId,
1677
+ // The two unambiguous companions to `amount`. The server has emitted
1678
+ // both for some time; omitting them here made a handler that reads them
1679
+ // look broken against a synthetic event.
1680
+ refund_amount: amount,
1681
+ amount_refunded_total: amount,
1682
+ reason: "customer_request",
1683
+ is_partial: false,
1684
+ original_charge_amount: amount,
1685
+ card: null
1686
+ };
1687
+ case "refund.failed":
1688
+ return {
1689
+ ...base,
1690
+ refund_id: `vpr_test_${randomUUID2().slice(0, 8)}`,
1691
+ // Mirrors charge.refunded: the Von transaction id of the refunded charge.
1692
+ vp_tx_id: ids.transactionId,
1693
+ // No money moved: this is the reversal amount that FAILED to move.
1694
+ refund_amount: amount,
1695
+ amount_refunded_total: 0,
1696
+ reason: "customer_request",
1697
+ // LOCKED two-value refund vocabulary — `refund_declined` (terminal) or
1698
+ // `refund_unresolved` (no provider response, true state unknown).
1699
+ reason_code: "refund_declined",
1700
+ is_partial: false,
1701
+ original_charge_amount: amount,
1702
+ retry_available: true,
1703
+ card: null
1704
+ };
1705
+ }
1706
+ }
1707
+ function buildPayload(event, opts) {
1708
+ const amount = opts.amount ?? 1499;
1709
+ const currency = opts.currency ?? "USD";
1710
+ const ids = {
1711
+ sessionId: opts.sessionId ?? `vp_cs_test_${randomUUID2().slice(0, 8)}`,
1712
+ transactionId: `vp_tx_test_${randomUUID2().slice(0, 8)}`,
1713
+ paymentIntentId: `vpi_test_${randomUUID2().slice(0, 8)}`
1714
+ };
1715
+ return {
1716
+ id: `vp_evt_test_${randomUUID2().slice(0, 8)}`,
1717
+ type: event,
1718
+ created: Math.floor(Date.now() / 1e3),
1719
+ livemode: false,
1720
+ merchant_id: `vp_mer_test_${randomUUID2().slice(0, 8)}`,
1721
+ test_event: true,
1722
+ data: buildEventData(event, ids, amount, currency)
1723
+ };
1724
+ }
1725
+ function triggerCommand() {
1726
+ 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(
1727
+ "--url-tunnel",
1728
+ "Allow a non-loopback --url (ngrok / Cloudflare Tunnel). Without it, --url must be this machine"
1729
+ ).addOption(retiredConfirmLiveOption()).action(async (event, opts) => {
1730
+ if (!SUPPORTED_EVENTS.includes(event)) {
1731
+ console.error(
1732
+ chalk12.red(
1733
+ `Unknown event: ${event}
1734
+ Supported: ${SUPPORTED_EVENTS.join(", ")}`
1735
+ )
1736
+ );
1737
+ process.exit(1);
1738
+ }
1739
+ warnIfUnsubscribable(event);
1740
+ const destination = validateForwardTo(String(opts.url ?? ""), {
1741
+ allowTunnel: opts.urlTunnel === true,
1742
+ flag: "--url",
1743
+ tunnelFlag: "--url-tunnel"
1744
+ });
1745
+ if (!destination.ok) {
1746
+ console.error(chalk12.red(destination.reason));
1747
+ process.exit(1);
1748
+ }
1749
+ if (destination.warning) console.warn(chalk12.yellow(destination.warning));
1750
+ const amount = optionalMinorUnitsOrExit(
1751
+ opts.amount === void 0 ? void 0 : String(opts.amount),
1752
+ "--amount"
1753
+ );
1754
+ const apiKey = requireApiKey();
1755
+ await confirmLiveAction(
1756
+ apiKey,
1757
+ {
1758
+ action: "trigger a test webhook event",
1759
+ details: [
1760
+ ["Event", event],
1761
+ ["Sent to", String(opts.url)]
1762
+ ],
1763
+ consequence: "The event is signed with your LIVE key: a receiver that verifies with that key will accept it as a genuine event."
1764
+ },
1765
+ opts
1766
+ );
1767
+ const payload = buildPayload(event, { ...opts, amount });
1768
+ const body = JSON.stringify(payload);
1769
+ const t = Math.floor(Date.now() / 1e3);
1770
+ const v1 = createHmac2("sha256", apiKey).update(`${t}.${body}`).digest("hex");
1771
+ const signatureHeader = `t=${t},v1=${v1}`;
1772
+ const spinner = ora2(`Sending ${event} to ${opts.url}...`).start();
1773
+ try {
1774
+ const response = await fetch(opts.url, {
1775
+ method: "POST",
1776
+ headers: {
1777
+ "Content-Type": "application/json",
1778
+ "x-vonpay-signature": signatureHeader,
1779
+ "User-Agent": "VonPay-Webhook/1.0 (CLI trigger)"
1780
+ },
1781
+ body,
1782
+ signal: AbortSignal.timeout(1e4),
1783
+ // Never follow a redirect: a 307/308 would re-send this signed body
1784
+ // to wherever it points, including a host --url never allowed.
1785
+ redirect: "manual"
1786
+ });
1787
+ spinner.stop();
1788
+ if (isRedirect(response)) {
1789
+ console.error(
1790
+ chalk12.red(
1791
+ `\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.`
1792
+ )
1793
+ );
1794
+ process.exit(1);
1795
+ }
1796
+ if (response.ok) {
1797
+ console.log(chalk12.green(`\u2713 ${event} delivered \u2192 ${response.status}`));
1798
+ } else {
1799
+ console.log(
1800
+ chalk12.yellow(
1801
+ `\u26A0 ${event} delivered but got ${response.status} response`
1802
+ )
1803
+ );
1804
+ }
1805
+ console.log(chalk12.dim(` Event: ${payload.id} (${payload.type})`));
1806
+ console.log(
1807
+ chalk12.dim(
1808
+ ` Amount: ${payload.data.amount} ${payload.data.currency}`
1809
+ )
1810
+ );
1811
+ console.log(
1812
+ chalk12.dim(
1813
+ `
1814
+ Verify with: vonpay.webhooks.constructEvent(rawBody, signatureHeader, secret)`
1815
+ )
1816
+ );
1817
+ console.log(
1818
+ chalk12.dim(
1819
+ ` (local dev: 'secret' is your API key here \u2014 production webhooks are signed with your endpoint's whsec_\u2026 secret, NOT your API key)`
1820
+ )
1821
+ );
1822
+ } catch (err) {
1823
+ spinner.stop();
1824
+ if (err instanceof Error && err.name === "TimeoutError") {
1825
+ console.error(chalk12.red(`Timeout: ${opts.url} did not respond within 10s`));
1826
+ } else {
1827
+ console.error(
1828
+ chalk12.red(
1829
+ `Failed to deliver: ${err instanceof Error ? err.message : err}`
1830
+ )
1831
+ );
1832
+ }
1833
+ process.exit(1);
1834
+ }
1835
+ });
1836
+ }
1837
+
1838
+ // src/commands/listen.ts
1839
+ import { Command as Command10 } from "commander";
1840
+ import { VonPayCheckout as VonPayCheckout2 } from "@vonpay/checkout-node";
1841
+
1842
+ // ../sse-parser/dist/index.js
1843
+ var SseParser = class {
1844
+ on;
1845
+ /** The stream's last event id (send as `Last-Event-ID` on a reconnect when not empty). */
1846
+ lastId = "";
1847
+ /** Reconnection time in ms from a `retry:` field, or -1 if none was sent. */
1848
+ retry = -1;
1849
+ rest = "";
1850
+ skipLF = false;
1851
+ data = "";
1852
+ type = "";
1853
+ idBuf = "";
1854
+ max;
1855
+ maxId;
1856
+ constructor(on, options = {}) {
1857
+ this.on = on;
1858
+ this.max = options.maxPending ?? 65536;
1859
+ this.maxId = options.maxIdLength ?? Infinity;
1860
+ }
1861
+ /**
1862
+ * Start a new stream (a reconnect): pending line, data and type buffers
1863
+ * cleared; the source's last event id and reconnection time survive.
1864
+ *
1865
+ * ⚠️ One deliberate departure from the letter of § 9.2.6: the spec
1866
+ * initialises a new stream's last-event-id buffer to "", so an id-less
1867
+ * event dispatched first on a reconnect (a server's ceiling frame has no
1868
+ * `id:`) would clear the source's last id and the NEXT reconnect would lose
1869
+ * its `Last-Event-ID`. We seed the buffer with the last id instead; an
1870
+ * explicit empty `id:` still clears it.
1871
+ */
1872
+ reset() {
1873
+ this.rest = this.data = this.type = "";
1874
+ this.idBuf = this.lastId;
1875
+ this.skipLF = false;
1876
+ }
1877
+ feed(chunk) {
1878
+ if (this.skipLF && chunk) {
1879
+ if (chunk[0] === "\n")
1880
+ chunk = chunk.slice(1);
1881
+ this.skipLF = false;
1882
+ }
1883
+ const s = this.rest + chunk;
1884
+ const re = /\r\n|\r|\n/g;
1885
+ re.lastIndex = this.rest.length;
1886
+ let start = 0;
1887
+ let m;
1888
+ while (m = re.exec(s)) {
1889
+ if (re.lastIndex === s.length && m[0] === "\r")
1890
+ this.skipLF = true;
1891
+ this.line(s.slice(start, m.index));
1892
+ start = re.lastIndex;
1893
+ }
1894
+ this.rest = s.slice(start);
1895
+ if (this.rest.length + this.data.length > this.max) {
1896
+ throw new Error("event stream frame too large");
1897
+ }
1898
+ }
1899
+ line(l) {
1900
+ if (!l) {
1901
+ this.lastId = this.idBuf;
1902
+ const d = this.data;
1903
+ const t = this.type;
1904
+ this.data = this.type = "";
1905
+ if (d)
1906
+ this.on(t || "message", d.slice(0, -1));
1907
+ return;
1908
+ }
1909
+ if (l[0] === ":")
1910
+ return;
1911
+ const c = l.indexOf(":");
1912
+ const f = c < 0 ? l : l.slice(0, c);
1913
+ let v = c < 0 ? "" : l.slice(c + 1);
1914
+ if (v[0] === " ")
1915
+ v = v.slice(1);
1916
+ if (f === "data")
1917
+ this.data += v + "\n";
1918
+ else if (f === "event")
1919
+ this.type = v;
1920
+ else if (f === "id") {
1921
+ if (!v.includes("\0") && v.length <= this.maxId)
1922
+ this.idBuf = v;
1923
+ } else if (f === "retry" && /^\d+$/.test(v))
1924
+ this.retry = +v;
1925
+ }
1926
+ };
1927
+
1928
+ // src/utils/sse-consumer.ts
1929
+ var MAX_PENDING_CHARS = 1048576;
1930
+ var MAX_EVENT_ID_LENGTH = 256;
1931
+ function resumeId(lastId) {
1932
+ return /^[\x20-\x7e]+$/.test(lastId) ? lastId : void 0;
1933
+ }
1934
+ async function* consumeSse(opts) {
1935
+ const initialBackoffMs = opts.initialBackoffMs ?? 1e3;
1936
+ const maxBackoffMs = opts.maxBackoffMs ?? 3e4;
1937
+ const pending = [];
1938
+ const parser = new SseParser(
1939
+ (type, data) => pending.push({ type, data, lastEventId: parser.lastId || void 0 }),
1940
+ { maxPending: MAX_PENDING_CHARS, maxIdLength: MAX_EVENT_ID_LENGTH }
1941
+ );
1942
+ let backoffMs = initialBackoffMs;
1943
+ const onLifecycle = opts.onLifecycle ?? noop;
1944
+ while (!opts.signal.aborted) {
1945
+ const headers = {
1946
+ Accept: "text/event-stream",
1947
+ "Cache-Control": "no-cache",
1948
+ ...opts.headers
1949
+ };
1950
+ const lastEventId = resumeId(parser.lastId);
1951
+ if (lastEventId !== void 0) {
1952
+ headers["Last-Event-ID"] = lastEventId;
1953
+ }
1954
+ onLifecycle({ kind: "connecting", lastEventId });
1955
+ let response;
1956
+ try {
1957
+ response = await fetch(opts.url, {
1958
+ method: "GET",
1959
+ headers,
1960
+ signal: opts.signal
1961
+ });
1962
+ } catch (err) {
1963
+ if (opts.signal.aborted) return;
1964
+ const reason = err instanceof Error ? err.message : String(err);
1965
+ onLifecycle({ kind: "disconnected", reason, willRetryInMs: backoffMs });
1966
+ await sleep(backoffMs, opts.signal);
1967
+ backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
1968
+ continue;
1969
+ }
1970
+ if (response.status === 401 || response.status === 403) {
1971
+ const body = await safeReadText(response);
1972
+ onLifecycle({ kind: "auth_failed", status: response.status, body });
1973
+ throw new SseAuthError(response.status, body);
1974
+ }
1975
+ if (!response.ok) {
1976
+ const body = await safeReadText(response);
1977
+ onLifecycle({
1978
+ kind: "disconnected",
1979
+ reason: `HTTP ${response.status}: ${truncate3(body, 200)}`,
1980
+ willRetryInMs: backoffMs
1981
+ });
1982
+ await sleep(backoffMs, opts.signal);
1983
+ backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
1984
+ continue;
1985
+ }
1986
+ if (!response.body) {
1987
+ onLifecycle({
1988
+ kind: "disconnected",
1989
+ reason: "response has no body",
1990
+ willRetryInMs: backoffMs
1991
+ });
1992
+ await sleep(backoffMs, opts.signal);
1993
+ backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
1994
+ continue;
1995
+ }
1996
+ onLifecycle({ kind: "connected" });
1997
+ backoffMs = initialBackoffMs;
1998
+ parser.reset();
1999
+ const reader = response.body.getReader();
2000
+ const decoder = new TextDecoder("utf-8");
2001
+ let serverInitiatedReconnect = false;
2002
+ try {
2003
+ while (!opts.signal.aborted) {
2004
+ const { value, done } = await reader.read();
2005
+ if (done) break;
2006
+ let feedError;
2007
+ try {
2008
+ parser.feed(decoder.decode(value, { stream: true }));
2009
+ } catch (err) {
2010
+ feedError = err;
2011
+ }
2012
+ for (const ev of pending.splice(0)) {
2013
+ if (ev.type === "timeout") {
2014
+ yield ev;
2015
+ serverInitiatedReconnect = true;
2016
+ } else {
2017
+ yield ev;
2018
+ }
2019
+ }
2020
+ if (feedError !== void 0) throw feedError;
2021
+ }
2022
+ } catch (err) {
2023
+ if (opts.signal.aborted) return;
2024
+ const reason = err instanceof Error ? err.message : String(err);
2025
+ onLifecycle({ kind: "disconnected", reason, willRetryInMs: backoffMs });
2026
+ } finally {
2027
+ try {
2028
+ await reader.cancel();
2029
+ } catch {
2030
+ }
2031
+ }
2032
+ if (opts.signal.aborted) return;
2033
+ if (serverInitiatedReconnect) {
2034
+ continue;
2035
+ }
2036
+ onLifecycle({
2037
+ kind: "disconnected",
2038
+ reason: "stream ended",
2039
+ willRetryInMs: backoffMs
2040
+ });
2041
+ await sleep(backoffMs, opts.signal);
2042
+ backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
2043
+ }
2044
+ }
2045
+ var SseAuthError = class extends Error {
2046
+ constructor(status, body) {
2047
+ super(
2048
+ `SSE auth failed: HTTP ${status}${body ? ` \u2014 ${truncate3(body, 200)}` : ""}`
2049
+ );
2050
+ this.status = status;
2051
+ this.body = body;
2052
+ this.name = "SseAuthError";
2053
+ }
2054
+ status;
2055
+ body;
2056
+ };
2057
+ function sleep(ms, signal) {
2058
+ return new Promise((resolve2) => {
2059
+ const timer = setTimeout(() => {
2060
+ signal.removeEventListener("abort", onAbort);
2061
+ resolve2();
2062
+ }, ms);
2063
+ const onAbort = () => {
2064
+ clearTimeout(timer);
2065
+ resolve2();
2066
+ };
2067
+ signal.addEventListener("abort", onAbort, { once: true });
2068
+ });
2069
+ }
2070
+ async function safeReadText(response) {
2071
+ try {
2072
+ return await response.text();
2073
+ } catch {
2074
+ return "";
2075
+ }
2076
+ }
2077
+ function truncate3(s, n) {
2078
+ return s.length <= n ? s : `${s.slice(0, n - 3)}...`;
2079
+ }
2080
+ function noop() {
2081
+ }
2082
+
2083
+ // src/utils/listen-output.ts
2084
+ import chalk13 from "chalk";
2085
+ function formatAttemptHuman(payload, forward) {
2086
+ const ts = formatTimestamp(payload.timestamp);
2087
+ const eventType = chalk13.cyan(payload.event_type.padEnd(24));
2088
+ const eventId = chalk13.gray(payload.event_id.padEnd(28));
2089
+ const attempt = chalk13.gray(`attempt ${payload.attempt}`);
2090
+ const arrow = chalk13.gray("\u2192");
2091
+ const upstream = formatUpstream(payload);
2092
+ const latency = chalk13.gray(`${payload.latency_ms}ms`);
2093
+ const liveBadge = payload.livemode ? chalk13.red(" [LIVE]") : "";
2094
+ let line = `[${ts}] ${eventType} ${eventId} ${attempt} ${arrow} ${upstream} ${latency}${liveBadge}`;
2095
+ if (forward && forward.attempted) {
2096
+ const fwdArrow = chalk13.gray("\u2192 local");
2097
+ if (forward.error) {
2098
+ line += ` ${fwdArrow} ${chalk13.red(forward.error)}`;
2099
+ } else if (forward.status !== null) {
2100
+ const fwdStatus = formatStatus(forward.status);
2101
+ const fwdLatency = chalk13.gray(`${forward.latencyMs}ms`);
2102
+ line += ` ${fwdArrow} ${fwdStatus} ${fwdLatency}`;
2103
+ }
2104
+ }
2105
+ return line;
2106
+ }
2107
+ function formatAttemptJson(payload, forward) {
2108
+ const body = {
2109
+ type: "webhook.attempt",
2110
+ event_id: payload.event_id,
2111
+ event_type: payload.event_type,
2112
+ subscription_id: payload.subscription_id,
2113
+ attempt: payload.attempt,
2114
+ succeeded: payload.succeeded,
2115
+ response_status: payload.response_status,
2116
+ latency_ms: payload.latency_ms,
2117
+ timestamp: payload.timestamp,
2118
+ livemode: payload.livemode
2119
+ };
2120
+ if (forward && forward.attempted) {
2121
+ body.forward = {
2122
+ status: forward.status,
2123
+ latency_ms: forward.latencyMs,
2124
+ error: forward.error
2125
+ };
2126
+ }
2127
+ return JSON.stringify(body);
2128
+ }
2129
+ function formatLifecycleHuman(message, kind) {
2130
+ const tag = kind === "info" ? chalk13.cyan("[vonpay]") : kind === "warn" ? chalk13.yellow("[vonpay]") : chalk13.red("[vonpay]");
2131
+ return `${tag} ${message}`;
2132
+ }
2133
+ function formatLifecycleJson(kind, extra = {}) {
2134
+ return JSON.stringify({ type: `cli.${kind}`, ...extra, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
2135
+ }
2136
+ function formatStatus(status) {
2137
+ if (status >= 200 && status < 300) return chalk13.green(String(status));
2138
+ if (status >= 400 && status < 500) return chalk13.yellow(String(status));
2139
+ if (status >= 500) return chalk13.red(String(status));
2140
+ return chalk13.gray(String(status));
2141
+ }
2142
+ function formatUpstream(payload) {
2143
+ if (payload.response_status === null) {
2144
+ return chalk13.red("timeout");
2145
+ }
2146
+ return formatStatus(payload.response_status);
2147
+ }
2148
+ function formatTimestamp(iso) {
2149
+ const date = new Date(iso);
2150
+ if (isNaN(date.getTime())) return iso.slice(0, 19);
2151
+ const h = String(date.getHours()).padStart(2, "0");
2152
+ const m = String(date.getMinutes()).padStart(2, "0");
2153
+ const s = String(date.getSeconds()).padStart(2, "0");
2154
+ return `${h}:${m}:${s}`;
2155
+ }
2156
+
2157
+ // src/commands/listen.ts
2158
+ function listenCommand() {
2159
+ return new Command10("listen").description(
2160
+ "Watch webhook delivery attempts in real time. Optionally re-forward each event to a local URL for development."
2161
+ ).option(
2162
+ "--forward-to <url>",
2163
+ "Local URL to receive a shadow copy of each delivered event (loopback only by default)"
2164
+ ).option(
2165
+ "--forward-to-tunnel",
2166
+ "Allow non-loopback --forward-to URLs (for ngrok / Cloudflare Tunnel workflows)"
2167
+ ).option(
2168
+ "--events <type>",
2169
+ "Only stream attempts for the given event type. Repeat the flag for multiple types.",
2170
+ (value, prev) => prev ? [...prev, value] : [value]
2171
+ ).option("--json", "Emit one JSON object per line on stdout instead of the colored transcript").option("--api-key <key>", "Override the API key from env / config").addOption(retiredConfirmLiveOption()).action(async (opts) => {
2172
+ const apiKey = opts.apiKey ?? requireApiKey();
2173
+ if (opts.apiKey !== void 0) warnLiveSecretFlag(opts.apiKey, "--api-key");
2174
+ const refuse = (message) => {
2175
+ if (jsonErrorsEnabled()) exitWithCliError(message, message);
2176
+ process.stderr.write(formatLifecycleHuman(message, "error") + "\n");
2177
+ return process.exit(1);
2178
+ };
2179
+ if (secretKeyMode(apiKey) === null) {
2180
+ refuse(
2181
+ "Listen requires a secret key (vp_sk_*). Publishable keys are rejected by the server."
2182
+ );
2183
+ }
2184
+ let forwardUrl;
2185
+ let forwardWarning;
2186
+ if (opts.forwardTo) {
2187
+ const verdict = validateForwardTo(opts.forwardTo, {
2188
+ allowTunnel: opts.forwardToTunnel === true
2189
+ });
2190
+ if (!verdict.ok) refuse(verdict.reason);
2191
+ forwardUrl = verdict.url;
2192
+ forwardWarning = verdict.warning;
2193
+ }
2194
+ const configuredBaseUrl = getBaseUrl();
2195
+ const destinationProblem = keyDestinationProblem(apiKey, configuredBaseUrl);
2196
+ if (destinationProblem) refuse(destinationProblem);
2197
+ const baseUrl = (configuredBaseUrl ?? "https://checkout.vonpay.com").replace(
2198
+ /\/+$/,
2199
+ ""
2200
+ );
2201
+ await confirmLiveAction(
2202
+ apiKey,
2203
+ {
2204
+ action: "stream live webhook delivery attempts",
2205
+ details: [
2206
+ ["Stream from", baseUrl],
2207
+ ...forwardUrl ? [["Forward to", forwardUrl.toString()]] : []
2208
+ ],
2209
+ consequence: forwardUrl ? "Live payment events, with buyer and order details, will be shown here, and each one is signed with your LIVE key and sent to the forward URL." : "Live payment events, with buyer and order details, will be shown here."
2210
+ },
2211
+ opts
2212
+ );
2213
+ const sseUrl = new URL(`${baseUrl}/v1/webhook_listen`);
2214
+ if (opts.events && opts.events.length > 0) {
2215
+ sseUrl.searchParams.set("event_types", opts.events.join(","));
2216
+ }
2217
+ const sdk = forwardUrl ? new VonPayCheckout2({
2218
+ apiKey,
2219
+ baseUrl,
2220
+ allowCustomBaseUrl: allowCustomBaseUrl(),
2221
+ appInfo: CLI_APP_INFO,
2222
+ errorReporter: SILENT_ERROR_REPORTER
2223
+ }) : null;
2224
+ const controller = new AbortController();
2225
+ let eventCount = 0;
2226
+ const startedAt = Date.now();
2227
+ const mode = opts.json === true ? "json" : "human";
2228
+ const onSignal = (sig) => {
2229
+ const elapsedSec = Math.round((Date.now() - startedAt) / 1e3);
2230
+ if (mode === "human") {
2231
+ process.stderr.write(
2232
+ "\n" + formatLifecycleHuman(
2233
+ `Shutting down (${sig}). Received ${eventCount} event${eventCount === 1 ? "" : "s"} in ${elapsedSec}s.`,
2234
+ "info"
2235
+ ) + "\n"
2236
+ );
2237
+ } else {
2238
+ process.stderr.write(
2239
+ formatLifecycleJson("shutdown", {
2240
+ signal: sig,
2241
+ event_count: eventCount,
2242
+ elapsed_seconds: elapsedSec
2243
+ }) + "\n"
2244
+ );
2245
+ }
2246
+ controller.abort();
2247
+ };
2248
+ process.once("SIGINT", () => onSignal("SIGINT"));
2249
+ process.once("SIGTERM", () => onSignal("SIGTERM"));
2250
+ const onLifecycle = (event) => {
2251
+ if (mode === "json") {
2252
+ process.stderr.write(
2253
+ formatLifecycleJson(event.kind, lifecyclePayload(event)) + "\n"
2254
+ );
2255
+ return;
2256
+ }
2257
+ process.stderr.write(formatLifecycleHuman(lifecycleHumanMessage(event), lifecycleSeverity(event.kind)) + "\n");
2258
+ };
2259
+ if (forwardWarning) {
2260
+ process.stderr.write(
2261
+ mode === "human" ? formatLifecycleHuman(forwardWarning, "warn") + "\n" : formatLifecycleJson("warning", { message: forwardWarning }) + "\n"
2262
+ );
2263
+ }
2264
+ if (mode === "human") {
2265
+ const filterNote = opts.events?.length ? ` (filtered to ${opts.events.join(", ")})` : "";
2266
+ const fwdNote = forwardUrl ? ` \u2192 forwarding to ${forwardUrl.toString()}` : "";
2267
+ process.stderr.write(
2268
+ formatLifecycleHuman(`Ready! Listening for webhook delivery attempts${filterNote}${fwdNote}.`, "info") + "\n"
2269
+ );
2270
+ } else {
2271
+ process.stderr.write(
2272
+ formatLifecycleJson("ready", {
2273
+ base_url: baseUrl,
2274
+ filter: opts.events ?? null,
2275
+ forward_to: forwardUrl?.toString() ?? null
2276
+ }) + "\n"
2277
+ );
2278
+ }
2279
+ try {
2280
+ for await (const sseEvent of consumeSse({
2281
+ url: sseUrl.toString(),
2282
+ headers: { Authorization: `Bearer ${apiKey}` },
2283
+ signal: controller.signal,
2284
+ onLifecycle
2285
+ })) {
2286
+ if (sseEvent.type === "timeout") {
2287
+ onLifecycle({
2288
+ kind: "disconnected",
2289
+ reason: "server-ceiling (1h cap; auto-reconnecting)",
2290
+ willRetryInMs: 0
2291
+ });
2292
+ continue;
2293
+ }
2294
+ if (sseEvent.type !== "webhook.attempt") {
2295
+ continue;
2296
+ }
2297
+ let payload;
2298
+ try {
2299
+ payload = JSON.parse(sseEvent.data);
2300
+ } catch {
2301
+ continue;
2302
+ }
2303
+ eventCount += 1;
2304
+ let forwardOutcome = null;
2305
+ if (forwardUrl && sdk) {
2306
+ try {
2307
+ const eventRecord = await sdk.webhookEvents.retrieve(payload.event_id);
2308
+ forwardOutcome = await forwardPayload(eventRecord.payload, {
2309
+ url: forwardUrl,
2310
+ signingSecret: apiKey,
2311
+ signal: controller.signal
2312
+ });
2313
+ } catch (err) {
2314
+ const message = err instanceof Error ? err.message : String(err);
2315
+ forwardOutcome = {
2316
+ attempted: true,
2317
+ status: null,
2318
+ latencyMs: null,
2319
+ error: `fetch-event failed: ${message.slice(0, 200)}`
2320
+ };
2321
+ }
2322
+ }
2323
+ if (mode === "human") {
2324
+ process.stdout.write(formatAttemptHuman(payload, forwardOutcome) + "\n");
2325
+ } else {
2326
+ process.stdout.write(formatAttemptJson(payload, forwardOutcome) + "\n");
2327
+ }
2328
+ }
2329
+ } catch (err) {
2330
+ if (err instanceof SseAuthError) {
2331
+ if (mode === "human") {
2332
+ process.stderr.write(
2333
+ formatLifecycleHuman(
2334
+ `Authentication failed (HTTP ${err.status}). Run \`vonpay checkout login\` with a valid secret key.`,
2335
+ "error"
2336
+ ) + "\n"
2337
+ );
2338
+ } else {
2339
+ process.stderr.write(
2340
+ formatLifecycleJson("auth_failed", { status: err.status }) + "\n"
2341
+ );
2342
+ }
2343
+ process.exit(1);
2344
+ }
2345
+ if (jsonErrorsEnabled()) handleError(err);
2346
+ throw err;
2347
+ }
2348
+ });
2349
+ }
2350
+ function lifecyclePayload(event) {
2351
+ switch (event.kind) {
2352
+ case "connecting":
2353
+ return { last_event_id: event.lastEventId ?? null };
2354
+ case "connected":
2355
+ return {};
2356
+ case "disconnected":
2357
+ return { reason: event.reason, will_retry_in_ms: event.willRetryInMs };
2358
+ case "auth_failed":
2359
+ return { status: event.status };
2360
+ case "fatal":
2361
+ return { error: event.error.message };
2362
+ }
2363
+ }
2364
+ function lifecycleHumanMessage(event) {
2365
+ switch (event.kind) {
2366
+ case "connecting":
2367
+ return event.lastEventId ? `Reconnecting (resume from ${event.lastEventId})` : "Connecting...";
2368
+ case "connected":
2369
+ return "Connected.";
2370
+ case "disconnected":
2371
+ return `Disconnected: ${event.reason}. Retrying in ${event.willRetryInMs}ms.`;
2372
+ case "auth_failed":
2373
+ return `Authentication failed (HTTP ${event.status}).`;
2374
+ case "fatal":
2375
+ return `Fatal: ${event.error.message}`;
2376
+ }
2377
+ }
2378
+ function lifecycleSeverity(kind) {
2379
+ switch (kind) {
2380
+ case "connecting":
2381
+ case "connected":
2382
+ return "info";
2383
+ case "disconnected":
2384
+ return "warn";
2385
+ case "auth_failed":
2386
+ case "fatal":
2387
+ return "error";
2388
+ }
2389
+ }
2390
+
2391
+ // src/commands/health.ts
2392
+ import { Command as Command11 } from "commander";
2393
+ import chalk14 from "chalk";
2394
+ import { VonPayError as VonPayError2 } from "@vonpay/checkout-node";
2395
+ function healthCommand() {
2396
+ return new Command11("health").description("Check checkout API health").option("--json", "Output raw JSON").action(async (opts) => {
2397
+ const client = makeCheckoutClient();
2398
+ const spinner = startSpinner("Checking API health...");
2399
+ try {
2400
+ const health = await client.health();
2401
+ spinner.stop();
2402
+ if (opts.json) {
2403
+ console.log(JSON.stringify(health, null, 2));
2404
+ } else {
2405
+ const statusColor = health.status === "ok" ? chalk14.green : health.status === "degraded" ? chalk14.yellow : chalk14.red;
2406
+ console.log(` Status: ${statusColor(health.status)}`);
2407
+ console.log(` Latency: ${health.latencyMs}ms`);
2408
+ if (health.version) {
2409
+ console.log(` Version: ${health.version}`);
2410
+ }
2411
+ }
2412
+ } catch (err) {
2413
+ spinner.stop();
2414
+ if (jsonErrorsEnabled()) handleError(err);
2415
+ if (err instanceof VonPayError2) {
2416
+ console.error(chalk14.red(`API error: ${scrubSecrets(err.message)}`));
2417
+ console.error(chalk14.dim(` Code: ${err.code}`));
2418
+ } else {
2419
+ console.error(
2420
+ chalk14.red(
2421
+ `Cannot reach API: ${scrubSecrets(err instanceof Error ? err.message : String(err))}`
2422
+ )
2423
+ );
2424
+ }
2425
+ process.exit(1);
2426
+ }
2427
+ });
2428
+ }
2429
+
2430
+ // src/commands/doctor.ts
2431
+ import { Command as Command12 } from "commander";
2432
+ import chalk15 from "chalk";
2433
+ import ora3 from "ora";
2434
+ import { homedir as homedir2 } from "os";
2435
+ import { existsSync as existsSync3 } from "fs";
2436
+ import {
2437
+ VonPayCheckout as VonPayCheckout3,
2438
+ VonPayError as VonPayError3,
2439
+ helpFor
2440
+ } from "@vonpay/checkout-node";
2441
+ var RELEVANT_ENV_VARS = [
2442
+ "VON_PAY_SECRET_KEY",
2443
+ "VON_PAY_PUBLISHABLE_KEY",
2444
+ "VON_PAY_SESSION_SECRET",
2445
+ "VONPAY_BASE_URL",
2446
+ "VONPAY_QUIET",
2447
+ "NEXT_PUBLIC_BASE_URL"
2448
+ ];
2449
+ function redactHome(p) {
2450
+ const home = homedir2();
2451
+ if (home && p.startsWith(home)) {
2452
+ return "~" + p.slice(home.length);
2453
+ }
2454
+ return p;
2455
+ }
2456
+ function maskPrefix(key) {
2457
+ if (!key) return void 0;
2458
+ const m = key.match(/^(vp_[ps]k_(test|live)_)([A-Za-z0-9_-]{0,4})/);
2459
+ if (!m) return void 0;
2460
+ return `${m[1]}${m[3]}\u2026`;
2461
+ }
2462
+ function keyMode(key) {
2463
+ if (!key) return void 0;
2464
+ return secretKeyMode(key) ?? "not_a_secret_key";
2465
+ }
2466
+ async function gather() {
2467
+ const config = loadConfig();
2468
+ const apiKey = process.env.VON_PAY_SECRET_KEY || config.checkout?.apiKey;
2469
+ const baseUrl = process.env.VONPAY_BASE_URL ?? "https://checkout.vonpay.com";
2470
+ const envVarsPresent = RELEVANT_ENV_VARS.filter(
2471
+ (n) => process.env[n] && process.env[n] !== ""
2472
+ );
2473
+ const report = {
2474
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2475
+ cli: { version: CLI_VERSION },
2476
+ runtime: {
2477
+ node: process.version,
2478
+ platform: process.platform,
2479
+ arch: process.arch,
2480
+ cwd: redactHome(process.cwd())
2481
+ },
2482
+ envVarsPresent,
2483
+ configFile: {
2484
+ path: `${process.env.HOME ?? process.env.USERPROFILE ?? "~"}/.vonpay/config.json`,
2485
+ present: existsSync3(
2486
+ `${process.env.HOME ?? process.env.USERPROFILE ?? ""}/.vonpay/config.json`
2487
+ ),
2488
+ keyType: config.checkout?.apiKey ? keyMode(config.checkout?.apiKey) : void 0
2489
+ },
2490
+ apiKeyPrefix: maskPrefix(apiKey),
2491
+ apiKeyMode: keyMode(apiKey),
2492
+ health: { baseUrl, reachable: false },
2493
+ hints: []
2494
+ };
2495
+ if (!apiKey) {
2496
+ report.hints.push(
2497
+ "No API key found. Set VON_PAY_SECRET_KEY in env, or run `vonpay checkout login` to save one. Without a key the health probe still runs but sessions.validate is skipped."
2498
+ );
2499
+ }
2500
+ const shapeProblem = keyDestinationProblem("", process.env.VONPAY_BASE_URL);
2501
+ if (shapeProblem) {
2502
+ report.health.baseUrl = "(not shown: the base URL was refused)";
2503
+ report.health.error = shapeProblem;
2504
+ report.hints.push(`${shapeProblem} No probe was run.`);
2505
+ }
2506
+ const start = Date.now();
2507
+ if (!shapeProblem) try {
2508
+ const controller = new AbortController();
2509
+ const timer = setTimeout(() => controller.abort(), 1e4);
2510
+ const res = await fetch(`${baseUrl}/api/health`, {
2511
+ method: "GET",
2512
+ headers: {
2513
+ Accept: "application/json",
2514
+ "User-Agent": `vonpay-cli/${CLI_VERSION} (doctor)`
2515
+ },
2516
+ signal: controller.signal
2517
+ });
2518
+ clearTimeout(timer);
2519
+ report.health.latencyMs = Date.now() - start;
2520
+ if (res.ok) {
2521
+ const body = await res.json().catch(() => ({}));
2522
+ report.health.reachable = true;
2523
+ report.health.status = body.status ?? "ok";
2524
+ } else {
2525
+ report.health.reachable = false;
2526
+ report.health.status = `http_${res.status}`;
2527
+ report.health.error = `HTTP ${res.status}`;
2528
+ report.hints.push(
2529
+ `Health endpoint returned HTTP ${res.status}. Either the API is having an incident or your baseUrl override is wrong.`
2530
+ );
2531
+ }
2532
+ } catch (err) {
2533
+ report.health.error = err instanceof Error ? err.message : String(err);
2534
+ report.health.latencyMs = Date.now() - start;
2535
+ report.hints.push(
2536
+ "Health probe could not reach the API. Check network / DNS / firewall \u2014 vonpay-checkout requires outbound HTTPS to checkout.vonpay.com."
2537
+ );
2538
+ }
2539
+ let mayProbe = Boolean(apiKey) && !shapeProblem;
2540
+ if (apiKey && report.apiKeyMode === "not_a_secret_key") {
2541
+ mayProbe = false;
2542
+ report.hints.push(`${NOT_A_SECRET_KEY_MESSAGE} The sessions.validate probe was not run.`);
2543
+ }
2544
+ const destinationProblem = apiKey && mayProbe ? keyDestinationProblem(apiKey, process.env.VONPAY_BASE_URL) : null;
2545
+ if (destinationProblem) {
2546
+ mayProbe = false;
2547
+ report.hints.push(`${destinationProblem} The sessions.validate probe was not run.`);
2548
+ }
2549
+ if (config.checkout?.apiKey && report.configFile.keyType === "live") {
2550
+ report.hints.push(
2551
+ "A LIVE key is saved in plaintext in ~/.vonpay/config.json. Live keys are no longer stored: run `vonpay checkout logout` to delete it, and supply it through VON_PAY_SECRET_KEY for the session instead."
2552
+ );
2553
+ }
2554
+ if (apiKey && mayProbe && report.health.reachable) {
2555
+ try {
2556
+ const probeClient = new VonPayCheckout3({
2557
+ apiKey,
2558
+ baseUrl,
2559
+ allowCustomBaseUrl: allowCustomBaseUrl(),
2560
+ timeout: 1e4,
2561
+ maxRetries: 0,
2562
+ errorReporter: () => {
2563
+ },
2564
+ // suppress default warn during the probe
2565
+ appInfo: CLI_APP_INFO
2566
+ });
2567
+ const result = await probeClient.sessions.validate({
2568
+ amount: 1499,
2569
+ currency: "USD"
2570
+ });
2571
+ report.validateProbe = {
2572
+ ok: result.valid,
2573
+ warnings: result.warnings
2574
+ };
2575
+ if (!result.valid) {
2576
+ report.hints.push(
2577
+ `sessions.validate returned valid=false with warnings: ${(result.warnings ?? []).join("; ")}.`
2578
+ );
2579
+ } else if (result.warnings && result.warnings.length > 0) {
2580
+ report.hints.push(
2581
+ `sessions.validate succeeded but with warnings: ${result.warnings.join("; ")}.`
2582
+ );
2583
+ }
2584
+ } catch (err) {
2585
+ if (err instanceof VonPayError3) {
2586
+ const help = helpFor(err.code);
2587
+ report.validateProbe = {
2588
+ ok: false,
2589
+ error: {
2590
+ code: err.code,
2591
+ status: err.status,
2592
+ nextAction: help.nextAction,
2593
+ llmHint: help.llmHint
2594
+ }
2595
+ };
2596
+ report.hints.push(
2597
+ `sessions.validate failed: ${err.code}. ${help.llmHint}`
2598
+ );
2599
+ } else {
2600
+ report.validateProbe = {
2601
+ ok: false,
2602
+ error: {
2603
+ code: "internal_error",
2604
+ status: 0,
2605
+ nextAction: "contact_support",
2606
+ llmHint: err instanceof Error ? err.message : String(err)
2607
+ }
2608
+ };
2609
+ }
2610
+ }
2611
+ }
2612
+ return report;
2613
+ }
2614
+ function renderHuman(r) {
2615
+ const lines = [];
2616
+ lines.push(chalk15.bold.blue("vonpay doctor \u2014 diagnostic report"));
2617
+ lines.push(chalk15.dim(`generated ${r.generatedAt}`));
2618
+ lines.push("");
2619
+ lines.push(chalk15.bold("Runtime"));
2620
+ lines.push(` CLI: ${r.cli.version}`);
2621
+ lines.push(` Node: ${r.runtime.node}`);
2622
+ lines.push(` Platform: ${r.runtime.platform} (${r.runtime.arch})`);
2623
+ lines.push(` CWD: ${r.runtime.cwd}`);
2624
+ lines.push("");
2625
+ lines.push(chalk15.bold("API key"));
2626
+ if (r.apiKeyPrefix) {
2627
+ lines.push(` Prefix: ${r.apiKeyPrefix}`);
2628
+ lines.push(` Mode: ${r.apiKeyMode}`);
2629
+ } else {
2630
+ lines.push(chalk15.dim(" (no key found in env or ~/.vonpay/config.json)"));
2631
+ }
2632
+ lines.push("");
2633
+ lines.push(chalk15.bold("Env vars present (names only)"));
2634
+ if (r.envVarsPresent.length === 0) {
2635
+ lines.push(chalk15.dim(" (none of the expected vonpay env vars are set)"));
2636
+ } else {
2637
+ for (const n of r.envVarsPresent) lines.push(` \u2713 ${n}`);
2638
+ }
2639
+ lines.push("");
2640
+ lines.push(chalk15.bold("Health probe"));
2641
+ if (r.health.reachable) {
2642
+ const statusColor = r.health.status === "ok" ? chalk15.green : r.health.status === "degraded" ? chalk15.yellow : chalk15.red;
2643
+ lines.push(` Reachable: ${chalk15.green("yes")}`);
2644
+ lines.push(` Status: ${statusColor(r.health.status ?? "?")}`);
2645
+ lines.push(` Latency: ${r.health.latencyMs}ms`);
2646
+ } else {
2647
+ lines.push(` Reachable: ${chalk15.red("no")}`);
2648
+ lines.push(` Error: ${r.health.error}`);
2649
+ }
2650
+ lines.push("");
2651
+ if (r.validateProbe) {
2652
+ lines.push(chalk15.bold("sessions.validate probe ($14.99 USD dry-run)"));
2653
+ if (r.validateProbe.ok) {
2654
+ lines.push(` Result: ${chalk15.green("valid")}`);
2655
+ } else {
2656
+ lines.push(` Result: ${chalk15.red("invalid")}`);
2657
+ if (r.validateProbe.error) {
2658
+ lines.push(` Code: ${r.validateProbe.error.code}`);
2659
+ lines.push(` Action: ${r.validateProbe.error.nextAction}`);
2660
+ }
2661
+ }
2662
+ if (r.validateProbe.warnings && r.validateProbe.warnings.length > 0) {
2663
+ lines.push(` Warnings:`);
2664
+ for (const w of r.validateProbe.warnings) lines.push(` - ${w}`);
2665
+ }
2666
+ lines.push("");
2667
+ }
2668
+ if (r.hints.length > 0) {
2669
+ lines.push(chalk15.bold("Hints"));
2670
+ for (const h of r.hints) lines.push(` \u2022 ${h}`);
2671
+ lines.push("");
2672
+ }
2673
+ return lines.join("\n");
2674
+ }
2675
+ function renderForLlm(r) {
2676
+ const out = [];
2677
+ out.push("# vonpay doctor \u2014 diagnostic report (LLM-readable)");
2678
+ out.push("");
2679
+ out.push("This is a structured environment + live-API diagnostic from the");
2680
+ out.push("Von Payments CLI. An LLM agent reading this should determine the");
2681
+ out.push("most-likely root cause of the integrator's problem and produce");
2682
+ out.push("specific next-step instructions.");
2683
+ out.push("");
2684
+ out.push(`generated_at: ${r.generatedAt}`);
2685
+ out.push("");
2686
+ out.push("## Runtime");
2687
+ out.push(`- vonpay-cli: ${r.cli.version}`);
2688
+ out.push(`- node: ${r.runtime.node}`);
2689
+ out.push(`- platform: ${r.runtime.platform} (${r.runtime.arch})`);
2690
+ out.push("");
2691
+ out.push("## API key");
2692
+ if (r.apiKeyPrefix) {
2693
+ out.push(`- prefix: \`${r.apiKeyPrefix}\` (first 4 chars of payload only \u2014 full key never printed)`);
2694
+ out.push(`- mode: ${r.apiKeyMode}`);
2695
+ } else {
2696
+ out.push("- no key found in env or `~/.vonpay/config.json`");
2697
+ out.push("- LLM-action: ask the human to set `VON_PAY_SECRET_KEY` or run `vonpay checkout login`");
2698
+ }
2699
+ out.push("");
2700
+ out.push("## Env vars present (names only \u2014 values never printed)");
2701
+ if (r.envVarsPresent.length === 0) {
2702
+ out.push("- (none of the expected vonpay env vars are set)");
2703
+ } else {
2704
+ for (const n of r.envVarsPresent) out.push(`- \`${n}\``);
2705
+ }
2706
+ out.push("");
2707
+ out.push("## Health probe");
2708
+ out.push(`- base_url: ${r.health.baseUrl}`);
2709
+ out.push(`- reachable: ${r.health.reachable}`);
2710
+ if (r.health.latencyMs !== void 0) {
2711
+ out.push(`- latency_ms: ${r.health.latencyMs}`);
2712
+ }
2713
+ if (r.health.status) out.push(`- status: ${r.health.status}`);
2714
+ if (r.health.error) out.push(`- error: ${r.health.error}`);
2715
+ out.push("");
2716
+ if (r.validateProbe) {
2717
+ out.push("## sessions.validate probe ($14.99 USD dry-run)");
2718
+ out.push(`- ok: ${r.validateProbe.ok}`);
2719
+ if (r.validateProbe.warnings && r.validateProbe.warnings.length > 0) {
2720
+ out.push("- warnings:");
2721
+ for (const w of r.validateProbe.warnings) out.push(` - ${w}`);
2722
+ }
2723
+ if (r.validateProbe.error) {
2724
+ out.push("- error:");
2725
+ out.push(` - code: \`${r.validateProbe.error.code}\``);
2726
+ out.push(` - status: ${r.validateProbe.error.status}`);
2727
+ out.push(` - next_action: \`${r.validateProbe.error.nextAction}\``);
2728
+ out.push(` - llm_hint: ${r.validateProbe.error.llmHint}`);
2729
+ }
2730
+ out.push("");
2731
+ }
2732
+ if (r.hints.length > 0) {
2733
+ out.push("## Hints (in priority order)");
2734
+ for (const h of r.hints) out.push(`- ${h}`);
2735
+ out.push("");
2736
+ }
2737
+ out.push("## Suggested LLM next steps");
2738
+ out.push("1. Determine which of the three categories applies: missing-config / unreachable-api / failing-validation.");
2739
+ out.push("2. For each hint above, propose ONE specific code or env change to the human.");
2740
+ out.push("3. Suggest the human re-run `vonpay checkout doctor --for-llm` after applying changes and paste the new output.");
2741
+ out.push("4. If the report shows everything healthy but the integrator still has a problem, the issue is likely in their own code (not vonpay) \u2014 ask them to share the failing call site.");
2742
+ out.push("");
2743
+ return out.join("\n");
2744
+ }
2745
+ function doctorCommand() {
2746
+ return new Command12("doctor").description(
2747
+ "Diagnose your Von Payments environment \u2014 runtime, env vars, live API probe; outputs human / JSON / LLM-readable formats"
2748
+ ).option("--json", "Output JSON (machine-parseable)").option(
2749
+ "--for-llm",
2750
+ "Output LLM-friendly markdown (paste into Claude, ChatGPT, Cursor)"
2751
+ ).action(async (opts) => {
2752
+ const spinner = ora3({
2753
+ text: "Running diagnostics...",
2754
+ // suppress spinner output when emitting JSON / LLM markdown so the
2755
+ // output remains pipeable
2756
+ isSilent: opts.json || opts.forLlm
2757
+ }).start();
2758
+ const report = await gather();
2759
+ spinner.stop();
2760
+ if (opts.json) {
2761
+ console.log(JSON.stringify(report, null, 2));
2762
+ } else if (opts.forLlm) {
2763
+ console.log(renderForLlm(report));
2764
+ } else {
2765
+ console.log(renderHuman(report));
2766
+ }
2767
+ });
2768
+ }
2769
+
2770
+ // src/commands/checkout.ts
2771
+ function registerCheckout(program2) {
2772
+ const checkout = program2.command("checkout").description("Checkout integration tools");
2773
+ checkout.hook("preAction", (_checkout, actionCommand) => {
2774
+ setJsonErrors(actionCommand.opts().json === true);
2775
+ });
2776
+ checkout.addCommand(loginCommand());
2777
+ checkout.addCommand(logoutCommand());
2778
+ checkout.addCommand(initCommand());
2779
+ checkout.addCommand(sessionsCommand());
2780
+ checkout.addCommand(paymentIntentsCommand());
2781
+ checkout.addCommand(refundsCommand());
2782
+ checkout.addCommand(tokensCommand());
2783
+ checkout.addCommand(capabilitiesCommand());
2784
+ checkout.addCommand(triggerCommand());
2785
+ checkout.addCommand(listenCommand());
2786
+ checkout.addCommand(healthCommand());
2787
+ checkout.addCommand(doctorCommand());
2788
+ }
2789
+
2790
+ // src/index.ts
2791
+ var __dirname = dirname2(fileURLToPath2(import.meta.url));
2792
+ var pkg2 = JSON.parse(
2793
+ readFileSync4(join3(__dirname, "..", "package.json"), "utf-8")
2794
+ );
2795
+ var program = new Command13();
2796
+ program.name("vonpay").description("Von Payments developer tools").version(pkg2.version);
14
2797
  registerCheckout(program);
15
2798
  program.parse();
16
- //# sourceMappingURL=index.js.map