@vonpay/checkout-cli 0.15.1 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +218 -29
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ All commands are namespaced under `vonpay checkout`:
|
|
|
53
53
|
| `vonpay checkout refunds list` | List one payment's refunds and its refund totals (`--payment-intent` or `--transaction`) |
|
|
54
54
|
| `vonpay checkout tokens create` | Create a payment-method token for saved-card / MIT flows |
|
|
55
55
|
| `vonpay checkout capabilities` | Show what this merchant's binder supports |
|
|
56
|
-
| `vonpay checkout trigger <event>` | Trigger a synthetic webhook event against your local endpoint (`
|
|
56
|
+
| `vonpay checkout trigger <event>` | Trigger a synthetic webhook event against your local endpoint (`charge.*`, `refund.failed`, `payment_intent.*`, `mirror.order.created`, `subscription.*`, `merchant.card_testing_suspected`, `session.*`; `vonpay checkout trigger --help` lists them all) |
|
|
57
57
|
| `vonpay checkout listen` | Stream live webhook delivery attempts; optionally re-forward each to a local URL with `--forward-to` |
|
|
58
58
|
| `vonpay checkout health` | Check API health and latency |
|
|
59
59
|
| `vonpay checkout doctor` | Diagnose runtime + env + live API probe |
|
package/dist/index.js
CHANGED
|
@@ -36,6 +36,12 @@ function emitCliError(json, text) {
|
|
|
36
36
|
// src/utils/key-mode.ts
|
|
37
37
|
import { isLiveKey } from "@vonpay/checkout-node";
|
|
38
38
|
var TEST_SECRET_KEY_PREFIX = "vp_sk_test_";
|
|
39
|
+
function requiresLiveConfirmation(key) {
|
|
40
|
+
if (typeof key !== "string") return true;
|
|
41
|
+
if (key.startsWith(TEST_SECRET_KEY_PREFIX)) return false;
|
|
42
|
+
if (key.startsWith("vp_pk_")) return false;
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
39
45
|
function secretKeyMode(key) {
|
|
40
46
|
if (typeof key !== "string") return null;
|
|
41
47
|
if (isLiveKey(key)) return "live";
|
|
@@ -373,8 +379,19 @@ import {
|
|
|
373
379
|
RETRY_WITHHELD_HELP,
|
|
374
380
|
VonPayError
|
|
375
381
|
} from "@vonpay/checkout-node";
|
|
382
|
+
|
|
383
|
+
// src/utils/secret-patterns.ts
|
|
376
384
|
var SECRET_PATTERNS = [
|
|
385
|
+
// Checkout-session client secret (2026-10-21): keep the session id, which is
|
|
386
|
+
// useful in a support thread, and drop the secret after `_secret_`.
|
|
387
|
+
[/(vp_cs_(?:test|live)_[A-Za-z0-9_-]{1,64}?)_secret_[A-Za-z0-9]+/g, "$1_secret_***"],
|
|
377
388
|
[/vp_sk_(?:test|live)_[A-Za-z0-9_-]+/g, "vp_sk_***"],
|
|
389
|
+
// A legacy API key (`vp_key_*`), which checkout still accepts and treats as
|
|
390
|
+
// secret, and a single-use subscription portal code (`vsc_` + 43 base64url
|
|
391
|
+
// characters; a longer run is taken whole). Both case-sensitive, as
|
|
392
|
+
// checkout's own rules (src/lib/scrub.ts). Added 2026-10-22.
|
|
393
|
+
[/vp_key_(?:test|live)_[A-Za-z0-9_-]+/g, "vp_key_***"],
|
|
394
|
+
[/vsc_[A-Za-z0-9_-]{43,}/g, "vsc_***"],
|
|
378
395
|
[/ss_(?:test|live)_[A-Za-z0-9_-]+/g, "ss_***"],
|
|
379
396
|
[/whsec_[A-Za-z0-9_-]+/g, "whsec_***"],
|
|
380
397
|
[/sk_(?:test|live)_[A-Za-z0-9_-]+/g, "sk_***"],
|
|
@@ -385,6 +402,25 @@ function scrubSecrets(s) {
|
|
|
385
402
|
for (const [re, replacement] of SECRET_PATTERNS) out = out.replace(re, replacement);
|
|
386
403
|
return out;
|
|
387
404
|
}
|
|
405
|
+
|
|
406
|
+
// src/utils/sanitize.ts
|
|
407
|
+
var ANSI_PATTERN = /\x1B\[[0-9;?]*[A-Za-z]/g;
|
|
408
|
+
var C0_CONTROL_PATTERN = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
|
|
409
|
+
function stripAnsi(s) {
|
|
410
|
+
return s.replace(ANSI_PATTERN, "").replace(C0_CONTROL_PATTERN, "");
|
|
411
|
+
}
|
|
412
|
+
function truncate(s, max) {
|
|
413
|
+
if (s.length <= max) return s;
|
|
414
|
+
return s.slice(0, max - 1) + "\u2026";
|
|
415
|
+
}
|
|
416
|
+
function safeDisplay(s, max = 200) {
|
|
417
|
+
return truncate(stripAnsi(s), max);
|
|
418
|
+
}
|
|
419
|
+
function safeInline(s, max = 200) {
|
|
420
|
+
return safeDisplay(s.replace(/[\r\n]+/g, " "), max);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// src/utils/errors.ts
|
|
388
424
|
var CLI_READ_BACK = "From this CLI: `vonpay checkout payment-intents get <id>` (charge, capture, void), `vonpay checkout refunds list --payment-intent <id>` or `--transaction <id>` (refund), `sessions get <id>` (checkout session).";
|
|
389
425
|
var READ_BACK_FIRST = "Check the payment first: read it back (`vonpay checkout payment-intents get <id>` for a charge, capture or void; `vonpay checkout refunds list --payment-intent <id>` (or `--transaction <id>`) for a refund; `sessions get <id>` for a checkout session) and act only on what it shows.";
|
|
390
426
|
var UNKNOWN_OUTCOME_GUIDANCE = `${READ_BACK_FIRST} If you then retry, reuse the SAME --idempotency-key as the first attempt, never a new one: a new key is a new action and can move money twice.`;
|
|
@@ -401,7 +437,10 @@ var FIELD_PRINTERS = {
|
|
|
401
437
|
// What to do is the SDK's RETRY_WITHHELD_HELP, printed on the Hint line
|
|
402
438
|
// (the SDK puts it in llmHint; quoted here only when the error carries
|
|
403
439
|
// something else). This line adds the CLI commands for the read-back.
|
|
404
|
-
"Retry withheld: the outcome is unknown and the CLI did not retry it. " +
|
|
440
|
+
"Retry withheld: the outcome is unknown and the CLI did not retry it. " + // startsWith, not equality: the SDK extends the base hint for one code
|
|
441
|
+
// (SETTLE_ONLY_UNVERIFIABLE_WITHHELD_HELP), and the Hint line already
|
|
442
|
+
// carries it then, so it must not be printed twice.
|
|
443
|
+
(typeof e.llmHint === "string" && e.llmHint.startsWith(RETRY_WITHHELD_HELP.llmHint) ? "" : `${RETRY_WITHHELD_HELP.llmHint} `) + CLI_READ_BACK
|
|
405
444
|
) : null,
|
|
406
445
|
captureOutcome: (e) => e.captureOutcome ? `Capture outcome: ${str(e.captureOutcome)} \u2014 the provider accepted the capture but it could not be recorded, so money may have moved. Do NOT capture again; read the payment intent back first.` : null,
|
|
407
446
|
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,
|
|
@@ -416,6 +455,44 @@ var FIELD_PRINTERS = {
|
|
|
416
455
|
rawReason: (e) => e.rawReason && e.rawReason !== e.reason ? `Reason as sent: ${str(e.rawReason)}` : null,
|
|
417
456
|
retryAfterSeconds: (e) => typeof e.retryAfterSeconds === "number" ? `Retry after: ${e.retryAfterSeconds} seconds \u2014 then ask again` : null,
|
|
418
457
|
session: (e) => e.session ? `Session: ${JSON.stringify(e.session)}` : null,
|
|
458
|
+
// Restricted API keys (built ahead of the API; served in production since 2026-10-27).
|
|
459
|
+
// The SDK validates these; they are server-supplied all the same, so every
|
|
460
|
+
// value goes through safeInline (no escape sequence, control character or
|
|
461
|
+
// line break reaches the terminal) before it is printed.
|
|
462
|
+
limit: (e) => e.limit ? `Key limit: ${safeInline(String(e.limit))}` : null,
|
|
463
|
+
limitMinor: (e) => typeof e.limitMinor === "number" ? `Key limit amount: ${safeInline(String(e.limitMinor))} (minor units)` : null,
|
|
464
|
+
usedMinor: (e) => typeof e.usedMinor === "number" ? `Used in the last 24 hours: ${safeInline(String(e.usedMinor))} (minor units)` : null,
|
|
465
|
+
limitCurrency: (e) => e.limitCurrency ? `Key limit currency: ${safeInline(String(e.limitCurrency))}` : null,
|
|
466
|
+
refusedFields: (e) => Array.isArray(e.refusedFields) && e.refusedFields.length > 0 ? `Fields this key may not send: ${e.refusedFields.map((f) => safeInline(String(f), 128)).join(", ")}` : null,
|
|
467
|
+
// A refused payment-only settle (mirror_settle_only_*; built ahead of the
|
|
468
|
+
// API's production release): the charge asked for and the store order's
|
|
469
|
+
// facts. Validated by the SDK, printed through safeInline all the same.
|
|
470
|
+
chargeAmount: (e) => typeof e.chargeAmount === "number" ? `Charge asked for: ${safeInline(String(e.chargeAmount))} (minor units)` : null,
|
|
471
|
+
chargeCurrency: (e) => e.chargeCurrency ? `Charge currency: ${safeInline(String(e.chargeCurrency))}` : null,
|
|
472
|
+
orderBalance: (e) => typeof e.orderBalance === "number" ? `Store order balance: ${safeInline(String(e.orderBalance))} (minor units) \u2014 on an amount mismatch, charge this instead` : e.orderBalance === null ? "Store order balance: unknown \u2014 the store could not be read" : null,
|
|
473
|
+
orderBalanceRaw: (e) => e.orderBalanceRaw ? `Store's own balance figure: ${safeInline(String(e.orderBalanceRaw))}` : null,
|
|
474
|
+
orderCurrency: (e) => e.orderCurrency ? `Store order currency: ${safeInline(String(e.orderCurrency))}` : null,
|
|
475
|
+
orderStatus: (e) => e.orderStatus ? `Store order status: ${safeInline(String(e.orderStatus))}` : null,
|
|
476
|
+
orderPaymentStatus: (e) => e.orderPaymentStatus ? `Store order payment status: ${safeInline(String(e.orderPaymentStatus))}` : null,
|
|
477
|
+
// A refused subscription change (built ahead of the API's production release). Validated by
|
|
478
|
+
// the SDK, printed through safeInline all the same.
|
|
479
|
+
subscriptionStatus: (e) => e.subscriptionStatus ? `Subscription status: ${safeInline(String(e.subscriptionStatus))}` : null,
|
|
480
|
+
refusalReason: (e) => e.refusalReason ? `Refused because: ${safeInline(String(e.refusalReason))}` : null,
|
|
481
|
+
minDate: (e) => e.minDate ? `Earliest date allowed: ${safeInline(String(e.minDate))}` : null,
|
|
482
|
+
maxDate: (e) => e.maxDate ? `Latest date allowed: ${safeInline(String(e.maxDate))}` : null,
|
|
483
|
+
currentAmount: (e) => typeof e.currentAmount === "number" ? `Current highest renewal charge: ${safeInline(String(e.currentAmount))} (minor units)` : null,
|
|
484
|
+
maxAmount: (e) => typeof e.maxAmount === "number" ? `Most this key may make a renewal charge: ${safeInline(String(e.maxAmount))} (minor units)` : e.maxAmount === null ? "Most this key may make a renewal charge: none \u2014 this key may only lower the price" : null,
|
|
485
|
+
amountCurrency: (e) => e.amountCurrency ? `Renewal charge currency: ${safeInline(String(e.amountCurrency))}` : null,
|
|
486
|
+
pendingKind: (e) => e.pendingKind ? `Schedule change waiting for its customer notice: ${safeInline(String(e.pendingKind))}` : null,
|
|
487
|
+
// A charge the payment provider refused before any money moved (built ahead
|
|
488
|
+
// of the API's production release): the failed payment intent that records
|
|
489
|
+
// the attempt. The SDK has checked it is a vpi_* id; printed through
|
|
490
|
+
// safeInline all the same. The next step depends on the code: a card the
|
|
491
|
+
// processor is still storing (payment_method_unvaulted) is retried as is
|
|
492
|
+
// after a short wait; every other refusal needs the request or card fixed.
|
|
493
|
+
// Both under a NEW key: the refused record does not hold the key, and the
|
|
494
|
+
// processor may refuse a resend under the same one.
|
|
495
|
+
paymentIntentId: (e) => e.paymentIntentId ? `Refused attempt recorded as: ${safeInline(String(e.paymentIntentId))} (status failed, nothing was charged; ` + (e.code === "payment_method_unvaulted" ? "wait a few seconds, then send the same charge again with a NEW --idempotency-key)" : "fix the request or the card, then charge with a NEW --idempotency-key)") : null,
|
|
419
496
|
// On a withheld retry the SDK sets this to RETRY_WITHHELD_HELP.llmHint — the
|
|
420
497
|
// one wording for every surface — so it is printed as is.
|
|
421
498
|
llmHint: (e) => e.llmHint ? `Hint: ${str(e.llmHint)}` : null,
|
|
@@ -535,6 +612,22 @@ function optionalMinorUnitsOrExit(raw, flag) {
|
|
|
535
612
|
return raw === void 0 ? void 0 : minorUnitsOrExit(raw, flag);
|
|
536
613
|
}
|
|
537
614
|
|
|
615
|
+
// src/utils/mask-secrets.ts
|
|
616
|
+
function maskSecretsInJson(json) {
|
|
617
|
+
return scrubSecrets(json);
|
|
618
|
+
}
|
|
619
|
+
var SHOW_SECRETS_HELP = "With --json, print the session's client secret in full. \u26A0 The output then contains a secret: do not paste it or let it reach a log";
|
|
620
|
+
function withClientSecret(session) {
|
|
621
|
+
if (session === null || typeof session !== "object" || Array.isArray(session)) return session;
|
|
622
|
+
if (!("clientSecret" in session)) return session;
|
|
623
|
+
return { ...session, clientSecret: session.clientSecret };
|
|
624
|
+
}
|
|
625
|
+
function sessionJson(session, showSecrets) {
|
|
626
|
+
const json = JSON.stringify(withClientSecret(session), null, 2);
|
|
627
|
+
if (json === void 0) return String(json);
|
|
628
|
+
return showSecrets === true ? json : maskSecretsInJson(json);
|
|
629
|
+
}
|
|
630
|
+
|
|
538
631
|
// src/commands/sessions.ts
|
|
539
632
|
var makeClient = makeCheckoutClient;
|
|
540
633
|
function sessionLines(session) {
|
|
@@ -582,7 +675,7 @@ function sessionsCommand() {
|
|
|
582
675
|
const sessions = new Command4("sessions").description(
|
|
583
676
|
"Create and manage checkout sessions"
|
|
584
677
|
);
|
|
585
|
-
sessions.command("create").description("Create a new checkout session").requiredOption("--amount <cents>", "Amount in minor units, a whole number (e.g. 1499 = $14.99)").requiredOption("--currency <code>", "Currency code (e.g. USD)").option("--country <code>", "Country code (e.g. US)").option("--description <text>", "Session description").option("--success-url <url>", "Redirect URL on success").option("--cancel-url <url>", "Redirect URL on cancel").option("--idempotency-key <key>", "Idempotency key for safe retries").option("--dry-run", "Validate without creating a session").option("--json", "Output raw JSON").action(async (opts) => {
|
|
678
|
+
sessions.command("create").description("Create a new checkout session").requiredOption("--amount <cents>", "Amount in minor units, a whole number (e.g. 1499 = $14.99)").requiredOption("--currency <code>", "Currency code (e.g. USD)").option("--country <code>", "Country code (e.g. US)").option("--description <text>", "Session description").option("--success-url <url>", "Redirect URL on success").option("--cancel-url <url>", "Redirect URL on cancel").option("--idempotency-key <key>", "Idempotency key for safe retries").option("--dry-run", "Validate without creating a session").option("--json", "Output raw JSON (the session's client secret masked, see --show-secrets)").option("--show-secrets", SHOW_SECRETS_HELP).action(async (opts) => {
|
|
586
679
|
const amount = minorUnitsOrExit(String(opts.amount), "--amount");
|
|
587
680
|
const client = makeClient();
|
|
588
681
|
const spinner = startSpinner("Creating session...");
|
|
@@ -619,7 +712,7 @@ function sessionsCommand() {
|
|
|
619
712
|
);
|
|
620
713
|
spinner.stop();
|
|
621
714
|
if (opts.json) {
|
|
622
|
-
console.log(
|
|
715
|
+
console.log(sessionJson(session, opts.showSecrets));
|
|
623
716
|
} else {
|
|
624
717
|
console.log(chalk5.green("\u2713 Session created"));
|
|
625
718
|
console.log(` ID: ${session.id}`);
|
|
@@ -631,14 +724,14 @@ function sessionsCommand() {
|
|
|
631
724
|
handleError(err);
|
|
632
725
|
}
|
|
633
726
|
});
|
|
634
|
-
sessions.command("get").description("Get session status").argument("<id>", "Session ID").option("--json", "Output raw JSON").action(async (id, opts) => {
|
|
727
|
+
sessions.command("get").description("Get session status").argument("<id>", "Session ID").option("--json", "Output raw JSON (the session's client secret masked, see --show-secrets)").option("--show-secrets", SHOW_SECRETS_HELP).action(async (id, opts) => {
|
|
635
728
|
const client = makeClient();
|
|
636
729
|
const spinner = startSpinner("Fetching session...");
|
|
637
730
|
try {
|
|
638
731
|
const session = await client.sessions.get(id);
|
|
639
732
|
spinner.stop();
|
|
640
733
|
if (opts.json) {
|
|
641
|
-
console.log(
|
|
734
|
+
console.log(sessionJson(session, opts.showSecrets));
|
|
642
735
|
} else {
|
|
643
736
|
for (const line of sessionLines(session)) console.log(line);
|
|
644
737
|
}
|
|
@@ -715,20 +808,6 @@ function parseMetadata(pairs) {
|
|
|
715
808
|
return out;
|
|
716
809
|
}
|
|
717
810
|
|
|
718
|
-
// src/utils/sanitize.ts
|
|
719
|
-
var ANSI_PATTERN = /\x1B\[[0-9;?]*[A-Za-z]/g;
|
|
720
|
-
var C0_CONTROL_PATTERN = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
|
|
721
|
-
function stripAnsi(s) {
|
|
722
|
-
return s.replace(ANSI_PATTERN, "").replace(C0_CONTROL_PATTERN, "");
|
|
723
|
-
}
|
|
724
|
-
function truncate(s, max) {
|
|
725
|
-
if (s.length <= max) return s;
|
|
726
|
-
return s.slice(0, max - 1) + "\u2026";
|
|
727
|
-
}
|
|
728
|
-
function safeDisplay(s, max = 200) {
|
|
729
|
-
return truncate(stripAnsi(s), max);
|
|
730
|
-
}
|
|
731
|
-
|
|
732
811
|
// src/utils/live-key.ts
|
|
733
812
|
import { createInterface as createInterface2 } from "readline";
|
|
734
813
|
import chalk7 from "chalk";
|
|
@@ -762,10 +841,10 @@ var terminal = {
|
|
|
762
841
|
};
|
|
763
842
|
function keyFingerprint(key) {
|
|
764
843
|
const m = key.match(/^(vp_sk_live_)([A-Za-z0-9_-]{0,4})/);
|
|
765
|
-
return m ? `${m[1]}${m[2]}\u2026` : "
|
|
844
|
+
return m ? `${m[1]}${m[2]}\u2026` : "a key that is not a sandbox key";
|
|
766
845
|
}
|
|
767
846
|
async function confirmLiveAction(key, summary, opts = {}) {
|
|
768
|
-
if (!
|
|
847
|
+
if (!requiresLiveConfirmation(key)) return;
|
|
769
848
|
if (!terminal.isInteractive()) {
|
|
770
849
|
const message = `Refusing to ${summary.action} with a live key: no person is at an interactive terminal to confirm it. A live-key action must be confirmed by a person typing at a terminal (stdin and stdout both a terminal); scripts, CI jobs and AI agents cannot confirm it, and --confirm-live is no longer enough. Use a sandbox key (vp_sk_test_\u2026) for automation, or run the command yourself in a terminal. Nothing was sent.`;
|
|
771
850
|
exitWithCliError(message, chalk7.red(message));
|
|
@@ -796,7 +875,7 @@ function retiredConfirmLiveOption() {
|
|
|
796
875
|
).hideHelp();
|
|
797
876
|
}
|
|
798
877
|
function warnLiveSecretFlag(key, flag) {
|
|
799
|
-
if (!
|
|
878
|
+
if (!requiresLiveConfirmation(key)) return;
|
|
800
879
|
console.warn(
|
|
801
880
|
chalk7.yellow(
|
|
802
881
|
`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.`
|
|
@@ -872,8 +951,15 @@ function buildMITBlock(opts) {
|
|
|
872
951
|
originalTransactionId
|
|
873
952
|
};
|
|
874
953
|
}
|
|
954
|
+
function statusTone(status) {
|
|
955
|
+
if (status === "succeeded") return "green";
|
|
956
|
+
if (status === "authorized") return "cyan";
|
|
957
|
+
if (status === "requires_action") return "yellow";
|
|
958
|
+
if (status === "failed" || status === "cancelled") return "red";
|
|
959
|
+
return "gray";
|
|
960
|
+
}
|
|
875
961
|
function printPaymentIntent(intent) {
|
|
876
|
-
const statusColor =
|
|
962
|
+
const statusColor = chalk8[statusTone(intent.status)];
|
|
877
963
|
console.log(` ID: ${intent.id}`);
|
|
878
964
|
console.log(` Status: ${statusColor(intent.status)}`);
|
|
879
965
|
console.log(
|
|
@@ -893,6 +979,12 @@ function printPaymentIntent(intent) {
|
|
|
893
979
|
if (intent.declineCode) {
|
|
894
980
|
console.log(` Decline code: ${chalk8.red(safeDisplay(intent.declineCode))}`);
|
|
895
981
|
}
|
|
982
|
+
if (intent.networkAdvice) {
|
|
983
|
+
console.log(` Network advice: ${chalk8.red(safeInline(intent.networkAdvice))}`);
|
|
984
|
+
if (typeof intent.networkAdviceRetryAfterMs === "number") {
|
|
985
|
+
console.log(` Retry after: ${intent.networkAdviceRetryAfterMs} ms (earliest)`);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
896
988
|
console.log(` Created: ${intent.createdAt}`);
|
|
897
989
|
}
|
|
898
990
|
var AMOUNT_HELP = "Amount in minor units, a whole number (e.g. 1499 = $14.99)";
|
|
@@ -1607,6 +1699,15 @@ var SUPPORTED_EVENTS = [
|
|
|
1607
1699
|
"payment_intent.failed",
|
|
1608
1700
|
"payment_intent.cancelled",
|
|
1609
1701
|
"mirror.order.created",
|
|
1702
|
+
// Opt-in events (an endpoint gets them only when it lists each by name).
|
|
1703
|
+
// Payloads follow checkout's own "Send test event" samples, which are built
|
|
1704
|
+
// by the same code as the real events.
|
|
1705
|
+
"subscription.created",
|
|
1706
|
+
"subscription.renewed",
|
|
1707
|
+
"subscription.payment_failed",
|
|
1708
|
+
"subscription.cancelled",
|
|
1709
|
+
"subscription.updated",
|
|
1710
|
+
"merchant.card_testing_suspected",
|
|
1610
1711
|
"session.succeeded",
|
|
1611
1712
|
"session.failed"
|
|
1612
1713
|
];
|
|
@@ -1627,6 +1728,58 @@ function warnIfUnsubscribable(event, write = (m) => console.warn(m)) {
|
|
|
1627
1728
|
);
|
|
1628
1729
|
return true;
|
|
1629
1730
|
}
|
|
1731
|
+
var PROCESSOR_CONNECTION = null;
|
|
1732
|
+
function subscriptionEventData(event, paymentIntentId, amount, currency) {
|
|
1733
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1734
|
+
const inAMonth = now + 30 * 24 * 60 * 60;
|
|
1735
|
+
const base = {
|
|
1736
|
+
subscription_id: `vsub_test_${randomUUID2().replace(/-/g, "").slice(0, 16)}`,
|
|
1737
|
+
status: "active",
|
|
1738
|
+
billing_cycle: null,
|
|
1739
|
+
next_bill_at: inAMonth,
|
|
1740
|
+
amount: null,
|
|
1741
|
+
renewal_amount: amount,
|
|
1742
|
+
currency,
|
|
1743
|
+
payment_intent_id: null,
|
|
1744
|
+
action: null,
|
|
1745
|
+
decline_message: null,
|
|
1746
|
+
decline_code: null,
|
|
1747
|
+
cancel_reason: null,
|
|
1748
|
+
changed_fields: null,
|
|
1749
|
+
// Who made the change (served since checkout's production release of 2026-10-30): an API key
|
|
1750
|
+
// with no id or label on these samples. Set on every event.
|
|
1751
|
+
actor: { type: "api_key", api_key_id: null, label: null },
|
|
1752
|
+
// A cancel scheduled for the end of the paid period, and the discount on future
|
|
1753
|
+
// renewals (served since 2026-10-30): none on these samples.
|
|
1754
|
+
cancel_at_period_end: false,
|
|
1755
|
+
discount: null,
|
|
1756
|
+
// The merchant's own sign-up metadata; `{}` when none was sent.
|
|
1757
|
+
metadata: {}
|
|
1758
|
+
};
|
|
1759
|
+
switch (event) {
|
|
1760
|
+
case "subscription.created":
|
|
1761
|
+
return { ...base, billing_cycle: 1, amount, payment_intent_id: paymentIntentId };
|
|
1762
|
+
case "subscription.renewed":
|
|
1763
|
+
return { ...base, billing_cycle: 2, amount, payment_intent_id: paymentIntentId };
|
|
1764
|
+
case "subscription.payment_failed":
|
|
1765
|
+
return {
|
|
1766
|
+
...base,
|
|
1767
|
+
status: "past_due",
|
|
1768
|
+
billing_cycle: 2,
|
|
1769
|
+
next_bill_at: now + 3 * 24 * 60 * 60,
|
|
1770
|
+
amount,
|
|
1771
|
+
payment_intent_id: paymentIntentId,
|
|
1772
|
+
// The same three values `charge.failed` carries for this code.
|
|
1773
|
+
action: "soft",
|
|
1774
|
+
decline_message: "The card was declined for insufficient funds.",
|
|
1775
|
+
decline_code: "insufficient_funds"
|
|
1776
|
+
};
|
|
1777
|
+
case "subscription.cancelled":
|
|
1778
|
+
return { ...base, status: "cancelled", next_bill_at: null, cancel_reason: "too_expensive" };
|
|
1779
|
+
case "subscription.updated":
|
|
1780
|
+
return { ...base, renewal_amount: Math.max(1, Math.floor(amount * 0.9)), changed_fields: ["renewal_amount"] };
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1630
1783
|
function buildEventData(event, ids, amount, currency) {
|
|
1631
1784
|
const base = {
|
|
1632
1785
|
session_id: ids.sessionId,
|
|
@@ -1635,19 +1788,27 @@ function buildEventData(event, ids, amount, currency) {
|
|
|
1635
1788
|
amount,
|
|
1636
1789
|
currency
|
|
1637
1790
|
};
|
|
1791
|
+
const notARenewal = { subscription_id: null, billing_cycle: null };
|
|
1638
1792
|
switch (event) {
|
|
1639
1793
|
case "session.succeeded":
|
|
1640
|
-
case "payment_intent.succeeded":
|
|
1641
1794
|
return base;
|
|
1795
|
+
case "payment_intent.succeeded":
|
|
1796
|
+
return { ...base, processor_connection: PROCESSOR_CONNECTION, ...notARenewal };
|
|
1642
1797
|
case "charge.succeeded":
|
|
1643
|
-
return { ...base, vp_tx_id: ids.transactionId, card: null };
|
|
1798
|
+
return { ...base, vp_tx_id: ids.transactionId, card: null, processor_connection: PROCESSOR_CONNECTION, ...notARenewal };
|
|
1644
1799
|
case "session.failed":
|
|
1645
1800
|
case "payment_intent.failed":
|
|
1646
1801
|
return {
|
|
1647
1802
|
...base,
|
|
1648
1803
|
failure_reason: "Card declined",
|
|
1649
1804
|
failure_code: "card_declined",
|
|
1650
|
-
network_decline_code: "05"
|
|
1805
|
+
network_decline_code: "05",
|
|
1806
|
+
// The card network's advice: null when the network gave none that
|
|
1807
|
+
// could be read (as on this ordinary decline) — never permission to retry.
|
|
1808
|
+
network_advice: null,
|
|
1809
|
+
network_advice_retry_after_ms: null,
|
|
1810
|
+
// payment_intent.failed only: session.failed sends neither.
|
|
1811
|
+
...event === "payment_intent.failed" ? { processor_connection: PROCESSOR_CONNECTION, ...notARenewal } : {}
|
|
1651
1812
|
};
|
|
1652
1813
|
case "charge.failed":
|
|
1653
1814
|
return {
|
|
@@ -1659,7 +1820,12 @@ function buildEventData(event, ids, amount, currency) {
|
|
|
1659
1820
|
rule_code: null,
|
|
1660
1821
|
// Always null on a decline: no money moved, no settlement row.
|
|
1661
1822
|
vp_tx_id: null,
|
|
1662
|
-
network_decline_code: "05"
|
|
1823
|
+
network_decline_code: "05",
|
|
1824
|
+
// Same two fields and values as payment_intent.failed / session.failed.
|
|
1825
|
+
network_advice: null,
|
|
1826
|
+
network_advice_retry_after_ms: null,
|
|
1827
|
+
processor_connection: PROCESSOR_CONNECTION,
|
|
1828
|
+
...notARenewal
|
|
1663
1829
|
};
|
|
1664
1830
|
case "mirror.order.created":
|
|
1665
1831
|
return {
|
|
@@ -1668,7 +1834,28 @@ function buildEventData(event, ids, amount, currency) {
|
|
|
1668
1834
|
order_status_url: null
|
|
1669
1835
|
};
|
|
1670
1836
|
case "payment_intent.cancelled":
|
|
1671
|
-
return { ...base, cancellation_reason: "buyer_abandoned" };
|
|
1837
|
+
return { ...base, cancellation_reason: "buyer_abandoned", processor_connection: PROCESSOR_CONNECTION, ...notARenewal };
|
|
1838
|
+
case "subscription.created":
|
|
1839
|
+
case "subscription.renewed":
|
|
1840
|
+
case "subscription.payment_failed":
|
|
1841
|
+
case "subscription.cancelled":
|
|
1842
|
+
case "subscription.updated":
|
|
1843
|
+
return subscriptionEventData(event, ids.paymentIntentId, amount, currency);
|
|
1844
|
+
case "merchant.card_testing_suspected": {
|
|
1845
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1846
|
+
return {
|
|
1847
|
+
patterns: ["expiry_enumeration", "refusal_burst"],
|
|
1848
|
+
window_start: now - 60 * 60,
|
|
1849
|
+
window_end: now,
|
|
1850
|
+
attempts: 30,
|
|
1851
|
+
refused: 24,
|
|
1852
|
+
accepted: 3,
|
|
1853
|
+
distinct_cards: 12,
|
|
1854
|
+
card: { brand: "visa", last4: "4242" },
|
|
1855
|
+
expiry_variants: 9,
|
|
1856
|
+
blocked: false
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1672
1859
|
case "charge.refunded":
|
|
1673
1860
|
return {
|
|
1674
1861
|
...base,
|
|
@@ -1682,7 +1869,9 @@ function buildEventData(event, ids, amount, currency) {
|
|
|
1682
1869
|
reason: "customer_request",
|
|
1683
1870
|
is_partial: false,
|
|
1684
1871
|
original_charge_amount: amount,
|
|
1685
|
-
card: null
|
|
1872
|
+
card: null,
|
|
1873
|
+
processor_connection: PROCESSOR_CONNECTION,
|
|
1874
|
+
...notARenewal
|
|
1686
1875
|
};
|
|
1687
1876
|
case "refund.failed":
|
|
1688
1877
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vonpay/checkout-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Von Payments CLI — developer tools for checkout integration",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"node": ">=20"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@vonpay/checkout-node": "^4.
|
|
23
|
+
"@vonpay/checkout-node": "^4.4.0",
|
|
24
24
|
"commander": "^13.0.0",
|
|
25
25
|
"chalk": "^5.4.0",
|
|
26
26
|
"ora": "^8.2.0"
|