@h-ai/serv 1.0.0-beta.1 → 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/dist/index.d.ts CHANGED
@@ -138,7 +138,7 @@ type ServProcedureHandler<TInput, TOutput, TContext extends ServContext = ServCo
138
138
  /**
139
139
  * @h-ai/serv — HTTP 配置
140
140
  *
141
- * 提供用于控制 API 前缀、OpenAPI、docs、健康检查、内部 RPC 与传输加密策略的配置结构,
141
+ * 提供用于控制 API 前缀、OpenAPI、docs、健康检查与传输加密策略的配置结构,
142
142
  * 并由 zod 负责默认值填充与边界校验。
143
143
  * @module serv-config
144
144
  */
@@ -217,24 +217,6 @@ interface ServHealthHttpConfig {
217
217
  readonly path: `/${string}`;
218
218
  readonly readyPath?: `/${string}`;
219
219
  }
220
- /**
221
- * 内部 RPC endpoint 配置。
222
- *
223
- * @example
224
- * ```ts
225
- * import type { ServRpcHttpConfig } from '@h-ai/serv'
226
- * import { serv } from '@h-ai/serv'
227
- *
228
- * const rpc: ServRpcHttpConfig = { prefix: '/rpc', access: 'loopback' }
229
- * // 缺少可信连接信息时拒绝;跨节点来源通过明确网关/网络策略控制。
230
- * ```
231
- */
232
- interface ServRpcHttpConfig {
233
- readonly prefix: `/${string}`;
234
- readonly access: 'loopback' | 'private-network' | 'gateway-only';
235
- readonly gatewayHeader?: string;
236
- readonly gatewaySecret?: string;
237
- }
238
220
  /**
239
221
  * 传输加密运行时配置(对应 `config/_serv.yml`)。
240
222
  *
@@ -281,7 +263,7 @@ interface ServCorsRuntimeConfig {
281
263
  * import type { ServHttpConfig } from '@h-ai/serv'
282
264
  * import { serv } from '@h-ai/serv'
283
265
  *
284
- * const http: ServHttpConfig = { trustedProxies: 'auto', access: false, apiPrefix: '/api/v1', openapi: false, docs: false, health: { path: '/health' }, rpc: false }
266
+ * const http: ServHttpConfig = { trustedProxies: 'auto', access: false, apiPrefix: '/api/v1', openapi: false, docs: false, health: { path: '/health' } }
285
267
  * ```
286
268
  */
287
269
  interface ServHttpConfig {
@@ -289,11 +271,10 @@ interface ServHttpConfig {
289
271
  readonly trustedProxies: TrustedProxies;
290
272
  /** 每次请求的 IP/时间策略;默认 false,内联与独立文件二选一。 */
291
273
  readonly access: RequestAccessConfigInput;
292
- readonly apiPrefix: `/api/${string}`;
274
+ readonly apiPrefix: `/${string}`;
293
275
  readonly openapi: false | ServOpenAPIHttpConfig;
294
276
  readonly docs: false | ServDocsHttpConfig;
295
277
  readonly health: false | ServHealthHttpConfig;
296
- readonly rpc: false | ServRpcHttpConfig;
297
278
  }
298
279
  /**
299
280
  * `config/_serv.yml` 的顶层配置结构。
@@ -359,11 +340,10 @@ type ServTransportRuntimeConfigInput = Partial<{
359
340
  type ServHttpConfigInput = Partial<{
360
341
  readonly trustedProxies: TrustedProxies;
361
342
  readonly access: RequestAccessConfigInput;
362
- readonly apiPrefix: `/api/${string}`;
343
+ readonly apiPrefix: `/${string}`;
363
344
  readonly openapi: false | Partial<ServOpenAPIHttpConfig>;
364
345
  readonly docs: false | Partial<ServDocsHttpConfig>;
365
346
  readonly health: false | Partial<ServHealthHttpConfig>;
366
- readonly rpc: false | Partial<ServRpcHttpConfig>;
367
347
  }>;
368
348
  /**
369
349
  * `config/_serv.yml` 的顶层输入类型。
@@ -447,7 +427,7 @@ declare const ServConfigSchema: z.ZodPipe<z.ZodObject<{
447
427
  }>>;
448
428
  }, z.core.$strict>>>;
449
429
  }, z.core.$strict>]>>;
450
- apiPrefix: z.ZodDefault<z.ZodCustom<`/api/${string}`, `/api/${string}`>>;
430
+ apiPrefix: z.ZodDefault<z.ZodCustom<`/${string}`, `/${string}`>>;
451
431
  openapi: z.ZodDefault<z.ZodUnion<readonly [z.ZodLiteral<false>, z.ZodObject<{
452
432
  path: z.ZodDefault<z.ZodCustom<`/${string}`, `/${string}`>>;
453
433
  }, z.core.$strip>]>>;
@@ -459,16 +439,6 @@ declare const ServConfigSchema: z.ZodPipe<z.ZodObject<{
459
439
  path: z.ZodDefault<z.ZodCustom<`/${string}`, `/${string}`>>;
460
440
  readyPath: z.ZodDefault<z.ZodOptional<z.ZodCustom<`/${string}`, `/${string}`>>>;
461
441
  }, z.core.$strip>]>>;
462
- rpc: z.ZodDefault<z.ZodUnion<readonly [z.ZodLiteral<false>, z.ZodObject<{
463
- prefix: z.ZodDefault<z.ZodCustom<`/${string}`, `/${string}`>>;
464
- access: z.ZodDefault<z.ZodEnum<{
465
- loopback: "loopback";
466
- "private-network": "private-network";
467
- "gateway-only": "gateway-only";
468
- }>>;
469
- gatewayHeader: z.ZodOptional<z.ZodString>;
470
- gatewaySecret: z.ZodOptional<z.ZodString>;
471
- }, z.core.$strip>]>>;
472
442
  }, z.core.$strip>>;
473
443
  cors: z.ZodOptional<z.ZodObject<{
474
444
  origin: z.ZodOptional<z.ZodString>;
@@ -523,7 +493,7 @@ declare const ServConfigSchema: z.ZodPipe<z.ZodObject<{
523
493
  }[];
524
494
  } | undefined;
525
495
  };
526
- apiPrefix: `/api/${string}`;
496
+ apiPrefix: `/${string}`;
527
497
  openapi: false | {
528
498
  path: `/${string}`;
529
499
  };
@@ -535,12 +505,6 @@ declare const ServConfigSchema: z.ZodPipe<z.ZodObject<{
535
505
  path: `/${string}`;
536
506
  readyPath: `/${string}`;
537
507
  };
538
- rpc: false | {
539
- prefix: `/${string}`;
540
- access: "loopback" | "private-network" | "gateway-only";
541
- gatewayHeader?: string | undefined;
542
- gatewaySecret?: string | undefined;
543
- };
544
508
  } | undefined;
545
509
  cors?: {
546
510
  allowedHeaders: string[];
@@ -1368,7 +1332,7 @@ declare const serv: {
1368
1332
  validateInputOrFail: typeof validateInputOrFail;
1369
1333
  WILDCARD_PERMISSION: string;
1370
1334
  WILDCARD_ROLE: string;
1371
- m: (key: "serv_transportPlaintextRequired" | "$schema" | "serv_validationFailed" | "serv_validationRequired" | "serv_validationInvalid" | "serv_validationInvalidType" | "serv_validationUnexpectedFields" | "serv_validationStringMin" | "serv_validationStringMax" | "serv_validationNumberMin" | "serv_validationNumberMax" | "serv_validationArrayMin" | "serv_validationArrayMax" | "serv_validationTooSmall" | "serv_validationTooBig" | "serv_validationEmail" | "serv_validationUrl" | "serv_validationUuid" | "serv_validationEnum" | "serv_errorUnauthorized" | "serv_errorForbidden" | "serv_errorInternal" | "serv_storageKeyRequired" | "serv_storageKeyTooLong" | "serv_storageKeyIllegalChars" | "serv_storageKeyDotSegments" | "serv_iamCannotDeleteCurrentUser" | "serv_transportInvalidPayload" | "serv_transportUnknownClient" | "serv_transportClientIdRequired" | "serv_transportClientKeyNotFound" | "serv_transportDecryptFailed" | "serv_transportEncryptFailed" | "serv_transportKeyExchangeFailed" | "serv_transportBusy" | "serv_feedbackUnavailable", options?: _h_ai_core.MessageOptions) => string;
1335
+ m: (key: "serv_transportPlaintextRequired" | "$schema" | "serv_validationFailed" | "serv_validationRequired" | "serv_validationInvalid" | "serv_validationInvalidType" | "serv_validationUnexpectedFields" | "serv_validationStringMin" | "serv_validationStringMax" | "serv_validationNumberMin" | "serv_validationNumberMax" | "serv_validationArrayMin" | "serv_validationArrayMax" | "serv_validationTooSmall" | "serv_validationTooBig" | "serv_validationEmail" | "serv_validationUrl" | "serv_validationUuid" | "serv_validationEnum" | "serv_errorUnauthorized" | "serv_errorForbidden" | "serv_errorInternal" | "serv_storageKeyRequired" | "serv_storageKeyTooLong" | "serv_storageKeyIllegalChars" | "serv_storageKeyDotSegments" | "serv_iamCannotDeleteCurrentUser" | "serv_transportInvalidPayload" | "serv_transportUnknownClient" | "serv_transportClientIdRequired" | "serv_transportClientKeyNotFound" | "serv_transportDecryptFailed" | "serv_transportEncryptFailed" | "serv_transportKeyExchangeFailed" | "serv_transportBusy" | "serv_feedbackUnavailable" | "serv_invalidApiPrefix", options?: _h_ai_core.MessageOptions) => string;
1372
1336
  };
1373
1337
 
1374
- export { type AudioTicketGrant, type AudioTicketVerification, type AuthorizedAudioRequest, type CreateServAppOptions, CreateServContext, type GenerateOpenAPISpecOptions, RefreshCookieConfig, type ServAudioConfig, type ServConfig, type ServConfigInput, ServConfigSchema, ServContext, type ServCorsConfig, type ServCorsRuntimeConfig, type ServCorsRuntimeConfigInput, type ServDocsHttpConfig, type ServFetchHandler, type ServHealthHttpConfig, type ServHttpApp, type ServHttpConfig, type ServHttpConfigInput, ServIam, type ServListenOptions, type ServMiddleware, type ServMiddlewareFactory, type ServMiddlewareMount, type ServNodeServer, type ServOpenAPIHttpConfig, type ServProcedureHandler, type ServProcedureOptions, ServRequestMetadata, type ServRpcHttpConfig, type ServRuntimeSecurityPolicy, type ServRuntimeSecurityPolicyInput, type ServServerConfig, type ServServerConfigInput, ServSession, type ServStorageAssetsConfig, type ServTransportConfig, type ServTransportRuntimeConfig, type ServTransportRuntimeConfigInput, type ServValidationFailureBody, serv };
1338
+ export { type AudioTicketGrant, type AudioTicketVerification, type AuthorizedAudioRequest, type CreateServAppOptions, CreateServContext, type GenerateOpenAPISpecOptions, RefreshCookieConfig, type ServAudioConfig, type ServConfig, type ServConfigInput, ServConfigSchema, ServContext, type ServCorsConfig, type ServCorsRuntimeConfig, type ServCorsRuntimeConfigInput, type ServDocsHttpConfig, type ServFetchHandler, type ServHealthHttpConfig, type ServHttpApp, type ServHttpConfig, type ServHttpConfigInput, ServIam, type ServListenOptions, type ServMiddleware, type ServMiddlewareFactory, type ServMiddlewareMount, type ServNodeServer, type ServOpenAPIHttpConfig, type ServProcedureHandler, type ServProcedureOptions, ServRequestMetadata, type ServRuntimeSecurityPolicy, type ServRuntimeSecurityPolicyInput, type ServServerConfig, type ServServerConfigInput, ServSession, type ServStorageAssetsConfig, type ServTransportConfig, type ServTransportRuntimeConfig, type ServTransportRuntimeConfigInput, type ServValidationFailureBody, serv };
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
- import { validateInputOrFail, resolveRequestLocale, localizeZodError } from './chunk-TWMOZPGU.js';
2
- import { servM, WILDCARD_ROLE, WILDCARD_PERMISSION, implement, buildHaiErrorBody } from './chunk-I3F7RN4I.js';
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
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';
@@ -548,75 +548,6 @@ function toArrayBuffer(data) {
548
548
  new Uint8Array(buffer).set(data);
549
549
  return buffer;
550
550
  }
551
- function readRemoteAddress(metadata) {
552
- if (!metadata || typeof metadata !== "object" || !("remoteAddress" in metadata) || typeof metadata.remoteAddress !== "string")
553
- return void 0;
554
- return core.access.normalizeClientIp(metadata.remoteAddress);
555
- }
556
-
557
- // src/pipelines/serv-pipeline-require-internal-rpc.ts
558
- var pipelineLogger = core.logger.child({ module: "serv", scope: "pipeline" });
559
- var cachedGetConnInfo;
560
- async function resolveGetConnInfo() {
561
- if (cachedGetConnInfo !== void 0)
562
- return cachedGetConnInfo;
563
- try {
564
- const mod = await import('@hono/node-server/conninfo');
565
- cachedGetConnInfo = mod.getConnInfo;
566
- } catch (cause) {
567
- pipelineLogger.warn("getConnInfo unavailable in this runtime; loopback/private-network access modes will fail closed", { error: cause });
568
- cachedGetConnInfo = null;
569
- }
570
- return cachedGetConnInfo;
571
- }
572
- var LOOPBACK_IPS = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1", "localhost"]);
573
- function requireInternalRPC(config) {
574
- return async (c, next) => {
575
- const locale = resolveRequestLocale(c.req.raw.headers);
576
- if (config.access === "gateway-only") {
577
- const headerName = config.gatewayHeader ?? "x-hai-internal-rpc";
578
- const received = c.req.header(headerName);
579
- if (!config.gatewaySecret || !received || !core.string.constantTimeEqual(received, config.gatewaySecret))
580
- return c.json(buildHaiErrorBody(HaiCommonError.FORBIDDEN, servM("serv_errorForbidden", { locale })), 403);
581
- return next();
582
- }
583
- const ip = await getRequestRemoteAddress(c) ?? "";
584
- if (config.access === "loopback" && !isLoopbackIP(ip))
585
- return c.json(buildHaiErrorBody(HaiCommonError.FORBIDDEN, servM("serv_errorForbidden", { locale })), 403);
586
- if (config.access === "private-network" && !isPrivateNetworkIP(ip))
587
- return c.json(buildHaiErrorBody(HaiCommonError.FORBIDDEN, servM("serv_errorForbidden", { locale })), 403);
588
- return next();
589
- };
590
- }
591
- async function getRequestRemoteAddress(c) {
592
- const remoteAddress = readRemoteAddress(c.env);
593
- if (remoteAddress)
594
- return remoteAddress;
595
- const getConnInfo = await resolveGetConnInfo();
596
- if (!getConnInfo)
597
- return void 0;
598
- try {
599
- return getConnInfo(c).remote.address;
600
- } catch {
601
- return void 0;
602
- }
603
- }
604
- function normalizeIpAddress(ip) {
605
- return ip.startsWith("::ffff:") ? ip.slice("::ffff:".length) : ip;
606
- }
607
- function isLoopbackIP(ip) {
608
- const normalized = normalizeIpAddress(ip);
609
- return LOOPBACK_IPS.has(ip) || LOOPBACK_IPS.has(normalized);
610
- }
611
- function isPrivateNetworkIP(ip) {
612
- const normalized = normalizeIpAddress(ip);
613
- if (isLoopbackIP(ip))
614
- return true;
615
- if (normalized.startsWith("10.") || normalized.startsWith("192.168."))
616
- return true;
617
- const second = Number(normalized.split(".")[1]);
618
- return normalized.startsWith("172.") && Number.isInteger(second) && second >= 16 && second <= 31;
619
- }
620
551
 
621
552
  // src/pipelines/serv-pipeline-security-headers.ts
622
553
  function securityHeaders() {
@@ -627,9 +558,14 @@ function securityHeaders() {
627
558
  await next();
628
559
  };
629
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
+ }
630
566
  var ApiPrefixSchema = z.custom(
631
- (value) => typeof value === "string" && value.startsWith("/api/"),
632
- "apiPrefix must start with /api/"
567
+ (value) => typeof value === "string" && /^\/[\w.-]+(?:\/[\w.-]+)*$/.test(value) && !value.split("/").some((segment) => segment === "." || segment === ".."),
568
+ { error: () => servM("serv_invalidApiPrefix") }
633
569
  );
634
570
  var AbsolutePathSchema = z.custom(
635
571
  (value) => typeof value === "string" && value.startsWith("/"),
@@ -653,15 +589,6 @@ var HealthHttpConfigInputSchema = z.union([
653
589
  readyPath: AbsolutePathSchema.optional().default("/ready")
654
590
  })
655
591
  ]);
656
- var RpcHttpConfigInputSchema = z.union([
657
- z.literal(false),
658
- z.object({
659
- prefix: AbsolutePathSchema.default("/rpc"),
660
- access: z.enum(["loopback", "private-network", "gateway-only"]).default("loopback"),
661
- gatewayHeader: z.string().optional(),
662
- gatewaySecret: z.string().optional()
663
- })
664
- ]);
665
592
  var TransportRuntimeConfigInputSchema = z.union([
666
593
  z.literal(false),
667
594
  z.object({
@@ -688,8 +615,7 @@ var ServHttpConfigInputSchema = z.object({
688
615
  apiPrefix: ApiPrefixSchema.default("/api/v1"),
689
616
  openapi: OpenAPIHttpConfigInputSchema.default(false),
690
617
  docs: DocsHttpConfigInputSchema.default(false),
691
- health: HealthHttpConfigInputSchema.default({ path: "/health", readyPath: "/ready" }),
692
- rpc: RpcHttpConfigInputSchema.default(false)
618
+ health: HealthHttpConfigInputSchema.default({ path: "/health", readyPath: "/ready" })
693
619
  });
694
620
  var ServConfigSchema = z.object({
695
621
  server: ServerConfigInputSchema.optional(),
@@ -1402,14 +1328,13 @@ function createApp(options) {
1402
1328
  audioWsServers.set(app, new WebSocketServer({ noServer: true }));
1403
1329
  }
1404
1330
  mountOpenAPIRoutes(app, options.procedures, http.apiPrefix, createContext);
1405
- if (http.rpc !== false)
1406
- mountRPCRoutes(app, options.procedures, http.rpc, createContext);
1407
1331
  if (http.openapi !== false) {
1408
1332
  app.get(http.openapi.path, async (c) => c.json(await getSpec()));
1409
1333
  }
1410
1334
  if (http.docs !== false) {
1411
1335
  const docs = http.docs;
1412
- app.get(SCALAR_ROUTE, async (c) => {
1336
+ const docsDirectory = docs.path.slice(0, docs.path.lastIndexOf("/") + 1);
1337
+ app.get(`${docsDirectory}${SCALAR_ROUTE.slice(1)}`, async (c) => {
1413
1338
  const content = await getScalarScript();
1414
1339
  if (!content)
1415
1340
  return c.notFound();
@@ -1423,8 +1348,8 @@ function createApp(options) {
1423
1348
  if (!context.session)
1424
1349
  return c.json(buildHaiErrorBody(HaiCommonError.UNAUTHORIZED, servM("serv_errorUnauthorized", { locale: resolveRequestLocale(c.req.raw.headers) })), 401);
1425
1350
  }
1426
- const specUrl = http.openapi === false ? void 0 : http.openapi.path;
1427
- 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) }));
1428
1353
  });
1429
1354
  }
1430
1355
  return app;
@@ -1442,12 +1367,6 @@ function mountOpenAPIRoutes(app, procedures, apiPrefix, createContext) {
1442
1367
  const handler = new OpenAPIHandler(procedures);
1443
1368
  app.on([...API_METHODS], `${apiPrefix}/*`, (c, next) => handleORPC(c, next, apiPrefix, handler, createContext));
1444
1369
  }
1445
- function mountRPCRoutes(app, procedures, rpcConfig, createContext) {
1446
- const handler = new RPCHandler(procedures);
1447
- const prefix = rpcConfig.prefix;
1448
- app.use(`${prefix}/*`, requireInternalRPC(rpcConfig));
1449
- app.on([...API_METHODS], `${prefix}/*`, (c, next) => handleORPC(c, next, prefix, handler, createContext));
1450
- }
1451
1370
  async function handleORPC(c, next, prefix, handler, createContext) {
1452
1371
  const context = await createContext({ request: c.req.raw, remoteAddress: readRemoteAddress(c.env) });
1453
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 }));