@h-ai/api-client 0.1.0-alpha.13 → 0.1.0-alpha.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -283
- package/dist/index.d.ts +1552 -268
- package/dist/index.js +327 -617
- package/dist/index.js.map +1 -1
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -1,340 +1,118 @@
|
|
|
1
1
|
# @h-ai/api-client
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> hai-framework 的跨端 oRPC/OpenAPI typed client,面向 Web、App、小程序等运行时提供统一 API 调用与 Token 管理。
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 能力概览
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
- 统一返回 `HaiResult<T>`
|
|
7
|
+
- `apiClient`:默认绑定 iam/storage/ai 领域的统一 typed client 入口。
|
|
8
|
+
- `apiClient.create(contract)`:为自定义 contract 创建 typed client。
|
|
9
|
+
- `apiClient.tokenStorage.{memory,localStorage,httpOnlyCookie}()`:Token 存储工厂。
|
|
10
|
+
- 支持自定义 `fetch`,适配浏览器、Node、Capacitor、小程序桥接层。
|
|
11
|
+
- 支持 Bearer Token 自动注入、401 后刷新并重试一次。
|
|
12
|
+
- 网络错误统一转换为 `HaiResult` 错误。
|
|
13
|
+
- 可选传输加密:`apiClient.init({ transport: { crypto } })` 自动使用 `crypto.transport.createClient()`。
|
|
14
|
+
- 传输加密与自动刷新可同时启用:401 后的 `/auth/refresh` 会继续走 encrypted fetch,不降级明文。
|
|
16
15
|
|
|
17
16
|
## 快速开始
|
|
18
17
|
|
|
19
18
|
```ts
|
|
20
|
-
import {
|
|
21
|
-
import { iamEndpoints } from '@h-ai/iam/api'
|
|
19
|
+
import { apiClient } from '@h-ai/api-client'
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
await api.init({
|
|
21
|
+
await apiClient.init({
|
|
25
22
|
baseUrl: 'https://api.example.com/api/v1',
|
|
26
|
-
auth: {
|
|
23
|
+
auth: {}, // 默认使用 httpOnly cookie 存储(推荐);SSR 测试请显式传入 apiClient.tokenStorage.memory()
|
|
27
24
|
})
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
const loginResult = await api.call(iamEndpoints.login, {
|
|
26
|
+
const login = await apiClient.iam.auth.login({
|
|
31
27
|
identifier: 'alice',
|
|
32
|
-
password: '
|
|
28
|
+
password: 'secret',
|
|
33
29
|
})
|
|
34
30
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
if (login.success) {
|
|
32
|
+
// httpOnly cookie 模式下 refresh token 由服务端 Set-Cookie 管理,
|
|
33
|
+
// api-client 默认存储只需要把 access token 写入内存。
|
|
34
|
+
await apiClient.auth.setTokens(login.data.tokens)
|
|
38
35
|
}
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
const me = await api.call(iamEndpoints.currentUser, {})
|
|
37
|
+
const me = await apiClient.iam.auth.currentUser()
|
|
42
38
|
|
|
43
|
-
|
|
44
|
-
await api.close()
|
|
39
|
+
await apiClient.close()
|
|
45
40
|
```
|
|
46
41
|
|
|
47
|
-
|
|
42
|
+
### 启用传输加密
|
|
48
43
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
`@h-ai/api-client` 与 `@h-ai/iam` 通过 **API 契约(`EndpointDef`)** 实现端到端类型安全。客户端和服务端共享同一份契约定义,编译时保证一致性。
|
|
52
|
-
|
|
53
|
-
### 架构总览
|
|
54
|
-
|
|
55
|
-
```
|
|
56
|
-
┌─────────────────────────────────────────────────┐
|
|
57
|
-
│ @h-ai/iam/api │
|
|
58
|
-
│ iam-api-schemas.ts ← Zod Schema(唯一真相源) │
|
|
59
|
-
│ iam-api-contract.ts ← iamEndpoints │
|
|
60
|
-
└────────┬────────────────────────┬────────────────┘
|
|
61
|
-
│ │
|
|
62
|
-
┌──────▼──────┐ ┌───────▼────────┐
|
|
63
|
-
│ 客户端 │ │ 服务端 │
|
|
64
|
-
│ api.call() │ HTTP │ kit.fromContract│
|
|
65
|
-
│ @h-ai/ │ ◄────► │ @h-ai/kit │
|
|
66
|
-
│ api-client │ │ │
|
|
67
|
-
└─────────────┘ └─────────────────┘
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
### 完整登录 → 使用 → 登出流程
|
|
44
|
+
服务端需先启用 `serv.createApp({ transport: { crypto } })`;客户端只注入同一个 `@h-ai/crypto` 服务实例,无需手写密钥协商代码。
|
|
71
45
|
|
|
72
46
|
```ts
|
|
73
|
-
import {
|
|
74
|
-
import {
|
|
75
|
-
|
|
76
|
-
// ── 初始化 ──
|
|
77
|
-
await api.init({
|
|
78
|
-
baseUrl: 'https://api.example.com/api/v1',
|
|
79
|
-
auth: {
|
|
80
|
-
refreshUrl: '/auth/refresh',
|
|
81
|
-
// Token 刷新成功后的回调(可选,适合更新全局状态)
|
|
82
|
-
onTokenRefreshed: (tokens) => {
|
|
83
|
-
// tokens 包含新的 accessToken / refreshToken / expiresIn
|
|
84
|
-
},
|
|
85
|
-
// Token 刷新失败后的回调(可选,常用于跳转登录页)
|
|
86
|
-
onRefreshFailed: () => {
|
|
87
|
-
window.location.href = '/login'
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
// ── 登录 ──
|
|
93
|
-
const loginResult = await api.call(iamEndpoints.login, {
|
|
94
|
-
identifier: 'alice',
|
|
95
|
-
password: 'StrongPassword123',
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
if (!loginResult.success) {
|
|
99
|
-
// loginResult.error.code 可能是:
|
|
100
|
-
// 1203 (UNAUTHORIZED) — 凭证错误
|
|
101
|
-
// 1206 (VALIDATION_FAILED) — 入参不合法
|
|
102
|
-
// 1202 (SERVER_ERROR) — 服务端异常
|
|
103
|
-
console.error('Login failed:', loginResult.error.message)
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
// loginResult.data 类型自动推导为 { user, tokens, agreements? }
|
|
107
|
-
const { user, tokens } = loginResult.data
|
|
108
|
-
|
|
109
|
-
// 保存 Token — 后续所有请求自动附加 Authorization: Bearer <accessToken>
|
|
110
|
-
await api.auth.setTokens(tokens)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// ── 获取当前用户 ──(自动携带 Bearer Token)
|
|
114
|
-
const meResult = await api.call(iamEndpoints.currentUser, {})
|
|
115
|
-
if (meResult.success) {
|
|
116
|
-
// meResult.data 类型推导为 { user, roles, permissions }
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// ── 修改密码 ──
|
|
120
|
-
await api.call(iamEndpoints.changePassword, {
|
|
121
|
-
oldPassword: 'OldPass123',
|
|
122
|
-
newPassword: 'NewPass456',
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
// ── 登出 ──
|
|
126
|
-
const accessToken = '...' // 从 Token 存储获取
|
|
127
|
-
await api.call(iamEndpoints.logout, { accessToken })
|
|
128
|
-
await api.auth.clear() // 清空本地 Token 存储
|
|
129
|
-
```
|
|
47
|
+
import { apiClient } from '@h-ai/api-client'
|
|
48
|
+
import { crypto } from '@h-ai/crypto'
|
|
130
49
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
当请求收到 401 响应时,`api-client` 自动执行以下流程:
|
|
134
|
-
|
|
135
|
-
1. 使用存储的 `refreshToken` 调用 `auth.refreshUrl`(POST)
|
|
136
|
-
2. 刷新成功 → 更新存储 → 用新 Token 重试原请求(仅重试一次)
|
|
137
|
-
3. 刷新失败 → 清空 Token → 触发 `onRefreshFailed` 回调
|
|
138
|
-
4. 多个并发 401 请求 → **自动去重**,只发一次刷新请求
|
|
139
|
-
|
|
140
|
-
> 流式请求(`api.stream()`)同样支持 401 自动刷新 + 重试。
|
|
141
|
-
|
|
142
|
-
### 可用的 IAM 端点
|
|
143
|
-
|
|
144
|
-
| 分类 | 端点 | 方法 | 路径 | 认证 |
|
|
145
|
-
| ------------ | --------------------------------- | ------ | ----------------------------- | ---- |
|
|
146
|
-
| **认证** | `iamEndpoints.login` | POST | /auth/login | 否 |
|
|
147
|
-
| | `iamEndpoints.loginWithOtp` | POST | /auth/login/otp | 否 |
|
|
148
|
-
| | `iamEndpoints.logout` | POST | /auth/logout | 是 |
|
|
149
|
-
| | `iamEndpoints.currentUser` | GET | /auth/me | 是 |
|
|
150
|
-
| | `iamEndpoints.refreshToken` | POST | /auth/refresh | 否 |
|
|
151
|
-
| | `iamEndpoints.sendOtp` | POST | /auth/otp/send | 否 |
|
|
152
|
-
| | `iamEndpoints.register` | POST | /auth/register | 否 |
|
|
153
|
-
| | `iamEndpoints.changePassword` | POST | /auth/change-password | 是 |
|
|
154
|
-
| | `iamEndpoints.updateCurrentUser` | PUT | /auth/me | 是 |
|
|
155
|
-
| **用户管理** | `iamEndpoints.listUsers` | GET | /iam/users | 是 |
|
|
156
|
-
| | `iamEndpoints.getUser` | GET | /iam/users/:id | 是 |
|
|
157
|
-
| | `iamEndpoints.createUser` | POST | /iam/users | 是 |
|
|
158
|
-
| | `iamEndpoints.updateUser` | PUT | /iam/users/:id | 是 |
|
|
159
|
-
| | `iamEndpoints.deleteUser` | DELETE | /iam/users/:id | 是 |
|
|
160
|
-
| | `iamEndpoints.adminResetPassword` | POST | /iam/users/:id/reset-password | 是 |
|
|
161
|
-
| **角色管理** | `iamEndpoints.listRoles` | GET | /iam/roles | 是 |
|
|
162
|
-
| | `iamEndpoints.getRole` | GET | /iam/roles/:id | 是 |
|
|
163
|
-
| | `iamEndpoints.createRole` | POST | /iam/roles | 是 |
|
|
164
|
-
| | `iamEndpoints.updateRole` | PUT | /iam/roles/:id | 是 |
|
|
165
|
-
| | `iamEndpoints.deleteRole` | DELETE | /iam/roles/:id | 是 |
|
|
166
|
-
| **权限管理** | `iamEndpoints.listPermissions` | GET | /iam/permissions | 是 |
|
|
167
|
-
| | `iamEndpoints.getPermission` | GET | /iam/permissions/:id | 是 |
|
|
168
|
-
| | `iamEndpoints.createPermission` | POST | /iam/permissions | 是 |
|
|
169
|
-
| | `iamEndpoints.deletePermission` | DELETE | /iam/permissions/:id | 是 |
|
|
170
|
-
|
|
171
|
-
## 常见配置场景
|
|
172
|
-
|
|
173
|
-
### 浏览器端(默认 localStorage)
|
|
50
|
+
await crypto.init()
|
|
174
51
|
|
|
175
|
-
|
|
176
|
-
import { api } from '@h-ai/api-client'
|
|
177
|
-
|
|
178
|
-
await api.init({
|
|
52
|
+
await apiClient.init({
|
|
179
53
|
baseUrl: 'https://api.example.com/api/v1',
|
|
180
|
-
auth: {
|
|
181
|
-
|
|
54
|
+
auth: {},
|
|
55
|
+
transport: { crypto }, // 默认协商路径:/api/v1/_hai/key-exchange
|
|
182
56
|
})
|
|
183
|
-
```
|
|
184
57
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
```ts
|
|
188
|
-
import { api } from '@h-ai/api-client'
|
|
189
|
-
import { createCapacitorTokenStorage } from '@h-ai/capacitor'
|
|
190
|
-
|
|
191
|
-
await api.init({
|
|
192
|
-
baseUrl: `${PUBLIC_API_BASE}/api/v1`,
|
|
193
|
-
auth: {
|
|
194
|
-
storage: createCapacitorTokenStorage(),
|
|
195
|
-
refreshUrl: '/auth/refresh',
|
|
196
|
-
},
|
|
197
|
-
})
|
|
58
|
+
await apiClient.close()
|
|
59
|
+
await crypto.close()
|
|
198
60
|
```
|
|
199
61
|
|
|
200
|
-
|
|
62
|
+
## API 契约
|
|
201
63
|
|
|
202
|
-
|
|
203
|
-
import { api, createMemoryTokenStorage } from '@h-ai/api-client'
|
|
204
|
-
|
|
205
|
-
await api.init({
|
|
206
|
-
baseUrl: 'https://api.example.com/api/v1',
|
|
207
|
-
auth: {
|
|
208
|
-
storage: createMemoryTokenStorage(),
|
|
209
|
-
refreshUrl: '/auth/refresh',
|
|
210
|
-
},
|
|
211
|
-
})
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
## 错误处理
|
|
215
|
-
|
|
216
|
-
所有通用 HTTP / 契约调用均返回 `HaiResult<T>`:
|
|
64
|
+
自定义应用可以绑定自己的 oRPC contract:
|
|
217
65
|
|
|
218
66
|
```ts
|
|
219
|
-
|
|
67
|
+
import { apiClient } from '@h-ai/api-client'
|
|
68
|
+
import { apiContract } from '@h-ai/api-contract'
|
|
220
69
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
case 1203: // UNAUTHORIZED — Token 失效且刷新也失败
|
|
224
|
-
redirectToLogin()
|
|
225
|
-
break
|
|
226
|
-
case 1201: // TIMEOUT — 请求超时
|
|
227
|
-
showRetryDialog()
|
|
228
|
-
break
|
|
229
|
-
case 1206: // VALIDATION_FAILED — 入参或出参校验失败
|
|
230
|
-
showValidationError(result.error.details)
|
|
231
|
-
break
|
|
232
|
-
default:
|
|
233
|
-
showGenericError(result.error.message)
|
|
234
|
-
}
|
|
235
|
-
return
|
|
236
|
-
}
|
|
70
|
+
const contract = apiContract.create({ iam: apiContract.iam })
|
|
71
|
+
const client = apiClient.create(contract)
|
|
237
72
|
|
|
238
|
-
|
|
239
|
-
|
|
73
|
+
await client.init({ baseUrl: 'https://api.example.com/api/v1' })
|
|
74
|
+
const result = await client.iam.auth.login({ identifier: 'alice', password: 'secret' })
|
|
240
75
|
```
|
|
241
76
|
|
|
242
|
-
|
|
77
|
+
## API 概览
|
|
243
78
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
| `HaiApiClientError.UNAUTHORIZED` | `hai:api-client:004` | 401 未认证 |
|
|
250
|
-
| `HaiApiClientError.FORBIDDEN` | `hai:api-client:005` | 403 无权限 |
|
|
251
|
-
| `HaiApiClientError.NOT_FOUND` | `hai:api-client:006` | 404 资源不存在 |
|
|
252
|
-
| `HaiApiClientError.VALIDATION_FAILED` | `hai:api-client:007` | 请求参数校验失败 |
|
|
253
|
-
| `HaiApiClientError.TOKEN_REFRESH_FAILED` | `hai:api-client:008` | Token 刷新失败 |
|
|
254
|
-
| `HaiApiClientError.NOT_INITIALIZED` | `hai:api-client:010` | 未初始化 |
|
|
255
|
-
| `HaiApiClientError.CONFIG_ERROR` | `hai:api-client:011` | 配置错误 |
|
|
256
|
-
| `HaiApiClientError.UNKNOWN` | `hai:api-client:099` | 未知错误 |
|
|
79
|
+
- `apiClient.init(config)`:初始化默认 client。
|
|
80
|
+
- `apiClient.close()`:清理 client 状态。
|
|
81
|
+
- `apiClient.auth.setTokens(tokens)`:写入 access token;非 httpOnly 存储会同时写入 refresh token。
|
|
82
|
+
- `apiClient.auth.clear()`:清理 token。
|
|
83
|
+
- `apiClient.create(contract)`:创建自定义 typed client。
|
|
257
84
|
|
|
258
|
-
##
|
|
85
|
+
## 配置
|
|
259
86
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
87
|
+
- `baseUrl`:API 基础地址,通常包含 `/api/v1`。
|
|
88
|
+
- `auth.storage`:Token 存储适配器;默认 `apiClient.tokenStorage.httpOnlyCookie()`(浏览器推荐);SSR / 测试场景请显式传入 `apiClient.tokenStorage.memory()`。
|
|
89
|
+
- `auth.refreshPath`:刷新 token 路径,默认 `/auth/refresh`。
|
|
90
|
+
- `timeout`:请求超时,默认 30000ms。
|
|
91
|
+
- `headers`:静态或动态公共请求头。
|
|
92
|
+
- `fetch`:自定义 fetch 实现。
|
|
93
|
+
- `transport.crypto`:启用透明请求/响应加解密,必须传入已初始化的 `@h-ai/crypto` 实例。
|
|
94
|
+
- `transport.keyExchangePath`:密钥协商子路径,默认 `/_hai/key-exchange`;会自动拼接到 `baseUrl` 后。
|
|
268
95
|
|
|
269
|
-
|
|
270
|
-
await api.auth.clear()
|
|
271
|
-
|
|
272
|
-
// 监听自动刷新事件
|
|
273
|
-
const unsubscribe = api.auth.onTokenRefreshed((tokens) => {
|
|
274
|
-
// Token 自动刷新成功时触发
|
|
275
|
-
})
|
|
276
|
-
|
|
277
|
-
// 取消监听
|
|
278
|
-
unsubscribe()
|
|
279
|
-
```
|
|
96
|
+
## 错误处理
|
|
280
97
|
|
|
281
|
-
|
|
98
|
+
业务 API 返回 `HaiResult<T>`:
|
|
282
99
|
|
|
283
100
|
```ts
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
// chunk 对应 SSE 的 data: 内容
|
|
288
|
-
process.stdout.write(chunk)
|
|
101
|
+
const result = await apiClient.iam.auth.currentUser()
|
|
102
|
+
if (!result.success) {
|
|
103
|
+
// result.error.code / result.error.message
|
|
289
104
|
}
|
|
290
|
-
|
|
291
|
-
// 主动停止流式响应
|
|
292
|
-
controller.abort()
|
|
293
105
|
```
|
|
294
106
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
- `stream()` 内置超时控制(连接阶段与流式读取阶段均生效)
|
|
298
|
-
- 401 会尝试自动刷新 Token 后重试一次
|
|
299
|
-
- 解析器支持跨 chunk 的 SSE 行缓冲
|
|
300
|
-
- 支持外部传入 `AbortSignal` 主动取消
|
|
301
|
-
|
|
302
|
-
## 拦截器(高级用法)
|
|
303
|
-
|
|
304
|
-
```ts
|
|
305
|
-
await api.init({
|
|
306
|
-
baseUrl: 'https://api.example.com/api/v1',
|
|
307
|
-
auth: { refreshUrl: '/auth/refresh' },
|
|
308
|
-
interceptors: {
|
|
309
|
-
request: [
|
|
310
|
-
async config => ({
|
|
311
|
-
...config,
|
|
312
|
-
headers: {
|
|
313
|
-
...config.headers,
|
|
314
|
-
'X-App-Version': '1.0.0',
|
|
315
|
-
},
|
|
316
|
-
}),
|
|
317
|
-
],
|
|
318
|
-
response: [
|
|
319
|
-
async (response) => {
|
|
320
|
-
// 可统一记录埋点、限流处理等
|
|
321
|
-
return response
|
|
322
|
-
},
|
|
323
|
-
],
|
|
324
|
-
},
|
|
325
|
-
})
|
|
326
|
-
```
|
|
107
|
+
未初始化、网络错误、超时、401/403/404 等客户端侧问题也会转换为失败的 `HaiResult`。
|
|
327
108
|
|
|
328
109
|
## 测试
|
|
329
110
|
|
|
330
111
|
```bash
|
|
331
112
|
pnpm --filter @h-ai/api-client test
|
|
113
|
+
pnpm --filter @h-ai/api-client typecheck
|
|
332
114
|
```
|
|
333
115
|
|
|
334
116
|
## License
|
|
335
117
|
|
|
336
118
|
Apache-2.0
|
|
337
|
-
|
|
338
|
-
## License
|
|
339
|
-
|
|
340
|
-
Apache-2.0
|