@h-ai/serv 0.1.0-alpha.41 → 0.1.0-alpha.43
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 +51 -0
- package/dist/index.d.ts +56 -1
- 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
|
: {
|
|
@@ -484,6 +492,17 @@ http:
|
|
|
484
492
|
readyPath: /ready
|
|
485
493
|
rpc: false
|
|
486
494
|
|
|
495
|
+
cors:
|
|
496
|
+
origin: https://app.example.com
|
|
497
|
+
nativeOrigins: capacitor://app.example.local,tauri://app.example.local
|
|
498
|
+
credentials: true
|
|
499
|
+
allowedHeaders:
|
|
500
|
+
- Authorization
|
|
501
|
+
- Content-Type
|
|
502
|
+
- X-Client-Id
|
|
503
|
+
exposedHeaders:
|
|
504
|
+
- X-Encrypted
|
|
505
|
+
|
|
487
506
|
transport:
|
|
488
507
|
keyExchangePath: /_hai/key-exchange
|
|
489
508
|
excludePaths:
|
|
@@ -502,8 +521,40 @@ transport:
|
|
|
502
521
|
- `openapi`:默认 `false`;启用:`{ path: '/openapi.json' }`
|
|
503
522
|
- `docs`:默认 `false`;启用:`{ path: '/docs' }`(依赖 `openapi`);启用后自动挂载 `/_hai/scalar.js`,从 `@scalar/api-reference` 的 browser bundle 提供本地 Scalar UI 脚本,无需外网 CDN
|
|
504
523
|
- `rpc`:默认 `false`;启用:`{ prefix: '/rpc', access: 'loopback' | 'private-network' | 'gateway-only', gatewayHeader?, gatewaySecret? }`
|
|
524
|
+
- `cors`:配置文件中的跨端 origin、credentials、allowedHeaders 与 exposedHeaders;应用通过 `serv.cors(...)` 装配。
|
|
505
525
|
- `transport`:`config/_serv.yml` 顶层配置;启用后可通过 `servConfig.transport` 装配到 `createApp({ transport })`,默认密钥协商子路径 `/_hai/key-exchange`
|
|
506
526
|
|
|
527
|
+
生产环境应先通过 `serv.createRuntimeSecurityPolicy(...)` 收敛 CORS、Cookie 与文档暴露策略:
|
|
528
|
+
|
|
529
|
+
```ts
|
|
530
|
+
const security = serv.createRuntimeSecurityPolicy({
|
|
531
|
+
environment: coreConfig.env,
|
|
532
|
+
corsOrigin: servConfig.cors.origin,
|
|
533
|
+
nativeOrigins: servConfig.cors.nativeOrigins,
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
const app = serv.createApp({
|
|
537
|
+
contract,
|
|
538
|
+
procedures,
|
|
539
|
+
http: {
|
|
540
|
+
...servConfig.http,
|
|
541
|
+
openapi: security.exposeApiDocs ? servConfig.http.openapi : false,
|
|
542
|
+
docs: security.exposeApiDocs ? servConfig.http.docs : false,
|
|
543
|
+
},
|
|
544
|
+
refreshCookie: { secure: security.secureRefreshCookie },
|
|
545
|
+
middlewares: [{
|
|
546
|
+
middleware: serv.cors({
|
|
547
|
+
origin: security.allowOrigin,
|
|
548
|
+
credentials: servConfig.cors.credentials,
|
|
549
|
+
allowedHeaders: [...servConfig.cors.allowedHeaders],
|
|
550
|
+
exposedHeaders: [...servConfig.cors.exposedHeaders],
|
|
551
|
+
}),
|
|
552
|
+
}],
|
|
553
|
+
})
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
生产环境下 `_serv.yml` 的 `cors.origin` 为空或为 `*` 会在启动时失败;Web 与 Capacitor/Tauri Origin 均按规范化后的完整 Origin 精确匹配,避免后缀伪造。生产环境同时强制 Secure refresh cookie 并关闭 OpenAPI/docs,配置文件仍决定非生产环境具体挂载哪些端点。
|
|
557
|
+
|
|
507
558
|
## 错误处理
|
|
508
559
|
|
|
509
560
|
- 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}`[];
|
|
@@ -717,6 +744,33 @@ interface GenerateOpenAPISpecOptions {
|
|
|
717
744
|
*/
|
|
718
745
|
declare function generateSpec(contract: AnyContractRouter, options?: GenerateOpenAPISpecOptions): Promise<OpenAPI.Document>;
|
|
719
746
|
|
|
747
|
+
/**
|
|
748
|
+
* @h-ai/serv — 部署环境 HTTP 安全策略
|
|
749
|
+
*
|
|
750
|
+
* 将环境与 CORS 配置收敛为 fail-closed 的运行时策略,避免每个业务应用重复实现
|
|
751
|
+
* Origin 解析、生产通配符保护、Cookie Secure 与 API 文档暴露规则。
|
|
752
|
+
* @module serv-runtime-security
|
|
753
|
+
*/
|
|
754
|
+
/** 运行时安全策略输入;通常来自 `_core.yml.env` 与 `_serv.yml.cors`。 */
|
|
755
|
+
interface ServRuntimeSecurityPolicyInput {
|
|
756
|
+
readonly environment?: string;
|
|
757
|
+
readonly corsOrigin?: string;
|
|
758
|
+
readonly nativeOrigins?: string;
|
|
759
|
+
}
|
|
760
|
+
/** 可直接装配到 CORS、refresh cookie 与 HTTP 文档配置的安全策略。 */
|
|
761
|
+
interface ServRuntimeSecurityPolicy {
|
|
762
|
+
readonly allowOrigin: (origin: string) => boolean;
|
|
763
|
+
readonly isNativeOrigin: (origin: string) => boolean;
|
|
764
|
+
readonly secureRefreshCookie: boolean;
|
|
765
|
+
readonly exposeApiDocs: boolean;
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* 创建部署环境安全策略。
|
|
769
|
+
*
|
|
770
|
+
* 生产环境必须显式配置 HTTP(S) Origin,禁止缺省或 `*`;开发环境保留通配符便利性。
|
|
771
|
+
*/
|
|
772
|
+
declare function createRuntimeSecurityPolicy(input: ServRuntimeSecurityPolicyInput): ServRuntimeSecurityPolicy;
|
|
773
|
+
|
|
720
774
|
/**
|
|
721
775
|
* @h-ai/serv — 校验工具
|
|
722
776
|
*
|
|
@@ -793,6 +847,7 @@ declare const serv: {
|
|
|
793
847
|
listen: typeof listen;
|
|
794
848
|
toFetch: typeof toFetch;
|
|
795
849
|
cors: typeof cors;
|
|
850
|
+
createRuntimeSecurityPolicy: typeof createRuntimeSecurityPolicy;
|
|
796
851
|
generateSpec: typeof generateSpec;
|
|
797
852
|
/** 基于 contract 创建可注入 context 的 procedure builder(封装 `@orpc/server` 的 `implement`)。 */
|
|
798
853
|
implement: typeof implement;
|
|
@@ -806,4 +861,4 @@ declare const serv: {
|
|
|
806
861
|
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
862
|
};
|
|
808
863
|
|
|
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 };
|
|
864
|
+
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`)。
|