@h-ai/serv 1.0.0-beta.2 → 1.0.0-beta.3

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
 
@@ -355,7 +355,7 @@ Execution order is fixed:
355
355
  2. Built-in CORS response wrapping; access checks for key exchange and preflight.
356
356
  3. Built-in `transport`, if enabled, and request access checks.
357
357
  4. Your `middlewares`.
358
- 5. health / refresh-cookie / OpenAPI / RPC / docs / oRPC routes
358
+ 5. health / refresh-cookie / OpenAPI / docs / oRPC routes
359
359
 
360
360
  Notes:
361
361
 
@@ -426,7 +426,7 @@ Public API boundaries:
426
426
 
427
427
  - `src/pipelines/*` holds internal defaults and is not a public subpath API.
428
428
  - The stable application-facing entry remains `@h-ai/serv`.
429
- - 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.
430
430
 
431
431
  #### httpOnly cookie authentication (recommended for production)
432
432
 
@@ -489,7 +489,7 @@ await apiClient.init({
489
489
 
490
490
  ### API overview
491
491
 
492
- - `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.
493
493
  - `serv.parseRequestContext({ request })`: extract Bearer token + requestId without populating session.
494
494
  - `serv.listen(app, options)`: start Node.js HTTP service; return `{ server, address, close }`.
495
495
  - `serv.toFetch(app)`: wrap as a standard `fetch(Request)` handler.
@@ -574,7 +574,6 @@ http:
574
574
  health:
575
575
  path: /health
576
576
  readyPath: /ready
577
- rpc: false
578
577
 
579
578
  cors:
580
579
  origin: https://app.example.com
@@ -601,11 +600,12 @@ transport:
601
600
 
602
601
  `ServHttpConfigInput`:
603
602
 
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
+
604
605
  - `apiPrefix`: defaults to `/api/v1`.
605
606
  - `health`: defaults to `{ path: '/health', readyPath: '/ready' }`.
606
607
  - `openapi`: defaults to `false`; enable with `{ path: '/openapi.json' }`.
607
- - `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.
608
- - `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.
609
609
  - `cors`: configured origins, credentials, allowedHeaders, and exposedHeaders; mount through `serv.cors(...)`.
610
610
  - `transport`: top-level `config/_serv.yml` option passed as `servConfig.transport` to `createApp({ transport })`; default key exchange subpath is `/_hai/key-exchange`.
611
611
 
@@ -652,7 +652,7 @@ createIamProcedures({ iam }) implements public config, password/LDAP/OTP login,
652
652
 
653
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.
654
654
 
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/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.
656
656
 
657
657
  - All default procedures return `HaiResult<T>`; clients check `result.success`.
658
658
  - Authentication failure → `HaiCommonError.UNAUTHORIZED`, localized by `.auth()` using `context.locale`.
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
 
@@ -355,7 +355,7 @@ const app = serv.createApp({
355
355
  2. 組み込み CORS 応答処理;鍵交換と preflight のアクセス判定。
356
356
  3. 組み込み `transport`(有効時)と各リクエストのアクセス判定。
357
357
  4. 独自の `middlewares`。
358
- 5. health / refresh-cookie / OpenAPI / RPC / docs / oRPC routes
358
+ 5. health / refresh-cookie / OpenAPI / docs / oRPC routes
359
359
 
360
360
  補足:
361
361
 
@@ -426,7 +426,7 @@ const procedures = serv
426
426
 
427
427
  - `src/pipelines/*` は内部の既定実装で、公開サブパス API ではありません。
428
428
  - アプリ向けの安定した入口はルートの `@h-ai/serv`。
429
- - `securityHeaders` / `requireInternalRPC` / `buildHaiErrorBody` などの内部 helper は独立サブパスとして公開しません。
429
+ - `securityHeaders` / `buildHaiErrorBody` などの内部 helper は独立サブパスとして公開しません。
430
430
 
431
431
  #### httpOnly Cookie 認証(本番推奨)
432
432
 
@@ -489,7 +489,7 @@ await apiClient.init({
489
489
 
490
490
  ### API 一覧
491
491
 
492
- - `serv.createApp(options)`:独自 middleware、ヘルスチェック、OpenAPI、任意の文書・RPC を備えた HTTP App を作成。
492
+ - `serv.createApp(options)`:独自 middleware、ヘルスチェック、OpenAPI、任意の文書を備えた HTTP App を作成。
493
493
  - `serv.parseRequestContext({ request })`:Bearer token + requestId を抽出し、session は設定しません。
494
494
  - `serv.listen(app, options)`:Node.js HTTP サービスを起動し、`{ server, address, close }` を返す。
495
495
  - `serv.toFetch(app)`:標準の `fetch(Request)` handler に変換。
@@ -574,7 +574,6 @@ http:
574
574
  health:
575
575
  path: /health
576
576
  readyPath: /ready
577
- rpc: false
578
577
 
579
578
  cors:
580
579
  origin: https://app.example.com
@@ -601,11 +600,12 @@ transport:
601
600
 
602
601
  `ServHttpConfigInput`:
603
602
 
603
+ ネイティブ RPC 入口、`HAI_SERV_HTTP_RPC`、`ServRpcHttpConfig` は削除しました。既存の OpenAPI/REST API、`api-client`、oRPC contract と procedure は引き続き利用できます。
604
+
604
605
  - `apiPrefix`:既定 `/api/v1`。
605
606
  - `health`:既定 `{ path: '/health', readyPath: '/ready' }`。
606
607
  - `openapi`:既定 `false`。`{ path: '/openapi.json' }` で有効化。
607
- - `docs`:既定 `false`。`{ path: '/docs' }` で有効化(`openapi` が必要)。`@scalar/api-reference` の browser bundle から `/_hai/scalar.js` を提供し、外部 CDN なしで Scalar UI を使えます。
608
- - `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` は後端で検証します。
609
609
  - `cors`:origin・credentials・allowedHeaders・exposedHeaders を設定し、`serv.cors(...)` で接続。
610
610
  - `transport`:`config/_serv.yml` のトップレベル設定。`servConfig.transport` を `createApp({ transport })` に渡し、既定の鍵交換サブパスは `/_hai/key-exchange`。
611
611
 
@@ -652,7 +652,7 @@ createIamProcedures({ iam }) は公開設定、LDAP/OTP/パスワード、登録
652
652
 
653
653
  通常の procedure は認証・認可・業務の失敗と捕捉した handler 例外を HaiResult として既定で HTTP 200 で返します。`error.httpStatus` はドメインエラーのメタデータで、HTTP 応答ステータスを変更しません。クライアントは `success` と `error.code` を確認してください。未認証では `error.httpStatus=401`、DB クエリー失敗や INTERNAL_ERROR では 500 です。
654
654
 
655
- 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 を使用します。
656
656
 
657
657
  - 既定 procedures はすべて `HaiResult<T>` を返し、クライアントで `result.success` を確認。
658
658
  - 認証失敗 → `HaiCommonError.UNAUTHORIZED`。`.auth()` が `context.locale` でローカライズ。
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
 
@@ -355,7 +355,7 @@ const app = serv.createApp({
355
355
  2. 内置 CORS 响应包装;密钥交换和预检先检查访问策略
356
356
  3. 内置 `transport`(若启用)及每次请求访问策略
357
357
  4. 你的 `middlewares`
358
- 5. health / refresh-cookie / OpenAPI / RPC / docs / oRPC routes
358
+ 5. health / refresh-cookie / OpenAPI / docs / oRPC routes
359
359
 
360
360
  说明:
361
361
 
@@ -426,7 +426,7 @@ const procedures = serv
426
426
 
427
427
  - `src/pipelines/*` 是 `@h-ai/serv` 的内部默认实现目录,不作为公开子路径 API 承诺
428
428
  - 对应用层公开的稳定入口仍然是根入口 `@h-ai/serv`
429
- - `securityHeaders` / `requireInternalRPC` / `buildHaiErrorBody` 等内部 helper/middleware 不作为单独子路径文档暴露
429
+ - `securityHeaders` / `buildHaiErrorBody` 等内部 helper/middleware 不作为单独子路径文档暴露
430
430
 
431
431
  #### httpOnly Cookie 认证(推荐生产方案)
432
432
 
@@ -489,7 +489,7 @@ await apiClient.init({
489
489
 
490
490
  ### API 概览
491
491
 
492
- - `serv.createApp(options)`:创建 HTTP App 抽象,挂载自定义 `middlewares`、健康检查、OpenAPI handler、可选文档与 RPC
492
+ - `serv.createApp(options)`:创建 HTTP App 抽象,挂载自定义 `middlewares`、健康检查、OpenAPI handler 与可选文档
493
493
  - `serv.parseRequestContext({ request })`:默认上下文解析(提取 Bearer token + requestId,不填充 session)
494
494
  - `serv.listen(app, options)`:在 Node.js 启动 HTTP 服务,返回 `{ server, address, close }`
495
495
  - `serv.toFetch(app)`:包装为标准 `fetch(Request)` handler
@@ -574,7 +574,6 @@ http:
574
574
  health:
575
575
  path: /health
576
576
  readyPath: /ready
577
- rpc: false
578
577
 
579
578
  cors:
580
579
  origin: https://app.example.com
@@ -601,11 +600,12 @@ transport:
601
600
 
602
601
  `ServHttpConfigInput`:
603
602
 
603
+ 原生 RPC 入口及 `HAI_SERV_HTTP_RPC`、`ServRpcHttpConfig` 已删除。业务继续使用现有 OpenAPI/REST 路径和 `api-client`,底层 oRPC contract 与 procedure 保留。
604
+
604
605
  - `apiPrefix`:默认 `/api/v1`
605
606
  - `health`:默认 `{ path: '/health', readyPath: '/ready' }`
606
607
  - `openapi`:默认 `false`;启用:`{ path: '/openapi.json' }`
607
- - `docs`:默认 `false`;启用:`{ path: '/docs' }`(依赖 `openapi`);启用后自动挂载 `/_hai/scalar.js`,从 `@scalar/api-reference` 的 browser bundle 提供本地 Scalar UI 脚本,无需外网 CDN
608
- - `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` 仍由后端校验。
609
609
  - `cors`:配置文件中的跨端 origin、credentials、allowedHeaders 与 exposedHeaders;应用通过 `serv.cors(...)` 装配。
610
610
  - `transport`:`config/_serv.yml` 顶层配置;启用后可通过 `servConfig.transport` 装配到 `createApp({ transport })`,默认密钥协商子路径 `/_hai/key-exchange`
611
611
 
@@ -652,7 +652,7 @@ const app = serv.createApp({
652
652
 
653
653
  普通 procedure 的认证、授权、业务失败和被捕获的 handler 异常都作为 `HaiResult` 输出,默认通过 HTTP 200 返回;`error.httpStatus` 是领域错误元数据,不会自动改写响应状态。客户端必须检查 `success` 与 `error.code`。例如未登录为 `error.httpStatus=401`,数据库查询失败或 INTERNAL_ERROR 为 500。
654
654
 
655
- 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 时刷新和重试复用加密链路。
656
656
 
657
657
  - Default procedures 全部返回 `HaiResult<T>`,客户端直接判断 `result.success`
658
658
  - 认证失败 → `HaiCommonError.UNAUTHORIZED`(由 `.auth()` 按 `context.locale` 本地化)
package/dist/index.d.ts CHANGED
@@ -138,7 +138,7 @@ type ServProcedureHandler<TInput, TOutput, TContext extends ServContext = ServCo
138
138
  /**
139
139
  * @h-ai/serv — HTTP 配置
140
140
  *
141
- * 提供用于控制 API 前缀、OpenAPI、docs、健康检查、内部 RPC 与传输加密策略的配置结构,
141
+ * 提供用于控制 API 前缀、OpenAPI、docs、健康检查与传输加密策略的配置结构,
142
142
  * 并由 zod 负责默认值填充与边界校验。
143
143
  * @module serv-config
144
144
  */
@@ -217,24 +217,6 @@ interface ServHealthHttpConfig {
217
217
  readonly path: `/${string}`;
218
218
  readonly readyPath?: `/${string}`;
219
219
  }
220
- /**
221
- * 内部 RPC endpoint 配置。
222
- *
223
- * @example
224
- * ```ts
225
- * import type { ServRpcHttpConfig } from '@h-ai/serv'
226
- * import { serv } from '@h-ai/serv'
227
- *
228
- * const rpc: ServRpcHttpConfig = { prefix: '/rpc', access: 'loopback' }
229
- * // 缺少可信连接信息时拒绝;跨节点来源通过明确网关/网络策略控制。
230
- * ```
231
- */
232
- interface ServRpcHttpConfig {
233
- readonly prefix: `/${string}`;
234
- readonly access: 'loopback' | 'private-network' | 'gateway-only';
235
- readonly gatewayHeader?: string;
236
- readonly gatewaySecret?: string;
237
- }
238
220
  /**
239
221
  * 传输加密运行时配置(对应 `config/_serv.yml`)。
240
222
  *
@@ -281,7 +263,7 @@ interface ServCorsRuntimeConfig {
281
263
  * import type { ServHttpConfig } from '@h-ai/serv'
282
264
  * import { serv } from '@h-ai/serv'
283
265
  *
284
- * const http: ServHttpConfig = { trustedProxies: 'auto', access: false, apiPrefix: '/api/v1', openapi: false, docs: false, health: { path: '/health' }, rpc: false }
266
+ * const http: ServHttpConfig = { trustedProxies: 'auto', access: false, apiPrefix: '/api/v1', openapi: false, docs: false, health: { path: '/health' } }
285
267
  * ```
286
268
  */
287
269
  interface ServHttpConfig {
@@ -293,7 +275,6 @@ interface ServHttpConfig {
293
275
  readonly openapi: false | ServOpenAPIHttpConfig;
294
276
  readonly docs: false | ServDocsHttpConfig;
295
277
  readonly health: false | ServHealthHttpConfig;
296
- readonly rpc: false | ServRpcHttpConfig;
297
278
  }
298
279
  /**
299
280
  * `config/_serv.yml` 的顶层配置结构。
@@ -363,7 +344,6 @@ type ServHttpConfigInput = Partial<{
363
344
  readonly openapi: false | Partial<ServOpenAPIHttpConfig>;
364
345
  readonly docs: false | Partial<ServDocsHttpConfig>;
365
346
  readonly health: false | Partial<ServHealthHttpConfig>;
366
- readonly rpc: false | Partial<ServRpcHttpConfig>;
367
347
  }>;
368
348
  /**
369
349
  * `config/_serv.yml` 的顶层输入类型。
@@ -459,16 +439,6 @@ declare const ServConfigSchema: z.ZodPipe<z.ZodObject<{
459
439
  path: z.ZodDefault<z.ZodCustom<`/${string}`, `/${string}`>>;
460
440
  readyPath: z.ZodDefault<z.ZodOptional<z.ZodCustom<`/${string}`, `/${string}`>>>;
461
441
  }, z.core.$strip>]>>;
462
- rpc: z.ZodDefault<z.ZodUnion<readonly [z.ZodLiteral<false>, z.ZodObject<{
463
- prefix: z.ZodDefault<z.ZodCustom<`/${string}`, `/${string}`>>;
464
- access: z.ZodDefault<z.ZodEnum<{
465
- loopback: "loopback";
466
- "private-network": "private-network";
467
- "gateway-only": "gateway-only";
468
- }>>;
469
- gatewayHeader: z.ZodOptional<z.ZodString>;
470
- gatewaySecret: z.ZodOptional<z.ZodString>;
471
- }, z.core.$strip>]>>;
472
442
  }, z.core.$strip>>;
473
443
  cors: z.ZodOptional<z.ZodObject<{
474
444
  origin: z.ZodOptional<z.ZodString>;
@@ -535,12 +505,6 @@ declare const ServConfigSchema: z.ZodPipe<z.ZodObject<{
535
505
  path: `/${string}`;
536
506
  readyPath: `/${string}`;
537
507
  };
538
- rpc: false | {
539
- prefix: `/${string}`;
540
- access: "loopback" | "private-network" | "gateway-only";
541
- gatewayHeader?: string | undefined;
542
- gatewaySecret?: string | undefined;
543
- };
544
508
  } | undefined;
545
509
  cors?: {
546
510
  allowedHeaders: string[];
@@ -1371,4 +1335,4 @@ declare const serv: {
1371
1335
  m: (key: "serv_transportPlaintextRequired" | "$schema" | "serv_validationFailed" | "serv_validationRequired" | "serv_validationInvalid" | "serv_validationInvalidType" | "serv_validationUnexpectedFields" | "serv_validationStringMin" | "serv_validationStringMax" | "serv_validationNumberMin" | "serv_validationNumberMax" | "serv_validationArrayMin" | "serv_validationArrayMax" | "serv_validationTooSmall" | "serv_validationTooBig" | "serv_validationEmail" | "serv_validationUrl" | "serv_validationUuid" | "serv_validationEnum" | "serv_errorUnauthorized" | "serv_errorForbidden" | "serv_errorInternal" | "serv_storageKeyRequired" | "serv_storageKeyTooLong" | "serv_storageKeyIllegalChars" | "serv_storageKeyDotSegments" | "serv_iamCannotDeleteCurrentUser" | "serv_transportInvalidPayload" | "serv_transportUnknownClient" | "serv_transportClientIdRequired" | "serv_transportClientKeyNotFound" | "serv_transportDecryptFailed" | "serv_transportEncryptFailed" | "serv_transportKeyExchangeFailed" | "serv_transportBusy" | "serv_feedbackUnavailable", options?: _h_ai_core.MessageOptions) => string;
1372
1336
  };
1373
1337
 
1374
- export { type AudioTicketGrant, type AudioTicketVerification, type AuthorizedAudioRequest, type CreateServAppOptions, CreateServContext, type GenerateOpenAPISpecOptions, RefreshCookieConfig, type ServAudioConfig, type ServConfig, type ServConfigInput, ServConfigSchema, ServContext, type ServCorsConfig, type ServCorsRuntimeConfig, type ServCorsRuntimeConfigInput, type ServDocsHttpConfig, type ServFetchHandler, type ServHealthHttpConfig, type ServHttpApp, type ServHttpConfig, type ServHttpConfigInput, ServIam, type ServListenOptions, type ServMiddleware, type ServMiddlewareFactory, type ServMiddlewareMount, type ServNodeServer, type ServOpenAPIHttpConfig, type ServProcedureHandler, type ServProcedureOptions, ServRequestMetadata, type ServRpcHttpConfig, type ServRuntimeSecurityPolicy, type ServRuntimeSecurityPolicyInput, type ServServerConfig, type ServServerConfigInput, ServSession, type ServStorageAssetsConfig, type ServTransportConfig, type ServTransportRuntimeConfig, type ServTransportRuntimeConfigInput, type ServValidationFailureBody, serv };
1338
+ export { type AudioTicketGrant, type AudioTicketVerification, type AuthorizedAudioRequest, type CreateServAppOptions, CreateServContext, type GenerateOpenAPISpecOptions, RefreshCookieConfig, type ServAudioConfig, type ServConfig, type ServConfigInput, ServConfigSchema, ServContext, type ServCorsConfig, type ServCorsRuntimeConfig, type ServCorsRuntimeConfigInput, type ServDocsHttpConfig, type ServFetchHandler, type ServHealthHttpConfig, type ServHttpApp, type ServHttpConfig, type ServHttpConfigInput, ServIam, type ServListenOptions, type ServMiddleware, type ServMiddlewareFactory, type ServMiddlewareMount, type ServNodeServer, type ServOpenAPIHttpConfig, type ServProcedureHandler, type ServProcedureOptions, ServRequestMetadata, type ServRuntimeSecurityPolicy, type ServRuntimeSecurityPolicyInput, type ServServerConfig, type ServServerConfigInput, ServSession, type ServStorageAssetsConfig, type ServTransportConfig, type ServTransportRuntimeConfig, type ServTransportRuntimeConfigInput, type ServValidationFailureBody, serv };
package/dist/index.js CHANGED
@@ -2,12 +2,12 @@ import { validateInputOrFail, resolveRequestLocale, localizeZodError } from './c
2
2
  import { servM, WILDCARD_ROLE, WILDCARD_PERMISSION, implement, buildHaiErrorBody } from './chunk-I3F7RN4I.js';
3
3
  import process from 'process';
4
4
  import { upgradeWebSocket, serve } from '@hono/node-server';
5
+ import { posix } from 'path';
5
6
  import { apiContract, AUDIO_WS_PATH, AudioWsClientMessageSchema } from '@h-ai/api-contract';
6
7
  import { audit } from '@h-ai/audit';
7
8
  import { core, RequestAccessConfigSchema, TrustedProxiesSchema, HaiCommonError, err, ok } from '@h-ai/core';
8
9
  import { TRANSPORT_PLAINTEXT_RANGE, createTransportBodyPolicy, TRANSPORT_PROTOCOL, authenticateTransportRequest, TransportTimeError, createTransportErrorResponse, TransportBodyTooLargeError, readTransportBody, TRANSPORT_BODY_LIMITS, isValidSM2PublicKey } from '@h-ai/crypto';
9
10
  import { OpenAPIHandler } from '@orpc/openapi/fetch';
10
- import { RPCHandler } from '@orpc/server/fetch';
11
11
  import { Hono } from 'hono';
12
12
  import { WebSocketServer } from 'ws';
13
13
  import { Buffer } from 'buffer';
@@ -548,75 +548,6 @@ function toArrayBuffer(data) {
548
548
  new Uint8Array(buffer).set(data);
549
549
  return buffer;
550
550
  }
551
- function readRemoteAddress(metadata) {
552
- if (!metadata || typeof metadata !== "object" || !("remoteAddress" in metadata) || typeof metadata.remoteAddress !== "string")
553
- return void 0;
554
- return core.access.normalizeClientIp(metadata.remoteAddress);
555
- }
556
-
557
- // src/pipelines/serv-pipeline-require-internal-rpc.ts
558
- var pipelineLogger = core.logger.child({ module: "serv", scope: "pipeline" });
559
- var cachedGetConnInfo;
560
- async function resolveGetConnInfo() {
561
- if (cachedGetConnInfo !== void 0)
562
- return cachedGetConnInfo;
563
- try {
564
- const mod = await import('@hono/node-server/conninfo');
565
- cachedGetConnInfo = mod.getConnInfo;
566
- } catch (cause) {
567
- pipelineLogger.warn("getConnInfo unavailable in this runtime; loopback/private-network access modes will fail closed", { error: cause });
568
- cachedGetConnInfo = null;
569
- }
570
- return cachedGetConnInfo;
571
- }
572
- var LOOPBACK_IPS = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1", "localhost"]);
573
- function requireInternalRPC(config) {
574
- return async (c, next) => {
575
- const locale = resolveRequestLocale(c.req.raw.headers);
576
- if (config.access === "gateway-only") {
577
- const headerName = config.gatewayHeader ?? "x-hai-internal-rpc";
578
- const received = c.req.header(headerName);
579
- if (!config.gatewaySecret || !received || !core.string.constantTimeEqual(received, config.gatewaySecret))
580
- return c.json(buildHaiErrorBody(HaiCommonError.FORBIDDEN, servM("serv_errorForbidden", { locale })), 403);
581
- return next();
582
- }
583
- const ip = await getRequestRemoteAddress(c) ?? "";
584
- if (config.access === "loopback" && !isLoopbackIP(ip))
585
- return c.json(buildHaiErrorBody(HaiCommonError.FORBIDDEN, servM("serv_errorForbidden", { locale })), 403);
586
- if (config.access === "private-network" && !isPrivateNetworkIP(ip))
587
- return c.json(buildHaiErrorBody(HaiCommonError.FORBIDDEN, servM("serv_errorForbidden", { locale })), 403);
588
- return next();
589
- };
590
- }
591
- async function getRequestRemoteAddress(c) {
592
- const remoteAddress = readRemoteAddress(c.env);
593
- if (remoteAddress)
594
- return remoteAddress;
595
- const getConnInfo = await resolveGetConnInfo();
596
- if (!getConnInfo)
597
- return void 0;
598
- try {
599
- return getConnInfo(c).remote.address;
600
- } catch {
601
- return void 0;
602
- }
603
- }
604
- function normalizeIpAddress(ip) {
605
- return ip.startsWith("::ffff:") ? ip.slice("::ffff:".length) : ip;
606
- }
607
- function isLoopbackIP(ip) {
608
- const normalized = normalizeIpAddress(ip);
609
- return LOOPBACK_IPS.has(ip) || LOOPBACK_IPS.has(normalized);
610
- }
611
- function isPrivateNetworkIP(ip) {
612
- const normalized = normalizeIpAddress(ip);
613
- if (isLoopbackIP(ip))
614
- return true;
615
- if (normalized.startsWith("10.") || normalized.startsWith("192.168."))
616
- return true;
617
- const second = Number(normalized.split(".")[1]);
618
- return normalized.startsWith("172.") && Number.isInteger(second) && second >= 16 && second <= 31;
619
- }
620
551
 
621
552
  // src/pipelines/serv-pipeline-security-headers.ts
622
553
  function securityHeaders() {
@@ -627,6 +558,11 @@ function securityHeaders() {
627
558
  await next();
628
559
  };
629
560
  }
561
+ function readRemoteAddress(metadata) {
562
+ if (!metadata || typeof metadata !== "object" || !("remoteAddress" in metadata) || typeof metadata.remoteAddress !== "string")
563
+ return void 0;
564
+ return core.access.normalizeClientIp(metadata.remoteAddress);
565
+ }
630
566
  var ApiPrefixSchema = z.custom(
631
567
  (value) => typeof value === "string" && value.startsWith("/api/"),
632
568
  "apiPrefix must start with /api/"
@@ -653,15 +589,6 @@ var HealthHttpConfigInputSchema = z.union([
653
589
  readyPath: AbsolutePathSchema.optional().default("/ready")
654
590
  })
655
591
  ]);
656
- var RpcHttpConfigInputSchema = z.union([
657
- z.literal(false),
658
- z.object({
659
- prefix: AbsolutePathSchema.default("/rpc"),
660
- access: z.enum(["loopback", "private-network", "gateway-only"]).default("loopback"),
661
- gatewayHeader: z.string().optional(),
662
- gatewaySecret: z.string().optional()
663
- })
664
- ]);
665
592
  var TransportRuntimeConfigInputSchema = z.union([
666
593
  z.literal(false),
667
594
  z.object({
@@ -688,8 +615,7 @@ var ServHttpConfigInputSchema = z.object({
688
615
  apiPrefix: ApiPrefixSchema.default("/api/v1"),
689
616
  openapi: OpenAPIHttpConfigInputSchema.default(false),
690
617
  docs: DocsHttpConfigInputSchema.default(false),
691
- health: HealthHttpConfigInputSchema.default({ path: "/health", readyPath: "/ready" }),
692
- rpc: RpcHttpConfigInputSchema.default(false)
618
+ health: HealthHttpConfigInputSchema.default({ path: "/health", readyPath: "/ready" })
693
619
  });
694
620
  var ServConfigSchema = z.object({
695
621
  server: ServerConfigInputSchema.optional(),
@@ -1402,14 +1328,13 @@ function createApp(options) {
1402
1328
  audioWsServers.set(app, new WebSocketServer({ noServer: true }));
1403
1329
  }
1404
1330
  mountOpenAPIRoutes(app, options.procedures, http.apiPrefix, createContext);
1405
- if (http.rpc !== false)
1406
- mountRPCRoutes(app, options.procedures, http.rpc, createContext);
1407
1331
  if (http.openapi !== false) {
1408
1332
  app.get(http.openapi.path, async (c) => c.json(await getSpec()));
1409
1333
  }
1410
1334
  if (http.docs !== false) {
1411
1335
  const docs = http.docs;
1412
- app.get(SCALAR_ROUTE, async (c) => {
1336
+ const docsDirectory = docs.path.slice(0, docs.path.lastIndexOf("/") + 1);
1337
+ app.get(`${docsDirectory}${SCALAR_ROUTE.slice(1)}`, async (c) => {
1413
1338
  const content = await getScalarScript();
1414
1339
  if (!content)
1415
1340
  return c.notFound();
@@ -1423,8 +1348,8 @@ function createApp(options) {
1423
1348
  if (!context.session)
1424
1349
  return c.json(buildHaiErrorBody(HaiCommonError.UNAUTHORIZED, servM("serv_errorUnauthorized", { locale: resolveRequestLocale(c.req.raw.headers) })), 401);
1425
1350
  }
1426
- const specUrl = http.openapi === false ? void 0 : http.openapi.path;
1427
- return c.html(createDocsPage(await getSpec(), { specUrl }));
1351
+ const specUrl = http.openapi === false ? void 0 : posix.relative(docsDirectory, http.openapi.path) || ".";
1352
+ return c.html(createDocsPage(await getSpec(), { specUrl, scriptUrl: SCALAR_ROUTE.slice(1) }));
1428
1353
  });
1429
1354
  }
1430
1355
  return app;
@@ -1442,12 +1367,6 @@ function mountOpenAPIRoutes(app, procedures, apiPrefix, createContext) {
1442
1367
  const handler = new OpenAPIHandler(procedures);
1443
1368
  app.on([...API_METHODS], `${apiPrefix}/*`, (c, next) => handleORPC(c, next, apiPrefix, handler, createContext));
1444
1369
  }
1445
- function mountRPCRoutes(app, procedures, rpcConfig, createContext) {
1446
- const handler = new RPCHandler(procedures);
1447
- const prefix = rpcConfig.prefix;
1448
- app.use(`${prefix}/*`, requireInternalRPC(rpcConfig));
1449
- app.on([...API_METHODS], `${prefix}/*`, (c, next) => handleORPC(c, next, prefix, handler, createContext));
1450
- }
1451
1370
  async function handleORPC(c, next, prefix, handler, createContext) {
1452
1371
  const context = await createContext({ request: c.req.raw, remoteAddress: readRemoteAddress(c.env) });
1453
1372
  const { matched, response } = await core.context.run({ requestId: context.requestId, userId: context.session?.userId, scopeId: context.scopeId }, () => handler.handle(c.req.raw, { prefix, context }));