@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.en.md +148 -89
- package/README.ja.md +147 -88
- package/README.md +147 -88
- package/dist/index.d.ts +27 -14
- package/dist/index.js +117 -24
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
package/README.en.md
CHANGED
|
@@ -6,54 +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.
|
|
11
|
+
## Quick Start
|
|
23
12
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
### Quick start
|
|
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.
|
|
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
|
-
|
|
32
|
-
baseUrl: '
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
identifier: 'alice',
|
|
38
|
-
password: 'secret',
|
|
39
|
-
})
|
|
48
|
+
## Features
|
|
40
49
|
|
|
41
|
-
|
|
42
|
-
// httpOnly cookie 模式下 refresh token 由服务端 Set-Cookie 管理,
|
|
43
|
-
// api-client 默认存储只需要把 access token 写入内存。
|
|
44
|
-
await apiClient.auth.setTokens(login.data.tokens)
|
|
45
|
-
}
|
|
50
|
+
### Purpose, inputs and outputs
|
|
46
51
|
|
|
47
|
-
|
|
48
|
-
if (me.success) {
|
|
49
|
-
// 身份资料与服务端重新校验后的访问范围一次返回,客户端无需持久化权限快照。
|
|
50
|
-
const { roles, permissions, ...user } = me.data
|
|
51
|
-
}
|
|
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.
|
|
52
53
|
|
|
53
|
-
|
|
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)
|
|
54
117
|
```
|
|
55
118
|
|
|
56
|
-
|
|
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
|
|
57
122
|
|
|
58
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.
|
|
59
124
|
|
|
@@ -75,7 +140,7 @@ await apiClient.close()
|
|
|
75
140
|
await crypto.close()
|
|
76
141
|
```
|
|
77
142
|
|
|
78
|
-
Only authenticated protocol v2 is accepted. Independent random SM4-CBC and HMAC-SM3 keys are wrapped with SM2; verify the sender signature and MAC before CBC decryption. Clients sign requests and the selected server key version signs responses. Authentication covers version, direction, serverKeyId, clientId, a random 128-bit requestId acting as the nonce, issuedAt, HTTP method, the SM3 pathHash of pathname plus the original search, and response status.
|
|
143
|
+
Only authenticated protocol v2 is accepted. Independent random SM4-CBC and HMAC-SM3 keys are wrapped with SM2; verify the sender signature and MAC before CBC decryption. Clients sign requests and the selected server key version signs responses. Authentication covers version, direction, serverKeyId, clientId, a random 128-bit requestId acting as the nonce, issuedAt, HTTP method, the SM3 pathHash of pathname plus the original search, and response status. By default, requests expire after five minutes and client timestamps may be up to 30 seconds ahead. Set `maxRequestAgeSeconds` (1–600) and `maxFutureSkewSeconds` (0–300) on the server to adjust these limits in whole seconds. The shared registry atomically consumes (clientId, requestId) and retains the nonce until issuedAt plus 11 minutes, covering the longest configurable window and server clock differences. Storage failures reject requests. Synchronize node clocks and use a Redis noeviction/durable policy that preserves live nonces. Transport replay prevention does not replace business idempotency keys.
|
|
79
144
|
|
|
80
145
|
Empty POSTs still send an authenticated envelope for empty plaintext. GET/HEAD carry it in X-Hai-Transport; bodyless responses such as 204/304/HEAD use the same header. Other requests/responses carry a JSON envelope and X-Encrypted: true. The client must match the original request context and HTTP status; missing markers, old formats and authentication failures reject. Serv/Kit hosts explicitly allow plaintext through `encryption: disabled` path rules or host `excludePaths`. Callers must select ordinary requests; API Client/Kit wrappers use their own `excludePaths`. CORS must allow and expose X-Hai-Transport. TLS authenticates handshake keys; this protocol does not replace IAM authorization.
|
|
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
|
-
###
|
|
100
|
-
|
|
101
|
-
- `apiClient.init(config)`: initialize the default client.
|
|
102
|
-
- `apiClient.close()`: clear client state.
|
|
103
|
-
- `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.
|
|
104
|
-
- `apiClient.auth.clear()`: clear tokens.
|
|
105
|
-
- `apiClient.create(contract)`: create a custom typed client.
|
|
164
|
+
### Credential changes and closing
|
|
106
165
|
|
|
107
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.
|
|
108
167
|
|
|
109
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.
|
|
110
169
|
|
|
111
|
-
### Configuration
|
|
112
|
-
|
|
113
|
-
- `baseUrl`: API base URL, usually including `/api/v1`.
|
|
114
|
-
- `auth.storage`: token storage adapter; defaults to `apiClient.tokenStorage.httpOnlyCookie()` (recommended for browsers). Explicitly use `apiClient.tokenStorage.memory()` for SSR / tests.
|
|
115
|
-
- `auth.refreshPath`: token refresh path; defaults to `/auth/refresh`.
|
|
116
|
-
- `timeout`: request timeout; defaults to 30000 ms.
|
|
117
|
-
- `headers`: static or dynamic shared request headers.
|
|
118
|
-
- `fetch`: custom fetch implementation.
|
|
119
|
-
- `transport.crypto`: transparent request/response encryption and decryption; requires an initialized `@h-ai/crypto` instance.
|
|
120
|
-
- `transport.keyExchangePath`: key exchange subpath; defaults to `/_hai/key-exchange` and is appended to `baseUrl` automatically.
|
|
121
|
-
- `apiClient.config`: a **redacted snapshot of runtime configuration**; credentials embedded in `baseUrl`, `headers.authorization`, and other sensitive values are hidden automatically.
|
|
122
|
-
|
|
123
170
|
### Configuration driven authentication
|
|
124
171
|
|
|
125
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.
|
|
@@ -130,31 +177,11 @@ apiClient.createAuthActions(client) supplies publicConfig, login, register, send
|
|
|
130
177
|
|
|
131
178
|
[完整 IAM 配置 / IAM configuration](../iam/README.en.md)
|
|
132
179
|
|
|
133
|
-
###
|
|
134
|
-
|
|
135
|
-
Business APIs return `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
|
-
Client-side failures such as missing initialization, network errors, timeouts, and 401/403/404 responses are also converted to failed `HaiResult` values.
|
|
148
|
-
|
|
149
|
-
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.
|
|
150
|
-
|
|
151
|
-
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.
|
|
152
|
-
|
|
153
|
-
## Input limits and HTTP capabilities
|
|
180
|
+
### Input limits and HTTP capabilities
|
|
154
181
|
|
|
155
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#存储输入约束与跨端能力).
|
|
156
183
|
|
|
157
|
-
|
|
184
|
+
### Logical request observation
|
|
158
185
|
|
|
159
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.
|
|
160
187
|
|
|
@@ -168,33 +195,65 @@ const unsubscribe = apiClient.observeRequests((record) => {
|
|
|
168
195
|
unsubscribe()
|
|
169
196
|
```
|
|
170
197
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
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.
|
|
174
|
-
|
|
175
|
-
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.
|
|
176
|
-
|
|
177
|
-
## Optional interactive challenges
|
|
198
|
+
### Optional interactive challenges
|
|
178
199
|
|
|
179
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.
|
|
180
201
|
|
|
181
202
|
See policy, defaults and recovery boundaries in [IAM](../iam/README.en.md).
|
|
182
203
|
|
|
183
|
-
|
|
204
|
+
### Sensitive request reauthentication
|
|
184
205
|
|
|
185
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.
|
|
186
207
|
|
|
187
|
-
|
|
208
|
+
### Recovering an expired session in place
|
|
188
209
|
|
|
189
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.
|
|
190
211
|
|
|
191
|
-
|
|
212
|
+
### Page exit session policy
|
|
192
213
|
|
|
193
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.
|
|
194
215
|
|
|
195
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.
|
|
196
217
|
|
|
197
|
-
##
|
|
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
|
|
198
257
|
|
|
199
258
|
```bash
|
|
200
259
|
pnpm --filter @h-ai/api-client test
|
|
@@ -203,7 +262,7 @@ pnpm --filter @h-ai/api-client typecheck
|
|
|
203
262
|
|
|
204
263
|
Clients learn `transport.maxPlaintextBytes` from server key exchange. An explicit local limit selects the smaller value. Capacity failures return `HaiApiClientError.PAYLOAD_TOO_LARGE` (`hai:api-client:020`) with direction, stage and byte diagnostics. See [capacity and upgrade](../crypto/TRANSPORT-CAPACITY.md).
|
|
205
264
|
|
|
206
|
-
|
|
265
|
+
Serv/Kit return an encrypted HTTP 400 for an out-of-window request after validating request binding, signature and MAC. The client throws `TransportTimeError` (`hai:crypto:071`), displays the server’s UTC time at validation, and asks the user to enable automatic synchronization or set the device time manually before refreshing. `serverTime` is Unix milliseconds. Clients still show synchronization guidance when an older server omits it. `api-client` maps the error to `VALIDATION_FAILED` and preserves `cause.transportCode` and `cause.serverTime`.
|
|
207
266
|
|
|
208
267
|
## License
|
|
209
268
|
|