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

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.
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・完全性を検証してから平文を返します。
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)` 关联原请求;客户端校验上下文、状态和完整性后才返回明文。
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
232
  /** 服务端业务明文 UTF-8 字节上限;缺省表示旧服务端的 1 MiB 默认值。 */
227
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。
package/dist/index.js CHANGED
@@ -1058,6 +1058,9 @@ var TRANSPORT_TIME_LIMITS = Object.freeze({
1058
1058
  });
1059
1059
 
1060
1060
  // src/transport/crypto-transport-codec.ts
1061
+ function isValidTransportKeyTtlSeconds(value) {
1062
+ return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= 2147483647;
1063
+ }
1061
1064
  function authenticatedText(payload) {
1062
1065
  const c = payload.context;
1063
1066
  return JSON.stringify([
@@ -1217,15 +1220,14 @@ async function createTransportErrorResponse(error, authenticated) {
1217
1220
  }
1218
1221
 
1219
1222
  // src/transport/crypto-transport-client.ts
1223
+ function isSessionUsable(session) {
1224
+ return Date.now() < session.renewAt && performance.now() < session.monotonicRenewAt;
1225
+ }
1220
1226
  function createTransportClient(options) {
1221
1227
  const baseFetch = options.fetch ?? fetch;
1222
1228
  const localLimits = options.maxPlaintextBytes === void 0 ? void 0 : resolveTransportBodyLimits(options.maxPlaintextBytes);
1223
- let bodyLimits = localLimits ?? resolveTransportBodyLimits();
1224
- let keyPair = null;
1225
- let serverPublicKey = null;
1226
- let serverKeyId = null;
1227
- let clientId = null;
1228
- let initPromise = null;
1229
+ let session;
1230
+ let initPromise;
1229
1231
  let generation = 0;
1230
1232
  let handshakeController;
1231
1233
  async function doInit(attemptGeneration) {
@@ -1237,6 +1239,8 @@ function createTransportClient(options) {
1237
1239
  return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportClientKeyGenerateFailed"));
1238
1240
  const controller = new AbortController();
1239
1241
  handshakeController = controller;
1242
+ const exchangeStartedAt = Date.now();
1243
+ const monotonicStart = performance.now();
1240
1244
  const timeout = setTimeout(() => controller.abort(new DOMException("Key exchange timed out", "TimeoutError")), 3e4);
1241
1245
  try {
1242
1246
  let response;
@@ -1269,6 +1273,7 @@ function createTransportClient(options) {
1269
1273
  if (!isKeyExchangeResponse(body)) {
1270
1274
  return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportKeyExchangeMissingFields"));
1271
1275
  }
1276
+ let bodyLimits;
1272
1277
  try {
1273
1278
  const serverLimits = resolveTransportBodyLimits(body.maxPlaintextBytes);
1274
1279
  bodyLimits = resolveTransportBodyLimits(Math.min(localLimits?.plaintextBytes ?? serverLimits.plaintextBytes, serverLimits.plaintextBytes));
@@ -1277,11 +1282,21 @@ function createTransportClient(options) {
1277
1282
  }
1278
1283
  if (attemptGeneration !== generation || controller.signal.aborted)
1279
1284
  return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportClientNotInitialized"));
1280
- keyPair = localKeyPair;
1281
- serverPublicKey = body.serverPublicKey;
1282
- serverKeyId = body.serverKeyId;
1283
- clientId = body.clientId;
1284
- return ok(void 0);
1285
+ const ttlMs = body.clientKeyTtlSeconds === void 0 ? Number.POSITIVE_INFINITY : body.clientKeyTtlSeconds * 1e3;
1286
+ const usableMs = ttlMs - Math.min(ttlMs / 10, 3e4);
1287
+ const nextSession = {
1288
+ keyPair: localKeyPair,
1289
+ serverPublicKey: body.serverPublicKey,
1290
+ serverKeyId: body.serverKeyId,
1291
+ clientId: body.clientId,
1292
+ bodyLimits,
1293
+ renewAt: exchangeStartedAt + usableMs,
1294
+ monotonicRenewAt: monotonicStart + usableMs
1295
+ };
1296
+ if (!isSessionUsable(nextSession))
1297
+ return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportKeyExchangeRequestFailed"));
1298
+ session = nextSession;
1299
+ return ok(nextSession);
1285
1300
  } finally {
1286
1301
  clearTimeout(timeout);
1287
1302
  if (handshakeController === controller)
@@ -1289,39 +1304,23 @@ function createTransportClient(options) {
1289
1304
  }
1290
1305
  }
1291
1306
  async function ensureReady() {
1292
- if (clientId && serverPublicKey)
1293
- return ok(void 0);
1307
+ if (session && isSessionUsable(session))
1308
+ return ok(session);
1294
1309
  if (!initPromise) {
1295
- const pending = doInit(generation).then((result) => {
1296
- if (!result.success && initPromise === pending)
1297
- initPromise = null;
1298
- return result;
1310
+ session = void 0;
1311
+ const pending = doInit(generation).finally(() => {
1312
+ if (initPromise === pending)
1313
+ initPromise = void 0;
1299
1314
  });
1300
1315
  initPromise = pending;
1301
1316
  }
1302
1317
  return initPromise;
1303
1318
  }
1304
- function encryptBody(plaintext, context) {
1305
- if (!serverPublicKey || !serverKeyId || !keyPair)
1306
- return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportClientNotInitialized"));
1307
- return sealTransportPayload(options.crypto, plaintext, context, "request", 0, serverPublicKey, keyPair.privateKey, bodyLimits);
1308
- }
1309
- function decryptPayload(payload) {
1310
- if (!keyPair || !serverPublicKey)
1311
- return err(HaiCommonError.INTERNAL_ERROR, cryptoM("crypto_transportClientNotInitialized"));
1312
- return openTransportPayload(options.crypto, payload, keyPair.privateKey, serverPublicKey, bodyLimits);
1313
- }
1314
- const encryptedFetch = async (input, init) => {
1315
- const requestInput = isRequestLike(input) ? input : void 0;
1316
- const signal = init?.signal ?? requestInput?.signal ?? void 0;
1319
+ async function waitForSession(signal) {
1317
1320
  signal?.throwIfAborted();
1318
- const targetUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : requestInput?.url ?? String(input);
1319
- if (targetUrl === options.keyExchangeUrl)
1320
- return baseFetch(input, init);
1321
1321
  let onAbort;
1322
- let ready;
1323
1322
  try {
1324
- ready = await (signal ? Promise.race([
1323
+ const ready = await (signal ? Promise.race([
1325
1324
  ensureReady(),
1326
1325
  new Promise((_resolve, reject) => {
1327
1326
  onAbort = () => reject(signal.reason);
@@ -1330,24 +1329,36 @@ function createTransportClient(options) {
1330
1329
  onAbort();
1331
1330
  })
1332
1331
  ]) : ensureReady());
1332
+ if (!ready.success)
1333
+ throw new Error(ready.error.message);
1334
+ return ready.data;
1333
1335
  } finally {
1334
1336
  if (onAbort)
1335
1337
  signal?.removeEventListener("abort", onAbort);
1336
1338
  }
1337
- if (!ready.success)
1338
- throw new Error(ready.error.message);
1339
+ }
1340
+ const encryptedFetch = async (input, init) => {
1341
+ const requestInput = isRequestLike(input) ? input : void 0;
1342
+ const signal = init?.signal ?? requestInput?.signal ?? void 0;
1343
+ signal?.throwIfAborted();
1344
+ const targetUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : requestInput?.url ?? String(input);
1345
+ if (targetUrl === options.keyExchangeUrl)
1346
+ return baseFetch(input, init);
1347
+ const requestGeneration = generation;
1348
+ let requestSession = await waitForSession(signal);
1349
+ if (requestGeneration !== generation)
1350
+ throw new Error(cryptoM("crypto_transportClientNotInitialized"));
1351
+ const method = (init?.method ?? requestInput?.method ?? "GET").toUpperCase();
1352
+ const requestBody = init?.body != null ? new Response(init.body).body : method === "GET" || method === "HEAD" ? null : requestInput?.body ?? null;
1353
+ const plaintext = await readTransportBody(requestBody, { maxBytes: requestSession.bodyLimits.plaintextBytes, signal });
1354
+ if (requestGeneration === generation && (requestSession !== session || !isSessionUsable(requestSession)))
1355
+ requestSession = await waitForSession(signal);
1356
+ signal?.throwIfAborted();
1357
+ if (requestGeneration !== generation || !isSessionUsable(requestSession))
1358
+ throw new Error(cryptoM("crypto_transportClientNotInitialized"));
1359
+ const { bodyLimits, clientId, keyPair, serverKeyId, serverPublicKey } = requestSession;
1339
1360
  const headers = new Headers(init?.headers ?? requestInput?.headers);
1340
1361
  headers.set(TRANSPORT_PROTOCOL.CLIENT_ID_HEADER, clientId);
1341
- let rawBody = init?.body;
1342
- if (rawBody == null && requestInput) {
1343
- const method2 = (init?.method ?? requestInput.method).toUpperCase();
1344
- if (method2 !== "GET" && method2 !== "HEAD") {
1345
- const text = await readTransportBody(requestInput.body, { maxBytes: bodyLimits.plaintextBytes, signal });
1346
- if (text)
1347
- rawBody = text;
1348
- }
1349
- }
1350
- const method = (init?.method ?? requestInput?.method ?? "GET").toUpperCase();
1351
1362
  const base = new URL(options.keyExchangeUrl, globalThis.location?.href ?? "http://localhost");
1352
1363
  const url = new URL(targetUrl, base);
1353
1364
  const pathHash = hashTransportPath(options.crypto, url.pathname + url.search);
@@ -1362,10 +1373,12 @@ function createTransportClient(options) {
1362
1373
  method,
1363
1374
  pathHash: pathHash.data
1364
1375
  };
1365
- const plaintext = rawBody == null ? "" : await readTransportBody(new Response(rawBody).body, { maxBytes: bodyLimits.plaintextBytes, signal });
1366
- const encResult = encryptBody(plaintext, context);
1367
- if (!encResult.success)
1376
+ const encResult = sealTransportPayload(options.crypto, plaintext, context, "request", 0, serverPublicKey, keyPair.privateKey, bodyLimits);
1377
+ if (!encResult.success) {
1378
+ if (encResult.error.cause instanceof TransportBodyTooLargeError)
1379
+ throw encResult.error.cause;
1368
1380
  throw new Error(encResult.error.message);
1381
+ }
1369
1382
  const serialized = JSON.stringify(encResult.data);
1370
1383
  if (new TextEncoder().encode(serialized).byteLength > bodyLimits.encryptedPayloadBytes)
1371
1384
  throw new TransportBodyTooLargeError(bodyLimits.encryptedPayloadBytes, new TextEncoder().encode(serialized).byteLength, { stage: "encrypted" });
@@ -1432,7 +1445,9 @@ function createTransportClient(options) {
1432
1445
  if (!isValidEncryptedPayload(payload, bodyLimits) || payload.direction !== "response" || payload.status !== response.status || !sameTransportContext(payload.context, context)) {
1433
1446
  throw new Error(cryptoM("crypto_transportResponseDecryptFailed"));
1434
1447
  }
1435
- const decResult = decryptPayload(payload);
1448
+ if (requestGeneration !== generation)
1449
+ throw new Error(cryptoM("crypto_transportClientNotInitialized"));
1450
+ const decResult = openTransportPayload(options.crypto, payload, keyPair.privateKey, serverPublicKey, bodyLimits);
1436
1451
  if (!decResult.success) {
1437
1452
  if (decResult.error.cause instanceof TransportBodyTooLargeError)
1438
1453
  throw decResult.error.cause;
@@ -1474,18 +1489,18 @@ function createTransportClient(options) {
1474
1489
  });
1475
1490
  };
1476
1491
  return {
1477
- init: ensureReady,
1492
+ async init() {
1493
+ const ready = await ensureReady();
1494
+ return ready.success ? ok(void 0) : ready;
1495
+ },
1478
1496
  encryptedFetch,
1479
- ready: () => clientId !== null && serverPublicKey !== null,
1497
+ ready: () => session !== void 0 && isSessionUsable(session),
1480
1498
  destroy() {
1481
1499
  generation++;
1482
1500
  handshakeController?.abort();
1483
1501
  handshakeController = void 0;
1484
- keyPair = null;
1485
- serverPublicKey = null;
1486
- serverKeyId = null;
1487
- clientId = null;
1488
- initPromise = null;
1502
+ session = void 0;
1503
+ initPromise = void 0;
1489
1504
  }
1490
1505
  };
1491
1506
  }
@@ -1500,7 +1515,7 @@ function isKeyExchangeResponse(body) {
1500
1515
  if (!body || typeof body !== "object")
1501
1516
  return false;
1502
1517
  const fields = body;
1503
- return typeof fields.serverPublicKey === "string" && fields.serverPublicKey.length > 0 && new TextEncoder().encode(fields.serverPublicKey).byteLength <= TRANSPORT_BODY_LIMITS.publicKeyBytes && typeof fields.clientId === "string" && fields.clientId.length > 0 && typeof fields.serverKeyId === "string" && /^[\w-]{1,64}$/.test(fields.serverKeyId);
1518
+ return typeof fields.serverPublicKey === "string" && fields.serverPublicKey.length > 0 && new TextEncoder().encode(fields.serverPublicKey).byteLength <= TRANSPORT_BODY_LIMITS.publicKeyBytes && typeof fields.clientId === "string" && fields.clientId.length > 0 && typeof fields.serverKeyId === "string" && /^[\w-]{1,64}$/.test(fields.serverKeyId) && (fields.clientKeyTtlSeconds === void 0 || isValidTransportKeyTtlSeconds(fields.clientKeyTtlSeconds));
1504
1519
  }
1505
1520
  function parseTransportServerKeyRing(serialized) {
1506
1521
  try {
@@ -1554,6 +1569,9 @@ function createTransportEncryption(cryptoService, options) {
1554
1569
  }
1555
1570
  if (core.module.requiresSharedState(globalThis.process?.env ?? {}) && options.keyStore.stateScope !== "shared")
1556
1571
  return err(HaiCryptoError.INVALID_INPUT, cryptoM("crypto_transportKeyStoreRequired"));
1572
+ const { clientKeyTtlSeconds } = options.keyStore;
1573
+ if (clientKeyTtlSeconds !== void 0 && !isValidTransportKeyTtlSeconds(clientKeyTtlSeconds))
1574
+ return err(HaiCryptoError.INVALID_INPUT, cryptoM("crypto_transportKeyStoreTtlInvalid"));
1557
1575
  const serverKeys = /* @__PURE__ */ new Map();
1558
1576
  for (const [id, key] of Object.entries(ring.data.keys)) {
1559
1577
  const probe = cryptoService.asymmetric.encrypt("hai-transport-key-check", key.publicKey);
@@ -1569,6 +1587,7 @@ function createTransportEncryption(cryptoService, options) {
1569
1587
  const keyStore = options.keyStore;
1570
1588
  const manager = {
1571
1589
  bodyLimits,
1590
+ clientKeyTtlSeconds,
1572
1591
  getServerKeyId() {
1573
1592
  return activeKeyId;
1574
1593
  },
@@ -1886,13 +1905,6 @@ function createInMemoryKeyStore(maxClients = DEFAULT_MAX_CLIENTS) {
1886
1905
 
1887
1906
  // src/transport/store-provider/crypto-transport-store-redis.ts
1888
1907
  var TRANSPORT_KEY_CACHE_PREFIX = "hai:crypto:transport:client";
1889
- function normalizeTtlSeconds(ttlSeconds) {
1890
- if (ttlSeconds === void 0)
1891
- return void 0;
1892
- if (!Number.isSafeInteger(ttlSeconds) || ttlSeconds <= 0 || ttlSeconds > 2147483647)
1893
- throw new TypeError(cryptoM("crypto_transportKeyStoreTtlInvalid"));
1894
- return ttlSeconds;
1895
- }
1896
1908
  function createClientId2() {
1897
1909
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function")
1898
1910
  return `c_${crypto.randomUUID()}`;
@@ -1909,11 +1921,14 @@ function unwrapResult(operation, result) {
1909
1921
  throw toOperationError(operation, result.error);
1910
1922
  }
1911
1923
  function createRedisTransportKeyStore(options) {
1912
- const ttlSeconds = normalizeTtlSeconds(options.ttlSeconds);
1924
+ const { ttlSeconds } = options;
1925
+ if (ttlSeconds !== void 0 && !isValidTransportKeyTtlSeconds(ttlSeconds))
1926
+ throw new TypeError(cryptoM("crypto_transportKeyStoreTtlInvalid"));
1913
1927
  function buildCacheKey(clientId) {
1914
1928
  return `${TRANSPORT_KEY_CACHE_PREFIX}:${clientId}`;
1915
1929
  }
1916
1930
  return {
1931
+ clientKeyTtlSeconds: ttlSeconds,
1917
1932
  get stateScope() {
1918
1933
  return options.cache.config?.type === "redis" ? "shared" : "local";
1919
1934
  },
@@ -1972,7 +1987,7 @@ function unwrapResult2(operation, result) {
1972
1987
  }
1973
1988
  function createReldbTransportKeyStore(options) {
1974
1989
  const { ttlSeconds } = options;
1975
- if (ttlSeconds !== void 0 && (!Number.isSafeInteger(ttlSeconds) || ttlSeconds <= 0 || ttlSeconds > 2147483647))
1990
+ if (ttlSeconds !== void 0 && !isValidTransportKeyTtlSeconds(ttlSeconds))
1976
1991
  throw new TypeError(cryptoM("crypto_transportKeyStoreTtlInvalid"));
1977
1992
  const maxClients = options.maxClients ?? 1e4;
1978
1993
  if (!Number.isSafeInteger(maxClients) || maxClients <= 0)
@@ -2072,6 +2087,7 @@ function createReldbTransportKeyStore(options) {
2072
2087
  return withCapacityLock(pruneRows);
2073
2088
  }
2074
2089
  return {
2090
+ clientKeyTtlSeconds: ttlSeconds,
2075
2091
  get stateScope() {
2076
2092
  return options.reldb.config?.type && options.reldb.config.type !== "sqlite" ? "shared" : "local";
2077
2093
  },