@h-ai/api-client 1.0.0-beta.19 → 1.0.0-beta.20
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 +145 -88
- package/README.ja.md +144 -87
- package/README.md +144 -87
- package/package.json +4 -4
package/README.en.md
CHANGED
|
@@ -6,56 +6,119 @@ Requests and token refresh use the current `core.i18n` locale as `Accept-Languag
|
|
|
6
6
|
|
|
7
7
|
[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)
|
|
8
8
|
|
|
9
|
-
## Introduction
|
|
10
|
-
|
|
11
9
|
A cross-platform oRPC/OpenAPI typed client for hai-framework, with consistent API calls and token management across Web, apps, and mini programs.
|
|
12
10
|
|
|
13
|
-
##
|
|
14
|
-
|
|
15
|
-
- `apiClient`: the default typed client for the iam/storage/ai domains.
|
|
16
|
-
- `apiClient.create(contract)`: create a typed client for a custom contract.
|
|
17
|
-
- `apiClient.tokenStorage.{memory,httpOnlyCookie}()`: token storage factories. Browsers keep access tokens in memory and refresh tokens in server-managed httpOnly cookies; native clients inject platform secure storage. Credentials are never written to Web Storage.
|
|
18
|
-
- Custom `fetch` support for browsers, Node, Capacitor, and mini-program bridges.
|
|
19
|
-
- Automatic Bearer token injection, with token refresh and one retry after a 401.
|
|
20
|
-
- Network failures are converted to `HaiResult` errors.
|
|
21
|
-
- Optional transport encryption: `apiClient.init({ transport: { crypto } })` automatically uses `crypto.transport.createClient()`.
|
|
22
|
-
- Transport encryption and automatic refresh can run together: `/auth/refresh` after a 401 still uses encrypted fetch without falling back to plaintext.
|
|
23
|
-
|
|
24
|
-
## Getting started
|
|
25
|
-
|
|
26
|
-
### Quick start
|
|
11
|
+
## Quick Start
|
|
27
12
|
|
|
28
13
|
`close()` invalidates retired refreshes and drains credential writes while preserving external TokenStorage values. After close completes, the same storage may be reused. `timeout` covers headers, the complete JSON body, and every unsuccessful response body, including HTML error pages. Timeout or cancellation prevents token refresh. The built-in Cookie adapter includes credentials for login and refresh, requiring explicit credentialed CORS origins; memory and native storage preserve the original request credentials. Custom application streams remain incremental and their bodies use the caller signal for cancellation.
|
|
29
14
|
|
|
30
15
|
```ts
|
|
16
|
+
import type { IamAuthLoginInput } from '@h-ai/api-client'
|
|
31
17
|
import { apiClient } from '@h-ai/api-client'
|
|
32
18
|
|
|
33
|
-
|
|
34
|
-
baseUrl: '
|
|
35
|
-
|
|
36
|
-
|
|
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
|
+
```
|
|
37
47
|
|
|
38
|
-
|
|
39
|
-
identifier: 'alice',
|
|
40
|
-
password: 'secret',
|
|
41
|
-
})
|
|
48
|
+
## Features
|
|
42
49
|
|
|
43
|
-
|
|
44
|
-
// httpOnly cookie 模式下 refresh token 由服务端 Set-Cookie 管理,
|
|
45
|
-
// api-client 默认存储只需要把 access token 写入内存。
|
|
46
|
-
await apiClient.auth.setTokens(login.data.tokens)
|
|
47
|
-
}
|
|
50
|
+
### Purpose, inputs and outputs
|
|
48
51
|
|
|
49
|
-
|
|
50
|
-
if (me.success) {
|
|
51
|
-
// 身份资料与服务端重新校验后的访问范围一次返回,客户端无需持久化权限快照。
|
|
52
|
-
const { roles, permissions, ...user } = me.data
|
|
53
|
-
}
|
|
52
|
+
Initialize first. Named variables are validated host input; check HaiResult.success. auth and storage Promise<void> operations may reject: surface the failure and handle cleanup faults instead of reporting successful login from a server result alone.
|
|
54
53
|
|
|
55
|
-
|
|
54
|
+
| Entry | Input | Output | Purpose and failure boundary | Example |
|
|
55
|
+
| --------------------------------------- | ------------------------------- | -------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------ |
|
|
56
|
+
| `apiClient.init` | config: baseUrl、认证及传输选项 | `Promise<HaiResult<void>>` | Initialize; stop on failure | `await apiClient.init({ baseUrl: '/api/v1', auth: {} })` |
|
|
57
|
+
| `apiClient.close` | none | `Promise<void>` | Invalidate refresh and release this instance; not server logout | `await apiClient.close()` |
|
|
58
|
+
| `apiClient.create` | shared contract | `ApiClient<TContract>` | Create isolated instances, one per SSR request | `apiClient.create(contract)` |
|
|
59
|
+
| `apiClient.auth.setTokens` | trusted response | `Promise<void>` | Serialize credentials and invalidate old refresh; storage failures reject | `await apiClient.auth.setTokens(tokens)` |
|
|
60
|
+
| `apiClient.auth.clear` | none | `Promise<void>` | Clear local credentials; may reject; server still needs logout | `await apiClient.auth.clear()` |
|
|
61
|
+
| `apiClient.auth.onTokenRefreshed` | callback(TokenPair) | `() => void` | Observe after persistence; returns unsubscribe; never log tokens | `apiClient.auth.onTokenRefreshed(() => refreshProfile())` |
|
|
62
|
+
| `apiClient.tokenStorage.memory` | none | `TokenStorage` | Isolated in-memory adapter for Node/SSR/tests | `apiClient.tokenStorage.memory()` |
|
|
63
|
+
| `apiClient.tokenStorage.httpOnlyCookie` | none | `TokenStorage` | Browser access in memory, refresh in server Cookie | `apiClient.tokenStorage.httpOnlyCookie()` |
|
|
64
|
+
| `apiClient.createAuthActions` | standard IAM contract | `Auth actions object` | Bind auth forms after init with auth enabled | `apiClient.createAuthActions(apiClient)` |
|
|
65
|
+
| `apiClient.observeRequests` | observer(ApiRequestRecord) | `() => void` | Observe sanitized logical outcomes; returns unsubscribe | `apiClient.observeRequests(record => reportOutcome(record))` |
|
|
66
|
+
|
|
67
|
+
#### Authentication form actions
|
|
68
|
+
|
|
69
|
+
`const actions = apiClient.createAuthActions(apiClient)`
|
|
70
|
+
|
|
71
|
+
| Entry | Input | Output | Purpose and failure boundary | Example |
|
|
72
|
+
| ------------------------------ | ----------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------ | -------------------------------------------------------------------- |
|
|
73
|
+
| `actions.publicConfig` | — | `Promise<HaiResult<T>>` → IamPublicConfig | Read public auth policy | `await actions.publicConfig()` |
|
|
74
|
+
| `actions.createCaptcha` | { operation } | `Promise<HaiResult<T>>` → IamCaptchaChallenge | Request an operation challenge | `await actions.createCaptcha({ operation: 'login' })` |
|
|
75
|
+
| `actions.login` | IamAuthLoginInput | `Promise<HaiResult<T>>` → auth result | Map password/ldap/email/sms and await credential persistence | `await actions.login({ identifier, password })` |
|
|
76
|
+
| `actions.register` | IamRegisterInput | `Promise<HaiResult<T>>` → auth result | Register under server policy and save credentials | `await actions.register({ username, password })` |
|
|
77
|
+
| `actions.sendOtp` | identifier, { captcha? }? | `Promise<HaiResult<T>>` → { expiresAt } | Send email/phone OTP; does not log in | `await actions.sendOtp(identifier)` |
|
|
78
|
+
| `actions.requestPasswordReset` | identifier, { captcha? }? | `Promise<HaiResult<T>>` → void | Request reset under server account-enumeration policy | `await actions.requestPasswordReset(identifier)` |
|
|
79
|
+
| `actions.confirmPasswordReset` | token, newPassword | `Promise<HaiResult<T>>` → void | Consume the dedicated reset token | `await actions.confirmPasswordReset(resetToken, newPassword)` |
|
|
80
|
+
| `actions.renewPassword` | { identifier, password, newPassword, captcha? } | `Promise<HaiResult<T>>` → void | Renew an expired password | `await actions.renewPassword({ identifier, password, newPassword })` |
|
|
81
|
+
| `actions.reauthenticate` | password, operation | `Promise<HaiResult<T>>` → purpose ticket | Obtain a short-lived operation-bound credential | `await actions.reauthenticate(password, 'users:delete')` |
|
|
82
|
+
| `actions.bindPageExit` | { enabled? } | `() => void` | Bind best-effort page-exit revocation; returns cleanup | `actions.bindPageExit({ enabled: endOnPageHide })` |
|
|
83
|
+
|
|
84
|
+
#### Secure storage instance
|
|
85
|
+
|
|
86
|
+
`const storage = apiClient.tokenStorage.memory()`
|
|
87
|
+
|
|
88
|
+
| Entry | Input | Output | Purpose and failure boundary | Example |
|
|
89
|
+
| ------------------------- | ----- | ------------------------- | ---------------------------------------------------------------- | --------------------------------------------- |
|
|
90
|
+
| `storage.getAccessToken` | — | `Promise<string \| null>` | Read access; null means absent only | `await storage.getAccessToken()` |
|
|
91
|
+
| `storage.setAccessToken` | token | `Promise<void>` | Save access; bound clients use auth.setTokens | `await storage.setAccessToken(accessToken)` |
|
|
92
|
+
| `storage.getRefreshToken` | — | `Promise<string \| null>` | Cookie mode yields an internal sentinel, not Cookie contents | `await storage.getRefreshToken()` |
|
|
93
|
+
| `storage.setRefreshToken` | token | `Promise<void>` | Memory saves; Cookie adapter does not change server Cookie | `await storage.setRefreshToken(refreshToken)` |
|
|
94
|
+
| `storage.clear` | — | `Promise<void>` | Clear adapter credentials; failures reject; no server revocation | `await storage.clear()` |
|
|
95
|
+
|
|
96
|
+
#### Default business domains
|
|
97
|
+
|
|
98
|
+
Default domains mirror HTTP contracts in api-contract, not the complete server module objects. A custom client contains only the supplied contract.
|
|
99
|
+
|
|
100
|
+
- `apiClient.iam`: IAM login, registration, users, roles and permissions — [API Contract](../api-contract/README.md), [IAM](../iam/README.md).
|
|
101
|
+
- `apiClient.storage`: Storage listing, metadata, presigning and deletion — [API Contract](../api-contract/README.md), [Storage](../storage/README.md).
|
|
102
|
+
- `apiClient.ai`: Non-streaming AI chat — [API Contract](../api-contract/README.md), [AI](../ai/README.md).
|
|
103
|
+
|
|
104
|
+
Follow the actual contract for arrays, permissions and business errors. The server authorizes identity/resources; client settings grant no permission.
|
|
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)
|
|
56
117
|
```
|
|
57
118
|
|
|
58
|
-
|
|
119
|
+
`apiClient.config` is a redacted runtime snapshot (null before initialization), not a source for restoring secrets. `apiClient.isInitialized` is the ready boolean. Both are read-only properties and make no network request.
|
|
120
|
+
|
|
121
|
+
### Enable transport encryption
|
|
59
122
|
|
|
60
123
|
First enable `serv.createApp({ transport: { crypto, serverKeyRing, keyStore } })` on the server. The client only needs the same `@h-ai/crypto` service instance; no manual key exchange code is required.
|
|
61
124
|
|
|
@@ -98,30 +161,12 @@ await client.init({ baseUrl: 'https://api.example.com/api/v1' })
|
|
|
98
161
|
const result = await client.iam.auth.login({ identifier: 'alice', password: 'secret' })
|
|
99
162
|
```
|
|
100
163
|
|
|
101
|
-
###
|
|
102
|
-
|
|
103
|
-
- `apiClient.init(config)`: initialize the default client.
|
|
104
|
-
- `apiClient.close()`: clear client state.
|
|
105
|
-
- `apiClient.auth.setTokens(tokens)`: replace credentials and store the access token; non-httpOnly storage saves the supplied refresh token or clears its previous value when omitted.
|
|
106
|
-
- `apiClient.auth.clear()`: clear tokens.
|
|
107
|
-
- `apiClient.create(contract)`: create a custom typed client.
|
|
164
|
+
### Credential changes and closing
|
|
108
165
|
|
|
109
166
|
`auth.clear()` and `auth.setTokens()` immediately cancel and invalidate this client's previous refresh request. A late success or 401 cannot restore a logged-out session, overwrite a new account, or notify stale refresh callbacks. Storage writes are serialized, so an earlier asynchronous write cannot overwrite credentials after either method completes. Use these methods when logging out or switching accounts instead of writing to storage directly. The server logout endpoint must still revoke the httpOnly cookie and server session.
|
|
110
167
|
|
|
111
168
|
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.
|
|
112
169
|
|
|
113
|
-
### Configuration
|
|
114
|
-
|
|
115
|
-
- `baseUrl`: API base URL, usually including `/api/v1`.
|
|
116
|
-
- `auth.storage`: token storage adapter; defaults to `apiClient.tokenStorage.httpOnlyCookie()` (recommended for browsers). Explicitly use `apiClient.tokenStorage.memory()` for SSR / tests.
|
|
117
|
-
- `auth.refreshPath`: token refresh path; defaults to `/auth/refresh`.
|
|
118
|
-
- `timeout`: request timeout; defaults to 30000 ms.
|
|
119
|
-
- `headers`: static or dynamic shared request headers.
|
|
120
|
-
- `fetch`: custom fetch implementation.
|
|
121
|
-
- `transport.crypto`: transparent request/response encryption and decryption; requires an initialized `@h-ai/crypto` instance.
|
|
122
|
-
- `transport.keyExchangePath`: key exchange subpath; defaults to `/_hai/key-exchange` and is appended to `baseUrl` automatically.
|
|
123
|
-
- `apiClient.config`: a **redacted snapshot of runtime configuration**; credentials embedded in `baseUrl`, `headers.authorization`, and other sensitive values are hidden automatically.
|
|
124
|
-
|
|
125
170
|
### Configuration driven authentication
|
|
126
171
|
|
|
127
172
|
Password login submits only `identifier` and `password`; `AuthPanel` maps the `LoginForm` field `username` to `identifier`. Server settings `session.maxAge` / `session.refreshTokenMaxAge` control session lifetimes. The login form has no per-login persistent-session option.
|
|
@@ -132,31 +177,11 @@ apiClient.createAuthActions(client) supplies publicConfig, login, register, send
|
|
|
132
177
|
|
|
133
178
|
[完整 IAM 配置 / IAM configuration](../iam/README.en.md)
|
|
134
179
|
|
|
135
|
-
###
|
|
136
|
-
|
|
137
|
-
Business APIs return `HaiResult<T>`:
|
|
138
|
-
|
|
139
|
-
```ts
|
|
140
|
-
const result = await apiClient.iam.auth.currentUser()
|
|
141
|
-
if (!result.success) {
|
|
142
|
-
// result.error.code / result.error.message
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
// result.data = User & { roles: string[], permissions: string[] }
|
|
146
|
-
}
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
Client-side failures such as missing initialization, network errors, timeouts, and 401/403/404 responses are also converted to failed `HaiResult` values.
|
|
150
|
-
|
|
151
|
-
A valid failed `HaiResult` returned with a non-2xx HTTP status retains its business error code, message and `traceId`; a 409 conflict can therefore explain how to retry. The actual HTTP response determines `httpStatus`. Internal 5xx diagnostics remain hidden even if the body claims status 400. Other response formats continue to use client error mappings.
|
|
152
|
-
|
|
153
|
-
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.
|
|
154
|
-
|
|
155
|
-
## Input limits and HTTP capabilities
|
|
180
|
+
### Input limits and HTTP capabilities
|
|
156
181
|
|
|
157
182
|
Object keys are limited to 1024 UTF-8 bytes and reject absolute paths, backslashes, control characters and dot segments. Each public HTTP batch delete accepts 1–1000 entries. `@h-ai/storage/schemas` exposes the shared browser-safe rules. Presign expiry is an integer from 1 to 604800 seconds (Node default 3600, HTTP default 900). Upload signing accepts only key/contentType/expiresIn; contentLength/maxSize/metadata/checksum are unsupported. Invalid input returns HTTP 400 and the typed client maps it to VALIDATION_FAILED. FileUpload limits guide file selection; applications must validate actual objects on the server. Node streaming/checksum/conditional-write options have no default HTTP endpoints. See the [capability and authorization matrix (Chinese)](../storage/README.md#存储输入约束与跨端能力).
|
|
158
183
|
|
|
159
|
-
|
|
184
|
+
### Logical request observation
|
|
160
185
|
|
|
161
186
|
`client.observeRequests` observes each logical call once and returns an unsubscribe function. Records contain requestId, procedure, duration, HTTP status, final success/errorCode, and business request attempts. A failed HaiResult with HTTP 200 is a failure. Authentication retries retain the same requestId. Records exclude bodies, tokens, and URL query parameters. Refresh and key exchange are not presented as user operations. close clears subscriptions, so subscribe again after reinitializing.
|
|
162
187
|
|
|
@@ -170,33 +195,65 @@ const unsubscribe = apiClient.observeRequests((record) => {
|
|
|
170
195
|
unsubscribe()
|
|
171
196
|
```
|
|
172
197
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
HTTP errors carry safe localized messages and a reference ID. ToastContainer hides the ID by default and reveals it through its diagnostic icon. The same ID remains in `error.traceId` and `X-Trace-Id`. The framework reuses the request ID to correlate browser and server logs. Sanitized exceptions, stacks and operation context remain in logs; internal cause/ext are not exposed. Field validation returns localized field names and messages only.
|
|
176
|
-
|
|
177
|
-
Use `core.error.toPublic(error, traceId?)` at HTTP/UI boundaries and `core.error.formatMessage(error)` after translating messages; formatting is idempotent. Pass complete Error objects to logs instead of discarding their context. Database failures log driver codes, sanitized messages/stacks and table/column context; SQL text, bound values and connection credentials are excluded.
|
|
178
|
-
|
|
179
|
-
## Optional interactive challenges
|
|
198
|
+
### Optional interactive challenges
|
|
180
199
|
|
|
181
200
|
createAuthActions(client) supplies createCaptcha({operation}) and preserves captcha in login mappings, renewal and registration. sendOtp(identifier,{captcha}) and requestPasswordReset(identifier,{captcha}) take an optional second argument. AuthPanel solves automatically; custom forms reuse ALTCHA without persisting proofs in TokenStorage.
|
|
182
201
|
|
|
183
202
|
See policy, defaults and recovery boundaries in [IAM](../iam/README.en.md).
|
|
184
203
|
|
|
185
|
-
|
|
204
|
+
### Sensitive request reauthentication
|
|
186
205
|
|
|
187
206
|
Pass the purpose-bound proof returned by `reauthenticate` through the individual call’s `options.context.reauthentication`; the request pipeline emits `X-Hai-Reauthentication`. Never place it in global client headers, persistent storage or logs. The server checks current permissions, resource scope and proof purpose before atomic consumption. Concurrent ordinary calls do not inherit the header. Cross-origin hosts must allow it; Serv/Kit defaults already include it.
|
|
188
207
|
|
|
189
|
-
|
|
208
|
+
### Recovering an expired session in place
|
|
190
209
|
|
|
191
210
|
Set `auth.recoverSession` to sign in again without discarding the current page. Use UI `createSessionRecovery` / `SessionRecoveryDialog`: configure the public policy and original user in the root layout, pass `recovery.recover` as the callback, provide standard authentication actions, and cancel on teardown. IAM checks login `expectedUserId` before issuing a session; captcha, lockout, password renewal and access rules remain active. Only an explicitly authentication-rejected request is retried once. Cancellation, 5xx, timeouts and ambiguous writes are not replayed. Sensitive-operation proofs from an old session may need a new confirmation. Without the callback, behavior is unchanged; with it, recovery handles failure instead of immediate onRefreshFailed navigation. auth.terminalType (web/app/desktop/api; default web) selects a session group and does not attest hardware identity.
|
|
192
211
|
|
|
193
|
-
|
|
212
|
+
### Page exit session policy
|
|
194
213
|
|
|
195
214
|
`session.endOnPageHide` defaults to `false`. Hosts call `actions.bindPageExit({ enabled: config.session.endOnPageHide })` and invoke its returned cleanup on unmount; standard admin/H5/fullstack templates already bind it. A non-BFCache `pagehide` (including reload and full document navigation) makes a best effort POST to `/auth/session/close`, revoking the held access and related refresh through the existing CSRF/encrypted transport with `keepalive` and no automatic refresh. This response does not clear cookies/local credentials, avoiding a late old response overwriting a new login. Explicit logout still revokes and clears current credentials normally. Browsers may omit page exit requests; configure server `idleTimeout`/`absoluteTimeout` as appropriate. Immediate revocation on browser close is not guaranteed.
|
|
196
215
|
|
|
197
216
|
For a sensitive call, supply `reauthorize: () => sensitiveActions.authorize(operation)` in the third Kit apiFetch argument or typed-client options.context. After session rotation/recovery or an explicit proof rejection, obtain a new purpose-bound proof before one retry. Cancellation sends no further write; without a callback, an old proof is never replayed.
|
|
198
217
|
|
|
199
|
-
##
|
|
218
|
+
## Configuration
|
|
219
|
+
|
|
220
|
+
- `baseUrl`: API base URL, usually including `/api/v1`.
|
|
221
|
+
- `auth.storage`: token storage adapter; defaults to `apiClient.tokenStorage.httpOnlyCookie()` (recommended for browsers). Explicitly use `apiClient.tokenStorage.memory()` for SSR / tests.
|
|
222
|
+
- `auth.refreshPath`: token refresh path; defaults to `/auth/refresh`.
|
|
223
|
+
- `timeout`: request timeout; defaults to 30000 ms.
|
|
224
|
+
- `headers`: static or dynamic shared request headers.
|
|
225
|
+
- `fetch`: custom fetch implementation.
|
|
226
|
+
- `transport.crypto`: transparent request/response encryption and decryption; requires an initialized `@h-ai/crypto` instance.
|
|
227
|
+
- `transport.keyExchangePath`: key exchange subpath; defaults to `/_hai/key-exchange` and is appended to `baseUrl` automatically.
|
|
228
|
+
- `apiClient.config`: a **redacted snapshot of runtime configuration**; credentials embedded in `baseUrl`, `headers.authorization`, and other sensitive values are hidden automatically.
|
|
229
|
+
|
|
230
|
+
## Error Handling
|
|
231
|
+
|
|
232
|
+
Business APIs return `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
|
+
Client-side failures such as missing initialization, network errors, timeouts, and 401/403/404 responses are also converted to failed `HaiResult` values.
|
|
245
|
+
|
|
246
|
+
A valid failed `HaiResult` returned with a non-2xx HTTP status retains its business error code, message and `traceId`; a 409 conflict can therefore explain how to retry. The actual HTTP response determines `httpStatus`. Internal 5xx diagnostics remain hidden even if the body claims status 400. Other response formats continue to use client error mappings.
|
|
247
|
+
|
|
248
|
+
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.
|
|
249
|
+
|
|
250
|
+
### Error messages and correlated logs
|
|
251
|
+
|
|
252
|
+
HTTP errors carry safe localized messages and a reference ID. ToastContainer hides the ID by default and reveals it through its diagnostic icon. The same ID remains in `error.traceId` and `X-Trace-Id`. The framework reuses the request ID to correlate browser and server logs. Sanitized exceptions, stacks and operation context remain in logs; internal cause/ext are not exposed. Field validation returns localized field names and messages only.
|
|
253
|
+
|
|
254
|
+
Use `core.error.toPublic(error, traceId?)` at HTTP/UI boundaries and `core.error.formatMessage(error)` after translating messages; formatting is idempotent. Pass complete Error objects to logs instead of discarding their context. Database failures log driver codes, sanitized messages/stacks and table/column context; SQL text, bound values and connection credentials are excluded.
|
|
255
|
+
|
|
256
|
+
## Development
|
|
200
257
|
|
|
201
258
|
```bash
|
|
202
259
|
pnpm --filter @h-ai/api-client test
|
package/README.ja.md
CHANGED
|
@@ -6,56 +6,119 @@
|
|
|
6
6
|
|
|
7
7
|
[フレームワークのホーム](../../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)
|
|
8
8
|
|
|
9
|
-
## 概要
|
|
10
|
-
|
|
11
9
|
hai-framework のクロスプラットフォーム oRPC/OpenAPI 型付きクライアント。Web、アプリ、ミニプログラムに共通の API 呼び出しと Token 管理を提供します。
|
|
12
10
|
|
|
13
|
-
##
|
|
14
|
-
|
|
15
|
-
- `apiClient`:iam/storage/ai ドメインに対応する既定の型付きクライアント。
|
|
16
|
-
- `apiClient.create(contract)`:独自の contract に対応する型付きクライアントを作成。
|
|
17
|
-
- `apiClient.tokenStorage.{memory,httpOnlyCookie}()`:Token ストレージのファクトリー。ブラウザーでは access token をメモリー、refresh token をサーバー管理の httpOnly Cookie に保持し、ネイティブではプラットフォームの安全なストレージを注入します。認証情報を Web Storage に保存しません。
|
|
18
|
-
- ブラウザー、Node、Capacitor、ミニプログラムのブリッジに対応する独自 `fetch` を指定可能。
|
|
19
|
-
- Bearer Token の自動付与と、401 応答後の更新および 1 回の再試行。
|
|
20
|
-
- ネットワークエラーを `HaiResult` のエラーに変換。
|
|
21
|
-
- 任意の通信暗号化:`apiClient.init({ transport: { crypto } })` が `crypto.transport.createClient()` を自動使用。
|
|
22
|
-
- 通信暗号化と自動更新を併用可能。401 後の `/auth/refresh` も暗号化された fetch を使用し、平文には切り替わりません。
|
|
23
|
-
|
|
24
|
-
## 使い始める
|
|
25
|
-
|
|
26
|
-
### クイックスタート
|
|
11
|
+
## クイックスタート
|
|
27
12
|
|
|
28
13
|
`close()` は旧更新を無効化し、進行中の認証情報書き込みを待ちます。外部 TokenStorage の値は保持され、終了後に再利用できます。`timeout` はヘッダー、JSON 本文全体、および HTML エラーページを含む全失敗応答の本文が対象です。タイムアウトや取消後は Token を更新しません。内蔵 Cookie アダプターはログインと更新で認証情報を送るため、CORS の Origin と credentials を設定します。メモリー・ネイティブ保存は元の要求の方針を維持します。独自ストリームは逐次読み取り、本文の中断には呼出側の signal を使います。
|
|
29
14
|
|
|
30
15
|
```ts
|
|
16
|
+
import type { IamAuthLoginInput } from '@h-ai/api-client'
|
|
31
17
|
import { apiClient } from '@h-ai/api-client'
|
|
32
18
|
|
|
33
|
-
|
|
34
|
-
baseUrl: '
|
|
35
|
-
|
|
36
|
-
|
|
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
|
+
```
|
|
37
47
|
|
|
38
|
-
|
|
39
|
-
identifier: 'alice',
|
|
40
|
-
password: 'secret',
|
|
41
|
-
})
|
|
48
|
+
## 機能
|
|
42
49
|
|
|
43
|
-
|
|
44
|
-
// httpOnly cookie 模式下 refresh token 由服务端 Set-Cookie 管理,
|
|
45
|
-
// api-client 默认存储只需要把 access token 写入内存。
|
|
46
|
-
await apiClient.auth.setTokens(login.data.tokens)
|
|
47
|
-
}
|
|
50
|
+
### 用途・入力・出力
|
|
48
51
|
|
|
49
|
-
|
|
50
|
-
if (me.success) {
|
|
51
|
-
// 身份资料与服务端重新校验后的访问范围一次返回,客户端无需持久化权限快照。
|
|
52
|
-
const { roles, permissions, ...user } = me.data
|
|
53
|
-
}
|
|
52
|
+
先に初期化します。名前付き変数は検証済みホスト入力、HaiResult.success を確認します。auth/保存の Promise<void> は reject し得るため、失敗と消去障害を処理し、サーバー成功だけでログイン完了を表示しません。
|
|
54
53
|
|
|
55
|
-
|
|
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` | 共有契約 | `ApiClient<TContract>` | 独立インスタンス。SSR は要求ごとに作成 | `apiClient.create(contract)` |
|
|
59
|
+
| `apiClient.auth.setTokens` | 検証済み応答 | `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` | 標準 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>>` → 認証結果 | 方式を選び資格情報保存の成功を待つ | `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 } | メール/電話 OTP を送信。ログインとは別 | `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 モードは内部番兵で 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 は渡した契約のみを持ちます。
|
|
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)
|
|
56
117
|
```
|
|
57
118
|
|
|
58
|
-
|
|
119
|
+
`apiClient.config` は秘匿済み実行設定(未初期化時 null)で、秘密の復元には使えません。`apiClient.isInitialized` は準備完了 boolean。どちらも読取専用で通信は発生しません。
|
|
120
|
+
|
|
121
|
+
### 通信暗号化の有効化
|
|
59
122
|
|
|
60
123
|
サーバーで先に `serv.createApp({ transport: { crypto, serverKeyRing, keyStore } })` を有効にします。クライアントには同じ `@h-ai/crypto` サービスのインスタンスを渡すだけで、鍵交換コードの実装は不要です。
|
|
61
124
|
|
|
@@ -98,30 +161,12 @@ await client.init({ baseUrl: 'https://api.example.com/api/v1' })
|
|
|
98
161
|
const result = await client.iam.auth.login({ identifier: 'alice', password: 'secret' })
|
|
99
162
|
```
|
|
100
163
|
|
|
101
|
-
###
|
|
102
|
-
|
|
103
|
-
- `apiClient.init(config)`:既定のクライアントを初期化。
|
|
104
|
-
- `apiClient.close()`:クライアントの状態を解放。
|
|
105
|
-
- `apiClient.auth.setTokens(tokens)`:認証情報を置き換えて access token を保存。httpOnly 以外では指定された refresh token も保存し、省略時は古い値を削除します。
|
|
106
|
-
- `apiClient.auth.clear()`:Token を削除。
|
|
107
|
-
- `apiClient.create(contract)`:独自の型付きクライアントを作成。
|
|
164
|
+
### 資格情報の更新と終了
|
|
108
165
|
|
|
109
166
|
`auth.clear()` と `auth.setTokens()` は、このクライアントの既存の更新リクエストを直ちにキャンセルして無効化します。遅れて届いた成功応答や 401 は、ログアウト済みの認証状態を復元したり、新しいアカウントを上書きしたり、古い更新コールバックを通知したりしません。ストレージへの書き込みは直列化され、メソッド完了後に以前の非同期書き込みが認証情報を上書きすることはありません。ログアウトとアカウント切り替えには、ストレージを直接変更せずこれらのメソッドを使ってください。httpOnly Cookie とサーバーセッションは、引き続きサーバーのログアウトエンドポイントで失効させる必要があります。
|
|
110
167
|
|
|
111
168
|
初期化済みのクライアントで `init()` を再度呼ぶと `CONFIG_ERROR` が返り、既存のクライアントは維持されます。`baseUrl` / `fetch` / `transport` を変更する場合は、先に `await apiClient.close()` を呼んでから再初期化してください。
|
|
112
169
|
|
|
113
|
-
### 設定
|
|
114
|
-
|
|
115
|
-
- `baseUrl`:API のベース URL。通常は `/api/v1` を含みます。
|
|
116
|
-
- `auth.storage`:Token ストレージアダプター。既定は `apiClient.tokenStorage.httpOnlyCookie()`(ブラウザー推奨)。SSR / テストでは `apiClient.tokenStorage.memory()` を明示してください。
|
|
117
|
-
- `auth.refreshPath`:Token 更新パス。既定は `/auth/refresh`。
|
|
118
|
-
- `timeout`:リクエストのタイムアウト。既定は 30000 ms。
|
|
119
|
-
- `headers`:静的または動的な共通リクエストヘッダー。
|
|
120
|
-
- `fetch`:独自の fetch 実装。
|
|
121
|
-
- `transport.crypto`:透過的なリクエスト/レスポンス暗号化・復号。初期化済みの `@h-ai/crypto` インスタンスが必要です。
|
|
122
|
-
- `transport.keyExchangePath`:鍵交換のサブパス。既定は `/_hai/key-exchange` で、`baseUrl` に自動的に連結されます。
|
|
123
|
-
- `apiClient.config`:**機密情報をマスクした実行時設定のスナップショット**。`baseUrl` 内の認証情報や `headers.authorization` などの機密値は自動的に非表示になります。
|
|
124
|
-
|
|
125
170
|
### 設定による認証画面
|
|
126
171
|
|
|
127
172
|
パスワードログインの送信項目は `identifier` と `password` のみです。`AuthPanel` は `LoginForm` の `username` を `identifier` に変換します。有効期間はサーバーの `session.maxAge` / `session.refreshTokenMaxAge` で設定し、フォームにログインごとの永続セッション選択肢はありません。
|
|
@@ -132,31 +177,11 @@ const result = await client.iam.auth.login({ identifier: 'alice', password: 'sec
|
|
|
132
177
|
|
|
133
178
|
[完整 IAM 配置 / IAM configuration](../iam/README.ja.md)
|
|
134
179
|
|
|
135
|
-
###
|
|
136
|
-
|
|
137
|
-
業務 API は `HaiResult<T>` を返します:
|
|
138
|
-
|
|
139
|
-
```ts
|
|
140
|
-
const result = await apiClient.iam.auth.currentUser()
|
|
141
|
-
if (!result.success) {
|
|
142
|
-
// result.error.code / result.error.message
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
// result.data = User & { roles: string[], permissions: string[] }
|
|
146
|
-
}
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
未初期化、ネットワークエラー、タイムアウト、401/403/404 などのクライアント側の問題も、失敗の `HaiResult` に変換されます。
|
|
150
|
-
|
|
151
|
-
非 2xx の HTTP 応答に有効な失敗 `HaiResult` が含まれる場合、業務エラーコード、メッセージ、`traceId` を保持します。409 の競合では再読み込みなどの対処方法を表示できます。`httpStatus` は実際の HTTP 応答を優先し、本文が 400 を指定しても 5xx の内部情報は公開しません。それ以外の応答形式はクライアント側のエラーに変換します。
|
|
152
|
-
|
|
153
|
-
自動更新は実際の 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` を確認し、更新失敗による認証失効を処理してください。
|
|
154
|
-
|
|
155
|
-
## 入力制約と HTTP 機能
|
|
180
|
+
### 入力制約と HTTP 機能
|
|
156
181
|
|
|
157
182
|
キーは UTF-8 で最大 1024 バイトで、絶対パス、バックスラッシュ、制御文字、ドットセグメントを拒否します。公開 HTTP 一括削除は一度に 1–1000 件です。共通規則はブラウザー対応の `@h-ai/storage/schemas` から参照できます。署名有効期限は整数 1–604800 秒(Node 既定 3600、HTTP 既定 900)。アップロード署名の入力は key/contentType/expiresIn のみで、contentLength/maxSize/metadata/checksum は未対応です。不正入力は HTTP 400、typed client では VALIDATION_FAILED です。FileUpload の制限は選択時の補助であり、実際のオブジェクトはサーバーで検証してください。Node のストリーム/checksum/条件付き書き込みには既定の HTTP エンドポイントがありません。[機能と認可の対応表(中国語)](../storage/README.md#存储输入约束与跨端能力)を参照してください。
|
|
158
183
|
|
|
159
|
-
|
|
184
|
+
### 論理リクエストの監視
|
|
160
185
|
|
|
161
186
|
`client.observeRequests` は論理呼び出しごとに一度通知し、解除関数を返します。requestId、procedure、所要時間、HTTP 状態、最終 success/errorCode、業務リクエストの attempts を含みます。HTTP 200 の失敗 HaiResult も失敗として扱い、認証更新後の再試行も同じ requestId を保持します。本文・Token・URL クエリーは含みません。認証更新や鍵交換をユーザー操作としません。close は購読を解除するため、再初期化後は再購読してください。
|
|
162
187
|
|
|
@@ -170,32 +195,64 @@ const unsubscribe = apiClient.observeRequests((record) => {
|
|
|
170
195
|
unsubscribe()
|
|
171
196
|
```
|
|
172
197
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
HTTP エラーには安全な翻訳文と追跡番号を含めます。ToastContainer は番号を既定で非表示にし、調査情報のアイコンから表示・コピーできます。同じ ID を error.traceId と X-Trace-Id に返します。requestId を traceId として共有し、ブラウザーとサーバーのログを関連付けます。脱機密化した例外・スタック・操作情報はログに残し、内部 cause/ext は公開しません。項目検証は翻訳済みの項目名とメッセージだけを返します。
|
|
176
|
-
|
|
177
|
-
HTTP/UI 境界には core.error.toPublic(error, traceId?)、再翻訳後の表示には core.error.formatMessage(error) を使い、ID を重複追加しません。ログには完全な Error を渡してください。DB ログにはドライバーコード、脱機密化したメッセージ・スタック、表・列を残し、SQL 本文、バインド値と接続認証情報を記録しません。
|
|
178
|
-
|
|
179
|
-
## 任意の対話チャレンジ
|
|
198
|
+
### 任意の対話チャレンジ
|
|
180
199
|
|
|
181
200
|
createAuthActions(client) は createCaptcha({operation}) を提供し、ログイン・更新・登録で captcha を保持します。sendOtp(identifier,{captcha}) と requestPasswordReset(identifier,{captcha}) は任意の第2引数に対応します。AuthPanel は自動求解し、独自フォームも ALTCHA 手順を使い、証明を TokenStorage に保存しません。
|
|
182
201
|
|
|
183
202
|
方針・既定値・復旧範囲は次を参照: [IAM](../iam/README.ja.md)。
|
|
184
203
|
|
|
185
|
-
|
|
204
|
+
### 機密操作の再認証
|
|
186
205
|
|
|
187
206
|
`reauthenticate` が返す用途限定証明を、個別呼び出しの `options.context.reauthentication` に渡すと `X-Hai-Reauthentication` が付きます。グローバルヘッダー、永続保存、ログには入れません。サーバーは現在の権限、対象範囲、用途を確認して原子的に消費し、並行する通常リクエストには引き継ぎません。クロスオリジン環境ではヘッダーを許可してください。Serv/Kit の既定設定には含まれます。
|
|
188
207
|
|
|
189
|
-
|
|
208
|
+
### 期限切れセッションの画面内復帰
|
|
190
209
|
|
|
191
210
|
`auth.recoverSession` によって現在の画面と入力を保持して再ログインできます。UI の `createSessionRecovery` / `SessionRecoveryDialog` を使い、ルートで公開方針と元の利用者を設定し、`recovery.recover` と標準認証 actions を渡して破棄時に cancel します。IAM は発行前に `expectedUserId` を確認し、通常の CAPTCHA・ロック・改密・アクセス規則を適用します。認証拒否が明確な要求だけ1回続行でき、取消・5xx・タイムアウト・結果不明の書込は再送しません。旧セッションの重要操作証明は再確認が必要な場合があります。コールバックなしは従来動作で、有効時は即時の onRefreshFailed 遷移を復帰処理が置き換えます。auth.terminalType(web/app/desktop/api、既定 web)は分類用で機器認証ではありません。
|
|
192
211
|
|
|
193
|
-
|
|
212
|
+
### ページ離脱時のセッション方針
|
|
194
213
|
|
|
195
214
|
`session.endOnPageHide` の既定値は `false` です。ホストは `actions.bindPageExit({ enabled: config.session.endOnPageHide })` を呼び、破棄時に戻り値の解除関数を実行します。標準 admin/H5/fullstack テンプレートは接続済みです。BFCache 以外の `pagehide`(再読み込み、文書全体の移動を含む)で POST `/auth/session/close` を最善努力で送信し、保持する access と関連 refresh を失効します。既存の CSRF/暗号化転送を使用し、`keepalive` を付け、自動更新しません。古い応答が新しいログインを上書きしないよう Cookie/ローカル資格情報を消去しません。明示的ログアウトは通常どおり失効と消去を行います。ブラウザーが離脱要求を送らない場合もあるため、業務に応じてサーバーの `idleTimeout`/`absoluteTimeout` を併用してください。閉じた瞬間の失効は保証できません。
|
|
196
215
|
|
|
197
216
|
重要操作では `reauthorize: () => sensitiveActions.authorize(operation)` を Kit apiFetch の第3引数、または typed client の options.context に渡します。セッション更新・復帰または明確な証明拒否の後、新しい用途証明で1回だけ続行します。取消時は追加の書込を送らず、コールバックがなければ旧証明を再送しません。
|
|
198
217
|
|
|
218
|
+
## 設定
|
|
219
|
+
|
|
220
|
+
- `baseUrl`:API のベース URL。通常は `/api/v1` を含みます。
|
|
221
|
+
- `auth.storage`:Token ストレージアダプター。既定は `apiClient.tokenStorage.httpOnlyCookie()`(ブラウザー推奨)。SSR / テストでは `apiClient.tokenStorage.memory()` を明示してください。
|
|
222
|
+
- `auth.refreshPath`:Token 更新パス。既定は `/auth/refresh`。
|
|
223
|
+
- `timeout`:リクエストのタイムアウト。既定は 30000 ms。
|
|
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 応答を優先し、本文が 400 を指定しても 5xx の内部情報は公開しません。それ以外の応答形式はクライアント側のエラーに変換します。
|
|
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 は番号を既定で非表示にし、調査情報のアイコンから表示・コピーできます。同じ ID を error.traceId と X-Trace-Id に返します。requestId を traceId として共有し、ブラウザーとサーバーのログを関連付けます。脱機密化した例外・スタック・操作情報はログに残し、内部 cause/ext は公開しません。項目検証は翻訳済みの項目名とメッセージだけを返します。
|
|
253
|
+
|
|
254
|
+
HTTP/UI 境界には core.error.toPublic(error, traceId?)、再翻訳後の表示には core.error.formatMessage(error) を使い、ID を重複追加しません。ログには完全な Error を渡してください。DB ログにはドライバーコード、脱機密化したメッセージ・スタック、表・列を残し、SQL 本文、バインド値と接続認証情報を記録しません。
|
|
255
|
+
|
|
199
256
|
## 開発とテスト
|
|
200
257
|
|
|
201
258
|
```bash
|
package/README.md
CHANGED
|
@@ -6,56 +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,不降级明文。
|
|
23
|
-
|
|
24
|
-
## 使用入门
|
|
25
|
-
|
|
26
|
-
### 快速开始
|
|
11
|
+
## 快速开始
|
|
27
12
|
|
|
28
13
|
`close()` 会使旧刷新失效并等待已有凭据写入完成,保留外置 TokenStorage 当前值;关闭完成后可复用同一存储重新初始化。`timeout` 覆盖响应头、完整 JSON 正文及所有非成功响应正文(包括 HTML 错误页);超时或取消后不再刷新凭据。内置 Cookie 适配器的登录和刷新使用 `credentials: include`,跨域服务须明确允许凭据与 Origin;内存和原生存储保留原请求的凭据策略。应用自定义流按需读取,其正文由调用方 signal 取消。
|
|
29
14
|
|
|
30
15
|
```ts
|
|
16
|
+
import type { IamAuthLoginInput } from '@h-ai/api-client'
|
|
31
17
|
import { apiClient } from '@h-ai/api-client'
|
|
32
18
|
|
|
33
|
-
|
|
34
|
-
baseUrl: '
|
|
35
|
-
|
|
36
|
-
|
|
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
|
+
```
|
|
37
47
|
|
|
38
|
-
|
|
39
|
-
identifier: 'alice',
|
|
40
|
-
password: 'secret',
|
|
41
|
-
})
|
|
48
|
+
## 功能
|
|
42
49
|
|
|
43
|
-
|
|
44
|
-
// httpOnly cookie 模式下 refresh token 由服务端 Set-Cookie 管理,
|
|
45
|
-
// api-client 默认存储只需要把 access token 写入内存。
|
|
46
|
-
await apiClient.auth.setTokens(login.data.tokens)
|
|
47
|
-
}
|
|
50
|
+
### 用途、输入与输出
|
|
48
51
|
|
|
49
|
-
|
|
50
|
-
if (me.success) {
|
|
51
|
-
// 身份资料与服务端重新校验后的访问范围一次返回,客户端无需持久化权限快照。
|
|
52
|
-
const { roles, permissions, ...user } = me.data
|
|
53
|
-
}
|
|
52
|
+
调用前完成本页初始化。命名变量为经校验的宿主输入;HaiResult 先判 success。auth 和存储操作的 Promise<void> 失败会 reject,必须显示失败并处理清理故障,不能按业务 success 假报登录完成。
|
|
54
53
|
|
|
55
|
-
|
|
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)
|
|
56
117
|
```
|
|
57
118
|
|
|
58
|
-
|
|
119
|
+
`apiClient.config` 是脱敏运行快照(未初始化为 null),不能用于恢复真实密钥;`apiClient.isInitialized` 为就绪 boolean。它们是只读属性,不发起网络请求。
|
|
120
|
+
|
|
121
|
+
### 启用传输加密
|
|
59
122
|
|
|
60
123
|
服务端需先启用 `serv.createApp({ transport: { crypto, serverKeyRing, keyStore } })`;客户端只注入同一个 `@h-ai/crypto` 服务实例,无需手写密钥协商代码。
|
|
61
124
|
|
|
@@ -98,30 +161,12 @@ await client.init({ baseUrl: 'https://api.example.com/api/v1' })
|
|
|
98
161
|
const result = await client.iam.auth.login({ identifier: 'alice', password: 'secret' })
|
|
99
162
|
```
|
|
100
163
|
|
|
101
|
-
###
|
|
102
|
-
|
|
103
|
-
- `apiClient.init(config)`:初始化默认 client。
|
|
104
|
-
- `apiClient.close()`:清理 client 状态。
|
|
105
|
-
- `apiClient.auth.setTokens(tokens)`:替换凭据,写入 access token;非 httpOnly 存储写入提供的 refresh token,缺省时清除旧值。
|
|
106
|
-
- `apiClient.auth.clear()`:清理 token。
|
|
107
|
-
- `apiClient.create(contract)`:创建自定义 typed client。
|
|
164
|
+
### 凭据更新与关闭
|
|
108
165
|
|
|
109
166
|
`auth.clear()` 和 `auth.setTokens()` 会立即取消并作废当前客户端的旧刷新请求。迟到的成功或 401 响应不会恢复已退出会话、覆盖新账号或触发旧刷新回调;存储写入串行提交,方法完成后不会再被之前已开始的异步写入覆盖。退出和切换账号须通过这些方法更新凭据,不直接修改 storage。httpOnly Cookie 和服务端会话仍由服务端退出接口撤销。
|
|
110
167
|
|
|
111
168
|
已初始化后再次调用 `init()` 会返回 `CONFIG_ERROR`,不会覆盖现有 client;如需切换 `baseUrl` / `fetch` / `transport`,请先 `await apiClient.close()` 再重新初始化。
|
|
112
169
|
|
|
113
|
-
### 配置
|
|
114
|
-
|
|
115
|
-
- `baseUrl`:API 基础地址,通常包含 `/api/v1`。
|
|
116
|
-
- `auth.storage`:Token 存储适配器;默认 `apiClient.tokenStorage.httpOnlyCookie()`(浏览器推荐);SSR / 测试场景请显式传入 `apiClient.tokenStorage.memory()`。
|
|
117
|
-
- `auth.refreshPath`:刷新 token 路径,默认 `/auth/refresh`。
|
|
118
|
-
- `timeout`:请求超时,默认 30000ms。
|
|
119
|
-
- `headers`:静态或动态公共请求头。
|
|
120
|
-
- `fetch`:自定义 fetch 实现。
|
|
121
|
-
- `transport.crypto`:启用透明请求/响应加解密,必须传入已初始化的 `@h-ai/crypto` 实例。
|
|
122
|
-
- `transport.keyExchangePath`:密钥协商子路径,默认 `/_hai/key-exchange`;会自动拼接到 `baseUrl` 后。
|
|
123
|
-
- `apiClient.config`:返回**脱敏后的运行配置快照**;`baseUrl` 的内嵌凭证、`headers.authorization` 等敏感值会自动隐藏。
|
|
124
|
-
|
|
125
170
|
### 配置驱动认证页面
|
|
126
171
|
|
|
127
172
|
密码登录只提交 `identifier` 和 `password`;`AuthPanel` 将 `LoginForm` 的 `username` 映射为 `identifier`。会话有效期由服务端 `session.maxAge` / `session.refreshTokenMaxAge` 配置决定,登录表单不提供按次登录的持久会话选项。
|
|
@@ -132,31 +177,11 @@ const result = await client.iam.auth.login({ identifier: 'alice', password: 'sec
|
|
|
132
177
|
|
|
133
178
|
[完整 IAM 配置 / IAM configuration](../iam/README.md)
|
|
134
179
|
|
|
135
|
-
###
|
|
136
|
-
|
|
137
|
-
业务 API 返回 `HaiResult<T>`:
|
|
138
|
-
|
|
139
|
-
```ts
|
|
140
|
-
const result = await apiClient.iam.auth.currentUser()
|
|
141
|
-
if (!result.success) {
|
|
142
|
-
// result.error.code / result.error.message
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
// result.data = User & { roles: string[], permissions: string[] }
|
|
146
|
-
}
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
未初始化、网络错误、超时、401/403/404 等客户端侧问题也会转换为失败的 `HaiResult`。
|
|
150
|
-
|
|
151
|
-
服务端以非 2xx HTTP 状态返回合法的失败 `HaiResult` 时,客户端保留业务错误码、消息和 `traceId`,例如 409 冲突仍可提示刷新后重试。`httpStatus` 以真实 HTTP 响应为准;5xx 的内部诊断仍被隐藏,正文不能通过声称 400 绕过脱敏。非框架响应继续使用客户端错误映射。
|
|
152
|
-
|
|
153
|
-
自动刷新在真实 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`,并处理刷新失败后的登录失效。
|
|
154
|
-
|
|
155
|
-
## 输入约束与 HTTP 能力
|
|
180
|
+
### 输入约束与 HTTP 能力
|
|
156
181
|
|
|
157
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#存储输入约束与跨端能力)。
|
|
158
183
|
|
|
159
|
-
|
|
184
|
+
### 逻辑请求观察
|
|
160
185
|
|
|
161
186
|
`client.observeRequests` 返回可取消的逻辑请求观察:requestId、procedure、耗时、HTTP 状态、最终 success/errorCode 与实际业务请求 attempts。HTTP 200 的失败 HaiResult 也记为失败,自动刷新后的重试保留同一 requestId,整个逻辑调用只产生一次结果。记录不包含正文、Token 或 URL 查询参数。刷新/密钥协商不伪装成用户业务操作;close 清理本实例的订阅,重新 init 后需重新订阅。
|
|
162
187
|
|
|
@@ -170,32 +195,64 @@ const unsubscribe = apiClient.observeRequests((record) => {
|
|
|
170
195
|
unsubscribe()
|
|
171
196
|
```
|
|
172
197
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
HTTP 错误返回本地化安全文案及追踪编号;ToastContainer 默认隐藏编号,由排查信息图标展开和复制。响应同时包含 `error.traceId` 和 `X-Trace-Id`。框架复用本次 requestId 作为 traceId,前后端日志自动关联。底层异常、堆栈和操作上下文经凭据脱敏后写入日志,不通过 cause/ext 向用户公开。字段校验只返回本地化的字段名和错误。
|
|
176
|
-
|
|
177
|
-
`core.error.toPublic(error, traceId?)` 用于接口/UI 边界;`core.error.formatMessage(error)` 用于重新翻译后的提示,重复调用不会重复追加标识。日志传入完整 Error 对象,不能只记录通用标题或提前 String(error) 丢失上下文。数据库失败记录驱动错误码、消息、堆栈及表/列;SQL 原文、绑定值和连接凭据不进入日志。
|
|
178
|
-
|
|
179
|
-
## 可选交互挑战
|
|
198
|
+
### 可选交互挑战
|
|
180
199
|
|
|
181
200
|
createAuthActions(client) 提供 createCaptcha({operation}),所有登录映射与续期/注册会保留 captcha;sendOtp(identifier,{captcha})、requestPasswordReset(identifier,{captcha}) 接收可选第二参数。框架 AuthPanel 自动求解,自定义表单复用 ALTCHA 协议,证明不进入安全 TokenStorage。
|
|
182
201
|
|
|
183
202
|
策略、默认值和恢复边界见 [IAM](../iam/README.md)。
|
|
184
203
|
|
|
185
|
-
|
|
204
|
+
### 敏感请求再认证
|
|
186
205
|
|
|
187
206
|
客户端单次调用的 `options.context.reauthentication` 携带 `reauthenticate` 返回的用途凭据,由请求管线生成 `X-Hai-Reauthentication`。不要写入客户端全局 header、持久存储或日志。服务端同时检查当前权限、资源范围和凭据用途,并原子消费;并发普通请求不会继承该 header。跨域宿主须允许此 header,默认 Serv/Kit 已包含。
|
|
188
207
|
|
|
189
|
-
|
|
208
|
+
### 原地恢复过期会话
|
|
190
209
|
|
|
191
210
|
配置 `auth.recoverSession` 回调可在服务端明确拒绝会话时原地重新登录并保留页面输入。使用 `@h-ai/ui` 的 `createSessionRecovery` / `SessionRecoveryDialog`:根布局绑定公开配置与原用户,回调设为 `recovery.recover`,弹窗使用标准认证 actions,销毁时 `cancel()`。登录的 `expectedUserId` 由 IAM 限定原主体;常规验证码、锁定、改密和登录限制照常执行。只对明确未认证拒绝的原请求续接一次;取消、5xx、超时和结果不明的网络写入不重放。敏感操作证明绑定旧会话时可能需要再次确认,不复用到新会话。未传回调保持原处理方式;配置回调后由恢复流程处理,不再立即调用 onRefreshFailed 导航离开。终端分组通过 auth.terminalType(web/app/desktop/api,默认 web)传入;它不是硬件身份认证。
|
|
192
211
|
|
|
193
|
-
|
|
212
|
+
### 页面离开会话策略
|
|
194
213
|
|
|
195
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`;不能据此保证关闭即撤销。
|
|
196
215
|
|
|
197
216
|
敏感调用通过本次 `reauthorize: () => sensitiveActions.authorize(operation)` 重新确认用途:Kit 放 apiFetch 第三参数,typed client 放 options.context。会话刷新/恢复或明确的再认证拒绝后仅续发一次;取消不写入,未提供回调不重放旧证明。
|
|
198
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
|
+
|
|
199
256
|
## 开发测试
|
|
200
257
|
|
|
201
258
|
```bash
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@h-ai/api-client",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.0-beta.
|
|
4
|
+
"version": "1.0.0-beta.20",
|
|
5
5
|
"description": "Hai Framework universal HTTP client with Bearer token and API contract support.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"exports": {
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
"node": "^22.23.4 || ^24.22.1 || ^26.12.1"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@h-ai/api-contract": "1.0.0-beta.
|
|
27
|
-
"@h-ai/core": "1.0.0-beta.
|
|
28
|
-
"@h-ai/crypto": "1.0.0-beta.
|
|
26
|
+
"@h-ai/api-contract": "1.0.0-beta.20",
|
|
27
|
+
"@h-ai/core": "1.0.0-beta.20",
|
|
28
|
+
"@h-ai/crypto": "1.0.0-beta.20",
|
|
29
29
|
"@orpc/client": "^1.16.2",
|
|
30
30
|
"@orpc/contract": "^1.16.2",
|
|
31
31
|
"@orpc/openapi-client": "^1.16.2",
|