@h-ai/serv 0.1.0-alpha.76 → 1.0.0-beta.10

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 CHANGED
@@ -18,7 +18,7 @@ hai-framework's API service runtime: a stable public HTTP App abstraction that m
18
18
  - Default feature procedures: `createIamProcedures()`, `createStorageProcedures()`, `createAiProcedures()`.
19
19
  - `build()` synchronously returns a plain router, so nested default feature procedures can be selected directly. Match the mounted contract to the selected procedures; unselected routes stay unavailable.
20
20
  - Missing routes return localized HaiResult JSON with HTTP 404. Transport encryption preserves that status instead of producing a 500 for a non-JSON response.
21
- - Built-in security headers and health checks; optional OpenAPI JSON, Scalar docs, and internal RPC endpoints.
21
+ - Built-in security headers and health checks; optional OpenAPI JSON, Scalar docs.
22
22
  - Optional transport encryption: `serv.createApp({ transport: { crypto, serverKeyRing, keyStore } })` mounts key exchange and request/response encryption automatically.
23
23
  - Optional audio: `serv.createApp({ audio: { ai, verifyTicket, authorize } })` mounts a unified audio WebSocket. Connections authenticate immediately with one-time tickets (`{ session, grant? }`, separate pre-auth timeout). Every frame uses runtime Zod validation and a strict state machine, with input/queue/send-buffer backpressure and cascading cancellation. Authorization verifies operations, models, voices, quotas, and concurrency on the server.
24
24
 
@@ -102,7 +102,7 @@ const handler = serv.toFetch(app)
102
102
  export default { fetch: handler }
103
103
  ```
104
104
 
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.
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`.
106
106
 
107
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
108
 
@@ -178,7 +178,8 @@ When using `@h-ai/core` and `ServConfigSchema` with `config/_serv.yml`, transpor
178
178
  ```yaml
179
179
  # config/_serv.yml
180
180
  http:
181
- trustedProxies: []
181
+ trustedProxies: auto
182
+ access: false
182
183
  apiPrefix: /api/v1
183
184
 
184
185
  transport:
@@ -224,7 +225,7 @@ const app = serv.createApp({
224
225
 
225
226
  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
 
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
+ Empty POSTs still send an authenticated envelope for empty plaintext. GET/HEAD carry it in X-Hai-Transport; bodyless responses such as 204/304/HEAD use the same header. Other requests/responses carry a JSON envelope and X-Encrypted: true. The client must match the original request context and HTTP status; missing markers, old formats and authentication failures reject. Serv/Kit hosts explicitly allow plaintext through `encryption: disabled` path rules or host `excludePaths`. Callers must select ordinary requests; API Client/Kit wrappers use their own `excludePaths`. CORS must allow and expose X-Hai-Transport. TLS authenticates handshake keys; this protocol does not replace IAM authorization.
228
229
 
229
230
  Set `transport.maxPlaintextBytes` for both request and response JSON plaintext, measured in UTF-8 bytes: default 1 MiB, integer range 1 KiB–64 MiB. Clients learn the server limit during key exchange. `resolveTransportBodyLimits()` derives encrypted/proxy bytes as twice the plaintext limit plus 32 KiB. Handshake, public-key, header and replay constraints remain independent. Oversized streams are cancelled and readers released: requests return 413; oversized responses return an encrypted 500 capacity error, recognized as `TransportBodyTooLargeError` / `hai:crypto:070`. [Transport capacity and upgrade](../crypto/TRANSPORT-CAPACITY.md)。
230
231
 
@@ -351,18 +352,19 @@ const app = serv.createApp({
351
352
  Execution order is fixed:
352
353
 
353
354
  1. Built-in `securityHeaders`.
354
- 2. Your `middlewares`.
355
- 3. Built-in `transport`, if enabled.
356
- 4. health / refresh-cookie / OpenAPI / RPC / docs / oRPC routes
355
+ 2. Built-in CORS response wrapping; access checks for key exchange and preflight.
356
+ 3. Built-in `transport`, if enabled, and request access checks.
357
+ 4. Your `middlewares`.
358
+ 5. health / refresh-cookie / OpenAPI / docs / oRPC routes
357
359
 
358
360
  Notes:
359
361
 
360
362
  - `middlewares` register in array order.
361
363
  - Omitted `path` defaults to `'*'`.
362
- - Middleware runs before transport, so CORS preflight or binary endpoints can short-circuit. Responses retain security headers but bypass transport.
364
+ - Ordinary middleware runs after decryption and access checks. Early responses still follow policy and response encryption. Binary endpoints must be listed in `transport.excludePaths`; those exclusions do not bypass access checks.
363
365
  - Prefer `serv.storageAssets(...)` for public read-only files. Applications must supply key/MIME allowlists, caching, and CORS; no business concepts such as avatars are built in.
364
366
  - Private resources, Range downloads, and dynamic authorization need custom middleware/procedures, not the public asset helper.
365
- - To read decrypted business bodies, extend context/procedure instead.
367
+ - `c.req.json()` reads decrypted bodies here; built-in `serv.cors(...)` preflight still follows access checks.
366
368
  - Expose custom response headers such as `X-Encrypted` explicitly through `serv.cors({ exposedHeaders: [...] })` for browsers.
367
369
  - This layer works with HTTP middleware context and returns **HTTP Response**, not `HaiResult`.
368
370
 
@@ -424,7 +426,7 @@ Public API boundaries:
424
426
 
425
427
  - `src/pipelines/*` holds internal defaults and is not a public subpath API.
426
428
  - The stable application-facing entry remains `@h-ai/serv`.
427
- - Internal helpers such as `securityHeaders` / `requireInternalRPC` / `buildHaiErrorBody` are not documented as separate subpath exports.
429
+ - Internal helpers such as `securityHeaders` / `buildHaiErrorBody` are not documented as separate subpath exports.
428
430
 
429
431
  #### httpOnly cookie authentication (recommended for production)
430
432
 
@@ -487,7 +489,7 @@ await apiClient.init({
487
489
 
488
490
  ### API overview
489
491
 
490
- - `serv.createApp(options)`: create the HTTP App with custom middleware, health checks, OpenAPI, optional docs, and RPC.
492
+ - `serv.createApp(options)`: create the HTTP App with custom middleware, health checks, OpenAPI, optional docs.
491
493
  - `serv.parseRequestContext({ request })`: extract Bearer token + requestId without populating session.
492
494
  - `serv.listen(app, options)`: start Node.js HTTP service; return `{ server, address, close }`.
493
495
  - `serv.toFetch(app)`: wrap as a standard `fetch(Request)` handler.
@@ -512,6 +514,16 @@ Additional root types / named exports:
512
514
 
513
515
  > Transport encryption is a `createApp` option delegated internally to `crypto.transport`, not a flat `serv.xxx` API.
514
516
 
517
+ ### Access checks on every request
518
+
519
+ Rules accept `encryption: required` or `disabled`; omission inherits the host. Disabled endpoints accept ordinary HTTPS data while retaining IP, time and authentication checks, and reject framework encryption envelopes with HTTP 400. Required mode overrides host `excludePaths` and rejects App creation without transport. Conflicting modes on overlapping paths and methods fail at compilation; there is no automatic downgrade or declaration-order override.
520
+
521
+ `http.access` defaults to `false`. To restrict external endpoints only, set `http.access.rules` in `_serv.yml`, for example `{ id: 'external', paths: ['/api/v1/external/**'], allowIps: ['192.0.2.0/24'] }`. `http.access.global` restricts all requests handled by the host, including health checks. Global conditions and every matching rule must pass. Timezones default to UTC; empty lists leave the corresponding dimension unrestricted. Existing sessions are checked on every request; IAM `loginAccess` checks login only.
522
+
523
+ Use `http.access: { file: './config/security/request-access.yml' }` for larger policies. Paths resolve from the service working directory. App creation reads the file once, up to 1 MiB; invalid configuration stops startup, and changes require a new App. Denials return HTTP 403 with `hai:common:104`; ordinary transport requests retain encrypted responses. Logs correlate rule ID and denial reason with the request ID without copying policy contents.
524
+
525
+ `http.trustedProxies` defaults to `auto`: ordinary hosts ignore forwarded headers; only private managed Compose proxy topologies adopt container networks automatically. Custom ingress uses explicit proxy IPs/CIDRs; `[]` disables trust. Proxies sanitize forwarded headers, and Node listeners or Fetch platforms supply trusted peer metadata. See the [request access handbook](../../docs/usage/configuration/request-access.md) for files, edge proxies and deployment rollback.
526
+
515
527
  ### Configuration
516
528
 
517
529
  `@h-ai/serv` does not scan YAML itself. Load `config/_serv.yml` through `@h-ai/core` and pass the result to `serv.createApp()`.
@@ -552,7 +564,8 @@ server:
552
564
  port: 3000
553
565
 
554
566
  http:
555
- trustedProxies: []
567
+ trustedProxies: auto
568
+ access: false
556
569
  apiPrefix: /api/v1
557
570
  openapi:
558
571
  path: /openapi.json
@@ -561,7 +574,6 @@ http:
561
574
  health:
562
575
  path: /health
563
576
  readyPath: /ready
564
- rpc: false
565
577
 
566
578
  cors:
567
579
  origin: https://app.example.com
@@ -588,11 +600,12 @@ transport:
588
600
 
589
601
  `ServHttpConfigInput`:
590
602
 
591
- - `apiPrefix`: defaults to `/api/v1`.
603
+ Native RPC endpoints, `HAI_SERV_HTTP_RPC` and `ServRpcHttpConfig` have been removed. Existing OpenAPI/REST APIs, `api-client`, and the oRPC contracts and procedures remain supported.
604
+
605
+ - `apiPrefix`: defaults to `/api/v1`; accepts full absolute prefixes such as `/somepath/api/v1`. Segments use letters, digits, underscores, hyphens and dots; trailing slashes, empty segments, standalone `.`/`..`, queries and fragments are rejected. Configure health, docs and public storage paths under the same directory separately.
592
606
  - `health`: defaults to `{ path: '/health', readyPath: '/ready' }`.
593
607
  - `openapi`: defaults to `false`; enable with `{ path: '/openapi.json' }`.
594
- - `docs`: defaults to `false`; enable with `{ path: '/docs' }` (requires `openapi`). It mounts `/_hai/scalar.js` from the `@scalar/api-reference` browser bundle for local Scalar UI without an external CDN.
595
- - `rpc`: defaults to `false`; enable with `{ prefix: '/rpc', access: 'loopback' | 'private-network' | 'gateway-only', gatewayHeader?, gatewaySecret? }`.
608
+ - `docs`: defaults to `false`; enable with `{ path: '/docs' }`. Scalar is mounted as `_hai/scalar.js` in the document directory (default `/_hai/scalar.js`), without an external CDN. Script and spec URLs are relative. When a gateway preserves a prefix, place docs and OpenAPI paths within it; stripping a prefix also works. Backend `requireAuth` checks remain enforced.
596
609
  - `cors`: configured origins, credentials, allowedHeaders, and exposedHeaders; mount through `serv.cors(...)`.
597
610
  - `transport`: top-level `config/_serv.yml` option passed as `servConfig.transport` to `createApp({ transport })`; default key exchange subpath is `/_hai/key-exchange`.
598
611
 
@@ -639,7 +652,7 @@ createIamProcedures({ iam }) implements public config, password/LDAP/OTP login,
639
652
 
640
653
  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.
641
654
 
642
- oRPC contract input validation fails before procedures with HTTP 400 and a localized body containing `error.httpStatus=400` and field errors. Procedure-level `serv.validateInputOrFail` failures remain HTTP 200. HTTP middleware, refresh-cookie, OpenAPI/RPC access checks and transport protocol failures follow their own HTTP response contracts. api-client refreshes after an actual HTTP 401 or a valid failed HaiResult over HTTP 200 (`success=false`, string `error.code/message` and `error.httpStatus=401`) and retries once on success. Built-in login and registration and the configured refresh path are excluded; 403/500 do not trigger refresh. With transport enabled, refresh and retry share the encrypted fetch path.
655
+ oRPC contract input validation fails before procedures with HTTP 400 and a localized body containing `error.httpStatus=400` and field errors. Procedure-level `serv.validateInputOrFail` failures remain HTTP 200. HTTP middleware, refresh-cookie, OpenAPI access checks and transport protocol failures follow their own HTTP response contracts. api-client refreshes after an actual HTTP 401 or a valid failed HaiResult over HTTP 200 (`success=false`, string `error.code/message` and `error.httpStatus=401`) and retries once on success. Built-in login and registration and the configured refresh path are excluded; 403/500 do not trigger refresh. With transport enabled, refresh and retry share the encrypted fetch path.
643
656
 
644
657
  - All default procedures return `HaiResult<T>`; clients check `result.success`.
645
658
  - Authentication failure → `HaiCommonError.UNAUTHORIZED`, localized by `.auth()` using `context.locale`.
@@ -730,7 +743,7 @@ const app = serv.createApp({
730
743
 
731
744
  Native Audio WebSocket uses the same safe error boundary: internal Provider/SDK messages and stacks remain in sanitized logs, while clients receive stable codes, safe messages and traceId. Custom cross-origin configuration must preserve X-Hai-Reauthentication for sensitive actions; Serv/Kit defaults allow it.
732
745
 
733
- HTTP errors expose localized safe messages followed by `(traceId: …)`, with the same ID in `error.traceId` and `X-Trace-Id`. The framework reuses the request ID to correlate browser and server logs. Sanitized exceptions, stacks and operation context remain in logs; internal cause/ext are not exposed. Field validation returns localized field names and messages only.
746
+ HTTP errors carry safe localized messages and a reference ID. ToastContainer hides the ID by default and reveals it through its diagnostic icon. The same ID remains in `error.traceId` and `X-Trace-Id`. The framework reuses the request ID to correlate browser and server logs. Sanitized exceptions, stacks and operation context remain in logs; internal cause/ext are not exposed. Field validation returns localized field names and messages only.
734
747
 
735
748
  Use `core.error.toPublic(error, traceId?)` at HTTP/UI boundaries and `core.error.formatMessage(error)` after translating messages; formatting is idempotent. Pass complete Error objects to logs instead of discarding their context. Database failures log driver codes, sanitized messages/stacks and table/column context; SQL text, bound values and connection credentials are excluded.
736
749
 
package/README.ja.md CHANGED
@@ -18,7 +18,7 @@ hai-framework の API サービスランタイム。公開 HTTP App 抽象を通
18
18
  - 既定の機能 procedures:`createIamProcedures()`、`createStorageProcedures()`、`createAiProcedures()`。
19
19
  - `build()` は通常の router を同期的に返すため、標準 feature のネストした procedures を直接選択できます。接続する contract と procedures を一致させ、未選択の入口は公開しません。
20
20
  - 存在しないルートは翻訳された HaiResult JSON と HTTP 404 を返します。通信暗号化でも 404 を保ち、非 JSON 応答による 500 にしません。
21
- - 安全な応答ヘッダー・ヘルスチェックを内蔵し、OpenAPI JSON・Scalar 文書・内部 RPC endpoint は任意。
21
+ - 安全な応答ヘッダー・ヘルスチェックを内蔵し、OpenAPI JSON・Scalar 文書・
22
22
  - 任意の通信暗号化:`serv.createApp({ transport: { crypto, serverKeyRing, keyStore } })` で鍵交換とリクエスト・レスポンスの暗号化を自動接続。
23
23
  - 任意の音声入口:`serv.createApp({ audio: { ai, verifyTicket, authorize } })` が共通 WebSocket を接続します。接続直後に単回 ticket で認証(`{ session, grant? }`、独立した事前認証 timeout)。各フレームを Zod と厳密な状態機械で検証し、入力・キュー・送信バッファの背圧と連鎖キャンセルを管理します。サーバーの認可処理で操作・モデル・声・割当量・同時使用数を確認します。
24
24
 
@@ -102,7 +102,7 @@ const handler = serv.toFetch(app)
102
102
  export default { fetch: handler }
103
103
  ```
104
104
 
105
- Node `serv.listen()` は socket の接続元を自動で渡し、既定では転送ヘッダーを無視します。`http.trustedProxies` の IP/CIDR のみ信頼し、XFF を右から左へ調べ、最初の非信頼アドレスで停止します。その左側は無視します。信頼チェーンが不正・長すぎる場合は IP 未定義となり、代理をクライアントとして扱いません。代理は転送ヘッダーを清掃・追加してください。Fetch は `app.fetch(request, { remoteAddress: platformPeerAddress })` で信頼できる接続元を渡します。メタデータなしは IP 未定義です。任意のヘッダーをコピーしないでください。独自 `createContext(input)` は input 全体を再利用し、解決済み `input.ip` を保持します。内部 RPC は接続元で制限します。
105
+ Node `serv.listen()` は socket の接続元を自動で渡し、既定では転送ヘッダーを無視します。`http.trustedProxies` の IP/CIDR のみ信頼し、XFF を右から左へ調べ、最初の非信頼アドレスで停止します。その左側は無視します。信頼チェーンが不正・長すぎる場合は IP 未定義となり、代理をクライアントとして扱いません。代理は転送ヘッダーを清掃・追加してください。Fetch は `app.fetch(request, { remoteAddress: platformPeerAddress })` で信頼できる接続元を渡します。メタデータなしは IP 未定義です。任意のヘッダーをコピーしないでください。独自 `createContext(input)` は input 全体を再利用し、解決済み `input.ip` を保持します。
106
106
 
107
107
  toFetch は Request・URL・文字列を標準 Request(input, init) で構築し、入力フィールドを変更せず method・headers・body・signal などを上書きします。消費済み・ロック中の本文や不正な本文指定は返却 Promise を reject します。派発前の取消しは handler を呼ばず、実行中の取消しは呼出側の待機を終了し、有効な signal をアプリへ伝えます。終了・失敗・取消しで listener を外し、取消し後に遅れて返る本文も cancel します。開始済みの外部効果は巻き戻せず、Response 返却後のストリーム終了はアプリが request.signal を消費して管理します。
108
108
 
@@ -178,7 +178,8 @@ await apiClient.init({
178
178
  ```yaml
179
179
  # config/_serv.yml
180
180
  http:
181
- trustedProxies: []
181
+ trustedProxies: auto
182
+ access: false
182
183
  apiPrefix: /api/v1
183
184
 
184
185
  transport:
@@ -224,7 +225,7 @@ const app = serv.createApp({
224
225
 
225
226
  認証済み 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
 
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
+ 空の POST も空平文の認証封装を送ります。GET/HEAD は X-Hai-Transport ヘッダー、204/304/HEAD など本文なし応答も同じヘッダーを使います。他の要求・応答は JSON 本文と X-Encrypted: true を使います。クライアントは元の要求コンテキストと HTTP status を照合し、マーカー欠落、旧形式、認証失敗では reject します。平文 endpoint は Serv/Kit の `encryption: disabled` ルールまたはホストの `excludePaths` で明示的に許可し、呼出側も通常の要求を選びます。API Client/Kit のラッパーは自身の `excludePaths` を使用します。CORS では X-Hai-Transport を許可・公開してください。鍵交換の公開鍵認証は TLS に依存し、IAM の業務認可は別に必要です。
228
229
 
229
230
  `transport.maxPlaintextBytes` でリクエストとレスポンスの JSON 平文容量を共通設定します。単位は UTF-8 バイト、既定値は 1 MiB、有効範囲は 1 KiB~64 MiB の整数です。クライアントは鍵交換で上限を取得し、`resolveTransportBodyLimits()` が暗号文とプロキシ容量(平文の 2 倍 + 32 KiB)を算出します。鍵交換、公開鍵、認証ヘッダー、防リプレイ制約は独立です。超過時は元ストリームを停止して reader を解放します。要求は 413、応答は暗号化された 500 容量エラーとなり、`TransportBodyTooLargeError` / `hai:crypto:070` で識別できます。 [Transport capacity and upgrade](../crypto/TRANSPORT-CAPACITY.md)。
230
231
 
@@ -351,18 +352,19 @@ const app = serv.createApp({
351
352
  実行順は固定です:
352
353
 
353
354
  1. 組み込み `securityHeaders`。
354
- 2. 独自の `middlewares`。
355
- 3. 組み込み `transport`(有効時)。
356
- 4. health / refresh-cookie / OpenAPI / RPC / docs / oRPC routes
355
+ 2. 組み込み CORS 応答処理;鍵交換と preflight のアクセス判定。
356
+ 3. 組み込み `transport`(有効時)と各リクエストのアクセス判定。
357
+ 4. 独自の `middlewares`。
358
+ 5. health / refresh-cookie / OpenAPI / docs / oRPC routes
357
359
 
358
360
  補足:
359
361
 
360
362
  - `middlewares` は配列順に登録。
361
363
  - `path` 省略時は `'*'`。
362
- - middleware は transport より先なので、CORS preflight・バイナリー endpoint は途中で応答できます。安全ヘッダーは残りますが transport は通りません。
364
+ - 通常の middleware は復号とアクセス判定の後に実行し、途中の応答もポリシーと応答暗号化の対象です。バイナリー endpoint は `transport.excludePaths` に明記しますが、アクセス判定は省略されません。
363
365
  - 公開読み取り専用ファイルは `serv.storageAssets(...)` を優先。key/MIME 許可リスト・キャッシュ・CORS はアプリが指定し、アバターなどの業務概念は内蔵しません。
364
366
  - 非公開リソース・Range ダウンロード・動的認可は独自 middleware/procedure を使い、公開 helper は使いません。
365
- - 復号済みの業務 body を読む場合は context / procedure 層を拡張。
367
+ - この段階の `c.req.json()` は復号後の本文を読みます。組み込み `serv.cors(...)` の preflight もアクセス判定の対象です。
366
368
  - ブラウザーが `X-Encrypted` などを読む場合は `serv.cors({ exposedHeaders: [...] })` で明示的に公開。
367
369
  - この層は HTTP middleware context を操作し、`HaiResult` ではなく **HTTP Response** を返します。
368
370
 
@@ -424,7 +426,7 @@ const procedures = serv
424
426
 
425
427
  - `src/pipelines/*` は内部の既定実装で、公開サブパス API ではありません。
426
428
  - アプリ向けの安定した入口はルートの `@h-ai/serv`。
427
- - `securityHeaders` / `requireInternalRPC` / `buildHaiErrorBody` などの内部 helper は独立サブパスとして公開しません。
429
+ - `securityHeaders` / `buildHaiErrorBody` などの内部 helper は独立サブパスとして公開しません。
428
430
 
429
431
  #### httpOnly Cookie 認証(本番推奨)
430
432
 
@@ -487,7 +489,7 @@ await apiClient.init({
487
489
 
488
490
  ### API 一覧
489
491
 
490
- - `serv.createApp(options)`:独自 middleware、ヘルスチェック、OpenAPI、任意の文書・RPC を備えた HTTP App を作成。
492
+ - `serv.createApp(options)`:独自 middleware、ヘルスチェック、OpenAPI、任意の文書を備えた HTTP App を作成。
491
493
  - `serv.parseRequestContext({ request })`:Bearer token + requestId を抽出し、session は設定しません。
492
494
  - `serv.listen(app, options)`:Node.js HTTP サービスを起動し、`{ server, address, close }` を返す。
493
495
  - `serv.toFetch(app)`:標準の `fetch(Request)` handler に変換。
@@ -512,6 +514,16 @@ await apiClient.init({
512
514
 
513
515
  > 通信暗号化は `createApp` の設定機能で、内部で `crypto.transport` に委譲します。`serv.xxx` の独立 API ではありません。
514
516
 
517
+ ### リクエストごとのアクセス制御
518
+
519
+ ルールの `encryption` は `required` / `disabled` を指定でき、省略時はホストに従います。disabled は通常の HTTPS データを受け付けますが、IP・時刻・認証は検証し、暗号化信封には 400 を返します。required は `excludePaths` より優先し、transport 未設定では App 作成に失敗します。重複するパスとメソッドの反対指定は装配時に拒否し、宣言順の上書きや自動降格は行いません。
520
+
521
+ `http.access` の既定値は `false` です。外部 API だけを制限する場合、`_serv.yml` の `http.access.rules` に `{ id: 'external', paths: ['/api/v1/external/**'], allowIps: ['192.0.2.0/24'] }` などを設定します。`http.access.global` はヘルスチェックを含むホストの全要求を制限します。全体条件と一致するすべてのルールを満たす必要があります。時区は既定 UTC、空のリストは対象の制限なしです。既存セッションも毎回判定し、IAM `loginAccess` はログイン時だけを制限します。
522
+
523
+ 大きな設定には `http.access: { file: './config/security/request-access.yml' }` を使用します。サービスの作業ディレクトリを基準に、App 生成時だけ最大 1 MiB を読みます。不正な設定では起動を停止し、変更時は App を作り直します。拒否は HTTP 403、`hai:common:104` で、通常の暗号化要求は応答も暗号化します。ポリシー本文をログに複製せず、ルール ID・拒否理由・要求 ID で調査します。
524
+
525
+ `http.trustedProxies` は既定 `auto` です。通常のホストでは転送ヘッダーを無視し、管理対象 Compose の非公開バックエンド構成だけでコンテナのネットワークを採用します。独自の代理は実際の IP/CIDR を指定し、`[]` は信頼を無効化します。代理はヘッダーを清掃し、Node・Fetch は信頼できる接続元情報を提供します。独立ファイル・外側の代理・復旧は[リクエスト制御手引き](../../docs/usage/configuration/request-access.md)を参照してください。
526
+
515
527
  ### 設定
516
528
 
517
529
  `@h-ai/serv` 自体は YAML を走査しません。`@h-ai/core` で `config/_serv.yml` を読み、解析結果を `serv.createApp()` に渡してください。
@@ -552,7 +564,8 @@ server:
552
564
  port: 3000
553
565
 
554
566
  http:
555
- trustedProxies: []
567
+ trustedProxies: auto
568
+ access: false
556
569
  apiPrefix: /api/v1
557
570
  openapi:
558
571
  path: /openapi.json
@@ -561,7 +574,6 @@ http:
561
574
  health:
562
575
  path: /health
563
576
  readyPath: /ready
564
- rpc: false
565
577
 
566
578
  cors:
567
579
  origin: https://app.example.com
@@ -588,11 +600,12 @@ transport:
588
600
 
589
601
  `ServHttpConfigInput`:
590
602
 
591
- - `apiPrefix`:既定 `/api/v1`。
603
+ ネイティブ RPC 入口、`HAI_SERV_HTTP_RPC`、`ServRpcHttpConfig` は削除しました。既存の OpenAPI/REST API、`api-client`、oRPC contract と procedure は引き続き利用できます。
604
+
605
+ - `apiPrefix`:既定 `/api/v1`。`/somepath/api/v1` などの絶対接頭辞を指定できます。英数字・アンダースコア・ハイフン・点を使用し、末尾のスラッシュ、空区間、単独の `.`/`..`、クエリとフラグメントは拒否します。ヘルス・文書・公開ストレージのパスも同じディレクトリに別途設定します。
592
606
  - `health`:既定 `{ path: '/health', readyPath: '/ready' }`。
593
607
  - `openapi`:既定 `false`。`{ path: '/openapi.json' }` で有効化。
594
- - `docs`:既定 `false`。`{ path: '/docs' }` で有効化(`openapi` が必要)。`@scalar/api-reference` の browser bundle から `/_hai/scalar.js` を提供し、外部 CDN なしで Scalar UI を使えます。
595
- - `rpc`:既定 `false`。`{ prefix: '/rpc', access: 'loopback' | 'private-network' | 'gateway-only', gatewayHeader?, gatewaySecret? }` で有効化。
608
+ - `docs`:既定 `false`。`{ path: '/docs' }` で有効化。文書ディレクトリ内の `_hai/scalar.js`(既定 `/_hai/scalar.js`)で Scalar UI を配信し、外部 CDN は不要です。スクリプトと spec の URL は相対指定です。ゲートウェイが接頭辞を保持する場合は docs/openapi も転送範囲内に配置し、接頭辞を除去する構成にも対応します。`requireAuth` は後端で検証します。
596
609
  - `cors`:origin・credentials・allowedHeaders・exposedHeaders を設定し、`serv.cors(...)` で接続。
597
610
  - `transport`:`config/_serv.yml` のトップレベル設定。`servConfig.transport` を `createApp({ transport })` に渡し、既定の鍵交換サブパスは `/_hai/key-exchange`。
598
611
 
@@ -639,7 +652,7 @@ createIamProcedures({ iam }) は公開設定、LDAP/OTP/パスワード、登録
639
652
 
640
653
  通常の procedure は認証・認可・業務の失敗と捕捉した handler 例外を HaiResult として既定で HTTP 200 で返します。`error.httpStatus` はドメインエラーのメタデータで、HTTP 応答ステータスを変更しません。クライアントは `success` と `error.code` を確認してください。未認証では `error.httpStatus=401`、DB クエリー失敗や INTERNAL_ERROR では 500 です。
641
654
 
642
- oRPC contract の入力検証は procedure の前に失敗すると HTTP 400 となり、ローカライズ済み body に `error.httpStatus=400` とフィールドエラーを含みます。procedure 内の `serv.validateInputOrFail` の失敗は HTTP 200 のままです。HTTP middleware、refresh-cookie、OpenAPI/RPC のアクセス確認、transport のプロトコルエラーは各自の HTTP 応答契約に従います。api-client は実際の HTTP 401、または HTTP 200 の有効な失敗 HaiResult(`success=false`、文字列の `error.code/message`、`error.httpStatus=401`)で自動更新し、成功後に一度だけ再試行します。組み込みのログイン・登録と設定された更新パスは対象外で、403/500 でも更新しません。transport 有効時は更新と再試行も同じ暗号化 fetch を使用します。
655
+ oRPC contract の入力検証は procedure の前に失敗すると HTTP 400 となり、ローカライズ済み body に `error.httpStatus=400` とフィールドエラーを含みます。procedure 内の `serv.validateInputOrFail` の失敗は HTTP 200 のままです。HTTP middleware、refresh-cookie、OpenAPI のアクセス確認、transport のプロトコルエラーは各自の HTTP 応答契約に従います。api-client は実際の HTTP 401、または HTTP 200 の有効な失敗 HaiResult(`success=false`、文字列の `error.code/message`、`error.httpStatus=401`)で自動更新し、成功後に一度だけ再試行します。組み込みのログイン・登録と設定された更新パスは対象外で、403/500 でも更新しません。transport 有効時は更新と再試行も同じ暗号化 fetch を使用します。
643
656
 
644
657
  - 既定 procedures はすべて `HaiResult<T>` を返し、クライアントで `result.success` を確認。
645
658
  - 認証失敗 → `HaiCommonError.UNAUTHORIZED`。`.auth()` が `context.locale` でローカライズ。
@@ -730,7 +743,7 @@ const app = serv.createApp({
730
743
 
731
744
  ネイティブ Audio WebSocket も安全なエラー境界を使います。Provider/SDK の内部情報やスタックはマスク済みログだけに残し、クライアントには安定コード、安全な表示、traceId を返します。独自 CORS 設定でも機密操作の X-Hai-Reauthentication を許可してください。Serv/Kit の既定設定には含まれます。
732
745
 
733
- HTTP エラーはローカライズ済みの安全な表示に `(traceId: …)` を付け、同じ ID を error.traceId と X-Trace-Id に返します。requestId を traceId として共有し、ブラウザーとサーバーのログを関連付けます。脱機密化した例外・スタック・操作情報はログに残し、内部 cause/ext は公開しません。項目検証は翻訳済みの項目名とメッセージだけを返します。
746
+ HTTP エラーには安全な翻訳文と追跡番号を含めます。ToastContainer は番号を既定で非表示にし、調査情報のアイコンから表示・コピーできます。同じ ID を error.traceId と X-Trace-Id に返します。requestId を traceId として共有し、ブラウザーとサーバーのログを関連付けます。脱機密化した例外・スタック・操作情報はログに残し、内部 cause/ext は公開しません。項目検証は翻訳済みの項目名とメッセージだけを返します。
734
747
 
735
748
  HTTP/UI 境界には core.error.toPublic(error, traceId?)、再翻訳後の表示には core.error.formatMessage(error) を使い、ID を重複追加しません。ログには完全な Error を渡してください。DB ログにはドライバーコード、脱機密化したメッセージ・スタック、表・列を残し、SQL 本文、バインド値と接続認証情報を記録しません。
736
749
 
package/README.md CHANGED
@@ -18,7 +18,7 @@ hai-framework 的 API 服务运行时:对外暴露稳定的 HTTP App 抽象,
18
18
  - 默认 feature procedures:`createIamProcedures()`、`createStorageProcedures()`、`createAiProcedures()`
19
19
  - `build()` 同步返回普通 router,可按嵌套属性选取默认 feature procedures;挂载的 contract 与所选 procedures 保持一致,未选入口不开放。
20
20
  - 缺失路由返回本地化的 HaiResult JSON 404,启用传输加密后保留 404,不误报为非 JSON 响应导致的 500。
21
- - 内置安全响应头、健康检查、可选 OpenAPI JSON、可选 Scalar 文档页、可选内部 RPC endpoint
21
+ - 内置安全响应头、健康检查、可选 OpenAPI JSON、可选 Scalar 文档页
22
22
  - 可选传输加密:`serv.createApp({ transport: { crypto, serverKeyRing, keyStore } })` 自动挂载密钥协商与请求/响应加解密
23
23
  - 可选语音入口:`serv.createApp({ audio: { ai, verifyTicket, authorize } })` 挂载统一语音 WebSocket;连接建立即用一次性 ticket 鉴权(返回 `{ session, grant? }`,独立预鉴权超时),每帧运行时 Zod 校验 + 严格状态机,输入/积压/发送缓冲背压与级联取消;授权回调负责服务端确认操作、模型、音色、配额与并发占用
24
24
 
@@ -102,7 +102,7 @@ const handler = serv.toFetch(app)
102
102
  export default { fetch: handler }
103
103
  ```
104
104
 
105
- 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 仍按连接对端限制。
105
+ 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`。
106
106
 
107
107
  toFetch 对 Request、URL、字符串统一调用标准 Request(input, init),RequestInit 可覆盖 method、headers、body、signal 等字段;不修改原 Request 的字段,正文已消费/锁定或非法正文组合会以 Promise 拒绝。预取消和派发前取消不进入 handler,进行中取消结束调用方等待并把有效 signal 传给应用;监听器在完成/失败/取消后释放,取消后的迟到响应会取消未消费正文。取消不能回滚已经开始的外部副作用;Response 交付后的流由应用监听 request.signal 并自行关闭。
108
108
 
@@ -178,7 +178,8 @@ await apiClient.init({
178
178
  ```yaml
179
179
  # config/_serv.yml
180
180
  http:
181
- trustedProxies: []
181
+ trustedProxies: auto
182
+ access: false
182
183
  apiPrefix: /api/v1
183
184
 
184
185
  transport:
@@ -224,7 +225,7 @@ const app = serv.createApp({
224
225
 
225
226
  认证协议仅支持 v2:独立随机 SM4-CBC 密钥和 HMAC-SM3 密钥由 SM2 封装,先验签、再验证 MAC,最后解密 CBC。请求由客户端私钥签名,响应由所选服务端版本签名。认证字段包括 version、direction、serverKeyId、clientId、128 位随机 requestId(兼 nonce)、issuedAt、HTTP method、pathname + 原始 search 的 SM3 pathHash,以及响应 status。请求有效期 5 分钟,未来时钟偏差最多 30 秒;共享 registry 原子消费 (clientId, requestId),nonce 至少保留到 issuedAt + 6 分钟以覆盖节点偏差,存储故障拒绝请求。Redis 必须保留有效 nonce,使用 noeviction/可靠持久化策略;各节点时钟须同步。此能力防止同一传输消息重放,业务重试仍需自己的幂等键。
226
227
 
227
- 空 POST 也发送空明文的认证封装;GET/HEAD 将封装放在 X-Hai-Transport 头,204/304/HEAD 等无正文响应也使用同名头承载认证封装。其它请求/响应在 JSON 正文承载封装并带 X-Encrypted: true。客户端必须关联原请求上下文和 HTTP status,缺标记、旧格式或认证失败一律 reject;明文接口只能经 excludePaths 显式排除。CORS 须允许并暴露 X-Hai-Transport。握手公钥的真实性依赖 TLS,不替代 IAM 业务授权。
228
+ 空 POST 也发送空明文的认证封装;GET/HEAD 将封装放在 X-Hai-Transport 头,204/304/HEAD 等无正文响应也使用同名头承载认证封装。其它请求/响应在 JSON 正文承载封装并带 X-Encrypted: true。客户端必须关联原请求上下文和 HTTP status,缺标记、旧格式或认证失败一律 reject;明文接口由 Serv/Kit 路径规则 `encryption: disabled` 或宿主 `excludePaths` 显式允许;调用方同步选择普通请求,API Client/Kit 包装器使用自己的 `excludePaths`。CORS 须允许并暴露 X-Hai-Transport。握手公钥的真实性依赖 TLS,不替代 IAM 业务授权。
228
229
 
229
230
  业务容量通过 `transport.maxPlaintextBytes` 统一配置:默认 1 MiB,整数范围 1 KiB~64 MiB,请求和响应共用 UTF-8 明文字节预算。客户端从握手自动获取上限,密文与部署代理预算由 `resolveTransportBodyLimits()` 推导(两倍明文 + 32 KiB)。握手、公钥、认证头和防重放约束独立;超限取消原流并释放 reader,请求 413、响应为加密的 500 容量错误,客户端识别 `TransportBodyTooLargeError` / `hai:crypto:070`。 [Transport capacity and upgrade](../crypto/TRANSPORT-CAPACITY.md)。
230
231
 
@@ -351,18 +352,19 @@ const app = serv.createApp({
351
352
  执行顺序固定为:
352
353
 
353
354
  1. 内置 `securityHeaders`
354
- 2. 你的 `middlewares`
355
- 3. 内置 `transport`(若启用)
356
- 4. health / refresh-cookie / OpenAPI / RPC / docs / oRPC routes
355
+ 2. 内置 CORS 响应包装;密钥交换和预检先检查访问策略
356
+ 3. 内置 `transport`(若启用)及每次请求访问策略
357
+ 4. 你的 `middlewares`
358
+ 5. health / refresh-cookie / OpenAPI / docs / oRPC routes
357
359
 
358
360
  说明:
359
361
 
360
362
  - `middlewares` 按数组顺序注册
361
363
  - `path` 省略时默认 `'*'`
362
- - 由于 `middlewares` 先于 `transport` 执行,CORS preflight 或二进制端点可以直接短路;短路响应仍带内置安全头,但不会经过 transport
364
+ - 普通 `middlewares` 在解密和访问策略之后执行;提前响应仍受策略约束并经过响应加密。二进制端点须显式列入 `transport.excludePaths`,该排除不会绕过访问策略
363
365
  - 公开只读文件优先使用 `serv.storageAssets(...)`;应用仍须显式提供 key/MIME 白名单、缓存与跨域策略,框架不内置头像等业务概念
364
366
  - 私有资源、Range 下载或动态授权继续使用自定义 middleware/procedure,不应套用公开资源 helper
365
- - 若需要读取解密后的业务 body,请改用 context / procedure 层扩展
367
+ - 此时 `c.req.json()` 读取解密后的正文;内置 `serv.cors(...)` 的预检仍检查访问策略
366
368
  - 浏览器若需读取自定义响应头(例如 transport 的 `X-Encrypted`),请通过 `serv.cors({ exposedHeaders: [...] })` 显式暴露
367
369
  - 这一层直接操作 HTTP middleware context,返回的是 **HTTP Response**,不是 `HaiResult`
368
370
 
@@ -424,7 +426,7 @@ const procedures = serv
424
426
 
425
427
  - `src/pipelines/*` 是 `@h-ai/serv` 的内部默认实现目录,不作为公开子路径 API 承诺
426
428
  - 对应用层公开的稳定入口仍然是根入口 `@h-ai/serv`
427
- - `securityHeaders` / `requireInternalRPC` / `buildHaiErrorBody` 等内部 helper/middleware 不作为单独子路径文档暴露
429
+ - `securityHeaders` / `buildHaiErrorBody` 等内部 helper/middleware 不作为单独子路径文档暴露
428
430
 
429
431
  #### httpOnly Cookie 认证(推荐生产方案)
430
432
 
@@ -487,7 +489,7 @@ await apiClient.init({
487
489
 
488
490
  ### API 概览
489
491
 
490
- - `serv.createApp(options)`:创建 HTTP App 抽象,挂载自定义 `middlewares`、健康检查、OpenAPI handler、可选文档与 RPC
492
+ - `serv.createApp(options)`:创建 HTTP App 抽象,挂载自定义 `middlewares`、健康检查、OpenAPI handler 与可选文档
491
493
  - `serv.parseRequestContext({ request })`:默认上下文解析(提取 Bearer token + requestId,不填充 session)
492
494
  - `serv.listen(app, options)`:在 Node.js 启动 HTTP 服务,返回 `{ server, address, close }`
493
495
  - `serv.toFetch(app)`:包装为标准 `fetch(Request)` handler
@@ -512,6 +514,16 @@ await apiClient.init({
512
514
 
513
515
  > 传输加密不作为 `serv.xxx` 扁平 API 暴露;它是 `createApp` 的配置能力,内部委托 `crypto.transport`。
514
516
 
517
+ ### 每次请求的访问控制
518
+
519
+ 路径规则可设置 `encryption: required` 或 `disabled`,省略沿用宿主。disabled 接口接收普通 HTTPS 数据,仍检查 IP、时段和认证;带框架加密信封的请求返回 400。required 优先于宿主 `excludePaths`,未装配 transport 时拒绝创建 App。重叠路径和方法的相反模式在装配时失败;不自动降级或按声明顺序覆盖。
520
+
521
+ `http.access` 默认 `false`。需要仅限制对外接口时,在 `_serv.yml` 配置 `http.access.rules`,例如 `{ id: 'external', paths: ['/api/v1/external/**'], allowIps: ['192.0.2.0/24'] }`;`http.access.global` 则约束宿主处理的全部请求,包含健康检查。全局与所有匹配规则同时生效,时间窗口默认 UTC,空列表不限对应维度。已有会话也会在每次请求时重新检查;IAM `loginAccess` 只负责登录。
522
+
523
+ 较大策略使用 `http.access: { file: './config/security/request-access.yml' }`。文件按服务工作目录解析,仅在创建 App 时读取,最大 1 MiB;无效配置阻止启动,修改后重新创建 App。拒绝返回 HTTP 403、`hai:common:104`,启用 transport 的普通请求保留加密响应。日志用规则 ID、拒绝原因和请求编号定位,不记录策略正文。
524
+
525
+ `http.trustedProxies` 默认 `auto`:普通宿主忽略转发头;受管 Compose 的私有代理拓扑才自动采用容器网段。自管代理指定真实 IP/CIDR,`[]` 显式关闭。代理必须清洗转发头,Node 监听器或 Fetch 平台必须提供可信对端元数据。独立文件、外层代理和部署回滚步骤见[请求访问控制手册](../../docs/usage/configuration/request-access.md)。
526
+
515
527
  ### 配置
516
528
 
517
529
  `@h-ai/serv` 本身不主动扫描 YAML;推荐由 `@h-ai/core` 统一加载 `config/_serv.yml`,再把解析结果传给 `serv.createApp()`。
@@ -552,7 +564,8 @@ server:
552
564
  port: 3000
553
565
 
554
566
  http:
555
- trustedProxies: []
567
+ trustedProxies: auto
568
+ access: false
556
569
  apiPrefix: /api/v1
557
570
  openapi:
558
571
  path: /openapi.json
@@ -561,7 +574,6 @@ http:
561
574
  health:
562
575
  path: /health
563
576
  readyPath: /ready
564
- rpc: false
565
577
 
566
578
  cors:
567
579
  origin: https://app.example.com
@@ -588,11 +600,12 @@ transport:
588
600
 
589
601
  `ServHttpConfigInput`:
590
602
 
591
- - `apiPrefix`:默认 `/api/v1`
603
+ 原生 RPC 入口及 `HAI_SERV_HTTP_RPC`、`ServRpcHttpConfig` 已删除。业务继续使用现有 OpenAPI/REST 路径和 `api-client`,底层 oRPC contract 与 procedure 保留。
604
+
605
+ - `apiPrefix`:默认 `/api/v1`;支持 `/somepath/api/v1` 等完整绝对前缀。路径段只含字母、数字、下划线、连字符和点,不接受末尾斜杠、空段、单独的 `.`/`..`、查询或片段。健康、文档和存储公开路径分别配置到同一目录。
592
606
  - `health`:默认 `{ path: '/health', readyPath: '/ready' }`
593
607
  - `openapi`:默认 `false`;启用:`{ path: '/openapi.json' }`
594
- - `docs`:默认 `false`;启用:`{ path: '/docs' }`(依赖 `openapi`);启用后自动挂载 `/_hai/scalar.js`,从 `@scalar/api-reference` 的 browser bundle 提供本地 Scalar UI 脚本,无需外网 CDN
595
- - `rpc`:默认 `false`;启用:`{ prefix: '/rpc', access: 'loopback' | 'private-network' | 'gateway-only', gatewayHeader?, gatewaySecret? }`
608
+ - `docs`:默认 `false`;启用:`{ path: '/docs' }`;文档目录下自动挂载 `_hai/scalar.js`(默认 `/_hai/scalar.js`),提供本地 Scalar UI,无需外网 CDN。页面中的脚本和 spec 使用相对地址;网关保留前缀时 docs/openapi 都应配置在转发范围内,剥离前缀时仍可用后端原路径。`requireAuth` 仍由后端校验。
596
609
  - `cors`:配置文件中的跨端 origin、credentials、allowedHeaders 与 exposedHeaders;应用通过 `serv.cors(...)` 装配。
597
610
  - `transport`:`config/_serv.yml` 顶层配置;启用后可通过 `servConfig.transport` 装配到 `createApp({ transport })`,默认密钥协商子路径 `/_hai/key-exchange`
598
611
 
@@ -639,7 +652,7 @@ const app = serv.createApp({
639
652
 
640
653
  普通 procedure 的认证、授权、业务失败和被捕获的 handler 异常都作为 `HaiResult` 输出,默认通过 HTTP 200 返回;`error.httpStatus` 是领域错误元数据,不会自动改写响应状态。客户端必须检查 `success` 与 `error.code`。例如未登录为 `error.httpStatus=401`,数据库查询失败或 INTERNAL_ERROR 为 500。
641
654
 
642
- oRPC contract 输入校验在 procedure 之前失败时返回 HTTP 400,本地化失败体包含 `error.httpStatus=400` 和字段错误;procedure 内 `serv.validateInputOrFail` 返回的失败仍为 HTTP 200。HTTP middleware、refresh-cookie、OpenAPI/RPC 权限与 transport 协议错误另按自身契约返回真实 HTTP 状态。api-client 在真实 HTTP 401 或 HTTP 200 的有效失败 HaiResult(`success=false`、字符串 `error.code/message`、`error.httpStatus=401`)后自动刷新,成功后只重试一次;内置登录、注册和配置的刷新路径除外,403/500 不刷新。启用 transport 时刷新和重试复用加密链路。
655
+ oRPC contract 输入校验在 procedure 之前失败时返回 HTTP 400,本地化失败体包含 `error.httpStatus=400` 和字段错误;procedure 内 `serv.validateInputOrFail` 返回的失败仍为 HTTP 200。HTTP middleware、refresh-cookie、OpenAPI 权限与 transport 协议错误另按自身契约返回真实 HTTP 状态。api-client 在真实 HTTP 401 或 HTTP 200 的有效失败 HaiResult(`success=false`、字符串 `error.code/message`、`error.httpStatus=401`)后自动刷新,成功后只重试一次;内置登录、注册和配置的刷新路径除外,403/500 不刷新。启用 transport 时刷新和重试复用加密链路。
643
656
 
644
657
  - Default procedures 全部返回 `HaiResult<T>`,客户端直接判断 `result.success`
645
658
  - 认证失败 → `HaiCommonError.UNAUTHORIZED`(由 `.auth()` 按 `context.locale` 本地化)
@@ -730,7 +743,7 @@ const app = serv.createApp({
730
743
 
731
744
  原生 Audio WebSocket 同样使用安全错误边界:内部 Provider/SDK 错误与堆栈只进入脱敏日志,客户端收到稳定错误码、安全提示和 traceId。自定义跨域配置需保留敏感操作使用的 X-Hai-Reauthentication;默认 Serv/Kit 已允许。
732
745
 
733
- HTTP 错误只向用户返回本地化的安全提示,末尾显示 `(traceId: …)`;响应同时包含 `error.traceId` 和 `X-Trace-Id`。框架复用本次 requestId 作为 traceId,前后端日志自动关联。底层异常、堆栈和操作上下文经凭据脱敏后写入日志,不通过 cause/ext 向用户公开。字段校验只返回本地化的字段名和错误。
746
+ HTTP 错误返回本地化安全文案及追踪编号;ToastContainer 默认隐藏编号,由排查信息图标展开和复制。响应同时包含 `error.traceId` 和 `X-Trace-Id`。框架复用本次 requestId 作为 traceId,前后端日志自动关联。底层异常、堆栈和操作上下文经凭据脱敏后写入日志,不通过 cause/ext 向用户公开。字段校验只返回本地化的字段名和错误。
734
747
 
735
748
  `core.error.toPublic(error, traceId?)` 用于接口/UI 边界;`core.error.formatMessage(error)` 用于重新翻译后的提示,重复调用不会重复追加标识。日志传入完整 Error 对象,不能只记录通用标题或提前 String(error) 丢失上下文。数据库失败记录驱动错误码、消息、堆栈及表/列;SQL 原文、绑定值和连接凭据不进入日志。
736
749
 
@@ -6,11 +6,13 @@ import { audit } from '@h-ai/audit';
6
6
 
7
7
  // messages/en-US.json
8
8
  var en_US_default = {
9
+ serv_transportPlaintextRequired: "This endpoint accepts unencrypted application data. Disable transport encryption for this request and retry over HTTPS.",
9
10
  $schema: "https://inlang.com/schema/inlang-message-format",
10
11
  serv_validationFailed: "Validation failed",
11
12
  serv_validationRequired: "This field is required",
12
13
  serv_validationInvalid: "Invalid input format",
13
14
  serv_validationInvalidType: "Invalid field type",
15
+ serv_validationUnexpectedFields: "The submitted data contains unsupported fields. Refresh the page and try again.",
14
16
  serv_validationStringMin: "Enter at least {min} characters",
15
17
  serv_validationStringMax: "Enter at most {max} UTF-8 bytes",
16
18
  serv_validationNumberMin: "Value must be at least {min}",
@@ -39,16 +41,19 @@ var en_US_default = {
39
41
  serv_transportEncryptFailed: "Failed to encrypt response",
40
42
  serv_transportKeyExchangeFailed: "Key exchange failed",
41
43
  serv_transportBusy: "Material processing is busy. Please retry shortly.",
42
- serv_feedbackUnavailable: "Feedback is temporarily unavailable. Business features remain available."
44
+ serv_feedbackUnavailable: "Feedback is temporarily unavailable. Business features remain available.",
45
+ serv_invalidApiPrefix: "API prefix must be a valid absolute path without empty segments, queries, fragments or traversal."
43
46
  };
44
47
 
45
48
  // messages/ja-JP.json
46
49
  var ja_JP_default = {
50
+ serv_transportPlaintextRequired: "\u3053\u306E\u30A8\u30F3\u30C9\u30DD\u30A4\u30F3\u30C8\u306F\u30A2\u30D7\u30EA\u30B1\u30FC\u30B7\u30E7\u30F3\u5C64\u3067\u6697\u53F7\u5316\u3055\u308C\u3066\u3044\u306A\u3044\u30C7\u30FC\u30BF\u3092\u53D7\u3051\u4ED8\u3051\u307E\u3059\u3002\u30EA\u30AF\u30A8\u30B9\u30C8\u306E\u4F1D\u9001\u6697\u53F7\u5316\u3092\u7121\u52B9\u306B\u3057\u3001HTTPS \u3067\u518D\u8A66\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
47
51
  $schema: "https://inlang.com/schema/inlang-message-format",
48
52
  serv_validationFailed: "\u5165\u529B\u5185\u5BB9\u306E\u691C\u8A3C\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
49
53
  serv_validationRequired: "\u3053\u306E\u9805\u76EE\u306F\u5FC5\u9808\u3067\u3059",
50
54
  serv_validationInvalid: "\u5165\u529B\u5F62\u5F0F\u304C\u7121\u52B9\u3067\u3059",
51
55
  serv_validationInvalidType: "\u30D5\u30A3\u30FC\u30EB\u30C9\u306E\u578B\u304C\u7121\u52B9\u3067\u3059",
56
+ serv_validationUnexpectedFields: "\u9001\u4FE1\u30C7\u30FC\u30BF\u306B\u3053\u306E\u64CD\u4F5C\u3067\u4F7F\u7528\u3067\u304D\u306A\u3044\u9805\u76EE\u304C\u542B\u307E\u308C\u3066\u3044\u307E\u3059\u3002\u30DA\u30FC\u30B8\u3092\u66F4\u65B0\u3057\u3066\u518D\u5EA6\u304A\u8A66\u3057\u304F\u3060\u3055\u3044\u3002",
52
57
  serv_validationStringMin: "{min}\u6587\u5B57\u4EE5\u4E0A\u3067\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044",
53
58
  serv_validationStringMax: "UTF-8 \u3067 {max} \u30D0\u30A4\u30C8\u4EE5\u5185\u3067\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044",
54
59
  serv_validationNumberMin: "{min}\u4EE5\u4E0A\u306E\u5024\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044",
@@ -77,16 +82,19 @@ var ja_JP_default = {
77
82
  serv_transportEncryptFailed: "\u30EC\u30B9\u30DD\u30F3\u30B9\u306E\u6697\u53F7\u5316\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
78
83
  serv_transportKeyExchangeFailed: "\u9375\u4EA4\u63DB\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
79
84
  serv_transportBusy: "\u8CC7\u6599\u306E\u51E6\u7406\u304C\u6DF7\u307F\u5408\u3063\u3066\u3044\u307E\u3059\u3002\u3057\u3070\u3089\u304F\u3057\u3066\u304B\u3089\u518D\u8A66\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
80
- serv_feedbackUnavailable: "\u30D5\u30A3\u30FC\u30C9\u30D0\u30C3\u30AF\u306F\u4E00\u6642\u7684\u306B\u5229\u7528\u3067\u304D\u307E\u305B\u3093\u3002\u696D\u52D9\u6A5F\u80FD\u306F\u5F15\u304D\u7D9A\u304D\u5229\u7528\u3067\u304D\u307E\u3059\u3002"
85
+ serv_feedbackUnavailable: "\u30D5\u30A3\u30FC\u30C9\u30D0\u30C3\u30AF\u306F\u4E00\u6642\u7684\u306B\u5229\u7528\u3067\u304D\u307E\u305B\u3093\u3002\u696D\u52D9\u6A5F\u80FD\u306F\u5F15\u304D\u7D9A\u304D\u5229\u7528\u3067\u304D\u307E\u3059\u3002",
86
+ serv_invalidApiPrefix: "API \u63A5\u982D\u8F9E\u306F\u7A7A\u306E\u533A\u9593\u30FB\u30AF\u30A8\u30EA\u30FB\u30D5\u30E9\u30B0\u30E1\u30F3\u30C8\u30FB\u76F8\u5BFE\u79FB\u52D5\u3092\u542B\u307E\u306A\u3044\u7D76\u5BFE\u30D1\u30B9\u306B\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
81
87
  };
82
88
 
83
89
  // messages/zh-CN.json
84
90
  var zh_CN_default = {
91
+ serv_transportPlaintextRequired: "\u6B64\u63A5\u53E3\u63A5\u6536\u672A\u52A0\u5BC6\u7684\u5E94\u7528\u6570\u636E\uFF0C\u8BF7\u5173\u95ED\u4F20\u8F93\u52A0\u5BC6\u540E\u901A\u8FC7 HTTPS \u91CD\u8BD5\u3002",
85
92
  $schema: "https://inlang.com/schema/inlang-message-format",
86
93
  serv_validationFailed: "\u6570\u636E\u9A8C\u8BC1\u5931\u8D25",
87
94
  serv_validationRequired: "\u6B64\u5B57\u6BB5\u5FC5\u586B",
88
95
  serv_validationInvalid: "\u8F93\u5165\u683C\u5F0F\u65E0\u6548",
89
96
  serv_validationInvalidType: "\u5B57\u6BB5\u7C7B\u578B\u4E0D\u6B63\u786E",
97
+ serv_validationUnexpectedFields: "\u63D0\u4EA4\u7684\u6570\u636E\u5305\u542B\u5F53\u524D\u64CD\u4F5C\u4E0D\u652F\u6301\u7684\u5185\u5BB9\uFF0C\u8BF7\u5237\u65B0\u9875\u9762\u540E\u91CD\u8BD5\u3002",
90
98
  serv_validationStringMin: "\u81F3\u5C11\u8F93\u5165 {min} \u4E2A\u5B57\u7B26",
91
99
  serv_validationStringMax: "\u6700\u591A\u8F93\u5165 {max} \u4E2A UTF-8 \u5B57\u8282",
92
100
  serv_validationNumberMin: "\u6570\u503C\u4E0D\u80FD\u5C0F\u4E8E {min}",
@@ -115,7 +123,8 @@ var zh_CN_default = {
115
123
  serv_transportEncryptFailed: "\u54CD\u5E94\u52A0\u5BC6\u5931\u8D25",
116
124
  serv_transportKeyExchangeFailed: "\u5BC6\u94A5\u534F\u5546\u5931\u8D25",
117
125
  serv_transportBusy: "\u6750\u6599\u5904\u7406\u7E41\u5FD9\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
118
- serv_feedbackUnavailable: "\u95EE\u9898\u53CD\u9988\u6682\u4E0D\u53EF\u7528\uFF0C\u4E1A\u52A1\u529F\u80FD\u53EF\u7EE7\u7EED\u4F7F\u7528"
126
+ serv_feedbackUnavailable: "\u95EE\u9898\u53CD\u9988\u6682\u4E0D\u53EF\u7528\uFF0C\u4E1A\u52A1\u529F\u80FD\u53EF\u7EE7\u7EED\u4F7F\u7528",
127
+ serv_invalidApiPrefix: "API \u524D\u7F00\u5FC5\u987B\u662F\u5408\u6CD5\u7684\u7EDD\u5BF9\u8DEF\u5F84\uFF0C\u4E0D\u80FD\u5305\u542B\u7A7A\u8DEF\u5F84\u6BB5\u3001\u67E5\u8BE2\u3001\u7247\u6BB5\u6216\u8DEF\u5F84\u7A7F\u8D8A\u3002"
119
128
  };
120
129
 
121
130
  // src/serv-i18n.ts
@@ -397,5 +406,5 @@ function getHandlerMethod(value) {
397
406
  }
398
407
 
399
408
  export { WILDCARD_PERMISSION, WILDCARD_ROLE, buildHaiErrorBody, implement, normalizeServLocale, servM };
400
- //# sourceMappingURL=chunk-JAQIEQJS.js.map
401
- //# sourceMappingURL=chunk-JAQIEQJS.js.map
409
+ //# sourceMappingURL=chunk-FEG22LCX.js.map
410
+ //# sourceMappingURL=chunk-FEG22LCX.js.map