@vonpay/checkout-cli 0.14.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -5
- package/dist/index.js +759 -331
- package/package.json +8 -3
package/dist/index.js
CHANGED
|
@@ -7,13 +7,12 @@ import { dirname as dirname2, join as join3 } from "path";
|
|
|
7
7
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8
8
|
|
|
9
9
|
// src/commands/login.ts
|
|
10
|
-
import { Command } from "commander";
|
|
10
|
+
import { Command, Option } from "commander";
|
|
11
11
|
import { createInterface } from "readline";
|
|
12
12
|
import chalk from "chalk";
|
|
13
|
-
import { isLiveKey } from "@vonpay/checkout-node";
|
|
14
13
|
|
|
15
14
|
// src/config.ts
|
|
16
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
15
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
17
16
|
import { homedir } from "os";
|
|
18
17
|
import { join } from "path";
|
|
19
18
|
|
|
@@ -34,6 +33,17 @@ function emitCliError(json, text) {
|
|
|
34
33
|
return process.exit(1);
|
|
35
34
|
}
|
|
36
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
|
+
|
|
37
47
|
// src/config.ts
|
|
38
48
|
var CONFIG_DIR = join(homedir(), ".vonpay");
|
|
39
49
|
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
@@ -48,7 +58,20 @@ function loadConfig() {
|
|
|
48
58
|
return {};
|
|
49
59
|
}
|
|
50
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
|
+
};
|
|
51
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
|
+
}
|
|
52
75
|
if (!existsSync(CONFIG_DIR)) {
|
|
53
76
|
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
54
77
|
}
|
|
@@ -56,6 +79,7 @@ function saveConfig(config) {
|
|
|
56
79
|
encoding: "utf-8",
|
|
57
80
|
mode: 384
|
|
58
81
|
});
|
|
82
|
+
chmodSync(CONFIG_FILE, 384);
|
|
59
83
|
}
|
|
60
84
|
function clearStoredApiKey() {
|
|
61
85
|
const config = loadConfig();
|
|
@@ -73,7 +97,17 @@ function getApiKey() {
|
|
|
73
97
|
const envKey = process.env.VON_PAY_SECRET_KEY;
|
|
74
98
|
if (envKey) return envKey;
|
|
75
99
|
const config = loadConfig();
|
|
76
|
-
|
|
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
|
+
);
|
|
77
111
|
}
|
|
78
112
|
function requireApiKey() {
|
|
79
113
|
const key = getApiKey();
|
|
@@ -81,21 +115,28 @@ function requireApiKey() {
|
|
|
81
115
|
const message = "No API key found. Run `vonpay checkout login` or set VON_PAY_SECRET_KEY.";
|
|
82
116
|
emitCliError({ error: message, code: null }, message);
|
|
83
117
|
}
|
|
118
|
+
if (secretKeyMode(key) === null) {
|
|
119
|
+
emitCliError({ error: NOT_A_SECRET_KEY_MESSAGE, code: null }, NOT_A_SECRET_KEY_MESSAGE);
|
|
120
|
+
}
|
|
84
121
|
return key;
|
|
85
122
|
}
|
|
86
123
|
|
|
87
124
|
// src/commands/login.ts
|
|
88
|
-
var
|
|
89
|
-
function evaluateCliKeyGate(key,
|
|
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 = {}) {
|
|
90
127
|
if (!key) return { action: "prompt" };
|
|
91
|
-
const
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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;
|
|
99
140
|
}
|
|
100
141
|
function readLine(prompt) {
|
|
101
142
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
@@ -107,18 +148,15 @@ function readLine(prompt) {
|
|
|
107
148
|
});
|
|
108
149
|
}
|
|
109
150
|
function loginCommand() {
|
|
110
|
-
return new Command("login").description("Store
|
|
111
|
-
|
|
112
|
-
|
|
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()
|
|
113
156
|
).action(async (key, opts) => {
|
|
114
157
|
let apiKey = key;
|
|
115
158
|
const verdict = evaluateCliKeyGate(key, opts);
|
|
116
159
|
if (verdict.action === "refuse") {
|
|
117
|
-
console.warn(
|
|
118
|
-
chalk.yellow(
|
|
119
|
-
"Warning: passing keys as a command argument exposes them in shell history and process-list (`ps`). Use interactive mode (`vonpay checkout login`) instead."
|
|
120
|
-
)
|
|
121
|
-
);
|
|
122
160
|
console.error(chalk.red(verdict.reason));
|
|
123
161
|
process.exit(1);
|
|
124
162
|
}
|
|
@@ -131,26 +169,22 @@ function loginCommand() {
|
|
|
131
169
|
"Get your API key from the Von Payments dashboard \u2192 Developers"
|
|
132
170
|
)
|
|
133
171
|
);
|
|
134
|
-
apiKey = await readLine("API key (
|
|
172
|
+
apiKey = await readLine("Sandbox API key (vp_sk_test_*): ");
|
|
135
173
|
}
|
|
136
174
|
if (!apiKey) {
|
|
137
175
|
console.error(chalk.red("No key provided."));
|
|
138
176
|
process.exit(1);
|
|
139
177
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
`Invalid key format. Must start with ${VALID_PREFIXES.join(" or ")}`
|
|
144
|
-
)
|
|
145
|
-
);
|
|
178
|
+
const refusal = evaluateKeyToSave(apiKey);
|
|
179
|
+
if (refusal) {
|
|
180
|
+
console.error(chalk.red(refusal.reason));
|
|
146
181
|
process.exit(1);
|
|
147
182
|
}
|
|
148
183
|
const config = loadConfig();
|
|
149
184
|
config.checkout = { ...config.checkout, apiKey };
|
|
150
185
|
saveConfig(config);
|
|
151
|
-
const mode = apiKey.includes("_test_") ? "test" : "live";
|
|
152
186
|
console.log(
|
|
153
|
-
chalk.green(`\u2713 API key saved (
|
|
187
|
+
chalk.green(`\u2713 API key saved (test mode) \u2192 ~/.vonpay/config.json`)
|
|
154
188
|
);
|
|
155
189
|
});
|
|
156
190
|
}
|
|
@@ -182,9 +216,10 @@ function logoutCommand() {
|
|
|
182
216
|
|
|
183
217
|
// src/commands/init.ts
|
|
184
218
|
import { Command as Command3 } from "commander";
|
|
185
|
-
import { appendFileSync, existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
219
|
+
import { appendFileSync, chmodSync as chmodSync2, existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
186
220
|
import { resolve } from "path";
|
|
187
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.";
|
|
188
223
|
function ensureGitignoreBlocksEnv() {
|
|
189
224
|
if (!existsSync2(".gitignore")) return "no-repo";
|
|
190
225
|
if (envAlreadyIgnored(".gitignore")) {
|
|
@@ -242,20 +277,14 @@ function initCommand() {
|
|
|
242
277
|
);
|
|
243
278
|
process.exit(1);
|
|
244
279
|
}
|
|
245
|
-
const mode = apiKey
|
|
280
|
+
const mode = secretKeyMode(apiKey);
|
|
281
|
+
if (mode === null) {
|
|
282
|
+
console.error(chalk3.red(NOT_A_SECRET_KEY_MESSAGE));
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
246
285
|
if (mode === "live") {
|
|
247
|
-
console.
|
|
248
|
-
|
|
249
|
-
`\u26A0 About to write a LIVE API key to ${absPath}.
|
|
250
|
-
Confirm this is the intended directory, then re-run with --force if you're sure.`
|
|
251
|
-
)
|
|
252
|
-
);
|
|
253
|
-
if (!opts.force) {
|
|
254
|
-
console.error(
|
|
255
|
-
chalk3.red("Refusing to write live key without --force.")
|
|
256
|
-
);
|
|
257
|
-
process.exit(1);
|
|
258
|
-
}
|
|
286
|
+
console.error(chalk3.red(LIVE_KEY_NOT_WRITTEN_MESSAGE));
|
|
287
|
+
process.exit(1);
|
|
259
288
|
}
|
|
260
289
|
const content = [
|
|
261
290
|
"# Von Payments Checkout",
|
|
@@ -264,6 +293,7 @@ function initCommand() {
|
|
|
264
293
|
""
|
|
265
294
|
].join("\n");
|
|
266
295
|
writeFileSync2(envFile, content, { encoding: "utf-8", mode: 384 });
|
|
296
|
+
chmodSync2(envFile, 384);
|
|
267
297
|
console.log(chalk3.green(`\u2713 .env created (${mode} mode) \u2192 ${absPath}`));
|
|
268
298
|
const gitignoreState = ensureGitignoreBlocksEnv();
|
|
269
299
|
if (gitignoreState === "added") {
|
|
@@ -308,29 +338,39 @@ var CLI_APP_INFO = { name: "vonpay-cli", version: CLI_VERSION };
|
|
|
308
338
|
var SILENT_ERROR_REPORTER = () => {
|
|
309
339
|
};
|
|
310
340
|
|
|
311
|
-
// src/
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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}`;
|
|
317
365
|
}
|
|
318
|
-
function
|
|
319
|
-
|
|
320
|
-
const baseUrl = getBaseUrl();
|
|
321
|
-
return new VonPayCheckout({
|
|
322
|
-
apiKey,
|
|
323
|
-
...baseUrl ? { baseUrl } : {},
|
|
324
|
-
appInfo: CLI_APP_INFO,
|
|
325
|
-
// The CLI prints every error itself (handleError / --json). Without this the SDK's fallback
|
|
326
|
-
// reporter adds its own `[vonpay] …` line to stderr on EVERY API error, live key included.
|
|
327
|
-
errorReporter: SILENT_ERROR_REPORTER
|
|
328
|
-
});
|
|
366
|
+
function allowCustomBaseUrl(env = process.env) {
|
|
367
|
+
return customBaseUrlAllowed(env);
|
|
329
368
|
}
|
|
330
369
|
|
|
331
370
|
// src/utils/errors.ts
|
|
332
371
|
import chalk4 from "chalk";
|
|
333
372
|
import {
|
|
373
|
+
RETRY_WITHHELD_HELP,
|
|
334
374
|
VonPayError
|
|
335
375
|
} from "@vonpay/checkout-node";
|
|
336
376
|
var SECRET_PATTERNS = [
|
|
@@ -345,13 +385,24 @@ function scrubSecrets(s) {
|
|
|
345
385
|
for (const [re, replacement] of SECRET_PATTERNS) out = out.replace(re, replacement);
|
|
346
386
|
return out;
|
|
347
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.`;
|
|
348
391
|
var str = (v) => v === void 0 || v === null ? null : String(v);
|
|
349
392
|
var FIELD_PRINTERS = {
|
|
350
393
|
code: (e) => `Code: ${str(e.code)}`,
|
|
351
394
|
fix: (e) => `Fix: ${str(e.fix)}`,
|
|
352
|
-
nextAction: (e) =>
|
|
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
|
+
),
|
|
353
399
|
retryable: (e) => typeof e.retryable === "boolean" ? `Retryable: ${e.retryable ? "yes" : "no \u2014 do not re-run this command as-is"}` : null,
|
|
354
|
-
retryWithheld: (e) => e.retryWithheld === true ?
|
|
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,
|
|
355
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,
|
|
356
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,
|
|
357
408
|
currentStatus: (e) => e.currentStatus ? `Current status: ${str(e.currentStatus)}` : null,
|
|
@@ -365,6 +416,8 @@ var FIELD_PRINTERS = {
|
|
|
365
416
|
rawReason: (e) => e.rawReason && e.rawReason !== e.reason ? `Reason as sent: ${str(e.rawReason)}` : null,
|
|
366
417
|
retryAfterSeconds: (e) => typeof e.retryAfterSeconds === "number" ? `Retry after: ${e.retryAfterSeconds} seconds \u2014 then ask again` : null,
|
|
367
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.
|
|
368
421
|
llmHint: (e) => e.llmHint ? `Hint: ${str(e.llmHint)}` : null,
|
|
369
422
|
docs: (e) => e.docs ? `Docs: ${str(e.docs)}` : null,
|
|
370
423
|
status: (e) => typeof e.status === "number" ? `HTTP status: ${e.status}` : null,
|
|
@@ -422,6 +475,66 @@ function handleError(err) {
|
|
|
422
475
|
process.exit(1);
|
|
423
476
|
}
|
|
424
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
|
+
|
|
425
538
|
// src/commands/sessions.ts
|
|
426
539
|
var makeClient = makeCheckoutClient;
|
|
427
540
|
function sessionLines(session) {
|
|
@@ -469,12 +582,13 @@ function sessionsCommand() {
|
|
|
469
582
|
const sessions = new Command4("sessions").description(
|
|
470
583
|
"Create and manage checkout sessions"
|
|
471
584
|
);
|
|
472
|
-
sessions.command("create").description("Create a new checkout session").requiredOption("--amount <cents>", "Amount in minor units (e.g. 1499 = $14.99)"
|
|
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");
|
|
473
587
|
const client = makeClient();
|
|
474
588
|
const spinner = startSpinner("Creating session...");
|
|
475
589
|
try {
|
|
476
590
|
const params = {
|
|
477
|
-
amount
|
|
591
|
+
amount,
|
|
478
592
|
currency: opts.currency.toUpperCase(),
|
|
479
593
|
country: opts.country?.toUpperCase(),
|
|
480
594
|
description: opts.description,
|
|
@@ -537,11 +651,30 @@ function sessionsCommand() {
|
|
|
537
651
|
}
|
|
538
652
|
|
|
539
653
|
// src/commands/payment-intents.ts
|
|
540
|
-
import { randomUUID } from "crypto";
|
|
541
654
|
import { Command as Command5 } from "commander";
|
|
542
|
-
import
|
|
655
|
+
import chalk8 from "chalk";
|
|
543
656
|
import { formatMinorUnits as formatMinorUnits2 } from "@vonpay/checkout-node";
|
|
544
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
|
+
|
|
545
678
|
// src/utils/metadata.ts
|
|
546
679
|
var MAX_LEN = 256;
|
|
547
680
|
var CONTROL_CHARS = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/;
|
|
@@ -597,25 +730,75 @@ function safeDisplay(s, max = 200) {
|
|
|
597
730
|
}
|
|
598
731
|
|
|
599
732
|
// src/utils/live-key.ts
|
|
600
|
-
import
|
|
733
|
+
import { createInterface as createInterface2 } from "readline";
|
|
734
|
+
import chalk7 from "chalk";
|
|
735
|
+
import { Option as Option2 } from "commander";
|
|
601
736
|
import { isLiveKey as isLiveKey2 } from "@vonpay/checkout-node";
|
|
602
|
-
|
|
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 = {}) {
|
|
603
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
|
+
}
|
|
604
773
|
if (opts.confirmLive) {
|
|
605
774
|
console.warn(
|
|
606
|
-
|
|
607
|
-
|
|
775
|
+
chalk7.yellow(
|
|
776
|
+
"Note: --confirm-live no longer confirms a live action by itself. A person confirms it at the terminal."
|
|
608
777
|
)
|
|
609
778
|
);
|
|
610
|
-
return;
|
|
611
779
|
}
|
|
612
|
-
const
|
|
613
|
-
|
|
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();
|
|
614
797
|
}
|
|
615
798
|
function warnLiveSecretFlag(key, flag) {
|
|
616
799
|
if (!isLiveKey2(key)) return;
|
|
617
800
|
console.warn(
|
|
618
|
-
|
|
801
|
+
chalk7.yellow(
|
|
619
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.`
|
|
620
803
|
)
|
|
621
804
|
);
|
|
@@ -660,11 +843,6 @@ function buildCreateParams(opts, resolved) {
|
|
|
660
843
|
}
|
|
661
844
|
return params;
|
|
662
845
|
}
|
|
663
|
-
function resolveIdempotencyKey(flag) {
|
|
664
|
-
const given = flag?.trim();
|
|
665
|
-
if (given) return { key: given, generated: false };
|
|
666
|
-
return { key: `cli-${randomUUID()}`, generated: true };
|
|
667
|
-
}
|
|
668
846
|
function buildMITBlock(opts) {
|
|
669
847
|
const initiator = opts.mitInitiator?.trim().toLowerCase();
|
|
670
848
|
const reason = opts.mitReason?.trim().toLowerCase();
|
|
@@ -695,7 +873,7 @@ function buildMITBlock(opts) {
|
|
|
695
873
|
};
|
|
696
874
|
}
|
|
697
875
|
function printPaymentIntent(intent) {
|
|
698
|
-
const statusColor = intent.status === "succeeded" ?
|
|
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;
|
|
699
877
|
console.log(` ID: ${intent.id}`);
|
|
700
878
|
console.log(` Status: ${statusColor(intent.status)}`);
|
|
701
879
|
console.log(
|
|
@@ -704,29 +882,25 @@ function printPaymentIntent(intent) {
|
|
|
704
882
|
console.log(` Capture: ${intent.captureMethod}`);
|
|
705
883
|
if (intent.nextAction) {
|
|
706
884
|
console.log(
|
|
707
|
-
` Next action: ${
|
|
885
|
+
` Next action: ${chalk8.yellow(safeDisplay(intent.nextAction.type))}`
|
|
708
886
|
);
|
|
709
887
|
if (intent.nextAction.type === "redirect_to_url") {
|
|
710
888
|
console.log(
|
|
711
|
-
` Redirect: ${
|
|
889
|
+
` Redirect: ${chalk8.yellow(safeDisplay(intent.nextAction.redirectToUrl.url))}`
|
|
712
890
|
);
|
|
713
891
|
}
|
|
714
892
|
}
|
|
715
893
|
if (intent.declineCode) {
|
|
716
|
-
console.log(` Decline code: ${
|
|
894
|
+
console.log(` Decline code: ${chalk8.red(safeDisplay(intent.declineCode))}`);
|
|
717
895
|
}
|
|
718
896
|
console.log(` Created: ${intent.createdAt}`);
|
|
719
897
|
}
|
|
720
|
-
var
|
|
898
|
+
var AMOUNT_HELP = "Amount in minor units, a whole number (e.g. 1499 = $14.99)";
|
|
721
899
|
function paymentIntentsCommand() {
|
|
722
900
|
const intents = new Command5("payment-intents").description(
|
|
723
|
-
"Create, capture, and void payment intents (discrete lifecycle)"
|
|
901
|
+
"Create, read, capture, and void payment intents (discrete lifecycle)"
|
|
724
902
|
);
|
|
725
|
-
intents.command("create").description("Create a new payment intent").requiredOption(
|
|
726
|
-
"--amount <cents>",
|
|
727
|
-
"Amount in minor units (e.g. 1499 = $14.99)",
|
|
728
|
-
(v) => parseInt(v, 10)
|
|
729
|
-
).requiredOption("--currency <code>", "ISO 4217 currency (e.g. USD)").option(
|
|
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(
|
|
730
904
|
"--capture-method <mode>",
|
|
731
905
|
"automatic (default) or manual (parks at authorized)"
|
|
732
906
|
).option(
|
|
@@ -753,33 +927,43 @@ function paymentIntentsCommand() {
|
|
|
753
927
|
).option("--buyer-email <email>", "Buyer email for this charge").option(
|
|
754
928
|
"--idempotency-key <key>",
|
|
755
929
|
"Idempotency key for this charge. Reuse the SAME key to retry the same charge safely. Generated (and printed) when omitted"
|
|
756
|
-
).
|
|
930
|
+
).addOption(retiredConfirmLiveOption()).option("--json", "Output raw JSON").action(async (raw) => {
|
|
757
931
|
const apiKey = requireApiKey();
|
|
758
|
-
|
|
759
|
-
const
|
|
760
|
-
|
|
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;
|
|
761
939
|
try {
|
|
762
|
-
const captureMethod = opts.captureMethod?.toLowerCase();
|
|
763
|
-
if (captureMethod !== void 0 && captureMethod !== "automatic" && captureMethod !== "manual") {
|
|
764
|
-
spinner.stop();
|
|
765
|
-
const message = `Invalid --capture-method "${opts.captureMethod}". Expected automatic or manual.`;
|
|
766
|
-
exitWithCliError(message, chalk7.red(message));
|
|
767
|
-
}
|
|
768
940
|
const metadata = parseMetadata(opts.metadata);
|
|
769
941
|
const mit = buildMITBlock(opts);
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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 {
|
|
783
967
|
const intent = await client.paymentIntents.create(params, {
|
|
784
968
|
idempotencyKey
|
|
785
969
|
});
|
|
@@ -787,7 +971,25 @@ function paymentIntentsCommand() {
|
|
|
787
971
|
if (opts.json) {
|
|
788
972
|
console.log(JSON.stringify(intent, null, 2));
|
|
789
973
|
} else {
|
|
790
|
-
console.log(
|
|
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 {
|
|
791
993
|
printPaymentIntent(intent);
|
|
792
994
|
}
|
|
793
995
|
} catch (err) {
|
|
@@ -797,26 +999,49 @@ function paymentIntentsCommand() {
|
|
|
797
999
|
});
|
|
798
1000
|
intents.command("capture").description("Capture an authorized payment intent").argument("<id>", "Payment intent ID (vpi_*)").option(
|
|
799
1001
|
"--amount-to-capture <cents>",
|
|
800
|
-
"Partial capture amount in minor units (omit for full)"
|
|
801
|
-
|
|
802
|
-
|
|
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(
|
|
803
1007
|
async (id, opts) => {
|
|
804
1008
|
const apiKey = requireApiKey();
|
|
805
|
-
|
|
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
|
+
);
|
|
806
1031
|
const client = makeCheckoutClient();
|
|
807
1032
|
const spinner = startSpinner("Capturing payment intent...");
|
|
808
1033
|
try {
|
|
809
1034
|
const intent = await client.paymentIntents.capture(
|
|
810
1035
|
id,
|
|
811
|
-
|
|
812
|
-
|
|
1036
|
+
amountToCapture !== void 0 ? { amountToCapture } : void 0,
|
|
1037
|
+
{ idempotencyKey }
|
|
813
1038
|
);
|
|
814
1039
|
spinner.stop();
|
|
815
1040
|
if (opts.json) {
|
|
816
1041
|
console.log(JSON.stringify(intent, null, 2));
|
|
817
1042
|
} else {
|
|
818
|
-
const captureLabel =
|
|
819
|
-
console.log(
|
|
1043
|
+
const captureLabel = amountToCapture !== void 0 ? ` (partial: ${formatMinorUnits2(amountToCapture, intent.currency)})` : "";
|
|
1044
|
+
console.log(chalk8.green(`\u2713 Payment intent captured${captureLabel}`));
|
|
820
1045
|
printPaymentIntent(intent);
|
|
821
1046
|
}
|
|
822
1047
|
} catch (err) {
|
|
@@ -825,22 +1050,36 @@ function paymentIntentsCommand() {
|
|
|
825
1050
|
}
|
|
826
1051
|
}
|
|
827
1052
|
);
|
|
828
|
-
intents.command("void").description("Void an authorized (uncaptured) payment intent").argument("<id>", "Payment intent ID (vpi_*)").option(
|
|
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(
|
|
829
1057
|
async (id, opts) => {
|
|
830
1058
|
const apiKey = requireApiKey();
|
|
831
|
-
|
|
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
|
+
);
|
|
832
1074
|
const client = makeCheckoutClient();
|
|
833
1075
|
const spinner = startSpinner("Voiding payment intent...");
|
|
834
1076
|
try {
|
|
835
|
-
const intent = await client.paymentIntents.void(
|
|
836
|
-
id,
|
|
837
|
-
opts.idempotencyKey ? { idempotencyKey: opts.idempotencyKey } : void 0
|
|
838
|
-
);
|
|
1077
|
+
const intent = await client.paymentIntents.void(id, { idempotencyKey });
|
|
839
1078
|
spinner.stop();
|
|
840
1079
|
if (opts.json) {
|
|
841
1080
|
console.log(JSON.stringify(intent, null, 2));
|
|
842
1081
|
} else {
|
|
843
|
-
console.log(
|
|
1082
|
+
console.log(chalk8.green("\u2713 Payment intent voided"));
|
|
844
1083
|
printPaymentIntent(intent);
|
|
845
1084
|
}
|
|
846
1085
|
} catch (err) {
|
|
@@ -854,7 +1093,7 @@ function paymentIntentsCommand() {
|
|
|
854
1093
|
|
|
855
1094
|
// src/commands/refunds.ts
|
|
856
1095
|
import { Command as Command6 } from "commander";
|
|
857
|
-
import
|
|
1096
|
+
import chalk9 from "chalk";
|
|
858
1097
|
import { formatMinorUnits as formatMinorUnits3 } from "@vonpay/checkout-node";
|
|
859
1098
|
function present(v) {
|
|
860
1099
|
return v !== void 0 && v.trim() !== "";
|
|
@@ -884,7 +1123,7 @@ function buildRefundParams(opts, metadata) {
|
|
|
884
1123
|
return params;
|
|
885
1124
|
}
|
|
886
1125
|
function printRefund(refund) {
|
|
887
|
-
const statusColor = refund.status === "succeeded" ?
|
|
1126
|
+
const statusColor = refund.status === "succeeded" ? chalk9.green : refund.status === "failed" ? chalk9.red : chalk9.yellow;
|
|
888
1127
|
console.log(` ID: ${refund.id}`);
|
|
889
1128
|
if (refund.transaction !== null) {
|
|
890
1129
|
console.log(` Transaction: ${refund.transaction}`);
|
|
@@ -913,37 +1152,60 @@ function refundsCommand() {
|
|
|
913
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"
|
|
914
1153
|
).option(
|
|
915
1154
|
"--amount <cents>",
|
|
916
|
-
"Refund amount in minor units (omit for full remaining balance)"
|
|
917
|
-
(v) => parseInt(v, 10)
|
|
1155
|
+
"Refund amount in minor units, a whole number (omit for full remaining balance)"
|
|
918
1156
|
).option("--currency <code>", "ISO 4217 currency code").option("--reason <text>", "Free-form reason (mirrored back on the record)").option(
|
|
919
1157
|
"--metadata <pair...>",
|
|
920
1158
|
"Metadata key=value (repeat flag for multiple pairs)"
|
|
921
|
-
).option(
|
|
922
|
-
"--
|
|
923
|
-
"
|
|
924
|
-
).option("--json", "Output raw JSON").action(async (
|
|
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) => {
|
|
925
1163
|
try {
|
|
926
|
-
resolveRefundTarget(
|
|
1164
|
+
resolveRefundTarget(raw);
|
|
927
1165
|
} catch (err) {
|
|
928
1166
|
const message = err.message;
|
|
929
|
-
exitWithCliError(message,
|
|
1167
|
+
exitWithCliError(message, chalk9.red(`Error: ${message}`));
|
|
930
1168
|
}
|
|
931
1169
|
const apiKey = requireApiKey();
|
|
932
|
-
|
|
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
|
+
);
|
|
933
1200
|
const client = makeCheckoutClient();
|
|
934
1201
|
const spinner = startSpinner("Creating refund...");
|
|
935
1202
|
try {
|
|
936
|
-
const
|
|
937
|
-
const params = buildRefundParams(opts, metadata);
|
|
938
|
-
const refund = await client.refunds.create(
|
|
939
|
-
params,
|
|
940
|
-
opts.idempotencyKey ? { idempotencyKey: opts.idempotencyKey } : void 0
|
|
941
|
-
);
|
|
1203
|
+
const refund = await client.refunds.create(params, { idempotencyKey });
|
|
942
1204
|
spinner.stop();
|
|
943
1205
|
if (opts.json) {
|
|
944
1206
|
console.log(JSON.stringify(refund, null, 2));
|
|
945
1207
|
} else {
|
|
946
|
-
console.log(
|
|
1208
|
+
console.log(chalk9.green("\u2713 Refund created"));
|
|
947
1209
|
printRefund(refund);
|
|
948
1210
|
}
|
|
949
1211
|
} catch (err) {
|
|
@@ -951,14 +1213,77 @@ function refundsCommand() {
|
|
|
951
1213
|
handleError(err);
|
|
952
1214
|
}
|
|
953
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
|
+
);
|
|
954
1255
|
return refunds;
|
|
955
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
|
+
}
|
|
956
1281
|
|
|
957
1282
|
// src/commands/tokens.ts
|
|
958
1283
|
import { Command as Command7 } from "commander";
|
|
959
|
-
import
|
|
1284
|
+
import chalk10 from "chalk";
|
|
960
1285
|
function printToken(token) {
|
|
961
|
-
const statusColor = token.status === "active" ?
|
|
1286
|
+
const statusColor = token.status === "active" ? chalk10.green : token.status === "expired" ? chalk10.yellow : chalk10.red;
|
|
962
1287
|
console.log(` ID: ${token.id}`);
|
|
963
1288
|
console.log(` Status: ${statusColor(token.status)}`);
|
|
964
1289
|
const card = formatTokenCard(token.card);
|
|
@@ -980,7 +1305,7 @@ function tokensCommand() {
|
|
|
980
1305
|
const apiKey = requireApiKey();
|
|
981
1306
|
if (!opts.buyerId && !opts.providerReference) {
|
|
982
1307
|
console.warn(
|
|
983
|
-
|
|
1308
|
+
chalk10.yellow(
|
|
984
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."
|
|
985
1310
|
)
|
|
986
1311
|
);
|
|
@@ -1006,7 +1331,7 @@ function tokensCommand() {
|
|
|
1006
1331
|
if (opts.json) {
|
|
1007
1332
|
console.log(JSON.stringify(token, null, 2));
|
|
1008
1333
|
} else {
|
|
1009
|
-
console.log(
|
|
1334
|
+
console.log(chalk10.green("\u2713 Token created"));
|
|
1010
1335
|
printToken(token);
|
|
1011
1336
|
}
|
|
1012
1337
|
} catch (err) {
|
|
@@ -1025,33 +1350,33 @@ function formatTokenCard(card) {
|
|
|
1025
1350
|
|
|
1026
1351
|
// src/commands/capabilities.ts
|
|
1027
1352
|
import { Command as Command8 } from "commander";
|
|
1028
|
-
import
|
|
1353
|
+
import chalk11 from "chalk";
|
|
1029
1354
|
function fmtBool(v) {
|
|
1030
|
-
return v ?
|
|
1355
|
+
return v ? chalk11.green("yes") : chalk11.dim("no");
|
|
1031
1356
|
}
|
|
1032
1357
|
function fmtVoidPolicy(v) {
|
|
1033
1358
|
switch (v) {
|
|
1034
1359
|
case "supported":
|
|
1035
|
-
return
|
|
1360
|
+
return chalk11.green(v);
|
|
1036
1361
|
case "rerouted_to_refund":
|
|
1037
|
-
return
|
|
1362
|
+
return chalk11.yellow(v);
|
|
1038
1363
|
case "not_supported":
|
|
1039
|
-
return
|
|
1364
|
+
return chalk11.dim(v);
|
|
1040
1365
|
default:
|
|
1041
|
-
return
|
|
1366
|
+
return chalk11.dim(String(v));
|
|
1042
1367
|
}
|
|
1043
1368
|
}
|
|
1044
1369
|
function fmtDisputeReporting(v) {
|
|
1045
|
-
return v === "tracked" ?
|
|
1370
|
+
return v === "tracked" ? chalk11.green(v) : chalk11.yellow(v);
|
|
1046
1371
|
}
|
|
1047
1372
|
function fmtSubscriptionBilling(v) {
|
|
1048
|
-
if (v === true) return
|
|
1049
|
-
if (v === false) return
|
|
1050
|
-
return
|
|
1373
|
+
if (v === true) return chalk11.yellow("yes");
|
|
1374
|
+
if (v === false) return chalk11.green("no");
|
|
1375
|
+
return chalk11.yellow("unknown");
|
|
1051
1376
|
}
|
|
1052
1377
|
function printCapabilities(caps) {
|
|
1053
1378
|
const ops = caps.supportedOperations;
|
|
1054
|
-
console.log(
|
|
1379
|
+
console.log(chalk11.bold("Supported operations"));
|
|
1055
1380
|
console.log(` auth/capture separation: ${fmtBool(ops.authCaptureSeparation)}`);
|
|
1056
1381
|
console.log(` partial capture: ${fmtBool(ops.partialCapture)}`);
|
|
1057
1382
|
console.log(` partial refund: ${fmtBool(ops.partialRefund)}`);
|
|
@@ -1066,17 +1391,17 @@ function printCapabilities(caps) {
|
|
|
1066
1391
|
console.log();
|
|
1067
1392
|
if (ops.disputeReporting !== "tracked") {
|
|
1068
1393
|
console.log(
|
|
1069
|
-
|
|
1394
|
+
chalk11.yellow(
|
|
1070
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."
|
|
1071
1396
|
)
|
|
1072
1397
|
);
|
|
1073
1398
|
console.log();
|
|
1074
1399
|
}
|
|
1075
|
-
console.log(
|
|
1400
|
+
console.log(chalk11.bold("Subscription billing"));
|
|
1076
1401
|
console.log(` Von Pay renews for you: ${fmtSubscriptionBilling(caps.subscriptionBilling?.enabled)}`);
|
|
1077
1402
|
if (caps.subscriptionBilling?.enabled !== false) {
|
|
1078
1403
|
console.log(
|
|
1079
|
-
|
|
1404
|
+
chalk11.yellow(
|
|
1080
1405
|
` \u26A0 Do not run renewals from your own system while this is not "no":
|
|
1081
1406
|
Von Pay may be charging these customers' renewals itself, and both
|
|
1082
1407
|
would charge them. Check again at the moment you renew.`
|
|
@@ -1084,12 +1409,12 @@ function printCapabilities(caps) {
|
|
|
1084
1409
|
);
|
|
1085
1410
|
}
|
|
1086
1411
|
console.log();
|
|
1087
|
-
console.log(
|
|
1412
|
+
console.log(chalk11.bold("Settlement currencies"));
|
|
1088
1413
|
console.log(
|
|
1089
|
-
` ${caps.settlementCurrencies.length ? caps.settlementCurrencies.join(", ") :
|
|
1414
|
+
` ${caps.settlementCurrencies.length ? caps.settlementCurrencies.join(", ") : chalk11.dim("(none reported)")}`
|
|
1090
1415
|
);
|
|
1091
1416
|
console.log();
|
|
1092
|
-
console.log(
|
|
1417
|
+
console.log(chalk11.bold("Rate limits"));
|
|
1093
1418
|
console.log(
|
|
1094
1419
|
` payment intents / minute: ${caps.rateLimits.paymentIntentsPerMinute}`
|
|
1095
1420
|
);
|
|
@@ -1115,8 +1440,8 @@ function capabilitiesCommand() {
|
|
|
1115
1440
|
|
|
1116
1441
|
// src/commands/trigger.ts
|
|
1117
1442
|
import { Command as Command9 } from "commander";
|
|
1118
|
-
import { createHmac, randomUUID as randomUUID2 } from "crypto";
|
|
1119
|
-
import
|
|
1443
|
+
import { createHmac as createHmac2, randomUUID as randomUUID2 } from "crypto";
|
|
1444
|
+
import chalk12 from "chalk";
|
|
1120
1445
|
import ora2 from "ora";
|
|
1121
1446
|
|
|
1122
1447
|
// src/utils/forward-to.ts
|
|
@@ -1158,6 +1483,12 @@ function validateForwardTo(raw, opts) {
|
|
|
1158
1483
|
reason: `${flag} scheme must be http or https; got ${url.protocol}`
|
|
1159
1484
|
};
|
|
1160
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
|
+
}
|
|
1161
1492
|
const host = normalizeHost(url.hostname);
|
|
1162
1493
|
const isLoopback = LOOPBACK_HOSTS.has(host);
|
|
1163
1494
|
if (!isLoopback && !opts.allowTunnel) {
|
|
@@ -1179,6 +1510,93 @@ function truncate2(s) {
|
|
|
1179
1510
|
return s.length <= 120 ? s : `${s.slice(0, 117)}...`;
|
|
1180
1511
|
}
|
|
1181
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
|
+
|
|
1182
1600
|
// src/commands/trigger.ts
|
|
1183
1601
|
var SUPPORTED_EVENTS = [
|
|
1184
1602
|
"charge.succeeded",
|
|
@@ -1200,7 +1618,7 @@ function warnIfUnsubscribable(event, write = (m) => console.warn(m)) {
|
|
|
1200
1618
|
if (!UNSUBSCRIBABLE_EVENTS.includes(event)) return false;
|
|
1201
1619
|
const suggestion = event === "session.succeeded" ? "charge.succeeded" : "charge.failed";
|
|
1202
1620
|
write(
|
|
1203
|
-
|
|
1621
|
+
chalk12.yellow(
|
|
1204
1622
|
`\u26A0 ${event} is emitted internally but is NOT subscribable.
|
|
1205
1623
|
The subscription API accepts the key, stores nothing and returns success, so a
|
|
1206
1624
|
real endpoint receives this event NEVER \u2014 a handler tested only against it is
|
|
@@ -1276,8 +1694,10 @@ function buildEventData(event, ids, amount, currency) {
|
|
|
1276
1694
|
refund_amount: amount,
|
|
1277
1695
|
amount_refunded_total: 0,
|
|
1278
1696
|
reason: "customer_request",
|
|
1279
|
-
//
|
|
1280
|
-
// `
|
|
1697
|
+
// An OPEN refund vocabulary (the SDK's RefundFailedData.reason_code):
|
|
1698
|
+
// `refund_declined` (the provider refused, retrying unchanged fails the
|
|
1699
|
+
// same way) and `refund_unresolved` (no provider response, true state
|
|
1700
|
+
// unknown) are the values sent so far. This sample uses the first.
|
|
1281
1701
|
reason_code: "refund_declined",
|
|
1282
1702
|
is_partial: false,
|
|
1283
1703
|
original_charge_amount: amount,
|
|
@@ -1305,16 +1725,13 @@ function buildPayload(event, opts) {
|
|
|
1305
1725
|
};
|
|
1306
1726
|
}
|
|
1307
1727
|
function triggerCommand() {
|
|
1308
|
-
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
|
|
1728
|
+
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(
|
|
1309
1729
|
"--url-tunnel",
|
|
1310
1730
|
"Allow a non-loopback --url (ngrok / Cloudflare Tunnel). Without it, --url must be this machine"
|
|
1311
|
-
).
|
|
1312
|
-
"--confirm-live",
|
|
1313
|
-
"Required when the loaded key is vp_sk_live_*: the event is signed with that LIVE key and sent to --url"
|
|
1314
|
-
).action(async (event, opts) => {
|
|
1731
|
+
).addOption(retiredConfirmLiveOption()).action(async (event, opts) => {
|
|
1315
1732
|
if (!SUPPORTED_EVENTS.includes(event)) {
|
|
1316
1733
|
console.error(
|
|
1317
|
-
|
|
1734
|
+
chalk12.red(
|
|
1318
1735
|
`Unknown event: ${event}
|
|
1319
1736
|
Supported: ${SUPPORTED_EVENTS.join(", ")}`
|
|
1320
1737
|
)
|
|
@@ -1328,21 +1745,31 @@ Supported: ${SUPPORTED_EVENTS.join(", ")}`
|
|
|
1328
1745
|
tunnelFlag: "--url-tunnel"
|
|
1329
1746
|
});
|
|
1330
1747
|
if (!destination.ok) {
|
|
1331
|
-
console.error(
|
|
1748
|
+
console.error(chalk12.red(destination.reason));
|
|
1332
1749
|
process.exit(1);
|
|
1333
1750
|
}
|
|
1334
|
-
if (destination.warning) console.warn(
|
|
1751
|
+
if (destination.warning) console.warn(chalk12.yellow(destination.warning));
|
|
1752
|
+
const amount = optionalMinorUnitsOrExit(
|
|
1753
|
+
opts.amount === void 0 ? void 0 : String(opts.amount),
|
|
1754
|
+
"--amount"
|
|
1755
|
+
);
|
|
1335
1756
|
const apiKey = requireApiKey();
|
|
1336
|
-
|
|
1757
|
+
await confirmLiveAction(
|
|
1337
1758
|
apiKey,
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1759
|
+
{
|
|
1760
|
+
action: "trigger a test webhook event",
|
|
1761
|
+
details: [
|
|
1762
|
+
["Event", event],
|
|
1763
|
+
["Sent to", String(opts.url)]
|
|
1764
|
+
],
|
|
1765
|
+
consequence: "The event is signed with your LIVE key: a receiver that verifies with that key will accept it as a genuine event."
|
|
1766
|
+
},
|
|
1767
|
+
opts
|
|
1341
1768
|
);
|
|
1342
|
-
const payload = buildPayload(event, opts);
|
|
1769
|
+
const payload = buildPayload(event, { ...opts, amount });
|
|
1343
1770
|
const body = JSON.stringify(payload);
|
|
1344
1771
|
const t = Math.floor(Date.now() / 1e3);
|
|
1345
|
-
const v1 =
|
|
1772
|
+
const v1 = createHmac2("sha256", apiKey).update(`${t}.${body}`).digest("hex");
|
|
1346
1773
|
const signatureHeader = `t=${t},v1=${v1}`;
|
|
1347
1774
|
const spinner = ora2(`Sending ${event} to ${opts.url}...`).start();
|
|
1348
1775
|
try {
|
|
@@ -1354,42 +1781,53 @@ Supported: ${SUPPORTED_EVENTS.join(", ")}`
|
|
|
1354
1781
|
"User-Agent": "VonPay-Webhook/1.0 (CLI trigger)"
|
|
1355
1782
|
},
|
|
1356
1783
|
body,
|
|
1357
|
-
signal: AbortSignal.timeout(1e4)
|
|
1784
|
+
signal: AbortSignal.timeout(1e4),
|
|
1785
|
+
// Never follow a redirect: a 307/308 would re-send this signed body
|
|
1786
|
+
// to wherever it points, including a host --url never allowed.
|
|
1787
|
+
redirect: "manual"
|
|
1358
1788
|
});
|
|
1359
1789
|
spinner.stop();
|
|
1790
|
+
if (isRedirect(response)) {
|
|
1791
|
+
console.error(
|
|
1792
|
+
chalk12.red(
|
|
1793
|
+
`\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.`
|
|
1794
|
+
)
|
|
1795
|
+
);
|
|
1796
|
+
process.exit(1);
|
|
1797
|
+
}
|
|
1360
1798
|
if (response.ok) {
|
|
1361
|
-
console.log(
|
|
1799
|
+
console.log(chalk12.green(`\u2713 ${event} delivered \u2192 ${response.status}`));
|
|
1362
1800
|
} else {
|
|
1363
1801
|
console.log(
|
|
1364
|
-
|
|
1802
|
+
chalk12.yellow(
|
|
1365
1803
|
`\u26A0 ${event} delivered but got ${response.status} response`
|
|
1366
1804
|
)
|
|
1367
1805
|
);
|
|
1368
1806
|
}
|
|
1369
|
-
console.log(
|
|
1807
|
+
console.log(chalk12.dim(` Event: ${payload.id} (${payload.type})`));
|
|
1370
1808
|
console.log(
|
|
1371
|
-
|
|
1809
|
+
chalk12.dim(
|
|
1372
1810
|
` Amount: ${payload.data.amount} ${payload.data.currency}`
|
|
1373
1811
|
)
|
|
1374
1812
|
);
|
|
1375
1813
|
console.log(
|
|
1376
|
-
|
|
1814
|
+
chalk12.dim(
|
|
1377
1815
|
`
|
|
1378
1816
|
Verify with: vonpay.webhooks.constructEvent(rawBody, signatureHeader, secret)`
|
|
1379
1817
|
)
|
|
1380
1818
|
);
|
|
1381
1819
|
console.log(
|
|
1382
|
-
|
|
1820
|
+
chalk12.dim(
|
|
1383
1821
|
` (local dev: 'secret' is your API key here \u2014 production webhooks are signed with your endpoint's whsec_\u2026 secret, NOT your API key)`
|
|
1384
1822
|
)
|
|
1385
1823
|
);
|
|
1386
1824
|
} catch (err) {
|
|
1387
1825
|
spinner.stop();
|
|
1388
1826
|
if (err instanceof Error && err.name === "TimeoutError") {
|
|
1389
|
-
console.error(
|
|
1827
|
+
console.error(chalk12.red(`Timeout: ${opts.url} did not respond within 10s`));
|
|
1390
1828
|
} else {
|
|
1391
1829
|
console.error(
|
|
1392
|
-
|
|
1830
|
+
chalk12.red(
|
|
1393
1831
|
`Failed to deliver: ${err instanceof Error ? err.message : err}`
|
|
1394
1832
|
)
|
|
1395
1833
|
);
|
|
@@ -1401,7 +1839,7 @@ Supported: ${SUPPORTED_EVENTS.join(", ")}`
|
|
|
1401
1839
|
|
|
1402
1840
|
// src/commands/listen.ts
|
|
1403
1841
|
import { Command as Command10 } from "commander";
|
|
1404
|
-
import { VonPayCheckout as VonPayCheckout2
|
|
1842
|
+
import { VonPayCheckout as VonPayCheckout2 } from "@vonpay/checkout-node";
|
|
1405
1843
|
|
|
1406
1844
|
// ../sse-parser/dist/index.js
|
|
1407
1845
|
var SseParser = class {
|
|
@@ -1492,6 +1930,9 @@ var SseParser = class {
|
|
|
1492
1930
|
// src/utils/sse-consumer.ts
|
|
1493
1931
|
var MAX_PENDING_CHARS = 1048576;
|
|
1494
1932
|
var MAX_EVENT_ID_LENGTH = 256;
|
|
1933
|
+
var SERVER_RECONNECT_FLOOR_MS = 250;
|
|
1934
|
+
var SERVER_RECONNECT_WINDOW_MS = 5e3;
|
|
1935
|
+
var SERVER_RECONNECT_MAX_IN_WINDOW = 3;
|
|
1495
1936
|
function resumeId(lastId) {
|
|
1496
1937
|
return /^[\x20-\x7e]+$/.test(lastId) ? lastId : void 0;
|
|
1497
1938
|
}
|
|
@@ -1505,6 +1946,8 @@ async function* consumeSse(opts) {
|
|
|
1505
1946
|
);
|
|
1506
1947
|
let backoffMs = initialBackoffMs;
|
|
1507
1948
|
const onLifecycle = opts.onLifecycle ?? noop;
|
|
1949
|
+
let serverReconnectTimes = [];
|
|
1950
|
+
let holdBackoff = false;
|
|
1508
1951
|
while (!opts.signal.aborted) {
|
|
1509
1952
|
const headers = {
|
|
1510
1953
|
Accept: "text/event-stream",
|
|
@@ -1558,7 +2001,8 @@ async function* consumeSse(opts) {
|
|
|
1558
2001
|
continue;
|
|
1559
2002
|
}
|
|
1560
2003
|
onLifecycle({ kind: "connected" });
|
|
1561
|
-
|
|
2004
|
+
const connectedAt = Date.now();
|
|
2005
|
+
if (!holdBackoff) backoffMs = initialBackoffMs;
|
|
1562
2006
|
parser.reset();
|
|
1563
2007
|
const reader = response.body.getReader();
|
|
1564
2008
|
const decoder = new TextDecoder("utf-8");
|
|
@@ -1575,11 +2019,13 @@ async function* consumeSse(opts) {
|
|
|
1575
2019
|
}
|
|
1576
2020
|
for (const ev of pending.splice(0)) {
|
|
1577
2021
|
if (ev.type === "timeout") {
|
|
1578
|
-
yield ev;
|
|
1579
2022
|
serverInitiatedReconnect = true;
|
|
1580
2023
|
} else {
|
|
1581
|
-
|
|
2024
|
+
serverReconnectTimes = [];
|
|
2025
|
+
holdBackoff = false;
|
|
2026
|
+
backoffMs = initialBackoffMs;
|
|
1582
2027
|
}
|
|
2028
|
+
yield ev;
|
|
1583
2029
|
}
|
|
1584
2030
|
if (feedError !== void 0) throw feedError;
|
|
1585
2031
|
}
|
|
@@ -1595,6 +2041,27 @@ async function* consumeSse(opts) {
|
|
|
1595
2041
|
}
|
|
1596
2042
|
if (opts.signal.aborted) return;
|
|
1597
2043
|
if (serverInitiatedReconnect) {
|
|
2044
|
+
const now = Date.now();
|
|
2045
|
+
if (now - connectedAt >= SERVER_RECONNECT_WINDOW_MS) {
|
|
2046
|
+
serverReconnectTimes = [];
|
|
2047
|
+
holdBackoff = false;
|
|
2048
|
+
backoffMs = initialBackoffMs;
|
|
2049
|
+
} else {
|
|
2050
|
+
serverReconnectTimes = serverReconnectTimes.filter((t) => now - t < SERVER_RECONNECT_WINDOW_MS);
|
|
2051
|
+
serverReconnectTimes.push(now);
|
|
2052
|
+
}
|
|
2053
|
+
if (holdBackoff || serverReconnectTimes.length > SERVER_RECONNECT_MAX_IN_WINDOW) {
|
|
2054
|
+
holdBackoff = true;
|
|
2055
|
+
onLifecycle({
|
|
2056
|
+
kind: "disconnected",
|
|
2057
|
+
reason: "the server keeps asking to reconnect; backing off",
|
|
2058
|
+
willRetryInMs: backoffMs
|
|
2059
|
+
});
|
|
2060
|
+
await sleep(backoffMs, opts.signal);
|
|
2061
|
+
backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
|
|
2062
|
+
continue;
|
|
2063
|
+
}
|
|
2064
|
+
await sleep(SERVER_RECONNECT_FLOOR_MS, opts.signal);
|
|
1598
2065
|
continue;
|
|
1599
2066
|
}
|
|
1600
2067
|
onLifecycle({
|
|
@@ -1645,24 +2112,24 @@ function noop() {
|
|
|
1645
2112
|
}
|
|
1646
2113
|
|
|
1647
2114
|
// src/utils/listen-output.ts
|
|
1648
|
-
import
|
|
2115
|
+
import chalk13 from "chalk";
|
|
1649
2116
|
function formatAttemptHuman(payload, forward) {
|
|
1650
2117
|
const ts = formatTimestamp(payload.timestamp);
|
|
1651
|
-
const eventType =
|
|
1652
|
-
const eventId =
|
|
1653
|
-
const attempt =
|
|
1654
|
-
const arrow =
|
|
2118
|
+
const eventType = chalk13.cyan(payload.event_type.padEnd(24));
|
|
2119
|
+
const eventId = chalk13.gray(payload.event_id.padEnd(28));
|
|
2120
|
+
const attempt = chalk13.gray(`attempt ${payload.attempt}`);
|
|
2121
|
+
const arrow = chalk13.gray("\u2192");
|
|
1655
2122
|
const upstream = formatUpstream(payload);
|
|
1656
|
-
const latency =
|
|
1657
|
-
const liveBadge = payload.livemode ?
|
|
2123
|
+
const latency = chalk13.gray(`${payload.latency_ms}ms`);
|
|
2124
|
+
const liveBadge = payload.livemode ? chalk13.red(" [LIVE]") : "";
|
|
1658
2125
|
let line = `[${ts}] ${eventType} ${eventId} ${attempt} ${arrow} ${upstream} ${latency}${liveBadge}`;
|
|
1659
2126
|
if (forward && forward.attempted) {
|
|
1660
|
-
const fwdArrow =
|
|
2127
|
+
const fwdArrow = chalk13.gray("\u2192 local");
|
|
1661
2128
|
if (forward.error) {
|
|
1662
|
-
line += ` ${fwdArrow} ${
|
|
2129
|
+
line += ` ${fwdArrow} ${chalk13.red(forward.error)}`;
|
|
1663
2130
|
} else if (forward.status !== null) {
|
|
1664
2131
|
const fwdStatus = formatStatus(forward.status);
|
|
1665
|
-
const fwdLatency =
|
|
2132
|
+
const fwdLatency = chalk13.gray(`${forward.latencyMs}ms`);
|
|
1666
2133
|
line += ` ${fwdArrow} ${fwdStatus} ${fwdLatency}`;
|
|
1667
2134
|
}
|
|
1668
2135
|
}
|
|
@@ -1691,21 +2158,21 @@ function formatAttemptJson(payload, forward) {
|
|
|
1691
2158
|
return JSON.stringify(body);
|
|
1692
2159
|
}
|
|
1693
2160
|
function formatLifecycleHuman(message, kind) {
|
|
1694
|
-
const tag = kind === "info" ?
|
|
2161
|
+
const tag = kind === "info" ? chalk13.cyan("[vonpay]") : kind === "warn" ? chalk13.yellow("[vonpay]") : chalk13.red("[vonpay]");
|
|
1695
2162
|
return `${tag} ${message}`;
|
|
1696
2163
|
}
|
|
1697
2164
|
function formatLifecycleJson(kind, extra = {}) {
|
|
1698
2165
|
return JSON.stringify({ type: `cli.${kind}`, ...extra, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1699
2166
|
}
|
|
1700
2167
|
function formatStatus(status) {
|
|
1701
|
-
if (status >= 200 && status < 300) return
|
|
1702
|
-
if (status >= 400 && status < 500) return
|
|
1703
|
-
if (status >= 500) return
|
|
1704
|
-
return
|
|
2168
|
+
if (status >= 200 && status < 300) return chalk13.green(String(status));
|
|
2169
|
+
if (status >= 400 && status < 500) return chalk13.yellow(String(status));
|
|
2170
|
+
if (status >= 500) return chalk13.red(String(status));
|
|
2171
|
+
return chalk13.gray(String(status));
|
|
1705
2172
|
}
|
|
1706
2173
|
function formatUpstream(payload) {
|
|
1707
2174
|
if (payload.response_status === null) {
|
|
1708
|
-
return
|
|
2175
|
+
return chalk13.red("timeout");
|
|
1709
2176
|
}
|
|
1710
2177
|
return formatStatus(payload.response_status);
|
|
1711
2178
|
}
|
|
@@ -1718,78 +2185,6 @@ function formatTimestamp(iso) {
|
|
|
1718
2185
|
return `${h}:${m}:${s}`;
|
|
1719
2186
|
}
|
|
1720
2187
|
|
|
1721
|
-
// src/utils/listen-forward.ts
|
|
1722
|
-
import { createHmac as createHmac2 } from "crypto";
|
|
1723
|
-
async function forwardPayload(payload, opts) {
|
|
1724
|
-
const body = JSON.stringify(payload);
|
|
1725
|
-
const t = Math.floor(Date.now() / 1e3);
|
|
1726
|
-
const v1 = createHmac2("sha256", opts.signingSecret).update(`${t}.${body}`).digest("hex");
|
|
1727
|
-
const signatureHeader = `t=${t},v1=${v1}`;
|
|
1728
|
-
const timeoutMs = opts.timeoutMs ?? 5e3;
|
|
1729
|
-
const localController = new AbortController();
|
|
1730
|
-
const timeoutId = setTimeout(() => localController.abort(), timeoutMs);
|
|
1731
|
-
const combinedSignal = anySignal([opts.signal, localController.signal]);
|
|
1732
|
-
const start = Date.now();
|
|
1733
|
-
try {
|
|
1734
|
-
const res = await fetch(opts.url.toString(), {
|
|
1735
|
-
method: "POST",
|
|
1736
|
-
headers: {
|
|
1737
|
-
"Content-Type": "application/json",
|
|
1738
|
-
"x-vonpay-signature": signatureHeader,
|
|
1739
|
-
"User-Agent": "vonpay-cli-listen"
|
|
1740
|
-
},
|
|
1741
|
-
body,
|
|
1742
|
-
signal: combinedSignal
|
|
1743
|
-
});
|
|
1744
|
-
try {
|
|
1745
|
-
await res.arrayBuffer();
|
|
1746
|
-
} catch {
|
|
1747
|
-
}
|
|
1748
|
-
return {
|
|
1749
|
-
attempted: true,
|
|
1750
|
-
status: res.status,
|
|
1751
|
-
latencyMs: Date.now() - start,
|
|
1752
|
-
error: null
|
|
1753
|
-
};
|
|
1754
|
-
} catch (err) {
|
|
1755
|
-
if (opts.signal.aborted) {
|
|
1756
|
-
return { attempted: true, status: null, latencyMs: null, error: "aborted" };
|
|
1757
|
-
}
|
|
1758
|
-
if (localController.signal.aborted) {
|
|
1759
|
-
return { attempted: true, status: null, latencyMs: null, error: `timeout after ${timeoutMs}ms` };
|
|
1760
|
-
}
|
|
1761
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1762
|
-
return { attempted: true, status: null, latencyMs: null, error: message };
|
|
1763
|
-
} finally {
|
|
1764
|
-
clearTimeout(timeoutId);
|
|
1765
|
-
}
|
|
1766
|
-
}
|
|
1767
|
-
function anySignal(signals) {
|
|
1768
|
-
if (typeof AbortSignal.any === "function") {
|
|
1769
|
-
return AbortSignal.any(signals);
|
|
1770
|
-
}
|
|
1771
|
-
const controller = new AbortController();
|
|
1772
|
-
const tracked = [];
|
|
1773
|
-
const onAbort = () => {
|
|
1774
|
-
for (const sig of tracked) {
|
|
1775
|
-
sig.removeEventListener("abort", onAbort);
|
|
1776
|
-
}
|
|
1777
|
-
controller.abort();
|
|
1778
|
-
};
|
|
1779
|
-
for (const signal of signals) {
|
|
1780
|
-
if (signal.aborted) {
|
|
1781
|
-
controller.abort();
|
|
1782
|
-
for (const sig of tracked) {
|
|
1783
|
-
sig.removeEventListener("abort", onAbort);
|
|
1784
|
-
}
|
|
1785
|
-
return controller.signal;
|
|
1786
|
-
}
|
|
1787
|
-
signal.addEventListener("abort", onAbort);
|
|
1788
|
-
tracked.push(signal);
|
|
1789
|
-
}
|
|
1790
|
-
return controller.signal;
|
|
1791
|
-
}
|
|
1792
|
-
|
|
1793
2188
|
// src/commands/listen.ts
|
|
1794
2189
|
function listenCommand() {
|
|
1795
2190
|
return new Command10("listen").description(
|
|
@@ -1804,22 +2199,15 @@ function listenCommand() {
|
|
|
1804
2199
|
"--events <type>",
|
|
1805
2200
|
"Only stream attempts for the given event type. Repeat the flag for multiple types.",
|
|
1806
2201
|
(value, prev) => prev ? [...prev, value] : [value]
|
|
1807
|
-
).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").
|
|
1808
|
-
"--confirm-live",
|
|
1809
|
-
"Acknowledge that the listen stream may surface production webhook attempts (required for vp_sk_live_* keys)"
|
|
1810
|
-
).action(async (opts) => {
|
|
2202
|
+
).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) => {
|
|
1811
2203
|
const apiKey = opts.apiKey ?? requireApiKey();
|
|
2204
|
+
if (opts.apiKey !== void 0) warnLiveSecretFlag(opts.apiKey, "--api-key");
|
|
1812
2205
|
const refuse = (message) => {
|
|
1813
2206
|
if (jsonErrorsEnabled()) exitWithCliError(message, message);
|
|
1814
2207
|
process.stderr.write(formatLifecycleHuman(message, "error") + "\n");
|
|
1815
2208
|
return process.exit(1);
|
|
1816
2209
|
};
|
|
1817
|
-
if (
|
|
1818
|
-
refuse(
|
|
1819
|
-
"Refusing to start listen with a live key (vp_sk_live_*). Re-run with --confirm-live to acknowledge that production webhook attempts will stream to this terminal."
|
|
1820
|
-
);
|
|
1821
|
-
}
|
|
1822
|
-
if (!apiKey.startsWith("vp_sk_test_") && !isLiveKey3(apiKey)) {
|
|
2210
|
+
if (secretKeyMode(apiKey) === null) {
|
|
1823
2211
|
refuse(
|
|
1824
2212
|
"Listen requires a secret key (vp_sk_*). Publishable keys are rejected by the server."
|
|
1825
2213
|
);
|
|
@@ -1834,15 +2222,36 @@ function listenCommand() {
|
|
|
1834
2222
|
forwardUrl = verdict.url;
|
|
1835
2223
|
forwardWarning = verdict.warning;
|
|
1836
2224
|
}
|
|
1837
|
-
const
|
|
2225
|
+
const configuredBaseUrl = getBaseUrl();
|
|
2226
|
+
const destinationProblem = keyDestinationProblem(apiKey, configuredBaseUrl);
|
|
2227
|
+
if (destinationProblem) refuse(destinationProblem);
|
|
2228
|
+
const baseUrl = (configuredBaseUrl ?? "https://checkout.vonpay.com").replace(
|
|
1838
2229
|
/\/+$/,
|
|
1839
2230
|
""
|
|
1840
2231
|
);
|
|
2232
|
+
await confirmLiveAction(
|
|
2233
|
+
apiKey,
|
|
2234
|
+
{
|
|
2235
|
+
action: "stream live webhook delivery attempts",
|
|
2236
|
+
details: [
|
|
2237
|
+
["Stream from", baseUrl],
|
|
2238
|
+
...forwardUrl ? [["Forward to", forwardUrl.toString()]] : []
|
|
2239
|
+
],
|
|
2240
|
+
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."
|
|
2241
|
+
},
|
|
2242
|
+
opts
|
|
2243
|
+
);
|
|
1841
2244
|
const sseUrl = new URL(`${baseUrl}/v1/webhook_listen`);
|
|
1842
2245
|
if (opts.events && opts.events.length > 0) {
|
|
1843
2246
|
sseUrl.searchParams.set("event_types", opts.events.join(","));
|
|
1844
2247
|
}
|
|
1845
|
-
const sdk = forwardUrl ? new VonPayCheckout2({
|
|
2248
|
+
const sdk = forwardUrl ? new VonPayCheckout2({
|
|
2249
|
+
apiKey,
|
|
2250
|
+
baseUrl,
|
|
2251
|
+
allowCustomBaseUrl: allowCustomBaseUrl(),
|
|
2252
|
+
appInfo: CLI_APP_INFO,
|
|
2253
|
+
errorReporter: SILENT_ERROR_REPORTER
|
|
2254
|
+
}) : null;
|
|
1846
2255
|
const controller = new AbortController();
|
|
1847
2256
|
let eventCount = 0;
|
|
1848
2257
|
const startedAt = Date.now();
|
|
@@ -1909,7 +2318,7 @@ function listenCommand() {
|
|
|
1909
2318
|
onLifecycle({
|
|
1910
2319
|
kind: "disconnected",
|
|
1911
2320
|
reason: "server-ceiling (1h cap; auto-reconnecting)",
|
|
1912
|
-
willRetryInMs:
|
|
2321
|
+
willRetryInMs: SERVER_RECONNECT_FLOOR_MS
|
|
1913
2322
|
});
|
|
1914
2323
|
continue;
|
|
1915
2324
|
}
|
|
@@ -2012,7 +2421,7 @@ function lifecycleSeverity(kind) {
|
|
|
2012
2421
|
|
|
2013
2422
|
// src/commands/health.ts
|
|
2014
2423
|
import { Command as Command11 } from "commander";
|
|
2015
|
-
import
|
|
2424
|
+
import chalk14 from "chalk";
|
|
2016
2425
|
import { VonPayError as VonPayError2 } from "@vonpay/checkout-node";
|
|
2017
2426
|
function healthCommand() {
|
|
2018
2427
|
return new Command11("health").description("Check checkout API health").option("--json", "Output raw JSON").action(async (opts) => {
|
|
@@ -2024,7 +2433,7 @@ function healthCommand() {
|
|
|
2024
2433
|
if (opts.json) {
|
|
2025
2434
|
console.log(JSON.stringify(health, null, 2));
|
|
2026
2435
|
} else {
|
|
2027
|
-
const statusColor = health.status === "ok" ?
|
|
2436
|
+
const statusColor = health.status === "ok" ? chalk14.green : health.status === "degraded" ? chalk14.yellow : chalk14.red;
|
|
2028
2437
|
console.log(` Status: ${statusColor(health.status)}`);
|
|
2029
2438
|
console.log(` Latency: ${health.latencyMs}ms`);
|
|
2030
2439
|
if (health.version) {
|
|
@@ -2035,11 +2444,11 @@ function healthCommand() {
|
|
|
2035
2444
|
spinner.stop();
|
|
2036
2445
|
if (jsonErrorsEnabled()) handleError(err);
|
|
2037
2446
|
if (err instanceof VonPayError2) {
|
|
2038
|
-
console.error(
|
|
2039
|
-
console.error(
|
|
2447
|
+
console.error(chalk14.red(`API error: ${scrubSecrets(err.message)}`));
|
|
2448
|
+
console.error(chalk14.dim(` Code: ${err.code}`));
|
|
2040
2449
|
} else {
|
|
2041
2450
|
console.error(
|
|
2042
|
-
|
|
2451
|
+
chalk14.red(
|
|
2043
2452
|
`Cannot reach API: ${scrubSecrets(err instanceof Error ? err.message : String(err))}`
|
|
2044
2453
|
)
|
|
2045
2454
|
);
|
|
@@ -2051,7 +2460,7 @@ function healthCommand() {
|
|
|
2051
2460
|
|
|
2052
2461
|
// src/commands/doctor.ts
|
|
2053
2462
|
import { Command as Command12 } from "commander";
|
|
2054
|
-
import
|
|
2463
|
+
import chalk15 from "chalk";
|
|
2055
2464
|
import ora3 from "ora";
|
|
2056
2465
|
import { homedir as homedir2 } from "os";
|
|
2057
2466
|
import { existsSync as existsSync3 } from "fs";
|
|
@@ -2083,14 +2492,11 @@ function maskPrefix(key) {
|
|
|
2083
2492
|
}
|
|
2084
2493
|
function keyMode(key) {
|
|
2085
2494
|
if (!key) return void 0;
|
|
2086
|
-
|
|
2087
|
-
if (key.includes("_test_")) return "test";
|
|
2088
|
-
if (key.includes("_live_")) return "live";
|
|
2089
|
-
return void 0;
|
|
2495
|
+
return secretKeyMode(key) ?? "not_a_secret_key";
|
|
2090
2496
|
}
|
|
2091
2497
|
async function gather() {
|
|
2092
2498
|
const config = loadConfig();
|
|
2093
|
-
const apiKey = process.env.VON_PAY_SECRET_KEY
|
|
2499
|
+
const apiKey = process.env.VON_PAY_SECRET_KEY || config.checkout?.apiKey;
|
|
2094
2500
|
const baseUrl = process.env.VONPAY_BASE_URL ?? "https://checkout.vonpay.com";
|
|
2095
2501
|
const envVarsPresent = RELEVANT_ENV_VARS.filter(
|
|
2096
2502
|
(n) => process.env[n] && process.env[n] !== ""
|
|
@@ -2122,8 +2528,14 @@ async function gather() {
|
|
|
2122
2528
|
"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."
|
|
2123
2529
|
);
|
|
2124
2530
|
}
|
|
2531
|
+
const shapeProblem = keyDestinationProblem("", process.env.VONPAY_BASE_URL);
|
|
2532
|
+
if (shapeProblem) {
|
|
2533
|
+
report.health.baseUrl = "(not shown: the base URL was refused)";
|
|
2534
|
+
report.health.error = shapeProblem;
|
|
2535
|
+
report.hints.push(`${shapeProblem} No probe was run.`);
|
|
2536
|
+
}
|
|
2125
2537
|
const start = Date.now();
|
|
2126
|
-
try {
|
|
2538
|
+
if (!shapeProblem) try {
|
|
2127
2539
|
const controller = new AbortController();
|
|
2128
2540
|
const timer = setTimeout(() => controller.abort(), 1e4);
|
|
2129
2541
|
const res = await fetch(`${baseUrl}/api/health`, {
|
|
@@ -2155,11 +2567,27 @@ async function gather() {
|
|
|
2155
2567
|
"Health probe could not reach the API. Check network / DNS / firewall \u2014 vonpay-checkout requires outbound HTTPS to checkout.vonpay.com."
|
|
2156
2568
|
);
|
|
2157
2569
|
}
|
|
2158
|
-
|
|
2570
|
+
let mayProbe = Boolean(apiKey) && !shapeProblem;
|
|
2571
|
+
if (apiKey && report.apiKeyMode === "not_a_secret_key") {
|
|
2572
|
+
mayProbe = false;
|
|
2573
|
+
report.hints.push(`${NOT_A_SECRET_KEY_MESSAGE} The sessions.validate probe was not run.`);
|
|
2574
|
+
}
|
|
2575
|
+
const destinationProblem = apiKey && mayProbe ? keyDestinationProblem(apiKey, process.env.VONPAY_BASE_URL) : null;
|
|
2576
|
+
if (destinationProblem) {
|
|
2577
|
+
mayProbe = false;
|
|
2578
|
+
report.hints.push(`${destinationProblem} The sessions.validate probe was not run.`);
|
|
2579
|
+
}
|
|
2580
|
+
if (config.checkout?.apiKey && report.configFile.keyType === "live") {
|
|
2581
|
+
report.hints.push(
|
|
2582
|
+
"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."
|
|
2583
|
+
);
|
|
2584
|
+
}
|
|
2585
|
+
if (apiKey && mayProbe && report.health.reachable) {
|
|
2159
2586
|
try {
|
|
2160
2587
|
const probeClient = new VonPayCheckout3({
|
|
2161
2588
|
apiKey,
|
|
2162
2589
|
baseUrl,
|
|
2590
|
+
allowCustomBaseUrl: allowCustomBaseUrl(),
|
|
2163
2591
|
timeout: 1e4,
|
|
2164
2592
|
maxRetries: 0,
|
|
2165
2593
|
errorReporter: () => {
|
|
@@ -2216,47 +2644,47 @@ async function gather() {
|
|
|
2216
2644
|
}
|
|
2217
2645
|
function renderHuman(r) {
|
|
2218
2646
|
const lines = [];
|
|
2219
|
-
lines.push(
|
|
2220
|
-
lines.push(
|
|
2647
|
+
lines.push(chalk15.bold.blue("vonpay doctor \u2014 diagnostic report"));
|
|
2648
|
+
lines.push(chalk15.dim(`generated ${r.generatedAt}`));
|
|
2221
2649
|
lines.push("");
|
|
2222
|
-
lines.push(
|
|
2650
|
+
lines.push(chalk15.bold("Runtime"));
|
|
2223
2651
|
lines.push(` CLI: ${r.cli.version}`);
|
|
2224
2652
|
lines.push(` Node: ${r.runtime.node}`);
|
|
2225
2653
|
lines.push(` Platform: ${r.runtime.platform} (${r.runtime.arch})`);
|
|
2226
2654
|
lines.push(` CWD: ${r.runtime.cwd}`);
|
|
2227
2655
|
lines.push("");
|
|
2228
|
-
lines.push(
|
|
2656
|
+
lines.push(chalk15.bold("API key"));
|
|
2229
2657
|
if (r.apiKeyPrefix) {
|
|
2230
2658
|
lines.push(` Prefix: ${r.apiKeyPrefix}`);
|
|
2231
2659
|
lines.push(` Mode: ${r.apiKeyMode}`);
|
|
2232
2660
|
} else {
|
|
2233
|
-
lines.push(
|
|
2661
|
+
lines.push(chalk15.dim(" (no key found in env or ~/.vonpay/config.json)"));
|
|
2234
2662
|
}
|
|
2235
2663
|
lines.push("");
|
|
2236
|
-
lines.push(
|
|
2664
|
+
lines.push(chalk15.bold("Env vars present (names only)"));
|
|
2237
2665
|
if (r.envVarsPresent.length === 0) {
|
|
2238
|
-
lines.push(
|
|
2666
|
+
lines.push(chalk15.dim(" (none of the expected vonpay env vars are set)"));
|
|
2239
2667
|
} else {
|
|
2240
2668
|
for (const n of r.envVarsPresent) lines.push(` \u2713 ${n}`);
|
|
2241
2669
|
}
|
|
2242
2670
|
lines.push("");
|
|
2243
|
-
lines.push(
|
|
2671
|
+
lines.push(chalk15.bold("Health probe"));
|
|
2244
2672
|
if (r.health.reachable) {
|
|
2245
|
-
const statusColor = r.health.status === "ok" ?
|
|
2246
|
-
lines.push(` Reachable: ${
|
|
2673
|
+
const statusColor = r.health.status === "ok" ? chalk15.green : r.health.status === "degraded" ? chalk15.yellow : chalk15.red;
|
|
2674
|
+
lines.push(` Reachable: ${chalk15.green("yes")}`);
|
|
2247
2675
|
lines.push(` Status: ${statusColor(r.health.status ?? "?")}`);
|
|
2248
2676
|
lines.push(` Latency: ${r.health.latencyMs}ms`);
|
|
2249
2677
|
} else {
|
|
2250
|
-
lines.push(` Reachable: ${
|
|
2678
|
+
lines.push(` Reachable: ${chalk15.red("no")}`);
|
|
2251
2679
|
lines.push(` Error: ${r.health.error}`);
|
|
2252
2680
|
}
|
|
2253
2681
|
lines.push("");
|
|
2254
2682
|
if (r.validateProbe) {
|
|
2255
|
-
lines.push(
|
|
2683
|
+
lines.push(chalk15.bold("sessions.validate probe ($14.99 USD dry-run)"));
|
|
2256
2684
|
if (r.validateProbe.ok) {
|
|
2257
|
-
lines.push(` Result: ${
|
|
2685
|
+
lines.push(` Result: ${chalk15.green("valid")}`);
|
|
2258
2686
|
} else {
|
|
2259
|
-
lines.push(` Result: ${
|
|
2687
|
+
lines.push(` Result: ${chalk15.red("invalid")}`);
|
|
2260
2688
|
if (r.validateProbe.error) {
|
|
2261
2689
|
lines.push(` Code: ${r.validateProbe.error.code}`);
|
|
2262
2690
|
lines.push(` Action: ${r.validateProbe.error.nextAction}`);
|
|
@@ -2269,7 +2697,7 @@ function renderHuman(r) {
|
|
|
2269
2697
|
lines.push("");
|
|
2270
2698
|
}
|
|
2271
2699
|
if (r.hints.length > 0) {
|
|
2272
|
-
lines.push(
|
|
2700
|
+
lines.push(chalk15.bold("Hints"));
|
|
2273
2701
|
for (const h of r.hints) lines.push(` \u2022 ${h}`);
|
|
2274
2702
|
lines.push("");
|
|
2275
2703
|
}
|