@h-ai/api-client 0.1.0-alpha9 → 1.0.0-beta.10

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