@h-ai/crypto 1.0.0-beta.21 → 1.0.0-beta.23

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
@@ -60,9 +60,9 @@ Initialize crypto successfully before using algorithms. Check HaiResult.success
60
60
  | `manager.decryptRequest` | Authenticate and decrypt a request | `payload: EncryptedRequestPayload, expected: TransportRequestContext` | `Promise<HaiResult<string>>` | `await manager.decryptRequest(encryptedRequest, requestContext)` |
61
61
  | `manager.encryptResponse` | Authenticate and encrypt a response | `request: TransportMessageContext, data: string, status: number` | `Promise<HaiResult<EncryptedPayload>>` | `await manager.encryptResponse(messageContext, "{}", 200)` |
62
62
  | `manager.close` | Release manager resources | `—` | `Promise<void>` | `await manager.close()` |
63
- | `session.init` | Initialize the encrypted session | `—` | `Promise<HaiResult<void>>` | `await session.init()` |
63
+ | `session.init` | Obtain a usable transport session | `—` | `Promise<HaiResult<void>>` | `await session.init()` |
64
64
  | `session.encryptedFetch` | Call an encrypted endpoint | `input: RequestInfo / URL, init?: RequestInit` | `Promise<Response>` | `await session.encryptedFetch("https://api.example.com/api/v1/orders")` |
65
- | `session.ready` | Check handshake readiness | `—` | `boolean` | `session.ready()` |
65
+ | `session.ready` | Check readiness for a new request | `—` | `boolean` | `session.ready()` |
66
66
  | `session.destroy` | Clear the local transport session | `—` | `void` | `session.destroy()` |
67
67
 
68
68
  Shared server/browser support (Node.js / browser)
@@ -204,8 +204,8 @@ Call `crypto.transport.createServer({ serverKeyRing, keyStore })` / `createClien
204
204
 
205
205
  1. Call `await crypto.init()` to initialize `crypto.asymmetric` and `crypto.symmetric`.
206
206
  2. On the server, create a `manager` with `crypto.transport.createServer({ serverKeyRing, keyStore })`. It holds the server key pair and stores client public keys. Inject a shared `keyStore` for multi-node deployments.
207
- 3. Expose a POST key exchange endpoint: accept `{ clientPublicKey }`, register it with `manager.registerClientKey()`, and return `{ serverKeyId: manager.getServerKeyId(), serverPublicKey: manager.getServerPublicKey(), clientId }`.
208
- 4. On the client, create a session with `crypto.transport.createClient({ keyExchangeUrl })`. The first `client.init()` or `client.encryptedFetch()` automatically performs key exchange.
207
+ 3. The POST key exchange endpoint accepts `{ clientPublicKey }`, registers it with `manager.registerClientKey()`, and returns `clientId`, `serverKeyId`, `serverPublicKey`, `maxPlaintextBytes` and optional `clientKeyTtlSeconds`. Serv and Kit take the lifetime from the registry. Custom adapters can read `manager.clientKeyTtlSeconds`.
208
+ 4. On the client, create a session with `crypto.transport.createClient({ keyExchangeUrl })`. Both `client.init()` and `client.encryptedFetch()` exchange keys on first use and when the registration nears expiry. Concurrent requests share the handshake and wait for it to succeed before sending.
209
209
  5. Every request sends an authenticated envelope `{ context, direction, status, encryptedKey, ciphertext, iv, tag, signature }`, including empty POST and GET/HEAD.
210
210
  6. Custom adapters first await `authenticateTransportRequest(manager, request)`. Only successful `data.plaintext` reaches business code; retain `data.context`.
211
211
  7. Encrypt responses with `await manager.encryptResponse(context, data, response.status)`; clients verify context, status and authenticity before returning plaintext.
@@ -279,7 +279,7 @@ const serverKeyRing = ring.data
279
279
  const server = crypto.transport.createServer({ serverKeyRing, keyStore })
280
280
  ```
281
281
 
282
- Key exchange returns `{ serverKeyId, serverPublicKey, clientId }`. Encrypted requests carry `serverKeyId`; the server selects that private key and rejects unknown/revoked IDs without trying other keys. Responses use the client's public key. Rotate by distributing both old and new versions to every node, then switching `activeKeyId` and rebuilding managers during rollout. Retain the old version through the transition, then remove it from every node. Old clients must call `destroy()` and negotiate again; do not automatically replay non-idempotent operations. Distribute the same ring through a Secret Manager or server-only private environment. Never put it in ordinary YAML, PUBLIC_/VITE_ variables, logs or public responses. CLI project creation generates separate key rings in Git-ignored `.env.dev.local/.env.test.local/.env.prod.local`. After cloning, distribute each environment’s ring through protected files or a Secret Manager. Runtime does not generate keys.
282
+ Key exchange returns `{ serverKeyId, serverPublicKey, clientId, maxPlaintextBytes }`. Encrypted requests carry `serverKeyId`; the server selects that private key and rejects unknown/revoked IDs without trying other keys. Responses use the client's public key. Rotate by distributing both old and new versions to every node, then switching `activeKeyId` and rebuilding managers during rollout. Retain the old version through the transition, then remove it from every node. Old clients must call `destroy()` and negotiate again; do not automatically replay non-idempotent operations. Distribute the same ring through a Secret Manager or server-only private environment. Never put it in ordinary YAML, PUBLIC_/VITE_ variables, logs or public responses. CLI project creation generates separate key rings in Git-ignored `.env.dev.local/.env.test.local/.env.prod.local`. After cloning, distribute each environment’s ring through protected files or a Secret Manager. Runtime does not generate keys.
283
283
 
284
284
  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.
285
285
 
@@ -289,7 +289,7 @@ Set `transport.maxPlaintextBytes` for both request and response JSON plaintext,
289
289
 
290
290
  Ordinary routes retain `transport.maxPlaintextBytes` (auto/omitted: 1 MiB). Declare Feedback report intake, evidence, attachments and ZIP download capacity separately using `routePlaintextBytes` and `feedback.budget.maxTransportBytes`; `*` matches one complete path segment and explicit host route values take precedence. Handshakes, adapter-node and proxies use the maximum route capacity. HTTP adapters admit at most 2 explicitly declared material requests above 1 MiB and a separate 16 ordinary business requests, releasing admission after the entire response or failure. Busy admission returns 503 / `hai:crypto:072`; business writes are not automatically replayed.
291
291
 
292
- 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`.
292
+ 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. `api-client` maps the error to `VALIDATION_FAILED` and preserves `cause.transportCode` and `cause.serverTime`.
293
293
 
294
294
  Custom HTTP adapters check `error instanceof TransportTimeError && error.context`, then encrypt the rejection with `createTransportErrorResponse(error, { manager, context: error.context })`. Never classify an unverified message by its timestamp.
295
295
 
package/README.ja.md CHANGED
@@ -60,9 +60,9 @@ if (!initialized.success)
60
60
  | `manager.decryptRequest` | リクエストを認証して復号する | `payload: EncryptedRequestPayload, expected: TransportRequestContext` | `Promise<HaiResult<string>>` | `await manager.decryptRequest(encryptedRequest, requestContext)` |
61
61
  | `manager.encryptResponse` | 対応する応答を認証して暗号化する | `request: TransportMessageContext, data: string, status: number` | `Promise<HaiResult<EncryptedPayload>>` | `await manager.encryptResponse(messageContext, "{}", 200)` |
62
62
  | `manager.close` | 管理器のリソースを解放する | `—` | `Promise<void>` | `await manager.close()` |
63
- | `session.init` | 暗号化セッションを初期化する | `—` | `Promise<HaiResult<void>>` | `await session.init()` |
63
+ | `session.init` | 利用可能な通信セッションを取得する | `—` | `Promise<HaiResult<void>>` | `await session.init()` |
64
64
  | `session.encryptedFetch` | 暗号化エンドポイントを呼び出す | `input: RequestInfo / URL, init?: RequestInit` | `Promise<Response>` | `await session.encryptedFetch("https://api.example.com/api/v1/orders")` |
65
- | `session.ready` | ハンドシェイク済みか確認する | `—` | `boolean` | `session.ready()` |
65
+ | `session.ready` | 新しいリクエストを送信できるか確認する | `—` | `boolean` | `session.ready()` |
66
66
  | `session.destroy` | ローカルの暗号化セッションを消去する | `—` | `void` | `session.destroy()` |
67
67
 
68
68
  サーバー・ブラウザー共通(Node.js / ブラウザー)
@@ -202,8 +202,8 @@ if (hashed.success) {
202
202
 
203
203
  1. `await crypto.init()` を呼び、`crypto.asymmetric` と `crypto.symmetric` を初期化します。
204
204
  2. サーバーで `crypto.transport.createServer({ serverKeyRing, keyStore })` を呼び、`manager` を作成します。サーバー鍵ペアとクライアント公開鍵を管理します。複数ノードでは共有 `keyStore` を注入できます。
205
- 3. POST の鍵交換 endpoint を用意します。`{ clientPublicKey }` を受け取り、`manager.registerClientKey()` で登録し、`{ serverKeyId: manager.getServerKeyId(), serverPublicKey: manager.getServerPublicKey(), clientId }` を返します。
206
- 4. クライアントで `crypto.transport.createClient({ keyExchangeUrl })` によりセッションを作成します。最初の `client.init()` または `client.encryptedFetch()` で鍵交換が自動実行されます。
205
+ 3. POST の鍵交換エンドポイントは `{ clientPublicKey }` を受け取り、`manager.registerClientKey()` で登録します。応答には `clientId`、`serverKeyId`、`serverPublicKey`、`maxPlaintextBytes`、設定時は `clientKeyTtlSeconds` が含まれます。Serv と Kit は登録ストアの有効期間を返します。独自アダプターでは `manager.clientKeyTtlSeconds` を参照できます。
206
+ 4. クライアントで `crypto.transport.createClient({ keyExchangeUrl })` によりセッションを作成します。`client.init()` と `client.encryptedFetch()` は、初回利用時と登録期限が近づいたときに鍵交換を行います。同時に発生したリクエストは鍵交換を共有し、成功してから送信します。
207
207
  5. 空の POST と GET/HEAD を含め、すべての要求が認証封装 `{ context, direction, status, encryptedKey, ciphertext, iv, tag, signature }` を送ります。
208
208
  6. 独自アダプターは先に `await authenticateTransportRequest(manager, request)` を実行し、成功時のみ `data.plaintext` を業務へ渡し、`data.context` を保持します。
209
209
  7. 応答は `await manager.encryptResponse(context, data, response.status)` で元要求に関連付け、クライアントがコンテキスト・status・完全性を検証してから平文を返します。
@@ -277,7 +277,7 @@ const serverKeyRing = ring.data
277
277
  const server = crypto.transport.createServer({ serverKeyRing, keyStore })
278
278
  ```
279
279
 
280
- 鍵交換は `{ serverKeyId, serverPublicKey, clientId }` を返し、暗号化リクエストは `serverKeyId` を送ります。サーバーは指定した秘密鍵だけを選び、不明・失効 ID は他の鍵を試さず拒否します。応答はクライアント公開鍵で暗号化します。更新時は全ノードに新旧の鍵を先に配布し、続いて `activeKeyId` を切り替えて manager を順次再構築します。移行期間後に古い ID を全ノードから削除します。旧クライアントは `destroy()` 後に再交渉し、非冪等処理を自動再送しません。同じ鍵環を Secret Manager またはサーバー専用の非公開環境から配布し、通常の YAML、PUBLIC_/VITE_、ログ、公開応答には含めないでください。CLI は Git 対象外の `.env.dev.local/.env.test.local/.env.prod.local` に独立した鍵環を作成します。クローン後は保護ファイルまたは Secret Manager で環境ごとに配布します。実行時には生成しません。
280
+ 鍵交換は `{ serverKeyId, serverPublicKey, clientId, maxPlaintextBytes }` を返し、暗号化リクエストは `serverKeyId` を送ります。サーバーは指定した秘密鍵だけを選び、不明・失効 ID は他の鍵を試さず拒否します。応答はクライアント公開鍵で暗号化します。更新時は全ノードに新旧の鍵を先に配布し、続いて `activeKeyId` を切り替えて manager を順次再構築します。移行期間後に古い ID を全ノードから削除します。旧クライアントは `destroy()` 後に再交渉し、非冪等処理を自動再送しません。同じ鍵環を Secret Manager またはサーバー専用の非公開環境から配布し、通常の YAML、PUBLIC_/VITE_、ログ、公開応答には含めないでください。CLI は Git 対象外の `.env.dev.local/.env.test.local/.env.prod.local` に独立した鍵環を作成します。クローン後は保護ファイルまたは Secret Manager で環境ごとに配布します。実行時には生成しません。
281
281
 
282
282
  認証済み 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 秒先まで許容されます。サーバー側の `maxRequestAgeSeconds`(1~600)と `maxFutureSkewSeconds`(0~300)で整数秒単位に調整できます。共有レジストリは (clientId, requestId) をアトミックに消費し、設定可能な最長の有効期間とノード間の時刻差をカバーするため、nonce を issuedAt の 11 分後まで保持します。ストレージ障害時は要求を拒否します。時計同期と Redis の noeviction/永続化により有効な nonce を保持してください。業務の再試行には別の冪等キーが必要です。
283
283
 
@@ -287,7 +287,7 @@ const server = crypto.transport.createServer({ serverKeyRing, keyStore })
287
287
 
288
288
  通常業務は `transport.maxPlaintextBytes`(auto/省略は 1 MiB)を維持します。Feedback の受理・証拠・添付・ZIP ダウンロードは `routePlaintextBytes` と `feedback.budget.maxTransportBytes` で別途宣言します。`*` は完全な1パス区間のみ一致し、ホストの明示値が優先です。ハンドシェイク、adapter-node、プロキシは経路の最大値を使用します。HTTP アダプターは個別宣言された 1 MiB 超の資料経路を最大2件、通常業務を別枠で16件まで許可し、応答完了・失敗時に解放します。混雑は503 / `hai:crypto:072` を返し、業務書込は自動再送しません。
289
289
 
290
- Serv/Kit は要求の結合、署名、MAC を検証した後、時刻の許容範囲を超えた要求に暗号化した HTTP 400 を返します。クライアントは `TransportTimeError`(`hai:crypto:071`)をスローし、検証時のサーバー UTC 時刻を表示して、自動時刻同期を有効にするか手動で時刻を設定してから再読み込みするよう案内します。`serverTime` は Unix ミリ秒です。旧サーバーが値を返さない場合も時刻調整の案内を表示します。`api-client` は `VALIDATION_FAILED` に変換し、`cause.transportCode` と `cause.serverTime` を保持します。
290
+ Serv/Kit は要求の結合、署名、MAC を検証した後、時刻の許容範囲を超えた要求に暗号化した HTTP 400 を返します。クライアントは `TransportTimeError`(`hai:crypto:071`)をスローし、検証時のサーバー UTC 時刻を表示して、自動時刻同期を有効にするか手動で時刻を設定してから再読み込みするよう案内します。`serverTime` は Unix ミリ秒です。`api-client` は `VALIDATION_FAILED` に変換し、`cause.transportCode` と `cause.serverTime` を保持します。
291
291
 
292
292
  独自 HTTP アダプターでは `error instanceof TransportTimeError && error.context` を確認し、`createTransportErrorResponse(error, { manager, context: error.context })` で拒否応答を暗号化します。未認証の報文のタイムスタンプだけで時刻異常を診断しないでください。
293
293
 
package/README.md CHANGED
@@ -60,9 +60,9 @@ if (!initialized.success)
60
60
  | `manager.decryptRequest` | 验证并解密请求 | `payload: EncryptedRequestPayload, expected: TransportRequestContext` | `Promise<HaiResult<string>>` | `await manager.decryptRequest(encryptedRequest, requestContext)` |
61
61
  | `manager.encryptResponse` | 认证并加密关联响应 | `request: TransportMessageContext, data: string, status: number` | `Promise<HaiResult<EncryptedPayload>>` | `await manager.encryptResponse(messageContext, "{}", 200)` |
62
62
  | `manager.close` | 释放管理器资源 | `—` | `Promise<void>` | `await manager.close()` |
63
- | `session.init` | 显式执行握手 | `—` | `Promise<HaiResult<void>>` | `await session.init()` |
63
+ | `session.init` | 取得可用传输会话 | `—` | `Promise<HaiResult<void>>` | `await session.init()` |
64
64
  | `session.encryptedFetch` | 按 fetch 契约调用加密接口 | `input: RequestInfo / URL, init?: RequestInit` | `Promise<Response>` | `await session.encryptedFetch("https://api.example.com/api/v1/orders")` |
65
- | `session.ready` | 检查握手状态 | `—` | `boolean` | `session.ready()` |
65
+ | `session.ready` | 检查会话能否发送新请求 | `—` | `boolean` | `session.ready()` |
66
66
  | `session.destroy` | 清除本地传输会话 | `—` | `void` | `session.destroy()` |
67
67
 
68
68
  SM2/SM3/SM4 与传输支持 Node.js / 浏览器;密码哈希仅服务端
@@ -204,8 +204,8 @@ if (hashed.success) {
204
204
 
205
205
  1. 先 `await crypto.init()`,确保 `crypto.asymmetric` 与 `crypto.symmetric` 已初始化。
206
206
  2. 服务端调用 `crypto.transport.createServer({ serverKeyRing, keyStore })` 创建 `manager`;它持有服务端密钥对,并负责保存客户端公钥。多节点部署时可注入共享 `keyStore`。
207
- 3. 服务端提供一个 POST 密钥协商端点:接收 `{ clientPublicKey }`,调用 `manager.registerClientKey()` 注册客户端,再返回 `{ serverKeyId: manager.getServerKeyId(), serverPublicKey: manager.getServerPublicKey(), clientId }`。
208
- 4. 客户端调用 `crypto.transport.createClient({ keyExchangeUrl })` 创建会话;首次 `client.init()` 或 `client.encryptedFetch()` 会自动完成这次协商。
207
+ 3. 服务端的 POST 密钥协商端点接收 `{ clientPublicKey }`,调用 `manager.registerClientKey()` 后返回 `clientId`、`serverKeyId`、`serverPublicKey`、`maxPlaintextBytes` 和可选的 `clientKeyTtlSeconds`。Serv/Kit 自动从注册表取得有效期,自定义适配器可读取 `manager.clientKeyTtlSeconds`。
208
+ 4. 客户端调用 `crypto.transport.createClient({ keyExchangeUrl })` 创建会话。首次使用或登记临近到期时,`client.init()` 和 `client.encryptedFetch()` 按需协商。并发请求共享握手,协商成功后才发送业务请求。
209
209
  5. 每次请求都发送认证封装 `{ context, direction, status, encryptedKey, ciphertext, iv, tag, signature }`;空 POST 和 GET/HEAD 也必须认证。
210
210
  6. 自定义适配器先 `await authenticateTransportRequest(manager, request)`,成功后才将 `data.plaintext` 交给业务并保存 `data.context`。
211
211
  7. 响应用 `await manager.encryptResponse(context, data, response.status)` 关联原请求;客户端校验上下文、状态和完整性后才返回明文。
@@ -279,7 +279,7 @@ const serverKeyRing = ring.data
279
279
  const server = crypto.transport.createServer({ serverKeyRing, keyStore })
280
280
  ```
281
281
 
282
- 握手返回 `{ serverKeyId, serverPublicKey, clientId }`;请求密文的 `context` 携带 `serverKeyId`,服务端按 ID 选择私钥,未知或撤销 ID 直接拒绝,不尝试其它密钥。响应仍用客户端公钥加密。轮换先向所有节点分发包含新旧版本的 ring,再切换 `activeKeyId` 并滚动重建 manager;过渡期保留旧版本,结束后删除旧 ID 并更新所有节点。旧客户端此后须 `destroy()` 后重新协商;不要自动重试非幂等业务。部署通过 Secret Manager 或服务端私有环境分发同一 ring,禁止写入普通 YAML、PUBLIC_/VITE_、日志或公开响应。CLI 创建服务端项目时为 `.env.dev.local/.env.test.local/.env.prod.local` 分别生成独立密钥环,文件不提交 Git;克隆后由受保护配置或 Secret Manager 分发对应环境秘密,也可通过私有环境或 Secret Manager 覆盖。运行时不生成密钥。
282
+ 握手返回 `{ serverKeyId, serverPublicKey, clientId, maxPlaintextBytes }`;请求密文的 `context` 携带 `serverKeyId`,服务端按 ID 选择私钥,未知或撤销 ID 直接拒绝,不尝试其它密钥。响应仍用客户端公钥加密。轮换先向所有节点分发包含新旧版本的 ring,再切换 `activeKeyId` 并滚动重建 manager;过渡期保留旧版本,结束后删除旧 ID 并更新所有节点。旧客户端此后须 `destroy()` 后重新协商;不要自动重试非幂等业务。部署通过 Secret Manager 或服务端私有环境分发同一 ring,禁止写入普通 YAML、PUBLIC_/VITE_、日志或公开响应。CLI 创建服务端项目时为 `.env.dev.local/.env.test.local/.env.prod.local` 分别生成独立密钥环,文件不提交 Git;克隆后由受保护配置或 Secret Manager 分发对应环境秘密,也可通过私有环境或 Secret Manager 覆盖。运行时不生成密钥。
283
283
 
284
284
  认证协议仅支持 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 秒。服务端可用 `maxRequestAgeSeconds`(1~600)和 `maxFutureSkewSeconds`(0~300)调整,单位为整数秒。共享 registry 原子消费 (clientId, requestId),nonce 保留到 issuedAt + 11 分钟,覆盖最长可配窗口及节点时钟差,存储故障拒绝请求。Redis 必须保留有效 nonce,使用 noeviction/可靠持久化策略;各节点时钟须同步。此能力防止同一传输消息重放,业务重试仍需自己的幂等键。
285
285
 
@@ -289,7 +289,7 @@ const server = crypto.transport.createServer({ serverKeyRing, keyStore })
289
289
 
290
290
  普通业务仍使用 `transport.maxPlaintextBytes`(auto/省略为 1 MiB);Feedback 的报告受理、证据、附件及 ZIP 下载路由通过 `routePlaintextBytes` 单独声明 `feedback.budget.maxTransportBytes`。路径中的 `*` 仅匹配一个完整路径段;宿主显式路由值优先。握手、adapter-node 与代理使用各路由最大容量,普通业务不会随材料预算扩大。HTTP 适配器为单独声明且大于 1 MiB 的材料路由保留最多 2 个槽,普通业务另有 16 个槽,完整响应/失败后释放;忙碌返回 503 / `hai:crypto:072`,不自动重放业务写入。
291
291
 
292
- Serv/Kit 对通过请求绑定、签名和 MAC 校验但超出时间窗口的请求返回加密的 HTTP 400 错误。客户端抛出 `TransportTimeError`(`hai:crypto:071`),显示校验时的服务器 UTC 时间,提示开启设备的自动校时或手工修改时间后刷新。`serverTime` 为 Unix 毫秒,旧服务端未提供该值时仍显示校时提示。`api-client` 映射为 `VALIDATION_FAILED`,在 `cause.transportCode` 和 `cause.serverTime` 保留诊断信息。
292
+ Serv/Kit 对通过请求绑定、签名和 MAC 校验但超出时间窗口的请求返回加密的 HTTP 400 错误。客户端抛出 `TransportTimeError`(`hai:crypto:071`),显示校验时的服务器 UTC 时间,提示开启设备的自动校时或手工修改时间后刷新。`serverTime` 为 Unix 毫秒。`api-client` 映射为 `VALIDATION_FAILED`,在 `cause.transportCode` 和 `cause.serverTime` 保留诊断信息。
293
293
 
294
294
  自定义 HTTP 适配器检查失败原因 `error instanceof TransportTimeError && error.context` 后,使用 `createTransportErrorResponse(error, { manager, context: error.context })` 加密拒绝响应;不能根据未认证报文的时间戳给出可信时间诊断。
295
295
 
package/dist/index.d.ts CHANGED
@@ -211,20 +211,28 @@ interface KeyExchangeRequest {
211
211
  readonly clientPublicKey: string;
212
212
  }
213
213
  /**
214
- * TLS协商响应,公开服务端版本、公钥和registry生成的clientId;不含服务端私钥。
214
+ * TLS 协商响应,包含服务端公钥、密钥版本和客户端登记信息。
215
215
  *
216
216
  * @example
217
217
  * ```ts
218
218
  * import type { KeyExchangeRequest, KeyExchangeResponse, TransportEncryptionManager } from '@h-ai/crypto'
219
219
  * async function exchange(manager: TransportEncryptionManager, body: KeyExchangeRequest): Promise<KeyExchangeResponse> {
220
220
  * const clientId = await manager.registerClientKey(body.clientPublicKey)
221
- * return { clientId, serverKeyId: manager.getServerKeyId(), serverPublicKey: manager.getServerPublicKey() }
221
+ * return {
222
+ * clientId,
223
+ * serverKeyId: manager.getServerKeyId(),
224
+ * serverPublicKey: manager.getServerPublicKey(),
225
+ * maxPlaintextBytes: manager.bodyLimits.plaintextBytes,
226
+ * clientKeyTtlSeconds: manager.clientKeyTtlSeconds,
227
+ * }
222
228
  * }
223
229
  * ```
224
230
  */
225
231
  interface KeyExchangeResponse {
226
- /** 服务端业务明文 UTF-8 字节上限;缺省表示旧服务端的 1 MiB 默认值。 */
227
- readonly maxPlaintextBytes?: number;
232
+ /** 服务端业务明文 UTF-8 字节上限。 */
233
+ readonly maxPlaintextBytes: number;
234
+ /** 客户端公钥从登记起的寿命,单位为秒;省略时客户端按无已知期限处理。 */
235
+ readonly clientKeyTtlSeconds?: number;
228
236
  /** 服务端当前加密密钥版本;业务请求必须带回该值。 */
229
237
  readonly serverKeyId: string;
230
238
  readonly serverPublicKey: string;
@@ -441,6 +449,8 @@ interface TransportCryptoServiceLike {
441
449
  interface TransportKeyStore {
442
450
  /** 公钥与nonce状态的真实共享能力;生产只接受shared。 */
443
451
  readonly stateScope: 'local' | 'shared';
452
+ /** 登记时赋予新公钥的 TTL,整数秒,范围 1~2147483647;未指定期限时省略。 */
453
+ readonly clientKeyTtlSeconds?: number;
444
454
  /**
445
455
  * 注册有效 SM2 公钥,返回服务端生成的 clientId;格式/曲线点无效或存储故障必须 reject。
446
456
  * 容量策略见具体工厂契约:数据库满容量拒绝,开发/测试内存实现允许 FIFO 淘汰公钥。
@@ -631,6 +641,8 @@ interface TransportCreateServerOptions {
631
641
  interface TransportEncryptionManager {
632
642
  /** 本管理器不可变的双向预算;由 maxPlaintextBytes 推导,安全预算独立。 */
633
643
  readonly bodyLimits: TransportBodyLimits;
644
+ /** 共享 registry 的客户端公钥 TTL,单位为秒,由协商响应传给客户端。 */
645
+ readonly clientKeyTtlSeconds?: number;
634
646
  /**
635
647
  * 当前新协商使用的服务端密钥 ID。
636
648
  * @returns 直接返回值,使用前先初始化模块。
@@ -759,7 +771,7 @@ interface TransportEncryptionManager {
759
771
  close: () => Promise<void>;
760
772
  }
761
773
  /**
762
- * 客户端单会话;首次调用自动协商,后续请求生成随机nonce并认证响应关联/签名/MAC。
774
+ * 客户端传输会话;首次调用或登记临近到期时按需协商,随后认证本次请求和响应。
763
775
  * init返回HaiResult;encryptedFetch按fetch契约reject;destroy清本地会话,不代表撤销服务端registry。
764
776
  *
765
777
  * @example
@@ -782,9 +794,9 @@ interface TransportEncryptionManager {
782
794
  */
783
795
  interface TransportClient {
784
796
  /**
785
- * 完成密钥协商;多次调用幂等(同一会话只协商一次)。
797
+ * 取得可用的传输会话;并发调用共享握手,有效期内复用已有会话。
786
798
  *
787
- * 通常无需手动调用:`encryptedFetch` 在首次请求前会自动 `init()`。
799
+ * `encryptedFetch` 在发送请求前会自动取得可用会话。
788
800
  * 协商在 30 秒内完成,握手 JSON 与单个公钥分别受 TRANSPORT_BODY_LIMITS 限制。
789
801
  * @returns HaiResult,先检查success再读取data。
790
802
  *
@@ -834,8 +846,8 @@ interface TransportClient {
834
846
  */
835
847
  encryptedFetch: typeof fetch;
836
848
  /**
837
- * 是否已完成密钥协商。
838
- * @returns 直接boolean,false表示不匹配/未就绪。
849
+ * 检查本地会话是否可用于发起新请求。
850
+ * @returns 已完成协商且尚未到续期时间时返回 true。
839
851
  *
840
852
  * @example
841
853
  * ```ts
@@ -1804,7 +1816,7 @@ interface TransportOperations {
1804
1816
  * options.fetch:实际发送 HTTP 请求的 fetch(默认 `globalThis.fetch`)。
1805
1817
  *
1806
1818
  * @remarks
1807
- * 返回的是“单会话”客户端:同一个实例会复用已协商得到的 `clientId` 与服务端公钥;
1819
+ * 同一实例在登记有效期内复用 `clientId` 与服务端公钥,临近到期时在新请求前按需协商。
1808
1820
  * 调用 `destroy()` 后会回到未协商状态。
1809
1821
  * @param options - 实际调用输入,按该方法约束验证。
1810
1822
  * @param options.keyExchangeUrl - 密钥协商端点完整 URL。
@@ -1965,15 +1977,15 @@ declare function isValidSM2PublicKey(publicKey: unknown): publicKey is string;
1965
1977
  * @example
1966
1978
  * ```ts
1967
1979
  * import { TransportTimeError } from '@h-ai/crypto'
1968
- * const error = new TransportTimeError()
1980
+ * const error = new TransportTimeError(undefined, Date.now())
1969
1981
  * // 展示 error.message;同步设备/服务器时间后由用户刷新重试。
1970
1982
  * ```
1971
1983
  */
1972
1984
  declare class TransportTimeError extends Error {
1973
- readonly context?: TransportMessageContext | undefined;
1985
+ readonly context: TransportMessageContext | undefined;
1986
+ readonly serverTime: number;
1974
1987
  readonly code: string | number;
1975
- readonly serverTime?: number;
1976
- constructor(context?: TransportMessageContext | undefined, serverTime?: number);
1988
+ constructor(context: TransportMessageContext | undefined, serverTime: number);
1977
1989
  }
1978
1990
  /**
1979
1991
  * 创建脱敏传输错误;已验证绑定、签名和 MAC 的时间/响应容量错误继续加密,请求读取超限返回 HTTP 413。