@h-ai/kit 0.1.0-alpha.13 → 0.1.0-alpha.16

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
@@ -1,11 +1,13 @@
1
1
  # @h-ai/kit
2
2
 
3
- `@h-ai/kit` 是 hai-framework 的 SvelteKit 集成模块,提供统一的请求管道与 API 工具:
3
+ `@h-ai/kit` 是 hai-framework 的 SvelteKit 集成模块,提供统一的请求管道与同源 endpoint 工具:
4
4
 
5
5
  - `kit.createHandle()`:会话解析、路由守卫、中间件链、可选传输加密与 Cookie 加密
6
6
  - `kit.handler()`:API 端点异常边界
7
7
  - `kit.guard` / `kit.response` / `kit.validate` / `kit.auth` / `kit.client` / `kit.crud`
8
8
 
9
+ > 公共跨端 HTTP API contract 统一放在 `@h-ai/api-contract`,由 `@h-ai/serv` 挂载,客户端通过 `@h-ai/api-client` typed client 调用;`@h-ai/kit` 不定义也不承载业务 API contract。
10
+
9
11
  ## 安装
10
12
 
11
13
  ```bash
@@ -22,7 +24,6 @@ npm install -D @sveltejs/vite-plugin-svelte vite
22
24
  - 统一 API 响应与请求验证(Zod)
23
25
  - Bearer Token 工具(服务端 login/logout + 浏览器端 Token 存储)
24
26
  - 浏览器端统一客户端(自动 CSRF,支持传输加密)
25
- - 基于 EndpointDef 的契约模式(`fromContract`)
26
27
  - 声明式 CRUD 资源定义(`crud.define`)
27
28
  - A2A 协议集成
28
29
  - 双构建模式(`createAdapter()`)
@@ -93,14 +94,6 @@ const haiHandle = kit.createHandle({
93
94
  export const handle: Handle = haiHandle
94
95
  ```
95
96
 
96
- **src/hooks.client.ts**:
97
-
98
- ```typescript
99
- import { kit } from '@h-ai/kit'
100
-
101
- export const handleFetch = kit.auth.createHandleFetch()
102
- ```
103
-
104
97
  ### API 端点
105
98
 
106
99
  ```typescript
@@ -119,22 +112,10 @@ export const POST = kit.handler(async ({ request, locals }) => {
119
112
  })
120
113
  ```
121
114
 
122
- ### 契约模式(推荐)
123
-
124
- ```typescript
125
- import { iamEndpoints } from '@h-ai/iam/api'
126
- import { kit } from '@h-ai/kit'
127
-
128
- export const POST = kit.fromContract(iamEndpoints.login, async (input) => {
129
- const result = await iam.auth.login(input)
130
- if (!result.success)
131
- return kit.response.unauthorized(result.error.message)
132
- return kit.response.ok(result.data)
133
- })
134
- ```
135
-
136
115
  ### 浏览器客户端
137
116
 
117
+ `kit.client.create()` 面向 SvelteKit 同源 endpoint;Web / App / 小程序跨域访问公共 API 时,请使用 `@h-ai/api-client`。
118
+
138
119
  ```typescript
139
120
  // src/lib/utils/api.ts — 全应用共享
140
121
  import { kit } from '@h-ai/kit'
@@ -152,6 +133,91 @@ const response = await apiFetch('/api/users', { method: 'GET' })
152
133
 
153
134
  > CSRF Header 注入由 `apiFetch` 自动处理;`@h-ai/kit` 不提供独立的 CSRF 中间件工厂。
154
135
 
136
+ ### 同源传输加密
137
+
138
+ kit 的传输加密也统一委托 `@h-ai/crypto`:服务端通过 `crypto.transport.createServer()` 创建管理器,浏览器端通过 `kit.client.installBrowserTransport(kitConfig, { crypto })` 一行安装同源 fetch 包装。应用层只配置顶层 `crypto` 句柄。
139
+
140
+ **src/hooks.server.ts**:
141
+
142
+ ```ts
143
+ import { crypto } from '@h-ai/crypto'
144
+ import { kit } from '@h-ai/kit'
145
+
146
+ await crypto.init()
147
+
148
+ export const handle = kit.createHandle({
149
+ auth: {
150
+ verifyToken,
151
+ protectedPaths: ['/api/*'],
152
+ publicPaths: ['/api/_hai/*'], // 默认密钥协商端点
153
+ },
154
+ crypto: {
155
+ crypto,
156
+ transport: { requireEncryption: true },
157
+ },
158
+ })
159
+ ```
160
+
161
+ **src/routes/+layout.svelte**(推荐入口:浏览器端只需一行):
162
+
163
+ ```svelte
164
+ <script lang='ts'>
165
+ import { browser } from '$app/environment'
166
+ import { appKitConfig } from '$lib/config/kit-config'
167
+ import { crypto } from '@h-ai/crypto'
168
+ import { kit } from '@h-ai/kit'
169
+
170
+ // 一次性安装:内部按 _kit.yml 决定是否启用 transport,并预热 crypto
171
+ if (browser) {
172
+ kit.client.installBrowserTransport(appKitConfig, { crypto })
173
+ }
174
+ </script>
175
+ ```
176
+
177
+ **src/lib/utils/api.ts**(业务层只看到 `apiFetch`):
178
+
179
+ ```ts
180
+ import { kit } from '@h-ai/kit'
181
+
182
+ export const { apiFetch } = kit.client.create()
183
+ ```
184
+
185
+ > 不再需要 `src/hooks.client.ts`:默认 `handleFetch` 行为已经够用,SvelteKit 内部 `__data.json` 请求会自动走上面安装的全局 fetch 包装。同源 Authorization 注入统一通过 `kit.client.create({ auth: true })` 完成,避免浏览器端 hooks 与全局 fetch 包装形成两条链路。
186
+
187
+ 默认密钥协商路径为 `/api/_hai/key-exchange`。如服务端自定义 `transport.keyExchangePath`,客户端需同步配置 `transport.keyExchangeUrl`。
188
+
189
+ transport 默认作用于同源 `/api/*` endpoint 与 SvelteKit `__data.json` 页面数据请求(以及密钥协商端点本身);页面文档与静态资源仍保持明文,不要求携带 `X-Client-Id`。文件上传等 `multipart/form-data` 请求会保持原样发送,需在服务端 `excludePaths` 中显式放行对应上传路径。
190
+
191
+ 安全策略默认 fail-closed:`requireEncryption: true` 时,受保护路径缺少 `X-Client-Id` 会直接返回 400;服务端 transport 管理器不可用、响应体无法加密或超过单次加密上限时返回错误,不会把业务明文响应透传给客户端。`requireEncryption: false` 仅用于迁移期灰度,不建议在生产启用。
192
+
193
+ ### 使用 `_kit.yml` 统一 transport 配置
194
+
195
+ `@h-ai/kit` 提供 `KitConfigSchema` / `resolveKitConfig()`,方便应用用同一份 `_kit.yml`
196
+ 同时驱动 `hooks.server.ts` 与浏览器端 `kit.client.installBrowserTransport()`:
197
+
198
+ ```yml
199
+ transport:
200
+ keyExchangePath: /api/_hai/key-exchange
201
+ requireEncryption: true
202
+ encryptResponse: true
203
+ excludePaths:
204
+ - /api/storage
205
+ - /api/public
206
+ - /api/auth/profile/avatar
207
+ maxClients: 10000
208
+ ```
209
+
210
+ ```ts
211
+ import { resolveKitConfig } from '@h-ai/kit'
212
+ import { parse } from 'yaml'
213
+ import rawKitConfig from '../config/_kit.yml?raw'
214
+
215
+ export const appKitConfig = resolveKitConfig(parse(rawKitConfig) ?? {})
216
+ ```
217
+
218
+ 如只在服务端读取配置,也可配合 `core.config.validate('kit', KitConfigSchema)` 使用。
219
+ 由于浏览器端也可能读取这份配置,`_kit.yml` 只应放公开的 transport 路径/开关,不要写入密钥。
220
+
155
221
  ## 配置
156
222
 
157
223
  `kit.createHandle()` 配置项:
@@ -169,14 +235,12 @@ const response = await apiFetch('/api/users', { method: 'GET' })
169
235
 
170
236
  ## 导出路径
171
237
 
172
- | 路径 | 用途 |
173
- | ------------------- | ---------------------------------------------- |
174
- | `@h-ai/kit` | 主入口:`kit` 命名空间、类型、`defineEndpoint` |
175
- | `@h-ai/kit/client` | 浏览器端客户端 |
176
- | `@h-ai/kit/adapter` | 双构建 adapter(`createAdapter()`) |
177
- | `@h-ai/kit/vite` | Vite 插件 |
178
- | `@h-ai/kit/a2a` | A2A 协议集成 |
179
- | `@h-ai/kit/crypto` | 加密模块集成 |
238
+ - `@h-ai/kit`:主入口,提供 `kit` 命名空间与类型。
239
+ - `@h-ai/kit/client`:浏览器端客户端。
240
+ - `@h-ai/kit/adapter`:双构建 adapter(`createAdapter()`)。
241
+ - `@h-ai/kit/vite`:Vite 插件。
242
+ - `@h-ai/kit/a2a`:A2A 协议集成。
243
+ - `@h-ai/kit/crypto`:加密模块集成。
180
244
 
181
245
  ## 错误处理
182
246
 
package/dist/browser.d.ts CHANGED
@@ -1,11 +1,12 @@
1
- import { c as createKitClient, s as setBrowserToken, a as clearBrowserToken, b as createTokenStore, d as createHandleFetch } from './index-BotXLTck.js';
2
- export { C as ClientTransportConfig, K as KitClient, e as KitClientConfig } from './index-BotXLTck.js';
1
+ import { c as createKitClient, i as installBrowserTransport, a as installBrowserTransportFetch, s as setBrowserToken, b as clearBrowserToken, d as createTokenStore } from './index-M00CsNUR.js';
2
+ export { C as ClientTransportConfig, K as KitClient, e as KitClientConfig, f as KitConfig, g as KitConfigInput, h as KitConfigSchema, j as KitTransportConfig, k as KitTransportConfigInput, l as createSvelteKitNavAdapter, r as resolveKitConfig, m as resolveKitTransportConfig } from './index-M00CsNUR.js';
3
3
  import { d as defineCrud, s as setAllModulesLocale } from './kit-i18n-CgpNTh9G.js';
4
- export { A as ApiResponse, a as AuthOperations, b as AuthOperationsProvider, C as CorsConfig, F as FormError, c as FormValidationResult, G as GuardConfig, d as GuardResult, H as HaiRequestEvent, e as HandleA2AConfig, f as HandleA2AOperations, g as HandleAuthConfig, h as HandleConfig, i as HookCryptoConfig, M as Middleware, j as MiddlewareContext, R as RateLimitConfig, k as RouteGuard, S as SessionData, l as SessionLike, T as TransportEncryptionOptions } from './kit-types-BHlDNNpd.js';
4
+ export { A as ApiResponse, a as AuthOperations, b as AuthOperationsProvider, C as CorsConfig, F as FormError, c as FormValidationResult, G as GuardConfig, d as GuardResult, H as HaiRequestEvent, e as HandleA2AConfig, f as HandleA2AOperations, g as HandleAuthConfig, h as HandleConfig, i as HookCryptoConfig, M as Middleware, j as MiddlewareContext, R as RateLimitConfig, k as RouteGuard, S as SessionData, l as SessionLike, T as TransportEncryptionOptions } from './kit-types-XUMggtk_.js';
5
+ import '@h-ai/crypto';
5
6
  import '@h-ai/core';
6
7
  import '@h-ai/iam';
8
+ import 'zod';
7
9
  import '@sveltejs/kit';
8
- import './kit-crypto-types-C-gEvx04.js';
9
10
 
10
11
  /**
11
12
  * @h-ai/kit — 浏览器入口
@@ -20,6 +21,8 @@ import './kit-crypto-types-C-gEvx04.js';
20
21
  declare const kit: {
21
22
  client: {
22
23
  create: typeof createKitClient;
24
+ installBrowserTransport: typeof installBrowserTransport;
25
+ installBrowserTransportFetch: typeof installBrowserTransportFetch;
23
26
  };
24
27
  crud: {
25
28
  define: typeof defineCrud;
@@ -28,11 +31,10 @@ declare const kit: {
28
31
  setBrowserToken: typeof setBrowserToken;
29
32
  clearBrowserToken: typeof clearBrowserToken;
30
33
  createTokenStore: typeof createTokenStore;
31
- createHandleFetch: typeof createHandleFetch;
32
34
  };
33
35
  i18n: {
34
36
  setLocale: typeof setAllModulesLocale;
35
37
  };
36
38
  };
37
39
 
38
- export { createKitClient, kit };
40
+ export { createKitClient, installBrowserTransport, installBrowserTransportFetch, kit };
package/dist/browser.js CHANGED
@@ -1,11 +1,14 @@
1
- import { defineCrud } from './chunk-FRYJMRTP.js';
2
- import { createHandleFetch, createTokenStore, clearBrowserToken, setBrowserToken, createKitClient } from './chunk-GRYHRXVC.js';
3
- import { setAllModulesLocale } from './chunk-BRZK7V5O.js';
1
+ import { defineCrud } from './chunk-KJUUEH7I.js';
2
+ export { KitConfigSchema, resolveKitConfig, resolveKitTransportConfig } from './chunk-KJUUEH7I.js';
3
+ import { createTokenStore, clearBrowserToken, setBrowserToken, installBrowserTransportFetch, installBrowserTransport, createKitClient } from './chunk-EMSCTFJJ.js';
4
+ import { setAllModulesLocale } from './chunk-MI65L22O.js';
4
5
 
5
6
  // src/browser.ts
6
7
  var kit = {
7
8
  client: {
8
- create: createKitClient
9
+ create: createKitClient,
10
+ installBrowserTransport,
11
+ installBrowserTransportFetch
9
12
  },
10
13
  crud: {
11
14
  define: defineCrud
@@ -13,8 +16,7 @@ var kit = {
13
16
  auth: {
14
17
  setBrowserToken,
15
18
  clearBrowserToken,
16
- createTokenStore,
17
- createHandleFetch
19
+ createTokenStore
18
20
  },
19
21
  i18n: {
20
22
  setLocale: setAllModulesLocale
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/browser.ts"],"names":[],"mappings":";;;;;AAeO,IAAM,GAAA,GAAM;AAAA,EACjB,MAAA,EAAQ;AAAA,IACN,MAAA,EAAQ;AAAA,GACV;AAAA,EACA,IAAA,EAAM;AAAA,IACJ,MAAA,EAAQ;AAAA,GACV;AAAA,EACA,IAAA,EAAM;AAAA,IACJ,eAAA;AAAA,IACA,iBAAA;AAAA,IACA,gBAAA;AAAA,IACA;AAAA,GACF;AAAA,EACA,IAAA,EAAM;AAAA,IACJ,SAAA,EAAW;AAAA;AAEf","file":"browser.js","sourcesContent":["/**\n * @h-ai/kit — 浏览器入口\n *\n * 为客户端提供精简且安全的运行时导出,避免将服务端 Hook/A2A/IAM 运行时代码打入浏览器包。\n * @module browser\n */\n\nimport { createKitClient } from './client/kit-client.js'\nimport { defineCrud } from './crud/kit-crud.js'\nimport { clearBrowserToken, createHandleFetch, createTokenStore, setBrowserToken } from './kit-auth.js'\nimport { setAllModulesLocale } from './kit-i18n.js'\n\n/**\n * 浏览器端 kit 对象(仅暴露 client-safe 能力)\n */\nexport const kit = {\n client: {\n create: createKitClient,\n },\n crud: {\n define: defineCrud,\n },\n auth: {\n setBrowserToken,\n clearBrowserToken,\n createTokenStore,\n createHandleFetch,\n },\n i18n: {\n setLocale: setAllModulesLocale,\n },\n}\n\nexport type * from './client/index.js'\nexport type * from './kit-types.js'\n"]}
1
+ {"version":3,"sources":["../src/browser.ts"],"names":[],"mappings":";;;;;;AAgBO,IAAM,GAAA,GAAM;AAAA,EACjB,MAAA,EAAQ;AAAA,IACN,MAAA,EAAQ,eAAA;AAAA,IACR,uBAAA;AAAA,IACA;AAAA,GACF;AAAA,EACA,IAAA,EAAM;AAAA,IACJ,MAAA,EAAQ;AAAA,GACV;AAAA,EACA,IAAA,EAAM;AAAA,IACJ,eAAA;AAAA,IACA,iBAAA;AAAA,IACA;AAAA,GACF;AAAA,EACA,IAAA,EAAM;AAAA,IACJ,SAAA,EAAW;AAAA;AAEf","file":"browser.js","sourcesContent":["/**\n * @h-ai/kit — 浏览器入口\n *\n * 为客户端提供精简且安全的运行时导出,避免将服务端 Hook/A2A/IAM 运行时代码打入浏览器包。\n * @module browser\n */\n\nimport { createKitClient, installBrowserTransport, installBrowserTransportFetch } from './client/kit-client.js'\nimport { defineCrud } from './crud/kit-crud.js'\nimport { clearBrowserToken, createTokenStore, setBrowserToken } from './kit-auth.js'\nimport { KitConfigSchema, resolveKitConfig, resolveKitTransportConfig } from './kit-config.js'\nimport { setAllModulesLocale } from './kit-i18n.js'\n\n/**\n * 浏览器端 kit 对象(仅暴露 client-safe 能力)\n */\nexport const kit = {\n client: {\n create: createKitClient,\n installBrowserTransport,\n installBrowserTransportFetch,\n },\n crud: {\n define: defineCrud,\n },\n auth: {\n setBrowserToken,\n clearBrowserToken,\n createTokenStore,\n },\n i18n: {\n setLocale: setAllModulesLocale,\n },\n}\n\nexport { KitConfigSchema, resolveKitConfig, resolveKitTransportConfig }\n\nexport type * from './client/index.js'\nexport type * from './kit-types.js'\n"]}
@@ -0,0 +1,211 @@
1
+ import { DEFAULT_TRANSPORT_KEY_EXCHANGE_PATH, shouldHandleTransportPath, shouldExcludeTransportPath, kitM } from './chunk-MI65L22O.js';
2
+ import { TRANSPORT_PROTOCOL } from '@h-ai/crypto';
3
+
4
+ var MAX_ENCRYPTED_BODY = 1048576;
5
+ var ENCRYPTABLE_RESPONSE_CONTENT_TYPES = ["application/json", "text/sveltekit-data"];
6
+ function transportEncryptionMiddleware(config) {
7
+ if (!config.enabled) {
8
+ return async (_context, next) => next();
9
+ }
10
+ const keyExchangePath = config.keyExchangePath ?? DEFAULT_TRANSPORT_KEY_EXCHANGE_PATH;
11
+ const excludePaths = config.excludePaths ?? [];
12
+ const encryptResponse = config.encryptResponse ?? true;
13
+ const requireEncryption = config.requireEncryption ?? true;
14
+ const result = config.crypto.transport.createServer({ maxClients: config.maxClients });
15
+ if (!result.success) {
16
+ return createUnavailableTransportMiddleware(keyExchangePath, excludePaths);
17
+ }
18
+ const manager = result.data;
19
+ return async (context, next) => {
20
+ const { event } = context;
21
+ const pathname = getRequestPathname(event.request);
22
+ if (pathname === keyExchangePath && event.request.method === "POST") {
23
+ return handleKeyExchange(manager, event.request);
24
+ }
25
+ if (!shouldHandleTransportPath(pathname, keyExchangePath)) {
26
+ return next();
27
+ }
28
+ if (shouldExcludeTransportPath(pathname, excludePaths, keyExchangePath)) {
29
+ return next();
30
+ }
31
+ const clientId = event.request.headers.get(TRANSPORT_PROTOCOL.CLIENT_ID_HEADER);
32
+ if (!clientId) {
33
+ if (requireEncryption) {
34
+ return jsonError(kitM("kit_transportClientIdRequired"), 400);
35
+ }
36
+ return next();
37
+ }
38
+ const clientPublicKey = await manager.getClientPublicKey(clientId);
39
+ if (!clientPublicKey) {
40
+ return jsonError(kitM("kit_transportClientKeyNotFound"), 400);
41
+ }
42
+ if (hasBody(event.request.method) && !isRequestBodyEmpty(event.request)) {
43
+ const decryptResult = await decryptRequestBody(event, manager, requireEncryption);
44
+ if (decryptResult) {
45
+ return decryptResult;
46
+ }
47
+ }
48
+ const response = await next();
49
+ if (!encryptResponse) {
50
+ return response;
51
+ }
52
+ if (isEmptyResponse(response)) {
53
+ return response;
54
+ }
55
+ const contentType = response.headers.get("Content-Type") ?? "";
56
+ if (!canEncryptResponseContentType(contentType)) {
57
+ return jsonError(kitM("kit_transportEncryptFailed"), 500);
58
+ }
59
+ const contentLength = response.headers.get("Content-Length");
60
+ if (contentLength && Number.parseInt(contentLength, 10) > MAX_ENCRYPTED_BODY) {
61
+ return jsonError(kitM("kit_transportEncryptFailed"), 500);
62
+ }
63
+ try {
64
+ const responseBody = await response.clone().text();
65
+ if (!responseBody) {
66
+ return response;
67
+ }
68
+ const encryptedPayload = await manager.encryptResponse(clientId, responseBody);
69
+ if (!encryptedPayload.success) {
70
+ return jsonError(kitM("kit_transportEncryptFailed"), 500);
71
+ }
72
+ const headers = new Headers(response.headers);
73
+ headers.set(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER, TRANSPORT_PROTOCOL.ENCRYPTED_HEADER_VALUE);
74
+ headers.delete("Content-Length");
75
+ return new Response(JSON.stringify(encryptedPayload.data), {
76
+ status: response.status,
77
+ statusText: response.statusText,
78
+ headers
79
+ });
80
+ } catch {
81
+ return jsonError(kitM("kit_transportEncryptFailed"), 500);
82
+ }
83
+ };
84
+ }
85
+ function canEncryptResponseContentType(contentType) {
86
+ return ENCRYPTABLE_RESPONSE_CONTENT_TYPES.some((type) => contentType.includes(type));
87
+ }
88
+ function createUnavailableTransportMiddleware(keyExchangePath, excludePaths) {
89
+ return async (context, next) => {
90
+ const pathname = getRequestPathname(context.event.request);
91
+ if (pathname === keyExchangePath) {
92
+ return jsonError(kitM("kit_transportKeyGenerationFailed"), 500);
93
+ }
94
+ if (!shouldHandleTransportPath(pathname, keyExchangePath)) {
95
+ return next();
96
+ }
97
+ if (shouldExcludeTransportPath(pathname, excludePaths, keyExchangePath)) {
98
+ return next();
99
+ }
100
+ return jsonError(kitM("kit_transportKeyGenerationFailed"), 500);
101
+ };
102
+ }
103
+ function isEmptyResponse(response) {
104
+ if ([204, 304].includes(response.status))
105
+ return true;
106
+ return response.headers.get("Content-Length") === "0";
107
+ }
108
+ function getRequestPathname(request) {
109
+ return new URL(request.url).pathname;
110
+ }
111
+ function jsonError(message, status) {
112
+ return new Response(JSON.stringify({ error: message }), {
113
+ status,
114
+ headers: { "Content-Type": "application/json" }
115
+ });
116
+ }
117
+ async function handleKeyExchange(manager, request) {
118
+ try {
119
+ const body = await request.json();
120
+ if (!body.clientPublicKey || typeof body.clientPublicKey !== "string") {
121
+ return jsonError(kitM("kit_transportInvalidPayload"), 400);
122
+ }
123
+ const clientId = await manager.registerClientKey(body.clientPublicKey);
124
+ const serverPublicKey = manager.getServerPublicKey();
125
+ return new Response(
126
+ JSON.stringify({ serverPublicKey, clientId }),
127
+ { status: 200, headers: { "Content-Type": "application/json" } }
128
+ );
129
+ } catch {
130
+ return jsonError(kitM("kit_transportKeyExchangeFailed"), 500);
131
+ }
132
+ }
133
+ function hasBody(method) {
134
+ return ["POST", "PUT", "PATCH", "DELETE"].includes(method.toUpperCase());
135
+ }
136
+ function isRequestBodyEmpty(request) {
137
+ if (request.body === null)
138
+ return true;
139
+ const contentLength = request.headers.get("Content-Length");
140
+ if (contentLength !== null)
141
+ return Number.parseInt(contentLength, 10) === 0;
142
+ return false;
143
+ }
144
+ function replaceRequestBody(event, body) {
145
+ const headers = new Headers(event.request.headers);
146
+ headers.delete(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER);
147
+ headers.delete("Content-Length");
148
+ headers.set("Content-Type", "application/json; charset=utf-8");
149
+ const newRequest = new Request(event.request.url, {
150
+ method: event.request.method,
151
+ headers,
152
+ body
153
+ });
154
+ Object.defineProperty(event, "request", {
155
+ value: newRequest,
156
+ writable: true,
157
+ configurable: true
158
+ });
159
+ }
160
+ async function decryptRequestBody(event, manager, requireEncryption) {
161
+ const isEncrypted = event.request.headers.get(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER) === TRANSPORT_PROTOCOL.ENCRYPTED_HEADER_VALUE;
162
+ if (!isEncrypted) {
163
+ if (!requireEncryption)
164
+ return null;
165
+ return jsonError(kitM("kit_transportInvalidPayload"), 400);
166
+ }
167
+ const contentType = event.request.headers.get("Content-Type") ?? "";
168
+ if (!contentType.includes("application/json")) {
169
+ if (!requireEncryption) {
170
+ return null;
171
+ }
172
+ return jsonError(kitM("kit_transportInvalidPayload"), 400);
173
+ }
174
+ const bodyText = await event.request.text();
175
+ if (!bodyText) {
176
+ return null;
177
+ }
178
+ let payload;
179
+ try {
180
+ payload = JSON.parse(bodyText);
181
+ } catch {
182
+ if (!requireEncryption) {
183
+ replaceRequestBody(event, bodyText);
184
+ return null;
185
+ }
186
+ return jsonError(kitM("kit_transportDecryptFailed"), 400);
187
+ }
188
+ if (!isEncryptedPayloadShape(payload)) {
189
+ if (!requireEncryption) {
190
+ replaceRequestBody(event, bodyText);
191
+ return null;
192
+ }
193
+ return jsonError(kitM("kit_transportInvalidPayload"), 400);
194
+ }
195
+ const plaintext = manager.decryptRequest(payload);
196
+ if (!plaintext.success) {
197
+ return jsonError(kitM("kit_transportDecryptFailed"), 400);
198
+ }
199
+ replaceRequestBody(event, plaintext.data);
200
+ return null;
201
+ }
202
+ function isEncryptedPayloadShape(payload) {
203
+ if (!payload || typeof payload !== "object")
204
+ return false;
205
+ const p = payload;
206
+ return typeof p.encryptedKey === "string" && typeof p.ciphertext === "string" && typeof p.iv === "string";
207
+ }
208
+
209
+ export { transportEncryptionMiddleware };
210
+ //# sourceMappingURL=chunk-BM7IP4W5.js.map
211
+ //# sourceMappingURL=chunk-BM7IP4W5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/modules/crypto/kit-transport-middleware.ts"],"names":[],"mappings":"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","file":"chunk-BM7IP4W5.js","sourcesContent":["/**\n * @h-ai/kit — 传输加密中间件\n *\n * SvelteKit 中间件,自动对请求/响应进行传输加密解密。\n * @module kit-transport-middleware\n */\n\nimport type { EncryptedPayload, TransportEncryptionManager } from '@h-ai/crypto'\nimport type { Middleware } from '../../kit-types.js'\nimport type { TransportEncryptionConfig } from './kit-crypto-types.js'\nimport { TRANSPORT_PROTOCOL } from '@h-ai/crypto'\nimport { kitM } from '../../kit-i18n.js'\nimport { DEFAULT_TRANSPORT_KEY_EXCHANGE_PATH, shouldExcludeTransportPath, shouldHandleTransportPath } from './kit-transport-paths.js'\n\nconst MAX_ENCRYPTED_BODY = 1_048_576\nconst ENCRYPTABLE_RESPONSE_CONTENT_TYPES = ['application/json', 'text/sveltekit-data']\n\n/**\n * 创建传输加密中间件\n *\n * @param config - 传输加密配置\n * @returns SvelteKit 中间件\n *\n * @example\n * ```ts\n * import { crypto } from '@h-ai/crypto'\n * import { kit } from '@h-ai/kit'\n *\n * // 传输加密通过 createHandle 的 crypto 配置自动启用:\n * export const handle = kit.createHandle({\n * crypto: { crypto, transport: true },\n * })\n * ```\n */\nexport function transportEncryptionMiddleware(config: TransportEncryptionConfig): Middleware {\n // 未启用时直接透传\n if (!config.enabled) {\n return async (_context, next) => next()\n }\n\n const keyExchangePath = config.keyExchangePath ?? DEFAULT_TRANSPORT_KEY_EXCHANGE_PATH\n const excludePaths = config.excludePaths ?? []\n const encryptResponse = config.encryptResponse ?? true\n const requireEncryption = config.requireEncryption ?? true\n\n // 初始化传输加密管理器\n const result = config.crypto.transport.createServer({ maxClients: config.maxClients })\n if (!result.success) {\n // 安全策略:transport 已启用但服务端管理器不可用时,受保护路由必须 fail-closed,禁止明文透传。\n return createUnavailableTransportMiddleware(keyExchangePath, excludePaths)\n }\n const manager = result.data\n\n return async (context, next) => {\n const { event } = context\n const pathname = getRequestPathname(event.request)\n\n // 密钥交换端点:委托给 keyExchangeHandler\n if (pathname === keyExchangePath && event.request.method === 'POST') {\n return handleKeyExchange(manager, event.request)\n }\n\n // 仅对同源 API endpoint 与 SvelteKit __data 请求启用 transport;\n // 页面文档、静态资源等仍透传,避免首页 HTML 被误拦截。\n if (!shouldHandleTransportPath(pathname, keyExchangePath)) {\n return next()\n }\n\n // 排除路径不做加解密\n if (shouldExcludeTransportPath(pathname, excludePaths, keyExchangePath)) {\n return next()\n }\n\n // 请求头中无 X-Client-Id 时的处理\n const clientId = event.request.headers.get(TRANSPORT_PROTOCOL.CLIENT_ID_HEADER)\n if (!clientId) {\n if (requireEncryption) {\n // 强制加密模式:缺少 X-Client-Id 说明未完成密钥交换,拒绝请求\n return jsonError(kitM('kit_transportClientIdRequired'), 400)\n }\n // 非强制模式:透传明文(渐进式迁移兼容)\n return next()\n }\n\n // 检查客户端是否已注册\n const clientPublicKey = await manager.getClientPublicKey(clientId)\n if (!clientPublicKey) {\n return jsonError(kitM('kit_transportClientKeyNotFound'), 400)\n }\n\n // ── 解密请求 ──\n if (hasBody(event.request.method) && !isRequestBodyEmpty(event.request)) {\n const decryptResult = await decryptRequestBody(event, manager, requireEncryption)\n if (decryptResult) {\n return decryptResult\n }\n }\n\n // ── 执行后续中间件/端点 ──\n const response = await next()\n\n // ── 加密响应 ──\n if (!encryptResponse) {\n return response\n }\n\n // 只允许可安全加密的响应体离开受保护路由;无法加密时 fail-closed,禁止明文泄露。\n if (isEmptyResponse(response)) {\n return response\n }\n\n const contentType = response.headers.get('Content-Type') ?? ''\n if (!canEncryptResponseContentType(contentType)) {\n return jsonError(kitM('kit_transportEncryptFailed'), 500)\n }\n const contentLength = response.headers.get('Content-Length')\n if (contentLength && Number.parseInt(contentLength, 10) > MAX_ENCRYPTED_BODY) {\n return jsonError(kitM('kit_transportEncryptFailed'), 500)\n }\n\n try {\n const responseBody = await response.clone().text()\n if (!responseBody) {\n return response\n }\n\n const encryptedPayload = await manager.encryptResponse(clientId, responseBody)\n if (!encryptedPayload.success) {\n return jsonError(kitM('kit_transportEncryptFailed'), 500)\n }\n const headers = new Headers(response.headers)\n headers.set(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER, TRANSPORT_PROTOCOL.ENCRYPTED_HEADER_VALUE)\n headers.delete('Content-Length')\n return new Response(JSON.stringify(encryptedPayload.data), {\n status: response.status,\n statusText: response.statusText,\n headers,\n })\n }\n catch {\n return jsonError(kitM('kit_transportEncryptFailed'), 500)\n }\n }\n}\n\nfunction canEncryptResponseContentType(contentType: string): boolean {\n return ENCRYPTABLE_RESPONSE_CONTENT_TYPES.some(type => contentType.includes(type))\n}\n\nfunction createUnavailableTransportMiddleware(\n keyExchangePath: string,\n excludePaths: string[],\n): Middleware {\n return async (context, next) => {\n const pathname = getRequestPathname(context.event.request)\n if (pathname === keyExchangePath) {\n return jsonError(kitM('kit_transportKeyGenerationFailed'), 500)\n }\n if (!shouldHandleTransportPath(pathname, keyExchangePath)) {\n return next()\n }\n if (shouldExcludeTransportPath(pathname, excludePaths, keyExchangePath)) {\n return next()\n }\n return jsonError(kitM('kit_transportKeyGenerationFailed'), 500)\n }\n}\n\nfunction isEmptyResponse(response: Response): boolean {\n if ([204, 304].includes(response.status))\n return true\n return response.headers.get('Content-Length') === '0'\n}\n\nfunction getRequestPathname(request: Request): string {\n return new URL(request.url).pathname\n}\n\nfunction jsonError(message: string, status: number): Response {\n return new Response(JSON.stringify({ error: message }), {\n status,\n headers: { 'Content-Type': 'application/json' },\n })\n}\n\n/**\n * 处理密钥交换 POST 请求\n *\n * 接收客户端公钥,注册并返回服务端公钥与 clientId。\n *\n * @param manager - 传输加密管理器\n * @param request - 原始 Request\n * @returns JSON Response\n */\nasync function handleKeyExchange(\n manager: TransportEncryptionManager,\n request: Request,\n): Promise<Response> {\n try {\n const body = await request.json() as { clientPublicKey?: string }\n\n if (!body.clientPublicKey || typeof body.clientPublicKey !== 'string') {\n return jsonError(kitM('kit_transportInvalidPayload'), 400)\n }\n\n const clientId = await manager.registerClientKey(body.clientPublicKey)\n const serverPublicKey = manager.getServerPublicKey()\n\n return new Response(\n JSON.stringify({ serverPublicKey, clientId }),\n { status: 200, headers: { 'Content-Type': 'application/json' } },\n )\n }\n catch {\n return jsonError(kitM('kit_transportKeyExchangeFailed'), 500)\n }\n}\n\n/**\n * 判断 HTTP 方法是否可能携带请求体\n *\n * @param method - HTTP 方法字符串\n * @returns `true` 表示 POST / PUT / PATCH / DELETE\n */\nfunction hasBody(method: string): boolean {\n return ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method.toUpperCase())\n}\n\n/** 判断请求是否真实携带 body,避免空 POST 被误判为非法密文。 */\nfunction isRequestBodyEmpty(request: Request): boolean {\n if (request.body === null)\n return true\n\n const contentLength = request.headers.get('Content-Length')\n if (contentLength !== null)\n return Number.parseInt(contentLength, 10) === 0\n\n return false\n}\n\ninterface RequestHolder {\n request: Request\n}\n\n/**\n * 重建 SvelteKit 事件中的 Request。\n *\n * `request.text()` 会消费 body;当请求不是加密载荷但允许明文透传时,必须把\n * 原始 body 放回去,避免后续端点再读取时得到空内容。\n *\n * @param event SvelteKit 请求事件\n * @param body 新请求体\n */\nfunction replaceRequestBody(event: RequestHolder, body: string): void {\n const headers = new Headers(event.request.headers)\n headers.delete(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER)\n headers.delete('Content-Length')\n headers.set('Content-Type', 'application/json; charset=utf-8')\n const newRequest = new Request(event.request.url, {\n method: event.request.method,\n headers,\n body,\n })\n Object.defineProperty(event, 'request', {\n value: newRequest,\n writable: true,\n configurable: true,\n })\n}\n\n/**\n * 尝试解密请求体。\n *\n * 非 JSON 请求体(例如 multipart/form-data 文件上传)无法承载传输加密 JSON\n * 载荷;在非强制加密模式下应原样透传,不读取 body,避免破坏浏览器生成的\n * multipart 边界。\n *\n * @param event SvelteKit 请求事件\n * @param manager 传输加密管理器\n * @param requireEncryption 是否强制要求请求加密\n * @returns 需要提前返回的错误响应;返回 null 表示继续后续中间件\n */\nasync function decryptRequestBody(\n event: RequestHolder,\n manager: TransportEncryptionManager,\n requireEncryption: boolean,\n): Promise<Response | null> {\n const isEncrypted = event.request.headers.get(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER)\n === TRANSPORT_PROTOCOL.ENCRYPTED_HEADER_VALUE\n if (!isEncrypted) {\n if (!requireEncryption)\n return null\n return jsonError(kitM('kit_transportInvalidPayload'), 400)\n }\n\n const contentType = event.request.headers.get('Content-Type') ?? ''\n if (!contentType.includes('application/json')) {\n if (!requireEncryption) {\n return null\n }\n return jsonError(kitM('kit_transportInvalidPayload'), 400)\n }\n\n const bodyText = await event.request.text()\n if (!bodyText) {\n return null\n }\n\n let payload: unknown\n try {\n payload = JSON.parse(bodyText) as unknown\n }\n catch {\n if (!requireEncryption) {\n replaceRequestBody(event, bodyText)\n return null\n }\n return jsonError(kitM('kit_transportDecryptFailed'), 400)\n }\n\n if (!isEncryptedPayloadShape(payload)) {\n if (!requireEncryption) {\n replaceRequestBody(event, bodyText)\n return null\n }\n return jsonError(kitM('kit_transportInvalidPayload'), 400)\n }\n\n const plaintext = manager.decryptRequest(payload)\n if (!plaintext.success) {\n return jsonError(kitM('kit_transportDecryptFailed'), 400)\n }\n replaceRequestBody(event, plaintext.data)\n return null\n}\n\nfunction isEncryptedPayloadShape(payload: unknown): payload is EncryptedPayload {\n if (!payload || typeof payload !== 'object')\n return false\n const p = payload as Record<string, unknown>\n return typeof p.encryptedKey === 'string' && typeof p.ciphertext === 'string' && typeof p.iv === 'string'\n}\n"]}