@h-ai/api-client 0.1.0-alpha.73 → 0.1.0-alpha.75
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 +6 -0
- package/README.ja.md +6 -0
- package/README.md +6 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +77 -16
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/README.en.md
CHANGED
|
@@ -182,10 +182,16 @@ See policy, defaults and recovery boundaries in [IAM](../iam/README.en.md).
|
|
|
182
182
|
|
|
183
183
|
Pass the purpose-bound proof returned by `reauthenticate` through the individual call’s `options.context.reauthentication`; the request pipeline emits `X-Hai-Reauthentication`. Never place it in global client headers, persistent storage or logs. The server checks current permissions, resource scope and proof purpose before atomic consumption. Concurrent ordinary calls do not inherit the header. Cross-origin hosts must allow it; Serv/Kit defaults already include it.
|
|
184
184
|
|
|
185
|
+
## Recovering an expired session in place
|
|
186
|
+
|
|
187
|
+
Set `auth.recoverSession` to sign in again without discarding the current page. Use UI `createSessionRecovery` / `SessionRecoveryDialog`: configure the public policy and original user in the root layout, pass `recovery.recover` as the callback, provide standard authentication actions, and cancel on teardown. IAM checks login `expectedUserId` before issuing a session; captcha, lockout, password renewal and access rules remain active. Only an explicitly authentication-rejected request is retried once. Cancellation, 5xx, timeouts and ambiguous writes are not replayed. Sensitive-operation proofs from an old session may need a new confirmation. Without the callback, behavior is unchanged; with it, recovery handles failure instead of immediate onRefreshFailed navigation. auth.terminalType (web/app/desktop/api; default web) selects a session group and does not attest hardware identity.
|
|
188
|
+
|
|
185
189
|
## Page exit session policy
|
|
186
190
|
|
|
187
191
|
`session.endOnPageHide` defaults to `false`. Hosts call `actions.bindPageExit({ enabled: config.session.endOnPageHide })` and invoke its returned cleanup on unmount; standard admin/H5/fullstack templates already bind it. A non-BFCache `pagehide` (including reload and full document navigation) makes a best effort POST to `/auth/session/close`, revoking the held access and related refresh through the existing CSRF/encrypted transport with `keepalive` and no automatic refresh. This response does not clear cookies/local credentials, avoiding a late old response overwriting a new login. Explicit logout still revokes and clears current credentials normally. Browsers may omit page exit requests; configure server `idleTimeout`/`absoluteTimeout` as appropriate. Immediate revocation on browser close is not guaranteed.
|
|
188
192
|
|
|
193
|
+
For a sensitive call, supply `reauthorize: () => sensitiveActions.authorize(operation)` in the third Kit apiFetch argument or typed-client options.context. After session rotation/recovery or an explicit proof rejection, obtain a new purpose-bound proof before one retry. Cancellation sends no further write; without a callback, an old proof is never replayed.
|
|
194
|
+
|
|
189
195
|
## Development and testing
|
|
190
196
|
|
|
191
197
|
```bash
|
package/README.ja.md
CHANGED
|
@@ -182,10 +182,16 @@ createAuthActions(client) は createCaptcha({operation}) を提供し、ログ
|
|
|
182
182
|
|
|
183
183
|
`reauthenticate` が返す用途限定証明を、個別呼び出しの `options.context.reauthentication` に渡すと `X-Hai-Reauthentication` が付きます。グローバルヘッダー、永続保存、ログには入れません。サーバーは現在の権限、対象範囲、用途を確認して原子的に消費し、並行する通常リクエストには引き継ぎません。クロスオリジン環境ではヘッダーを許可してください。Serv/Kit の既定設定には含まれます。
|
|
184
184
|
|
|
185
|
+
## 期限切れセッションの画面内復帰
|
|
186
|
+
|
|
187
|
+
`auth.recoverSession` によって現在の画面と入力を保持して再ログインできます。UI の `createSessionRecovery` / `SessionRecoveryDialog` を使い、ルートで公開方針と元の利用者を設定し、`recovery.recover` と標準認証 actions を渡して破棄時に cancel します。IAM は発行前に `expectedUserId` を確認し、通常の CAPTCHA・ロック・改密・アクセス規則を適用します。認証拒否が明確な要求だけ1回続行でき、取消・5xx・タイムアウト・結果不明の書込は再送しません。旧セッションの重要操作証明は再確認が必要な場合があります。コールバックなしは従来動作で、有効時は即時の onRefreshFailed 遷移を復帰処理が置き換えます。auth.terminalType(web/app/desktop/api、既定 web)は分類用で機器認証ではありません。
|
|
188
|
+
|
|
185
189
|
## ページ離脱時のセッション方針
|
|
186
190
|
|
|
187
191
|
`session.endOnPageHide` の既定値は `false` です。ホストは `actions.bindPageExit({ enabled: config.session.endOnPageHide })` を呼び、破棄時に戻り値の解除関数を実行します。標準 admin/H5/fullstack テンプレートは接続済みです。BFCache 以外の `pagehide`(再読み込み、文書全体の移動を含む)で POST `/auth/session/close` を最善努力で送信し、保持する access と関連 refresh を失効します。既存の CSRF/暗号化転送を使用し、`keepalive` を付け、自動更新しません。古い応答が新しいログインを上書きしないよう Cookie/ローカル資格情報を消去しません。明示的ログアウトは通常どおり失効と消去を行います。ブラウザーが離脱要求を送らない場合もあるため、業務に応じてサーバーの `idleTimeout`/`absoluteTimeout` を併用してください。閉じた瞬間の失効は保証できません。
|
|
188
192
|
|
|
193
|
+
重要操作では `reauthorize: () => sensitiveActions.authorize(operation)` を Kit apiFetch の第3引数、または typed client の options.context に渡します。セッション更新・復帰または明確な証明拒否の後、新しい用途証明で1回だけ続行します。取消時は追加の書込を送らず、コールバックがなければ旧証明を再送しません。
|
|
194
|
+
|
|
189
195
|
## 開発とテスト
|
|
190
196
|
|
|
191
197
|
```bash
|
package/README.md
CHANGED
|
@@ -182,10 +182,16 @@ createAuthActions(client) 提供 createCaptcha({operation}),所有登录映射
|
|
|
182
182
|
|
|
183
183
|
客户端单次调用的 `options.context.reauthentication` 携带 `reauthenticate` 返回的用途凭据,由请求管线生成 `X-Hai-Reauthentication`。不要写入客户端全局 header、持久存储或日志。服务端同时检查当前权限、资源范围和凭据用途,并原子消费;并发普通请求不会继承该 header。跨域宿主须允许此 header,默认 Serv/Kit 已包含。
|
|
184
184
|
|
|
185
|
+
## 原地恢复过期会话
|
|
186
|
+
|
|
187
|
+
配置 `auth.recoverSession` 回调可在服务端明确拒绝会话时原地重新登录并保留页面输入。使用 `@h-ai/ui` 的 `createSessionRecovery` / `SessionRecoveryDialog`:根布局绑定公开配置与原用户,回调设为 `recovery.recover`,弹窗使用标准认证 actions,销毁时 `cancel()`。登录的 `expectedUserId` 由 IAM 限定原主体;常规验证码、锁定、改密和登录限制照常执行。只对明确未认证拒绝的原请求续接一次;取消、5xx、超时和结果不明的网络写入不重放。敏感操作证明绑定旧会话时可能需要再次确认,不复用到新会话。未传回调保持原处理方式;配置回调后由恢复流程处理,不再立即调用 onRefreshFailed 导航离开。终端分组通过 auth.terminalType(web/app/desktop/api,默认 web)传入;它不是硬件身份认证。
|
|
188
|
+
|
|
185
189
|
## 页面离开会话策略
|
|
186
190
|
|
|
187
191
|
`session.endOnPageHide` 默认 `false`。启用后宿主调用 `actions.bindPageExit({ enabled: config.session.endOnPageHide })` 并在卸载时调用返回的清理函数;标准 admin/H5/fullstack 模板已经接入。非 BFCache 的 `pagehide`(也包括刷新、整页导航)通过 POST `/auth/session/close` 尽力撤销当前 access 及关联 refresh,沿用 CSRF/传输加密,使用 `keepalive` 且不自动刷新会话。该响应不清 Cookie/本地凭据,避免旧响应覆盖下一次登录。显式退出仍使用正常 logout,可靠撤销并清理当前凭据。浏览器可能不发送页面离开请求,因此必须按业务配合服务端 `idleTimeout`/`absoluteTimeout`;不能据此保证关闭即撤销。
|
|
188
192
|
|
|
193
|
+
敏感调用通过本次 `reauthorize: () => sensitiveActions.authorize(operation)` 重新确认用途:Kit 放 apiFetch 第三参数,typed client 放 options.context。会话刷新/恢复或明确的再认证拒绝后仅续发一次;取消不写入,未提供回调不重放旧证明。
|
|
194
|
+
|
|
189
195
|
## 开发测试
|
|
190
196
|
|
|
191
197
|
```bash
|
package/dist/index.d.ts
CHANGED
|
@@ -164,6 +164,10 @@ interface TokenStorage {
|
|
|
164
164
|
* ```
|
|
165
165
|
*/
|
|
166
166
|
interface AuthConfig {
|
|
167
|
+
/** refresh明确失效后原地恢复同一用户会话,成功时只重试原请求一次;取消/结果不明不得返回true。 */
|
|
168
|
+
readonly recoverSession?: () => Promise<boolean>;
|
|
169
|
+
/** 登录会话终端分组;默认web,可选app/desktop/api。服务端必须允许该类;不代表物理设备证明。 */
|
|
170
|
+
readonly terminalType?: 'web' | 'app' | 'desktop' | 'api';
|
|
167
171
|
/**
|
|
168
172
|
* Token 存储适配器;默认 `apiClient.tokenStorage.httpOnlyCookie()`(refresh token 由服务端管理,防 XSS)。
|
|
169
173
|
*
|
|
@@ -1134,6 +1138,7 @@ declare const defaultApiContract: _h_ai_api_contract.CreatedApiContract<{
|
|
|
1134
1138
|
username: zod.ZodString;
|
|
1135
1139
|
password: zod.ZodString;
|
|
1136
1140
|
captcha: zod.ZodOptional<zod.ZodString>;
|
|
1141
|
+
expectedUserId: zod.ZodOptional<zod.ZodString>;
|
|
1137
1142
|
}, zod_v4_core.$strip>, zod.ZodDiscriminatedUnion<[zod.ZodObject<{
|
|
1138
1143
|
success: zod.ZodLiteral<true>;
|
|
1139
1144
|
data: zod.ZodObject<{
|
|
@@ -1235,6 +1240,7 @@ declare const defaultApiContract: _h_ai_api_contract.CreatedApiContract<{
|
|
|
1235
1240
|
}, zod_v4_core.$strip>;
|
|
1236
1241
|
}, zod_v4_core.$strip>], "success">, Record<never, never>, Record<never, never>>;
|
|
1237
1242
|
login: _orpc_contract.ContractProcedureBuilderWithInputOutput<zod.ZodObject<{
|
|
1243
|
+
expectedUserId: zod.ZodOptional<zod.ZodString>;
|
|
1238
1244
|
captcha: zod.ZodOptional<zod.ZodString>;
|
|
1239
1245
|
identifier: zod.ZodString;
|
|
1240
1246
|
password: zod.ZodString;
|
|
@@ -1299,6 +1305,7 @@ declare const defaultApiContract: _h_ai_api_contract.CreatedApiContract<{
|
|
|
1299
1305
|
}, zod_v4_core.$strip>;
|
|
1300
1306
|
}, zod_v4_core.$strip>], "success">, Record<never, never>, Record<never, never>>;
|
|
1301
1307
|
loginWithOtp: _orpc_contract.ContractProcedureBuilderWithInputOutput<zod.ZodObject<{
|
|
1308
|
+
expectedUserId: zod.ZodOptional<zod.ZodString>;
|
|
1302
1309
|
captcha: zod.ZodOptional<zod.ZodString>;
|
|
1303
1310
|
identifier: zod.ZodString;
|
|
1304
1311
|
code: zod.ZodString;
|
|
@@ -2088,6 +2095,26 @@ declare const defaultApiContract: _h_ai_api_contract.CreatedApiContract<{
|
|
|
2088
2095
|
}, zod_v4_core.$strip>], "success">, Record<never, never>, Record<never, never>>;
|
|
2089
2096
|
};
|
|
2090
2097
|
permissions: {
|
|
2098
|
+
setSensitiveOperations: _orpc_contract.ContractProcedureBuilderWithInputOutput<zod.ZodObject<{
|
|
2099
|
+
id: zod.ZodString;
|
|
2100
|
+
sensitiveOperations: zod.ZodPipe<zod.ZodOptional<zod.ZodNullable<zod.ZodArray<zod.ZodString>>>, zod.ZodTransform<string[], string[] | null | undefined>>;
|
|
2101
|
+
}, zod_v4_core.$strip>, zod.ZodDiscriminatedUnion<[zod.ZodObject<{
|
|
2102
|
+
success: zod.ZodLiteral<true>;
|
|
2103
|
+
data: zod.ZodVoid;
|
|
2104
|
+
}, zod_v4_core.$strip>, zod.ZodObject<{
|
|
2105
|
+
success: zod.ZodLiteral<false>;
|
|
2106
|
+
error: zod.ZodObject<{
|
|
2107
|
+
code: zod.ZodUnion<readonly [zod.ZodString, zod.ZodNumber]>;
|
|
2108
|
+
message: zod.ZodString;
|
|
2109
|
+
traceId: zod.ZodOptional<zod.ZodString>;
|
|
2110
|
+
httpStatus: zod.ZodOptional<zod.ZodNumber>;
|
|
2111
|
+
system: zod.ZodOptional<zod.ZodString>;
|
|
2112
|
+
module: zod.ZodOptional<zod.ZodString>;
|
|
2113
|
+
cause: zod.ZodOptional<zod.ZodUnknown>;
|
|
2114
|
+
suggestion: zod.ZodOptional<zod.ZodString>;
|
|
2115
|
+
ext: zod.ZodOptional<zod.ZodRecord<zod.ZodString, zod.ZodUnknown>>;
|
|
2116
|
+
}, zod_v4_core.$strip>;
|
|
2117
|
+
}, zod_v4_core.$strip>], "success">, Record<never, never>, Record<never, never>>;
|
|
2091
2118
|
list: _orpc_contract.ContractProcedureBuilderWithInputOutput<zod.ZodObject<{
|
|
2092
2119
|
page: zod.ZodOptional<zod.ZodCoercedNumber<unknown>>;
|
|
2093
2120
|
pageSize: zod.ZodOptional<zod.ZodCoercedNumber<unknown>>;
|
|
@@ -2101,6 +2128,7 @@ declare const defaultApiContract: _h_ai_api_contract.CreatedApiContract<{
|
|
|
2101
2128
|
success: zod.ZodLiteral<true>;
|
|
2102
2129
|
data: zod.ZodObject<{
|
|
2103
2130
|
items: zod.ZodArray<zod.ZodObject<{
|
|
2131
|
+
sensitiveOperations: zod.ZodPipe<zod.ZodOptional<zod.ZodNullable<zod.ZodArray<zod.ZodString>>>, zod.ZodTransform<string[], string[] | null | undefined>>;
|
|
2104
2132
|
id: zod.ZodString;
|
|
2105
2133
|
code: zod.ZodString;
|
|
2106
2134
|
name: zod.ZodString;
|
|
@@ -2138,6 +2166,7 @@ declare const defaultApiContract: _h_ai_api_contract.CreatedApiContract<{
|
|
|
2138
2166
|
}, zod_v4_core.$strip>, zod.ZodDiscriminatedUnion<[zod.ZodObject<{
|
|
2139
2167
|
success: zod.ZodLiteral<true>;
|
|
2140
2168
|
data: zod.ZodNullable<zod.ZodObject<{
|
|
2169
|
+
sensitiveOperations: zod.ZodPipe<zod.ZodOptional<zod.ZodNullable<zod.ZodArray<zod.ZodString>>>, zod.ZodTransform<string[], string[] | null | undefined>>;
|
|
2141
2170
|
id: zod.ZodString;
|
|
2142
2171
|
code: zod.ZodString;
|
|
2143
2172
|
name: zod.ZodString;
|
|
@@ -2167,6 +2196,7 @@ declare const defaultApiContract: _h_ai_api_contract.CreatedApiContract<{
|
|
|
2167
2196
|
}, zod_v4_core.$strip>;
|
|
2168
2197
|
}, zod_v4_core.$strip>], "success">, Record<never, never>, Record<never, never>>;
|
|
2169
2198
|
create: _orpc_contract.ContractProcedureBuilderWithInputOutput<zod.ZodObject<{
|
|
2199
|
+
sensitiveOperations: zod.ZodDefault<zod.ZodArray<zod.ZodString>>;
|
|
2170
2200
|
code: zod.ZodString;
|
|
2171
2201
|
name: zod.ZodString;
|
|
2172
2202
|
description: zod.ZodOptional<zod.ZodString>;
|
|
@@ -2180,6 +2210,7 @@ declare const defaultApiContract: _h_ai_api_contract.CreatedApiContract<{
|
|
|
2180
2210
|
}, zod_v4_core.$strip>, zod.ZodDiscriminatedUnion<[zod.ZodObject<{
|
|
2181
2211
|
success: zod.ZodLiteral<true>;
|
|
2182
2212
|
data: zod.ZodObject<{
|
|
2213
|
+
sensitiveOperations: zod.ZodPipe<zod.ZodOptional<zod.ZodNullable<zod.ZodArray<zod.ZodString>>>, zod.ZodTransform<string[], string[] | null | undefined>>;
|
|
2183
2214
|
id: zod.ZodString;
|
|
2184
2215
|
code: zod.ZodString;
|
|
2185
2216
|
name: zod.ZodString;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { apiContract } from '@h-ai/api-contract';
|
|
1
|
+
import { apiContract, IamSessionErrorCodeSchema } from '@h-ai/api-contract';
|
|
2
2
|
import { core, err, ok } from '@h-ai/core';
|
|
3
3
|
import { TransportTimeError, TransportBodyTooLargeError } from '@h-ai/crypto';
|
|
4
4
|
import { createORPCClient, ORPCError } from '@orpc/client';
|
|
@@ -63,6 +63,7 @@ function createTokenManager(storage, refreshEndpointUrl, fetchFn, onRefreshFaile
|
|
|
63
63
|
let refreshPromise = null;
|
|
64
64
|
let refreshController = null;
|
|
65
65
|
let generation = 0;
|
|
66
|
+
let sessionInvalid = false;
|
|
66
67
|
let storageMutation = Promise.resolve();
|
|
67
68
|
function mutateStorage(operation) {
|
|
68
69
|
const pending = storageMutation.then(operation);
|
|
@@ -83,8 +84,10 @@ function createTokenManager(storage, refreshEndpointUrl, fetchFn, onRefreshFaile
|
|
|
83
84
|
if (refreshGeneration !== generation)
|
|
84
85
|
return;
|
|
85
86
|
await storage.clear();
|
|
86
|
-
if (refreshGeneration === generation)
|
|
87
|
+
if (refreshGeneration === generation) {
|
|
88
|
+
sessionInvalid = true;
|
|
87
89
|
onRefreshFailed?.();
|
|
90
|
+
}
|
|
88
91
|
});
|
|
89
92
|
}
|
|
90
93
|
async function refresh() {
|
|
@@ -111,7 +114,8 @@ function createTokenManager(storage, refreshEndpointUrl, fetchFn, onRefreshFaile
|
|
|
111
114
|
if (refreshGeneration !== generation)
|
|
112
115
|
return null;
|
|
113
116
|
if (!refreshToken) {
|
|
114
|
-
|
|
117
|
+
sessionInvalid = true;
|
|
118
|
+
logger.debug("Token refresh skipped, no refresh token available");
|
|
115
119
|
onRefreshFailed?.();
|
|
116
120
|
return null;
|
|
117
121
|
}
|
|
@@ -171,7 +175,8 @@ function createTokenManager(storage, refreshEndpointUrl, fetchFn, onRefreshFaile
|
|
|
171
175
|
});
|
|
172
176
|
if (!committed || refreshGeneration !== generation)
|
|
173
177
|
return null;
|
|
174
|
-
|
|
178
|
+
sessionInvalid = false;
|
|
179
|
+
logger.debug("Token refreshed successfully");
|
|
175
180
|
for (const cb of callbacks) {
|
|
176
181
|
if (refreshGeneration !== generation)
|
|
177
182
|
return null;
|
|
@@ -189,9 +194,16 @@ function createTokenManager(storage, refreshEndpointUrl, fetchFn, onRefreshFaile
|
|
|
189
194
|
}
|
|
190
195
|
return {
|
|
191
196
|
storage,
|
|
197
|
+
get generation() {
|
|
198
|
+
return generation;
|
|
199
|
+
},
|
|
200
|
+
get sessionInvalid() {
|
|
201
|
+
return sessionInvalid;
|
|
202
|
+
},
|
|
192
203
|
refresh,
|
|
193
204
|
async setTokens(tokens) {
|
|
194
205
|
const previousController = invalidateRefresh();
|
|
206
|
+
sessionInvalid = false;
|
|
195
207
|
const mutationGeneration = generation;
|
|
196
208
|
const pending = mutateStorage(async () => {
|
|
197
209
|
if (mutationGeneration !== generation)
|
|
@@ -329,7 +341,7 @@ function createIamAuthActions(client) {
|
|
|
329
341
|
publicConfig: () => client.iam.auth.publicConfig(),
|
|
330
342
|
createCaptcha: (input) => client.iam.auth.createCaptcha(input),
|
|
331
343
|
login: async (input) => {
|
|
332
|
-
const result = input.method === "ldap" ? await client.iam.auth.loginWithLdap({ username: input.identifier, password: input.password, captcha: input.captcha }) : input.method === "email" || input.method === "sms" ? await client.iam.auth.loginWithOtp({ identifier: input.identifier, code: input.code ?? "", captcha: input.captcha }) : await client.iam.auth.login({ identifier: input.identifier, password: input.password, captcha: input.captcha });
|
|
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 });
|
|
333
345
|
return save(result);
|
|
334
346
|
},
|
|
335
347
|
register: async (input) => save(await client.iam.auth.register(input)),
|
|
@@ -576,7 +588,7 @@ function buildTokenManager(config, transport) {
|
|
|
576
588
|
new Headers(init?.headers).forEach((value, name) => headers.set(name, value));
|
|
577
589
|
return fetchFn(input, { ...init, headers });
|
|
578
590
|
};
|
|
579
|
-
const manager = createTokenManager(storage, refreshUrl, refreshFetch, config.auth.onRefreshFailed, config.auth.refreshTimeoutMs);
|
|
591
|
+
const manager = createTokenManager(storage, refreshUrl, refreshFetch, config.auth.recoverSession ? void 0 : config.auth.onRefreshFailed, config.auth.refreshTimeoutMs);
|
|
580
592
|
if (config.auth.onTokenRefreshed) {
|
|
581
593
|
manager.onTokenRefreshed(config.auth.onTokenRefreshed);
|
|
582
594
|
}
|
|
@@ -610,7 +622,12 @@ function buildRawClient(contract, config, tokenManager, transport) {
|
|
|
610
622
|
record.url = core.sanitize.sanitizeUrl(url.toString());
|
|
611
623
|
}
|
|
612
624
|
const requestInit = options.context.keepalive === true ? { ...init, keepalive: true } : init;
|
|
613
|
-
const
|
|
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);
|
|
614
631
|
if (record) {
|
|
615
632
|
record.status = response.status;
|
|
616
633
|
const traceId = response.headers.get("x-trace-id") ?? response.headers.get("x-request-id");
|
|
@@ -629,6 +646,7 @@ function getObservation(context) {
|
|
|
629
646
|
async function buildHeaders(config, tokenManager, requestId) {
|
|
630
647
|
const configured = typeof config.headers === "function" ? await config.headers() : config.headers ?? {};
|
|
631
648
|
const headers = new Headers(configured);
|
|
649
|
+
headers.set("x-hai-terminal", config.auth?.terminalType ?? "web");
|
|
632
650
|
if (!headers.has("accept-language"))
|
|
633
651
|
headers.set("accept-language", core.i18n.getGlobalLocale());
|
|
634
652
|
const accessToken = await tokenManager?.storage.getAccessToken();
|
|
@@ -639,28 +657,71 @@ async function buildHeaders(config, tokenManager, requestId) {
|
|
|
639
657
|
...accessToken ? { authorization: `Bearer ${accessToken}` } : {}
|
|
640
658
|
};
|
|
641
659
|
}
|
|
642
|
-
async function fetchWithRefresh(request, init, config, tokenManager, transport, observation) {
|
|
643
|
-
const retryRequest = tokenManager ? request.clone() : void 0;
|
|
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;
|
|
644
663
|
if (observation)
|
|
645
664
|
observation.attempts++;
|
|
646
665
|
const response = await fetchWithTimeout(request, init, config, transport);
|
|
647
|
-
if (!
|
|
666
|
+
if (!retryRequest)
|
|
648
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);
|
|
691
|
+
}
|
|
649
692
|
const baseUrl = config.baseUrl.replace(TRAILING_SLASHES_REGEX, "");
|
|
650
693
|
const refreshPath = config.auth?.refreshPath ?? DEFAULT_REFRESH_PATH;
|
|
651
694
|
const excludedPaths = [...AUTH_ENTRY_PATHS, refreshPath];
|
|
652
695
|
if (excludedPaths.some((path) => new URL(`${baseUrl}${path.startsWith("/") ? path : `/${path}`}`).pathname === new URL(request.url).pathname))
|
|
653
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;
|
|
654
703
|
if (response.status !== 401 && !await isUnauthorizedResult(response))
|
|
655
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;
|
|
656
710
|
const tokens = await tokenManager.refresh();
|
|
657
|
-
if (
|
|
711
|
+
if (tokenManager.generation !== identityGeneration)
|
|
658
712
|
return response;
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
if (!accessToken)
|
|
723
|
+
return response;
|
|
724
|
+
return retryAuthorized(accessToken);
|
|
664
725
|
}
|
|
665
726
|
async function isUnauthorizedResult(response) {
|
|
666
727
|
if (response.status !== 200 || !response.headers.get("content-type")?.includes("application/json"))
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/api-client-auth.ts","../messages/en-US.json","../messages/ja-JP.json","../messages/zh-CN.json","../src/api-client-i18n.ts","../src/api-client-iam.ts","../src/api-client-types.ts","../src/api-client-main.ts"],"names":["core"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @h-ai/api-client — Token 存储与刷新管理\n *\n * 提供两个内置 Token 存储适配器(httpOnly Cookie / 内存)和一个 Token 管理器\n * (封装存储读写、自动刷新、并发去重、回调通知)。\n *\n * @example 浏览器 SPA\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n *\n * const initialized = await apiClient.init({\n * baseUrl: 'https://api.example.com/api/v1',\n * auth: { storage: apiClient.tokenStorage.httpOnlyCookie() },\n * })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * await apiClient.close()\n * ```\n *\n * @example Node.js 测试\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n *\n * const storage = apiClient.tokenStorage.memory()\n * await storage.setAccessToken('test-access')\n * await storage.clear()\n * ```\n *\n * @module api-client-auth\n */\n\nimport type { TokenPair, TokenStorage } from './api-client-types.js'\nimport { core } from '@h-ai/core'\nimport { z } from 'zod'\n\nconst logger = core.logger.child({ module: 'api-client', scope: 'auth' })\n\n/**\n * httpOnly cookie 模式哨兵值。\n * `apiClient.tokenStorage.httpOnlyCookie()` 的 `getRefreshToken()` 返回此值,\n * 告知 `doRefresh` 应依赖浏览器自动发送的 httpOnly Cookie,\n * 而不是将 refreshToken 写入请求体。\n */\nconst HTTPONLY_COOKIE_SENTINEL = '__httponly_cookie__'\n\n// ─── 内存存储 ─────────────────────────────────────────────────────────────────\n\n/**\n * 创建仅属于本适配器实例的内存凭据存储,页面刷新或进程结束后丢失。\n * SSR 应为每个请求创建客户端和存储实例,避免不同用户共享默认单例。\n * 通过 client.auth 更新已绑定的存储,才能撤销旧代际刷新;直接存储写入不提供这种保证。\n * @returns 两个初始为空的 Token 槽位及清理操作。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const storage = apiClient.tokenStorage.memory()\n * await storage.setAccessToken('test-access')\n * const saved = await storage.getAccessToken()\n * await storage.clear()\n * // saved 为本实例的测试值;不把 Token 写入 Web Storage。\n * ```\n */\nexport function createMemoryTokenStorage(): TokenStorage {\n let accessToken: string | null = null\n let refreshToken: string | null = null\n return {\n async getAccessToken() { return accessToken },\n async setAccessToken(token) { accessToken = token },\n async getRefreshToken() { return refreshToken },\n async setRefreshToken(token) { refreshToken = token },\n async clear() {\n accessToken = null\n refreshToken = null\n },\n }\n}\n\n// ─── httpOnly Cookie 存储 ────────────────────────────────────────────────────\n\n/**\n * 创建浏览器使用的内存 access token 与服务端 httpOnly refresh Cookie 适配器。\n * refresh getter 返回内部哨兵,setter 不修改 Cookie;clear 只清除内存 access token。\n * 刷新请求使用 credentials: include;服务端须实际设置/撤销 Cookie,配置适当的 Secure、SameSite、Path 与跨域凭据策略。\n * HttpOnly 限制脚本直接读取 Cookie,不能阻止已执行的恶意脚本代用户发请求;服务端仍须授权和防护 CSRF。\n * @returns 独立内存 access token 与 Cookie 模式标记;不接触 localStorage。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const initialized = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1', auth: { storage: apiClient.tokenStorage.httpOnlyCookie() } })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try {\n * const me = await apiClient.iam.auth.currentUser()\n * if (!me.success) throw new Error(me.error.message)\n * const userId = me.data.id\n * }\n * finally { await apiClient.close() }\n * ```\n */\nexport function createHttpOnlyCookieTokenStorage(): TokenStorage {\n // access token 保存在内存中,用于注入 Authorization 请求头\n let memoryAccessToken: string | null = null\n return {\n async getAccessToken() { return memoryAccessToken },\n async setAccessToken(token) { memoryAccessToken = token },\n // refresh token 由服务端通过 Set-Cookie 设置为 httpOnly cookie;\n // JS 不可见,返回哨兵值让 TokenManager 识别此模式。\n async getRefreshToken() { return HTTPONLY_COOKIE_SENTINEL },\n // refresh token 由服务端管理,JS 端无法写入 httpOnly cookie,忽略此调用。\n async setRefreshToken(_token) { /* 由服务端 Set-Cookie 响应头管理,客户端无需操作 */ },\n async clear() {\n memoryAccessToken = null\n // httpOnly cookie 的清除必须由服务端 logout 端点完成(Set-Cookie: Max-Age=0);\n // 客户端无法通过 JS 直接删除 httpOnly cookie。\n },\n }\n}\n\n// ─── Token 管理器 ─────────────────────────────────────────────────────────────\n\n/** 校验 refresh 响应体中的 Token 字段;httpOnly cookie 模式允许不返回 refreshToken。 */\nconst RefreshTokenPayloadSchema = z.object({\n accessToken: z.string().min(1),\n refreshToken: z.string().min(1).optional(),\n expiresIn: z.number().optional(),\n tokenType: z.string().optional(),\n})\n\n/**\n * 当前代际刷新成功后的同步通知;参数包含敏感凭据,不应记录 Token。\n * 回调在持久写入成功后调用,应自行处理业务异常,不能把抛错当成刷新重试机制。\n *\n * @example 模块内部上下文\n * ```ts\n * const observed: number[] = []\n * const callback: RefreshCallback = tokens => { observed.push(tokens.expiresIn) }\n * callback({ accessToken: 'test-access', expiresIn: 3600, tokenType: 'Bearer' })\n * ```\n */\ntype RefreshCallback = (tokens: TokenPair) => void\n\n/**\n * 创建模块内部 Token 管理器;包用户通过 apiClient.auth 使用,包入口不导出此工厂。\n * 同一代际的并发刷新共享请求;clear/setTokens 同步撤销旧代际,所有存储变更串行提交。\n * 迟到响应不能覆盖新账号、恢复退出凭据或触发旧回调;已开始的存储写入须完成后才能执行后续身份变更。\n * 4xx、无效响应和非 Cookie 模式缺少 refreshToken 会尝试清理凭据;5xx、网络/超时保留凭据并返回 null。\n * 无刷新凭据时通知失败并返回 null;存储读取失败可拒绝 Promise,setTokens/clear 的存储失败向调用方传播。\n * @param storage - 独占的安全存储适配器。\n * @param refreshEndpointUrl - baseUrl 与 refreshPath 组合后的完整 URL。\n * @param fetchFn - 实际 fetch;Cookie 模式发送凭据,普通模式发送 JSON refreshToken。\n * @param onRefreshFailed - 当前代际明确失效后的同步通知。\n * @param refreshTimeoutMs - fetch 中断超时毫秒数;依赖 fetch 实现响应 signal。\n * @returns 内部管理器;不要绕过它直接更新已绑定的 storage。\n *\n * @example 模块内部上下文\n * ```ts\n * const storage = createMemoryTokenStorage()\n * const fetchTokens: typeof fetch = async () => Response.json({ data: { accessToken: 'test-access-next', refreshToken: 'test-refresh-next', expiresIn: 3600 } })\n * const manager = createTokenManager(storage, 'https://api.example.com/api/v1/auth/refresh', fetchTokens)\n * await manager.setTokens({ accessToken: 'test-access', refreshToken: 'test-refresh', expiresIn: 3600, tokenType: 'Bearer' })\n * const off = manager.onTokenRefreshed(tokens => { const lifetime = tokens.expiresIn })\n * try { const refreshed = await manager.refresh() }\n * finally { off(); await manager.clear() }\n * ```\n */\nexport function createTokenManager(\n storage: TokenStorage,\n refreshEndpointUrl: string,\n fetchFn: typeof globalThis.fetch,\n onRefreshFailed?: () => void,\n refreshTimeoutMs: number = 10_000,\n) {\n const callbacks: RefreshCallback[] = []\n let refreshPromise: Promise<TokenPair | null> | null = null\n let refreshController: AbortController | null = null\n let generation = 0\n let storageMutation: Promise<void> = Promise.resolve()\n\n /**\n * 按调用顺序提交异步存储写入;单次失败只拒绝该调用,后续清理/登录仍可运行。\n * @param operation - 本次需要排序的写入操作。\n * @returns 本次操作结果,不吞掉当前写入故障。\n *\n * @example 模块内部上下文\n * ```ts\n * const order: string[] = []\n * await mutateStorage(async () => { order.push('login') })\n * await mutateStorage(async () => { order.push('logout') })\n * ```\n */\n function mutateStorage<T>(operation: () => Promise<T>): Promise<T> {\n const pending = storageMutation.then(operation)\n storageMutation = pending.then(() => {}, () => {})\n return pending\n }\n\n /**\n * 同步推进身份代际并移除旧刷新去重状态;返回旧控制器供调用方排好新写入后中断。\n * @returns 旧刷新控制器或 null;即使底层忽略 abort,代际检查仍拒绝旧结果提交。\n *\n * @example 模块内部上下文\n * ```ts\n * const old = invalidateRefresh()\n * // 生产调用先排入新身份存储变更,再触发可能重入的 abort 监听器。\n * old?.abort()\n * ```\n */\n function invalidateRefresh(): AbortController | null {\n const previousController = refreshController\n generation++\n refreshController = null\n refreshPromise = null\n return previousController\n }\n\n /**\n * 仅当前刷新代际可尝试清理存储并通知失效;旧账号迟到 401 不影响新身份。\n * @param refreshGeneration - 发起刷新时记录的身份代际。\n * @returns 清理完成;存储失败拒绝,由 doRefresh 的异常分支处理。\n *\n * @example 模块内部上下文\n * ```ts\n * await failRefresh(generation)\n * // 传入旧 generation 会在串行写入队列中再次校验并跳过。\n * ```\n */\n async function failRefresh(refreshGeneration: number): Promise<void> {\n await mutateStorage(async () => {\n if (refreshGeneration !== generation)\n return\n await storage.clear()\n if (refreshGeneration === generation)\n onRefreshFailed?.()\n })\n }\n\n /**\n * 同一代际只发起一个刷新请求;旧请求 finally 不能清除新身份的在途状态。\n * @returns 成功且仍有效的 Token,或 null;读取适配器失败可能拒绝。\n *\n * @example 模块内部上下文\n * ```ts\n * const [first, second] = await Promise.all([refresh(), refresh()])\n * // 两个调用共享在途 fetch;结果仍以各自读取到的当前代际为准。\n * ```\n */\n async function refresh(): Promise<TokenPair | null> {\n // 并发去重:同一时刻只发一次 refresh,其它调用复用同一个 Promise\n if (refreshPromise)\n return refreshPromise\n const controller = new AbortController()\n const pending = doRefresh(generation, controller)\n refreshController = controller\n refreshPromise = pending\n try {\n return await pending\n }\n finally {\n // 旧请求结束不能移除换账号后新一轮刷新的去重状态。\n if (refreshPromise === pending) {\n refreshPromise = null\n refreshController = null\n }\n }\n }\n\n /**\n * 等待身份写入完成后读取刷新凭据,执行超时请求并在每个异步边界校验代际。\n * 4xx/无效数据尝试清理;5xx/网络故障保留凭据。只有当前代际成功写入后才通知订阅者。\n * @param refreshGeneration - 请求开始时的代际。\n * @param controller - 本次请求中断控制器;底层 fetch 应响应 signal。\n * @returns 当前刷新 Token 或 null;清理/回调等请求段异常记录为失败,不声称持久清理成功。\n *\n * @example 模块内部上下文\n * ```ts\n * const controller = new AbortController()\n * const tokens = await doRefresh(generation, controller)\n * if (tokens) { const lifetime = tokens.expiresIn }\n * ```\n */\n async function doRefresh(refreshGeneration: number, controller: AbortController): Promise<TokenPair | null> {\n // 登录/退出可能正在异步落盘,刷新只能读取其完成后的凭据。\n await storageMutation\n if (refreshGeneration !== generation)\n return null\n const refreshToken = await storage.getRefreshToken()\n if (refreshGeneration !== generation)\n return null\n if (!refreshToken) {\n logger.warn('Token refresh skipped, no refresh token available')\n onRefreshFailed?.()\n return null\n }\n\n logger.debug('Refreshing token')\n // 超时保护:避免 refresh 接口挂死时 dedup Promise 永不 resolve,连带所有等待者 hang。\n const timeoutId = setTimeout(() => controller.abort(), refreshTimeoutMs)\n try {\n // httpOnly cookie 模式:浏览器自动发送 Cookie,请求体无需携带 refreshToken;\n // 同时设置 credentials:'include' 以支持跨域 Cookie 发送。\n const useHttpOnlyCookie = refreshToken === HTTPONLY_COOKIE_SENTINEL\n const requestInit: RequestInit = {\n method: 'POST',\n body: useHttpOnlyCookie ? undefined : JSON.stringify({ refreshToken }),\n credentials: useHttpOnlyCookie ? 'include' : 'same-origin',\n signal: controller.signal,\n }\n if (!useHttpOnlyCookie) {\n requestInit.headers = { 'Content-Type': 'application/json' }\n }\n const response = await fetchFn(refreshEndpointUrl, requestInit)\n if (refreshGeneration !== generation)\n return null\n\n if (!response.ok) {\n // 仅在服务端明确返回 401/4xx 时才清除 Token(refresh token 已失效);\n // 5xx 作为服务端临时故障,保留 Token 让下一次请求重试。\n if (response.status >= 400 && response.status < 500) {\n await failRefresh(refreshGeneration)\n }\n else {\n logger.warn('Token refresh transient failure, keeping tokens', { status: response.status })\n }\n return null\n }\n\n const body = await response.json() as { data?: unknown }\n if (refreshGeneration !== generation)\n return null\n const parsed = RefreshTokenPayloadSchema.safeParse(readTokenPayload(body.data))\n if (!parsed.success) {\n logger.warn('Token refresh returned invalid data', { issues: parsed.error.issues })\n await failRefresh(refreshGeneration)\n return null\n }\n if (!useHttpOnlyCookie && !parsed.data.refreshToken) {\n logger.warn('Token refresh returned invalid data', { issues: [{ path: ['refreshToken'], message: 'Required' }] })\n await failRefresh(refreshGeneration)\n return null\n }\n\n // 默认值兜底,避免不安全强转\n const refreshTokenForStorage = parsed.data.refreshToken ?? HTTPONLY_COOKIE_SENTINEL\n const tokens: TokenPair = {\n accessToken: parsed.data.accessToken,\n refreshToken: refreshTokenForStorage,\n expiresIn: parsed.data.expiresIn ?? 3600,\n tokenType: 'Bearer',\n }\n const committed = await mutateStorage(async () => {\n if (refreshGeneration !== generation)\n return false\n await storage.setAccessToken(tokens.accessToken)\n if (refreshGeneration !== generation)\n return false\n await storage.setRefreshToken(refreshTokenForStorage)\n return refreshGeneration === generation\n })\n if (!committed || refreshGeneration !== generation)\n return null\n logger.info('Token refreshed successfully')\n for (const cb of callbacks) {\n if (refreshGeneration !== generation)\n return null\n cb(tokens)\n }\n return refreshGeneration === generation ? tokens : null\n }\n catch (error) {\n if (refreshGeneration !== generation)\n return null\n // 网络或 fetch 本身抛异常(超时、断网、CORS)视为可重试的暂时性错误,\n // 不清除 Token,仅记录日志。调用方可在下次请求时重试。\n logger.error('Token refresh failed (transient)', { error })\n return null\n }\n finally {\n clearTimeout(timeoutId)\n }\n }\n\n return {\n storage,\n refresh,\n async setTokens(tokens: TokenPair) {\n const previousController = invalidateRefresh()\n const mutationGeneration = generation\n const pending = mutateStorage(async () => {\n if (mutationGeneration !== generation)\n return\n // 替换完整凭据:新账号未提供 refresh token 时不能继续使用旧账号的值。\n await storage.clear()\n if (mutationGeneration !== generation)\n return\n await storage.setAccessToken(tokens.accessToken)\n if (tokens.refreshToken && mutationGeneration === generation)\n await storage.setRefreshToken(tokens.refreshToken)\n })\n // 先排好新身份写入,再触发 abort 事件;可重入的监听器只能读取新操作后的存储。\n previousController?.abort()\n await pending\n },\n async clear() {\n const previousController = invalidateRefresh()\n const pending = mutateStorage(() => storage.clear())\n previousController?.abort()\n await pending\n },\n onTokenRefreshed(callback: RefreshCallback): () => void {\n callbacks.push(callback)\n return () => {\n const idx = callbacks.indexOf(callback)\n if (idx >= 0)\n callbacks.splice(idx, 1)\n }\n },\n }\n}\n\n/**\n * Token 管理器内部契约,从真实工厂返回值推导;不属于包公开类型。\n *\n * @example 模块内部上下文\n * ```ts\n * const manager: TokenManager = createTokenManager(createMemoryTokenStorage(), 'https://api.example.com/api/v1/auth/refresh', fetch)\n * await manager.clear()\n * // clear 先废弃旧代际,再等待已有写入及本次清理完成。\n * ```\n */\nexport type TokenManager = ReturnType<typeof createTokenManager>\n\n/**\n * 从直接 tokens 或带 tokens 字段的 data 提取待校验内容;本函数不证明字段合法。\n * @param data - 已解析 JSON 包络的 data。\n * @returns 交给 RefreshTokenPayloadSchema 校验的未知值。\n *\n * @example 模块内部上下文\n * ```ts\n * const candidate = readTokenPayload({ tokens: { accessToken: 'test-access', expiresIn: 3600 } })\n * const parsed = RefreshTokenPayloadSchema.safeParse(candidate)\n * if (parsed.success) { const accessToken = parsed.data.accessToken }\n * ```\n */\nfunction readTokenPayload(data: unknown): unknown {\n if (typeof data === 'object' && data !== null && 'tokens' in data) {\n return (data as { tokens: unknown }).tokens\n }\n return data\n}\n","{\n \"apiClient_networkError\": \"Network request failed\",\n \"apiClient_timeout\": \"Request timeout\",\n \"apiClient_serverError\": \"Server error ({status})\",\n \"apiClient_unauthorized\": \"Unauthorized, please log in again\",\n \"apiClient_forbidden\": \"Access forbidden\",\n \"apiClient_notFound\": \"Resource not found\",\n \"apiClient_validationFailed\": \"Request validation failed\",\n \"apiClient_tokenRefreshFailed\": \"Token refresh failed, please log in again\",\n \"apiClient_notInitialized\": \"API client not initialized, please call apiClient.init() first\",\n \"apiClient_alreadyInitialized\": \"API client is already initialized; call apiClient.close() before init()\",\n \"apiClient_configError\": \"API client config error: {error}\",\n \"apiClient_responseValidationFailed\": \"Response validation failed\",\n \"apiClient_unknown\": \"Unknown error\"\n}\n","{\n \"apiClient_networkError\": \"ネットワークリクエストに失敗しました\",\n \"apiClient_timeout\": \"リクエストがタイムアウトしました\",\n \"apiClient_serverError\": \"サーバーエラー({status})\",\n \"apiClient_unauthorized\": \"認証されていません。再度ログインしてください\",\n \"apiClient_forbidden\": \"アクセスが拒否されました\",\n \"apiClient_notFound\": \"リソースが見つかりません\",\n \"apiClient_validationFailed\": \"リクエストの検証に失敗しました\",\n \"apiClient_tokenRefreshFailed\": \"トークンの更新に失敗しました。再度ログインしてください\",\n \"apiClient_notInitialized\": \"APIクライアントが初期化されていません。先にapiClient.init()を呼び出してください\",\n \"apiClient_alreadyInitialized\": \"APIクライアントは初期化済みです。init()の前にapiClient.close()を呼び出してください\",\n \"apiClient_configError\": \"APIクライアントの設定エラー: {error}\",\n \"apiClient_responseValidationFailed\": \"レスポンスの検証に失敗しました\",\n \"apiClient_unknown\": \"不明なエラー\"\n}\n","{\n \"apiClient_networkError\": \"网络请求失败\",\n \"apiClient_timeout\": \"请求超时\",\n \"apiClient_serverError\": \"服务器错误({status})\",\n \"apiClient_unauthorized\": \"未授权,请重新登录\",\n \"apiClient_forbidden\": \"无权限访问\",\n \"apiClient_notFound\": \"请求的资源不存在\",\n \"apiClient_validationFailed\": \"请求参数校验失败\",\n \"apiClient_tokenRefreshFailed\": \"Token刷新失败,请重新登录\",\n \"apiClient_notInitialized\": \"API 客户端未初始化,请先调用 apiClient.init()\",\n \"apiClient_alreadyInitialized\": \"API 客户端已初始化,请先调用 apiClient.close() 再重新初始化\",\n \"apiClient_configError\": \"API 客户端配置错误:{error}\",\n \"apiClient_responseValidationFailed\": \"响应数据校验失败\",\n \"apiClient_unknown\": \"未知错误\"\n}\n","/**\n * @h-ai/api-client — i18n\n *\n * 模块 i18n 文案访问入口。\n * @module api-client-i18n\n */\n\nimport { core } from '@h-ai/core'\nimport messagesEnUS from '../messages/en-US.json'\nimport messagesJaJP from '../messages/ja-JP.json'\nimport messagesZhCN from '../messages/zh-CN.json'\n\n/**\n * 模块内合法翻译键;由中文词典推导并与现有英文词典配套维护。\n *\n * @example 模块内部上下文\n * ```ts\n * const key: ApiClientMessageKey = 'apiClient_notInitialized'\n * const message = apiClientM(key)\n * ```\n */\ntype ApiClientMessageKey = keyof typeof messagesZhCN\n\n/**\n * 获取 API Client 本地化消息;跟随 Core 当前 locale,不记录凭据或响应正文。\n * @param key - 真实模块消息键。\n * @param options - 语言覆盖及 params 插值选项。\n * @returns 已插值的中文或英文消息。\n *\n * @example 模块内部上下文\n * ```ts\n * const message = apiClientM('apiClient_configError', { locale: 'en-US', params: { error: 'Invalid endpoint' } })\n * ```\n */\nexport const apiClientM = core.i18n.createMessageGetter<ApiClientMessageKey>({\n 'zh-CN': messagesZhCN,\n 'en-US': messagesEnUS,\n 'ja-JP': messagesJaJP,\n})\n","/** 认证表单动作:统一选择登录方式及安全 Token 存储。 */\nimport type { apiContract, IamLoginInput, IamRegisterInput } from '@h-ai/api-contract'\nimport type { HaiResult } from '@h-ai/core'\nimport type { ApiClient } from './api-client-types.js'\nimport { core } from '@h-ai/core'\n\n// 仅持有本机 DOM 监听器;业务会话真值仍由服务端撤销与超时决定。\nconst pageExitBindings = new WeakMap<object, Set<() => void>>()\n\n/**\n * 客户端关闭时清理本实例安装的页面监听器,不清理其它实例。\n * @example 模块内部上下文\n * ```ts\n * clearIamPageExitBindings({}) // 没有绑定时幂等。\n * ```\n */\nexport function clearIamPageExitBindings(auth: object): void {\n for (const stop of pageExitBindings.get(auth) ?? [])\n stop()\n pageExitBindings.delete(auth)\n}\n\n/**\n * 认证表单登录输入;默认 password,ldap 使用 identifier/password,email/sms 使用 identifier/code。\n * method/code 为客户端表单辅助字段,按所选方式映射到标准 IAM procedure。\n *\n * @example\n * ```ts\n * import type { IamAuthLoginInput } from '@h-ai/api-client'\n * const input: IamAuthLoginInput = { identifier: 'alice@example.com', password: '', method: 'email', code: '123456' }\n * ```\n */\nexport type IamAuthLoginInput = IamLoginInput & { method?: 'password' | 'ldap' | 'email' | 'sms', code?: string }\n\n/**\n * 将含标准 IAM 契约的客户端绑定为认证表单动作;包用户通过 apiClient.createAuthActions 调用。\n * 登录/注册成功后等待 client.auth.setTokens,失败 HaiResult 原样返回;存储失败拒绝 Promise,不能据服务端成功就显示已登录。\n * 调用方必须先 init 并启用 auth;未启用 auth 时客户端 setTokens 不会建立 Token 管理器。\n * @param client - 具有标准 IAM procedure 与 setTokens 能力的已初始化客户端。\n * @param client.iam - 标准 IAM 契约的领域分组。\n * @param client.iam.auth - 公开配置、登录、注册、验证码和密码重置 procedure。\n * @param client.auth - 认证成功后的凭据写入能力(setTokens)。\n * @returns publicConfig、按方式登录、注册、验证码和密码重置动作。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const initialized = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1', auth: {} })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try {\n * const actions = apiClient.createAuthActions(apiClient)\n * // 运行时凭据来自已验证的表单;此处只请求公开配置,不提交示例密码。\n * const config = await actions.publicConfig()\n * if (!config.success) throw new Error(config.error.message)\n * }\n * finally { await apiClient.close() }\n * ```\n */\nexport function createIamAuthActions(client: {\n iam: { auth: Pick<ApiClient<{ iam: typeof apiContract.iam }>['iam']['auth'], 'publicConfig' | 'createCaptcha' | 'closeSession' | 'login' | 'loginWithLdap' | 'loginWithOtp' | 'register' | 'sendOtp' | 'requestPasswordReset' | 'confirmPasswordReset' | 'renewPassword' | 'reauthenticate'> }\n auth: Pick<ApiClient<{ iam: typeof apiContract.iam }>['auth'], 'setTokens'>\n}) {\n /**\n * 认证成功后等待凭据保存;失败 HaiResult 不写存储,写入失败向调用方拒绝。\n * @param result - 标准 IAM 登录/注册结果,成功数据含 TokenPair。\n * @returns 同一结果;不能把服务端成功与本地安全存储成功混为一谈。\n *\n * @example 模块内部上下文\n * ```ts\n * const failure = { success: false as const, error: { code: 'test:denied', message: 'Denied', httpStatus: 401 } }\n * const result = await save(failure)\n * if (!result.success) { const message = result.error.message }\n * ```\n */\n async function save<T extends { tokens: Parameters<typeof client.auth.setTokens>[0] }>(result: HaiResult<T>) {\n if (result.success)\n await client.auth.setTokens(result.data.tokens)\n return result\n }\n return {\n bindPageExit(options: { enabled?: boolean } = {}) {\n if (!options.enabled || typeof window === 'undefined')\n return () => {}\n const target = window\n let sent = false\n const onPageHide = (event: PageTransitionEvent) => {\n if (event.persisted || sent)\n return\n sent = true\n // 不清理 Cookie/本地存储,避免迟到响应覆盖下一文档刚建立的新身份。\n void client.iam.auth.closeSession({}, { context: { keepalive: true, autoRefresh: false } }).then((result) => {\n if (!result.success)\n core.logger.warn('Page exit session revocation failed', { errorCode: result.error.code })\n }).catch(() => core.logger.warn('Page exit session request did not complete'))\n }\n target.addEventListener('pagehide', onPageHide)\n const bindings = pageExitBindings.get(client.auth) ?? new Set<() => void>()\n const stop = () => {\n target.removeEventListener('pagehide', onPageHide)\n bindings.delete(stop)\n if (!bindings.size)\n pageExitBindings.delete(client.auth)\n }\n bindings.add(stop)\n pageExitBindings.set(client.auth, bindings)\n return stop\n },\n publicConfig: () => client.iam.auth.publicConfig(),\n createCaptcha: (input: Parameters<typeof client.iam.auth.createCaptcha>[0]) => client.iam.auth.createCaptcha(input),\n login: async (input: IamAuthLoginInput) => {\n const result = input.method === 'ldap'\n ? await client.iam.auth.loginWithLdap({ username: input.identifier, password: input.password, captcha: input.captcha })\n : input.method === 'email' || input.method === 'sms'\n ? await client.iam.auth.loginWithOtp({ identifier: input.identifier, code: input.code ?? '', captcha: input.captcha })\n : await client.iam.auth.login({ identifier: input.identifier, password: input.password, captcha: input.captcha })\n return save(result)\n },\n register: async (input: IamRegisterInput) => save(await client.iam.auth.register(input)),\n sendOtp: (identifier: string, options?: { captcha?: string }) => client.iam.auth.sendOtp({ identifier, ...options }),\n requestPasswordReset: (identifier: string, options?: { captcha?: string }) => client.iam.auth.requestPasswordReset({ identifier, ...options }),\n confirmPasswordReset: (token: string, newPassword: string) => client.iam.auth.confirmPasswordReset({ token, newPassword }),\n renewPassword: (input: { identifier: string, password: string, newPassword: string, captcha?: string }) => client.iam.auth.renewPassword(input),\n reauthenticate: (password: string, operation: string) => client.iam.auth.reauthenticate({ password, operation }),\n }\n}\n","/**\n * @h-ai/api-client — 类型与错误码\n *\n * 公共类型(TokenPair、TokenStorage、ApiClientConfig 等)与标准错误集合(HaiApiClientError)。\n * 不包含实现,仅用于跨文件共享与对外导出。\n * @module api-client-types\n */\n\nimport type { ErrorInfo, HaiResult } from '@h-ai/core'\nimport type { CryptoFunctions } from '@h-ai/crypto'\nimport type { AnyContractRouter, ContractRouterClient } from '@orpc/contract'\nimport type { JsonifiedClient } from '@orpc/openapi-client'\nimport { core } from '@h-ai/core'\n\n// ─── 错误码 ───────────────────────────────────────────────────────────────────\n\nconst ApiClientErrorInfo = {\n NETWORK_ERROR: '001:502',\n TIMEOUT: '002:504',\n SERVER_ERROR: '003:502',\n UNAUTHORIZED: '004:401',\n FORBIDDEN: '005:403',\n NOT_FOUND: '006:404',\n VALIDATION_FAILED: '007:400',\n TOKEN_REFRESH_FAILED: '008:401',\n PAYLOAD_TOO_LARGE: '020:413',\n NOT_INITIALIZED: '010:500',\n CONFIG_ERROR: '011:500',\n UNKNOWN: '099:500',\n} as const satisfies ErrorInfo\n\n/**\n * 客户端标准错误集合;包含稳定 code/httpStatus 与构造错误的方法,业务调用先检查 success。\n * 传输失败在 procedure 代理中映射;服务端 HaiResult 的业务错误保持原契约。\n *\n * @example\n * ```ts\n * import { apiClient, HaiApiClientError } from '@h-ai/api-client'\n * const initialized = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1' })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try {\n * const result = await apiClient.iam.auth.currentUser()\n * if (!result.success && result.error.code === HaiApiClientError.UNAUTHORIZED.code) {\n * // 由应用切换到登录页面;不输出 Token。\n * const message = result.error.message\n * }\n * }\n * finally { await apiClient.close() }\n * ```\n */\nexport const HaiApiClientError = core.error.buildHaiErrorsDef('api-client', ApiClientErrorInfo)\n\n// ─── Token ────────────────────────────────────────────────────────────────────\n\n/**\n * 短期 access token、可选 refresh token 与有效期;HTTP 头使用 Bearer。\n * Cookie 模式的 refresh token 由服务端管理,不能要求浏览器读取或持久化它。\n *\n * @example\n * ```ts\n * import type { TokenPair } from '@h-ai/api-client'\n * const tokens: TokenPair = { accessToken: 'test-access', expiresIn: 3600, tokenType: 'Bearer' }\n * // 示例为测试凭据;生产 Token 只能来自可信服务端认证响应。\n * ```\n */\nexport interface TokenPair {\n /** 短期访问凭证,请求时作为 `Authorization: Bearer ...`。 */\n readonly accessToken: string\n /** 长期刷新凭证;httpOnly cookie 模式下由服务端管理,响应体中可能不存在。 */\n readonly refreshToken?: string\n /** accessToken 有效期秒数。 */\n readonly expiresIn: number\n /** 固定为 `'Bearer'`。 */\n readonly tokenType: 'Bearer'\n}\n\n/**\n * 异步安全凭据存储契约;null 仅表示无可用凭据,读写故障须拒绝 Promise。\n * 浏览器使用内存 access token/httpOnly refresh Cookie,原生端注入平台安全存储,禁止敏感 Token 放入 localStorage。\n * 已绑定客户端的身份切换通过 client.auth,避免绕过刷新代际与串行写入;部分写入失败须执行清理并报告清理失败。\n *\n * 内置实现:\n * - `apiClient.tokenStorage.httpOnlyCookie()` — **默认**;浏览器端推荐,refresh token 由服务端管理,防 XSS。\n * - `apiClient.tokenStorage.memory()` — Node.js 测试 / SSR(需显式传入)。\n *\n * Capacitor / 小程序场景请由对应模块提供(例如 `@h-ai/capacitor`)。\n *\n * @example\n * ```ts\n * import { apiClient, type TokenStorage } from '@h-ai/api-client'\n * const storage: TokenStorage = apiClient.tokenStorage.memory()\n * await storage.setAccessToken('test-access')\n * const accessToken = await storage.getAccessToken()\n * await storage.clear()\n * ```\n */\nexport interface TokenStorage {\n /**\n * 读取本适配器可用 access token;无凭据返回 null,存储故障拒绝。\n * @returns Token 或 null;不能把读取失败当成不存在。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const storage = apiClient.tokenStorage.memory()\n * const accessToken = await storage.getAccessToken()\n * ```\n */\n readonly getAccessToken: () => Promise<string | null>\n /**\n * 保存 access token;已绑定客户端应通过 auth.setTokens 维护代际。\n * @param token - 可信服务端认证返回的 access token。\n * @returns 写入完成;底层故障须拒绝。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const storage = apiClient.tokenStorage.memory()\n * await storage.setAccessToken('test-access')\n * await storage.clear()\n * ```\n */\n readonly setAccessToken: (token: string) => Promise<void>\n /**\n * 读取 refresh token 或 Cookie 模式内部哨兵;null 仅表示没有凭据。\n * @returns Token、模式哨兵或 null,不是对 httpOnly Cookie 内容的脚本读取。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const storage = apiClient.tokenStorage.memory()\n * const refreshToken = await storage.getRefreshToken()\n * // 内存适配器初始为空;Cookie 适配器由管理器识别模式哨兵。\n * ```\n */\n readonly getRefreshToken: () => Promise<string | null>\n /**\n * 保存 refresh token;Cookie 实现不通过脚本修改服务端 Cookie。\n * @param token - 服务端返回的刷新凭据;禁止写入 Web Storage。\n * @returns 写入完成;原生安全存储故障须拒绝。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const storage = apiClient.tokenStorage.memory()\n * await storage.setRefreshToken('test-refresh')\n * await storage.clear()\n * ```\n */\n readonly setRefreshToken: (token: string) => Promise<void>\n /**\n * 清空适配器可管理的凭据;Cookie 实现只能清内存 access token。\n * @returns 清理完成;失败须拒绝,服务端 Cookie/会话另由退出接口撤销。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const storage = apiClient.tokenStorage.memory()\n * await storage.clear()\n * const remaining = await storage.getAccessToken()\n * ```\n */\n readonly clear: () => Promise<void>\n}\n\n// ─── 配置 ─────────────────────────────────────────────────────────────────────\n\n/**\n * 启用 Token 注入与自动刷新;默认 Cookie 适配器,refreshPath 相对 baseUrl。\n * 网络/超时/5xx 为暂时失败,保留凭据;明确失效才清理并通知。回调不应抛出或记录 Token。\n *\n * @example\n * ```ts\n * import { apiClient, type AuthConfig } from '@h-ai/api-client'\n * const auth: AuthConfig = { storage: apiClient.tokenStorage.memory(), refreshPath: '/auth/refresh', refreshTimeoutMs: 5000, onTokenRefreshed: tokens => { const lifetime = tokens.expiresIn } }\n * // Node/SSR 选择独立内存存储;浏览器通常直接配置 auth: {}。\n * ```\n */\nexport interface AuthConfig {\n /**\n * Token 存储适配器;默认 `apiClient.tokenStorage.httpOnlyCookie()`(refresh token 由服务端管理,防 XSS)。\n *\n * - SSR / Node.js 测试场景请显式传入 `apiClient.tokenStorage.memory()`。\n * - 浏览器刷新后通过 httpOnly Cookie 恢复会话,不将 Token 持久化到 Web Storage。\n * - Capacitor / 小程序请使用对应平台的安全存储适配器(如 `@h-ai/capacitor`)。\n */\n readonly storage?: TokenStorage\n /** 刷新接口路径,相对于 `baseUrl`,默认 `/auth/refresh`。 */\n readonly refreshPath?: string\n /**\n * 刷新请求超时(ms),默认 10_000。\n * 避免 refresh 接口上游挂死时所有 dedup 调用方一起 hang。\n * 超时后按现有 transient 失败处理(不清 token,下次请求可重试)。\n */\n readonly refreshTimeoutMs?: number\n /** 刷新成功回调,用于同步状态(例如多 Tab 广播)。 */\n readonly onTokenRefreshed?: (tokens: TokenPair) => void\n /** 刷新失败回调(如 refresh token 已失效),通常用于跳转登录页。 */\n readonly onRefreshFailed?: () => void\n}\n\n/**\n * 客户端初始化配置;baseUrl 包含 API 前缀,headers/fetch 用于平台适配。\n * auth 缺省时不创建 TokenManager;timeout 是底层请求中断期限,不代表服务端已取消副作用。\n * transport 需要已初始化 crypto,首次业务请求前协商会话;客户端不接收服务端私钥。\n *\n * @example\n * ```ts\n * import { apiClient, type ApiClientConfig } from '@h-ai/api-client'\n * const config: ApiClientConfig = { baseUrl: 'https://api.example.com/api/v1', auth: { storage: apiClient.tokenStorage.memory() }, timeout: 15000, clientName: 'orders-web', headers: () => ({ 'x-app-channel': 'web' }) }\n * const initialized = await apiClient.init(config)\n * if (!initialized.success) throw new Error(initialized.error.message)\n * await apiClient.close()\n * ```\n */\nexport interface ApiClientConfig {\n /** API service 完整 baseUrl,通常含 `apiPrefix`,例如 `https://api.example.com/api/v1`。 */\n readonly baseUrl: string\n /** 认证配置;不传则不启用 Token 注入与自动刷新。 */\n readonly auth?: AuthConfig\n /** 请求超时(ms),默认 30000。 */\n readonly timeout?: number\n /** 客户端标识,写入 `x-request-client` 请求头,默认 `hai-api-client`。 */\n readonly clientName?: string\n /** 额外请求头,可为静态对象或异步函数。 */\n readonly headers?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>)\n /** 自定义 fetch(测试 mock / Capacitor / 小程序桥接)。 */\n readonly fetch?: typeof globalThis.fetch\n /**\n * 启用端到端传输加密(opt-in)。\n *\n * 配置后所有出站请求会在底层 fetch 之上做一次混合加密,并自动解密\n * 标记为 `X-Encrypted: true` 的响应。服务端需启用对应的 `serv.createApp({ transport })`。\n *\n * `crypto` 字段结构与 `@h-ai/crypto` 的 `crypto` 实例兼容;通常直接传入:\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * import { crypto } from '@h-ai/crypto'\n *\n * const cryptography = await crypto.init()\n * if (!cryptography.success) throw new Error(cryptography.error.message)\n * try {\n * const initialized = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1', transport: { crypto } })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * await apiClient.close()\n * }\n * finally { await crypto.close() }\n * ```\n */\n readonly transport?: ApiClientTransportConfig\n}\n\n// ─── 传输加密 ────────────────────────────────────────────────────────────────\n\n/**\n * 可选加密会话配置;密钥协商路径相对 baseUrl,并与服务端配置匹配。\n * crypto 必须先初始化;服务器版本公钥通过受保护的 TLS 握手取得,不配置服务端私钥。\n * 传输防重放不能代替业务幂等性,失败后不要自动重放非幂等操作。\n *\n * @example\n * ```ts\n * import { apiClient, type ApiClientTransportConfig } from '@h-ai/api-client'\n * import { crypto } from '@h-ai/crypto'\n * const cryptography = await crypto.init()\n * if (!cryptography.success) throw new Error(cryptography.error.message)\n * try {\n * const transport: ApiClientTransportConfig = { crypto, keyExchangePath: '/_hai/key-exchange' }\n * const initialized = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1', transport })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * await apiClient.close()\n * }\n * finally { await crypto.close() }\n * ```\n */\nexport interface ApiClientTransportConfig {\n /** 可选本地明文 UTF-8 字节上限;缺省自动跟随服务端,指定时取较小值。 */\n readonly maxPlaintextBytes?: number\n /**\n * crypto 服务实例(通常直接传入 `@h-ai/crypto` 的 `crypto`)。\n * api-client 通过 `crypto.transport.createClient(...)` 创建会话。\n */\n readonly crypto: CryptoFunctions\n /**\n * 密钥协商端点路径,相对于 `baseUrl`,默认 `/_hai/key-exchange`。\n *\n * 必须与服务端 `serv.createApp({ transport: { keyExchangePath } })` 一致。\n */\n readonly keyExchangePath?: string\n}\n\n// ─── Client 接口 ──────────────────────────────────────────────────────────────\n\n/**\n * 当前客户端的凭据辅助操作;仅 init 启用 auth 时建立实际管理器。\n * setTokens/clear 撤销旧刷新代际并等待串行存储变更;故障拒绝 Promise,调用方处理失败。\n * clear 不能撤销服务端会话或删除 httpOnly Cookie,退出仍需服务端接口成功。\n *\n * @example\n * ```ts\n * import { apiClient, type ApiClientAuth } from '@h-ai/api-client'\n * const initialized = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1', auth: {} })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try {\n * const auth: ApiClientAuth = apiClient.auth\n * const off = auth.onTokenRefreshed(tokens => { const lifetime = tokens.expiresIn })\n * off()\n * await auth.clear()\n * }\n * finally { await apiClient.close() }\n * ```\n */\nexport interface ApiClientAuth {\n /**\n * 替换整对凭据并同步废弃旧刷新代际;缺省 refreshToken 时清除旧值。\n * @param tokens - 服务端成功认证返回的真实 TokenPair。\n * @returns 写入完成;安全存储失败拒绝 Promise,调用方应清理部分凭据并报告清理失败。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const initialized = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1', auth: {} })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try {\n * const form = { identifier: 'alice', password: 'test-password' }\n * const login = await apiClient.iam.auth.login(form)\n * if (!login.success) throw new Error(login.error.message)\n * try { await apiClient.auth.setTokens(login.data.tokens) }\n * catch (writeError) {\n * try { await apiClient.auth.clear() }\n * catch (cleanupError) { throw new AggregateError([writeError, cleanupError], 'Credential persistence and cleanup failed') }\n * throw writeError\n * }\n * }\n * finally { await apiClient.close() }\n * ```\n */\n readonly setTokens: (tokens: TokenPair) => Promise<void>\n /**\n * 同步废弃旧刷新代际并等待存储清理;失败拒绝 Promise,不声称已持久删除。\n * 服务端会话与 httpOnly Cookie 仍需服务端退出撤销;本操作不能替代退出接口。\n * @returns 本地清理完成,不返回 HaiResult。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const initialized = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1', auth: {} })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try {\n * const logout = await apiClient.iam.auth.logout({})\n * await apiClient.auth.clear()\n * if (!logout.success) throw new Error(logout.error.message)\n * // 只有服务端退出和本地清理都成功,才能确认两侧均已处理。\n * }\n * finally { await apiClient.close() }\n * ```\n */\n readonly clear: () => Promise<void>\n /**\n * 订阅当前管理器刷新成功事件;回调在写入后同步调用,避免抛错或输出 Token。\n * @param callback - 接收 TokenPair 的同步回调。\n * @returns 取消订阅函数;未启用 auth 时返回空操作。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const initialized = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1', auth: {} })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try {\n * const off = apiClient.auth.onTokenRefreshed(tokens => { const expiresIn = tokens.expiresIn })\n * off()\n * }\n * finally { await apiClient.close() }\n * ```\n */\n readonly onTokenRefreshed: (callback: (tokens: TokenPair) => void) => () => void\n}\n\n/**\n * 客户端生命周期与认证辅助能力;init 的 HaiResult 必须先检查 success。\n * 同一实例并发 init 共享在途初始化,已初始化后再 init 返回 CONFIG_ERROR;切换配置先 close。\n * config 为脱敏快照;close 清理运行状态/加密会话,不等于服务端退出或凭据存储清理。\n *\n * @example\n * ```ts\n * import { apiClient, type ApiClientLifecycle } from '@h-ai/api-client'\n * const client: ApiClientLifecycle = apiClient\n * const initialized = await client.init({ baseUrl: 'https://api.example.com/api/v1' })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * const active = client.isInitialized\n * const snapshot = client.config\n * await client.close()\n * ```\n */\nexport interface ApiClientLifecycle {\n /**\n * 观察一次逻辑调用的最终结果;刷新重试保留关联 ID,不包含请求正文或凭据。\n * close 清理订阅;回调异常不影响请求,返回函数可提前取消。\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const unsubscribe = apiClient.observeRequests(record => { const requestId = record.requestId })\n * unsubscribe()\n * ```\n */\n readonly observeRequests: (observer: ApiRequestObserver) => () => void\n /**\n * 构造并提交客户端运行状态;初始化失败返回 CONFIG_ERROR,未提交半初始化对象。\n * 并发 init 共享同一初始化;已初始化再调用会失败,切换配置先 close。\n * @param config - baseUrl、认证、fetch 与可选加密配置。\n * @returns HaiResult;必须先判断 success,再发起 procedure。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const initialized = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1', auth: {} })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try {\n * const result = await apiClient.iam.auth.publicConfig()\n * if (!result.success) throw new Error(result.error.message)\n * }\n * finally { await apiClient.close() }\n * ```\n */\n readonly init: (config: ApiClientConfig) => Promise<HaiResult<void>>\n /**\n * 销毁当前加密会话并清除运行状态,允许重新 init;不发送服务端退出、不清空外置存储。\n * @returns 资源清理完成;不是 HaiResult,不应读取 success。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const initialized = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1', auth: {} })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * await apiClient.close()\n * const reinitialized = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1', auth: {} })\n * if (!reinitialized.success) throw new Error(reinitialized.error.message)\n * await apiClient.close()\n * ```\n */\n readonly close: () => Promise<void>\n /** 当前脱敏后的运行配置快照;未初始化时为 `null`。 */\n readonly config: ApiClientConfig | null\n /** 是否已完成初始化。 */\n readonly isInitialized: boolean\n /** Token 辅助操作。 */\n readonly auth: ApiClientAuth\n}\n\n/**\n * 逻辑请求诊断摘要。HTTP 200 的失败 HaiResult 同样 success=false。\n * @example\n * ```ts\n * import type { ApiRequestRecord } from '@h-ai/api-client'\n * const record: ApiRequestRecord = { requestId: 'request-1', procedure: 'orders.get', timestamp: 1, duration: 10, success: false, attempts: 1, errorCode: 'orders:missing' }\n * ```\n */\nexport interface ApiRequestRecord {\n readonly requestId: string\n readonly procedure: string\n readonly timestamp: number\n readonly duration: number\n readonly success: boolean\n readonly attempts: number\n readonly errorCode?: string\n readonly method?: string\n readonly url?: string\n readonly status?: number\n}\n\n/**\n * 同步观察者,只做有界收集。\n * @example\n * ```ts\n * import { apiClient, type ApiRequestObserver } from '@h-ai/api-client'\n * const observer: ApiRequestObserver = record => { const failed = !record.success }\n * const unsubscribe = apiClient.observeRequests(observer)\n * unsubscribe()\n * ```\n */\nexport type ApiRequestObserver = (record: ApiRequestRecord) => void\n\n/**\n * 由 oRPC 契约推导的 JSON typed client,与生命周期组合;procedure 的输入/输出由契约决定。\n * 调用先检查初始化和业务 HaiResult;自定义契约不能覆盖 init/close/config/isInitialized/auth 顶层成员。\n *\n * @example\n * ```ts\n * import { apiClient, type ApiClient } from '@h-ai/api-client'\n * import { apiContract } from '@h-ai/api-contract'\n * const contract = apiContract.create({ iam: apiContract.iam })\n * const client: ApiClient<typeof contract> = apiClient.create(contract)\n * const initialized = await client.init({ baseUrl: 'https://api.example.com/api/v1', auth: {} })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try {\n * const result = await client.iam.auth.currentUser()\n * if (!result.success) throw new Error(result.error.message)\n * const userId = result.data.id\n * }\n * finally { await client.close() }\n * ```\n */\nexport type ApiClient<TContract extends AnyContractRouter>\n = ApiClientLifecycle & JsonifiedClient<ContractRouterClient<TContract>>\n","/**\n * @h-ai/api-client — 主实现\n *\n * 包含两部分:\n * - `createApiClient(contract)`:根据 oRPC contract 创建 typed client 工厂(内部实现)。\n * - `apiClient`:统一公开入口;自身就是默认单例,同时挂载 `create` / `tokenStorage`。\n *\n * Client 同时具备生命周期能力(init/close/config/isInitialized/auth)和 contract\n * 推导出的 procedure 嵌套调用能力(如 `client.iam.auth.login(input)`)。\n *\n * 内置能力:\n * - 401 自动 refresh + 单次重试\n * - 并发 refresh 共享 Promise\n * - 请求超时(AbortController)\n * - `ORPCError` → `HaiApiClientError` 标准化映射\n * - 未初始化时调用 procedure 返回 `NOT_INITIALIZED`,不抛异常\n *\n * @example 默认单例\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n *\n * const initialized = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1', auth: {} })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try {\n * const me = await apiClient.iam.auth.currentUser()\n * if (!me.success) throw new Error(me.error.message)\n * const userId = me.data.id\n * }\n * finally { await apiClient.close() }\n * ```\n *\n * @example 自定义 contract\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * import { apiContract } from '@h-ai/api-contract'\n *\n * const client = apiClient.create(apiContract.create({ iam: apiContract.iam }))\n * const initialized = await client.init({ baseUrl: 'https://api.example.com/api/v1', auth: {} })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try {\n * const result = await client.iam.auth.publicConfig()\n * if (!result.success) throw new Error(result.error.message)\n * }\n * finally { await client.close() }\n * ```\n *\n * @module api-client-main\n */\n\nimport type { HaiResult } from '@h-ai/core'\nimport type { TransportClient } from '@h-ai/crypto'\nimport type { AnyContractRouter, ContractRouterClient } from '@orpc/contract'\nimport type { JsonifiedClient } from '@orpc/openapi-client'\nimport type { TokenManager } from './api-client-auth.js'\nimport type {\n ApiClient,\n ApiClientAuth,\n ApiClientConfig,\n ApiClientLifecycle,\n ApiRequestObserver,\n ApiRequestRecord,\n} from './api-client-types.js'\nimport { apiContract } from '@h-ai/api-contract'\nimport { core, err, ok } from '@h-ai/core'\nimport { TransportBodyTooLargeError, TransportTimeError } from '@h-ai/crypto'\nimport { createORPCClient, ORPCError } from '@orpc/client'\nimport { OpenAPILink } from '@orpc/openapi-client/fetch'\nimport {\n createHttpOnlyCookieTokenStorage,\n createMemoryTokenStorage,\n createTokenManager,\n} from './api-client-auth.js'\nimport { apiClientM } from './api-client-i18n.js'\nimport { clearIamPageExitBindings, createIamAuthActions } from './api-client-iam.js'\nimport { HaiApiClientError } from './api-client-types.js'\n\nconst DEFAULT_TIMEOUT = 30_000\nconst DEFAULT_CLIENT_NAME = 'hai-api-client'\nconst DEFAULT_REFRESH_PATH = apiContract.pathOf(apiContract.iam.auth.refresh)\nconst AUTH_ENTRY_PATHS = [\n apiContract.pathOf(apiContract.iam.auth.login),\n apiContract.pathOf(apiContract.iam.auth.loginWithOtp),\n apiContract.pathOf(apiContract.iam.auth.loginWithLdap),\n apiContract.pathOf(apiContract.iam.auth.requestPasswordReset),\n apiContract.pathOf(apiContract.iam.auth.confirmPasswordReset),\n apiContract.pathOf(apiContract.iam.auth.register),\n]\nconst TRAILING_SLASHES_REGEX = /\\/+$/\nconst REQUEST_OBSERVATION = Symbol('hai.api-client.request-observation')\n/**\n * 内部构造的可变摘要,不对消费者暴露写入能力。\n * @example 模块内部上下文\n * ```ts\n * function update(record: MutableRequestRecord) { record.attempts += 1 }\n * ```\n */\ntype MutableRequestRecord = { -readonly [Key in keyof ApiRequestRecord]: ApiRequestRecord[Key] }\n\n/** Token 存储方案集合:内存 / httpOnly Cookie。 */\nconst tokenStorage = {\n /** 内存存储——SSR / Node 测试或一次性会话使用。 */\n memory: createMemoryTokenStorage,\n /** httpOnly Cookie 存储——浏览器场景推荐方案。 */\n httpOnlyCookie: createHttpOnlyCookieTokenStorage,\n} as const\n\n// ─── 工厂 ─────────────────────────────────────────────────────────────────────\n\n/**\n * 按真实 oRPC 契约创建独立 typed client;公开调用入口为 apiClient.create(contract)。\n * init 返回 HaiResult,成功后才调用 procedure;已初始化实例必须先 close 再换配置。\n * procedure 将传输异常转换为 HaiResult;auth.setTokens/clear 的安全存储失败仍会拒绝 Promise。\n * SSR 应为每个请求创建客户端和内存存储;不共享默认单例中的用户凭据。\n * @param contract - 已定义的嵌套 oRPC contract;顶层命名不能与生命周期成员冲突。\n * @returns 生命周期、认证辅助操作与契约推导出的 procedure 代理。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * import { apiContract } from '@h-ai/api-contract'\n * const client = apiClient.create(apiContract.create({ iam: apiContract.iam }))\n * const initialized = await client.init({ baseUrl: 'https://api.example.com/api/v1', auth: { storage: apiClient.tokenStorage.memory() }, timeout: 10000 })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try {\n * const result = await client.iam.auth.currentUser()\n * if (!result.success) throw new Error(result.error.message)\n * const userId = result.data.id\n * }\n * finally { await client.close() }\n * ```\n */\nexport function createApiClient<const TContract extends AnyContractRouter>(\n contract: TContract,\n): ApiClient<TContract> {\n // 客户端运行时状态\n const state: {\n config: ApiClientConfig | null\n rawClient: JsonifiedClient<ContractRouterClient<TContract>> | null\n tokenManager: TokenManager | undefined\n transport: TransportClient | undefined\n observers: Set<ApiRequestObserver>\n } = {\n config: null,\n rawClient: null,\n tokenManager: undefined,\n transport: undefined,\n observers: new Set(),\n }\n\n const auth: ApiClientAuth = {\n async setTokens(tokens) { await state.tokenManager?.setTokens(tokens) },\n async clear() { await state.tokenManager?.clear() },\n onTokenRefreshed(callback) {\n return state.tokenManager?.onTokenRefreshed(callback) ?? (() => {})\n },\n }\n\n let initPromise: Promise<HaiResult<void>> | null = null\n const lifecycle: ApiClientLifecycle = {\n async init(config) {\n // 并发重入防护:同时多次 init() 只执行一次。\n // 另一个调用方 await 同一个 in-flight Promise,避免重复创建 raw client / token manager。\n if (initPromise)\n return initPromise\n // 已初始化后必须先 close() 再重新 init,避免旧 transport 会话被新状态覆盖后无法 destroy。\n if (state.rawClient) {\n return err(\n HaiApiClientError.CONFIG_ERROR,\n apiClientM('apiClient_alreadyInitialized'),\n )\n }\n initPromise = (async (): Promise<HaiResult<void>> => {\n let nextTransport: TransportClient | undefined\n try {\n nextTransport = buildTransportSession(config)\n const nextTokenManager = buildTokenManager(config, nextTransport)\n const nextRawClient = buildRawClient(contract, config, nextTokenManager, nextTransport)\n\n // 所有可能失败的构建步骤完成后再提交状态,避免调用方看到半初始化 client。\n state.config = config\n state.transport = nextTransport\n state.tokenManager = nextTokenManager\n state.rawClient = nextRawClient\n return ok(undefined)\n }\n catch (error) {\n // 构建过程中如果 transport 已创建但后续失败,必须主动释放,避免会话/定时器泄漏。\n nextTransport?.destroy()\n state.config = null\n state.rawClient = null\n state.tokenManager = undefined\n state.transport = undefined\n return err(\n HaiApiClientError.CONFIG_ERROR,\n apiClientM('apiClient_configError', { params: { error: String(error) } }),\n error,\n )\n }\n })()\n try {\n return await initPromise\n }\n finally {\n initPromise = null\n }\n },\n async close() {\n clearIamPageExitBindings(auth)\n state.observers.clear()\n state.transport?.destroy()\n state.config = null\n state.rawClient = null\n state.tokenManager = undefined\n state.transport = undefined\n },\n get config() { return state.config ? core.sanitize.sanitizeSensitiveFields(state.config) : null },\n get isInitialized() { return state.rawClient !== null },\n auth,\n observeRequests(observer) {\n state.observers.add(observer)\n return () => {\n state.observers.delete(observer)\n }\n },\n }\n\n // 生命周期成员优先,其余属性只记录真实契约路径,末端调用才交给 rawClient。\n // 契约顶层不能与 init/close/config/isInitialized/auth 重名;此处不提供额外路由注册。\n return new Proxy({}, {\n get(_target, property) {\n // Proxy 的属性键不一定是 string,也可能是 Symbol(例如某些运行时内部探测)。\n // 本 client 只处理字符串路径;非字符串键直接忽略,避免把 Symbol 误当成 API 路径的一部分。\n if (typeof property !== 'string')\n return undefined\n\n // 顶层字段命中 lifecycle 时,直接返回真实成员。\n // 例如:\n // - property === 'init' → 返回 init 函数\n // - property === 'auth' → 返回 auth 对象\n // 这样 `client.init()` / `client.auth.clear()` 就能像普通对象一样工作。\n if (property in lifecycle) {\n return Reflect.get(lifecycle, property)\n }\n\n // 走到这里,说明它不是生命周期字段,那就把它当成 contract 路径的第一段。\n // 例如访问 `client.iam` 时,这里会创建一个 path = ['iam'] 的嵌套 Proxy。\n // 后续如果继续访问 `.auth.login`,path 会逐步变成:\n // ['iam', 'auth'] → ['iam', 'auth', 'login']\n // 最终在函数调用时再映射到真正的 rawClient.iam.auth.login(...args)。\n return createProcedureProxy(state, [property])\n },\n // 这里的返回值在运行时是一个 Proxy,但从“可用能力”上看,它符合 `ApiClient<TContract>`:\n // - 一部分成员来自 lifecycle\n // - 另一部分成员来自 contract 推导出的 procedure 层级\n // 因此使用类型断言把这个动态对象声明为最终对外的 typed client。\n }) as ApiClient<TContract>\n}\n\n// ─── 默认单例 ─────────────────────────────────────────────────────────────────\n\n/**\n * 默认 API typed client:绑定 `iam` / `storage` / `ai` 三个领域。\n *\n * 大多数应用只需 `import { apiClient }` 并调用 `apiClient.init()` 即可。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n *\n * const initialized = await apiClient.init({\n * baseUrl: 'http://localhost:3000/api/v1',\n * auth: { refreshPath: '/auth/refresh' },\n * })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try {\n * const result = await apiClient.iam.auth.currentUser()\n * if (!result.success) throw new Error(result.error.message)\n * }\n * finally { await apiClient.close() }\n * ```\n */\nconst defaultApiContract = apiContract.create({ iam: apiContract.iam, storage: apiContract.storage, ai: apiContract.ai })\n\n/**\n * 默认 iam/storage/ai typed client 与 create、tokenStorage、createAuthActions 命名空间的交叉类型。\n * 具体项目契约使用 apiClient.create 的返回类型,不给默认单例附加未知 procedure。\n *\n * @example\n * ```ts\n * import { apiClient, type DefaultApiClient } from '@h-ai/api-client'\n * const client: DefaultApiClient = apiClient\n * const initialized = await client.init({ baseUrl: 'https://api.example.com/api/v1', auth: {} })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try { const actions = client.createAuthActions(client); const result = await actions.publicConfig(); if (!result.success) throw new Error(result.error.message) }\n * finally { await client.close() }\n * ```\n */\nexport type DefaultApiClient = ApiClient<typeof defaultApiContract> & {\n readonly create: typeof createApiClient\n readonly tokenStorage: typeof tokenStorage\n readonly createAuthActions: typeof createIamAuthActions\n}\n\n/**\n * API Client 公开入口:默认 iam/storage/ai 单例,以及独立实例、安全存储和认证动作工厂。\n * 未启用 auth 时不注入或刷新 Token;浏览器 auth: {} 使用 httpOnly Cookie 方案。\n * close 释放客户端运行状态/加密会话,不能替代服务端退出或 auth.clear。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const initialized = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1', auth: {} })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try {\n * const result = await apiClient.iam.auth.currentUser()\n * if (!result.success) throw new Error(result.error.message)\n * const permissions = result.data.permissions\n * }\n * finally { await apiClient.close() }\n * ```\n */\nexport const apiClient = new Proxy(createApiClient(defaultApiContract) as ApiClient<typeof defaultApiContract>, {\n get(target, property, receiver) {\n if (property === 'create')\n return createApiClient\n if (property === 'tokenStorage')\n return tokenStorage\n if (property === 'createAuthActions')\n return createIamAuthActions\n return Reflect.get(target as object, property, receiver)\n },\n}) as DefaultApiClient\n\n// ─── 内部:procedure 代理 ─────────────────────────────────────────────────────\n\n/**\n * 构造只记录属性路径的内部代理,末端调用交给 callProcedure;不在访问属性时发送请求。\n * @param state - 当前 rawClient,可在 close 后变为 null。\n * @param state.rawClient - 当前 oRPC 客户端;未初始化或关闭后为 null。\n * @param state.observers - 本实例逻辑请求订阅集合。\n * @param path - 已访问的 contract 属性路径。\n * @returns 动态代理;由契约公开类型约束调用路径,不对外承诺任意字符串 API。\n *\n * @example 模块内部上下文\n * ```ts\n * const proxy = createProcedureProxy({ rawClient: null }, ['iam'])\n * // 直到 apply 才调用 callProcedure;未初始化返回失败 HaiResult。\n * ```\n */\nfunction createProcedureProxy<TContract extends AnyContractRouter>(\n state: { rawClient: JsonifiedClient<ContractRouterClient<TContract>> | null, observers?: Set<ApiRequestObserver> },\n path: string[],\n): unknown {\n return new Proxy(() => undefined, {\n get(_t, property) {\n if (typeof property !== 'string')\n return undefined\n return createProcedureProxy(state, [...path, property])\n },\n apply(_t, _thisArg, args) {\n return callObservedProcedure(state.rawClient, path, args, state.observers)\n },\n })\n}\n\n/** 不复制业务入参;通过 oRPC 每次调用的 context 将同一关联传给传输层。 */\nasync function callObservedProcedure(rawClient: unknown, path: string[], args: unknown[], observers?: ReadonlySet<ApiRequestObserver>): Promise<unknown> {\n const record: MutableRequestRecord = { requestId: core.context.get().requestId ?? core.id.uuid(), procedure: path.join('.'), timestamp: Date.now(), duration: 0, success: false, attempts: 0 }\n const provided = args[1] && typeof args[1] === 'object' ? args[1] as Record<string, unknown> : {}\n const providedContext = provided.context && typeof provided.context === 'object' ? provided.context : {}\n const options = { ...provided, context: { ...providedContext, [REQUEST_OBSERVATION]: record } }\n let result = await callProcedure(rawClient, path, [args[0], options])\n record.duration = Math.max(0, Date.now() - record.timestamp)\n record.success = !(result && typeof result === 'object' && 'success' in result && result.success === false)\n if (result && typeof result === 'object' && 'error' in result && result.error && typeof result.error === 'object' && 'code' in result.error) {\n record.errorCode = String(result.error.code)\n if ('message' in result.error && typeof result.error.message === 'string') {\n const error = result.error as Parameters<typeof core.error.toPublic>[0]\n const receivedId = error.traceId && /^[\\w.:-]{1,128}$/.test(error.traceId) ? error.traceId : record.requestId\n record.requestId = receivedId\n const safe = core.error.toPublic({ ...error, publicExt: error.ext }, receivedId)\n if (error.cause && typeof error.cause === 'object' && !Array.isArray(error.cause)) {\n const metadata: Record<string, unknown> = {}\n for (const key of ['orpcCode', 'status', 'transportCode', 'direction', 'stage', 'limitBytes', 'actualBytes']) {\n const value: unknown = Reflect.get(error.cause, key)\n if (typeof value === 'number' || typeof value === 'string')\n metadata[key] = core.sanitize.sanitizeSensitiveValue(value, key)\n }\n if (Object.keys(metadata).length)\n safe.cause = metadata\n }\n core.logger.error('API request failed', { traceId: safe.traceId, requestId: record.requestId, procedure: record.procedure, status: record.status, error: result.error })\n result = { ...result, error: safe }\n }\n }\n for (const observer of observers ?? []) {\n try {\n const observed: unknown = observer(Object.freeze({ ...record }))\n if (observed instanceof Promise)\n void observed.catch(() => {})\n }\n catch { /* 观察失败不改变请求结果。 */ }\n }\n return result\n}\n\n/**\n * 解析真实 rawClient 中的 procedure 并调用,将调用异常映射为标准 HaiResult 失败。\n * @param rawClient - 已构建的 oRPC client;缺省时返回 NOT_INITIALIZED。\n * @param path - 契约层级路径。\n * @param args - 原始 procedure 参数。\n * @returns 原契约结果或客户端失败,不把失败改成成功。\n *\n * @example 模块内部上下文\n * ```ts\n * const result = await callProcedure(null, ['iam', 'auth', 'currentUser'], [])\n * // 实际得到 NOT_INITIALIZED;返回值为 unknown,外部类型来自真实契约。\n * ```\n */\nasync function callProcedure(\n rawClient: unknown,\n path: string[],\n args: unknown[],\n): Promise<unknown> {\n if (!rawClient) {\n return err(HaiApiClientError.NOT_INITIALIZED, apiClientM('apiClient_notInitialized'))\n }\n\n // 沿 path 解析 rawClient[a][b][c]...\n let target: unknown = rawClient\n for (const segment of path) {\n if ((typeof target !== 'object' || target === null) && typeof target !== 'function') {\n return err(HaiApiClientError.UNKNOWN, apiClientM('apiClient_unknown'))\n }\n target = Reflect.get(target as object, segment)\n }\n if (typeof target !== 'function') {\n return err(HaiApiClientError.UNKNOWN, apiClientM('apiClient_unknown'))\n }\n\n try {\n return await (target as (...a: unknown[]) => Promise<unknown>)(...args)\n }\n catch (error) {\n return mapClientError(error)\n }\n}\n\n// ─── 内部:构建 raw client 与 token manager ──────────────────────────────────\n\n/**\n * 仅 auth 存在时创建 TokenManager;刷新沿用自定义头、客户端标识与同一加密 fetch。\n * @param config - 已选定的客户端配置。\n * @param transport - 可选已创建的加密会话。\n * @returns 本实例管理器;没有 auth 时为 undefined,不自行启用认证。\n *\n * @example 模块内部上下文\n * ```ts\n * const manager = buildTokenManager({ baseUrl: 'https://api.example.com/api/v1', auth: { storage: createMemoryTokenStorage() }, headers: { 'x-app-channel': 'web' } }, undefined)\n * await manager?.clear()\n * ```\n */\nfunction buildTokenManager(config: ApiClientConfig, transport: TransportClient | undefined): TokenManager | undefined {\n if (!config.auth)\n return undefined\n\n const fetchFn = transport?.encryptedFetch ?? config.fetch ?? globalThis.fetch.bind(globalThis)\n // 默认使用 httpOnly cookie 存储(推荐方案:refresh token 由服务端管理,防 XSS)。\n // SSR / Node.js 测试场景请显式传入 `apiClient.tokenStorage.memory()`。\n // 原生客户端显式传入平台安全存储,浏览器不持久化 Token 到 Web Storage。\n const storage = config.auth.storage ?? createHttpOnlyCookieTokenStorage()\n const baseUrl = config.baseUrl.replace(TRAILING_SLASHES_REGEX, '')\n const refreshPath = config.auth.refreshPath ?? DEFAULT_REFRESH_PATH\n const refreshUrl = `${baseUrl}${refreshPath.startsWith('/') ? refreshPath : `/${refreshPath}`}`\n\n // 刷新沿用客户端标识与自定义头(包括原生 body 通道标识),并复用同一加密 fetch。\n const refreshFetch: typeof fetch = async (input, init) => {\n const configured = typeof config.headers === 'function' ? await config.headers() : config.headers ?? {}\n const headers = new Headers(configured)\n if (!headers.has('accept-language'))\n headers.set('accept-language', core.i18n.getGlobalLocale())\n headers.set('x-request-client', config.clientName ?? DEFAULT_CLIENT_NAME)\n new Headers(init?.headers).forEach((value, name) => headers.set(name, value))\n return fetchFn(input, { ...init, headers })\n }\n const manager = createTokenManager(storage, refreshUrl, refreshFetch, config.auth.onRefreshFailed, config.auth.refreshTimeoutMs)\n if (config.auth.onTokenRefreshed) {\n manager.onTokenRefreshed(config.auth.onTokenRefreshed)\n }\n return manager\n}\n\n/**\n * 根据显式 transport 配置创建会话;首次 encryptedFetch 才进行实际密钥协商。\n * @param config - 客户端配置及已初始化 crypto。\n * @returns 新会话或 undefined;调用方在失败/close 时 destroy。\n *\n * @example 模块内部上下文\n * ```ts\n * const session = buildTransportSession({ baseUrl: 'https://api.example.com/api/v1' })\n * // 未配置 transport 时不产生会话;正常明文请求仍由实际 fetch 发送。\n * session?.destroy()\n * ```\n */\nfunction buildTransportSession(config: ApiClientConfig): TransportClient | undefined {\n if (!config.transport)\n return undefined\n const baseFetch = config.fetch ?? globalThis.fetch.bind(globalThis)\n const baseUrl = config.baseUrl.replace(TRAILING_SLASHES_REGEX, '')\n const path = config.transport.keyExchangePath ?? '/_hai/key-exchange'\n const keyExchangeUrl = `${baseUrl}${path.startsWith('/') ? path : `/${path}`}`\n return config.transport.crypto.transport.createClient({ keyExchangeUrl, fetch: baseFetch, maxPlaintextBytes: config.transport.maxPlaintextBytes })\n}\n\n/**\n * 以同一 contract 构造 OpenAPILink 与 JSON typed client,统一头组装、超时和认证重试。\n * @param contract - 实际 procedure 契约。\n * @param config - 目标地址与平台 fetch。\n * @param tokenManager - 可选 Token 管理器。\n * @param transport - 可选加密会话。\n * @returns oRPC JSON client;模块外层代理将调用异常映射为失败 HaiResult。\n *\n * @example 模块内部上下文\n * ```ts\n * const contract = apiContract.create({ iam: apiContract.iam })\n * const raw = buildRawClient(contract, { baseUrl: 'https://api.example.com/api/v1' }, undefined, undefined)\n * // 构造不发送请求;业务调用通过外层 ApiClient 代理。\n * ```\n */\nfunction buildRawClient<TContract extends AnyContractRouter>(\n contract: TContract,\n config: ApiClientConfig,\n tokenManager: TokenManager | undefined,\n transport: TransportClient | undefined,\n): JsonifiedClient<ContractRouterClient<TContract>> {\n const link = new OpenAPILink<Record<PropertyKey, unknown>>(contract, {\n url: config.baseUrl,\n headers: async (options) => {\n const headers = await buildHeaders(config, tokenManager, getObservation(options.context)?.requestId)\n // 单次操作凭据只附加到本次调用,不能放进跨并发请求共享的默认 header。\n if (typeof options.context.reauthentication === 'string')\n headers['x-hai-reauthentication'] = options.context.reauthentication\n return headers\n },\n fetch: async (request, init, options) => {\n const record = getObservation(options.context)\n if (record) {\n record.method = request.method\n const url = new URL(request.url)\n url.search = ''\n url.hash = ''\n record.url = core.sanitize.sanitizeUrl(url.toString())\n }\n const requestInit = options.context.keepalive === true ? { ...init, keepalive: true } : init\n const response = await fetchWithRefresh(request, requestInit, config, options.context.autoRefresh === false ? undefined : tokenManager, transport, record)\n if (record) {\n record.status = response.status\n const traceId = response.headers.get('x-trace-id') ?? response.headers.get('x-request-id')\n if (traceId && /^[\\w.:-]{1,128}$/.test(traceId))\n record.requestId = traceId\n }\n return response\n },\n })\n return createORPCClient<JsonifiedClient<ContractRouterClient<TContract>>>(link)\n}\n\n/** 内部 context 属性只由逻辑调用建立,不能从业务输入读取。 */\nfunction getObservation(context: Record<PropertyKey, unknown>): MutableRequestRecord | undefined {\n const record = context[REQUEST_OBSERVATION]\n return record && typeof record === 'object' && 'requestId' in record ? record as MutableRequestRecord : undefined\n}\n\n/**\n * 合并静态/动态配置头与客户端标识,读取可用 access token 后设置 Bearer 头。\n * @param config - 用户请求头与客户端标识。\n * @param tokenManager - 可选管理器;读取安全存储失败向上游传播。\n * @returns 单次请求的头快照,不改变调用方配置对象。\n *\n * @example 模块内部上下文\n * ```ts\n * const headers = await buildHeaders({ baseUrl: 'https://api.example.com/api/v1', clientName: 'orders-web', headers: () => ({ 'x-app-channel': 'web' }) }, undefined)\n * const channel = headers['x-app-channel']\n * ```\n */\nasync function buildHeaders(\n config: ApiClientConfig,\n tokenManager: TokenManager | undefined,\n requestId?: string,\n): Promise<Record<string, string>> {\n const configured = typeof config.headers === 'function' ? await config.headers() : config.headers ?? {}\n const headers = new Headers(configured)\n if (!headers.has('accept-language'))\n headers.set('accept-language', core.i18n.getGlobalLocale())\n const accessToken = await tokenManager?.storage.getAccessToken()\n return {\n ...Object.fromEntries(headers),\n 'x-request-client': config.clientName ?? DEFAULT_CLIENT_NAME,\n 'x-request-id': requestId ?? core.context.get().requestId ?? core.id.uuid(),\n ...(accessToken ? { authorization: `Bearer ${accessToken}` } : {}),\n }\n}\n\n/**\n * 发送前克隆一次性请求体;HTTP 401 或有效失败包络的 401 才尝试刷新并最多重试一次。\n * 登录/注册/找回密码与 refresh 入口保留原始失败,不递归刷新;重试保留 body 并替换 Bearer 头。\n * @param request - 尚未消费的 Request。\n * @param init - oRPC 提供的重定向选项。\n * @param init.redirect - 可选的 fetch 重定向策略,原样传给首次请求及重试。\n * @param init.keepalive - 页面离开请求保持存活的浏览器提示,原样传递。\n * @param config - 请求与刷新配置。\n * @param tokenManager - 缺省时直接返回首次响应。\n * @param transport - 刷新与重试共用的可选加密会话。\n * @returns 最终 HTTP Response;不保证远端非幂等副作用没有发生。\n *\n * @example 模块内部上下文\n * ```ts\n * const config: ApiClientConfig = { baseUrl: 'https://api.example.com/api/v1', fetch: async () => Response.json({ success: true, data: {} }) }\n * const response = await fetchWithRefresh(new Request('https://api.example.com/api/v1/auth/me'), {}, config, undefined, undefined)\n * const status = response.status\n * ```\n */\nasync function fetchWithRefresh(\n request: Request,\n init: { redirect?: Request['redirect'], keepalive?: boolean },\n config: ApiClientConfig,\n tokenManager: TokenManager | undefined,\n transport: TransportClient | undefined,\n observation?: MutableRequestRecord,\n): Promise<Response> {\n // Fetch Request 的 body 是一次性可读流;为了支持 401 后重发 POST/PUT 等带 body 的请求,\n // 必须在首次发送前 `clone()` 一份。即使没有 401,clone 后的副本也只会被 GC 回收,无副作用。\n const retryRequest = tokenManager ? request.clone() : undefined\n if (observation)\n observation.attempts++\n const response = await fetchWithTimeout(request, init, config, transport)\n if (!tokenManager || !retryRequest)\n return response\n\n // 登录失败必须保留原始错误;refresh 本身也不能再次触发自动刷新。\n const baseUrl = config.baseUrl.replace(TRAILING_SLASHES_REGEX, '')\n const refreshPath = config.auth?.refreshPath ?? DEFAULT_REFRESH_PATH\n const excludedPaths = [...AUTH_ENTRY_PATHS, refreshPath]\n if (excludedPaths.some(path => new URL(`${baseUrl}${path.startsWith('/') ? path : `/${path}`}`).pathname === new URL(request.url).pathname))\n return response\n if (response.status !== 401 && !await isUnauthorizedResult(response))\n return response\n\n const tokens = await tokenManager.refresh()\n if (!tokens)\n return response\n\n const headers = new Headers(retryRequest.headers)\n headers.set('authorization', `Bearer ${tokens.accessToken}`)\n if (observation)\n observation.attempts++\n return fetchWithTimeout(new Request(retryRequest, { headers }), init, config, transport)\n}\n\n/**\n * 只从 HTTP 200 JSON 的有效失败 HaiResult 识别 httpStatus=401;克隆响应避免消费原正文。\n * @param response - 原始 HTTP 响应。\n * @returns 是否满足认证失效包络;非法 JSON/字段缺失留给原解析路径处理。\n *\n * @example 模块内部上下文\n * ```ts\n * const response = Response.json({ success: false, error: { code: 'hai:iam:001', message: 'Session expired', httpStatus: 401 } })\n * const unauthorized = await isUnauthorizedResult(response)\n * const originalBody = await response.json()\n * // 克隆检查没有消耗原响应正文。\n * ```\n */\nasync function isUnauthorizedResult(response: Response): Promise<boolean> {\n if (response.status !== 200 || !response.headers.get('content-type')?.includes('application/json'))\n return false\n try {\n const body: unknown = await response.clone().json()\n if (!body || typeof body !== 'object' || !('success' in body) || body.success !== false\n || !('error' in body) || !body.error || typeof body.error !== 'object') {\n return false\n }\n return 'httpStatus' in body.error && body.error.httpStatus === 401\n && 'code' in body.error && typeof body.error.code === 'string'\n && 'message' in body.error && typeof body.error.message === 'string'\n }\n catch {\n // 非 JSON/非法包络交由既有客户端解析路径报告,不掩盖为认证失败。\n return false\n }\n}\n\n/**\n * 把请求超时和调用方 signal 连接到本次 fetch;finally 清除本次超时计时器。\n * @param request - 含调用方取消信号的 Request。\n * @param init - oRPC 重定向设置。\n * @param init.redirect - 可选的 fetch 重定向策略,原样交给本次实际 fetch。\n * @param init.keepalive - 页面离开请求保持存活的浏览器提示,原样传递。\n * @param config - 超时与 fetch 配置。\n * @param transport - 可选加密 fetch,优先于原始 fetch。\n * @returns HTTP 响应;超时/取消异常由外层 procedure 映射,不能视为远端回滚。\n *\n * @example 模块内部上下文\n * ```ts\n * const request = new Request('https://api.example.com/api/v1/auth/me')\n * const response = await fetchWithTimeout(request, {}, { baseUrl: 'https://api.example.com/api/v1', timeout: 1000, fetch: async () => new Response(null, { status: 204 }) }, undefined)\n * const status = response.status\n * ```\n */\nasync function fetchWithTimeout(\n request: Request,\n init: { redirect?: Request['redirect'], keepalive?: boolean },\n config: ApiClientConfig,\n transport: TransportClient | undefined,\n): Promise<Response> {\n // 启用传输加密时,用 encryptedFetch 作为实际 fetch;\n // 它会首次请求前自动完成密钥协商,后续请求复用会话。\n const fetchFn = transport?.encryptedFetch ?? config.fetch ?? globalThis.fetch.bind(globalThis)\n const timeout = config.timeout ?? DEFAULT_TIMEOUT\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), timeout)\n // 保留调用方原始 signal 的中断能力:上游主动 abort 也能控制超时 controller。\n const userSignal = request.signal\n if (userSignal) {\n if (userSignal.aborted) {\n controller.abort()\n }\n else {\n userSignal.addEventListener('abort', () => controller.abort(), { once: true })\n }\n }\n try {\n return await fetchFn(new Request(request, { signal: controller.signal }), init)\n }\n finally {\n clearTimeout(timeoutId)\n }\n}\n\n/**\n * 将 oRPC 状态、AbortError 与其它传输异常映射为稳定客户端错误;保留 oRPC code/status/data 详情。\n * @param error - procedure 抛出的未知异常。\n * @returns 失败 HaiResult,不包含虚构的成功数据。\n *\n * @example 模块内部上下文\n * ```ts\n * const result = mapClientError(new DOMException('Request aborted', 'AbortError'))\n * if (!result.success) { const code = result.error.code }\n * ```\n */\nfunction mapClientError(error: unknown) {\n const time = error instanceof TransportTimeError ? error : error instanceof Error && error.cause instanceof TransportTimeError ? error.cause : undefined\n if (time)\n return err(HaiApiClientError.VALIDATION_FAILED, time.message, { transportCode: time.code })\n const capacity = error instanceof TransportBodyTooLargeError ? error : error instanceof Error && error.cause instanceof TransportBodyTooLargeError ? error.cause : undefined\n if (capacity)\n return err(HaiApiClientError.PAYLOAD_TOO_LARGE, capacity.message, { direction: capacity.direction, stage: capacity.stage, limitBytes: capacity.limitBytes, actualBytes: capacity.actualBytes })\n if (error instanceof ORPCError) {\n // 保留 ORPCError 的原始 code / status / data,供上层区分具体业务异常(不仅是 HTTP 状态码)。\n const detail = { orpcCode: error.code, status: error.status, data: error.data }\n if (error.status === 413)\n return err(HaiApiClientError.PAYLOAD_TOO_LARGE, error.message, detail)\n if (error.status === 400 || error.status === 422)\n return err(HaiApiClientError.VALIDATION_FAILED, error.message, detail)\n if (error.status === 401)\n return err(HaiApiClientError.UNAUTHORIZED, error.message, detail)\n if (error.status === 403)\n return err(HaiApiClientError.FORBIDDEN, error.message, detail)\n if (error.status === 404)\n return err(HaiApiClientError.NOT_FOUND, error.message, detail)\n return err(HaiApiClientError.SERVER_ERROR, error.message, detail)\n }\n if (error instanceof DOMException && error.name === 'AbortError') {\n return err(HaiApiClientError.TIMEOUT, apiClientM('apiClient_timeout'), error)\n }\n return err(HaiApiClientError.NETWORK_ERROR, apiClientM('apiClient_networkError'), error)\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/api-client-auth.ts","../messages/en-US.json","../messages/ja-JP.json","../messages/zh-CN.json","../src/api-client-i18n.ts","../src/api-client-iam.ts","../src/api-client-types.ts","../src/api-client-main.ts"],"names":["core","accessToken"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @h-ai/api-client — Token 存储与刷新管理\n *\n * 提供两个内置 Token 存储适配器(httpOnly Cookie / 内存)和一个 Token 管理器\n * (封装存储读写、自动刷新、并发去重、回调通知)。\n *\n * @example 浏览器 SPA\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n *\n * const initialized = await apiClient.init({\n * baseUrl: 'https://api.example.com/api/v1',\n * auth: { storage: apiClient.tokenStorage.httpOnlyCookie() },\n * })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * await apiClient.close()\n * ```\n *\n * @example Node.js 测试\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n *\n * const storage = apiClient.tokenStorage.memory()\n * await storage.setAccessToken('test-access')\n * await storage.clear()\n * ```\n *\n * @module api-client-auth\n */\n\nimport type { TokenPair, TokenStorage } from './api-client-types.js'\nimport { core } from '@h-ai/core'\nimport { z } from 'zod'\n\nconst logger = core.logger.child({ module: 'api-client', scope: 'auth' })\n\n/**\n * httpOnly cookie 模式哨兵值。\n * `apiClient.tokenStorage.httpOnlyCookie()` 的 `getRefreshToken()` 返回此值,\n * 告知 `doRefresh` 应依赖浏览器自动发送的 httpOnly Cookie,\n * 而不是将 refreshToken 写入请求体。\n */\nconst HTTPONLY_COOKIE_SENTINEL = '__httponly_cookie__'\n\n// ─── 内存存储 ─────────────────────────────────────────────────────────────────\n\n/**\n * 创建仅属于本适配器实例的内存凭据存储,页面刷新或进程结束后丢失。\n * SSR 应为每个请求创建客户端和存储实例,避免不同用户共享默认单例。\n * 通过 client.auth 更新已绑定的存储,才能撤销旧代际刷新;直接存储写入不提供这种保证。\n * @returns 两个初始为空的 Token 槽位及清理操作。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const storage = apiClient.tokenStorage.memory()\n * await storage.setAccessToken('test-access')\n * const saved = await storage.getAccessToken()\n * await storage.clear()\n * // saved 为本实例的测试值;不把 Token 写入 Web Storage。\n * ```\n */\nexport function createMemoryTokenStorage(): TokenStorage {\n let accessToken: string | null = null\n let refreshToken: string | null = null\n return {\n async getAccessToken() { return accessToken },\n async setAccessToken(token) { accessToken = token },\n async getRefreshToken() { return refreshToken },\n async setRefreshToken(token) { refreshToken = token },\n async clear() {\n accessToken = null\n refreshToken = null\n },\n }\n}\n\n// ─── httpOnly Cookie 存储 ────────────────────────────────────────────────────\n\n/**\n * 创建浏览器使用的内存 access token 与服务端 httpOnly refresh Cookie 适配器。\n * refresh getter 返回内部哨兵,setter 不修改 Cookie;clear 只清除内存 access token。\n * 刷新请求使用 credentials: include;服务端须实际设置/撤销 Cookie,配置适当的 Secure、SameSite、Path 与跨域凭据策略。\n * HttpOnly 限制脚本直接读取 Cookie,不能阻止已执行的恶意脚本代用户发请求;服务端仍须授权和防护 CSRF。\n * @returns 独立内存 access token 与 Cookie 模式标记;不接触 localStorage。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const initialized = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1', auth: { storage: apiClient.tokenStorage.httpOnlyCookie() } })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try {\n * const me = await apiClient.iam.auth.currentUser()\n * if (!me.success) throw new Error(me.error.message)\n * const userId = me.data.id\n * }\n * finally { await apiClient.close() }\n * ```\n */\nexport function createHttpOnlyCookieTokenStorage(): TokenStorage {\n // access token 保存在内存中,用于注入 Authorization 请求头\n let memoryAccessToken: string | null = null\n return {\n async getAccessToken() { return memoryAccessToken },\n async setAccessToken(token) { memoryAccessToken = token },\n // refresh token 由服务端通过 Set-Cookie 设置为 httpOnly cookie;\n // JS 不可见,返回哨兵值让 TokenManager 识别此模式。\n async getRefreshToken() { return HTTPONLY_COOKIE_SENTINEL },\n // refresh token 由服务端管理,JS 端无法写入 httpOnly cookie,忽略此调用。\n async setRefreshToken(_token) { /* 由服务端 Set-Cookie 响应头管理,客户端无需操作 */ },\n async clear() {\n memoryAccessToken = null\n // httpOnly cookie 的清除必须由服务端 logout 端点完成(Set-Cookie: Max-Age=0);\n // 客户端无法通过 JS 直接删除 httpOnly cookie。\n },\n }\n}\n\n// ─── Token 管理器 ─────────────────────────────────────────────────────────────\n\n/** 校验 refresh 响应体中的 Token 字段;httpOnly cookie 模式允许不返回 refreshToken。 */\nconst RefreshTokenPayloadSchema = z.object({\n accessToken: z.string().min(1),\n refreshToken: z.string().min(1).optional(),\n expiresIn: z.number().optional(),\n tokenType: z.string().optional(),\n})\n\n/**\n * 当前代际刷新成功后的同步通知;参数包含敏感凭据,不应记录 Token。\n * 回调在持久写入成功后调用,应自行处理业务异常,不能把抛错当成刷新重试机制。\n *\n * @example 模块内部上下文\n * ```ts\n * const observed: number[] = []\n * const callback: RefreshCallback = tokens => { observed.push(tokens.expiresIn) }\n * callback({ accessToken: 'test-access', expiresIn: 3600, tokenType: 'Bearer' })\n * ```\n */\ntype RefreshCallback = (tokens: TokenPair) => void\n\n/**\n * 创建模块内部 Token 管理器;包用户通过 apiClient.auth 使用,包入口不导出此工厂。\n * 同一代际的并发刷新共享请求;clear/setTokens 同步撤销旧代际,所有存储变更串行提交。\n * 迟到响应不能覆盖新账号、恢复退出凭据或触发旧回调;已开始的存储写入须完成后才能执行后续身份变更。\n * 4xx、无效响应和非 Cookie 模式缺少 refreshToken 会尝试清理凭据;5xx、网络/超时保留凭据并返回 null。\n * 无刷新凭据时通知失败并返回 null;存储读取失败可拒绝 Promise,setTokens/clear 的存储失败向调用方传播。\n * @param storage - 独占的安全存储适配器。\n * @param refreshEndpointUrl - baseUrl 与 refreshPath 组合后的完整 URL。\n * @param fetchFn - 实际 fetch;Cookie 模式发送凭据,普通模式发送 JSON refreshToken。\n * @param onRefreshFailed - 当前代际明确失效后的同步通知。\n * @param refreshTimeoutMs - fetch 中断超时毫秒数;依赖 fetch 实现响应 signal。\n * @returns 内部管理器;不要绕过它直接更新已绑定的 storage。\n *\n * @example 模块内部上下文\n * ```ts\n * const storage = createMemoryTokenStorage()\n * const fetchTokens: typeof fetch = async () => Response.json({ data: { accessToken: 'test-access-next', refreshToken: 'test-refresh-next', expiresIn: 3600 } })\n * const manager = createTokenManager(storage, 'https://api.example.com/api/v1/auth/refresh', fetchTokens)\n * await manager.setTokens({ accessToken: 'test-access', refreshToken: 'test-refresh', expiresIn: 3600, tokenType: 'Bearer' })\n * const off = manager.onTokenRefreshed(tokens => { const lifetime = tokens.expiresIn })\n * try { const refreshed = await manager.refresh() }\n * finally { off(); await manager.clear() }\n * ```\n */\nexport function createTokenManager(\n storage: TokenStorage,\n refreshEndpointUrl: string,\n fetchFn: typeof globalThis.fetch,\n onRefreshFailed?: () => void,\n refreshTimeoutMs: number = 10_000,\n) {\n const callbacks: RefreshCallback[] = []\n let refreshPromise: Promise<TokenPair | null> | null = null\n let refreshController: AbortController | null = null\n let generation = 0\n let sessionInvalid = false\n let storageMutation: Promise<void> = Promise.resolve()\n\n /**\n * 按调用顺序提交异步存储写入;单次失败只拒绝该调用,后续清理/登录仍可运行。\n * @param operation - 本次需要排序的写入操作。\n * @returns 本次操作结果,不吞掉当前写入故障。\n *\n * @example 模块内部上下文\n * ```ts\n * const order: string[] = []\n * await mutateStorage(async () => { order.push('login') })\n * await mutateStorage(async () => { order.push('logout') })\n * ```\n */\n function mutateStorage<T>(operation: () => Promise<T>): Promise<T> {\n const pending = storageMutation.then(operation)\n storageMutation = pending.then(() => {}, () => {})\n return pending\n }\n\n /**\n * 同步推进身份代际并移除旧刷新去重状态;返回旧控制器供调用方排好新写入后中断。\n * @returns 旧刷新控制器或 null;即使底层忽略 abort,代际检查仍拒绝旧结果提交。\n *\n * @example 模块内部上下文\n * ```ts\n * const old = invalidateRefresh()\n * // 生产调用先排入新身份存储变更,再触发可能重入的 abort 监听器。\n * old?.abort()\n * ```\n */\n function invalidateRefresh(): AbortController | null {\n const previousController = refreshController\n generation++\n refreshController = null\n refreshPromise = null\n return previousController\n }\n\n /**\n * 仅当前刷新代际可尝试清理存储并通知失效;旧账号迟到 401 不影响新身份。\n * @param refreshGeneration - 发起刷新时记录的身份代际。\n * @returns 清理完成;存储失败拒绝,由 doRefresh 的异常分支处理。\n *\n * @example 模块内部上下文\n * ```ts\n * await failRefresh(generation)\n * // 传入旧 generation 会在串行写入队列中再次校验并跳过。\n * ```\n */\n async function failRefresh(refreshGeneration: number): Promise<void> {\n await mutateStorage(async () => {\n if (refreshGeneration !== generation)\n return\n await storage.clear()\n if (refreshGeneration === generation) {\n sessionInvalid = true\n onRefreshFailed?.()\n }\n })\n }\n\n /**\n * 同一代际只发起一个刷新请求;旧请求 finally 不能清除新身份的在途状态。\n * @returns 成功且仍有效的 Token,或 null;读取适配器失败可能拒绝。\n *\n * @example 模块内部上下文\n * ```ts\n * const [first, second] = await Promise.all([refresh(), refresh()])\n * // 两个调用共享在途 fetch;结果仍以各自读取到的当前代际为准。\n * ```\n */\n async function refresh(): Promise<TokenPair | null> {\n // 并发去重:同一时刻只发一次 refresh,其它调用复用同一个 Promise\n if (refreshPromise)\n return refreshPromise\n const controller = new AbortController()\n const pending = doRefresh(generation, controller)\n refreshController = controller\n refreshPromise = pending\n try {\n return await pending\n }\n finally {\n // 旧请求结束不能移除换账号后新一轮刷新的去重状态。\n if (refreshPromise === pending) {\n refreshPromise = null\n refreshController = null\n }\n }\n }\n\n /**\n * 等待身份写入完成后读取刷新凭据,执行超时请求并在每个异步边界校验代际。\n * 4xx/无效数据尝试清理;5xx/网络故障保留凭据。只有当前代际成功写入后才通知订阅者。\n * @param refreshGeneration - 请求开始时的代际。\n * @param controller - 本次请求中断控制器;底层 fetch 应响应 signal。\n * @returns 当前刷新 Token 或 null;清理/回调等请求段异常记录为失败,不声称持久清理成功。\n *\n * @example 模块内部上下文\n * ```ts\n * const controller = new AbortController()\n * const tokens = await doRefresh(generation, controller)\n * if (tokens) { const lifetime = tokens.expiresIn }\n * ```\n */\n async function doRefresh(refreshGeneration: number, controller: AbortController): Promise<TokenPair | null> {\n // 登录/退出可能正在异步落盘,刷新只能读取其完成后的凭据。\n await storageMutation\n if (refreshGeneration !== generation)\n return null\n const refreshToken = await storage.getRefreshToken()\n if (refreshGeneration !== generation)\n return null\n if (!refreshToken) {\n sessionInvalid = true\n logger.debug('Token refresh skipped, no refresh token available')\n onRefreshFailed?.()\n return null\n }\n\n logger.debug('Refreshing token')\n // 超时保护:避免 refresh 接口挂死时 dedup Promise 永不 resolve,连带所有等待者 hang。\n const timeoutId = setTimeout(() => controller.abort(), refreshTimeoutMs)\n try {\n // httpOnly cookie 模式:浏览器自动发送 Cookie,请求体无需携带 refreshToken;\n // 同时设置 credentials:'include' 以支持跨域 Cookie 发送。\n const useHttpOnlyCookie = refreshToken === HTTPONLY_COOKIE_SENTINEL\n const requestInit: RequestInit = {\n method: 'POST',\n body: useHttpOnlyCookie ? undefined : JSON.stringify({ refreshToken }),\n credentials: useHttpOnlyCookie ? 'include' : 'same-origin',\n signal: controller.signal,\n }\n if (!useHttpOnlyCookie) {\n requestInit.headers = { 'Content-Type': 'application/json' }\n }\n const response = await fetchFn(refreshEndpointUrl, requestInit)\n if (refreshGeneration !== generation)\n return null\n\n if (!response.ok) {\n // 仅在服务端明确返回 401/4xx 时才清除 Token(refresh token 已失效);\n // 5xx 作为服务端临时故障,保留 Token 让下一次请求重试。\n if (response.status >= 400 && response.status < 500) {\n await failRefresh(refreshGeneration)\n }\n else {\n logger.warn('Token refresh transient failure, keeping tokens', { status: response.status })\n }\n return null\n }\n\n const body = await response.json() as { data?: unknown }\n if (refreshGeneration !== generation)\n return null\n const parsed = RefreshTokenPayloadSchema.safeParse(readTokenPayload(body.data))\n if (!parsed.success) {\n logger.warn('Token refresh returned invalid data', { issues: parsed.error.issues })\n await failRefresh(refreshGeneration)\n return null\n }\n if (!useHttpOnlyCookie && !parsed.data.refreshToken) {\n logger.warn('Token refresh returned invalid data', { issues: [{ path: ['refreshToken'], message: 'Required' }] })\n await failRefresh(refreshGeneration)\n return null\n }\n\n // 默认值兜底,避免不安全强转\n const refreshTokenForStorage = parsed.data.refreshToken ?? HTTPONLY_COOKIE_SENTINEL\n const tokens: TokenPair = {\n accessToken: parsed.data.accessToken,\n refreshToken: refreshTokenForStorage,\n expiresIn: parsed.data.expiresIn ?? 3600,\n tokenType: 'Bearer',\n }\n const committed = await mutateStorage(async () => {\n if (refreshGeneration !== generation)\n return false\n await storage.setAccessToken(tokens.accessToken)\n if (refreshGeneration !== generation)\n return false\n await storage.setRefreshToken(refreshTokenForStorage)\n return refreshGeneration === generation\n })\n if (!committed || refreshGeneration !== generation)\n return null\n sessionInvalid = false\n logger.debug('Token refreshed successfully')\n for (const cb of callbacks) {\n if (refreshGeneration !== generation)\n return null\n cb(tokens)\n }\n return refreshGeneration === generation ? tokens : null\n }\n catch (error) {\n if (refreshGeneration !== generation)\n return null\n // 网络或 fetch 本身抛异常(超时、断网、CORS)视为可重试的暂时性错误,\n // 不清除 Token,仅记录日志。调用方可在下次请求时重试。\n logger.error('Token refresh failed (transient)', { error })\n return null\n }\n finally {\n clearTimeout(timeoutId)\n }\n }\n\n return {\n storage,\n get generation() { return generation },\n get sessionInvalid() { return sessionInvalid },\n refresh,\n async setTokens(tokens: TokenPair) {\n const previousController = invalidateRefresh()\n sessionInvalid = false\n const mutationGeneration = generation\n const pending = mutateStorage(async () => {\n if (mutationGeneration !== generation)\n return\n // 替换完整凭据:新账号未提供 refresh token 时不能继续使用旧账号的值。\n await storage.clear()\n if (mutationGeneration !== generation)\n return\n await storage.setAccessToken(tokens.accessToken)\n if (tokens.refreshToken && mutationGeneration === generation)\n await storage.setRefreshToken(tokens.refreshToken)\n })\n // 先排好新身份写入,再触发 abort 事件;可重入的监听器只能读取新操作后的存储。\n previousController?.abort()\n await pending\n },\n async clear() {\n const previousController = invalidateRefresh()\n const pending = mutateStorage(() => storage.clear())\n previousController?.abort()\n await pending\n },\n onTokenRefreshed(callback: RefreshCallback): () => void {\n callbacks.push(callback)\n return () => {\n const idx = callbacks.indexOf(callback)\n if (idx >= 0)\n callbacks.splice(idx, 1)\n }\n },\n }\n}\n\n/**\n * Token 管理器内部契约,从真实工厂返回值推导;不属于包公开类型。\n *\n * @example 模块内部上下文\n * ```ts\n * const manager: TokenManager = createTokenManager(createMemoryTokenStorage(), 'https://api.example.com/api/v1/auth/refresh', fetch)\n * await manager.clear()\n * // clear 先废弃旧代际,再等待已有写入及本次清理完成。\n * ```\n */\nexport type TokenManager = ReturnType<typeof createTokenManager>\n\n/**\n * 从直接 tokens 或带 tokens 字段的 data 提取待校验内容;本函数不证明字段合法。\n * @param data - 已解析 JSON 包络的 data。\n * @returns 交给 RefreshTokenPayloadSchema 校验的未知值。\n *\n * @example 模块内部上下文\n * ```ts\n * const candidate = readTokenPayload({ tokens: { accessToken: 'test-access', expiresIn: 3600 } })\n * const parsed = RefreshTokenPayloadSchema.safeParse(candidate)\n * if (parsed.success) { const accessToken = parsed.data.accessToken }\n * ```\n */\nfunction readTokenPayload(data: unknown): unknown {\n if (typeof data === 'object' && data !== null && 'tokens' in data) {\n return (data as { tokens: unknown }).tokens\n }\n return data\n}\n","{\n \"apiClient_networkError\": \"Network request failed\",\n \"apiClient_timeout\": \"Request timeout\",\n \"apiClient_serverError\": \"Server error ({status})\",\n \"apiClient_unauthorized\": \"Unauthorized, please log in again\",\n \"apiClient_forbidden\": \"Access forbidden\",\n \"apiClient_notFound\": \"Resource not found\",\n \"apiClient_validationFailed\": \"Request validation failed\",\n \"apiClient_tokenRefreshFailed\": \"Token refresh failed, please log in again\",\n \"apiClient_notInitialized\": \"API client not initialized, please call apiClient.init() first\",\n \"apiClient_alreadyInitialized\": \"API client is already initialized; call apiClient.close() before init()\",\n \"apiClient_configError\": \"API client config error: {error}\",\n \"apiClient_responseValidationFailed\": \"Response validation failed\",\n \"apiClient_unknown\": \"Unknown error\"\n}\n","{\n \"apiClient_networkError\": \"ネットワークリクエストに失敗しました\",\n \"apiClient_timeout\": \"リクエストがタイムアウトしました\",\n \"apiClient_serverError\": \"サーバーエラー({status})\",\n \"apiClient_unauthorized\": \"認証されていません。再度ログインしてください\",\n \"apiClient_forbidden\": \"アクセスが拒否されました\",\n \"apiClient_notFound\": \"リソースが見つかりません\",\n \"apiClient_validationFailed\": \"リクエストの検証に失敗しました\",\n \"apiClient_tokenRefreshFailed\": \"トークンの更新に失敗しました。再度ログインしてください\",\n \"apiClient_notInitialized\": \"APIクライアントが初期化されていません。先にapiClient.init()を呼び出してください\",\n \"apiClient_alreadyInitialized\": \"APIクライアントは初期化済みです。init()の前にapiClient.close()を呼び出してください\",\n \"apiClient_configError\": \"APIクライアントの設定エラー: {error}\",\n \"apiClient_responseValidationFailed\": \"レスポンスの検証に失敗しました\",\n \"apiClient_unknown\": \"不明なエラー\"\n}\n","{\n \"apiClient_networkError\": \"网络请求失败\",\n \"apiClient_timeout\": \"请求超时\",\n \"apiClient_serverError\": \"服务器错误({status})\",\n \"apiClient_unauthorized\": \"未授权,请重新登录\",\n \"apiClient_forbidden\": \"无权限访问\",\n \"apiClient_notFound\": \"请求的资源不存在\",\n \"apiClient_validationFailed\": \"请求参数校验失败\",\n \"apiClient_tokenRefreshFailed\": \"Token刷新失败,请重新登录\",\n \"apiClient_notInitialized\": \"API 客户端未初始化,请先调用 apiClient.init()\",\n \"apiClient_alreadyInitialized\": \"API 客户端已初始化,请先调用 apiClient.close() 再重新初始化\",\n \"apiClient_configError\": \"API 客户端配置错误:{error}\",\n \"apiClient_responseValidationFailed\": \"响应数据校验失败\",\n \"apiClient_unknown\": \"未知错误\"\n}\n","/**\n * @h-ai/api-client — i18n\n *\n * 模块 i18n 文案访问入口。\n * @module api-client-i18n\n */\n\nimport { core } from '@h-ai/core'\nimport messagesEnUS from '../messages/en-US.json'\nimport messagesJaJP from '../messages/ja-JP.json'\nimport messagesZhCN from '../messages/zh-CN.json'\n\n/**\n * 模块内合法翻译键;由中文词典推导并与现有英文词典配套维护。\n *\n * @example 模块内部上下文\n * ```ts\n * const key: ApiClientMessageKey = 'apiClient_notInitialized'\n * const message = apiClientM(key)\n * ```\n */\ntype ApiClientMessageKey = keyof typeof messagesZhCN\n\n/**\n * 获取 API Client 本地化消息;跟随 Core 当前 locale,不记录凭据或响应正文。\n * @param key - 真实模块消息键。\n * @param options - 语言覆盖及 params 插值选项。\n * @returns 已插值的中文或英文消息。\n *\n * @example 模块内部上下文\n * ```ts\n * const message = apiClientM('apiClient_configError', { locale: 'en-US', params: { error: 'Invalid endpoint' } })\n * ```\n */\nexport const apiClientM = core.i18n.createMessageGetter<ApiClientMessageKey>({\n 'zh-CN': messagesZhCN,\n 'en-US': messagesEnUS,\n 'ja-JP': messagesJaJP,\n})\n","/** 认证表单动作:统一选择登录方式及安全 Token 存储。 */\nimport type { apiContract, IamLoginInput, IamRegisterInput } from '@h-ai/api-contract'\nimport type { HaiResult } from '@h-ai/core'\nimport type { ApiClient } from './api-client-types.js'\nimport { core } from '@h-ai/core'\n\n// 仅持有本机 DOM 监听器;业务会话真值仍由服务端撤销与超时决定。\nconst pageExitBindings = new WeakMap<object, Set<() => void>>()\n\n/**\n * 客户端关闭时清理本实例安装的页面监听器,不清理其它实例。\n * @example 模块内部上下文\n * ```ts\n * clearIamPageExitBindings({}) // 没有绑定时幂等。\n * ```\n */\nexport function clearIamPageExitBindings(auth: object): void {\n for (const stop of pageExitBindings.get(auth) ?? [])\n stop()\n pageExitBindings.delete(auth)\n}\n\n/**\n * 认证表单登录输入;默认 password,ldap 使用 identifier/password,email/sms 使用 identifier/code。\n * method/code 为客户端表单辅助字段,按所选方式映射到标准 IAM procedure。\n *\n * @example\n * ```ts\n * import type { IamAuthLoginInput } from '@h-ai/api-client'\n * const input: IamAuthLoginInput = { identifier: 'alice@example.com', password: '', method: 'email', code: '123456' }\n * ```\n */\nexport type IamAuthLoginInput = IamLoginInput & { method?: 'password' | 'ldap' | 'email' | 'sms', code?: string }\n\n/**\n * 将含标准 IAM 契约的客户端绑定为认证表单动作;包用户通过 apiClient.createAuthActions 调用。\n * 登录/注册成功后等待 client.auth.setTokens,失败 HaiResult 原样返回;存储失败拒绝 Promise,不能据服务端成功就显示已登录。\n * 调用方必须先 init 并启用 auth;未启用 auth 时客户端 setTokens 不会建立 Token 管理器。\n * @param client - 具有标准 IAM procedure 与 setTokens 能力的已初始化客户端。\n * @param client.iam - 标准 IAM 契约的领域分组。\n * @param client.iam.auth - 公开配置、登录、注册、验证码和密码重置 procedure。\n * @param client.auth - 认证成功后的凭据写入能力(setTokens)。\n * @returns publicConfig、按方式登录、注册、验证码和密码重置动作。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const initialized = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1', auth: {} })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try {\n * const actions = apiClient.createAuthActions(apiClient)\n * // 运行时凭据来自已验证的表单;此处只请求公开配置,不提交示例密码。\n * const config = await actions.publicConfig()\n * if (!config.success) throw new Error(config.error.message)\n * }\n * finally { await apiClient.close() }\n * ```\n */\nexport function createIamAuthActions(client: {\n iam: { auth: Pick<ApiClient<{ iam: typeof apiContract.iam }>['iam']['auth'], 'publicConfig' | 'createCaptcha' | 'closeSession' | 'login' | 'loginWithLdap' | 'loginWithOtp' | 'register' | 'sendOtp' | 'requestPasswordReset' | 'confirmPasswordReset' | 'renewPassword' | 'reauthenticate'> }\n auth: Pick<ApiClient<{ iam: typeof apiContract.iam }>['auth'], 'setTokens'>\n}) {\n /**\n * 认证成功后等待凭据保存;失败 HaiResult 不写存储,写入失败向调用方拒绝。\n * @param result - 标准 IAM 登录/注册结果,成功数据含 TokenPair。\n * @returns 同一结果;不能把服务端成功与本地安全存储成功混为一谈。\n *\n * @example 模块内部上下文\n * ```ts\n * const failure = { success: false as const, error: { code: 'test:denied', message: 'Denied', httpStatus: 401 } }\n * const result = await save(failure)\n * if (!result.success) { const message = result.error.message }\n * ```\n */\n async function save<T extends { tokens: Parameters<typeof client.auth.setTokens>[0] }>(result: HaiResult<T>) {\n if (result.success)\n await client.auth.setTokens(result.data.tokens)\n return result\n }\n return {\n bindPageExit(options: { enabled?: boolean } = {}) {\n if (!options.enabled || typeof window === 'undefined')\n return () => {}\n const target = window\n let sent = false\n const onPageHide = (event: PageTransitionEvent) => {\n if (event.persisted || sent)\n return\n sent = true\n // 不清理 Cookie/本地存储,避免迟到响应覆盖下一文档刚建立的新身份。\n void client.iam.auth.closeSession({}, { context: { keepalive: true, autoRefresh: false } }).then((result) => {\n if (!result.success)\n core.logger.warn('Page exit session revocation failed', { errorCode: result.error.code })\n }).catch(() => core.logger.warn('Page exit session request did not complete'))\n }\n target.addEventListener('pagehide', onPageHide)\n const bindings = pageExitBindings.get(client.auth) ?? new Set<() => void>()\n const stop = () => {\n target.removeEventListener('pagehide', onPageHide)\n bindings.delete(stop)\n if (!bindings.size)\n pageExitBindings.delete(client.auth)\n }\n bindings.add(stop)\n pageExitBindings.set(client.auth, bindings)\n return stop\n },\n publicConfig: () => client.iam.auth.publicConfig(),\n createCaptcha: (input: Parameters<typeof client.iam.auth.createCaptcha>[0]) => client.iam.auth.createCaptcha(input),\n login: async (input: IamAuthLoginInput) => {\n const result = input.method === 'ldap'\n ? await client.iam.auth.loginWithLdap({ username: input.identifier, password: input.password, captcha: input.captcha, expectedUserId: input.expectedUserId })\n : input.method === 'email' || input.method === 'sms'\n ? await client.iam.auth.loginWithOtp({ identifier: input.identifier, code: input.code ?? '', captcha: input.captcha, expectedUserId: input.expectedUserId })\n : await client.iam.auth.login({ identifier: input.identifier, password: input.password, captcha: input.captcha, expectedUserId: input.expectedUserId })\n return save(result)\n },\n register: async (input: IamRegisterInput) => save(await client.iam.auth.register(input)),\n sendOtp: (identifier: string, options?: { captcha?: string }) => client.iam.auth.sendOtp({ identifier, ...options }),\n requestPasswordReset: (identifier: string, options?: { captcha?: string }) => client.iam.auth.requestPasswordReset({ identifier, ...options }),\n confirmPasswordReset: (token: string, newPassword: string) => client.iam.auth.confirmPasswordReset({ token, newPassword }),\n renewPassword: (input: { identifier: string, password: string, newPassword: string, captcha?: string }) => client.iam.auth.renewPassword(input),\n reauthenticate: (password: string, operation: string) => client.iam.auth.reauthenticate({ password, operation }),\n }\n}\n","/**\n * @h-ai/api-client — 类型与错误码\n *\n * 公共类型(TokenPair、TokenStorage、ApiClientConfig 等)与标准错误集合(HaiApiClientError)。\n * 不包含实现,仅用于跨文件共享与对外导出。\n * @module api-client-types\n */\n\nimport type { ErrorInfo, HaiResult } from '@h-ai/core'\nimport type { CryptoFunctions } from '@h-ai/crypto'\nimport type { AnyContractRouter, ContractRouterClient } from '@orpc/contract'\nimport type { JsonifiedClient } from '@orpc/openapi-client'\nimport { core } from '@h-ai/core'\n\n// ─── 错误码 ───────────────────────────────────────────────────────────────────\n\nconst ApiClientErrorInfo = {\n NETWORK_ERROR: '001:502',\n TIMEOUT: '002:504',\n SERVER_ERROR: '003:502',\n UNAUTHORIZED: '004:401',\n FORBIDDEN: '005:403',\n NOT_FOUND: '006:404',\n VALIDATION_FAILED: '007:400',\n TOKEN_REFRESH_FAILED: '008:401',\n PAYLOAD_TOO_LARGE: '020:413',\n NOT_INITIALIZED: '010:500',\n CONFIG_ERROR: '011:500',\n UNKNOWN: '099:500',\n} as const satisfies ErrorInfo\n\n/**\n * 客户端标准错误集合;包含稳定 code/httpStatus 与构造错误的方法,业务调用先检查 success。\n * 传输失败在 procedure 代理中映射;服务端 HaiResult 的业务错误保持原契约。\n *\n * @example\n * ```ts\n * import { apiClient, HaiApiClientError } from '@h-ai/api-client'\n * const initialized = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1' })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try {\n * const result = await apiClient.iam.auth.currentUser()\n * if (!result.success && result.error.code === HaiApiClientError.UNAUTHORIZED.code) {\n * // 由应用切换到登录页面;不输出 Token。\n * const message = result.error.message\n * }\n * }\n * finally { await apiClient.close() }\n * ```\n */\nexport const HaiApiClientError = core.error.buildHaiErrorsDef('api-client', ApiClientErrorInfo)\n\n// ─── Token ────────────────────────────────────────────────────────────────────\n\n/**\n * 短期 access token、可选 refresh token 与有效期;HTTP 头使用 Bearer。\n * Cookie 模式的 refresh token 由服务端管理,不能要求浏览器读取或持久化它。\n *\n * @example\n * ```ts\n * import type { TokenPair } from '@h-ai/api-client'\n * const tokens: TokenPair = { accessToken: 'test-access', expiresIn: 3600, tokenType: 'Bearer' }\n * // 示例为测试凭据;生产 Token 只能来自可信服务端认证响应。\n * ```\n */\nexport interface TokenPair {\n /** 短期访问凭证,请求时作为 `Authorization: Bearer ...`。 */\n readonly accessToken: string\n /** 长期刷新凭证;httpOnly cookie 模式下由服务端管理,响应体中可能不存在。 */\n readonly refreshToken?: string\n /** accessToken 有效期秒数。 */\n readonly expiresIn: number\n /** 固定为 `'Bearer'`。 */\n readonly tokenType: 'Bearer'\n}\n\n/**\n * 异步安全凭据存储契约;null 仅表示无可用凭据,读写故障须拒绝 Promise。\n * 浏览器使用内存 access token/httpOnly refresh Cookie,原生端注入平台安全存储,禁止敏感 Token 放入 localStorage。\n * 已绑定客户端的身份切换通过 client.auth,避免绕过刷新代际与串行写入;部分写入失败须执行清理并报告清理失败。\n *\n * 内置实现:\n * - `apiClient.tokenStorage.httpOnlyCookie()` — **默认**;浏览器端推荐,refresh token 由服务端管理,防 XSS。\n * - `apiClient.tokenStorage.memory()` — Node.js 测试 / SSR(需显式传入)。\n *\n * Capacitor / 小程序场景请由对应模块提供(例如 `@h-ai/capacitor`)。\n *\n * @example\n * ```ts\n * import { apiClient, type TokenStorage } from '@h-ai/api-client'\n * const storage: TokenStorage = apiClient.tokenStorage.memory()\n * await storage.setAccessToken('test-access')\n * const accessToken = await storage.getAccessToken()\n * await storage.clear()\n * ```\n */\nexport interface TokenStorage {\n /**\n * 读取本适配器可用 access token;无凭据返回 null,存储故障拒绝。\n * @returns Token 或 null;不能把读取失败当成不存在。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const storage = apiClient.tokenStorage.memory()\n * const accessToken = await storage.getAccessToken()\n * ```\n */\n readonly getAccessToken: () => Promise<string | null>\n /**\n * 保存 access token;已绑定客户端应通过 auth.setTokens 维护代际。\n * @param token - 可信服务端认证返回的 access token。\n * @returns 写入完成;底层故障须拒绝。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const storage = apiClient.tokenStorage.memory()\n * await storage.setAccessToken('test-access')\n * await storage.clear()\n * ```\n */\n readonly setAccessToken: (token: string) => Promise<void>\n /**\n * 读取 refresh token 或 Cookie 模式内部哨兵;null 仅表示没有凭据。\n * @returns Token、模式哨兵或 null,不是对 httpOnly Cookie 内容的脚本读取。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const storage = apiClient.tokenStorage.memory()\n * const refreshToken = await storage.getRefreshToken()\n * // 内存适配器初始为空;Cookie 适配器由管理器识别模式哨兵。\n * ```\n */\n readonly getRefreshToken: () => Promise<string | null>\n /**\n * 保存 refresh token;Cookie 实现不通过脚本修改服务端 Cookie。\n * @param token - 服务端返回的刷新凭据;禁止写入 Web Storage。\n * @returns 写入完成;原生安全存储故障须拒绝。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const storage = apiClient.tokenStorage.memory()\n * await storage.setRefreshToken('test-refresh')\n * await storage.clear()\n * ```\n */\n readonly setRefreshToken: (token: string) => Promise<void>\n /**\n * 清空适配器可管理的凭据;Cookie 实现只能清内存 access token。\n * @returns 清理完成;失败须拒绝,服务端 Cookie/会话另由退出接口撤销。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const storage = apiClient.tokenStorage.memory()\n * await storage.clear()\n * const remaining = await storage.getAccessToken()\n * ```\n */\n readonly clear: () => Promise<void>\n}\n\n// ─── 配置 ─────────────────────────────────────────────────────────────────────\n\n/**\n * 启用 Token 注入与自动刷新;默认 Cookie 适配器,refreshPath 相对 baseUrl。\n * 网络/超时/5xx 为暂时失败,保留凭据;明确失效才清理并通知。回调不应抛出或记录 Token。\n *\n * @example\n * ```ts\n * import { apiClient, type AuthConfig } from '@h-ai/api-client'\n * const auth: AuthConfig = { storage: apiClient.tokenStorage.memory(), refreshPath: '/auth/refresh', refreshTimeoutMs: 5000, onTokenRefreshed: tokens => { const lifetime = tokens.expiresIn } }\n * // Node/SSR 选择独立内存存储;浏览器通常直接配置 auth: {}。\n * ```\n */\nexport interface AuthConfig {\n /** refresh明确失效后原地恢复同一用户会话,成功时只重试原请求一次;取消/结果不明不得返回true。 */\n readonly recoverSession?: () => Promise<boolean>\n /** 登录会话终端分组;默认web,可选app/desktop/api。服务端必须允许该类;不代表物理设备证明。 */\n readonly terminalType?: 'web' | 'app' | 'desktop' | 'api'\n /**\n * Token 存储适配器;默认 `apiClient.tokenStorage.httpOnlyCookie()`(refresh token 由服务端管理,防 XSS)。\n *\n * - SSR / Node.js 测试场景请显式传入 `apiClient.tokenStorage.memory()`。\n * - 浏览器刷新后通过 httpOnly Cookie 恢复会话,不将 Token 持久化到 Web Storage。\n * - Capacitor / 小程序请使用对应平台的安全存储适配器(如 `@h-ai/capacitor`)。\n */\n readonly storage?: TokenStorage\n /** 刷新接口路径,相对于 `baseUrl`,默认 `/auth/refresh`。 */\n readonly refreshPath?: string\n /**\n * 刷新请求超时(ms),默认 10_000。\n * 避免 refresh 接口上游挂死时所有 dedup 调用方一起 hang。\n * 超时后按现有 transient 失败处理(不清 token,下次请求可重试)。\n */\n readonly refreshTimeoutMs?: number\n /** 刷新成功回调,用于同步状态(例如多 Tab 广播)。 */\n readonly onTokenRefreshed?: (tokens: TokenPair) => void\n /** 刷新失败回调(如 refresh token 已失效),通常用于跳转登录页。 */\n readonly onRefreshFailed?: () => void\n}\n\n/**\n * 客户端初始化配置;baseUrl 包含 API 前缀,headers/fetch 用于平台适配。\n * auth 缺省时不创建 TokenManager;timeout 是底层请求中断期限,不代表服务端已取消副作用。\n * transport 需要已初始化 crypto,首次业务请求前协商会话;客户端不接收服务端私钥。\n *\n * @example\n * ```ts\n * import { apiClient, type ApiClientConfig } from '@h-ai/api-client'\n * const config: ApiClientConfig = { baseUrl: 'https://api.example.com/api/v1', auth: { storage: apiClient.tokenStorage.memory() }, timeout: 15000, clientName: 'orders-web', headers: () => ({ 'x-app-channel': 'web' }) }\n * const initialized = await apiClient.init(config)\n * if (!initialized.success) throw new Error(initialized.error.message)\n * await apiClient.close()\n * ```\n */\nexport interface ApiClientConfig {\n /** API service 完整 baseUrl,通常含 `apiPrefix`,例如 `https://api.example.com/api/v1`。 */\n readonly baseUrl: string\n /** 认证配置;不传则不启用 Token 注入与自动刷新。 */\n readonly auth?: AuthConfig\n /** 请求超时(ms),默认 30000。 */\n readonly timeout?: number\n /** 客户端标识,写入 `x-request-client` 请求头,默认 `hai-api-client`。 */\n readonly clientName?: string\n /** 额外请求头,可为静态对象或异步函数。 */\n readonly headers?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>)\n /** 自定义 fetch(测试 mock / Capacitor / 小程序桥接)。 */\n readonly fetch?: typeof globalThis.fetch\n /**\n * 启用端到端传输加密(opt-in)。\n *\n * 配置后所有出站请求会在底层 fetch 之上做一次混合加密,并自动解密\n * 标记为 `X-Encrypted: true` 的响应。服务端需启用对应的 `serv.createApp({ transport })`。\n *\n * `crypto` 字段结构与 `@h-ai/crypto` 的 `crypto` 实例兼容;通常直接传入:\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * import { crypto } from '@h-ai/crypto'\n *\n * const cryptography = await crypto.init()\n * if (!cryptography.success) throw new Error(cryptography.error.message)\n * try {\n * const initialized = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1', transport: { crypto } })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * await apiClient.close()\n * }\n * finally { await crypto.close() }\n * ```\n */\n readonly transport?: ApiClientTransportConfig\n}\n\n// ─── 传输加密 ────────────────────────────────────────────────────────────────\n\n/**\n * 可选加密会话配置;密钥协商路径相对 baseUrl,并与服务端配置匹配。\n * crypto 必须先初始化;服务器版本公钥通过受保护的 TLS 握手取得,不配置服务端私钥。\n * 传输防重放不能代替业务幂等性,失败后不要自动重放非幂等操作。\n *\n * @example\n * ```ts\n * import { apiClient, type ApiClientTransportConfig } from '@h-ai/api-client'\n * import { crypto } from '@h-ai/crypto'\n * const cryptography = await crypto.init()\n * if (!cryptography.success) throw new Error(cryptography.error.message)\n * try {\n * const transport: ApiClientTransportConfig = { crypto, keyExchangePath: '/_hai/key-exchange' }\n * const initialized = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1', transport })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * await apiClient.close()\n * }\n * finally { await crypto.close() }\n * ```\n */\nexport interface ApiClientTransportConfig {\n /** 可选本地明文 UTF-8 字节上限;缺省自动跟随服务端,指定时取较小值。 */\n readonly maxPlaintextBytes?: number\n /**\n * crypto 服务实例(通常直接传入 `@h-ai/crypto` 的 `crypto`)。\n * api-client 通过 `crypto.transport.createClient(...)` 创建会话。\n */\n readonly crypto: CryptoFunctions\n /**\n * 密钥协商端点路径,相对于 `baseUrl`,默认 `/_hai/key-exchange`。\n *\n * 必须与服务端 `serv.createApp({ transport: { keyExchangePath } })` 一致。\n */\n readonly keyExchangePath?: string\n}\n\n// ─── Client 接口 ──────────────────────────────────────────────────────────────\n\n/**\n * 当前客户端的凭据辅助操作;仅 init 启用 auth 时建立实际管理器。\n * setTokens/clear 撤销旧刷新代际并等待串行存储变更;故障拒绝 Promise,调用方处理失败。\n * clear 不能撤销服务端会话或删除 httpOnly Cookie,退出仍需服务端接口成功。\n *\n * @example\n * ```ts\n * import { apiClient, type ApiClientAuth } from '@h-ai/api-client'\n * const initialized = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1', auth: {} })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try {\n * const auth: ApiClientAuth = apiClient.auth\n * const off = auth.onTokenRefreshed(tokens => { const lifetime = tokens.expiresIn })\n * off()\n * await auth.clear()\n * }\n * finally { await apiClient.close() }\n * ```\n */\nexport interface ApiClientAuth {\n /**\n * 替换整对凭据并同步废弃旧刷新代际;缺省 refreshToken 时清除旧值。\n * @param tokens - 服务端成功认证返回的真实 TokenPair。\n * @returns 写入完成;安全存储失败拒绝 Promise,调用方应清理部分凭据并报告清理失败。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const initialized = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1', auth: {} })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try {\n * const form = { identifier: 'alice', password: 'test-password' }\n * const login = await apiClient.iam.auth.login(form)\n * if (!login.success) throw new Error(login.error.message)\n * try { await apiClient.auth.setTokens(login.data.tokens) }\n * catch (writeError) {\n * try { await apiClient.auth.clear() }\n * catch (cleanupError) { throw new AggregateError([writeError, cleanupError], 'Credential persistence and cleanup failed') }\n * throw writeError\n * }\n * }\n * finally { await apiClient.close() }\n * ```\n */\n readonly setTokens: (tokens: TokenPair) => Promise<void>\n /**\n * 同步废弃旧刷新代际并等待存储清理;失败拒绝 Promise,不声称已持久删除。\n * 服务端会话与 httpOnly Cookie 仍需服务端退出撤销;本操作不能替代退出接口。\n * @returns 本地清理完成,不返回 HaiResult。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const initialized = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1', auth: {} })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try {\n * const logout = await apiClient.iam.auth.logout({})\n * await apiClient.auth.clear()\n * if (!logout.success) throw new Error(logout.error.message)\n * // 只有服务端退出和本地清理都成功,才能确认两侧均已处理。\n * }\n * finally { await apiClient.close() }\n * ```\n */\n readonly clear: () => Promise<void>\n /**\n * 订阅当前管理器刷新成功事件;回调在写入后同步调用,避免抛错或输出 Token。\n * @param callback - 接收 TokenPair 的同步回调。\n * @returns 取消订阅函数;未启用 auth 时返回空操作。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const initialized = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1', auth: {} })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try {\n * const off = apiClient.auth.onTokenRefreshed(tokens => { const expiresIn = tokens.expiresIn })\n * off()\n * }\n * finally { await apiClient.close() }\n * ```\n */\n readonly onTokenRefreshed: (callback: (tokens: TokenPair) => void) => () => void\n}\n\n/**\n * 客户端生命周期与认证辅助能力;init 的 HaiResult 必须先检查 success。\n * 同一实例并发 init 共享在途初始化,已初始化后再 init 返回 CONFIG_ERROR;切换配置先 close。\n * config 为脱敏快照;close 清理运行状态/加密会话,不等于服务端退出或凭据存储清理。\n *\n * @example\n * ```ts\n * import { apiClient, type ApiClientLifecycle } from '@h-ai/api-client'\n * const client: ApiClientLifecycle = apiClient\n * const initialized = await client.init({ baseUrl: 'https://api.example.com/api/v1' })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * const active = client.isInitialized\n * const snapshot = client.config\n * await client.close()\n * ```\n */\nexport interface ApiClientLifecycle {\n /**\n * 观察一次逻辑调用的最终结果;刷新重试保留关联 ID,不包含请求正文或凭据。\n * close 清理订阅;回调异常不影响请求,返回函数可提前取消。\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const unsubscribe = apiClient.observeRequests(record => { const requestId = record.requestId })\n * unsubscribe()\n * ```\n */\n readonly observeRequests: (observer: ApiRequestObserver) => () => void\n /**\n * 构造并提交客户端运行状态;初始化失败返回 CONFIG_ERROR,未提交半初始化对象。\n * 并发 init 共享同一初始化;已初始化再调用会失败,切换配置先 close。\n * @param config - baseUrl、认证、fetch 与可选加密配置。\n * @returns HaiResult;必须先判断 success,再发起 procedure。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const initialized = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1', auth: {} })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try {\n * const result = await apiClient.iam.auth.publicConfig()\n * if (!result.success) throw new Error(result.error.message)\n * }\n * finally { await apiClient.close() }\n * ```\n */\n readonly init: (config: ApiClientConfig) => Promise<HaiResult<void>>\n /**\n * 销毁当前加密会话并清除运行状态,允许重新 init;不发送服务端退出、不清空外置存储。\n * @returns 资源清理完成;不是 HaiResult,不应读取 success。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const initialized = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1', auth: {} })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * await apiClient.close()\n * const reinitialized = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1', auth: {} })\n * if (!reinitialized.success) throw new Error(reinitialized.error.message)\n * await apiClient.close()\n * ```\n */\n readonly close: () => Promise<void>\n /** 当前脱敏后的运行配置快照;未初始化时为 `null`。 */\n readonly config: ApiClientConfig | null\n /** 是否已完成初始化。 */\n readonly isInitialized: boolean\n /** Token 辅助操作。 */\n readonly auth: ApiClientAuth\n}\n\n/**\n * 逻辑请求诊断摘要。HTTP 200 的失败 HaiResult 同样 success=false。\n * @example\n * ```ts\n * import type { ApiRequestRecord } from '@h-ai/api-client'\n * const record: ApiRequestRecord = { requestId: 'request-1', procedure: 'orders.get', timestamp: 1, duration: 10, success: false, attempts: 1, errorCode: 'orders:missing' }\n * ```\n */\nexport interface ApiRequestRecord {\n readonly requestId: string\n readonly procedure: string\n readonly timestamp: number\n readonly duration: number\n readonly success: boolean\n readonly attempts: number\n readonly errorCode?: string\n readonly method?: string\n readonly url?: string\n readonly status?: number\n}\n\n/**\n * 同步观察者,只做有界收集。\n * @example\n * ```ts\n * import { apiClient, type ApiRequestObserver } from '@h-ai/api-client'\n * const observer: ApiRequestObserver = record => { const failed = !record.success }\n * const unsubscribe = apiClient.observeRequests(observer)\n * unsubscribe()\n * ```\n */\nexport type ApiRequestObserver = (record: ApiRequestRecord) => void\n\n/**\n * 由 oRPC 契约推导的 JSON typed client,与生命周期组合;procedure 的输入/输出由契约决定。\n * 调用先检查初始化和业务 HaiResult;自定义契约不能覆盖 init/close/config/isInitialized/auth 顶层成员。\n *\n * @example\n * ```ts\n * import { apiClient, type ApiClient } from '@h-ai/api-client'\n * import { apiContract } from '@h-ai/api-contract'\n * const contract = apiContract.create({ iam: apiContract.iam })\n * const client: ApiClient<typeof contract> = apiClient.create(contract)\n * const initialized = await client.init({ baseUrl: 'https://api.example.com/api/v1', auth: {} })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try {\n * const result = await client.iam.auth.currentUser()\n * if (!result.success) throw new Error(result.error.message)\n * const userId = result.data.id\n * }\n * finally { await client.close() }\n * ```\n */\nexport type ApiClient<TContract extends AnyContractRouter>\n = ApiClientLifecycle & JsonifiedClient<ContractRouterClient<TContract>>\n","/**\n * @h-ai/api-client — 主实现\n *\n * 包含两部分:\n * - `createApiClient(contract)`:根据 oRPC contract 创建 typed client 工厂(内部实现)。\n * - `apiClient`:统一公开入口;自身就是默认单例,同时挂载 `create` / `tokenStorage`。\n *\n * Client 同时具备生命周期能力(init/close/config/isInitialized/auth)和 contract\n * 推导出的 procedure 嵌套调用能力(如 `client.iam.auth.login(input)`)。\n *\n * 内置能力:\n * - 401 自动 refresh + 单次重试\n * - 并发 refresh 共享 Promise\n * - 请求超时(AbortController)\n * - `ORPCError` → `HaiApiClientError` 标准化映射\n * - 未初始化时调用 procedure 返回 `NOT_INITIALIZED`,不抛异常\n *\n * @example 默认单例\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n *\n * const initialized = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1', auth: {} })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try {\n * const me = await apiClient.iam.auth.currentUser()\n * if (!me.success) throw new Error(me.error.message)\n * const userId = me.data.id\n * }\n * finally { await apiClient.close() }\n * ```\n *\n * @example 自定义 contract\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * import { apiContract } from '@h-ai/api-contract'\n *\n * const client = apiClient.create(apiContract.create({ iam: apiContract.iam }))\n * const initialized = await client.init({ baseUrl: 'https://api.example.com/api/v1', auth: {} })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try {\n * const result = await client.iam.auth.publicConfig()\n * if (!result.success) throw new Error(result.error.message)\n * }\n * finally { await client.close() }\n * ```\n *\n * @module api-client-main\n */\n\nimport type { HaiResult } from '@h-ai/core'\nimport type { TransportClient } from '@h-ai/crypto'\nimport type { AnyContractRouter, ContractRouterClient } from '@orpc/contract'\nimport type { JsonifiedClient } from '@orpc/openapi-client'\nimport type { TokenManager } from './api-client-auth.js'\nimport type {\n ApiClient,\n ApiClientAuth,\n ApiClientConfig,\n ApiClientLifecycle,\n ApiRequestObserver,\n ApiRequestRecord,\n} from './api-client-types.js'\nimport { apiContract, IamSessionErrorCodeSchema } from '@h-ai/api-contract'\nimport { core, err, ok } from '@h-ai/core'\nimport { TransportBodyTooLargeError, TransportTimeError } from '@h-ai/crypto'\nimport { createORPCClient, ORPCError } from '@orpc/client'\nimport { OpenAPILink } from '@orpc/openapi-client/fetch'\nimport {\n createHttpOnlyCookieTokenStorage,\n createMemoryTokenStorage,\n createTokenManager,\n} from './api-client-auth.js'\nimport { apiClientM } from './api-client-i18n.js'\nimport { clearIamPageExitBindings, createIamAuthActions } from './api-client-iam.js'\nimport { HaiApiClientError } from './api-client-types.js'\n\nconst DEFAULT_TIMEOUT = 30_000\nconst DEFAULT_CLIENT_NAME = 'hai-api-client'\nconst DEFAULT_REFRESH_PATH = apiContract.pathOf(apiContract.iam.auth.refresh)\nconst AUTH_ENTRY_PATHS = [\n apiContract.pathOf(apiContract.iam.auth.login),\n apiContract.pathOf(apiContract.iam.auth.loginWithOtp),\n apiContract.pathOf(apiContract.iam.auth.loginWithLdap),\n apiContract.pathOf(apiContract.iam.auth.requestPasswordReset),\n apiContract.pathOf(apiContract.iam.auth.confirmPasswordReset),\n apiContract.pathOf(apiContract.iam.auth.register),\n]\nconst TRAILING_SLASHES_REGEX = /\\/+$/\nconst REQUEST_OBSERVATION = Symbol('hai.api-client.request-observation')\n/**\n * 内部构造的可变摘要,不对消费者暴露写入能力。\n * @example 模块内部上下文\n * ```ts\n * function update(record: MutableRequestRecord) { record.attempts += 1 }\n * ```\n */\ntype MutableRequestRecord = { -readonly [Key in keyof ApiRequestRecord]: ApiRequestRecord[Key] }\n\n/** Token 存储方案集合:内存 / httpOnly Cookie。 */\nconst tokenStorage = {\n /** 内存存储——SSR / Node 测试或一次性会话使用。 */\n memory: createMemoryTokenStorage,\n /** httpOnly Cookie 存储——浏览器场景推荐方案。 */\n httpOnlyCookie: createHttpOnlyCookieTokenStorage,\n} as const\n\n// ─── 工厂 ─────────────────────────────────────────────────────────────────────\n\n/**\n * 按真实 oRPC 契约创建独立 typed client;公开调用入口为 apiClient.create(contract)。\n * init 返回 HaiResult,成功后才调用 procedure;已初始化实例必须先 close 再换配置。\n * procedure 将传输异常转换为 HaiResult;auth.setTokens/clear 的安全存储失败仍会拒绝 Promise。\n * SSR 应为每个请求创建客户端和内存存储;不共享默认单例中的用户凭据。\n * @param contract - 已定义的嵌套 oRPC contract;顶层命名不能与生命周期成员冲突。\n * @returns 生命周期、认证辅助操作与契约推导出的 procedure 代理。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * import { apiContract } from '@h-ai/api-contract'\n * const client = apiClient.create(apiContract.create({ iam: apiContract.iam }))\n * const initialized = await client.init({ baseUrl: 'https://api.example.com/api/v1', auth: { storage: apiClient.tokenStorage.memory() }, timeout: 10000 })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try {\n * const result = await client.iam.auth.currentUser()\n * if (!result.success) throw new Error(result.error.message)\n * const userId = result.data.id\n * }\n * finally { await client.close() }\n * ```\n */\nexport function createApiClient<const TContract extends AnyContractRouter>(\n contract: TContract,\n): ApiClient<TContract> {\n // 客户端运行时状态\n const state: {\n config: ApiClientConfig | null\n rawClient: JsonifiedClient<ContractRouterClient<TContract>> | null\n tokenManager: TokenManager | undefined\n transport: TransportClient | undefined\n observers: Set<ApiRequestObserver>\n } = {\n config: null,\n rawClient: null,\n tokenManager: undefined,\n transport: undefined,\n observers: new Set(),\n }\n\n const auth: ApiClientAuth = {\n async setTokens(tokens) { await state.tokenManager?.setTokens(tokens) },\n async clear() { await state.tokenManager?.clear() },\n onTokenRefreshed(callback) {\n return state.tokenManager?.onTokenRefreshed(callback) ?? (() => {})\n },\n }\n\n let initPromise: Promise<HaiResult<void>> | null = null\n const lifecycle: ApiClientLifecycle = {\n async init(config) {\n // 并发重入防护:同时多次 init() 只执行一次。\n // 另一个调用方 await 同一个 in-flight Promise,避免重复创建 raw client / token manager。\n if (initPromise)\n return initPromise\n // 已初始化后必须先 close() 再重新 init,避免旧 transport 会话被新状态覆盖后无法 destroy。\n if (state.rawClient) {\n return err(\n HaiApiClientError.CONFIG_ERROR,\n apiClientM('apiClient_alreadyInitialized'),\n )\n }\n initPromise = (async (): Promise<HaiResult<void>> => {\n let nextTransport: TransportClient | undefined\n try {\n nextTransport = buildTransportSession(config)\n const nextTokenManager = buildTokenManager(config, nextTransport)\n const nextRawClient = buildRawClient(contract, config, nextTokenManager, nextTransport)\n\n // 所有可能失败的构建步骤完成后再提交状态,避免调用方看到半初始化 client。\n state.config = config\n state.transport = nextTransport\n state.tokenManager = nextTokenManager\n state.rawClient = nextRawClient\n return ok(undefined)\n }\n catch (error) {\n // 构建过程中如果 transport 已创建但后续失败,必须主动释放,避免会话/定时器泄漏。\n nextTransport?.destroy()\n state.config = null\n state.rawClient = null\n state.tokenManager = undefined\n state.transport = undefined\n return err(\n HaiApiClientError.CONFIG_ERROR,\n apiClientM('apiClient_configError', { params: { error: String(error) } }),\n error,\n )\n }\n })()\n try {\n return await initPromise\n }\n finally {\n initPromise = null\n }\n },\n async close() {\n clearIamPageExitBindings(auth)\n state.observers.clear()\n state.transport?.destroy()\n state.config = null\n state.rawClient = null\n state.tokenManager = undefined\n state.transport = undefined\n },\n get config() { return state.config ? core.sanitize.sanitizeSensitiveFields(state.config) : null },\n get isInitialized() { return state.rawClient !== null },\n auth,\n observeRequests(observer) {\n state.observers.add(observer)\n return () => {\n state.observers.delete(observer)\n }\n },\n }\n\n // 生命周期成员优先,其余属性只记录真实契约路径,末端调用才交给 rawClient。\n // 契约顶层不能与 init/close/config/isInitialized/auth 重名;此处不提供额外路由注册。\n return new Proxy({}, {\n get(_target, property) {\n // Proxy 的属性键不一定是 string,也可能是 Symbol(例如某些运行时内部探测)。\n // 本 client 只处理字符串路径;非字符串键直接忽略,避免把 Symbol 误当成 API 路径的一部分。\n if (typeof property !== 'string')\n return undefined\n\n // 顶层字段命中 lifecycle 时,直接返回真实成员。\n // 例如:\n // - property === 'init' → 返回 init 函数\n // - property === 'auth' → 返回 auth 对象\n // 这样 `client.init()` / `client.auth.clear()` 就能像普通对象一样工作。\n if (property in lifecycle) {\n return Reflect.get(lifecycle, property)\n }\n\n // 走到这里,说明它不是生命周期字段,那就把它当成 contract 路径的第一段。\n // 例如访问 `client.iam` 时,这里会创建一个 path = ['iam'] 的嵌套 Proxy。\n // 后续如果继续访问 `.auth.login`,path 会逐步变成:\n // ['iam', 'auth'] → ['iam', 'auth', 'login']\n // 最终在函数调用时再映射到真正的 rawClient.iam.auth.login(...args)。\n return createProcedureProxy(state, [property])\n },\n // 这里的返回值在运行时是一个 Proxy,但从“可用能力”上看,它符合 `ApiClient<TContract>`:\n // - 一部分成员来自 lifecycle\n // - 另一部分成员来自 contract 推导出的 procedure 层级\n // 因此使用类型断言把这个动态对象声明为最终对外的 typed client。\n }) as ApiClient<TContract>\n}\n\n// ─── 默认单例 ─────────────────────────────────────────────────────────────────\n\n/**\n * 默认 API typed client:绑定 `iam` / `storage` / `ai` 三个领域。\n *\n * 大多数应用只需 `import { apiClient }` 并调用 `apiClient.init()` 即可。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n *\n * const initialized = await apiClient.init({\n * baseUrl: 'http://localhost:3000/api/v1',\n * auth: { refreshPath: '/auth/refresh' },\n * })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try {\n * const result = await apiClient.iam.auth.currentUser()\n * if (!result.success) throw new Error(result.error.message)\n * }\n * finally { await apiClient.close() }\n * ```\n */\nconst defaultApiContract = apiContract.create({ iam: apiContract.iam, storage: apiContract.storage, ai: apiContract.ai })\n\n/**\n * 默认 iam/storage/ai typed client 与 create、tokenStorage、createAuthActions 命名空间的交叉类型。\n * 具体项目契约使用 apiClient.create 的返回类型,不给默认单例附加未知 procedure。\n *\n * @example\n * ```ts\n * import { apiClient, type DefaultApiClient } from '@h-ai/api-client'\n * const client: DefaultApiClient = apiClient\n * const initialized = await client.init({ baseUrl: 'https://api.example.com/api/v1', auth: {} })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try { const actions = client.createAuthActions(client); const result = await actions.publicConfig(); if (!result.success) throw new Error(result.error.message) }\n * finally { await client.close() }\n * ```\n */\nexport type DefaultApiClient = ApiClient<typeof defaultApiContract> & {\n readonly create: typeof createApiClient\n readonly tokenStorage: typeof tokenStorage\n readonly createAuthActions: typeof createIamAuthActions\n}\n\n/**\n * API Client 公开入口:默认 iam/storage/ai 单例,以及独立实例、安全存储和认证动作工厂。\n * 未启用 auth 时不注入或刷新 Token;浏览器 auth: {} 使用 httpOnly Cookie 方案。\n * close 释放客户端运行状态/加密会话,不能替代服务端退出或 auth.clear。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * const initialized = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1', auth: {} })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try {\n * const result = await apiClient.iam.auth.currentUser()\n * if (!result.success) throw new Error(result.error.message)\n * const permissions = result.data.permissions\n * }\n * finally { await apiClient.close() }\n * ```\n */\nexport const apiClient = new Proxy(createApiClient(defaultApiContract) as ApiClient<typeof defaultApiContract>, {\n get(target, property, receiver) {\n if (property === 'create')\n return createApiClient\n if (property === 'tokenStorage')\n return tokenStorage\n if (property === 'createAuthActions')\n return createIamAuthActions\n return Reflect.get(target as object, property, receiver)\n },\n}) as DefaultApiClient\n\n// ─── 内部:procedure 代理 ─────────────────────────────────────────────────────\n\n/**\n * 构造只记录属性路径的内部代理,末端调用交给 callProcedure;不在访问属性时发送请求。\n * @param state - 当前 rawClient,可在 close 后变为 null。\n * @param state.rawClient - 当前 oRPC 客户端;未初始化或关闭后为 null。\n * @param state.observers - 本实例逻辑请求订阅集合。\n * @param path - 已访问的 contract 属性路径。\n * @returns 动态代理;由契约公开类型约束调用路径,不对外承诺任意字符串 API。\n *\n * @example 模块内部上下文\n * ```ts\n * const proxy = createProcedureProxy({ rawClient: null }, ['iam'])\n * // 直到 apply 才调用 callProcedure;未初始化返回失败 HaiResult。\n * ```\n */\nfunction createProcedureProxy<TContract extends AnyContractRouter>(\n state: { rawClient: JsonifiedClient<ContractRouterClient<TContract>> | null, observers?: Set<ApiRequestObserver> },\n path: string[],\n): unknown {\n return new Proxy(() => undefined, {\n get(_t, property) {\n if (typeof property !== 'string')\n return undefined\n return createProcedureProxy(state, [...path, property])\n },\n apply(_t, _thisArg, args) {\n return callObservedProcedure(state.rawClient, path, args, state.observers)\n },\n })\n}\n\n/** 不复制业务入参;通过 oRPC 每次调用的 context 将同一关联传给传输层。 */\nasync function callObservedProcedure(rawClient: unknown, path: string[], args: unknown[], observers?: ReadonlySet<ApiRequestObserver>): Promise<unknown> {\n const record: MutableRequestRecord = { requestId: core.context.get().requestId ?? core.id.uuid(), procedure: path.join('.'), timestamp: Date.now(), duration: 0, success: false, attempts: 0 }\n const provided = args[1] && typeof args[1] === 'object' ? args[1] as Record<string, unknown> : {}\n const providedContext = provided.context && typeof provided.context === 'object' ? provided.context : {}\n const options = { ...provided, context: { ...providedContext, [REQUEST_OBSERVATION]: record } }\n let result = await callProcedure(rawClient, path, [args[0], options])\n record.duration = Math.max(0, Date.now() - record.timestamp)\n record.success = !(result && typeof result === 'object' && 'success' in result && result.success === false)\n if (result && typeof result === 'object' && 'error' in result && result.error && typeof result.error === 'object' && 'code' in result.error) {\n record.errorCode = String(result.error.code)\n if ('message' in result.error && typeof result.error.message === 'string') {\n const error = result.error as Parameters<typeof core.error.toPublic>[0]\n const receivedId = error.traceId && /^[\\w.:-]{1,128}$/.test(error.traceId) ? error.traceId : record.requestId\n record.requestId = receivedId\n const safe = core.error.toPublic({ ...error, publicExt: error.ext }, receivedId)\n if (error.cause && typeof error.cause === 'object' && !Array.isArray(error.cause)) {\n const metadata: Record<string, unknown> = {}\n for (const key of ['orpcCode', 'status', 'transportCode', 'direction', 'stage', 'limitBytes', 'actualBytes']) {\n const value: unknown = Reflect.get(error.cause, key)\n if (typeof value === 'number' || typeof value === 'string')\n metadata[key] = core.sanitize.sanitizeSensitiveValue(value, key)\n }\n if (Object.keys(metadata).length)\n safe.cause = metadata\n }\n core.logger.error('API request failed', { traceId: safe.traceId, requestId: record.requestId, procedure: record.procedure, status: record.status, error: result.error })\n result = { ...result, error: safe }\n }\n }\n for (const observer of observers ?? []) {\n try {\n const observed: unknown = observer(Object.freeze({ ...record }))\n if (observed instanceof Promise)\n void observed.catch(() => {})\n }\n catch { /* 观察失败不改变请求结果。 */ }\n }\n return result\n}\n\n/**\n * 解析真实 rawClient 中的 procedure 并调用,将调用异常映射为标准 HaiResult 失败。\n * @param rawClient - 已构建的 oRPC client;缺省时返回 NOT_INITIALIZED。\n * @param path - 契约层级路径。\n * @param args - 原始 procedure 参数。\n * @returns 原契约结果或客户端失败,不把失败改成成功。\n *\n * @example 模块内部上下文\n * ```ts\n * const result = await callProcedure(null, ['iam', 'auth', 'currentUser'], [])\n * // 实际得到 NOT_INITIALIZED;返回值为 unknown,外部类型来自真实契约。\n * ```\n */\nasync function callProcedure(\n rawClient: unknown,\n path: string[],\n args: unknown[],\n): Promise<unknown> {\n if (!rawClient) {\n return err(HaiApiClientError.NOT_INITIALIZED, apiClientM('apiClient_notInitialized'))\n }\n\n // 沿 path 解析 rawClient[a][b][c]...\n let target: unknown = rawClient\n for (const segment of path) {\n if ((typeof target !== 'object' || target === null) && typeof target !== 'function') {\n return err(HaiApiClientError.UNKNOWN, apiClientM('apiClient_unknown'))\n }\n target = Reflect.get(target as object, segment)\n }\n if (typeof target !== 'function') {\n return err(HaiApiClientError.UNKNOWN, apiClientM('apiClient_unknown'))\n }\n\n try {\n return await (target as (...a: unknown[]) => Promise<unknown>)(...args)\n }\n catch (error) {\n return mapClientError(error)\n }\n}\n\n// ─── 内部:构建 raw client 与 token manager ──────────────────────────────────\n\n/**\n * 仅 auth 存在时创建 TokenManager;刷新沿用自定义头、客户端标识与同一加密 fetch。\n * @param config - 已选定的客户端配置。\n * @param transport - 可选已创建的加密会话。\n * @returns 本实例管理器;没有 auth 时为 undefined,不自行启用认证。\n *\n * @example 模块内部上下文\n * ```ts\n * const manager = buildTokenManager({ baseUrl: 'https://api.example.com/api/v1', auth: { storage: createMemoryTokenStorage() }, headers: { 'x-app-channel': 'web' } }, undefined)\n * await manager?.clear()\n * ```\n */\nfunction buildTokenManager(config: ApiClientConfig, transport: TransportClient | undefined): TokenManager | undefined {\n if (!config.auth)\n return undefined\n\n const fetchFn = transport?.encryptedFetch ?? config.fetch ?? globalThis.fetch.bind(globalThis)\n // 默认使用 httpOnly cookie 存储(推荐方案:refresh token 由服务端管理,防 XSS)。\n // SSR / Node.js 测试场景请显式传入 `apiClient.tokenStorage.memory()`。\n // 原生客户端显式传入平台安全存储,浏览器不持久化 Token 到 Web Storage。\n const storage = config.auth.storage ?? createHttpOnlyCookieTokenStorage()\n const baseUrl = config.baseUrl.replace(TRAILING_SLASHES_REGEX, '')\n const refreshPath = config.auth.refreshPath ?? DEFAULT_REFRESH_PATH\n const refreshUrl = `${baseUrl}${refreshPath.startsWith('/') ? refreshPath : `/${refreshPath}`}`\n\n // 刷新沿用客户端标识与自定义头(包括原生 body 通道标识),并复用同一加密 fetch。\n const refreshFetch: typeof fetch = async (input, init) => {\n const configured = typeof config.headers === 'function' ? await config.headers() : config.headers ?? {}\n const headers = new Headers(configured)\n if (!headers.has('accept-language'))\n headers.set('accept-language', core.i18n.getGlobalLocale())\n headers.set('x-request-client', config.clientName ?? DEFAULT_CLIENT_NAME)\n new Headers(init?.headers).forEach((value, name) => headers.set(name, value))\n return fetchFn(input, { ...init, headers })\n }\n const manager = createTokenManager(storage, refreshUrl, refreshFetch, config.auth.recoverSession ? undefined : config.auth.onRefreshFailed, config.auth.refreshTimeoutMs)\n if (config.auth.onTokenRefreshed) {\n manager.onTokenRefreshed(config.auth.onTokenRefreshed)\n }\n return manager\n}\n\n/**\n * 根据显式 transport 配置创建会话;首次 encryptedFetch 才进行实际密钥协商。\n * @param config - 客户端配置及已初始化 crypto。\n * @returns 新会话或 undefined;调用方在失败/close 时 destroy。\n *\n * @example 模块内部上下文\n * ```ts\n * const session = buildTransportSession({ baseUrl: 'https://api.example.com/api/v1' })\n * // 未配置 transport 时不产生会话;正常明文请求仍由实际 fetch 发送。\n * session?.destroy()\n * ```\n */\nfunction buildTransportSession(config: ApiClientConfig): TransportClient | undefined {\n if (!config.transport)\n return undefined\n const baseFetch = config.fetch ?? globalThis.fetch.bind(globalThis)\n const baseUrl = config.baseUrl.replace(TRAILING_SLASHES_REGEX, '')\n const path = config.transport.keyExchangePath ?? '/_hai/key-exchange'\n const keyExchangeUrl = `${baseUrl}${path.startsWith('/') ? path : `/${path}`}`\n return config.transport.crypto.transport.createClient({ keyExchangeUrl, fetch: baseFetch, maxPlaintextBytes: config.transport.maxPlaintextBytes })\n}\n\n/**\n * 以同一 contract 构造 OpenAPILink 与 JSON typed client,统一头组装、超时和认证重试。\n * @param contract - 实际 procedure 契约。\n * @param config - 目标地址与平台 fetch。\n * @param tokenManager - 可选 Token 管理器。\n * @param transport - 可选加密会话。\n * @returns oRPC JSON client;模块外层代理将调用异常映射为失败 HaiResult。\n *\n * @example 模块内部上下文\n * ```ts\n * const contract = apiContract.create({ iam: apiContract.iam })\n * const raw = buildRawClient(contract, { baseUrl: 'https://api.example.com/api/v1' }, undefined, undefined)\n * // 构造不发送请求;业务调用通过外层 ApiClient 代理。\n * ```\n */\nfunction buildRawClient<TContract extends AnyContractRouter>(\n contract: TContract,\n config: ApiClientConfig,\n tokenManager: TokenManager | undefined,\n transport: TransportClient | undefined,\n): JsonifiedClient<ContractRouterClient<TContract>> {\n const link = new OpenAPILink<Record<PropertyKey, unknown>>(contract, {\n url: config.baseUrl,\n headers: async (options) => {\n const headers = await buildHeaders(config, tokenManager, getObservation(options.context)?.requestId)\n // 单次操作凭据只附加到本次调用,不能放进跨并发请求共享的默认 header。\n if (typeof options.context.reauthentication === 'string')\n headers['x-hai-reauthentication'] = options.context.reauthentication\n return headers\n },\n fetch: async (request, init, options) => {\n const record = getObservation(options.context)\n if (record) {\n record.method = request.method\n const url = new URL(request.url)\n url.search = ''\n url.hash = ''\n record.url = core.sanitize.sanitizeUrl(url.toString())\n }\n const requestInit = options.context.keepalive === true ? { ...init, keepalive: true } : init\n const confirm = options.context.reauthorize\n const reauthorize = typeof confirm === 'function'\n ? async () => {\n const value: unknown = await confirm()\n return typeof value === 'string' || value === undefined ? value : null\n }\n : undefined\n const response = await fetchWithRefresh(request, requestInit, config, options.context.autoRefresh === false ? undefined : tokenManager, transport, record, reauthorize)\n if (record) {\n record.status = response.status\n const traceId = response.headers.get('x-trace-id') ?? response.headers.get('x-request-id')\n if (traceId && /^[\\w.:-]{1,128}$/.test(traceId))\n record.requestId = traceId\n }\n return response\n },\n })\n return createORPCClient<JsonifiedClient<ContractRouterClient<TContract>>>(link)\n}\n\n/** 内部 context 属性只由逻辑调用建立,不能从业务输入读取。 */\nfunction getObservation(context: Record<PropertyKey, unknown>): MutableRequestRecord | undefined {\n const record = context[REQUEST_OBSERVATION]\n return record && typeof record === 'object' && 'requestId' in record ? record as MutableRequestRecord : undefined\n}\n\n/**\n * 合并静态/动态配置头与客户端标识,读取可用 access token 后设置 Bearer 头。\n * @param config - 用户请求头与客户端标识。\n * @param tokenManager - 可选管理器;读取安全存储失败向上游传播。\n * @returns 单次请求的头快照,不改变调用方配置对象。\n *\n * @example 模块内部上下文\n * ```ts\n * const headers = await buildHeaders({ baseUrl: 'https://api.example.com/api/v1', clientName: 'orders-web', headers: () => ({ 'x-app-channel': 'web' }) }, undefined)\n * const channel = headers['x-app-channel']\n * ```\n */\nasync function buildHeaders(\n config: ApiClientConfig,\n tokenManager: TokenManager | undefined,\n requestId?: string,\n): Promise<Record<string, string>> {\n const configured = typeof config.headers === 'function' ? await config.headers() : config.headers ?? {}\n const headers = new Headers(configured)\n headers.set('x-hai-terminal', config.auth?.terminalType ?? 'web')\n if (!headers.has('accept-language'))\n headers.set('accept-language', core.i18n.getGlobalLocale())\n const accessToken = await tokenManager?.storage.getAccessToken()\n return {\n ...Object.fromEntries(headers),\n 'x-request-client': config.clientName ?? DEFAULT_CLIENT_NAME,\n 'x-request-id': requestId ?? core.context.get().requestId ?? core.id.uuid(),\n ...(accessToken ? { authorization: `Bearer ${accessToken}` } : {}),\n }\n}\n\n/**\n * 发送前克隆一次性请求体;HTTP 401 或有效失败包络的 401 才尝试刷新并最多重试一次。\n * 登录/注册/找回密码与 refresh 入口保留原始失败,不递归刷新;重试保留 body 并替换 Bearer 头。\n * @param request - 尚未消费的 Request。\n * @param init - oRPC 提供的重定向选项。\n * @param init.redirect - 可选的 fetch 重定向策略,原样传给首次请求及重试。\n * @param init.keepalive - 页面离开请求保持存活的浏览器提示,原样传递。\n * @param config - 请求与刷新配置。\n * @param tokenManager - 缺省时直接返回首次响应。\n * @param transport - 刷新与重试共用的可选加密会话。\n * @returns 最终 HTTP Response;不保证远端非幂等副作用没有发生。\n *\n * @example 模块内部上下文\n * ```ts\n * const config: ApiClientConfig = { baseUrl: 'https://api.example.com/api/v1', fetch: async () => Response.json({ success: true, data: {} }) }\n * const response = await fetchWithRefresh(new Request('https://api.example.com/api/v1/auth/me'), {}, config, undefined, undefined)\n * const status = response.status\n * ```\n */\nasync function fetchWithRefresh(\n request: Request,\n init: { redirect?: Request['redirect'], keepalive?: boolean },\n config: ApiClientConfig,\n tokenManager: TokenManager | undefined,\n transport: TransportClient | undefined,\n observation?: MutableRequestRecord,\n reauthorize?: () => Promise<string | null | undefined>,\n): Promise<Response> {\n // Fetch Request 的 body 是一次性可读流;为了支持 401 后重发 POST/PUT 等带 body 的请求,\n // 必须在首次发送前 `clone()` 一份。即使没有 401,clone 后的副本也只会被 GC 回收,无副作用。\n const retryRequest = tokenManager || reauthorize ? request.clone() : undefined\n const identityGeneration = tokenManager?.generation\n if (observation)\n observation.attempts++\n const response = await fetchWithTimeout(request, init, config, transport)\n if (!retryRequest)\n return response\n\n async function retryAuthorized(accessToken?: string): Promise<Response> {\n if (!retryRequest || retryRequest.signal.aborted)\n return response\n const generation = tokenManager?.generation\n const headers = new Headers(retryRequest.headers)\n if (headers.has('x-hai-reauthentication') || reauthorize) {\n if (!reauthorize)\n return response\n const ticket = await reauthorize()\n if (ticket === null || retryRequest.signal.aborted || tokenManager?.generation !== generation)\n return response\n headers.delete('x-hai-reauthentication')\n if (ticket)\n headers.set('x-hai-reauthentication', ticket)\n }\n const currentToken = accessToken ?? await tokenManager?.storage.getAccessToken()\n if (tokenManager?.generation !== generation)\n return response\n if (currentToken)\n headers.set('authorization', `Bearer ${currentToken}`)\n if (observation)\n observation.attempts++\n return fetchWithTimeout(new Request(retryRequest, { headers }), init, config, transport)\n }\n\n // 登录失败必须保留原始错误;refresh 本身也不能再次触发自动刷新。\n const baseUrl = config.baseUrl.replace(TRAILING_SLASHES_REGEX, '')\n const refreshPath = config.auth?.refreshPath ?? DEFAULT_REFRESH_PATH\n const excludedPaths = [...AUTH_ENTRY_PATHS, refreshPath]\n if (excludedPaths.some(path => new URL(`${baseUrl}${path.startsWith('/') ? path : `/${path}`}`).pathname === new URL(request.url).pathname))\n return response\n const failure: unknown = response.status === 401 || response.headers.get('content-type')?.includes('application/json')\n ? await response.clone().json().catch(() => undefined)\n : undefined\n 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 : undefined\n if (code === 'hai:iam:027' && reauthorize)\n return retryAuthorized()\n if (!tokenManager)\n return response\n if (response.status !== 401 && !await isUnauthorizedResult(response))\n return response\n if (failure && typeof failure === 'object' && 'error' in failure && failure.error && typeof failure.error === 'object' && 'code' in failure.error\n && typeof failure.error.code === 'string' && failure.error.code.startsWith('hai:iam:') && !IamSessionErrorCodeSchema.safeParse(failure.error.code).success) {\n return response\n }\n if (tokenManager.generation !== identityGeneration)\n return response\n\n const tokens = await tokenManager.refresh()\n if (tokenManager.generation !== identityGeneration)\n return response\n let accessToken = tokens?.accessToken\n // refresh网络/上游故障会保留凭据,不能误判为会话失效弹窗,更不能重放结果不明的写入。\n if (!accessToken && config.auth?.recoverSession && tokenManager.sessionInvalid) {\n if (await config.auth.recoverSession()) {\n const restoredGeneration = tokenManager.generation\n accessToken = await tokenManager.storage.getAccessToken() ?? undefined\n if (tokenManager.generation !== restoredGeneration)\n return response\n }\n }\n if (!accessToken)\n return response\n\n return retryAuthorized(accessToken)\n}\n\n/**\n * 只从 HTTP 200 JSON 的有效失败 HaiResult 识别 httpStatus=401;克隆响应避免消费原正文。\n * @param response - 原始 HTTP 响应。\n * @returns 是否满足认证失效包络;非法 JSON/字段缺失留给原解析路径处理。\n *\n * @example 模块内部上下文\n * ```ts\n * const response = Response.json({ success: false, error: { code: 'hai:iam:001', message: 'Session expired', httpStatus: 401 } })\n * const unauthorized = await isUnauthorizedResult(response)\n * const originalBody = await response.json()\n * // 克隆检查没有消耗原响应正文。\n * ```\n */\nasync function isUnauthorizedResult(response: Response): Promise<boolean> {\n if (response.status !== 200 || !response.headers.get('content-type')?.includes('application/json'))\n return false\n try {\n const body: unknown = await response.clone().json()\n if (!body || typeof body !== 'object' || !('success' in body) || body.success !== false\n || !('error' in body) || !body.error || typeof body.error !== 'object') {\n return false\n }\n return 'httpStatus' in body.error && body.error.httpStatus === 401\n && 'code' in body.error && typeof body.error.code === 'string'\n && 'message' in body.error && typeof body.error.message === 'string'\n }\n catch {\n // 非 JSON/非法包络交由既有客户端解析路径报告,不掩盖为认证失败。\n return false\n }\n}\n\n/**\n * 把请求超时和调用方 signal 连接到本次 fetch;finally 清除本次超时计时器。\n * @param request - 含调用方取消信号的 Request。\n * @param init - oRPC 重定向设置。\n * @param init.redirect - 可选的 fetch 重定向策略,原样交给本次实际 fetch。\n * @param init.keepalive - 页面离开请求保持存活的浏览器提示,原样传递。\n * @param config - 超时与 fetch 配置。\n * @param transport - 可选加密 fetch,优先于原始 fetch。\n * @returns HTTP 响应;超时/取消异常由外层 procedure 映射,不能视为远端回滚。\n *\n * @example 模块内部上下文\n * ```ts\n * const request = new Request('https://api.example.com/api/v1/auth/me')\n * const response = await fetchWithTimeout(request, {}, { baseUrl: 'https://api.example.com/api/v1', timeout: 1000, fetch: async () => new Response(null, { status: 204 }) }, undefined)\n * const status = response.status\n * ```\n */\nasync function fetchWithTimeout(\n request: Request,\n init: { redirect?: Request['redirect'], keepalive?: boolean },\n config: ApiClientConfig,\n transport: TransportClient | undefined,\n): Promise<Response> {\n // 启用传输加密时,用 encryptedFetch 作为实际 fetch;\n // 它会首次请求前自动完成密钥协商,后续请求复用会话。\n const fetchFn = transport?.encryptedFetch ?? config.fetch ?? globalThis.fetch.bind(globalThis)\n const timeout = config.timeout ?? DEFAULT_TIMEOUT\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), timeout)\n // 保留调用方原始 signal 的中断能力:上游主动 abort 也能控制超时 controller。\n const userSignal = request.signal\n if (userSignal) {\n if (userSignal.aborted) {\n controller.abort()\n }\n else {\n userSignal.addEventListener('abort', () => controller.abort(), { once: true })\n }\n }\n try {\n return await fetchFn(new Request(request, { signal: controller.signal }), init)\n }\n finally {\n clearTimeout(timeoutId)\n }\n}\n\n/**\n * 将 oRPC 状态、AbortError 与其它传输异常映射为稳定客户端错误;保留 oRPC code/status/data 详情。\n * @param error - procedure 抛出的未知异常。\n * @returns 失败 HaiResult,不包含虚构的成功数据。\n *\n * @example 模块内部上下文\n * ```ts\n * const result = mapClientError(new DOMException('Request aborted', 'AbortError'))\n * if (!result.success) { const code = result.error.code }\n * ```\n */\nfunction mapClientError(error: unknown) {\n const time = error instanceof TransportTimeError ? error : error instanceof Error && error.cause instanceof TransportTimeError ? error.cause : undefined\n if (time)\n return err(HaiApiClientError.VALIDATION_FAILED, time.message, { transportCode: time.code })\n const capacity = error instanceof TransportBodyTooLargeError ? error : error instanceof Error && error.cause instanceof TransportBodyTooLargeError ? error.cause : undefined\n if (capacity)\n return err(HaiApiClientError.PAYLOAD_TOO_LARGE, capacity.message, { direction: capacity.direction, stage: capacity.stage, limitBytes: capacity.limitBytes, actualBytes: capacity.actualBytes })\n if (error instanceof ORPCError) {\n // 保留 ORPCError 的原始 code / status / data,供上层区分具体业务异常(不仅是 HTTP 状态码)。\n const detail = { orpcCode: error.code, status: error.status, data: error.data }\n if (error.status === 413)\n return err(HaiApiClientError.PAYLOAD_TOO_LARGE, error.message, detail)\n if (error.status === 400 || error.status === 422)\n return err(HaiApiClientError.VALIDATION_FAILED, error.message, detail)\n if (error.status === 401)\n return err(HaiApiClientError.UNAUTHORIZED, error.message, detail)\n if (error.status === 403)\n return err(HaiApiClientError.FORBIDDEN, error.message, detail)\n if (error.status === 404)\n return err(HaiApiClientError.NOT_FOUND, error.message, detail)\n return err(HaiApiClientError.SERVER_ERROR, error.message, detail)\n }\n if (error instanceof DOMException && error.name === 'AbortError') {\n return err(HaiApiClientError.TIMEOUT, apiClientM('apiClient_timeout'), error)\n }\n return err(HaiApiClientError.NETWORK_ERROR, apiClientM('apiClient_networkError'), error)\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@h-ai/api-client",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.0-alpha.
|
|
4
|
+
"version": "0.1.0-alpha.75",
|
|
5
5
|
"description": "Hai Framework universal HTTP client with Bearer token and API contract support.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"exports": {
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
"node": "^22.23.4 || ^24.22.1 || ^26.12.1"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@h-ai/api-contract": "0.1.0-alpha.
|
|
27
|
-
"@h-ai/core": "0.1.0-alpha.
|
|
28
|
-
"@h-ai/crypto": "0.1.0-alpha.
|
|
26
|
+
"@h-ai/api-contract": "0.1.0-alpha.75",
|
|
27
|
+
"@h-ai/core": "0.1.0-alpha.75",
|
|
28
|
+
"@h-ai/crypto": "0.1.0-alpha.75",
|
|
29
29
|
"@orpc/client": "^1.15.5",
|
|
30
30
|
"@orpc/contract": "^1.15.5",
|
|
31
31
|
"@orpc/openapi-client": "^1.15.5",
|