@agenzo/token-cli 0.3.3 → 0.4.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 CHANGED
@@ -560,6 +560,29 @@ var STATUS_ICONS = {
560
560
  warning: "\u26A0",
561
561
  loading: "\u280B"
562
562
  };
563
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
564
+ function createSpinner(message, intervalMs = 60) {
565
+ let frameIdx = 0;
566
+ let currentMessage = message;
567
+ const render2 = () => {
568
+ process.stdout.write(`\r\x1B[K${SPINNER_FRAMES[frameIdx]} ${currentMessage}`);
569
+ frameIdx = (frameIdx + 1) % SPINNER_FRAMES.length;
570
+ };
571
+ render2();
572
+ const timer = setInterval(render2, intervalMs);
573
+ return {
574
+ update(msg) {
575
+ currentMessage = msg;
576
+ },
577
+ stop(type, finalMessage) {
578
+ clearInterval(timer);
579
+ process.stdout.write("\r\x1B[K");
580
+ if (type && finalMessage) {
581
+ console.log(Formatter.status(type, finalMessage));
582
+ }
583
+ }
584
+ };
585
+ }
563
586
  var Formatter = class _Formatter {
564
587
  /** Get display width of a string (CJK characters count as 2) */
565
588
  static displayWidth(str) {
@@ -697,10 +720,6 @@ var PromptEngine = class {
697
720
  if (flagValue !== void 0) {
698
721
  return flagValue;
699
722
  }
700
- const envKey = process.env.AGENZO_API_KEY;
701
- if (envKey) {
702
- return envKey;
703
- }
704
723
  if (config.type === "password") {
705
724
  return password({ message: config.message, mask: "*" });
706
725
  }
@@ -747,6 +766,167 @@ async function collectPaymentMethodParams(type, flags) {
747
766
  return params;
748
767
  }
749
768
 
769
+ // src/verb-schema.ts
770
+ var CLI_NAME = "agenzo-token-cli";
771
+ function wantsJsonSchema(argv = process.argv) {
772
+ for (let i = 0; i < argv.length; i++) {
773
+ const a = argv[i];
774
+ if (a === "--format=json") return true;
775
+ if (a === "--format" && argv[i + 1] === "json") return true;
776
+ }
777
+ return false;
778
+ }
779
+ function emitSchema(schema) {
780
+ console.log(JSON.stringify(schema, null, 2));
781
+ }
782
+ function attachSchemaHelp(cmd, schema) {
783
+ const baseHelp = cmd.helpInformation.bind(cmd);
784
+ cmd.helpInformation = (context) => {
785
+ if (!wantsJsonSchema()) return baseHelp(context);
786
+ emitSchema(schema);
787
+ return "";
788
+ };
789
+ return cmd;
790
+ }
791
+ var pmAddSchema = {
792
+ cli: CLI_NAME,
793
+ noun: "payment-methods",
794
+ verb: "add",
795
+ description: "Add a payment method. --payment-brand evo (default): card binding + 3DS via Drop-in session. --payment-brand unionpay: UPI Agent Pay enrollment (requires --member); returns enroll_url, result arrives asynchronously via webhook.",
796
+ flags: {
797
+ "payment-brand": {
798
+ type: "string",
799
+ required: false,
800
+ default: "evo",
801
+ description: 'Payment brand: "evo" (default; 3DS/Drop-in binding) or "unionpay" (UPI Agent Pay enrollment)',
802
+ constraints: "evo | unionpay"
803
+ },
804
+ member: {
805
+ type: "string",
806
+ required: "conditional",
807
+ description: "End-user member id (required when --payment-brand unionpay; identifies which user this card belongs to). Ignored for evo payment brand.",
808
+ source: "from_previous_step",
809
+ from: "user.member_id or auth context"
810
+ },
811
+ mode: {
812
+ type: "string",
813
+ required: false,
814
+ default: "dropin",
815
+ description: 'Add mode (evo payment brand only): "manual" (CLI collects card details) or "dropin" (mint a Drop-in session)',
816
+ constraints: "manual | dropin"
817
+ },
818
+ email: {
819
+ type: "string",
820
+ required: false,
821
+ description: "Email for 3DS verification (evo/manual) or Drop-in session reference (evo/dropin)"
822
+ },
823
+ "no-poll": {
824
+ type: "bool",
825
+ required: false,
826
+ default: false,
827
+ description: "Dropin mode: mint session and exit immediately without polling verification status"
828
+ }
829
+ },
830
+ response: {
831
+ id: { type: "string", description: "Payment method id" },
832
+ status: {
833
+ type: "string",
834
+ description: "PENDING (awaiting enrollment/3DS) / ACTIVE / FAILED / DISABLED"
835
+ },
836
+ "payment-brand": { type: "string", description: "evo or unionpay" },
837
+ session_id: {
838
+ type: "string|absent",
839
+ description: "Drop-in session id (evo/dropin mode only)"
840
+ },
841
+ enroll_url: {
842
+ type: "string|absent",
843
+ description: "UnionPay enrollment URL (unionpay payment brand only) \u2014 user opens this to bind card"
844
+ },
845
+ correlation_id: {
846
+ type: "string|absent",
847
+ description: "src_correlation_id for tracking enrollment result (unionpay payment brand only)"
848
+ }
849
+ },
850
+ example: {
851
+ command: "agenzo-token-cli payment-methods add --payment-brand unionpay --member usr_abc123 --format json",
852
+ output_summary: "Returns enroll_url. User opens the URL in a browser to complete card binding. Result arrives async via webhook; poll with `payment-methods list` or `payment-methods get`."
853
+ },
854
+ error_recovery: {
855
+ PARAM_INVALID: "Fix the offending flag (--payment-brand must be evo|unionpay; --member required for unionpay).",
856
+ UPSTREAM_ERROR: "UPI enrollment initiation failed. Retry after a short delay (may be transient network).",
857
+ MEMBER_REQUIRED: "UnionPay payment brand requires --member <id>. Supply the end-user member identifier."
858
+ }
859
+ };
860
+ var pmListSchema = {
861
+ cli: CLI_NAME,
862
+ noun: "payment-methods",
863
+ verb: "list",
864
+ description: "List all payment methods for the authenticated developer",
865
+ flags: {},
866
+ response: {
867
+ payment_methods: {
868
+ type: "array",
869
+ description: "List of payment methods",
870
+ items: {
871
+ id: { type: "string", description: "Payment method id" },
872
+ status: { type: "string", description: "PENDING / ACTIVE / FAILED / DISABLED" },
873
+ payment_brand: { type: "string", description: "evo or unionpay" },
874
+ brand: { type: "string|null", description: "Card brand (Visa, Mastercard, UnionPay, etc.)" },
875
+ last4: { type: "string|null", description: "Last 4 digits of card" },
876
+ exp_month: { type: "int|null", description: "Expiry month" },
877
+ exp_year: { type: "int|null", description: "Expiry year" }
878
+ }
879
+ }
880
+ },
881
+ example: {
882
+ command: "agenzo-token-cli payment-methods list --format json",
883
+ output_summary: "Returns array of payment methods with status and card info."
884
+ }
885
+ };
886
+ var pmGetSchema = {
887
+ cli: CLI_NAME,
888
+ noun: "payment-methods",
889
+ verb: "get",
890
+ description: "Get a payment method by ID",
891
+ flags: {
892
+ id: { type: "string", required: true, description: "Payment method id", source: "from_previous_step", from: "payment_methods[].id" }
893
+ },
894
+ response: {
895
+ id: { type: "string", description: "Payment method id" },
896
+ status: { type: "string", description: "PENDING / ACTIVE / FAILED / DISABLED" },
897
+ payment_brand: { type: "string", description: "evo or unionpay" },
898
+ brand: { type: "string|null", description: "Card brand" },
899
+ last4: { type: "string|null", description: "Last 4 digits" },
900
+ exp_month: { type: "int|null", description: "Expiry month" },
901
+ exp_year: { type: "int|null", description: "Expiry year" }
902
+ },
903
+ example: {
904
+ command: "agenzo-token-cli payment-methods get --id pm_abc123 --format json",
905
+ output_summary: "Returns full payment method details including card info and status."
906
+ }
907
+ };
908
+ var pmDisableSchema = {
909
+ cli: CLI_NAME,
910
+ noun: "payment-methods",
911
+ verb: "disable",
912
+ description: "Disable a payment method (cascades revoke on active tokens)",
913
+ flags: {
914
+ id: { type: "string", required: true, description: "Payment method id to disable", source: "from_previous_step", from: "payment_methods[].id" }
915
+ },
916
+ response: {
917
+ id: { type: "string", description: "Payment method id" },
918
+ status: { type: "string", description: "DISABLED" }
919
+ },
920
+ example: {
921
+ command: "agenzo-token-cli payment-methods disable --id pm_abc123 --format json",
922
+ output_summary: "Disables the payment method and revokes any active tokens."
923
+ },
924
+ error_recovery: {
925
+ NOT_FOUND: "Payment method id does not exist. Check the id with `payment-methods list`.",
926
+ ALREADY_DISABLED: "Card is already disabled. No action needed."
927
+ }
928
+ };
929
+
750
930
  // src/payment-methods/add.ts
751
931
  var MANUAL_POLL_INTERVAL_MS = 3e3;
752
932
  var MANUAL_POLL_TIMEOUT_MS = 15 * 60 * 1e3;
@@ -755,6 +935,13 @@ var DROPIN_POLL_TIMEOUT_MS = 30 * 60 * 1e3;
755
935
  var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["ACTIVE", "FAILED", "EXPIRED"]);
756
936
  function registerAddCommand(parent, deps) {
757
937
  const cmd = parent.command("add").description("Add a payment method (manual 3DS or Drop-in session)").option("--api-key <key>", "API Key for authentication").option("--type <type>", "Payment method type (default: card)", "card").option(
938
+ "--payment-brand <brand>",
939
+ 'Payment brand: "evo" (default; existing 3DS/Drop-in binding) or "unionpay" (UPI Agent Pay enrollment)',
940
+ "evo"
941
+ ).option(
942
+ "--member <id>",
943
+ "End-user member id (required when --payment-brand unionpay; identifies which end-user this card belongs to)"
944
+ ).option(
758
945
  "--mode <mode>",
759
946
  'Add mode: "manual" (default; CLI collects card details and polls 3DS) or "dropin" (mint a Drop-in session and poll until the user finishes adding the payment method in the browser)',
760
947
  "manual"
@@ -764,11 +951,26 @@ function registerAddCommand(parent, deps) {
764
951
  ).option("--card-number <number>", "Card number (manual mode only)").option("--expiry <mmyy>", "Expiry date (MMYY format) (manual mode only)").option("--cvv <cvv>", "Card CVV (manual mode only)").option(
765
952
  "--idempotency-key <key>",
766
953
  "Idempotency key forwarded verbatim as the Idempotency-Key header (manual mode only)"
954
+ ).option(
955
+ "--no-poll",
956
+ "Dropin mode: mint the session, print it, and exit immediately without polling verification status (for server/SDK-driven flows where the front-end completes the binding)"
767
957
  );
958
+ attachSchemaHelp(cmd, pmAddSchema);
768
959
  cmd.action(async () => {
769
960
  const opts = cmd.optsWithGlobals();
770
961
  const format = resolveFormat(opts.format);
771
962
  const isYes = Boolean(opts.yes);
963
+ const paymentBrand = String(opts.paymentBrand ?? "evo").toLowerCase();
964
+ if (paymentBrand !== "evo" && paymentBrand !== "unionpay") {
965
+ throw new CliError(
966
+ "PARAM_INVALID",
967
+ `Unknown --payment-brand "${opts.paymentBrand}". Expected "evo" or "unionpay".`
968
+ );
969
+ }
970
+ if (paymentBrand === "unionpay") {
971
+ await handleUnionpayPaymentBrand(deps, opts, format);
972
+ return;
973
+ }
772
974
  const mode = String(opts.mode ?? "manual").toLowerCase();
773
975
  if (mode !== "manual" && mode !== "dropin") {
774
976
  throw new CliError(
@@ -892,6 +1094,96 @@ async function handleManualMode(deps, opts, format, isYes) {
892
1094
  }
893
1095
  }
894
1096
  }
1097
+ async function handleUnionpayPaymentBrand(deps, opts, format) {
1098
+ const isYes = Boolean(opts.yes);
1099
+ let member;
1100
+ if (opts.member) {
1101
+ member = String(opts.member);
1102
+ } else if (isYes) {
1103
+ throw new CliError(
1104
+ "PARAM_INVALID",
1105
+ "Missing required --member <id> for --payment-brand unionpay (required in --yes mode)"
1106
+ );
1107
+ } else {
1108
+ member = await PromptEngine.resolveInput(void 0, {
1109
+ message: "Member ID (end-user identity this card belongs to):",
1110
+ validate: (v) => v.trim().length > 0 || "Member ID is required for --payment-brand unionpay"
1111
+ });
1112
+ }
1113
+ const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
1114
+ message: "API Key:",
1115
+ type: "password"
1116
+ });
1117
+ const email = await PromptEngine.resolveInput(opts.email, {
1118
+ message: "Email:"
1119
+ });
1120
+ const result = await deps.apiClient.post(
1121
+ "/payment-methods/create",
1122
+ { type: "api-key", key: apiKey },
1123
+ { type: "card", payment_brand: "unionpay", member_id: member, email }
1124
+ );
1125
+ if (!result.success) {
1126
+ throw CliError.fromApi(result, { auth: "api-key" });
1127
+ }
1128
+ const pm = result.data;
1129
+ notify(format, "success", "Card binding initiated");
1130
+ const createdResult = {
1131
+ data: pm,
1132
+ text: () => Formatter.keyValue([
1133
+ ["ID", pm.id],
1134
+ ["Status", pm.status],
1135
+ ["Enroll URL", pm.enroll_url ?? "-"],
1136
+ ["Correlation ID", pm.correlation_id ?? "-"]
1137
+ ])
1138
+ };
1139
+ const configManager = new ConfigManager();
1140
+ await renderWithContext(createdResult, { format }, configManager);
1141
+ notify(
1142
+ format,
1143
+ "info",
1144
+ "Open the Enroll URL in a browser to complete card binding. Waiting for result..."
1145
+ );
1146
+ const UNIONPAY_POLL_INTERVAL_MS = 5e3;
1147
+ const UNIONPAY_POLL_TIMEOUT_MS = 6e4;
1148
+ const startTime = Date.now();
1149
+ const spinner = format !== "json" ? createSpinner("Waiting for card binding result...") : null;
1150
+ while (Date.now() - startTime < UNIONPAY_POLL_TIMEOUT_MS) {
1151
+ await sleep(UNIONPAY_POLL_INTERVAL_MS);
1152
+ const pollResult = await deps.apiClient.get(
1153
+ `/payment-methods/${pm.id}`,
1154
+ { type: "api-key", key: apiKey }
1155
+ );
1156
+ if (pollResult.success) {
1157
+ const status = pollResult.data.status;
1158
+ if (status === "ACTIVE") {
1159
+ spinner?.stop();
1160
+ notify(format, "success", "Payment method activated");
1161
+ const activatedPm = pollResult.data;
1162
+ const activatedResult = {
1163
+ data: activatedPm,
1164
+ text: () => {
1165
+ const lines = [
1166
+ ["ID", activatedPm.id],
1167
+ ["Type", activatedPm.type ?? "card"],
1168
+ ["Status", activatedPm.status]
1169
+ ];
1170
+ if (activatedPm.brand) lines.push(["Brand", activatedPm.brand]);
1171
+ if (activatedPm.first6) lines.push(["First 6", activatedPm.first6]);
1172
+ if (activatedPm.last4) lines.push(["Last 4", activatedPm.last4]);
1173
+ return Formatter.keyValue(lines);
1174
+ }
1175
+ };
1176
+ await renderWithContext(activatedResult, { format }, configManager);
1177
+ return;
1178
+ }
1179
+ if (status === "FAILED") {
1180
+ spinner?.stop("error", "Card binding failed.");
1181
+ return;
1182
+ }
1183
+ }
1184
+ }
1185
+ spinner?.stop("info", "Timed out waiting for card binding result. Check status later with: payment-methods get " + pm.id);
1186
+ }
895
1187
  async function handleDropinMode(deps, opts, format) {
896
1188
  const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
897
1189
  message: "API Key:",
@@ -922,6 +1214,9 @@ async function handleDropinMode(deps, opts, format) {
922
1214
  "info",
923
1215
  "Use the Session ID to add the payment method in the browser via the Drop-in SDK"
924
1216
  );
1217
+ if (opts.poll === false) {
1218
+ return;
1219
+ }
925
1220
  const finalPm = await pollVerificationStatus(deps.apiClient, apiKey, pmId, {
926
1221
  intervalMs: DROPIN_POLL_INTERVAL_MS,
927
1222
  timeoutMs: DROPIN_POLL_TIMEOUT_MS
@@ -1012,6 +1307,7 @@ function sleep(ms) {
1012
1307
  // src/payment-methods/list.ts
1013
1308
  function registerListCommand(parent, deps) {
1014
1309
  const cmd = parent.command("list").description("List payment methods").option("--api-key <key>", "API Key for authentication").option("--member <member_id>", "Filter by member ID");
1310
+ attachSchemaHelp(cmd, pmListSchema);
1015
1311
  cmd.action(async () => {
1016
1312
  const opts = cmd.optsWithGlobals();
1017
1313
  const format = resolveFormat(opts.format);
@@ -1058,6 +1354,7 @@ function registerListCommand(parent, deps) {
1058
1354
  // src/payment-methods/get.ts
1059
1355
  function registerGetCommand(parent, deps) {
1060
1356
  const cmd = parent.command("get <pm_id>").description("Get a payment method by ID").option("--api-key <key>", "API key for authentication");
1357
+ attachSchemaHelp(cmd, pmGetSchema);
1061
1358
  cmd.action(async (pmId) => {
1062
1359
  const opts = cmd.optsWithGlobals();
1063
1360
  const format = resolveFormat(opts.format);
@@ -1103,6 +1400,7 @@ function registerDisableCommand(parent, deps) {
1103
1400
  "--idempotency-key <key>",
1104
1401
  "Idempotency key forwarded verbatim as the Idempotency-Key header"
1105
1402
  );
1403
+ attachSchemaHelp(cmd, pmDisableSchema);
1106
1404
  cmd.action(async (pmId) => {
1107
1405
  const opts = cmd.optsWithGlobals();
1108
1406
  const format = resolveFormat(opts.format);
@@ -1190,10 +1488,17 @@ function registerDropinCreateCommand(parent, deps) {
1190
1488
 
1191
1489
  // src/payment-methods/dropin-status.ts
1192
1490
  function registerDropinStatusCommand(parent, deps) {
1193
- const cmd = parent.command("dropin-status <pm_id>").description("Query the current Drop-in binding status for a payment method (single check)").option("--api-key <key>", "API Key for authentication");
1194
- cmd.action(async (pmId) => {
1491
+ const cmd = parent.command("dropin-status [pm_id]").description("Query the current Drop-in binding status for a payment method (single check)").option("--api-key <key>", "API Key for authentication").option(
1492
+ "--payment-method-id <id>",
1493
+ "Payment method id to query (alternative to the positional <pm_id>, for programmatic callers that pass flags only)"
1494
+ );
1495
+ cmd.action(async (pmIdArg) => {
1195
1496
  const opts = cmd.optsWithGlobals();
1196
1497
  const format = resolveFormat(opts.format);
1498
+ const pmId = await PromptEngine.resolveInput(
1499
+ pmIdArg ?? opts.paymentMethodId,
1500
+ { message: "Payment method id:" }
1501
+ );
1197
1502
  const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
1198
1503
  message: "API Key:",
1199
1504
  type: "password"
@@ -1225,6 +1530,7 @@ function registerDropinStatusCommand(parent, deps) {
1225
1530
  import { confirm, select as select2 } from "@inquirer/prompts";
1226
1531
  var DEFAULT_NT_FEE_CENTS = 500;
1227
1532
  var USDC_UNIT = 1e6;
1533
+ var DECIMAL_AMOUNT_RE = /^\d+(\.\d+)?$/;
1228
1534
  function mapTokenType(cliType) {
1229
1535
  if (cliType === "network-token") return "network_token";
1230
1536
  return cliType;
@@ -1302,14 +1608,23 @@ function formatPaymentToken(data) {
1302
1608
  lines.push(["Currency", String(vcn.currency || "USD")]);
1303
1609
  lines.push(["Status", String(vcn.status || data.status || "")]);
1304
1610
  } else if (type === "network_token") {
1305
- const nt = data.network_token ?? data;
1306
- lines.push(["Payment Token ID", String(data.id || nt.id || "")]);
1307
- lines.push(["Type", "Network Token"]);
1308
- lines.push(["Brand", String(nt.payment_brand || nt.brand || "")]);
1309
- lines.push(["ECI", String(nt.eci || "")]);
1310
- lines.push(["Cryptogram", String(nt.token_cryptogram || nt.cryptogram || "")]);
1311
- lines.push(["Expiry", String(nt.expiry_date || nt.expiry || "")]);
1312
- lines.push(["Value", String(nt.value || "")]);
1611
+ if (data.status === "PENDING" && data.checkout_url) {
1612
+ lines.push(["Payment Token ID", String(data.id || "")]);
1613
+ lines.push(["Type", "Network Token"]);
1614
+ lines.push(["Status", "PENDING"]);
1615
+ lines.push(["Checkout URL", String(data.checkout_url || "")]);
1616
+ lines.push(["Correlation ID", String(data.correlation_id || "")]);
1617
+ } else {
1618
+ const nt = data.network_token ?? data;
1619
+ lines.push(["Payment Token ID", String(data.id || nt.id || "")]);
1620
+ lines.push(["Type", "Network Token"]);
1621
+ lines.push(["Brand", String(nt.payment_brand || nt.brand || "")]);
1622
+ const eci = nt.eci || "";
1623
+ if (eci) lines.push(["ECI", String(eci)]);
1624
+ lines.push(["Cryptogram", String(nt.token_cryptogram || nt.cryptogram || "")]);
1625
+ lines.push(["Expiry", String(nt.expiry_date || nt.expiry || "")]);
1626
+ lines.push(["Token Number", String(nt.value || "")]);
1627
+ }
1313
1628
  } else if (type === "x402") {
1314
1629
  const x402 = data.x402 ?? data;
1315
1630
  lines.push(["Payment Token ID", String(data.id || x402.id || "")]);
@@ -1329,8 +1644,13 @@ function formatCentsToUsd(cents) {
1329
1644
  const remainder = cents % 100;
1330
1645
  return `${dollars}.${String(remainder).padStart(2, "0")}`;
1331
1646
  }
1647
+ function isValidUnionpayAmount(amountStr) {
1648
+ const trimmed = amountStr.trim();
1649
+ if (!DECIMAL_AMOUNT_RE.test(trimmed)) return false;
1650
+ return parseFloat(trimmed) > 0;
1651
+ }
1332
1652
  function registerCreateCommand(parent, deps) {
1333
- const cmd = parent.command("create").description("Create a payment token (VCN / Network Token / X402)").option("--api-key <key>", "API Key for authentication").option("--type <type>", "Token type: vcn | network-token | x402").option("--payment-method-id <id>", "Payment method ID to use").option("--card <last4>", "Match payment method by last 4 digits").option("--member <member_id>", "Member ID").option("--amount <amount>", "Amount in USD (VCN / X402)").option("--currency <currency>", "Currency (default: USD)").option("--pay-to <address>", "Pay-to address (X402)").option("--nonce <nonce>", "Nonce (X402)").option("--network <network>", "Network (X402)").option("--deadline <deadline>", "Deadline (X402)").option("--external-tx-id <id>", "External transaction ID").option(
1653
+ const cmd = parent.command("create").description("Create a payment token (VCN / Network Token / X402)").option("--api-key <key>", "API Key for authentication").option("--type <type>", "Token type: vcn | network-token | x402").option("--payment-method-id <id>", "Payment method ID to use").option("--card <last4>", "Match payment method by last 4 digits").option("--member <member_id>", "Member ID").option("--amount <amount>", "Amount in USD (VCN / X402)").option("--currency <currency>", "Currency (default: USD)").option("--pay-to <address>", "Pay-to address (X402)").option("--nonce <nonce>", "Nonce (X402)").option("--network <network>", "Network (X402)").option("--deadline <deadline>", "Deadline (X402)").option("--external-tx-id <id>", "External transaction ID").option("--recipient-first-name <name>", "UnionPay network token: recipient first name (order delivery details)").option("--recipient-last-name <name>", "UnionPay network token: recipient last name (order delivery details)").option("--recipient-email <email>", "UnionPay network token: recipient email (recipient-email or recipient-phone required)").option("--recipient-phone <phone>", "UnionPay network token: recipient phone (recipient-email or recipient-phone required)").option("--unionpay-amount <amount>", 'UnionPay network token: intent amount as a decimal string, e.g. "174.58"').option(
1334
1654
  "--idempotency-key <key>",
1335
1655
  "Idempotency key forwarded verbatim as the Idempotency-Key header"
1336
1656
  );
@@ -1357,6 +1677,24 @@ function registerCreateCommand(parent, deps) {
1357
1677
  card: opts.card,
1358
1678
  yes: isYes
1359
1679
  });
1680
+ let selectedPmPaymentBrand;
1681
+ if (serverType === "network_token") {
1682
+ const pmResult = await deps.apiClient.get(
1683
+ `/payment-methods/${paymentMethodId}`,
1684
+ { type: "api-key", key: apiKey }
1685
+ );
1686
+ if (!pmResult.success) {
1687
+ throw CliError.fromApi(pmResult, { auth: "api-key" });
1688
+ }
1689
+ selectedPmPaymentBrand = pmResult.data.payment_brand;
1690
+ if (selectedPmPaymentBrand === "unionpay" && opts.card && !opts.paymentMethodId) {
1691
+ throw new CliError(
1692
+ "PARAM_INVALID",
1693
+ "UnionPay cards must be selected via --payment-method-id; --card (last4 matching) is not supported for unionpay payment brand."
1694
+ );
1695
+ }
1696
+ }
1697
+ const isUnionpayNetworkToken = serverType === "network_token" && selectedPmPaymentBrand === "unionpay";
1360
1698
  let member = opts.member;
1361
1699
  if (!member && !isYes) {
1362
1700
  const memberInput = await PromptEngine.resolveInput(void 0, {
@@ -1405,10 +1743,66 @@ function registerCreateCommand(parent, deps) {
1405
1743
  typeBody.currency = opts.currency;
1406
1744
  }
1407
1745
  } else if (serverType === "network_token") {
1408
- feeCents = await fetchNetworkTokenFee(deps.apiClient, apiKey);
1409
- freezeAmountCents = feeCents;
1410
- feeDisplay = `$${formatCentsToUsd(feeCents)}`;
1411
- freezeDisplay = feeDisplay;
1746
+ if (isUnionpayNetworkToken) {
1747
+ const unionpayAmountStr = await PromptEngine.resolveInput(
1748
+ opts.unionpayAmount,
1749
+ {
1750
+ message: "UnionPay intent amount (USD, e.g. 174.58):",
1751
+ validate: (v) => isValidUnionpayAmount(v) || 'Amount must be a positive decimal, e.g. "174.58"'
1752
+ }
1753
+ );
1754
+ if (!isValidUnionpayAmount(unionpayAmountStr)) {
1755
+ throw new CliError(
1756
+ "PARAM_INVALID",
1757
+ `Invalid --unionpay-amount "${unionpayAmountStr}". Expected a positive decimal string, e.g. "174.58".`
1758
+ );
1759
+ }
1760
+ typeBody.unionpay_amount = unionpayAmountStr.trim();
1761
+ typeBody.recipient_first_name = await PromptEngine.resolveInput(
1762
+ opts.recipientFirstName,
1763
+ {
1764
+ message: "Recipient first name:",
1765
+ validate: (v) => v.trim().length > 0 || "Recipient first name is required"
1766
+ }
1767
+ );
1768
+ typeBody.recipient_last_name = await PromptEngine.resolveInput(
1769
+ opts.recipientLastName,
1770
+ {
1771
+ message: "Recipient last name:",
1772
+ validate: (v) => v.trim().length > 0 || "Recipient last name is required"
1773
+ }
1774
+ );
1775
+ let recipientEmail = opts.recipientEmail;
1776
+ let recipientPhone = opts.recipientPhone;
1777
+ if (!recipientEmail && !recipientPhone) {
1778
+ if (isYes) {
1779
+ throw new CliError(
1780
+ "PARAM_INVALID",
1781
+ "--recipient-email or --recipient-phone is required for unionpay network tokens."
1782
+ );
1783
+ }
1784
+ const emailInput = await PromptEngine.resolveInput(void 0, {
1785
+ message: "Recipient email (optional, press Enter to skip and enter phone instead):",
1786
+ validate: () => true
1787
+ });
1788
+ if (emailInput.trim()) {
1789
+ recipientEmail = emailInput.trim();
1790
+ } else {
1791
+ const phoneInput = await PromptEngine.resolveInput(void 0, {
1792
+ message: "Recipient phone (required, since no email was given):",
1793
+ validate: (v) => v.trim().length > 0 || "Recipient email or phone is required"
1794
+ });
1795
+ recipientPhone = phoneInput.trim();
1796
+ }
1797
+ }
1798
+ if (recipientEmail) typeBody.recipient_email = recipientEmail;
1799
+ if (recipientPhone) typeBody.recipient_phone = recipientPhone;
1800
+ } else {
1801
+ feeCents = await fetchNetworkTokenFee(deps.apiClient, apiKey);
1802
+ freezeAmountCents = feeCents;
1803
+ feeDisplay = `$${formatCentsToUsd(feeCents)}`;
1804
+ freezeDisplay = feeDisplay;
1805
+ }
1412
1806
  } else if (serverType === "x402") {
1413
1807
  const amountStr = await PromptEngine.resolveInput(opts.amount, {
1414
1808
  message: "Amount (USDC):",
@@ -1450,10 +1844,15 @@ function registerCreateCommand(parent, deps) {
1450
1844
  typeBody.deadline = deadlineNum;
1451
1845
  }
1452
1846
  if (!isYes) {
1453
- const warningLines = [`Freeze: ${freezeDisplay}`, `Fee: ${feeDisplay}`];
1454
- notify(format, "warning", warningLines.join(" | "));
1847
+ if (freezeDisplay !== void 0 && feeDisplay !== void 0) {
1848
+ const warningLines = [`Freeze: ${freezeDisplay}`, `Fee: ${feeDisplay}`];
1849
+ notify(format, "warning", warningLines.join(" | "));
1850
+ } else if (isUnionpayNetworkToken) {
1851
+ notify(format, "info", "UnionPay: no fee (clearing network not yet enabled)");
1852
+ }
1853
+ const confirmMessage = isUnionpayNetworkToken ? "Proceed with UnionPay network token request?" : "Proceed with token creation?";
1455
1854
  const confirmed = await confirm({
1456
- message: "Proceed with token creation?",
1855
+ message: confirmMessage,
1457
1856
  default: true
1458
1857
  });
1459
1858
  if (!confirmed) {
@@ -1510,6 +1909,42 @@ function registerCreateCommand(parent, deps) {
1510
1909
  }
1511
1910
  };
1512
1911
  await renderWithContext(commandResult, { format }, configManager);
1912
+ if (isUnionpayNetworkToken) {
1913
+ notify(
1914
+ format,
1915
+ "info",
1916
+ "Open the Checkout URL to complete the UnionPay payment verification. Waiting for result..."
1917
+ );
1918
+ const UNIONPAY_TOKEN_POLL_INTERVAL_MS = 5e3;
1919
+ const UNIONPAY_TOKEN_POLL_TIMEOUT_MS = 6e4;
1920
+ const pollStart = Date.now();
1921
+ const tokenId = tokenData.id;
1922
+ while (Date.now() - pollStart < UNIONPAY_TOKEN_POLL_TIMEOUT_MS) {
1923
+ await new Promise((resolve) => setTimeout(resolve, UNIONPAY_TOKEN_POLL_INTERVAL_MS));
1924
+ const pollResult = await deps.apiClient.get(
1925
+ `/payment-tokens/${tokenId}`,
1926
+ { type: "api-key", key: apiKey }
1927
+ );
1928
+ if (pollResult.success) {
1929
+ const status = pollResult.data.status;
1930
+ if (status === "ACTIVE") {
1931
+ notify(format, "success", "UnionPay network token activated!");
1932
+ if (!pollResult.data.type) pollResult.data.type = serverType;
1933
+ const activatedResult = {
1934
+ data: pollResult.data,
1935
+ text: () => formatPaymentToken(pollResult.data)
1936
+ };
1937
+ await renderWithContext(activatedResult, { format }, configManager);
1938
+ return;
1939
+ }
1940
+ if (status === "FAILED") {
1941
+ notify(format, "error", "UnionPay network token failed.");
1942
+ return;
1943
+ }
1944
+ }
1945
+ }
1946
+ notify(format, "info", `Timed out waiting for token activation. Check status later with: payment-tokens get ${tokenId}`);
1947
+ }
1513
1948
  });
1514
1949
  }
1515
1950
  async function checkVcnFeatureEnabled(apiClient, apiKey) {