@h-ai/kit 0.1.0-alpha.67 → 0.1.0-alpha.69
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 +67 -13
- package/README.ja.md +67 -13
- package/README.md +67 -13
- package/dist/adapter/index.d.ts +17 -2
- package/dist/adapter/index.js.map +1 -1
- package/dist/browser.d.ts +12 -6
- package/dist/browser.js +6 -7
- package/dist/browser.js.map +1 -1
- package/dist/{chunk-DVW7CQ3H.js → chunk-2PWGJCWX.js} +3 -3
- package/dist/chunk-2PWGJCWX.js.map +1 -0
- package/dist/chunk-OVRZIDCD.js +175 -0
- package/dist/chunk-OVRZIDCD.js.map +1 -0
- package/dist/{chunk-VOTDAVCB.js → chunk-QNLZLTVC.js} +29 -42
- package/dist/chunk-QNLZLTVC.js.map +1 -0
- package/dist/{chunk-MLSP67JF.js → chunk-UTCS6FR7.js} +67 -110
- package/dist/chunk-UTCS6FR7.js.map +1 -0
- package/dist/{chunk-NVYIUBU7.js → chunk-XC7I2L3G.js} +2 -2
- package/dist/chunk-XC7I2L3G.js.map +1 -0
- package/dist/{chunk-L7BTMAKX.js → chunk-Y2YUA3VR.js} +5 -6
- package/dist/chunk-Y2YUA3VR.js.map +1 -0
- package/dist/client/index.d.ts +47 -1
- package/dist/client/index.js +3 -3
- package/dist/client/index.js.map +1 -1
- package/dist/index.d.ts +251 -347
- package/dist/index.js +347 -171
- package/dist/index.js.map +1 -1
- package/dist/kit-a2a-types-DwcmE668.d.ts +136 -0
- package/dist/{index-B7s-0mZl.d.ts → kit-client-auth-B5y1g5_A.d.ts} +228 -102
- package/dist/kit-crypto-types-Cwj9bprV.d.ts +196 -0
- package/dist/{kit-i18n-D9Qqk-1k.d.ts → kit-i18n-DEzmzE5T.d.ts} +142 -29
- package/dist/{kit-types-ByWdbCeO.d.ts → kit-types-DQYqP1Cn.d.ts} +242 -162
- package/dist/modules/a2a/index.d.ts +13 -111
- package/dist/modules/a2a/index.js +2 -2
- package/dist/modules/crypto/index.d.ts +28 -195
- package/dist/modules/crypto/index.js +3 -3
- package/dist/modules/crypto/index.js.map +1 -1
- package/package.json +16 -11
- package/dist/chunk-DVW7CQ3H.js.map +0 -1
- package/dist/chunk-L7BTMAKX.js.map +0 -1
- package/dist/chunk-MLSP67JF.js.map +0 -1
- package/dist/chunk-NVYIUBU7.js.map +0 -1
- package/dist/chunk-QHYNL24G.js +0 -117
- package/dist/chunk-QHYNL24G.js.map +0 -1
- package/dist/chunk-VOTDAVCB.js.map +0 -1
package/README.en.md
CHANGED
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
|
|
23
23
|
## Getting started
|
|
24
24
|
|
|
25
|
+
`sequence` preserves SvelteKit resolve options: page transformations run from inner to outer, while the first preload and serialized-header filter win. This preserves the i18n HTML language replacement.
|
|
26
|
+
|
|
25
27
|
### Installation
|
|
26
28
|
|
|
27
29
|
```bash
|
|
@@ -51,7 +53,7 @@ const config = {
|
|
|
51
53
|
kit: {
|
|
52
54
|
adapter: adapter(),
|
|
53
55
|
// 双构建模式:import { createAdapter } from '@h-ai/kit/adapter'
|
|
54
|
-
// adapter: createAdapter(),
|
|
56
|
+
// adapter: await createAdapter(),
|
|
55
57
|
},
|
|
56
58
|
}
|
|
57
59
|
|
|
@@ -80,24 +82,28 @@ When using Crypto or Kit/IAM, include `sm-crypto@0.5.7` in application productio
|
|
|
80
82
|
|
|
81
83
|
```typescript
|
|
82
84
|
import type { Handle } from '@sveltejs/kit'
|
|
85
|
+
import { cache } from '@h-ai/cache'
|
|
83
86
|
import { kit } from '@h-ai/kit'
|
|
84
87
|
|
|
85
88
|
const haiHandle = kit.createHandle({
|
|
86
89
|
auth: {
|
|
87
90
|
verifyToken: async (token) => {
|
|
88
|
-
|
|
91
|
+
const verified = await iam.auth.verifyToken(token)
|
|
92
|
+
return verified.success ? verified.data : null
|
|
89
93
|
},
|
|
90
94
|
loginUrl: '/auth/login',
|
|
91
95
|
protectedPaths: ['/admin/*', '/api/*'],
|
|
92
96
|
publicPaths: ['/api/auth/*', '/api/public/*'],
|
|
93
97
|
},
|
|
94
|
-
rateLimit: { windowMs: 60_000, maxRequests: 100 },
|
|
98
|
+
rateLimit: { windowMs: 60_000, maxRequests: 100, store: kit.middleware.createCacheRateLimitStore(cache, { namespace: 'app' }) },
|
|
95
99
|
logging: true,
|
|
96
100
|
})
|
|
97
101
|
|
|
98
102
|
export const handle: Handle = haiHandle
|
|
99
103
|
```
|
|
100
104
|
|
|
105
|
+
Rate limits use fixed windows beginning at the first successful commit; later requests preserve the TTL. A store is required. Initialize shared Redis and check its result before serving; all nodes use the same namespace/windowMs/maxRequests. Cache read/commit failure or exhausted contention returns 503, while exceeding the limit returns 429, without get+set or local fallback. createMemoryRateLimitStore is for development or explicit HAI_E2E/HAI_E2E_MOCK tests; it has no timer and can be garbage collected when released; each request scans at most 64 entries, capacity is 10000 windows, new keys receive 503 at capacity without evicting active counts, and external stores remain owned by their creators. Never enable test flags in production. Reset headers estimate node time; Redis TTL controls the actual window, so synchronize clocks. The default key uses the trusted adapter getClientAddress, normalizes equivalent IPv6/mapped IPv4 addresses and ignores raw forwarded headers; configure proxy trust in the deployment adapter.
|
|
106
|
+
|
|
101
107
|
#### API endpoints
|
|
102
108
|
|
|
103
109
|
```typescript
|
|
@@ -136,21 +142,26 @@ const response = await apiFetch('/api/users', { method: 'GET' })
|
|
|
136
142
|
```
|
|
137
143
|
|
|
138
144
|
> `apiFetch` injects the CSRF header automatically; `@h-ai/kit` does not expose a separate CSRF middleware factory.
|
|
145
|
+
> `apiFetch` accepts a string, URL, or Request. It preserves the Request method, headers, and body and adds CSRF for writes. With transport enabled, supported bodies are encrypted and multipart uploads retain their original boundaries.
|
|
139
146
|
>
|
|
140
|
-
>
|
|
147
|
+
> Use httpOnly cookies for same-origin SvelteKit endpoints and verify them on the server. For browser Bearer headers, set an in-memory token with `kit.auth.setBrowserToken()` before `kit.client.create({ auth: true })`. Custom `BrowserTokenStore` implementations must use memory or platform secure storage; never persist sensitive credentials in Web Storage.
|
|
141
148
|
|
|
142
149
|
#### Same-origin transport encryption
|
|
143
150
|
|
|
144
|
-
kit delegates transport encryption to `@h-ai/crypto`. The server creates a manager with `crypto.transport.createServer()`; the browser installs a same-origin fetch wrapper with one call to `kit.client.installBrowserTransport(kitConfig, { crypto })`.
|
|
151
|
+
kit delegates transport encryption to `@h-ai/crypto`. The server creates a manager with `crypto.transport.createServer({ serverKeyRing, keyStore })`; the browser installs a same-origin fetch wrapper with one call to `kit.client.installBrowserTransport(kitConfig, { crypto })`. Servers must also inject a managed key ring and shared registry.
|
|
145
152
|
|
|
146
153
|
**src/hooks.server.ts**:
|
|
147
154
|
|
|
148
155
|
```ts
|
|
149
156
|
import { cache } from '@h-ai/cache'
|
|
150
|
-
import { createRedisTransportKeyStore, crypto } from '@h-ai/crypto'
|
|
157
|
+
import { createRedisTransportKeyStore, crypto, parseTransportServerKeyRing } from '@h-ai/crypto'
|
|
151
158
|
import { kit } from '@h-ai/kit'
|
|
152
159
|
|
|
153
160
|
await crypto.init()
|
|
161
|
+
const ring = parseTransportServerKeyRing(process.env.HAI_CRYPTO_TRANSPORT_SERVERKEYRING)
|
|
162
|
+
if (!ring.success)
|
|
163
|
+
throw new Error(ring.error.message)
|
|
164
|
+
const serverKeyRing = ring.data
|
|
154
165
|
await cache.init({ type: 'redis', host: '127.0.0.1', port: 6379 })
|
|
155
166
|
|
|
156
167
|
export const handle = kit.createHandle({
|
|
@@ -162,7 +173,7 @@ export const handle = kit.createHandle({
|
|
|
162
173
|
crypto: {
|
|
163
174
|
crypto,
|
|
164
175
|
transport: {
|
|
165
|
-
|
|
176
|
+
serverKeyRing,
|
|
166
177
|
keyStore: createRedisTransportKeyStore({ cache, ttlSeconds: 3600 }),
|
|
167
178
|
},
|
|
168
179
|
},
|
|
@@ -197,11 +208,17 @@ export const { apiFetch } = kit.client.create()
|
|
|
197
208
|
|
|
198
209
|
The default key exchange path is `/api/_hai/key-exchange`. If the server customizes `transport.keyExchangePath`, update the client's `transport.keyExchangeUrl` accordingly.
|
|
199
210
|
|
|
200
|
-
|
|
211
|
+
Servers must explicitly inject the same managed `serverKeyRing` and a shared `keyStore` (Redis or a shared database). Sharing client public keys alone cannot enable cross-node decryption. There is no implicit process key or memory-registry fallback; development/tests may explicitly use `createInMemoryKeyStore(capacity)`. Reload the same ring after restart; sticky sessions are unnecessary.
|
|
201
212
|
|
|
202
213
|
Transport applies by default to same-origin `/api/*` endpoints, SvelteKit `__data.json` page data, and key exchange itself. Page documents and static assets remain plaintext and do not require `X-Client-Id`. `multipart/form-data` requests such as uploads are sent unchanged; explicitly allow their paths through server `excludePaths`.
|
|
203
214
|
|
|
204
|
-
|
|
215
|
+
Only authenticated protocol v2 is accepted. Independent random SM4-CBC and HMAC-SM3 keys are wrapped with SM2; verify the sender signature and MAC before CBC decryption. Clients sign requests and the selected server key version signs responses. Authentication covers version, direction, serverKeyId, clientId, a random 128-bit requestId acting as the nonce, issuedAt, HTTP method, the SM3 pathHash of pathname plus the original search, and response status. Requests have a five-minute lifetime and at most 30 seconds of future clock skew. The shared registry atomically consumes (clientId, requestId), retaining it until at least issuedAt plus six minutes to cover node skew; storage failures reject requests. Synchronize node clocks and use a Redis noeviction/durable policy that preserves live nonces. Transport replay prevention does not replace business idempotency keys.
|
|
216
|
+
|
|
217
|
+
Empty POSTs still send an authenticated envelope for empty plaintext. GET/HEAD carry it in X-Hai-Transport; bodyless responses such as 204/304/HEAD use the same header. Other requests/responses carry a JSON envelope and X-Encrypted: true. The client must match the original request context and HTTP status; missing markers, old formats and authentication failures reject. Exclude plaintext endpoints explicitly with excludePaths. CORS must allow and expose X-Hai-Transport. TLS authenticates handshake keys; this protocol does not replace IAM authorization.
|
|
218
|
+
|
|
219
|
+
Set `transport.maxPlaintextBytes` for both request and response JSON plaintext, measured in UTF-8 bytes: default 1 MiB, integer range 1 KiB–64 MiB. Clients learn the server limit during key exchange. `resolveTransportBodyLimits()` derives encrypted/proxy bytes as twice the plaintext limit plus 32 KiB. Handshake, public-key, header and replay constraints remain independent. Oversized streams are cancelled and readers released: requests return 413; oversized responses return an encrypted 500 capacity error, recognized as `TransportBodyTooLargeError` / `hai:crypto:070`. [Transport capacity and upgrade](../crypto/TRANSPORT-CAPACITY.md)。
|
|
220
|
+
|
|
221
|
+
serv/kit return an encrypted HTTP 400 time error for an out-of-window message only after request binding, signature and MAC verification. The client throws `TransportTimeError` (`hai:crypto:071`) with advice to synchronize device/server clocks and refresh. `api-client` maps it to `VALIDATION_FAILED` and preserves `cause.transportCode`. Requests are not replayed automatically and replay windows remain unchanged; tampered or unauthenticated messages retain the generic authentication failure.
|
|
205
222
|
|
|
206
223
|
#### Shared transport configuration with `_kit.yml`
|
|
207
224
|
|
|
@@ -215,13 +232,10 @@ server:
|
|
|
215
232
|
|
|
216
233
|
transport:
|
|
217
234
|
keyExchangePath: /api/_hai/key-exchange
|
|
218
|
-
requireEncryption: true
|
|
219
|
-
encryptResponse: true
|
|
220
235
|
excludePaths:
|
|
221
236
|
- /api/storage
|
|
222
237
|
- /api/public
|
|
223
238
|
- /api/auth/profile/avatar
|
|
224
|
-
maxClients: 10000
|
|
225
239
|
```
|
|
226
240
|
|
|
227
241
|
```ts
|
|
@@ -253,23 +267,30 @@ Because the browser may read this configuration, `_kit.yml` must contain only pu
|
|
|
253
267
|
|
|
254
268
|
With `crypto.encryptedCookies`, also provide `crypto.cookieEncryptionKey` or `HAI_KIT_COOKIE_KEY`. Missing keys fail immediately in `createHandle()`. Plaintext, damaged ciphertext, or decryption failures for protected cookies are treated as absent; encryption failures never fall back to writing plaintext.
|
|
255
269
|
|
|
270
|
+
Authentication cookies default to `Secure` when either `NODE_ENV` or `HAI_CORE_ENV` is `production`; `HAI_E2E` / `HAI_E2E_MOCK` do not relax cookie security. Production rate limiting recognizes both flags; only explicit test switches permit memory counters.
|
|
271
|
+
|
|
256
272
|
### Export paths
|
|
257
273
|
|
|
274
|
+
Use runtime methods through the `kit` namespace or explicit subpaths; types are exported separately. Browser declarations do not expose server factories. Vite configuration uses `@sveltejs/kit/vite`; there is no `@h-ai/kit/vite` plugin.
|
|
275
|
+
|
|
258
276
|
- `@h-ai/kit`: main entry, with the `kit` namespace and types.
|
|
259
277
|
- `@h-ai/kit/client`: browser client.
|
|
260
278
|
- `@h-ai/kit/adapter`: dual build adapter (`createAdapter()`).
|
|
261
|
-
- `@h-ai/kit/vite`: Vite plugin.
|
|
262
279
|
- `@h-ai/kit/a2a`: A2A integration.
|
|
263
280
|
- `@h-ai/kit/crypto`: crypto integration.
|
|
264
281
|
|
|
265
282
|
### Configuration driven authentication
|
|
266
283
|
|
|
284
|
+
Password login submits only `identifier` and `password`; `AuthPanel` maps the `LoginForm` field `username` to `identifier`. Server settings `session.maxAge` / `session.refreshTokenMaxAge` control session lifetimes. The login form has no per-login persistent-session option.
|
|
285
|
+
|
|
267
286
|
Agreements support configuration-loaded userAgreementMarkdown/privacyPolicyMarkdown and URLs. Markdown takes priority for the same agreement and is safely rendered in a modal.
|
|
268
287
|
|
|
269
288
|
Export standard handlers from kit.auth.createApi(iam), use kit.auth.publicPaths for anonymous endpoints, and connect AuthPanel with kit.client.createAuthActions({ apiFetch }). Validation, Cookie writes and HaiResult mapping are built in; current-user and password-change endpoints remain guarded.
|
|
270
289
|
|
|
271
290
|
[完整 IAM 配置 / IAM configuration](../iam/README.en.md)
|
|
272
291
|
|
|
292
|
+
`kit.auth.logout(cookies, accessToken)` returns `Promise<HaiResult<void>>`. Without a token it only clears cookies and succeeds; with a token but no configured auth operations it fails. Local cookies are cleared for every server outcome. Callers must preserve failed results in their HTTP response or UI; local clearing does not confirm server revocation. Underlying control-flow exceptions still propagate.
|
|
293
|
+
|
|
273
294
|
### Error handling
|
|
274
295
|
|
|
275
296
|
- Wrap API endpoints with `kit.handler()`.
|
|
@@ -288,12 +309,45 @@ Declarative CRUD fields support `colSpan: 'auto' | 1 | 2` for UI form widths. `v
|
|
|
288
309
|
|
|
289
310
|
> Shared cross-platform HTTP contracts belong in `@h-ai/api-contract`, are mounted by `@h-ai/serv`, and are called through the `@h-ai/api-client` typed client. `@h-ai/kit` does not define or host business API contracts.
|
|
290
311
|
|
|
312
|
+
### Request locale isolation
|
|
313
|
+
|
|
314
|
+
Wrap Node requests and SSR in `core.i18n.runWithLocale(locale, () => handleRequest())`. SvelteKit can use `kit.i18n.runWithLocale(locale, () => resolve(event))`; serv creates the scope automatically from request headers. Async descendants inherit it. Explicit `MessageOptions.locale` takes precedence over the request locale and the startup default. Within a scope, `setGlobalLocale` only changes that request; outside a scope it configures the startup default and must not be called by request hooks.
|
|
315
|
+
|
|
316
|
+
Browsers keep application-level `setGlobalLocale` / `createLocaleStore`. During SSR, UI store initialization and setters only update the store; messages read the request scope. `runWithLocale` requires the Node entry; browser translations use explicit locale options or application state.
|
|
317
|
+
|
|
291
318
|
## Development and testing
|
|
292
319
|
|
|
293
320
|
```bash
|
|
294
321
|
pnpm --filter @h-ai/kit test
|
|
295
322
|
```
|
|
296
323
|
|
|
324
|
+
Kit has no module-wide init/close. The application initializes and closes shared cache, IAM and crypto dependencies. See the [JSDoc conventions (Chinese)](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证) for typechecked public examples. Authenticate requests with iam.auth.verifyToken; a nonempty token is not proof of identity.
|
|
325
|
+
|
|
326
|
+
## Feedback endpoint and request correlation
|
|
327
|
+
|
|
328
|
+
`kit.createFeedbackHandler` can serve GET/POST/PATCH in `/api/feedback/[...path]/+server.ts`. It reuses the same api-contract schemas as serv, bounds the input stream, and rejects client identity/scope fields. resolveContext maps an already verified locals.session to server-controlled scope and permissions; null denies access. Keep the existing createHandle authentication, CSRF, and transport encryption.
|
|
329
|
+
|
|
330
|
+
createHandle preserves a valid bounded incoming X-Request-Id, adds it to responses, and propagates the request and verified user across asynchronous work. To collect ordinary business logs, wrap the actual business operation in a host middleware using `core.context.run({ scopeId: 'configured-product' }, next)`. This trusted scope must match feedback scope and cannot come from client headers.
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
import type { SessionData } from '@h-ai/kit'
|
|
334
|
+
import { feedback } from '@h-ai/feedback'
|
|
335
|
+
import { kit } from '@h-ai/kit'
|
|
336
|
+
|
|
337
|
+
const handler = kit.createFeedbackHandler({
|
|
338
|
+
feedback,
|
|
339
|
+
resolveContext: (event) => {
|
|
340
|
+
const session = (event.locals as { session?: SessionData }).session
|
|
341
|
+
return session
|
|
342
|
+
? { scopeId: 'configured-product', userId: session.userId, permissions: session.permissions }
|
|
343
|
+
: null
|
|
344
|
+
},
|
|
345
|
+
})
|
|
346
|
+
export const GET = handler
|
|
347
|
+
export const POST = handler
|
|
348
|
+
export const PATCH = handler
|
|
349
|
+
```
|
|
350
|
+
|
|
297
351
|
## License
|
|
298
352
|
|
|
299
353
|
Apache-2.0
|
package/README.ja.md
CHANGED
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
|
|
29
29
|
## 使い始める
|
|
30
30
|
|
|
31
|
+
`sequence` は SvelteKit の resolve オプションを保持します。ページ変換は内側から外側へ実行し、preload とシリアライズ対象ヘッダーのフィルターは最初の設定を優先します。
|
|
32
|
+
|
|
31
33
|
### インストール
|
|
32
34
|
|
|
33
35
|
```bash
|
|
@@ -57,7 +59,7 @@ const config = {
|
|
|
57
59
|
kit: {
|
|
58
60
|
adapter: adapter(),
|
|
59
61
|
// 双构建模式:import { createAdapter } from '@h-ai/kit/adapter'
|
|
60
|
-
// adapter: createAdapter(),
|
|
62
|
+
// adapter: await createAdapter(),
|
|
61
63
|
},
|
|
62
64
|
}
|
|
63
65
|
|
|
@@ -86,24 +88,28 @@ Crypto または Kit/IAM を使う場合、アプリの本番依存に `sm-crypt
|
|
|
86
88
|
|
|
87
89
|
```typescript
|
|
88
90
|
import type { Handle } from '@sveltejs/kit'
|
|
91
|
+
import { cache } from '@h-ai/cache'
|
|
89
92
|
import { kit } from '@h-ai/kit'
|
|
90
93
|
|
|
91
94
|
const haiHandle = kit.createHandle({
|
|
92
95
|
auth: {
|
|
93
96
|
verifyToken: async (token) => {
|
|
94
|
-
|
|
97
|
+
const verified = await iam.auth.verifyToken(token)
|
|
98
|
+
return verified.success ? verified.data : null
|
|
95
99
|
},
|
|
96
100
|
loginUrl: '/auth/login',
|
|
97
101
|
protectedPaths: ['/admin/*', '/api/*'],
|
|
98
102
|
publicPaths: ['/api/auth/*', '/api/public/*'],
|
|
99
103
|
},
|
|
100
|
-
rateLimit: { windowMs: 60_000, maxRequests: 100 },
|
|
104
|
+
rateLimit: { windowMs: 60_000, maxRequests: 100, store: kit.middleware.createCacheRateLimitStore(cache, { namespace: 'app' }) },
|
|
101
105
|
logging: true,
|
|
102
106
|
})
|
|
103
107
|
|
|
104
108
|
export const handle: Handle = haiHandle
|
|
105
109
|
```
|
|
106
110
|
|
|
111
|
+
最初の確定時点から固定ウィンドウを開始し、後続リクエストはTTLを延長しません。storeは必須です。共有Redisの初期化成功を確認し、全ノードのnamespace/windowMs/maxRequestsを一致させます。キャッシュ障害や競合上限は503、超過は429で、get+setやローカル計数へフォールバックしません。createMemoryRateLimitStoreは開発または明示HAI_E2E/HAI_E2E_MOCKテスト用で、常駐タイマーを使わず参照解放で回収されます。1リクエスト最大64項目を遅延走査し、上限10000ウィンドウです。満杯時は有効な計数を追い出さず新キーを503にし、外部storeのライフサイクルは作成者が管理します。本番でテスト変数を設定しません。ヘッダー時刻は推定、実際の期間はRedis TTLで制御するため時計を同期します。キーは信頼済みadapterのgetClientAddressのみ使い、等価IPv6/IPv4表記を正規化します。生の転送ヘッダーを信頼せず、代理信頼はadapterで設定します。
|
|
112
|
+
|
|
107
113
|
#### API endpoint
|
|
108
114
|
|
|
109
115
|
```typescript
|
|
@@ -142,21 +148,26 @@ const response = await apiFetch('/api/users', { method: 'GET' })
|
|
|
142
148
|
```
|
|
143
149
|
|
|
144
150
|
> CSRF Header は `apiFetch` が自動付与します。`@h-ai/kit` は独立した CSRF middleware ファクトリーを提供しません。
|
|
151
|
+
> `apiFetch` は文字列、URL、Request を受け付けます。Request のメソッド、ヘッダー、本文を保持し、書き込みには CSRF を付与します。transport 有効時は対応する本文のみ暗号化し、multipart の境界を保持します。
|
|
145
152
|
>
|
|
146
|
-
>
|
|
153
|
+
> 同一オリジンの SvelteKit endpoint は httpOnly Cookie を使い、サーバーで検証します。ブラウザーの Bearer Header が必要なら `kit.auth.setBrowserToken()` でページ内メモリーに保存してから `kit.client.create({ auth: true })` を使います。独自の `BrowserTokenStore` もメモリーまたはプラットフォームの安全なストレージを使用し、機密の認証情報を Web Storage に永続化しないでください。
|
|
147
154
|
|
|
148
155
|
#### 同一オリジンの通信暗号化
|
|
149
156
|
|
|
150
|
-
kit の通信暗号化は `@h-ai/crypto` に委譲します。サーバーは `crypto.transport.createServer()` で管理器を作成し、ブラウザーは `kit.client.installBrowserTransport(kitConfig, { crypto })` の 1 行で同一オリジンの fetch
|
|
157
|
+
kit の通信暗号化は `@h-ai/crypto` に委譲します。サーバーは `crypto.transport.createServer({ serverKeyRing, keyStore })` で管理器を作成し、ブラウザーは `kit.client.installBrowserTransport(kitConfig, { crypto })` の 1 行で同一オリジンの fetch ラッパーを導入します。サーバーには管理済み鍵環と共有 registry も渡します。
|
|
151
158
|
|
|
152
159
|
**src/hooks.server.ts**:
|
|
153
160
|
|
|
154
161
|
```ts
|
|
155
162
|
import { cache } from '@h-ai/cache'
|
|
156
|
-
import { createRedisTransportKeyStore, crypto } from '@h-ai/crypto'
|
|
163
|
+
import { createRedisTransportKeyStore, crypto, parseTransportServerKeyRing } from '@h-ai/crypto'
|
|
157
164
|
import { kit } from '@h-ai/kit'
|
|
158
165
|
|
|
159
166
|
await crypto.init()
|
|
167
|
+
const ring = parseTransportServerKeyRing(process.env.HAI_CRYPTO_TRANSPORT_SERVERKEYRING)
|
|
168
|
+
if (!ring.success)
|
|
169
|
+
throw new Error(ring.error.message)
|
|
170
|
+
const serverKeyRing = ring.data
|
|
160
171
|
await cache.init({ type: 'redis', host: '127.0.0.1', port: 6379 })
|
|
161
172
|
|
|
162
173
|
export const handle = kit.createHandle({
|
|
@@ -168,7 +179,7 @@ export const handle = kit.createHandle({
|
|
|
168
179
|
crypto: {
|
|
169
180
|
crypto,
|
|
170
181
|
transport: {
|
|
171
|
-
|
|
182
|
+
serverKeyRing,
|
|
172
183
|
keyStore: createRedisTransportKeyStore({ cache, ttlSeconds: 3600 }),
|
|
173
184
|
},
|
|
174
185
|
},
|
|
@@ -203,11 +214,17 @@ export const { apiFetch } = kit.client.create()
|
|
|
203
214
|
|
|
204
215
|
既定の鍵交換パスは `/api/_hai/key-exchange` です。サーバーで `transport.keyExchangePath` を変更した場合、クライアントの `transport.keyExchangeUrl` も合わせて設定してください。
|
|
205
216
|
|
|
206
|
-
|
|
217
|
+
サーバーには同じ管理済み `serverKeyRing` と共有 `keyStore`(Redis または共有 DB)を明示的に渡します。クライアント公開鍵の共有だけではノード間で復号できません。暗黙のプロセス鍵・メモリー registry はなく、開発・テストでは `createInMemoryKeyStore(capacity)` を明示します。再起動後は同じ鍵環を読み込み、sticky session は不要です。
|
|
207
218
|
|
|
208
219
|
transport は既定で同一オリジンの `/api/*`、SvelteKit の `__data.json`、鍵交換 endpoint に適用します。ページ文書・静的ファイルは平文で `X-Client-Id` は不要です。アップロードなどの `multipart/form-data` はそのまま送信し、サーバーの `excludePaths` で明示的に許可してください。
|
|
209
220
|
|
|
210
|
-
|
|
221
|
+
認証済み v2 のみを受け付けます。独立したランダム SM4-CBC 鍵と HMAC-SM3 鍵を SM2 で包み、送信者署名、MAC の順に検証してから CBC を復号します。要求はクライアント秘密鍵、応答は選択されたサーバー鍵バージョンで署名します。version、direction、serverKeyId、clientId、nonce を兼ねるランダム 128 ビット requestId、issuedAt、HTTP method、pathname と元の search の SM3 pathHash、応答 status を認証します。要求の有効期限は 5 分、未来方向の時計差は最大 30 秒です。共有 registry が (clientId, requestId) を原子的に消費し、ノード間の時計差を含め issuedAt + 6 分まで保持します。保存障害では拒否します。時計同期と Redis の noeviction/永続化により有効な nonce を保持してください。業務の再試行には別の冪等キーが必要です。
|
|
222
|
+
|
|
223
|
+
空の POST も空平文の認証封装を送ります。GET/HEAD は X-Hai-Transport ヘッダー、204/304/HEAD など本文なし応答も同じヘッダーを使います。他の要求・応答は JSON 本文と X-Encrypted: true を使います。クライアントは元の要求コンテキストと HTTP status を照合し、マーカー欠落、旧形式、認証失敗では reject します。平文 endpoint は excludePaths で明示的に除外し、CORS では X-Hai-Transport を許可・公開してください。鍵交換の公開鍵認証は TLS に依存し、IAM の業務認可は別に必要です。
|
|
224
|
+
|
|
225
|
+
`transport.maxPlaintextBytes` でリクエストとレスポンスの JSON 平文容量を共通設定します。単位は UTF-8 バイト、既定値は 1 MiB、有効範囲は 1 KiB~64 MiB の整数です。クライアントは鍵交換で上限を取得し、`resolveTransportBodyLimits()` が暗号文とプロキシ容量(平文の 2 倍 + 32 KiB)を算出します。鍵交換、公開鍵、認証ヘッダー、防リプレイ制約は独立です。超過時は元ストリームを停止して reader を解放します。要求は 413、応答は暗号化された 500 容量エラーとなり、`TransportBodyTooLargeError` / `hai:crypto:070` で識別できます。 [Transport capacity and upgrade](../crypto/TRANSPORT-CAPACITY.md)。
|
|
226
|
+
|
|
227
|
+
serv/kit は要求の結合、署名、MAC を検証した後、時間窓を超えた報文に暗号化した HTTP 400 時刻エラーを返します。クライアントは `TransportTimeError`(`hai:crypto:071`)で端末とサーバーの時刻同期・再読み込みを案内します。`api-client` は `VALIDATION_FAILED` に変換し `cause.transportCode` に原因を保持します。自動再送やリプレイ時間窓の緩和は行わず、改ざん・未認証の要求は従来の認証エラーです。
|
|
211
228
|
|
|
212
229
|
#### `_kit.yml` による共通 transport 設定
|
|
213
230
|
|
|
@@ -221,13 +238,10 @@ server:
|
|
|
221
238
|
|
|
222
239
|
transport:
|
|
223
240
|
keyExchangePath: /api/_hai/key-exchange
|
|
224
|
-
requireEncryption: true
|
|
225
|
-
encryptResponse: true
|
|
226
241
|
excludePaths:
|
|
227
242
|
- /api/storage
|
|
228
243
|
- /api/public
|
|
229
244
|
- /api/auth/profile/avatar
|
|
230
|
-
maxClients: 10000
|
|
231
245
|
```
|
|
232
246
|
|
|
233
247
|
```ts
|
|
@@ -259,23 +273,30 @@ export const appKitConfig = resolveKitConfig(parse(rawKitConfig) ?? {})
|
|
|
259
273
|
|
|
260
274
|
`crypto.encryptedCookies` を設定する場合、`crypto.cookieEncryptionKey` または `HAI_KIT_COOKIE_KEY` が必要です。鍵がなければ `createHandle()` ですぐエラーになります。保護対象 Cookie の平文・破損暗号文・復号失敗は未存在として扱い、暗号化失敗時も平文保存に切り替えません。
|
|
261
275
|
|
|
276
|
+
認証 Cookie は `NODE_ENV` または `HAI_CORE_ENV` が `production` の場合に `Secure` を既定で有効にします。`HAI_E2E` / `HAI_E2E_MOCK` は Cookie の安全要件を緩和しません。本番のレート制限も両方の環境変数を認識し、明示的なテストフラグだけがメモリカウンターを許可します。
|
|
277
|
+
|
|
262
278
|
### エクスポートパス
|
|
263
279
|
|
|
280
|
+
実行時メソッドは `kit` 名前空間または明示したサブパスを使用し、型は個別に公開します。browser 宣言はサーバーファクトリを公開しません。Vite 設定には `@sveltejs/kit/vite` を使い、`@h-ai/kit/vite` プラグインはありません。
|
|
281
|
+
|
|
264
282
|
- `@h-ai/kit`:主入口。`kit` 名前空間と型を提供。
|
|
265
283
|
- `@h-ai/kit/client`:ブラウザークライアント。
|
|
266
284
|
- `@h-ai/kit/adapter`:2 モードのビルド adapter(`createAdapter()`)。
|
|
267
|
-
- `@h-ai/kit/vite`:Vite プラグイン。
|
|
268
285
|
- `@h-ai/kit/a2a`:A2A 統合。
|
|
269
286
|
- `@h-ai/kit/crypto`:暗号モジュール統合。
|
|
270
287
|
|
|
271
288
|
### 設定による認証画面
|
|
272
289
|
|
|
290
|
+
パスワードログインの送信項目は `identifier` と `password` のみです。`AuthPanel` は `LoginForm` の `username` を `identifier` に変換します。有効期間はサーバーの `session.maxAge` / `session.refreshTokenMaxAge` で設定し、フォームにログインごとの永続セッション選択肢はありません。
|
|
291
|
+
|
|
273
292
|
規約は設定の userAgreementMarkdown/privacyPolicyMarkdown または URL を使用できます。同じ規約は Markdown 本文を優先して安全な Modal で描画します。
|
|
274
293
|
|
|
275
294
|
kit.auth.createApi(iam) の標準 handler と kit.auth.publicPaths を使用し、AuthPanel には kit.client.createAuthActions({ apiFetch }) を接続します。検証、Cookie、HaiResult 変換を共通化し、ユーザー情報と改密の保護を維持します。
|
|
276
295
|
|
|
277
296
|
[完整 IAM 配置 / IAM configuration](../iam/README.ja.md)
|
|
278
297
|
|
|
298
|
+
`kit.auth.logout(cookies, accessToken)` は `Promise<HaiResult<void>>` を返します。Token がなければ Cookie のみ消去して成功し、Token があるのに認証操作が未設定なら失敗します。サーバー結果にかかわらず Cookie は消去しますが、呼出側は失敗を HTTP 応答や UI に伝えます。ローカル消去はサーバー失効の確認ではありません。制御フロー例外は引き続き伝播します。
|
|
299
|
+
|
|
279
300
|
### エラー処理
|
|
280
301
|
|
|
281
302
|
- API endpoint は `kit.handler()` でラップすることを推奨。
|
|
@@ -286,12 +307,45 @@ kit.auth.createApi(iam) の標準 handler と kit.auth.publicPaths を使用し
|
|
|
286
307
|
|
|
287
308
|
宣言型 CRUD 項目は UI フォームの幅を指定する `colSpan: 'auto' | 1 | 2` に対応します。`validation.message` は文字列または `() => string` を受け取り、現在の言語で表示できます。レイアウトは `CrudPage.form` で設定し、サーバーでも検証してください。
|
|
288
309
|
|
|
310
|
+
### リクエスト言語の分離
|
|
311
|
+
|
|
312
|
+
Node リクエストと SSR は `core.i18n.runWithLocale(locale, () => handleRequest())` で囲みます。SvelteKit は `kit.i18n.runWithLocale(locale, () => resolve(event))` を使えます。serv はリクエストヘッダーから自動設定します。非同期の子処理も言語を継承し、明示的な `MessageOptions.locale`、リクエスト言語、起動時の既定値の順で優先します。コンテキスト内の `setGlobalLocale` はそのリクエストだけを変更します。コンテキスト外の設定は起動時に限定し、リクエスト hook から呼びません。
|
|
313
|
+
|
|
314
|
+
ブラウザーはアプリ全体の `setGlobalLocale` / `createLocaleStore` を使います。SSR の UI store 初期化と setter は自身の状態のみ変更し、メッセージはリクエストコンテキストを参照します。`runWithLocale` は Node 入口専用です。
|
|
315
|
+
|
|
289
316
|
## 開発とテスト
|
|
290
317
|
|
|
291
318
|
```bash
|
|
292
319
|
pnpm --filter @h-ai/kit test
|
|
293
320
|
```
|
|
294
321
|
|
|
322
|
+
Kit に共通の init/close はありません。共有 cache・IAM・crypto はアプリが初期化と終了を管理します。型検査できる例は [JSDoc 規約(中国語)](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证) を参照してください。認証は iam.auth.verifyToken で検証し、空でない token だけでは許可しません。
|
|
323
|
+
|
|
324
|
+
## フィードバック入口とリクエスト関連付け
|
|
325
|
+
|
|
326
|
+
`kit.createFeedbackHandler` は `/api/feedback/[...path]/+server.ts` の GET/POST/PATCH に使用できます。serv と同じ api-contract schema を再利用し、入力ストリームの容量とクライアント認証情報の混入を制限します。resolveContext は検証済み locals.session から範囲・権限を作り、null は拒否します。既存 createHandle の認証・CSRF・通信暗号化を維持してください。
|
|
327
|
+
|
|
328
|
+
createHandle は有効な上限付き X-Request-Id を保持して応答に付け、非同期業務にリクエストと検証済みユーザーを伝播します。通常の業務ログを収集する場合、ホストの middleware で `core.context.run({ scopeId: 'configured-product' }, next)` により業務を包みます。範囲は feedback と一致させ、クライアントのヘッダーから取得しません。
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
import type { SessionData } from '@h-ai/kit'
|
|
332
|
+
import { feedback } from '@h-ai/feedback'
|
|
333
|
+
import { kit } from '@h-ai/kit'
|
|
334
|
+
|
|
335
|
+
const handler = kit.createFeedbackHandler({
|
|
336
|
+
feedback,
|
|
337
|
+
resolveContext: (event) => {
|
|
338
|
+
const session = (event.locals as { session?: SessionData }).session
|
|
339
|
+
return session
|
|
340
|
+
? { scopeId: 'configured-product', userId: session.userId, permissions: session.permissions }
|
|
341
|
+
: null
|
|
342
|
+
},
|
|
343
|
+
})
|
|
344
|
+
export const GET = handler
|
|
345
|
+
export const POST = handler
|
|
346
|
+
export const PATCH = handler
|
|
347
|
+
```
|
|
348
|
+
|
|
295
349
|
## ライセンス
|
|
296
350
|
|
|
297
351
|
Apache-2.0
|
package/README.md
CHANGED
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
|
|
29
29
|
## 使用入门
|
|
30
30
|
|
|
31
|
+
`sequence` 保留并组合 SvelteKit 的 `resolve` 选项:页面转换由内向外执行,`preload` 与序列化响应头过滤使用首个配置,确保 i18n 的 HTML 语言标记正确替换。
|
|
32
|
+
|
|
31
33
|
### 安装
|
|
32
34
|
|
|
33
35
|
```bash
|
|
@@ -57,7 +59,7 @@ const config = {
|
|
|
57
59
|
kit: {
|
|
58
60
|
adapter: adapter(),
|
|
59
61
|
// 双构建模式:import { createAdapter } from '@h-ai/kit/adapter'
|
|
60
|
-
// adapter: createAdapter(),
|
|
62
|
+
// adapter: await createAdapter(),
|
|
61
63
|
},
|
|
62
64
|
}
|
|
63
65
|
|
|
@@ -86,24 +88,28 @@ export default defineConfig({
|
|
|
86
88
|
|
|
87
89
|
```typescript
|
|
88
90
|
import type { Handle } from '@sveltejs/kit'
|
|
91
|
+
import { cache } from '@h-ai/cache'
|
|
89
92
|
import { kit } from '@h-ai/kit'
|
|
90
93
|
|
|
91
94
|
const haiHandle = kit.createHandle({
|
|
92
95
|
auth: {
|
|
93
96
|
verifyToken: async (token) => {
|
|
94
|
-
|
|
97
|
+
const verified = await iam.auth.verifyToken(token)
|
|
98
|
+
return verified.success ? verified.data : null
|
|
95
99
|
},
|
|
96
100
|
loginUrl: '/auth/login',
|
|
97
101
|
protectedPaths: ['/admin/*', '/api/*'],
|
|
98
102
|
publicPaths: ['/api/auth/*', '/api/public/*'],
|
|
99
103
|
},
|
|
100
|
-
rateLimit: { windowMs: 60_000, maxRequests: 100 },
|
|
104
|
+
rateLimit: { windowMs: 60_000, maxRequests: 100, store: kit.middleware.createCacheRateLimitStore(cache, { namespace: 'app' }) },
|
|
101
105
|
logging: true,
|
|
102
106
|
})
|
|
103
107
|
|
|
104
108
|
export const handle: Handle = haiHandle
|
|
105
109
|
```
|
|
106
110
|
|
|
111
|
+
限流采用首次成功提交为起点的固定窗口,后续请求不延长 TTL。必须显式传 store;应用先检查共享 Redis 初始化结果,所有节点使用相同 namespace/windowMs/maxRequests。缓存读取/条件提交失败或冲突耗尽返回503,超限429;不会回退 get+set 或本地计数。createMemoryRateLimitStore 仅开发或显式 HAI_E2E/HAI_E2E_MOCK 测试使用,无常驻定时器,丢弃引用即可回收;每次请求最多惰性扫描 64 项,最多保存 10000 个窗口,容量满时对新 key 返回503且不驱逐有效计数,外部 store 生命周期由创建者管理;生产不得设置测试开关。响应重置时间为节点时钟估算,实际窗口由 Redis TTL 决定,须同步时钟。默认键只读取可信 adapter 的 getClientAddress、规范化等价IPv6和IPv4映射地址,不采信原始转发头;代理信任通过部署adapter配置。
|
|
112
|
+
|
|
107
113
|
#### API 端点
|
|
108
114
|
|
|
109
115
|
```typescript
|
|
@@ -142,21 +148,26 @@ const response = await apiFetch('/api/users', { method: 'GET' })
|
|
|
142
148
|
```
|
|
143
149
|
|
|
144
150
|
> CSRF Header 注入由 `apiFetch` 自动处理;`@h-ai/kit` 不提供独立的 CSRF 中间件工厂。
|
|
151
|
+
> `apiFetch` 接受字符串、URL 或 Request;Request 自带的方法、请求头与正文会保留,写方法同样附加 CSRF。启用 transport 时仅加密可支持的正文,multipart 上传保持原始边界。
|
|
145
152
|
>
|
|
146
|
-
>
|
|
153
|
+
> 同源 SvelteKit endpoint 使用 httpOnly Cookie,由服务端读取 Cookie 完成鉴权。浏览器 Bearer Token 使用 `kit.auth.setBrowserToken()` 写入页面内存,再通过 `kit.client.create({ auth: true })` 注入。自定义 `BrowserTokenStore` 也必须使用内存或平台安全存储,禁止将敏感凭据写入 Web Storage。
|
|
147
154
|
|
|
148
155
|
#### 同源传输加密
|
|
149
156
|
|
|
150
|
-
kit 的传输加密也统一委托 `@h-ai/crypto`:服务端通过 `crypto.transport.createServer()` 创建管理器,浏览器端通过 `kit.client.installBrowserTransport(kitConfig, { crypto })` 一行安装同源 fetch
|
|
157
|
+
kit 的传输加密也统一委托 `@h-ai/crypto`:服务端通过 `crypto.transport.createServer({ serverKeyRing, keyStore })` 创建管理器,浏览器端通过 `kit.client.installBrowserTransport(kitConfig, { crypto })` 一行安装同源 fetch 包装。服务端还须注入受管密钥环和共享 registry。
|
|
151
158
|
|
|
152
159
|
**src/hooks.server.ts**:
|
|
153
160
|
|
|
154
161
|
```ts
|
|
155
162
|
import { cache } from '@h-ai/cache'
|
|
156
|
-
import { createRedisTransportKeyStore, crypto } from '@h-ai/crypto'
|
|
163
|
+
import { createRedisTransportKeyStore, crypto, parseTransportServerKeyRing } from '@h-ai/crypto'
|
|
157
164
|
import { kit } from '@h-ai/kit'
|
|
158
165
|
|
|
159
166
|
await crypto.init()
|
|
167
|
+
const ring = parseTransportServerKeyRing(process.env.HAI_CRYPTO_TRANSPORT_SERVERKEYRING)
|
|
168
|
+
if (!ring.success)
|
|
169
|
+
throw new Error(ring.error.message)
|
|
170
|
+
const serverKeyRing = ring.data
|
|
160
171
|
await cache.init({ type: 'redis', host: '127.0.0.1', port: 6379 })
|
|
161
172
|
|
|
162
173
|
export const handle = kit.createHandle({
|
|
@@ -168,7 +179,7 @@ export const handle = kit.createHandle({
|
|
|
168
179
|
crypto: {
|
|
169
180
|
crypto,
|
|
170
181
|
transport: {
|
|
171
|
-
|
|
182
|
+
serverKeyRing,
|
|
172
183
|
keyStore: createRedisTransportKeyStore({ cache, ttlSeconds: 3600 }),
|
|
173
184
|
},
|
|
174
185
|
},
|
|
@@ -203,11 +214,17 @@ export const { apiFetch } = kit.client.create()
|
|
|
203
214
|
|
|
204
215
|
默认密钥协商路径为 `/api/_hai/key-exchange`。如服务端自定义 `transport.keyExchangePath`,客户端需同步配置 `transport.keyExchangeUrl`。
|
|
205
216
|
|
|
206
|
-
|
|
217
|
+
服务端必须显式注入相同的受管 `serverKeyRing` 和共享 `keyStore`(Redis 或共享数据库)。仅共享客户端公钥不足以跨节点解密。没有隐式进程私钥或内存 registry 回退;开发/测试显式用 `createInMemoryKeyStore(capacity)`。服务端重启后重新加载同一密钥环即可,无需 sticky session。
|
|
207
218
|
|
|
208
219
|
transport 默认作用于同源 `/api/*` endpoint 与 SvelteKit `__data.json` 页面数据请求(以及密钥协商端点本身);页面文档与静态资源仍保持明文,不要求携带 `X-Client-Id`。文件上传等 `multipart/form-data` 请求会保持原样发送,需在服务端 `excludePaths` 中显式放行对应上传路径。
|
|
209
220
|
|
|
210
|
-
|
|
221
|
+
认证协议仅支持 v2:独立随机 SM4-CBC 密钥和 HMAC-SM3 密钥由 SM2 封装,先验签、再验证 MAC,最后解密 CBC。请求由客户端私钥签名,响应由所选服务端版本签名。认证字段包括 version、direction、serverKeyId、clientId、128 位随机 requestId(兼 nonce)、issuedAt、HTTP method、pathname + 原始 search 的 SM3 pathHash,以及响应 status。请求有效期 5 分钟,未来时钟偏差最多 30 秒;共享 registry 原子消费 (clientId, requestId),nonce 至少保留到 issuedAt + 6 分钟以覆盖节点偏差,存储故障拒绝请求。Redis 必须保留有效 nonce,使用 noeviction/可靠持久化策略;各节点时钟须同步。此能力防止同一传输消息重放,业务重试仍需自己的幂等键。
|
|
222
|
+
|
|
223
|
+
空 POST 也发送空明文的认证封装;GET/HEAD 将封装放在 X-Hai-Transport 头,204/304/HEAD 等无正文响应也使用同名头承载认证封装。其它请求/响应在 JSON 正文承载封装并带 X-Encrypted: true。客户端必须关联原请求上下文和 HTTP status,缺标记、旧格式或认证失败一律 reject;明文接口只能经 excludePaths 显式排除。CORS 须允许并暴露 X-Hai-Transport。握手公钥的真实性依赖 TLS,不替代 IAM 业务授权。
|
|
224
|
+
|
|
225
|
+
业务容量通过 `transport.maxPlaintextBytes` 统一配置:默认 1 MiB,整数范围 1 KiB~64 MiB,请求和响应共用 UTF-8 明文字节预算。客户端从握手自动获取上限,密文与部署代理预算由 `resolveTransportBodyLimits()` 推导(两倍明文 + 32 KiB)。握手、公钥、认证头和防重放约束独立;超限取消原流并释放 reader,请求 413、响应为加密的 500 容量错误,客户端识别 `TransportBodyTooLargeError` / `hai:crypto:070`。 [Transport capacity and upgrade](../crypto/TRANSPORT-CAPACITY.md)。
|
|
226
|
+
|
|
227
|
+
serv/kit 对通过请求绑定、签名和 MAC 校验但超出时间窗口的报文返回加密的 HTTP 400 时间错误;客户端抛 `TransportTimeError`(`hai:crypto:071`),提示同步设备和服务器时间后刷新重试。`api-client` 映射为 `VALIDATION_FAILED`,在 `cause.transportCode` 保留该原因。不会自动重放请求或放宽防重放窗口;篡改或未认证报文仍返回通用认证失败。
|
|
211
228
|
|
|
212
229
|
#### 使用 `_kit.yml` 统一 transport 配置
|
|
213
230
|
|
|
@@ -221,13 +238,10 @@ server:
|
|
|
221
238
|
|
|
222
239
|
transport:
|
|
223
240
|
keyExchangePath: /api/_hai/key-exchange
|
|
224
|
-
requireEncryption: true
|
|
225
|
-
encryptResponse: true
|
|
226
241
|
excludePaths:
|
|
227
242
|
- /api/storage
|
|
228
243
|
- /api/public
|
|
229
244
|
- /api/auth/profile/avatar
|
|
230
|
-
maxClients: 10000
|
|
231
245
|
```
|
|
232
246
|
|
|
233
247
|
```ts
|
|
@@ -259,23 +273,30 @@ export const appKitConfig = resolveKitConfig(parse(rawKitConfig) ?? {})
|
|
|
259
273
|
|
|
260
274
|
配置 `crypto.encryptedCookies` 时必须同时提供 `crypto.cookieEncryptionKey`,或设置 `HAI_KIT_COOKIE_KEY`。缺少密钥会在 `createHandle()` 时立即报错;受保护 Cookie 的明文、损坏密文或解密失败都按不存在处理,加密失败也不会回退写入明文。
|
|
261
275
|
|
|
276
|
+
认证 Cookie 的 `Secure` 默认值在 `NODE_ENV` 或 `HAI_CORE_ENV` 为 `production` 时开启,`HAI_E2E` / `HAI_E2E_MOCK` 不降低 Cookie 安全要求。生产限流同样识别这两个环境标志;仅显式测试开关可放宽内存计数器限制。
|
|
277
|
+
|
|
262
278
|
### 导出路径
|
|
263
279
|
|
|
280
|
+
子模块运行时方法通过 `kit` 命名空间或明确子路径使用,类型单独导出;浏览器声明不暴露服务端工厂。Vite 配置使用 `@sveltejs/kit/vite`,不存在 `@h-ai/kit/vite` 插件。
|
|
281
|
+
|
|
264
282
|
- `@h-ai/kit`:主入口,提供 `kit` 命名空间与类型。
|
|
265
283
|
- `@h-ai/kit/client`:浏览器端客户端。
|
|
266
284
|
- `@h-ai/kit/adapter`:双构建 adapter(`createAdapter()`)。
|
|
267
|
-
- `@h-ai/kit/vite`:Vite 插件。
|
|
268
285
|
- `@h-ai/kit/a2a`:A2A 协议集成。
|
|
269
286
|
- `@h-ai/kit/crypto`:加密模块集成。
|
|
270
287
|
|
|
271
288
|
### 配置驱动认证页面
|
|
272
289
|
|
|
290
|
+
密码登录只提交 `identifier` 和 `password`;`AuthPanel` 将 `LoginForm` 的 `username` 映射为 `identifier`。会话有效期由服务端 `session.maxAge` / `session.refreshTokenMaxAge` 配置决定,登录表单不提供按次登录的持久会话选项。
|
|
291
|
+
|
|
273
292
|
协议支持配置中的 userAgreementMarkdown/privacyPolicyMarkdown 正文及 URL;同主题优先 Markdown,由安全渲染器在弹窗显示。
|
|
274
293
|
|
|
275
294
|
路由直接导出 `kit.auth.createApi(iam)` 的 login、loginWithOtp、loginWithLdap、register、publicConfig、sendOtp、requestPasswordReset、confirmPasswordReset handler;自动执行 schema 验证、Cookie 设置和 HaiResult 映射。匿名白名单使用 `kit.auth.publicPaths`,不会放开 /me 和改密端点。浏览器用 `kit.client.createAuthActions({ apiFetch })` 直接连接 AuthPanel,复用 CSRF/transport。
|
|
276
295
|
|
|
277
296
|
[完整 IAM 配置 / IAM configuration](../iam/README.md)
|
|
278
297
|
|
|
298
|
+
`kit.auth.logout(cookies, accessToken)` 返回 `Promise<HaiResult<void>>`。无令牌时仅清除 Cookie 并成功;有令牌但未配置认证操作时返回失败。无论服务端结果如何都清除本地 Cookie,调用方仍必须把失败转换为错误响应或提示;仅成功结果代表服务端撤销已经确认。底层控制流异常继续传播。
|
|
299
|
+
|
|
279
300
|
### 错误处理
|
|
280
301
|
|
|
281
302
|
- 推荐使用 `kit.handler()` 包装 API 端点。
|
|
@@ -286,12 +307,45 @@ export const appKitConfig = resolveKitConfig(parse(rawKitConfig) ?? {})
|
|
|
286
307
|
|
|
287
308
|
声明式 CRUD 字段支持 `colSpan: 'auto' | 1 | 2`,供 UI 双列表单决定字段宽度;`validation.message` 支持字符串或 `() => string`,便于随当前语言显示校验错误。UI 布局在 `CrudPage.form` 上配置,服务端仍须独立校验。
|
|
288
309
|
|
|
310
|
+
### 请求语言隔离
|
|
311
|
+
|
|
312
|
+
Node 请求与 SSR 使用 `core.i18n.runWithLocale(locale, () => handleRequest())`;SvelteKit 可使用 `kit.i18n.runWithLocale(locale, () => resolve(event))`,serv 自动按请求头建立上下文。异步子任务继承请求语言,优先级为显式 `MessageOptions.locale`、请求语言、进程启动默认语言。`setGlobalLocale` 在请求上下文中只更新当前请求;上下文外仅用于启动默认值,不能在请求 hook 中调用它修改默认值。
|
|
313
|
+
|
|
314
|
+
浏览器继续使用应用级 `setGlobalLocale` / `createLocaleStore`。SSR 的 UI store 初始化和 setter 只修改自身状态,不写全局语言;UI 消息从请求上下文读取。`runWithLocale` 仅支持 Node 入口,浏览器通过显式 locale 或应用状态翻译。
|
|
315
|
+
|
|
289
316
|
## 开发测试
|
|
290
317
|
|
|
291
318
|
```bash
|
|
292
319
|
pnpm --filter @h-ai/kit test
|
|
293
320
|
```
|
|
294
321
|
|
|
322
|
+
模块无统一 init/close;共享缓存、IAM 与 crypto 由应用启动时初始化并统一关闭。示例使用真实公开入口并作类型检查,见[模块 JSDoc 约定](../../.agents/instructions/module-conventions.md#jsdoc-与示例验证)。请求认证必须调用 iam.auth.verifyToken,不能把非空 token 视为有效身份。
|
|
323
|
+
|
|
324
|
+
## 反馈接口与普通请求关联
|
|
325
|
+
|
|
326
|
+
`kit.createFeedbackHandler` 可作为 `/api/feedback/[...path]/+server.ts` 的 GET/POST/PATCH handler,复用 api-contract schema,与 serv 使用同一数据契约;输入流受大小上限约束,客户端身份/租户字段被拒绝。宿主通过 resolveContext 从已经验证的 locals.session 映射范围和权限,null 拒绝访问,并保留现有 createHandle 的 CSRF、认证与传输加密。
|
|
327
|
+
|
|
328
|
+
createHandle 保留有效且有界的入站 X-Request-Id,补写响应头,并为异步业务传递请求 ID 和已验证用户。需要收集普通业务后端日志时,在宿主 middleware 中以 `core.context.run({ scopeId: 'configured-product' }, next)` 包住实际业务;该可信范围应与反馈接口一致,不能从客户端头推导。
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
import type { SessionData } from '@h-ai/kit'
|
|
332
|
+
import { feedback } from '@h-ai/feedback'
|
|
333
|
+
import { kit } from '@h-ai/kit'
|
|
334
|
+
|
|
335
|
+
const handler = kit.createFeedbackHandler({
|
|
336
|
+
feedback,
|
|
337
|
+
resolveContext: (event) => {
|
|
338
|
+
const session = (event.locals as { session?: SessionData }).session
|
|
339
|
+
return session
|
|
340
|
+
? { scopeId: 'configured-product', userId: session.userId, permissions: session.permissions }
|
|
341
|
+
: null
|
|
342
|
+
},
|
|
343
|
+
})
|
|
344
|
+
export const GET = handler
|
|
345
|
+
export const POST = handler
|
|
346
|
+
export const PATCH = handler
|
|
347
|
+
```
|
|
348
|
+
|
|
295
349
|
## 许可证
|
|
296
350
|
|
|
297
351
|
Apache-2.0
|