@h-ai/serv 0.1.0-alpha.42 → 0.1.0-alpha.44
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 +63 -2
- package/dist/index.d.ts +60 -4
- package/dist/index.js +97 -8
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -156,6 +156,14 @@ const app = serv.createApp({
|
|
|
156
156
|
contract,
|
|
157
157
|
procedures,
|
|
158
158
|
http: servConfig.http,
|
|
159
|
+
middlewares: [{
|
|
160
|
+
middleware: serv.cors({
|
|
161
|
+
origin: servConfig.cors.origin,
|
|
162
|
+
credentials: servConfig.cors.credentials,
|
|
163
|
+
allowedHeaders: [...servConfig.cors.allowedHeaders],
|
|
164
|
+
exposedHeaders: [...servConfig.cors.exposedHeaders],
|
|
165
|
+
}),
|
|
166
|
+
}],
|
|
159
167
|
transport: servConfig.transport === false
|
|
160
168
|
? undefined
|
|
161
169
|
: {
|
|
@@ -241,7 +249,7 @@ const updateProfile = p.users.update.handler(
|
|
|
241
249
|
|
|
242
250
|
#### 1) 自定义 HTTP middleware
|
|
243
251
|
|
|
244
|
-
当你需要加请求日志、trace、限流、CORS、租户头校验等 **HTTP
|
|
252
|
+
当你需要加请求日志、trace、限流、CORS、租户头校验等 **HTTP 层横切逻辑**,或挂载不适合进入 JSON/oRPC contract 的 WebSocket、文件和二进制端点时,使用 `middlewares`。常见 CORS 场景可直接复用 `serv.cors(...)`:
|
|
245
253
|
|
|
246
254
|
```ts
|
|
247
255
|
import type { ServMiddleware } from '@h-ai/serv'
|
|
@@ -274,6 +282,14 @@ const app = serv.createApp({
|
|
|
274
282
|
await next()
|
|
275
283
|
},
|
|
276
284
|
},
|
|
285
|
+
{
|
|
286
|
+
path: '/assets/*',
|
|
287
|
+
// 路径级 middleware 可以终结请求;响应仍经过内置 securityHeaders。
|
|
288
|
+
middleware: c => c.body(imageBytes, 200, {
|
|
289
|
+
'Content-Type': 'image/png',
|
|
290
|
+
'Cache-Control': 'public, max-age=31536000, immutable',
|
|
291
|
+
}),
|
|
292
|
+
},
|
|
277
293
|
],
|
|
278
294
|
})
|
|
279
295
|
```
|
|
@@ -289,7 +305,9 @@ const app = serv.createApp({
|
|
|
289
305
|
|
|
290
306
|
- `middlewares` 按数组顺序注册
|
|
291
307
|
- `path` 省略时默认 `'*'`
|
|
292
|
-
- 由于 `middlewares` 先于 `transport` 执行,CORS
|
|
308
|
+
- 由于 `middlewares` 先于 `transport` 执行,CORS preflight 或二进制端点可以直接短路;短路响应仍带内置安全头,但不会经过 transport
|
|
309
|
+
- 文件端点的允许方法、路径校验、MIME、缓存和访问授权由应用显式决定;框架不内置头像等业务概念
|
|
310
|
+
- 若需要读取解密后的业务 body,请改用 context / procedure 层扩展
|
|
293
311
|
- 浏览器若需读取自定义响应头(例如 transport 的 `X-Encrypted`),请通过 `serv.cors({ exposedHeaders: [...] })` 显式暴露
|
|
294
312
|
- 这一层直接操作 HTTP middleware context,返回的是 **HTTP Response**,不是 `HaiResult`
|
|
295
313
|
|
|
@@ -484,6 +502,17 @@ http:
|
|
|
484
502
|
readyPath: /ready
|
|
485
503
|
rpc: false
|
|
486
504
|
|
|
505
|
+
cors:
|
|
506
|
+
origin: https://app.example.com
|
|
507
|
+
nativeOrigins: capacitor://app.example.local,tauri://app.example.local
|
|
508
|
+
credentials: true
|
|
509
|
+
allowedHeaders:
|
|
510
|
+
- Authorization
|
|
511
|
+
- Content-Type
|
|
512
|
+
- X-Client-Id
|
|
513
|
+
exposedHeaders:
|
|
514
|
+
- X-Encrypted
|
|
515
|
+
|
|
487
516
|
transport:
|
|
488
517
|
keyExchangePath: /_hai/key-exchange
|
|
489
518
|
excludePaths:
|
|
@@ -502,8 +531,40 @@ transport:
|
|
|
502
531
|
- `openapi`:默认 `false`;启用:`{ path: '/openapi.json' }`
|
|
503
532
|
- `docs`:默认 `false`;启用:`{ path: '/docs' }`(依赖 `openapi`);启用后自动挂载 `/_hai/scalar.js`,从 `@scalar/api-reference` 的 browser bundle 提供本地 Scalar UI 脚本,无需外网 CDN
|
|
504
533
|
- `rpc`:默认 `false`;启用:`{ prefix: '/rpc', access: 'loopback' | 'private-network' | 'gateway-only', gatewayHeader?, gatewaySecret? }`
|
|
534
|
+
- `cors`:配置文件中的跨端 origin、credentials、allowedHeaders 与 exposedHeaders;应用通过 `serv.cors(...)` 装配。
|
|
505
535
|
- `transport`:`config/_serv.yml` 顶层配置;启用后可通过 `servConfig.transport` 装配到 `createApp({ transport })`,默认密钥协商子路径 `/_hai/key-exchange`
|
|
506
536
|
|
|
537
|
+
生产环境应先通过 `serv.createRuntimeSecurityPolicy(...)` 收敛 CORS、Cookie 与文档暴露策略:
|
|
538
|
+
|
|
539
|
+
```ts
|
|
540
|
+
const security = serv.createRuntimeSecurityPolicy({
|
|
541
|
+
environment: coreConfig.env,
|
|
542
|
+
corsOrigin: servConfig.cors.origin,
|
|
543
|
+
nativeOrigins: servConfig.cors.nativeOrigins,
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
const app = serv.createApp({
|
|
547
|
+
contract,
|
|
548
|
+
procedures,
|
|
549
|
+
http: {
|
|
550
|
+
...servConfig.http,
|
|
551
|
+
openapi: security.exposeApiDocs ? servConfig.http.openapi : false,
|
|
552
|
+
docs: security.exposeApiDocs ? servConfig.http.docs : false,
|
|
553
|
+
},
|
|
554
|
+
refreshCookie: { secure: security.secureRefreshCookie },
|
|
555
|
+
middlewares: [{
|
|
556
|
+
middleware: serv.cors({
|
|
557
|
+
origin: security.allowOrigin,
|
|
558
|
+
credentials: servConfig.cors.credentials,
|
|
559
|
+
allowedHeaders: [...servConfig.cors.allowedHeaders],
|
|
560
|
+
exposedHeaders: [...servConfig.cors.exposedHeaders],
|
|
561
|
+
}),
|
|
562
|
+
}],
|
|
563
|
+
})
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
生产环境下 `_serv.yml` 的 `cors.origin` 为空或为 `*` 会在启动时失败;Web 与 Capacitor/Tauri Origin 均按规范化后的完整 Origin 精确匹配,避免后缀伪造。生产环境同时强制 Secure refresh cookie 并关闭 OpenAPI/docs,配置文件仍决定非生产环境具体挂载哪些端点。
|
|
567
|
+
|
|
507
568
|
## 错误处理
|
|
508
569
|
|
|
509
570
|
- Default procedures 全部返回 `HaiResult<T>`,客户端直接判断 `result.success`
|
package/dist/index.d.ts
CHANGED
|
@@ -115,6 +115,14 @@ interface ServTransportRuntimeConfig {
|
|
|
115
115
|
readonly excludePaths: readonly `/${string}`[];
|
|
116
116
|
readonly maxClients: number;
|
|
117
117
|
}
|
|
118
|
+
/** `config/_serv.yml` 中可序列化的跨端 CORS 配置。 */
|
|
119
|
+
interface ServCorsRuntimeConfig {
|
|
120
|
+
readonly origin?: string;
|
|
121
|
+
readonly nativeOrigins?: string;
|
|
122
|
+
readonly allowedHeaders: readonly string[];
|
|
123
|
+
readonly exposedHeaders: readonly string[];
|
|
124
|
+
readonly credentials: boolean;
|
|
125
|
+
}
|
|
118
126
|
/** ServHttpApp 的 HTTP 挂载配置。 */
|
|
119
127
|
interface ServHttpConfig {
|
|
120
128
|
readonly apiPrefix: `/api/${string}`;
|
|
@@ -126,8 +134,11 @@ interface ServHttpConfig {
|
|
|
126
134
|
/** `config/_serv.yml` 的顶层配置结构。 */
|
|
127
135
|
interface ServConfig {
|
|
128
136
|
readonly http: ServHttpConfig;
|
|
137
|
+
readonly cors: ServCorsRuntimeConfig;
|
|
129
138
|
readonly transport: false | ServTransportRuntimeConfig;
|
|
130
139
|
}
|
|
140
|
+
/** `config/_serv.yml` 中的 CORS 输入类型。 */
|
|
141
|
+
type ServCorsRuntimeConfigInput = Partial<ServCorsRuntimeConfig>;
|
|
131
142
|
/** `config/_serv.yml` 中的传输加密输入类型。 */
|
|
132
143
|
type ServTransportRuntimeConfigInput = Partial<{
|
|
133
144
|
readonly keyExchangePath: `/${string}`;
|
|
@@ -145,6 +156,7 @@ type ServHttpConfigInput = Partial<{
|
|
|
145
156
|
/** `config/_serv.yml` 的顶层输入类型。 */
|
|
146
157
|
interface ServConfigInput {
|
|
147
158
|
readonly http?: ServHttpConfigInput;
|
|
159
|
+
readonly cors?: ServCorsRuntimeConfigInput;
|
|
148
160
|
readonly transport?: false | ServTransportRuntimeConfigInput;
|
|
149
161
|
}
|
|
150
162
|
/** `config/_serv.yml` 对应的配置 Schema。 */
|
|
@@ -173,6 +185,13 @@ declare const ServConfigSchema: z.ZodPipe<z.ZodObject<{
|
|
|
173
185
|
gatewaySecret: z.ZodOptional<z.ZodString>;
|
|
174
186
|
}, z.core.$strip>]>>;
|
|
175
187
|
}, z.core.$strip>>;
|
|
188
|
+
cors: z.ZodOptional<z.ZodObject<{
|
|
189
|
+
origin: z.ZodOptional<z.ZodString>;
|
|
190
|
+
nativeOrigins: z.ZodOptional<z.ZodString>;
|
|
191
|
+
allowedHeaders: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
192
|
+
exposedHeaders: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
193
|
+
credentials: z.ZodDefault<z.ZodBoolean>;
|
|
194
|
+
}, z.core.$strip>>;
|
|
176
195
|
transport: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<false>, z.ZodObject<{
|
|
177
196
|
keyExchangePath: z.ZodDefault<z.ZodCustom<`/${string}`, `/${string}`>>;
|
|
178
197
|
excludePaths: z.ZodDefault<z.ZodArray<z.ZodCustom<`/${string}`, `/${string}`>>>;
|
|
@@ -180,6 +199,7 @@ declare const ServConfigSchema: z.ZodPipe<z.ZodObject<{
|
|
|
180
199
|
}, z.core.$strip>]>>;
|
|
181
200
|
}, z.core.$strip>, z.ZodTransform<{
|
|
182
201
|
http: ServHttpConfig;
|
|
202
|
+
cors: ServCorsRuntimeConfig;
|
|
183
203
|
transport: false | ServTransportRuntimeConfig;
|
|
184
204
|
}, {
|
|
185
205
|
http?: {
|
|
@@ -202,6 +222,13 @@ declare const ServConfigSchema: z.ZodPipe<z.ZodObject<{
|
|
|
202
222
|
gatewaySecret?: string | undefined;
|
|
203
223
|
};
|
|
204
224
|
} | undefined;
|
|
225
|
+
cors?: {
|
|
226
|
+
allowedHeaders: string[];
|
|
227
|
+
exposedHeaders: string[];
|
|
228
|
+
credentials: boolean;
|
|
229
|
+
origin?: string | undefined;
|
|
230
|
+
nativeOrigins?: string | undefined;
|
|
231
|
+
} | undefined;
|
|
205
232
|
transport?: false | {
|
|
206
233
|
keyExchangePath: `/${string}`;
|
|
207
234
|
excludePaths: `/${string}`[];
|
|
@@ -313,10 +340,11 @@ interface CreateServAppOptions<TContract extends AnyContractRouter, TProcedures
|
|
|
313
340
|
/**
|
|
314
341
|
* 自定义 HTTP middleware(当前内部由 Hono 承载,但应用 API 不暴露 Hono app)。
|
|
315
342
|
*
|
|
316
|
-
* 典型用途:请求日志、trace、指标、CORS
|
|
343
|
+
* 典型用途:请求日志、trace、指标、CORS、限流、租户头校验,以及不适合放入
|
|
344
|
+
* JSON/oRPC contract 的 WebSocket、文件或其它二进制 HTTP 端点。
|
|
317
345
|
* 这些 middleware 会在内置安全头之后、传输加密与业务路由之前注册;
|
|
318
|
-
* 因此 CORS
|
|
319
|
-
*
|
|
346
|
+
* 因此 CORS preflight 或二进制端点可以直接短路返回。短路响应仍包含内置安全头,
|
|
347
|
+
* 但不会经过 transport;若需要读取解密后的业务 body,应改用 context / procedure 层扩展。
|
|
320
348
|
*/
|
|
321
349
|
readonly middlewares?: readonly ServMiddlewareMount[];
|
|
322
350
|
/**
|
|
@@ -717,6 +745,33 @@ interface GenerateOpenAPISpecOptions {
|
|
|
717
745
|
*/
|
|
718
746
|
declare function generateSpec(contract: AnyContractRouter, options?: GenerateOpenAPISpecOptions): Promise<OpenAPI.Document>;
|
|
719
747
|
|
|
748
|
+
/**
|
|
749
|
+
* @h-ai/serv — 部署环境 HTTP 安全策略
|
|
750
|
+
*
|
|
751
|
+
* 将环境与 CORS 配置收敛为 fail-closed 的运行时策略,避免每个业务应用重复实现
|
|
752
|
+
* Origin 解析、生产通配符保护、Cookie Secure 与 API 文档暴露规则。
|
|
753
|
+
* @module serv-runtime-security
|
|
754
|
+
*/
|
|
755
|
+
/** 运行时安全策略输入;通常来自 `_core.yml.env` 与 `_serv.yml.cors`。 */
|
|
756
|
+
interface ServRuntimeSecurityPolicyInput {
|
|
757
|
+
readonly environment?: string;
|
|
758
|
+
readonly corsOrigin?: string;
|
|
759
|
+
readonly nativeOrigins?: string;
|
|
760
|
+
}
|
|
761
|
+
/** 可直接装配到 CORS、refresh cookie 与 HTTP 文档配置的安全策略。 */
|
|
762
|
+
interface ServRuntimeSecurityPolicy {
|
|
763
|
+
readonly allowOrigin: (origin: string) => boolean;
|
|
764
|
+
readonly isNativeOrigin: (origin: string) => boolean;
|
|
765
|
+
readonly secureRefreshCookie: boolean;
|
|
766
|
+
readonly exposeApiDocs: boolean;
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* 创建部署环境安全策略。
|
|
770
|
+
*
|
|
771
|
+
* 生产环境必须显式配置 HTTP(S) Origin,禁止缺省或 `*`;开发环境保留通配符便利性。
|
|
772
|
+
*/
|
|
773
|
+
declare function createRuntimeSecurityPolicy(input: ServRuntimeSecurityPolicyInput): ServRuntimeSecurityPolicy;
|
|
774
|
+
|
|
720
775
|
/**
|
|
721
776
|
* @h-ai/serv — 校验工具
|
|
722
777
|
*
|
|
@@ -793,6 +848,7 @@ declare const serv: {
|
|
|
793
848
|
listen: typeof listen;
|
|
794
849
|
toFetch: typeof toFetch;
|
|
795
850
|
cors: typeof cors;
|
|
851
|
+
createRuntimeSecurityPolicy: typeof createRuntimeSecurityPolicy;
|
|
796
852
|
generateSpec: typeof generateSpec;
|
|
797
853
|
/** 基于 contract 创建可注入 context 的 procedure builder(封装 `@orpc/server` 的 `implement`)。 */
|
|
798
854
|
implement: typeof implement;
|
|
@@ -806,4 +862,4 @@ declare const serv: {
|
|
|
806
862
|
m: (key: "$schema" | "serv_validationFailed" | "serv_validationRequired" | "serv_validationInvalid" | "serv_validationInvalidType" | "serv_validationStringMin" | "serv_validationStringMax" | "serv_validationNumberMin" | "serv_validationNumberMax" | "serv_validationArrayMin" | "serv_validationArrayMax" | "serv_validationTooSmall" | "serv_validationTooBig" | "serv_validationEmail" | "serv_validationUrl" | "serv_validationUuid" | "serv_validationEnum" | "serv_errorUnauthorized" | "serv_errorForbidden" | "serv_errorInternal" | "serv_storageKeyRequired" | "serv_storageKeyTooLong" | "serv_storageKeyIllegalChars" | "serv_storageKeyDotSegments" | "serv_iamCannotDeleteCurrentUser" | "serv_transportInvalidPayload" | "serv_transportUnknownClient" | "serv_transportClientIdRequired" | "serv_transportClientKeyNotFound" | "serv_transportDecryptFailed" | "serv_transportEncryptFailed" | "serv_transportKeyExchangeFailed", options?: _h_ai_core.MessageOptions) => string;
|
|
807
863
|
};
|
|
808
864
|
|
|
809
|
-
export { type AudioTicketGrant, type AudioTicketVerification, type AuthorizedAudioRequest, type CreateServAppOptions, CreateServContext, type GenerateOpenAPISpecOptions, RefreshCookieConfig, type ServAudioConfig, type ServConfig, type ServConfigInput, ServConfigSchema, ServContext, type ServCorsConfig, type ServDocsHttpConfig, type ServFetchHandler, type ServGuardedProcedureWrapper, type ServHealthHttpConfig, type ServHttpApp, type ServHttpConfig, type ServHttpConfigInput, ServIam, type ServListenOptions, type ServMiddleware, type ServMiddlewareFactory, type ServMiddlewareMount, type ServNodeServer, type ServOpenAPIHttpConfig, type ServProcedureHandler, type ServProcedureOptions, type ServProcedureWrapper, type ServRpcHttpConfig, ServSession, type ServTransportConfig, type ServTransportRuntimeConfig, type ServTransportRuntimeConfigInput, type ServValidationFailureBody, serv };
|
|
865
|
+
export { type AudioTicketGrant, type AudioTicketVerification, type AuthorizedAudioRequest, type CreateServAppOptions, CreateServContext, type GenerateOpenAPISpecOptions, RefreshCookieConfig, type ServAudioConfig, type ServConfig, type ServConfigInput, ServConfigSchema, ServContext, type ServCorsConfig, type ServCorsRuntimeConfig, type ServCorsRuntimeConfigInput, type ServDocsHttpConfig, type ServFetchHandler, type ServGuardedProcedureWrapper, type ServHealthHttpConfig, type ServHttpApp, type ServHttpConfig, type ServHttpConfigInput, ServIam, type ServListenOptions, type ServMiddleware, type ServMiddlewareFactory, type ServMiddlewareMount, type ServNodeServer, type ServOpenAPIHttpConfig, type ServProcedureHandler, type ServProcedureOptions, type ServProcedureWrapper, type ServRpcHttpConfig, type ServRuntimeSecurityPolicy, type ServRuntimeSecurityPolicyInput, ServSession, type ServTransportConfig, type ServTransportRuntimeConfig, type ServTransportRuntimeConfigInput, type ServValidationFailureBody, serv };
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { servM, mapHaiError, requireAuth, normalizeServLocale, buildHaiErrorBody
|
|
|
3
3
|
import { implement } from '@orpc/server';
|
|
4
4
|
import process2 from 'process';
|
|
5
5
|
import { serve } from '@hono/node-server';
|
|
6
|
-
import {
|
|
6
|
+
import { apiContract, AUDIO_WS_PATH, AudioWsClientMessageSchema } from '@h-ai/api-contract';
|
|
7
7
|
import { core, ok, err, HaiCommonError } from '@h-ai/core';
|
|
8
8
|
import { createNodeWebSocket } from '@hono/node-ws';
|
|
9
9
|
import { OpenAPIHandler } from '@orpc/openapi/fetch';
|
|
@@ -662,6 +662,13 @@ var TransportRuntimeConfigInputSchema = z.union([
|
|
|
662
662
|
maxClients: z.number().int().positive().default(1e4)
|
|
663
663
|
})
|
|
664
664
|
]);
|
|
665
|
+
var CorsRuntimeConfigInputSchema = z.object({
|
|
666
|
+
origin: z.string().optional(),
|
|
667
|
+
nativeOrigins: z.string().optional(),
|
|
668
|
+
allowedHeaders: z.array(z.string()).default(["Authorization", "Content-Type", "X-Requested-With"]),
|
|
669
|
+
exposedHeaders: z.array(z.string()).default([]),
|
|
670
|
+
credentials: z.boolean().default(false)
|
|
671
|
+
});
|
|
665
672
|
var ServHttpConfigInputSchema = z.object({
|
|
666
673
|
apiPrefix: ApiPrefixSchema.default("/api/v1"),
|
|
667
674
|
openapi: OpenAPIHttpConfigInputSchema.default(false),
|
|
@@ -671,14 +678,19 @@ var ServHttpConfigInputSchema = z.object({
|
|
|
671
678
|
});
|
|
672
679
|
var ServConfigSchema = z.object({
|
|
673
680
|
http: ServHttpConfigInputSchema.optional(),
|
|
681
|
+
cors: CorsRuntimeConfigInputSchema.optional(),
|
|
674
682
|
transport: TransportRuntimeConfigInputSchema.optional()
|
|
675
|
-
}).transform(({ http, transport }) => ({
|
|
683
|
+
}).transform(({ http, cors: cors2, transport }) => ({
|
|
676
684
|
http: resolveServHttpConfig(http ?? {}),
|
|
685
|
+
cors: resolveServCorsRuntimeConfig(cors2 ?? {}),
|
|
677
686
|
transport: resolveServTransportRuntimeConfig(transport ?? false)
|
|
678
687
|
}));
|
|
679
688
|
function resolveServHttpConfig(input = {}) {
|
|
680
689
|
return ServHttpConfigInputSchema.parse(input);
|
|
681
690
|
}
|
|
691
|
+
function resolveServCorsRuntimeConfig(input = {}) {
|
|
692
|
+
return CorsRuntimeConfigInputSchema.parse(input);
|
|
693
|
+
}
|
|
682
694
|
function resolveServTransportRuntimeConfig(input = false) {
|
|
683
695
|
return TransportRuntimeConfigInputSchema.parse(input);
|
|
684
696
|
}
|
|
@@ -727,6 +739,13 @@ function buildAuthContextFactory(verifyToken) {
|
|
|
727
739
|
}
|
|
728
740
|
};
|
|
729
741
|
}
|
|
742
|
+
var IAM_AUTH_PATHS = {
|
|
743
|
+
login: apiContract.pathOf(apiContract.iam.auth.login),
|
|
744
|
+
loginWithOtp: apiContract.pathOf(apiContract.iam.auth.loginWithOtp),
|
|
745
|
+
register: apiContract.pathOf(apiContract.iam.auth.register),
|
|
746
|
+
logout: apiContract.pathOf(apiContract.iam.auth.logout),
|
|
747
|
+
refresh: apiContract.pathOf(apiContract.iam.auth.refresh)
|
|
748
|
+
};
|
|
730
749
|
function resolveRefreshCookieConfig(apiPrefix, config, iam) {
|
|
731
750
|
const onRefresh = config.onRefresh ?? (iam ? (refreshToken) => iam.session.refresh(refreshToken) : void 0);
|
|
732
751
|
if (!onRefresh) {
|
|
@@ -739,7 +758,7 @@ function resolveRefreshCookieConfig(apiPrefix, config, iam) {
|
|
|
739
758
|
cookieName: config.cookieName ?? "hai_refresh_token",
|
|
740
759
|
maxAge: config.maxAge ?? 30 * 24 * 3600,
|
|
741
760
|
isSecure: config.secure ?? process2.env.NODE_ENV === "production",
|
|
742
|
-
refreshCookiePath: `${apiPrefix}${
|
|
761
|
+
refreshCookiePath: `${apiPrefix}${IAM_AUTH_PATHS.refresh}`,
|
|
743
762
|
onRefresh
|
|
744
763
|
};
|
|
745
764
|
}
|
|
@@ -792,9 +811,9 @@ function stripRefreshTokenFromPair(tokens) {
|
|
|
792
811
|
function mountRefreshCookieRoutes(app, apiPrefix, config, iam) {
|
|
793
812
|
const { cookieName, maxAge, isSecure, refreshCookiePath, onRefresh } = resolveRefreshCookieConfig(apiPrefix, config, iam);
|
|
794
813
|
const loginPaths = [
|
|
795
|
-
`${apiPrefix}${
|
|
796
|
-
`${apiPrefix}${
|
|
797
|
-
`${apiPrefix}${
|
|
814
|
+
`${apiPrefix}${IAM_AUTH_PATHS.login}`,
|
|
815
|
+
`${apiPrefix}${IAM_AUTH_PATHS.loginWithOtp}`,
|
|
816
|
+
`${apiPrefix}${IAM_AUTH_PATHS.register}`
|
|
798
817
|
];
|
|
799
818
|
for (const path of loginPaths) {
|
|
800
819
|
app.post(path, async (c, next) => {
|
|
@@ -819,7 +838,7 @@ function mountRefreshCookieRoutes(app, apiPrefix, config, iam) {
|
|
|
819
838
|
c.res = new Response(responseBody, { status: c.res.status, statusText: c.res.statusText, headers });
|
|
820
839
|
});
|
|
821
840
|
}
|
|
822
|
-
app.post(`${apiPrefix}${
|
|
841
|
+
app.post(`${apiPrefix}${IAM_AUTH_PATHS.logout}`, async (c, next) => {
|
|
823
842
|
await next();
|
|
824
843
|
if (c.res.status !== 200)
|
|
825
844
|
return;
|
|
@@ -833,7 +852,7 @@ function mountRefreshCookieRoutes(app, apiPrefix, config, iam) {
|
|
|
833
852
|
}
|
|
834
853
|
c.res = new Response(bodyText, { status: c.res.status, statusText: c.res.statusText, headers });
|
|
835
854
|
});
|
|
836
|
-
app.post(`${apiPrefix}${
|
|
855
|
+
app.post(`${apiPrefix}${IAM_AUTH_PATHS.refresh}`, async (c) => {
|
|
837
856
|
const refreshToken = getCookie(c, cookieName);
|
|
838
857
|
if (!refreshToken) {
|
|
839
858
|
const locale = resolveRequestLocale(c.req.raw.headers);
|
|
@@ -1382,6 +1401,75 @@ function buildCorsHeaders(options) {
|
|
|
1382
1401
|
return headers;
|
|
1383
1402
|
}
|
|
1384
1403
|
|
|
1404
|
+
// src/serv-runtime-security.ts
|
|
1405
|
+
function createRuntimeSecurityPolicy(input) {
|
|
1406
|
+
const production = input.environment?.trim().toLowerCase() === "production";
|
|
1407
|
+
const configuredOrigins = parseOrigins(input.corsOrigin);
|
|
1408
|
+
const configuredNativeOrigins = parseNativeOrigins(input.nativeOrigins);
|
|
1409
|
+
const allowsEveryOrigin = configuredOrigins === "*";
|
|
1410
|
+
if (production && (allowsEveryOrigin || configuredOrigins.length === 0))
|
|
1411
|
+
throw new Error("serv.cors.origin must contain explicit HTTP(S) origins in production");
|
|
1412
|
+
const allowedOrigins = allowsEveryOrigin ? void 0 : new Set(configuredOrigins);
|
|
1413
|
+
const allowedNativeOrigins = new Set(configuredNativeOrigins);
|
|
1414
|
+
return {
|
|
1415
|
+
allowOrigin: (origin) => {
|
|
1416
|
+
const normalizedNativeOrigin = tryNormalizeNativeOrigin(origin);
|
|
1417
|
+
if (normalizedNativeOrigin !== void 0 && allowedNativeOrigins.has(normalizedNativeOrigin))
|
|
1418
|
+
return true;
|
|
1419
|
+
if (allowedOrigins === void 0)
|
|
1420
|
+
return true;
|
|
1421
|
+
const normalizedWebOrigin = tryNormalizeWebOrigin(origin);
|
|
1422
|
+
return normalizedWebOrigin !== void 0 && allowedOrigins.has(normalizedWebOrigin);
|
|
1423
|
+
},
|
|
1424
|
+
isNativeOrigin: (origin) => allowedNativeOrigins.has(tryNormalizeNativeOrigin(origin) ?? ""),
|
|
1425
|
+
secureRefreshCookie: production,
|
|
1426
|
+
exposeApiDocs: !production
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
function parseOrigins(value) {
|
|
1430
|
+
const entries = (value ?? "*").split(",").map((origin) => origin.trim()).filter(Boolean);
|
|
1431
|
+
if (entries.includes("*"))
|
|
1432
|
+
return "*";
|
|
1433
|
+
return entries.map(normalizeWebOrigin);
|
|
1434
|
+
}
|
|
1435
|
+
function parseNativeOrigins(value) {
|
|
1436
|
+
return (value ?? "").split(",").map((origin) => origin.trim()).filter(Boolean).map(normalizeNativeOrigin);
|
|
1437
|
+
}
|
|
1438
|
+
function normalizeWebOrigin(value) {
|
|
1439
|
+
const normalized = tryNormalizeWebOrigin(value);
|
|
1440
|
+
if (normalized === void 0)
|
|
1441
|
+
throw new Error(`serv.cors.origin contains an invalid origin: ${value}`);
|
|
1442
|
+
return normalized;
|
|
1443
|
+
}
|
|
1444
|
+
function normalizeNativeOrigin(value) {
|
|
1445
|
+
const normalized = tryNormalizeNativeOrigin(value);
|
|
1446
|
+
if (normalized === void 0)
|
|
1447
|
+
throw new Error(`serv.cors.nativeOrigins contains an invalid origin: ${value}`);
|
|
1448
|
+
return normalized;
|
|
1449
|
+
}
|
|
1450
|
+
function tryNormalizeWebOrigin(value) {
|
|
1451
|
+
let url;
|
|
1452
|
+
try {
|
|
1453
|
+
url = new URL(value);
|
|
1454
|
+
} catch {
|
|
1455
|
+
return void 0;
|
|
1456
|
+
}
|
|
1457
|
+
if (!["http:", "https:"].includes(url.protocol) || url.origin === "null" || url.pathname !== "/" || url.search || url.hash)
|
|
1458
|
+
return void 0;
|
|
1459
|
+
return url.origin;
|
|
1460
|
+
}
|
|
1461
|
+
function tryNormalizeNativeOrigin(value) {
|
|
1462
|
+
let url;
|
|
1463
|
+
try {
|
|
1464
|
+
url = new URL(value);
|
|
1465
|
+
} catch {
|
|
1466
|
+
return void 0;
|
|
1467
|
+
}
|
|
1468
|
+
if (!["http:", "https:", "capacitor:", "tauri:"].includes(url.protocol) || !url.hostname || url.username || url.password || !["", "/"].includes(url.pathname) || url.search || url.hash)
|
|
1469
|
+
return void 0;
|
|
1470
|
+
return ["capacitor:", "tauri:"].includes(url.protocol) ? `${url.protocol}//${url.host}` : url.origin;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1385
1473
|
// src/serv-main.ts
|
|
1386
1474
|
var serv = {
|
|
1387
1475
|
// 应用与上下文
|
|
@@ -1393,6 +1481,7 @@ var serv = {
|
|
|
1393
1481
|
listen,
|
|
1394
1482
|
toFetch,
|
|
1395
1483
|
cors,
|
|
1484
|
+
createRuntimeSecurityPolicy,
|
|
1396
1485
|
// OpenAPI / 导出
|
|
1397
1486
|
generateSpec,
|
|
1398
1487
|
// Procedure 装配(contract 路由定义统一使用 `apiContract.route`)。
|