@itpay/cli 2.1.40 → 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.
- package/README.md +5 -12
- package/dist/src/client/backend.js +2 -38
- package/dist/src/client/http.js +0 -2
- package/dist/src/commands/buy.js +58 -17
- package/dist/src/commands/cart.js +15 -17
- package/dist/src/commands/catalog.js +14 -39
- package/dist/src/commands/checkout.js +5 -146
- package/dist/src/commands/checkout_handoff.js +61 -11
- package/dist/src/commands/docs.js +18 -7
- package/dist/src/commands/guidance.js +134 -1
- package/dist/src/commands/install.js +3 -6
- package/dist/src/commands/order.js +3 -34
- package/dist/src/commands/orders.js +9 -4
- package/dist/src/commands/pay.js +1 -1
- package/dist/src/commands/readyz.js +1 -1
- package/dist/src/commands/refund.js +57 -13
- package/dist/src/commands/services.js +1068 -14
- package/dist/src/commands/skill.js +5 -8
- package/dist/src/commands/vault.js +20 -27
- package/dist/src/commands/vault_handoff.js +0 -18
- package/dist/src/main.js +43 -330
- package/dist/src/render/markdown.js +3 -0
- package/dist/src/render/qr.js +1 -1
- package/dist/src/state/cart_session.js +1 -4
- package/dist/src/state/client_context.js +0 -72
- package/dist/src/state/config.js +14 -44
- package/dist/src/state/device_authority.js +41 -141
- package/docs/agent/buyer/cart-checkout.json +6 -8
- package/docs/agent/buyer/catalog-list.json +9 -29
- package/docs/agent/buyer/identity-and-sessions.json +9 -26
- package/docs/agent/buyer/install-and-setup.json +9 -10
- package/docs/agent/buyer/orders-refunds.json +9 -25
- package/docs/agent/buyer/payment-flow.json +4 -6
- package/docs/agent/buyer/purchased-content.json +4 -18
- package/docs/agent/buyer/quickstart.json +17 -26
- package/docs/agent/buyer/render-hosts.json +12 -23
- package/docs/agent/buyer/service-feedback.json +3 -4
- package/docs/cli-reference/agent-types.md +2 -7
- package/docs/cli-reference/commands/buy.md +1 -2
- package/docs/cli-reference/commands/cart/add.md +1 -5
- package/docs/cli-reference/commands/cart/clear.md +1 -1
- package/docs/cli-reference/commands/cart/index.md +1 -1
- package/docs/cli-reference/commands/cart/next.md +1 -5
- package/docs/cli-reference/commands/cart/remove.md +1 -1
- package/docs/cli-reference/commands/cart/show.md +1 -1
- package/docs/cli-reference/commands/catalog/index.md +1 -1
- package/docs/cli-reference/commands/catalog/list.md +2 -2
- package/docs/cli-reference/commands/checkout.md +2 -12
- package/docs/cli-reference/commands/device.md +11 -18
- package/docs/cli-reference/commands/docs/index.md +1 -1
- package/docs/cli-reference/commands/docs/list.md +1 -1
- package/docs/cli-reference/commands/docs/search.md +1 -1
- package/docs/cli-reference/commands/docs/show.md +1 -1
- package/docs/cli-reference/commands/install.md +6 -8
- package/docs/cli-reference/commands/next.md +1 -1
- package/docs/cli-reference/commands/order.md +1 -1
- package/docs/cli-reference/commands/pay.md +1 -1
- package/docs/cli-reference/commands/readyz.md +11 -11
- package/docs/cli-reference/commands/refund/cancel.md +1 -1
- package/docs/cli-reference/commands/refund/create.md +1 -1
- package/docs/cli-reference/commands/refund/get.md +2 -2
- package/docs/cli-reference/commands/refund/index.md +1 -1
- package/docs/cli-reference/commands/refund/list.md +1 -1
- package/docs/cli-reference/commands/refund/watch.md +1 -1
- package/docs/cli-reference/commands/services/action.md +3 -24
- package/docs/cli-reference/commands/services/checkout.md +2 -17
- package/docs/cli-reference/commands/services/events.md +1 -1
- package/docs/cli-reference/commands/services/get.md +2 -4
- package/docs/cli-reference/commands/services/index.md +3 -4
- package/docs/cli-reference/commands/services/invoke.md +4 -6
- package/docs/cli-reference/commands/services/list.md +1 -1
- package/docs/cli-reference/commands/services/next.md +12 -78
- package/docs/cli-reference/commands/services/quote.md +4 -4
- package/docs/cli-reference/commands/services/read-result.md +6 -17
- package/docs/cli-reference/commands/services/start.md +3 -5
- package/docs/cli-reference/commands/vault/access.md +1 -5
- package/docs/cli-reference/conventions.md +6 -4
- package/docs/cli-reference/index.md +1 -10
- package/docs/skill-bundle-rollout/01-mcp-authentication.md +134 -0
- package/docs/skill-bundle-rollout/02-platform-bundle-repositories.md +210 -0
- package/docs/skill-bundle-rollout/03-platform-publishing.md +161 -0
- package/docs/skill-bundle-rollout/04-first-wave-platforms.md +69 -0
- package/docs/skill-bundle-rollout/05-sync-operations.md +62 -0
- package/docs/skill-bundle-rollout/README.md +151 -0
- package/package.json +12 -20
- package/skills/itpay/SKILL.md +102 -110
- package/assets/sell-preview/index.html +0 -6
- package/assets/sell-preview/preview.js +0 -9
- package/bin/darwin-amd64/itpay-sell +0 -0
- package/bin/darwin-arm64/itpay-sell +0 -0
- package/bin/linux-amd64/itpay-sell +0 -0
- package/bin/linux-arm64/itpay-sell +0 -0
- package/bin/windows-amd64/itpay-sell.exe +0 -0
- package/dist/src/commands/aftercare.js +0 -12
- package/dist/src/commands/presentation.js +0 -153
- package/dist/src/commands/rail_catalog.js +0 -1
- package/dist/src/commands/services/actions.js +0 -133
- package/dist/src/commands/services/commerce.js +0 -197
- package/dist/src/commands/services/lifecycle.js +0 -418
- package/dist/src/commands/services/results.js +0 -130
- package/dist/src/render/browser.js +0 -65
- package/dist/src/render/checkout_handoff.js +0 -101
- package/dist/src/render/checkout_plan.js +0 -47
- package/dist/src/sell/commands.js +0 -80
- package/dist/src/sell/contract.js +0 -86
- package/dist/src/sell/local.js +0 -260
- package/dist/src/sell/mcp.js +0 -49
- package/dist/src/sell/preview.js +0 -70
- package/dist/src/sell/sync.js +0 -145
- package/dist/src/services/adapter.js +0 -1
- package/dist/src/services/presentation.js +0 -198
- package/dist/src/services/rail/actions.js +0 -51
- package/dist/src/services/rail/adapter.js +0 -27
- package/dist/src/services/rail/aftercare.js +0 -45
- package/dist/src/services/rail/catalog.js +0 -261
- package/dist/src/services/rail/checkout.js +0 -19
- package/dist/src/services/rail/fulfillment.js +0 -76
- package/dist/src/services/rail/guide.js +0 -110
- package/dist/src/services/rail/invocation.js +0 -54
- package/dist/src/services/rail/page.js +0 -76
- package/dist/src/services/rail/pagination.js +0 -6
- package/dist/src/services/rail/query.js +0 -71
- package/dist/src/services/rail/readers.js +0 -107
- package/dist/src/services/rail/reading.js +0 -125
- package/dist/src/services/rail/recovery.js +0 -51
- package/dist/src/services/rail/results.js +0 -378
- package/dist/src/services/rail/review.js +0 -268
- package/dist/src/services/rail/saved.js +0 -49
- package/dist/src/services/rail/wait.js +0 -11
- package/dist/src/services/registry.js +0 -31
- package/dist/src/services/schema.js +0 -21
- package/dist/src/services/shared/envelope.js +0 -184
- package/dist/src/services/shared/input.js +0 -39
- package/dist/src/services/shared/pagination.js +0 -28
- package/dist/src/services/shared/platform.js +0 -365
- package/dist/src/services/shared/refund.js +0 -40
- package/dist/src/services/shared/workflow.js +0 -28
- package/dist/src/state/account_auth.js +0 -347
- package/dist/src/state/task_journal.js +0 -90
- package/docs/agent/buyer/rail-booking.json +0 -176
- package/docs/agent/buyer/rail-fast-checkout.json +0 -48
- package/docs/cli-reference/commands/aftercare.md +0 -15
- package/docs/cli-reference/commands/auth.md +0 -96
- package/docs/cli-reference/commands/sell.md +0 -1364
- package/docs/cli-reference/commands/services/page.md +0 -71
- package/docs/cli-reference/commands/services/run.md +0 -152
- package/docs/sell.md +0 -84
- /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
|
-
}
|
package/dist/src/state/config.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
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
|
|
18
|
-
export const
|
|
19
|
-
export const API_CONTRACT_REVISION = "sha256:700d9834de969f16d0fc58cbbdbaa092f969f497e30e4ec291c790cea3e7f73a";
|
|
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}
|
|
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 (
|
|
44
|
+
if (requested !== DEV_BASE_URL && requested !== `${DEV_BASE_URL}/`)
|
|
53
45
|
return command;
|
|
54
|
-
|
|
55
|
-
if (target === DEFAULT_BASE_URL)
|
|
46
|
+
if (!command.startsWith("itpay ") || command.startsWith(`ITPAY_BACKEND_URL=${DEV_BASE_URL} `))
|
|
56
47
|
return command;
|
|
57
|
-
|
|
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
|
|
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}
|
|
54
|
+
return dot < 0 ? `${filename}.dev` : `${filename.slice(0, dot)}.dev${filename.slice(dot)}`;
|
|
67
55
|
}
|
|
68
|
-
|
|
69
|
-
return
|
|
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 ===
|
|
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/
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
|
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 =
|
|
305
|
+
const ownerToken = randomUUID();
|
|
373
306
|
let acquired = false;
|
|
374
|
-
for (let attempt = 0; attempt <
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|
|
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
|
-
"
|
|
38
|
-
"
|
|
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.",
|
|
@@ -60,6 +59,5 @@
|
|
|
60
59
|
"service execution",
|
|
61
60
|
"resume",
|
|
62
61
|
"contact"
|
|
63
|
-
]
|
|
64
|
-
"purchase_consent": "Candidate selection is not price approval. Service-backed quote/cart/checkout requires a current scoped approve_purchase record; use the complete server template, default false. Only genuine matching approval allows true; material price/subject/terms changes invalidate it. Restore an existing Checkout before creating anything."
|
|
62
|
+
]
|
|
65
63
|
}
|
|
@@ -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
|
|
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.",
|
|
@@ -18,44 +18,25 @@
|
|
|
18
18
|
],
|
|
19
19
|
"output_interpretation": {
|
|
20
20
|
"item_format": "Explain the title, what the human receives, any auxiliary discovery step, and the published price.",
|
|
21
|
-
"service_flow": "
|
|
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.",
|
|
25
|
-
"service_definition": "Prefer immutable service_definition: one public service with declared operations, pinned schemas and billing modes. Free query does not mean free purchase; dynamic needs a real quote. Do not choose a zero placeholder variant."
|
|
23
|
+
"price_note": "amount_minor is in minor units (e.g., 10000 = 100.00 CNY). The CLI formats prices as XX.YY currency automatically."
|
|
26
24
|
},
|
|
27
25
|
"agent_rules": [
|
|
28
26
|
"Use catalog list only when the human asks for a new service, not when they want an earlier purchase.",
|
|
29
27
|
"Read the variant IDs and prices from the output; do not invent them.",
|
|
30
|
-
"Explain the
|
|
28
|
+
"Explain the customer journey from service_flow instead of presenting every variant as an unrelated service.",
|
|
31
29
|
"State free quota, paid continuation price, email requirement, and claim purpose exactly as published; do not exaggerate or omit conditions.",
|
|
32
30
|
"Ask the human to choose only when multiple services could satisfy the goal; never make them choose an internal variant or offer ID.",
|
|
33
|
-
"
|
|
31
|
+
"After services start, submit service keywords only through the returned capability command's --input key=value options; --target is never a search keyword."
|
|
34
32
|
],
|
|
35
33
|
"human_language": {
|
|
36
|
-
"prefer": [
|
|
37
|
-
|
|
38
|
-
"查询",
|
|
39
|
-
"报告",
|
|
40
|
-
"免费次数",
|
|
41
|
-
"价格",
|
|
42
|
-
"你会得到什么"
|
|
43
|
-
],
|
|
44
|
-
"avoid": [
|
|
45
|
-
"service_id",
|
|
46
|
-
"variant_id",
|
|
47
|
-
"offer_id",
|
|
48
|
-
"capability",
|
|
49
|
-
"manifest"
|
|
50
|
-
]
|
|
34
|
+
"prefer": ["服务", "查询", "报告", "免费次数", "价格", "你会得到什么"],
|
|
35
|
+
"avoid": ["service_id", "variant_id", "offer_id", "capability", "manifest"]
|
|
51
36
|
},
|
|
52
37
|
"next_docs": [
|
|
53
38
|
{
|
|
54
|
-
"condition": "
|
|
55
|
-
"topic": "rail-booking"
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
"condition": "Non-rail Buyer selected a service and wants to purchase",
|
|
39
|
+
"condition": "Buyer selected a service and wants to purchase",
|
|
59
40
|
"topic": "cart-checkout",
|
|
60
41
|
"file": "docs/agent/buyer/cart-checkout.json"
|
|
61
42
|
}
|
|
@@ -69,6 +50,5 @@
|
|
|
69
50
|
"what to buy",
|
|
70
51
|
"有哪些",
|
|
71
52
|
"服务列表"
|
|
72
|
-
]
|
|
73
|
-
"purchase_consent": "Candidate selection is not price approval. Service-backed quote/cart/checkout requires a current scoped approve_purchase record; use the complete server template, default false. Only genuine matching approval allows true; material price/subject/terms changes invalidate it. Restore an existing Checkout before creating anything."
|
|
53
|
+
]
|
|
74
54
|
}
|