@vonpay/checkout-cli 0.15.1 → 0.16.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.
Files changed (2) hide show
  1. package/dist/index.js +115 -29
  2. package/package.json +2 -2
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. " + (e.llmHint === RETRY_WITHHELD_HELP.llmHint ? "" : `${RETRY_WITHHELD_HELP.llmHint} `) + CLI_READ_BACK
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,25 @@ 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,
419
477
  // On a withheld retry the SDK sets this to RETRY_WITHHELD_HELP.llmHint — the
420
478
  // one wording for every surface — so it is printed as is.
421
479
  llmHint: (e) => e.llmHint ? `Hint: ${str(e.llmHint)}` : null,
@@ -535,6 +593,22 @@ function optionalMinorUnitsOrExit(raw, flag) {
535
593
  return raw === void 0 ? void 0 : minorUnitsOrExit(raw, flag);
536
594
  }
537
595
 
596
+ // src/utils/mask-secrets.ts
597
+ function maskSecretsInJson(json) {
598
+ return scrubSecrets(json);
599
+ }
600
+ 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";
601
+ function withClientSecret(session) {
602
+ if (session === null || typeof session !== "object" || Array.isArray(session)) return session;
603
+ if (!("clientSecret" in session)) return session;
604
+ return { ...session, clientSecret: session.clientSecret };
605
+ }
606
+ function sessionJson(session, showSecrets) {
607
+ const json = JSON.stringify(withClientSecret(session), null, 2);
608
+ if (json === void 0) return String(json);
609
+ return showSecrets === true ? json : maskSecretsInJson(json);
610
+ }
611
+
538
612
  // src/commands/sessions.ts
539
613
  var makeClient = makeCheckoutClient;
540
614
  function sessionLines(session) {
@@ -582,7 +656,7 @@ function sessionsCommand() {
582
656
  const sessions = new Command4("sessions").description(
583
657
  "Create and manage checkout sessions"
584
658
  );
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) => {
659
+ sessions.command("create").description("Create a new checkout session").requiredOption("--amount <cents>", "Amount in minor units, a whole number (e.g. 1499 = $14.99)").requiredOption("--currency <code>", "Currency code (e.g. USD)").option("--country <code>", "Country code (e.g. US)").option("--description <text>", "Session description").option("--success-url <url>", "Redirect URL on success").option("--cancel-url <url>", "Redirect URL on cancel").option("--idempotency-key <key>", "Idempotency key for safe retries").option("--dry-run", "Validate without creating a session").option("--json", "Output raw JSON (the session's client secret masked, see --show-secrets)").option("--show-secrets", SHOW_SECRETS_HELP).action(async (opts) => {
586
660
  const amount = minorUnitsOrExit(String(opts.amount), "--amount");
587
661
  const client = makeClient();
588
662
  const spinner = startSpinner("Creating session...");
@@ -619,7 +693,7 @@ function sessionsCommand() {
619
693
  );
620
694
  spinner.stop();
621
695
  if (opts.json) {
622
- console.log(JSON.stringify(session, null, 2));
696
+ console.log(sessionJson(session, opts.showSecrets));
623
697
  } else {
624
698
  console.log(chalk5.green("\u2713 Session created"));
625
699
  console.log(` ID: ${session.id}`);
@@ -631,14 +705,14 @@ function sessionsCommand() {
631
705
  handleError(err);
632
706
  }
633
707
  });
634
- sessions.command("get").description("Get session status").argument("<id>", "Session ID").option("--json", "Output raw JSON").action(async (id, opts) => {
708
+ 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
709
  const client = makeClient();
636
710
  const spinner = startSpinner("Fetching session...");
637
711
  try {
638
712
  const session = await client.sessions.get(id);
639
713
  spinner.stop();
640
714
  if (opts.json) {
641
- console.log(JSON.stringify(session, null, 2));
715
+ console.log(sessionJson(session, opts.showSecrets));
642
716
  } else {
643
717
  for (const line of sessionLines(session)) console.log(line);
644
718
  }
@@ -715,20 +789,6 @@ function parseMetadata(pairs) {
715
789
  return out;
716
790
  }
717
791
 
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
792
  // src/utils/live-key.ts
733
793
  import { createInterface as createInterface2 } from "readline";
734
794
  import chalk7 from "chalk";
@@ -762,10 +822,10 @@ var terminal = {
762
822
  };
763
823
  function keyFingerprint(key) {
764
824
  const m = key.match(/^(vp_sk_live_)([A-Za-z0-9_-]{0,4})/);
765
- return m ? `${m[1]}${m[2]}\u2026` : "vp_sk_live_\u2026";
825
+ return m ? `${m[1]}${m[2]}\u2026` : "a key that is not a sandbox key";
766
826
  }
767
827
  async function confirmLiveAction(key, summary, opts = {}) {
768
- if (!isLiveKey2(key)) return;
828
+ if (!requiresLiveConfirmation(key)) return;
769
829
  if (!terminal.isInteractive()) {
770
830
  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
831
  exitWithCliError(message, chalk7.red(message));
@@ -796,7 +856,7 @@ function retiredConfirmLiveOption() {
796
856
  ).hideHelp();
797
857
  }
798
858
  function warnLiveSecretFlag(key, flag) {
799
- if (!isLiveKey2(key)) return;
859
+ if (!requiresLiveConfirmation(key)) return;
800
860
  console.warn(
801
861
  chalk7.yellow(
802
862
  `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 +932,15 @@ function buildMITBlock(opts) {
872
932
  originalTransactionId
873
933
  };
874
934
  }
935
+ function statusTone(status) {
936
+ if (status === "succeeded") return "green";
937
+ if (status === "authorized") return "cyan";
938
+ if (status === "requires_action") return "yellow";
939
+ if (status === "failed" || status === "cancelled") return "red";
940
+ return "gray";
941
+ }
875
942
  function printPaymentIntent(intent) {
876
- const statusColor = intent.status === "succeeded" ? chalk8.green : intent.status === "authorized" ? chalk8.cyan : intent.status === "requires_action" ? chalk8.yellow : intent.status === "failed" || intent.status === "canceled" ? chalk8.red : chalk8.gray;
943
+ const statusColor = chalk8[statusTone(intent.status)];
877
944
  console.log(` ID: ${intent.id}`);
878
945
  console.log(` Status: ${statusColor(intent.status)}`);
879
946
  console.log(
@@ -893,6 +960,12 @@ function printPaymentIntent(intent) {
893
960
  if (intent.declineCode) {
894
961
  console.log(` Decline code: ${chalk8.red(safeDisplay(intent.declineCode))}`);
895
962
  }
963
+ if (intent.networkAdvice) {
964
+ console.log(` Network advice: ${chalk8.red(safeInline(intent.networkAdvice))}`);
965
+ if (typeof intent.networkAdviceRetryAfterMs === "number") {
966
+ console.log(` Retry after: ${intent.networkAdviceRetryAfterMs} ms (earliest)`);
967
+ }
968
+ }
896
969
  console.log(` Created: ${intent.createdAt}`);
897
970
  }
898
971
  var AMOUNT_HELP = "Amount in minor units, a whole number (e.g. 1499 = $14.99)";
@@ -1627,6 +1700,7 @@ function warnIfUnsubscribable(event, write = (m) => console.warn(m)) {
1627
1700
  );
1628
1701
  return true;
1629
1702
  }
1703
+ var PROCESSOR_CONNECTION = null;
1630
1704
  function buildEventData(event, ids, amount, currency) {
1631
1705
  const base = {
1632
1706
  session_id: ids.sessionId,
@@ -1637,17 +1711,24 @@ function buildEventData(event, ids, amount, currency) {
1637
1711
  };
1638
1712
  switch (event) {
1639
1713
  case "session.succeeded":
1640
- case "payment_intent.succeeded":
1641
1714
  return base;
1715
+ case "payment_intent.succeeded":
1716
+ return { ...base, processor_connection: PROCESSOR_CONNECTION };
1642
1717
  case "charge.succeeded":
1643
- return { ...base, vp_tx_id: ids.transactionId, card: null };
1718
+ return { ...base, vp_tx_id: ids.transactionId, card: null, processor_connection: PROCESSOR_CONNECTION };
1644
1719
  case "session.failed":
1645
1720
  case "payment_intent.failed":
1646
1721
  return {
1647
1722
  ...base,
1648
1723
  failure_reason: "Card declined",
1649
1724
  failure_code: "card_declined",
1650
- network_decline_code: "05"
1725
+ network_decline_code: "05",
1726
+ // The card network's advice: null when the network gave none that
1727
+ // could be read (as on this ordinary decline) — never permission to retry.
1728
+ network_advice: null,
1729
+ network_advice_retry_after_ms: null,
1730
+ // payment_intent.failed only: session.failed does not send it.
1731
+ ...event === "payment_intent.failed" ? { processor_connection: PROCESSOR_CONNECTION } : {}
1651
1732
  };
1652
1733
  case "charge.failed":
1653
1734
  return {
@@ -1659,7 +1740,11 @@ function buildEventData(event, ids, amount, currency) {
1659
1740
  rule_code: null,
1660
1741
  // Always null on a decline: no money moved, no settlement row.
1661
1742
  vp_tx_id: null,
1662
- network_decline_code: "05"
1743
+ network_decline_code: "05",
1744
+ // Same two fields and values as payment_intent.failed / session.failed.
1745
+ network_advice: null,
1746
+ network_advice_retry_after_ms: null,
1747
+ processor_connection: PROCESSOR_CONNECTION
1663
1748
  };
1664
1749
  case "mirror.order.created":
1665
1750
  return {
@@ -1668,7 +1753,7 @@ function buildEventData(event, ids, amount, currency) {
1668
1753
  order_status_url: null
1669
1754
  };
1670
1755
  case "payment_intent.cancelled":
1671
- return { ...base, cancellation_reason: "buyer_abandoned" };
1756
+ return { ...base, cancellation_reason: "buyer_abandoned", processor_connection: PROCESSOR_CONNECTION };
1672
1757
  case "charge.refunded":
1673
1758
  return {
1674
1759
  ...base,
@@ -1682,7 +1767,8 @@ function buildEventData(event, ids, amount, currency) {
1682
1767
  reason: "customer_request",
1683
1768
  is_partial: false,
1684
1769
  original_charge_amount: amount,
1685
- card: null
1770
+ card: null,
1771
+ processor_connection: PROCESSOR_CONNECTION
1686
1772
  };
1687
1773
  case "refund.failed":
1688
1774
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vonpay/checkout-cli",
3
- "version": "0.15.1",
3
+ "version": "0.16.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.0.0",
23
+ "@vonpay/checkout-node": "^4.3.0",
24
24
  "commander": "^13.0.0",
25
25
  "chalk": "^5.4.0",
26
26
  "ora": "^8.2.0"