@h-ai/api-client 0.1.0-alpha.13 → 0.1.0-alpha.16
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.md +61 -283
- package/dist/index.d.ts +1552 -268
- package/dist/index.js +327 -617
- package/dist/index.js.map +1 -1
- package/package.json +7 -2
package/dist/index.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
import { IAM_AUTH_ROUTES, apiContract } from '@h-ai/api-contract';
|
|
1
2
|
import { core, ok, err } from '@h-ai/core';
|
|
3
|
+
import { createORPCClient, ORPCError } from '@orpc/client';
|
|
4
|
+
import { OpenAPILink } from '@orpc/openapi-client/fetch';
|
|
2
5
|
import { z } from 'zod';
|
|
3
6
|
|
|
4
|
-
// src/api-client-
|
|
7
|
+
// src/api-client-main.ts
|
|
8
|
+
var logger = core.logger.child({ module: "api-client", scope: "auth" });
|
|
9
|
+
var HTTPONLY_COOKIE_SENTINEL = "__httponly_cookie__";
|
|
5
10
|
var LS_ACCESS_KEY = "hai_access_token";
|
|
6
11
|
var LS_REFRESH_KEY = "hai_refresh_token";
|
|
7
12
|
function createLocalStorageTokenStorage() {
|
|
13
|
+
logger.warn("apiClient.tokenStorage.localStorage: storing tokens in localStorage exposes them to XSS. Prefer httpOnly cookies for production.");
|
|
8
14
|
return {
|
|
9
15
|
async getAccessToken() {
|
|
10
16
|
return globalThis.localStorage?.getItem(LS_ACCESS_KEY) ?? null;
|
|
@@ -46,445 +52,40 @@ function createMemoryTokenStorage() {
|
|
|
46
52
|
}
|
|
47
53
|
};
|
|
48
54
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
apiClient_networkError: "\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25",
|
|
69
|
-
apiClient_timeout: "\u8BF7\u6C42\u8D85\u65F6",
|
|
70
|
-
apiClient_serverError: "\u670D\u52A1\u5668\u9519\u8BEF\uFF08{status}\uFF09",
|
|
71
|
-
apiClient_unauthorized: "\u672A\u6388\u6743\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55",
|
|
72
|
-
apiClient_forbidden: "\u65E0\u6743\u9650\u8BBF\u95EE",
|
|
73
|
-
apiClient_notFound: "\u8BF7\u6C42\u7684\u8D44\u6E90\u4E0D\u5B58\u5728",
|
|
74
|
-
apiClient_validationFailed: "\u8BF7\u6C42\u53C2\u6570\u6821\u9A8C\u5931\u8D25",
|
|
75
|
-
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()",
|
|
77
|
-
apiClient_configError: "API \u5BA2\u6237\u7AEF\u914D\u7F6E\u9519\u8BEF\uFF1A{error}",
|
|
78
|
-
apiClient_responseValidationFailed: "\u54CD\u5E94\u6570\u636E\u6821\u9A8C\u5931\u8D25",
|
|
79
|
-
apiClient_unknown: "\u672A\u77E5\u9519\u8BEF"
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
// src/api-client-i18n.ts
|
|
83
|
-
var apiClientM = core.i18n.createMessageGetter({
|
|
84
|
-
"zh-CN": zh_CN_default,
|
|
85
|
-
"en-US": en_US_default
|
|
86
|
-
});
|
|
87
|
-
var ApiClientErrorInfo = {
|
|
88
|
-
NETWORK_ERROR: "001:502",
|
|
89
|
-
TIMEOUT: "002:504",
|
|
90
|
-
SERVER_ERROR: "003:502",
|
|
91
|
-
UNAUTHORIZED: "004:401",
|
|
92
|
-
FORBIDDEN: "005:403",
|
|
93
|
-
NOT_FOUND: "006:404",
|
|
94
|
-
VALIDATION_FAILED: "007:400",
|
|
95
|
-
TOKEN_REFRESH_FAILED: "008:401",
|
|
96
|
-
NOT_INITIALIZED: "010:500",
|
|
97
|
-
CONFIG_ERROR: "011:500",
|
|
98
|
-
UNKNOWN: "099:500"
|
|
99
|
-
};
|
|
100
|
-
var HaiApiClientError = core.error.buildHaiErrorsDef("api-client", ApiClientErrorInfo);
|
|
101
|
-
function defineEndpoint(def) {
|
|
102
|
-
return def;
|
|
103
|
-
}
|
|
104
|
-
|
|
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
|
-
);
|
|
115
|
-
}
|
|
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:
|
|
135
|
-
return err(
|
|
136
|
-
HaiApiClientError.UNKNOWN,
|
|
137
|
-
apiClientM("apiClient_unknown")
|
|
138
|
-
);
|
|
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
|
-
var TRAILING_SLASHES_REGEX = /\/+$/;
|
|
223
|
-
function createFetchClient(config, tokenManager) {
|
|
224
|
-
const fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
225
|
-
const timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
226
|
-
const requestInterceptors = config.interceptors?.request ?? [];
|
|
227
|
-
const responseInterceptors = config.interceptors?.response ?? [];
|
|
228
|
-
function buildUrl(path, params) {
|
|
229
|
-
const base = config.baseUrl.replace(TRAILING_SLASHES_REGEX, "");
|
|
230
|
-
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
231
|
-
const url = new URL(`${base}${normalizedPath}`);
|
|
232
|
-
if (params) {
|
|
233
|
-
for (const [key, value] of Object.entries(params)) {
|
|
234
|
-
if (value !== void 0 && value !== null) {
|
|
235
|
-
url.searchParams.set(key, String(value));
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
return url.toString();
|
|
240
|
-
}
|
|
241
|
-
async function execute(requestConfig, retryOnUnauthorized = true) {
|
|
242
|
-
logger.trace("Executing request", { method: requestConfig.method, url: requestConfig.url });
|
|
243
|
-
if (tokenManager) {
|
|
244
|
-
const accessToken = await tokenManager.storage.getAccessToken();
|
|
245
|
-
if (accessToken) {
|
|
246
|
-
requestConfig.headers.Authorization = `Bearer ${accessToken}`;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
let finalConfig = requestConfig;
|
|
250
|
-
for (const interceptor of requestInterceptors) {
|
|
251
|
-
finalConfig = await interceptor(finalConfig);
|
|
252
|
-
}
|
|
253
|
-
const controller = new AbortController();
|
|
254
|
-
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
255
|
-
finalConfig.signal = controller.signal;
|
|
256
|
-
try {
|
|
257
|
-
let response = await fetchFn(finalConfig.url, {
|
|
258
|
-
method: finalConfig.method,
|
|
259
|
-
headers: finalConfig.headers,
|
|
260
|
-
body: finalConfig.body,
|
|
261
|
-
signal: finalConfig.signal
|
|
262
|
-
});
|
|
263
|
-
clearTimeout(timeoutId);
|
|
264
|
-
for (const interceptor of responseInterceptors) {
|
|
265
|
-
response = await interceptor(response);
|
|
266
|
-
}
|
|
267
|
-
if (response.status === 401 && retryOnUnauthorized && tokenManager) {
|
|
268
|
-
logger.trace("Received 401, attempting token refresh", { url: requestConfig.url });
|
|
269
|
-
const tokens = await tokenManager.refresh();
|
|
270
|
-
if (tokens) {
|
|
271
|
-
logger.trace("Token refreshed, retrying request", { url: requestConfig.url });
|
|
272
|
-
requestConfig.headers.Authorization = `Bearer ${tokens.accessToken}`;
|
|
273
|
-
return execute(requestConfig, false);
|
|
274
|
-
}
|
|
275
|
-
logger.warn("Token refresh failed, returning 401 error", { url: requestConfig.url });
|
|
276
|
-
}
|
|
277
|
-
if (!response.ok) {
|
|
278
|
-
return responseToError(response);
|
|
279
|
-
}
|
|
280
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
281
|
-
if (contentType.includes("application/json")) {
|
|
282
|
-
const body = await response.json();
|
|
283
|
-
if (body && typeof body === "object" && "data" in body) {
|
|
284
|
-
return ok(body.data);
|
|
285
|
-
}
|
|
286
|
-
return ok(body);
|
|
287
|
-
}
|
|
288
|
-
return ok(void 0);
|
|
289
|
-
} catch (cause) {
|
|
290
|
-
clearTimeout(timeoutId);
|
|
291
|
-
logger.warn("Request failed", { url: requestConfig.url, error: cause });
|
|
292
|
-
return networkErrorToResult(cause);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
async function get(path, params) {
|
|
296
|
-
return execute({
|
|
297
|
-
url: buildUrl(path, params),
|
|
298
|
-
method: "GET",
|
|
299
|
-
headers: { Accept: "application/json" }
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
async function post(path, body) {
|
|
303
|
-
return execute({
|
|
304
|
-
url: buildUrl(path),
|
|
305
|
-
method: "POST",
|
|
306
|
-
headers: { "Content-Type": "application/json", "Accept": "application/json" },
|
|
307
|
-
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
async function put(path, body) {
|
|
311
|
-
return execute({
|
|
312
|
-
url: buildUrl(path),
|
|
313
|
-
method: "PUT",
|
|
314
|
-
headers: { "Content-Type": "application/json", "Accept": "application/json" },
|
|
315
|
-
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
async function patch(path, body) {
|
|
319
|
-
return execute({
|
|
320
|
-
url: buildUrl(path),
|
|
321
|
-
method: "PATCH",
|
|
322
|
-
headers: { "Content-Type": "application/json", "Accept": "application/json" },
|
|
323
|
-
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
async function del(path, params) {
|
|
327
|
-
return execute({
|
|
328
|
-
url: buildUrl(path, params),
|
|
329
|
-
method: "DELETE",
|
|
330
|
-
headers: { Accept: "application/json" }
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
async function upload(path, file, options) {
|
|
334
|
-
const formData = new FormData();
|
|
335
|
-
formData.append(options?.fieldName ?? "file", file);
|
|
336
|
-
if (options?.extraFields) {
|
|
337
|
-
for (const [key, value] of Object.entries(options.extraFields)) {
|
|
338
|
-
formData.append(key, value);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
return execute({
|
|
342
|
-
url: buildUrl(path),
|
|
343
|
-
method: "POST",
|
|
344
|
-
headers: { Accept: "application/json" },
|
|
345
|
-
body: formData
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
async function* stream(path, body, options) {
|
|
349
|
-
const requestConfig = {
|
|
350
|
-
url: buildUrl(path),
|
|
351
|
-
method: "POST",
|
|
352
|
-
headers: { "Content-Type": "application/json", "Accept": "text/event-stream" },
|
|
353
|
-
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
354
|
-
};
|
|
355
|
-
if (tokenManager) {
|
|
356
|
-
const accessToken = await tokenManager.storage.getAccessToken();
|
|
357
|
-
if (accessToken) {
|
|
358
|
-
requestConfig.headers.Authorization = `Bearer ${accessToken}`;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
let finalConfig = requestConfig;
|
|
362
|
-
for (const interceptor of requestInterceptors) {
|
|
363
|
-
finalConfig = await interceptor(finalConfig);
|
|
364
|
-
}
|
|
365
|
-
const controller = new AbortController();
|
|
366
|
-
let timeoutId = null;
|
|
367
|
-
let timeoutTriggered = false;
|
|
368
|
-
const clearStreamTimeout = () => {
|
|
369
|
-
if (timeoutId) {
|
|
370
|
-
clearTimeout(timeoutId);
|
|
371
|
-
timeoutId = null;
|
|
372
|
-
}
|
|
373
|
-
};
|
|
374
|
-
const armStreamTimeout = () => {
|
|
375
|
-
clearStreamTimeout();
|
|
376
|
-
timeoutTriggered = false;
|
|
377
|
-
timeoutId = setTimeout(() => {
|
|
378
|
-
timeoutTriggered = true;
|
|
379
|
-
controller.abort();
|
|
380
|
-
}, timeout);
|
|
381
|
-
};
|
|
382
|
-
const externalSignal = options?.signal;
|
|
383
|
-
const onExternalAbort = () => {
|
|
384
|
-
controller.abort();
|
|
385
|
-
};
|
|
386
|
-
if (externalSignal) {
|
|
387
|
-
if (externalSignal.aborted) {
|
|
388
|
-
controller.abort();
|
|
389
|
-
} else {
|
|
390
|
-
externalSignal.addEventListener("abort", onExternalAbort, { once: true });
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
async function doStreamFetch() {
|
|
394
|
-
armStreamTimeout();
|
|
395
|
-
try {
|
|
396
|
-
return await fetchFn(finalConfig.url, {
|
|
397
|
-
method: finalConfig.method,
|
|
398
|
-
headers: finalConfig.headers,
|
|
399
|
-
body: finalConfig.body,
|
|
400
|
-
signal: controller.signal
|
|
401
|
-
});
|
|
402
|
-
} catch (cause) {
|
|
403
|
-
clearStreamTimeout();
|
|
404
|
-
throw cause;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
let reader = null;
|
|
408
|
-
try {
|
|
409
|
-
let response = await doStreamFetch();
|
|
410
|
-
if (response.status === 401 && tokenManager) {
|
|
411
|
-
logger.info("Stream received 401, attempting token refresh", { url: requestConfig.url });
|
|
412
|
-
const tokens = await tokenManager.refresh();
|
|
413
|
-
if (tokens) {
|
|
414
|
-
finalConfig.headers.Authorization = `Bearer ${tokens.accessToken}`;
|
|
415
|
-
response = await doStreamFetch();
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
if (!response.ok) {
|
|
419
|
-
const errorResult = await responseToError(response);
|
|
420
|
-
if (!errorResult.success) {
|
|
421
|
-
throw new Error(errorResult.error.message, { cause: errorResult.error });
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
if (!response.body) {
|
|
425
|
-
throw new Error(apiClientM("apiClient_networkError"));
|
|
426
|
-
}
|
|
427
|
-
reader = response.body.getReader();
|
|
428
|
-
const decoder = new TextDecoder();
|
|
429
|
-
let lineBuffer = "";
|
|
430
|
-
while (true) {
|
|
431
|
-
armStreamTimeout();
|
|
432
|
-
const { done, value } = await reader.read();
|
|
433
|
-
clearStreamTimeout();
|
|
434
|
-
if (done)
|
|
435
|
-
break;
|
|
436
|
-
lineBuffer += decoder.decode(value, { stream: true });
|
|
437
|
-
const parts = lineBuffer.split("\n");
|
|
438
|
-
lineBuffer = parts.pop() ?? "";
|
|
439
|
-
for (const line of parts) {
|
|
440
|
-
if (line.startsWith("data: ")) {
|
|
441
|
-
const data = line.slice(6);
|
|
442
|
-
if (data !== "[DONE]") {
|
|
443
|
-
yield data;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
if (lineBuffer.startsWith("data: ")) {
|
|
449
|
-
const data = lineBuffer.slice(6);
|
|
450
|
-
if (data !== "[DONE]") {
|
|
451
|
-
yield data;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
} catch (cause) {
|
|
455
|
-
if (cause instanceof DOMException && cause.name === "AbortError") {
|
|
456
|
-
if (externalSignal?.aborted && !timeoutTriggered) {
|
|
457
|
-
throw cause;
|
|
458
|
-
}
|
|
459
|
-
throw new Error(apiClientM("apiClient_timeout"), { cause });
|
|
460
|
-
}
|
|
461
|
-
throw cause;
|
|
462
|
-
} finally {
|
|
463
|
-
clearStreamTimeout();
|
|
464
|
-
if (externalSignal) {
|
|
465
|
-
externalSignal.removeEventListener("abort", onExternalAbort);
|
|
466
|
-
}
|
|
467
|
-
if (reader) {
|
|
468
|
-
reader.releaseLock();
|
|
469
|
-
}
|
|
55
|
+
function createHttpOnlyCookieTokenStorage() {
|
|
56
|
+
let memoryAccessToken = null;
|
|
57
|
+
return {
|
|
58
|
+
async getAccessToken() {
|
|
59
|
+
return memoryAccessToken;
|
|
60
|
+
},
|
|
61
|
+
async setAccessToken(token) {
|
|
62
|
+
memoryAccessToken = token;
|
|
63
|
+
},
|
|
64
|
+
// refresh token 由服务端通过 Set-Cookie 设置为 httpOnly cookie;
|
|
65
|
+
// JS 不可见,返回哨兵值让 TokenManager 识别此模式。
|
|
66
|
+
async getRefreshToken() {
|
|
67
|
+
return HTTPONLY_COOKIE_SENTINEL;
|
|
68
|
+
},
|
|
69
|
+
// refresh token 由服务端管理,JS 端无法写入 httpOnly cookie,忽略此调用。
|
|
70
|
+
async setRefreshToken(_token) {
|
|
71
|
+
},
|
|
72
|
+
async clear() {
|
|
73
|
+
memoryAccessToken = null;
|
|
470
74
|
}
|
|
471
|
-
}
|
|
472
|
-
return { get, post, put, patch, delete: del, upload, stream, execute, buildUrl };
|
|
75
|
+
};
|
|
473
76
|
}
|
|
474
|
-
var
|
|
475
|
-
var TokenPairSchema = z.object({
|
|
77
|
+
var RefreshTokenPayloadSchema = z.object({
|
|
476
78
|
accessToken: z.string().min(1),
|
|
477
|
-
refreshToken: z.string().min(1),
|
|
79
|
+
refreshToken: z.string().min(1).optional(),
|
|
478
80
|
expiresIn: z.number().optional(),
|
|
479
81
|
tokenType: z.string().optional()
|
|
480
82
|
});
|
|
481
|
-
function createTokenManager(storage,
|
|
83
|
+
function createTokenManager(storage, refreshEndpointUrl, fetchFn, onRefreshFailed, refreshTimeoutMs = 1e4) {
|
|
482
84
|
const callbacks = [];
|
|
483
85
|
let refreshPromise = null;
|
|
484
86
|
async function refresh() {
|
|
485
|
-
if (refreshPromise)
|
|
87
|
+
if (refreshPromise)
|
|
486
88
|
return refreshPromise;
|
|
487
|
-
}
|
|
488
89
|
refreshPromise = doRefresh();
|
|
489
90
|
try {
|
|
490
91
|
return await refreshPromise;
|
|
@@ -495,48 +96,65 @@ function createTokenManager(storage, refreshUrl, fetchFn, onRefreshFailed) {
|
|
|
495
96
|
async function doRefresh() {
|
|
496
97
|
const refreshToken = await storage.getRefreshToken();
|
|
497
98
|
if (!refreshToken) {
|
|
498
|
-
|
|
99
|
+
logger.warn("Token refresh skipped, no refresh token available");
|
|
499
100
|
onRefreshFailed?.();
|
|
500
101
|
return null;
|
|
501
102
|
}
|
|
502
|
-
|
|
103
|
+
logger.debug("Refreshing token");
|
|
104
|
+
const controller = new AbortController();
|
|
105
|
+
const timeoutId = setTimeout(() => controller.abort(), refreshTimeoutMs);
|
|
503
106
|
try {
|
|
504
|
-
const
|
|
107
|
+
const useHttpOnlyCookie = refreshToken === HTTPONLY_COOKIE_SENTINEL;
|
|
108
|
+
const requestInit = {
|
|
505
109
|
method: "POST",
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
110
|
+
body: useHttpOnlyCookie ? void 0 : JSON.stringify({ refreshToken }),
|
|
111
|
+
credentials: useHttpOnlyCookie ? "include" : "same-origin",
|
|
112
|
+
signal: controller.signal
|
|
113
|
+
};
|
|
114
|
+
if (!useHttpOnlyCookie) {
|
|
115
|
+
requestInit.headers = { "Content-Type": "application/json" };
|
|
116
|
+
}
|
|
117
|
+
const response = await fetchFn(refreshEndpointUrl, requestInit);
|
|
509
118
|
if (!response.ok) {
|
|
510
|
-
|
|
511
|
-
|
|
119
|
+
if (response.status >= 400 && response.status < 500) {
|
|
120
|
+
await storage.clear();
|
|
121
|
+
onRefreshFailed?.();
|
|
122
|
+
} else {
|
|
123
|
+
logger.warn("Token refresh transient failure, keeping tokens", { status: response.status });
|
|
124
|
+
}
|
|
512
125
|
return null;
|
|
513
126
|
}
|
|
514
127
|
const body = await response.json();
|
|
515
|
-
const parsed =
|
|
128
|
+
const parsed = RefreshTokenPayloadSchema.safeParse(readTokenPayload(body.data));
|
|
516
129
|
if (!parsed.success) {
|
|
517
|
-
|
|
130
|
+
logger.warn("Token refresh returned invalid data", { issues: parsed.error.issues });
|
|
131
|
+
await storage.clear();
|
|
132
|
+
onRefreshFailed?.();
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
if (!useHttpOnlyCookie && !parsed.data.refreshToken) {
|
|
136
|
+
logger.warn("Token refresh returned invalid data", { issues: [{ path: ["refreshToken"], message: "Required" }] });
|
|
518
137
|
await storage.clear();
|
|
519
138
|
onRefreshFailed?.();
|
|
520
139
|
return null;
|
|
521
140
|
}
|
|
141
|
+
const refreshTokenForStorage = parsed.data.refreshToken ?? HTTPONLY_COOKIE_SENTINEL;
|
|
522
142
|
const tokens = {
|
|
523
143
|
accessToken: parsed.data.accessToken,
|
|
524
|
-
refreshToken:
|
|
144
|
+
refreshToken: refreshTokenForStorage,
|
|
525
145
|
expiresIn: parsed.data.expiresIn ?? 3600,
|
|
526
146
|
tokenType: "Bearer"
|
|
527
147
|
};
|
|
528
148
|
await storage.setAccessToken(tokens.accessToken);
|
|
529
|
-
await storage.setRefreshToken(
|
|
530
|
-
|
|
531
|
-
for (const cb of callbacks)
|
|
532
|
-
cb(tokens);
|
|
533
|
-
}
|
|
149
|
+
await storage.setRefreshToken(refreshTokenForStorage);
|
|
150
|
+
logger.info("Token refreshed successfully");
|
|
151
|
+
for (const cb of callbacks) cb(tokens);
|
|
534
152
|
return tokens;
|
|
535
153
|
} catch (error) {
|
|
536
|
-
|
|
537
|
-
await storage.clear();
|
|
538
|
-
onRefreshFailed?.();
|
|
154
|
+
logger.error("Token refresh failed (transient)", { error });
|
|
539
155
|
return null;
|
|
156
|
+
} finally {
|
|
157
|
+
clearTimeout(timeoutId);
|
|
540
158
|
}
|
|
541
159
|
}
|
|
542
160
|
return {
|
|
@@ -544,7 +162,8 @@ function createTokenManager(storage, refreshUrl, fetchFn, onRefreshFailed) {
|
|
|
544
162
|
refresh,
|
|
545
163
|
async setTokens(tokens) {
|
|
546
164
|
await storage.setAccessToken(tokens.accessToken);
|
|
547
|
-
|
|
165
|
+
if (tokens.refreshToken)
|
|
166
|
+
await storage.setRefreshToken(tokens.refreshToken);
|
|
548
167
|
},
|
|
549
168
|
async clear() {
|
|
550
169
|
await storage.clear();
|
|
@@ -559,192 +178,283 @@ function createTokenManager(storage, refreshUrl, fetchFn, onRefreshFailed) {
|
|
|
559
178
|
}
|
|
560
179
|
};
|
|
561
180
|
}
|
|
181
|
+
function readTokenPayload(data) {
|
|
182
|
+
if (typeof data === "object" && data !== null && "tokens" in data) {
|
|
183
|
+
return data.tokens;
|
|
184
|
+
}
|
|
185
|
+
return data;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// messages/en-US.json
|
|
189
|
+
var en_US_default = {
|
|
190
|
+
apiClient_networkError: "Network request failed",
|
|
191
|
+
apiClient_timeout: "Request timeout",
|
|
192
|
+
apiClient_serverError: "Server error ({status})",
|
|
193
|
+
apiClient_unauthorized: "Unauthorized, please log in again",
|
|
194
|
+
apiClient_forbidden: "Access forbidden",
|
|
195
|
+
apiClient_notFound: "Resource not found",
|
|
196
|
+
apiClient_validationFailed: "Request validation failed",
|
|
197
|
+
apiClient_tokenRefreshFailed: "Token refresh failed, please log in again",
|
|
198
|
+
apiClient_notInitialized: "API client not initialized, please call api.init() first",
|
|
199
|
+
apiClient_configError: "API client config error: {error}",
|
|
200
|
+
apiClient_responseValidationFailed: "Response validation failed",
|
|
201
|
+
apiClient_unknown: "Unknown error"
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// messages/zh-CN.json
|
|
205
|
+
var zh_CN_default = {
|
|
206
|
+
apiClient_networkError: "\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25",
|
|
207
|
+
apiClient_timeout: "\u8BF7\u6C42\u8D85\u65F6",
|
|
208
|
+
apiClient_serverError: "\u670D\u52A1\u5668\u9519\u8BEF\uFF08{status}\uFF09",
|
|
209
|
+
apiClient_unauthorized: "\u672A\u6388\u6743\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55",
|
|
210
|
+
apiClient_forbidden: "\u65E0\u6743\u9650\u8BBF\u95EE",
|
|
211
|
+
apiClient_notFound: "\u8BF7\u6C42\u7684\u8D44\u6E90\u4E0D\u5B58\u5728",
|
|
212
|
+
apiClient_validationFailed: "\u8BF7\u6C42\u53C2\u6570\u6821\u9A8C\u5931\u8D25",
|
|
213
|
+
apiClient_tokenRefreshFailed: "Token\u5237\u65B0\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55",
|
|
214
|
+
apiClient_notInitialized: "API \u5BA2\u6237\u7AEF\u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 api.init()",
|
|
215
|
+
apiClient_configError: "API \u5BA2\u6237\u7AEF\u914D\u7F6E\u9519\u8BEF\uFF1A{error}",
|
|
216
|
+
apiClient_responseValidationFailed: "\u54CD\u5E94\u6570\u636E\u6821\u9A8C\u5931\u8D25",
|
|
217
|
+
apiClient_unknown: "\u672A\u77E5\u9519\u8BEF"
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// src/api-client-i18n.ts
|
|
221
|
+
var apiClientM = core.i18n.createMessageGetter({
|
|
222
|
+
"zh-CN": zh_CN_default,
|
|
223
|
+
"en-US": en_US_default
|
|
224
|
+
});
|
|
225
|
+
var ApiClientErrorInfo = {
|
|
226
|
+
NETWORK_ERROR: "001:502",
|
|
227
|
+
TIMEOUT: "002:504",
|
|
228
|
+
SERVER_ERROR: "003:502",
|
|
229
|
+
UNAUTHORIZED: "004:401",
|
|
230
|
+
FORBIDDEN: "005:403",
|
|
231
|
+
NOT_FOUND: "006:404",
|
|
232
|
+
VALIDATION_FAILED: "007:400",
|
|
233
|
+
TOKEN_REFRESH_FAILED: "008:401",
|
|
234
|
+
NOT_INITIALIZED: "010:500",
|
|
235
|
+
CONFIG_ERROR: "011:500",
|
|
236
|
+
UNKNOWN: "099:500"
|
|
237
|
+
};
|
|
238
|
+
var HaiApiClientError = core.error.buildHaiErrorsDef("api-client", ApiClientErrorInfo);
|
|
562
239
|
|
|
563
240
|
// src/api-client-main.ts
|
|
564
|
-
var
|
|
565
|
-
var
|
|
566
|
-
var
|
|
567
|
-
var
|
|
568
|
-
var
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
241
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
242
|
+
var DEFAULT_CLIENT_NAME = "hai-api-client";
|
|
243
|
+
var DEFAULT_REFRESH_PATH = IAM_AUTH_ROUTES.refresh;
|
|
244
|
+
var TRAILING_SLASHES_REGEX = /\/+$/;
|
|
245
|
+
var tokenStorage = {
|
|
246
|
+
/** 内存存储——SSR / Node 测试或一次性会话使用。 */
|
|
247
|
+
memory: createMemoryTokenStorage,
|
|
248
|
+
/** 浏览器 `localStorage` 存储——单页应用便捷选项;注意 XSS 风险。 */
|
|
249
|
+
localStorage: createLocalStorageTokenStorage,
|
|
250
|
+
/** httpOnly Cookie 存储——浏览器场景推荐方案。 */
|
|
251
|
+
httpOnlyCookie: createHttpOnlyCookieTokenStorage
|
|
252
|
+
};
|
|
253
|
+
function createApiClient(contract) {
|
|
254
|
+
const state = {
|
|
255
|
+
config: null,
|
|
256
|
+
rawClient: null,
|
|
257
|
+
tokenManager: void 0,
|
|
258
|
+
transport: void 0
|
|
259
|
+
};
|
|
583
260
|
const auth = {
|
|
584
261
|
async setTokens(tokens) {
|
|
585
|
-
|
|
586
|
-
await tokenManager.setTokens(tokens);
|
|
587
|
-
}
|
|
262
|
+
await state.tokenManager?.setTokens(tokens);
|
|
588
263
|
},
|
|
589
264
|
async clear() {
|
|
590
|
-
|
|
591
|
-
await tokenManager.clear();
|
|
592
|
-
}
|
|
265
|
+
await state.tokenManager?.clear();
|
|
593
266
|
},
|
|
594
267
|
onTokenRefreshed(callback) {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
}
|
|
598
|
-
return () => {
|
|
599
|
-
};
|
|
268
|
+
return state.tokenManager?.onTokenRefreshed(callback) ?? (() => {
|
|
269
|
+
});
|
|
600
270
|
}
|
|
601
271
|
};
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
272
|
+
let initPromise = null;
|
|
273
|
+
const lifecycle = {
|
|
274
|
+
async init(config) {
|
|
275
|
+
if (initPromise)
|
|
276
|
+
return initPromise;
|
|
277
|
+
initPromise = (async () => {
|
|
278
|
+
try {
|
|
279
|
+
state.config = config;
|
|
280
|
+
state.transport = buildTransportSession(config);
|
|
281
|
+
state.tokenManager = buildTokenManager(config, state.transport);
|
|
282
|
+
state.rawClient = buildRawClient(contract, config, state.tokenManager, state.transport);
|
|
283
|
+
return ok(void 0);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
return err(
|
|
286
|
+
HaiApiClientError.CONFIG_ERROR,
|
|
287
|
+
apiClientM("apiClient_configError", { params: { error: String(error) } }),
|
|
288
|
+
error
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
})();
|
|
292
|
+
try {
|
|
293
|
+
return await initPromise;
|
|
294
|
+
} finally {
|
|
295
|
+
initPromise = null;
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
async close() {
|
|
299
|
+
state.transport?.destroy();
|
|
300
|
+
state.config = null;
|
|
301
|
+
state.rawClient = null;
|
|
302
|
+
state.tokenManager = void 0;
|
|
303
|
+
state.transport = void 0;
|
|
304
|
+
},
|
|
305
|
+
get config() {
|
|
306
|
+
return state.config;
|
|
307
|
+
},
|
|
308
|
+
get isInitialized() {
|
|
309
|
+
return state.rawClient !== null;
|
|
310
|
+
},
|
|
611
311
|
auth
|
|
612
312
|
};
|
|
313
|
+
return new Proxy({}, {
|
|
314
|
+
get(_target, property) {
|
|
315
|
+
if (typeof property !== "string")
|
|
316
|
+
return void 0;
|
|
317
|
+
if (property in lifecycle) {
|
|
318
|
+
return Reflect.get(lifecycle, property);
|
|
319
|
+
}
|
|
320
|
+
return createProcedureProxy(state, [property]);
|
|
321
|
+
}
|
|
322
|
+
// 这里的返回值在运行时是一个 Proxy,但从“可用能力”上看,它符合 `ApiClient<TContract>`:
|
|
323
|
+
// - 一部分成员来自 lifecycle
|
|
324
|
+
// - 另一部分成员来自 contract 推导出的 procedure 层级
|
|
325
|
+
// 因此使用类型断言把这个动态对象声明为最终对外的 typed client。
|
|
326
|
+
});
|
|
613
327
|
}
|
|
614
|
-
var
|
|
615
|
-
|
|
616
|
-
(
|
|
617
|
-
)
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
},
|
|
623
|
-
async clear() {
|
|
624
|
-
logger3.warn("api.auth.clear called before initialization, ignored");
|
|
625
|
-
},
|
|
626
|
-
onTokenRefreshed() {
|
|
627
|
-
logger3.warn("api.auth.onTokenRefreshed called before initialization, ignored");
|
|
628
|
-
return () => {
|
|
629
|
-
};
|
|
328
|
+
var defaultApiContract = apiContract.create({ iam: apiContract.iam, storage: apiContract.storage, ai: apiContract.ai });
|
|
329
|
+
var apiClient = new Proxy(createApiClient(defaultApiContract), {
|
|
330
|
+
get(target, property, receiver) {
|
|
331
|
+
if (property === "create")
|
|
332
|
+
return createApiClient;
|
|
333
|
+
if (property === "tokenStorage")
|
|
334
|
+
return tokenStorage;
|
|
335
|
+
return Reflect.get(target, property, receiver);
|
|
630
336
|
}
|
|
631
|
-
};
|
|
632
|
-
function
|
|
633
|
-
return {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
337
|
+
});
|
|
338
|
+
function createProcedureProxy(state, path) {
|
|
339
|
+
return new Proxy(() => void 0, {
|
|
340
|
+
get(_t, property) {
|
|
341
|
+
if (typeof property !== "string")
|
|
342
|
+
return void 0;
|
|
343
|
+
return createProcedureProxy(state, [...path, property]);
|
|
344
|
+
},
|
|
345
|
+
apply(_t, _thisArg, args) {
|
|
346
|
+
return callProcedure(state.rawClient, path, args);
|
|
640
347
|
}
|
|
641
|
-
};
|
|
348
|
+
});
|
|
642
349
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
* @returns 成功时返回 ok(undefined);失败时返回包含错误码和消息的 err。
|
|
652
|
-
*/
|
|
653
|
-
async init(config) {
|
|
654
|
-
if (initInProgress) {
|
|
655
|
-
return err(
|
|
656
|
-
HaiApiClientError.CONFIG_ERROR,
|
|
657
|
-
apiClientM("apiClient_configError", { params: { error: "initialization already in progress" } })
|
|
658
|
-
);
|
|
659
|
-
}
|
|
660
|
-
initInProgress = true;
|
|
661
|
-
try {
|
|
662
|
-
if (currentClient) {
|
|
663
|
-
logger3.warn("Api-client module is already initialized, reinitializing");
|
|
664
|
-
await api.close();
|
|
665
|
-
}
|
|
666
|
-
logger3.info("Initializing api-client module");
|
|
667
|
-
currentClient = createClient(config);
|
|
668
|
-
currentConfig = config;
|
|
669
|
-
logger3.info("Api-client module initialized");
|
|
670
|
-
return ok(void 0);
|
|
671
|
-
} catch (error) {
|
|
672
|
-
logger3.error("Api-client module initialization failed", { error });
|
|
673
|
-
return err(
|
|
674
|
-
HaiApiClientError.CONFIG_ERROR,
|
|
675
|
-
apiClientM("apiClient_configError", { params: { error: error instanceof Error ? error.message : String(error) } }),
|
|
676
|
-
error
|
|
677
|
-
);
|
|
678
|
-
} finally {
|
|
679
|
-
initInProgress = false;
|
|
350
|
+
async function callProcedure(rawClient, path, args) {
|
|
351
|
+
if (!rawClient) {
|
|
352
|
+
return err(HaiApiClientError.NOT_INITIALIZED, apiClientM("apiClient_notInitialized"));
|
|
353
|
+
}
|
|
354
|
+
let target = rawClient;
|
|
355
|
+
for (const segment of path) {
|
|
356
|
+
if ((typeof target !== "object" || target === null) && typeof target !== "function") {
|
|
357
|
+
return err(HaiApiClientError.UNKNOWN, apiClientM("apiClient_unknown"));
|
|
680
358
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
return
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
return
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
359
|
+
target = Reflect.get(target, segment);
|
|
360
|
+
}
|
|
361
|
+
if (typeof target !== "function") {
|
|
362
|
+
return err(HaiApiClientError.UNKNOWN, apiClientM("apiClient_unknown"));
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
return await target(...args);
|
|
366
|
+
} catch (error) {
|
|
367
|
+
return mapClientError(error);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function buildTokenManager(config, transport) {
|
|
371
|
+
if (!config.auth)
|
|
372
|
+
return void 0;
|
|
373
|
+
const fetchFn = transport?.encryptedFetch ?? config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
374
|
+
const storage = config.auth.storage ?? createHttpOnlyCookieTokenStorage();
|
|
375
|
+
const baseUrl = config.baseUrl.replace(TRAILING_SLASHES_REGEX, "");
|
|
376
|
+
const refreshPath = config.auth.refreshPath ?? DEFAULT_REFRESH_PATH;
|
|
377
|
+
const refreshUrl = `${baseUrl}${refreshPath.startsWith("/") ? refreshPath : `/${refreshPath}`}`;
|
|
378
|
+
const manager = createTokenManager(storage, refreshUrl, fetchFn, config.auth.onRefreshFailed, config.auth.refreshTimeoutMs);
|
|
379
|
+
if (config.auth.onTokenRefreshed) {
|
|
380
|
+
manager.onTokenRefreshed(config.auth.onTokenRefreshed);
|
|
381
|
+
}
|
|
382
|
+
return manager;
|
|
383
|
+
}
|
|
384
|
+
function buildTransportSession(config) {
|
|
385
|
+
if (!config.transport)
|
|
386
|
+
return void 0;
|
|
387
|
+
const baseFetch = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
388
|
+
const baseUrl = config.baseUrl.replace(TRAILING_SLASHES_REGEX, "");
|
|
389
|
+
const path = config.transport.keyExchangePath ?? "/_hai/key-exchange";
|
|
390
|
+
const keyExchangeUrl = `${baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
|
|
391
|
+
return config.transport.crypto.transport.createClient({ keyExchangeUrl, fetch: baseFetch });
|
|
392
|
+
}
|
|
393
|
+
function buildRawClient(contract, config, tokenManager, transport) {
|
|
394
|
+
const link = new OpenAPILink(contract, {
|
|
395
|
+
url: config.baseUrl,
|
|
396
|
+
headers: async () => buildHeaders(config, tokenManager),
|
|
397
|
+
fetch: async (request, init) => fetchWithRefresh(request, init, config, tokenManager, transport)
|
|
398
|
+
});
|
|
399
|
+
return createORPCClient(link);
|
|
400
|
+
}
|
|
401
|
+
async function buildHeaders(config, tokenManager) {
|
|
402
|
+
const configured = typeof config.headers === "function" ? await config.headers() : config.headers ?? {};
|
|
403
|
+
const accessToken = await tokenManager?.storage.getAccessToken();
|
|
404
|
+
return {
|
|
405
|
+
...configured,
|
|
406
|
+
"x-request-client": config.clientName ?? DEFAULT_CLIENT_NAME,
|
|
407
|
+
...accessToken ? { authorization: `Bearer ${accessToken}` } : {}
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
async function fetchWithRefresh(request, init, config, tokenManager, transport) {
|
|
411
|
+
const retryRequest = tokenManager ? request.clone() : void 0;
|
|
412
|
+
const response = await fetchWithTimeout(request, init, config, transport);
|
|
413
|
+
if (response.status !== 401 || !tokenManager || !retryRequest)
|
|
414
|
+
return response;
|
|
415
|
+
const tokens = await tokenManager.refresh();
|
|
416
|
+
if (!tokens)
|
|
417
|
+
return response;
|
|
418
|
+
const headers = new Headers(retryRequest.headers);
|
|
419
|
+
headers.set("authorization", `Bearer ${tokens.accessToken}`);
|
|
420
|
+
return fetchWithTimeout(new Request(retryRequest, { headers }), init, config, transport);
|
|
421
|
+
}
|
|
422
|
+
async function fetchWithTimeout(request, init, config, transport) {
|
|
423
|
+
const fetchFn = transport?.encryptedFetch ?? config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
424
|
+
const timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
425
|
+
const controller = new AbortController();
|
|
426
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
427
|
+
const userSignal = request.signal;
|
|
428
|
+
if (userSignal) {
|
|
429
|
+
if (userSignal.aborted) {
|
|
430
|
+
controller.abort();
|
|
431
|
+
} else {
|
|
432
|
+
userSignal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
740
433
|
}
|
|
741
|
-
logger3.info("Closing api-client module");
|
|
742
|
-
currentClient = null;
|
|
743
|
-
currentConfig = null;
|
|
744
|
-
logger3.info("Api-client module closed");
|
|
745
434
|
}
|
|
746
|
-
|
|
435
|
+
try {
|
|
436
|
+
return await fetchFn(new Request(request, { signal: controller.signal }), init);
|
|
437
|
+
} finally {
|
|
438
|
+
clearTimeout(timeoutId);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function mapClientError(error) {
|
|
442
|
+
if (error instanceof ORPCError) {
|
|
443
|
+
const detail = { orpcCode: error.code, status: error.status, data: error.data };
|
|
444
|
+
if (error.status === 401)
|
|
445
|
+
return err(HaiApiClientError.UNAUTHORIZED, error.message, detail);
|
|
446
|
+
if (error.status === 403)
|
|
447
|
+
return err(HaiApiClientError.FORBIDDEN, error.message, detail);
|
|
448
|
+
if (error.status === 404)
|
|
449
|
+
return err(HaiApiClientError.NOT_FOUND, error.message, detail);
|
|
450
|
+
return err(HaiApiClientError.SERVER_ERROR, error.message, detail);
|
|
451
|
+
}
|
|
452
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
453
|
+
return err(HaiApiClientError.TIMEOUT, apiClientM("apiClient_timeout"), error);
|
|
454
|
+
}
|
|
455
|
+
return err(HaiApiClientError.NETWORK_ERROR, apiClientM("apiClient_networkError"), error);
|
|
456
|
+
}
|
|
747
457
|
|
|
748
|
-
export { HaiApiClientError,
|
|
458
|
+
export { HaiApiClientError, apiClient };
|
|
749
459
|
//# sourceMappingURL=index.js.map
|
|
750
460
|
//# sourceMappingURL=index.js.map
|