@h-ai/serv 0.1.0-alpha.61 → 0.1.0-alpha.62

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 CHANGED
@@ -84,6 +84,8 @@ const handler = serv.toFetch(app)
84
84
  export default { fetch: handler }
85
85
  ```
86
86
 
87
+ Node `serv.listen()` supplies the socket peer address. Forwarded headers are ignored by default. Only proxy IPs/CIDRs in `http.trustedProxies` may supply them: XFF is walked from right to left until the first untrusted address; content to the left of that address is ignored. An invalid or excessively long trusted chain leaves IP undefined rather than promoting the proxy to client identity. Proxies must sanitize or append forwarded headers. Fetch platforms explicitly provide a trusted peer through `app.fetch(request, { remoteAddress: platformPeerAddress })`; without metadata IP is undefined. Do not copy arbitrary headers into metadata. Custom `createContext(input)` forwards the complete input to preserve resolved `input.ip`. Internal RPC checks the connection peer.
88
+
87
89
  ### Audio WebSocket: tickets and server authorization
88
90
 
89
91
  Audio connections require short-lived, one-time tickets. Issue them through an authenticated HTTP endpoint (recommended: `iam.ticket.issue`) and consume atomically in `verifyTicket` (recommended: `iam.ticket.consume`). Authentication runs at connection time with a separate timeout; no business frames are processed beforehand. The framework stores the returned `ServSession` and optional `grant`, then calls `authorize` to approve paid parameters:
@@ -152,6 +154,7 @@ When using `@h-ai/core` and `ServConfigSchema` with `config/_serv.yml`, transpor
152
154
  ```yaml
153
155
  # config/_serv.yml
154
156
  http:
157
+ trustedProxies: []
155
158
  apiPrefix: /api/v1
156
159
 
157
160
  transport:
@@ -205,10 +208,10 @@ const app = serv.createApp({
205
208
 
206
209
  Pass the top-level `iam` handle to `serv.createApp` to derive:
207
210
 
208
- - **Access token verification**: `iam.session.verifyToken` fills `context.session` on every request without caching.
211
+ - **Access token verification**: `iam.auth.verifyToken` fills `context.session` on every request without caching.
209
212
  - **Refresh rotation**: with `refreshCookie`, `iam.session.refresh` becomes the httpOnly-cookie refresh callback.
210
213
 
211
- **Context factory precedence**: explicit `createContext` > `verifyToken` > `iam.session.verifyToken` > metadata-only default.
214
+ **Context factory precedence**: explicit `createContext` > `verifyToken` > `iam.auth.verifyToken` > metadata-only default.
212
215
 
213
216
  ```ts
214
217
  // 推荐:传入 iam,一行打开认证与 Cookie 刷新
@@ -233,7 +236,7 @@ const app = serv.createApp({
233
236
  createContext: async ({ request }) => {
234
237
  const base = serv.parseRequestContext({ request })
235
238
  // 手动填充 session(serv 不再自动填充)
236
- const result = await iam.session.verifyToken(base.accessToken ?? '')
239
+ const result = await iam.auth.verifyToken(base.accessToken ?? '')
237
240
  return {
238
241
  ...base,
239
242
  session: result.success ? result.data : undefined,
@@ -339,16 +342,16 @@ To reuse `Bearer token -> session` and add tenant, workspace, or source-system f
339
342
  prefer `serv.buildAuthContextFactory()` to assemble a custom `createContext`:
340
343
 
341
344
  ```ts
342
- const baseContext = serv.buildAuthContextFactory(token => iam.session.verifyToken(token))
345
+ const baseContext = serv.buildAuthContextFactory(token => iam.auth.verifyToken(token))
343
346
 
344
347
  const app = serv.createApp({
345
348
  contract,
346
349
  procedures,
347
- createContext: async ({ request }) => {
348
- const context = await baseContext({ request })
350
+ createContext: async (input) => {
351
+ const context = await baseContext(input)
349
352
  return {
350
353
  ...context,
351
- tenantId: request.headers.get('x-tenant-id') ?? null,
354
+ tenantId: input.request.headers.get('x-tenant-id') ?? null,
352
355
  }
353
356
  },
354
357
  })
@@ -436,7 +439,7 @@ const app = serv.createApp({
436
439
  && security.isNativeOrigin(request.headers.get('origin') ?? ''),
437
440
  },
438
441
  },
439
- // ✅ 无需再传 verifyToken,iam.session.verifyToken 自动用于填充 context.session
442
+ // ✅ 无需再传 verifyToken,iam.auth.verifyToken 自动用于填充 context.session
440
443
  })
441
444
  ```
442
445
 
@@ -518,6 +521,7 @@ server:
518
521
  port: 3000
519
522
 
520
523
  http:
524
+ trustedProxies: []
521
525
  apiPrefix: /api/v1
522
526
  openapi:
523
527
  path: /openapi.json
package/README.ja.md CHANGED
@@ -84,6 +84,8 @@ const handler = serv.toFetch(app)
84
84
  export default { fetch: handler }
85
85
  ```
86
86
 
87
+ Node `serv.listen()` は socket の接続元を自動で渡し、既定では転送ヘッダーを無視します。`http.trustedProxies` の IP/CIDR のみ信頼し、XFF を右から左へ調べ、最初の非信頼アドレスで停止します。その左側は無視します。信頼チェーンが不正・長すぎる場合は IP 未定義となり、代理をクライアントとして扱いません。代理は転送ヘッダーを清掃・追加してください。Fetch は `app.fetch(request, { remoteAddress: platformPeerAddress })` で信頼できる接続元を渡します。メタデータなしは IP 未定義です。任意のヘッダーをコピーしないでください。独自 `createContext(input)` は input 全体を再利用し、解決済み `input.ip` を保持します。内部 RPC は接続元で制限します。
88
+
87
89
  ### Audio WebSocket:ticket とサーバー認可
88
90
 
89
91
  音声接続には短期・単回 ticket が必要です。認証済み HTTP endpoint で発行(推奨:`iam.ticket.issue`)し、`verifyTicket` でアトミックに消費します(推奨:`iam.ticket.consume`)。接続直後に独立 timeout で認証し、認証前に業務フレームを処理しません。返された `ServSession` と任意の `grant` を保存してから `authorize` で有料パラメーターを確認します:
@@ -152,6 +154,7 @@ await apiClient.init({
152
154
  ```yaml
153
155
  # config/_serv.yml
154
156
  http:
157
+ trustedProxies: []
155
158
  apiPrefix: /api/v1
156
159
 
157
160
  transport:
@@ -205,10 +208,10 @@ const app = serv.createApp({
205
208
 
206
209
  `serv.createApp` にトップレベルの `iam` を渡すと、次を自動構成します:
207
210
 
208
- - **access token 検証**:各リクエストで `iam.session.verifyToken` を呼び `context.session` を設定(キャッシュなし)。
211
+ - **access token 検証**:各リクエストで `iam.auth.verifyToken` を呼び `context.session` を設定(キャッシュなし)。
209
212
  - **refresh token の更新**:`refreshCookie` 有効時、`iam.session.refresh` を httpOnly cookie 更新処理に使用。
210
213
 
211
- **context の優先順位**:明示的 `createContext` > `verifyToken` > `iam.session.verifyToken` > メタデータのみの既定解析。
214
+ **context の優先順位**:明示的 `createContext` > `verifyToken` > `iam.auth.verifyToken` > メタデータのみの既定解析。
212
215
 
213
216
  ```ts
214
217
  // 推荐:传入 iam,一行打开认证与 Cookie 刷新
@@ -233,7 +236,7 @@ const app = serv.createApp({
233
236
  createContext: async ({ request }) => {
234
237
  const base = serv.parseRequestContext({ request })
235
238
  // 手动填充 session(serv 不再自动填充)
236
- const result = await iam.session.verifyToken(base.accessToken ?? '')
239
+ const result = await iam.auth.verifyToken(base.accessToken ?? '')
237
240
  return {
238
241
  ...base,
239
242
  session: result.success ? result.data : undefined,
@@ -339,16 +342,16 @@ const app = serv.createApp({
339
342
  `serv.buildAuthContextFactory()` で独自の `createContext` を構成してください:
340
343
 
341
344
  ```ts
342
- const baseContext = serv.buildAuthContextFactory(token => iam.session.verifyToken(token))
345
+ const baseContext = serv.buildAuthContextFactory(token => iam.auth.verifyToken(token))
343
346
 
344
347
  const app = serv.createApp({
345
348
  contract,
346
349
  procedures,
347
- createContext: async ({ request }) => {
348
- const context = await baseContext({ request })
350
+ createContext: async (input) => {
351
+ const context = await baseContext(input)
349
352
  return {
350
353
  ...context,
351
- tenantId: request.headers.get('x-tenant-id') ?? null,
354
+ tenantId: input.request.headers.get('x-tenant-id') ?? null,
352
355
  }
353
356
  },
354
357
  })
@@ -436,7 +439,7 @@ const app = serv.createApp({
436
439
  && security.isNativeOrigin(request.headers.get('origin') ?? ''),
437
440
  },
438
441
  },
439
- // ✅ 无需再传 verifyToken,iam.session.verifyToken 自动用于填充 context.session
442
+ // ✅ 无需再传 verifyToken,iam.auth.verifyToken 自动用于填充 context.session
440
443
  })
441
444
  ```
442
445
 
@@ -518,6 +521,7 @@ server:
518
521
  port: 3000
519
522
 
520
523
  http:
524
+ trustedProxies: []
521
525
  apiPrefix: /api/v1
522
526
  openapi:
523
527
  path: /openapi.json
package/README.md CHANGED
@@ -84,6 +84,8 @@ const handler = serv.toFetch(app)
84
84
  export default { fetch: handler }
85
85
  ```
86
86
 
87
+ Node `serv.listen()` 自动提供 socket 对端地址;默认忽略 `X-Forwarded-For` / `X-Real-IP`。只有 `http.trustedProxies` 中列出的代理 IP/CIDR 才能提供转发头,XFF 从右向左停在首个不可信地址,忽略其左侧内容;可信链无法解析或过长时 IP 为 undefined,不能回退为代理身份。代理必须清洗或追加转发头。Fetch 平台通过 `app.fetch(request, { remoteAddress: platformPeerAddress })` 显式提供可信对端,缺失时 IP 为 undefined,不能从任意请求头复制。自定义 `createContext(input)` 复用完整 input,保留框架已解析的 `input.ip`。内部 RPC 仍按连接对端限制。
88
+
87
89
  ### Audio WebSocket:ticket 与服务端授权
88
90
 
89
91
  语音入口必须使用短期、一次性 ticket。应用通过已登录 HTTP endpoint 签发 ticket(推荐 `iam.ticket.issue`),并在 `verifyTicket` 中原子消费(推荐 `iam.ticket.consume`);连接建立即完成鉴权(独立预鉴权超时),未鉴权不处理任何业务帧。框架保存返回的 `ServSession` 与可选 `grant`,再调用 `authorize` 确认付费参数:
@@ -152,6 +154,7 @@ await apiClient.init({
152
154
  ```yaml
153
155
  # config/_serv.yml
154
156
  http:
157
+ trustedProxies: []
155
158
  apiPrefix: /api/v1
156
159
 
157
160
  transport:
@@ -205,10 +208,10 @@ const app = serv.createApp({
205
208
 
206
209
  `serv.createApp` 推荐直接传入顶层 `iam` 句柄,自动派生两件事:
207
210
 
208
- - **access token 校验**:每个请求调用 `iam.session.verifyToken` 填充 `context.session`(不缓存)。
211
+ - **access token 校验**:每个请求调用 `iam.auth.verifyToken` 填充 `context.session`(不缓存)。
209
212
  - **refresh token 轮换**:启用 `refreshCookie` 后,自动使用 `iam.session.refresh` 作为 httpOnly cookie 刷新回调。
210
213
 
211
- **上下文工厂优先级**:显式 `createContext` > `verifyToken` > `iam.session.verifyToken` > 默认仅解析元数据。
214
+ **上下文工厂优先级**:显式 `createContext` > `verifyToken` > `iam.auth.verifyToken` > 默认仅解析元数据。
212
215
 
213
216
  ```ts
214
217
  // 推荐:传入 iam,一行打开认证与 Cookie 刷新
@@ -233,7 +236,7 @@ const app = serv.createApp({
233
236
  createContext: async ({ request }) => {
234
237
  const base = serv.parseRequestContext({ request })
235
238
  // 手动填充 session(serv 不再自动填充)
236
- const result = await iam.session.verifyToken(base.accessToken ?? '')
239
+ const result = await iam.auth.verifyToken(base.accessToken ?? '')
237
240
  return {
238
241
  ...base,
239
242
  session: result.success ? result.data : undefined,
@@ -339,16 +342,16 @@ const app = serv.createApp({
339
342
  优先用 `serv.buildAuthContextFactory()` 组装自定义 `createContext`:
340
343
 
341
344
  ```ts
342
- const baseContext = serv.buildAuthContextFactory(token => iam.session.verifyToken(token))
345
+ const baseContext = serv.buildAuthContextFactory(token => iam.auth.verifyToken(token))
343
346
 
344
347
  const app = serv.createApp({
345
348
  contract,
346
349
  procedures,
347
- createContext: async ({ request }) => {
348
- const context = await baseContext({ request })
350
+ createContext: async (input) => {
351
+ const context = await baseContext(input)
349
352
  return {
350
353
  ...context,
351
- tenantId: request.headers.get('x-tenant-id') ?? null,
354
+ tenantId: input.request.headers.get('x-tenant-id') ?? null,
352
355
  }
353
356
  },
354
357
  })
@@ -436,7 +439,7 @@ const app = serv.createApp({
436
439
  && security.isNativeOrigin(request.headers.get('origin') ?? ''),
437
440
  },
438
441
  },
439
- // ✅ 无需再传 verifyToken,iam.session.verifyToken 自动用于填充 context.session
442
+ // ✅ 无需再传 verifyToken,iam.auth.verifyToken 自动用于填充 context.session
440
443
  })
441
444
  ```
442
445
 
@@ -518,6 +521,7 @@ server:
518
521
  port: 3000
519
522
 
520
523
  http:
524
+ trustedProxies: []
521
525
  apiPrefix: /api/v1
522
526
  openapi:
523
527
  path: /openapi.json
@@ -2,7 +2,7 @@ import * as _orpc_server from '@orpc/server';
2
2
  import * as zod_v4_core from 'zod/v4/core';
3
3
  import * as zod from 'zod';
4
4
  import { AIFunctions } from '@h-ai/ai';
5
- import { S as ServContext } from '../serv-context-CkbS0M9G.js';
5
+ import { S as ServContext } from '../serv-context-CWzyrsdq.js';
6
6
  import '@h-ai/core';
7
7
 
8
8
  /** AI 默认 procedures 依赖。 */
@@ -4,7 +4,7 @@ import * as zod from 'zod';
4
4
  import * as _orpc_contract from '@orpc/contract';
5
5
  import { IamFunctions } from '@h-ai/iam';
6
6
  export { TokenPair } from '@h-ai/iam';
7
- import { S as ServContext } from '../serv-context-CkbS0M9G.js';
7
+ import { S as ServContext } from '../serv-context-CWzyrsdq.js';
8
8
  import '@h-ai/core';
9
9
 
10
10
  /** IAM 默认 procedures 依赖。 */
@@ -480,6 +480,7 @@ declare function createIamProcedures(deps: IamProcedureDeps): {
480
480
  list: _orpc_server.Procedure<ServContext & {}, any, zod.ZodObject<{
481
481
  page: zod.ZodOptional<zod.ZodCoercedNumber<unknown>>;
482
482
  pageSize: zod.ZodOptional<zod.ZodCoercedNumber<unknown>>;
483
+ roleCode: zod.ZodOptional<zod.ZodString>;
483
484
  search: zod.ZodOptional<zod.ZodString>;
484
485
  enabled: zod.ZodPipe<zod.ZodOptional<zod.ZodEnum<{
485
486
  true: "true";
@@ -2,7 +2,7 @@ import * as _orpc_server from '@orpc/server';
2
2
  import * as zod_v4_core from 'zod/v4/core';
3
3
  import * as zod from 'zod';
4
4
  import { StorageFunctions } from '@h-ai/storage';
5
- import { S as ServContext } from '../serv-context-CkbS0M9G.js';
5
+ import { S as ServContext } from '../serv-context-CWzyrsdq.js';
6
6
  import '@h-ai/core';
7
7
 
8
8
  /** Storage 默认 procedures 依赖。 */
package/dist/index.d.ts CHANGED
@@ -3,8 +3,8 @@ import { HaiResult, ValidationFormError } from '@h-ai/core';
3
3
  import { AIFunctions, AudioWsStartMessage, AudioFormat } from '@h-ai/ai';
4
4
  import { AnyContractRouter, OpenAPI } from '@orpc/contract';
5
5
  import { ProcedureHandler, ErrorMap, Meta, ProcedureHandlerOptions, ORPCErrorConstructorMap, Router, ContractRouter, Context, ContractProcedure, InferSchemaInput, InferSchemaOutput, UnlaziedRouter } from '@orpc/server';
6
- import { a as ServSession, S as ServContext, b as ServIam, R as RefreshCookieConfig, C as CreateServContext, p as parseRequestContext, c as buildAuthContextFactory } from './serv-context-CkbS0M9G.js';
7
- export { d as CreateServContextInput, N as NativeRefreshTokenTransportConfig, e as RefreshTokenPair } from './serv-context-CkbS0M9G.js';
6
+ import { a as ServSession, S as ServContext, b as ServIam, R as RefreshCookieConfig, C as CreateServContext, c as ServRequestMetadata, p as parseRequestContext, d as buildAuthContextFactory } from './serv-context-CWzyrsdq.js';
7
+ export { e as CreateServContextInput, N as NativeRefreshTokenTransportConfig, f as RefreshTokenPair } from './serv-context-CWzyrsdq.js';
8
8
  import { MiddlewareHandler } from 'hono';
9
9
  import { z, ZodType } from 'zod';
10
10
  import { CryptoFunctions, TransportKeyStore } from '@h-ai/crypto';
@@ -128,6 +128,8 @@ interface ServCorsRuntimeConfig {
128
128
  }
129
129
  /** ServHttpApp 的 HTTP 挂载配置。 */
130
130
  interface ServHttpConfig {
131
+ /** 可信任的连接代理 IP/CIDR;默认空数组,不采纳转发头。 */
132
+ readonly trustedProxies: readonly string[];
131
133
  readonly apiPrefix: `/api/${string}`;
132
134
  readonly openapi: false | ServOpenAPIHttpConfig;
133
135
  readonly docs: false | ServDocsHttpConfig;
@@ -151,6 +153,7 @@ type ServTransportRuntimeConfigInput = Partial<{
151
153
  }>;
152
154
  /** `serv.createApp` 接受的 HTTP 配置入参类型。 */
153
155
  type ServHttpConfigInput = Partial<{
156
+ readonly trustedProxies: readonly string[];
154
157
  readonly apiPrefix: `/api/${string}`;
155
158
  readonly openapi: false | Partial<ServOpenAPIHttpConfig>;
156
159
  readonly docs: false | Partial<ServDocsHttpConfig>;
@@ -171,6 +174,7 @@ declare const ServConfigSchema: z.ZodPipe<z.ZodObject<{
171
174
  port: z.ZodDefault<z.ZodNumber>;
172
175
  }, z.core.$strip>>;
173
176
  http: z.ZodOptional<z.ZodObject<{
177
+ trustedProxies: z.ZodDefault<z.ZodArray<z.ZodString>>;
174
178
  apiPrefix: z.ZodDefault<z.ZodCustom<`/api/${string}`, `/api/${string}`>>;
175
179
  openapi: z.ZodDefault<z.ZodUnion<readonly [z.ZodLiteral<false>, z.ZodObject<{
176
180
  path: z.ZodDefault<z.ZodCustom<`/${string}`, `/${string}`>>;
@@ -217,6 +221,7 @@ declare const ServConfigSchema: z.ZodPipe<z.ZodObject<{
217
221
  port: number;
218
222
  } | undefined;
219
223
  http?: {
224
+ trustedProxies: string[];
220
225
  apiPrefix: `/api/${string}`;
221
226
  openapi: false | {
222
227
  path: `/${string}`;
@@ -327,7 +332,7 @@ interface ServMiddlewareMount {
327
332
  * - `request`:测试 / 本地直连时发起 in-process 请求,避免消费方 import Hono。
328
333
  */
329
334
  interface ServHttpApp {
330
- readonly fetch: (request: Request) => Response | Promise<Response>;
335
+ readonly fetch: (request: Request, metadata?: ServRequestMetadata) => Response | Promise<Response>;
331
336
  readonly request: (input: RequestInfo | URL, init?: RequestInit) => Response | Promise<Response>;
332
337
  }
333
338
  /**
@@ -336,13 +341,13 @@ interface ServHttpApp {
336
341
  * **认证 / 会话填充设计**(遵循最小知识原则):
337
342
  *
338
343
  * - **`iam`**(推荐):单一顶层引用。提供后 serv 自动:
339
- * 1. 使用 `iam.session.verifyToken` 填充 `context.session` → route guard 生效
344
+ * 1. 使用 `iam.auth.verifyToken` 填充 `context.session` → route guard 生效
340
345
  * 2. 若同时启用 `refreshCookie`,使用 `iam.session.refresh` 实现 refresh token 轮换
341
346
  *
342
347
  * - **`refreshCookie`**(可选):浏览器默认用 httpOnly cookie;受信原生客户端可配置 body 通道。与 `iam` 正交:
343
348
  * `iam` 决定 *谁* 验证 token,`refreshCookie` 决定 refresh token *怎么* 传输。
344
349
  *
345
- * - **`verifyToken`**(高级):不使用 IAM 模块的逆脱口。优先于 `iam.session.verifyToken`。
350
+ * - **`verifyToken`**(高级):不使用 IAM 模块的逆脱口。优先于 `iam.auth.verifyToken`。
346
351
  *
347
352
  * - **`createContext`**(高级):完全接管上下文构造,设置后 serv **不再**自动填充 session,
348
353
  * 由调用方负责。
@@ -362,7 +367,7 @@ interface CreateServAppOptions<TContract extends AnyContractRouter, TProcedures
362
367
  */
363
368
  readonly middlewares?: readonly ServMiddlewareMount[];
364
369
  /**
365
- * IAM 模块引用(推荐)。提供后 serv 自动使用 `iam.session.verifyToken` 填充 session,
370
+ * IAM 模块引用(推荐)。提供后 serv 自动使用 `iam.auth.verifyToken` 填充 session,
366
371
  * 以及在 `refreshCookie` 启用时使用 `iam.session.refresh` 实现 cookie 刷新。
367
372
  *
368
373
  * 类型上要求 {@link ServIam}(`iam` 可以直接传入,结构类型保证兼容)。
@@ -374,7 +379,7 @@ interface CreateServAppOptions<TContract extends AnyContractRouter, TProcedures
374
379
  */
375
380
  readonly refreshCookie?: RefreshCookieConfig;
376
381
  /**
377
- * 高级:自定义访问令牌校验回调,覆盖 `iam.session.verifyToken`。
382
+ * 高级:自定义访问令牌校验回调,覆盖 `iam.auth.verifyToken`。
378
383
  * 不使用 IAM 模块的场景下使用。
379
384
  */
380
385
  readonly verifyToken?: (token: string) => Promise<HaiResult<ServSession>>;
@@ -915,4 +920,4 @@ declare const serv: {
915
920
  m: (key: "$schema" | "serv_validationFailed" | "serv_validationRequired" | "serv_validationInvalid" | "serv_validationInvalidType" | "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", options?: _h_ai_core.MessageOptions) => string;
916
921
  };
917
922
 
918
- export { type AudioTicketGrant, type AudioTicketVerification, type AuthenticatedServContext, 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 ServImplementer, type ServListenOptions, type ServMiddleware, type ServMiddlewareFactory, type ServMiddlewareMount, type ServNodeServer, type ServOpenAPIHttpConfig, type ServProcedureHandler, type ServProcedureOptions, type ServRouteHandler, type ServRoutePath, type ServRouterBuilder, type ServRpcHttpConfig, type ServRuntimeSecurityPolicy, type ServRuntimeSecurityPolicyInput, type ServServerConfig, type ServServerConfigInput, ServSession, type ServStorageAssetsConfig, type ServTransportConfig, type ServTransportRuntimeConfig, type ServTransportRuntimeConfigInput, type ServValidationFailureBody, serv };
923
+ export { type AudioTicketGrant, type AudioTicketVerification, type AuthenticatedServContext, 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 ServImplementer, type ServListenOptions, type ServMiddleware, type ServMiddlewareFactory, type ServMiddlewareMount, type ServNodeServer, type ServOpenAPIHttpConfig, type ServProcedureHandler, type ServProcedureOptions, ServRequestMetadata, type ServRouteHandler, type ServRoutePath, type ServRouterBuilder, type ServRpcHttpConfig, 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
@@ -8,6 +8,7 @@ import { RPCHandler } from '@orpc/server/fetch';
8
8
  import { Hono } from 'hono';
9
9
  import { WebSocketServer } from 'ws';
10
10
  import { Buffer } from 'buffer';
11
+ import { BlockList, isIP } from 'net';
11
12
  import { z } from 'zod';
12
13
  import { getCookie } from 'hono/cookie';
13
14
  import { readFile } from 'fs/promises';
@@ -520,6 +521,70 @@ function toArrayBuffer(data) {
520
521
  new Uint8Array(buffer).set(data);
521
522
  return buffer;
522
523
  }
524
+ function readRemoteAddress(metadata) {
525
+ if (!metadata || typeof metadata !== "object" || !("remoteAddress" in metadata) || typeof metadata.remoteAddress !== "string")
526
+ return void 0;
527
+ return normalizeClientIp(metadata.remoteAddress);
528
+ }
529
+ function normalizeClientIp(value) {
530
+ if (!value)
531
+ return void 0;
532
+ const address = value.trim();
533
+ if (!isIP(address))
534
+ return void 0;
535
+ const mapped = address.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i);
536
+ return mapped?.[1] ?? address;
537
+ }
538
+ function addProxy(blockList, entry) {
539
+ const [address, prefix, extra] = entry.split("/");
540
+ const family = address ? isIP(address) : 0;
541
+ if (!family || extra !== void 0)
542
+ throw new Error("trustedProxies entries must be IP addresses or CIDRs");
543
+ const type = family === 4 ? "ipv4" : "ipv6";
544
+ if (prefix === void 0) {
545
+ blockList.addAddress(address, type);
546
+ return;
547
+ }
548
+ const bits = Number(prefix);
549
+ if (!/^\d+$/.test(prefix) || bits < 0 || bits > (family === 4 ? 32 : 128))
550
+ throw new Error("Invalid trustedProxies CIDR prefix");
551
+ blockList.addSubnet(address, bits, type);
552
+ }
553
+ function isTrustedProxyEntry(entry) {
554
+ try {
555
+ addProxy(new BlockList(), entry);
556
+ return true;
557
+ } catch {
558
+ return false;
559
+ }
560
+ }
561
+ function createClientIpResolver(trustedProxies) {
562
+ const proxies = new BlockList();
563
+ for (const entry of trustedProxies)
564
+ addProxy(proxies, entry);
565
+ const trusted = (address) => proxies.check(address, isIP(address) === 4 ? "ipv4" : "ipv6");
566
+ return (request, remoteAddress) => {
567
+ const peer = normalizeClientIp(remoteAddress);
568
+ if (!peer || !trusted(peer))
569
+ return peer;
570
+ const forwarded = request.headers.get("x-forwarded-for");
571
+ if (forwarded === null) {
572
+ const realIp = request.headers.get("x-real-ip");
573
+ return realIp === null ? peer : normalizeClientIp(realIp);
574
+ }
575
+ const chain = forwarded.split(",");
576
+ if (chain.length > 32)
577
+ return void 0;
578
+ let current = peer;
579
+ for (let index = chain.length - 1; index >= 0 && trusted(current); index--) {
580
+ const address = normalizeClientIp(chain[index]);
581
+ if (!address)
582
+ return void 0;
583
+ current = address;
584
+ }
585
+ return current;
586
+ };
587
+ }
523
588
  var DEFAULT_LOCALE = "zh-CN";
524
589
  function resolveRequestLocale(headers) {
525
590
  const get = (name) => {
@@ -586,6 +651,9 @@ function requireInternalRPC(config) {
586
651
  };
587
652
  }
588
653
  async function getRequestRemoteAddress(c) {
654
+ const remoteAddress = readRemoteAddress(c.env);
655
+ if (remoteAddress)
656
+ return remoteAddress;
589
657
  const getConnInfo = await resolveGetConnInfo();
590
658
  if (!getConnInfo)
591
659
  return void 0;
@@ -676,6 +744,7 @@ var CorsRuntimeConfigInputSchema = z.object({
676
744
  credentials: z.boolean().default(false)
677
745
  });
678
746
  var ServHttpConfigInputSchema = z.object({
747
+ trustedProxies: z.array(z.string().trim().refine(isTrustedProxyEntry, "Expected an IP address or CIDR")).default([]),
679
748
  apiPrefix: ApiPrefixSchema.default("/api/v1"),
680
749
  openapi: OpenAPIHttpConfigInputSchema.default(false),
681
750
  docs: DocsHttpConfigInputSchema.default(false),
@@ -719,8 +788,6 @@ function resolveServTransportRuntimeConfig(input = false) {
719
788
  return TransportRuntimeConfigInputSchema.parse(input);
720
789
  }
721
790
  var HEADER_REQUEST_ID = "x-request-id";
722
- var HEADER_FORWARDED_FOR = "x-forwarded-for";
723
- var HEADER_REAL_IP = "x-real-ip";
724
791
  var HEADER_AUTHORIZATION = "authorization";
725
792
  var HEADER_USER_AGENT = "user-agent";
726
793
  function extractBearerToken(value) {
@@ -734,8 +801,7 @@ function extractBearerToken(value) {
734
801
  function parseRequestContext(input) {
735
802
  const headers = input.request.headers;
736
803
  const requestId = headers.get(HEADER_REQUEST_ID) ?? core.id.uuid();
737
- const forwardedFor = headers.get(HEADER_FORWARDED_FOR);
738
- const ip = headers.get(HEADER_REAL_IP) ?? forwardedFor?.split(",")[0]?.trim();
804
+ const ip = normalizeClientIp(Object.hasOwn(input, "ip") ? input.ip : input.remoteAddress);
739
805
  const locale = resolveRequestLocale(headers);
740
806
  const accessToken = extractBearerToken(headers.get(HEADER_AUTHORIZATION));
741
807
  return {
@@ -1160,8 +1226,10 @@ function createApp(options) {
1160
1226
  core.i18n.coreM("core_errorNotFound", { locale: resolveRequestLocale(c.req.raw.headers) })
1161
1227
  ), 404));
1162
1228
  const iamRef = options.iam;
1163
- const verifyToken = options.verifyToken ?? (iamRef ? (token) => iamRef.session.verifyToken(token) : void 0);
1164
- const createContext = options.createContext ?? (verifyToken ? buildAuthContextFactory(verifyToken) : parseRequestContext);
1229
+ const verifyToken = options.verifyToken ?? (iamRef ? (token) => iamRef.auth.verifyToken(token) : void 0);
1230
+ const contextFactory = options.createContext ?? (verifyToken ? buildAuthContextFactory(verifyToken) : parseRequestContext);
1231
+ const resolveIp = createClientIpResolver(http.trustedProxies);
1232
+ const createContext = (input) => contextFactory({ ...input, ip: resolveIp(input.request, input.remoteAddress) });
1165
1233
  let cachedSpec;
1166
1234
  const getSpec = () => {
1167
1235
  cachedSpec ??= generateSpec(options.contract, { apiPrefix: http.apiPrefix });
@@ -1224,7 +1292,7 @@ function createApp(options) {
1224
1292
  });
1225
1293
  app.get(docs.path, async (c) => {
1226
1294
  if (docs.requireAuth) {
1227
- const context = await createContext({ request: c.req.raw });
1295
+ const context = await createContext({ request: c.req.raw, remoteAddress: readRemoteAddress(c.env) });
1228
1296
  if (!context.session)
1229
1297
  return c.json(buildHaiErrorBody(HaiCommonError.UNAUTHORIZED, servM("serv_errorUnauthorized", { locale: resolveRequestLocale(c.req.raw.headers) })), 401);
1230
1298
  }
@@ -1254,7 +1322,7 @@ function mountRPCRoutes(app, procedures, rpcConfig, createContext) {
1254
1322
  app.on([...API_METHODS], `${prefix}/*`, (c, next) => handleORPC(c, next, prefix, handler, createContext));
1255
1323
  }
1256
1324
  async function handleORPC(c, next, prefix, handler, createContext) {
1257
- const context = await createContext({ request: c.req.raw });
1325
+ const context = await createContext({ request: c.req.raw, remoteAddress: readRemoteAddress(c.env) });
1258
1326
  const { matched, response } = await handler.handle(c.req.raw, { prefix, context });
1259
1327
  if (!matched) {
1260
1328
  await next();
@@ -1321,7 +1389,13 @@ function listen(app, options = {}) {
1321
1389
  const port = options.port ?? DEFAULT_SERV_PORT;
1322
1390
  const audioServer = audioWsServers.get(app);
1323
1391
  const server = serve(
1324
- { fetch: app.fetch, hostname: host, port, websocket: audioServer ? { server: audioServer } : void 0 },
1392
+ {
1393
+ // 保留原 bindings 对象,WebSocket 升级状态由 Node adapter 在同一对象上共享。
1394
+ fetch: (request, bindings) => app.fetch(request, Object.assign(bindings, { remoteAddress: bindings.incoming.socket.remoteAddress })),
1395
+ hostname: host,
1396
+ port,
1397
+ websocket: audioServer ? { server: audioServer } : void 0
1398
+ },
1325
1399
  options.onListening
1326
1400
  );
1327
1401
  const handle = {