@h-ai/serv 0.1.0-alpha.67 → 0.1.0-alpha.68
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 +53 -19
- package/README.ja.md +53 -19
- package/README.md +54 -20
- package/dist/chunk-BGWE4S6Z.js +37 -0
- package/dist/chunk-BGWE4S6Z.js.map +1 -0
- package/dist/{chunk-R354OMWL.js → chunk-TWHCU6HT.js} +48 -6
- package/dist/chunk-TWHCU6HT.js.map +1 -0
- package/dist/{chunk-27PCPE37.js → chunk-XKCLD42I.js} +2 -2
- package/dist/chunk-XKCLD42I.js.map +1 -0
- package/dist/features/ai.d.ts +139 -57
- package/dist/features/ai.js +57 -46
- package/dist/features/ai.js.map +1 -1
- package/dist/features/iam.d.ts +29 -4
- package/dist/features/iam.js +3 -3
- package/dist/features/iam.js.map +1 -1
- package/dist/features/storage.d.ts +94 -9
- package/dist/features/storage.js +48 -12
- package/dist/features/storage.js.map +1 -1
- package/dist/index.d.ts +587 -187
- package/dist/index.js +89 -111
- package/dist/index.js.map +1 -1
- package/dist/{serv-context-CWzyrsdq.d.ts → serv-context-C48WdBcd.d.ts} +127 -7
- package/dist/serv-router-BX8hgYxo.d.ts +322 -0
- package/package.json +14 -10
- package/dist/chunk-27PCPE37.js.map +0 -1
- package/dist/chunk-R354OMWL.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
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,
|
|
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';
|
|
@@ -14,13 +15,39 @@ import { getCookie } from 'hono/cookie';
|
|
|
14
15
|
import { readFile } from 'fs/promises';
|
|
15
16
|
import { OpenAPIGenerator } from '@orpc/openapi';
|
|
16
17
|
import { ZodToJsonSchemaConverter } from '@orpc/zod/zod4';
|
|
17
|
-
import { TRANSPORT_PROTOCOL } from '@h-ai/crypto';
|
|
18
|
+
import { TRANSPORT_PROTOCOL, authenticateTransportRequest, readTransportBody, TRANSPORT_BODY_LIMITS, isValidSM2PublicKey } 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 =
|
|
23
|
-
|
|
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" });
|
|
@@ -728,8 +725,7 @@ var TransportRuntimeConfigInputSchema = z.union([
|
|
|
728
725
|
z.literal(false),
|
|
729
726
|
z.object({
|
|
730
727
|
keyExchangePath: AbsolutePathSchema.default("/_hai/key-exchange"),
|
|
731
|
-
excludePaths: z.array(AbsolutePathSchema).default([])
|
|
732
|
-
maxClients: z.number().int().positive().default(1e4)
|
|
728
|
+
excludePaths: z.array(AbsolutePathSchema).default([])
|
|
733
729
|
})
|
|
734
730
|
]);
|
|
735
731
|
var ServerConfigInputSchema = z.object({
|
|
@@ -848,7 +844,7 @@ function resolveRefreshCookieConfig(apiPrefix, config, iam) {
|
|
|
848
844
|
return {
|
|
849
845
|
cookieName: config.cookieName ?? "hai_refresh_token",
|
|
850
846
|
maxAge: config.maxAge ?? 30 * 24 * 3600,
|
|
851
|
-
isSecure: config.secure ?? process.env.NODE_ENV === "production",
|
|
847
|
+
isSecure: config.secure ?? (process.env.NODE_ENV === "production" || process.env.HAI_CORE_ENV === "production"),
|
|
852
848
|
refreshCookiePath: `${apiPrefix}${IAM_AUTH_PATHS.refresh}`,
|
|
853
849
|
onRefresh,
|
|
854
850
|
nativeTokenTransport: config.nativeTokenTransport
|
|
@@ -1058,7 +1054,6 @@ function readScalarBrowserEntry(packageInfo) {
|
|
|
1058
1054
|
return typeof browser === "string" ? browser : void 0;
|
|
1059
1055
|
}
|
|
1060
1056
|
var logger2 = core.logger.child({ module: "serv", scope: "transport" });
|
|
1061
|
-
var MAX_ENCRYPTED_BODY = 1048576;
|
|
1062
1057
|
var ENCRYPTABLE_RESPONSE_CONTENT_TYPES = ["application/json"];
|
|
1063
1058
|
function createTransportMiddleware(manager, keyExchangePath, excludePaths = []) {
|
|
1064
1059
|
return async (c, next) => {
|
|
@@ -1070,55 +1065,62 @@ function createTransportMiddleware(manager, keyExchangePath, excludePaths = [])
|
|
|
1070
1065
|
const clientId = c.req.header(TRANSPORT_PROTOCOL.CLIENT_ID_HEADER);
|
|
1071
1066
|
if (!clientId)
|
|
1072
1067
|
return jsonError(servM("serv_transportClientIdRequired"), 400);
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1068
|
+
let requestContext;
|
|
1069
|
+
try {
|
|
1070
|
+
const verified = await authenticateTransportRequest(manager, c.req.raw);
|
|
1071
|
+
if (!verified.success)
|
|
1072
|
+
return jsonError(servM("serv_transportDecryptFailed"), 400);
|
|
1073
|
+
requestContext = verified.data.context;
|
|
1074
|
+
if (c.req.method !== "GET" && c.req.method !== "HEAD")
|
|
1075
|
+
replaceRequestBody(c, verified.data.plaintext);
|
|
1076
|
+
} catch (error) {
|
|
1077
|
+
return jsonError(servM("serv_transportInvalidPayload"), error instanceof RangeError ? 413 : 400);
|
|
1080
1078
|
}
|
|
1081
1079
|
await next();
|
|
1082
1080
|
const response = c.res;
|
|
1083
1081
|
if (!response)
|
|
1084
1082
|
return;
|
|
1085
1083
|
const contentType = response.headers.get("Content-Type") ?? "";
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1084
|
+
const bodyless = c.req.method === "HEAD" || isEmptyResponse(response);
|
|
1085
|
+
if (bodyless)
|
|
1086
|
+
void response.body?.cancel().catch(() => {
|
|
1087
|
+
});
|
|
1088
|
+
c.header("Content-Length", void 0);
|
|
1089
|
+
c.header("Content-Encoding", void 0);
|
|
1090
|
+
c.header(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER, void 0);
|
|
1091
|
+
c.header(TRANSPORT_PROTOCOL.ENVELOPE_HEADER, void 0);
|
|
1092
|
+
if (!bodyless && !canEncryptResponseContentType(contentType)) {
|
|
1093
|
+
void response.body?.cancel().catch(() => {
|
|
1094
|
+
});
|
|
1094
1095
|
c.res = jsonError(servM("serv_transportEncryptFailed"), 500);
|
|
1095
1096
|
return;
|
|
1096
1097
|
}
|
|
1097
1098
|
let bodyText;
|
|
1098
1099
|
try {
|
|
1099
|
-
bodyText = await response.
|
|
1100
|
+
bodyText = bodyless ? "" : await readTransportBody(response.body, {
|
|
1101
|
+
maxBytes: TRANSPORT_BODY_LIMITS.plaintextBytes,
|
|
1102
|
+
signal: c.req.raw.signal
|
|
1103
|
+
});
|
|
1100
1104
|
} catch (cause) {
|
|
1101
1105
|
logger2.warn("Failed to read response body for encryption", { error: cause });
|
|
1102
1106
|
c.res = jsonError(servM("serv_transportEncryptFailed"), 500);
|
|
1103
1107
|
return;
|
|
1104
1108
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
}
|
|
1111
|
-
const encResult = await manager.encryptResponse(clientId, bodyText);
|
|
1109
|
+
const headers = new Headers(response.headers);
|
|
1110
|
+
headers.delete(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER);
|
|
1111
|
+
headers.delete("Content-Length");
|
|
1112
|
+
headers.delete("Content-Encoding");
|
|
1113
|
+
const encResult = await manager.encryptResponse(requestContext, bodyText, response.status);
|
|
1112
1114
|
if (!encResult.success) {
|
|
1113
1115
|
logger2.warn("Failed to encrypt response", { error: encResult.error });
|
|
1114
1116
|
c.res = jsonError(servM("serv_transportEncryptFailed"), 500);
|
|
1115
1117
|
return;
|
|
1116
1118
|
}
|
|
1117
|
-
const headers = new Headers(response.headers);
|
|
1118
1119
|
headers.set("Content-Type", "application/json; charset=utf-8");
|
|
1119
1120
|
headers.set(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER, TRANSPORT_PROTOCOL.ENCRYPTED_HEADER_VALUE);
|
|
1120
|
-
|
|
1121
|
-
|
|
1121
|
+
if (bodyless)
|
|
1122
|
+
headers.set(TRANSPORT_PROTOCOL.ENVELOPE_HEADER, JSON.stringify(encResult.data));
|
|
1123
|
+
c.res = new Response(bodyless ? null : JSON.stringify(encResult.data), {
|
|
1122
1124
|
status: response.status,
|
|
1123
1125
|
statusText: response.statusText,
|
|
1124
1126
|
headers
|
|
@@ -1128,16 +1130,22 @@ function createTransportMiddleware(manager, keyExchangePath, excludePaths = [])
|
|
|
1128
1130
|
async function handleKeyExchange(request, manager) {
|
|
1129
1131
|
let body;
|
|
1130
1132
|
try {
|
|
1131
|
-
body = await request.
|
|
1132
|
-
|
|
1133
|
-
|
|
1133
|
+
body = JSON.parse(await readTransportBody(request.body, {
|
|
1134
|
+
maxBytes: TRANSPORT_BODY_LIMITS.keyExchangeBytes,
|
|
1135
|
+
signal: request.signal
|
|
1136
|
+
}));
|
|
1137
|
+
} catch (error) {
|
|
1138
|
+
return jsonError(servM("serv_transportInvalidPayload"), error instanceof RangeError ? 413 : 400);
|
|
1134
1139
|
}
|
|
1135
|
-
if (typeof body
|
|
1140
|
+
if (!body || typeof body !== "object" || !("clientPublicKey" in body) || !isValidSM2PublicKey(body.clientPublicKey) || new TextEncoder().encode(body.clientPublicKey).byteLength > TRANSPORT_BODY_LIMITS.publicKeyBytes) {
|
|
1136
1141
|
return jsonError(servM("serv_transportInvalidPayload"), 400);
|
|
1142
|
+
}
|
|
1137
1143
|
try {
|
|
1138
|
-
const clientId = await manager.registerClientKey(body.clientPublicKey);
|
|
1139
1144
|
const serverPublicKey = manager.getServerPublicKey();
|
|
1140
|
-
|
|
1145
|
+
if (new TextEncoder().encode(serverPublicKey).byteLength > TRANSPORT_BODY_LIMITS.publicKeyBytes)
|
|
1146
|
+
return jsonError(servM("serv_transportKeyExchangeFailed"), 500);
|
|
1147
|
+
const clientId = await manager.registerClientKey(body.clientPublicKey);
|
|
1148
|
+
return new Response(JSON.stringify({ serverKeyId: manager.getServerKeyId(), serverPublicKey, clientId }), {
|
|
1141
1149
|
status: 200,
|
|
1142
1150
|
headers: { "Content-Type": "application/json; charset=utf-8" }
|
|
1143
1151
|
});
|
|
@@ -1146,37 +1154,23 @@ async function handleKeyExchange(request, manager) {
|
|
|
1146
1154
|
return jsonError(servM("serv_transportKeyExchangeFailed"), 500);
|
|
1147
1155
|
}
|
|
1148
1156
|
}
|
|
1149
|
-
|
|
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
|
-
}
|
|
1157
|
+
function replaceRequestBody(c, body) {
|
|
1166
1158
|
const original = c.req.raw;
|
|
1167
1159
|
const newHeaders = new Headers(original.headers);
|
|
1168
1160
|
newHeaders.delete(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER);
|
|
1169
1161
|
newHeaders.delete("Content-Length");
|
|
1162
|
+
newHeaders.delete("Content-Encoding");
|
|
1170
1163
|
newHeaders.set("Content-Type", "application/json; charset=utf-8");
|
|
1171
1164
|
const newRequest = new Request(original.url, {
|
|
1172
1165
|
method: original.method,
|
|
1173
1166
|
headers: newHeaders,
|
|
1174
|
-
body
|
|
1167
|
+
body,
|
|
1168
|
+
signal: original.signal,
|
|
1175
1169
|
// @ts-expect-error Node fetch 需要 duplex 才能携带流式 body。
|
|
1176
1170
|
duplex: "half"
|
|
1177
1171
|
});
|
|
1178
1172
|
c.req.raw = newRequest;
|
|
1179
|
-
|
|
1173
|
+
c.req.bodyCache = {};
|
|
1180
1174
|
}
|
|
1181
1175
|
function shouldExclude(pathname, excludePaths, keyExchangePath) {
|
|
1182
1176
|
if (pathname === keyExchangePath)
|
|
@@ -1189,18 +1183,7 @@ function canEncryptResponseContentType(contentType) {
|
|
|
1189
1183
|
function isEmptyResponse(response) {
|
|
1190
1184
|
if ([204, 304].includes(response.status))
|
|
1191
1185
|
return true;
|
|
1192
|
-
return response.
|
|
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");
|
|
1186
|
+
return response.body === null;
|
|
1204
1187
|
}
|
|
1205
1188
|
function jsonError(message, status) {
|
|
1206
1189
|
return new Response(JSON.stringify({ error: message }), {
|
|
@@ -1208,12 +1191,6 @@ function jsonError(message, status) {
|
|
|
1208
1191
|
headers: { "Content-Type": "application/json; charset=utf-8" }
|
|
1209
1192
|
});
|
|
1210
1193
|
}
|
|
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
1194
|
|
|
1218
1195
|
// src/serv-app.ts
|
|
1219
1196
|
var API_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
|
|
@@ -1221,6 +1198,7 @@ var audioWsServers = /* @__PURE__ */ new WeakMap();
|
|
|
1221
1198
|
function createApp(options) {
|
|
1222
1199
|
const http = resolveServHttpConfig(options.http);
|
|
1223
1200
|
const app = new Hono();
|
|
1201
|
+
app.use("*", (c, next) => core.i18n.runWithLocale(resolveRequestLocale(c.req.raw.headers), next));
|
|
1224
1202
|
app.notFound((c) => c.json(buildHaiErrorBody(
|
|
1225
1203
|
HaiCommonError.NOT_FOUND,
|
|
1226
1204
|
core.i18n.coreM("core_errorNotFound", { locale: resolveRequestLocale(c.req.raw.headers) })
|
|
@@ -1240,8 +1218,8 @@ function createApp(options) {
|
|
|
1240
1218
|
mountCustomMiddlewares(app, options.middlewares);
|
|
1241
1219
|
if (options.transport) {
|
|
1242
1220
|
const mgr = options.transport.crypto.transport.createServer({
|
|
1243
|
-
|
|
1244
|
-
|
|
1221
|
+
serverKeyRing: options.transport.serverKeyRing,
|
|
1222
|
+
keyStore: options.transport.keyStore
|
|
1245
1223
|
});
|
|
1246
1224
|
if (!mgr.success) {
|
|
1247
1225
|
throw core.error.buildHaiErrorInst(
|