@vonpay/checkout-cli 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -6
- package/dist/index.js +1397 -334
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
|
2
19
|
|
|
3
|
-
// src/
|
|
4
|
-
import { Command as
|
|
5
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
6
|
-
import { dirname as dirname2, join as join3 } from "path";
|
|
7
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
20
|
+
// src/program.ts
|
|
21
|
+
import { Command as Command14 } from "commander";
|
|
8
22
|
|
|
9
23
|
// src/commands/login.ts
|
|
10
24
|
import { Command, Option } from "commander";
|
|
@@ -12,9 +26,9 @@ import { createInterface } from "readline";
|
|
|
12
26
|
import chalk from "chalk";
|
|
13
27
|
|
|
14
28
|
// src/config.ts
|
|
15
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
29
|
+
import { chmodSync as chmodSync2, existsSync, mkdirSync as mkdirSync2, readFileSync, rmSync, writeFileSync } from "fs";
|
|
16
30
|
import { homedir } from "os";
|
|
17
|
-
import { join } from "path";
|
|
31
|
+
import { join as join2 } from "path";
|
|
18
32
|
|
|
19
33
|
// src/utils/json-errors.ts
|
|
20
34
|
var jsonErrors = false;
|
|
@@ -34,25 +48,362 @@ function emitCliError(json, text) {
|
|
|
34
48
|
}
|
|
35
49
|
|
|
36
50
|
// src/utils/key-mode.ts
|
|
37
|
-
import {
|
|
38
|
-
var TEST_SECRET_KEY_PREFIX = "vp_sk_test_";
|
|
39
|
-
function requiresLiveConfirmation(key) {
|
|
40
|
-
if (typeof key !== "string") return true;
|
|
41
|
-
if (key.startsWith(TEST_SECRET_KEY_PREFIX)) return false;
|
|
42
|
-
if (key.startsWith("vp_pk_")) return false;
|
|
43
|
-
return true;
|
|
44
|
-
}
|
|
51
|
+
import { keyMode } from "@vonpay/checkout-node";
|
|
45
52
|
function secretKeyMode(key) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (key.startsWith(TEST_SECRET_KEY_PREFIX)) return "test";
|
|
49
|
-
return null;
|
|
53
|
+
const mode = keyMode(key);
|
|
54
|
+
return mode === "test" || mode === "live" ? mode : null;
|
|
50
55
|
}
|
|
51
56
|
var NOT_A_SECRET_KEY_MESSAGE = "This is not a Von Payments secret key: it must start with vp_sk_test_ (sandbox) or vp_sk_live_ (live). Publishable keys (vp_pk_\u2026) and keys from other services cannot be used with this CLI.";
|
|
52
57
|
|
|
58
|
+
// src/utils/audit-log.ts
|
|
59
|
+
var audit_log_exports = {};
|
|
60
|
+
__export(audit_log_exports, {
|
|
61
|
+
AUDIT_FIELDS: () => AUDIT_FIELDS,
|
|
62
|
+
COMMAND_KIND: () => COMMAND_KIND,
|
|
63
|
+
MONEY: () => MONEY,
|
|
64
|
+
UNKNOWN_COMMAND: () => UNKNOWN_COMMAND,
|
|
65
|
+
_resetAuditForTests: () => _resetAuditForTests,
|
|
66
|
+
_resetAuditWarningsForTests: () => _resetAuditWarningsForTests,
|
|
67
|
+
auditError: () => auditError,
|
|
68
|
+
auditFinish: () => auditFinish,
|
|
69
|
+
auditIntent: () => auditIntent,
|
|
70
|
+
auditLine: () => auditLine,
|
|
71
|
+
auditNote: () => auditNote,
|
|
72
|
+
auditSignalHandledByCommand: () => auditSignalHandledByCommand,
|
|
73
|
+
auditStart: () => auditStart,
|
|
74
|
+
auditUsageError: () => auditUsageError,
|
|
75
|
+
fullRecordLine: () => fullRecordLine,
|
|
76
|
+
isMoneyCommand: () => isMoneyCommand,
|
|
77
|
+
pruneAuditFiles: () => pruneAuditFiles,
|
|
78
|
+
writeAuditLine: () => writeAuditLine
|
|
79
|
+
});
|
|
80
|
+
__reExport(audit_log_exports, audit_log_star);
|
|
81
|
+
import {
|
|
82
|
+
chmodSync,
|
|
83
|
+
closeSync,
|
|
84
|
+
fchmodSync,
|
|
85
|
+
fstatSync,
|
|
86
|
+
lstatSync,
|
|
87
|
+
mkdirSync,
|
|
88
|
+
openSync,
|
|
89
|
+
readdirSync,
|
|
90
|
+
readSync,
|
|
91
|
+
unlinkSync,
|
|
92
|
+
writeSync
|
|
93
|
+
} from "fs";
|
|
94
|
+
import { join } from "path";
|
|
95
|
+
import {
|
|
96
|
+
AUDIT_DIR_ENV,
|
|
97
|
+
AUDIT_SWITCH_ENV,
|
|
98
|
+
DEFAULT_AUDIT_LIMITS,
|
|
99
|
+
FULL_RECORD_SUFFIX,
|
|
100
|
+
auditAppendFlags,
|
|
101
|
+
auditErrorCodeOf,
|
|
102
|
+
auditFileExpired,
|
|
103
|
+
auditKeyFields,
|
|
104
|
+
auditLineFormat,
|
|
105
|
+
auditSettingsFromEnv,
|
|
106
|
+
auditSpaceWarnings,
|
|
107
|
+
checkAuditDirStats,
|
|
108
|
+
checkAuditFileStats,
|
|
109
|
+
isReservedLine,
|
|
110
|
+
spaceDecision,
|
|
111
|
+
stderrWarn
|
|
112
|
+
} from "@vonpay/checkout-node/internal/audit-log";
|
|
113
|
+
import * as audit_log_star from "@vonpay/checkout-node/internal/audit-log";
|
|
114
|
+
var UNKNOWN_COMMAND = "[unknown command]";
|
|
115
|
+
var FORMAT = auditLineFormat({ nameField: "command", namePattern: /^[a-z0-9-]+( [a-z0-9-]+){0,3}$/, unknownName: UNKNOWN_COMMAND });
|
|
116
|
+
var AUDIT_FIELDS = FORMAT.fields;
|
|
117
|
+
var auditLine = FORMAT.line;
|
|
118
|
+
var fullRecordLine = FORMAT.fullRecordLine;
|
|
119
|
+
function checkDir(dir, st) {
|
|
120
|
+
if (checkAuditDirStats(st)) chmodSync(dir, 448);
|
|
121
|
+
}
|
|
122
|
+
var FILE_NAME = /^cli-(\d{4})-(\d{2})-(\d{2})\.jsonl$/;
|
|
123
|
+
var warned = false;
|
|
124
|
+
var warnedFull = false;
|
|
125
|
+
var warnedReserve = false;
|
|
126
|
+
var warnedTooLong = false;
|
|
127
|
+
function _resetAuditWarningsForTests() {
|
|
128
|
+
warned = false;
|
|
129
|
+
warnedFull = false;
|
|
130
|
+
warnedReserve = false;
|
|
131
|
+
warnedTooLong = false;
|
|
132
|
+
}
|
|
133
|
+
function warnOnce(dir, err, warn) {
|
|
134
|
+
if (warned) return;
|
|
135
|
+
warned = true;
|
|
136
|
+
warn(
|
|
137
|
+
`[vonpay] The local audit log cannot be written (${auditErrorCodeOf(err)}) in ${dir}; the command went on without it. Set ${AUDIT_DIR_ENV} to a writable directory of your own, or ${AUDIT_SWITCH_ENV}=off.`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
function pruneAuditFiles(dir, retentionDays, now = /* @__PURE__ */ new Date(), warn = stderrWarn) {
|
|
141
|
+
let names;
|
|
142
|
+
try {
|
|
143
|
+
checkDir(dir, lstatSync(dir));
|
|
144
|
+
names = readdirSync(dir);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
if (auditErrorCodeOf(err) !== "ENOENT") warnOnce(dir, err, warn);
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
let removed = 0;
|
|
150
|
+
let skipped = 0;
|
|
151
|
+
for (const name of names) {
|
|
152
|
+
const m = FILE_NAME.exec(name);
|
|
153
|
+
if (!m) continue;
|
|
154
|
+
if (!auditFileExpired(`${m[1]}-${m[2]}-${m[3]}`, now, retentionDays)) continue;
|
|
155
|
+
const path = join(dir, name);
|
|
156
|
+
try {
|
|
157
|
+
if (!lstatSync(path).isFile()) {
|
|
158
|
+
skipped++;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
unlinkSync(path);
|
|
162
|
+
removed++;
|
|
163
|
+
} catch {
|
|
164
|
+
skipped++;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (skipped > 0) warn(auditSpaceWarnings.notPruned(skipped, dir));
|
|
168
|
+
return removed;
|
|
169
|
+
}
|
|
170
|
+
function endsWithFullRecord(fd, size) {
|
|
171
|
+
const n = Buffer.byteLength(FULL_RECORD_SUFFIX);
|
|
172
|
+
if (size < n) return false;
|
|
173
|
+
const buf = Buffer.alloc(n);
|
|
174
|
+
readSync(fd, buf, 0, n, size - n);
|
|
175
|
+
return buf.toString("utf8") === FULL_RECORD_SUFFIX;
|
|
176
|
+
}
|
|
177
|
+
function appendSecure(file, line, reserved, record, now, limits, dir, warn) {
|
|
178
|
+
const fd = openSync(file, auditAppendFlags(), 384);
|
|
179
|
+
try {
|
|
180
|
+
const st = fstatSync(fd);
|
|
181
|
+
if (checkAuditFileStats(st)) fchmodSync(fd, 384);
|
|
182
|
+
const decision = spaceDecision(st.size, Buffer.byteLength(line), reserved, limits);
|
|
183
|
+
if (decision === "write") {
|
|
184
|
+
writeSync(fd, line);
|
|
185
|
+
} else if (decision === "too_long") {
|
|
186
|
+
if (!warnedTooLong) {
|
|
187
|
+
warnedTooLong = true;
|
|
188
|
+
warn(auditSpaceWarnings.tooLong(limits.maxLineBytes, dir));
|
|
189
|
+
}
|
|
190
|
+
} else if (decision === "drop") {
|
|
191
|
+
if (!warnedReserve) {
|
|
192
|
+
warnedReserve = true;
|
|
193
|
+
warn(auditSpaceWarnings.nearlyFull(dir));
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
const full = fullRecordLine(record.surface, record.version, now);
|
|
197
|
+
if (!endsWithFullRecord(fd, st.size) && st.size + Buffer.byteLength(full) <= limits.maxFileBytes) writeSync(fd, full);
|
|
198
|
+
if (!warnedFull) {
|
|
199
|
+
warnedFull = true;
|
|
200
|
+
warn(auditSpaceWarnings.full(dir));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} finally {
|
|
204
|
+
closeSync(fd);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function writeAuditLine(dir, record, now = /* @__PURE__ */ new Date(), warn = stderrWarn, limits = DEFAULT_AUDIT_LIMITS) {
|
|
208
|
+
const file = join(dir, `cli-${now.toISOString().slice(0, 10)}.jsonl`);
|
|
209
|
+
try {
|
|
210
|
+
const line = auditLine(record);
|
|
211
|
+
const reserved = isReservedLine(record, typeof record.command === "string" && isMoneyCommand(record.command));
|
|
212
|
+
const attempt = () => {
|
|
213
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
214
|
+
checkDir(dir, lstatSync(dir));
|
|
215
|
+
appendSecure(file, line, reserved, record, now, limits, dir, warn);
|
|
216
|
+
};
|
|
217
|
+
try {
|
|
218
|
+
attempt();
|
|
219
|
+
} catch (err) {
|
|
220
|
+
if (auditErrorCodeOf(err) !== "ENOENT") throw err;
|
|
221
|
+
attempt();
|
|
222
|
+
}
|
|
223
|
+
return true;
|
|
224
|
+
} catch (err) {
|
|
225
|
+
warnOnce(dir, err, warn);
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
var COMMAND_KIND = {
|
|
230
|
+
"checkout login": "local",
|
|
231
|
+
"checkout logout": "local",
|
|
232
|
+
"checkout init": "local",
|
|
233
|
+
"checkout trigger": "local",
|
|
234
|
+
// signs a synthetic event and sends it to YOUR endpoint, not the API
|
|
235
|
+
"checkout sessions create": "api",
|
|
236
|
+
"checkout sessions get": "api",
|
|
237
|
+
"checkout payment-intents create": "api",
|
|
238
|
+
"checkout payment-intents get": "api",
|
|
239
|
+
"checkout payment-intents capture": "api",
|
|
240
|
+
"checkout payment-intents void": "api",
|
|
241
|
+
"checkout payment-intents list": "api",
|
|
242
|
+
"checkout payment-intents summary": "api",
|
|
243
|
+
"checkout refunds create": "api",
|
|
244
|
+
"checkout refunds list": "api",
|
|
245
|
+
"checkout tokens create": "api",
|
|
246
|
+
"checkout subscription-products list": "api",
|
|
247
|
+
"checkout subscription-products terms": "api",
|
|
248
|
+
"checkout capabilities": "api",
|
|
249
|
+
"checkout listen": "api",
|
|
250
|
+
"checkout health": "api",
|
|
251
|
+
"checkout doctor": "api"
|
|
252
|
+
};
|
|
253
|
+
var MONEY = {
|
|
254
|
+
"checkout payment-intents create": (o) => ({ amount: wholeNumber(o.amount), currency: o.currency, target: o.sessionId }),
|
|
255
|
+
"checkout payment-intents capture": (o, a) => ({ amount: wholeNumber(o.amountToCapture), target: a[0] }),
|
|
256
|
+
"checkout payment-intents void": (_o, a) => ({ target: a[0] }),
|
|
257
|
+
"checkout refunds create": (o) => ({ amount: wholeNumber(o.amount), currency: o.currency, target: o.paymentIntent ?? o.transaction }),
|
|
258
|
+
"checkout sessions create": (o) => ({ amount: wholeNumber(o.amount), currency: o.currency })
|
|
259
|
+
};
|
|
260
|
+
function isMoneyCommand(command) {
|
|
261
|
+
return Object.prototype.hasOwnProperty.call(MONEY, command);
|
|
262
|
+
}
|
|
263
|
+
function wholeNumber(v) {
|
|
264
|
+
return typeof v === "string" && /^[0-9]{1,15}$/.test(v.trim()) ? Number(v.trim()) : typeof v === "number" ? v : void 0;
|
|
265
|
+
}
|
|
266
|
+
var pending = null;
|
|
267
|
+
var hooked = false;
|
|
268
|
+
var SIGNAL_EXIT = { SIGINT: 130, SIGTERM: 143 };
|
|
269
|
+
var processHandlers = [];
|
|
270
|
+
var signalsHandledByCommand = false;
|
|
271
|
+
function auditSignalHandledByCommand() {
|
|
272
|
+
signalsHandledByCommand = true;
|
|
273
|
+
}
|
|
274
|
+
function hookProcess() {
|
|
275
|
+
if (hooked) return;
|
|
276
|
+
hooked = true;
|
|
277
|
+
const onExit = (code) => auditFinish(code);
|
|
278
|
+
const onCrash = () => auditNote({ crashed: true });
|
|
279
|
+
process.on("exit", onExit);
|
|
280
|
+
process.on("uncaughtExceptionMonitor", onCrash);
|
|
281
|
+
processHandlers.push(["exit", onExit], ["uncaughtExceptionMonitor", onCrash]);
|
|
282
|
+
for (const sig of ["SIGINT", "SIGTERM"]) {
|
|
283
|
+
const handler = () => {
|
|
284
|
+
if (signalsHandledByCommand) return;
|
|
285
|
+
auditNote({ interrupted: true });
|
|
286
|
+
auditFinish(SIGNAL_EXIT[sig]);
|
|
287
|
+
process.exit(SIGNAL_EXIT[sig]);
|
|
288
|
+
};
|
|
289
|
+
processHandlers.push([sig, handler]);
|
|
290
|
+
process.on(sig, handler);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function auditStart(command, version, opts = {}, args = []) {
|
|
294
|
+
const settings = auditSettingsFromEnv();
|
|
295
|
+
if (settings.dir === null) {
|
|
296
|
+
pending = null;
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
pruneAuditFiles(settings.dir, settings.retentionDays);
|
|
300
|
+
let money2 = {};
|
|
301
|
+
try {
|
|
302
|
+
money2 = Object.prototype.hasOwnProperty.call(MONEY, command) ? MONEY[command](opts, args) : {};
|
|
303
|
+
} catch {
|
|
304
|
+
money2 = {};
|
|
305
|
+
}
|
|
306
|
+
pending = { command, money: money2, settings, version, intentWritten: false, written: false };
|
|
307
|
+
if (typeof opts.apiKey === "string") pending.apiKey = opts.apiKey;
|
|
308
|
+
hookProcess();
|
|
309
|
+
}
|
|
310
|
+
function auditNote(note, onlyIfUnset = false) {
|
|
311
|
+
if (!pending) return;
|
|
312
|
+
if (onlyIfUnset && pending.outcome !== void 0 && note.outcome !== void 0) return;
|
|
313
|
+
Object.assign(pending, note);
|
|
314
|
+
}
|
|
315
|
+
function auditError(err) {
|
|
316
|
+
if (err === null || typeof err !== "object") return;
|
|
317
|
+
const e = err;
|
|
318
|
+
if (typeof e.status === "number" && typeof e.code === "string") {
|
|
319
|
+
auditNote({ apiCode: e.code, ...typeof e.requestId === "string" ? { requestId: e.requestId } : {} });
|
|
320
|
+
} else if (e.cause !== void 0 && err instanceof TypeError) {
|
|
321
|
+
auditNote({ networkError: true });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function auditIntent(now = /* @__PURE__ */ new Date()) {
|
|
325
|
+
const p = pending;
|
|
326
|
+
if (!p || p.intentWritten || p.settings.dir === null || !Object.prototype.hasOwnProperty.call(MONEY, p.command)) return;
|
|
327
|
+
p.intentWritten = true;
|
|
328
|
+
writeAuditLine(p.settings.dir, { command: p.command, ts: now.toISOString(), surface: "cli", version: p.version, ...auditKeyFields(p.apiKey), outcome: "pending", ...p.money }, now);
|
|
329
|
+
}
|
|
330
|
+
function auditFinish(exitCode, now = /* @__PURE__ */ new Date()) {
|
|
331
|
+
const p = pending;
|
|
332
|
+
if (!p || p.written || p.settings.dir === null) return;
|
|
333
|
+
p.written = true;
|
|
334
|
+
try {
|
|
335
|
+
let outcome;
|
|
336
|
+
let code = p.code;
|
|
337
|
+
if (p.crashed) [outcome, code] = ["error", "internal_error"];
|
|
338
|
+
else if (p.interrupted) [outcome, code] = ["error", "interrupted"];
|
|
339
|
+
else if (p.outcome) outcome = p.outcome;
|
|
340
|
+
else if (p.apiCode) [outcome, code] = ["error", p.apiCode];
|
|
341
|
+
else if (p.networkError) [outcome, code] = ["error", "network_error"];
|
|
342
|
+
else if (exitCode === 0) outcome = p.approved ? "approved" : COMMAND_KIND[p.command] === "local" ? "local" : "sent";
|
|
343
|
+
else [outcome, code] = ["refused_local", code ?? "local_refusal"];
|
|
344
|
+
writeAuditLine(
|
|
345
|
+
p.settings.dir,
|
|
346
|
+
{
|
|
347
|
+
command: p.command,
|
|
348
|
+
ts: now.toISOString(),
|
|
349
|
+
surface: "cli",
|
|
350
|
+
version: p.version,
|
|
351
|
+
...auditKeyFields(p.apiKey),
|
|
352
|
+
outcome,
|
|
353
|
+
...code ? { error_code: code } : {},
|
|
354
|
+
...p.requestId ? { request_id: p.requestId } : {},
|
|
355
|
+
...p.money
|
|
356
|
+
},
|
|
357
|
+
now
|
|
358
|
+
);
|
|
359
|
+
} catch (err) {
|
|
360
|
+
warnOnce(p.settings.dir, err, stderrWarn);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function usageReason(commanderCode) {
|
|
364
|
+
switch (commanderCode) {
|
|
365
|
+
case "commander.unknownCommand":
|
|
366
|
+
return "unknown_command";
|
|
367
|
+
case "commander.unknownOption":
|
|
368
|
+
return "unknown_option";
|
|
369
|
+
case "commander.missingArgument":
|
|
370
|
+
case "commander.optionMissingArgument":
|
|
371
|
+
case "commander.missingMandatoryOptionValue":
|
|
372
|
+
return "missing_argument";
|
|
373
|
+
case "commander.invalidArgument":
|
|
374
|
+
return "invalid_argument";
|
|
375
|
+
case "commander.excessArguments":
|
|
376
|
+
return "excess_arguments";
|
|
377
|
+
default:
|
|
378
|
+
return "usage_error";
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function auditUsageError(command, version, commanderCode, exitCode, now = /* @__PURE__ */ new Date()) {
|
|
382
|
+
try {
|
|
383
|
+
if (exitCode === 0) return;
|
|
384
|
+
const reason = usageReason(commanderCode);
|
|
385
|
+
if (pending) {
|
|
386
|
+
auditNote({ outcome: "refused_local", code: reason }, true);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const settings = auditSettingsFromEnv();
|
|
390
|
+
if (settings.dir === null) return;
|
|
391
|
+
const name = reason === "unknown_command" || command === "" ? UNKNOWN_COMMAND : command;
|
|
392
|
+
writeAuditLine(settings.dir, { command: name, ts: now.toISOString(), surface: "cli", version, outcome: "refused_local", error_code: reason }, now);
|
|
393
|
+
} catch (err) {
|
|
394
|
+
void err;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function _resetAuditForTests() {
|
|
398
|
+
pending = null;
|
|
399
|
+
for (const [event, h] of processHandlers.splice(0)) process.removeListener(event, h);
|
|
400
|
+
hooked = false;
|
|
401
|
+
signalsHandledByCommand = false;
|
|
402
|
+
}
|
|
403
|
+
|
|
53
404
|
// src/config.ts
|
|
54
|
-
var CONFIG_DIR =
|
|
55
|
-
var CONFIG_FILE =
|
|
405
|
+
var CONFIG_DIR = join2(homedir(), ".vonpay");
|
|
406
|
+
var CONFIG_FILE = join2(CONFIG_DIR, "config.json");
|
|
56
407
|
function loadConfig() {
|
|
57
408
|
if (!existsSync(CONFIG_FILE)) return {};
|
|
58
409
|
try {
|
|
@@ -79,13 +430,13 @@ function saveConfig(config) {
|
|
|
79
430
|
warnStoredLiveKey();
|
|
80
431
|
}
|
|
81
432
|
if (!existsSync(CONFIG_DIR)) {
|
|
82
|
-
|
|
433
|
+
mkdirSync2(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
83
434
|
}
|
|
84
435
|
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", {
|
|
85
436
|
encoding: "utf-8",
|
|
86
437
|
mode: 384
|
|
87
438
|
});
|
|
88
|
-
|
|
439
|
+
chmodSync2(CONFIG_FILE, 384);
|
|
89
440
|
}
|
|
90
441
|
function clearStoredApiKey() {
|
|
91
442
|
const config = loadConfig();
|
|
@@ -124,6 +475,7 @@ function requireApiKey() {
|
|
|
124
475
|
if (secretKeyMode(key) === null) {
|
|
125
476
|
emitCliError({ error: NOT_A_SECRET_KEY_MESSAGE, code: null }, NOT_A_SECRET_KEY_MESSAGE);
|
|
126
477
|
}
|
|
478
|
+
auditNote({ apiKey: key });
|
|
127
479
|
return key;
|
|
128
480
|
}
|
|
129
481
|
|
|
@@ -222,7 +574,7 @@ function logoutCommand() {
|
|
|
222
574
|
|
|
223
575
|
// src/commands/init.ts
|
|
224
576
|
import { Command as Command3 } from "commander";
|
|
225
|
-
import { appendFileSync, chmodSync as
|
|
577
|
+
import { appendFileSync, chmodSync as chmodSync3, existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
226
578
|
import { resolve } from "path";
|
|
227
579
|
import chalk3 from "chalk";
|
|
228
580
|
var LIVE_KEY_NOT_WRITTEN_MESSAGE = "Refusing to write a live key (vp_sk_live_\u2026) to .env: live keys are never written to disk. Run `init` with a sandbox key (vp_sk_test_\u2026), and give your app its live key through your deployment's secret store or the environment when you go live.";
|
|
@@ -299,7 +651,7 @@ function initCommand() {
|
|
|
299
651
|
""
|
|
300
652
|
].join("\n");
|
|
301
653
|
writeFileSync2(envFile, content, { encoding: "utf-8", mode: 384 });
|
|
302
|
-
|
|
654
|
+
chmodSync3(envFile, 384);
|
|
303
655
|
console.log(chalk3.green(`\u2713 .env created (${mode} mode) \u2192 ${absPath}`));
|
|
304
656
|
const gitignoreState = ensureGitignoreBlocksEnv();
|
|
305
657
|
if (gitignoreState === "added") {
|
|
@@ -318,7 +670,7 @@ function initCommand() {
|
|
|
318
670
|
|
|
319
671
|
// src/commands/sessions.ts
|
|
320
672
|
import { Command as Command4 } from "commander";
|
|
321
|
-
import
|
|
673
|
+
import chalk6 from "chalk";
|
|
322
674
|
|
|
323
675
|
// src/utils/spinner.ts
|
|
324
676
|
import ora from "ora";
|
|
@@ -334,10 +686,10 @@ import { VonPayCheckout } from "@vonpay/checkout-node";
|
|
|
334
686
|
|
|
335
687
|
// src/app-info.ts
|
|
336
688
|
import { readFileSync as readFileSync3 } from "fs";
|
|
337
|
-
import { dirname, join as
|
|
689
|
+
import { dirname, join as join3 } from "path";
|
|
338
690
|
import { fileURLToPath } from "url";
|
|
339
691
|
var pkg = JSON.parse(
|
|
340
|
-
readFileSync3(
|
|
692
|
+
readFileSync3(join3(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf-8")
|
|
341
693
|
);
|
|
342
694
|
var CLI_VERSION = pkg.version;
|
|
343
695
|
var CLI_APP_INFO = { name: "vonpay-cli", version: CLI_VERSION };
|
|
@@ -347,12 +699,18 @@ var SILENT_ERROR_REPORTER = () => {
|
|
|
347
699
|
// src/utils/base-url.ts
|
|
348
700
|
import { keyDestinationProblem as sdkKeyDestinationProblem } from "@vonpay/checkout-node";
|
|
349
701
|
var ALLOW_CUSTOM_BASE_URL_ENV = "VONPAY_ALLOW_CUSTOM_BASE_URL";
|
|
350
|
-
|
|
351
|
-
|
|
702
|
+
var ALLOW_CUSTOM_BASE_URL_FLAG = "--allow-custom-base-url";
|
|
703
|
+
var flagGiven = false;
|
|
704
|
+
function setAllowCustomBaseUrlFlag(given) {
|
|
705
|
+
flagGiven = given === true;
|
|
706
|
+
}
|
|
707
|
+
function customBaseUrlAllowed(env = process.env, flag = flagGiven) {
|
|
708
|
+
return env[ALLOW_CUSTOM_BASE_URL_ENV] === "1" && flag === true;
|
|
352
709
|
}
|
|
353
710
|
var UNSAFE_URL_CHARS = /[\s\x00-\x1f\x7f\\]/;
|
|
354
711
|
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
|
-
|
|
712
|
+
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.`;
|
|
713
|
+
function keyDestinationProblem(apiKey, baseUrl, env = process.env, flag = flagGiven) {
|
|
356
714
|
if (baseUrl === void 0) return null;
|
|
357
715
|
if (UNSAFE_URL_CHARS.test(baseUrl)) {
|
|
358
716
|
return `Refusing to use the base URL: it contains spaces, control characters or backslashes. ${WHERE}`;
|
|
@@ -366,11 +724,11 @@ function keyDestinationProblem(apiKey, baseUrl, env = process.env) {
|
|
|
366
724
|
if (url.username !== "" || url.password !== "") {
|
|
367
725
|
return `Refusing to use the base URL: it contains a user name or password, which would be sent with every request. ${WHERE}`;
|
|
368
726
|
}
|
|
369
|
-
const problem = sdkKeyDestinationProblem(apiKey, baseUrl, { allowCustom: customBaseUrlAllowed(env) });
|
|
370
|
-
return problem === null ? null : `${problem} ${WHERE}`;
|
|
727
|
+
const problem = sdkKeyDestinationProblem(apiKey, baseUrl, { allowCustom: customBaseUrlAllowed(env, flag) });
|
|
728
|
+
return problem === null ? null : `${problem} ${WHERE} ${CLI_OPT_IN}`;
|
|
371
729
|
}
|
|
372
|
-
function allowCustomBaseUrl(env = process.env) {
|
|
373
|
-
return customBaseUrlAllowed(env);
|
|
730
|
+
function allowCustomBaseUrl(env = process.env, flag = flagGiven) {
|
|
731
|
+
return customBaseUrlAllowed(env, flag);
|
|
374
732
|
}
|
|
375
733
|
|
|
376
734
|
// src/utils/errors.ts
|
|
@@ -404,10 +762,14 @@ function scrubSecrets(s) {
|
|
|
404
762
|
}
|
|
405
763
|
|
|
406
764
|
// src/utils/sanitize.ts
|
|
765
|
+
import { stripHiddenCharacters } from "@vonpay/checkout-node";
|
|
407
766
|
var ANSI_PATTERN = /\x1B\[[0-9;?]*[A-Za-z]/g;
|
|
408
|
-
var
|
|
767
|
+
var C1_CSI_PATTERN = /\u{9B}[0-9;?]*[A-Za-z]/gu;
|
|
768
|
+
var CONTROL_PATTERN = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\u{80}-\u{9F}]/gu;
|
|
769
|
+
var LINE_SEPARATOR_PATTERN = /[\u{2028}\u{2029}]/gu;
|
|
770
|
+
var PLAIN_TEXT_PATTERN = /^[\x20-\x7e]*$/;
|
|
409
771
|
function stripAnsi(s) {
|
|
410
|
-
return s.replace(ANSI_PATTERN, "").replace(
|
|
772
|
+
return stripHiddenCharacters(s.replace(ANSI_PATTERN, "").replace(C1_CSI_PATTERN, "").replace(CONTROL_PATTERN, ""));
|
|
411
773
|
}
|
|
412
774
|
function truncate(s, max) {
|
|
413
775
|
if (s.length <= max) return s;
|
|
@@ -417,7 +779,43 @@ function safeDisplay(s, max = 200) {
|
|
|
417
779
|
return truncate(stripAnsi(s), max);
|
|
418
780
|
}
|
|
419
781
|
function safeInline(s, max = 200) {
|
|
420
|
-
return safeDisplay(s.replace(/[\r\n]+/g, " "), max);
|
|
782
|
+
return safeDisplay(s.replace(/[\r\n]+/g, " ").replace(LINE_SEPARATOR_PATTERN, " "), max);
|
|
783
|
+
}
|
|
784
|
+
var READ_SECRET_PATTERNS = SECRET_PATTERNS.map(
|
|
785
|
+
([re, replacement]) => [new RegExp(`(?<![A-Za-z0-9])(?:${re.source})`, re.flags), replacement]
|
|
786
|
+
);
|
|
787
|
+
function redactSecretShapes(s) {
|
|
788
|
+
let out = s;
|
|
789
|
+
for (const [re, replacement] of READ_SECRET_PATTERNS) out = out.replace(re, replacement);
|
|
790
|
+
return out;
|
|
791
|
+
}
|
|
792
|
+
function uniqueKey(cleaned, taken) {
|
|
793
|
+
let key = cleaned;
|
|
794
|
+
for (let n = 2; taken.has(key); n++) key = `${cleaned}#${n}`;
|
|
795
|
+
taken.add(key);
|
|
796
|
+
return key;
|
|
797
|
+
}
|
|
798
|
+
function cleanJsonValue(value, options = {}) {
|
|
799
|
+
const { lines = "keep", maxChars = Number.MAX_SAFE_INTEGER, redact = redactSecretShapes, exactKeys } = options;
|
|
800
|
+
const text = (s) => {
|
|
801
|
+
if (PLAIN_TEXT_PATTERN.test(s)) return truncate(s, maxChars);
|
|
802
|
+
return lines === "join" ? safeInline(s, maxChars) : truncate(stripAnsi(s.replace(LINE_SEPARATOR_PATTERN, " ")), maxChars);
|
|
803
|
+
};
|
|
804
|
+
const walk = (v, exact) => {
|
|
805
|
+
if (typeof v === "string") return exact ? text(v) : redact(text(v));
|
|
806
|
+
if (v === null || typeof v !== "object") return v;
|
|
807
|
+
const toJSON = v.toJSON;
|
|
808
|
+
if (typeof toJSON === "function") return walk(toJSON.call(v), exact);
|
|
809
|
+
if (Array.isArray(v)) return v.map((item) => walk(item, exact));
|
|
810
|
+
const taken = /* @__PURE__ */ new Set();
|
|
811
|
+
return Object.fromEntries(
|
|
812
|
+
Object.entries(v).map(([k, item]) => [uniqueKey(text(k), taken), walk(item, exact || exactKeys?.has(k) === true)])
|
|
813
|
+
);
|
|
814
|
+
};
|
|
815
|
+
return walk(value, false);
|
|
816
|
+
}
|
|
817
|
+
function jsonOutput(value, options) {
|
|
818
|
+
return JSON.stringify(cleanJsonValue(value, options), null, 2);
|
|
421
819
|
}
|
|
422
820
|
|
|
423
821
|
// src/utils/errors.ts
|
|
@@ -425,6 +823,12 @@ var CLI_READ_BACK = "From this CLI: `vonpay checkout payment-intents get <id>` (
|
|
|
425
823
|
var READ_BACK_FIRST = "Check the payment first: read it back (`vonpay checkout payment-intents get <id>` for a charge, capture or void; `vonpay checkout refunds list --payment-intent <id>` (or `--transaction <id>`) for a refund; `sessions get <id>` for a checkout session) and act only on what it shows.";
|
|
426
824
|
var UNKNOWN_OUTCOME_GUIDANCE = `${READ_BACK_FIRST} If you then retry, reuse the SAME --idempotency-key as the first attempt, never a new one: a new key is a new action and can move money twice.`;
|
|
427
825
|
var str = (v) => v === void 0 || v === null ? null : String(v);
|
|
826
|
+
var ONE_LINE_MAX_CHARS = 2e3;
|
|
827
|
+
var oneLine = (s) => safeInline(s, ONE_LINE_MAX_CHARS);
|
|
828
|
+
function paramFlagHint(param) {
|
|
829
|
+
const flag = param.startsWith("metadata") ? "--metadata" : `--${param.replace(/\[(gte|lt)\]$/, "-$1").replace(/_/g, "-")}`;
|
|
830
|
+
return /^--[a-z-]+$/.test(flag) ? ` (the ${flag} flag)` : "";
|
|
831
|
+
}
|
|
428
832
|
var FIELD_PRINTERS = {
|
|
429
833
|
code: (e) => `Code: ${str(e.code)}`,
|
|
430
834
|
fix: (e) => `Fix: ${str(e.fix)}`,
|
|
@@ -464,8 +868,8 @@ var FIELD_PRINTERS = {
|
|
|
464
868
|
usedMinor: (e) => typeof e.usedMinor === "number" ? `Used in the last 24 hours: ${safeInline(String(e.usedMinor))} (minor units)` : null,
|
|
465
869
|
limitCurrency: (e) => e.limitCurrency ? `Key limit currency: ${safeInline(String(e.limitCurrency))}` : null,
|
|
466
870
|
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
|
-
//
|
|
871
|
+
// A refused payment-only settle (mirror_settle_only_*; served in production
|
|
872
|
+
// since 2026-10-27): the charge asked for and the store order's
|
|
469
873
|
// facts. Validated by the SDK, printed through safeInline all the same.
|
|
470
874
|
chargeAmount: (e) => typeof e.chargeAmount === "number" ? `Charge asked for: ${safeInline(String(e.chargeAmount))} (minor units)` : null,
|
|
471
875
|
chargeCurrency: (e) => e.chargeCurrency ? `Charge currency: ${safeInline(String(e.chargeCurrency))}` : null,
|
|
@@ -474,7 +878,7 @@ var FIELD_PRINTERS = {
|
|
|
474
878
|
orderCurrency: (e) => e.orderCurrency ? `Store order currency: ${safeInline(String(e.orderCurrency))}` : null,
|
|
475
879
|
orderStatus: (e) => e.orderStatus ? `Store order status: ${safeInline(String(e.orderStatus))}` : null,
|
|
476
880
|
orderPaymentStatus: (e) => e.orderPaymentStatus ? `Store order payment status: ${safeInline(String(e.orderPaymentStatus))}` : null,
|
|
477
|
-
// A refused subscription change (
|
|
881
|
+
// A refused subscription change (served in production since 2026-10-30). Validated by
|
|
478
882
|
// the SDK, printed through safeInline all the same.
|
|
479
883
|
subscriptionStatus: (e) => e.subscriptionStatus ? `Subscription status: ${safeInline(String(e.subscriptionStatus))}` : null,
|
|
480
884
|
refusalReason: (e) => e.refusalReason ? `Refused because: ${safeInline(String(e.refusalReason))}` : null,
|
|
@@ -484,14 +888,23 @@ var FIELD_PRINTERS = {
|
|
|
484
888
|
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,
|
|
485
889
|
amountCurrency: (e) => e.amountCurrency ? `Renewal charge currency: ${safeInline(String(e.amountCurrency))}` : null,
|
|
486
890
|
pendingKind: (e) => e.pendingKind ? `Schedule change waiting for its customer notice: ${safeInline(String(e.pendingKind))}` : null,
|
|
487
|
-
|
|
488
|
-
//
|
|
891
|
+
nextChangeAllowedOn: (e) => e.nextChangeAllowedOn ? `Customer's next schedule change allowed from: ${safeInline(String(e.nextChangeAllowedOn))}` : null,
|
|
892
|
+
// A charge the payment provider refused before any money moved (served in
|
|
893
|
+
// production since 2026-10-30): the failed payment intent that records
|
|
489
894
|
// the attempt. The SDK has checked it is a vpi_* id; printed through
|
|
490
895
|
// safeInline all the same. The next step depends on the code: a card the
|
|
491
896
|
// processor is still storing (payment_method_unvaulted) is retried as is
|
|
492
897
|
// after a short wait; every other refusal needs the request or card fixed.
|
|
493
898
|
// Both under a NEW key: the refused record does not hold the key, and the
|
|
494
899
|
// processor may refuse a resend under the same one.
|
|
900
|
+
// A refused payment list / summary filter (`payment-intents list` / `summary`): the ONE
|
|
901
|
+
// query parameter at fault, a plain parameter name the SDK has checked. The flag that sets it
|
|
902
|
+
// is named alongside, so the fix is obvious.
|
|
903
|
+
param: (e) => e.param ? `Refused parameter: ${safeInline(String(e.param))}${paramFlagHint(String(e.param))}` : null,
|
|
904
|
+
// A payment report too large to answer (report_too_large): which limit refused it. The SDK
|
|
905
|
+
// validates both as positive whole numbers.
|
|
906
|
+
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,
|
|
907
|
+
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,
|
|
495
908
|
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,
|
|
496
909
|
// On a withheld retry the SDK sets this to RETRY_WITHHELD_HELP.llmHint — the
|
|
497
910
|
// one wording for every surface — so it is printed as is.
|
|
@@ -507,20 +920,11 @@ function errorDetailLines(err) {
|
|
|
507
920
|
const lines = [];
|
|
508
921
|
for (const print of Object.values(FIELD_PRINTERS)) {
|
|
509
922
|
const line = print(source);
|
|
510
|
-
if (line !== null) lines.push(` ${scrubSecrets(line)}`);
|
|
923
|
+
if (line !== null) lines.push(` ${scrubSecrets(oneLine(line))}`);
|
|
511
924
|
}
|
|
512
925
|
return lines;
|
|
513
926
|
}
|
|
514
|
-
|
|
515
|
-
if (typeof value === "string") return scrubSecrets(value);
|
|
516
|
-
if (Array.isArray(value)) return value.map(scrubDeep);
|
|
517
|
-
if (value !== null && typeof value === "object") {
|
|
518
|
-
const out = {};
|
|
519
|
-
for (const [k, v] of Object.entries(value)) out[k] = scrubDeep(v);
|
|
520
|
-
return out;
|
|
521
|
-
}
|
|
522
|
-
return value;
|
|
523
|
-
}
|
|
927
|
+
var ERROR_JSON_CLEANING = { lines: "join", maxChars: ONE_LINE_MAX_CHARS, redact: scrubSecrets };
|
|
524
928
|
function errorJson(err) {
|
|
525
929
|
if (err instanceof VonPayError) {
|
|
526
930
|
const out = { error: err.message };
|
|
@@ -529,26 +933,28 @@ function errorJson(err) {
|
|
|
529
933
|
const value = source[field];
|
|
530
934
|
if (value !== void 0) out[field] = value;
|
|
531
935
|
}
|
|
532
|
-
return
|
|
936
|
+
return cleanJsonValue(out, ERROR_JSON_CLEANING);
|
|
533
937
|
}
|
|
534
938
|
const message = err instanceof Error ? err.message : String(err);
|
|
535
|
-
return { error: scrubSecrets(message), code: null };
|
|
939
|
+
return { error: scrubSecrets(oneLine(message)), code: null };
|
|
536
940
|
}
|
|
537
941
|
function exitWithCliError(message, text) {
|
|
538
|
-
|
|
942
|
+
auditNote({ outcome: "refused_local", code: "local_refusal" }, true);
|
|
943
|
+
return emitCliError({ error: cleanJsonValue(message, ERROR_JSON_CLEANING), code: null }, text);
|
|
539
944
|
}
|
|
540
945
|
function handleError(err) {
|
|
946
|
+
auditError(err);
|
|
541
947
|
if (jsonErrorsEnabled()) {
|
|
542
948
|
printJsonError(errorJson(err));
|
|
543
949
|
process.exit(1);
|
|
544
950
|
}
|
|
545
951
|
if (err instanceof VonPayError) {
|
|
546
|
-
console.error(chalk4.red(`Error: ${scrubSecrets(err.message)}`));
|
|
952
|
+
console.error(chalk4.red(`Error: ${scrubSecrets(oneLine(err.message))}`));
|
|
547
953
|
for (const line of errorDetailLines(err)) console.error(chalk4.dim(line));
|
|
548
954
|
process.exit(1);
|
|
549
955
|
}
|
|
550
956
|
const message = err instanceof Error ? err.message : String(err);
|
|
551
|
-
console.error(chalk4.red(`Error: ${scrubSecrets(message)}`));
|
|
957
|
+
console.error(chalk4.red(`Error: ${scrubSecrets(oneLine(message))}`));
|
|
552
958
|
process.exit(1);
|
|
553
959
|
}
|
|
554
960
|
|
|
@@ -573,11 +979,30 @@ function apiHostDetail() {
|
|
|
573
979
|
}
|
|
574
980
|
return origin === "https://checkout.vonpay.com" ? [] : [["API host", origin]];
|
|
575
981
|
}
|
|
982
|
+
var MONEY_METHODS = {
|
|
983
|
+
paymentIntents: ["create", "capture", "void"],
|
|
984
|
+
refunds: ["create"],
|
|
985
|
+
sessions: ["create"]
|
|
986
|
+
};
|
|
987
|
+
function withAuditIntent(client) {
|
|
988
|
+
for (const [resource, methods] of Object.entries(MONEY_METHODS)) {
|
|
989
|
+
const target = client[resource];
|
|
990
|
+
for (const method of methods) {
|
|
991
|
+
const original = target?.[method];
|
|
992
|
+
if (typeof original !== "function") continue;
|
|
993
|
+
target[method] = (...args) => {
|
|
994
|
+
auditIntent();
|
|
995
|
+
return original.apply(target, args);
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
return client;
|
|
1000
|
+
}
|
|
576
1001
|
function makeCheckoutClient() {
|
|
577
1002
|
const apiKey = requireApiKey();
|
|
578
1003
|
const baseUrl = getBaseUrl();
|
|
579
1004
|
refuseUnsafeLiveDestination(apiKey);
|
|
580
|
-
return new VonPayCheckout({
|
|
1005
|
+
return withAuditIntent(new VonPayCheckout({
|
|
581
1006
|
apiKey,
|
|
582
1007
|
...baseUrl ? { baseUrl } : {},
|
|
583
1008
|
// The SDK applies the same host rule at construction; the opt-in is the
|
|
@@ -587,7 +1012,86 @@ function makeCheckoutClient() {
|
|
|
587
1012
|
// The CLI prints every error itself (handleError / --json). Without this the SDK's fallback
|
|
588
1013
|
// reporter adds its own `[vonpay] …` line to stderr on EVERY API error, live key included.
|
|
589
1014
|
errorReporter: SILENT_ERROR_REPORTER
|
|
590
|
-
});
|
|
1015
|
+
}));
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// src/utils/live-key.ts
|
|
1019
|
+
import { createInterface as createInterface2 } from "readline";
|
|
1020
|
+
import chalk5 from "chalk";
|
|
1021
|
+
import { Option as Option2 } from "commander";
|
|
1022
|
+
import { heldAsLive, isLiveKey } from "@vonpay/checkout-node";
|
|
1023
|
+
var CONFIRM_WORD = "confirm";
|
|
1024
|
+
var terminal = {
|
|
1025
|
+
/**
|
|
1026
|
+
* Both stdin and stdout are a terminal. A command run by a script, a CI job or
|
|
1027
|
+
* an AI agent's shell tool has at least one of them piped.
|
|
1028
|
+
*/
|
|
1029
|
+
isInteractive() {
|
|
1030
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
1031
|
+
},
|
|
1032
|
+
/** Ask one line on the terminal. End of input or Ctrl-C answers "" (cancel). */
|
|
1033
|
+
ask(prompt) {
|
|
1034
|
+
return new Promise((resolve2) => {
|
|
1035
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
1036
|
+
let answered = false;
|
|
1037
|
+
rl.on("SIGINT", () => rl.close());
|
|
1038
|
+
rl.on("close", () => {
|
|
1039
|
+
if (!answered) resolve2("");
|
|
1040
|
+
});
|
|
1041
|
+
rl.question(prompt, (answer) => {
|
|
1042
|
+
answered = true;
|
|
1043
|
+
rl.close();
|
|
1044
|
+
resolve2(answer);
|
|
1045
|
+
});
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
function keyFingerprint(key) {
|
|
1050
|
+
const m = key.match(/^(vp_sk_live_)([A-Za-z0-9_-]{0,4})/);
|
|
1051
|
+
return m ? `${m[1]}${m[2]}\u2026` : "a key that is not a sandbox key";
|
|
1052
|
+
}
|
|
1053
|
+
async function confirmLiveAction(key, summary, opts = {}) {
|
|
1054
|
+
if (!heldAsLive(key)) return;
|
|
1055
|
+
if (!terminal.isInteractive()) {
|
|
1056
|
+
const message = `Refusing to ${summary.action} with a live key: no person is at an interactive terminal to confirm it. A live-key action must be confirmed by a person typing at a terminal (stdin and stdout both a terminal); scripts, CI jobs and AI agents cannot confirm it, and --confirm-live is no longer enough. Use a sandbox key (vp_sk_test_\u2026) for automation, or run the command yourself in a terminal. Nothing was sent.`;
|
|
1057
|
+
auditNote({ outcome: "withheld", code: "no_terminal" });
|
|
1058
|
+
exitWithCliError(message, chalk5.red(message));
|
|
1059
|
+
}
|
|
1060
|
+
if (opts.confirmLive) {
|
|
1061
|
+
console.warn(
|
|
1062
|
+
chalk5.yellow(
|
|
1063
|
+
"Note: --confirm-live no longer confirms a live action by itself. A person confirms it at the terminal."
|
|
1064
|
+
)
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
const lines = [
|
|
1068
|
+
chalk5.bold.red(`You are about to ${summary.action} with a LIVE key (${keyFingerprint(key)}).`),
|
|
1069
|
+
// At least one space after the label, however long it is.
|
|
1070
|
+
...(summary.details ?? []).map(([label, value]) => ` ${`${label}:`.padEnd(Math.max(17, label.length + 2))}${safeInline(value)}`),
|
|
1071
|
+
chalk5.yellow(summary.consequence ?? "This moves real money and cannot be undone from here.")
|
|
1072
|
+
];
|
|
1073
|
+
for (const line of lines) console.log(line);
|
|
1074
|
+
const answer = await terminal.ask(`Type "${CONFIRM_WORD}" to proceed, anything else to cancel: `);
|
|
1075
|
+
if (answer.trim() !== CONFIRM_WORD) {
|
|
1076
|
+
auditNote({ outcome: "declined", code: "declined" });
|
|
1077
|
+
const message = `Cancelled: you did not type "${CONFIRM_WORD}". Nothing was sent.`;
|
|
1078
|
+
exitWithCliError(message, chalk5.red(message));
|
|
1079
|
+
}
|
|
1080
|
+
auditNote({ approved: true });
|
|
1081
|
+
}
|
|
1082
|
+
function retiredConfirmLiveOption() {
|
|
1083
|
+
return new Option2(
|
|
1084
|
+
"--confirm-live",
|
|
1085
|
+
"No longer confirms anything: a live-key action is confirmed by a person at the terminal"
|
|
1086
|
+
).hideHelp();
|
|
1087
|
+
}
|
|
1088
|
+
function warnLiveSecretFlag(key, flag) {
|
|
1089
|
+
if (!heldAsLive(key)) return;
|
|
1090
|
+
console.warn(
|
|
1091
|
+
chalk5.yellow(
|
|
1092
|
+
`Warning: passing ${flag} as a command argument with a live key exposes it to shell history and process-list (\`ps\`). Consider piping or env-injecting the value instead.`
|
|
1093
|
+
)
|
|
1094
|
+
);
|
|
591
1095
|
}
|
|
592
1096
|
|
|
593
1097
|
// src/utils/amount.ts
|
|
@@ -603,6 +1107,11 @@ function parseMinorUnits(raw, flag) {
|
|
|
603
1107
|
}
|
|
604
1108
|
return { ok: true, value };
|
|
605
1109
|
}
|
|
1110
|
+
function positiveWholeNumber(raw, flag) {
|
|
1111
|
+
const parsed = parseMinorUnits(raw, flag);
|
|
1112
|
+
if (!parsed.ok || parsed.value < 1) throw new Error(`${flag} must be a whole number of at least 1; got ${JSON.stringify(raw)}.`);
|
|
1113
|
+
return parsed.value;
|
|
1114
|
+
}
|
|
606
1115
|
function minorUnitsOrExit(raw, flag) {
|
|
607
1116
|
const parsed = parseMinorUnits(raw, flag);
|
|
608
1117
|
if (!parsed.ok) exitWithCliError(parsed.message, `Error: ${parsed.message}`);
|
|
@@ -613,9 +1122,7 @@ function optionalMinorUnitsOrExit(raw, flag) {
|
|
|
613
1122
|
}
|
|
614
1123
|
|
|
615
1124
|
// src/utils/mask-secrets.ts
|
|
616
|
-
|
|
617
|
-
return scrubSecrets(json);
|
|
618
|
-
}
|
|
1125
|
+
var SHOW_SECRETS_CLEANING = { exactKeys: /* @__PURE__ */ new Set(["clientSecret"]) };
|
|
619
1126
|
var SHOW_SECRETS_HELP = "With --json, print the session's client secret in full. \u26A0 The output then contains a secret: do not paste it or let it reach a log";
|
|
620
1127
|
function withClientSecret(session) {
|
|
621
1128
|
if (session === null || typeof session !== "object" || Array.isArray(session)) return session;
|
|
@@ -623,26 +1130,26 @@ function withClientSecret(session) {
|
|
|
623
1130
|
return { ...session, clientSecret: session.clientSecret };
|
|
624
1131
|
}
|
|
625
1132
|
function sessionJson(session, showSecrets) {
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
return
|
|
1133
|
+
const body = cleanJsonValue(withClientSecret(session), showSecrets === true ? SHOW_SECRETS_CLEANING : {});
|
|
1134
|
+
const json = JSON.stringify(body, null, 2);
|
|
1135
|
+
return json === void 0 ? String(json) : json;
|
|
629
1136
|
}
|
|
630
1137
|
|
|
631
1138
|
// src/commands/sessions.ts
|
|
632
1139
|
var makeClient = makeCheckoutClient;
|
|
633
1140
|
function sessionLines(session) {
|
|
634
|
-
const statusColor = session.status === "succeeded" ?
|
|
1141
|
+
const statusColor = session.status === "succeeded" ? chalk6.green : session.status === "failed" ? chalk6.red : session.status === "expired" ? chalk6.gray : chalk6.yellow;
|
|
635
1142
|
const lines = [
|
|
636
1143
|
` ID: ${session.id}`,
|
|
637
1144
|
` Status: ${statusColor(session.status)}`,
|
|
638
1145
|
` Amount: ${formatMinorUnits(session.amount, session.currency)}`
|
|
639
1146
|
];
|
|
640
1147
|
if (session.paymentStatus) {
|
|
641
|
-
const paymentColor = session.paymentStatus === "paid" ?
|
|
1148
|
+
const paymentColor = session.paymentStatus === "paid" ? chalk6.green : session.paymentStatus === "held" ? chalk6.yellow : chalk6.gray;
|
|
642
1149
|
lines.push(` Payment: ${paymentColor(session.paymentStatus)}`);
|
|
643
1150
|
if (session.paymentStatus === "held") {
|
|
644
1151
|
lines.push(
|
|
645
|
-
|
|
1152
|
+
chalk6.yellow(
|
|
646
1153
|
" Held, not taken: capture it to take the money, or void it. Do not ship on a hold alone."
|
|
647
1154
|
)
|
|
648
1155
|
);
|
|
@@ -661,7 +1168,7 @@ function sessionLines(session) {
|
|
|
661
1168
|
lines.push(` Payments: ${session.vp_tx_count}`);
|
|
662
1169
|
if (session.vp_tx_count >= 2) {
|
|
663
1170
|
lines.push(
|
|
664
|
-
|
|
1171
|
+
chalk6.red(
|
|
665
1172
|
` \u26A0 Charged ${session.vp_tx_count} times: there is no single refund id. Have a person check the payments before refunding or fulfilling.`
|
|
666
1173
|
)
|
|
667
1174
|
);
|
|
@@ -671,12 +1178,44 @@ function sessionLines(session) {
|
|
|
671
1178
|
lines.push(` Expires: ${session.expiresAt}`);
|
|
672
1179
|
return lines;
|
|
673
1180
|
}
|
|
1181
|
+
function redirectHost(url) {
|
|
1182
|
+
if (url === void 0) return "none given (the account default)";
|
|
1183
|
+
let parsed;
|
|
1184
|
+
try {
|
|
1185
|
+
parsed = new URL(url);
|
|
1186
|
+
} catch {
|
|
1187
|
+
return "[not a valid address]";
|
|
1188
|
+
}
|
|
1189
|
+
const host = parsed.hostname;
|
|
1190
|
+
if (host === "") return "[not a web address]";
|
|
1191
|
+
if (host.length > 253) return "[host too long]";
|
|
1192
|
+
if (!/^([a-z0-9-]{1,63}\.)*[a-z0-9-]{1,63}$/.test(host) && !/^\[[0-9a-f:.]+\]$/.test(host)) return "[unrecognised host withheld]";
|
|
1193
|
+
return parsed.port ? `${host}:${parsed.port}` : host;
|
|
1194
|
+
}
|
|
674
1195
|
function sessionsCommand() {
|
|
675
1196
|
const sessions = new Command4("sessions").description(
|
|
676
1197
|
"Create and manage checkout sessions"
|
|
677
1198
|
);
|
|
678
|
-
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) => {
|
|
1199
|
+
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) => {
|
|
679
1200
|
const amount = minorUnitsOrExit(String(opts.amount), "--amount");
|
|
1201
|
+
if (!opts.dryRun) {
|
|
1202
|
+
const apiKey = requireApiKey();
|
|
1203
|
+
refuseUnsafeLiveDestination(apiKey);
|
|
1204
|
+
await confirmLiveAction(
|
|
1205
|
+
apiKey,
|
|
1206
|
+
{
|
|
1207
|
+
action: "create a checkout session (a payment page a buyer can pay)",
|
|
1208
|
+
details: [
|
|
1209
|
+
...apiHostDetail(),
|
|
1210
|
+
["Amount", formatMinorUnits(amount, String(opts.currency).toUpperCase())],
|
|
1211
|
+
["After paying, to", redirectHost(opts.successUrl)],
|
|
1212
|
+
["If cancelled, to", redirectHost(opts.cancelUrl)]
|
|
1213
|
+
],
|
|
1214
|
+
consequence: "A buyer can pay real money on this page until it expires."
|
|
1215
|
+
},
|
|
1216
|
+
opts
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
680
1219
|
const client = makeClient();
|
|
681
1220
|
const spinner = startSpinner("Creating session...");
|
|
682
1221
|
try {
|
|
@@ -693,16 +1232,16 @@ function sessionsCommand() {
|
|
|
693
1232
|
const result = await client.sessions.validate(params);
|
|
694
1233
|
spinner.stop();
|
|
695
1234
|
if (opts.json) {
|
|
696
|
-
console.log(
|
|
1235
|
+
console.log(jsonOutput(result));
|
|
697
1236
|
} else if (result.valid) {
|
|
698
|
-
console.log(
|
|
1237
|
+
console.log(chalk6.green("\u2713 Valid request"));
|
|
699
1238
|
if (result.warnings.length > 0) {
|
|
700
1239
|
for (const w of result.warnings) {
|
|
701
|
-
console.log(
|
|
1240
|
+
console.log(chalk6.yellow(` \u26A0 ${w}`));
|
|
702
1241
|
}
|
|
703
1242
|
}
|
|
704
1243
|
} else {
|
|
705
|
-
console.log(
|
|
1244
|
+
console.log(chalk6.red("\u2717 Invalid request"));
|
|
706
1245
|
}
|
|
707
1246
|
return;
|
|
708
1247
|
}
|
|
@@ -714,9 +1253,9 @@ function sessionsCommand() {
|
|
|
714
1253
|
if (opts.json) {
|
|
715
1254
|
console.log(sessionJson(session, opts.showSecrets));
|
|
716
1255
|
} else {
|
|
717
|
-
console.log(
|
|
1256
|
+
console.log(chalk6.green("\u2713 Session created"));
|
|
718
1257
|
console.log(` ID: ${session.id}`);
|
|
719
|
-
console.log(` URL: ${
|
|
1258
|
+
console.log(` URL: ${chalk6.cyan(session.checkoutUrl)}`);
|
|
720
1259
|
console.log(` Expires: ${session.expiresAt}`);
|
|
721
1260
|
}
|
|
722
1261
|
} catch (err) {
|
|
@@ -745,28 +1284,38 @@ function sessionsCommand() {
|
|
|
745
1284
|
|
|
746
1285
|
// src/commands/payment-intents.ts
|
|
747
1286
|
import { Command as Command5 } from "commander";
|
|
748
|
-
import
|
|
749
|
-
import { formatMinorUnits as
|
|
1287
|
+
import chalk9 from "chalk";
|
|
1288
|
+
import { formatMinorUnits as formatMinorUnits3 } from "@vonpay/checkout-node";
|
|
750
1289
|
|
|
751
1290
|
// src/utils/idempotency.ts
|
|
752
1291
|
import { randomUUID } from "crypto";
|
|
753
|
-
import
|
|
1292
|
+
import chalk7 from "chalk";
|
|
1293
|
+
import { heldAsLive as heldAsLive2 } from "@vonpay/checkout-node";
|
|
754
1294
|
function resolveIdempotencyKey(flag) {
|
|
755
1295
|
const given = flag?.trim();
|
|
756
1296
|
if (given) return { key: given, generated: false };
|
|
757
1297
|
return { key: `cli-${randomUUID()}`, generated: true };
|
|
758
1298
|
}
|
|
759
|
-
function idempotencyKeyFor(flag, what) {
|
|
1299
|
+
function idempotencyKeyFor(flag, what, apiKey) {
|
|
760
1300
|
const { key, generated } = resolveIdempotencyKey(flag);
|
|
1301
|
+
if (generated && apiKey !== void 0 && heldAsLive2(apiKey)) {
|
|
1302
|
+
const message = `Refusing to send this ${what} with a live key without --idempotency-key. Choose one key for this ${what} (for example your order id plus the action, such as "order_1042:${what}") and pass the SAME key if you ever run the command again: a new key is a new action and can move money twice. Nothing was sent.`;
|
|
1303
|
+
auditNote({ outcome: "refused_local", code: "idempotency_key_required" });
|
|
1304
|
+
exitWithCliError(message, chalk7.red(message));
|
|
1305
|
+
}
|
|
761
1306
|
if (generated) {
|
|
762
1307
|
console.error(
|
|
763
|
-
|
|
1308
|
+
chalk7.dim(
|
|
764
1309
|
`Idempotency key: ${key} (to retry this same ${what}, re-run with --idempotency-key ${key})`
|
|
765
1310
|
)
|
|
766
1311
|
);
|
|
767
1312
|
}
|
|
768
1313
|
return key;
|
|
769
1314
|
}
|
|
1315
|
+
var DISPLAYABLE_KEY = /^[A-Za-z0-9_:.-]{1,64}$/;
|
|
1316
|
+
function displayIdempotencyKey(key) {
|
|
1317
|
+
return DISPLAYABLE_KEY.test(key) ? key : "[custom key withheld]";
|
|
1318
|
+
}
|
|
770
1319
|
|
|
771
1320
|
// src/utils/metadata.ts
|
|
772
1321
|
var MAX_LEN = 256;
|
|
@@ -807,80 +1356,266 @@ function parseMetadata(pairs) {
|
|
|
807
1356
|
}
|
|
808
1357
|
return out;
|
|
809
1358
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
var terminal = {
|
|
818
|
-
/**
|
|
819
|
-
* Both stdin and stdout are a terminal. A command run by a script, a CI job or
|
|
820
|
-
* an AI agent's shell tool has at least one of them piped.
|
|
821
|
-
*/
|
|
822
|
-
isInteractive() {
|
|
823
|
-
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
824
|
-
},
|
|
825
|
-
/** Ask one line on the terminal. End of input or Ctrl-C answers "" (cancel). */
|
|
826
|
-
ask(prompt) {
|
|
827
|
-
return new Promise((resolve2) => {
|
|
828
|
-
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
829
|
-
let answered = false;
|
|
830
|
-
rl.on("SIGINT", () => rl.close());
|
|
831
|
-
rl.on("close", () => {
|
|
832
|
-
if (!answered) resolve2("");
|
|
833
|
-
});
|
|
834
|
-
rl.question(prompt, (answer) => {
|
|
835
|
-
answered = true;
|
|
836
|
-
rl.close();
|
|
837
|
-
resolve2(answer);
|
|
838
|
-
});
|
|
839
|
-
});
|
|
1359
|
+
var METADATA_FILTER_KEY = /^[A-Za-z0-9_.:-]{1,64}$/;
|
|
1360
|
+
var METADATA_FILTER_VALUE_MAX = 500;
|
|
1361
|
+
function assertMetadataFilterKey(key, flag) {
|
|
1362
|
+
if (!METADATA_FILTER_KEY.test(key)) {
|
|
1363
|
+
throw new Error(
|
|
1364
|
+
`${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))}.`
|
|
1365
|
+
);
|
|
840
1366
|
}
|
|
841
|
-
|
|
842
|
-
function keyFingerprint(key) {
|
|
843
|
-
const m = key.match(/^(vp_sk_live_)([A-Za-z0-9_-]{0,4})/);
|
|
844
|
-
return m ? `${m[1]}${m[2]}\u2026` : "a key that is not a sandbox key";
|
|
1367
|
+
return key;
|
|
845
1368
|
}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
if (
|
|
849
|
-
|
|
850
|
-
|
|
1369
|
+
function parseMetadataFilter(raw, flag) {
|
|
1370
|
+
const idx = raw.indexOf("=");
|
|
1371
|
+
if (idx === -1) throw new Error(`${flag}: expected key=value (ONE tag); got ${JSON.stringify(raw.slice(0, 80))}.`);
|
|
1372
|
+
const key = assertMetadataFilterKey(raw.slice(0, idx), flag);
|
|
1373
|
+
const value = raw.slice(idx + 1);
|
|
1374
|
+
if (value.length < 1 || value.length > METADATA_FILTER_VALUE_MAX) {
|
|
1375
|
+
throw new Error(`${flag}: the value must be 1-${METADATA_FILTER_VALUE_MAX} characters; got ${value.length}.`);
|
|
851
1376
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
1377
|
+
return { [key]: value };
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// src/commands/payment-reports.ts
|
|
1381
|
+
import chalk8 from "chalk";
|
|
1382
|
+
import { formatMinorUnits as formatMinorUnits2 } from "@vonpay/checkout-node";
|
|
1383
|
+
|
|
1384
|
+
// src/utils/list-flag.ts
|
|
1385
|
+
function parseListFlag(raw, flag, options = {}) {
|
|
1386
|
+
const items = raw.split(",").map((s) => options.lowercase ? s.trim().toLowerCase() : s.trim());
|
|
1387
|
+
if (items.length === 0 || items.some((s) => s.length === 0)) {
|
|
1388
|
+
throw new Error(`${flag} must be a comma-separated list with no empty items; got ${JSON.stringify(raw)}.`);
|
|
1389
|
+
}
|
|
1390
|
+
return [...new Set(items)];
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// src/commands/payment-reports.ts
|
|
1394
|
+
function buildFilters(opts) {
|
|
1395
|
+
const filters = {};
|
|
1396
|
+
if (opts.createdGte !== void 0) filters.createdGte = opts.createdGte;
|
|
1397
|
+
if (opts.createdLt !== void 0) filters.createdLt = opts.createdLt;
|
|
1398
|
+
if (opts.outcome !== void 0) filters.outcome = parseListFlag(opts.outcome, "--outcome", { lowercase: true });
|
|
1399
|
+
if (opts.store !== void 0) filters.store = opts.store;
|
|
1400
|
+
if (opts.metadata !== void 0) filters.metadata = parseMetadataFilter(opts.metadata, "--metadata");
|
|
1401
|
+
if (opts.cardBrand !== void 0) {
|
|
1402
|
+
filters.cardBrand = parseListFlag(opts.cardBrand, "--card-brand", { lowercase: true });
|
|
1403
|
+
}
|
|
1404
|
+
if (opts.currency !== void 0) filters.currency = opts.currency.toUpperCase();
|
|
1405
|
+
if (opts.initiator !== void 0) {
|
|
1406
|
+
const initiator = opts.initiator.toLowerCase();
|
|
1407
|
+
if (initiator !== "customer" && initiator !== "merchant") {
|
|
1408
|
+
throw new Error(`--initiator must be customer or merchant; got ${JSON.stringify(opts.initiator)}.`);
|
|
1409
|
+
}
|
|
1410
|
+
filters.initiator = initiator;
|
|
1411
|
+
}
|
|
1412
|
+
if (opts.declineCode !== void 0) {
|
|
1413
|
+
filters.declineCode = parseListFlag(opts.declineCode, "--decline-code", { lowercase: true });
|
|
1414
|
+
}
|
|
1415
|
+
return filters;
|
|
1416
|
+
}
|
|
1417
|
+
var PAYMENT_LIST_JSON_FIELDS = [
|
|
1418
|
+
"id",
|
|
1419
|
+
"status",
|
|
1420
|
+
"outcome",
|
|
1421
|
+
"failureClass",
|
|
1422
|
+
"amount",
|
|
1423
|
+
"currency",
|
|
1424
|
+
"captureMethod",
|
|
1425
|
+
"createdAt",
|
|
1426
|
+
"updatedAt",
|
|
1427
|
+
"store",
|
|
1428
|
+
"card",
|
|
1429
|
+
"declineCode",
|
|
1430
|
+
"action",
|
|
1431
|
+
"ruleCode",
|
|
1432
|
+
"networkAdvice"
|
|
1433
|
+
];
|
|
1434
|
+
function listItemJson(pi, includeMetadata) {
|
|
1435
|
+
if (includeMetadata) return pi;
|
|
1436
|
+
const src = pi;
|
|
1437
|
+
return Object.fromEntries(PAYMENT_LIST_JSON_FIELDS.filter((k) => src[k] !== void 0).map((k) => [k, src[k]]));
|
|
1438
|
+
}
|
|
1439
|
+
var ALL_DEFAULT_MAX_ITEMS = 1e4;
|
|
1440
|
+
function stoppedAtDefaultCap() {
|
|
1441
|
+
return `Stopped at ${ALL_DEFAULT_MAX_ITEMS} payments (the --all default); there may be more. Pass --max-items to read more, or narrow the window.`;
|
|
1442
|
+
}
|
|
1443
|
+
function buildListParams(opts) {
|
|
1444
|
+
const params = buildFilters(opts);
|
|
1445
|
+
if (opts.status !== void 0) {
|
|
1446
|
+
params.status = parseListFlag(opts.status, "--status", { lowercase: true });
|
|
1447
|
+
}
|
|
1448
|
+
if (opts.limit !== void 0) params.limit = positiveWholeNumber(opts.limit, "--limit");
|
|
1449
|
+
if (opts.startingAfter !== void 0) params.startingAfter = opts.startingAfter;
|
|
1450
|
+
if (opts.maxItems !== void 0 && !opts.all) throw new Error("--max-items is used with --all.");
|
|
1451
|
+
if (opts.includeMetadata && !opts.json) throw new Error("--include-metadata is used with --json.");
|
|
1452
|
+
const maxItems = opts.maxItems !== void 0 ? positiveWholeNumber(opts.maxItems, "--max-items") : opts.all ? ALL_DEFAULT_MAX_ITEMS : void 0;
|
|
1453
|
+
return { params, ...maxItems !== void 0 ? { maxItems } : {} };
|
|
1454
|
+
}
|
|
1455
|
+
function buildSummaryParams(opts) {
|
|
1456
|
+
const params = buildFilters(opts);
|
|
1457
|
+
if (opts.groupBy !== void 0) {
|
|
1458
|
+
params.groupBy = parseListFlag(opts.groupBy, "--group-by", { lowercase: true });
|
|
1459
|
+
}
|
|
1460
|
+
if (opts.groupByMetadataKey !== void 0) {
|
|
1461
|
+
params.groupByMetadataKey = assertMetadataFilterKey(opts.groupByMetadataKey, "--group-by-metadata-key");
|
|
1462
|
+
}
|
|
1463
|
+
if (opts.timeZone !== void 0) params.timeZone = opts.timeZone;
|
|
1464
|
+
return params;
|
|
1465
|
+
}
|
|
1466
|
+
function outcomeTone(outcome) {
|
|
1467
|
+
if (outcome === "approved") return chalk8.green(outcome);
|
|
1468
|
+
if (outcome === "authorized" || outcome === "in_progress" || outcome === "voided") return chalk8.yellow(outcome);
|
|
1469
|
+
return chalk8.red(outcome);
|
|
1470
|
+
}
|
|
1471
|
+
function paymentListItemLine(pi) {
|
|
1472
|
+
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() : "";
|
|
1473
|
+
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)}` : "");
|
|
1474
|
+
}
|
|
1475
|
+
function paymentListLines(page) {
|
|
1476
|
+
const lines = [` Window: ${safeInline(page.created.gte)} to ${safeInline(page.created.lt)} (end exclusive)`];
|
|
1477
|
+
if (page.data.length === 0) lines.push(" No payments match.");
|
|
1478
|
+
for (const pi of page.data) lines.push(paymentListItemLine(pi));
|
|
1479
|
+
if (page.hasMore && page.nextCursor) {
|
|
1480
|
+
lines.push(
|
|
1481
|
+
` 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`
|
|
857
1482
|
);
|
|
858
1483
|
}
|
|
1484
|
+
return lines;
|
|
1485
|
+
}
|
|
1486
|
+
function formatRate(rate) {
|
|
1487
|
+
return rate === null ? "no data" : `${(rate * 100).toFixed(1)}%`;
|
|
1488
|
+
}
|
|
1489
|
+
function metricsLine(m) {
|
|
1490
|
+
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)}`;
|
|
1491
|
+
}
|
|
1492
|
+
function summaryLines(s) {
|
|
1493
|
+
const age = typeof s.ageSeconds === "number" ? `, ${s.ageSeconds} s ago` : "";
|
|
859
1494
|
const lines = [
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1495
|
+
` Payment attempts (as the dashboard counts them), ${safeInline(s.created.gte)} to ${safeInline(s.created.lt)}, time zone ${safeInline(s.timeZone)}`,
|
|
1496
|
+
` Computed at ${safeInline(s.generatedAt)}${age} (a summary is kept 45 s: for fresh numbers change the window or wait)`,
|
|
1497
|
+
` Total: ${metricsLine(s.totals)}`
|
|
863
1498
|
];
|
|
864
|
-
for (const
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
1499
|
+
for (const a of s.totals.amounts) {
|
|
1500
|
+
lines.push(
|
|
1501
|
+
` ${safeInline(a.currency)}: approved ${safeInline(formatMinorUnits2(a.approved, a.currency))}, declined ${safeInline(formatMinorUnits2(a.declined, a.currency))}`
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
for (const g of s.groups) {
|
|
1505
|
+
const key = Object.entries(g.key).map(([dimension, value]) => `${safeInline(dimension)}=${value === null ? "(none)" : safeInline(value, 80)}`).join(" ");
|
|
1506
|
+
lines.push(` ${key}: ${metricsLine(g)}`);
|
|
869
1507
|
}
|
|
1508
|
+
if (s.groupBy.length > 0) lines.push(" Only groups with at least one attempt are shown. Add counts to combine groups; never average rates.");
|
|
1509
|
+
return lines;
|
|
870
1510
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1511
|
+
var FILTER_HELP = {
|
|
1512
|
+
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)",
|
|
1513
|
+
createdLt: "End of the window, exclusive (default: now). At most 31 days after the start (7 with summary --group-by hour)",
|
|
1514
|
+
outcome: "Only these outcomes, comma-separated (e.g. bank_declined,refused_before_bank)",
|
|
1515
|
+
store: "Only payments for this connected store hostname (e.g. shop.example.com)",
|
|
1516
|
+
metadata: "Only payments with this metadata tag, key=value (ONE tag; key 1-64 of letters, digits, _ . : -; value 1-500 characters)",
|
|
1517
|
+
cardBrand: "Only these card brands, comma-separated (visa, mastercard, amex, discover, unionpay, jcb, diners, unknown)",
|
|
1518
|
+
currency: "Only payments in this currency (e.g. USD)",
|
|
1519
|
+
initiator: "merchant (merchant-initiated charges on a saved card) or customer",
|
|
1520
|
+
declineCode: "Only declined payments with these decline codes, comma-separated (e.g. insufficient_funds)"
|
|
1521
|
+
};
|
|
1522
|
+
function addFilterOptions(cmd) {
|
|
1523
|
+
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);
|
|
876
1524
|
}
|
|
877
|
-
function
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1525
|
+
function exitOnBadFlag(err) {
|
|
1526
|
+
const message = err.message;
|
|
1527
|
+
exitWithCliError(message, chalk8.red(`Error: ${message}`));
|
|
1528
|
+
}
|
|
1529
|
+
function addPaymentReportCommands(intents) {
|
|
1530
|
+
addFilterOptions(
|
|
1531
|
+
intents.command("list").description(
|
|
1532
|
+
"List payment intents, newest first, with filters \u2014 raw data. For approval and decline rates use `payment-intents summary`"
|
|
882
1533
|
)
|
|
883
|
-
);
|
|
1534
|
+
).option("--status <list>", "Only these statuses, comma-separated (e.g. failed,succeeded)").option("--limit <n>", "Page size, up to 100 per page (default 25; larger values are treated as 100)").option(
|
|
1535
|
+
"--starting-after <cursor>",
|
|
1536
|
+
"The next-page cursor printed by a previous run, unchanged, with exactly the same filters and window (it lasts 24 hours)"
|
|
1537
|
+
).option(
|
|
1538
|
+
"--all",
|
|
1539
|
+
`Read every page, one request at a time, up to ${ALL_DEFAULT_MAX_ITEMS} payments unless --max-items says otherwise (text: printed as each payment arrives)`
|
|
1540
|
+
).option("--max-items <n>", `With --all: stop after this many payments (default ${ALL_DEFAULT_MAX_ITEMS})`).option(
|
|
1541
|
+
"--json",
|
|
1542
|
+
"Output JSON: each payment's status, outcome, amount, card, store and decline fields \u2014 not your metadata (with --all: one JSON array)"
|
|
1543
|
+
).option("--include-metadata", "With --json: print every field of each payment, your metadata included").action(async (opts) => {
|
|
1544
|
+
let built;
|
|
1545
|
+
try {
|
|
1546
|
+
built = buildListParams(opts);
|
|
1547
|
+
} catch (err) {
|
|
1548
|
+
exitOnBadFlag(err);
|
|
1549
|
+
}
|
|
1550
|
+
const client = makeCheckoutClient();
|
|
1551
|
+
const spinner = startSpinner("Listing payment intents...");
|
|
1552
|
+
try {
|
|
1553
|
+
if (opts.all) {
|
|
1554
|
+
const pager = client.paymentIntents.listAutoPaging({
|
|
1555
|
+
...built.params,
|
|
1556
|
+
...built.maxItems ? { maxItems: built.maxItems } : {}
|
|
1557
|
+
});
|
|
1558
|
+
if (opts.json) {
|
|
1559
|
+
const items = [];
|
|
1560
|
+
for await (const pi of pager) items.push(listItemJson(pi, opts.includeMetadata));
|
|
1561
|
+
spinner.stop();
|
|
1562
|
+
console.log(jsonOutput(items));
|
|
1563
|
+
if (opts.maxItems === void 0 && items.length >= ALL_DEFAULT_MAX_ITEMS) console.error(stoppedAtDefaultCap());
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
let count = 0;
|
|
1567
|
+
for await (const pi of pager) {
|
|
1568
|
+
if (count === 0) spinner.stop();
|
|
1569
|
+
console.log(paymentListItemLine(pi));
|
|
1570
|
+
count += 1;
|
|
1571
|
+
}
|
|
1572
|
+
spinner.stop();
|
|
1573
|
+
if (count === 0) console.log(" No payments match.");
|
|
1574
|
+
console.log(` ${count} payment(s).`);
|
|
1575
|
+
if (opts.maxItems === void 0 && count >= ALL_DEFAULT_MAX_ITEMS) console.error(stoppedAtDefaultCap());
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
const page = await client.paymentIntents.list(built.params);
|
|
1579
|
+
spinner.stop();
|
|
1580
|
+
if (opts.json) {
|
|
1581
|
+
console.log(jsonOutput({ ...page, data: page.data.map((pi) => listItemJson(pi, opts.includeMetadata)) }));
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
for (const line of paymentListLines(page)) console.log(line);
|
|
1585
|
+
} catch (err) {
|
|
1586
|
+
spinner.stop();
|
|
1587
|
+
handleError(err);
|
|
1588
|
+
}
|
|
1589
|
+
});
|
|
1590
|
+
addFilterOptions(
|
|
1591
|
+
intents.command("summary").description(
|
|
1592
|
+
"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"
|
|
1593
|
+
)
|
|
1594
|
+
).option("--group-by <list>", "Up to 3 of hour, day, week, store, metadata, card_brand, currency, outcome, decline_code (one time unit)").option(
|
|
1595
|
+
"--group-by-metadata-key <key>",
|
|
1596
|
+
"With --group-by metadata: the metadata key whose values become groups (1-64 of letters, digits, _ . : -)"
|
|
1597
|
+
).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) => {
|
|
1598
|
+
let params;
|
|
1599
|
+
try {
|
|
1600
|
+
params = buildSummaryParams(opts);
|
|
1601
|
+
} catch (err) {
|
|
1602
|
+
exitOnBadFlag(err);
|
|
1603
|
+
}
|
|
1604
|
+
const client = makeCheckoutClient();
|
|
1605
|
+
const spinner = startSpinner("Summarizing payments...");
|
|
1606
|
+
try {
|
|
1607
|
+
const summary = await client.paymentIntents.summary(params);
|
|
1608
|
+
spinner.stop();
|
|
1609
|
+
if (opts.json) {
|
|
1610
|
+
console.log(jsonOutput(summary));
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
for (const line of summaryLines(summary)) console.log(line);
|
|
1614
|
+
} catch (err) {
|
|
1615
|
+
spinner.stop();
|
|
1616
|
+
handleError(err);
|
|
1617
|
+
}
|
|
1618
|
+
});
|
|
884
1619
|
}
|
|
885
1620
|
|
|
886
1621
|
// src/commands/payment-intents.ts
|
|
@@ -959,28 +1694,28 @@ function statusTone(status) {
|
|
|
959
1694
|
return "gray";
|
|
960
1695
|
}
|
|
961
1696
|
function printPaymentIntent(intent) {
|
|
962
|
-
const statusColor =
|
|
1697
|
+
const statusColor = chalk9[statusTone(intent.status)];
|
|
963
1698
|
console.log(` ID: ${intent.id}`);
|
|
964
1699
|
console.log(` Status: ${statusColor(intent.status)}`);
|
|
965
1700
|
console.log(
|
|
966
|
-
` Amount: ${
|
|
1701
|
+
` Amount: ${formatMinorUnits3(intent.amount, intent.currency)}`
|
|
967
1702
|
);
|
|
968
1703
|
console.log(` Capture: ${intent.captureMethod}`);
|
|
969
1704
|
if (intent.nextAction) {
|
|
970
1705
|
console.log(
|
|
971
|
-
` Next action: ${
|
|
1706
|
+
` Next action: ${chalk9.yellow(safeDisplay(intent.nextAction.type))}`
|
|
972
1707
|
);
|
|
973
1708
|
if (intent.nextAction.type === "redirect_to_url") {
|
|
974
1709
|
console.log(
|
|
975
|
-
` Redirect: ${
|
|
1710
|
+
` Redirect: ${chalk9.yellow(safeDisplay(intent.nextAction.redirectToUrl.url))}`
|
|
976
1711
|
);
|
|
977
1712
|
}
|
|
978
1713
|
}
|
|
979
1714
|
if (intent.declineCode) {
|
|
980
|
-
console.log(` Decline code: ${
|
|
1715
|
+
console.log(` Decline code: ${chalk9.red(safeDisplay(intent.declineCode))}`);
|
|
981
1716
|
}
|
|
982
1717
|
if (intent.networkAdvice) {
|
|
983
|
-
console.log(` Network advice: ${
|
|
1718
|
+
console.log(` Network advice: ${chalk9.red(safeInline(intent.networkAdvice))}`);
|
|
984
1719
|
if (typeof intent.networkAdviceRetryAfterMs === "number") {
|
|
985
1720
|
console.log(` Retry after: ${intent.networkAdviceRetryAfterMs} ms (earliest)`);
|
|
986
1721
|
}
|
|
@@ -1018,14 +1753,14 @@ function paymentIntentsCommand() {
|
|
|
1018
1753
|
"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"
|
|
1019
1754
|
).option("--buyer-email <email>", "Buyer email for this charge").option(
|
|
1020
1755
|
"--idempotency-key <key>",
|
|
1021
|
-
"Idempotency key for this charge. Reuse the SAME key to retry the same charge safely.
|
|
1756
|
+
"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"
|
|
1022
1757
|
).addOption(retiredConfirmLiveOption()).option("--json", "Output raw JSON").action(async (raw) => {
|
|
1023
1758
|
const apiKey = requireApiKey();
|
|
1024
1759
|
const opts = { ...raw, amount: minorUnitsOrExit(raw.amount, "--amount") };
|
|
1025
1760
|
const captureMethod = opts.captureMethod?.toLowerCase();
|
|
1026
1761
|
if (captureMethod !== void 0 && captureMethod !== "automatic" && captureMethod !== "manual") {
|
|
1027
1762
|
const message = `Invalid --capture-method "${opts.captureMethod}". Expected automatic or manual.`;
|
|
1028
|
-
exitWithCliError(message,
|
|
1763
|
+
exitWithCliError(message, chalk9.red(message));
|
|
1029
1764
|
}
|
|
1030
1765
|
let params;
|
|
1031
1766
|
try {
|
|
@@ -1036,19 +1771,19 @@ function paymentIntentsCommand() {
|
|
|
1036
1771
|
handleError(err);
|
|
1037
1772
|
}
|
|
1038
1773
|
refuseUnsafeLiveDestination(apiKey);
|
|
1039
|
-
const idempotencyKey = idempotencyKeyFor(opts.idempotencyKey, "charge");
|
|
1774
|
+
const idempotencyKey = idempotencyKeyFor(opts.idempotencyKey, "charge", apiKey);
|
|
1040
1775
|
await confirmLiveAction(
|
|
1041
1776
|
apiKey,
|
|
1042
1777
|
{
|
|
1043
1778
|
action: "create a payment intent (charge a payment)",
|
|
1044
1779
|
details: [
|
|
1045
1780
|
...apiHostDetail(),
|
|
1046
|
-
["Amount",
|
|
1781
|
+
["Amount", formatMinorUnits3(params.amount, params.currency)],
|
|
1047
1782
|
["Capture", params.captureMethod ?? "automatic"],
|
|
1048
1783
|
...params.paymentMethod ? [["Saved card", params.paymentMethod.id]] : [],
|
|
1049
1784
|
...params.buyerId ? [["Buyer", params.buyerId]] : [],
|
|
1050
1785
|
...params.sessionId ? [["Session", params.sessionId]] : [],
|
|
1051
|
-
["Idempotency key", idempotencyKey]
|
|
1786
|
+
["Idempotency key", displayIdempotencyKey(idempotencyKey)]
|
|
1052
1787
|
]
|
|
1053
1788
|
},
|
|
1054
1789
|
opts
|
|
@@ -1061,9 +1796,9 @@ function paymentIntentsCommand() {
|
|
|
1061
1796
|
});
|
|
1062
1797
|
spinner.stop();
|
|
1063
1798
|
if (opts.json) {
|
|
1064
|
-
console.log(
|
|
1799
|
+
console.log(jsonOutput(intent));
|
|
1065
1800
|
} else {
|
|
1066
|
-
console.log(
|
|
1801
|
+
console.log(chalk9.green("\u2713 Payment intent created"));
|
|
1067
1802
|
printPaymentIntent(intent);
|
|
1068
1803
|
}
|
|
1069
1804
|
} catch (err) {
|
|
@@ -1080,7 +1815,7 @@ function paymentIntentsCommand() {
|
|
|
1080
1815
|
const intent = await client.paymentIntents.retrieve(id);
|
|
1081
1816
|
spinner.stop();
|
|
1082
1817
|
if (opts.json) {
|
|
1083
|
-
console.log(
|
|
1818
|
+
console.log(jsonOutput(intent));
|
|
1084
1819
|
} else {
|
|
1085
1820
|
printPaymentIntent(intent);
|
|
1086
1821
|
}
|
|
@@ -1094,7 +1829,7 @@ function paymentIntentsCommand() {
|
|
|
1094
1829
|
"Partial capture amount in minor units, a whole number (omit for full)"
|
|
1095
1830
|
).option(
|
|
1096
1831
|
"--idempotency-key <key>",
|
|
1097
|
-
"Idempotency key. Reuse the SAME key to retry safely.
|
|
1832
|
+
"Idempotency key. Reuse the SAME key to retry safely. REQUIRED on a live key; on a sandbox key generated (and printed) when omitted"
|
|
1098
1833
|
).addOption(retiredConfirmLiveOption()).option("--json", "Output raw JSON").action(
|
|
1099
1834
|
async (id, opts) => {
|
|
1100
1835
|
const apiKey = requireApiKey();
|
|
@@ -1103,7 +1838,7 @@ function paymentIntentsCommand() {
|
|
|
1103
1838
|
"--amount-to-capture"
|
|
1104
1839
|
);
|
|
1105
1840
|
refuseUnsafeLiveDestination(apiKey);
|
|
1106
|
-
const idempotencyKey = idempotencyKeyFor(opts.idempotencyKey, "capture");
|
|
1841
|
+
const idempotencyKey = idempotencyKeyFor(opts.idempotencyKey, "capture", apiKey);
|
|
1107
1842
|
await confirmLiveAction(
|
|
1108
1843
|
apiKey,
|
|
1109
1844
|
{
|
|
@@ -1115,7 +1850,7 @@ function paymentIntentsCommand() {
|
|
|
1115
1850
|
"Amount",
|
|
1116
1851
|
amountToCapture !== void 0 ? `${amountToCapture} in minor units (partial capture)` : "the full authorized amount"
|
|
1117
1852
|
],
|
|
1118
|
-
["Idempotency key", idempotencyKey]
|
|
1853
|
+
["Idempotency key", displayIdempotencyKey(idempotencyKey)]
|
|
1119
1854
|
]
|
|
1120
1855
|
},
|
|
1121
1856
|
opts
|
|
@@ -1130,10 +1865,10 @@ function paymentIntentsCommand() {
|
|
|
1130
1865
|
);
|
|
1131
1866
|
spinner.stop();
|
|
1132
1867
|
if (opts.json) {
|
|
1133
|
-
console.log(
|
|
1868
|
+
console.log(jsonOutput(intent));
|
|
1134
1869
|
} else {
|
|
1135
|
-
const captureLabel = amountToCapture !== void 0 ? ` (partial: ${
|
|
1136
|
-
console.log(
|
|
1870
|
+
const captureLabel = amountToCapture !== void 0 ? ` (partial: ${formatMinorUnits3(amountToCapture, intent.currency)})` : "";
|
|
1871
|
+
console.log(chalk9.green(`\u2713 Payment intent captured${captureLabel}`));
|
|
1137
1872
|
printPaymentIntent(intent);
|
|
1138
1873
|
}
|
|
1139
1874
|
} catch (err) {
|
|
@@ -1144,12 +1879,12 @@ function paymentIntentsCommand() {
|
|
|
1144
1879
|
);
|
|
1145
1880
|
intents.command("void").description("Void an authorized (uncaptured) payment intent").argument("<id>", "Payment intent ID (vpi_*)").option(
|
|
1146
1881
|
"--idempotency-key <key>",
|
|
1147
|
-
"Idempotency key. Reuse the SAME key to retry safely.
|
|
1882
|
+
"Idempotency key. Reuse the SAME key to retry safely. REQUIRED on a live key; on a sandbox key generated (and printed) when omitted"
|
|
1148
1883
|
).addOption(retiredConfirmLiveOption()).option("--json", "Output raw JSON").action(
|
|
1149
1884
|
async (id, opts) => {
|
|
1150
1885
|
const apiKey = requireApiKey();
|
|
1151
1886
|
refuseUnsafeLiveDestination(apiKey);
|
|
1152
|
-
const idempotencyKey = idempotencyKeyFor(opts.idempotencyKey, "void");
|
|
1887
|
+
const idempotencyKey = idempotencyKeyFor(opts.idempotencyKey, "void", apiKey);
|
|
1153
1888
|
await confirmLiveAction(
|
|
1154
1889
|
apiKey,
|
|
1155
1890
|
{
|
|
@@ -1157,7 +1892,7 @@ function paymentIntentsCommand() {
|
|
|
1157
1892
|
details: [
|
|
1158
1893
|
...apiHostDetail(),
|
|
1159
1894
|
["Payment intent", id],
|
|
1160
|
-
["Idempotency key", idempotencyKey]
|
|
1895
|
+
["Idempotency key", displayIdempotencyKey(idempotencyKey)]
|
|
1161
1896
|
],
|
|
1162
1897
|
consequence: "A voided payment cannot be captured afterwards; this cannot be undone from here."
|
|
1163
1898
|
},
|
|
@@ -1169,9 +1904,9 @@ function paymentIntentsCommand() {
|
|
|
1169
1904
|
const intent = await client.paymentIntents.void(id, { idempotencyKey });
|
|
1170
1905
|
spinner.stop();
|
|
1171
1906
|
if (opts.json) {
|
|
1172
|
-
console.log(
|
|
1907
|
+
console.log(jsonOutput(intent));
|
|
1173
1908
|
} else {
|
|
1174
|
-
console.log(
|
|
1909
|
+
console.log(chalk9.green("\u2713 Payment intent voided"));
|
|
1175
1910
|
printPaymentIntent(intent);
|
|
1176
1911
|
}
|
|
1177
1912
|
} catch (err) {
|
|
@@ -1180,13 +1915,14 @@ function paymentIntentsCommand() {
|
|
|
1180
1915
|
}
|
|
1181
1916
|
}
|
|
1182
1917
|
);
|
|
1918
|
+
addPaymentReportCommands(intents);
|
|
1183
1919
|
return intents;
|
|
1184
1920
|
}
|
|
1185
1921
|
|
|
1186
1922
|
// src/commands/refunds.ts
|
|
1187
1923
|
import { Command as Command6 } from "commander";
|
|
1188
|
-
import
|
|
1189
|
-
import { formatMinorUnits as
|
|
1924
|
+
import chalk10 from "chalk";
|
|
1925
|
+
import { formatMinorUnits as formatMinorUnits4, heldAsLive as heldAsLive3, VonPayError as VonPayError2 } from "@vonpay/checkout-node";
|
|
1190
1926
|
function present(v) {
|
|
1191
1927
|
return v !== void 0 && v.trim() !== "";
|
|
1192
1928
|
}
|
|
@@ -1214,8 +1950,83 @@ function buildRefundParams(opts, metadata) {
|
|
|
1214
1950
|
if (metadata !== void 0) params.metadata = metadata;
|
|
1215
1951
|
return params;
|
|
1216
1952
|
}
|
|
1953
|
+
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.";
|
|
1954
|
+
var SESSION_WITH_PAYMENT_INTENT_MESSAGE = "--session-id is only used with --transaction. Refund a payment intent with --payment-intent alone. Nothing was sent.";
|
|
1955
|
+
function refundSessionProblem(target, sessionId, live) {
|
|
1956
|
+
const hasSession = present(sessionId);
|
|
1957
|
+
if ("paymentIntent" in target && hasSession) return SESSION_WITH_PAYMENT_INTENT_MESSAGE;
|
|
1958
|
+
if ("transaction" in target && live && !hasSession) return TRANSACTION_NEEDS_SESSION_MESSAGE;
|
|
1959
|
+
return null;
|
|
1960
|
+
}
|
|
1961
|
+
function errorCodeSuffix(err) {
|
|
1962
|
+
const code = err instanceof VonPayError2 ? err.code : void 0;
|
|
1963
|
+
return typeof code === "string" && /^[a-z_]{1,64}$/.test(code) ? ` (error code: ${code})` : "";
|
|
1964
|
+
}
|
|
1965
|
+
function refuseLookup(message) {
|
|
1966
|
+
auditNote({ outcome: "lookup_refused", code: "lookup_failed" });
|
|
1967
|
+
return exitWithCliError(message, chalk10.red(message));
|
|
1968
|
+
}
|
|
1969
|
+
async function refundLookupDetails(client, target, sessionId, amount, namedCurrency) {
|
|
1970
|
+
const lines = [];
|
|
1971
|
+
let currency;
|
|
1972
|
+
if ("paymentIntent" in target) {
|
|
1973
|
+
let intent;
|
|
1974
|
+
try {
|
|
1975
|
+
intent = await client.paymentIntents.retrieve(target.paymentIntent);
|
|
1976
|
+
} catch (err) {
|
|
1977
|
+
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.`);
|
|
1978
|
+
}
|
|
1979
|
+
currency = intent.currency;
|
|
1980
|
+
lines.push(
|
|
1981
|
+
["Original payment", formatMinorUnits4(intent.amount, intent.currency)],
|
|
1982
|
+
["Status", String(intent.status)],
|
|
1983
|
+
["Created", String(intent.createdAt)]
|
|
1984
|
+
);
|
|
1985
|
+
} else {
|
|
1986
|
+
let session;
|
|
1987
|
+
try {
|
|
1988
|
+
session = await client.sessions.get(sessionId);
|
|
1989
|
+
} catch (err) {
|
|
1990
|
+
refuseLookup(`Refusing the refund: the session could not be looked up${errorCodeSuffix(err)}, so the transaction cannot be checked against it. Nothing was sent.`);
|
|
1991
|
+
}
|
|
1992
|
+
if (session.vp_tx_count !== 1) {
|
|
1993
|
+
refuseLookup(
|
|
1994
|
+
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."
|
|
1995
|
+
);
|
|
1996
|
+
}
|
|
1997
|
+
if (session.vp_tx_id !== target.transaction) {
|
|
1998
|
+
refuseLookup("Refusing the refund: that transaction is not this session's payment. Check the order in your own records. Nothing was sent.");
|
|
1999
|
+
}
|
|
2000
|
+
currency = session.currency;
|
|
2001
|
+
lines.push(
|
|
2002
|
+
["Session total", formatMinorUnits4(session.amount, session.currency)],
|
|
2003
|
+
["Payment", String(session.paymentStatus ?? "not reported")],
|
|
2004
|
+
["Created", String(session.createdAt)]
|
|
2005
|
+
);
|
|
2006
|
+
}
|
|
2007
|
+
let totals;
|
|
2008
|
+
try {
|
|
2009
|
+
totals = await client.refunds.list(target, { limit: 1 });
|
|
2010
|
+
} catch (err) {
|
|
2011
|
+
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.`);
|
|
2012
|
+
}
|
|
2013
|
+
lines.push(
|
|
2014
|
+
["Already refunded", formatMinorUnits4(totals.amountRefunded, currency)],
|
|
2015
|
+
[
|
|
2016
|
+
"Still refundable",
|
|
2017
|
+
`${formatMinorUnits4(totals.remainingRefundable, currency)}${totals.refundable ? "" : " \u2014 Von Payments says this payment cannot be refunded now"}`
|
|
2018
|
+
]
|
|
2019
|
+
);
|
|
2020
|
+
if (amount !== void 0 && amount > totals.remainingRefundable) {
|
|
2021
|
+
lines.push(["\u26A0 Check", "the amount to refund is MORE than what is still refundable"]);
|
|
2022
|
+
}
|
|
2023
|
+
if (namedCurrency !== void 0 && typeof currency === "string" && namedCurrency.toUpperCase() !== currency.toUpperCase()) {
|
|
2024
|
+
lines.push(["\u26A0 Check", "the currency named for this refund is NOT the payment's currency looked up above"]);
|
|
2025
|
+
}
|
|
2026
|
+
return lines;
|
|
2027
|
+
}
|
|
1217
2028
|
function printRefund(refund) {
|
|
1218
|
-
const statusColor = refund.status === "succeeded" ?
|
|
2029
|
+
const statusColor = refund.status === "succeeded" ? chalk10.green : refund.status === "failed" ? chalk10.red : chalk10.yellow;
|
|
1219
2030
|
console.log(` ID: ${refund.id}`);
|
|
1220
2031
|
if (refund.transaction !== null) {
|
|
1221
2032
|
console.log(` Transaction: ${refund.transaction}`);
|
|
@@ -1224,7 +2035,7 @@ function printRefund(refund) {
|
|
|
1224
2035
|
}
|
|
1225
2036
|
console.log(` Status: ${statusColor(refund.status)}`);
|
|
1226
2037
|
console.log(
|
|
1227
|
-
` Amount: ${
|
|
2038
|
+
` Amount: ${formatMinorUnits4(refund.amount, refund.currency)}`
|
|
1228
2039
|
);
|
|
1229
2040
|
if (refund.reason) {
|
|
1230
2041
|
console.log(` Reason: ${safeDisplay(refund.reason)}`);
|
|
@@ -1248,15 +2059,18 @@ function refundsCommand() {
|
|
|
1248
2059
|
).option("--currency <code>", "ISO 4217 currency code").option("--reason <text>", "Free-form reason (mirrored back on the record)").option(
|
|
1249
2060
|
"--metadata <pair...>",
|
|
1250
2061
|
"Metadata key=value (repeat flag for multiple pairs)"
|
|
2062
|
+
).option(
|
|
2063
|
+
"--session-id <id>",
|
|
2064
|
+
"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"
|
|
1251
2065
|
).option(
|
|
1252
2066
|
"--idempotency-key <key>",
|
|
1253
|
-
"Idempotency key. Reuse the SAME key to retry safely.
|
|
2067
|
+
"Idempotency key. Reuse the SAME key to retry safely. REQUIRED on a live key; on a sandbox key generated (and printed) when omitted"
|
|
1254
2068
|
).addOption(retiredConfirmLiveOption()).option("--json", "Output raw JSON").action(async (raw) => {
|
|
1255
2069
|
try {
|
|
1256
2070
|
resolveRefundTarget(raw);
|
|
1257
2071
|
} catch (err) {
|
|
1258
2072
|
const message = err.message;
|
|
1259
|
-
exitWithCliError(message,
|
|
2073
|
+
exitWithCliError(message, chalk10.red(`Error: ${message}`));
|
|
1260
2074
|
}
|
|
1261
2075
|
const apiKey = requireApiKey();
|
|
1262
2076
|
const opts = {
|
|
@@ -1270,8 +2084,11 @@ function refundsCommand() {
|
|
|
1270
2084
|
handleError(err);
|
|
1271
2085
|
}
|
|
1272
2086
|
const target = resolveRefundTarget(opts);
|
|
2087
|
+
const sessionProblem = refundSessionProblem(target, opts.sessionId, heldAsLive3(apiKey));
|
|
2088
|
+
if (sessionProblem) exitWithCliError(sessionProblem, chalk10.red(sessionProblem));
|
|
1273
2089
|
refuseUnsafeLiveDestination(apiKey);
|
|
1274
|
-
const idempotencyKey = idempotencyKeyFor(opts.idempotencyKey, "refund");
|
|
2090
|
+
const idempotencyKey = idempotencyKeyFor(opts.idempotencyKey, "refund", apiKey);
|
|
2091
|
+
const lookedUp = heldAsLive3(apiKey) && terminal.isInteractive() ? await refundLookupDetails(makeCheckoutClient(), target, opts.sessionId?.trim(), params.amount, params.currency) : [];
|
|
1275
2092
|
await confirmLiveAction(
|
|
1276
2093
|
apiKey,
|
|
1277
2094
|
{
|
|
@@ -1281,9 +2098,10 @@ function refundsCommand() {
|
|
|
1281
2098
|
"transaction" in target ? ["Transaction", target.transaction] : ["Payment intent", target.paymentIntent],
|
|
1282
2099
|
[
|
|
1283
2100
|
"Amount",
|
|
1284
|
-
params.amount === void 0 ? "the full remaining balance" : params.currency ?
|
|
2101
|
+
params.amount === void 0 ? "the full remaining balance" : params.currency ? formatMinorUnits4(params.amount, params.currency) : `${params.amount} in minor units`
|
|
1285
2102
|
],
|
|
1286
|
-
|
|
2103
|
+
...lookedUp,
|
|
2104
|
+
["Idempotency key", displayIdempotencyKey(idempotencyKey)]
|
|
1287
2105
|
],
|
|
1288
2106
|
consequence: "This pays money back out of the merchant's balance and cannot be reversed once settled."
|
|
1289
2107
|
},
|
|
@@ -1295,9 +2113,9 @@ function refundsCommand() {
|
|
|
1295
2113
|
const refund = await client.refunds.create(params, { idempotencyKey });
|
|
1296
2114
|
spinner.stop();
|
|
1297
2115
|
if (opts.json) {
|
|
1298
|
-
console.log(
|
|
2116
|
+
console.log(jsonOutput(refund));
|
|
1299
2117
|
} else {
|
|
1300
|
-
console.log(
|
|
2118
|
+
console.log(chalk10.green("\u2713 Refund created"));
|
|
1301
2119
|
printRefund(refund);
|
|
1302
2120
|
}
|
|
1303
2121
|
} catch (err) {
|
|
@@ -1307,23 +2125,23 @@ function refundsCommand() {
|
|
|
1307
2125
|
});
|
|
1308
2126
|
refunds.command("list").description(
|
|
1309
2127
|
"List the refunds of ONE payment and its refund totals \u2014 the way to check a refund whose outcome was unclear"
|
|
1310
|
-
).option("--payment-intent <id>", "The payment's payment intent ID (vpi_*). Use this OR --transaction").option("--transaction <id>", "The payment's transaction ID (vp_tx_*), instead of --payment-intent").option("--limit <n>", "How many refunds to show (default 25
|
|
2128
|
+
).option("--payment-intent <id>", "The payment's payment intent ID (vpi_*). Use this OR --transaction").option("--transaction <id>", "The payment's transaction ID (vp_tx_*), instead of --payment-intent").option("--limit <n>", "How many refunds to show (default 25; the API allows at most 100)").option("--starting-after <cursor>", "The next-page cursor printed by a previous run").option("--json", "Output raw JSON").action(
|
|
1311
2129
|
async (opts) => {
|
|
1312
2130
|
let target;
|
|
1313
2131
|
try {
|
|
1314
2132
|
target = resolveRefundTarget(opts);
|
|
1315
2133
|
} catch (err) {
|
|
1316
2134
|
const message = err.message.replace("A refund target is required", "A payment is required");
|
|
1317
|
-
exitWithCliError(message,
|
|
2135
|
+
exitWithCliError(message, chalk10.red(`Error: ${message}`));
|
|
1318
2136
|
}
|
|
1319
2137
|
let limit;
|
|
1320
2138
|
if (opts.limit !== void 0) {
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
2139
|
+
try {
|
|
2140
|
+
limit = positiveWholeNumber(opts.limit, "--limit");
|
|
2141
|
+
} catch (err) {
|
|
2142
|
+
const message = err.message;
|
|
2143
|
+
exitWithCliError(message, chalk10.red(`Error: ${message}`));
|
|
1325
2144
|
}
|
|
1326
|
-
limit = parsed.value;
|
|
1327
2145
|
}
|
|
1328
2146
|
const client = makeCheckoutClient();
|
|
1329
2147
|
const spinner = startSpinner("Listing refunds...");
|
|
@@ -1334,7 +2152,7 @@ function refundsCommand() {
|
|
|
1334
2152
|
});
|
|
1335
2153
|
spinner.stop();
|
|
1336
2154
|
if (opts.json) {
|
|
1337
|
-
console.log(
|
|
2155
|
+
console.log(jsonOutput(list));
|
|
1338
2156
|
return;
|
|
1339
2157
|
}
|
|
1340
2158
|
for (const line of refundListLines(list)) console.log(line);
|
|
@@ -1348,22 +2166,22 @@ function refundsCommand() {
|
|
|
1348
2166
|
}
|
|
1349
2167
|
function refundListLines(list) {
|
|
1350
2168
|
const currency = list.data[0]?.currency;
|
|
1351
|
-
const
|
|
2169
|
+
const money2 = (n) => currency ? formatMinorUnits4(n, currency) : `${n} in minor units`;
|
|
1352
2170
|
const lines = [
|
|
1353
|
-
` Refunded so far: ${
|
|
1354
|
-
` Still refundable: ${
|
|
2171
|
+
` Refunded so far: ${money2(list.amountRefunded)} (succeeded + still in progress)`,
|
|
2172
|
+
` Still refundable: ${money2(list.remainingRefundable)}${list.refundable ? "" : " \u2014 this payment cannot be refunded now"}`
|
|
1355
2173
|
];
|
|
1356
2174
|
if (list.data.length === 0) {
|
|
1357
2175
|
lines.push(" No refunds on this payment.");
|
|
1358
2176
|
}
|
|
1359
2177
|
for (const r of list.data) {
|
|
1360
|
-
const status = r.status === "succeeded" ?
|
|
2178
|
+
const status = r.status === "succeeded" ? chalk10.green(r.status) : r.status === "failed" ? chalk10.red(r.status) : chalk10.yellow(r.status);
|
|
1361
2179
|
lines.push(
|
|
1362
|
-
` ${r.id} ${status} ${
|
|
2180
|
+
` ${r.id} ${status} ${formatMinorUnits4(r.amount, r.currency)}` + (r.createdAt ? ` ${r.createdAt}` : "") + (r.idempotencyKey ? ` key ${safeDisplay(r.idempotencyKey)}` : "")
|
|
1363
2181
|
);
|
|
1364
2182
|
}
|
|
1365
2183
|
if (list.data.some((r) => r.status === "requested")) {
|
|
1366
|
-
lines.push(
|
|
2184
|
+
lines.push(chalk10.yellow(" A refund marked requested is still in progress and can still fail: do not refund again."));
|
|
1367
2185
|
}
|
|
1368
2186
|
if (list.hasMore && list.nextCursor) {
|
|
1369
2187
|
lines.push(` More: re-run with --starting-after ${safeDisplay(list.nextCursor)}`);
|
|
@@ -1373,9 +2191,9 @@ function refundListLines(list) {
|
|
|
1373
2191
|
|
|
1374
2192
|
// src/commands/tokens.ts
|
|
1375
2193
|
import { Command as Command7 } from "commander";
|
|
1376
|
-
import
|
|
2194
|
+
import chalk11 from "chalk";
|
|
1377
2195
|
function printToken(token) {
|
|
1378
|
-
const statusColor = token.status === "active" ?
|
|
2196
|
+
const statusColor = token.status === "active" ? chalk11.green : token.status === "expired" ? chalk11.yellow : chalk11.red;
|
|
1379
2197
|
console.log(` ID: ${token.id}`);
|
|
1380
2198
|
console.log(` Status: ${statusColor(token.status)}`);
|
|
1381
2199
|
const card = formatTokenCard(token.card);
|
|
@@ -1397,7 +2215,7 @@ function tokensCommand() {
|
|
|
1397
2215
|
const apiKey = requireApiKey();
|
|
1398
2216
|
if (!opts.buyerId && !opts.providerReference) {
|
|
1399
2217
|
console.warn(
|
|
1400
|
-
|
|
2218
|
+
chalk11.yellow(
|
|
1401
2219
|
"Warning: no --buyer-id or --provider-reference supplied. Providers with an iframe vault (sandbox and live alike) will fail with a 422 validation_error."
|
|
1402
2220
|
)
|
|
1403
2221
|
);
|
|
@@ -1421,9 +2239,9 @@ function tokensCommand() {
|
|
|
1421
2239
|
);
|
|
1422
2240
|
spinner.stop();
|
|
1423
2241
|
if (opts.json) {
|
|
1424
|
-
console.log(
|
|
2242
|
+
console.log(jsonOutput(token));
|
|
1425
2243
|
} else {
|
|
1426
|
-
console.log(
|
|
2244
|
+
console.log(chalk11.green("\u2713 Token created"));
|
|
1427
2245
|
printToken(token);
|
|
1428
2246
|
}
|
|
1429
2247
|
} catch (err) {
|
|
@@ -1440,35 +2258,166 @@ function formatTokenCard(card) {
|
|
|
1440
2258
|
return { card: cardLine, expires };
|
|
1441
2259
|
}
|
|
1442
2260
|
|
|
1443
|
-
// src/commands/
|
|
2261
|
+
// src/commands/subscription-products.ts
|
|
1444
2262
|
import { Command as Command8 } from "commander";
|
|
1445
|
-
import
|
|
2263
|
+
import chalk12 from "chalk";
|
|
2264
|
+
import { formatMinorUnits as formatMinorUnits5 } from "@vonpay/checkout-node";
|
|
2265
|
+
var STATUSES = ["active", "archived"];
|
|
2266
|
+
function buildListParams2(opts) {
|
|
2267
|
+
const params = {};
|
|
2268
|
+
if (opts.productRef !== void 0) params.productRef = opts.productRef;
|
|
2269
|
+
if (opts.status !== void 0) {
|
|
2270
|
+
const status = opts.status.trim().toLowerCase();
|
|
2271
|
+
if (!STATUSES.includes(status)) {
|
|
2272
|
+
throw new Error(`--status must be active or archived; got ${JSON.stringify(opts.status)}.`);
|
|
2273
|
+
}
|
|
2274
|
+
params.status = status;
|
|
2275
|
+
}
|
|
2276
|
+
if (opts.limit !== void 0) params.limit = positiveWholeNumber(opts.limit, "--limit");
|
|
2277
|
+
if (opts.startingAfter !== void 0) params.startingAfter = opts.startingAfter;
|
|
2278
|
+
return params;
|
|
2279
|
+
}
|
|
2280
|
+
function buildTermsParams(opts) {
|
|
2281
|
+
if (opts.amount === void 0) return void 0;
|
|
2282
|
+
const parsed = parseMinorUnits(opts.amount, "--amount");
|
|
2283
|
+
if (!parsed.ok) throw new Error(parsed.message);
|
|
2284
|
+
if (parsed.value < 1) throw new Error(`--amount must be at least 1 (minor units); got ${JSON.stringify(opts.amount)}.`);
|
|
2285
|
+
return { amount: parsed.value };
|
|
2286
|
+
}
|
|
2287
|
+
var LEGAL_TEXT_MAX_CHARS = 500;
|
|
2288
|
+
var CUT_MARK = "\u2026 [CUT: not the full text; use --json to read all of it]";
|
|
2289
|
+
function legalText(raw) {
|
|
2290
|
+
const clean = safeInline(raw, Number.MAX_SAFE_INTEGER);
|
|
2291
|
+
return clean.length > LEGAL_TEXT_MAX_CHARS ? { text: `${clean.slice(0, LEGAL_TEXT_MAX_CHARS)}${CUT_MARK}`, cut: true } : { text: clean, cut: false };
|
|
2292
|
+
}
|
|
2293
|
+
function money(amount, currency) {
|
|
2294
|
+
if (typeof amount !== "number") return "?";
|
|
2295
|
+
return currency ? safeInline(formatMinorUnits5(amount, currency)) : `${amount} in minor units`;
|
|
2296
|
+
}
|
|
2297
|
+
function every(count, interval) {
|
|
2298
|
+
if (!interval) return "";
|
|
2299
|
+
const unit = safeInline(interval);
|
|
2300
|
+
return typeof count === "number" && count !== 1 ? `every ${count} ${unit}s` : `every ${unit}`;
|
|
2301
|
+
}
|
|
2302
|
+
function statusTone2(status) {
|
|
2303
|
+
return status === "active" ? chalk12.green(status) : chalk12.yellow(status);
|
|
2304
|
+
}
|
|
2305
|
+
function productLines(p) {
|
|
2306
|
+
const head = ` ${safeInline(p.id)} ${statusTone2(safeInline(p.status))} ${safeInline(p.name, 120)} (ref ${safeInline(p.productRef, 128)})`;
|
|
2307
|
+
const schedule = p.interval ? `first payment ${money(p.firstPrice, p.currency)}, then ${money(p.renewalPrice, p.currency)} ${every(p.intervalCount, p.interval)}` : `${money(p.renewalPrice, p.currency)} \u2014 no renewal schedule yet (it cannot start a subscription)`;
|
|
2308
|
+
const lines = [head, ` ${schedule}`];
|
|
2309
|
+
const terms = p.terms;
|
|
2310
|
+
if (terms && typeof terms.disclosure === "string") lines.push(` Disclosure: ${legalText(terms.disclosure).text}`);
|
|
2311
|
+
return lines;
|
|
2312
|
+
}
|
|
2313
|
+
function productListLines(page) {
|
|
2314
|
+
const lines = [];
|
|
2315
|
+
if (page.data.length === 0) lines.push(" No subscription products match.");
|
|
2316
|
+
for (const p of page.data) lines.push(...productLines(p));
|
|
2317
|
+
if (page.hasMore && page.nextCursor) {
|
|
2318
|
+
lines.push(` More: re-run with the same filters and --starting-after ${safeInline(page.nextCursor)}`);
|
|
2319
|
+
}
|
|
2320
|
+
return lines;
|
|
2321
|
+
}
|
|
2322
|
+
function termsLines(t) {
|
|
2323
|
+
const disclosure = legalText(t.disclosure);
|
|
2324
|
+
const phrase = legalText(t.pricePhrase);
|
|
2325
|
+
return [
|
|
2326
|
+
` Product: ${safeInline(t.product)} (ref ${safeInline(t.productRef, 128)})`,
|
|
2327
|
+
` Disclosure: ${disclosure.text}`,
|
|
2328
|
+
` Price phrase: ${phrase.text}`,
|
|
2329
|
+
` First payment: ${money(t.firstAmount, t.currency)}`,
|
|
2330
|
+
` Renewal: ${money(t.amount, t.currency)} ${every(t.intervalCount, t.interval)}`,
|
|
2331
|
+
` Terms version: ${safeInline(t.termsVersion)}`,
|
|
2332
|
+
disclosure.cut || phrase.cut ? " The text above was cut. Read all of it with --json before you show it next to the order button." : " Show the disclosure, or your own words containing the price phrase, with your business name and how to cancel."
|
|
2333
|
+
];
|
|
2334
|
+
}
|
|
2335
|
+
function exitOnBadFlag2(err) {
|
|
2336
|
+
const message = err.message;
|
|
2337
|
+
exitWithCliError(message, chalk12.red(`Error: ${message}`));
|
|
2338
|
+
}
|
|
2339
|
+
function subscriptionProductsCommand() {
|
|
2340
|
+
const products = new Command8("subscription-products").description(
|
|
2341
|
+
"Read your subscription products and the renewal terms a sign-up states (read-only; products are created and changed in the dashboard)"
|
|
2342
|
+
);
|
|
2343
|
+
products.command("list").description("List your subscription products, newest first, each with the terms a sign-up on it states").option("--product-ref <ref>", "Only the product with this reference (your store's product number), matched exactly").option("--status <status>", "Only products with this status: active or archived").option("--limit <n>", "Page size, 1 to 100 (default 10)").option("--starting-after <id>", "The id of the last product on the previous page (printed by a previous run)").option("--json", "Output raw JSON").action(async (opts) => {
|
|
2344
|
+
let params;
|
|
2345
|
+
try {
|
|
2346
|
+
params = buildListParams2(opts);
|
|
2347
|
+
} catch (err) {
|
|
2348
|
+
exitOnBadFlag2(err);
|
|
2349
|
+
}
|
|
2350
|
+
const client = makeCheckoutClient();
|
|
2351
|
+
const spinner = startSpinner("Listing subscription products...");
|
|
2352
|
+
try {
|
|
2353
|
+
const page = await client.subscriptionProducts.list(params);
|
|
2354
|
+
spinner.stop();
|
|
2355
|
+
if (opts.json) {
|
|
2356
|
+
console.log(jsonOutput(page));
|
|
2357
|
+
return;
|
|
2358
|
+
}
|
|
2359
|
+
for (const line of productListLines(page)) console.log(line);
|
|
2360
|
+
} catch (err) {
|
|
2361
|
+
spinner.stop();
|
|
2362
|
+
handleError(err);
|
|
2363
|
+
}
|
|
2364
|
+
});
|
|
2365
|
+
products.command("terms <product>").description(
|
|
2366
|
+
"Read one subscription product's renewal terms, to show next to the order button before the sign-up charge. <product> is the product's id (vsprod_*) or its reference"
|
|
2367
|
+
).option("--amount <minor>", "An intro first payment in minor units (e.g. 100 = 1.00 USD), at or below the product's price").option("--json", "Output raw JSON").action(async (product, opts) => {
|
|
2368
|
+
let params;
|
|
2369
|
+
try {
|
|
2370
|
+
params = buildTermsParams(opts);
|
|
2371
|
+
} catch (err) {
|
|
2372
|
+
exitOnBadFlag2(err);
|
|
2373
|
+
}
|
|
2374
|
+
const client = makeCheckoutClient();
|
|
2375
|
+
const spinner = startSpinner("Reading subscription product terms...");
|
|
2376
|
+
try {
|
|
2377
|
+
const terms = await client.subscriptionProducts.terms(product, params);
|
|
2378
|
+
spinner.stop();
|
|
2379
|
+
if (opts.json) {
|
|
2380
|
+
console.log(jsonOutput(terms));
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
for (const line of termsLines(terms)) console.log(line);
|
|
2384
|
+
} catch (err) {
|
|
2385
|
+
spinner.stop();
|
|
2386
|
+
handleError(err);
|
|
2387
|
+
}
|
|
2388
|
+
});
|
|
2389
|
+
return products;
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
// src/commands/capabilities.ts
|
|
2393
|
+
import { Command as Command9 } from "commander";
|
|
2394
|
+
import chalk13 from "chalk";
|
|
1446
2395
|
function fmtBool(v) {
|
|
1447
|
-
return v ?
|
|
2396
|
+
return v ? chalk13.green("yes") : chalk13.dim("no");
|
|
1448
2397
|
}
|
|
1449
2398
|
function fmtVoidPolicy(v) {
|
|
1450
2399
|
switch (v) {
|
|
1451
2400
|
case "supported":
|
|
1452
|
-
return
|
|
2401
|
+
return chalk13.green(v);
|
|
1453
2402
|
case "rerouted_to_refund":
|
|
1454
|
-
return
|
|
2403
|
+
return chalk13.yellow(v);
|
|
1455
2404
|
case "not_supported":
|
|
1456
|
-
return
|
|
2405
|
+
return chalk13.dim(v);
|
|
1457
2406
|
default:
|
|
1458
|
-
return
|
|
2407
|
+
return chalk13.dim(String(v));
|
|
1459
2408
|
}
|
|
1460
2409
|
}
|
|
1461
2410
|
function fmtDisputeReporting(v) {
|
|
1462
|
-
return v === "tracked" ?
|
|
2411
|
+
return v === "tracked" ? chalk13.green(v) : chalk13.yellow(v);
|
|
1463
2412
|
}
|
|
1464
2413
|
function fmtSubscriptionBilling(v) {
|
|
1465
|
-
if (v === true) return
|
|
1466
|
-
if (v === false) return
|
|
1467
|
-
return
|
|
2414
|
+
if (v === true) return chalk13.yellow("yes");
|
|
2415
|
+
if (v === false) return chalk13.green("no");
|
|
2416
|
+
return chalk13.yellow("unknown");
|
|
1468
2417
|
}
|
|
1469
2418
|
function printCapabilities(caps) {
|
|
1470
2419
|
const ops = caps.supportedOperations;
|
|
1471
|
-
console.log(
|
|
2420
|
+
console.log(chalk13.bold("Supported operations"));
|
|
1472
2421
|
console.log(` auth/capture separation: ${fmtBool(ops.authCaptureSeparation)}`);
|
|
1473
2422
|
console.log(` partial capture: ${fmtBool(ops.partialCapture)}`);
|
|
1474
2423
|
console.log(` partial refund: ${fmtBool(ops.partialRefund)}`);
|
|
@@ -1483,17 +2432,17 @@ function printCapabilities(caps) {
|
|
|
1483
2432
|
console.log();
|
|
1484
2433
|
if (ops.disputeReporting !== "tracked") {
|
|
1485
2434
|
console.log(
|
|
1486
|
-
|
|
2435
|
+
chalk13.yellow(
|
|
1487
2436
|
" \u26A0 Chargebacks on this connection do NOT reach Von Payments.\n A `succeeded` payment is not evidence it was never disputed.\n Monitor and answer disputes in your processor's own dashboard \u2014\n they carry a deadline we cannot see or report."
|
|
1488
2437
|
)
|
|
1489
2438
|
);
|
|
1490
2439
|
console.log();
|
|
1491
2440
|
}
|
|
1492
|
-
console.log(
|
|
2441
|
+
console.log(chalk13.bold("Subscription billing"));
|
|
1493
2442
|
console.log(` Von Pay renews for you: ${fmtSubscriptionBilling(caps.subscriptionBilling?.enabled)}`);
|
|
1494
2443
|
if (caps.subscriptionBilling?.enabled !== false) {
|
|
1495
2444
|
console.log(
|
|
1496
|
-
|
|
2445
|
+
chalk13.yellow(
|
|
1497
2446
|
` \u26A0 Do not run renewals from your own system while this is not "no":
|
|
1498
2447
|
Von Pay may be charging these customers' renewals itself, and both
|
|
1499
2448
|
would charge them. Check again at the moment you renew.`
|
|
@@ -1501,25 +2450,25 @@ function printCapabilities(caps) {
|
|
|
1501
2450
|
);
|
|
1502
2451
|
}
|
|
1503
2452
|
console.log();
|
|
1504
|
-
console.log(
|
|
2453
|
+
console.log(chalk13.bold("Settlement currencies"));
|
|
1505
2454
|
console.log(
|
|
1506
|
-
` ${caps.settlementCurrencies.length ? caps.settlementCurrencies.join(", ") :
|
|
2455
|
+
` ${caps.settlementCurrencies.length ? caps.settlementCurrencies.join(", ") : chalk13.dim("(none reported)")}`
|
|
1507
2456
|
);
|
|
1508
2457
|
console.log();
|
|
1509
|
-
console.log(
|
|
2458
|
+
console.log(chalk13.bold("Rate limits"));
|
|
1510
2459
|
console.log(
|
|
1511
2460
|
` payment intents / minute: ${caps.rateLimits.paymentIntentsPerMinute}`
|
|
1512
2461
|
);
|
|
1513
2462
|
}
|
|
1514
2463
|
function capabilitiesCommand() {
|
|
1515
|
-
return new
|
|
2464
|
+
return new Command9("capabilities").description("Show what this merchant's binder supports").option("--json", "Output raw JSON").action(async (opts) => {
|
|
1516
2465
|
const client = makeCheckoutClient();
|
|
1517
2466
|
const spinner = startSpinner("Fetching capabilities...");
|
|
1518
2467
|
try {
|
|
1519
2468
|
const caps = await client.capabilities.get();
|
|
1520
2469
|
spinner.stop();
|
|
1521
2470
|
if (opts.json) {
|
|
1522
|
-
console.log(
|
|
2471
|
+
console.log(jsonOutput(caps));
|
|
1523
2472
|
} else {
|
|
1524
2473
|
printCapabilities(caps);
|
|
1525
2474
|
}
|
|
@@ -1531,11 +2480,43 @@ function capabilitiesCommand() {
|
|
|
1531
2480
|
}
|
|
1532
2481
|
|
|
1533
2482
|
// src/commands/trigger.ts
|
|
1534
|
-
import { Command as
|
|
2483
|
+
import { Command as Command10 } from "commander";
|
|
1535
2484
|
import { createHmac as createHmac2, randomUUID as randomUUID2 } from "crypto";
|
|
1536
|
-
import
|
|
2485
|
+
import chalk14 from "chalk";
|
|
1537
2486
|
import ora2 from "ora";
|
|
1538
2487
|
|
|
2488
|
+
// src/utils/signing-secret.ts
|
|
2489
|
+
import { randomBytes } from "crypto";
|
|
2490
|
+
var SIGNING_SECRET_ENV = "VONPAY_CLI_SIGNING_SECRET";
|
|
2491
|
+
var SIGNING_SECRET_FLAG = "--signing-secret <secret>";
|
|
2492
|
+
function generateSigningSecret() {
|
|
2493
|
+
return `whsec_${randomBytes(32).toString("base64url")}`;
|
|
2494
|
+
}
|
|
2495
|
+
function pinnedSecretProblem(value, from, apiKey) {
|
|
2496
|
+
if (apiKey !== void 0 && apiKey !== "" && value === apiKey || /^(vp_sk_|vp_pk_|vp_key_|sk_|rk_)/.test(value)) {
|
|
2497
|
+
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).`;
|
|
2498
|
+
}
|
|
2499
|
+
if (value.length < 16 || /[\s\x00-\x1f\x7f]/.test(value)) {
|
|
2500
|
+
return `${from} must be at least 16 characters with no spaces or control characters.`;
|
|
2501
|
+
}
|
|
2502
|
+
return null;
|
|
2503
|
+
}
|
|
2504
|
+
function resolveSigningSecret(flag, env = process.env, apiKey) {
|
|
2505
|
+
if (flag !== void 0) {
|
|
2506
|
+
const problem = pinnedSecretProblem(flag, "--signing-secret", apiKey);
|
|
2507
|
+
return problem ? { error: problem } : { secret: flag, source: "flag" };
|
|
2508
|
+
}
|
|
2509
|
+
const fromEnv = env[SIGNING_SECRET_ENV];
|
|
2510
|
+
if (fromEnv !== void 0 && fromEnv !== "") {
|
|
2511
|
+
const problem = pinnedSecretProblem(fromEnv, SIGNING_SECRET_ENV, apiKey);
|
|
2512
|
+
return problem ? { error: problem } : { secret: fromEnv, source: "env" };
|
|
2513
|
+
}
|
|
2514
|
+
return { secret: generateSigningSecret(), source: "generated" };
|
|
2515
|
+
}
|
|
2516
|
+
function signingSecretNotice(s) {
|
|
2517
|
+
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.`;
|
|
2518
|
+
}
|
|
2519
|
+
|
|
1539
2520
|
// src/utils/forward-to.ts
|
|
1540
2521
|
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
1541
2522
|
"localhost",
|
|
@@ -1719,7 +2700,7 @@ function warnIfUnsubscribable(event, write = (m) => console.warn(m)) {
|
|
|
1719
2700
|
if (!UNSUBSCRIBABLE_EVENTS.includes(event)) return false;
|
|
1720
2701
|
const suggestion = event === "session.succeeded" ? "charge.succeeded" : "charge.failed";
|
|
1721
2702
|
write(
|
|
1722
|
-
|
|
2703
|
+
chalk14.yellow(
|
|
1723
2704
|
`\u26A0 ${event} is emitted internally but is NOT subscribable.
|
|
1724
2705
|
The subscription API accepts the key, stores nothing and returns success, so a
|
|
1725
2706
|
real endpoint receives this event NEVER \u2014 a handler tested only against it is
|
|
@@ -1914,13 +2895,16 @@ function buildPayload(event, opts) {
|
|
|
1914
2895
|
};
|
|
1915
2896
|
}
|
|
1916
2897
|
function triggerCommand() {
|
|
1917
|
-
return new
|
|
2898
|
+
return new Command10("trigger").description("Send a test webhook event to a local URL").argument("<event>", `Event type (${SUPPORTED_EVENTS.join(", ")})`).requiredOption("--url <url>", "Local webhook URL (e.g. http://localhost:3000/webhooks)").option("--session-id <id>", "Custom session ID").option("--amount <cents>", "Amount in minor units, a whole number").option("--currency <code>", "Currency code").option(
|
|
1918
2899
|
"--url-tunnel",
|
|
1919
2900
|
"Allow a non-loopback --url (ngrok / Cloudflare Tunnel). Without it, --url must be this machine"
|
|
2901
|
+
).option(
|
|
2902
|
+
SIGNING_SECRET_FLAG,
|
|
2903
|
+
`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`
|
|
1920
2904
|
).addOption(retiredConfirmLiveOption()).action(async (event, opts) => {
|
|
1921
2905
|
if (!SUPPORTED_EVENTS.includes(event)) {
|
|
1922
2906
|
console.error(
|
|
1923
|
-
|
|
2907
|
+
chalk14.red(
|
|
1924
2908
|
`Unknown event: ${event}
|
|
1925
2909
|
Supported: ${SUPPORTED_EVENTS.join(", ")}`
|
|
1926
2910
|
)
|
|
@@ -1934,15 +2918,20 @@ Supported: ${SUPPORTED_EVENTS.join(", ")}`
|
|
|
1934
2918
|
tunnelFlag: "--url-tunnel"
|
|
1935
2919
|
});
|
|
1936
2920
|
if (!destination.ok) {
|
|
1937
|
-
console.error(
|
|
2921
|
+
console.error(chalk14.red(destination.reason));
|
|
1938
2922
|
process.exit(1);
|
|
1939
2923
|
}
|
|
1940
|
-
if (destination.warning) console.warn(
|
|
2924
|
+
if (destination.warning) console.warn(chalk14.yellow(destination.warning));
|
|
1941
2925
|
const amount = optionalMinorUnitsOrExit(
|
|
1942
2926
|
opts.amount === void 0 ? void 0 : String(opts.amount),
|
|
1943
2927
|
"--amount"
|
|
1944
2928
|
);
|
|
1945
2929
|
const apiKey = requireApiKey();
|
|
2930
|
+
const signing = resolveSigningSecret(opts.signingSecret, process.env, apiKey);
|
|
2931
|
+
if ("error" in signing) {
|
|
2932
|
+
console.error(chalk14.red(signing.error));
|
|
2933
|
+
process.exit(1);
|
|
2934
|
+
}
|
|
1946
2935
|
await confirmLiveAction(
|
|
1947
2936
|
apiKey,
|
|
1948
2937
|
{
|
|
@@ -1951,14 +2940,15 @@ Supported: ${SUPPORTED_EVENTS.join(", ")}`
|
|
|
1951
2940
|
["Event", event],
|
|
1952
2941
|
["Sent to", String(opts.url)]
|
|
1953
2942
|
],
|
|
1954
|
-
consequence: "
|
|
2943
|
+
consequence: "A synthetic event is sent to this URL, signed with this run's signing secret (never your API key)."
|
|
1955
2944
|
},
|
|
1956
2945
|
opts
|
|
1957
2946
|
);
|
|
1958
2947
|
const payload = buildPayload(event, { ...opts, amount });
|
|
1959
2948
|
const body = JSON.stringify(payload);
|
|
2949
|
+
console.log(chalk14.dim(signingSecretNotice(signing)));
|
|
1960
2950
|
const t = Math.floor(Date.now() / 1e3);
|
|
1961
|
-
const v1 = createHmac2("sha256",
|
|
2951
|
+
const v1 = createHmac2("sha256", signing.secret).update(`${t}.${body}`).digest("hex");
|
|
1962
2952
|
const signatureHeader = `t=${t},v1=${v1}`;
|
|
1963
2953
|
const spinner = ora2(`Sending ${event} to ${opts.url}...`).start();
|
|
1964
2954
|
try {
|
|
@@ -1978,45 +2968,45 @@ Supported: ${SUPPORTED_EVENTS.join(", ")}`
|
|
|
1978
2968
|
spinner.stop();
|
|
1979
2969
|
if (isRedirect(response)) {
|
|
1980
2970
|
console.error(
|
|
1981
|
-
|
|
2971
|
+
chalk14.red(
|
|
1982
2972
|
`\u2717 ${event} not delivered: ${safeDisplay(String(opts.url))} answered with a redirect (HTTP ${response.status}). It was not followed, and the signed event was not sent anywhere else. Point --url at the endpoint itself.`
|
|
1983
2973
|
)
|
|
1984
2974
|
);
|
|
1985
2975
|
process.exit(1);
|
|
1986
2976
|
}
|
|
1987
2977
|
if (response.ok) {
|
|
1988
|
-
console.log(
|
|
2978
|
+
console.log(chalk14.green(`\u2713 ${event} delivered \u2192 ${response.status}`));
|
|
1989
2979
|
} else {
|
|
1990
2980
|
console.log(
|
|
1991
|
-
|
|
2981
|
+
chalk14.yellow(
|
|
1992
2982
|
`\u26A0 ${event} delivered but got ${response.status} response`
|
|
1993
2983
|
)
|
|
1994
2984
|
);
|
|
1995
2985
|
}
|
|
1996
|
-
console.log(
|
|
2986
|
+
console.log(chalk14.dim(` Event: ${payload.id} (${payload.type})`));
|
|
1997
2987
|
console.log(
|
|
1998
|
-
|
|
2988
|
+
chalk14.dim(
|
|
1999
2989
|
` Amount: ${payload.data.amount} ${payload.data.currency}`
|
|
2000
2990
|
)
|
|
2001
2991
|
);
|
|
2002
2992
|
console.log(
|
|
2003
|
-
|
|
2993
|
+
chalk14.dim(
|
|
2004
2994
|
`
|
|
2005
2995
|
Verify with: vonpay.webhooks.constructEvent(rawBody, signatureHeader, secret)`
|
|
2006
2996
|
)
|
|
2007
2997
|
);
|
|
2008
2998
|
console.log(
|
|
2009
|
-
|
|
2010
|
-
` (
|
|
2999
|
+
chalk14.dim(
|
|
3000
|
+
` ('secret' is this run's signing secret, printed above \u2014 production webhooks are signed with your endpoint's whsec_\u2026 secret, and never with your API key)`
|
|
2011
3001
|
)
|
|
2012
3002
|
);
|
|
2013
3003
|
} catch (err) {
|
|
2014
3004
|
spinner.stop();
|
|
2015
3005
|
if (err instanceof Error && err.name === "TimeoutError") {
|
|
2016
|
-
console.error(
|
|
3006
|
+
console.error(chalk14.red(`Timeout: ${opts.url} did not respond within 10s`));
|
|
2017
3007
|
} else {
|
|
2018
3008
|
console.error(
|
|
2019
|
-
|
|
3009
|
+
chalk14.red(
|
|
2020
3010
|
`Failed to deliver: ${err instanceof Error ? err.message : err}`
|
|
2021
3011
|
)
|
|
2022
3012
|
);
|
|
@@ -2027,7 +3017,7 @@ Supported: ${SUPPORTED_EVENTS.join(", ")}`
|
|
|
2027
3017
|
}
|
|
2028
3018
|
|
|
2029
3019
|
// src/commands/listen.ts
|
|
2030
|
-
import { Command as
|
|
3020
|
+
import { Command as Command11 } from "commander";
|
|
2031
3021
|
import { VonPayCheckout as VonPayCheckout2 } from "@vonpay/checkout-node";
|
|
2032
3022
|
|
|
2033
3023
|
// ../sse-parser/dist/index.js
|
|
@@ -2128,9 +3118,9 @@ function resumeId(lastId) {
|
|
|
2128
3118
|
async function* consumeSse(opts) {
|
|
2129
3119
|
const initialBackoffMs = opts.initialBackoffMs ?? 1e3;
|
|
2130
3120
|
const maxBackoffMs = opts.maxBackoffMs ?? 3e4;
|
|
2131
|
-
const
|
|
3121
|
+
const pending2 = [];
|
|
2132
3122
|
const parser = new SseParser(
|
|
2133
|
-
(type, data) =>
|
|
3123
|
+
(type, data) => pending2.push({ type, data, lastEventId: parser.lastId || void 0 }),
|
|
2134
3124
|
{ maxPending: MAX_PENDING_CHARS, maxIdLength: MAX_EVENT_ID_LENGTH }
|
|
2135
3125
|
);
|
|
2136
3126
|
let backoffMs = initialBackoffMs;
|
|
@@ -2153,7 +3143,10 @@ async function* consumeSse(opts) {
|
|
|
2153
3143
|
response = await fetch(opts.url, {
|
|
2154
3144
|
method: "GET",
|
|
2155
3145
|
headers,
|
|
2156
|
-
signal: opts.signal
|
|
3146
|
+
signal: opts.signal,
|
|
3147
|
+
// Never follow a redirect: the request carries the API key, and a 3xx
|
|
3148
|
+
// would re-send it to wherever the answer points.
|
|
3149
|
+
redirect: "manual"
|
|
2157
3150
|
});
|
|
2158
3151
|
} catch (err) {
|
|
2159
3152
|
if (opts.signal.aborted) return;
|
|
@@ -2163,6 +3156,11 @@ async function* consumeSse(opts) {
|
|
|
2163
3156
|
backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
|
|
2164
3157
|
continue;
|
|
2165
3158
|
}
|
|
3159
|
+
if (response.status >= 300 && response.status < 400) {
|
|
3160
|
+
const error = new SseRedirectError(response.status);
|
|
3161
|
+
onLifecycle({ kind: "fatal", error });
|
|
3162
|
+
throw error;
|
|
3163
|
+
}
|
|
2166
3164
|
if (response.status === 401 || response.status === 403) {
|
|
2167
3165
|
const body = await safeReadText(response);
|
|
2168
3166
|
onLifecycle({ kind: "auth_failed", status: response.status, body });
|
|
@@ -2206,7 +3204,7 @@ async function* consumeSse(opts) {
|
|
|
2206
3204
|
} catch (err) {
|
|
2207
3205
|
feedError = err;
|
|
2208
3206
|
}
|
|
2209
|
-
for (const ev of
|
|
3207
|
+
for (const ev of pending2.splice(0)) {
|
|
2210
3208
|
if (ev.type === "timeout") {
|
|
2211
3209
|
serverInitiatedReconnect = true;
|
|
2212
3210
|
} else {
|
|
@@ -2274,6 +3272,16 @@ var SseAuthError = class extends Error {
|
|
|
2274
3272
|
status;
|
|
2275
3273
|
body;
|
|
2276
3274
|
};
|
|
3275
|
+
var SseRedirectError = class extends Error {
|
|
3276
|
+
constructor(status) {
|
|
3277
|
+
super(
|
|
3278
|
+
`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.`
|
|
3279
|
+
);
|
|
3280
|
+
this.status = status;
|
|
3281
|
+
this.name = "SseRedirectError";
|
|
3282
|
+
}
|
|
3283
|
+
status;
|
|
3284
|
+
};
|
|
2277
3285
|
function sleep(ms, signal) {
|
|
2278
3286
|
return new Promise((resolve2) => {
|
|
2279
3287
|
const timer = setTimeout(() => {
|
|
@@ -2301,24 +3309,38 @@ function noop() {
|
|
|
2301
3309
|
}
|
|
2302
3310
|
|
|
2303
3311
|
// src/utils/listen-output.ts
|
|
2304
|
-
import
|
|
3312
|
+
import chalk15 from "chalk";
|
|
3313
|
+
function parseAttemptPayload(data) {
|
|
3314
|
+
let raw;
|
|
3315
|
+
try {
|
|
3316
|
+
raw = JSON.parse(data);
|
|
3317
|
+
} catch {
|
|
3318
|
+
return null;
|
|
3319
|
+
}
|
|
3320
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
3321
|
+
const r = raw;
|
|
3322
|
+
const str2 = (k) => typeof r[k] === "string";
|
|
3323
|
+
const num = (k) => typeof r[k] === "number" && Number.isFinite(r[k]);
|
|
3324
|
+
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"));
|
|
3325
|
+
return ok ? r : null;
|
|
3326
|
+
}
|
|
2305
3327
|
function formatAttemptHuman(payload, forward) {
|
|
2306
3328
|
const ts = formatTimestamp(payload.timestamp);
|
|
2307
|
-
const eventType =
|
|
2308
|
-
const eventId =
|
|
2309
|
-
const attempt =
|
|
2310
|
-
const arrow =
|
|
3329
|
+
const eventType = chalk15.cyan(safeInline(payload.event_type, 64).padEnd(24));
|
|
3330
|
+
const eventId = chalk15.gray(safeInline(payload.event_id, 64).padEnd(28));
|
|
3331
|
+
const attempt = chalk15.gray(`attempt ${payload.attempt}`);
|
|
3332
|
+
const arrow = chalk15.gray("\u2192");
|
|
2311
3333
|
const upstream = formatUpstream(payload);
|
|
2312
|
-
const latency =
|
|
2313
|
-
const liveBadge = payload.livemode ?
|
|
3334
|
+
const latency = chalk15.gray(`${payload.latency_ms}ms`);
|
|
3335
|
+
const liveBadge = payload.livemode ? chalk15.red(" [LIVE]") : "";
|
|
2314
3336
|
let line = `[${ts}] ${eventType} ${eventId} ${attempt} ${arrow} ${upstream} ${latency}${liveBadge}`;
|
|
2315
3337
|
if (forward && forward.attempted) {
|
|
2316
|
-
const fwdArrow =
|
|
3338
|
+
const fwdArrow = chalk15.gray("\u2192 local");
|
|
2317
3339
|
if (forward.error) {
|
|
2318
|
-
line += ` ${fwdArrow} ${
|
|
3340
|
+
line += ` ${fwdArrow} ${chalk15.red(safeInline(forward.error))}`;
|
|
2319
3341
|
} else if (forward.status !== null) {
|
|
2320
3342
|
const fwdStatus = formatStatus(forward.status);
|
|
2321
|
-
const fwdLatency =
|
|
3343
|
+
const fwdLatency = chalk15.gray(`${forward.latencyMs}ms`);
|
|
2322
3344
|
line += ` ${fwdArrow} ${fwdStatus} ${fwdLatency}`;
|
|
2323
3345
|
}
|
|
2324
3346
|
}
|
|
@@ -2327,47 +3349,50 @@ function formatAttemptHuman(payload, forward) {
|
|
|
2327
3349
|
function formatAttemptJson(payload, forward) {
|
|
2328
3350
|
const body = {
|
|
2329
3351
|
type: "webhook.attempt",
|
|
2330
|
-
event_id: payload.event_id,
|
|
2331
|
-
event_type: payload.event_type,
|
|
2332
|
-
subscription_id: payload.subscription_id,
|
|
3352
|
+
event_id: safeInline(payload.event_id, 200),
|
|
3353
|
+
event_type: safeInline(payload.event_type, 200),
|
|
3354
|
+
subscription_id: safeInline(payload.subscription_id, 200),
|
|
2333
3355
|
attempt: payload.attempt,
|
|
2334
3356
|
succeeded: payload.succeeded,
|
|
2335
3357
|
response_status: payload.response_status,
|
|
2336
3358
|
latency_ms: payload.latency_ms,
|
|
2337
|
-
timestamp: payload.timestamp,
|
|
3359
|
+
timestamp: safeInline(payload.timestamp, 64),
|
|
2338
3360
|
livemode: payload.livemode
|
|
2339
3361
|
};
|
|
2340
3362
|
if (forward && forward.attempted) {
|
|
2341
3363
|
body.forward = {
|
|
2342
3364
|
status: forward.status,
|
|
2343
3365
|
latency_ms: forward.latencyMs,
|
|
2344
|
-
error: forward.error
|
|
3366
|
+
error: forward.error === null ? null : safeInline(forward.error)
|
|
2345
3367
|
};
|
|
2346
3368
|
}
|
|
2347
|
-
return JSON.stringify(body);
|
|
3369
|
+
return JSON.stringify(cleanJsonValue(body));
|
|
2348
3370
|
}
|
|
3371
|
+
var LIFECYCLE_EXACT_KEYS = /* @__PURE__ */ new Set(["signing_secret"]);
|
|
2349
3372
|
function formatLifecycleHuman(message, kind) {
|
|
2350
|
-
const tag = kind === "info" ?
|
|
3373
|
+
const tag = kind === "info" ? chalk15.cyan("[vonpay]") : kind === "warn" ? chalk15.yellow("[vonpay]") : chalk15.red("[vonpay]");
|
|
2351
3374
|
return `${tag} ${message}`;
|
|
2352
3375
|
}
|
|
2353
3376
|
function formatLifecycleJson(kind, extra = {}) {
|
|
2354
|
-
return JSON.stringify(
|
|
3377
|
+
return JSON.stringify(
|
|
3378
|
+
cleanJsonValue({ type: `cli.${kind}`, ...extra, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, { exactKeys: LIFECYCLE_EXACT_KEYS })
|
|
3379
|
+
);
|
|
2355
3380
|
}
|
|
2356
3381
|
function formatStatus(status) {
|
|
2357
|
-
if (status >= 200 && status < 300) return
|
|
2358
|
-
if (status >= 400 && status < 500) return
|
|
2359
|
-
if (status >= 500) return
|
|
2360
|
-
return
|
|
3382
|
+
if (status >= 200 && status < 300) return chalk15.green(String(status));
|
|
3383
|
+
if (status >= 400 && status < 500) return chalk15.yellow(String(status));
|
|
3384
|
+
if (status >= 500) return chalk15.red(String(status));
|
|
3385
|
+
return chalk15.gray(String(status));
|
|
2361
3386
|
}
|
|
2362
3387
|
function formatUpstream(payload) {
|
|
2363
3388
|
if (payload.response_status === null) {
|
|
2364
|
-
return
|
|
3389
|
+
return chalk15.red("timeout");
|
|
2365
3390
|
}
|
|
2366
3391
|
return formatStatus(payload.response_status);
|
|
2367
3392
|
}
|
|
2368
3393
|
function formatTimestamp(iso) {
|
|
2369
3394
|
const date = new Date(iso);
|
|
2370
|
-
if (isNaN(date.getTime())) return iso
|
|
3395
|
+
if (isNaN(date.getTime())) return safeInline(iso, 19);
|
|
2371
3396
|
const h = String(date.getHours()).padStart(2, "0");
|
|
2372
3397
|
const m = String(date.getMinutes()).padStart(2, "0");
|
|
2373
3398
|
const s = String(date.getSeconds()).padStart(2, "0");
|
|
@@ -2376,7 +3401,7 @@ function formatTimestamp(iso) {
|
|
|
2376
3401
|
|
|
2377
3402
|
// src/commands/listen.ts
|
|
2378
3403
|
function listenCommand() {
|
|
2379
|
-
return new
|
|
3404
|
+
return new Command11("listen").description(
|
|
2380
3405
|
"Watch webhook delivery attempts in real time. Optionally re-forward each event to a local URL for development."
|
|
2381
3406
|
).option(
|
|
2382
3407
|
"--forward-to <url>",
|
|
@@ -2388,7 +3413,10 @@ function listenCommand() {
|
|
|
2388
3413
|
"--events <type>",
|
|
2389
3414
|
"Only stream attempts for the given event type. Repeat the flag for multiple types.",
|
|
2390
3415
|
(value, prev) => prev ? [...prev, value] : [value]
|
|
2391
|
-
).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").
|
|
3416
|
+
).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(
|
|
3417
|
+
SIGNING_SECRET_FLAG,
|
|
3418
|
+
`With --forward-to: sign forwarded events with this secret instead of a new random one for this run (or set ${SIGNING_SECRET_ENV})`
|
|
3419
|
+
).addOption(retiredConfirmLiveOption()).action(async (opts) => {
|
|
2392
3420
|
const apiKey = opts.apiKey ?? requireApiKey();
|
|
2393
3421
|
if (opts.apiKey !== void 0) warnLiveSecretFlag(opts.apiKey, "--api-key");
|
|
2394
3422
|
const refuse = (message) => {
|
|
@@ -2411,6 +3439,12 @@ function listenCommand() {
|
|
|
2411
3439
|
forwardUrl = verdict.url;
|
|
2412
3440
|
forwardWarning = verdict.warning;
|
|
2413
3441
|
}
|
|
3442
|
+
let signing;
|
|
3443
|
+
if (forwardUrl) {
|
|
3444
|
+
const resolved = resolveSigningSecret(opts.signingSecret, process.env, apiKey);
|
|
3445
|
+
if ("error" in resolved) refuse(resolved.error);
|
|
3446
|
+
signing = resolved;
|
|
3447
|
+
}
|
|
2414
3448
|
const configuredBaseUrl = getBaseUrl();
|
|
2415
3449
|
const destinationProblem = keyDestinationProblem(apiKey, configuredBaseUrl);
|
|
2416
3450
|
if (destinationProblem) refuse(destinationProblem);
|
|
@@ -2426,7 +3460,7 @@ function listenCommand() {
|
|
|
2426
3460
|
["Stream from", baseUrl],
|
|
2427
3461
|
...forwardUrl ? [["Forward to", forwardUrl.toString()]] : []
|
|
2428
3462
|
],
|
|
2429
|
-
consequence: forwardUrl ? "Live payment events, with buyer and order details, will be shown here, and each one is signed with
|
|
3463
|
+
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."
|
|
2430
3464
|
},
|
|
2431
3465
|
opts
|
|
2432
3466
|
);
|
|
@@ -2465,6 +3499,7 @@ function listenCommand() {
|
|
|
2465
3499
|
}
|
|
2466
3500
|
controller.abort();
|
|
2467
3501
|
};
|
|
3502
|
+
auditSignalHandledByCommand();
|
|
2468
3503
|
process.once("SIGINT", () => onSignal("SIGINT"));
|
|
2469
3504
|
process.once("SIGTERM", () => onSignal("SIGTERM"));
|
|
2470
3505
|
const onLifecycle = (event) => {
|
|
@@ -2487,12 +3522,16 @@ function listenCommand() {
|
|
|
2487
3522
|
process.stderr.write(
|
|
2488
3523
|
formatLifecycleHuman(`Ready! Listening for webhook delivery attempts${filterNote}${fwdNote}.`, "info") + "\n"
|
|
2489
3524
|
);
|
|
3525
|
+
if (signing) process.stderr.write(formatLifecycleHuman(signingSecretNotice(signing), "info") + "\n");
|
|
2490
3526
|
} else {
|
|
2491
3527
|
process.stderr.write(
|
|
2492
3528
|
formatLifecycleJson("ready", {
|
|
2493
3529
|
base_url: baseUrl,
|
|
2494
3530
|
filter: opts.events ?? null,
|
|
2495
|
-
forward_to: forwardUrl?.toString() ?? null
|
|
3531
|
+
forward_to: forwardUrl?.toString() ?? null,
|
|
3532
|
+
// The generated secret in full (the local receiver needs it); a pinned one by source only.
|
|
3533
|
+
signing_secret: signing?.source === "generated" ? signing.secret : null,
|
|
3534
|
+
signing_secret_source: signing?.source ?? null
|
|
2496
3535
|
}) + "\n"
|
|
2497
3536
|
);
|
|
2498
3537
|
}
|
|
@@ -2514,12 +3553,8 @@ function listenCommand() {
|
|
|
2514
3553
|
if (sseEvent.type !== "webhook.attempt") {
|
|
2515
3554
|
continue;
|
|
2516
3555
|
}
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
payload = JSON.parse(sseEvent.data);
|
|
2520
|
-
} catch {
|
|
2521
|
-
continue;
|
|
2522
|
-
}
|
|
3556
|
+
const payload = parseAttemptPayload(sseEvent.data);
|
|
3557
|
+
if (payload === null) continue;
|
|
2523
3558
|
eventCount += 1;
|
|
2524
3559
|
let forwardOutcome = null;
|
|
2525
3560
|
if (forwardUrl && sdk) {
|
|
@@ -2527,7 +3562,7 @@ function listenCommand() {
|
|
|
2527
3562
|
const eventRecord = await sdk.webhookEvents.retrieve(payload.event_id);
|
|
2528
3563
|
forwardOutcome = await forwardPayload(eventRecord.payload, {
|
|
2529
3564
|
url: forwardUrl,
|
|
2530
|
-
signingSecret:
|
|
3565
|
+
signingSecret: signing.secret,
|
|
2531
3566
|
signal: controller.signal
|
|
2532
3567
|
});
|
|
2533
3568
|
} catch (err) {
|
|
@@ -2548,6 +3583,7 @@ function listenCommand() {
|
|
|
2548
3583
|
}
|
|
2549
3584
|
} catch (err) {
|
|
2550
3585
|
if (err instanceof SseAuthError) {
|
|
3586
|
+
auditNote({ outcome: "error", code: "auth_failed" });
|
|
2551
3587
|
if (mode === "human") {
|
|
2552
3588
|
process.stderr.write(
|
|
2553
3589
|
formatLifecycleHuman(
|
|
@@ -2562,6 +3598,10 @@ function listenCommand() {
|
|
|
2562
3598
|
}
|
|
2563
3599
|
process.exit(1);
|
|
2564
3600
|
}
|
|
3601
|
+
if (err instanceof SseRedirectError) {
|
|
3602
|
+
if (jsonErrorsEnabled()) exitWithCliError(err.message, err.message);
|
|
3603
|
+
process.exit(1);
|
|
3604
|
+
}
|
|
2565
3605
|
if (jsonErrorsEnabled()) handleError(err);
|
|
2566
3606
|
throw err;
|
|
2567
3607
|
}
|
|
@@ -2570,29 +3610,29 @@ function listenCommand() {
|
|
|
2570
3610
|
function lifecyclePayload(event) {
|
|
2571
3611
|
switch (event.kind) {
|
|
2572
3612
|
case "connecting":
|
|
2573
|
-
return { last_event_id: event.lastEventId
|
|
3613
|
+
return { last_event_id: event.lastEventId === void 0 ? null : safeInline(event.lastEventId, 200) };
|
|
2574
3614
|
case "connected":
|
|
2575
3615
|
return {};
|
|
2576
3616
|
case "disconnected":
|
|
2577
|
-
return { reason: event.reason, will_retry_in_ms: event.willRetryInMs };
|
|
3617
|
+
return { reason: safeInline(event.reason, 400), will_retry_in_ms: event.willRetryInMs };
|
|
2578
3618
|
case "auth_failed":
|
|
2579
3619
|
return { status: event.status };
|
|
2580
3620
|
case "fatal":
|
|
2581
|
-
return { error: event.error.message };
|
|
3621
|
+
return { error: safeInline(event.error.message, 400) };
|
|
2582
3622
|
}
|
|
2583
3623
|
}
|
|
2584
3624
|
function lifecycleHumanMessage(event) {
|
|
2585
3625
|
switch (event.kind) {
|
|
2586
3626
|
case "connecting":
|
|
2587
|
-
return event.lastEventId ? `Reconnecting (resume from ${event.lastEventId})` : "Connecting...";
|
|
3627
|
+
return event.lastEventId ? `Reconnecting (resume from ${safeInline(event.lastEventId, 200)})` : "Connecting...";
|
|
2588
3628
|
case "connected":
|
|
2589
3629
|
return "Connected.";
|
|
2590
3630
|
case "disconnected":
|
|
2591
|
-
return `Disconnected: ${event.reason}. Retrying in ${event.willRetryInMs}ms.`;
|
|
3631
|
+
return `Disconnected: ${safeInline(event.reason, 400)}. Retrying in ${event.willRetryInMs}ms.`;
|
|
2592
3632
|
case "auth_failed":
|
|
2593
3633
|
return `Authentication failed (HTTP ${event.status}).`;
|
|
2594
3634
|
case "fatal":
|
|
2595
|
-
return `Fatal: ${event.error.message}`;
|
|
3635
|
+
return `Fatal: ${safeInline(event.error.message, 400)}`;
|
|
2596
3636
|
}
|
|
2597
3637
|
}
|
|
2598
3638
|
function lifecycleSeverity(kind) {
|
|
@@ -2609,36 +3649,37 @@ function lifecycleSeverity(kind) {
|
|
|
2609
3649
|
}
|
|
2610
3650
|
|
|
2611
3651
|
// src/commands/health.ts
|
|
2612
|
-
import { Command as
|
|
2613
|
-
import
|
|
2614
|
-
import { VonPayError as
|
|
3652
|
+
import { Command as Command12 } from "commander";
|
|
3653
|
+
import chalk16 from "chalk";
|
|
3654
|
+
import { VonPayError as VonPayError3 } from "@vonpay/checkout-node";
|
|
2615
3655
|
function healthCommand() {
|
|
2616
|
-
return new
|
|
3656
|
+
return new Command12("health").description("Check checkout API health").option("--json", "Output raw JSON").action(async (opts) => {
|
|
2617
3657
|
const client = makeCheckoutClient();
|
|
2618
3658
|
const spinner = startSpinner("Checking API health...");
|
|
2619
3659
|
try {
|
|
2620
3660
|
const health = await client.health();
|
|
2621
3661
|
spinner.stop();
|
|
2622
3662
|
if (opts.json) {
|
|
2623
|
-
console.log(
|
|
3663
|
+
console.log(jsonOutput(health));
|
|
2624
3664
|
} else {
|
|
2625
|
-
const statusColor = health.status === "ok" ?
|
|
2626
|
-
console.log(` Status: ${statusColor(health.status)}`);
|
|
2627
|
-
console.log(` Latency: ${health.latencyMs}ms`);
|
|
3665
|
+
const statusColor = health.status === "ok" ? chalk16.green : health.status === "degraded" ? chalk16.yellow : chalk16.red;
|
|
3666
|
+
console.log(` Status: ${statusColor(safeInline(String(health.status)))}`);
|
|
3667
|
+
console.log(` Latency: ${safeInline(String(health.latencyMs))}ms`);
|
|
2628
3668
|
if (health.version) {
|
|
2629
|
-
console.log(` Version: ${health.version}`);
|
|
3669
|
+
console.log(` Version: ${safeInline(String(health.version))}`);
|
|
2630
3670
|
}
|
|
2631
3671
|
}
|
|
2632
3672
|
} catch (err) {
|
|
2633
3673
|
spinner.stop();
|
|
3674
|
+
auditError(err);
|
|
2634
3675
|
if (jsonErrorsEnabled()) handleError(err);
|
|
2635
|
-
if (err instanceof
|
|
2636
|
-
console.error(
|
|
2637
|
-
console.error(
|
|
3676
|
+
if (err instanceof VonPayError3) {
|
|
3677
|
+
console.error(chalk16.red(`API error: ${scrubSecrets(safeInline(err.message, 2e3))}`));
|
|
3678
|
+
console.error(chalk16.dim(` Code: ${safeInline(String(err.code))}`));
|
|
2638
3679
|
} else {
|
|
2639
3680
|
console.error(
|
|
2640
|
-
|
|
2641
|
-
`Cannot reach API: ${scrubSecrets(err instanceof Error ? err.message : String(err))}`
|
|
3681
|
+
chalk16.red(
|
|
3682
|
+
`Cannot reach API: ${scrubSecrets(safeInline(err instanceof Error ? err.message : String(err), 2e3))}`
|
|
2642
3683
|
)
|
|
2643
3684
|
);
|
|
2644
3685
|
}
|
|
@@ -2648,14 +3689,14 @@ function healthCommand() {
|
|
|
2648
3689
|
}
|
|
2649
3690
|
|
|
2650
3691
|
// src/commands/doctor.ts
|
|
2651
|
-
import { Command as
|
|
2652
|
-
import
|
|
3692
|
+
import { Command as Command13 } from "commander";
|
|
3693
|
+
import chalk17 from "chalk";
|
|
2653
3694
|
import ora3 from "ora";
|
|
2654
3695
|
import { homedir as homedir2 } from "os";
|
|
2655
3696
|
import { existsSync as existsSync3 } from "fs";
|
|
2656
3697
|
import {
|
|
2657
3698
|
VonPayCheckout as VonPayCheckout3,
|
|
2658
|
-
VonPayError as
|
|
3699
|
+
VonPayError as VonPayError4,
|
|
2659
3700
|
helpFor
|
|
2660
3701
|
} from "@vonpay/checkout-node";
|
|
2661
3702
|
var RELEVANT_ENV_VARS = [
|
|
@@ -2679,7 +3720,8 @@ function maskPrefix(key) {
|
|
|
2679
3720
|
if (!m) return void 0;
|
|
2680
3721
|
return `${m[1]}${m[3]}\u2026`;
|
|
2681
3722
|
}
|
|
2682
|
-
|
|
3723
|
+
var DOCTOR_JSON_CLEANING = { exactKeys: /* @__PURE__ */ new Set(["apiKeyPrefix"]) };
|
|
3724
|
+
function keyMode2(key) {
|
|
2683
3725
|
if (!key) return void 0;
|
|
2684
3726
|
return secretKeyMode(key) ?? "not_a_secret_key";
|
|
2685
3727
|
}
|
|
@@ -2705,10 +3747,10 @@ async function gather() {
|
|
|
2705
3747
|
present: existsSync3(
|
|
2706
3748
|
`${process.env.HOME ?? process.env.USERPROFILE ?? ""}/.vonpay/config.json`
|
|
2707
3749
|
),
|
|
2708
|
-
keyType: config.checkout?.apiKey ?
|
|
3750
|
+
keyType: config.checkout?.apiKey ? keyMode2(config.checkout?.apiKey) : void 0
|
|
2709
3751
|
},
|
|
2710
3752
|
apiKeyPrefix: maskPrefix(apiKey),
|
|
2711
|
-
apiKeyMode:
|
|
3753
|
+
apiKeyMode: keyMode2(apiKey),
|
|
2712
3754
|
health: { baseUrl, reachable: false },
|
|
2713
3755
|
hints: []
|
|
2714
3756
|
};
|
|
@@ -2802,7 +3844,7 @@ async function gather() {
|
|
|
2802
3844
|
);
|
|
2803
3845
|
}
|
|
2804
3846
|
} catch (err) {
|
|
2805
|
-
if (err instanceof
|
|
3847
|
+
if (err instanceof VonPayError4) {
|
|
2806
3848
|
const help = helpFor(err.code);
|
|
2807
3849
|
report.validateProbe = {
|
|
2808
3850
|
ok: false,
|
|
@@ -2833,47 +3875,47 @@ async function gather() {
|
|
|
2833
3875
|
}
|
|
2834
3876
|
function renderHuman(r) {
|
|
2835
3877
|
const lines = [];
|
|
2836
|
-
lines.push(
|
|
2837
|
-
lines.push(
|
|
3878
|
+
lines.push(chalk17.bold.blue("vonpay doctor \u2014 diagnostic report"));
|
|
3879
|
+
lines.push(chalk17.dim(`generated ${r.generatedAt}`));
|
|
2838
3880
|
lines.push("");
|
|
2839
|
-
lines.push(
|
|
3881
|
+
lines.push(chalk17.bold("Runtime"));
|
|
2840
3882
|
lines.push(` CLI: ${r.cli.version}`);
|
|
2841
3883
|
lines.push(` Node: ${r.runtime.node}`);
|
|
2842
3884
|
lines.push(` Platform: ${r.runtime.platform} (${r.runtime.arch})`);
|
|
2843
3885
|
lines.push(` CWD: ${r.runtime.cwd}`);
|
|
2844
3886
|
lines.push("");
|
|
2845
|
-
lines.push(
|
|
3887
|
+
lines.push(chalk17.bold("API key"));
|
|
2846
3888
|
if (r.apiKeyPrefix) {
|
|
2847
3889
|
lines.push(` Prefix: ${r.apiKeyPrefix}`);
|
|
2848
3890
|
lines.push(` Mode: ${r.apiKeyMode}`);
|
|
2849
3891
|
} else {
|
|
2850
|
-
lines.push(
|
|
3892
|
+
lines.push(chalk17.dim(" (no key found in env or ~/.vonpay/config.json)"));
|
|
2851
3893
|
}
|
|
2852
3894
|
lines.push("");
|
|
2853
|
-
lines.push(
|
|
3895
|
+
lines.push(chalk17.bold("Env vars present (names only)"));
|
|
2854
3896
|
if (r.envVarsPresent.length === 0) {
|
|
2855
|
-
lines.push(
|
|
3897
|
+
lines.push(chalk17.dim(" (none of the expected vonpay env vars are set)"));
|
|
2856
3898
|
} else {
|
|
2857
3899
|
for (const n of r.envVarsPresent) lines.push(` \u2713 ${n}`);
|
|
2858
3900
|
}
|
|
2859
3901
|
lines.push("");
|
|
2860
|
-
lines.push(
|
|
3902
|
+
lines.push(chalk17.bold("Health probe"));
|
|
2861
3903
|
if (r.health.reachable) {
|
|
2862
|
-
const statusColor = r.health.status === "ok" ?
|
|
2863
|
-
lines.push(` Reachable: ${
|
|
3904
|
+
const statusColor = r.health.status === "ok" ? chalk17.green : r.health.status === "degraded" ? chalk17.yellow : chalk17.red;
|
|
3905
|
+
lines.push(` Reachable: ${chalk17.green("yes")}`);
|
|
2864
3906
|
lines.push(` Status: ${statusColor(r.health.status ?? "?")}`);
|
|
2865
3907
|
lines.push(` Latency: ${r.health.latencyMs}ms`);
|
|
2866
3908
|
} else {
|
|
2867
|
-
lines.push(` Reachable: ${
|
|
3909
|
+
lines.push(` Reachable: ${chalk17.red("no")}`);
|
|
2868
3910
|
lines.push(` Error: ${r.health.error}`);
|
|
2869
3911
|
}
|
|
2870
3912
|
lines.push("");
|
|
2871
3913
|
if (r.validateProbe) {
|
|
2872
|
-
lines.push(
|
|
3914
|
+
lines.push(chalk17.bold("sessions.validate probe ($14.99 USD dry-run)"));
|
|
2873
3915
|
if (r.validateProbe.ok) {
|
|
2874
|
-
lines.push(` Result: ${
|
|
3916
|
+
lines.push(` Result: ${chalk17.green("valid")}`);
|
|
2875
3917
|
} else {
|
|
2876
|
-
lines.push(` Result: ${
|
|
3918
|
+
lines.push(` Result: ${chalk17.red("invalid")}`);
|
|
2877
3919
|
if (r.validateProbe.error) {
|
|
2878
3920
|
lines.push(` Code: ${r.validateProbe.error.code}`);
|
|
2879
3921
|
lines.push(` Action: ${r.validateProbe.error.nextAction}`);
|
|
@@ -2886,7 +3928,7 @@ function renderHuman(r) {
|
|
|
2886
3928
|
lines.push("");
|
|
2887
3929
|
}
|
|
2888
3930
|
if (r.hints.length > 0) {
|
|
2889
|
-
lines.push(
|
|
3931
|
+
lines.push(chalk17.bold("Hints"));
|
|
2890
3932
|
for (const h of r.hints) lines.push(` \u2022 ${h}`);
|
|
2891
3933
|
lines.push("");
|
|
2892
3934
|
}
|
|
@@ -2963,7 +4005,7 @@ function renderForLlm(r) {
|
|
|
2963
4005
|
return out.join("\n");
|
|
2964
4006
|
}
|
|
2965
4007
|
function doctorCommand() {
|
|
2966
|
-
return new
|
|
4008
|
+
return new Command13("doctor").description(
|
|
2967
4009
|
"Diagnose your Von Payments environment \u2014 runtime, env vars, live API probe; outputs human / JSON / LLM-readable formats"
|
|
2968
4010
|
).option("--json", "Output JSON (machine-parseable)").option(
|
|
2969
4011
|
"--for-llm",
|
|
@@ -2978,7 +4020,7 @@ function doctorCommand() {
|
|
|
2978
4020
|
const report = await gather();
|
|
2979
4021
|
spinner.stop();
|
|
2980
4022
|
if (opts.json) {
|
|
2981
|
-
console.log(
|
|
4023
|
+
console.log(jsonOutput(report, DOCTOR_JSON_CLEANING));
|
|
2982
4024
|
} else if (opts.forLlm) {
|
|
2983
4025
|
console.log(renderForLlm(report));
|
|
2984
4026
|
} else {
|
|
@@ -2988,8 +4030,8 @@ function doctorCommand() {
|
|
|
2988
4030
|
}
|
|
2989
4031
|
|
|
2990
4032
|
// src/commands/checkout.ts
|
|
2991
|
-
function registerCheckout(
|
|
2992
|
-
const checkout =
|
|
4033
|
+
function registerCheckout(program) {
|
|
4034
|
+
const checkout = program.command("checkout").description("Checkout integration tools");
|
|
2993
4035
|
checkout.hook("preAction", (_checkout, actionCommand) => {
|
|
2994
4036
|
setJsonErrors(actionCommand.opts().json === true);
|
|
2995
4037
|
});
|
|
@@ -3000,6 +4042,7 @@ function registerCheckout(program2) {
|
|
|
3000
4042
|
checkout.addCommand(paymentIntentsCommand());
|
|
3001
4043
|
checkout.addCommand(refundsCommand());
|
|
3002
4044
|
checkout.addCommand(tokensCommand());
|
|
4045
|
+
checkout.addCommand(subscriptionProductsCommand());
|
|
3003
4046
|
checkout.addCommand(capabilitiesCommand());
|
|
3004
4047
|
checkout.addCommand(triggerCommand());
|
|
3005
4048
|
checkout.addCommand(listenCommand());
|
|
@@ -3007,12 +4050,32 @@ function registerCheckout(program2) {
|
|
|
3007
4050
|
checkout.addCommand(doctorCommand());
|
|
3008
4051
|
}
|
|
3009
4052
|
|
|
4053
|
+
// src/program.ts
|
|
4054
|
+
function commandPath(cmd) {
|
|
4055
|
+
const names = [];
|
|
4056
|
+
for (let c = cmd; c && c.parent; c = c.parent) names.unshift(c.name());
|
|
4057
|
+
return names.join(" ");
|
|
4058
|
+
}
|
|
4059
|
+
function buildProgram() {
|
|
4060
|
+
const program = new Command14();
|
|
4061
|
+
program.name("vonpay").description("Von Payments developer tools").version(CLI_VERSION).option(
|
|
4062
|
+
ALLOW_CUSTOM_BASE_URL_FLAG,
|
|
4063
|
+
"With VONPAY_ALLOW_CUSTOM_BASE_URL=1: send a live key to a base URL that is not a Von Payments host (a proxy you run)"
|
|
4064
|
+
).hook("preAction", (_root, actionCommand) => {
|
|
4065
|
+
setAllowCustomBaseUrlFlag(program.opts().allowCustomBaseUrl === true);
|
|
4066
|
+
try {
|
|
4067
|
+
auditStart(commandPath(actionCommand), CLI_VERSION, actionCommand.opts(), actionCommand.args);
|
|
4068
|
+
} catch {
|
|
4069
|
+
}
|
|
4070
|
+
});
|
|
4071
|
+
registerCheckout(program);
|
|
4072
|
+
const withUsageAudit = (cmd) => {
|
|
4073
|
+
cmd.exitOverride((err) => auditUsageError(commandPath(cmd), CLI_VERSION, err.code, err.exitCode));
|
|
4074
|
+
cmd.commands.forEach(withUsageAudit);
|
|
4075
|
+
};
|
|
4076
|
+
withUsageAudit(program);
|
|
4077
|
+
return program;
|
|
4078
|
+
}
|
|
4079
|
+
|
|
3010
4080
|
// src/index.ts
|
|
3011
|
-
|
|
3012
|
-
var pkg2 = JSON.parse(
|
|
3013
|
-
readFileSync4(join3(__dirname, "..", "package.json"), "utf-8")
|
|
3014
|
-
);
|
|
3015
|
-
var program = new Command13();
|
|
3016
|
-
program.name("vonpay").description("Von Payments developer tools").version(pkg2.version);
|
|
3017
|
-
registerCheckout(program);
|
|
3018
|
-
program.parse();
|
|
4081
|
+
buildProgram().parse();
|