@vonpay/checkout-cli 0.16.0 → 0.18.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.
- package/README.md +19 -7
- package/dist/index.js +1368 -285
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/
|
|
3
|
+
// src/program.ts
|
|
4
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
5
|
|
|
9
6
|
// src/commands/login.ts
|
|
10
7
|
import { Command, Option } from "commander";
|
|
@@ -12,9 +9,9 @@ import { createInterface } from "readline";
|
|
|
12
9
|
import chalk from "chalk";
|
|
13
10
|
|
|
14
11
|
// src/config.ts
|
|
15
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
16
|
-
import { homedir } from "os";
|
|
17
|
-
import { join } from "path";
|
|
12
|
+
import { chmodSync as chmodSync2, existsSync, mkdirSync as mkdirSync2, readFileSync, rmSync, writeFileSync } from "fs";
|
|
13
|
+
import { homedir as homedir2 } from "os";
|
|
14
|
+
import { join as join2 } from "path";
|
|
18
15
|
|
|
19
16
|
// src/utils/json-errors.ts
|
|
20
17
|
var jsonErrors = false;
|
|
@@ -50,9 +47,447 @@ function secretKeyMode(key) {
|
|
|
50
47
|
}
|
|
51
48
|
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.";
|
|
52
49
|
|
|
50
|
+
// src/utils/audit-log.ts
|
|
51
|
+
import {
|
|
52
|
+
chmodSync,
|
|
53
|
+
closeSync,
|
|
54
|
+
constants as FS,
|
|
55
|
+
fchmodSync,
|
|
56
|
+
fstatSync,
|
|
57
|
+
lstatSync,
|
|
58
|
+
mkdirSync,
|
|
59
|
+
openSync,
|
|
60
|
+
readdirSync,
|
|
61
|
+
unlinkSync,
|
|
62
|
+
writeSync
|
|
63
|
+
} from "fs";
|
|
64
|
+
import { homedir } from "os";
|
|
65
|
+
import { join, resolve } from "path";
|
|
66
|
+
import { isErrorCode, stripHiddenCharacters } from "@vonpay/checkout-node";
|
|
67
|
+
var AUDIT_FIELDS = [
|
|
68
|
+
"ts",
|
|
69
|
+
"surface",
|
|
70
|
+
"version",
|
|
71
|
+
"command",
|
|
72
|
+
"key_mode",
|
|
73
|
+
"key_fingerprint",
|
|
74
|
+
"outcome",
|
|
75
|
+
"error_code",
|
|
76
|
+
"request_id",
|
|
77
|
+
"amount",
|
|
78
|
+
"currency",
|
|
79
|
+
"target"
|
|
80
|
+
];
|
|
81
|
+
var AUDIT_OUTCOMES = [
|
|
82
|
+
"pending",
|
|
83
|
+
// a money command is about to send; the command's final line follows
|
|
84
|
+
"sent",
|
|
85
|
+
// an API command ran to the end (sent and answered); no person was asked
|
|
86
|
+
"local",
|
|
87
|
+
// a command that sends nothing to the API (login, logout, init, trigger) ran to the end
|
|
88
|
+
"approved",
|
|
89
|
+
// a person confirmed it at the terminal, and it ran to the end
|
|
90
|
+
"declined",
|
|
91
|
+
// the person did not type "confirm"
|
|
92
|
+
"budget_refused",
|
|
93
|
+
// (the MCP server's spending budget; the CLI has none)
|
|
94
|
+
"read_mode_blocked",
|
|
95
|
+
// (the MCP server's read-only mode; the CLI has none)
|
|
96
|
+
"withheld",
|
|
97
|
+
// a live action with no person at an interactive terminal to confirm it
|
|
98
|
+
"lookup_refused",
|
|
99
|
+
// the check before the confirmation failed (a refund's payment could not be looked up or did not match)
|
|
100
|
+
"refused_local",
|
|
101
|
+
// refused by the CLI before anything was sent (a flag, a key rule, an unsafe base URL, a command line it cannot parse, …)
|
|
102
|
+
"error"
|
|
103
|
+
// the API (or the network) answered with an error, the run was interrupted, or the CLI crashed (internal_error)
|
|
104
|
+
];
|
|
105
|
+
var LOCAL_ERROR_CODES = [
|
|
106
|
+
"auth_failed",
|
|
107
|
+
"budget",
|
|
108
|
+
"cancelled",
|
|
109
|
+
"confirm_live_required",
|
|
110
|
+
"currency_mismatch",
|
|
111
|
+
"declined",
|
|
112
|
+
"dismissed",
|
|
113
|
+
"excess_arguments",
|
|
114
|
+
"idempotency_key_required",
|
|
115
|
+
"internal_error",
|
|
116
|
+
"interrupted",
|
|
117
|
+
"invalid_argument",
|
|
118
|
+
"invalid_input",
|
|
119
|
+
"live_loopback_url",
|
|
120
|
+
"local_refusal",
|
|
121
|
+
"lookup_failed",
|
|
122
|
+
"missing_argument",
|
|
123
|
+
"network_error",
|
|
124
|
+
"no_approval_channel",
|
|
125
|
+
"no_terminal",
|
|
126
|
+
"not_ticked",
|
|
127
|
+
"prompt_failed",
|
|
128
|
+
"read_mode",
|
|
129
|
+
"unknown_command",
|
|
130
|
+
"unknown_option",
|
|
131
|
+
"unknown_tool",
|
|
132
|
+
"usage_error"
|
|
133
|
+
];
|
|
134
|
+
var LOCAL_CODES = new Set(LOCAL_ERROR_CODES);
|
|
135
|
+
var AUDIT_DIR_ENV = "VONPAY_AUDIT_DIR";
|
|
136
|
+
var AUDIT_SWITCH_ENV = "VONPAY_AUDIT_LOG";
|
|
137
|
+
var AUDIT_RETENTION_ENV = "VONPAY_AUDIT_RETENTION_DAYS";
|
|
138
|
+
var DEFAULT_RETENTION_DAYS = 30;
|
|
139
|
+
var MAX_FILE_BYTES = 50 * 1024 * 1024;
|
|
140
|
+
function auditText(s, max) {
|
|
141
|
+
const line = stripHiddenCharacters(s).replace(/[\p{Cc}\u{2028}\u{2029}]/gu, " ").trim();
|
|
142
|
+
return line.length > max ? line.slice(0, max) : line;
|
|
143
|
+
}
|
|
144
|
+
function keyFingerprint(key) {
|
|
145
|
+
if (key.length >= 24) return `${key.slice(0, 12)}\u2026${key.slice(-4)}`;
|
|
146
|
+
const head = Math.min(12, Math.floor(key.length / 3));
|
|
147
|
+
const tail = Math.min(4, Math.floor(key.length / 4));
|
|
148
|
+
return `${key.slice(0, head)}\u2026${key.slice(key.length - tail)}`;
|
|
149
|
+
}
|
|
150
|
+
var TARGET_ID = /^(vpi|vp_tx|vsub|vp_cs)_(live|test)_[A-Za-z0-9_-]{16}$/;
|
|
151
|
+
var REQUEST_ID = /^[A-Za-z0-9_-]{12}$/;
|
|
152
|
+
var CURRENCY = /^[A-Za-z]{3}$/;
|
|
153
|
+
var COMMAND = /^[a-z0-9-]+( [a-z0-9-]+){0,3}$/;
|
|
154
|
+
var UNKNOWN_COMMAND = "[unknown command]";
|
|
155
|
+
var UNRECOGNISED_ID = "[unrecognised id]";
|
|
156
|
+
var UNRECOGNISED_REQUEST_ID = "[unrecognised request id]";
|
|
157
|
+
function knownErrorCode(code) {
|
|
158
|
+
return typeof code === "string" && (isErrorCode(code) || LOCAL_CODES.has(code));
|
|
159
|
+
}
|
|
160
|
+
function auditLine(record) {
|
|
161
|
+
const out = {};
|
|
162
|
+
for (const field of AUDIT_FIELDS) {
|
|
163
|
+
const v = record[field];
|
|
164
|
+
if (v === void 0 || v === null) continue;
|
|
165
|
+
switch (field) {
|
|
166
|
+
case "amount":
|
|
167
|
+
if (typeof v === "number" && Number.isSafeInteger(v) && v >= 0) out[field] = v;
|
|
168
|
+
break;
|
|
169
|
+
case "currency":
|
|
170
|
+
if (typeof v === "string" && CURRENCY.test(v)) out[field] = v.toUpperCase();
|
|
171
|
+
break;
|
|
172
|
+
case "target":
|
|
173
|
+
if (typeof v === "string") out[field] = TARGET_ID.test(v) ? v : UNRECOGNISED_ID;
|
|
174
|
+
break;
|
|
175
|
+
case "error_code":
|
|
176
|
+
if (typeof v === "string") out[field] = knownErrorCode(v) ? v : "unrecognised_code";
|
|
177
|
+
break;
|
|
178
|
+
case "request_id":
|
|
179
|
+
if (typeof v === "string") out[field] = REQUEST_ID.test(v) ? v : UNRECOGNISED_REQUEST_ID;
|
|
180
|
+
break;
|
|
181
|
+
case "outcome":
|
|
182
|
+
if (typeof v === "string" && AUDIT_OUTCOMES.includes(v)) out[field] = v;
|
|
183
|
+
break;
|
|
184
|
+
case "key_mode":
|
|
185
|
+
if (v === "test" || v === "live") out[field] = v;
|
|
186
|
+
break;
|
|
187
|
+
case "command":
|
|
188
|
+
if (typeof v === "string") out[field] = COMMAND.test(v) ? v : UNKNOWN_COMMAND;
|
|
189
|
+
break;
|
|
190
|
+
default:
|
|
191
|
+
if (typeof v === "string") out[field] = auditText(v, field === "ts" ? 40 : 80);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return `${JSON.stringify(out)}
|
|
195
|
+
`;
|
|
196
|
+
}
|
|
197
|
+
function auditSettingsFromEnv(env = process.env, warn = stderrWarn) {
|
|
198
|
+
const off = ["off", "0", "false", "no"].includes((env[AUDIT_SWITCH_ENV] ?? "").trim().toLowerCase());
|
|
199
|
+
const raw = (env[AUDIT_RETENTION_ENV] ?? "").trim();
|
|
200
|
+
let retentionDays = DEFAULT_RETENTION_DAYS;
|
|
201
|
+
if (raw !== "") {
|
|
202
|
+
const n = Number(raw);
|
|
203
|
+
if (/^[0-9]{1,3}$/.test(raw) && n >= 1 && n <= 365) retentionDays = n;
|
|
204
|
+
else warn(`[vonpay] ${AUDIT_RETENTION_ENV} must be a whole number from 1 to 365; keeping audit files ${DEFAULT_RETENTION_DAYS} days.`);
|
|
205
|
+
}
|
|
206
|
+
const dir = env[AUDIT_DIR_ENV]?.trim() ? resolve(env[AUDIT_DIR_ENV].trim()) : join(homedir(), ".vonpay", "audit");
|
|
207
|
+
return { dir: off ? null : dir, retentionDays };
|
|
208
|
+
}
|
|
209
|
+
function stderrWarn(m) {
|
|
210
|
+
try {
|
|
211
|
+
process.stderr.write(`${m}
|
|
212
|
+
`);
|
|
213
|
+
} catch (err) {
|
|
214
|
+
void err;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function errorCodeOf(err) {
|
|
218
|
+
return err !== null && typeof err === "object" && typeof err.code === "string" ? err.code : "error";
|
|
219
|
+
}
|
|
220
|
+
var AuditPlaceRefused = class extends Error {
|
|
221
|
+
constructor(code) {
|
|
222
|
+
super(code);
|
|
223
|
+
this.code = code;
|
|
224
|
+
}
|
|
225
|
+
code;
|
|
226
|
+
};
|
|
227
|
+
function myUid() {
|
|
228
|
+
return typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
229
|
+
}
|
|
230
|
+
function checkDir(dir, st) {
|
|
231
|
+
if (st.isSymbolicLink()) throw new AuditPlaceRefused("directory_is_a_symlink");
|
|
232
|
+
if (!st.isDirectory()) throw new AuditPlaceRefused("not_a_directory");
|
|
233
|
+
const uid = myUid();
|
|
234
|
+
if (uid !== void 0 && st.uid !== uid) throw new AuditPlaceRefused("directory_owned_by_another_user");
|
|
235
|
+
if (uid !== void 0 && (st.mode & 63) !== 0) chmodSync(dir, 448);
|
|
236
|
+
}
|
|
237
|
+
var FILE_NAME = /^cli-(\d{4})-(\d{2})-(\d{2})\.jsonl$/;
|
|
238
|
+
var appendFlags = () => FS.O_APPEND | FS.O_CREAT | FS.O_WRONLY | (FS.O_NOFOLLOW ?? 0);
|
|
239
|
+
var warned = false;
|
|
240
|
+
var warnedFull = false;
|
|
241
|
+
function warnOnce(dir, err, warn) {
|
|
242
|
+
if (warned) return;
|
|
243
|
+
warned = true;
|
|
244
|
+
warn(
|
|
245
|
+
`[vonpay] The local audit log cannot be written (${errorCodeOf(err)}) in ${dir}; the command went on without it. Set ${AUDIT_DIR_ENV} to a writable directory of your own, or ${AUDIT_SWITCH_ENV}=off.`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
function pruneAuditFiles(dir, retentionDays, now = /* @__PURE__ */ new Date(), warn = stderrWarn) {
|
|
249
|
+
let names;
|
|
250
|
+
try {
|
|
251
|
+
checkDir(dir, lstatSync(dir));
|
|
252
|
+
names = readdirSync(dir);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
if (errorCodeOf(err) !== "ENOENT") warnOnce(dir, err, warn);
|
|
255
|
+
return 0;
|
|
256
|
+
}
|
|
257
|
+
const today = Date.parse(`${now.toISOString().slice(0, 10)}T00:00:00Z`);
|
|
258
|
+
let removed = 0;
|
|
259
|
+
let skipped = 0;
|
|
260
|
+
for (const name of names) {
|
|
261
|
+
const m = FILE_NAME.exec(name);
|
|
262
|
+
if (!m) continue;
|
|
263
|
+
const day = Date.parse(`${m[1]}-${m[2]}-${m[3]}T00:00:00Z`);
|
|
264
|
+
if (!Number.isFinite(day) || (today - day) / 864e5 <= retentionDays) continue;
|
|
265
|
+
const path = join(dir, name);
|
|
266
|
+
try {
|
|
267
|
+
if (!lstatSync(path).isFile()) {
|
|
268
|
+
skipped++;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
unlinkSync(path);
|
|
272
|
+
removed++;
|
|
273
|
+
} catch {
|
|
274
|
+
skipped++;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (skipped > 0) warn(`[vonpay] ${skipped} old audit file(s) in ${dir} were not deleted (not a regular file, or could not be removed).`);
|
|
278
|
+
return removed;
|
|
279
|
+
}
|
|
280
|
+
function appendSecure(file, line, maxBytes, dir, warn) {
|
|
281
|
+
const fd = openSync(file, appendFlags(), 384);
|
|
282
|
+
try {
|
|
283
|
+
const st = fstatSync(fd);
|
|
284
|
+
if (!st.isFile()) throw new AuditPlaceRefused("not_a_regular_file");
|
|
285
|
+
const uid = myUid();
|
|
286
|
+
if (uid !== void 0 && st.uid !== uid) throw new AuditPlaceRefused("file_owned_by_another_user");
|
|
287
|
+
if (uid !== void 0 && (st.mode & 63) !== 0) fchmodSync(fd, 384);
|
|
288
|
+
if (st.size + Buffer.byteLength(line) > maxBytes) {
|
|
289
|
+
if (!warnedFull) {
|
|
290
|
+
warnedFull = true;
|
|
291
|
+
warn(`[vonpay] Today's audit file in ${dir} is full; no more lines are written to it today.`);
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
writeSync(fd, line);
|
|
296
|
+
} finally {
|
|
297
|
+
closeSync(fd);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function writeAuditLine(dir, record, now = /* @__PURE__ */ new Date(), warn = stderrWarn, maxBytes = MAX_FILE_BYTES) {
|
|
301
|
+
const file = join(dir, `cli-${now.toISOString().slice(0, 10)}.jsonl`);
|
|
302
|
+
try {
|
|
303
|
+
const line = auditLine(record);
|
|
304
|
+
const attempt = () => {
|
|
305
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
306
|
+
checkDir(dir, lstatSync(dir));
|
|
307
|
+
appendSecure(file, line, maxBytes, dir, warn);
|
|
308
|
+
};
|
|
309
|
+
try {
|
|
310
|
+
attempt();
|
|
311
|
+
} catch (err) {
|
|
312
|
+
if (errorCodeOf(err) !== "ENOENT") throw err;
|
|
313
|
+
attempt();
|
|
314
|
+
}
|
|
315
|
+
return true;
|
|
316
|
+
} catch (err) {
|
|
317
|
+
warnOnce(dir, err, warn);
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
var COMMAND_KIND = {
|
|
322
|
+
"checkout login": "local",
|
|
323
|
+
"checkout logout": "local",
|
|
324
|
+
"checkout init": "local",
|
|
325
|
+
"checkout trigger": "local",
|
|
326
|
+
// signs a synthetic event and sends it to YOUR endpoint, not the API
|
|
327
|
+
"checkout sessions create": "api",
|
|
328
|
+
"checkout sessions get": "api",
|
|
329
|
+
"checkout payment-intents create": "api",
|
|
330
|
+
"checkout payment-intents get": "api",
|
|
331
|
+
"checkout payment-intents capture": "api",
|
|
332
|
+
"checkout payment-intents void": "api",
|
|
333
|
+
"checkout payment-intents list": "api",
|
|
334
|
+
"checkout payment-intents summary": "api",
|
|
335
|
+
"checkout refunds create": "api",
|
|
336
|
+
"checkout refunds list": "api",
|
|
337
|
+
"checkout tokens create": "api",
|
|
338
|
+
"checkout capabilities": "api",
|
|
339
|
+
"checkout listen": "api",
|
|
340
|
+
"checkout health": "api",
|
|
341
|
+
"checkout doctor": "api"
|
|
342
|
+
};
|
|
343
|
+
var MONEY = {
|
|
344
|
+
"checkout payment-intents create": (o) => ({ amount: wholeNumber(o.amount), currency: o.currency, target: o.sessionId }),
|
|
345
|
+
"checkout payment-intents capture": (o, a) => ({ amount: wholeNumber(o.amountToCapture), target: a[0] }),
|
|
346
|
+
"checkout payment-intents void": (_o, a) => ({ target: a[0] }),
|
|
347
|
+
"checkout refunds create": (o) => ({ amount: wholeNumber(o.amount), currency: o.currency, target: o.paymentIntent ?? o.transaction }),
|
|
348
|
+
"checkout sessions create": (o) => ({ amount: wholeNumber(o.amount), currency: o.currency })
|
|
349
|
+
};
|
|
350
|
+
function wholeNumber(v) {
|
|
351
|
+
return typeof v === "string" && /^[0-9]{1,15}$/.test(v.trim()) ? Number(v.trim()) : typeof v === "number" ? v : void 0;
|
|
352
|
+
}
|
|
353
|
+
var pending = null;
|
|
354
|
+
var hooked = false;
|
|
355
|
+
var SIGNAL_EXIT = { SIGINT: 130, SIGTERM: 143 };
|
|
356
|
+
var processHandlers = [];
|
|
357
|
+
function keyFields(apiKey) {
|
|
358
|
+
return apiKey ? { key_mode: apiKey.startsWith("vp_sk_test_") ? "test" : "live", key_fingerprint: keyFingerprint(apiKey) } : {};
|
|
359
|
+
}
|
|
360
|
+
var signalsHandledByCommand = false;
|
|
361
|
+
function auditSignalHandledByCommand() {
|
|
362
|
+
signalsHandledByCommand = true;
|
|
363
|
+
}
|
|
364
|
+
function hookProcess() {
|
|
365
|
+
if (hooked) return;
|
|
366
|
+
hooked = true;
|
|
367
|
+
const onExit = (code) => auditFinish(code);
|
|
368
|
+
const onCrash = () => auditNote({ crashed: true });
|
|
369
|
+
process.on("exit", onExit);
|
|
370
|
+
process.on("uncaughtExceptionMonitor", onCrash);
|
|
371
|
+
processHandlers.push(["exit", onExit], ["uncaughtExceptionMonitor", onCrash]);
|
|
372
|
+
for (const sig of ["SIGINT", "SIGTERM"]) {
|
|
373
|
+
const handler = () => {
|
|
374
|
+
if (signalsHandledByCommand) return;
|
|
375
|
+
auditNote({ interrupted: true });
|
|
376
|
+
auditFinish(SIGNAL_EXIT[sig]);
|
|
377
|
+
process.exit(SIGNAL_EXIT[sig]);
|
|
378
|
+
};
|
|
379
|
+
processHandlers.push([sig, handler]);
|
|
380
|
+
process.on(sig, handler);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function auditStart(command, version, opts = {}, args = []) {
|
|
384
|
+
const settings = auditSettingsFromEnv();
|
|
385
|
+
if (settings.dir === null) {
|
|
386
|
+
pending = null;
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
pruneAuditFiles(settings.dir, settings.retentionDays);
|
|
390
|
+
let money = {};
|
|
391
|
+
try {
|
|
392
|
+
money = Object.prototype.hasOwnProperty.call(MONEY, command) ? MONEY[command](opts, args) : {};
|
|
393
|
+
} catch {
|
|
394
|
+
money = {};
|
|
395
|
+
}
|
|
396
|
+
pending = { command, money, settings, version, intentWritten: false, written: false };
|
|
397
|
+
if (typeof opts.apiKey === "string") pending.apiKey = opts.apiKey;
|
|
398
|
+
hookProcess();
|
|
399
|
+
}
|
|
400
|
+
function auditNote(note, onlyIfUnset = false) {
|
|
401
|
+
if (!pending) return;
|
|
402
|
+
if (onlyIfUnset && pending.outcome !== void 0 && note.outcome !== void 0) return;
|
|
403
|
+
Object.assign(pending, note);
|
|
404
|
+
}
|
|
405
|
+
function auditError(err) {
|
|
406
|
+
if (err === null || typeof err !== "object") return;
|
|
407
|
+
const e = err;
|
|
408
|
+
if (typeof e.status === "number" && typeof e.code === "string") {
|
|
409
|
+
auditNote({ apiCode: e.code, ...typeof e.requestId === "string" ? { requestId: e.requestId } : {} });
|
|
410
|
+
} else if (e.cause !== void 0 && err instanceof TypeError) {
|
|
411
|
+
auditNote({ networkError: true });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function auditIntent(now = /* @__PURE__ */ new Date()) {
|
|
415
|
+
const p = pending;
|
|
416
|
+
if (!p || p.intentWritten || p.settings.dir === null || !Object.prototype.hasOwnProperty.call(MONEY, p.command)) return;
|
|
417
|
+
p.intentWritten = true;
|
|
418
|
+
writeAuditLine(p.settings.dir, { command: p.command, ts: now.toISOString(), surface: "cli", version: p.version, ...keyFields(p.apiKey), outcome: "pending", ...p.money }, now);
|
|
419
|
+
}
|
|
420
|
+
function auditFinish(exitCode, now = /* @__PURE__ */ new Date()) {
|
|
421
|
+
const p = pending;
|
|
422
|
+
if (!p || p.written || p.settings.dir === null) return;
|
|
423
|
+
p.written = true;
|
|
424
|
+
try {
|
|
425
|
+
let outcome;
|
|
426
|
+
let code = p.code;
|
|
427
|
+
if (p.crashed) [outcome, code] = ["error", "internal_error"];
|
|
428
|
+
else if (p.interrupted) [outcome, code] = ["error", "interrupted"];
|
|
429
|
+
else if (p.outcome) outcome = p.outcome;
|
|
430
|
+
else if (p.apiCode) [outcome, code] = ["error", p.apiCode];
|
|
431
|
+
else if (p.networkError) [outcome, code] = ["error", "network_error"];
|
|
432
|
+
else if (exitCode === 0) outcome = p.approved ? "approved" : COMMAND_KIND[p.command] === "local" ? "local" : "sent";
|
|
433
|
+
else [outcome, code] = ["refused_local", code ?? "local_refusal"];
|
|
434
|
+
writeAuditLine(
|
|
435
|
+
p.settings.dir,
|
|
436
|
+
{
|
|
437
|
+
command: p.command,
|
|
438
|
+
ts: now.toISOString(),
|
|
439
|
+
surface: "cli",
|
|
440
|
+
version: p.version,
|
|
441
|
+
...keyFields(p.apiKey),
|
|
442
|
+
outcome,
|
|
443
|
+
...code ? { error_code: code } : {},
|
|
444
|
+
...p.requestId ? { request_id: p.requestId } : {},
|
|
445
|
+
...p.money
|
|
446
|
+
},
|
|
447
|
+
now
|
|
448
|
+
);
|
|
449
|
+
} catch (err) {
|
|
450
|
+
warnOnce(p.settings.dir, err, stderrWarn);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function usageReason(commanderCode) {
|
|
454
|
+
switch (commanderCode) {
|
|
455
|
+
case "commander.unknownCommand":
|
|
456
|
+
return "unknown_command";
|
|
457
|
+
case "commander.unknownOption":
|
|
458
|
+
return "unknown_option";
|
|
459
|
+
case "commander.missingArgument":
|
|
460
|
+
case "commander.optionMissingArgument":
|
|
461
|
+
case "commander.missingMandatoryOptionValue":
|
|
462
|
+
return "missing_argument";
|
|
463
|
+
case "commander.invalidArgument":
|
|
464
|
+
return "invalid_argument";
|
|
465
|
+
case "commander.excessArguments":
|
|
466
|
+
return "excess_arguments";
|
|
467
|
+
default:
|
|
468
|
+
return "usage_error";
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
function auditUsageError(command, version, commanderCode, exitCode, now = /* @__PURE__ */ new Date()) {
|
|
472
|
+
try {
|
|
473
|
+
if (exitCode === 0) return;
|
|
474
|
+
const reason = usageReason(commanderCode);
|
|
475
|
+
if (pending) {
|
|
476
|
+
auditNote({ outcome: "refused_local", code: reason }, true);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const settings = auditSettingsFromEnv();
|
|
480
|
+
if (settings.dir === null) return;
|
|
481
|
+
const name = reason === "unknown_command" || command === "" ? UNKNOWN_COMMAND : command;
|
|
482
|
+
writeAuditLine(settings.dir, { command: name, ts: now.toISOString(), surface: "cli", version, outcome: "refused_local", error_code: reason }, now);
|
|
483
|
+
} catch (err) {
|
|
484
|
+
void err;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
53
488
|
// src/config.ts
|
|
54
|
-
var CONFIG_DIR =
|
|
55
|
-
var CONFIG_FILE =
|
|
489
|
+
var CONFIG_DIR = join2(homedir2(), ".vonpay");
|
|
490
|
+
var CONFIG_FILE = join2(CONFIG_DIR, "config.json");
|
|
56
491
|
function loadConfig() {
|
|
57
492
|
if (!existsSync(CONFIG_FILE)) return {};
|
|
58
493
|
try {
|
|
@@ -79,13 +514,13 @@ function saveConfig(config) {
|
|
|
79
514
|
warnStoredLiveKey();
|
|
80
515
|
}
|
|
81
516
|
if (!existsSync(CONFIG_DIR)) {
|
|
82
|
-
|
|
517
|
+
mkdirSync2(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
83
518
|
}
|
|
84
519
|
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", {
|
|
85
520
|
encoding: "utf-8",
|
|
86
521
|
mode: 384
|
|
87
522
|
});
|
|
88
|
-
|
|
523
|
+
chmodSync2(CONFIG_FILE, 384);
|
|
89
524
|
}
|
|
90
525
|
function clearStoredApiKey() {
|
|
91
526
|
const config = loadConfig();
|
|
@@ -124,6 +559,7 @@ function requireApiKey() {
|
|
|
124
559
|
if (secretKeyMode(key) === null) {
|
|
125
560
|
emitCliError({ error: NOT_A_SECRET_KEY_MESSAGE, code: null }, NOT_A_SECRET_KEY_MESSAGE);
|
|
126
561
|
}
|
|
562
|
+
auditNote({ apiKey: key });
|
|
127
563
|
return key;
|
|
128
564
|
}
|
|
129
565
|
|
|
@@ -146,10 +582,10 @@ function evaluateKeyToSave(key) {
|
|
|
146
582
|
}
|
|
147
583
|
function readLine(prompt) {
|
|
148
584
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
149
|
-
return new Promise((
|
|
585
|
+
return new Promise((resolve3) => {
|
|
150
586
|
rl.question(prompt, (answer) => {
|
|
151
587
|
rl.close();
|
|
152
|
-
|
|
588
|
+
resolve3(answer.trim());
|
|
153
589
|
});
|
|
154
590
|
});
|
|
155
591
|
}
|
|
@@ -222,8 +658,8 @@ function logoutCommand() {
|
|
|
222
658
|
|
|
223
659
|
// src/commands/init.ts
|
|
224
660
|
import { Command as Command3 } from "commander";
|
|
225
|
-
import { appendFileSync, chmodSync as
|
|
226
|
-
import { resolve } from "path";
|
|
661
|
+
import { appendFileSync, chmodSync as chmodSync3, existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
662
|
+
import { resolve as resolve2 } from "path";
|
|
227
663
|
import chalk3 from "chalk";
|
|
228
664
|
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.";
|
|
229
665
|
function ensureGitignoreBlocksEnv() {
|
|
@@ -274,7 +710,7 @@ function initCommand() {
|
|
|
274
710
|
process.exit(1);
|
|
275
711
|
}
|
|
276
712
|
const envFile = ".env";
|
|
277
|
-
const absPath =
|
|
713
|
+
const absPath = resolve2(envFile);
|
|
278
714
|
if (existsSync2(envFile) && !opts.force) {
|
|
279
715
|
console.error(
|
|
280
716
|
chalk3.yellow(
|
|
@@ -299,7 +735,7 @@ function initCommand() {
|
|
|
299
735
|
""
|
|
300
736
|
].join("\n");
|
|
301
737
|
writeFileSync2(envFile, content, { encoding: "utf-8", mode: 384 });
|
|
302
|
-
|
|
738
|
+
chmodSync3(envFile, 384);
|
|
303
739
|
console.log(chalk3.green(`\u2713 .env created (${mode} mode) \u2192 ${absPath}`));
|
|
304
740
|
const gitignoreState = ensureGitignoreBlocksEnv();
|
|
305
741
|
if (gitignoreState === "added") {
|
|
@@ -318,7 +754,7 @@ function initCommand() {
|
|
|
318
754
|
|
|
319
755
|
// src/commands/sessions.ts
|
|
320
756
|
import { Command as Command4 } from "commander";
|
|
321
|
-
import
|
|
757
|
+
import chalk6 from "chalk";
|
|
322
758
|
|
|
323
759
|
// src/utils/spinner.ts
|
|
324
760
|
import ora from "ora";
|
|
@@ -334,10 +770,10 @@ import { VonPayCheckout } from "@vonpay/checkout-node";
|
|
|
334
770
|
|
|
335
771
|
// src/app-info.ts
|
|
336
772
|
import { readFileSync as readFileSync3 } from "fs";
|
|
337
|
-
import { dirname, join as
|
|
773
|
+
import { dirname, join as join3 } from "path";
|
|
338
774
|
import { fileURLToPath } from "url";
|
|
339
775
|
var pkg = JSON.parse(
|
|
340
|
-
readFileSync3(
|
|
776
|
+
readFileSync3(join3(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf-8")
|
|
341
777
|
);
|
|
342
778
|
var CLI_VERSION = pkg.version;
|
|
343
779
|
var CLI_APP_INFO = { name: "vonpay-cli", version: CLI_VERSION };
|
|
@@ -347,12 +783,18 @@ var SILENT_ERROR_REPORTER = () => {
|
|
|
347
783
|
// src/utils/base-url.ts
|
|
348
784
|
import { keyDestinationProblem as sdkKeyDestinationProblem } from "@vonpay/checkout-node";
|
|
349
785
|
var ALLOW_CUSTOM_BASE_URL_ENV = "VONPAY_ALLOW_CUSTOM_BASE_URL";
|
|
350
|
-
|
|
351
|
-
|
|
786
|
+
var ALLOW_CUSTOM_BASE_URL_FLAG = "--allow-custom-base-url";
|
|
787
|
+
var flagGiven = false;
|
|
788
|
+
function setAllowCustomBaseUrlFlag(given) {
|
|
789
|
+
flagGiven = given === true;
|
|
790
|
+
}
|
|
791
|
+
function customBaseUrlAllowed(env = process.env, flag = flagGiven) {
|
|
792
|
+
return env[ALLOW_CUSTOM_BASE_URL_ENV] === "1" && flag === true;
|
|
352
793
|
}
|
|
353
794
|
var UNSAFE_URL_CHARS = /[\s\x00-\x1f\x7f\\]/;
|
|
354
795
|
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.";
|
|
355
|
-
|
|
796
|
+
var CLI_OPT_IN = `For a proxy you run yourself, set ${ALLOW_CUSTOM_BASE_URL_ENV}=1 AND pass ${ALLOW_CUSTOM_BASE_URL_FLAG} on the command; either alone is not enough.`;
|
|
797
|
+
function keyDestinationProblem(apiKey, baseUrl, env = process.env, flag = flagGiven) {
|
|
356
798
|
if (baseUrl === void 0) return null;
|
|
357
799
|
if (UNSAFE_URL_CHARS.test(baseUrl)) {
|
|
358
800
|
return `Refusing to use the base URL: it contains spaces, control characters or backslashes. ${WHERE}`;
|
|
@@ -366,11 +808,11 @@ function keyDestinationProblem(apiKey, baseUrl, env = process.env) {
|
|
|
366
808
|
if (url.username !== "" || url.password !== "") {
|
|
367
809
|
return `Refusing to use the base URL: it contains a user name or password, which would be sent with every request. ${WHERE}`;
|
|
368
810
|
}
|
|
369
|
-
const problem = sdkKeyDestinationProblem(apiKey, baseUrl, { allowCustom: customBaseUrlAllowed(env) });
|
|
370
|
-
return problem === null ? null : `${problem} ${WHERE}`;
|
|
811
|
+
const problem = sdkKeyDestinationProblem(apiKey, baseUrl, { allowCustom: customBaseUrlAllowed(env, flag) });
|
|
812
|
+
return problem === null ? null : `${problem} ${WHERE} ${CLI_OPT_IN}`;
|
|
371
813
|
}
|
|
372
|
-
function allowCustomBaseUrl(env = process.env) {
|
|
373
|
-
return customBaseUrlAllowed(env);
|
|
814
|
+
function allowCustomBaseUrl(env = process.env, flag = flagGiven) {
|
|
815
|
+
return customBaseUrlAllowed(env, flag);
|
|
374
816
|
}
|
|
375
817
|
|
|
376
818
|
// src/utils/errors.ts
|
|
@@ -404,10 +846,13 @@ function scrubSecrets(s) {
|
|
|
404
846
|
}
|
|
405
847
|
|
|
406
848
|
// src/utils/sanitize.ts
|
|
849
|
+
import { stripHiddenCharacters as stripHiddenCharacters2 } from "@vonpay/checkout-node";
|
|
407
850
|
var ANSI_PATTERN = /\x1B\[[0-9;?]*[A-Za-z]/g;
|
|
408
|
-
var
|
|
851
|
+
var C1_CSI_PATTERN = /\u{9B}[0-9;?]*[A-Za-z]/gu;
|
|
852
|
+
var CONTROL_PATTERN = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\u{80}-\u{9F}]/gu;
|
|
853
|
+
var LINE_SEPARATOR_PATTERN = /[\u{2028}\u{2029}]/gu;
|
|
409
854
|
function stripAnsi(s) {
|
|
410
|
-
return s.replace(ANSI_PATTERN, "").replace(
|
|
855
|
+
return stripHiddenCharacters2(s.replace(ANSI_PATTERN, "").replace(C1_CSI_PATTERN, "").replace(CONTROL_PATTERN, ""));
|
|
411
856
|
}
|
|
412
857
|
function truncate(s, max) {
|
|
413
858
|
if (s.length <= max) return s;
|
|
@@ -417,7 +862,7 @@ function safeDisplay(s, max = 200) {
|
|
|
417
862
|
return truncate(stripAnsi(s), max);
|
|
418
863
|
}
|
|
419
864
|
function safeInline(s, max = 200) {
|
|
420
|
-
return safeDisplay(s.replace(/[\r\n]+/g, " "), max);
|
|
865
|
+
return safeDisplay(s.replace(/[\r\n]+/g, " ").replace(LINE_SEPARATOR_PATTERN, " "), max);
|
|
421
866
|
}
|
|
422
867
|
|
|
423
868
|
// src/utils/errors.ts
|
|
@@ -425,6 +870,11 @@ var CLI_READ_BACK = "From this CLI: `vonpay checkout payment-intents get <id>` (
|
|
|
425
870
|
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.";
|
|
426
871
|
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.`;
|
|
427
872
|
var str = (v) => v === void 0 || v === null ? null : String(v);
|
|
873
|
+
var oneLine = (s) => safeInline(s, 2e3);
|
|
874
|
+
function paramFlagHint(param) {
|
|
875
|
+
const flag = param.startsWith("metadata") ? "--metadata" : `--${param.replace(/\[(gte|lt)\]$/, "-$1").replace(/_/g, "-")}`;
|
|
876
|
+
return /^--[a-z-]+$/.test(flag) ? ` (the ${flag} flag)` : "";
|
|
877
|
+
}
|
|
428
878
|
var FIELD_PRINTERS = {
|
|
429
879
|
code: (e) => `Code: ${str(e.code)}`,
|
|
430
880
|
fix: (e) => `Fix: ${str(e.fix)}`,
|
|
@@ -464,8 +914,8 @@ var FIELD_PRINTERS = {
|
|
|
464
914
|
usedMinor: (e) => typeof e.usedMinor === "number" ? `Used in the last 24 hours: ${safeInline(String(e.usedMinor))} (minor units)` : null,
|
|
465
915
|
limitCurrency: (e) => e.limitCurrency ? `Key limit currency: ${safeInline(String(e.limitCurrency))}` : null,
|
|
466
916
|
refusedFields: (e) => Array.isArray(e.refusedFields) && e.refusedFields.length > 0 ? `Fields this key may not send: ${e.refusedFields.map((f) => safeInline(String(f), 128)).join(", ")}` : null,
|
|
467
|
-
// A refused payment-only settle (mirror_settle_only_*;
|
|
468
|
-
//
|
|
917
|
+
// A refused payment-only settle (mirror_settle_only_*; served in production
|
|
918
|
+
// since 2026-10-27): the charge asked for and the store order's
|
|
469
919
|
// facts. Validated by the SDK, printed through safeInline all the same.
|
|
470
920
|
chargeAmount: (e) => typeof e.chargeAmount === "number" ? `Charge asked for: ${safeInline(String(e.chargeAmount))} (minor units)` : null,
|
|
471
921
|
chargeCurrency: (e) => e.chargeCurrency ? `Charge currency: ${safeInline(String(e.chargeCurrency))}` : null,
|
|
@@ -474,6 +924,34 @@ var FIELD_PRINTERS = {
|
|
|
474
924
|
orderCurrency: (e) => e.orderCurrency ? `Store order currency: ${safeInline(String(e.orderCurrency))}` : null,
|
|
475
925
|
orderStatus: (e) => e.orderStatus ? `Store order status: ${safeInline(String(e.orderStatus))}` : null,
|
|
476
926
|
orderPaymentStatus: (e) => e.orderPaymentStatus ? `Store order payment status: ${safeInline(String(e.orderPaymentStatus))}` : null,
|
|
927
|
+
// A refused subscription change (served in production since 2026-10-30). Validated by
|
|
928
|
+
// the SDK, printed through safeInline all the same.
|
|
929
|
+
subscriptionStatus: (e) => e.subscriptionStatus ? `Subscription status: ${safeInline(String(e.subscriptionStatus))}` : null,
|
|
930
|
+
refusalReason: (e) => e.refusalReason ? `Refused because: ${safeInline(String(e.refusalReason))}` : null,
|
|
931
|
+
minDate: (e) => e.minDate ? `Earliest date allowed: ${safeInline(String(e.minDate))}` : null,
|
|
932
|
+
maxDate: (e) => e.maxDate ? `Latest date allowed: ${safeInline(String(e.maxDate))}` : null,
|
|
933
|
+
currentAmount: (e) => typeof e.currentAmount === "number" ? `Current highest renewal charge: ${safeInline(String(e.currentAmount))} (minor units)` : null,
|
|
934
|
+
maxAmount: (e) => typeof e.maxAmount === "number" ? `Most this key may make a renewal charge: ${safeInline(String(e.maxAmount))} (minor units)` : e.maxAmount === null ? "Most this key may make a renewal charge: none \u2014 this key may only lower the price" : null,
|
|
935
|
+
amountCurrency: (e) => e.amountCurrency ? `Renewal charge currency: ${safeInline(String(e.amountCurrency))}` : null,
|
|
936
|
+
pendingKind: (e) => e.pendingKind ? `Schedule change waiting for its customer notice: ${safeInline(String(e.pendingKind))}` : null,
|
|
937
|
+
nextChangeAllowedOn: (e) => e.nextChangeAllowedOn ? `Customer's next schedule change allowed from: ${safeInline(String(e.nextChangeAllowedOn))}` : null,
|
|
938
|
+
// A charge the payment provider refused before any money moved (served in
|
|
939
|
+
// production since 2026-10-30): the failed payment intent that records
|
|
940
|
+
// the attempt. The SDK has checked it is a vpi_* id; printed through
|
|
941
|
+
// safeInline all the same. The next step depends on the code: a card the
|
|
942
|
+
// processor is still storing (payment_method_unvaulted) is retried as is
|
|
943
|
+
// after a short wait; every other refusal needs the request or card fixed.
|
|
944
|
+
// Both under a NEW key: the refused record does not hold the key, and the
|
|
945
|
+
// processor may refuse a resend under the same one.
|
|
946
|
+
// A refused payment list / summary filter (`payment-intents list` / `summary`): the ONE
|
|
947
|
+
// query parameter at fault, a plain parameter name the SDK has checked. The flag that sets it
|
|
948
|
+
// is named alongside, so the fix is obvious.
|
|
949
|
+
param: (e) => e.param ? `Refused parameter: ${safeInline(String(e.param))}${paramFlagHint(String(e.param))}` : null,
|
|
950
|
+
// A payment report too large to answer (report_too_large): which limit refused it. The SDK
|
|
951
|
+
// validates both as positive whole numbers.
|
|
952
|
+
maxGroups: (e) => typeof e.maxGroups === "number" ? `Most groups one summary returns: ${e.maxGroups} \u2014 group by fewer dimensions or a coarser time unit, or add filters` : null,
|
|
953
|
+
timeoutMs: (e) => typeof e.timeoutMs === "number" ? `Time limit exceeded: ${e.timeoutMs} ms \u2014 use a shorter window or add filters (retrying unchanged fails the same way)` : null,
|
|
954
|
+
paymentIntentId: (e) => e.paymentIntentId ? `Refused attempt recorded as: ${safeInline(String(e.paymentIntentId))} (status failed, nothing was charged; ` + (e.code === "payment_method_unvaulted" ? "wait a few seconds, then send the same charge again with a NEW --idempotency-key)" : "fix the request or the card, then charge with a NEW --idempotency-key)") : null,
|
|
477
955
|
// On a withheld retry the SDK sets this to RETRY_WITHHELD_HELP.llmHint — the
|
|
478
956
|
// one wording for every surface — so it is printed as is.
|
|
479
957
|
llmHint: (e) => e.llmHint ? `Hint: ${str(e.llmHint)}` : null,
|
|
@@ -488,12 +966,12 @@ function errorDetailLines(err) {
|
|
|
488
966
|
const lines = [];
|
|
489
967
|
for (const print of Object.values(FIELD_PRINTERS)) {
|
|
490
968
|
const line = print(source);
|
|
491
|
-
if (line !== null) lines.push(` ${scrubSecrets(line)}`);
|
|
969
|
+
if (line !== null) lines.push(` ${scrubSecrets(oneLine(line))}`);
|
|
492
970
|
}
|
|
493
971
|
return lines;
|
|
494
972
|
}
|
|
495
973
|
function scrubDeep(value) {
|
|
496
|
-
if (typeof value === "string") return scrubSecrets(value);
|
|
974
|
+
if (typeof value === "string") return scrubSecrets(oneLine(value));
|
|
497
975
|
if (Array.isArray(value)) return value.map(scrubDeep);
|
|
498
976
|
if (value !== null && typeof value === "object") {
|
|
499
977
|
const out = {};
|
|
@@ -513,23 +991,25 @@ function errorJson(err) {
|
|
|
513
991
|
return scrubDeep(out);
|
|
514
992
|
}
|
|
515
993
|
const message = err instanceof Error ? err.message : String(err);
|
|
516
|
-
return { error: scrubSecrets(message), code: null };
|
|
994
|
+
return { error: scrubSecrets(oneLine(message)), code: null };
|
|
517
995
|
}
|
|
518
996
|
function exitWithCliError(message, text) {
|
|
997
|
+
auditNote({ outcome: "refused_local", code: "local_refusal" }, true);
|
|
519
998
|
return emitCliError({ error: scrubSecrets(message), code: null }, text);
|
|
520
999
|
}
|
|
521
1000
|
function handleError(err) {
|
|
1001
|
+
auditError(err);
|
|
522
1002
|
if (jsonErrorsEnabled()) {
|
|
523
1003
|
printJsonError(errorJson(err));
|
|
524
1004
|
process.exit(1);
|
|
525
1005
|
}
|
|
526
1006
|
if (err instanceof VonPayError) {
|
|
527
|
-
console.error(chalk4.red(`Error: ${scrubSecrets(err.message)}`));
|
|
1007
|
+
console.error(chalk4.red(`Error: ${scrubSecrets(oneLine(err.message))}`));
|
|
528
1008
|
for (const line of errorDetailLines(err)) console.error(chalk4.dim(line));
|
|
529
1009
|
process.exit(1);
|
|
530
1010
|
}
|
|
531
1011
|
const message = err instanceof Error ? err.message : String(err);
|
|
532
|
-
console.error(chalk4.red(`Error: ${scrubSecrets(message)}`));
|
|
1012
|
+
console.error(chalk4.red(`Error: ${scrubSecrets(oneLine(message))}`));
|
|
533
1013
|
process.exit(1);
|
|
534
1014
|
}
|
|
535
1015
|
|
|
@@ -554,11 +1034,30 @@ function apiHostDetail() {
|
|
|
554
1034
|
}
|
|
555
1035
|
return origin === "https://checkout.vonpay.com" ? [] : [["API host", origin]];
|
|
556
1036
|
}
|
|
1037
|
+
var MONEY_METHODS = {
|
|
1038
|
+
paymentIntents: ["create", "capture", "void"],
|
|
1039
|
+
refunds: ["create"],
|
|
1040
|
+
sessions: ["create"]
|
|
1041
|
+
};
|
|
1042
|
+
function withAuditIntent(client) {
|
|
1043
|
+
for (const [resource, methods] of Object.entries(MONEY_METHODS)) {
|
|
1044
|
+
const target = client[resource];
|
|
1045
|
+
for (const method of methods) {
|
|
1046
|
+
const original = target?.[method];
|
|
1047
|
+
if (typeof original !== "function") continue;
|
|
1048
|
+
target[method] = (...args) => {
|
|
1049
|
+
auditIntent();
|
|
1050
|
+
return original.apply(target, args);
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
return client;
|
|
1055
|
+
}
|
|
557
1056
|
function makeCheckoutClient() {
|
|
558
1057
|
const apiKey = requireApiKey();
|
|
559
1058
|
const baseUrl = getBaseUrl();
|
|
560
1059
|
refuseUnsafeLiveDestination(apiKey);
|
|
561
|
-
return new VonPayCheckout({
|
|
1060
|
+
return withAuditIntent(new VonPayCheckout({
|
|
562
1061
|
apiKey,
|
|
563
1062
|
...baseUrl ? { baseUrl } : {},
|
|
564
1063
|
// The SDK applies the same host rule at construction; the opt-in is the
|
|
@@ -568,7 +1067,86 @@ function makeCheckoutClient() {
|
|
|
568
1067
|
// The CLI prints every error itself (handleError / --json). Without this the SDK's fallback
|
|
569
1068
|
// reporter adds its own `[vonpay] …` line to stderr on EVERY API error, live key included.
|
|
570
1069
|
errorReporter: SILENT_ERROR_REPORTER
|
|
571
|
-
});
|
|
1070
|
+
}));
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// src/utils/live-key.ts
|
|
1074
|
+
import { createInterface as createInterface2 } from "readline";
|
|
1075
|
+
import chalk5 from "chalk";
|
|
1076
|
+
import { Option as Option2 } from "commander";
|
|
1077
|
+
import { isLiveKey as isLiveKey2 } from "@vonpay/checkout-node";
|
|
1078
|
+
var CONFIRM_WORD = "confirm";
|
|
1079
|
+
var terminal = {
|
|
1080
|
+
/**
|
|
1081
|
+
* Both stdin and stdout are a terminal. A command run by a script, a CI job or
|
|
1082
|
+
* an AI agent's shell tool has at least one of them piped.
|
|
1083
|
+
*/
|
|
1084
|
+
isInteractive() {
|
|
1085
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
1086
|
+
},
|
|
1087
|
+
/** Ask one line on the terminal. End of input or Ctrl-C answers "" (cancel). */
|
|
1088
|
+
ask(prompt) {
|
|
1089
|
+
return new Promise((resolve3) => {
|
|
1090
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
1091
|
+
let answered = false;
|
|
1092
|
+
rl.on("SIGINT", () => rl.close());
|
|
1093
|
+
rl.on("close", () => {
|
|
1094
|
+
if (!answered) resolve3("");
|
|
1095
|
+
});
|
|
1096
|
+
rl.question(prompt, (answer) => {
|
|
1097
|
+
answered = true;
|
|
1098
|
+
rl.close();
|
|
1099
|
+
resolve3(answer);
|
|
1100
|
+
});
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
function keyFingerprint2(key) {
|
|
1105
|
+
const m = key.match(/^(vp_sk_live_)([A-Za-z0-9_-]{0,4})/);
|
|
1106
|
+
return m ? `${m[1]}${m[2]}\u2026` : "a key that is not a sandbox key";
|
|
1107
|
+
}
|
|
1108
|
+
async function confirmLiveAction(key, summary, opts = {}) {
|
|
1109
|
+
if (!requiresLiveConfirmation(key)) return;
|
|
1110
|
+
if (!terminal.isInteractive()) {
|
|
1111
|
+
const message = `Refusing to ${summary.action} with a live key: no person is at an interactive terminal to confirm it. A live-key action must be confirmed by a person typing at a terminal (stdin and stdout both a terminal); scripts, CI jobs and AI agents cannot confirm it, and --confirm-live is no longer enough. Use a sandbox key (vp_sk_test_\u2026) for automation, or run the command yourself in a terminal. Nothing was sent.`;
|
|
1112
|
+
auditNote({ outcome: "withheld", code: "no_terminal" });
|
|
1113
|
+
exitWithCliError(message, chalk5.red(message));
|
|
1114
|
+
}
|
|
1115
|
+
if (opts.confirmLive) {
|
|
1116
|
+
console.warn(
|
|
1117
|
+
chalk5.yellow(
|
|
1118
|
+
"Note: --confirm-live no longer confirms a live action by itself. A person confirms it at the terminal."
|
|
1119
|
+
)
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
const lines = [
|
|
1123
|
+
chalk5.bold.red(`You are about to ${summary.action} with a LIVE key (${keyFingerprint2(key)}).`),
|
|
1124
|
+
// At least one space after the label, however long it is.
|
|
1125
|
+
...(summary.details ?? []).map(([label, value]) => ` ${`${label}:`.padEnd(Math.max(17, label.length + 2))}${safeInline(value)}`),
|
|
1126
|
+
chalk5.yellow(summary.consequence ?? "This moves real money and cannot be undone from here.")
|
|
1127
|
+
];
|
|
1128
|
+
for (const line of lines) console.log(line);
|
|
1129
|
+
const answer = await terminal.ask(`Type "${CONFIRM_WORD}" to proceed, anything else to cancel: `);
|
|
1130
|
+
if (answer.trim() !== CONFIRM_WORD) {
|
|
1131
|
+
auditNote({ outcome: "declined", code: "declined" });
|
|
1132
|
+
const message = `Cancelled: you did not type "${CONFIRM_WORD}". Nothing was sent.`;
|
|
1133
|
+
exitWithCliError(message, chalk5.red(message));
|
|
1134
|
+
}
|
|
1135
|
+
auditNote({ approved: true });
|
|
1136
|
+
}
|
|
1137
|
+
function retiredConfirmLiveOption() {
|
|
1138
|
+
return new Option2(
|
|
1139
|
+
"--confirm-live",
|
|
1140
|
+
"No longer confirms anything: a live-key action is confirmed by a person at the terminal"
|
|
1141
|
+
).hideHelp();
|
|
1142
|
+
}
|
|
1143
|
+
function warnLiveSecretFlag(key, flag) {
|
|
1144
|
+
if (!requiresLiveConfirmation(key)) return;
|
|
1145
|
+
console.warn(
|
|
1146
|
+
chalk5.yellow(
|
|
1147
|
+
`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.`
|
|
1148
|
+
)
|
|
1149
|
+
);
|
|
572
1150
|
}
|
|
573
1151
|
|
|
574
1152
|
// src/utils/amount.ts
|
|
@@ -612,18 +1190,18 @@ function sessionJson(session, showSecrets) {
|
|
|
612
1190
|
// src/commands/sessions.ts
|
|
613
1191
|
var makeClient = makeCheckoutClient;
|
|
614
1192
|
function sessionLines(session) {
|
|
615
|
-
const statusColor = session.status === "succeeded" ?
|
|
1193
|
+
const statusColor = session.status === "succeeded" ? chalk6.green : session.status === "failed" ? chalk6.red : session.status === "expired" ? chalk6.gray : chalk6.yellow;
|
|
616
1194
|
const lines = [
|
|
617
1195
|
` ID: ${session.id}`,
|
|
618
1196
|
` Status: ${statusColor(session.status)}`,
|
|
619
1197
|
` Amount: ${formatMinorUnits(session.amount, session.currency)}`
|
|
620
1198
|
];
|
|
621
1199
|
if (session.paymentStatus) {
|
|
622
|
-
const paymentColor = session.paymentStatus === "paid" ?
|
|
1200
|
+
const paymentColor = session.paymentStatus === "paid" ? chalk6.green : session.paymentStatus === "held" ? chalk6.yellow : chalk6.gray;
|
|
623
1201
|
lines.push(` Payment: ${paymentColor(session.paymentStatus)}`);
|
|
624
1202
|
if (session.paymentStatus === "held") {
|
|
625
1203
|
lines.push(
|
|
626
|
-
|
|
1204
|
+
chalk6.yellow(
|
|
627
1205
|
" Held, not taken: capture it to take the money, or void it. Do not ship on a hold alone."
|
|
628
1206
|
)
|
|
629
1207
|
);
|
|
@@ -642,7 +1220,7 @@ function sessionLines(session) {
|
|
|
642
1220
|
lines.push(` Payments: ${session.vp_tx_count}`);
|
|
643
1221
|
if (session.vp_tx_count >= 2) {
|
|
644
1222
|
lines.push(
|
|
645
|
-
|
|
1223
|
+
chalk6.red(
|
|
646
1224
|
` \u26A0 Charged ${session.vp_tx_count} times: there is no single refund id. Have a person check the payments before refunding or fulfilling.`
|
|
647
1225
|
)
|
|
648
1226
|
);
|
|
@@ -652,12 +1230,44 @@ function sessionLines(session) {
|
|
|
652
1230
|
lines.push(` Expires: ${session.expiresAt}`);
|
|
653
1231
|
return lines;
|
|
654
1232
|
}
|
|
1233
|
+
function redirectHost(url) {
|
|
1234
|
+
if (url === void 0) return "none given (the account default)";
|
|
1235
|
+
let parsed;
|
|
1236
|
+
try {
|
|
1237
|
+
parsed = new URL(url);
|
|
1238
|
+
} catch {
|
|
1239
|
+
return "[not a valid address]";
|
|
1240
|
+
}
|
|
1241
|
+
const host = parsed.hostname;
|
|
1242
|
+
if (host === "") return "[not a web address]";
|
|
1243
|
+
if (host.length > 253) return "[host too long]";
|
|
1244
|
+
if (!/^([a-z0-9-]{1,63}\.)*[a-z0-9-]{1,63}$/.test(host) && !/^\[[0-9a-f:.]+\]$/.test(host)) return "[unrecognised host withheld]";
|
|
1245
|
+
return parsed.port ? `${host}:${parsed.port}` : host;
|
|
1246
|
+
}
|
|
655
1247
|
function sessionsCommand() {
|
|
656
1248
|
const sessions = new Command4("sessions").description(
|
|
657
1249
|
"Create and manage checkout sessions"
|
|
658
1250
|
);
|
|
659
|
-
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 (the session's client secret masked, see --show-secrets)").option("--show-secrets", SHOW_SECRETS_HELP).action(async (opts) => {
|
|
1251
|
+
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").addOption(retiredConfirmLiveOption()).option("--json", "Output raw JSON (the session's client secret masked, see --show-secrets)").option("--show-secrets", SHOW_SECRETS_HELP).action(async (opts) => {
|
|
660
1252
|
const amount = minorUnitsOrExit(String(opts.amount), "--amount");
|
|
1253
|
+
if (!opts.dryRun) {
|
|
1254
|
+
const apiKey = requireApiKey();
|
|
1255
|
+
refuseUnsafeLiveDestination(apiKey);
|
|
1256
|
+
await confirmLiveAction(
|
|
1257
|
+
apiKey,
|
|
1258
|
+
{
|
|
1259
|
+
action: "create a checkout session (a payment page a buyer can pay)",
|
|
1260
|
+
details: [
|
|
1261
|
+
...apiHostDetail(),
|
|
1262
|
+
["Amount", formatMinorUnits(amount, String(opts.currency).toUpperCase())],
|
|
1263
|
+
["After paying, to", redirectHost(opts.successUrl)],
|
|
1264
|
+
["If cancelled, to", redirectHost(opts.cancelUrl)]
|
|
1265
|
+
],
|
|
1266
|
+
consequence: "A buyer can pay real money on this page until it expires."
|
|
1267
|
+
},
|
|
1268
|
+
opts
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
661
1271
|
const client = makeClient();
|
|
662
1272
|
const spinner = startSpinner("Creating session...");
|
|
663
1273
|
try {
|
|
@@ -676,14 +1286,14 @@ function sessionsCommand() {
|
|
|
676
1286
|
if (opts.json) {
|
|
677
1287
|
console.log(JSON.stringify(result, null, 2));
|
|
678
1288
|
} else if (result.valid) {
|
|
679
|
-
console.log(
|
|
1289
|
+
console.log(chalk6.green("\u2713 Valid request"));
|
|
680
1290
|
if (result.warnings.length > 0) {
|
|
681
1291
|
for (const w of result.warnings) {
|
|
682
|
-
console.log(
|
|
1292
|
+
console.log(chalk6.yellow(` \u26A0 ${w}`));
|
|
683
1293
|
}
|
|
684
1294
|
}
|
|
685
1295
|
} else {
|
|
686
|
-
console.log(
|
|
1296
|
+
console.log(chalk6.red("\u2717 Invalid request"));
|
|
687
1297
|
}
|
|
688
1298
|
return;
|
|
689
1299
|
}
|
|
@@ -695,9 +1305,9 @@ function sessionsCommand() {
|
|
|
695
1305
|
if (opts.json) {
|
|
696
1306
|
console.log(sessionJson(session, opts.showSecrets));
|
|
697
1307
|
} else {
|
|
698
|
-
console.log(
|
|
1308
|
+
console.log(chalk6.green("\u2713 Session created"));
|
|
699
1309
|
console.log(` ID: ${session.id}`);
|
|
700
|
-
console.log(` URL: ${
|
|
1310
|
+
console.log(` URL: ${chalk6.cyan(session.checkoutUrl)}`);
|
|
701
1311
|
console.log(` Expires: ${session.expiresAt}`);
|
|
702
1312
|
}
|
|
703
1313
|
} catch (err) {
|
|
@@ -726,28 +1336,37 @@ function sessionsCommand() {
|
|
|
726
1336
|
|
|
727
1337
|
// src/commands/payment-intents.ts
|
|
728
1338
|
import { Command as Command5 } from "commander";
|
|
729
|
-
import
|
|
730
|
-
import { formatMinorUnits as
|
|
1339
|
+
import chalk9 from "chalk";
|
|
1340
|
+
import { formatMinorUnits as formatMinorUnits3 } from "@vonpay/checkout-node";
|
|
731
1341
|
|
|
732
1342
|
// src/utils/idempotency.ts
|
|
733
1343
|
import { randomUUID } from "crypto";
|
|
734
|
-
import
|
|
1344
|
+
import chalk7 from "chalk";
|
|
735
1345
|
function resolveIdempotencyKey(flag) {
|
|
736
1346
|
const given = flag?.trim();
|
|
737
1347
|
if (given) return { key: given, generated: false };
|
|
738
1348
|
return { key: `cli-${randomUUID()}`, generated: true };
|
|
739
1349
|
}
|
|
740
|
-
function idempotencyKeyFor(flag, what) {
|
|
1350
|
+
function idempotencyKeyFor(flag, what, apiKey) {
|
|
741
1351
|
const { key, generated } = resolveIdempotencyKey(flag);
|
|
1352
|
+
if (generated && apiKey !== void 0 && requiresLiveConfirmation(apiKey)) {
|
|
1353
|
+
const message = `Refusing to send this ${what} with a live key without --idempotency-key. Choose one key for this ${what} (for example your order id plus the action, such as "order_1042:${what}") and pass the SAME key if you ever run the command again: a new key is a new action and can move money twice. Nothing was sent.`;
|
|
1354
|
+
auditNote({ outcome: "refused_local", code: "idempotency_key_required" });
|
|
1355
|
+
exitWithCliError(message, chalk7.red(message));
|
|
1356
|
+
}
|
|
742
1357
|
if (generated) {
|
|
743
1358
|
console.error(
|
|
744
|
-
|
|
1359
|
+
chalk7.dim(
|
|
745
1360
|
`Idempotency key: ${key} (to retry this same ${what}, re-run with --idempotency-key ${key})`
|
|
746
1361
|
)
|
|
747
1362
|
);
|
|
748
1363
|
}
|
|
749
1364
|
return key;
|
|
750
1365
|
}
|
|
1366
|
+
var DISPLAYABLE_KEY = /^[A-Za-z0-9_:.-]{1,64}$/;
|
|
1367
|
+
function displayIdempotencyKey(key) {
|
|
1368
|
+
return DISPLAYABLE_KEY.test(key) ? key : "[custom key withheld]";
|
|
1369
|
+
}
|
|
751
1370
|
|
|
752
1371
|
// src/utils/metadata.ts
|
|
753
1372
|
var MAX_LEN = 256;
|
|
@@ -788,80 +1407,268 @@ function parseMetadata(pairs) {
|
|
|
788
1407
|
}
|
|
789
1408
|
return out;
|
|
790
1409
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
var terminal = {
|
|
799
|
-
/**
|
|
800
|
-
* Both stdin and stdout are a terminal. A command run by a script, a CI job or
|
|
801
|
-
* an AI agent's shell tool has at least one of them piped.
|
|
802
|
-
*/
|
|
803
|
-
isInteractive() {
|
|
804
|
-
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
805
|
-
},
|
|
806
|
-
/** Ask one line on the terminal. End of input or Ctrl-C answers "" (cancel). */
|
|
807
|
-
ask(prompt) {
|
|
808
|
-
return new Promise((resolve2) => {
|
|
809
|
-
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
810
|
-
let answered = false;
|
|
811
|
-
rl.on("SIGINT", () => rl.close());
|
|
812
|
-
rl.on("close", () => {
|
|
813
|
-
if (!answered) resolve2("");
|
|
814
|
-
});
|
|
815
|
-
rl.question(prompt, (answer) => {
|
|
816
|
-
answered = true;
|
|
817
|
-
rl.close();
|
|
818
|
-
resolve2(answer);
|
|
819
|
-
});
|
|
820
|
-
});
|
|
1410
|
+
var METADATA_FILTER_KEY = /^[A-Za-z0-9_.:-]{1,64}$/;
|
|
1411
|
+
var METADATA_FILTER_VALUE_MAX = 500;
|
|
1412
|
+
function assertMetadataFilterKey(key, flag) {
|
|
1413
|
+
if (!METADATA_FILTER_KEY.test(key)) {
|
|
1414
|
+
throw new Error(
|
|
1415
|
+
`${flag}: the metadata key must be 1-64 letters, digits, _ . : or - (the API's pattern ^[A-Za-z0-9_.:-]{1,64}$); got ${JSON.stringify(key.slice(0, 80))}.`
|
|
1416
|
+
);
|
|
821
1417
|
}
|
|
822
|
-
|
|
823
|
-
function keyFingerprint(key) {
|
|
824
|
-
const m = key.match(/^(vp_sk_live_)([A-Za-z0-9_-]{0,4})/);
|
|
825
|
-
return m ? `${m[1]}${m[2]}\u2026` : "a key that is not a sandbox key";
|
|
1418
|
+
return key;
|
|
826
1419
|
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
if (
|
|
830
|
-
|
|
831
|
-
|
|
1420
|
+
function parseMetadataFilter(raw, flag) {
|
|
1421
|
+
const idx = raw.indexOf("=");
|
|
1422
|
+
if (idx === -1) throw new Error(`${flag}: expected key=value (ONE tag); got ${JSON.stringify(raw.slice(0, 80))}.`);
|
|
1423
|
+
const key = assertMetadataFilterKey(raw.slice(0, idx), flag);
|
|
1424
|
+
const value = raw.slice(idx + 1);
|
|
1425
|
+
if (value.length < 1 || value.length > METADATA_FILTER_VALUE_MAX) {
|
|
1426
|
+
throw new Error(`${flag}: the value must be 1-${METADATA_FILTER_VALUE_MAX} characters; got ${value.length}.`);
|
|
832
1427
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
1428
|
+
return { [key]: value };
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// src/commands/payment-reports.ts
|
|
1432
|
+
import chalk8 from "chalk";
|
|
1433
|
+
import { formatMinorUnits as formatMinorUnits2 } from "@vonpay/checkout-node";
|
|
1434
|
+
|
|
1435
|
+
// src/utils/list-flag.ts
|
|
1436
|
+
function parseListFlag(raw, flag, options = {}) {
|
|
1437
|
+
const items = raw.split(",").map((s) => options.lowercase ? s.trim().toLowerCase() : s.trim());
|
|
1438
|
+
if (items.length === 0 || items.some((s) => s.length === 0)) {
|
|
1439
|
+
throw new Error(`${flag} must be a comma-separated list with no empty items; got ${JSON.stringify(raw)}.`);
|
|
1440
|
+
}
|
|
1441
|
+
return [...new Set(items)];
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// src/commands/payment-reports.ts
|
|
1445
|
+
function buildFilters(opts) {
|
|
1446
|
+
const filters = {};
|
|
1447
|
+
if (opts.createdGte !== void 0) filters.createdGte = opts.createdGte;
|
|
1448
|
+
if (opts.createdLt !== void 0) filters.createdLt = opts.createdLt;
|
|
1449
|
+
if (opts.outcome !== void 0) filters.outcome = parseListFlag(opts.outcome, "--outcome", { lowercase: true });
|
|
1450
|
+
if (opts.store !== void 0) filters.store = opts.store;
|
|
1451
|
+
if (opts.metadata !== void 0) filters.metadata = parseMetadataFilter(opts.metadata, "--metadata");
|
|
1452
|
+
if (opts.cardBrand !== void 0) {
|
|
1453
|
+
filters.cardBrand = parseListFlag(opts.cardBrand, "--card-brand", { lowercase: true });
|
|
1454
|
+
}
|
|
1455
|
+
if (opts.currency !== void 0) filters.currency = opts.currency.toUpperCase();
|
|
1456
|
+
if (opts.initiator !== void 0) {
|
|
1457
|
+
const initiator = opts.initiator.toLowerCase();
|
|
1458
|
+
if (initiator !== "customer" && initiator !== "merchant") {
|
|
1459
|
+
throw new Error(`--initiator must be customer or merchant; got ${JSON.stringify(opts.initiator)}.`);
|
|
1460
|
+
}
|
|
1461
|
+
filters.initiator = initiator;
|
|
1462
|
+
}
|
|
1463
|
+
if (opts.declineCode !== void 0) {
|
|
1464
|
+
filters.declineCode = parseListFlag(opts.declineCode, "--decline-code", { lowercase: true });
|
|
1465
|
+
}
|
|
1466
|
+
return filters;
|
|
1467
|
+
}
|
|
1468
|
+
function wholeNumber2(raw, flag) {
|
|
1469
|
+
const parsed = parseMinorUnits(raw, flag);
|
|
1470
|
+
if (!parsed.ok || parsed.value < 1) throw new Error(`${flag} must be a whole number of at least 1; got ${JSON.stringify(raw)}.`);
|
|
1471
|
+
return parsed.value;
|
|
1472
|
+
}
|
|
1473
|
+
var PAYMENT_LIST_JSON_FIELDS = [
|
|
1474
|
+
"id",
|
|
1475
|
+
"status",
|
|
1476
|
+
"outcome",
|
|
1477
|
+
"failureClass",
|
|
1478
|
+
"amount",
|
|
1479
|
+
"currency",
|
|
1480
|
+
"captureMethod",
|
|
1481
|
+
"createdAt",
|
|
1482
|
+
"updatedAt",
|
|
1483
|
+
"store",
|
|
1484
|
+
"card",
|
|
1485
|
+
"declineCode",
|
|
1486
|
+
"action",
|
|
1487
|
+
"ruleCode",
|
|
1488
|
+
"networkAdvice"
|
|
1489
|
+
];
|
|
1490
|
+
function listItemJson(pi, includeMetadata) {
|
|
1491
|
+
if (includeMetadata) return pi;
|
|
1492
|
+
const src = pi;
|
|
1493
|
+
return Object.fromEntries(PAYMENT_LIST_JSON_FIELDS.filter((k) => src[k] !== void 0).map((k) => [k, src[k]]));
|
|
1494
|
+
}
|
|
1495
|
+
var ALL_JSON_DEFAULT_MAX_ITEMS = 1e4;
|
|
1496
|
+
function buildListParams(opts) {
|
|
1497
|
+
const params = buildFilters(opts);
|
|
1498
|
+
if (opts.status !== void 0) {
|
|
1499
|
+
params.status = parseListFlag(opts.status, "--status", { lowercase: true });
|
|
1500
|
+
}
|
|
1501
|
+
if (opts.limit !== void 0) params.limit = wholeNumber2(opts.limit, "--limit");
|
|
1502
|
+
if (opts.startingAfter !== void 0) params.startingAfter = opts.startingAfter;
|
|
1503
|
+
if (opts.maxItems !== void 0 && !opts.all) throw new Error("--max-items is used with --all.");
|
|
1504
|
+
if (opts.includeMetadata && !opts.json) throw new Error("--include-metadata is used with --json.");
|
|
1505
|
+
const maxItems = opts.maxItems !== void 0 ? wholeNumber2(opts.maxItems, "--max-items") : opts.all && opts.json ? ALL_JSON_DEFAULT_MAX_ITEMS : void 0;
|
|
1506
|
+
return { params, ...maxItems !== void 0 ? { maxItems } : {} };
|
|
1507
|
+
}
|
|
1508
|
+
function buildSummaryParams(opts) {
|
|
1509
|
+
const params = buildFilters(opts);
|
|
1510
|
+
if (opts.groupBy !== void 0) {
|
|
1511
|
+
params.groupBy = parseListFlag(opts.groupBy, "--group-by", { lowercase: true });
|
|
1512
|
+
}
|
|
1513
|
+
if (opts.groupByMetadataKey !== void 0) {
|
|
1514
|
+
params.groupByMetadataKey = assertMetadataFilterKey(opts.groupByMetadataKey, "--group-by-metadata-key");
|
|
1515
|
+
}
|
|
1516
|
+
if (opts.timeZone !== void 0) params.timeZone = opts.timeZone;
|
|
1517
|
+
return params;
|
|
1518
|
+
}
|
|
1519
|
+
function outcomeTone(outcome) {
|
|
1520
|
+
if (outcome === "approved") return chalk8.green(outcome);
|
|
1521
|
+
if (outcome === "authorized" || outcome === "in_progress" || outcome === "voided") return chalk8.yellow(outcome);
|
|
1522
|
+
return chalk8.red(outcome);
|
|
1523
|
+
}
|
|
1524
|
+
function paymentListItemLine(pi) {
|
|
1525
|
+
const card = pi.card.brand || pi.card.last4 ? ` ${safeInline(pi.card.brand ?? "card")} ${pi.card.last4 ? `\u2022\u2022\u2022\u2022 ${safeInline(pi.card.last4)}` : ""}`.trimEnd() : "";
|
|
1526
|
+
return ` ${safeInline(pi.id)} ${outcomeTone(safeInline(pi.outcome))} ${safeInline(formatMinorUnits2(pi.amount, pi.currency))} ${safeInline(pi.createdAt)}` + (pi.store ? ` ${safeInline(pi.store)}` : "") + card + (pi.declineCode ? ` ${safeInline(pi.declineCode)}` : "");
|
|
1527
|
+
}
|
|
1528
|
+
function paymentListLines(page) {
|
|
1529
|
+
const lines = [` Window: ${safeInline(page.created.gte)} to ${safeInline(page.created.lt)} (end exclusive)`];
|
|
1530
|
+
if (page.data.length === 0) lines.push(" No payments match.");
|
|
1531
|
+
for (const pi of page.data) lines.push(paymentListItemLine(pi));
|
|
1532
|
+
if (page.hasMore && page.nextCursor) {
|
|
1533
|
+
lines.push(
|
|
1534
|
+
` More: re-run with exactly the same filters (and window, or none if you gave none) and --starting-after ${safeInline(page.nextCursor, 1100)} \u2014 the cursor lasts 24 hours; or use --all`
|
|
838
1535
|
);
|
|
839
1536
|
}
|
|
1537
|
+
return lines;
|
|
1538
|
+
}
|
|
1539
|
+
function formatRate(rate) {
|
|
1540
|
+
return rate === null ? "no data" : `${(rate * 100).toFixed(1)}%`;
|
|
1541
|
+
}
|
|
1542
|
+
function metricsLine(m) {
|
|
1543
|
+
return `attempts ${m.counts.attempts} approved ${m.counts.approved} declined ${m.counts.declined} approval ${formatRate(m.rates.approvalRate)} bank decline ${formatRate(m.rates.bankDeclineRate)} decline ${formatRate(m.rates.declineRate)}`;
|
|
1544
|
+
}
|
|
1545
|
+
function summaryLines(s) {
|
|
1546
|
+
const age = typeof s.ageSeconds === "number" ? `, ${s.ageSeconds} s ago` : "";
|
|
840
1547
|
const lines = [
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1548
|
+
` Payment attempts (as the dashboard counts them), ${safeInline(s.created.gte)} to ${safeInline(s.created.lt)}, time zone ${safeInline(s.timeZone)}`,
|
|
1549
|
+
` Computed at ${safeInline(s.generatedAt)}${age} (a summary is kept 45 s: for fresh numbers change the window or wait)`,
|
|
1550
|
+
` Total: ${metricsLine(s.totals)}`
|
|
844
1551
|
];
|
|
845
|
-
for (const
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
exitWithCliError(message, chalk7.red(message));
|
|
1552
|
+
for (const a of s.totals.amounts) {
|
|
1553
|
+
lines.push(
|
|
1554
|
+
` ${safeInline(a.currency)}: approved ${safeInline(formatMinorUnits2(a.approved, a.currency))}, declined ${safeInline(formatMinorUnits2(a.declined, a.currency))}`
|
|
1555
|
+
);
|
|
850
1556
|
}
|
|
1557
|
+
for (const g of s.groups) {
|
|
1558
|
+
const key = Object.entries(g.key).map(([dimension, value]) => `${safeInline(dimension)}=${value === null ? "(none)" : safeInline(value, 80)}`).join(" ");
|
|
1559
|
+
lines.push(` ${key}: ${metricsLine(g)}`);
|
|
1560
|
+
}
|
|
1561
|
+
if (s.groupBy.length > 0) lines.push(" Only groups with at least one attempt are shown. Add counts to combine groups; never average rates.");
|
|
1562
|
+
return lines;
|
|
851
1563
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
1564
|
+
var FILTER_HELP = {
|
|
1565
|
+
createdGte: "Start of the window, inclusive: Unix seconds or an ISO-8601 time with a zone (default: 31 days before the end; 7 with summary --group-by hour)",
|
|
1566
|
+
createdLt: "End of the window, exclusive (default: now). At most 31 days after the start (7 with summary --group-by hour)",
|
|
1567
|
+
outcome: "Only these outcomes, comma-separated (e.g. bank_declined,refused_before_bank)",
|
|
1568
|
+
store: "Only payments for this connected store hostname (e.g. shop.example.com)",
|
|
1569
|
+
metadata: "Only payments with this metadata tag, key=value (ONE tag; key 1-64 of letters, digits, _ . : -; value 1-500 characters)",
|
|
1570
|
+
cardBrand: "Only these card brands, comma-separated (visa, mastercard, amex, discover, unionpay, jcb, diners, unknown)",
|
|
1571
|
+
currency: "Only payments in this currency (e.g. USD)",
|
|
1572
|
+
initiator: "merchant (merchant-initiated charges on a saved card) or customer",
|
|
1573
|
+
declineCode: "Only declined payments with these decline codes, comma-separated (e.g. insufficient_funds)"
|
|
1574
|
+
};
|
|
1575
|
+
function addFilterOptions(cmd) {
|
|
1576
|
+
return cmd.option("--created-gte <time>", FILTER_HELP.createdGte).option("--created-lt <time>", FILTER_HELP.createdLt).option("--outcome <list>", FILTER_HELP.outcome).option("--store <hostname>", FILTER_HELP.store).option("--metadata <key=value>", FILTER_HELP.metadata).option("--card-brand <list>", FILTER_HELP.cardBrand).option("--currency <code>", FILTER_HELP.currency).option("--initiator <who>", FILTER_HELP.initiator).option("--decline-code <list>", FILTER_HELP.declineCode);
|
|
857
1577
|
}
|
|
858
|
-
function
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1578
|
+
function exitOnBadFlag(err) {
|
|
1579
|
+
const message = err.message;
|
|
1580
|
+
exitWithCliError(message, chalk8.red(`Error: ${message}`));
|
|
1581
|
+
}
|
|
1582
|
+
function addPaymentReportCommands(intents) {
|
|
1583
|
+
addFilterOptions(
|
|
1584
|
+
intents.command("list").description(
|
|
1585
|
+
"List payment intents, newest first, with filters \u2014 raw data. For approval and decline rates use `payment-intents summary`"
|
|
863
1586
|
)
|
|
864
|
-
);
|
|
1587
|
+
).option("--status <list>", "Only these statuses, comma-separated (e.g. failed,succeeded)").option("--limit <n>", "Page size, up to 100 per page (default 25; larger values are treated as 100)").option(
|
|
1588
|
+
"--starting-after <cursor>",
|
|
1589
|
+
"The next-page cursor printed by a previous run, unchanged, with exactly the same filters and window (it lasts 24 hours)"
|
|
1590
|
+
).option("--all", "Read every page, one request at a time (text: printed as each payment arrives)").option("--max-items <n>", `With --all: stop after this many payments (with --json, default ${ALL_JSON_DEFAULT_MAX_ITEMS})`).option(
|
|
1591
|
+
"--json",
|
|
1592
|
+
"Output JSON: each payment's status, outcome, amount, card, store and decline fields \u2014 not your metadata (with --all: one JSON array)"
|
|
1593
|
+
).option("--include-metadata", "With --json: print every field of each payment, your metadata included").action(async (opts) => {
|
|
1594
|
+
let built;
|
|
1595
|
+
try {
|
|
1596
|
+
built = buildListParams(opts);
|
|
1597
|
+
} catch (err) {
|
|
1598
|
+
exitOnBadFlag(err);
|
|
1599
|
+
}
|
|
1600
|
+
const client = makeCheckoutClient();
|
|
1601
|
+
const spinner = startSpinner("Listing payment intents...");
|
|
1602
|
+
try {
|
|
1603
|
+
if (opts.all) {
|
|
1604
|
+
const pager = client.paymentIntents.listAutoPaging({
|
|
1605
|
+
...built.params,
|
|
1606
|
+
...built.maxItems ? { maxItems: built.maxItems } : {}
|
|
1607
|
+
});
|
|
1608
|
+
if (opts.json) {
|
|
1609
|
+
const items = [];
|
|
1610
|
+
for await (const pi of pager) items.push(listItemJson(pi, opts.includeMetadata));
|
|
1611
|
+
spinner.stop();
|
|
1612
|
+
console.log(JSON.stringify(items, null, 2));
|
|
1613
|
+
if (opts.maxItems === void 0 && items.length >= ALL_JSON_DEFAULT_MAX_ITEMS) {
|
|
1614
|
+
console.error(
|
|
1615
|
+
`Stopped at ${ALL_JSON_DEFAULT_MAX_ITEMS} payments (the --all --json default). Pass --max-items to read more, or narrow the window.`
|
|
1616
|
+
);
|
|
1617
|
+
}
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
let count = 0;
|
|
1621
|
+
for await (const pi of pager) {
|
|
1622
|
+
if (count === 0) spinner.stop();
|
|
1623
|
+
console.log(paymentListItemLine(pi));
|
|
1624
|
+
count += 1;
|
|
1625
|
+
}
|
|
1626
|
+
spinner.stop();
|
|
1627
|
+
if (count === 0) console.log(" No payments match.");
|
|
1628
|
+
console.log(` ${count} payment(s).`);
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
const page = await client.paymentIntents.list(built.params);
|
|
1632
|
+
spinner.stop();
|
|
1633
|
+
if (opts.json) {
|
|
1634
|
+
console.log(JSON.stringify({ ...page, data: page.data.map((pi) => listItemJson(pi, opts.includeMetadata)) }, null, 2));
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
for (const line of paymentListLines(page)) console.log(line);
|
|
1638
|
+
} catch (err) {
|
|
1639
|
+
spinner.stop();
|
|
1640
|
+
handleError(err);
|
|
1641
|
+
}
|
|
1642
|
+
});
|
|
1643
|
+
addFilterOptions(
|
|
1644
|
+
intents.command("summary").description(
|
|
1645
|
+
"Approval and decline rates, counts and amounts for a window, as the dashboard counts payment attempts. A rate with no traffic shows as no data, never 0%. A summary is kept 45 s: for fresh numbers, change the window or wait"
|
|
1646
|
+
)
|
|
1647
|
+
).option("--group-by <list>", "Up to 3 of hour, day, week, store, metadata, card_brand, currency, outcome, decline_code (one time unit)").option(
|
|
1648
|
+
"--group-by-metadata-key <key>",
|
|
1649
|
+
"With --group-by metadata: the metadata key whose values become groups (1-64 of letters, digits, _ . : -)"
|
|
1650
|
+
).option("--time-zone <iana>", "Time zone for hour / day / week buckets (default UTC), e.g. America/New_York").option("--json", "Output raw JSON").action(async (opts) => {
|
|
1651
|
+
let params;
|
|
1652
|
+
try {
|
|
1653
|
+
params = buildSummaryParams(opts);
|
|
1654
|
+
} catch (err) {
|
|
1655
|
+
exitOnBadFlag(err);
|
|
1656
|
+
}
|
|
1657
|
+
const client = makeCheckoutClient();
|
|
1658
|
+
const spinner = startSpinner("Summarizing payments...");
|
|
1659
|
+
try {
|
|
1660
|
+
const summary = await client.paymentIntents.summary(params);
|
|
1661
|
+
spinner.stop();
|
|
1662
|
+
if (opts.json) {
|
|
1663
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
for (const line of summaryLines(summary)) console.log(line);
|
|
1667
|
+
} catch (err) {
|
|
1668
|
+
spinner.stop();
|
|
1669
|
+
handleError(err);
|
|
1670
|
+
}
|
|
1671
|
+
});
|
|
865
1672
|
}
|
|
866
1673
|
|
|
867
1674
|
// src/commands/payment-intents.ts
|
|
@@ -940,28 +1747,28 @@ function statusTone(status) {
|
|
|
940
1747
|
return "gray";
|
|
941
1748
|
}
|
|
942
1749
|
function printPaymentIntent(intent) {
|
|
943
|
-
const statusColor =
|
|
1750
|
+
const statusColor = chalk9[statusTone(intent.status)];
|
|
944
1751
|
console.log(` ID: ${intent.id}`);
|
|
945
1752
|
console.log(` Status: ${statusColor(intent.status)}`);
|
|
946
1753
|
console.log(
|
|
947
|
-
` Amount: ${
|
|
1754
|
+
` Amount: ${formatMinorUnits3(intent.amount, intent.currency)}`
|
|
948
1755
|
);
|
|
949
1756
|
console.log(` Capture: ${intent.captureMethod}`);
|
|
950
1757
|
if (intent.nextAction) {
|
|
951
1758
|
console.log(
|
|
952
|
-
` Next action: ${
|
|
1759
|
+
` Next action: ${chalk9.yellow(safeDisplay(intent.nextAction.type))}`
|
|
953
1760
|
);
|
|
954
1761
|
if (intent.nextAction.type === "redirect_to_url") {
|
|
955
1762
|
console.log(
|
|
956
|
-
` Redirect: ${
|
|
1763
|
+
` Redirect: ${chalk9.yellow(safeDisplay(intent.nextAction.redirectToUrl.url))}`
|
|
957
1764
|
);
|
|
958
1765
|
}
|
|
959
1766
|
}
|
|
960
1767
|
if (intent.declineCode) {
|
|
961
|
-
console.log(` Decline code: ${
|
|
1768
|
+
console.log(` Decline code: ${chalk9.red(safeDisplay(intent.declineCode))}`);
|
|
962
1769
|
}
|
|
963
1770
|
if (intent.networkAdvice) {
|
|
964
|
-
console.log(` Network advice: ${
|
|
1771
|
+
console.log(` Network advice: ${chalk9.red(safeInline(intent.networkAdvice))}`);
|
|
965
1772
|
if (typeof intent.networkAdviceRetryAfterMs === "number") {
|
|
966
1773
|
console.log(` Retry after: ${intent.networkAdviceRetryAfterMs} ms (earliest)`);
|
|
967
1774
|
}
|
|
@@ -999,14 +1806,14 @@ function paymentIntentsCommand() {
|
|
|
999
1806
|
"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"
|
|
1000
1807
|
).option("--buyer-email <email>", "Buyer email for this charge").option(
|
|
1001
1808
|
"--idempotency-key <key>",
|
|
1002
|
-
"Idempotency key for this charge. Reuse the SAME key to retry the same charge safely.
|
|
1809
|
+
"Idempotency key for this charge. Reuse the SAME key to retry the same charge safely. REQUIRED on a live key; on a sandbox key generated (and printed) when omitted"
|
|
1003
1810
|
).addOption(retiredConfirmLiveOption()).option("--json", "Output raw JSON").action(async (raw) => {
|
|
1004
1811
|
const apiKey = requireApiKey();
|
|
1005
1812
|
const opts = { ...raw, amount: minorUnitsOrExit(raw.amount, "--amount") };
|
|
1006
1813
|
const captureMethod = opts.captureMethod?.toLowerCase();
|
|
1007
1814
|
if (captureMethod !== void 0 && captureMethod !== "automatic" && captureMethod !== "manual") {
|
|
1008
1815
|
const message = `Invalid --capture-method "${opts.captureMethod}". Expected automatic or manual.`;
|
|
1009
|
-
exitWithCliError(message,
|
|
1816
|
+
exitWithCliError(message, chalk9.red(message));
|
|
1010
1817
|
}
|
|
1011
1818
|
let params;
|
|
1012
1819
|
try {
|
|
@@ -1017,19 +1824,19 @@ function paymentIntentsCommand() {
|
|
|
1017
1824
|
handleError(err);
|
|
1018
1825
|
}
|
|
1019
1826
|
refuseUnsafeLiveDestination(apiKey);
|
|
1020
|
-
const idempotencyKey = idempotencyKeyFor(opts.idempotencyKey, "charge");
|
|
1827
|
+
const idempotencyKey = idempotencyKeyFor(opts.idempotencyKey, "charge", apiKey);
|
|
1021
1828
|
await confirmLiveAction(
|
|
1022
1829
|
apiKey,
|
|
1023
1830
|
{
|
|
1024
1831
|
action: "create a payment intent (charge a payment)",
|
|
1025
1832
|
details: [
|
|
1026
1833
|
...apiHostDetail(),
|
|
1027
|
-
["Amount",
|
|
1834
|
+
["Amount", formatMinorUnits3(params.amount, params.currency)],
|
|
1028
1835
|
["Capture", params.captureMethod ?? "automatic"],
|
|
1029
1836
|
...params.paymentMethod ? [["Saved card", params.paymentMethod.id]] : [],
|
|
1030
1837
|
...params.buyerId ? [["Buyer", params.buyerId]] : [],
|
|
1031
1838
|
...params.sessionId ? [["Session", params.sessionId]] : [],
|
|
1032
|
-
["Idempotency key", idempotencyKey]
|
|
1839
|
+
["Idempotency key", displayIdempotencyKey(idempotencyKey)]
|
|
1033
1840
|
]
|
|
1034
1841
|
},
|
|
1035
1842
|
opts
|
|
@@ -1044,7 +1851,7 @@ function paymentIntentsCommand() {
|
|
|
1044
1851
|
if (opts.json) {
|
|
1045
1852
|
console.log(JSON.stringify(intent, null, 2));
|
|
1046
1853
|
} else {
|
|
1047
|
-
console.log(
|
|
1854
|
+
console.log(chalk9.green("\u2713 Payment intent created"));
|
|
1048
1855
|
printPaymentIntent(intent);
|
|
1049
1856
|
}
|
|
1050
1857
|
} catch (err) {
|
|
@@ -1075,7 +1882,7 @@ function paymentIntentsCommand() {
|
|
|
1075
1882
|
"Partial capture amount in minor units, a whole number (omit for full)"
|
|
1076
1883
|
).option(
|
|
1077
1884
|
"--idempotency-key <key>",
|
|
1078
|
-
"Idempotency key. Reuse the SAME key to retry safely.
|
|
1885
|
+
"Idempotency key. Reuse the SAME key to retry safely. REQUIRED on a live key; on a sandbox key generated (and printed) when omitted"
|
|
1079
1886
|
).addOption(retiredConfirmLiveOption()).option("--json", "Output raw JSON").action(
|
|
1080
1887
|
async (id, opts) => {
|
|
1081
1888
|
const apiKey = requireApiKey();
|
|
@@ -1084,7 +1891,7 @@ function paymentIntentsCommand() {
|
|
|
1084
1891
|
"--amount-to-capture"
|
|
1085
1892
|
);
|
|
1086
1893
|
refuseUnsafeLiveDestination(apiKey);
|
|
1087
|
-
const idempotencyKey = idempotencyKeyFor(opts.idempotencyKey, "capture");
|
|
1894
|
+
const idempotencyKey = idempotencyKeyFor(opts.idempotencyKey, "capture", apiKey);
|
|
1088
1895
|
await confirmLiveAction(
|
|
1089
1896
|
apiKey,
|
|
1090
1897
|
{
|
|
@@ -1096,7 +1903,7 @@ function paymentIntentsCommand() {
|
|
|
1096
1903
|
"Amount",
|
|
1097
1904
|
amountToCapture !== void 0 ? `${amountToCapture} in minor units (partial capture)` : "the full authorized amount"
|
|
1098
1905
|
],
|
|
1099
|
-
["Idempotency key", idempotencyKey]
|
|
1906
|
+
["Idempotency key", displayIdempotencyKey(idempotencyKey)]
|
|
1100
1907
|
]
|
|
1101
1908
|
},
|
|
1102
1909
|
opts
|
|
@@ -1113,8 +1920,8 @@ function paymentIntentsCommand() {
|
|
|
1113
1920
|
if (opts.json) {
|
|
1114
1921
|
console.log(JSON.stringify(intent, null, 2));
|
|
1115
1922
|
} else {
|
|
1116
|
-
const captureLabel = amountToCapture !== void 0 ? ` (partial: ${
|
|
1117
|
-
console.log(
|
|
1923
|
+
const captureLabel = amountToCapture !== void 0 ? ` (partial: ${formatMinorUnits3(amountToCapture, intent.currency)})` : "";
|
|
1924
|
+
console.log(chalk9.green(`\u2713 Payment intent captured${captureLabel}`));
|
|
1118
1925
|
printPaymentIntent(intent);
|
|
1119
1926
|
}
|
|
1120
1927
|
} catch (err) {
|
|
@@ -1125,12 +1932,12 @@ function paymentIntentsCommand() {
|
|
|
1125
1932
|
);
|
|
1126
1933
|
intents.command("void").description("Void an authorized (uncaptured) payment intent").argument("<id>", "Payment intent ID (vpi_*)").option(
|
|
1127
1934
|
"--idempotency-key <key>",
|
|
1128
|
-
"Idempotency key. Reuse the SAME key to retry safely.
|
|
1935
|
+
"Idempotency key. Reuse the SAME key to retry safely. REQUIRED on a live key; on a sandbox key generated (and printed) when omitted"
|
|
1129
1936
|
).addOption(retiredConfirmLiveOption()).option("--json", "Output raw JSON").action(
|
|
1130
1937
|
async (id, opts) => {
|
|
1131
1938
|
const apiKey = requireApiKey();
|
|
1132
1939
|
refuseUnsafeLiveDestination(apiKey);
|
|
1133
|
-
const idempotencyKey = idempotencyKeyFor(opts.idempotencyKey, "void");
|
|
1940
|
+
const idempotencyKey = idempotencyKeyFor(opts.idempotencyKey, "void", apiKey);
|
|
1134
1941
|
await confirmLiveAction(
|
|
1135
1942
|
apiKey,
|
|
1136
1943
|
{
|
|
@@ -1138,7 +1945,7 @@ function paymentIntentsCommand() {
|
|
|
1138
1945
|
details: [
|
|
1139
1946
|
...apiHostDetail(),
|
|
1140
1947
|
["Payment intent", id],
|
|
1141
|
-
["Idempotency key", idempotencyKey]
|
|
1948
|
+
["Idempotency key", displayIdempotencyKey(idempotencyKey)]
|
|
1142
1949
|
],
|
|
1143
1950
|
consequence: "A voided payment cannot be captured afterwards; this cannot be undone from here."
|
|
1144
1951
|
},
|
|
@@ -1152,7 +1959,7 @@ function paymentIntentsCommand() {
|
|
|
1152
1959
|
if (opts.json) {
|
|
1153
1960
|
console.log(JSON.stringify(intent, null, 2));
|
|
1154
1961
|
} else {
|
|
1155
|
-
console.log(
|
|
1962
|
+
console.log(chalk9.green("\u2713 Payment intent voided"));
|
|
1156
1963
|
printPaymentIntent(intent);
|
|
1157
1964
|
}
|
|
1158
1965
|
} catch (err) {
|
|
@@ -1161,13 +1968,14 @@ function paymentIntentsCommand() {
|
|
|
1161
1968
|
}
|
|
1162
1969
|
}
|
|
1163
1970
|
);
|
|
1971
|
+
addPaymentReportCommands(intents);
|
|
1164
1972
|
return intents;
|
|
1165
1973
|
}
|
|
1166
1974
|
|
|
1167
1975
|
// src/commands/refunds.ts
|
|
1168
1976
|
import { Command as Command6 } from "commander";
|
|
1169
|
-
import
|
|
1170
|
-
import { formatMinorUnits as
|
|
1977
|
+
import chalk10 from "chalk";
|
|
1978
|
+
import { formatMinorUnits as formatMinorUnits4, VonPayError as VonPayError2 } from "@vonpay/checkout-node";
|
|
1171
1979
|
function present(v) {
|
|
1172
1980
|
return v !== void 0 && v.trim() !== "";
|
|
1173
1981
|
}
|
|
@@ -1195,8 +2003,83 @@ function buildRefundParams(opts, metadata) {
|
|
|
1195
2003
|
if (metadata !== void 0) params.metadata = metadata;
|
|
1196
2004
|
return params;
|
|
1197
2005
|
}
|
|
2006
|
+
var TRANSACTION_NEEDS_SESSION_MESSAGE = "Refusing a live-key refund by --transaction without --session-id: the transaction is checked against the session it belongs to (it must be that session's single payment) before you are asked to confirm. Prefer --payment-intent when the payment has one. Nothing was sent.";
|
|
2007
|
+
var SESSION_WITH_PAYMENT_INTENT_MESSAGE = "--session-id is only used with --transaction. Refund a payment intent with --payment-intent alone. Nothing was sent.";
|
|
2008
|
+
function refundSessionProblem(target, sessionId, live) {
|
|
2009
|
+
const hasSession = present(sessionId);
|
|
2010
|
+
if ("paymentIntent" in target && hasSession) return SESSION_WITH_PAYMENT_INTENT_MESSAGE;
|
|
2011
|
+
if ("transaction" in target && live && !hasSession) return TRANSACTION_NEEDS_SESSION_MESSAGE;
|
|
2012
|
+
return null;
|
|
2013
|
+
}
|
|
2014
|
+
function errorCodeSuffix(err) {
|
|
2015
|
+
const code = err instanceof VonPayError2 ? err.code : void 0;
|
|
2016
|
+
return typeof code === "string" && /^[a-z_]{1,64}$/.test(code) ? ` (error code: ${code})` : "";
|
|
2017
|
+
}
|
|
2018
|
+
function refuseLookup(message) {
|
|
2019
|
+
auditNote({ outcome: "lookup_refused", code: "lookup_failed" });
|
|
2020
|
+
return exitWithCliError(message, chalk10.red(message));
|
|
2021
|
+
}
|
|
2022
|
+
async function refundLookupDetails(client, target, sessionId, amount, namedCurrency) {
|
|
2023
|
+
const lines = [];
|
|
2024
|
+
let currency;
|
|
2025
|
+
if ("paymentIntent" in target) {
|
|
2026
|
+
let intent;
|
|
2027
|
+
try {
|
|
2028
|
+
intent = await client.paymentIntents.retrieve(target.paymentIntent);
|
|
2029
|
+
} catch (err) {
|
|
2030
|
+
refuseLookup(`Refusing the refund: the payment intent could not be looked up${errorCodeSuffix(err)}, so it cannot be shown to you before you confirm. Nothing was sent.`);
|
|
2031
|
+
}
|
|
2032
|
+
currency = intent.currency;
|
|
2033
|
+
lines.push(
|
|
2034
|
+
["Original payment", formatMinorUnits4(intent.amount, intent.currency)],
|
|
2035
|
+
["Status", String(intent.status)],
|
|
2036
|
+
["Created", String(intent.createdAt)]
|
|
2037
|
+
);
|
|
2038
|
+
} else {
|
|
2039
|
+
let session;
|
|
2040
|
+
try {
|
|
2041
|
+
session = await client.sessions.get(sessionId);
|
|
2042
|
+
} catch (err) {
|
|
2043
|
+
refuseLookup(`Refusing the refund: the session could not be looked up${errorCodeSuffix(err)}, so the transaction cannot be checked against it. Nothing was sent.`);
|
|
2044
|
+
}
|
|
2045
|
+
if (session.vp_tx_count !== 1) {
|
|
2046
|
+
refuseLookup(
|
|
2047
|
+
typeof session.vp_tx_count === "number" && session.vp_tx_count >= 2 ? "Refusing the refund: this session has more than one payment \u2014 the buyer may have been charged more than once. Do not refund anything; have a person check the payments. Nothing was sent." : "Refusing the refund: the session does not show exactly one captured payment, so the transaction cannot be checked against it. Nothing was sent."
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
2050
|
+
if (session.vp_tx_id !== target.transaction) {
|
|
2051
|
+
refuseLookup("Refusing the refund: that transaction is not this session's payment. Check the order in your own records. Nothing was sent.");
|
|
2052
|
+
}
|
|
2053
|
+
currency = session.currency;
|
|
2054
|
+
lines.push(
|
|
2055
|
+
["Session total", formatMinorUnits4(session.amount, session.currency)],
|
|
2056
|
+
["Payment", String(session.paymentStatus ?? "not reported")],
|
|
2057
|
+
["Created", String(session.createdAt)]
|
|
2058
|
+
);
|
|
2059
|
+
}
|
|
2060
|
+
let totals;
|
|
2061
|
+
try {
|
|
2062
|
+
totals = await client.refunds.list(target, { limit: 1 });
|
|
2063
|
+
} catch (err) {
|
|
2064
|
+
refuseLookup(`Refusing the refund: the payment's earlier refunds could not be looked up${errorCodeSuffix(err)}, so what was already refunded cannot be shown to you. Nothing was sent.`);
|
|
2065
|
+
}
|
|
2066
|
+
lines.push(
|
|
2067
|
+
["Already refunded", formatMinorUnits4(totals.amountRefunded, currency)],
|
|
2068
|
+
[
|
|
2069
|
+
"Still refundable",
|
|
2070
|
+
`${formatMinorUnits4(totals.remainingRefundable, currency)}${totals.refundable ? "" : " \u2014 Von Payments says this payment cannot be refunded now"}`
|
|
2071
|
+
]
|
|
2072
|
+
);
|
|
2073
|
+
if (amount !== void 0 && amount > totals.remainingRefundable) {
|
|
2074
|
+
lines.push(["\u26A0 Check", "the amount to refund is MORE than what is still refundable"]);
|
|
2075
|
+
}
|
|
2076
|
+
if (namedCurrency !== void 0 && typeof currency === "string" && namedCurrency.toUpperCase() !== currency.toUpperCase()) {
|
|
2077
|
+
lines.push(["\u26A0 Check", "the currency named for this refund is NOT the payment's currency looked up above"]);
|
|
2078
|
+
}
|
|
2079
|
+
return lines;
|
|
2080
|
+
}
|
|
1198
2081
|
function printRefund(refund) {
|
|
1199
|
-
const statusColor = refund.status === "succeeded" ?
|
|
2082
|
+
const statusColor = refund.status === "succeeded" ? chalk10.green : refund.status === "failed" ? chalk10.red : chalk10.yellow;
|
|
1200
2083
|
console.log(` ID: ${refund.id}`);
|
|
1201
2084
|
if (refund.transaction !== null) {
|
|
1202
2085
|
console.log(` Transaction: ${refund.transaction}`);
|
|
@@ -1205,7 +2088,7 @@ function printRefund(refund) {
|
|
|
1205
2088
|
}
|
|
1206
2089
|
console.log(` Status: ${statusColor(refund.status)}`);
|
|
1207
2090
|
console.log(
|
|
1208
|
-
` Amount: ${
|
|
2091
|
+
` Amount: ${formatMinorUnits4(refund.amount, refund.currency)}`
|
|
1209
2092
|
);
|
|
1210
2093
|
if (refund.reason) {
|
|
1211
2094
|
console.log(` Reason: ${safeDisplay(refund.reason)}`);
|
|
@@ -1229,15 +2112,18 @@ function refundsCommand() {
|
|
|
1229
2112
|
).option("--currency <code>", "ISO 4217 currency code").option("--reason <text>", "Free-form reason (mirrored back on the record)").option(
|
|
1230
2113
|
"--metadata <pair...>",
|
|
1231
2114
|
"Metadata key=value (repeat flag for multiple pairs)"
|
|
2115
|
+
).option(
|
|
2116
|
+
"--session-id <id>",
|
|
2117
|
+
"With --transaction: the checkout session this payment belongs to (vp_cs_*). REQUIRED with --transaction on a live key: the transaction is checked against it (the session's single payment) before you are asked to confirm"
|
|
1232
2118
|
).option(
|
|
1233
2119
|
"--idempotency-key <key>",
|
|
1234
|
-
"Idempotency key. Reuse the SAME key to retry safely.
|
|
2120
|
+
"Idempotency key. Reuse the SAME key to retry safely. REQUIRED on a live key; on a sandbox key generated (and printed) when omitted"
|
|
1235
2121
|
).addOption(retiredConfirmLiveOption()).option("--json", "Output raw JSON").action(async (raw) => {
|
|
1236
2122
|
try {
|
|
1237
2123
|
resolveRefundTarget(raw);
|
|
1238
2124
|
} catch (err) {
|
|
1239
2125
|
const message = err.message;
|
|
1240
|
-
exitWithCliError(message,
|
|
2126
|
+
exitWithCliError(message, chalk10.red(`Error: ${message}`));
|
|
1241
2127
|
}
|
|
1242
2128
|
const apiKey = requireApiKey();
|
|
1243
2129
|
const opts = {
|
|
@@ -1251,8 +2137,11 @@ function refundsCommand() {
|
|
|
1251
2137
|
handleError(err);
|
|
1252
2138
|
}
|
|
1253
2139
|
const target = resolveRefundTarget(opts);
|
|
2140
|
+
const sessionProblem = refundSessionProblem(target, opts.sessionId, requiresLiveConfirmation(apiKey));
|
|
2141
|
+
if (sessionProblem) exitWithCliError(sessionProblem, chalk10.red(sessionProblem));
|
|
1254
2142
|
refuseUnsafeLiveDestination(apiKey);
|
|
1255
|
-
const idempotencyKey = idempotencyKeyFor(opts.idempotencyKey, "refund");
|
|
2143
|
+
const idempotencyKey = idempotencyKeyFor(opts.idempotencyKey, "refund", apiKey);
|
|
2144
|
+
const lookedUp = requiresLiveConfirmation(apiKey) && terminal.isInteractive() ? await refundLookupDetails(makeCheckoutClient(), target, opts.sessionId?.trim(), params.amount, params.currency) : [];
|
|
1256
2145
|
await confirmLiveAction(
|
|
1257
2146
|
apiKey,
|
|
1258
2147
|
{
|
|
@@ -1262,9 +2151,10 @@ function refundsCommand() {
|
|
|
1262
2151
|
"transaction" in target ? ["Transaction", target.transaction] : ["Payment intent", target.paymentIntent],
|
|
1263
2152
|
[
|
|
1264
2153
|
"Amount",
|
|
1265
|
-
params.amount === void 0 ? "the full remaining balance" : params.currency ?
|
|
2154
|
+
params.amount === void 0 ? "the full remaining balance" : params.currency ? formatMinorUnits4(params.amount, params.currency) : `${params.amount} in minor units`
|
|
1266
2155
|
],
|
|
1267
|
-
|
|
2156
|
+
...lookedUp,
|
|
2157
|
+
["Idempotency key", displayIdempotencyKey(idempotencyKey)]
|
|
1268
2158
|
],
|
|
1269
2159
|
consequence: "This pays money back out of the merchant's balance and cannot be reversed once settled."
|
|
1270
2160
|
},
|
|
@@ -1278,7 +2168,7 @@ function refundsCommand() {
|
|
|
1278
2168
|
if (opts.json) {
|
|
1279
2169
|
console.log(JSON.stringify(refund, null, 2));
|
|
1280
2170
|
} else {
|
|
1281
|
-
console.log(
|
|
2171
|
+
console.log(chalk10.green("\u2713 Refund created"));
|
|
1282
2172
|
printRefund(refund);
|
|
1283
2173
|
}
|
|
1284
2174
|
} catch (err) {
|
|
@@ -1295,14 +2185,14 @@ function refundsCommand() {
|
|
|
1295
2185
|
target = resolveRefundTarget(opts);
|
|
1296
2186
|
} catch (err) {
|
|
1297
2187
|
const message = err.message.replace("A refund target is required", "A payment is required");
|
|
1298
|
-
exitWithCliError(message,
|
|
2188
|
+
exitWithCliError(message, chalk10.red(`Error: ${message}`));
|
|
1299
2189
|
}
|
|
1300
2190
|
let limit;
|
|
1301
2191
|
if (opts.limit !== void 0) {
|
|
1302
2192
|
const parsed = parseMinorUnits(opts.limit, "--limit");
|
|
1303
2193
|
if (!parsed.ok || parsed.value < 1) {
|
|
1304
2194
|
const message = `--limit must be a whole number from 1 to 100; got ${JSON.stringify(opts.limit)}.`;
|
|
1305
|
-
exitWithCliError(message,
|
|
2195
|
+
exitWithCliError(message, chalk10.red(`Error: ${message}`));
|
|
1306
2196
|
}
|
|
1307
2197
|
limit = parsed.value;
|
|
1308
2198
|
}
|
|
@@ -1329,7 +2219,7 @@ function refundsCommand() {
|
|
|
1329
2219
|
}
|
|
1330
2220
|
function refundListLines(list) {
|
|
1331
2221
|
const currency = list.data[0]?.currency;
|
|
1332
|
-
const money = (n) => currency ?
|
|
2222
|
+
const money = (n) => currency ? formatMinorUnits4(n, currency) : `${n} in minor units`;
|
|
1333
2223
|
const lines = [
|
|
1334
2224
|
` Refunded so far: ${money(list.amountRefunded)} (succeeded + still in progress)`,
|
|
1335
2225
|
` Still refundable: ${money(list.remainingRefundable)}${list.refundable ? "" : " \u2014 this payment cannot be refunded now"}`
|
|
@@ -1338,13 +2228,13 @@ function refundListLines(list) {
|
|
|
1338
2228
|
lines.push(" No refunds on this payment.");
|
|
1339
2229
|
}
|
|
1340
2230
|
for (const r of list.data) {
|
|
1341
|
-
const status = r.status === "succeeded" ?
|
|
2231
|
+
const status = r.status === "succeeded" ? chalk10.green(r.status) : r.status === "failed" ? chalk10.red(r.status) : chalk10.yellow(r.status);
|
|
1342
2232
|
lines.push(
|
|
1343
|
-
` ${r.id} ${status} ${
|
|
2233
|
+
` ${r.id} ${status} ${formatMinorUnits4(r.amount, r.currency)}` + (r.createdAt ? ` ${r.createdAt}` : "") + (r.idempotencyKey ? ` key ${safeDisplay(r.idempotencyKey)}` : "")
|
|
1344
2234
|
);
|
|
1345
2235
|
}
|
|
1346
2236
|
if (list.data.some((r) => r.status === "requested")) {
|
|
1347
|
-
lines.push(
|
|
2237
|
+
lines.push(chalk10.yellow(" A refund marked requested is still in progress and can still fail: do not refund again."));
|
|
1348
2238
|
}
|
|
1349
2239
|
if (list.hasMore && list.nextCursor) {
|
|
1350
2240
|
lines.push(` More: re-run with --starting-after ${safeDisplay(list.nextCursor)}`);
|
|
@@ -1354,9 +2244,9 @@ function refundListLines(list) {
|
|
|
1354
2244
|
|
|
1355
2245
|
// src/commands/tokens.ts
|
|
1356
2246
|
import { Command as Command7 } from "commander";
|
|
1357
|
-
import
|
|
2247
|
+
import chalk11 from "chalk";
|
|
1358
2248
|
function printToken(token) {
|
|
1359
|
-
const statusColor = token.status === "active" ?
|
|
2249
|
+
const statusColor = token.status === "active" ? chalk11.green : token.status === "expired" ? chalk11.yellow : chalk11.red;
|
|
1360
2250
|
console.log(` ID: ${token.id}`);
|
|
1361
2251
|
console.log(` Status: ${statusColor(token.status)}`);
|
|
1362
2252
|
const card = formatTokenCard(token.card);
|
|
@@ -1378,7 +2268,7 @@ function tokensCommand() {
|
|
|
1378
2268
|
const apiKey = requireApiKey();
|
|
1379
2269
|
if (!opts.buyerId && !opts.providerReference) {
|
|
1380
2270
|
console.warn(
|
|
1381
|
-
|
|
2271
|
+
chalk11.yellow(
|
|
1382
2272
|
"Warning: no --buyer-id or --provider-reference supplied. Providers with an iframe vault (sandbox and live alike) will fail with a 422 validation_error."
|
|
1383
2273
|
)
|
|
1384
2274
|
);
|
|
@@ -1404,7 +2294,7 @@ function tokensCommand() {
|
|
|
1404
2294
|
if (opts.json) {
|
|
1405
2295
|
console.log(JSON.stringify(token, null, 2));
|
|
1406
2296
|
} else {
|
|
1407
|
-
console.log(
|
|
2297
|
+
console.log(chalk11.green("\u2713 Token created"));
|
|
1408
2298
|
printToken(token);
|
|
1409
2299
|
}
|
|
1410
2300
|
} catch (err) {
|
|
@@ -1423,33 +2313,33 @@ function formatTokenCard(card) {
|
|
|
1423
2313
|
|
|
1424
2314
|
// src/commands/capabilities.ts
|
|
1425
2315
|
import { Command as Command8 } from "commander";
|
|
1426
|
-
import
|
|
2316
|
+
import chalk12 from "chalk";
|
|
1427
2317
|
function fmtBool(v) {
|
|
1428
|
-
return v ?
|
|
2318
|
+
return v ? chalk12.green("yes") : chalk12.dim("no");
|
|
1429
2319
|
}
|
|
1430
2320
|
function fmtVoidPolicy(v) {
|
|
1431
2321
|
switch (v) {
|
|
1432
2322
|
case "supported":
|
|
1433
|
-
return
|
|
2323
|
+
return chalk12.green(v);
|
|
1434
2324
|
case "rerouted_to_refund":
|
|
1435
|
-
return
|
|
2325
|
+
return chalk12.yellow(v);
|
|
1436
2326
|
case "not_supported":
|
|
1437
|
-
return
|
|
2327
|
+
return chalk12.dim(v);
|
|
1438
2328
|
default:
|
|
1439
|
-
return
|
|
2329
|
+
return chalk12.dim(String(v));
|
|
1440
2330
|
}
|
|
1441
2331
|
}
|
|
1442
2332
|
function fmtDisputeReporting(v) {
|
|
1443
|
-
return v === "tracked" ?
|
|
2333
|
+
return v === "tracked" ? chalk12.green(v) : chalk12.yellow(v);
|
|
1444
2334
|
}
|
|
1445
2335
|
function fmtSubscriptionBilling(v) {
|
|
1446
|
-
if (v === true) return
|
|
1447
|
-
if (v === false) return
|
|
1448
|
-
return
|
|
2336
|
+
if (v === true) return chalk12.yellow("yes");
|
|
2337
|
+
if (v === false) return chalk12.green("no");
|
|
2338
|
+
return chalk12.yellow("unknown");
|
|
1449
2339
|
}
|
|
1450
2340
|
function printCapabilities(caps) {
|
|
1451
2341
|
const ops = caps.supportedOperations;
|
|
1452
|
-
console.log(
|
|
2342
|
+
console.log(chalk12.bold("Supported operations"));
|
|
1453
2343
|
console.log(` auth/capture separation: ${fmtBool(ops.authCaptureSeparation)}`);
|
|
1454
2344
|
console.log(` partial capture: ${fmtBool(ops.partialCapture)}`);
|
|
1455
2345
|
console.log(` partial refund: ${fmtBool(ops.partialRefund)}`);
|
|
@@ -1464,17 +2354,17 @@ function printCapabilities(caps) {
|
|
|
1464
2354
|
console.log();
|
|
1465
2355
|
if (ops.disputeReporting !== "tracked") {
|
|
1466
2356
|
console.log(
|
|
1467
|
-
|
|
2357
|
+
chalk12.yellow(
|
|
1468
2358
|
" \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."
|
|
1469
2359
|
)
|
|
1470
2360
|
);
|
|
1471
2361
|
console.log();
|
|
1472
2362
|
}
|
|
1473
|
-
console.log(
|
|
2363
|
+
console.log(chalk12.bold("Subscription billing"));
|
|
1474
2364
|
console.log(` Von Pay renews for you: ${fmtSubscriptionBilling(caps.subscriptionBilling?.enabled)}`);
|
|
1475
2365
|
if (caps.subscriptionBilling?.enabled !== false) {
|
|
1476
2366
|
console.log(
|
|
1477
|
-
|
|
2367
|
+
chalk12.yellow(
|
|
1478
2368
|
` \u26A0 Do not run renewals from your own system while this is not "no":
|
|
1479
2369
|
Von Pay may be charging these customers' renewals itself, and both
|
|
1480
2370
|
would charge them. Check again at the moment you renew.`
|
|
@@ -1482,12 +2372,12 @@ function printCapabilities(caps) {
|
|
|
1482
2372
|
);
|
|
1483
2373
|
}
|
|
1484
2374
|
console.log();
|
|
1485
|
-
console.log(
|
|
2375
|
+
console.log(chalk12.bold("Settlement currencies"));
|
|
1486
2376
|
console.log(
|
|
1487
|
-
` ${caps.settlementCurrencies.length ? caps.settlementCurrencies.join(", ") :
|
|
2377
|
+
` ${caps.settlementCurrencies.length ? caps.settlementCurrencies.join(", ") : chalk12.dim("(none reported)")}`
|
|
1488
2378
|
);
|
|
1489
2379
|
console.log();
|
|
1490
|
-
console.log(
|
|
2380
|
+
console.log(chalk12.bold("Rate limits"));
|
|
1491
2381
|
console.log(
|
|
1492
2382
|
` payment intents / minute: ${caps.rateLimits.paymentIntentsPerMinute}`
|
|
1493
2383
|
);
|
|
@@ -1514,9 +2404,41 @@ function capabilitiesCommand() {
|
|
|
1514
2404
|
// src/commands/trigger.ts
|
|
1515
2405
|
import { Command as Command9 } from "commander";
|
|
1516
2406
|
import { createHmac as createHmac2, randomUUID as randomUUID2 } from "crypto";
|
|
1517
|
-
import
|
|
2407
|
+
import chalk13 from "chalk";
|
|
1518
2408
|
import ora2 from "ora";
|
|
1519
2409
|
|
|
2410
|
+
// src/utils/signing-secret.ts
|
|
2411
|
+
import { randomBytes } from "crypto";
|
|
2412
|
+
var SIGNING_SECRET_ENV = "VONPAY_CLI_SIGNING_SECRET";
|
|
2413
|
+
var SIGNING_SECRET_FLAG = "--signing-secret <secret>";
|
|
2414
|
+
function generateSigningSecret() {
|
|
2415
|
+
return `whsec_${randomBytes(32).toString("base64url")}`;
|
|
2416
|
+
}
|
|
2417
|
+
function pinnedSecretProblem(value, from, apiKey) {
|
|
2418
|
+
if (apiKey !== void 0 && apiKey !== "" && value === apiKey || /^(vp_sk_|vp_pk_|vp_key_|sk_|rk_)/.test(value)) {
|
|
2419
|
+
return `${from} looks like an API key. Local deliveries are never signed with an API key: use a separate secret (for example the one this command prints when you leave ${from} out).`;
|
|
2420
|
+
}
|
|
2421
|
+
if (value.length < 16 || /[\s\x00-\x1f\x7f]/.test(value)) {
|
|
2422
|
+
return `${from} must be at least 16 characters with no spaces or control characters.`;
|
|
2423
|
+
}
|
|
2424
|
+
return null;
|
|
2425
|
+
}
|
|
2426
|
+
function resolveSigningSecret(flag, env = process.env, apiKey) {
|
|
2427
|
+
if (flag !== void 0) {
|
|
2428
|
+
const problem = pinnedSecretProblem(flag, "--signing-secret", apiKey);
|
|
2429
|
+
return problem ? { error: problem } : { secret: flag, source: "flag" };
|
|
2430
|
+
}
|
|
2431
|
+
const fromEnv = env[SIGNING_SECRET_ENV];
|
|
2432
|
+
if (fromEnv !== void 0 && fromEnv !== "") {
|
|
2433
|
+
const problem = pinnedSecretProblem(fromEnv, SIGNING_SECRET_ENV, apiKey);
|
|
2434
|
+
return problem ? { error: problem } : { secret: fromEnv, source: "env" };
|
|
2435
|
+
}
|
|
2436
|
+
return { secret: generateSigningSecret(), source: "generated" };
|
|
2437
|
+
}
|
|
2438
|
+
function signingSecretNotice(s) {
|
|
2439
|
+
return s.source === "generated" ? `Signing secret for this run: ${s.secret} \u2014 verify the events with it (constructEvent(rawBody, signatureHeader, secret)). It changes every run; to keep one, pass --signing-secret or set ${SIGNING_SECRET_ENV}.` : `Signing with the secret from ${s.source === "flag" ? "--signing-secret" : SIGNING_SECRET_ENV} \u2014 verify the events with the same value.`;
|
|
2440
|
+
}
|
|
2441
|
+
|
|
1520
2442
|
// src/utils/forward-to.ts
|
|
1521
2443
|
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
1522
2444
|
"localhost",
|
|
@@ -1680,6 +2602,15 @@ var SUPPORTED_EVENTS = [
|
|
|
1680
2602
|
"payment_intent.failed",
|
|
1681
2603
|
"payment_intent.cancelled",
|
|
1682
2604
|
"mirror.order.created",
|
|
2605
|
+
// Opt-in events (an endpoint gets them only when it lists each by name).
|
|
2606
|
+
// Payloads follow checkout's own "Send test event" samples, which are built
|
|
2607
|
+
// by the same code as the real events.
|
|
2608
|
+
"subscription.created",
|
|
2609
|
+
"subscription.renewed",
|
|
2610
|
+
"subscription.payment_failed",
|
|
2611
|
+
"subscription.cancelled",
|
|
2612
|
+
"subscription.updated",
|
|
2613
|
+
"merchant.card_testing_suspected",
|
|
1683
2614
|
"session.succeeded",
|
|
1684
2615
|
"session.failed"
|
|
1685
2616
|
];
|
|
@@ -1691,7 +2622,7 @@ function warnIfUnsubscribable(event, write = (m) => console.warn(m)) {
|
|
|
1691
2622
|
if (!UNSUBSCRIBABLE_EVENTS.includes(event)) return false;
|
|
1692
2623
|
const suggestion = event === "session.succeeded" ? "charge.succeeded" : "charge.failed";
|
|
1693
2624
|
write(
|
|
1694
|
-
|
|
2625
|
+
chalk13.yellow(
|
|
1695
2626
|
`\u26A0 ${event} is emitted internally but is NOT subscribable.
|
|
1696
2627
|
The subscription API accepts the key, stores nothing and returns success, so a
|
|
1697
2628
|
real endpoint receives this event NEVER \u2014 a handler tested only against it is
|
|
@@ -1701,6 +2632,57 @@ function warnIfUnsubscribable(event, write = (m) => console.warn(m)) {
|
|
|
1701
2632
|
return true;
|
|
1702
2633
|
}
|
|
1703
2634
|
var PROCESSOR_CONNECTION = null;
|
|
2635
|
+
function subscriptionEventData(event, paymentIntentId, amount, currency) {
|
|
2636
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
2637
|
+
const inAMonth = now + 30 * 24 * 60 * 60;
|
|
2638
|
+
const base = {
|
|
2639
|
+
subscription_id: `vsub_test_${randomUUID2().replace(/-/g, "").slice(0, 16)}`,
|
|
2640
|
+
status: "active",
|
|
2641
|
+
billing_cycle: null,
|
|
2642
|
+
next_bill_at: inAMonth,
|
|
2643
|
+
amount: null,
|
|
2644
|
+
renewal_amount: amount,
|
|
2645
|
+
currency,
|
|
2646
|
+
payment_intent_id: null,
|
|
2647
|
+
action: null,
|
|
2648
|
+
decline_message: null,
|
|
2649
|
+
decline_code: null,
|
|
2650
|
+
cancel_reason: null,
|
|
2651
|
+
changed_fields: null,
|
|
2652
|
+
// Who made the change (served since checkout's production release of 2026-10-30): an API key
|
|
2653
|
+
// with no id or label on these samples. Set on every event.
|
|
2654
|
+
actor: { type: "api_key", api_key_id: null, label: null },
|
|
2655
|
+
// A cancel scheduled for the end of the paid period, and the discount on future
|
|
2656
|
+
// renewals (served since 2026-10-30): none on these samples.
|
|
2657
|
+
cancel_at_period_end: false,
|
|
2658
|
+
discount: null,
|
|
2659
|
+
// The merchant's own sign-up metadata; `{}` when none was sent.
|
|
2660
|
+
metadata: {}
|
|
2661
|
+
};
|
|
2662
|
+
switch (event) {
|
|
2663
|
+
case "subscription.created":
|
|
2664
|
+
return { ...base, billing_cycle: 1, amount, payment_intent_id: paymentIntentId };
|
|
2665
|
+
case "subscription.renewed":
|
|
2666
|
+
return { ...base, billing_cycle: 2, amount, payment_intent_id: paymentIntentId };
|
|
2667
|
+
case "subscription.payment_failed":
|
|
2668
|
+
return {
|
|
2669
|
+
...base,
|
|
2670
|
+
status: "past_due",
|
|
2671
|
+
billing_cycle: 2,
|
|
2672
|
+
next_bill_at: now + 3 * 24 * 60 * 60,
|
|
2673
|
+
amount,
|
|
2674
|
+
payment_intent_id: paymentIntentId,
|
|
2675
|
+
// The same three values `charge.failed` carries for this code.
|
|
2676
|
+
action: "soft",
|
|
2677
|
+
decline_message: "The card was declined for insufficient funds.",
|
|
2678
|
+
decline_code: "insufficient_funds"
|
|
2679
|
+
};
|
|
2680
|
+
case "subscription.cancelled":
|
|
2681
|
+
return { ...base, status: "cancelled", next_bill_at: null, cancel_reason: "too_expensive" };
|
|
2682
|
+
case "subscription.updated":
|
|
2683
|
+
return { ...base, renewal_amount: Math.max(1, Math.floor(amount * 0.9)), changed_fields: ["renewal_amount"] };
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
1704
2686
|
function buildEventData(event, ids, amount, currency) {
|
|
1705
2687
|
const base = {
|
|
1706
2688
|
session_id: ids.sessionId,
|
|
@@ -1709,13 +2691,14 @@ function buildEventData(event, ids, amount, currency) {
|
|
|
1709
2691
|
amount,
|
|
1710
2692
|
currency
|
|
1711
2693
|
};
|
|
2694
|
+
const notARenewal = { subscription_id: null, billing_cycle: null };
|
|
1712
2695
|
switch (event) {
|
|
1713
2696
|
case "session.succeeded":
|
|
1714
2697
|
return base;
|
|
1715
2698
|
case "payment_intent.succeeded":
|
|
1716
|
-
return { ...base, processor_connection: PROCESSOR_CONNECTION };
|
|
2699
|
+
return { ...base, processor_connection: PROCESSOR_CONNECTION, ...notARenewal };
|
|
1717
2700
|
case "charge.succeeded":
|
|
1718
|
-
return { ...base, vp_tx_id: ids.transactionId, card: null, processor_connection: PROCESSOR_CONNECTION };
|
|
2701
|
+
return { ...base, vp_tx_id: ids.transactionId, card: null, processor_connection: PROCESSOR_CONNECTION, ...notARenewal };
|
|
1719
2702
|
case "session.failed":
|
|
1720
2703
|
case "payment_intent.failed":
|
|
1721
2704
|
return {
|
|
@@ -1727,8 +2710,8 @@ function buildEventData(event, ids, amount, currency) {
|
|
|
1727
2710
|
// could be read (as on this ordinary decline) — never permission to retry.
|
|
1728
2711
|
network_advice: null,
|
|
1729
2712
|
network_advice_retry_after_ms: null,
|
|
1730
|
-
// payment_intent.failed only: session.failed
|
|
1731
|
-
...event === "payment_intent.failed" ? { processor_connection: PROCESSOR_CONNECTION } : {}
|
|
2713
|
+
// payment_intent.failed only: session.failed sends neither.
|
|
2714
|
+
...event === "payment_intent.failed" ? { processor_connection: PROCESSOR_CONNECTION, ...notARenewal } : {}
|
|
1732
2715
|
};
|
|
1733
2716
|
case "charge.failed":
|
|
1734
2717
|
return {
|
|
@@ -1744,7 +2727,8 @@ function buildEventData(event, ids, amount, currency) {
|
|
|
1744
2727
|
// Same two fields and values as payment_intent.failed / session.failed.
|
|
1745
2728
|
network_advice: null,
|
|
1746
2729
|
network_advice_retry_after_ms: null,
|
|
1747
|
-
processor_connection: PROCESSOR_CONNECTION
|
|
2730
|
+
processor_connection: PROCESSOR_CONNECTION,
|
|
2731
|
+
...notARenewal
|
|
1748
2732
|
};
|
|
1749
2733
|
case "mirror.order.created":
|
|
1750
2734
|
return {
|
|
@@ -1753,7 +2737,28 @@ function buildEventData(event, ids, amount, currency) {
|
|
|
1753
2737
|
order_status_url: null
|
|
1754
2738
|
};
|
|
1755
2739
|
case "payment_intent.cancelled":
|
|
1756
|
-
return { ...base, cancellation_reason: "buyer_abandoned", processor_connection: PROCESSOR_CONNECTION };
|
|
2740
|
+
return { ...base, cancellation_reason: "buyer_abandoned", processor_connection: PROCESSOR_CONNECTION, ...notARenewal };
|
|
2741
|
+
case "subscription.created":
|
|
2742
|
+
case "subscription.renewed":
|
|
2743
|
+
case "subscription.payment_failed":
|
|
2744
|
+
case "subscription.cancelled":
|
|
2745
|
+
case "subscription.updated":
|
|
2746
|
+
return subscriptionEventData(event, ids.paymentIntentId, amount, currency);
|
|
2747
|
+
case "merchant.card_testing_suspected": {
|
|
2748
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
2749
|
+
return {
|
|
2750
|
+
patterns: ["expiry_enumeration", "refusal_burst"],
|
|
2751
|
+
window_start: now - 60 * 60,
|
|
2752
|
+
window_end: now,
|
|
2753
|
+
attempts: 30,
|
|
2754
|
+
refused: 24,
|
|
2755
|
+
accepted: 3,
|
|
2756
|
+
distinct_cards: 12,
|
|
2757
|
+
card: { brand: "visa", last4: "4242" },
|
|
2758
|
+
expiry_variants: 9,
|
|
2759
|
+
blocked: false
|
|
2760
|
+
};
|
|
2761
|
+
}
|
|
1757
2762
|
case "charge.refunded":
|
|
1758
2763
|
return {
|
|
1759
2764
|
...base,
|
|
@@ -1768,7 +2773,8 @@ function buildEventData(event, ids, amount, currency) {
|
|
|
1768
2773
|
is_partial: false,
|
|
1769
2774
|
original_charge_amount: amount,
|
|
1770
2775
|
card: null,
|
|
1771
|
-
processor_connection: PROCESSOR_CONNECTION
|
|
2776
|
+
processor_connection: PROCESSOR_CONNECTION,
|
|
2777
|
+
...notARenewal
|
|
1772
2778
|
};
|
|
1773
2779
|
case "refund.failed":
|
|
1774
2780
|
return {
|
|
@@ -1814,10 +2820,13 @@ function triggerCommand() {
|
|
|
1814
2820
|
return new Command9("trigger").description("Send a test webhook event to a local URL").argument("<event>", `Event type (${SUPPORTED_EVENTS.join(", ")})`).requiredOption("--url <url>", "Local webhook URL (e.g. http://localhost:3000/webhooks)").option("--session-id <id>", "Custom session ID").option("--amount <cents>", "Amount in minor units, a whole number").option("--currency <code>", "Currency code").option(
|
|
1815
2821
|
"--url-tunnel",
|
|
1816
2822
|
"Allow a non-loopback --url (ngrok / Cloudflare Tunnel). Without it, --url must be this machine"
|
|
2823
|
+
).option(
|
|
2824
|
+
SIGNING_SECRET_FLAG,
|
|
2825
|
+
`Sign the event with this secret instead of a new random one for this run (or set ${SIGNING_SECRET_ENV}); your handler verifies with the same value`
|
|
1817
2826
|
).addOption(retiredConfirmLiveOption()).action(async (event, opts) => {
|
|
1818
2827
|
if (!SUPPORTED_EVENTS.includes(event)) {
|
|
1819
2828
|
console.error(
|
|
1820
|
-
|
|
2829
|
+
chalk13.red(
|
|
1821
2830
|
`Unknown event: ${event}
|
|
1822
2831
|
Supported: ${SUPPORTED_EVENTS.join(", ")}`
|
|
1823
2832
|
)
|
|
@@ -1831,15 +2840,20 @@ Supported: ${SUPPORTED_EVENTS.join(", ")}`
|
|
|
1831
2840
|
tunnelFlag: "--url-tunnel"
|
|
1832
2841
|
});
|
|
1833
2842
|
if (!destination.ok) {
|
|
1834
|
-
console.error(
|
|
2843
|
+
console.error(chalk13.red(destination.reason));
|
|
1835
2844
|
process.exit(1);
|
|
1836
2845
|
}
|
|
1837
|
-
if (destination.warning) console.warn(
|
|
2846
|
+
if (destination.warning) console.warn(chalk13.yellow(destination.warning));
|
|
1838
2847
|
const amount = optionalMinorUnitsOrExit(
|
|
1839
2848
|
opts.amount === void 0 ? void 0 : String(opts.amount),
|
|
1840
2849
|
"--amount"
|
|
1841
2850
|
);
|
|
1842
2851
|
const apiKey = requireApiKey();
|
|
2852
|
+
const signing = resolveSigningSecret(opts.signingSecret, process.env, apiKey);
|
|
2853
|
+
if ("error" in signing) {
|
|
2854
|
+
console.error(chalk13.red(signing.error));
|
|
2855
|
+
process.exit(1);
|
|
2856
|
+
}
|
|
1843
2857
|
await confirmLiveAction(
|
|
1844
2858
|
apiKey,
|
|
1845
2859
|
{
|
|
@@ -1848,14 +2862,15 @@ Supported: ${SUPPORTED_EVENTS.join(", ")}`
|
|
|
1848
2862
|
["Event", event],
|
|
1849
2863
|
["Sent to", String(opts.url)]
|
|
1850
2864
|
],
|
|
1851
|
-
consequence: "
|
|
2865
|
+
consequence: "A synthetic event is sent to this URL, signed with this run's signing secret (never your API key)."
|
|
1852
2866
|
},
|
|
1853
2867
|
opts
|
|
1854
2868
|
);
|
|
1855
2869
|
const payload = buildPayload(event, { ...opts, amount });
|
|
1856
2870
|
const body = JSON.stringify(payload);
|
|
2871
|
+
console.log(chalk13.dim(signingSecretNotice(signing)));
|
|
1857
2872
|
const t = Math.floor(Date.now() / 1e3);
|
|
1858
|
-
const v1 = createHmac2("sha256",
|
|
2873
|
+
const v1 = createHmac2("sha256", signing.secret).update(`${t}.${body}`).digest("hex");
|
|
1859
2874
|
const signatureHeader = `t=${t},v1=${v1}`;
|
|
1860
2875
|
const spinner = ora2(`Sending ${event} to ${opts.url}...`).start();
|
|
1861
2876
|
try {
|
|
@@ -1875,45 +2890,45 @@ Supported: ${SUPPORTED_EVENTS.join(", ")}`
|
|
|
1875
2890
|
spinner.stop();
|
|
1876
2891
|
if (isRedirect(response)) {
|
|
1877
2892
|
console.error(
|
|
1878
|
-
|
|
2893
|
+
chalk13.red(
|
|
1879
2894
|
`\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.`
|
|
1880
2895
|
)
|
|
1881
2896
|
);
|
|
1882
2897
|
process.exit(1);
|
|
1883
2898
|
}
|
|
1884
2899
|
if (response.ok) {
|
|
1885
|
-
console.log(
|
|
2900
|
+
console.log(chalk13.green(`\u2713 ${event} delivered \u2192 ${response.status}`));
|
|
1886
2901
|
} else {
|
|
1887
2902
|
console.log(
|
|
1888
|
-
|
|
2903
|
+
chalk13.yellow(
|
|
1889
2904
|
`\u26A0 ${event} delivered but got ${response.status} response`
|
|
1890
2905
|
)
|
|
1891
2906
|
);
|
|
1892
2907
|
}
|
|
1893
|
-
console.log(
|
|
2908
|
+
console.log(chalk13.dim(` Event: ${payload.id} (${payload.type})`));
|
|
1894
2909
|
console.log(
|
|
1895
|
-
|
|
2910
|
+
chalk13.dim(
|
|
1896
2911
|
` Amount: ${payload.data.amount} ${payload.data.currency}`
|
|
1897
2912
|
)
|
|
1898
2913
|
);
|
|
1899
2914
|
console.log(
|
|
1900
|
-
|
|
2915
|
+
chalk13.dim(
|
|
1901
2916
|
`
|
|
1902
2917
|
Verify with: vonpay.webhooks.constructEvent(rawBody, signatureHeader, secret)`
|
|
1903
2918
|
)
|
|
1904
2919
|
);
|
|
1905
2920
|
console.log(
|
|
1906
|
-
|
|
1907
|
-
` (
|
|
2921
|
+
chalk13.dim(
|
|
2922
|
+
` ('secret' is this run's signing secret, printed above \u2014 production webhooks are signed with your endpoint's whsec_\u2026 secret, and never with your API key)`
|
|
1908
2923
|
)
|
|
1909
2924
|
);
|
|
1910
2925
|
} catch (err) {
|
|
1911
2926
|
spinner.stop();
|
|
1912
2927
|
if (err instanceof Error && err.name === "TimeoutError") {
|
|
1913
|
-
console.error(
|
|
2928
|
+
console.error(chalk13.red(`Timeout: ${opts.url} did not respond within 10s`));
|
|
1914
2929
|
} else {
|
|
1915
2930
|
console.error(
|
|
1916
|
-
|
|
2931
|
+
chalk13.red(
|
|
1917
2932
|
`Failed to deliver: ${err instanceof Error ? err.message : err}`
|
|
1918
2933
|
)
|
|
1919
2934
|
);
|
|
@@ -2025,9 +3040,9 @@ function resumeId(lastId) {
|
|
|
2025
3040
|
async function* consumeSse(opts) {
|
|
2026
3041
|
const initialBackoffMs = opts.initialBackoffMs ?? 1e3;
|
|
2027
3042
|
const maxBackoffMs = opts.maxBackoffMs ?? 3e4;
|
|
2028
|
-
const
|
|
3043
|
+
const pending2 = [];
|
|
2029
3044
|
const parser = new SseParser(
|
|
2030
|
-
(type, data) =>
|
|
3045
|
+
(type, data) => pending2.push({ type, data, lastEventId: parser.lastId || void 0 }),
|
|
2031
3046
|
{ maxPending: MAX_PENDING_CHARS, maxIdLength: MAX_EVENT_ID_LENGTH }
|
|
2032
3047
|
);
|
|
2033
3048
|
let backoffMs = initialBackoffMs;
|
|
@@ -2050,7 +3065,10 @@ async function* consumeSse(opts) {
|
|
|
2050
3065
|
response = await fetch(opts.url, {
|
|
2051
3066
|
method: "GET",
|
|
2052
3067
|
headers,
|
|
2053
|
-
signal: opts.signal
|
|
3068
|
+
signal: opts.signal,
|
|
3069
|
+
// Never follow a redirect: the request carries the API key, and a 3xx
|
|
3070
|
+
// would re-send it to wherever the answer points.
|
|
3071
|
+
redirect: "manual"
|
|
2054
3072
|
});
|
|
2055
3073
|
} catch (err) {
|
|
2056
3074
|
if (opts.signal.aborted) return;
|
|
@@ -2060,6 +3078,11 @@ async function* consumeSse(opts) {
|
|
|
2060
3078
|
backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
|
|
2061
3079
|
continue;
|
|
2062
3080
|
}
|
|
3081
|
+
if (response.status >= 300 && response.status < 400) {
|
|
3082
|
+
const error = new SseRedirectError(response.status);
|
|
3083
|
+
onLifecycle({ kind: "fatal", error });
|
|
3084
|
+
throw error;
|
|
3085
|
+
}
|
|
2063
3086
|
if (response.status === 401 || response.status === 403) {
|
|
2064
3087
|
const body = await safeReadText(response);
|
|
2065
3088
|
onLifecycle({ kind: "auth_failed", status: response.status, body });
|
|
@@ -2103,7 +3126,7 @@ async function* consumeSse(opts) {
|
|
|
2103
3126
|
} catch (err) {
|
|
2104
3127
|
feedError = err;
|
|
2105
3128
|
}
|
|
2106
|
-
for (const ev of
|
|
3129
|
+
for (const ev of pending2.splice(0)) {
|
|
2107
3130
|
if (ev.type === "timeout") {
|
|
2108
3131
|
serverInitiatedReconnect = true;
|
|
2109
3132
|
} else {
|
|
@@ -2171,15 +3194,25 @@ var SseAuthError = class extends Error {
|
|
|
2171
3194
|
status;
|
|
2172
3195
|
body;
|
|
2173
3196
|
};
|
|
3197
|
+
var SseRedirectError = class extends Error {
|
|
3198
|
+
constructor(status) {
|
|
3199
|
+
super(
|
|
3200
|
+
`The event stream answered with a redirect (HTTP ${status}). It was not followed, so your API key was not sent anywhere else. Check VONPAY_BASE_URL.`
|
|
3201
|
+
);
|
|
3202
|
+
this.status = status;
|
|
3203
|
+
this.name = "SseRedirectError";
|
|
3204
|
+
}
|
|
3205
|
+
status;
|
|
3206
|
+
};
|
|
2174
3207
|
function sleep(ms, signal) {
|
|
2175
|
-
return new Promise((
|
|
3208
|
+
return new Promise((resolve3) => {
|
|
2176
3209
|
const timer = setTimeout(() => {
|
|
2177
3210
|
signal.removeEventListener("abort", onAbort);
|
|
2178
|
-
|
|
3211
|
+
resolve3();
|
|
2179
3212
|
}, ms);
|
|
2180
3213
|
const onAbort = () => {
|
|
2181
3214
|
clearTimeout(timer);
|
|
2182
|
-
|
|
3215
|
+
resolve3();
|
|
2183
3216
|
};
|
|
2184
3217
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
2185
3218
|
});
|
|
@@ -2198,24 +3231,38 @@ function noop() {
|
|
|
2198
3231
|
}
|
|
2199
3232
|
|
|
2200
3233
|
// src/utils/listen-output.ts
|
|
2201
|
-
import
|
|
3234
|
+
import chalk14 from "chalk";
|
|
3235
|
+
function parseAttemptPayload(data) {
|
|
3236
|
+
let raw;
|
|
3237
|
+
try {
|
|
3238
|
+
raw = JSON.parse(data);
|
|
3239
|
+
} catch {
|
|
3240
|
+
return null;
|
|
3241
|
+
}
|
|
3242
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
3243
|
+
const r = raw;
|
|
3244
|
+
const str2 = (k) => typeof r[k] === "string";
|
|
3245
|
+
const num = (k) => typeof r[k] === "number" && Number.isFinite(r[k]);
|
|
3246
|
+
const ok = str2("event_id") && str2("event_type") && str2("subscription_id") && str2("timestamp") && num("attempt") && num("latency_ms") && typeof r["succeeded"] === "boolean" && typeof r["livemode"] === "boolean" && (r["response_status"] === null || num("response_status"));
|
|
3247
|
+
return ok ? r : null;
|
|
3248
|
+
}
|
|
2202
3249
|
function formatAttemptHuman(payload, forward) {
|
|
2203
3250
|
const ts = formatTimestamp(payload.timestamp);
|
|
2204
|
-
const eventType =
|
|
2205
|
-
const eventId =
|
|
2206
|
-
const attempt =
|
|
2207
|
-
const arrow =
|
|
3251
|
+
const eventType = chalk14.cyan(safeInline(payload.event_type, 64).padEnd(24));
|
|
3252
|
+
const eventId = chalk14.gray(safeInline(payload.event_id, 64).padEnd(28));
|
|
3253
|
+
const attempt = chalk14.gray(`attempt ${payload.attempt}`);
|
|
3254
|
+
const arrow = chalk14.gray("\u2192");
|
|
2208
3255
|
const upstream = formatUpstream(payload);
|
|
2209
|
-
const latency =
|
|
2210
|
-
const liveBadge = payload.livemode ?
|
|
3256
|
+
const latency = chalk14.gray(`${payload.latency_ms}ms`);
|
|
3257
|
+
const liveBadge = payload.livemode ? chalk14.red(" [LIVE]") : "";
|
|
2211
3258
|
let line = `[${ts}] ${eventType} ${eventId} ${attempt} ${arrow} ${upstream} ${latency}${liveBadge}`;
|
|
2212
3259
|
if (forward && forward.attempted) {
|
|
2213
|
-
const fwdArrow =
|
|
3260
|
+
const fwdArrow = chalk14.gray("\u2192 local");
|
|
2214
3261
|
if (forward.error) {
|
|
2215
|
-
line += ` ${fwdArrow} ${
|
|
3262
|
+
line += ` ${fwdArrow} ${chalk14.red(safeInline(forward.error))}`;
|
|
2216
3263
|
} else if (forward.status !== null) {
|
|
2217
3264
|
const fwdStatus = formatStatus(forward.status);
|
|
2218
|
-
const fwdLatency =
|
|
3265
|
+
const fwdLatency = chalk14.gray(`${forward.latencyMs}ms`);
|
|
2219
3266
|
line += ` ${fwdArrow} ${fwdStatus} ${fwdLatency}`;
|
|
2220
3267
|
}
|
|
2221
3268
|
}
|
|
@@ -2224,47 +3271,47 @@ function formatAttemptHuman(payload, forward) {
|
|
|
2224
3271
|
function formatAttemptJson(payload, forward) {
|
|
2225
3272
|
const body = {
|
|
2226
3273
|
type: "webhook.attempt",
|
|
2227
|
-
event_id: payload.event_id,
|
|
2228
|
-
event_type: payload.event_type,
|
|
2229
|
-
subscription_id: payload.subscription_id,
|
|
3274
|
+
event_id: safeInline(payload.event_id, 200),
|
|
3275
|
+
event_type: safeInline(payload.event_type, 200),
|
|
3276
|
+
subscription_id: safeInline(payload.subscription_id, 200),
|
|
2230
3277
|
attempt: payload.attempt,
|
|
2231
3278
|
succeeded: payload.succeeded,
|
|
2232
3279
|
response_status: payload.response_status,
|
|
2233
3280
|
latency_ms: payload.latency_ms,
|
|
2234
|
-
timestamp: payload.timestamp,
|
|
3281
|
+
timestamp: safeInline(payload.timestamp, 64),
|
|
2235
3282
|
livemode: payload.livemode
|
|
2236
3283
|
};
|
|
2237
3284
|
if (forward && forward.attempted) {
|
|
2238
3285
|
body.forward = {
|
|
2239
3286
|
status: forward.status,
|
|
2240
3287
|
latency_ms: forward.latencyMs,
|
|
2241
|
-
error: forward.error
|
|
3288
|
+
error: forward.error === null ? null : safeInline(forward.error)
|
|
2242
3289
|
};
|
|
2243
3290
|
}
|
|
2244
3291
|
return JSON.stringify(body);
|
|
2245
3292
|
}
|
|
2246
3293
|
function formatLifecycleHuman(message, kind) {
|
|
2247
|
-
const tag = kind === "info" ?
|
|
3294
|
+
const tag = kind === "info" ? chalk14.cyan("[vonpay]") : kind === "warn" ? chalk14.yellow("[vonpay]") : chalk14.red("[vonpay]");
|
|
2248
3295
|
return `${tag} ${message}`;
|
|
2249
3296
|
}
|
|
2250
3297
|
function formatLifecycleJson(kind, extra = {}) {
|
|
2251
3298
|
return JSON.stringify({ type: `cli.${kind}`, ...extra, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2252
3299
|
}
|
|
2253
3300
|
function formatStatus(status) {
|
|
2254
|
-
if (status >= 200 && status < 300) return
|
|
2255
|
-
if (status >= 400 && status < 500) return
|
|
2256
|
-
if (status >= 500) return
|
|
2257
|
-
return
|
|
3301
|
+
if (status >= 200 && status < 300) return chalk14.green(String(status));
|
|
3302
|
+
if (status >= 400 && status < 500) return chalk14.yellow(String(status));
|
|
3303
|
+
if (status >= 500) return chalk14.red(String(status));
|
|
3304
|
+
return chalk14.gray(String(status));
|
|
2258
3305
|
}
|
|
2259
3306
|
function formatUpstream(payload) {
|
|
2260
3307
|
if (payload.response_status === null) {
|
|
2261
|
-
return
|
|
3308
|
+
return chalk14.red("timeout");
|
|
2262
3309
|
}
|
|
2263
3310
|
return formatStatus(payload.response_status);
|
|
2264
3311
|
}
|
|
2265
3312
|
function formatTimestamp(iso) {
|
|
2266
3313
|
const date = new Date(iso);
|
|
2267
|
-
if (isNaN(date.getTime())) return iso
|
|
3314
|
+
if (isNaN(date.getTime())) return safeInline(iso, 19);
|
|
2268
3315
|
const h = String(date.getHours()).padStart(2, "0");
|
|
2269
3316
|
const m = String(date.getMinutes()).padStart(2, "0");
|
|
2270
3317
|
const s = String(date.getSeconds()).padStart(2, "0");
|
|
@@ -2285,7 +3332,10 @@ function listenCommand() {
|
|
|
2285
3332
|
"--events <type>",
|
|
2286
3333
|
"Only stream attempts for the given event type. Repeat the flag for multiple types.",
|
|
2287
3334
|
(value, prev) => prev ? [...prev, value] : [value]
|
|
2288
|
-
).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").
|
|
3335
|
+
).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").option(
|
|
3336
|
+
SIGNING_SECRET_FLAG,
|
|
3337
|
+
`With --forward-to: sign forwarded events with this secret instead of a new random one for this run (or set ${SIGNING_SECRET_ENV})`
|
|
3338
|
+
).addOption(retiredConfirmLiveOption()).action(async (opts) => {
|
|
2289
3339
|
const apiKey = opts.apiKey ?? requireApiKey();
|
|
2290
3340
|
if (opts.apiKey !== void 0) warnLiveSecretFlag(opts.apiKey, "--api-key");
|
|
2291
3341
|
const refuse = (message) => {
|
|
@@ -2308,6 +3358,12 @@ function listenCommand() {
|
|
|
2308
3358
|
forwardUrl = verdict.url;
|
|
2309
3359
|
forwardWarning = verdict.warning;
|
|
2310
3360
|
}
|
|
3361
|
+
let signing;
|
|
3362
|
+
if (forwardUrl) {
|
|
3363
|
+
const resolved = resolveSigningSecret(opts.signingSecret, process.env, apiKey);
|
|
3364
|
+
if ("error" in resolved) refuse(resolved.error);
|
|
3365
|
+
signing = resolved;
|
|
3366
|
+
}
|
|
2311
3367
|
const configuredBaseUrl = getBaseUrl();
|
|
2312
3368
|
const destinationProblem = keyDestinationProblem(apiKey, configuredBaseUrl);
|
|
2313
3369
|
if (destinationProblem) refuse(destinationProblem);
|
|
@@ -2323,7 +3379,7 @@ function listenCommand() {
|
|
|
2323
3379
|
["Stream from", baseUrl],
|
|
2324
3380
|
...forwardUrl ? [["Forward to", forwardUrl.toString()]] : []
|
|
2325
3381
|
],
|
|
2326
|
-
consequence: forwardUrl ? "Live payment events, with buyer and order details, will be shown here, and each one is signed with
|
|
3382
|
+
consequence: forwardUrl ? "Live payment events, with buyer and order details, will be shown here, and each one is sent to the forward URL (signed with this run's signing secret, never your API key)." : "Live payment events, with buyer and order details, will be shown here."
|
|
2327
3383
|
},
|
|
2328
3384
|
opts
|
|
2329
3385
|
);
|
|
@@ -2362,6 +3418,7 @@ function listenCommand() {
|
|
|
2362
3418
|
}
|
|
2363
3419
|
controller.abort();
|
|
2364
3420
|
};
|
|
3421
|
+
auditSignalHandledByCommand();
|
|
2365
3422
|
process.once("SIGINT", () => onSignal("SIGINT"));
|
|
2366
3423
|
process.once("SIGTERM", () => onSignal("SIGTERM"));
|
|
2367
3424
|
const onLifecycle = (event) => {
|
|
@@ -2384,12 +3441,16 @@ function listenCommand() {
|
|
|
2384
3441
|
process.stderr.write(
|
|
2385
3442
|
formatLifecycleHuman(`Ready! Listening for webhook delivery attempts${filterNote}${fwdNote}.`, "info") + "\n"
|
|
2386
3443
|
);
|
|
3444
|
+
if (signing) process.stderr.write(formatLifecycleHuman(signingSecretNotice(signing), "info") + "\n");
|
|
2387
3445
|
} else {
|
|
2388
3446
|
process.stderr.write(
|
|
2389
3447
|
formatLifecycleJson("ready", {
|
|
2390
3448
|
base_url: baseUrl,
|
|
2391
3449
|
filter: opts.events ?? null,
|
|
2392
|
-
forward_to: forwardUrl?.toString() ?? null
|
|
3450
|
+
forward_to: forwardUrl?.toString() ?? null,
|
|
3451
|
+
// The generated secret in full (the local receiver needs it); a pinned one by source only.
|
|
3452
|
+
signing_secret: signing?.source === "generated" ? signing.secret : null,
|
|
3453
|
+
signing_secret_source: signing?.source ?? null
|
|
2393
3454
|
}) + "\n"
|
|
2394
3455
|
);
|
|
2395
3456
|
}
|
|
@@ -2411,12 +3472,8 @@ function listenCommand() {
|
|
|
2411
3472
|
if (sseEvent.type !== "webhook.attempt") {
|
|
2412
3473
|
continue;
|
|
2413
3474
|
}
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
payload = JSON.parse(sseEvent.data);
|
|
2417
|
-
} catch {
|
|
2418
|
-
continue;
|
|
2419
|
-
}
|
|
3475
|
+
const payload = parseAttemptPayload(sseEvent.data);
|
|
3476
|
+
if (payload === null) continue;
|
|
2420
3477
|
eventCount += 1;
|
|
2421
3478
|
let forwardOutcome = null;
|
|
2422
3479
|
if (forwardUrl && sdk) {
|
|
@@ -2424,7 +3481,7 @@ function listenCommand() {
|
|
|
2424
3481
|
const eventRecord = await sdk.webhookEvents.retrieve(payload.event_id);
|
|
2425
3482
|
forwardOutcome = await forwardPayload(eventRecord.payload, {
|
|
2426
3483
|
url: forwardUrl,
|
|
2427
|
-
signingSecret:
|
|
3484
|
+
signingSecret: signing.secret,
|
|
2428
3485
|
signal: controller.signal
|
|
2429
3486
|
});
|
|
2430
3487
|
} catch (err) {
|
|
@@ -2445,6 +3502,7 @@ function listenCommand() {
|
|
|
2445
3502
|
}
|
|
2446
3503
|
} catch (err) {
|
|
2447
3504
|
if (err instanceof SseAuthError) {
|
|
3505
|
+
auditNote({ outcome: "error", code: "auth_failed" });
|
|
2448
3506
|
if (mode === "human") {
|
|
2449
3507
|
process.stderr.write(
|
|
2450
3508
|
formatLifecycleHuman(
|
|
@@ -2459,6 +3517,10 @@ function listenCommand() {
|
|
|
2459
3517
|
}
|
|
2460
3518
|
process.exit(1);
|
|
2461
3519
|
}
|
|
3520
|
+
if (err instanceof SseRedirectError) {
|
|
3521
|
+
if (jsonErrorsEnabled()) exitWithCliError(err.message, err.message);
|
|
3522
|
+
process.exit(1);
|
|
3523
|
+
}
|
|
2462
3524
|
if (jsonErrorsEnabled()) handleError(err);
|
|
2463
3525
|
throw err;
|
|
2464
3526
|
}
|
|
@@ -2467,29 +3529,29 @@ function listenCommand() {
|
|
|
2467
3529
|
function lifecyclePayload(event) {
|
|
2468
3530
|
switch (event.kind) {
|
|
2469
3531
|
case "connecting":
|
|
2470
|
-
return { last_event_id: event.lastEventId
|
|
3532
|
+
return { last_event_id: event.lastEventId === void 0 ? null : safeInline(event.lastEventId, 200) };
|
|
2471
3533
|
case "connected":
|
|
2472
3534
|
return {};
|
|
2473
3535
|
case "disconnected":
|
|
2474
|
-
return { reason: event.reason, will_retry_in_ms: event.willRetryInMs };
|
|
3536
|
+
return { reason: safeInline(event.reason, 400), will_retry_in_ms: event.willRetryInMs };
|
|
2475
3537
|
case "auth_failed":
|
|
2476
3538
|
return { status: event.status };
|
|
2477
3539
|
case "fatal":
|
|
2478
|
-
return { error: event.error.message };
|
|
3540
|
+
return { error: safeInline(event.error.message, 400) };
|
|
2479
3541
|
}
|
|
2480
3542
|
}
|
|
2481
3543
|
function lifecycleHumanMessage(event) {
|
|
2482
3544
|
switch (event.kind) {
|
|
2483
3545
|
case "connecting":
|
|
2484
|
-
return event.lastEventId ? `Reconnecting (resume from ${event.lastEventId})` : "Connecting...";
|
|
3546
|
+
return event.lastEventId ? `Reconnecting (resume from ${safeInline(event.lastEventId, 200)})` : "Connecting...";
|
|
2485
3547
|
case "connected":
|
|
2486
3548
|
return "Connected.";
|
|
2487
3549
|
case "disconnected":
|
|
2488
|
-
return `Disconnected: ${event.reason}. Retrying in ${event.willRetryInMs}ms.`;
|
|
3550
|
+
return `Disconnected: ${safeInline(event.reason, 400)}. Retrying in ${event.willRetryInMs}ms.`;
|
|
2489
3551
|
case "auth_failed":
|
|
2490
3552
|
return `Authentication failed (HTTP ${event.status}).`;
|
|
2491
3553
|
case "fatal":
|
|
2492
|
-
return `Fatal: ${event.error.message}`;
|
|
3554
|
+
return `Fatal: ${safeInline(event.error.message, 400)}`;
|
|
2493
3555
|
}
|
|
2494
3556
|
}
|
|
2495
3557
|
function lifecycleSeverity(kind) {
|
|
@@ -2507,8 +3569,8 @@ function lifecycleSeverity(kind) {
|
|
|
2507
3569
|
|
|
2508
3570
|
// src/commands/health.ts
|
|
2509
3571
|
import { Command as Command11 } from "commander";
|
|
2510
|
-
import
|
|
2511
|
-
import { VonPayError as
|
|
3572
|
+
import chalk15 from "chalk";
|
|
3573
|
+
import { VonPayError as VonPayError3 } from "@vonpay/checkout-node";
|
|
2512
3574
|
function healthCommand() {
|
|
2513
3575
|
return new Command11("health").description("Check checkout API health").option("--json", "Output raw JSON").action(async (opts) => {
|
|
2514
3576
|
const client = makeCheckoutClient();
|
|
@@ -2519,23 +3581,24 @@ function healthCommand() {
|
|
|
2519
3581
|
if (opts.json) {
|
|
2520
3582
|
console.log(JSON.stringify(health, null, 2));
|
|
2521
3583
|
} else {
|
|
2522
|
-
const statusColor = health.status === "ok" ?
|
|
2523
|
-
console.log(` Status: ${statusColor(health.status)}`);
|
|
2524
|
-
console.log(` Latency: ${health.latencyMs}ms`);
|
|
3584
|
+
const statusColor = health.status === "ok" ? chalk15.green : health.status === "degraded" ? chalk15.yellow : chalk15.red;
|
|
3585
|
+
console.log(` Status: ${statusColor(safeInline(String(health.status)))}`);
|
|
3586
|
+
console.log(` Latency: ${safeInline(String(health.latencyMs))}ms`);
|
|
2525
3587
|
if (health.version) {
|
|
2526
|
-
console.log(` Version: ${health.version}`);
|
|
3588
|
+
console.log(` Version: ${safeInline(String(health.version))}`);
|
|
2527
3589
|
}
|
|
2528
3590
|
}
|
|
2529
3591
|
} catch (err) {
|
|
2530
3592
|
spinner.stop();
|
|
3593
|
+
auditError(err);
|
|
2531
3594
|
if (jsonErrorsEnabled()) handleError(err);
|
|
2532
|
-
if (err instanceof
|
|
2533
|
-
console.error(
|
|
2534
|
-
console.error(
|
|
3595
|
+
if (err instanceof VonPayError3) {
|
|
3596
|
+
console.error(chalk15.red(`API error: ${scrubSecrets(safeInline(err.message, 2e3))}`));
|
|
3597
|
+
console.error(chalk15.dim(` Code: ${safeInline(String(err.code))}`));
|
|
2535
3598
|
} else {
|
|
2536
3599
|
console.error(
|
|
2537
|
-
|
|
2538
|
-
`Cannot reach API: ${scrubSecrets(err instanceof Error ? err.message : String(err))}`
|
|
3600
|
+
chalk15.red(
|
|
3601
|
+
`Cannot reach API: ${scrubSecrets(safeInline(err instanceof Error ? err.message : String(err), 2e3))}`
|
|
2539
3602
|
)
|
|
2540
3603
|
);
|
|
2541
3604
|
}
|
|
@@ -2546,13 +3609,13 @@ function healthCommand() {
|
|
|
2546
3609
|
|
|
2547
3610
|
// src/commands/doctor.ts
|
|
2548
3611
|
import { Command as Command12 } from "commander";
|
|
2549
|
-
import
|
|
3612
|
+
import chalk16 from "chalk";
|
|
2550
3613
|
import ora3 from "ora";
|
|
2551
|
-
import { homedir as
|
|
3614
|
+
import { homedir as homedir3 } from "os";
|
|
2552
3615
|
import { existsSync as existsSync3 } from "fs";
|
|
2553
3616
|
import {
|
|
2554
3617
|
VonPayCheckout as VonPayCheckout3,
|
|
2555
|
-
VonPayError as
|
|
3618
|
+
VonPayError as VonPayError4,
|
|
2556
3619
|
helpFor
|
|
2557
3620
|
} from "@vonpay/checkout-node";
|
|
2558
3621
|
var RELEVANT_ENV_VARS = [
|
|
@@ -2564,7 +3627,7 @@ var RELEVANT_ENV_VARS = [
|
|
|
2564
3627
|
"NEXT_PUBLIC_BASE_URL"
|
|
2565
3628
|
];
|
|
2566
3629
|
function redactHome(p) {
|
|
2567
|
-
const home =
|
|
3630
|
+
const home = homedir3();
|
|
2568
3631
|
if (home && p.startsWith(home)) {
|
|
2569
3632
|
return "~" + p.slice(home.length);
|
|
2570
3633
|
}
|
|
@@ -2699,7 +3762,7 @@ async function gather() {
|
|
|
2699
3762
|
);
|
|
2700
3763
|
}
|
|
2701
3764
|
} catch (err) {
|
|
2702
|
-
if (err instanceof
|
|
3765
|
+
if (err instanceof VonPayError4) {
|
|
2703
3766
|
const help = helpFor(err.code);
|
|
2704
3767
|
report.validateProbe = {
|
|
2705
3768
|
ok: false,
|
|
@@ -2730,47 +3793,47 @@ async function gather() {
|
|
|
2730
3793
|
}
|
|
2731
3794
|
function renderHuman(r) {
|
|
2732
3795
|
const lines = [];
|
|
2733
|
-
lines.push(
|
|
2734
|
-
lines.push(
|
|
3796
|
+
lines.push(chalk16.bold.blue("vonpay doctor \u2014 diagnostic report"));
|
|
3797
|
+
lines.push(chalk16.dim(`generated ${r.generatedAt}`));
|
|
2735
3798
|
lines.push("");
|
|
2736
|
-
lines.push(
|
|
3799
|
+
lines.push(chalk16.bold("Runtime"));
|
|
2737
3800
|
lines.push(` CLI: ${r.cli.version}`);
|
|
2738
3801
|
lines.push(` Node: ${r.runtime.node}`);
|
|
2739
3802
|
lines.push(` Platform: ${r.runtime.platform} (${r.runtime.arch})`);
|
|
2740
3803
|
lines.push(` CWD: ${r.runtime.cwd}`);
|
|
2741
3804
|
lines.push("");
|
|
2742
|
-
lines.push(
|
|
3805
|
+
lines.push(chalk16.bold("API key"));
|
|
2743
3806
|
if (r.apiKeyPrefix) {
|
|
2744
3807
|
lines.push(` Prefix: ${r.apiKeyPrefix}`);
|
|
2745
3808
|
lines.push(` Mode: ${r.apiKeyMode}`);
|
|
2746
3809
|
} else {
|
|
2747
|
-
lines.push(
|
|
3810
|
+
lines.push(chalk16.dim(" (no key found in env or ~/.vonpay/config.json)"));
|
|
2748
3811
|
}
|
|
2749
3812
|
lines.push("");
|
|
2750
|
-
lines.push(
|
|
3813
|
+
lines.push(chalk16.bold("Env vars present (names only)"));
|
|
2751
3814
|
if (r.envVarsPresent.length === 0) {
|
|
2752
|
-
lines.push(
|
|
3815
|
+
lines.push(chalk16.dim(" (none of the expected vonpay env vars are set)"));
|
|
2753
3816
|
} else {
|
|
2754
3817
|
for (const n of r.envVarsPresent) lines.push(` \u2713 ${n}`);
|
|
2755
3818
|
}
|
|
2756
3819
|
lines.push("");
|
|
2757
|
-
lines.push(
|
|
3820
|
+
lines.push(chalk16.bold("Health probe"));
|
|
2758
3821
|
if (r.health.reachable) {
|
|
2759
|
-
const statusColor = r.health.status === "ok" ?
|
|
2760
|
-
lines.push(` Reachable: ${
|
|
3822
|
+
const statusColor = r.health.status === "ok" ? chalk16.green : r.health.status === "degraded" ? chalk16.yellow : chalk16.red;
|
|
3823
|
+
lines.push(` Reachable: ${chalk16.green("yes")}`);
|
|
2761
3824
|
lines.push(` Status: ${statusColor(r.health.status ?? "?")}`);
|
|
2762
3825
|
lines.push(` Latency: ${r.health.latencyMs}ms`);
|
|
2763
3826
|
} else {
|
|
2764
|
-
lines.push(` Reachable: ${
|
|
3827
|
+
lines.push(` Reachable: ${chalk16.red("no")}`);
|
|
2765
3828
|
lines.push(` Error: ${r.health.error}`);
|
|
2766
3829
|
}
|
|
2767
3830
|
lines.push("");
|
|
2768
3831
|
if (r.validateProbe) {
|
|
2769
|
-
lines.push(
|
|
3832
|
+
lines.push(chalk16.bold("sessions.validate probe ($14.99 USD dry-run)"));
|
|
2770
3833
|
if (r.validateProbe.ok) {
|
|
2771
|
-
lines.push(` Result: ${
|
|
3834
|
+
lines.push(` Result: ${chalk16.green("valid")}`);
|
|
2772
3835
|
} else {
|
|
2773
|
-
lines.push(` Result: ${
|
|
3836
|
+
lines.push(` Result: ${chalk16.red("invalid")}`);
|
|
2774
3837
|
if (r.validateProbe.error) {
|
|
2775
3838
|
lines.push(` Code: ${r.validateProbe.error.code}`);
|
|
2776
3839
|
lines.push(` Action: ${r.validateProbe.error.nextAction}`);
|
|
@@ -2783,7 +3846,7 @@ function renderHuman(r) {
|
|
|
2783
3846
|
lines.push("");
|
|
2784
3847
|
}
|
|
2785
3848
|
if (r.hints.length > 0) {
|
|
2786
|
-
lines.push(
|
|
3849
|
+
lines.push(chalk16.bold("Hints"));
|
|
2787
3850
|
for (const h of r.hints) lines.push(` \u2022 ${h}`);
|
|
2788
3851
|
lines.push("");
|
|
2789
3852
|
}
|
|
@@ -2885,8 +3948,8 @@ function doctorCommand() {
|
|
|
2885
3948
|
}
|
|
2886
3949
|
|
|
2887
3950
|
// src/commands/checkout.ts
|
|
2888
|
-
function registerCheckout(
|
|
2889
|
-
const checkout =
|
|
3951
|
+
function registerCheckout(program) {
|
|
3952
|
+
const checkout = program.command("checkout").description("Checkout integration tools");
|
|
2890
3953
|
checkout.hook("preAction", (_checkout, actionCommand) => {
|
|
2891
3954
|
setJsonErrors(actionCommand.opts().json === true);
|
|
2892
3955
|
});
|
|
@@ -2904,12 +3967,32 @@ function registerCheckout(program2) {
|
|
|
2904
3967
|
checkout.addCommand(doctorCommand());
|
|
2905
3968
|
}
|
|
2906
3969
|
|
|
3970
|
+
// src/program.ts
|
|
3971
|
+
function commandPath(cmd) {
|
|
3972
|
+
const names = [];
|
|
3973
|
+
for (let c = cmd; c && c.parent; c = c.parent) names.unshift(c.name());
|
|
3974
|
+
return names.join(" ");
|
|
3975
|
+
}
|
|
3976
|
+
function buildProgram() {
|
|
3977
|
+
const program = new Command13();
|
|
3978
|
+
program.name("vonpay").description("Von Payments developer tools").version(CLI_VERSION).option(
|
|
3979
|
+
ALLOW_CUSTOM_BASE_URL_FLAG,
|
|
3980
|
+
"With VONPAY_ALLOW_CUSTOM_BASE_URL=1: send a live key to a base URL that is not a Von Payments host (a proxy you run)"
|
|
3981
|
+
).hook("preAction", (_root, actionCommand) => {
|
|
3982
|
+
setAllowCustomBaseUrlFlag(program.opts().allowCustomBaseUrl === true);
|
|
3983
|
+
try {
|
|
3984
|
+
auditStart(commandPath(actionCommand), CLI_VERSION, actionCommand.opts(), actionCommand.args);
|
|
3985
|
+
} catch {
|
|
3986
|
+
}
|
|
3987
|
+
});
|
|
3988
|
+
registerCheckout(program);
|
|
3989
|
+
const withUsageAudit = (cmd) => {
|
|
3990
|
+
cmd.exitOverride((err) => auditUsageError(commandPath(cmd), CLI_VERSION, err.code, err.exitCode));
|
|
3991
|
+
cmd.commands.forEach(withUsageAudit);
|
|
3992
|
+
};
|
|
3993
|
+
withUsageAudit(program);
|
|
3994
|
+
return program;
|
|
3995
|
+
}
|
|
3996
|
+
|
|
2907
3997
|
// src/index.ts
|
|
2908
|
-
|
|
2909
|
-
var pkg2 = JSON.parse(
|
|
2910
|
-
readFileSync4(join3(__dirname, "..", "package.json"), "utf-8")
|
|
2911
|
-
);
|
|
2912
|
-
var program = new Command13();
|
|
2913
|
-
program.name("vonpay").description("Von Payments developer tools").version(pkg2.version);
|
|
2914
|
-
registerCheckout(program);
|
|
2915
|
-
program.parse();
|
|
3998
|
+
buildProgram().parse();
|