@lyrify/znl 0.4.1 → 0.5.1

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