@h-ai/serv 1.0.0-beta.17 → 1.0.0-beta.19

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,11 +1,12 @@
1
- import { validateInputOrFail, resolveRequestLocale, localizeZodError } from './chunk-IQZQTAEW.js';
2
- import { servM, WILDCARD_ROLE, WILDCARD_PERMISSION, implement, buildHaiErrorBody } from './chunk-FEG22LCX.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';
5
7
  import { posix } from 'path';
6
8
  import { apiContract, AUDIO_WS_PATH, AudioWsClientMessageSchema } from '@h-ai/api-contract';
7
9
  import { audit } from '@h-ai/audit';
8
- import { core, RequestAccessConfigSchema, TrustedProxiesSchema, HaiCommonError, err, ok } from '@h-ai/core';
9
10
  import { TRANSPORT_PLAINTEXT_RANGE, TRANSPORT_TIME_LIMITS, TRANSPORT_PROTOCOL, createTransportBodyPolicy, authenticateTransportRequest, TransportTimeError, createTransportErrorResponse, TransportBodyTooLargeError, readTransportBody, TRANSPORT_BODY_LIMITS, isValidSM2PublicKey } from '@h-ai/crypto';
10
11
  import { OpenAPIHandler } from '@orpc/openapi/fetch';
11
12
  import { Hono } from 'hono';
@@ -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
  }
@@ -565,7 +574,9 @@ function readRemoteAddress(metadata) {
565
574
  }
566
575
  var ApiPrefixSchema = z.custom(
567
576
  (value) => typeof value === "string" && /^\/[\w.-]+(?:\/[\w.-]+)*$/.test(value) && !value.split("/").some((segment) => segment === "." || segment === ".."),
568
- { error: () => servM("serv_invalidApiPrefix") }
577
+ {
578
+ error: () => servM("serv_invalidApiPrefix")
579
+ }
569
580
  );
570
581
  var AbsolutePathSchema = z.custom(
571
582
  (value) => typeof value === "string" && value.startsWith("/"),
@@ -573,7 +584,9 @@ var AbsolutePathSchema = z.custom(
573
584
  );
574
585
  var OpenAPIHttpConfigInputSchema = z.union([
575
586
  z.literal(false),
576
- z.object({ path: AbsolutePathSchema.default("/openapi.json") })
587
+ z.object({
588
+ path: AbsolutePathSchema.default("/openapi.json")
589
+ })
577
590
  ]);
578
591
  var DocsHttpConfigInputSchema = z.union([
579
592
  z.literal(false),
@@ -592,7 +605,10 @@ var HealthHttpConfigInputSchema = z.union([
592
605
  var TransportRuntimeConfigInputSchema = z.union([
593
606
  z.literal(false),
594
607
  z.object({
595
- 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" }),
596
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 }),
597
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 }),
598
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"),
@@ -607,7 +623,13 @@ var ServerConfigInputSchema = z.object({
607
623
  var CorsRuntimeConfigInputSchema = z.object({
608
624
  origin: z.string().optional(),
609
625
  nativeOrigins: z.string().optional(),
610
- 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
+ ]),
611
633
  exposedHeaders: z.array(z.string()).default([]),
612
634
  credentials: z.boolean().default(false)
613
635
  });
@@ -632,10 +654,10 @@ var ServConfigSchema = z.object({
632
654
  }));
633
655
  function readServerEnvOverride() {
634
656
  const override = {};
635
- const host = process.env.HOST;
657
+ const host = process4.env.HOST;
636
658
  if (host)
637
659
  override.host = host;
638
- const rawPort = process.env.PORT;
660
+ const rawPort = process4.env.PORT;
639
661
  if (rawPort) {
640
662
  const port = Number(rawPort);
641
663
  if (Number.isInteger(port) && port >= 1 && port <= 65535)
@@ -644,7 +666,10 @@ function readServerEnvOverride() {
644
666
  return override;
645
667
  }
646
668
  function resolveServServerConfig(input = {}) {
647
- return { ...ServerConfigInputSchema.parse(input), ...readServerEnvOverride() };
669
+ return {
670
+ ...ServerConfigInputSchema.parse(input),
671
+ ...readServerEnvOverride()
672
+ };
648
673
  }
649
674
  function resolveServHttpConfig(input = {}) {
650
675
  return ServHttpConfigInputSchema.parse(input);
@@ -727,7 +752,7 @@ function createRuntimeSecurityPolicy(input) {
727
752
  };
728
753
  }
729
754
  function resolveSecureRefreshCookie(defaultSecure) {
730
- const override = process.env.HAI_SERV_REFRESH_COOKIE_SECURE;
755
+ const override = process4.env.HAI_SERV_REFRESH_COOKIE_SECURE;
731
756
  if (override === "true" || override === "false")
732
757
  return override === "true";
733
758
  return defaultSecure;
@@ -771,8 +796,14 @@ function tryNormalizeNativeOrigin(value) {
771
796
  } catch {
772
797
  return void 0;
773
798
  }
774
- if (!["http:", "https:", "capacitor:", "tauri:"].includes(url.protocol) || !url.hostname || url.username || url.password || !["", "/"].includes(url.pathname) || url.search || url.hash)
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) {
775
805
  return void 0;
806
+ }
776
807
  return ["capacitor:", "tauri:"].includes(url.protocol) ? `${url.protocol}//${url.host}` : url.origin;
777
808
  }
778
809
 
@@ -796,7 +827,7 @@ function resolveRefreshCookieConfig(apiPrefix, config, iam) {
796
827
  return {
797
828
  cookieName: config.cookieName ?? "hai_refresh_token",
798
829
  maxAge: config.maxAge ?? 30 * 24 * 3600,
799
- isSecure: config.secure ?? resolveSecureRefreshCookie(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"),
800
831
  refreshCookiePath: `${apiPrefix}${IAM_AUTH_PATHS.refresh}`,
801
832
  onRefresh,
802
833
  nativeTokenTransport: config.nativeTokenTransport
@@ -911,26 +942,46 @@ function mountRefreshCookieRoutes(app, apiPrefix, config, iam) {
911
942
  const locale = resolveRequestLocale(c.req.raw.headers);
912
943
  return c.json(buildHaiErrorBody(HaiCommonError.UNAUTHORIZED, servM("serv_errorUnauthorized", { locale })), 401);
913
944
  }
914
- const result = await onRefresh(refreshToken);
915
- if (!result.success) {
916
- 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 });
917
948
  const headers = new Headers({ "Content-Type": "application/json" });
918
- if (!useNativeTokenTransport)
949
+ if (!useNativeTokenTransport && safe.httpStatus === 401)
919
950
  headers.set("Set-Cookie", buildClearCookieHeader(cookieName, refreshCookiePath));
920
- return new Response(JSON.stringify({ success: false, error: result.error }), {
921
- status,
951
+ return new Response(JSON.stringify({ success: false, error: safe }), {
952
+ status: safe.httpStatus ?? 500,
922
953
  headers
923
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
+ });
924
965
  }
966
+ if (!result.success)
967
+ return fail(result.error);
925
968
  const tokens = result.data;
926
969
  if (useNativeTokenTransport) {
927
- return new Response(JSON.stringify({ success: true, data: { tokens } }), {
970
+ return new Response(JSON.stringify({
971
+ success: true,
972
+ data: { tokens }
973
+ }), {
928
974
  status: 200,
929
975
  headers: { "Content-Type": "application/json" }
930
976
  });
931
977
  }
932
978
  const setCookie = buildSetCookieHeader(cookieName, tokens.refreshToken, refreshCookiePath, maxAge, isSecure);
933
- 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
+ }), {
934
985
  status: 200,
935
986
  headers: {
936
987
  "Content-Type": "application/json",
@@ -945,17 +996,41 @@ async function readRefreshTokenFromBody(request) {
945
996
  }
946
997
 
947
998
  // src/serv-cors.ts
948
- var DEFAULT_CORS_METHODS = ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE", "OPTIONS"];
949
- 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
+ ];
950
1015
  var DEFAULT_CORS_MAX_AGE = 86400;
951
1016
  var corsMiddlewares = /* @__PURE__ */ new WeakSet();
952
1017
  function isCorsMiddleware(middleware) {
953
1018
  return corsMiddlewares.has(middleware);
954
1019
  }
955
1020
  function cors(config = {}) {
956
- const methods = [...config.methods ?? DEFAULT_CORS_METHODS];
957
- const allowedHeaders = [...config.allowedHeaders ?? DEFAULT_CORS_ALLOWED_HEADERS];
958
- 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
+ ];
959
1034
  const credentials = config.credentials ?? false;
960
1035
  const maxAge = config.maxAge ?? DEFAULT_CORS_MAX_AGE;
961
1036
  const originConfig = config.origin ?? "*";
@@ -1002,12 +1077,15 @@ function resolveCorsOrigin(origin, configOrigin) {
1002
1077
  return null;
1003
1078
  if (configOrigin === "*")
1004
1079
  return "*";
1005
- if (typeof configOrigin === "string")
1080
+ if (typeof configOrigin === "string") {
1006
1081
  return origin === configOrigin ? origin : null;
1007
- if (Array.isArray(configOrigin))
1082
+ }
1083
+ if (Array.isArray(configOrigin)) {
1008
1084
  return configOrigin.includes(origin) ? origin : null;
1009
- if (typeof configOrigin === "function")
1085
+ }
1086
+ if (typeof configOrigin === "function") {
1010
1087
  return configOrigin(origin) ? origin : null;
1088
+ }
1011
1089
  return null;
1012
1090
  }
1013
1091
  function buildCorsHeaders(options) {
@@ -1038,7 +1116,9 @@ var SCALAR_BROWSER_ENTRY_FALLBACK = "./dist/browser/standalone.js";
1038
1116
  var scalarScriptCache;
1039
1117
  async function generateSpec(contract, options = {}) {
1040
1118
  const generator = new OpenAPIGenerator({
1041
- schemaConverters: [new ZodToJsonSchemaConverter()]
1119
+ schemaConverters: [
1120
+ new ZodToJsonSchemaConverter()
1121
+ ]
1042
1122
  });
1043
1123
  return generator.generate(contract, {
1044
1124
  info: {
@@ -1046,8 +1126,12 @@ async function generateSpec(contract, options = {}) {
1046
1126
  version: options.version ?? "0.1.0",
1047
1127
  description: options.description
1048
1128
  },
1049
- servers: options.apiPrefix ? [{ url: options.apiPrefix }] : void 0,
1050
- security: [{ bearerAuth: [] }],
1129
+ servers: options.apiPrefix ? [
1130
+ { url: options.apiPrefix }
1131
+ ] : void 0,
1132
+ security: [
1133
+ { bearerAuth: [] }
1134
+ ],
1051
1135
  components: {
1052
1136
  securitySchemes: {
1053
1137
  bearerAuth: { type: "http", scheme: "bearer" }
@@ -1106,8 +1190,13 @@ function createTransportMiddleware(manager, keyExchangePath, excludePaths = [],
1106
1190
  return handleKeyExchange(c.req.raw, manager);
1107
1191
  const mode = encryption?.(pathname, c.req.method);
1108
1192
  if (mode === "disabled") {
1109
- if (c.req.raw.headers.has(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER) || c.req.raw.headers.has(TRANSPORT_PROTOCOL.ENVELOPE_HEADER))
1110
- 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
+ }
1111
1200
  return next();
1112
1201
  }
1113
1202
  if (mode !== "required" && shouldExclude(pathname, excludePaths, keyExchangePath))
@@ -1212,7 +1301,12 @@ async function handleKeyExchange(request, manager) {
1212
1301
  if (new TextEncoder().encode(serverPublicKey).byteLength > TRANSPORT_BODY_LIMITS.publicKeyBytes)
1213
1302
  return jsonError(servM("serv_transportKeyExchangeFailed"), 500);
1214
1303
  const clientId = await manager.registerClientKey(body.clientPublicKey);
1215
- 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
+ }), {
1216
1310
  status: 200,
1217
1311
  headers: { "Content-Type": "application/json; charset=utf-8" }
1218
1312
  });
@@ -1260,7 +1354,14 @@ function jsonError(message, status) {
1260
1354
  }
1261
1355
 
1262
1356
  // src/serv-app.ts
1263
- 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
+ ];
1264
1365
  var audioWsServers = /* @__PURE__ */ new WeakMap();
1265
1366
  function createApp(options) {
1266
1367
  const http = resolveServHttpConfig(options.http);
@@ -1279,7 +1380,12 @@ function createApp(options) {
1279
1380
  });
1280
1381
  app.use("*", async (c, next) => {
1281
1382
  const requestId = resolveRequestId(c.req.header("x-request-id") ?? null);
1282
- 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({
1283
1389
  actor: null,
1284
1390
  requestId,
1285
1391
  traceId: requestId,
@@ -1309,14 +1415,26 @@ function createApp(options) {
1309
1415
  });
1310
1416
  app.notFound((c) => c.json(buildHaiErrorBody(
1311
1417
  HaiCommonError.NOT_FOUND,
1312
- 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
+ })
1313
1421
  ), 404));
1314
1422
  const iamRef = options.iam;
1315
1423
  const verifyToken = options.verifyToken ?? (iamRef ? (token) => iamRef.auth.verifyToken(token) : void 0);
1316
1424
  const contextFactory = options.createContext ?? (verifyToken ? buildAuthContextFactory(verifyToken) : parseRequestContext);
1317
1425
  const createContext = async (input) => {
1318
- const context = await contextFactory({ ...input, ip: resolveIp(input.request, input.remoteAddress) });
1319
- 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
+ });
1320
1438
  return context;
1321
1439
  };
1322
1440
  let cachedSpec;
@@ -1334,7 +1452,12 @@ function createApp(options) {
1334
1452
  }
1335
1453
  return void 0;
1336
1454
  }
1337
- 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
+ });
1338
1461
  return c.json(buildHaiErrorBody(HaiCommonError.REQUEST_ACCESS_DENIED, core.i18n.coreM("core_requestAccessDenied")), 403);
1339
1462
  };
1340
1463
  const cors2 = options.middlewares?.filter((entry) => isCorsMiddleware(entry.middleware)) ?? [];
@@ -1423,12 +1546,21 @@ function createApp(options) {
1423
1546
  });
1424
1547
  app.get(docs.path, async (c) => {
1425
1548
  if (docs.requireAuth) {
1426
- const context = await createContext({ request: c.req.raw, remoteAddress: readRemoteAddress(c.env) });
1427
- if (!context.session)
1428
- 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
+ }
1429
1558
  }
1430
1559
  const specUrl = http.openapi === false ? void 0 : posix.relative(docsDirectory, http.openapi.path) || ".";
1431
- return c.html(createDocsPage(await getSpec(), { specUrl, scriptUrl: SCALAR_ROUTE.slice(1) }));
1560
+ return c.html(createDocsPage(await getSpec(), {
1561
+ specUrl,
1562
+ scriptUrl: SCALAR_ROUTE.slice(1)
1563
+ }));
1432
1564
  });
1433
1565
  }
1434
1566
  return app;
@@ -1447,7 +1579,10 @@ function mountOpenAPIRoutes(app, procedures, apiPrefix, createContext) {
1447
1579
  app.on([...API_METHODS], `${apiPrefix}/*`, (c, next) => handleORPC(c, next, apiPrefix, handler, createContext));
1448
1580
  }
1449
1581
  async function handleORPC(c, next, prefix, handler, createContext) {
1450
- 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
+ });
1451
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 }));
1452
1587
  if (!matched) {
1453
1588
  await next();
@@ -1477,7 +1612,10 @@ async function localizeValidationResponse(response, requestHeaders) {
1477
1612
  error: {
1478
1613
  code: HaiCommonError.VALIDATION_ERROR.code,
1479
1614
  traceId: core.context.get().traceId ?? core.context.get().requestId,
1480
- 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
+ }),
1481
1619
  httpStatus: HaiCommonError.VALIDATION_ERROR.httpStatus,
1482
1620
  system: HaiCommonError.VALIDATION_ERROR.system,
1483
1621
  module: HaiCommonError.VALIDATION_ERROR.module,
@@ -1514,6 +1652,7 @@ function listen(app, options = {}) {
1514
1652
  const host = options.host ?? DEFAULT_SERV_HOST;
1515
1653
  const port = options.port ?? DEFAULT_SERV_PORT;
1516
1654
  const audioServer = audioWsServers.get(app);
1655
+ const logger3 = core.logger.child({ module: "serv", scope: "node-adapter" });
1517
1656
  const server = serve(
1518
1657
  {
1519
1658
  // 保留原 bindings 对象,WebSocket 升级状态由 Node adapter 在同一对象上共享。
@@ -1524,16 +1663,55 @@ function listen(app, options = {}) {
1524
1663
  },
1525
1664
  options.onListening
1526
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
+ };
1527
1675
  const handle = {
1528
1676
  server,
1529
1677
  address: { host, port },
1530
- close: () => new Promise((resolve, reject) => {
1531
- server.close((error) => {
1532
- if (error)
1533
- reject(error);
1534
- 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
+ });
1535
1691
  });
1536
- })
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
+ })()
1537
1715
  };
1538
1716
  if (options.onClose) {
1539
1717
  const onClose = options.onClose;
@@ -1544,15 +1722,24 @@ function listen(app, options = {}) {
1544
1722
  shuttingDown = true;
1545
1723
  try {
1546
1724
  await handle.close();
1547
- } catch {
1725
+ } catch (error) {
1726
+ logger3.error("Service listener shutdown failed", { error });
1548
1727
  }
1549
1728
  try {
1550
1729
  await onClose();
1551
- } catch {
1730
+ } catch (error) {
1731
+ logger3.error("Service resource shutdown failed", { error });
1552
1732
  }
1553
1733
  };
1554
- process.once("SIGINT", () => void shutdown());
1555
- 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
+ };
1556
1743
  }
1557
1744
  return handle;
1558
1745
  }