@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 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 层横切逻辑** 时,使用 `middlewares`。常见 CORS 场景可直接复用 `serv.cors(...)`:
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 这类 preflight middleware 可以直接短路;若需要读取解密后的业务 body,请改用 context / procedure 层扩展
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、限流、租户头校验等 HTTP 层横切逻辑。
343
+ * 典型用途:请求日志、trace、指标、CORS、限流、租户头校验,以及不适合放入
344
+ * JSON/oRPC contract 的 WebSocket、文件或其它二进制 HTTP 端点。
317
345
  * 这些 middleware 会在内置安全头之后、传输加密与业务路由之前注册;
318
- * 因此 CORS 这类 preflight middleware 可以直接短路返回,若需要读取解密后的业务 body,
319
- * 应改用 context / procedure 层扩展,而不是 HTTP middleware。
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 { AUDIO_WS_PATH, IAM_AUTH_ROUTES, AudioWsClientMessageSchema } from '@h-ai/api-contract';
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}${IAM_AUTH_ROUTES.refresh}`,
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}${IAM_AUTH_ROUTES.login}`,
796
- `${apiPrefix}${IAM_AUTH_ROUTES.loginWithOtp}`,
797
- `${apiPrefix}${IAM_AUTH_ROUTES.register}`
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}${IAM_AUTH_ROUTES.logout}`, async (c, next) => {
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}${IAM_AUTH_ROUTES.refresh}`, async (c) => {
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`)。