@h-ai/serv 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 +172 -124
- package/README.ja.md +172 -124
- package/README.md +172 -124
- package/dist/{chunk-TWMOZPGU.js → chunk-CZ6VA2EN.js} +3 -3
- package/dist/{chunk-TWMOZPGU.js.map → chunk-CZ6VA2EN.js.map} +1 -1
- package/dist/{chunk-I3F7RN4I.js → chunk-PLGCHDZM.js} +30 -13
- package/dist/chunk-PLGCHDZM.js.map +1 -0
- package/dist/features/ai.d.ts +9 -8
- package/dist/features/ai.js +34 -13
- package/dist/features/ai.js.map +1 -1
- package/dist/features/feedback.d.ts +20 -19
- package/dist/features/feedback.js +1 -1
- package/dist/features/feedback.js.map +1 -1
- package/dist/features/iam.d.ts +4 -3
- package/dist/features/iam.js +4 -2
- package/dist/features/iam.js.map +1 -1
- package/dist/features/storage.d.ts +4 -3
- package/dist/features/storage.js +34 -9
- package/dist/features/storage.js.map +1 -1
- package/dist/index.d.ts +97 -68
- package/dist/index.js +338 -222
- package/dist/index.js.map +1 -1
- package/dist/{serv-context-DE1JH5bd.d.ts → serv-context-DSsS_ksL.d.ts} +4 -60
- package/dist/{serv-router-BJk04-oS.d.ts → serv-router-DWjYC0zD.d.ts} +2 -18
- package/package.json +29 -29
- package/dist/chunk-I3F7RN4I.js.map +0 -1
package/README.en.md
CHANGED
|
@@ -4,29 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
[Framework home](../../README.en.md) · Related modules: [@h-ai/ai](../ai/README.en.md) · [@h-ai/api-contract](../api-contract/README.en.md) · [@h-ai/core](../core/README.en.md) · [@h-ai/crypto](../crypto/README.en.md) · [@h-ai/iam](../iam/README.en.md) · [@h-ai/storage](../storage/README.en.md)
|
|
6
6
|
|
|
7
|
-
## Introduction
|
|
8
|
-
|
|
9
7
|
hai-framework's API service runtime: a stable public HTTP App abstraction that mounts `@h-ai/api-contract` contracts as cross-platform HTTP APIs. Hono is an internal implementation detail.
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
Use runtime operations through `serv.xx`. Import `ServFunctions` from the root entry when annotating or re-exporting the namespace. Context factories and generic `implement` signatures support downstream declaration emission through public entries; no internal build paths or standalone helpers are needed.
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
- Flat API: `serv.createApp / serv.implement / serv.listen / serv.toFetch / serv.generateSpec / ...`.
|
|
15
|
-
- Procedure chains: `implement().context().route().auth/permission/role().handle().build()`.
|
|
16
|
-
- Custom pipelines: mount HTTP middleware with `createApp({ middlewares })`; inject request fields through context factories.
|
|
17
|
-
- Read-only storage assets: `serv.storageAssets(...)` handles safe keys, MIME allowlists, GET/HEAD, ETags, and cache headers.
|
|
18
|
-
- Default feature procedures: `createIamProcedures()`, `createStorageProcedures()`, `createAiProcedures()`.
|
|
19
|
-
- `build()` synchronously returns a plain router, so nested default feature procedures can be selected directly. Match the mounted contract to the selected procedures; unselected routes stay unavailable.
|
|
20
|
-
- Missing routes return localized HaiResult JSON with HTTP 404. Transport encryption preserves that status instead of producing a 500 for a non-JSON response.
|
|
21
|
-
- Built-in security headers and health checks; optional OpenAPI JSON, Scalar docs, and internal RPC endpoints.
|
|
22
|
-
- Optional transport encryption: `serv.createApp({ transport: { crypto, serverKeyRing, keyStore } })` mounts key exchange and request/response encryption automatically.
|
|
23
|
-
- Optional audio: `serv.createApp({ audio: { ai, verifyTicket, authorize } })` mounts a unified audio WebSocket. Connections authenticate immediately with one-time tickets (`{ session, grant? }`, separate pre-auth timeout). Every frame uses runtime Zod validation and a strict state machine, with input/queue/send-buffer backpressure and cascading cancellation. Authorization verifies operations, models, voices, quotas, and concurrency on the server.
|
|
24
|
-
|
|
25
|
-
> Lifecycle: `@h-ai/serv` is a stateless HTTP App assembler and needs no `init()` / `close()`. Initialize `iam` / `storage` / `ai` / `crypto` before app creation, then release them in reverse order through `serv.listen(..., { onClose })`. `serv.createApp()` throws immediately on startup configuration errors such as transport manager creation failure. onClose is the SIGINT/SIGTERM shutdown callback. Calling the returned handle.close directly closes HTTP only; the host separately releases business modules in reverse order.
|
|
26
|
-
|
|
27
|
-
## Getting started
|
|
11
|
+
## Quick Start
|
|
28
12
|
|
|
29
|
-
|
|
13
|
+
The assembly below assumes application-initialized iam, storage, ai, core and logger, plus an application-owned closeApp callback. It is not an initialization script. A health-only app may use an empty contract and procedures. Serv is a factory with no init/close; close the listener and release business modules in reverse order.
|
|
30
14
|
|
|
31
15
|
```ts
|
|
32
16
|
import { apiContract } from '@h-ai/api-contract'
|
|
@@ -53,6 +37,7 @@ const procedures = {
|
|
|
53
37
|
const app = serv.createApp({
|
|
54
38
|
contract,
|
|
55
39
|
procedures,
|
|
40
|
+
iam,
|
|
56
41
|
http: {
|
|
57
42
|
apiPrefix: '/api/v1',
|
|
58
43
|
openapi: { path: '/openapi.json' },
|
|
@@ -80,7 +65,47 @@ AI procedures bind `objectId` to the trusted `context.session.userId` when the c
|
|
|
80
65
|
|
|
81
66
|
`chats.createCompletion` (`POST /ai/chats/completions`) returns one non-streaming `HaiResult<AiChatCompletionData>`. Function `tools` retain their complete nested JSON Schema and `strict`, and `tool_choice` supports `auto`, `none`, `required`, or a provided function name. Messages preserve roles, `name`, user images, and tool calls. Tool results require `role: 'tool'` and a nonempty `tool_call_id`; named choices must reference a supplied tool. The procedure does not execute tools: the application authorizes and executes them, then sends the assistant's `tool_calls` with their tool results. The input has no `stream` field; either boolean value is rejected with HTTP 400 before the provider runs. For streaming, define a separate endpoint with a matching stream output using `ai.llm.chatStream()`.
|
|
82
67
|
|
|
83
|
-
|
|
68
|
+
## Features
|
|
69
|
+
|
|
70
|
+
### Purpose, inputs and outputs
|
|
71
|
+
|
|
72
|
+
Call fragments reuse the initialized service from this page. Named variables stand for application-provided requests, schemas, resource definitions or options. Follow the corresponding examples for setup and failure handling. Check HaiResult.success and await asynchronous calls.
|
|
73
|
+
|
|
74
|
+
| Entry | Use case | Input | Output | Call fragment |
|
|
75
|
+
| ---------------------------------- | ---------------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
76
|
+
| `serv.createApp` | Compose API app | `contract, procedures, http?, iam?, middlewares?, transport?` | `ServHttpApp (fetch/request)` | `serv.createApp({ contract, procedures })` |
|
|
77
|
+
| `serv.listen` | Start Node listener | `app, { host?, port?, onClose?, onListening? }` | `ServNodeServer { server, address, close(): Promise<void> }` | `serv.listen(app, { host: "127.0.0.1", port: 3000 })` |
|
|
78
|
+
| `serv.toFetch` | Adapt Fetch host | `ServHttpApp` | `(Request) => Promise<Response>` | `serv.toFetch(app)` |
|
|
79
|
+
| `serv.implement` | Implement domain routes | `typed contract` | `ServImplementer; context/route/auth/permission/role/handle/build` | `serv.implement(contract)` |
|
|
80
|
+
| `serv.parseRequestContext` | Parse request metadata | `{ request, platform? }` | `ServContext with requestId/locale/accessToken; no verified session` | `serv.parseRequestContext({ request })` |
|
|
81
|
+
| `serv.buildAuthContextFactory` | Build authenticated context | `(accessToken) => Promise<HaiResult<ServSession>>` | `CreateServContext; invalid credentials leave session absent` | `serv.buildAuthContextFactory(token => iam.auth.verifyToken(token))` |
|
|
82
|
+
| `serv.cors` | Restrict cross-origin access | `{ origin?, methods?, allowedHeaders?, exposedHeaders?, credentials? }` | `ServMiddleware` | `serv.cors({ origin: "https://app.example.com", credentials: true })` |
|
|
83
|
+
| `serv.createRuntimeSecurityPolicy` | Derive runtime security | `{ environment, corsOrigin, nativeOrigins? }` | `allowOrigin, secureRefreshCookie, exposeApiDocs` | `serv.createRuntimeSecurityPolicy({ environment: "production", corsOrigin: "https://app.example.com" })` |
|
|
84
|
+
| `serv.storageAssets` | Authorized read-only assets | `ServStorageAssetsConfig with reader/key/MIME policy` | `GET/HEAD middleware with ETag` | `serv.storageAssets(assetOptions)` |
|
|
85
|
+
| `serv.generateSpec` | Generate OpenAPI | `contract, { title?, version?, description?, apiPrefix? }` | `Promise<OpenAPI.Document>` | `serv.generateSpec(contract, { title: "Orders", version: "1.0.0" })` |
|
|
86
|
+
| `serv.validateInputOrFail` | Validate domain input | `Zod schema, input, locale?` | `HaiResult<z.infer<Schema>>; failure contains safe field errors` | `serv.validateInputOrFail(InputSchema, input, context.locale)` |
|
|
87
|
+
| `serv.resolveRequestLocale` | Resolve request locale | `Headers` | `normalized supported locale` | `serv.resolveRequestLocale(request.headers)` |
|
|
88
|
+
| `serv.m` | Translate module message | `message key, { locale?, ...messageOptions }` | `string` | `serv.m("serv_errorForbidden", { locale: "zh-CN" })` |
|
|
89
|
+
| `serv.WILDCARD_PERMISSION` | Wildcard permission | `none` | `string constant; only use in trusted permission policy` | `serv.WILDCARD_PERMISSION` |
|
|
90
|
+
| `serv.WILDCARD_ROLE` | Wildcard role | `none` | `string constant; does not authorize arbitrary requests` | `serv.WILDCARD_ROLE` |
|
|
91
|
+
|
|
92
|
+
- Start with `serv.createApp(...)` → `serv.listen(app, { port })`; applications need not import Hono.
|
|
93
|
+
- Flat API: `serv.createApp / serv.implement / serv.listen / serv.toFetch / serv.generateSpec / ...`.
|
|
94
|
+
- Procedure chains: `implement().context().route().auth/permission/role().handle().build()`.
|
|
95
|
+
- Custom pipelines: mount HTTP middleware with `createApp({ middlewares })`; inject request fields through context factories.
|
|
96
|
+
- Read-only storage assets: `serv.storageAssets(...)` handles safe keys, MIME allowlists, GET/HEAD, ETags, and cache headers.
|
|
97
|
+
- Default feature procedures: `createIamProcedures()`, `createStorageProcedures()`, `createAiProcedures()`.
|
|
98
|
+
- `build()` synchronously returns a plain router, so nested default feature procedures can be selected directly. Match the mounted contract to the selected procedures; unselected routes stay unavailable.
|
|
99
|
+
- Missing routes return localized HaiResult JSON with HTTP 404. Transport encryption preserves that status instead of producing a 500 for a non-JSON response.
|
|
100
|
+
- Built-in security headers and health checks; optional OpenAPI JSON, Scalar docs.
|
|
101
|
+
- Optional transport encryption: `serv.createApp({ transport: { crypto, serverKeyRing, keyStore } })` mounts key exchange and request/response encryption automatically.
|
|
102
|
+
- Optional audio: `serv.createApp({ audio: { ai, verifyTicket, authorize } })` mounts a unified audio WebSocket. Connections authenticate immediately with one-time tickets (`{ session, grant? }`, separate pre-auth timeout). Every frame uses runtime Zod validation and a strict state machine, with input/queue/send-buffer backpressure and cascading cancellation. Authorization verifies operations, models, voices, quotas, and concurrency on the server.
|
|
103
|
+
|
|
104
|
+
> Lifecycle: `@h-ai/serv` is a stateless HTTP App assembler and needs no `init()` / `close()`. Initialize `iam` / `storage` / `ai` / `crypto` before app creation, then release them in reverse order through `serv.listen(..., { onClose })`. `serv.createApp()` throws immediately on startup configuration errors such as transport manager creation failure. onClose is the SIGINT/SIGTERM shutdown callback. Calling the returned handle.close directly closes the HTTP listener and audio connections; the host separately releases business modules in reverse order.
|
|
105
|
+
|
|
106
|
+
The listener handle can close immediately after startup to cancel a pending bind; repeated closes share the same result. Actual listen failures, such as an occupied address, still reject the close result instead of reporting a normal shutdown.
|
|
107
|
+
|
|
108
|
+
### host / port configuration
|
|
84
109
|
|
|
85
110
|
Host/port come from `server` in `config/_serv.yml`: `HOST` / `PORT` environment variables > configuration > `127.0.0.1:3000`. The default `127.0.0.1` is local-only; use `0.0.0.0` to expose a container:
|
|
86
111
|
|
|
@@ -93,7 +118,7 @@ server:
|
|
|
93
118
|
|
|
94
119
|
Pass the resolved `server` to `serv.listen`; explicit `host` / `port` arguments can also override it.
|
|
95
120
|
|
|
96
|
-
|
|
121
|
+
### Standard fetch signature adapter
|
|
97
122
|
|
|
98
123
|
toFetch provides the standard fetch signature for Node-compatible hosts and test injection. The package root still depends on Node APIs; this does not establish support for Workers, Deno or edge deployment without those APIs.
|
|
99
124
|
|
|
@@ -102,11 +127,11 @@ const handler = serv.toFetch(app)
|
|
|
102
127
|
export default { fetch: handler }
|
|
103
128
|
```
|
|
104
129
|
|
|
105
|
-
Node `serv.listen()` supplies the socket peer address. Forwarded headers are ignored by default. Only proxy IPs/CIDRs in `http.trustedProxies` may supply them: XFF is walked from right to left until the first untrusted address; content to the left of that address is ignored. An invalid or excessively long trusted chain leaves IP undefined rather than promoting the proxy to client identity. Proxies must sanitize or append forwarded headers. Fetch platforms explicitly provide a trusted peer through `app.fetch(request, { remoteAddress: platformPeerAddress })`; without metadata IP is undefined. Do not copy arbitrary headers into metadata. Custom `createContext(input)` forwards the complete input to preserve resolved `input.ip`.
|
|
130
|
+
Node `serv.listen()` supplies the socket peer address. Forwarded headers are ignored by default. Only proxy IPs/CIDRs in `http.trustedProxies` may supply them: XFF is walked from right to left until the first untrusted address; content to the left of that address is ignored. An invalid or excessively long trusted chain leaves IP undefined rather than promoting the proxy to client identity. Proxies must sanitize or append forwarded headers. Fetch platforms explicitly provide a trusted peer through `app.fetch(request, { remoteAddress: platformPeerAddress })`; without metadata IP is undefined. Do not copy arbitrary headers into metadata. Custom `createContext(input)` forwards the complete input to preserve resolved `input.ip`.
|
|
106
131
|
|
|
107
132
|
toFetch constructs Request(input, init) for Request, URL and string inputs, applying RequestInit overrides including method, headers, body and signal without mutating input fields. Consumed or locked bodies and invalid body combinations reject the returned Promise. Cancellation before dispatch skips the handler; in-flight cancellation ends the caller wait and reaches the app through the effective signal. Listeners are released on completion, failure or cancellation, and a discarded late response body is cancelled. Cancellation cannot roll back external effects already started; after Response delivery the app owns streaming cleanup through request.signal.
|
|
108
133
|
|
|
109
|
-
|
|
134
|
+
### Audio WebSocket: tickets and server authorization
|
|
110
135
|
|
|
111
136
|
Audio connections require short-lived, one-time tickets. Issue them through an authenticated HTTP endpoint (recommended: `iam.ticket.issue`) and consume atomically in `verifyTicket` (recommended: `iam.ticket.consume`). Authentication runs at connection time with a separate timeout; no business frames are processed beforehand. The framework stores the returned `ServSession` and optional `grant`, then calls `authorize` to approve paid parameters:
|
|
112
137
|
|
|
@@ -132,7 +157,7 @@ const app = serv.createApp({
|
|
|
132
157
|
- `onSessionEnd` runs at most once after success, failure, or disconnection to release concurrency slots.
|
|
133
158
|
- Browser `createAudioClient({ url, getTicket })` puts only one-time `ticket` in the WebSocket URL. `getTicket(request)` receives the operation summary (`operation` / `model`) for a tightly bound ticket. Never pass ordinary IAM access tokens.
|
|
134
159
|
|
|
135
|
-
|
|
160
|
+
### Transport encryption (shared with `@h-ai/crypto`)
|
|
136
161
|
|
|
137
162
|
serv does not expose its own encryption factory; the server manager comes from `crypto.transport`. Applications only pass the top-level `crypto` handle:
|
|
138
163
|
|
|
@@ -199,14 +224,16 @@ const app = serv.createApp({
|
|
|
199
224
|
contract,
|
|
200
225
|
procedures,
|
|
201
226
|
http: servConfig.http,
|
|
202
|
-
middlewares: [
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
227
|
+
middlewares: [
|
|
228
|
+
{
|
|
229
|
+
middleware: serv.cors({
|
|
230
|
+
origin: servConfig.cors.origin,
|
|
231
|
+
credentials: servConfig.cors.credentials,
|
|
232
|
+
allowedHeaders: [...servConfig.cors.allowedHeaders],
|
|
233
|
+
exposedHeaders: [...servConfig.cors.exposedHeaders],
|
|
234
|
+
}),
|
|
235
|
+
}
|
|
236
|
+
],
|
|
210
237
|
transport: servConfig.transport === false
|
|
211
238
|
? undefined
|
|
212
239
|
: {
|
|
@@ -223,17 +250,15 @@ const app = serv.createApp({
|
|
|
223
250
|
|
|
224
251
|
> **Security**: with `transport` enabled, business requests need valid `X-Client-Id` except `excludePaths` and key exchange. Missing/unknown clientId returns 400. Unencryptable bodies, non-JSON responses, or encryption size limits return errors rather than forwarding plaintext business data.
|
|
225
252
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
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.
|
|
253
|
+
Transport uses authenticated v2 envelopes, verifying signature/MAC before decryption and binding method, path, time and response status. The shared registry atomically consumes nonces. The host still owns TLS, trusted proxies, authorization and idempotency. Custom CORS hosts must allow and expose X-Hai-Transport. See the [Transport protocol](../crypto/TRANSPORT-PROTOCOL.md) for fields, bodyless responses, clock windows and replay protection.
|
|
229
254
|
|
|
230
255
|
Set `transport.maxPlaintextBytes` for both request and response JSON plaintext, measured in UTF-8 bytes: default 1 MiB, integer range 1 KiB–64 MiB. Clients learn the server limit during key exchange. `resolveTransportBodyLimits()` derives encrypted/proxy bytes as twice the plaintext limit plus 32 KiB. Handshake, public-key, header and replay constraints remain independent. Oversized streams are cancelled and readers released: requests return 413; oversized responses return an encrypted 500 capacity error, recognized as `TransportBodyTooLargeError` / `hai:crypto:070`. [Transport capacity and upgrade](../crypto/TRANSPORT-CAPACITY.md)。
|
|
231
256
|
|
|
232
257
|
Ordinary routes retain `transport.maxPlaintextBytes` (auto/omitted: 1 MiB). Declare Feedback report intake, evidence, attachments and ZIP download capacity separately using `routePlaintextBytes` and `feedback.budget.maxTransportBytes`; `*` matches one complete path segment and explicit host route values take precedence. Handshakes, adapter-node and proxies use the maximum route capacity. HTTP adapters admit at most 2 explicitly declared material requests above 1 MiB and a separate 16 ordinary business requests, releasing admission after the entire response or failure. Busy admission returns 503 / `hai:crypto:072`; business writes are not automatically replayed.
|
|
233
258
|
|
|
234
|
-
|
|
259
|
+
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`.
|
|
235
260
|
|
|
236
|
-
|
|
261
|
+
### Token authentication (`iam` / `refreshCookie`)
|
|
237
262
|
|
|
238
263
|
Pass the top-level `iam` handle to `serv.createApp` to derive:
|
|
239
264
|
|
|
@@ -277,7 +302,7 @@ const app = serv.createApp({
|
|
|
277
302
|
|
|
278
303
|
The default `auth.logout` endpoint takes the complete token from input.accessToken or Authorization Bearer without requiring live access authentication. It grants no userId-based authority and accepts no userId revocation target. Missing tokens fail; unknown tokens succeed idempotently. Shared-store failures remain failed HaiResult values, and cookie middleware clears refresh cookies only after success. Ordinary procedures retain the existing HTTP 200 / HaiResult envelope: clients must inspect success, not only the HTTP status.
|
|
279
304
|
|
|
280
|
-
|
|
305
|
+
### Custom procedures (authentication + permissions)
|
|
281
306
|
|
|
282
307
|
```ts
|
|
283
308
|
const procedures = serv
|
|
@@ -291,7 +316,7 @@ const procedures = serv
|
|
|
291
316
|
.build()
|
|
292
317
|
```
|
|
293
318
|
|
|
294
|
-
|
|
319
|
+
### Advanced pipelines (HTTP middleware + context + route guards)
|
|
295
320
|
|
|
296
321
|
`@h-ai/serv` pipelines have three layers and extension points:
|
|
297
322
|
|
|
@@ -299,7 +324,7 @@ const procedures = serv
|
|
|
299
324
|
2. **Context**: customize through `verifyToken` / `createContext` / `serv.buildAuthContextFactory()`.
|
|
300
325
|
3. **Route guards**: declare authentication/authorization with `.auth()` / `.permission()` / `.role()`.
|
|
301
326
|
|
|
302
|
-
|
|
327
|
+
#### 1) Custom HTTP middleware
|
|
303
328
|
|
|
304
329
|
Use `middlewares` for **HTTP cross-cutting logic** such as logging, tracing, rate limits, CORS, and tenant headers, or WebSocket/file/binary endpoints unsuited to JSON/oRPC contracts. Reuse `serv.cors(...)` for common CORS needs:
|
|
305
330
|
|
|
@@ -355,7 +380,7 @@ Execution order is fixed:
|
|
|
355
380
|
2. Built-in CORS response wrapping; access checks for key exchange and preflight.
|
|
356
381
|
3. Built-in `transport`, if enabled, and request access checks.
|
|
357
382
|
4. Your `middlewares`.
|
|
358
|
-
5. health / refresh-cookie / OpenAPI /
|
|
383
|
+
5. health / refresh-cookie / OpenAPI / docs / oRPC routes
|
|
359
384
|
|
|
360
385
|
Notes:
|
|
361
386
|
|
|
@@ -368,7 +393,7 @@ Notes:
|
|
|
368
393
|
- Expose custom response headers such as `X-Encrypted` explicitly through `serv.cors({ exposedHeaders: [...] })` for browsers.
|
|
369
394
|
- This layer works with HTTP middleware context and returns **HTTP Response**, not `HaiResult`.
|
|
370
395
|
|
|
371
|
-
|
|
396
|
+
#### 2) Custom context pipelines
|
|
372
397
|
|
|
373
398
|
To reuse `Bearer token -> session` and add tenant, workspace, or source-system fields,
|
|
374
399
|
prefer `serv.buildAuthContextFactory()` to assemble a custom `createContext`:
|
|
@@ -395,7 +420,7 @@ Choose the appropriate extension:
|
|
|
395
420
|
- Add fields to default authentication context: use `buildAuthContextFactory(...) + createContext`.
|
|
396
421
|
- Replace context construction entirely: use `createContext` directly.
|
|
397
422
|
|
|
398
|
-
|
|
423
|
+
#### 3) Route guard
|
|
399
424
|
|
|
400
425
|
Public procedures use `.route(path, handler)`. For guarded routes, use
|
|
401
426
|
`.route(path).auth/permission/role().handle(handler)`:
|
|
@@ -419,6 +444,7 @@ Constraints:
|
|
|
419
444
|
|
|
420
445
|
- `.permission()` and `.role()` require authentication automatically. Failures return HaiResult with `error.httpStatus` 401/403; ordinary procedure responses remain HTTP 200 by default.
|
|
421
446
|
- Multiple permissions/roles on one route must all pass. `serv.WILDCARD_PERMISSION` / `serv.WILDCARD_ROLE` allow their respective categories.
|
|
447
|
+
- Permissions also support the IAM/Kit colon-prefix rule: `users:*` matches `users:read`, but not `users2:read` or dot-separated permissions.
|
|
422
448
|
- Unhandled handler exceptions become INTERNAL_ERROR HaiResult values; expected business failures should return `err(...)` directly.
|
|
423
449
|
- Paths, input, and output come from the contract. Duplicate/unknown paths are rejected; `.build()` requires complete contract implementation.
|
|
424
450
|
|
|
@@ -426,9 +452,9 @@ Public API boundaries:
|
|
|
426
452
|
|
|
427
453
|
- `src/pipelines/*` holds internal defaults and is not a public subpath API.
|
|
428
454
|
- The stable application-facing entry remains `@h-ai/serv`.
|
|
429
|
-
- Internal helpers such as `securityHeaders` / `
|
|
455
|
+
- Internal helpers such as `securityHeaders` / `buildHaiErrorBody` are not documented as separate subpath exports.
|
|
430
456
|
|
|
431
|
-
|
|
457
|
+
### httpOnly cookie authentication (recommended for production)
|
|
432
458
|
|
|
433
459
|
httpOnly cookie mode stores refresh tokens in server-managed cookies unreadable to browser JavaScript, reducing XSS exposure.
|
|
434
460
|
|
|
@@ -485,13 +511,16 @@ await apiClient.init({
|
|
|
485
511
|
})
|
|
486
512
|
```
|
|
487
513
|
|
|
488
|
-
>
|
|
514
|
+
> Refresh cookies use `HttpOnly` and `SameSite=Strict` and are scoped to `{apiPrefix}/auth/refresh`. Production adds `Secure` by default.
|
|
515
|
+
|
|
516
|
+
Refresh failures use the public error boundary too: 5xx and missing or invalid statuses return a safe message and traceId, while internal causes stay in server logs. Only a 401 authentication failure clears the refresh cookie; rate limits and backend failures preserve credentials for retry. Automatic auditing uses the same normalized failure status.
|
|
489
517
|
|
|
490
518
|
### API overview
|
|
491
519
|
|
|
492
|
-
- `serv.createApp(options)`: create the HTTP App with custom middleware, health checks, OpenAPI, optional docs
|
|
520
|
+
- `serv.createApp(options)`: create the HTTP App with custom middleware, health checks, OpenAPI, optional docs.
|
|
493
521
|
- `serv.parseRequestContext({ request })`: extract Bearer token + requestId without populating session.
|
|
494
522
|
- `serv.listen(app, options)`: start Node.js HTTP service; return `{ server, address, close }`.
|
|
523
|
+
The returned `close()` is idempotent: it stops accepting connections, closes Audio WebSockets, terminates uncooperative audio clients after one second, and waits for HTTP shutdown. Manual close also removes process signal listeners; the host still releases business modules in reverse order.
|
|
495
524
|
- `serv.toFetch(app)`: wrap as a standard `fetch(Request)` handler.
|
|
496
525
|
- `serv.generateSpec(contract, options)`: generate an OpenAPI 3.1 spec from the contract.
|
|
497
526
|
- `serv.storageAssets(config)`: read-only storage middleware with key/MIME allowlists, GET/HEAD, and ETags.
|
|
@@ -524,7 +553,81 @@ Use `http.access: { file: './config/security/request-access.yml' }` for larger p
|
|
|
524
553
|
|
|
525
554
|
`http.trustedProxies` defaults to `auto`: ordinary hosts ignore forwarded headers; only private managed Compose proxy topologies adopt container networks automatically. Custom ingress uses explicit proxy IPs/CIDRs; `[]` disables trust. Proxies sanitize forwarded headers, and Node listeners or Fetch platforms supply trusted peer metadata. See the [request access handbook](../../docs/usage/configuration/request-access.md) for files, edge proxies and deployment rollback.
|
|
526
555
|
|
|
527
|
-
### Configuration
|
|
556
|
+
### Configuration driven authentication
|
|
557
|
+
|
|
558
|
+
Agreements support configuration-loaded userAgreementMarkdown/privacyPolicyMarkdown and URLs. Markdown takes priority for the same agreement and is safely rendered in a modal.
|
|
559
|
+
|
|
560
|
+
createIamProcedures({ iam }) implements public config, password/LDAP/OTP login, registration and recovery directly. LDAP uses the existing refresh Cookie/native transport policy. Administrator creation uses iam.user.createUser and remains available when public registration is disabled; existing permission guards apply.
|
|
561
|
+
|
|
562
|
+
[完整 IAM 配置 / IAM configuration](../iam/README.en.md)
|
|
563
|
+
|
|
564
|
+
### Request locale isolation
|
|
565
|
+
|
|
566
|
+
Wrap Node requests and SSR in `core.i18n.runWithLocale(locale, () => handleRequest())`. SvelteKit can use `kit.i18n.runWithLocale(locale, () => resolve(event))`; serv creates the scope automatically from request headers. Async descendants inherit it. Explicit `MessageOptions.locale` takes precedence over the request locale and the startup default. Within a scope, `setGlobalLocale` only changes that request; outside a scope it configures the startup default and must not be called by request hooks.
|
|
567
|
+
|
|
568
|
+
Browsers keep application-level `setGlobalLocale` / `createLocaleStore`. During SSR, UI store initialization and setters only update the store; messages read the request scope. `runWithLocale` requires the Node entry; browser translations use explicit locale options or application state.
|
|
569
|
+
|
|
570
|
+
### Input limits and HTTP capabilities
|
|
571
|
+
|
|
572
|
+
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#存储输入约束与跨端能力).
|
|
573
|
+
|
|
574
|
+
### Optional feedback service and trusted scope
|
|
575
|
+
|
|
576
|
+
Mount `apiContract.feedback` with `createFeedbackProcedures` from `@h-ai/serv/features/feedback`. The runtime root does not load the feedback service. Initialize reldb/storage/reach/feedback in the host, then map verified sessions and deployment policy to feedback scope. A null resolver result or mismatched user is denied; the domain module also checks scope and permissions.
|
|
577
|
+
|
|
578
|
+
All HTTP responses, including health, 404, and middleware short circuits, carry a bounded X-Request-Id. For ordinary business logs, provide the optional scopeId from the existing createContext factory. It is never read from client headers. The HTTP boundary clears previous identities, and authenticated asynchronous operations inherit the current request and user.
|
|
579
|
+
|
|
580
|
+
```ts
|
|
581
|
+
import { apiContract } from '@h-ai/api-contract'
|
|
582
|
+
import { feedback } from '@h-ai/feedback'
|
|
583
|
+
import { iam } from '@h-ai/iam'
|
|
584
|
+
import { serv } from '@h-ai/serv'
|
|
585
|
+
import { createFeedbackProcedures } from '@h-ai/serv/features/feedback'
|
|
586
|
+
|
|
587
|
+
const contract = apiContract.create({ feedback: apiContract.feedback })
|
|
588
|
+
const authenticate = serv.buildAuthContextFactory(token => iam.auth.verifyToken(token))
|
|
589
|
+
const procedures = {
|
|
590
|
+
feedback: createFeedbackProcedures({
|
|
591
|
+
feedback,
|
|
592
|
+
resolveContext: context => context.scopeId
|
|
593
|
+
? { scopeId: context.scopeId, userId: context.session.userId, permissions: context.session.permissions }
|
|
594
|
+
: null,
|
|
595
|
+
}),
|
|
596
|
+
}
|
|
597
|
+
const app = serv.createApp({
|
|
598
|
+
contract,
|
|
599
|
+
procedures,
|
|
600
|
+
createContext: async input => ({
|
|
601
|
+
...await authenticate(input),
|
|
602
|
+
scopeId: 'configured-product'
|
|
603
|
+
}),
|
|
604
|
+
})
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
### Optional interactive challenges
|
|
608
|
+
|
|
609
|
+
Standard IAM procedures include POST /auth/captcha, taking operation and returning a public ALTCHA challenge. With IAM captcha enabled, password/OTP/LDAP, registration, notification requests and anonymous password renewal enforce one-use proofs in IAM; mappings preserve captcha instead of relying on client checks.
|
|
610
|
+
|
|
611
|
+
See policy, defaults and recovery boundaries in [IAM](../iam/README.en.md).
|
|
612
|
+
|
|
613
|
+
### Page exit session policy
|
|
614
|
+
|
|
615
|
+
`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.
|
|
616
|
+
|
|
617
|
+
## Configuration
|
|
618
|
+
|
|
619
|
+
### Choose configuration by task
|
|
620
|
+
|
|
621
|
+
| Configuration entry | Scenario | Input | Effect | Example |
|
|
622
|
+
| ------------------------------------------------------ | ----------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------- | -------------------------------------------------------------- |
|
|
623
|
+
| `server` | Node listener | host/port; HOST and PORT take precedence | options passed to serv.listen; default 127.0.0.1:3000 | `server: { host: 127.0.0.1, port: 3000 }` |
|
|
624
|
+
| `http.apiPrefix` | API prefix | absolute validated prefix, default /api/v1 | OpenAPI/REST mounting prefix | `http: { apiPrefix: /api/v1 }` |
|
|
625
|
+
| `http.health` | Health and readiness | path, readyPath | health/readiness routes | `health: { path: /health, readyPath: /ready }` |
|
|
626
|
+
| `http.openapi / http.docs` | API documentation | false or explicit path/authorization settings | spec/Scalar routes; production exposure controlled by host policy | `openapi: false` |
|
|
627
|
+
| `http.trustedProxies / http.access` | Trusted ingress | proxy list/auto and access policy | trusted address and per-request entry policy | `trustedProxies: []` |
|
|
628
|
+
| `cors` | Cross-origin rules | origin/nativeOrigins/credentials/allowedHeaders/exposedHeaders | serv.cors middleware input; explicit production allowlist | `cors: { origin: https://app.example.com, credentials: true }` |
|
|
629
|
+
| `transport` | Authenticated transport | false or public paths/limits; serverKeyRing/keyStore injected separately | handshake and authenticated request/response handling | `transport: { maxPlaintextBytes: auto }` |
|
|
630
|
+
| `createApp.iam / refreshCookie / nativeTokenTransport` | Host auth assembly | initialized IAM, secure Cookie policy and native transport rule | trusted session/token channel; frontend receives only allowed fields | `serv.createApp({ contract, procedures, iam })` |
|
|
528
631
|
|
|
529
632
|
`@h-ai/serv` does not scan YAML itself. Load `config/_serv.yml` through `@h-ai/core` and pass the result to `serv.createApp()`.
|
|
530
633
|
|
|
@@ -574,7 +677,6 @@ http:
|
|
|
574
677
|
health:
|
|
575
678
|
path: /health
|
|
576
679
|
readyPath: /ready
|
|
577
|
-
rpc: false
|
|
578
680
|
|
|
579
681
|
cors:
|
|
580
682
|
origin: https://app.example.com
|
|
@@ -601,11 +703,10 @@ transport:
|
|
|
601
703
|
|
|
602
704
|
`ServHttpConfigInput`:
|
|
603
705
|
|
|
604
|
-
- `apiPrefix`: defaults to `/api/v1`.
|
|
706
|
+
- `apiPrefix`: defaults to `/api/v1`; accepts full absolute prefixes such as `/somepath/api/v1`. Segments use letters, digits, underscores, hyphens and dots; trailing slashes, empty segments, standalone `.`/`..`, queries and fragments are rejected. Configure health, docs and public storage paths under the same directory separately.
|
|
605
707
|
- `health`: defaults to `{ path: '/health', readyPath: '/ready' }`.
|
|
606
708
|
- `openapi`: defaults to `false`; enable with `{ path: '/openapi.json' }`.
|
|
607
|
-
- `docs`: defaults to `false`; enable with `{ path: '/docs' }
|
|
608
|
-
- `rpc`: defaults to `false`; enable with `{ prefix: '/rpc', access: 'loopback' | 'private-network' | 'gateway-only', gatewayHeader?, gatewaySecret? }`.
|
|
709
|
+
- `docs`: defaults to `false`; enable with `{ path: '/docs' }`. Scalar is mounted as `_hai/scalar.js` in the document directory (default `/_hai/scalar.js`), without an external CDN. Script and spec URLs are relative. When a gateway preserves a prefix, place docs and OpenAPI paths within it; stripping a prefix also works. Backend `requireAuth` checks remain enforced.
|
|
609
710
|
- `cors`: configured origins, credentials, allowedHeaders, and exposedHeaders; mount through `serv.cors(...)`.
|
|
610
711
|
- `transport`: top-level `config/_serv.yml` option passed as `servConfig.transport` to `createApp({ transport })`; default key exchange subpath is `/_hai/key-exchange`.
|
|
611
712
|
|
|
@@ -626,33 +727,32 @@ const app = serv.createApp({
|
|
|
626
727
|
openapi: security.exposeApiDocs ? servConfig.http.openapi : false,
|
|
627
728
|
docs: security.exposeApiDocs ? servConfig.http.docs : false,
|
|
628
729
|
},
|
|
730
|
+
iam,
|
|
629
731
|
refreshCookie: { secure: security.secureRefreshCookie },
|
|
630
|
-
middlewares: [
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
732
|
+
middlewares: [
|
|
733
|
+
{
|
|
734
|
+
middleware: serv.cors({
|
|
735
|
+
origin: security.allowOrigin,
|
|
736
|
+
credentials: servConfig.cors.credentials,
|
|
737
|
+
allowedHeaders: [...servConfig.cors.allowedHeaders],
|
|
738
|
+
exposedHeaders: [...servConfig.cors.exposedHeaders],
|
|
739
|
+
}),
|
|
740
|
+
}
|
|
741
|
+
],
|
|
638
742
|
})
|
|
639
743
|
```
|
|
640
744
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
### Configuration driven authentication
|
|
745
|
+
Production requires an explicit `_serv.yml` `cors.origin` allowlist and disables OpenAPI and docs. Web and Capacitor/Tauri origins match by their exact normalized values.
|
|
644
746
|
|
|
645
|
-
|
|
747
|
+
For a production HTTP endpoint on a controlled private network or VPN, set `HAI_SERV_REFRESH_COOKIE_SECURE=false` in the backend process environment. After restarting the backend and signing in, login and refresh responses omit `Secure` from the cookie, and HTTP carries the refresh credential in plaintext. `true` forces `Secure`. An empty or unrecognized value uses the environment default. When calling `serv.createApp` directly, an explicit `refreshCookie.secure` takes precedence over this variable, followed by the production check on `NODE_ENV` or `HAI_CORE_ENV`.
|
|
646
748
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
[完整 IAM 配置 / IAM configuration](../iam/README.en.md)
|
|
749
|
+
### Transport capacity and module budgets
|
|
650
750
|
|
|
651
|
-
|
|
751
|
+
## Error Handling
|
|
652
752
|
|
|
653
753
|
Ordinary procedures return authentication, authorization, business failures and caught handler exceptions as HaiResult over HTTP 200 by default. `error.httpStatus` is domain-error metadata and does not change the response status. Clients must check `success` and `error.code`: an unauthenticated failure has `error.httpStatus=401`, while database query failures or INTERNAL_ERROR have 500.
|
|
654
754
|
|
|
655
|
-
oRPC contract input validation fails before procedures with HTTP 400 and a localized body containing `error.httpStatus=400` and field errors. Procedure-level `serv.validateInputOrFail` failures remain HTTP 200. HTTP middleware, refresh-cookie, OpenAPI
|
|
755
|
+
oRPC contract input validation fails before procedures with HTTP 400 and a localized body containing `error.httpStatus=400` and field errors. Procedure-level `serv.validateInputOrFail` failures remain HTTP 200. HTTP middleware, refresh-cookie, OpenAPI access checks and transport protocol failures follow their own HTTP response contracts. api-client refreshes after an actual HTTP 401 or a valid failed HaiResult over HTTP 200 (`success=false`, string `error.code/message` and `error.httpStatus=401`) and retries once on success. Built-in login and registration and the configured refresh path are excluded; 403/500 do not trigger refresh. With transport enabled, refresh and retry share the encrypted fetch path.
|
|
656
756
|
|
|
657
757
|
- All default procedures return `HaiResult<T>`; clients check `result.success`.
|
|
658
758
|
- Authentication failure → `HaiCommonError.UNAUTHORIZED`, localized by `.auth()` using `context.locale`.
|
|
@@ -697,49 +797,9 @@ const procedures = serv
|
|
|
697
797
|
.build()
|
|
698
798
|
```
|
|
699
799
|
|
|
700
|
-
>
|
|
701
|
-
|
|
702
|
-
### Request locale isolation
|
|
703
|
-
|
|
704
|
-
Wrap Node requests and SSR in `core.i18n.runWithLocale(locale, () => handleRequest())`. SvelteKit can use `kit.i18n.runWithLocale(locale, () => resolve(event))`; serv creates the scope automatically from request headers. Async descendants inherit it. Explicit `MessageOptions.locale` takes precedence over the request locale and the startup default. Within a scope, `setGlobalLocale` only changes that request; outside a scope it configures the startup default and must not be called by request hooks.
|
|
705
|
-
|
|
706
|
-
Browsers keep application-level `setGlobalLocale` / `createLocaleStore`. During SSR, UI store initialization and setters only update the store; messages read the request scope. `runWithLocale` requires the Node entry; browser translations use explicit locale options or application state.
|
|
707
|
-
|
|
708
|
-
## Input limits and HTTP capabilities
|
|
709
|
-
|
|
710
|
-
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#存储输入约束与跨端能力).
|
|
800
|
+
> Downstream 4xx messages are sanitized for public responses. 5xx and missing or invalid statuses use a safe internal-error message with a traceId; full causes remain in server logs. `HaiError` has no `messageKey/params`, so domain-message localization still belongs to the module that creates the message with the request locale.
|
|
711
801
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
Mount `apiContract.feedback` with `createFeedbackProcedures` from `@h-ai/serv/features/feedback`. The runtime root does not load the feedback service. Initialize reldb/storage/reach/feedback in the host, then map verified sessions and deployment policy to feedback scope. A null resolver result or mismatched user is denied; the domain module also checks scope and permissions.
|
|
715
|
-
|
|
716
|
-
All HTTP responses, including health, 404, and middleware short circuits, carry a bounded X-Request-Id. For ordinary business logs, provide the optional scopeId from the existing createContext factory. It is never read from client headers. The HTTP boundary clears previous identities, and authenticated asynchronous operations inherit the current request and user.
|
|
717
|
-
|
|
718
|
-
```ts
|
|
719
|
-
import { apiContract } from '@h-ai/api-contract'
|
|
720
|
-
import { feedback } from '@h-ai/feedback'
|
|
721
|
-
import { iam } from '@h-ai/iam'
|
|
722
|
-
import { serv } from '@h-ai/serv'
|
|
723
|
-
import { createFeedbackProcedures } from '@h-ai/serv/features/feedback'
|
|
724
|
-
|
|
725
|
-
const contract = apiContract.create({ feedback: apiContract.feedback })
|
|
726
|
-
const authenticate = serv.buildAuthContextFactory(token => iam.auth.verifyToken(token))
|
|
727
|
-
const procedures = {
|
|
728
|
-
feedback: createFeedbackProcedures({
|
|
729
|
-
feedback,
|
|
730
|
-
resolveContext: context => context.scopeId
|
|
731
|
-
? { scopeId: context.scopeId, userId: context.session.userId, permissions: context.session.permissions }
|
|
732
|
-
: null,
|
|
733
|
-
}),
|
|
734
|
-
}
|
|
735
|
-
const app = serv.createApp({
|
|
736
|
-
contract,
|
|
737
|
-
procedures,
|
|
738
|
-
createContext: async input => ({ ...await authenticate(input), scopeId: 'configured-product' }),
|
|
739
|
-
})
|
|
740
|
-
```
|
|
741
|
-
|
|
742
|
-
## Error messages and correlated logs
|
|
802
|
+
### Error messages and correlated logs
|
|
743
803
|
|
|
744
804
|
Native Audio WebSocket uses the same safe error boundary: internal Provider/SDK messages and stacks remain in sanitized logs, while clients receive stable codes, safe messages and traceId. Custom cross-origin configuration must preserve X-Hai-Reauthentication for sensitive actions; Serv/Kit defaults allow it.
|
|
745
805
|
|
|
@@ -749,19 +809,7 @@ Use `core.error.toPublic(error, traceId?)` at HTTP/UI boundaries and `core.error
|
|
|
749
809
|
|
|
750
810
|
With audit initialized, procedures bind verified actors, trusted client addresses and request/scope IDs; domain audit.log inherits this context. Writes and failed/denied procedures produce request.execute. Contract validation, protocol, middleware and missing-route HTTP errors produce http.reject. Events contain operation/method/status or reason codes, without bodies, dynamic URLs, tokens or internal errors. capture.enabled disables automatic capture and capture.includeReads enables successful reads; explicit domain logs remain active. Applications still record meaningful domain completion/failure events.
|
|
751
811
|
|
|
752
|
-
##
|
|
753
|
-
|
|
754
|
-
Standard IAM procedures include POST /auth/captcha, taking operation and returning a public ALTCHA challenge. With IAM captcha enabled, password/OTP/LDAP, registration, notification requests and anonymous password renewal enforce one-use proofs in IAM; mappings preserve captcha instead of relying on client checks.
|
|
755
|
-
|
|
756
|
-
See policy, defaults and recovery boundaries in [IAM](../iam/README.en.md).
|
|
757
|
-
|
|
758
|
-
## Page exit session policy
|
|
759
|
-
|
|
760
|
-
`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.
|
|
761
|
-
|
|
762
|
-
## Transport capacity and module budgets
|
|
763
|
-
|
|
764
|
-
## Development and testing
|
|
812
|
+
## Development
|
|
765
813
|
|
|
766
814
|
```bash
|
|
767
815
|
pnpm --filter @h-ai/serv test
|