@lyrify/znl 0.5.2 → 0.5.6

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.
Files changed (3) hide show
  1. package/README.md +17 -2
  2. package/package.json +1 -1
  3. package/src/ZNL.js +281 -39
package/README.md CHANGED
@@ -15,7 +15,8 @@
15
15
  - `true`:签名 + 防重放 + payload 透明加密(AES-256-GCM)
16
16
  - 可选关闭 payload 摘要校验(`enablePayloadDigest=false`)以提升性能
17
17
  - 建议 master/slave 两端保持一致配置,避免认证不一致
18
- - `authKey` 仅在 `encrypted=true` 时必填
18
+ - `authKey` 或 `authKeyMap` 仅在 `encrypted=true` 时必填
19
+ - `authKeyMap` 支持 master 按 `slaveId` 配置不同 key(未命中时会回退到 `authKey`)
19
20
  - Payload 支持 `string`、`Buffer`、`Uint8Array` 及其数组(多帧)
20
21
 
21
22
  ## 安装
@@ -103,6 +104,7 @@ new ZNL({
103
104
  },
104
105
  maxPending: 1000,
105
106
  authKey: "",
107
+ authKeyMap: { "slave-001": "k1", "slave-002": "k2" },
106
108
  heartbeatInterval: 3000,
107
109
  heartbeatTimeoutMs: 0,
108
110
  encrypted: false,
@@ -118,7 +120,8 @@ new ZNL({
118
120
  | `id` | ✓ | 节点唯一标识;slave 端同时作为 ZMQ `routingId` |
119
121
  | `endpoints.router` | | ROUTER 端点,默认 `tcp://127.0.0.1:6003` |
120
122
  | `maxPending` | | 最大并发 RPC 请求数,默认 `1000`;`0` 表示不限制 |
121
- | `authKey` | | 共享认证 Key;仅在 `encrypted=true` 时必填(用于签名/加密) |
123
+ | `authKey` | | 共享认证 Key;与 `authKeyMap` 二选一(`encrypted=true` 时至少提供一个) |
124
+ | `authKeyMap` | | master 侧 slaveId → authKey 映射;未命中时回退到 `authKey` |
122
125
  | `heartbeatInterval` | | 心跳间隔(毫秒),默认 `3000`,`0` 表示禁用心跳 |
123
126
  | `heartbeatTimeoutMs` | | 心跳超时时间(毫秒),默认 `0` 表示使用 `heartbeatInterval × 3` |
124
127
  | `encrypted` | | 是否启用加密:`false`(默认,明文) / `true`(签名+防重放+透明加密) |
@@ -158,6 +161,18 @@ new ZNL({
158
161
  - `identityOrHandler` 为函数时:注册 master 侧自动回复处理器(Slave 发来 RPC 请求时触发)
159
162
  - `identityOrHandler` 为 identity(slave ID)时:Master 主动向指定 Slave 发送 RPC 请求并等待响应,返回 `Promise<Buffer | Array>`
160
163
 
164
+ ### `addAuthKey(slaveId, authKey)`
165
+
166
+ **Master 侧调用:**
167
+
168
+ - 动态添加/更新某个 slave 的 authKey(立即生效)
169
+
170
+ ### `removeAuthKey(slaveId)`
171
+
172
+ **Master 侧调用:**
173
+
174
+ - 移除某个 slave 的 authKey(立即生效),并触发 `slave_disconnected`
175
+
161
176
  ### `options.timeoutMs`
162
177
 
163
178
  单次 RPC 请求超时时间,默认 `5000` ms。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lyrify/znl",
3
- "version": "0.5.2",
3
+ "version": "0.5.6",
4
4
  "description": "ZNL - ZeroMQ Node Link",
5
5
  "type": "module",
6
6
  "main": "./index.js",
package/src/ZNL.js CHANGED
@@ -127,13 +127,19 @@ export class ZNL extends EventEmitter {
127
127
  /** @type {boolean} 是否启用 payload 摘要校验(安全模式用) */
128
128
  #enablePayloadDigest = DEFAULT_ENABLE_PAYLOAD_DIGEST;
129
129
 
130
+ /** @type {Map<string, string>} master 侧 authKey 配置表(按 slaveId) */
131
+ #authKeyMap = new Map();
132
+
133
+ /** @type {Map<string, { signKey: Buffer, encryptKey: Buffer }>} master 侧派生密钥缓存 */
134
+ #slaveKeyCache = new Map();
135
+
130
136
  // ─── PUB/SUB 状态 ────────────────────────────────────────────────────────
131
137
 
132
138
  /**
133
139
  * master 侧:已注册的在线 slave 表
134
140
  * key = slaveId(字符串)
135
- * value = { identity: Buffer, lastSeen: number }
136
- * @type {Map<string, { identity: Buffer, lastSeen: number }>}
141
+ * value = { identity: Buffer, lastSeen: number, signKey: Buffer|null, encryptKey: Buffer|null }
142
+ * @type {Map<string, { identity: Buffer, lastSeen: number, signKey: Buffer|null, encryptKey: Buffer|null }>}
137
143
  */
138
144
  #slaves = new Map();
139
145
 
@@ -185,6 +191,7 @@ export class ZNL extends EventEmitter {
185
191
  * endpoints? : { router?: string },
186
192
  * maxPending? : number,
187
193
  * authKey? : string,
194
+ * authKeyMap? : Record<string, string>,
188
195
  * heartbeatInterval? : number,
189
196
  * heartbeatTimeoutMs? : number,
190
197
  * encrypted? : boolean,
@@ -199,6 +206,7 @@ export class ZNL extends EventEmitter {
199
206
  endpoints = {},
200
207
  maxPending = DEFAULT_MAX_PENDING,
201
208
  authKey = "",
209
+ authKeyMap = null,
202
210
  heartbeatInterval = DEFAULT_HEARTBEAT_INTERVAL,
203
211
  heartbeatTimeoutMs = DEFAULT_HEARTBEAT_TIMEOUT_MS,
204
212
  encrypted = false,
@@ -220,16 +228,35 @@ export class ZNL extends EventEmitter {
220
228
  this.endpoints = { ...DEFAULT_ENDPOINTS, ...endpoints };
221
229
  this.authKey = authKey == null ? "" : String(authKey);
222
230
 
231
+ if (this.role === "master") {
232
+ if (authKeyMap != null) {
233
+ if (typeof authKeyMap !== "object") {
234
+ throw new TypeError(
235
+ "`authKeyMap` 必须是对象(slaveId -> authKey)。",
236
+ );
237
+ }
238
+ for (const [slaveId, key] of Object.entries(authKeyMap)) {
239
+ if (key == null) continue;
240
+ this.#authKeyMap.set(String(slaveId), String(key));
241
+ }
242
+ }
243
+ }
244
+
223
245
  this.encrypted = Boolean(encrypted);
224
246
  this.#secureEnabled = this.encrypted;
225
247
 
226
248
  if (this.#secureEnabled) {
227
- if (!this.authKey) {
228
- throw new Error("启用 encrypted=true 时,必须提供非空 authKey。");
249
+ const hasAuthMap = this.role === "master" && authKeyMap != null;
250
+ if (!this.authKey && !hasAuthMap) {
251
+ throw new Error(
252
+ "启用 encrypted=true 时,必须提供非空 authKey 或 authKeyMap。",
253
+ );
254
+ }
255
+ if (this.authKey) {
256
+ const { signKey, encryptKey } = deriveKeys(this.authKey);
257
+ this.#signKey = signKey;
258
+ this.#encryptKey = encryptKey;
229
259
  }
230
- const { signKey, encryptKey } = deriveKeys(this.authKey);
231
- this.#signKey = signKey;
232
- this.#encryptKey = encryptKey;
233
260
  }
234
261
 
235
262
  this.#pending = new PendingManager(maxPending);
@@ -356,19 +383,34 @@ export class ZNL extends EventEmitter {
356
383
  if (this.#slaves.size === 0) return;
357
384
 
358
385
  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);
369
386
 
370
387
  for (const [slaveId, entry] of this.#slaves) {
388
+ const keys = this.#secureEnabled ? this.#resolveSlaveKeys(slaveId) : null;
389
+ if (this.#secureEnabled && !keys) {
390
+ if (this.#slaves.delete(slaveId)) {
391
+ this.emit("slave_disconnected", slaveId);
392
+ }
393
+ continue;
394
+ }
395
+
396
+ const payloadFrames = this.#sealPayloadFrames(
397
+ "publish",
398
+ topicText,
399
+ payload,
400
+ keys?.encryptKey ?? null,
401
+ );
402
+ const authProof = this.#secureEnabled
403
+ ? this.#createAuthProof(
404
+ "publish",
405
+ topicText,
406
+ payloadFrames,
407
+ keys.signKey,
408
+ )
409
+ : "";
410
+
411
+ const frames = buildPublishFrames(topicText, payloadFrames, authProof);
371
412
  const idFrame = identityToBuffer(entry.identity);
413
+
372
414
  this.#sendQueue
373
415
  .enqueue("router", () => socket.send([idFrame, ...frames]))
374
416
  .catch(() => {
@@ -424,6 +466,63 @@ export class ZNL extends EventEmitter {
424
466
  return [...this.#slaves.keys()];
425
467
  }
426
468
 
469
+ /**
470
+ * 【Master 侧】动态添加/更新某个 slave 的 authKey(立即生效)
471
+ * @param {string} slaveId
472
+ * @param {string} authKey
473
+ * @returns {this}
474
+ */
475
+ addAuthKey(slaveId, authKey) {
476
+ if (this.role !== "master") {
477
+ throw new Error("addAuthKey() 只能在 master 侧调用。");
478
+ }
479
+ const id = String(slaveId ?? "");
480
+ if (!id) {
481
+ throw new Error("slaveId 不能为空。");
482
+ }
483
+ const key = authKey == null ? "" : String(authKey);
484
+ if (!key) {
485
+ throw new Error("authKey 不能为空。");
486
+ }
487
+
488
+ this.#authKeyMap.set(id, key);
489
+
490
+ if (this.#secureEnabled) {
491
+ const derived = deriveKeys(key);
492
+ this.#slaveKeyCache.set(id, derived);
493
+ const entry = this.#slaves.get(id);
494
+ if (entry) {
495
+ entry.signKey = derived.signKey;
496
+ entry.encryptKey = derived.encryptKey;
497
+ }
498
+ }
499
+ return this;
500
+ }
501
+
502
+ /**
503
+ * 【Master 侧】移除某个 slave 的 authKey(立即生效)
504
+ * @param {string} slaveId
505
+ * @returns {this}
506
+ */
507
+ removeAuthKey(slaveId) {
508
+ if (this.role !== "master") {
509
+ throw new Error("removeAuthKey() 只能在 master 侧调用。");
510
+ }
511
+ const id = String(slaveId ?? "");
512
+ if (!id) {
513
+ throw new Error("slaveId 不能为空。");
514
+ }
515
+
516
+ this.#authKeyMap.delete(id);
517
+ this.#slaveKeyCache.delete(id);
518
+
519
+ if (this.#slaves.has(id)) {
520
+ this.#slaves.delete(id);
521
+ this.emit("slave_disconnected", id);
522
+ }
523
+ return this;
524
+ }
525
+
427
526
  // ═══════════════════════════════════════════════════════════════════════════
428
527
  // 生命周期内部实现
429
528
  // ═══════════════════════════════════════════════════════════════════════════
@@ -486,6 +585,7 @@ export class ZNL extends EventEmitter {
486
585
  this.#sockets = {};
487
586
  this.#sendQueue.clear();
488
587
  this.#slaves.clear();
588
+ this.#slaveKeyCache.clear();
489
589
  this.#masterNodeId = null;
490
590
  this.#replayGuard.clear();
491
591
  }
@@ -498,7 +598,7 @@ export class ZNL extends EventEmitter {
498
598
  * 初始化 Master 侧 ROUTER socket(bind 模式),并启动读循环和死节点扫描定时器
499
599
  */
500
600
  async #startMasterSockets() {
501
- const router = new zmq.Router();
601
+ const router = new zmq.Router({ handover: true });
502
602
  await router.bind(this.endpoints.router);
503
603
  this.#sockets.router = router;
504
604
 
@@ -555,17 +655,34 @@ export class ZNL extends EventEmitter {
555
655
  // ── 心跳 ────────────────────────────────────────────────────────────────
556
656
  if (parsed.kind === "heartbeat") {
557
657
  if (this.#secureEnabled) {
658
+ const keys = this.#resolveSlaveKeys(identityText);
659
+ if (!keys) {
660
+ this.#emitAuthFailed(event, "未配置该 slave 的 authKey。");
661
+ if (this.#slaves.delete(identityText)) {
662
+ this.emit("slave_disconnected", identityText);
663
+ }
664
+ return;
665
+ }
666
+
558
667
  const v = this.#verifyIncomingProof({
559
668
  kind: "heartbeat",
560
669
  proofToken: parsed.authProof,
561
670
  requestId: "",
562
671
  payloadFrames: [],
563
672
  expectedNodeId: identityText,
673
+ signKey: keys.signKey,
564
674
  });
565
675
  if (!v.ok) {
566
676
  this.#emitAuthFailed(event, v.error);
567
677
  return;
568
678
  }
679
+
680
+ this.#ensureSlaveOnline(identityText, identity, {
681
+ touch: true,
682
+ signKey: keys.signKey,
683
+ encryptKey: keys.encryptKey,
684
+ });
685
+ return;
569
686
  }
570
687
 
571
688
  // 心跳视为在线确认:必要时自动补注册
@@ -576,20 +693,42 @@ export class ZNL extends EventEmitter {
576
693
  // ── 注册 ────────────────────────────────────────────────────────────────
577
694
  if (parsed.kind === "register") {
578
695
  if (this.#secureEnabled) {
696
+ const keys = this.#resolveSlaveKeys(identityText);
697
+ if (!keys) {
698
+ this.#emitAuthFailed(event, "未配置该 slave 的 authKey。");
699
+ if (this.#slaves.delete(identityText)) {
700
+ this.emit("slave_disconnected", identityText);
701
+ }
702
+ return;
703
+ }
704
+
579
705
  const v = this.#verifyIncomingProof({
580
706
  kind: "register",
581
707
  proofToken: parsed.authKey, // register 复用 authKey 字段承载 proof
582
708
  requestId: "",
583
709
  payloadFrames: [],
584
710
  expectedNodeId: identityText,
711
+ signKey: keys.signKey,
585
712
  });
586
713
  if (!v.ok) {
587
714
  this.#emitAuthFailed(event, v.error);
588
715
  return;
589
716
  }
717
+
718
+ this.#ensureSlaveOnline(identityText, identity, {
719
+ touch: true,
720
+ signKey: keys.signKey,
721
+ encryptKey: keys.encryptKey,
722
+ });
723
+ return;
590
724
  }
591
725
 
592
- this.#slaves.set(identityText, { identity, lastSeen: Date.now() });
726
+ this.#slaves.set(identityText, {
727
+ identity,
728
+ lastSeen: Date.now(),
729
+ signKey: null,
730
+ encryptKey: null,
731
+ });
593
732
  this.emit("slave_connected", identityText);
594
733
  return;
595
734
  }
@@ -607,12 +746,22 @@ export class ZNL extends EventEmitter {
607
746
  let finalFrames = parsed.payloadFrames;
608
747
 
609
748
  if (this.#secureEnabled) {
749
+ const keys = this.#resolveSlaveKeys(identityText);
750
+ if (!keys) {
751
+ this.#emitAuthFailed(event, "未配置该 slave 的 authKey。");
752
+ if (this.#slaves.delete(identityText)) {
753
+ this.emit("slave_disconnected", identityText);
754
+ }
755
+ return;
756
+ }
757
+
610
758
  const v = this.#verifyIncomingProof({
611
759
  kind: "request",
612
760
  proofToken: parsed.authKey, // request 复用 authKey 字段承载 proof
613
761
  requestId: parsed.requestId,
614
762
  payloadFrames: parsed.payloadFrames,
615
763
  expectedNodeId: identityText,
764
+ signKey: keys.signKey,
616
765
  });
617
766
  if (!v.ok) {
618
767
  this.#emitAuthFailed(event, v.error);
@@ -620,7 +769,11 @@ export class ZNL extends EventEmitter {
620
769
  }
621
770
 
622
771
  // 认证通过后允许补注册(避免 master 重启后丢失在线表)
623
- this.#ensureSlaveOnline(identityText, identity, { touch: true });
772
+ this.#ensureSlaveOnline(identityText, identity, {
773
+ touch: true,
774
+ signKey: keys.signKey,
775
+ encryptKey: keys.encryptKey,
776
+ });
624
777
 
625
778
  if (this.encrypted) {
626
779
  try {
@@ -629,6 +782,7 @@ export class ZNL extends EventEmitter {
629
782
  parsed.requestId,
630
783
  parsed.payloadFrames,
631
784
  identityText,
785
+ keys.encryptKey,
632
786
  );
633
787
  } catch (error) {
634
788
  this.#emitAuthFailed(
@@ -663,12 +817,22 @@ export class ZNL extends EventEmitter {
663
817
  let finalFrames = parsed.payloadFrames;
664
818
 
665
819
  if (this.#secureEnabled) {
820
+ const keys = this.#resolveSlaveKeys(identityText);
821
+ if (!keys) {
822
+ this.#emitAuthFailed(event, "未配置该 slave 的 authKey。");
823
+ if (this.#slaves.delete(identityText)) {
824
+ this.emit("slave_disconnected", identityText);
825
+ }
826
+ return;
827
+ }
828
+
666
829
  const v = this.#verifyIncomingProof({
667
830
  kind: "response",
668
831
  proofToken: parsed.authProof,
669
832
  requestId: parsed.requestId,
670
833
  payloadFrames: parsed.payloadFrames,
671
834
  expectedNodeId: identityText,
835
+ signKey: keys.signKey,
672
836
  });
673
837
  if (!v.ok) {
674
838
  this.#emitAuthFailed(event, v.error);
@@ -676,7 +840,11 @@ export class ZNL extends EventEmitter {
676
840
  }
677
841
 
678
842
  // 认证通过后允许补注册(避免 master 重启后丢失在线表)
679
- this.#ensureSlaveOnline(identityText, identity, { touch: true });
843
+ this.#ensureSlaveOnline(identityText, identity, {
844
+ touch: true,
845
+ signKey: keys.signKey,
846
+ encryptKey: keys.encryptKey,
847
+ });
680
848
 
681
849
  if (this.encrypted) {
682
850
  try {
@@ -685,6 +853,7 @@ export class ZNL extends EventEmitter {
685
853
  parsed.requestId,
686
854
  parsed.payloadFrames,
687
855
  identityText,
856
+ keys.encryptKey,
688
857
  );
689
858
  } catch (error) {
690
859
  this.#emitAuthFailed(
@@ -927,13 +1096,21 @@ export class ZNL extends EventEmitter {
927
1096
  identityText,
928
1097
  );
929
1098
 
1099
+ const keys = this.#secureEnabled
1100
+ ? this.#resolveSlaveKeys(identityText)
1101
+ : null;
1102
+ if (this.#secureEnabled && !keys) {
1103
+ throw new Error(`未配置该 slave 的 authKey:${identityText}`);
1104
+ }
1105
+
930
1106
  const payloadFrames = this.#sealPayloadFrames(
931
1107
  "request",
932
1108
  requestId,
933
1109
  payload,
1110
+ keys?.encryptKey ?? null,
934
1111
  );
935
1112
  const proofOrAuthKey = this.#secureEnabled
936
- ? this.#createAuthProof("request", requestId, payloadFrames)
1113
+ ? this.#createAuthProof("request", requestId, payloadFrames, keys.signKey)
937
1114
  : "";
938
1115
 
939
1116
  const frames = buildRequestFrames(requestId, payloadFrames, proofOrAuthKey);
@@ -981,15 +1158,29 @@ export class ZNL extends EventEmitter {
981
1158
  */
982
1159
  async #replyTo(identity, requestId, payload) {
983
1160
  const socket = this.#requireSocket("router", "ROUTER");
1161
+ const identityText = identityToString(identity);
984
1162
  const idFrame = identityToBuffer(identity);
985
1163
 
1164
+ const keys = this.#secureEnabled
1165
+ ? this.#resolveSlaveKeys(identityText)
1166
+ : null;
1167
+ if (this.#secureEnabled && !keys) {
1168
+ throw new Error(`未配置该 slave 的 authKey:${identityText}`);
1169
+ }
1170
+
986
1171
  const payloadFrames = this.#sealPayloadFrames(
987
1172
  "response",
988
1173
  requestId,
989
1174
  payload,
1175
+ keys?.encryptKey ?? null,
990
1176
  );
991
1177
  const authProof = this.#secureEnabled
992
- ? this.#createAuthProof("response", requestId, payloadFrames)
1178
+ ? this.#createAuthProof(
1179
+ "response",
1180
+ requestId,
1181
+ payloadFrames,
1182
+ keys.signKey,
1183
+ )
993
1184
  : "";
994
1185
 
995
1186
  const frames = buildResponseFrames(requestId, payloadFrames, authProof);
@@ -1061,8 +1252,9 @@ export class ZNL extends EventEmitter {
1061
1252
  * @param {Buffer[]} payloadFrames
1062
1253
  * @returns {string}
1063
1254
  */
1064
- #createAuthProof(kind, requestId, payloadFrames) {
1065
- if (!this.#secureEnabled || !this.#signKey) return "";
1255
+ #createAuthProof(kind, requestId, payloadFrames, signKeyOverride = null) {
1256
+ const signKey = signKeyOverride ?? this.#signKey;
1257
+ if (!this.#secureEnabled || !signKey) return "";
1066
1258
 
1067
1259
  const envelope = {
1068
1260
  kind: String(kind),
@@ -1079,7 +1271,7 @@ export class ZNL extends EventEmitter {
1079
1271
  // 保留 canonical 文本,便于后续排障(签名实际在 token 内完成)
1080
1272
  canonicalSignInput(envelope);
1081
1273
 
1082
- return encodeAuthProofToken(this.#signKey, envelope);
1274
+ return encodeAuthProofToken(signKey, envelope);
1083
1275
  }
1084
1276
 
1085
1277
  /**
@@ -1100,9 +1292,12 @@ export class ZNL extends EventEmitter {
1100
1292
  requestId,
1101
1293
  payloadFrames,
1102
1294
  expectedNodeId = null,
1295
+ signKey = null,
1103
1296
  }) {
1104
1297
  if (!this.#secureEnabled) return { ok: true, envelope: null };
1105
- if (!this.#signKey) {
1298
+
1299
+ const verifyKey = signKey ?? this.#signKey;
1300
+ if (!verifyKey) {
1106
1301
  return { ok: false, error: "签名密钥未初始化。" };
1107
1302
  }
1108
1303
 
@@ -1110,7 +1305,7 @@ export class ZNL extends EventEmitter {
1110
1305
  return { ok: false, error: "缺少认证证明(proofToken)。" };
1111
1306
  }
1112
1307
 
1113
- const decoded = decodeAuthProofToken(this.#signKey, proofToken, {
1308
+ const decoded = decodeAuthProofToken(verifyKey, proofToken, {
1114
1309
  maxSkewMs: this.#maxTimeSkewMs,
1115
1310
  now: Date.now(),
1116
1311
  });
@@ -1167,13 +1362,14 @@ export class ZNL extends EventEmitter {
1167
1362
  * @param {*} payload
1168
1363
  * @returns {Buffer[]}
1169
1364
  */
1170
- #sealPayloadFrames(kind, requestId, payload) {
1365
+ #sealPayloadFrames(kind, requestId, payload, encryptKeyOverride = null) {
1171
1366
  if (!this.encrypted) {
1172
1367
  // 非加密模式保持历史行为:沿用协议层的原始帧类型(string/Buffer)
1173
1368
  return normalizeFrames(payload);
1174
1369
  }
1175
1370
 
1176
- if (!this.#encryptKey) {
1371
+ const encryptKey = encryptKeyOverride ?? this.#encryptKey;
1372
+ if (!encryptKey) {
1177
1373
  throw new Error("加密密钥未初始化。");
1178
1374
  }
1179
1375
 
@@ -1183,11 +1379,7 @@ export class ZNL extends EventEmitter {
1183
1379
  "utf8",
1184
1380
  );
1185
1381
 
1186
- const { iv, ciphertext, tag } = encryptFrames(
1187
- this.#encryptKey,
1188
- rawFrames,
1189
- aad,
1190
- );
1382
+ const { iv, ciphertext, tag } = encryptFrames(encryptKey, rawFrames, aad);
1191
1383
 
1192
1384
  // 用统一信封包装:version + iv + tag + ciphertext
1193
1385
  return [Buffer.from(SECURITY_ENVELOPE_VERSION), iv, tag, ciphertext];
@@ -1204,9 +1396,16 @@ export class ZNL extends EventEmitter {
1204
1396
  * @param {string} senderNodeId
1205
1397
  * @returns {Buffer[]}
1206
1398
  */
1207
- #openPayloadFrames(kind, requestId, payloadFrames, senderNodeId) {
1399
+ #openPayloadFrames(
1400
+ kind,
1401
+ requestId,
1402
+ payloadFrames,
1403
+ senderNodeId,
1404
+ encryptKeyOverride = null,
1405
+ ) {
1208
1406
  if (!this.encrypted) return payloadFrames;
1209
- if (!this.#encryptKey) throw new Error("加密密钥未初始化。");
1407
+ const encryptKey = encryptKeyOverride ?? this.#encryptKey;
1408
+ if (!encryptKey) throw new Error("加密密钥未初始化。");
1210
1409
 
1211
1410
  if (!Array.isArray(payloadFrames) || payloadFrames.length !== 4) {
1212
1411
  throw new Error("加密信封格式非法:期望 4 帧。");
@@ -1222,7 +1421,7 @@ export class ZNL extends EventEmitter {
1222
1421
  "utf8",
1223
1422
  );
1224
1423
 
1225
- return decryptFrames(this.#encryptKey, iv, ciphertext, tag, aad);
1424
+ return decryptFrames(encryptKey, iv, ciphertext, tag, aad);
1226
1425
  }
1227
1426
 
1228
1427
  /**
@@ -1247,7 +1446,11 @@ export class ZNL extends EventEmitter {
1247
1446
  * @param {Buffer|string|Uint8Array} identity
1248
1447
  * @param {{ touch?: boolean }} [options]
1249
1448
  */
1250
- #ensureSlaveOnline(identityText, identity, { touch = true } = {}) {
1449
+ #ensureSlaveOnline(
1450
+ identityText,
1451
+ identity,
1452
+ { touch = true, signKey = null, encryptKey = null } = {},
1453
+ ) {
1251
1454
  const id = String(identityText ?? "");
1252
1455
  if (!id) return;
1253
1456
 
@@ -1255,16 +1458,55 @@ export class ZNL extends EventEmitter {
1255
1458
  const entry = this.#slaves.get(id);
1256
1459
  if (entry) {
1257
1460
  if (touch) entry.lastSeen = now;
1461
+ if (signKey) entry.signKey = signKey;
1462
+ if (encryptKey) entry.encryptKey = encryptKey;
1258
1463
  return;
1259
1464
  }
1260
1465
 
1261
1466
  this.#slaves.set(id, {
1262
1467
  identity: identityToBuffer(identity),
1263
1468
  lastSeen: now,
1469
+ signKey: signKey ?? null,
1470
+ encryptKey: encryptKey ?? null,
1264
1471
  });
1265
1472
  this.emit("slave_connected", id);
1266
1473
  }
1267
1474
 
1475
+ /**
1476
+ * master 侧解析某个 slave 的签名/加密密钥
1477
+ * - 优先匹配 authKeyMap
1478
+ * - 未命中时回退到 this.authKey(若提供)
1479
+ *
1480
+ * @param {string} slaveId
1481
+ * @returns {{ signKey: Buffer, encryptKey: Buffer }|null}
1482
+ */
1483
+ #resolveSlaveKeys(slaveId) {
1484
+ if (!this.#secureEnabled) return null;
1485
+
1486
+ const id = String(slaveId ?? "");
1487
+ if (!id) return null;
1488
+
1489
+ const cached = this.#slaveKeyCache.get(id);
1490
+ if (cached) return cached;
1491
+
1492
+ let key = this.#authKeyMap.get(id);
1493
+ if (!key && this.authKey) {
1494
+ key = this.authKey;
1495
+ }
1496
+ if (!key) return null;
1497
+
1498
+ const derived = deriveKeys(key);
1499
+ this.#slaveKeyCache.set(id, derived);
1500
+
1501
+ const entry = this.#slaves.get(id);
1502
+ if (entry) {
1503
+ entry.signKey = derived.signKey;
1504
+ entry.encryptKey = derived.encryptKey;
1505
+ }
1506
+
1507
+ return derived;
1508
+ }
1509
+
1268
1510
  // ═══════════════════════════════════════════════════════════════════════════
1269
1511
  // 工具方法
1270
1512
  // ═══════════════════════════════════════════════════════════════════════════