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 +28 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.js +227 -228
- package/package.json +2 -2
- package/src/index.ts +59 -5
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
|
|
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
|
|
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
|
-
|
|
17
|
+
const size = getMessageSize(payload);
|
|
38
18
|
if (size > maxSize)
|
|
39
|
-
throw new Error(
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
43
|
+
const RESERVED_EVENTS = [
|
|
48
44
|
'ping',
|
|
49
45
|
'pong',
|
|
50
46
|
'__heartbeat',
|
|
51
47
|
'__heartbeat_response'
|
|
52
48
|
];
|
|
53
|
-
|
|
54
|
-
|
|
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 =
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
80
|
-
var _this = this;
|
|
79
|
+
startHeartbeat() {
|
|
81
80
|
if (!this.options.heartbeatInterval)
|
|
82
81
|
return;
|
|
83
|
-
this.heartbeatTimer = setInterval(
|
|
84
|
-
if (
|
|
85
|
-
|
|
82
|
+
this.heartbeatTimer = setInterval(() => {
|
|
83
|
+
if (this.isConnected()) {
|
|
84
|
+
const now = Date.now();
|
|
86
85
|
// Check if peer is still responsive
|
|
87
|
-
if (
|
|
88
|
-
&& (now -
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
94
|
+
this.emit('__heartbeat', { timestamp: now });
|
|
96
95
|
}
|
|
97
96
|
catch (error) {
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
this.debug(`[${this.peer.type}] Heartbeat send failed:`, error);
|
|
98
|
+
this.handleConnectionLoss();
|
|
100
99
|
}
|
|
101
100
|
}
|
|
102
101
|
}, this.options.heartbeatInterval);
|
|
103
|
-
}
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
var _this = this;
|
|
119
|
+
}
|
|
120
|
+
attemptReconnection() {
|
|
123
121
|
if (this.reconnectTimer)
|
|
124
122
|
return;
|
|
125
123
|
this.reconnectAttempts++;
|
|
126
|
-
|
|
127
|
-
this.debug(
|
|
128
|
-
this.fire('reconnecting', { attempt: this.reconnectAttempts, delay
|
|
129
|
-
this.reconnectTimer = setTimeout(
|
|
130
|
-
|
|
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
|
-
|
|
133
|
-
&&
|
|
134
|
-
&&
|
|
135
|
-
&&
|
|
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(
|
|
139
|
-
if (
|
|
136
|
+
setTimeout(() => {
|
|
137
|
+
if (this.peer.connected)
|
|
140
138
|
return;
|
|
141
|
-
|
|
142
|
-
?
|
|
143
|
-
:
|
|
144
|
-
},
|
|
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
|
-
|
|
146
|
+
checkRateLimit() {
|
|
149
147
|
if (!this.options.maxMessagesPerSecond)
|
|
150
148
|
return true;
|
|
151
|
-
|
|
149
|
+
const now = Date.now(), aSecondAgo = now - 1000;
|
|
152
150
|
// Clean old entries
|
|
153
|
-
this.messageRateTracker = this.messageRateTracker.filter(
|
|
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
|
-
|
|
165
|
+
queueMessage(_event, payload, fn) {
|
|
168
166
|
if (this.messageQueue.length >= this.options.messageQueueSize) {
|
|
169
167
|
// Remove oldest message
|
|
170
|
-
|
|
171
|
-
this.debug(
|
|
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
|
|
175
|
-
payload
|
|
176
|
-
fn
|
|
172
|
+
_event,
|
|
173
|
+
payload,
|
|
174
|
+
fn,
|
|
177
175
|
timestamp: Date.now()
|
|
178
176
|
});
|
|
179
|
-
this.debug(
|
|
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
|
-
|
|
183
|
-
var _this = this;
|
|
180
|
+
processMessageQueue() {
|
|
184
181
|
if (!this.isConnected() || this.messageQueue.length === 0)
|
|
185
182
|
return;
|
|
186
|
-
this.debug(
|
|
187
|
-
|
|
183
|
+
this.debug(`[${this.peer.type}] Processing ${this.messageQueue.length} queued messages`);
|
|
184
|
+
const queue = [...this.messageQueue];
|
|
188
185
|
this.messageQueue = [];
|
|
189
|
-
queue.forEach(
|
|
186
|
+
queue.forEach(message => {
|
|
190
187
|
try {
|
|
191
|
-
|
|
188
|
+
this.emit(message._event, message.payload, message.fn);
|
|
192
189
|
}
|
|
193
190
|
catch (error) {
|
|
194
|
-
|
|
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
|
-
|
|
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 =
|
|
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 !==
|
|
213
|
+
if (origin !== this.peer.origin
|
|
219
214
|
|| !source
|
|
220
215
|
|| typeof data !== 'object'
|
|
221
216
|
|| !data.hasOwnProperty('_event'))
|
|
222
217
|
return;
|
|
223
|
-
|
|
218
|
+
const { _event, payload, cid, timestamp } = data;
|
|
224
219
|
// Handle heartbeat responses
|
|
225
220
|
if (_event === '__heartbeat_response') {
|
|
226
|
-
|
|
221
|
+
this.peer.lastHeartbeat = Date.now();
|
|
227
222
|
return;
|
|
228
223
|
}
|
|
229
224
|
// Handle heartbeat requests
|
|
230
225
|
if (_event === '__heartbeat') {
|
|
231
|
-
|
|
232
|
-
|
|
226
|
+
this.emit('__heartbeat_response', { timestamp: Date.now() });
|
|
227
|
+
this.peer.lastHeartbeat = Date.now();
|
|
233
228
|
return;
|
|
234
229
|
}
|
|
235
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
267
|
+
this.fire(_event, payload, cid);
|
|
250
268
|
}
|
|
251
269
|
catch (error) {
|
|
252
|
-
|
|
253
|
-
|
|
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
|
|
274
|
+
origin
|
|
257
275
|
});
|
|
258
276
|
}
|
|
259
277
|
};
|
|
260
278
|
window.addEventListener('message', this.messageListener, false);
|
|
261
|
-
this.debug(
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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 (!
|
|
295
|
-
|
|
296
|
-
|
|
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 !==
|
|
300
|
-
|
|
315
|
+
else if (origin !== this.peer.origin) {
|
|
316
|
+
this.fire('error', {
|
|
301
317
|
type: 'ORIGIN_MISMATCH',
|
|
302
|
-
expected:
|
|
318
|
+
expected: this.peer.origin,
|
|
303
319
|
received: origin
|
|
304
320
|
});
|
|
305
321
|
return;
|
|
306
322
|
}
|
|
307
|
-
|
|
323
|
+
const { _event, payload, cid, timestamp } = data;
|
|
308
324
|
// Handle heartbeat responses
|
|
309
325
|
if (_event === '__heartbeat_response') {
|
|
310
|
-
|
|
326
|
+
this.peer.lastHeartbeat = Date.now();
|
|
311
327
|
return;
|
|
312
328
|
}
|
|
313
329
|
// Handle heartbeat requests
|
|
314
330
|
if (_event === '__heartbeat') {
|
|
315
|
-
|
|
316
|
-
|
|
331
|
+
this.emit('__heartbeat_response', { timestamp: Date.now() });
|
|
332
|
+
this.peer.lastHeartbeat = Date.now();
|
|
317
333
|
return;
|
|
318
334
|
}
|
|
319
|
-
|
|
335
|
+
this.debug(`[${this.peer.type}] Message: ${_event}`, payload || '');
|
|
320
336
|
// Handshake or availability check events
|
|
321
337
|
if (_event == 'ping') {
|
|
322
|
-
|
|
338
|
+
this.emit('pong');
|
|
323
339
|
// Iframe is connected to content window
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
350
|
+
this.fire(_event, payload, cid);
|
|
335
351
|
}
|
|
336
352
|
catch (error) {
|
|
337
|
-
|
|
338
|
-
|
|
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
|
|
357
|
+
origin
|
|
342
358
|
});
|
|
343
359
|
}
|
|
344
360
|
};
|
|
345
361
|
window.addEventListener('message', this.messageListener, false);
|
|
346
362
|
return this;
|
|
347
|
-
}
|
|
348
|
-
|
|
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(
|
|
367
|
+
this.debug(`[${this.peer.type}] No <${_event}> listener defined`);
|
|
353
368
|
return;
|
|
354
369
|
}
|
|
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 });
|
|
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
|
-
|
|
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(
|
|
387
|
+
listeners.forEach(fn => {
|
|
377
388
|
try {
|
|
378
389
|
payload !== undefined ? fn(payload, ackFn) : fn(ackFn);
|
|
379
390
|
}
|
|
380
391
|
catch (error) {
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
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
|
-
|
|
423
|
+
const sanitizedPayload = payload
|
|
413
424
|
? sanitizePayload(payload, this.options.maxMessageSize)
|
|
414
425
|
: payload;
|
|
415
426
|
// Acknowledge event listener
|
|
416
|
-
|
|
427
|
+
let cid;
|
|
417
428
|
if (typeof fn === 'function') {
|
|
418
|
-
|
|
429
|
+
const ackFunction = fn;
|
|
419
430
|
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
|
-
});
|
|
431
|
+
this.once(`${_event}--${cid}--@ack`, ({ error, args }) => ackFunction(error, ...args));
|
|
424
432
|
}
|
|
425
|
-
|
|
426
|
-
_event
|
|
433
|
+
const messageData = {
|
|
434
|
+
_event,
|
|
427
435
|
payload: sanitizedPayload,
|
|
428
|
-
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(
|
|
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
|
-
|
|
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(
|
|
460
|
+
this.debug(`[${this.peer.type}] New <${_event}> listener on`);
|
|
453
461
|
return this;
|
|
454
|
-
}
|
|
455
|
-
|
|
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(
|
|
469
|
+
this.debug(`[${this.peer.type}] New <${_event} once> listener on`);
|
|
462
470
|
return this;
|
|
463
|
-
}
|
|
464
|
-
|
|
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
|
-
|
|
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(
|
|
488
|
+
this.debug(`[${this.peer.type}] <${_event}> listener off`);
|
|
481
489
|
return this;
|
|
482
|
-
}
|
|
483
|
-
|
|
490
|
+
}
|
|
491
|
+
removeListeners(fn) {
|
|
484
492
|
// Clear all event listeners
|
|
485
493
|
this.Events = {};
|
|
486
494
|
typeof fn == 'function' && fn();
|
|
487
|
-
this.debug(
|
|
495
|
+
this.debug(`[${this.peer.type}] All listeners removed`);
|
|
488
496
|
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")));
|
|
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
|
-
|
|
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
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
525
|
-
|
|
524
|
+
const timeoutId = setTimeout(() => {
|
|
525
|
+
this.off('connect', connectHandler);
|
|
526
526
|
reject(new Error('Connection timeout'));
|
|
527
|
-
}, timeout ||
|
|
528
|
-
|
|
527
|
+
}, timeout || this.options.connectionTimeout);
|
|
528
|
+
const connectHandler = () => {
|
|
529
529
|
clearTimeout(timeoutId);
|
|
530
530
|
resolve();
|
|
531
531
|
};
|
|
532
|
-
|
|
532
|
+
this.once('connect', connectHandler);
|
|
533
533
|
});
|
|
534
|
-
}
|
|
534
|
+
}
|
|
535
535
|
// Clean up all resources
|
|
536
|
-
|
|
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
|
-
|
|
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(
|
|
559
|
+
this.debug(`[${this.peer.type}] Disconnected`);
|
|
560
560
|
return this;
|
|
561
|
-
}
|
|
561
|
+
}
|
|
562
562
|
// Get connection statistics
|
|
563
|
-
|
|
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
|
-
|
|
577
|
-
|
|
576
|
+
clearQueue() {
|
|
577
|
+
const queueSize = this.messageQueue.length;
|
|
578
578
|
this.messageQueue = [];
|
|
579
|
-
this.debug(
|
|
579
|
+
this.debug(`[${this.peer.type}] Cleared ${queueSize} queued messages`);
|
|
580
580
|
return this;
|
|
581
|
-
}
|
|
582
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
75
|
-
|
|
103
|
+
timestampFallback = Date.now(),
|
|
104
|
+
randomFallback = Math.floor( Math.random() * ( rmax - rmin + 1 ) + rmin )
|
|
76
105
|
|
|
77
|
-
return `${
|
|
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?:
|
|
93
|
-
private reconnectTimer?:
|
|
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
|
}
|