@itpay/cli 0.1.8 → 0.1.10

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
@@ -155,7 +155,7 @@ from the current state.
155
155
  Before starting a new purchase, agents should inspect recoverable local state:
156
156
 
157
157
  ```bash
158
- itp status --json
158
+ itp status --refresh --json
159
159
  ```
160
160
 
161
161
  If an unfinished run exists, continue it:
@@ -241,6 +241,15 @@ itp buyer vault read --order <order_id> --artifact <vault_artifact_id> --json
241
241
  Agents must not ask humans to paste claim links, claim tokens, raw API results,
242
242
  provider keys, or grant ids into chat.
243
243
 
244
+ Order/account/refund commands require a server-verified buyer session, not a
245
+ vault grant. If they fail with a buyer session error, run:
246
+
247
+ ```bash
248
+ itp status --refresh --json
249
+ ```
250
+
251
+ Then follow the returned `next.command`.
252
+
244
253
  ## Agent Skill And Docs
245
254
 
246
255
  Installed agents can read the buyer skill and docs graph at any time:
package/bin/itp CHANGED
@@ -222,6 +222,7 @@ async function main() {
222
222
  "buyer vault read --order <order_id> --artifact <vault_artifact_id> --json",
223
223
  "account login-link --json",
224
224
  "status --json",
225
+ "status --refresh --json",
225
226
  "docs list --role buyer --json",
226
227
  "docs show quickstart --role buyer --json",
227
228
  "docs search <question> --role buyer --json",
@@ -1954,26 +1955,37 @@ async function agentStatus(flags) {
1954
1955
  if (!run) {
1955
1956
  const config = readConfig();
1956
1957
  const credentials = readCredentials();
1957
- let auth = { authenticated: Boolean(readSessionToken(credentials)), account_id: config.account_id || null, device_id: config.device_id || null };
1958
- if (auth.authenticated && flags.refresh) {
1959
- try {
1960
- auth = await api("/api/itp/auth/status", { method: "GET" }, flags);
1961
- } catch (error) {
1962
- auth = { authenticated: false, account_id: config.account_id || null, error: error.message };
1963
- }
1964
- }
1965
- output({
1966
- schema_version: "itp.agent.v1",
1967
- status: auth.authenticated ? "idle" : "unauthenticated",
1958
+ const hasLocalSession = Boolean(readSessionToken(credentials));
1959
+ const auth = flags.refresh
1960
+ ? await refreshBuyerSessionForStatus(flags)
1961
+ : {
1962
+ authenticated: hasLocalSession,
1963
+ session_verified: false,
1964
+ account_id: config.account_id || null,
1965
+ device_id: config.device_id || null,
1966
+ auth_source: hasLocalSession ? "local_files_unverified" : "local_files"
1967
+ };
1968
+ output({
1969
+ schema_version: "itp.agent.v1",
1970
+ status: auth.authenticated
1971
+ ? (auth.session_verified === false ? "local_session_unverified" : "idle")
1972
+ : "unauthenticated",
1968
1973
  phase: auth.authenticated ? "idle" : "unauthenticated",
1969
1974
  authenticated: Boolean(auth.authenticated),
1970
- account_id: auth.account_id || null,
1971
- device_id: auth.device_id || null,
1972
- next: auth.authenticated
1973
- ? { type: "choose_credits_or_plan", command: cliCommand("plans", "--json"), safe_for_agent: true }
1974
- : { type: "start_auth", command: cliCommand("setup", "--plan", "credit-300", "--method", "alipay", "--json"), safe_for_agent: true },
1975
- secrets: { raw_key_included: false, session_token_included: false }
1976
- });
1975
+ session_verified: auth.session_verified !== false,
1976
+ auth_source: auth.auth_source,
1977
+ account_id: auth.account_id || null,
1978
+ device_id: auth.device_id || null,
1979
+ next: nextActionForAuthStatus(auth, flags),
1980
+ agent_next_actions: auth.authenticated && auth.session_verified !== false
1981
+ ? ["search_catalog", "view_orders", "create_refund_if_needed", "list_agent_read_grants"]
1982
+ : ["run_status_refresh", "start_human_auth_if_unauthenticated"],
1983
+ note: auth.authenticated && auth.session_verified === false
1984
+ ? "Local buyer session exists but the server has not verified it. Run the next.command before order/refund/account operations."
1985
+ : undefined,
1986
+ error: auth.error || undefined,
1987
+ secrets: { raw_key_included: false, session_token_included: false }
1988
+ });
1977
1989
  return;
1978
1990
  }
1979
1991
  const refreshed = flags.refresh ? await refreshRun(run, flags) : run;
@@ -1981,6 +1993,50 @@ async function agentStatus(flags) {
1981
1993
  output(agentRunResponse(refreshed));
1982
1994
  }
1983
1995
 
1996
+ async function refreshBuyerSessionForStatus(flags = {}) {
1997
+ const config = readConfig();
1998
+ const credentials = readCredentials();
1999
+ if (!readSessionToken(credentials)) {
2000
+ return {
2001
+ authenticated: false,
2002
+ session_verified: true,
2003
+ account_id: config.account_id || null,
2004
+ device_id: config.device_id || null,
2005
+ auth_source: "core_no_session"
2006
+ };
2007
+ }
2008
+ try {
2009
+ const status = await coreApi("/v1/me/auth/status", { method: "GET" }, flags);
2010
+ return {
2011
+ authenticated: true,
2012
+ session_verified: true,
2013
+ account_id: status.buyer_account_id || config.account_id || null,
2014
+ device_id: config.device_id || null,
2015
+ account_status: status.account_status || null,
2016
+ auth_source: "core"
2017
+ };
2018
+ } catch (error) {
2019
+ return {
2020
+ authenticated: false,
2021
+ session_verified: true,
2022
+ account_id: config.account_id || null,
2023
+ device_id: config.device_id || null,
2024
+ auth_source: "core",
2025
+ error: safeErrorMessage(error)
2026
+ };
2027
+ }
2028
+ }
2029
+
2030
+ function nextActionForAuthStatus(auth = {}, flags = {}) {
2031
+ if (auth.authenticated && auth.session_verified === false) {
2032
+ return { type: "verify_buyer_session", command: cliCommand("status", "--refresh", "--json"), safe_for_agent: true };
2033
+ }
2034
+ if (auth.authenticated) {
2035
+ return { type: "buyer_ready", command: cliCommand("buyer", "auth", "status", "--json"), safe_for_agent: true };
2036
+ }
2037
+ return { type: "start_auth", command: cliCommand("setup", "--plan", "credit-300", "--method", "alipay", "--json"), safe_for_agent: true };
2038
+ }
2039
+
1984
2040
  async function resume(flags) {
1985
2041
  const runId = flags.run_id || readState().current_run_id;
1986
2042
  const run = readRun(runId);
@@ -3422,6 +3478,15 @@ async function renderHumanAction(action, flags = {}) {
3422
3478
  return { rendered: false, mode: "json-local-qr", outputs: [localPath] };
3423
3479
  }
3424
3480
  }
3481
+ if (!qrImageURL && shouldGenerateLocalQRFromActionURL(action, mode, flags)) {
3482
+ const localPath = await prepareLocalQRFromActionURL(action, flags, mode !== "file" && !flags.qr_file && !process.env.ITP_QR_FILE);
3483
+ if (localPath) {
3484
+ attachAgentLocalQR(action, localPath);
3485
+ annotateHumanActionPresentation(action, "");
3486
+ persistHumanAction(action, flags);
3487
+ return { rendered: false, mode: "json-local-url-qr", outputs: [localPath] };
3488
+ }
3489
+ }
3425
3490
  return { rendered: false, mode: flags.json ? "json" : mode };
3426
3491
  }
3427
3492
  const host = String(process.env.ITP_HOST || flags.host || "").toLowerCase();
@@ -3435,9 +3500,18 @@ async function renderHumanAction(action, flags = {}) {
3435
3500
  persistHumanAction(action, flags);
3436
3501
  return { rendered: false, mode: localPath ? "agent-local-image-qr" : "agent-image-qr", host, outputs: [localPath || "preferred_qr_url"] };
3437
3502
  }
3438
- process.stderr.write(`No QR image available. Open payment URL: ${action.url}\n`);
3503
+ if (shouldGenerateLocalQRFromActionURL(action, mode, flags)) {
3504
+ const localPath = await prepareLocalQRFromActionURL(action, flags, true);
3505
+ if (localPath) {
3506
+ attachAgentLocalQR(action, localPath);
3507
+ annotateHumanActionPresentation(action, "");
3508
+ persistHumanAction(action, flags);
3509
+ return { rendered: false, mode: "agent-local-url-qr", host, outputs: [localPath] };
3510
+ }
3511
+ }
3512
+ process.stderr.write(`No QR image available. Open action URL: ${action.url}\n`);
3439
3513
  persistHumanAction(action, flags);
3440
- return { rendered: false, mode: "agent-url-fallback", host, outputs: ["payment_url"] };
3514
+ return { rendered: false, mode: "agent-url-fallback", host, outputs: ["action_url"] };
3441
3515
  }
3442
3516
 
3443
3517
  const renderResult = { rendered: false, mode, outputs: [] };
@@ -3461,7 +3535,19 @@ async function renderHumanAction(action, flags = {}) {
3461
3535
  renderResult.outputs.push(file);
3462
3536
  if (mode === "file") return renderResult;
3463
3537
  } else {
3464
- process.stderr.write(`No QR image available. Open payment URL: ${action.url}\n`);
3538
+ const localPath = shouldGenerateLocalQRFromActionURL(action, mode, flags)
3539
+ ? await prepareLocalQRFromActionURL(action, flags, false)
3540
+ : "";
3541
+ if (localPath) {
3542
+ attachAgentLocalQR(action, localPath);
3543
+ annotateHumanActionPresentation(action, "");
3544
+ process.stderr.write(`Alipay action QR image: ${localPath}\n`);
3545
+ renderResult.rendered = true;
3546
+ renderResult.outputs.push(localPath);
3547
+ if (mode === "file") return renderResult;
3548
+ } else {
3549
+ process.stderr.write(`No QR image available. Open action URL: ${action.url}\n`);
3550
+ }
3465
3551
  }
3466
3552
  }
3467
3553
 
@@ -3535,6 +3621,12 @@ function shouldPrepareLocalQRForJSON(mode, flags = {}, action = {}) {
3535
3621
  return false;
3536
3622
  }
3537
3623
 
3624
+ function shouldGenerateLocalQRFromActionURL(action = {}, mode = "", flags = {}) {
3625
+ if (!action?.url) return false;
3626
+ if (action.kind === "auth_qr") return true;
3627
+ return mode === "file" || Boolean(flags.qr_file || process.env.ITP_QR_FILE);
3628
+ }
3629
+
3538
3630
  async function prepareLocalQRFile(action, qrImageURL, flags = {}, optional = false) {
3539
3631
  if (!qrImageURL) return "";
3540
3632
  const file = flags.qr_file || process.env.ITP_QR_FILE || defaultQRFilePath(action, qrImageURL);
@@ -3550,11 +3642,37 @@ async function prepareLocalQRFile(action, qrImageURL, flags = {}, optional = fal
3550
3642
  return file;
3551
3643
  }
3552
3644
 
3645
+ async function prepareLocalQRFromActionURL(action, flags = {}, optional = false) {
3646
+ if (!action?.url) return "";
3647
+ const file = flags.qr_file || process.env.ITP_QR_FILE || defaultGeneratedQRFilePath(action);
3648
+ try {
3649
+ fs.mkdirSync(path.dirname(file), { recursive: true });
3650
+ await QRCode.toFile(file, action.url, {
3651
+ type: "png",
3652
+ errorCorrectionLevel: "M",
3653
+ margin: 2,
3654
+ width: 512
3655
+ });
3656
+ } catch (error) {
3657
+ if (!optional) throw error;
3658
+ action.local_qr_error = safeErrorMessage(error);
3659
+ return "";
3660
+ }
3661
+ action.local_qr_path = file;
3662
+ action.local_qr_mime = "image/png";
3663
+ return file;
3664
+ }
3665
+
3553
3666
  function defaultQRFilePath(action, qrImageURL) {
3554
3667
  const id = sanitizeFilename(action?.payment_intent_id || action?.id || "qr");
3555
3668
  return path.join(os.tmpdir(), `itp-${id}.${qrFileExtension(qrImageURL, action)}`);
3556
3669
  }
3557
3670
 
3671
+ function defaultGeneratedQRFilePath(action) {
3672
+ const id = sanitizeFilename(action?.payment_intent_id || action?.id || "qr");
3673
+ return path.join(os.tmpdir(), `itp-${id}.png`);
3674
+ }
3675
+
3558
3676
  function qrFileExtension(qrImageURL, action = {}) {
3559
3677
  const mime = qrMimeType(qrImageURL, action);
3560
3678
  if (mime === "image/png") return "png";
@@ -3620,6 +3738,22 @@ function attachAgentQRImage(action, qrImageURL, localPath = "") {
3620
3738
  return action;
3621
3739
  }
3622
3740
 
3741
+ function attachAgentLocalQR(action, localPath = "") {
3742
+ if (!action || !localPath) return action;
3743
+ if (!Array.isArray(action.display)) {
3744
+ action.display = [];
3745
+ }
3746
+ if (!action.display.some((item) => item?.type === "image" && item?.local_path === localPath)) {
3747
+ action.display.push({
3748
+ type: "image",
3749
+ format: "png",
3750
+ local_path: localPath,
3751
+ instructions: "Render this local QR image for the human to scan. It encodes the ItPay human action URL, not a provider raw QR payload."
3752
+ });
3753
+ }
3754
+ return action;
3755
+ }
3756
+
3623
3757
  async function downloadQRImage(qrImageURL, file, flags = {}) {
3624
3758
  const controller = new AbortController();
3625
3759
  const timer = setTimeout(() => controller.abort(), apiTimeoutMs(flags));
@@ -3772,9 +3906,9 @@ async function coreApi(pathname, options = {}, flags = {}) {
3772
3906
  });
3773
3907
  } catch (error) {
3774
3908
  if (error?.name === "AbortError") {
3775
- throw new Error(`request timed out after ${Math.ceil(timeoutMs / 1000)}s: ${targetURL}`);
3909
+ throw new Error(`request timed out after ${Math.ceil(timeoutMs / 1000)}s: ${safeRequestTarget(targetURL)}`);
3776
3910
  }
3777
- throw error;
3911
+ throw new Error(`network request failed: ${safeRequestTarget(targetURL)}: ${safeErrorMessage(error)}`);
3778
3912
  } finally {
3779
3913
  clearTimeout(timer);
3780
3914
  }
@@ -3790,11 +3924,27 @@ async function coreApi(pathname, options = {}, flags = {}) {
3790
3924
  if (!response.ok || payload.success === false) {
3791
3925
  const error = new Error(payload.error || payload.message || `request failed: ${response.status}`);
3792
3926
  error.status = response.status;
3927
+ if (response.status === 401 && String(pathname).startsWith("/v1/me/")) {
3928
+ error.message = "buyer session required or expired; run status --refresh --json, then run setup --method alipay --json if unauthenticated";
3929
+ error.next = [
3930
+ { type: "verify_buyer_session", command: cliCommand("status", "--refresh", "--json"), safe_for_agent: true },
3931
+ { type: "start_auth_if_needed", command: cliCommand("setup", "--method", "alipay", "--json"), safe_for_agent: true }
3932
+ ];
3933
+ }
3793
3934
  throw error;
3794
3935
  }
3795
3936
  return payload.data ?? payload;
3796
3937
  }
3797
3938
 
3939
+ function safeRequestTarget(target) {
3940
+ try {
3941
+ const url = new URL(String(target));
3942
+ return `${url.origin}${url.pathname}`;
3943
+ } catch {
3944
+ return String(target).split("?")[0];
3945
+ }
3946
+ }
3947
+
3798
3948
  function coreAgentFingerprint(flags = {}) {
3799
3949
  const explicit = flags.agent_fingerprint || flags.agent_device_fingerprint || process.env.ITPAY_AGENT_FINGERPRINT || process.env.ITPAY_AGENT_DEVICE_FINGERPRINT;
3800
3950
  if (explicit) return String(explicit);
@@ -4131,7 +4281,12 @@ async function withStateLock(fn) {
4131
4281
  fd = fs.openSync(LOCK_PATH, "wx", 0o600);
4132
4282
  fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, started_at: new Date().toISOString() }));
4133
4283
  } catch {
4134
- throw new Error("another itp setup/status operation is running; retry shortly or remove stale ~/.itp/state.lock");
4284
+ const error = new Error(`another itp setup/status operation is running; if no other ItPay CLI is active, remove the stale lock and retry: rm -f ${LOCK_PATH}`);
4285
+ error.next = [
4286
+ { type: "check_status", command: cliCommand("status", "--refresh", "--json"), safe_for_agent: true },
4287
+ { type: "clear_stale_lock", command: `rm -f ${shellQuote(LOCK_PATH)}`, safe_for_agent: true }
4288
+ ];
4289
+ throw error;
4135
4290
  } finally {
4136
4291
  if (fd !== undefined) fs.closeSync(fd);
4137
4292
  }
@@ -4481,7 +4636,9 @@ function output(value) {
4481
4636
  }
4482
4637
 
4483
4638
  function outputError(error) {
4484
- console.error(JSON.stringify({ success: false, message: safeErrorMessage(error) }, null, 2));
4639
+ const payload = { success: false, message: safeErrorMessage(error) };
4640
+ if (error?.next) payload.next = error.next;
4641
+ console.error(JSON.stringify(payload, null, 2));
4485
4642
  }
4486
4643
 
4487
4644
  function maskSecret(secret) {
@@ -14,6 +14,11 @@
14
14
  "must_not_need": ["ops token", "claim token", "raw key", "provider raw payload"]
15
15
  },
16
16
  "commands": [
17
+ {
18
+ "intent": "verify whether this agent/device already has a server-valid buyer session",
19
+ "command": "itp status --refresh --json",
20
+ "success_signal": "response.status is idle when authenticated, or response.next.command tells the agent how to start human auth"
21
+ },
17
22
  {
18
23
  "intent": "list the available agent docs topics",
19
24
  "command": "itp docs list --role buyer --json",
@@ -37,6 +42,7 @@
37
42
  ],
38
43
  "agent_rules": [
39
44
  "Use --json for every ItPay command.",
45
+ "Use `itp status --refresh --json` before account, order, refund, or repeat-purchase decisions. Plain local memory is not identity.",
40
46
  "Search and explain options before buying unless the user already named a variant.",
41
47
  "Add the selected UCP Variant.id to cart; do not bypass cart for CORE-028 flows.",
42
48
  "When explaining prices, remember ItPay JSON money fields use minor units. For CNY, amount=10 means CNY 0.10. Prefer display_amount when present.",
@@ -45,6 +51,7 @@
45
51
  "For first purchase, show auth_qr as the ItPay first-purchase entry: the human first approves provider login/registration/profile sharing, then ItPay should continue the same checkout to payment.",
46
52
  "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.",
47
53
  "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.",
54
+ "Vault grants are only for reading approved delivered artifacts. They are not buyer session credentials and do not authorize refunds or account/order management.",
48
55
  "Use high-level itp buy when possible; it is designed to show the first-purchase entry, wait/resume, continue to payment, wait for verification, and poll delivery.",
49
56
  "Show the returned payment QR exactly as provided.",
50
57
  "Payment truth comes only from payment_intent.verified.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itpay/cli",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "ItPay CLI, buyer skill, and agent-readable docs for agent-native commerce.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,6 +23,7 @@ protocol from this file. Use the CLI docs graph whenever you need details.
23
23
  Run these commands before buying:
24
24
 
25
25
  ```bash
26
+ itp status --refresh --json
26
27
  itp docs show quickstart --role buyer --json
27
28
  itp docs list --role buyer --json
28
29
  ```
@@ -37,6 +38,7 @@ itp docs search "<what you need to know>" --role buyer --json
37
38
 
38
39
  ```text
39
40
  read this skill
41
+ -> run status --refresh and follow next.command if unauthenticated
40
42
  -> read quickstart doc
41
43
  -> search catalog
42
44
  -> explain/recommend a variant
@@ -108,6 +110,9 @@ Refund commands use ItPay shared order state. If `itp buyer refund create`
108
110
  returns `policy_risk_confirmation_required`, explain the returned
109
111
  `refund_eligibility.policy` and `agent_guidance` to the human first. Only retry
110
112
  with `--confirm-policy-risk true` after explicit human confirmation.
113
+ Refund commands require a server-verified buyer session, not a vault grant. If
114
+ the CLI says the buyer session is required or expired, run
115
+ `itp status --refresh --json` and follow the returned `next.command`.
111
116
  Current buyer refunds are whole-order only; do not use line-item refund scope.
112
117
  If the human cancels a refund before provider or money movement starts, use
113
118
  `buyer refund cancel <refund_id> --json`; after cancel, the delivery claim can