@itpay/cli 2.1.64 → 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 +941 -788
- package/dist/src/state/config.js +1 -1
- package/docs/cli-reference/api.md +18 -0
- package/package.json +2 -2
- package/skills/itpay-api/SKILL.md +11 -0
package/dist/src/main.js
CHANGED
|
@@ -3064,6 +3064,538 @@ var require_commander = __commonJS({
|
|
|
3064
3064
|
}
|
|
3065
3065
|
});
|
|
3066
3066
|
|
|
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";
|
|
3071
|
+
|
|
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;
|
|
3094
|
+
}
|
|
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 (; ; ) {
|
|
3105
|
+
try {
|
|
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 {
|
|
3116
|
+
}
|
|
3117
|
+
continue;
|
|
3118
|
+
}
|
|
3119
|
+
if (Date.now() > deadline) throw new Error(`task store lock busy: ${lockPath}`);
|
|
3120
|
+
sleepSync(20);
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
try {
|
|
3124
|
+
return fn();
|
|
3125
|
+
} finally {
|
|
3126
|
+
try {
|
|
3127
|
+
if (readFileSync(lockPath, "utf8") === token) unlinkSync(lockPath);
|
|
3128
|
+
} catch {
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
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
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
try {
|
|
3155
|
+
return await fn();
|
|
3156
|
+
} finally {
|
|
3157
|
+
try {
|
|
3158
|
+
if (readFileSync(lockPath, "utf8") === token) unlinkSync(lockPath);
|
|
3159
|
+
} catch {
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """)}</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");
|
|
3282
|
+
}
|
|
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;
|
|
3303
|
+
}
|
|
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;
|
|
3314
|
+
}
|
|
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
|
+
}
|
|
3354
|
+
}
|
|
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)
|
|
3363
|
+
});
|
|
3364
|
+
} catch {
|
|
3365
|
+
throw new Error("connection_verification_pending: identity check unavailable; preserve the original connection and references, do not reconnect");
|
|
3366
|
+
}
|
|
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");
|
|
3370
|
+
}
|
|
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");
|
|
3379
|
+
}
|
|
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;
|
|
3393
|
+
}
|
|
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
|
+
|
|
3067
3599
|
// src/commands/admin.ts
|
|
3068
3600
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
3069
3601
|
|
|
@@ -3369,792 +3901,379 @@ var HubClient = class {
|
|
|
3369
3901
|
adminInventory(query = {}) {
|
|
3370
3902
|
return this.request("GET", "/v1/admin/operations", { query });
|
|
3371
3903
|
}
|
|
3372
|
-
adminOperation(operationId) {
|
|
3373
|
-
return this.request("GET", `/v1/admin/operations/${encodeURIComponent(operationId)}`);
|
|
3374
|
-
}
|
|
3375
|
-
saveAPIDraft(body) {
|
|
3376
|
-
return this.request("POST", "/v1/admin/apis", { body });
|
|
3377
|
-
}
|
|
3378
|
-
intakeAPI(body) {
|
|
3379
|
-
return this.request("POST", "/v1/admin/intakes", { body });
|
|
3380
|
-
}
|
|
3381
|
-
intakeReport(intakeId) {
|
|
3382
|
-
return this.request("GET", `/v1/admin/intakes/${encodeURIComponent(intakeId)}`);
|
|
3383
|
-
}
|
|
3384
|
-
validateAPI(operationId, body) {
|
|
3385
|
-
return this.request("POST", `/v1/admin/operations/${encodeURIComponent(operationId)}/validate`, { body });
|
|
3386
|
-
}
|
|
3387
|
-
reviewAPI(operationId, body) {
|
|
3388
|
-
return this.request("POST", `/v1/admin/operations/${encodeURIComponent(operationId)}/review`, { body });
|
|
3389
|
-
}
|
|
3390
|
-
activateAPI(operationId) {
|
|
3391
|
-
return this.request("POST", `/v1/admin/operations/${encodeURIComponent(operationId)}/activate`, { body: {} });
|
|
3392
|
-
}
|
|
3393
|
-
recordAPIHealth(operationId, status, sampleCount = 1) {
|
|
3394
|
-
return this.request("POST", `/v1/admin/operations/${encodeURIComponent(operationId)}/health`, { body: { status, sample_count: sampleCount } });
|
|
3395
|
-
}
|
|
3396
|
-
publishAPI(operationId, body) {
|
|
3397
|
-
return this.request("POST", `/v1/admin/operations/${encodeURIComponent(operationId)}/publish`, { body });
|
|
3398
|
-
}
|
|
3399
|
-
rollbackAPI(operationId, body) {
|
|
3400
|
-
return this.request("POST", `/v1/admin/operations/${encodeURIComponent(operationId)}/rollback`, { body });
|
|
3401
|
-
}
|
|
3402
|
-
setAPIAccepting(operationId, accepting, body) {
|
|
3403
|
-
return this.request("POST", `/v1/admin/operations/${encodeURIComponent(operationId)}/${accepting ? "unsuspend" : "suspend"}`, { body });
|
|
3404
|
-
}
|
|
3405
|
-
probePermits(operationId, digest) {
|
|
3406
|
-
return this.request("GET", `/v1/admin/operations/${encodeURIComponent(operationId)}/probe-permits`, { query: { digest } });
|
|
3407
|
-
}
|
|
3408
|
-
/** Account-session only. Raw supplier secrets are write-only. */
|
|
3409
|
-
userCredentials() {
|
|
3410
|
-
return this.request("GET", "/v1/me/credentials");
|
|
3411
|
-
}
|
|
3412
|
-
/** Account-session only; exact publication requirements, never secret values. */
|
|
3413
|
-
nativeCredentialRequirements(operationId, publicationId) {
|
|
3414
|
-
return this.request("GET", `/v1/operations/${encodeURIComponent(operationId)}/credential-requirements`, { query: { publication_id: publicationId } });
|
|
3415
|
-
}
|
|
3416
|
-
credentialProviders() {
|
|
3417
|
-
return this.request("GET", "/v1/me/credential-providers");
|
|
3418
|
-
}
|
|
3419
|
-
saveUserCredential(body) {
|
|
3420
|
-
return this.request("POST", "/v1/me/credentials", { body });
|
|
3421
|
-
}
|
|
3422
|
-
revokeUserCredential(id, expectedVersion) {
|
|
3423
|
-
return this.request("DELETE", `/v1/me/credentials/${encodeURIComponent(id)}`, { body: { expected_version: expectedVersion } });
|
|
3424
|
-
}
|
|
3425
|
-
credentials() {
|
|
3426
|
-
return this.request("GET", "/v1/admin/credentials");
|
|
3427
|
-
}
|
|
3428
|
-
/** One-time secret upload. Never use this payload in logs, URLs or persistence. */
|
|
3429
|
-
rotateCredential(ref, body) {
|
|
3430
|
-
return this.request("POST", `/v1/admin/credentials/${encodeURIComponent(ref)}/rotate`, { body });
|
|
3431
|
-
}
|
|
3432
|
-
revokeCredential(ref, expectedVersion) {
|
|
3433
|
-
return this.request("POST", `/v1/admin/credentials/${encodeURIComponent(ref)}/revoke`, { body: { expected_version: expectedVersion } });
|
|
3434
|
-
}
|
|
3435
|
-
paymentReconciliation() {
|
|
3436
|
-
return this.request("GET", "/v1/admin/payment-reconciliation");
|
|
3437
|
-
}
|
|
3438
|
-
compensatePayment(eventId, reason, idempotencyKey) {
|
|
3439
|
-
return this.request("POST", `/v1/admin/collection-events/${encodeURIComponent(eventId)}/refund`, { body: { reason }, idempotencyKey });
|
|
3440
|
-
}
|
|
3441
|
-
switchTopupChannel(topupId, channel, mode, expectedCollectionRef, idempotencyKey) {
|
|
3442
|
-
return this.request("POST", `/v1/me/topups/${encodeURIComponent(topupId)}/switch-channel`, { body: { channel, mode, expected_collection_ref: expectedCollectionRef }, idempotencyKey });
|
|
3443
|
-
}
|
|
3444
|
-
};
|
|
3445
|
-
|
|
3446
|
-
// src/client/transport.ts
|
|
3447
|
-
var HttpTransportError = class extends Error {
|
|
3448
|
-
constructor(code, attempts, causeCode, cause) {
|
|
3449
|
-
const suffix = attempts > 1 ? ` after ${attempts} attempts` : "";
|
|
3450
|
-
super(`${transportMessage(code)} before a complete HTTP response was received${suffix}`, { cause });
|
|
3451
|
-
this.code = code;
|
|
3452
|
-
this.attempts = attempts;
|
|
3453
|
-
this.causeCode = causeCode;
|
|
3454
|
-
this.name = "HttpTransportError";
|
|
3455
|
-
}
|
|
3456
|
-
code;
|
|
3457
|
-
attempts;
|
|
3458
|
-
causeCode;
|
|
3459
|
-
static {
|
|
3460
|
-
__name(this, "HttpTransportError");
|
|
3461
|
-
}
|
|
3462
|
-
retryable = true;
|
|
3463
|
-
diagnostics = [];
|
|
3464
|
-
};
|
|
3465
|
-
function asTransientTransportError(error, attempts) {
|
|
3466
|
-
if (isAbort(error)) return void 0;
|
|
3467
|
-
const causeCode = findCauseCode(error);
|
|
3468
|
-
const code = causeCode ? classifyCauseCode(causeCode) : classifyFetchFailure(error);
|
|
3469
|
-
return code ? new HttpTransportError(code, attempts, causeCode, error) : void 0;
|
|
3470
|
-
}
|
|
3471
|
-
__name(asTransientTransportError, "asTransientTransportError");
|
|
3472
|
-
function classifyCauseCode(code) {
|
|
3473
|
-
switch (code) {
|
|
3474
|
-
case "ECONNRESET":
|
|
3475
|
-
return "network_connection_reset";
|
|
3476
|
-
case "ETIMEDOUT":
|
|
3477
|
-
case "UND_ERR_CONNECT_TIMEOUT":
|
|
3478
|
-
case "UND_ERR_HEADERS_TIMEOUT":
|
|
3479
|
-
case "UND_ERR_BODY_TIMEOUT":
|
|
3480
|
-
return "network_timeout";
|
|
3481
|
-
case "EAI_AGAIN":
|
|
3482
|
-
return "network_dns_temporary";
|
|
3483
|
-
case "ENETUNREACH":
|
|
3484
|
-
case "EHOSTUNREACH":
|
|
3485
|
-
return "network_unreachable";
|
|
3486
|
-
case "ECONNREFUSED":
|
|
3487
|
-
return "network_connection_refused";
|
|
3488
|
-
case "UND_ERR_SOCKET":
|
|
3489
|
-
return "network_socket_error";
|
|
3490
|
-
default:
|
|
3491
|
-
return void 0;
|
|
3904
|
+
adminOperation(operationId) {
|
|
3905
|
+
return this.request("GET", `/v1/admin/operations/${encodeURIComponent(operationId)}`);
|
|
3492
3906
|
}
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
function classifyFetchFailure(error) {
|
|
3496
|
-
return error instanceof TypeError && error.message === "fetch failed" ? "network_transport_failed" : void 0;
|
|
3497
|
-
}
|
|
3498
|
-
__name(classifyFetchFailure, "classifyFetchFailure");
|
|
3499
|
-
function findCauseCode(error) {
|
|
3500
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3501
|
-
let current = error;
|
|
3502
|
-
while (current && typeof current === "object" && !seen.has(current)) {
|
|
3503
|
-
seen.add(current);
|
|
3504
|
-
const code = current.code;
|
|
3505
|
-
if (typeof code === "string" && code !== "") return code;
|
|
3506
|
-
current = current.cause;
|
|
3907
|
+
saveAPIDraft(body) {
|
|
3908
|
+
return this.request("POST", "/v1/admin/apis", { body });
|
|
3507
3909
|
}
|
|
3508
|
-
|
|
3509
|
-
}
|
|
3510
|
-
__name(findCauseCode, "findCauseCode");
|
|
3511
|
-
function isAbort(error) {
|
|
3512
|
-
return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
3513
|
-
}
|
|
3514
|
-
__name(isAbort, "isAbort");
|
|
3515
|
-
function transportMessage(code) {
|
|
3516
|
-
switch (code) {
|
|
3517
|
-
case "network_connection_reset":
|
|
3518
|
-
return "network connection was reset";
|
|
3519
|
-
case "network_timeout":
|
|
3520
|
-
return "network connection timed out";
|
|
3521
|
-
case "network_dns_temporary":
|
|
3522
|
-
return "DNS lookup was temporarily unavailable";
|
|
3523
|
-
case "network_unreachable":
|
|
3524
|
-
return "network was unreachable";
|
|
3525
|
-
case "network_connection_refused":
|
|
3526
|
-
return "network connection was refused";
|
|
3527
|
-
case "network_socket_error":
|
|
3528
|
-
return "network socket failed";
|
|
3529
|
-
case "network_transport_failed":
|
|
3530
|
-
return "network transport failed";
|
|
3910
|
+
intakeAPI(body) {
|
|
3911
|
+
return this.request("POST", "/v1/admin/intakes", { body });
|
|
3531
3912
|
}
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
// src/client/http.ts
|
|
3536
|
-
var HttpError = class extends Error {
|
|
3537
|
-
static {
|
|
3538
|
-
__name(this, "HttpError");
|
|
3913
|
+
intakeReport(intakeId) {
|
|
3914
|
+
return this.request("GET", `/v1/admin/intakes/${encodeURIComponent(intakeId)}`);
|
|
3539
3915
|
}
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
status;
|
|
3543
|
-
code;
|
|
3544
|
-
payload;
|
|
3545
|
-
constructor(status, payload, fallbackMessage) {
|
|
3546
|
-
super(payload?.message || fallbackMessage);
|
|
3547
|
-
this.status = status;
|
|
3548
|
-
this.code = payload?.code || "unknown_error";
|
|
3549
|
-
this.payload = payload;
|
|
3916
|
+
validateAPI(operationId, body) {
|
|
3917
|
+
return this.request("POST", `/v1/admin/operations/${encodeURIComponent(operationId)}/validate`, { body });
|
|
3550
3918
|
}
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
static {
|
|
3554
|
-
__name(this, "HttpClient");
|
|
3919
|
+
reviewAPI(operationId, body) {
|
|
3920
|
+
return this.request("POST", `/v1/admin/operations/${encodeURIComponent(operationId)}/review`, { body });
|
|
3555
3921
|
}
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
baseURL;
|
|
3559
|
-
fetchImpl;
|
|
3560
|
-
defaultHeaders;
|
|
3561
|
-
requestAuthorizer;
|
|
3562
|
-
recoverAuthorization;
|
|
3563
|
-
transportRetryDelayMs;
|
|
3564
|
-
onDiagnostic;
|
|
3565
|
-
constructor(config) {
|
|
3566
|
-
this.baseURL = config.baseURL.replace(/\/$/, "");
|
|
3567
|
-
this.fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
3568
|
-
this.defaultHeaders = {
|
|
3569
|
-
"Content-Type": "application/json",
|
|
3570
|
-
Accept: "application/json",
|
|
3571
|
-
...config.defaultHeaders ?? {}
|
|
3572
|
-
};
|
|
3573
|
-
this.requestAuthorizer = config.requestAuthorizer;
|
|
3574
|
-
this.recoverAuthorization = config.recoverAuthorization;
|
|
3575
|
-
this.onDiagnostic = config.onDiagnostic;
|
|
3576
|
-
this.transportRetryDelayMs = Math.max(0, config.transportRetryDelayMs ?? 200);
|
|
3922
|
+
activateAPI(operationId) {
|
|
3923
|
+
return this.request("POST", `/v1/admin/operations/${encodeURIComponent(operationId)}/activate`, { body: {} });
|
|
3577
3924
|
}
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
const abort = /* @__PURE__ */ __name(() => controller.abort(options.signal?.reason), "abort");
|
|
3581
|
-
options.signal?.addEventListener("abort", abort, { once: true });
|
|
3582
|
-
if (options.signal?.aborted) abort();
|
|
3583
|
-
const remaining = (options.deadline ?? Date.now() + 3e4) - Date.now();
|
|
3584
|
-
const timeoutReason = new DOMException("request deadline reached", "TimeoutError");
|
|
3585
|
-
if (remaining <= 0) controller.abort(timeoutReason);
|
|
3586
|
-
const timer = remaining > 0 ? setTimeout(() => controller.abort(timeoutReason), remaining) : void 0;
|
|
3587
|
-
const requestStarted = Date.now();
|
|
3588
|
-
const diagnostics = [];
|
|
3589
|
-
let abortListener;
|
|
3590
|
-
const cancelled = new Promise((_, reject) => {
|
|
3591
|
-
abortListener = /* @__PURE__ */ __name(() => reject(controller.signal.reason), "abortListener");
|
|
3592
|
-
if (controller.signal.aborted) abortListener();
|
|
3593
|
-
else controller.signal.addEventListener("abort", abortListener, { once: true });
|
|
3594
|
-
});
|
|
3595
|
-
try {
|
|
3596
|
-
return await Promise.race([this.performRequest(path2, { ...options, signal: controller.signal }, diagnostics), cancelled]);
|
|
3597
|
-
} catch (error) {
|
|
3598
|
-
if (controller.signal.aborted && !options.signal?.aborted) {
|
|
3599
|
-
const timeout = new HttpTransportError("network_timeout", Math.max(1, diagnostics.length), "DEADLINE_EXCEEDED", error);
|
|
3600
|
-
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" });
|
|
3601
|
-
const last = diagnostics.at(-1);
|
|
3602
|
-
if (last) last.total_ms = Date.now() - requestStarted;
|
|
3603
|
-
timeout.diagnostics = diagnostics;
|
|
3604
|
-
throw timeout;
|
|
3605
|
-
}
|
|
3606
|
-
if (error instanceof HttpError || error instanceof HttpTransportError) error.diagnostics = diagnostics;
|
|
3607
|
-
throw error;
|
|
3608
|
-
} finally {
|
|
3609
|
-
if (timer !== void 0) clearTimeout(timer);
|
|
3610
|
-
options.signal?.removeEventListener("abort", abort);
|
|
3611
|
-
if (abortListener) controller.signal.removeEventListener("abort", abortListener);
|
|
3612
|
-
}
|
|
3925
|
+
recordAPIHealth(operationId, status, sampleCount = 1) {
|
|
3926
|
+
return this.request("POST", `/v1/admin/operations/${encodeURIComponent(operationId)}/health`, { body: { status, sample_count: sampleCount } });
|
|
3613
3927
|
}
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
const replaySafe = method === "GET" || Boolean(options.idempotencyKey) || options.replaySafe === true;
|
|
3620
|
-
const started = Date.now();
|
|
3621
|
-
let authorizationRecovered = false;
|
|
3622
|
-
let transportRetries = 0;
|
|
3623
|
-
for (; ; ) {
|
|
3624
|
-
options.signal?.throwIfAborted();
|
|
3625
|
-
const attemptStarted = Date.now();
|
|
3626
|
-
const attempt = diagnostics.length + 1;
|
|
3627
|
-
let phase = "unknown";
|
|
3628
|
-
let requestID;
|
|
3629
|
-
const diagnostic = { method, route_template: safeRouteTemplate(new URL(url).pathname), attempt, attempt_ms: 0, total_ms: Date.now() - started, phase };
|
|
3630
|
-
diagnostics.push(diagnostic);
|
|
3631
|
-
const abortAttempt = /* @__PURE__ */ __name(() => record("DEADLINE_OR_ABORT"), "abortAttempt");
|
|
3632
|
-
const record = /* @__PURE__ */ __name((causeCode) => {
|
|
3633
|
-
options.signal?.removeEventListener("abort", abortAttempt);
|
|
3634
|
-
Object.assign(diagnostic, { attempt_ms: Date.now() - attemptStarted, total_ms: Date.now() - started, phase, ...requestID ? { request_id: requestID } : {}, ...causeCode ? { cause_code: causeCode } : {} });
|
|
3635
|
-
this.onDiagnostic?.(diagnostic);
|
|
3636
|
-
}, "record");
|
|
3637
|
-
options.signal?.addEventListener("abort", abortAttempt, { once: true });
|
|
3638
|
-
const headers = { ...this.defaultHeaders, ...options.responseType === "png" ? { Accept: "image/png" } : {} };
|
|
3639
|
-
try {
|
|
3640
|
-
if (this.requestAuthorizer) {
|
|
3641
|
-
Object.assign(headers, await this.requestAuthorizer({ method, path: requestPath, body, ...options.signal ? { signal: options.signal } : {}, ...options.deadline !== void 0 ? { deadline: options.deadline } : {} }));
|
|
3642
|
-
}
|
|
3643
|
-
} catch (error2) {
|
|
3644
|
-
record(error2 instanceof HttpTransportError ? error2.causeCode : "AUTHORIZATION_FAILED");
|
|
3645
|
-
if (error2 instanceof HttpTransportError) throw error2;
|
|
3646
|
-
throw asTransientTransportError(error2, 1) ?? error2;
|
|
3647
|
-
}
|
|
3648
|
-
options.signal?.throwIfAborted();
|
|
3649
|
-
if (options.bearer) headers.Authorization = `Bearer ${options.bearer}`;
|
|
3650
|
-
if (options.idempotencyKey) headers["Idempotency-Key"] = options.idempotencyKey;
|
|
3651
|
-
let response;
|
|
3652
|
-
let text2;
|
|
3653
|
-
try {
|
|
3654
|
-
response = await this.fetchImpl(url, {
|
|
3655
|
-
method,
|
|
3656
|
-
headers,
|
|
3657
|
-
...options.responseType === "png" ? { redirect: "error" } : {},
|
|
3658
|
-
...options.body !== void 0 ? { body } : {},
|
|
3659
|
-
...options.signal ? { signal: options.signal } : {}
|
|
3660
|
-
});
|
|
3661
|
-
phase = "response_body";
|
|
3662
|
-
diagnostic.phase = phase;
|
|
3663
|
-
const headerID = response.headers.get("X-ItPay-Request-ID");
|
|
3664
|
-
if (headerID && /^req_[a-f0-9]{1,64}$/.test(headerID)) {
|
|
3665
|
-
requestID = headerID;
|
|
3666
|
-
diagnostic.request_id = headerID;
|
|
3667
|
-
}
|
|
3668
|
-
if (response.ok && options.responseType === "png") {
|
|
3669
|
-
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
3670
|
-
const magic = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
3671
|
-
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");
|
|
3672
|
-
record();
|
|
3673
|
-
return bytes;
|
|
3674
|
-
}
|
|
3675
|
-
text2 = await response.text();
|
|
3676
|
-
this.localSimulation = response.headers.get("X-ItPay-Environment") === "local_simulation";
|
|
3677
|
-
} catch (error2) {
|
|
3678
|
-
if (options.signal?.aborted) {
|
|
3679
|
-
record("DEADLINE_OR_ABORT");
|
|
3680
|
-
throw error2;
|
|
3681
|
-
}
|
|
3682
|
-
const transportError = asTransientTransportError(error2, transportRetries + 1);
|
|
3683
|
-
record(transportError?.causeCode ?? "UNKNOWN_TRANSPORT_FAILURE");
|
|
3684
|
-
if (!transportError) throw error2;
|
|
3685
|
-
if (replaySafe && transportRetries < _HttpClient.MAX_TRANSPORT_RETRIES) {
|
|
3686
|
-
transportRetries += 1;
|
|
3687
|
-
await delay(this.transportRetryDelayMs * 2 ** (transportRetries - 1), options.signal);
|
|
3688
|
-
continue;
|
|
3689
|
-
}
|
|
3690
|
-
throw asTransientTransportError(error2, transportRetries + 1) ?? error2;
|
|
3691
|
-
}
|
|
3692
|
-
record(response.ok ? void 0 : `HTTP_${response.status}`);
|
|
3693
|
-
const parsed = text2.length > 0 ? safeParseJson(text2) : void 0;
|
|
3694
|
-
if (response.ok) return parsed;
|
|
3695
|
-
const error = new HttpError(response.status, parsed, `HTTP ${response.status}`);
|
|
3696
|
-
if (!authorizationRecovered && error.status === 401 && error.code === "agent_device_session_required" && this.recoverAuthorization) {
|
|
3697
|
-
authorizationRecovered = true;
|
|
3698
|
-
options.signal?.throwIfAborted();
|
|
3699
|
-
await this.recoverAuthorization({ ...options.signal ? { signal: options.signal } : {}, ...options.deadline !== void 0 ? { deadline: options.deadline } : {} });
|
|
3700
|
-
options.signal?.throwIfAborted();
|
|
3701
|
-
continue;
|
|
3702
|
-
}
|
|
3703
|
-
throw error;
|
|
3704
|
-
}
|
|
3928
|
+
publishAPI(operationId, body) {
|
|
3929
|
+
return this.request("POST", `/v1/admin/operations/${encodeURIComponent(operationId)}/publish`, { body });
|
|
3930
|
+
}
|
|
3931
|
+
rollbackAPI(operationId, body) {
|
|
3932
|
+
return this.request("POST", `/v1/admin/operations/${encodeURIComponent(operationId)}/rollback`, { body });
|
|
3705
3933
|
}
|
|
3706
|
-
|
|
3707
|
-
return this.request(
|
|
3934
|
+
setAPIAccepting(operationId, accepting, body) {
|
|
3935
|
+
return this.request("POST", `/v1/admin/operations/${encodeURIComponent(operationId)}/${accepting ? "unsuspend" : "suspend"}`, { body });
|
|
3708
3936
|
}
|
|
3709
|
-
|
|
3710
|
-
return this.request(
|
|
3937
|
+
probePermits(operationId, digest) {
|
|
3938
|
+
return this.request("GET", `/v1/admin/operations/${encodeURIComponent(operationId)}/probe-permits`, { query: { digest } });
|
|
3711
3939
|
}
|
|
3712
|
-
|
|
3713
|
-
|
|
3940
|
+
/** Account-session only. Raw supplier secrets are write-only. */
|
|
3941
|
+
userCredentials() {
|
|
3942
|
+
return this.request("GET", "/v1/me/credentials");
|
|
3714
3943
|
}
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
if (milliseconds === 0) return Promise.resolve();
|
|
3719
|
-
return new Promise((resolve10, reject) => {
|
|
3720
|
-
const abort = /* @__PURE__ */ __name(() => {
|
|
3721
|
-
clearTimeout(timer);
|
|
3722
|
-
reject(signal?.reason);
|
|
3723
|
-
}, "abort");
|
|
3724
|
-
const timer = setTimeout(() => {
|
|
3725
|
-
signal?.removeEventListener("abort", abort);
|
|
3726
|
-
resolve10();
|
|
3727
|
-
}, milliseconds);
|
|
3728
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
3729
|
-
});
|
|
3730
|
-
}
|
|
3731
|
-
__name(delay, "delay");
|
|
3732
|
-
function safeRouteTemplate(path2) {
|
|
3733
|
-
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"]);
|
|
3734
|
-
return path2.split("/").map((segment) => staticSegments.has(segment) ? segment : segment ? "{value}" : "").join("/");
|
|
3735
|
-
}
|
|
3736
|
-
__name(safeRouteTemplate, "safeRouteTemplate");
|
|
3737
|
-
function safeParseJson(text2) {
|
|
3738
|
-
try {
|
|
3739
|
-
return JSON.parse(text2);
|
|
3740
|
-
} catch {
|
|
3741
|
-
return void 0;
|
|
3944
|
+
/** Account-session only; exact publication requirements, never secret values. */
|
|
3945
|
+
nativeCredentialRequirements(operationId, publicationId) {
|
|
3946
|
+
return this.request("GET", `/v1/operations/${encodeURIComponent(operationId)}/credential-requirements`, { query: { publication_id: publicationId } });
|
|
3742
3947
|
}
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
// src/state/hub_credentials.ts
|
|
3747
|
-
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
3748
|
-
import { chmodSync as chmodSync2, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
3749
|
-
import { dirname as dirname2, resolve } from "node:path";
|
|
3750
|
-
|
|
3751
|
-
// src/state/file_lock.ts
|
|
3752
|
-
import { chmodSync, lstatSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3753
|
-
import { dirname } from "node:path";
|
|
3754
|
-
import { randomUUID } from "node:crypto";
|
|
3755
|
-
var STALE_MS = 3e4;
|
|
3756
|
-
function sleepSync(ms) {
|
|
3757
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
3758
|
-
}
|
|
3759
|
-
__name(sleepSync, "sleepSync");
|
|
3760
|
-
function ownerAlive(path2) {
|
|
3761
|
-
try {
|
|
3762
|
-
const st = lstatSync(path2);
|
|
3763
|
-
if (st.isSymbolicLink()) return false;
|
|
3764
|
-
if (Date.now() - st.mtimeMs < STALE_MS) return true;
|
|
3765
|
-
const token = readFileSync(path2, "utf8");
|
|
3766
|
-
const pid = Number(/^(\d+):/.exec(token)?.[1]);
|
|
3767
|
-
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
3768
|
-
try {
|
|
3769
|
-
process.kill(pid, 0);
|
|
3770
|
-
return true;
|
|
3771
|
-
} catch {
|
|
3772
|
-
return false;
|
|
3773
|
-
}
|
|
3774
|
-
} catch {
|
|
3775
|
-
return false;
|
|
3948
|
+
credentialProviders() {
|
|
3949
|
+
return this.request("GET", "/v1/me/credential-providers");
|
|
3776
3950
|
}
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
function withFileLock(lockPath, fn) {
|
|
3780
|
-
mkdirSync(dirname(lockPath), { recursive: true, mode: 448 });
|
|
3781
|
-
const token = `${process.pid}:${randomUUID()}`;
|
|
3782
|
-
const deadline = Date.now() + 15e3;
|
|
3783
|
-
for (; ; ) {
|
|
3784
|
-
try {
|
|
3785
|
-
writeFileSync(lockPath, token, { encoding: "utf8", flag: "wx", mode: 384 });
|
|
3786
|
-
chmodSync(lockPath, 384);
|
|
3787
|
-
break;
|
|
3788
|
-
} catch (error) {
|
|
3789
|
-
const code = error.code;
|
|
3790
|
-
if (code !== "EEXIST") throw error;
|
|
3791
|
-
if (!ownerAlive(lockPath)) {
|
|
3792
|
-
try {
|
|
3793
|
-
unlinkSync(lockPath);
|
|
3794
|
-
} catch {
|
|
3795
|
-
}
|
|
3796
|
-
continue;
|
|
3797
|
-
}
|
|
3798
|
-
if (Date.now() > deadline) throw new Error(`task store lock busy: ${lockPath}`);
|
|
3799
|
-
sleepSync(20);
|
|
3800
|
-
}
|
|
3951
|
+
saveUserCredential(body) {
|
|
3952
|
+
return this.request("POST", "/v1/me/credentials", { body });
|
|
3801
3953
|
}
|
|
3802
|
-
|
|
3803
|
-
return
|
|
3804
|
-
} finally {
|
|
3805
|
-
try {
|
|
3806
|
-
if (readFileSync(lockPath, "utf8") === token) unlinkSync(lockPath);
|
|
3807
|
-
} catch {
|
|
3808
|
-
}
|
|
3954
|
+
revokeUserCredential(id, expectedVersion) {
|
|
3955
|
+
return this.request("DELETE", `/v1/me/credentials/${encodeURIComponent(id)}`, { body: { expected_version: expectedVersion } });
|
|
3809
3956
|
}
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
async function withAsyncFileLock(lockPath, fn) {
|
|
3813
|
-
mkdirSync(dirname(lockPath), { recursive: true, mode: 448 });
|
|
3814
|
-
const token = `${process.pid}:${randomUUID()}`;
|
|
3815
|
-
const deadline = Date.now() + 2e4;
|
|
3816
|
-
for (; ; ) {
|
|
3817
|
-
try {
|
|
3818
|
-
writeFileSync(lockPath, token, { encoding: "utf8", flag: "wx", mode: 384 });
|
|
3819
|
-
break;
|
|
3820
|
-
} catch (error) {
|
|
3821
|
-
if (error.code !== "EEXIST") throw error;
|
|
3822
|
-
if (!ownerAlive(lockPath)) {
|
|
3823
|
-
try {
|
|
3824
|
-
unlinkSync(lockPath);
|
|
3825
|
-
} catch {
|
|
3826
|
-
}
|
|
3827
|
-
continue;
|
|
3828
|
-
}
|
|
3829
|
-
if (Date.now() > deadline) throw new Error("credential refresh lock busy");
|
|
3830
|
-
await new Promise((resolve10) => setTimeout(resolve10, 20));
|
|
3831
|
-
}
|
|
3957
|
+
credentials() {
|
|
3958
|
+
return this.request("GET", "/v1/admin/credentials");
|
|
3832
3959
|
}
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
try {
|
|
3837
|
-
if (readFileSync(lockPath, "utf8") === token) unlinkSync(lockPath);
|
|
3838
|
-
} catch {
|
|
3839
|
-
}
|
|
3960
|
+
/** One-time secret upload. Never use this payload in logs, URLs or persistence. */
|
|
3961
|
+
rotateCredential(ref, body) {
|
|
3962
|
+
return this.request("POST", `/v1/admin/credentials/${encodeURIComponent(ref)}/rotate`, { body });
|
|
3840
3963
|
}
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
resource: (env.ITPAY_HUB_URL ?? "http://localhost:18090").replace(/\/+$/, ""),
|
|
3855
|
-
scope: env.ITPAY_HUB_OAUTH_SCOPE ?? "openid offline_access itpay.hub"
|
|
3856
|
-
};
|
|
3857
|
-
}
|
|
3858
|
-
__name(hubOAuthConfig, "hubOAuthConfig");
|
|
3859
|
-
function pkce() {
|
|
3860
|
-
const verifier = randomBytes(32).toString("base64url");
|
|
3861
|
-
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
3862
|
-
return { verifier, challenge };
|
|
3863
|
-
}
|
|
3864
|
-
__name(pkce, "pkce");
|
|
3865
|
-
async function runConnectFlow(cfg, timeoutMs = 3e5) {
|
|
3866
|
-
const { verifier, challenge } = pkce();
|
|
3867
|
-
const state = randomBytes(16).toString("hex");
|
|
3868
|
-
const server = createServer();
|
|
3869
|
-
const done = new Promise((resolvePromise, reject) => {
|
|
3870
|
-
const timer = setTimeout(() => {
|
|
3871
|
-
server.close();
|
|
3872
|
-
reject(new Error("connect timed out waiting for browser authorization"));
|
|
3873
|
-
}, timeoutMs);
|
|
3874
|
-
server.on("request", (req, res) => {
|
|
3875
|
-
const u = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
3876
|
-
const finish = /* @__PURE__ */ __name((ok, msg) => {
|
|
3877
|
-
res.writeHead(ok ? 200 : 400, { "Content-Type": "text/html; charset=utf-8" });
|
|
3878
|
-
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """)}</h2><p>\u53EF\u4EE5\u5173\u95ED\u6B64\u9875\u9762\uFF0C\u56DE\u5230\u7EC8\u7AEF\u7EE7\u7EED\u3002</p></body>`);
|
|
3879
|
-
}, "finish");
|
|
3880
|
-
(async () => {
|
|
3881
|
-
if (u.pathname !== "/callback") return finish(false, "\u65E0\u6548\u7684\u56DE\u8C03");
|
|
3882
|
-
if (u.searchParams.get("state") !== state) return finish(false, "state \u4E0D\u5339\u914D");
|
|
3883
|
-
if (u.searchParams.get("error")) {
|
|
3884
|
-
clearTimeout(timer);
|
|
3885
|
-
server.close();
|
|
3886
|
-
finish(false, "\u6388\u6743\u88AB\u62D2\u7EDD");
|
|
3887
|
-
reject(new Error("authorization_required: consent denied"));
|
|
3888
|
-
return;
|
|
3889
|
-
}
|
|
3890
|
-
const code = u.searchParams.get("code");
|
|
3891
|
-
if (!code) {
|
|
3892
|
-
clearTimeout(timer);
|
|
3893
|
-
server.close();
|
|
3894
|
-
finish(false, "\u7F3A\u5C11 code");
|
|
3895
|
-
reject(new Error("missing code"));
|
|
3896
|
-
return;
|
|
3897
|
-
}
|
|
3898
|
-
const form = new URLSearchParams({
|
|
3899
|
-
grant_type: "authorization_code",
|
|
3900
|
-
code,
|
|
3901
|
-
redirect_uri: `http://127.0.0.1:${server.address().port}/callback`,
|
|
3902
|
-
client_id: cfg.clientID,
|
|
3903
|
-
code_verifier: verifier,
|
|
3904
|
-
resource: cfg.resource
|
|
3905
|
-
});
|
|
3906
|
-
const tok = await fetch(cfg.tokenURL, {
|
|
3907
|
-
method: "POST",
|
|
3908
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
3909
|
-
body: form,
|
|
3910
|
-
signal: AbortSignal.timeout(1e4)
|
|
3911
|
-
});
|
|
3912
|
-
if (!tok.ok) {
|
|
3913
|
-
clearTimeout(timer);
|
|
3914
|
-
server.close();
|
|
3915
|
-
finish(false, "token \u4EA4\u6362\u5931\u8D25");
|
|
3916
|
-
reject(new Error(`token endpoint ${tok.status}`));
|
|
3917
|
-
return;
|
|
3918
|
-
}
|
|
3919
|
-
const t = await tok.json();
|
|
3920
|
-
clearTimeout(timer);
|
|
3921
|
-
server.close();
|
|
3922
|
-
finish(true, "ItPay \u5DF2\u8FDE\u63A5");
|
|
3923
|
-
resolvePromise({ accessToken: t.access_token, refreshToken: t.refresh_token, expiresIn: t.expires_in });
|
|
3924
|
-
})().catch((e) => {
|
|
3925
|
-
clearTimeout(timer);
|
|
3926
|
-
server.close();
|
|
3927
|
-
reject(e);
|
|
3928
|
-
});
|
|
3929
|
-
});
|
|
3930
|
-
});
|
|
3931
|
-
await new Promise((resolvePromise, reject) => {
|
|
3932
|
-
server.once("error", reject);
|
|
3933
|
-
server.listen(0, "127.0.0.1", () => resolvePromise());
|
|
3934
|
-
});
|
|
3935
|
-
const port = server.address().port;
|
|
3936
|
-
const authorizeURL = `${cfg.authorizeURL}?` + new URLSearchParams({
|
|
3937
|
-
response_type: "code",
|
|
3938
|
-
client_id: cfg.clientID,
|
|
3939
|
-
redirect_uri: `http://127.0.0.1:${port}/callback`,
|
|
3940
|
-
scope: cfg.scope,
|
|
3941
|
-
state,
|
|
3942
|
-
code_challenge: challenge,
|
|
3943
|
-
code_challenge_method: "S256",
|
|
3944
|
-
resource: cfg.resource
|
|
3945
|
-
}).toString();
|
|
3946
|
-
return { authorizeURL, done };
|
|
3947
|
-
}
|
|
3948
|
-
__name(runConnectFlow, "runConnectFlow");
|
|
3964
|
+
revokeCredential(ref, expectedVersion) {
|
|
3965
|
+
return this.request("POST", `/v1/admin/credentials/${encodeURIComponent(ref)}/revoke`, { body: { expected_version: expectedVersion } });
|
|
3966
|
+
}
|
|
3967
|
+
paymentReconciliation() {
|
|
3968
|
+
return this.request("GET", "/v1/admin/payment-reconciliation");
|
|
3969
|
+
}
|
|
3970
|
+
compensatePayment(eventId, reason, idempotencyKey) {
|
|
3971
|
+
return this.request("POST", `/v1/admin/collection-events/${encodeURIComponent(eventId)}/refund`, { body: { reason }, idempotencyKey });
|
|
3972
|
+
}
|
|
3973
|
+
switchTopupChannel(topupId, channel, mode, expectedCollectionRef, idempotencyKey) {
|
|
3974
|
+
return this.request("POST", `/v1/me/topups/${encodeURIComponent(topupId)}/switch-channel`, { body: { channel, mode, expected_collection_ref: expectedCollectionRef }, idempotencyKey });
|
|
3975
|
+
}
|
|
3976
|
+
};
|
|
3949
3977
|
|
|
3950
|
-
// src/
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
if (!/^itk_[A-Za-z0-9_-]{8,}$/.test(apiKey)) {
|
|
3960
|
-
throw new Error("invalid key format \u2014 expected an itk_ API key");
|
|
3978
|
+
// src/client/transport.ts
|
|
3979
|
+
var HttpTransportError = class extends Error {
|
|
3980
|
+
constructor(code, attempts, causeCode, cause) {
|
|
3981
|
+
const suffix = attempts > 1 ? ` after ${attempts} attempts` : "";
|
|
3982
|
+
super(`${transportMessage(code)} before a complete HTTP response was received${suffix}`, { cause });
|
|
3983
|
+
this.code = code;
|
|
3984
|
+
this.attempts = attempts;
|
|
3985
|
+
this.causeCode = causeCode;
|
|
3986
|
+
this.name = "HttpTransportError";
|
|
3961
3987
|
}
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3988
|
+
code;
|
|
3989
|
+
attempts;
|
|
3990
|
+
causeCode;
|
|
3991
|
+
static {
|
|
3992
|
+
__name(this, "HttpTransportError");
|
|
3993
|
+
}
|
|
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;
|
|
3972
4002
|
}
|
|
3973
|
-
__name(
|
|
3974
|
-
function
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
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;
|
|
3982
4024
|
}
|
|
3983
4025
|
}
|
|
3984
|
-
__name(
|
|
3985
|
-
function
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
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;
|
|
3993
4039
|
}
|
|
4040
|
+
return void 0;
|
|
3994
4041
|
}
|
|
3995
|
-
__name(
|
|
3996
|
-
function
|
|
3997
|
-
|
|
3998
|
-
const file = existsSync(path2) ? JSON.parse(readFileSync2(path2, "utf8")) : { schema_version: "itpay.hub_credentials.v1", profiles: {} };
|
|
3999
|
-
const issuedAt = Date.now();
|
|
4000
|
-
const margin = Math.min(3e4, Math.max(100, expiresIn * 100));
|
|
4001
|
-
file.profiles["default"] = {
|
|
4002
|
-
base_url: baseURL,
|
|
4003
|
-
access_token: accessToken,
|
|
4004
|
-
...refreshToken ? { refresh_token: refreshToken } : {},
|
|
4005
|
-
expires_at: new Date(issuedAt + expiresIn * 1e3).toISOString(),
|
|
4006
|
-
refresh_after: new Date(issuedAt + expiresIn * 1e3 - margin).toISOString(),
|
|
4007
|
-
...subject ? { subject } : {},
|
|
4008
|
-
consumer_id: consumerID ?? `oauth:${hubOAuthConfig(env).clientID}`,
|
|
4009
|
-
client_id: hubOAuthConfig(env).clientID,
|
|
4010
|
-
token_url: hubOAuthConfig(env).tokenURL,
|
|
4011
|
-
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
4012
|
-
};
|
|
4013
|
-
saveHubProfileUnlocked(file.profiles["default"], env);
|
|
4042
|
+
__name(findCauseCode, "findCauseCode");
|
|
4043
|
+
function isAbort(error) {
|
|
4044
|
+
return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
4014
4045
|
}
|
|
4015
|
-
__name(
|
|
4016
|
-
function
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
}
|
|
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";
|
|
4033
4063
|
}
|
|
4034
4064
|
}
|
|
4035
|
-
__name(
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
signal: AbortSignal.timeout(1e4)
|
|
4042
|
-
});
|
|
4043
|
-
} catch {
|
|
4044
|
-
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");
|
|
4045
4071
|
}
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
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;
|
|
4049
4082
|
}
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
if (typeof me.buyer_id !== "string" || !me.buyer_id || typeof me.consumer_id !== "string" || !me.consumer_id) throw new Error();
|
|
4055
|
-
return { buyer_id: me.buyer_id, consumer_id: me.consumer_id };
|
|
4056
|
-
} catch {
|
|
4057
|
-
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");
|
|
4058
4087
|
}
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
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);
|
|
4072
4109
|
}
|
|
4073
|
-
}
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
const
|
|
4084
|
-
|
|
4085
|
-
const
|
|
4086
|
-
|
|
4087
|
-
|
|
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);
|
|
4088
4144
|
}
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
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" } : {} };
|
|
4095
4171
|
try {
|
|
4096
|
-
|
|
4097
|
-
method:
|
|
4098
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
4099
|
-
body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: p.refresh_token, client_id: p.client_id, resource: cfg.resource }),
|
|
4100
|
-
signal: AbortSignal.timeout(1e4)
|
|
4101
|
-
});
|
|
4102
|
-
} catch {
|
|
4103
|
-
throw new Error("connection_verification_pending: refresh outcome unknown; preserve the original connection and references, do not repeat authorization or business requests");
|
|
4104
|
-
}
|
|
4105
|
-
if (!response.ok) {
|
|
4106
|
-
let rejected = response.status === 401 || response.status === 403;
|
|
4107
|
-
if (response.status === 400) {
|
|
4108
|
-
try {
|
|
4109
|
-
const problem = await response.json();
|
|
4110
|
-
rejected = ["invalid_grant", "invalid_client", "unauthorized_client"].includes(problem?.error ?? "");
|
|
4111
|
-
} catch {
|
|
4112
|
-
}
|
|
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 } : {} }));
|
|
4113
4174
|
}
|
|
4114
|
-
|
|
4115
|
-
|
|
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;
|
|
4116
4179
|
}
|
|
4117
|
-
|
|
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;
|
|
4118
4185
|
try {
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
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;
|
|
4122
4223
|
}
|
|
4123
|
-
|
|
4124
|
-
|
|
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;
|
|
4125
4234
|
}
|
|
4126
|
-
|
|
4127
|
-
candidate = {
|
|
4128
|
-
...p,
|
|
4129
|
-
access_token: token.access_token,
|
|
4130
|
-
refresh_token: token.refresh_token ?? p.refresh_token,
|
|
4131
|
-
expires_at: new Date(now + token.expires_in * 1e3).toISOString(),
|
|
4132
|
-
refresh_after: new Date(now + token.expires_in * 1e3 - Math.min(3e4, Math.max(100, token.expires_in * 100))).toISOString(),
|
|
4133
|
-
identity_verification_pending: true
|
|
4134
|
-
};
|
|
4135
|
-
saveHubProfileUnlocked(candidate, env);
|
|
4136
|
-
}
|
|
4137
|
-
const identity = await probeHubIdentity(cfg.resource, candidate.access_token);
|
|
4138
|
-
if (identity.buyer_id !== p.subject || identity.consumer_id !== p.consumer_id) {
|
|
4139
|
-
throw new Error("authorization_required: refreshed identity changed");
|
|
4140
|
-
}
|
|
4141
|
-
if (!(Date.parse(candidate.expires_at ?? "") > Date.now())) {
|
|
4142
|
-
throw new Error("connection_verification_pending: token expired during identity check; preserve the original connection and references");
|
|
4235
|
+
throw error;
|
|
4143
4236
|
}
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
return
|
|
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 });
|
|
4147
4261
|
});
|
|
4148
4262
|
}
|
|
4149
|
-
__name(
|
|
4150
|
-
function
|
|
4151
|
-
|
|
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("/");
|
|
4152
4267
|
}
|
|
4153
|
-
__name(
|
|
4154
|
-
function
|
|
4155
|
-
|
|
4268
|
+
__name(safeRouteTemplate, "safeRouteTemplate");
|
|
4269
|
+
function safeParseJson(text2) {
|
|
4270
|
+
try {
|
|
4271
|
+
return JSON.parse(text2);
|
|
4272
|
+
} catch {
|
|
4273
|
+
return void 0;
|
|
4274
|
+
}
|
|
4156
4275
|
}
|
|
4157
|
-
__name(
|
|
4276
|
+
__name(safeParseJson, "safeParseJson");
|
|
4158
4277
|
|
|
4159
4278
|
// src/client/hub.ts
|
|
4160
4279
|
function loadHubConfig(env = process.env) {
|
|
@@ -6383,63 +6502,82 @@ async function runHubCredentialsImport(options) {
|
|
|
6383
6502
|
}
|
|
6384
6503
|
__name(runHubCredentialsImport, "runHubCredentialsImport");
|
|
6385
6504
|
async function runHubConnect(options) {
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
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}
|
|
6389
6510
|
`);
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
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;
|
|
6411
6534
|
}
|
|
6412
|
-
throw
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
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
|
+
});
|
|
6431
6553
|
});
|
|
6432
6554
|
}
|
|
6433
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");
|
|
6434
6566
|
|
|
6435
6567
|
// src/commands/api_commands.ts
|
|
6436
6568
|
function reportCLIError(error, contract) {
|
|
6437
|
-
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
|
+
}
|
|
6438
6576
|
const auth = code === "authorization_required" || code === "unauthenticated";
|
|
6439
6577
|
const taskID = error instanceof HubTaskRequestError ? error.requestID : void 0;
|
|
6440
6578
|
const references = error instanceof HubTaskRequestError ? error.references : void 0;
|
|
6441
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;
|
|
6442
|
-
writeCommandEnvelope({ status: "error", error: { code, message: error instanceof Error ? error.message : String(error), ...hubErrorDetails(error) ? { details: hubErrorDetails(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") });
|
|
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") });
|
|
6443
6581
|
process.exitCode = 1;
|
|
6444
6582
|
}
|
|
6445
6583
|
__name(reportCLIError, "reportCLIError");
|
|
@@ -6459,6 +6597,21 @@ function registerAPICommands(program3) {
|
|
|
6459
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: [] });
|
|
6460
6598
|
}
|
|
6461
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
|
+
});
|
|
6462
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) => {
|
|
6463
6616
|
try {
|
|
6464
6617
|
await runHubDiscover({
|
|
@@ -16947,7 +17100,7 @@ import { createHash as createHash12 } from "node:crypto";
|
|
|
16947
17100
|
|
|
16948
17101
|
// src/state/skill_identity.generated.ts
|
|
16949
17102
|
var CANONICAL_SKILL_SHA256 = "4a38087a41ed4b9b6e879726b93bdef99ca67fccd2a0f9837123d5b334b046c0";
|
|
16950
|
-
var CANONICAL_API_SKILL_SHA256 = "
|
|
17103
|
+
var CANONICAL_API_SKILL_SHA256 = "4faa2890ce35283256611d28d216d044e06e209e9d532597e4bba9079e66eb21";
|
|
16951
17104
|
|
|
16952
17105
|
// src/commands/skill.ts
|
|
16953
17106
|
import { existsSync as existsSync9, readFileSync as readFileSync16 } from "node:fs";
|