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/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 (_a) {
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
- var size = getMessageSize(payload);
19
+ const size = getMessageSize(payload);
38
20
  if (size > maxSize)
39
- throw new Error("Message size ".concat(size, " exceeds limit ").concat(maxSize));
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
- var ackId = function () {
44
- var rmin = 100000, rmax = 999999, timestamp = Date.now(), random = Math.floor(Math.random() * (rmax - rmin + 1) + rmin);
45
- return "".concat(timestamp, "_").concat(random);
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
- var RESERVED_EVENTS = [
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
- var IOF = /** @class */ (function () {
54
- function IOF(options) {
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 = __assign({ debug: false, heartbeatInterval: 30000, connectionTimeout: 10000, maxMessageSize: 1024 * 1024, maxMessagesPerSecond: 100, autoReconnect: true, messageQueueSize: 50 }, 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
- IOF.prototype.debug = function () {
69
- var args = [];
70
- for (var _i = 0; _i < arguments.length; _i++) {
71
- args[_i] = arguments[_i];
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
- this.options.debug && console.debug.apply(console, args);
74
- };
75
- IOF.prototype.isConnected = function () {
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
- IOF.prototype.startHeartbeat = function () {
80
- var _this = this;
535
+ startHeartbeat() {
81
536
  if (!this.options.heartbeatInterval)
82
537
  return;
83
- this.heartbeatTimer = setInterval(function () {
84
- if (_this.isConnected()) {
85
- var now = Date.now();
538
+ this.heartbeatTimer = setInterval(() => {
539
+ if (this.isConnected()) {
540
+ const now = Date.now();
86
541
  // Check if peer is still responsive
87
- if (_this.peer.lastHeartbeat
88
- && (now - _this.peer.lastHeartbeat) > (_this.options.heartbeatInterval * 2)) {
89
- _this.debug("[".concat(_this.peer.type, "] Heartbeat timeout detected"));
90
- _this.handleConnectionLoss();
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
- _this.emit('__heartbeat', { timestamp: now });
550
+ this.emit('__heartbeat', { timestamp: now });
96
551
  }
97
552
  catch (error) {
98
- _this.debug("[".concat(_this.peer.type, "] Heartbeat send failed:"), error);
99
- _this.handleConnectionLoss();
553
+ this.debug(`[${this.peer.type}] Heartbeat send failed:`, error);
554
+ this.handleConnectionLoss();
100
555
  }
101
556
  }
102
557
  }, this.options.heartbeatInterval);
103
- };
104
- IOF.prototype.stopHeartbeat = function () {
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
- IOF.prototype.handleConnectionLoss = function () {
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
- IOF.prototype.attemptReconnection = function () {
122
- var _this = this;
576
+ }
577
+ attemptReconnection() {
123
578
  if (this.reconnectTimer)
124
579
  return;
125
580
  this.reconnectAttempts++;
126
- var delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts - 1), 30000); // Exponential backoff, max 30s
127
- this.debug("[".concat(this.peer.type, "] Attempting reconnection ").concat(this.reconnectAttempts, "/").concat(this.maxReconnectAttempts, " in ").concat(delay, "ms"));
128
- this.fire('reconnecting', { attempt: this.reconnectAttempts, delay: delay });
129
- this.reconnectTimer = setTimeout(function () {
130
- _this.reconnectTimer = undefined;
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
- _this.peer.type === 'WINDOW'
133
- && _this.peer.source
134
- && _this.peer.origin
135
- && _this.emit('ping');
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(function () {
139
- if (_this.peer.connected)
593
+ setTimeout(() => {
594
+ if (this.peer.connected)
140
595
  return;
141
- _this.reconnectAttempts < _this.maxReconnectAttempts
142
- ? _this.attemptReconnection()
143
- : _this.fire('reconnection_failed', { attempts: _this.reconnectAttempts });
144
- }, _this.options.connectionTimeout);
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
- IOF.prototype.checkRateLimit = function () {
603
+ checkRateLimit() {
149
604
  if (!this.options.maxMessagesPerSecond)
150
605
  return true;
151
- var now = Date.now(), aSecondAgo = now - 1000;
606
+ const now = Date.now(), aSecondAgo = now - 1000;
152
607
  // Clean old entries
153
- this.messageRateTracker = this.messageRateTracker.filter(function (timestamp) { return timestamp > aSecondAgo; });
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
- IOF.prototype.queueMessage = function (_event, payload, fn) {
622
+ queueMessage(_event, payload, fn) {
168
623
  if (this.messageQueue.length >= this.options.messageQueueSize) {
169
624
  // Remove oldest message
170
- var removed = this.messageQueue.shift();
171
- this.debug("[".concat(this.peer.type, "] Message queue full, removed oldest message:"), removed === null || removed === void 0 ? void 0 : removed._event);
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: _event,
175
- payload: payload,
176
- fn: fn,
629
+ _event,
630
+ payload,
631
+ fn,
177
632
  timestamp: Date.now()
178
633
  });
179
- this.debug("[".concat(this.peer.type, "] Queued message: ").concat(_event, " (queue size: ").concat(this.messageQueue.length, ")"));
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
- IOF.prototype.processMessageQueue = function () {
183
- var _this = this;
637
+ processMessageQueue() {
184
638
  if (!this.isConnected() || this.messageQueue.length === 0)
185
639
  return;
186
- this.debug("[".concat(this.peer.type, "] Processing ").concat(this.messageQueue.length, " queued messages"));
187
- var queue = __spreadArray([], this.messageQueue, true);
640
+ this.debug(`[${this.peer.type}] Processing ${this.messageQueue.length} queued messages`);
641
+ const queue = [...this.messageQueue];
188
642
  this.messageQueue = [];
189
- queue.forEach(function (message) {
643
+ queue.forEach(message => {
190
644
  try {
191
- _this.emit(message._event, message.payload, message.fn);
645
+ this.emit(message._event, message.payload, message.fn);
192
646
  }
193
647
  catch (error) {
194
- _this.debug("[".concat(_this.peer.type, "] Failed to send queued message:"), error);
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
- IOF.prototype.initiate = function (contentWindow, iframeOrigin) {
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 = function (_a) {
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 !== _this.peer.origin
670
+ if (origin !== this.peer.origin
219
671
  || !source
220
672
  || typeof data !== 'object'
221
673
  || !data.hasOwnProperty('_event'))
222
674
  return;
223
- var _b = data, _event = _b._event, payload = _b.payload, cid = _b.cid, timestamp = _b.timestamp;
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
- _this.peer.lastHeartbeat = Date.now();
718
+ this.peer.lastHeartbeat = Date.now();
227
719
  return;
228
720
  }
229
721
  // Handle heartbeat requests
230
722
  if (_event === '__heartbeat') {
231
- _this.emit('__heartbeat_response', { timestamp: Date.now() });
232
- _this.peer.lastHeartbeat = Date.now();
723
+ this.emit('__heartbeat_response', { timestamp: Date.now() });
724
+ this.peer.lastHeartbeat = Date.now();
233
725
  return;
234
726
  }
235
- _this.debug("[".concat(_this.peer.type, "] Message: ").concat(_event), payload || '');
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
- _this.peer.connected = true;
240
- _this.reconnectAttempts = 0;
241
- _this.peer.lastHeartbeat = Date.now();
242
- _this.startHeartbeat();
243
- _this.fire('connect');
244
- _this.processMessageQueue();
245
- _this.debug("[".concat(_this.peer.type, "] connected"));
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
- _this.fire(_event, payload, cid);
760
+ this.fire(_event, payload, cid);
250
761
  }
251
762
  catch (error) {
252
- _this.debug("[".concat(_this.peer.type, "] Message handling error:"), error);
253
- _this.fire('error', {
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: origin
767
+ origin
257
768
  });
258
769
  }
259
770
  };
260
771
  window.addEventListener('message', this.messageListener, false);
261
- this.debug("[".concat(this.peer.type, "] Initiate connection: IFrame origin <").concat(iframeOrigin, ">"));
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
- IOF.prototype.listen = function (hostOrigin) {
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("[".concat(this.peer.type, "] Listening to connect").concat(hostOrigin ? ": Host <".concat(hostOrigin, ">") : ''));
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 = function (_a) {
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
- _this.fire('error', {
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 (!_this.peer.source) {
295
- _this.peer = __assign(__assign({}, _this.peer), { source: source, origin: origin });
296
- _this.debug("[".concat(_this.peer.type, "] Connect to ").concat(origin));
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 !== _this.peer.origin) {
300
- _this.fire('error', {
808
+ else if (origin !== this.peer.origin) {
809
+ this.fire('error', {
301
810
  type: 'ORIGIN_MISMATCH',
302
- expected: _this.peer.origin,
811
+ expected: this.peer.origin,
303
812
  received: origin
304
813
  });
305
814
  return;
306
815
  }
307
- var _event = data._event, payload = data.payload, cid = data.cid, timestamp = data.timestamp;
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
- _this.peer.lastHeartbeat = Date.now();
859
+ this.peer.lastHeartbeat = Date.now();
311
860
  return;
312
861
  }
313
862
  // Handle heartbeat requests
314
863
  if (_event === '__heartbeat') {
315
- _this.emit('__heartbeat_response', { timestamp: Date.now() });
316
- _this.peer.lastHeartbeat = Date.now();
864
+ this.emit('__heartbeat_response', { timestamp: Date.now() });
865
+ this.peer.lastHeartbeat = Date.now();
317
866
  return;
318
867
  }
319
- _this.debug("[".concat(_this.peer.type, "] Message: ").concat(_event), payload || '');
868
+ this.debug(`[${this.peer.type}] Message v${messageVersion}: ${_event}`, payload || '');
320
869
  // Handshake or availability check events
321
870
  if (_event == 'ping') {
322
- _this.emit('pong');
871
+ this.emit('pong');
323
872
  // Iframe is connected to content window
324
- _this.peer.connected = true;
325
- _this.reconnectAttempts = 0;
326
- _this.peer.lastHeartbeat = Date.now();
327
- _this.startHeartbeat();
328
- _this.fire('connect');
329
- _this.processMessageQueue();
330
- _this.debug("[".concat(_this.peer.type, "] connected"));
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
- _this.fire(_event, payload, cid);
902
+ this.fire(_event, payload, cid);
335
903
  }
336
904
  catch (error) {
337
- _this.debug("[".concat(_this.peer.type, "] Message handling error:"), error);
338
- _this.fire('error', {
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: origin
909
+ origin
342
910
  });
343
911
  }
344
912
  };
345
913
  window.addEventListener('message', this.messageListener, false);
346
914
  return this;
347
- };
348
- IOF.prototype.fire = function (_event, payload, cid) {
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("[".concat(this.peer.type, "] No <").concat(_event, "> listener defined"));
919
+ this.debug(`[${this.peer.type}] No <${_event}> listener defined`);
353
920
  return;
354
921
  }
355
- var ackFn = cid
356
- ? function (error) {
357
- var args = [];
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
- var listeners = [];
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(function (fn) {
939
+ listeners.forEach(fn => {
377
940
  try {
378
941
  payload !== undefined ? fn(payload, ackFn) : fn(ackFn);
379
942
  }
380
943
  catch (error) {
381
- _this.debug("[".concat(_this.peer.type, "] Listener error for ").concat(_event, ":"), error);
382
- _this.fire('error', {
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
- IOF.prototype.emit = function (_event, payload, fn) {
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
- var sanitizedPayload = payload
975
+ const sanitizedPayload = payload
413
976
  ? sanitizePayload(payload, this.options.maxMessageSize)
414
977
  : payload;
415
978
  // Acknowledge event listener
416
- var cid = void 0;
979
+ let cid;
417
980
  if (typeof fn === 'function') {
418
- var ackFunction_1 = fn;
981
+ const ackFunction = fn;
419
982
  cid = ackId();
420
- this.once("".concat(_event, "--").concat(cid, "--@ack"), function (_a) {
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
- var messageData = {
426
- _event: _event,
985
+ const messageData = {
986
+ v: PROTOCOL_VERSION,
987
+ _event,
427
988
  payload: sanitizedPayload,
428
- cid: 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("[".concat(this.peer.type, "] Emit error:"), error);
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
- IOF.prototype.on = function (_event, fn) {
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("[".concat(this.peer.type, "] New <").concat(_event, "> listener on"));
1086
+ this.debug(`[${this.peer.type}] New <${_event}> listener on`);
453
1087
  return this;
454
- };
455
- IOF.prototype.once = function (_event, fn) {
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("[".concat(this.peer.type, "] New <").concat(_event, " once> listener on"));
1095
+ this.debug(`[${this.peer.type}] New <${_event} once> listener on`);
462
1096
  return this;
463
- };
464
- IOF.prototype.off = function (_event, fn) {
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
- var index = this.Events[_event].indexOf(fn);
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("[".concat(this.peer.type, "] <").concat(_event, "> listener off"));
1114
+ this.debug(`[${this.peer.type}] <${_event}> listener off`);
481
1115
  return this;
482
- };
483
- IOF.prototype.removeListeners = function (fn) {
1116
+ }
1117
+ removeListeners(fn) {
484
1118
  // Clear all event listeners
485
1119
  this.Events = {};
486
1120
  typeof fn == 'function' && fn();
487
- this.debug("[".concat(this.peer.type, "] All listeners removed"));
1121
+ this.debug(`[${this.peer.type}] All listeners removed`);
488
1122
  return this;
489
- };
490
- IOF.prototype.emitAsync = function (_event, payload, timeout) {
491
- var _this = this;
492
- if (timeout === void 0) { timeout = 5000; }
493
- return new Promise(function (resolve, reject) {
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
- _this.emit(_event, payload, function (error) {
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
- IOF.prototype.onceAsync = function (_event) {
516
- var _this = this;
517
- return new Promise(function (resolve) { return _this.once(_event, resolve); });
518
- };
519
- IOF.prototype.connectAsync = function (timeout) {
520
- var _this = this;
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
- var timeoutId = setTimeout(function () {
525
- _this.off('connect', connectHandler);
1150
+ const timeoutId = setTimeout(() => {
1151
+ this.off('connect', connectHandler);
526
1152
  reject(new Error('Connection timeout'));
527
- }, timeout || _this.options.connectionTimeout);
528
- var connectHandler = function () {
1153
+ }, timeout || this.options.connectionTimeout);
1154
+ const connectHandler = () => {
529
1155
  clearTimeout(timeoutId);
530
1156
  resolve();
531
1157
  };
532
- _this.once('connect', connectHandler);
1158
+ this.once('connect', connectHandler);
533
1159
  });
534
- };
1160
+ }
535
1161
  // Clean up all resources
536
- IOF.prototype.cleanup = function () {
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
- IOF.prototype.disconnect = function (fn) {
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("[".concat(this.peer.type, "] Disconnected"));
1193
+ this.debug(`[${this.peer.type}] Disconnected`);
560
1194
  return this;
561
- };
1195
+ }
562
1196
  // Get connection statistics
563
- IOF.prototype.getStats = function () {
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
- IOF.prototype.clearQueue = function () {
577
- var queueSize = this.messageQueue.length;
1214
+ clearQueue() {
1215
+ const queueSize = this.messageQueue.length;
578
1216
  this.messageQueue = [];
579
- this.debug("[".concat(this.peer.type, "] Cleared ").concat(queueSize, " queued messages"));
1217
+ this.debug(`[${this.peer.type}] Cleared ${queueSize} queued messages`);
580
1218
  return this;
581
- };
582
- return IOF;
583
- }());
1219
+ }
1220
+ }
584
1221
  exports.default = IOF;