@itpay/cli 2.1.41 → 2.1.44
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 +12 -5
- package/assets/sell-preview/index.html +6 -0
- package/assets/sell-preview/preview.js +9 -0
- 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/client/backend.js +38 -2
- package/dist/src/client/http.js +2 -0
- package/dist/src/commands/aftercare.js +12 -0
- package/dist/src/commands/buy.js +17 -58
- package/dist/src/commands/cart.js +17 -15
- package/dist/src/commands/catalog.js +39 -14
- package/dist/src/commands/checkout.js +146 -5
- package/dist/src/commands/checkout_handoff.js +11 -61
- package/dist/src/commands/docs.js +7 -18
- package/dist/src/commands/guidance.js +1 -134
- package/dist/src/commands/install.js +6 -3
- package/dist/src/commands/order.js +34 -3
- package/dist/src/commands/orders.js +4 -9
- package/dist/src/commands/pay.js +1 -1
- package/dist/src/commands/presentation.js +153 -0
- package/dist/src/commands/rail_catalog.js +1 -0
- package/dist/src/commands/readyz.js +1 -1
- package/dist/src/commands/refund.js +13 -57
- package/dist/src/commands/services/actions.js +133 -0
- package/dist/src/commands/services/commerce.js +197 -0
- package/dist/src/commands/services/lifecycle.js +418 -0
- package/dist/src/commands/services/results.js +130 -0
- package/dist/src/commands/services.js +14 -1068
- package/dist/src/commands/skill.js +8 -5
- package/dist/src/commands/vault.js +27 -20
- package/dist/src/commands/vault_handoff.js +18 -0
- package/dist/src/main.js +332 -46
- package/dist/src/render/browser.js +65 -0
- package/dist/src/render/checkout_handoff.js +101 -0
- package/dist/src/render/checkout_plan.js +47 -0
- package/dist/src/render/markdown.js +0 -3
- package/dist/src/render/qr.js +1 -1
- package/dist/src/sell/commands.js +80 -0
- package/dist/src/sell/contract.js +86 -0
- package/dist/src/sell/local.js +260 -0
- package/dist/src/sell/mcp.js +49 -0
- package/dist/src/sell/preview.js +70 -0
- package/dist/src/sell/sync.js +145 -0
- package/dist/src/services/adapter.js +1 -0
- package/dist/src/services/presentation.js +198 -0
- package/dist/src/services/rail/actions.js +51 -0
- package/dist/src/services/rail/adapter.js +36 -0
- package/dist/src/services/rail/aftercare.js +45 -0
- package/dist/src/services/rail/catalog.js +261 -0
- package/dist/src/services/rail/checkout.js +19 -0
- package/dist/src/services/rail/fulfillment.js +76 -0
- package/dist/src/services/rail/guide.js +110 -0
- package/dist/src/services/rail/invocation.js +54 -0
- package/dist/src/services/rail/page.js +76 -0
- package/dist/src/services/rail/pagination.js +6 -0
- package/dist/src/services/rail/query.js +71 -0
- package/dist/src/services/rail/readers.js +107 -0
- package/dist/src/services/rail/reading.js +125 -0
- package/dist/src/services/rail/recovery.js +51 -0
- package/dist/src/services/rail/results.js +378 -0
- package/dist/src/services/rail/review.js +268 -0
- package/dist/src/services/rail/saved.js +49 -0
- package/dist/src/services/rail/wait.js +11 -0
- package/dist/src/services/registry.js +31 -0
- package/dist/src/services/schema.js +21 -0
- package/dist/src/services/shared/envelope.js +184 -0
- package/dist/src/services/shared/input.js +39 -0
- package/dist/src/services/shared/pagination.js +28 -0
- package/dist/src/services/shared/platform.js +365 -0
- package/dist/src/services/shared/refund.js +40 -0
- package/dist/src/services/shared/workflow.js +28 -0
- package/dist/src/state/account_auth.js +354 -0
- package/dist/src/state/cart_session.js +4 -1
- package/dist/src/state/client_context.js +72 -0
- package/dist/src/state/config.js +44 -14
- package/dist/src/state/device_authority.js +402 -72
- package/dist/src/state/task_journal.js +90 -0
- package/docs/agent/buyer/cart-checkout.json +8 -6
- package/docs/agent/buyer/catalog-list.json +29 -9
- package/docs/agent/buyer/identity-and-sessions.json +26 -9
- package/docs/agent/buyer/install-and-setup.json +17 -15
- package/docs/agent/buyer/orders-refunds.json +25 -9
- package/docs/agent/buyer/payment-flow.json +6 -4
- package/docs/agent/buyer/purchased-content.json +18 -4
- package/docs/agent/buyer/quickstart.json +26 -17
- package/docs/agent/buyer/rail-booking.json +176 -0
- package/docs/agent/buyer/rail-fast-checkout.json +48 -0
- package/docs/agent/buyer/render-hosts.json +23 -12
- package/docs/agent/buyer/service-feedback.json +4 -3
- package/docs/cli-reference/agent-types.md +7 -2
- package/docs/cli-reference/commands/aftercare.md +15 -0
- package/docs/cli-reference/commands/auth.md +96 -0
- package/docs/cli-reference/commands/buy.md +2 -1
- package/docs/cli-reference/commands/cart/add.md +5 -1
- 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 +5 -1
- 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 +12 -2
- package/docs/cli-reference/commands/device.md +18 -11
- 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 +8 -6
- 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/sell.md +1364 -0
- package/docs/cli-reference/commands/services/action.md +24 -3
- package/docs/cli-reference/commands/services/checkout.md +17 -2
- package/docs/cli-reference/commands/services/events.md +1 -1
- package/docs/cli-reference/commands/services/get.md +4 -2
- package/docs/cli-reference/commands/services/index.md +4 -3
- package/docs/cli-reference/commands/services/invoke.md +6 -4
- package/docs/cli-reference/commands/services/list.md +1 -1
- package/docs/cli-reference/commands/services/next.md +78 -12
- package/docs/cli-reference/commands/services/page.md +71 -0
- package/docs/cli-reference/commands/services/quote.md +4 -4
- package/docs/cli-reference/commands/services/read-result.md +17 -6
- package/docs/cli-reference/commands/services/run.md +152 -0
- package/docs/cli-reference/commands/services/start.md +5 -3
- package/docs/cli-reference/commands/vault/access.md +5 -1
- package/docs/cli-reference/conventions.md +4 -6
- package/docs/cli-reference/index.md +10 -1
- package/docs/sell.md +84 -0
- package/package.json +20 -12
- package/skills/itpay/SKILL.md +112 -102
- package/docs/skill-bundle-rollout/01-mcp-authentication.md +0 -134
- package/docs/skill-bundle-rollout/02-platform-bundle-repositories.md +0 -210
- package/docs/skill-bundle-rollout/03-platform-publishing.md +0 -161
- package/docs/skill-bundle-rollout/04-first-wave-platforms.md +0 -69
- package/docs/skill-bundle-rollout/05-sync-operations.md +0 -62
- package/docs/skill-bundle-rollout/README.md +0 -151
- /package/bin/{itp → itp.mjs} +0 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { HttpError } from '../client/http.js';
|
|
2
|
+
import { HttpTransportError } from '../client/transport.js';
|
|
3
|
+
import { DeviceAuthorizationError, DeviceStateError, DeviceLockBusyError } from './device_authority.js';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { dirname, resolve } from 'node:path';
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, rmSync } from 'node:fs';
|
|
7
|
+
import { writeLocalPNG } from '../render/qr.js';
|
|
8
|
+
import { TaskJournal } from './task_journal.js';
|
|
9
|
+
import { taskJournalPath } from './config.js';
|
|
10
|
+
export function sellerAuthPath(baseURL, env = process.env, purpose = "seller") {
|
|
11
|
+
return resolve(stateDir(env), `${purpose}-${createHash('sha256').update(baseURL).digest('hex').slice(0, 16)}.json`);
|
|
12
|
+
}
|
|
13
|
+
function read(baseURL, env = process.env, purpose = "seller") {
|
|
14
|
+
const path = sellerAuthPath(baseURL, env, purpose);
|
|
15
|
+
if (!existsSync(path))
|
|
16
|
+
return;
|
|
17
|
+
const state = JSON.parse(readFileSync(path, 'utf8'));
|
|
18
|
+
if (state.baseURL !== baseURL)
|
|
19
|
+
throw new Error('Seller session backend mismatch');
|
|
20
|
+
return state;
|
|
21
|
+
}
|
|
22
|
+
function save(state, env = process.env, purpose = "seller") {
|
|
23
|
+
const path = sellerAuthPath(state.baseURL, env, purpose);
|
|
24
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
25
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
26
|
+
writeFileSync(tmp, JSON.stringify(state), { mode: 0o600 });
|
|
27
|
+
renameSync(tmp, path);
|
|
28
|
+
}
|
|
29
|
+
export function sellerSessionToken(baseURL, env = process.env) {
|
|
30
|
+
const state = read(baseURL, env);
|
|
31
|
+
return state?.expiresAt && Date.parse(state.expiresAt) > Date.now() ? state.sessionToken : undefined;
|
|
32
|
+
}
|
|
33
|
+
export function sellerAuth(action, baseURL, env = process.env, fetcher = fetch) {
|
|
34
|
+
return accountAuth(action, baseURL, env, fetcher);
|
|
35
|
+
}
|
|
36
|
+
export function agentAuth(action, baseURL, backend, env = process.env, fetcher = fetch) {
|
|
37
|
+
return accountAuth(action, baseURL, env, fetcher, backend);
|
|
38
|
+
}
|
|
39
|
+
// AUTH01: dashboard session stages stay distinct — the human must know exactly
|
|
40
|
+
// what the official page still needs instead of a generic "pending".
|
|
41
|
+
const ACTIVE_SESSION_STAGES = ['created', 'waiting_provider', 'email_verification_required', 'merge_confirmation_required'];
|
|
42
|
+
function stageInstruction(stage) {
|
|
43
|
+
if (stage === 'email_verification_required') {
|
|
44
|
+
return '官方页返回显式或历史身份验证状态;按页面当前要求完成验证。普通登录只需一种有效身份,不要求补齐邮箱和手机。完成后恢复原执行。';
|
|
45
|
+
}
|
|
46
|
+
if (stage === 'merge_confirmation_required') {
|
|
47
|
+
return '官方页需要用户核对两侧账号证明及主账号,再明确确认或取消合并;这不是付款确认。不要替用户同意,不自动重启原服务或授权请求。';
|
|
48
|
+
}
|
|
49
|
+
return '用户仍在官方页面完成登录;保留当前 handoff,不要生成新二维码或新请求。';
|
|
50
|
+
}
|
|
51
|
+
// AUTH07: distinct outcomes stay distinct — a technical failure is never
|
|
52
|
+
// reported as a user denial, and a missing session is its own state.
|
|
53
|
+
const TERMINAL_SESSION_STATES = {
|
|
54
|
+
expired: {
|
|
55
|
+
status: 'auth_expired',
|
|
56
|
+
instruction: '授权请求已过期。重新运行 itpay auth login 生成新请求;之前的查询输入在服务端保留,可恢复。',
|
|
57
|
+
},
|
|
58
|
+
failed: {
|
|
59
|
+
status: 'auth_failed',
|
|
60
|
+
instruction: '官方授权因技术原因未完成(不是用户拒绝)。重新运行 itpay auth login 生成新请求。',
|
|
61
|
+
},
|
|
62
|
+
cancelled: {
|
|
63
|
+
status: 'auth_cancelled',
|
|
64
|
+
instruction: '用户在官方页取消了授权;停止登录。只有用户明确重新要求授权时才发起新的登录。',
|
|
65
|
+
},
|
|
66
|
+
denied: {
|
|
67
|
+
status: 'auth_denied',
|
|
68
|
+
instruction: '官方授权被拒绝;停止登录,不自动创建新请求。只有用户明确重新要求授权时才重新登录。',
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
class AuthRequestError extends Error {
|
|
72
|
+
httpStatus;
|
|
73
|
+
constructor(httpStatus, message) {
|
|
74
|
+
super(message);
|
|
75
|
+
this.httpStatus = httpStatus;
|
|
76
|
+
this.name = 'AuthRequestError';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// AUTH03: transport/read uncertainty never maps to "denied" or a new session —
|
|
80
|
+
// the stored session stays on disk and the caller retries the same poll.
|
|
81
|
+
function authStatusUnknown(sessionID, error, failureStage = "authorization_status_read") {
|
|
82
|
+
// These errors have definite compatibility/identity/state facts. Let the
|
|
83
|
+
// command boundary report them rather than turning them into polling.
|
|
84
|
+
if ((error instanceof HttpError && error.status < 500)
|
|
85
|
+
|| (error instanceof AuthRequestError && error.httpStatus < 500)
|
|
86
|
+
|| error instanceof DeviceAuthorizationError || error instanceof DeviceStateError
|
|
87
|
+
|| error instanceof DeviceLockBusyError)
|
|
88
|
+
throw error;
|
|
89
|
+
const hostFailure = error && typeof error === "object" && "code" in error
|
|
90
|
+
&& (error.code === "CODEBUDDY_BROKER_DENY" || error.code === "CODEBUDDY_BROKER_OUTCOME_UNKNOWN");
|
|
91
|
+
const failure = error instanceof HttpTransportError
|
|
92
|
+
? { category: error.code, attempts: error.attempts, source: "network_transport" }
|
|
93
|
+
: error instanceof HttpError || error instanceof AuthRequestError
|
|
94
|
+
? { category: "http_server_error", http_status: error instanceof HttpError ? error.status : error.httpStatus, source: "http_response" }
|
|
95
|
+
: hostFailure ? { category: "host_operation_unavailable", source: "host_broker" }
|
|
96
|
+
: { category: "authorization_status_unavailable", source: "unclassified" };
|
|
97
|
+
return {
|
|
98
|
+
status: 'auth_status_unknown',
|
|
99
|
+
result: { ...failure, failure_stage: failureStage, ...(sessionID ? { dashboard_auth_session_id: sessionID } : {}) },
|
|
100
|
+
instruction: hostFailure || failure.source === 'unclassified'
|
|
101
|
+
? '本轮授权检查未完成,底层原因尚未确定;保留设备和已有授权,请维护方核对当前失败阶段及宿主结果,不自动登录、换身份或盲目重复修改。'
|
|
102
|
+
: sessionID
|
|
103
|
+
? '读取官方授权状态失败;原授权请求已保留。稍后重读同一状态,不要新建授权、不要清除本地登记。'
|
|
104
|
+
: '本轮账号绑定检查未完成,设备与绑定状态尚未确认;没有保存的授权请求。结束本轮,网络或服务恢复后可重新检查,不发起登录。',
|
|
105
|
+
next: sessionID && !hostFailure && failure.source !== 'unclassified' ? { command: 'itpay auth status --json', reason: '稍后重读同一授权状态', poll_after_ms: 5000 } : null,
|
|
106
|
+
recovery: [],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function sessionGone(env, purpose, baseURL, sessionID) {
|
|
110
|
+
rmSync(sellerAuthPath(baseURL, env, purpose), { force: true });
|
|
111
|
+
return {
|
|
112
|
+
status: 'auth_session_missing',
|
|
113
|
+
result: sessionID ? { dashboard_auth_session_id: sessionID } : {},
|
|
114
|
+
instruction: '之前的授权请求在服务端已不存在。重新运行 itpay auth login 生成新请求。',
|
|
115
|
+
next: { command: 'itpay auth login --json', reason: '重新发起官方授权' },
|
|
116
|
+
recovery: [],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// AUTH04: a successful bind resumes the SAME task that paused for login —
|
|
120
|
+
// from the local task journal, never a placeholder and never a guess at the
|
|
121
|
+
// latest order.
|
|
122
|
+
function authenticatedEnvelope(env, phoneVerified) {
|
|
123
|
+
const paused = new TaskJournal(taskJournalPath(env)).pausedTasks()
|
|
124
|
+
.filter((task) => task.stage === 'login_required' || task.stage === 'quota_paused');
|
|
125
|
+
const resumable = paused.filter((task) => typeof task.resume_command === 'string');
|
|
126
|
+
const result = { bound: true, phone_verified: phoneVerified };
|
|
127
|
+
let instruction = '登录与绑定已完成,告知用户可继续之前的查询;已注册账号查询不消耗试用次数,仍受正常限流。';
|
|
128
|
+
let next = null;
|
|
129
|
+
if (resumable.length === 1) {
|
|
130
|
+
next = { command: resumable[0].resume_command, reason: '恢复登录前暂停的同一查询' };
|
|
131
|
+
}
|
|
132
|
+
else if (resumable.length > 1) {
|
|
133
|
+
result.paused_executions = resumable.map((task) => ({
|
|
134
|
+
service_execution_id: task.service_execution_id,
|
|
135
|
+
...(task.service_id ? { service_id: task.service_id } : {}),
|
|
136
|
+
resume_command: task.resume_command,
|
|
137
|
+
}));
|
|
138
|
+
instruction += ' 有多个暂停的查询:只恢复与当前用户目标对应的那一个,不要一次恢复全部。';
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
instruction += ' 当前没有待恢复的暂停查询;不构造恢复命令。';
|
|
142
|
+
}
|
|
143
|
+
return { status: 'authenticated', result, instruction, next, recovery: [] };
|
|
144
|
+
}
|
|
145
|
+
async function accountAuth(action, baseURL, env, fetcher, backend) {
|
|
146
|
+
const purpose = backend ? 'agent-login' : 'seller';
|
|
147
|
+
const command = backend ? 'itpay auth status' : 'itpay sell auth status';
|
|
148
|
+
if (backend) {
|
|
149
|
+
try {
|
|
150
|
+
const current = await backend.agentAccountStatus();
|
|
151
|
+
if (current.status === 'authenticated')
|
|
152
|
+
return authenticatedEnvelope(env, current.phone_verified === true);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (error instanceof AuthRequestError)
|
|
156
|
+
throw error;
|
|
157
|
+
// AUTH03: even the binding-status probe can hit transport failure; the
|
|
158
|
+
// truthful state is "unknown", never an implicit unbound or a fresh login.
|
|
159
|
+
return authStatusUnknown(read(baseURL, env, purpose)?.sessionID, error, "account_binding_probe");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function request(path, init = {}) {
|
|
163
|
+
const response = await fetcher(baseURL + path, { ...init, redirect: 'error', signal: AbortSignal.timeout(15000) });
|
|
164
|
+
if (!response.ok)
|
|
165
|
+
throw new AuthRequestError(response.status, `ItPay authorization failed (${response.status}); retry login if expired`);
|
|
166
|
+
return response;
|
|
167
|
+
}
|
|
168
|
+
function pollOpenSession(state) {
|
|
169
|
+
return request(`/v1/dashboard/auth-sessions/${encodeURIComponent(state.sessionID)}?poll_token=${encodeURIComponent(state.pollToken)}`)
|
|
170
|
+
.then((response) => response.json());
|
|
171
|
+
}
|
|
172
|
+
async function bindSavedSession(state) {
|
|
173
|
+
const bound = await backend.bindAgentAccount({ dashboard_auth_session_id: state.sessionID, start_token: state.startToken });
|
|
174
|
+
if (bound.status !== 'authenticated')
|
|
175
|
+
throw new Error('Agent binding did not complete');
|
|
176
|
+
rmSync(sellerAuthPath(baseURL, env, purpose), { force: true });
|
|
177
|
+
return authenticatedEnvelope(env, bound.phone_verified === true);
|
|
178
|
+
}
|
|
179
|
+
if (action === 'login') {
|
|
180
|
+
if (backend) {
|
|
181
|
+
const open = read(baseURL, env, purpose);
|
|
182
|
+
if (open?.sessionID && open.pollToken && open.startToken) {
|
|
183
|
+
try {
|
|
184
|
+
const current = await pollOpenSession(open);
|
|
185
|
+
// AUTH02: a completed-but-unbound session binds in place; never
|
|
186
|
+
// create a second auth session for the same login.
|
|
187
|
+
if (current.status === 'completed')
|
|
188
|
+
return await bindSavedSession(open);
|
|
189
|
+
if (ACTIVE_SESSION_STAGES.includes(current.status)) {
|
|
190
|
+
const url = open.authURL ?? '';
|
|
191
|
+
const qr = url ? await writeLocalPNG(url).catch(() => undefined) : undefined;
|
|
192
|
+
return {
|
|
193
|
+
status: 'auth_pending',
|
|
194
|
+
result: {
|
|
195
|
+
dashboard_auth_session_id: open.sessionID,
|
|
196
|
+
stage: current.status,
|
|
197
|
+
expires_at: current.expires_at,
|
|
198
|
+
methods: ['phone', 'email', 'alipay', 'wechat'],
|
|
199
|
+
},
|
|
200
|
+
// SEC01: the handoff URL carries one-time credentials — it goes
|
|
201
|
+
// to the human only, never into logs, prompts, or PR material.
|
|
202
|
+
handoff: {
|
|
203
|
+
url,
|
|
204
|
+
contains_credentials: true,
|
|
205
|
+
...(qr ? { qr_local_path: qr.filePath, markdown: `` } : {}),
|
|
206
|
+
},
|
|
207
|
+
instruction: `${stageInstruction(current.status)} 沿用同一链接或二维码;不要生成新请求。`,
|
|
208
|
+
next: { command: 'itpay auth status --json', reason: '用户完成页面登录后确认绑定', poll_after_ms: 5000 },
|
|
209
|
+
recovery: [],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const terminal = TERMINAL_SESSION_STATES[current.status];
|
|
213
|
+
if (terminal) {
|
|
214
|
+
rmSync(sellerAuthPath(baseURL, env, purpose), { force: true });
|
|
215
|
+
// Fall through to a fresh session only after a definitive
|
|
216
|
+
// terminal state; anything else keeps the stored session.
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
return authStatusUnknown(open.sessionID);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
if (error instanceof AuthRequestError && (error.httpStatus === 404 || error.httpStatus === 410)) {
|
|
224
|
+
rmSync(sellerAuthPath(baseURL, env, purpose), { force: true });
|
|
225
|
+
// The explicit login call may open one fresh session now. Status
|
|
226
|
+
// remains read-only and still reports the missing old session.
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
return authStatusUnknown(open.sessionID, error);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const response = await request('/v1/dashboard/auth-sessions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ return_to: backend ? '/' : '/seller' }) });
|
|
235
|
+
const result = await response.json();
|
|
236
|
+
const url = new URL(result.start_url, baseURL);
|
|
237
|
+
const sameOrigin = url.origin === new URL(baseURL).origin;
|
|
238
|
+
const alipay = url.origin === 'https://openauth.alipay.com' && url.pathname === '/oauth2/publicAppAuthorize.htm' && !url.username && !url.password;
|
|
239
|
+
const local = response.headers.get('X-ItPay-Environment') === 'local_simulation'
|
|
240
|
+
&& new URL(baseURL).protocol === 'http:' && new URL(baseURL).hostname === '127.0.0.1'
|
|
241
|
+
&& url.protocol === 'http:' && url.hostname === '127.0.0.1' && !url.username && !url.password;
|
|
242
|
+
if (!sameOrigin && !alipay && !local)
|
|
243
|
+
throw new Error('Unexpected authorization origin');
|
|
244
|
+
const fragment = new URLSearchParams(url.hash.replace(/^#dashboard-auth\?/, ''));
|
|
245
|
+
const state = url.searchParams.get('state')?.split('.');
|
|
246
|
+
const startToken = alipay
|
|
247
|
+
? (state?.length === 2 && state[0] === result.dashboard_auth_session_id ? state[1] : undefined)
|
|
248
|
+
: url.searchParams.get('start_token') || fragment.get('start_token');
|
|
249
|
+
if (!startToken || !result.poll_token || !result.dashboard_auth_session_id)
|
|
250
|
+
throw new Error('Incomplete authorization response');
|
|
251
|
+
save({ baseURL, sessionID: result.dashboard_auth_session_id, pollToken: result.poll_token, startToken, authURL: url.href }, env, purpose);
|
|
252
|
+
if (backend) {
|
|
253
|
+
const qr = await writeLocalPNG(url.href).catch(() => undefined);
|
|
254
|
+
return {
|
|
255
|
+
status: 'auth_pending',
|
|
256
|
+
result: {
|
|
257
|
+
dashboard_auth_session_id: result.dashboard_auth_session_id,
|
|
258
|
+
stage: 'created',
|
|
259
|
+
expires_at: result.expires_at,
|
|
260
|
+
methods: ['phone', 'email', 'alipay', 'wechat'],
|
|
261
|
+
},
|
|
262
|
+
handoff: {
|
|
263
|
+
url: url.href,
|
|
264
|
+
contains_credentials: true,
|
|
265
|
+
...(qr ? { qr_local_path: qr.filePath, markdown: `` } : {}),
|
|
266
|
+
},
|
|
267
|
+
instruction: '把官方授权页或二维码交给用户。用户在页面内选择手机号验证码、邮箱或钱包完成登录;不要替用户输入手机号或验证码,start_token 不要写入聊天记录或日志。',
|
|
268
|
+
next: { command: 'itpay auth status --json', reason: '用户完成页面登录后确认绑定', poll_after_ms: 5000 },
|
|
269
|
+
recovery: [],
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
return { status: 'authorization_required', authorization_url: url.href, instruction: `Complete ItPay login in the browser, then run ${command}.` };
|
|
273
|
+
}
|
|
274
|
+
const state = read(baseURL, env, purpose);
|
|
275
|
+
if (action === 'logout') {
|
|
276
|
+
const token = sellerSessionToken(baseURL, env);
|
|
277
|
+
if (token)
|
|
278
|
+
await request('/v1/me/logout', { method: 'POST', headers: { Authorization: `Bearer ${token}` } });
|
|
279
|
+
rmSync(sellerAuthPath(baseURL, env), { force: true });
|
|
280
|
+
return { status: 'logged_out' };
|
|
281
|
+
}
|
|
282
|
+
if (!backend && sellerSessionToken(baseURL, env))
|
|
283
|
+
return { status: 'authenticated', base_url: baseURL, expires_at: state?.expiresAt };
|
|
284
|
+
if (!state?.sessionID || !state.pollToken || !state.startToken) {
|
|
285
|
+
// AUTH07: a bare `status` with no session is its own truthful state, not a
|
|
286
|
+
// failure and not an implicit login.
|
|
287
|
+
if (!backend)
|
|
288
|
+
return { status: 'login_required' };
|
|
289
|
+
return {
|
|
290
|
+
status: 'login_required',
|
|
291
|
+
result: {},
|
|
292
|
+
instruction: '当前设备尚未绑定账号,也没有进行中的授权请求。需要时运行 itpay auth login 发起官方登录。',
|
|
293
|
+
next: { command: 'itpay auth login --json', reason: '需要账号绑定时发起官方授权' },
|
|
294
|
+
recovery: [],
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
let progress;
|
|
298
|
+
try {
|
|
299
|
+
progress = await pollOpenSession(state);
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
if (error instanceof AuthRequestError && (error.httpStatus === 404 || error.httpStatus === 410)) {
|
|
303
|
+
return sessionGone(env, purpose, baseURL, state.sessionID);
|
|
304
|
+
}
|
|
305
|
+
return authStatusUnknown(state.sessionID, error);
|
|
306
|
+
}
|
|
307
|
+
if (progress.status !== 'completed') {
|
|
308
|
+
if (backend) {
|
|
309
|
+
const terminal = TERMINAL_SESSION_STATES[progress.status];
|
|
310
|
+
if (terminal) {
|
|
311
|
+
rmSync(sellerAuthPath(baseURL, env, purpose), { force: true });
|
|
312
|
+
return {
|
|
313
|
+
status: terminal.status,
|
|
314
|
+
result: { dashboard_auth_session_id: state.sessionID, stage: progress.status },
|
|
315
|
+
instruction: terminal.instruction,
|
|
316
|
+
next: ['denied', 'cancelled'].includes(progress.status) ? null : { command: 'itpay auth login --json', reason: '需要登录时重新发起官方授权' },
|
|
317
|
+
recovery: [],
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
status: 'auth_pending',
|
|
322
|
+
result: {
|
|
323
|
+
dashboard_auth_session_id: state.sessionID,
|
|
324
|
+
stage: progress.status,
|
|
325
|
+
...(progress.expires_at ? { expires_at: progress.expires_at } : {}),
|
|
326
|
+
},
|
|
327
|
+
instruction: stageInstruction(progress.status),
|
|
328
|
+
next: { command: 'itpay auth status --json', reason: '轮询同一授权请求', poll_after_ms: 5000 },
|
|
329
|
+
recovery: [],
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
return { status: progress.status, instruction: 'Finish login and email verification in the browser.' };
|
|
333
|
+
}
|
|
334
|
+
if (backend) {
|
|
335
|
+
try {
|
|
336
|
+
return await bindSavedSession(state);
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
if (error instanceof AuthRequestError)
|
|
340
|
+
throw error;
|
|
341
|
+
// Binding outcome unknown: keep the session so a retry can still bind
|
|
342
|
+
// the same completed login instead of creating a new one.
|
|
343
|
+
return authStatusUnknown(state.sessionID, error);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const claimed = await request(`/v1/dashboard/auth-sessions/${encodeURIComponent(state.sessionID)}/claim?start_token=${encodeURIComponent(state.startToken)}`, { method: 'POST' });
|
|
347
|
+
const token = /(?:^|[, ]+)itpay_buyer_session=([^;]+)/.exec(claimed.headers.get('set-cookie') ?? '')?.[1];
|
|
348
|
+
const session = await claimed.json();
|
|
349
|
+
if (!token || !session.expires_at || !(Date.parse(session.expires_at) > Date.now()))
|
|
350
|
+
throw new Error('Incomplete Seller session');
|
|
351
|
+
save({ baseURL, sessionToken: token, expiresAt: session.expires_at }, env);
|
|
352
|
+
return { status: 'authenticated', base_url: baseURL, expires_at: session.expires_at };
|
|
353
|
+
}
|
|
354
|
+
import { stateDir } from "./config.js";
|
|
@@ -55,9 +55,12 @@ export class CartSession {
|
|
|
55
55
|
...(this.state.lastDisplayToken ? { lastDisplayToken: this.state.lastDisplayToken } : {}),
|
|
56
56
|
...(this.state.lastCheckoutURL ? { lastCheckoutURL: this.state.lastCheckoutURL } : {}),
|
|
57
57
|
};
|
|
58
|
+
const contents = JSON.stringify(toSave, null, 2);
|
|
59
|
+
if (existsSync(path) && readFileSync(path, "utf-8") === contents)
|
|
60
|
+
return;
|
|
58
61
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
59
62
|
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
60
|
-
writeFileSync(temporary,
|
|
63
|
+
writeFileSync(temporary, contents, { encoding: "utf-8", mode: 0o600 });
|
|
61
64
|
chmodSync(temporary, 0o600);
|
|
62
65
|
renameSync(temporary, path);
|
|
63
66
|
chmodSync(path, 0o600);
|
|
@@ -64,6 +64,8 @@ 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";
|
|
67
69
|
if (normalized === "openclaw")
|
|
68
70
|
return undefined;
|
|
69
71
|
return "terminal";
|
|
@@ -77,3 +79,73 @@ export function validateContext(host, target) {
|
|
|
77
79
|
}
|
|
78
80
|
return undefined;
|
|
79
81
|
}
|
|
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,7 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
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
|
|
3
4
|
// display-token persistence belongs to the cart session file, protected with
|
|
4
5
|
// owner-only permissions. Provider secrets are explicitly out of scope here.
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
5
7
|
import { homedir } from "node:os";
|
|
6
8
|
import { mkdirSync } from "node:fs";
|
|
7
9
|
import { resolve } from "node:path";
|
|
@@ -12,11 +14,13 @@ import { DeviceAuthority } from "./device_authority.js";
|
|
|
12
14
|
import { OperationJournal } from "./operation_journal.js";
|
|
13
15
|
export const DEFAULT_BASE_URL = "https://app.itpay.ai";
|
|
14
16
|
export const DEV_BASE_URL = "https://dev.itpay.ai";
|
|
15
|
-
export const
|
|
16
|
-
export const
|
|
17
|
+
export const SANDBOX_BASE_URL = "https://sandbox.itpay.ai";
|
|
18
|
+
export const CLI_VERSION = "2.1.44";
|
|
19
|
+
export const API_CONTRACT_REVISION = "sha256:c1e1f459c1d3844f6eeae21535b532abb2e48c3e320173c4faccd62992890dcd";
|
|
17
20
|
const CART_SESSION_DEFAULT_DIR = ".itpay-v3";
|
|
18
21
|
const CART_SESSION_FILENAME = "cart.json";
|
|
19
22
|
const OPERATION_JOURNAL_FILENAME = "operations.json";
|
|
23
|
+
const TASK_JOURNAL_FILENAME = "tasks.json";
|
|
20
24
|
export function cliDistribution(env = process.env) {
|
|
21
25
|
if (env.ITPAY_DISTRIBUTION === "openclaw-skill-bundle")
|
|
22
26
|
return "openclaw-skill-bundle";
|
|
@@ -27,7 +31,7 @@ export function cliDistribution(env = process.env) {
|
|
|
27
31
|
export class BackendOverrideError extends Error {
|
|
28
32
|
code = "backend_override_forbidden";
|
|
29
33
|
constructor() {
|
|
30
|
-
super(`ITPAY_BACKEND_URL only supports ${DEFAULT_BASE_URL} or ${DEV_BASE_URL}`);
|
|
34
|
+
super(`ITPAY_BACKEND_URL only supports ${DEFAULT_BASE_URL}, ${SANDBOX_BASE_URL}, or ${DEV_BASE_URL}`);
|
|
31
35
|
this.name = "BackendOverrideError";
|
|
32
36
|
}
|
|
33
37
|
}
|
|
@@ -35,26 +39,34 @@ export function resolveBackendURL(env = process.env) {
|
|
|
35
39
|
const requested = env.ITPAY_BACKEND_URL?.trim();
|
|
36
40
|
if (!requested || requested === DEFAULT_BASE_URL || requested === `${DEFAULT_BASE_URL}/`)
|
|
37
41
|
return DEFAULT_BASE_URL;
|
|
42
|
+
if (requested === SANDBOX_BASE_URL || requested === `${SANDBOX_BASE_URL}/`)
|
|
43
|
+
return SANDBOX_BASE_URL;
|
|
38
44
|
if (requested === DEV_BASE_URL || requested === `${DEV_BASE_URL}/`)
|
|
39
45
|
return DEV_BASE_URL;
|
|
46
|
+
if (env.ITPAY_CLI_DEV === "1" && /^http:\/\/(localhost|127\.0\.0\.1):\d+\/?$/.test(requested))
|
|
47
|
+
return requested.replace(/\/$/, "");
|
|
40
48
|
throw new BackendOverrideError();
|
|
41
49
|
}
|
|
42
50
|
export function qualifyBackendCommand(command, env = process.env) {
|
|
43
51
|
const requested = env.ITPAY_BACKEND_URL?.trim();
|
|
44
|
-
if (requested
|
|
52
|
+
if (!requested)
|
|
45
53
|
return command;
|
|
46
|
-
|
|
54
|
+
const target = resolveBackendURL(env);
|
|
55
|
+
if (target === DEFAULT_BASE_URL)
|
|
47
56
|
return command;
|
|
48
|
-
|
|
57
|
+
if (!command.startsWith("itpay ") || command.startsWith(`ITPAY_BACKEND_URL=${target} `))
|
|
58
|
+
return command;
|
|
59
|
+
return `ITPAY_BACKEND_URL=${target} ${command}`;
|
|
49
60
|
}
|
|
50
61
|
function stateFilename(filename, baseURL) {
|
|
51
|
-
if (baseURL
|
|
62
|
+
if (baseURL === DEFAULT_BASE_URL)
|
|
52
63
|
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)}`;
|
|
53
65
|
const dot = filename.lastIndexOf(".");
|
|
54
|
-
return dot < 0 ? `${filename}
|
|
66
|
+
return dot < 0 ? `${filename}.${suffix}` : `${filename.slice(0, dot)}.${suffix}${filename.slice(dot)}`;
|
|
55
67
|
}
|
|
56
|
-
function stateDir(env) {
|
|
57
|
-
return resolve(env.HOME || homedir(), CART_SESSION_DEFAULT_DIR);
|
|
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);
|
|
58
70
|
}
|
|
59
71
|
export function cartSessionPath(env = process.env) {
|
|
60
72
|
if (env.ITPAY_CART_SESSION_PATH) {
|
|
@@ -64,6 +76,16 @@ export function cartSessionPath(env = process.env) {
|
|
|
64
76
|
mkdirSync(dir, { recursive: true });
|
|
65
77
|
return resolve(dir, stateFilename(CART_SESSION_FILENAME, resolveBackendURL(env)));
|
|
66
78
|
}
|
|
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
|
+
}
|
|
67
89
|
export function loadConfig(env = process.env) {
|
|
68
90
|
const baseURL = resolveBackendURL(env);
|
|
69
91
|
const bearerToken = env.ITPAY_BEARER_TOKEN || undefined;
|
|
@@ -74,7 +96,7 @@ export function loadConfig(env = process.env) {
|
|
|
74
96
|
const ideImageDirOverride = env.ITPAY_IDE_IMAGE_DIR_OVERRIDE;
|
|
75
97
|
return {
|
|
76
98
|
baseURL,
|
|
77
|
-
environment: baseURL ===
|
|
99
|
+
environment: baseURL === DEFAULT_BASE_URL ? "production" : "development",
|
|
78
100
|
...(agentType ? { agentType } : {}),
|
|
79
101
|
checkoutCurrency,
|
|
80
102
|
idempotencyKey,
|
|
@@ -104,7 +126,15 @@ export function newBackendClient(config) {
|
|
|
104
126
|
"X-ItPay-CLI-Version": CLI_VERSION,
|
|
105
127
|
"X-ItPay-Contract-Revision": API_CONTRACT_REVISION,
|
|
106
128
|
},
|
|
107
|
-
requestAuthorizer: (input) =>
|
|
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
|
+
},
|
|
108
138
|
recoverAuthorization: () => authority.recoverAuthorization(),
|
|
109
139
|
});
|
|
110
140
|
return new BackendClient(http);
|