@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/README.md +28 -6
- package/package.json +2 -2
- package/src/ZNL.js +580 -77
- package/src/constants.js +24 -0
- package/src/protocol.js +74 -12
- package/src/security.js +560 -0
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
|
-
|
|
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
|
-
|
|
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
|
|
170
|
-
* requestId
|
|
171
|
-
* authKey
|
|
172
|
-
*
|
|
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(
|
|
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
|
-
//
|
|
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
|
};
|