@h-ai/serv 0.1.0-alpha.65 → 0.1.0-alpha.67
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 +23 -19
- package/README.ja.md +24 -20
- package/README.md +24 -20
- package/package.json +10 -10
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 / ...`.
|
|
@@ -22,7 +24,9 @@
|
|
|
22
24
|
|
|
23
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.
|
|
24
26
|
|
|
25
|
-
##
|
|
27
|
+
## Getting started
|
|
28
|
+
|
|
29
|
+
### Quick start
|
|
26
30
|
|
|
27
31
|
```ts
|
|
28
32
|
import { apiContract } from '@h-ai/api-contract'
|
|
@@ -64,7 +68,7 @@ serv.listen(app, {
|
|
|
64
68
|
// 5) 优雅关闭由 serv 托管,无需手动注册 process.once('SIGINT', ...)
|
|
65
69
|
```
|
|
66
70
|
|
|
67
|
-
|
|
71
|
+
#### host / port configuration
|
|
68
72
|
|
|
69
73
|
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
74
|
|
|
@@ -77,7 +81,7 @@ server:
|
|
|
77
81
|
|
|
78
82
|
Pass the resolved `server` to `serv.listen`; explicit `host` / `port` arguments can also override it.
|
|
79
83
|
|
|
80
|
-
|
|
84
|
+
#### Fetch Runtime(Workers / Bun / Deno)
|
|
81
85
|
|
|
82
86
|
```ts
|
|
83
87
|
const handler = serv.toFetch(app)
|
|
@@ -86,7 +90,7 @@ export default { fetch: handler }
|
|
|
86
90
|
|
|
87
91
|
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
92
|
|
|
89
|
-
|
|
93
|
+
#### Audio WebSocket: tickets and server authorization
|
|
90
94
|
|
|
91
95
|
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
96
|
|
|
@@ -112,7 +116,7 @@ const app = serv.createApp({
|
|
|
112
116
|
- `onSessionEnd` runs at most once after success, failure, or disconnection to release concurrency slots.
|
|
113
117
|
- Browser `createAIClient({ audio: { 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
118
|
|
|
115
|
-
|
|
119
|
+
#### Transport encryption (shared with `@h-ai/crypto`)
|
|
116
120
|
|
|
117
121
|
serv does not expose its own encryption factory; the server manager comes from `crypto.transport`. Applications only pass the top-level `crypto` handle:
|
|
118
122
|
|
|
@@ -204,7 +208,7 @@ const app = serv.createApp({
|
|
|
204
208
|
|
|
205
209
|
> **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
210
|
|
|
207
|
-
|
|
211
|
+
#### Token authentication (`iam` / `refreshCookie`)
|
|
208
212
|
|
|
209
213
|
Pass the top-level `iam` handle to `serv.createApp` to derive:
|
|
210
214
|
|
|
@@ -246,7 +250,7 @@ const app = serv.createApp({
|
|
|
246
250
|
})
|
|
247
251
|
```
|
|
248
252
|
|
|
249
|
-
|
|
253
|
+
#### Custom procedures (authentication + permissions)
|
|
250
254
|
|
|
251
255
|
```ts
|
|
252
256
|
const procedures = serv
|
|
@@ -260,7 +264,7 @@ const procedures = serv
|
|
|
260
264
|
.build()
|
|
261
265
|
```
|
|
262
266
|
|
|
263
|
-
|
|
267
|
+
#### Advanced pipelines (HTTP middleware + context + route guards)
|
|
264
268
|
|
|
265
269
|
`@h-ai/serv` pipelines have three layers and extension points:
|
|
266
270
|
|
|
@@ -268,7 +272,7 @@ const procedures = serv
|
|
|
268
272
|
2. **Context**: customize through `verifyToken` / `createContext` / `serv.buildAuthContextFactory()`.
|
|
269
273
|
3. **Route guards**: declare authentication/authorization with `.auth()` / `.permission()` / `.role()`.
|
|
270
274
|
|
|
271
|
-
|
|
275
|
+
##### 1) Custom HTTP middleware
|
|
272
276
|
|
|
273
277
|
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
278
|
|
|
@@ -336,7 +340,7 @@ Notes:
|
|
|
336
340
|
- Expose custom response headers such as `X-Encrypted` explicitly through `serv.cors({ exposedHeaders: [...] })` for browsers.
|
|
337
341
|
- This layer works with HTTP middleware context and returns **HTTP Response**, not `HaiResult`.
|
|
338
342
|
|
|
339
|
-
|
|
343
|
+
##### 2) Custom context pipelines
|
|
340
344
|
|
|
341
345
|
To reuse `Bearer token -> session` and add tenant, workspace, or source-system fields,
|
|
342
346
|
prefer `serv.buildAuthContextFactory()` to assemble a custom `createContext`:
|
|
@@ -363,7 +367,7 @@ Choose the appropriate extension:
|
|
|
363
367
|
- Add fields to default authentication context: use `buildAuthContextFactory(...) + createContext`.
|
|
364
368
|
- Replace context construction entirely: use `createContext` directly.
|
|
365
369
|
|
|
366
|
-
|
|
370
|
+
##### 3) Route guard
|
|
367
371
|
|
|
368
372
|
Public procedures use `.route(path, handler)`. For guarded routes, use
|
|
369
373
|
`.route(path).auth/permission/role().handle(handler)`:
|
|
@@ -396,7 +400,7 @@ Public API boundaries:
|
|
|
396
400
|
- The stable application-facing entry remains `@h-ai/serv`.
|
|
397
401
|
- Internal helpers such as `securityHeaders` / `requireInternalRPC` / `buildHaiErrorBody` are not documented as separate subpath exports.
|
|
398
402
|
|
|
399
|
-
|
|
403
|
+
#### httpOnly cookie authentication (recommended for production)
|
|
400
404
|
|
|
401
405
|
httpOnly cookie mode stores refresh tokens in server-managed cookies unreadable to browser JavaScript, reducing XSS exposure.
|
|
402
406
|
|
|
@@ -455,7 +459,7 @@ await apiClient.init({
|
|
|
455
459
|
|
|
456
460
|
> **Cookie attributes**: `HttpOnly; SameSite=Strict; Secure` in production; `Path` is limited to `{apiPrefix}/auth/refresh`.
|
|
457
461
|
|
|
458
|
-
|
|
462
|
+
### API overview
|
|
459
463
|
|
|
460
464
|
- `serv.createApp(options)`: create the HTTP App with custom middleware, health checks, OpenAPI, optional docs, and RPC.
|
|
461
465
|
- `serv.parseRequestContext({ request })`: extract Bearer token + requestId without populating session.
|
|
@@ -482,7 +486,7 @@ Additional root types / named exports:
|
|
|
482
486
|
|
|
483
487
|
> Transport encryption is a `createApp` option delegated internally to `crypto.transport`, not a flat `serv.xxx` API.
|
|
484
488
|
|
|
485
|
-
|
|
489
|
+
### Configuration
|
|
486
490
|
|
|
487
491
|
`@h-ai/serv` does not scan YAML itself. Load `config/_serv.yml` through `@h-ai/core` and pass the result to `serv.createApp()`.
|
|
488
492
|
|
|
@@ -597,7 +601,7 @@ const app = serv.createApp({
|
|
|
597
601
|
|
|
598
602
|
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
603
|
|
|
600
|
-
|
|
604
|
+
### Configuration driven authentication
|
|
601
605
|
|
|
602
606
|
Agreements support configuration-loaded userAgreementMarkdown/privacyPolicyMarkdown and URLs. Markdown takes priority for the same agreement and is safely rendered in a modal.
|
|
603
607
|
|
|
@@ -605,7 +609,7 @@ createIamProcedures({ iam }) implements public config, password/LDAP/OTP login,
|
|
|
605
609
|
|
|
606
610
|
[完整 IAM 配置 / IAM configuration](../iam/README.en.md)
|
|
607
611
|
|
|
608
|
-
|
|
612
|
+
### Error handling
|
|
609
613
|
|
|
610
614
|
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
615
|
|
|
@@ -656,7 +660,7 @@ const procedures = serv
|
|
|
656
660
|
|
|
657
661
|
> 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
662
|
|
|
659
|
-
##
|
|
663
|
+
## Development and testing
|
|
660
664
|
|
|
661
665
|
```bash
|
|
662
666
|
pnpm --filter @h-ai/serv test
|
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 / ...`。
|
|
@@ -22,7 +24,9 @@
|
|
|
22
24
|
|
|
23
25
|
> ライフサイクル:`@h-ai/serv` は状態を持たない HTTP App の構成機能で、`init()` / `close()` は不要です。app 作成前に `iam` / `storage` / `ai` / `crypto` などを初期化し、`serv.listen(..., { onClose })` で逆順に解放します。`serv.createApp()` は transport 管理器の生成失敗など起動設定のエラーを即座に throw します。
|
|
24
26
|
|
|
25
|
-
##
|
|
27
|
+
## 使い始める
|
|
28
|
+
|
|
29
|
+
### クイックスタート
|
|
26
30
|
|
|
27
31
|
```ts
|
|
28
32
|
import { apiContract } from '@h-ai/api-contract'
|
|
@@ -64,7 +68,7 @@ serv.listen(app, {
|
|
|
64
68
|
// 5) 优雅关闭由 serv 托管,无需手动注册 process.once('SIGINT', ...)
|
|
65
69
|
```
|
|
66
70
|
|
|
67
|
-
|
|
71
|
+
#### host / port 設定
|
|
68
72
|
|
|
69
73
|
host/port は `config/_serv.yml` の `server` で指定し、`HOST` / `PORT` 環境変数 > 設定ファイル > `127.0.0.1:3000` の順です。既定の `127.0.0.1` はローカル専用で、コンテナー外に公開する場合は `0.0.0.0` にします:
|
|
70
74
|
|
|
@@ -77,7 +81,7 @@ server:
|
|
|
77
81
|
|
|
78
82
|
解決済みの `server` を `serv.listen` に渡します。`host` / `port` の明示的な指定で上書きすることもできます。
|
|
79
83
|
|
|
80
|
-
|
|
84
|
+
#### Fetch Runtime(Workers / Bun / Deno)
|
|
81
85
|
|
|
82
86
|
```ts
|
|
83
87
|
const handler = serv.toFetch(app)
|
|
@@ -86,7 +90,7 @@ export default { fetch: handler }
|
|
|
86
90
|
|
|
87
91
|
Node `serv.listen()` は socket の接続元を自動で渡し、既定では転送ヘッダーを無視します。`http.trustedProxies` の IP/CIDR のみ信頼し、XFF を右から左へ調べ、最初の非信頼アドレスで停止します。その左側は無視します。信頼チェーンが不正・長すぎる場合は IP 未定義となり、代理をクライアントとして扱いません。代理は転送ヘッダーを清掃・追加してください。Fetch は `app.fetch(request, { remoteAddress: platformPeerAddress })` で信頼できる接続元を渡します。メタデータなしは IP 未定義です。任意のヘッダーをコピーしないでください。独自 `createContext(input)` は input 全体を再利用し、解決済み `input.ip` を保持します。内部 RPC は接続元で制限します。
|
|
88
92
|
|
|
89
|
-
|
|
93
|
+
#### Audio WebSocket:ticket とサーバー認可
|
|
90
94
|
|
|
91
95
|
音声接続には短期・単回 ticket が必要です。認証済み HTTP endpoint で発行(推奨:`iam.ticket.issue`)し、`verifyTicket` でアトミックに消費します(推奨:`iam.ticket.consume`)。接続直後に独立 timeout で認証し、認証前に業務フレームを処理しません。返された `ServSession` と任意の `grant` を保存してから `authorize` で有料パラメーターを確認します:
|
|
92
96
|
|
|
@@ -112,7 +116,7 @@ const app = serv.createApp({
|
|
|
112
116
|
- `onSessionEnd` は成功・失敗・切断後に最大 1 回呼び、同時実行枠の解放に使用。
|
|
113
117
|
- ブラウザーの `createAIClient({ audio: { url, getTicket } })` は単回 `ticket` のみを URL に付けます。`getTicket(request)` は `operation` / `model` の概要から今回の操作専用 ticket を発行できます。通常の IAM access token は渡さないでください。
|
|
114
118
|
|
|
115
|
-
|
|
119
|
+
#### 通信暗号化(`@h-ai/crypto` と共通)
|
|
116
120
|
|
|
117
121
|
serv 独自の暗号化ファクトリーはなく、`crypto.transport` から管理器を作成します。利用側はトップレベルの `crypto` のみを渡します:
|
|
118
122
|
|
|
@@ -204,7 +208,7 @@ const app = serv.createApp({
|
|
|
204
208
|
|
|
205
209
|
> **安全方針**:`transport` 有効時は `excludePaths` と鍵交換以外で有効な `X-Client-Id` が必要です。不足・未知の clientId は 400、暗号化不能・非 JSON 応答・上限超過はエラーになり、平文の業務データは渡しません。
|
|
206
210
|
|
|
207
|
-
|
|
211
|
+
#### Token 認証(`iam` / `refreshCookie`)
|
|
208
212
|
|
|
209
213
|
`serv.createApp` にトップレベルの `iam` を渡すと、次を自動構成します:
|
|
210
214
|
|
|
@@ -246,7 +250,7 @@ const app = serv.createApp({
|
|
|
246
250
|
})
|
|
247
251
|
```
|
|
248
252
|
|
|
249
|
-
|
|
253
|
+
#### 独自 procedure(認証 + 権限)
|
|
250
254
|
|
|
251
255
|
```ts
|
|
252
256
|
const procedures = serv
|
|
@@ -260,7 +264,7 @@ const procedures = serv
|
|
|
260
264
|
.build()
|
|
261
265
|
```
|
|
262
266
|
|
|
263
|
-
|
|
267
|
+
#### 高度なパイプライン(HTTP middleware + context + route guard)
|
|
264
268
|
|
|
265
269
|
`@h-ai/serv` のパイプラインは 3 層で、それぞれの拡張入口があります:
|
|
266
270
|
|
|
@@ -268,7 +272,7 @@ const procedures = serv
|
|
|
268
272
|
2. **context 層**:`verifyToken` / `createContext` / `serv.buildAuthContextFactory()` で拡張。
|
|
269
273
|
3. **route guard 層**:`.auth()` / `.permission()` / `.role()` で認証・認可を宣言。
|
|
270
274
|
|
|
271
|
-
|
|
275
|
+
##### 1) 独自 HTTP middleware
|
|
272
276
|
|
|
273
277
|
ログ・trace・レート制限・CORS・テナントヘッダーなどの **HTTP 共通処理**、または JSON/oRPC contract に適さない WebSocket・ファイル・バイナリー endpoint には `middlewares` を使います。一般的な CORS は `serv.cors(...)` を再利用できます:
|
|
274
278
|
|
|
@@ -336,7 +340,7 @@ const app = serv.createApp({
|
|
|
336
340
|
- ブラウザーが `X-Encrypted` などを読む場合は `serv.cors({ exposedHeaders: [...] })` で明示的に公開。
|
|
337
341
|
- この層は HTTP middleware context を操作し、`HaiResult` ではなく **HTTP Response** を返します。
|
|
338
342
|
|
|
339
|
-
|
|
343
|
+
##### 2) 独自 context パイプライン
|
|
340
344
|
|
|
341
345
|
`Bearer token -> session` を再利用し、テナント・workspace・送信元などの項目を追加する場合は、
|
|
342
346
|
`serv.buildAuthContextFactory()` で独自の `createContext` を構成してください:
|
|
@@ -363,7 +367,7 @@ const app = serv.createApp({
|
|
|
363
367
|
- 既定の認証 context に項目を追加:`buildAuthContextFactory(...) + createContext`。
|
|
364
368
|
- context 生成全体を置換:直接 `createContext`。
|
|
365
369
|
|
|
366
|
-
|
|
370
|
+
##### 3) Route guard
|
|
367
371
|
|
|
368
372
|
公開 procedure は `.route(path, handler)` を使い、ガードが必要な場合は次を使います:
|
|
369
373
|
`.route(path).auth/permission/role().handle(handler)`:
|
|
@@ -396,7 +400,7 @@ const procedures = serv
|
|
|
396
400
|
- アプリ向けの安定した入口はルートの `@h-ai/serv`。
|
|
397
401
|
- `securityHeaders` / `requireInternalRPC` / `buildHaiErrorBody` などの内部 helper は独立サブパスとして公開しません。
|
|
398
402
|
|
|
399
|
-
|
|
403
|
+
#### httpOnly Cookie 認証(本番推奨)
|
|
400
404
|
|
|
401
405
|
httpOnly cookie モードは refresh token をサーバー管理の Cookie に保存し、ブラウザー JS から読めなくして XSS による漏洩を抑えます。
|
|
402
406
|
|
|
@@ -455,7 +459,7 @@ await apiClient.init({
|
|
|
455
459
|
|
|
456
460
|
> **Cookie 属性**:本番は `HttpOnly; SameSite=Strict; Secure`。`Path` は `{apiPrefix}/auth/refresh` に限定。
|
|
457
461
|
|
|
458
|
-
|
|
462
|
+
### API 一覧
|
|
459
463
|
|
|
460
464
|
- `serv.createApp(options)`:独自 middleware、ヘルスチェック、OpenAPI、任意の文書・RPC を備えた HTTP App を作成。
|
|
461
465
|
- `serv.parseRequestContext({ request })`:Bearer token + requestId を抽出し、session は設定しません。
|
|
@@ -482,7 +486,7 @@ await apiClient.init({
|
|
|
482
486
|
|
|
483
487
|
> 通信暗号化は `createApp` の設定機能で、内部で `crypto.transport` に委譲します。`serv.xxx` の独立 API ではありません。
|
|
484
488
|
|
|
485
|
-
|
|
489
|
+
### 設定
|
|
486
490
|
|
|
487
491
|
`@h-ai/serv` 自体は YAML を走査しません。`@h-ai/core` で `config/_serv.yml` を読み、解析結果を `serv.createApp()` に渡してください。
|
|
488
492
|
|
|
@@ -597,7 +601,7 @@ const app = serv.createApp({
|
|
|
597
601
|
|
|
598
602
|
本番では `_serv.yml` の `cors.origin` が空・`*` なら起動に失敗します。Web・Capacitor/Tauri は正規化済み Origin 全体を厳密照合し、末尾偽装を防ぎます。本番は Secure refresh cookie を強制し OpenAPI/docs を無効化します。非本番の endpoint は設定ファイルで決めます。
|
|
599
603
|
|
|
600
|
-
|
|
604
|
+
### 設定による認証画面
|
|
601
605
|
|
|
602
606
|
規約は設定の userAgreementMarkdown/privacyPolicyMarkdown または URL を使用できます。同じ規約は Markdown 本文を優先して安全な Modal で描画します。
|
|
603
607
|
|
|
@@ -605,7 +609,7 @@ createIamProcedures({ iam }) は公開設定、LDAP/OTP/パスワード、登録
|
|
|
605
609
|
|
|
606
610
|
[完整 IAM 配置 / IAM configuration](../iam/README.ja.md)
|
|
607
611
|
|
|
608
|
-
|
|
612
|
+
### エラー処理
|
|
609
613
|
|
|
610
614
|
通常の procedure は認証・認可・業務の失敗と捕捉した handler 例外を HaiResult として既定で HTTP 200 で返します。`error.httpStatus` はドメインエラーのメタデータで、HTTP 応答ステータスを変更しません。クライアントは `success` と `error.code` を確認してください。未認証では `error.httpStatus=401`、DB クエリー失敗や INTERNAL_ERROR では 500 です。
|
|
611
615
|
|
|
@@ -656,7 +660,7 @@ const procedures = serv
|
|
|
656
660
|
|
|
657
661
|
> `iam.user.xxx()` など下流が直接 `HaiResult` を返す場合、serv は `error.message` をそのまま渡します。`HaiError` は最終 `message` のみで `messageKey/params` を持たないため、**serv の境界で任意の下流エラーを汎用的に再翻訳することはできません**。リクエストごとの i18n はエラーを生成するモジュールで locale を受け取る必要があります。
|
|
658
662
|
|
|
659
|
-
##
|
|
663
|
+
## 開発とテスト
|
|
660
664
|
|
|
661
665
|
```bash
|
|
662
666
|
pnpm --filter @h-ai/serv test
|
|
@@ -664,6 +668,6 @@ pnpm --filter @h-ai/serv typecheck
|
|
|
664
668
|
pnpm --filter @h-ai/serv lint
|
|
665
669
|
```
|
|
666
670
|
|
|
667
|
-
##
|
|
671
|
+
## ライセンス
|
|
668
672
|
|
|
669
673
|
Apache-2.0
|
package/README.md
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
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
|
-
|
|
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 })`,应用代码不需要 import Hono
|
|
12
14
|
- 扁平 API(最小知识):`serv.createApp / serv.implement / serv.listen / serv.toFetch / serv.generateSpec / ...`
|
|
@@ -22,7 +24,9 @@
|
|
|
22
24
|
|
|
23
25
|
> 生命周期说明:`@h-ai/serv` 本身是无状态 HTTP App 装配器,不需要 `init()` / `close()`;请在创建 app 前初始化 `iam` / `storage` / `ai` / `crypto` 等依赖,并在 `serv.listen(..., { onClose })` 中反向释放它们。`serv.createApp()` 遇到启动期配置错误(例如 transport manager 创建失败)会 fail-fast 抛出,便于进程启动阶段暴露问题。
|
|
24
26
|
|
|
25
|
-
##
|
|
27
|
+
## 使用入门
|
|
28
|
+
|
|
29
|
+
### 快速开始
|
|
26
30
|
|
|
27
31
|
```ts
|
|
28
32
|
import { apiContract } from '@h-ai/api-contract'
|
|
@@ -64,7 +68,7 @@ serv.listen(app, {
|
|
|
64
68
|
// 5) 优雅关闭由 serv 托管,无需手动注册 process.once('SIGINT', ...)
|
|
65
69
|
```
|
|
66
70
|
|
|
67
|
-
|
|
71
|
+
#### host / port 配置
|
|
68
72
|
|
|
69
73
|
host/port 统一由 `config/_serv.yml` 的 `server` 提供,优先级:`HOST` / `PORT` 环境变量 > 配置文件 > 默认 `127.0.0.1:3000`。默认 `127.0.0.1` 仅本机可达,容器对外暴露时改为 `0.0.0.0`:
|
|
70
74
|
|
|
@@ -77,7 +81,7 @@ server:
|
|
|
77
81
|
|
|
78
82
|
应用把解析后的 `server` 传给 `serv.listen`(`serv.listen` 也可直接传 `host` / `port` 显式覆盖)。
|
|
79
83
|
|
|
80
|
-
|
|
84
|
+
#### Fetch Runtime(Workers / Bun / Deno)
|
|
81
85
|
|
|
82
86
|
```ts
|
|
83
87
|
const handler = serv.toFetch(app)
|
|
@@ -86,7 +90,7 @@ export default { fetch: handler }
|
|
|
86
90
|
|
|
87
91
|
Node `serv.listen()` 自动提供 socket 对端地址;默认忽略 `X-Forwarded-For` / `X-Real-IP`。只有 `http.trustedProxies` 中列出的代理 IP/CIDR 才能提供转发头,XFF 从右向左停在首个不可信地址,忽略其左侧内容;可信链无法解析或过长时 IP 为 undefined,不能回退为代理身份。代理必须清洗或追加转发头。Fetch 平台通过 `app.fetch(request, { remoteAddress: platformPeerAddress })` 显式提供可信对端,缺失时 IP 为 undefined,不能从任意请求头复制。自定义 `createContext(input)` 复用完整 input,保留框架已解析的 `input.ip`。内部 RPC 仍按连接对端限制。
|
|
88
92
|
|
|
89
|
-
|
|
93
|
+
#### Audio WebSocket:ticket 与服务端授权
|
|
90
94
|
|
|
91
95
|
语音入口必须使用短期、一次性 ticket。应用通过已登录 HTTP endpoint 签发 ticket(推荐 `iam.ticket.issue`),并在 `verifyTicket` 中原子消费(推荐 `iam.ticket.consume`);连接建立即完成鉴权(独立预鉴权超时),未鉴权不处理任何业务帧。框架保存返回的 `ServSession` 与可选 `grant`,再调用 `authorize` 确认付费参数:
|
|
92
96
|
|
|
@@ -112,7 +116,7 @@ const app = serv.createApp({
|
|
|
112
116
|
- `onSessionEnd` 在成功、失败或断连后至多调用一次,供应用释放并发占用。
|
|
113
117
|
- 浏览器 `createAIClient({ audio: { url, getTicket } })` 只把一次性 `ticket` 放入 WebSocket URL;`getTicket(request)` 接收本次操作摘要(`operation` / `model`),可签发与本次操作严格绑定的票据;禁止传普通 IAM access token。
|
|
114
118
|
|
|
115
|
-
|
|
119
|
+
#### 传输加密(与 `@h-ai/crypto` 统一)
|
|
116
120
|
|
|
117
121
|
serv 不暴露本地传输加密工厂;统一从 `crypto.transport` 创建服务端管理器。使用方只需传入顶层 `crypto` 句柄:
|
|
118
122
|
|
|
@@ -204,7 +208,7 @@ const app = serv.createApp({
|
|
|
204
208
|
|
|
205
209
|
> **安全策略**:启用 `transport` 后,除 `excludePaths` 与密钥协商端点外,业务请求必须携带有效 `X-Client-Id`。缺失或未知 clientId 会返回 400;响应体无法加密、非 JSON 业务响应或超过单次加密上限时返回错误,不会明文透传业务数据。
|
|
206
210
|
|
|
207
|
-
|
|
211
|
+
#### Token 认证(`iam` / `refreshCookie`)
|
|
208
212
|
|
|
209
213
|
`serv.createApp` 推荐直接传入顶层 `iam` 句柄,自动派生两件事:
|
|
210
214
|
|
|
@@ -246,7 +250,7 @@ const app = serv.createApp({
|
|
|
246
250
|
})
|
|
247
251
|
```
|
|
248
252
|
|
|
249
|
-
|
|
253
|
+
#### 自定义 procedure(认证 + 权限)
|
|
250
254
|
|
|
251
255
|
```ts
|
|
252
256
|
const procedures = serv
|
|
@@ -260,7 +264,7 @@ const procedures = serv
|
|
|
260
264
|
.build()
|
|
261
265
|
```
|
|
262
266
|
|
|
263
|
-
|
|
267
|
+
#### 高级:自定义 pipeline(HTTP middleware + context + route guard)
|
|
264
268
|
|
|
265
269
|
`@h-ai/serv` 的“pipeline”分三层,分别对应三种扩展方式:
|
|
266
270
|
|
|
@@ -268,7 +272,7 @@ const procedures = serv
|
|
|
268
272
|
2. **context 层**:通过 `verifyToken` / `createContext` / `serv.buildAuthContextFactory()` 定制请求上下文。
|
|
269
273
|
3. **route guard 层**:通过 `.auth()` / `.permission()` / `.role()` 声明认证授权。
|
|
270
274
|
|
|
271
|
-
|
|
275
|
+
##### 1) 自定义 HTTP middleware
|
|
272
276
|
|
|
273
277
|
当你需要加请求日志、trace、限流、CORS、租户头校验等 **HTTP 层横切逻辑**,或挂载不适合进入 JSON/oRPC contract 的 WebSocket、文件和二进制端点时,使用 `middlewares`。常见 CORS 场景可直接复用 `serv.cors(...)`:
|
|
274
278
|
|
|
@@ -336,7 +340,7 @@ const app = serv.createApp({
|
|
|
336
340
|
- 浏览器若需读取自定义响应头(例如 transport 的 `X-Encrypted`),请通过 `serv.cors({ exposedHeaders: [...] })` 显式暴露
|
|
337
341
|
- 这一层直接操作 HTTP middleware context,返回的是 **HTTP Response**,不是 `HaiResult`
|
|
338
342
|
|
|
339
|
-
|
|
343
|
+
##### 2) 自定义 context pipeline
|
|
340
344
|
|
|
341
345
|
如果你只想复用 `Bearer token -> session` 这一段逻辑,再额外补充租户、工作区、来源系统等字段,
|
|
342
346
|
优先用 `serv.buildAuthContextFactory()` 组装自定义 `createContext`:
|
|
@@ -363,7 +367,7 @@ const app = serv.createApp({
|
|
|
363
367
|
- 想在默认认证上下文之上追加字段:用 `buildAuthContextFactory(...) + createContext`
|
|
364
368
|
- 想完全接管上下文构造:直接用 `createContext`
|
|
365
369
|
|
|
366
|
-
|
|
370
|
+
##### 3) Route guard
|
|
367
371
|
|
|
368
372
|
公开 procedure 使用 `.route(path, handler)`;需要 guard 时使用
|
|
369
373
|
`.route(path).auth/permission/role().handle(handler)`:
|
|
@@ -396,7 +400,7 @@ const procedures = serv
|
|
|
396
400
|
- 对应用层公开的稳定入口仍然是根入口 `@h-ai/serv`
|
|
397
401
|
- `securityHeaders` / `requireInternalRPC` / `buildHaiErrorBody` 等内部 helper/middleware 不作为单独子路径文档暴露
|
|
398
402
|
|
|
399
|
-
|
|
403
|
+
#### httpOnly Cookie 认证(推荐生产方案)
|
|
400
404
|
|
|
401
405
|
httpOnly cookie 模式将 refresh token 存储在服务端管理的 cookie 中(浏览器 JS 无法读取),避免 XSS 风险。
|
|
402
406
|
|
|
@@ -455,7 +459,7 @@ await apiClient.init({
|
|
|
455
459
|
|
|
456
460
|
> **Cookie 规格**:`HttpOnly; SameSite=Strict; Secure`(生产);`Path` 限制为 `{apiPrefix}/auth/refresh`。
|
|
457
461
|
|
|
458
|
-
|
|
462
|
+
### API 概览
|
|
459
463
|
|
|
460
464
|
- `serv.createApp(options)`:创建 HTTP App 抽象,挂载自定义 `middlewares`、健康检查、OpenAPI handler、可选文档与 RPC
|
|
461
465
|
- `serv.parseRequestContext({ request })`:默认上下文解析(提取 Bearer token + requestId,不填充 session)
|
|
@@ -482,7 +486,7 @@ await apiClient.init({
|
|
|
482
486
|
|
|
483
487
|
> 传输加密不作为 `serv.xxx` 扁平 API 暴露;它是 `createApp` 的配置能力,内部委托 `crypto.transport`。
|
|
484
488
|
|
|
485
|
-
|
|
489
|
+
### 配置
|
|
486
490
|
|
|
487
491
|
`@h-ai/serv` 本身不主动扫描 YAML;推荐由 `@h-ai/core` 统一加载 `config/_serv.yml`,再把解析结果传给 `serv.createApp()`。
|
|
488
492
|
|
|
@@ -597,7 +601,7 @@ const app = serv.createApp({
|
|
|
597
601
|
|
|
598
602
|
生产环境下 `_serv.yml` 的 `cors.origin` 为空或为 `*` 会在启动时失败;Web 与 Capacitor/Tauri Origin 均按规范化后的完整 Origin 精确匹配,避免后缀伪造。生产环境同时强制 Secure refresh cookie 并关闭 OpenAPI/docs,配置文件仍决定非生产环境具体挂载哪些端点。
|
|
599
603
|
|
|
600
|
-
|
|
604
|
+
### 配置驱动认证页面
|
|
601
605
|
|
|
602
606
|
协议支持配置中的 userAgreementMarkdown/privacyPolicyMarkdown 正文及 URL;同主题优先 Markdown,由安全渲染器在弹窗显示。
|
|
603
607
|
|
|
@@ -605,7 +609,7 @@ const app = serv.createApp({
|
|
|
605
609
|
|
|
606
610
|
[完整 IAM 配置 / IAM configuration](../iam/README.md)
|
|
607
611
|
|
|
608
|
-
|
|
612
|
+
### 错误处理
|
|
609
613
|
|
|
610
614
|
普通 procedure 的认证、授权、业务失败和被捕获的 handler 异常都作为 `HaiResult` 输出,默认通过 HTTP 200 返回;`error.httpStatus` 是领域错误元数据,不会自动改写响应状态。客户端必须检查 `success` 与 `error.code`。例如未登录为 `error.httpStatus=401`,数据库查询失败或 INTERNAL_ERROR 为 500。
|
|
611
615
|
|
|
@@ -656,7 +660,7 @@ const procedures = serv
|
|
|
656
660
|
|
|
657
661
|
> 如果错误来自下游业务模块(如 `iam.user.xxx()`)且该模块已经直接返回 `HaiResult`,serv 会原样透传它的 `error.message`。当前 `HaiError` 只携带最终 `message`,不携带 `messageKey/params`,因此 **serv 边界无法对任意下游错误再做一次通用 i18n 重翻译**;要支持请求级 i18n,必须在创建该错误消息的模块里就拿到 locale。
|
|
658
662
|
|
|
659
|
-
##
|
|
663
|
+
## 开发测试
|
|
660
664
|
|
|
661
665
|
```bash
|
|
662
666
|
pnpm --filter @h-ai/serv test
|
|
@@ -664,6 +668,6 @@ pnpm --filter @h-ai/serv typecheck
|
|
|
664
668
|
pnpm --filter @h-ai/serv lint
|
|
665
669
|
```
|
|
666
670
|
|
|
667
|
-
##
|
|
671
|
+
## 许可证
|
|
668
672
|
|
|
669
673
|
Apache-2.0
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@h-ai/serv",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.0-alpha.
|
|
4
|
+
"version": "0.1.0-alpha.67",
|
|
5
5
|
"description": "Hai Framework service runtime powered by Hono and oRPC.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"exports": {
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@h-ai/ai": "0.1.0-alpha.
|
|
39
|
-
"@h-ai/iam": "0.1.0-alpha.
|
|
40
|
-
"@h-ai/storage": "0.1.0-alpha.
|
|
38
|
+
"@h-ai/ai": "0.1.0-alpha.67",
|
|
39
|
+
"@h-ai/iam": "0.1.0-alpha.67",
|
|
40
|
+
"@h-ai/storage": "0.1.0-alpha.67"
|
|
41
41
|
},
|
|
42
42
|
"peerDependenciesMeta": {
|
|
43
43
|
"@h-ai/ai": {
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
}
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@h-ai/api-contract": "0.1.0-alpha.
|
|
55
|
-
"@h-ai/core": "0.1.0-alpha.
|
|
56
|
-
"@h-ai/crypto": "0.1.0-alpha.
|
|
54
|
+
"@h-ai/api-contract": "0.1.0-alpha.67",
|
|
55
|
+
"@h-ai/core": "0.1.0-alpha.67",
|
|
56
|
+
"@h-ai/crypto": "0.1.0-alpha.67",
|
|
57
57
|
"@hono/node-server": "^2.1.4",
|
|
58
58
|
"@orpc/openapi": "^1.15.5",
|
|
59
59
|
"@orpc/server": "^1.15.5",
|
|
@@ -64,9 +64,9 @@
|
|
|
64
64
|
"zod": "^4.6.5"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
|
-
"@h-ai/ai": "0.1.0-alpha.
|
|
68
|
-
"@h-ai/iam": "0.1.0-alpha.
|
|
69
|
-
"@h-ai/storage": "0.1.0-alpha.
|
|
67
|
+
"@h-ai/ai": "0.1.0-alpha.67",
|
|
68
|
+
"@h-ai/iam": "0.1.0-alpha.67",
|
|
69
|
+
"@h-ai/storage": "0.1.0-alpha.67",
|
|
70
70
|
"@types/node": "^26.6.5",
|
|
71
71
|
"@types/ws": "^8.18.2",
|
|
72
72
|
"rimraf": "^6.1.3",
|