@h-ai/serv 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 +133 -104
- package/README.ja.md +133 -104
- package/README.md +133 -104
- package/package.json +16 -16
package/README.en.md
CHANGED
|
@@ -4,33 +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
|
-
|
|
15
|
-
- Start with `serv.createApp(...)` → `serv.listen(app, { port })`; applications need not import Hono.
|
|
16
|
-
- Flat API: `serv.createApp / serv.implement / serv.listen / serv.toFetch / serv.generateSpec / ...`.
|
|
17
|
-
- Procedure chains: `implement().context().route().auth/permission/role().handle().build()`.
|
|
18
|
-
- Custom pipelines: mount HTTP middleware with `createApp({ middlewares })`; inject request fields through context factories.
|
|
19
|
-
- Read-only storage assets: `serv.storageAssets(...)` handles safe keys, MIME allowlists, GET/HEAD, ETags, and cache headers.
|
|
20
|
-
- Default feature procedures: `createIamProcedures()`, `createStorageProcedures()`, `createAiProcedures()`.
|
|
21
|
-
- `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.
|
|
22
|
-
- Missing routes return localized HaiResult JSON with HTTP 404. Transport encryption preserves that status instead of producing a 500 for a non-JSON response.
|
|
23
|
-
- Built-in security headers and health checks; optional OpenAPI JSON, Scalar docs.
|
|
24
|
-
- Optional transport encryption: `serv.createApp({ transport: { crypto, serverKeyRing, keyStore } })` mounts key exchange and request/response encryption automatically.
|
|
25
|
-
- 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.
|
|
26
|
-
|
|
27
|
-
> 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.
|
|
28
|
-
|
|
29
|
-
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.
|
|
30
|
-
|
|
31
|
-
## Getting started
|
|
11
|
+
## Quick Start
|
|
32
12
|
|
|
33
|
-
|
|
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.
|
|
34
14
|
|
|
35
15
|
```ts
|
|
36
16
|
import { apiContract } from '@h-ai/api-contract'
|
|
@@ -85,7 +65,47 @@ AI procedures bind `objectId` to the trusted `context.session.userId` when the c
|
|
|
85
65
|
|
|
86
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()`.
|
|
87
67
|
|
|
88
|
-
|
|
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
|
|
89
109
|
|
|
90
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:
|
|
91
111
|
|
|
@@ -98,7 +118,7 @@ server:
|
|
|
98
118
|
|
|
99
119
|
Pass the resolved `server` to `serv.listen`; explicit `host` / `port` arguments can also override it.
|
|
100
120
|
|
|
101
|
-
|
|
121
|
+
### Standard fetch signature adapter
|
|
102
122
|
|
|
103
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.
|
|
104
124
|
|
|
@@ -111,7 +131,7 @@ Node `serv.listen()` supplies the socket peer address. Forwarded headers are ign
|
|
|
111
131
|
|
|
112
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.
|
|
113
133
|
|
|
114
|
-
|
|
134
|
+
### Audio WebSocket: tickets and server authorization
|
|
115
135
|
|
|
116
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:
|
|
117
137
|
|
|
@@ -137,7 +157,7 @@ const app = serv.createApp({
|
|
|
137
157
|
- `onSessionEnd` runs at most once after success, failure, or disconnection to release concurrency slots.
|
|
138
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.
|
|
139
159
|
|
|
140
|
-
|
|
160
|
+
### Transport encryption (shared with `@h-ai/crypto`)
|
|
141
161
|
|
|
142
162
|
serv does not expose its own encryption factory; the server manager comes from `crypto.transport`. Applications only pass the top-level `crypto` handle:
|
|
143
163
|
|
|
@@ -230,9 +250,7 @@ const app = serv.createApp({
|
|
|
230
250
|
|
|
231
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.
|
|
232
252
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
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.
|
|
236
254
|
|
|
237
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)。
|
|
238
256
|
|
|
@@ -240,7 +258,7 @@ Ordinary routes retain `transport.maxPlaintextBytes` (auto/omitted: 1 MiB). Decl
|
|
|
240
258
|
|
|
241
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`.
|
|
242
260
|
|
|
243
|
-
|
|
261
|
+
### Token authentication (`iam` / `refreshCookie`)
|
|
244
262
|
|
|
245
263
|
Pass the top-level `iam` handle to `serv.createApp` to derive:
|
|
246
264
|
|
|
@@ -284,7 +302,7 @@ const app = serv.createApp({
|
|
|
284
302
|
|
|
285
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.
|
|
286
304
|
|
|
287
|
-
|
|
305
|
+
### Custom procedures (authentication + permissions)
|
|
288
306
|
|
|
289
307
|
```ts
|
|
290
308
|
const procedures = serv
|
|
@@ -298,7 +316,7 @@ const procedures = serv
|
|
|
298
316
|
.build()
|
|
299
317
|
```
|
|
300
318
|
|
|
301
|
-
|
|
319
|
+
### Advanced pipelines (HTTP middleware + context + route guards)
|
|
302
320
|
|
|
303
321
|
`@h-ai/serv` pipelines have three layers and extension points:
|
|
304
322
|
|
|
@@ -306,7 +324,7 @@ const procedures = serv
|
|
|
306
324
|
2. **Context**: customize through `verifyToken` / `createContext` / `serv.buildAuthContextFactory()`.
|
|
307
325
|
3. **Route guards**: declare authentication/authorization with `.auth()` / `.permission()` / `.role()`.
|
|
308
326
|
|
|
309
|
-
|
|
327
|
+
#### 1) Custom HTTP middleware
|
|
310
328
|
|
|
311
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:
|
|
312
330
|
|
|
@@ -375,7 +393,7 @@ Notes:
|
|
|
375
393
|
- Expose custom response headers such as `X-Encrypted` explicitly through `serv.cors({ exposedHeaders: [...] })` for browsers.
|
|
376
394
|
- This layer works with HTTP middleware context and returns **HTTP Response**, not `HaiResult`.
|
|
377
395
|
|
|
378
|
-
|
|
396
|
+
#### 2) Custom context pipelines
|
|
379
397
|
|
|
380
398
|
To reuse `Bearer token -> session` and add tenant, workspace, or source-system fields,
|
|
381
399
|
prefer `serv.buildAuthContextFactory()` to assemble a custom `createContext`:
|
|
@@ -402,7 +420,7 @@ Choose the appropriate extension:
|
|
|
402
420
|
- Add fields to default authentication context: use `buildAuthContextFactory(...) + createContext`.
|
|
403
421
|
- Replace context construction entirely: use `createContext` directly.
|
|
404
422
|
|
|
405
|
-
|
|
423
|
+
#### 3) Route guard
|
|
406
424
|
|
|
407
425
|
Public procedures use `.route(path, handler)`. For guarded routes, use
|
|
408
426
|
`.route(path).auth/permission/role().handle(handler)`:
|
|
@@ -436,7 +454,7 @@ Public API boundaries:
|
|
|
436
454
|
- The stable application-facing entry remains `@h-ai/serv`.
|
|
437
455
|
- Internal helpers such as `securityHeaders` / `buildHaiErrorBody` are not documented as separate subpath exports.
|
|
438
456
|
|
|
439
|
-
|
|
457
|
+
### httpOnly cookie authentication (recommended for production)
|
|
440
458
|
|
|
441
459
|
httpOnly cookie mode stores refresh tokens in server-managed cookies unreadable to browser JavaScript, reducing XSS exposure.
|
|
442
460
|
|
|
@@ -535,7 +553,81 @@ Use `http.access: { file: './config/security/request-access.yml' }` for larger p
|
|
|
535
553
|
|
|
536
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.
|
|
537
555
|
|
|
538
|
-
### 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 })` |
|
|
539
631
|
|
|
540
632
|
`@h-ai/serv` does not scan YAML itself. Load `config/_serv.yml` through `@h-ai/core` and pass the result to `serv.createApp()`.
|
|
541
633
|
|
|
@@ -611,8 +703,6 @@ transport:
|
|
|
611
703
|
|
|
612
704
|
`ServHttpConfigInput`:
|
|
613
705
|
|
|
614
|
-
Native RPC endpoints, `HAI_SERV_HTTP_RPC` and `ServRpcHttpConfig` have been removed. Existing OpenAPI/REST APIs, `api-client`, and the oRPC contracts and procedures remain supported.
|
|
615
|
-
|
|
616
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.
|
|
617
707
|
- `health`: defaults to `{ path: '/health', readyPath: '/ready' }`.
|
|
618
708
|
- `openapi`: defaults to `false`; enable with `{ path: '/openapi.json' }`.
|
|
@@ -656,15 +746,9 @@ Production requires an explicit `_serv.yml` `cors.origin` allowlist and disables
|
|
|
656
746
|
|
|
657
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`.
|
|
658
748
|
|
|
659
|
-
###
|
|
660
|
-
|
|
661
|
-
Agreements support configuration-loaded userAgreementMarkdown/privacyPolicyMarkdown and URLs. Markdown takes priority for the same agreement and is safely rendered in a modal.
|
|
662
|
-
|
|
663
|
-
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.
|
|
749
|
+
### Transport capacity and module budgets
|
|
664
750
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
### Error handling
|
|
751
|
+
## Error Handling
|
|
668
752
|
|
|
669
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.
|
|
670
754
|
|
|
@@ -715,50 +799,7 @@ const procedures = serv
|
|
|
715
799
|
|
|
716
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.
|
|
717
801
|
|
|
718
|
-
###
|
|
719
|
-
|
|
720
|
-
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.
|
|
721
|
-
|
|
722
|
-
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.
|
|
723
|
-
|
|
724
|
-
## Input limits and HTTP capabilities
|
|
725
|
-
|
|
726
|
-
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#存储输入约束与跨端能力).
|
|
727
|
-
|
|
728
|
-
## Optional feedback service and trusted scope
|
|
729
|
-
|
|
730
|
-
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.
|
|
731
|
-
|
|
732
|
-
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.
|
|
733
|
-
|
|
734
|
-
```ts
|
|
735
|
-
import { apiContract } from '@h-ai/api-contract'
|
|
736
|
-
import { feedback } from '@h-ai/feedback'
|
|
737
|
-
import { iam } from '@h-ai/iam'
|
|
738
|
-
import { serv } from '@h-ai/serv'
|
|
739
|
-
import { createFeedbackProcedures } from '@h-ai/serv/features/feedback'
|
|
740
|
-
|
|
741
|
-
const contract = apiContract.create({ feedback: apiContract.feedback })
|
|
742
|
-
const authenticate = serv.buildAuthContextFactory(token => iam.auth.verifyToken(token))
|
|
743
|
-
const procedures = {
|
|
744
|
-
feedback: createFeedbackProcedures({
|
|
745
|
-
feedback,
|
|
746
|
-
resolveContext: context => context.scopeId
|
|
747
|
-
? { scopeId: context.scopeId, userId: context.session.userId, permissions: context.session.permissions }
|
|
748
|
-
: null,
|
|
749
|
-
}),
|
|
750
|
-
}
|
|
751
|
-
const app = serv.createApp({
|
|
752
|
-
contract,
|
|
753
|
-
procedures,
|
|
754
|
-
createContext: async input => ({
|
|
755
|
-
...await authenticate(input),
|
|
756
|
-
scopeId: 'configured-product'
|
|
757
|
-
}),
|
|
758
|
-
})
|
|
759
|
-
```
|
|
760
|
-
|
|
761
|
-
## Error messages and correlated logs
|
|
802
|
+
### Error messages and correlated logs
|
|
762
803
|
|
|
763
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.
|
|
764
805
|
|
|
@@ -768,19 +809,7 @@ Use `core.error.toPublic(error, traceId?)` at HTTP/UI boundaries and `core.error
|
|
|
768
809
|
|
|
769
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.
|
|
770
811
|
|
|
771
|
-
##
|
|
772
|
-
|
|
773
|
-
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.
|
|
774
|
-
|
|
775
|
-
See policy, defaults and recovery boundaries in [IAM](../iam/README.en.md).
|
|
776
|
-
|
|
777
|
-
## Page exit session policy
|
|
778
|
-
|
|
779
|
-
`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.
|
|
780
|
-
|
|
781
|
-
## Transport capacity and module budgets
|
|
782
|
-
|
|
783
|
-
## Development and testing
|
|
812
|
+
## Development
|
|
784
813
|
|
|
785
814
|
```bash
|
|
786
815
|
pnpm --filter @h-ai/serv test
|
package/README.ja.md
CHANGED
|
@@ -4,33 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
[フレームワークのホーム](../../README.ja.md) · 関連モジュール:[@h-ai/ai](../ai/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) · [@h-ai/iam](../iam/README.ja.md) · [@h-ai/storage](../storage/README.ja.md)
|
|
6
6
|
|
|
7
|
-
## 概要
|
|
8
|
-
|
|
9
7
|
hai-framework の API サービスランタイム。公開 HTTP App 抽象を通じて `@h-ai/api-contract` を複数プラットフォームから使える HTTP API として提供します。Hono は内部実装です。
|
|
10
8
|
|
|
11
9
|
実行時の操作は `serv.xx` を使います。名前空間の型注釈や再エクスポートにはルート入口の `ServFunctions` を利用できます。context ファクトリーと汎用 `implement` の公開シグネチャーは利用側の宣言生成に対応し、内部ビルドパスや独立した helper は不要です。
|
|
12
10
|
|
|
13
|
-
##
|
|
14
|
-
|
|
15
|
-
- `serv.createApp(...)` → `serv.listen(app, { port })` で起動。アプリは Hono を import する必要がありません。
|
|
16
|
-
- フラットな API:`serv.createApp / serv.implement / serv.listen / serv.toFetch / serv.generateSpec / ...`。
|
|
17
|
-
- procedure のチェーン:`implement().context().route().auth/permission/role().handle().build()`。
|
|
18
|
-
- 独自パイプライン:`createApp({ middlewares })` で HTTP middleware を接続し、context ファクトリーでリクエストごとの値を注入。
|
|
19
|
-
- 読み取り専用 Storage:`serv.storageAssets(...)` で安全な key、MIME 許可リスト、GET/HEAD、ETag、キャッシュヘッダーを管理。
|
|
20
|
-
- 既定の機能 procedures:`createIamProcedures()`、`createStorageProcedures()`、`createAiProcedures()`。
|
|
21
|
-
- `build()` は通常の router を同期的に返すため、標準 feature のネストした procedures を直接選択できます。接続する contract と procedures を一致させ、未選択の入口は公開しません。
|
|
22
|
-
- 存在しないルートは翻訳された HaiResult JSON と HTTP 404 を返します。通信暗号化でも 404 を保ち、非 JSON 応答による 500 にしません。
|
|
23
|
-
- 安全な応答ヘッダー・ヘルスチェックを内蔵し、OpenAPI JSON・Scalar 文書・
|
|
24
|
-
- 任意の通信暗号化:`serv.createApp({ transport: { crypto, serverKeyRing, keyStore } })` で鍵交換とリクエスト・レスポンスの暗号化を自動接続。
|
|
25
|
-
- 任意の音声入口:`serv.createApp({ audio: { ai, verifyTicket, authorize } })` が共通 WebSocket を接続します。接続直後に単回 ticket で認証(`{ session, grant? }`、独立した事前認証 timeout)。各フレームを Zod と厳密な状態機械で検証し、入力・キュー・送信バッファの背圧と連鎖キャンセルを管理します。サーバーの認可処理で操作・モデル・声・割当量・同時使用数を確認します。
|
|
26
|
-
|
|
27
|
-
> ライフサイクル:`@h-ai/serv` は状態を持たない HTTP App の構成機能で、`init()` / `close()` は不要です。app 作成前に `iam` / `storage` / `ai` / `crypto` などを初期化し、`serv.listen(..., { onClose })` で逆順に解放します。`serv.createApp()` は transport 管理器の生成失敗など起動設定のエラーを即座に throw します。 onClose は SIGINT/SIGTERM 用の終了コールバックです。返された handle.close の直接呼出しは HTTP リスナーと音声接続を閉じ、業務モジュールはホストが別途逆順に解放します。
|
|
28
|
-
|
|
29
|
-
開始直後の close は未完了のバインドを取り消し、繰り返しの close は同じ結果を共有します。アドレス使用中など実際のリッスン失敗は正常終了として扱わず、close の拒否理由として保持します。
|
|
30
|
-
|
|
31
|
-
## 使い始める
|
|
11
|
+
## クイックスタート
|
|
32
12
|
|
|
33
|
-
|
|
13
|
+
以下の組立て例は、アプリで初期化済みの iam・storage・ai・core・logger と、アプリ側の closeApp を前提とします。初期化スクリプトではありません。健康確認だけなら空の contract/procedures を使えます。serv に init/close はなく、終了時はリスナーと業務モジュールを逆順に閉じます。
|
|
34
14
|
|
|
35
15
|
```ts
|
|
36
16
|
import { apiContract } from '@h-ai/api-contract'
|
|
@@ -85,7 +65,47 @@ AI procedures は省略された `objectId` を信頼できる `context.session.
|
|
|
85
65
|
|
|
86
66
|
`chats.createCompletion`(`POST /ai/chats/completions`)は非ストリーミングの `HaiResult<AiChatCompletionData>` を一度返します。function `tools` のネストした JSON Schema と `strict`、`tool_choice`(`auto` / `none` / `required` / 提供した関数名)をモデルへ渡し、役割、`name`、ユーザー画像、ツール呼び出しを保持します。ツール結果には `role: 'tool'` と空でない `tool_call_id` が必須で、指定名は今回のツール定義に含まれる必要があります。procedure はツールを実行しません。アプリ側で認可・実行後、助手の `tool_calls` と対応する結果を送信します。入力に `stream` はなく、`true` / `false` とも Provider 呼び出し前に HTTP 400 で拒否します。ストリーミングには `ai.llm.chatStream()` と対応する出力型を使う専用 endpoint を定義します。
|
|
87
67
|
|
|
88
|
-
|
|
68
|
+
## 機能
|
|
69
|
+
|
|
70
|
+
### 用途・入力・出力
|
|
71
|
+
|
|
72
|
+
呼出し断片は本ページで初期化したサービスを使います。変数はアプリ側で用意する要求・Schema・リソース定義・設定を表します。初期化と失敗処理は各機能の例を参照し、HaiResult.success の確認と非同期処理の await を行ってください。
|
|
73
|
+
|
|
74
|
+
| 入口 | 用途 | 入力 | 出力 | 呼出し断片 |
|
|
75
|
+
| ---------------------------------- | -------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
76
|
+
| `serv.createApp` | API アプリを組立て | `contract, procedures, http?, iam?, middlewares?, transport?` | `ServHttpApp (fetch/request)` | `serv.createApp({ contract, procedures })` |
|
|
77
|
+
| `serv.listen` | Node リスナーを起動 | `app, { host?, port?, onClose?, onListening? }` | `ServNodeServer { server, address, close(): Promise<void> }` | `serv.listen(app, { host: "127.0.0.1", port: 3000 })` |
|
|
78
|
+
| `serv.toFetch` | Fetch ホストへ接続 | `ServHttpApp` | `(Request) => Promise<Response>` | `serv.toFetch(app)` |
|
|
79
|
+
| `serv.implement` | 領域ルートを実装 | `typed contract` | `ServImplementer; context/route/auth/permission/role/handle/build` | `serv.implement(contract)` |
|
|
80
|
+
| `serv.parseRequestContext` | 要求情報を解析 | `{ request, platform? }` | `ServContext with requestId/locale/accessToken; no verified session` | `serv.parseRequestContext({ request })` |
|
|
81
|
+
| `serv.buildAuthContextFactory` | 認証済み文脈を作成 | `(accessToken) => Promise<HaiResult<ServSession>>` | `CreateServContext; invalid credentials leave session absent` | `serv.buildAuthContextFactory(token => iam.auth.verifyToken(token))` |
|
|
82
|
+
| `serv.cors` | CORS を制限 | `{ origin?, methods?, allowedHeaders?, exposedHeaders?, credentials? }` | `ServMiddleware` | `serv.cors({ origin: "https://app.example.com", credentials: true })` |
|
|
83
|
+
| `serv.createRuntimeSecurityPolicy` | 実行時の安全方針 | `{ environment, corsOrigin, nativeOrigins? }` | `allowOrigin, secureRefreshCookie, exposeApiDocs` | `serv.createRuntimeSecurityPolicy({ environment: "production", corsOrigin: "https://app.example.com" })` |
|
|
84
|
+
| `serv.storageAssets` | 認可済み読取専用資源 | `ServStorageAssetsConfig with reader/key/MIME policy` | `GET/HEAD middleware with ETag` | `serv.storageAssets(assetOptions)` |
|
|
85
|
+
| `serv.generateSpec` | OpenAPI を生成 | `contract, { title?, version?, description?, apiPrefix? }` | `Promise<OpenAPI.Document>` | `serv.generateSpec(contract, { title: "Orders", version: "1.0.0" })` |
|
|
86
|
+
| `serv.validateInputOrFail` | 業務入力を検証 | `Zod schema, input, locale?` | `HaiResult<z.infer<Schema>>; failure contains safe field errors` | `serv.validateInputOrFail(InputSchema, input, context.locale)` |
|
|
87
|
+
| `serv.resolveRequestLocale` | 要求言語を解決 | `Headers` | `normalized supported locale` | `serv.resolveRequestLocale(request.headers)` |
|
|
88
|
+
| `serv.m` | モジュール文言を翻訳 | `message key, { locale?, ...messageOptions }` | `string` | `serv.m("serv_errorForbidden", { locale: "zh-CN" })` |
|
|
89
|
+
| `serv.WILDCARD_PERMISSION` | 全権限定数 | `none` | `string constant; only use in trusted permission policy` | `serv.WILDCARD_PERMISSION` |
|
|
90
|
+
| `serv.WILDCARD_ROLE` | 全ロール定数 | `none` | `string constant; does not authorize arbitrary requests` | `serv.WILDCARD_ROLE` |
|
|
91
|
+
|
|
92
|
+
- `serv.createApp(...)` → `serv.listen(app, { port })` で起動。アプリは Hono を import する必要がありません。
|
|
93
|
+
- フラットな API:`serv.createApp / serv.implement / serv.listen / serv.toFetch / serv.generateSpec / ...`。
|
|
94
|
+
- procedure のチェーン:`implement().context().route().auth/permission/role().handle().build()`。
|
|
95
|
+
- 独自パイプライン:`createApp({ middlewares })` で HTTP middleware を接続し、context ファクトリーでリクエストごとの値を注入。
|
|
96
|
+
- 読み取り専用 Storage:`serv.storageAssets(...)` で安全な key、MIME 許可リスト、GET/HEAD、ETag、キャッシュヘッダーを管理。
|
|
97
|
+
- 既定の機能 procedures:`createIamProcedures()`、`createStorageProcedures()`、`createAiProcedures()`。
|
|
98
|
+
- `build()` は通常の router を同期的に返すため、標準 feature のネストした procedures を直接選択できます。接続する contract と procedures を一致させ、未選択の入口は公開しません。
|
|
99
|
+
- 存在しないルートは翻訳された HaiResult JSON と HTTP 404 を返します。通信暗号化でも 404 を保ち、非 JSON 応答による 500 にしません。
|
|
100
|
+
- 安全な応答ヘッダー・ヘルスチェックを内蔵し、OpenAPI JSON・Scalar 文書・
|
|
101
|
+
- 任意の通信暗号化:`serv.createApp({ transport: { crypto, serverKeyRing, keyStore } })` で鍵交換とリクエスト・レスポンスの暗号化を自動接続。
|
|
102
|
+
- 任意の音声入口:`serv.createApp({ audio: { ai, verifyTicket, authorize } })` が共通 WebSocket を接続します。接続直後に単回 ticket で認証(`{ session, grant? }`、独立した事前認証 timeout)。各フレームを Zod と厳密な状態機械で検証し、入力・キュー・送信バッファの背圧と連鎖キャンセルを管理します。サーバーの認可処理で操作・モデル・声・割当量・同時使用数を確認します。
|
|
103
|
+
|
|
104
|
+
> ライフサイクル:`@h-ai/serv` は状態を持たない HTTP App の構成機能で、`init()` / `close()` は不要です。app 作成前に `iam` / `storage` / `ai` / `crypto` などを初期化し、`serv.listen(..., { onClose })` で逆順に解放します。`serv.createApp()` は transport 管理器の生成失敗など起動設定のエラーを即座に throw します。 onClose は SIGINT/SIGTERM 用の終了コールバックです。返された handle.close の直接呼出しは HTTP リスナーと音声接続を閉じ、業務モジュールはホストが別途逆順に解放します。
|
|
105
|
+
|
|
106
|
+
開始直後の close は未完了のバインドを取り消し、繰り返しの close は同じ結果を共有します。アドレス使用中など実際のリッスン失敗は正常終了として扱わず、close の拒否理由として保持します。
|
|
107
|
+
|
|
108
|
+
### host / port 設定
|
|
89
109
|
|
|
90
110
|
host/port は `config/_serv.yml` の `server` で指定し、`HOST` / `PORT` 環境変数 > 設定ファイル > `127.0.0.1:3000` の順です。既定の `127.0.0.1` はローカル専用で、コンテナー外に公開する場合は `0.0.0.0` にします:
|
|
91
111
|
|
|
@@ -98,7 +118,7 @@ server:
|
|
|
98
118
|
|
|
99
119
|
解決済みの `server` を `serv.listen` に渡します。`host` / `port` の明示的な指定で上書きすることもできます。
|
|
100
120
|
|
|
101
|
-
|
|
121
|
+
### 標準 fetch シグネチャのアダプター
|
|
102
122
|
|
|
103
123
|
toFetch は Node 互換ホストやテスト注入用の標準 fetch シグネチャです。パッケージルートは Node API に依存しており、それらを持たない Workers・Deno・エッジへの対応を保証しません。
|
|
104
124
|
|
|
@@ -111,7 +131,7 @@ Node `serv.listen()` は socket の接続元を自動で渡し、既定では転
|
|
|
111
131
|
|
|
112
132
|
toFetch は Request・URL・文字列を標準 Request(input, init) で構築し、入力フィールドを変更せず method・headers・body・signal などを上書きします。消費済み・ロック中の本文や不正な本文指定は返却 Promise を reject します。派発前の取消しは handler を呼ばず、実行中の取消しは呼出側の待機を終了し、有効な signal をアプリへ伝えます。終了・失敗・取消しで listener を外し、取消し後に遅れて返る本文も cancel します。開始済みの外部効果は巻き戻せず、Response 返却後のストリーム終了はアプリが request.signal を消費して管理します。
|
|
113
133
|
|
|
114
|
-
|
|
134
|
+
### Audio WebSocket:ticket とサーバー認可
|
|
115
135
|
|
|
116
136
|
音声接続には短期・単回 ticket が必要です。認証済み HTTP endpoint で発行(推奨:`iam.ticket.issue`)し、`verifyTicket` でアトミックに消費します(推奨:`iam.ticket.consume`)。接続直後に独立 timeout で認証し、認証前に業務フレームを処理しません。返された `ServSession` と任意の `grant` を保存してから `authorize` で有料パラメーターを確認します:
|
|
117
137
|
|
|
@@ -137,7 +157,7 @@ const app = serv.createApp({
|
|
|
137
157
|
- `onSessionEnd` は成功・失敗・切断後に最大 1 回呼び、同時実行枠の解放に使用。
|
|
138
158
|
- ブラウザーの `createAudioClient({ url, getTicket })` は単回 `ticket` のみを URL に付けます。`getTicket(request)` は `operation` / `model` の概要から今回の操作専用 ticket を発行できます。通常の IAM access token は渡さないでください。
|
|
139
159
|
|
|
140
|
-
|
|
160
|
+
### 通信暗号化(`@h-ai/crypto` と共通)
|
|
141
161
|
|
|
142
162
|
serv 独自の暗号化ファクトリーはなく、`crypto.transport` から管理器を作成します。利用側はトップレベルの `crypto` のみを渡します:
|
|
143
163
|
|
|
@@ -230,9 +250,7 @@ const app = serv.createApp({
|
|
|
230
250
|
|
|
231
251
|
> **安全方針**:`transport` 有効時は `excludePaths` と鍵交換以外で有効な `X-Client-Id` が必要です。不足・未知の clientId は 400、暗号化不能・非 JSON 応答・上限超過はエラーになり、平文の業務データは渡しません。
|
|
232
252
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
空の POST も空平文の認証封装を送ります。GET/HEAD は X-Hai-Transport ヘッダー、204/304/HEAD など本文なし応答も同じヘッダーを使います。他の要求・応答は JSON 本文と X-Encrypted: true を使います。クライアントは元の要求コンテキストと HTTP status を照合し、マーカー欠落、旧形式、認証失敗では reject します。平文 endpoint は Serv/Kit の `encryption: disabled` ルールまたはホストの `excludePaths` で明示的に許可し、呼出側も通常の要求を選びます。API Client/Kit のラッパーは自身の `excludePaths` を使用します。CORS では X-Hai-Transport を許可・公開してください。鍵交換の公開鍵認証は TLS に依存し、IAM の業務認可は別に必要です。
|
|
253
|
+
転送は認証済み v2 封装を使い、署名と MAC を確認してから復号し、方法・パス・時刻・応答状態を束縛します。共有 registry が nonce を原子的に消費します。TLS・信頼する代理・業務認可・冪等性はホストの責任です。独自 CORS 入口は X-Hai-Transport を許可・公開してください。詳細は[転送プロトコル](../crypto/TRANSPORT-PROTOCOL.md)を参照してください。
|
|
236
254
|
|
|
237
255
|
`transport.maxPlaintextBytes` でリクエストとレスポンスの JSON 平文容量を共通設定します。単位は UTF-8 バイト、既定値は 1 MiB、有効範囲は 1 KiB~64 MiB の整数です。クライアントは鍵交換で上限を取得し、`resolveTransportBodyLimits()` が暗号文とプロキシ容量(平文の 2 倍 + 32 KiB)を算出します。鍵交換、公開鍵、認証ヘッダー、防リプレイ制約は独立です。超過時は元ストリームを停止して reader を解放します。要求は 413、応答は暗号化された 500 容量エラーとなり、`TransportBodyTooLargeError` / `hai:crypto:070` で識別できます。 [Transport capacity and upgrade](../crypto/TRANSPORT-CAPACITY.md)。
|
|
238
256
|
|
|
@@ -240,7 +258,7 @@ const app = serv.createApp({
|
|
|
240
258
|
|
|
241
259
|
Serv/Kit は要求の結合、署名、MAC を検証した後、時刻の許容範囲を超えた要求に暗号化した HTTP 400 を返します。クライアントは `TransportTimeError`(`hai:crypto:071`)をスローし、検証時のサーバー UTC 時刻を表示して、自動時刻同期を有効にするか手動で時刻を設定してから再読み込みするよう案内します。`serverTime` は Unix ミリ秒です。旧サーバーが値を返さない場合も時刻調整の案内を表示します。`api-client` は `VALIDATION_FAILED` に変換し、`cause.transportCode` と `cause.serverTime` を保持します。
|
|
242
260
|
|
|
243
|
-
|
|
261
|
+
### Token 認証(`iam` / `refreshCookie`)
|
|
244
262
|
|
|
245
263
|
`serv.createApp` にトップレベルの `iam` を渡すと、次を自動構成します:
|
|
246
264
|
|
|
@@ -284,7 +302,7 @@ const app = serv.createApp({
|
|
|
284
302
|
|
|
285
303
|
標準 `auth.logout` は input.accessToken または Authorization Bearer の完全な Token を取り消し、有効な access 認証を前提にしません。userId による権限や取消対象は受け付けません。Token 不足は失敗、不存在 Token は冪等成功です。共有ストア障害は失敗 HaiResult を保ち、Cookie middleware は成功時だけ refresh Cookie を消去します。通常 procedure の HTTP 200 / HaiResult 契約は同じなので、クライアントは HTTP status だけでなく success を確認します。
|
|
286
304
|
|
|
287
|
-
|
|
305
|
+
### 独自 procedure(認証 + 権限)
|
|
288
306
|
|
|
289
307
|
```ts
|
|
290
308
|
const procedures = serv
|
|
@@ -298,7 +316,7 @@ const procedures = serv
|
|
|
298
316
|
.build()
|
|
299
317
|
```
|
|
300
318
|
|
|
301
|
-
|
|
319
|
+
### 高度なパイプライン(HTTP middleware + context + route guard)
|
|
302
320
|
|
|
303
321
|
`@h-ai/serv` のパイプラインは 3 層で、それぞれの拡張入口があります:
|
|
304
322
|
|
|
@@ -306,7 +324,7 @@ const procedures = serv
|
|
|
306
324
|
2. **context 層**:`verifyToken` / `createContext` / `serv.buildAuthContextFactory()` で拡張。
|
|
307
325
|
3. **route guard 層**:`.auth()` / `.permission()` / `.role()` で認証・認可を宣言。
|
|
308
326
|
|
|
309
|
-
|
|
327
|
+
#### 1) 独自 HTTP middleware
|
|
310
328
|
|
|
311
329
|
ログ・trace・レート制限・CORS・テナントヘッダーなどの **HTTP 共通処理**、または JSON/oRPC contract に適さない WebSocket・ファイル・バイナリー endpoint には `middlewares` を使います。一般的な CORS は `serv.cors(...)` を再利用できます:
|
|
312
330
|
|
|
@@ -375,7 +393,7 @@ const app = serv.createApp({
|
|
|
375
393
|
- ブラウザーが `X-Encrypted` などを読む場合は `serv.cors({ exposedHeaders: [...] })` で明示的に公開。
|
|
376
394
|
- この層は HTTP middleware context を操作し、`HaiResult` ではなく **HTTP Response** を返します。
|
|
377
395
|
|
|
378
|
-
|
|
396
|
+
#### 2) 独自 context パイプライン
|
|
379
397
|
|
|
380
398
|
`Bearer token -> session` を再利用し、テナント・workspace・送信元などの項目を追加する場合は、
|
|
381
399
|
`serv.buildAuthContextFactory()` で独自の `createContext` を構成してください:
|
|
@@ -402,7 +420,7 @@ const app = serv.createApp({
|
|
|
402
420
|
- 既定の認証 context に項目を追加:`buildAuthContextFactory(...) + createContext`。
|
|
403
421
|
- context 生成全体を置換:直接 `createContext`。
|
|
404
422
|
|
|
405
|
-
|
|
423
|
+
#### 3) Route guard
|
|
406
424
|
|
|
407
425
|
公開 procedure は `.route(path, handler)` を使い、ガードが必要な場合は次を使います:
|
|
408
426
|
`.route(path).auth/permission/role().handle(handler)`:
|
|
@@ -436,7 +454,7 @@ const procedures = serv
|
|
|
436
454
|
- アプリ向けの安定した入口はルートの `@h-ai/serv`。
|
|
437
455
|
- `securityHeaders` / `buildHaiErrorBody` などの内部 helper は独立サブパスとして公開しません。
|
|
438
456
|
|
|
439
|
-
|
|
457
|
+
### httpOnly Cookie 認証(本番推奨)
|
|
440
458
|
|
|
441
459
|
httpOnly cookie モードは refresh token をサーバー管理の Cookie に保存し、ブラウザー JS から読めなくして XSS による漏洩を抑えます。
|
|
442
460
|
|
|
@@ -535,7 +553,81 @@ await apiClient.init({
|
|
|
535
553
|
|
|
536
554
|
`http.trustedProxies` は既定 `auto` です。通常のホストでは転送ヘッダーを無視し、管理対象 Compose の非公開バックエンド構成だけでコンテナのネットワークを採用します。独自の代理は実際の IP/CIDR を指定し、`[]` は信頼を無効化します。代理はヘッダーを清掃し、Node・Fetch は信頼できる接続元情報を提供します。独立ファイル・外側の代理・復旧は[リクエスト制御手引き](../../docs/usage/configuration/request-access.md)を参照してください。
|
|
537
555
|
|
|
538
|
-
###
|
|
556
|
+
### 設定による認証画面
|
|
557
|
+
|
|
558
|
+
規約は設定の userAgreementMarkdown/privacyPolicyMarkdown または URL を使用できます。同じ規約は Markdown 本文を優先して安全な Modal で描画します。
|
|
559
|
+
|
|
560
|
+
createIamProcedures({ iam }) は公開設定、LDAP/OTP/パスワード、登録、復旧を直接実装します。LDAP も既存の Cookie/ネイティブ Token 方針を使用し、自助登録が無効でも管理者のユーザー作成は権限検証の下で利用できます。
|
|
561
|
+
|
|
562
|
+
[完整 IAM 配置 / IAM configuration](../iam/README.ja.md)
|
|
563
|
+
|
|
564
|
+
### リクエスト言語の分離
|
|
565
|
+
|
|
566
|
+
Node リクエストと SSR は `core.i18n.runWithLocale(locale, () => handleRequest())` で囲みます。SvelteKit は `kit.i18n.runWithLocale(locale, () => resolve(event))` を使えます。serv はリクエストヘッダーから自動設定します。非同期の子処理も言語を継承し、明示的な `MessageOptions.locale`、リクエスト言語、起動時の既定値の順で優先します。コンテキスト内の `setGlobalLocale` はそのリクエストだけを変更します。コンテキスト外の設定は起動時に限定し、リクエスト hook から呼びません。
|
|
567
|
+
|
|
568
|
+
ブラウザーはアプリ全体の `setGlobalLocale` / `createLocaleStore` を使います。SSR の UI store 初期化と setter は自身の状態のみ変更し、メッセージはリクエストコンテキストを参照します。`runWithLocale` は Node 入口専用です。
|
|
569
|
+
|
|
570
|
+
### 入力制約と HTTP 機能
|
|
571
|
+
|
|
572
|
+
キーは 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#存储输入约束与跨端能力)を参照してください。
|
|
573
|
+
|
|
574
|
+
### オプションのフィードバックサービスと信頼範囲
|
|
575
|
+
|
|
576
|
+
`@h-ai/serv/features/feedback` の `createFeedbackProcedures` で `apiContract.feedback` を接続します。通常のランタイム入口は feedback サービスを読み込みません。ホストで reldb/storage/reach/feedback を初期化し、検証済みセッションと配置ポリシーから範囲を作ります。null またはユーザー不一致は拒否し、ドメイン側も範囲と権限を検証します。
|
|
577
|
+
|
|
578
|
+
ヘルスチェック・404・中間件の早期応答を含む全 HTTP 応答に上限付き X-Request-Id を付けます。通常の業務ログでは既存 createContext の省略可能な scopeId を信頼できるホストから設定します。クライアントのヘッダーからは取得しません。HTTP 境界で古い認証情報を消去し、認証後の非同期処理に現在の ID とユーザーを伝播します。
|
|
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
|
+
### 任意の対話チャレンジ
|
|
608
|
+
|
|
609
|
+
標準 IAM procedure は operation を受けて公開 ALTCHA チャレンジを返す POST /auth/captcha を含みます。有効時はパスワード/OTP/LDAP、登録、通知要求、匿名の旧パスワード更新で IAM が単用証明を検証し、captcha を保持してクライアント判断に依存しません。
|
|
610
|
+
|
|
611
|
+
方針・既定値・復旧範囲は次を参照: [IAM](../iam/README.ja.md)。
|
|
612
|
+
|
|
613
|
+
### ページ離脱時のセッション方針
|
|
614
|
+
|
|
615
|
+
`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` を併用してください。閉じた瞬間の失効は保証できません。
|
|
616
|
+
|
|
617
|
+
## 設定
|
|
618
|
+
|
|
619
|
+
### 用途に応じた設定入口
|
|
620
|
+
|
|
621
|
+
| 設定入口 | 用途 | 入力 | 効果 | 例 |
|
|
622
|
+
| ------------------------------------------------------ | ------------------ | ------------------------------------------------------------------------ | -------------------------------------------------------------------- | -------------------------------------------------------------- |
|
|
623
|
+
| `server` | Node リスナー | 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 接頭辞 | absolute validated prefix, default /api/v1 | OpenAPI/REST mounting prefix | `http: { apiPrefix: /api/v1 }` |
|
|
625
|
+
| `http.health` | 生存と準備確認 | path, readyPath | health/readiness routes | `health: { path: /health, readyPath: /ready }` |
|
|
626
|
+
| `http.openapi / http.docs` | API 文書 | false or explicit path/authorization settings | spec/Scalar routes; production exposure controlled by host policy | `openapi: false` |
|
|
627
|
+
| `http.trustedProxies / http.access` | 信頼する入口 | proxy list/auto and access policy | trusted address and per-request entry policy | `trustedProxies: []` |
|
|
628
|
+
| `cors` | CORS 方針 | origin/nativeOrigins/credentials/allowedHeaders/exposedHeaders | serv.cors middleware input; explicit production allowlist | `cors: { origin: https://app.example.com, credentials: true }` |
|
|
629
|
+
| `transport` | 認証付き転送 | false or public paths/limits; serverKeyRing/keyStore injected separately | handshake and authenticated request/response handling | `transport: { maxPlaintextBytes: auto }` |
|
|
630
|
+
| `createApp.iam / refreshCookie / nativeTokenTransport` | ホスト認証の組立て | initialized IAM, secure Cookie policy and native transport rule | trusted session/token channel; frontend receives only allowed fields | `serv.createApp({ contract, procedures, iam })` |
|
|
539
631
|
|
|
540
632
|
`@h-ai/serv` 自体は YAML を走査しません。`@h-ai/core` で `config/_serv.yml` を読み、解析結果を `serv.createApp()` に渡してください。
|
|
541
633
|
|
|
@@ -611,8 +703,6 @@ transport:
|
|
|
611
703
|
|
|
612
704
|
`ServHttpConfigInput`:
|
|
613
705
|
|
|
614
|
-
ネイティブ RPC 入口、`HAI_SERV_HTTP_RPC`、`ServRpcHttpConfig` は削除しました。既存の OpenAPI/REST API、`api-client`、oRPC contract と procedure は引き続き利用できます。
|
|
615
|
-
|
|
616
706
|
- `apiPrefix`:既定 `/api/v1`。`/somepath/api/v1` などの絶対接頭辞を指定できます。英数字・アンダースコア・ハイフン・点を使用し、末尾のスラッシュ、空区間、単独の `.`/`..`、クエリとフラグメントは拒否します。ヘルス・文書・公開ストレージのパスも同じディレクトリに別途設定します。
|
|
617
707
|
- `health`:既定 `{ path: '/health', readyPath: '/ready' }`。
|
|
618
708
|
- `openapi`:既定 `false`。`{ path: '/openapi.json' }` で有効化。
|
|
@@ -656,15 +746,11 @@ const app = serv.createApp({
|
|
|
656
746
|
|
|
657
747
|
管理されたプライベートネットワークや VPN の本番 HTTP 入口では、バックエンドのプロセス環境に `HAI_SERV_REFRESH_COOKIE_SECURE=false` を設定できます。再起動してログインし直すと、ログイン応答と更新応答の Cookie から `Secure` が外れ、HTTP でリフレッシュ認証情報が平文で送信されます。`true` は `Secure` を強制し、空欄や未対応の値では環境の既定値が使われます。`serv.createApp` を直接呼ぶ場合は、明示した `refreshCookie.secure`、この環境変数、`NODE_ENV` または `HAI_CORE_ENV` による本番判定の順に適用されます。
|
|
658
748
|
|
|
659
|
-
###
|
|
749
|
+
### 転送容量とモジュール予算
|
|
660
750
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
createIamProcedures({ iam }) は公開設定、LDAP/OTP/パスワード、登録、復旧を直接実装します。LDAP も既存の Cookie/ネイティブ Token 方針を使用し、自助登録が無効でも管理者のユーザー作成は権限検証の下で利用できます。
|
|
664
|
-
|
|
665
|
-
[完整 IAM 配置 / IAM configuration](../iam/README.ja.md)
|
|
751
|
+
設定 transport.maxPlaintextBytes は 1 KiB~64 MiB の明示整数または auto を受け付けます。auto/省略は undefined に正規化され、要件なしの既定は 1 MiB、フィードバックのホストでは crypto.resolveTransportBodyLimits の requiredPlaintextBytes で統合します。明示上限の不足は起動/梱包時の失敗とし、上書きしません。プロキシ・adapter-node・ハンドシェイク・実行時の予算を一致させ、添付・展開・行数の制限は独立して検証します。
|
|
666
752
|
|
|
667
|
-
|
|
753
|
+
## エラー処理
|
|
668
754
|
|
|
669
755
|
通常の procedure は認証・認可・業務の失敗と捕捉した handler 例外を HaiResult として既定で HTTP 200 で返します。`error.httpStatus` はドメインエラーのメタデータで、HTTP 応答ステータスを変更しません。クライアントは `success` と `error.code` を確認してください。未認証では `error.httpStatus=401`、DB クエリー失敗や INTERNAL_ERROR では 500 です。
|
|
670
756
|
|
|
@@ -715,50 +801,7 @@ const procedures = serv
|
|
|
715
801
|
|
|
716
802
|
> 下流の 4xx メッセージは秘匿処理後に公開します。5xx と欠落・不正なステータスは traceId 付きの安全な内部エラーメッセージに統一し、完全な原因はサーバーログだけに記録します。`HaiError` は `messageKey/params` を持たないため、ドメインメッセージの i18n は生成元のモジュールでリクエストの locale を受け取って行います。
|
|
717
803
|
|
|
718
|
-
###
|
|
719
|
-
|
|
720
|
-
Node リクエストと SSR は `core.i18n.runWithLocale(locale, () => handleRequest())` で囲みます。SvelteKit は `kit.i18n.runWithLocale(locale, () => resolve(event))` を使えます。serv はリクエストヘッダーから自動設定します。非同期の子処理も言語を継承し、明示的な `MessageOptions.locale`、リクエスト言語、起動時の既定値の順で優先します。コンテキスト内の `setGlobalLocale` はそのリクエストだけを変更します。コンテキスト外の設定は起動時に限定し、リクエスト hook から呼びません。
|
|
721
|
-
|
|
722
|
-
ブラウザーはアプリ全体の `setGlobalLocale` / `createLocaleStore` を使います。SSR の UI store 初期化と setter は自身の状態のみ変更し、メッセージはリクエストコンテキストを参照します。`runWithLocale` は Node 入口専用です。
|
|
723
|
-
|
|
724
|
-
## 入力制約と HTTP 機能
|
|
725
|
-
|
|
726
|
-
キーは 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#存储输入约束与跨端能力)を参照してください。
|
|
727
|
-
|
|
728
|
-
## オプションのフィードバックサービスと信頼範囲
|
|
729
|
-
|
|
730
|
-
`@h-ai/serv/features/feedback` の `createFeedbackProcedures` で `apiContract.feedback` を接続します。通常のランタイム入口は feedback サービスを読み込みません。ホストで reldb/storage/reach/feedback を初期化し、検証済みセッションと配置ポリシーから範囲を作ります。null またはユーザー不一致は拒否し、ドメイン側も範囲と権限を検証します。
|
|
731
|
-
|
|
732
|
-
ヘルスチェック・404・中間件の早期応答を含む全 HTTP 応答に上限付き X-Request-Id を付けます。通常の業務ログでは既存 createContext の省略可能な scopeId を信頼できるホストから設定します。クライアントのヘッダーからは取得しません。HTTP 境界で古い認証情報を消去し、認証後の非同期処理に現在の ID とユーザーを伝播します。
|
|
733
|
-
|
|
734
|
-
```ts
|
|
735
|
-
import { apiContract } from '@h-ai/api-contract'
|
|
736
|
-
import { feedback } from '@h-ai/feedback'
|
|
737
|
-
import { iam } from '@h-ai/iam'
|
|
738
|
-
import { serv } from '@h-ai/serv'
|
|
739
|
-
import { createFeedbackProcedures } from '@h-ai/serv/features/feedback'
|
|
740
|
-
|
|
741
|
-
const contract = apiContract.create({ feedback: apiContract.feedback })
|
|
742
|
-
const authenticate = serv.buildAuthContextFactory(token => iam.auth.verifyToken(token))
|
|
743
|
-
const procedures = {
|
|
744
|
-
feedback: createFeedbackProcedures({
|
|
745
|
-
feedback,
|
|
746
|
-
resolveContext: context => context.scopeId
|
|
747
|
-
? { scopeId: context.scopeId, userId: context.session.userId, permissions: context.session.permissions }
|
|
748
|
-
: null,
|
|
749
|
-
}),
|
|
750
|
-
}
|
|
751
|
-
const app = serv.createApp({
|
|
752
|
-
contract,
|
|
753
|
-
procedures,
|
|
754
|
-
createContext: async input => ({
|
|
755
|
-
...await authenticate(input),
|
|
756
|
-
scopeId: 'configured-product'
|
|
757
|
-
}),
|
|
758
|
-
})
|
|
759
|
-
```
|
|
760
|
-
|
|
761
|
-
## エラー表示とログの関連付け
|
|
804
|
+
### エラー表示とログの関連付け
|
|
762
805
|
|
|
763
806
|
ネイティブ Audio WebSocket も安全なエラー境界を使います。Provider/SDK の内部情報やスタックはマスク済みログだけに残し、クライアントには安定コード、安全な表示、traceId を返します。独自 CORS 設定でも機密操作の X-Hai-Reauthentication を許可してください。Serv/Kit の既定設定には含まれます。
|
|
764
807
|
|
|
@@ -768,20 +811,6 @@ HTTP/UI 境界には core.error.toPublic(error, traceId?)、再翻訳後の表
|
|
|
768
811
|
|
|
769
812
|
audit の初期化後、procedure は検証済み主体・信頼できる接続元・要求/範囲 ID を関連付け、業務 audit.log に継承します。書込と失敗/拒否は request.execute、入力検証・プロトコル・ミドルウェア・ルート未検出の HTTP エラーは http.reject を記録します。操作・メソッド・状態・理由コードのみを記録し、要求本文・動的 URL・トークン・内部エラーは記録しません。capture.enabled で自動記録を無効化、capture.includeReads で成功読取を有効化できます。明示的業務 log は維持し、業務の完了/失敗はアプリが記録します。
|
|
770
813
|
|
|
771
|
-
## 任意の対話チャレンジ
|
|
772
|
-
|
|
773
|
-
標準 IAM procedure は operation を受けて公開 ALTCHA チャレンジを返す POST /auth/captcha を含みます。有効時はパスワード/OTP/LDAP、登録、通知要求、匿名の旧パスワード更新で IAM が単用証明を検証し、captcha を保持してクライアント判断に依存しません。
|
|
774
|
-
|
|
775
|
-
方針・既定値・復旧範囲は次を参照: [IAM](../iam/README.ja.md)。
|
|
776
|
-
|
|
777
|
-
## ページ離脱時のセッション方針
|
|
778
|
-
|
|
779
|
-
`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` を併用してください。閉じた瞬間の失効は保証できません。
|
|
780
|
-
|
|
781
|
-
## 転送容量とモジュール予算
|
|
782
|
-
|
|
783
|
-
設定 transport.maxPlaintextBytes は 1 KiB~64 MiB の明示整数または auto を受け付けます。auto/省略は undefined に正規化され、要件なしの既定は 1 MiB、フィードバックのホストでは crypto.resolveTransportBodyLimits の requiredPlaintextBytes で統合します。明示上限の不足は起動/梱包時の失敗とし、上書きしません。プロキシ・adapter-node・ハンドシェイク・実行時の予算を一致させ、添付・展開・行数の制限は独立して検証します。
|
|
784
|
-
|
|
785
814
|
## 開発とテスト
|
|
786
815
|
|
|
787
816
|
```bash
|
package/README.md
CHANGED
|
@@ -4,33 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
[框架首页](../../README.md) · 相关模块:[@h-ai/ai](../ai/README.md) · [@h-ai/api-contract](../api-contract/README.md) · [@h-ai/core](../core/README.md) · [@h-ai/crypto](../crypto/README.md) · [@h-ai/iam](../iam/README.md) · [@h-ai/storage](../storage/README.md)
|
|
6
6
|
|
|
7
|
-
## 简介
|
|
8
|
-
|
|
9
7
|
hai-framework 的 API 服务运行时:对外暴露稳定的 HTTP App 抽象,把 `@h-ai/api-contract` 契约挂载为跨端可访问的 HTTP API;Hono 仅作为内部运行时实现。
|
|
10
8
|
|
|
11
9
|
运行时统一通过 `serv.xx` 使用。需要标注或转导出整个命名空间时,从根入口导入 `ServFunctions`;上下文工厂与泛型 `implement` 的公开签名可直接用于消费者声明生成,无需引用内部构建文件或独立 helper。
|
|
12
10
|
|
|
13
|
-
##
|
|
14
|
-
|
|
15
|
-
- 一行启动:`serv.createApp(...)` → `serv.listen(app, { port })`,应用代码不需要 import Hono
|
|
16
|
-
- 扁平 API(最小知识):`serv.createApp / serv.implement / serv.listen / serv.toFetch / serv.generateSpec / ...`
|
|
17
|
-
- 链式 procedure:`implement().context().route().auth/permission/role().handle().build()`
|
|
18
|
-
- 自定义 pipeline:`createApp({ middlewares })` 挂 HTTP middleware;请求级字段通过 context 工厂注入
|
|
19
|
-
- 只读 Storage 资源:`serv.storageAssets(...)` 统一处理安全 key、MIME 白名单、GET/HEAD、ETag 与缓存头
|
|
20
|
-
- 默认 feature procedures:`createIamProcedures()`、`createStorageProcedures()`、`createAiProcedures()`
|
|
21
|
-
- `build()` 同步返回普通 router,可按嵌套属性选取默认 feature procedures;挂载的 contract 与所选 procedures 保持一致,未选入口不开放。
|
|
22
|
-
- 缺失路由返回本地化的 HaiResult JSON 404,启用传输加密后保留 404,不误报为非 JSON 响应导致的 500。
|
|
23
|
-
- 内置安全响应头、健康检查、可选 OpenAPI JSON、可选 Scalar 文档页
|
|
24
|
-
- 可选传输加密:`serv.createApp({ transport: { crypto, serverKeyRing, keyStore } })` 自动挂载密钥协商与请求/响应加解密
|
|
25
|
-
- 可选语音入口:`serv.createApp({ audio: { ai, verifyTicket, authorize } })` 挂载统一语音 WebSocket;连接建立即用一次性 ticket 鉴权(返回 `{ session, grant? }`,独立预鉴权超时),每帧运行时 Zod 校验 + 严格状态机,输入/积压/发送缓冲背压与级联取消;授权回调负责服务端确认操作、模型、音色、配额与并发占用
|
|
26
|
-
|
|
27
|
-
> 生命周期说明:`@h-ai/serv` 本身是无状态 HTTP App 装配器,不需要 `init()` / `close()`;请在创建 app 前初始化 `iam` / `storage` / `ai` / `crypto` 等依赖,并在 `serv.listen(..., { onClose })` 中反向释放它们。`serv.createApp()` 遇到启动期配置错误(例如 transport manager 创建失败)会 fail-fast 抛出,便于进程启动阶段暴露问题。 onClose 是 SIGINT/SIGTERM 的关闭回调;主动调用返回句柄的 close 关闭 HTTP 监听与语音连接,业务模块由宿主另行反序释放。
|
|
28
|
-
|
|
29
|
-
监听句柄可在启动后立即关闭,以取消尚未完成的绑定;重复关闭复用同一结果。地址占用等真实监听失败仍会拒绝关闭结果,不会当作正常退出。
|
|
30
|
-
|
|
31
|
-
## 使用入门
|
|
11
|
+
## 快速开始
|
|
32
12
|
|
|
33
|
-
|
|
13
|
+
下面业务装配片段使用应用已初始化的 `iam`、`storage`、`ai`、`core` 与 logger,并由应用提供 `closeApp`;不是独立初始化脚本。只需要健康检查时可用空 contract 和空 procedures 创建 App。`serv` 是工厂,无 init/close,退出时关闭 listen 返回的句柄并反序释放业务模块。
|
|
34
14
|
|
|
35
15
|
```ts
|
|
36
16
|
import { apiContract } from '@h-ai/api-contract'
|
|
@@ -85,7 +65,47 @@ AI 默认 procedures 从可信 `context.session.userId` 绑定 `objectId`,客
|
|
|
85
65
|
|
|
86
66
|
`chats.createCompletion`(`POST /ai/chats/completions`)只返回一次非流式 `HaiResult<AiChatCompletionData>`。完整 function `tools`(含嵌套 JSON Schema、`strict`)和 `tool_choice`(`auto` / `none` / `required` / 指定名称)传递到模型;消息保留角色、`name`、用户图片和工具调用。工具结果必须使用 `role: 'tool'` 与非空 `tool_call_id`,指定工具选择须引用本次提供的工具。默认 procedure 不自动执行工具,应用授权并执行后,将助手的 `tool_calls` 与对应工具结果一并回传。输入不提供 `stream` 字段,传入 `true` 或 `false` 均在 HTTP 边界返回 400;需要流式时由应用使用 `ai.llm.chatStream()` 定义匹配流输出的独立 endpoint。
|
|
87
67
|
|
|
88
|
-
|
|
68
|
+
## 功能
|
|
69
|
+
|
|
70
|
+
### 用途、输入与输出
|
|
71
|
+
|
|
72
|
+
下表的调用片段复用本页已初始化的服务;命名变量代表应用已提供的请求、Schema、资源定义或配置。完整步骤和失败处理见对应功能示例。`HaiResult` 先判断 `success`,异步项按签名 `await`。
|
|
73
|
+
|
|
74
|
+
| 入口 | 场景 | 输入 | 输出 | 调用片段 |
|
|
75
|
+
| ---------------------------------- | ---------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
76
|
+
| `serv.createApp` | 组装接口应用 | `contract, procedures, http?, iam?, middlewares?, transport?` | `ServHttpApp (fetch/request)` | `serv.createApp({ contract, procedures })` |
|
|
77
|
+
| `serv.listen` | 启动 Node 监听 | `app, { host?, port?, onClose?, onListening? }` | `ServNodeServer { server, address, close(): Promise<void> }` | `serv.listen(app, { host: "127.0.0.1", port: 3000 })` |
|
|
78
|
+
| `serv.toFetch` | 接入 Fetch 宿主 | `ServHttpApp` | `(Request) => Promise<Response>` | `serv.toFetch(app)` |
|
|
79
|
+
| `serv.implement` | 实现领域路由 | `typed contract` | `ServImplementer; context/route/auth/permission/role/handle/build` | `serv.implement(contract)` |
|
|
80
|
+
| `serv.parseRequestContext` | 解析追踪与令牌 | `{ request, platform? }` | `ServContext with requestId/locale/accessToken; no verified session` | `serv.parseRequestContext({ request })` |
|
|
81
|
+
| `serv.buildAuthContextFactory` | 构造可信会话 | `(accessToken) => Promise<HaiResult<ServSession>>` | `CreateServContext; invalid credentials leave session absent` | `serv.buildAuthContextFactory(token => iam.auth.verifyToken(token))` |
|
|
82
|
+
| `serv.cors` | 限定跨域入口 | `{ origin?, methods?, allowedHeaders?, exposedHeaders?, credentials? }` | `ServMiddleware` | `serv.cors({ origin: "https://app.example.com", credentials: true })` |
|
|
83
|
+
| `serv.createRuntimeSecurityPolicy` | 收敛环境安全策略 | `{ environment, corsOrigin, nativeOrigins? }` | `allowOrigin, secureRefreshCookie, exposeApiDocs` | `serv.createRuntimeSecurityPolicy({ environment: "production", corsOrigin: "https://app.example.com" })` |
|
|
84
|
+
| `serv.storageAssets` | 授权的只读资源 | `ServStorageAssetsConfig with reader/key/MIME policy` | `GET/HEAD middleware with ETag` | `serv.storageAssets(assetOptions)` |
|
|
85
|
+
| `serv.generateSpec` | 生成 OpenAPI | `contract, { title?, version?, description?, apiPrefix? }` | `Promise<OpenAPI.Document>` | `serv.generateSpec(contract, { title: "Orders", version: "1.0.0" })` |
|
|
86
|
+
| `serv.validateInputOrFail` | 校验业务输入 | `Zod schema, input, locale?` | `HaiResult<z.infer<Schema>>; failure contains safe field errors` | `serv.validateInputOrFail(InputSchema, input, context.locale)` |
|
|
87
|
+
| `serv.resolveRequestLocale` | 解析请求语言 | `Headers` | `normalized supported locale` | `serv.resolveRequestLocale(request.headers)` |
|
|
88
|
+
| `serv.m` | 翻译模块提示 | `message key, { locale?, ...messageOptions }` | `string` | `serv.m("serv_errorForbidden", { locale: "zh-CN" })` |
|
|
89
|
+
| `serv.WILDCARD_PERMISSION` | 显式全权限常量 | `none` | `string constant; only use in trusted permission policy` | `serv.WILDCARD_PERMISSION` |
|
|
90
|
+
| `serv.WILDCARD_ROLE` | 显式全角色常量 | `none` | `string constant; does not authorize arbitrary requests` | `serv.WILDCARD_ROLE` |
|
|
91
|
+
|
|
92
|
+
- 一行启动:`serv.createApp(...)` → `serv.listen(app, { port })`,应用代码不需要 import Hono
|
|
93
|
+
- 扁平 API(最小知识):`serv.createApp / serv.implement / serv.listen / serv.toFetch / serv.generateSpec / ...`
|
|
94
|
+
- 链式 procedure:`implement().context().route().auth/permission/role().handle().build()`
|
|
95
|
+
- 自定义 pipeline:`createApp({ middlewares })` 挂 HTTP middleware;请求级字段通过 context 工厂注入
|
|
96
|
+
- 只读 Storage 资源:`serv.storageAssets(...)` 统一处理安全 key、MIME 白名单、GET/HEAD、ETag 与缓存头
|
|
97
|
+
- 默认 feature procedures:`createIamProcedures()`、`createStorageProcedures()`、`createAiProcedures()`
|
|
98
|
+
- `build()` 同步返回普通 router,可按嵌套属性选取默认 feature procedures;挂载的 contract 与所选 procedures 保持一致,未选入口不开放。
|
|
99
|
+
- 缺失路由返回本地化的 HaiResult JSON 404,启用传输加密后保留 404,不误报为非 JSON 响应导致的 500。
|
|
100
|
+
- 内置安全响应头、健康检查、可选 OpenAPI JSON、可选 Scalar 文档页
|
|
101
|
+
- 可选传输加密:`serv.createApp({ transport: { crypto, serverKeyRing, keyStore } })` 自动挂载密钥协商与请求/响应加解密
|
|
102
|
+
- 可选语音入口:`serv.createApp({ audio: { ai, verifyTicket, authorize } })` 挂载统一语音 WebSocket;连接建立即用一次性 ticket 鉴权(返回 `{ session, grant? }`,独立预鉴权超时),每帧运行时 Zod 校验 + 严格状态机,输入/积压/发送缓冲背压与级联取消;授权回调负责服务端确认操作、模型、音色、配额与并发占用
|
|
103
|
+
|
|
104
|
+
> 生命周期说明:`@h-ai/serv` 本身是无状态 HTTP App 装配器,不需要 `init()` / `close()`;请在创建 app 前初始化 `iam` / `storage` / `ai` / `crypto` 等依赖,并在 `serv.listen(..., { onClose })` 中反向释放它们。`serv.createApp()` 遇到启动期配置错误(例如 transport manager 创建失败)会 fail-fast 抛出,便于进程启动阶段暴露问题。 onClose 是 SIGINT/SIGTERM 的关闭回调;主动调用返回句柄的 close 关闭 HTTP 监听与语音连接,业务模块由宿主另行反序释放。
|
|
105
|
+
|
|
106
|
+
监听句柄可在启动后立即关闭,以取消尚未完成的绑定;重复关闭复用同一结果。地址占用等真实监听失败仍会拒绝关闭结果,不会当作正常退出。
|
|
107
|
+
|
|
108
|
+
### host / port 配置
|
|
89
109
|
|
|
90
110
|
host/port 统一由 `config/_serv.yml` 的 `server` 提供,优先级:`HOST` / `PORT` 环境变量 > 配置文件 > 默认 `127.0.0.1:3000`。默认 `127.0.0.1` 仅本机可达,容器对外暴露时改为 `0.0.0.0`:
|
|
91
111
|
|
|
@@ -98,7 +118,7 @@ server:
|
|
|
98
118
|
|
|
99
119
|
应用把解析后的 `server` 传给 `serv.listen`(`serv.listen` 也可直接传 `host` / `port` 显式覆盖)。
|
|
100
120
|
|
|
101
|
-
|
|
121
|
+
### 标准 fetch 签名适配
|
|
102
122
|
|
|
103
123
|
toFetch 提供标准 fetch 调用签名,适合 Node 兼容宿主与测试注入;包根仍依赖 Node API,不能据此承诺无需 Node 能力的 Workers/Deno/边缘部署。
|
|
104
124
|
|
|
@@ -111,7 +131,7 @@ Node `serv.listen()` 自动提供 socket 对端地址;默认忽略 `X-Forwarde
|
|
|
111
131
|
|
|
112
132
|
toFetch 对 Request、URL、字符串统一调用标准 Request(input, init),RequestInit 可覆盖 method、headers、body、signal 等字段;不修改原 Request 的字段,正文已消费/锁定或非法正文组合会以 Promise 拒绝。预取消和派发前取消不进入 handler,进行中取消结束调用方等待并把有效 signal 传给应用;监听器在完成/失败/取消后释放,取消后的迟到响应会取消未消费正文。取消不能回滚已经开始的外部副作用;Response 交付后的流由应用监听 request.signal 并自行关闭。
|
|
113
133
|
|
|
114
|
-
|
|
134
|
+
### Audio WebSocket:ticket 与服务端授权
|
|
115
135
|
|
|
116
136
|
语音入口必须使用短期、一次性 ticket。应用通过已登录 HTTP endpoint 签发 ticket(推荐 `iam.ticket.issue`),并在 `verifyTicket` 中原子消费(推荐 `iam.ticket.consume`);连接建立即完成鉴权(独立预鉴权超时),未鉴权不处理任何业务帧。框架保存返回的 `ServSession` 与可选 `grant`,再调用 `authorize` 确认付费参数:
|
|
117
137
|
|
|
@@ -137,7 +157,7 @@ const app = serv.createApp({
|
|
|
137
157
|
- `onSessionEnd` 在成功、失败或断连后至多调用一次,供应用释放并发占用。
|
|
138
158
|
- 浏览器 `createAudioClient({ url, getTicket })` 只把一次性 `ticket` 放入 WebSocket URL;`getTicket(request)` 接收本次操作摘要(`operation` / `model`),可签发与本次操作严格绑定的票据;禁止传普通 IAM access token。
|
|
139
159
|
|
|
140
|
-
|
|
160
|
+
### 传输加密(与 `@h-ai/crypto` 统一)
|
|
141
161
|
|
|
142
162
|
serv 不暴露本地传输加密工厂;统一从 `crypto.transport` 创建服务端管理器。服务端显式注入顶层 `crypto`、受管密钥环和共享 registry:
|
|
143
163
|
|
|
@@ -230,9 +250,7 @@ const app = serv.createApp({
|
|
|
230
250
|
|
|
231
251
|
> **安全策略**:启用 `transport` 后,除 `excludePaths` 与密钥协商端点外,业务请求必须携带有效 `X-Client-Id`。缺失或未知 clientId 会返回 400;响应体无法加密、非 JSON 业务响应或超过单次加密上限时返回错误,不会明文透传业务数据。
|
|
232
252
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
空 POST 也发送空明文的认证封装;GET/HEAD 将封装放在 X-Hai-Transport 头,204/304/HEAD 等无正文响应也使用同名头承载认证封装。其它请求/响应在 JSON 正文承载封装并带 X-Encrypted: true。客户端必须关联原请求上下文和 HTTP status,缺标记、旧格式或认证失败一律 reject;明文接口由 Serv/Kit 路径规则 `encryption: disabled` 或宿主 `excludePaths` 显式允许;调用方同步选择普通请求,API Client/Kit 包装器使用自己的 `excludePaths`。CORS 须允许并暴露 X-Hai-Transport。握手公钥的真实性依赖 TLS,不替代 IAM 业务授权。
|
|
253
|
+
传输使用认证 v2 封装,先验签和 MAC 再解密,并绑定方法、路径、时间与响应状态。共享 registry 原子消费 nonce;TLS、可信代理、业务授权及幂等仍由宿主负责。自定义跨域入口须允许并暴露 `X-Hai-Transport`。完整字段、无正文响应、时钟窗口和防重放要求统一见 [Transport 协议](../crypto/TRANSPORT-PROTOCOL.md)。
|
|
236
254
|
|
|
237
255
|
业务容量通过 `transport.maxPlaintextBytes` 统一配置:默认 1 MiB,整数范围 1 KiB~64 MiB,请求和响应共用 UTF-8 明文字节预算。客户端从握手自动获取上限,密文与部署代理预算由 `resolveTransportBodyLimits()` 推导(两倍明文 + 32 KiB)。握手、公钥、认证头和防重放约束独立;超限取消原流并释放 reader,请求 413、响应为加密的 500 容量错误,客户端识别 `TransportBodyTooLargeError` / `hai:crypto:070`。 [Transport capacity and upgrade](../crypto/TRANSPORT-CAPACITY.md)。
|
|
238
256
|
|
|
@@ -240,7 +258,7 @@ const app = serv.createApp({
|
|
|
240
258
|
|
|
241
259
|
Serv/Kit 对通过请求绑定、签名和 MAC 校验但超出时间窗口的请求返回加密的 HTTP 400 错误。客户端抛出 `TransportTimeError`(`hai:crypto:071`),显示校验时的服务器 UTC 时间,提示开启设备的自动校时或手工修改时间后刷新。`serverTime` 为 Unix 毫秒,旧服务端未提供该值时仍显示校时提示。`api-client` 映射为 `VALIDATION_FAILED`,在 `cause.transportCode` 和 `cause.serverTime` 保留诊断信息。
|
|
242
260
|
|
|
243
|
-
|
|
261
|
+
### Token 认证(`iam` / `refreshCookie`)
|
|
244
262
|
|
|
245
263
|
`serv.createApp` 推荐直接传入顶层 `iam` 句柄,自动派生两件事:
|
|
246
264
|
|
|
@@ -284,7 +302,7 @@ const app = serv.createApp({
|
|
|
284
302
|
|
|
285
303
|
标准登出入口 `auth.logout` 从 input.accessToken 或 Authorization Bearer 取得要撤销的完整令牌,不要求 access 仍有效。它不接受 userId 授权或撤销目标;缺少令牌返回失败,未知令牌幂等成功。共享存储故障原样保留失败 HaiResult,Cookie middleware 只对成功结果清理 refresh Cookie。普通 procedure 继续使用既有 HTTP 200 / HaiResult 协议,客户端必须检查 success,不能只检查状态码。
|
|
286
304
|
|
|
287
|
-
|
|
305
|
+
### 自定义 procedure(认证 + 权限)
|
|
288
306
|
|
|
289
307
|
```ts
|
|
290
308
|
const procedures = serv
|
|
@@ -298,7 +316,7 @@ const procedures = serv
|
|
|
298
316
|
.build()
|
|
299
317
|
```
|
|
300
318
|
|
|
301
|
-
|
|
319
|
+
### 高级:自定义 pipeline(HTTP middleware + context + route guard)
|
|
302
320
|
|
|
303
321
|
`@h-ai/serv` 的“pipeline”分三层,分别对应三种扩展方式:
|
|
304
322
|
|
|
@@ -306,7 +324,7 @@ const procedures = serv
|
|
|
306
324
|
2. **context 层**:通过 `verifyToken` / `createContext` / `serv.buildAuthContextFactory()` 定制请求上下文。
|
|
307
325
|
3. **route guard 层**:通过 `.auth()` / `.permission()` / `.role()` 声明认证授权。
|
|
308
326
|
|
|
309
|
-
|
|
327
|
+
#### 1) 自定义 HTTP middleware
|
|
310
328
|
|
|
311
329
|
当你需要加请求日志、trace、限流、CORS、租户头校验等 **HTTP 层横切逻辑**,或挂载不适合进入 JSON/oRPC contract 的 WebSocket、文件和二进制端点时,使用 `middlewares`。常见 CORS 场景可直接复用 `serv.cors(...)`:
|
|
312
330
|
|
|
@@ -375,7 +393,7 @@ const app = serv.createApp({
|
|
|
375
393
|
- 浏览器若需读取自定义响应头(例如 transport 的 `X-Encrypted`),请通过 `serv.cors({ exposedHeaders: [...] })` 显式暴露
|
|
376
394
|
- 这一层直接操作 HTTP middleware context,返回的是 **HTTP Response**,不是 `HaiResult`
|
|
377
395
|
|
|
378
|
-
|
|
396
|
+
#### 2) 自定义 context pipeline
|
|
379
397
|
|
|
380
398
|
如果你只想复用 `Bearer token -> session` 这一段逻辑,再额外补充租户、工作区、来源系统等字段,
|
|
381
399
|
优先用 `serv.buildAuthContextFactory()` 组装自定义 `createContext`:
|
|
@@ -402,7 +420,7 @@ const app = serv.createApp({
|
|
|
402
420
|
- 想在默认认证上下文之上追加字段:用 `buildAuthContextFactory(...) + createContext`
|
|
403
421
|
- 想完全接管上下文构造:直接用 `createContext`
|
|
404
422
|
|
|
405
|
-
|
|
423
|
+
#### 3) Route guard
|
|
406
424
|
|
|
407
425
|
公开 procedure 使用 `.route(path, handler)`;需要 guard 时使用
|
|
408
426
|
`.route(path).auth/permission/role().handle(handler)`:
|
|
@@ -436,7 +454,7 @@ const procedures = serv
|
|
|
436
454
|
- 对应用层公开的稳定入口仍然是根入口 `@h-ai/serv`
|
|
437
455
|
- `securityHeaders` / `buildHaiErrorBody` 等内部 helper/middleware 不作为单独子路径文档暴露
|
|
438
456
|
|
|
439
|
-
|
|
457
|
+
### httpOnly Cookie 认证(推荐生产方案)
|
|
440
458
|
|
|
441
459
|
httpOnly cookie 模式将 refresh token 存储在服务端管理的 cookie 中(浏览器 JS 无法读取),避免 XSS 风险。
|
|
442
460
|
|
|
@@ -535,7 +553,81 @@ await apiClient.init({
|
|
|
535
553
|
|
|
536
554
|
`http.trustedProxies` 默认 `auto`:普通宿主忽略转发头;受管 Compose 的私有代理拓扑才自动采用容器网段。自管代理指定真实 IP/CIDR,`[]` 显式关闭。代理必须清洗转发头,Node 监听器或 Fetch 平台必须提供可信对端元数据。独立文件、外层代理和部署回滚步骤见[请求访问控制手册](../../docs/usage/configuration/request-access.md)。
|
|
537
555
|
|
|
538
|
-
###
|
|
556
|
+
### 配置驱动认证页面
|
|
557
|
+
|
|
558
|
+
协议支持配置中的 userAgreementMarkdown/privacyPolicyMarkdown 正文及 URL;同主题优先 Markdown,由安全渲染器在弹窗显示。
|
|
559
|
+
|
|
560
|
+
`createIamProcedures({ iam })` 直接实现公开配置、密码/LDAP/OTP、注册与密码重置,无需应用复制 procedure。LDAP 登录沿用现有浏览器 httpOnly refresh Cookie 和可信原生 Token 传输。管理端创建调用 iam.user.createUser,自助注册关闭时管理功能仍可用;授权仍由既有 permission guard 执行。
|
|
561
|
+
|
|
562
|
+
[完整 IAM 配置 / IAM configuration](../iam/README.md)
|
|
563
|
+
|
|
564
|
+
### 请求语言隔离
|
|
565
|
+
|
|
566
|
+
Node 请求与 SSR 使用 `core.i18n.runWithLocale(locale, () => handleRequest())`;SvelteKit 可使用 `kit.i18n.runWithLocale(locale, () => resolve(event))`,serv 自动按请求头建立上下文。异步子任务继承请求语言,优先级为显式 `MessageOptions.locale`、请求语言、进程启动默认语言。`setGlobalLocale` 在请求上下文中只更新当前请求;上下文外仅用于启动默认值,不能在请求 hook 中调用它修改默认值。
|
|
567
|
+
|
|
568
|
+
浏览器继续使用应用级 `setGlobalLocale` / `createLocaleStore`。SSR 的 UI store 初始化和 setter 只修改自身状态,不写全局语言;UI 消息从请求上下文读取。`runWithLocale` 仅支持 Node 入口,浏览器通过显式 locale 或应用状态翻译。
|
|
569
|
+
|
|
570
|
+
### 输入约束与 HTTP 能力
|
|
571
|
+
|
|
572
|
+
存储 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#存储输入约束与跨端能力)。
|
|
573
|
+
|
|
574
|
+
### 可选的反馈服务与可信范围
|
|
575
|
+
|
|
576
|
+
通过 `@h-ai/serv/features/feedback` 的 `createFeedbackProcedures` 挂载 `apiContract.feedback`;只导入运行时根入口不会加载反馈服务。宿主先初始化 reldb/storage/reach/feedback,再从已验证的会话与部署策略映射反馈范围。resolveContext 返回 null 或用户不匹配时拒绝访问;领域模块再次校验范围与权限。
|
|
577
|
+
|
|
578
|
+
所有 HTTP 响应(包括探活、404、中间件短路)携带有界 X-Request-Id。普通业务日志需由已有 createContext 的可选 scopeId 建立可信范围,scopeId 从不读取客户端头。HTTP 入口清理旧身份,认证后的异步业务日志自动继承当前请求 ID 与用户。
|
|
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
|
+
### 可选交互挑战
|
|
608
|
+
|
|
609
|
+
标准 IAM procedure 包含 POST /auth/captcha,输入 operation,返回可公开的 ALTCHA 挑战。开启 IAM captcha 时密码/OTP/LDAP、注册、通知请求及匿名原密码续期均由 IAM 强制检查单用证明;请求映射保留 captcha,不依赖菜单或客户端判断。
|
|
610
|
+
|
|
611
|
+
策略、默认值和恢复边界见 [IAM](../iam/README.md)。
|
|
612
|
+
|
|
613
|
+
### 页面离开会话策略
|
|
614
|
+
|
|
615
|
+
`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`;不能据此保证关闭即撤销。
|
|
616
|
+
|
|
617
|
+
## 配置
|
|
618
|
+
|
|
619
|
+
### 按场景选择配置入口
|
|
620
|
+
|
|
621
|
+
| 配置入口 | 场景 | 输入 | 生效结果 | 示例 |
|
|
622
|
+
| ------------------------------------------------------ | ---------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------- | -------------------------------------------------------------- |
|
|
623
|
+
| `server` | Node 监听地址 | 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` | 业务路径前缀 | absolute validated prefix, default /api/v1 | OpenAPI/REST mounting prefix | `http: { apiPrefix: /api/v1 }` |
|
|
625
|
+
| `http.health` | 存活与就绪检查 | path, readyPath | health/readiness routes | `health: { path: /health, readyPath: /ready }` |
|
|
626
|
+
| `http.openapi / http.docs` | 接口说明 | false or explicit path/authorization settings | spec/Scalar routes; production exposure controlled by host policy | `openapi: false` |
|
|
627
|
+
| `http.trustedProxies / http.access` | 入口可信边界 | proxy list/auto and access policy | trusted address and per-request entry policy | `trustedProxies: []` |
|
|
628
|
+
| `cors` | 跨端来源与请求头 | origin/nativeOrigins/credentials/allowedHeaders/exposedHeaders | serv.cors middleware input; explicit production allowlist | `cors: { origin: https://app.example.com, credentials: true }` |
|
|
629
|
+
| `transport` | 双向认证加密 | false or public paths/limits; serverKeyRing/keyStore injected separately | handshake and authenticated request/response handling | `transport: { maxPlaintextBytes: auto }` |
|
|
630
|
+
| `createApp.iam / refreshCookie / nativeTokenTransport` | 宿主认证装配 | initialized IAM, secure Cookie policy and native transport rule | trusted session/token channel; frontend receives only allowed fields | `serv.createApp({ contract, procedures, iam })` |
|
|
539
631
|
|
|
540
632
|
`@h-ai/serv` 本身不主动扫描 YAML;推荐由 `@h-ai/core` 统一加载 `config/_serv.yml`,再把解析结果传给 `serv.createApp()`。
|
|
541
633
|
|
|
@@ -611,8 +703,6 @@ transport:
|
|
|
611
703
|
|
|
612
704
|
`ServHttpConfigInput`:
|
|
613
705
|
|
|
614
|
-
原生 RPC 入口及 `HAI_SERV_HTTP_RPC`、`ServRpcHttpConfig` 已删除。业务继续使用现有 OpenAPI/REST 路径和 `api-client`,底层 oRPC contract 与 procedure 保留。
|
|
615
|
-
|
|
616
706
|
- `apiPrefix`:默认 `/api/v1`;支持 `/somepath/api/v1` 等完整绝对前缀。路径段只含字母、数字、下划线、连字符和点,不接受末尾斜杠、空段、单独的 `.`/`..`、查询或片段。健康、文档和存储公开路径分别配置到同一目录。
|
|
617
707
|
- `health`:默认 `{ path: '/health', readyPath: '/ready' }`
|
|
618
708
|
- `openapi`:默认 `false`;启用:`{ path: '/openapi.json' }`
|
|
@@ -656,15 +746,11 @@ const app = serv.createApp({
|
|
|
656
746
|
|
|
657
747
|
受控内网或 VPN 的生产 HTTP 入口可以在后端进程环境设置 `HAI_SERV_REFRESH_COOKIE_SECURE=false`。重新启动后端并登录后,登录和刷新响应会下发省略 `Secure` 的 Cookie,刷新凭据会经 HTTP 明文传输。设置 `true` 可强制启用 `Secure`,留空或其他值沿用环境默认值。直接调用 `serv.createApp` 时,显式 `refreshCookie.secure` 优先,其次是此变量,再按 `NODE_ENV` 或 `HAI_CORE_ENV` 判断是否为生产。
|
|
658
748
|
|
|
659
|
-
###
|
|
749
|
+
### 传输容量与模块预算
|
|
660
750
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
`createIamProcedures({ iam })` 直接实现公开配置、密码/LDAP/OTP、注册与密码重置,无需应用复制 procedure。LDAP 登录沿用现有浏览器 httpOnly refresh Cookie 和可信原生 Token 传输。管理端创建调用 iam.user.createUser,自助注册关闭时管理功能仍可用;授权仍由既有 permission guard 执行。
|
|
664
|
-
|
|
665
|
-
[完整 IAM 配置 / IAM configuration](../iam/README.md)
|
|
751
|
+
配置 `transport.maxPlaintextBytes` 支持 1 KiB~64 MiB 的显式整数或 `auto`。auto/省略在解析后为 undefined:无模块需求时仍为 1 MiB,反馈宿主使用 crypto.resolveTransportBodyLimits 的 requiredPlaintextBytes 统一推导。显式上限不足时启动/打包失败,不能被反馈覆盖;代理、adapter-node、握手与运行时使用一致预算。业务附件、解压和数据行数上限继续独立校验。
|
|
666
752
|
|
|
667
|
-
|
|
753
|
+
## 错误处理
|
|
668
754
|
|
|
669
755
|
普通 procedure 的认证、授权、业务失败和被捕获的 handler 异常都作为 `HaiResult` 输出,默认通过 HTTP 200 返回;`error.httpStatus` 是领域错误元数据,不会自动改写响应状态。客户端必须检查 `success` 与 `error.code`。例如未登录为 `error.httpStatus=401`,数据库查询失败或 INTERNAL_ERROR 为 500。
|
|
670
756
|
|
|
@@ -715,50 +801,7 @@ const procedures = serv
|
|
|
715
801
|
|
|
716
802
|
> 下游 `HaiResult` 的 4xx 消息经脱敏后公开;5xx 和缺失或非法状态统一为安全内部错误,保留 traceId,完整原因只记录到服务端日志。`HaiError` 不携带 `messageKey/params`,领域消息的请求级 i18n 仍由创建消息的模块接收 locale 后完成。
|
|
717
803
|
|
|
718
|
-
###
|
|
719
|
-
|
|
720
|
-
Node 请求与 SSR 使用 `core.i18n.runWithLocale(locale, () => handleRequest())`;SvelteKit 可使用 `kit.i18n.runWithLocale(locale, () => resolve(event))`,serv 自动按请求头建立上下文。异步子任务继承请求语言,优先级为显式 `MessageOptions.locale`、请求语言、进程启动默认语言。`setGlobalLocale` 在请求上下文中只更新当前请求;上下文外仅用于启动默认值,不能在请求 hook 中调用它修改默认值。
|
|
721
|
-
|
|
722
|
-
浏览器继续使用应用级 `setGlobalLocale` / `createLocaleStore`。SSR 的 UI store 初始化和 setter 只修改自身状态,不写全局语言;UI 消息从请求上下文读取。`runWithLocale` 仅支持 Node 入口,浏览器通过显式 locale 或应用状态翻译。
|
|
723
|
-
|
|
724
|
-
## 输入约束与 HTTP 能力
|
|
725
|
-
|
|
726
|
-
存储 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#存储输入约束与跨端能力)。
|
|
727
|
-
|
|
728
|
-
## 可选的反馈服务与可信范围
|
|
729
|
-
|
|
730
|
-
通过 `@h-ai/serv/features/feedback` 的 `createFeedbackProcedures` 挂载 `apiContract.feedback`;只导入运行时根入口不会加载反馈服务。宿主先初始化 reldb/storage/reach/feedback,再从已验证的会话与部署策略映射反馈范围。resolveContext 返回 null 或用户不匹配时拒绝访问;领域模块再次校验范围与权限。
|
|
731
|
-
|
|
732
|
-
所有 HTTP 响应(包括探活、404、中间件短路)携带有界 X-Request-Id。普通业务日志需由已有 createContext 的可选 scopeId 建立可信范围,scopeId 从不读取客户端头。HTTP 入口清理旧身份,认证后的异步业务日志自动继承当前请求 ID 与用户。
|
|
733
|
-
|
|
734
|
-
```ts
|
|
735
|
-
import { apiContract } from '@h-ai/api-contract'
|
|
736
|
-
import { feedback } from '@h-ai/feedback'
|
|
737
|
-
import { iam } from '@h-ai/iam'
|
|
738
|
-
import { serv } from '@h-ai/serv'
|
|
739
|
-
import { createFeedbackProcedures } from '@h-ai/serv/features/feedback'
|
|
740
|
-
|
|
741
|
-
const contract = apiContract.create({ feedback: apiContract.feedback })
|
|
742
|
-
const authenticate = serv.buildAuthContextFactory(token => iam.auth.verifyToken(token))
|
|
743
|
-
const procedures = {
|
|
744
|
-
feedback: createFeedbackProcedures({
|
|
745
|
-
feedback,
|
|
746
|
-
resolveContext: context => context.scopeId
|
|
747
|
-
? { scopeId: context.scopeId, userId: context.session.userId, permissions: context.session.permissions }
|
|
748
|
-
: null,
|
|
749
|
-
}),
|
|
750
|
-
}
|
|
751
|
-
const app = serv.createApp({
|
|
752
|
-
contract,
|
|
753
|
-
procedures,
|
|
754
|
-
createContext: async input => ({
|
|
755
|
-
...await authenticate(input),
|
|
756
|
-
scopeId: 'configured-product'
|
|
757
|
-
}),
|
|
758
|
-
})
|
|
759
|
-
```
|
|
760
|
-
|
|
761
|
-
## 错误提示与关联日志
|
|
804
|
+
### 错误提示与关联日志
|
|
762
805
|
|
|
763
806
|
原生 Audio WebSocket 同样使用安全错误边界:内部 Provider/SDK 错误与堆栈只进入脱敏日志,客户端收到稳定错误码、安全提示和 traceId。自定义跨域配置需保留敏感操作使用的 X-Hai-Reauthentication;默认 Serv/Kit 已允许。
|
|
764
807
|
|
|
@@ -768,20 +811,6 @@ HTTP 错误返回本地化安全文案及追踪编号;ToastContainer 默认隐
|
|
|
768
811
|
|
|
769
812
|
audit 已初始化时,procedure 自动绑定经过验证的身份、可信连接地址与请求/范围编号,业务 audit.log 继承此上下文。写操作及失败/拒绝记录 request.execute;契约校验、协议、中间件和缺失路由的 HTTP 拒绝记录 http.reject。入口只记录操作、方法、状态和原因码,不复制请求体、动态 URL、令牌或错误详情。capture.enabled 可关闭自动采集,capture.includeReads 可开启成功读取;显式业务 log 不受影响。业务语义仍须应用在完成/失败边界记录。
|
|
770
813
|
|
|
771
|
-
## 可选交互挑战
|
|
772
|
-
|
|
773
|
-
标准 IAM procedure 包含 POST /auth/captcha,输入 operation,返回可公开的 ALTCHA 挑战。开启 IAM captcha 时密码/OTP/LDAP、注册、通知请求及匿名原密码续期均由 IAM 强制检查单用证明;请求映射保留 captcha,不依赖菜单或客户端判断。
|
|
774
|
-
|
|
775
|
-
策略、默认值和恢复边界见 [IAM](../iam/README.md)。
|
|
776
|
-
|
|
777
|
-
## 页面离开会话策略
|
|
778
|
-
|
|
779
|
-
`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`;不能据此保证关闭即撤销。
|
|
780
|
-
|
|
781
|
-
## 传输容量与模块预算
|
|
782
|
-
|
|
783
|
-
配置 `transport.maxPlaintextBytes` 支持 1 KiB~64 MiB 的显式整数或 `auto`。auto/省略在解析后为 undefined:无模块需求时仍为 1 MiB,反馈宿主使用 crypto.resolveTransportBodyLimits 的 requiredPlaintextBytes 统一推导。显式上限不足时启动/打包失败,不能被反馈覆盖;代理、adapter-node、握手与运行时使用一致预算。业务附件、解压和数据行数上限继续独立校验。
|
|
784
|
-
|
|
785
814
|
## 开发测试
|
|
786
815
|
|
|
787
816
|
```bash
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@h-ai/serv",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.0-beta.
|
|
4
|
+
"version": "1.0.0-beta.20",
|
|
5
5
|
"description": "Hai Framework service runtime powered by Hono and oRPC.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"exports": {
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"node": "^22.23.4 || ^24.22.1 || ^26.12.1"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@h-ai/ai": "1.0.0-beta.
|
|
43
|
-
"@h-ai/feedback": "1.0.0-beta.
|
|
44
|
-
"@h-ai/iam": "1.0.0-beta.
|
|
45
|
-
"@h-ai/storage": "1.0.0-beta.
|
|
42
|
+
"@h-ai/ai": "1.0.0-beta.20",
|
|
43
|
+
"@h-ai/feedback": "1.0.0-beta.20",
|
|
44
|
+
"@h-ai/iam": "1.0.0-beta.20",
|
|
45
|
+
"@h-ai/storage": "1.0.0-beta.20"
|
|
46
46
|
},
|
|
47
47
|
"peerDependenciesMeta": {
|
|
48
48
|
"@h-ai/ai": {
|
|
@@ -59,10 +59,10 @@
|
|
|
59
59
|
}
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@h-ai/api-contract": "1.0.0-beta.
|
|
63
|
-
"@h-ai/audit": "1.0.0-beta.
|
|
64
|
-
"@h-ai/core": "1.0.0-beta.
|
|
65
|
-
"@h-ai/crypto": "1.0.0-beta.
|
|
62
|
+
"@h-ai/api-contract": "1.0.0-beta.20",
|
|
63
|
+
"@h-ai/audit": "1.0.0-beta.20",
|
|
64
|
+
"@h-ai/core": "1.0.0-beta.20",
|
|
65
|
+
"@h-ai/crypto": "1.0.0-beta.20",
|
|
66
66
|
"@hono/node-server": "^2.1.6",
|
|
67
67
|
"@orpc/openapi": "^1.16.2",
|
|
68
68
|
"@orpc/server": "^1.16.2",
|
|
@@ -73,13 +73,13 @@
|
|
|
73
73
|
"zod": "^4.6.7"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@h-ai/ai": "1.0.0-beta.
|
|
77
|
-
"@h-ai/api-client": "1.0.0-beta.
|
|
78
|
-
"@h-ai/cache": "1.0.0-beta.
|
|
79
|
-
"@h-ai/feedback": "1.0.0-beta.
|
|
80
|
-
"@h-ai/iam": "1.0.0-beta.
|
|
81
|
-
"@h-ai/reldb": "1.0.0-beta.
|
|
82
|
-
"@h-ai/storage": "1.0.0-beta.
|
|
76
|
+
"@h-ai/ai": "1.0.0-beta.20",
|
|
77
|
+
"@h-ai/api-client": "1.0.0-beta.20",
|
|
78
|
+
"@h-ai/cache": "1.0.0-beta.20",
|
|
79
|
+
"@h-ai/feedback": "1.0.0-beta.20",
|
|
80
|
+
"@h-ai/iam": "1.0.0-beta.20",
|
|
81
|
+
"@h-ai/reldb": "1.0.0-beta.20",
|
|
82
|
+
"@h-ai/storage": "1.0.0-beta.20",
|
|
83
83
|
"@types/node": "^26.6.6",
|
|
84
84
|
"@types/ws": "^8.18.2",
|
|
85
85
|
"altcha": "^3.5.0",
|