@itpay/cli 0.2.3 → 0.2.5

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 CHANGED
@@ -213,7 +213,7 @@ redacted secure delivery status:
213
213
  ```bash
214
214
  itp buyer catalog search --query 企业工商 --json
215
215
  itp buyer cart create --variant var_itpay_enterprise_fuzzy_search_cny01 --input company_name=阿里 --json
216
- itp buyer checkout create --cart <cart_id> --email buyer@example.com --json
216
+ itp buyer checkout create --cart <cart_id> --email <buyer_email> --json
217
217
  itp buyer payment wait <payment_intent_id> --json
218
218
  itp buyer checkout status <checkout_id> --json
219
219
  ```
@@ -221,7 +221,7 @@ itp buyer checkout status <checkout_id> --json
221
221
  For the one-command buyer helper:
222
222
 
223
223
  ```bash
224
- itp buy var_itpay_enterprise_fuzzy_search_cny01 --email buyer@example.com --input company_name=阿里 --display agent --no-wait-payment --json
224
+ itp buy var_itpay_enterprise_fuzzy_search_cny01 --email <buyer_email> --input company_name=阿里 --display agent --no-wait-payment --json
225
225
  ```
226
226
 
227
227
  For multi-item cart tests:
@@ -239,7 +239,8 @@ Payment QR rules:
239
239
  - Otherwise render the ItPay-hosted `qr_png_url` / `preferred_qr_url`.
240
240
  - Use `mobile_wallet_url` only as a human mobile fallback.
241
241
  - Do not generate your own QR from payment URLs.
242
- - In agent app clients, send `payment_handoff.markdown` to the human first, then run `payment_handoff.wait_command`.
242
+ - In agent app clients, send `human_visible_markdown` to the human first, then run `after_human_visible_markdown.command`.
243
+ - If status is `payment_handoff_required`, `next` is the human reply step, not payment wait.
243
244
  - Treat only `payment_intent.verified` as payment success.
244
245
 
245
246
  If the human wants the agent to analyze delivered content, the human must reveal
package/bin/itp CHANGED
@@ -93,8 +93,8 @@ async function main() {
93
93
  output({
94
94
  version: VERSION,
95
95
  commands: [
96
- "buy var_pubg_couple_skin_cny20 --sandbox --email buyer@example.com --phone +8613800000000 --display agent --no-wait-payment --json",
97
- "buy var_pubg_couple_skin_cny20 --sandbox --email buyer@example.com --phone +8613800000000 --no-wait --json",
96
+ "buy var_pubg_couple_skin_cny20 --email <buyer_email> --phone +8613800000000 --display agent --no-wait-payment --json",
97
+ "buy var_pubg_couple_skin_cny20 --email <buyer_email> --phone +8613800000000 --no-wait --json",
98
98
  "buyer catalog search --query 企业工商 --category business_data_api --provider itpay_enterprise_data --json",
99
99
  "buyer catalog get --variant var_pubg_couple_skin_cny20 --json",
100
100
  "buyer cart create --variant var_pubg_couple_skin_cny20 --json",
@@ -105,7 +105,7 @@ async function main() {
105
105
  "buyer shelf manifest --json",
106
106
  "buyer shelf snapshot --version <catalog_version> --json",
107
107
  "buyer shelf delta --since <catalog_version> --json",
108
- "buyer checkout create --cart <cart_id> --method alipay --email buyer@example.com --phone +8613800000000 --json",
108
+ "buyer checkout create --cart <cart_id> --method alipay --email <buyer_email> --phone +8613800000000 --json",
109
109
  "buyer checkout status <checkout_id> --json",
110
110
  "buyer checkout resume <checkout_id> --json",
111
111
  "buyer auth status --json",
@@ -68,8 +68,8 @@
68
68
  },
69
69
  {
70
70
  "intent": "do both steps through the high-level command",
71
- "command": "itp buy <variant_id> --sandbox --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --json",
72
- "success_signal": "response.status is waiting_human_auth, waiting_user_payment, payment_verified, or delivery_claimable"
71
+ "command": "itp buy <variant_id> --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --json",
72
+ "success_signal": "response.status is waiting_human_auth, payment_handoff_required, payment_verified, or delivery_claimable"
73
73
  }
74
74
  ],
75
75
  "agent_rules": [
@@ -8,7 +8,7 @@
8
8
  "Checkout returns human_action.kind=auth_qr for the first-purchase auth-to-payment entry.",
9
9
  "PaymentIntent has been created.",
10
10
  "The CLI returned human_action.kind=payment_qr, qr_png_url, preferred_qr_url, or qr_image_url.",
11
- "The human needs to scan with Alipay sandbox."
11
+ "The human needs to scan with the payment provider."
12
12
  ],
13
13
  "required_state": {
14
14
  "needs": ["payment_intent_id", "human_action", "qr_png_url/preferred_qr_url or local_qr_path"],
@@ -17,8 +17,8 @@
17
17
  "commands": [
18
18
  {
19
19
  "intent": "create payment intent through the high-level buy flow",
20
- "command": "itp buy <variant_id> --sandbox --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --json",
21
- "success_signal": "response.payment_handoff.markdown and response.payment_handoff.wait_command are present"
20
+ "command": "itp buy <variant_id> --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --json",
21
+ "success_signal": "response.status is payment_handoff_required; response.human_visible_markdown is present; response.after_human_visible_markdown.command is the wait command to run after showing the QR/link"
22
22
  },
23
23
  {
24
24
  "intent": "recover a scanner order-not-found display problem",
@@ -33,17 +33,18 @@
33
33
  "Payment QR display requires both a valid/saved buyer session and payment_intent_id. If payment_intent_id appears before buyer_session is saved, run buyer checkout resume/status instead of showing payment QR.",
34
34
  "Do not stop after showing auth_qr unless the human explicitly asks you to pause. Keep waiting/resuming the same checkout so the post-auth payment handoff can continue.",
35
35
  "Do not describe auth_qr as payment success or payment proof. Only payment_intent.verified proves payment.",
36
- "In Codex or Claude Code app clients, the default UX is show-then-wait: show response.payment_handoff.markdown first, then run response.payment_handoff.wait_command.",
37
- "For Telegram-style message clients, show response.payment_handoff.markdown and stop unless the human asks you to keep waiting or returns with a payment question.",
36
+ "In Codex or Claude Code app clients, the default UX is show-then-wait: send response.human_visible_markdown to the human first, then run response.after_human_visible_markdown.command.",
37
+ "For Telegram-style message clients, show response.human_visible_markdown and stop unless the human asks you to keep waiting or returns with a payment question.",
38
+ "When response.status is payment_handoff_required, response.next is the human reply step, not payment wait.",
38
39
  "MUST show local_qr_path first when present. Many agent clients do not reliably render remote QR images; local_qr_path is the most reliable desktop/chat display artifact.",
39
40
  "If local_qr_path is not present, show qr_png_url or preferred_qr_url as the primary scannable QR. This is an ItPay-hosted human QR image; it may render the native provider payment code for scanner reliability, but the agent must not request or decode the raw provider payload.",
40
41
  "Use qr_image_url/SVG only as a fallback when PNG/local rendering is unavailable.",
41
42
  "For mobile users, present mobile_wallet_url as a clickable payment button when present.",
42
43
  "payment_entry_url is the stable ItPay status/payment page. qr_png_url/local_qr_path is usually the better desktop scanner artifact.",
43
- "In Alipay sandbox, the provider can briefly show order not found even after precreate succeeded. Tell the human to wait 30-60 seconds and retry the same QR/page. Do not create a new checkout, do not create a new payment intent, and do not repeatedly refresh unless the ItPay page or CLI explicitly asks for recovery.",
44
+ "The provider can briefly show order not found even after precreate succeeded. Tell the human to wait 30-60 seconds and retry the same QR/page. Do not create a new checkout, do not create a new payment intent, and do not repeatedly refresh unless the ItPay page or CLI explicitly asks for recovery.",
44
45
  "Opening the payment page must not be treated as paid.",
45
- "Do not run payment wait before the QR/link has been sent to the human.",
46
- "After showing QR, run payment_handoff.wait_command or buyer payment wait for the same payment_intent_id."
46
+ "Do not run payment wait before the QR/link has been sent to the human. If you accidentally do, CLI returns payment_handoff_required instead of blocking.",
47
+ "After showing QR, run after_human_visible_markdown.command or buyer payment wait <payment_intent_id> --qr-shown for the same payment_intent_id."
47
48
  ],
48
49
  "forbidden": [
49
50
  "Do not encode payment_entry_url into a new QR yourself when qr_png_url/local_qr_path is available.",
@@ -61,7 +62,7 @@
61
62
  "command": "itp docs show payment-wait --role buyer --json"
62
63
  },
63
64
  {
64
- "condition": "Alipay sandbox says order not found after waiting on the same QR/page",
65
+ "condition": "the payment provider says order not found after waiting on the same QR/page",
65
66
  "topic": "qr-refresh",
66
67
  "command": "itp docs show qr-refresh --role buyer --json"
67
68
  }
@@ -17,7 +17,7 @@
17
17
  "commands": [
18
18
  {
19
19
  "intent": "wait for payment verified event",
20
- "command": "itp buyer payment wait <payment_intent_id> --json",
20
+ "command": "itp buyer payment wait <payment_intent_id> --qr-shown --json",
21
21
  "success_signal": "response.payment_event.event_type == payment_intent.verified"
22
22
  },
23
23
  {
@@ -27,7 +27,8 @@
27
27
  }
28
28
  ],
29
29
  "agent_rules": [
30
- "Start payment wait only after the QR image/link has already been sent to the human.",
30
+ "Start payment wait only after the QR image/link has already been sent to the human; use --qr-shown to confirm that step is complete.",
31
+ "If you run payment wait without --qr-shown while payment is still pending, CLI returns payment_handoff_required and human_visible_markdown instead of long-polling.",
31
32
  "Payment wait is a recovery loop around the same payment_intent_id, not a one-shot command.",
32
33
  "wait.timeout is a heartbeat, not a payment failure.",
33
34
  "In Codex or Claude Code app clients, after showing QR, wait online for a short visible cycle first. If the human still has not paid, explain that the same QR/link remains valid and pause for their reply.",
@@ -5,7 +5,7 @@
5
5
  "title": "QR Refresh Recovery",
6
6
  "purpose": "Teach the agent how to recover scanner problems without changing payment truth or creating a second order.",
7
7
  "when_to_use": [
8
- "The human reports Alipay sandbox says order not found after waiting 30-60 seconds on the same QR/page.",
8
+ "The human reports the payment provider says order not found after waiting 30-60 seconds on the same QR/page.",
9
9
  "The QR expired or the wallet cannot load it.",
10
10
  "The payment intent is still not verified."
11
11
  ],
@@ -29,7 +29,7 @@
29
29
  "QR refresh is display recovery only. ItPay may return the same QR again when the current provider QR is still valid.",
30
30
  "Use the same payment_intent_id.",
31
31
  "If QR does not render, first use the returned local_qr_path, qr_png_url, or preferred_qr_url. Do not generate a new QR from guessed provider payload.",
32
- "For Alipay sandbox order-not-found, first ask the human to wait and scan/open the same QR/page again; use refresh only after that wait has failed.",
32
+ "For provider order-not-found, first ask the human to wait and scan/open the same QR/page again; use refresh only after that wait has failed.",
33
33
  "If refresh succeeds, show only the returned ItPay QR/display URL and continue waiting on the same payment_intent_id.",
34
34
  "After refresh, continue waiting through the normal wait contract."
35
35
  ],
@@ -36,8 +36,8 @@
36
36
  },
37
37
  {
38
38
  "intent": "buy a selected variant with the high-level cart-first flow",
39
- "command": "itp buy <variant_id> --sandbox --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --json",
40
- "success_signal": "response.status becomes waiting_human_auth or waiting_user_payment; if waiting_user_payment, response.payment_handoff contains markdown and wait_command"
39
+ "command": "itp buy <variant_id> --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --json",
40
+ "success_signal": "response.status becomes waiting_human_auth or payment_handoff_required; if payment_handoff_required, send response.human_visible_markdown to the human before running response.after_human_visible_markdown.command"
41
41
  }
42
42
  ],
43
43
  "agent_rules": [
@@ -54,8 +54,9 @@
54
54
  "After showing auth_qr, keep running/resuming the same checkout unless the human explicitly asks you to pause. Do not stop merely because a QR was displayed.",
55
55
  "When a buyer command returns buyer_session.status=buyer_session_saved, the CLI has stored the buyer account session for this agent device. You may continue with checkout/payment or `buyer vault` commands without asking the human for a token.",
56
56
  "Vault grants are only for reading approved delivered artifacts. They are not buyer session credentials and do not authorize refunds or account/order management.",
57
- "Use high-level itp buy when possible, but in agent app clients split payment into two visible steps: first return and show payment_handoff, then run payment_handoff.wait_command.",
58
- "If response.payment_handoff.markdown is present, send it to the human immediately in the same user-visible message. It includes the QR image and payment link.",
57
+ "Use high-level itp buy when possible, but in agent app clients split payment into two visible steps: first return and show human_visible_markdown, then run after_human_visible_markdown.command.",
58
+ "If response.human_visible_markdown is present, send it to the human immediately in the same user-visible message. It includes the QR image and payment link.",
59
+ "If response.status is payment_handoff_required, do not run payment wait from next.command; next is the human-visible reply. Only after the QR/link is visible may you run after_human_visible_markdown.command.",
59
60
  "Show the returned payment QR exactly as provided before starting payment wait.",
60
61
  "Payment truth comes only from payment_intent.verified.",
61
62
  "Secure delivery goes to the human first; report redacted delivery status only."
package/lib/buyer.js CHANGED
@@ -7,9 +7,10 @@ import { coreApi, coreApiBase } from "./http.js";
7
7
  import { renderHumanAction, renderItPayPaymentAction, shouldReturnAfterAgentTextQR, writeWaitHeartbeat } from "./render-human.js";
8
8
 
9
9
  async function buyerBuy(flags) {
10
+ rejectBuyerSandboxFlag(flags);
10
11
  const showThenWait = shouldReturnAfterAgentTextQR(flags);
11
12
  const selectionID = flags.selection || flags.variant || flags.catalog_variant_id || flags.item || flags.catalog_item_id;
12
- if (!selectionID) throw new Error("catalog variant id is required, for example: itp buy var_pubg_couple_skin_cny20 --sandbox --email buyer@example.com --phone +8613800000000 --json");
13
+ if (!selectionID) throw new Error("catalog variant id is required, for example: itp buy <variant_id> --email <buyer_email> --json");
13
14
  const selection = await resolveBuyerCatalogSelection(selectionID, flags);
14
15
  const cart = await createBuyerCart(selection, flags);
15
16
  let checkout = await createBuyerCheckoutFromCart(cart, selection, flags);
@@ -44,18 +45,15 @@ async function buyerBuy(flags) {
44
45
 
45
46
  if (flags.no_wait || flags.no_wait_payment || showThenWait) {
46
47
  output(buyerRunOutput({
47
- status: "waiting_user_payment",
48
+ status: "payment_handoff_required",
48
49
  selection,
49
50
  cart,
50
51
  checkout,
51
52
  payment_intent: intent,
52
53
  payment_handoff: paymentHandoff(intent),
53
- agent_next_actions: intent.agent_next_actions || ["wait_payment"],
54
- next: {
55
- command: paymentWaitCommand(intent.payment_intent_id, { short: true }),
56
- safe_for_agent: true,
57
- instruction: "Send payment_handoff.markdown to the human first, then run this wait command unless the human explicitly asked you to pause."
58
- }
54
+ agent_next_actions: paymentHandoffAgentNextActions(),
55
+ next: paymentHandoffNext(),
56
+ after_human_visible_markdown: paymentHandoffAfter(intent)
59
57
  }));
60
58
  return;
61
59
  }
@@ -81,6 +79,7 @@ async function buyerBuy(flags) {
81
79
  }
82
80
 
83
81
  async function buyer(command, rest, flags) {
82
+ rejectBuyerSandboxFlag(flags);
84
83
  const subcommand = rest[0] && !String(rest[0]).startsWith("--") ? rest[0] : "";
85
84
  if (command === "catalog") {
86
85
  if (subcommand === "search") {
@@ -315,19 +314,16 @@ async function buyer(command, rest, flags) {
315
314
  const intent = await getBuyerPaymentIntent(checkout.payment_intent_id, flags);
316
315
  await renderItPayPaymentAction(intent, flags);
317
316
  output(buyerRunOutput({
318
- status: intent.status === "verified" ? "payment_verified" : "waiting_user_payment",
317
+ status: intent.status === "verified" ? "payment_verified" : "payment_handoff_required",
319
318
  checkout,
320
319
  payment_intent: intent,
321
320
  payment_handoff: intent.status === "verified" ? undefined : paymentHandoff(intent),
322
321
  buyer_session: buyerSessionClaimStatus(claimedSession),
323
- agent_next_actions: intent.agent_next_actions || checkout.agent_next_actions || ["wait_payment"],
322
+ agent_next_actions: intent.status === "verified" ? (intent.agent_next_actions || checkout.agent_next_actions || ["poll_checkout"]) : paymentHandoffAgentNextActions(),
324
323
  next: intent.status === "verified"
325
324
  ? { command: cliCommand("buyer", "checkout", "status", checkout.checkout_id, "--json"), safe_for_agent: true }
326
- : {
327
- command: paymentWaitCommand(intent.payment_intent_id, { short: true }),
328
- safe_for_agent: true,
329
- instruction: "Send payment_handoff.markdown to the human first, then run this wait command unless the human explicitly asked you to pause."
330
- }
325
+ : paymentHandoffNext(),
326
+ after_human_visible_markdown: intent.status === "verified" ? undefined : paymentHandoffAfter(intent)
331
327
  }));
332
328
  return;
333
329
  }
@@ -341,16 +337,13 @@ async function buyer(command, rest, flags) {
341
337
  const intent = await createBuyerPaymentIntent(checkout.checkout_id, flags);
342
338
  await renderItPayPaymentAction(intent, flags);
343
339
  output(buyerRunOutput({
344
- status: "waiting_user_payment",
340
+ status: "payment_handoff_required",
345
341
  checkout,
346
342
  payment_intent: intent,
347
343
  payment_handoff: paymentHandoff(intent),
348
- agent_next_actions: intent.agent_next_actions || ["wait_payment"],
349
- next: {
350
- command: paymentWaitCommand(intent.payment_intent_id, { short: true }),
351
- safe_for_agent: true,
352
- instruction: "Send payment_handoff.markdown to the human first, then run this wait command unless the human explicitly asked you to pause."
353
- }
344
+ agent_next_actions: paymentHandoffAgentNextActions(),
345
+ next: paymentHandoffNext(),
346
+ after_human_visible_markdown: paymentHandoffAfter(intent)
354
347
  }));
355
348
  return;
356
349
  }
@@ -369,6 +362,19 @@ async function buyer(command, rest, flags) {
369
362
  const paymentIntentID = flags.payment_intent || flags.payment_intent_id || positional(rest, 1) || readState().last_core_payment_intent_id;
370
363
  if (!paymentIntentID) throw new Error("payment_intent_id is required");
371
364
  const intent = await getBuyerPaymentIntent(paymentIntentID, flags);
365
+ if (intent.status !== "verified" && !paymentQRShown(flags)) {
366
+ await renderItPayPaymentAction(intent, flags);
367
+ output(buyerRunOutput({
368
+ status: "payment_handoff_required",
369
+ payment_intent: intent,
370
+ payment_handoff: paymentHandoff(intent),
371
+ payment_guidance: paymentRecoveryGuidance(intent, { event_type: "payment_display_required" }),
372
+ agent_next_actions: paymentHandoffAgentNextActions(),
373
+ next: paymentHandoffNext(),
374
+ after_human_visible_markdown: paymentHandoffAfter(intent)
375
+ }));
376
+ return;
377
+ }
372
378
  const event = intent.status === "verified"
373
379
  ? { event_type: "payment_intent.verified", payment_intent_id: paymentIntentID, agent_next_actions: intent.agent_next_actions || ["poll_checkout"] }
374
380
  : await waitBuyerPayment(intent, flags);
@@ -380,7 +386,7 @@ async function buyer(command, rest, flags) {
380
386
  agent_next_actions: event.agent_next_actions || intent.agent_next_actions || paymentAgentNextActions(intent, event),
381
387
  next: event.event_type === "payment_intent.verified"
382
388
  ? { command: intent.checkout_id ? cliCommand("buyer", "checkout", "status", intent.checkout_id, "--json") : undefined, safe_for_agent: true }
383
- : { command: cliCommand("buyer", "payment", "wait", paymentIntentID, "--json"), safe_for_agent: true }
389
+ : { command: paymentWaitCommand(paymentIntentID, { qrShown: true }), safe_for_agent: true }
384
390
  }));
385
391
  return;
386
392
  }
@@ -390,18 +396,15 @@ async function buyer(command, rest, flags) {
390
396
  const refreshed = await refreshBuyerPaymentQR(paymentIntentID, flags);
391
397
  await renderItPayPaymentAction(refreshed, flags);
392
398
  output(buyerRunOutput({
393
- status: refreshed.status === "verified" ? "payment_verified" : "waiting_user_payment",
399
+ status: refreshed.status === "verified" ? "payment_verified" : "payment_handoff_required",
394
400
  payment_intent: refreshed,
395
401
  payment_handoff: refreshed.status === "verified" ? undefined : paymentHandoff(refreshed),
396
402
  payment_guidance: paymentRecoveryGuidance(refreshed, { event_type: refreshed.status === "verified" ? "payment_intent.verified" : "qr_refreshed" }),
397
- agent_next_actions: refreshed.agent_next_actions || paymentAgentNextActions(refreshed, { event_type: refreshed.status === "verified" ? "payment_intent.verified" : "qr_refreshed" }),
403
+ agent_next_actions: refreshed.status === "verified" ? (refreshed.agent_next_actions || paymentAgentNextActions(refreshed, { event_type: "payment_intent.verified" })) : paymentHandoffAgentNextActions(),
398
404
  next: refreshed.status === "verified"
399
405
  ? { command: cliCommand("buyer", "checkout", "status", refreshed.checkout_id, "--json"), safe_for_agent: true }
400
- : {
401
- command: paymentWaitCommand(refreshed.payment_intent_id, { short: true }),
402
- safe_for_agent: true,
403
- instruction: "Send payment_handoff.markdown to the human first, then run this wait command unless the human explicitly asked you to pause."
404
- }
406
+ : paymentHandoffNext(),
407
+ after_human_visible_markdown: refreshed.status === "verified" ? undefined : paymentHandoffAfter(refreshed)
405
408
  }));
406
409
  return;
407
410
  }
@@ -822,7 +825,7 @@ async function createBuyerCheckoutFromCart(cart, selection = null, flags = {}) {
822
825
  const cartID = typeof cart === "string" ? cart : (cart?.cart_id || cart?.id);
823
826
  if (!cartID) throw new Error("cart_id is required");
824
827
  const deliveryContact = {};
825
- if (flags.email) deliveryContact.email = flags.email;
828
+ if (flags.email) deliveryContact.email = realBuyerEmail(flags.email);
826
829
  if (flags.phone) deliveryContact.phone = flags.phone;
827
830
  const missing = requiredDeliveryContactFields(selection).filter((field) => !deliveryContact[field]);
828
831
  if (missing.length) {
@@ -861,6 +864,21 @@ function requiredDeliveryContactFields(selection) {
861
864
  return Array.isArray(fields) ? fields.map((field) => String(field).trim()).filter(Boolean) : [];
862
865
  }
863
866
 
867
+ function rejectBuyerSandboxFlag(flags = {}) {
868
+ if (!flags?.sandbox) return;
869
+ throw new Error("--sandbox is not used by buyer commands; environment is selected by the ItPay API base. Run the command without --sandbox.");
870
+ }
871
+
872
+ function realBuyerEmail(value) {
873
+ const email = String(value || "").trim();
874
+ if (!email) return "";
875
+ const domain = email.split("@").pop().toLowerCase();
876
+ if (["example.com", "example.net", "example.org"].includes(domain)) {
877
+ throw new Error("placeholder email rejected; ask the human for their real delivery email before checkout.");
878
+ }
879
+ return email;
880
+ }
881
+
864
882
  async function createBuyerPaymentIntent(checkoutID, flags = {}) {
865
883
  if (!checkoutID) throw new Error("checkout_id is required");
866
884
  const method = String(flags.method || flags.payment_method || "alipay").toLowerCase();
@@ -1035,7 +1053,7 @@ function paymentHandoff(intent = {}) {
1035
1053
  const preferredQRURL = action.preferred_qr_url || qrPNGURL || action.qr_image_url || intent.qr_image_url || intent.qr?.image_url || "";
1036
1054
  const localQRPath = action.local_qr_path || intent.local_qr_path || "";
1037
1055
  const mobileWalletURL = action.mobile_wallet_url || intent.mobile_wallet_url || "";
1038
- const waitCommand = paymentWaitCommand(paymentIntentID, { short: true });
1056
+ const waitCommand = paymentWaitCommand(paymentIntentID, { short: true, qrShown: true });
1039
1057
  return {
1040
1058
  type: "payment_qr_handoff",
1041
1059
  payment_intent_id: paymentIntentID || null,
@@ -1049,7 +1067,8 @@ function paymentHandoff(intent = {}) {
1049
1067
  markdown: paymentHandoffMarkdown({ localQRPath, qrPNGURL, preferredQRURL, entryURL, mobileWalletURL }),
1050
1068
  wait_command: waitCommand || null,
1051
1069
  safe_for_agent: true,
1052
- instruction: "Show markdown to the human first. Then run wait_command and wait for payment_intent.verified unless the human asked you to pause."
1070
+ instruction: "Show markdown to the human first. Then run wait_command and wait for payment_intent.verified unless the human asked you to pause.",
1071
+ display_targets: paymentHandoffDisplayTargets()
1053
1072
  };
1054
1073
  }
1055
1074
 
@@ -1063,11 +1082,46 @@ function paymentHandoffMarkdown({ localQRPath = "", qrPNGURL = "", preferredQRUR
1063
1082
  return lines.join("\n\n");
1064
1083
  }
1065
1084
 
1066
- function paymentWaitCommand(paymentIntentID, { short = false } = {}) {
1085
+ function paymentWaitCommand(paymentIntentID, { short = false, qrShown = false } = {}) {
1067
1086
  if (!paymentIntentID) return "";
1068
- return short
1069
- ? cliCommand("buyer", "payment", "wait", paymentIntentID, "--timeout", "60", "--json")
1070
- : cliCommand("buyer", "payment", "wait", paymentIntentID, "--json");
1087
+ const args = ["buyer", "payment", "wait", paymentIntentID];
1088
+ if (short) args.push("--timeout", "60");
1089
+ if (qrShown) args.push("--qr-shown");
1090
+ args.push("--json");
1091
+ return cliCommand(...args);
1092
+ }
1093
+
1094
+ function paymentQRShown(flags = {}) {
1095
+ if (flags.qr_shown === undefined) return false;
1096
+ return booleanFlag(flags.qr_shown);
1097
+ }
1098
+
1099
+ function paymentHandoffAgentNextActions() {
1100
+ return ["show_human_visible_markdown"];
1101
+ }
1102
+
1103
+ function paymentHandoffNext() {
1104
+ return {
1105
+ type: "reply_to_human",
1106
+ safe_for_agent: false,
1107
+ instruction: "Send human_visible_markdown to the human now. Do not run payment wait until this message is visible to the human."
1108
+ };
1109
+ }
1110
+
1111
+ function paymentHandoffAfter(intent = {}) {
1112
+ return {
1113
+ command: paymentWaitCommand(intent.payment_intent_id, { short: true, qrShown: true }),
1114
+ safe_for_agent: true,
1115
+ instruction: "Run this only after human_visible_markdown has been sent to the human."
1116
+ };
1117
+ }
1118
+
1119
+ function paymentHandoffDisplayTargets() {
1120
+ return {
1121
+ desktop_app: ["local_qr_path", "qr_png_url", "payment_entry_url"],
1122
+ terminal: ["qr_png_url", "payment_entry_url", "mobile_wallet_url"],
1123
+ chat_cloud: ["qr_png_url", "payment_entry_url", "mobile_wallet_url"]
1124
+ };
1071
1125
  }
1072
1126
 
1073
1127
  function paymentRecoveryGuidance(intent = {}, event = {}) {
@@ -1367,7 +1421,7 @@ async function refreshBuyerPaymentQR(paymentIntentID, flags = {}) {
1367
1421
  if (intent.status === "verified") return intent;
1368
1422
  const refreshURL = intent.qr_refresh_url;
1369
1423
  if (!refreshURL) {
1370
- throw new Error("payment intent does not expose qr_refresh_url; refresh is supported only for refreshable Alipay sandbox QR intents");
1424
+ throw new Error("payment intent does not expose qr_refresh_url; refresh is supported only for refreshable provider QR intents");
1371
1425
  }
1372
1426
  return await coreApi(refreshURL, {
1373
1427
  method: "POST",
@@ -1409,7 +1463,7 @@ async function waitBuyerPayment(intent, flags = {}) {
1409
1463
  action: intent?.human_action || null,
1410
1464
  lastHeartbeatAt,
1411
1465
  flags,
1412
- command: cliCommand("buyer", "payment", "wait", paymentIntentID, "--json")
1466
+ command: paymentWaitCommand(paymentIntentID, { qrShown: true })
1413
1467
  });
1414
1468
  }
1415
1469
  return lastEvent || { event_type: "wait.timeout", payment_intent_id: paymentIntentID, cursor, agent_next_actions: ["wait_payment"] };
@@ -1436,10 +1490,11 @@ function isBuyerDeliveryComplete(result) {
1436
1490
  }
1437
1491
 
1438
1492
  function buyerRunOutput(value = {}) {
1493
+ const body = compactBuyerOutput(value);
1439
1494
  return normalizeBuyerMoneyFields(stripInternalBuyerFields({
1440
1495
  schema_version: "itp.buyer.v1",
1496
+ ...body,
1441
1497
  docs: value.docs || buyerDocsFor(value),
1442
- ...value,
1443
1498
  secrets: {
1444
1499
  raw_content_included: false,
1445
1500
  claim_token_included: false,
@@ -1448,6 +1503,158 @@ function buyerRunOutput(value = {}) {
1448
1503
  }));
1449
1504
  }
1450
1505
 
1506
+ function compactBuyerOutput(value = {}) {
1507
+ const result = {};
1508
+ if (value.status !== undefined) result.status = value.status;
1509
+ if (value.payment_handoff?.markdown) {
1510
+ result.must_reply_to_human_before_next_command = true;
1511
+ result.human_visible_markdown = value.payment_handoff.markdown;
1512
+ }
1513
+ for (const [key, item] of Object.entries(value)) {
1514
+ if (["docs", "status"].includes(key)) continue;
1515
+ result[key] = compactBuyerField(key, item);
1516
+ }
1517
+ return result;
1518
+ }
1519
+
1520
+ function compactBuyerField(key, value) {
1521
+ if (key === "selection") return compactSelection(value);
1522
+ if (key === "selections") return Array.isArray(value) ? value.map(compactSelection) : value;
1523
+ if (key === "product") return compactProduct(value);
1524
+ if (key === "products") return Array.isArray(value) ? value.map(compactProduct) : value;
1525
+ if (key === "cart") return compactCart(value);
1526
+ if (key === "checkout") return compactCheckout(value);
1527
+ if (key === "payment_intent") return compactPaymentIntent(value);
1528
+ return value;
1529
+ }
1530
+
1531
+ function compactSelection(selection = {}) {
1532
+ if (!selection || typeof selection !== "object") return selection;
1533
+ return compactObject({
1534
+ catalog_item_id: selection.catalog_item_id,
1535
+ catalog_variant_id: selection.catalog_variant_id,
1536
+ ucp_variant_id: selection.ucp_variant_id,
1537
+ offer_id: selection.offer_id,
1538
+ title: selection.title,
1539
+ variant_title: selection.variant_title,
1540
+ amount: selection.expected_amount,
1541
+ currency: selection.currency,
1542
+ required_contact_fields: selection.required_contact_fields,
1543
+ purchasable: selection.purchasable
1544
+ });
1545
+ }
1546
+
1547
+ function compactProduct(product = {}) {
1548
+ if (!product || typeof product !== "object") return product;
1549
+ return compactObject({
1550
+ id: product.id,
1551
+ title: product.title,
1552
+ description: product.description,
1553
+ selected: product.selected,
1554
+ variants: Array.isArray(product.variants) ? product.variants.map(compactVariant) : undefined
1555
+ });
1556
+ }
1557
+
1558
+ function compactVariant(variant = {}) {
1559
+ const metadata = variant.metadata || {};
1560
+ return compactObject({
1561
+ id: variant.id,
1562
+ title: variant.title,
1563
+ description: variant.description,
1564
+ price: variant.price,
1565
+ availability: variant.availability,
1566
+ required_profile_fields: metadata["ai.itpay.required_profile_fields"],
1567
+ input_schema_json: metadata["ai.itpay.api.input_schema_json"],
1568
+ agent_explanation_zh: metadata["ai.itpay.agent_explanation_zh"]
1569
+ });
1570
+ }
1571
+
1572
+ function compactCart(cart = {}) {
1573
+ if (!cart || typeof cart !== "object") return cart;
1574
+ return compactObject({
1575
+ cart_id: cart.cart_id || cart.id,
1576
+ status: cart.status,
1577
+ amount: cart.amount,
1578
+ currency: cart.currency,
1579
+ line_items: Array.isArray(cart.line_items) ? cart.line_items.map(compactCartLine) : undefined
1580
+ });
1581
+ }
1582
+
1583
+ function compactCartLine(line = {}) {
1584
+ return compactObject({
1585
+ id: line.id,
1586
+ quantity: line.quantity,
1587
+ amount: line.amount,
1588
+ currency: line.currency,
1589
+ input: line.input,
1590
+ item: line.item ? compactObject({
1591
+ id: line.item.id,
1592
+ title: line.item.title,
1593
+ catalog_item_id: line.item.catalog_item_id,
1594
+ catalog_variant_id: line.item.catalog_variant_id,
1595
+ offer_id: line.item.offer_id
1596
+ }) : undefined
1597
+ });
1598
+ }
1599
+
1600
+ function compactCheckout(checkout = {}) {
1601
+ if (!checkout || typeof checkout !== "object") return checkout;
1602
+ return compactObject({
1603
+ checkout_id: checkout.checkout_id,
1604
+ cart_id: checkout.cart_id,
1605
+ order_id: checkout.order_id,
1606
+ status: checkout.status,
1607
+ delivery_status: checkout.delivery_status,
1608
+ identity_status: checkout.identity_status,
1609
+ next_required_action: checkout.next_required_action,
1610
+ payment_intent_id: checkout.payment_intent_id,
1611
+ amount: checkout.amount,
1612
+ currency: checkout.currency,
1613
+ delivery: checkout.delivery,
1614
+ human_action: checkout.human_action,
1615
+ agent_next_actions: checkout.agent_next_actions
1616
+ });
1617
+ }
1618
+
1619
+ function compactPaymentIntent(intent = {}) {
1620
+ if (!intent || typeof intent !== "object") return intent;
1621
+ return compactObject({
1622
+ payment_intent_id: intent.payment_intent_id,
1623
+ payment_attempt_id: intent.payment_attempt_id,
1624
+ checkout_id: intent.checkout_id,
1625
+ order_id: intent.order_id,
1626
+ status: intent.status,
1627
+ amount: intent.amount,
1628
+ currency: intent.currency,
1629
+ human_action: compactHumanAction(intent.human_action),
1630
+ qr_png_url: intent.qr_png_url,
1631
+ qr_image_url: intent.qr_image_url,
1632
+ mobile_wallet_url: intent.mobile_wallet_url,
1633
+ agent_wait: intent.agent_wait,
1634
+ agent_next_actions: intent.agent_next_actions
1635
+ });
1636
+ }
1637
+
1638
+ function compactHumanAction(action = null) {
1639
+ if (!action || typeof action !== "object") return action;
1640
+ return compactObject({
1641
+ kind: action.kind,
1642
+ url: action.url,
1643
+ local_qr_path: action.local_qr_path,
1644
+ local_qr_mime: action.local_qr_mime,
1645
+ qr_png_url: action.qr_png_url,
1646
+ qr_image_url: action.qr_image_url,
1647
+ preferred_qr_url: action.preferred_qr_url,
1648
+ mobile_wallet_url: action.mobile_wallet_url,
1649
+ agent_display_hint: action.agent_display_hint,
1650
+ expires_at: action.expires_at
1651
+ });
1652
+ }
1653
+
1654
+ function compactObject(value = {}) {
1655
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== null));
1656
+ }
1657
+
1451
1658
  function normalizeBuyerMoneyFields(value) {
1452
1659
  if (Array.isArray(value)) return value.map((item) => normalizeBuyerMoneyFields(item));
1453
1660
  if (!value || typeof value !== "object") return value;
@@ -1481,7 +1688,7 @@ function buyerDocsFor(value = {}) {
1481
1688
  topics.add("cart-checkout");
1482
1689
  topics.add("payment-qr");
1483
1690
  }
1484
- if (status.includes("waiting_user_payment") || actions.includes("wait_payment") || value.payment_intent?.human_action || value.payment_intent?.qr_image_url) {
1691
+ if (status.includes("payment_handoff") || status.includes("waiting_user_payment") || actions.includes("show_human_visible_markdown") || actions.includes("wait_payment") || value.payment_intent?.human_action || value.payment_intent?.qr_image_url) {
1485
1692
  topics.add("payment-qr");
1486
1693
  topics.add("payment-wait");
1487
1694
  }
package/lib/runtime.js CHANGED
@@ -448,7 +448,7 @@ async function maybeMockApproveDeviceAuth(authId, flags) {
448
448
  return;
449
449
  }
450
450
  if (!fakeTestingAllowed(flags)) {
451
- throw new Error("mock approval is developer-only and disabled for agent runs; use real Alipay sandbox authentication");
451
+ throw new Error("mock approval is developer-only and disabled for agent runs; use real payment-provider authentication");
452
452
  }
453
453
  const alipayUserId = flags.alipay_user_id || process.env.ITPAY_MOCK_ALIPAY_USER_ID || `2088${crypto.randomInt(100000000000, 999999999999)}`;
454
454
  await api(`/api/itp/auth/device/${encodeURIComponent(authId)}/mock-approve`, {
@@ -692,10 +692,10 @@ async function createCheckoutResult(flags) {
692
692
 
693
693
  function validateLivePaymentFlags(method, flags = {}) {
694
694
  if (String(method).toLowerCase() === "fake" && !fakeTestingAllowed(flags)) {
695
- throw new Error("fake payment is developer-only and disabled for agent runs; use --method alipay for local, sandbox, and live testing");
695
+ throw new Error("fake payment is developer-only and disabled for agent runs; use --method alipay for local and live testing");
696
696
  }
697
697
  if ((flags.mock_approve || process.env.ITPAY_MOCK_APPROVE === "true" || process.env.ITPAY_MOCK_APPROVE === "1") && !fakeTestingAllowed(flags)) {
698
- throw new Error("mock approval is developer-only and disabled for agent runs; use real Alipay sandbox authentication");
698
+ throw new Error("mock approval is developer-only and disabled for agent runs; use real payment-provider authentication");
699
699
  }
700
700
  }
701
701
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itpay/cli",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "ItPay CLI, buyer skill, and agent-readable docs for agent-native commerce.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -59,7 +59,7 @@ read this skill
59
59
  The high-level command can wrap this flow:
60
60
 
61
61
  ```bash
62
- itp buy <variant_id> --sandbox --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --json
62
+ itp buy <variant_id> --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --json
63
63
  ```
64
64
 
65
65
  For step-by-step testing:
@@ -93,7 +93,7 @@ itp buyer cart create --variant var_itpay_enterprise_fuzzy_search_cny01 --input
93
93
  itp buyer cart show <cart_id> --json
94
94
  itp buyer cart add <cart_id> --variant var_itpay_enterprise_fuzzy_search_cny01 --input company_name=美团 --json
95
95
  itp buyer cart create --variant var_itpay_enterprise_precise_lookup_cny05 --input company_name_or_credit_no=北京京东世纪贸易有限公司 --json
96
- itp buy var_itpay_enterprise_fuzzy_search_cny01 --sandbox --email <buyer_email> --input company_name=京东 --display agent --no-wait-payment --json
96
+ itp buy var_itpay_enterprise_fuzzy_search_cny01 --email <buyer_email> --input company_name=京东 --display agent --no-wait-payment --json
97
97
  ```
98
98
 
99
99
  For cart edits, always read the server cart first with `buyer cart show`.
@@ -206,23 +206,28 @@ Each docs page includes `next_docs`. Follow those links as the state changes.
206
206
  For payment creation in an agent/chat client, prefer:
207
207
 
208
208
  ```bash
209
- itp buy <variant_id> --sandbox --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --json
209
+ itp buy <variant_id> --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --json
210
210
  ```
211
211
 
212
212
  This keeps JSON output machine-readable while allowing the CLI to prepare a
213
213
  local QR image path for clients that cannot render remote SVG reliably. In
214
214
  Codex or Claude Code app clients, prefer `--no-wait-payment`: send
215
- `payment_handoff.markdown` to the human first, then run
216
- `payment_handoff.wait_command`. If the human is on mobile, present
215
+ `human_visible_markdown` to the human first, then run
216
+ `after_human_visible_markdown.command`. If the human is on mobile, present
217
217
  `mobile_wallet_url` as a clickable human-only fallback; do not convert it into a
218
218
  QR.
219
219
 
220
+ If a response has `status=payment_handoff_required`, `next` is the user-visible
221
+ reply step, not payment wait. Do not run `buyer payment wait` until the QR/link
222
+ has been sent to the human; then use the returned
223
+ `after_human_visible_markdown.command` with `--qr-shown`.
224
+
220
225
  For first-purchase auth, treat the returned ItPay authorization entry as a
221
226
  single human orchestration entry. It may open Alipay login/registration first
222
227
  and then payment after ItPay receives the OAuth callback. Do not call
223
228
  `oauth_start_url` directly unless the ItPay auth page asks for fallback.
224
- If the payment page says the Alipay sandbox entry is stabilizing/preparing,
225
- or if Alipay sandbox says "order not found", tell the human to wait 30-60
229
+ If the payment page says provider entry is stabilizing/preparing,
230
+ or if the payment provider says "order not found", tell the human to wait 30-60
226
231
  seconds and use the same page/QR again. Do not ask them to refresh repeatedly,
227
232
  and do not create another checkout or payment intent. Use
228
233
  `itp buyer payment refresh-qr ... --reason order-not-found` only after the
@@ -238,7 +243,7 @@ I found the service and selected the matching variant.
238
243
  I created the cart and checkout.
239
244
  Please open the returned ItPay authorization link and approve Alipay login.
240
245
  I am waiting for ItPay account authorization.
241
- Please scan the returned ItPay-hosted QR image with Alipay sandbox.
246
+ Please scan the returned ItPay-hosted QR image with the payment provider.
242
247
  I am waiting for ItPay payment verification.
243
248
  Payment is verified.
244
249
  Delivery is claimable by the human buyer. Please check your email.