iframe.io 1.1.0 → 1.2.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 CHANGED
@@ -89,7 +89,9 @@ const iframeIO = new IOF({
89
89
  maxMessageSize: 1024 * 1024, // Max message size in bytes (1MB)
90
90
  maxMessagesPerSecond: 100, // Rate limit (100 messages/second)
91
91
  autoReconnect: true, // Enable automatic reconnection
92
- messageQueueSize: 50 // Max queued messages when disconnected
92
+ messageQueueSize: 50, // Max queued messages when disconnected
93
+ allowedIncomingEvents: ['hello', 'response'], // Optional incoming event allowlist (non-reserved events)
94
+ validateIncoming: (event, payload, origin) => true // Optional custom incoming validator
93
95
  })
94
96
  ```
95
97
 
@@ -210,6 +212,29 @@ iframeIO.on('error', (error) => {
210
212
  })
211
213
  ```
212
214
 
215
+ ### Incoming Event Allowlist & Validation
216
+
217
+ For defense-in-depth, you can restrict which **application-level** events are accepted and/or validate incoming payloads. Reserved internal events (`ping`, `pong`, `__heartbeat`, `__heartbeat_response`) are always allowed.
218
+
219
+ ```javascript
220
+ const iframeIO = new IOF({
221
+ type: 'IFRAME',
222
+ debug: true,
223
+ allowedIncomingEvents: ['getData', 'hello'],
224
+ validateIncoming: (event, payload, origin) => {
225
+ // Example: basic shape checks
226
+ if (event === 'getData') return payload && typeof payload.id === 'number'
227
+ return true
228
+ }
229
+ })
230
+
231
+ iframeIO.on('error', (error) => {
232
+ if (error.type === 'DISALLOWED_EVENT' || error.type === 'INVALID_MESSAGE') {
233
+ console.warn('Dropped incoming message:', error)
234
+ }
235
+ })
236
+ ```
237
+
213
238
  ## Comprehensive Error Handling
214
239
 
215
240
  ```javascript
@@ -338,6 +363,8 @@ const response = await iframeIO.emitAsync<{ query: string }, ApiResponse>(
338
363
  | `MESSAGE_HANDLING_ERROR` | Error processing incoming message |
339
364
  | `EMIT_ERROR` | Error sending message |
340
365
  | `LISTENER_ERROR` | Error in event listener |
366
+ | `DISALLOWED_EVENT` | Incoming event rejected by `allowedIncomingEvents` |
367
+ | `INVALID_MESSAGE` | Incoming message rejected by `validateIncoming` |
341
368
  | `RATE_LIMIT_EXCEEDED` | Too many messages sent |
342
369
  | `NO_CONNECTION` | Attempted to send without connection |
343
370
 
package/dist/index.d.ts CHANGED
@@ -10,6 +10,16 @@ export type Options = {
10
10
  maxMessagesPerSecond?: number;
11
11
  autoReconnect?: boolean;
12
12
  messageQueueSize?: number;
13
+ /**
14
+ * Optional allowlist of incoming application-level events.
15
+ * Reserved internal events (ping/pong/heartbeats) are always allowed.
16
+ */
17
+ allowedIncomingEvents?: string[];
18
+ /**
19
+ * Optional custom validator for incoming messages.
20
+ * Return false to drop a message; an 'error' event will be emitted.
21
+ */
22
+ validateIncoming?: (event: string, payload: any, origin: string) => boolean;
13
23
  };
14
24
  export interface RegisteredEvents {
15
25
  [index: string]: Listener[];
package/dist/index.js CHANGED
@@ -1,24 +1,4 @@
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 });
23
3
  function newObject(data) {
24
4
  return JSON.parse(JSON.stringify(data));
@@ -27,88 +7,107 @@ function getMessageSize(data) {
27
7
  try {
28
8
  return JSON.stringify(data).length;
29
9
  }
30
- catch (_a) {
10
+ catch {
31
11
  return 0;
32
12
  }
33
13
  }
34
14
  function sanitizePayload(payload, maxSize) {
35
15
  if (!payload)
36
16
  return payload;
37
- var size = getMessageSize(payload);
17
+ const size = getMessageSize(payload);
38
18
  if (size > maxSize)
39
- throw new Error("Message size ".concat(size, " exceeds limit ").concat(maxSize));
19
+ throw new Error(`Message size ${size} exceeds limit ${maxSize}`);
40
20
  // Basic sanitization - remove functions and undefined values
41
21
  return JSON.parse(JSON.stringify(payload));
42
22
  }
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);
23
+ const ackId = () => {
24
+ // Prefer cryptographically strong randomness when available
25
+ try {
26
+ const globalCrypto = (typeof crypto !== 'undefined'
27
+ ? crypto
28
+ : (typeof window !== 'undefined' && window.crypto)
29
+ || (typeof globalThis !== 'undefined' && globalThis.crypto));
30
+ if (globalCrypto && typeof globalCrypto.getRandomValues === 'function') {
31
+ const buffer = new Uint32Array(4);
32
+ globalCrypto.getRandomValues(buffer);
33
+ const randomPart = Array.from(buffer).map(n => n.toString(16)).join('');
34
+ return `${Date.now()}_${randomPart}`;
35
+ }
36
+ }
37
+ catch {
38
+ // Fall back to Math.random-based implementation below
39
+ }
40
+ const rmin = 100000, rmax = 999999, timestampFallback = Date.now(), randomFallback = Math.floor(Math.random() * (rmax - rmin + 1) + rmin);
41
+ return `${timestampFallback}_${randomFallback}`;
46
42
  };
47
- var RESERVED_EVENTS = [
43
+ const RESERVED_EVENTS = [
48
44
  'ping',
49
45
  'pong',
50
46
  '__heartbeat',
51
47
  '__heartbeat_response'
52
48
  ];
53
- var IOF = /** @class */ (function () {
54
- function IOF(options) {
55
- if (options === void 0) { options = {}; }
49
+ class IOF {
50
+ constructor(options = {}) {
56
51
  this.messageQueue = [];
57
52
  this.messageRateTracker = [];
58
53
  this.reconnectAttempts = 0;
59
54
  this.maxReconnectAttempts = 5;
60
55
  if (options && typeof options !== 'object')
61
56
  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);
57
+ this.options = {
58
+ debug: false,
59
+ heartbeatInterval: 30000,
60
+ connectionTimeout: 10000,
61
+ maxMessageSize: 1024 * 1024,
62
+ maxMessagesPerSecond: 100,
63
+ autoReconnect: true,
64
+ messageQueueSize: 50,
65
+ ...options
66
+ };
63
67
  this.Events = {};
64
68
  this.peer = { type: 'IFRAME', connected: false };
65
69
  if (options.type)
66
70
  this.peer.type = options.type.toUpperCase();
67
71
  }
68
- IOF.prototype.debug = function () {
69
- var args = [];
70
- for (var _i = 0; _i < arguments.length; _i++) {
71
- args[_i] = arguments[_i];
72
- }
73
- this.options.debug && console.debug.apply(console, args);
74
- };
75
- IOF.prototype.isConnected = function () {
72
+ debug(...args) {
73
+ this.options.debug && console.debug(...args);
74
+ }
75
+ isConnected() {
76
76
  return !!this.peer.connected && !!this.peer.source;
77
- };
77
+ }
78
78
  // Enhanced connection health monitoring
79
- IOF.prototype.startHeartbeat = function () {
80
- var _this = this;
79
+ startHeartbeat() {
81
80
  if (!this.options.heartbeatInterval)
82
81
  return;
83
- this.heartbeatTimer = setInterval(function () {
84
- if (_this.isConnected()) {
85
- var now = Date.now();
82
+ this.heartbeatTimer = setInterval(() => {
83
+ if (this.isConnected()) {
84
+ const now = Date.now();
86
85
  // 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();
86
+ if (this.peer.lastHeartbeat
87
+ && (now - this.peer.lastHeartbeat) > (this.options.heartbeatInterval * 2)) {
88
+ this.debug(`[${this.peer.type}] Heartbeat timeout detected`);
89
+ this.handleConnectionLoss();
91
90
  return;
92
91
  }
93
92
  // Send heartbeat
94
93
  try {
95
- _this.emit('__heartbeat', { timestamp: now });
94
+ this.emit('__heartbeat', { timestamp: now });
96
95
  }
97
96
  catch (error) {
98
- _this.debug("[".concat(_this.peer.type, "] Heartbeat send failed:"), error);
99
- _this.handleConnectionLoss();
97
+ this.debug(`[${this.peer.type}] Heartbeat send failed:`, error);
98
+ this.handleConnectionLoss();
100
99
  }
101
100
  }
102
101
  }, this.options.heartbeatInterval);
103
- };
104
- IOF.prototype.stopHeartbeat = function () {
102
+ }
103
+ stopHeartbeat() {
105
104
  if (!this.heartbeatTimer)
106
105
  return;
107
106
  clearInterval(this.heartbeatTimer);
108
107
  this.heartbeatTimer = undefined;
109
- };
108
+ }
110
109
  // Handle connection loss and potential reconnection
111
- IOF.prototype.handleConnectionLoss = function () {
110
+ handleConnectionLoss() {
112
111
  if (!this.peer.connected)
113
112
  return;
114
113
  this.peer.connected = false;
@@ -117,40 +116,39 @@ var IOF = /** @class */ (function () {
117
116
  this.options.autoReconnect
118
117
  && this.reconnectAttempts < this.maxReconnectAttempts
119
118
  && this.attemptReconnection();
120
- };
121
- IOF.prototype.attemptReconnection = function () {
122
- var _this = this;
119
+ }
120
+ attemptReconnection() {
123
121
  if (this.reconnectTimer)
124
122
  return;
125
123
  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;
124
+ const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts - 1), 30000); // Exponential backoff, max 30s
125
+ this.debug(`[${this.peer.type}] Attempting reconnection ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`);
126
+ this.fire('reconnecting', { attempt: this.reconnectAttempts, delay });
127
+ this.reconnectTimer = setTimeout(() => {
128
+ this.reconnectTimer = undefined;
131
129
  // Re-initiate connection for WINDOW type
132
- _this.peer.type === 'WINDOW'
133
- && _this.peer.source
134
- && _this.peer.origin
135
- && _this.emit('ping');
130
+ this.peer.type === 'WINDOW'
131
+ && this.peer.source
132
+ && this.peer.origin
133
+ && this.emit('ping');
136
134
  // For IFRAME type, just wait for incoming connection
137
135
  // Set timeout for this reconnection attempt
138
- setTimeout(function () {
139
- if (_this.peer.connected)
136
+ setTimeout(() => {
137
+ if (this.peer.connected)
140
138
  return;
141
- _this.reconnectAttempts < _this.maxReconnectAttempts
142
- ? _this.attemptReconnection()
143
- : _this.fire('reconnection_failed', { attempts: _this.reconnectAttempts });
144
- }, _this.options.connectionTimeout);
139
+ this.reconnectAttempts < this.maxReconnectAttempts
140
+ ? this.attemptReconnection()
141
+ : this.fire('reconnection_failed', { attempts: this.reconnectAttempts });
142
+ }, this.options.connectionTimeout);
145
143
  }, delay);
146
- };
144
+ }
147
145
  // Message rate limiting
148
- IOF.prototype.checkRateLimit = function () {
146
+ checkRateLimit() {
149
147
  if (!this.options.maxMessagesPerSecond)
150
148
  return true;
151
- var now = Date.now(), aSecondAgo = now - 1000;
149
+ const now = Date.now(), aSecondAgo = now - 1000;
152
150
  // Clean old entries
153
- this.messageRateTracker = this.messageRateTracker.filter(function (timestamp) { return timestamp > aSecondAgo; });
151
+ this.messageRateTracker = this.messageRateTracker.filter(timestamp => timestamp > aSecondAgo);
154
152
  // Check if limit exceeded
155
153
  if (this.messageRateTracker.length >= this.options.maxMessagesPerSecond) {
156
154
  this.fire('error', {
@@ -162,45 +160,43 @@ var IOF = /** @class */ (function () {
162
160
  }
163
161
  this.messageRateTracker.push(now);
164
162
  return true;
165
- };
163
+ }
166
164
  // Queue messages when not connected
167
- IOF.prototype.queueMessage = function (_event, payload, fn) {
165
+ queueMessage(_event, payload, fn) {
168
166
  if (this.messageQueue.length >= this.options.messageQueueSize) {
169
167
  // 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);
168
+ const removed = this.messageQueue.shift();
169
+ this.debug(`[${this.peer.type}] Message queue full, removed oldest message:`, removed?._event);
172
170
  }
173
171
  this.messageQueue.push({
174
- _event: _event,
175
- payload: payload,
176
- fn: fn,
172
+ _event,
173
+ payload,
174
+ fn,
177
175
  timestamp: Date.now()
178
176
  });
179
- this.debug("[".concat(this.peer.type, "] Queued message: ").concat(_event, " (queue size: ").concat(this.messageQueue.length, ")"));
180
- };
177
+ this.debug(`[${this.peer.type}] Queued message: ${_event} (queue size: ${this.messageQueue.length})`);
178
+ }
181
179
  // Process queued messages when connection is established
182
- IOF.prototype.processMessageQueue = function () {
183
- var _this = this;
180
+ processMessageQueue() {
184
181
  if (!this.isConnected() || this.messageQueue.length === 0)
185
182
  return;
186
- this.debug("[".concat(this.peer.type, "] Processing ").concat(this.messageQueue.length, " queued messages"));
187
- var queue = __spreadArray([], this.messageQueue, true);
183
+ this.debug(`[${this.peer.type}] Processing ${this.messageQueue.length} queued messages`);
184
+ const queue = [...this.messageQueue];
188
185
  this.messageQueue = [];
189
- queue.forEach(function (message) {
186
+ queue.forEach(message => {
190
187
  try {
191
- _this.emit(message._event, message.payload, message.fn);
188
+ this.emit(message._event, message.payload, message.fn);
192
189
  }
193
190
  catch (error) {
194
- _this.debug("[".concat(_this.peer.type, "] Failed to send queued message:"), error);
191
+ this.debug(`[${this.peer.type}] Failed to send queued message:`, error);
195
192
  }
196
193
  });
197
- };
194
+ }
198
195
  /**
199
196
  * Establish a connection with an iframe containing
200
197
  * in the current window
201
198
  */
202
- IOF.prototype.initiate = function (contentWindow, iframeOrigin) {
203
- var _this = this;
199
+ initiate(contentWindow, iframeOrigin) {
204
200
  if (!contentWindow || !iframeOrigin)
205
201
  throw new Error('Invalid Connection initiation arguments');
206
202
  if (this.peer.type === 'IFRAME')
@@ -211,74 +207,94 @@ var IOF = /** @class */ (function () {
211
207
  this.peer.origin = iframeOrigin;
212
208
  this.peer.connected = false;
213
209
  this.reconnectAttempts = 0;
214
- this.messageListener = function (_a) {
215
- var origin = _a.origin, data = _a.data, source = _a.source;
210
+ this.messageListener = ({ origin, data, source }) => {
216
211
  try {
217
212
  // Enhanced security: check valid message structure
218
- if (origin !== _this.peer.origin
213
+ if (origin !== this.peer.origin
219
214
  || !source
220
215
  || typeof data !== 'object'
221
216
  || !data.hasOwnProperty('_event'))
222
217
  return;
223
- var _b = data, _event = _b._event, payload = _b.payload, cid = _b.cid, timestamp = _b.timestamp;
218
+ const { _event, payload, cid, timestamp } = data;
224
219
  // Handle heartbeat responses
225
220
  if (_event === '__heartbeat_response') {
226
- _this.peer.lastHeartbeat = Date.now();
221
+ this.peer.lastHeartbeat = Date.now();
227
222
  return;
228
223
  }
229
224
  // Handle heartbeat requests
230
225
  if (_event === '__heartbeat') {
231
- _this.emit('__heartbeat_response', { timestamp: Date.now() });
232
- _this.peer.lastHeartbeat = Date.now();
226
+ this.emit('__heartbeat_response', { timestamp: Date.now() });
227
+ this.peer.lastHeartbeat = Date.now();
233
228
  return;
234
229
  }
235
- _this.debug("[".concat(_this.peer.type, "] Message: ").concat(_event), payload || '');
230
+ this.debug(`[${this.peer.type}] Message: ${_event}`, payload || '');
236
231
  // Handshake or availability check events
237
232
  if (_event == 'pong') {
238
233
  // 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"));
234
+ this.peer.connected = true;
235
+ this.reconnectAttempts = 0;
236
+ this.peer.lastHeartbeat = Date.now();
237
+ this.startHeartbeat();
238
+ this.fire('connect');
239
+ this.processMessageQueue();
240
+ this.debug(`[${this.peer.type}] connected`);
246
241
  return;
247
242
  }
243
+ // Optional application-level incoming validation (non-reserved events only)
244
+ if (!RESERVED_EVENTS.includes(_event)) {
245
+ if (this.options.allowedIncomingEvents
246
+ && !this.options.allowedIncomingEvents.includes(_event)) {
247
+ this.fire('error', {
248
+ type: 'DISALLOWED_EVENT',
249
+ direction: 'incoming',
250
+ event: _event,
251
+ origin
252
+ });
253
+ return;
254
+ }
255
+ if (this.options.validateIncoming
256
+ && !this.options.validateIncoming(_event, payload, origin)) {
257
+ this.fire('error', {
258
+ type: 'INVALID_MESSAGE',
259
+ direction: 'incoming',
260
+ event: _event,
261
+ origin
262
+ });
263
+ return;
264
+ }
265
+ }
248
266
  // Fire available event listeners
249
- _this.fire(_event, payload, cid);
267
+ this.fire(_event, payload, cid);
250
268
  }
251
269
  catch (error) {
252
- _this.debug("[".concat(_this.peer.type, "] Message handling error:"), error);
253
- _this.fire('error', {
270
+ this.debug(`[${this.peer.type}] Message handling error:`, error);
271
+ this.fire('error', {
254
272
  type: 'MESSAGE_HANDLING_ERROR',
255
273
  error: error instanceof Error ? error.message : String(error),
256
- origin: origin
274
+ origin
257
275
  });
258
276
  }
259
277
  };
260
278
  window.addEventListener('message', this.messageListener, false);
261
- this.debug("[".concat(this.peer.type, "] Initiate connection: IFrame origin <").concat(iframeOrigin, ">"));
279
+ this.debug(`[${this.peer.type}] Initiate connection: IFrame origin <${iframeOrigin}>`);
262
280
  this.emit('ping');
263
281
  return this;
264
- };
282
+ }
265
283
  /**
266
284
  * Listening to connection from the content window
267
285
  */
268
- IOF.prototype.listen = function (hostOrigin) {
269
- var _this = this;
286
+ listen(hostOrigin) {
270
287
  this.peer.type = 'IFRAME'; // iframe.io connection listener is automatically set as IFRAME
271
288
  this.peer.connected = false;
272
289
  this.reconnectAttempts = 0;
273
- this.debug("[".concat(this.peer.type, "] Listening to connect").concat(hostOrigin ? ": Host <".concat(hostOrigin, ">") : ''));
290
+ this.debug(`[${this.peer.type}] Listening to connect${hostOrigin ? `: Host <${hostOrigin}>` : ''}`);
274
291
  // Clean up existing listener if any
275
292
  this.cleanup();
276
- this.messageListener = function (_a) {
277
- var origin = _a.origin, data = _a.data, source = _a.source;
293
+ this.messageListener = ({ origin, data, source }) => {
278
294
  try {
279
295
  // Enhanced security: check host origin where event must only come from
280
296
  if (hostOrigin && hostOrigin !== origin) {
281
- _this.fire('error', {
297
+ this.fire('error', {
282
298
  type: 'INVALID_ORIGIN',
283
299
  expected: hostOrigin,
284
300
  received: origin
@@ -291,78 +307,73 @@ var IOF = /** @class */ (function () {
291
307
  || !data.hasOwnProperty('_event'))
292
308
  return;
293
309
  // 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));
310
+ if (!this.peer.source) {
311
+ this.peer = { ...this.peer, source: source, origin };
312
+ this.debug(`[${this.peer.type}] Connect to ${origin}`);
297
313
  }
298
314
  // Origin different from handshaked source origin
299
- else if (origin !== _this.peer.origin) {
300
- _this.fire('error', {
315
+ else if (origin !== this.peer.origin) {
316
+ this.fire('error', {
301
317
  type: 'ORIGIN_MISMATCH',
302
- expected: _this.peer.origin,
318
+ expected: this.peer.origin,
303
319
  received: origin
304
320
  });
305
321
  return;
306
322
  }
307
- var _event = data._event, payload = data.payload, cid = data.cid, timestamp = data.timestamp;
323
+ const { _event, payload, cid, timestamp } = data;
308
324
  // Handle heartbeat responses
309
325
  if (_event === '__heartbeat_response') {
310
- _this.peer.lastHeartbeat = Date.now();
326
+ this.peer.lastHeartbeat = Date.now();
311
327
  return;
312
328
  }
313
329
  // Handle heartbeat requests
314
330
  if (_event === '__heartbeat') {
315
- _this.emit('__heartbeat_response', { timestamp: Date.now() });
316
- _this.peer.lastHeartbeat = Date.now();
331
+ this.emit('__heartbeat_response', { timestamp: Date.now() });
332
+ this.peer.lastHeartbeat = Date.now();
317
333
  return;
318
334
  }
319
- _this.debug("[".concat(_this.peer.type, "] Message: ").concat(_event), payload || '');
335
+ this.debug(`[${this.peer.type}] Message: ${_event}`, payload || '');
320
336
  // Handshake or availability check events
321
337
  if (_event == 'ping') {
322
- _this.emit('pong');
338
+ this.emit('pong');
323
339
  // 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"));
340
+ this.peer.connected = true;
341
+ this.reconnectAttempts = 0;
342
+ this.peer.lastHeartbeat = Date.now();
343
+ this.startHeartbeat();
344
+ this.fire('connect');
345
+ this.processMessageQueue();
346
+ this.debug(`[${this.peer.type}] connected`);
331
347
  return;
332
348
  }
333
349
  // Fire available event listeners
334
- _this.fire(_event, payload, cid);
350
+ this.fire(_event, payload, cid);
335
351
  }
336
352
  catch (error) {
337
- _this.debug("[".concat(_this.peer.type, "] Message handling error:"), error);
338
- _this.fire('error', {
353
+ this.debug(`[${this.peer.type}] Message handling error:`, error);
354
+ this.fire('error', {
339
355
  type: 'MESSAGE_HANDLING_ERROR',
340
356
  error: error instanceof Error ? error.message : String(error),
341
- origin: origin
357
+ origin
342
358
  });
343
359
  }
344
360
  };
345
361
  window.addEventListener('message', this.messageListener, false);
346
362
  return this;
347
- };
348
- IOF.prototype.fire = function (_event, payload, cid) {
349
- var _this = this;
363
+ }
364
+ fire(_event, payload, cid) {
350
365
  // Volatile event - check if any listeners exist
351
366
  if (!this.Events[_event] && !this.Events[_event + '--@once']) {
352
- this.debug("[".concat(this.peer.type, "] No <").concat(_event, "> listener defined"));
367
+ this.debug(`[${this.peer.type}] No <${_event}> listener defined`);
353
368
  return;
354
369
  }
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 });
370
+ const ackFn = cid
371
+ ? (error, ...args) => {
372
+ this.emit(`${_event}--${cid}--@ack`, { error: error || false, args });
362
373
  return;
363
374
  }
364
375
  : undefined;
365
- var listeners = [];
376
+ let listeners = [];
366
377
  if (this.Events[_event + '--@once']) {
367
378
  // Once triggable event
368
379
  _event += '--@once';
@@ -373,21 +384,21 @@ var IOF = /** @class */ (function () {
373
384
  else
374
385
  listeners = this.Events[_event];
375
386
  // Fire listeners with error handling
376
- listeners.forEach(function (fn) {
387
+ listeners.forEach(fn => {
377
388
  try {
378
389
  payload !== undefined ? fn(payload, ackFn) : fn(ackFn);
379
390
  }
380
391
  catch (error) {
381
- _this.debug("[".concat(_this.peer.type, "] Listener error for ").concat(_event, ":"), error);
382
- _this.fire('error', {
392
+ this.debug(`[${this.peer.type}] Listener error for ${_event}:`, error);
393
+ this.fire('error', {
383
394
  type: 'LISTENER_ERROR',
384
395
  event: _event,
385
396
  error: error instanceof Error ? error.message : String(error)
386
397
  });
387
398
  }
388
399
  });
389
- };
390
- IOF.prototype.emit = function (_event, payload, fn) {
400
+ }
401
+ emit(_event, payload, fn) {
391
402
  // Check rate limiting
392
403
  if (!this.checkRateLimit())
393
404
  return this;
@@ -409,30 +420,27 @@ var IOF = /** @class */ (function () {
409
420
  }
410
421
  try {
411
422
  // Enhanced security: sanitize and validate payload
412
- var sanitizedPayload = payload
423
+ const sanitizedPayload = payload
413
424
  ? sanitizePayload(payload, this.options.maxMessageSize)
414
425
  : payload;
415
426
  // Acknowledge event listener
416
- var cid = void 0;
427
+ let cid;
417
428
  if (typeof fn === 'function') {
418
- var ackFunction_1 = fn;
429
+ const ackFunction = fn;
419
430
  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
- });
431
+ this.once(`${_event}--${cid}--@ack`, ({ error, args }) => ackFunction(error, ...args));
424
432
  }
425
- var messageData = {
426
- _event: _event,
433
+ const messageData = {
434
+ _event,
427
435
  payload: sanitizedPayload,
428
- cid: cid,
436
+ cid,
429
437
  timestamp: Date.now(),
430
438
  size: getMessageSize(sanitizedPayload)
431
439
  };
432
440
  this.peer.source.postMessage(newObject(messageData), this.peer.origin);
433
441
  }
434
442
  catch (error) {
435
- this.debug("[".concat(this.peer.type, "] Emit error:"), error);
443
+ this.debug(`[${this.peer.type}] Emit error:`, error);
436
444
  this.fire('error', {
437
445
  type: 'EMIT_ERROR',
438
446
  event: _event,
@@ -443,29 +451,29 @@ var IOF = /** @class */ (function () {
443
451
  && fn(error instanceof Error ? error.message : String(error));
444
452
  }
445
453
  return this;
446
- };
447
- IOF.prototype.on = function (_event, fn) {
454
+ }
455
+ on(_event, fn) {
448
456
  // Add Event listener
449
457
  if (!this.Events[_event])
450
458
  this.Events[_event] = [];
451
459
  this.Events[_event].push(fn);
452
- this.debug("[".concat(this.peer.type, "] New <").concat(_event, "> listener on"));
460
+ this.debug(`[${this.peer.type}] New <${_event}> listener on`);
453
461
  return this;
454
- };
455
- IOF.prototype.once = function (_event, fn) {
462
+ }
463
+ once(_event, fn) {
456
464
  // Add Once Event listener
457
465
  _event += '--@once';
458
466
  if (!this.Events[_event])
459
467
  this.Events[_event] = [];
460
468
  this.Events[_event].push(fn);
461
- this.debug("[".concat(this.peer.type, "] New <").concat(_event, " once> listener on"));
469
+ this.debug(`[${this.peer.type}] New <${_event} once> listener on`);
462
470
  return this;
463
- };
464
- IOF.prototype.off = function (_event, fn) {
471
+ }
472
+ off(_event, fn) {
465
473
  // Remove Event listener
466
474
  if (fn && this.Events[_event]) {
467
475
  // Remove specific listener if provided
468
- var index = this.Events[_event].indexOf(fn);
476
+ const index = this.Events[_event].indexOf(fn);
469
477
  if (index > -1) {
470
478
  this.Events[_event].splice(index, 1);
471
479
  // Remove event array if empty
@@ -477,29 +485,23 @@ var IOF = /** @class */ (function () {
477
485
  else
478
486
  delete this.Events[_event];
479
487
  typeof fn == 'function' && fn();
480
- this.debug("[".concat(this.peer.type, "] <").concat(_event, "> listener off"));
488
+ this.debug(`[${this.peer.type}] <${_event}> listener off`);
481
489
  return this;
482
- };
483
- IOF.prototype.removeListeners = function (fn) {
490
+ }
491
+ removeListeners(fn) {
484
492
  // Clear all event listeners
485
493
  this.Events = {};
486
494
  typeof fn == 'function' && fn();
487
- this.debug("[".concat(this.peer.type, "] All listeners removed"));
495
+ this.debug(`[${this.peer.type}] All listeners removed`);
488
496
  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")));
497
+ }
498
+ emitAsync(_event, payload, timeout = 5000) {
499
+ return new Promise((resolve, reject) => {
500
+ const timeoutId = setTimeout(() => {
501
+ reject(new Error(`Event '${_event}' acknowledgment timeout after ${timeout}ms`));
496
502
  }, timeout);
497
503
  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
- }
504
+ this.emit(_event, payload, (error, ...args) => {
503
505
  clearTimeout(timeoutId);
504
506
  error
505
507
  ? reject(new Error(typeof error === 'string' ? error : 'Ack error'))
@@ -511,29 +513,27 @@ var IOF = /** @class */ (function () {
511
513
  reject(error);
512
514
  }
513
515
  });
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())
516
+ }
517
+ onceAsync(_event) {
518
+ return new Promise(resolve => this.once(_event, resolve));
519
+ }
520
+ connectAsync(timeout) {
521
+ return new Promise((resolve, reject) => {
522
+ if (this.isConnected())
523
523
  return resolve();
524
- var timeoutId = setTimeout(function () {
525
- _this.off('connect', connectHandler);
524
+ const timeoutId = setTimeout(() => {
525
+ this.off('connect', connectHandler);
526
526
  reject(new Error('Connection timeout'));
527
- }, timeout || _this.options.connectionTimeout);
528
- var connectHandler = function () {
527
+ }, timeout || this.options.connectionTimeout);
528
+ const connectHandler = () => {
529
529
  clearTimeout(timeoutId);
530
530
  resolve();
531
531
  };
532
- _this.once('connect', connectHandler);
532
+ this.once('connect', connectHandler);
533
533
  });
534
- };
534
+ }
535
535
  // Clean up all resources
536
- IOF.prototype.cleanup = function () {
536
+ cleanup() {
537
537
  if (this.messageListener) {
538
538
  window.removeEventListener('message', this.messageListener);
539
539
  this.messageListener = undefined;
@@ -543,8 +543,8 @@ var IOF = /** @class */ (function () {
543
543
  clearTimeout(this.reconnectTimer);
544
544
  this.reconnectTimer = undefined;
545
545
  }
546
- };
547
- IOF.prototype.disconnect = function (fn) {
546
+ }
547
+ disconnect(fn) {
548
548
  // Cleanup on disconnect
549
549
  this.cleanup();
550
550
  this.peer.connected = false;
@@ -556,11 +556,11 @@ var IOF = /** @class */ (function () {
556
556
  this.reconnectAttempts = 0;
557
557
  this.removeListeners();
558
558
  typeof fn == 'function' && fn();
559
- this.debug("[".concat(this.peer.type, "] Disconnected"));
559
+ this.debug(`[${this.peer.type}] Disconnected`);
560
560
  return this;
561
- };
561
+ }
562
562
  // Get connection statistics
563
- IOF.prototype.getStats = function () {
563
+ getStats() {
564
564
  return {
565
565
  connected: this.isConnected(),
566
566
  peerType: this.peer.type,
@@ -571,14 +571,13 @@ var IOF = /** @class */ (function () {
571
571
  activeListeners: Object.keys(this.Events).length,
572
572
  messageRate: this.messageRateTracker.length
573
573
  };
574
- };
574
+ }
575
575
  // Clear message queue manually
576
- IOF.prototype.clearQueue = function () {
577
- var queueSize = this.messageQueue.length;
576
+ clearQueue() {
577
+ const queueSize = this.messageQueue.length;
578
578
  this.messageQueue = [];
579
- this.debug("[".concat(this.peer.type, "] Cleared ").concat(queueSize, " queued messages"));
579
+ this.debug(`[${this.peer.type}] Cleared ${queueSize} queued messages`);
580
580
  return this;
581
- };
582
- return IOF;
583
- }());
581
+ }
582
+ }
584
583
  exports.default = IOF;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iframe.io",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Easy and friendly API to connect and interact between content window and its containing iframe",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -45,5 +45,5 @@
45
45
  "events",
46
46
  "io"
47
47
  ],
48
- "author": "Fabrice K.M.E"
48
+ "author": "Fabrice K.E.M"
49
49
  }
package/src/index.ts CHANGED
@@ -12,6 +12,16 @@ export type Options = {
12
12
  maxMessagesPerSecond?: number
13
13
  autoReconnect?: boolean
14
14
  messageQueueSize?: number
15
+ /**
16
+ * Optional allowlist of incoming application-level events.
17
+ * Reserved internal events (ping/pong/heartbeats) are always allowed.
18
+ */
19
+ allowedIncomingEvents?: string[]
20
+ /**
21
+ * Optional custom validator for incoming messages.
22
+ * Return false to drop a message; an 'error' event will be emitted.
23
+ */
24
+ validateIncoming?: ( event: string, payload: any, origin: string ) => boolean
15
25
  }
16
26
 
17
27
  export interface RegisteredEvents {
@@ -68,13 +78,32 @@ function sanitizePayload( payload: any, maxSize: number ): any {
68
78
  }
69
79
 
70
80
  const ackId = () => {
81
+ // Prefer cryptographically strong randomness when available
82
+ try {
83
+ const globalCrypto = (typeof crypto !== 'undefined'
84
+ ? crypto
85
+ : (typeof window !== 'undefined' && (window as any).crypto)
86
+ || (typeof globalThis !== 'undefined' && (globalThis as any).crypto))
87
+
88
+ if( globalCrypto && typeof globalCrypto.getRandomValues === 'function' ){
89
+ const buffer = new Uint32Array(4)
90
+ globalCrypto.getRandomValues( buffer )
91
+
92
+ const randomPart = Array.from( buffer ).map( n => n.toString( 16 ) ).join('')
93
+ return `${Date.now()}_${randomPart}`
94
+ }
95
+ }
96
+ catch{
97
+ // Fall back to Math.random-based implementation below
98
+ }
99
+
71
100
  const
72
101
  rmin = 100000,
73
102
  rmax = 999999,
74
- timestamp = Date.now(),
75
- random = Math.floor( Math.random() * ( rmax - rmin + 1 ) + rmin )
103
+ timestampFallback = Date.now(),
104
+ randomFallback = Math.floor( Math.random() * ( rmax - rmin + 1 ) + rmin )
76
105
 
77
- return `${timestamp}_${random}`
106
+ return `${timestampFallback}_${randomFallback}`
78
107
  }
79
108
 
80
109
  const RESERVED_EVENTS = [
@@ -89,8 +118,8 @@ export default class IOF {
89
118
  peer: Peer
90
119
  options: Options
91
120
  private messageListener?: ( event: MessageEvent ) => void
92
- private heartbeatTimer?: NodeJS.Timeout
93
- private reconnectTimer?: NodeJS.Timeout
121
+ private heartbeatTimer?: number
122
+ private reconnectTimer?: number
94
123
  private messageQueue: QueuedMessage[] = []
95
124
  private messageRateTracker: number[] = []
96
125
  private reconnectAttempts: number = 0
@@ -321,6 +350,31 @@ export default class IOF {
321
350
  return
322
351
  }
323
352
 
353
+ // Optional application-level incoming validation (non-reserved events only)
354
+ if( !RESERVED_EVENTS.includes( _event ) ){
355
+ if( this.options.allowedIncomingEvents
356
+ && !this.options.allowedIncomingEvents.includes( _event ) ){
357
+ this.fire('error', {
358
+ type: 'DISALLOWED_EVENT',
359
+ direction: 'incoming',
360
+ event: _event,
361
+ origin
362
+ })
363
+ return
364
+ }
365
+
366
+ if( this.options.validateIncoming
367
+ && !this.options.validateIncoming( _event, payload, origin ) ){
368
+ this.fire('error', {
369
+ type: 'INVALID_MESSAGE',
370
+ direction: 'incoming',
371
+ event: _event,
372
+ origin
373
+ })
374
+ return
375
+ }
376
+ }
377
+
324
378
  // Fire available event listeners
325
379
  this.fire( _event, payload, cid )
326
380
  }