@h-ai/api-client 0.1.0-alpha9 → 1.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +210 -0
- package/README.ja.md +210 -0
- package/README.md +135 -265
- package/dist/index.d.ts +2758 -250
- package/dist/index.js +677 -632
- package/dist/index.js.map +1 -1
- package/package.json +19 -10
package/dist/index.js
CHANGED
|
@@ -1,51 +1,245 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { apiContract, IamSessionErrorCodeSchema, HaiErrorSchema } from '@h-ai/api-contract';
|
|
2
|
+
import { core, err, ok } from '@h-ai/core';
|
|
3
|
+
import { TransportTimeError, TransportBodyTooLargeError } from '@h-ai/crypto';
|
|
4
|
+
import { createORPCClient, ORPCError } from '@orpc/client';
|
|
5
|
+
import { OpenAPILink } from '@orpc/openapi-client/fetch';
|
|
2
6
|
import { z } from 'zod';
|
|
3
7
|
|
|
4
|
-
// src/api-client-
|
|
5
|
-
var
|
|
6
|
-
var
|
|
7
|
-
function
|
|
8
|
+
// src/api-client-main.ts
|
|
9
|
+
var logger = core.logger.child({ module: "api-client", scope: "auth" });
|
|
10
|
+
var HTTPONLY_COOKIE_SENTINEL = "__httponly_cookie__";
|
|
11
|
+
function createMemoryTokenStorage() {
|
|
12
|
+
let accessToken = null;
|
|
13
|
+
let refreshToken = null;
|
|
8
14
|
return {
|
|
9
15
|
async getAccessToken() {
|
|
10
|
-
return
|
|
16
|
+
return accessToken;
|
|
11
17
|
},
|
|
12
18
|
async setAccessToken(token) {
|
|
13
|
-
|
|
19
|
+
accessToken = token;
|
|
14
20
|
},
|
|
15
21
|
async getRefreshToken() {
|
|
16
|
-
return
|
|
22
|
+
return refreshToken;
|
|
17
23
|
},
|
|
18
24
|
async setRefreshToken(token) {
|
|
19
|
-
|
|
25
|
+
refreshToken = token;
|
|
20
26
|
},
|
|
21
27
|
async clear() {
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
accessToken = null;
|
|
29
|
+
refreshToken = null;
|
|
24
30
|
}
|
|
25
31
|
};
|
|
26
32
|
}
|
|
27
|
-
function
|
|
28
|
-
let
|
|
29
|
-
let refreshToken = null;
|
|
33
|
+
function createHttpOnlyCookieTokenStorage() {
|
|
34
|
+
let memoryAccessToken = null;
|
|
30
35
|
return {
|
|
31
36
|
async getAccessToken() {
|
|
32
|
-
return
|
|
37
|
+
return memoryAccessToken;
|
|
33
38
|
},
|
|
34
39
|
async setAccessToken(token) {
|
|
35
|
-
|
|
40
|
+
memoryAccessToken = token;
|
|
36
41
|
},
|
|
42
|
+
// refresh token 由服务端通过 Set-Cookie 设置为 httpOnly cookie;
|
|
43
|
+
// JS 不可见,返回哨兵值让 TokenManager 识别此模式。
|
|
37
44
|
async getRefreshToken() {
|
|
38
|
-
return
|
|
45
|
+
return HTTPONLY_COOKIE_SENTINEL;
|
|
39
46
|
},
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
// refresh token 由服务端管理,JS 端无法写入 httpOnly cookie,忽略此调用。
|
|
48
|
+
async setRefreshToken(_token) {
|
|
42
49
|
},
|
|
43
50
|
async clear() {
|
|
44
|
-
|
|
45
|
-
|
|
51
|
+
memoryAccessToken = null;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
var RefreshTokenPayloadSchema = z.object({
|
|
56
|
+
accessToken: z.string().min(1),
|
|
57
|
+
refreshToken: z.string().min(1).optional(),
|
|
58
|
+
expiresIn: z.number().optional(),
|
|
59
|
+
tokenType: z.string().optional()
|
|
60
|
+
});
|
|
61
|
+
function createTokenManager(storage, refreshEndpointUrl, fetchFn, onRefreshFailed, refreshTimeoutMs = 1e4) {
|
|
62
|
+
const callbacks = [];
|
|
63
|
+
let refreshPromise = null;
|
|
64
|
+
let refreshController = null;
|
|
65
|
+
let generation = 0;
|
|
66
|
+
let sessionInvalid = false;
|
|
67
|
+
let storageMutation = Promise.resolve();
|
|
68
|
+
function mutateStorage(operation) {
|
|
69
|
+
const pending = storageMutation.then(operation);
|
|
70
|
+
storageMutation = pending.then(() => {
|
|
71
|
+
}, () => {
|
|
72
|
+
});
|
|
73
|
+
return pending;
|
|
74
|
+
}
|
|
75
|
+
function invalidateRefresh() {
|
|
76
|
+
const previousController = refreshController;
|
|
77
|
+
generation++;
|
|
78
|
+
refreshController = null;
|
|
79
|
+
refreshPromise = null;
|
|
80
|
+
return previousController;
|
|
81
|
+
}
|
|
82
|
+
async function failRefresh(refreshGeneration) {
|
|
83
|
+
await mutateStorage(async () => {
|
|
84
|
+
if (refreshGeneration !== generation)
|
|
85
|
+
return;
|
|
86
|
+
await storage.clear();
|
|
87
|
+
if (refreshGeneration === generation) {
|
|
88
|
+
sessionInvalid = true;
|
|
89
|
+
onRefreshFailed?.();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
async function refresh() {
|
|
94
|
+
if (refreshPromise)
|
|
95
|
+
return refreshPromise;
|
|
96
|
+
const controller = new AbortController();
|
|
97
|
+
const pending = doRefresh(generation, controller);
|
|
98
|
+
refreshController = controller;
|
|
99
|
+
refreshPromise = pending;
|
|
100
|
+
try {
|
|
101
|
+
return await pending;
|
|
102
|
+
} finally {
|
|
103
|
+
if (refreshPromise === pending) {
|
|
104
|
+
refreshPromise = null;
|
|
105
|
+
refreshController = null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function doRefresh(refreshGeneration, controller) {
|
|
110
|
+
await storageMutation;
|
|
111
|
+
if (refreshGeneration !== generation)
|
|
112
|
+
return null;
|
|
113
|
+
const refreshToken = await storage.getRefreshToken();
|
|
114
|
+
if (refreshGeneration !== generation)
|
|
115
|
+
return null;
|
|
116
|
+
if (!refreshToken) {
|
|
117
|
+
sessionInvalid = true;
|
|
118
|
+
logger.debug("Token refresh skipped, no refresh token available");
|
|
119
|
+
onRefreshFailed?.();
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
logger.debug("Refreshing token");
|
|
123
|
+
const timeoutId = setTimeout(() => controller.abort(), refreshTimeoutMs);
|
|
124
|
+
try {
|
|
125
|
+
const useHttpOnlyCookie = refreshToken === HTTPONLY_COOKIE_SENTINEL;
|
|
126
|
+
const requestInit = {
|
|
127
|
+
method: "POST",
|
|
128
|
+
body: useHttpOnlyCookie ? void 0 : JSON.stringify({ refreshToken }),
|
|
129
|
+
credentials: useHttpOnlyCookie ? "include" : "same-origin",
|
|
130
|
+
signal: controller.signal
|
|
131
|
+
};
|
|
132
|
+
if (!useHttpOnlyCookie) {
|
|
133
|
+
requestInit.headers = { "Content-Type": "application/json" };
|
|
134
|
+
}
|
|
135
|
+
const response = await fetchFn(refreshEndpointUrl, requestInit);
|
|
136
|
+
if (refreshGeneration !== generation)
|
|
137
|
+
return null;
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
if (response.status >= 400 && response.status < 500) {
|
|
140
|
+
await failRefresh(refreshGeneration);
|
|
141
|
+
} else {
|
|
142
|
+
logger.warn("Token refresh transient failure, keeping tokens", { status: response.status });
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
const body = await response.json();
|
|
147
|
+
if (refreshGeneration !== generation)
|
|
148
|
+
return null;
|
|
149
|
+
const parsed = RefreshTokenPayloadSchema.safeParse(readTokenPayload(body.data));
|
|
150
|
+
if (!parsed.success) {
|
|
151
|
+
logger.warn("Token refresh returned invalid data", { issues: parsed.error.issues });
|
|
152
|
+
await failRefresh(refreshGeneration);
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
if (!useHttpOnlyCookie && !parsed.data.refreshToken) {
|
|
156
|
+
logger.warn("Token refresh returned invalid data", { issues: [{ path: ["refreshToken"], message: "Required" }] });
|
|
157
|
+
await failRefresh(refreshGeneration);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
const refreshTokenForStorage = parsed.data.refreshToken ?? HTTPONLY_COOKIE_SENTINEL;
|
|
161
|
+
const tokens = {
|
|
162
|
+
accessToken: parsed.data.accessToken,
|
|
163
|
+
refreshToken: refreshTokenForStorage,
|
|
164
|
+
expiresIn: parsed.data.expiresIn ?? 3600,
|
|
165
|
+
tokenType: "Bearer"
|
|
166
|
+
};
|
|
167
|
+
const committed = await mutateStorage(async () => {
|
|
168
|
+
if (refreshGeneration !== generation)
|
|
169
|
+
return false;
|
|
170
|
+
await storage.setAccessToken(tokens.accessToken);
|
|
171
|
+
if (refreshGeneration !== generation)
|
|
172
|
+
return false;
|
|
173
|
+
await storage.setRefreshToken(refreshTokenForStorage);
|
|
174
|
+
return refreshGeneration === generation;
|
|
175
|
+
});
|
|
176
|
+
if (!committed || refreshGeneration !== generation)
|
|
177
|
+
return null;
|
|
178
|
+
sessionInvalid = false;
|
|
179
|
+
logger.debug("Token refreshed successfully");
|
|
180
|
+
for (const cb of callbacks) {
|
|
181
|
+
if (refreshGeneration !== generation)
|
|
182
|
+
return null;
|
|
183
|
+
cb(tokens);
|
|
184
|
+
}
|
|
185
|
+
return refreshGeneration === generation ? tokens : null;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (refreshGeneration !== generation)
|
|
188
|
+
return null;
|
|
189
|
+
logger.error("Token refresh failed (transient)", { error });
|
|
190
|
+
return null;
|
|
191
|
+
} finally {
|
|
192
|
+
clearTimeout(timeoutId);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
storage,
|
|
197
|
+
get generation() {
|
|
198
|
+
return generation;
|
|
199
|
+
},
|
|
200
|
+
get sessionInvalid() {
|
|
201
|
+
return sessionInvalid;
|
|
202
|
+
},
|
|
203
|
+
refresh,
|
|
204
|
+
async setTokens(tokens) {
|
|
205
|
+
const previousController = invalidateRefresh();
|
|
206
|
+
sessionInvalid = false;
|
|
207
|
+
const mutationGeneration = generation;
|
|
208
|
+
const pending = mutateStorage(async () => {
|
|
209
|
+
if (mutationGeneration !== generation)
|
|
210
|
+
return;
|
|
211
|
+
await storage.clear();
|
|
212
|
+
if (mutationGeneration !== generation)
|
|
213
|
+
return;
|
|
214
|
+
await storage.setAccessToken(tokens.accessToken);
|
|
215
|
+
if (tokens.refreshToken && mutationGeneration === generation)
|
|
216
|
+
await storage.setRefreshToken(tokens.refreshToken);
|
|
217
|
+
});
|
|
218
|
+
previousController?.abort();
|
|
219
|
+
await pending;
|
|
220
|
+
},
|
|
221
|
+
async clear() {
|
|
222
|
+
const previousController = invalidateRefresh();
|
|
223
|
+
const pending = mutateStorage(() => storage.clear());
|
|
224
|
+
previousController?.abort();
|
|
225
|
+
await pending;
|
|
226
|
+
},
|
|
227
|
+
onTokenRefreshed(callback) {
|
|
228
|
+
callbacks.push(callback);
|
|
229
|
+
return () => {
|
|
230
|
+
const idx = callbacks.indexOf(callback);
|
|
231
|
+
if (idx >= 0)
|
|
232
|
+
callbacks.splice(idx, 1);
|
|
233
|
+
};
|
|
46
234
|
}
|
|
47
235
|
};
|
|
48
236
|
}
|
|
237
|
+
function readTokenPayload(data) {
|
|
238
|
+
if (typeof data === "object" && data !== null && "tokens" in data) {
|
|
239
|
+
return data.tokens;
|
|
240
|
+
}
|
|
241
|
+
return data;
|
|
242
|
+
}
|
|
49
243
|
|
|
50
244
|
// messages/en-US.json
|
|
51
245
|
var en_US_default = {
|
|
@@ -57,12 +251,30 @@ var en_US_default = {
|
|
|
57
251
|
apiClient_notFound: "Resource not found",
|
|
58
252
|
apiClient_validationFailed: "Request validation failed",
|
|
59
253
|
apiClient_tokenRefreshFailed: "Token refresh failed, please log in again",
|
|
60
|
-
apiClient_notInitialized: "API client not initialized, please call
|
|
254
|
+
apiClient_notInitialized: "API client not initialized, please call apiClient.init() first",
|
|
255
|
+
apiClient_alreadyInitialized: "API client is already initialized; call apiClient.close() before init()",
|
|
61
256
|
apiClient_configError: "API client config error: {error}",
|
|
62
257
|
apiClient_responseValidationFailed: "Response validation failed",
|
|
63
258
|
apiClient_unknown: "Unknown error"
|
|
64
259
|
};
|
|
65
260
|
|
|
261
|
+
// messages/ja-JP.json
|
|
262
|
+
var ja_JP_default = {
|
|
263
|
+
apiClient_networkError: "\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u30EA\u30AF\u30A8\u30B9\u30C8\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
264
|
+
apiClient_timeout: "\u30EA\u30AF\u30A8\u30B9\u30C8\u304C\u30BF\u30A4\u30E0\u30A2\u30A6\u30C8\u3057\u307E\u3057\u305F",
|
|
265
|
+
apiClient_serverError: "\u30B5\u30FC\u30D0\u30FC\u30A8\u30E9\u30FC\uFF08{status}\uFF09",
|
|
266
|
+
apiClient_unauthorized: "\u8A8D\u8A3C\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002\u518D\u5EA6\u30ED\u30B0\u30A4\u30F3\u3057\u3066\u304F\u3060\u3055\u3044",
|
|
267
|
+
apiClient_forbidden: "\u30A2\u30AF\u30BB\u30B9\u304C\u62D2\u5426\u3055\u308C\u307E\u3057\u305F",
|
|
268
|
+
apiClient_notFound: "\u30EA\u30BD\u30FC\u30B9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093",
|
|
269
|
+
apiClient_validationFailed: "\u30EA\u30AF\u30A8\u30B9\u30C8\u306E\u691C\u8A3C\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
270
|
+
apiClient_tokenRefreshFailed: "\u30C8\u30FC\u30AF\u30F3\u306E\u66F4\u65B0\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002\u518D\u5EA6\u30ED\u30B0\u30A4\u30F3\u3057\u3066\u304F\u3060\u3055\u3044",
|
|
271
|
+
apiClient_notInitialized: "API\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u304C\u521D\u671F\u5316\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002\u5148\u306BapiClient.init()\u3092\u547C\u3073\u51FA\u3057\u3066\u304F\u3060\u3055\u3044",
|
|
272
|
+
apiClient_alreadyInitialized: "API\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u306F\u521D\u671F\u5316\u6E08\u307F\u3067\u3059\u3002init()\u306E\u524D\u306BapiClient.close()\u3092\u547C\u3073\u51FA\u3057\u3066\u304F\u3060\u3055\u3044",
|
|
273
|
+
apiClient_configError: "API\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u306E\u8A2D\u5B9A\u30A8\u30E9\u30FC: {error}",
|
|
274
|
+
apiClient_responseValidationFailed: "\u30EC\u30B9\u30DD\u30F3\u30B9\u306E\u691C\u8A3C\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
275
|
+
apiClient_unknown: "\u4E0D\u660E\u306A\u30A8\u30E9\u30FC"
|
|
276
|
+
};
|
|
277
|
+
|
|
66
278
|
// messages/zh-CN.json
|
|
67
279
|
var zh_CN_default = {
|
|
68
280
|
apiClient_networkError: "\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25",
|
|
@@ -73,7 +285,8 @@ var zh_CN_default = {
|
|
|
73
285
|
apiClient_notFound: "\u8BF7\u6C42\u7684\u8D44\u6E90\u4E0D\u5B58\u5728",
|
|
74
286
|
apiClient_validationFailed: "\u8BF7\u6C42\u53C2\u6570\u6821\u9A8C\u5931\u8D25",
|
|
75
287
|
apiClient_tokenRefreshFailed: "Token\u5237\u65B0\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55",
|
|
76
|
-
apiClient_notInitialized: "API \u5BA2\u6237\u7AEF\u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528
|
|
288
|
+
apiClient_notInitialized: "API \u5BA2\u6237\u7AEF\u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 apiClient.init()",
|
|
289
|
+
apiClient_alreadyInitialized: "API \u5BA2\u6237\u7AEF\u5DF2\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 apiClient.close() \u518D\u91CD\u65B0\u521D\u59CB\u5316",
|
|
77
290
|
apiClient_configError: "API \u5BA2\u6237\u7AEF\u914D\u7F6E\u9519\u8BEF\uFF1A{error}",
|
|
78
291
|
apiClient_responseValidationFailed: "\u54CD\u5E94\u6570\u636E\u6821\u9A8C\u5931\u8D25",
|
|
79
292
|
apiClient_unknown: "\u672A\u77E5\u9519\u8BEF"
|
|
@@ -82,8 +295,63 @@ var zh_CN_default = {
|
|
|
82
295
|
// src/api-client-i18n.ts
|
|
83
296
|
var apiClientM = core.i18n.createMessageGetter({
|
|
84
297
|
"zh-CN": zh_CN_default,
|
|
85
|
-
"en-US": en_US_default
|
|
298
|
+
"en-US": en_US_default,
|
|
299
|
+
"ja-JP": ja_JP_default
|
|
86
300
|
});
|
|
301
|
+
var pageExitBindings = /* @__PURE__ */ new WeakMap();
|
|
302
|
+
function clearIamPageExitBindings(auth) {
|
|
303
|
+
for (const stop of pageExitBindings.get(auth) ?? [])
|
|
304
|
+
stop();
|
|
305
|
+
pageExitBindings.delete(auth);
|
|
306
|
+
}
|
|
307
|
+
function createIamAuthActions(client) {
|
|
308
|
+
async function save(result) {
|
|
309
|
+
if (result.success)
|
|
310
|
+
await client.auth.setTokens(result.data.tokens);
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
bindPageExit(options = {}) {
|
|
315
|
+
if (!options.enabled || typeof window === "undefined")
|
|
316
|
+
return () => {
|
|
317
|
+
};
|
|
318
|
+
const target = window;
|
|
319
|
+
let sent = false;
|
|
320
|
+
const onPageHide = (event) => {
|
|
321
|
+
if (event.persisted || sent)
|
|
322
|
+
return;
|
|
323
|
+
sent = true;
|
|
324
|
+
void client.iam.auth.closeSession({}, { context: { keepalive: true, autoRefresh: false } }).then((result) => {
|
|
325
|
+
if (!result.success)
|
|
326
|
+
core.logger.warn("Page exit session revocation failed", { errorCode: result.error.code });
|
|
327
|
+
}).catch(() => core.logger.warn("Page exit session request did not complete"));
|
|
328
|
+
};
|
|
329
|
+
target.addEventListener("pagehide", onPageHide);
|
|
330
|
+
const bindings = pageExitBindings.get(client.auth) ?? /* @__PURE__ */ new Set();
|
|
331
|
+
const stop = () => {
|
|
332
|
+
target.removeEventListener("pagehide", onPageHide);
|
|
333
|
+
bindings.delete(stop);
|
|
334
|
+
if (!bindings.size)
|
|
335
|
+
pageExitBindings.delete(client.auth);
|
|
336
|
+
};
|
|
337
|
+
bindings.add(stop);
|
|
338
|
+
pageExitBindings.set(client.auth, bindings);
|
|
339
|
+
return stop;
|
|
340
|
+
},
|
|
341
|
+
publicConfig: () => client.iam.auth.publicConfig(),
|
|
342
|
+
createCaptcha: (input) => client.iam.auth.createCaptcha(input),
|
|
343
|
+
login: async (input) => {
|
|
344
|
+
const result = input.method === "ldap" ? await client.iam.auth.loginWithLdap({ username: input.identifier, password: input.password, captcha: input.captcha, expectedUserId: input.expectedUserId }) : input.method === "email" || input.method === "sms" ? await client.iam.auth.loginWithOtp({ identifier: input.identifier, code: input.code ?? "", captcha: input.captcha, expectedUserId: input.expectedUserId }) : await client.iam.auth.login({ identifier: input.identifier, password: input.password, captcha: input.captcha, expectedUserId: input.expectedUserId });
|
|
345
|
+
return save(result);
|
|
346
|
+
},
|
|
347
|
+
register: async (input) => save(await client.iam.auth.register(input)),
|
|
348
|
+
sendOtp: (identifier, options) => client.iam.auth.sendOtp({ identifier, ...options }),
|
|
349
|
+
requestPasswordReset: (identifier, options) => client.iam.auth.requestPasswordReset({ identifier, ...options }),
|
|
350
|
+
confirmPasswordReset: (token, newPassword) => client.iam.auth.confirmPasswordReset({ token, newPassword }),
|
|
351
|
+
renewPassword: (input) => client.iam.auth.renewPassword(input),
|
|
352
|
+
reauthenticate: (password, operation) => client.iam.auth.reauthenticate({ password, operation })
|
|
353
|
+
};
|
|
354
|
+
}
|
|
87
355
|
var ApiClientErrorInfo = {
|
|
88
356
|
NETWORK_ERROR: "001:502",
|
|
89
357
|
TIMEOUT: "002:504",
|
|
@@ -93,656 +361,433 @@ var ApiClientErrorInfo = {
|
|
|
93
361
|
NOT_FOUND: "006:404",
|
|
94
362
|
VALIDATION_FAILED: "007:400",
|
|
95
363
|
TOKEN_REFRESH_FAILED: "008:401",
|
|
364
|
+
PAYLOAD_TOO_LARGE: "020:413",
|
|
96
365
|
NOT_INITIALIZED: "010:500",
|
|
97
366
|
CONFIG_ERROR: "011:500",
|
|
98
367
|
UNKNOWN: "099:500"
|
|
99
368
|
};
|
|
100
369
|
var HaiApiClientError = core.error.buildHaiErrorsDef("api-client", ApiClientErrorInfo);
|
|
101
|
-
function defineEndpoint(def) {
|
|
102
|
-
return def;
|
|
103
|
-
}
|
|
104
370
|
|
|
105
|
-
// src/api-client-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
371
|
+
// src/api-client-main.ts
|
|
372
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
373
|
+
var DEFAULT_CLIENT_NAME = "hai-api-client";
|
|
374
|
+
var DEFAULT_REFRESH_PATH = apiContract.pathOf(apiContract.iam.auth.refresh);
|
|
375
|
+
var AUTH_ENTRY_PATHS = [
|
|
376
|
+
apiContract.pathOf(apiContract.iam.auth.login),
|
|
377
|
+
apiContract.pathOf(apiContract.iam.auth.loginWithOtp),
|
|
378
|
+
apiContract.pathOf(apiContract.iam.auth.loginWithLdap),
|
|
379
|
+
apiContract.pathOf(apiContract.iam.auth.requestPasswordReset),
|
|
380
|
+
apiContract.pathOf(apiContract.iam.auth.confirmPasswordReset),
|
|
381
|
+
apiContract.pathOf(apiContract.iam.auth.register)
|
|
382
|
+
];
|
|
383
|
+
var TRAILING_SLASHES_REGEX = /\/+$/;
|
|
384
|
+
var REQUEST_OBSERVATION = /* @__PURE__ */ Symbol("hai.api-client.request-observation");
|
|
385
|
+
var tokenStorage = {
|
|
386
|
+
/** 内存存储——SSR / Node 测试或一次性会话使用。 */
|
|
387
|
+
memory: createMemoryTokenStorage,
|
|
388
|
+
/** httpOnly Cookie 存储——浏览器场景推荐方案。 */
|
|
389
|
+
httpOnlyCookie: createHttpOnlyCookieTokenStorage
|
|
390
|
+
};
|
|
391
|
+
function createApiClient(contract) {
|
|
392
|
+
const state = {
|
|
393
|
+
config: null,
|
|
394
|
+
rawClient: null,
|
|
395
|
+
tokenManager: void 0,
|
|
396
|
+
transport: void 0,
|
|
397
|
+
observers: /* @__PURE__ */ new Set()
|
|
398
|
+
};
|
|
399
|
+
const auth = {
|
|
400
|
+
async setTokens(tokens) {
|
|
401
|
+
await state.tokenManager?.setTokens(tokens);
|
|
402
|
+
},
|
|
403
|
+
async clear() {
|
|
404
|
+
await state.tokenManager?.clear();
|
|
405
|
+
},
|
|
406
|
+
onTokenRefreshed(callback) {
|
|
407
|
+
return state.tokenManager?.onTokenRefreshed(callback) ?? (() => {
|
|
408
|
+
});
|
|
115
409
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
result = await fetchClient.post(endpoint.path, validInput);
|
|
124
|
-
break;
|
|
125
|
-
case "PUT":
|
|
126
|
-
result = await fetchClient.put(endpoint.path, validInput);
|
|
127
|
-
break;
|
|
128
|
-
case "PATCH":
|
|
129
|
-
result = await fetchClient.patch(endpoint.path, validInput);
|
|
130
|
-
break;
|
|
131
|
-
case "DELETE":
|
|
132
|
-
result = await fetchClient.delete(endpoint.path, validInput);
|
|
133
|
-
break;
|
|
134
|
-
default:
|
|
410
|
+
};
|
|
411
|
+
let initPromise = null;
|
|
412
|
+
const lifecycle = {
|
|
413
|
+
async init(config) {
|
|
414
|
+
if (initPromise)
|
|
415
|
+
return initPromise;
|
|
416
|
+
if (state.rawClient) {
|
|
135
417
|
return err(
|
|
136
|
-
HaiApiClientError.
|
|
137
|
-
apiClientM("
|
|
418
|
+
HaiApiClientError.CONFIG_ERROR,
|
|
419
|
+
apiClientM("apiClient_alreadyInitialized")
|
|
138
420
|
);
|
|
139
|
-
}
|
|
140
|
-
if (!result.success) {
|
|
141
|
-
return result;
|
|
142
|
-
}
|
|
143
|
-
const outputParsed = endpoint.output.safeParse(result.data);
|
|
144
|
-
if (!outputParsed.success) {
|
|
145
|
-
return err(
|
|
146
|
-
HaiApiClientError.VALIDATION_FAILED,
|
|
147
|
-
apiClientM("apiClient_responseValidationFailed"),
|
|
148
|
-
outputParsed.error.issues
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
return ok(outputParsed.data);
|
|
152
|
-
}
|
|
153
|
-
return call;
|
|
154
|
-
}
|
|
155
|
-
async function responseToError(response) {
|
|
156
|
-
let details;
|
|
157
|
-
try {
|
|
158
|
-
const body = await response.json();
|
|
159
|
-
details = body;
|
|
160
|
-
} catch {
|
|
161
|
-
}
|
|
162
|
-
const status = response.status;
|
|
163
|
-
if (status === 401) {
|
|
164
|
-
return err(
|
|
165
|
-
HaiApiClientError.UNAUTHORIZED,
|
|
166
|
-
apiClientM("apiClient_unauthorized"),
|
|
167
|
-
details
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
if (status === 403) {
|
|
171
|
-
return err(
|
|
172
|
-
HaiApiClientError.FORBIDDEN,
|
|
173
|
-
apiClientM("apiClient_forbidden"),
|
|
174
|
-
details
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
if (status === 404) {
|
|
178
|
-
return err(
|
|
179
|
-
HaiApiClientError.NOT_FOUND,
|
|
180
|
-
apiClientM("apiClient_notFound"),
|
|
181
|
-
details
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
if (status === 400 || status === 422) {
|
|
185
|
-
return err(
|
|
186
|
-
HaiApiClientError.VALIDATION_FAILED,
|
|
187
|
-
apiClientM("apiClient_validationFailed"),
|
|
188
|
-
details
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
if (status >= 500) {
|
|
192
|
-
return err(
|
|
193
|
-
HaiApiClientError.SERVER_ERROR,
|
|
194
|
-
apiClientM("apiClient_serverError", { params: { status: String(status) } }),
|
|
195
|
-
details
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
return err(
|
|
199
|
-
HaiApiClientError.UNKNOWN,
|
|
200
|
-
apiClientM("apiClient_unknown"),
|
|
201
|
-
details
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
function networkErrorToResult(cause) {
|
|
205
|
-
if (cause instanceof DOMException && cause.name === "AbortError") {
|
|
206
|
-
return err(
|
|
207
|
-
HaiApiClientError.TIMEOUT,
|
|
208
|
-
apiClientM("apiClient_timeout"),
|
|
209
|
-
cause
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
return err(
|
|
213
|
-
HaiApiClientError.NETWORK_ERROR,
|
|
214
|
-
apiClientM("apiClient_networkError"),
|
|
215
|
-
cause
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// src/api-client-fetch.ts
|
|
220
|
-
var logger = core.logger.child({ module: "api-client", scope: "fetch" });
|
|
221
|
-
var DEFAULT_TIMEOUT = 3e4;
|
|
222
|
-
function createFetchClient(config, tokenManager) {
|
|
223
|
-
const fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
224
|
-
const timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
225
|
-
const requestInterceptors = config.interceptors?.request ?? [];
|
|
226
|
-
const responseInterceptors = config.interceptors?.response ?? [];
|
|
227
|
-
function buildUrl(path, params) {
|
|
228
|
-
const base = config.baseUrl.replace(/\/+$/, "");
|
|
229
|
-
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
230
|
-
const url = new URL(`${base}${normalizedPath}`);
|
|
231
|
-
if (params) {
|
|
232
|
-
for (const [key, value] of Object.entries(params)) {
|
|
233
|
-
if (value !== void 0 && value !== null) {
|
|
234
|
-
url.searchParams.set(key, String(value));
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
return url.toString();
|
|
239
|
-
}
|
|
240
|
-
async function execute(requestConfig, retryOnUnauthorized = true) {
|
|
241
|
-
logger.trace("Executing request", { method: requestConfig.method, url: requestConfig.url });
|
|
242
|
-
if (tokenManager) {
|
|
243
|
-
const accessToken = await tokenManager.storage.getAccessToken();
|
|
244
|
-
if (accessToken) {
|
|
245
|
-
requestConfig.headers.Authorization = `Bearer ${accessToken}`;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
let finalConfig = requestConfig;
|
|
249
|
-
for (const interceptor of requestInterceptors) {
|
|
250
|
-
finalConfig = await interceptor(finalConfig);
|
|
251
|
-
}
|
|
252
|
-
const controller = new AbortController();
|
|
253
|
-
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
254
|
-
finalConfig.signal = controller.signal;
|
|
255
|
-
try {
|
|
256
|
-
let response = await fetchFn(finalConfig.url, {
|
|
257
|
-
method: finalConfig.method,
|
|
258
|
-
headers: finalConfig.headers,
|
|
259
|
-
body: finalConfig.body,
|
|
260
|
-
signal: finalConfig.signal
|
|
261
|
-
});
|
|
262
|
-
clearTimeout(timeoutId);
|
|
263
|
-
for (const interceptor of responseInterceptors) {
|
|
264
|
-
response = await interceptor(response);
|
|
265
421
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
422
|
+
initPromise = (async () => {
|
|
423
|
+
let nextTransport;
|
|
424
|
+
try {
|
|
425
|
+
nextTransport = buildTransportSession(config);
|
|
426
|
+
const nextTokenManager = buildTokenManager(config, nextTransport);
|
|
427
|
+
const nextRawClient = buildRawClient(contract, config, nextTokenManager, nextTransport);
|
|
428
|
+
state.config = config;
|
|
429
|
+
state.transport = nextTransport;
|
|
430
|
+
state.tokenManager = nextTokenManager;
|
|
431
|
+
state.rawClient = nextRawClient;
|
|
432
|
+
return ok(void 0);
|
|
433
|
+
} catch (error) {
|
|
434
|
+
nextTransport?.destroy();
|
|
435
|
+
state.config = null;
|
|
436
|
+
state.rawClient = null;
|
|
437
|
+
state.tokenManager = void 0;
|
|
438
|
+
state.transport = void 0;
|
|
439
|
+
return err(
|
|
440
|
+
HaiApiClientError.CONFIG_ERROR,
|
|
441
|
+
apiClientM("apiClient_configError", { params: { error: String(error) } }),
|
|
442
|
+
error
|
|
443
|
+
);
|
|
273
444
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
280
|
-
if (contentType.includes("application/json")) {
|
|
281
|
-
const body = await response.json();
|
|
282
|
-
if (body && typeof body === "object" && "data" in body) {
|
|
283
|
-
return ok(body.data);
|
|
284
|
-
}
|
|
285
|
-
return ok(body);
|
|
445
|
+
})();
|
|
446
|
+
try {
|
|
447
|
+
return await initPromise;
|
|
448
|
+
} finally {
|
|
449
|
+
initPromise = null;
|
|
286
450
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
451
|
+
},
|
|
452
|
+
async close() {
|
|
453
|
+
clearIamPageExitBindings(auth);
|
|
454
|
+
state.observers.clear();
|
|
455
|
+
state.transport?.destroy();
|
|
456
|
+
state.config = null;
|
|
457
|
+
state.rawClient = null;
|
|
458
|
+
state.tokenManager = void 0;
|
|
459
|
+
state.transport = void 0;
|
|
460
|
+
},
|
|
461
|
+
get config() {
|
|
462
|
+
return state.config ? core.sanitize.sanitizeSensitiveFields(state.config) : null;
|
|
463
|
+
},
|
|
464
|
+
get isInitialized() {
|
|
465
|
+
return state.rawClient !== null;
|
|
466
|
+
},
|
|
467
|
+
auth,
|
|
468
|
+
observeRequests(observer) {
|
|
469
|
+
state.observers.add(observer);
|
|
470
|
+
return () => {
|
|
471
|
+
state.observers.delete(observer);
|
|
472
|
+
};
|
|
292
473
|
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
async function post(path, body) {
|
|
302
|
-
return execute({
|
|
303
|
-
url: buildUrl(path),
|
|
304
|
-
method: "POST",
|
|
305
|
-
headers: { "Content-Type": "application/json", "Accept": "application/json" },
|
|
306
|
-
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
async function put(path, body) {
|
|
310
|
-
return execute({
|
|
311
|
-
url: buildUrl(path),
|
|
312
|
-
method: "PUT",
|
|
313
|
-
headers: { "Content-Type": "application/json", "Accept": "application/json" },
|
|
314
|
-
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
async function patch(path, body) {
|
|
318
|
-
return execute({
|
|
319
|
-
url: buildUrl(path),
|
|
320
|
-
method: "PATCH",
|
|
321
|
-
headers: { "Content-Type": "application/json", "Accept": "application/json" },
|
|
322
|
-
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
async function del(path, params) {
|
|
326
|
-
return execute({
|
|
327
|
-
url: buildUrl(path, params),
|
|
328
|
-
method: "DELETE",
|
|
329
|
-
headers: { Accept: "application/json" }
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
async function upload(path, file, options) {
|
|
333
|
-
const formData = new FormData();
|
|
334
|
-
formData.append(options?.fieldName ?? "file", file);
|
|
335
|
-
if (options?.extraFields) {
|
|
336
|
-
for (const [key, value] of Object.entries(options.extraFields)) {
|
|
337
|
-
formData.append(key, value);
|
|
474
|
+
};
|
|
475
|
+
return new Proxy({}, {
|
|
476
|
+
get(_target, property) {
|
|
477
|
+
if (typeof property !== "string")
|
|
478
|
+
return void 0;
|
|
479
|
+
if (property in lifecycle) {
|
|
480
|
+
return Reflect.get(lifecycle, property);
|
|
338
481
|
}
|
|
482
|
+
return createProcedureProxy(state, [property]);
|
|
339
483
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
484
|
+
// 这里的返回值在运行时是一个 Proxy,但从“可用能力”上看,它符合 `ApiClient<TContract>`:
|
|
485
|
+
// - 一部分成员来自 lifecycle
|
|
486
|
+
// - 另一部分成员来自 contract 推导出的 procedure 层级
|
|
487
|
+
// 因此使用类型断言把这个动态对象声明为最终对外的 typed client。
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
var defaultApiContract = apiContract.create({ iam: apiContract.iam, storage: apiContract.storage, ai: apiContract.ai });
|
|
491
|
+
var apiClient = new Proxy(createApiClient(defaultApiContract), {
|
|
492
|
+
get(target, property, receiver) {
|
|
493
|
+
if (property === "create")
|
|
494
|
+
return createApiClient;
|
|
495
|
+
if (property === "tokenStorage")
|
|
496
|
+
return tokenStorage;
|
|
497
|
+
if (property === "createAuthActions")
|
|
498
|
+
return createIamAuthActions;
|
|
499
|
+
return Reflect.get(target, property, receiver);
|
|
346
500
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
requestConfig.headers.Authorization = `Bearer ${accessToken}`;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
let finalConfig = requestConfig;
|
|
361
|
-
for (const interceptor of requestInterceptors) {
|
|
362
|
-
finalConfig = await interceptor(finalConfig);
|
|
363
|
-
}
|
|
364
|
-
const controller = new AbortController();
|
|
365
|
-
let timeoutId = null;
|
|
366
|
-
let timeoutTriggered = false;
|
|
367
|
-
const clearStreamTimeout = () => {
|
|
368
|
-
if (timeoutId) {
|
|
369
|
-
clearTimeout(timeoutId);
|
|
370
|
-
timeoutId = null;
|
|
371
|
-
}
|
|
372
|
-
};
|
|
373
|
-
const armStreamTimeout = () => {
|
|
374
|
-
clearStreamTimeout();
|
|
375
|
-
timeoutTriggered = false;
|
|
376
|
-
timeoutId = setTimeout(() => {
|
|
377
|
-
timeoutTriggered = true;
|
|
378
|
-
controller.abort();
|
|
379
|
-
}, timeout);
|
|
380
|
-
};
|
|
381
|
-
const externalSignal = options?.signal;
|
|
382
|
-
const onExternalAbort = () => {
|
|
383
|
-
controller.abort();
|
|
384
|
-
};
|
|
385
|
-
if (externalSignal) {
|
|
386
|
-
if (externalSignal.aborted) {
|
|
387
|
-
controller.abort();
|
|
388
|
-
} else {
|
|
389
|
-
externalSignal.addEventListener("abort", onExternalAbort, { once: true });
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
async function doStreamFetch() {
|
|
393
|
-
armStreamTimeout();
|
|
394
|
-
try {
|
|
395
|
-
return await fetchFn(finalConfig.url, {
|
|
396
|
-
method: finalConfig.method,
|
|
397
|
-
headers: finalConfig.headers,
|
|
398
|
-
body: finalConfig.body,
|
|
399
|
-
signal: controller.signal
|
|
400
|
-
});
|
|
401
|
-
} catch (cause) {
|
|
402
|
-
clearStreamTimeout();
|
|
403
|
-
throw cause;
|
|
404
|
-
}
|
|
501
|
+
});
|
|
502
|
+
function createProcedureProxy(state, path) {
|
|
503
|
+
return new Proxy(() => void 0, {
|
|
504
|
+
get(_t, property) {
|
|
505
|
+
if (typeof property !== "string")
|
|
506
|
+
return void 0;
|
|
507
|
+
return createProcedureProxy(state, [...path, property]);
|
|
508
|
+
},
|
|
509
|
+
apply(_t, _thisArg, args) {
|
|
510
|
+
return callObservedProcedure(state.rawClient, path, args, state.observers);
|
|
405
511
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}
|
|
423
|
-
if (!
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
while (true) {
|
|
430
|
-
armStreamTimeout();
|
|
431
|
-
const { done, value } = await reader.read();
|
|
432
|
-
clearStreamTimeout();
|
|
433
|
-
if (done)
|
|
434
|
-
break;
|
|
435
|
-
lineBuffer += decoder.decode(value, { stream: true });
|
|
436
|
-
const parts = lineBuffer.split("\n");
|
|
437
|
-
lineBuffer = parts.pop() ?? "";
|
|
438
|
-
for (const line of parts) {
|
|
439
|
-
if (line.startsWith("data: ")) {
|
|
440
|
-
const data = line.slice(6);
|
|
441
|
-
if (data !== "[DONE]") {
|
|
442
|
-
yield data;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
if (lineBuffer.startsWith("data: ")) {
|
|
448
|
-
const data = lineBuffer.slice(6);
|
|
449
|
-
if (data !== "[DONE]") {
|
|
450
|
-
yield data;
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
} catch (cause) {
|
|
454
|
-
if (cause instanceof DOMException && cause.name === "AbortError") {
|
|
455
|
-
if (externalSignal?.aborted && !timeoutTriggered) {
|
|
456
|
-
throw cause;
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
async function callObservedProcedure(rawClient, path, args, observers) {
|
|
515
|
+
const record = { requestId: core.context.get().requestId ?? core.id.uuid(), procedure: path.join("."), timestamp: Date.now(), duration: 0, success: false, attempts: 0 };
|
|
516
|
+
const provided = args[1] && typeof args[1] === "object" ? args[1] : {};
|
|
517
|
+
const providedContext = provided.context && typeof provided.context === "object" ? provided.context : {};
|
|
518
|
+
const options = { ...provided, context: { ...providedContext, [REQUEST_OBSERVATION]: record } };
|
|
519
|
+
let result = await callProcedure(rawClient, path, [args[0], options]);
|
|
520
|
+
record.duration = Math.max(0, Date.now() - record.timestamp);
|
|
521
|
+
record.success = !(result && typeof result === "object" && "success" in result && result.success === false);
|
|
522
|
+
if (result && typeof result === "object" && "error" in result && result.error && typeof result.error === "object" && "code" in result.error) {
|
|
523
|
+
record.errorCode = String(result.error.code);
|
|
524
|
+
if ("message" in result.error && typeof result.error.message === "string") {
|
|
525
|
+
const error = result.error;
|
|
526
|
+
const receivedId = error.traceId && /^[\w.:-]{1,128}$/.test(error.traceId) ? error.traceId : record.requestId;
|
|
527
|
+
record.requestId = receivedId;
|
|
528
|
+
const safe = core.error.toPublic({ ...error, publicExt: error.ext }, receivedId);
|
|
529
|
+
if (error.cause && typeof error.cause === "object" && !Array.isArray(error.cause)) {
|
|
530
|
+
const metadata = {};
|
|
531
|
+
for (const key of ["orpcCode", "status", "transportCode", "direction", "stage", "limitBytes", "actualBytes"]) {
|
|
532
|
+
const value = Reflect.get(error.cause, key);
|
|
533
|
+
if (typeof value === "number" || typeof value === "string")
|
|
534
|
+
metadata[key] = core.sanitize.sanitizeSensitiveValue(value, key);
|
|
457
535
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
throw cause;
|
|
461
|
-
} finally {
|
|
462
|
-
clearStreamTimeout();
|
|
463
|
-
if (externalSignal) {
|
|
464
|
-
externalSignal.removeEventListener("abort", onExternalAbort);
|
|
465
|
-
}
|
|
466
|
-
if (reader) {
|
|
467
|
-
reader.releaseLock();
|
|
536
|
+
if (Object.keys(metadata).length)
|
|
537
|
+
safe.cause = metadata;
|
|
468
538
|
}
|
|
539
|
+
core.logger.error("API request failed", { traceId: safe.traceId, requestId: record.requestId, procedure: record.procedure, status: record.status, error: result.error });
|
|
540
|
+
result = { ...result, error: safe };
|
|
469
541
|
}
|
|
470
542
|
}
|
|
471
|
-
|
|
472
|
-
}
|
|
473
|
-
var logger2 = core.logger.child({ module: "api-client", scope: "auth" });
|
|
474
|
-
var TokenPairSchema = z.object({
|
|
475
|
-
accessToken: z.string().min(1),
|
|
476
|
-
refreshToken: z.string().min(1),
|
|
477
|
-
expiresIn: z.number().optional(),
|
|
478
|
-
tokenType: z.string().optional()
|
|
479
|
-
});
|
|
480
|
-
function createTokenManager(storage, refreshUrl, fetchFn, onRefreshFailed) {
|
|
481
|
-
const callbacks = [];
|
|
482
|
-
let refreshPromise = null;
|
|
483
|
-
async function refresh() {
|
|
484
|
-
if (refreshPromise) {
|
|
485
|
-
return refreshPromise;
|
|
486
|
-
}
|
|
487
|
-
refreshPromise = doRefresh();
|
|
543
|
+
for (const observer of observers ?? []) {
|
|
488
544
|
try {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
545
|
+
const observed = observer(Object.freeze({ ...record }));
|
|
546
|
+
if (observed instanceof Promise)
|
|
547
|
+
void observed.catch(() => {
|
|
548
|
+
});
|
|
549
|
+
} catch {
|
|
492
550
|
}
|
|
493
551
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
return null;
|
|
500
|
-
}
|
|
501
|
-
logger2.debug("Refreshing token", { url: refreshUrl });
|
|
502
|
-
try {
|
|
503
|
-
const response = await fetchFn(refreshUrl, {
|
|
504
|
-
method: "POST",
|
|
505
|
-
headers: { "Content-Type": "application/json" },
|
|
506
|
-
body: JSON.stringify({ refreshToken })
|
|
507
|
-
});
|
|
508
|
-
if (!response.ok) {
|
|
509
|
-
await storage.clear();
|
|
510
|
-
onRefreshFailed?.();
|
|
511
|
-
return null;
|
|
512
|
-
}
|
|
513
|
-
const body = await response.json();
|
|
514
|
-
const parsed = TokenPairSchema.safeParse(body.data);
|
|
515
|
-
if (!parsed.success) {
|
|
516
|
-
logger2.warn("Token refresh returned invalid data", { issues: parsed.error.issues });
|
|
517
|
-
await storage.clear();
|
|
518
|
-
onRefreshFailed?.();
|
|
519
|
-
return null;
|
|
520
|
-
}
|
|
521
|
-
const tokens = {
|
|
522
|
-
accessToken: parsed.data.accessToken,
|
|
523
|
-
refreshToken: parsed.data.refreshToken,
|
|
524
|
-
expiresIn: parsed.data.expiresIn ?? 3600,
|
|
525
|
-
tokenType: "Bearer"
|
|
526
|
-
};
|
|
527
|
-
await storage.setAccessToken(tokens.accessToken);
|
|
528
|
-
await storage.setRefreshToken(tokens.refreshToken);
|
|
529
|
-
logger2.info("Token refreshed successfully");
|
|
530
|
-
for (const cb of callbacks) {
|
|
531
|
-
cb(tokens);
|
|
532
|
-
}
|
|
533
|
-
return tokens;
|
|
534
|
-
} catch (error) {
|
|
535
|
-
logger2.error("Token refresh failed", { error });
|
|
536
|
-
await storage.clear();
|
|
537
|
-
onRefreshFailed?.();
|
|
538
|
-
return null;
|
|
539
|
-
}
|
|
552
|
+
return result;
|
|
553
|
+
}
|
|
554
|
+
async function callProcedure(rawClient, path, args) {
|
|
555
|
+
if (!rawClient) {
|
|
556
|
+
return err(HaiApiClientError.NOT_INITIALIZED, apiClientM("apiClient_notInitialized"));
|
|
540
557
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
await storage.setAccessToken(tokens.accessToken);
|
|
546
|
-
await storage.setRefreshToken(tokens.refreshToken);
|
|
547
|
-
},
|
|
548
|
-
async clear() {
|
|
549
|
-
await storage.clear();
|
|
550
|
-
},
|
|
551
|
-
onTokenRefreshed(callback) {
|
|
552
|
-
callbacks.push(callback);
|
|
553
|
-
return () => {
|
|
554
|
-
const idx = callbacks.indexOf(callback);
|
|
555
|
-
if (idx >= 0)
|
|
556
|
-
callbacks.splice(idx, 1);
|
|
557
|
-
};
|
|
558
|
+
let target = rawClient;
|
|
559
|
+
for (const segment of path) {
|
|
560
|
+
if ((typeof target !== "object" || target === null) && typeof target !== "function") {
|
|
561
|
+
return err(HaiApiClientError.UNKNOWN, apiClientM("apiClient_unknown"));
|
|
558
562
|
}
|
|
559
|
-
|
|
563
|
+
target = Reflect.get(target, segment);
|
|
564
|
+
}
|
|
565
|
+
if (typeof target !== "function") {
|
|
566
|
+
return err(HaiApiClientError.UNKNOWN, apiClientM("apiClient_unknown"));
|
|
567
|
+
}
|
|
568
|
+
try {
|
|
569
|
+
return await target(...args);
|
|
570
|
+
} catch (error) {
|
|
571
|
+
return mapClientError(error);
|
|
572
|
+
}
|
|
560
573
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
const
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
config.
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
574
|
+
function buildTokenManager(config, transport) {
|
|
575
|
+
if (!config.auth)
|
|
576
|
+
return void 0;
|
|
577
|
+
const fetchFn = transport?.encryptedFetch ?? config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
578
|
+
const storage = config.auth.storage ?? createHttpOnlyCookieTokenStorage();
|
|
579
|
+
const baseUrl = config.baseUrl.replace(TRAILING_SLASHES_REGEX, "");
|
|
580
|
+
const refreshPath = config.auth.refreshPath ?? DEFAULT_REFRESH_PATH;
|
|
581
|
+
const refreshUrl = `${baseUrl}${refreshPath.startsWith("/") ? refreshPath : `/${refreshPath}`}`;
|
|
582
|
+
const refreshFetch = async (input, init) => {
|
|
583
|
+
const configured = typeof config.headers === "function" ? await config.headers() : config.headers ?? {};
|
|
584
|
+
const headers = new Headers(configured);
|
|
585
|
+
if (!headers.has("accept-language"))
|
|
586
|
+
headers.set("accept-language", core.i18n.getGlobalLocale());
|
|
587
|
+
headers.set("x-request-client", config.clientName ?? DEFAULT_CLIENT_NAME);
|
|
588
|
+
new Headers(init?.headers).forEach((value, name) => headers.set(name, value));
|
|
589
|
+
return fetchFn(input, { ...init, headers });
|
|
590
|
+
};
|
|
591
|
+
const manager = createTokenManager(storage, refreshUrl, refreshFetch, config.auth.recoverSession ? void 0 : config.auth.onRefreshFailed, config.auth.refreshTimeoutMs);
|
|
592
|
+
if (config.auth.onTokenRefreshed) {
|
|
593
|
+
manager.onTokenRefreshed(config.auth.onTokenRefreshed);
|
|
578
594
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
595
|
+
return manager;
|
|
596
|
+
}
|
|
597
|
+
function buildTransportSession(config) {
|
|
598
|
+
if (!config.transport)
|
|
599
|
+
return void 0;
|
|
600
|
+
const baseFetch = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
601
|
+
const baseUrl = config.baseUrl.replace(TRAILING_SLASHES_REGEX, "");
|
|
602
|
+
const path = config.transport.keyExchangePath ?? "/_hai/key-exchange";
|
|
603
|
+
const keyExchangeUrl = `${baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
|
|
604
|
+
return config.transport.crypto.transport.createClient({ keyExchangeUrl, fetch: baseFetch, maxPlaintextBytes: config.transport.maxPlaintextBytes });
|
|
605
|
+
}
|
|
606
|
+
function buildRawClient(contract, config, tokenManager, transport) {
|
|
607
|
+
const link = new OpenAPILink(contract, {
|
|
608
|
+
url: config.baseUrl,
|
|
609
|
+
headers: async (options) => {
|
|
610
|
+
const headers = await buildHeaders(config, tokenManager, getObservation(options.context)?.requestId);
|
|
611
|
+
if (typeof options.context.reauthentication === "string")
|
|
612
|
+
headers["x-hai-reauthentication"] = options.context.reauthentication;
|
|
613
|
+
return headers;
|
|
586
614
|
},
|
|
587
|
-
async
|
|
588
|
-
|
|
589
|
-
|
|
615
|
+
fetch: async (request, init, options) => {
|
|
616
|
+
const record = getObservation(options.context);
|
|
617
|
+
if (record) {
|
|
618
|
+
record.method = request.method;
|
|
619
|
+
const url = new URL(request.url);
|
|
620
|
+
url.search = "";
|
|
621
|
+
url.hash = "";
|
|
622
|
+
record.url = core.sanitize.sanitizeUrl(url.toString());
|
|
590
623
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
624
|
+
const requestInit = options.context.keepalive === true ? { ...init, keepalive: true } : init;
|
|
625
|
+
const confirm = options.context.reauthorize;
|
|
626
|
+
const reauthorize = typeof confirm === "function" ? async () => {
|
|
627
|
+
const value = await confirm();
|
|
628
|
+
return typeof value === "string" || value === void 0 ? value : null;
|
|
629
|
+
} : void 0;
|
|
630
|
+
const response = await fetchWithRefresh(request, requestInit, config, options.context.autoRefresh === false ? void 0 : tokenManager, transport, record, reauthorize);
|
|
631
|
+
if (record) {
|
|
632
|
+
record.status = response.status;
|
|
633
|
+
const traceId = response.headers.get("x-trace-id") ?? response.headers.get("x-request-id");
|
|
634
|
+
if (traceId && /^[\w.:-]{1,128}$/.test(traceId))
|
|
635
|
+
record.requestId = traceId;
|
|
595
636
|
}
|
|
596
|
-
return
|
|
597
|
-
};
|
|
637
|
+
return response;
|
|
598
638
|
}
|
|
599
|
-
};
|
|
639
|
+
});
|
|
640
|
+
return createORPCClient(link);
|
|
641
|
+
}
|
|
642
|
+
function getObservation(context) {
|
|
643
|
+
const record = context[REQUEST_OBSERVATION];
|
|
644
|
+
return record && typeof record === "object" && "requestId" in record ? record : void 0;
|
|
645
|
+
}
|
|
646
|
+
async function buildHeaders(config, tokenManager, requestId) {
|
|
647
|
+
const configured = typeof config.headers === "function" ? await config.headers() : config.headers ?? {};
|
|
648
|
+
const headers = new Headers(configured);
|
|
649
|
+
headers.set("x-hai-terminal", config.auth?.terminalType ?? "web");
|
|
650
|
+
if (!headers.has("accept-language"))
|
|
651
|
+
headers.set("accept-language", core.i18n.getGlobalLocale());
|
|
652
|
+
const accessToken = await tokenManager?.storage.getAccessToken();
|
|
600
653
|
return {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
delete: fetchClient.delete,
|
|
606
|
-
upload: fetchClient.upload,
|
|
607
|
-
stream: fetchClient.stream,
|
|
608
|
-
call,
|
|
609
|
-
auth
|
|
654
|
+
...Object.fromEntries(headers),
|
|
655
|
+
"x-request-client": config.clientName ?? DEFAULT_CLIENT_NAME,
|
|
656
|
+
"x-request-id": requestId ?? core.context.get().requestId ?? core.id.uuid(),
|
|
657
|
+
...accessToken ? { authorization: `Bearer ${accessToken}` } : {}
|
|
610
658
|
};
|
|
611
659
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
)
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
660
|
+
async function fetchWithRefresh(request, init, config, tokenManager, transport, observation, reauthorize) {
|
|
661
|
+
const retryRequest = tokenManager || reauthorize ? request.clone() : void 0;
|
|
662
|
+
const identityGeneration = tokenManager?.generation;
|
|
663
|
+
if (observation)
|
|
664
|
+
observation.attempts++;
|
|
665
|
+
const response = await fetchWithTimeout(request, init, config, transport);
|
|
666
|
+
if (!retryRequest)
|
|
667
|
+
return response;
|
|
668
|
+
async function retryAuthorized(accessToken2) {
|
|
669
|
+
if (!retryRequest || retryRequest.signal.aborted)
|
|
670
|
+
return response;
|
|
671
|
+
const generation = tokenManager?.generation;
|
|
672
|
+
const headers = new Headers(retryRequest.headers);
|
|
673
|
+
if (headers.has("x-hai-reauthentication") || reauthorize) {
|
|
674
|
+
if (!reauthorize)
|
|
675
|
+
return response;
|
|
676
|
+
const ticket = await reauthorize();
|
|
677
|
+
if (ticket === null || retryRequest.signal.aborted || tokenManager?.generation !== generation)
|
|
678
|
+
return response;
|
|
679
|
+
headers.delete("x-hai-reauthentication");
|
|
680
|
+
if (ticket)
|
|
681
|
+
headers.set("x-hai-reauthentication", ticket);
|
|
682
|
+
}
|
|
683
|
+
const currentToken = accessToken2 ?? await tokenManager?.storage.getAccessToken();
|
|
684
|
+
if (tokenManager?.generation !== generation)
|
|
685
|
+
return response;
|
|
686
|
+
if (currentToken)
|
|
687
|
+
headers.set("authorization", `Bearer ${currentToken}`);
|
|
688
|
+
if (observation)
|
|
689
|
+
observation.attempts++;
|
|
690
|
+
return fetchWithTimeout(new Request(retryRequest, { headers }), init, config, transport);
|
|
628
691
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
692
|
+
const baseUrl = config.baseUrl.replace(TRAILING_SLASHES_REGEX, "");
|
|
693
|
+
const refreshPath = config.auth?.refreshPath ?? DEFAULT_REFRESH_PATH;
|
|
694
|
+
const excludedPaths = [...AUTH_ENTRY_PATHS, refreshPath];
|
|
695
|
+
if (excludedPaths.some((path) => new URL(`${baseUrl}${path.startsWith("/") ? path : `/${path}`}`).pathname === new URL(request.url).pathname))
|
|
696
|
+
return response;
|
|
697
|
+
const failure = response.status === 401 || response.headers.get("content-type")?.includes("application/json") ? await response.clone().json().catch(() => void 0) : void 0;
|
|
698
|
+
const code = failure && typeof failure === "object" && "success" in failure && failure.success === false && "error" in failure && failure.error && typeof failure.error === "object" && "code" in failure.error ? failure.error.code : void 0;
|
|
699
|
+
if (code === "hai:iam:027" && reauthorize)
|
|
700
|
+
return retryAuthorized();
|
|
701
|
+
if (!tokenManager)
|
|
702
|
+
return response;
|
|
703
|
+
if (response.status !== 401 && !await isUnauthorizedResult(response))
|
|
704
|
+
return response;
|
|
705
|
+
if (failure && typeof failure === "object" && "error" in failure && failure.error && typeof failure.error === "object" && "code" in failure.error && typeof failure.error.code === "string" && failure.error.code.startsWith("hai:iam:") && !IamSessionErrorCodeSchema.safeParse(failure.error.code).success) {
|
|
706
|
+
return response;
|
|
707
|
+
}
|
|
708
|
+
if (tokenManager.generation !== identityGeneration)
|
|
709
|
+
return response;
|
|
710
|
+
const tokens = await tokenManager.refresh();
|
|
711
|
+
if (tokenManager.generation !== identityGeneration)
|
|
712
|
+
return response;
|
|
713
|
+
let accessToken = tokens?.accessToken;
|
|
714
|
+
if (!accessToken && config.auth?.recoverSession && tokenManager.sessionInvalid) {
|
|
715
|
+
if (await config.auth.recoverSession()) {
|
|
716
|
+
const restoredGeneration = tokenManager.generation;
|
|
717
|
+
accessToken = await tokenManager.storage.getAccessToken() ?? void 0;
|
|
718
|
+
if (tokenManager.generation !== restoredGeneration)
|
|
719
|
+
return response;
|
|
638
720
|
}
|
|
639
|
-
}
|
|
721
|
+
}
|
|
722
|
+
if (!accessToken)
|
|
723
|
+
return response;
|
|
724
|
+
return retryAuthorized(accessToken);
|
|
640
725
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
* @param config 客户端配置(baseUrl、auth 等)
|
|
649
|
-
* @returns 成功时返回 ok(undefined);失败时返回包含错误码和消息的 err。
|
|
650
|
-
*/
|
|
651
|
-
async init(config) {
|
|
652
|
-
if (initInProgress) {
|
|
653
|
-
return err(
|
|
654
|
-
HaiApiClientError.CONFIG_ERROR,
|
|
655
|
-
apiClientM("apiClient_configError", { params: { error: "initialization already in progress" } })
|
|
656
|
-
);
|
|
726
|
+
async function isUnauthorizedResult(response) {
|
|
727
|
+
if (response.status !== 200 || !response.headers.get("content-type")?.includes("application/json"))
|
|
728
|
+
return false;
|
|
729
|
+
try {
|
|
730
|
+
const body = await response.clone().json();
|
|
731
|
+
if (!body || typeof body !== "object" || !("success" in body) || body.success !== false || !("error" in body) || !body.error || typeof body.error !== "object") {
|
|
732
|
+
return false;
|
|
657
733
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
error
|
|
675
|
-
);
|
|
676
|
-
} finally {
|
|
677
|
-
initInProgress = false;
|
|
734
|
+
return "httpStatus" in body.error && body.error.httpStatus === 401 && "code" in body.error && typeof body.error.code === "string" && "message" in body.error && typeof body.error.message === "string";
|
|
735
|
+
} catch {
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
async function fetchWithTimeout(request, init, config, transport) {
|
|
740
|
+
const fetchFn = transport?.encryptedFetch ?? config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
741
|
+
const timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
742
|
+
const controller = new AbortController();
|
|
743
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
744
|
+
const userSignal = request.signal;
|
|
745
|
+
if (userSignal) {
|
|
746
|
+
if (userSignal.aborted) {
|
|
747
|
+
controller.abort();
|
|
748
|
+
} else {
|
|
749
|
+
userSignal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
678
750
|
}
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
},
|
|
700
|
-
/** 文件上传。未初始化时返回 NOT_INITIALIZED 错误 */
|
|
701
|
-
get upload() {
|
|
702
|
-
return currentClient?.upload ?? notInitializedOps.upload;
|
|
703
|
-
},
|
|
704
|
-
/** 契约调用。未初始化时返回 NOT_INITIALIZED 错误 */
|
|
705
|
-
get call() {
|
|
706
|
-
return currentClient?.call ?? notInitializedOps.call;
|
|
707
|
-
},
|
|
708
|
-
/**
|
|
709
|
-
* 流式请求。未初始化时迭代抛出异常。
|
|
710
|
-
*
|
|
711
|
-
* @throws 未初始化时抛出 Error
|
|
712
|
-
*/
|
|
713
|
-
get stream() {
|
|
714
|
-
return currentClient?.stream ?? notInitializedStreamFn;
|
|
715
|
-
},
|
|
716
|
-
/** Token 管理接口。未初始化时为空操作 */
|
|
717
|
-
get auth() {
|
|
718
|
-
return currentClient?.auth ?? notInitializedAuth;
|
|
719
|
-
},
|
|
720
|
-
/** 当前客户端配置;未初始化或已关闭时为 null */
|
|
721
|
-
get config() {
|
|
722
|
-
return currentConfig;
|
|
723
|
-
},
|
|
724
|
-
/** 是否已完成初始化 */
|
|
725
|
-
get isInitialized() {
|
|
726
|
-
return currentClient !== null;
|
|
727
|
-
},
|
|
728
|
-
/**
|
|
729
|
-
* 关闭 API 客户端并释放资源
|
|
730
|
-
*
|
|
731
|
-
* 关闭后所有 HTTP 操作将返回 NOT_INITIALIZED 错误。
|
|
732
|
-
* 重复调用不会报错。
|
|
733
|
-
*/
|
|
734
|
-
async close() {
|
|
735
|
-
if (!currentClient) {
|
|
736
|
-
currentConfig = null;
|
|
737
|
-
return;
|
|
751
|
+
}
|
|
752
|
+
try {
|
|
753
|
+
return await fetchFn(new Request(request, { signal: controller.signal }), init);
|
|
754
|
+
} finally {
|
|
755
|
+
clearTimeout(timeoutId);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
function mapClientError(error) {
|
|
759
|
+
const time = error instanceof TransportTimeError ? error : error instanceof Error && error.cause instanceof TransportTimeError ? error.cause : void 0;
|
|
760
|
+
if (time)
|
|
761
|
+
return err(HaiApiClientError.VALIDATION_FAILED, time.message, { transportCode: time.code });
|
|
762
|
+
const capacity = error instanceof TransportBodyTooLargeError ? error : error instanceof Error && error.cause instanceof TransportBodyTooLargeError ? error.cause : void 0;
|
|
763
|
+
if (capacity)
|
|
764
|
+
return err(HaiApiClientError.PAYLOAD_TOO_LARGE, capacity.message, { direction: capacity.direction, stage: capacity.stage, limitBytes: capacity.limitBytes, actualBytes: capacity.actualBytes });
|
|
765
|
+
if (error instanceof ORPCError) {
|
|
766
|
+
const body = error.data && typeof error.data === "object" && "body" in error.data ? error.data.body : void 0;
|
|
767
|
+
if (body && typeof body === "object" && "success" in body && body.success === false && "error" in body) {
|
|
768
|
+
const parsed = HaiErrorSchema.safeParse(body.error);
|
|
769
|
+
if (parsed.success)
|
|
770
|
+
return err({ ...parsed.data, httpStatus: error.status });
|
|
738
771
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
772
|
+
const detail = { orpcCode: error.code, status: error.status, data: error.data };
|
|
773
|
+
if (error.status === 413)
|
|
774
|
+
return err(HaiApiClientError.PAYLOAD_TOO_LARGE, error.message, detail);
|
|
775
|
+
if (error.status === 400 || error.status === 422)
|
|
776
|
+
return err(HaiApiClientError.VALIDATION_FAILED, error.message, detail);
|
|
777
|
+
if (error.status === 401)
|
|
778
|
+
return err(HaiApiClientError.UNAUTHORIZED, error.message, detail);
|
|
779
|
+
if (error.status === 403)
|
|
780
|
+
return err(HaiApiClientError.FORBIDDEN, error.message, detail);
|
|
781
|
+
if (error.status === 404)
|
|
782
|
+
return err(HaiApiClientError.NOT_FOUND, error.message, detail);
|
|
783
|
+
return err(HaiApiClientError.SERVER_ERROR, error.message, detail);
|
|
743
784
|
}
|
|
744
|
-
|
|
785
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
786
|
+
return err(HaiApiClientError.TIMEOUT, apiClientM("apiClient_timeout"), error);
|
|
787
|
+
}
|
|
788
|
+
return err(HaiApiClientError.NETWORK_ERROR, apiClientM("apiClient_networkError"), error);
|
|
789
|
+
}
|
|
745
790
|
|
|
746
|
-
export { HaiApiClientError,
|
|
791
|
+
export { HaiApiClientError, apiClient };
|
|
747
792
|
//# sourceMappingURL=index.js.map
|
|
748
793
|
//# sourceMappingURL=index.js.map
|