@lyrify/znl 0.4.1 → 0.5.2

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/src/ZNL.js CHANGED
@@ -13,10 +13,15 @@
13
13
  * auth_failed / slave_connected / slave_disconnected
14
14
  * publish / error
15
15
  *
16
+ * 加密模式(encrypted):
17
+ * - false : 明文模式(不签名/不加密)
18
+ * - true : 签名 + 防重放 + payload 透明加密(AES-256-GCM)
19
+ *
16
20
  * 心跳机制:
17
21
  * - slave 每隔 heartbeatInterval ms 向 master 发送一帧心跳
18
22
  * - master 每隔 heartbeatInterval ms 扫描一次在线列表
19
- * - 超过 heartbeatInterval × 3 ms 未收到心跳,判定 slave 已崩溃并移除
23
+ * - 超过 heartbeatTimeoutMs 未收到心跳,判定 slave 已崩溃并移除
24
+ * (heartbeatTimeoutMs <= 0 时,默认按 heartbeatInterval × 3 计算)
20
25
  */
21
26
 
22
27
  import { randomUUID } from "node:crypto";
@@ -26,9 +31,16 @@ import * as zmq from "zeromq";
26
31
  import {
27
32
  DEFAULT_ENDPOINTS,
28
33
  DEFAULT_HEARTBEAT_INTERVAL,
34
+ DEFAULT_HEARTBEAT_TIMEOUT_MS,
35
+ DEFAULT_MAX_PENDING,
36
+ DEFAULT_ENABLE_PAYLOAD_DIGEST,
29
37
  CONTROL_PREFIX,
30
38
  CONTROL_HEARTBEAT,
39
+ SECURITY_ENVELOPE_VERSION,
40
+ MAX_TIME_SKEW_MS,
41
+ REPLAY_WINDOW_MS,
31
42
  } from "./constants.js";
43
+
32
44
  import {
33
45
  identityToString,
34
46
  identityToBuffer,
@@ -36,11 +48,27 @@ import {
36
48
  payloadFromFrames,
37
49
  buildRegisterFrames,
38
50
  buildUnregisterFrames,
51
+ buildHeartbeatFrames,
39
52
  buildRequestFrames,
40
53
  buildResponseFrames,
41
54
  buildPublishFrames,
42
55
  parseControlFrames,
43
56
  } from "./protocol.js";
57
+
58
+ import {
59
+ ReplayGuard,
60
+ deriveKeys,
61
+ toFrameBuffers,
62
+ digestFrames,
63
+ generateNonce,
64
+ nowMs,
65
+ canonicalSignInput,
66
+ encodeAuthProofToken,
67
+ decodeAuthProofToken,
68
+ encryptFrames,
69
+ decryptFrames,
70
+ } from "./security.js";
71
+
44
72
  import { PendingManager } from "./PendingManager.js";
45
73
  import { SendQueue } from "./SendQueue.js";
46
74
 
@@ -49,14 +77,18 @@ export class ZNL extends EventEmitter {
49
77
 
50
78
  /** @type {"master"|"slave"} */
51
79
  role;
80
+
52
81
  /** @type {string} 节点唯一标识,slave 侧同时作为 ZMQ routingId */
53
82
  id;
83
+
54
84
  /** @type {{ router: string }} */
55
85
  endpoints;
56
- /** @type {string} 认证 Key(空字符串表示不启用认证) */
86
+
87
+ /** @type {string} 共享认证 Key(仅 encrypted=true 使用) */
57
88
  authKey;
58
- /** @type {boolean} master 侧是否强制校验认证 Key */
59
- requireAuth;
89
+
90
+ /** @type {boolean} 是否启用加密安全(签名+防重放+透明加密) */
91
+ encrypted;
60
92
 
61
93
  // ─── 运行状态 ─────────────────────────────────────────────────────────────
62
94
 
@@ -89,12 +121,18 @@ export class ZNL extends EventEmitter {
89
121
  /** @type {Function|null} slave 侧收到 master 请求时的自动回复处理器 */
90
122
  #dealerAutoHandler = null;
91
123
 
124
+ /** @type {number} 心跳超时时间(ms),0 表示使用 interval × 3 */
125
+ #heartbeatTimeoutMs = DEFAULT_HEARTBEAT_TIMEOUT_MS;
126
+
127
+ /** @type {boolean} 是否启用 payload 摘要校验(安全模式用) */
128
+ #enablePayloadDigest = DEFAULT_ENABLE_PAYLOAD_DIGEST;
129
+
92
130
  // ─── PUB/SUB 状态 ────────────────────────────────────────────────────────
93
131
 
94
132
  /**
95
133
  * master 侧:已注册的在线 slave 表
96
134
  * key = slaveId(字符串)
97
- * value = { identity: Buffer(用于 ROUTER 发送), lastSeen: number(最后心跳时间戳)}
135
+ * value = { identity: Buffer, lastSeen: number }
98
136
  * @type {Map<string, { identity: Buffer, lastSeen: number }>}
99
137
  */
100
138
  #slaves = new Map();
@@ -116,27 +154,57 @@ export class ZNL extends EventEmitter {
116
154
  /** master 侧扫描死节点的定时器句柄 */
117
155
  #heartbeatCheckTimer = null;
118
156
 
157
+ // ─── 安全运行时状态 ───────────────────────────────────────────────────────
158
+
159
+ /** @type {boolean} 当前是否启用签名安全(auth / encrypted) */
160
+ #secureEnabled = false;
161
+
162
+ /** @type {Buffer|null} HMAC 签名密钥(由 authKey HKDF 派生) */
163
+ #signKey = null;
164
+
165
+ /** @type {Buffer|null} 对称加密密钥(由 authKey HKDF 派生) */
166
+ #encryptKey = null;
167
+
168
+ /** @type {ReplayGuard} 防重放缓存 */
169
+ #replayGuard;
170
+
171
+ /** @type {number} 时间戳最大允许漂移(毫秒) */
172
+ #maxTimeSkewMs = MAX_TIME_SKEW_MS;
173
+
174
+ /** @type {string|null} slave 侧学习到的 master 节点 ID(来自签名证明) */
175
+ #masterNodeId = null;
176
+
119
177
  // ═══════════════════════════════════════════════════════════════════════════
120
178
  // 构造函数
121
179
  // ═══════════════════════════════════════════════════════════════════════════
122
180
 
123
181
  /**
124
182
  * @param {{
125
- * role : "master"|"slave",
126
- * id : string,
127
- * endpoints? : { router?: string },
128
- * maxPending? : number,
129
- * authKey? : string,
130
- * heartbeatInterval? : number,
183
+ * role : "master"|"slave",
184
+ * id : string,
185
+ * endpoints? : { router?: string },
186
+ * maxPending? : number,
187
+ * authKey? : string,
188
+ * heartbeatInterval? : number,
189
+ * heartbeatTimeoutMs? : number,
190
+ * encrypted? : boolean,
191
+ * enablePayloadDigest? : boolean,
192
+ * maxTimeSkewMs? : number,
193
+ * replayWindowMs? : number,
131
194
  * }} options
132
195
  */
133
196
  constructor({
134
197
  role,
135
198
  id,
136
199
  endpoints = {},
137
- maxPending = 0,
200
+ maxPending = DEFAULT_MAX_PENDING,
138
201
  authKey = "",
139
202
  heartbeatInterval = DEFAULT_HEARTBEAT_INTERVAL,
203
+ heartbeatTimeoutMs = DEFAULT_HEARTBEAT_TIMEOUT_MS,
204
+ encrypted = false,
205
+ enablePayloadDigest = DEFAULT_ENABLE_PAYLOAD_DIGEST,
206
+ maxTimeSkewMs = MAX_TIME_SKEW_MS,
207
+ replayWindowMs = REPLAY_WINDOW_MS,
140
208
  } = {}) {
141
209
  super();
142
210
 
@@ -151,12 +219,40 @@ export class ZNL extends EventEmitter {
151
219
  this.id = String(id);
152
220
  this.endpoints = { ...DEFAULT_ENDPOINTS, ...endpoints };
153
221
  this.authKey = authKey == null ? "" : String(authKey);
154
- this.requireAuth = this.role === "master" && this.authKey.length > 0;
222
+
223
+ this.encrypted = Boolean(encrypted);
224
+ this.#secureEnabled = this.encrypted;
225
+
226
+ if (this.#secureEnabled) {
227
+ if (!this.authKey) {
228
+ throw new Error("启用 encrypted=true 时,必须提供非空 authKey。");
229
+ }
230
+ const { signKey, encryptKey } = deriveKeys(this.authKey);
231
+ this.#signKey = signKey;
232
+ this.#encryptKey = encryptKey;
233
+ }
155
234
 
156
235
  this.#pending = new PendingManager(maxPending);
157
236
  this.#sendQueue = new SendQueue();
158
237
  this.#heartbeatInterval =
159
238
  this.#normalizeHeartbeatInterval(heartbeatInterval);
239
+
240
+ // 是否启用 payload 摘要校验(安全模式用)
241
+ this.#enablePayloadDigest = Boolean(enablePayloadDigest);
242
+
243
+ // 心跳超时:<=0 则由 heartbeatInterval 推导
244
+ this.#heartbeatTimeoutMs = this.#normalizePositiveInt(
245
+ heartbeatTimeoutMs,
246
+ DEFAULT_HEARTBEAT_TIMEOUT_MS,
247
+ );
248
+
249
+ this.#maxTimeSkewMs = this.#normalizePositiveInt(
250
+ maxTimeSkewMs,
251
+ MAX_TIME_SKEW_MS,
252
+ );
253
+ this.#replayGuard = new ReplayGuard({
254
+ windowMs: this.#normalizePositiveInt(replayWindowMs, REPLAY_WINDOW_MS),
255
+ });
160
256
  }
161
257
 
162
258
  // ═══════════════════════════════════════════════════════════════════════════
@@ -259,16 +355,23 @@ export class ZNL extends EventEmitter {
259
355
  const socket = this.#requireSocket("router", "ROUTER");
260
356
  if (this.#slaves.size === 0) return;
261
357
 
262
- const frames = buildPublishFrames(String(topic), normalizeFrames(payload));
358
+ const topicText = String(topic);
359
+ const payloadFrames = this.#sealPayloadFrames(
360
+ "publish",
361
+ topicText,
362
+ payload,
363
+ );
364
+ const authProof = this.#secureEnabled
365
+ ? this.#createAuthProof("publish", topicText, payloadFrames)
366
+ : "";
367
+
368
+ const frames = buildPublishFrames(topicText, payloadFrames, authProof);
263
369
 
264
- // 遍历所有在线 slave,逐一入队发送
265
- // 使用 #sendQueue 保证 socket 写入安全,不与 RPC 帧交叉
266
370
  for (const [slaveId, entry] of this.#slaves) {
267
371
  const idFrame = identityToBuffer(entry.identity);
268
372
  this.#sendQueue
269
373
  .enqueue("router", () => socket.send([idFrame, ...frames]))
270
374
  .catch(() => {
271
- // 发送失败说明 slave 已断线,静默移除并通知外部
272
375
  if (this.#slaves.delete(slaveId)) {
273
376
  this.emit("slave_disconnected", slaveId);
274
377
  }
@@ -333,7 +436,6 @@ export class ZNL extends EventEmitter {
333
436
  ? this.#startMasterSockets()
334
437
  : this.#startSlaveSockets());
335
438
  } catch (error) {
336
- // 启动失败则回滚所有状态
337
439
  this.running = false;
338
440
  await this.#teardown();
339
441
  throw error;
@@ -348,16 +450,14 @@ export class ZNL extends EventEmitter {
348
450
  */
349
451
  async #doStop() {
350
452
  if (this.role === "slave" && this.#sockets.dealer) {
351
- // 先停心跳,避免关闭期间继续发送
352
453
  clearInterval(this.#heartbeatTimer);
353
454
  this.#heartbeatTimer = null;
354
455
 
355
- // 优雅下线:在关闭 socket 前先发送注销帧
356
456
  await this.#sendQueue
357
457
  .enqueue("dealer", () =>
358
458
  this.#sockets.dealer.send(buildUnregisterFrames()),
359
459
  )
360
- .catch(() => {}); // master 可能已关闭,忽略发送失败
460
+ .catch(() => {});
361
461
  }
362
462
 
363
463
  this.running = false;
@@ -370,7 +470,6 @@ export class ZNL extends EventEmitter {
370
470
  * 顺序:停止定时器 → 关闭 socket → 等待读循环退出 → 清空所有注册表
371
471
  */
372
472
  async #teardown() {
373
- // 停止心跳相关定时器
374
473
  clearInterval(this.#heartbeatTimer);
375
474
  clearInterval(this.#heartbeatCheckTimer);
376
475
  this.#heartbeatTimer = null;
@@ -386,7 +485,9 @@ export class ZNL extends EventEmitter {
386
485
  this.#readLoops = [];
387
486
  this.#sockets = {};
388
487
  this.#sendQueue.clear();
389
- this.#slaves.clear(); // 清空 slave 注册表,避免 restart 时残留旧条目
488
+ this.#slaves.clear();
489
+ this.#masterNodeId = null;
490
+ this.#replayGuard.clear();
390
491
  }
391
492
 
392
493
  // ═══════════════════════════════════════════════════════════════════════════
@@ -410,19 +511,23 @@ export class ZNL extends EventEmitter {
410
511
  * 连接后立即发送注册帧,再启动心跳定时器
411
512
  */
412
513
  async #startSlaveSockets() {
413
- // routingId 即节点 id,Master 侧通过此字段识别发送方身份
414
514
  const dealer = new zmq.Dealer({ routingId: this.id });
415
515
  dealer.connect(this.endpoints.router);
416
516
  this.#sockets.dealer = dealer;
417
517
 
418
518
  this.#consume(dealer, (frames) => this.#handleDealerFrames(frames));
419
519
 
420
- // 自动发送注册帧(携带 authKey,供 master 进行认证)
520
+ // 注册帧:
521
+ // - encrypted=true : 发送签名证明令牌(放在 authKey 字段位)
522
+ // - encrypted=false : 不携带认证信息
523
+ const registerToken = this.#secureEnabled
524
+ ? this.#createAuthProof("register", "", [])
525
+ : "";
526
+
421
527
  await this.#sendQueue.enqueue("dealer", () =>
422
- dealer.send(buildRegisterFrames(this.authKey)),
528
+ dealer.send(buildRegisterFrames(registerToken)),
423
529
  );
424
530
 
425
- // 注册成功后启动心跳
426
531
  this.#startHeartbeat();
427
532
  }
428
533
 
@@ -433,7 +538,6 @@ export class ZNL extends EventEmitter {
433
538
  /**
434
539
  * 处理 Master 侧 ROUTER 收到的帧
435
540
  * ZMQ ROUTER 帧结构:[identity, ...控制帧]
436
- * identity 由 ZMQ 自动注入,表示发送方的 routingId
437
541
  */
438
542
  async #handleRouterFrames(rawFrames) {
439
543
  const [identity, ...bodyFrames] = rawFrames;
@@ -448,26 +552,49 @@ export class ZNL extends EventEmitter {
448
552
  ...parsed,
449
553
  });
450
554
 
451
- // ── 心跳:更新 slave 的最后活跃时间 ───────────────────────────────────
555
+ // ── 心跳 ────────────────────────────────────────────────────────────────
452
556
  if (parsed.kind === "heartbeat") {
453
- const entry = this.#slaves.get(identityText);
454
- if (entry) entry.lastSeen = Date.now();
557
+ if (this.#secureEnabled) {
558
+ const v = this.#verifyIncomingProof({
559
+ kind: "heartbeat",
560
+ proofToken: parsed.authProof,
561
+ requestId: "",
562
+ payloadFrames: [],
563
+ expectedNodeId: identityText,
564
+ });
565
+ if (!v.ok) {
566
+ this.#emitAuthFailed(event, v.error);
567
+ return;
568
+ }
569
+ }
570
+
571
+ // 心跳视为在线确认:必要时自动补注册
572
+ this.#ensureSlaveOnline(identityText, identity, { touch: true });
455
573
  return;
456
574
  }
457
575
 
458
- // ── 注册:slave 上线 ────────────────────────────────────────────────────
576
+ // ── 注册 ────────────────────────────────────────────────────────────────
459
577
  if (parsed.kind === "register") {
460
- // 若启用认证,注册时同样校验 authKey
461
- if (this.requireAuth && parsed.authKey !== this.authKey) {
462
- this.emit("auth_failed", { ...event, expectedAuthKey: this.authKey });
463
- return;
578
+ if (this.#secureEnabled) {
579
+ const v = this.#verifyIncomingProof({
580
+ kind: "register",
581
+ proofToken: parsed.authKey, // register 复用 authKey 字段承载 proof
582
+ requestId: "",
583
+ payloadFrames: [],
584
+ expectedNodeId: identityText,
585
+ });
586
+ if (!v.ok) {
587
+ this.#emitAuthFailed(event, v.error);
588
+ return;
589
+ }
464
590
  }
591
+
465
592
  this.#slaves.set(identityText, { identity, lastSeen: Date.now() });
466
593
  this.emit("slave_connected", identityText);
467
594
  return;
468
595
  }
469
596
 
470
- // ── 注销:slave 主动下线 ────────────────────────────────────────────────
597
+ // ── 注销 ────────────────────────────────────────────────────────────────
471
598
  if (parsed.kind === "unregister") {
472
599
  if (this.#slaves.delete(identityText)) {
473
600
  this.emit("slave_disconnected", identityText);
@@ -475,20 +602,54 @@ export class ZNL extends EventEmitter {
475
602
  return;
476
603
  }
477
604
 
478
- // ── RPC 请求:slave 发来的请求 ──────────────────────────────────────────
605
+ // ── RPC 请求:slave -> master ──────────────────────────────────────────
479
606
  if (parsed.kind === "request") {
480
- // 认证校验:Key 不匹配则丢弃并触发 auth_failed 事件
481
- if (this.requireAuth && parsed.authKey !== this.authKey) {
482
- this.emit("auth_failed", { ...event, expectedAuthKey: this.authKey });
483
- return;
607
+ let finalFrames = parsed.payloadFrames;
608
+
609
+ if (this.#secureEnabled) {
610
+ const v = this.#verifyIncomingProof({
611
+ kind: "request",
612
+ proofToken: parsed.authKey, // request 复用 authKey 字段承载 proof
613
+ requestId: parsed.requestId,
614
+ payloadFrames: parsed.payloadFrames,
615
+ expectedNodeId: identityText,
616
+ });
617
+ if (!v.ok) {
618
+ this.#emitAuthFailed(event, v.error);
619
+ return;
620
+ }
621
+
622
+ // 认证通过后允许补注册(避免 master 重启后丢失在线表)
623
+ this.#ensureSlaveOnline(identityText, identity, { touch: true });
624
+
625
+ if (this.encrypted) {
626
+ try {
627
+ finalFrames = this.#openPayloadFrames(
628
+ "request",
629
+ parsed.requestId,
630
+ parsed.payloadFrames,
631
+ identityText,
632
+ );
633
+ } catch (error) {
634
+ this.#emitAuthFailed(
635
+ event,
636
+ `请求 payload 解密失败:${error?.message ?? error}`,
637
+ );
638
+ return;
639
+ }
640
+ }
641
+ } else {
642
+ // 明文模式同样补注册
643
+ this.#ensureSlaveOnline(identityText, identity, { touch: true });
484
644
  }
485
645
 
486
- this.emit("request", event);
646
+ const finalPayload = payloadFromFrames(finalFrames);
647
+ const requestEvent = { ...event, payload: finalPayload };
648
+ this.emit("request", requestEvent);
487
649
 
488
- // 有自动回复处理器时,执行并回复
489
650
  if (this.#routerAutoHandler) {
490
651
  try {
491
- const replyPayload = await this.#routerAutoHandler(event);
652
+ const replyPayload = await this.#routerAutoHandler(requestEvent);
492
653
  await this.#replyTo(identity, parsed.requestId, replyPayload);
493
654
  } catch (error) {
494
655
  this.emit("error", error);
@@ -497,11 +658,52 @@ export class ZNL extends EventEmitter {
497
658
  return;
498
659
  }
499
660
 
500
- // ── RPC 响应:slave 回复了 master 之前主动发出的请求 ───────────────────
661
+ // ── RPC 响应:slave -> master ──────────────────────────────────────────
501
662
  if (parsed.kind === "response") {
663
+ let finalFrames = parsed.payloadFrames;
664
+
665
+ if (this.#secureEnabled) {
666
+ const v = this.#verifyIncomingProof({
667
+ kind: "response",
668
+ proofToken: parsed.authProof,
669
+ requestId: parsed.requestId,
670
+ payloadFrames: parsed.payloadFrames,
671
+ expectedNodeId: identityText,
672
+ });
673
+ if (!v.ok) {
674
+ this.#emitAuthFailed(event, v.error);
675
+ return;
676
+ }
677
+
678
+ // 认证通过后允许补注册(避免 master 重启后丢失在线表)
679
+ this.#ensureSlaveOnline(identityText, identity, { touch: true });
680
+
681
+ if (this.encrypted) {
682
+ try {
683
+ finalFrames = this.#openPayloadFrames(
684
+ "response",
685
+ parsed.requestId,
686
+ parsed.payloadFrames,
687
+ identityText,
688
+ );
689
+ } catch (error) {
690
+ this.#emitAuthFailed(
691
+ event,
692
+ `响应 payload 解密失败:${error?.message ?? error}`,
693
+ );
694
+ return;
695
+ }
696
+ }
697
+ } else {
698
+ // 明文模式同样补注册
699
+ this.#ensureSlaveOnline(identityText, identity, { touch: true });
700
+ }
701
+
702
+ const finalPayload = payloadFromFrames(finalFrames);
703
+ const responseEvent = { ...event, payload: finalPayload };
502
704
  const key = this.#pending.key(parsed.requestId, identityText);
503
- this.#pending.resolve(key, event);
504
- this.emit("response", event);
705
+ this.#pending.resolve(key, responseEvent);
706
+ this.emit("response", responseEvent);
505
707
  }
506
708
  }
507
709
 
@@ -515,14 +717,49 @@ export class ZNL extends EventEmitter {
515
717
 
516
718
  const event = this.#buildAndEmit("dealer", frames, { payload, ...parsed });
517
719
 
518
- // ── PUB 广播:master 推送的消息 ────────────────────────────────────────
720
+ // ── PUB 广播:master -> slave ──────────────────────────────────────────
519
721
  if (parsed.kind === "publish") {
520
- const pubEvent = { topic: parsed.topic, payload };
722
+ let finalFrames = parsed.payloadFrames;
723
+
724
+ if (this.#secureEnabled) {
725
+ const v = this.#verifyIncomingProof({
726
+ kind: "publish",
727
+ proofToken: parsed.authProof,
728
+ requestId: parsed.topic ?? "",
729
+ payloadFrames: parsed.payloadFrames,
730
+ expectedNodeId: this.#masterNodeId,
731
+ });
732
+ if (!v.ok) {
733
+ this.#emitAuthFailed(event, v.error);
734
+ return;
735
+ }
736
+
737
+ // 第一次学习 master 节点 ID,后续锁定
738
+ if (!this.#masterNodeId) this.#masterNodeId = v.envelope.nodeId;
739
+
740
+ if (this.encrypted) {
741
+ try {
742
+ finalFrames = this.#openPayloadFrames(
743
+ "publish",
744
+ parsed.topic ?? "",
745
+ parsed.payloadFrames,
746
+ this.#masterNodeId,
747
+ );
748
+ } catch (error) {
749
+ this.#emitAuthFailed(
750
+ event,
751
+ `广播 payload 解密失败:${error?.message ?? error}`,
752
+ );
753
+ return;
754
+ }
755
+ }
756
+ }
757
+
758
+ const finalPayload = payloadFromFrames(finalFrames);
759
+ const pubEvent = { topic: parsed.topic, payload: finalPayload };
521
760
 
522
- // 触发统一广播事件(所有 topic 都会触发,方便兜底监听)
523
761
  this.emit("publish", pubEvent);
524
762
 
525
- // 触发精确 topic 订阅处理器
526
763
  const handler = this.#subscriptions.get(parsed.topic);
527
764
  if (handler) {
528
765
  try {
@@ -534,13 +771,50 @@ export class ZNL extends EventEmitter {
534
771
  return;
535
772
  }
536
773
 
537
- // ── RPC 请求:master 主动发来的请求 ────────────────────────────────────
774
+ // ── RPC 请求:master -> slave ──────────────────────────────────────────
538
775
  if (parsed.kind === "request") {
539
- this.emit("request", event);
776
+ let finalFrames = parsed.payloadFrames;
777
+
778
+ if (this.#secureEnabled) {
779
+ const v = this.#verifyIncomingProof({
780
+ kind: "request",
781
+ proofToken: parsed.authKey, // request 复用 authKey 字段承载 proof
782
+ requestId: parsed.requestId,
783
+ payloadFrames: parsed.payloadFrames,
784
+ expectedNodeId: this.#masterNodeId,
785
+ });
786
+ if (!v.ok) {
787
+ this.#emitAuthFailed(event, v.error);
788
+ return;
789
+ }
790
+
791
+ if (!this.#masterNodeId) this.#masterNodeId = v.envelope.nodeId;
792
+
793
+ if (this.encrypted) {
794
+ try {
795
+ finalFrames = this.#openPayloadFrames(
796
+ "request",
797
+ parsed.requestId,
798
+ parsed.payloadFrames,
799
+ this.#masterNodeId,
800
+ );
801
+ } catch (error) {
802
+ this.#emitAuthFailed(
803
+ event,
804
+ `请求 payload 解密失败:${error?.message ?? error}`,
805
+ );
806
+ return;
807
+ }
808
+ }
809
+ }
810
+
811
+ const finalPayload = payloadFromFrames(finalFrames);
812
+ const requestEvent = { ...event, payload: finalPayload };
813
+ this.emit("request", requestEvent);
540
814
 
541
815
  if (this.#dealerAutoHandler) {
542
816
  try {
543
- const replyPayload = await this.#dealerAutoHandler(event);
817
+ const replyPayload = await this.#dealerAutoHandler(requestEvent);
544
818
  await this.#reply(parsed.requestId, replyPayload);
545
819
  } catch (error) {
546
820
  this.emit("error", error);
@@ -549,11 +823,48 @@ export class ZNL extends EventEmitter {
549
823
  return;
550
824
  }
551
825
 
552
- // ── RPC 响应:master 回复了 slave 之前发出的请求 ───────────────────────
826
+ // ── RPC 响应:master -> slave ──────────────────────────────────────────
553
827
  if (parsed.kind === "response") {
828
+ let finalFrames = parsed.payloadFrames;
829
+
830
+ if (this.#secureEnabled) {
831
+ const v = this.#verifyIncomingProof({
832
+ kind: "response",
833
+ proofToken: parsed.authProof,
834
+ requestId: parsed.requestId,
835
+ payloadFrames: parsed.payloadFrames,
836
+ expectedNodeId: this.#masterNodeId,
837
+ });
838
+ if (!v.ok) {
839
+ this.#emitAuthFailed(event, v.error);
840
+ return;
841
+ }
842
+
843
+ if (!this.#masterNodeId) this.#masterNodeId = v.envelope.nodeId;
844
+
845
+ if (this.encrypted) {
846
+ try {
847
+ finalFrames = this.#openPayloadFrames(
848
+ "response",
849
+ parsed.requestId,
850
+ parsed.payloadFrames,
851
+ this.#masterNodeId,
852
+ );
853
+ } catch (error) {
854
+ this.#emitAuthFailed(
855
+ event,
856
+ `响应 payload 解密失败:${error?.message ?? error}`,
857
+ );
858
+ return;
859
+ }
860
+ }
861
+ }
862
+
863
+ const finalPayload = payloadFromFrames(finalFrames);
864
+ const responseEvent = { ...event, payload: finalPayload };
554
865
  const key = this.#pending.key(parsed.requestId);
555
- this.#pending.resolve(key, event);
556
- this.emit("response", event);
866
+ this.#pending.resolve(key, responseEvent);
867
+ this.emit("response", responseEvent);
557
868
  }
558
869
  }
559
870
 
@@ -563,7 +874,6 @@ export class ZNL extends EventEmitter {
563
874
 
564
875
  /**
565
876
  * Slave → Master:通过 DEALER socket 发送 RPC 请求,返回响应 Promise
566
- * 流程:创建 pending → 入队等待发送 → 发送后启动超时计时 → 等待响应 resolve
567
877
  */
568
878
  async #request(payload, { timeoutMs } = {}) {
569
879
  const socket = this.#requireSocket("dealer", "DEALER");
@@ -576,13 +886,18 @@ export class ZNL extends EventEmitter {
576
886
  timeoutMs,
577
887
  requestId,
578
888
  );
579
- const frames = buildRequestFrames(
889
+
890
+ const payloadFrames = this.#sealPayloadFrames(
891
+ "request",
580
892
  requestId,
581
- normalizeFrames(payload),
582
- this.authKey,
893
+ payload,
583
894
  );
895
+ const proofOrAuthKey = this.#secureEnabled
896
+ ? this.#createAuthProof("request", requestId, payloadFrames)
897
+ : "";
898
+
899
+ const frames = buildRequestFrames(requestId, payloadFrames, proofOrAuthKey);
584
900
 
585
- // 入队:等前一个发送完成后再发;发送完毕后立即启动超时计时
586
901
  this.#sendQueue
587
902
  .enqueue("dealer", async () => {
588
903
  startTimer();
@@ -602,6 +917,7 @@ export class ZNL extends EventEmitter {
602
917
 
603
918
  const identityText = identityToString(identity);
604
919
  const idFrame = identityToBuffer(identity);
920
+
605
921
  const requestId = randomUUID();
606
922
  const key = this.#pending.key(requestId, identityText);
607
923
  const { promise, startTimer } = this.#pending.create(
@@ -610,16 +926,21 @@ export class ZNL extends EventEmitter {
610
926
  requestId,
611
927
  identityText,
612
928
  );
613
- const frames = buildRequestFrames(
929
+
930
+ const payloadFrames = this.#sealPayloadFrames(
931
+ "request",
614
932
  requestId,
615
- normalizeFrames(payload),
616
- this.authKey,
933
+ payload,
617
934
  );
935
+ const proofOrAuthKey = this.#secureEnabled
936
+ ? this.#createAuthProof("request", requestId, payloadFrames)
937
+ : "";
938
+
939
+ const frames = buildRequestFrames(requestId, payloadFrames, proofOrAuthKey);
618
940
 
619
941
  this.#sendQueue
620
942
  .enqueue("router", async () => {
621
943
  startTimer();
622
- // ROUTER socket 发送时必须在最前面附上 identity 帧
623
944
  await socket.send([idFrame, ...frames]);
624
945
  })
625
946
  .catch((error) => this.#pending.reject(key, error));
@@ -638,7 +959,17 @@ export class ZNL extends EventEmitter {
638
959
  */
639
960
  async #reply(requestId, payload) {
640
961
  const socket = this.#requireSocket("dealer", "DEALER");
641
- const frames = buildResponseFrames(requestId, normalizeFrames(payload));
962
+
963
+ const payloadFrames = this.#sealPayloadFrames(
964
+ "response",
965
+ requestId,
966
+ payload,
967
+ );
968
+ const authProof = this.#secureEnabled
969
+ ? this.#createAuthProof("response", requestId, payloadFrames)
970
+ : "";
971
+
972
+ const frames = buildResponseFrames(requestId, payloadFrames, authProof);
642
973
  await this.#sendQueue.enqueue("dealer", () => socket.send(frames));
643
974
  }
644
975
 
@@ -651,7 +982,17 @@ export class ZNL extends EventEmitter {
651
982
  async #replyTo(identity, requestId, payload) {
652
983
  const socket = this.#requireSocket("router", "ROUTER");
653
984
  const idFrame = identityToBuffer(identity);
654
- const frames = buildResponseFrames(requestId, normalizeFrames(payload));
985
+
986
+ const payloadFrames = this.#sealPayloadFrames(
987
+ "response",
988
+ requestId,
989
+ payload,
990
+ );
991
+ const authProof = this.#secureEnabled
992
+ ? this.#createAuthProof("response", requestId, payloadFrames)
993
+ : "";
994
+
995
+ const frames = buildResponseFrames(requestId, payloadFrames, authProof);
655
996
  await this.#sendQueue.enqueue("router", () =>
656
997
  socket.send([idFrame, ...frames]),
657
998
  );
@@ -663,30 +1004,39 @@ export class ZNL extends EventEmitter {
663
1004
 
664
1005
  /**
665
1006
  * 启动 slave 侧心跳发送定时器
666
- * 每隔 #heartbeatInterval ms 向 master 发送一帧心跳,证明自己还活着
667
1007
  */
668
1008
  #startHeartbeat() {
669
1009
  if (this.#heartbeatInterval <= 0) return;
670
1010
 
671
1011
  this.#heartbeatTimer = setInterval(() => {
672
1012
  if (!this.running || !this.#sockets.dealer) return;
1013
+
1014
+ const proof = this.#secureEnabled
1015
+ ? this.#createAuthProof("heartbeat", "", [])
1016
+ : "";
1017
+
1018
+ // plain 模式仍保持历史帧结构 [CONTROL_PREFIX, CONTROL_HEARTBEAT]
1019
+ const frames = this.#secureEnabled
1020
+ ? buildHeartbeatFrames(proof)
1021
+ : [CONTROL_PREFIX, CONTROL_HEARTBEAT];
1022
+
673
1023
  this.#sendQueue
674
- .enqueue("dealer", () =>
675
- this.#sockets.dealer.send([CONTROL_PREFIX, CONTROL_HEARTBEAT]),
676
- )
677
- .catch(() => {}); // 发送失败静默处理,不影响业务
1024
+ .enqueue("dealer", () => this.#sockets.dealer.send(frames))
1025
+ .catch(() => {});
678
1026
  }, this.#heartbeatInterval);
679
1027
  }
680
1028
 
681
1029
  /**
682
1030
  * 启动 master 侧死节点扫描定时器
683
- * 每隔 #heartbeatInterval ms 扫描一次,将超过 3 个周期未活跃的 slave 视为崩溃并移除
684
1031
  */
685
1032
  #startHeartbeatCheck() {
686
1033
  if (this.#heartbeatInterval <= 0) return;
687
1034
 
688
- // 超时阈值 = 3 个心跳周期,给网络抖动留出充分余量
689
- const timeout = this.#heartbeatInterval * 3;
1035
+ // 心跳超时:优先使用配置值,<=0 则回退到 interval × 3
1036
+ const timeout =
1037
+ this.#heartbeatTimeoutMs > 0
1038
+ ? this.#heartbeatTimeoutMs
1039
+ : this.#heartbeatInterval * 3;
690
1040
 
691
1041
  this.#heartbeatCheckTimer = setInterval(() => {
692
1042
  const now = Date.now();
@@ -699,14 +1049,228 @@ export class ZNL extends EventEmitter {
699
1049
  }, this.#heartbeatInterval);
700
1050
  }
701
1051
 
1052
+ // ═══════════════════════════════════════════════════════════════════════════
1053
+ // 安全辅助(签名/防重放/加密)
1054
+ // ═══════════════════════════════════════════════════════════════════════════
1055
+
1056
+ /**
1057
+ * 生成签名证明令牌
1058
+ *
1059
+ * @param {"register"|"heartbeat"|"request"|"response"|"publish"} kind
1060
+ * @param {string} requestId
1061
+ * @param {Buffer[]} payloadFrames
1062
+ * @returns {string}
1063
+ */
1064
+ #createAuthProof(kind, requestId, payloadFrames) {
1065
+ if (!this.#secureEnabled || !this.#signKey) return "";
1066
+
1067
+ const envelope = {
1068
+ kind: String(kind),
1069
+ nodeId: this.id,
1070
+ requestId: String(requestId ?? ""),
1071
+ timestamp: nowMs(),
1072
+ nonce: generateNonce(),
1073
+ // 可选 payload 摘要:关闭时写空串以提升性能
1074
+ payloadDigest: this.#enablePayloadDigest
1075
+ ? digestFrames(payloadFrames)
1076
+ : "",
1077
+ };
1078
+
1079
+ // 保留 canonical 文本,便于后续排障(签名实际在 token 内完成)
1080
+ canonicalSignInput(envelope);
1081
+
1082
+ return encodeAuthProofToken(this.#signKey, envelope);
1083
+ }
1084
+
1085
+ /**
1086
+ * 校验入站签名证明 + 防重放 + 摘要一致性
1087
+ *
1088
+ * @param {{
1089
+ * kind: "register"|"heartbeat"|"request"|"response"|"publish",
1090
+ * proofToken: string|null,
1091
+ * requestId: string|null,
1092
+ * payloadFrames: Buffer[],
1093
+ * expectedNodeId?: string|null,
1094
+ * }} input
1095
+ * @returns {{ ok: true, envelope: any } | { ok: false, error: string }}
1096
+ */
1097
+ #verifyIncomingProof({
1098
+ kind,
1099
+ proofToken,
1100
+ requestId,
1101
+ payloadFrames,
1102
+ expectedNodeId = null,
1103
+ }) {
1104
+ if (!this.#secureEnabled) return { ok: true, envelope: null };
1105
+ if (!this.#signKey) {
1106
+ return { ok: false, error: "签名密钥未初始化。" };
1107
+ }
1108
+
1109
+ if (!proofToken) {
1110
+ return { ok: false, error: "缺少认证证明(proofToken)。" };
1111
+ }
1112
+
1113
+ const decoded = decodeAuthProofToken(this.#signKey, proofToken, {
1114
+ maxSkewMs: this.#maxTimeSkewMs,
1115
+ now: Date.now(),
1116
+ });
1117
+ if (!decoded.ok) {
1118
+ return { ok: false, error: decoded.error || "认证证明解析失败。" };
1119
+ }
1120
+
1121
+ const envelope = decoded.envelope;
1122
+
1123
+ if (envelope.kind !== String(kind)) {
1124
+ return {
1125
+ ok: false,
1126
+ error: `证明 kind 不匹配:expect=${kind}, got=${envelope.kind}`,
1127
+ };
1128
+ }
1129
+
1130
+ if (String(envelope.requestId ?? "") !== String(requestId ?? "")) {
1131
+ return {
1132
+ ok: false,
1133
+ error: `证明 requestId 不匹配:expect=${requestId ?? ""}, got=${envelope.requestId ?? ""}`,
1134
+ };
1135
+ }
1136
+
1137
+ if (expectedNodeId && String(envelope.nodeId) !== String(expectedNodeId)) {
1138
+ return {
1139
+ ok: false,
1140
+ error: `证明 nodeId 不匹配:expect=${expectedNodeId}, got=${envelope.nodeId}`,
1141
+ };
1142
+ }
1143
+
1144
+ if (this.#enablePayloadDigest) {
1145
+ const currentDigest = digestFrames(payloadFrames);
1146
+ if (String(envelope.payloadDigest) !== currentDigest) {
1147
+ return { ok: false, error: "payload 摘要不一致,疑似篡改。" };
1148
+ }
1149
+ }
1150
+
1151
+ // 重放 key 加入 requestId,降低 nonce 碰撞误杀风险
1152
+ const replayKey = `${envelope.kind}|${envelope.nodeId}|${envelope.requestId}|${envelope.nonce}`;
1153
+ if (this.#replayGuard.seenOrAdd(replayKey)) {
1154
+ return { ok: false, error: "检测到重放请求(nonce 重复)。" };
1155
+ }
1156
+
1157
+ return { ok: true, envelope };
1158
+ }
1159
+
1160
+ /**
1161
+ * 出站 payload 处理:
1162
+ * - plain/auth : 维持原有 normalizeFrames
1163
+ * - encrypted : 透明加密为安全信封
1164
+ *
1165
+ * @param {"request"|"response"|"publish"} kind
1166
+ * @param {string} requestId
1167
+ * @param {*} payload
1168
+ * @returns {Buffer[]}
1169
+ */
1170
+ #sealPayloadFrames(kind, requestId, payload) {
1171
+ if (!this.encrypted) {
1172
+ // 非加密模式保持历史行为:沿用协议层的原始帧类型(string/Buffer)
1173
+ return normalizeFrames(payload);
1174
+ }
1175
+
1176
+ if (!this.#encryptKey) {
1177
+ throw new Error("加密密钥未初始化。");
1178
+ }
1179
+
1180
+ const rawFrames = toFrameBuffers(payload);
1181
+ const aad = Buffer.from(
1182
+ `znl-aad-v1|${String(kind)}|${String(this.id)}|${String(requestId ?? "")}`,
1183
+ "utf8",
1184
+ );
1185
+
1186
+ const { iv, ciphertext, tag } = encryptFrames(
1187
+ this.#encryptKey,
1188
+ rawFrames,
1189
+ aad,
1190
+ );
1191
+
1192
+ // 用统一信封包装:version + iv + tag + ciphertext
1193
+ return [Buffer.from(SECURITY_ENVELOPE_VERSION), iv, tag, ciphertext];
1194
+ }
1195
+
1196
+ /**
1197
+ * 入站 payload 解封:
1198
+ * - encrypted=true 时要求收到加密信封
1199
+ * - 解密后恢复为 Buffer[],再由 payloadFromFrames 还原
1200
+ *
1201
+ * @param {"request"|"response"|"publish"} kind
1202
+ * @param {string} requestId
1203
+ * @param {Buffer[]} payloadFrames
1204
+ * @param {string} senderNodeId
1205
+ * @returns {Buffer[]}
1206
+ */
1207
+ #openPayloadFrames(kind, requestId, payloadFrames, senderNodeId) {
1208
+ if (!this.encrypted) return payloadFrames;
1209
+ if (!this.#encryptKey) throw new Error("加密密钥未初始化。");
1210
+
1211
+ if (!Array.isArray(payloadFrames) || payloadFrames.length !== 4) {
1212
+ throw new Error("加密信封格式非法:期望 4 帧。");
1213
+ }
1214
+
1215
+ const [version, iv, tag, ciphertext] = payloadFrames;
1216
+ if (String(version?.toString?.() ?? "") !== SECURITY_ENVELOPE_VERSION) {
1217
+ throw new Error("加密信封版本不匹配。");
1218
+ }
1219
+
1220
+ const aad = Buffer.from(
1221
+ `znl-aad-v1|${String(kind)}|${String(senderNodeId)}|${String(requestId ?? "")}`,
1222
+ "utf8",
1223
+ );
1224
+
1225
+ return decryptFrames(this.#encryptKey, iv, ciphertext, tag, aad);
1226
+ }
1227
+
1228
+ /**
1229
+ * 触发统一 auth_failed 事件
1230
+ * @param {object} baseEvent
1231
+ * @param {string} reason
1232
+ */
1233
+ #emitAuthFailed(baseEvent, reason) {
1234
+ this.emit("auth_failed", {
1235
+ ...baseEvent,
1236
+ reason: String(reason ?? "认证失败"),
1237
+ encrypted: this.encrypted,
1238
+ });
1239
+ }
1240
+
1241
+ /**
1242
+ * master 侧确保某个 slave 已登记在线
1243
+ * - 仅在认证通过后调用
1244
+ * - 支持心跳/请求/响应等路径的自动补注册
1245
+ *
1246
+ * @param {string} identityText
1247
+ * @param {Buffer|string|Uint8Array} identity
1248
+ * @param {{ touch?: boolean }} [options]
1249
+ */
1250
+ #ensureSlaveOnline(identityText, identity, { touch = true } = {}) {
1251
+ const id = String(identityText ?? "");
1252
+ if (!id) return;
1253
+
1254
+ const now = Date.now();
1255
+ const entry = this.#slaves.get(id);
1256
+ if (entry) {
1257
+ if (touch) entry.lastSeen = now;
1258
+ return;
1259
+ }
1260
+
1261
+ this.#slaves.set(id, {
1262
+ identity: identityToBuffer(identity),
1263
+ lastSeen: now,
1264
+ });
1265
+ this.emit("slave_connected", id);
1266
+ }
1267
+
702
1268
  // ═══════════════════════════════════════════════════════════════════════════
703
1269
  // 工具方法
704
1270
  // ═══════════════════════════════════════════════════════════════════════════
705
1271
 
706
1272
  /**
707
1273
  * 启动 socket 的异步读取循环(for-await-of)
708
- * 循环在 socket 被关闭后自然退出,Promise 保存到 #readLoops 供 teardown 等待
709
- *
710
1274
  * @param {import("zeromq").Socket} socket
711
1275
  * @param {(frames: Array) => Promise<void>} handler
712
1276
  */
@@ -715,7 +1279,6 @@ export class ZNL extends EventEmitter {
715
1279
  try {
716
1280
  for await (const rawFrames of socket) {
717
1281
  if (!this.running) return;
718
- // zeromq v6 单帧时返回 Buffer,多帧时返回数组,统一转为数组处理
719
1282
  const frames = Array.isArray(rawFrames) ? rawFrames : [rawFrames];
720
1283
  try {
721
1284
  await handler(frames);
@@ -724,7 +1287,6 @@ export class ZNL extends EventEmitter {
724
1287
  }
725
1288
  }
726
1289
  } catch (error) {
727
- // socket 关闭时 for-await 会抛出,仅在运行中时才视为错误
728
1290
  if (this.running) this.emit("error", error);
729
1291
  }
730
1292
  })();
@@ -737,7 +1299,7 @@ export class ZNL extends EventEmitter {
737
1299
  * @param {string} channel
738
1300
  * @param {Array} frames
739
1301
  * @param {object} extra
740
- * @returns {object} 事件对象
1302
+ * @returns {object}
741
1303
  */
742
1304
  #buildAndEmit(channel, frames, extra = {}) {
743
1305
  const event = { channel, frames, ...extra };
@@ -747,7 +1309,7 @@ export class ZNL extends EventEmitter {
747
1309
  }
748
1310
 
749
1311
  /**
750
- * 获取指定 socket,不存在时抛出明确的错误提示
1312
+ * 获取指定 socket,不存在时抛出明确错误
751
1313
  * @param {string} name
752
1314
  * @param {string} displayName
753
1315
  * @returns {import("zeromq").Socket}
@@ -770,4 +1332,16 @@ export class ZNL extends EventEmitter {
770
1332
  if (Number.isFinite(v) && v >= 0) return Math.floor(v);
771
1333
  return DEFAULT_HEARTBEAT_INTERVAL;
772
1334
  }
1335
+
1336
+ /**
1337
+ * 规范化正整数(<=0 时回退默认值)
1338
+ * @param {*} n
1339
+ * @param {number} fallback
1340
+ * @returns {number}
1341
+ */
1342
+ #normalizePositiveInt(n, fallback) {
1343
+ const v = Number(n);
1344
+ if (Number.isFinite(v) && v > 0) return Math.floor(v);
1345
+ return fallback;
1346
+ }
773
1347
  }