@h-ai/crypto 1.0.0-beta.20 → 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 +4 -4
- package/README.ja.md +4 -4
- package/README.md +4 -4
- package/dist/index.d.ts +20 -8
- package/dist/index.js +84 -68
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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` |
|
|
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
|
|
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.
|
|
208
|
-
4. On the client, create a session with `crypto.transport.createClient({ keyExchangeUrl })`.
|
|
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` |
|
|
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` |
|
|
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
|
|
206
|
-
4. クライアントで `crypto.transport.createClient({ keyExchangeUrl })`
|
|
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` |
|
|
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` |
|
|
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.
|
|
208
|
-
4. 客户端调用 `crypto.transport.createClient({ keyExchangeUrl })`
|
|
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
|
|
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 {
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
1224
|
-
let
|
|
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
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
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 (
|
|
1293
|
-
return ok(
|
|
1307
|
+
if (session && isSessionUsable(session))
|
|
1308
|
+
return ok(session);
|
|
1294
1309
|
if (!initPromise) {
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
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
|
|
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
|
-
|
|
1338
|
-
|
|
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
|
|
1366
|
-
|
|
1367
|
-
|
|
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
|
-
|
|
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
|
|
1492
|
+
async init() {
|
|
1493
|
+
const ready = await ensureReady();
|
|
1494
|
+
return ready.success ? ok(void 0) : ready;
|
|
1495
|
+
},
|
|
1478
1496
|
encryptedFetch,
|
|
1479
|
-
ready: () =>
|
|
1497
|
+
ready: () => session !== void 0 && isSessionUsable(session),
|
|
1480
1498
|
destroy() {
|
|
1481
1499
|
generation++;
|
|
1482
1500
|
handshakeController?.abort();
|
|
1483
1501
|
handshakeController = void 0;
|
|
1484
|
-
|
|
1485
|
-
|
|
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 =
|
|
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 &&
|
|
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
|
},
|