mqtt-crypto-client 0.1.0 → 0.2.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/README.md +68 -5
- package/dist/index.browser.js +9 -0
- package/dist/index.browser.js.map +7 -0
- package/dist/index.iife.js +9 -0
- package/dist/index.iife.js.map +7 -0
- package/package.json +9 -2
- package/src/browser.js +8 -0
- package/src/crypto-browser.js +295 -0
- package/src/crypto-js.d.ts +4 -0
- package/src/events-browser.js +114 -0
- package/src/process-browser.js +23 -0
- package/types/index.d.ts +5 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mqtt-crypto-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "MQTT over WebSocket client with CryptoJS-compatible AES envelope encryption, heartbeat, message deduplication and request/response helpers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mqtt",
|
|
@@ -14,10 +14,14 @@
|
|
|
14
14
|
"type": "module",
|
|
15
15
|
"main": "./dist/index.cjs",
|
|
16
16
|
"module": "./src/index.js",
|
|
17
|
+
"browser": "./dist/index.browser.js",
|
|
17
18
|
"types": "./types/index.d.ts",
|
|
19
|
+
"unpkg": "./dist/index.iife.js",
|
|
20
|
+
"jsdelivr": "./dist/index.iife.js",
|
|
18
21
|
"exports": {
|
|
19
22
|
".": {
|
|
20
23
|
"types": "./types/index.d.ts",
|
|
24
|
+
"browser": "./dist/index.browser.js",
|
|
21
25
|
"import": "./src/index.js",
|
|
22
26
|
"require": "./dist/index.cjs"
|
|
23
27
|
},
|
|
@@ -35,7 +39,7 @@
|
|
|
35
39
|
"node": ">=18"
|
|
36
40
|
},
|
|
37
41
|
"scripts": {
|
|
38
|
-
"build": "node scripts/build-cjs.mjs",
|
|
42
|
+
"build": "node scripts/build-cjs.mjs && node scripts/build-browser.mjs && node scripts/check-browser.mjs",
|
|
39
43
|
"test": "node --test",
|
|
40
44
|
"typecheck": "tsc -p tsconfig.json",
|
|
41
45
|
"prepack": "npm run build",
|
|
@@ -47,6 +51,9 @@
|
|
|
47
51
|
},
|
|
48
52
|
"devDependencies": {
|
|
49
53
|
"@types/node": "^22.10.0",
|
|
54
|
+
"acorn": "^8.19.0",
|
|
55
|
+
"acorn-walk": "^8.3.5",
|
|
56
|
+
"crypto-js": "^4.2.0",
|
|
50
57
|
"esbuild": "^0.25.0",
|
|
51
58
|
"typescript": "^5.7.0"
|
|
52
59
|
},
|
package/src/browser.js
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 浏览器端加解密实现:使用 crypto-js(仅打包进 IIFE),保持与 Node 版逐字节兼容。
|
|
3
|
+
*
|
|
4
|
+
* 兼容格式(OpenSSL "Salted__" 格式):
|
|
5
|
+
* 密文 = base64( "Salted__"(8 字节) + salt(默认 8 字节) + AES-256-CBC 密文 )
|
|
6
|
+
* 密钥/IV = EVP_BytesToKey(MD5, 1 次迭代, passphrase, salt),key 32 字节 + IV 16 字节
|
|
7
|
+
* 填充 = PKCS#7
|
|
8
|
+
*
|
|
9
|
+
* 与 Node 版差异:salt 参数接受 Uint8Array / crypto-js WordArray(Node 版要求 Buffer);
|
|
10
|
+
* deriveKeyAndIv 返回 Uint8Array。API 保持同步,不依赖 WebCrypto。
|
|
11
|
+
*/
|
|
12
|
+
import CryptoJS from 'crypto-js';
|
|
13
|
+
import { CryptoError } from './errors.js';
|
|
14
|
+
|
|
15
|
+
const SALT_HEADER = new Uint8Array([0x53, 0x61, 0x6c, 0x74, 0x65, 0x64, 0x5f, 0x5f]);
|
|
16
|
+
const KEY_LENGTH = 32;
|
|
17
|
+
const IV_LENGTH = 16;
|
|
18
|
+
const DEFAULT_SALT_LENGTH = 8;
|
|
19
|
+
const BLOCK_SIZE = 16;
|
|
20
|
+
const BASE64_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
21
|
+
|
|
22
|
+
const strictUtf8Decoder = new TextDecoder('utf-8', { fatal: true });
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {unknown} secret
|
|
26
|
+
*/
|
|
27
|
+
function assertSecret(secret) {
|
|
28
|
+
if (typeof secret !== 'string' || secret.length === 0) {
|
|
29
|
+
throw new CryptoError('secret 必须是非空字符串', 'MISSING_SECRET');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {unknown} value
|
|
35
|
+
* @returns {value is Uint8Array}
|
|
36
|
+
*/
|
|
37
|
+
function isByteView(value) {
|
|
38
|
+
return ArrayBuffer.isView(value) && /** @type {any} */ (value).BYTES_PER_ELEMENT === 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {unknown} value
|
|
43
|
+
* @returns {value is { words: number[], sigBytes: number }}
|
|
44
|
+
*/
|
|
45
|
+
function isWordArray(value) {
|
|
46
|
+
return Boolean(
|
|
47
|
+
value &&
|
|
48
|
+
typeof value === 'object' &&
|
|
49
|
+
Array.isArray(/** @type {any} */ (value).words) &&
|
|
50
|
+
typeof /** @type {any} */ (value).sigBytes === 'number',
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {{ words: number[], sigBytes: number }} wordArray
|
|
56
|
+
* @returns {Uint8Array}
|
|
57
|
+
*/
|
|
58
|
+
function wordArrayToBytes(wordArray) {
|
|
59
|
+
const bytes = new Uint8Array(wordArray.sigBytes);
|
|
60
|
+
for (let i = 0; i < wordArray.sigBytes; i += 1) {
|
|
61
|
+
bytes[i] = (wordArray.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
|
|
62
|
+
}
|
|
63
|
+
return bytes;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {Uint8Array} bytes
|
|
68
|
+
* @returns {{ words: number[], sigBytes: number }}
|
|
69
|
+
*/
|
|
70
|
+
function bytesToWordArray(bytes) {
|
|
71
|
+
/** @type {number[]} */
|
|
72
|
+
const words = [];
|
|
73
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
74
|
+
words[i >>> 2] = (words[i >>> 2] ?? 0) | (bytes[i] << (24 - (i % 4) * 8));
|
|
75
|
+
}
|
|
76
|
+
return CryptoJS.lib.WordArray.create(words, bytes.length);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {unknown} value
|
|
81
|
+
* @param {string} name
|
|
82
|
+
* @returns {Uint8Array}
|
|
83
|
+
*/
|
|
84
|
+
function toBytes(value, name) {
|
|
85
|
+
if (isByteView(value)) return new Uint8Array(value);
|
|
86
|
+
if (isWordArray(value)) return wordArrayToBytes(value);
|
|
87
|
+
throw new CryptoError(`${name} 必须是 Uint8Array 或 crypto-js WordArray`, 'INVALID_SALT');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {string} secret
|
|
92
|
+
* @param {Uint8Array} salt
|
|
93
|
+
* @param {number} [keyLength]
|
|
94
|
+
* @param {number} [ivLength]
|
|
95
|
+
* @returns {{ key: { words: number[], sigBytes: number }, iv: { words: number[], sigBytes: number } }}
|
|
96
|
+
*/
|
|
97
|
+
function deriveWordArrays(secret, salt, keyLength = KEY_LENGTH, ivLength = IV_LENGTH) {
|
|
98
|
+
if (!Number.isInteger(keyLength) || keyLength <= 0 || keyLength % 4 !== 0) {
|
|
99
|
+
throw new CryptoError('keyLength 必须是 4 的正整数倍', 'INVALID_ARGUMENT');
|
|
100
|
+
}
|
|
101
|
+
if (!Number.isInteger(ivLength) || ivLength <= 0 || ivLength % 4 !== 0) {
|
|
102
|
+
throw new CryptoError('ivLength 必须是 4 的正整数倍', 'INVALID_ARGUMENT');
|
|
103
|
+
}
|
|
104
|
+
const kdf = CryptoJS.kdf.OpenSSL.execute(
|
|
105
|
+
secret,
|
|
106
|
+
keyLength / 4,
|
|
107
|
+
ivLength / 4,
|
|
108
|
+
bytesToWordArray(salt),
|
|
109
|
+
);
|
|
110
|
+
return { key: kdf.key, iv: kdf.iv };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* OpenSSL EVP_BytesToKey(MD5,count=1),与 CryptoJS 的 OpenSSLKdf 一致。
|
|
115
|
+
* @param {string} secret 口令
|
|
116
|
+
* @param {Uint8Array | { words: number[], sigBytes: number }} salt
|
|
117
|
+
* @param {number} [keyLength]
|
|
118
|
+
* @param {number} [ivLength]
|
|
119
|
+
* @returns {{ key: Uint8Array, iv: Uint8Array }}
|
|
120
|
+
*/
|
|
121
|
+
export function deriveKeyAndIv(secret, salt, keyLength = KEY_LENGTH, ivLength = IV_LENGTH) {
|
|
122
|
+
assertSecret(secret);
|
|
123
|
+
const saltBytes = toBytes(salt, 'salt');
|
|
124
|
+
const { key, iv } = deriveWordArrays(secret, saltBytes, keyLength, ivLength);
|
|
125
|
+
return { key: wordArrayToBytes(key), iv: wordArrayToBytes(iv) };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @param {Uint8Array} bytes
|
|
130
|
+
* @returns {Uint8Array}
|
|
131
|
+
*/
|
|
132
|
+
function padPkcs7(bytes) {
|
|
133
|
+
const padLength = BLOCK_SIZE - (bytes.length % BLOCK_SIZE);
|
|
134
|
+
const padded = new Uint8Array(bytes.length + padLength);
|
|
135
|
+
padded.set(bytes, 0);
|
|
136
|
+
padded.fill(padLength, bytes.length);
|
|
137
|
+
return padded;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @param {Uint8Array} padded
|
|
142
|
+
* @returns {Uint8Array}
|
|
143
|
+
*/
|
|
144
|
+
function unpadPkcs7(padded) {
|
|
145
|
+
const padLength = padded[padded.length - 1];
|
|
146
|
+
if (padLength === undefined || padLength < 1 || padLength > BLOCK_SIZE || padLength > padded.length) {
|
|
147
|
+
throw new CryptoError('解密失败(PKCS#7 填充非法)', 'DECRYPT_FAILED');
|
|
148
|
+
}
|
|
149
|
+
for (let i = padded.length - padLength; i < padded.length; i += 1) {
|
|
150
|
+
if (padded[i] !== padLength) {
|
|
151
|
+
throw new CryptoError('解密失败(PKCS#7 填充非法)', 'DECRYPT_FAILED');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return padded.subarray(0, padded.length - padLength);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 加密任意字符串,返回与 Node 版 / CryptoJS 完全兼容的 base64 密文。
|
|
159
|
+
* @param {string} plaintext
|
|
160
|
+
* @param {string} secret
|
|
161
|
+
* @param {{ saltLength?: number, salt?: Uint8Array | { words: number[], sigBytes: number } }} [options]
|
|
162
|
+
* @returns {string}
|
|
163
|
+
*/
|
|
164
|
+
export function encryptPayload(plaintext, secret, options = {}) {
|
|
165
|
+
if (typeof plaintext !== 'string') {
|
|
166
|
+
throw new CryptoError('plaintext 必须是字符串', 'INVALID_PLAINTEXT');
|
|
167
|
+
}
|
|
168
|
+
assertSecret(secret);
|
|
169
|
+
|
|
170
|
+
const saltLength = options.saltLength ?? DEFAULT_SALT_LENGTH;
|
|
171
|
+
if (!Number.isInteger(saltLength) || saltLength < 0) {
|
|
172
|
+
throw new CryptoError('saltLength 必须是非负整数', 'INVALID_SALT');
|
|
173
|
+
}
|
|
174
|
+
const salt = options.salt !== undefined
|
|
175
|
+
? toBytes(options.salt, 'salt')
|
|
176
|
+
: wordArrayToBytes(CryptoJS.lib.WordArray.random(saltLength));
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const { key, iv } = deriveWordArrays(secret, salt);
|
|
180
|
+
const plaintextBytes = new TextEncoder().encode(plaintext);
|
|
181
|
+
const encrypted = CryptoJS.AES.encrypt(bytesToWordArray(padPkcs7(plaintextBytes)), key, {
|
|
182
|
+
iv,
|
|
183
|
+
mode: CryptoJS.mode.CBC,
|
|
184
|
+
padding: CryptoJS.pad.NoPadding,
|
|
185
|
+
});
|
|
186
|
+
const output = CryptoJS.lib.WordArray.create(
|
|
187
|
+
[0x53616c74, 0x65645f5f],
|
|
188
|
+
SALT_HEADER.length,
|
|
189
|
+
);
|
|
190
|
+
const withSalt = output.concat(bytesToWordArray(salt));
|
|
191
|
+
return withSalt.concat(encrypted.ciphertext).toString(CryptoJS.enc.Base64);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (error instanceof CryptoError) throw error;
|
|
194
|
+
throw new CryptoError('加密失败', 'ENCRYPT_FAILED', /** @type {Error} */ (error));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* @param {Uint8Array} raw
|
|
200
|
+
* @returns {{ salt: Uint8Array, ciphertext: Uint8Array }}
|
|
201
|
+
*/
|
|
202
|
+
function splitCiphertext(raw) {
|
|
203
|
+
const hasHeader =
|
|
204
|
+
raw.length >= SALT_HEADER.length &&
|
|
205
|
+
SALT_HEADER.every((byte, index) => raw[index] === byte);
|
|
206
|
+
|
|
207
|
+
if (hasHeader) {
|
|
208
|
+
if (raw.length < SALT_HEADER.length + DEFAULT_SALT_LENGTH) {
|
|
209
|
+
throw new CryptoError('密文缺少 salt', 'INVALID_CIPHERTEXT');
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
salt: raw.subarray(SALT_HEADER.length, SALT_HEADER.length + DEFAULT_SALT_LENGTH),
|
|
213
|
+
ciphertext: raw.subarray(SALT_HEADER.length + DEFAULT_SALT_LENGTH),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
return { salt: new Uint8Array(0), ciphertext: raw };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* 解密并返回 UTF-8 明文;失败时抛出 CryptoError。
|
|
221
|
+
* @param {string} ciphertext base64 密文
|
|
222
|
+
* @param {string} secret 口令
|
|
223
|
+
* @returns {string}
|
|
224
|
+
*/
|
|
225
|
+
export function decryptPayload(ciphertext, secret) {
|
|
226
|
+
if (typeof ciphertext !== 'string') {
|
|
227
|
+
throw new CryptoError('密文必须是字符串', 'INVALID_CIPHERTEXT');
|
|
228
|
+
}
|
|
229
|
+
assertSecret(secret);
|
|
230
|
+
|
|
231
|
+
const normalized = ciphertext.trim();
|
|
232
|
+
if (!normalized || normalized.length % 4 !== 0 || !BASE64_PATTERN.test(normalized)) {
|
|
233
|
+
throw new CryptoError('密文不是合法的 base64', 'INVALID_CIPHERTEXT');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const raw = wordArrayToBytes(CryptoJS.enc.Base64.parse(normalized));
|
|
237
|
+
if (raw.length < SALT_HEADER.length) {
|
|
238
|
+
throw new CryptoError('密文长度不足', 'INVALID_CIPHERTEXT');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const { salt, ciphertext: body } = splitCiphertext(raw);
|
|
242
|
+
if (body.length === 0 || body.length % BLOCK_SIZE !== 0) {
|
|
243
|
+
throw new CryptoError('密文块长度非法', 'INVALID_CIPHERTEXT');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const { key, iv } = deriveWordArrays(secret, salt);
|
|
248
|
+
const decrypted = CryptoJS.AES.decrypt(
|
|
249
|
+
CryptoJS.lib.CipherParams.create({ ciphertext: bytesToWordArray(body) }),
|
|
250
|
+
key,
|
|
251
|
+
{ iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.NoPadding },
|
|
252
|
+
);
|
|
253
|
+
const unpadded = unpadPkcs7(wordArrayToBytes(decrypted));
|
|
254
|
+
return strictUtf8Decoder.decode(unpadded);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (error instanceof CryptoError) throw error;
|
|
257
|
+
throw new CryptoError(
|
|
258
|
+
'解密失败(口令错误、密文损坏或不是 UTF-8)',
|
|
259
|
+
'DECRYPT_FAILED',
|
|
260
|
+
/** @type {Error} */ (error),
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* 宽松解密:任何失败都返回 null。
|
|
267
|
+
* @param {string} ciphertext
|
|
268
|
+
* @param {string} secret
|
|
269
|
+
* @returns {string | null}
|
|
270
|
+
*/
|
|
271
|
+
export function tryDecrypt(ciphertext, secret) {
|
|
272
|
+
if (typeof ciphertext !== 'string' || !ciphertext || !secret) return null;
|
|
273
|
+
try {
|
|
274
|
+
const plaintext = decryptPayload(ciphertext, secret);
|
|
275
|
+
return plaintext.length > 0 ? plaintext : null;
|
|
276
|
+
} catch {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* 判断字符串是否是本库/CryptoJS 的 "Salted__" base64 密文(仅看头部)。
|
|
283
|
+
* @param {string} value
|
|
284
|
+
* @returns {boolean}
|
|
285
|
+
*/
|
|
286
|
+
export function isEncryptedPayload(value) {
|
|
287
|
+
if (typeof value !== 'string') return false;
|
|
288
|
+
const normalized = value.trim();
|
|
289
|
+
if (!normalized || normalized.length % 4 !== 0 || !BASE64_PATTERN.test(normalized)) return false;
|
|
290
|
+
const raw = wordArrayToBytes(CryptoJS.enc.Base64.parse(normalized));
|
|
291
|
+
return (
|
|
292
|
+
raw.length >= SALT_HEADER.length &&
|
|
293
|
+
SALT_HEADER.every((byte, index) => raw[index] === byte)
|
|
294
|
+
);
|
|
295
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 浏览器构建用的极简 EventEmitter,替代 Node 的 `node:events`。
|
|
3
|
+
* 仅实现本库客户端实际使用的方法,避免把 events 包变成运行时依赖。
|
|
4
|
+
*/
|
|
5
|
+
export class EventEmitter {
|
|
6
|
+
constructor() {
|
|
7
|
+
/** @type {Map<string | symbol, Set<Function>>} */
|
|
8
|
+
this._listeners = new Map();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string | symbol} event
|
|
13
|
+
* @param {Function} listener
|
|
14
|
+
* @returns {this}
|
|
15
|
+
*/
|
|
16
|
+
on(event, listener) {
|
|
17
|
+
if (typeof listener !== 'function') {
|
|
18
|
+
throw new TypeError('listener 必须是函数');
|
|
19
|
+
}
|
|
20
|
+
let listeners = this._listeners.get(event);
|
|
21
|
+
if (!listeners) {
|
|
22
|
+
listeners = new Set();
|
|
23
|
+
this._listeners.set(event, listeners);
|
|
24
|
+
}
|
|
25
|
+
listeners.add(listener);
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string | symbol} event
|
|
31
|
+
* @param {Function} listener
|
|
32
|
+
* @returns {this}
|
|
33
|
+
*/
|
|
34
|
+
addListener(event, listener) {
|
|
35
|
+
return this.on(event, listener);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {string | symbol} event
|
|
40
|
+
* @param {Function} listener
|
|
41
|
+
* @returns {this}
|
|
42
|
+
*/
|
|
43
|
+
once(event, listener) {
|
|
44
|
+
/** @type {((...args: any[]) => void) & { listener?: Function }} */
|
|
45
|
+
const wrapper = (...args) => {
|
|
46
|
+
this.off(event, wrapper);
|
|
47
|
+
listener(...args);
|
|
48
|
+
};
|
|
49
|
+
wrapper.listener = listener;
|
|
50
|
+
return this.on(event, wrapper);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {string | symbol} event
|
|
55
|
+
* @param {Function} listener
|
|
56
|
+
* @returns {this}
|
|
57
|
+
*/
|
|
58
|
+
off(event, listener) {
|
|
59
|
+
const listeners = this._listeners.get(event);
|
|
60
|
+
if (!listeners) return this;
|
|
61
|
+
for (const candidate of listeners) {
|
|
62
|
+
const original = /** @type {any} */ (candidate).listener;
|
|
63
|
+
if (candidate === listener || original === listener) {
|
|
64
|
+
listeners.delete(candidate);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (listeners.size === 0) this._listeners.delete(event);
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {string | symbol} event
|
|
73
|
+
* @param {Function} listener
|
|
74
|
+
* @returns {this}
|
|
75
|
+
*/
|
|
76
|
+
removeListener(event, listener) {
|
|
77
|
+
return this.off(event, listener);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {string | symbol} [event]
|
|
82
|
+
* @returns {this}
|
|
83
|
+
*/
|
|
84
|
+
removeAllListeners(event) {
|
|
85
|
+
if (event === undefined) this._listeners.clear();
|
|
86
|
+
else this._listeners.delete(event);
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {string | symbol} event
|
|
92
|
+
* @returns {number}
|
|
93
|
+
*/
|
|
94
|
+
listenerCount(event) {
|
|
95
|
+
return this._listeners.get(event)?.size ?? 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param {string | symbol} event
|
|
100
|
+
* @param {...any} args
|
|
101
|
+
* @returns {boolean}
|
|
102
|
+
*/
|
|
103
|
+
emit(event, ...args) {
|
|
104
|
+
const listeners = this._listeners.get(event);
|
|
105
|
+
if (!listeners || listeners.size === 0) {
|
|
106
|
+
if (event === 'error') throw args[0] instanceof Error ? args[0] : new Error(String(args[0]));
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
for (const listener of [...listeners]) {
|
|
110
|
+
listener(...args);
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 仅用于浏览器 IIFE 的 process 兜底(通过 esbuild `inject` 注入)。
|
|
3
|
+
* Node/ESM/CJS 构建不会包含本文件。
|
|
4
|
+
*
|
|
5
|
+
* 优先由构建期 define 消除 `process.env.*` 等引用;本 shim 只处理无法静态消除的裸 `process`。
|
|
6
|
+
*/
|
|
7
|
+
const env = Object.freeze({ NODE_ENV: 'production' });
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {Function} callback
|
|
11
|
+
* @param {...any} args
|
|
12
|
+
*/
|
|
13
|
+
function nextTick(callback, ...args) {
|
|
14
|
+
queueMicrotask(() => callback(...args));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const process = Object.freeze({
|
|
18
|
+
env,
|
|
19
|
+
browser: true,
|
|
20
|
+
version: '0.0.0',
|
|
21
|
+
platform: 'browser',
|
|
22
|
+
nextTick,
|
|
23
|
+
});
|
package/types/index.d.ts
CHANGED
|
@@ -171,14 +171,15 @@ export declare function isResponseCommand(command: Record<string, any> | null |
|
|
|
171
171
|
export declare function encryptPayload(
|
|
172
172
|
plaintext: string,
|
|
173
173
|
secret: string,
|
|
174
|
-
options?: { saltLength?: number; salt?:
|
|
174
|
+
options?: { saltLength?: number; salt?: Uint8Array },
|
|
175
175
|
): string;
|
|
176
176
|
export declare function decryptPayload(ciphertext: string, secret: string): string;
|
|
177
177
|
export declare function tryDecrypt(ciphertext: string, secret: string): string | null;
|
|
178
178
|
export declare function isEncryptedPayload(value: string): boolean;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
179
|
+
/** Node 返回 Buffer;浏览器 IIFE 返回 Uint8Array(均兼容 Uint8Array 操作) */
|
|
180
|
+
export declare function deriveKeyAndIv(secret: string, salt: Uint8Array, keyLength?: number, ivLength?: number): {
|
|
181
|
+
key: Uint8Array;
|
|
182
|
+
iv: Uint8Array;
|
|
182
183
|
};
|
|
183
184
|
|
|
184
185
|
export declare function decodeIncoming(text: string, secret?: string): {
|