@h-ai/api-client 0.1.0-alpha.66 → 0.1.0-alpha.68

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
@@ -7,29 +7,6 @@ import { z } from 'zod';
7
7
  // src/api-client-main.ts
8
8
  var logger = core.logger.child({ module: "api-client", scope: "auth" });
9
9
  var HTTPONLY_COOKIE_SENTINEL = "__httponly_cookie__";
10
- var LS_ACCESS_KEY = "hai_access_token";
11
- var LS_REFRESH_KEY = "hai_refresh_token";
12
- function createLocalStorageTokenStorage() {
13
- logger.warn("apiClient.tokenStorage.localStorage: storing tokens in localStorage exposes them to XSS. Prefer httpOnly cookies for production.");
14
- return {
15
- async getAccessToken() {
16
- return globalThis.localStorage?.getItem(LS_ACCESS_KEY) ?? null;
17
- },
18
- async setAccessToken(token) {
19
- globalThis.localStorage?.setItem(LS_ACCESS_KEY, token);
20
- },
21
- async getRefreshToken() {
22
- return globalThis.localStorage?.getItem(LS_REFRESH_KEY) ?? null;
23
- },
24
- async setRefreshToken(token) {
25
- globalThis.localStorage?.setItem(LS_REFRESH_KEY, token);
26
- },
27
- async clear() {
28
- globalThis.localStorage?.removeItem(LS_ACCESS_KEY);
29
- globalThis.localStorage?.removeItem(LS_REFRESH_KEY);
30
- }
31
- };
32
- }
33
10
  function createMemoryTokenStorage() {
34
11
  let accessToken = null;
35
12
  let refreshToken = null;
@@ -83,25 +60,61 @@ var RefreshTokenPayloadSchema = z.object({
83
60
  function createTokenManager(storage, refreshEndpointUrl, fetchFn, onRefreshFailed, refreshTimeoutMs = 1e4) {
84
61
  const callbacks = [];
85
62
  let refreshPromise = null;
63
+ let refreshController = null;
64
+ let generation = 0;
65
+ let storageMutation = Promise.resolve();
66
+ function mutateStorage(operation) {
67
+ const pending = storageMutation.then(operation);
68
+ storageMutation = pending.then(() => {
69
+ }, () => {
70
+ });
71
+ return pending;
72
+ }
73
+ function invalidateRefresh() {
74
+ const previousController = refreshController;
75
+ generation++;
76
+ refreshController = null;
77
+ refreshPromise = null;
78
+ return previousController;
79
+ }
80
+ async function failRefresh(refreshGeneration) {
81
+ await mutateStorage(async () => {
82
+ if (refreshGeneration !== generation)
83
+ return;
84
+ await storage.clear();
85
+ if (refreshGeneration === generation)
86
+ onRefreshFailed?.();
87
+ });
88
+ }
86
89
  async function refresh() {
87
90
  if (refreshPromise)
88
91
  return refreshPromise;
89
- refreshPromise = doRefresh();
92
+ const controller = new AbortController();
93
+ const pending = doRefresh(generation, controller);
94
+ refreshController = controller;
95
+ refreshPromise = pending;
90
96
  try {
91
- return await refreshPromise;
97
+ return await pending;
92
98
  } finally {
93
- refreshPromise = null;
99
+ if (refreshPromise === pending) {
100
+ refreshPromise = null;
101
+ refreshController = null;
102
+ }
94
103
  }
95
104
  }
96
- async function doRefresh() {
105
+ async function doRefresh(refreshGeneration, controller) {
106
+ await storageMutation;
107
+ if (refreshGeneration !== generation)
108
+ return null;
97
109
  const refreshToken = await storage.getRefreshToken();
110
+ if (refreshGeneration !== generation)
111
+ return null;
98
112
  if (!refreshToken) {
99
113
  logger.warn("Token refresh skipped, no refresh token available");
100
114
  onRefreshFailed?.();
101
115
  return null;
102
116
  }
103
117
  logger.debug("Refreshing token");
104
- const controller = new AbortController();
105
118
  const timeoutId = setTimeout(() => controller.abort(), refreshTimeoutMs);
106
119
  try {
107
120
  const useHttpOnlyCookie = refreshToken === HTTPONLY_COOKIE_SENTINEL;
@@ -115,27 +128,28 @@ function createTokenManager(storage, refreshEndpointUrl, fetchFn, onRefreshFaile
115
128
  requestInit.headers = { "Content-Type": "application/json" };
116
129
  }
117
130
  const response = await fetchFn(refreshEndpointUrl, requestInit);
131
+ if (refreshGeneration !== generation)
132
+ return null;
118
133
  if (!response.ok) {
119
134
  if (response.status >= 400 && response.status < 500) {
120
- await storage.clear();
121
- onRefreshFailed?.();
135
+ await failRefresh(refreshGeneration);
122
136
  } else {
123
137
  logger.warn("Token refresh transient failure, keeping tokens", { status: response.status });
124
138
  }
125
139
  return null;
126
140
  }
127
141
  const body = await response.json();
142
+ if (refreshGeneration !== generation)
143
+ return null;
128
144
  const parsed = RefreshTokenPayloadSchema.safeParse(readTokenPayload(body.data));
129
145
  if (!parsed.success) {
130
146
  logger.warn("Token refresh returned invalid data", { issues: parsed.error.issues });
131
- await storage.clear();
132
- onRefreshFailed?.();
147
+ await failRefresh(refreshGeneration);
133
148
  return null;
134
149
  }
135
150
  if (!useHttpOnlyCookie && !parsed.data.refreshToken) {
136
151
  logger.warn("Token refresh returned invalid data", { issues: [{ path: ["refreshToken"], message: "Required" }] });
137
- await storage.clear();
138
- onRefreshFailed?.();
152
+ await failRefresh(refreshGeneration);
139
153
  return null;
140
154
  }
141
155
  const refreshTokenForStorage = parsed.data.refreshToken ?? HTTPONLY_COOKIE_SENTINEL;
@@ -145,12 +159,27 @@ function createTokenManager(storage, refreshEndpointUrl, fetchFn, onRefreshFaile
145
159
  expiresIn: parsed.data.expiresIn ?? 3600,
146
160
  tokenType: "Bearer"
147
161
  };
148
- await storage.setAccessToken(tokens.accessToken);
149
- await storage.setRefreshToken(refreshTokenForStorage);
162
+ const committed = await mutateStorage(async () => {
163
+ if (refreshGeneration !== generation)
164
+ return false;
165
+ await storage.setAccessToken(tokens.accessToken);
166
+ if (refreshGeneration !== generation)
167
+ return false;
168
+ await storage.setRefreshToken(refreshTokenForStorage);
169
+ return refreshGeneration === generation;
170
+ });
171
+ if (!committed || refreshGeneration !== generation)
172
+ return null;
150
173
  logger.info("Token refreshed successfully");
151
- for (const cb of callbacks) cb(tokens);
152
- return tokens;
174
+ for (const cb of callbacks) {
175
+ if (refreshGeneration !== generation)
176
+ return null;
177
+ cb(tokens);
178
+ }
179
+ return refreshGeneration === generation ? tokens : null;
153
180
  } catch (error) {
181
+ if (refreshGeneration !== generation)
182
+ return null;
154
183
  logger.error("Token refresh failed (transient)", { error });
155
184
  return null;
156
185
  } finally {
@@ -161,12 +190,26 @@ function createTokenManager(storage, refreshEndpointUrl, fetchFn, onRefreshFaile
161
190
  storage,
162
191
  refresh,
163
192
  async setTokens(tokens) {
164
- await storage.setAccessToken(tokens.accessToken);
165
- if (tokens.refreshToken)
166
- await storage.setRefreshToken(tokens.refreshToken);
193
+ const previousController = invalidateRefresh();
194
+ const mutationGeneration = generation;
195
+ const pending = mutateStorage(async () => {
196
+ if (mutationGeneration !== generation)
197
+ return;
198
+ await storage.clear();
199
+ if (mutationGeneration !== generation)
200
+ return;
201
+ await storage.setAccessToken(tokens.accessToken);
202
+ if (tokens.refreshToken && mutationGeneration === generation)
203
+ await storage.setRefreshToken(tokens.refreshToken);
204
+ });
205
+ previousController?.abort();
206
+ await pending;
167
207
  },
168
208
  async clear() {
169
- await storage.clear();
209
+ const previousController = invalidateRefresh();
210
+ const pending = mutateStorage(() => storage.clear());
211
+ previousController?.abort();
212
+ await pending;
170
213
  },
171
214
  onTokenRefreshed(callback) {
172
215
  callbacks.push(callback);
@@ -202,6 +245,23 @@ var en_US_default = {
202
245
  apiClient_unknown: "Unknown error"
203
246
  };
204
247
 
248
+ // messages/ja-JP.json
249
+ var ja_JP_default = {
250
+ apiClient_networkError: "\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u30EA\u30AF\u30A8\u30B9\u30C8\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
251
+ apiClient_timeout: "\u30EA\u30AF\u30A8\u30B9\u30C8\u304C\u30BF\u30A4\u30E0\u30A2\u30A6\u30C8\u3057\u307E\u3057\u305F",
252
+ apiClient_serverError: "\u30B5\u30FC\u30D0\u30FC\u30A8\u30E9\u30FC\uFF08{status}\uFF09",
253
+ apiClient_unauthorized: "\u8A8D\u8A3C\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002\u518D\u5EA6\u30ED\u30B0\u30A4\u30F3\u3057\u3066\u304F\u3060\u3055\u3044",
254
+ apiClient_forbidden: "\u30A2\u30AF\u30BB\u30B9\u304C\u62D2\u5426\u3055\u308C\u307E\u3057\u305F",
255
+ apiClient_notFound: "\u30EA\u30BD\u30FC\u30B9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093",
256
+ apiClient_validationFailed: "\u30EA\u30AF\u30A8\u30B9\u30C8\u306E\u691C\u8A3C\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
257
+ 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",
258
+ 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",
259
+ 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",
260
+ apiClient_configError: "API\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u306E\u8A2D\u5B9A\u30A8\u30E9\u30FC: {error}",
261
+ apiClient_responseValidationFailed: "\u30EC\u30B9\u30DD\u30F3\u30B9\u306E\u691C\u8A3C\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
262
+ apiClient_unknown: "\u4E0D\u660E\u306A\u30A8\u30E9\u30FC"
263
+ };
264
+
205
265
  // messages/zh-CN.json
206
266
  var zh_CN_default = {
207
267
  apiClient_networkError: "\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25",
@@ -222,7 +282,8 @@ var zh_CN_default = {
222
282
  // src/api-client-i18n.ts
223
283
  var apiClientM = core.i18n.createMessageGetter({
224
284
  "zh-CN": zh_CN_default,
225
- "en-US": en_US_default
285
+ "en-US": en_US_default,
286
+ "ja-JP": ja_JP_default
226
287
  });
227
288
 
228
289
  // src/api-client-iam.ts
@@ -235,7 +296,7 @@ function createIamAuthActions(client) {
235
296
  return {
236
297
  publicConfig: () => client.iam.auth.publicConfig(),
237
298
  login: async (input) => {
238
- const result = input.method === "ldap" ? await client.iam.auth.loginWithLdap({ username: input.identifier, password: input.password }) : input.method === "email" || input.method === "sms" ? await client.iam.auth.loginWithOtp({ identifier: input.identifier, code: input.code ?? "" }) : await client.iam.auth.login({ identifier: input.identifier, password: input.password, rememberMe: input.rememberMe });
299
+ const result = input.method === "ldap" ? await client.iam.auth.loginWithLdap({ username: input.identifier, password: input.password }) : input.method === "email" || input.method === "sms" ? await client.iam.auth.loginWithOtp({ identifier: input.identifier, code: input.code ?? "" }) : await client.iam.auth.login({ identifier: input.identifier, password: input.password });
239
300
  return save(result);
240
301
  },
241
302
  register: async (input) => save(await client.iam.auth.register(input)),
@@ -275,8 +336,6 @@ var TRAILING_SLASHES_REGEX = /\/+$/;
275
336
  var tokenStorage = {
276
337
  /** 内存存储——SSR / Node 测试或一次性会话使用。 */
277
338
  memory: createMemoryTokenStorage,
278
- /** 浏览器 `localStorage` 存储——单页应用便捷选项;注意 XSS 风险。 */
279
- localStorage: createLocalStorageTokenStorage,
280
339
  /** httpOnly Cookie 存储——浏览器场景推荐方案。 */
281
340
  httpOnlyCookie: createHttpOnlyCookieTokenStorage
282
341
  };
@@ -425,6 +484,8 @@ function buildTokenManager(config, transport) {
425
484
  const refreshFetch = async (input, init) => {
426
485
  const configured = typeof config.headers === "function" ? await config.headers() : config.headers ?? {};
427
486
  const headers = new Headers(configured);
487
+ if (!headers.has("accept-language"))
488
+ headers.set("accept-language", core.i18n.getGlobalLocale());
428
489
  headers.set("x-request-client", config.clientName ?? DEFAULT_CLIENT_NAME);
429
490
  new Headers(init?.headers).forEach((value, name) => headers.set(name, value));
430
491
  return fetchFn(input, { ...init, headers });
@@ -454,9 +515,12 @@ function buildRawClient(contract, config, tokenManager, transport) {
454
515
  }
455
516
  async function buildHeaders(config, tokenManager) {
456
517
  const configured = typeof config.headers === "function" ? await config.headers() : config.headers ?? {};
518
+ const headers = new Headers(configured);
519
+ if (!headers.has("accept-language"))
520
+ headers.set("accept-language", core.i18n.getGlobalLocale());
457
521
  const accessToken = await tokenManager?.storage.getAccessToken();
458
522
  return {
459
- ...configured,
523
+ ...Object.fromEntries(headers),
460
524
  "x-request-client": config.clientName ?? DEFAULT_CLIENT_NAME,
461
525
  ...accessToken ? { authorization: `Bearer ${accessToken}` } : {}
462
526
  };
@@ -515,6 +579,8 @@ async function fetchWithTimeout(request, init, config, transport) {
515
579
  function mapClientError(error) {
516
580
  if (error instanceof ORPCError) {
517
581
  const detail = { orpcCode: error.code, status: error.status, data: error.data };
582
+ if (error.status === 400 || error.status === 422)
583
+ return err(HaiApiClientError.VALIDATION_FAILED, error.message, detail);
518
584
  if (error.status === 401)
519
585
  return err(HaiApiClientError.UNAUTHORIZED, error.message, detail);
520
586
  if (error.status === 403)