@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/README.en.md +36 -19
- package/README.ja.md +36 -19
- package/README.md +36 -19
- package/dist/{chunk-IQZQTAEW.js → chunk-CZ6VA2EN.js} +3 -3
- package/dist/{chunk-IQZQTAEW.js.map → chunk-CZ6VA2EN.js.map} +1 -1
- package/dist/{chunk-FEG22LCX.js → chunk-PLGCHDZM.js} +24 -10
- package/dist/chunk-PLGCHDZM.js.map +1 -0
- package/dist/features/ai.d.ts +5 -4
- 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 +75 -26
- package/dist/index.js +248 -61
- package/dist/index.js.map +1 -1
- package/dist/{serv-context-BsNvQ-bb.d.ts → serv-context-DSsS_ksL.d.ts} +1 -56
- package/dist/{serv-router-BnkxPvS6.d.ts → serv-router-DWjYC0zD.d.ts} +2 -18
- package/package.json +29 -29
- package/dist/chunk-FEG22LCX.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
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';
|
|
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", {
|
|
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
|
}
|
|
@@ -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
|
-
{
|
|
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({
|
|
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([
|
|
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([
|
|
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 =
|
|
657
|
+
const host = process4.env.HOST;
|
|
636
658
|
if (host)
|
|
637
659
|
override.host = host;
|
|
638
|
-
const rawPort =
|
|
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 {
|
|
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 =
|
|
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 (![
|
|
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(
|
|
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
|
|
915
|
-
|
|
916
|
-
|
|
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:
|
|
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({
|
|
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({
|
|
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 = [
|
|
949
|
-
|
|
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 = [
|
|
957
|
-
|
|
958
|
-
|
|
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
|
-
|
|
1082
|
+
}
|
|
1083
|
+
if (Array.isArray(configOrigin)) {
|
|
1008
1084
|
return configOrigin.includes(origin) ? origin : null;
|
|
1009
|
-
|
|
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: [
|
|
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 ? [
|
|
1050
|
-
|
|
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({
|
|
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({
|
|
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 = [
|
|
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({
|
|
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", {
|
|
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({
|
|
1319
|
-
|
|
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", {
|
|
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({
|
|
1427
|
-
|
|
1428
|
-
|
|
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(), {
|
|
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({
|
|
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({
|
|
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: () =>
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
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
|
-
|
|
1555
|
-
|
|
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
|
}
|