@h-ai/serv 0.1.0-alpha.76 → 1.0.0-beta.10
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 +31 -18
- package/README.ja.md +31 -18
- package/README.md +31 -18
- package/dist/{chunk-JAQIEQJS.js → chunk-FEG22LCX.js} +14 -5
- package/dist/chunk-FEG22LCX.js.map +1 -0
- package/dist/{chunk-65XNI7TH.js → chunk-IQZQTAEW.js} +3 -3
- package/dist/{chunk-65XNI7TH.js.map → chunk-IQZQTAEW.js.map} +1 -1
- package/dist/features/ai.js +1 -1
- package/dist/features/feedback.js +1 -1
- package/dist/features/iam.d.ts +42 -44
- package/dist/features/iam.js +1 -9
- package/dist/features/iam.js.map +1 -1
- package/dist/features/storage.js +2 -2
- package/dist/index.d.ts +91 -55
- package/dist/index.js +166 -250
- package/dist/index.js.map +1 -1
- package/package.json +16 -16
- package/dist/chunk-JAQIEQJS.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
import { validateInputOrFail, resolveRequestLocale, localizeZodError } from './chunk-
|
|
2
|
-
import { servM, WILDCARD_ROLE, WILDCARD_PERMISSION, implement, buildHaiErrorBody } from './chunk-
|
|
1
|
+
import { validateInputOrFail, resolveRequestLocale, localizeZodError } from './chunk-IQZQTAEW.js';
|
|
2
|
+
import { servM, WILDCARD_ROLE, WILDCARD_PERMISSION, implement, buildHaiErrorBody } from './chunk-FEG22LCX.js';
|
|
3
3
|
import process from 'process';
|
|
4
4
|
import { upgradeWebSocket, serve } from '@hono/node-server';
|
|
5
|
+
import { posix } from 'path';
|
|
5
6
|
import { apiContract, AUDIO_WS_PATH, AudioWsClientMessageSchema } from '@h-ai/api-contract';
|
|
6
7
|
import { audit } from '@h-ai/audit';
|
|
7
|
-
import { core, HaiCommonError, err, ok } from '@h-ai/core';
|
|
8
|
+
import { core, RequestAccessConfigSchema, TrustedProxiesSchema, HaiCommonError, err, ok } from '@h-ai/core';
|
|
8
9
|
import { TRANSPORT_PLAINTEXT_RANGE, createTransportBodyPolicy, TRANSPORT_PROTOCOL, authenticateTransportRequest, TransportTimeError, createTransportErrorResponse, TransportBodyTooLargeError, readTransportBody, TRANSPORT_BODY_LIMITS, isValidSM2PublicKey } from '@h-ai/crypto';
|
|
9
10
|
import { OpenAPIHandler } from '@orpc/openapi/fetch';
|
|
10
|
-
import { RPCHandler } from '@orpc/server/fetch';
|
|
11
11
|
import { Hono } from 'hono';
|
|
12
12
|
import { WebSocketServer } from 'ws';
|
|
13
13
|
import { Buffer } from 'buffer';
|
|
14
|
-
import { BlockList, isIP } from 'net';
|
|
15
14
|
import { z } from 'zod';
|
|
16
15
|
import { getCookie } from 'hono/cookie';
|
|
17
16
|
import { readFile } from 'fs/promises';
|
|
@@ -549,134 +548,6 @@ function toArrayBuffer(data) {
|
|
|
549
548
|
new Uint8Array(buffer).set(data);
|
|
550
549
|
return buffer;
|
|
551
550
|
}
|
|
552
|
-
function readRemoteAddress(metadata) {
|
|
553
|
-
if (!metadata || typeof metadata !== "object" || !("remoteAddress" in metadata) || typeof metadata.remoteAddress !== "string")
|
|
554
|
-
return void 0;
|
|
555
|
-
return normalizeClientIp(metadata.remoteAddress);
|
|
556
|
-
}
|
|
557
|
-
function normalizeClientIp(value) {
|
|
558
|
-
if (!value)
|
|
559
|
-
return void 0;
|
|
560
|
-
const address = value.trim();
|
|
561
|
-
if (!isIP(address))
|
|
562
|
-
return void 0;
|
|
563
|
-
const mapped = address.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i);
|
|
564
|
-
return mapped?.[1] ?? address;
|
|
565
|
-
}
|
|
566
|
-
function addProxy(blockList, entry) {
|
|
567
|
-
const [address, prefix, extra] = entry.split("/");
|
|
568
|
-
const family = address ? isIP(address) : 0;
|
|
569
|
-
if (!family || extra !== void 0)
|
|
570
|
-
throw new Error("trustedProxies entries must be IP addresses or CIDRs");
|
|
571
|
-
const type = family === 4 ? "ipv4" : "ipv6";
|
|
572
|
-
if (prefix === void 0) {
|
|
573
|
-
blockList.addAddress(address, type);
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
const bits = Number(prefix);
|
|
577
|
-
if (!/^\d+$/.test(prefix) || bits < 0 || bits > (family === 4 ? 32 : 128))
|
|
578
|
-
throw new Error("Invalid trustedProxies CIDR prefix");
|
|
579
|
-
blockList.addSubnet(address, bits, type);
|
|
580
|
-
}
|
|
581
|
-
function isTrustedProxyEntry(entry) {
|
|
582
|
-
try {
|
|
583
|
-
addProxy(new BlockList(), entry);
|
|
584
|
-
return true;
|
|
585
|
-
} catch {
|
|
586
|
-
return false;
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
function createClientIpResolver(trustedProxies) {
|
|
590
|
-
const proxies = new BlockList();
|
|
591
|
-
for (const entry of trustedProxies)
|
|
592
|
-
addProxy(proxies, entry);
|
|
593
|
-
const trusted = (address) => proxies.check(address, isIP(address) === 4 ? "ipv4" : "ipv6");
|
|
594
|
-
return (request, remoteAddress) => {
|
|
595
|
-
const peer = normalizeClientIp(remoteAddress);
|
|
596
|
-
if (!peer || !trusted(peer))
|
|
597
|
-
return peer;
|
|
598
|
-
const forwarded = request.headers.get("x-forwarded-for");
|
|
599
|
-
if (forwarded === null) {
|
|
600
|
-
const realIp = request.headers.get("x-real-ip");
|
|
601
|
-
return realIp === null ? peer : normalizeClientIp(realIp);
|
|
602
|
-
}
|
|
603
|
-
const chain = forwarded.split(",");
|
|
604
|
-
if (chain.length > 32)
|
|
605
|
-
return void 0;
|
|
606
|
-
let current = peer;
|
|
607
|
-
for (let index = chain.length - 1; index >= 0 && trusted(current); index--) {
|
|
608
|
-
const address = normalizeClientIp(chain[index]);
|
|
609
|
-
if (!address)
|
|
610
|
-
return void 0;
|
|
611
|
-
current = address;
|
|
612
|
-
}
|
|
613
|
-
return current;
|
|
614
|
-
};
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// src/pipelines/serv-pipeline-require-internal-rpc.ts
|
|
618
|
-
var pipelineLogger = core.logger.child({ module: "serv", scope: "pipeline" });
|
|
619
|
-
var cachedGetConnInfo;
|
|
620
|
-
async function resolveGetConnInfo() {
|
|
621
|
-
if (cachedGetConnInfo !== void 0)
|
|
622
|
-
return cachedGetConnInfo;
|
|
623
|
-
try {
|
|
624
|
-
const mod = await import('@hono/node-server/conninfo');
|
|
625
|
-
cachedGetConnInfo = mod.getConnInfo;
|
|
626
|
-
} catch (cause) {
|
|
627
|
-
pipelineLogger.warn("getConnInfo unavailable in this runtime; loopback/private-network access modes will fail closed", { error: cause });
|
|
628
|
-
cachedGetConnInfo = null;
|
|
629
|
-
}
|
|
630
|
-
return cachedGetConnInfo;
|
|
631
|
-
}
|
|
632
|
-
var LOOPBACK_IPS = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1", "localhost"]);
|
|
633
|
-
function requireInternalRPC(config) {
|
|
634
|
-
return async (c, next) => {
|
|
635
|
-
const locale = resolveRequestLocale(c.req.raw.headers);
|
|
636
|
-
if (config.access === "gateway-only") {
|
|
637
|
-
const headerName = config.gatewayHeader ?? "x-hai-internal-rpc";
|
|
638
|
-
const received = c.req.header(headerName);
|
|
639
|
-
if (!config.gatewaySecret || !received || !core.string.constantTimeEqual(received, config.gatewaySecret))
|
|
640
|
-
return c.json(buildHaiErrorBody(HaiCommonError.FORBIDDEN, servM("serv_errorForbidden", { locale })), 403);
|
|
641
|
-
return next();
|
|
642
|
-
}
|
|
643
|
-
const ip = await getRequestRemoteAddress(c) ?? "";
|
|
644
|
-
if (config.access === "loopback" && !isLoopbackIP(ip))
|
|
645
|
-
return c.json(buildHaiErrorBody(HaiCommonError.FORBIDDEN, servM("serv_errorForbidden", { locale })), 403);
|
|
646
|
-
if (config.access === "private-network" && !isPrivateNetworkIP(ip))
|
|
647
|
-
return c.json(buildHaiErrorBody(HaiCommonError.FORBIDDEN, servM("serv_errorForbidden", { locale })), 403);
|
|
648
|
-
return next();
|
|
649
|
-
};
|
|
650
|
-
}
|
|
651
|
-
async function getRequestRemoteAddress(c) {
|
|
652
|
-
const remoteAddress = readRemoteAddress(c.env);
|
|
653
|
-
if (remoteAddress)
|
|
654
|
-
return remoteAddress;
|
|
655
|
-
const getConnInfo = await resolveGetConnInfo();
|
|
656
|
-
if (!getConnInfo)
|
|
657
|
-
return void 0;
|
|
658
|
-
try {
|
|
659
|
-
return getConnInfo(c).remote.address;
|
|
660
|
-
} catch {
|
|
661
|
-
return void 0;
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
function normalizeIpAddress(ip) {
|
|
665
|
-
return ip.startsWith("::ffff:") ? ip.slice("::ffff:".length) : ip;
|
|
666
|
-
}
|
|
667
|
-
function isLoopbackIP(ip) {
|
|
668
|
-
const normalized = normalizeIpAddress(ip);
|
|
669
|
-
return LOOPBACK_IPS.has(ip) || LOOPBACK_IPS.has(normalized);
|
|
670
|
-
}
|
|
671
|
-
function isPrivateNetworkIP(ip) {
|
|
672
|
-
const normalized = normalizeIpAddress(ip);
|
|
673
|
-
if (isLoopbackIP(ip))
|
|
674
|
-
return true;
|
|
675
|
-
if (normalized.startsWith("10.") || normalized.startsWith("192.168."))
|
|
676
|
-
return true;
|
|
677
|
-
const second = Number(normalized.split(".")[1]);
|
|
678
|
-
return normalized.startsWith("172.") && Number.isInteger(second) && second >= 16 && second <= 31;
|
|
679
|
-
}
|
|
680
551
|
|
|
681
552
|
// src/pipelines/serv-pipeline-security-headers.ts
|
|
682
553
|
function securityHeaders() {
|
|
@@ -687,9 +558,14 @@ function securityHeaders() {
|
|
|
687
558
|
await next();
|
|
688
559
|
};
|
|
689
560
|
}
|
|
561
|
+
function readRemoteAddress(metadata) {
|
|
562
|
+
if (!metadata || typeof metadata !== "object" || !("remoteAddress" in metadata) || typeof metadata.remoteAddress !== "string")
|
|
563
|
+
return void 0;
|
|
564
|
+
return core.access.normalizeClientIp(metadata.remoteAddress);
|
|
565
|
+
}
|
|
690
566
|
var ApiPrefixSchema = z.custom(
|
|
691
|
-
(value) => typeof value === "string" && value.
|
|
692
|
-
|
|
567
|
+
(value) => typeof value === "string" && /^\/[\w.-]+(?:\/[\w.-]+)*$/.test(value) && !value.split("/").some((segment) => segment === "." || segment === ".."),
|
|
568
|
+
{ error: () => servM("serv_invalidApiPrefix") }
|
|
693
569
|
);
|
|
694
570
|
var AbsolutePathSchema = z.custom(
|
|
695
571
|
(value) => typeof value === "string" && value.startsWith("/"),
|
|
@@ -713,15 +589,6 @@ var HealthHttpConfigInputSchema = z.union([
|
|
|
713
589
|
readyPath: AbsolutePathSchema.optional().default("/ready")
|
|
714
590
|
})
|
|
715
591
|
]);
|
|
716
|
-
var RpcHttpConfigInputSchema = z.union([
|
|
717
|
-
z.literal(false),
|
|
718
|
-
z.object({
|
|
719
|
-
prefix: AbsolutePathSchema.default("/rpc"),
|
|
720
|
-
access: z.enum(["loopback", "private-network", "gateway-only"]).default("loopback"),
|
|
721
|
-
gatewayHeader: z.string().optional(),
|
|
722
|
-
gatewaySecret: z.string().optional()
|
|
723
|
-
})
|
|
724
|
-
]);
|
|
725
592
|
var TransportRuntimeConfigInputSchema = z.union([
|
|
726
593
|
z.literal(false),
|
|
727
594
|
z.object({
|
|
@@ -743,12 +610,12 @@ var CorsRuntimeConfigInputSchema = z.object({
|
|
|
743
610
|
credentials: z.boolean().default(false)
|
|
744
611
|
});
|
|
745
612
|
var ServHttpConfigInputSchema = z.object({
|
|
746
|
-
trustedProxies:
|
|
613
|
+
trustedProxies: TrustedProxiesSchema.default("auto"),
|
|
614
|
+
access: RequestAccessConfigSchema.default(false),
|
|
747
615
|
apiPrefix: ApiPrefixSchema.default("/api/v1"),
|
|
748
616
|
openapi: OpenAPIHttpConfigInputSchema.default(false),
|
|
749
617
|
docs: DocsHttpConfigInputSchema.default(false),
|
|
750
|
-
health: HealthHttpConfigInputSchema.default({ path: "/health", readyPath: "/ready" })
|
|
751
|
-
rpc: RpcHttpConfigInputSchema.default(false)
|
|
618
|
+
health: HealthHttpConfigInputSchema.default({ path: "/health", readyPath: "/ready" })
|
|
752
619
|
});
|
|
753
620
|
var ServConfigSchema = z.object({
|
|
754
621
|
server: ServerConfigInputSchema.optional(),
|
|
@@ -803,7 +670,7 @@ function extractBearerToken(value) {
|
|
|
803
670
|
function parseRequestContext(input) {
|
|
804
671
|
const headers = input.request.headers;
|
|
805
672
|
const requestId = core.context.get().requestId ?? resolveRequestId(headers.get(HEADER_REQUEST_ID));
|
|
806
|
-
const ip = normalizeClientIp(Object.hasOwn(input, "ip") ? input.ip : input.remoteAddress);
|
|
673
|
+
const ip = core.access.normalizeClientIp(Object.hasOwn(input, "ip") ? input.ip : input.remoteAddress);
|
|
807
674
|
const locale = resolveRequestLocale(headers);
|
|
808
675
|
const accessToken = extractBearerToken(headers.get(HEADER_AUTHORIZATION));
|
|
809
676
|
const terminal = headers.get("x-hai-terminal");
|
|
@@ -999,6 +866,96 @@ async function readRefreshTokenFromBody(request) {
|
|
|
999
866
|
const body = await request.json().catch(() => void 0);
|
|
1000
867
|
return typeof body?.refreshToken === "string" && body.refreshToken.length > 0 ? body.refreshToken : void 0;
|
|
1001
868
|
}
|
|
869
|
+
|
|
870
|
+
// src/serv-cors.ts
|
|
871
|
+
var DEFAULT_CORS_METHODS = ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE", "OPTIONS"];
|
|
872
|
+
var DEFAULT_CORS_ALLOWED_HEADERS = ["Authorization", "Content-Type", "X-Requested-With", "X-Hai-Reauthentication", "X-Hai-Terminal"];
|
|
873
|
+
var DEFAULT_CORS_MAX_AGE = 86400;
|
|
874
|
+
var corsMiddlewares = /* @__PURE__ */ new WeakSet();
|
|
875
|
+
function isCorsMiddleware(middleware) {
|
|
876
|
+
return corsMiddlewares.has(middleware);
|
|
877
|
+
}
|
|
878
|
+
function cors(config = {}) {
|
|
879
|
+
const methods = [...config.methods ?? DEFAULT_CORS_METHODS];
|
|
880
|
+
const allowedHeaders = [...config.allowedHeaders ?? DEFAULT_CORS_ALLOWED_HEADERS];
|
|
881
|
+
const exposedHeaders = [.../* @__PURE__ */ new Set(["X-Request-Id", "X-Trace-Id", ...config.exposedHeaders ?? []])];
|
|
882
|
+
const credentials = config.credentials ?? false;
|
|
883
|
+
const maxAge = config.maxAge ?? DEFAULT_CORS_MAX_AGE;
|
|
884
|
+
const originConfig = config.origin ?? "*";
|
|
885
|
+
const middleware = async (c, next) => {
|
|
886
|
+
const allowedOrigin = resolveCorsOrigin(c.req.header("Origin"), originConfig);
|
|
887
|
+
if (c.req.method === "OPTIONS") {
|
|
888
|
+
return new Response(null, {
|
|
889
|
+
status: 204,
|
|
890
|
+
headers: buildCorsHeaders({
|
|
891
|
+
allowedOrigin,
|
|
892
|
+
allowedHeaders,
|
|
893
|
+
credentials,
|
|
894
|
+
exposedHeaders,
|
|
895
|
+
maxAge,
|
|
896
|
+
methods,
|
|
897
|
+
request: c.req.raw
|
|
898
|
+
})
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
await next();
|
|
902
|
+
const corsHeaders = buildCorsHeaders({
|
|
903
|
+
allowedOrigin,
|
|
904
|
+
allowedHeaders,
|
|
905
|
+
credentials,
|
|
906
|
+
exposedHeaders,
|
|
907
|
+
maxAge,
|
|
908
|
+
methods,
|
|
909
|
+
request: c.req.raw
|
|
910
|
+
});
|
|
911
|
+
for (const [key, value] of corsHeaders) {
|
|
912
|
+
if (key.toLowerCase() === "vary") {
|
|
913
|
+
c.res.headers.append(key, value);
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
c.res.headers.set(key, value);
|
|
917
|
+
}
|
|
918
|
+
return void 0;
|
|
919
|
+
};
|
|
920
|
+
corsMiddlewares.add(middleware);
|
|
921
|
+
return middleware;
|
|
922
|
+
}
|
|
923
|
+
function resolveCorsOrigin(origin, configOrigin) {
|
|
924
|
+
if (!origin)
|
|
925
|
+
return null;
|
|
926
|
+
if (configOrigin === "*")
|
|
927
|
+
return "*";
|
|
928
|
+
if (typeof configOrigin === "string")
|
|
929
|
+
return origin === configOrigin ? origin : null;
|
|
930
|
+
if (Array.isArray(configOrigin))
|
|
931
|
+
return configOrigin.includes(origin) ? origin : null;
|
|
932
|
+
if (typeof configOrigin === "function")
|
|
933
|
+
return configOrigin(origin) ? origin : null;
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
function buildCorsHeaders(options) {
|
|
937
|
+
const headers = new Headers();
|
|
938
|
+
if (!options.allowedOrigin)
|
|
939
|
+
return headers;
|
|
940
|
+
const requestedHeaders = options.request.headers.get("Access-Control-Request-Headers");
|
|
941
|
+
headers.set("Access-Control-Allow-Origin", options.allowedOrigin);
|
|
942
|
+
if (options.credentials)
|
|
943
|
+
headers.set("Access-Control-Allow-Credentials", "true");
|
|
944
|
+
if (options.allowedOrigin !== "*") {
|
|
945
|
+
headers.set(
|
|
946
|
+
"Vary",
|
|
947
|
+
requestedHeaders ? "Origin, Access-Control-Request-Headers" : "Origin"
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
if (options.exposedHeaders.length > 0)
|
|
951
|
+
headers.set("Access-Control-Expose-Headers", options.exposedHeaders.join(", "));
|
|
952
|
+
if (options.request.method === "OPTIONS") {
|
|
953
|
+
headers.set("Access-Control-Allow-Methods", options.methods.join(", "));
|
|
954
|
+
headers.set("Access-Control-Allow-Headers", requestedHeaders ?? options.allowedHeaders.join(", "));
|
|
955
|
+
headers.set("Access-Control-Max-Age", String(options.maxAge));
|
|
956
|
+
}
|
|
957
|
+
return headers;
|
|
958
|
+
}
|
|
1002
959
|
var SCALAR_ROUTE = "/_hai/scalar.js";
|
|
1003
960
|
var SCALAR_BROWSER_ENTRY_FALLBACK = "./dist/browser/standalone.js";
|
|
1004
961
|
var scalarScriptCache;
|
|
@@ -1063,12 +1020,20 @@ function readScalarBrowserEntry(packageInfo) {
|
|
|
1063
1020
|
}
|
|
1064
1021
|
var logger2 = core.logger.child({ module: "serv", scope: "transport" });
|
|
1065
1022
|
var ENCRYPTABLE_RESPONSE_CONTENT_TYPES = ["application/json"];
|
|
1066
|
-
function createTransportMiddleware(manager, keyExchangePath, excludePaths = [], policy = createTransportBodyPolicy(manager.bodyLimits.plaintextBytes)) {
|
|
1023
|
+
function createTransportMiddleware(manager, keyExchangePath, excludePaths = [], policy = createTransportBodyPolicy(manager.bodyLimits.plaintextBytes), encryption) {
|
|
1067
1024
|
return async (c, next) => {
|
|
1068
1025
|
const pathname = new URL(c.req.url).pathname;
|
|
1026
|
+
if (c.req.method === "OPTIONS")
|
|
1027
|
+
return next();
|
|
1069
1028
|
if (pathname === keyExchangePath && c.req.method === "POST")
|
|
1070
1029
|
return handleKeyExchange(c.req.raw, manager);
|
|
1071
|
-
|
|
1030
|
+
const mode = encryption?.(pathname, c.req.method);
|
|
1031
|
+
if (mode === "disabled") {
|
|
1032
|
+
if (c.req.raw.headers.has(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER) || c.req.raw.headers.has(TRANSPORT_PROTOCOL.ENVELOPE_HEADER))
|
|
1033
|
+
return Response.json(err({ ...HaiCommonError.VALIDATION_ERROR, message: servM("serv_transportPlaintextRequired"), traceId: core.context.get().traceId ?? core.context.get().requestId }), { status: 400 });
|
|
1034
|
+
return next();
|
|
1035
|
+
}
|
|
1036
|
+
if (mode !== "required" && shouldExclude(pathname, excludePaths, keyExchangePath))
|
|
1072
1037
|
return next();
|
|
1073
1038
|
const clientId = c.req.header(TRANSPORT_PROTOCOL.CLIENT_ID_HEADER);
|
|
1074
1039
|
if (!clientId)
|
|
@@ -1222,7 +1187,10 @@ var API_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
|
|
|
1222
1187
|
var audioWsServers = /* @__PURE__ */ new WeakMap();
|
|
1223
1188
|
function createApp(options) {
|
|
1224
1189
|
const http = resolveServHttpConfig(options.http);
|
|
1225
|
-
const resolveIp = createClientIpResolver(http.trustedProxies);
|
|
1190
|
+
const resolveIp = core.access.createClientIpResolver(http.trustedProxies);
|
|
1191
|
+
const checkAccess = core.access.createRequestCheck(http.access);
|
|
1192
|
+
if (checkAccess.requiresEncryption && !options.transport)
|
|
1193
|
+
throw new Error("Request access rules require transport encryption, but Serv transport is not configured");
|
|
1226
1194
|
const app = new Hono();
|
|
1227
1195
|
const requestAuditContexts = /* @__PURE__ */ new WeakMap();
|
|
1228
1196
|
app.onError((error, c) => {
|
|
@@ -1280,8 +1248,45 @@ function createApp(options) {
|
|
|
1280
1248
|
return cachedSpec;
|
|
1281
1249
|
};
|
|
1282
1250
|
app.use("*", securityHeaders());
|
|
1283
|
-
|
|
1284
|
-
|
|
1251
|
+
const accessDenied = (c) => {
|
|
1252
|
+
const ipAddress = resolveIp(c.req.raw, readRemoteAddress(c.env));
|
|
1253
|
+
const decision = checkAccess(c.req.path, c.req.method, ipAddress);
|
|
1254
|
+
if (decision.allowed) {
|
|
1255
|
+
if (!options.transport && c.req.method !== "OPTIONS" && (c.req.raw.headers.has(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER) || c.req.raw.headers.has(TRANSPORT_PROTOCOL.ENVELOPE_HEADER)) && checkAccess.encryption(c.req.path, c.req.method) === "disabled") {
|
|
1256
|
+
return c.json(buildHaiErrorBody(HaiCommonError.VALIDATION_ERROR, servM("serv_transportPlaintextRequired")), 400);
|
|
1257
|
+
}
|
|
1258
|
+
return void 0;
|
|
1259
|
+
}
|
|
1260
|
+
core.logger.info("Request access policy denied", { ruleId: decision.ruleId, reason: decision.reason, method: c.req.method, ipAddress });
|
|
1261
|
+
return c.json(buildHaiErrorBody(HaiCommonError.REQUEST_ACCESS_DENIED, core.i18n.coreM("core_requestAccessDenied")), 403);
|
|
1262
|
+
};
|
|
1263
|
+
const cors2 = options.middlewares?.filter((entry) => isCorsMiddleware(entry.middleware)) ?? [];
|
|
1264
|
+
const custom = options.middlewares?.filter((entry) => !isCorsMiddleware(entry.middleware)) ?? [];
|
|
1265
|
+
for (const entry of cors2) {
|
|
1266
|
+
if (http.access === false) {
|
|
1267
|
+
app.use(entry.path ?? "*", entry.middleware);
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
1270
|
+
app.use(entry.path ?? "*", async (c, next) => {
|
|
1271
|
+
if (c.req.method !== "OPTIONS")
|
|
1272
|
+
return entry.middleware(c, next);
|
|
1273
|
+
const rejected = accessDenied(c);
|
|
1274
|
+
const preflight = await entry.middleware(c, async () => {
|
|
1275
|
+
});
|
|
1276
|
+
if (!rejected)
|
|
1277
|
+
return preflight;
|
|
1278
|
+
if (preflight instanceof Response) {
|
|
1279
|
+
for (const [key, value] of preflight.headers)
|
|
1280
|
+
rejected.headers.set(key, value);
|
|
1281
|
+
rejected.headers.set("content-type", "application/json; charset=utf-8");
|
|
1282
|
+
}
|
|
1283
|
+
return rejected;
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
const keyExchangePath = options.transport ? `${http.apiPrefix}${options.transport.keyExchangePath ?? "/_hai/key-exchange"}` : void 0;
|
|
1287
|
+
if (http.access !== false && keyExchangePath) {
|
|
1288
|
+
app.use("*", async (c, next) => c.req.path === keyExchangePath && c.req.method === "POST" ? accessDenied(c) ?? next() : next());
|
|
1289
|
+
}
|
|
1285
1290
|
if (options.transport) {
|
|
1286
1291
|
const policy = createTransportBodyPolicy(options.transport.maxPlaintextBytes, options.transport.routePlaintextBytes);
|
|
1287
1292
|
const mgr = options.transport.crypto.transport.createServer({
|
|
@@ -1296,9 +1301,11 @@ function createApp(options) {
|
|
|
1296
1301
|
mgr.error
|
|
1297
1302
|
);
|
|
1298
1303
|
}
|
|
1299
|
-
|
|
1300
|
-
app.use("*", createTransportMiddleware(mgr.data, keyExchangePath, options.transport.excludePaths, policy));
|
|
1304
|
+
app.use("*", createTransportMiddleware(mgr.data, keyExchangePath, options.transport.excludePaths, policy, checkAccess.encryption));
|
|
1301
1305
|
}
|
|
1306
|
+
if (http.access !== false)
|
|
1307
|
+
app.use("*", async (c, next) => accessDenied(c) ?? next());
|
|
1308
|
+
mountCustomMiddlewares(app, custom);
|
|
1302
1309
|
if (http.health !== false)
|
|
1303
1310
|
mountHealthRoutes(app, http.health);
|
|
1304
1311
|
if (options.refreshCookie)
|
|
@@ -1321,14 +1328,13 @@ function createApp(options) {
|
|
|
1321
1328
|
audioWsServers.set(app, new WebSocketServer({ noServer: true }));
|
|
1322
1329
|
}
|
|
1323
1330
|
mountOpenAPIRoutes(app, options.procedures, http.apiPrefix, createContext);
|
|
1324
|
-
if (http.rpc !== false)
|
|
1325
|
-
mountRPCRoutes(app, options.procedures, http.rpc, createContext);
|
|
1326
1331
|
if (http.openapi !== false) {
|
|
1327
1332
|
app.get(http.openapi.path, async (c) => c.json(await getSpec()));
|
|
1328
1333
|
}
|
|
1329
1334
|
if (http.docs !== false) {
|
|
1330
1335
|
const docs = http.docs;
|
|
1331
|
-
|
|
1336
|
+
const docsDirectory = docs.path.slice(0, docs.path.lastIndexOf("/") + 1);
|
|
1337
|
+
app.get(`${docsDirectory}${SCALAR_ROUTE.slice(1)}`, async (c) => {
|
|
1332
1338
|
const content = await getScalarScript();
|
|
1333
1339
|
if (!content)
|
|
1334
1340
|
return c.notFound();
|
|
@@ -1342,8 +1348,8 @@ function createApp(options) {
|
|
|
1342
1348
|
if (!context.session)
|
|
1343
1349
|
return c.json(buildHaiErrorBody(HaiCommonError.UNAUTHORIZED, servM("serv_errorUnauthorized", { locale: resolveRequestLocale(c.req.raw.headers) })), 401);
|
|
1344
1350
|
}
|
|
1345
|
-
const specUrl = http.openapi === false ? void 0 : http.openapi.path;
|
|
1346
|
-
return c.html(createDocsPage(await getSpec(), { specUrl }));
|
|
1351
|
+
const specUrl = http.openapi === false ? void 0 : posix.relative(docsDirectory, http.openapi.path) || ".";
|
|
1352
|
+
return c.html(createDocsPage(await getSpec(), { specUrl, scriptUrl: SCALAR_ROUTE.slice(1) }));
|
|
1347
1353
|
});
|
|
1348
1354
|
}
|
|
1349
1355
|
return app;
|
|
@@ -1361,12 +1367,6 @@ function mountOpenAPIRoutes(app, procedures, apiPrefix, createContext) {
|
|
|
1361
1367
|
const handler = new OpenAPIHandler(procedures);
|
|
1362
1368
|
app.on([...API_METHODS], `${apiPrefix}/*`, (c, next) => handleORPC(c, next, apiPrefix, handler, createContext));
|
|
1363
1369
|
}
|
|
1364
|
-
function mountRPCRoutes(app, procedures, rpcConfig, createContext) {
|
|
1365
|
-
const handler = new RPCHandler(procedures);
|
|
1366
|
-
const prefix = rpcConfig.prefix;
|
|
1367
|
-
app.use(`${prefix}/*`, requireInternalRPC(rpcConfig));
|
|
1368
|
-
app.on([...API_METHODS], `${prefix}/*`, (c, next) => handleORPC(c, next, prefix, handler, createContext));
|
|
1369
|
-
}
|
|
1370
1370
|
async function handleORPC(c, next, prefix, handler, createContext) {
|
|
1371
1371
|
const context = await createContext({ request: c.req.raw, remoteAddress: readRemoteAddress(c.env) });
|
|
1372
1372
|
const { matched, response } = await core.context.run({ requestId: context.requestId, userId: context.session?.userId, scopeId: context.scopeId }, () => handler.handle(c.req.raw, { prefix, context }));
|
|
@@ -1478,90 +1478,6 @@ function listen(app, options = {}) {
|
|
|
1478
1478
|
return handle;
|
|
1479
1479
|
}
|
|
1480
1480
|
|
|
1481
|
-
// src/serv-cors.ts
|
|
1482
|
-
var DEFAULT_CORS_METHODS = ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE", "OPTIONS"];
|
|
1483
|
-
var DEFAULT_CORS_ALLOWED_HEADERS = ["Authorization", "Content-Type", "X-Requested-With", "X-Hai-Reauthentication", "X-Hai-Terminal"];
|
|
1484
|
-
var DEFAULT_CORS_MAX_AGE = 86400;
|
|
1485
|
-
function cors(config = {}) {
|
|
1486
|
-
const methods = [...config.methods ?? DEFAULT_CORS_METHODS];
|
|
1487
|
-
const allowedHeaders = [...config.allowedHeaders ?? DEFAULT_CORS_ALLOWED_HEADERS];
|
|
1488
|
-
const exposedHeaders = [.../* @__PURE__ */ new Set(["X-Request-Id", "X-Trace-Id", ...config.exposedHeaders ?? []])];
|
|
1489
|
-
const credentials = config.credentials ?? false;
|
|
1490
|
-
const maxAge = config.maxAge ?? DEFAULT_CORS_MAX_AGE;
|
|
1491
|
-
const originConfig = config.origin ?? "*";
|
|
1492
|
-
return async (c, next) => {
|
|
1493
|
-
const allowedOrigin = resolveCorsOrigin(c.req.header("Origin"), originConfig);
|
|
1494
|
-
if (c.req.method === "OPTIONS") {
|
|
1495
|
-
return new Response(null, {
|
|
1496
|
-
status: 204,
|
|
1497
|
-
headers: buildCorsHeaders({
|
|
1498
|
-
allowedOrigin,
|
|
1499
|
-
allowedHeaders,
|
|
1500
|
-
credentials,
|
|
1501
|
-
exposedHeaders,
|
|
1502
|
-
maxAge,
|
|
1503
|
-
methods,
|
|
1504
|
-
request: c.req.raw
|
|
1505
|
-
})
|
|
1506
|
-
});
|
|
1507
|
-
}
|
|
1508
|
-
await next();
|
|
1509
|
-
const corsHeaders = buildCorsHeaders({
|
|
1510
|
-
allowedOrigin,
|
|
1511
|
-
allowedHeaders,
|
|
1512
|
-
credentials,
|
|
1513
|
-
exposedHeaders,
|
|
1514
|
-
maxAge,
|
|
1515
|
-
methods,
|
|
1516
|
-
request: c.req.raw
|
|
1517
|
-
});
|
|
1518
|
-
for (const [key, value] of corsHeaders) {
|
|
1519
|
-
if (key.toLowerCase() === "vary") {
|
|
1520
|
-
c.res.headers.append(key, value);
|
|
1521
|
-
continue;
|
|
1522
|
-
}
|
|
1523
|
-
c.res.headers.set(key, value);
|
|
1524
|
-
}
|
|
1525
|
-
return void 0;
|
|
1526
|
-
};
|
|
1527
|
-
}
|
|
1528
|
-
function resolveCorsOrigin(origin, configOrigin) {
|
|
1529
|
-
if (!origin)
|
|
1530
|
-
return null;
|
|
1531
|
-
if (configOrigin === "*")
|
|
1532
|
-
return "*";
|
|
1533
|
-
if (typeof configOrigin === "string")
|
|
1534
|
-
return origin === configOrigin ? origin : null;
|
|
1535
|
-
if (Array.isArray(configOrigin))
|
|
1536
|
-
return configOrigin.includes(origin) ? origin : null;
|
|
1537
|
-
if (typeof configOrigin === "function")
|
|
1538
|
-
return configOrigin(origin) ? origin : null;
|
|
1539
|
-
return null;
|
|
1540
|
-
}
|
|
1541
|
-
function buildCorsHeaders(options) {
|
|
1542
|
-
const headers = new Headers();
|
|
1543
|
-
if (!options.allowedOrigin)
|
|
1544
|
-
return headers;
|
|
1545
|
-
const requestedHeaders = options.request.headers.get("Access-Control-Request-Headers");
|
|
1546
|
-
headers.set("Access-Control-Allow-Origin", options.allowedOrigin);
|
|
1547
|
-
if (options.credentials)
|
|
1548
|
-
headers.set("Access-Control-Allow-Credentials", "true");
|
|
1549
|
-
if (options.allowedOrigin !== "*") {
|
|
1550
|
-
headers.set(
|
|
1551
|
-
"Vary",
|
|
1552
|
-
requestedHeaders ? "Origin, Access-Control-Request-Headers" : "Origin"
|
|
1553
|
-
);
|
|
1554
|
-
}
|
|
1555
|
-
if (options.exposedHeaders.length > 0)
|
|
1556
|
-
headers.set("Access-Control-Expose-Headers", options.exposedHeaders.join(", "));
|
|
1557
|
-
if (options.request.method === "OPTIONS") {
|
|
1558
|
-
headers.set("Access-Control-Allow-Methods", options.methods.join(", "));
|
|
1559
|
-
headers.set("Access-Control-Allow-Headers", requestedHeaders ?? options.allowedHeaders.join(", "));
|
|
1560
|
-
headers.set("Access-Control-Max-Age", String(options.maxAge));
|
|
1561
|
-
}
|
|
1562
|
-
return headers;
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
1481
|
// src/serv-runtime-security.ts
|
|
1566
1482
|
function createRuntimeSecurityPolicy(input) {
|
|
1567
1483
|
const production = input.environment?.trim().toLowerCase() === "production";
|