@h-ai/serv 0.1.0-alpha.66 → 0.1.0-alpha.68
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 +75 -37
- package/README.ja.md +76 -38
- package/README.md +77 -39
- package/dist/chunk-BGWE4S6Z.js +37 -0
- package/dist/chunk-BGWE4S6Z.js.map +1 -0
- package/dist/{chunk-R354OMWL.js → chunk-TWHCU6HT.js} +48 -6
- package/dist/chunk-TWHCU6HT.js.map +1 -0
- package/dist/{chunk-27PCPE37.js → chunk-XKCLD42I.js} +2 -2
- package/dist/chunk-XKCLD42I.js.map +1 -0
- package/dist/features/ai.d.ts +139 -57
- package/dist/features/ai.js +57 -46
- package/dist/features/ai.js.map +1 -1
- package/dist/features/iam.d.ts +29 -4
- package/dist/features/iam.js +3 -3
- package/dist/features/iam.js.map +1 -1
- package/dist/features/storage.d.ts +94 -9
- package/dist/features/storage.js +48 -12
- package/dist/features/storage.js.map +1 -1
- package/dist/index.d.ts +587 -187
- package/dist/index.js +89 -111
- package/dist/index.js.map +1 -1
- package/dist/{serv-context-CWzyrsdq.d.ts → serv-context-C48WdBcd.d.ts} +127 -7
- package/dist/serv-router-BX8hgYxo.d.ts +322 -0
- package/package.json +14 -10
- package/dist/chunk-27PCPE37.js.map +0 -1
- package/dist/chunk-R354OMWL.js.map +0 -1
package/README.en.md
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
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
|
-
|
|
7
|
+
## Introduction
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
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
|
+
|
|
11
|
+
## Core features
|
|
10
12
|
|
|
11
13
|
- Start with `serv.createApp(...)` → `serv.listen(app, { port })`; applications need not import Hono.
|
|
12
14
|
- Flat API: `serv.createApp / serv.implement / serv.listen / serv.toFetch / serv.generateSpec / ...`.
|
|
@@ -17,12 +19,14 @@
|
|
|
17
19
|
- `build()` synchronously returns a plain router, so nested default feature procedures can be selected directly. Match the mounted contract to the selected procedures; unselected routes stay unavailable.
|
|
18
20
|
- Missing routes return localized HaiResult JSON with HTTP 404. Transport encryption preserves that status instead of producing a 500 for a non-JSON response.
|
|
19
21
|
- Built-in security headers and health checks; optional OpenAPI JSON, Scalar docs, and internal RPC endpoints.
|
|
20
|
-
- Optional transport encryption: `serv.createApp({ transport: { crypto } })` mounts key exchange and request/response encryption automatically.
|
|
22
|
+
- Optional transport encryption: `serv.createApp({ transport: { crypto, serverKeyRing, keyStore } })` mounts key exchange and request/response encryption automatically.
|
|
21
23
|
- Optional audio: `serv.createApp({ audio: { ai, verifyTicket, authorize } })` mounts a unified audio WebSocket. Connections authenticate immediately with one-time tickets (`{ session, grant? }`, separate pre-auth timeout). Every frame uses runtime Zod validation and a strict state machine, with input/queue/send-buffer backpressure and cascading cancellation. Authorization verifies operations, models, voices, quotas, and concurrency on the server.
|
|
22
24
|
|
|
23
|
-
> 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.
|
|
25
|
+
> Lifecycle: `@h-ai/serv` is a stateless HTTP App assembler and needs no `init()` / `close()`. Initialize `iam` / `storage` / `ai` / `crypto` before app creation, then release them in reverse order through `serv.listen(..., { onClose })`. `serv.createApp()` throws immediately on startup configuration errors such as transport manager creation failure. onClose is the SIGINT/SIGTERM shutdown callback. Calling the returned handle.close directly closes HTTP only; the host separately releases business modules in reverse order.
|
|
26
|
+
|
|
27
|
+
## Getting started
|
|
24
28
|
|
|
25
|
-
|
|
29
|
+
### Quick start
|
|
26
30
|
|
|
27
31
|
```ts
|
|
28
32
|
import { apiContract } from '@h-ai/api-contract'
|
|
@@ -37,7 +41,11 @@ const contract = apiContract.create({ iam: apiContract.iam, storage: apiContract
|
|
|
37
41
|
// 2) 组装 procedures(注入 iam / storage / ai 已初始化的 functions)
|
|
38
42
|
const procedures = {
|
|
39
43
|
iam: createIamProcedures({ iam }),
|
|
40
|
-
storage: createStorageProcedures({
|
|
44
|
+
storage: createStorageProcedures({
|
|
45
|
+
storage,
|
|
46
|
+
authorize: ({ context, keys }) => keys.every(key =>
|
|
47
|
+
key.startsWith(`users/${encodeURIComponent(context.session.userId)}/`)),
|
|
48
|
+
}),
|
|
41
49
|
ai: createAiProcedures({ ai }),
|
|
42
50
|
}
|
|
43
51
|
|
|
@@ -64,7 +72,15 @@ serv.listen(app, {
|
|
|
64
72
|
// 5) 优雅关闭由 serv 托管,无需手动注册 process.once('SIGINT', ...)
|
|
65
73
|
```
|
|
66
74
|
|
|
67
|
-
|
|
75
|
+
Storage procedures require `authorize({ context, operation, keys })` and deny access if the policy is missing. Derive resource ownership from the authenticated session for downloads, uploads, listing, metadata and deletion. The example only permits keys under `users/<encoded userId>/`; list requests must include that prefix. A batch is executed only after every key is authorized. Grant administrative access explicitly in the application policy.
|
|
76
|
+
|
|
77
|
+
Batch deletion via `storage.files.deleteMany` returns `{ deletedKeys, failures: [] }` on success. Failures preserve progress in `error.ext.deleteMany` (`failed / unknown / notAttempted`); HTTP 200 alone does not mean completion. See [Storage batch deletion](../storage/README.en.md#api-overview).
|
|
78
|
+
|
|
79
|
+
AI procedures bind `objectId` to the trusted `context.session.userId` when the client omits it. Chat/message writes, history, session lists and memory recall/list all use the authorized owner. Sessions are scoped by `objectId + sessionId`; writes default to session `default`. A different objectId is forbidden unless an explicit `authorizeObject` policy grants access using trusted permissions and tenant ownership, including for administrators.
|
|
80
|
+
|
|
81
|
+
`chats.createCompletion` (`POST /ai/chats/completions`) returns one non-streaming `HaiResult<AiChatCompletionData>`. Function `tools` retain their complete nested JSON Schema and `strict`, and `tool_choice` supports `auto`, `none`, `required`, or a provided function name. Messages preserve roles, `name`, user images, and tool calls. Tool results require `role: 'tool'` and a nonempty `tool_call_id`; named choices must reference a supplied tool. The procedure does not execute tools: the application authorizes and executes them, then sends the assistant's `tool_calls` with their tool results. The input has no `stream` field; either boolean value is rejected with HTTP 400 before the provider runs. For streaming, define a separate endpoint with a matching stream output using `ai.llm.chatStream()`.
|
|
82
|
+
|
|
83
|
+
#### host / port configuration
|
|
68
84
|
|
|
69
85
|
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:
|
|
70
86
|
|
|
@@ -77,7 +93,9 @@ server:
|
|
|
77
93
|
|
|
78
94
|
Pass the resolved `server` to `serv.listen`; explicit `host` / `port` arguments can also override it.
|
|
79
95
|
|
|
80
|
-
|
|
96
|
+
#### Standard fetch signature adapter
|
|
97
|
+
|
|
98
|
+
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.
|
|
81
99
|
|
|
82
100
|
```ts
|
|
83
101
|
const handler = serv.toFetch(app)
|
|
@@ -86,7 +104,9 @@ export default { fetch: handler }
|
|
|
86
104
|
|
|
87
105
|
Node `serv.listen()` supplies the socket peer address. Forwarded headers are ignored by default. Only proxy IPs/CIDRs in `http.trustedProxies` may supply them: XFF is walked from right to left until the first untrusted address; content to the left of that address is ignored. An invalid or excessively long trusted chain leaves IP undefined rather than promoting the proxy to client identity. Proxies must sanitize or append forwarded headers. Fetch platforms explicitly provide a trusted peer through `app.fetch(request, { remoteAddress: platformPeerAddress })`; without metadata IP is undefined. Do not copy arbitrary headers into metadata. Custom `createContext(input)` forwards the complete input to preserve resolved `input.ip`. Internal RPC checks the connection peer.
|
|
88
106
|
|
|
89
|
-
|
|
107
|
+
toFetch constructs Request(input, init) for Request, URL and string inputs, applying RequestInit overrides including method, headers, body and signal without mutating input fields. Consumed or locked bodies and invalid body combinations reject the returned Promise. Cancellation before dispatch skips the handler; in-flight cancellation ends the caller wait and reaches the app through the effective signal. Listeners are released on completion, failure or cancellation, and a discarded late response body is cancelled. Cancellation cannot roll back external effects already started; after Response delivery the app owns streaming cleanup through request.signal.
|
|
108
|
+
|
|
109
|
+
#### Audio WebSocket: tickets and server authorization
|
|
90
110
|
|
|
91
111
|
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:
|
|
92
112
|
|
|
@@ -110,18 +130,22 @@ const app = serv.createApp({
|
|
|
110
130
|
- Each frame is validated with Zod and a strict state machine: transcription accepts audio, synthesis accepts text, `start`/`done` occur once, and `segmentId` is unique per session. Input, pending-message, and send buffers are bounded; limits or cancellation close with domain errors and abort upstream work.
|
|
111
131
|
- Tunables: `preAuthTimeoutMs` (5000), `maxPendingMessages` (256), `maxSendBufferBytes` (8 MiB), and `maxMessageBytes` / `maxBufferedBytes` / `maxTextBytes` / `maxSessionMs`.
|
|
112
132
|
- `onSessionEnd` runs at most once after success, failure, or disconnection to release concurrency slots.
|
|
113
|
-
- Browser `
|
|
133
|
+
- 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.
|
|
114
134
|
|
|
115
|
-
|
|
135
|
+
#### Transport encryption (shared with `@h-ai/crypto`)
|
|
116
136
|
|
|
117
137
|
serv does not expose its own encryption factory; the server manager comes from `crypto.transport`. Applications only pass the top-level `crypto` handle:
|
|
118
138
|
|
|
119
139
|
```ts
|
|
120
140
|
import { cache } from '@h-ai/cache'
|
|
121
|
-
import { createRedisTransportKeyStore, crypto } from '@h-ai/crypto'
|
|
141
|
+
import { createRedisTransportKeyStore, crypto, parseTransportServerKeyRing } from '@h-ai/crypto'
|
|
122
142
|
import { serv } from '@h-ai/serv'
|
|
123
143
|
|
|
124
144
|
await crypto.init()
|
|
145
|
+
const ring = parseTransportServerKeyRing(process.env.HAI_CRYPTO_TRANSPORT_SERVERKEYRING)
|
|
146
|
+
if (!ring.success)
|
|
147
|
+
throw new Error(ring.error.message)
|
|
148
|
+
const serverKeyRing = ring.data
|
|
125
149
|
await cache.init({ type: 'redis', host: '127.0.0.1', port: 6379 })
|
|
126
150
|
|
|
127
151
|
const app = serv.createApp({
|
|
@@ -130,9 +154,9 @@ const app = serv.createApp({
|
|
|
130
154
|
http: { apiPrefix: '/api/v1' },
|
|
131
155
|
transport: {
|
|
132
156
|
crypto,
|
|
157
|
+
serverKeyRing,
|
|
133
158
|
// keyExchangePath 默认 '/_hai/key-exchange',最终端点为 '/api/v1/_hai/key-exchange'
|
|
134
159
|
// excludePaths: ['/health'],
|
|
135
|
-
// maxClients: 10000,
|
|
136
160
|
keyStore: createRedisTransportKeyStore({ cache, ttlSeconds: 3600 }),
|
|
137
161
|
},
|
|
138
162
|
})
|
|
@@ -165,7 +189,6 @@ transport:
|
|
|
165
189
|
- /openapi.json
|
|
166
190
|
- /docs
|
|
167
191
|
- /_hai/scalar.js
|
|
168
|
-
maxClients: 10000
|
|
169
192
|
```
|
|
170
193
|
|
|
171
194
|
```ts
|
|
@@ -187,24 +210,25 @@ const app = serv.createApp({
|
|
|
187
210
|
? undefined
|
|
188
211
|
: {
|
|
189
212
|
crypto,
|
|
213
|
+
serverKeyRing,
|
|
214
|
+
keyStore,
|
|
190
215
|
keyExchangePath: servConfig.transport.keyExchangePath,
|
|
191
216
|
excludePaths: [...servConfig.transport.excludePaths],
|
|
192
|
-
maxClients: servConfig.transport.maxClients,
|
|
193
217
|
},
|
|
194
218
|
})
|
|
195
219
|
```
|
|
196
220
|
|
|
197
|
-
>
|
|
198
|
-
>
|
|
199
|
-
> - Enable **sticky sessions** in the load balancer so each client reaches the same node.
|
|
200
|
-
> - Perform a separate initial key exchange for each client on each node.
|
|
201
|
-
> - Inject a shared `keyStore`, such as `createRedisTransportKeyStore()` / `createReldbTransportKeyStore()`.
|
|
202
|
-
>
|
|
203
|
-
> `keyStore` is a runtime object dependency and must not go in `config/_serv.yml`; configuration contains only static paths, allowlists, and capacities.
|
|
221
|
+
> Servers must explicitly inject the same managed `serverKeyRing` and a shared `keyStore` (Redis or a shared database). Sharing client public keys alone cannot enable cross-node decryption. There is no implicit process key or memory-registry fallback; development/tests may explicitly use `createInMemoryKeyStore(capacity)`. Reload the same ring after restart; sticky sessions are unnecessary.
|
|
204
222
|
|
|
205
223
|
> **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.
|
|
206
224
|
|
|
207
|
-
|
|
225
|
+
Only authenticated protocol v2 is accepted. Independent random SM4-CBC and HMAC-SM3 keys are wrapped with SM2; verify the sender signature and MAC before CBC decryption. Clients sign requests and the selected server key version signs responses. Authentication covers version, direction, serverKeyId, clientId, a random 128-bit requestId acting as the nonce, issuedAt, HTTP method, the SM3 pathHash of pathname plus the original search, and response status. Requests have a five-minute lifetime and at most 30 seconds of future clock skew. The shared registry atomically consumes (clientId, requestId), retaining it until at least issuedAt plus six minutes to cover node skew; storage failures reject requests. Synchronize node clocks and use a Redis noeviction/durable policy that preserves live nonces. Transport replay prevention does not replace business idempotency keys.
|
|
226
|
+
|
|
227
|
+
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. Exclude plaintext endpoints explicitly with excludePaths. CORS must allow and expose X-Hai-Transport. TLS authenticates handshake keys; this protocol does not replace IAM authorization.
|
|
228
|
+
|
|
229
|
+
Fixed protocol budgets come from `@h-ai/crypto`'s `TRANSPORT_BODY_LIMITS`: 16 KiB key exchange JSON, 4 KiB per public key, 1 MiB plaintext, and 2 MiB + 32 KiB encrypted JSON, measured in UTF-8 bytes. The original stream is read incrementally; limits or cancellation stop/cancel it and release the reader without cloning and fully buffering it. Oversized requests return 413, oversized responses return 500, and invalid public keys return 400. Rebuilt requests preserve signals and clear Hono's body cache; rebuilt requests/responses remove stale Content-Length/Content-Encoding. These budgets are protocol constants, not YAML or environment settings.
|
|
230
|
+
|
|
231
|
+
#### Token authentication (`iam` / `refreshCookie`)
|
|
208
232
|
|
|
209
233
|
Pass the top-level `iam` handle to `serv.createApp` to derive:
|
|
210
234
|
|
|
@@ -246,7 +270,9 @@ const app = serv.createApp({
|
|
|
246
270
|
})
|
|
247
271
|
```
|
|
248
272
|
|
|
249
|
-
|
|
273
|
+
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.
|
|
274
|
+
|
|
275
|
+
#### Custom procedures (authentication + permissions)
|
|
250
276
|
|
|
251
277
|
```ts
|
|
252
278
|
const procedures = serv
|
|
@@ -260,7 +286,7 @@ const procedures = serv
|
|
|
260
286
|
.build()
|
|
261
287
|
```
|
|
262
288
|
|
|
263
|
-
|
|
289
|
+
#### Advanced pipelines (HTTP middleware + context + route guards)
|
|
264
290
|
|
|
265
291
|
`@h-ai/serv` pipelines have three layers and extension points:
|
|
266
292
|
|
|
@@ -268,7 +294,7 @@ const procedures = serv
|
|
|
268
294
|
2. **Context**: customize through `verifyToken` / `createContext` / `serv.buildAuthContextFactory()`.
|
|
269
295
|
3. **Route guards**: declare authentication/authorization with `.auth()` / `.permission()` / `.role()`.
|
|
270
296
|
|
|
271
|
-
|
|
297
|
+
##### 1) Custom HTTP middleware
|
|
272
298
|
|
|
273
299
|
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:
|
|
274
300
|
|
|
@@ -336,7 +362,7 @@ Notes:
|
|
|
336
362
|
- Expose custom response headers such as `X-Encrypted` explicitly through `serv.cors({ exposedHeaders: [...] })` for browsers.
|
|
337
363
|
- This layer works with HTTP middleware context and returns **HTTP Response**, not `HaiResult`.
|
|
338
364
|
|
|
339
|
-
|
|
365
|
+
##### 2) Custom context pipelines
|
|
340
366
|
|
|
341
367
|
To reuse `Bearer token -> session` and add tenant, workspace, or source-system fields,
|
|
342
368
|
prefer `serv.buildAuthContextFactory()` to assemble a custom `createContext`:
|
|
@@ -363,7 +389,7 @@ Choose the appropriate extension:
|
|
|
363
389
|
- Add fields to default authentication context: use `buildAuthContextFactory(...) + createContext`.
|
|
364
390
|
- Replace context construction entirely: use `createContext` directly.
|
|
365
391
|
|
|
366
|
-
|
|
392
|
+
##### 3) Route guard
|
|
367
393
|
|
|
368
394
|
Public procedures use `.route(path, handler)`. For guarded routes, use
|
|
369
395
|
`.route(path).auth/permission/role().handle(handler)`:
|
|
@@ -396,7 +422,7 @@ Public API boundaries:
|
|
|
396
422
|
- The stable application-facing entry remains `@h-ai/serv`.
|
|
397
423
|
- Internal helpers such as `securityHeaders` / `requireInternalRPC` / `buildHaiErrorBody` are not documented as separate subpath exports.
|
|
398
424
|
|
|
399
|
-
|
|
425
|
+
#### httpOnly cookie authentication (recommended for production)
|
|
400
426
|
|
|
401
427
|
httpOnly cookie mode stores refresh tokens in server-managed cookies unreadable to browser JavaScript, reducing XSS exposure.
|
|
402
428
|
|
|
@@ -430,7 +456,7 @@ const app = serv.createApp({
|
|
|
430
456
|
refreshCookie: {
|
|
431
457
|
// cookieName: 'hai_refresh_token', // 默认
|
|
432
458
|
// maxAge: 30 * 24 * 3600, // 默认 30 天
|
|
433
|
-
// secure: true, //
|
|
459
|
+
// secure: true, // NODE_ENV 或 HAI_CORE_ENV 为 production 时默认开启
|
|
434
460
|
// onRefresh: customFn, // 可覆盖 iam.session.refresh。
|
|
435
461
|
nativeTokenTransport: {
|
|
436
462
|
// 必须 fail-closed:同时校验平台标识与允许的原生 Origin。
|
|
@@ -455,7 +481,7 @@ await apiClient.init({
|
|
|
455
481
|
|
|
456
482
|
> **Cookie attributes**: `HttpOnly; SameSite=Strict; Secure` in production; `Path` is limited to `{apiPrefix}/auth/refresh`.
|
|
457
483
|
|
|
458
|
-
|
|
484
|
+
### API overview
|
|
459
485
|
|
|
460
486
|
- `serv.createApp(options)`: create the HTTP App with custom middleware, health checks, OpenAPI, optional docs, and RPC.
|
|
461
487
|
- `serv.parseRequestContext({ request })`: extract Bearer token + requestId without populating session.
|
|
@@ -482,7 +508,7 @@ Additional root types / named exports:
|
|
|
482
508
|
|
|
483
509
|
> Transport encryption is a `createApp` option delegated internally to `crypto.transport`, not a flat `serv.xxx` API.
|
|
484
510
|
|
|
485
|
-
|
|
511
|
+
### Configuration
|
|
486
512
|
|
|
487
513
|
`@h-ai/serv` does not scan YAML itself. Load `config/_serv.yml` through `@h-ai/core` and pass the result to `serv.createApp()`.
|
|
488
514
|
|
|
@@ -506,9 +532,10 @@ const app = serv.createApp({
|
|
|
506
532
|
? undefined
|
|
507
533
|
: {
|
|
508
534
|
crypto,
|
|
535
|
+
serverKeyRing,
|
|
536
|
+
keyStore,
|
|
509
537
|
keyExchangePath: servConfig.transport.keyExchangePath,
|
|
510
538
|
excludePaths: [...servConfig.transport.excludePaths],
|
|
511
|
-
maxClients: servConfig.transport.maxClients,
|
|
512
539
|
},
|
|
513
540
|
})
|
|
514
541
|
```
|
|
@@ -551,7 +578,6 @@ transport:
|
|
|
551
578
|
- /openapi.json
|
|
552
579
|
- /docs
|
|
553
580
|
- /_hai/scalar.js
|
|
554
|
-
maxClients: 10000
|
|
555
581
|
```
|
|
556
582
|
|
|
557
583
|
`server` (host/port): `HOST` / `PORT` environment variables > configuration > `127.0.0.1:3000`. Applications pass `servConfig.server` to `serv.listen`.
|
|
@@ -597,7 +623,7 @@ const app = serv.createApp({
|
|
|
597
623
|
|
|
598
624
|
In production, empty or `*` `_serv.yml` `cors.origin` fails startup. Web and Capacitor/Tauri origins use exact normalized full-Origin matching to prevent suffix spoofing. Production also enforces Secure refresh cookies and disables OpenAPI/docs; configuration controls nonproduction endpoints.
|
|
599
625
|
|
|
600
|
-
|
|
626
|
+
### Configuration driven authentication
|
|
601
627
|
|
|
602
628
|
Agreements support configuration-loaded userAgreementMarkdown/privacyPolicyMarkdown and URLs. Markdown takes priority for the same agreement and is safely rendered in a modal.
|
|
603
629
|
|
|
@@ -605,7 +631,7 @@ createIamProcedures({ iam }) implements public config, password/LDAP/OTP login,
|
|
|
605
631
|
|
|
606
632
|
[完整 IAM 配置 / IAM configuration](../iam/README.en.md)
|
|
607
633
|
|
|
608
|
-
|
|
634
|
+
### Error handling
|
|
609
635
|
|
|
610
636
|
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.
|
|
611
637
|
|
|
@@ -656,7 +682,17 @@ const procedures = serv
|
|
|
656
682
|
|
|
657
683
|
> When downstream modules (such as `iam.user.xxx()`) return `HaiResult` directly, serv forwards `error.message` unchanged. `HaiError` holds only the final `message`, not `messageKey/params`, so **serv cannot generically retranslate arbitrary downstream errors**. Request-level localization must receive locale at the module that creates the error message.
|
|
658
684
|
|
|
659
|
-
|
|
685
|
+
### Request locale isolation
|
|
686
|
+
|
|
687
|
+
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.
|
|
688
|
+
|
|
689
|
+
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.
|
|
690
|
+
|
|
691
|
+
## Input limits and HTTP capabilities
|
|
692
|
+
|
|
693
|
+
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#存储输入约束与跨端能力).
|
|
694
|
+
|
|
695
|
+
## Development and testing
|
|
660
696
|
|
|
661
697
|
```bash
|
|
662
698
|
pnpm --filter @h-ai/serv test
|
|
@@ -664,6 +700,8 @@ pnpm --filter @h-ai/serv typecheck
|
|
|
664
700
|
pnpm --filter @h-ai/serv lint
|
|
665
701
|
```
|
|
666
702
|
|
|
703
|
+
Declaration JSDoc examples import actual `@h-ai/serv` entries or published feature subpaths. Internal Hono, guard, router and transport examples are marked as internal module context and are not root APIs. Examples distinguish startup configuration exceptions, domain HaiResult, HTTP middleware statuses, and listener shutdown returning Promise<void>; default AI completion still returns non-streaming JSON only. Maintainers can run `node scripts/check-module-jsdoc.mjs --package serv` against actual declarations and internal lexical bindings. Compilation does not replace real HTTP, authorization or distributed replay-protection validation.
|
|
704
|
+
|
|
667
705
|
## License
|
|
668
706
|
|
|
669
707
|
Apache-2.0
|
package/README.ja.md
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
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
|
-
|
|
7
|
+
## 概要
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
hai-framework の API サービスランタイム。公開 HTTP App 抽象を通じて `@h-ai/api-contract` を複数プラットフォームから使える HTTP API として提供します。Hono は内部実装です。
|
|
10
|
+
|
|
11
|
+
## 主な機能
|
|
10
12
|
|
|
11
13
|
- `serv.createApp(...)` → `serv.listen(app, { port })` で起動。アプリは Hono を import する必要がありません。
|
|
12
14
|
- フラットな API:`serv.createApp / serv.implement / serv.listen / serv.toFetch / serv.generateSpec / ...`。
|
|
@@ -17,12 +19,14 @@
|
|
|
17
19
|
- `build()` は通常の router を同期的に返すため、標準 feature のネストした procedures を直接選択できます。接続する contract と procedures を一致させ、未選択の入口は公開しません。
|
|
18
20
|
- 存在しないルートは翻訳された HaiResult JSON と HTTP 404 を返します。通信暗号化でも 404 を保ち、非 JSON 応答による 500 にしません。
|
|
19
21
|
- 安全な応答ヘッダー・ヘルスチェックを内蔵し、OpenAPI JSON・Scalar 文書・内部 RPC endpoint は任意。
|
|
20
|
-
- 任意の通信暗号化:`serv.createApp({ transport: { crypto } })` で鍵交換とリクエスト・レスポンスの暗号化を自動接続。
|
|
22
|
+
- 任意の通信暗号化:`serv.createApp({ transport: { crypto, serverKeyRing, keyStore } })` で鍵交換とリクエスト・レスポンスの暗号化を自動接続。
|
|
21
23
|
- 任意の音声入口:`serv.createApp({ audio: { ai, verifyTicket, authorize } })` が共通 WebSocket を接続します。接続直後に単回 ticket で認証(`{ session, grant? }`、独立した事前認証 timeout)。各フレームを Zod と厳密な状態機械で検証し、入力・キュー・送信バッファの背圧と連鎖キャンセルを管理します。サーバーの認可処理で操作・モデル・声・割当量・同時使用数を確認します。
|
|
22
24
|
|
|
23
|
-
> ライフサイクル:`@h-ai/serv` は状態を持たない HTTP App の構成機能で、`init()` / `close()` は不要です。app 作成前に `iam` / `storage` / `ai` / `crypto` などを初期化し、`serv.listen(..., { onClose })` で逆順に解放します。`serv.createApp()` は transport 管理器の生成失敗など起動設定のエラーを即座に throw します。
|
|
25
|
+
> ライフサイクル:`@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 だけを閉じ、業務モジュールはホストが別途逆順に解放します。
|
|
26
|
+
|
|
27
|
+
## 使い始める
|
|
24
28
|
|
|
25
|
-
|
|
29
|
+
### クイックスタート
|
|
26
30
|
|
|
27
31
|
```ts
|
|
28
32
|
import { apiContract } from '@h-ai/api-contract'
|
|
@@ -37,7 +41,11 @@ const contract = apiContract.create({ iam: apiContract.iam, storage: apiContract
|
|
|
37
41
|
// 2) 组装 procedures(注入 iam / storage / ai 已初始化的 functions)
|
|
38
42
|
const procedures = {
|
|
39
43
|
iam: createIamProcedures({ iam }),
|
|
40
|
-
storage: createStorageProcedures({
|
|
44
|
+
storage: createStorageProcedures({
|
|
45
|
+
storage,
|
|
46
|
+
authorize: ({ context, keys }) => keys.every(key =>
|
|
47
|
+
key.startsWith(`users/${encodeURIComponent(context.session.userId)}/`)),
|
|
48
|
+
}),
|
|
41
49
|
ai: createAiProcedures({ ai }),
|
|
42
50
|
}
|
|
43
51
|
|
|
@@ -64,7 +72,15 @@ serv.listen(app, {
|
|
|
64
72
|
// 5) 优雅关闭由 serv 托管,无需手动注册 process.once('SIGINT', ...)
|
|
65
73
|
```
|
|
66
74
|
|
|
67
|
-
|
|
75
|
+
Storage procedures には `authorize({ context, operation, keys })` が必須で、未設定の場合はアクセスを拒否します。認証済み session から所有者・テナントを判定し、ダウンロード、アップロード、一覧、メタデータ、削除に適用します。上記の例は `users/<エンコードした userId>/` 配下だけを許可し、一覧にもその prefix が必要です。一括処理はすべての key が許可された後に実行し、管理者アクセスはアプリ側で明示的に許可します。
|
|
76
|
+
|
|
77
|
+
`storage.files.deleteMany` の成功データは `{ deletedKeys, failures: [] }` です。失敗進捗は `error.ext.deleteMany`(`failed / unknown / notAttempted`)にあり、HTTP 200 だけで完了と判断しません。[Storage の一括削除](../storage/README.ja.md#api-一覧)を参照してください。
|
|
78
|
+
|
|
79
|
+
AI procedures は省略された `objectId` を信頼できる `context.session.userId` に結び付けます。会話・メッセージの保存、履歴、会話一覧、記憶 recall/list は認可された主体で処理し、会話は `objectId + sessionId` で分離します。書き込み時の sessionId の既定値は `default` です。他の objectId へのアクセスは既定で拒否し、管理者も信頼できる権限とテナント関係を検証する明示的な `authorizeObject` ポリシーが必要です。
|
|
80
|
+
|
|
81
|
+
`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 を定義します。
|
|
82
|
+
|
|
83
|
+
#### host / port 設定
|
|
68
84
|
|
|
69
85
|
host/port は `config/_serv.yml` の `server` で指定し、`HOST` / `PORT` 環境変数 > 設定ファイル > `127.0.0.1:3000` の順です。既定の `127.0.0.1` はローカル専用で、コンテナー外に公開する場合は `0.0.0.0` にします:
|
|
70
86
|
|
|
@@ -77,7 +93,9 @@ server:
|
|
|
77
93
|
|
|
78
94
|
解決済みの `server` を `serv.listen` に渡します。`host` / `port` の明示的な指定で上書きすることもできます。
|
|
79
95
|
|
|
80
|
-
|
|
96
|
+
#### 標準 fetch シグネチャのアダプター
|
|
97
|
+
|
|
98
|
+
toFetch は Node 互換ホストやテスト注入用の標準 fetch シグネチャです。パッケージルートは Node API に依存しており、それらを持たない Workers・Deno・エッジへの対応を保証しません。
|
|
81
99
|
|
|
82
100
|
```ts
|
|
83
101
|
const handler = serv.toFetch(app)
|
|
@@ -86,7 +104,9 @@ export default { fetch: handler }
|
|
|
86
104
|
|
|
87
105
|
Node `serv.listen()` は socket の接続元を自動で渡し、既定では転送ヘッダーを無視します。`http.trustedProxies` の IP/CIDR のみ信頼し、XFF を右から左へ調べ、最初の非信頼アドレスで停止します。その左側は無視します。信頼チェーンが不正・長すぎる場合は IP 未定義となり、代理をクライアントとして扱いません。代理は転送ヘッダーを清掃・追加してください。Fetch は `app.fetch(request, { remoteAddress: platformPeerAddress })` で信頼できる接続元を渡します。メタデータなしは IP 未定義です。任意のヘッダーをコピーしないでください。独自 `createContext(input)` は input 全体を再利用し、解決済み `input.ip` を保持します。内部 RPC は接続元で制限します。
|
|
88
106
|
|
|
89
|
-
|
|
107
|
+
toFetch は Request・URL・文字列を標準 Request(input, init) で構築し、入力フィールドを変更せず method・headers・body・signal などを上書きします。消費済み・ロック中の本文や不正な本文指定は返却 Promise を reject します。派発前の取消しは handler を呼ばず、実行中の取消しは呼出側の待機を終了し、有効な signal をアプリへ伝えます。終了・失敗・取消しで listener を外し、取消し後に遅れて返る本文も cancel します。開始済みの外部効果は巻き戻せず、Response 返却後のストリーム終了はアプリが request.signal を消費して管理します。
|
|
108
|
+
|
|
109
|
+
#### Audio WebSocket:ticket とサーバー認可
|
|
90
110
|
|
|
91
111
|
音声接続には短期・単回 ticket が必要です。認証済み HTTP endpoint で発行(推奨:`iam.ticket.issue`)し、`verifyTicket` でアトミックに消費します(推奨:`iam.ticket.consume`)。接続直後に独立 timeout で認証し、認証前に業務フレームを処理しません。返された `ServSession` と任意の `grant` を保存してから `authorize` で有料パラメーターを確認します:
|
|
92
112
|
|
|
@@ -110,18 +130,22 @@ const app = serv.createApp({
|
|
|
110
130
|
- 各フレームを Zod と厳密な状態機械で検証します。認識は音声、合成はテキストのみ、`start`/`done` は各 1 回、`segmentId` はセッション内で一意です。入力・滞留メッセージ・送信バッファを制限し、超過・キャンセル時はドメインエラーで閉じて上流も中止します。
|
|
111
131
|
- 調整項目:`preAuthTimeoutMs`(既定 5000)、`maxPendingMessages`(256)、`maxSendBufferBytes`(8 MiB)、`maxMessageBytes` / `maxBufferedBytes` / `maxTextBytes` / `maxSessionMs`。
|
|
112
132
|
- `onSessionEnd` は成功・失敗・切断後に最大 1 回呼び、同時実行枠の解放に使用。
|
|
113
|
-
- ブラウザーの `
|
|
133
|
+
- ブラウザーの `createAudioClient({ url, getTicket })` は単回 `ticket` のみを URL に付けます。`getTicket(request)` は `operation` / `model` の概要から今回の操作専用 ticket を発行できます。通常の IAM access token は渡さないでください。
|
|
114
134
|
|
|
115
|
-
|
|
135
|
+
#### 通信暗号化(`@h-ai/crypto` と共通)
|
|
116
136
|
|
|
117
137
|
serv 独自の暗号化ファクトリーはなく、`crypto.transport` から管理器を作成します。利用側はトップレベルの `crypto` のみを渡します:
|
|
118
138
|
|
|
119
139
|
```ts
|
|
120
140
|
import { cache } from '@h-ai/cache'
|
|
121
|
-
import { createRedisTransportKeyStore, crypto } from '@h-ai/crypto'
|
|
141
|
+
import { createRedisTransportKeyStore, crypto, parseTransportServerKeyRing } from '@h-ai/crypto'
|
|
122
142
|
import { serv } from '@h-ai/serv'
|
|
123
143
|
|
|
124
144
|
await crypto.init()
|
|
145
|
+
const ring = parseTransportServerKeyRing(process.env.HAI_CRYPTO_TRANSPORT_SERVERKEYRING)
|
|
146
|
+
if (!ring.success)
|
|
147
|
+
throw new Error(ring.error.message)
|
|
148
|
+
const serverKeyRing = ring.data
|
|
125
149
|
await cache.init({ type: 'redis', host: '127.0.0.1', port: 6379 })
|
|
126
150
|
|
|
127
151
|
const app = serv.createApp({
|
|
@@ -130,9 +154,9 @@ const app = serv.createApp({
|
|
|
130
154
|
http: { apiPrefix: '/api/v1' },
|
|
131
155
|
transport: {
|
|
132
156
|
crypto,
|
|
157
|
+
serverKeyRing,
|
|
133
158
|
// keyExchangePath 默认 '/_hai/key-exchange',最终端点为 '/api/v1/_hai/key-exchange'
|
|
134
159
|
// excludePaths: ['/health'],
|
|
135
|
-
// maxClients: 10000,
|
|
136
160
|
keyStore: createRedisTransportKeyStore({ cache, ttlSeconds: 3600 }),
|
|
137
161
|
},
|
|
138
162
|
})
|
|
@@ -165,7 +189,6 @@ transport:
|
|
|
165
189
|
- /openapi.json
|
|
166
190
|
- /docs
|
|
167
191
|
- /_hai/scalar.js
|
|
168
|
-
maxClients: 10000
|
|
169
192
|
```
|
|
170
193
|
|
|
171
194
|
```ts
|
|
@@ -187,24 +210,25 @@ const app = serv.createApp({
|
|
|
187
210
|
? undefined
|
|
188
211
|
: {
|
|
189
212
|
crypto,
|
|
213
|
+
serverKeyRing,
|
|
214
|
+
keyStore,
|
|
190
215
|
keyExchangePath: servConfig.transport.keyExchangePath,
|
|
191
216
|
excludePaths: [...servConfig.transport.excludePaths],
|
|
192
|
-
maxClients: servConfig.transport.maxClients,
|
|
193
217
|
},
|
|
194
218
|
})
|
|
195
219
|
```
|
|
196
220
|
|
|
197
|
-
>
|
|
198
|
-
>
|
|
199
|
-
> - ロードバランサーで **sticky session** を有効化し、同じクライアントを同じノードへ接続。
|
|
200
|
-
> - 各クライアントが各ノードへの初回接続時に個別に鍵交換。
|
|
201
|
-
> - `createRedisTransportKeyStore()` / `createReldbTransportKeyStore()` などの共有 `keyStore` を注入。
|
|
202
|
-
>
|
|
203
|
-
> `keyStore` は実行時オブジェクトの依存で、`config/_serv.yml` に保存しません。設定にはパス・許可リスト・容量などの静的項目のみを記載します。
|
|
221
|
+
> サーバーには同じ管理済み `serverKeyRing` と共有 `keyStore`(Redis または共有 DB)を明示的に渡します。クライアント公開鍵の共有だけではノード間で復号できません。暗黙のプロセス鍵・メモリー registry はなく、開発・テストでは `createInMemoryKeyStore(capacity)` を明示します。再起動後は同じ鍵環を読み込み、sticky session は不要です。
|
|
204
222
|
|
|
205
223
|
> **安全方針**:`transport` 有効時は `excludePaths` と鍵交換以外で有効な `X-Client-Id` が必要です。不足・未知の clientId は 400、暗号化不能・非 JSON 応答・上限超過はエラーになり、平文の業務データは渡しません。
|
|
206
224
|
|
|
207
|
-
|
|
225
|
+
認証済み v2 のみを受け付けます。独立したランダム SM4-CBC 鍵と HMAC-SM3 鍵を SM2 で包み、送信者署名、MAC の順に検証してから CBC を復号します。要求はクライアント秘密鍵、応答は選択されたサーバー鍵バージョンで署名します。version、direction、serverKeyId、clientId、nonce を兼ねるランダム 128 ビット requestId、issuedAt、HTTP method、pathname と元の search の SM3 pathHash、応答 status を認証します。要求の有効期限は 5 分、未来方向の時計差は最大 30 秒です。共有 registry が (clientId, requestId) を原子的に消費し、ノード間の時計差を含め issuedAt + 6 分まで保持します。保存障害では拒否します。時計同期と Redis の noeviction/永続化により有効な nonce を保持してください。業務の再試行には別の冪等キーが必要です。
|
|
226
|
+
|
|
227
|
+
空の POST も空平文の認証封装を送ります。GET/HEAD は X-Hai-Transport ヘッダー、204/304/HEAD など本文なし応答も同じヘッダーを使います。他の要求・応答は JSON 本文と X-Encrypted: true を使います。クライアントは元の要求コンテキストと HTTP status を照合し、マーカー欠落、旧形式、認証失敗では reject します。平文 endpoint は excludePaths で明示的に除外し、CORS では X-Hai-Transport を許可・公開してください。鍵交換の公開鍵認証は TLS に依存し、IAM の業務認可は別に必要です。
|
|
228
|
+
|
|
229
|
+
固定上限は `@h-ai/crypto` の `TRANSPORT_BODY_LIMITS` を共有し、鍵交換 JSON は 16 KiB、公開鍵は 1 個あたり 4 KiB、平文は 1 MiB、暗号文 JSON は 2 MiB + 32 KiB です。実際の UTF-8 バイトを元のストリームから逐次読み取り、超過・キャンセル時は停止・キャンセルして reader を解放します。clone して全量を読み取る処理は行いません。リクエスト超過は 413、応答超過は 500、不正な公開鍵は 400 です。再構築したリクエストは signal を維持して Hono の本文キャッシュを消去し、リクエスト・応答の古い Content-Length/Content-Encoding を削除します。上限はプロトコル定数であり、YAML・環境変数では設定しません。
|
|
230
|
+
|
|
231
|
+
#### Token 認証(`iam` / `refreshCookie`)
|
|
208
232
|
|
|
209
233
|
`serv.createApp` にトップレベルの `iam` を渡すと、次を自動構成します:
|
|
210
234
|
|
|
@@ -246,7 +270,9 @@ const app = serv.createApp({
|
|
|
246
270
|
})
|
|
247
271
|
```
|
|
248
272
|
|
|
249
|
-
|
|
273
|
+
標準 `auth.logout` は input.accessToken または Authorization Bearer の完全な Token を取り消し、有効な access 認証を前提にしません。userId による権限や取消対象は受け付けません。Token 不足は失敗、不存在 Token は冪等成功です。共有ストア障害は失敗 HaiResult を保ち、Cookie middleware は成功時だけ refresh Cookie を消去します。通常 procedure の HTTP 200 / HaiResult 契約は同じなので、クライアントは HTTP status だけでなく success を確認します。
|
|
274
|
+
|
|
275
|
+
#### 独自 procedure(認証 + 権限)
|
|
250
276
|
|
|
251
277
|
```ts
|
|
252
278
|
const procedures = serv
|
|
@@ -260,7 +286,7 @@ const procedures = serv
|
|
|
260
286
|
.build()
|
|
261
287
|
```
|
|
262
288
|
|
|
263
|
-
|
|
289
|
+
#### 高度なパイプライン(HTTP middleware + context + route guard)
|
|
264
290
|
|
|
265
291
|
`@h-ai/serv` のパイプラインは 3 層で、それぞれの拡張入口があります:
|
|
266
292
|
|
|
@@ -268,7 +294,7 @@ const procedures = serv
|
|
|
268
294
|
2. **context 層**:`verifyToken` / `createContext` / `serv.buildAuthContextFactory()` で拡張。
|
|
269
295
|
3. **route guard 層**:`.auth()` / `.permission()` / `.role()` で認証・認可を宣言。
|
|
270
296
|
|
|
271
|
-
|
|
297
|
+
##### 1) 独自 HTTP middleware
|
|
272
298
|
|
|
273
299
|
ログ・trace・レート制限・CORS・テナントヘッダーなどの **HTTP 共通処理**、または JSON/oRPC contract に適さない WebSocket・ファイル・バイナリー endpoint には `middlewares` を使います。一般的な CORS は `serv.cors(...)` を再利用できます:
|
|
274
300
|
|
|
@@ -336,7 +362,7 @@ const app = serv.createApp({
|
|
|
336
362
|
- ブラウザーが `X-Encrypted` などを読む場合は `serv.cors({ exposedHeaders: [...] })` で明示的に公開。
|
|
337
363
|
- この層は HTTP middleware context を操作し、`HaiResult` ではなく **HTTP Response** を返します。
|
|
338
364
|
|
|
339
|
-
|
|
365
|
+
##### 2) 独自 context パイプライン
|
|
340
366
|
|
|
341
367
|
`Bearer token -> session` を再利用し、テナント・workspace・送信元などの項目を追加する場合は、
|
|
342
368
|
`serv.buildAuthContextFactory()` で独自の `createContext` を構成してください:
|
|
@@ -363,7 +389,7 @@ const app = serv.createApp({
|
|
|
363
389
|
- 既定の認証 context に項目を追加:`buildAuthContextFactory(...) + createContext`。
|
|
364
390
|
- context 生成全体を置換:直接 `createContext`。
|
|
365
391
|
|
|
366
|
-
|
|
392
|
+
##### 3) Route guard
|
|
367
393
|
|
|
368
394
|
公開 procedure は `.route(path, handler)` を使い、ガードが必要な場合は次を使います:
|
|
369
395
|
`.route(path).auth/permission/role().handle(handler)`:
|
|
@@ -396,7 +422,7 @@ const procedures = serv
|
|
|
396
422
|
- アプリ向けの安定した入口はルートの `@h-ai/serv`。
|
|
397
423
|
- `securityHeaders` / `requireInternalRPC` / `buildHaiErrorBody` などの内部 helper は独立サブパスとして公開しません。
|
|
398
424
|
|
|
399
|
-
|
|
425
|
+
#### httpOnly Cookie 認証(本番推奨)
|
|
400
426
|
|
|
401
427
|
httpOnly cookie モードは refresh token をサーバー管理の Cookie に保存し、ブラウザー JS から読めなくして XSS による漏洩を抑えます。
|
|
402
428
|
|
|
@@ -430,7 +456,7 @@ const app = serv.createApp({
|
|
|
430
456
|
refreshCookie: {
|
|
431
457
|
// cookieName: 'hai_refresh_token', // 默认
|
|
432
458
|
// maxAge: 30 * 24 * 3600, // 默认 30 天
|
|
433
|
-
// secure: true, //
|
|
459
|
+
// secure: true, // NODE_ENV 或 HAI_CORE_ENV 为 production 时默认开启
|
|
434
460
|
// onRefresh: customFn, // 可覆盖 iam.session.refresh。
|
|
435
461
|
nativeTokenTransport: {
|
|
436
462
|
// 必须 fail-closed:同时校验平台标识与允许的原生 Origin。
|
|
@@ -455,7 +481,7 @@ await apiClient.init({
|
|
|
455
481
|
|
|
456
482
|
> **Cookie 属性**:本番は `HttpOnly; SameSite=Strict; Secure`。`Path` は `{apiPrefix}/auth/refresh` に限定。
|
|
457
483
|
|
|
458
|
-
|
|
484
|
+
### API 一覧
|
|
459
485
|
|
|
460
486
|
- `serv.createApp(options)`:独自 middleware、ヘルスチェック、OpenAPI、任意の文書・RPC を備えた HTTP App を作成。
|
|
461
487
|
- `serv.parseRequestContext({ request })`:Bearer token + requestId を抽出し、session は設定しません。
|
|
@@ -482,7 +508,7 @@ await apiClient.init({
|
|
|
482
508
|
|
|
483
509
|
> 通信暗号化は `createApp` の設定機能で、内部で `crypto.transport` に委譲します。`serv.xxx` の独立 API ではありません。
|
|
484
510
|
|
|
485
|
-
|
|
511
|
+
### 設定
|
|
486
512
|
|
|
487
513
|
`@h-ai/serv` 自体は YAML を走査しません。`@h-ai/core` で `config/_serv.yml` を読み、解析結果を `serv.createApp()` に渡してください。
|
|
488
514
|
|
|
@@ -506,9 +532,10 @@ const app = serv.createApp({
|
|
|
506
532
|
? undefined
|
|
507
533
|
: {
|
|
508
534
|
crypto,
|
|
535
|
+
serverKeyRing,
|
|
536
|
+
keyStore,
|
|
509
537
|
keyExchangePath: servConfig.transport.keyExchangePath,
|
|
510
538
|
excludePaths: [...servConfig.transport.excludePaths],
|
|
511
|
-
maxClients: servConfig.transport.maxClients,
|
|
512
539
|
},
|
|
513
540
|
})
|
|
514
541
|
```
|
|
@@ -551,7 +578,6 @@ transport:
|
|
|
551
578
|
- /openapi.json
|
|
552
579
|
- /docs
|
|
553
580
|
- /_hai/scalar.js
|
|
554
|
-
maxClients: 10000
|
|
555
581
|
```
|
|
556
582
|
|
|
557
583
|
`server`(host/port):`HOST` / `PORT` 環境変数 > 設定ファイル > `127.0.0.1:3000`。アプリが `servConfig.server` を `serv.listen` に渡します。
|
|
@@ -597,7 +623,7 @@ const app = serv.createApp({
|
|
|
597
623
|
|
|
598
624
|
本番では `_serv.yml` の `cors.origin` が空・`*` なら起動に失敗します。Web・Capacitor/Tauri は正規化済み Origin 全体を厳密照合し、末尾偽装を防ぎます。本番は Secure refresh cookie を強制し OpenAPI/docs を無効化します。非本番の endpoint は設定ファイルで決めます。
|
|
599
625
|
|
|
600
|
-
|
|
626
|
+
### 設定による認証画面
|
|
601
627
|
|
|
602
628
|
規約は設定の userAgreementMarkdown/privacyPolicyMarkdown または URL を使用できます。同じ規約は Markdown 本文を優先して安全な Modal で描画します。
|
|
603
629
|
|
|
@@ -605,7 +631,7 @@ createIamProcedures({ iam }) は公開設定、LDAP/OTP/パスワード、登録
|
|
|
605
631
|
|
|
606
632
|
[完整 IAM 配置 / IAM configuration](../iam/README.ja.md)
|
|
607
633
|
|
|
608
|
-
|
|
634
|
+
### エラー処理
|
|
609
635
|
|
|
610
636
|
通常の procedure は認証・認可・業務の失敗と捕捉した handler 例外を HaiResult として既定で HTTP 200 で返します。`error.httpStatus` はドメインエラーのメタデータで、HTTP 応答ステータスを変更しません。クライアントは `success` と `error.code` を確認してください。未認証では `error.httpStatus=401`、DB クエリー失敗や INTERNAL_ERROR では 500 です。
|
|
611
637
|
|
|
@@ -656,7 +682,17 @@ const procedures = serv
|
|
|
656
682
|
|
|
657
683
|
> `iam.user.xxx()` など下流が直接 `HaiResult` を返す場合、serv は `error.message` をそのまま渡します。`HaiError` は最終 `message` のみで `messageKey/params` を持たないため、**serv の境界で任意の下流エラーを汎用的に再翻訳することはできません**。リクエストごとの i18n はエラーを生成するモジュールで locale を受け取る必要があります。
|
|
658
684
|
|
|
659
|
-
|
|
685
|
+
### リクエスト言語の分離
|
|
686
|
+
|
|
687
|
+
Node リクエストと SSR は `core.i18n.runWithLocale(locale, () => handleRequest())` で囲みます。SvelteKit は `kit.i18n.runWithLocale(locale, () => resolve(event))` を使えます。serv はリクエストヘッダーから自動設定します。非同期の子処理も言語を継承し、明示的な `MessageOptions.locale`、リクエスト言語、起動時の既定値の順で優先します。コンテキスト内の `setGlobalLocale` はそのリクエストだけを変更します。コンテキスト外の設定は起動時に限定し、リクエスト hook から呼びません。
|
|
688
|
+
|
|
689
|
+
ブラウザーはアプリ全体の `setGlobalLocale` / `createLocaleStore` を使います。SSR の UI store 初期化と setter は自身の状態のみ変更し、メッセージはリクエストコンテキストを参照します。`runWithLocale` は Node 入口専用です。
|
|
690
|
+
|
|
691
|
+
## 入力制約と HTTP 機能
|
|
692
|
+
|
|
693
|
+
キーは 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#存储输入约束与跨端能力)を参照してください。
|
|
694
|
+
|
|
695
|
+
## 開発とテスト
|
|
660
696
|
|
|
661
697
|
```bash
|
|
662
698
|
pnpm --filter @h-ai/serv test
|
|
@@ -664,6 +700,8 @@ pnpm --filter @h-ai/serv typecheck
|
|
|
664
700
|
pnpm --filter @h-ai/serv lint
|
|
665
701
|
```
|
|
666
702
|
|
|
667
|
-
|
|
703
|
+
宣言の JSDoc 例は実在する `@h-ai/serv` または公開 features サブパスから import します。内部 Hono・guard・router・transport は「模块内部上下文」と明記し、ルート API として扱いません。起動設定の例外、業務 HaiResult、HTTP middleware の status、listener の Promise<void> 終了を区別し、既定の AI completion は非ストリーミング JSON のままです。保守時は `node scripts/check-module-jsdoc.mjs --package serv` で実際の宣言と内部スコープに対して例をコンパイルします。実 HTTP、認可、共有リプレイ防止の検証を代替しません。
|
|
704
|
+
|
|
705
|
+
## ライセンス
|
|
668
706
|
|
|
669
707
|
Apache-2.0
|