@h-ai/kit 1.0.0-beta.19 → 1.0.0-beta.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -4,25 +4,11 @@
4
4
 
5
5
  [Framework home](../../README.en.md) · Related modules: [@h-ai/core](../core/README.en.md) · [@h-ai/crypto](../crypto/README.en.md) · [@h-ai/iam](../iam/README.en.md)
6
6
 
7
- ## Introduction
8
-
9
7
  `@h-ai/kit` integrates SvelteKit with hai-framework, providing a shared request pipeline and same-origin endpoint utilities:
10
8
 
11
9
  Use runtime operations through `kit.xx`. Public namespace types are `KitFunctions` on the server and `BrowserKitFunctions` in the browser. The browser condition exposes only clients, CRUD, page tokens, and default locale operations. `AuthCookieWriter` describes the current request's minimal server authentication cookie writer. Re-exported methods and namespaces support consumer declaration emission without importing internal helpers.
12
10
 
13
- ## Core features
14
-
15
- - Handle hook composition (`createHandle` / `sequence`)
16
- - Authentication/role/permission guards (`guard.require` / `guard.check`)
17
- - Built-in Logging / RateLimit / CORS middleware, enabled through `createHandle` configuration
18
- - Shared API responses and request validation (Zod)
19
- - Bearer token utilities (server login/logout + browser token storage)
20
- - Browser client with automatic CSRF and transport encryption support
21
- - Declarative CRUD resources (`crud.define`)
22
- - A2A integration: discovery defaults to `/.well-known/agent-card.json`. With `authenticate` configured, null/undefined returns 401; anonymous access is allowed only without authentication configured. API Key parameters come from the complete Agent Card's `securitySchemes.apiKey`. Hooks and explicit A2A routes share JSON-RPC parse-error and SSE cancellation cleanup logic.
23
- - Dual build mode (`createAdapter()`)
24
-
25
- ## Getting started
11
+ ## Quick Start
26
12
 
27
13
  `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.
28
14
 
@@ -34,14 +20,7 @@ npm install svelte @sveltejs/kit zod
34
20
  npm install -D @sveltejs/vite-plugin-svelte vite
35
21
  ```
36
22
 
37
- ### Dependencies
38
-
39
- - `@h-ai/iam`: optional authentication/authorization for A2A; initialize before using kit's A2A features.
40
- - `@h-ai/crypto`: optional transport/cookie encryption; initialize before using encryption in kit.
41
-
42
- ### Quick start
43
-
44
- #### Project configuration
23
+ ### Project configuration
45
24
 
46
25
  **vite.config.ts**:
47
26
 
@@ -77,7 +56,72 @@ export default defineConfig({
77
56
 
78
57
  When using Crypto or Kit/IAM, include `sm-crypto@0.5.8` in application production dependencies. CLI projects already do this; for manual setup, run `pnpm add sm-crypto@0.5.8`. The external option alone is insufficient: adapter-node also uses direct production dependencies to preserve native package loading.
79
58
 
80
- #### Build the request pipeline
59
+ ## Features
60
+
61
+ ### Purpose, inputs and outputs
62
+
63
+ Call fragments reuse the initialized service from this page. Named variables stand for application-provided requests, schemas, resource definitions or options. Follow the corresponding examples for setup and failure handling. Check HaiResult.success and await asynchronous calls.
64
+
65
+ | Entry | Use case | Input | Output | Call fragment |
66
+ | ------------------------------------------- | ----------------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------- | --------------------------------------------------------------------------- |
67
+ | `kit.createHandle` | Request pipeline | `HandleConfig: auth/guards/logging/rateLimit/crypto/access` | `SvelteKit Handle` | `kit.createHandle({ logging: true })` |
68
+ | `kit.sequence` | Compose hooks | `...Handle` | `Handle preserving resolve options` | `kit.sequence(i18nHandle, authHandle)` |
69
+ | `kit.handler` | Endpoint boundary | `(RequestEvent) => Response \| Promise<Response>` | `SvelteKit request handler` | `kit.handler(async () => kit.response.ok({ ready: true }))` |
70
+ | `kit.guard.check` | Check permission | `session, permission` | `boolean` | `kit.guard.check(session, "orders.read")` |
71
+ | `kit.guard.require` | Require permission | `session, permission` | `void; denied access throws Response` | `kit.guard.require(session, "orders.write")` |
72
+ | `kit.guard.requireSensitive` | Verify operation purpose | `initialized IAM, current RequestEvent request/cookies, operation` | `Promise<void>; invalid proof throws Response` | `kit.guard.requireSensitive(iam, event, "orders.approve")` |
73
+ | `kit.response.ok` | Success | `data, requestId?` | `Response with HTTP 200 and success data` | `kit.response.ok({ id: "o1" })` |
74
+ | `kit.response.created` | Created | `data, requestId?` | `Response with HTTP 201 and success data` | `kit.response.created({ id: "o1" })` |
75
+ | `kit.response.noContent` | No content | `none` | `Response with HTTP 204` | `kit.response.noContent()` |
76
+ | `kit.response.error` | Custom error | `code, message, status?, requestId?, public details?` | `safe failure Response` | `kit.response.error("app:orders:001", "请求无效", 400)` |
77
+ | `kit.response.badRequest` | Bad request | `message?, requestId?` | `HTTP 400 Response` | `kit.response.badRequest("Invalid request")` |
78
+ | `kit.response.unauthorized` | Unauthorized | `message?, requestId?` | `HTTP 401 Response` | `kit.response.unauthorized()` |
79
+ | `kit.response.forbidden` | Forbidden | `message?, requestId?` | `HTTP 403 Response` | `kit.response.forbidden()` |
80
+ | `kit.response.notFound` | Not found | `message?, requestId?` | `HTTP 404 Response` | `kit.response.notFound()` |
81
+ | `kit.response.conflict` | Conflict | `message, requestId?` | `HTTP 409 Response` | `kit.response.conflict("Data changed")` |
82
+ | `kit.response.validationError` | Field validation | `{ field, message }[], requestId?` | `HTTP 422 Response; errors in safe error.cause` | `kit.response.validationError([{ field: "title", message: "请输入标题" }])` |
83
+ | `kit.response.internalError` | Internal failure | `message?, requestId?` | `HTTP 500 Response with safe message` | `kit.response.internalError()` |
84
+ | `kit.response.redirect` | Redirect | `location, status?` | `redirect Response` | `kit.response.redirect("/auth/login")` |
85
+ | `kit.response.fromResult` | Map domain result | `HaiResult<T>, httpStatusMap?, requestId?` | `HTTP Response; preserves safe code/status/traceId` | `kit.response.fromResult(result)` |
86
+ | `kit.response.fromError` | Map domain error | `HaiError, requestId?` | `safe HTTP failure Response` | `kit.response.fromError(result.error)` |
87
+ | `kit.validate.body` | Body validation | `Request, Zod schema` | `Promise<schema data>; invalid input throws Response` | `kit.validate.body(request, InputSchema)` |
88
+ | `kit.validate.query` | Query validation | `URL, Zod schema` | `schema data; invalid input throws Response` | `kit.validate.query(url, QuerySchema)` |
89
+ | `kit.validate.params` | Route validation | `Record<string,string>, Zod schema` | `schema data; invalid input throws Response` | `kit.validate.params(params, IdSchema)` |
90
+ | `kit.validate.IdParamSchema` | Resource ID | `{ id: string }` | `Zod parsed ID object` | `kit.validate.IdParamSchema.parse({ id: "o1" })` |
91
+ | `kit.validate.PaginationQuerySchema` | Pagination | `page/pageSize/search query` | `normalized pagination object` | `kit.validate.PaginationQuerySchema.parse({ page: "1" })` |
92
+ | `kit.client.create` | Same-origin client | `KitClientConfig: auth/transport/recoverSession?` | `KitClient { apiFetch, init, destroy, ready }` | `kit.client.create()` |
93
+ | `kit.client.createAuthActions` | Auth actions | `{ basePath?, ...auth client options }?` | `AuthPanel-compatible actions` | `kit.client.createAuthActions({ basePath: "/api/auth" })` |
94
+ | `kit.client.installBrowserTransport` | Install transport | `public KitConfig, { crypto, basePath? }` | `void; installs same-origin global fetch wrapper when enabled` | `kit.client.installBrowserTransport(publicKitConfig, { crypto })` |
95
+ | `kit.client.installBrowserTransportFetch` | Custom transport installation | `ClientTransportConfig` | `void; installs browser global fetch wrapper` | `kit.client.installBrowserTransportFetch(transportOptions)` |
96
+ | `kit.auth.publicPaths` | Anonymous endpoints | `none` | `string[]; prepend the host base path when needed` | `kit.auth.publicPaths` |
97
+ | `kit.auth.createApi` | Auth handlers | `initialized IAM service` | `RequestHandler map for login/OTP/reset/config/etc.` | `kit.auth.createApi(iam)` |
98
+ | `kit.auth.login` | Password login | `Cookie writer, PasswordCredentials, request context?` | `Promise<HaiResult<AuthResult>>; writes token cookies on success` | `kit.auth.login(cookies, { identifier: "alice", password })` |
99
+ | `kit.auth.loginWithOtp` | OTP login | `Cookie writer, OtpCredentials, request context?` | `Promise<HaiResult<AuthResult>>` | `kit.auth.loginWithOtp(cookies, otpCredentials)` |
100
+ | `kit.auth.loginWithLdap` | LDAP login | `Cookie writer, LdapCredentials, request context?` | `Promise<HaiResult<AuthResult>>` | `kit.auth.loginWithLdap(cookies, ldapCredentials)` |
101
+ | `kit.auth.loginWithApiKey` | API key login | `Cookie writer, ApiKeyCredentials, request context?` | `Promise<HaiResult<AuthResult>>` | `kit.auth.loginWithApiKey(cookies, apiKeyCredentials)` |
102
+ | `kit.auth.registerAndLogin` | Register and login | `Cookie writer, RegisterOptions, request context?` | `Promise<HaiResult<AuthResult>>` | `kit.auth.registerAndLogin(cookies, registration)` |
103
+ | `kit.auth.logout` | Logout | `Cookie writer, accessToken?` | `Promise<HaiResult<void>>` | `kit.auth.logout(cookies, accessToken)` |
104
+ | `kit.auth.setBrowserToken` | Page memory token | `access token string` | `void; no Web Storage persistence` | `kit.auth.setBrowserToken(accessToken)` |
105
+ | `kit.auth.clearBrowserToken` | Clear page token | `none` | `void` | `kit.auth.clearBrowserToken()` |
106
+ | `kit.crud.define` | Declare CRUD | `CrudResourceConfig` | `resource operation object` | `kit.crud.define(resourceDefinition)` |
107
+ | `kit.middleware.rateLimit` | Fixed-window limit | `{ windowMs, maxRequests, store, ...options }` | `Middleware; 429 on quota and 503 on store failure` | `kit.middleware.rateLimit({ windowMs: 60000, maxRequests: 100, store })` |
108
+ | `kit.middleware.createCacheRateLimitStore` | Shared counter | `initialized shared cache, { namespace, allowMemory? }` | `RateLimitStore` | `kit.middleware.createCacheRateLimitStore(cache, { namespace: "orders" })` |
109
+ | `kit.middleware.createMemoryRateLimitStore` | Development counter | `none; development or explicit test only` | `MemoryRateLimitStore` | `kit.middleware.createMemoryRateLimitStore()` |
110
+ | `kit.createFeedbackHandler` | Controlled feedback route | `{ feedback, resolveContext, ...options }` | `catch-all RequestHandler` | `kit.createFeedbackHandler({ feedback, resolveContext })` |
111
+ | `kit.i18n.setLocale` | Startup locale | `locale string` | `void; do not change process default per request` | `kit.i18n.setLocale("zh-CN")` |
112
+ | `kit.i18n.runWithLocale` | Request locale isolation | `locale, callback; Node server only` | `same callback return value` | `kit.i18n.runWithLocale("en-US", () => resolve(event))` |
113
+
114
+ - Handle hook composition (`createHandle` / `sequence`)
115
+ - Authentication/role/permission guards (`guard.require` / `guard.check`)
116
+ - Built-in Logging / RateLimit / CORS middleware, enabled through `createHandle` configuration
117
+ - Shared API responses and request validation (Zod)
118
+ - Bearer token utilities (server login/logout + browser token storage)
119
+ - Browser client with automatic CSRF and transport encryption support
120
+ - Declarative CRUD resources (`crud.define`)
121
+ - A2A integration: discovery defaults to `/.well-known/agent-card.json`. With `authenticate` configured, null/undefined returns 401; anonymous access is allowed only without authentication configured. API Key parameters come from the complete Agent Card's `securitySchemes.apiKey`. Hooks and explicit A2A routes share JSON-RPC parse-error and SSE cancellation cleanup logic.
122
+ - Dual build mode (`createAdapter()`)
123
+
124
+ ### Build the request pipeline
81
125
 
82
126
  When mounting an application under `/yyy/`, keep SvelteKit's `paths.base` and the actual URLs used by guards consistent. Prefix `protectedPaths`, `publicPaths` and `loginUrl` with `resolve('/').replace(/\/$/, '')` from `$app/paths` with `paths.relative: false`, for example `${base}/api/*`. Authentication pages use ``kit.client.createAuthActions({ basePath: `${base}/api/auth` })``. Kit recognizes application `/api/*` routes through route groups and mount prefixes: unauthenticated APIs return 401, while pages follow the configured login redirect. Generated templates include this wiring.
83
127
 
@@ -86,7 +130,7 @@ The Hook examples assume the application has initialized its database, shared ca
86
130
  **src/hooks.server.ts**:
87
131
 
88
132
  ```typescript
89
- import type { Handle } from '@sveltejs/kit'
133
+ import type { Handle } from '@sveltejs/kit/hooks'
90
134
  import { cache } from '@h-ai/cache'
91
135
  import { iam } from '@h-ai/iam'
92
136
  import { kit } from '@h-ai/kit'
@@ -114,7 +158,7 @@ export const handle: Handle = haiHandle
114
158
 
115
159
  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.
116
160
 
117
- #### API endpoints
161
+ ### API endpoints
118
162
 
119
163
  ```typescript
120
164
  import { kit } from '@h-ai/kit'
@@ -132,7 +176,7 @@ export const POST = kit.handler(async ({ request, locals }) => {
132
176
  })
133
177
  ```
134
178
 
135
- #### Browser client
179
+ ### Browser client
136
180
 
137
181
  `kit.client.create()` targets SvelteKit same-origin endpoints. Use `@h-ai/api-client` for cross-origin access to public APIs from Web, apps, or mini programs.
138
182
 
@@ -156,7 +200,7 @@ const response = await apiFetch('/api/users', { method: 'GET' })
156
200
  >
157
201
  > 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.
158
202
 
159
- #### Same-origin transport encryption
203
+ ### Same-origin transport encryption
160
204
 
161
205
  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.
162
206
 
@@ -242,9 +286,7 @@ Servers must explicitly inject the same managed `serverKeyRing` and a shared `ke
242
286
 
243
287
  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`.
244
288
 
245
- 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. By default, requests expire after five minutes and client timestamps may be up to 30 seconds ahead. Set `maxRequestAgeSeconds` (1–600) and `maxFutureSkewSeconds` (0–300) on the server to adjust these limits in whole seconds. The shared registry atomically consumes (clientId, requestId) and retains the nonce until issuedAt plus 11 minutes, covering the longest configurable window and server clock differences. 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.
246
-
247
- 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. Serv/Kit hosts explicitly allow plaintext through `encryption: disabled` path rules or host `excludePaths`. Callers must select ordinary requests; API Client/Kit wrappers use their own `excludePaths`. CORS must allow and expose X-Hai-Transport. TLS authenticates handshake keys; this protocol does not replace IAM authorization.
289
+ Transport uses authenticated v2 envelopes, verifying signature/MAC before decryption and binding method, path, time and response status. The shared registry atomically consumes nonces. The host still owns TLS, trusted proxies, authorization and idempotency. Custom CORS hosts must allow and expose X-Hai-Transport. See the [Transport protocol](../crypto/TRANSPORT-PROTOCOL.md) for fields, bodyless responses, clock windows and replay protection.
248
290
 
249
291
  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)。
250
292
 
@@ -252,7 +294,7 @@ Ordinary routes retain `transport.maxPlaintextBytes` (auto/omitted: 1 MiB). Decl
252
294
 
253
295
  Serv/Kit return an encrypted HTTP 400 for an out-of-window request after validating request binding, signature and MAC. The client throws `TransportTimeError` (`hai:crypto:071`), displays the server’s UTC time at validation, and asks the user to enable automatic synchronization or set the device time manually before refreshing. `serverTime` is Unix milliseconds. Clients still show synchronization guidance when an older server omits it. `api-client` maps the error to `VALIDATION_FAILED` and preserves `cause.transportCode` and `cause.serverTime`. Standard authentication actions convert authenticated transport time errors to `HaiResult` with the server time and synchronization guidance for AuthPanel.
254
296
 
255
- #### Shared transport configuration with `_kit.yml`
297
+ ### Shared transport configuration with `_kit.yml`
256
298
 
257
299
  `@h-ai/kit` provides `KitConfigSchema` / `resolveKitConfig()` so a single `_kit.yml`
258
300
  can drive both `hooks.server.ts` and browser `kit.client.installBrowserTransport()`:
@@ -296,32 +338,10 @@ export function load() {
296
338
  For server-only configuration, use `core.config.validate('kit', KitConfigSchema)` as well.
297
339
  Read the complete `_kit.yml` on the server; it can contain private proxy networks and policy-file paths. Return only an explicit public projection such as `{ kitConfig: publicKitConfig }` from `+layout.server.ts`, and consume its transport fields in the browser. Never bundle the full configuration or policy file with `?raw`, and keep secrets outside these files.
298
340
 
299
- ### Configuration
300
-
301
- `kit.createHandle()` options:
302
-
303
- | Field | Type | Description |
304
- | ------------ | ---------------------------------------- | ---------------------------------------------------------------------- |
305
- | `auth` | `HandleAuthConfig` | Authentication (verifyToken / protectedPaths / publicPaths / loginUrl) |
306
- | `rateLimit` | `object \| false` | Rate limiting (windowMs / maxRequests) |
307
- | `logging` | `boolean \| object` | Request logging (default `true`; `{ logBody: true }` logs the body) |
308
- | `crypto` | `HookCryptoConfig` | Transport + cookie encryption |
309
- | `guards` | `GuardConfig[]` | Custom guards (after automatic auth guards) |
310
- | `middleware` | `Middleware[]` | Custom middleware (after built-in middleware) |
311
- | `a2a` | `HandleA2AOperations \| HandleA2AConfig` | A2A integration (Agent Card + JSON-RPC) |
312
- | `onError` | `function` | Custom error handling |
313
-
314
- Request/response summaries use the `trace` level. Logging is enabled by default and `logBody` defaults to false. Query values come from the original `request.url`, while the path remains SvelteKit's current `event.url.pathname`. Logging does not access the tracked `event.url.search/searchParams`, so static pages can prerender with logging enabled. Password/token/secret, authorization and API-key fields are redacted in queries and optional JSON-body logs. The body is read from a clone and remains available to application code.
315
-
316
- Automatic request auditing runs only when `audit.isInitialized`. Static sites and applications without Audit need not initialize it for this handle. State is checked per request, so modules may initialize after the handle is created. Applications selecting Audit must still check `audit.init()` and stop startup on failure. Explicit `audit.log/capture` calls retain their uninitialized errors, and initialized modules still report actual write failures as `Request audit failed`. Mandatory business auditing must check explicit results and enforce the application's transaction rules.
317
-
318
- 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.
319
-
320
- The key must contain 32 hexadecimal characters. Cookies use separately derived SM4-CBC and HMAC-SHA256 keys, authenticating the version, cookie name, IV, and ciphertext before decryption to reject tampering and cross-name substitution. Legacy unauthenticated cookies become invalid after upgrading and require signing in again; there is no legacy fallback.
321
-
322
- Unused Crypto Helpers without a runtime entry or real consumers have been removed, along with the `CryptoServiceLike`, `EncryptedCookieConfig`, `CryptoCsrfConfig`, and `WebhookVerifyConfig` type exports. Protect cookies through the `kit.createHandle` configuration above; use the actual public `@h-ai/crypto` contract for other cryptographic operations.
341
+ ### Dependencies
323
342
 
324
- 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.
343
+ - `@h-ai/iam`: optional authentication/authorization for A2A; initialize before using kit's A2A features.
344
+ - `@h-ai/crypto`: optional transport/cookie encryption; initialize before using encryption in kit.
325
345
 
326
346
  ### Export paths
327
347
 
@@ -345,12 +365,6 @@ Export standard handlers from kit.auth.createApi(iam), use kit.auth.publicPaths
345
365
 
346
366
  `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.
347
367
 
348
- ### Error handling
349
-
350
- - Wrap API endpoints with `kit.handler()`.
351
- - `kit.validate.body` / `kit.guard.require` throw `Response`, handled by `kit.handler` as API control flow. Page `load` uses `kit.guard.check` with SvelteKit `error`; parse query schemas with `safeParse` and raise native page errors instead of throwing API responses.
352
- - `kit.handler()` converts non-control-flow exceptions into `500 INTERNAL_ERROR` responses.
353
-
354
368
  ### CRUD fields
355
369
 
356
370
  Declarative CRUD fields support `colSpan: 'auto' | 1 | 2` for UI form widths. `validation.message` accepts a string or `() => string` for current-locale validation messages. Configure layout through `CrudPage.form`; validate independently on the server.
@@ -369,7 +383,7 @@ Wrap Node requests and SSR in `core.i18n.runWithLocale(locale, () => handleReque
369
383
 
370
384
  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.
371
385
 
372
- ## Feedback endpoint and request correlation
386
+ ### Feedback endpoint and request correlation
373
387
 
374
388
  `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.
375
389
 
@@ -394,50 +408,96 @@ export const POST = handler
394
408
  export const PATCH = handler
395
409
  ```
396
410
 
397
- ## Error messages and correlated logs
411
+ ### Optional interactive challenges
398
412
 
399
- `kit.response.fromError/fromResult` preserve safe field-validation `cause`, explicitly public `publicExt` (mapped to `ext`), and `httpStatus`. Internal causes and extensions remain private. Each module failure produces one correlated log entry.
413
+ Export kit.auth.createApi(iam).createCaptcha as POST /api/auth/captcha; kit.auth.publicPaths includes it. kit.client.createAuthActions preserves captcha and supplies createCaptcha for automatic AuthPanel solving. Export /api/auth/password/renew for initial/expired password changes. IAM enforces proofs server-side.
400
414
 
401
- HTTP/UI boundaries expose business messages only for explicit valid 4xx statuses. Missing or invalid `httpStatus` is treated as 500 with a safe message. Custom business errors must carry a complete error definition or an explicit status.
415
+ See policy, defaults and recovery boundaries in [IAM](../iam/README.en.md).
402
416
 
403
- `kit.response.validationError(errors)` uses the same allowlist: `error.cause` contains only localized `field/message` pairs. It does not echo rejected values, SQL or additional fields, and no longer uses `details.errors`.
404
- Shortcut responses, authentication guards and rate limiting use `HaiCommonError` codes with `httpStatus`, `traceId` and `X-Trace-Id`. Unauthenticated requests use `UNAUTHORIZED.code`, denied permissions use `FORBIDDEN.code`, and throttled requests use `RATE_LIMIT_EXCEEDED.code` (429). Rate limit storage failures return a safe 503 and retain the original cause in server logs.
417
+ ### Recovering an expired session in place
405
418
 
406
- HTTP errors carry safe localized messages and a reference ID. ToastContainer hides the ID by default and reveals it through its diagnostic icon. The same ID remains in `error.traceId` and `X-Trace-Id`. The framework reuses the request ID to correlate browser and server logs. Sanitized exceptions, stacks and operation context remain in logs; internal cause/ext are not exposed. Field validation returns localized field names and messages only.
419
+ Set `kit.client.create({ recoverSession })` to sign in again without discarding the current page. Use UI `createSessionRecovery` / `SessionRecoveryDialog`: configure the public policy and original user in the root layout, pass `recovery.recover` as the callback, provide standard authentication actions, and cancel on teardown. IAM checks login `expectedUserId` before issuing a session; captcha, lockout, password renewal and access rules remain active. Only an explicitly authentication-rejected request is retried once. Cancellation, 5xx, timeouts and ambiguous writes are not replayed. Sensitive-operation proofs from an old session may need a new confirmation. Standard authentication actions disable recovery for their own calls. Custom authentication requests should pass `{ recoverSession: false }` as the third apiFetch argument to avoid recursive dialogs. terminalType (web/app/desktop/api; default web) selects a session group and does not attest hardware identity.
407
420
 
408
- Use `core.error.toPublic(error, traceId?)` at HTTP/UI boundaries and `core.error.formatMessage(error)` after translating messages; formatting is idempotent. Pass complete Error objects to logs instead of discarding their context. Database failures log driver codes, sanitized messages/stacks and table/column context; SQL text, bound values and connection credentials are excluded.
421
+ ### Page exit session policy
409
422
 
410
- When audit is initialized, createHandle binds verified session identity/roles, the platform getClientAddress result and request ID for domain audit.log calls. Automatic request.execute events contain the route template, method, status and success/failure/denial, including guard redirects and SvelteKit HTTP control flow. Bodies and error details are not copied. capture.enabled controls automatic capture; capture.includeReads enables successful reads. Failed reads remain recorded by default. Applications still explicitly record domain completion/failure events; disabling automatic capture does not disable domain logs.
423
+ `session.endOnPageHide` defaults to `false`. Hosts call `actions.bindPageExit({ enabled: config.session.endOnPageHide })` and invoke its returned cleanup on unmount; standard admin/H5/fullstack templates already bind it. A non-BFCache `pagehide` (including reload and full document navigation) makes a best effort POST to `/auth/session/close`, revoking the held access and related refresh through the existing CSRF/encrypted transport with `keepalive` and no automatic refresh. This response does not clear cookies/local credentials, avoiding a late old response overwriting a new login. Explicit logout still revokes and clears current credentials normally. Browsers may omit page exit requests; configure server `idleTimeout`/`absoluteTimeout` as appropriate. Immediate revocation on browser close is not guaranteed.
411
424
 
412
- ## Optional interactive challenges
425
+ ### Access checks on every request
413
426
 
414
- Export kit.auth.createApi(iam).createCaptcha as POST /api/auth/captcha; kit.auth.publicPaths includes it. kit.client.createAuthActions preserves captcha and supplies createCaptcha for automatic AuthPanel solving. Export /api/auth/password/renew for initial/expired password changes. IAM enforces proofs server-side.
427
+ Path rules accept `encryption: required` or `disabled`; omission inherits the host. Disabled endpoints skip application encryption while retaining access and identity checks, and reject framework envelopes. Required mode can protect custom JSON endpoints and overrides `excludePaths`; Crypto, a managed key ring and a registry are required. Conflicting modes fail when creating the Handle. Crypto-enabled CLI templates install both server and browser transport; use db and a shared database in production. Private policy and proxy configuration remain server-side; plaintext callers select their protocol using the public transport exclusions.
415
428
 
416
- See policy, defaults and recovery boundaries in [IAM](../iam/README.en.md).
429
+ Pass server-side `_kit.yml` fields `trustedProxies` and `access` explicitly to `kit.createHandle`. Examples and generated templates create one Handle after application initialization. `access` defaults to `false`. Inline `{ rules: [{ id: 'external', paths: ['/api/v1/external/**'], allowIps: ['192.0.2.0/24'] }] }` restricts matching endpoints only; `global` applies to all host-handled requests. Paths include the deployment prefix. Assets served directly by static hosting do not pass through Handle.
417
430
 
418
- ## Recovering an expired session in place
431
+ Use `access: { file: './config/security/request-access.yml' }` for a separate YAML file. It resolves from the working directory and is read once at startup, up to 1 MiB; changes require a new Handle. Global conditions and all matching rules must pass. Empty lists leave their dimension unrestricted; the default timezone is UTC. Existing sessions are denied after an allowed time window ends. Preflight and key exchange also follow policy; ordinary transport requests preserve encrypted HTTP 403 / `hai:common:104` responses.
419
432
 
420
- Set `kit.client.create({ recoverSession })` to sign in again without discarding the current page. Use UI `createSessionRecovery` / `SessionRecoveryDialog`: configure the public policy and original user in the root layout, pass `recovery.recover` as the callback, provide standard authentication actions, and cancel on teardown. IAM checks login `expectedUserId` before issuing a session; captcha, lockout, password renewal and access rules remain active. Only an explicitly authentication-rejected request is retried once. Cancellation, 5xx, timeouts and ambiguous writes are not replayed. Sensitive-operation proofs from an old session may need a new confirmation. Standard authentication actions disable recovery for their own calls. Custom authentication requests should pass `{ recoverSession: false }` as the third apiFetch argument to avoid recursive dialogs. terminalType (web/app/desktop/api; default web) selects a session group and does not attest hardware identity.
433
+ Default `trustedProxies: auto` ignores forwarded headers on ordinary hosts and adopts container networks only for private managed Compose proxy topologies. Custom ingress uses explicit proxy IPs/CIDRs; `[]` disables trust. Resolved `event.getClientAddress()`, policy, IAM and audit share the same address. See the [request access handbook](../../docs/usage/configuration/request-access.md) for edge proxies, lost source addresses, environment overrides and rollback.
421
434
 
422
- ## Page exit session policy
435
+ ## Configuration
423
436
 
424
- `session.endOnPageHide` defaults to `false`. Hosts call `actions.bindPageExit({ enabled: config.session.endOnPageHide })` and invoke its returned cleanup on unmount; standard admin/H5/fullstack templates already bind it. A non-BFCache `pagehide` (including reload and full document navigation) makes a best effort POST to `/auth/session/close`, revoking the held access and related refresh through the existing CSRF/encrypted transport with `keepalive` and no automatic refresh. This response does not clear cookies/local credentials, avoiding a late old response overwriting a new login. Explicit logout still revokes and clears current credentials normally. Browsers may omit page exit requests; configure server `idleTimeout`/`absoluteTimeout` as appropriate. Immediate revocation on browser close is not guaranteed.
437
+ ### Choose configuration by task
438
+
439
+ | Configuration entry | Scenario | Input | Effect | Example |
440
+ | ----------------------------------------- | ---------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------- | ----------------------------------------------- |
441
+ | `server.host / server.port` | Server listener | host string, port integer; HOST/PORT override | resolved Kit server config; default 127.0.0.1:3000 | `server: { host: 127.0.0.1, port: 3000 }` |
442
+ | `transport` | Same-origin transport | false or keyExchangePath/excludePaths/byte/time/route budgets | public transport config for hook and browser; no private keys | `transport: { maxPlaintextBytes: auto }` |
443
+ | `trustedProxies` | Trusted peer | auto or explicit IP/CIDR list; [] disables forwarded headers | normalized client address for access policy | `trustedProxies: []` |
444
+ | `access` | Request access policy | false or independent policy configuration | per-request IP/time/path checks; not business ownership | `access: false` |
445
+ | `createHandle.auth / guards / middleware` | Host authentication pipeline | verifyToken/operations, paths and guard/middleware callbacks | RequestEvent locals/session and rejection Response | `kit.createHandle({ auth: authOptions })` |
446
+ | `createHandle.rateLimit` | Shared rate limit | windowMs, maxRequests and explicit atomic store | fixed window; 429 over quota, 503 on unavailable storage | `kit.createHandle({ rateLimit: limitOptions })` |
447
+ | `createHandle.crypto` | Inject private dependencies | initialized crypto, serverKeyRing/keyStore, encryptedCookies/key | authenticated payload/cookies; ring stays server-only | `kit.createHandle({ crypto: cryptoOptions })` |
448
+ | `client.create` | Browser request options | auth memory store, CSRF names, transport, recoverSession | same-origin apiFetch plus init/destroy/ready | `kit.client.create({ auth: true })` |
449
+
450
+ When building Svelte applications with pnpm, register the `deepmerge@4.3.1` patch in the project's `patchedDependencies` and keep the patch file. Retain the DaisyUI style patch when that package is used. CLI-generated projects include applicable entries. Installing a framework npm package does not propagate this repository's pnpm patch configuration; manual integrations must copy the relevant [patch files](../../patches/) and registrations before reinstalling dependencies.
425
451
 
426
- ## Transport capacity and module budgets
452
+ `kit.createHandle()` options:
453
+
454
+ | Field | Type | Description |
455
+ | ------------ | ---------------------------------------- | ---------------------------------------------------------------------- |
456
+ | `auth` | `HandleAuthConfig` | Authentication (verifyToken / protectedPaths / publicPaths / loginUrl) |
457
+ | `rateLimit` | `object \| false` | Rate limiting (windowMs / maxRequests) |
458
+ | `logging` | `boolean \| object` | Request logging (default `true`; `{ logBody: true }` logs the body) |
459
+ | `crypto` | `HookCryptoConfig` | Transport + cookie encryption |
460
+ | `guards` | `GuardConfig[]` | Custom guards (after automatic auth guards) |
461
+ | `middleware` | `Middleware[]` | Custom middleware (after built-in middleware) |
462
+ | `a2a` | `HandleA2AOperations \| HandleA2AConfig` | A2A integration (Agent Card + JSON-RPC) |
463
+ | `onError` | `function` | Custom error handling |
464
+
465
+ Request/response summaries use the `trace` level. Logging is enabled by default and `logBody` defaults to false. Query values come from the original `request.url`, while the path remains SvelteKit's current `event.url.pathname`. Logging does not access the tracked `event.url.search/searchParams`, so static pages can prerender with logging enabled. Password/token/secret, authorization and API-key fields are redacted in queries and optional JSON-body logs. The body is read from a clone and remains available to application code.
466
+
467
+ Automatic request auditing runs only when `audit.isInitialized`. Static sites and applications without Audit need not initialize it for this handle. State is checked per request, so modules may initialize after the handle is created. Applications selecting Audit must still check `audit.init()` and stop startup on failure. Explicit `audit.log/capture` calls retain their uninitialized errors, and initialized modules still report actual write failures as `Request audit failed`. Mandatory business auditing must check explicit results and enforce the application's transaction rules.
468
+
469
+ 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.
470
+
471
+ The key must contain 32 hexadecimal characters. Cookies use separately derived SM4-CBC and HMAC-SHA256 keys, authenticating the version, cookie name, IV, and ciphertext before decryption to reject tampering and cross-name substitution. Legacy unauthenticated cookies become invalid after upgrading and require signing in again; there is no legacy fallback.
472
+
473
+ 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.
474
+
475
+ ### Transport capacity and module budgets
427
476
 
428
477
  For a sensitive call, supply `reauthorize: () => sensitiveActions.authorize(operation)` in the third Kit apiFetch argument or typed-client options.context. After session rotation/recovery or an explicit proof rejection, obtain a new purpose-bound proof before one retry. Cancellation sends no further write; without a callback, an old proof is never replayed.
429
478
 
430
- ## Access checks on every request
479
+ ## Error Handling
431
480
 
432
- Path rules accept `encryption: required` or `disabled`; omission inherits the host. Disabled endpoints skip application encryption while retaining access and identity checks, and reject framework envelopes. Required mode can protect custom JSON endpoints and overrides `excludePaths`; Crypto, a managed key ring and a registry are required. Conflicting modes fail when creating the Handle. Crypto-enabled CLI templates install both server and browser transport; use db and a shared database in production. Private policy and proxy configuration remain server-side; plaintext callers select their protocol using the public transport exclusions.
481
+ - Wrap API endpoints with `kit.handler()`.
482
+ - `kit.validate.body` / `kit.guard.require` throw `Response`, handled by `kit.handler` as API control flow. Page `load` uses `kit.guard.check` with SvelteKit `error`; parse query schemas with `safeParse` and raise native page errors instead of throwing API responses.
483
+ - `kit.handler()` converts non-control-flow exceptions into `500 INTERNAL_ERROR` responses.
433
484
 
434
- Pass server-side `_kit.yml` fields `trustedProxies` and `access` explicitly to `kit.createHandle`. Examples and generated templates create one Handle after application initialization. `access` defaults to `false`. Inline `{ rules: [{ id: 'external', paths: ['/api/v1/external/**'], allowIps: ['192.0.2.0/24'] }] }` restricts matching endpoints only; `global` applies to all host-handled requests. Paths include the deployment prefix. Assets served directly by static hosting do not pass through Handle.
485
+ ### Error messages and correlated logs
435
486
 
436
- Use `access: { file: './config/security/request-access.yml' }` for a separate YAML file. It resolves from the working directory and is read once at startup, up to 1 MiB; changes require a new Handle. Global conditions and all matching rules must pass. Empty lists leave their dimension unrestricted; the default timezone is UTC. Existing sessions are denied after an allowed time window ends. Preflight and key exchange also follow policy; ordinary transport requests preserve encrypted HTTP 403 / `hai:common:104` responses.
487
+ `kit.response.fromError/fromResult` preserve safe field-validation `cause`, explicitly public `publicExt` (mapped to `ext`), and `httpStatus`. Internal causes and extensions remain private. Each module failure produces one correlated log entry.
437
488
 
438
- Default `trustedProxies: auto` ignores forwarded headers on ordinary hosts and adopts container networks only for private managed Compose proxy topologies. Custom ingress uses explicit proxy IPs/CIDRs; `[]` disables trust. Resolved `event.getClientAddress()`, policy, IAM and audit share the same address. See the [request access handbook](../../docs/usage/configuration/request-access.md) for edge proxies, lost source addresses, environment overrides and rollback.
489
+ HTTP/UI boundaries expose business messages only for explicit valid 4xx statuses. Missing or invalid `httpStatus` is treated as 500 with a safe message. Custom business errors must carry a complete error definition or an explicit status.
490
+
491
+ `kit.response.validationError(errors)` uses the same allowlist: `error.cause` contains only localized `field/message` pairs. It does not echo rejected values, SQL or additional fields, and no longer uses `details.errors`.
492
+ Shortcut responses, authentication guards and rate limiting use `HaiCommonError` codes with `httpStatus`, `traceId` and `X-Trace-Id`. Unauthenticated requests use `UNAUTHORIZED.code`, denied permissions use `FORBIDDEN.code`, and throttled requests use `RATE_LIMIT_EXCEEDED.code` (429). Rate limit storage failures return a safe 503 and retain the original cause in server logs.
493
+
494
+ HTTP errors carry safe localized messages and a reference ID. ToastContainer hides the ID by default and reveals it through its diagnostic icon. The same ID remains in `error.traceId` and `X-Trace-Id`. The framework reuses the request ID to correlate browser and server logs. Sanitized exceptions, stacks and operation context remain in logs; internal cause/ext are not exposed. Field validation returns localized field names and messages only.
495
+
496
+ Use `core.error.toPublic(error, traceId?)` at HTTP/UI boundaries and `core.error.formatMessage(error)` after translating messages; formatting is idempotent. Pass complete Error objects to logs instead of discarding their context. Database failures log driver codes, sanitized messages/stacks and table/column context; SQL text, bound values and connection credentials are excluded.
497
+
498
+ When audit is initialized, createHandle binds verified session identity/roles, the platform getClientAddress result and request ID for domain audit.log calls. Automatic request.execute events contain the route template, method, status and success/failure/denial, including guard redirects and SvelteKit HTTP control flow. Bodies and error details are not copied. capture.enabled controls automatic capture; capture.includeReads enables successful reads. Failed reads remain recorded by default. Applications still explicitly record domain completion/failure events; disabling automatic capture does not disable domain logs.
439
499
 
440
- ## Development and testing
500
+ ## Development
441
501
 
442
502
  ```bash
443
503
  pnpm --filter @h-ai/kit test