@h-ai/serv 1.0.0-beta.2 → 1.0.0-beta.21
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 +172 -124
- package/README.ja.md +172 -124
- package/README.md +172 -124
- package/dist/{chunk-TWMOZPGU.js → chunk-CZ6VA2EN.js} +3 -3
- package/dist/{chunk-TWMOZPGU.js.map → chunk-CZ6VA2EN.js.map} +1 -1
- package/dist/{chunk-I3F7RN4I.js → chunk-PLGCHDZM.js} +30 -13
- package/dist/chunk-PLGCHDZM.js.map +1 -0
- package/dist/features/ai.d.ts +9 -8
- package/dist/features/ai.js +34 -13
- package/dist/features/ai.js.map +1 -1
- package/dist/features/feedback.d.ts +20 -19
- package/dist/features/feedback.js +1 -1
- package/dist/features/feedback.js.map +1 -1
- package/dist/features/iam.d.ts +4 -3
- package/dist/features/iam.js +4 -2
- package/dist/features/iam.js.map +1 -1
- package/dist/features/storage.d.ts +4 -3
- package/dist/features/storage.js +34 -9
- package/dist/features/storage.js.map +1 -1
- package/dist/index.d.ts +97 -68
- package/dist/index.js +338 -222
- package/dist/index.js.map +1 -1
- package/dist/{serv-context-DE1JH5bd.d.ts → serv-context-DSsS_ksL.d.ts} +4 -60
- package/dist/{serv-router-BJk04-oS.d.ts → serv-router-DWjYC0zD.d.ts} +2 -18
- package/package.json +29 -29
- package/dist/chunk-I3F7RN4I.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { validateInputOrFail, resolveRequestLocale, localizeZodError } from './chunk-
|
|
2
|
-
import { servM, WILDCARD_ROLE, WILDCARD_PERMISSION, implement, buildHaiErrorBody } from './chunk-
|
|
3
|
-
import
|
|
1
|
+
import { validateInputOrFail, resolveRequestLocale, localizeZodError } from './chunk-CZ6VA2EN.js';
|
|
2
|
+
import { servM, WILDCARD_ROLE, WILDCARD_PERMISSION, implement, buildHaiErrorBody } from './chunk-PLGCHDZM.js';
|
|
3
|
+
import { errorMonitor } from 'events';
|
|
4
|
+
import process4 from 'process';
|
|
5
|
+
import { core, RequestAccessConfigSchema, TrustedProxiesSchema, HaiCommonError, err, ok } from '@h-ai/core';
|
|
4
6
|
import { upgradeWebSocket, serve } from '@hono/node-server';
|
|
7
|
+
import { posix } from 'path';
|
|
5
8
|
import { apiContract, AUDIO_WS_PATH, AudioWsClientMessageSchema } from '@h-ai/api-contract';
|
|
6
9
|
import { audit } from '@h-ai/audit';
|
|
7
|
-
import {
|
|
8
|
-
import { TRANSPORT_PLAINTEXT_RANGE, createTransportBodyPolicy, TRANSPORT_PROTOCOL, authenticateTransportRequest, TransportTimeError, createTransportErrorResponse, TransportBodyTooLargeError, readTransportBody, TRANSPORT_BODY_LIMITS, isValidSM2PublicKey } from '@h-ai/crypto';
|
|
10
|
+
import { TRANSPORT_PLAINTEXT_RANGE, TRANSPORT_TIME_LIMITS, TRANSPORT_PROTOCOL, createTransportBodyPolicy, authenticateTransportRequest, TransportTimeError, createTransportErrorResponse, TransportBodyTooLargeError, readTransportBody, TRANSPORT_BODY_LIMITS, isValidSM2PublicKey } from '@h-ai/crypto';
|
|
9
11
|
import { OpenAPIHandler } from '@orpc/openapi/fetch';
|
|
10
|
-
import { RPCHandler } from '@orpc/server/fetch';
|
|
11
12
|
import { Hono } from 'hono';
|
|
12
13
|
import { WebSocketServer } from 'ws';
|
|
13
14
|
import { Buffer } from 'buffer';
|
|
@@ -201,7 +202,9 @@ var AudioConnection = class {
|
|
|
201
202
|
return;
|
|
202
203
|
}
|
|
203
204
|
this.queue = this.queue.then(() => task()).catch((error) => {
|
|
204
|
-
logger.debug("audio ws task failed", {
|
|
205
|
+
logger.debug("audio ws task failed", {
|
|
206
|
+
error: error instanceof Error ? error.message : String(error)
|
|
207
|
+
});
|
|
205
208
|
if (!this.closed)
|
|
206
209
|
this.failFromError(ws, error);
|
|
207
210
|
}).finally(() => {
|
|
@@ -482,7 +485,11 @@ var AudioConnection = class {
|
|
|
482
485
|
if (this.closed)
|
|
483
486
|
return;
|
|
484
487
|
logger.warn("Audio WebSocket operation failed", { error });
|
|
485
|
-
const hai = error && typeof error === "object" && "code" in error && typeof error.code === "string" && "message" in error && typeof error.message === "string" ? {
|
|
488
|
+
const hai = error && typeof error === "object" && "code" in error && typeof error.code === "string" && "message" in error && typeof error.message === "string" ? {
|
|
489
|
+
code: error.code,
|
|
490
|
+
message: error.message,
|
|
491
|
+
httpStatus: "httpStatus" in error && typeof error.httpStatus === "number" ? error.httpStatus : 500
|
|
492
|
+
} : { code: HaiCommonError.INTERNAL_ERROR.code, message: "", httpStatus: 500 };
|
|
486
493
|
const safe = core.error.toPublic(hai);
|
|
487
494
|
this.fail(ws, String(safe.code), safe.message);
|
|
488
495
|
}
|
|
@@ -513,7 +520,9 @@ var AudioConnection = class {
|
|
|
513
520
|
this.controller.abort();
|
|
514
521
|
if (this.session && this.authorizedRequest && this.deps.onSessionEnd) {
|
|
515
522
|
void Promise.resolve(this.deps.onSessionEnd(this.session, this.authorizedRequest)).catch((error) => {
|
|
516
|
-
logger.warn("audio ws session end hook failed", {
|
|
523
|
+
logger.warn("audio ws session end hook failed", {
|
|
524
|
+
error: error instanceof Error ? error.message : String(error)
|
|
525
|
+
});
|
|
517
526
|
});
|
|
518
527
|
}
|
|
519
528
|
}
|
|
@@ -548,75 +557,6 @@ function toArrayBuffer(data) {
|
|
|
548
557
|
new Uint8Array(buffer).set(data);
|
|
549
558
|
return buffer;
|
|
550
559
|
}
|
|
551
|
-
function readRemoteAddress(metadata) {
|
|
552
|
-
if (!metadata || typeof metadata !== "object" || !("remoteAddress" in metadata) || typeof metadata.remoteAddress !== "string")
|
|
553
|
-
return void 0;
|
|
554
|
-
return core.access.normalizeClientIp(metadata.remoteAddress);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
// src/pipelines/serv-pipeline-require-internal-rpc.ts
|
|
558
|
-
var pipelineLogger = core.logger.child({ module: "serv", scope: "pipeline" });
|
|
559
|
-
var cachedGetConnInfo;
|
|
560
|
-
async function resolveGetConnInfo() {
|
|
561
|
-
if (cachedGetConnInfo !== void 0)
|
|
562
|
-
return cachedGetConnInfo;
|
|
563
|
-
try {
|
|
564
|
-
const mod = await import('@hono/node-server/conninfo');
|
|
565
|
-
cachedGetConnInfo = mod.getConnInfo;
|
|
566
|
-
} catch (cause) {
|
|
567
|
-
pipelineLogger.warn("getConnInfo unavailable in this runtime; loopback/private-network access modes will fail closed", { error: cause });
|
|
568
|
-
cachedGetConnInfo = null;
|
|
569
|
-
}
|
|
570
|
-
return cachedGetConnInfo;
|
|
571
|
-
}
|
|
572
|
-
var LOOPBACK_IPS = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1", "localhost"]);
|
|
573
|
-
function requireInternalRPC(config) {
|
|
574
|
-
return async (c, next) => {
|
|
575
|
-
const locale = resolveRequestLocale(c.req.raw.headers);
|
|
576
|
-
if (config.access === "gateway-only") {
|
|
577
|
-
const headerName = config.gatewayHeader ?? "x-hai-internal-rpc";
|
|
578
|
-
const received = c.req.header(headerName);
|
|
579
|
-
if (!config.gatewaySecret || !received || !core.string.constantTimeEqual(received, config.gatewaySecret))
|
|
580
|
-
return c.json(buildHaiErrorBody(HaiCommonError.FORBIDDEN, servM("serv_errorForbidden", { locale })), 403);
|
|
581
|
-
return next();
|
|
582
|
-
}
|
|
583
|
-
const ip = await getRequestRemoteAddress(c) ?? "";
|
|
584
|
-
if (config.access === "loopback" && !isLoopbackIP(ip))
|
|
585
|
-
return c.json(buildHaiErrorBody(HaiCommonError.FORBIDDEN, servM("serv_errorForbidden", { locale })), 403);
|
|
586
|
-
if (config.access === "private-network" && !isPrivateNetworkIP(ip))
|
|
587
|
-
return c.json(buildHaiErrorBody(HaiCommonError.FORBIDDEN, servM("serv_errorForbidden", { locale })), 403);
|
|
588
|
-
return next();
|
|
589
|
-
};
|
|
590
|
-
}
|
|
591
|
-
async function getRequestRemoteAddress(c) {
|
|
592
|
-
const remoteAddress = readRemoteAddress(c.env);
|
|
593
|
-
if (remoteAddress)
|
|
594
|
-
return remoteAddress;
|
|
595
|
-
const getConnInfo = await resolveGetConnInfo();
|
|
596
|
-
if (!getConnInfo)
|
|
597
|
-
return void 0;
|
|
598
|
-
try {
|
|
599
|
-
return getConnInfo(c).remote.address;
|
|
600
|
-
} catch {
|
|
601
|
-
return void 0;
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
function normalizeIpAddress(ip) {
|
|
605
|
-
return ip.startsWith("::ffff:") ? ip.slice("::ffff:".length) : ip;
|
|
606
|
-
}
|
|
607
|
-
function isLoopbackIP(ip) {
|
|
608
|
-
const normalized = normalizeIpAddress(ip);
|
|
609
|
-
return LOOPBACK_IPS.has(ip) || LOOPBACK_IPS.has(normalized);
|
|
610
|
-
}
|
|
611
|
-
function isPrivateNetworkIP(ip) {
|
|
612
|
-
const normalized = normalizeIpAddress(ip);
|
|
613
|
-
if (isLoopbackIP(ip))
|
|
614
|
-
return true;
|
|
615
|
-
if (normalized.startsWith("10.") || normalized.startsWith("192.168."))
|
|
616
|
-
return true;
|
|
617
|
-
const second = Number(normalized.split(".")[1]);
|
|
618
|
-
return normalized.startsWith("172.") && Number.isInteger(second) && second >= 16 && second <= 31;
|
|
619
|
-
}
|
|
620
560
|
|
|
621
561
|
// src/pipelines/serv-pipeline-security-headers.ts
|
|
622
562
|
function securityHeaders() {
|
|
@@ -627,9 +567,16 @@ function securityHeaders() {
|
|
|
627
567
|
await next();
|
|
628
568
|
};
|
|
629
569
|
}
|
|
570
|
+
function readRemoteAddress(metadata) {
|
|
571
|
+
if (!metadata || typeof metadata !== "object" || !("remoteAddress" in metadata) || typeof metadata.remoteAddress !== "string")
|
|
572
|
+
return void 0;
|
|
573
|
+
return core.access.normalizeClientIp(metadata.remoteAddress);
|
|
574
|
+
}
|
|
630
575
|
var ApiPrefixSchema = z.custom(
|
|
631
|
-
(value) => typeof value === "string" && value.
|
|
632
|
-
|
|
576
|
+
(value) => typeof value === "string" && /^\/[\w.-]+(?:\/[\w.-]+)*$/.test(value) && !value.split("/").some((segment) => segment === "." || segment === ".."),
|
|
577
|
+
{
|
|
578
|
+
error: () => servM("serv_invalidApiPrefix")
|
|
579
|
+
}
|
|
633
580
|
);
|
|
634
581
|
var AbsolutePathSchema = z.custom(
|
|
635
582
|
(value) => typeof value === "string" && value.startsWith("/"),
|
|
@@ -637,7 +584,9 @@ var AbsolutePathSchema = z.custom(
|
|
|
637
584
|
);
|
|
638
585
|
var OpenAPIHttpConfigInputSchema = z.union([
|
|
639
586
|
z.literal(false),
|
|
640
|
-
z.object({
|
|
587
|
+
z.object({
|
|
588
|
+
path: AbsolutePathSchema.default("/openapi.json")
|
|
589
|
+
})
|
|
641
590
|
]);
|
|
642
591
|
var DocsHttpConfigInputSchema = z.union([
|
|
643
592
|
z.literal(false),
|
|
@@ -653,19 +602,15 @@ var HealthHttpConfigInputSchema = z.union([
|
|
|
653
602
|
readyPath: AbsolutePathSchema.optional().default("/ready")
|
|
654
603
|
})
|
|
655
604
|
]);
|
|
656
|
-
var RpcHttpConfigInputSchema = z.union([
|
|
657
|
-
z.literal(false),
|
|
658
|
-
z.object({
|
|
659
|
-
prefix: AbsolutePathSchema.default("/rpc"),
|
|
660
|
-
access: z.enum(["loopback", "private-network", "gateway-only"]).default("loopback"),
|
|
661
|
-
gatewayHeader: z.string().optional(),
|
|
662
|
-
gatewaySecret: z.string().optional()
|
|
663
|
-
})
|
|
664
|
-
]);
|
|
665
605
|
var TransportRuntimeConfigInputSchema = z.union([
|
|
666
606
|
z.literal(false),
|
|
667
607
|
z.object({
|
|
668
|
-
maxPlaintextBytes: z.union([
|
|
608
|
+
maxPlaintextBytes: z.union([
|
|
609
|
+
z.number().int().min(TRANSPORT_PLAINTEXT_RANGE.min).max(TRANSPORT_PLAINTEXT_RANGE.max),
|
|
610
|
+
z.literal("auto")
|
|
611
|
+
]).transform((value) => value === "auto" ? void 0 : value).optional().describe("\u666E\u901A\u4E1A\u52A1\u53CC\u5411\u660E\u6587\u4E0A\u9650\uFF1Bauto \u4E3A 1 MiB\uFF1B\u5927\u6750\u6599\u4F7F\u7528 routePlaintextBytes \u5355\u72EC\u58F0\u660E").meta({ default: "auto" }),
|
|
612
|
+
maxFutureSkewSeconds: z.number().int().min(0).max(TRANSPORT_TIME_LIMITS.maxFutureSkewSeconds).optional().describe("\u5141\u8BB8\u5BA2\u6237\u7AEF\u8BF7\u6C42\u65F6\u95F4\u9886\u5148\u670D\u52A1\u5668\u7684\u79D2\u6570").meta({ default: TRANSPORT_PROTOCOL.CLOCK_SKEW_MS / 1e3 }),
|
|
613
|
+
maxRequestAgeSeconds: z.number().int().min(TRANSPORT_TIME_LIMITS.minRequestAgeSeconds).max(TRANSPORT_TIME_LIMITS.maxRequestAgeSeconds).optional().describe("\u8BF7\u6C42\u4ECE\u7B7E\u53D1\u8D77\u7684\u6709\u6548\u79D2\u6570").meta({ default: TRANSPORT_PROTOCOL.REQUEST_WINDOW_MS / 1e3 }),
|
|
669
614
|
routePlaintextBytes: z.record(AbsolutePathSchema, z.number().int().min(TRANSPORT_PLAINTEXT_RANGE.min).max(TRANSPORT_PLAINTEXT_RANGE.max)).optional().describe("\u5B8C\u6574\u8DEF\u5F84\u6BB5\u5230\u53CC\u5411\u660E\u6587\u5B57\u8282\u4E0A\u9650\u7684\u6620\u5C04\uFF1B\u661F\u53F7\u5339\u914D\u5355\u6BB5\uFF0C\u4F9B\u9644\u4EF6\u548C\u5F52\u6863\u72EC\u7ACB\u5BB9\u91CF"),
|
|
670
615
|
keyExchangePath: AbsolutePathSchema.default("/_hai/key-exchange"),
|
|
671
616
|
excludePaths: z.array(AbsolutePathSchema).default([])
|
|
@@ -678,7 +623,13 @@ var ServerConfigInputSchema = z.object({
|
|
|
678
623
|
var CorsRuntimeConfigInputSchema = z.object({
|
|
679
624
|
origin: z.string().optional(),
|
|
680
625
|
nativeOrigins: z.string().optional(),
|
|
681
|
-
allowedHeaders: z.array(z.string()).default([
|
|
626
|
+
allowedHeaders: z.array(z.string()).default([
|
|
627
|
+
"Authorization",
|
|
628
|
+
"Content-Type",
|
|
629
|
+
"X-Requested-With",
|
|
630
|
+
"X-Hai-Reauthentication",
|
|
631
|
+
"X-Hai-Terminal"
|
|
632
|
+
]),
|
|
682
633
|
exposedHeaders: z.array(z.string()).default([]),
|
|
683
634
|
credentials: z.boolean().default(false)
|
|
684
635
|
});
|
|
@@ -688,8 +639,7 @@ var ServHttpConfigInputSchema = z.object({
|
|
|
688
639
|
apiPrefix: ApiPrefixSchema.default("/api/v1"),
|
|
689
640
|
openapi: OpenAPIHttpConfigInputSchema.default(false),
|
|
690
641
|
docs: DocsHttpConfigInputSchema.default(false),
|
|
691
|
-
health: HealthHttpConfigInputSchema.default({ path: "/health", readyPath: "/ready" })
|
|
692
|
-
rpc: RpcHttpConfigInputSchema.default(false)
|
|
642
|
+
health: HealthHttpConfigInputSchema.default({ path: "/health", readyPath: "/ready" })
|
|
693
643
|
});
|
|
694
644
|
var ServConfigSchema = z.object({
|
|
695
645
|
server: ServerConfigInputSchema.optional(),
|
|
@@ -704,10 +654,10 @@ var ServConfigSchema = z.object({
|
|
|
704
654
|
}));
|
|
705
655
|
function readServerEnvOverride() {
|
|
706
656
|
const override = {};
|
|
707
|
-
const host =
|
|
657
|
+
const host = process4.env.HOST;
|
|
708
658
|
if (host)
|
|
709
659
|
override.host = host;
|
|
710
|
-
const rawPort =
|
|
660
|
+
const rawPort = process4.env.PORT;
|
|
711
661
|
if (rawPort) {
|
|
712
662
|
const port = Number(rawPort);
|
|
713
663
|
if (Number.isInteger(port) && port >= 1 && port <= 65535)
|
|
@@ -716,7 +666,10 @@ function readServerEnvOverride() {
|
|
|
716
666
|
return override;
|
|
717
667
|
}
|
|
718
668
|
function resolveServServerConfig(input = {}) {
|
|
719
|
-
return {
|
|
669
|
+
return {
|
|
670
|
+
...ServerConfigInputSchema.parse(input),
|
|
671
|
+
...readServerEnvOverride()
|
|
672
|
+
};
|
|
720
673
|
}
|
|
721
674
|
function resolveServHttpConfig(input = {}) {
|
|
722
675
|
return ServHttpConfigInputSchema.parse(input);
|
|
@@ -774,6 +727,87 @@ function buildAuthContextFactory(verifyToken) {
|
|
|
774
727
|
}
|
|
775
728
|
};
|
|
776
729
|
}
|
|
730
|
+
function createRuntimeSecurityPolicy(input) {
|
|
731
|
+
const production = input.environment?.trim().toLowerCase() === "production";
|
|
732
|
+
const configuredOrigins = parseOrigins(input.corsOrigin);
|
|
733
|
+
const configuredNativeOrigins = parseNativeOrigins(input.nativeOrigins);
|
|
734
|
+
const allowsEveryOrigin = configuredOrigins === "*";
|
|
735
|
+
if (production && (allowsEveryOrigin || configuredOrigins.length === 0))
|
|
736
|
+
throw new Error("serv.cors.origin must contain explicit HTTP(S) origins in production");
|
|
737
|
+
const allowedOrigins = allowsEveryOrigin ? void 0 : new Set(configuredOrigins);
|
|
738
|
+
const allowedNativeOrigins = new Set(configuredNativeOrigins);
|
|
739
|
+
return {
|
|
740
|
+
allowOrigin: (origin) => {
|
|
741
|
+
const normalizedNativeOrigin = tryNormalizeNativeOrigin(origin);
|
|
742
|
+
if (normalizedNativeOrigin !== void 0 && allowedNativeOrigins.has(normalizedNativeOrigin))
|
|
743
|
+
return true;
|
|
744
|
+
if (allowedOrigins === void 0)
|
|
745
|
+
return true;
|
|
746
|
+
const normalizedWebOrigin = tryNormalizeWebOrigin(origin);
|
|
747
|
+
return normalizedWebOrigin !== void 0 && allowedOrigins.has(normalizedWebOrigin);
|
|
748
|
+
},
|
|
749
|
+
isNativeOrigin: (origin) => allowedNativeOrigins.has(tryNormalizeNativeOrigin(origin) ?? ""),
|
|
750
|
+
secureRefreshCookie: resolveSecureRefreshCookie(production),
|
|
751
|
+
exposeApiDocs: !production
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
function resolveSecureRefreshCookie(defaultSecure) {
|
|
755
|
+
const override = process4.env.HAI_SERV_REFRESH_COOKIE_SECURE;
|
|
756
|
+
if (override === "true" || override === "false")
|
|
757
|
+
return override === "true";
|
|
758
|
+
return defaultSecure;
|
|
759
|
+
}
|
|
760
|
+
function parseOrigins(value) {
|
|
761
|
+
const entries = (value ?? "*").split(",").map((origin) => origin.trim()).filter(Boolean);
|
|
762
|
+
if (entries.includes("*"))
|
|
763
|
+
return "*";
|
|
764
|
+
return entries.map(normalizeWebOrigin);
|
|
765
|
+
}
|
|
766
|
+
function parseNativeOrigins(value) {
|
|
767
|
+
return (value ?? "").split(",").map((origin) => origin.trim()).filter(Boolean).map(normalizeNativeOrigin);
|
|
768
|
+
}
|
|
769
|
+
function normalizeWebOrigin(value) {
|
|
770
|
+
const normalized = tryNormalizeWebOrigin(value);
|
|
771
|
+
if (normalized === void 0)
|
|
772
|
+
throw new Error(`serv.cors.origin contains an invalid origin: ${value}`);
|
|
773
|
+
return normalized;
|
|
774
|
+
}
|
|
775
|
+
function normalizeNativeOrigin(value) {
|
|
776
|
+
const normalized = tryNormalizeNativeOrigin(value);
|
|
777
|
+
if (normalized === void 0)
|
|
778
|
+
throw new Error(`serv.cors.nativeOrigins contains an invalid origin: ${value}`);
|
|
779
|
+
return normalized;
|
|
780
|
+
}
|
|
781
|
+
function tryNormalizeWebOrigin(value) {
|
|
782
|
+
let url;
|
|
783
|
+
try {
|
|
784
|
+
url = new URL(value);
|
|
785
|
+
} catch {
|
|
786
|
+
return void 0;
|
|
787
|
+
}
|
|
788
|
+
if (!["http:", "https:"].includes(url.protocol) || url.origin === "null" || url.pathname !== "/" || url.search || url.hash)
|
|
789
|
+
return void 0;
|
|
790
|
+
return url.origin;
|
|
791
|
+
}
|
|
792
|
+
function tryNormalizeNativeOrigin(value) {
|
|
793
|
+
let url;
|
|
794
|
+
try {
|
|
795
|
+
url = new URL(value);
|
|
796
|
+
} catch {
|
|
797
|
+
return void 0;
|
|
798
|
+
}
|
|
799
|
+
if (![
|
|
800
|
+
"http:",
|
|
801
|
+
"https:",
|
|
802
|
+
"capacitor:",
|
|
803
|
+
"tauri:"
|
|
804
|
+
].includes(url.protocol) || !url.hostname || url.username || url.password || !["", "/"].includes(url.pathname) || url.search || url.hash) {
|
|
805
|
+
return void 0;
|
|
806
|
+
}
|
|
807
|
+
return ["capacitor:", "tauri:"].includes(url.protocol) ? `${url.protocol}//${url.host}` : url.origin;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// src/serv-cookie-auth.ts
|
|
777
811
|
var IAM_AUTH_PATHS = {
|
|
778
812
|
login: apiContract.pathOf(apiContract.iam.auth.login),
|
|
779
813
|
loginWithOtp: apiContract.pathOf(apiContract.iam.auth.loginWithOtp),
|
|
@@ -793,7 +827,7 @@ function resolveRefreshCookieConfig(apiPrefix, config, iam) {
|
|
|
793
827
|
return {
|
|
794
828
|
cookieName: config.cookieName ?? "hai_refresh_token",
|
|
795
829
|
maxAge: config.maxAge ?? 30 * 24 * 3600,
|
|
796
|
-
isSecure: config.secure ?? (
|
|
830
|
+
isSecure: config.secure ?? resolveSecureRefreshCookie(process4.env.NODE_ENV === "production" || process4.env.HAI_CORE_ENV === "production"),
|
|
797
831
|
refreshCookiePath: `${apiPrefix}${IAM_AUTH_PATHS.refresh}`,
|
|
798
832
|
onRefresh,
|
|
799
833
|
nativeTokenTransport: config.nativeTokenTransport
|
|
@@ -908,26 +942,46 @@ function mountRefreshCookieRoutes(app, apiPrefix, config, iam) {
|
|
|
908
942
|
const locale = resolveRequestLocale(c.req.raw.headers);
|
|
909
943
|
return c.json(buildHaiErrorBody(HaiCommonError.UNAUTHORIZED, servM("serv_errorUnauthorized", { locale })), 401);
|
|
910
944
|
}
|
|
911
|
-
const
|
|
912
|
-
|
|
913
|
-
|
|
945
|
+
const fail = (internal) => {
|
|
946
|
+
const safe = core.error.toPublic(internal, void 0, resolveRequestLocale(c.req.raw.headers));
|
|
947
|
+
core.logger.error("Refresh token request failed", { traceId: safe.traceId, error: internal });
|
|
914
948
|
const headers = new Headers({ "Content-Type": "application/json" });
|
|
915
|
-
if (!useNativeTokenTransport)
|
|
949
|
+
if (!useNativeTokenTransport && safe.httpStatus === 401)
|
|
916
950
|
headers.set("Set-Cookie", buildClearCookieHeader(cookieName, refreshCookiePath));
|
|
917
|
-
return new Response(JSON.stringify({ success: false, error:
|
|
918
|
-
status,
|
|
951
|
+
return new Response(JSON.stringify({ success: false, error: safe }), {
|
|
952
|
+
status: safe.httpStatus ?? 500,
|
|
919
953
|
headers
|
|
920
954
|
});
|
|
955
|
+
};
|
|
956
|
+
let result;
|
|
957
|
+
try {
|
|
958
|
+
result = await onRefresh(refreshToken);
|
|
959
|
+
} catch (cause) {
|
|
960
|
+
return fail({
|
|
961
|
+
...HaiCommonError.INTERNAL_ERROR,
|
|
962
|
+
message: servM("serv_errorInternal"),
|
|
963
|
+
cause
|
|
964
|
+
});
|
|
921
965
|
}
|
|
966
|
+
if (!result.success)
|
|
967
|
+
return fail(result.error);
|
|
922
968
|
const tokens = result.data;
|
|
923
969
|
if (useNativeTokenTransport) {
|
|
924
|
-
return new Response(JSON.stringify({
|
|
970
|
+
return new Response(JSON.stringify({
|
|
971
|
+
success: true,
|
|
972
|
+
data: { tokens }
|
|
973
|
+
}), {
|
|
925
974
|
status: 200,
|
|
926
975
|
headers: { "Content-Type": "application/json" }
|
|
927
976
|
});
|
|
928
977
|
}
|
|
929
978
|
const setCookie = buildSetCookieHeader(cookieName, tokens.refreshToken, refreshCookiePath, maxAge, isSecure);
|
|
930
|
-
return new Response(JSON.stringify({
|
|
979
|
+
return new Response(JSON.stringify({
|
|
980
|
+
success: true,
|
|
981
|
+
data: {
|
|
982
|
+
tokens: stripRefreshTokenFromPair(tokens)
|
|
983
|
+
}
|
|
984
|
+
}), {
|
|
931
985
|
status: 200,
|
|
932
986
|
headers: {
|
|
933
987
|
"Content-Type": "application/json",
|
|
@@ -942,17 +996,41 @@ async function readRefreshTokenFromBody(request) {
|
|
|
942
996
|
}
|
|
943
997
|
|
|
944
998
|
// src/serv-cors.ts
|
|
945
|
-
var DEFAULT_CORS_METHODS = [
|
|
946
|
-
|
|
999
|
+
var DEFAULT_CORS_METHODS = [
|
|
1000
|
+
"GET",
|
|
1001
|
+
"HEAD",
|
|
1002
|
+
"PUT",
|
|
1003
|
+
"PATCH",
|
|
1004
|
+
"POST",
|
|
1005
|
+
"DELETE",
|
|
1006
|
+
"OPTIONS"
|
|
1007
|
+
];
|
|
1008
|
+
var DEFAULT_CORS_ALLOWED_HEADERS = [
|
|
1009
|
+
"Authorization",
|
|
1010
|
+
"Content-Type",
|
|
1011
|
+
"X-Requested-With",
|
|
1012
|
+
"X-Hai-Reauthentication",
|
|
1013
|
+
"X-Hai-Terminal"
|
|
1014
|
+
];
|
|
947
1015
|
var DEFAULT_CORS_MAX_AGE = 86400;
|
|
948
1016
|
var corsMiddlewares = /* @__PURE__ */ new WeakSet();
|
|
949
1017
|
function isCorsMiddleware(middleware) {
|
|
950
1018
|
return corsMiddlewares.has(middleware);
|
|
951
1019
|
}
|
|
952
1020
|
function cors(config = {}) {
|
|
953
|
-
const methods = [
|
|
954
|
-
|
|
955
|
-
|
|
1021
|
+
const methods = [
|
|
1022
|
+
...config.methods ?? DEFAULT_CORS_METHODS
|
|
1023
|
+
];
|
|
1024
|
+
const allowedHeaders = [
|
|
1025
|
+
...config.allowedHeaders ?? DEFAULT_CORS_ALLOWED_HEADERS
|
|
1026
|
+
];
|
|
1027
|
+
const exposedHeaders = [
|
|
1028
|
+
.../* @__PURE__ */ new Set([
|
|
1029
|
+
"X-Request-Id",
|
|
1030
|
+
"X-Trace-Id",
|
|
1031
|
+
...config.exposedHeaders ?? []
|
|
1032
|
+
])
|
|
1033
|
+
];
|
|
956
1034
|
const credentials = config.credentials ?? false;
|
|
957
1035
|
const maxAge = config.maxAge ?? DEFAULT_CORS_MAX_AGE;
|
|
958
1036
|
const originConfig = config.origin ?? "*";
|
|
@@ -999,12 +1077,15 @@ function resolveCorsOrigin(origin, configOrigin) {
|
|
|
999
1077
|
return null;
|
|
1000
1078
|
if (configOrigin === "*")
|
|
1001
1079
|
return "*";
|
|
1002
|
-
if (typeof configOrigin === "string")
|
|
1080
|
+
if (typeof configOrigin === "string") {
|
|
1003
1081
|
return origin === configOrigin ? origin : null;
|
|
1004
|
-
|
|
1082
|
+
}
|
|
1083
|
+
if (Array.isArray(configOrigin)) {
|
|
1005
1084
|
return configOrigin.includes(origin) ? origin : null;
|
|
1006
|
-
|
|
1085
|
+
}
|
|
1086
|
+
if (typeof configOrigin === "function") {
|
|
1007
1087
|
return configOrigin(origin) ? origin : null;
|
|
1088
|
+
}
|
|
1008
1089
|
return null;
|
|
1009
1090
|
}
|
|
1010
1091
|
function buildCorsHeaders(options) {
|
|
@@ -1035,7 +1116,9 @@ var SCALAR_BROWSER_ENTRY_FALLBACK = "./dist/browser/standalone.js";
|
|
|
1035
1116
|
var scalarScriptCache;
|
|
1036
1117
|
async function generateSpec(contract, options = {}) {
|
|
1037
1118
|
const generator = new OpenAPIGenerator({
|
|
1038
|
-
schemaConverters: [
|
|
1119
|
+
schemaConverters: [
|
|
1120
|
+
new ZodToJsonSchemaConverter()
|
|
1121
|
+
]
|
|
1039
1122
|
});
|
|
1040
1123
|
return generator.generate(contract, {
|
|
1041
1124
|
info: {
|
|
@@ -1043,8 +1126,12 @@ async function generateSpec(contract, options = {}) {
|
|
|
1043
1126
|
version: options.version ?? "0.1.0",
|
|
1044
1127
|
description: options.description
|
|
1045
1128
|
},
|
|
1046
|
-
servers: options.apiPrefix ? [
|
|
1047
|
-
|
|
1129
|
+
servers: options.apiPrefix ? [
|
|
1130
|
+
{ url: options.apiPrefix }
|
|
1131
|
+
] : void 0,
|
|
1132
|
+
security: [
|
|
1133
|
+
{ bearerAuth: [] }
|
|
1134
|
+
],
|
|
1048
1135
|
components: {
|
|
1049
1136
|
securitySchemes: {
|
|
1050
1137
|
bearerAuth: { type: "http", scheme: "bearer" }
|
|
@@ -1103,8 +1190,13 @@ function createTransportMiddleware(manager, keyExchangePath, excludePaths = [],
|
|
|
1103
1190
|
return handleKeyExchange(c.req.raw, manager);
|
|
1104
1191
|
const mode = encryption?.(pathname, c.req.method);
|
|
1105
1192
|
if (mode === "disabled") {
|
|
1106
|
-
if (c.req.raw.headers.has(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER) || c.req.raw.headers.has(TRANSPORT_PROTOCOL.ENVELOPE_HEADER))
|
|
1107
|
-
return Response.json(err({
|
|
1193
|
+
if (c.req.raw.headers.has(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER) || c.req.raw.headers.has(TRANSPORT_PROTOCOL.ENVELOPE_HEADER)) {
|
|
1194
|
+
return Response.json(err({
|
|
1195
|
+
...HaiCommonError.VALIDATION_ERROR,
|
|
1196
|
+
message: servM("serv_transportPlaintextRequired"),
|
|
1197
|
+
traceId: core.context.get().traceId ?? core.context.get().requestId
|
|
1198
|
+
}), { status: 400 });
|
|
1199
|
+
}
|
|
1108
1200
|
return next();
|
|
1109
1201
|
}
|
|
1110
1202
|
if (mode !== "required" && shouldExclude(pathname, excludePaths, keyExchangePath))
|
|
@@ -1209,7 +1301,12 @@ async function handleKeyExchange(request, manager) {
|
|
|
1209
1301
|
if (new TextEncoder().encode(serverPublicKey).byteLength > TRANSPORT_BODY_LIMITS.publicKeyBytes)
|
|
1210
1302
|
return jsonError(servM("serv_transportKeyExchangeFailed"), 500);
|
|
1211
1303
|
const clientId = await manager.registerClientKey(body.clientPublicKey);
|
|
1212
|
-
return new Response(JSON.stringify({
|
|
1304
|
+
return new Response(JSON.stringify({
|
|
1305
|
+
serverKeyId: manager.getServerKeyId(),
|
|
1306
|
+
serverPublicKey,
|
|
1307
|
+
clientId,
|
|
1308
|
+
maxPlaintextBytes: manager.bodyLimits.plaintextBytes
|
|
1309
|
+
}), {
|
|
1213
1310
|
status: 200,
|
|
1214
1311
|
headers: { "Content-Type": "application/json; charset=utf-8" }
|
|
1215
1312
|
});
|
|
@@ -1257,7 +1354,14 @@ function jsonError(message, status) {
|
|
|
1257
1354
|
}
|
|
1258
1355
|
|
|
1259
1356
|
// src/serv-app.ts
|
|
1260
|
-
var API_METHODS = [
|
|
1357
|
+
var API_METHODS = [
|
|
1358
|
+
"GET",
|
|
1359
|
+
"POST",
|
|
1360
|
+
"PUT",
|
|
1361
|
+
"PATCH",
|
|
1362
|
+
"DELETE",
|
|
1363
|
+
"OPTIONS"
|
|
1364
|
+
];
|
|
1261
1365
|
var audioWsServers = /* @__PURE__ */ new WeakMap();
|
|
1262
1366
|
function createApp(options) {
|
|
1263
1367
|
const http = resolveServHttpConfig(options.http);
|
|
@@ -1276,7 +1380,12 @@ function createApp(options) {
|
|
|
1276
1380
|
});
|
|
1277
1381
|
app.use("*", async (c, next) => {
|
|
1278
1382
|
const requestId = resolveRequestId(c.req.header("x-request-id") ?? null);
|
|
1279
|
-
await core.context.run({
|
|
1383
|
+
await core.context.run({
|
|
1384
|
+
requestId,
|
|
1385
|
+
userId: void 0,
|
|
1386
|
+
scopeId: void 0,
|
|
1387
|
+
jobId: void 0
|
|
1388
|
+
}, () => core.i18n.runWithLocale(resolveRequestLocale(c.req.raw.headers), () => audit.withContext({
|
|
1280
1389
|
actor: null,
|
|
1281
1390
|
requestId,
|
|
1282
1391
|
traceId: requestId,
|
|
@@ -1306,14 +1415,26 @@ function createApp(options) {
|
|
|
1306
1415
|
});
|
|
1307
1416
|
app.notFound((c) => c.json(buildHaiErrorBody(
|
|
1308
1417
|
HaiCommonError.NOT_FOUND,
|
|
1309
|
-
core.i18n.coreM("core_errorNotFound", {
|
|
1418
|
+
core.i18n.coreM("core_errorNotFound", {
|
|
1419
|
+
locale: resolveRequestLocale(c.req.raw.headers)
|
|
1420
|
+
})
|
|
1310
1421
|
), 404));
|
|
1311
1422
|
const iamRef = options.iam;
|
|
1312
1423
|
const verifyToken = options.verifyToken ?? (iamRef ? (token) => iamRef.auth.verifyToken(token) : void 0);
|
|
1313
1424
|
const contextFactory = options.createContext ?? (verifyToken ? buildAuthContextFactory(verifyToken) : parseRequestContext);
|
|
1314
1425
|
const createContext = async (input) => {
|
|
1315
|
-
const context = await contextFactory({
|
|
1316
|
-
|
|
1426
|
+
const context = await contextFactory({
|
|
1427
|
+
...input,
|
|
1428
|
+
ip: resolveIp(input.request, input.remoteAddress)
|
|
1429
|
+
});
|
|
1430
|
+
requestAuditContexts.set(input.request, {
|
|
1431
|
+
actor: context.session ? {
|
|
1432
|
+
id: context.session.userId,
|
|
1433
|
+
username: context.session.username,
|
|
1434
|
+
roles: [...context.session.roles]
|
|
1435
|
+
} : null,
|
|
1436
|
+
scopeId: context.scopeId
|
|
1437
|
+
});
|
|
1317
1438
|
return context;
|
|
1318
1439
|
};
|
|
1319
1440
|
let cachedSpec;
|
|
@@ -1331,7 +1452,12 @@ function createApp(options) {
|
|
|
1331
1452
|
}
|
|
1332
1453
|
return void 0;
|
|
1333
1454
|
}
|
|
1334
|
-
core.logger.info("Request access policy denied", {
|
|
1455
|
+
core.logger.info("Request access policy denied", {
|
|
1456
|
+
ruleId: decision.ruleId,
|
|
1457
|
+
reason: decision.reason,
|
|
1458
|
+
method: c.req.method,
|
|
1459
|
+
ipAddress
|
|
1460
|
+
});
|
|
1335
1461
|
return c.json(buildHaiErrorBody(HaiCommonError.REQUEST_ACCESS_DENIED, core.i18n.coreM("core_requestAccessDenied")), 403);
|
|
1336
1462
|
};
|
|
1337
1463
|
const cors2 = options.middlewares?.filter((entry) => isCorsMiddleware(entry.middleware)) ?? [];
|
|
@@ -1366,7 +1492,9 @@ function createApp(options) {
|
|
|
1366
1492
|
const mgr = options.transport.crypto.transport.createServer({
|
|
1367
1493
|
serverKeyRing: options.transport.serverKeyRing,
|
|
1368
1494
|
keyStore: options.transport.keyStore,
|
|
1369
|
-
maxPlaintextBytes: policy.maximum.plaintextBytes
|
|
1495
|
+
maxPlaintextBytes: policy.maximum.plaintextBytes,
|
|
1496
|
+
maxFutureSkewSeconds: options.transport.maxFutureSkewSeconds,
|
|
1497
|
+
maxRequestAgeSeconds: options.transport.maxRequestAgeSeconds
|
|
1370
1498
|
});
|
|
1371
1499
|
if (!mgr.success) {
|
|
1372
1500
|
throw core.error.buildHaiErrorInst(
|
|
@@ -1402,14 +1530,13 @@ function createApp(options) {
|
|
|
1402
1530
|
audioWsServers.set(app, new WebSocketServer({ noServer: true }));
|
|
1403
1531
|
}
|
|
1404
1532
|
mountOpenAPIRoutes(app, options.procedures, http.apiPrefix, createContext);
|
|
1405
|
-
if (http.rpc !== false)
|
|
1406
|
-
mountRPCRoutes(app, options.procedures, http.rpc, createContext);
|
|
1407
1533
|
if (http.openapi !== false) {
|
|
1408
1534
|
app.get(http.openapi.path, async (c) => c.json(await getSpec()));
|
|
1409
1535
|
}
|
|
1410
1536
|
if (http.docs !== false) {
|
|
1411
1537
|
const docs = http.docs;
|
|
1412
|
-
|
|
1538
|
+
const docsDirectory = docs.path.slice(0, docs.path.lastIndexOf("/") + 1);
|
|
1539
|
+
app.get(`${docsDirectory}${SCALAR_ROUTE.slice(1)}`, async (c) => {
|
|
1413
1540
|
const content = await getScalarScript();
|
|
1414
1541
|
if (!content)
|
|
1415
1542
|
return c.notFound();
|
|
@@ -1419,12 +1546,21 @@ function createApp(options) {
|
|
|
1419
1546
|
});
|
|
1420
1547
|
app.get(docs.path, async (c) => {
|
|
1421
1548
|
if (docs.requireAuth) {
|
|
1422
|
-
const context = await createContext({
|
|
1423
|
-
|
|
1424
|
-
|
|
1549
|
+
const context = await createContext({
|
|
1550
|
+
request: c.req.raw,
|
|
1551
|
+
remoteAddress: readRemoteAddress(c.env)
|
|
1552
|
+
});
|
|
1553
|
+
if (!context.session) {
|
|
1554
|
+
return c.json(buildHaiErrorBody(HaiCommonError.UNAUTHORIZED, servM("serv_errorUnauthorized", {
|
|
1555
|
+
locale: resolveRequestLocale(c.req.raw.headers)
|
|
1556
|
+
})), 401);
|
|
1557
|
+
}
|
|
1425
1558
|
}
|
|
1426
|
-
const specUrl = http.openapi === false ? void 0 : http.openapi.path;
|
|
1427
|
-
return c.html(createDocsPage(await getSpec(), {
|
|
1559
|
+
const specUrl = http.openapi === false ? void 0 : posix.relative(docsDirectory, http.openapi.path) || ".";
|
|
1560
|
+
return c.html(createDocsPage(await getSpec(), {
|
|
1561
|
+
specUrl,
|
|
1562
|
+
scriptUrl: SCALAR_ROUTE.slice(1)
|
|
1563
|
+
}));
|
|
1428
1564
|
});
|
|
1429
1565
|
}
|
|
1430
1566
|
return app;
|
|
@@ -1442,14 +1578,11 @@ function mountOpenAPIRoutes(app, procedures, apiPrefix, createContext) {
|
|
|
1442
1578
|
const handler = new OpenAPIHandler(procedures);
|
|
1443
1579
|
app.on([...API_METHODS], `${apiPrefix}/*`, (c, next) => handleORPC(c, next, apiPrefix, handler, createContext));
|
|
1444
1580
|
}
|
|
1445
|
-
function mountRPCRoutes(app, procedures, rpcConfig, createContext) {
|
|
1446
|
-
const handler = new RPCHandler(procedures);
|
|
1447
|
-
const prefix = rpcConfig.prefix;
|
|
1448
|
-
app.use(`${prefix}/*`, requireInternalRPC(rpcConfig));
|
|
1449
|
-
app.on([...API_METHODS], `${prefix}/*`, (c, next) => handleORPC(c, next, prefix, handler, createContext));
|
|
1450
|
-
}
|
|
1451
1581
|
async function handleORPC(c, next, prefix, handler, createContext) {
|
|
1452
|
-
const context = await createContext({
|
|
1582
|
+
const context = await createContext({
|
|
1583
|
+
request: c.req.raw,
|
|
1584
|
+
remoteAddress: readRemoteAddress(c.env)
|
|
1585
|
+
});
|
|
1453
1586
|
const { matched, response } = await core.context.run({ requestId: context.requestId, userId: context.session?.userId, scopeId: context.scopeId }, () => handler.handle(c.req.raw, { prefix, context }));
|
|
1454
1587
|
if (!matched) {
|
|
1455
1588
|
await next();
|
|
@@ -1479,7 +1612,10 @@ async function localizeValidationResponse(response, requestHeaders) {
|
|
|
1479
1612
|
error: {
|
|
1480
1613
|
code: HaiCommonError.VALIDATION_ERROR.code,
|
|
1481
1614
|
traceId: core.context.get().traceId ?? core.context.get().requestId,
|
|
1482
|
-
message: core.error.formatMessage({
|
|
1615
|
+
message: core.error.formatMessage({
|
|
1616
|
+
message: servM("serv_validationFailed", { locale }),
|
|
1617
|
+
traceId: core.context.get().traceId ?? core.context.get().requestId
|
|
1618
|
+
}),
|
|
1483
1619
|
httpStatus: HaiCommonError.VALIDATION_ERROR.httpStatus,
|
|
1484
1620
|
system: HaiCommonError.VALIDATION_ERROR.system,
|
|
1485
1621
|
module: HaiCommonError.VALIDATION_ERROR.module,
|
|
@@ -1516,6 +1652,7 @@ function listen(app, options = {}) {
|
|
|
1516
1652
|
const host = options.host ?? DEFAULT_SERV_HOST;
|
|
1517
1653
|
const port = options.port ?? DEFAULT_SERV_PORT;
|
|
1518
1654
|
const audioServer = audioWsServers.get(app);
|
|
1655
|
+
const logger3 = core.logger.child({ module: "serv", scope: "node-adapter" });
|
|
1519
1656
|
const server = serve(
|
|
1520
1657
|
{
|
|
1521
1658
|
// 保留原 bindings 对象,WebSocket 升级状态由 Node adapter 在同一对象上共享。
|
|
@@ -1526,16 +1663,55 @@ function listen(app, options = {}) {
|
|
|
1526
1663
|
},
|
|
1527
1664
|
options.onListening
|
|
1528
1665
|
);
|
|
1666
|
+
let listenFailure;
|
|
1667
|
+
const rememberListenFailure = (error) => {
|
|
1668
|
+
listenFailure = error;
|
|
1669
|
+
logger3.error("Service listener failed", { error });
|
|
1670
|
+
};
|
|
1671
|
+
server.on(errorMonitor, rememberListenFailure);
|
|
1672
|
+
let closing;
|
|
1673
|
+
let removeSignalListeners = () => {
|
|
1674
|
+
};
|
|
1529
1675
|
const handle = {
|
|
1530
1676
|
server,
|
|
1531
1677
|
address: { host, port },
|
|
1532
|
-
close: () =>
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1678
|
+
close: () => closing ??= (async () => {
|
|
1679
|
+
removeSignalListeners();
|
|
1680
|
+
const httpClosed = new Promise((resolve, reject) => {
|
|
1681
|
+
server.close((error) => {
|
|
1682
|
+
server.removeListener(errorMonitor, rememberListenFailure);
|
|
1683
|
+
if (listenFailure) {
|
|
1684
|
+
reject(listenFailure);
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
if (error && !("code" in error && error.code === "ERR_SERVER_NOT_RUNNING" && !server.listening))
|
|
1688
|
+
reject(error);
|
|
1689
|
+
else resolve();
|
|
1690
|
+
});
|
|
1537
1691
|
});
|
|
1538
|
-
|
|
1692
|
+
const audioClosed = new Promise((resolve, reject) => {
|
|
1693
|
+
if (!audioServer) {
|
|
1694
|
+
resolve();
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
const deadline = setTimeout(() => {
|
|
1698
|
+
for (const socket of audioServer.clients)
|
|
1699
|
+
socket.terminate();
|
|
1700
|
+
}, 1e3);
|
|
1701
|
+
audioServer.close((error) => {
|
|
1702
|
+
clearTimeout(deadline);
|
|
1703
|
+
if (error)
|
|
1704
|
+
reject(error);
|
|
1705
|
+
else resolve();
|
|
1706
|
+
});
|
|
1707
|
+
for (const socket of audioServer.clients)
|
|
1708
|
+
socket.close(1001, "Server shutting down");
|
|
1709
|
+
});
|
|
1710
|
+
const results = await Promise.allSettled([httpClosed, audioClosed]);
|
|
1711
|
+
const errors = results.filter((result) => result.status === "rejected").map((result) => result.reason);
|
|
1712
|
+
if (errors.length)
|
|
1713
|
+
throw new AggregateError(errors, "Service listener shutdown failed");
|
|
1714
|
+
})()
|
|
1539
1715
|
};
|
|
1540
1716
|
if (options.onClose) {
|
|
1541
1717
|
const onClose = options.onClose;
|
|
@@ -1546,88 +1722,28 @@ function listen(app, options = {}) {
|
|
|
1546
1722
|
shuttingDown = true;
|
|
1547
1723
|
try {
|
|
1548
1724
|
await handle.close();
|
|
1549
|
-
} catch {
|
|
1725
|
+
} catch (error) {
|
|
1726
|
+
logger3.error("Service listener shutdown failed", { error });
|
|
1550
1727
|
}
|
|
1551
1728
|
try {
|
|
1552
1729
|
await onClose();
|
|
1553
|
-
} catch {
|
|
1730
|
+
} catch (error) {
|
|
1731
|
+
logger3.error("Service resource shutdown failed", { error });
|
|
1554
1732
|
}
|
|
1555
1733
|
};
|
|
1556
|
-
|
|
1557
|
-
|
|
1734
|
+
const onSignal = () => {
|
|
1735
|
+
void shutdown();
|
|
1736
|
+
};
|
|
1737
|
+
process4.once("SIGINT", onSignal);
|
|
1738
|
+
process4.once("SIGTERM", onSignal);
|
|
1739
|
+
removeSignalListeners = () => {
|
|
1740
|
+
process4.removeListener("SIGINT", onSignal);
|
|
1741
|
+
process4.removeListener("SIGTERM", onSignal);
|
|
1742
|
+
};
|
|
1558
1743
|
}
|
|
1559
1744
|
return handle;
|
|
1560
1745
|
}
|
|
1561
1746
|
|
|
1562
|
-
// src/serv-runtime-security.ts
|
|
1563
|
-
function createRuntimeSecurityPolicy(input) {
|
|
1564
|
-
const production = input.environment?.trim().toLowerCase() === "production";
|
|
1565
|
-
const configuredOrigins = parseOrigins(input.corsOrigin);
|
|
1566
|
-
const configuredNativeOrigins = parseNativeOrigins(input.nativeOrigins);
|
|
1567
|
-
const allowsEveryOrigin = configuredOrigins === "*";
|
|
1568
|
-
if (production && (allowsEveryOrigin || configuredOrigins.length === 0))
|
|
1569
|
-
throw new Error("serv.cors.origin must contain explicit HTTP(S) origins in production");
|
|
1570
|
-
const allowedOrigins = allowsEveryOrigin ? void 0 : new Set(configuredOrigins);
|
|
1571
|
-
const allowedNativeOrigins = new Set(configuredNativeOrigins);
|
|
1572
|
-
return {
|
|
1573
|
-
allowOrigin: (origin) => {
|
|
1574
|
-
const normalizedNativeOrigin = tryNormalizeNativeOrigin(origin);
|
|
1575
|
-
if (normalizedNativeOrigin !== void 0 && allowedNativeOrigins.has(normalizedNativeOrigin))
|
|
1576
|
-
return true;
|
|
1577
|
-
if (allowedOrigins === void 0)
|
|
1578
|
-
return true;
|
|
1579
|
-
const normalizedWebOrigin = tryNormalizeWebOrigin(origin);
|
|
1580
|
-
return normalizedWebOrigin !== void 0 && allowedOrigins.has(normalizedWebOrigin);
|
|
1581
|
-
},
|
|
1582
|
-
isNativeOrigin: (origin) => allowedNativeOrigins.has(tryNormalizeNativeOrigin(origin) ?? ""),
|
|
1583
|
-
secureRefreshCookie: production,
|
|
1584
|
-
exposeApiDocs: !production
|
|
1585
|
-
};
|
|
1586
|
-
}
|
|
1587
|
-
function parseOrigins(value) {
|
|
1588
|
-
const entries = (value ?? "*").split(",").map((origin) => origin.trim()).filter(Boolean);
|
|
1589
|
-
if (entries.includes("*"))
|
|
1590
|
-
return "*";
|
|
1591
|
-
return entries.map(normalizeWebOrigin);
|
|
1592
|
-
}
|
|
1593
|
-
function parseNativeOrigins(value) {
|
|
1594
|
-
return (value ?? "").split(",").map((origin) => origin.trim()).filter(Boolean).map(normalizeNativeOrigin);
|
|
1595
|
-
}
|
|
1596
|
-
function normalizeWebOrigin(value) {
|
|
1597
|
-
const normalized = tryNormalizeWebOrigin(value);
|
|
1598
|
-
if (normalized === void 0)
|
|
1599
|
-
throw new Error(`serv.cors.origin contains an invalid origin: ${value}`);
|
|
1600
|
-
return normalized;
|
|
1601
|
-
}
|
|
1602
|
-
function normalizeNativeOrigin(value) {
|
|
1603
|
-
const normalized = tryNormalizeNativeOrigin(value);
|
|
1604
|
-
if (normalized === void 0)
|
|
1605
|
-
throw new Error(`serv.cors.nativeOrigins contains an invalid origin: ${value}`);
|
|
1606
|
-
return normalized;
|
|
1607
|
-
}
|
|
1608
|
-
function tryNormalizeWebOrigin(value) {
|
|
1609
|
-
let url;
|
|
1610
|
-
try {
|
|
1611
|
-
url = new URL(value);
|
|
1612
|
-
} catch {
|
|
1613
|
-
return void 0;
|
|
1614
|
-
}
|
|
1615
|
-
if (!["http:", "https:"].includes(url.protocol) || url.origin === "null" || url.pathname !== "/" || url.search || url.hash)
|
|
1616
|
-
return void 0;
|
|
1617
|
-
return url.origin;
|
|
1618
|
-
}
|
|
1619
|
-
function tryNormalizeNativeOrigin(value) {
|
|
1620
|
-
let url;
|
|
1621
|
-
try {
|
|
1622
|
-
url = new URL(value);
|
|
1623
|
-
} catch {
|
|
1624
|
-
return void 0;
|
|
1625
|
-
}
|
|
1626
|
-
if (!["http:", "https:", "capacitor:", "tauri:"].includes(url.protocol) || !url.hostname || url.username || url.password || !["", "/"].includes(url.pathname) || url.search || url.hash)
|
|
1627
|
-
return void 0;
|
|
1628
|
-
return ["capacitor:", "tauri:"].includes(url.protocol) ? `${url.protocol}//${url.host}` : url.origin;
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
1747
|
// src/serv-storage-assets.ts
|
|
1632
1748
|
function storageAssets(config) {
|
|
1633
1749
|
const pathPrefix = config.pathPrefix.endsWith("/") ? config.pathPrefix : `${config.pathPrefix}/`;
|