@h-ai/crypto 1.0.0-beta.14 → 1.0.0-beta.15

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
@@ -250,7 +250,7 @@ const server = crypto.transport.createServer({ serverKeyRing, keyStore })
250
250
 
251
251
  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.
252
252
 
253
- Only authenticated protocol v2 is accepted. Independent random SM4-CBC and HMAC-SM3 keys are wrapped with SM2; verify the sender signature and MAC before CBC decryption. Clients sign requests and the selected server key version signs responses. Authentication covers version, direction, serverKeyId, clientId, a random 128-bit requestId acting as the nonce, issuedAt, HTTP method, the SM3 pathHash of pathname plus the original search, and response status. Requests have a five-minute lifetime and at most 30 seconds of future clock skew. The shared registry atomically consumes (clientId, requestId), retaining it until at least issuedAt plus six minutes to cover node skew; storage failures reject requests. Synchronize node clocks and use a Redis noeviction/durable policy that preserves live nonces. Transport replay prevention does not replace business idempotency keys.
253
+ 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.
254
254
 
255
255
  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.
256
256
 
@@ -258,7 +258,7 @@ Set `transport.maxPlaintextBytes` for both request and response JSON plaintext,
258
258
 
259
259
  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.
260
260
 
261
- serv/kit return an encrypted HTTP 400 time error for an out-of-window message only after request binding, signature and MAC verification. The client throws `TransportTimeError` (`hai:crypto:071`) with advice to synchronize device/server clocks and refresh. `api-client` maps it to `VALIDATION_FAILED` and preserves `cause.transportCode`. Requests are not replayed automatically and replay windows remain unchanged; tampered or unauthenticated messages retain the generic authentication failure.
261
+ 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`.
262
262
 
263
263
  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.
264
264
 
package/README.ja.md CHANGED
@@ -250,7 +250,7 @@ const server = crypto.transport.createServer({ serverKeyRing, keyStore })
250
250
 
251
251
  鍵交換は `{ 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 で環境ごとに配布します。実行時には生成しません。
252
252
 
253
- 認証済み v2 のみを受け付けます。独立したランダム SM4-CBC 鍵と HMAC-SM3 鍵を SM2 で包み、送信者署名、MAC の順に検証してから CBC を復号します。要求はクライアント秘密鍵、応答は選択されたサーバー鍵バージョンで署名します。version、direction、serverKeyId、clientId、nonce を兼ねるランダム 128 ビット requestId、issuedAt、HTTP method、pathname と元の search の SM3 pathHash、応答 status を認証します。要求の有効期限は 5 分、未来方向の時計差は最大 30 秒です。共有 registry が (clientId, requestId) を原子的に消費し、ノード間の時計差を含め issuedAt + 6 分まで保持します。保存障害では拒否します。時計同期と Redis の noeviction/永続化により有効な nonce を保持してください。業務の再試行には別の冪等キーが必要です。
253
+ 認証済み 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 を保持してください。業務の再試行には別の冪等キーが必要です。
254
254
 
255
255
  空の POST も空平文の認証封装を送ります。GET/HEAD は X-Hai-Transport ヘッダー、204/304/HEAD など本文なし応答も同じヘッダーを使います。他の要求・応答は JSON 本文と X-Encrypted: true を使います。クライアントは元の要求コンテキストと HTTP status を照合し、マーカー欠落、旧形式、認証失敗では reject します。平文 endpoint は Serv/Kit の `encryption: disabled` ルールまたはホストの `excludePaths` で明示的に許可し、呼出側も通常の要求を選びます。API Client/Kit のラッパーは自身の `excludePaths` を使用します。CORS では X-Hai-Transport を許可・公開してください。鍵交換の公開鍵認証は TLS に依存し、IAM の業務認可は別に必要です。
256
256
 
@@ -258,7 +258,7 @@ const server = crypto.transport.createServer({ serverKeyRing, keyStore })
258
258
 
259
259
  通常業務は `transport.maxPlaintextBytes`(auto/省略は 1 MiB)を維持します。Feedback の受理・証拠・添付・ZIP ダウンロードは `routePlaintextBytes` と `feedback.budget.maxTransportBytes` で別途宣言します。`*` は完全な1パス区間のみ一致し、ホストの明示値が優先です。ハンドシェイク、adapter-node、プロキシは経路の最大値を使用します。HTTP アダプターは個別宣言された 1 MiB 超の資料経路を最大2件、通常業務を別枠で16件まで許可し、応答完了・失敗時に解放します。混雑は503 / `hai:crypto:072` を返し、業務書込は自動再送しません。
260
260
 
261
- serv/kit は要求の結合、署名、MAC を検証した後、時間窓を超えた報文に暗号化した HTTP 400 時刻エラーを返します。クライアントは `TransportTimeError`(`hai:crypto:071`)で端末とサーバーの時刻同期・再読み込みを案内します。`api-client` は `VALIDATION_FAILED` に変換し `cause.transportCode` に原因を保持します。自動再送やリプレイ時間窓の緩和は行わず、改ざん・未認証の要求は従来の認証エラーです。
261
+ Serv/Kit は要求の結合、署名、MAC を検証した後、時刻の許容範囲を超えた要求に暗号化した HTTP 400 を返します。クライアントは `TransportTimeError`(`hai:crypto:071`)をスローし、検証時のサーバー UTC 時刻を表示して、自動時刻同期を有効にするか手動で時刻を設定してから再読み込みするよう案内します。`serverTime` は Unix ミリ秒です。旧サーバーが値を返さない場合も時刻調整の案内を表示します。`api-client` は `VALIDATION_FAILED` に変換し、`cause.transportCode` と `cause.serverTime` を保持します。
262
262
 
263
263
  独自 HTTP アダプターでは `error instanceof TransportTimeError && error.context` を確認し、`createTransportErrorResponse(error, { manager, context: error.context })` で拒否応答を暗号化します。未認証の報文のタイムスタンプだけで時刻異常を診断しないでください。
264
264
 
package/README.md CHANGED
@@ -250,7 +250,7 @@ const server = crypto.transport.createServer({ serverKeyRing, keyStore })
250
250
 
251
251
  握手返回 `{ 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 覆盖。运行时不生成密钥。
252
252
 
253
- 认证协议仅支持 v2:独立随机 SM4-CBC 密钥和 HMAC-SM3 密钥由 SM2 封装,先验签、再验证 MAC,最后解密 CBC。请求由客户端私钥签名,响应由所选服务端版本签名。认证字段包括 version、direction、serverKeyId、clientId、128 位随机 requestId(兼 nonce)、issuedAt、HTTP method、pathname + 原始 search 的 SM3 pathHash,以及响应 status。请求有效期 5 分钟,未来时钟偏差最多 30 秒;共享 registry 原子消费 (clientId, requestId),nonce 至少保留到 issuedAt + 6 分钟以覆盖节点偏差,存储故障拒绝请求。Redis 必须保留有效 nonce,使用 noeviction/可靠持久化策略;各节点时钟须同步。此能力防止同一传输消息重放,业务重试仍需自己的幂等键。
253
+ 认证协议仅支持 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/可靠持久化策略;各节点时钟须同步。此能力防止同一传输消息重放,业务重试仍需自己的幂等键。
254
254
 
255
255
  空 POST 也发送空明文的认证封装;GET/HEAD 将封装放在 X-Hai-Transport 头,204/304/HEAD 等无正文响应也使用同名头承载认证封装。其它请求/响应在 JSON 正文承载封装并带 X-Encrypted: true。客户端必须关联原请求上下文和 HTTP status,缺标记、旧格式或认证失败一律 reject;明文接口由 Serv/Kit 路径规则 `encryption: disabled` 或宿主 `excludePaths` 显式允许;调用方同步选择普通请求,API Client/Kit 包装器使用自己的 `excludePaths`。CORS 须允许并暴露 X-Hai-Transport。握手公钥的真实性依赖 TLS,不替代 IAM 业务授权。
256
256
 
@@ -258,7 +258,7 @@ const server = crypto.transport.createServer({ serverKeyRing, keyStore })
258
258
 
259
259
  普通业务仍使用 `transport.maxPlaintextBytes`(auto/省略为 1 MiB);Feedback 的报告受理、证据、附件及 ZIP 下载路由通过 `routePlaintextBytes` 单独声明 `feedback.budget.maxTransportBytes`。路径中的 `*` 仅匹配一个完整路径段;宿主显式路由值优先。握手、adapter-node 与代理使用各路由最大容量,普通业务不会随材料预算扩大。HTTP 适配器为单独声明且大于 1 MiB 的材料路由保留最多 2 个槽,普通业务另有 16 个槽,完整响应/失败后释放;忙碌返回 503 / `hai:crypto:072`,不自动重放业务写入。
260
260
 
261
- serv/kit 对通过请求绑定、签名和 MAC 校验但超出时间窗口的报文返回加密的 HTTP 400 时间错误;客户端抛 `TransportTimeError`(`hai:crypto:071`),提示同步设备和服务器时间后刷新重试。`api-client` 映射为 `VALIDATION_FAILED`,在 `cause.transportCode` 保留该原因。不会自动重放请求或放宽防重放窗口;篡改或未认证报文仍返回通用认证失败。
261
+ Serv/Kit 对通过请求绑定、签名和 MAC 校验但超出时间窗口的请求返回加密的 HTTP 400 错误。客户端抛出 `TransportTimeError`(`hai:crypto:071`),显示校验时的服务器 UTC 时间,提示开启设备的自动校时或手工修改时间后刷新。`serverTime` 为 Unix 毫秒,旧服务端未提供该值时仍显示校时提示。`api-client` 映射为 `VALIDATION_FAILED`,在 `cause.transportCode` 和 `cause.serverTime` 保留诊断信息。
262
262
 
263
263
  自定义 HTTP 适配器检查失败原因 `error instanceof TransportTimeError && error.context` 后,使用 `createTransportErrorResponse(error, { manager, context: error.context })` 加密拒绝响应;不能根据未认证报文的时间戳给出可信时间诊断。
264
264
 
package/dist/index.d.ts CHANGED
@@ -165,9 +165,9 @@ declare const TRANSPORT_PROTOCOL: {
165
165
  readonly ENVELOPE_HEADER: "X-Hai-Transport";
166
166
  /** 唯一支持的认证协议版本;旧未认证格式拒绝。 */
167
167
  readonly VERSION: 2;
168
- /** 请求允许的过去时间窗口(毫秒)。 */
168
+ /** 默认请求有效期(毫秒)。 */
169
169
  readonly REQUEST_WINDOW_MS: 300000;
170
- /** 节点时钟允许的最大未来偏差(毫秒)。 */
170
+ /** 默认允许的客户端未来偏差(毫秒),也是部署约定的节点对时误差上限。 */
171
171
  readonly CLOCK_SKEW_MS: 30000;
172
172
  /** 标识响应体已加密的响应头名。 */
173
173
  readonly ENCRYPTED_HEADER: "X-Encrypted";
@@ -176,6 +176,19 @@ declare const TRANSPORT_PROTOCOL: {
176
176
  /** 密钥协商端点的默认子路径(相对于挂载前缀)。 */
177
177
  readonly DEFAULT_KEY_EXCHANGE_PATH: "/_hai/key-exchange";
178
178
  };
179
+ /**
180
+ * 服务端传输时间配置的允许范围,供 Crypto 工厂与 Serv/Kit 配置共用。
181
+ * @example
182
+ * ```ts
183
+ * import { TRANSPORT_TIME_LIMITS } from '@h-ai/crypto'
184
+ * const latest = TRANSPORT_TIME_LIMITS.maxRequestAgeSeconds
185
+ * ```
186
+ */
187
+ declare const TRANSPORT_TIME_LIMITS: Readonly<{
188
+ maxFutureSkewSeconds: 300;
189
+ minRequestAgeSeconds: 1;
190
+ maxRequestAgeSeconds: 600;
191
+ }>;
179
192
  /**
180
193
  * 密钥协商请求;只发送客户端公钥,私钥始终留在客户端,注册仍验证曲线点。
181
194
  *
@@ -296,7 +309,7 @@ interface TransportMessageContext {
296
309
  readonly clientId: string;
297
310
  /** 128 位随机十六进制 nonce,同时用于响应关联。 */
298
311
  readonly requestId: string;
299
- /** 请求创建时间,Unix 毫秒;允许过去 5 分钟、未来 30 秒。 */
312
+ /** 请求创建时间,Unix 毫秒;时间窗口由服务端配置。 */
300
313
  readonly issuedAt: number;
301
314
  /** 大写 HTTP 方法,必须与实际 Request 完全一致。 */
302
315
  readonly method: string;
@@ -573,6 +586,10 @@ interface TransportKeyStore {
573
586
  * ```
574
587
  */
575
588
  interface TransportCreateServerOptions {
589
+ /** 请求时间允许领先服务器的秒数,默认 30,整数范围 0~300。 */
590
+ readonly maxFutureSkewSeconds?: number;
591
+ /** 请求从签发起的有效秒数,默认 300,整数范围 1~600。 */
592
+ readonly maxRequestAgeSeconds?: number;
576
593
  /** 双向业务明文 UTF-8 字节上限;默认 1 MiB,整数范围 1 KiB~64 MiB。 */
577
594
  readonly maxPlaintextBytes?: number;
578
595
  /** 必填受管服务端密钥环;没有隐式随机生成或进程密钥回退。 */
@@ -1944,7 +1961,7 @@ declare function isValidSM2PublicKey(publicKey: unknown): publicKey is string;
1944
1961
  /** @h-ai/crypto — serv/kit 共用的安全传输错误响应。 */
1945
1962
 
1946
1963
  /**
1947
- * 已认证请求的时间窗口错误;只提供校时建议,不调整时钟或放宽防重放窗口。
1964
+ * 已认证请求的时间窗口错误;附带校验失败时的服务器时间(Unix 毫秒)。
1948
1965
  * @example
1949
1966
  * ```ts
1950
1967
  * import { TransportTimeError } from '@h-ai/crypto'
@@ -1955,7 +1972,8 @@ declare function isValidSM2PublicKey(publicKey: unknown): publicKey is string;
1955
1972
  declare class TransportTimeError extends Error {
1956
1973
  readonly context?: TransportMessageContext | undefined;
1957
1974
  readonly code: string | number;
1958
- constructor(context?: TransportMessageContext | undefined);
1975
+ readonly serverTime?: number;
1976
+ constructor(context?: TransportMessageContext | undefined, serverTime?: number);
1959
1977
  }
1960
1978
  /**
1961
1979
  * 创建脱敏传输错误;已验证绑定、签名和 MAC 的时间/响应容量错误继续加密,请求读取超限返回 HTTP 413。
@@ -2211,4 +2229,4 @@ interface ReldbTransportKeyStore extends TransportKeyStore {
2211
2229
  */
2212
2230
  declare function createReldbTransportKeyStore(options: CreateReldbTransportKeyStoreOptions): ReldbTransportKeyStore;
2213
2231
 
2214
- export { type AsymmetricEncryptOptions, type AsymmetricOperations, type CipherMode, type CreateRedisTransportKeyStoreOptions, type CreateReldbTransportKeyStoreOptions, type CryptoFunctions, type EncryptWithIVResult, type EncryptedPayload, type EncryptedRequestPayload, HaiCryptoError, type HashOperations, type HashOptions, type KeyExchangeRequest, type KeyExchangeResponse, type KeyPair, type PasswordConfig, type PasswordOperations, type ReadTransportBodyOptions, type ReldbTransportKeyStore, type SignOptions, type SymmetricCbcEncryptedPayload, type SymmetricCiphertextEncoding, type SymmetricDecryptInput, type SymmetricEncryptOptions, type SymmetricEncryptedPayload, type SymmetricMode, type SymmetricOperations, TRANSPORT_BODY_LIMITS, TRANSPORT_PLAINTEXT_RANGE, TRANSPORT_PROTOCOL, type TransportBodyLimits, TransportBodyTooLargeError, type TransportClient, type TransportCreateServerOptions, type TransportCryptoServiceLike, type TransportEncryptionManager, type TransportKeyPair, type TransportKeyStore, type TransportMessageContext, type TransportOperations, type TransportRequestContext, type TransportServerKeyRing, TransportTimeError, authenticateTransportRequest, createInMemoryKeyStore, createRedisTransportKeyStore, createReldbTransportKeyStore, createTransportBodyPolicy, createTransportErrorResponse, crypto, isValidSM2PublicKey, parseTransportServerKeyRing, readTransportBody, resolveTransportBodyLimits };
2232
+ export { type AsymmetricEncryptOptions, type AsymmetricOperations, type CipherMode, type CreateRedisTransportKeyStoreOptions, type CreateReldbTransportKeyStoreOptions, type CryptoFunctions, type EncryptWithIVResult, type EncryptedPayload, type EncryptedRequestPayload, HaiCryptoError, type HashOperations, type HashOptions, type KeyExchangeRequest, type KeyExchangeResponse, type KeyPair, type PasswordConfig, type PasswordOperations, type ReadTransportBodyOptions, type ReldbTransportKeyStore, type SignOptions, type SymmetricCbcEncryptedPayload, type SymmetricCiphertextEncoding, type SymmetricDecryptInput, type SymmetricEncryptOptions, type SymmetricEncryptedPayload, type SymmetricMode, type SymmetricOperations, TRANSPORT_BODY_LIMITS, TRANSPORT_PLAINTEXT_RANGE, TRANSPORT_PROTOCOL, TRANSPORT_TIME_LIMITS, type TransportBodyLimits, TransportBodyTooLargeError, type TransportClient, type TransportCreateServerOptions, type TransportCryptoServiceLike, type TransportEncryptionManager, type TransportKeyPair, type TransportKeyStore, type TransportMessageContext, type TransportOperations, type TransportRequestContext, type TransportServerKeyRing, TransportTimeError, authenticateTransportRequest, createInMemoryKeyStore, createRedisTransportKeyStore, createReldbTransportKeyStore, createTransportBodyPolicy, createTransportErrorResponse, crypto, isValidSM2PublicKey, parseTransportServerKeyRing, readTransportBody, resolveTransportBodyLimits };
package/dist/index.js CHANGED
@@ -58,7 +58,9 @@ var en_US_default = {
58
58
  crypto_transportSessionKeyDecryptFailed: "Failed to decrypt transport session key",
59
59
  crypto_transportBodyLimitInvalid: "Transport plaintext limit must be an integer between 1024 and 67108864 bytes",
60
60
  crypto_transportBodyTooLarge: "Encrypted transport payload exceeds the configured byte limit",
61
- crypto_transportTimeInvalid: "Your device and server clocks may be out of sync, or the request has expired. Enable automatic time synchronization and refresh to retry. If this continues, ask an administrator to check the server clock.",
61
+ crypto_transportTimeInvalid: "Your device and server clocks may be out of sync, or the request has expired. Enable automatic time synchronization or set your device time manually, then refresh to retry. If this continues, ask an administrator to check the server clock.",
62
+ crypto_transportTimeInvalidWithServerTime: "Your device and server clocks may be out of sync, or the request has expired. Server time is {serverTime}. Enable automatic time synchronization or set your device time manually, then refresh to retry. If this continues, ask an administrator to check the server clock.",
63
+ crypto_transportTimeConfigInvalid: "Invalid transport time settings. Future clock skew must be an integer from 0 to 300 seconds and request lifetime from 1 to 600 seconds.",
62
64
  crypto_transportBodyLimitConflict: "Explicit transport limit {limit} bytes is below the enabled module requirement of {required} bytes; adjust the module budget or explicit limit",
63
65
  crypto_transportBusy: "Material processing is busy. Please retry shortly."
64
66
  };
@@ -118,7 +120,9 @@ var ja_JP_default = {
118
120
  crypto_transportSessionKeyDecryptFailed: "\u901A\u4FE1\u30BB\u30C3\u30B7\u30E7\u30F3\u9375\u306E\u5FA9\u53F7\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
119
121
  crypto_transportBodyLimitInvalid: "\u901A\u4FE1\u5E73\u6587\u306E\u4E0A\u9650\u306F 1024 \u304B\u3089 67108864 \u30D0\u30A4\u30C8\u306E\u6574\u6570\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044",
120
122
  crypto_transportBodyTooLarge: "\u6697\u53F7\u5316\u901A\u4FE1\u306E\u30C7\u30FC\u30BF\u304C\u8A2D\u5B9A\u3055\u308C\u305F\u30D0\u30A4\u30C8\u4E0A\u9650\u3092\u8D85\u3048\u3066\u3044\u307E\u3059",
121
- crypto_transportTimeInvalid: "\u7AEF\u672B\u3068\u30B5\u30FC\u30D0\u30FC\u306E\u6642\u523B\u304C\u540C\u671F\u3057\u3066\u3044\u306A\u3044\u304B\u3001\u8981\u6C42\u306E\u6709\u52B9\u671F\u9650\u304C\u5207\u308C\u3066\u3044\u307E\u3059\u3002\u81EA\u52D5\u6642\u523B\u540C\u671F\u3092\u6709\u52B9\u306B\u3057\u3066\u30DA\u30FC\u30B8\u3092\u518D\u8AAD\u307F\u8FBC\u307F\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u89E3\u6D88\u3057\u306A\u3044\u5834\u5408\u306F\u7BA1\u7406\u8005\u306B\u30B5\u30FC\u30D0\u30FC\u306E\u6642\u523B\u78BA\u8A8D\u3092\u4F9D\u983C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
123
+ crypto_transportTimeInvalid: "\u7AEF\u672B\u3068\u30B5\u30FC\u30D0\u30FC\u306E\u6642\u523B\u304C\u540C\u671F\u3057\u3066\u3044\u306A\u3044\u304B\u3001\u8981\u6C42\u306E\u6709\u52B9\u671F\u9650\u304C\u5207\u308C\u3066\u3044\u307E\u3059\u3002\u7AEF\u672B\u306E\u81EA\u52D5\u6642\u523B\u540C\u671F\u3092\u6709\u52B9\u306B\u3059\u308B\u304B\u3001\u624B\u52D5\u3067\u6642\u523B\u3092\u8A2D\u5B9A\u3057\u3066\u304B\u3089\u3001\u30DA\u30FC\u30B8\u3092\u518D\u8AAD\u307F\u8FBC\u307F\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u89E3\u6D88\u3057\u306A\u3044\u5834\u5408\u306F\u7BA1\u7406\u8005\u306B\u30B5\u30FC\u30D0\u30FC\u306E\u6642\u523B\u78BA\u8A8D\u3092\u4F9D\u983C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
124
+ crypto_transportTimeInvalidWithServerTime: "\u7AEF\u672B\u3068\u30B5\u30FC\u30D0\u30FC\u306E\u6642\u523B\u304C\u540C\u671F\u3057\u3066\u3044\u306A\u3044\u304B\u3001\u8981\u6C42\u306E\u6709\u52B9\u671F\u9650\u304C\u5207\u308C\u3066\u3044\u307E\u3059\u3002\u30B5\u30FC\u30D0\u30FC\u6642\u523B\u306F {serverTime} \u3067\u3059\u3002\u7AEF\u672B\u306E\u81EA\u52D5\u6642\u523B\u540C\u671F\u3092\u6709\u52B9\u306B\u3059\u308B\u304B\u3001\u624B\u52D5\u3067\u6642\u523B\u3092\u8A2D\u5B9A\u3057\u3066\u304B\u3089\u3001\u30DA\u30FC\u30B8\u3092\u518D\u8AAD\u307F\u8FBC\u307F\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u89E3\u6D88\u3057\u306A\u3044\u5834\u5408\u306F\u7BA1\u7406\u8005\u306B\u30B5\u30FC\u30D0\u30FC\u306E\u6642\u523B\u78BA\u8A8D\u3092\u4F9D\u983C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
125
+ crypto_transportTimeConfigInvalid: "\u30C8\u30E9\u30F3\u30B9\u30DD\u30FC\u30C8\u306E\u6642\u523B\u8A2D\u5B9A\u304C\u7121\u52B9\u3067\u3059\u3002\u672A\u6765\u65B9\u5411\u306E\u6642\u523B\u5DEE\u306F 0\uFF5E300 \u79D2\u3001\u8981\u6C42\u306E\u6709\u52B9\u671F\u9593\u306F 1\uFF5E600 \u79D2\u306E\u6574\u6570\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
122
126
  crypto_transportBodyLimitConflict: "\u660E\u793A\u3055\u308C\u305F\u8EE2\u9001\u4E0A\u9650 {limit} \u30D0\u30A4\u30C8\u306F\u6709\u52B9\u306A\u30E2\u30B8\u30E5\u30FC\u30EB\u306E\u5FC5\u8981\u91CF {required} \u30D0\u30A4\u30C8\u672A\u6E80\u3067\u3059\u3002\u4E88\u7B97\u307E\u305F\u306F\u4E0A\u9650\u3092\u5909\u66F4\u3057\u3066\u304F\u3060\u3055\u3044",
123
127
  crypto_transportBusy: "\u8CC7\u6599\u306E\u51E6\u7406\u304C\u6DF7\u307F\u5408\u3063\u3066\u3044\u307E\u3059\u3002\u3057\u3070\u3089\u304F\u3057\u3066\u304B\u3089\u518D\u8A66\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
124
128
  };
@@ -178,7 +182,9 @@ var zh_CN_default = {
178
182
  crypto_transportSessionKeyDecryptFailed: "\u4F20\u8F93\u52A0\u5BC6\u4F1A\u8BDD\u5BC6\u94A5\u89E3\u5BC6\u5931\u8D25",
179
183
  crypto_transportBodyLimitInvalid: "\u4F20\u8F93\u660E\u6587\u5BB9\u91CF\u5FC5\u987B\u662F 1024 \u5230 67108864 \u5B57\u8282\u4E4B\u95F4\u7684\u6574\u6570",
180
184
  crypto_transportBodyTooLarge: "\u52A0\u5BC6\u4F20\u8F93\u62A5\u6587\u8D85\u8FC7\u914D\u7F6E\u7684\u5B57\u8282\u4E0A\u9650",
181
- crypto_transportTimeInvalid: "\u8BBE\u5907\u4E0E\u670D\u52A1\u5668\u65F6\u95F4\u53EF\u80FD\u4E0D\u540C\u6B65\uFF0C\u6216\u8BF7\u6C42\u5DF2\u8FC7\u671F\u3002\u8BF7\u5F00\u542F\u81EA\u52A8\u6821\u65F6\u5E76\u5237\u65B0\u91CD\u8BD5\uFF1B\u82E5\u4ECD\u5931\u8D25\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u68C0\u67E5\u670D\u52A1\u5668\u65F6\u95F4\u3002",
185
+ crypto_transportTimeInvalid: "\u8BBE\u5907\u4E0E\u670D\u52A1\u5668\u65F6\u95F4\u53EF\u80FD\u4E0D\u540C\u6B65\uFF0C\u6216\u8BF7\u6C42\u5DF2\u8FC7\u671F\u3002\u8BF7\u5F00\u542F\u8BBE\u5907\u7684\u81EA\u52A8\u6821\u65F6\u6216\u624B\u5DE5\u4FEE\u6539\u65F6\u95F4\uFF0C\u7136\u540E\u5237\u65B0\u91CD\u8BD5\u3002\u82E5\u4ECD\u5931\u8D25\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u68C0\u67E5\u670D\u52A1\u5668\u65F6\u95F4\u3002",
186
+ crypto_transportTimeInvalidWithServerTime: "\u8BBE\u5907\u4E0E\u670D\u52A1\u5668\u65F6\u95F4\u53EF\u80FD\u4E0D\u540C\u6B65\uFF0C\u6216\u8BF7\u6C42\u5DF2\u8FC7\u671F\u3002\u670D\u52A1\u5668\u65F6\u95F4\u4E3A {serverTime}\u3002\u8BF7\u5F00\u542F\u8BBE\u5907\u7684\u81EA\u52A8\u6821\u65F6\u6216\u624B\u5DE5\u4FEE\u6539\u65F6\u95F4\uFF0C\u7136\u540E\u5237\u65B0\u91CD\u8BD5\u3002\u82E5\u4ECD\u5931\u8D25\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u68C0\u67E5\u670D\u52A1\u5668\u65F6\u95F4\u3002",
187
+ crypto_transportTimeConfigInvalid: "\u4F20\u8F93\u65F6\u95F4\u914D\u7F6E\u65E0\u6548\u3002\u5141\u8BB8\u7684\u672A\u6765\u504F\u5DEE\u987B\u4E3A 0 \u81F3 300 \u79D2\u7684\u6574\u6570\uFF0C\u8BF7\u6C42\u6709\u6548\u671F\u987B\u4E3A 1 \u81F3 600 \u79D2\u7684\u6574\u6570\u3002",
182
188
  crypto_transportBodyLimitConflict: "\u663E\u5F0F\u4F20\u8F93\u4E0A\u9650 {limit} \u5B57\u8282\u5C0F\u4E8E\u5DF2\u542F\u7528\u6A21\u5757\u6240\u9700\u7684 {required} \u5B57\u8282\uFF1B\u8BF7\u8C03\u6574\u6A21\u5757\u9884\u7B97\u6216\u663E\u5F0F\u4E0A\u9650",
183
189
  crypto_transportBusy: "\u6750\u6599\u5904\u7406\u7E41\u5FD9\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5"
184
190
  };
@@ -979,9 +985,9 @@ var TRANSPORT_PROTOCOL = {
979
985
  ENVELOPE_HEADER: "X-Hai-Transport",
980
986
  /** 唯一支持的认证协议版本;旧未认证格式拒绝。 */
981
987
  VERSION: 2,
982
- /** 请求允许的过去时间窗口(毫秒)。 */
988
+ /** 默认请求有效期(毫秒)。 */
983
989
  REQUEST_WINDOW_MS: 3e5,
984
- /** 节点时钟允许的最大未来偏差(毫秒)。 */
990
+ /** 默认允许的客户端未来偏差(毫秒),也是部署约定的节点对时误差上限。 */
985
991
  CLOCK_SKEW_MS: 3e4,
986
992
  /** 标识响应体已加密的响应头名。 */
987
993
  ENCRYPTED_HEADER: "X-Encrypted",
@@ -990,6 +996,11 @@ var TRANSPORT_PROTOCOL = {
990
996
  /** 密钥协商端点的默认子路径(相对于挂载前缀)。 */
991
997
  DEFAULT_KEY_EXCHANGE_PATH: "/_hai/key-exchange"
992
998
  };
999
+ var TRANSPORT_TIME_LIMITS = Object.freeze({
1000
+ maxFutureSkewSeconds: 300,
1001
+ minRequestAgeSeconds: 1,
1002
+ maxRequestAgeSeconds: 600
1003
+ });
993
1004
 
994
1005
  // src/transport/crypto-transport-codec.ts
995
1006
  function authenticatedText(payload) {
@@ -1090,29 +1101,41 @@ function sameTransportContext(a, b) {
1090
1101
 
1091
1102
  // src/transport/crypto-transport-error.ts
1092
1103
  var TransportTimeError = class extends Error {
1093
- constructor(context) {
1094
- super(cryptoM("crypto_transportTimeInvalid"));
1104
+ constructor(context, serverTime) {
1105
+ const validTime = typeof serverTime === "number" && Number.isSafeInteger(serverTime) && serverTime >= 0 && serverTime <= 864e13 ? serverTime : void 0;
1106
+ const message = validTime === void 0 ? cryptoM("crypto_transportTimeInvalid") : cryptoM("crypto_transportTimeInvalidWithServerTime", { params: { serverTime: new Date(validTime).toISOString().replace("T", " ").replace(/\.\d{3}Z$/, " UTC") } });
1107
+ super(message);
1095
1108
  this.context = context;
1096
1109
  this.name = "TransportTimeError";
1110
+ this.serverTime = validTime;
1097
1111
  }
1098
1112
  context;
1099
1113
  code = HaiCryptoError.TRANSPORT_TIME_INVALID.code;
1114
+ serverTime;
1100
1115
  };
1101
1116
  async function createTransportErrorResponse(error, authenticated) {
1102
- const status = error instanceof TransportTimeError ? 400 : error.direction === "request" ? 413 : 500;
1103
- const body = JSON.stringify({ success: false, error: { code: error.code, message: error.message, httpStatus: status, details: error instanceof TransportTimeError ? void 0 : {
1117
+ const isTimeError = error instanceof TransportTimeError;
1118
+ const status = isTimeError ? 400 : error.direction === "request" ? 413 : 500;
1119
+ const publicError = { code: error.code, message: isTimeError ? cryptoM("crypto_transportTimeInvalid") : error.message, httpStatus: status, details: isTimeError ? void 0 : {
1104
1120
  direction: error.direction,
1105
1121
  stage: error.stage,
1106
1122
  limitBytes: error.limitBytes,
1107
1123
  actualBytes: error.actualBytes
1108
- } } });
1124
+ } };
1109
1125
  if (authenticated) {
1126
+ const encryptedError = isTimeError && error.serverTime !== void 0 ? { ...publicError, message: error.message, details: { serverTime: error.serverTime } } : publicError;
1127
+ const body = JSON.stringify({ success: false, error: encryptedError });
1110
1128
  const encrypted = await authenticated.manager.encryptResponse(authenticated.context, body, status);
1111
1129
  if (encrypted.success) {
1112
- return Response.json(encrypted.data, { status, headers: { [TRANSPORT_PROTOCOL.ENCRYPTED_HEADER]: TRANSPORT_PROTOCOL.ENCRYPTED_HEADER_VALUE } });
1130
+ const headers = new Headers({ [TRANSPORT_PROTOCOL.ENCRYPTED_HEADER]: TRANSPORT_PROTOCOL.ENCRYPTED_HEADER_VALUE });
1131
+ if (authenticated.context.method === "HEAD") {
1132
+ headers.set(TRANSPORT_PROTOCOL.ENVELOPE_HEADER, JSON.stringify(encrypted.data));
1133
+ return new Response(null, { status, headers });
1134
+ }
1135
+ return Response.json(encrypted.data, { status, headers });
1113
1136
  }
1114
1137
  }
1115
- return new Response(body, { status, headers: { "Content-Type": "application/json; charset=utf-8" } });
1138
+ return new Response(JSON.stringify({ success: false, error: publicError }), { status, headers: { "Content-Type": "application/json; charset=utf-8" } });
1116
1139
  }
1117
1140
 
1118
1141
  // src/transport/crypto-transport-client.ts
@@ -1338,8 +1361,10 @@ function createTransportClient(options) {
1338
1361
  failure = JSON.parse(decResult.data);
1339
1362
  } catch {
1340
1363
  }
1341
- if (failure?.success === false && failure.error?.code === HaiCryptoError.TRANSPORT_TIME_INVALID.code)
1342
- throw new TransportTimeError();
1364
+ if (failure?.success === false && failure.error?.code === HaiCryptoError.TRANSPORT_TIME_INVALID.code) {
1365
+ const serverTime = failure.error.details?.serverTime;
1366
+ throw new TransportTimeError(void 0, typeof serverTime === "number" ? serverTime : void 0);
1367
+ }
1343
1368
  }
1344
1369
  if (response.status === 500) {
1345
1370
  let failure;
@@ -1420,6 +1445,16 @@ function isTransportServerKeyRing(value) {
1420
1445
 
1421
1446
  // src/transport/crypto-transport-server.ts
1422
1447
  function createTransportEncryption(cryptoService, options) {
1448
+ const {
1449
+ maxFutureSkewSeconds = TRANSPORT_PROTOCOL.CLOCK_SKEW_MS / 1e3,
1450
+ maxRequestAgeSeconds = TRANSPORT_PROTOCOL.REQUEST_WINDOW_MS / 1e3
1451
+ } = options ?? {};
1452
+ if (!Number.isInteger(maxFutureSkewSeconds) || maxFutureSkewSeconds < 0 || maxFutureSkewSeconds > TRANSPORT_TIME_LIMITS.maxFutureSkewSeconds || !Number.isInteger(maxRequestAgeSeconds) || maxRequestAgeSeconds < TRANSPORT_TIME_LIMITS.minRequestAgeSeconds || maxRequestAgeSeconds > TRANSPORT_TIME_LIMITS.maxRequestAgeSeconds) {
1453
+ return err(HaiCryptoError.INVALID_INPUT, cryptoM("crypto_transportTimeConfigInvalid"));
1454
+ }
1455
+ const futureSkewMs = maxFutureSkewSeconds * 1e3;
1456
+ const requestAgeMs = maxRequestAgeSeconds * 1e3;
1457
+ const nonceRetentionMs = TRANSPORT_TIME_LIMITS.maxRequestAgeSeconds * 1e3 + 2 * TRANSPORT_PROTOCOL.CLOCK_SKEW_MS;
1423
1458
  let bodyLimits;
1424
1459
  try {
1425
1460
  bodyLimits = resolveTransportBodyLimits(options?.maxPlaintextBytes);
@@ -1494,18 +1529,20 @@ function createTransportEncryption(cryptoService, options) {
1494
1529
  const plaintext = openTransportPayload(cryptoService, authenticated, key.privateKey, clientPublicKey, bodyLimits);
1495
1530
  if (!plaintext.success)
1496
1531
  return plaintext;
1497
- const timeFailure = () => {
1498
- const error = new TransportTimeError(c);
1532
+ const timeFailure = (serverTime) => {
1533
+ const error = new TransportTimeError(c, serverTime);
1499
1534
  return err(HaiCryptoError.TRANSPORT_TIME_INVALID, error.message, error);
1500
1535
  };
1501
1536
  const now = Date.now();
1502
- if (c.issuedAt > now + TRANSPORT_PROTOCOL.CLOCK_SKEW_MS || now >= c.issuedAt + TRANSPORT_PROTOCOL.REQUEST_WINDOW_MS)
1503
- return timeFailure();
1504
- if (!await keyStore.claimRequest(c.clientId, c.requestId, c.issuedAt + TRANSPORT_PROTOCOL.REQUEST_WINDOW_MS + 2 * TRANSPORT_PROTOCOL.CLOCK_SKEW_MS)) {
1537
+ const expiresAt = c.issuedAt + requestAgeMs;
1538
+ if (c.issuedAt > now + futureSkewMs || now >= expiresAt)
1539
+ return timeFailure(now);
1540
+ if (!await keyStore.claimRequest(c.clientId, c.requestId, c.issuedAt + nonceRetentionMs)) {
1505
1541
  return transportAuthenticationFailure();
1506
1542
  }
1507
- if (Date.now() >= c.issuedAt + TRANSPORT_PROTOCOL.REQUEST_WINDOW_MS)
1508
- return timeFailure();
1543
+ const claimedAt = Date.now();
1544
+ if (claimedAt >= expiresAt)
1545
+ return timeFailure(claimedAt);
1509
1546
  return plaintext;
1510
1547
  } catch {
1511
1548
  return transportAuthenticationFailure();
@@ -1984,6 +2021,6 @@ function createReldbTransportKeyStore(options) {
1984
2021
  };
1985
2022
  }
1986
2023
 
1987
- export { HaiCryptoError, TRANSPORT_BODY_LIMITS, TRANSPORT_PLAINTEXT_RANGE, TRANSPORT_PROTOCOL, TransportBodyTooLargeError, TransportTimeError, authenticateTransportRequest, createInMemoryKeyStore, createRedisTransportKeyStore, createReldbTransportKeyStore, createTransportBodyPolicy, createTransportErrorResponse, crypto2 as crypto, isValidSM2PublicKey, parseTransportServerKeyRing, readTransportBody, resolveTransportBodyLimits };
2024
+ export { HaiCryptoError, TRANSPORT_BODY_LIMITS, TRANSPORT_PLAINTEXT_RANGE, TRANSPORT_PROTOCOL, TRANSPORT_TIME_LIMITS, TransportBodyTooLargeError, TransportTimeError, authenticateTransportRequest, createInMemoryKeyStore, createRedisTransportKeyStore, createReldbTransportKeyStore, createTransportBodyPolicy, createTransportErrorResponse, crypto2 as crypto, isValidSM2PublicKey, parseTransportServerKeyRing, readTransportBody, resolveTransportBodyLimits };
1988
2025
  //# sourceMappingURL=index.js.map
1989
2026
  //# sourceMappingURL=index.js.map