@h-ai/api-client 0.1.0-alpha.56 → 0.1.0-alpha.58
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 +134 -0
- package/README.ja.md +134 -0
- package/README.md +6 -0
- package/dist/index.js +33 -2
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
package/README.en.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# @h-ai/api-client
|
|
2
|
+
|
|
3
|
+
[简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
|
|
4
|
+
|
|
5
|
+
[Framework home](../../README.en.md) · Related modules: [@h-ai/api-contract](../api-contract/README.en.md) · [@h-ai/core](../core/README.en.md) · [@h-ai/crypto](../crypto/README.en.md)
|
|
6
|
+
|
|
7
|
+
> A cross-platform oRPC/OpenAPI typed client for hai-framework, with consistent API calls and token management across Web, apps, and mini programs.
|
|
8
|
+
|
|
9
|
+
## Capabilities
|
|
10
|
+
|
|
11
|
+
- `apiClient`: the default typed client for the iam/storage/ai domains.
|
|
12
|
+
- `apiClient.create(contract)`: create a typed client for a custom contract.
|
|
13
|
+
- `apiClient.tokenStorage.{memory,localStorage,httpOnlyCookie}()`: token storage factories.
|
|
14
|
+
- Custom `fetch` support for browsers, Node, Capacitor, and mini-program bridges.
|
|
15
|
+
- Automatic Bearer token injection, with token refresh and one retry after a 401.
|
|
16
|
+
- Network failures are converted to `HaiResult` errors.
|
|
17
|
+
- Optional transport encryption: `apiClient.init({ transport: { crypto } })` automatically uses `crypto.transport.createClient()`.
|
|
18
|
+
- Transport encryption and automatic refresh can run together: `/auth/refresh` after a 401 still uses encrypted fetch without falling back to plaintext.
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { apiClient } from '@h-ai/api-client'
|
|
24
|
+
|
|
25
|
+
await apiClient.init({
|
|
26
|
+
baseUrl: 'https://api.example.com/api/v1',
|
|
27
|
+
auth: {}, // 默认使用 httpOnly cookie 存储(推荐);SSR 测试请显式传入 apiClient.tokenStorage.memory()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const login = await apiClient.iam.auth.login({
|
|
31
|
+
identifier: 'alice',
|
|
32
|
+
password: 'secret',
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
if (login.success) {
|
|
36
|
+
// httpOnly cookie 模式下 refresh token 由服务端 Set-Cookie 管理,
|
|
37
|
+
// api-client 默认存储只需要把 access token 写入内存。
|
|
38
|
+
await apiClient.auth.setTokens(login.data.tokens)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const me = await apiClient.iam.auth.currentUser()
|
|
42
|
+
if (me.success) {
|
|
43
|
+
// 身份资料与服务端重新校验后的访问范围一次返回,客户端无需持久化权限快照。
|
|
44
|
+
const { roles, permissions, ...user } = me.data
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await apiClient.close()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Enable transport encryption
|
|
51
|
+
|
|
52
|
+
First enable `serv.createApp({ transport: { crypto } })` on the server. The client only needs the same `@h-ai/crypto` service instance; no manual key exchange code is required.
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { apiClient } from '@h-ai/api-client'
|
|
56
|
+
import { crypto } from '@h-ai/crypto'
|
|
57
|
+
|
|
58
|
+
await crypto.init()
|
|
59
|
+
|
|
60
|
+
await apiClient.init({
|
|
61
|
+
baseUrl: 'https://api.example.com/api/v1',
|
|
62
|
+
auth: {},
|
|
63
|
+
transport: { crypto }, // 默认协商路径:/api/v1/_hai/key-exchange
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
await apiClient.close()
|
|
67
|
+
await crypto.close()
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## API contracts
|
|
71
|
+
|
|
72
|
+
Custom applications can bind their own oRPC contract:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { apiClient } from '@h-ai/api-client'
|
|
76
|
+
import { apiContract } from '@h-ai/api-contract'
|
|
77
|
+
|
|
78
|
+
const contract = apiContract.create({ iam: apiContract.iam })
|
|
79
|
+
const client = apiClient.create(contract)
|
|
80
|
+
|
|
81
|
+
await client.init({ baseUrl: 'https://api.example.com/api/v1' })
|
|
82
|
+
const result = await client.iam.auth.login({ identifier: 'alice', password: 'secret' })
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## API overview
|
|
86
|
+
|
|
87
|
+
- `apiClient.init(config)`: initialize the default client.
|
|
88
|
+
- `apiClient.close()`: clear client state.
|
|
89
|
+
- `apiClient.auth.setTokens(tokens)`: store the access token; storage other than httpOnly also stores the refresh token.
|
|
90
|
+
- `apiClient.auth.clear()`: clear tokens.
|
|
91
|
+
- `apiClient.create(contract)`: create a custom typed client.
|
|
92
|
+
|
|
93
|
+
Calling `init()` on an initialized client returns `CONFIG_ERROR` and leaves the existing client intact. To change `baseUrl` / `fetch` / `transport`, call `await apiClient.close()` before initializing again.
|
|
94
|
+
|
|
95
|
+
## Configuration
|
|
96
|
+
|
|
97
|
+
- `baseUrl`: API base URL, usually including `/api/v1`.
|
|
98
|
+
- `auth.storage`: token storage adapter; defaults to `apiClient.tokenStorage.httpOnlyCookie()` (recommended for browsers). Explicitly use `apiClient.tokenStorage.memory()` for SSR / tests.
|
|
99
|
+
- `auth.refreshPath`: token refresh path; defaults to `/auth/refresh`.
|
|
100
|
+
- `timeout`: request timeout; defaults to 30000 ms.
|
|
101
|
+
- `headers`: static or dynamic shared request headers.
|
|
102
|
+
- `fetch`: custom fetch implementation.
|
|
103
|
+
- `transport.crypto`: transparent request/response encryption and decryption; requires an initialized `@h-ai/crypto` instance.
|
|
104
|
+
- `transport.keyExchangePath`: key exchange subpath; defaults to `/_hai/key-exchange` and is appended to `baseUrl` automatically.
|
|
105
|
+
- `apiClient.config`: a **redacted snapshot of runtime configuration**; credentials embedded in `baseUrl`, `headers.authorization`, and other sensitive values are hidden automatically.
|
|
106
|
+
|
|
107
|
+
## Error handling
|
|
108
|
+
|
|
109
|
+
Business APIs return `HaiResult<T>`:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const result = await apiClient.iam.auth.currentUser()
|
|
113
|
+
if (!result.success) {
|
|
114
|
+
// result.error.code / result.error.message
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// result.data = User & { roles: string[], permissions: string[] }
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Client-side failures such as missing initialization, network errors, timeouts, and 401/403/404 responses are also converted to failed `HaiResult` values.
|
|
122
|
+
|
|
123
|
+
Automatic refresh handles an actual HTTP 401 (`Response.status`) or a valid failed HaiResult over HTTP 200 (`success=false`, string `error.code/message` and `error.httpStatus=401`). After a successful refresh, the original request is retried once. Built-in login and registration routes and the configured `auth.refreshPath` never trigger automatic refresh; other failures such as 403/500 do not trigger it either. Ordinary serv procedures still return HTTP 200. Callers must check the final `success` and `error.code` and handle expired authentication when refresh fails.
|
|
124
|
+
|
|
125
|
+
## Tests
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
pnpm --filter @h-ai/api-client test
|
|
129
|
+
pnpm --filter @h-ai/api-client typecheck
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
Apache-2.0
|
package/README.ja.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# @h-ai/api-client
|
|
2
|
+
|
|
3
|
+
[简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
|
|
4
|
+
|
|
5
|
+
[フレームワークのホーム](../../README.ja.md) · 関連モジュール:[@h-ai/api-contract](../api-contract/README.ja.md) · [@h-ai/core](../core/README.ja.md) · [@h-ai/crypto](../crypto/README.ja.md)
|
|
6
|
+
|
|
7
|
+
> hai-framework のクロスプラットフォーム oRPC/OpenAPI 型付きクライアント。Web、アプリ、ミニプログラムに共通の API 呼び出しと Token 管理を提供します。
|
|
8
|
+
|
|
9
|
+
## 機能概要
|
|
10
|
+
|
|
11
|
+
- `apiClient`:iam/storage/ai ドメインに対応する既定の型付きクライアント。
|
|
12
|
+
- `apiClient.create(contract)`:独自の contract に対応する型付きクライアントを作成。
|
|
13
|
+
- `apiClient.tokenStorage.{memory,localStorage,httpOnlyCookie}()`:Token ストレージのファクトリー。
|
|
14
|
+
- ブラウザー、Node、Capacitor、ミニプログラムのブリッジに対応する独自 `fetch` を指定可能。
|
|
15
|
+
- Bearer Token の自動付与と、401 応答後の更新および 1 回の再試行。
|
|
16
|
+
- ネットワークエラーを `HaiResult` のエラーに変換。
|
|
17
|
+
- 任意の通信暗号化:`apiClient.init({ transport: { crypto } })` が `crypto.transport.createClient()` を自動使用。
|
|
18
|
+
- 通信暗号化と自動更新を併用可能。401 後の `/auth/refresh` も暗号化された fetch を使用し、平文には切り替わりません。
|
|
19
|
+
|
|
20
|
+
## クイックスタート
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { apiClient } from '@h-ai/api-client'
|
|
24
|
+
|
|
25
|
+
await apiClient.init({
|
|
26
|
+
baseUrl: 'https://api.example.com/api/v1',
|
|
27
|
+
auth: {}, // 默认使用 httpOnly cookie 存储(推荐);SSR 测试请显式传入 apiClient.tokenStorage.memory()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const login = await apiClient.iam.auth.login({
|
|
31
|
+
identifier: 'alice',
|
|
32
|
+
password: 'secret',
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
if (login.success) {
|
|
36
|
+
// httpOnly cookie 模式下 refresh token 由服务端 Set-Cookie 管理,
|
|
37
|
+
// api-client 默认存储只需要把 access token 写入内存。
|
|
38
|
+
await apiClient.auth.setTokens(login.data.tokens)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const me = await apiClient.iam.auth.currentUser()
|
|
42
|
+
if (me.success) {
|
|
43
|
+
// 身份资料与服务端重新校验后的访问范围一次返回,客户端无需持久化权限快照。
|
|
44
|
+
const { roles, permissions, ...user } = me.data
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await apiClient.close()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 通信暗号化の有効化
|
|
51
|
+
|
|
52
|
+
サーバーで先に `serv.createApp({ transport: { crypto } })` を有効にします。クライアントには同じ `@h-ai/crypto` サービスのインスタンスを渡すだけで、鍵交換コードの実装は不要です。
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { apiClient } from '@h-ai/api-client'
|
|
56
|
+
import { crypto } from '@h-ai/crypto'
|
|
57
|
+
|
|
58
|
+
await crypto.init()
|
|
59
|
+
|
|
60
|
+
await apiClient.init({
|
|
61
|
+
baseUrl: 'https://api.example.com/api/v1',
|
|
62
|
+
auth: {},
|
|
63
|
+
transport: { crypto }, // 默认协商路径:/api/v1/_hai/key-exchange
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
await apiClient.close()
|
|
67
|
+
await crypto.close()
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## API 契約
|
|
71
|
+
|
|
72
|
+
独自のアプリケーションでは、専用の oRPC contract を指定できます:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { apiClient } from '@h-ai/api-client'
|
|
76
|
+
import { apiContract } from '@h-ai/api-contract'
|
|
77
|
+
|
|
78
|
+
const contract = apiContract.create({ iam: apiContract.iam })
|
|
79
|
+
const client = apiClient.create(contract)
|
|
80
|
+
|
|
81
|
+
await client.init({ baseUrl: 'https://api.example.com/api/v1' })
|
|
82
|
+
const result = await client.iam.auth.login({ identifier: 'alice', password: 'secret' })
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## API 一覧
|
|
86
|
+
|
|
87
|
+
- `apiClient.init(config)`:既定のクライアントを初期化。
|
|
88
|
+
- `apiClient.close()`:クライアントの状態を解放。
|
|
89
|
+
- `apiClient.auth.setTokens(tokens)`:access token を保存。httpOnly 以外のストレージでは refresh token も保存します。
|
|
90
|
+
- `apiClient.auth.clear()`:Token を削除。
|
|
91
|
+
- `apiClient.create(contract)`:独自の型付きクライアントを作成。
|
|
92
|
+
|
|
93
|
+
初期化済みのクライアントで `init()` を再度呼ぶと `CONFIG_ERROR` が返り、既存のクライアントは維持されます。`baseUrl` / `fetch` / `transport` を変更する場合は、先に `await apiClient.close()` を呼んでから再初期化してください。
|
|
94
|
+
|
|
95
|
+
## 設定
|
|
96
|
+
|
|
97
|
+
- `baseUrl`:API のベース URL。通常は `/api/v1` を含みます。
|
|
98
|
+
- `auth.storage`:Token ストレージアダプター。既定は `apiClient.tokenStorage.httpOnlyCookie()`(ブラウザー推奨)。SSR / テストでは `apiClient.tokenStorage.memory()` を明示してください。
|
|
99
|
+
- `auth.refreshPath`:Token 更新パス。既定は `/auth/refresh`。
|
|
100
|
+
- `timeout`:リクエストのタイムアウト。既定は 30000 ms。
|
|
101
|
+
- `headers`:静的または動的な共通リクエストヘッダー。
|
|
102
|
+
- `fetch`:独自の fetch 実装。
|
|
103
|
+
- `transport.crypto`:透過的なリクエスト/レスポンス暗号化・復号。初期化済みの `@h-ai/crypto` インスタンスが必要です。
|
|
104
|
+
- `transport.keyExchangePath`:鍵交換のサブパス。既定は `/_hai/key-exchange` で、`baseUrl` に自動的に連結されます。
|
|
105
|
+
- `apiClient.config`:**機密情報をマスクした実行時設定のスナップショット**。`baseUrl` 内の認証情報や `headers.authorization` などの機密値は自動的に非表示になります。
|
|
106
|
+
|
|
107
|
+
## エラー処理
|
|
108
|
+
|
|
109
|
+
業務 API は `HaiResult<T>` を返します:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const result = await apiClient.iam.auth.currentUser()
|
|
113
|
+
if (!result.success) {
|
|
114
|
+
// result.error.code / result.error.message
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// result.data = User & { roles: string[], permissions: string[] }
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
未初期化、ネットワークエラー、タイムアウト、401/403/404 などのクライアント側の問題も、失敗の `HaiResult` に変換されます。
|
|
122
|
+
|
|
123
|
+
自動更新は実際の HTTP 401(`Response.status`)、または HTTP 200 の有効な失敗 HaiResult(`success=false`、文字列の `error.code/message`、`error.httpStatus=401`)で動作します。更新が成功すると元のリクエストを一度だけ再試行します。組み込みのログイン・登録と設定された `auth.refreshPath` は自動更新の対象外で、403/500 など他の失敗でも更新しません。serv の通常の procedure は引き続き HTTP 200 を返します。呼び出し側は最終結果の `success` と `error.code` を確認し、更新失敗による認証失効を処理してください。
|
|
124
|
+
|
|
125
|
+
## テスト
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
pnpm --filter @h-ai/api-client test
|
|
129
|
+
pnpm --filter @h-ai/api-client typecheck
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
Apache-2.0
|
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# @h-ai/api-client
|
|
2
2
|
|
|
3
|
+
[简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
|
|
4
|
+
|
|
5
|
+
[框架首页](../../README.md) · 相关模块:[@h-ai/api-contract](../api-contract/README.md) · [@h-ai/core](../core/README.md) · [@h-ai/crypto](../crypto/README.md)
|
|
6
|
+
|
|
3
7
|
> hai-framework 的跨端 oRPC/OpenAPI typed client,面向 Web、App、小程序等运行时提供统一 API 调用与 Token 管理。
|
|
4
8
|
|
|
5
9
|
## 能力概览
|
|
@@ -116,6 +120,8 @@ else {
|
|
|
116
120
|
|
|
117
121
|
未初始化、网络错误、超时、401/403/404 等客户端侧问题也会转换为失败的 `HaiResult`。
|
|
118
122
|
|
|
123
|
+
自动刷新在真实 HTTP 401(`Response.status`)或 HTTP 200 的有效失败 HaiResult(`success=false`、`error.code/message` 为字符串且 `error.httpStatus=401`)后触发,刷新成功后只重试一次原请求。内置登录、注册与配置的 `auth.refreshPath` 不触发自动刷新,403/500 等其它失败也不会触发。serv 普通 procedure 的 HTTP 状态仍为 200;调用方须检查最终结果的 `success` 与 `error.code`,并处理刷新失败后的登录失效。
|
|
124
|
+
|
|
119
125
|
## 测试
|
|
120
126
|
|
|
121
127
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -243,6 +243,10 @@ var HaiApiClientError = core.error.buildHaiErrorsDef("api-client", ApiClientErro
|
|
|
243
243
|
var DEFAULT_TIMEOUT = 3e4;
|
|
244
244
|
var DEFAULT_CLIENT_NAME = "hai-api-client";
|
|
245
245
|
var DEFAULT_REFRESH_PATH = apiContract.pathOf(apiContract.iam.auth.refresh);
|
|
246
|
+
var AUTH_ENTRY_PATHS = [
|
|
247
|
+
apiContract.pathOf(apiContract.iam.auth.login),
|
|
248
|
+
apiContract.pathOf(apiContract.iam.auth.register)
|
|
249
|
+
];
|
|
246
250
|
var TRAILING_SLASHES_REGEX = /\/+$/;
|
|
247
251
|
var tokenStorage = {
|
|
248
252
|
/** 内存存储——SSR / Node 测试或一次性会话使用。 */
|
|
@@ -392,7 +396,14 @@ function buildTokenManager(config, transport) {
|
|
|
392
396
|
const baseUrl = config.baseUrl.replace(TRAILING_SLASHES_REGEX, "");
|
|
393
397
|
const refreshPath = config.auth.refreshPath ?? DEFAULT_REFRESH_PATH;
|
|
394
398
|
const refreshUrl = `${baseUrl}${refreshPath.startsWith("/") ? refreshPath : `/${refreshPath}`}`;
|
|
395
|
-
const
|
|
399
|
+
const refreshFetch = async (input, init) => {
|
|
400
|
+
const configured = typeof config.headers === "function" ? await config.headers() : config.headers ?? {};
|
|
401
|
+
const headers = new Headers(configured);
|
|
402
|
+
headers.set("x-request-client", config.clientName ?? DEFAULT_CLIENT_NAME);
|
|
403
|
+
new Headers(init?.headers).forEach((value, name) => headers.set(name, value));
|
|
404
|
+
return fetchFn(input, { ...init, headers });
|
|
405
|
+
};
|
|
406
|
+
const manager = createTokenManager(storage, refreshUrl, refreshFetch, config.auth.onRefreshFailed, config.auth.refreshTimeoutMs);
|
|
396
407
|
if (config.auth.onTokenRefreshed) {
|
|
397
408
|
manager.onTokenRefreshed(config.auth.onTokenRefreshed);
|
|
398
409
|
}
|
|
@@ -427,7 +438,14 @@ async function buildHeaders(config, tokenManager) {
|
|
|
427
438
|
async function fetchWithRefresh(request, init, config, tokenManager, transport) {
|
|
428
439
|
const retryRequest = tokenManager ? request.clone() : void 0;
|
|
429
440
|
const response = await fetchWithTimeout(request, init, config, transport);
|
|
430
|
-
if (
|
|
441
|
+
if (!tokenManager || !retryRequest)
|
|
442
|
+
return response;
|
|
443
|
+
const baseUrl = config.baseUrl.replace(TRAILING_SLASHES_REGEX, "");
|
|
444
|
+
const refreshPath = config.auth?.refreshPath ?? DEFAULT_REFRESH_PATH;
|
|
445
|
+
const excludedPaths = [...AUTH_ENTRY_PATHS, refreshPath];
|
|
446
|
+
if (excludedPaths.some((path) => new URL(`${baseUrl}${path.startsWith("/") ? path : `/${path}`}`).pathname === new URL(request.url).pathname))
|
|
447
|
+
return response;
|
|
448
|
+
if (response.status !== 401 && !await isUnauthorizedResult(response))
|
|
431
449
|
return response;
|
|
432
450
|
const tokens = await tokenManager.refresh();
|
|
433
451
|
if (!tokens)
|
|
@@ -436,6 +454,19 @@ async function fetchWithRefresh(request, init, config, tokenManager, transport)
|
|
|
436
454
|
headers.set("authorization", `Bearer ${tokens.accessToken}`);
|
|
437
455
|
return fetchWithTimeout(new Request(retryRequest, { headers }), init, config, transport);
|
|
438
456
|
}
|
|
457
|
+
async function isUnauthorizedResult(response) {
|
|
458
|
+
if (response.status !== 200 || !response.headers.get("content-type")?.includes("application/json"))
|
|
459
|
+
return false;
|
|
460
|
+
try {
|
|
461
|
+
const body = await response.clone().json();
|
|
462
|
+
if (!body || typeof body !== "object" || !("success" in body) || body.success !== false || !("error" in body) || !body.error || typeof body.error !== "object") {
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
return "httpStatus" in body.error && body.error.httpStatus === 401 && "code" in body.error && typeof body.error.code === "string" && "message" in body.error && typeof body.error.message === "string";
|
|
466
|
+
} catch {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
439
470
|
async function fetchWithTimeout(request, init, config, transport) {
|
|
440
471
|
const fetchFn = transport?.encryptedFetch ?? config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
441
472
|
const timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/api-client-auth.ts","../messages/en-US.json","../messages/zh-CN.json","../src/api-client-i18n.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 存储适配器(localStorage / 内存)和一个 Token 管理器\n * (封装存储读写、自动刷新、并发去重、回调通知)。\n *\n * @example 浏览器 SPA\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n *\n * await apiClient.init({\n * baseUrl: 'https://api.example.com/api/v1',\n * auth: { storage: apiClient.tokenStorage.localStorage() },\n * })\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('abc')\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// ─── localStorage 存储 ────────────────────────────────────────────────────────\n\nconst LS_ACCESS_KEY = 'hai_access_token'\nconst LS_REFRESH_KEY = 'hai_refresh_token'\n\n/**\n * 创建基于 `localStorage` 的 Token 存储(浏览器 SPA / PWA)。\n *\n * ⚠️ 安全提示(XSS 风险):\n * - `localStorage` 对同源脚本可读,任意 XSS 漏洞都可以偷取 Token。\n * - 生产环境应优先使用 httpOnly cookie + SameSite 或 BFF 主机代理。\n * - 本适配器仅适用于内部工具、Demo、或者已接受 XSS 风险的浏览器场景,\n * 并且在调用方明确选择使用。`apiClient.create` 默认不会使用本适配器,\n * 需调用方显式传入 `auth.storage = apiClient.tokenStorage.localStorage()`。\n *\n * @example\n * ```ts\n * await apiClient.init({\n * baseUrl: 'https://api.example.com/api/v1',\n * auth: { storage: apiClient.tokenStorage.localStorage() },\n * })\n * ```\n */\nexport function createLocalStorageTokenStorage(): TokenStorage {\n // 首次实例化时警告一次,提示调用方可能需要重新评估安全模型。\n logger.warn('apiClient.tokenStorage.localStorage: storing tokens in localStorage exposes them to XSS. Prefer httpOnly cookies for production.')\n return {\n async getAccessToken() {\n return globalThis.localStorage?.getItem(LS_ACCESS_KEY) ?? null\n },\n async setAccessToken(token) {\n globalThis.localStorage?.setItem(LS_ACCESS_KEY, token)\n },\n async getRefreshToken() {\n return globalThis.localStorage?.getItem(LS_REFRESH_KEY) ?? null\n },\n async setRefreshToken(token) {\n globalThis.localStorage?.setItem(LS_REFRESH_KEY, token)\n },\n async clear() {\n globalThis.localStorage?.removeItem(LS_ACCESS_KEY)\n globalThis.localStorage?.removeItem(LS_REFRESH_KEY)\n },\n }\n}\n\n// ─── 内存存储 ─────────────────────────────────────────────────────────────────\n\n/**\n * 创建内存 Token 存储(Node.js 测试 / SSR / 短生命周期场景)。\n * 页面刷新后丢失。\n *\n * @example\n * ```ts\n * const storage = apiClient.tokenStorage.memory()\n * await storage.setAccessToken('abc')\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 * 创建基于 httpOnly cookie 的 Token 存储(生产环境推荐方案)。\n *\n * **安全模型:**\n * - `accessToken` 保存在**内存**中:JS 可读写,用于注入 `Authorization: Bearer`;\n * 页面刷新后丢失,由 refresh 流程自动补充。\n * - `refreshToken` 保存在服务端设置的 **httpOnly cookie** 中:\n * - JS 不可读写,XSS 脚本无法直接盗取。\n * - 结合 `SameSite=Strict` 可阻止 CSRF 攻击。\n * - 浏览器在每次请求时自动携带,无需客户端显式传递。\n *\n * **服务端配套要求:**\n * 1. 登录端点在响应中设置:\n * ```\n * Set-Cookie: refresh_token=<token>; HttpOnly; SameSite=Strict; Secure;\n * Path=/api/v1/auth/refresh; Max-Age=<seconds>\n * ```\n * `Path` 限定为 refresh 端点,减少 Cookie 暴露面。\n * 2. Refresh 端点从 Cookie 读取 refresh token(忽略请求体中的 `refreshToken` 字段)。\n * 3. Logout 端点清除 Cookie:\n * ```\n * Set-Cookie: refresh_token=; HttpOnly; SameSite=Strict; Secure;\n * Path=/api/v1/auth/refresh; Max-Age=0\n * ```\n *\n * **跨域注意事项:**\n * 若 API 与前端不同源,服务端还需设置:\n * `Access-Control-Allow-Origin: <前端域名>` 和 `Access-Control-Allow-Credentials: true`。\n * 调用方不需要做额外配置,`apiClient.create` 会自动识别并设置 `credentials: 'include'`。\n *\n * @example\n * ```ts\n * await apiClient.init({\n * baseUrl: 'https://api.example.com/api/v1',\n * auth: { storage: apiClient.tokenStorage.httpOnlyCookie() },\n * })\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\ntype RefreshCallback = (tokens: TokenPair) => void\n\n/**\n * 创建 Token 管理器:封装存储读写、自动刷新、并发去重与回调通知。\n *\n * 主要供 `apiClient.create` 内部使用,同时作为 named export 暴露给测试与高级用户。\n *\n * 行为约定:\n * - 并发多次 `refresh()` 共享同一个请求。\n * - refresh 接口失败、响应字段缺失或 401,都视为刷新失败:清空存储并触发 `onRefreshFailed`。\n * - 兼容两种响应格式:`{ data: tokens }` 与 `{ data: { tokens } }`(HaiResult 包装)。\n *\n * @example\n * ```ts\n * const manager = createTokenManager(\n * apiClient.tokenStorage.memory(),\n * 'https://api.example.com/api/v1/auth/refresh',\n * fetch,\n * () => notifyLoginRequired(),\n * )\n *\n * const off = manager.onTokenRefreshed(tokens => syncTokens(tokens))\n * const tokens = await manager.refresh()\n * off()\n * ```\n *\n * @param storage - Token 存储适配器\n * @param refreshEndpointUrl - refresh 接口完整 URL(`baseUrl + refreshPath`)\n * @param fetchFn - fetch 实现(允许注入 mock)\n * @param onRefreshFailed - 刷新失败回调\n * @param refreshTimeoutMs - 刷新请求超时(ms),默认 10_000\n * @returns Token 管理器实例\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\n async function refresh(): Promise<TokenPair | null> {\n // 并发去重:同一时刻只发一次 refresh,其它调用复用同一个 Promise\n if (refreshPromise)\n return refreshPromise\n refreshPromise = doRefresh()\n try {\n return await refreshPromise\n }\n finally {\n refreshPromise = null\n }\n }\n\n async function doRefresh(): Promise<TokenPair | null> {\n const refreshToken = await storage.getRefreshToken()\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 controller = new AbortController()\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\n if (!response.ok) {\n // 仅在服务端明确返回 401/4xx 时才清除 Token(refresh token 已失效);\n // 5xx 作为服务端临时故障,保留 Token 让下一次请求重试。\n if (response.status >= 400 && response.status < 500) {\n await storage.clear()\n onRefreshFailed?.()\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 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 storage.clear()\n onRefreshFailed?.()\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 storage.clear()\n onRefreshFailed?.()\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 await storage.setAccessToken(tokens.accessToken)\n await storage.setRefreshToken(refreshTokenForStorage)\n logger.info('Token refreshed successfully')\n for (const cb of callbacks) cb(tokens)\n return tokens\n }\n catch (error) {\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 await storage.setAccessToken(tokens.accessToken)\n if (tokens.refreshToken)\n await storage.setRefreshToken(tokens.refreshToken)\n },\n async clear() {\n await storage.clear()\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/** Token 管理器类型(`createTokenManager` 返回值)。 */\nexport type TokenManager = ReturnType<typeof createTokenManager>\n\n/** 兼容 `{ data: tokens }` 与 `{ data: { tokens } }` 两种 refresh 响应体。 */\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\": \"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 messagesZhCN from '../messages/zh-CN.json'\n\ntype ApiClientMessageKey = keyof typeof messagesZhCN\n\n/**\n * api-client 模块 i18n 文案获取器\n *\n * @param key - 文案 Key\n * @param options - 插值参数\n * @returns 本地化后的文案字符串\n */\nexport const apiClientM = core.i18n.createMessageGetter<ApiClientMessageKey>({\n 'zh-CN': messagesZhCN,\n 'en-US': messagesEnUS,\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 NOT_INITIALIZED: '010:500',\n CONFIG_ERROR: '011:500',\n UNKNOWN: '099:500',\n} as const satisfies ErrorInfo\n\n/**\n * API client 标准错误集合(遵循 `@h-ai/core` 错误码规范)。\n *\n * @example\n * ```ts\n * if (!result.success && result.error.code === HaiApiClientError.UNAUTHORIZED.code) {\n * redirectToLogin()\n * }\n * ```\n */\nexport const HaiApiClientError = core.error.buildHaiErrorsDef('api-client', ApiClientErrorInfo)\n\n// ─── Token ────────────────────────────────────────────────────────────────────\n\n/** Token 对(与 `@h-ai/iam` 的 TokenPair 对齐)。 */\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 * Token 存储适配器。\n *\n * 内置实现:\n * - `apiClient.tokenStorage.httpOnlyCookie()` — **默认**;浏览器端推荐,refresh token 由服务端管理,防 XSS。\n * - `apiClient.tokenStorage.memory()` — Node.js 测试 / SSR(需显式传入)。\n * - `apiClient.tokenStorage.localStorage()` — 浏览器 SPA / PWA(有 XSS 风险,生产不推荐)。\n *\n * Capacitor / 小程序场景请由对应模块提供(例如 `@h-ai/capacitor`)。\n */\nexport interface TokenStorage {\n readonly getAccessToken: () => Promise<string | null>\n readonly setAccessToken: (token: string) => Promise<void>\n readonly getRefreshToken: () => Promise<string | null>\n readonly setRefreshToken: (token: string) => Promise<void>\n readonly clear: () => Promise<void>\n}\n\n// ─── 配置 ─────────────────────────────────────────────────────────────────────\n\n/** Token 自动刷新配置。 */\nexport interface AuthConfig {\n /**\n * Token 存储适配器;默认 `apiClient.tokenStorage.httpOnlyCookie()`(refresh token 由服务端管理,防 XSS)。\n *\n * - SSR / Node.js 测试场景请显式传入 `apiClient.tokenStorage.memory()`。\n * - 浏览器 SPA 如确需持久化,可显式传 `apiClient.tokenStorage.localStorage()`,\n * 但请先评估 XSS 风险。\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/** API client 初始化配置。 */\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 * await crypto.init()\n * await apiClient.init({\n * baseUrl: 'https://api.example.com/api/v1',\n * transport: { crypto },\n * })\n * ```\n */\n readonly transport?: ApiClientTransportConfig\n}\n\n// ─── 传输加密 ────────────────────────────────────────────────────────────────\n\n/** {@link ApiClientConfig.transport} 的配置项。 */\nexport interface ApiClientTransportConfig {\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/** Token 辅助操作。 */\nexport interface ApiClientAuth {\n /** 手动写入 Token(例如登录成功后)。 */\n readonly setTokens: (tokens: TokenPair) => Promise<void>\n /** 清空存储的 Token(登出)。 */\n readonly clear: () => Promise<void>\n /** 订阅 Token 刷新成功事件,返回取消订阅函数。 */\n readonly onTokenRefreshed: (callback: (tokens: TokenPair) => void) => () => void\n}\n\n/** API client 生命周期能力。 */\nexport interface ApiClientLifecycle {\n /**\n * 初始化 client;必须在调用任何 procedure 前完成。\n *\n * @example\n * ```ts\n * await client.init({\n * baseUrl: 'https://api.example.com/api/v1',\n * auth: { refreshPath: '/auth/refresh' },\n * timeout: 15_000,\n * })\n * ```\n */\n readonly init: (config: ApiClientConfig) => Promise<HaiResult<void>>\n /** 释放资源并允许重新 `init`。 */\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/** 根据 oRPC contract 推导出的 typed API client(生命周期 + procedure 调用)。 */\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 * await apiClient.init({ baseUrl: 'https://api.example.com/api/v1' })\n * const me = await apiClient.iam.auth.currentUser()\n * 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 * await client.init({ baseUrl: 'https://api.example.com/api/v1' })\n * const result = await client.iam.auth.login({ identifier: 'alice', password: 'secret' })\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} from './api-client-types.js'\nimport { apiContract } from '@h-ai/api-contract'\nimport { core, err, ok } from '@h-ai/core'\nimport { createORPCClient, ORPCError } from '@orpc/client'\nimport { OpenAPILink } from '@orpc/openapi-client/fetch'\nimport {\n createHttpOnlyCookieTokenStorage,\n createLocalStorageTokenStorage,\n createMemoryTokenStorage,\n createTokenManager,\n} from './api-client-auth.js'\nimport { apiClientM } from './api-client-i18n.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 TRAILING_SLASHES_REGEX = /\\/+$/\n\n/** Token 存储方案集合:内存 / 浏览器 localStorage / httpOnly Cookie。 */\nconst tokenStorage = {\n /** 内存存储——SSR / Node 测试或一次性会话使用。 */\n memory: createMemoryTokenStorage,\n /** 浏览器 `localStorage` 存储——单页应用便捷选项;注意 XSS 风险。 */\n localStorage: createLocalStorageTokenStorage,\n /** httpOnly Cookie 存储——浏览器场景推荐方案。 */\n httpOnlyCookie: createHttpOnlyCookieTokenStorage,\n} as const\n\n// ─── 工厂 ─────────────────────────────────────────────────────────────────────\n\n/**\n * 根据 oRPC contract 创建 typed API client。\n *\n * 返回对象同时具备:\n * - **生命周期能力**:`init` / `close` / `config` / `isInitialized` / `auth`\n * - **contract procedure**:按 contract 嵌套结构调用,例如 `client.iam.auth.login(input)`\n *\n * @example 基础用法\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 * await client.init({ baseUrl: 'https://api.example.com/api/v1' })\n *\n * const result = await client.iam.auth.login({ identifier: 'alice', password: 'secret' })\n * if (result.success) {\n * await client.auth.setTokens(result.data.tokens)\n * }\n *\n * await client.close()\n * ```\n *\n * @example 自定义 fetch / 超时\n * ```ts\n * const client = apiClient.create(contract)\n * await client.init({\n * baseUrl: 'https://api.example.com/api/v1',\n * fetch: customFetch,\n * timeout: 10_000,\n * })\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 } = {\n config: null,\n rawClient: null,\n tokenManager: undefined,\n transport: undefined,\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 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 }\n\n /**\n * 顶层 Proxy:把“一个对象同时扮演两种角色”的需求揉到一起。\n *\n * 这个 client 既要像一个普通服务对象,暴露生命周期方法:\n * - `client.init()`\n * - `client.close()`\n * - `client.config`\n * - `client.isInitialized`\n * - `client.auth`\n *\n * 又要像一个按 contract 动态展开的 oRPC client,支持:\n * - `client.iam.auth.login(input)`\n * - `client.storage.object.put(input)`\n * - `client.ai.chat.send(input)`\n *\n * 由于第二类 API 的层级结构是“运行时按属性链逐步访问”的,无法靠写死对象字面量覆盖所有路径,\n * 所以这里使用 Proxy:\n * - 先拦截顶层属性访问;\n * - 如果访问的是生命周期字段,就直接返回 lifecycle 中的真实成员;\n * - 否则把这个属性当成 contract 路径的第一段,继续交给 `createProcedureProxy()` 处理。\n *\n * 换句话说:\n * - `client.init` → 返回 lifecycle.init\n * - `client.auth` → 返回 lifecycle.auth\n * - `client.iam` → 返回一个新的嵌套 Proxy,继续等待 `.auth.login(...)`\n *\n * 注意:如果将来 contract 顶层字段和 lifecycle 字段重名,这里会“优先返回 lifecycle”。\n */\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 * await apiClient.init({\n * baseUrl: 'http://localhost:3000/api/v1',\n * auth: { refreshPath: '/auth/refresh' },\n * })\n * const result = await apiClient.iam.auth.login({ identifier: 'alice', password: 'secret' })\n * await apiClient.close()\n * ```\n */\nconst defaultApiContract = apiContract.create({ iam: apiContract.iam, storage: apiContract.storage, ai: apiContract.ai })\n\n/** `apiClient` 默认单例 + 工厂命名空间的统一对外类型。 */\nexport type DefaultApiClient = ApiClient<typeof defaultApiContract> & {\n readonly create: typeof createApiClient\n readonly tokenStorage: typeof tokenStorage\n}\n\n/** hai-framework API client 统一入口(默认单例 + `create` + `tokenStorage`)。 */\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 return Reflect.get(target as object, property, receiver)\n },\n}) as DefaultApiClient\n\n// ─── 内部:procedure 代理 ─────────────────────────────────────────────────────\n\n/**\n * 构造嵌套 procedure 代理。\n *\n * oRPC client 接口是任意层嵌套的属性 + 末端调用(如 `client.iam.auth.login(input)`)。\n * 这里用统一的 Proxy 透传所有路径,叶子节点(被 apply 时)映射到\n * `state.rawClient[a][b][c](input)` 并把错误标准化为 `HaiResult`。\n */\nfunction createProcedureProxy<TContract extends AnyContractRouter>(\n state: { rawClient: JsonifiedClient<ContractRouterClient<TContract>> | null },\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 callProcedure(state.rawClient, path, args)\n },\n })\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/** 根据 `auth` 配置决定是否创建 TokenManager。 */\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 // 浏览器 localStorage 场景请显式传入 `apiClient.tokenStorage.localStorage()`(有 XSS 风险)。\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 const manager = createTokenManager(storage, refreshUrl, fetchFn, config.auth.onRefreshFailed, config.auth.refreshTimeoutMs)\n if (config.auth.onTokenRefreshed) {\n manager.onTokenRefreshed(config.auth.onTokenRefreshed)\n }\n return manager\n}\n\n/** 根据 `transport` 配置创建传输加密会话。 */\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 })\n}\n\n/** 构造 oRPC OpenAPILink + raw client。 */\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(contract, {\n url: config.baseUrl,\n headers: async () => buildHeaders(config, tokenManager),\n fetch: async (request, init) => fetchWithRefresh(request, init, config, tokenManager, transport),\n })\n return createORPCClient<JsonifiedClient<ContractRouterClient<TContract>>>(link)\n}\n\n/** 组装请求头:用户配置 + 客户端标识 + Bearer Token。 */\nasync function buildHeaders(\n config: ApiClientConfig,\n tokenManager: TokenManager | undefined,\n): Promise<Record<string, string>> {\n const configured = typeof config.headers === 'function' ? await config.headers() : config.headers ?? {}\n const accessToken = await tokenManager?.storage.getAccessToken()\n return {\n ...configured,\n 'x-request-client': config.clientName ?? DEFAULT_CLIENT_NAME,\n ...(accessToken ? { authorization: `Bearer ${accessToken}` } : {}),\n }\n}\n\n/** 401 自动 refresh 并重试一次。 */\nasync function fetchWithRefresh(\n request: Request,\n init: { redirect?: Request['redirect'] },\n config: ApiClientConfig,\n tokenManager: TokenManager | undefined,\n transport: TransportClient | undefined,\n): Promise<Response> {\n // Fetch Request 的 body 是一次性可读流;为了支持 401 后重发 POST/PUT 等带 body 的请求,\n // 必须在首次发送前 `clone()` 一份。即使没有 401,clone 后的副本也只会被 GC 回收,无副作用。\n const retryRequest = tokenManager ? request.clone() : undefined\n const response = await fetchWithTimeout(request, init, config, transport)\n if (response.status !== 401 || !tokenManager || !retryRequest)\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 return fetchWithTimeout(new Request(retryRequest, { headers }), init, config, transport)\n}\n\n/** fetch + AbortController 超时控制(联动调用方原始 signal)。 */\nasync function fetchWithTimeout(\n request: Request,\n init: { redirect?: Request['redirect'] },\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/** ORPCError / AbortError / 其他异常 → 标准化 HaiResult 错误。 */\nfunction mapClientError(error: unknown) {\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 === 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/zh-CN.json","../src/api-client-i18n.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 存储适配器(localStorage / 内存)和一个 Token 管理器\n * (封装存储读写、自动刷新、并发去重、回调通知)。\n *\n * @example 浏览器 SPA\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n *\n * await apiClient.init({\n * baseUrl: 'https://api.example.com/api/v1',\n * auth: { storage: apiClient.tokenStorage.localStorage() },\n * })\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('abc')\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// ─── localStorage 存储 ────────────────────────────────────────────────────────\n\nconst LS_ACCESS_KEY = 'hai_access_token'\nconst LS_REFRESH_KEY = 'hai_refresh_token'\n\n/**\n * 创建基于 `localStorage` 的 Token 存储(浏览器 SPA / PWA)。\n *\n * ⚠️ 安全提示(XSS 风险):\n * - `localStorage` 对同源脚本可读,任意 XSS 漏洞都可以偷取 Token。\n * - 生产环境应优先使用 httpOnly cookie + SameSite 或 BFF 主机代理。\n * - 本适配器仅适用于内部工具、Demo、或者已接受 XSS 风险的浏览器场景,\n * 并且在调用方明确选择使用。`apiClient.create` 默认不会使用本适配器,\n * 需调用方显式传入 `auth.storage = apiClient.tokenStorage.localStorage()`。\n *\n * @example\n * ```ts\n * await apiClient.init({\n * baseUrl: 'https://api.example.com/api/v1',\n * auth: { storage: apiClient.tokenStorage.localStorage() },\n * })\n * ```\n */\nexport function createLocalStorageTokenStorage(): TokenStorage {\n // 首次实例化时警告一次,提示调用方可能需要重新评估安全模型。\n logger.warn('apiClient.tokenStorage.localStorage: storing tokens in localStorage exposes them to XSS. Prefer httpOnly cookies for production.')\n return {\n async getAccessToken() {\n return globalThis.localStorage?.getItem(LS_ACCESS_KEY) ?? null\n },\n async setAccessToken(token) {\n globalThis.localStorage?.setItem(LS_ACCESS_KEY, token)\n },\n async getRefreshToken() {\n return globalThis.localStorage?.getItem(LS_REFRESH_KEY) ?? null\n },\n async setRefreshToken(token) {\n globalThis.localStorage?.setItem(LS_REFRESH_KEY, token)\n },\n async clear() {\n globalThis.localStorage?.removeItem(LS_ACCESS_KEY)\n globalThis.localStorage?.removeItem(LS_REFRESH_KEY)\n },\n }\n}\n\n// ─── 内存存储 ─────────────────────────────────────────────────────────────────\n\n/**\n * 创建内存 Token 存储(Node.js 测试 / SSR / 短生命周期场景)。\n * 页面刷新后丢失。\n *\n * @example\n * ```ts\n * const storage = apiClient.tokenStorage.memory()\n * await storage.setAccessToken('abc')\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 * 创建基于 httpOnly cookie 的 Token 存储(生产环境推荐方案)。\n *\n * **安全模型:**\n * - `accessToken` 保存在**内存**中:JS 可读写,用于注入 `Authorization: Bearer`;\n * 页面刷新后丢失,由 refresh 流程自动补充。\n * - `refreshToken` 保存在服务端设置的 **httpOnly cookie** 中:\n * - JS 不可读写,XSS 脚本无法直接盗取。\n * - 结合 `SameSite=Strict` 可阻止 CSRF 攻击。\n * - 浏览器在每次请求时自动携带,无需客户端显式传递。\n *\n * **服务端配套要求:**\n * 1. 登录端点在响应中设置:\n * ```\n * Set-Cookie: refresh_token=<token>; HttpOnly; SameSite=Strict; Secure;\n * Path=/api/v1/auth/refresh; Max-Age=<seconds>\n * ```\n * `Path` 限定为 refresh 端点,减少 Cookie 暴露面。\n * 2. Refresh 端点从 Cookie 读取 refresh token(忽略请求体中的 `refreshToken` 字段)。\n * 3. Logout 端点清除 Cookie:\n * ```\n * Set-Cookie: refresh_token=; HttpOnly; SameSite=Strict; Secure;\n * Path=/api/v1/auth/refresh; Max-Age=0\n * ```\n *\n * **跨域注意事项:**\n * 若 API 与前端不同源,服务端还需设置:\n * `Access-Control-Allow-Origin: <前端域名>` 和 `Access-Control-Allow-Credentials: true`。\n * 调用方不需要做额外配置,`apiClient.create` 会自动识别并设置 `credentials: 'include'`。\n *\n * @example\n * ```ts\n * await apiClient.init({\n * baseUrl: 'https://api.example.com/api/v1',\n * auth: { storage: apiClient.tokenStorage.httpOnlyCookie() },\n * })\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\ntype RefreshCallback = (tokens: TokenPair) => void\n\n/**\n * 创建 Token 管理器:封装存储读写、自动刷新、并发去重与回调通知。\n *\n * 主要供 `apiClient.create` 内部使用,同时作为 named export 暴露给测试与高级用户。\n *\n * 行为约定:\n * - 并发多次 `refresh()` 共享同一个请求。\n * - refresh 接口失败、响应字段缺失或 401,都视为刷新失败:清空存储并触发 `onRefreshFailed`。\n * - 兼容两种响应格式:`{ data: tokens }` 与 `{ data: { tokens } }`(HaiResult 包装)。\n *\n * @example\n * ```ts\n * const manager = createTokenManager(\n * apiClient.tokenStorage.memory(),\n * 'https://api.example.com/api/v1/auth/refresh',\n * fetch,\n * () => notifyLoginRequired(),\n * )\n *\n * const off = manager.onTokenRefreshed(tokens => syncTokens(tokens))\n * const tokens = await manager.refresh()\n * off()\n * ```\n *\n * @param storage - Token 存储适配器\n * @param refreshEndpointUrl - refresh 接口完整 URL(`baseUrl + refreshPath`)\n * @param fetchFn - fetch 实现(允许注入 mock)\n * @param onRefreshFailed - 刷新失败回调\n * @param refreshTimeoutMs - 刷新请求超时(ms),默认 10_000\n * @returns Token 管理器实例\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\n async function refresh(): Promise<TokenPair | null> {\n // 并发去重:同一时刻只发一次 refresh,其它调用复用同一个 Promise\n if (refreshPromise)\n return refreshPromise\n refreshPromise = doRefresh()\n try {\n return await refreshPromise\n }\n finally {\n refreshPromise = null\n }\n }\n\n async function doRefresh(): Promise<TokenPair | null> {\n const refreshToken = await storage.getRefreshToken()\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 controller = new AbortController()\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\n if (!response.ok) {\n // 仅在服务端明确返回 401/4xx 时才清除 Token(refresh token 已失效);\n // 5xx 作为服务端临时故障,保留 Token 让下一次请求重试。\n if (response.status >= 400 && response.status < 500) {\n await storage.clear()\n onRefreshFailed?.()\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 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 storage.clear()\n onRefreshFailed?.()\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 storage.clear()\n onRefreshFailed?.()\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 await storage.setAccessToken(tokens.accessToken)\n await storage.setRefreshToken(refreshTokenForStorage)\n logger.info('Token refreshed successfully')\n for (const cb of callbacks) cb(tokens)\n return tokens\n }\n catch (error) {\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 await storage.setAccessToken(tokens.accessToken)\n if (tokens.refreshToken)\n await storage.setRefreshToken(tokens.refreshToken)\n },\n async clear() {\n await storage.clear()\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/** Token 管理器类型(`createTokenManager` 返回值)。 */\nexport type TokenManager = ReturnType<typeof createTokenManager>\n\n/** 兼容 `{ data: tokens }` 与 `{ data: { tokens } }` 两种 refresh 响应体。 */\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\": \"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 messagesZhCN from '../messages/zh-CN.json'\n\ntype ApiClientMessageKey = keyof typeof messagesZhCN\n\n/**\n * api-client 模块 i18n 文案获取器\n *\n * @param key - 文案 Key\n * @param options - 插值参数\n * @returns 本地化后的文案字符串\n */\nexport const apiClientM = core.i18n.createMessageGetter<ApiClientMessageKey>({\n 'zh-CN': messagesZhCN,\n 'en-US': messagesEnUS,\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 NOT_INITIALIZED: '010:500',\n CONFIG_ERROR: '011:500',\n UNKNOWN: '099:500',\n} as const satisfies ErrorInfo\n\n/**\n * API client 标准错误集合(遵循 `@h-ai/core` 错误码规范)。\n *\n * @example\n * ```ts\n * if (!result.success && result.error.code === HaiApiClientError.UNAUTHORIZED.code) {\n * redirectToLogin()\n * }\n * ```\n */\nexport const HaiApiClientError = core.error.buildHaiErrorsDef('api-client', ApiClientErrorInfo)\n\n// ─── Token ────────────────────────────────────────────────────────────────────\n\n/** Token 对(与 `@h-ai/iam` 的 TokenPair 对齐)。 */\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 * Token 存储适配器。\n *\n * 内置实现:\n * - `apiClient.tokenStorage.httpOnlyCookie()` — **默认**;浏览器端推荐,refresh token 由服务端管理,防 XSS。\n * - `apiClient.tokenStorage.memory()` — Node.js 测试 / SSR(需显式传入)。\n * - `apiClient.tokenStorage.localStorage()` — 浏览器 SPA / PWA(有 XSS 风险,生产不推荐)。\n *\n * Capacitor / 小程序场景请由对应模块提供(例如 `@h-ai/capacitor`)。\n */\nexport interface TokenStorage {\n readonly getAccessToken: () => Promise<string | null>\n readonly setAccessToken: (token: string) => Promise<void>\n readonly getRefreshToken: () => Promise<string | null>\n readonly setRefreshToken: (token: string) => Promise<void>\n readonly clear: () => Promise<void>\n}\n\n// ─── 配置 ─────────────────────────────────────────────────────────────────────\n\n/** Token 自动刷新配置。 */\nexport interface AuthConfig {\n /**\n * Token 存储适配器;默认 `apiClient.tokenStorage.httpOnlyCookie()`(refresh token 由服务端管理,防 XSS)。\n *\n * - SSR / Node.js 测试场景请显式传入 `apiClient.tokenStorage.memory()`。\n * - 浏览器 SPA 如确需持久化,可显式传 `apiClient.tokenStorage.localStorage()`,\n * 但请先评估 XSS 风险。\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/** API client 初始化配置。 */\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 * await crypto.init()\n * await apiClient.init({\n * baseUrl: 'https://api.example.com/api/v1',\n * transport: { crypto },\n * })\n * ```\n */\n readonly transport?: ApiClientTransportConfig\n}\n\n// ─── 传输加密 ────────────────────────────────────────────────────────────────\n\n/** {@link ApiClientConfig.transport} 的配置项。 */\nexport interface ApiClientTransportConfig {\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/** Token 辅助操作。 */\nexport interface ApiClientAuth {\n /** 手动写入 Token(例如登录成功后)。 */\n readonly setTokens: (tokens: TokenPair) => Promise<void>\n /** 清空存储的 Token(登出)。 */\n readonly clear: () => Promise<void>\n /** 订阅 Token 刷新成功事件,返回取消订阅函数。 */\n readonly onTokenRefreshed: (callback: (tokens: TokenPair) => void) => () => void\n}\n\n/** API client 生命周期能力。 */\nexport interface ApiClientLifecycle {\n /**\n * 初始化 client;必须在调用任何 procedure 前完成。\n *\n * @example\n * ```ts\n * await client.init({\n * baseUrl: 'https://api.example.com/api/v1',\n * auth: { refreshPath: '/auth/refresh' },\n * timeout: 15_000,\n * })\n * ```\n */\n readonly init: (config: ApiClientConfig) => Promise<HaiResult<void>>\n /** 释放资源并允许重新 `init`。 */\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/** 根据 oRPC contract 推导出的 typed API client(生命周期 + procedure 调用)。 */\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 * await apiClient.init({ baseUrl: 'https://api.example.com/api/v1' })\n * const me = await apiClient.iam.auth.currentUser()\n * 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 * await client.init({ baseUrl: 'https://api.example.com/api/v1' })\n * const result = await client.iam.auth.login({ identifier: 'alice', password: 'secret' })\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} from './api-client-types.js'\nimport { apiContract } from '@h-ai/api-contract'\nimport { core, err, ok } from '@h-ai/core'\nimport { createORPCClient, ORPCError } from '@orpc/client'\nimport { OpenAPILink } from '@orpc/openapi-client/fetch'\nimport {\n createHttpOnlyCookieTokenStorage,\n createLocalStorageTokenStorage,\n createMemoryTokenStorage,\n createTokenManager,\n} from './api-client-auth.js'\nimport { apiClientM } from './api-client-i18n.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.register),\n]\nconst TRAILING_SLASHES_REGEX = /\\/+$/\n\n/** Token 存储方案集合:内存 / 浏览器 localStorage / httpOnly Cookie。 */\nconst tokenStorage = {\n /** 内存存储——SSR / Node 测试或一次性会话使用。 */\n memory: createMemoryTokenStorage,\n /** 浏览器 `localStorage` 存储——单页应用便捷选项;注意 XSS 风险。 */\n localStorage: createLocalStorageTokenStorage,\n /** httpOnly Cookie 存储——浏览器场景推荐方案。 */\n httpOnlyCookie: createHttpOnlyCookieTokenStorage,\n} as const\n\n// ─── 工厂 ─────────────────────────────────────────────────────────────────────\n\n/**\n * 根据 oRPC contract 创建 typed API client。\n *\n * 返回对象同时具备:\n * - **生命周期能力**:`init` / `close` / `config` / `isInitialized` / `auth`\n * - **contract procedure**:按 contract 嵌套结构调用,例如 `client.iam.auth.login(input)`\n *\n * @example 基础用法\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 * await client.init({ baseUrl: 'https://api.example.com/api/v1' })\n *\n * const result = await client.iam.auth.login({ identifier: 'alice', password: 'secret' })\n * if (result.success) {\n * await client.auth.setTokens(result.data.tokens)\n * }\n *\n * await client.close()\n * ```\n *\n * @example 自定义 fetch / 超时\n * ```ts\n * const client = apiClient.create(contract)\n * await client.init({\n * baseUrl: 'https://api.example.com/api/v1',\n * fetch: customFetch,\n * timeout: 10_000,\n * })\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 } = {\n config: null,\n rawClient: null,\n tokenManager: undefined,\n transport: undefined,\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 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 }\n\n /**\n * 顶层 Proxy:把“一个对象同时扮演两种角色”的需求揉到一起。\n *\n * 这个 client 既要像一个普通服务对象,暴露生命周期方法:\n * - `client.init()`\n * - `client.close()`\n * - `client.config`\n * - `client.isInitialized`\n * - `client.auth`\n *\n * 又要像一个按 contract 动态展开的 oRPC client,支持:\n * - `client.iam.auth.login(input)`\n * - `client.storage.object.put(input)`\n * - `client.ai.chat.send(input)`\n *\n * 由于第二类 API 的层级结构是“运行时按属性链逐步访问”的,无法靠写死对象字面量覆盖所有路径,\n * 所以这里使用 Proxy:\n * - 先拦截顶层属性访问;\n * - 如果访问的是生命周期字段,就直接返回 lifecycle 中的真实成员;\n * - 否则把这个属性当成 contract 路径的第一段,继续交给 `createProcedureProxy()` 处理。\n *\n * 换句话说:\n * - `client.init` → 返回 lifecycle.init\n * - `client.auth` → 返回 lifecycle.auth\n * - `client.iam` → 返回一个新的嵌套 Proxy,继续等待 `.auth.login(...)`\n *\n * 注意:如果将来 contract 顶层字段和 lifecycle 字段重名,这里会“优先返回 lifecycle”。\n */\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 * await apiClient.init({\n * baseUrl: 'http://localhost:3000/api/v1',\n * auth: { refreshPath: '/auth/refresh' },\n * })\n * const result = await apiClient.iam.auth.login({ identifier: 'alice', password: 'secret' })\n * await apiClient.close()\n * ```\n */\nconst defaultApiContract = apiContract.create({ iam: apiContract.iam, storage: apiContract.storage, ai: apiContract.ai })\n\n/** `apiClient` 默认单例 + 工厂命名空间的统一对外类型。 */\nexport type DefaultApiClient = ApiClient<typeof defaultApiContract> & {\n readonly create: typeof createApiClient\n readonly tokenStorage: typeof tokenStorage\n}\n\n/** hai-framework API client 统一入口(默认单例 + `create` + `tokenStorage`)。 */\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 return Reflect.get(target as object, property, receiver)\n },\n}) as DefaultApiClient\n\n// ─── 内部:procedure 代理 ─────────────────────────────────────────────────────\n\n/**\n * 构造嵌套 procedure 代理。\n *\n * oRPC client 接口是任意层嵌套的属性 + 末端调用(如 `client.iam.auth.login(input)`)。\n * 这里用统一的 Proxy 透传所有路径,叶子节点(被 apply 时)映射到\n * `state.rawClient[a][b][c](input)` 并把错误标准化为 `HaiResult`。\n */\nfunction createProcedureProxy<TContract extends AnyContractRouter>(\n state: { rawClient: JsonifiedClient<ContractRouterClient<TContract>> | null },\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 callProcedure(state.rawClient, path, args)\n },\n })\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/** 根据 `auth` 配置决定是否创建 TokenManager。 */\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 // 浏览器 localStorage 场景请显式传入 `apiClient.tokenStorage.localStorage()`(有 XSS 风险)。\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 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/** 根据 `transport` 配置创建传输加密会话。 */\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 })\n}\n\n/** 构造 oRPC OpenAPILink + raw client。 */\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(contract, {\n url: config.baseUrl,\n headers: async () => buildHeaders(config, tokenManager),\n fetch: async (request, init) => fetchWithRefresh(request, init, config, tokenManager, transport),\n })\n return createORPCClient<JsonifiedClient<ContractRouterClient<TContract>>>(link)\n}\n\n/** 组装请求头:用户配置 + 客户端标识 + Bearer Token。 */\nasync function buildHeaders(\n config: ApiClientConfig,\n tokenManager: TokenManager | undefined,\n): Promise<Record<string, string>> {\n const configured = typeof config.headers === 'function' ? await config.headers() : config.headers ?? {}\n const accessToken = await tokenManager?.storage.getAccessToken()\n return {\n ...configured,\n 'x-request-client': config.clientName ?? DEFAULT_CLIENT_NAME,\n ...(accessToken ? { authorization: `Bearer ${accessToken}` } : {}),\n }\n}\n\n/** HTTP / HaiResult 认证失效时自动 refresh 并重试一次。 */\nasync function fetchWithRefresh(\n request: Request,\n init: { redirect?: Request['redirect'] },\n config: ApiClientConfig,\n tokenManager: TokenManager | undefined,\n transport: TransportClient | undefined,\n): Promise<Response> {\n // Fetch Request 的 body 是一次性可读流;为了支持 401 后重发 POST/PUT 等带 body 的请求,\n // 必须在首次发送前 `clone()` 一份。即使没有 401,clone 后的副本也只会被 GC 回收,无副作用。\n const retryRequest = tokenManager ? request.clone() : undefined\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 return fetchWithTimeout(new Request(retryRequest, { headers }), init, config, transport)\n}\n\n/** serv 领域失败保留 HTTP 200;克隆响应读取元数据,原响应仍交给 oRPC 消费。 */\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/** fetch + AbortController 超时控制(联动调用方原始 signal)。 */\nasync function fetchWithTimeout(\n request: Request,\n init: { redirect?: Request['redirect'] },\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/** ORPCError / AbortError / 其他异常 → 标准化 HaiResult 错误。 */\nfunction mapClientError(error: unknown) {\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 === 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.58",
|
|
5
5
|
"description": "Hai Framework universal HTTP client with Bearer token and API contract support.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"exports": {
|
|
@@ -20,30 +20,30 @@
|
|
|
20
20
|
"access": "public"
|
|
21
21
|
},
|
|
22
22
|
"engines": {
|
|
23
|
-
"node": "
|
|
23
|
+
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@
|
|
27
|
-
"@
|
|
28
|
-
"@
|
|
29
|
-
"
|
|
30
|
-
"@
|
|
31
|
-
"@
|
|
32
|
-
"
|
|
26
|
+
"@h-ai/api-contract": "0.1.0-alpha.58",
|
|
27
|
+
"@h-ai/core": "0.1.0-alpha.58",
|
|
28
|
+
"@h-ai/crypto": "0.1.0-alpha.58",
|
|
29
|
+
"@orpc/client": "^1.15.5",
|
|
30
|
+
"@orpc/contract": "^1.15.5",
|
|
31
|
+
"@orpc/openapi-client": "^1.15.5",
|
|
32
|
+
"zod": "^4.6.5"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@types/node": "^
|
|
35
|
+
"@types/node": "^26.6.5",
|
|
36
36
|
"rimraf": "^6.1.3",
|
|
37
37
|
"tsup": "^8.5.1",
|
|
38
38
|
"typescript": "^6.0.3",
|
|
39
|
-
"vitest": "^4.1.
|
|
39
|
+
"vitest": "^4.1.11"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"build": "tsup",
|
|
43
43
|
"dev": "tsup --watch",
|
|
44
44
|
"test": "vitest run",
|
|
45
45
|
"test:watch": "vitest",
|
|
46
|
-
"typecheck": "
|
|
46
|
+
"typecheck": "node ../../scripts/typecheck.mjs --noEmit",
|
|
47
47
|
"lint": "eslint .",
|
|
48
48
|
"lint:fix": "eslint . --fix",
|
|
49
49
|
"clean": "rimraf dist .turbo",
|