@h-ai/serv 0.1.0-alpha.44 → 0.1.0-alpha.46

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.md CHANGED
@@ -7,6 +7,7 @@
7
7
  - 一行启动:`serv.createApp(...)` → `serv.listen(app, { port })`,应用代码不需要 import Hono
8
8
  - 扁平 API(最小知识):`serv.listen / serv.toFetch / serv.requireAuth / serv.requirePermission / serv.generateSpec / ...`
9
9
  - 自定义 pipeline:`createApp({ middlewares })` 挂 HTTP middleware;procedure wrapper 复用根入口导出的共享类型
10
+ - 只读 Storage 资源:`serv.storageAssets(...)` 统一处理安全 key、MIME 白名单、GET/HEAD、ETag 与缓存头
10
11
  - 默认 feature procedures:`createIamProcedures()`、`createStorageProcedures()`、`createAiProcedures()`
11
12
  - 内置安全响应头、健康检查、可选 OpenAPI JSON、可选 Scalar 文档页、可选内部 RPC endpoint
12
13
  - 可选传输加密:`serv.createApp({ transport: { crypto } })` 自动挂载密钥协商与请求/响应加解密
@@ -284,10 +285,13 @@ const app = serv.createApp({
284
285
  },
285
286
  {
286
287
  path: '/assets/*',
287
- // 路径级 middleware 可以终结请求;响应仍经过内置 securityHeaders。
288
- middleware: c => c.body(imageBytes, 200, {
289
- 'Content-Type': 'image/png',
290
- 'Cache-Control': 'public, max-age=31536000, immutable',
288
+ middleware: serv.storageAssets({
289
+ storage,
290
+ pathPrefix: '/assets/',
291
+ keyPattern: /^public\/[\w-]+\.png$/,
292
+ allowedContentTypes: ['image/png'],
293
+ cacheControl: 'public, max-age=31536000, immutable',
294
+ crossOriginResourcePolicy: 'cross-origin',
291
295
  }),
292
296
  },
293
297
  ],
@@ -306,7 +310,8 @@ const app = serv.createApp({
306
310
  - `middlewares` 按数组顺序注册
307
311
  - `path` 省略时默认 `'*'`
308
312
  - 由于 `middlewares` 先于 `transport` 执行,CORS preflight 或二进制端点可以直接短路;短路响应仍带内置安全头,但不会经过 transport
309
- - 文件端点的允许方法、路径校验、MIME、缓存和访问授权由应用显式决定;框架不内置头像等业务概念
313
+ - 公开只读文件优先使用 `serv.storageAssets(...)`;应用仍须显式提供 key/MIME 白名单、缓存与跨域策略,框架不内置头像等业务概念
314
+ - 私有资源、Range 下载或动态授权继续使用自定义 middleware/procedure,不应套用公开资源 helper
310
315
  - 若需要读取解密后的业务 body,请改用 context / procedure 层扩展
311
316
  - 浏览器若需读取自定义响应头(例如 transport 的 `X-Encrypted`),请通过 `serv.cors({ exposedHeaders: [...] })` 显式暴露
312
317
  - 这一层直接操作 HTTP middleware context,返回的是 **HTTP Response**,不是 `HaiResult`
@@ -394,6 +399,7 @@ httpOnly cookie 模式将 refresh token 存储在服务端管理的 cookie 中
394
399
  `serv.parseRequestContext()` / `extractBearerToken()` 解析的就是它;`buildAuthContextFactory()` 校验的也是它。
395
400
  - `refreshToken`:长期凭证。启用 `refreshCookie` 后,它不会再暴露给前端 JS,而是只保存在浏览器的 httpOnly cookie 中。
396
401
  它**不会**被 `extractBearerToken()` 读取,只会在 `/auth/refresh` 被服务端取出,用来换发新的 access token。
402
+ 受信原生客户端可通过 `nativeTokenTransport` 改用 JSON body,但必须同时校验客户端标识与 Origin。
397
403
 
398
404
  **工作原理:**
399
405
 
@@ -402,6 +408,9 @@ httpOnly cookie 模式将 refresh token 存储在服务端管理的 cookie 中
402
408
  3. Access token 过期时,浏览器自动携带 cookie 访问 `/auth/refresh`
403
409
  4. 服务端读取 cookie → 调用 `onRefresh` → 返回新 access token + 更新 cookie(响应体不暴露 refresh token)
404
410
 
411
+ 原生客户端匹配 `nativeTokenTransport.isRequest` 后,登录/注册/刷新响应会保留轮换后的
412
+ `refreshToken`,刷新请求从 `{ refreshToken }` body 读取,且整个流程不读写浏览器 Cookie。
413
+
405
414
  **服务端配置:**
406
415
 
407
416
  ```ts
@@ -416,6 +425,12 @@ const app = serv.createApp({
416
425
  // maxAge: 30 * 24 * 3600, // 默认 30 天
417
426
  // secure: true, // 默认在 NODE_ENV=production 时开启
418
427
  // onRefresh: customFn, // 可覆盖 iam.session.refresh。
428
+ nativeTokenTransport: {
429
+ // 必须 fail-closed:同时校验平台标识与允许的原生 Origin。
430
+ isRequest: request =>
431
+ request.headers.get('x-client-platform') === 'capacitor'
432
+ && security.isNativeOrigin(request.headers.get('origin') ?? ''),
433
+ },
419
434
  },
420
435
  // ✅ 无需再传 verifyToken,iam.session.verifyToken 自动用于填充 context.session
421
436
  })
@@ -440,6 +455,7 @@ await apiClient.init({
440
455
  - `serv.listen(app, options)`:在 Node.js 启动 HTTP 服务,返回 `{ server, address, close }`
441
456
  - `serv.toFetch(app)`:包装为标准 `fetch(Request)` handler
442
457
  - `serv.generateSpec(contract, options)`:由 contract 生成 OpenAPI 3.1 spec
458
+ - `serv.storageAssets(config)`:创建只读 Storage 资源 middleware,内置 key/MIME 白名单、GET/HEAD 与 ETag
443
459
  - `serv.requireAuth(handler)`:procedure 认证包装器(`context.session` 为空 → UNAUTHORIZED)
444
460
  - `serv.requirePermission(perm, handler)`:procedure 权限包装器(缺失权限 → FORBIDDEN)
445
461
  - `serv.requireRole(role, handler)`:procedure 角色包装器(缺失角色 → FORBIDDEN)
@@ -454,6 +470,7 @@ await apiClient.init({
454
470
  - HTTP App 抽象:`serv.createApp()` 返回具备 `fetch/request` 能力的公开应用对象;内部是否使用 Hono 不影响应用代码
455
471
  - `ServMiddlewareMount`:`createApp({ middlewares })` 的挂载项类型(`{ path?, middleware }`)
456
472
  - `ServMiddleware` / `ServProcedureWrapper` / `ServGuardedProcedureWrapper`:自定义 pipeline 时复用的共享类型
473
+ - `NativeRefreshTokenTransportConfig` / `ServStorageAssetsConfig`:原生 Token 通道与只读资源 middleware 配置
457
474
 
458
475
  > 传输加密不作为 `serv.xxx` 扁平 API 暴露;它是 `createApp` 的配置能力,内部委托 `crypto.transport`。
459
476
 
@@ -3,7 +3,7 @@ import * as zod_v4_core from 'zod/v4/core';
3
3
  import * as zod from 'zod';
4
4
  import * as _orpc_server from '@orpc/server';
5
5
  import { AIFunctions } from '@h-ai/ai';
6
- import { a as ServContext } from '../serv-context-CatIx8r-.js';
6
+ import { a as ServContext } from '../serv-context-BUrtWoFC.js';
7
7
  import '@h-ai/core';
8
8
 
9
9
  /** AI 默认 procedures 依赖。 */
@@ -4,7 +4,7 @@ import * as zod from 'zod';
4
4
  import * as _orpc_server from '@orpc/server';
5
5
  import { IamFunctions } from '@h-ai/iam';
6
6
  export { TokenPair } from '@h-ai/iam';
7
- import { a as ServContext } from '../serv-context-CatIx8r-.js';
7
+ import { a as ServContext } from '../serv-context-BUrtWoFC.js';
8
8
  import '@h-ai/core';
9
9
 
10
10
  /** IAM 默认 procedures 依赖。 */
@@ -3,7 +3,7 @@ import * as zod_v4_core from 'zod/v4/core';
3
3
  import * as zod from 'zod';
4
4
  import * as _orpc_server from '@orpc/server';
5
5
  import { StorageFunctions } from '@h-ai/storage';
6
- import { a as ServContext } from '../serv-context-CatIx8r-.js';
6
+ import { a as ServContext } from '../serv-context-BUrtWoFC.js';
7
7
  import '@h-ai/core';
8
8
 
9
9
  /** Storage 默认 procedures 依赖。 */
package/dist/index.d.ts CHANGED
@@ -3,13 +3,14 @@ import { HaiResult, ValidationFormError } from '@h-ai/core';
3
3
  import { Router, implement } from '@orpc/server';
4
4
  import { AIFunctions, AudioWsStartMessage, AudioFormat } from '@h-ai/ai';
5
5
  import { AnyContractRouter, OpenAPI } from '@orpc/contract';
6
- import { S as ServSession, a as ServContext, b as ServIam, R as RefreshCookieConfig, C as CreateServContext, p as parseRequestContext, c as buildAuthContextFactory } from './serv-context-CatIx8r-.js';
7
- export { d as CreateServContextInput, e as RefreshTokenPair } from './serv-context-CatIx8r-.js';
6
+ import { S as ServSession, a as ServContext, b as ServIam, R as RefreshCookieConfig, C as CreateServContext, p as parseRequestContext, c as buildAuthContextFactory } from './serv-context-BUrtWoFC.js';
7
+ export { d as CreateServContextInput, N as NativeRefreshTokenTransportConfig, e as RefreshTokenPair } from './serv-context-BUrtWoFC.js';
8
8
  import { MiddlewareHandler } from 'hono';
9
9
  import { z, ZodType } from 'zod';
10
10
  import { CryptoFunctions, TransportKeyStore } from '@h-ai/crypto';
11
11
  import { ServerType } from '@hono/node-server';
12
12
  import { AddressInfo } from 'node:net';
13
+ import { StorageFunctions } from '@h-ai/storage';
13
14
 
14
15
  /**
15
16
  * @h-ai/serv — 统一语音 WebSocket 接入
@@ -323,9 +324,9 @@ interface ServHttpApp {
323
324
  *
324
325
  * - **`iam`**(推荐):单一顶层引用。提供后 serv 自动:
325
326
  * 1. 使用 `iam.session.verifyToken` 填充 `context.session` → `requireAuth/Permission/Role` 生效
326
- * 2. 若同时启用 `refreshCookie`,使用 `iam.session.refresh` 实现 cookie 刷新
327
+ * 2. 若同时启用 `refreshCookie`,使用 `iam.session.refresh` 实现 refresh token 轮换
327
328
  *
328
- * - **`refreshCookie`**(可选):opt-in httpOnly cookie 传输 refresh token。与 `iam` 正交:
329
+ * - **`refreshCookie`**(可选):浏览器默认用 httpOnly cookie;受信原生客户端可配置 body 通道。与 `iam` 正交:
329
330
  * `iam` 决定 *谁* 验证 token,`refreshCookie` 决定 refresh token *怎么* 传输。
330
331
  *
331
332
  * - **`verifyToken`**(高级):不使用 IAM 模块的逆脱口。优先于 `iam.session.verifyToken`。
@@ -355,7 +356,7 @@ interface CreateServAppOptions<TContract extends AnyContractRouter, TProcedures
355
356
  */
356
357
  readonly iam?: ServIam;
357
358
  /**
358
- * 启用 httpOnly Cookie 传输 refresh token(opt-in)。
359
+ * 启用 refresh token 传输(opt-in):浏览器使用 httpOnly Cookie,可选配置受信原生 body 通道。
359
360
  * 需配合 `iam` 或 `refreshCookie.onRefresh` 使用。
360
361
  */
361
362
  readonly refreshCookie?: RefreshCookieConfig;
@@ -772,6 +773,57 @@ interface ServRuntimeSecurityPolicy {
772
773
  */
773
774
  declare function createRuntimeSecurityPolicy(input: ServRuntimeSecurityPolicyInput): ServRuntimeSecurityPolicy;
774
775
 
776
+ /**
777
+ * @h-ai/serv — Storage 只读资源 middleware
778
+ *
779
+ * 将经过路径与 MIME 白名单校验的 `@h-ai/storage` 文件作为 HTTP 资源返回,
780
+ * 统一处理 GET/HEAD、ETag 与缓存响应头,不包含头像等业务领域概念。
781
+ * @module serv-storage-assets
782
+ */
783
+
784
+ /** 可由只读资源 middleware 使用的最小 Storage 能力。 */
785
+ interface StorageAssetSource {
786
+ readonly file: Pick<StorageFunctions['file'], 'get' | 'head'>;
787
+ }
788
+ /** Storage 只读资源 middleware 配置。 */
789
+ interface ServStorageAssetsConfig {
790
+ /** 已初始化的 Storage 模块或等价文件读取能力。 */
791
+ readonly storage: StorageAssetSource;
792
+ /**
793
+ * 从请求 pathname 中移除的路由前缀,例如 `/api/v1/assets/`。
794
+ * 是否挂载通配符仍由 `createApp({ middlewares })` 的 `path` 决定。
795
+ */
796
+ readonly pathPrefix: `/${string}`;
797
+ /** 允许访问的完整 Storage key 白名单;框架仍会先拒绝路径穿越与非法 key。 */
798
+ readonly keyPattern: RegExp;
799
+ /** 允许返回的 MIME 白名单。 */
800
+ readonly allowedContentTypes: readonly string[];
801
+ /** Cache-Control 响应头,默认 `no-store`。 */
802
+ readonly cacheControl?: string;
803
+ /** 可选的 Cross-Origin-Resource-Policy 响应头。 */
804
+ readonly crossOriginResourcePolicy?: 'same-origin' | 'same-site' | 'cross-origin';
805
+ }
806
+ /**
807
+ * 创建由 Storage 提供内容的只读资源 middleware。
808
+ *
809
+ * 仅允许 GET/HEAD;非法路径、非白名单 MIME 与 Storage 读取失败统一返回 404,
810
+ * 避免向外暴露文件是否存在或后端错误细节。
811
+ *
812
+ * @param config - Storage、路径、key、MIME 与缓存策略。
813
+ * @returns 可挂载到 `serv.createApp({ middlewares })` 的 middleware。
814
+ * @example
815
+ * ```ts
816
+ * const middleware = serv.storageAssets({
817
+ * storage,
818
+ * pathPrefix: '/api/v1/avatar-assets/',
819
+ * keyPattern: /^avatars\/[\w-]+\/[\w-]+\.webp$/,
820
+ * allowedContentTypes: ['image/webp'],
821
+ * cacheControl: 'public, max-age=31536000, immutable',
822
+ * })
823
+ * ```
824
+ */
825
+ declare function storageAssets(config: ServStorageAssetsConfig): ServMiddleware;
826
+
775
827
  /**
776
828
  * @h-ai/serv — 校验工具
777
829
  *
@@ -848,6 +900,7 @@ declare const serv: {
848
900
  listen: typeof listen;
849
901
  toFetch: typeof toFetch;
850
902
  cors: typeof cors;
903
+ storageAssets: typeof storageAssets;
851
904
  createRuntimeSecurityPolicy: typeof createRuntimeSecurityPolicy;
852
905
  generateSpec: typeof generateSpec;
853
906
  /** 基于 contract 创建可注入 context 的 procedure builder(封装 `@orpc/server` 的 `implement`)。 */
@@ -862,4 +915,4 @@ declare const serv: {
862
915
  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;
863
916
  };
864
917
 
865
- 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 ServGuardedProcedureWrapper, 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, type ServProcedureWrapper, type ServRpcHttpConfig, type ServRuntimeSecurityPolicy, type ServRuntimeSecurityPolicyInput, ServSession, type ServTransportConfig, type ServTransportRuntimeConfig, type ServTransportRuntimeConfigInput, type ServValidationFailureBody, serv };
918
+ 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 ServGuardedProcedureWrapper, 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, type ServProcedureWrapper, type ServRpcHttpConfig, type ServRuntimeSecurityPolicy, type ServRuntimeSecurityPolicyInput, ServSession, type ServStorageAssetsConfig, type ServTransportConfig, type ServTransportRuntimeConfig, type ServTransportRuntimeConfigInput, type ServValidationFailureBody, serv };
package/dist/index.js CHANGED
@@ -759,7 +759,8 @@ function resolveRefreshCookieConfig(apiPrefix, config, iam) {
759
759
  maxAge: config.maxAge ?? 30 * 24 * 3600,
760
760
  isSecure: config.secure ?? process2.env.NODE_ENV === "production",
761
761
  refreshCookiePath: `${apiPrefix}${IAM_AUTH_PATHS.refresh}`,
762
- onRefresh
762
+ onRefresh,
763
+ nativeTokenTransport: config.nativeTokenTransport
763
764
  };
764
765
  }
765
766
  function buildSetCookieHeader(name, value, path, maxAge, secure) {
@@ -809,7 +810,14 @@ function stripRefreshTokenFromPair(tokens) {
809
810
  };
810
811
  }
811
812
  function mountRefreshCookieRoutes(app, apiPrefix, config, iam) {
812
- const { cookieName, maxAge, isSecure, refreshCookiePath, onRefresh } = resolveRefreshCookieConfig(apiPrefix, config, iam);
813
+ const {
814
+ cookieName,
815
+ maxAge,
816
+ isSecure,
817
+ refreshCookiePath,
818
+ onRefresh,
819
+ nativeTokenTransport
820
+ } = resolveRefreshCookieConfig(apiPrefix, config, iam);
813
821
  const loginPaths = [
814
822
  `${apiPrefix}${IAM_AUTH_PATHS.login}`,
815
823
  `${apiPrefix}${IAM_AUTH_PATHS.loginWithOtp}`,
@@ -817,6 +825,7 @@ function mountRefreshCookieRoutes(app, apiPrefix, config, iam) {
817
825
  ];
818
826
  for (const path of loginPaths) {
819
827
  app.post(path, async (c, next) => {
828
+ const useNativeTokenTransport = nativeTokenTransport?.isRequest(c.req.raw) === true;
820
829
  await next();
821
830
  if (c.res.status !== 200)
822
831
  return;
@@ -827,11 +836,13 @@ function mountRefreshCookieRoutes(app, apiPrefix, config, iam) {
827
836
  const body = JSON.parse(bodyText);
828
837
  if (body.success) {
829
838
  const refreshToken = extractRefreshTokenFromData(body.data);
830
- if (refreshToken)
831
- headers.append("Set-Cookie", buildSetCookieHeader(cookieName, refreshToken, refreshCookiePath, maxAge, isSecure));
832
- body.data = stripRefreshTokenFromData(body.data);
833
- responseBody = JSON.stringify(body);
834
- headers.delete("Content-Length");
839
+ if (!useNativeTokenTransport) {
840
+ if (refreshToken)
841
+ headers.append("Set-Cookie", buildSetCookieHeader(cookieName, refreshToken, refreshCookiePath, maxAge, isSecure));
842
+ body.data = stripRefreshTokenFromData(body.data);
843
+ responseBody = JSON.stringify(body);
844
+ headers.delete("Content-Length");
845
+ }
835
846
  }
836
847
  } catch {
837
848
  }
@@ -839,6 +850,7 @@ function mountRefreshCookieRoutes(app, apiPrefix, config, iam) {
839
850
  });
840
851
  }
841
852
  app.post(`${apiPrefix}${IAM_AUTH_PATHS.logout}`, async (c, next) => {
853
+ const useNativeTokenTransport = nativeTokenTransport?.isRequest(c.req.raw) === true;
842
854
  await next();
843
855
  if (c.res.status !== 200)
844
856
  return;
@@ -846,14 +858,15 @@ function mountRefreshCookieRoutes(app, apiPrefix, config, iam) {
846
858
  const headers = new Headers(c.res.headers);
847
859
  try {
848
860
  const body = JSON.parse(bodyText);
849
- if (body.success)
861
+ if (body.success && !useNativeTokenTransport)
850
862
  headers.append("Set-Cookie", buildClearCookieHeader(cookieName, refreshCookiePath));
851
863
  } catch {
852
864
  }
853
865
  c.res = new Response(bodyText, { status: c.res.status, statusText: c.res.statusText, headers });
854
866
  });
855
867
  app.post(`${apiPrefix}${IAM_AUTH_PATHS.refresh}`, async (c) => {
856
- const refreshToken = getCookie(c, cookieName);
868
+ const useNativeTokenTransport = nativeTokenTransport?.isRequest(c.req.raw) === true;
869
+ const refreshToken = useNativeTokenTransport ? await readRefreshTokenFromBody(c.req.raw) : getCookie(c, cookieName);
857
870
  if (!refreshToken) {
858
871
  const locale = resolveRequestLocale(c.req.raw.headers);
859
872
  return c.json(buildHaiErrorBody(HaiCommonError.UNAUTHORIZED, servM("serv_errorUnauthorized", { locale })), 401);
@@ -861,16 +874,21 @@ function mountRefreshCookieRoutes(app, apiPrefix, config, iam) {
861
874
  const result = await onRefresh(refreshToken);
862
875
  if (!result.success) {
863
876
  const status = result.error.httpStatus ?? 401;
864
- const clearCookie = buildClearCookieHeader(cookieName, refreshCookiePath);
877
+ const headers = new Headers({ "Content-Type": "application/json" });
878
+ if (!useNativeTokenTransport)
879
+ headers.set("Set-Cookie", buildClearCookieHeader(cookieName, refreshCookiePath));
865
880
  return new Response(JSON.stringify({ success: false, error: result.error }), {
866
881
  status,
867
- headers: {
868
- "Content-Type": "application/json",
869
- "Set-Cookie": clearCookie
870
- }
882
+ headers
871
883
  });
872
884
  }
873
885
  const tokens = result.data;
886
+ if (useNativeTokenTransport) {
887
+ return new Response(JSON.stringify({ success: true, data: { tokens } }), {
888
+ status: 200,
889
+ headers: { "Content-Type": "application/json" }
890
+ });
891
+ }
874
892
  const setCookie = buildSetCookieHeader(cookieName, tokens.refreshToken, refreshCookiePath, maxAge, isSecure);
875
893
  return new Response(JSON.stringify({ success: true, data: { tokens: stripRefreshTokenFromPair(tokens) } }), {
876
894
  status: 200,
@@ -881,6 +899,10 @@ function mountRefreshCookieRoutes(app, apiPrefix, config, iam) {
881
899
  });
882
900
  });
883
901
  }
902
+ async function readRefreshTokenFromBody(request) {
903
+ const body = await request.json().catch(() => void 0);
904
+ return typeof body?.refreshToken === "string" && body.refreshToken.length > 0 ? body.refreshToken : void 0;
905
+ }
884
906
  var SCALAR_ROUTE = "/_hai/scalar.js";
885
907
  var SCALAR_BROWSER_ENTRY_FALLBACK = "./dist/browser/standalone.js";
886
908
  var scalarScriptCache;
@@ -1470,6 +1492,68 @@ function tryNormalizeNativeOrigin(value) {
1470
1492
  return ["capacitor:", "tauri:"].includes(url.protocol) ? `${url.protocol}//${url.host}` : url.origin;
1471
1493
  }
1472
1494
 
1495
+ // src/serv-storage-assets.ts
1496
+ function storageAssets(config) {
1497
+ const pathPrefix = config.pathPrefix.endsWith("/") ? config.pathPrefix : `${config.pathPrefix}/`;
1498
+ const allowedKeyPattern = new RegExp(config.keyPattern.source, config.keyPattern.flags.replace(/[gy]/g, ""));
1499
+ const allowedContentTypes = new Set(config.allowedContentTypes);
1500
+ return async (c) => {
1501
+ const request = c.req.raw;
1502
+ const path = new URL(request.url).pathname;
1503
+ if (request.method !== "GET" && request.method !== "HEAD")
1504
+ return c.body(null, 405, { Allow: "GET, HEAD" });
1505
+ if (!path.startsWith(pathPrefix))
1506
+ return notFound(c);
1507
+ const key = decodeStorageAssetKey(path.slice(pathPrefix.length), allowedKeyPattern);
1508
+ if (!key)
1509
+ return notFound(c);
1510
+ const metadata = await config.storage.file.head(key);
1511
+ if (!metadata.success || !allowedContentTypes.has(metadata.data.contentType))
1512
+ return notFound(c);
1513
+ const headers = {
1514
+ "Cache-Control": config.cacheControl ?? "no-store",
1515
+ "Content-Length": String(metadata.data.size),
1516
+ "Content-Type": metadata.data.contentType,
1517
+ ...config.crossOriginResourcePolicy ? { "Cross-Origin-Resource-Policy": config.crossOriginResourcePolicy } : {},
1518
+ ...metadata.data.etag ? { ETag: metadata.data.etag } : {}
1519
+ };
1520
+ if (metadata.data.etag && matchesEtag(request.headers.get("if-none-match"), metadata.data.etag))
1521
+ return c.body(null, 304, headers);
1522
+ if (request.method === "HEAD")
1523
+ return c.body(null, 200, headers);
1524
+ const file = await config.storage.file.get(key);
1525
+ if (!file.success)
1526
+ return notFound(c);
1527
+ return c.body(Uint8Array.from(file.data).buffer, 200, headers);
1528
+ };
1529
+ }
1530
+ function decodeStorageAssetKey(value, allowedKeyPattern) {
1531
+ try {
1532
+ const key = decodeURIComponent(value);
1533
+ if (key.length === 0 || key.length > 1024 || key.startsWith("/") || key.includes("\\") || key.includes("\0") || key.split("/").some((segment) => segment === "." || segment === "..")) {
1534
+ return void 0;
1535
+ }
1536
+ const match = allowedKeyPattern.exec(key);
1537
+ return match?.[0] === key ? key : void 0;
1538
+ } catch {
1539
+ return void 0;
1540
+ }
1541
+ }
1542
+ function matchesEtag(ifNoneMatch, etag) {
1543
+ if (!ifNoneMatch)
1544
+ return false;
1545
+ if (ifNoneMatch.trim() === "*")
1546
+ return true;
1547
+ const expected = normalizeEtag(etag);
1548
+ return ifNoneMatch.split(",").some((candidate) => normalizeEtag(candidate) === expected);
1549
+ }
1550
+ function normalizeEtag(value) {
1551
+ return value.trim().replace(/^W\//i, "");
1552
+ }
1553
+ function notFound(c) {
1554
+ return c.body(null, 404, { "Cache-Control": "no-store" });
1555
+ }
1556
+
1473
1557
  // src/serv-main.ts
1474
1558
  var serv = {
1475
1559
  // 应用与上下文
@@ -1481,6 +1565,7 @@ var serv = {
1481
1565
  listen,
1482
1566
  toFetch,
1483
1567
  cors,
1568
+ storageAssets,
1484
1569
  createRuntimeSecurityPolicy,
1485
1570
  // OpenAPI / 导出
1486
1571
  generateSpec,