@h-ai/serv 1.0.0-beta.2 → 1.0.0-beta.20

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/dist/index.js CHANGED
@@ -1,13 +1,14 @@
1
- import { validateInputOrFail, resolveRequestLocale, localizeZodError } from './chunk-TWMOZPGU.js';
2
- import { servM, WILDCARD_ROLE, WILDCARD_PERMISSION, implement, buildHaiErrorBody } from './chunk-I3F7RN4I.js';
3
- import process from 'process';
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 { core, RequestAccessConfigSchema, TrustedProxiesSchema, HaiCommonError, err, ok } from '@h-ai/core';
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", { error: error instanceof Error ? error.message : String(error) });
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" ? { code: error.code, message: error.message, httpStatus: "httpStatus" in error && typeof error.httpStatus === "number" ? error.httpStatus : 500 } : { code: HaiCommonError.INTERNAL_ERROR.code, message: "", httpStatus: 500 };
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", { error: error instanceof Error ? error.message : String(error) });
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.startsWith("/api/"),
632
- "apiPrefix must start with /api/"
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({ path: AbsolutePathSchema.default("/openapi.json") })
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([z.number().int().min(TRANSPORT_PLAINTEXT_RANGE.min).max(TRANSPORT_PLAINTEXT_RANGE.max), z.literal("auto")]).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" }),
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(["Authorization", "Content-Type", "X-Requested-With", "X-Hai-Reauthentication", "X-Hai-Terminal"]),
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 = process.env.HOST;
657
+ const host = process4.env.HOST;
708
658
  if (host)
709
659
  override.host = host;
710
- const rawPort = process.env.PORT;
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 { ...ServerConfigInputSchema.parse(input), ...readServerEnvOverride() };
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 ?? (process.env.NODE_ENV === "production" || process.env.HAI_CORE_ENV === "production"),
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 result = await onRefresh(refreshToken);
912
- if (!result.success) {
913
- const status = result.error.httpStatus ?? 401;
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: result.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({ success: true, data: { tokens } }), {
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({ success: true, data: { tokens: stripRefreshTokenFromPair(tokens) } }), {
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 = ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE", "OPTIONS"];
946
- var DEFAULT_CORS_ALLOWED_HEADERS = ["Authorization", "Content-Type", "X-Requested-With", "X-Hai-Reauthentication", "X-Hai-Terminal"];
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 = [...config.methods ?? DEFAULT_CORS_METHODS];
954
- const allowedHeaders = [...config.allowedHeaders ?? DEFAULT_CORS_ALLOWED_HEADERS];
955
- const exposedHeaders = [.../* @__PURE__ */ new Set(["X-Request-Id", "X-Trace-Id", ...config.exposedHeaders ?? []])];
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
- if (Array.isArray(configOrigin))
1082
+ }
1083
+ if (Array.isArray(configOrigin)) {
1005
1084
  return configOrigin.includes(origin) ? origin : null;
1006
- if (typeof configOrigin === "function")
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: [new ZodToJsonSchemaConverter()]
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 ? [{ url: options.apiPrefix }] : void 0,
1047
- security: [{ bearerAuth: [] }],
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({ ...HaiCommonError.VALIDATION_ERROR, message: servM("serv_transportPlaintextRequired"), traceId: core.context.get().traceId ?? core.context.get().requestId }), { status: 400 });
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({ serverKeyId: manager.getServerKeyId(), serverPublicKey, clientId, maxPlaintextBytes: manager.bodyLimits.plaintextBytes }), {
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 = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
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({ requestId, userId: void 0, scopeId: void 0, jobId: void 0 }, () => core.i18n.runWithLocale(resolveRequestLocale(c.req.raw.headers), () => audit.withContext({
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", { locale: resolveRequestLocale(c.req.raw.headers) })
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({ ...input, ip: resolveIp(input.request, input.remoteAddress) });
1316
- requestAuditContexts.set(input.request, { actor: context.session ? { id: context.session.userId, username: context.session.username, roles: [...context.session.roles] } : null, scopeId: context.scopeId });
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", { ruleId: decision.ruleId, reason: decision.reason, method: c.req.method, ipAddress });
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
- app.get(SCALAR_ROUTE, async (c) => {
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({ request: c.req.raw, remoteAddress: readRemoteAddress(c.env) });
1423
- if (!context.session)
1424
- return c.json(buildHaiErrorBody(HaiCommonError.UNAUTHORIZED, servM("serv_errorUnauthorized", { locale: resolveRequestLocale(c.req.raw.headers) })), 401);
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(), { specUrl }));
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({ request: c.req.raw, remoteAddress: readRemoteAddress(c.env) });
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({ message: servM("serv_validationFailed", { locale }), traceId: core.context.get().traceId ?? core.context.get().requestId }),
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: () => new Promise((resolve, reject) => {
1533
- server.close((error) => {
1534
- if (error)
1535
- reject(error);
1536
- else resolve();
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
- process.once("SIGINT", () => void shutdown());
1557
- process.once("SIGTERM", () => void shutdown());
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}/`;