@h-ai/serv 0.1.0-alpha.66 → 0.1.0-alpha.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,9 +4,11 @@
4
4
 
5
5
  [框架首页](../../README.md) · 相关模块:[@h-ai/ai](../ai/README.md) · [@h-ai/api-contract](../api-contract/README.md) · [@h-ai/core](../core/README.md) · [@h-ai/crypto](../crypto/README.md) · [@h-ai/iam](../iam/README.md) · [@h-ai/storage](../storage/README.md)
6
6
 
7
- > hai-framework 的 API 服务运行时:对外暴露稳定的 HTTP App 抽象,把 `@h-ai/api-contract` 契约挂载为跨端可访问的 HTTP API;Hono 仅作为内部运行时实现。
7
+ ## 简介
8
8
 
9
- ## 能力概览
9
+ hai-framework 的 API 服务运行时:对外暴露稳定的 HTTP App 抽象,把 `@h-ai/api-contract` 契约挂载为跨端可访问的 HTTP API;Hono 仅作为内部运行时实现。
10
+
11
+ ## 核心功能清单
10
12
 
11
13
  - 一行启动:`serv.createApp(...)` → `serv.listen(app, { port })`,应用代码不需要 import Hono
12
14
  - 扁平 API(最小知识):`serv.createApp / serv.implement / serv.listen / serv.toFetch / serv.generateSpec / ...`
@@ -17,12 +19,14 @@
17
19
  - `build()` 同步返回普通 router,可按嵌套属性选取默认 feature procedures;挂载的 contract 与所选 procedures 保持一致,未选入口不开放。
18
20
  - 缺失路由返回本地化的 HaiResult JSON 404,启用传输加密后保留 404,不误报为非 JSON 响应导致的 500。
19
21
  - 内置安全响应头、健康检查、可选 OpenAPI JSON、可选 Scalar 文档页、可选内部 RPC endpoint
20
- - 可选传输加密:`serv.createApp({ transport: { crypto } })` 自动挂载密钥协商与请求/响应加解密
22
+ - 可选传输加密:`serv.createApp({ transport: { crypto, serverKeyRing, keyStore } })` 自动挂载密钥协商与请求/响应加解密
21
23
  - 可选语音入口:`serv.createApp({ audio: { ai, verifyTicket, authorize } })` 挂载统一语音 WebSocket;连接建立即用一次性 ticket 鉴权(返回 `{ session, grant? }`,独立预鉴权超时),每帧运行时 Zod 校验 + 严格状态机,输入/积压/发送缓冲背压与级联取消;授权回调负责服务端确认操作、模型、音色、配额与并发占用
22
24
 
23
- > 生命周期说明:`@h-ai/serv` 本身是无状态 HTTP App 装配器,不需要 `init()` / `close()`;请在创建 app 前初始化 `iam` / `storage` / `ai` / `crypto` 等依赖,并在 `serv.listen(..., { onClose })` 中反向释放它们。`serv.createApp()` 遇到启动期配置错误(例如 transport manager 创建失败)会 fail-fast 抛出,便于进程启动阶段暴露问题。
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 仅关闭 HTTP,业务模块由宿主另行反序释放。
26
+
27
+ ## 使用入门
24
28
 
25
- ## 快速开始
29
+ ### 快速开始
26
30
 
27
31
  ```ts
28
32
  import { apiContract } from '@h-ai/api-contract'
@@ -37,7 +41,11 @@ const contract = apiContract.create({ iam: apiContract.iam, storage: apiContract
37
41
  // 2) 组装 procedures(注入 iam / storage / ai 已初始化的 functions)
38
42
  const procedures = {
39
43
  iam: createIamProcedures({ iam }),
40
- storage: createStorageProcedures({ storage }),
44
+ storage: createStorageProcedures({
45
+ storage,
46
+ authorize: ({ context, keys }) => keys.every(key =>
47
+ key.startsWith(`users/${encodeURIComponent(context.session.userId)}/`)),
48
+ }),
41
49
  ai: createAiProcedures({ ai }),
42
50
  }
43
51
 
@@ -64,7 +72,15 @@ serv.listen(app, {
64
72
  // 5) 优雅关闭由 serv 托管,无需手动注册 process.once('SIGINT', ...)
65
73
  ```
66
74
 
67
- ### host / port 配置
75
+ Storage 默认 procedures 必须注入 `authorize({ context, operation, keys })`;未提供策略时默认拒绝。策略从已认证 session 确定资源范围,并覆盖下载、上传、列表、元数据和删除。上例只允许 `users/<编码后的 userId>/` 下的 key,列表同样必须携带该用户前缀;批量中任何一个 key 未获授权时全部不执行。管理员访问须由应用显式授权。
76
+
77
+ 批量删除 `storage.files.deleteMany` 成功时返回 `{ deletedKeys, failures: [] }`;失败进度位于 `error.ext.deleteMany`(`failed / unknown / notAttempted`),不能只根据 HTTP 200 判断完成。详见 [Storage 批量删除](../storage/README.md#api-概览)。
78
+
79
+ AI 默认 procedures 从可信 `context.session.userId` 绑定 `objectId`,客户端可省略该字段。聊天与消息写入、历史、会话列表和记忆 recall/list 均使用该主体;会话按 `objectId + sessionId` 隔离,写入未指定 sessionId 时使用 `default`。传入其它 objectId 默认返回 FORBIDDEN,管理员跨主体访问也必须显式传入 `authorizeObject` 并根据可信权限及租户关系授权。
80
+
81
+ `chats.createCompletion`(`POST /ai/chats/completions`)只返回一次非流式 `HaiResult<AiChatCompletionData>`。完整 function `tools`(含嵌套 JSON Schema、`strict`)和 `tool_choice`(`auto` / `none` / `required` / 指定名称)传递到模型;消息保留角色、`name`、用户图片和工具调用。工具结果必须使用 `role: 'tool'` 与非空 `tool_call_id`,指定工具选择须引用本次提供的工具。默认 procedure 不自动执行工具,应用授权并执行后,将助手的 `tool_calls` 与对应工具结果一并回传。输入不提供 `stream` 字段,传入 `true` 或 `false` 均在 HTTP 边界返回 400;需要流式时由应用使用 `ai.llm.chatStream()` 定义匹配流输出的独立 endpoint。
82
+
83
+ #### host / port 配置
68
84
 
69
85
  host/port 统一由 `config/_serv.yml` 的 `server` 提供,优先级:`HOST` / `PORT` 环境变量 > 配置文件 > 默认 `127.0.0.1:3000`。默认 `127.0.0.1` 仅本机可达,容器对外暴露时改为 `0.0.0.0`:
70
86
 
@@ -77,7 +93,9 @@ server:
77
93
 
78
94
  应用把解析后的 `server` 传给 `serv.listen`(`serv.listen` 也可直接传 `host` / `port` 显式覆盖)。
79
95
 
80
- ### Fetch Runtime(Workers / Bun / Deno)
96
+ #### 标准 fetch 签名适配
97
+
98
+ toFetch 提供标准 fetch 调用签名,适合 Node 兼容宿主与测试注入;包根仍依赖 Node API,不能据此承诺无需 Node 能力的 Workers/Deno/边缘部署。
81
99
 
82
100
  ```ts
83
101
  const handler = serv.toFetch(app)
@@ -86,7 +104,9 @@ export default { fetch: handler }
86
104
 
87
105
  Node `serv.listen()` 自动提供 socket 对端地址;默认忽略 `X-Forwarded-For` / `X-Real-IP`。只有 `http.trustedProxies` 中列出的代理 IP/CIDR 才能提供转发头,XFF 从右向左停在首个不可信地址,忽略其左侧内容;可信链无法解析或过长时 IP 为 undefined,不能回退为代理身份。代理必须清洗或追加转发头。Fetch 平台通过 `app.fetch(request, { remoteAddress: platformPeerAddress })` 显式提供可信对端,缺失时 IP 为 undefined,不能从任意请求头复制。自定义 `createContext(input)` 复用完整 input,保留框架已解析的 `input.ip`。内部 RPC 仍按连接对端限制。
88
106
 
89
- ### Audio WebSocket:ticket 与服务端授权
107
+ toFetch 对 Request、URL、字符串统一调用标准 Request(input, init),RequestInit 可覆盖 method、headers、body、signal 等字段;不修改原 Request 的字段,正文已消费/锁定或非法正文组合会以 Promise 拒绝。预取消和派发前取消不进入 handler,进行中取消结束调用方等待并把有效 signal 传给应用;监听器在完成/失败/取消后释放,取消后的迟到响应会取消未消费正文。取消不能回滚已经开始的外部副作用;Response 交付后的流由应用监听 request.signal 并自行关闭。
108
+
109
+ #### Audio WebSocket:ticket 与服务端授权
90
110
 
91
111
  语音入口必须使用短期、一次性 ticket。应用通过已登录 HTTP endpoint 签发 ticket(推荐 `iam.ticket.issue`),并在 `verifyTicket` 中原子消费(推荐 `iam.ticket.consume`);连接建立即完成鉴权(独立预鉴权超时),未鉴权不处理任何业务帧。框架保存返回的 `ServSession` 与可选 `grant`,再调用 `authorize` 确认付费参数:
92
112
 
@@ -110,18 +130,22 @@ const app = serv.createApp({
110
130
  - 协议边界对每一帧做运行时 Zod 校验并按严格状态机接受(识别只收音频、合成只收文本、`start`/`done` 各一次、`segmentId` 会话内唯一);输入队列、消息积压与发送缓冲均有上限,超限或取消时以领域错误关闭并级联中止上游。
111
131
  - 可调参数:`preAuthTimeoutMs`(默认 5000)、`maxPendingMessages`(默认 256)、`maxSendBufferBytes`(默认 8 MiB)、`maxMessageBytes` / `maxBufferedBytes` / `maxTextBytes` / `maxSessionMs`。
112
132
  - `onSessionEnd` 在成功、失败或断连后至多调用一次,供应用释放并发占用。
113
- - 浏览器 `createAIClient({ audio: { url, getTicket } })` 只把一次性 `ticket` 放入 WebSocket URL;`getTicket(request)` 接收本次操作摘要(`operation` / `model`),可签发与本次操作严格绑定的票据;禁止传普通 IAM access token。
133
+ - 浏览器 `createAudioClient({ url, getTicket })` 只把一次性 `ticket` 放入 WebSocket URL;`getTicket(request)` 接收本次操作摘要(`operation` / `model`),可签发与本次操作严格绑定的票据;禁止传普通 IAM access token。
114
134
 
115
- ### 传输加密(与 `@h-ai/crypto` 统一)
135
+ #### 传输加密(与 `@h-ai/crypto` 统一)
116
136
 
117
- serv 不暴露本地传输加密工厂;统一从 `crypto.transport` 创建服务端管理器。使用方只需传入顶层 `crypto` 句柄:
137
+ serv 不暴露本地传输加密工厂;统一从 `crypto.transport` 创建服务端管理器。服务端显式注入顶层 `crypto`、受管密钥环和共享 registry:
118
138
 
119
139
  ```ts
120
140
  import { cache } from '@h-ai/cache'
121
- import { createRedisTransportKeyStore, crypto } from '@h-ai/crypto'
141
+ import { createRedisTransportKeyStore, crypto, parseTransportServerKeyRing } from '@h-ai/crypto'
122
142
  import { serv } from '@h-ai/serv'
123
143
 
124
144
  await crypto.init()
145
+ const ring = parseTransportServerKeyRing(process.env.HAI_CRYPTO_TRANSPORT_SERVERKEYRING)
146
+ if (!ring.success)
147
+ throw new Error(ring.error.message)
148
+ const serverKeyRing = ring.data
125
149
  await cache.init({ type: 'redis', host: '127.0.0.1', port: 6379 })
126
150
 
127
151
  const app = serv.createApp({
@@ -130,9 +154,9 @@ const app = serv.createApp({
130
154
  http: { apiPrefix: '/api/v1' },
131
155
  transport: {
132
156
  crypto,
157
+ serverKeyRing,
133
158
  // keyExchangePath 默认 '/_hai/key-exchange',最终端点为 '/api/v1/_hai/key-exchange'
134
159
  // excludePaths: ['/health'],
135
- // maxClients: 10000,
136
160
  keyStore: createRedisTransportKeyStore({ cache, ttlSeconds: 3600 }),
137
161
  },
138
162
  })
@@ -165,7 +189,6 @@ transport:
165
189
  - /openapi.json
166
190
  - /docs
167
191
  - /_hai/scalar.js
168
- maxClients: 10000
169
192
  ```
170
193
 
171
194
  ```ts
@@ -187,24 +210,25 @@ const app = serv.createApp({
187
210
  ? undefined
188
211
  : {
189
212
  crypto,
213
+ serverKeyRing,
214
+ keyStore,
190
215
  keyExchangePath: servConfig.transport.keyExchangePath,
191
216
  excludePaths: [...servConfig.transport.excludePaths],
192
- maxClients: servConfig.transport.maxClients,
193
217
  },
194
218
  })
195
219
  ```
196
220
 
197
- > **多节点部署提示**:传输加密的会话密钥默认保存在 **服务端进程内**,多副本部署时必须满足下列条件之一才能正常工作:
198
- >
199
- > - 在负载均衡层启用**会话粘性(sticky session)**,确保同一客户端命中同一节点;
200
- > - 或为每个客户端在每个节点首次访问时分别完成一次密钥协商;
201
- > - 或注入共享 `keyStore`(如 `createRedisTransportKeyStore()` / `createReldbTransportKeyStore()`)。
202
- >
203
- > `keyStore` 属于运行时对象依赖,不应写入 `config/_serv.yml`;配置文件仍只保存路径、白名单和容量等静态项。
221
+ > 服务端必须显式注入相同的受管 `serverKeyRing` 和共享 `keyStore`(Redis 或共享数据库)。仅共享客户端公钥不足以跨节点解密。没有隐式进程私钥或内存 registry 回退;开发/测试显式用 `createInMemoryKeyStore(capacity)`。服务端重启后重新加载同一密钥环即可,无需 sticky session。
204
222
 
205
223
  > **安全策略**:启用 `transport` 后,除 `excludePaths` 与密钥协商端点外,业务请求必须携带有效 `X-Client-Id`。缺失或未知 clientId 会返回 400;响应体无法加密、非 JSON 业务响应或超过单次加密上限时返回错误,不会明文透传业务数据。
206
224
 
207
- ### Token 认证(`iam` / `refreshCookie`)
225
+ 认证协议仅支持 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
+ 空 POST 也发送空明文的认证封装;GET/HEAD 将封装放在 X-Hai-Transport 头,204/304/HEAD 等无正文响应也使用同名头承载认证封装。其它请求/响应在 JSON 正文承载封装并带 X-Encrypted: true。客户端必须关联原请求上下文和 HTTP status,缺标记、旧格式或认证失败一律 reject;明文接口只能经 excludePaths 显式排除。CORS 须允许并暴露 X-Hai-Transport。握手公钥的真实性依赖 TLS,不替代 IAM 业务授权。
228
+
229
+ 固定协议预算统一来自 `@h-ai/crypto` 的 `TRANSPORT_BODY_LIMITS`:握手 JSON 16 KiB,单个公钥 4 KiB,明文 1 MiB,密文 JSON 2 MiB + 32 KiB,均按 UTF-8 字节计数。原始流逐块有界读取,超限或取消会停止读取、取消流并释放 reader,不克隆后完整缓冲。请求正文超限返回 413,响应超限返回 500;无效公钥返回 400。重建请求时保留 signal、清空 Hono 正文缓存;重建请求/响应时清除旧 Content-Length/Content-Encoding。这些预算是协议常量,不是 YAML 或环境变量配置。
230
+
231
+ #### Token 认证(`iam` / `refreshCookie`)
208
232
 
209
233
  `serv.createApp` 推荐直接传入顶层 `iam` 句柄,自动派生两件事:
210
234
 
@@ -246,7 +270,9 @@ const app = serv.createApp({
246
270
  })
247
271
  ```
248
272
 
249
- ### 自定义 procedure(认证 + 权限)
273
+ 标准登出入口 `auth.logout` 从 input.accessToken 或 Authorization Bearer 取得要撤销的完整令牌,不要求 access 仍有效。它不接受 userId 授权或撤销目标;缺少令牌返回失败,未知令牌幂等成功。共享存储故障原样保留失败 HaiResult,Cookie middleware 只对成功结果清理 refresh Cookie。普通 procedure 继续使用既有 HTTP 200 / HaiResult 协议,客户端必须检查 success,不能只检查状态码。
274
+
275
+ #### 自定义 procedure(认证 + 权限)
250
276
 
251
277
  ```ts
252
278
  const procedures = serv
@@ -260,7 +286,7 @@ const procedures = serv
260
286
  .build()
261
287
  ```
262
288
 
263
- ### 高级:自定义 pipeline(HTTP middleware + context + route guard)
289
+ #### 高级:自定义 pipeline(HTTP middleware + context + route guard)
264
290
 
265
291
  `@h-ai/serv` 的“pipeline”分三层,分别对应三种扩展方式:
266
292
 
@@ -268,7 +294,7 @@ const procedures = serv
268
294
  2. **context 层**:通过 `verifyToken` / `createContext` / `serv.buildAuthContextFactory()` 定制请求上下文。
269
295
  3. **route guard 层**:通过 `.auth()` / `.permission()` / `.role()` 声明认证授权。
270
296
 
271
- #### 1) 自定义 HTTP middleware
297
+ ##### 1) 自定义 HTTP middleware
272
298
 
273
299
  当你需要加请求日志、trace、限流、CORS、租户头校验等 **HTTP 层横切逻辑**,或挂载不适合进入 JSON/oRPC contract 的 WebSocket、文件和二进制端点时,使用 `middlewares`。常见 CORS 场景可直接复用 `serv.cors(...)`:
274
300
 
@@ -336,7 +362,7 @@ const app = serv.createApp({
336
362
  - 浏览器若需读取自定义响应头(例如 transport 的 `X-Encrypted`),请通过 `serv.cors({ exposedHeaders: [...] })` 显式暴露
337
363
  - 这一层直接操作 HTTP middleware context,返回的是 **HTTP Response**,不是 `HaiResult`
338
364
 
339
- #### 2) 自定义 context pipeline
365
+ ##### 2) 自定义 context pipeline
340
366
 
341
367
  如果你只想复用 `Bearer token -> session` 这一段逻辑,再额外补充租户、工作区、来源系统等字段,
342
368
  优先用 `serv.buildAuthContextFactory()` 组装自定义 `createContext`:
@@ -363,7 +389,7 @@ const app = serv.createApp({
363
389
  - 想在默认认证上下文之上追加字段:用 `buildAuthContextFactory(...) + createContext`
364
390
  - 想完全接管上下文构造:直接用 `createContext`
365
391
 
366
- #### 3) Route guard
392
+ ##### 3) Route guard
367
393
 
368
394
  公开 procedure 使用 `.route(path, handler)`;需要 guard 时使用
369
395
  `.route(path).auth/permission/role().handle(handler)`:
@@ -396,7 +422,7 @@ const procedures = serv
396
422
  - 对应用层公开的稳定入口仍然是根入口 `@h-ai/serv`
397
423
  - `securityHeaders` / `requireInternalRPC` / `buildHaiErrorBody` 等内部 helper/middleware 不作为单独子路径文档暴露
398
424
 
399
- ### httpOnly Cookie 认证(推荐生产方案)
425
+ #### httpOnly Cookie 认证(推荐生产方案)
400
426
 
401
427
  httpOnly cookie 模式将 refresh token 存储在服务端管理的 cookie 中(浏览器 JS 无法读取),避免 XSS 风险。
402
428
 
@@ -430,7 +456,7 @@ const app = serv.createApp({
430
456
  refreshCookie: {
431
457
  // cookieName: 'hai_refresh_token', // 默认
432
458
  // maxAge: 30 * 24 * 3600, // 默认 30 天
433
- // secure: true, // 默认在 NODE_ENV=production 时开启
459
+ // secure: true, // NODE_ENV 或 HAI_CORE_ENV 为 production 时默认开启
434
460
  // onRefresh: customFn, // 可覆盖 iam.session.refresh。
435
461
  nativeTokenTransport: {
436
462
  // 必须 fail-closed:同时校验平台标识与允许的原生 Origin。
@@ -455,7 +481,7 @@ await apiClient.init({
455
481
 
456
482
  > **Cookie 规格**:`HttpOnly; SameSite=Strict; Secure`(生产);`Path` 限制为 `{apiPrefix}/auth/refresh`。
457
483
 
458
- ## API 概览
484
+ ### API 概览
459
485
 
460
486
  - `serv.createApp(options)`:创建 HTTP App 抽象,挂载自定义 `middlewares`、健康检查、OpenAPI handler、可选文档与 RPC
461
487
  - `serv.parseRequestContext({ request })`:默认上下文解析(提取 Bearer token + requestId,不填充 session)
@@ -482,7 +508,7 @@ await apiClient.init({
482
508
 
483
509
  > 传输加密不作为 `serv.xxx` 扁平 API 暴露;它是 `createApp` 的配置能力,内部委托 `crypto.transport`。
484
510
 
485
- ## 配置
511
+ ### 配置
486
512
 
487
513
  `@h-ai/serv` 本身不主动扫描 YAML;推荐由 `@h-ai/core` 统一加载 `config/_serv.yml`,再把解析结果传给 `serv.createApp()`。
488
514
 
@@ -506,9 +532,10 @@ const app = serv.createApp({
506
532
  ? undefined
507
533
  : {
508
534
  crypto,
535
+ serverKeyRing,
536
+ keyStore,
509
537
  keyExchangePath: servConfig.transport.keyExchangePath,
510
538
  excludePaths: [...servConfig.transport.excludePaths],
511
- maxClients: servConfig.transport.maxClients,
512
539
  },
513
540
  })
514
541
  ```
@@ -551,7 +578,6 @@ transport:
551
578
  - /openapi.json
552
579
  - /docs
553
580
  - /_hai/scalar.js
554
- maxClients: 10000
555
581
  ```
556
582
 
557
583
  `server`(host/port):优先级 `HOST` / `PORT` 环境变量 > 配置文件 > 默认 `127.0.0.1:3000`;由应用把 `servConfig.server` 传给 `serv.listen`。
@@ -597,7 +623,7 @@ const app = serv.createApp({
597
623
 
598
624
  生产环境下 `_serv.yml` 的 `cors.origin` 为空或为 `*` 会在启动时失败;Web 与 Capacitor/Tauri Origin 均按规范化后的完整 Origin 精确匹配,避免后缀伪造。生产环境同时强制 Secure refresh cookie 并关闭 OpenAPI/docs,配置文件仍决定非生产环境具体挂载哪些端点。
599
625
 
600
- ## 配置驱动认证页面
626
+ ### 配置驱动认证页面
601
627
 
602
628
  协议支持配置中的 userAgreementMarkdown/privacyPolicyMarkdown 正文及 URL;同主题优先 Markdown,由安全渲染器在弹窗显示。
603
629
 
@@ -605,7 +631,7 @@ const app = serv.createApp({
605
631
 
606
632
  [完整 IAM 配置 / IAM configuration](../iam/README.md)
607
633
 
608
- ## 错误处理
634
+ ### 错误处理
609
635
 
610
636
  普通 procedure 的认证、授权、业务失败和被捕获的 handler 异常都作为 `HaiResult` 输出,默认通过 HTTP 200 返回;`error.httpStatus` 是领域错误元数据,不会自动改写响应状态。客户端必须检查 `success` 与 `error.code`。例如未登录为 `error.httpStatus=401`,数据库查询失败或 INTERNAL_ERROR 为 500。
611
637
 
@@ -656,7 +682,17 @@ const procedures = serv
656
682
 
657
683
  > 如果错误来自下游业务模块(如 `iam.user.xxx()`)且该模块已经直接返回 `HaiResult`,serv 会原样透传它的 `error.message`。当前 `HaiError` 只携带最终 `message`,不携带 `messageKey/params`,因此 **serv 边界无法对任意下游错误再做一次通用 i18n 重翻译**;要支持请求级 i18n,必须在创建该错误消息的模块里就拿到 locale。
658
684
 
659
- ## 测试
685
+ ### 请求语言隔离
686
+
687
+ Node 请求与 SSR 使用 `core.i18n.runWithLocale(locale, () => handleRequest())`;SvelteKit 可使用 `kit.i18n.runWithLocale(locale, () => resolve(event))`,serv 自动按请求头建立上下文。异步子任务继承请求语言,优先级为显式 `MessageOptions.locale`、请求语言、进程启动默认语言。`setGlobalLocale` 在请求上下文中只更新当前请求;上下文外仅用于启动默认值,不能在请求 hook 中调用它修改默认值。
688
+
689
+ 浏览器继续使用应用级 `setGlobalLocale` / `createLocaleStore`。SSR 的 UI store 初始化和 setter 只修改自身状态,不写全局语言;UI 消息从请求上下文读取。`runWithLocale` 仅支持 Node 入口,浏览器通过显式 locale 或应用状态翻译。
690
+
691
+ ## 输入约束与 HTTP 能力
692
+
693
+ 存储 key 按 UTF-8 最多 1024 字节,并拒绝绝对路径、反斜线、控制字符及点路径段;公开 HTTP 批量删除每次 1–1000 项。`@h-ai/storage/schemas` 提供浏览器安全的共同规则。预签名有效期为整数 1–604800 秒(Node 默认 3600,HTTP 默认 900);HTTP 上传仅接受 key/contentType/expiresIn,不支持 contentLength/maxSize/metadata/checksum。无效输入返回 HTTP 400,typed client 映射 VALIDATION_FAILED。FileUpload 的大小/类型限制仅为前端体验,实际对象须由应用服务端复核。Node 流式读写/checksum/条件写等没有默认 HTTP 端点。完整映射与授权边界见 [存储 API 能力](../storage/README.md#存储输入约束与跨端能力)。
694
+
695
+ ## 开发测试
660
696
 
661
697
  ```bash
662
698
  pnpm --filter @h-ai/serv test
@@ -664,6 +700,8 @@ pnpm --filter @h-ai/serv typecheck
664
700
  pnpm --filter @h-ai/serv lint
665
701
  ```
666
702
 
667
- ## License
703
+ 声明旁的 JSDoc 示例从实际 `@h-ai/serv` 或已公开的 features 子路径导入;内部 Hono/guard/router/transport 示例明确标注“模块内部上下文”,不代表根 API。示例区分工厂的启动配置异常、领域 HaiResult、HTTP middleware 状态码和监听句柄的 Promise<void> 关闭;默认 AI completion 仍只返回非流式 JSON。框架维护运行 `node scripts/check-module-jsdoc.mjs --package serv` 对实际声明和内部词法绑定编译示例,编译不代替真实 HTTP、授权与分布式防重放验收。
704
+
705
+ ## 许可证
668
706
 
669
707
  Apache-2.0
@@ -0,0 +1,37 @@
1
+ import { normalizeServLocale, servM } from './chunk-TWHCU6HT.js';
2
+ import { core, ok, err, HaiCommonError } from '@h-ai/core';
3
+
4
+ var DEFAULT_LOCALE = "zh-CN";
5
+ function resolveRequestLocale(headers) {
6
+ const get = (name) => {
7
+ if (headers instanceof Headers)
8
+ return headers.get(name) ?? void 0;
9
+ return headers[name] ?? headers[name.toLowerCase()];
10
+ };
11
+ const explicit = get("x-hai-locale");
12
+ if (explicit)
13
+ return normalizeServLocale(explicit);
14
+ return normalizeServLocale(get("accept-language") ?? get("Accept-Language") ?? DEFAULT_LOCALE);
15
+ }
16
+ function localizeZodError(zodError, locale) {
17
+ const resolvedLocale = normalizeServLocale(locale);
18
+ return core.zodValidation.mapZodErrorToFormErrors(
19
+ zodError,
20
+ core.zodValidation.createPrefixedZodMessageGetter(
21
+ "serv",
22
+ (messageKey, params) => servM(messageKey, { locale: resolvedLocale, params })
23
+ )
24
+ );
25
+ }
26
+ function validateInputOrFail(schema, input, locale = DEFAULT_LOCALE) {
27
+ const resolvedLocale = normalizeServLocale(locale);
28
+ const result = schema.safeParse(input);
29
+ if (result.success)
30
+ return ok(result.data);
31
+ const errors = localizeZodError(result.error, resolvedLocale);
32
+ return err(HaiCommonError.VALIDATION_ERROR, servM("serv_validationFailed", { locale: resolvedLocale }), errors);
33
+ }
34
+
35
+ export { localizeZodError, resolveRequestLocale, validateInputOrFail };
36
+ //# sourceMappingURL=chunk-BGWE4S6Z.js.map
37
+ //# sourceMappingURL=chunk-BGWE4S6Z.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/serv-validation.ts"],"names":[],"mappings":";;;AAiBA,IAAM,cAAA,GAAiB,OAAA;AAkDhB,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-BGWE4S6Z.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 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"]}
@@ -11,7 +11,7 @@ var en_US_default = {
11
11
  serv_validationInvalid: "Invalid input format",
12
12
  serv_validationInvalidType: "Invalid field type",
13
13
  serv_validationStringMin: "Enter at least {min} characters",
14
- serv_validationStringMax: "Enter at most {max} characters",
14
+ serv_validationStringMax: "Enter at most {max} UTF-8 bytes",
15
15
  serv_validationNumberMin: "Value must be at least {min}",
16
16
  serv_validationNumberMax: "Value must be at most {max}",
17
17
  serv_validationArrayMin: "Select at least {min} item(s)",
@@ -39,6 +39,42 @@ var en_US_default = {
39
39
  serv_transportKeyExchangeFailed: "Key exchange failed"
40
40
  };
41
41
 
42
+ // messages/ja-JP.json
43
+ var ja_JP_default = {
44
+ $schema: "https://inlang.com/schema/inlang-message-format",
45
+ serv_validationFailed: "\u5165\u529B\u5185\u5BB9\u306E\u691C\u8A3C\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
46
+ serv_validationRequired: "\u3053\u306E\u9805\u76EE\u306F\u5FC5\u9808\u3067\u3059",
47
+ serv_validationInvalid: "\u5165\u529B\u5F62\u5F0F\u304C\u7121\u52B9\u3067\u3059",
48
+ serv_validationInvalidType: "\u30D5\u30A3\u30FC\u30EB\u30C9\u306E\u578B\u304C\u7121\u52B9\u3067\u3059",
49
+ serv_validationStringMin: "{min}\u6587\u5B57\u4EE5\u4E0A\u3067\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044",
50
+ serv_validationStringMax: "UTF-8 \u3067 {max} \u30D0\u30A4\u30C8\u4EE5\u5185\u3067\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044",
51
+ serv_validationNumberMin: "{min}\u4EE5\u4E0A\u306E\u5024\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044",
52
+ serv_validationNumberMax: "{max}\u4EE5\u4E0B\u306E\u5024\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044",
53
+ serv_validationArrayMin: "{min}\u4EF6\u4EE5\u4E0A\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044",
54
+ serv_validationArrayMax: "{max}\u4EF6\u4EE5\u5185\u3067\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044",
55
+ serv_validationTooSmall: "{min}\u4EE5\u4E0A\u306E\u5024\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044",
56
+ serv_validationTooBig: "{max}\u4EE5\u4E0B\u306E\u5024\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044",
57
+ serv_validationEmail: "\u6709\u52B9\u306A\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044",
58
+ serv_validationUrl: "\u6709\u52B9\u306AURL\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044",
59
+ serv_validationUuid: "\u6709\u52B9\u306AUUID\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044",
60
+ serv_validationEnum: "\u6709\u52B9\u306A\u9078\u629E\u80A2\u3092\u9078\u3093\u3067\u304F\u3060\u3055\u3044",
61
+ serv_errorUnauthorized: "\u8A8D\u8A3C\u3055\u308C\u3066\u3044\u307E\u305B\u3093",
62
+ serv_errorForbidden: "\u30A2\u30AF\u30BB\u30B9\u304C\u62D2\u5426\u3055\u308C\u307E\u3057\u305F",
63
+ serv_errorInternal: "\u30B5\u30FC\u30D0\u30FC\u5185\u90E8\u30A8\u30E9\u30FC",
64
+ serv_storageKeyRequired: "\u30B9\u30C8\u30EC\u30FC\u30B8\u30AD\u30FC\u3092\u7A7A\u306B\u3059\u308B\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093",
65
+ serv_storageKeyTooLong: "\u30B9\u30C8\u30EC\u30FC\u30B8\u30AD\u30FC\u306F{max}\u6587\u5B57\u4EE5\u5185\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044",
66
+ serv_storageKeyIllegalChars: "\u30B9\u30C8\u30EC\u30FC\u30B8\u30AD\u30FC\u306B\u7121\u52B9\u306A\u6587\u5B57\u304C\u542B\u307E\u308C\u3066\u3044\u307E\u3059",
67
+ serv_storageKeyDotSegments: "\u30B9\u30C8\u30EC\u30FC\u30B8\u30AD\u30FC\u306B\u300C.\u300D\u3084\u300C..\u300D\u306E\u30D1\u30B9\u8981\u7D20\u3092\u542B\u3081\u308B\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093",
68
+ serv_iamCannotDeleteCurrentUser: "\u73FE\u5728\u30ED\u30B0\u30A4\u30F3\u4E2D\u306E\u30E6\u30FC\u30B6\u30FC\u306F\u524A\u9664\u3067\u304D\u307E\u305B\u3093",
69
+ serv_transportInvalidPayload: "\u6697\u53F7\u5316\u3055\u308C\u305F\u901A\u4FE1\u30DA\u30A4\u30ED\u30FC\u30C9\u304C\u7121\u52B9\u3067\u3059",
70
+ serv_transportUnknownClient: "\u6697\u53F7\u5316\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u304C\u4E0D\u660E\u3067\u3059\u3002\u9375\u4EA4\u63DB\u304C\u5FC5\u8981\u3067\u3059",
71
+ serv_transportClientIdRequired: "X-Client-Id\u30D8\u30C3\u30C0\u30FC\u304C\u3042\u308A\u307E\u305B\u3093\u3002\u9375\u4EA4\u63DB\u304C\u5FC5\u8981\u3067\u3059",
72
+ serv_transportClientKeyNotFound: "\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u304C\u672A\u767B\u9332\u304B\u3001\u30BB\u30C3\u30B7\u30E7\u30F3\u306E\u6709\u52B9\u671F\u9650\u304C\u5207\u308C\u3066\u3044\u307E\u3059\u3002\u518D\u5EA6\u9375\u4EA4\u63DB\u3092\u884C\u3063\u3066\u304F\u3060\u3055\u3044",
73
+ serv_transportDecryptFailed: "\u30EA\u30AF\u30A8\u30B9\u30C8\u306E\u5FA9\u53F7\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
74
+ serv_transportEncryptFailed: "\u30EC\u30B9\u30DD\u30F3\u30B9\u306E\u6697\u53F7\u5316\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
75
+ serv_transportKeyExchangeFailed: "\u9375\u4EA4\u63DB\u306B\u5931\u6557\u3057\u307E\u3057\u305F"
76
+ };
77
+
42
78
  // messages/zh-CN.json
43
79
  var zh_CN_default = {
44
80
  $schema: "https://inlang.com/schema/inlang-message-format",
@@ -47,7 +83,7 @@ var zh_CN_default = {
47
83
  serv_validationInvalid: "\u8F93\u5165\u683C\u5F0F\u65E0\u6548",
48
84
  serv_validationInvalidType: "\u5B57\u6BB5\u7C7B\u578B\u4E0D\u6B63\u786E",
49
85
  serv_validationStringMin: "\u81F3\u5C11\u8F93\u5165 {min} \u4E2A\u5B57\u7B26",
50
- serv_validationStringMax: "\u6700\u591A\u8F93\u5165 {max} \u4E2A\u5B57\u7B26",
86
+ serv_validationStringMax: "\u6700\u591A\u8F93\u5165 {max} \u4E2A UTF-8 \u5B57\u8282",
51
87
  serv_validationNumberMin: "\u6570\u503C\u4E0D\u80FD\u5C0F\u4E8E {min}",
52
88
  serv_validationNumberMax: "\u6570\u503C\u4E0D\u80FD\u5927\u4E8E {max}",
53
89
  serv_validationArrayMin: "\u81F3\u5C11\u9009\u62E9 {min} \u9879",
@@ -76,7 +112,7 @@ var zh_CN_default = {
76
112
  };
77
113
 
78
114
  // src/serv-i18n.ts
79
- var SUPPORTED_SERV_LOCALES = ["zh-CN", "en-US"];
115
+ var SUPPORTED_SERV_LOCALES = ["zh-CN", "en-US", "ja-JP"];
80
116
  var DEFAULT_SERV_LOCALE = "zh-CN";
81
117
  function normalizeServLocale(locale) {
82
118
  const candidate = locale?.split(",")[0]?.trim();
@@ -88,11 +124,17 @@ function normalizeServLocale(locale) {
88
124
  const baseLanguage = candidate.split("-")[0]?.toLowerCase();
89
125
  if (baseLanguage === "en")
90
126
  return "en-US";
127
+ if (baseLanguage === "ja")
128
+ return "ja-JP";
91
129
  if (baseLanguage === "zh")
92
130
  return "zh-CN";
93
131
  return DEFAULT_SERV_LOCALE;
94
132
  }
95
- var servM = core.i18n.createMessageGetter({ "zh-CN": zh_CN_default, "en-US": en_US_default });
133
+ var servM = core.i18n.createMessageGetter({
134
+ "zh-CN": zh_CN_default,
135
+ "en-US": en_US_default,
136
+ "ja-JP": ja_JP_default
137
+ });
96
138
  var WILDCARD_PERMISSION = "*";
97
139
  var WILDCARD_ROLE = "*";
98
140
  function applyServGuards(requirements, handler) {
@@ -316,5 +358,5 @@ function getHandlerMethod(value) {
316
358
  }
317
359
 
318
360
  export { WILDCARD_PERMISSION, WILDCARD_ROLE, buildHaiErrorBody, implement, normalizeServLocale, servM };
319
- //# sourceMappingURL=chunk-R354OMWL.js.map
320
- //# sourceMappingURL=chunk-R354OMWL.js.map
361
+ //# sourceMappingURL=chunk-TWHCU6HT.js.map
362
+ //# sourceMappingURL=chunk-TWHCU6HT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../messages/en-US.json","../messages/ja-JP.json","../messages/zh-CN.json","../src/serv-i18n.ts","../src/pipelines/serv-pipeline-guard.ts","../src/pipelines/serv-pipeline-helper.ts","../src/serv-router.ts"],"names":["err","HaiCommonError","orpcImplement"],"mappings":"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","file":"chunk-TWHCU6HT.js","sourcesContent":["{\n \"$schema\": \"https://inlang.com/schema/inlang-message-format\",\n \"serv_validationFailed\": \"Validation failed\",\n \"serv_validationRequired\": \"This field is required\",\n \"serv_validationInvalid\": \"Invalid input format\",\n \"serv_validationInvalidType\": \"Invalid field type\",\n \"serv_validationStringMin\": \"Enter at least {min} characters\",\n \"serv_validationStringMax\": \"Enter at most {max} UTF-8 bytes\",\n \"serv_validationNumberMin\": \"Value must be at least {min}\",\n \"serv_validationNumberMax\": \"Value must be at most {max}\",\n \"serv_validationArrayMin\": \"Select at least {min} item(s)\",\n \"serv_validationArrayMax\": \"Select at most {max} item(s)\",\n \"serv_validationTooSmall\": \"Value must be at least {min}\",\n \"serv_validationTooBig\": \"Value must be at most {max}\",\n \"serv_validationEmail\": \"Enter a valid email address\",\n \"serv_validationUrl\": \"Enter a valid URL\",\n \"serv_validationUuid\": \"Enter a valid UUID\",\n \"serv_validationEnum\": \"Select a valid option\",\n \"serv_errorUnauthorized\": \"Unauthorized\",\n \"serv_errorForbidden\": \"Forbidden\",\n \"serv_errorInternal\": \"Internal server error\",\n \"serv_storageKeyRequired\": \"Storage key must not be empty\",\n \"serv_storageKeyTooLong\": \"Storage key must not exceed {max} characters\",\n \"serv_storageKeyIllegalChars\": \"Storage key contains illegal characters\",\n \"serv_storageKeyDotSegments\": \"Storage key must not contain . or .. path segments\",\n \"serv_iamCannotDeleteCurrentUser\": \"Cannot delete the current logged-in user\",\n \"serv_transportInvalidPayload\": \"Invalid encrypted transport payload\",\n \"serv_transportUnknownClient\": \"Unknown encryption client; key exchange required\",\n \"serv_transportClientIdRequired\": \"Missing X-Client-Id header; key exchange required\",\n \"serv_transportClientKeyNotFound\": \"Client not registered or session expired; complete key exchange again\",\n \"serv_transportDecryptFailed\": \"Failed to decrypt request\",\n \"serv_transportEncryptFailed\": \"Failed to encrypt response\",\n \"serv_transportKeyExchangeFailed\": \"Key exchange failed\"\n}\n","{\n \"$schema\": \"https://inlang.com/schema/inlang-message-format\",\n \"serv_validationFailed\": \"入力内容の検証に失敗しました\",\n \"serv_validationRequired\": \"この項目は必須です\",\n \"serv_validationInvalid\": \"入力形式が無効です\",\n \"serv_validationInvalidType\": \"フィールドの型が無効です\",\n \"serv_validationStringMin\": \"{min}文字以上で入力してください\",\n \"serv_validationStringMax\": \"UTF-8 で {max} バイト以内で入力してください\",\n \"serv_validationNumberMin\": \"{min}以上の値を入力してください\",\n \"serv_validationNumberMax\": \"{max}以下の値を入力してください\",\n \"serv_validationArrayMin\": \"{min}件以上選択してください\",\n \"serv_validationArrayMax\": \"{max}件以内で選択してください\",\n \"serv_validationTooSmall\": \"{min}以上の値を入力してください\",\n \"serv_validationTooBig\": \"{max}以下の値を入力してください\",\n \"serv_validationEmail\": \"有効なメールアドレスを入力してください\",\n \"serv_validationUrl\": \"有効なURLを入力してください\",\n \"serv_validationUuid\": \"有効なUUIDを入力してください\",\n \"serv_validationEnum\": \"有効な選択肢を選んでください\",\n \"serv_errorUnauthorized\": \"認証されていません\",\n \"serv_errorForbidden\": \"アクセスが拒否されました\",\n \"serv_errorInternal\": \"サーバー内部エラー\",\n \"serv_storageKeyRequired\": \"ストレージキーを空にすることはできません\",\n \"serv_storageKeyTooLong\": \"ストレージキーは{max}文字以内で指定してください\",\n \"serv_storageKeyIllegalChars\": \"ストレージキーに無効な文字が含まれています\",\n \"serv_storageKeyDotSegments\": \"ストレージキーに「.」や「..」のパス要素を含めることはできません\",\n \"serv_iamCannotDeleteCurrentUser\": \"現在ログイン中のユーザーは削除できません\",\n \"serv_transportInvalidPayload\": \"暗号化された通信ペイロードが無効です\",\n \"serv_transportUnknownClient\": \"暗号化クライアントが不明です。鍵交換が必要です\",\n \"serv_transportClientIdRequired\": \"X-Client-Idヘッダーがありません。鍵交換が必要です\",\n \"serv_transportClientKeyNotFound\": \"クライアントが未登録か、セッションの有効期限が切れています。再度鍵交換を行ってください\",\n \"serv_transportDecryptFailed\": \"リクエストの復号に失敗しました\",\n \"serv_transportEncryptFailed\": \"レスポンスの暗号化に失敗しました\",\n \"serv_transportKeyExchangeFailed\": \"鍵交換に失敗しました\"\n}\n","{\n \"$schema\": \"https://inlang.com/schema/inlang-message-format\",\n \"serv_validationFailed\": \"数据验证失败\",\n \"serv_validationRequired\": \"此字段必填\",\n \"serv_validationInvalid\": \"输入格式无效\",\n \"serv_validationInvalidType\": \"字段类型不正确\",\n \"serv_validationStringMin\": \"至少输入 {min} 个字符\",\n \"serv_validationStringMax\": \"最多输入 {max} 个 UTF-8 字节\",\n \"serv_validationNumberMin\": \"数值不能小于 {min}\",\n \"serv_validationNumberMax\": \"数值不能大于 {max}\",\n \"serv_validationArrayMin\": \"至少选择 {min} 项\",\n \"serv_validationArrayMax\": \"最多选择 {max} 项\",\n \"serv_validationTooSmall\": \"不能小于 {min}\",\n \"serv_validationTooBig\": \"不能大于 {max}\",\n \"serv_validationEmail\": \"请输入有效的邮箱地址\",\n \"serv_validationUrl\": \"请输入有效的网址\",\n \"serv_validationUuid\": \"请输入有效的 UUID\",\n \"serv_validationEnum\": \"请选择有效选项\",\n \"serv_errorUnauthorized\": \"未登录或登录已失效\",\n \"serv_errorForbidden\": \"无权执行该操作\",\n \"serv_errorInternal\": \"服务器内部错误\",\n \"serv_storageKeyRequired\": \"存储 key 不能为空\",\n \"serv_storageKeyTooLong\": \"存储 key 不能超过 {max} 个字符\",\n \"serv_storageKeyIllegalChars\": \"存储 key 包含非法字符\",\n \"serv_storageKeyDotSegments\": \"存储 key 不能包含 . 或 .. 路径段\",\n \"serv_iamCannotDeleteCurrentUser\": \"不能删除当前登录用户\",\n \"serv_transportInvalidPayload\": \"传输层加密载荷格式无效\",\n \"serv_transportUnknownClient\": \"未识别的加密客户端,请先完成密钥协商\",\n \"serv_transportClientIdRequired\": \"缺少 X-Client-Id 请求头,请先完成密钥协商\",\n \"serv_transportClientKeyNotFound\": \"客户端未注册或会话已过期,请重新完成密钥协商\",\n \"serv_transportDecryptFailed\": \"请求解密失败\",\n \"serv_transportEncryptFailed\": \"响应加密失败\",\n \"serv_transportKeyExchangeFailed\": \"密钥协商失败\"\n}\n","/**\n * @h-ai/serv — i18n 工具\n *\n * Serv 模块的国际化消息工具,从 @h-ai/core 调用 i18n API。\n * 与 kit-i18n 结构对齐,便于跨包统一维护。\n * @module serv-i18n\n */\n\nimport { core } from '@h-ai/core'\nimport messagesEnUS from '../messages/en-US.json'\nimport messagesJaJP from '../messages/ja-JP.json'\nimport messagesZhCN from '../messages/zh-CN.json'\n\n/**\n * Serv 本地化消息的真实 key 联合。\n *\n * @example\n * ```ts\n * // 模块内部上下文:示例复用当前声明所在模块的实际绑定。\n * const key: ServMessageKey = 'serv_errorForbidden'\n * const message = servM(key, { locale: 'en-US' })\n * ```\n */\ntype ServMessageKey = keyof typeof messagesZhCN\n\nconst SUPPORTED_SERV_LOCALES = ['zh-CN', 'en-US', 'ja-JP'] as const\nconst DEFAULT_SERV_LOCALE = 'zh-CN'\n\n/**\n * 规范化 serv 请求 locale。\n *\n * 支持:\n * - 完整 locale:`zh-CN` / `en-US` / `ja-JP`\n * - 简写:`zh` / `en` / `ja`\n * - `Accept-Language` 风格值:`en-US,en;q=0.9`\n *\n * 未识别时回退到默认 `zh-CN`。\n *\n * @example\n * ```ts\n * // 模块内部上下文:示例复用当前声明所在模块的实际绑定。\n * const english = normalizeServLocale('en-US,en;q=0.9')\n * const fallback = normalizeServLocale(undefined)\n * ```\n */\nexport function normalizeServLocale(locale: string | undefined): string {\n const candidate = locale?.split(',')[0]?.trim()\n if (!candidate)\n return DEFAULT_SERV_LOCALE\n\n const exact = SUPPORTED_SERV_LOCALES.find(item => item.toLowerCase() === candidate.toLowerCase())\n if (exact)\n return exact\n\n const baseLanguage = candidate.split('-')[0]?.toLowerCase()\n if (baseLanguage === 'en')\n return 'en-US'\n if (baseLanguage === 'ja')\n return 'ja-JP'\n if (baseLanguage === 'zh')\n return 'zh-CN'\n\n return DEFAULT_SERV_LOCALE\n}\n\n/**\n * 获取 Serv 模块的 i18n 消息。\n *\n * 单次调用可通过 `options.locale` 指定该次调用使用的语言,\n * 用于在请求处理过程中根据 `Accept-Language` 头本地化错误消息,\n * 而不污染全局 locale(多并发请求安全)。\n *\n * @example\n * ```ts\n * import { serv } from '@h-ai/serv'\n *\n * const message = serv.m('serv_validationStringMin', { locale: 'en-US', params: { min: 3 } })\n * // 单次 locale 不修改并发请求的全局语言。\n * ```\n */\nexport const servM = core.i18n.createMessageGetter<ServMessageKey>({\n 'zh-CN': messagesZhCN,\n 'en-US': messagesEnUS,\n 'ja-JP': messagesJaJP,\n})\n\nexport type { ServMessageKey }\n","/**\n * @h-ai/serv — procedure 认证与授权 guard\n *\n * 将认证、权限、角色及异常映射合并为单个 handler 包装层,并在类型上把\n * `context.session` 收窄为非空。\n *\n * @module pipelines/serv-pipeline-guard\n */\n\nimport type { HaiResult } from '@h-ai/core'\nimport type { ErrorMap, Meta, ProcedureHandler } from '@orpc/server'\nimport type { ServContext } from '../serv-context.js'\nimport type { AuthenticatedServContext } from '../serv-router.js'\nimport { err, HaiCommonError } from '@h-ai/core'\nimport { servM } from '../serv-i18n.js'\n\n/**\n * 通配符权限。仅应授予受信任的超级管理员,可通过任意 permission guard。\n *\n * @example\n * ```ts\n * import { serv } from '@h-ai/serv'\n *\n * const wildcard = serv.WILDCARD_PERMISSION\n * // 仅可信超级管理员可拥有通配符 permissions;普通用户使用明确能力列表。\n * ```\n */\nexport const WILDCARD_PERMISSION = '*'\n\n/**\n * 通配符角色。仅应授予受信任的超级管理员,可通过任意 role guard。\n *\n * @example\n * ```ts\n * import { serv } from '@h-ai/serv'\n *\n * const wildcard = serv.WILDCARD_ROLE\n * // 仅可信超级管理员可拥有通配符 roles;普通用户使用明确能力列表。\n * ```\n */\nexport const WILDCARD_ROLE = '*'\n\n/**\n * 内部 route guard 要求;权限/角色检查以可信非空会话为前提。\n *\n * @example\n * ```ts\n * // 模块内部上下文:示例复用当前声明所在模块的实际绑定。\n * const requirements: ServGuardRequirements = { auth: true, permissions: ['reports.read'] }\n * const guarded = applyServGuards(requirements, ({ context }) => ({ success: true, data: { userId: context.session.userId } }))\n * ```\n */\nexport interface ServGuardRequirements {\n readonly auth?: boolean\n readonly permissions?: readonly string[]\n readonly roles?: readonly string[]\n}\n\n/**\n * 将 guard 应用于一个使用 HaiResult 输出的 oRPC handler。\n *\n * 完整保留 oRPC 的 `path`、`procedure`、`signal`、`lastEventId`、`errors` 等\n * options;只在校验成功后把 `context` 收窄为带非空 session 的类型。\n *\n * @example\n * ```ts\n * // 模块内部上下文:示例复用当前声明所在模块的实际绑定。\n * const guarded = applyServGuards({ permissions: ['reports.read'] }, ({ context }) => ({ success: true, data: { userId: context.session.userId } }))\n * // 业务 handler 只在可信 session 满足全部权限后执行。\n * ```\n */\nexport function applyServGuards<\n TContext extends ServContext,\n TInput,\n TData,\n TErrorMap extends ErrorMap,\n TMeta extends Meta,\n>(\n requirements: ServGuardRequirements,\n handler: ProcedureHandler<\n AuthenticatedServContext<TContext>,\n TInput,\n HaiResult<TData>,\n TErrorMap,\n TMeta\n >,\n): ProcedureHandler<TContext, TInput, HaiResult<TData>, TErrorMap, TMeta> {\n return async (options) => {\n try {\n if (!hasAuthenticatedSession(options.context)) {\n return err(\n HaiCommonError.UNAUTHORIZED,\n servM('serv_errorUnauthorized', { locale: options.context.locale }),\n )\n }\n\n const { session } = options.context\n if (requirements.permissions?.length) {\n const allowed = session.permissions.includes(WILDCARD_PERMISSION)\n || requirements.permissions.every(\n permission => session.permissions.includes(permission),\n )\n if (!allowed) {\n return err(\n HaiCommonError.FORBIDDEN,\n servM('serv_errorForbidden', { locale: options.context.locale }),\n )\n }\n }\n\n if (requirements.roles?.length) {\n const allowed = session.roles.includes(WILDCARD_ROLE)\n || requirements.roles.every(\n role => session.roles.includes(role),\n )\n if (!allowed) {\n return err(\n HaiCommonError.FORBIDDEN,\n servM('serv_errorForbidden', { locale: options.context.locale }),\n )\n }\n }\n\n return await handler({\n ...options,\n context: options.context,\n })\n }\n catch (error) {\n return err(\n HaiCommonError.INTERNAL_ERROR,\n servM('serv_errorInternal', { locale: options.context.locale }),\n error,\n )\n }\n }\n}\n\nfunction hasAuthenticatedSession<TContext extends ServContext>(\n context: TContext,\n): context is AuthenticatedServContext<TContext> {\n return context.session !== undefined && context.session !== null\n}\n","/**\n * @h-ai/serv — Pipeline 公共 helper\n *\n * 放置不属于 Hono middleware / route guard 默认实现本身、\n * 但会被多个 pipeline 复用的公共工具。\n * @module pipelines/serv-pipeline-helper\n */\n\nimport type { ServProcedureHandler } from './serv-pipeline-types.js'\nimport { err, HaiCommonError } from '@h-ai/core'\nimport { servM } from '../serv-i18n.js'\n\n/**\n * 构造 `HaiResult` 失败分支同构的 HTTP 错误定义。\n *\n * @example\n * ```ts\n * // 模块内部上下文:示例复用当前声明所在模块的实际绑定。\n * const definition: ServErrorBodyDef = HaiCommonError.UNAUTHORIZED\n * const body = buildHaiErrorBody(definition, servM('serv_errorUnauthorized'))\n * ```\n */\nexport interface ServErrorBodyDef {\n readonly code: string | number\n readonly httpStatus: number\n readonly system: string\n readonly module: string\n}\n\n/**\n * 构造与 `HaiResult` 失败分支同构的 HTTP 响应体。\n *\n * - 字段顺序与 `haiResultSchema()` 输出一致,便于客户端统一解析。\n * - 仅暴露最小信息(code/message/httpStatus),避免泄漏内部细节。\n *\n * @example\n * ```ts\n * // 模块内部上下文:示例复用当前声明所在模块的实际绑定。\n * const body = buildHaiErrorBody(HaiCommonError.FORBIDDEN, servM('serv_errorForbidden'))\n * // HTTP middleware 再设置 status;body 只包含安全的领域错误字段。\n * ```\n */\nexport function buildHaiErrorBody(def: ServErrorBodyDef, message: string) {\n return {\n success: false as const,\n error: {\n code: def.code,\n message,\n httpStatus: def.httpStatus,\n system: def.system,\n module: def.module,\n },\n }\n}\n\n/**\n * 捕获未处理异常并转换为 HaiResult。\n *\n * 由 contract router 在注册 handler 时统一应用,业务代码无需手动包装。\n *\n * @param handler - 被包装的 procedure handler\n * @returns 带异常保护的新 handler\n *\n * @example\n * ```ts\n * // 模块内部上下文:示例复用当前声明所在模块的实际绑定。\n * const wrapped = mapHaiError<{ id: string }, { id: string }>(async ({ input }) => ({ success: true, data: { id: input.id } }))\n * // oRPC 原始 options 原样传入;未处理异常在此转换为领域错误。\n * ```\n */\nexport function mapHaiError<TInput, TOutput>(handler: ServProcedureHandler<TInput, TOutput>): ServProcedureHandler<TInput, TOutput> {\n return async (options) => {\n try {\n return await handler(options)\n }\n catch (error) {\n // 所有未捕获异常统一在这里收口,避免直接把异常冒泡到 HTTP 层。\n return err(HaiCommonError.INTERNAL_ERROR, servM('serv_errorInternal', { locale: options.context.locale }), error)\n }\n }\n}\n","/**\n * @h-ai/serv — contract-first 链式 procedure 实现器\n *\n * 在 oRPC `implement()` 之上提供 route 名只出现一次的链式 DSL。\n * 应用只接触 contract 路径、上下文与 guard,不需要组装 oRPC router 对象。\n *\n * @module serv-router\n */\n\nimport type { HaiResult } from '@h-ai/core'\nimport type {\n Context,\n ContractProcedure,\n ContractRouter,\n ErrorMap,\n Implementer,\n InferSchemaInput,\n InferSchemaOutput,\n Meta,\n ProcedureHandler,\n Router,\n UnlaziedRouter,\n} from '@orpc/server'\nimport type { ServGuardRequirements } from './pipelines/serv-pipeline-guard.js'\nimport type { ServContext } from './serv-context.js'\nimport { implement as orpcImplement } from '@orpc/server'\nimport { applyServGuards } from './pipelines/serv-pipeline-guard.js'\nimport { mapHaiError } from './pipelines/serv-pipeline-helper.js'\n\n/**\n * 内部初始 context 空约束,用于保持 oRPC 推导而不伪造运行时字段。\n *\n * @example\n * ```ts\n * // 模块内部上下文:示例复用当前声明所在模块的实际绑定。\n * const { serv } = await import('@h-ai/serv')\n * const context: ServContext & EmptyContext = serv.parseRequestContext({ request: new Request('https://example.com/ping') })\n * const requestId = context.requestId\n * ```\n */\ntype EmptyContext = Record<never, never>\n/**\n * 提取 contract 的字符串键,排除 symbol/数字索引。\n *\n * @example\n * ```ts\n * // 模块内部上下文:示例复用当前声明所在模块的实际绑定。\n * const { apiContract } = await import('@h-ai/api-contract')\n * const { z } = await import('zod')\n * const { ok } = await import('@h-ai/core')\n * const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }\n * const path: StringKey<typeof contract> = 'ping'\n * const procedure = contract[path]\n * ```\n */\ntype StringKey<T> = Extract<keyof T, string>\n/**\n * 内部 contract 泛型边界;真实 procedure 输出须为 HaiResult。\n *\n * @example\n * ```ts\n * // 模块内部上下文:示例复用当前声明所在模块的实际绑定。\n * const { apiContract } = await import('@h-ai/api-contract')\n * const { z } = await import('zod')\n * const { ok } = await import('@h-ai/core')\n * const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }\n * const checked = contract satisfies ServContract\n * const procedures = implement(checked).context<ServContext>().route('ping', () => ok({ ready: true })).build()\n * ```\n */\ntype ServContract = ContractRouter<Meta>\n\n/**\n * contract 中所有 procedure 的点分路径。\n *\n * @example\n * ```ts\n * import type { ServRoutePath } from '@h-ai/serv'\n * import type { ServContext } from '@h-ai/serv'\n * import { apiContract } from '@h-ai/api-contract'\n * import { ok } from '@h-ai/core'\n * import { serv } from '@h-ai/serv'\n * import { z } from 'zod'\n *\n * const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }\n * const path: ServRoutePath<typeof contract> = 'ping'\n * const procedures = serv.implement(contract).context<ServContext>().route(path, () => ok({ ready: true })).build()\n * ```\n */\nexport type ServRoutePath<TContract extends ServContract>\n = TContract extends ContractProcedure<\n infer _TInputSchema,\n infer _TOutputSchema,\n infer _TErrorMap,\n infer _TMeta\n >\n ? never\n : {\n [K in StringKey<TContract>]: TContract[K] extends ContractProcedure<\n infer _TChildInputSchema,\n infer _TChildOutputSchema,\n infer _TChildErrorMap,\n infer _TChildMeta\n >\n ? K\n : TContract[K] extends ServContract\n ? ServRoutePath<TContract[K]> extends infer TChild extends string\n ? `${K}.${TChild}`\n : never\n : never\n }[StringKey<TContract>]\n\n/**\n * 按点分路径查找原始 contract procedure;不存在的路径推导为 never。\n *\n * @example\n * ```ts\n * // 模块内部上下文:示例复用当前声明所在模块的实际绑定。\n * const { apiContract } = await import('@h-ai/api-contract')\n * const { z } = await import('zod')\n * const { ok } = await import('@h-ai/core')\n * const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }\n * const procedure: ContractAtPath<typeof contract, 'ping'> = contract.ping\n * const scoped = implement({ ping: procedure }).context<ServContext>().route('ping', () => ok({ ready: true })).build()\n * ```\n */\ntype ContractAtPath<TContract, TPath extends string>\n = TPath extends `${infer THead}.${infer TTail}`\n ? THead extends keyof TContract\n ? ContractAtPath<TContract[THead], TTail>\n : never\n : TPath extends keyof TContract\n ? TContract[TPath]\n : never\n\n/**\n * 将合法路由路径映射到对应 contract,供 handler 推导复用。\n *\n * @example\n * ```ts\n * // 模块内部上下文:示例复用当前声明所在模块的实际绑定。\n * const { apiContract } = await import('@h-ai/api-contract')\n * const { z } = await import('zod')\n * const { ok } = await import('@h-ai/core')\n * const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }\n * const procedure: RouteContract<typeof contract, 'ping'> = contract.ping\n * const scoped = implement({ ping: procedure }).context<ServContext>().route('ping', () => ok({ ready: true })).build()\n * ```\n */\ntype RouteContract<\n TContract extends ServContract,\n TPath extends ServRoutePath<TContract>,\n> = ContractAtPath<TContract, TPath>\n\n/**\n * `.auth()`、`.permission()`、`.role()` 后的非空会话上下文。\n *\n * @example\n * ```ts\n * import type { AuthenticatedServContext } from '@h-ai/serv'\n *\n * function owner(context: AuthenticatedServContext): string {\n * return context.session.userId\n * }\n * // 仅成功 guard 或可信认证结果能建立此上下文,禁止用客户端字段断言。\n * ```\n */\nexport type AuthenticatedServContext<TContext extends ServContext = ServContext>\n = TContext & {\n readonly session: NonNullable<TContext['session']>\n }\n\n/**\n * 内部从具体 procedure 的输入/输出与 context 推导 HaiResult handler。\n *\n * @example\n * ```ts\n * // 模块内部上下文:示例复用当前声明所在模块的实际绑定。\n * const { apiContract } = await import('@h-ai/api-contract')\n * const { z } = await import('zod')\n * const { ok } = await import('@h-ai/core')\n * const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }\n * const handler: RouteHandler<typeof contract.ping, ServContext> = () => ok({ ready: true })\n * const procedures = implement(contract).context<ServContext>().route('ping', handler).build()\n * ```\n */\ntype RouteHandler<\n TRouteContract,\n TContext extends Context,\n> = TRouteContract extends ContractProcedure<\n infer TInputSchema,\n infer TOutputSchema,\n infer TErrorMap,\n infer TMeta\n>\n ? InferSchemaInput<TOutputSchema> extends HaiResult<unknown>\n ? ProcedureHandler<\n TContext,\n InferSchemaOutput<TInputSchema>,\n InferSchemaInput<TOutputSchema>,\n TErrorMap,\n TMeta\n >\n : never\n : never\n\n/**\n * 从 contract procedure 精确推导当前 route 的 HaiResult handler。\n *\n * @example\n * ```ts\n * import type { ServRouteHandler } from '@h-ai/serv'\n * import type { ServContext } from '@h-ai/serv'\n * import { apiContract } from '@h-ai/api-contract'\n * import { ok } from '@h-ai/core'\n * import { serv } from '@h-ai/serv'\n * import { z } from 'zod'\n *\n * const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }\n * const handler: ServRouteHandler<typeof contract, 'ping', ServContext> = () => ok({ ready: true })\n * const procedures = serv.implement(contract).context<ServContext>().route('ping', handler).build()\n * ```\n */\nexport type ServRouteHandler<\n TContract extends ServContract,\n TPath extends ServRoutePath<TContract>,\n TContext extends Context,\n> = RouteHandler<RouteContract<TContract, TPath>, TContext>\n\n/**\n * 内部认证成功 handler 类型;session 在 guard 之后非空。\n *\n * @example\n * ```ts\n * // 模块内部上下文:示例复用当前声明所在模块的实际绑定。\n * const { apiContract } = await import('@h-ai/api-contract')\n * const { z } = await import('zod')\n * const { ok } = await import('@h-ai/core')\n * const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }\n * const handler: ServGuardedRouteHandler<typeof contract, 'ping', ServContext> = ({ context }) => ok({ ready: context.session.userId.length > 0 })\n * const procedures = implement(contract).context<ServContext>().route('ping').auth().handle(handler).build()\n * ```\n */\ntype ServGuardedRouteHandler<\n TContract extends ServContract,\n TPath extends ServRoutePath<TContract>,\n TContext extends ServContext,\n> = RouteHandler<RouteContract<TContract, TPath>, AuthenticatedServContext<TContext>>\n\n/**\n * 一次注册后移除该路径的 builder;全部路径完成才提供 build。\n *\n * @example\n * ```ts\n * // 模块内部上下文:示例复用当前声明所在模块的实际绑定。\n * const { apiContract } = await import('@h-ai/api-contract')\n * const { z } = await import('zod')\n * const { ok } = await import('@h-ai/core')\n * const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }\n * const next: NextBuilder<typeof contract, ServContext, ServContext, 'ping', 'ping'> = implement(contract).context<ServContext>().route('ping', () => ok({ ready: true }))\n * const procedures = next.build()\n * ```\n */\ntype NextBuilder<\n TContract extends ServContract,\n TInitialContext extends Context,\n TCurrentContext extends ServContext,\n TRemaining extends ServRoutePath<TContract>,\n TPath extends TRemaining,\n> = ServRouterBuilder<\n TContract,\n TInitialContext,\n TCurrentContext,\n Exclude<TRemaining, TPath>\n>\n\n/**\n * 尚未声明 guard 的单 route 注册器。\n *\n * @example\n * ```ts\n * // 模块内部上下文:示例复用当前声明所在模块的实际绑定。\n * const { apiContract } = await import('@h-ai/api-contract')\n * const { z } = await import('zod')\n * const { ok } = await import('@h-ai/core')\n * const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }\n * const route: ServRouteRegistration<typeof contract, ServContext, ServContext, 'ping', 'ping'> = implement(contract).context<ServContext>().route('ping')\n * const procedures = route.auth().handle(() => ok({ ready: true })).build()\n * ```\n */\nexport interface ServRouteRegistration<\n TContract extends ServContract,\n TInitialContext extends Context,\n TCurrentContext extends ServContext,\n TRemaining extends ServRoutePath<TContract>,\n TPath extends TRemaining,\n> {\n readonly auth: () => ServGuardedRouteRegistration<\n TContract,\n TInitialContext,\n TCurrentContext,\n TRemaining,\n TPath\n >\n readonly permission: (permission: string) => ServGuardedRouteRegistration<\n TContract,\n TInitialContext,\n TCurrentContext,\n TRemaining,\n TPath\n >\n readonly role: (role: string) => ServGuardedRouteRegistration<\n TContract,\n TInitialContext,\n TCurrentContext,\n TRemaining,\n TPath\n >\n}\n\n/**\n * 已声明认证或授权 guard 的单 route 注册器。\n *\n * @example\n * ```ts\n * // 模块内部上下文:示例复用当前声明所在模块的实际绑定。\n * const { apiContract } = await import('@h-ai/api-contract')\n * const { z } = await import('zod')\n * const { ok } = await import('@h-ai/core')\n * const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }\n * const guarded: ServGuardedRouteRegistration<typeof contract, ServContext, ServContext, 'ping', 'ping'> = implement(contract).context<ServContext>().route('ping').permission('reports.read')\n * const procedures = guarded.handle(() => ok({ ready: true })).build()\n * ```\n */\nexport interface ServGuardedRouteRegistration<\n TContract extends ServContract,\n TInitialContext extends Context,\n TCurrentContext extends ServContext,\n TRemaining extends ServRoutePath<TContract>,\n TPath extends TRemaining,\n> {\n readonly auth: () => ServGuardedRouteRegistration<\n TContract,\n TInitialContext,\n TCurrentContext,\n TRemaining,\n TPath\n >\n readonly permission: (permission: string) => ServGuardedRouteRegistration<\n TContract,\n TInitialContext,\n TCurrentContext,\n TRemaining,\n TPath\n >\n readonly role: (role: string) => ServGuardedRouteRegistration<\n TContract,\n TInitialContext,\n TCurrentContext,\n TRemaining,\n TPath\n >\n readonly handle: (\n handler: ServGuardedRouteHandler<TContract, TPath, TCurrentContext>,\n ) => NextBuilder<TContract, TInitialContext, TCurrentContext, TRemaining, TPath>\n}\n\n/**\n * 内部 route 重载:直接 handler 或先声明 guard,再收窄剩余路径。\n *\n * @example\n * ```ts\n * // 模块内部上下文:示例复用当前声明所在模块的实际绑定。\n * const { apiContract } = await import('@h-ai/api-contract')\n * const { z } = await import('zod')\n * const { ok } = await import('@h-ai/core')\n * const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }\n * const route: ServRouteMethod<typeof contract, ServContext, ServContext, 'ping'> = implement(contract).context<ServContext>().route\n * const procedures = route('ping', () => ok({ ready: true })).build()\n * ```\n */\ninterface ServRouteMethod<\n TContract extends ServContract,\n TInitialContext extends Context,\n TCurrentContext extends ServContext,\n TRemaining extends ServRoutePath<TContract>,\n> {\n <TPath extends TRemaining>(\n path: TPath,\n handler: ServRouteHandler<TContract, TPath, TCurrentContext>,\n ): NextBuilder<TContract, TInitialContext, TCurrentContext, TRemaining, TPath>\n <TPath extends TRemaining>(\n path: TPath,\n ): ServRouteRegistration<\n TContract,\n TInitialContext,\n TCurrentContext,\n TRemaining,\n TPath\n >\n}\n\n/**\n * 剩余路径为 never 时才暴露 build,否则仅提供缺失路径类型提示。\n *\n * @example\n * ```ts\n * // 模块内部上下文:示例复用当前声明所在模块的实际绑定。\n * const { apiContract } = await import('@h-ai/api-contract')\n * const { z } = await import('zod')\n * const { ok } = await import('@h-ai/core')\n * const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }\n * const complete: ServRouterBuilderBuild<typeof contract, ServContext, never> = implement(contract).context<ServContext>().route('ping', () => ok({ ready: true }))\n * const procedures = complete.build()\n * ```\n */\ntype ServRouterBuilderBuild<\n TContract extends ServContract,\n TInitialContext extends Context,\n TRemaining,\n> = [TRemaining] extends [never]\n ? {\n /** 同步返回完整的普通 router,允许按需选用嵌套的默认 procedures。 */\n readonly build: () => UnlaziedRouter<Router<TContract, TInitialContext>>\n }\n : {\n /** 仅供 TypeScript/IDE 展示尚未实现的 route;运行时不存在。 */\n readonly missingRoutes: TRemaining\n }\n\n/**\n * 链式 router builder。\n *\n * `TRemaining` 会随着 `.route()` 注册逐步收窄;未实现完整 contract 时没有 `.build()`。\n *\n * @example\n * ```ts\n * import type { ServRouterBuilder } from '@h-ai/serv'\n * import type { ServContext } from '@h-ai/serv'\n * import { apiContract } from '@h-ai/api-contract'\n * import { ok } from '@h-ai/core'\n * import { serv } from '@h-ai/serv'\n * import { z } from 'zod'\n *\n * const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }\n * const builder: ServRouterBuilder<typeof contract, ServContext, ServContext> = serv.implement(contract).context<ServContext>()\n * const procedures = builder.route('ping', () => ok({ ready: true })).build()\n * ```\n */\nexport type ServRouterBuilder<\n TContract extends ServContract,\n TInitialContext extends Context,\n TCurrentContext extends ServContext,\n TRemaining extends ServRoutePath<TContract> = ServRoutePath<TContract>,\n> = {\n readonly route: ServRouteMethod<\n TContract,\n TInitialContext,\n TCurrentContext,\n TRemaining\n >\n} & ServRouterBuilderBuild<TContract, TInitialContext, TRemaining>\n\n/**\n * hai-framework 链式 contract 实现器。\n *\n * @example\n * ```ts\n * import type { ServImplementer } from '@h-ai/serv'\n * import type { ServContext } from '@h-ai/serv'\n * import { apiContract } from '@h-ai/api-contract'\n * import { ok } from '@h-ai/core'\n * import { serv } from '@h-ai/serv'\n * import { z } from 'zod'\n *\n * const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }\n * const implementer: ServImplementer<typeof contract> = serv.implement(contract)\n * const procedures = implementer.context<ServContext>().route('ping', () => ok({ ready: true })).build()\n * ```\n */\nexport interface ServImplementer<TContract extends ServContract> {\n readonly context: <TContext extends ServContext>() => ServRouterBuilder<\n TContract,\n TContext & EmptyContext,\n TContext\n >\n}\n\n/**\n * 创建链式 contract 实现器。\n *\n * @example\n * ```ts\n * import type { ServContext } from '@h-ai/serv'\n * import { apiContract } from '@h-ai/api-contract'\n * import { ok } from '@h-ai/core'\n * import { serv } from '@h-ai/serv'\n * import { z } from 'zod'\n *\n * const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }\n * const procedures = serv.implement(contract).context<ServContext>().route('ping', () => ok({ ready: true })).build()\n * ```\n */\nexport function implement<TContract extends ServContract>(\n contract: TContract,\n): ServImplementer<TContract> {\n return {\n context: <TContext extends ServContext>() => {\n const native = orpcImplement(contract).$context<TContext>()\n return createServRouterBuilder(contract, native)\n },\n }\n}\n\nfunction createServRouterBuilder<\n TContract extends ServContract,\n TInitialContext extends Context,\n TCurrentContext extends ServContext,\n>(\n contract: TContract,\n implementer: Implementer<TContract, TInitialContext, TCurrentContext>,\n): ServRouterBuilder<TContract, TInitialContext, TCurrentContext> {\n const runtime = new ServRouterBuilderRuntime(contract, implementer)\n\n // oRPC 的 contract 泛型只能在编译期表示;运行时 builder 由同一 contract\n // 创建并逐项校验路径,因此这里将动态实现收敛到唯一的类型边界。\n return runtime as unknown as ServRouterBuilder<\n TContract,\n TInitialContext,\n TCurrentContext\n >\n}\n\n/**\n * 内部运行时 handler 边界;最终输入/输出验证仍由对应 contract 决定。\n *\n * @example\n * ```ts\n * // 模块内部上下文:示例复用当前声明所在模块的实际绑定。\n * const { ok } = await import('@h-ai/core')\n * const handler: RuntimeProcedureHandler = ({ input }) => ok(input)\n * // 动态装配边界仍由相同 contract 校验路径与 options,不提升未知输入类型。\n * ```\n */\ntype RuntimeProcedureHandler = ProcedureHandler<\n ServContext,\n unknown,\n HaiResult<unknown>,\n ErrorMap,\n Meta\n>\n/**\n * 内部认证后的运行时 handler,原始 oRPC options 与可信 session 保留。\n *\n * @example\n * ```ts\n * // 模块内部上下文:示例复用当前声明所在模块的实际绑定。\n * const { ok } = await import('@h-ai/core')\n * const handler: AuthenticatedRuntimeProcedureHandler = ({ context }) => ok({ userId: context.session.userId })\n * const guarded = applyServGuards({ auth: true }, handler)\n * ```\n */\ntype AuthenticatedRuntimeProcedureHandler = ProcedureHandler<\n AuthenticatedServContext,\n unknown,\n HaiResult<unknown>,\n ErrorMap,\n Meta\n>\n\nclass ServRouterBuilderRuntime {\n private readonly expectedPaths: ReadonlySet<string>\n private readonly implementedRoutes: Record<string, unknown>\n private readonly registeredPaths = new Set<string>()\n\n constructor(\n contract: ServContract,\n private readonly implementer: object,\n ) {\n this.expectedPaths = new Set(collectProcedurePaths(contract))\n this.implementedRoutes = createRouterShape(contract)\n }\n\n route(path: string, handler?: unknown): unknown {\n if (handler !== undefined)\n return this.register(path, {}, handler)\n\n return new ServRouteRegistrationRuntime(this, path)\n }\n\n register(path: string, guards: ServGuardRequirements, handler: unknown): this {\n if (!this.expectedPaths.has(path))\n throw new TypeError(`Contract path is not implementable: ${path}`)\n if (this.registeredPaths.has(path))\n throw new TypeError(`Duplicate procedure implementation: ${path}`)\n if (typeof handler !== 'function')\n throw new TypeError(`Procedure handler must be a function: ${path}`)\n\n const procedureImplementer = getAtPath(this.implementer, path)\n const implementHandler = getHandlerMethod(procedureImplementer)\n if (!implementHandler)\n throw new TypeError(`Contract path is not implementable: ${path}`)\n\n // 点路径反射会丢失单条 contract 的泛型;路径已由 expectedPaths 校验,\n // handler 的精确 input/output 则由公开的 ServRouteHandler 在调用侧保证。\n const safeHandler = hasGuards(guards)\n ? applyServGuards(guards, handler as AuthenticatedRuntimeProcedureHandler)\n : mapHaiError(handler as RuntimeProcedureHandler)\n const implemented = Reflect.apply(\n implementHandler,\n procedureImplementer,\n [safeHandler],\n )\n\n setAtPath(this.implementedRoutes, path, implemented)\n this.registeredPaths.add(path)\n return this\n }\n\n build(): unknown {\n const missingPaths = [...this.expectedPaths]\n .filter(path => !this.registeredPaths.has(path))\n .sort()\n if (missingPaths.length > 0) {\n throw new TypeError(\n `Cannot build router. Missing procedures: ${missingPaths.join(', ')}`,\n )\n }\n\n const routerMethod = Reflect.get(this.implementer, 'router')\n if (typeof routerMethod !== 'function')\n throw new TypeError('The oRPC implementer does not expose router()')\n\n return Reflect.apply(routerMethod, this.implementer, [this.implementedRoutes])\n }\n}\n\nclass ServRouteRegistrationRuntime {\n private authRequired = false\n private readonly permissions = new Set<string>()\n private readonly roles = new Set<string>()\n\n constructor(\n private readonly parent: ServRouterBuilderRuntime,\n private readonly path: string,\n ) {}\n\n auth(): this {\n this.authRequired = true\n return this\n }\n\n permission(permission: string): this {\n this.authRequired = true\n this.permissions.add(permission)\n return this\n }\n\n role(role: string): this {\n this.authRequired = true\n this.roles.add(role)\n return this\n }\n\n handle(handler: unknown): ServRouterBuilderRuntime {\n return this.parent.register(this.path, {\n auth: this.authRequired,\n permissions: [...this.permissions],\n roles: [...this.roles],\n }, handler)\n }\n}\n\nfunction hasGuards(guards: ServGuardRequirements): boolean {\n return guards.auth === true\n || (guards.permissions?.length ?? 0) > 0\n || (guards.roles?.length ?? 0) > 0\n}\n\nfunction collectProcedurePaths(\n contract: ServContract,\n prefix: readonly string[] = [],\n): string[] {\n if (isContractProcedure(contract))\n return prefix.length > 0 ? [prefix.join('.')] : []\n\n return Object.entries(contract).flatMap(([key, child]) =>\n collectProcedurePaths(child, [...prefix, key]))\n}\n\nfunction isContractProcedure(contract: ServContract): boolean {\n return '~orpc' in contract\n}\n\nfunction createRouterShape(contract: ServContract): Record<string, unknown> {\n const shape: Record<string, unknown> = Object.create(null)\n\n for (const [key, child] of Object.entries(contract)) {\n if (!isContractProcedure(child))\n shape[key] = createRouterShape(child)\n }\n\n return shape\n}\n\nfunction getAtPath(root: object, path: string): unknown {\n let current: unknown = root\n\n for (const segment of path.split('.')) {\n if (\n (typeof current !== 'object' && typeof current !== 'function')\n || current === null\n || !(segment in current)\n ) {\n return undefined\n }\n\n current = Reflect.get(current, segment)\n }\n\n return current\n}\n\nfunction setAtPath(root: Record<string, unknown>, path: string, value: unknown): void {\n const segments = path.split('.')\n const leaf = segments.pop()\n if (!leaf)\n throw new TypeError('Procedure path cannot be empty')\n\n let current = root\n for (const segment of segments) {\n const existing = current[segment]\n if (existing === undefined) {\n const child: Record<string, unknown> = Object.create(null)\n current[segment] = child\n current = child\n continue\n }\n\n if (!existing || typeof existing !== 'object')\n throw new TypeError(`Procedure path conflicts with an existing route: ${path}`)\n\n current = existing as Record<string, unknown>\n }\n\n current[leaf] = value\n}\n\nfunction getHandlerMethod(value: unknown): ((handler: unknown) => unknown) | undefined {\n if ((typeof value !== 'object' && typeof value !== 'function') || value === null)\n return undefined\n\n const handler = Reflect.get(value, 'handler')\n return typeof handler === 'function'\n ? handler as (handler: unknown) => unknown\n : undefined\n}\n"]}
@@ -13,5 +13,5 @@ function mapHaiResult(result, mapper) {
13
13
  }
14
14
 
15
15
  export { mapHaiResult, wrapItemsResult };
16
- //# sourceMappingURL=chunk-27PCPE37.js.map
17
- //# sourceMappingURL=chunk-27PCPE37.js.map
16
+ //# sourceMappingURL=chunk-XKCLD42I.js.map
17
+ //# sourceMappingURL=chunk-XKCLD42I.js.map