@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/constants.js CHANGED
@@ -40,3 +40,27 @@ export const DEFAULT_HEARTBEAT_INTERVAL = 3000;
40
40
  export const DEFAULT_ENDPOINTS = {
41
41
  router: "tcp://127.0.0.1:6003",
42
42
  };
43
+
44
+ /** 安全信封版本标识(用于区分普通帧与带安全元数据的帧) */
45
+ export const SECURITY_ENVELOPE_VERSION = "__znl_sec_v1__";
46
+
47
+ /** 允许的最大时间偏移(毫秒),超过则视为可疑重放 */
48
+ export const MAX_TIME_SKEW_MS = 30_000;
49
+
50
+ /** Nonce 去重保留窗口(毫秒),用于重放检测 */
51
+ export const REPLAY_WINDOW_MS = 120_000;
52
+
53
+ /** AES-GCM 推荐 IV 长度(字节) */
54
+ export const ENCRYPT_IV_BYTES = 12;
55
+
56
+ /** AES-GCM 认证标签长度(字节) */
57
+ export const ENCRYPT_TAG_BYTES = 16;
58
+
59
+ /** 对称加密算法(基于 authKey 派生出的密钥) */
60
+ export const ENCRYPT_ALGORITHM = "aes-256-gcm";
61
+
62
+ /** 密钥派生用途标签:加密 */
63
+ export const KDF_INFO_ENCRYPT = "znl-kdf-encrypt-v1";
64
+
65
+ /** 密钥派生用途标签:签名 */
66
+ export const KDF_INFO_SIGN = "znl-kdf-sign-v1";
package/src/protocol.js CHANGED
@@ -131,24 +131,53 @@ export function buildRequestFrames(requestId, payloadFrames, authKey = "") {
131
131
 
132
132
  /**
133
133
  * 构建响应控制帧数组
134
- * 帧结构:[PREFIX, "res", requestId, ...payloadFrames]
134
+ * 帧结构:[PREFIX, "res", requestId, (AUTH_MARKER, authProof)?, ...payloadFrames]
135
+ *
136
+ * 说明:
137
+ * - `authProof` 是可选的认证证明(例如签名令牌),与 `authKey` 概念不同。
138
+ * - 不传时与历史协议完全兼容。
139
+ *
135
140
  * @param {string} requestId
136
141
  * @param {Array<string|Buffer>} payloadFrames
142
+ * @param {string} [authProof] - 可选认证证明
137
143
  * @returns {Array}
138
144
  */
139
- export function buildResponseFrames(requestId, payloadFrames) {
140
- return [CONTROL_PREFIX, CONTROL_RES, String(requestId), ...payloadFrames];
145
+ export function buildResponseFrames(requestId, payloadFrames, authProof = "") {
146
+ const header = [CONTROL_PREFIX, CONTROL_RES, String(requestId)];
147
+ if (authProof) header.push(CONTROL_AUTH, authProof);
148
+ return [...header, ...payloadFrames];
141
149
  }
142
150
 
143
151
  /**
144
152
  * 构建广播控制帧数组(master → slave)
145
- * 帧结构:[PREFIX, "pub", topic, ...payloadFrames]
153
+ * 帧结构:[PREFIX, "pub", topic, (AUTH_MARKER, authProof)?, ...payloadFrames]
154
+ *
155
+ * 说明:
156
+ * - `authProof` 是可选的认证证明(例如签名令牌)。
157
+ * - 不传时与历史协议完全兼容。
158
+ *
146
159
  * @param {string} topic - 消息主题
147
160
  * @param {Array<string|Buffer>} payloadFrames - 已标准化的 payload 帧
161
+ * @param {string} [authProof] - 可选认证证明
148
162
  * @returns {Array}
149
163
  */
150
- export function buildPublishFrames(topic, payloadFrames) {
151
- return [CONTROL_PREFIX, CONTROL_PUB, String(topic), ...payloadFrames];
164
+ export function buildPublishFrames(topic, payloadFrames, authProof = "") {
165
+ const header = [CONTROL_PREFIX, CONTROL_PUB, String(topic)];
166
+ if (authProof) header.push(CONTROL_AUTH, authProof);
167
+ return [...header, ...payloadFrames];
168
+ }
169
+
170
+ /**
171
+ * 构建心跳控制帧数组(slave → master)
172
+ * 帧结构:[PREFIX, "heartbeat", (AUTH_MARKER, authProof)?]
173
+ *
174
+ * @param {string} [authProof] - 可选认证证明
175
+ * @returns {Array}
176
+ */
177
+ export function buildHeartbeatFrames(authProof = "") {
178
+ const frames = [CONTROL_PREFIX, CONTROL_HEARTBEAT];
179
+ if (authProof) frames.push(CONTROL_AUTH, authProof);
180
+ return frames;
152
181
  }
153
182
 
154
183
  // ─── 帧解析 ───────────────────────────────────────────────────────────────────
@@ -166,10 +195,11 @@ export function buildPublishFrames(topic, payloadFrames) {
166
195
  *
167
196
  * @param {Array} frames - 不含 identity 帧的帧数组
168
197
  * @returns {{
169
- * kind : "register"|"unregister"|"publish"|"request"|"response"|"message",
170
- * requestId : string|null,
171
- * authKey : string|null,
172
- * topic : string|null,
198
+ * kind : "register"|"unregister"|"heartbeat"|"publish"|"request"|"response"|"message",
199
+ * requestId : string|null,
200
+ * authKey : string|null,
201
+ * authProof : string|null,
202
+ * topic : string|null,
173
203
  * payloadFrames: Array
174
204
  * }}
175
205
  */
@@ -180,6 +210,7 @@ export function parseControlFrames(frames) {
180
210
  kind: "message",
181
211
  requestId: null,
182
212
  authKey: null,
213
+ authProof: null,
183
214
  topic: null,
184
215
  payloadFrames: frames,
185
216
  };
@@ -197,6 +228,7 @@ export function parseControlFrames(frames) {
197
228
  kind: "register",
198
229
  requestId: null,
199
230
  authKey,
231
+ authProof: null,
200
232
  topic: null,
201
233
  payloadFrames: [],
202
234
  };
@@ -208,6 +240,7 @@ export function parseControlFrames(frames) {
208
240
  kind: "unregister",
209
241
  requestId: null,
210
242
  authKey: null,
243
+ authProof: null,
211
244
  topic: null,
212
245
  payloadFrames: [],
213
246
  };
@@ -215,10 +248,16 @@ export function parseControlFrames(frames) {
215
248
 
216
249
  // ── 心跳帧:[PREFIX, "heartbeat"] ─────────────────────────────────────────
217
250
  if (action === CONTROL_HEARTBEAT) {
251
+ let authProof = null;
252
+ if (frames.length >= 4 && frames[2]?.toString() === CONTROL_AUTH) {
253
+ authProof = frames[3]?.toString() ?? "";
254
+ }
255
+
218
256
  return {
219
257
  kind: "heartbeat",
220
258
  requestId: null,
221
259
  authKey: null,
260
+ authProof,
222
261
  topic: null,
223
262
  payloadFrames: [],
224
263
  };
@@ -227,12 +266,22 @@ export function parseControlFrames(frames) {
227
266
  // ── 广播帧:[PREFIX, "pub", topic, ...payloadFrames] ─────────────────────
228
267
  if (action === CONTROL_PUB && frames.length >= 3) {
229
268
  const topic = frames[2]?.toString() ?? "";
269
+ let payloadStart = 3;
270
+ let authProof = null;
271
+
272
+ // 广播帧可携带可选认证证明:[PREFIX, "pub", topic, AUTH_MARKER, authProof, ...payload]
273
+ if (frames.length >= 5 && frames[3]?.toString() === CONTROL_AUTH) {
274
+ authProof = frames[4]?.toString() ?? "";
275
+ payloadStart = 5;
276
+ }
277
+
230
278
  return {
231
279
  kind: "publish",
232
280
  requestId: null,
233
281
  authKey: null,
282
+ authProof,
234
283
  topic,
235
- payloadFrames: frames.slice(3),
284
+ payloadFrames: frames.slice(payloadStart),
236
285
  };
237
286
  }
238
287
 
@@ -245,8 +294,9 @@ export function parseControlFrames(frames) {
245
294
  if (requestId) {
246
295
  let payloadStart = 3;
247
296
  let authKey = null;
297
+ let authProof = null;
248
298
 
249
- // 请求帧中可能携带认证 Key(仅 CONTROL_REQ 有此结构)
299
+ // 请求帧可携带 authKey:[PREFIX, "req", requestId, AUTH_MARKER, authKey, ...payload]
250
300
  if (
251
301
  action === CONTROL_REQ &&
252
302
  frames.length >= 5 &&
@@ -256,10 +306,21 @@ export function parseControlFrames(frames) {
256
306
  payloadStart = 5;
257
307
  }
258
308
 
309
+ // 响应帧可携带 authProof:[PREFIX, "res", requestId, AUTH_MARKER, authProof, ...payload]
310
+ if (
311
+ action === CONTROL_RES &&
312
+ frames.length >= 5 &&
313
+ frames[3]?.toString() === CONTROL_AUTH
314
+ ) {
315
+ authProof = frames[4]?.toString() ?? "";
316
+ payloadStart = 5;
317
+ }
318
+
259
319
  return {
260
320
  kind: action === CONTROL_REQ ? "request" : "response",
261
321
  requestId,
262
322
  authKey,
323
+ authProof,
263
324
  topic: null,
264
325
  payloadFrames: frames.slice(payloadStart),
265
326
  };
@@ -271,6 +332,7 @@ export function parseControlFrames(frames) {
271
332
  kind: "message",
272
333
  requestId: null,
273
334
  authKey: null,
335
+ authProof: null,
274
336
  topic: null,
275
337
  payloadFrames: frames,
276
338
  };