@h-ai/serv 0.1.0-alpha.67 → 0.1.0-alpha.69

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,26 +1,53 @@
1
- import { servM, WILDCARD_ROLE, WILDCARD_PERMISSION, implement, normalizeServLocale, buildHaiErrorBody } from './chunk-R354OMWL.js';
1
+ import { validateInputOrFail, resolveRequestLocale, localizeZodError } from './chunk-BGWE4S6Z.js';
2
+ import { servM, WILDCARD_ROLE, WILDCARD_PERMISSION, implement, buildHaiErrorBody } from './chunk-TWHCU6HT.js';
2
3
  import process from 'process';
3
4
  import { upgradeWebSocket, serve } from '@hono/node-server';
4
5
  import { apiContract, AUDIO_WS_PATH, AudioWsClientMessageSchema } from '@h-ai/api-contract';
5
- import { core, ok, err, HaiCommonError } from '@h-ai/core';
6
+ import { core, HaiCommonError, err, ok } from '@h-ai/core';
6
7
  import { OpenAPIHandler } from '@orpc/openapi/fetch';
7
8
  import { RPCHandler } from '@orpc/server/fetch';
8
9
  import { Hono } from 'hono';
9
10
  import { WebSocketServer } from 'ws';
10
11
  import { Buffer } from 'buffer';
11
12
  import { BlockList, isIP } from 'net';
13
+ import { TRANSPORT_PLAINTEXT_RANGE, TRANSPORT_BODY_LIMITS, TRANSPORT_PROTOCOL, authenticateTransportRequest, TransportTimeError, createTransportErrorResponse, TransportBodyTooLargeError, readTransportBody, isValidSM2PublicKey } from '@h-ai/crypto';
12
14
  import { z } from 'zod';
13
15
  import { getCookie } from 'hono/cookie';
14
16
  import { readFile } from 'fs/promises';
15
17
  import { OpenAPIGenerator } from '@orpc/openapi';
16
18
  import { ZodToJsonSchemaConverter } from '@orpc/zod/zod4';
17
- import { TRANSPORT_PROTOCOL } from '@h-ai/crypto';
18
19
 
19
20
  // src/adapters/serv-adapter-fetch.ts
20
21
  function toFetch(app) {
21
- const handler = (input, init) => {
22
- const request = input instanceof Request ? input : new Request(typeof input === "string" ? input : input.toString(), init);
23
- return Promise.resolve(app.fetch(request));
22
+ const handler = async (input, init) => {
23
+ const request = new Request(input, init);
24
+ const { signal } = request;
25
+ signal.throwIfAborted();
26
+ let onAbort;
27
+ const cancelled = new Promise((_resolve, reject) => {
28
+ onAbort = () => reject(signal.reason);
29
+ signal.addEventListener("abort", onAbort, { once: true });
30
+ });
31
+ try {
32
+ return await Promise.race([
33
+ Promise.resolve().then(async () => {
34
+ signal.throwIfAborted();
35
+ const response = await app.fetch(request);
36
+ if (signal.aborted) {
37
+ try {
38
+ await response.body?.cancel(signal.reason);
39
+ } catch {
40
+ }
41
+ signal.throwIfAborted();
42
+ }
43
+ return response;
44
+ }),
45
+ cancelled
46
+ ]);
47
+ } finally {
48
+ if (onAbort)
49
+ signal.removeEventListener("abort", onAbort);
50
+ }
24
51
  };
25
52
  return handler;
26
53
  }
@@ -585,36 +612,6 @@ function createClientIpResolver(trustedProxies) {
585
612
  return current;
586
613
  };
587
614
  }
588
- var DEFAULT_LOCALE = "zh-CN";
589
- function resolveRequestLocale(headers) {
590
- const get = (name) => {
591
- if (headers instanceof Headers)
592
- return headers.get(name) ?? void 0;
593
- return headers[name] ?? headers[name.toLowerCase()];
594
- };
595
- const explicit = get("x-hai-locale");
596
- if (explicit)
597
- return normalizeServLocale(explicit);
598
- return normalizeServLocale(get("accept-language") ?? get("Accept-Language") ?? DEFAULT_LOCALE);
599
- }
600
- function localizeZodError(zodError, locale) {
601
- const resolvedLocale = normalizeServLocale(locale);
602
- return core.zodValidation.mapZodErrorToFormErrors(
603
- zodError,
604
- core.zodValidation.createPrefixedZodMessageGetter(
605
- "serv",
606
- (messageKey, params) => servM(messageKey, { locale: resolvedLocale, params })
607
- )
608
- );
609
- }
610
- function validateInputOrFail(schema, input, locale = DEFAULT_LOCALE) {
611
- const resolvedLocale = normalizeServLocale(locale);
612
- const result = schema.safeParse(input);
613
- if (result.success)
614
- return ok(result.data);
615
- const errors = localizeZodError(result.error, resolvedLocale);
616
- return err(HaiCommonError.VALIDATION_ERROR, servM("serv_validationFailed", { locale: resolvedLocale }), errors);
617
- }
618
615
 
619
616
  // src/pipelines/serv-pipeline-require-internal-rpc.ts
620
617
  var pipelineLogger = core.logger.child({ module: "serv", scope: "pipeline" });
@@ -727,9 +724,9 @@ var RpcHttpConfigInputSchema = z.union([
727
724
  var TransportRuntimeConfigInputSchema = z.union([
728
725
  z.literal(false),
729
726
  z.object({
727
+ maxPlaintextBytes: z.number().int().min(TRANSPORT_PLAINTEXT_RANGE.min).max(TRANSPORT_PLAINTEXT_RANGE.max).optional().describe("\u53CC\u5411\u4E1A\u52A1\u660E\u6587 UTF-8 \u5B57\u8282\u4E0A\u9650\uFF1B\u9ED8\u8BA4 1 MiB\uFF0C\u5BC6\u6587\u4E0E\u4EE3\u7406\u9884\u7B97\u81EA\u52A8\u63A8\u5BFC").meta({ default: TRANSPORT_BODY_LIMITS.plaintextBytes }),
730
728
  keyExchangePath: AbsolutePathSchema.default("/_hai/key-exchange"),
731
- excludePaths: z.array(AbsolutePathSchema).default([]),
732
- maxClients: z.number().int().positive().default(1e4)
729
+ excludePaths: z.array(AbsolutePathSchema).default([])
733
730
  })
734
731
  ]);
735
732
  var ServerConfigInputSchema = z.object({
@@ -790,6 +787,9 @@ function resolveServTransportRuntimeConfig(input = false) {
790
787
  var HEADER_REQUEST_ID = "x-request-id";
791
788
  var HEADER_AUTHORIZATION = "authorization";
792
789
  var HEADER_USER_AGENT = "user-agent";
790
+ function resolveRequestId(value) {
791
+ return value && /^[\w.:-]{1,128}$/.test(value) ? value : core.id.uuid();
792
+ }
793
793
  function extractBearerToken(value) {
794
794
  if (!value)
795
795
  return void 0;
@@ -800,7 +800,7 @@ function extractBearerToken(value) {
800
800
  }
801
801
  function parseRequestContext(input) {
802
802
  const headers = input.request.headers;
803
- const requestId = headers.get(HEADER_REQUEST_ID) ?? core.id.uuid();
803
+ const requestId = core.context.get().requestId ?? resolveRequestId(headers.get(HEADER_REQUEST_ID));
804
804
  const ip = normalizeClientIp(Object.hasOwn(input, "ip") ? input.ip : input.remoteAddress);
805
805
  const locale = resolveRequestLocale(headers);
806
806
  const accessToken = extractBearerToken(headers.get(HEADER_AUTHORIZATION));
@@ -848,7 +848,7 @@ function resolveRefreshCookieConfig(apiPrefix, config, iam) {
848
848
  return {
849
849
  cookieName: config.cookieName ?? "hai_refresh_token",
850
850
  maxAge: config.maxAge ?? 30 * 24 * 3600,
851
- isSecure: config.secure ?? process.env.NODE_ENV === "production",
851
+ isSecure: config.secure ?? (process.env.NODE_ENV === "production" || process.env.HAI_CORE_ENV === "production"),
852
852
  refreshCookiePath: `${apiPrefix}${IAM_AUTH_PATHS.refresh}`,
853
853
  onRefresh,
854
854
  nativeTokenTransport: config.nativeTokenTransport
@@ -1058,7 +1058,6 @@ function readScalarBrowserEntry(packageInfo) {
1058
1058
  return typeof browser === "string" ? browser : void 0;
1059
1059
  }
1060
1060
  var logger2 = core.logger.child({ module: "serv", scope: "transport" });
1061
- var MAX_ENCRYPTED_BODY = 1048576;
1062
1061
  var ENCRYPTABLE_RESPONSE_CONTENT_TYPES = ["application/json"];
1063
1062
  function createTransportMiddleware(manager, keyExchangePath, excludePaths = []) {
1064
1063
  return async (c, next) => {
@@ -1070,55 +1069,72 @@ function createTransportMiddleware(manager, keyExchangePath, excludePaths = [])
1070
1069
  const clientId = c.req.header(TRANSPORT_PROTOCOL.CLIENT_ID_HEADER);
1071
1070
  if (!clientId)
1072
1071
  return jsonError(servM("serv_transportClientIdRequired"), 400);
1073
- const clientPublicKey = await manager.getClientPublicKey(clientId);
1074
- if (!clientPublicKey)
1075
- return jsonError(servM("serv_transportClientKeyNotFound"), 400);
1076
- if (hasBody(c.req.method) && !isEmptyBody(c.req.raw)) {
1077
- const decErr = await decryptRequestInPlace(c, manager);
1078
- if (decErr)
1079
- return decErr;
1072
+ let requestContext;
1073
+ try {
1074
+ const verified = await authenticateTransportRequest(manager, c.req.raw);
1075
+ if (!verified.success) {
1076
+ if (verified.error.cause instanceof TransportTimeError && verified.error.cause.context)
1077
+ return createTransportErrorResponse(verified.error.cause, { manager, context: verified.error.cause.context });
1078
+ return jsonError(servM("serv_transportDecryptFailed"), 400);
1079
+ }
1080
+ requestContext = verified.data.context;
1081
+ if (c.req.method !== "GET" && c.req.method !== "HEAD")
1082
+ replaceRequestBody(c, verified.data.plaintext);
1083
+ } catch (error) {
1084
+ if (error instanceof TransportBodyTooLargeError)
1085
+ return createTransportErrorResponse(error);
1086
+ return jsonError(servM("serv_transportInvalidPayload"), error instanceof RangeError ? 413 : 400);
1080
1087
  }
1081
1088
  await next();
1082
1089
  const response = c.res;
1083
1090
  if (!response)
1084
1091
  return;
1085
1092
  const contentType = response.headers.get("Content-Type") ?? "";
1086
- if (isEmptyResponse(response))
1087
- return;
1088
- if (!canEncryptResponseContentType(contentType)) {
1089
- c.res = jsonError(servM("serv_transportEncryptFailed"), 500);
1090
- return;
1091
- }
1092
- const contentLength = response.headers.get("Content-Length");
1093
- if (contentLength && Number.parseInt(contentLength, 10) > MAX_ENCRYPTED_BODY) {
1093
+ const bodyless = c.req.method === "HEAD" || isEmptyResponse(response);
1094
+ if (bodyless)
1095
+ void response.body?.cancel().catch(() => {
1096
+ });
1097
+ c.header("Content-Length", void 0);
1098
+ c.header("Content-Encoding", void 0);
1099
+ c.header(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER, void 0);
1100
+ c.header(TRANSPORT_PROTOCOL.ENVELOPE_HEADER, void 0);
1101
+ if (!bodyless && !canEncryptResponseContentType(contentType)) {
1102
+ void response.body?.cancel().catch(() => {
1103
+ });
1094
1104
  c.res = jsonError(servM("serv_transportEncryptFailed"), 500);
1095
1105
  return;
1096
1106
  }
1097
1107
  let bodyText;
1098
1108
  try {
1099
- bodyText = await response.clone().text();
1109
+ bodyText = bodyless ? "" : await readTransportBody(response.body, {
1110
+ maxBytes: manager.bodyLimits.plaintextBytes,
1111
+ direction: "response",
1112
+ signal: c.req.raw.signal
1113
+ });
1100
1114
  } catch (cause) {
1115
+ if (cause instanceof TransportBodyTooLargeError) {
1116
+ c.res = await createTransportErrorResponse(cause, { manager, context: requestContext });
1117
+ return;
1118
+ }
1101
1119
  logger2.warn("Failed to read response body for encryption", { error: cause });
1102
1120
  c.res = jsonError(servM("serv_transportEncryptFailed"), 500);
1103
1121
  return;
1104
1122
  }
1105
- if (!bodyText)
1106
- return;
1107
- if (bodyText.length > MAX_ENCRYPTED_BODY) {
1108
- c.res = jsonError(servM("serv_transportEncryptFailed"), 500);
1109
- return;
1110
- }
1111
- const encResult = await manager.encryptResponse(clientId, bodyText);
1123
+ const headers = new Headers(response.headers);
1124
+ headers.delete(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER);
1125
+ headers.delete("Content-Length");
1126
+ headers.delete("Content-Encoding");
1127
+ const encResult = await manager.encryptResponse(requestContext, bodyText, response.status);
1112
1128
  if (!encResult.success) {
1113
1129
  logger2.warn("Failed to encrypt response", { error: encResult.error });
1114
1130
  c.res = jsonError(servM("serv_transportEncryptFailed"), 500);
1115
1131
  return;
1116
1132
  }
1117
- const headers = new Headers(response.headers);
1118
1133
  headers.set("Content-Type", "application/json; charset=utf-8");
1119
1134
  headers.set(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER, TRANSPORT_PROTOCOL.ENCRYPTED_HEADER_VALUE);
1120
- headers.delete("Content-Length");
1121
- c.res = new Response(JSON.stringify(encResult.data), {
1135
+ if (bodyless)
1136
+ headers.set(TRANSPORT_PROTOCOL.ENVELOPE_HEADER, JSON.stringify(encResult.data));
1137
+ c.res = new Response(bodyless ? null : JSON.stringify(encResult.data), {
1122
1138
  status: response.status,
1123
1139
  statusText: response.statusText,
1124
1140
  headers
@@ -1128,16 +1144,22 @@ function createTransportMiddleware(manager, keyExchangePath, excludePaths = [])
1128
1144
  async function handleKeyExchange(request, manager) {
1129
1145
  let body;
1130
1146
  try {
1131
- body = await request.json();
1132
- } catch {
1133
- return jsonError(servM("serv_transportInvalidPayload"), 400);
1147
+ body = JSON.parse(await readTransportBody(request.body, {
1148
+ maxBytes: TRANSPORT_BODY_LIMITS.keyExchangeBytes,
1149
+ signal: request.signal
1150
+ }));
1151
+ } catch (error) {
1152
+ return jsonError(servM("serv_transportInvalidPayload"), error instanceof RangeError ? 413 : 400);
1134
1153
  }
1135
- if (typeof body.clientPublicKey !== "string" || body.clientPublicKey.length === 0)
1154
+ if (!body || typeof body !== "object" || !("clientPublicKey" in body) || !isValidSM2PublicKey(body.clientPublicKey) || new TextEncoder().encode(body.clientPublicKey).byteLength > TRANSPORT_BODY_LIMITS.publicKeyBytes) {
1136
1155
  return jsonError(servM("serv_transportInvalidPayload"), 400);
1156
+ }
1137
1157
  try {
1138
- const clientId = await manager.registerClientKey(body.clientPublicKey);
1139
1158
  const serverPublicKey = manager.getServerPublicKey();
1140
- return new Response(JSON.stringify({ serverPublicKey, clientId }), {
1159
+ if (new TextEncoder().encode(serverPublicKey).byteLength > TRANSPORT_BODY_LIMITS.publicKeyBytes)
1160
+ return jsonError(servM("serv_transportKeyExchangeFailed"), 500);
1161
+ const clientId = await manager.registerClientKey(body.clientPublicKey);
1162
+ return new Response(JSON.stringify({ serverKeyId: manager.getServerKeyId(), serverPublicKey, clientId, maxPlaintextBytes: manager.bodyLimits.plaintextBytes }), {
1141
1163
  status: 200,
1142
1164
  headers: { "Content-Type": "application/json; charset=utf-8" }
1143
1165
  });
@@ -1146,37 +1168,23 @@ async function handleKeyExchange(request, manager) {
1146
1168
  return jsonError(servM("serv_transportKeyExchangeFailed"), 500);
1147
1169
  }
1148
1170
  }
1149
- async function decryptRequestInPlace(c, manager) {
1150
- const isEncrypted = c.req.header(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER) === TRANSPORT_PROTOCOL.ENCRYPTED_HEADER_VALUE;
1151
- if (!isEncrypted)
1152
- return jsonError(servM("serv_transportInvalidPayload"), 400);
1153
- let payload;
1154
- try {
1155
- payload = await c.req.raw.clone().json();
1156
- } catch {
1157
- return jsonError(servM("serv_transportInvalidPayload"), 400);
1158
- }
1159
- if (!isEncryptedPayloadShape(payload))
1160
- return jsonError(servM("serv_transportInvalidPayload"), 400);
1161
- const decResult = manager.decryptRequest(payload);
1162
- if (!decResult.success) {
1163
- logger2.warn("Failed to decrypt request", { error: decResult.error });
1164
- return jsonError(servM("serv_transportDecryptFailed"), 400);
1165
- }
1171
+ function replaceRequestBody(c, body) {
1166
1172
  const original = c.req.raw;
1167
1173
  const newHeaders = new Headers(original.headers);
1168
1174
  newHeaders.delete(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER);
1169
1175
  newHeaders.delete("Content-Length");
1176
+ newHeaders.delete("Content-Encoding");
1170
1177
  newHeaders.set("Content-Type", "application/json; charset=utf-8");
1171
1178
  const newRequest = new Request(original.url, {
1172
1179
  method: original.method,
1173
1180
  headers: newHeaders,
1174
- body: decResult.data,
1181
+ body,
1182
+ signal: original.signal,
1175
1183
  // @ts-expect-error Node fetch 需要 duplex 才能携带流式 body。
1176
1184
  duplex: "half"
1177
1185
  });
1178
1186
  c.req.raw = newRequest;
1179
- return void 0;
1187
+ c.req.bodyCache = {};
1180
1188
  }
1181
1189
  function shouldExclude(pathname, excludePaths, keyExchangePath) {
1182
1190
  if (pathname === keyExchangePath)
@@ -1189,18 +1197,7 @@ function canEncryptResponseContentType(contentType) {
1189
1197
  function isEmptyResponse(response) {
1190
1198
  if ([204, 304].includes(response.status))
1191
1199
  return true;
1192
- return response.headers.get("Content-Length") === "0";
1193
- }
1194
- function hasBody(method) {
1195
- return ["POST", "PUT", "PATCH", "DELETE"].includes(method.toUpperCase());
1196
- }
1197
- function isEmptyBody(req) {
1198
- if (req.body === null)
1199
- return true;
1200
- const cl = req.headers.get("Content-Length");
1201
- if (cl !== null)
1202
- return Number.parseInt(cl, 10) === 0;
1203
- return !req.headers.get("Content-Type");
1200
+ return response.body === null;
1204
1201
  }
1205
1202
  function jsonError(message, status) {
1206
1203
  return new Response(JSON.stringify({ error: message }), {
@@ -1208,12 +1205,6 @@ function jsonError(message, status) {
1208
1205
  headers: { "Content-Type": "application/json; charset=utf-8" }
1209
1206
  });
1210
1207
  }
1211
- function isEncryptedPayloadShape(value) {
1212
- if (!value || typeof value !== "object")
1213
- return false;
1214
- const p = value;
1215
- return typeof p.encryptedKey === "string" && typeof p.ciphertext === "string" && typeof p.iv === "string";
1216
- }
1217
1208
 
1218
1209
  // src/serv-app.ts
1219
1210
  var API_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
@@ -1221,6 +1212,11 @@ var audioWsServers = /* @__PURE__ */ new WeakMap();
1221
1212
  function createApp(options) {
1222
1213
  const http = resolveServHttpConfig(options.http);
1223
1214
  const app = new Hono();
1215
+ app.use("*", async (c, next) => {
1216
+ const requestId = resolveRequestId(c.req.header("x-request-id") ?? null);
1217
+ await core.context.run({ requestId, userId: void 0, scopeId: void 0, jobId: void 0 }, () => core.i18n.runWithLocale(resolveRequestLocale(c.req.raw.headers), next));
1218
+ c.header("x-request-id", requestId);
1219
+ });
1224
1220
  app.notFound((c) => c.json(buildHaiErrorBody(
1225
1221
  HaiCommonError.NOT_FOUND,
1226
1222
  core.i18n.coreM("core_errorNotFound", { locale: resolveRequestLocale(c.req.raw.headers) })
@@ -1240,8 +1236,9 @@ function createApp(options) {
1240
1236
  mountCustomMiddlewares(app, options.middlewares);
1241
1237
  if (options.transport) {
1242
1238
  const mgr = options.transport.crypto.transport.createServer({
1239
+ serverKeyRing: options.transport.serverKeyRing,
1243
1240
  keyStore: options.transport.keyStore,
1244
- maxClients: options.transport.maxClients
1241
+ maxPlaintextBytes: options.transport.maxPlaintextBytes
1245
1242
  });
1246
1243
  if (!mgr.success) {
1247
1244
  throw core.error.buildHaiErrorInst(
@@ -1323,7 +1320,7 @@ function mountRPCRoutes(app, procedures, rpcConfig, createContext) {
1323
1320
  }
1324
1321
  async function handleORPC(c, next, prefix, handler, createContext) {
1325
1322
  const context = await createContext({ request: c.req.raw, remoteAddress: readRemoteAddress(c.env) });
1326
- const { matched, response } = await handler.handle(c.req.raw, { prefix, context });
1323
+ const { matched, response } = await core.context.run({ requestId: context.requestId, userId: context.session?.userId, scopeId: context.scopeId }, () => handler.handle(c.req.raw, { prefix, context }));
1327
1324
  if (!matched) {
1328
1325
  await next();
1329
1326
  return;