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