@h-ai/serv 1.0.0-beta.18 → 1.0.0-beta.19
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 +36 -19
- package/README.ja.md +36 -19
- package/README.md +36 -19
- package/dist/{chunk-IQZQTAEW.js → chunk-CZ6VA2EN.js} +3 -3
- package/dist/{chunk-IQZQTAEW.js.map → chunk-CZ6VA2EN.js.map} +1 -1
- package/dist/{chunk-FEG22LCX.js → chunk-PLGCHDZM.js} +24 -10
- package/dist/chunk-PLGCHDZM.js.map +1 -0
- package/dist/features/ai.d.ts +5 -4
- package/dist/features/ai.js +34 -13
- package/dist/features/ai.js.map +1 -1
- package/dist/features/feedback.d.ts +20 -19
- package/dist/features/feedback.js +1 -1
- package/dist/features/feedback.js.map +1 -1
- package/dist/features/iam.d.ts +4 -3
- package/dist/features/iam.js +4 -2
- package/dist/features/iam.js.map +1 -1
- package/dist/features/storage.d.ts +4 -3
- package/dist/features/storage.js +34 -9
- package/dist/features/storage.js.map +1 -1
- package/dist/index.d.ts +75 -26
- package/dist/index.js +248 -61
- package/dist/index.js.map +1 -1
- package/dist/{serv-context-BsNvQ-bb.d.ts → serv-context-DSsS_ksL.d.ts} +1 -56
- package/dist/{serv-router-BnkxPvS6.d.ts → serv-router-DWjYC0zD.d.ts} +2 -18
- package/package.json +29 -29
- package/dist/chunk-FEG22LCX.js.map +0 -1
package/README.en.md
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
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
10
|
|
|
11
|
+
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
|
+
|
|
11
13
|
## Core features
|
|
12
14
|
|
|
13
15
|
- Start with `serv.createApp(...)` → `serv.listen(app, { port })`; applications need not import Hono.
|
|
@@ -22,7 +24,9 @@ hai-framework's API service runtime: a stable public HTTP App abstraction that m
|
|
|
22
24
|
- Optional transport encryption: `serv.createApp({ transport: { crypto, serverKeyRing, keyStore } })` mounts key exchange and request/response encryption automatically.
|
|
23
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.
|
|
24
26
|
|
|
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
|
|
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.
|
|
26
30
|
|
|
27
31
|
## Getting started
|
|
28
32
|
|
|
@@ -53,6 +57,7 @@ const procedures = {
|
|
|
53
57
|
const app = serv.createApp({
|
|
54
58
|
contract,
|
|
55
59
|
procedures,
|
|
60
|
+
iam,
|
|
56
61
|
http: {
|
|
57
62
|
apiPrefix: '/api/v1',
|
|
58
63
|
openapi: { path: '/openapi.json' },
|
|
@@ -199,14 +204,16 @@ const app = serv.createApp({
|
|
|
199
204
|
contract,
|
|
200
205
|
procedures,
|
|
201
206
|
http: servConfig.http,
|
|
202
|
-
middlewares: [
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
207
|
+
middlewares: [
|
|
208
|
+
{
|
|
209
|
+
middleware: serv.cors({
|
|
210
|
+
origin: servConfig.cors.origin,
|
|
211
|
+
credentials: servConfig.cors.credentials,
|
|
212
|
+
allowedHeaders: [...servConfig.cors.allowedHeaders],
|
|
213
|
+
exposedHeaders: [...servConfig.cors.exposedHeaders],
|
|
214
|
+
}),
|
|
215
|
+
}
|
|
216
|
+
],
|
|
210
217
|
transport: servConfig.transport === false
|
|
211
218
|
? undefined
|
|
212
219
|
: {
|
|
@@ -419,6 +426,7 @@ Constraints:
|
|
|
419
426
|
|
|
420
427
|
- `.permission()` and `.role()` require authentication automatically. Failures return HaiResult with `error.httpStatus` 401/403; ordinary procedure responses remain HTTP 200 by default.
|
|
421
428
|
- Multiple permissions/roles on one route must all pass. `serv.WILDCARD_PERMISSION` / `serv.WILDCARD_ROLE` allow their respective categories.
|
|
429
|
+
- Permissions also support the IAM/Kit colon-prefix rule: `users:*` matches `users:read`, but not `users2:read` or dot-separated permissions.
|
|
422
430
|
- Unhandled handler exceptions become INTERNAL_ERROR HaiResult values; expected business failures should return `err(...)` directly.
|
|
423
431
|
- Paths, input, and output come from the contract. Duplicate/unknown paths are rejected; `.build()` requires complete contract implementation.
|
|
424
432
|
|
|
@@ -487,11 +495,14 @@ await apiClient.init({
|
|
|
487
495
|
|
|
488
496
|
> Refresh cookies use `HttpOnly` and `SameSite=Strict` and are scoped to `{apiPrefix}/auth/refresh`. Production adds `Secure` by default.
|
|
489
497
|
|
|
498
|
+
Refresh failures use the public error boundary too: 5xx and missing or invalid statuses return a safe message and traceId, while internal causes stay in server logs. Only a 401 authentication failure clears the refresh cookie; rate limits and backend failures preserve credentials for retry. Automatic auditing uses the same normalized failure status.
|
|
499
|
+
|
|
490
500
|
### API overview
|
|
491
501
|
|
|
492
502
|
- `serv.createApp(options)`: create the HTTP App with custom middleware, health checks, OpenAPI, optional docs.
|
|
493
503
|
- `serv.parseRequestContext({ request })`: extract Bearer token + requestId without populating session.
|
|
494
504
|
- `serv.listen(app, options)`: start Node.js HTTP service; return `{ server, address, close }`.
|
|
505
|
+
The returned `close()` is idempotent: it stops accepting connections, closes Audio WebSockets, terminates uncooperative audio clients after one second, and waits for HTTP shutdown. Manual close also removes process signal listeners; the host still releases business modules in reverse order.
|
|
495
506
|
- `serv.toFetch(app)`: wrap as a standard `fetch(Request)` handler.
|
|
496
507
|
- `serv.generateSpec(contract, options)`: generate an OpenAPI 3.1 spec from the contract.
|
|
497
508
|
- `serv.storageAssets(config)`: read-only storage middleware with key/MIME allowlists, GET/HEAD, and ETags.
|
|
@@ -626,15 +637,18 @@ const app = serv.createApp({
|
|
|
626
637
|
openapi: security.exposeApiDocs ? servConfig.http.openapi : false,
|
|
627
638
|
docs: security.exposeApiDocs ? servConfig.http.docs : false,
|
|
628
639
|
},
|
|
640
|
+
iam,
|
|
629
641
|
refreshCookie: { secure: security.secureRefreshCookie },
|
|
630
|
-
middlewares: [
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
642
|
+
middlewares: [
|
|
643
|
+
{
|
|
644
|
+
middleware: serv.cors({
|
|
645
|
+
origin: security.allowOrigin,
|
|
646
|
+
credentials: servConfig.cors.credentials,
|
|
647
|
+
allowedHeaders: [...servConfig.cors.allowedHeaders],
|
|
648
|
+
exposedHeaders: [...servConfig.cors.exposedHeaders],
|
|
649
|
+
}),
|
|
650
|
+
}
|
|
651
|
+
],
|
|
638
652
|
})
|
|
639
653
|
```
|
|
640
654
|
|
|
@@ -699,7 +713,7 @@ const procedures = serv
|
|
|
699
713
|
.build()
|
|
700
714
|
```
|
|
701
715
|
|
|
702
|
-
>
|
|
716
|
+
> 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.
|
|
703
717
|
|
|
704
718
|
### Request locale isolation
|
|
705
719
|
|
|
@@ -737,7 +751,10 @@ const procedures = {
|
|
|
737
751
|
const app = serv.createApp({
|
|
738
752
|
contract,
|
|
739
753
|
procedures,
|
|
740
|
-
createContext: async input => ({
|
|
754
|
+
createContext: async input => ({
|
|
755
|
+
...await authenticate(input),
|
|
756
|
+
scopeId: 'configured-product'
|
|
757
|
+
}),
|
|
741
758
|
})
|
|
742
759
|
```
|
|
743
760
|
|
package/README.ja.md
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
hai-framework の API サービスランタイム。公開 HTTP App 抽象を通じて `@h-ai/api-contract` を複数プラットフォームから使える HTTP API として提供します。Hono は内部実装です。
|
|
10
10
|
|
|
11
|
+
実行時の操作は `serv.xx` を使います。名前空間の型注釈や再エクスポートにはルート入口の `ServFunctions` を利用できます。context ファクトリーと汎用 `implement` の公開シグネチャーは利用側の宣言生成に対応し、内部ビルドパスや独立した helper は不要です。
|
|
12
|
+
|
|
11
13
|
## 主な機能
|
|
12
14
|
|
|
13
15
|
- `serv.createApp(...)` → `serv.listen(app, { port })` で起動。アプリは Hono を import する必要がありません。
|
|
@@ -22,7 +24,9 @@ hai-framework の API サービスランタイム。公開 HTTP App 抽象を通
|
|
|
22
24
|
- 任意の通信暗号化:`serv.createApp({ transport: { crypto, serverKeyRing, keyStore } })` で鍵交換とリクエスト・レスポンスの暗号化を自動接続。
|
|
23
25
|
- 任意の音声入口:`serv.createApp({ audio: { ai, verifyTicket, authorize } })` が共通 WebSocket を接続します。接続直後に単回 ticket で認証(`{ session, grant? }`、独立した事前認証 timeout)。各フレームを Zod と厳密な状態機械で検証し、入力・キュー・送信バッファの背圧と連鎖キャンセルを管理します。サーバーの認可処理で操作・モデル・声・割当量・同時使用数を確認します。
|
|
24
26
|
|
|
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
|
|
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 の拒否理由として保持します。
|
|
26
30
|
|
|
27
31
|
## 使い始める
|
|
28
32
|
|
|
@@ -53,6 +57,7 @@ const procedures = {
|
|
|
53
57
|
const app = serv.createApp({
|
|
54
58
|
contract,
|
|
55
59
|
procedures,
|
|
60
|
+
iam,
|
|
56
61
|
http: {
|
|
57
62
|
apiPrefix: '/api/v1',
|
|
58
63
|
openapi: { path: '/openapi.json' },
|
|
@@ -199,14 +204,16 @@ const app = serv.createApp({
|
|
|
199
204
|
contract,
|
|
200
205
|
procedures,
|
|
201
206
|
http: servConfig.http,
|
|
202
|
-
middlewares: [
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
207
|
+
middlewares: [
|
|
208
|
+
{
|
|
209
|
+
middleware: serv.cors({
|
|
210
|
+
origin: servConfig.cors.origin,
|
|
211
|
+
credentials: servConfig.cors.credentials,
|
|
212
|
+
allowedHeaders: [...servConfig.cors.allowedHeaders],
|
|
213
|
+
exposedHeaders: [...servConfig.cors.exposedHeaders],
|
|
214
|
+
}),
|
|
215
|
+
}
|
|
216
|
+
],
|
|
210
217
|
transport: servConfig.transport === false
|
|
211
218
|
? undefined
|
|
212
219
|
: {
|
|
@@ -419,6 +426,7 @@ const procedures = serv
|
|
|
419
426
|
|
|
420
427
|
- `.permission()` と `.role()` は認証も要求します。失敗時の HaiResult の `error.httpStatus` は未認証で 401、権限不足で 403 ですが、通常の procedure の HTTP 応答は既定で 200 です。
|
|
421
428
|
- 同じルートの複数 permission/role はすべて満たす必要があります。`serv.WILDCARD_PERMISSION` / `serv.WILDCARD_ROLE` は各分類を許可。
|
|
429
|
+
- 権限は IAM/Kit と同じコロン接頭辞にも対応します。`users:*` は `users:read` に一致し、`users2:read` やドット区切りの権限には一致しません。
|
|
422
430
|
- 未処理の handler 例外は INTERNAL_ERROR の HaiResult に変換。予期する業務失敗は直接 `err(...)` を返します。
|
|
423
431
|
- path・input・output は contract から推論。重複・未知のパスを拒否し、完全に実装するまで `.build()` は呼べません。
|
|
424
432
|
|
|
@@ -487,11 +495,14 @@ await apiClient.init({
|
|
|
487
495
|
|
|
488
496
|
> リフレッシュ Cookie には `HttpOnly` と `SameSite=Strict` が付き、パスは `{apiPrefix}/auth/refresh` に限定されます。本番では既定で `Secure` も付きます。
|
|
489
497
|
|
|
498
|
+
更新失敗にも公開エラー処理を適用します。5xx と欠落・不正なステータスは安全なメッセージと traceId を返し、内部原因はサーバーログだけに残します。更新 Cookie を消去するのは認証失効の 401 のみです。レート制限やバックエンド障害では再試行できるよう資格情報を保持します。自動監査にも同じ正規化済み失敗ステータスを使います。
|
|
499
|
+
|
|
490
500
|
### API 一覧
|
|
491
501
|
|
|
492
502
|
- `serv.createApp(options)`:独自 middleware、ヘルスチェック、OpenAPI、任意の文書を備えた HTTP App を作成。
|
|
493
503
|
- `serv.parseRequestContext({ request })`:Bearer token + requestId を抽出し、session は設定しません。
|
|
494
504
|
- `serv.listen(app, options)`:Node.js HTTP サービスを起動し、`{ server, address, close }` を返す。
|
|
505
|
+
返された `close()` は冪等です。新規接続を止め、Audio WebSocket を閉じ、応答しない音声接続を 1 秒後に強制終了して HTTP 終了を待ちます。直接呼出しでもプロセスのシグナルリスナーを解除します。業務モジュールは引き続きホストが逆順に解放します。
|
|
495
506
|
- `serv.toFetch(app)`:標準の `fetch(Request)` handler に変換。
|
|
496
507
|
- `serv.generateSpec(contract, options)`:contract から OpenAPI 3.1 spec を生成。
|
|
497
508
|
- `serv.storageAssets(config)`:key/MIME 許可リスト、GET/HEAD、ETag 付きの読み取り専用 Storage middleware。
|
|
@@ -626,15 +637,18 @@ const app = serv.createApp({
|
|
|
626
637
|
openapi: security.exposeApiDocs ? servConfig.http.openapi : false,
|
|
627
638
|
docs: security.exposeApiDocs ? servConfig.http.docs : false,
|
|
628
639
|
},
|
|
640
|
+
iam,
|
|
629
641
|
refreshCookie: { secure: security.secureRefreshCookie },
|
|
630
|
-
middlewares: [
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
642
|
+
middlewares: [
|
|
643
|
+
{
|
|
644
|
+
middleware: serv.cors({
|
|
645
|
+
origin: security.allowOrigin,
|
|
646
|
+
credentials: servConfig.cors.credentials,
|
|
647
|
+
allowedHeaders: [...servConfig.cors.allowedHeaders],
|
|
648
|
+
exposedHeaders: [...servConfig.cors.exposedHeaders],
|
|
649
|
+
}),
|
|
650
|
+
}
|
|
651
|
+
],
|
|
638
652
|
})
|
|
639
653
|
```
|
|
640
654
|
|
|
@@ -699,7 +713,7 @@ const procedures = serv
|
|
|
699
713
|
.build()
|
|
700
714
|
```
|
|
701
715
|
|
|
702
|
-
>
|
|
716
|
+
> 下流の 4xx メッセージは秘匿処理後に公開します。5xx と欠落・不正なステータスは traceId 付きの安全な内部エラーメッセージに統一し、完全な原因はサーバーログだけに記録します。`HaiError` は `messageKey/params` を持たないため、ドメインメッセージの i18n は生成元のモジュールでリクエストの locale を受け取って行います。
|
|
703
717
|
|
|
704
718
|
### リクエスト言語の分離
|
|
705
719
|
|
|
@@ -737,7 +751,10 @@ const procedures = {
|
|
|
737
751
|
const app = serv.createApp({
|
|
738
752
|
contract,
|
|
739
753
|
procedures,
|
|
740
|
-
createContext: async input => ({
|
|
754
|
+
createContext: async input => ({
|
|
755
|
+
...await authenticate(input),
|
|
756
|
+
scopeId: 'configured-product'
|
|
757
|
+
}),
|
|
741
758
|
})
|
|
742
759
|
```
|
|
743
760
|
|
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
hai-framework 的 API 服务运行时:对外暴露稳定的 HTTP App 抽象,把 `@h-ai/api-contract` 契约挂载为跨端可访问的 HTTP API;Hono 仅作为内部运行时实现。
|
|
10
10
|
|
|
11
|
+
运行时统一通过 `serv.xx` 使用。需要标注或转导出整个命名空间时,从根入口导入 `ServFunctions`;上下文工厂与泛型 `implement` 的公开签名可直接用于消费者声明生成,无需引用内部构建文件或独立 helper。
|
|
12
|
+
|
|
11
13
|
## 核心功能清单
|
|
12
14
|
|
|
13
15
|
- 一行启动:`serv.createApp(...)` → `serv.listen(app, { port })`,应用代码不需要 import Hono
|
|
@@ -22,7 +24,9 @@ hai-framework 的 API 服务运行时:对外暴露稳定的 HTTP App 抽象,
|
|
|
22
24
|
- 可选传输加密:`serv.createApp({ transport: { crypto, serverKeyRing, keyStore } })` 自动挂载密钥协商与请求/响应加解密
|
|
23
25
|
- 可选语音入口:`serv.createApp({ audio: { ai, verifyTicket, authorize } })` 挂载统一语音 WebSocket;连接建立即用一次性 ticket 鉴权(返回 `{ session, grant? }`,独立预鉴权超时),每帧运行时 Zod 校验 + 严格状态机,输入/积压/发送缓冲背压与级联取消;授权回调负责服务端确认操作、模型、音色、配额与并发占用
|
|
24
26
|
|
|
25
|
-
> 生命周期说明:`@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
|
|
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
|
+
监听句柄可在启动后立即关闭,以取消尚未完成的绑定;重复关闭复用同一结果。地址占用等真实监听失败仍会拒绝关闭结果,不会当作正常退出。
|
|
26
30
|
|
|
27
31
|
## 使用入门
|
|
28
32
|
|
|
@@ -53,6 +57,7 @@ const procedures = {
|
|
|
53
57
|
const app = serv.createApp({
|
|
54
58
|
contract,
|
|
55
59
|
procedures,
|
|
60
|
+
iam,
|
|
56
61
|
http: {
|
|
57
62
|
apiPrefix: '/api/v1',
|
|
58
63
|
openapi: { path: '/openapi.json' },
|
|
@@ -199,14 +204,16 @@ const app = serv.createApp({
|
|
|
199
204
|
contract,
|
|
200
205
|
procedures,
|
|
201
206
|
http: servConfig.http,
|
|
202
|
-
middlewares: [
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
207
|
+
middlewares: [
|
|
208
|
+
{
|
|
209
|
+
middleware: serv.cors({
|
|
210
|
+
origin: servConfig.cors.origin,
|
|
211
|
+
credentials: servConfig.cors.credentials,
|
|
212
|
+
allowedHeaders: [...servConfig.cors.allowedHeaders],
|
|
213
|
+
exposedHeaders: [...servConfig.cors.exposedHeaders],
|
|
214
|
+
}),
|
|
215
|
+
}
|
|
216
|
+
],
|
|
210
217
|
transport: servConfig.transport === false
|
|
211
218
|
? undefined
|
|
212
219
|
: {
|
|
@@ -419,6 +426,7 @@ const procedures = serv
|
|
|
419
426
|
|
|
420
427
|
- `.permission()` 与 `.role()` 自动要求认证;未登录/授权不足返回失败 HaiResult,`error.httpStatus` 分别为 401/403;普通 procedure 的 HTTP 响应默认仍为 200。
|
|
421
428
|
- 同一路由声明多个 permission/role 时必须全部满足;`serv.WILDCARD_PERMISSION` / `serv.WILDCARD_ROLE` 可放行对应类别。
|
|
429
|
+
- 权限还支持 IAM/Kit 一致的冒号前缀授权:`users:*` 匹配 `users:read`,不匹配 `users2:read` 或点号权限。
|
|
422
430
|
- handler 抛出的未处理异常统一转成 INTERNAL_ERROR HaiResult;预期业务失败仍应直接返回 `err(...)`。
|
|
423
431
|
- 路径、input、output 从 contract 推导;重复或未知路径会被拒绝,未实现完整 contract 时不能调用 `.build()`。
|
|
424
432
|
|
|
@@ -487,11 +495,14 @@ await apiClient.init({
|
|
|
487
495
|
|
|
488
496
|
> 刷新 Cookie 带有 `HttpOnly` 和 `SameSite=Strict`,路径限制为 `{apiPrefix}/auth/refresh`。生产默认还会设置 `Secure`。
|
|
489
497
|
|
|
498
|
+
刷新失败也经过统一公开错误投影:5xx 和缺失或非法状态返回安全提示与 traceId,内部原因只写入服务端日志。只有 401 认证失效会清除刷新 Cookie;限流和后端故障保留凭据,允许稍后重试。自动审计使用同一归一化状态记录失败。
|
|
499
|
+
|
|
490
500
|
### API 概览
|
|
491
501
|
|
|
492
502
|
- `serv.createApp(options)`:创建 HTTP App 抽象,挂载自定义 `middlewares`、健康检查、OpenAPI handler 与可选文档
|
|
493
503
|
- `serv.parseRequestContext({ request })`:默认上下文解析(提取 Bearer token + requestId,不填充 session)
|
|
494
504
|
- `serv.listen(app, options)`:在 Node.js 启动 HTTP 服务,返回 `{ server, address, close }`
|
|
505
|
+
返回句柄的 `close()` 可重复调用:停止接入并关闭 Audio WebSocket,1 秒后强制终止未配合的音频连接,随后等待 HTTP 关闭。主动关闭也移除进程信号监听器;业务模块仍由宿主反序释放。
|
|
495
506
|
- `serv.toFetch(app)`:包装为标准 `fetch(Request)` handler
|
|
496
507
|
- `serv.generateSpec(contract, options)`:由 contract 生成 OpenAPI 3.1 spec
|
|
497
508
|
- `serv.storageAssets(config)`:创建只读 Storage 资源 middleware,内置 key/MIME 白名单、GET/HEAD 与 ETag
|
|
@@ -626,15 +637,18 @@ const app = serv.createApp({
|
|
|
626
637
|
openapi: security.exposeApiDocs ? servConfig.http.openapi : false,
|
|
627
638
|
docs: security.exposeApiDocs ? servConfig.http.docs : false,
|
|
628
639
|
},
|
|
640
|
+
iam,
|
|
629
641
|
refreshCookie: { secure: security.secureRefreshCookie },
|
|
630
|
-
middlewares: [
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
642
|
+
middlewares: [
|
|
643
|
+
{
|
|
644
|
+
middleware: serv.cors({
|
|
645
|
+
origin: security.allowOrigin,
|
|
646
|
+
credentials: servConfig.cors.credentials,
|
|
647
|
+
allowedHeaders: [...servConfig.cors.allowedHeaders],
|
|
648
|
+
exposedHeaders: [...servConfig.cors.exposedHeaders],
|
|
649
|
+
}),
|
|
650
|
+
}
|
|
651
|
+
],
|
|
638
652
|
})
|
|
639
653
|
```
|
|
640
654
|
|
|
@@ -699,7 +713,7 @@ const procedures = serv
|
|
|
699
713
|
.build()
|
|
700
714
|
```
|
|
701
715
|
|
|
702
|
-
>
|
|
716
|
+
> 下游 `HaiResult` 的 4xx 消息经脱敏后公开;5xx 和缺失或非法状态统一为安全内部错误,保留 traceId,完整原因只记录到服务端日志。`HaiError` 不携带 `messageKey/params`,领域消息的请求级 i18n 仍由创建消息的模块接收 locale 后完成。
|
|
703
717
|
|
|
704
718
|
### 请求语言隔离
|
|
705
719
|
|
|
@@ -737,7 +751,10 @@ const procedures = {
|
|
|
737
751
|
const app = serv.createApp({
|
|
738
752
|
contract,
|
|
739
753
|
procedures,
|
|
740
|
-
createContext: async input => ({
|
|
754
|
+
createContext: async input => ({
|
|
755
|
+
...await authenticate(input),
|
|
756
|
+
scopeId: 'configured-product'
|
|
757
|
+
}),
|
|
741
758
|
})
|
|
742
759
|
```
|
|
743
760
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { normalizeServLocale, servM } from './chunk-
|
|
1
|
+
import { normalizeServLocale, servM } from './chunk-PLGCHDZM.js';
|
|
2
2
|
import { core, ok, err, HaiCommonError } from '@h-ai/core';
|
|
3
3
|
|
|
4
4
|
var DEFAULT_LOCALE = "zh-CN";
|
|
@@ -33,5 +33,5 @@ function validateInputOrFail(schema, input, locale = DEFAULT_LOCALE) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export { localizeZodError, resolveRequestLocale, validateInputOrFail };
|
|
36
|
-
//# sourceMappingURL=chunk-
|
|
37
|
-
//# sourceMappingURL=chunk-
|
|
36
|
+
//# sourceMappingURL=chunk-CZ6VA2EN.js.map
|
|
37
|
+
//# sourceMappingURL=chunk-CZ6VA2EN.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/serv-validation.ts"],"names":[],"mappings":";;;AAiBA,IAAM,cAAA,GAAiB,OAAA;AAmDhB,SAAS,qBAAqB,OAAA,EAA+D;AAClG,EAAA,MAAM,GAAA,GAAM,CAAC,IAAA,KAAqC;AAChD,IAAA,IAAI,OAAA,YAAmB,OAAA;AACrB,MAAA,OAAO,OAAA,CAAQ,GAAA,CAAI,IAAI,CAAA,IAAK,MAAA;AAC9B,IAAA,OAAO,QAAQ,IAAI,CAAA,IAAK,OAAA,CAAQ,IAAA,CAAK,aAAa,CAAA;AAAA,EACpD,CAAA;AAEA,EAAA,MAAM,QAAA,GAAW,IAAI,cAAc,CAAA;AACnC,EAAA,IAAI,QAAA;AACF,IAAA,OAAO,oBAAoB,QAAQ,CAAA;AAErC,EAAA,OAAO,oBAAoB,GAAA,CAAI,iBAAiB,KAAK,GAAA,CAAI,iBAAiB,KAAK,cAAc,CAAA;AAC/F;AAiBO,SAAS,gBAAA,CAAiB,UAAmB,MAAA,EAAuC;AACzF,EAAA,MAAM,cAAA,GAAiB,oBAAoB,MAAM,CAAA;AACjD,EAAA,OAAO,KAAK,aAAA,CAAc,uBAAA;AAAA,IACxB,QAAA;AAAA,IACA,KAAK,aAAA,CAAc,8BAAA;AAAA,MACjB,MAAA;AAAA,MACA,CAAC,YAAY,MAAA,KAAW,KAAA,CAAM,YAAY,EAAE,MAAA,EAAQ,cAAA,EAAgB,MAAA,EAAQ;AAAA;AAC9E,GACF;AACF;AA2CO,SAAS,mBAAA,CACd,MAAA,EACA,KAAA,EACA,MAAA,GAAiB,cAAA,EACG;AACpB,EAAA,MAAM,cAAA,GAAiB,oBAAoB,MAAM,CAAA;AACjD,EAAA,MAAM,MAAA,GAAS,MAAA,CAAO,SAAA,CAAU,KAAK,CAAA;AACrC,EAAA,IAAI,MAAA,CAAO,OAAA;AACT,IAAA,OAAO,EAAA,CAAG,OAAO,IAAI,CAAA;AACvB,EAAA,MAAM,MAAA,GAAS,gBAAA,CAAiB,MAAA,CAAO,KAAA,EAAO,cAAc,CAAA;AAC5D,EAAA,OAAO,GAAA,CAAI,cAAA,CAAe,gBAAA,EAAkB,KAAA,CAAM,uBAAA,EAAyB,EAAE,MAAA,EAAQ,cAAA,EAAgB,CAAA,EAAG,MAAM,CAAA;AAChH","file":"chunk-
|
|
1
|
+
{"version":3,"sources":["../src/serv-validation.ts"],"names":[],"mappings":";;;AAiBA,IAAM,cAAA,GAAiB,OAAA;AAmDhB,SAAS,qBAAqB,OAAA,EAA+D;AAClG,EAAA,MAAM,GAAA,GAAM,CAAC,IAAA,KAAqC;AAChD,IAAA,IAAI,OAAA,YAAmB,OAAA;AACrB,MAAA,OAAO,OAAA,CAAQ,GAAA,CAAI,IAAI,CAAA,IAAK,MAAA;AAC9B,IAAA,OAAO,QAAQ,IAAI,CAAA,IAAK,OAAA,CAAQ,IAAA,CAAK,aAAa,CAAA;AAAA,EACpD,CAAA;AAEA,EAAA,MAAM,QAAA,GAAW,IAAI,cAAc,CAAA;AACnC,EAAA,IAAI,QAAA;AACF,IAAA,OAAO,oBAAoB,QAAQ,CAAA;AAErC,EAAA,OAAO,oBAAoB,GAAA,CAAI,iBAAiB,KAAK,GAAA,CAAI,iBAAiB,KAAK,cAAc,CAAA;AAC/F;AAiBO,SAAS,gBAAA,CAAiB,UAAmB,MAAA,EAAuC;AACzF,EAAA,MAAM,cAAA,GAAiB,oBAAoB,MAAM,CAAA;AACjD,EAAA,OAAO,KAAK,aAAA,CAAc,uBAAA;AAAA,IACxB,QAAA;AAAA,IACA,KAAK,aAAA,CAAc,8BAAA;AAAA,MACjB,MAAA;AAAA,MACA,CAAC,YAAY,MAAA,KAAW,KAAA,CAAM,YAAY,EAAE,MAAA,EAAQ,cAAA,EAAgB,MAAA,EAAQ;AAAA;AAC9E,GACF;AACF;AA2CO,SAAS,mBAAA,CACd,MAAA,EACA,KAAA,EACA,MAAA,GAAiB,cAAA,EACG;AACpB,EAAA,MAAM,cAAA,GAAiB,oBAAoB,MAAM,CAAA;AACjD,EAAA,MAAM,MAAA,GAAS,MAAA,CAAO,SAAA,CAAU,KAAK,CAAA;AACrC,EAAA,IAAI,MAAA,CAAO,OAAA;AACT,IAAA,OAAO,EAAA,CAAG,OAAO,IAAI,CAAA;AACvB,EAAA,MAAM,MAAA,GAAS,gBAAA,CAAiB,MAAA,CAAO,KAAA,EAAO,cAAc,CAAA;AAC5D,EAAA,OAAO,GAAA,CAAI,cAAA,CAAe,gBAAA,EAAkB,KAAA,CAAM,uBAAA,EAAyB,EAAE,MAAA,EAAQ,cAAA,EAAgB,CAAA,EAAG,MAAM,CAAA;AAChH","file":"chunk-CZ6VA2EN.js","sourcesContent":["/**\n * @h-ai/serv — 校验工具\n *\n * 为 API service 提供与 kit 对等的校验 i18n 能力:\n * - {@link resolveRequestLocale}:从请求头(`x-hai-locale` / `Accept-Language`)解析语言。\n * - {@link validateInputOrFail}:在 procedure 内显式校验数据,失败返回 `HaiResult` 错误。\n * - oRPC contract 输入校验失败的自动本地化由 `serv-app.ts` 中的 oRPC 响应拦截器处理。\n *\n * @module serv-validation\n */\n\nimport type { HaiResult, ValidationFormError } from '@h-ai/core'\nimport type { ZodType } from 'zod'\nimport type { ServMessageKey } from './serv-i18n.js'\nimport { core, err, HaiCommonError, ok } from '@h-ai/core'\nimport { normalizeServLocale, servM } from './serv-i18n.js'\n\nconst DEFAULT_LOCALE = 'zh-CN'\n\n/**\n * 校验失败响应体(含 i18n 错误列表)。\n *\n * 复用 `HaiResult` 失败分支结构。字段错误同时在两处出现:\n * - `error.cause`:与 `validateInputOrFail` 输出对齐,HaiResult 标准位置\n * - 顶层 `errors`:保留以兼容已有客户端\n *\n * @example\n * ```ts\n * import type { ServValidationFailureBody } from '@h-ai/serv'\n * import { serv } from '@h-ai/serv'\n *\n * function fields(body: ServValidationFailureBody) {\n * return body.error.cause ?? body.errors\n * }\n * // 业务调用先判断 success,字段错误仅包含安全的本地化文案。\n * ```\n */\nexport interface ServValidationFailureBody {\n readonly success: false\n readonly error: {\n readonly code: string | number\n readonly message: string\n readonly traceId?: string\n readonly httpStatus: number\n readonly system: string\n readonly module: string\n readonly cause?: readonly ValidationFormError[]\n }\n readonly errors: readonly ValidationFormError[]\n}\n\n/**\n * 从请求头解析客户端期望 locale。\n *\n * 优先级:`x-hai-locale` > `Accept-Language` 第一个匹配项 > 默认值(`zh-CN`)。\n * 返回值会被规范化为 serv 当前支持的 locale(如 `en` → `en-US`)。\n *\n * @param headers - 请求头(`Headers` 或与之等价的对象)\n * @returns 解析得到的 locale\n *\n * @example\n * ```ts\n * import { serv } from '@h-ai/serv'\n *\n * const locale = serv.resolveRequestLocale(new Headers({ 'x-hai-locale': 'en-US' }))\n * const fallback = serv.resolveRequestLocale(new Headers())\n * ```\n */\nexport function resolveRequestLocale(headers: Headers | Record<string, string | undefined>): string {\n const get = (name: string): string | undefined => {\n if (headers instanceof Headers)\n return headers.get(name) ?? undefined\n return headers[name] ?? headers[name.toLowerCase()]\n }\n\n const explicit = get('x-hai-locale')\n if (explicit)\n return normalizeServLocale(explicit)\n\n return normalizeServLocale(get('accept-language') ?? get('Accept-Language') ?? DEFAULT_LOCALE)\n}\n\n/**\n * 把 Zod 错误转为本地化的扁平错误列表。\n *\n * @param zodError - Zod `SafeParseError` 或 `ZodError`\n * @param locale - 期望 locale\n *\n * @example\n * ```ts\n * // 模块内部上下文:示例复用当前声明所在模块的实际绑定。\n * const { z } = await import('zod')\n * const parsed = z.object({ name: z.string().min(1) }).safeParse({ name: '' })\n * if (parsed.success) return []\n * return localizeZodError(parsed.error, 'en-US')\n * ```\n */\nexport function localizeZodError(zodError: unknown, locale: string): ValidationFormError[] {\n const resolvedLocale = normalizeServLocale(locale)\n return core.zodValidation.mapZodErrorToFormErrors(\n zodError,\n core.zodValidation.createPrefixedZodMessageGetter<ServMessageKey>(\n 'serv',\n (messageKey, params) => servM(messageKey, { locale: resolvedLocale, params }),\n ),\n )\n}\n\n/**\n * 在 procedure handler 内显式校验数据。\n *\n * 与 oRPC contract 自动校验不同,本函数用于业务层二次校验(例如对从数据库读到的对象再校验)。\n * 失败时返回 `HaiResult` 错误分支,错误的 `cause` 字段携带 `ValidationFormError[]`。\n *\n * 推荐使用方式:\n * ```ts\n * import { z } from 'zod'\n * import { serv } from '@h-ai/serv'\n *\n * const Schema = z.object({ name: z.string().min(1) })\n *\n * export const procedures = serv\n * .implement(contract)\n * .context<ServContext>()\n * .route('profile.update', ({ input, context }) => {\n * const result = serv.validateInputOrFail(Schema, input, context.locale)\n * if (!result.success) return result\n * return profileService.update(result.data)\n * })\n * .build()\n * ```\n *\n * @param schema - Zod schema\n * @param input - 待校验数据\n * @param locale - 期望 locale(通常来自请求上下文)\n *\n * @example\n * ```ts\n * import { serv } from '@h-ai/serv'\n * import { z } from 'zod'\n *\n * const result = serv.validateInputOrFail(z.object({ name: z.string().min(1) }), { name: '' }, 'en-US')\n * if (!result.success) {\n * const fields = result.error.cause\n * } else {\n * const name = result.data.name\n * }\n * ```\n */\nexport function validateInputOrFail<TOutput>(\n schema: ZodType<TOutput>,\n input: unknown,\n locale: string = DEFAULT_LOCALE,\n): HaiResult<TOutput> {\n const resolvedLocale = normalizeServLocale(locale)\n const result = schema.safeParse(input)\n if (result.success)\n return ok(result.data)\n const errors = localizeZodError(result.error, resolvedLocale)\n return err(HaiCommonError.VALIDATION_ERROR, servM('serv_validationFailed', { locale: resolvedLocale }), errors)\n}\n"]}
|
|
@@ -165,7 +165,7 @@ function applyServGuards(requirements, handler) {
|
|
|
165
165
|
const { session } = options.context;
|
|
166
166
|
if (requirements.permissions?.length) {
|
|
167
167
|
const allowed = session.permissions.includes(WILDCARD_PERMISSION) || requirements.permissions.every(
|
|
168
|
-
(permission) => session.permissions.
|
|
168
|
+
(permission) => session.permissions.some((granted) => granted === permission || granted.endsWith(":*") && permission.startsWith(granted.slice(0, -1)))
|
|
169
169
|
);
|
|
170
170
|
if (!allowed) {
|
|
171
171
|
return err(
|
|
@@ -232,21 +232,32 @@ function mapHaiError(handler) {
|
|
|
232
232
|
resource,
|
|
233
233
|
category: /^(?:iam|auth|audit)(?:\.|$)/.test(resource) ? "system" : "business",
|
|
234
234
|
outcome: [401, 403, 423, 429].includes(status) ? "denied" : status >= 400 ? "failure" : "success",
|
|
235
|
-
details: {
|
|
236
|
-
|
|
235
|
+
details: {
|
|
236
|
+
method: options.context.request.method,
|
|
237
|
+
status,
|
|
238
|
+
...reasonCode === void 0 ? {} : { reasonCode }
|
|
239
|
+
}
|
|
240
|
+
}, {
|
|
241
|
+
read: ["GET", "HEAD", "OPTIONS"].includes(options.context.request.method)
|
|
242
|
+
});
|
|
237
243
|
if (!written.success)
|
|
238
244
|
options.context.logger.error("Request audit failed", { resource, error: written.error });
|
|
239
245
|
};
|
|
240
246
|
try {
|
|
241
247
|
const result = await handler(options);
|
|
242
|
-
|
|
243
|
-
|
|
248
|
+
if (result.success) {
|
|
249
|
+
await capture(200);
|
|
244
250
|
return result;
|
|
251
|
+
}
|
|
245
252
|
const error = core.error.toPublic(result.error, options.context.requestId, options.context.locale);
|
|
253
|
+
await capture(error.httpStatus ?? 500, error.code);
|
|
246
254
|
options.context.logger.error("Procedure failed", { traceId: error.traceId, error: result.error });
|
|
247
255
|
return { success: false, error };
|
|
248
256
|
} catch (error) {
|
|
249
|
-
const safe = core.error.toPublic({
|
|
257
|
+
const safe = core.error.toPublic({
|
|
258
|
+
...HaiCommonError.INTERNAL_ERROR,
|
|
259
|
+
message: servM("serv_errorInternal", { locale: options.context.locale })
|
|
260
|
+
}, options.context.requestId, options.context.locale);
|
|
250
261
|
options.context.logger.error("Procedure threw an exception", { traceId: safe.traceId, error });
|
|
251
262
|
await capture(500, HaiCommonError.INTERNAL_ERROR.code);
|
|
252
263
|
return err(safe);
|
|
@@ -353,8 +364,11 @@ function hasGuards(guards) {
|
|
|
353
364
|
return guards.auth === true || (guards.permissions?.length ?? 0) > 0 || (guards.roles?.length ?? 0) > 0;
|
|
354
365
|
}
|
|
355
366
|
function collectProcedurePaths(contract, prefix = []) {
|
|
356
|
-
if (isContractProcedure(contract))
|
|
357
|
-
return prefix.length > 0 ? [
|
|
367
|
+
if (isContractProcedure(contract)) {
|
|
368
|
+
return prefix.length > 0 ? [
|
|
369
|
+
prefix.join(".")
|
|
370
|
+
] : [];
|
|
371
|
+
}
|
|
358
372
|
return Object.entries(contract).flatMap(([key, child]) => collectProcedurePaths(child, [...prefix, key]));
|
|
359
373
|
}
|
|
360
374
|
function isContractProcedure(contract) {
|
|
@@ -406,5 +420,5 @@ function getHandlerMethod(value) {
|
|
|
406
420
|
}
|
|
407
421
|
|
|
408
422
|
export { WILDCARD_PERMISSION, WILDCARD_ROLE, buildHaiErrorBody, implement, normalizeServLocale, servM };
|
|
409
|
-
//# sourceMappingURL=chunk-
|
|
410
|
-
//# sourceMappingURL=chunk-
|
|
423
|
+
//# sourceMappingURL=chunk-PLGCHDZM.js.map
|
|
424
|
+
//# sourceMappingURL=chunk-PLGCHDZM.js.map
|