@itpay/cli 2.1.34 → 2.1.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +5 -12
  2. package/dist/src/client/backend.js +2 -33
  3. package/dist/src/client/http.js +0 -2
  4. package/dist/src/commands/buy.js +4 -7
  5. package/dist/src/commands/cart.js +15 -15
  6. package/dist/src/commands/catalog.js +7 -17
  7. package/dist/src/commands/checkout.js +5 -146
  8. package/dist/src/commands/checkout_handoff.js +10 -48
  9. package/dist/src/commands/docs.js +18 -7
  10. package/dist/src/commands/guidance.js +19 -69
  11. package/dist/src/commands/install.js +3 -6
  12. package/dist/src/commands/order.js +1 -14
  13. package/dist/src/commands/orders.js +9 -4
  14. package/dist/src/commands/pay.js +1 -1
  15. package/dist/src/commands/readyz.js +2 -2
  16. package/dist/src/commands/refund.js +5 -5
  17. package/dist/src/commands/services.js +167 -1712
  18. package/dist/src/commands/skill.js +5 -8
  19. package/dist/src/commands/vault.js +20 -27
  20. package/dist/src/commands/vault_handoff.js +0 -18
  21. package/dist/src/main.js +58 -294
  22. package/dist/src/render/markdown.js +3 -0
  23. package/dist/src/render/qr.js +1 -1
  24. package/dist/src/state/cart_session.js +1 -4
  25. package/dist/src/state/client_context.js +0 -72
  26. package/dist/src/state/config.js +14 -44
  27. package/dist/src/state/device_authority.js +41 -141
  28. package/docs/agent/buyer/cart-checkout.json +5 -6
  29. package/docs/agent/buyer/catalog-list.json +6 -24
  30. package/docs/agent/buyer/identity-and-sessions.json +9 -25
  31. package/docs/agent/buyer/install-and-setup.json +15 -11
  32. package/docs/agent/buyer/orders-refunds.json +5 -12
  33. package/docs/agent/buyer/payment-flow.json +3 -4
  34. package/docs/agent/buyer/purchased-content.json +4 -18
  35. package/docs/agent/buyer/quickstart.json +16 -24
  36. package/docs/agent/buyer/render-hosts.json +12 -23
  37. package/docs/agent/buyer/service-feedback.json +1 -2
  38. package/docs/cli-reference/agent-types.md +2 -7
  39. package/docs/cli-reference/commands/buy.md +1 -2
  40. package/docs/cli-reference/commands/cart/add.md +1 -1
  41. package/docs/cli-reference/commands/cart/clear.md +1 -1
  42. package/docs/cli-reference/commands/cart/index.md +1 -1
  43. package/docs/cli-reference/commands/cart/next.md +1 -1
  44. package/docs/cli-reference/commands/cart/remove.md +1 -1
  45. package/docs/cli-reference/commands/cart/show.md +1 -1
  46. package/docs/cli-reference/commands/catalog/index.md +1 -1
  47. package/docs/cli-reference/commands/catalog/list.md +2 -2
  48. package/docs/cli-reference/commands/checkout.md +2 -12
  49. package/docs/cli-reference/commands/device.md +11 -18
  50. package/docs/cli-reference/commands/docs/index.md +1 -1
  51. package/docs/cli-reference/commands/docs/list.md +1 -1
  52. package/docs/cli-reference/commands/docs/search.md +1 -1
  53. package/docs/cli-reference/commands/docs/show.md +1 -1
  54. package/docs/cli-reference/commands/install.md +6 -8
  55. package/docs/cli-reference/commands/next.md +1 -1
  56. package/docs/cli-reference/commands/order.md +1 -1
  57. package/docs/cli-reference/commands/pay.md +1 -1
  58. package/docs/cli-reference/commands/readyz.md +10 -58
  59. package/docs/cli-reference/commands/refund/cancel.md +1 -1
  60. package/docs/cli-reference/commands/refund/create.md +1 -1
  61. package/docs/cli-reference/commands/refund/get.md +2 -2
  62. package/docs/cli-reference/commands/refund/index.md +1 -1
  63. package/docs/cli-reference/commands/refund/list.md +1 -1
  64. package/docs/cli-reference/commands/refund/watch.md +1 -1
  65. package/docs/cli-reference/commands/services/action.md +3 -24
  66. package/docs/cli-reference/commands/services/checkout.md +2 -13
  67. package/docs/cli-reference/commands/services/events.md +1 -1
  68. package/docs/cli-reference/commands/services/get.md +2 -4
  69. package/docs/cli-reference/commands/services/index.md +3 -4
  70. package/docs/cli-reference/commands/services/invoke.md +4 -6
  71. package/docs/cli-reference/commands/services/list.md +1 -1
  72. package/docs/cli-reference/commands/services/next.md +12 -105
  73. package/docs/cli-reference/commands/services/quote.md +3 -3
  74. package/docs/cli-reference/commands/services/read-result.md +6 -17
  75. package/docs/cli-reference/commands/services/start.md +2 -2
  76. package/docs/cli-reference/commands/vault/access.md +1 -5
  77. package/docs/cli-reference/conventions.md +6 -4
  78. package/docs/cli-reference/index.md +1 -8
  79. package/docs/skill-bundle-rollout/01-mcp-authentication.md +134 -0
  80. package/docs/skill-bundle-rollout/02-platform-bundle-repositories.md +210 -0
  81. package/docs/skill-bundle-rollout/03-platform-publishing.md +161 -0
  82. package/docs/skill-bundle-rollout/04-first-wave-platforms.md +69 -0
  83. package/docs/skill-bundle-rollout/05-sync-operations.md +62 -0
  84. package/docs/skill-bundle-rollout/README.md +151 -0
  85. package/package.json +13 -20
  86. package/skills/itpay/SKILL.md +105 -101
  87. package/assets/sell-preview/index.html +0 -6
  88. package/assets/sell-preview/preview.js +0 -9
  89. package/bin/darwin-amd64/itpay-sell +0 -0
  90. package/bin/darwin-arm64/itpay-sell +0 -0
  91. package/bin/linux-amd64/itpay-sell +0 -0
  92. package/bin/linux-arm64/itpay-sell +0 -0
  93. package/bin/windows-amd64/itpay-sell.exe +0 -0
  94. package/dist/src/commands/presentation.js +0 -153
  95. package/dist/src/commands/rail_catalog.js +0 -261
  96. package/dist/src/render/browser.js +0 -65
  97. package/dist/src/sell/commands.js +0 -80
  98. package/dist/src/sell/contract.js +0 -86
  99. package/dist/src/sell/local.js +0 -260
  100. package/dist/src/sell/mcp.js +0 -49
  101. package/dist/src/sell/preview.js +0 -70
  102. package/dist/src/sell/sync.js +0 -145
  103. package/dist/src/state/account_auth.js +0 -332
  104. package/dist/src/state/task_journal.js +0 -90
  105. package/docs/agent/buyer/rail-booking.json +0 -162
  106. package/docs/agent/buyer/rail-fast-checkout.json +0 -48
  107. package/docs/cli-reference/commands/auth.md +0 -96
  108. package/docs/cli-reference/commands/sell.md +0 -1364
  109. package/docs/cli-reference/commands/services/page.md +0 -71
  110. package/docs/cli-reference/commands/services/run.md +0 -156
  111. package/docs/sell.md +0 -84
  112. /package/bin/{itp.mjs → itp} +0 -0
@@ -64,8 +64,6 @@ export function defaultHostForAgentType(agentType) {
64
64
  return "plain-chat";
65
65
  if (normalized === "zcode")
66
66
  return "plain-chat";
67
- if (normalized === "doubao-work")
68
- return "plain-chat";
69
67
  if (normalized === "openclaw")
70
68
  return undefined;
71
69
  return "terminal";
@@ -79,73 +77,3 @@ export function validateContext(host, target) {
79
77
  }
80
78
  return undefined;
81
79
  }
82
- export function normalizeViewer(raw) {
83
- const v = raw?.trim().toLowerCase();
84
- if (v === "desktop" || v === "mobile")
85
- return v;
86
- return "unknown";
87
- }
88
- export function normalizeLocality(raw) {
89
- const v = raw?.trim().toLowerCase();
90
- if (v === "same_device" || v === "remote" || v === "cloud")
91
- return v;
92
- return "unknown";
93
- }
94
- // Conservative declared capability table per host. Terminal/desktop agent
95
- // hosts run where a human watches a screen and a system browser exists;
96
- // IM hosts can send links/buttons but cannot be assumed to open pages;
97
- // plain-chat surfaces get links and copyable entries only.
98
- export function hostCapabilities(host) {
99
- switch (host) {
100
- case "terminal":
101
- case "codex":
102
- case "claude-code":
103
- return {
104
- user_visible_browser: "unknown",
105
- system_browser: "yes",
106
- clickable_https: "yes",
107
- image_visible: host === "terminal" ? "no" : "unknown",
108
- native_url_button: "no",
109
- user_visible_terminal: "yes",
110
- other_device_scan: "yes",
111
- send_message: "yes",
112
- };
113
- case "telegram":
114
- case "feishu":
115
- case "lark":
116
- return {
117
- user_visible_browser: "no",
118
- system_browser: "no",
119
- clickable_https: "yes",
120
- image_visible: "yes",
121
- native_url_button: "yes",
122
- user_visible_terminal: "no",
123
- other_device_scan: "unknown",
124
- send_message: "yes",
125
- };
126
- case "discord":
127
- case "whatsapp":
128
- return {
129
- user_visible_browser: "no",
130
- system_browser: "no",
131
- clickable_https: "yes",
132
- image_visible: "yes",
133
- native_url_button: "unknown",
134
- user_visible_terminal: "no",
135
- other_device_scan: "unknown",
136
- send_message: "yes",
137
- };
138
- case "plain-chat":
139
- default:
140
- return {
141
- user_visible_browser: "no",
142
- system_browser: "unknown",
143
- clickable_https: "yes",
144
- image_visible: "no",
145
- native_url_button: "unknown",
146
- user_visible_terminal: "no",
147
- other_device_scan: "unknown",
148
- send_message: "yes",
149
- };
150
- }
151
- }
@@ -1,9 +1,7 @@
1
- import { sellerSessionToken } from "./account_auth.js";
2
- // CLI configuration loader. Production defaults to app.itpay.ai; supported public overrides
3
- // are the official sandbox and dev Backends. Checkout
1
+ // CLI configuration loader. Production defaults to app.itpay.ai; the only
2
+ // allowed override is the official dev Backend. Checkout
4
3
  // display-token persistence belongs to the cart session file, protected with
5
4
  // owner-only permissions. Provider secrets are explicitly out of scope here.
6
- import { createHash } from "node:crypto";
7
5
  import { homedir } from "node:os";
8
6
  import { mkdirSync } from "node:fs";
9
7
  import { resolve } from "node:path";
@@ -14,13 +12,11 @@ import { DeviceAuthority } from "./device_authority.js";
14
12
  import { OperationJournal } from "./operation_journal.js";
15
13
  export const DEFAULT_BASE_URL = "https://app.itpay.ai";
16
14
  export const DEV_BASE_URL = "https://dev.itpay.ai";
17
- export const SANDBOX_BASE_URL = "https://sandbox.itpay.ai";
18
- export const CLI_VERSION = "2.1.34";
19
- export const API_CONTRACT_REVISION = "sha256:80d69dbad36debbe4bdfbe468127a934c87b657e7795f369bb04de54863da476";
15
+ export const CLI_VERSION = "2.1.41";
16
+ export const API_CONTRACT_REVISION = "sha256:b97025ee4e6f7539757d6aedbe193dd65b3a0f84f1111f678abb5e757065f888";
20
17
  const CART_SESSION_DEFAULT_DIR = ".itpay-v3";
21
18
  const CART_SESSION_FILENAME = "cart.json";
22
19
  const OPERATION_JOURNAL_FILENAME = "operations.json";
23
- const TASK_JOURNAL_FILENAME = "tasks.json";
24
20
  export function cliDistribution(env = process.env) {
25
21
  if (env.ITPAY_DISTRIBUTION === "openclaw-skill-bundle")
26
22
  return "openclaw-skill-bundle";
@@ -31,7 +27,7 @@ export function cliDistribution(env = process.env) {
31
27
  export class BackendOverrideError extends Error {
32
28
  code = "backend_override_forbidden";
33
29
  constructor() {
34
- super(`ITPAY_BACKEND_URL only supports ${DEFAULT_BASE_URL}, ${SANDBOX_BASE_URL}, or ${DEV_BASE_URL}`);
30
+ super(`ITPAY_BACKEND_URL only supports ${DEFAULT_BASE_URL} or ${DEV_BASE_URL}`);
35
31
  this.name = "BackendOverrideError";
36
32
  }
37
33
  }
@@ -39,34 +35,26 @@ export function resolveBackendURL(env = process.env) {
39
35
  const requested = env.ITPAY_BACKEND_URL?.trim();
40
36
  if (!requested || requested === DEFAULT_BASE_URL || requested === `${DEFAULT_BASE_URL}/`)
41
37
  return DEFAULT_BASE_URL;
42
- if (requested === SANDBOX_BASE_URL || requested === `${SANDBOX_BASE_URL}/`)
43
- return SANDBOX_BASE_URL;
44
38
  if (requested === DEV_BASE_URL || requested === `${DEV_BASE_URL}/`)
45
39
  return DEV_BASE_URL;
46
- if (env.ITPAY_CLI_DEV === "1" && /^http:\/\/(localhost|127\.0\.0\.1):\d+\/?$/.test(requested))
47
- return requested.replace(/\/$/, "");
48
40
  throw new BackendOverrideError();
49
41
  }
50
42
  export function qualifyBackendCommand(command, env = process.env) {
51
43
  const requested = env.ITPAY_BACKEND_URL?.trim();
52
- if (!requested)
44
+ if (requested !== DEV_BASE_URL && requested !== `${DEV_BASE_URL}/`)
53
45
  return command;
54
- const target = resolveBackendURL(env);
55
- if (target === DEFAULT_BASE_URL)
46
+ if (!command.startsWith("itpay ") || command.startsWith(`ITPAY_BACKEND_URL=${DEV_BASE_URL} `))
56
47
  return command;
57
- if (!command.startsWith("itpay ") || command.startsWith(`ITPAY_BACKEND_URL=${target} `))
58
- return command;
59
- return `ITPAY_BACKEND_URL=${target} ${command}`;
48
+ return `ITPAY_BACKEND_URL=${DEV_BASE_URL} ${command}`;
60
49
  }
61
50
  function stateFilename(filename, baseURL) {
62
- if (baseURL === DEFAULT_BASE_URL)
51
+ if (baseURL !== DEV_BASE_URL)
63
52
  return filename;
64
- const suffix = baseURL === SANDBOX_BASE_URL ? "sandbox" : baseURL === DEV_BASE_URL ? "dev" : `local-${createHash("sha256").update(baseURL).digest("hex").slice(0, 16)}`;
65
53
  const dot = filename.lastIndexOf(".");
66
- return dot < 0 ? `${filename}.${suffix}` : `${filename.slice(0, dot)}.${suffix}${filename.slice(dot)}`;
54
+ return dot < 0 ? `${filename}.dev` : `${filename.slice(0, dot)}.dev${filename.slice(dot)}`;
67
55
  }
68
- export function stateDir(env = process.env) {
69
- return env.ITPAY_STATE_DIR ? resolve(env.ITPAY_STATE_DIR) : resolve(env.HOME || homedir(), CART_SESSION_DEFAULT_DIR);
56
+ function stateDir(env) {
57
+ return resolve(env.HOME || homedir(), CART_SESSION_DEFAULT_DIR);
70
58
  }
71
59
  export function cartSessionPath(env = process.env) {
72
60
  if (env.ITPAY_CART_SESSION_PATH) {
@@ -76,16 +64,6 @@ export function cartSessionPath(env = process.env) {
76
64
  mkdirSync(dir, { recursive: true });
77
65
  return resolve(dir, stateFilename(CART_SESSION_FILENAME, resolveBackendURL(env)));
78
66
  }
79
- // Task journal (AUTH04): paused Service Executions per backend, so a login
80
- // completion can resume the SAME task instead of guessing from the latest order.
81
- export function taskJournalPath(env = process.env) {
82
- if (env.ITPAY_TASK_JOURNAL_PATH) {
83
- return resolve(env.ITPAY_TASK_JOURNAL_PATH);
84
- }
85
- const dir = stateDir(env);
86
- mkdirSync(dir, { recursive: true });
87
- return resolve(dir, stateFilename(TASK_JOURNAL_FILENAME, resolveBackendURL(env)));
88
- }
89
67
  export function loadConfig(env = process.env) {
90
68
  const baseURL = resolveBackendURL(env);
91
69
  const bearerToken = env.ITPAY_BEARER_TOKEN || undefined;
@@ -96,7 +74,7 @@ export function loadConfig(env = process.env) {
96
74
  const ideImageDirOverride = env.ITPAY_IDE_IMAGE_DIR_OVERRIDE;
97
75
  return {
98
76
  baseURL,
99
- environment: baseURL === DEFAULT_BASE_URL ? "production" : "development",
77
+ environment: baseURL === DEV_BASE_URL ? "development" : "production",
100
78
  ...(agentType ? { agentType } : {}),
101
79
  checkoutCurrency,
102
80
  idempotencyKey,
@@ -126,15 +104,7 @@ export function newBackendClient(config) {
126
104
  "X-ItPay-CLI-Version": CLI_VERSION,
127
105
  "X-ItPay-Contract-Revision": API_CONTRACT_REVISION,
128
106
  },
129
- requestAuthorizer: (input) => {
130
- if (/^\/v1\/(seller\/organizations(?:\/|\?|$)|library\/)/.test(input.path)) {
131
- const token = config.bearerToken ?? sellerSessionToken(config.baseURL);
132
- if (!token)
133
- throw new Error("Seller login required; run itpay sell auth login");
134
- return Promise.resolve({ Authorization: `Bearer ${token}` });
135
- }
136
- return authority.authorizationHeaders(input);
137
- },
107
+ requestAuthorizer: (input) => authority.authorizationHeaders(input),
138
108
  recoverAuthorization: () => authority.recoverAuthorization(),
139
109
  });
140
110
  return new BackendClient(http);
@@ -1,7 +1,8 @@
1
1
  import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, randomUUID, sign, } from "node:crypto";
2
2
  import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
3
+ import { homedir } from "node:os";
3
4
  import { dirname, resolve } from "node:path";
4
- const PROTECTED_PATHS = ["/v1/agent-device-account-bindings", "/v1/carts", "/v1/service-executions", "/v1/agent-instances", "/v1/orders", "/v1/refunds", "/v1/me", "/v1/vault", "/v1/rail/phone-links"];
5
+ const PROTECTED_PATHS = ["/v1/carts", "/v1/service-executions", "/v1/agent-instances", "/v1/orders", "/v1/refunds", "/v1/me", "/v1/vault"];
5
6
  export class DeviceAuthority {
6
7
  baseURL;
7
8
  backendKey;
@@ -16,7 +17,7 @@ export class DeviceAuthority {
16
17
  this.backendKey = normalizeBackendKey(options.baseURL);
17
18
  this.requestedAgentType = options.requestedAgentType;
18
19
  this.compatibilityHeaders = options.compatibilityHeaders;
19
- const root = resolve(stateDir(), "device");
20
+ const root = resolve(homedir(), ".itpay-v3", "device");
20
21
  this.statePath = options.statePath ?? resolve(root, "identity.json");
21
22
  this.privateKeyPath = options.privateKeyPath ?? resolve(root, "device-private.pem");
22
23
  this.fetchImpl = (options.fetchImpl ?? globalThis.fetch).bind(globalThis);
@@ -42,24 +43,21 @@ export class DeviceAuthority {
42
43
  }
43
44
  async ensureAuthorization() {
44
45
  if (!this.pending) {
45
- this.pending = this.cachedAuthorization() ?? withFileLock(`${this.statePath}.lock`, () => this.prepareAuthorization());
46
- this.pending = this.pending.finally(() => {
46
+ this.pending = withFileLock(`${this.statePath}.lock`, () => this.prepareAuthorization()).finally(() => {
47
47
  this.pending = undefined;
48
48
  });
49
49
  }
50
50
  return this.pending;
51
51
  }
52
- cachedAuthorization() {
53
- const agentType = this.requestedAgentType;
54
- if (!agentType)
55
- return undefined;
56
- const state = this.readState();
57
- const registration = state?.registrations[this.backendKey];
58
- const session = registration?.sessions[agentType];
59
- if (!registration?.agentInstances[agentType] || !session || Date.parse(session.expiresAt) <= Date.now() + 60_000)
60
- return undefined;
61
- const privateKey = this.readPrivateKey();
62
- return privateKey ? Promise.resolve({ state: registration, agentType, session, privateKey }) : undefined;
52
+ async status() {
53
+ // Readiness needs a current server proof, not an unexpired cached token.
54
+ return withFileLock(`${this.statePath}.lock`, async () => {
55
+ const auth = await this.prepareAuthorization(true);
56
+ if (!auth.session.token || !(Date.parse(auth.session.expiresAt) > Date.now())) {
57
+ throw new Error('Device verification returned an invalid session');
58
+ }
59
+ return { agent_type: auth.agentType, expires_at: auth.session.expiresAt };
60
+ });
63
61
  }
64
62
  async recoverAuthorization() {
65
63
  await withFileLock(`${this.statePath}.lock`, async () => {
@@ -73,9 +71,6 @@ export class DeviceAuthority {
73
71
  this.writeState(state);
74
72
  });
75
73
  }
76
- repairLock() {
77
- return inspectAndRecoverLock(`${this.statePath}.lock`);
78
- }
79
74
  async recoverBackendReset() {
80
75
  return withFileLock(`${this.statePath}.lock`, async () => {
81
76
  const state = this.readState();
@@ -88,28 +83,7 @@ export class DeviceAuthority {
88
83
  return { removed: true, agentTypes };
89
84
  });
90
85
  }
91
- async resetDeviceKey() {
92
- return withFileLock(`${this.statePath}.lock`, async () => {
93
- const state = this.readState();
94
- const removedBackends = state ? Object.keys(state.registrations).sort() : [];
95
- // Delete the key first: if the process dies before the state write, the
96
- // next run treats the missing key as a fresh install and completes the
97
- // reset itself; it can never pair a new key with stale registrations.
98
- if (existsSync(this.privateKeyPath)) {
99
- try {
100
- unlinkSync(this.privateKeyPath);
101
- }
102
- catch (error) {
103
- if (error.code !== "ENOENT") {
104
- throw asDeviceStateError(error, "remove_private_key") ?? error;
105
- }
106
- }
107
- }
108
- this.writeState(emptyDeviceState());
109
- return { removedBackends };
110
- });
111
- }
112
- async prepareAuthorization() {
86
+ async prepareAuthorization(forceSession = false) {
113
87
  let state = this.readState() ?? emptyDeviceState();
114
88
  const agentType = this.requestedAgentType;
115
89
  if (!agentType) {
@@ -119,14 +93,8 @@ export class DeviceAuthority {
119
93
  if (!privateKey) {
120
94
  const pair = generateKeyPairSync("ed25519");
121
95
  privateKey = pair.privateKey;
122
- // Persist an empty state before the new key ever reaches disk so a crash
123
- // can never pair the new key with registrations bound to the old one.
124
- // A crash between the two writes leaves either no key (fresh retry) or a
125
- // key with no registrations (the backend re-attaches to the existing
126
- // device instead of creating a duplicate).
127
- state = emptyDeviceState();
128
- this.writeState(state);
129
96
  this.writePrivateKey(pair.privateKey.export({ format: "pem", type: "pkcs8" }).toString());
97
+ state = emptyDeviceState();
130
98
  }
131
99
  let registration = state.registrations[this.backendKey];
132
100
  if (!registration && state.legacyRegistration) {
@@ -144,21 +112,7 @@ export class DeviceAuthority {
144
112
  registration = await this.enroll(agentType, privateKey);
145
113
  }
146
114
  state.registrations[this.backendKey] = registration;
147
- let session;
148
- try {
149
- session = await this.ensureRegistrationAgentType(registration, agentType, privateKey, false);
150
- }
151
- catch (error) {
152
- if (!isUnknownDeviceRegistration(error))
153
- throw error;
154
- // The backend lost this registration (e.g. its identity store was
155
- // rebuilt). Drop the stale record and enroll once: the same key either
156
- // re-attaches to the surviving device or creates a fresh one.
157
- delete state.registrations[this.backendKey];
158
- registration = await this.enroll(agentType, privateKey);
159
- state.registrations[this.backendKey] = registration;
160
- session = await this.ensureRegistrationAgentType(registration, agentType, privateKey, false);
161
- }
115
+ const session = await this.ensureRegistrationAgentType(registration, agentType, privateKey, forceSession);
162
116
  this.writeState(state);
163
117
  return { state: registration, agentType, session, privateKey };
164
118
  }
@@ -192,23 +146,16 @@ export class DeviceAuthority {
192
146
  if (!publicJWK.x)
193
147
  throw new Error("unable to export Ed25519 public key");
194
148
  const publicKey = Buffer.from(publicJWK.x, "base64url").toString("base64");
195
- try {
196
- const started = await this.publicJSON("/v1/agent-device-enrollments", { public_key: publicKey, agent_type: agentType });
197
- const proof = enrollmentProofMessage(started.agent_device_enrollment_id, started.challenge);
198
- const verified = await this.publicJSON(`/v1/agent-device-enrollments/${encodeURIComponent(started.agent_device_enrollment_id)}/verify`, { challenge: started.challenge, signature: sign(null, Buffer.from(proof), privateKey).toString("base64") });
199
- return {
200
- deviceID: verified.agent_device_id,
201
- deviceKeyID: verified.agent_device_key_id,
202
- quotaLineageID: verified.quota_lineage_id,
203
- agentInstances: { [verified.agent_type]: verified.agent_instance_id },
204
- sessions: {},
205
- };
206
- }
207
- catch (error) {
208
- if (error instanceof DeviceAuthorizationError)
209
- error.enrollmentFailed = true;
210
- throw error;
211
- }
149
+ const started = await this.publicJSON("/v1/agent-device-enrollments", { public_key: publicKey, agent_type: agentType });
150
+ const proof = enrollmentProofMessage(started.agent_device_enrollment_id, started.challenge);
151
+ const verified = await this.publicJSON(`/v1/agent-device-enrollments/${encodeURIComponent(started.agent_device_enrollment_id)}/verify`, { challenge: started.challenge, signature: sign(null, Buffer.from(proof), privateKey).toString("base64") });
152
+ return {
153
+ deviceID: verified.agent_device_id,
154
+ deviceKeyID: verified.agent_device_key_id,
155
+ quotaLineageID: verified.quota_lineage_id,
156
+ agentInstances: { [verified.agent_type]: verified.agent_instance_id },
157
+ sessions: {},
158
+ };
212
159
  }
213
160
  async ensureSession(state, agentType, privateKey, force = false) {
214
161
  const existing = state.sessions[agentType];
@@ -251,7 +198,6 @@ export class DeviceAuthority {
251
198
  method: "POST",
252
199
  headers: { "Content-Type": "application/json", Accept: "application/json", ...this.compatibilityHeaders, ...extraHeaders },
253
200
  body,
254
- signal: AbortSignal.timeout(15_000),
255
201
  });
256
202
  const payload = await response.json().catch(() => ({}));
257
203
  if (!response.ok)
@@ -297,7 +243,6 @@ export class DeviceAuthority {
297
243
  export class DeviceAuthorizationError extends Error {
298
244
  status;
299
245
  code;
300
- enrollmentFailed = false;
301
246
  constructor(status, code, message) {
302
247
  super(message);
303
248
  this.status = status;
@@ -308,32 +253,20 @@ export class DeviceAuthorizationError extends Error {
308
253
  export class DeviceStateError extends Error {
309
254
  operation;
310
255
  causeCode;
311
- blockedPath;
312
256
  code = "device_state_unwritable";
313
- constructor(operation, causeCode, blockedPath) {
257
+ constructor(operation, causeCode) {
314
258
  super(`ItPay device state operation failed: ${operation} (${causeCode})`);
315
259
  this.operation = operation;
316
260
  this.causeCode = causeCode;
317
- this.blockedPath = blockedPath;
318
261
  this.name = "DeviceStateError";
319
262
  }
320
263
  }
321
- export class DeviceLockBusyError extends Error {
322
- code = "device_lock_busy";
323
- constructor() {
324
- super("ItPay device identity is being updated by another process");
325
- this.name = "DeviceLockBusyError";
326
- }
327
- }
328
264
  function emptyDeviceState() {
329
265
  return { schemaVersion: "itpay.device.v2", registrations: {} };
330
266
  }
331
267
  function canMovePastLegacyRegistration(error) {
332
268
  return error instanceof DeviceAuthorizationError && (error.code === "agent_device_revoked" || error.status === 404);
333
269
  }
334
- function isUnknownDeviceRegistration(error) {
335
- return error instanceof DeviceAuthorizationError && (error.code === "agent_device_not_found" || error.status === 404);
336
- }
337
270
  function normalizeBackendKey(value) {
338
271
  const url = new URL(value);
339
272
  url.search = "";
@@ -369,9 +302,9 @@ async function withFileLock(path, run) {
369
302
  catch (error) {
370
303
  throw asDeviceStatePathError(error, "prepare_lock") ?? error;
371
304
  }
372
- const ownerToken = `${process.pid}:${randomUUID()}`;
305
+ const ownerToken = randomUUID();
373
306
  let acquired = false;
374
- for (let attempt = 0; attempt < 800; attempt += 1) {
307
+ for (let attempt = 0; attempt < 200; attempt += 1) {
375
308
  try {
376
309
  writeFileSync(path, ownerToken, { encoding: "utf8", flag: "wx", mode: 0o600 });
377
310
  chmodSync(path, 0o600);
@@ -382,12 +315,20 @@ async function withFileLock(path, run) {
382
315
  const code = error.code;
383
316
  if (code !== "EEXIST")
384
317
  throw asDeviceStateError(error, "acquire_lock") ?? error;
385
- inspectAndRecoverLock(path);
318
+ try {
319
+ if (Date.now() - statSync(path).mtimeMs > 30_000)
320
+ moveLockAside(path, "stale", "remove_stale_lock");
321
+ }
322
+ catch (statError) {
323
+ if (statError.code !== "ENOENT") {
324
+ throw asDeviceStateError(statError, "inspect_lock") ?? statError;
325
+ }
326
+ }
386
327
  await new Promise((resolve) => setTimeout(resolve, 25));
387
328
  }
388
329
  }
389
330
  if (!acquired)
390
- throw new DeviceLockBusyError();
331
+ throw new Error("timed out waiting for ItPay device identity lock");
391
332
  try {
392
333
  return await run();
393
334
  }
@@ -395,46 +336,6 @@ async function withFileLock(path, run) {
395
336
  releaseLock(path, ownerToken);
396
337
  }
397
338
  }
398
- function inspectAndRecoverLock(path) {
399
- let token;
400
- let age;
401
- try {
402
- const lockStat = statSync(path);
403
- age = Date.now() - lockStat.mtimeMs;
404
- token = lockStat.isDirectory() ? "" : readFileSync(path, "utf8");
405
- }
406
- catch (error) {
407
- if (error.code === "ENOENT")
408
- return "absent";
409
- throw asDeviceStateError(error, "inspect_lock") ?? error;
410
- }
411
- const ownerPID = /^(\d+):[0-9a-f-]+$/.exec(token)?.[1];
412
- if (ownerPID) {
413
- try {
414
- process.kill(Number(ownerPID), 0);
415
- return "active";
416
- }
417
- catch (error) {
418
- if (error.code !== "ESRCH")
419
- return "active";
420
- }
421
- }
422
- else if (age <= 30_000) {
423
- return "active";
424
- }
425
- // Recheck the exact owner before moving the lock; another process may have renewed it.
426
- try {
427
- if (token ? readFileSync(path, "utf8") !== token : !statSync(path).isDirectory())
428
- return "active";
429
- }
430
- catch (error) {
431
- if (error.code === "ENOENT")
432
- return "absent";
433
- throw asDeviceStateError(error, "inspect_lock") ?? error;
434
- }
435
- moveLockAside(path, "stale", "remove_stale_lock");
436
- return "recovered";
437
- }
438
339
  function releaseLock(path, ownerToken) {
439
340
  try {
440
341
  if (readFileSync(path, "utf8") !== ownerToken)
@@ -464,11 +365,10 @@ function moveLockAside(path, suffix, operation) {
464
365
  function asDeviceStateError(error, operation) {
465
366
  const code = error.code;
466
367
  return code === "EACCES" || code === "EPERM" || code === "EROFS" || code === "ENOTDIR" || code === "EISDIR"
467
- ? new DeviceStateError(operation, code, error.path)
368
+ ? new DeviceStateError(operation, code)
468
369
  : undefined;
469
370
  }
470
371
  function asDeviceStatePathError(error, operation) {
471
372
  const code = error.code;
472
- return code === "EEXIST" ? new DeviceStateError(operation, code, error.path) : asDeviceStateError(error, operation);
373
+ return code === "EEXIST" ? new DeviceStateError(operation, code) : asDeviceStateError(error, operation);
473
374
  }
474
- import { stateDir } from "./config.js";
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": "itp.agent_doc.v1",
3
3
  "role": "buyer",
4
- "product_scope": "itpay is the single public CLI entry point for Buyer services and the existing itpay sell Seller workflow; $itpay is its user-facing Skill invocation.",
4
+ "product_scope": "itpay is the single public CLI entry point, and $itpay is its user-facing Skill invocation. Under that one product entry point, the two top-level commerce actions are buy and sell: Buyer workflows are available now; Seller workflows will use the same entry point and are not implemented yet.",
5
5
  "topic": "cart-checkout",
6
6
  "title": "Canonical Cart And Checkout Routing",
7
7
  "purpose": "Use the canonical server Cart and route service-backed lines through Service Execution without hardcoded service logic.",
@@ -32,12 +32,11 @@
32
32
  }
33
33
  ],
34
34
  "agent_rules": [
35
- "The Backend Cart is business truth; the current backend-isolated local cart state file only stores local recovery handles and explicit drafts.",
35
+ "The Backend Cart is business truth; ~/.itpay-v3/cart.json only stores local recovery handles and explicit drafts.",
36
36
  "Run the exact next.command returned by the CLI. Do not extract or invent a capability from a raw list.",
37
- "Railway query and booking use distinct Service Executions for one purchase journey. Start another execution for an independent intent.",
38
- "Only non-rail capabilities explicitly requiring delivery email may ask its purpose and value. Railway notification contact belongs to the protected checkout page with verified account email reuse; the Agent must not guess or collect it in chat.",
39
- "A lost Checkout response is retried with the same Cart and persisted idempotency operation.",
40
- "A single railway service checkout normally follows its booking execution; Cart is used only when the returned flow calls for it."
37
+ "One service intent uses one Service Execution. Start another execution for another independent intent.",
38
+ "Ask for email only when the selected capability requires protected delivery; never invent contact data.",
39
+ "A lost Checkout response is retried with the same Cart and persisted idempotency operation."
41
40
  ],
42
41
  "forbidden": [
43
42
  "Do not call ordinary buy for a service-backed Cart line.",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": "itp.agent_doc.v1",
3
3
  "role": "buyer",
4
- "product_scope": "itpay is the single public CLI entry point for Buyer services and the existing itpay sell Seller workflow; $itpay is its user-facing Skill invocation.",
4
+ "product_scope": "itpay is the single public CLI entry point, and $itpay is its user-facing Skill invocation. Under that one product entry point, the two top-level commerce actions are buy and sell: Buyer workflows are available now; Seller workflows will use the same entry point and are not implemented yet.",
5
5
  "topic": "catalog-list",
6
6
  "title": "Browse ItPay V3 Service Catalog",
7
7
  "purpose": "Explain available services, outcomes, conditions, and prices in human language without exposing catalog identifiers.",
@@ -20,8 +20,7 @@
20
20
  "item_format": "Explain the title, what the human receives, any auxiliary discovery step, and the published price.",
21
21
  "service_flow": "Explain discovery as an auxiliary identity-confirmation step, including free quota and paid continuation. Present primary_service as the actual paid outcome. Read all values from the manifest.",
22
22
  "variant_selection": "Internal IDs are for the Agent's next command only and are never shown or requested from the human.",
23
- "price_note": "amount_minor is in minor units (e.g., 10000 = 100.00 CNY). The CLI formats prices as XX.YY currency automatically.",
24
- "railway": "Railway entries point to itpay docs show rail-booking; use its query and booking contracts instead of a generic start→invoke→cart sequence."
23
+ "price_note": "amount_minor is in minor units (e.g., 10000 = 100.00 CNY). The CLI formats prices as XX.YY currency automatically."
25
24
  },
26
25
  "agent_rules": [
27
26
  "Use catalog list only when the human asks for a new service, not when they want an earlier purchase.",
@@ -29,32 +28,15 @@
29
28
  "Explain the customer journey from service_flow instead of presenting every variant as an unrelated service.",
30
29
  "State free quota, paid continuation price, email requirement, and claim purpose exactly as published; do not exaggerate or omit conditions.",
31
30
  "Ask the human to choose only when multiple services could satisfy the goal; never make them choose an internal variant or offer ID.",
32
- "Use each service’s returned input contract; railway planning and booking follow rail-booking, not a universal Cart route."
31
+ "After services start, submit service keywords only through the returned capability command's --input key=value options; --target is never a search keyword."
33
32
  ],
34
33
  "human_language": {
35
- "prefer": [
36
- "服务",
37
- "查询",
38
- "报告",
39
- "免费次数",
40
- "价格",
41
- "你会得到什么"
42
- ],
43
- "avoid": [
44
- "service_id",
45
- "variant_id",
46
- "offer_id",
47
- "capability",
48
- "manifest"
49
- ]
34
+ "prefer": ["服务", "查询", "报告", "免费次数", "价格", "你会得到什么"],
35
+ "avoid": ["service_id", "variant_id", "offer_id", "capability", "manifest"]
50
36
  },
51
37
  "next_docs": [
52
38
  {
53
- "condition": "Railway service selected",
54
- "topic": "rail-booking"
55
- },
56
- {
57
- "condition": "Non-rail Buyer selected a service and wants to purchase",
39
+ "condition": "Buyer selected a service and wants to purchase",
58
40
  "topic": "cart-checkout",
59
41
  "file": "docs/agent/buyer/cart-checkout.json"
60
42
  }