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

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,51 +1,245 @@
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
+ function createMemoryTokenStorage() {
12
+ let accessToken = null;
13
+ let refreshToken = null;
8
14
  return {
9
15
  async getAccessToken() {
10
- return globalThis.localStorage?.getItem(LS_ACCESS_KEY) ?? null;
16
+ return accessToken;
11
17
  },
12
18
  async setAccessToken(token) {
13
- globalThis.localStorage?.setItem(LS_ACCESS_KEY, token);
19
+ accessToken = token;
14
20
  },
15
21
  async getRefreshToken() {
16
- return globalThis.localStorage?.getItem(LS_REFRESH_KEY) ?? null;
22
+ return refreshToken;
17
23
  },
18
24
  async setRefreshToken(token) {
19
- globalThis.localStorage?.setItem(LS_REFRESH_KEY, token);
25
+ refreshToken = token;
20
26
  },
21
27
  async clear() {
22
- globalThis.localStorage?.removeItem(LS_ACCESS_KEY);
23
- globalThis.localStorage?.removeItem(LS_REFRESH_KEY);
28
+ accessToken = null;
29
+ refreshToken = null;
24
30
  }
25
31
  };
26
32
  }
27
- function createMemoryTokenStorage() {
28
- let accessToken = null;
29
- let refreshToken = null;
33
+ function createHttpOnlyCookieTokenStorage() {
34
+ let memoryAccessToken = null;
30
35
  return {
31
36
  async getAccessToken() {
32
- return accessToken;
37
+ return memoryAccessToken;
33
38
  },
34
39
  async setAccessToken(token) {
35
- accessToken = token;
40
+ memoryAccessToken = token;
36
41
  },
42
+ // refresh token 由服务端通过 Set-Cookie 设置为 httpOnly cookie;
43
+ // JS 不可见,返回哨兵值让 TokenManager 识别此模式。
37
44
  async getRefreshToken() {
38
- return refreshToken;
45
+ return HTTPONLY_COOKIE_SENTINEL;
39
46
  },
40
- async setRefreshToken(token) {
41
- refreshToken = token;
47
+ // refresh token 由服务端管理,JS 端无法写入 httpOnly cookie,忽略此调用。
48
+ async setRefreshToken(_token) {
42
49
  },
43
50
  async clear() {
44
- accessToken = null;
45
- refreshToken = null;
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 api.init() first",
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 api.init()",
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-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
- );
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
- 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:
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.UNKNOWN,
137
- apiClientM("apiClient_unknown")
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
- 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);
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
- logger.warn("Token refresh failed, returning 401 error", { url: requestConfig.url });
275
- }
276
- if (!response.ok) {
277
- return responseToError(response);
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
- return ok(void 0);
288
- } catch (cause) {
289
- clearTimeout(timeoutId);
290
- logger.warn("Request failed", { url: requestConfig.url, error: cause });
291
- return networkErrorToResult(cause);
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
- 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);
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
- return execute({
341
- url: buildUrl(path),
342
- method: "POST",
343
- headers: { Accept: "application/json" },
344
- body: formData
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
- 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
- }
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
- 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;
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
- 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();
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
- 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();
543
+ for (const observer of observers ?? []) {
488
544
  try {
489
- return await refreshPromise;
490
- } finally {
491
- refreshPromise = null;
545
+ const observed = observer(Object.freeze({ ...record }));
546
+ if (observed instanceof Promise)
547
+ void observed.catch(() => {
548
+ });
549
+ } catch {
492
550
  }
493
551
  }
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
- }
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
- 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
- };
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
- // 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);
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
- 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
- }
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 clear() {
588
- if (tokenManager) {
589
- await tokenManager.clear();
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
- onTokenRefreshed(callback) {
593
- if (tokenManager) {
594
- return tokenManager.onTokenRefreshed(callback);
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
- 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
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
- 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
- };
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
- function notInitializedStreamFn(_path, _body, _options) {
631
- return {
632
- [Symbol.asyncIterator]() {
633
- return {
634
- async next() {
635
- throw new Error(apiClientM("apiClient_notInitialized"));
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
- 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
- );
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
- 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;
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
- /** 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;
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
- logger3.info("Closing api-client module");
740
- currentClient = null;
741
- currentConfig = null;
742
- logger3.info("Api-client module closed");
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, api, createLocalStorageTokenStorage, createMemoryTokenStorage, defineEndpoint };
791
+ export { HaiApiClientError, apiClient };
747
792
  //# sourceMappingURL=index.js.map
748
793
  //# sourceMappingURL=index.js.map