iframe.io 1.1.0 → 1.3.0
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 +407 -110
- package/dist/index.d.ts +162 -0
- package/dist/index.js +866 -229
- package/package.json +4 -6
- package/src/index.ts +872 -12
package/dist/index.js
CHANGED
|
@@ -1,25 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __assign = (this && this.__assign) || function () {
|
|
3
|
-
__assign = Object.assign || function(t) {
|
|
4
|
-
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
5
|
-
s = arguments[i];
|
|
6
|
-
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
7
|
-
t[p] = s[p];
|
|
8
|
-
}
|
|
9
|
-
return t;
|
|
10
|
-
};
|
|
11
|
-
return __assign.apply(this, arguments);
|
|
12
|
-
};
|
|
13
|
-
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
14
|
-
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
15
|
-
if (ar || !(i in from)) {
|
|
16
|
-
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
17
|
-
ar[i] = from[i];
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
return to.concat(ar || Array.prototype.slice.call(from));
|
|
21
|
-
};
|
|
22
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// Current protocol version
|
|
4
|
+
const PROTOCOL_VERSION = 1;
|
|
23
5
|
function newObject(data) {
|
|
24
6
|
return JSON.parse(JSON.stringify(data));
|
|
25
7
|
}
|
|
@@ -27,130 +9,603 @@ function getMessageSize(data) {
|
|
|
27
9
|
try {
|
|
28
10
|
return JSON.stringify(data).length;
|
|
29
11
|
}
|
|
30
|
-
catch
|
|
12
|
+
catch {
|
|
31
13
|
return 0;
|
|
32
14
|
}
|
|
33
15
|
}
|
|
34
16
|
function sanitizePayload(payload, maxSize) {
|
|
35
17
|
if (!payload)
|
|
36
18
|
return payload;
|
|
37
|
-
|
|
19
|
+
const size = getMessageSize(payload);
|
|
38
20
|
if (size > maxSize)
|
|
39
|
-
throw new Error(
|
|
21
|
+
throw new Error(`Message size ${size} exceeds limit ${maxSize}`);
|
|
40
22
|
// Basic sanitization - remove functions and undefined values
|
|
41
23
|
return JSON.parse(JSON.stringify(payload));
|
|
42
24
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
25
|
+
function constantTimeEqual(a, b) {
|
|
26
|
+
if (a.length !== b.length)
|
|
27
|
+
return false;
|
|
28
|
+
let out = 0;
|
|
29
|
+
for (let i = 0; i < a.length; i++)
|
|
30
|
+
out |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
31
|
+
return out === 0;
|
|
32
|
+
}
|
|
33
|
+
function getGlobalCrypto() {
|
|
34
|
+
return (typeof crypto !== 'undefined'
|
|
35
|
+
? crypto
|
|
36
|
+
: (typeof window !== 'undefined' && window.crypto)
|
|
37
|
+
|| (typeof globalThis !== 'undefined' && globalThis.crypto));
|
|
38
|
+
}
|
|
39
|
+
function randomHex(bytes) {
|
|
40
|
+
try {
|
|
41
|
+
const globalCrypto = getGlobalCrypto();
|
|
42
|
+
if (globalCrypto && typeof globalCrypto.getRandomValues === 'function') {
|
|
43
|
+
const buf = new Uint8Array(bytes);
|
|
44
|
+
globalCrypto.getRandomValues(buf);
|
|
45
|
+
return Array.from(buf).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch { }
|
|
49
|
+
// Fallback (NOT cryptographically strong)
|
|
50
|
+
return Array.from({ length: bytes }, () => Math.floor(Math.random() * 256).toString(16).padStart(2, '0')).join('');
|
|
51
|
+
}
|
|
52
|
+
async function hmacSha256Base64Url(secret, message) {
|
|
53
|
+
// Browser/WebCrypto
|
|
54
|
+
try {
|
|
55
|
+
const globalCrypto = getGlobalCrypto();
|
|
56
|
+
const subtle = globalCrypto?.subtle;
|
|
57
|
+
if (subtle && typeof subtle.importKey === 'function') {
|
|
58
|
+
const enc = new TextEncoder();
|
|
59
|
+
const key = await subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
60
|
+
const sig = await subtle.sign('HMAC', key, enc.encode(message));
|
|
61
|
+
const bytes = new Uint8Array(sig);
|
|
62
|
+
const b64 = btoa(String.fromCharCode(...bytes));
|
|
63
|
+
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// fallthrough to Node implementation
|
|
68
|
+
}
|
|
69
|
+
// Node.js (commonjs) - optional
|
|
70
|
+
try {
|
|
71
|
+
const nodeCrypto = globalThis.__iof_node_crypto
|
|
72
|
+
|| (globalThis.__iof_node_crypto = (typeof globalThis.require === 'function'
|
|
73
|
+
? globalThis.require('crypto')
|
|
74
|
+
: undefined));
|
|
75
|
+
if (!nodeCrypto)
|
|
76
|
+
throw new Error('node crypto unavailable');
|
|
77
|
+
const b64 = nodeCrypto.createHmac('sha256', secret).update(message).digest('base64');
|
|
78
|
+
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
throw new Error('No crypto implementation available for HMAC-SHA256');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Derive a session key using HKDF-like construction
|
|
86
|
+
* HKDF(masterSecret, salt, info) where:
|
|
87
|
+
* - masterSecret: the shared secret
|
|
88
|
+
* - salt: combined session IDs
|
|
89
|
+
* - info: context string
|
|
90
|
+
*/
|
|
91
|
+
async function deriveSessionKey(masterSecret, sessionId1, sessionId2, keyId) {
|
|
92
|
+
/**
|
|
93
|
+
* Two HMAC steps, both through `hmacSha256Base64Url`, deliberately.
|
|
94
|
+
*
|
|
95
|
+
* This used to have a WebCrypto branch alongside this one, and the two did
|
|
96
|
+
* not agree: the WebCrypto path fed the extract step's RAW bytes into the
|
|
97
|
+
* expand step and appended \x01 to the info string, while this path fed the
|
|
98
|
+
* base64url TEXT of those bytes and appended nothing. Same inputs, different
|
|
99
|
+
* keys.
|
|
100
|
+
*
|
|
101
|
+
* Peers do not have to share an implementation for that to matter, only an
|
|
102
|
+
* environment: `crypto.subtle` is undefined outside a secure context, so a
|
|
103
|
+
* WebView or iframe served over plain HTTP took one branch while an HTTPS
|
|
104
|
+
* host took the other. Every signed message then failed to verify — and
|
|
105
|
+
* because the WebCrypto branch was also entered from a silent catch, a
|
|
106
|
+
* single transient failure on one side desynchronised the pair for the rest
|
|
107
|
+
* of the connection.
|
|
108
|
+
*
|
|
109
|
+
* `hmacSha256Base64Url` already resolves WebCrypto vs Node internally and
|
|
110
|
+
* returns the same string either way, so deriving through it twice is both
|
|
111
|
+
* shorter and the only version that can agree with itself.
|
|
112
|
+
*/
|
|
113
|
+
const salt = sessionId1 + '|' + sessionId2,
|
|
114
|
+
// Extract: PRK = HMAC( masterSecret, salt )
|
|
115
|
+
prk = await hmacSha256Base64Url(masterSecret, salt),
|
|
116
|
+
// Expand: OKM = HMAC( PRK, info )
|
|
117
|
+
info = 'iframe.io-session-key-' + keyId;
|
|
118
|
+
return await hmacSha256Base64Url(prk, info);
|
|
119
|
+
}
|
|
120
|
+
const ackId = () => {
|
|
121
|
+
// Prefer cryptographically strong randomness when available
|
|
122
|
+
try {
|
|
123
|
+
const globalCrypto = getGlobalCrypto();
|
|
124
|
+
if (globalCrypto && typeof globalCrypto.getRandomValues === 'function') {
|
|
125
|
+
const buffer = new Uint32Array(4);
|
|
126
|
+
globalCrypto.getRandomValues(buffer);
|
|
127
|
+
const randomPart = Array.from(buffer).map(n => n.toString(16)).join('');
|
|
128
|
+
return `${Date.now()}_${randomPart}`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Fall back to Math.random-based implementation below
|
|
133
|
+
}
|
|
134
|
+
const rmin = 100000, rmax = 999999, timestampFallback = Date.now(), randomFallback = Math.floor(Math.random() * (rmax - rmin + 1) + rmin);
|
|
135
|
+
return `${timestampFallback}_${randomFallback}`;
|
|
46
136
|
};
|
|
47
|
-
|
|
137
|
+
// Answered before authentication, so they are gated separately — see the
|
|
138
|
+
// handlers in initiate() and listen().
|
|
139
|
+
const RESERVED_SESSION_KEY_EVENTS = [
|
|
140
|
+
'__session_key_init',
|
|
141
|
+
'__session_key_rotate',
|
|
142
|
+
'__session_key_ack'
|
|
143
|
+
];
|
|
144
|
+
const RESERVED_EVENTS = [
|
|
48
145
|
'ping',
|
|
49
146
|
'pong',
|
|
50
147
|
'__heartbeat',
|
|
51
|
-
'__heartbeat_response'
|
|
148
|
+
'__heartbeat_response',
|
|
149
|
+
'__session_key_init',
|
|
150
|
+
'__session_key_rotate',
|
|
151
|
+
'__session_key_ack'
|
|
52
152
|
];
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (options === void 0) { options = {}; }
|
|
153
|
+
class IOF {
|
|
154
|
+
constructor(options = {}) {
|
|
56
155
|
this.messageQueue = [];
|
|
57
156
|
this.messageRateTracker = [];
|
|
58
157
|
this.reconnectAttempts = 0;
|
|
59
158
|
this.maxReconnectAttempts = 5;
|
|
159
|
+
this.seenNonces = new Map();
|
|
60
160
|
if (options && typeof options !== 'object')
|
|
61
161
|
throw new Error('Invalid Options');
|
|
62
|
-
this.options =
|
|
162
|
+
this.options = {
|
|
163
|
+
debug: false,
|
|
164
|
+
heartbeatInterval: 30000,
|
|
165
|
+
connectionTimeout: 10000,
|
|
166
|
+
maxMessageSize: 1024 * 1024,
|
|
167
|
+
maxMessagesPerSecond: 100,
|
|
168
|
+
autoReconnect: true,
|
|
169
|
+
messageQueueSize: 50,
|
|
170
|
+
...options
|
|
171
|
+
};
|
|
63
172
|
this.Events = {};
|
|
64
173
|
this.peer = { type: 'IFRAME', connected: false };
|
|
65
174
|
if (options.type)
|
|
66
175
|
this.peer.type = options.type.toUpperCase();
|
|
67
176
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
177
|
+
cryptoCfg() {
|
|
178
|
+
if (!this.options.cryptoAuth)
|
|
179
|
+
return undefined;
|
|
180
|
+
return {
|
|
181
|
+
secret: this.options.cryptoAuth.secret,
|
|
182
|
+
requireSigned: !!this.options.cryptoAuth.requireSigned,
|
|
183
|
+
maxSkewMs: this.options.cryptoAuth.maxSkewMs ?? 2 * 60 * 1000,
|
|
184
|
+
replayWindowSize: this.options.cryptoAuth.replayWindowSize ?? 500,
|
|
185
|
+
enableSessionKeys: !!this.options.cryptoAuth.enableSessionKeys,
|
|
186
|
+
sessionKeyRotationInterval: this.options.cryptoAuth.sessionKeyRotationInterval ?? 3600000 // 1 hour
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Forget nonces that can no longer be replayed, and only then cap the map.
|
|
191
|
+
*
|
|
192
|
+
* Age is what actually decides replayability: a captured message is refused
|
|
193
|
+
* once its `ts` falls outside maxSkewMs, so a nonce is only worth keeping
|
|
194
|
+
* that long. Pruning purely by count — as this did — made the two defaults
|
|
195
|
+
* contradict each other: 500 remembered nonces at the default 100 messages a
|
|
196
|
+
* second is five seconds of history guarding a two-minute acceptance window,
|
|
197
|
+
* so anything captured could simply be replayed after five seconds.
|
|
198
|
+
*
|
|
199
|
+
* The count cap stays as a memory bound. Reaching it means the rate limiter
|
|
200
|
+
* is admitting more traffic than the window can remember, so it is reported
|
|
201
|
+
* rather than applied in silence.
|
|
202
|
+
*/
|
|
203
|
+
pruneNonces(maxSize) {
|
|
204
|
+
const cutoff = Date.now() - (this.cryptoCfg()?.maxSkewMs ?? 2 * 60 * 1000);
|
|
205
|
+
for (const [nonce, ts] of this.seenNonces)
|
|
206
|
+
if (ts < cutoff)
|
|
207
|
+
this.seenNonces.delete(nonce);
|
|
208
|
+
if (this.seenNonces.size <= maxSize)
|
|
209
|
+
return;
|
|
210
|
+
this.fire('error', {
|
|
211
|
+
type: 'REPLAY_WINDOW_EXCEEDED',
|
|
212
|
+
remembered: this.seenNonces.size,
|
|
213
|
+
maxSize
|
|
214
|
+
});
|
|
215
|
+
// Oldest first — Map iterates in insertion order, and nonces are inserted
|
|
216
|
+
// as they arrive.
|
|
217
|
+
const toRemove = this.seenNonces.size - maxSize;
|
|
218
|
+
let i = 0;
|
|
219
|
+
for (const key of this.seenNonces.keys()) {
|
|
220
|
+
this.seenNonces.delete(key);
|
|
221
|
+
if (++i >= toRemove)
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Get the appropriate secret for signing messages
|
|
227
|
+
* Uses session key if available, otherwise falls back to master secret
|
|
228
|
+
*/
|
|
229
|
+
getSigningSecret() {
|
|
230
|
+
const cfg = this.cryptoCfg();
|
|
231
|
+
if (!cfg)
|
|
232
|
+
return { secret: '' };
|
|
233
|
+
// Use session key if enabled and available
|
|
234
|
+
if (cfg.enableSessionKeys && this.currentSessionKey) {
|
|
235
|
+
return {
|
|
236
|
+
secret: this.currentSessionKey.key,
|
|
237
|
+
keyId: this.currentSessionKey.keyId
|
|
238
|
+
};
|
|
72
239
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
240
|
+
// Fall back to master secret
|
|
241
|
+
return { secret: cfg.secret };
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Get the appropriate secret for verifying incoming messages
|
|
245
|
+
* Tries current key, then pending, then previous, then master
|
|
246
|
+
*/
|
|
247
|
+
getVerificationSecrets() {
|
|
248
|
+
const cfg = this.cryptoCfg();
|
|
249
|
+
if (!cfg)
|
|
250
|
+
return [];
|
|
251
|
+
const secrets = [];
|
|
252
|
+
if (cfg.enableSessionKeys) {
|
|
253
|
+
// Try current session key first
|
|
254
|
+
if (this.currentSessionKey) {
|
|
255
|
+
secrets.push({
|
|
256
|
+
secret: this.currentSessionKey.key,
|
|
257
|
+
keyId: this.currentSessionKey.keyId
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
// Try pending key during rotation
|
|
261
|
+
if (this.pendingSessionKey) {
|
|
262
|
+
secrets.push({
|
|
263
|
+
secret: this.pendingSessionKey.key,
|
|
264
|
+
keyId: this.pendingSessionKey.keyId
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
// Try previous key for grace period
|
|
268
|
+
if (this.previousSessionKey) {
|
|
269
|
+
const now = Date.now();
|
|
270
|
+
if (now < this.previousSessionKey.expiresAt) {
|
|
271
|
+
secrets.push({
|
|
272
|
+
secret: this.previousSessionKey.key,
|
|
273
|
+
keyId: this.previousSessionKey.keyId
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Always try master secret as fallback
|
|
279
|
+
secrets.push({ secret: cfg.secret });
|
|
280
|
+
return secrets;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Application-level admission check for one incoming message.
|
|
284
|
+
*
|
|
285
|
+
* Reserved events bypass it deliberately: the handshake and the heartbeats
|
|
286
|
+
* must survive an allow-list that does not name them, or configuring one
|
|
287
|
+
* would silently sever the connection.
|
|
288
|
+
*
|
|
289
|
+
* This lives in a method because it used to be written out at each place a
|
|
290
|
+
* message can arrive — the authenticated and unauthenticated branches of
|
|
291
|
+
* `initiate()` and of `listen()` — and `listen()`'s unauthenticated branch
|
|
292
|
+
* never got a copy. An embedded bridge configured with an allow-list and no
|
|
293
|
+
* cryptoAuth, which is exactly how de.eui runs it, therefore accepted every
|
|
294
|
+
* event name a host cared to send.
|
|
295
|
+
*/
|
|
296
|
+
acceptIncoming(_event, payload, origin) {
|
|
297
|
+
if (RESERVED_EVENTS.includes(_event))
|
|
298
|
+
return true;
|
|
299
|
+
if (this.options.allowedIncomingEvents
|
|
300
|
+
&& !this.options.allowedIncomingEvents.includes(_event)) {
|
|
301
|
+
this.fire('error', {
|
|
302
|
+
type: 'DISALLOWED_EVENT',
|
|
303
|
+
direction: 'incoming',
|
|
304
|
+
event: _event,
|
|
305
|
+
origin
|
|
306
|
+
});
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
if (this.options.validateIncoming
|
|
310
|
+
&& !this.options.validateIncoming(_event, payload, origin)) {
|
|
311
|
+
this.fire('error', {
|
|
312
|
+
type: 'INVALID_MESSAGE',
|
|
313
|
+
direction: 'incoming',
|
|
314
|
+
event: _event,
|
|
315
|
+
origin
|
|
316
|
+
});
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Initialize session key exchange
|
|
323
|
+
* Called after connection is established if enableSessionKeys is true
|
|
324
|
+
*/
|
|
325
|
+
async initiateSessionKeyExchange() {
|
|
326
|
+
const cfg = this.cryptoCfg();
|
|
327
|
+
if (!cfg || !cfg.enableSessionKeys)
|
|
328
|
+
return;
|
|
329
|
+
this.debug(`[${this.peer.type}] Initiating session key exchange`);
|
|
330
|
+
// Generate my session ID
|
|
331
|
+
this.mySessionId = randomHex(32);
|
|
332
|
+
// Send session ID to peer
|
|
333
|
+
this.emit('__session_key_init', { sessionId: this.mySessionId });
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Handle incoming session key initialization
|
|
337
|
+
*/
|
|
338
|
+
async handleSessionKeyInit(peerSessionId) {
|
|
339
|
+
const cfg = this.cryptoCfg();
|
|
340
|
+
if (!cfg || !cfg.enableSessionKeys)
|
|
341
|
+
return;
|
|
342
|
+
this.debug(`[${this.peer.type}] Received session key init from peer`);
|
|
343
|
+
// Generate my session ID if not already done
|
|
344
|
+
if (!this.mySessionId) {
|
|
345
|
+
this.mySessionId = randomHex(32);
|
|
346
|
+
}
|
|
347
|
+
// Store peer session ID
|
|
348
|
+
this.peer.sessionId = peerSessionId;
|
|
349
|
+
// Derive session key
|
|
350
|
+
const keyId = `key-${Date.now()}-${randomHex(8)}`;
|
|
351
|
+
const sessionKey = await this.deriveAndStoreSessionKey(keyId);
|
|
352
|
+
// Send acknowledgment with my session ID
|
|
353
|
+
this.emit('__session_key_ack', {
|
|
354
|
+
sessionId: this.mySessionId,
|
|
355
|
+
keyId: keyId
|
|
356
|
+
});
|
|
357
|
+
this.debug(`[${this.peer.type}] Session key established: ${keyId}`);
|
|
358
|
+
// Start rotation timer
|
|
359
|
+
this.startSessionKeyRotation();
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Handle session key acknowledgment
|
|
363
|
+
*/
|
|
364
|
+
async handleSessionKeyAck(data) {
|
|
365
|
+
const cfg = this.cryptoCfg();
|
|
366
|
+
if (!cfg || !cfg.enableSessionKeys)
|
|
367
|
+
return;
|
|
368
|
+
this.debug(`[${this.peer.type}] Received session key ack from peer`);
|
|
369
|
+
// Store peer session ID
|
|
370
|
+
this.peer.sessionId = data.sessionId;
|
|
371
|
+
// Derive session key using the same keyId
|
|
372
|
+
await this.deriveAndStoreSessionKey(data.keyId);
|
|
373
|
+
this.debug(`[${this.peer.type}] Session key established: ${data.keyId}`);
|
|
374
|
+
// Start rotation timer
|
|
375
|
+
this.startSessionKeyRotation();
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Derive and store a session key
|
|
379
|
+
*/
|
|
380
|
+
async deriveAndStoreSessionKey(keyId) {
|
|
381
|
+
const cfg = this.cryptoCfg();
|
|
382
|
+
if (!cfg || !this.mySessionId || !this.peer.sessionId) {
|
|
383
|
+
throw new Error('Cannot derive session key: missing session IDs');
|
|
384
|
+
}
|
|
385
|
+
// Ensure consistent ordering of session IDs
|
|
386
|
+
const [id1, id2] = [this.mySessionId, this.peer.sessionId].sort();
|
|
387
|
+
const key = await deriveSessionKey(cfg.secret, id1, id2, keyId);
|
|
388
|
+
const now = Date.now();
|
|
389
|
+
const sessionKeyInfo = {
|
|
390
|
+
keyId,
|
|
391
|
+
key,
|
|
392
|
+
createdAt: now,
|
|
393
|
+
expiresAt: now + cfg.sessionKeyRotationInterval + 60000 // Grace period of 1 minute
|
|
394
|
+
};
|
|
395
|
+
// Rotate keys: current -> previous, new -> current
|
|
396
|
+
if (this.currentSessionKey) {
|
|
397
|
+
this.previousSessionKey = this.currentSessionKey;
|
|
398
|
+
}
|
|
399
|
+
this.currentSessionKey = sessionKeyInfo;
|
|
400
|
+
this.fire('session_key_established', { keyId });
|
|
401
|
+
return sessionKeyInfo;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Start session key rotation timer
|
|
405
|
+
*/
|
|
406
|
+
startSessionKeyRotation() {
|
|
407
|
+
const cfg = this.cryptoCfg();
|
|
408
|
+
if (!cfg || !cfg.enableSessionKeys)
|
|
409
|
+
return;
|
|
410
|
+
// Clear existing timer
|
|
411
|
+
if (this.sessionKeyRotationTimer) {
|
|
412
|
+
clearInterval(this.sessionKeyRotationTimer);
|
|
413
|
+
}
|
|
414
|
+
this.sessionKeyRotationTimer = setInterval(() => {
|
|
415
|
+
this.rotateSessionKey();
|
|
416
|
+
}, cfg.sessionKeyRotationInterval);
|
|
417
|
+
this.debug(`[${this.peer.type}] Session key rotation timer started (${cfg.sessionKeyRotationInterval}ms)`);
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Rotate session key
|
|
421
|
+
*/
|
|
422
|
+
async rotateSessionKey() {
|
|
423
|
+
const cfg = this.cryptoCfg();
|
|
424
|
+
if (!cfg || !cfg.enableSessionKeys || !this.isConnected())
|
|
425
|
+
return;
|
|
426
|
+
this.debug(`[${this.peer.type}] Rotating session key`);
|
|
427
|
+
const newKeyId = `key-${Date.now()}-${randomHex(8)}`;
|
|
428
|
+
// Derive new key
|
|
429
|
+
const newSessionKey = await this.deriveAndStoreSessionKey(newKeyId);
|
|
430
|
+
// Set as pending until peer acknowledges
|
|
431
|
+
this.pendingSessionKey = newSessionKey;
|
|
432
|
+
// Notify peer of rotation
|
|
433
|
+
this.emit('__session_key_rotate', { keyId: newKeyId });
|
|
434
|
+
this.fire('session_key_rotating', { keyId: newKeyId });
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Handle incoming session key rotation
|
|
438
|
+
*/
|
|
439
|
+
async handleSessionKeyRotate(data) {
|
|
440
|
+
const cfg = this.cryptoCfg();
|
|
441
|
+
if (!cfg || !cfg.enableSessionKeys)
|
|
442
|
+
return;
|
|
443
|
+
this.debug(`[${this.peer.type}] Peer rotating session key to: ${data.keyId}`);
|
|
444
|
+
// Derive the same key
|
|
445
|
+
await this.deriveAndStoreSessionKey(data.keyId);
|
|
446
|
+
this.fire('session_key_rotated', { keyId: data.keyId });
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Stop session key rotation timer
|
|
450
|
+
*/
|
|
451
|
+
stopSessionKeyRotation() {
|
|
452
|
+
if (this.sessionKeyRotationTimer) {
|
|
453
|
+
clearInterval(this.sessionKeyRotationTimer);
|
|
454
|
+
this.sessionKeyRotationTimer = undefined;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
async signOutgoing(messageData) {
|
|
458
|
+
const cfg = this.cryptoCfg();
|
|
459
|
+
if (!cfg)
|
|
460
|
+
return undefined;
|
|
461
|
+
const { secret, keyId } = this.getSigningSecret();
|
|
462
|
+
const ts = Date.now();
|
|
463
|
+
const nonce = randomHex(16);
|
|
464
|
+
const canonical = JSON.stringify({
|
|
465
|
+
v: messageData.v,
|
|
466
|
+
_event: messageData._event,
|
|
467
|
+
payload: messageData.payload,
|
|
468
|
+
cid: messageData.cid,
|
|
469
|
+
timestamp: messageData.timestamp,
|
|
470
|
+
size: messageData.size,
|
|
471
|
+
ts,
|
|
472
|
+
nonce
|
|
473
|
+
});
|
|
474
|
+
const sig = await hmacSha256Base64Url(secret, canonical);
|
|
475
|
+
return { alg: 'HMAC-SHA256', ts, nonce, sig, keyId };
|
|
476
|
+
}
|
|
477
|
+
async verifyIncomingAuth(data, origin) {
|
|
478
|
+
const cfg = this.cryptoCfg();
|
|
479
|
+
if (!cfg)
|
|
480
|
+
return true;
|
|
481
|
+
if (!data.auth) {
|
|
482
|
+
return !cfg.requireSigned;
|
|
483
|
+
}
|
|
484
|
+
const { alg, ts, nonce, sig, keyId } = data.auth;
|
|
485
|
+
if (alg !== 'HMAC-SHA256')
|
|
486
|
+
return false;
|
|
487
|
+
if (typeof ts !== 'number' || typeof nonce !== 'string' || typeof sig !== 'string')
|
|
488
|
+
return false;
|
|
489
|
+
const now = Date.now();
|
|
490
|
+
if (Math.abs(now - ts) > cfg.maxSkewMs)
|
|
491
|
+
return false;
|
|
492
|
+
// Replay protection. The nonce is only recorded once the signature has been
|
|
493
|
+
// checked, further down: burning it here let an unsigned or badly signed
|
|
494
|
+
// message consume the nonce of a legitimate one still in flight.
|
|
495
|
+
if (this.seenNonces.has(nonce))
|
|
496
|
+
return false;
|
|
497
|
+
const canonical = JSON.stringify({
|
|
498
|
+
v: data.v,
|
|
499
|
+
_event: data._event,
|
|
500
|
+
payload: data.payload,
|
|
501
|
+
cid: data.cid,
|
|
502
|
+
timestamp: data.timestamp,
|
|
503
|
+
size: data.size,
|
|
504
|
+
ts,
|
|
505
|
+
nonce
|
|
506
|
+
});
|
|
507
|
+
// Try all available secrets
|
|
508
|
+
const secrets = this.getVerificationSecrets();
|
|
509
|
+
for (const { secret, keyId: secretKeyId } of secrets) {
|
|
510
|
+
// If message has keyId, only try matching secret
|
|
511
|
+
if (keyId && secretKeyId && keyId !== secretKeyId)
|
|
512
|
+
continue;
|
|
513
|
+
try {
|
|
514
|
+
const expected = await hmacSha256Base64Url(secret, canonical);
|
|
515
|
+
if (constantTimeEqual(expected, sig)) {
|
|
516
|
+
this.seenNonces.set(nonce, ts);
|
|
517
|
+
this.pruneNonces(cfg.replayWindowSize);
|
|
518
|
+
this.debug(`[${this.peer.type}] Auth verified${keyId ? ` with key: ${keyId}` : ''}`);
|
|
519
|
+
return true;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
this.debug(`[${this.peer.type}] Auth verification error:`, error);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
debug(...args) {
|
|
529
|
+
this.options.debug && console.debug(...args);
|
|
530
|
+
}
|
|
531
|
+
isConnected() {
|
|
76
532
|
return !!this.peer.connected && !!this.peer.source;
|
|
77
|
-
}
|
|
533
|
+
}
|
|
78
534
|
// Enhanced connection health monitoring
|
|
79
|
-
|
|
80
|
-
var _this = this;
|
|
535
|
+
startHeartbeat() {
|
|
81
536
|
if (!this.options.heartbeatInterval)
|
|
82
537
|
return;
|
|
83
|
-
this.heartbeatTimer = setInterval(
|
|
84
|
-
if (
|
|
85
|
-
|
|
538
|
+
this.heartbeatTimer = setInterval(() => {
|
|
539
|
+
if (this.isConnected()) {
|
|
540
|
+
const now = Date.now();
|
|
86
541
|
// Check if peer is still responsive
|
|
87
|
-
if (
|
|
88
|
-
&& (now -
|
|
89
|
-
|
|
90
|
-
|
|
542
|
+
if (this.peer.lastHeartbeat
|
|
543
|
+
&& (now - this.peer.lastHeartbeat) > (this.options.heartbeatInterval * 2)) {
|
|
544
|
+
this.debug(`[${this.peer.type}] Heartbeat timeout detected`);
|
|
545
|
+
this.handleConnectionLoss();
|
|
91
546
|
return;
|
|
92
547
|
}
|
|
93
548
|
// Send heartbeat
|
|
94
549
|
try {
|
|
95
|
-
|
|
550
|
+
this.emit('__heartbeat', { timestamp: now });
|
|
96
551
|
}
|
|
97
552
|
catch (error) {
|
|
98
|
-
|
|
99
|
-
|
|
553
|
+
this.debug(`[${this.peer.type}] Heartbeat send failed:`, error);
|
|
554
|
+
this.handleConnectionLoss();
|
|
100
555
|
}
|
|
101
556
|
}
|
|
102
557
|
}, this.options.heartbeatInterval);
|
|
103
|
-
}
|
|
104
|
-
|
|
558
|
+
}
|
|
559
|
+
stopHeartbeat() {
|
|
105
560
|
if (!this.heartbeatTimer)
|
|
106
561
|
return;
|
|
107
562
|
clearInterval(this.heartbeatTimer);
|
|
108
563
|
this.heartbeatTimer = undefined;
|
|
109
|
-
}
|
|
564
|
+
}
|
|
110
565
|
// Handle connection loss and potential reconnection
|
|
111
|
-
|
|
566
|
+
handleConnectionLoss() {
|
|
112
567
|
if (!this.peer.connected)
|
|
113
568
|
return;
|
|
114
569
|
this.peer.connected = false;
|
|
115
570
|
this.stopHeartbeat();
|
|
571
|
+
this.stopSessionKeyRotation();
|
|
116
572
|
this.fire('disconnect', { reason: 'CONNECTION_LOST' });
|
|
117
573
|
this.options.autoReconnect
|
|
118
574
|
&& this.reconnectAttempts < this.maxReconnectAttempts
|
|
119
575
|
&& this.attemptReconnection();
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
var _this = this;
|
|
576
|
+
}
|
|
577
|
+
attemptReconnection() {
|
|
123
578
|
if (this.reconnectTimer)
|
|
124
579
|
return;
|
|
125
580
|
this.reconnectAttempts++;
|
|
126
|
-
|
|
127
|
-
this.debug(
|
|
128
|
-
this.fire('reconnecting', { attempt: this.reconnectAttempts, delay
|
|
129
|
-
this.reconnectTimer = setTimeout(
|
|
130
|
-
|
|
581
|
+
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts - 1), 30000); // Exponential backoff, max 30s
|
|
582
|
+
this.debug(`[${this.peer.type}] Attempting reconnection ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`);
|
|
583
|
+
this.fire('reconnecting', { attempt: this.reconnectAttempts, delay });
|
|
584
|
+
this.reconnectTimer = setTimeout(() => {
|
|
585
|
+
this.reconnectTimer = undefined;
|
|
131
586
|
// Re-initiate connection for WINDOW type
|
|
132
|
-
|
|
133
|
-
&&
|
|
134
|
-
&&
|
|
135
|
-
&&
|
|
587
|
+
this.peer.type === 'WINDOW'
|
|
588
|
+
&& this.peer.source
|
|
589
|
+
&& this.peer.origin
|
|
590
|
+
&& this.emit('ping');
|
|
136
591
|
// For IFRAME type, just wait for incoming connection
|
|
137
592
|
// Set timeout for this reconnection attempt
|
|
138
|
-
setTimeout(
|
|
139
|
-
if (
|
|
593
|
+
setTimeout(() => {
|
|
594
|
+
if (this.peer.connected)
|
|
140
595
|
return;
|
|
141
|
-
|
|
142
|
-
?
|
|
143
|
-
:
|
|
144
|
-
},
|
|
596
|
+
this.reconnectAttempts < this.maxReconnectAttempts
|
|
597
|
+
? this.attemptReconnection()
|
|
598
|
+
: this.fire('reconnection_failed', { attempts: this.reconnectAttempts });
|
|
599
|
+
}, this.options.connectionTimeout);
|
|
145
600
|
}, delay);
|
|
146
|
-
}
|
|
601
|
+
}
|
|
147
602
|
// Message rate limiting
|
|
148
|
-
|
|
603
|
+
checkRateLimit() {
|
|
149
604
|
if (!this.options.maxMessagesPerSecond)
|
|
150
605
|
return true;
|
|
151
|
-
|
|
606
|
+
const now = Date.now(), aSecondAgo = now - 1000;
|
|
152
607
|
// Clean old entries
|
|
153
|
-
this.messageRateTracker = this.messageRateTracker.filter(
|
|
608
|
+
this.messageRateTracker = this.messageRateTracker.filter(timestamp => timestamp > aSecondAgo);
|
|
154
609
|
// Check if limit exceeded
|
|
155
610
|
if (this.messageRateTracker.length >= this.options.maxMessagesPerSecond) {
|
|
156
611
|
this.fire('error', {
|
|
@@ -162,45 +617,43 @@ var IOF = /** @class */ (function () {
|
|
|
162
617
|
}
|
|
163
618
|
this.messageRateTracker.push(now);
|
|
164
619
|
return true;
|
|
165
|
-
}
|
|
620
|
+
}
|
|
166
621
|
// Queue messages when not connected
|
|
167
|
-
|
|
622
|
+
queueMessage(_event, payload, fn) {
|
|
168
623
|
if (this.messageQueue.length >= this.options.messageQueueSize) {
|
|
169
624
|
// Remove oldest message
|
|
170
|
-
|
|
171
|
-
this.debug(
|
|
625
|
+
const removed = this.messageQueue.shift();
|
|
626
|
+
this.debug(`[${this.peer.type}] Message queue full, removed oldest message:`, removed?._event);
|
|
172
627
|
}
|
|
173
628
|
this.messageQueue.push({
|
|
174
|
-
_event
|
|
175
|
-
payload
|
|
176
|
-
fn
|
|
629
|
+
_event,
|
|
630
|
+
payload,
|
|
631
|
+
fn,
|
|
177
632
|
timestamp: Date.now()
|
|
178
633
|
});
|
|
179
|
-
this.debug(
|
|
180
|
-
}
|
|
634
|
+
this.debug(`[${this.peer.type}] Queued message: ${_event} (queue size: ${this.messageQueue.length})`);
|
|
635
|
+
}
|
|
181
636
|
// Process queued messages when connection is established
|
|
182
|
-
|
|
183
|
-
var _this = this;
|
|
637
|
+
processMessageQueue() {
|
|
184
638
|
if (!this.isConnected() || this.messageQueue.length === 0)
|
|
185
639
|
return;
|
|
186
|
-
this.debug(
|
|
187
|
-
|
|
640
|
+
this.debug(`[${this.peer.type}] Processing ${this.messageQueue.length} queued messages`);
|
|
641
|
+
const queue = [...this.messageQueue];
|
|
188
642
|
this.messageQueue = [];
|
|
189
|
-
queue.forEach(
|
|
643
|
+
queue.forEach(message => {
|
|
190
644
|
try {
|
|
191
|
-
|
|
645
|
+
this.emit(message._event, message.payload, message.fn);
|
|
192
646
|
}
|
|
193
647
|
catch (error) {
|
|
194
|
-
|
|
648
|
+
this.debug(`[${this.peer.type}] Failed to send queued message:`, error);
|
|
195
649
|
}
|
|
196
650
|
});
|
|
197
|
-
}
|
|
651
|
+
}
|
|
198
652
|
/**
|
|
199
653
|
* Establish a connection with an iframe containing
|
|
200
654
|
* in the current window
|
|
201
655
|
*/
|
|
202
|
-
|
|
203
|
-
var _this = this;
|
|
656
|
+
initiate(contentWindow, iframeOrigin) {
|
|
204
657
|
if (!contentWindow || !iframeOrigin)
|
|
205
658
|
throw new Error('Invalid Connection initiation arguments');
|
|
206
659
|
if (this.peer.type === 'IFRAME')
|
|
@@ -211,74 +664,130 @@ var IOF = /** @class */ (function () {
|
|
|
211
664
|
this.peer.origin = iframeOrigin;
|
|
212
665
|
this.peer.connected = false;
|
|
213
666
|
this.reconnectAttempts = 0;
|
|
214
|
-
this.messageListener =
|
|
215
|
-
var origin = _a.origin, data = _a.data, source = _a.source;
|
|
667
|
+
this.messageListener = ({ origin, data, source }) => {
|
|
216
668
|
try {
|
|
217
669
|
// Enhanced security: check valid message structure
|
|
218
|
-
if (origin !==
|
|
670
|
+
if (origin !== this.peer.origin
|
|
219
671
|
|| !source
|
|
220
672
|
|| typeof data !== 'object'
|
|
221
673
|
|| !data.hasOwnProperty('_event'))
|
|
222
674
|
return;
|
|
223
|
-
|
|
675
|
+
const { v, _event, payload, cid, timestamp, sessionId } = data;
|
|
676
|
+
// Protocol version check
|
|
677
|
+
const messageVersion = v || 1;
|
|
678
|
+
if (messageVersion > PROTOCOL_VERSION) {
|
|
679
|
+
this.fire('error', {
|
|
680
|
+
type: 'UNSUPPORTED_VERSION',
|
|
681
|
+
received: messageVersion,
|
|
682
|
+
supported: PROTOCOL_VERSION
|
|
683
|
+
});
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
// Store peer protocol version
|
|
687
|
+
if (!this.peer.protocolVersion || this.peer.protocolVersion < messageVersion) {
|
|
688
|
+
this.peer.protocolVersion = messageVersion;
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Session key control events.
|
|
692
|
+
*
|
|
693
|
+
* These are answered before authentication — they are what establishes
|
|
694
|
+
* the key authentication will use — so they are gated on the feature
|
|
695
|
+
* actually being switched on. Without that, a peer that never enabled
|
|
696
|
+
* session keys would still derive and rotate them on request, and a
|
|
697
|
+
* malformed payload would throw out of the handler.
|
|
698
|
+
*/
|
|
699
|
+
if (RESERVED_SESSION_KEY_EVENTS.includes(_event)) {
|
|
700
|
+
if (!this.cryptoCfg()?.enableSessionKeys) {
|
|
701
|
+
this.fire('error', { type: 'SESSION_KEYS_DISABLED', event: _event, origin });
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
if (!payload || typeof payload !== 'object') {
|
|
705
|
+
this.fire('error', { type: 'MALFORMED_SESSION_KEY_EVENT', event: _event, origin });
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
if (_event === '__session_key_init')
|
|
709
|
+
this.handleSessionKeyInit(payload.sessionId);
|
|
710
|
+
else if (_event === '__session_key_ack')
|
|
711
|
+
this.handleSessionKeyAck(payload);
|
|
712
|
+
else
|
|
713
|
+
this.handleSessionKeyRotate(payload);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
224
716
|
// Handle heartbeat responses
|
|
225
717
|
if (_event === '__heartbeat_response') {
|
|
226
|
-
|
|
718
|
+
this.peer.lastHeartbeat = Date.now();
|
|
227
719
|
return;
|
|
228
720
|
}
|
|
229
721
|
// Handle heartbeat requests
|
|
230
722
|
if (_event === '__heartbeat') {
|
|
231
|
-
|
|
232
|
-
|
|
723
|
+
this.emit('__heartbeat_response', { timestamp: Date.now() });
|
|
724
|
+
this.peer.lastHeartbeat = Date.now();
|
|
233
725
|
return;
|
|
234
726
|
}
|
|
235
|
-
|
|
727
|
+
this.debug(`[${this.peer.type}] Message v${messageVersion}: ${_event}`, payload || '');
|
|
236
728
|
// Handshake or availability check events
|
|
237
729
|
if (_event == 'pong') {
|
|
238
730
|
// Content Window is connected to iframe
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
731
|
+
this.peer.connected = true;
|
|
732
|
+
this.reconnectAttempts = 0;
|
|
733
|
+
this.peer.lastHeartbeat = Date.now();
|
|
734
|
+
this.startHeartbeat();
|
|
735
|
+
this.fire('connect');
|
|
736
|
+
// Initiate session key exchange if enabled
|
|
737
|
+
this.initiateSessionKeyExchange();
|
|
738
|
+
this.processMessageQueue();
|
|
739
|
+
this.debug(`[${this.peer.type}] connected`);
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
// Cryptographic authentication (optional)
|
|
743
|
+
if (this.options.cryptoAuth) {
|
|
744
|
+
this.verifyIncomingAuth(data, origin)
|
|
745
|
+
.then(ok => {
|
|
746
|
+
if (!ok) {
|
|
747
|
+
this.fire('error', { type: 'AUTH_FAILED', origin, event: _event });
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
if (!this.acceptIncoming(_event, payload, origin))
|
|
751
|
+
return;
|
|
752
|
+
this.fire(_event, payload, cid);
|
|
753
|
+
})
|
|
754
|
+
.catch(error => this.fire('error', { type: 'AUTH_ERROR', origin, event: _event, error: String(error) }));
|
|
246
755
|
return;
|
|
247
756
|
}
|
|
757
|
+
if (!this.acceptIncoming(_event, payload, origin))
|
|
758
|
+
return;
|
|
248
759
|
// Fire available event listeners
|
|
249
|
-
|
|
760
|
+
this.fire(_event, payload, cid);
|
|
250
761
|
}
|
|
251
762
|
catch (error) {
|
|
252
|
-
|
|
253
|
-
|
|
763
|
+
this.debug(`[${this.peer.type}] Message handling error:`, error);
|
|
764
|
+
this.fire('error', {
|
|
254
765
|
type: 'MESSAGE_HANDLING_ERROR',
|
|
255
766
|
error: error instanceof Error ? error.message : String(error),
|
|
256
|
-
origin
|
|
767
|
+
origin
|
|
257
768
|
});
|
|
258
769
|
}
|
|
259
770
|
};
|
|
260
771
|
window.addEventListener('message', this.messageListener, false);
|
|
261
|
-
this.debug(
|
|
772
|
+
this.debug(`[${this.peer.type}] Initiate connection: IFrame origin <${iframeOrigin}>`);
|
|
262
773
|
this.emit('ping');
|
|
263
774
|
return this;
|
|
264
|
-
}
|
|
775
|
+
}
|
|
265
776
|
/**
|
|
266
777
|
* Listening to connection from the content window
|
|
267
778
|
*/
|
|
268
|
-
|
|
269
|
-
var _this = this;
|
|
779
|
+
listen(hostOrigin) {
|
|
270
780
|
this.peer.type = 'IFRAME'; // iframe.io connection listener is automatically set as IFRAME
|
|
271
781
|
this.peer.connected = false;
|
|
272
782
|
this.reconnectAttempts = 0;
|
|
273
|
-
this.debug(
|
|
783
|
+
this.debug(`[${this.peer.type}] Listening to connect${hostOrigin ? `: Host <${hostOrigin}>` : ''}`);
|
|
274
784
|
// Clean up existing listener if any
|
|
275
785
|
this.cleanup();
|
|
276
|
-
this.messageListener =
|
|
277
|
-
var origin = _a.origin, data = _a.data, source = _a.source;
|
|
786
|
+
this.messageListener = ({ origin, data, source }) => {
|
|
278
787
|
try {
|
|
279
788
|
// Enhanced security: check host origin where event must only come from
|
|
280
789
|
if (hostOrigin && hostOrigin !== origin) {
|
|
281
|
-
|
|
790
|
+
this.fire('error', {
|
|
282
791
|
type: 'INVALID_ORIGIN',
|
|
283
792
|
expected: hostOrigin,
|
|
284
793
|
received: origin
|
|
@@ -291,78 +800,132 @@ var IOF = /** @class */ (function () {
|
|
|
291
800
|
|| !data.hasOwnProperty('_event'))
|
|
292
801
|
return;
|
|
293
802
|
// Define peer source window and origin
|
|
294
|
-
if (!
|
|
295
|
-
|
|
296
|
-
|
|
803
|
+
if (!this.peer.source) {
|
|
804
|
+
this.peer = { ...this.peer, source: source, origin };
|
|
805
|
+
this.debug(`[${this.peer.type}] Connect to ${origin}`);
|
|
297
806
|
}
|
|
298
807
|
// Origin different from handshaked source origin
|
|
299
|
-
else if (origin !==
|
|
300
|
-
|
|
808
|
+
else if (origin !== this.peer.origin) {
|
|
809
|
+
this.fire('error', {
|
|
301
810
|
type: 'ORIGIN_MISMATCH',
|
|
302
|
-
expected:
|
|
811
|
+
expected: this.peer.origin,
|
|
303
812
|
received: origin
|
|
304
813
|
});
|
|
305
814
|
return;
|
|
306
815
|
}
|
|
307
|
-
|
|
816
|
+
const { v, _event, payload, cid, timestamp } = data;
|
|
817
|
+
// Protocol version check
|
|
818
|
+
const messageVersion = v || 1;
|
|
819
|
+
if (messageVersion > PROTOCOL_VERSION) {
|
|
820
|
+
this.fire('error', {
|
|
821
|
+
type: 'UNSUPPORTED_VERSION',
|
|
822
|
+
received: messageVersion,
|
|
823
|
+
supported: PROTOCOL_VERSION
|
|
824
|
+
});
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
// Store peer protocol version
|
|
828
|
+
if (!this.peer.protocolVersion || this.peer.protocolVersion < messageVersion) {
|
|
829
|
+
this.peer.protocolVersion = messageVersion;
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Session key control events.
|
|
833
|
+
*
|
|
834
|
+
* These are answered before authentication — they are what establishes
|
|
835
|
+
* the key authentication will use — so they are gated on the feature
|
|
836
|
+
* actually being switched on. Without that, a peer that never enabled
|
|
837
|
+
* session keys would still derive and rotate them on request, and a
|
|
838
|
+
* malformed payload would throw out of the handler.
|
|
839
|
+
*/
|
|
840
|
+
if (RESERVED_SESSION_KEY_EVENTS.includes(_event)) {
|
|
841
|
+
if (!this.cryptoCfg()?.enableSessionKeys) {
|
|
842
|
+
this.fire('error', { type: 'SESSION_KEYS_DISABLED', event: _event, origin });
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
if (!payload || typeof payload !== 'object') {
|
|
846
|
+
this.fire('error', { type: 'MALFORMED_SESSION_KEY_EVENT', event: _event, origin });
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
if (_event === '__session_key_init')
|
|
850
|
+
this.handleSessionKeyInit(payload.sessionId);
|
|
851
|
+
else if (_event === '__session_key_ack')
|
|
852
|
+
this.handleSessionKeyAck(payload);
|
|
853
|
+
else
|
|
854
|
+
this.handleSessionKeyRotate(payload);
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
308
857
|
// Handle heartbeat responses
|
|
309
858
|
if (_event === '__heartbeat_response') {
|
|
310
|
-
|
|
859
|
+
this.peer.lastHeartbeat = Date.now();
|
|
311
860
|
return;
|
|
312
861
|
}
|
|
313
862
|
// Handle heartbeat requests
|
|
314
863
|
if (_event === '__heartbeat') {
|
|
315
|
-
|
|
316
|
-
|
|
864
|
+
this.emit('__heartbeat_response', { timestamp: Date.now() });
|
|
865
|
+
this.peer.lastHeartbeat = Date.now();
|
|
317
866
|
return;
|
|
318
867
|
}
|
|
319
|
-
|
|
868
|
+
this.debug(`[${this.peer.type}] Message v${messageVersion}: ${_event}`, payload || '');
|
|
320
869
|
// Handshake or availability check events
|
|
321
870
|
if (_event == 'ping') {
|
|
322
|
-
|
|
871
|
+
this.emit('pong');
|
|
323
872
|
// Iframe is connected to content window
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
873
|
+
this.peer.connected = true;
|
|
874
|
+
this.reconnectAttempts = 0;
|
|
875
|
+
this.peer.lastHeartbeat = Date.now();
|
|
876
|
+
this.startHeartbeat();
|
|
877
|
+
this.fire('connect');
|
|
878
|
+
// Initiate session key exchange if enabled
|
|
879
|
+
this.initiateSessionKeyExchange();
|
|
880
|
+
this.processMessageQueue();
|
|
881
|
+
this.debug(`[${this.peer.type}] connected`);
|
|
331
882
|
return;
|
|
332
883
|
}
|
|
884
|
+
// Cryptographic authentication (optional)
|
|
885
|
+
if (this.options.cryptoAuth) {
|
|
886
|
+
this.verifyIncomingAuth(data, origin)
|
|
887
|
+
.then(ok => {
|
|
888
|
+
if (!ok) {
|
|
889
|
+
this.fire('error', { type: 'AUTH_FAILED', origin, event: _event });
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
if (!this.acceptIncoming(_event, payload, origin))
|
|
893
|
+
return;
|
|
894
|
+
this.fire(_event, payload, cid);
|
|
895
|
+
})
|
|
896
|
+
.catch(error => this.fire('error', { type: 'AUTH_ERROR', origin, event: _event, error: String(error) }));
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
if (!this.acceptIncoming(_event, payload, origin))
|
|
900
|
+
return;
|
|
333
901
|
// Fire available event listeners
|
|
334
|
-
|
|
902
|
+
this.fire(_event, payload, cid);
|
|
335
903
|
}
|
|
336
904
|
catch (error) {
|
|
337
|
-
|
|
338
|
-
|
|
905
|
+
this.debug(`[${this.peer.type}] Message handling error:`, error);
|
|
906
|
+
this.fire('error', {
|
|
339
907
|
type: 'MESSAGE_HANDLING_ERROR',
|
|
340
908
|
error: error instanceof Error ? error.message : String(error),
|
|
341
|
-
origin
|
|
909
|
+
origin
|
|
342
910
|
});
|
|
343
911
|
}
|
|
344
912
|
};
|
|
345
913
|
window.addEventListener('message', this.messageListener, false);
|
|
346
914
|
return this;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
var _this = this;
|
|
915
|
+
}
|
|
916
|
+
fire(_event, payload, cid) {
|
|
350
917
|
// Volatile event - check if any listeners exist
|
|
351
918
|
if (!this.Events[_event] && !this.Events[_event + '--@once']) {
|
|
352
|
-
this.debug(
|
|
919
|
+
this.debug(`[${this.peer.type}] No <${_event}> listener defined`);
|
|
353
920
|
return;
|
|
354
921
|
}
|
|
355
|
-
|
|
356
|
-
?
|
|
357
|
-
|
|
358
|
-
for (var _i = 1; _i < arguments.length; _i++) {
|
|
359
|
-
args[_i - 1] = arguments[_i];
|
|
360
|
-
}
|
|
361
|
-
_this.emit("".concat(_event, "--").concat(cid, "--@ack"), { error: error || false, args: args });
|
|
922
|
+
const ackFn = cid
|
|
923
|
+
? (error, ...args) => {
|
|
924
|
+
this.emit(`${_event}--${cid}--@ack`, { error: error || false, args });
|
|
362
925
|
return;
|
|
363
926
|
}
|
|
364
927
|
: undefined;
|
|
365
|
-
|
|
928
|
+
let listeners = [];
|
|
366
929
|
if (this.Events[_event + '--@once']) {
|
|
367
930
|
// Once triggable event
|
|
368
931
|
_event += '--@once';
|
|
@@ -373,21 +936,21 @@ var IOF = /** @class */ (function () {
|
|
|
373
936
|
else
|
|
374
937
|
listeners = this.Events[_event];
|
|
375
938
|
// Fire listeners with error handling
|
|
376
|
-
listeners.forEach(
|
|
939
|
+
listeners.forEach(fn => {
|
|
377
940
|
try {
|
|
378
941
|
payload !== undefined ? fn(payload, ackFn) : fn(ackFn);
|
|
379
942
|
}
|
|
380
943
|
catch (error) {
|
|
381
|
-
|
|
382
|
-
|
|
944
|
+
this.debug(`[${this.peer.type}] Listener error for ${_event}:`, error);
|
|
945
|
+
this.fire('error', {
|
|
383
946
|
type: 'LISTENER_ERROR',
|
|
384
947
|
event: _event,
|
|
385
948
|
error: error instanceof Error ? error.message : String(error)
|
|
386
949
|
});
|
|
387
950
|
}
|
|
388
951
|
});
|
|
389
|
-
}
|
|
390
|
-
|
|
952
|
+
}
|
|
953
|
+
emit(_event, payload, fn) {
|
|
391
954
|
// Check rate limiting
|
|
392
955
|
if (!this.checkRateLimit())
|
|
393
956
|
return this;
|
|
@@ -409,30 +972,28 @@ var IOF = /** @class */ (function () {
|
|
|
409
972
|
}
|
|
410
973
|
try {
|
|
411
974
|
// Enhanced security: sanitize and validate payload
|
|
412
|
-
|
|
975
|
+
const sanitizedPayload = payload
|
|
413
976
|
? sanitizePayload(payload, this.options.maxMessageSize)
|
|
414
977
|
: payload;
|
|
415
978
|
// Acknowledge event listener
|
|
416
|
-
|
|
979
|
+
let cid;
|
|
417
980
|
if (typeof fn === 'function') {
|
|
418
|
-
|
|
981
|
+
const ackFunction = fn;
|
|
419
982
|
cid = ackId();
|
|
420
|
-
this.once(
|
|
421
|
-
var error = _a.error, args = _a.args;
|
|
422
|
-
return ackFunction_1.apply(void 0, __spreadArray([error], args, false));
|
|
423
|
-
});
|
|
983
|
+
this.once(`${_event}--${cid}--@ack`, ({ error, args }) => ackFunction(error, ...args));
|
|
424
984
|
}
|
|
425
|
-
|
|
426
|
-
|
|
985
|
+
const messageData = {
|
|
986
|
+
v: PROTOCOL_VERSION,
|
|
987
|
+
_event,
|
|
427
988
|
payload: sanitizedPayload,
|
|
428
|
-
cid
|
|
989
|
+
cid,
|
|
429
990
|
timestamp: Date.now(),
|
|
430
991
|
size: getMessageSize(sanitizedPayload)
|
|
431
992
|
};
|
|
432
993
|
this.peer.source.postMessage(newObject(messageData), this.peer.origin);
|
|
433
994
|
}
|
|
434
995
|
catch (error) {
|
|
435
|
-
this.debug(
|
|
996
|
+
this.debug(`[${this.peer.type}] Emit error:`, error);
|
|
436
997
|
this.fire('error', {
|
|
437
998
|
type: 'EMIT_ERROR',
|
|
438
999
|
event: _event,
|
|
@@ -443,29 +1004,102 @@ var IOF = /** @class */ (function () {
|
|
|
443
1004
|
&& fn(error instanceof Error ? error.message : String(error));
|
|
444
1005
|
}
|
|
445
1006
|
return this;
|
|
446
|
-
}
|
|
447
|
-
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Send a signed message (HMAC-SHA256) when `options.cryptoAuth` is configured.
|
|
1010
|
+
* This is async because WebCrypto signing is async.
|
|
1011
|
+
*/
|
|
1012
|
+
async emitSigned(_event, payload, fn) {
|
|
1013
|
+
// Check rate limiting
|
|
1014
|
+
if (!this.checkRateLimit())
|
|
1015
|
+
return this;
|
|
1016
|
+
if (!this.options.cryptoAuth) {
|
|
1017
|
+
// If auth not enabled, fall back to normal emit behavior
|
|
1018
|
+
this.emit(_event, payload, fn);
|
|
1019
|
+
return this;
|
|
1020
|
+
}
|
|
1021
|
+
if (!this.isConnected() && !RESERVED_EVENTS.includes(_event)) {
|
|
1022
|
+
this.queueMessage(_event, payload, fn);
|
|
1023
|
+
return this;
|
|
1024
|
+
}
|
|
1025
|
+
if (!this.peer.source) {
|
|
1026
|
+
this.fire('error', { type: 'NO_CONNECTION', event: _event });
|
|
1027
|
+
return this;
|
|
1028
|
+
}
|
|
1029
|
+
if (typeof payload == 'function') {
|
|
1030
|
+
fn = payload;
|
|
1031
|
+
payload = undefined;
|
|
1032
|
+
}
|
|
1033
|
+
try {
|
|
1034
|
+
const sanitizedPayload = payload
|
|
1035
|
+
? sanitizePayload(payload, this.options.maxMessageSize)
|
|
1036
|
+
: payload;
|
|
1037
|
+
let cid;
|
|
1038
|
+
if (typeof fn === 'function') {
|
|
1039
|
+
const ackFunction = fn;
|
|
1040
|
+
cid = ackId();
|
|
1041
|
+
this.once(`${_event}--${cid}--@ack`, ({ error, args }) => ackFunction(error, ...args));
|
|
1042
|
+
}
|
|
1043
|
+
const unsigned = {
|
|
1044
|
+
v: PROTOCOL_VERSION,
|
|
1045
|
+
_event,
|
|
1046
|
+
payload: sanitizedPayload,
|
|
1047
|
+
cid,
|
|
1048
|
+
timestamp: Date.now(),
|
|
1049
|
+
size: getMessageSize(sanitizedPayload)
|
|
1050
|
+
};
|
|
1051
|
+
const auth = await this.signOutgoing(unsigned);
|
|
1052
|
+
const messageData = { ...unsigned, auth };
|
|
1053
|
+
this.peer.source.postMessage(newObject(messageData), this.peer.origin);
|
|
1054
|
+
}
|
|
1055
|
+
catch (error) {
|
|
1056
|
+
this.debug(`[${this.peer.type}] EmitSigned error:`, error);
|
|
1057
|
+
this.fire('error', {
|
|
1058
|
+
type: 'EMIT_ERROR',
|
|
1059
|
+
event: _event,
|
|
1060
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1061
|
+
});
|
|
1062
|
+
typeof fn === 'function'
|
|
1063
|
+
&& fn(error instanceof Error ? error.message : String(error));
|
|
1064
|
+
}
|
|
1065
|
+
return this;
|
|
1066
|
+
}
|
|
1067
|
+
async emitAsyncSigned(_event, payload, timeout = 5000) {
|
|
1068
|
+
return new Promise((resolve, reject) => {
|
|
1069
|
+
const timeoutId = setTimeout(() => reject(new Error(`Event '${_event}' acknowledgment timeout after ${timeout}ms`)), timeout);
|
|
1070
|
+
this.emitSigned(_event, payload, (error, ...args) => {
|
|
1071
|
+
clearTimeout(timeoutId);
|
|
1072
|
+
error
|
|
1073
|
+
? reject(new Error(typeof error === 'string' ? error : 'Ack error'))
|
|
1074
|
+
: resolve(args.length === 0 ? undefined : args.length === 1 ? args[0] : args);
|
|
1075
|
+
}).catch(err => {
|
|
1076
|
+
clearTimeout(timeoutId);
|
|
1077
|
+
reject(err);
|
|
1078
|
+
});
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
on(_event, fn) {
|
|
448
1082
|
// Add Event listener
|
|
449
1083
|
if (!this.Events[_event])
|
|
450
1084
|
this.Events[_event] = [];
|
|
451
1085
|
this.Events[_event].push(fn);
|
|
452
|
-
this.debug(
|
|
1086
|
+
this.debug(`[${this.peer.type}] New <${_event}> listener on`);
|
|
453
1087
|
return this;
|
|
454
|
-
}
|
|
455
|
-
|
|
1088
|
+
}
|
|
1089
|
+
once(_event, fn) {
|
|
456
1090
|
// Add Once Event listener
|
|
457
1091
|
_event += '--@once';
|
|
458
1092
|
if (!this.Events[_event])
|
|
459
1093
|
this.Events[_event] = [];
|
|
460
1094
|
this.Events[_event].push(fn);
|
|
461
|
-
this.debug(
|
|
1095
|
+
this.debug(`[${this.peer.type}] New <${_event} once> listener on`);
|
|
462
1096
|
return this;
|
|
463
|
-
}
|
|
464
|
-
|
|
1097
|
+
}
|
|
1098
|
+
off(_event, fn) {
|
|
465
1099
|
// Remove Event listener
|
|
466
1100
|
if (fn && this.Events[_event]) {
|
|
467
1101
|
// Remove specific listener if provided
|
|
468
|
-
|
|
1102
|
+
const index = this.Events[_event].indexOf(fn);
|
|
469
1103
|
if (index > -1) {
|
|
470
1104
|
this.Events[_event].splice(index, 1);
|
|
471
1105
|
// Remove event array if empty
|
|
@@ -477,29 +1111,23 @@ var IOF = /** @class */ (function () {
|
|
|
477
1111
|
else
|
|
478
1112
|
delete this.Events[_event];
|
|
479
1113
|
typeof fn == 'function' && fn();
|
|
480
|
-
this.debug(
|
|
1114
|
+
this.debug(`[${this.peer.type}] <${_event}> listener off`);
|
|
481
1115
|
return this;
|
|
482
|
-
}
|
|
483
|
-
|
|
1116
|
+
}
|
|
1117
|
+
removeListeners(fn) {
|
|
484
1118
|
// Clear all event listeners
|
|
485
1119
|
this.Events = {};
|
|
486
1120
|
typeof fn == 'function' && fn();
|
|
487
|
-
this.debug(
|
|
1121
|
+
this.debug(`[${this.peer.type}] All listeners removed`);
|
|
488
1122
|
return this;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
var timeoutId = setTimeout(function () {
|
|
495
|
-
reject(new Error("Event '".concat(_event, "' acknowledgment timeout after ").concat(timeout, "ms")));
|
|
1123
|
+
}
|
|
1124
|
+
emitAsync(_event, payload, timeout = 5000) {
|
|
1125
|
+
return new Promise((resolve, reject) => {
|
|
1126
|
+
const timeoutId = setTimeout(() => {
|
|
1127
|
+
reject(new Error(`Event '${_event}' acknowledgment timeout after ${timeout}ms`));
|
|
496
1128
|
}, timeout);
|
|
497
1129
|
try {
|
|
498
|
-
|
|
499
|
-
var args = [];
|
|
500
|
-
for (var _i = 1; _i < arguments.length; _i++) {
|
|
501
|
-
args[_i - 1] = arguments[_i];
|
|
502
|
-
}
|
|
1130
|
+
this.emit(_event, payload, (error, ...args) => {
|
|
503
1131
|
clearTimeout(timeoutId);
|
|
504
1132
|
error
|
|
505
1133
|
? reject(new Error(typeof error === 'string' ? error : 'Ack error'))
|
|
@@ -511,56 +1139,62 @@ var IOF = /** @class */ (function () {
|
|
|
511
1139
|
reject(error);
|
|
512
1140
|
}
|
|
513
1141
|
});
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
return new Promise(function (resolve, reject) {
|
|
522
|
-
if (_this.isConnected())
|
|
1142
|
+
}
|
|
1143
|
+
onceAsync(_event) {
|
|
1144
|
+
return new Promise(resolve => this.once(_event, resolve));
|
|
1145
|
+
}
|
|
1146
|
+
connectAsync(timeout) {
|
|
1147
|
+
return new Promise((resolve, reject) => {
|
|
1148
|
+
if (this.isConnected())
|
|
523
1149
|
return resolve();
|
|
524
|
-
|
|
525
|
-
|
|
1150
|
+
const timeoutId = setTimeout(() => {
|
|
1151
|
+
this.off('connect', connectHandler);
|
|
526
1152
|
reject(new Error('Connection timeout'));
|
|
527
|
-
}, timeout ||
|
|
528
|
-
|
|
1153
|
+
}, timeout || this.options.connectionTimeout);
|
|
1154
|
+
const connectHandler = () => {
|
|
529
1155
|
clearTimeout(timeoutId);
|
|
530
1156
|
resolve();
|
|
531
1157
|
};
|
|
532
|
-
|
|
1158
|
+
this.once('connect', connectHandler);
|
|
533
1159
|
});
|
|
534
|
-
}
|
|
1160
|
+
}
|
|
535
1161
|
// Clean up all resources
|
|
536
|
-
|
|
1162
|
+
cleanup() {
|
|
537
1163
|
if (this.messageListener) {
|
|
538
1164
|
window.removeEventListener('message', this.messageListener);
|
|
539
1165
|
this.messageListener = undefined;
|
|
540
1166
|
}
|
|
541
1167
|
this.stopHeartbeat();
|
|
1168
|
+
this.stopSessionKeyRotation();
|
|
542
1169
|
if (this.reconnectTimer) {
|
|
543
1170
|
clearTimeout(this.reconnectTimer);
|
|
544
1171
|
this.reconnectTimer = undefined;
|
|
545
1172
|
}
|
|
546
|
-
}
|
|
547
|
-
|
|
1173
|
+
}
|
|
1174
|
+
disconnect(fn) {
|
|
548
1175
|
// Cleanup on disconnect
|
|
549
1176
|
this.cleanup();
|
|
550
1177
|
this.peer.connected = false;
|
|
551
1178
|
this.peer.source = undefined;
|
|
552
1179
|
this.peer.origin = undefined;
|
|
553
1180
|
this.peer.lastHeartbeat = undefined;
|
|
1181
|
+
this.peer.protocolVersion = undefined;
|
|
1182
|
+
this.peer.sessionId = undefined;
|
|
554
1183
|
this.messageQueue = [];
|
|
555
1184
|
this.messageRateTracker = [];
|
|
556
1185
|
this.reconnectAttempts = 0;
|
|
1186
|
+
// Clear session keys
|
|
1187
|
+
this.currentSessionKey = undefined;
|
|
1188
|
+
this.pendingSessionKey = undefined;
|
|
1189
|
+
this.previousSessionKey = undefined;
|
|
1190
|
+
this.mySessionId = undefined;
|
|
557
1191
|
this.removeListeners();
|
|
558
1192
|
typeof fn == 'function' && fn();
|
|
559
|
-
this.debug(
|
|
1193
|
+
this.debug(`[${this.peer.type}] Disconnected`);
|
|
560
1194
|
return this;
|
|
561
|
-
}
|
|
1195
|
+
}
|
|
562
1196
|
// Get connection statistics
|
|
563
|
-
|
|
1197
|
+
getStats() {
|
|
564
1198
|
return {
|
|
565
1199
|
connected: this.isConnected(),
|
|
566
1200
|
peerType: this.peer.type,
|
|
@@ -569,16 +1203,19 @@ var IOF = /** @class */ (function () {
|
|
|
569
1203
|
queuedMessages: this.messageQueue.length,
|
|
570
1204
|
reconnectAttempts: this.reconnectAttempts,
|
|
571
1205
|
activeListeners: Object.keys(this.Events).length,
|
|
572
|
-
messageRate: this.messageRateTracker.length
|
|
1206
|
+
messageRate: this.messageRateTracker.length,
|
|
1207
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
1208
|
+
peerProtocolVersion: this.peer.protocolVersion,
|
|
1209
|
+
sessionKeyActive: !!this.currentSessionKey,
|
|
1210
|
+
sessionKeyId: this.currentSessionKey?.keyId
|
|
573
1211
|
};
|
|
574
|
-
}
|
|
1212
|
+
}
|
|
575
1213
|
// Clear message queue manually
|
|
576
|
-
|
|
577
|
-
|
|
1214
|
+
clearQueue() {
|
|
1215
|
+
const queueSize = this.messageQueue.length;
|
|
578
1216
|
this.messageQueue = [];
|
|
579
|
-
this.debug(
|
|
1217
|
+
this.debug(`[${this.peer.type}] Cleared ${queueSize} queued messages`);
|
|
580
1218
|
return this;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
}());
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
584
1221
|
exports.default = IOF;
|