@itpay/cli 2.1.63 → 2.1.65

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/dist/src/main.js CHANGED
@@ -3064,268 +3064,804 @@ var require_commander = __commonJS({
3064
3064
  }
3065
3065
  });
3066
3066
 
3067
- // src/commands/admin.ts
3068
- import { readFileSync as readFileSync3 } from "node:fs";
3067
+ // src/state/hub_credentials.ts
3068
+ import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
3069
+ import { chmodSync as chmodSync2, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
3070
+ import { dirname as dirname2, resolve } from "node:path";
3069
3071
 
3070
- // node_modules/@itpay/hub-client/dist/index.js
3071
- var HUB_CONTRACT_REVISION = "hub.v1.2026-10-26.integration1";
3072
- var isRunReceipt = /* @__PURE__ */ __name((r) => "run_id" in r, "isRunReceipt");
3073
- var HubApiError = class extends Error {
3074
- static {
3075
- __name(this, "HubApiError");
3076
- }
3077
- status;
3078
- envelope;
3079
- constructor(status, envelope) {
3080
- super(`${status} ${envelope.error.code}: ${envelope.error.message}`);
3081
- this.status = status;
3082
- this.envelope = envelope;
3083
- this.name = "HubApiError";
3084
- }
3085
- get code() {
3086
- return this.envelope.error.code;
3087
- }
3088
- };
3089
- var HubClient = class {
3090
- static {
3091
- __name(this, "HubClient");
3092
- }
3093
- baseUrl;
3094
- token;
3095
- resource;
3096
- apiKeyHeader;
3097
- fetchImpl;
3098
- budgets;
3099
- constructor(opts) {
3100
- this.resource = opts.resource;
3101
- this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
3102
- this.token = opts.token;
3103
- this.apiKeyHeader = opts.apiKeyHeader ?? "Authorization";
3104
- this.fetchImpl = opts.fetchImpl ?? fetch;
3105
- this.budgets = { metadata: 5e3, status: 5e3, action: 15e3, ...opts.requestBudgetsMs };
3106
- for (const value of Object.values(this.budgets))
3107
- if (!Number.isFinite(value) || value <= 0)
3108
- throw new Error("request budget must be finite and positive");
3109
- }
3110
- async request(method, path2, opts = {}) {
3111
- const url = new URL(this.baseUrl + path2);
3112
- for (const [k, v] of Object.entries(opts.query ?? {})) {
3113
- if (v !== void 0)
3114
- url.searchParams.set(k, String(v));
3072
+ // src/state/file_lock.ts
3073
+ import { chmodSync, lstatSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
3074
+ import { dirname } from "node:path";
3075
+ import { randomUUID } from "node:crypto";
3076
+ var STALE_MS = 3e4;
3077
+ function sleepSync(ms) {
3078
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
3079
+ }
3080
+ __name(sleepSync, "sleepSync");
3081
+ function ownerAlive(path2) {
3082
+ try {
3083
+ const st = lstatSync(path2);
3084
+ if (st.isSymbolicLink()) return false;
3085
+ if (Date.now() - st.mtimeMs < STALE_MS) return true;
3086
+ const token = readFileSync(path2, "utf8");
3087
+ const pid = Number(/^(\d+):/.exec(token)?.[1]);
3088
+ if (!Number.isInteger(pid) || pid <= 0) return false;
3089
+ try {
3090
+ process.kill(pid, 0);
3091
+ return true;
3092
+ } catch {
3093
+ return false;
3115
3094
  }
3116
- const headers = { Accept: "application/json", "X-ItPay-Hub-Contract": HUB_CONTRACT_REVISION };
3117
- if (this.resource)
3118
- headers["X-ItPay-OAuth-Resource"] = this.resource;
3119
- if (this.token)
3120
- headers[this.apiKeyHeader] = this.apiKeyHeader === "Authorization" ? `Bearer ${this.token}` : this.token;
3121
- if (opts.body !== void 0)
3122
- headers["Content-Type"] = "application/json";
3123
- if (opts.idempotencyKey)
3124
- headers["Idempotency-Key"] = opts.idempotencyKey;
3125
- if (opts.payerCurrency)
3126
- headers["ItPay-Payment-Currency"] = opts.payerCurrency;
3127
- const init = { method, headers };
3128
- if (opts.body !== void 0)
3129
- init.body = JSON.stringify(opts.body);
3130
- const stage = path2.startsWith("/v1/runs/") && method === "GET" ? "status" : method === "GET" || path2 === "/v1/discover" ? "metadata" : "action";
3131
- const timeoutMs = Math.min(opts.timeoutMs ?? this.budgets[stage], this.budgets[stage]);
3132
- if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
3133
- throw new Error("request budget exhausted");
3134
- const controller = new AbortController();
3135
- let timer;
3136
- const aborted = new Promise((_, reject) => {
3137
- controller.signal.addEventListener("abort", () => reject(new HubApiError(0, { error: { code: opts.signal?.aborted ? "request_cancelled" : "request_timeout", message: method === "POST" && path2 !== "/v1/discover" ? "Request outcome is unknown; read the original request/run before exact-key recovery. No automatic write retry." : `Hub ${stage} read timed out after ${timeoutMs} ms; no spending-budget conclusion. Keep the original reference and read it later.`, details: { stage, method, path: path2 } } })), { once: true });
3138
- timer = setTimeout(() => controller.abort(), timeoutMs);
3139
- });
3140
- const cancel = /* @__PURE__ */ __name(() => controller.abort(), "cancel");
3141
- opts.signal?.addEventListener("abort", cancel, { once: true });
3142
- if (opts.signal?.aborted)
3143
- controller.abort();
3095
+ } catch {
3096
+ return false;
3097
+ }
3098
+ }
3099
+ __name(ownerAlive, "ownerAlive");
3100
+ function withFileLock(lockPath, fn) {
3101
+ mkdirSync(dirname(lockPath), { recursive: true, mode: 448 });
3102
+ const token = `${process.pid}:${randomUUID()}`;
3103
+ const deadline = Date.now() + 15e3;
3104
+ for (; ; ) {
3144
3105
  try {
3145
- return await Promise.race([(async () => {
3146
- const res = await this.fetchImpl(url, { ...init, signal: controller.signal });
3147
- const serverRevision = res.headers.get("X-ItPay-Hub-Contract");
3148
- if (serverRevision && serverRevision !== HUB_CONTRACT_REVISION)
3149
- throw new HubApiError(409, { error: { code: "contract_mismatch", message: "Hub/SDK contract mismatch; install the paired CLI/MCP candidate before executing", details: { expected: HUB_CONTRACT_REVISION, received: serverRevision } } });
3150
- const text2 = await res.text();
3151
- const payload = text2 ? JSON.parse(text2) : void 0;
3152
- if (!res.ok) {
3153
- const p = payload;
3154
- if (opts.intentResponse && p && typeof p.intent_id === "string") {
3155
- return payload;
3156
- }
3157
- const raw = payload;
3158
- const envelope = raw && raw.error && typeof raw.error.code === "string" ? raw : { error: { code: "internal", message: `HTTP ${res.status}` } };
3159
- throw new HubApiError(res.status, envelope);
3106
+ writeFileSync(lockPath, token, { encoding: "utf8", flag: "wx", mode: 384 });
3107
+ chmodSync(lockPath, 384);
3108
+ break;
3109
+ } catch (error) {
3110
+ const code = error.code;
3111
+ if (code !== "EEXIST") throw error;
3112
+ if (!ownerAlive(lockPath)) {
3113
+ try {
3114
+ unlinkSync(lockPath);
3115
+ } catch {
3160
3116
  }
3161
- return payload;
3162
- })(), aborted]);
3163
- } finally {
3164
- clearTimeout(timer);
3165
- opts.signal?.removeEventListener("abort", cancel);
3117
+ continue;
3118
+ }
3119
+ if (Date.now() > deadline) throw new Error(`task store lock busy: ${lockPath}`);
3120
+ sleepSync(20);
3166
3121
  }
3167
3122
  }
3168
- listCommerceTransactions(cursor) {
3169
- return this.request("GET", "/v1/me/transactions" + (cursor ? `?cursor=${encodeURIComponent(cursor)}` : ""));
3170
- }
3171
- getPurchaseCollectionOptions(transactionId) {
3172
- return this.request("GET", `/v1/me/transactions/${encodeURIComponent(transactionId)}/collection-options`);
3173
- }
3174
- getPurchaseCollection(transactionId) {
3175
- return this.request("GET", `/v1/me/transactions/${encodeURIComponent(transactionId)}/collection`);
3176
- }
3177
- requestPurchaseCollection(transactionId, body) {
3178
- return this.request("POST", `/v1/me/transactions/${encodeURIComponent(transactionId)}/collection`, { body });
3179
- }
3180
- refreshPurchaseCollection(transactionId) {
3181
- return this.request("POST", `/v1/me/transactions/${encodeURIComponent(transactionId)}/collection/refresh`, { body: {} });
3182
- }
3183
- getSupplementCollectionOptions(caseId) {
3184
- return this.request("GET", `/v1/me/after-sales/${encodeURIComponent(caseId)}/collection-options`);
3185
- }
3186
- getSupplementCollection(caseId) {
3187
- return this.request("GET", `/v1/me/after-sales/${encodeURIComponent(caseId)}/collection`);
3188
- }
3189
- requestSupplementCollection(caseId, body) {
3190
- return this.request("POST", `/v1/me/after-sales/${encodeURIComponent(caseId)}/collection`, { body });
3191
- }
3192
- refreshSupplementCollection(caseId) {
3193
- return this.request("POST", `/v1/me/after-sales/${encodeURIComponent(caseId)}/collection/refresh`, { body: {} });
3194
- }
3195
- getCommerceTransaction(id) {
3196
- return this.request("GET", `/v1/me/transactions/${encodeURIComponent(id)}`);
3197
- }
3198
- confirmCommerceAfterSales(id, body, idempotencyKey) {
3199
- return this.request("POST", `/v1/me/transactions/${encodeURIComponent(id)}/after-sales`, { body, idempotencyKey });
3200
- }
3201
- getCommerceAfterSales(id) {
3202
- return this.request("GET", `/v1/me/after-sales/${encodeURIComponent(id)}`);
3203
- }
3204
- getCommerceRefund(id) {
3205
- return this.request("GET", `/v1/me/refunds/${encodeURIComponent(id)}`);
3206
- }
3207
- getCommerceAfterSalesQuoteOptions(transactionId, cursor) {
3208
- return this.request("GET", `/v1/me/transactions/${encodeURIComponent(transactionId)}/quote-options${cursor ? `?cursor=${encodeURIComponent(cursor)}` : ""}`);
3209
- }
3210
- prepareCommerceAfterSalesQuote(transactionId, body, idempotencyKey) {
3211
- return this.request("POST", `/v1/me/transactions/${encodeURIComponent(transactionId)}/quote-requests`, { body, idempotencyKey });
3212
- }
3213
- getCommerceQuoteCredentialRequirements(requestId) {
3214
- return this.request("GET", `/v1/me/quote-requests/${encodeURIComponent(requestId)}/credential-requirements`);
3215
- }
3216
- getCommerceCaseCredentialRequirements(caseId) {
3217
- return this.request("GET", `/v1/me/after-sales/${encodeURIComponent(caseId)}/credential-requirements`);
3218
- }
3219
- getCommerceRefundCredentialRequirements(refundId, mode) {
3220
- return this.request("GET", `/v1/me/refunds/${encodeURIComponent(refundId)}/credential-requirements/${mode}`);
3221
- }
3222
- startCommerceAfterSalesQuote(requestId, body) {
3223
- return this.request("POST", `/v1/me/quote-requests/${encodeURIComponent(requestId)}/execution`, { body });
3224
- }
3225
- getCommerceAfterSalesQuoteRequest(requestId) {
3226
- return this.request("GET", `/v1/me/quote-requests/${encodeURIComponent(requestId)}`);
3227
- }
3228
- getCommerceAfterSalesExecution(caseId) {
3229
- return this.request("GET", `/v1/me/after-sales/${encodeURIComponent(caseId)}/execution`);
3230
- }
3231
- startCommerceAfterSalesExecution(caseId, body) {
3232
- return this.request("POST", `/v1/me/after-sales/${encodeURIComponent(caseId)}/execution`, { body });
3233
- }
3234
- getCommerceRefundExecutionAuthorization(refundId) {
3235
- return this.request("GET", `/v1/me/refunds/${encodeURIComponent(refundId)}/execution-authorization`);
3236
- }
3237
- authorizeCommerceRefundExecution(refundId, body) {
3238
- return this.request("POST", `/v1/me/refunds/${encodeURIComponent(refundId)}/execution-authorization`, { body });
3123
+ try {
3124
+ return fn();
3125
+ } finally {
3126
+ try {
3127
+ if (readFileSync(lockPath, "utf8") === token) unlinkSync(lockPath);
3128
+ } catch {
3129
+ }
3239
3130
  }
3240
- requestCommerceRefund(caseId, body, idempotencyKey) {
3241
- return this.request("POST", `/v1/me/after-sales/${encodeURIComponent(caseId)}/refunds`, { body, idempotencyKey });
3131
+ }
3132
+ __name(withFileLock, "withFileLock");
3133
+ async function withAsyncFileLock(lockPath, fn) {
3134
+ mkdirSync(dirname(lockPath), { recursive: true, mode: 448 });
3135
+ const token = `${process.pid}:${randomUUID()}`;
3136
+ const deadline = Date.now() + 2e4;
3137
+ for (; ; ) {
3138
+ try {
3139
+ writeFileSync(lockPath, token, { encoding: "utf8", flag: "wx", mode: 384 });
3140
+ break;
3141
+ } catch (error) {
3142
+ if (error.code !== "EEXIST") throw error;
3143
+ if (!ownerAlive(lockPath)) {
3144
+ try {
3145
+ unlinkSync(lockPath);
3146
+ } catch {
3147
+ }
3148
+ continue;
3149
+ }
3150
+ if (Date.now() > deadline) throw new Error("credential refresh lock busy");
3151
+ await new Promise((resolve10) => setTimeout(resolve10, 20));
3152
+ }
3242
3153
  }
3243
- requestSupplementRefund(caseId, body, idempotencyKey) {
3244
- return this.request("POST", `/v1/me/after-sales/${encodeURIComponent(caseId)}/supplement-refunds`, { body, idempotencyKey });
3154
+ try {
3155
+ return await fn();
3156
+ } finally {
3157
+ try {
3158
+ if (readFileSync(lockPath, "utf8") === token) unlinkSync(lockPath);
3159
+ } catch {
3160
+ }
3245
3161
  }
3246
- createAPIIngestion(body, idempotencyKey) {
3247
- return this.request("POST", "/v1/me/api-ingestions", { body, idempotencyKey });
3248
- }
3249
- listAPIIngestions() {
3250
- return this.request("GET", "/v1/me/api-ingestions");
3251
- }
3252
- getAPIIngestion(jobId) {
3253
- return this.request("GET", `/v1/me/api-ingestions/${encodeURIComponent(jobId)}`);
3162
+ }
3163
+ __name(withAsyncFileLock, "withAsyncFileLock");
3164
+
3165
+ // src/state/hub_oauth.ts
3166
+ import { createHash, randomBytes } from "node:crypto";
3167
+ import { createServer } from "node:http";
3168
+ function hubOAuthConfig(env = process.env) {
3169
+ const devHub = (env.ITPAY_HUB_URL ?? "").replace(/\/+$/, "") === "https://market-v4-dev.itpay.ai";
3170
+ const issuer = (env.ITPAY_HUB_OAUTH_ISSUER ?? (devHub ? "https://auth-v4-dev.itpay.ai" : "http://localhost:24444")).replace(/\/+$/, "");
3171
+ return {
3172
+ authorizeURL: `${issuer}/oauth2/auth`,
3173
+ tokenURL: `${issuer}/oauth2/token`,
3174
+ clientID: env.ITPAY_HUB_OAUTH_CLIENT ?? "itpay-cli-local",
3175
+ resource: (env.ITPAY_HUB_URL ?? "http://localhost:18090").replace(/\/+$/, ""),
3176
+ scope: env.ITPAY_HUB_OAUTH_SCOPE ?? "openid offline_access itpay.hub"
3177
+ };
3178
+ }
3179
+ __name(hubOAuthConfig, "hubOAuthConfig");
3180
+ function pkce() {
3181
+ const verifier = randomBytes(32).toString("base64url");
3182
+ const challenge = createHash("sha256").update(verifier).digest("base64url");
3183
+ return { verifier, challenge };
3184
+ }
3185
+ __name(pkce, "pkce");
3186
+ async function runConnectFlow(cfg, timeoutMs = 3e5) {
3187
+ const { verifier, challenge } = pkce();
3188
+ const state = randomBytes(16).toString("hex");
3189
+ const server = createServer();
3190
+ const done = new Promise((resolvePromise, reject) => {
3191
+ const timer = setTimeout(() => {
3192
+ server.close();
3193
+ reject(new Error("connect timed out waiting for browser authorization"));
3194
+ }, timeoutMs);
3195
+ server.on("request", (req, res) => {
3196
+ const u = new URL(req.url ?? "/", "http://127.0.0.1");
3197
+ const finish = /* @__PURE__ */ __name((ok, msg) => {
3198
+ res.writeHead(ok ? 200 : 400, { "Content-Type": "text/html; charset=utf-8" });
3199
+ res.end(`<!doctype html><meta charset="utf-8"><title>ItPay</title><body style="font-family:system-ui;padding:3em;text-align:center"><h2>${msg.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;")}</h2><p>\u53EF\u4EE5\u5173\u95ED\u6B64\u9875\u9762\uFF0C\u56DE\u5230\u7EC8\u7AEF\u7EE7\u7EED\u3002</p></body>`);
3200
+ }, "finish");
3201
+ (async () => {
3202
+ if (u.pathname !== "/callback") return finish(false, "\u65E0\u6548\u7684\u56DE\u8C03");
3203
+ if (u.searchParams.get("state") !== state) return finish(false, "state \u4E0D\u5339\u914D");
3204
+ if (u.searchParams.get("error")) {
3205
+ clearTimeout(timer);
3206
+ server.close();
3207
+ finish(false, "\u6388\u6743\u88AB\u62D2\u7EDD");
3208
+ reject(new Error("authorization_required: consent denied"));
3209
+ return;
3210
+ }
3211
+ const code = u.searchParams.get("code");
3212
+ if (!code) {
3213
+ clearTimeout(timer);
3214
+ server.close();
3215
+ finish(false, "\u7F3A\u5C11 code");
3216
+ reject(new Error("missing code"));
3217
+ return;
3218
+ }
3219
+ const form = new URLSearchParams({
3220
+ grant_type: "authorization_code",
3221
+ code,
3222
+ redirect_uri: `http://127.0.0.1:${server.address().port}/callback`,
3223
+ client_id: cfg.clientID,
3224
+ code_verifier: verifier,
3225
+ resource: cfg.resource
3226
+ });
3227
+ const tok = await fetch(cfg.tokenURL, {
3228
+ method: "POST",
3229
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
3230
+ body: form,
3231
+ signal: AbortSignal.timeout(1e4)
3232
+ });
3233
+ if (!tok.ok) {
3234
+ clearTimeout(timer);
3235
+ server.close();
3236
+ finish(false, "token \u4EA4\u6362\u5931\u8D25");
3237
+ reject(new Error(`token endpoint ${tok.status}`));
3238
+ return;
3239
+ }
3240
+ const t = await tok.json();
3241
+ clearTimeout(timer);
3242
+ server.close();
3243
+ finish(true, "ItPay \u5DF2\u8FDE\u63A5");
3244
+ resolvePromise({ accessToken: t.access_token, refreshToken: t.refresh_token, expiresIn: t.expires_in });
3245
+ })().catch((e) => {
3246
+ clearTimeout(timer);
3247
+ server.close();
3248
+ reject(e);
3249
+ });
3250
+ });
3251
+ });
3252
+ await new Promise((resolvePromise, reject) => {
3253
+ server.once("error", reject);
3254
+ server.listen(0, "127.0.0.1", () => resolvePromise());
3255
+ });
3256
+ const port = server.address().port;
3257
+ const authorizeURL = `${cfg.authorizeURL}?` + new URLSearchParams({
3258
+ response_type: "code",
3259
+ client_id: cfg.clientID,
3260
+ redirect_uri: `http://127.0.0.1:${port}/callback`,
3261
+ scope: cfg.scope,
3262
+ state,
3263
+ code_challenge: challenge,
3264
+ code_challenge_method: "S256",
3265
+ resource: cfg.resource
3266
+ }).toString();
3267
+ return { authorizeURL, done };
3268
+ }
3269
+ __name(runConnectFlow, "runConnectFlow");
3270
+
3271
+ // src/state/hub_credentials.ts
3272
+ import { homedir } from "node:os";
3273
+ function hubCredentialsPath(env = process.env) {
3274
+ const dir = env.ITPAY_STATE_DIR ? resolve(env.ITPAY_STATE_DIR) : resolve(env.HOME || homedir(), ".itpay-v3");
3275
+ const suffix = createHash2("sha256").update(env.ITPAY_HUB_URL ?? "http://localhost:18090").digest("hex").slice(0, 12);
3276
+ return resolve(dir, `hub-credentials.${suffix}.json`);
3277
+ }
3278
+ __name(hubCredentialsPath, "hubCredentialsPath");
3279
+ function importHubKeyUnlocked(apiKey, baseURL, env = process.env) {
3280
+ if (!/^itk_[A-Za-z0-9_-]{8,}$/.test(apiKey)) {
3281
+ throw new Error("invalid key format \u2014 expected an itk_ API key");
3254
3282
  }
3255
- retryAPIIngestion(jobId) {
3256
- return this.request("POST", `/v1/me/api-ingestions/${encodeURIComponent(jobId)}/retry`, { body: {} });
3283
+ const path2 = hubCredentialsPath(env);
3284
+ const file = existsSync(path2) ? JSON.parse(readFileSync2(path2, "utf8")) : { schema_version: "itpay.hub_credentials.v1", profiles: {} };
3285
+ file.profiles["default"] = {
3286
+ base_url: baseURL,
3287
+ key_prefix: apiKey.slice(0, 14),
3288
+ api_key: apiKey,
3289
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
3290
+ };
3291
+ saveHubProfileUnlocked(file.profiles["default"], env);
3292
+ return { key_prefix: apiKey.slice(0, 14) };
3293
+ }
3294
+ __name(importHubKeyUnlocked, "importHubKeyUnlocked");
3295
+ function storedHubKey(env = process.env) {
3296
+ const path2 = hubCredentialsPath(env);
3297
+ if (!existsSync(path2)) return void 0;
3298
+ try {
3299
+ const file = JSON.parse(readFileSync2(path2, "utf8"));
3300
+ return file.profiles["default"]?.api_key;
3301
+ } catch {
3302
+ return void 0;
3257
3303
  }
3258
- discover(req = {}) {
3259
- return this.request("POST", "/v1/discover", { body: req });
3304
+ }
3305
+ __name(storedHubKey, "storedHubKey");
3306
+ function storedHubKeyPrefix(env = process.env) {
3307
+ const path2 = hubCredentialsPath(env);
3308
+ if (!existsSync(path2)) return void 0;
3309
+ try {
3310
+ const file = JSON.parse(readFileSync2(path2, "utf8"));
3311
+ return file.profiles["default"]?.key_prefix;
3312
+ } catch {
3313
+ return void 0;
3260
3314
  }
3261
- inspect(operationId) {
3262
- return this.request("GET", `/v1/operations/${encodeURIComponent(operationId)}`);
3315
+ }
3316
+ __name(storedHubKeyPrefix, "storedHubKeyPrefix");
3317
+ function storeHubSessionUnlocked(accessToken, refreshToken, expiresIn, subject, baseURL, env = process.env, consumerID) {
3318
+ const path2 = hubCredentialsPath(env);
3319
+ const file = existsSync(path2) ? JSON.parse(readFileSync2(path2, "utf8")) : { schema_version: "itpay.hub_credentials.v1", profiles: {} };
3320
+ const issuedAt = Date.now();
3321
+ const margin = Math.min(3e4, Math.max(100, expiresIn * 100));
3322
+ file.profiles["default"] = {
3323
+ base_url: baseURL,
3324
+ access_token: accessToken,
3325
+ ...refreshToken ? { refresh_token: refreshToken } : {},
3326
+ expires_at: new Date(issuedAt + expiresIn * 1e3).toISOString(),
3327
+ refresh_after: new Date(issuedAt + expiresIn * 1e3 - margin).toISOString(),
3328
+ ...subject ? { subject } : {},
3329
+ consumer_id: consumerID ?? `oauth:${hubOAuthConfig(env).clientID}`,
3330
+ client_id: hubOAuthConfig(env).clientID,
3331
+ token_url: hubOAuthConfig(env).tokenURL,
3332
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
3333
+ };
3334
+ saveHubProfileUnlocked(file.profiles["default"], env);
3335
+ }
3336
+ __name(storeHubSessionUnlocked, "storeHubSessionUnlocked");
3337
+ function saveHubProfileUnlocked(profile, env) {
3338
+ const path2 = hubCredentialsPath(env);
3339
+ const tmp = `${path2}.${randomUUID2()}.tmp`;
3340
+ try {
3341
+ const file = existsSync(path2) ? JSON.parse(readFileSync2(path2, "utf8")) : { schema_version: "itpay.hub_credentials.v1", profiles: {} };
3342
+ file.profiles.default = profile;
3343
+ mkdirSync2(dirname2(path2), { recursive: true, mode: 448 });
3344
+ writeFileSync2(tmp, JSON.stringify(file, null, 2), { encoding: "utf8", flag: "wx", mode: 384 });
3345
+ chmodSync2(tmp, 384);
3346
+ renameSync(tmp, path2);
3347
+ } catch {
3348
+ throw new Error("credential_persistence_failed: could not save the original connection; stop and check private storage before retrying");
3349
+ } finally {
3350
+ try {
3351
+ unlinkSync2(tmp);
3352
+ } catch {
3353
+ }
3263
3354
  }
3264
- /** The single run-creation entry. Always sends expected_publication_id. */
3265
- run(operationId, admission, idempotencyKey, payment = {}) {
3266
- return this.request("POST", `/v1/operations/${encodeURIComponent(operationId)}/runs`, {
3267
- body: admission,
3268
- idempotencyKey,
3269
- payerCurrency: payment.payerCurrency,
3270
- intentResponse: true
3355
+ }
3356
+ __name(saveHubProfileUnlocked, "saveHubProfileUnlocked");
3357
+ async function probeHubIdentity(resource, accessToken) {
3358
+ let response;
3359
+ try {
3360
+ response = await fetch(`${resource}/v1/me`, {
3361
+ headers: { Authorization: `Bearer ${accessToken}`, "X-ItPay-OAuth-Resource": resource },
3362
+ signal: AbortSignal.timeout(1e4)
3271
3363
  });
3364
+ } catch {
3365
+ throw new Error("connection_verification_pending: identity check unavailable; preserve the original connection and references, do not reconnect");
3272
3366
  }
3273
- getRun(runId, options = {}) {
3274
- const { actionRef, ...budget } = options;
3275
- const query = actionRef === void 0 ? "" : `?${new URLSearchParams({ action_ref: actionRef })}`;
3276
- return this.request("GET", `/v1/runs/${encodeURIComponent(runId)}${query}`, budget);
3277
- }
3278
- serviceAction(runId, actionRef, input, idempotencyKey, expectedPurpose) {
3279
- return this.request("POST", `/v1/runs/${encodeURIComponent(runId)}/service-actions`, { body: { action_ref: actionRef, input, ...expectedPurpose ? { expected_purpose: expectedPurpose } : {} }, idempotencyKey });
3280
- }
3281
- servicePurchaseReview(runId) {
3282
- return this.request("GET", `/v1/runs/${encodeURIComponent(runId)}/purchase`);
3367
+ if (!response.ok) {
3368
+ if (response.status === 401 || response.status === 403) throw new Error("authorization_required: original connection identity rejected");
3369
+ throw new Error("connection_verification_pending: identity check unavailable; preserve the original connection and references, do not reconnect");
3283
3370
  }
3284
- servicePurchaseConfirm(runId, input) {
3285
- return this.request("POST", `/v1/runs/${encodeURIComponent(runId)}/purchase/confirm`, { body: input });
3371
+ try {
3372
+ const identity = await response.json();
3373
+ if (!identity || typeof identity !== "object" || Array.isArray(identity)) throw new Error();
3374
+ const me = identity;
3375
+ if (typeof me.buyer_id !== "string" || !me.buyer_id || typeof me.consumer_id !== "string" || !me.consumer_id) throw new Error();
3376
+ return { buyer_id: me.buyer_id, consumer_id: me.consumer_id };
3377
+ } catch {
3378
+ throw new Error("connection_verification_pending: identity response invalid; preserve the original connection and references, do not reconnect");
3286
3379
  }
3287
- servicePurchaseResume(runId) {
3288
- return this.request("POST", `/v1/runs/${encodeURIComponent(runId)}/purchase/resume`, { body: {} });
3380
+ }
3381
+ __name(probeHubIdentity, "probeHubIdentity");
3382
+ function storedHubToken(env = process.env) {
3383
+ const path2 = hubCredentialsPath(env);
3384
+ if (!existsSync(path2) || existsSync(path2 + ".recovery.json")) return void 0;
3385
+ try {
3386
+ const file = JSON.parse(readFileSync2(path2, "utf8"));
3387
+ const p = file.profiles["default"];
3388
+ if (!p?.access_token || p.identity_verification_pending) return void 0;
3389
+ if (p.expires_at && Date.parse(p.expires_at) <= Date.now()) return void 0;
3390
+ return p.access_token;
3391
+ } catch {
3392
+ return void 0;
3289
3393
  }
3290
- serviceAftercare(runId, mode, caseId, input = {}, idempotencyKey) {
3291
- if (!["directory", "get", "create", "respond", "cancel"].includes(mode))
3292
- throw new Error("unsupported original aftercare mode");
3293
- if (["get", "respond", "cancel"].includes(mode) && !caseId)
3294
- throw new Error("original case required");
3295
- const root = `/v1/runs/${encodeURIComponent(runId)}/aftercare`;
3296
- if (mode === "directory")
3297
- return this.request("GET", root);
3298
- if (mode === "get")
3299
- return this.request("GET", `${root}/${encodeURIComponent(caseId)}`);
3300
- return this.request("POST", mode === "create" ? root : `${root}/${encodeURIComponent(caseId)}/${mode}`, { body: input, ...idempotencyKey ? { idempotencyKey } : {} });
3394
+ }
3395
+ __name(storedHubToken, "storedHubToken");
3396
+ function storedHubProfile(env = process.env) {
3397
+ const path2 = hubCredentialsPath(env);
3398
+ if (!existsSync(path2)) return void 0;
3399
+ return JSON.parse(readFileSync2(path2, "utf8")).profiles.default;
3400
+ }
3401
+ __name(storedHubProfile, "storedHubProfile");
3402
+ async function connectedHubToken(env = process.env, rejectedToken) {
3403
+ return withAsyncFileLock(`${hubCredentialsPath(env)}.refresh.lock`, async () => {
3404
+ if (existsSync(hubCredentialsPath(env) + ".recovery.json")) throw new Error("connection_verification_pending: original recovery pending; only api recover-connection may verify its candidate, no business retry");
3405
+ const p = storedHubProfile(env);
3406
+ if (!p?.access_token || !p.subject) throw new Error("authorization_required: reconnect ItPay");
3407
+ const cfg = hubOAuthConfig(env);
3408
+ if (p.base_url !== cfg.resource || p.token_url !== cfg.tokenURL || p.client_id !== cfg.clientID || !p.consumer_id) {
3409
+ throw new Error("authorization_required: connection configuration changed");
3410
+ }
3411
+ const refreshAt = p.refresh_after ? Date.parse(p.refresh_after) : Date.parse(p.expires_at ?? "") - 3e4;
3412
+ if (!p.identity_verification_pending && p.access_token !== rejectedToken && refreshAt > Date.now()) return p.access_token;
3413
+ let candidate = p;
3414
+ if (!p.identity_verification_pending || !(Date.parse(p.expires_at ?? "") > Date.now())) {
3415
+ if (!p.refresh_token) throw new Error("authorization_required: reconnect the original ItPay connection");
3416
+ let response;
3417
+ try {
3418
+ response = await fetch(p.token_url, {
3419
+ method: "POST",
3420
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
3421
+ body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: p.refresh_token, client_id: p.client_id, resource: cfg.resource }),
3422
+ signal: AbortSignal.timeout(1e4)
3423
+ });
3424
+ } catch {
3425
+ throw new Error("connection_verification_pending: refresh outcome unknown; preserve the original connection and references, do not repeat authorization or business requests");
3426
+ }
3427
+ if (!response.ok) {
3428
+ let rejected = response.status === 401 || response.status === 403;
3429
+ if (response.status === 400) {
3430
+ try {
3431
+ const problem = await response.json();
3432
+ rejected = ["invalid_grant", "invalid_client", "unauthorized_client"].includes(problem?.error ?? "");
3433
+ } catch {
3434
+ }
3435
+ }
3436
+ if (rejected) throw new Error("authorization_required: connection refresh rejected");
3437
+ throw new Error("connection_verification_pending: refresh unavailable; preserve the original connection and references");
3438
+ }
3439
+ let token;
3440
+ try {
3441
+ token = await response.json();
3442
+ } catch {
3443
+ throw new Error("connection_verification_pending: invalid refresh response; preserve the original connection and references");
3444
+ }
3445
+ if (!token || typeof token.access_token !== "string" || !token.access_token || typeof token.token_type !== "string" || token.token_type.toLowerCase() !== "bearer" || typeof token.expires_in !== "number" || !Number.isFinite(token.expires_in) || token.expires_in <= 0 || (!Number.isFinite(Date.now() + token.expires_in * 1e3) || Date.now() + token.expires_in * 1e3 > 864e13) || token.refresh_token !== void 0 && (typeof token.refresh_token !== "string" || !token.refresh_token)) {
3446
+ throw new Error("connection_verification_pending: invalid refresh response; preserve the original connection and references");
3447
+ }
3448
+ const now = Date.now();
3449
+ candidate = {
3450
+ ...p,
3451
+ access_token: token.access_token,
3452
+ refresh_token: token.refresh_token ?? p.refresh_token,
3453
+ expires_at: new Date(now + token.expires_in * 1e3).toISOString(),
3454
+ refresh_after: new Date(now + token.expires_in * 1e3 - Math.min(3e4, Math.max(100, token.expires_in * 100))).toISOString(),
3455
+ identity_verification_pending: true
3456
+ };
3457
+ saveHubProfileUnlocked(candidate, env);
3458
+ }
3459
+ const identity = await probeHubIdentity(cfg.resource, candidate.access_token);
3460
+ if (identity.buyer_id !== p.subject || identity.consumer_id !== p.consumer_id) {
3461
+ throw new Error("authorization_required: refreshed identity changed");
3462
+ }
3463
+ if (!(Date.parse(candidate.expires_at ?? "") > Date.now())) {
3464
+ throw new Error("connection_verification_pending: token expired during identity check; preserve the original connection and references");
3465
+ }
3466
+ delete candidate.identity_verification_pending;
3467
+ saveHubProfileUnlocked(candidate, env);
3468
+ return candidate.access_token;
3469
+ });
3470
+ }
3471
+ __name(connectedHubToken, "connectedHubToken");
3472
+ function importHubKey(apiKey, baseURL, env = process.env) {
3473
+ return withFileLock(`${hubCredentialsPath(env)}.refresh.lock`, () => importHubKeyUnlocked(apiKey, baseURL, env));
3474
+ }
3475
+ __name(importHubKey, "importHubKey");
3476
+ async function recoverHubConnection(env = process.env) {
3477
+ await withOriginalHubCandidate(env);
3478
+ }
3479
+ __name(recoverHubConnection, "recoverHubConnection");
3480
+ async function readOriginalHubReceipt(requestKey, env = process.env) {
3481
+ if (!requestKey || requestKey.length > 512 || /[\r\n\0]/.test(requestKey)) throw new Error("receipt_denied: exact original request key required");
3482
+ return withOriginalHubCandidate(env, requestKey);
3483
+ }
3484
+ __name(readOriginalHubReceipt, "readOriginalHubReceipt");
3485
+ async function withOriginalHubCandidate(env, receiptRequest) {
3486
+ return withAsyncFileLock(`${hubCredentialsPath(env)}.refresh.lock`, async () => {
3487
+ const original = storedHubProfile(env), cfg = hubOAuthConfig(env);
3488
+ if (!original?.subject || !original.consumer_id || !original.refresh_token || original.api_key || original.base_url !== cfg.resource || original.client_id !== cfg.clientID || original.token_url !== cfg.tokenURL || env.ITPAY_HUB_KEY || env.ITPAY_HUB_TOKEN) throw new Error("connection_recovery_denied: original OAuth binding and refresh proof required; preserve profile and tasks");
3489
+ const path2 = hubCredentialsPath(env) + ".recovery.json";
3490
+ const binding = { base_url: original.base_url, subject: original.subject, consumer_id: original.consumer_id, client_id: original.client_id, token_url: original.token_url };
3491
+ const save = /* @__PURE__ */ __name((pending2) => {
3492
+ const tmp = path2 + "." + randomUUID2() + ".tmp";
3493
+ try {
3494
+ writeFileSync2(tmp, JSON.stringify(pending2), { flag: "wx", mode: 384 });
3495
+ renameSync(tmp, path2);
3496
+ } finally {
3497
+ try {
3498
+ unlinkSync2(tmp);
3499
+ } catch {
3500
+ }
3501
+ }
3502
+ }, "save");
3503
+ let pending;
3504
+ let refreshSource = original;
3505
+ if (existsSync(path2)) {
3506
+ try {
3507
+ pending = JSON.parse(readFileSync2(path2, "utf8"));
3508
+ } catch {
3509
+ throw new Error("connection_recovery_denied: private recovery material unreadable; preserve original profile");
3510
+ }
3511
+ if (!pending || typeof pending !== "object" || JSON.stringify(pending.binding) !== JSON.stringify(binding)) throw new Error("connection_recovery_denied: recovery binding changed; preserve original profile");
3512
+ if (pending.phase !== "candidate" || !pending.profile) throw new Error("connection_verification_pending: refresh outcome unknown; preserve private recovery material, do not repeat refresh or connect");
3513
+ if (!Object.entries(binding).every(([k, v]) => pending.profile[k] === v)) throw new Error("connection_recovery_denied: private candidate binding mismatch; preserve original profile");
3514
+ }
3515
+ if (!pending || !(Date.parse(pending.profile?.expires_at ?? "") > Date.now())) {
3516
+ refreshSource = pending?.profile ?? original;
3517
+ if (!refreshSource.refresh_token) throw new Error("connection_recovery_denied: original refresh proof unavailable; preserve profile and candidate");
3518
+ save({ binding, phase: "refresh_unknown", profile: refreshSource });
3519
+ let response2;
3520
+ try {
3521
+ response2 = await fetch(cfg.tokenURL, { method: "POST", redirect: "error", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshSource.refresh_token, client_id: cfg.clientID, resource: cfg.resource }), signal: AbortSignal.timeout(1e4) });
3522
+ } catch {
3523
+ throw new Error("connection_verification_pending: refresh outcome unknown; preserve original profile and private recovery material");
3524
+ }
3525
+ if (!response2.ok) {
3526
+ let rejected = response2.status === 401 || response2.status === 403;
3527
+ if (response2.status === 400) {
3528
+ try {
3529
+ const problem = await response2.json();
3530
+ rejected = ["invalid_grant", "invalid_client", "unauthorized_client"].includes(problem?.error ?? "");
3531
+ } catch {
3532
+ }
3533
+ }
3534
+ if (rejected) throw new Error("connection_recovery_denied: original refresh proof rejected; preserve profile and tasks, do not create a replacement connection");
3535
+ throw new Error("connection_verification_pending: refresh unavailable or outcome unknown; preserve original profile and private recovery material");
3536
+ }
3537
+ let t;
3538
+ try {
3539
+ t = await response2.json();
3540
+ } catch {
3541
+ throw new Error("connection_verification_pending: invalid refresh response; preserve private recovery material");
3542
+ }
3543
+ if (!t || typeof t !== "object" || typeof t.access_token !== "string" || !t.access_token || typeof t.token_type !== "string" || t.token_type.toLowerCase() !== "bearer" || typeof t.expires_in !== "number" || !Number.isFinite(t.expires_in) || t.expires_in <= 0 || Date.now() + t.expires_in * 1e3 > 864e13 || t.refresh_token !== void 0 && (typeof t.refresh_token !== "string" || !t.refresh_token)) throw new Error("connection_verification_pending: invalid refresh response; preserve private recovery material");
3544
+ const now = Date.now();
3545
+ pending = { binding, phase: "candidate", profile: { ...original, access_token: t.access_token, refresh_token: t.refresh_token ?? refreshSource.refresh_token, expires_at: new Date(now + t.expires_in * 1e3).toISOString(), refresh_after: new Date(now + t.expires_in * 1e3 - Math.min(3e4, Math.max(100, t.expires_in * 100))).toISOString(), identity_verification_pending: true } };
3546
+ save(pending);
3547
+ }
3548
+ const candidate = pending.profile;
3549
+ if (!Object.entries(binding).every(([k, v]) => candidate[k] === v)) throw new Error("connection_recovery_denied: private candidate binding mismatch; preserve original profile");
3550
+ if (!candidate.access_token || !(Date.parse(candidate.expires_at ?? "") > Date.now())) throw new Error("connection_recovery_denied: recovery candidate expired; preserve private proof and request owner review");
3551
+ if (receiptRequest !== void 0) {
3552
+ let response2;
3553
+ try {
3554
+ response2 = await fetch(cfg.resource + "/v1/requests/" + encodeURIComponent(receiptRequest), { method: "GET", redirect: "error", headers: { Authorization: "Bearer " + candidate.access_token, "X-ItPay-OAuth-Resource": cfg.resource, "X-ItPay-Receipt-Only": "1", "X-ItPay-Expected-Buyer": binding.subject, "X-ItPay-Expected-Consumer": binding.consumer_id }, signal: AbortSignal.timeout(1e4) });
3555
+ } catch {
3556
+ throw new Error("receipt_unavailable: original receipt unavailable; preserve profile and private candidate");
3557
+ }
3558
+ if (!response2.ok) throw new Error("receipt_denied: original receipt rejected; preserve profile and private candidate");
3559
+ const result2 = await response2.json();
3560
+ if (result2?.request_id !== receiptRequest || result2.read_only !== true || typeof result2.run?.run_id !== "string") throw new Error("receipt_denied: invalid original receipt reply");
3561
+ return result2;
3562
+ }
3563
+ let response;
3564
+ try {
3565
+ response = await fetch(cfg.resource + "/v1/connection/recover", { method: "POST", redirect: "error", headers: { "Content-Type": "application/json", Authorization: "Bearer " + candidate.access_token, "X-ItPay-OAuth-Resource": cfg.resource }, body: JSON.stringify({ expected_buyer_id: binding.subject, expected_consumer_id: binding.consumer_id }), signal: AbortSignal.timeout(1e4) });
3566
+ } catch {
3567
+ throw new Error("connection_verification_pending: recovery outcome unknown; preserve original profile and candidate; retry only this recovery, never business");
3568
+ }
3569
+ if (response.status === 409) throw new Error("connection_recovery_conflict: another active connection requires owner review; preserve original profile, tasks and candidate");
3570
+ if (response.status >= 500 || response.status === 404 || response.status === 405) throw new Error("connection_verification_pending: paired recovery owner unavailable; preserve profile and candidate");
3571
+ if (!response.ok) throw new Error("connection_recovery_denied: owner rejected original binding; preserve profile, tasks and private candidate");
3572
+ let result;
3573
+ try {
3574
+ result = await response.json();
3575
+ } catch {
3576
+ throw new Error("connection_verification_pending: recovery reply invalid; preserve original profile and candidate");
3577
+ }
3578
+ if (!result || typeof result !== "object" || result.buyer_id !== binding.subject || result.consumer_id !== binding.consumer_id || result.status !== "recovered") throw new Error("connection_recovery_denied: owner recovery binding mismatch; preserve original profile");
3579
+ const identity = await probeHubIdentity(cfg.resource, candidate.access_token);
3580
+ if (identity.buyer_id !== binding.subject || identity.consumer_id !== binding.consumer_id) throw new Error("connection_recovery_denied: verified original identity mismatch; preserve profile and tasks");
3581
+ if (!(Date.parse(candidate.expires_at ?? "") > Date.now())) throw new Error("connection_verification_pending: candidate expired during verification; preserve original profile");
3582
+ delete candidate.identity_verification_pending;
3583
+ saveHubProfileUnlocked(candidate, env);
3584
+ unlinkSync2(path2);
3585
+ });
3586
+ }
3587
+ __name(withOriginalHubCandidate, "withOriginalHubCandidate");
3588
+ async function withNewHubSession(action, env = process.env) {
3589
+ return withAsyncFileLock(`${hubCredentialsPath(env)}.refresh.lock`, async () => {
3590
+ if (storedHubProfile(env) || existsSync(hubCredentialsPath(env) + ".recovery.json")) throw new Error("connection_recovery_denied: existing connection preserved; use api recover-connection, never replace it");
3591
+ return action((access, refresh, seconds, subject, base, consumer) => {
3592
+ if (storedHubProfile(env) || existsSync(hubCredentialsPath(env) + ".recovery.json")) throw new Error("connection_recovery_denied: original profile appeared; new connection not saved");
3593
+ storeHubSessionUnlocked(access, refresh, seconds, subject, base, env, consumer);
3594
+ });
3595
+ });
3596
+ }
3597
+ __name(withNewHubSession, "withNewHubSession");
3598
+
3599
+ // src/commands/admin.ts
3600
+ import { readFileSync as readFileSync3 } from "node:fs";
3601
+
3602
+ // node_modules/@itpay/hub-client/dist/index.js
3603
+ var HUB_CONTRACT_REVISION = "hub.v1.2026-10-26.integration1";
3604
+ var isRunReceipt = /* @__PURE__ */ __name((r) => "run_id" in r, "isRunReceipt");
3605
+ var HubApiError = class extends Error {
3606
+ static {
3607
+ __name(this, "HubApiError");
3301
3608
  }
3302
- createAPIPaymentRequest(intentId, payment = {}) {
3303
- return this.request("POST", `/v1/intents/${encodeURIComponent(intentId)}/payment-request`, { payerCurrency: payment.payerCurrency });
3609
+ status;
3610
+ envelope;
3611
+ constructor(status, envelope) {
3612
+ super(`${status} ${envelope.error.code}: ${envelope.error.message}`);
3613
+ this.status = status;
3614
+ this.envelope = envelope;
3615
+ this.name = "HubApiError";
3304
3616
  }
3305
- getAPIPaymentRequest(intentId) {
3306
- return this.request("GET", `/v1/intents/${encodeURIComponent(intentId)}/payment-request`);
3617
+ get code() {
3618
+ return this.envelope.error.code;
3307
3619
  }
3308
- continueIntent(intentId, idempotencyKey, body = {}) {
3309
- return this.request("POST", `/v1/intents/${encodeURIComponent(intentId)}/continue`, {
3310
- body,
3311
- idempotencyKey,
3312
- intentResponse: true
3313
- });
3620
+ };
3621
+ var HubClient = class {
3622
+ static {
3623
+ __name(this, "HubClient");
3314
3624
  }
3315
- me() {
3316
- return this.request("GET", "/v1/me");
3625
+ baseUrl;
3626
+ token;
3627
+ resource;
3628
+ apiKeyHeader;
3629
+ fetchImpl;
3630
+ budgets;
3631
+ constructor(opts) {
3632
+ this.resource = opts.resource;
3633
+ this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
3634
+ this.token = opts.token;
3635
+ this.apiKeyHeader = opts.apiKeyHeader ?? "Authorization";
3636
+ this.fetchImpl = opts.fetchImpl ?? fetch;
3637
+ this.budgets = { metadata: 5e3, status: 5e3, action: 15e3, ...opts.requestBudgetsMs };
3638
+ for (const value of Object.values(this.budgets))
3639
+ if (!Number.isFinite(value) || value <= 0)
3640
+ throw new Error("request budget must be finite and positive");
3317
3641
  }
3318
- usage() {
3319
- return this.request("GET", "/v1/me/usage");
3642
+ async request(method, path2, opts = {}) {
3643
+ const url = new URL(this.baseUrl + path2);
3644
+ for (const [k, v] of Object.entries(opts.query ?? {})) {
3645
+ if (v !== void 0)
3646
+ url.searchParams.set(k, String(v));
3647
+ }
3648
+ const headers = { Accept: "application/json", "X-ItPay-Hub-Contract": HUB_CONTRACT_REVISION };
3649
+ if (this.resource)
3650
+ headers["X-ItPay-OAuth-Resource"] = this.resource;
3651
+ if (this.token)
3652
+ headers[this.apiKeyHeader] = this.apiKeyHeader === "Authorization" ? `Bearer ${this.token}` : this.token;
3653
+ if (opts.body !== void 0)
3654
+ headers["Content-Type"] = "application/json";
3655
+ if (opts.idempotencyKey)
3656
+ headers["Idempotency-Key"] = opts.idempotencyKey;
3657
+ if (opts.payerCurrency)
3658
+ headers["ItPay-Payment-Currency"] = opts.payerCurrency;
3659
+ const init = { method, headers };
3660
+ if (opts.body !== void 0)
3661
+ init.body = JSON.stringify(opts.body);
3662
+ const stage = (path2.startsWith("/v1/runs/") || path2.startsWith("/v1/requests/")) && method === "GET" ? "status" : method === "GET" || path2 === "/v1/discover" ? "metadata" : "action";
3663
+ const timeoutMs = Math.min(opts.timeoutMs ?? this.budgets[stage], this.budgets[stage]);
3664
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
3665
+ throw new Error("request budget exhausted");
3666
+ const controller = new AbortController();
3667
+ let timer;
3668
+ const aborted = new Promise((_, reject) => {
3669
+ controller.signal.addEventListener("abort", () => reject(new HubApiError(0, { error: { code: opts.signal?.aborted ? "request_cancelled" : "request_timeout", message: method === "POST" && path2 !== "/v1/discover" ? "Request outcome is unknown; read the original request/run before exact-key recovery. No automatic write retry." : `Hub ${stage} read timed out after ${timeoutMs} ms; no spending-budget conclusion. Keep the original reference and read it later.`, details: { stage, method, path: path2 } } })), { once: true });
3670
+ timer = setTimeout(() => controller.abort(), timeoutMs);
3671
+ });
3672
+ const cancel = /* @__PURE__ */ __name(() => controller.abort(), "cancel");
3673
+ opts.signal?.addEventListener("abort", cancel, { once: true });
3674
+ if (opts.signal?.aborted)
3675
+ controller.abort();
3676
+ try {
3677
+ return await Promise.race([(async () => {
3678
+ const res = await this.fetchImpl(url, { ...init, signal: controller.signal });
3679
+ const serverRevision = res.headers.get("X-ItPay-Hub-Contract");
3680
+ if (serverRevision && serverRevision !== HUB_CONTRACT_REVISION)
3681
+ throw new HubApiError(409, { error: { code: "contract_mismatch", message: "Hub/SDK contract mismatch; install the paired CLI/MCP candidate before executing", details: { expected: HUB_CONTRACT_REVISION, received: serverRevision } } });
3682
+ const text2 = await res.text();
3683
+ const payload = text2 ? JSON.parse(text2) : void 0;
3684
+ if (!res.ok) {
3685
+ const p = payload;
3686
+ if (opts.intentResponse && p && typeof p.intent_id === "string") {
3687
+ return payload;
3688
+ }
3689
+ const raw = payload;
3690
+ const envelope = raw && raw.error && typeof raw.error.code === "string" ? raw : { error: { code: "internal", message: `HTTP ${res.status}` } };
3691
+ throw new HubApiError(res.status, envelope);
3692
+ }
3693
+ return payload;
3694
+ })(), aborted]);
3695
+ } finally {
3696
+ clearTimeout(timer);
3697
+ opts.signal?.removeEventListener("abort", cancel);
3698
+ }
3320
3699
  }
3321
- wallet() {
3322
- return this.request("GET", "/v1/me/wallet");
3700
+ listCommerceTransactions(cursor) {
3701
+ return this.request("GET", "/v1/me/transactions" + (cursor ? `?cursor=${encodeURIComponent(cursor)}` : ""));
3323
3702
  }
3324
- walletTransactions(opts = {}) {
3325
- return this.request("GET", "/v1/me/wallet/transactions", { query: opts });
3703
+ getPurchaseCollectionOptions(transactionId) {
3704
+ return this.request("GET", `/v1/me/transactions/${encodeURIComponent(transactionId)}/collection-options`);
3326
3705
  }
3327
- topupSkus() {
3328
- return this.request("GET", "/v1/topup-skus");
3706
+ getPurchaseCollection(transactionId) {
3707
+ return this.request("GET", `/v1/me/transactions/${encodeURIComponent(transactionId)}/collection`);
3708
+ }
3709
+ requestPurchaseCollection(transactionId, body) {
3710
+ return this.request("POST", `/v1/me/transactions/${encodeURIComponent(transactionId)}/collection`, { body });
3711
+ }
3712
+ refreshPurchaseCollection(transactionId) {
3713
+ return this.request("POST", `/v1/me/transactions/${encodeURIComponent(transactionId)}/collection/refresh`, { body: {} });
3714
+ }
3715
+ getSupplementCollectionOptions(caseId) {
3716
+ return this.request("GET", `/v1/me/after-sales/${encodeURIComponent(caseId)}/collection-options`);
3717
+ }
3718
+ getSupplementCollection(caseId) {
3719
+ return this.request("GET", `/v1/me/after-sales/${encodeURIComponent(caseId)}/collection`);
3720
+ }
3721
+ requestSupplementCollection(caseId, body) {
3722
+ return this.request("POST", `/v1/me/after-sales/${encodeURIComponent(caseId)}/collection`, { body });
3723
+ }
3724
+ refreshSupplementCollection(caseId) {
3725
+ return this.request("POST", `/v1/me/after-sales/${encodeURIComponent(caseId)}/collection/refresh`, { body: {} });
3726
+ }
3727
+ getCommerceTransaction(id) {
3728
+ return this.request("GET", `/v1/me/transactions/${encodeURIComponent(id)}`);
3729
+ }
3730
+ confirmCommerceAfterSales(id, body, idempotencyKey) {
3731
+ return this.request("POST", `/v1/me/transactions/${encodeURIComponent(id)}/after-sales`, { body, idempotencyKey });
3732
+ }
3733
+ getCommerceAfterSales(id) {
3734
+ return this.request("GET", `/v1/me/after-sales/${encodeURIComponent(id)}`);
3735
+ }
3736
+ getCommerceRefund(id) {
3737
+ return this.request("GET", `/v1/me/refunds/${encodeURIComponent(id)}`);
3738
+ }
3739
+ getCommerceAfterSalesQuoteOptions(transactionId, cursor) {
3740
+ return this.request("GET", `/v1/me/transactions/${encodeURIComponent(transactionId)}/quote-options${cursor ? `?cursor=${encodeURIComponent(cursor)}` : ""}`);
3741
+ }
3742
+ prepareCommerceAfterSalesQuote(transactionId, body, idempotencyKey) {
3743
+ return this.request("POST", `/v1/me/transactions/${encodeURIComponent(transactionId)}/quote-requests`, { body, idempotencyKey });
3744
+ }
3745
+ getCommerceQuoteCredentialRequirements(requestId) {
3746
+ return this.request("GET", `/v1/me/quote-requests/${encodeURIComponent(requestId)}/credential-requirements`);
3747
+ }
3748
+ getCommerceCaseCredentialRequirements(caseId) {
3749
+ return this.request("GET", `/v1/me/after-sales/${encodeURIComponent(caseId)}/credential-requirements`);
3750
+ }
3751
+ getCommerceRefundCredentialRequirements(refundId, mode) {
3752
+ return this.request("GET", `/v1/me/refunds/${encodeURIComponent(refundId)}/credential-requirements/${mode}`);
3753
+ }
3754
+ startCommerceAfterSalesQuote(requestId, body) {
3755
+ return this.request("POST", `/v1/me/quote-requests/${encodeURIComponent(requestId)}/execution`, { body });
3756
+ }
3757
+ getCommerceAfterSalesQuoteRequest(requestId) {
3758
+ return this.request("GET", `/v1/me/quote-requests/${encodeURIComponent(requestId)}`);
3759
+ }
3760
+ getCommerceAfterSalesExecution(caseId) {
3761
+ return this.request("GET", `/v1/me/after-sales/${encodeURIComponent(caseId)}/execution`);
3762
+ }
3763
+ startCommerceAfterSalesExecution(caseId, body) {
3764
+ return this.request("POST", `/v1/me/after-sales/${encodeURIComponent(caseId)}/execution`, { body });
3765
+ }
3766
+ getCommerceRefundExecutionAuthorization(refundId) {
3767
+ return this.request("GET", `/v1/me/refunds/${encodeURIComponent(refundId)}/execution-authorization`);
3768
+ }
3769
+ authorizeCommerceRefundExecution(refundId, body) {
3770
+ return this.request("POST", `/v1/me/refunds/${encodeURIComponent(refundId)}/execution-authorization`, { body });
3771
+ }
3772
+ requestCommerceRefund(caseId, body, idempotencyKey) {
3773
+ return this.request("POST", `/v1/me/after-sales/${encodeURIComponent(caseId)}/refunds`, { body, idempotencyKey });
3774
+ }
3775
+ requestSupplementRefund(caseId, body, idempotencyKey) {
3776
+ return this.request("POST", `/v1/me/after-sales/${encodeURIComponent(caseId)}/supplement-refunds`, { body, idempotencyKey });
3777
+ }
3778
+ createAPIIngestion(body, idempotencyKey) {
3779
+ return this.request("POST", "/v1/me/api-ingestions", { body, idempotencyKey });
3780
+ }
3781
+ listAPIIngestions() {
3782
+ return this.request("GET", "/v1/me/api-ingestions");
3783
+ }
3784
+ getAPIIngestion(jobId) {
3785
+ return this.request("GET", `/v1/me/api-ingestions/${encodeURIComponent(jobId)}`);
3786
+ }
3787
+ retryAPIIngestion(jobId) {
3788
+ return this.request("POST", `/v1/me/api-ingestions/${encodeURIComponent(jobId)}/retry`, { body: {} });
3789
+ }
3790
+ discover(req = {}) {
3791
+ return this.request("POST", "/v1/discover", { body: req });
3792
+ }
3793
+ inspect(operationId) {
3794
+ return this.request("GET", `/v1/operations/${encodeURIComponent(operationId)}`);
3795
+ }
3796
+ /** The single run-creation entry. Always sends expected_publication_id. */
3797
+ run(operationId, admission, idempotencyKey, payment = {}) {
3798
+ return this.request("POST", `/v1/operations/${encodeURIComponent(operationId)}/runs`, {
3799
+ body: admission,
3800
+ idempotencyKey,
3801
+ payerCurrency: payment.payerCurrency,
3802
+ intentResponse: true
3803
+ });
3804
+ }
3805
+ getRun(runId, options = {}) {
3806
+ const { actionRef, ...budget } = options;
3807
+ const query = actionRef === void 0 ? "" : `?${new URLSearchParams({ action_ref: actionRef })}`;
3808
+ return this.request("GET", `/v1/runs/${encodeURIComponent(runId)}${query}`, budget);
3809
+ }
3810
+ serviceAction(runId, actionRef, input, idempotencyKey, expectedPurpose) {
3811
+ return this.request("POST", `/v1/runs/${encodeURIComponent(runId)}/service-actions`, { body: { action_ref: actionRef, input, ...expectedPurpose ? { expected_purpose: expectedPurpose } : {} }, idempotencyKey });
3812
+ }
3813
+ servicePurchaseReview(runId) {
3814
+ return this.request("GET", `/v1/runs/${encodeURIComponent(runId)}/purchase`);
3815
+ }
3816
+ servicePurchaseConfirm(runId, input) {
3817
+ return this.request("POST", `/v1/runs/${encodeURIComponent(runId)}/purchase/confirm`, { body: input });
3818
+ }
3819
+ servicePurchaseResume(runId) {
3820
+ return this.request("POST", `/v1/runs/${encodeURIComponent(runId)}/purchase/resume`, { body: {} });
3821
+ }
3822
+ serviceAftercare(runId, mode, caseId, input = {}, idempotencyKey) {
3823
+ if (!["directory", "get", "create", "respond", "cancel"].includes(mode))
3824
+ throw new Error("unsupported original aftercare mode");
3825
+ if (["get", "respond", "cancel"].includes(mode) && !caseId)
3826
+ throw new Error("original case required");
3827
+ const root = `/v1/runs/${encodeURIComponent(runId)}/aftercare`;
3828
+ if (mode === "directory")
3829
+ return this.request("GET", root);
3830
+ if (mode === "get")
3831
+ return this.request("GET", `${root}/${encodeURIComponent(caseId)}`);
3832
+ return this.request("POST", mode === "create" ? root : `${root}/${encodeURIComponent(caseId)}/${mode}`, { body: input, ...idempotencyKey ? { idempotencyKey } : {} });
3833
+ }
3834
+ createAPIPaymentRequest(intentId, payment = {}) {
3835
+ return this.request("POST", `/v1/intents/${encodeURIComponent(intentId)}/payment-request`, { payerCurrency: payment.payerCurrency });
3836
+ }
3837
+ getAPIPaymentRequest(intentId) {
3838
+ return this.request("GET", `/v1/intents/${encodeURIComponent(intentId)}/payment-request`);
3839
+ }
3840
+ /** Read-only original request lookup. Never admits, continues, funds or retries a write. */
3841
+ getRequest(requestID) {
3842
+ return this.request("GET", `/v1/requests/${encodeURIComponent(requestID)}`);
3843
+ }
3844
+ continueIntent(intentId, idempotencyKey, body = {}) {
3845
+ return this.request("POST", `/v1/intents/${encodeURIComponent(intentId)}/continue`, {
3846
+ body,
3847
+ idempotencyKey,
3848
+ intentResponse: true
3849
+ });
3850
+ }
3851
+ me() {
3852
+ return this.request("GET", "/v1/me");
3853
+ }
3854
+ usage() {
3855
+ return this.request("GET", "/v1/me/usage");
3856
+ }
3857
+ wallet() {
3858
+ return this.request("GET", "/v1/me/wallet");
3859
+ }
3860
+ walletTransactions(opts = {}) {
3861
+ return this.request("GET", "/v1/me/wallet/transactions", { query: opts });
3862
+ }
3863
+ topupSkus() {
3864
+ return this.request("GET", "/v1/topup-skus");
3329
3865
  }
3330
3866
  createTopup(req, idempotencyKey) {
3331
3867
  return this.request("POST", "/v1/me/topups", { body: req, idempotencyKey });
@@ -3449,708 +3985,295 @@ var HttpTransportError = class extends Error {
3449
3985
  this.causeCode = causeCode;
3450
3986
  this.name = "HttpTransportError";
3451
3987
  }
3452
- code;
3453
- attempts;
3454
- causeCode;
3455
- static {
3456
- __name(this, "HttpTransportError");
3457
- }
3458
- retryable = true;
3459
- diagnostics = [];
3460
- };
3461
- function asTransientTransportError(error, attempts) {
3462
- if (isAbort(error)) return void 0;
3463
- const causeCode = findCauseCode(error);
3464
- const code = causeCode ? classifyCauseCode(causeCode) : classifyFetchFailure(error);
3465
- return code ? new HttpTransportError(code, attempts, causeCode, error) : void 0;
3466
- }
3467
- __name(asTransientTransportError, "asTransientTransportError");
3468
- function classifyCauseCode(code) {
3469
- switch (code) {
3470
- case "ECONNRESET":
3471
- return "network_connection_reset";
3472
- case "ETIMEDOUT":
3473
- case "UND_ERR_CONNECT_TIMEOUT":
3474
- case "UND_ERR_HEADERS_TIMEOUT":
3475
- case "UND_ERR_BODY_TIMEOUT":
3476
- return "network_timeout";
3477
- case "EAI_AGAIN":
3478
- return "network_dns_temporary";
3479
- case "ENETUNREACH":
3480
- case "EHOSTUNREACH":
3481
- return "network_unreachable";
3482
- case "ECONNREFUSED":
3483
- return "network_connection_refused";
3484
- case "UND_ERR_SOCKET":
3485
- return "network_socket_error";
3486
- default:
3487
- return void 0;
3488
- }
3489
- }
3490
- __name(classifyCauseCode, "classifyCauseCode");
3491
- function classifyFetchFailure(error) {
3492
- return error instanceof TypeError && error.message === "fetch failed" ? "network_transport_failed" : void 0;
3493
- }
3494
- __name(classifyFetchFailure, "classifyFetchFailure");
3495
- function findCauseCode(error) {
3496
- const seen = /* @__PURE__ */ new Set();
3497
- let current = error;
3498
- while (current && typeof current === "object" && !seen.has(current)) {
3499
- seen.add(current);
3500
- const code = current.code;
3501
- if (typeof code === "string" && code !== "") return code;
3502
- current = current.cause;
3503
- }
3504
- return void 0;
3505
- }
3506
- __name(findCauseCode, "findCauseCode");
3507
- function isAbort(error) {
3508
- return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
3509
- }
3510
- __name(isAbort, "isAbort");
3511
- function transportMessage(code) {
3512
- switch (code) {
3513
- case "network_connection_reset":
3514
- return "network connection was reset";
3515
- case "network_timeout":
3516
- return "network connection timed out";
3517
- case "network_dns_temporary":
3518
- return "DNS lookup was temporarily unavailable";
3519
- case "network_unreachable":
3520
- return "network was unreachable";
3521
- case "network_connection_refused":
3522
- return "network connection was refused";
3523
- case "network_socket_error":
3524
- return "network socket failed";
3525
- case "network_transport_failed":
3526
- return "network transport failed";
3527
- }
3528
- }
3529
- __name(transportMessage, "transportMessage");
3530
-
3531
- // src/client/http.ts
3532
- var HttpError = class extends Error {
3533
- static {
3534
- __name(this, "HttpError");
3535
- }
3536
- executionContext;
3537
- diagnostics = [];
3538
- status;
3539
- code;
3540
- payload;
3541
- constructor(status, payload, fallbackMessage) {
3542
- super(payload?.message || fallbackMessage);
3543
- this.status = status;
3544
- this.code = payload?.code || "unknown_error";
3545
- this.payload = payload;
3546
- }
3547
- };
3548
- var HttpClient = class _HttpClient {
3549
- static {
3550
- __name(this, "HttpClient");
3551
- }
3552
- localSimulation = false;
3553
- static MAX_TRANSPORT_RETRIES = 2;
3554
- baseURL;
3555
- fetchImpl;
3556
- defaultHeaders;
3557
- requestAuthorizer;
3558
- recoverAuthorization;
3559
- transportRetryDelayMs;
3560
- onDiagnostic;
3561
- constructor(config) {
3562
- this.baseURL = config.baseURL.replace(/\/$/, "");
3563
- this.fetchImpl = config.fetchImpl ?? globalThis.fetch;
3564
- this.defaultHeaders = {
3565
- "Content-Type": "application/json",
3566
- Accept: "application/json",
3567
- ...config.defaultHeaders ?? {}
3568
- };
3569
- this.requestAuthorizer = config.requestAuthorizer;
3570
- this.recoverAuthorization = config.recoverAuthorization;
3571
- this.onDiagnostic = config.onDiagnostic;
3572
- this.transportRetryDelayMs = Math.max(0, config.transportRetryDelayMs ?? 200);
3573
- }
3574
- async request(path2, options = {}) {
3575
- const controller = new AbortController();
3576
- const abort = /* @__PURE__ */ __name(() => controller.abort(options.signal?.reason), "abort");
3577
- options.signal?.addEventListener("abort", abort, { once: true });
3578
- if (options.signal?.aborted) abort();
3579
- const remaining = (options.deadline ?? Date.now() + 3e4) - Date.now();
3580
- const timeoutReason = new DOMException("request deadline reached", "TimeoutError");
3581
- if (remaining <= 0) controller.abort(timeoutReason);
3582
- const timer = remaining > 0 ? setTimeout(() => controller.abort(timeoutReason), remaining) : void 0;
3583
- const requestStarted = Date.now();
3584
- const diagnostics = [];
3585
- let abortListener;
3586
- const cancelled = new Promise((_, reject) => {
3587
- abortListener = /* @__PURE__ */ __name(() => reject(controller.signal.reason), "abortListener");
3588
- if (controller.signal.aborted) abortListener();
3589
- else controller.signal.addEventListener("abort", abortListener, { once: true });
3590
- });
3591
- try {
3592
- return await Promise.race([this.performRequest(path2, { ...options, signal: controller.signal }, diagnostics), cancelled]);
3593
- } catch (error) {
3594
- if (controller.signal.aborted && !options.signal?.aborted) {
3595
- const timeout = new HttpTransportError("network_timeout", Math.max(1, diagnostics.length), "DEADLINE_EXCEEDED", error);
3596
- if (diagnostics.length === 0) diagnostics.push({ method: options.method ?? "GET", route_template: safeRouteTemplate(new URL(path2, this.baseURL).pathname), attempt: 1, attempt_ms: Math.max(0, remaining), total_ms: Math.max(0, remaining), phase: "unknown", cause_code: "DEADLINE_EXCEEDED" });
3597
- const last = diagnostics.at(-1);
3598
- if (last) last.total_ms = Date.now() - requestStarted;
3599
- timeout.diagnostics = diagnostics;
3600
- throw timeout;
3601
- }
3602
- if (error instanceof HttpError || error instanceof HttpTransportError) error.diagnostics = diagnostics;
3603
- throw error;
3604
- } finally {
3605
- if (timer !== void 0) clearTimeout(timer);
3606
- options.signal?.removeEventListener("abort", abort);
3607
- if (abortListener) controller.signal.removeEventListener("abort", abortListener);
3608
- }
3609
- }
3610
- async performRequest(path2, options, diagnostics) {
3611
- const url = path2.startsWith("http") ? path2 : this.baseURL + path2;
3612
- const method = options.method ?? "GET";
3613
- const body = options.body !== void 0 ? JSON.stringify(options.body) : "";
3614
- const requestPath = new URL(url).pathname + new URL(url).search;
3615
- const replaySafe = method === "GET" || Boolean(options.idempotencyKey) || options.replaySafe === true;
3616
- const started = Date.now();
3617
- let authorizationRecovered = false;
3618
- let transportRetries = 0;
3619
- for (; ; ) {
3620
- options.signal?.throwIfAborted();
3621
- const attemptStarted = Date.now();
3622
- const attempt = diagnostics.length + 1;
3623
- let phase = "unknown";
3624
- let requestID;
3625
- const diagnostic = { method, route_template: safeRouteTemplate(new URL(url).pathname), attempt, attempt_ms: 0, total_ms: Date.now() - started, phase };
3626
- diagnostics.push(diagnostic);
3627
- const abortAttempt = /* @__PURE__ */ __name(() => record("DEADLINE_OR_ABORT"), "abortAttempt");
3628
- const record = /* @__PURE__ */ __name((causeCode) => {
3629
- options.signal?.removeEventListener("abort", abortAttempt);
3630
- Object.assign(diagnostic, { attempt_ms: Date.now() - attemptStarted, total_ms: Date.now() - started, phase, ...requestID ? { request_id: requestID } : {}, ...causeCode ? { cause_code: causeCode } : {} });
3631
- this.onDiagnostic?.(diagnostic);
3632
- }, "record");
3633
- options.signal?.addEventListener("abort", abortAttempt, { once: true });
3634
- const headers = { ...this.defaultHeaders, ...options.responseType === "png" ? { Accept: "image/png" } : {} };
3635
- try {
3636
- if (this.requestAuthorizer) {
3637
- Object.assign(headers, await this.requestAuthorizer({ method, path: requestPath, body, ...options.signal ? { signal: options.signal } : {}, ...options.deadline !== void 0 ? { deadline: options.deadline } : {} }));
3638
- }
3639
- } catch (error2) {
3640
- record(error2 instanceof HttpTransportError ? error2.causeCode : "AUTHORIZATION_FAILED");
3641
- if (error2 instanceof HttpTransportError) throw error2;
3642
- throw asTransientTransportError(error2, 1) ?? error2;
3643
- }
3644
- options.signal?.throwIfAborted();
3645
- if (options.bearer) headers.Authorization = `Bearer ${options.bearer}`;
3646
- if (options.idempotencyKey) headers["Idempotency-Key"] = options.idempotencyKey;
3647
- let response;
3648
- let text2;
3649
- try {
3650
- response = await this.fetchImpl(url, {
3651
- method,
3652
- headers,
3653
- ...options.responseType === "png" ? { redirect: "error" } : {},
3654
- ...options.body !== void 0 ? { body } : {},
3655
- ...options.signal ? { signal: options.signal } : {}
3656
- });
3657
- phase = "response_body";
3658
- diagnostic.phase = phase;
3659
- const headerID = response.headers.get("X-ItPay-Request-ID");
3660
- if (headerID && /^req_[a-f0-9]{1,64}$/.test(headerID)) {
3661
- requestID = headerID;
3662
- diagnostic.request_id = headerID;
3663
- }
3664
- if (response.ok && options.responseType === "png") {
3665
- const bytes = new Uint8Array(await response.arrayBuffer());
3666
- const magic = [137, 80, 78, 71, 13, 10, 26, 10];
3667
- if (response.headers.get("Content-Type")?.split(";")[0] !== "image/png" || bytes.length > 10 * 1024 * 1024 || !magic.every((value, index) => bytes[index] === value)) throw new Error("invalid official PNG response");
3668
- record();
3669
- return bytes;
3670
- }
3671
- text2 = await response.text();
3672
- this.localSimulation = response.headers.get("X-ItPay-Environment") === "local_simulation";
3673
- } catch (error2) {
3674
- if (options.signal?.aborted) {
3675
- record("DEADLINE_OR_ABORT");
3676
- throw error2;
3677
- }
3678
- const transportError = asTransientTransportError(error2, transportRetries + 1);
3679
- record(transportError?.causeCode ?? "UNKNOWN_TRANSPORT_FAILURE");
3680
- if (!transportError) throw error2;
3681
- if (replaySafe && transportRetries < _HttpClient.MAX_TRANSPORT_RETRIES) {
3682
- transportRetries += 1;
3683
- await delay(this.transportRetryDelayMs * 2 ** (transportRetries - 1), options.signal);
3684
- continue;
3685
- }
3686
- throw asTransientTransportError(error2, transportRetries + 1) ?? error2;
3687
- }
3688
- record(response.ok ? void 0 : `HTTP_${response.status}`);
3689
- const parsed = text2.length > 0 ? safeParseJson(text2) : void 0;
3690
- if (response.ok) return parsed;
3691
- const error = new HttpError(response.status, parsed, `HTTP ${response.status}`);
3692
- if (!authorizationRecovered && error.status === 401 && error.code === "agent_device_session_required" && this.recoverAuthorization) {
3693
- authorizationRecovered = true;
3694
- options.signal?.throwIfAborted();
3695
- await this.recoverAuthorization({ ...options.signal ? { signal: options.signal } : {}, ...options.deadline !== void 0 ? { deadline: options.deadline } : {} });
3696
- options.signal?.throwIfAborted();
3697
- continue;
3698
- }
3699
- throw error;
3700
- }
3701
- }
3702
- get(path2, options = {}) {
3703
- return this.request(path2, { ...options, method: "GET" });
3704
- }
3705
- post(path2, body, options = {}) {
3706
- return this.request(path2, { ...options, method: "POST", body });
3707
- }
3708
- delete(path2, options = {}) {
3709
- return this.request(path2, { ...options, method: "DELETE" });
3710
- }
3711
- };
3712
- function delay(milliseconds, signal) {
3713
- signal?.throwIfAborted();
3714
- if (milliseconds === 0) return Promise.resolve();
3715
- return new Promise((resolve10, reject) => {
3716
- const abort = /* @__PURE__ */ __name(() => {
3717
- clearTimeout(timer);
3718
- reject(signal?.reason);
3719
- }, "abort");
3720
- const timer = setTimeout(() => {
3721
- signal?.removeEventListener("abort", abort);
3722
- resolve10();
3723
- }, milliseconds);
3724
- signal?.addEventListener("abort", abort, { once: true });
3725
- });
3726
- }
3727
- __name(delay, "delay");
3728
- function safeRouteTemplate(path2) {
3729
- const staticSegments = /* @__PURE__ */ new Set(["v1", "v3", "readyz", "service-executions", "service-definitions", "services", "advance", "actions", "checkout", "checkouts", "presentation", "capabilities", "invoke", "result-items", "events", "orders", "refunds", "cancel", "cart", "carts", "items", "quotes", "quote", "agent-device-enrollments", "agent-device-sessions", "verify", "auth-sessions"]);
3730
- return path2.split("/").map((segment) => staticSegments.has(segment) ? segment : segment ? "{value}" : "").join("/");
3731
- }
3732
- __name(safeRouteTemplate, "safeRouteTemplate");
3733
- function safeParseJson(text2) {
3734
- try {
3735
- return JSON.parse(text2);
3736
- } catch {
3737
- return void 0;
3738
- }
3739
- }
3740
- __name(safeParseJson, "safeParseJson");
3741
-
3742
- // src/state/hub_credentials.ts
3743
- import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
3744
- import { chmodSync as chmodSync2, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
3745
- import { dirname as dirname2, resolve } from "node:path";
3746
-
3747
- // src/state/file_lock.ts
3748
- import { chmodSync, lstatSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
3749
- import { dirname } from "node:path";
3750
- import { randomUUID } from "node:crypto";
3751
- var STALE_MS = 3e4;
3752
- function sleepSync(ms) {
3753
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
3754
- }
3755
- __name(sleepSync, "sleepSync");
3756
- function ownerAlive(path2) {
3757
- try {
3758
- const st = lstatSync(path2);
3759
- if (st.isSymbolicLink()) return false;
3760
- if (Date.now() - st.mtimeMs < STALE_MS) return true;
3761
- const token = readFileSync(path2, "utf8");
3762
- const pid = Number(/^(\d+):/.exec(token)?.[1]);
3763
- if (!Number.isInteger(pid) || pid <= 0) return false;
3764
- try {
3765
- process.kill(pid, 0);
3766
- return true;
3767
- } catch {
3768
- return false;
3769
- }
3770
- } catch {
3771
- return false;
3772
- }
3773
- }
3774
- __name(ownerAlive, "ownerAlive");
3775
- function withFileLock(lockPath, fn) {
3776
- mkdirSync(dirname(lockPath), { recursive: true, mode: 448 });
3777
- const token = `${process.pid}:${randomUUID()}`;
3778
- const deadline = Date.now() + 15e3;
3779
- for (; ; ) {
3780
- try {
3781
- writeFileSync(lockPath, token, { encoding: "utf8", flag: "wx", mode: 384 });
3782
- chmodSync(lockPath, 384);
3783
- break;
3784
- } catch (error) {
3785
- const code = error.code;
3786
- if (code !== "EEXIST") throw error;
3787
- if (!ownerAlive(lockPath)) {
3788
- try {
3789
- unlinkSync(lockPath);
3790
- } catch {
3791
- }
3792
- continue;
3793
- }
3794
- if (Date.now() > deadline) throw new Error(`task store lock busy: ${lockPath}`);
3795
- sleepSync(20);
3796
- }
3797
- }
3798
- try {
3799
- return fn();
3800
- } finally {
3801
- try {
3802
- if (readFileSync(lockPath, "utf8") === token) unlinkSync(lockPath);
3803
- } catch {
3804
- }
3805
- }
3806
- }
3807
- __name(withFileLock, "withFileLock");
3808
- async function withAsyncFileLock(lockPath, fn) {
3809
- mkdirSync(dirname(lockPath), { recursive: true, mode: 448 });
3810
- const token = `${process.pid}:${randomUUID()}`;
3811
- const deadline = Date.now() + 2e4;
3812
- for (; ; ) {
3813
- try {
3814
- writeFileSync(lockPath, token, { encoding: "utf8", flag: "wx", mode: 384 });
3815
- break;
3816
- } catch (error) {
3817
- if (error.code !== "EEXIST") throw error;
3818
- if (!ownerAlive(lockPath)) {
3819
- try {
3820
- unlinkSync(lockPath);
3821
- } catch {
3822
- }
3823
- continue;
3824
- }
3825
- if (Date.now() > deadline) throw new Error("credential refresh lock busy");
3826
- await new Promise((resolve10) => setTimeout(resolve10, 20));
3827
- }
3828
- }
3829
- try {
3830
- return await fn();
3831
- } finally {
3832
- try {
3833
- if (readFileSync(lockPath, "utf8") === token) unlinkSync(lockPath);
3834
- } catch {
3835
- }
3836
- }
3837
- }
3838
- __name(withAsyncFileLock, "withAsyncFileLock");
3839
-
3840
- // src/state/hub_oauth.ts
3841
- import { createHash, randomBytes } from "node:crypto";
3842
- import { createServer } from "node:http";
3843
- function hubOAuthConfig(env = process.env) {
3844
- const devHub = (env.ITPAY_HUB_URL ?? "").replace(/\/+$/, "") === "https://market-v4-dev.itpay.ai";
3845
- const issuer = (env.ITPAY_HUB_OAUTH_ISSUER ?? (devHub ? "https://auth-v4-dev.itpay.ai" : "http://localhost:24444")).replace(/\/+$/, "");
3846
- return {
3847
- authorizeURL: `${issuer}/oauth2/auth`,
3848
- tokenURL: `${issuer}/oauth2/token`,
3849
- clientID: env.ITPAY_HUB_OAUTH_CLIENT ?? "itpay-cli-local",
3850
- resource: (env.ITPAY_HUB_URL ?? "http://localhost:18090").replace(/\/+$/, ""),
3851
- scope: env.ITPAY_HUB_OAUTH_SCOPE ?? "openid offline_access itpay.hub"
3852
- };
3853
- }
3854
- __name(hubOAuthConfig, "hubOAuthConfig");
3855
- function pkce() {
3856
- const verifier = randomBytes(32).toString("base64url");
3857
- const challenge = createHash("sha256").update(verifier).digest("base64url");
3858
- return { verifier, challenge };
3859
- }
3860
- __name(pkce, "pkce");
3861
- async function runConnectFlow(cfg, timeoutMs = 3e5) {
3862
- const { verifier, challenge } = pkce();
3863
- const state = randomBytes(16).toString("hex");
3864
- const server = createServer();
3865
- const done = new Promise((resolvePromise, reject) => {
3866
- const timer = setTimeout(() => {
3867
- server.close();
3868
- reject(new Error("connect timed out waiting for browser authorization"));
3869
- }, timeoutMs);
3870
- server.on("request", (req, res) => {
3871
- const u = new URL(req.url ?? "/", "http://127.0.0.1");
3872
- const finish = /* @__PURE__ */ __name((ok, msg) => {
3873
- res.writeHead(ok ? 200 : 400, { "Content-Type": "text/html; charset=utf-8" });
3874
- res.end(`<!doctype html><meta charset="utf-8"><title>ItPay</title><body style="font-family:system-ui;padding:3em;text-align:center"><h2>${msg.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;")}</h2><p>\u53EF\u4EE5\u5173\u95ED\u6B64\u9875\u9762\uFF0C\u56DE\u5230\u7EC8\u7AEF\u7EE7\u7EED\u3002</p></body>`);
3875
- }, "finish");
3876
- (async () => {
3877
- if (u.pathname !== "/callback") return finish(false, "\u65E0\u6548\u7684\u56DE\u8C03");
3878
- if (u.searchParams.get("state") !== state) return finish(false, "state \u4E0D\u5339\u914D");
3879
- if (u.searchParams.get("error")) {
3880
- clearTimeout(timer);
3881
- server.close();
3882
- finish(false, "\u6388\u6743\u88AB\u62D2\u7EDD");
3883
- reject(new Error("authorization_required: consent denied"));
3884
- return;
3885
- }
3886
- const code = u.searchParams.get("code");
3887
- if (!code) {
3888
- clearTimeout(timer);
3889
- server.close();
3890
- finish(false, "\u7F3A\u5C11 code");
3891
- reject(new Error("missing code"));
3892
- return;
3893
- }
3894
- const form = new URLSearchParams({
3895
- grant_type: "authorization_code",
3896
- code,
3897
- redirect_uri: `http://127.0.0.1:${server.address().port}/callback`,
3898
- client_id: cfg.clientID,
3899
- code_verifier: verifier,
3900
- resource: cfg.resource
3901
- });
3902
- const tok = await fetch(cfg.tokenURL, {
3903
- method: "POST",
3904
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
3905
- body: form,
3906
- signal: AbortSignal.timeout(1e4)
3907
- });
3908
- if (!tok.ok) {
3909
- clearTimeout(timer);
3910
- server.close();
3911
- finish(false, "token \u4EA4\u6362\u5931\u8D25");
3912
- reject(new Error(`token endpoint ${tok.status}`));
3913
- return;
3914
- }
3915
- const t = await tok.json();
3916
- clearTimeout(timer);
3917
- server.close();
3918
- finish(true, "ItPay \u5DF2\u8FDE\u63A5");
3919
- resolvePromise({ accessToken: t.access_token, refreshToken: t.refresh_token, expiresIn: t.expires_in });
3920
- })().catch((e) => {
3921
- clearTimeout(timer);
3922
- server.close();
3923
- reject(e);
3924
- });
3925
- });
3926
- });
3927
- await new Promise((resolvePromise, reject) => {
3928
- server.once("error", reject);
3929
- server.listen(0, "127.0.0.1", () => resolvePromise());
3930
- });
3931
- const port = server.address().port;
3932
- const authorizeURL = `${cfg.authorizeURL}?` + new URLSearchParams({
3933
- response_type: "code",
3934
- client_id: cfg.clientID,
3935
- redirect_uri: `http://127.0.0.1:${port}/callback`,
3936
- scope: cfg.scope,
3937
- state,
3938
- code_challenge: challenge,
3939
- code_challenge_method: "S256",
3940
- resource: cfg.resource
3941
- }).toString();
3942
- return { authorizeURL, done };
3943
- }
3944
- __name(runConnectFlow, "runConnectFlow");
3945
-
3946
- // src/state/hub_credentials.ts
3947
- import { homedir } from "node:os";
3948
- function hubCredentialsPath(env = process.env) {
3949
- const dir = env.ITPAY_STATE_DIR ? resolve(env.ITPAY_STATE_DIR) : resolve(env.HOME || homedir(), ".itpay-v3");
3950
- const suffix = createHash2("sha256").update(env.ITPAY_HUB_URL ?? "http://localhost:18090").digest("hex").slice(0, 12);
3951
- return resolve(dir, `hub-credentials.${suffix}.json`);
3952
- }
3953
- __name(hubCredentialsPath, "hubCredentialsPath");
3954
- function importHubKeyUnlocked(apiKey, baseURL, env = process.env) {
3955
- if (!/^itk_[A-Za-z0-9_-]{8,}$/.test(apiKey)) {
3956
- throw new Error("invalid key format \u2014 expected an itk_ API key");
3988
+ code;
3989
+ attempts;
3990
+ causeCode;
3991
+ static {
3992
+ __name(this, "HttpTransportError");
3957
3993
  }
3958
- const path2 = hubCredentialsPath(env);
3959
- const file = existsSync(path2) ? JSON.parse(readFileSync2(path2, "utf8")) : { schema_version: "itpay.hub_credentials.v1", profiles: {} };
3960
- file.profiles["default"] = {
3961
- base_url: baseURL,
3962
- key_prefix: apiKey.slice(0, 14),
3963
- api_key: apiKey,
3964
- created_at: (/* @__PURE__ */ new Date()).toISOString()
3965
- };
3966
- saveHubProfileUnlocked(file.profiles["default"], env);
3967
- return { key_prefix: apiKey.slice(0, 14) };
3994
+ retryable = true;
3995
+ diagnostics = [];
3996
+ };
3997
+ function asTransientTransportError(error, attempts) {
3998
+ if (isAbort(error)) return void 0;
3999
+ const causeCode = findCauseCode(error);
4000
+ const code = causeCode ? classifyCauseCode(causeCode) : classifyFetchFailure(error);
4001
+ return code ? new HttpTransportError(code, attempts, causeCode, error) : void 0;
3968
4002
  }
3969
- __name(importHubKeyUnlocked, "importHubKeyUnlocked");
3970
- function storedHubKey(env = process.env) {
3971
- const path2 = hubCredentialsPath(env);
3972
- if (!existsSync(path2)) return void 0;
3973
- try {
3974
- const file = JSON.parse(readFileSync2(path2, "utf8"));
3975
- return file.profiles["default"]?.api_key;
3976
- } catch {
3977
- return void 0;
4003
+ __name(asTransientTransportError, "asTransientTransportError");
4004
+ function classifyCauseCode(code) {
4005
+ switch (code) {
4006
+ case "ECONNRESET":
4007
+ return "network_connection_reset";
4008
+ case "ETIMEDOUT":
4009
+ case "UND_ERR_CONNECT_TIMEOUT":
4010
+ case "UND_ERR_HEADERS_TIMEOUT":
4011
+ case "UND_ERR_BODY_TIMEOUT":
4012
+ return "network_timeout";
4013
+ case "EAI_AGAIN":
4014
+ return "network_dns_temporary";
4015
+ case "ENETUNREACH":
4016
+ case "EHOSTUNREACH":
4017
+ return "network_unreachable";
4018
+ case "ECONNREFUSED":
4019
+ return "network_connection_refused";
4020
+ case "UND_ERR_SOCKET":
4021
+ return "network_socket_error";
4022
+ default:
4023
+ return void 0;
3978
4024
  }
3979
4025
  }
3980
- __name(storedHubKey, "storedHubKey");
3981
- function storedHubKeyPrefix(env = process.env) {
3982
- const path2 = hubCredentialsPath(env);
3983
- if (!existsSync(path2)) return void 0;
3984
- try {
3985
- const file = JSON.parse(readFileSync2(path2, "utf8"));
3986
- return file.profiles["default"]?.key_prefix;
3987
- } catch {
3988
- return void 0;
4026
+ __name(classifyCauseCode, "classifyCauseCode");
4027
+ function classifyFetchFailure(error) {
4028
+ return error instanceof TypeError && error.message === "fetch failed" ? "network_transport_failed" : void 0;
4029
+ }
4030
+ __name(classifyFetchFailure, "classifyFetchFailure");
4031
+ function findCauseCode(error) {
4032
+ const seen = /* @__PURE__ */ new Set();
4033
+ let current = error;
4034
+ while (current && typeof current === "object" && !seen.has(current)) {
4035
+ seen.add(current);
4036
+ const code = current.code;
4037
+ if (typeof code === "string" && code !== "") return code;
4038
+ current = current.cause;
3989
4039
  }
4040
+ return void 0;
3990
4041
  }
3991
- __name(storedHubKeyPrefix, "storedHubKeyPrefix");
3992
- function storeHubSessionUnlocked(accessToken, refreshToken, expiresIn, subject, baseURL, env = process.env, consumerID) {
3993
- const path2 = hubCredentialsPath(env);
3994
- const file = existsSync(path2) ? JSON.parse(readFileSync2(path2, "utf8")) : { schema_version: "itpay.hub_credentials.v1", profiles: {} };
3995
- const issuedAt = Date.now();
3996
- const margin = Math.min(3e4, Math.max(100, expiresIn * 100));
3997
- file.profiles["default"] = {
3998
- base_url: baseURL,
3999
- access_token: accessToken,
4000
- ...refreshToken ? { refresh_token: refreshToken } : {},
4001
- expires_at: new Date(issuedAt + expiresIn * 1e3).toISOString(),
4002
- refresh_after: new Date(issuedAt + expiresIn * 1e3 - margin).toISOString(),
4003
- ...subject ? { subject } : {},
4004
- consumer_id: consumerID ?? `oauth:${hubOAuthConfig(env).clientID}`,
4005
- client_id: hubOAuthConfig(env).clientID,
4006
- token_url: hubOAuthConfig(env).tokenURL,
4007
- created_at: (/* @__PURE__ */ new Date()).toISOString()
4008
- };
4009
- saveHubProfileUnlocked(file.profiles["default"], env);
4042
+ __name(findCauseCode, "findCauseCode");
4043
+ function isAbort(error) {
4044
+ return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
4010
4045
  }
4011
- __name(storeHubSessionUnlocked, "storeHubSessionUnlocked");
4012
- function saveHubProfileUnlocked(profile, env) {
4013
- const path2 = hubCredentialsPath(env);
4014
- const tmp = `${path2}.${randomUUID2()}.tmp`;
4015
- try {
4016
- const file = existsSync(path2) ? JSON.parse(readFileSync2(path2, "utf8")) : { schema_version: "itpay.hub_credentials.v1", profiles: {} };
4017
- file.profiles.default = profile;
4018
- mkdirSync2(dirname2(path2), { recursive: true, mode: 448 });
4019
- writeFileSync2(tmp, JSON.stringify(file, null, 2), { encoding: "utf8", flag: "wx", mode: 384 });
4020
- chmodSync2(tmp, 384);
4021
- renameSync(tmp, path2);
4022
- } catch {
4023
- throw new Error("credential_persistence_failed: could not save the original connection; stop and check private storage before retrying");
4024
- } finally {
4025
- try {
4026
- unlinkSync2(tmp);
4027
- } catch {
4028
- }
4046
+ __name(isAbort, "isAbort");
4047
+ function transportMessage(code) {
4048
+ switch (code) {
4049
+ case "network_connection_reset":
4050
+ return "network connection was reset";
4051
+ case "network_timeout":
4052
+ return "network connection timed out";
4053
+ case "network_dns_temporary":
4054
+ return "DNS lookup was temporarily unavailable";
4055
+ case "network_unreachable":
4056
+ return "network was unreachable";
4057
+ case "network_connection_refused":
4058
+ return "network connection was refused";
4059
+ case "network_socket_error":
4060
+ return "network socket failed";
4061
+ case "network_transport_failed":
4062
+ return "network transport failed";
4029
4063
  }
4030
4064
  }
4031
- __name(saveHubProfileUnlocked, "saveHubProfileUnlocked");
4032
- async function probeHubIdentity(resource, accessToken) {
4033
- let response;
4034
- try {
4035
- response = await fetch(`${resource}/v1/me`, {
4036
- headers: { Authorization: `Bearer ${accessToken}`, "X-ItPay-OAuth-Resource": resource },
4037
- signal: AbortSignal.timeout(1e4)
4038
- });
4039
- } catch {
4040
- throw new Error("connection_verification_pending: identity check unavailable; preserve the original connection and references, do not reconnect");
4065
+ __name(transportMessage, "transportMessage");
4066
+
4067
+ // src/client/http.ts
4068
+ var HttpError = class extends Error {
4069
+ static {
4070
+ __name(this, "HttpError");
4041
4071
  }
4042
- if (!response.ok) {
4043
- if (response.status === 401 || response.status === 403) throw new Error("authorization_required: original connection identity rejected");
4044
- throw new Error("connection_verification_pending: identity check unavailable; preserve the original connection and references, do not reconnect");
4072
+ executionContext;
4073
+ diagnostics = [];
4074
+ status;
4075
+ code;
4076
+ payload;
4077
+ constructor(status, payload, fallbackMessage) {
4078
+ super(payload?.message || fallbackMessage);
4079
+ this.status = status;
4080
+ this.code = payload?.code || "unknown_error";
4081
+ this.payload = payload;
4045
4082
  }
4046
- try {
4047
- const identity = await response.json();
4048
- if (!identity || typeof identity !== "object" || Array.isArray(identity)) throw new Error();
4049
- const me = identity;
4050
- if (typeof me.buyer_id !== "string" || !me.buyer_id || typeof me.consumer_id !== "string" || !me.consumer_id) throw new Error();
4051
- return { buyer_id: me.buyer_id, consumer_id: me.consumer_id };
4052
- } catch {
4053
- throw new Error("connection_verification_pending: identity response invalid; preserve the original connection and references, do not reconnect");
4083
+ };
4084
+ var HttpClient = class _HttpClient {
4085
+ static {
4086
+ __name(this, "HttpClient");
4054
4087
  }
4055
- }
4056
- __name(probeHubIdentity, "probeHubIdentity");
4057
- function storedHubToken(env = process.env) {
4058
- const path2 = hubCredentialsPath(env);
4059
- if (!existsSync(path2)) return void 0;
4060
- try {
4061
- const file = JSON.parse(readFileSync2(path2, "utf8"));
4062
- const p = file.profiles["default"];
4063
- if (!p?.access_token || p.identity_verification_pending) return void 0;
4064
- if (p.expires_at && Date.parse(p.expires_at) <= Date.now()) return void 0;
4065
- return p.access_token;
4066
- } catch {
4067
- return void 0;
4088
+ localSimulation = false;
4089
+ static MAX_TRANSPORT_RETRIES = 2;
4090
+ baseURL;
4091
+ fetchImpl;
4092
+ defaultHeaders;
4093
+ requestAuthorizer;
4094
+ recoverAuthorization;
4095
+ transportRetryDelayMs;
4096
+ onDiagnostic;
4097
+ constructor(config) {
4098
+ this.baseURL = config.baseURL.replace(/\/$/, "");
4099
+ this.fetchImpl = config.fetchImpl ?? globalThis.fetch;
4100
+ this.defaultHeaders = {
4101
+ "Content-Type": "application/json",
4102
+ Accept: "application/json",
4103
+ ...config.defaultHeaders ?? {}
4104
+ };
4105
+ this.requestAuthorizer = config.requestAuthorizer;
4106
+ this.recoverAuthorization = config.recoverAuthorization;
4107
+ this.onDiagnostic = config.onDiagnostic;
4108
+ this.transportRetryDelayMs = Math.max(0, config.transportRetryDelayMs ?? 200);
4068
4109
  }
4069
- }
4070
- __name(storedHubToken, "storedHubToken");
4071
- function storedHubProfile(env = process.env) {
4072
- const path2 = hubCredentialsPath(env);
4073
- if (!existsSync(path2)) return void 0;
4074
- return JSON.parse(readFileSync2(path2, "utf8")).profiles.default;
4075
- }
4076
- __name(storedHubProfile, "storedHubProfile");
4077
- async function connectedHubToken(env = process.env, rejectedToken) {
4078
- return withAsyncFileLock(`${hubCredentialsPath(env)}.refresh.lock`, async () => {
4079
- const p = storedHubProfile(env);
4080
- if (!p?.access_token || !p.subject) throw new Error("authorization_required: reconnect ItPay");
4081
- const cfg = hubOAuthConfig(env);
4082
- if (p.base_url !== cfg.resource || p.token_url !== cfg.tokenURL || p.client_id !== cfg.clientID || !p.consumer_id) {
4083
- throw new Error("authorization_required: connection configuration changed");
4110
+ async request(path2, options = {}) {
4111
+ const controller = new AbortController();
4112
+ const abort = /* @__PURE__ */ __name(() => controller.abort(options.signal?.reason), "abort");
4113
+ options.signal?.addEventListener("abort", abort, { once: true });
4114
+ if (options.signal?.aborted) abort();
4115
+ const remaining = (options.deadline ?? Date.now() + 3e4) - Date.now();
4116
+ const timeoutReason = new DOMException("request deadline reached", "TimeoutError");
4117
+ if (remaining <= 0) controller.abort(timeoutReason);
4118
+ const timer = remaining > 0 ? setTimeout(() => controller.abort(timeoutReason), remaining) : void 0;
4119
+ const requestStarted = Date.now();
4120
+ const diagnostics = [];
4121
+ let abortListener;
4122
+ const cancelled = new Promise((_, reject) => {
4123
+ abortListener = /* @__PURE__ */ __name(() => reject(controller.signal.reason), "abortListener");
4124
+ if (controller.signal.aborted) abortListener();
4125
+ else controller.signal.addEventListener("abort", abortListener, { once: true });
4126
+ });
4127
+ try {
4128
+ return await Promise.race([this.performRequest(path2, { ...options, signal: controller.signal }, diagnostics), cancelled]);
4129
+ } catch (error) {
4130
+ if (controller.signal.aborted && !options.signal?.aborted) {
4131
+ const timeout = new HttpTransportError("network_timeout", Math.max(1, diagnostics.length), "DEADLINE_EXCEEDED", error);
4132
+ if (diagnostics.length === 0) diagnostics.push({ method: options.method ?? "GET", route_template: safeRouteTemplate(new URL(path2, this.baseURL).pathname), attempt: 1, attempt_ms: Math.max(0, remaining), total_ms: Math.max(0, remaining), phase: "unknown", cause_code: "DEADLINE_EXCEEDED" });
4133
+ const last = diagnostics.at(-1);
4134
+ if (last) last.total_ms = Date.now() - requestStarted;
4135
+ timeout.diagnostics = diagnostics;
4136
+ throw timeout;
4137
+ }
4138
+ if (error instanceof HttpError || error instanceof HttpTransportError) error.diagnostics = diagnostics;
4139
+ throw error;
4140
+ } finally {
4141
+ if (timer !== void 0) clearTimeout(timer);
4142
+ options.signal?.removeEventListener("abort", abort);
4143
+ if (abortListener) controller.signal.removeEventListener("abort", abortListener);
4084
4144
  }
4085
- const refreshAt = p.refresh_after ? Date.parse(p.refresh_after) : Date.parse(p.expires_at ?? "") - 3e4;
4086
- if (!p.identity_verification_pending && p.access_token !== rejectedToken && refreshAt > Date.now()) return p.access_token;
4087
- let candidate = p;
4088
- if (!p.identity_verification_pending || !(Date.parse(p.expires_at ?? "") > Date.now())) {
4089
- if (!p.refresh_token) throw new Error("authorization_required: reconnect the original ItPay connection");
4090
- let response;
4145
+ }
4146
+ async performRequest(path2, options, diagnostics) {
4147
+ const url = path2.startsWith("http") ? path2 : this.baseURL + path2;
4148
+ const method = options.method ?? "GET";
4149
+ const body = options.body !== void 0 ? JSON.stringify(options.body) : "";
4150
+ const requestPath = new URL(url).pathname + new URL(url).search;
4151
+ const replaySafe = method === "GET" || Boolean(options.idempotencyKey) || options.replaySafe === true;
4152
+ const started = Date.now();
4153
+ let authorizationRecovered = false;
4154
+ let transportRetries = 0;
4155
+ for (; ; ) {
4156
+ options.signal?.throwIfAborted();
4157
+ const attemptStarted = Date.now();
4158
+ const attempt = diagnostics.length + 1;
4159
+ let phase = "unknown";
4160
+ let requestID;
4161
+ const diagnostic = { method, route_template: safeRouteTemplate(new URL(url).pathname), attempt, attempt_ms: 0, total_ms: Date.now() - started, phase };
4162
+ diagnostics.push(diagnostic);
4163
+ const abortAttempt = /* @__PURE__ */ __name(() => record("DEADLINE_OR_ABORT"), "abortAttempt");
4164
+ const record = /* @__PURE__ */ __name((causeCode) => {
4165
+ options.signal?.removeEventListener("abort", abortAttempt);
4166
+ Object.assign(diagnostic, { attempt_ms: Date.now() - attemptStarted, total_ms: Date.now() - started, phase, ...requestID ? { request_id: requestID } : {}, ...causeCode ? { cause_code: causeCode } : {} });
4167
+ this.onDiagnostic?.(diagnostic);
4168
+ }, "record");
4169
+ options.signal?.addEventListener("abort", abortAttempt, { once: true });
4170
+ const headers = { ...this.defaultHeaders, ...options.responseType === "png" ? { Accept: "image/png" } : {} };
4091
4171
  try {
4092
- response = await fetch(p.token_url, {
4093
- method: "POST",
4094
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
4095
- body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: p.refresh_token, client_id: p.client_id, resource: cfg.resource }),
4096
- signal: AbortSignal.timeout(1e4)
4097
- });
4098
- } catch {
4099
- throw new Error("connection_verification_pending: refresh outcome unknown; preserve the original connection and references, do not repeat authorization or business requests");
4100
- }
4101
- if (!response.ok) {
4102
- let rejected = response.status === 401 || response.status === 403;
4103
- if (response.status === 400) {
4104
- try {
4105
- const problem = await response.json();
4106
- rejected = ["invalid_grant", "invalid_client", "unauthorized_client"].includes(problem?.error ?? "");
4107
- } catch {
4108
- }
4172
+ if (this.requestAuthorizer) {
4173
+ Object.assign(headers, await this.requestAuthorizer({ method, path: requestPath, body, ...options.signal ? { signal: options.signal } : {}, ...options.deadline !== void 0 ? { deadline: options.deadline } : {} }));
4109
4174
  }
4110
- if (rejected) throw new Error("authorization_required: connection refresh rejected");
4111
- throw new Error("connection_verification_pending: refresh unavailable; preserve the original connection and references");
4175
+ } catch (error2) {
4176
+ record(error2 instanceof HttpTransportError ? error2.causeCode : "AUTHORIZATION_FAILED");
4177
+ if (error2 instanceof HttpTransportError) throw error2;
4178
+ throw asTransientTransportError(error2, 1) ?? error2;
4112
4179
  }
4113
- let token;
4180
+ options.signal?.throwIfAborted();
4181
+ if (options.bearer) headers.Authorization = `Bearer ${options.bearer}`;
4182
+ if (options.idempotencyKey) headers["Idempotency-Key"] = options.idempotencyKey;
4183
+ let response;
4184
+ let text2;
4114
4185
  try {
4115
- token = await response.json();
4116
- } catch {
4117
- throw new Error("connection_verification_pending: invalid refresh response; preserve the original connection and references");
4186
+ response = await this.fetchImpl(url, {
4187
+ method,
4188
+ headers,
4189
+ ...options.responseType === "png" ? { redirect: "error" } : {},
4190
+ ...options.body !== void 0 ? { body } : {},
4191
+ ...options.signal ? { signal: options.signal } : {}
4192
+ });
4193
+ phase = "response_body";
4194
+ diagnostic.phase = phase;
4195
+ const headerID = response.headers.get("X-ItPay-Request-ID");
4196
+ if (headerID && /^req_[a-f0-9]{1,64}$/.test(headerID)) {
4197
+ requestID = headerID;
4198
+ diagnostic.request_id = headerID;
4199
+ }
4200
+ if (response.ok && options.responseType === "png") {
4201
+ const bytes = new Uint8Array(await response.arrayBuffer());
4202
+ const magic = [137, 80, 78, 71, 13, 10, 26, 10];
4203
+ if (response.headers.get("Content-Type")?.split(";")[0] !== "image/png" || bytes.length > 10 * 1024 * 1024 || !magic.every((value, index) => bytes[index] === value)) throw new Error("invalid official PNG response");
4204
+ record();
4205
+ return bytes;
4206
+ }
4207
+ text2 = await response.text();
4208
+ this.localSimulation = response.headers.get("X-ItPay-Environment") === "local_simulation";
4209
+ } catch (error2) {
4210
+ if (options.signal?.aborted) {
4211
+ record("DEADLINE_OR_ABORT");
4212
+ throw error2;
4213
+ }
4214
+ const transportError = asTransientTransportError(error2, transportRetries + 1);
4215
+ record(transportError?.causeCode ?? "UNKNOWN_TRANSPORT_FAILURE");
4216
+ if (!transportError) throw error2;
4217
+ if (replaySafe && transportRetries < _HttpClient.MAX_TRANSPORT_RETRIES) {
4218
+ transportRetries += 1;
4219
+ await delay(this.transportRetryDelayMs * 2 ** (transportRetries - 1), options.signal);
4220
+ continue;
4221
+ }
4222
+ throw asTransientTransportError(error2, transportRetries + 1) ?? error2;
4118
4223
  }
4119
- if (!token || typeof token.access_token !== "string" || !token.access_token || typeof token.token_type !== "string" || token.token_type.toLowerCase() !== "bearer" || typeof token.expires_in !== "number" || !Number.isFinite(token.expires_in) || token.expires_in <= 0 || (!Number.isFinite(Date.now() + token.expires_in * 1e3) || Date.now() + token.expires_in * 1e3 > 864e13) || token.refresh_token !== void 0 && (typeof token.refresh_token !== "string" || !token.refresh_token)) {
4120
- throw new Error("connection_verification_pending: invalid refresh response; preserve the original connection and references");
4224
+ record(response.ok ? void 0 : `HTTP_${response.status}`);
4225
+ const parsed = text2.length > 0 ? safeParseJson(text2) : void 0;
4226
+ if (response.ok) return parsed;
4227
+ const error = new HttpError(response.status, parsed, `HTTP ${response.status}`);
4228
+ if (!authorizationRecovered && error.status === 401 && error.code === "agent_device_session_required" && this.recoverAuthorization) {
4229
+ authorizationRecovered = true;
4230
+ options.signal?.throwIfAborted();
4231
+ await this.recoverAuthorization({ ...options.signal ? { signal: options.signal } : {}, ...options.deadline !== void 0 ? { deadline: options.deadline } : {} });
4232
+ options.signal?.throwIfAborted();
4233
+ continue;
4121
4234
  }
4122
- const now = Date.now();
4123
- candidate = {
4124
- ...p,
4125
- access_token: token.access_token,
4126
- refresh_token: token.refresh_token ?? p.refresh_token,
4127
- expires_at: new Date(now + token.expires_in * 1e3).toISOString(),
4128
- refresh_after: new Date(now + token.expires_in * 1e3 - Math.min(3e4, Math.max(100, token.expires_in * 100))).toISOString(),
4129
- identity_verification_pending: true
4130
- };
4131
- saveHubProfileUnlocked(candidate, env);
4132
- }
4133
- const identity = await probeHubIdentity(cfg.resource, candidate.access_token);
4134
- if (identity.buyer_id !== p.subject || identity.consumer_id !== p.consumer_id) {
4135
- throw new Error("authorization_required: refreshed identity changed");
4136
- }
4137
- if (!(Date.parse(candidate.expires_at ?? "") > Date.now())) {
4138
- throw new Error("connection_verification_pending: token expired during identity check; preserve the original connection and references");
4235
+ throw error;
4139
4236
  }
4140
- delete candidate.identity_verification_pending;
4141
- saveHubProfileUnlocked(candidate, env);
4142
- return candidate.access_token;
4237
+ }
4238
+ get(path2, options = {}) {
4239
+ return this.request(path2, { ...options, method: "GET" });
4240
+ }
4241
+ post(path2, body, options = {}) {
4242
+ return this.request(path2, { ...options, method: "POST", body });
4243
+ }
4244
+ delete(path2, options = {}) {
4245
+ return this.request(path2, { ...options, method: "DELETE" });
4246
+ }
4247
+ };
4248
+ function delay(milliseconds, signal) {
4249
+ signal?.throwIfAborted();
4250
+ if (milliseconds === 0) return Promise.resolve();
4251
+ return new Promise((resolve10, reject) => {
4252
+ const abort = /* @__PURE__ */ __name(() => {
4253
+ clearTimeout(timer);
4254
+ reject(signal?.reason);
4255
+ }, "abort");
4256
+ const timer = setTimeout(() => {
4257
+ signal?.removeEventListener("abort", abort);
4258
+ resolve10();
4259
+ }, milliseconds);
4260
+ signal?.addEventListener("abort", abort, { once: true });
4143
4261
  });
4144
4262
  }
4145
- __name(connectedHubToken, "connectedHubToken");
4146
- function importHubKey(apiKey, baseURL, env = process.env) {
4147
- return withFileLock(`${hubCredentialsPath(env)}.refresh.lock`, () => importHubKeyUnlocked(apiKey, baseURL, env));
4263
+ __name(delay, "delay");
4264
+ function safeRouteTemplate(path2) {
4265
+ const staticSegments = /* @__PURE__ */ new Set(["v1", "v3", "readyz", "service-executions", "service-definitions", "services", "advance", "actions", "checkout", "checkouts", "presentation", "capabilities", "invoke", "result-items", "events", "orders", "refunds", "cancel", "cart", "carts", "items", "quotes", "quote", "agent-device-enrollments", "agent-device-sessions", "verify", "auth-sessions"]);
4266
+ return path2.split("/").map((segment) => staticSegments.has(segment) ? segment : segment ? "{value}" : "").join("/");
4148
4267
  }
4149
- __name(importHubKey, "importHubKey");
4150
- function storeHubSession(accessToken, refreshToken, expiresIn, subject, baseURL, env = process.env, consumerID) {
4151
- withFileLock(`${hubCredentialsPath(env)}.refresh.lock`, () => storeHubSessionUnlocked(accessToken, refreshToken, expiresIn, subject, baseURL, env, consumerID));
4268
+ __name(safeRouteTemplate, "safeRouteTemplate");
4269
+ function safeParseJson(text2) {
4270
+ try {
4271
+ return JSON.parse(text2);
4272
+ } catch {
4273
+ return void 0;
4274
+ }
4152
4275
  }
4153
- __name(storeHubSession, "storeHubSession");
4276
+ __name(safeParseJson, "safeParseJson");
4154
4277
 
4155
4278
  // src/client/hub.ts
4156
4279
  function loadHubConfig(env = process.env) {
@@ -4202,6 +4325,9 @@ var HubClient2 = class {
4202
4325
  runStatus(runID) {
4203
4326
  return this.wrap(() => this.sdk.getRun(runID));
4204
4327
  }
4328
+ getRequest(requestID) {
4329
+ return this.wrap(() => this.sdk.getRequest(requestID));
4330
+ }
4205
4331
  continueIntent(intentID, idempotencyKey) {
4206
4332
  return this.wrap(() => this.sdk.continueIntent(intentID, idempotencyKey));
4207
4333
  }
@@ -4254,13 +4380,47 @@ var HubClient2 = class {
4254
4380
  if (err instanceof HubApiError) {
4255
4381
  throw new HttpError(err.status, {
4256
4382
  code: err.envelope.error.code,
4257
- message: err.envelope.error.message
4383
+ message: err.envelope.error.message,
4384
+ ...safeDetailsPayload(err.envelope.error.details)
4258
4385
  }, `hub request failed`);
4259
4386
  }
4260
4387
  throw err;
4261
4388
  }
4262
4389
  }
4263
4390
  };
4391
+ function safeHubErrorDetails(raw) {
4392
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
4393
+ const d = raw;
4394
+ const out = {};
4395
+ if (d.stage === "metadata" || d.stage === "status" || d.stage === "action") out.stage = d.stage;
4396
+ if (typeof d.method === "string" && ["GET", "POST", "PUT", "DELETE", "PATCH"].includes(d.method)) out.method = d.method;
4397
+ if (typeof d.path === "string") {
4398
+ const path2 = d.path.split(/[?#]/, 1)[0];
4399
+ const routes = [
4400
+ [/^\/v1\/operations\/[^/\s]+\/runs$/, "/v1/operations/{operation_id}/runs"],
4401
+ [/^\/v1\/operations\/[^/\s]+$/, "/v1/operations/{operation_id}"],
4402
+ [/^\/v1\/requests\/[^/\s]+$/, "/v1/requests/{request_id}"],
4403
+ [/^\/v1\/runs\/[^/\s]+$/, "/v1/runs/{run_id}"],
4404
+ [/^\/v1\/intents\/[^/\s]+\/continue$/, "/v1/intents/{intent_id}/continue"]
4405
+ ];
4406
+ const matched = routes.find(([pattern]) => pattern.test(path2));
4407
+ if (matched) out.path = matched[1];
4408
+ else if (["/v1/discover", "/v1/me", "/v1/me/wallet", "/v1/me/usage"].includes(path2)) out.path = path2;
4409
+ }
4410
+ return Object.keys(out).length ? out : void 0;
4411
+ }
4412
+ __name(safeHubErrorDetails, "safeHubErrorDetails");
4413
+ function safeDetailsPayload(raw) {
4414
+ const details = safeHubErrorDetails(raw);
4415
+ return details ? { details } : {};
4416
+ }
4417
+ __name(safeDetailsPayload, "safeDetailsPayload");
4418
+ function hubErrorDetails(error) {
4419
+ if (error instanceof HttpError) return safeHubErrorDetails(error.payload?.details);
4420
+ if (error && typeof error === "object" && "details" in error) return safeHubErrorDetails(error.details);
4421
+ return void 0;
4422
+ }
4423
+ __name(hubErrorDetails, "hubErrorDetails");
4264
4424
 
4265
4425
  // src/commands/admin.ts
4266
4426
  var actions = /* @__PURE__ */ new Set(["operations_list", "ingestions_list", "import_github", "ingestion_status", "ingestion_retry", "credentials_list", "credential_put", "source_requirements", "source_bind", "acceptance_status", "acceptance_set"]);
@@ -4446,8 +4606,11 @@ var HubTaskStore = class {
4446
4606
  if (!existsSync2(file)) return void 0;
4447
4607
  return withFileLock(`${file}.lock`, () => {
4448
4608
  const existing = this.readTask(file);
4449
- if (STATUS_RANK[existing.status] >= STATUS_RANK.succeeded) return existing;
4450
- const next = { ...existing, ...patch, request_id: requestID, updated_at: (/* @__PURE__ */ new Date()).toISOString() };
4609
+ if (existing.run_id && patch.run_id && existing.run_id !== patch.run_id || existing.intent_id && patch.intent_id && existing.intent_id !== patch.intent_id) {
4610
+ throw new Error("invalid_state: authoritative receipt changed the original task binding");
4611
+ }
4612
+ const terminal = STATUS_RANK[existing.status] >= STATUS_RANK.succeeded;
4613
+ const next = { ...existing, ...patch, ...terminal ? { status: existing.status, terminal: true, pending_stage: null, next: null, blocking_reason: void 0 } : {}, request_id: requestID, updated_at: (/* @__PURE__ */ new Date()).toISOString() };
4451
4614
  this.writeTask(file, next);
4452
4615
  return next;
4453
4616
  });
@@ -4466,7 +4629,7 @@ var HubTaskStore = class {
4466
4629
  }
4467
4630
  /** Bind a migrated/legacy task to the current scope on explicit access. */
4468
4631
  bindScope(task, file) {
4469
- if (task.hub_origin && task.key_prefix !== void 0) return task;
4632
+ if (task.hub_origin && (task.key_prefix !== void 0 || !this.scopeFields.key_prefix)) return task;
4470
4633
  const bound = { ...task, ...this.scopeFields, updated_at: (/* @__PURE__ */ new Date()).toISOString() };
4471
4634
  this.writeTask(file, bound);
4472
4635
  return bound;
@@ -5827,19 +5990,60 @@ function readHubCredentialSelection(credentialID, sourceSpec) {
5827
5990
  return { source_credentials: Object.fromEntries(entries) };
5828
5991
  }
5829
5992
  __name(readHubCredentialSelection, "readHubCredentialSelection");
5830
- async function waitForRun(client, runID, seconds) {
5993
+ function runTerminal(rec) {
5994
+ if (typeof rec.terminal === "boolean") return rec.terminal;
5995
+ return ["succeeded", "failed", "cancelled"].includes(rec.execution_state) && ["free", "captured", "released", "refunded", "partially_refunded"].includes(rec.settlement_state);
5996
+ }
5997
+ __name(runTerminal, "runTerminal");
5998
+ function runPendingStage(rec) {
5999
+ if (runTerminal(rec)) return null;
6000
+ if (rec.pending_stage) return rec.pending_stage;
6001
+ if (["succeeded", "failed", "cancelled"].includes(rec.execution_state)) return "settlement";
6002
+ if (rec.execution_state === "unknown" || rec.settlement_state === "reconciling") return "reconciliation";
6003
+ return rec.execution_state === "queued" || rec.execution_state === "accepted" ? "queued" : "unknown";
6004
+ }
6005
+ __name(runPendingStage, "runPendingStage");
6006
+ function updateFromRun(store, task, rec) {
6007
+ if (!rec.run_id || rec.operation_id && rec.operation_id !== task.operation_id || rec.publication_id && rec.publication_id !== task.publication_id) {
6008
+ throw new Error("invalid_state: authoritative run does not match the original task");
6009
+ }
6010
+ const terminal = runTerminal(rec);
6011
+ return store.update(task.request_id, {
6012
+ run_id: rec.run_id,
6013
+ blocking_reason: void 0,
6014
+ status: terminal ? rec.execution_state === "succeeded" ? "succeeded" : "failed" : rec.execution_state === "unknown" ? "unknown" : "dispatched",
6015
+ terminal,
6016
+ pending_stage: runPendingStage(rec),
6017
+ next: terminal ? null : { read_only: { method: "GET", path: `/v1/runs/${encodeURIComponent(rec.run_id)}` } },
6018
+ state_source: "hub",
6019
+ state_stale: (task.terminal === true || ["succeeded", "failed"].includes(task.status)) && !terminal,
6020
+ last_checked_at: (/* @__PURE__ */ new Date()).toISOString(),
6021
+ refresh_error: (task.terminal === true || ["succeeded", "failed"].includes(task.status)) && !terminal ? "inconsistent_receipt" : void 0
6022
+ });
6023
+ }
6024
+ __name(updateFromRun, "updateFromRun");
6025
+ function validateWait(seconds) {
6026
+ if (!Number.isFinite(seconds) || !Number.isFinite(seconds * 1e3 + Date.now()) || seconds < 0) throw new Error("invalid_input: --wait must be a finite nonnegative number of seconds");
6027
+ }
6028
+ __name(validateWait, "validateWait");
6029
+ async function waitForRun(client, runID, seconds, initial) {
6030
+ validateWait(seconds);
5831
6031
  const deadline = Date.now() + seconds * 1e3;
5832
6032
  let delay3 = 500;
6033
+ if (initial && (runTerminal(initial) || seconds === 0)) return initial;
6034
+ let latest = initial;
5833
6035
  for (; ; ) {
6036
+ if (latest && Date.now() >= deadline) return latest;
5834
6037
  const rec = await client.runStatus(runID);
5835
- if (["succeeded", "failed", "cancelled"].includes(rec.execution_state)) return rec;
5836
- if (Date.now() >= deadline) return rec;
5837
- await new Promise((r) => setTimeout(r, delay3));
6038
+ latest = rec;
6039
+ if (runTerminal(rec) || Date.now() >= deadline) return rec;
6040
+ await new Promise((r) => setTimeout(r, Math.min(delay3, Math.max(0, deadline - Date.now()))));
5838
6041
  delay3 = Math.min(delay3 * 2, 5e3);
5839
6042
  }
5840
6043
  }
5841
6044
  __name(waitForRun, "waitForRun");
5842
6045
  async function runHubRun(operationID2, options) {
6046
+ validateWait(options.waitSeconds ?? 0);
5843
6047
  const selections = readHubCredentialSelection(options.credentialId, options.sourceCredentials);
5844
6048
  const client = hub();
5845
6049
  const store = new HubTaskStore();
@@ -5854,11 +6058,11 @@ async function runHubRun(operationID2, options) {
5854
6058
  ...options.maxCharge ? { max_charge_atoms: options.maxCharge } : {}
5855
6059
  });
5856
6060
  if (!created && task.input_digest !== digest) {
5857
- throw new Error(`idempotency_conflict: request ${task.request_id} already exists with different input/publication/credential selection \u2014 use a new --request-id or "itpay api resume ${task.request_id}"`);
6061
+ throw new Error(`idempotency_conflict: original request ${task.request_id} has different frozen facts; preserve its identity, input and publication, do not replace it with a new request`);
5858
6062
  }
5859
6063
  if (!created && task.run_id) {
5860
6064
  const rec = await waitForRun(client, task.run_id, options.waitSeconds ?? 0);
5861
- store.update(task.request_id, { status: rec.execution_state === "succeeded" ? "succeeded" : rec.execution_state === "failed" ? "failed" : "dispatched" });
6065
+ updateFromRun(store, task, rec);
5862
6066
  renderRun(rec, options.jsonOutput, "replayed");
5863
6067
  return;
5864
6068
  }
@@ -5876,9 +6080,11 @@ var HubTaskRequestError = class extends Error {
5876
6080
  requestID;
5877
6081
  references;
5878
6082
  code;
6083
+ details;
5879
6084
  constructor(error, task) {
5880
6085
  super(error instanceof Error ? error.message : "original Hub request did not return a receipt");
5881
6086
  this.requestID = task.request_id;
6087
+ this.details = hubErrorDetails(error);
5882
6088
  this.references = { request_id: task.request_id, ...task.run_id ? { run_id: task.run_id } : {}, ...task.intent_id ? { intent_id: task.intent_id } : {} };
5883
6089
  this.code = error instanceof HttpError ? error.code : ["authorization_required", "connection_verification_pending", "credential_persistence_failed", "idempotency_conflict", "invalid_state"].find((code) => this.message.startsWith(code + ":")) ?? "hub_request_unknown";
5884
6090
  }
@@ -5906,13 +6112,11 @@ async function dispatchOriginalTask(client, store, task, options) {
5906
6112
  return;
5907
6113
  }
5908
6114
  let rec = admission;
5909
- store.update(task.request_id, { run_id: rec.run_id });
6115
+ updateFromRun(store, task, rec);
5910
6116
  if (options.waitSeconds) {
5911
- rec = await waitForRun(client, rec.run_id, options.waitSeconds);
6117
+ rec = await waitForRun(client, rec.run_id, options.waitSeconds, rec);
5912
6118
  }
5913
- store.update(task.request_id, {
5914
- status: rec.execution_state === "succeeded" ? "succeeded" : rec.execution_state === "failed" ? "failed" : "dispatched"
5915
- });
6119
+ updateFromRun(store, task, rec);
5916
6120
  renderRun(rec, options.jsonOutput);
5917
6121
  } catch (error) {
5918
6122
  throw new HubTaskRequestError(error, store.get(task.request_id) ?? task);
@@ -5923,29 +6127,43 @@ function renderRun(rec, jsonOutput, note) {
5923
6127
  const charged = rec.actual_charge ? atomsToYuan(rec.actual_charge.amount_atoms) : "\u8D39\u7528\u56DE\u6267\u672A\u63D0\u4F9B";
5924
6128
  writeCommandEnvelope({
5925
6129
  status: rec.execution_state === "succeeded" ? "executed" : rec.execution_state,
5926
- result: { run_id: rec.run_id, execution_state: rec.execution_state, settlement_state: rec.settlement_state, ...rec.delivery_state ? { delivery_state: rec.delivery_state } : {}, actual_charge: rec.actual_charge ?? null, result: rec.result ?? null, error: rec.error ?? null, ...note ? { note } : {} },
6130
+ result: { terminal: runTerminal(rec), pending_stage: runPendingStage(rec), next: runTerminal(rec) ? null : rec.next ?? { read_only: { method: "GET", path: `/v1/runs/${encodeURIComponent(rec.run_id)}` } }, run_id: rec.run_id, execution_state: rec.execution_state, settlement_state: rec.settlement_state, ...rec.delivery_state ? { delivery_state: rec.delivery_state } : {}, actual_charge: rec.actual_charge ?? null, result: rec.result ?? null, error: rec.error ?? null, first_error: rec.first_error ?? null, ...note ? { note } : {} },
5927
6131
  instruction: rec.execution_state === "succeeded" ? `\u6267\u884C\u72B6\u6001\u4E3A succeeded\uFF1B\u4EA4\u4ED8\u72B6\u6001 ${rec.delivery_state ?? "\u672A\u63D0\u4F9B"}\uFF0C\u7ED3\u7B97\u72B6\u6001 ${rec.settlement_state ?? "\u672A\u63D0\u4F9B"}\uFF0C\u5B9E\u9645\u8D39\u7528\u56DE\u6267 ${charged}\u3002\u4EC5\u636E\u5B9E\u9645 result \u548C\u4EA4\u4ED8\u4E8B\u5B9E\u56DE\u7B54\uFF0C\u7A7A\u503C\u4E0D\u4EE3\u8868\u67E5\u8BE2\u6210\u529F\u4E14\u96F6\u6761\u3002` : `\u6267\u884C\u72B6\u6001 ${rec.execution_state}\uFF08${rec.error?.code ?? "\u65E0\u9519\u8BEF\u4EE3\u7801"}\uFF09\uFF0C\u7ED3\u7B97\u72B6\u6001 ${rec.settlement_state ?? "\u672A\u63D0\u4F9B"}\uFF0C\u5B9E\u9645\u8D39\u7528\u56DE\u6267 ${charged}\uFF1B\u7528 itpay api run-status ${rec.run_id} \u67E5\u8BE2\u539F\u4EFB\u52A1\uFF0C\u4E0D\u91CD\u590D\u53D1\u8D77\u3002`,
5928
- next: ["succeeded", "failed"].includes(rec.execution_state) ? null : { command: `itpay api run-status ${rec.run_id}`, reason: "\u67E5\u8BE2\u539F run \u72B6\u6001\uFF0C\u4E0D\u65B0\u5EFA\u8BF7\u6C42" },
6132
+ next: runTerminal(rec) ? null : { command: `itpay api run-status ${rec.run_id}`, reason: "\u53EA\u8BFB\u67E5\u8BE2\u539F run \u72B6\u6001\uFF0C\u4E0D\u65B0\u5EFA\u8BF7\u6C42", read_only: { method: "GET", path: `/v1/runs/${encodeURIComponent(rec.run_id)}` } },
5929
6133
  recovery: []
5930
6134
  }, {
5931
6135
  jsonOutput: Boolean(jsonOutput),
5932
- plainResult: [`run ${rec.run_id} ${rec.execution_state} ${rec.settlement_state} ${charged}${note ? " (" + note + ")" : ""}`]
6136
+ plainResult: [`run ${rec.run_id} ${rec.execution_state} ${rec.settlement_state} ${charged}${note ? " (" + note + ")" : ""}`, ...rec.first_error ? [`first_error: ${rec.first_error.code}`] : []]
5933
6137
  });
5934
6138
  }
5935
6139
  __name(renderRun, "renderRun");
5936
6140
  async function renderBlockedTask(task, admission, jsonOutput) {
5937
- if (!admission && task.intent_id) {
5938
- try {
5939
- const payment2 = await hub().getAPIPaymentRequest(task.intent_id);
5940
- admission = { intent_id: task.intent_id, blocking_reason: "insufficient_balance", payment_request: payment2, required_action: payment2.collection_action };
5941
- } catch {
6141
+ if (!admission) {
6142
+ const store = new HubTaskStore();
6143
+ const stored = store.get(task.request_id);
6144
+ if (!stored) throw new Error("not_found: original local task is unavailable");
6145
+ const snapshot = await refreshOriginalTask(hub(), store, stored);
6146
+ const updated = store.get(task.request_id);
6147
+ if (snapshot?.run) {
6148
+ renderRun(snapshot.run, jsonOutput);
6149
+ return;
5942
6150
  }
6151
+ writeCommandEnvelope({
6152
+ status: updated.state_stale ? "unknown" : updated.blocking_reason ?? "blocked",
6153
+ result: { request_id: task.request_id, intent_id: updated.intent_id, run_id: updated.run_id, terminal: updated.terminal ?? false, pending_stage: updated.pending_stage ?? "unknown", state_stale: updated.state_stale, next: snapshot?.next ?? null },
6154
+ instruction: "\u539F\u8BF7\u6C42\u53EA\u8BFB\u72B6\u6001\uFF1B\u672A\u53D6\u5F97\u5F53\u524D\u5B98\u65B9\u529E\u7406\u52A8\u4F5C\u4E0D\u4EE3\u8868\u4F59\u989D\u4E0D\u8DB3\u3002\u4FDD\u7559\u539F\u5F15\u7528\uFF0C\u505C\u6B62\u81EA\u52A8\u6062\u590D\u6216\u5145\u503C\u3002",
6155
+ next: { command: `itpay api tasks --refresh --request-id ${task.request_id}${jsonOutput ? " --json" : ""}`, reason: "\u53EA\u8BFB\u5237\u65B0\u8FD9\u4E00\u7B14\u539F\u5F15\u7528", read_only: { method: "GET", path: `/v1/requests/${encodeURIComponent(store.idempotencyKey(task.request_id))}` } },
6156
+ recovery: []
6157
+ }, { jsonOutput: Boolean(jsonOutput) });
6158
+ return;
5943
6159
  }
5944
6160
  const intentID = admission?.intent_id ?? task.intent_id;
5945
6161
  const action = admission?.required_action;
5946
6162
  const reason = admission?.blocking_reason ?? "blocked";
5947
6163
  const payment = admission?.payment_request;
5948
6164
  const paymentRequired = payment && /^[1-9][0-9]*$/.test(String(payment.payer_minor_amount ?? ""));
6165
+ const readOnlyNext = { command: `itpay api tasks --refresh --request-id ${task.request_id}${jsonOutput ? " --json" : ""}`, reason: "\u53EA\u8BFB\u6838\u5BF9\u539F\u8BF7\u6C42", read_only: { method: "GET", path: `/v1/requests/${encodeURIComponent("cli-req-" + task.request_id)}` } };
6166
+ const progress = { terminal: false, pending_stage: admission?.pending_stage ?? (["authorization_denied", "authorization_revoked", "budget_exceeded"].includes(reason) ? "authorization" : reason === "insufficient_balance" ? "funding" : "unknown"), next: admission?.next ?? { read_only: readOnlyNext.read_only } };
5949
6167
  if (action && !paymentRequired) {
5950
6168
  let target2;
5951
6169
  try {
@@ -5955,11 +6173,18 @@ async function renderBlockedTask(task, admission, jsonOutput) {
5955
6173
  }
5956
6174
  const approval = ["authorization_denied", "budget_exceeded"].includes(reason);
5957
6175
  const instruction = approval ? "\u8BF7\u5728\u5B98\u65B9\u9875\u9762\u6838\u5BF9\u8BE5 Agent\u3001API \u6548\u679C\u8303\u56F4\u548C\u6D88\u8D39\u9884\u7B97\uFF0C\u660E\u786E\u6279\u51C6\u540E\u6062\u590D\u540C\u4E00\u539F\u8BF7\u6C42\u3002\u6D88\u8D39\u6388\u6743\u4E0D\u4F1A\u5145\u503C\u6216\u81EA\u52A8\u6263\u6B3E\u3002" : reason === "insufficient_balance" ? "\u4F59\u989D\u4E0D\u8DB3\uFF1B\u8BF7\u5728\u5B98\u65B9\u5145\u503C\u9875\u6838\u5BF9\u539F\u8BF7\u6C42\u548C\u5B9E\u9645\u91D1\u989D\uFF0C\u5230\u8D26\u540E\u6062\u590D\u540C\u4E00\u539F\u8BF7\u6C42\u3002" : "\u8BF7\u5728\u5B98\u65B9\u9875\u9762\u5B8C\u6210\u6240\u9700\u64CD\u4F5C\uFF0C\u518D\u67E5\u8BE2\u540C\u4E00\u539F\u8BF7\u6C42\u3002";
5958
- writeCommandEnvelope({ status: reason, result: { request_id: task.request_id, intent_id: intentID, ...admission ?? {} }, ...target2 ? { handoff: { type: "open_url", url: target2, label: action.label ?? "\u6253\u5F00\u5B98\u65B9\u9875\u9762" } } : {}, instruction: target2 ? instruction : "\u672A\u53D6\u5F97\u53EF\u7528\u5B98\u65B9\u64CD\u4F5C\u94FE\u63A5\uFF1B\u4FDD\u7559\u539F\u8BF7\u6C42\u5E76\u6838\u5BF9\u5F53\u524D\u72B6\u6001\u3002", next: target2 ? { command: qualifyItPayCommand(`itpay api resume ${task.request_id}${jsonOutput ? " --json" : ""}`, declaredAgentType()), reason: approval ? "\u5B8C\u6210\u6D88\u8D39\u6388\u6743\u540E\u6062\u590D\u540C\u4E00\u539F\u8BF7\u6C42" : "\u5B8C\u6210\u5B98\u65B9\u64CD\u4F5C\u540E\u67E5\u8BE2\u540C\u4E00\u539F\u8BF7\u6C42" } : null, recovery: [] }, { jsonOutput: Boolean(jsonOutput), plainResult: [`request ${task.request_id} blocked intent ${intentID ?? "-"}`, ...target2 ? [target2] : []] });
6176
+ writeCommandEnvelope({ status: reason, result: { request_id: task.request_id, intent_id: intentID, ...admission ?? {}, ...progress }, ...target2 ? { handoff: { type: "open_url", url: target2, label: action.label ?? "\u6253\u5F00\u5B98\u65B9\u9875\u9762" } } : {}, instruction: target2 ? instruction : "\u672A\u53D6\u5F97\u53EF\u7528\u5B98\u65B9\u64CD\u4F5C\u94FE\u63A5\uFF1B\u4FDD\u7559\u539F\u8BF7\u6C42\u5E76\u6838\u5BF9\u5F53\u524D\u72B6\u6001\u3002", next: { command: `itpay api tasks --refresh --request-id ${task.request_id}${jsonOutput ? " --json" : ""}`, reason: "\u53EA\u8BFB\u6838\u5BF9\u539F\u8BF7\u6C42", read_only: { method: "GET", path: `/v1/requests/${encodeURIComponent("cli-req-" + task.request_id)}` } }, recovery: target2 ? [{ command: qualifyItPayCommand(`itpay api resume ${task.request_id}${jsonOutput ? " --json" : ""}`, declaredAgentType()), reason: approval ? "\u4EC5\u5B8C\u6210\u6D88\u8D39\u6388\u6743\u540E\u7EED\u8DD1\u540C\u4E00\u539F\u8BF7\u6C42\uFF1B\u6B64\u52A8\u4F5C\u53EF\u80FD\u5199\u5165" : "\u4EC5\u5B8C\u6210\u5B98\u65B9\u64CD\u4F5C\u4E14\u660E\u786E\u5141\u8BB8\u6062\u590D\u540E\u7EED\u8DD1\u539F\u8BF7\u6C42\uFF1B\u6B64\u52A8\u4F5C\u53EF\u80FD\u5199\u5165" }] : [] }, { jsonOutput: Boolean(jsonOutput), plainResult: [`request ${task.request_id} blocked intent ${intentID ?? "-"}`, ...target2 ? [target2] : []] });
5959
6177
  return;
5960
6178
  }
5961
6179
  const handoff = action && paymentRequired ? await renderPaymentHandoff({ action, requestID: task.request_id, amountMinor: Number(admission?.payment_request?.payer_minor_amount ?? 0), currency: admission?.payment_request?.payer_currency ?? "CNY", jsonOutput: Boolean(jsonOutput) }) : null;
5962
- writeCommandEnvelope({ status: admission?.blocking_reason ?? "blocked", result: { request_id: task.request_id, intent_id: intentID, ...admission ?? {}, ...handoff ? { qr_kind: action.branded === true ? "branded_payment_card" : "official_checkout_link" } : {} }, ...handoff ? { handoff: handoff.handoff } : {}, instruction: handoff?.instruction ?? "\u5F53\u524D\u6CA1\u6709\u53EF\u7528\u7684\u5B98\u65B9\u54C1\u724C\u4ED8\u6B3E\u5361\u7247\u6216\u6536\u94F6\u53F0\u94FE\u63A5\u3002\u4FDD\u7559\u539F\u8BF7\u6C42\uFF0C\u67E5\u8BE2\u72B6\u6001\uFF1B\u4E0D\u8981\u751F\u6210\u6216\u5C55\u793A raw QR\u3002", next: handoff?.next ?? { command: `itpay api resume ${task.request_id}`, reason: "\u6062\u590D\u540C\u4E00\u539F\u8BF7\u6C42" }, recovery: [] }, { jsonOutput: Boolean(jsonOutput), plainResult: [`request ${task.request_id} blocked intent ${intentID ?? "-"}`, ...handoff ? [JSON.stringify(handoff.handoff)] : []] });
6180
+ writeCommandEnvelope({
6181
+ status: reason,
6182
+ result: { request_id: task.request_id, intent_id: intentID, ...admission, ...progress, ...handoff ? { qr_kind: action.branded === true ? "branded_payment_card" : "official_checkout_link" } : {} },
6183
+ ...handoff ? { handoff: handoff.handoff } : {},
6184
+ instruction: handoff?.instruction ?? "\u5C1A\u65E0\u5F53\u524D\u53EF\u7528\u7684\u5B98\u65B9\u529E\u7406\u52A8\u4F5C\u3002\u4FDD\u7559\u539F\u8BF7\u6C42\uFF0C\u53EA\u8BFB\u6838\u5BF9\u72B6\u6001\uFF1B\u4E0D\u63A8\u6D4B\u4F59\u989D\u3001\u4E0D\u5145\u503C\u6216\u91CD\u53D1\u3002",
6185
+ next: readOnlyNext,
6186
+ recovery: handoff?.next ? [{ ...handoff.next, reason: "\u4EC5\u5B8C\u6210\u539F\u5B98\u65B9\u4ED8\u6B3E\u4E14\u5141\u8BB8\u7EE7\u7EED\u540E\u6062\u590D\u540C\u4E00\u8BF7\u6C42\uFF1B\u6B64\u52A8\u4F5C\u53EF\u80FD\u5199\u5165" }] : []
6187
+ }, { jsonOutput: Boolean(jsonOutput), plainResult: [`request ${task.request_id} blocked intent ${intentID ?? "-"}`, ...handoff ? [JSON.stringify(handoff.handoff)] : []] });
5963
6188
  }
5964
6189
  __name(renderBlockedTask, "renderBlockedTask");
5965
6190
  async function runHubWallet(options) {
@@ -6134,8 +6359,8 @@ async function runHubContinue(intentID, options) {
6134
6359
  writeCommandEnvelope({
6135
6360
  status: res.state ?? res.status ?? "open",
6136
6361
  result: res,
6137
- instruction: res.run && res.run.execution_state === "succeeded" ? `\u8FD0\u884C\u5B8C\u6210\uFF0C\u5DF2\u6309\u4EF7\u683C\u5361\u7ED3\u7B97 ${res.run.actual_charge ? atomsToYuan(res.run.actual_charge.amount_atoms) : "\xA50.000000"}\u3002` : "\u7EED\u8DD1\u672A\u6210\u529F\u6216\u4ECD\u5728\u9014\u4E2D\uFF1B\u540C\u4E00 intent_id \u53EF\u5B89\u5168\u91CD\u8BD5\u3002",
6138
- next: null,
6362
+ instruction: res.run && runTerminal(res.run) ? `\u539F\u8FD0\u884C\u5DF2\u7ED3\u675F\uFF0C\u5B9E\u9645\u8D39\u7528 ${res.run.actual_charge ? atomsToYuan(res.run.actual_charge.amount_atoms) : "\u56DE\u6267\u672A\u63D0\u4F9B"}\uFF1B\u53EA\u636E\u6743\u5A01\u4EA4\u4ED8\u4E8B\u5B9E\u56DE\u7B54\u3002` : "\u539F intent \u5DF2\u63D0\u4EA4\u7EED\u8DD1\u6216\u4ECD\u5F85\u6240\u9700\u64CD\u4F5C\uFF1B\u4FDD\u7559\u539F\u5F15\u7528\uFF0C\u4EC5\u53EA\u8BFB\u6838\u5BF9\uFF0C\u4E0D\u81EA\u52A8\u518D\u6B21 POST \u6216\u5145\u503C\u3002",
6363
+ next: res.run && !runTerminal(res.run) ? { command: `itpay api run-status ${res.run.run_id}${options.jsonOutput ? " --json" : ""}`, reason: "\u53EA\u8BFB\u67E5\u8BE2\u539F\u8FD0\u884C", read_only: { method: "GET", path: `/v1/runs/${encodeURIComponent(res.run.run_id)}` } } : null,
6139
6364
  recovery: []
6140
6365
  }, {
6141
6366
  jsonOutput: Boolean(options.jsonOutput),
@@ -6144,6 +6369,7 @@ async function runHubContinue(intentID, options) {
6144
6369
  }
6145
6370
  __name(runHubContinue, "runHubContinue");
6146
6371
  async function runHubResume(requestID, options) {
6372
+ validateWait(options.waitSeconds ?? 0);
6147
6373
  const client = hub();
6148
6374
  const store = new HubTaskStore();
6149
6375
  const task = store.get(requestID);
@@ -6153,15 +6379,17 @@ async function runHubResume(requestID, options) {
6153
6379
  try {
6154
6380
  if (task.run_id) {
6155
6381
  const rec = await waitForRun(client, task.run_id, options.waitSeconds ?? 0);
6156
- store.update(requestID, { status: rec.execution_state === "succeeded" ? "succeeded" : rec.execution_state === "failed" ? "failed" : "dispatched" });
6382
+ updateFromRun(store, task, rec);
6157
6383
  renderRun(rec, options.jsonOutput);
6158
6384
  return;
6159
6385
  }
6160
6386
  if (task.intent_id) {
6161
6387
  const res = await client.continueIntent(task.intent_id, `cli-continue-${requestID}`);
6162
6388
  if (res.run) {
6163
- store.update(requestID, { run_id: res.run.run_id, status: res.run.execution_state === "succeeded" ? "succeeded" : "dispatched" });
6164
- renderRun(res.run, options.jsonOutput);
6389
+ updateFromRun(store, task, res.run);
6390
+ const rec = await waitForRun(client, res.run.run_id, options.waitSeconds ?? 0, res.run);
6391
+ updateFromRun(store, task, rec);
6392
+ renderRun(rec, options.jsonOutput);
6165
6393
  } else {
6166
6394
  const state = res.blocking_reason ?? res.state ?? res.status ?? "blocked";
6167
6395
  store.update(requestID, { blocking_reason: state });
@@ -6179,22 +6407,65 @@ async function runHubResume(requestID, options) {
6179
6407
  }
6180
6408
  }
6181
6409
  __name(runHubResume, "runHubResume");
6410
+ async function refreshOriginalTask(client, store, task) {
6411
+ try {
6412
+ const snapshot = await client.getRequest(store.idempotencyKey(task.request_id));
6413
+ if (snapshot.request_id !== store.idempotencyKey(task.request_id) || task.intent_id && snapshot.intent_id && task.intent_id !== snapshot.intent_id || task.run_id && snapshot.run?.run_id !== task.run_id) {
6414
+ throw new Error("invalid_state: authoritative request binding differs");
6415
+ }
6416
+ if (snapshot.run) {
6417
+ if (snapshot.run.operation_id !== task.operation_id || snapshot.run.publication_id !== task.publication_id) throw new Error("invalid_state: authoritative request changed the frozen operation/publication");
6418
+ updateFromRun(store, task, snapshot.run);
6419
+ } else if (snapshot.intent) store.update(task.request_id, {
6420
+ intent_id: snapshot.intent.intent_id,
6421
+ blocking_reason: snapshot.intent.blocking_reason,
6422
+ status: snapshot.terminal && snapshot.intent.status !== "succeeded" ? "failed" : snapshot.intent.status === "open" ? "blocked" : "unknown",
6423
+ terminal: snapshot.terminal,
6424
+ pending_stage: snapshot.pending_stage,
6425
+ next: snapshot.terminal ? null : { read_only: { method: "GET", path: `/v1/requests/${encodeURIComponent(store.idempotencyKey(task.request_id))}` } },
6426
+ state_source: "hub",
6427
+ state_stale: false,
6428
+ last_checked_at: (/* @__PURE__ */ new Date()).toISOString(),
6429
+ refresh_error: void 0
6430
+ });
6431
+ else throw new Error("invalid_state: original request has no authoritative binding");
6432
+ return snapshot;
6433
+ } catch (error) {
6434
+ store.update(task.request_id, { status: "unknown", blocking_reason: void 0, terminal: false, pending_stage: "unknown", next: { read_only: { method: "GET", path: `/v1/requests/${encodeURIComponent(store.idempotencyKey(task.request_id))}` } }, state_source: "hub", state_stale: true, last_checked_at: (/* @__PURE__ */ new Date()).toISOString(), refresh_error: error instanceof HttpError && error.status === 404 ? "not_found" : error instanceof Error && error.message.startsWith("invalid_state:") ? "binding_conflict" : "unavailable" });
6435
+ return void 0;
6436
+ }
6437
+ }
6438
+ __name(refreshOriginalTask, "refreshOriginalTask");
6182
6439
  async function runHubTasks(options) {
6183
- const tasks = new HubTaskStore().list();
6440
+ const store = new HubTaskStore();
6441
+ const tasks = options.requestID ? [store.get(options.requestID)].filter((t) => !!t) : store.list();
6442
+ const selected = tasks.slice(0, 20);
6443
+ if (options.refresh) {
6444
+ const client = hub();
6445
+ for (const task of selected) await refreshOriginalTask(client, store, task);
6446
+ }
6447
+ const refreshed = new Set(options.refresh ? selected.map((t) => t.request_id) : []);
6448
+ const items = tasks.map((task) => {
6449
+ const current = store.get(task.request_id);
6450
+ return { ...current, terminal: current.terminal ?? false, pending_stage: current.terminal ? null : current.pending_stage ?? "unknown", next: current.terminal ? null : current.next ?? { read_only: { method: "GET", path: `/v1/requests/${encodeURIComponent(store.idempotencyKey(task.request_id))}` } }, ...!refreshed.has(task.request_id) ? { state_stale: true, state_source: "local" } : {} };
6451
+ });
6184
6452
  writeCommandEnvelope({
6185
6453
  status: "listed",
6186
- result: { items: tasks },
6187
- instruction: "\u672C\u5730\u4EFB\u52A1\u8D26\u672C\uFF1Bitpay api resume <request_id> \u6062\u590D\uFF0Citpay api run-status <run_id> \u67E5\u8BE2\u3002",
6454
+ result: { items, refresh_attempted_count: refreshed.size, refreshed_count: items.filter((t) => refreshed.has(t.request_id) && !t.state_stale).length, refresh_limit: 20 },
6455
+ instruction: options.refresh ? "\u53EA\u8BFB\u5237\u65B0\u539F\u8BF7\u6C42\uFF1Bunknown/stale\u8868\u793A\u672A\u53D6\u5F97\u5F53\u524D\u6743\u5A01\u72B6\u6001\uFF0C\u4E0D\u4EE3\u8868\u9700\u8981\u5145\u503C\u6216\u53EF\u4EE5\u91CD\u53D1\u3002\u6BCF\u6B21\u6700\u591A20\u6761\uFF1B\u53EF\u7528 --request-id \u9650\u5B9A\u539F\u5F15\u7528\u3002" : "\u672C\u5730\u4EFB\u52A1\u5FEB\u7167\uFF0C\u53EF\u80FD\u5DF2\u8FC7\u671F\uFF1B\u7528 tasks --refresh \u53EA\u8BFB\u6838\u5BF9\u6743\u5A01\u72B6\u6001\u3002\u6B64\u5217\u8868\u4E0D\u6388\u6743\u7EED\u8DD1\u6216\u5145\u503C\u3002",
6188
6456
  next: null,
6189
6457
  recovery: []
6190
- }, {
6191
- jsonOutput: Boolean(options.jsonOutput),
6192
- plainResult: tasks.map((t) => `${t.request_id} ${t.operation_id} ${t.status} ${t.run_id ?? t.intent_id ?? "-"}`)
6193
- });
6458
+ }, { jsonOutput: Boolean(options.jsonOutput), plainResult: items.map((t) => `${t.request_id} ${t.operation_id} ${t.status} ${t.run_id ?? t.intent_id ?? "-"}${t.state_stale ? " stale" : ""}`) });
6194
6459
  }
6195
6460
  __name(runHubTasks, "runHubTasks");
6196
6461
  async function runHubRunStatus(runID, options) {
6197
6462
  const rec = await hub().runStatus(runID);
6463
+ let store;
6464
+ try {
6465
+ store = new HubTaskStore();
6466
+ } catch {
6467
+ }
6468
+ if (store) for (const task of store.list().filter((t) => t.run_id === runID)) updateFromRun(store, task, rec);
6198
6469
  renderRun(rec, options.jsonOutput);
6199
6470
  }
6200
6471
  __name(runHubRunStatus, "runHubRunStatus");
@@ -6231,63 +6502,82 @@ async function runHubCredentialsImport(options) {
6231
6502
  }
6232
6503
  __name(runHubCredentialsImport, "runHubCredentialsImport");
6233
6504
  async function runHubConnect(options) {
6234
- const cfg = hubOAuthConfig();
6235
- const { authorizeURL, done } = await runConnectFlow(cfg);
6236
- if (options.jsonOutput) process.stderr.write(`Open to authorize ItPay: ${authorizeURL}
6505
+ return withNewHubSession(async (storeNewHubSession) => {
6506
+ if (storedHubProfile()) throw new Error("connection_recovery_denied: existing connection preserved; use api recover-connection for exact-binding recovery, do not replace it");
6507
+ const cfg = hubOAuthConfig();
6508
+ const { authorizeURL, done } = await runConnectFlow(cfg);
6509
+ if (options.jsonOutput) process.stderr.write(`Open to authorize ItPay: ${authorizeURL}
6237
6510
  `);
6238
- else writeCommandEnvelope({
6239
- status: "authorization_required",
6240
- result: {
6241
- authorize_url: authorizeURL,
6242
- client_id: cfg.clientID,
6243
- resource: cfg.resource
6244
- },
6245
- instruction: "\u5728\u6D4F\u89C8\u5668\u5B8C\u6210\u4E00\u6B21\u6388\u6743\uFF1BCLI \u7B49\u5F85\u56DE\u8C03\u540E\u81EA\u52A8\u7ED1\u5B9A\u3002",
6246
- next: null,
6247
- recovery: []
6248
- }, {
6249
- jsonOutput: Boolean(options.jsonOutput),
6250
- plainResult: ["\u6253\u5F00\u6D4F\u89C8\u5668\u5B8C\u6210\u6388\u6743\uFF1A", ` ${authorizeURL}`]
6251
- });
6252
- const tokens = await done;
6253
- let me;
6254
- try {
6255
- me = await probeHubIdentity(cfg.resource, tokens.accessToken);
6256
- } catch (error) {
6257
- if (error instanceof Error && error.message.startsWith("connection_verification_pending:")) {
6258
- throw new Error("connection_not_saved: identity verification unavailable; this connection was not saved and the existing connection was not replaced");
6511
+ else writeCommandEnvelope({
6512
+ status: "authorization_required",
6513
+ result: {
6514
+ authorize_url: authorizeURL,
6515
+ client_id: cfg.clientID,
6516
+ resource: cfg.resource
6517
+ },
6518
+ instruction: "\u5728\u6D4F\u89C8\u5668\u5B8C\u6210\u4E00\u6B21\u6388\u6743\uFF1BCLI \u7B49\u5F85\u56DE\u8C03\u540E\u81EA\u52A8\u7ED1\u5B9A\u3002",
6519
+ next: null,
6520
+ recovery: []
6521
+ }, {
6522
+ jsonOutput: Boolean(options.jsonOutput),
6523
+ plainResult: ["\u6253\u5F00\u6D4F\u89C8\u5668\u5B8C\u6210\u6388\u6743\uFF1A", ` ${authorizeURL}`]
6524
+ });
6525
+ const tokens = await done;
6526
+ let me;
6527
+ try {
6528
+ me = await probeHubIdentity(cfg.resource, tokens.accessToken);
6529
+ } catch (error) {
6530
+ if (error instanceof Error && error.message.startsWith("connection_verification_pending:")) {
6531
+ throw new Error("connection_not_saved: identity verification unavailable; this connection was not saved and the existing connection was not replaced");
6532
+ }
6533
+ throw error;
6259
6534
  }
6260
- throw error;
6261
- }
6262
- if (!me.buyer_id || (!me.consumer_id || me.consumer_id !== `oauth:${cfg.clientID}` && !me.consumer_id.startsWith(`oauth:${cfg.clientID}:`))) throw new Error("authorization_required: OAuth consumer mismatch");
6263
- const subject = me.buyer_id;
6264
- storeHubSession(tokens.accessToken, tokens.refreshToken, tokens.expiresIn, subject, cfg.resource, process.env, me.consumer_id);
6265
- writeCommandEnvelope({
6266
- status: "connected",
6267
- result: {
6268
- hub: cfg.resource,
6269
- subject: subject ?? null,
6270
- expires_in: tokens.expiresIn,
6271
- refresh_token: Boolean(tokens.refreshToken)
6272
- },
6273
- instruction: "\u5DF2\u8FDE\u63A5\u3002\u53EF\u4EE5 itpay api discover / run\uFF1B\u5145\u503C\u8BF7\u5728\u7F51\u9875\u6216 topup \u547D\u4EE4\u4E2D\u660E\u786E\u8FDB\u884C\u3002",
6274
- next: { command: "itpay api doctor --json", reason: "\u9A8C\u8BC1\u8FDE\u63A5\u4E0E\u76EE\u5F55\u53EF\u7528" },
6275
- recovery: []
6276
- }, {
6277
- jsonOutput: Boolean(options.jsonOutput),
6278
- plainResult: [`\u5DF2\u8FDE\u63A5 ${cfg.resource}${subject ? ` (${subject})` : ""}`]
6535
+ if (!me.buyer_id || (!me.consumer_id || me.consumer_id !== `oauth:${cfg.clientID}` && !me.consumer_id.startsWith(`oauth:${cfg.clientID}:`))) throw new Error("authorization_required: OAuth consumer mismatch");
6536
+ const subject = me.buyer_id;
6537
+ storeNewHubSession(tokens.accessToken, tokens.refreshToken, tokens.expiresIn, subject, cfg.resource, me.consumer_id);
6538
+ writeCommandEnvelope({
6539
+ status: "connected",
6540
+ result: {
6541
+ hub: cfg.resource,
6542
+ subject: subject ?? null,
6543
+ expires_in: tokens.expiresIn,
6544
+ refresh_token: Boolean(tokens.refreshToken)
6545
+ },
6546
+ instruction: "\u5DF2\u8FDE\u63A5\u3002\u53EF\u4EE5 itpay api discover / run\uFF1B\u5145\u503C\u8BF7\u5728\u7F51\u9875\u6216 topup \u547D\u4EE4\u4E2D\u660E\u786E\u8FDB\u884C\u3002",
6547
+ next: { command: "itpay api doctor --json", reason: "\u9A8C\u8BC1\u8FDE\u63A5\u4E0E\u76EE\u5F55\u53EF\u7528" },
6548
+ recovery: []
6549
+ }, {
6550
+ jsonOutput: Boolean(options.jsonOutput),
6551
+ plainResult: [`\u5DF2\u8FDE\u63A5 ${cfg.resource}${subject ? ` (${subject})` : ""}`]
6552
+ });
6279
6553
  });
6280
6554
  }
6281
6555
  __name(runHubConnect, "runHubConnect");
6556
+ async function runHubRecoverConnection(options) {
6557
+ await recoverHubConnection();
6558
+ writeCommandEnvelope({ status: "ok", result: { status: "recovered", original_binding_preserved: true, business_calls: 0 }, instruction: "\u539F\u8FDE\u63A5\u5DF2\u6838\u5BF9\u5E76\u6062\u590D\u3002\u4EC5\u4F7F\u7528\u539F\u5F15\u7528 tasks --refresh \u6216 run-status \u8BFB\u53D6\uFF1B\u672C\u6B21\u672A\u7EED\u8DD1\u3001\u6D3E\u53D1\u6216\u6263\u8D39\u3002", next: null, recovery: [] }, { jsonOutput: Boolean(options.jsonOutput) });
6559
+ }
6560
+ __name(runHubRecoverConnection, "runHubRecoverConnection");
6561
+ async function runHubOriginalReceipt(requestKey, options) {
6562
+ const receipt = await readOriginalHubReceipt(requestKey);
6563
+ writeCommandEnvelope({ status: "ok", result: receipt, instruction: "\u4EC5\u56DE\u8BFB\u539F consumer \u7684\u4FDD\u5B58\u7ED3\u679C\u4E0E\u5B9E\u9645\u8D39\u7528\uFF1B\u539F\u8FDE\u63A5\u672A\u6FC0\u6D3B\uFF0C\u5176\u4ED6\u8FDE\u63A5\u4FDD\u6301\u6D3B\u8DC3\u3002\u672C\u6B21\u4E0D\u6388\u6743\u7EED\u8DD1\u6216\u65B0\u4E1A\u52A1\u3002", next: null, recovery: [] }, { jsonOutput: Boolean(options.jsonOutput) });
6564
+ }
6565
+ __name(runHubOriginalReceipt, "runHubOriginalReceipt");
6282
6566
 
6283
6567
  // src/commands/api_commands.ts
6284
6568
  function reportCLIError(error, contract) {
6285
- const code = error instanceof HubTaskRequestError ? error.code : error instanceof HttpError ? error.code : error instanceof Error && ["authorization_required", "connection_not_saved", "connection_verification_pending", "credential_persistence_failed"].some((code2) => error.message.startsWith(code2 + ":")) ? error.message.split(":", 1)[0] : contract?.code ?? "command_failed";
6569
+ const code = error instanceof HubTaskRequestError ? error.code : error instanceof HttpError ? error.code : error instanceof Error && ["connection_recovery_conflict", "connection_recovery_denied", "authorization_required", "connection_not_saved", "connection_verification_pending", "credential_persistence_failed"].some((code2) => error.message.startsWith(code2 + ":")) ? error.message.split(":", 1)[0] : contract?.code ?? "command_failed";
6570
+ let original = false;
6571
+ try {
6572
+ original = Boolean(storedHubProfile()?.consumer_id);
6573
+ } catch {
6574
+ original = true;
6575
+ }
6286
6576
  const auth = code === "authorization_required" || code === "unauthenticated";
6287
6577
  const taskID = error instanceof HubTaskRequestError ? error.requestID : void 0;
6288
6578
  const references = error instanceof HubTaskRequestError ? error.references : void 0;
6289
- const recover = taskID ? { command: qualifyItPayCommand(references?.run_id ? `itpay api run-status ${references.run_id}${contract?.jsonOutput ? " --json" : ""}` : `itpay api resume ${taskID}${contract?.jsonOutput ? " --json" : ""}`, declaredAgentType()), reason: "\u8BFB\u53D6\u6216\u6062\u590D\u540C\u4E00\u539F\u4EFB\u52A1\uFF0C\u4E0D\u521B\u5EFA\u66FF\u4EE3\u8BF7\u6C42" } : null;
6290
- writeCommandEnvelope({ status: "error", error: { code, message: error instanceof Error ? error.message : String(error) }, ...references ? { result: references } : {}, instruction: code === "connection_not_saved" ? "\u672C\u6B21\u8FDE\u63A5\u6821\u9A8C\u6682\u672A\u5B8C\u6210\uFF0C\u5C1A\u672A\u4FDD\u5B58\uFF1B\u539F\u6709\u8FDE\u63A5\u672A\u66FF\u6362\u3002\u670D\u52A1\u6062\u590D\u540E\u6309\u539F\u914D\u7F6E\u6B63\u5E38 connect\uFF0C\u4FDD\u7559\u539F\u8EAB\u4EFD\u548C\u4EFB\u52A1\uFF0C\u4E0D\u91CD\u53D1\u4E1A\u52A1\u3002" : code === "connection_verification_pending" ? "\u539F\u8FDE\u63A5\u6821\u9A8C\u6682\u672A\u5B8C\u6210\uFF1B\u4FDD\u7559\u539F\u8EAB\u4EFD\u548C\u5F15\u7528\uFF0C\u4E0D\u91CD\u65B0\u6388\u6743\u3001\u4E0D\u91CD\u53D1\u4E1A\u52A1\u3002\u7A0D\u540E\u53EA\u8BFB\u6838\u5BF9\u539F\u4EFB\u52A1\u3002" : code === "credential_persistence_failed" ? "\u539F\u8FDE\u63A5\u672A\u80FD\u5B89\u5168\u843D\u76D8\uFF1B\u505C\u6B62\u91CD\u8BD5\u5E76\u6838\u5BF9\u79C1\u6709\u5B58\u50A8\uFF0C\u4E0D\u6E05\u72B6\u6001\u6216\u6362\u8EAB\u4EFD\u3002" : auth ? "\u5148\u5B8C\u6210 ItPay API \u6B63\u5E38\u8FDE\u63A5\uFF1B\u4FDD\u7559\u539F\u8EAB\u4EFD\u548C\u4EFB\u52A1\uFF0C\u4E0D\u8981\u521B\u5EFA\u66FF\u4EE3\u8BF7\u6C42\u3002" : ["idempotency_conflict", "invalid_state"].includes(code) ? "\u539F\u4EFB\u52A1\u8BB0\u5F55\u4E0D\u4E00\u81F4\uFF1B\u4FDD\u7559\u8BB0\u5F55\u6838\u5BF9\uFF0C\u4E0D\u80FD\u4FEE\u6539\u51BB\u7ED3\u53C2\u6570\u7EE7\u7EED\u53D1\u9001\u3002" : taskID ? "\u539F\u8BF7\u6C42\u5C1A\u672A\u53D6\u5F97\u53EF\u7528\u56DE\u6267\uFF1B\u8FD9\u4E0D\u4EE3\u8868\u672A\u5230\u8FBE\u670D\u52A1\u5668\u3002\u4FDD\u7559\u539F\u8BF7\u6C42\u6062\u590D\uFF0C\u4E0D\u91CD\u65B0\u8C03\u7528\u6216\u5145\u503C\u3002" : hubFailureInstruction(error, contract?.instruction ?? "Check the command and current Hub connection; preserve original tasks."), next: auth || code === "connection_not_saved" ? { command: qualifyItPayCommand(`itpay api connect${contract?.jsonOutput ? " --json" : ""}`, declaredAgentType()), reason: code === "connection_not_saved" ? "\u672C\u6B21\u8FDE\u63A5\u672A\u4FDD\u5B58\uFF1B\u6309\u539F\u914D\u7F6E\u5B8C\u6210\u6B63\u5E38\u8FDE\u63A5" : "\u6B63\u5E38\u6388\u6743\u540E\u7EE7\u7EED\u539F\u4EFB\u52A1" } : ["idempotency_conflict", "invalid_state", "forbidden", "publication_changed", "credential_revalidation_required", "credential_persistence_failed"].includes(code) ? null : recover, recovery: auth && recover ? [recover] : contract?.recovery ?? [] }, { jsonOutput: contract?.jsonOutput ?? process.argv.includes("--json") });
6579
+ const recover = taskID ? { command: qualifyItPayCommand(references?.run_id ? `itpay api run-status ${references.run_id}${contract?.jsonOutput ? " --json" : ""}` : `itpay api tasks --refresh --request-id ${taskID}${contract?.jsonOutput ? " --json" : ""}`, declaredAgentType()), reason: "\u53EA\u8BFB\u6838\u5BF9\u540C\u4E00\u539F\u4EFB\u52A1\uFF0C\u4E0D\u521B\u5EFA\u6216\u7EED\u8DD1\u8BF7\u6C42", read_only: { method: "GET", path: references?.run_id ? `/v1/runs/${encodeURIComponent(references.run_id)}` : `/v1/requests/${encodeURIComponent("cli-req-" + taskID)}` } } : null;
6580
+ writeCommandEnvelope({ status: "error", error: { code, message: error instanceof Error ? error.message : String(error), ...hubErrorDetails(error) ? { details: hubErrorDetails(error) } : {} }, ...references ? { result: references } : {}, instruction: code === "connection_recovery_conflict" ? "\u5DF2\u6709\u5176\u4ED6active\u8FDE\u63A5\uFF1B\u4FDD\u7559\u539F\u8FDE\u63A5\u3001\u4EFB\u52A1\u4E0E\u79C1\u6709\u5019\u9009\uFF0C\u4EA4owner\u88C1\u51B3\uFF0C\u4E0D\u66FF\u6362\u6216\u64A4\u9500\u5176\u4ED6\u8FDE\u63A5\u3002" : code === "connection_recovery_denied" ? "\u539F\u8FDE\u63A5\u6062\u590D\u672A\u901A\u8FC7\uFF1B\u4FDD\u7559\u539Fprofile\u3001\u4EFB\u52A1\u53CA\u79C1\u6709\u6062\u590D\u6750\u6599\uFF0C\u4E0D\u65B0\u5EFAconnect\u3001\u4E0D\u91CD\u53D1\u4E1A\u52A1\u3002" : code === "connection_not_saved" ? "\u672C\u6B21\u8FDE\u63A5\u6821\u9A8C\u6682\u672A\u5B8C\u6210\uFF0C\u5C1A\u672A\u4FDD\u5B58\uFF1B\u539F\u6709\u8FDE\u63A5\u672A\u66FF\u6362\u3002\u670D\u52A1\u6062\u590D\u540E\u6309\u539F\u914D\u7F6E\u6B63\u5E38 connect\uFF0C\u4FDD\u7559\u539F\u8EAB\u4EFD\u548C\u4EFB\u52A1\uFF0C\u4E0D\u91CD\u53D1\u4E1A\u52A1\u3002" : code === "connection_verification_pending" ? "\u539F\u8FDE\u63A5\u6821\u9A8C\u6682\u672A\u5B8C\u6210\uFF1B\u4FDD\u7559\u539F\u8EAB\u4EFD\u548C\u5F15\u7528\uFF0C\u4E0D\u91CD\u65B0\u6388\u6743\u3001\u4E0D\u91CD\u53D1\u4E1A\u52A1\u3002\u7A0D\u540E\u53EA\u8BFB\u6838\u5BF9\u539F\u4EFB\u52A1\u3002" : code === "credential_persistence_failed" ? "\u539F\u8FDE\u63A5\u672A\u80FD\u5B89\u5168\u843D\u76D8\uFF1B\u505C\u6B62\u91CD\u8BD5\u5E76\u6838\u5BF9\u79C1\u6709\u5B58\u50A8\uFF0C\u4E0D\u6E05\u72B6\u6001\u6216\u6362\u8EAB\u4EFD\u3002" : auth && original ? "\u539F\u8FDE\u63A5\u5931\u6548\uFF1B\u4EC5\u7528 api recover-connection \u9A8C\u8BC1\u539F\u6388\u6743\u6062\u590D\uFF0C\u4FDD\u7559\u539Fprofile\u548C\u4EFB\u52A1\uFF0C\u4E0D\u65B0\u5EFA\u8FDE\u63A5\u6216\u91CD\u53D1\u4E1A\u52A1\u3002" : auth ? "\u5148\u5B8C\u6210 ItPay API \u6B63\u5E38\u8FDE\u63A5\uFF1B\u4FDD\u7559\u539F\u8EAB\u4EFD\u548C\u4EFB\u52A1\uFF0C\u4E0D\u8981\u521B\u5EFA\u66FF\u4EE3\u8BF7\u6C42\u3002" : ["idempotency_conflict", "invalid_state"].includes(code) ? "\u539F\u4EFB\u52A1\u8BB0\u5F55\u4E0D\u4E00\u81F4\uFF1B\u4FDD\u7559\u8BB0\u5F55\u6838\u5BF9\uFF0C\u4E0D\u80FD\u4FEE\u6539\u51BB\u7ED3\u53C2\u6570\u7EE7\u7EED\u53D1\u9001\u3002" : taskID ? "\u539F\u8BF7\u6C42\u5C1A\u672A\u53D6\u5F97\u53EF\u7528\u56DE\u6267\uFF1B\u8FD9\u4E0D\u4EE3\u8868\u672A\u5230\u8FBE\u670D\u52A1\u5668\u3002\u4FDD\u7559\u539F\u8BF7\u6C42\u6062\u590D\uFF0C\u4E0D\u91CD\u65B0\u8C03\u7528\u6216\u5145\u503C\u3002" : hubFailureInstruction(error, contract?.instruction ?? "Check the command and current Hub connection; preserve original tasks."), next: auth || code === "connection_not_saved" ? { command: qualifyItPayCommand(`itpay api ${original ? "recover-connection" : "connect"}${contract?.jsonOutput ? " --json" : ""}`, declaredAgentType()), reason: code === "connection_not_saved" ? "\u672C\u6B21\u8FDE\u63A5\u672A\u4FDD\u5B58\uFF1B\u6309\u539F\u914D\u7F6E\u5B8C\u6210\u6B63\u5E38\u8FDE\u63A5" : "\u6B63\u5E38\u6388\u6743\u540E\u7EE7\u7EED\u539F\u4EFB\u52A1" } : ["idempotency_conflict", "invalid_state", "forbidden", "publication_changed", "credential_revalidation_required", "credential_persistence_failed"].includes(code) ? null : recover, recovery: auth && recover ? [recover] : contract?.recovery ?? [] }, { jsonOutput: contract?.jsonOutput ?? process.argv.includes("--json") });
6291
6581
  process.exitCode = 1;
6292
6582
  }
6293
6583
  __name(reportCLIError, "reportCLIError");
@@ -6307,6 +6597,21 @@ function registerAPICommands(program3) {
6307
6597
  reportCLIError(error, { jsonOutput: Boolean(options.json), code: "hub_connect_failed", instruction: "\u786E\u8BA4 ITPAY_HUB_URL \u53EF\u8FBE\u4E14\u6D4F\u89C8\u5668\u6388\u6743\u5DF2\u5B8C\u6210\u3002", recovery: [] });
6308
6598
  }
6309
6599
  });
6600
+ hubCmd.command("original-receipt <request_key>").description("Read only the original consumer's saved run result and charge; never activate or resume").option("--json", "output JSON").action(async (requestKey, options) => {
6601
+ try {
6602
+ await runHubOriginalReceipt(requestKey, { jsonOutput: Boolean(options.json) });
6603
+ } catch {
6604
+ writeCommandEnvelope({ status: "error", error: { code: "receipt_unavailable", message: "\u539F\u5386\u53F2\u56DE\u6267\u672A\u53D6\u5F97\u3002" }, instruction: "\u4FDD\u7559\u539F\u8EAB\u4EFD\u4E0E\u79C1\u6709\u5019\u9009\uFF0C\u4EA4 owner \u6838\u5BF9\uFF0C\u4E0D\u6062\u590D/\u65B0\u5EFA\u8FDE\u63A5\u6216\u7EED\u8DD1\u3002", next: null, recovery: [] }, { jsonOutput: Boolean(options.json) });
6605
+ process.exitCode = 1;
6606
+ }
6607
+ });
6608
+ hubCmd.command("recover-connection").description("Recover the exact original OAuth consumer with its original refresh proof; no browser or business retry").option("--json", "output JSON").action(async (options) => {
6609
+ try {
6610
+ await runHubRecoverConnection({ jsonOutput: Boolean(options.json) });
6611
+ } catch (error) {
6612
+ reportCLIError(error, { jsonOutput: Boolean(options.json), code: "connection_recovery_denied", instruction: "\u4FDD\u7559\u539F\u8FDE\u63A5\u53CA\u4EFB\u52A1\uFF1B\u4E0D\u5F97\u65B0\u5EFA\u8FDE\u63A5\u66FF\u4EE3\u3002", recovery: [] });
6613
+ }
6614
+ });
6310
6615
  hubCmd.command("discover").description("Search the published API catalog").option("--query <text>").option("--kinds <kinds>", "comma-separated kinds").option("--effect-classes <classes>", "comma-separated effect classes").option("--limit <n>", "max items", "50").option("--json", "output JSON").action(async (options) => {
6311
6616
  try {
6312
6617
  await runHubDiscover({
@@ -6343,21 +6648,21 @@ function registerAPICommands(program3) {
6343
6648
  reportCLIError(error, { jsonOutput: Boolean(options.json), code: "hub_run_failed", instruction: "\u786E\u8BA4 publication_id \u4E0E input \u7B26\u5408 inspect \u8FD4\u56DE\u7684\u5951\u7EA6\u3002", recovery: [] });
6344
6649
  }
6345
6650
  });
6346
- hubCmd.command("resume").description("Resume a persisted task by request_id \u2014 same key, never a new request").argument("<request_id>").option("--wait <seconds>", "poll run status until terminal", "0").option("--json", "output JSON").action(async (requestID, options) => {
6651
+ hubCmd.command("resume").description("Resume the original task (may POST its intent once); --wait only reads the bound run").argument("<request_id>").option("--wait <seconds>", "poll run status until terminal", "0").option("--json", "output JSON").action(async (requestID, options) => {
6347
6652
  try {
6348
6653
  await runHubResume(requestID, { waitSeconds: Number(options.wait), jsonOutput: Boolean(options.json) });
6349
6654
  } catch (error) {
6350
6655
  reportCLIError(error, { jsonOutput: Boolean(options.json), code: "hub_resume_failed", instruction: "request_id \u6765\u81EA itpay api tasks\u3002", recovery: [] });
6351
6656
  }
6352
6657
  });
6353
- hubCmd.command("tasks").description("List durable Hub tasks (request \u2192 run/intent binding)").option("--json", "output JSON").action(async (options) => {
6658
+ hubCmd.command("tasks").description("List local Hub task snapshots; --refresh reads original authoritative bindings only").option("--refresh", "read-only refresh of at most 20 original requests").option("--request-id <id>", "select one original local request").option("--json", "output JSON").action(async (options) => {
6354
6659
  try {
6355
- await runHubTasks({ jsonOutput: Boolean(options.json) });
6660
+ await runHubTasks({ jsonOutput: Boolean(options.json), refresh: Boolean(options.refresh), requestID: options.requestId });
6356
6661
  } catch (error) {
6357
6662
  reportCLIError(error, { jsonOutput: Boolean(options.json), code: "hub_tasks_failed", instruction: "\u672C\u5730\u4EFB\u52A1\u8D26\u672C\u4E0D\u53EF\u8BFB\u3002", recovery: [] });
6358
6663
  }
6359
6664
  });
6360
- hubCmd.command("run-status").description("Query an existing run \u2014 never creates a new request").argument("<run_id>").option("--json", "output JSON").action(async (runID, options) => {
6665
+ hubCmd.command("run-status").description("Query an existing run once (read-only); --wait is not supported").showHelpAfterError("run-status is a single read and rejects --wait; inspect api resume --help only for an authorized original-task continuation.").addHelpText("after", "\nrun-status accepts no --wait. Use resume <request_id> --wait only when continuing that original task is authorized; an intent resume may perform a write.").argument("<run_id>").option("--json", "output JSON").action(async (runID, options) => {
6361
6666
  try {
6362
6667
  await runHubRunStatus(runID, { jsonOutput: Boolean(options.json) });
6363
6668
  } catch (error) {
@@ -16795,7 +17100,7 @@ import { createHash as createHash12 } from "node:crypto";
16795
17100
 
16796
17101
  // src/state/skill_identity.generated.ts
16797
17102
  var CANONICAL_SKILL_SHA256 = "4a38087a41ed4b9b6e879726b93bdef99ca67fccd2a0f9837123d5b334b046c0";
16798
- var CANONICAL_API_SKILL_SHA256 = "dffec781d5053bbdb93ec555ad2b1b4a7cad55c2281a3c9c19597a4a9418bde6";
17103
+ var CANONICAL_API_SKILL_SHA256 = "4faa2890ce35283256611d28d216d044e06e209e9d532597e4bba9079e66eb21";
16799
17104
 
16800
17105
  // src/commands/skill.ts
16801
17106
  import { existsSync as existsSync9, readFileSync as readFileSync16 } from "node:fs";