@h-ai/serv 0.1.0-alpha.67 → 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.en.md +53 -19
- package/README.ja.md +53 -19
- package/README.md +54 -20
- package/dist/chunk-BGWE4S6Z.js +37 -0
- package/dist/chunk-BGWE4S6Z.js.map +1 -0
- package/dist/{chunk-R354OMWL.js → chunk-TWHCU6HT.js} +48 -6
- package/dist/chunk-TWHCU6HT.js.map +1 -0
- package/dist/{chunk-27PCPE37.js → chunk-XKCLD42I.js} +2 -2
- package/dist/chunk-XKCLD42I.js.map +1 -0
- package/dist/features/ai.d.ts +139 -57
- package/dist/features/ai.js +57 -46
- package/dist/features/ai.js.map +1 -1
- package/dist/features/iam.d.ts +29 -4
- package/dist/features/iam.js +3 -3
- package/dist/features/iam.js.map +1 -1
- package/dist/features/storage.d.ts +94 -9
- package/dist/features/storage.js +48 -12
- package/dist/features/storage.js.map +1 -1
- package/dist/index.d.ts +587 -187
- package/dist/index.js +89 -111
- package/dist/index.js.map +1 -1
- package/dist/{serv-context-CWzyrsdq.d.ts → serv-context-C48WdBcd.d.ts} +127 -7
- package/dist/serv-router-BX8hgYxo.d.ts +322 -0
- package/package.json +14 -10
- package/dist/chunk-27PCPE37.js.map +0 -1
- package/dist/chunk-R354OMWL.js.map +0 -1
|
@@ -21,6 +21,17 @@ import { HaiResult, Logger } from '@h-ai/core';
|
|
|
21
21
|
/**
|
|
22
22
|
* Refresh 流程返回的 Token 对结构。
|
|
23
23
|
* 与 `@h-ai/iam` 的 `TokenPair` 结构兼容;此处独立定义以避免对可选 peer dep 的强依赖。
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* import type { RefreshTokenPair } from '@h-ai/serv'
|
|
28
|
+
* import { serv } from '@h-ai/serv'
|
|
29
|
+
*
|
|
30
|
+
* function publicAccess(pair: RefreshTokenPair) {
|
|
31
|
+
* return { accessToken: pair.accessToken, expiresIn: pair.expiresIn }
|
|
32
|
+
* }
|
|
33
|
+
* // 浏览器 refreshToken 只走 HttpOnly Cookie,不能写入可读持久存储。
|
|
34
|
+
* ```
|
|
24
35
|
*/
|
|
25
36
|
interface RefreshTokenPair {
|
|
26
37
|
readonly accessToken: string;
|
|
@@ -34,6 +45,17 @@ interface RefreshTokenPair {
|
|
|
34
45
|
* 浏览器继续使用 httpOnly cookie;只有经过应用可信来源校验的原生请求,才允许在
|
|
35
46
|
* JSON body 中接收和返回 refresh token。`isRequest` 必须同时校验客户端标识与 Origin,
|
|
36
47
|
* 不能仅信任可伪造的单个请求头。
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* import type { NativeRefreshTokenTransportConfig } from '@h-ai/serv'
|
|
52
|
+
* import { serv } from '@h-ai/serv'
|
|
53
|
+
*
|
|
54
|
+
* function configureNative(isTrustedRequest: NativeRefreshTokenTransportConfig['isRequest']): NativeRefreshTokenTransportConfig {
|
|
55
|
+
* return { isRequest: isTrustedRequest }
|
|
56
|
+
* }
|
|
57
|
+
* // 宿主同时验证原生客户端标识与允许的 Origin,不能只检查单个可伪造头。
|
|
58
|
+
* ```
|
|
37
59
|
*/
|
|
38
60
|
interface NativeRefreshTokenTransportConfig {
|
|
39
61
|
/** 判断当前请求是否来自受信原生客户端。 */
|
|
@@ -48,6 +70,15 @@ interface NativeRefreshTokenTransportConfig {
|
|
|
48
70
|
* 刷新回调来源(优先级从高到低):
|
|
49
71
|
* 1. `onRefresh`(显式覆盖)
|
|
50
72
|
* 2. 顶层 `iam.session.refresh`(推荐:`createApp({ iam, refreshCookie: {} })` 即开箱可用)
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* import type { RefreshCookieConfig } from '@h-ai/serv'
|
|
77
|
+
* import { serv } from '@h-ai/serv'
|
|
78
|
+
*
|
|
79
|
+
* const config: RefreshCookieConfig = { cookieName: 'hai_refresh_token', maxAge: 2592000, secure: true }
|
|
80
|
+
* // createApp 同时提供 iam 或 onRefresh;此配置只负责 refresh 传输,不校验 access。
|
|
81
|
+
* ```
|
|
51
82
|
*/
|
|
52
83
|
interface RefreshCookieConfig {
|
|
53
84
|
/** Cookie 名,默认 `hai_refresh_token`。 */
|
|
@@ -59,7 +90,7 @@ interface RefreshCookieConfig {
|
|
|
59
90
|
readonly maxAge?: number;
|
|
60
91
|
/**
|
|
61
92
|
* 强制 Secure 属性。
|
|
62
|
-
* - 未传时:`NODE_ENV
|
|
93
|
+
* - 未传时:`NODE_ENV` 或 `HAI_CORE_ENV` 为 `production` 时自动开启;测试开关不放宽此要求。
|
|
63
94
|
* - 本地 HTTP 开发设为 `false` 可让 Cookie 在 HTTP 下正常工作。
|
|
64
95
|
*/
|
|
65
96
|
readonly secure?: boolean;
|
|
@@ -93,7 +124,20 @@ interface RefreshCookieConfig {
|
|
|
93
124
|
* @module serv-context
|
|
94
125
|
*/
|
|
95
126
|
|
|
96
|
-
/**
|
|
127
|
+
/**
|
|
128
|
+
* 当前请求解析出的会话摘要。
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```ts
|
|
132
|
+
* import type { ServSession } from '@h-ai/serv'
|
|
133
|
+
* import { serv } from '@h-ai/serv'
|
|
134
|
+
*
|
|
135
|
+
* function canRead(session: ServSession): boolean {
|
|
136
|
+
* return session.permissions.includes('reports.read')
|
|
137
|
+
* }
|
|
138
|
+
* // 会话来自可信 verifyToken,不能将客户端 userId/roles 直接提升为会话。
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
97
141
|
interface ServSession {
|
|
98
142
|
readonly userId: string;
|
|
99
143
|
readonly username?: string;
|
|
@@ -109,6 +153,17 @@ interface ServSession {
|
|
|
109
153
|
* - refresh token 轮换(仅当启用 `refreshCookie` 时使用)
|
|
110
154
|
*
|
|
111
155
|
* 模块消费者**无需**知道 IAM 内部 Session 类型与 ServSession 的差异:结构类型确保兼容。
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* ```ts
|
|
159
|
+
* import type { ServIam } from '@h-ai/serv'
|
|
160
|
+
* import { serv } from '@h-ai/serv'
|
|
161
|
+
*
|
|
162
|
+
* import { iam } from '@h-ai/iam'
|
|
163
|
+
*
|
|
164
|
+
* const auth: ServIam = iam
|
|
165
|
+
* const factory = serv.buildAuthContextFactory(token => auth.auth.verifyToken(token))
|
|
166
|
+
* ```
|
|
112
167
|
*/
|
|
113
168
|
interface ServIam {
|
|
114
169
|
readonly auth: {
|
|
@@ -116,11 +171,23 @@ interface ServIam {
|
|
|
116
171
|
verifyToken: (accessToken: string) => Promise<HaiResult<ServSession>>;
|
|
117
172
|
};
|
|
118
173
|
readonly session: {
|
|
119
|
-
/** 使用 refresh token
|
|
174
|
+
/** 使用 refresh token 换发新令牌对;refreshCookie 启用时调用,不要求旧 access 有效。 */
|
|
120
175
|
refresh: (refreshToken: string) => Promise<HaiResult<RefreshTokenPair>>;
|
|
121
176
|
};
|
|
122
177
|
}
|
|
123
|
-
/**
|
|
178
|
+
/**
|
|
179
|
+
* `@h-ai/serv` 在 procedures 中传递的统一上下文。
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* ```ts
|
|
183
|
+
* import type { ServContext } from '@h-ai/serv'
|
|
184
|
+
* import { serv } from '@h-ai/serv'
|
|
185
|
+
*
|
|
186
|
+
* const context: ServContext = serv.parseRequestContext({ request: new Request('https://example.com/health'), remoteAddress: '192.0.2.10' })
|
|
187
|
+
* const locale = context.locale
|
|
188
|
+
* // parseRequestContext 只读元数据,不产生已认证 session。
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
124
191
|
interface ServContext {
|
|
125
192
|
readonly requestId: string;
|
|
126
193
|
readonly locale: string;
|
|
@@ -131,17 +198,50 @@ interface ServContext {
|
|
|
131
198
|
readonly request: Request;
|
|
132
199
|
readonly logger: Logger;
|
|
133
200
|
}
|
|
134
|
-
/**
|
|
201
|
+
/**
|
|
202
|
+
* 运行时提供的连接元数据;必须来自 socket/平台,不能从请求头复制。
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```ts
|
|
206
|
+
* import type { ServRequestMetadata } from '@h-ai/serv'
|
|
207
|
+
* import { serv } from '@h-ai/serv'
|
|
208
|
+
*
|
|
209
|
+
* const metadata: ServRequestMetadata = { remoteAddress: '192.0.2.10' }
|
|
210
|
+
* // 由运行时 socket 提供;转发头必须另经过 trustedProxies 策略。
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
135
213
|
interface ServRequestMetadata {
|
|
136
214
|
readonly remoteAddress?: string;
|
|
137
215
|
}
|
|
138
|
-
/**
|
|
216
|
+
/**
|
|
217
|
+
* 创建上下文所需的输入。
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* ```ts
|
|
221
|
+
* import type { CreateServContextInput } from '@h-ai/serv'
|
|
222
|
+
* import { serv } from '@h-ai/serv'
|
|
223
|
+
*
|
|
224
|
+
* const input: CreateServContextInput = { request: new Request('https://example.com/health'), remoteAddress: '192.0.2.10' }
|
|
225
|
+
* const context = serv.parseRequestContext(input)
|
|
226
|
+
* ```
|
|
227
|
+
*/
|
|
139
228
|
interface CreateServContextInput extends ServRequestMetadata {
|
|
140
229
|
readonly request: Request;
|
|
141
230
|
/** createApp 已按 trustedProxies 解析的客户端地址,供自定义上下文复用。 */
|
|
142
231
|
readonly ip?: string;
|
|
143
232
|
}
|
|
144
|
-
/**
|
|
233
|
+
/**
|
|
234
|
+
* 应用可传入的上下文工厂。
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```ts
|
|
238
|
+
* import type { CreateServContext } from '@h-ai/serv'
|
|
239
|
+
* import { serv } from '@h-ai/serv'
|
|
240
|
+
*
|
|
241
|
+
* const factory: CreateServContext = input => serv.parseRequestContext(input)
|
|
242
|
+
* const context = await factory({ request: new Request('https://example.com/health') })
|
|
243
|
+
* ```
|
|
244
|
+
*/
|
|
145
245
|
type CreateServContext = (input: CreateServContextInput) => ServContext | Promise<ServContext>;
|
|
146
246
|
/**
|
|
147
247
|
* 解析 HTTP 请求元数据并构造基础 ServContext(**同步**,**不**填充 session)。
|
|
@@ -155,6 +255,15 @@ type CreateServContext = (input: CreateServContextInput) => ServContext | Promis
|
|
|
155
255
|
*
|
|
156
256
|
* @param input - 上下文输入
|
|
157
257
|
* @returns 仅含元数据的 ServContext
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* ```ts
|
|
261
|
+
* import { serv } from '@h-ai/serv'
|
|
262
|
+
*
|
|
263
|
+
* const request = new Request('https://example.com/health', { headers: { 'x-hai-locale': 'en-US' } })
|
|
264
|
+
* const context = serv.parseRequestContext({ request, remoteAddress: '192.0.2.10' })
|
|
265
|
+
* const locale = context.locale
|
|
266
|
+
* ```
|
|
158
267
|
*/
|
|
159
268
|
declare function parseRequestContext(input: CreateServContextInput): ServContext;
|
|
160
269
|
/**
|
|
@@ -176,6 +285,17 @@ declare function parseRequestContext(input: CreateServContextInput): ServContext
|
|
|
176
285
|
*
|
|
177
286
|
* @param verifyToken - 访问令牌校验回调(通常是 `token => iam.auth.verifyToken(token)` 或自定义函数)
|
|
178
287
|
* @returns 异步上下文工厂
|
|
288
|
+
*
|
|
289
|
+
* @example
|
|
290
|
+
* ```ts
|
|
291
|
+
* import { iam } from '@h-ai/iam'
|
|
292
|
+
* import { serv } from '@h-ai/serv'
|
|
293
|
+
*
|
|
294
|
+
* // IAM 已初始化;每个请求重新验证 token 与当前持久凭据/权限状态。
|
|
295
|
+
* const factory = serv.buildAuthContextFactory(token => iam.auth.verifyToken(token))
|
|
296
|
+
* const context = await factory({ request: new Request('https://example.com/profile') })
|
|
297
|
+
* // 未携带/验证失败时 session 为空,由 route guard 决定是否拒绝。
|
|
298
|
+
* ```
|
|
179
299
|
*/
|
|
180
300
|
declare function buildAuthContextFactory(verifyToken: (accessToken: string) => Promise<HaiResult<ServSession>>): CreateServContext;
|
|
181
301
|
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { HaiResult } from '@h-ai/core';
|
|
2
|
+
import { ContractRouter, Meta, Context, ContractProcedure, InferSchemaInput, ProcedureHandler, InferSchemaOutput, UnlaziedRouter, Router } from '@orpc/server';
|
|
3
|
+
import { S as ServContext } from './serv-context-C48WdBcd.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @h-ai/serv — contract-first 链式 procedure 实现器
|
|
7
|
+
*
|
|
8
|
+
* 在 oRPC `implement()` 之上提供 route 名只出现一次的链式 DSL。
|
|
9
|
+
* 应用只接触 contract 路径、上下文与 guard,不需要组装 oRPC router 对象。
|
|
10
|
+
*
|
|
11
|
+
* @module serv-router
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 内部初始 context 空约束,用于保持 oRPC 推导而不伪造运行时字段。
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* // 模块内部上下文:示例复用当前声明所在模块的实际绑定。
|
|
20
|
+
* const { serv } = await import('@h-ai/serv')
|
|
21
|
+
* const context: ServContext & EmptyContext = serv.parseRequestContext({ request: new Request('https://example.com/ping') })
|
|
22
|
+
* const requestId = context.requestId
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
type EmptyContext = Record<never, never>;
|
|
26
|
+
/**
|
|
27
|
+
* 提取 contract 的字符串键,排除 symbol/数字索引。
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* // 模块内部上下文:示例复用当前声明所在模块的实际绑定。
|
|
32
|
+
* const { apiContract } = await import('@h-ai/api-contract')
|
|
33
|
+
* const { z } = await import('zod')
|
|
34
|
+
* const { ok } = await import('@h-ai/core')
|
|
35
|
+
* const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }
|
|
36
|
+
* const path: StringKey<typeof contract> = 'ping'
|
|
37
|
+
* const procedure = contract[path]
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
type StringKey<T> = Extract<keyof T, string>;
|
|
41
|
+
/**
|
|
42
|
+
* 内部 contract 泛型边界;真实 procedure 输出须为 HaiResult。
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* // 模块内部上下文:示例复用当前声明所在模块的实际绑定。
|
|
47
|
+
* const { apiContract } = await import('@h-ai/api-contract')
|
|
48
|
+
* const { z } = await import('zod')
|
|
49
|
+
* const { ok } = await import('@h-ai/core')
|
|
50
|
+
* const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }
|
|
51
|
+
* const checked = contract satisfies ServContract
|
|
52
|
+
* const procedures = implement(checked).context<ServContext>().route('ping', () => ok({ ready: true })).build()
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
type ServContract = ContractRouter<Meta>;
|
|
56
|
+
/**
|
|
57
|
+
* contract 中所有 procedure 的点分路径。
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* import type { ServRoutePath } from '@h-ai/serv'
|
|
62
|
+
* import type { ServContext } from '@h-ai/serv'
|
|
63
|
+
* import { apiContract } from '@h-ai/api-contract'
|
|
64
|
+
* import { ok } from '@h-ai/core'
|
|
65
|
+
* import { serv } from '@h-ai/serv'
|
|
66
|
+
* import { z } from 'zod'
|
|
67
|
+
*
|
|
68
|
+
* const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }
|
|
69
|
+
* const path: ServRoutePath<typeof contract> = 'ping'
|
|
70
|
+
* const procedures = serv.implement(contract).context<ServContext>().route(path, () => ok({ ready: true })).build()
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
type ServRoutePath<TContract extends ServContract> = TContract extends ContractProcedure<infer _TInputSchema, infer _TOutputSchema, infer _TErrorMap, infer _TMeta> ? never : {
|
|
74
|
+
[K in StringKey<TContract>]: TContract[K] extends ContractProcedure<infer _TChildInputSchema, infer _TChildOutputSchema, infer _TChildErrorMap, infer _TChildMeta> ? K : TContract[K] extends ServContract ? ServRoutePath<TContract[K]> extends infer TChild extends string ? `${K}.${TChild}` : never : never;
|
|
75
|
+
}[StringKey<TContract>];
|
|
76
|
+
/**
|
|
77
|
+
* 按点分路径查找原始 contract procedure;不存在的路径推导为 never。
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```ts
|
|
81
|
+
* // 模块内部上下文:示例复用当前声明所在模块的实际绑定。
|
|
82
|
+
* const { apiContract } = await import('@h-ai/api-contract')
|
|
83
|
+
* const { z } = await import('zod')
|
|
84
|
+
* const { ok } = await import('@h-ai/core')
|
|
85
|
+
* const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }
|
|
86
|
+
* const procedure: ContractAtPath<typeof contract, 'ping'> = contract.ping
|
|
87
|
+
* const scoped = implement({ ping: procedure }).context<ServContext>().route('ping', () => ok({ ready: true })).build()
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
type ContractAtPath<TContract, TPath extends string> = TPath extends `${infer THead}.${infer TTail}` ? THead extends keyof TContract ? ContractAtPath<TContract[THead], TTail> : never : TPath extends keyof TContract ? TContract[TPath] : never;
|
|
91
|
+
/**
|
|
92
|
+
* 将合法路由路径映射到对应 contract,供 handler 推导复用。
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* // 模块内部上下文:示例复用当前声明所在模块的实际绑定。
|
|
97
|
+
* const { apiContract } = await import('@h-ai/api-contract')
|
|
98
|
+
* const { z } = await import('zod')
|
|
99
|
+
* const { ok } = await import('@h-ai/core')
|
|
100
|
+
* const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }
|
|
101
|
+
* const procedure: RouteContract<typeof contract, 'ping'> = contract.ping
|
|
102
|
+
* const scoped = implement({ ping: procedure }).context<ServContext>().route('ping', () => ok({ ready: true })).build()
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
type RouteContract<TContract extends ServContract, TPath extends ServRoutePath<TContract>> = ContractAtPath<TContract, TPath>;
|
|
106
|
+
/**
|
|
107
|
+
* `.auth()`、`.permission()`、`.role()` 后的非空会话上下文。
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```ts
|
|
111
|
+
* import type { AuthenticatedServContext } from '@h-ai/serv'
|
|
112
|
+
*
|
|
113
|
+
* function owner(context: AuthenticatedServContext): string {
|
|
114
|
+
* return context.session.userId
|
|
115
|
+
* }
|
|
116
|
+
* // 仅成功 guard 或可信认证结果能建立此上下文,禁止用客户端字段断言。
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
type AuthenticatedServContext<TContext extends ServContext = ServContext> = TContext & {
|
|
120
|
+
readonly session: NonNullable<TContext['session']>;
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* 内部从具体 procedure 的输入/输出与 context 推导 HaiResult handler。
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```ts
|
|
127
|
+
* // 模块内部上下文:示例复用当前声明所在模块的实际绑定。
|
|
128
|
+
* const { apiContract } = await import('@h-ai/api-contract')
|
|
129
|
+
* const { z } = await import('zod')
|
|
130
|
+
* const { ok } = await import('@h-ai/core')
|
|
131
|
+
* const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }
|
|
132
|
+
* const handler: RouteHandler<typeof contract.ping, ServContext> = () => ok({ ready: true })
|
|
133
|
+
* const procedures = implement(contract).context<ServContext>().route('ping', handler).build()
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
type RouteHandler<TRouteContract, TContext extends Context> = TRouteContract extends ContractProcedure<infer TInputSchema, infer TOutputSchema, infer TErrorMap, infer TMeta> ? InferSchemaInput<TOutputSchema> extends HaiResult<unknown> ? ProcedureHandler<TContext, InferSchemaOutput<TInputSchema>, InferSchemaInput<TOutputSchema>, TErrorMap, TMeta> : never : never;
|
|
137
|
+
/**
|
|
138
|
+
* 从 contract procedure 精确推导当前 route 的 HaiResult handler。
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```ts
|
|
142
|
+
* import type { ServRouteHandler } from '@h-ai/serv'
|
|
143
|
+
* import type { ServContext } from '@h-ai/serv'
|
|
144
|
+
* import { apiContract } from '@h-ai/api-contract'
|
|
145
|
+
* import { ok } from '@h-ai/core'
|
|
146
|
+
* import { serv } from '@h-ai/serv'
|
|
147
|
+
* import { z } from 'zod'
|
|
148
|
+
*
|
|
149
|
+
* const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }
|
|
150
|
+
* const handler: ServRouteHandler<typeof contract, 'ping', ServContext> = () => ok({ ready: true })
|
|
151
|
+
* const procedures = serv.implement(contract).context<ServContext>().route('ping', handler).build()
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
type ServRouteHandler<TContract extends ServContract, TPath extends ServRoutePath<TContract>, TContext extends Context> = RouteHandler<RouteContract<TContract, TPath>, TContext>;
|
|
155
|
+
/**
|
|
156
|
+
* 内部认证成功 handler 类型;session 在 guard 之后非空。
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```ts
|
|
160
|
+
* // 模块内部上下文:示例复用当前声明所在模块的实际绑定。
|
|
161
|
+
* const { apiContract } = await import('@h-ai/api-contract')
|
|
162
|
+
* const { z } = await import('zod')
|
|
163
|
+
* const { ok } = await import('@h-ai/core')
|
|
164
|
+
* const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }
|
|
165
|
+
* const handler: ServGuardedRouteHandler<typeof contract, 'ping', ServContext> = ({ context }) => ok({ ready: context.session.userId.length > 0 })
|
|
166
|
+
* const procedures = implement(contract).context<ServContext>().route('ping').auth().handle(handler).build()
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
type ServGuardedRouteHandler<TContract extends ServContract, TPath extends ServRoutePath<TContract>, TContext extends ServContext> = RouteHandler<RouteContract<TContract, TPath>, AuthenticatedServContext<TContext>>;
|
|
170
|
+
/**
|
|
171
|
+
* 一次注册后移除该路径的 builder;全部路径完成才提供 build。
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```ts
|
|
175
|
+
* // 模块内部上下文:示例复用当前声明所在模块的实际绑定。
|
|
176
|
+
* const { apiContract } = await import('@h-ai/api-contract')
|
|
177
|
+
* const { z } = await import('zod')
|
|
178
|
+
* const { ok } = await import('@h-ai/core')
|
|
179
|
+
* const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }
|
|
180
|
+
* const next: NextBuilder<typeof contract, ServContext, ServContext, 'ping', 'ping'> = implement(contract).context<ServContext>().route('ping', () => ok({ ready: true }))
|
|
181
|
+
* const procedures = next.build()
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
type NextBuilder<TContract extends ServContract, TInitialContext extends Context, TCurrentContext extends ServContext, TRemaining extends ServRoutePath<TContract>, TPath extends TRemaining> = ServRouterBuilder<TContract, TInitialContext, TCurrentContext, Exclude<TRemaining, TPath>>;
|
|
185
|
+
/**
|
|
186
|
+
* 尚未声明 guard 的单 route 注册器。
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```ts
|
|
190
|
+
* // 模块内部上下文:示例复用当前声明所在模块的实际绑定。
|
|
191
|
+
* const { apiContract } = await import('@h-ai/api-contract')
|
|
192
|
+
* const { z } = await import('zod')
|
|
193
|
+
* const { ok } = await import('@h-ai/core')
|
|
194
|
+
* const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }
|
|
195
|
+
* const route: ServRouteRegistration<typeof contract, ServContext, ServContext, 'ping', 'ping'> = implement(contract).context<ServContext>().route('ping')
|
|
196
|
+
* const procedures = route.auth().handle(() => ok({ ready: true })).build()
|
|
197
|
+
* ```
|
|
198
|
+
*/
|
|
199
|
+
interface ServRouteRegistration<TContract extends ServContract, TInitialContext extends Context, TCurrentContext extends ServContext, TRemaining extends ServRoutePath<TContract>, TPath extends TRemaining> {
|
|
200
|
+
readonly auth: () => ServGuardedRouteRegistration<TContract, TInitialContext, TCurrentContext, TRemaining, TPath>;
|
|
201
|
+
readonly permission: (permission: string) => ServGuardedRouteRegistration<TContract, TInitialContext, TCurrentContext, TRemaining, TPath>;
|
|
202
|
+
readonly role: (role: string) => ServGuardedRouteRegistration<TContract, TInitialContext, TCurrentContext, TRemaining, TPath>;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* 已声明认证或授权 guard 的单 route 注册器。
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```ts
|
|
209
|
+
* // 模块内部上下文:示例复用当前声明所在模块的实际绑定。
|
|
210
|
+
* const { apiContract } = await import('@h-ai/api-contract')
|
|
211
|
+
* const { z } = await import('zod')
|
|
212
|
+
* const { ok } = await import('@h-ai/core')
|
|
213
|
+
* const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }
|
|
214
|
+
* const guarded: ServGuardedRouteRegistration<typeof contract, ServContext, ServContext, 'ping', 'ping'> = implement(contract).context<ServContext>().route('ping').permission('reports.read')
|
|
215
|
+
* const procedures = guarded.handle(() => ok({ ready: true })).build()
|
|
216
|
+
* ```
|
|
217
|
+
*/
|
|
218
|
+
interface ServGuardedRouteRegistration<TContract extends ServContract, TInitialContext extends Context, TCurrentContext extends ServContext, TRemaining extends ServRoutePath<TContract>, TPath extends TRemaining> {
|
|
219
|
+
readonly auth: () => ServGuardedRouteRegistration<TContract, TInitialContext, TCurrentContext, TRemaining, TPath>;
|
|
220
|
+
readonly permission: (permission: string) => ServGuardedRouteRegistration<TContract, TInitialContext, TCurrentContext, TRemaining, TPath>;
|
|
221
|
+
readonly role: (role: string) => ServGuardedRouteRegistration<TContract, TInitialContext, TCurrentContext, TRemaining, TPath>;
|
|
222
|
+
readonly handle: (handler: ServGuardedRouteHandler<TContract, TPath, TCurrentContext>) => NextBuilder<TContract, TInitialContext, TCurrentContext, TRemaining, TPath>;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* 内部 route 重载:直接 handler 或先声明 guard,再收窄剩余路径。
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```ts
|
|
229
|
+
* // 模块内部上下文:示例复用当前声明所在模块的实际绑定。
|
|
230
|
+
* const { apiContract } = await import('@h-ai/api-contract')
|
|
231
|
+
* const { z } = await import('zod')
|
|
232
|
+
* const { ok } = await import('@h-ai/core')
|
|
233
|
+
* const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }
|
|
234
|
+
* const route: ServRouteMethod<typeof contract, ServContext, ServContext, 'ping'> = implement(contract).context<ServContext>().route
|
|
235
|
+
* const procedures = route('ping', () => ok({ ready: true })).build()
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
interface ServRouteMethod<TContract extends ServContract, TInitialContext extends Context, TCurrentContext extends ServContext, TRemaining extends ServRoutePath<TContract>> {
|
|
239
|
+
<TPath extends TRemaining>(path: TPath, handler: ServRouteHandler<TContract, TPath, TCurrentContext>): NextBuilder<TContract, TInitialContext, TCurrentContext, TRemaining, TPath>;
|
|
240
|
+
<TPath extends TRemaining>(path: TPath): ServRouteRegistration<TContract, TInitialContext, TCurrentContext, TRemaining, TPath>;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* 剩余路径为 never 时才暴露 build,否则仅提供缺失路径类型提示。
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* ```ts
|
|
247
|
+
* // 模块内部上下文:示例复用当前声明所在模块的实际绑定。
|
|
248
|
+
* const { apiContract } = await import('@h-ai/api-contract')
|
|
249
|
+
* const { z } = await import('zod')
|
|
250
|
+
* const { ok } = await import('@h-ai/core')
|
|
251
|
+
* const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }
|
|
252
|
+
* const complete: ServRouterBuilderBuild<typeof contract, ServContext, never> = implement(contract).context<ServContext>().route('ping', () => ok({ ready: true }))
|
|
253
|
+
* const procedures = complete.build()
|
|
254
|
+
* ```
|
|
255
|
+
*/
|
|
256
|
+
type ServRouterBuilderBuild<TContract extends ServContract, TInitialContext extends Context, TRemaining> = [TRemaining] extends [never] ? {
|
|
257
|
+
/** 同步返回完整的普通 router,允许按需选用嵌套的默认 procedures。 */
|
|
258
|
+
readonly build: () => UnlaziedRouter<Router<TContract, TInitialContext>>;
|
|
259
|
+
} : {
|
|
260
|
+
/** 仅供 TypeScript/IDE 展示尚未实现的 route;运行时不存在。 */
|
|
261
|
+
readonly missingRoutes: TRemaining;
|
|
262
|
+
};
|
|
263
|
+
/**
|
|
264
|
+
* 链式 router builder。
|
|
265
|
+
*
|
|
266
|
+
* `TRemaining` 会随着 `.route()` 注册逐步收窄;未实现完整 contract 时没有 `.build()`。
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* ```ts
|
|
270
|
+
* import type { ServRouterBuilder } from '@h-ai/serv'
|
|
271
|
+
* import type { ServContext } from '@h-ai/serv'
|
|
272
|
+
* import { apiContract } from '@h-ai/api-contract'
|
|
273
|
+
* import { ok } from '@h-ai/core'
|
|
274
|
+
* import { serv } from '@h-ai/serv'
|
|
275
|
+
* import { z } from 'zod'
|
|
276
|
+
*
|
|
277
|
+
* const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }
|
|
278
|
+
* const builder: ServRouterBuilder<typeof contract, ServContext, ServContext> = serv.implement(contract).context<ServContext>()
|
|
279
|
+
* const procedures = builder.route('ping', () => ok({ ready: true })).build()
|
|
280
|
+
* ```
|
|
281
|
+
*/
|
|
282
|
+
type ServRouterBuilder<TContract extends ServContract, TInitialContext extends Context, TCurrentContext extends ServContext, TRemaining extends ServRoutePath<TContract> = ServRoutePath<TContract>> = {
|
|
283
|
+
readonly route: ServRouteMethod<TContract, TInitialContext, TCurrentContext, TRemaining>;
|
|
284
|
+
} & ServRouterBuilderBuild<TContract, TInitialContext, TRemaining>;
|
|
285
|
+
/**
|
|
286
|
+
* hai-framework 链式 contract 实现器。
|
|
287
|
+
*
|
|
288
|
+
* @example
|
|
289
|
+
* ```ts
|
|
290
|
+
* import type { ServImplementer } from '@h-ai/serv'
|
|
291
|
+
* import type { ServContext } from '@h-ai/serv'
|
|
292
|
+
* import { apiContract } from '@h-ai/api-contract'
|
|
293
|
+
* import { ok } from '@h-ai/core'
|
|
294
|
+
* import { serv } from '@h-ai/serv'
|
|
295
|
+
* import { z } from 'zod'
|
|
296
|
+
*
|
|
297
|
+
* const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }
|
|
298
|
+
* const implementer: ServImplementer<typeof contract> = serv.implement(contract)
|
|
299
|
+
* const procedures = implementer.context<ServContext>().route('ping', () => ok({ ready: true })).build()
|
|
300
|
+
* ```
|
|
301
|
+
*/
|
|
302
|
+
interface ServImplementer<TContract extends ServContract> {
|
|
303
|
+
readonly context: <TContext extends ServContext>() => ServRouterBuilder<TContract, TContext & EmptyContext, TContext>;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* 创建链式 contract 实现器。
|
|
307
|
+
*
|
|
308
|
+
* @example
|
|
309
|
+
* ```ts
|
|
310
|
+
* import type { ServContext } from '@h-ai/serv'
|
|
311
|
+
* import { apiContract } from '@h-ai/api-contract'
|
|
312
|
+
* import { ok } from '@h-ai/core'
|
|
313
|
+
* import { serv } from '@h-ai/serv'
|
|
314
|
+
* import { z } from 'zod'
|
|
315
|
+
*
|
|
316
|
+
* const contract = { ping: apiContract.route({ method: 'GET', path: '/ping' }).output(apiContract.haiResultSchema(z.object({ ready: z.boolean() }))) }
|
|
317
|
+
* const procedures = serv.implement(contract).context<ServContext>().route('ping', () => ok({ ready: true })).build()
|
|
318
|
+
* ```
|
|
319
|
+
*/
|
|
320
|
+
declare function implement<TContract extends ServContract>(contract: TContract): ServImplementer<TContract>;
|
|
321
|
+
|
|
322
|
+
export { type AuthenticatedServContext as A, type ServImplementer as S, type ServRouteHandler as a, type ServRoutePath as b, type ServRouterBuilder as c, implement as i };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@h-ai/serv",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.0-alpha.
|
|
4
|
+
"version": "0.1.0-alpha.68",
|
|
5
5
|
"description": "Hai Framework service runtime powered by Hono and oRPC.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"exports": {
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@h-ai/ai": "0.1.0-alpha.
|
|
39
|
-
"@h-ai/iam": "0.1.0-alpha.
|
|
40
|
-
"@h-ai/storage": "0.1.0-alpha.
|
|
38
|
+
"@h-ai/ai": "0.1.0-alpha.68",
|
|
39
|
+
"@h-ai/iam": "0.1.0-alpha.68",
|
|
40
|
+
"@h-ai/storage": "0.1.0-alpha.68"
|
|
41
41
|
},
|
|
42
42
|
"peerDependenciesMeta": {
|
|
43
43
|
"@h-ai/ai": {
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
}
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@h-ai/api-contract": "0.1.0-alpha.
|
|
55
|
-
"@h-ai/core": "0.1.0-alpha.
|
|
56
|
-
"@h-ai/crypto": "0.1.0-alpha.
|
|
54
|
+
"@h-ai/api-contract": "0.1.0-alpha.68",
|
|
55
|
+
"@h-ai/core": "0.1.0-alpha.68",
|
|
56
|
+
"@h-ai/crypto": "0.1.0-alpha.68",
|
|
57
57
|
"@hono/node-server": "^2.1.4",
|
|
58
58
|
"@orpc/openapi": "^1.15.5",
|
|
59
59
|
"@orpc/server": "^1.15.5",
|
|
@@ -64,12 +64,16 @@
|
|
|
64
64
|
"zod": "^4.6.5"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
|
-
"@h-ai/ai": "0.1.0-alpha.
|
|
68
|
-
"@h-ai/
|
|
69
|
-
"@h-ai/
|
|
67
|
+
"@h-ai/ai": "0.1.0-alpha.68",
|
|
68
|
+
"@h-ai/api-client": "0.1.0-alpha.68",
|
|
69
|
+
"@h-ai/cache": "0.1.0-alpha.68",
|
|
70
|
+
"@h-ai/iam": "0.1.0-alpha.68",
|
|
71
|
+
"@h-ai/reldb": "0.1.0-alpha.68",
|
|
72
|
+
"@h-ai/storage": "0.1.0-alpha.68",
|
|
70
73
|
"@types/node": "^26.6.5",
|
|
71
74
|
"@types/ws": "^8.18.2",
|
|
72
75
|
"rimraf": "^6.1.3",
|
|
76
|
+
"testcontainers": "^12.2.0",
|
|
73
77
|
"tsup": "^8.5.1",
|
|
74
78
|
"typescript": "^6.0.3",
|
|
75
79
|
"vitest": "^4.1.11"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/features/serv-feature-helpers.ts"],"names":[],"mappings":";;;AAgBO,SAAS,gBAAmB,MAAA,EAAmD;AACpF,EAAA,IAAI,CAAC,MAAA,CAAO,OAAA;AACV,IAAA,OAAO,MAAA;AACT,EAAA,OAAO,EAAA,CAAG,EAAE,KAAA,EAAO,MAAA,CAAO,MAAM,CAAA;AAClC;AAKO,SAAS,YAAA,CAAmB,QAAsB,MAAA,EAAsC;AAC7F,EAAA,IAAI,CAAC,MAAA,CAAO,OAAA;AACV,IAAA,OAAO,MAAA;AACT,EAAA,OAAO,EAAA,CAAG,MAAA,CAAO,MAAA,CAAO,IAAI,CAAC,CAAA;AAC/B","file":"chunk-27PCPE37.js","sourcesContent":["/**\n * @h-ai/serv — features 公共辅助\n *\n * 把多个 feature procedure 中重复出现的 `HaiResult` 包装逻辑收敛到一处。\n * @module features/serv-feature-helpers\n */\n\nimport type { HaiResult } from '@h-ai/core'\nimport { ok } from '@h-ai/core'\n\n/**\n * 把 `HaiResult<T[]>` 转换为 `HaiResult<{ items: T[] }>`。\n *\n * oRPC contract 中 list 类返回常用 `{ items: [...] }` 信封;\n * 业务模块通常直接返回数组,本工具负责套上信封。\n */\nexport function wrapItemsResult<T>(result: HaiResult<T[]>): HaiResult<{ items: T[] }> {\n if (!result.success)\n return result\n return ok({ items: result.data })\n}\n\n/**\n * 把 `HaiResult<T>` 通过 mapper 映射为 `HaiResult<U>`,错误透传。\n */\nexport function mapHaiResult<T, U>(result: HaiResult<T>, mapper: (data: T) => U): HaiResult<U> {\n if (!result.success)\n return result\n return ok(mapper(result.data))\n}\n"]}
|