@h-ai/serv 0.1.0-alpha.45 → 0.1.0-alpha.47
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 +61 -64
- package/dist/chunk-KIWHKWLD.js +317 -0
- package/dist/chunk-KIWHKWLD.js.map +1 -0
- package/dist/features/ai.d.ts +20 -21
- package/dist/features/ai.js +14 -31
- package/dist/features/ai.js.map +1 -1
- package/dist/features/iam.d.ts +58 -58
- package/dist/features/iam.js +38 -97
- package/dist/features/iam.js.map +1 -1
- package/dist/features/storage.d.ts +18 -19
- package/dist/features/storage.js +39 -44
- package/dist/features/storage.js.map +1 -1
- package/dist/index.d.ts +102 -122
- package/dist/index.js +3 -19
- package/dist/index.js.map +1 -1
- package/dist/{serv-context-BUrtWoFC.d.ts → serv-context-jc3rZxbj.d.ts} +2 -2
- package/package.json +10 -10
- package/dist/chunk-4QBF4PUG.js +0 -128
- package/dist/chunk-4QBF4PUG.js.map +0 -1
- package/dist/chunk-YQV2OMQK.js +0 -16
- package/dist/chunk-YQV2OMQK.js.map +0 -1
package/README.md
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
## 能力概览
|
|
6
6
|
|
|
7
7
|
- 一行启动:`serv.createApp(...)` → `serv.listen(app, { port })`,应用代码不需要 import Hono
|
|
8
|
-
- 扁平 API(最小知识):`serv.
|
|
9
|
-
-
|
|
8
|
+
- 扁平 API(最小知识):`serv.createApp / serv.implement / serv.listen / serv.toFetch / serv.generateSpec / ...`
|
|
9
|
+
- 链式 procedure:`implement().context().route().auth/permission/role().handle().build()`
|
|
10
|
+
- 自定义 pipeline:`createApp({ middlewares })` 挂 HTTP middleware;请求级字段通过 context 工厂注入
|
|
10
11
|
- 只读 Storage 资源:`serv.storageAssets(...)` 统一处理安全 key、MIME 白名单、GET/HEAD、ETag 与缓存头
|
|
11
12
|
- 默认 feature procedures:`createIamProcedures()`、`createStorageProcedures()`、`createAiProcedures()`
|
|
12
13
|
- 内置安全响应头、健康检查、可选 OpenAPI JSON、可选 Scalar 文档页、可选内部 RPC endpoint
|
|
@@ -231,22 +232,24 @@ const app = serv.createApp({
|
|
|
231
232
|
### 自定义 procedure(认证 + 权限)
|
|
232
233
|
|
|
233
234
|
```ts
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
235
|
+
const procedures = serv
|
|
236
|
+
.implement(myContract)
|
|
237
|
+
.context<ServContext>()
|
|
238
|
+
.route('users.update')
|
|
239
|
+
.permission('users.write')
|
|
240
|
+
.handle(async ({ input, context }) => {
|
|
238
241
|
return await userService.update(input.id, input)
|
|
239
|
-
})
|
|
240
|
-
)
|
|
242
|
+
})
|
|
243
|
+
.build()
|
|
241
244
|
```
|
|
242
245
|
|
|
243
|
-
### 高级:自定义 pipeline(HTTP middleware + context +
|
|
246
|
+
### 高级:自定义 pipeline(HTTP middleware + context + route guard)
|
|
244
247
|
|
|
245
248
|
`@h-ai/serv` 的“pipeline”分三层,分别对应三种扩展方式:
|
|
246
249
|
|
|
247
250
|
1. **HTTP middleware 层**:通过 `serv.createApp({ middlewares })` 注入 HTTP middleware。
|
|
248
251
|
2. **context 层**:通过 `verifyToken` / `createContext` / `serv.buildAuthContextFactory()` 定制请求上下文。
|
|
249
|
-
3. **
|
|
252
|
+
3. **route guard 层**:通过 `.auth()` / `.permission()` / `.role()` 声明认证授权。
|
|
250
253
|
|
|
251
254
|
#### 1) 自定义 HTTP middleware
|
|
252
255
|
|
|
@@ -343,45 +346,32 @@ const app = serv.createApp({
|
|
|
343
346
|
- 想在默认认证上下文之上追加字段:用 `buildAuthContextFactory(...) + createContext`
|
|
344
347
|
- 想完全接管上下文构造:直接用 `createContext`
|
|
345
348
|
|
|
346
|
-
#### 3)
|
|
349
|
+
#### 3) Route guard
|
|
347
350
|
|
|
348
|
-
|
|
351
|
+
公开 procedure 使用 `.route(path, handler)`;需要 guard 时使用
|
|
352
|
+
`.route(path).auth/permission/role().handle(handler)`:
|
|
349
353
|
|
|
350
354
|
```ts
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
HaiCommonError.FORBIDDEN,
|
|
364
|
-
serv.m('serv_errorForbidden', { locale: options.context.locale }),
|
|
365
|
-
)
|
|
366
|
-
}
|
|
367
|
-
return await handler(options)
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const createWidget = serv.mapHaiError(
|
|
371
|
-
serv.requireAuth(
|
|
372
|
-
withAudit(
|
|
373
|
-
requireTenant('tenant-a', async ({ input }) => widgetService.create(input)),
|
|
374
|
-
),
|
|
375
|
-
),
|
|
376
|
-
)
|
|
355
|
+
const procedures = serv
|
|
356
|
+
.implement(widgetContract)
|
|
357
|
+
.context<ServContext>()
|
|
358
|
+
.route('widgets.list', ({ input }) => widgetService.list(input))
|
|
359
|
+
.route('widgets.create')
|
|
360
|
+
.permission('widgets.write')
|
|
361
|
+
.role('editor')
|
|
362
|
+
.handle(({ input, context }) => {
|
|
363
|
+
// guard 通过后 session 在类型上已是非空,不需要 `!`。
|
|
364
|
+
return widgetService.create(context.session.userId, input)
|
|
365
|
+
})
|
|
366
|
+
.build()
|
|
377
367
|
```
|
|
378
368
|
|
|
379
|
-
|
|
369
|
+
约束:
|
|
380
370
|
|
|
381
|
-
- `
|
|
382
|
-
-
|
|
383
|
-
-
|
|
384
|
-
-
|
|
371
|
+
- `.permission()` 与 `.role()` 自动要求认证;未登录返回 401,授权不足返回 403。
|
|
372
|
+
- 同一路由声明多个 permission/role 时必须全部满足;`serv.WILDCARD_PERMISSION` / `serv.WILDCARD_ROLE` 可放行对应类别。
|
|
373
|
+
- handler 抛出的未处理异常统一转成 INTERNAL_ERROR HaiResult;预期业务失败仍应直接返回 `err(...)`。
|
|
374
|
+
- 路径、input、output 从 contract 推导;重复或未知路径会被拒绝,未实现完整 contract 时不能调用 `.build()`。
|
|
385
375
|
|
|
386
376
|
边界约束:
|
|
387
377
|
|
|
@@ -456,10 +446,11 @@ await apiClient.init({
|
|
|
456
446
|
- `serv.toFetch(app)`:包装为标准 `fetch(Request)` handler
|
|
457
447
|
- `serv.generateSpec(contract, options)`:由 contract 生成 OpenAPI 3.1 spec
|
|
458
448
|
- `serv.storageAssets(config)`:创建只读 Storage 资源 middleware,内置 key/MIME 白名单、GET/HEAD 与 ETag
|
|
459
|
-
- `serv.
|
|
460
|
-
-
|
|
461
|
-
-
|
|
462
|
-
-
|
|
449
|
+
- `serv.implement(contract)`:创建链式 contract 实现器
|
|
450
|
+
- `.context<ServContext>()`:声明 procedure 请求上下文
|
|
451
|
+
- `.route(path, handler)`:注册公开 procedure
|
|
452
|
+
- `.route(path).auth/permission/role().handle(handler)`:注册带认证授权的 procedure
|
|
453
|
+
- `.build()`:所有 contract procedures 完整实现后构建 router
|
|
463
454
|
- `serv.validateInputOrFail(zodSchema, input, locale)`:在 procedure 内做 Zod 二次校验,失败时返回本地化 `HaiResult` + `ValidationFormError[]`
|
|
464
455
|
- `serv.resolveRequestLocale(headers)`:从 `x-hai-locale` / `Accept-Language` 解析并规范化请求 locale
|
|
465
456
|
- `serv.m(key, options)`:读取 `@h-ai/serv` 自身消息,支持 `options.locale` 单次调用本地化
|
|
@@ -469,7 +460,7 @@ await apiClient.init({
|
|
|
469
460
|
- `ServConfigSchema`:用于 `core.config.validate('serv', ServConfigSchema)` 校验 `config/_serv.yml`
|
|
470
461
|
- HTTP App 抽象:`serv.createApp()` 返回具备 `fetch/request` 能力的公开应用对象;内部是否使用 Hono 不影响应用代码
|
|
471
462
|
- `ServMiddlewareMount`:`createApp({ middlewares })` 的挂载项类型(`{ path?, middleware }`)
|
|
472
|
-
- `ServMiddleware` / `
|
|
463
|
+
- `ServMiddleware` / `ServProcedureHandler`:自定义 pipeline 时复用的共享类型
|
|
473
464
|
- `NativeRefreshTokenTransportConfig` / `ServStorageAssetsConfig`:原生 Token 通道与只读资源 middleware 配置
|
|
474
465
|
|
|
475
466
|
> 传输加密不作为 `serv.xxx` 扁平 API 暴露;它是 `createApp` 的配置能力,内部委托 `crypto.transport`。
|
|
@@ -585,9 +576,9 @@ const app = serv.createApp({
|
|
|
585
576
|
## 错误处理
|
|
586
577
|
|
|
587
578
|
- Default procedures 全部返回 `HaiResult<T>`,客户端直接判断 `result.success`
|
|
588
|
-
- 认证失败 → `HaiCommonError.UNAUTHORIZED`(由 `
|
|
589
|
-
- 授权失败 → `HaiCommonError.FORBIDDEN`(由 `
|
|
590
|
-
- 未捕获异常 → `HaiCommonError.INTERNAL_ERROR`(由
|
|
579
|
+
- 认证失败 → `HaiCommonError.UNAUTHORIZED`(由 `.auth()` 按 `context.locale` 本地化)
|
|
580
|
+
- 授权失败 → `HaiCommonError.FORBIDDEN`(由 `.permission()` / `.role()` 按 `context.locale` 本地化)
|
|
581
|
+
- 未捕获异常 → `HaiCommonError.INTERNAL_ERROR`(由 route 实现器统一兜底并按 `context.locale` 本地化)
|
|
591
582
|
|
|
592
583
|
正常业务请求建议按三层处理:
|
|
593
584
|
|
|
@@ -605,20 +596,26 @@ const CreateWidgetSchema = z.object({
|
|
|
605
596
|
slug: z.string().min(3),
|
|
606
597
|
})
|
|
607
598
|
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
599
|
+
const procedures = serv
|
|
600
|
+
.implement(widgetContract)
|
|
601
|
+
.context<ServContext>()
|
|
602
|
+
.route('widgets.create')
|
|
603
|
+
.permission('widgets.write')
|
|
604
|
+
.handle(({ input, context }) => {
|
|
605
|
+
const validated = serv.validateInputOrFail(CreateWidgetSchema, input, context.locale)
|
|
606
|
+
if (!validated.success)
|
|
607
|
+
return validated
|
|
608
|
+
|
|
609
|
+
if (validated.data.slug === 'admin') {
|
|
610
|
+
return err(
|
|
611
|
+
HaiCommonError.FORBIDDEN,
|
|
612
|
+
serv.m('serv_errorForbidden', { locale: context.locale }),
|
|
613
|
+
)
|
|
614
|
+
}
|
|
619
615
|
|
|
620
|
-
|
|
621
|
-
})
|
|
616
|
+
return widgetService.create(validated.data)
|
|
617
|
+
})
|
|
618
|
+
.build()
|
|
622
619
|
```
|
|
623
620
|
|
|
624
621
|
> 如果错误来自下游业务模块(如 `iam.user.xxx()`)且该模块已经直接返回 `HaiResult`,serv 会原样透传它的 `error.message`。当前 `HaiError` 只携带最终 `message`,不携带 `messageKey/params`,因此 **serv 边界无法对任意下游错误再做一次通用 i18n 重翻译**;要支持请求级 i18n,必须在创建该错误消息的模块里就拿到 locale。
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { core, err, HaiCommonError } from '@h-ai/core';
|
|
2
|
+
import { implement as implement$1 } from '@orpc/server';
|
|
3
|
+
|
|
4
|
+
// src/serv-i18n.ts
|
|
5
|
+
|
|
6
|
+
// messages/en-US.json
|
|
7
|
+
var en_US_default = {
|
|
8
|
+
$schema: "https://inlang.com/schema/inlang-message-format",
|
|
9
|
+
serv_validationFailed: "Validation failed",
|
|
10
|
+
serv_validationRequired: "This field is required",
|
|
11
|
+
serv_validationInvalid: "Invalid input format",
|
|
12
|
+
serv_validationInvalidType: "Invalid field type",
|
|
13
|
+
serv_validationStringMin: "Enter at least {min} characters",
|
|
14
|
+
serv_validationStringMax: "Enter at most {max} characters",
|
|
15
|
+
serv_validationNumberMin: "Value must be at least {min}",
|
|
16
|
+
serv_validationNumberMax: "Value must be at most {max}",
|
|
17
|
+
serv_validationArrayMin: "Select at least {min} item(s)",
|
|
18
|
+
serv_validationArrayMax: "Select at most {max} item(s)",
|
|
19
|
+
serv_validationTooSmall: "Value must be at least {min}",
|
|
20
|
+
serv_validationTooBig: "Value must be at most {max}",
|
|
21
|
+
serv_validationEmail: "Enter a valid email address",
|
|
22
|
+
serv_validationUrl: "Enter a valid URL",
|
|
23
|
+
serv_validationUuid: "Enter a valid UUID",
|
|
24
|
+
serv_validationEnum: "Select a valid option",
|
|
25
|
+
serv_errorUnauthorized: "Unauthorized",
|
|
26
|
+
serv_errorForbidden: "Forbidden",
|
|
27
|
+
serv_errorInternal: "Internal server error",
|
|
28
|
+
serv_storageKeyRequired: "Storage key must not be empty",
|
|
29
|
+
serv_storageKeyTooLong: "Storage key must not exceed {max} characters",
|
|
30
|
+
serv_storageKeyIllegalChars: "Storage key contains illegal characters",
|
|
31
|
+
serv_storageKeyDotSegments: "Storage key must not contain . or .. path segments",
|
|
32
|
+
serv_iamCannotDeleteCurrentUser: "Cannot delete the current logged-in user",
|
|
33
|
+
serv_transportInvalidPayload: "Invalid encrypted transport payload",
|
|
34
|
+
serv_transportUnknownClient: "Unknown encryption client; key exchange required",
|
|
35
|
+
serv_transportClientIdRequired: "Missing X-Client-Id header; key exchange required",
|
|
36
|
+
serv_transportClientKeyNotFound: "Client not registered or session expired; complete key exchange again",
|
|
37
|
+
serv_transportDecryptFailed: "Failed to decrypt request",
|
|
38
|
+
serv_transportEncryptFailed: "Failed to encrypt response",
|
|
39
|
+
serv_transportKeyExchangeFailed: "Key exchange failed"
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// messages/zh-CN.json
|
|
43
|
+
var zh_CN_default = {
|
|
44
|
+
$schema: "https://inlang.com/schema/inlang-message-format",
|
|
45
|
+
serv_validationFailed: "\u6570\u636E\u9A8C\u8BC1\u5931\u8D25",
|
|
46
|
+
serv_validationRequired: "\u6B64\u5B57\u6BB5\u5FC5\u586B",
|
|
47
|
+
serv_validationInvalid: "\u8F93\u5165\u683C\u5F0F\u65E0\u6548",
|
|
48
|
+
serv_validationInvalidType: "\u5B57\u6BB5\u7C7B\u578B\u4E0D\u6B63\u786E",
|
|
49
|
+
serv_validationStringMin: "\u81F3\u5C11\u8F93\u5165 {min} \u4E2A\u5B57\u7B26",
|
|
50
|
+
serv_validationStringMax: "\u6700\u591A\u8F93\u5165 {max} \u4E2A\u5B57\u7B26",
|
|
51
|
+
serv_validationNumberMin: "\u6570\u503C\u4E0D\u80FD\u5C0F\u4E8E {min}",
|
|
52
|
+
serv_validationNumberMax: "\u6570\u503C\u4E0D\u80FD\u5927\u4E8E {max}",
|
|
53
|
+
serv_validationArrayMin: "\u81F3\u5C11\u9009\u62E9 {min} \u9879",
|
|
54
|
+
serv_validationArrayMax: "\u6700\u591A\u9009\u62E9 {max} \u9879",
|
|
55
|
+
serv_validationTooSmall: "\u4E0D\u80FD\u5C0F\u4E8E {min}",
|
|
56
|
+
serv_validationTooBig: "\u4E0D\u80FD\u5927\u4E8E {max}",
|
|
57
|
+
serv_validationEmail: "\u8BF7\u8F93\u5165\u6709\u6548\u7684\u90AE\u7BB1\u5730\u5740",
|
|
58
|
+
serv_validationUrl: "\u8BF7\u8F93\u5165\u6709\u6548\u7684\u7F51\u5740",
|
|
59
|
+
serv_validationUuid: "\u8BF7\u8F93\u5165\u6709\u6548\u7684 UUID",
|
|
60
|
+
serv_validationEnum: "\u8BF7\u9009\u62E9\u6709\u6548\u9009\u9879",
|
|
61
|
+
serv_errorUnauthorized: "\u672A\u767B\u5F55\u6216\u767B\u5F55\u5DF2\u5931\u6548",
|
|
62
|
+
serv_errorForbidden: "\u65E0\u6743\u6267\u884C\u8BE5\u64CD\u4F5C",
|
|
63
|
+
serv_errorInternal: "\u670D\u52A1\u5668\u5185\u90E8\u9519\u8BEF",
|
|
64
|
+
serv_storageKeyRequired: "\u5B58\u50A8 key \u4E0D\u80FD\u4E3A\u7A7A",
|
|
65
|
+
serv_storageKeyTooLong: "\u5B58\u50A8 key \u4E0D\u80FD\u8D85\u8FC7 {max} \u4E2A\u5B57\u7B26",
|
|
66
|
+
serv_storageKeyIllegalChars: "\u5B58\u50A8 key \u5305\u542B\u975E\u6CD5\u5B57\u7B26",
|
|
67
|
+
serv_storageKeyDotSegments: "\u5B58\u50A8 key \u4E0D\u80FD\u5305\u542B . \u6216 .. \u8DEF\u5F84\u6BB5",
|
|
68
|
+
serv_iamCannotDeleteCurrentUser: "\u4E0D\u80FD\u5220\u9664\u5F53\u524D\u767B\u5F55\u7528\u6237",
|
|
69
|
+
serv_transportInvalidPayload: "\u4F20\u8F93\u5C42\u52A0\u5BC6\u8F7D\u8377\u683C\u5F0F\u65E0\u6548",
|
|
70
|
+
serv_transportUnknownClient: "\u672A\u8BC6\u522B\u7684\u52A0\u5BC6\u5BA2\u6237\u7AEF\uFF0C\u8BF7\u5148\u5B8C\u6210\u5BC6\u94A5\u534F\u5546",
|
|
71
|
+
serv_transportClientIdRequired: "\u7F3A\u5C11 X-Client-Id \u8BF7\u6C42\u5934\uFF0C\u8BF7\u5148\u5B8C\u6210\u5BC6\u94A5\u534F\u5546",
|
|
72
|
+
serv_transportClientKeyNotFound: "\u5BA2\u6237\u7AEF\u672A\u6CE8\u518C\u6216\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u5B8C\u6210\u5BC6\u94A5\u534F\u5546",
|
|
73
|
+
serv_transportDecryptFailed: "\u8BF7\u6C42\u89E3\u5BC6\u5931\u8D25",
|
|
74
|
+
serv_transportEncryptFailed: "\u54CD\u5E94\u52A0\u5BC6\u5931\u8D25",
|
|
75
|
+
serv_transportKeyExchangeFailed: "\u5BC6\u94A5\u534F\u5546\u5931\u8D25"
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// src/serv-i18n.ts
|
|
79
|
+
var SUPPORTED_SERV_LOCALES = ["zh-CN", "en-US"];
|
|
80
|
+
var DEFAULT_SERV_LOCALE = "zh-CN";
|
|
81
|
+
function normalizeServLocale(locale) {
|
|
82
|
+
const candidate = locale?.split(",")[0]?.trim();
|
|
83
|
+
if (!candidate)
|
|
84
|
+
return DEFAULT_SERV_LOCALE;
|
|
85
|
+
const exact = SUPPORTED_SERV_LOCALES.find((item) => item.toLowerCase() === candidate.toLowerCase());
|
|
86
|
+
if (exact)
|
|
87
|
+
return exact;
|
|
88
|
+
const baseLanguage = candidate.split("-")[0]?.toLowerCase();
|
|
89
|
+
if (baseLanguage === "en")
|
|
90
|
+
return "en-US";
|
|
91
|
+
if (baseLanguage === "zh")
|
|
92
|
+
return "zh-CN";
|
|
93
|
+
return DEFAULT_SERV_LOCALE;
|
|
94
|
+
}
|
|
95
|
+
var servM = core.i18n.createMessageGetter({ "zh-CN": zh_CN_default, "en-US": en_US_default });
|
|
96
|
+
var WILDCARD_PERMISSION = "*";
|
|
97
|
+
var WILDCARD_ROLE = "*";
|
|
98
|
+
function applyServGuards(requirements, handler) {
|
|
99
|
+
return async (options) => {
|
|
100
|
+
try {
|
|
101
|
+
if (!hasAuthenticatedSession(options.context)) {
|
|
102
|
+
return err(
|
|
103
|
+
HaiCommonError.UNAUTHORIZED,
|
|
104
|
+
servM("serv_errorUnauthorized", { locale: options.context.locale })
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
const { session } = options.context;
|
|
108
|
+
if (requirements.permissions?.length) {
|
|
109
|
+
const allowed = session.permissions.includes(WILDCARD_PERMISSION) || requirements.permissions.every(
|
|
110
|
+
(permission) => session.permissions.includes(permission)
|
|
111
|
+
);
|
|
112
|
+
if (!allowed) {
|
|
113
|
+
return err(
|
|
114
|
+
HaiCommonError.FORBIDDEN,
|
|
115
|
+
servM("serv_errorForbidden", { locale: options.context.locale })
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (requirements.roles?.length) {
|
|
120
|
+
const allowed = session.roles.includes(WILDCARD_ROLE) || requirements.roles.every(
|
|
121
|
+
(role) => session.roles.includes(role)
|
|
122
|
+
);
|
|
123
|
+
if (!allowed) {
|
|
124
|
+
return err(
|
|
125
|
+
HaiCommonError.FORBIDDEN,
|
|
126
|
+
servM("serv_errorForbidden", { locale: options.context.locale })
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return await handler({
|
|
131
|
+
...options,
|
|
132
|
+
context: options.context
|
|
133
|
+
});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return err(
|
|
136
|
+
HaiCommonError.INTERNAL_ERROR,
|
|
137
|
+
servM("serv_errorInternal", { locale: options.context.locale }),
|
|
138
|
+
error
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function hasAuthenticatedSession(context) {
|
|
144
|
+
return context.session !== void 0 && context.session !== null;
|
|
145
|
+
}
|
|
146
|
+
function buildHaiErrorBody(def, message) {
|
|
147
|
+
return {
|
|
148
|
+
success: false,
|
|
149
|
+
error: {
|
|
150
|
+
code: def.code,
|
|
151
|
+
message,
|
|
152
|
+
httpStatus: def.httpStatus,
|
|
153
|
+
system: def.system,
|
|
154
|
+
module: def.module
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function mapHaiError(handler) {
|
|
159
|
+
return async (options) => {
|
|
160
|
+
try {
|
|
161
|
+
return await handler(options);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
return err(HaiCommonError.INTERNAL_ERROR, servM("serv_errorInternal", { locale: options.context.locale }), error);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/serv-router.ts
|
|
169
|
+
function implement(contract) {
|
|
170
|
+
return {
|
|
171
|
+
context: () => {
|
|
172
|
+
const native = implement$1(contract).$context();
|
|
173
|
+
return createServRouterBuilder(contract, native);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function createServRouterBuilder(contract, implementer) {
|
|
178
|
+
const runtime = new ServRouterBuilderRuntime(contract, implementer);
|
|
179
|
+
return runtime;
|
|
180
|
+
}
|
|
181
|
+
var ServRouterBuilderRuntime = class {
|
|
182
|
+
constructor(contract, implementer) {
|
|
183
|
+
this.implementer = implementer;
|
|
184
|
+
this.expectedPaths = new Set(collectProcedurePaths(contract));
|
|
185
|
+
this.implementedRoutes = createRouterShape(contract);
|
|
186
|
+
}
|
|
187
|
+
expectedPaths;
|
|
188
|
+
implementedRoutes;
|
|
189
|
+
registeredPaths = /* @__PURE__ */ new Set();
|
|
190
|
+
route(path, handler) {
|
|
191
|
+
if (handler !== void 0)
|
|
192
|
+
return this.register(path, {}, handler);
|
|
193
|
+
return new ServRouteRegistrationRuntime(this, path);
|
|
194
|
+
}
|
|
195
|
+
register(path, guards, handler) {
|
|
196
|
+
if (!this.expectedPaths.has(path))
|
|
197
|
+
throw new TypeError(`Contract path is not implementable: ${path}`);
|
|
198
|
+
if (this.registeredPaths.has(path))
|
|
199
|
+
throw new TypeError(`Duplicate procedure implementation: ${path}`);
|
|
200
|
+
if (typeof handler !== "function")
|
|
201
|
+
throw new TypeError(`Procedure handler must be a function: ${path}`);
|
|
202
|
+
const procedureImplementer = getAtPath(this.implementer, path);
|
|
203
|
+
const implementHandler = getHandlerMethod(procedureImplementer);
|
|
204
|
+
if (!implementHandler)
|
|
205
|
+
throw new TypeError(`Contract path is not implementable: ${path}`);
|
|
206
|
+
const safeHandler = hasGuards(guards) ? applyServGuards(guards, handler) : mapHaiError(handler);
|
|
207
|
+
const implemented = Reflect.apply(
|
|
208
|
+
implementHandler,
|
|
209
|
+
procedureImplementer,
|
|
210
|
+
[safeHandler]
|
|
211
|
+
);
|
|
212
|
+
setAtPath(this.implementedRoutes, path, implemented);
|
|
213
|
+
this.registeredPaths.add(path);
|
|
214
|
+
return this;
|
|
215
|
+
}
|
|
216
|
+
build() {
|
|
217
|
+
const missingPaths = [...this.expectedPaths].filter((path) => !this.registeredPaths.has(path)).sort();
|
|
218
|
+
if (missingPaths.length > 0) {
|
|
219
|
+
throw new TypeError(
|
|
220
|
+
`Cannot build router. Missing procedures: ${missingPaths.join(", ")}`
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
const routerMethod = Reflect.get(this.implementer, "router");
|
|
224
|
+
if (typeof routerMethod !== "function")
|
|
225
|
+
throw new TypeError("The oRPC implementer does not expose router()");
|
|
226
|
+
return Reflect.apply(routerMethod, this.implementer, [this.implementedRoutes]);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
var ServRouteRegistrationRuntime = class {
|
|
230
|
+
constructor(parent, path) {
|
|
231
|
+
this.parent = parent;
|
|
232
|
+
this.path = path;
|
|
233
|
+
}
|
|
234
|
+
authRequired = false;
|
|
235
|
+
permissions = /* @__PURE__ */ new Set();
|
|
236
|
+
roles = /* @__PURE__ */ new Set();
|
|
237
|
+
auth() {
|
|
238
|
+
this.authRequired = true;
|
|
239
|
+
return this;
|
|
240
|
+
}
|
|
241
|
+
permission(permission) {
|
|
242
|
+
this.authRequired = true;
|
|
243
|
+
this.permissions.add(permission);
|
|
244
|
+
return this;
|
|
245
|
+
}
|
|
246
|
+
role(role) {
|
|
247
|
+
this.authRequired = true;
|
|
248
|
+
this.roles.add(role);
|
|
249
|
+
return this;
|
|
250
|
+
}
|
|
251
|
+
handle(handler) {
|
|
252
|
+
return this.parent.register(this.path, {
|
|
253
|
+
auth: this.authRequired,
|
|
254
|
+
permissions: [...this.permissions],
|
|
255
|
+
roles: [...this.roles]
|
|
256
|
+
}, handler);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
function hasGuards(guards) {
|
|
260
|
+
return guards.auth === true || (guards.permissions?.length ?? 0) > 0 || (guards.roles?.length ?? 0) > 0;
|
|
261
|
+
}
|
|
262
|
+
function collectProcedurePaths(contract, prefix = []) {
|
|
263
|
+
if (isContractProcedure(contract))
|
|
264
|
+
return prefix.length > 0 ? [prefix.join(".")] : [];
|
|
265
|
+
return Object.entries(contract).flatMap(([key, child]) => collectProcedurePaths(child, [...prefix, key]));
|
|
266
|
+
}
|
|
267
|
+
function isContractProcedure(contract) {
|
|
268
|
+
return "~orpc" in contract;
|
|
269
|
+
}
|
|
270
|
+
function createRouterShape(contract) {
|
|
271
|
+
const shape = /* @__PURE__ */ Object.create(null);
|
|
272
|
+
for (const [key, child] of Object.entries(contract)) {
|
|
273
|
+
if (!isContractProcedure(child))
|
|
274
|
+
shape[key] = createRouterShape(child);
|
|
275
|
+
}
|
|
276
|
+
return shape;
|
|
277
|
+
}
|
|
278
|
+
function getAtPath(root, path) {
|
|
279
|
+
let current = root;
|
|
280
|
+
for (const segment of path.split(".")) {
|
|
281
|
+
if (typeof current !== "object" && typeof current !== "function" || current === null || !(segment in current)) {
|
|
282
|
+
return void 0;
|
|
283
|
+
}
|
|
284
|
+
current = Reflect.get(current, segment);
|
|
285
|
+
}
|
|
286
|
+
return current;
|
|
287
|
+
}
|
|
288
|
+
function setAtPath(root, path, value) {
|
|
289
|
+
const segments = path.split(".");
|
|
290
|
+
const leaf = segments.pop();
|
|
291
|
+
if (!leaf)
|
|
292
|
+
throw new TypeError("Procedure path cannot be empty");
|
|
293
|
+
let current = root;
|
|
294
|
+
for (const segment of segments) {
|
|
295
|
+
const existing = current[segment];
|
|
296
|
+
if (existing === void 0) {
|
|
297
|
+
const child = /* @__PURE__ */ Object.create(null);
|
|
298
|
+
current[segment] = child;
|
|
299
|
+
current = child;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (!existing || typeof existing !== "object")
|
|
303
|
+
throw new TypeError(`Procedure path conflicts with an existing route: ${path}`);
|
|
304
|
+
current = existing;
|
|
305
|
+
}
|
|
306
|
+
current[leaf] = value;
|
|
307
|
+
}
|
|
308
|
+
function getHandlerMethod(value) {
|
|
309
|
+
if (typeof value !== "object" && typeof value !== "function" || value === null)
|
|
310
|
+
return void 0;
|
|
311
|
+
const handler = Reflect.get(value, "handler");
|
|
312
|
+
return typeof handler === "function" ? handler : void 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export { WILDCARD_PERMISSION, WILDCARD_ROLE, buildHaiErrorBody, implement, normalizeServLocale, servM };
|
|
316
|
+
//# sourceMappingURL=chunk-KIWHKWLD.js.map
|
|
317
|
+
//# sourceMappingURL=chunk-KIWHKWLD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../messages/en-US.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-KIWHKWLD.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} characters\",\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\": \"最多输入 {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\": \"请输入有效的网址\",\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 messagesZhCN from '../messages/zh-CN.json'\n\ntype ServMessageKey = keyof typeof messagesZhCN\n\nconst SUPPORTED_SERV_LOCALES = ['zh-CN', 'en-US'] as const\nconst DEFAULT_SERV_LOCALE = 'zh-CN'\n\n/**\n * 规范化 serv 请求 locale。\n *\n * 支持:\n * - 完整 locale:`zh-CN` / `en-US`\n * - 简写:`zh` / `en`\n * - `Accept-Language` 风格值:`en-US,en;q=0.9`\n *\n * 未识别时回退到默认 `zh-CN`。\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 === '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 * // 使用全局 locale\n * servM('serv_validationRequired')\n *\n * // 使用请求自身的 locale\n * servM('serv_validationStringMin', { locale: 'en-US', params: { min: 3 } })\n * ```\n */\nexport const servM\n = core.i18n.createMessageGetter<ServMessageKey>({ 'zh-CN': messagesZhCN, 'en-US': messagesEnUS })\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 */\nexport const WILDCARD_PERMISSION = '*'\n\n/**\n * 通配符角色。仅应授予受信任的超级管理员,可通过任意 role guard。\n */\nexport const WILDCARD_ROLE = '*'\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 */\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/** 构造 `HaiResult` 失败分支同构的 HTTP 错误定义。 */\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 */\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 */\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} 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\ntype EmptyContext = Record<never, never>\ntype StringKey<T> = Extract<keyof T, string>\ntype ServContract = ContractRouter<Meta>\n\n/** contract 中所有 procedure 的点分路径。 */\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\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\ntype RouteContract<\n TContract extends ServContract,\n TPath extends ServRoutePath<TContract>,\n> = ContractAtPath<TContract, TPath>\n\n/** `.auth()`、`.permission()`、`.role()` 后的非空会话上下文。 */\nexport type AuthenticatedServContext<TContext extends ServContext = ServContext>\n = TContext & {\n readonly session: NonNullable<TContext['session']>\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/** 从 contract procedure 精确推导当前 route 的 HaiResult handler。 */\nexport type ServRouteHandler<\n TContract extends ServContract,\n TPath extends ServRoutePath<TContract>,\n TContext extends Context,\n> = RouteHandler<RouteContract<TContract, TPath>, TContext>\n\ntype ServGuardedRouteHandler<\n TContract extends ServContract,\n TPath extends ServRoutePath<TContract>,\n TContext extends ServContext,\n> = RouteHandler<RouteContract<TContract, TPath>, AuthenticatedServContext<TContext>>\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/** 尚未声明 guard 的单 route 注册器。 */\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/** 已声明认证或授权 guard 的单 route 注册器。 */\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\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\ntype ServRouterBuilderBuild<\n TContract extends ServContract,\n TInitialContext extends Context,\n TRemaining,\n> = [TRemaining] extends [never]\n ? {\n /** 所有 contract procedures 均实现后才可调用。 */\n readonly build: () => 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 */\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/** hai-framework 链式 contract 实现器。 */\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 * serv\n * .implement(contract)\n * .context<ServContext>()\n * .route('info', infoHandler)\n * .route('echo')\n * .auth()\n * .handle(echoHandler)\n * .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\ntype RuntimeProcedureHandler = ProcedureHandler<\n ServContext,\n unknown,\n HaiResult<unknown>,\n ErrorMap,\n Meta\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"]}
|