@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 +22 -5
- package/dist/features/ai.d.ts +1 -1
- package/dist/features/iam.d.ts +1 -1
- package/dist/features/storage.d.ts +1 -1
- package/dist/index.d.ts +59 -6
- package/dist/index.js +99 -14
- package/dist/index.js.map +1 -1
- package/dist/{serv-context-CatIx8r-.d.ts → serv-context-BUrtWoFC.d.ts} +30 -11
- package/package.json +10 -10
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
-
|
|
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
|
|
package/dist/features/ai.d.ts
CHANGED
|
@@ -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-
|
|
6
|
+
import { a as ServContext } from '../serv-context-BUrtWoFC.js';
|
|
7
7
|
import '@h-ai/core';
|
|
8
8
|
|
|
9
9
|
/** AI 默认 procedures 依赖。 */
|
package/dist/features/iam.d.ts
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
7
|
-
export { d as CreateServContextInput, e as RefreshTokenPair } from './serv-context-
|
|
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` 实现
|
|
327
|
+
* 2. 若同时启用 `refreshCookie`,使用 `iam.session.refresh` 实现 refresh token 轮换
|
|
327
328
|
*
|
|
328
|
-
* - **`refreshCookie
|
|
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
|
-
* 启用
|
|
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 {
|
|
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 (
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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
|
|
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
|
|
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,
|