@h-ai/api-client 1.0.0-beta.2 → 1.0.0-beta.21

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 CHANGED
@@ -6,54 +6,119 @@
6
6
 
7
7
  [框架首页](../../README.md) · 相关模块:[@h-ai/api-contract](../api-contract/README.md) · [@h-ai/core](../core/README.md) · [@h-ai/crypto](../crypto/README.md)
8
8
 
9
- ## 简介
10
-
11
9
  hai-framework 的跨端 oRPC/OpenAPI typed client,面向 Web、App、小程序等运行时提供统一 API 调用与 Token 管理。
12
10
 
13
- ## 核心功能清单
14
-
15
- - `apiClient`:默认绑定 iam/storage/ai 领域的统一 typed client 入口。
16
- - `apiClient.create(contract)`:为自定义 contract 创建 typed client。
17
- - `apiClient.tokenStorage.{memory,httpOnlyCookie}()`:Token 存储工厂。浏览器使用内存 access token + 服务端 httpOnly refresh Cookie;原生端注入平台安全存储,不将凭据写入 Web Storage。
18
- - 支持自定义 `fetch`,适配浏览器、Node、Capacitor、小程序桥接层。
19
- - 支持 Bearer Token 自动注入、401 后刷新并重试一次。
20
- - 网络错误统一转换为 `HaiResult` 错误。
21
- - 可选传输加密:`apiClient.init({ transport: { crypto } })` 自动使用 `crypto.transport.createClient()`。
22
- - 传输加密与自动刷新可同时启用:401 后的 `/auth/refresh` 会继续走 encrypted fetch,不降级明文。
11
+ ## 快速开始
23
12
 
24
- ## 使用入门
25
-
26
- ### 快速开始
13
+ `close()` 会使旧刷新失效并等待已有凭据写入完成,保留外置 TokenStorage 当前值;关闭完成后可复用同一存储重新初始化。`timeout` 覆盖响应头、完整 JSON 正文及所有非成功响应正文(包括 HTML 错误页);超时或取消后不再刷新凭据。内置 Cookie 适配器的登录和刷新使用 `credentials: include`,跨域服务须明确允许凭据与 Origin;内存和原生存储保留原请求的凭据策略。应用自定义流按需读取,其正文由调用方 signal 取消。
27
14
 
28
15
  ```ts
16
+ import type { IamAuthLoginInput } from '@h-ai/api-client'
29
17
  import { apiClient } from '@h-ai/api-client'
30
18
 
31
- await apiClient.init({
32
- baseUrl: 'https://api.example.com/api/v1',
33
- auth: {}, // 默认使用 httpOnly cookie 存储(推荐);SSR 测试请显式传入 apiClient.tokenStorage.memory()
34
- })
19
+ async function loginAndLoadProfile(input: IamAuthLoginInput) {
20
+ const initialized = await apiClient.init({ baseUrl: '/api/v1', auth: {} })
21
+ if (!initialized.success)
22
+ return initialized
23
+ try {
24
+ const actions = apiClient.createAuthActions(apiClient)
25
+ const loggedIn = await actions.login(input)
26
+ if (!loggedIn.success)
27
+ return loggedIn
28
+ const profile = await apiClient.iam.auth.currentUser()
29
+ if (!profile.success)
30
+ return profile
31
+ return profile
32
+ }
33
+ catch (writeError) {
34
+ try {
35
+ await apiClient.auth.clear()
36
+ }
37
+ catch (cleanupError) {
38
+ throw new AggregateError([writeError, cleanupError], 'Credential persistence and cleanup failed')
39
+ }
40
+ throw writeError
41
+ }
42
+ finally {
43
+ await apiClient.close()
44
+ }
45
+ }
46
+ ```
35
47
 
36
- const login = await apiClient.iam.auth.login({
37
- identifier: 'alice',
38
- password: 'secret',
39
- })
48
+ ## 功能
40
49
 
41
- if (login.success) {
42
- // httpOnly cookie 模式下 refresh token 由服务端 Set-Cookie 管理,
43
- // api-client 默认存储只需要把 access token 写入内存。
44
- await apiClient.auth.setTokens(login.data.tokens)
45
- }
50
+ ### 用途、输入与输出
46
51
 
47
- const me = await apiClient.iam.auth.currentUser()
48
- if (me.success) {
49
- // 身份资料与服务端重新校验后的访问范围一次返回,客户端无需持久化权限快照。
50
- const { roles, permissions, ...user } = me.data
51
- }
52
+ 调用前完成本页初始化。命名变量为经校验的宿主输入;HaiResult 先判 success。auth 和存储操作的 Promise<void> 失败会 reject,必须显示失败并处理清理故障,不能按业务 success 假报登录完成。
52
53
 
53
- await apiClient.close()
54
+ | 入口 | 输入 | 输出 | 用途与失败边界 | 示例 |
55
+ | --------------------------------------- | ------------------------------- | -------------------------- | -------------------------------------------------- | ------------------------------------------------------------ |
56
+ | `apiClient.init` | config: baseUrl、认证及传输选项 | `Promise<HaiResult<void>>` | 准备当前客户端,失败停止调用 | `await apiClient.init({ baseUrl: '/api/v1', auth: {} })` |
57
+ | `apiClient.close` | 无参数 | `Promise<void>` | 作废在途刷新并释放本实例;不等于服务端退出 | `await apiClient.close()` |
58
+ | `apiClient.create` | contract: 应用共享契约 | `ApiClient<TContract>` | 创建隔离实例,SSR 每个请求独立创建 | `apiClient.create(contract)` |
59
+ | `apiClient.auth.setTokens` | TokenPair: 可信认证响应 | `Promise<void>` | 串行保存并废弃旧刷新;存储故障 reject | `await apiClient.auth.setTokens(tokens)` |
60
+ | `apiClient.auth.clear` | 无参数 | `Promise<void>` | 清理本地凭据;失败 reject,服务端仍须 logout | `await apiClient.auth.clear()` |
61
+ | `apiClient.auth.onTokenRefreshed` | callback(TokenPair) | `() => void` | 保存成功后观察刷新,返回取消函数;不输出 Token | `apiClient.auth.onTokenRefreshed(() => refreshProfile())` |
62
+ | `apiClient.tokenStorage.memory` | 无参数 | `TokenStorage` | Node/SSR/测试的独立内存适配器 | `apiClient.tokenStorage.memory()` |
63
+ | `apiClient.tokenStorage.httpOnlyCookie` | 无参数 | `TokenStorage` | 浏览器 access 在内存,refresh 由服务端 Cookie 管理 | `apiClient.tokenStorage.httpOnlyCookie()` |
64
+ | `apiClient.createAuthActions` | client: 含标准 IAM 契约 | `Auth actions object` | 连接认证表单;先 init 并启用 auth | `apiClient.createAuthActions(apiClient)` |
65
+ | `apiClient.observeRequests` | observer(ApiRequestRecord) | `() => void` | 观察脱敏逻辑结果,返回取消函数 | `apiClient.observeRequests(record => reportOutcome(record))` |
66
+
67
+ #### 认证表单动作
68
+
69
+ `const actions = apiClient.createAuthActions(apiClient)`
70
+
71
+ | 入口 | 输入 | 输出 | 用途与失败边界 | 示例 |
72
+ | ------------------------------ | ----------------------------------------------- | --------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------------------- |
73
+ | `actions.publicConfig` | — | `Promise<HaiResult<T>>` → IamPublicConfig | 读取公开登录/注册策略 | `await actions.publicConfig()` |
74
+ | `actions.createCaptcha` | { operation } | `Promise<HaiResult<T>>` → IamCaptchaChallenge | 获取所选操作的挑战 | `await actions.createCaptcha({ operation: 'login' })` |
75
+ | `actions.login` | IamAuthLoginInput | `Promise<HaiResult<T>>` → 认证结果与 Token | 映射 password/ldap/email/sms 并等待存储成功 | `await actions.login({ identifier, password })` |
76
+ | `actions.register` | IamRegisterInput | `Promise<HaiResult<T>>` → 注册认证结果 | 按服务端注册策略创建身份并保存凭据 | `await actions.register({ username, password })` |
77
+ | `actions.sendOtp` | identifier, { captcha? }? | `Promise<HaiResult<T>>` → { expiresAt } | 发送邮箱/手机验证码,不等于完成登录 | `await actions.sendOtp(identifier)` |
78
+ | `actions.requestPasswordReset` | identifier, { captcha? }? | `Promise<HaiResult<T>>` → void | 申请重置,按服务端策略隐藏账户存在性 | `await actions.requestPasswordReset(identifier)` |
79
+ | `actions.confirmPasswordReset` | token, newPassword | `Promise<HaiResult<T>>` → void | 消费专用重置 Token 修改密码 | `await actions.confirmPasswordReset(resetToken, newPassword)` |
80
+ | `actions.renewPassword` | { identifier, password, newPassword, captcha? } | `Promise<HaiResult<T>>` → void | 完成密码到期续密流程 | `await actions.renewPassword({ identifier, password, newPassword })` |
81
+ | `actions.reauthenticate` | password, operation | `Promise<HaiResult<T>>` → 用途票据 | 为本次敏感操作取得短期用途凭据 | `await actions.reauthenticate(password, 'users:delete')` |
82
+ | `actions.bindPageExit` | { enabled? } | `() => void` | 安装页面退出监听并返回清理函数;尽力撤销而非可靠离线通知 | `actions.bindPageExit({ enabled: endOnPageHide })` |
83
+
84
+ #### 安全存储实例
85
+
86
+ `const storage = apiClient.tokenStorage.memory()`
87
+
88
+ | 入口 | 输入 | 输出 | 用途与失败边界 | 示例 |
89
+ | ------------------------- | ----- | ------------------------- | -------------------------------------------------- | --------------------------------------------- |
90
+ | `storage.getAccessToken` | — | `Promise<string \| null>` | 读取 access;null 仅为不存在 | `await storage.getAccessToken()` |
91
+ | `storage.setAccessToken` | token | `Promise<void>` | 保存 access,已绑定客户端改用 auth.setTokens | `await storage.setAccessToken(accessToken)` |
92
+ | `storage.getRefreshToken` | — | `Promise<string \| null>` | Cookie 模式是内部哨兵,不读取 httpOnly Cookie 原文 | `await storage.getRefreshToken()` |
93
+ | `storage.setRefreshToken` | token | `Promise<void>` | 内存适配器保存;Cookie 适配器不改服务端 Cookie | `await storage.setRefreshToken(refreshToken)` |
94
+ | `storage.clear` | — | `Promise<void>` | 清本适配器凭据,失败 reject;不撤销服务端会话 | `await storage.clear()` |
95
+
96
+ #### 默认业务领域
97
+
98
+ 默认领域是 api-contract 的 HTTP 契约,不是服务端模块对象的完整镜像;自定义 client 仅包含传入 contract 的领域。
99
+
100
+ - `apiClient.iam`: IAM 登录/注册/用户/角色/权限 — [API Contract](../api-contract/README.md), [IAM](../iam/README.md).
101
+ - `apiClient.storage`: 存储列表、元数据、预签名和删除 — [API Contract](../api-contract/README.md), [Storage](../storage/README.md).
102
+ - `apiClient.ai`: AI 非流式聊天 — [API Contract](../api-contract/README.md), [AI](../ai/README.md).
103
+
104
+ 数组型输入/输出、权限与业务错误按实际契约。服务端决定身份和资源授权,客户端配置不能授予权限。
105
+
106
+ ```ts
107
+ const files = await apiClient.storage.files.list({ prefix: 'reports/', maxKeys: 20 })
108
+ if (!files.success)
109
+ throw new Error(files.error.message)
110
+ const chat = await apiClient.ai.chats.createCompletion({
111
+ messages: [
112
+ { role: 'user', content: 'Summarize this public report.' }
113
+ ],
114
+ })
115
+ if (!chat.success)
116
+ throw new Error(chat.error.message)
54
117
  ```
55
118
 
56
- #### 启用传输加密
119
+ `apiClient.config` 是脱敏运行快照(未初始化为 null),不能用于恢复真实密钥;`apiClient.isInitialized` 为就绪 boolean。它们是只读属性,不发起网络请求。
120
+
121
+ ### 启用传输加密
57
122
 
58
123
  服务端需先启用 `serv.createApp({ transport: { crypto, serverKeyRing, keyStore } })`;客户端只注入同一个 `@h-ai/crypto` 服务实例,无需手写密钥协商代码。
59
124
 
@@ -75,7 +140,7 @@ await apiClient.close()
75
140
  await crypto.close()
76
141
  ```
77
142
 
78
- 认证协议仅支持 v2:独立随机 SM4-CBC 密钥和 HMAC-SM3 密钥由 SM2 封装,先验签、再验证 MAC,最后解密 CBC。请求由客户端私钥签名,响应由所选服务端版本签名。认证字段包括 version、direction、serverKeyId、clientId、128 位随机 requestId(兼 nonce)、issuedAt、HTTP method、pathname + 原始 search 的 SM3 pathHash,以及响应 status。请求有效期 5 分钟,未来时钟偏差最多 30 秒;共享 registry 原子消费 (clientId, requestId),nonce 至少保留到 issuedAt + 6 分钟以覆盖节点偏差,存储故障拒绝请求。Redis 必须保留有效 nonce,使用 noeviction/可靠持久化策略;各节点时钟须同步。此能力防止同一传输消息重放,业务重试仍需自己的幂等键。
143
+ 认证协议仅支持 v2:独立随机 SM4-CBC 密钥和 HMAC-SM3 密钥由 SM2 封装,先验签、再验证 MAC,最后解密 CBC。请求由客户端私钥签名,响应由所选服务端版本签名。认证字段包括 version、direction、serverKeyId、clientId、128 位随机 requestId(兼 nonce)、issuedAt、HTTP method、pathname + 原始 search 的 SM3 pathHash,以及响应 status。请求默认有效期为 5 分钟,客户端时间最多可领先服务器 30 秒。服务端可用 `maxRequestAgeSeconds`(1~600)和 `maxFutureSkewSeconds`(0~300)调整,单位为整数秒。共享 registry 原子消费 (clientId, requestId),nonce 保留到 issuedAt + 11 分钟,覆盖最长可配窗口及节点时钟差,存储故障拒绝请求。Redis 必须保留有效 nonce,使用 noeviction/可靠持久化策略;各节点时钟须同步。此能力防止同一传输消息重放,业务重试仍需自己的幂等键。
79
144
 
80
145
  空 POST 也发送空明文的认证封装;GET/HEAD 将封装放在 X-Hai-Transport 头,204/304/HEAD 等无正文响应也使用同名头承载认证封装。其它请求/响应在 JSON 正文承载封装并带 X-Encrypted: true。客户端必须关联原请求上下文和 HTTP status,缺标记、旧格式或认证失败一律 reject;明文接口由 Serv/Kit 路径规则 `encryption: disabled` 或宿主 `excludePaths` 显式允许;调用方同步选择普通请求,API Client/Kit 包装器使用自己的 `excludePaths`。CORS 须允许并暴露 X-Hai-Transport。握手公钥的真实性依赖 TLS,不替代 IAM 业务授权。
81
146
 
@@ -96,30 +161,12 @@ await client.init({ baseUrl: 'https://api.example.com/api/v1' })
96
161
  const result = await client.iam.auth.login({ identifier: 'alice', password: 'secret' })
97
162
  ```
98
163
 
99
- ### API 概览
100
-
101
- - `apiClient.init(config)`:初始化默认 client。
102
- - `apiClient.close()`:清理 client 状态。
103
- - `apiClient.auth.setTokens(tokens)`:替换凭据,写入 access token;非 httpOnly 存储写入提供的 refresh token,缺省时清除旧值。
104
- - `apiClient.auth.clear()`:清理 token。
105
- - `apiClient.create(contract)`:创建自定义 typed client。
164
+ ### 凭据更新与关闭
106
165
 
107
166
  `auth.clear()` 和 `auth.setTokens()` 会立即取消并作废当前客户端的旧刷新请求。迟到的成功或 401 响应不会恢复已退出会话、覆盖新账号或触发旧刷新回调;存储写入串行提交,方法完成后不会再被之前已开始的异步写入覆盖。退出和切换账号须通过这些方法更新凭据,不直接修改 storage。httpOnly Cookie 和服务端会话仍由服务端退出接口撤销。
108
167
 
109
168
  已初始化后再次调用 `init()` 会返回 `CONFIG_ERROR`,不会覆盖现有 client;如需切换 `baseUrl` / `fetch` / `transport`,请先 `await apiClient.close()` 再重新初始化。
110
169
 
111
- ### 配置
112
-
113
- - `baseUrl`:API 基础地址,通常包含 `/api/v1`。
114
- - `auth.storage`:Token 存储适配器;默认 `apiClient.tokenStorage.httpOnlyCookie()`(浏览器推荐);SSR / 测试场景请显式传入 `apiClient.tokenStorage.memory()`。
115
- - `auth.refreshPath`:刷新 token 路径,默认 `/auth/refresh`。
116
- - `timeout`:请求超时,默认 30000ms。
117
- - `headers`:静态或动态公共请求头。
118
- - `fetch`:自定义 fetch 实现。
119
- - `transport.crypto`:启用透明请求/响应加解密,必须传入已初始化的 `@h-ai/crypto` 实例。
120
- - `transport.keyExchangePath`:密钥协商子路径,默认 `/_hai/key-exchange`;会自动拼接到 `baseUrl` 后。
121
- - `apiClient.config`:返回**脱敏后的运行配置快照**;`baseUrl` 的内嵌凭证、`headers.authorization` 等敏感值会自动隐藏。
122
-
123
170
  ### 配置驱动认证页面
124
171
 
125
172
  密码登录只提交 `identifier` 和 `password`;`AuthPanel` 将 `LoginForm` 的 `username` 映射为 `identifier`。会话有效期由服务端 `session.maxAge` / `session.refreshTokenMaxAge` 配置决定,登录表单不提供按次登录的持久会话选项。
@@ -130,31 +177,11 @@ const result = await client.iam.auth.login({ identifier: 'alice', password: 'sec
130
177
 
131
178
  [完整 IAM 配置 / IAM configuration](../iam/README.md)
132
179
 
133
- ### 错误处理
134
-
135
- 业务 API 返回 `HaiResult<T>`:
136
-
137
- ```ts
138
- const result = await apiClient.iam.auth.currentUser()
139
- if (!result.success) {
140
- // result.error.code / result.error.message
141
- }
142
- else {
143
- // result.data = User & { roles: string[], permissions: string[] }
144
- }
145
- ```
146
-
147
- 未初始化、网络错误、超时、401/403/404 等客户端侧问题也会转换为失败的 `HaiResult`。
148
-
149
- 服务端以非 2xx HTTP 状态返回合法的失败 `HaiResult` 时,客户端保留业务错误码、消息和 `traceId`,例如 409 冲突仍可提示刷新后重试。`httpStatus` 以真实 HTTP 响应为准;5xx 的内部诊断仍被隐藏,正文不能通过声称 400 绕过脱敏。非框架响应继续使用客户端错误映射。
150
-
151
- 自动刷新在真实 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`,并处理刷新失败后的登录失效。
152
-
153
- ## 输入约束与 HTTP 能力
180
+ ### 输入约束与 HTTP 能力
154
181
 
155
182
  存储 key 按 UTF-8 最多 1024 字节,并拒绝绝对路径、反斜线、控制字符及点路径段;公开 HTTP 批量删除每次 1–1000 项。`@h-ai/storage/schemas` 提供浏览器安全的共同规则。预签名有效期为整数 1–604800 秒(Node 默认 3600,HTTP 默认 900);HTTP 上传仅接受 key/contentType/expiresIn,不支持 contentLength/maxSize/metadata/checksum。无效输入返回 HTTP 400,typed client 映射 VALIDATION_FAILED。FileUpload 的大小/类型限制仅为前端体验,实际对象须由应用服务端复核。Node 流式读写/checksum/条件写等没有默认 HTTP 端点。完整映射与授权边界见 [存储 API 能力](../storage/README.md#存储输入约束与跨端能力)。
156
183
 
157
- ## 逻辑请求观察
184
+ ### 逻辑请求观察
158
185
 
159
186
  `client.observeRequests` 返回可取消的逻辑请求观察:requestId、procedure、耗时、HTTP 状态、最终 success/errorCode 与实际业务请求 attempts。HTTP 200 的失败 HaiResult 也记为失败,自动刷新后的重试保留同一 requestId,整个逻辑调用只产生一次结果。记录不包含正文、Token 或 URL 查询参数。刷新/密钥协商不伪装成用户业务操作;close 清理本实例的订阅,重新 init 后需重新订阅。
160
187
 
@@ -168,32 +195,64 @@ const unsubscribe = apiClient.observeRequests((record) => {
168
195
  unsubscribe()
169
196
  ```
170
197
 
171
- ## 错误提示与关联日志
172
-
173
- HTTP 错误返回本地化安全文案及追踪编号;ToastContainer 默认隐藏编号,由排查信息图标展开和复制。响应同时包含 `error.traceId` 和 `X-Trace-Id`。框架复用本次 requestId 作为 traceId,前后端日志自动关联。底层异常、堆栈和操作上下文经凭据脱敏后写入日志,不通过 cause/ext 向用户公开。字段校验只返回本地化的字段名和错误。
174
-
175
- `core.error.toPublic(error, traceId?)` 用于接口/UI 边界;`core.error.formatMessage(error)` 用于重新翻译后的提示,重复调用不会重复追加标识。日志传入完整 Error 对象,不能只记录通用标题或提前 String(error) 丢失上下文。数据库失败记录驱动错误码、消息、堆栈及表/列;SQL 原文、绑定值和连接凭据不进入日志。
176
-
177
- ## 可选交互挑战
198
+ ### 可选交互挑战
178
199
 
179
200
  createAuthActions(client) 提供 createCaptcha({operation}),所有登录映射与续期/注册会保留 captcha;sendOtp(identifier,{captcha})、requestPasswordReset(identifier,{captcha}) 接收可选第二参数。框架 AuthPanel 自动求解,自定义表单复用 ALTCHA 协议,证明不进入安全 TokenStorage。
180
201
 
181
202
  策略、默认值和恢复边界见 [IAM](../iam/README.md)。
182
203
 
183
- ## 敏感请求再认证
204
+ ### 敏感请求再认证
184
205
 
185
206
  客户端单次调用的 `options.context.reauthentication` 携带 `reauthenticate` 返回的用途凭据,由请求管线生成 `X-Hai-Reauthentication`。不要写入客户端全局 header、持久存储或日志。服务端同时检查当前权限、资源范围和凭据用途,并原子消费;并发普通请求不会继承该 header。跨域宿主须允许此 header,默认 Serv/Kit 已包含。
186
207
 
187
- ## 原地恢复过期会话
208
+ ### 原地恢复过期会话
188
209
 
189
210
  配置 `auth.recoverSession` 回调可在服务端明确拒绝会话时原地重新登录并保留页面输入。使用 `@h-ai/ui` 的 `createSessionRecovery` / `SessionRecoveryDialog`:根布局绑定公开配置与原用户,回调设为 `recovery.recover`,弹窗使用标准认证 actions,销毁时 `cancel()`。登录的 `expectedUserId` 由 IAM 限定原主体;常规验证码、锁定、改密和登录限制照常执行。只对明确未认证拒绝的原请求续接一次;取消、5xx、超时和结果不明的网络写入不重放。敏感操作证明绑定旧会话时可能需要再次确认,不复用到新会话。未传回调保持原处理方式;配置回调后由恢复流程处理,不再立即调用 onRefreshFailed 导航离开。终端分组通过 auth.terminalType(web/app/desktop/api,默认 web)传入;它不是硬件身份认证。
190
211
 
191
- ## 页面离开会话策略
212
+ ### 页面离开会话策略
192
213
 
193
214
  `session.endOnPageHide` 默认 `false`。启用后宿主调用 `actions.bindPageExit({ enabled: config.session.endOnPageHide })` 并在卸载时调用返回的清理函数;标准 admin/H5/fullstack 模板已经接入。非 BFCache 的 `pagehide`(也包括刷新、整页导航)通过 POST `/auth/session/close` 尽力撤销当前 access 及关联 refresh,沿用 CSRF/传输加密,使用 `keepalive` 且不自动刷新会话。该响应不清 Cookie/本地凭据,避免旧响应覆盖下一次登录。显式退出仍使用正常 logout,可靠撤销并清理当前凭据。浏览器可能不发送页面离开请求,因此必须按业务配合服务端 `idleTimeout`/`absoluteTimeout`;不能据此保证关闭即撤销。
194
215
 
195
216
  敏感调用通过本次 `reauthorize: () => sensitiveActions.authorize(operation)` 重新确认用途:Kit 放 apiFetch 第三参数,typed client 放 options.context。会话刷新/恢复或明确的再认证拒绝后仅续发一次;取消不写入,未提供回调不重放旧证明。
196
217
 
218
+ ## 配置
219
+
220
+ - `baseUrl`:API 基础地址,通常包含 `/api/v1`。
221
+ - `auth.storage`:Token 存储适配器;默认 `apiClient.tokenStorage.httpOnlyCookie()`(浏览器推荐);SSR / 测试场景请显式传入 `apiClient.tokenStorage.memory()`。
222
+ - `auth.refreshPath`:刷新 token 路径,默认 `/auth/refresh`。
223
+ - `timeout`:请求超时,默认 30000ms。
224
+ - `headers`:静态或动态公共请求头。
225
+ - `fetch`:自定义 fetch 实现。
226
+ - `transport.crypto`:启用透明请求/响应加解密,必须传入已初始化的 `@h-ai/crypto` 实例。
227
+ - `transport.keyExchangePath`:密钥协商子路径,默认 `/_hai/key-exchange`;会自动拼接到 `baseUrl` 后。
228
+ - `apiClient.config`:返回**脱敏后的运行配置快照**;`baseUrl` 的内嵌凭证、`headers.authorization` 等敏感值会自动隐藏。
229
+
230
+ ## 错误处理
231
+
232
+ 业务 API 返回 `HaiResult<T>`:
233
+
234
+ ```ts
235
+ const result = await apiClient.iam.auth.currentUser()
236
+ if (!result.success) {
237
+ // result.error.code / result.error.message
238
+ }
239
+ else {
240
+ // result.data = User & { roles: string[], permissions: string[] }
241
+ }
242
+ ```
243
+
244
+ 未初始化、网络错误、超时、401/403/404 等客户端侧问题也会转换为失败的 `HaiResult`。
245
+
246
+ 服务端以非 2xx HTTP 状态返回合法的失败 `HaiResult` 时,客户端保留业务错误码、消息和 `traceId`,例如 409 冲突仍可提示刷新后重试。`httpStatus` 以真实 HTTP 响应为准;5xx 的内部诊断仍被隐藏,正文不能通过声称 400 绕过脱敏。非框架响应继续使用客户端错误映射。
247
+
248
+ 自动刷新在真实 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`,并处理刷新失败后的登录失效。
249
+
250
+ ### 错误提示与关联日志
251
+
252
+ HTTP 错误返回本地化安全文案及追踪编号;ToastContainer 默认隐藏编号,由排查信息图标展开和复制。响应同时包含 `error.traceId` 和 `X-Trace-Id`。框架复用本次 requestId 作为 traceId,前后端日志自动关联。底层异常、堆栈和操作上下文经凭据脱敏后写入日志,不通过 cause/ext 向用户公开。字段校验只返回本地化的字段名和错误。
253
+
254
+ `core.error.toPublic(error, traceId?)` 用于接口/UI 边界;`core.error.formatMessage(error)` 用于重新翻译后的提示,重复调用不会重复追加标识。日志传入完整 Error 对象,不能只记录通用标题或提前 String(error) 丢失上下文。数据库失败记录驱动错误码、消息、堆栈及表/列;SQL 原文、绑定值和连接凭据不进入日志。
255
+
197
256
  ## 开发测试
198
257
 
199
258
  ```bash
@@ -203,7 +262,7 @@ pnpm --filter @h-ai/api-client typecheck
203
262
 
204
263
  客户端默认从服务端握手获取 `transport.maxPlaintextBytes`;可显式设置本地同名字段取较小值。容量失败返回 `HaiApiClientError.PAYLOAD_TOO_LARGE`(`hai:api-client:020`),保留方向、阶段和字节诊断。见 [容量配置与升级](../crypto/TRANSPORT-CAPACITY.md)。
205
264
 
206
- serv/kit 对通过请求绑定、签名和 MAC 校验但超出时间窗口的报文返回加密的 HTTP 400 时间错误;客户端抛 `TransportTimeError`(`hai:crypto:071`),提示同步设备和服务器时间后刷新重试。`api-client` 映射为 `VALIDATION_FAILED`,在 `cause.transportCode` 保留该原因。不会自动重放请求或放宽防重放窗口;篡改或未认证报文仍返回通用认证失败。
265
+ Serv/Kit 对通过请求绑定、签名和 MAC 校验但超出时间窗口的请求返回加密的 HTTP 400 错误。客户端抛出 `TransportTimeError`(`hai:crypto:071`),显示校验时的服务器 UTC 时间,提示开启设备的自动校时或手工修改时间后刷新。`serverTime` 为 Unix 毫秒,旧服务端未提供该值时仍显示校时提示。`api-client` 映射为 `VALIDATION_FAILED`,在 `cause.transportCode` 和 `cause.serverTime` 保留诊断信息。
207
266
 
208
267
  ## 许可证
209
268
 
package/dist/index.d.ts CHANGED
@@ -208,7 +208,7 @@ interface ApiClientConfig {
208
208
  readonly baseUrl: string;
209
209
  /** 认证配置;不传则不启用 Token 注入与自动刷新。 */
210
210
  readonly auth?: AuthConfig;
211
- /** 请求超时(ms),默认 30000。 */
211
+ /** 响应头及完整 JSON 正文期限(ms),默认 30000;自定义流正文由调用方 signal 取消。 */
212
212
  readonly timeout?: number;
213
213
  /** 客户端标识,写入 `x-request-client` 请求头,默认 `hai-api-client`。 */
214
214
  readonly clientName?: string;
@@ -888,7 +888,7 @@ declare function createMemoryTokenStorage(): TokenStorage;
888
888
  /**
889
889
  * 创建浏览器使用的内存 access token 与服务端 httpOnly refresh Cookie 适配器。
890
890
  * refresh getter 返回内部哨兵,setter 不修改 Cookie;clear 只清除内存 access token。
891
- * 刷新请求使用 credentials: include;服务端须实际设置/撤销 Cookie,配置适当的 Secure、SameSite、Path 与跨域凭据策略。
891
+ * 登录和刷新请求使用 credentials: include;服务端须实际设置/撤销 Cookie,配置适当的 Secure、SameSite、Path 与跨域凭据策略。
892
892
  * HttpOnly 限制脚本直接读取 Cookie,不能阻止已执行的恶意脚本代用户发请求;服务端仍须授权和防护 CSRF。
893
893
  * @returns 独立内存 access token 与 Cookie 模式标记;不接触 localStorage。
894
894
  *
@@ -934,7 +934,9 @@ declare const tokenStorage: {
934
934
  * if (!result.success) throw new Error(result.error.message)
935
935
  * const userId = result.data.id
936
936
  * }
937
- * finally { await client.close() }
937
+ * finally {
938
+ * await client.close()
939
+ * }
938
940
  * ```
939
941
  */
940
942
  declare function createApiClient<const TContract extends AnyContractRouter>(contract: TContract): ApiClient<TContract>;
@@ -956,7 +958,9 @@ declare function createApiClient<const TContract extends AnyContractRouter>(cont
956
958
  * const result = await apiClient.iam.auth.currentUser()
957
959
  * if (!result.success) throw new Error(result.error.message)
958
960
  * }
959
- * finally { await apiClient.close() }
961
+ * finally {
962
+ * await apiClient.close()
963
+ * }
960
964
  * ```
961
965
  */
962
966
  declare const defaultApiContract: _h_ai_api_contract.CreatedApiContract<{
@@ -1633,8 +1637,8 @@ declare const defaultApiContract: _h_ai_api_contract.CreatedApiContract<{
1633
1637
  invite: _orpc_contract.ContractProcedureBuilderWithInputOutput<zod.ZodObject<{
1634
1638
  metadata: zod.ZodOptional<zod.ZodRecord<zod.ZodString, zod.ZodUnknown>>;
1635
1639
  email: zod.ZodOptional<zod.ZodString>;
1636
- username: zod.ZodString;
1637
1640
  phone: zod.ZodOptional<zod.ZodString>;
1641
+ username: zod.ZodString;
1638
1642
  displayName: zod.ZodOptional<zod.ZodString>;
1639
1643
  accountType: zod.ZodOptional<zod.ZodEnum<{
1640
1644
  permanent: "permanent";
@@ -2528,9 +2532,9 @@ declare const defaultApiContract: _h_ai_api_contract.CreatedApiContract<{
2528
2532
  }, zod_v4_core.$strict>>>;
2529
2533
  }, zod_v4_core.$strip>;
2530
2534
  finish_reason: zod.ZodEnum<{
2531
- length: "length";
2532
2535
  tool_calls: "tool_calls";
2533
2536
  stop: "stop";
2537
+ length: "length";
2534
2538
  content_filter: "content_filter";
2535
2539
  }>;
2536
2540
  }, zod_v4_core.$strip>>;
@@ -2692,11 +2696,11 @@ declare const defaultApiContract: _h_ai_api_contract.CreatedApiContract<{
2692
2696
  query: zod.ZodString;
2693
2697
  topK: zod.ZodOptional<zod.ZodNumber>;
2694
2698
  types: zod.ZodOptional<zod.ZodArray<zod.ZodEnum<{
2695
- instruction: "instruction";
2696
2699
  fact: "fact";
2697
2700
  preference: "preference";
2698
2701
  event: "event";
2699
2702
  entity: "entity";
2703
+ instruction: "instruction";
2700
2704
  }>>>;
2701
2705
  minImportance: zod.ZodOptional<zod.ZodNumber>;
2702
2706
  objectId: zod.ZodOptional<zod.ZodString>;
@@ -2707,11 +2711,11 @@ declare const defaultApiContract: _h_ai_api_contract.CreatedApiContract<{
2707
2711
  id: zod.ZodString;
2708
2712
  content: zod.ZodString;
2709
2713
  type: zod.ZodEnum<{
2710
- instruction: "instruction";
2711
2714
  fact: "fact";
2712
2715
  preference: "preference";
2713
2716
  event: "event";
2714
2717
  entity: "entity";
2718
+ instruction: "instruction";
2715
2719
  }>;
2716
2720
  importance: zod.ZodNumber;
2717
2721
  objectId: zod.ZodOptional<zod.ZodString>;
@@ -2737,11 +2741,11 @@ declare const defaultApiContract: _h_ai_api_contract.CreatedApiContract<{
2737
2741
  }, zod_v4_core.$strip>], "success">, Record<never, never>, Record<never, never>>;
2738
2742
  list: _orpc_contract.ContractProcedureBuilderWithInputOutput<zod.ZodObject<{
2739
2743
  types: zod.ZodOptional<zod.ZodArray<zod.ZodEnum<{
2740
- instruction: "instruction";
2741
2744
  fact: "fact";
2742
2745
  preference: "preference";
2743
2746
  event: "event";
2744
2747
  entity: "entity";
2748
+ instruction: "instruction";
2745
2749
  }>>>;
2746
2750
  objectId: zod.ZodOptional<zod.ZodString>;
2747
2751
  offset: zod.ZodOptional<zod.ZodNumber>;
@@ -2753,11 +2757,11 @@ declare const defaultApiContract: _h_ai_api_contract.CreatedApiContract<{
2753
2757
  id: zod.ZodString;
2754
2758
  content: zod.ZodString;
2755
2759
  type: zod.ZodEnum<{
2756
- instruction: "instruction";
2757
2760
  fact: "fact";
2758
2761
  preference: "preference";
2759
2762
  event: "event";
2760
2763
  entity: "entity";
2764
+ instruction: "instruction";
2761
2765
  }>;
2762
2766
  importance: zod.ZodNumber;
2763
2767
  objectId: zod.ZodOptional<zod.ZodString>;
@@ -2824,8 +2828,14 @@ declare const defaultApiContract: _h_ai_api_contract.CreatedApiContract<{
2824
2828
  * const client: DefaultApiClient = apiClient
2825
2829
  * const initialized = await client.init({ baseUrl: 'https://api.example.com/api/v1', auth: {} })
2826
2830
  * if (!initialized.success) throw new Error(initialized.error.message)
2827
- * try { const actions = client.createAuthActions(client); const result = await actions.publicConfig(); if (!result.success) throw new Error(result.error.message) }
2828
- * finally { await client.close() }
2831
+ * try {
2832
+ * const actions = client.createAuthActions(client)
2833
+ * const result = await actions.publicConfig()
2834
+ * if (!result.success) throw new Error(result.error.message)
2835
+ * }
2836
+ * finally {
2837
+ * await client.close()
2838
+ * }
2829
2839
  * ```
2830
2840
  */
2831
2841
  type DefaultApiClient = ApiClient<typeof defaultApiContract> & {
@@ -2848,9 +2858,12 @@ type DefaultApiClient = ApiClient<typeof defaultApiContract> & {
2848
2858
  * if (!result.success) throw new Error(result.error.message)
2849
2859
  * const permissions = result.data.permissions
2850
2860
  * }
2851
- * finally { await apiClient.close() }
2861
+ * finally {
2862
+ * await apiClient.close()
2863
+ * }
2852
2864
  * ```
2853
2865
  */
2854
2866
  declare const apiClient: DefaultApiClient;
2855
2867
 
2856
- export { type ApiClient, type ApiClientAuth, type ApiClientConfig, type ApiClientLifecycle, type ApiClientTransportConfig, type ApiRequestObserver, type ApiRequestRecord, type AuthConfig, type DefaultApiClient, HaiApiClientError, type IamAuthLoginInput, type TokenPair, type TokenStorage, apiClient };
2868
+ export { HaiApiClientError, apiClient };
2869
+ export type { ApiClient, ApiClientAuth, ApiClientConfig, ApiClientLifecycle, ApiClientTransportConfig, ApiRequestObserver, ApiRequestRecord, AuthConfig, DefaultApiClient, IamAuthLoginInput, TokenPair, TokenStorage };