@h-ai/api-client 0.1.0-alpha.67 → 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/README.en.md +21 -3
- package/README.ja.md +21 -3
- package/README.md +21 -3
- package/dist/index.d.ts +518 -179
- package/dist/index.js +113 -47
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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
|
-
|
|
92
|
+
const controller = new AbortController();
|
|
93
|
+
const pending = doRefresh(generation, controller);
|
|
94
|
+
refreshController = controller;
|
|
95
|
+
refreshPromise = pending;
|
|
90
96
|
try {
|
|
91
|
-
return await
|
|
97
|
+
return await pending;
|
|
92
98
|
} finally {
|
|
93
|
-
refreshPromise
|
|
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
|
|
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
|
|
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
|
|
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
|
|
149
|
-
|
|
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)
|
|
152
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
...
|
|
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)
|