@h-ai/serv 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 +2 -2
- package/README.ja.md +2 -2
- package/README.md +2 -2
- package/dist/index.d.ts +14 -0
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/package.json +16 -16
package/README.en.md
CHANGED
|
@@ -223,7 +223,7 @@ const app = serv.createApp({
|
|
|
223
223
|
|
|
224
224
|
> **Security**: with `transport` enabled, business requests need valid `X-Client-Id` except `excludePaths` and key exchange. Missing/unknown clientId returns 400. Unencryptable bodies, non-JSON responses, or encryption size limits return errors rather than forwarding plaintext business data.
|
|
225
225
|
|
|
226
|
-
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.
|
|
226
|
+
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.
|
|
227
227
|
|
|
228
228
|
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.
|
|
229
229
|
|
|
@@ -231,7 +231,7 @@ Set `transport.maxPlaintextBytes` for both request and response JSON plaintext,
|
|
|
231
231
|
|
|
232
232
|
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.
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
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`.
|
|
235
235
|
|
|
236
236
|
#### Token authentication (`iam` / `refreshCookie`)
|
|
237
237
|
|
package/README.ja.md
CHANGED
|
@@ -223,7 +223,7 @@ const app = serv.createApp({
|
|
|
223
223
|
|
|
224
224
|
> **安全方針**:`transport` 有効時は `excludePaths` と鍵交換以外で有効な `X-Client-Id` が必要です。不足・未知の clientId は 400、暗号化不能・非 JSON 応答・上限超過はエラーになり、平文の業務データは渡しません。
|
|
225
225
|
|
|
226
|
-
認証済み v2 のみを受け付けます。独立したランダム SM4-CBC 鍵と HMAC-SM3 鍵を SM2 で包み、送信者署名、MAC の順に検証してから CBC を復号します。要求はクライアント秘密鍵、応答は選択されたサーバー鍵バージョンで署名します。version、direction、serverKeyId、clientId、nonce を兼ねるランダム 128 ビット requestId、issuedAt、HTTP method、pathname と元の search の SM3 pathHash、応答 status
|
|
226
|
+
認証済み 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 を保持してください。業務の再試行には別の冪等キーが必要です。
|
|
227
227
|
|
|
228
228
|
空の 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 の業務認可は別に必要です。
|
|
229
229
|
|
|
@@ -231,7 +231,7 @@ const app = serv.createApp({
|
|
|
231
231
|
|
|
232
232
|
通常業務は `transport.maxPlaintextBytes`(auto/省略は 1 MiB)を維持します。Feedback の受理・証拠・添付・ZIP ダウンロードは `routePlaintextBytes` と `feedback.budget.maxTransportBytes` で別途宣言します。`*` は完全な1パス区間のみ一致し、ホストの明示値が優先です。ハンドシェイク、adapter-node、プロキシは経路の最大値を使用します。HTTP アダプターは個別宣言された 1 MiB 超の資料経路を最大2件、通常業務を別枠で16件まで許可し、応答完了・失敗時に解放します。混雑は503 / `hai:crypto:072` を返し、業務書込は自動再送しません。
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
Serv/Kit は要求の結合、署名、MAC を検証した後、時刻の許容範囲を超えた要求に暗号化した HTTP 400 を返します。クライアントは `TransportTimeError`(`hai:crypto:071`)をスローし、検証時のサーバー UTC 時刻を表示して、自動時刻同期を有効にするか手動で時刻を設定してから再読み込みするよう案内します。`serverTime` は Unix ミリ秒です。旧サーバーが値を返さない場合も時刻調整の案内を表示します。`api-client` は `VALIDATION_FAILED` に変換し、`cause.transportCode` と `cause.serverTime` を保持します。
|
|
235
235
|
|
|
236
236
|
#### Token 認証(`iam` / `refreshCookie`)
|
|
237
237
|
|
package/README.md
CHANGED
|
@@ -223,7 +223,7 @@ const app = serv.createApp({
|
|
|
223
223
|
|
|
224
224
|
> **安全策略**:启用 `transport` 后,除 `excludePaths` 与密钥协商端点外,业务请求必须携带有效 `X-Client-Id`。缺失或未知 clientId 会返回 400;响应体无法加密、非 JSON 业务响应或超过单次加密上限时返回错误,不会明文透传业务数据。
|
|
225
225
|
|
|
226
|
-
认证协议仅支持 v2:独立随机 SM4-CBC 密钥和 HMAC-SM3 密钥由 SM2 封装,先验签、再验证 MAC,最后解密 CBC。请求由客户端私钥签名,响应由所选服务端版本签名。认证字段包括 version、direction、serverKeyId、clientId、128 位随机 requestId(兼 nonce)、issuedAt、HTTP method、pathname + 原始 search 的 SM3 pathHash,以及响应 status
|
|
226
|
+
认证协议仅支持 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/可靠持久化策略;各节点时钟须同步。此能力防止同一传输消息重放,业务重试仍需自己的幂等键。
|
|
227
227
|
|
|
228
228
|
空 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 业务授权。
|
|
229
229
|
|
|
@@ -231,7 +231,7 @@ const app = serv.createApp({
|
|
|
231
231
|
|
|
232
232
|
普通业务仍使用 `transport.maxPlaintextBytes`(auto/省略为 1 MiB);Feedback 的报告受理、证据、附件及 ZIP 下载路由通过 `routePlaintextBytes` 单独声明 `feedback.budget.maxTransportBytes`。路径中的 `*` 仅匹配一个完整路径段;宿主显式路由值优先。握手、adapter-node 与代理使用各路由最大容量,普通业务不会随材料预算扩大。HTTP 适配器为单独声明且大于 1 MiB 的材料路由保留最多 2 个槽,普通业务另有 16 个槽,完整响应/失败后释放;忙碌返回 503 / `hai:crypto:072`,不自动重放业务写入。
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
Serv/Kit 对通过请求绑定、签名和 MAC 校验但超出时间窗口的请求返回加密的 HTTP 400 错误。客户端抛出 `TransportTimeError`(`hai:crypto:071`),显示校验时的服务器 UTC 时间,提示开启设备的自动校时或手工修改时间后刷新。`serverTime` 为 Unix 毫秒,旧服务端未提供该值时仍显示校时提示。`api-client` 映射为 `VALIDATION_FAILED`,在 `cause.transportCode` 和 `cause.serverTime` 保留诊断信息。
|
|
235
235
|
|
|
236
236
|
#### Token 认证(`iam` / `refreshCookie`)
|
|
237
237
|
|
package/dist/index.d.ts
CHANGED
|
@@ -232,6 +232,10 @@ interface ServHealthHttpConfig {
|
|
|
232
232
|
interface ServTransportRuntimeConfig {
|
|
233
233
|
/** 双向明文 UTF-8 字节上限;省略使用 1 MiB,范围 1 KiB~64 MiB。 */
|
|
234
234
|
readonly maxPlaintextBytes?: number;
|
|
235
|
+
/** 请求时间允许领先服务器的秒数,默认 30,整数范围 0~300。 */
|
|
236
|
+
readonly maxFutureSkewSeconds?: number;
|
|
237
|
+
/** 请求从签发起的有效秒数,默认 300,整数范围 1~600。 */
|
|
238
|
+
readonly maxRequestAgeSeconds?: number;
|
|
235
239
|
/** 按完整路径段匹配的明文容量;* 匹配单段。 */
|
|
236
240
|
readonly routePlaintextBytes?: Readonly<Record<string, number>>;
|
|
237
241
|
readonly keyExchangePath: `/${string}`;
|
|
@@ -322,6 +326,8 @@ type ServCorsRuntimeConfigInput = Partial<ServCorsRuntimeConfig>;
|
|
|
322
326
|
*/
|
|
323
327
|
type ServTransportRuntimeConfigInput = Partial<{
|
|
324
328
|
readonly maxPlaintextBytes: number | 'auto';
|
|
329
|
+
readonly maxFutureSkewSeconds: number;
|
|
330
|
+
readonly maxRequestAgeSeconds: number;
|
|
325
331
|
readonly routePlaintextBytes: Readonly<Record<string, number>>;
|
|
326
332
|
readonly keyExchangePath: `/${string}`;
|
|
327
333
|
readonly excludePaths: readonly `/${string}`[];
|
|
@@ -449,6 +455,8 @@ declare const ServConfigSchema: z.ZodPipe<z.ZodObject<{
|
|
|
449
455
|
}, z.core.$strip>>;
|
|
450
456
|
transport: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<false>, z.ZodObject<{
|
|
451
457
|
maxPlaintextBytes: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodLiteral<"auto">]>, z.ZodTransform<number | undefined, number | "auto">>>;
|
|
458
|
+
maxFutureSkewSeconds: z.ZodOptional<z.ZodNumber>;
|
|
459
|
+
maxRequestAgeSeconds: z.ZodOptional<z.ZodNumber>;
|
|
452
460
|
routePlaintextBytes: z.ZodOptional<z.ZodRecord<z.ZodCustom<`/${string}`, `/${string}`>, z.ZodNumber>>;
|
|
453
461
|
keyExchangePath: z.ZodDefault<z.ZodCustom<`/${string}`, `/${string}`>>;
|
|
454
462
|
excludePaths: z.ZodDefault<z.ZodArray<z.ZodCustom<`/${string}`, `/${string}`>>>;
|
|
@@ -517,6 +525,8 @@ declare const ServConfigSchema: z.ZodPipe<z.ZodObject<{
|
|
|
517
525
|
keyExchangePath: `/${string}`;
|
|
518
526
|
excludePaths: `/${string}`[];
|
|
519
527
|
maxPlaintextBytes?: number | undefined;
|
|
528
|
+
maxFutureSkewSeconds?: number | undefined;
|
|
529
|
+
maxRequestAgeSeconds?: number | undefined;
|
|
520
530
|
routePlaintextBytes?: Record<`/${string}`, number> | undefined;
|
|
521
531
|
} | undefined;
|
|
522
532
|
}>>;
|
|
@@ -541,6 +551,10 @@ interface ServTransportConfig {
|
|
|
541
551
|
/** 受管服务端密钥环,仅运行时从私有 Secret 注入。 */
|
|
542
552
|
/** 双向业务明文 UTF-8 字节上限;默认 1 MiB,整数范围 1 KiB~64 MiB。 */
|
|
543
553
|
readonly maxPlaintextBytes?: number;
|
|
554
|
+
/** 请求时间允许领先服务器的秒数,默认 30,整数范围 0~300。 */
|
|
555
|
+
readonly maxFutureSkewSeconds?: number;
|
|
556
|
+
/** 请求从签发起的有效秒数,默认 300,整数范围 1~600。 */
|
|
557
|
+
readonly maxRequestAgeSeconds?: number;
|
|
544
558
|
/** 路由明文容量;* 匹配一个完整路径段。 */
|
|
545
559
|
routePlaintextBytes?: Readonly<Record<string, number>>;
|
|
546
560
|
readonly serverKeyRing: TransportServerKeyRing;
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { posix } from 'path';
|
|
|
6
6
|
import { apiContract, AUDIO_WS_PATH, AudioWsClientMessageSchema } from '@h-ai/api-contract';
|
|
7
7
|
import { audit } from '@h-ai/audit';
|
|
8
8
|
import { core, RequestAccessConfigSchema, TrustedProxiesSchema, HaiCommonError, err, ok } from '@h-ai/core';
|
|
9
|
-
import { TRANSPORT_PLAINTEXT_RANGE,
|
|
9
|
+
import { TRANSPORT_PLAINTEXT_RANGE, TRANSPORT_TIME_LIMITS, TRANSPORT_PROTOCOL, createTransportBodyPolicy, authenticateTransportRequest, TransportTimeError, createTransportErrorResponse, TransportBodyTooLargeError, readTransportBody, TRANSPORT_BODY_LIMITS, isValidSM2PublicKey } from '@h-ai/crypto';
|
|
10
10
|
import { OpenAPIHandler } from '@orpc/openapi/fetch';
|
|
11
11
|
import { Hono } from 'hono';
|
|
12
12
|
import { WebSocketServer } from 'ws';
|
|
@@ -593,6 +593,8 @@ var TransportRuntimeConfigInputSchema = z.union([
|
|
|
593
593
|
z.literal(false),
|
|
594
594
|
z.object({
|
|
595
595
|
maxPlaintextBytes: z.union([z.number().int().min(TRANSPORT_PLAINTEXT_RANGE.min).max(TRANSPORT_PLAINTEXT_RANGE.max), z.literal("auto")]).transform((value) => value === "auto" ? void 0 : value).optional().describe("\u666E\u901A\u4E1A\u52A1\u53CC\u5411\u660E\u6587\u4E0A\u9650\uFF1Bauto \u4E3A 1 MiB\uFF1B\u5927\u6750\u6599\u4F7F\u7528 routePlaintextBytes \u5355\u72EC\u58F0\u660E").meta({ default: "auto" }),
|
|
596
|
+
maxFutureSkewSeconds: z.number().int().min(0).max(TRANSPORT_TIME_LIMITS.maxFutureSkewSeconds).optional().describe("\u5141\u8BB8\u5BA2\u6237\u7AEF\u8BF7\u6C42\u65F6\u95F4\u9886\u5148\u670D\u52A1\u5668\u7684\u79D2\u6570").meta({ default: TRANSPORT_PROTOCOL.CLOCK_SKEW_MS / 1e3 }),
|
|
597
|
+
maxRequestAgeSeconds: z.number().int().min(TRANSPORT_TIME_LIMITS.minRequestAgeSeconds).max(TRANSPORT_TIME_LIMITS.maxRequestAgeSeconds).optional().describe("\u8BF7\u6C42\u4ECE\u7B7E\u53D1\u8D77\u7684\u6709\u6548\u79D2\u6570").meta({ default: TRANSPORT_PROTOCOL.REQUEST_WINDOW_MS / 1e3 }),
|
|
596
598
|
routePlaintextBytes: z.record(AbsolutePathSchema, z.number().int().min(TRANSPORT_PLAINTEXT_RANGE.min).max(TRANSPORT_PLAINTEXT_RANGE.max)).optional().describe("\u5B8C\u6574\u8DEF\u5F84\u6BB5\u5230\u53CC\u5411\u660E\u6587\u5B57\u8282\u4E0A\u9650\u7684\u6620\u5C04\uFF1B\u661F\u53F7\u5339\u914D\u5355\u6BB5\uFF0C\u4F9B\u9644\u4EF6\u548C\u5F52\u6863\u72EC\u7ACB\u5BB9\u91CF"),
|
|
597
599
|
keyExchangePath: AbsolutePathSchema.default("/_hai/key-exchange"),
|
|
598
600
|
excludePaths: z.array(AbsolutePathSchema).default([])
|
|
@@ -1292,7 +1294,9 @@ function createApp(options) {
|
|
|
1292
1294
|
const mgr = options.transport.crypto.transport.createServer({
|
|
1293
1295
|
serverKeyRing: options.transport.serverKeyRing,
|
|
1294
1296
|
keyStore: options.transport.keyStore,
|
|
1295
|
-
maxPlaintextBytes: policy.maximum.plaintextBytes
|
|
1297
|
+
maxPlaintextBytes: policy.maximum.plaintextBytes,
|
|
1298
|
+
maxFutureSkewSeconds: options.transport.maxFutureSkewSeconds,
|
|
1299
|
+
maxRequestAgeSeconds: options.transport.maxRequestAgeSeconds
|
|
1296
1300
|
});
|
|
1297
1301
|
if (!mgr.success) {
|
|
1298
1302
|
throw core.error.buildHaiErrorInst(
|