webview.io 1.0.6 → 1.1.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 +71 -18
- package/dist/index.d.ts +10 -0
- package/dist/index.js +38 -1
- package/package.json +1 -1
- package/src/index.ts +68 -16
package/README.md
CHANGED
|
@@ -106,9 +106,11 @@ function MapComponent() {
|
|
|
106
106
|
|
|
107
107
|
```javascript
|
|
108
108
|
// In your HTML/JS loaded in the WebView
|
|
109
|
+
// The bridge is automatically available as window._wio after injection
|
|
109
110
|
|
|
110
|
-
//
|
|
111
|
+
// IMPORTANT: You must manually call listen() to start the connection
|
|
111
112
|
const wio = window._wio
|
|
113
|
+
wio.listen() // Required to initiate connection handshake
|
|
112
114
|
|
|
113
115
|
// Handle connection
|
|
114
116
|
wio.on('connect', () => {
|
|
@@ -140,7 +142,9 @@ const wio = new WIO({
|
|
|
140
142
|
maxMessageSize: 1024 * 1024, // Max message size in bytes (1MB)
|
|
141
143
|
maxMessagesPerSecond: 100, // Rate limit (100 messages/second)
|
|
142
144
|
autoReconnect: true, // Enable automatic reconnection
|
|
143
|
-
messageQueueSize: 50
|
|
145
|
+
messageQueueSize: 50, // Max queued messages when disconnected
|
|
146
|
+
allowedIncomingEvents: ['hello', 'response'], // Optional incoming event allowlist (non-reserved events)
|
|
147
|
+
validateIncoming: (event, payload) => true // Optional custom incoming validator
|
|
144
148
|
})
|
|
145
149
|
```
|
|
146
150
|
|
|
@@ -180,12 +184,28 @@ This prevents:
|
|
|
180
184
|
- Fires `connect_timeout` event if all attempts fail
|
|
181
185
|
|
|
182
186
|
**EMBEDDED Side:**
|
|
183
|
-
-
|
|
187
|
+
- Bridge is available as `window._wio` after injection
|
|
188
|
+
- **Must manually call `window._wio.listen()` to start connection**
|
|
189
|
+
- Automatically announces `__embedded_ready` when `listen()` is called
|
|
184
190
|
- Retries announcement every 2 seconds until connected
|
|
185
191
|
- Handles race condition if loaded before React Native
|
|
186
192
|
- Responds to `ping` with `pong` containing same token
|
|
187
193
|
- Confirms connection upon receiving `__connection_ack`
|
|
188
194
|
|
|
195
|
+
## Reserved Events
|
|
196
|
+
|
|
197
|
+
The library uses several internal events for connection management. These events are handled automatically and **bypass normal message queuing** to ensure reliable connection establishment:
|
|
198
|
+
|
|
199
|
+
- **`ping`** - Initial connection request from WEBVIEW
|
|
200
|
+
- **`pong`** - Connection response from EMBEDDED
|
|
201
|
+
- **`__heartbeat`** - Periodic health check request
|
|
202
|
+
- **`__heartbeat_response`** - Health check response
|
|
203
|
+
- **`__embedded_ready`** - EMBEDDED announces it's ready to connect
|
|
204
|
+
- **`__connection_ack`** - Final handshake acknowledgment from WEBVIEW
|
|
205
|
+
- **`__webview_ready`** - Signal that WEBVIEW peer is ready (currently logged but not actively used)
|
|
206
|
+
|
|
207
|
+
**Important:** These events are sent immediately even when disconnected, unlike regular events which are queued. Do not use these event names for your application logic.
|
|
208
|
+
|
|
189
209
|
## Async/Await Support
|
|
190
210
|
|
|
191
211
|
### Send Messages with Acknowledgments
|
|
@@ -231,7 +251,7 @@ console.log('User data received:', userData)
|
|
|
231
251
|
### Connection Timeout Handling
|
|
232
252
|
|
|
233
253
|
```javascript
|
|
234
|
-
// Handle connection timeout
|
|
254
|
+
// Handle connection timeout
|
|
235
255
|
wio.on('connect_timeout', ({ attempts }) => {
|
|
236
256
|
console.log(`Failed to connect after ${attempts} attempts`)
|
|
237
257
|
// Optionally retry manually or show error to user
|
|
@@ -267,13 +287,13 @@ const stats = wio.getStats()
|
|
|
267
287
|
console.log(stats)
|
|
268
288
|
// {
|
|
269
289
|
// connected: true,
|
|
270
|
-
// embeddedReady: true, //
|
|
290
|
+
// embeddedReady: true, // EMBEDDED peer readiness state
|
|
271
291
|
// peerType: 'WEBVIEW',
|
|
272
292
|
// origin: 'https://example.com',
|
|
273
293
|
// lastHeartbeat: 1609459200000,
|
|
274
294
|
// queuedMessages: 0,
|
|
275
295
|
// reconnectAttempts: 0,
|
|
276
|
-
// connectionAttempts: 0, //
|
|
296
|
+
// connectionAttempts: 0, // Current connection attempts
|
|
277
297
|
// activeListeners: 5,
|
|
278
298
|
// messageRate: 2
|
|
279
299
|
// }
|
|
@@ -288,7 +308,7 @@ Each connection uses a unique token for validation:
|
|
|
288
308
|
```javascript
|
|
289
309
|
// Automatically generated and validated during handshake
|
|
290
310
|
// Prevents accepting messages from previous connections
|
|
291
|
-
// Tokens are only used for connection
|
|
311
|
+
// Tokens are only used for reserved connection events
|
|
292
312
|
```
|
|
293
313
|
|
|
294
314
|
### Origin Validation
|
|
@@ -312,6 +332,29 @@ wio.emit('data', {
|
|
|
312
332
|
})
|
|
313
333
|
```
|
|
314
334
|
|
|
335
|
+
### Incoming Event Allowlist & Validation
|
|
336
|
+
|
|
337
|
+
For defense-in-depth, you can restrict which **application-level** events are accepted and/or validate incoming payloads. Reserved internal events (handshake/heartbeat/readiness) are always allowed.
|
|
338
|
+
|
|
339
|
+
```javascript
|
|
340
|
+
const wio = new WIO({
|
|
341
|
+
type: 'WEBVIEW',
|
|
342
|
+
debug: true,
|
|
343
|
+
allowedIncomingEvents: ['get:location', 'location:picked'],
|
|
344
|
+
validateIncoming: (event, payload) => {
|
|
345
|
+
// Example: simple checks
|
|
346
|
+
if (event === 'location:picked') return payload && typeof payload.lat === 'number'
|
|
347
|
+
return true
|
|
348
|
+
}
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
wio.on('error', (error) => {
|
|
352
|
+
if (error.type === 'DISALLOWED_EVENT' || error.type === 'INVALID_MESSAGE') {
|
|
353
|
+
console.warn('Dropped incoming message:', error)
|
|
354
|
+
}
|
|
355
|
+
})
|
|
356
|
+
```
|
|
357
|
+
|
|
315
358
|
### Rate Limiting
|
|
316
359
|
|
|
317
360
|
```javascript
|
|
@@ -356,6 +399,7 @@ wio.on('error', (error) => {
|
|
|
356
399
|
|
|
357
400
|
```javascript
|
|
358
401
|
// Messages are automatically queued when disconnected
|
|
402
|
+
// Reserved events bypass the queue for reliable connection handling
|
|
359
403
|
wio.emit('important-data', { data: 'This will be queued if disconnected' })
|
|
360
404
|
|
|
361
405
|
// Clear queue manually if needed
|
|
@@ -382,7 +426,7 @@ console.log(`${stats.queuedMessages} messages queued`)
|
|
|
382
426
|
|
|
383
427
|
#### Connection Methods
|
|
384
428
|
- **`initiate(webViewRef, origin)`** - Establish connection with retry logic (WEBVIEW peer only)
|
|
385
|
-
- **`listen(hostOrigin?)`** -
|
|
429
|
+
- **`listen(hostOrigin?)`** - Start listening for connection (EMBEDDED peer only). The `hostOrigin` parameter is informational only and used for logging - no validation is performed.
|
|
386
430
|
- **`handleMessage(event)`** - Handle incoming message from WebView
|
|
387
431
|
- **`disconnect(callback?)`** - Disconnect and cleanup all resources
|
|
388
432
|
- **`isConnected()`** - Check connection status
|
|
@@ -399,15 +443,19 @@ console.log(`${stats.queuedMessages} messages queued`)
|
|
|
399
443
|
#### Connection Events
|
|
400
444
|
- **`connect`** - Connection established (after 3-way handshake)
|
|
401
445
|
- **`disconnect`** - Connection lost with reason
|
|
402
|
-
- **`connect_timeout`** - Initial connection failed after all attempts
|
|
446
|
+
- **`connect_timeout`** - Initial connection failed after all attempts
|
|
403
447
|
- **`reconnecting`** - Reconnection attempt started
|
|
404
448
|
- **`reconnection_failed`** - All reconnection attempts failed
|
|
405
449
|
|
|
406
|
-
#### Internal Events
|
|
450
|
+
#### Reserved Internal Events
|
|
451
|
+
These events are handled automatically by the library. Do not use these names for your application events:
|
|
452
|
+
- **`ping`** - Connection initiation from WEBVIEW
|
|
453
|
+
- **`pong`** - Connection response from EMBEDDED
|
|
407
454
|
- **`__embedded_ready`** - EMBEDDED peer announces readiness
|
|
408
455
|
- **`__connection_ack`** - Final acknowledgment in 3-way handshake
|
|
409
|
-
- **`__heartbeat`** - Connection health check
|
|
456
|
+
- **`__heartbeat`** - Connection health check request
|
|
410
457
|
- **`__heartbeat_response`** - Heartbeat response
|
|
458
|
+
- **`__webview_ready`** - WEBVIEW ready signal (informational)
|
|
411
459
|
|
|
412
460
|
#### Error Events
|
|
413
461
|
- **`error`** - Various error conditions with detailed error objects
|
|
@@ -575,6 +623,8 @@ const response = await wio.emitAsync<{ query: string }, ApiResponse>(
|
|
|
575
623
|
| `MESSAGE_HANDLING_ERROR` | Error processing incoming message |
|
|
576
624
|
| `EMIT_ERROR` | Error sending message |
|
|
577
625
|
| `LISTENER_ERROR` | Error in event listener |
|
|
626
|
+
| `DISALLOWED_EVENT` | Incoming event rejected by `allowedIncomingEvents` |
|
|
627
|
+
| `INVALID_MESSAGE` | Incoming message rejected by `validateIncoming` |
|
|
578
628
|
| `RATE_LIMIT_EXCEEDED` | Too many messages sent |
|
|
579
629
|
| `NO_CONNECTION` | Attempted to send without connection |
|
|
580
630
|
|
|
@@ -600,12 +650,13 @@ const response = await wio.emitAsync<{ query: string }, ApiResponse>(
|
|
|
600
650
|
**Symptoms:** `connect_timeout` event fires, connection never succeeds
|
|
601
651
|
|
|
602
652
|
**Solutions:**
|
|
603
|
-
1.
|
|
604
|
-
2.
|
|
605
|
-
3.
|
|
606
|
-
4.
|
|
607
|
-
5.
|
|
608
|
-
6.
|
|
653
|
+
1. **Verify `listen()` is called** - The EMBEDDED side must manually call `window._wio.listen()` to start the connection
|
|
654
|
+
2. Check that `injectedJavaScript` is properly set on WebView
|
|
655
|
+
3. Verify `javaScriptEnabled={true}` is set
|
|
656
|
+
4. Check browser console for JavaScript errors in WebView
|
|
657
|
+
5. Ensure origin matches exactly (including protocol)
|
|
658
|
+
6. Increase `connectionTimeout` for slow networks
|
|
659
|
+
7. Check that WebView content loads successfully
|
|
609
660
|
|
|
610
661
|
### Messages Not Received
|
|
611
662
|
|
|
@@ -617,6 +668,7 @@ const response = await wio.emitAsync<{ query: string }, ApiResponse>(
|
|
|
617
668
|
3. Look for errors in `error` event listener
|
|
618
669
|
4. Check if rate limiting is being exceeded
|
|
619
670
|
5. Verify message size is under `maxMessageSize`
|
|
671
|
+
6. Ensure you're not using reserved event names
|
|
620
672
|
|
|
621
673
|
### Frequent Disconnections
|
|
622
674
|
|
|
@@ -637,7 +689,7 @@ const response = await wio.emitAsync<{ query: string }, ApiResponse>(
|
|
|
637
689
|
- Already handled! The three-way handshake with readiness announcements prevents this
|
|
638
690
|
- EMBEDDED announces readiness periodically until connected
|
|
639
691
|
- WEBVIEW retries connection attempts automatically
|
|
640
|
-
-
|
|
692
|
+
- Just ensure `window._wio.listen()` is called in your WebView code
|
|
641
693
|
|
|
642
694
|
## Differences from iframe.io
|
|
643
695
|
|
|
@@ -647,6 +699,7 @@ const response = await wio.emitAsync<{ query: string }, ApiResponse>(
|
|
|
647
699
|
- **Initialization**: Uses `RefObject<WebView>` instead of `Window` object
|
|
648
700
|
- **Message Handling**: Requires explicit `handleMessage()` call in `onMessage` prop
|
|
649
701
|
- **Injected Script**: Uses `getInjectedJavaScript()` to setup bridge in WebView
|
|
702
|
+
- **Manual Connection**: EMBEDDED side must call `listen()` to start connection
|
|
650
703
|
- **No DOM Dependencies**: Works in React Native environment without DOM APIs
|
|
651
704
|
- **Enhanced Handshake**: Three-way handshake with token validation for mobile reliability
|
|
652
705
|
- **Connection Retry**: Built-in retry logic for spotty mobile connections
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,16 @@ export type Options = {
|
|
|
14
14
|
messageQueueSize?: number;
|
|
15
15
|
connectionPingInterval?: number;
|
|
16
16
|
maxConnectionAttempts?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Optional allowlist of incoming application-level events.
|
|
19
|
+
* Reserved internal events (ping/pong/heartbeat/handshake) are always allowed.
|
|
20
|
+
*/
|
|
21
|
+
allowedIncomingEvents?: string[];
|
|
22
|
+
/**
|
|
23
|
+
* Optional custom validator for incoming messages.
|
|
24
|
+
* Return false to drop a message; an 'error' event will be emitted.
|
|
25
|
+
*/
|
|
26
|
+
validateIncoming?: (event: string, payload: any) => boolean;
|
|
17
27
|
};
|
|
18
28
|
export interface RegisteredEvents {
|
|
19
29
|
[index: string]: Listener[];
|
package/dist/index.js
CHANGED
|
@@ -45,6 +45,22 @@ var ackId = function () {
|
|
|
45
45
|
return "".concat(timestamp, "_").concat(random);
|
|
46
46
|
};
|
|
47
47
|
var generateToken = function () {
|
|
48
|
+
// Prefer cryptographically strong randomness when available
|
|
49
|
+
try {
|
|
50
|
+
var globalCrypto = (typeof crypto !== 'undefined'
|
|
51
|
+
? crypto
|
|
52
|
+
: (typeof window !== 'undefined' && window.crypto)
|
|
53
|
+
|| (typeof globalThis !== 'undefined' && globalThis.crypto));
|
|
54
|
+
if (globalCrypto && typeof globalCrypto.getRandomValues === 'function') {
|
|
55
|
+
var buffer = new Uint32Array(4);
|
|
56
|
+
globalCrypto.getRandomValues(buffer);
|
|
57
|
+
var randomPart = Array.from(buffer).map(function (n) { return n.toString(16); }).join('');
|
|
58
|
+
return "".concat(Date.now(), "_").concat(randomPart);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (_a) {
|
|
62
|
+
// Fall back to Math.random-based implementation below
|
|
63
|
+
}
|
|
48
64
|
return "".concat(Date.now(), "_").concat(Math.random().toString(36).substring(2, 15));
|
|
49
65
|
};
|
|
50
66
|
var RESERVED_EVENTS = [
|
|
@@ -419,6 +435,27 @@ var WIO = /** @class */ (function () {
|
|
|
419
435
|
}
|
|
420
436
|
return;
|
|
421
437
|
}
|
|
438
|
+
// Optional application-level incoming validation (non-reserved events only)
|
|
439
|
+
if (!RESERVED_EVENTS.includes(_event)) {
|
|
440
|
+
if (this.options.allowedIncomingEvents
|
|
441
|
+
&& !this.options.allowedIncomingEvents.includes(_event)) {
|
|
442
|
+
this.fire('error', {
|
|
443
|
+
type: 'DISALLOWED_EVENT',
|
|
444
|
+
direction: 'incoming',
|
|
445
|
+
event: _event
|
|
446
|
+
});
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (this.options.validateIncoming
|
|
450
|
+
&& !this.options.validateIncoming(_event, payload)) {
|
|
451
|
+
this.fire('error', {
|
|
452
|
+
type: 'INVALID_MESSAGE',
|
|
453
|
+
direction: 'incoming',
|
|
454
|
+
event: _event
|
|
455
|
+
});
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
422
459
|
// Fire available event listeners
|
|
423
460
|
this.fire(_event, payload, cid);
|
|
424
461
|
}
|
|
@@ -673,7 +710,7 @@ var WIO = /** @class */ (function () {
|
|
|
673
710
|
* NOTE: Does not auto-initialize - page must call window._wio.listen()
|
|
674
711
|
*/
|
|
675
712
|
WIO.prototype.getInjectedJavaScript = function () {
|
|
676
|
-
return "\n (function() {\n try {\n console.log('[EMBEDDED] Initializing WIO bridge...')\n \n const RESERVED_EVENTS = [\n 'ping',\n 'pong',\n '__heartbeat',\n '__heartbeat_response',\n '__embedded_ready',\n '__connection_ack',\n '__webview_ready'\n ]\n \n // Use closure variable to avoid 'this' binding issues\n window._wio = {\n type: 'EMBEDDED',\n connected: false,\n Events: {},\n messageQueue: [],\n connectionToken: null,\n setupComplete: false,\n\n listen: function(){\n if( this.setupComplete ){\n console.warn('[EMBEDDED] Already listening')\n return this\n }\n\n console.log('[EMBEDDED] Setting up message listeners...')\n\n // Listen to messages from React Native\n window.addEventListener('message', function( event ){\n try {\n const message = typeof event.data === 'string' ? JSON.parse( event.data ) : event.data\n window._wio.handleMessage( message )\n }\n catch( error ){ console.error('[EMBEDDED] Parse error:', error ) }\n })\n \n // Android support\n if( typeof document !== 'undefined' ){\n document.addEventListener('message', function( event ){\n try {\n const message = typeof event.data === 'string' ? JSON.parse( event.data ) : event.data\n window._wio.handleMessage( message )\n }\n catch( error ){ console.error('[EMBEDDED] Parse error:', error ) }\n })\n }\n\n this.setupComplete = true\n console.log('[EMBEDDED] Setup complete, starting ready announcements')\n\n // Start announcing readiness\n this.announceReady()\n\n return this\n },\n \n ackId: function(){\n const\n rmin = 100000,\n rmax = 999999,\n timestamp = Date.now(),\n random = Math.floor( Math.random() * ( rmax - rmin + 1 ) + rmin )\n\n return timestamp + '_' + random\n },\n \n fire: function( _event, payload, cid ){\n if( !window._wio.Events[_event] && !window._wio.Events[_event + '--@once'] ){\n console.log('[EMBEDDED] No listener for:', _event)\n return\n }\n \n const ackFn = cid\n ? ( error, ...args ) => window._wio.emit( _event + '--' + cid + '--@ack', { error: error || false, args } )\n : undefined\n \n let listeners = []\n if( window._wio.Events[_event + '--@once'] ){\n _event += '--@once'\n listeners = window._wio.Events[_event]\n\n delete window._wio.Events[_event]\n }\n else listeners = window._wio.Events[_event] || []\n \n listeners.forEach( fn => {\n try { payload !== undefined ? fn( payload, ackFn ) : fn( ackFn ) }\n catch( error ){ console.error('[EMBEDDED] Listener error:', error ) }\n })\n },\n \n emit: function( _event, payload, fn ){\n if( typeof payload === 'function' ){\n fn = payload\n payload = undefined\n }\n \n if( !window._wio.connected && !RESERVED_EVENTS.includes(_event) ){\n window._wio.messageQueue.push({ _event, payload, fn, timestamp: Date.now() })\n console.log('[EMBEDDED] Queued message:', _event)\n return\n }\n \n try {\n let cid\n if( typeof fn === 'function' ){\n cid = window._wio.ackId()\n window._wio.once( _event + '--' + cid + '--@ack', ({ error, args }) => fn( error, ...args ) )\n }\n \n const messageData = {\n _event,\n payload,\n cid,\n timestamp: Date.now(),\n token: RESERVED_EVENTS.includes(_event) ? window._wio.connectionToken : undefined\n }\n \n if( typeof window.ReactNativeWebView !== 'undefined' )\n window.ReactNativeWebView.postMessage( JSON.stringify( messageData ) )\n else console.error('[EMBEDDED] ReactNativeWebView not available')\n }\n catch( error ){\n console.error('[EMBEDDED] Emit error:', error )\n typeof fn === 'function' && fn( String(error) )\n }\n },\n \n on: function( _event, fn ){\n if( !window._wio.Events[_event] ) window._wio.Events[_event] = []\n window._wio.Events[_event].push( fn )\n\n return window._wio\n },\n \n once: function( _event, fn ){\n _event += '--@once'\n if( !window._wio.Events[_event] ) window._wio.Events[_event] = []\n\n window._wio.Events[_event].push( fn )\n\n return window._wio\n },\n \n off: function( _event, fn ){\n if( fn && window._wio.Events[_event] ){\n const index = window._wio.Events[_event].indexOf( fn )\n if( index > -1 ){\n window._wio.Events[_event].splice( index, 1 )\n if( window._wio.Events[_event].length === 0 ) delete window._wio.Events[_event]\n }\n }\n else delete window._wio.Events[_event]\n\n return window._wio\n },\n \n processMessageQueue: function(){\n if( !window._wio.connected || window._wio.messageQueue.length === 0 ) return\n \n console.log('[EMBEDDED] Processing', window._wio.messageQueue.length, 'queued messages')\n const queue = [ ...window._wio.messageQueue ]\n window._wio.messageQueue = []\n \n queue.forEach( msg => {\n try { window._wio.emit( msg._event, msg.payload, msg.fn ) }\n catch( error ){ console.error('[EMBEDDED] Queue process error:', error ) }\n })\n },\n \n handleMessage: function( data ){\n if( !data || !data._event ) return\n \n const { _event, payload, cid, token } = data\n \n console.log('[EMBEDDED] Received:', _event )\n \n // Handle heartbeat response\n if( _event === '__heartbeat_response' )\n return\n \n // Handle heartbeat request\n if( _event === '__heartbeat' ){\n window._wio.emit('__heartbeat_response', { timestamp: Date.now() })\n return\n }\n \n // Handle webview ready signal\n if( _event === '__webview_ready' ){\n console.log('[EMBEDDED] WebView ready signal received')\n return\n }\n \n // Handle ping from WEBVIEW\n if( _event === 'ping' ){\n console.log('[EMBEDDED] Received ping, sending pong')\n window._wio.connectionToken = token\n\n window._wio.emit('pong', { token: window._wio.connectionToken })\n return\n }\n \n // Handle connection acknowledgment\n if( _event === '__connection_ack' ){\n if( token && token !== window._wio.connectionToken ){\n console.error('[EMBEDDED] Invalid connection token in ack')\n return\n }\n \n console.log('[EMBEDDED] Connection established (received ack)')\n\n window._wio.connected = true\n window._wio.processMessageQueue()\n window._wio.fire('connect')\n\n return\n }\n \n // Fire event listeners\n window._wio.fire( _event, payload, cid )\n },\n \n announceReady: function(){\n let attempts = 0\n const maxAttempts = 10\n const interval = 1000\n \n console.log('[EMBEDDED] Starting ready announcements')\n \n const announce = () => {\n if( window._wio.connected ){\n console.log('[EMBEDDED] Connected, stopping announcements')\n return\n }\n \n attempts++\n if( attempts > maxAttempts ){\n console.log('[EMBEDDED] Max announcement attempts reached')\n return\n }\n \n console.log('[EMBEDDED] Announcing ready (attempt', attempts + '/' + maxAttempts + ')')\n window._wio.emit('__embedded_ready')\n \n setTimeout( announce, interval )\n }\n \n announce()\n }\n }\n\n console.log('[EMBEDDED] WIO bridge ready. Call window._wio.listen() to connect.')\n }\n catch( error ){\n console.error('[EMBEDDED] Setup failed:', error )\n \n // Create minimal fallback\n window._wio = {\n error: error.toString(),\n listen: function(){ console.error('[EMBEDDED] WIO failed to initialize') },\n emit: function(){ console.error('[EMBEDDED] WIO failed to initialize') },\n on: function(){},\n once: function(){},\n off: function(){}\n }\n }\n\n true\n })()\n ";
|
|
713
|
+
return "\n (function() {\n try {\n console.debug('[EMBEDDED] Initializing WIO bridge...')\n \n const RESERVED_EVENTS = [\n 'ping',\n 'pong',\n '__heartbeat',\n '__heartbeat_response',\n '__embedded_ready',\n '__connection_ack',\n '__webview_ready'\n ]\n \n // Use closure variable to avoid 'this' binding issues\n window._wio = {\n type: 'EMBEDDED',\n connected: false,\n Events: {},\n messageQueue: [],\n connectionToken: null,\n setupComplete: false,\n\n listen: function(){\n if( this.setupComplete ){\n console.warn('[EMBEDDED] Already listening')\n return this\n }\n\n console.debug('[EMBEDDED] Setting up message listeners...')\n\n // Listen to messages from React Native\n window.addEventListener('message', function( event ){\n try {\n const message = typeof event.data === 'string' ? JSON.parse( event.data ) : event.data\n window._wio.handleMessage( message )\n }\n catch( error ){ console.error('[EMBEDDED] Parse error:', error ) }\n })\n \n // Android support\n if( typeof document !== 'undefined' ){\n document.addEventListener('message', function( event ){\n try {\n const message = typeof event.data === 'string' ? JSON.parse( event.data ) : event.data\n window._wio.handleMessage( message )\n }\n catch( error ){ console.error('[EMBEDDED] Parse error:', error ) }\n })\n }\n\n this.setupComplete = true\n console.debug('[EMBEDDED] Setup complete, starting ready announcements')\n\n // Start announcing readiness\n this.announceReady()\n\n return this\n },\n \n ackId: function(){\n const\n rmin = 100000,\n rmax = 999999,\n timestamp = Date.now(),\n random = Math.floor( Math.random() * ( rmax - rmin + 1 ) + rmin )\n\n return timestamp + '_' + random\n },\n \n fire: function( _event, payload, cid ){\n if( !window._wio.Events[_event] && !window._wio.Events[_event + '--@once'] ){\n console.debug('[EMBEDDED] No listener for:', _event)\n return\n }\n \n const ackFn = cid\n ? ( error, ...args ) => window._wio.emit( _event + '--' + cid + '--@ack', { error: error || false, args } )\n : undefined\n \n let listeners = []\n if( window._wio.Events[_event + '--@once'] ){\n _event += '--@once'\n listeners = window._wio.Events[_event]\n\n delete window._wio.Events[_event]\n }\n else listeners = window._wio.Events[_event] || []\n \n listeners.forEach( fn => {\n try { payload !== undefined ? fn( payload, ackFn ) : fn( ackFn ) }\n catch( error ){ console.error('[EMBEDDED] Listener error:', error ) }\n })\n },\n \n emit: function( _event, payload, fn ){\n if( typeof payload === 'function' ){\n fn = payload\n payload = undefined\n }\n \n if( !window._wio.connected && !RESERVED_EVENTS.includes(_event) ){\n window._wio.messageQueue.push({ _event, payload, fn, timestamp: Date.now() })\n console.debug('[EMBEDDED] Queued message:', _event)\n return\n }\n \n try {\n let cid\n if( typeof fn === 'function' ){\n cid = window._wio.ackId()\n window._wio.once( _event + '--' + cid + '--@ack', ({ error, args }) => fn( error, ...args ) )\n }\n \n const messageData = {\n _event,\n payload,\n cid,\n timestamp: Date.now(),\n token: RESERVED_EVENTS.includes(_event) ? window._wio.connectionToken : undefined\n }\n \n if( typeof window.ReactNativeWebView !== 'undefined' )\n window.ReactNativeWebView.postMessage( JSON.stringify( messageData ) )\n else console.error('[EMBEDDED] ReactNativeWebView not available')\n }\n catch( error ){\n console.error('[EMBEDDED] Emit error:', error )\n typeof fn === 'function' && fn( String(error) )\n }\n },\n \n on: function( _event, fn ){\n if( !window._wio.Events[_event] ) window._wio.Events[_event] = []\n window._wio.Events[_event].push( fn )\n\n return window._wio\n },\n \n once: function( _event, fn ){\n _event += '--@once'\n if( !window._wio.Events[_event] ) window._wio.Events[_event] = []\n\n window._wio.Events[_event].push( fn )\n\n return window._wio\n },\n \n off: function( _event, fn ){\n if( fn && window._wio.Events[_event] ){\n const index = window._wio.Events[_event].indexOf( fn )\n if( index > -1 ){\n window._wio.Events[_event].splice( index, 1 )\n if( window._wio.Events[_event].length === 0 ) delete window._wio.Events[_event]\n }\n }\n else delete window._wio.Events[_event]\n\n return window._wio\n },\n \n processMessageQueue: function(){\n if( !window._wio.connected || window._wio.messageQueue.length === 0 ) return\n \n console.debug('[EMBEDDED] Processing', window._wio.messageQueue.length, 'queued messages')\n const queue = [ ...window._wio.messageQueue ]\n window._wio.messageQueue = []\n \n queue.forEach( msg => {\n try { window._wio.emit( msg._event, msg.payload, msg.fn ) }\n catch( error ){ console.error('[EMBEDDED] Queue process error:', error ) }\n })\n },\n \n handleMessage: function( data ){\n if( !data || !data._event ) return\n \n const { _event, payload, cid, token } = data\n \n console.debug('[EMBEDDED] Received:', _event )\n \n // Handle heartbeat response\n if( _event === '__heartbeat_response' )\n return\n \n // Handle heartbeat request\n if( _event === '__heartbeat' ){\n window._wio.emit('__heartbeat_response', { timestamp: Date.now() })\n return\n }\n \n // Handle webview ready signal\n if( _event === '__webview_ready' ){\n console.debug('[EMBEDDED] WebView ready signal received')\n return\n }\n \n // Handle ping from WEBVIEW\n if( _event === 'ping' ){\n console.debug('[EMBEDDED] Received ping, sending pong')\n window._wio.connectionToken = token\n\n window._wio.emit('pong', { token: window._wio.connectionToken })\n return\n }\n \n // Handle connection acknowledgment\n if( _event === '__connection_ack' ){\n if( token && token !== window._wio.connectionToken ){\n console.error('[EMBEDDED] Invalid connection token in ack')\n return\n }\n \n console.debug('[EMBEDDED] Connection established (received ack)')\n\n window._wio.connected = true\n window._wio.processMessageQueue()\n window._wio.fire('connect')\n\n return\n }\n \n // Fire event listeners\n window._wio.fire( _event, payload, cid )\n },\n \n announceReady: function(){\n let attempts = 0\n const maxAttempts = 10\n const interval = 1000\n \n console.debug('[EMBEDDED] Starting ready announcements')\n \n const announce = () => {\n if( window._wio.connected ){\n console.debug('[EMBEDDED] Connected, stopping announcements')\n return\n }\n \n attempts++\n if( attempts > maxAttempts ){\n console.debug('[EMBEDDED] Max announcement attempts reached')\n return\n }\n \n console.debug('[EMBEDDED] Announcing ready (attempt', attempts + '/' + maxAttempts + ')')\n window._wio.emit('__embedded_ready')\n \n setTimeout( announce, interval )\n }\n \n announce()\n }\n }\n\n console.debug('[EMBEDDED] WIO bridge ready. Call window._wio.listen() to connect.')\n }\n catch( error ){\n console.error('[EMBEDDED] Setup failed:', error )\n \n // Create minimal fallback\n window._wio = {\n error: error.toString(),\n listen: function(){ console.error('[EMBEDDED] WIO failed to initialize') },\n emit: function(){ console.error('[EMBEDDED] WIO failed to initialize') },\n on: function(){},\n once: function(){},\n off: function(){}\n }\n }\n\n true\n })()\n ";
|
|
677
714
|
};
|
|
678
715
|
return WIO;
|
|
679
716
|
}());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webview.io",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Easy and friendly API to connect and interact between React Native and WebView with enhanced security, reliability, and modern async/await support.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
package/src/index.ts
CHANGED
|
@@ -17,6 +17,16 @@ export type Options = {
|
|
|
17
17
|
messageQueueSize?: number
|
|
18
18
|
connectionPingInterval?: number
|
|
19
19
|
maxConnectionAttempts?: number
|
|
20
|
+
/**
|
|
21
|
+
* Optional allowlist of incoming application-level events.
|
|
22
|
+
* Reserved internal events (ping/pong/heartbeat/handshake) are always allowed.
|
|
23
|
+
*/
|
|
24
|
+
allowedIncomingEvents?: string[]
|
|
25
|
+
/**
|
|
26
|
+
* Optional custom validator for incoming messages.
|
|
27
|
+
* Return false to drop a message; an 'error' event will be emitted.
|
|
28
|
+
*/
|
|
29
|
+
validateIncoming?: ( event: string, payload: any ) => boolean
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
export interface RegisteredEvents {
|
|
@@ -83,7 +93,26 @@ const ackId = () => {
|
|
|
83
93
|
}
|
|
84
94
|
|
|
85
95
|
const generateToken = () => {
|
|
86
|
-
|
|
96
|
+
// Prefer cryptographically strong randomness when available
|
|
97
|
+
try {
|
|
98
|
+
const globalCrypto = (typeof crypto !== 'undefined'
|
|
99
|
+
? crypto
|
|
100
|
+
: (typeof window !== 'undefined' && (window as any).crypto)
|
|
101
|
+
|| (typeof globalThis !== 'undefined' && (globalThis as any).crypto))
|
|
102
|
+
|
|
103
|
+
if( globalCrypto && typeof globalCrypto.getRandomValues === 'function' ){
|
|
104
|
+
const buffer = new Uint32Array(4)
|
|
105
|
+
globalCrypto.getRandomValues( buffer )
|
|
106
|
+
|
|
107
|
+
const randomPart = Array.from( buffer ).map( n => n.toString( 16 ) ).join('')
|
|
108
|
+
return `${Date.now()}_${randomPart}`
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch{
|
|
112
|
+
// Fall back to Math.random-based implementation below
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return `${Date.now()}_${Math.random().toString( 36 ).substring( 2, 15 )}`
|
|
87
116
|
}
|
|
88
117
|
|
|
89
118
|
const RESERVED_EVENTS = [
|
|
@@ -544,6 +573,29 @@ export default class WIO {
|
|
|
544
573
|
return
|
|
545
574
|
}
|
|
546
575
|
|
|
576
|
+
// Optional application-level incoming validation (non-reserved events only)
|
|
577
|
+
if( !RESERVED_EVENTS.includes( _event ) ){
|
|
578
|
+
if( this.options.allowedIncomingEvents
|
|
579
|
+
&& !this.options.allowedIncomingEvents.includes( _event ) ){
|
|
580
|
+
this.fire('error', {
|
|
581
|
+
type: 'DISALLOWED_EVENT',
|
|
582
|
+
direction: 'incoming',
|
|
583
|
+
event: _event
|
|
584
|
+
})
|
|
585
|
+
return
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if( this.options.validateIncoming
|
|
589
|
+
&& !this.options.validateIncoming( _event, payload ) ){
|
|
590
|
+
this.fire('error', {
|
|
591
|
+
type: 'INVALID_MESSAGE',
|
|
592
|
+
direction: 'incoming',
|
|
593
|
+
event: _event
|
|
594
|
+
})
|
|
595
|
+
return
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
547
599
|
// Fire available event listeners
|
|
548
600
|
this.fire( _event, payload, cid )
|
|
549
601
|
}
|
|
@@ -821,7 +873,7 @@ export default class WIO {
|
|
|
821
873
|
return `
|
|
822
874
|
(function() {
|
|
823
875
|
try {
|
|
824
|
-
console.
|
|
876
|
+
console.debug('[EMBEDDED] Initializing WIO bridge...')
|
|
825
877
|
|
|
826
878
|
const RESERVED_EVENTS = [
|
|
827
879
|
'ping',
|
|
@@ -848,7 +900,7 @@ export default class WIO {
|
|
|
848
900
|
return this
|
|
849
901
|
}
|
|
850
902
|
|
|
851
|
-
console.
|
|
903
|
+
console.debug('[EMBEDDED] Setting up message listeners...')
|
|
852
904
|
|
|
853
905
|
// Listen to messages from React Native
|
|
854
906
|
window.addEventListener('message', function( event ){
|
|
@@ -871,7 +923,7 @@ export default class WIO {
|
|
|
871
923
|
}
|
|
872
924
|
|
|
873
925
|
this.setupComplete = true
|
|
874
|
-
console.
|
|
926
|
+
console.debug('[EMBEDDED] Setup complete, starting ready announcements')
|
|
875
927
|
|
|
876
928
|
// Start announcing readiness
|
|
877
929
|
this.announceReady()
|
|
@@ -891,7 +943,7 @@ export default class WIO {
|
|
|
891
943
|
|
|
892
944
|
fire: function( _event, payload, cid ){
|
|
893
945
|
if( !window._wio.Events[_event] && !window._wio.Events[_event + '--@once'] ){
|
|
894
|
-
console.
|
|
946
|
+
console.debug('[EMBEDDED] No listener for:', _event)
|
|
895
947
|
return
|
|
896
948
|
}
|
|
897
949
|
|
|
@@ -922,7 +974,7 @@ export default class WIO {
|
|
|
922
974
|
|
|
923
975
|
if( !window._wio.connected && !RESERVED_EVENTS.includes(_event) ){
|
|
924
976
|
window._wio.messageQueue.push({ _event, payload, fn, timestamp: Date.now() })
|
|
925
|
-
console.
|
|
977
|
+
console.debug('[EMBEDDED] Queued message:', _event)
|
|
926
978
|
return
|
|
927
979
|
}
|
|
928
980
|
|
|
@@ -983,7 +1035,7 @@ export default class WIO {
|
|
|
983
1035
|
processMessageQueue: function(){
|
|
984
1036
|
if( !window._wio.connected || window._wio.messageQueue.length === 0 ) return
|
|
985
1037
|
|
|
986
|
-
console.
|
|
1038
|
+
console.debug('[EMBEDDED] Processing', window._wio.messageQueue.length, 'queued messages')
|
|
987
1039
|
const queue = [ ...window._wio.messageQueue ]
|
|
988
1040
|
window._wio.messageQueue = []
|
|
989
1041
|
|
|
@@ -998,7 +1050,7 @@ export default class WIO {
|
|
|
998
1050
|
|
|
999
1051
|
const { _event, payload, cid, token } = data
|
|
1000
1052
|
|
|
1001
|
-
console.
|
|
1053
|
+
console.debug('[EMBEDDED] Received:', _event )
|
|
1002
1054
|
|
|
1003
1055
|
// Handle heartbeat response
|
|
1004
1056
|
if( _event === '__heartbeat_response' )
|
|
@@ -1012,13 +1064,13 @@ export default class WIO {
|
|
|
1012
1064
|
|
|
1013
1065
|
// Handle webview ready signal
|
|
1014
1066
|
if( _event === '__webview_ready' ){
|
|
1015
|
-
console.
|
|
1067
|
+
console.debug('[EMBEDDED] WebView ready signal received')
|
|
1016
1068
|
return
|
|
1017
1069
|
}
|
|
1018
1070
|
|
|
1019
1071
|
// Handle ping from WEBVIEW
|
|
1020
1072
|
if( _event === 'ping' ){
|
|
1021
|
-
console.
|
|
1073
|
+
console.debug('[EMBEDDED] Received ping, sending pong')
|
|
1022
1074
|
window._wio.connectionToken = token
|
|
1023
1075
|
|
|
1024
1076
|
window._wio.emit('pong', { token: window._wio.connectionToken })
|
|
@@ -1032,7 +1084,7 @@ export default class WIO {
|
|
|
1032
1084
|
return
|
|
1033
1085
|
}
|
|
1034
1086
|
|
|
1035
|
-
console.
|
|
1087
|
+
console.debug('[EMBEDDED] Connection established (received ack)')
|
|
1036
1088
|
|
|
1037
1089
|
window._wio.connected = true
|
|
1038
1090
|
window._wio.processMessageQueue()
|
|
@@ -1050,21 +1102,21 @@ export default class WIO {
|
|
|
1050
1102
|
const maxAttempts = 10
|
|
1051
1103
|
const interval = 1000
|
|
1052
1104
|
|
|
1053
|
-
console.
|
|
1105
|
+
console.debug('[EMBEDDED] Starting ready announcements')
|
|
1054
1106
|
|
|
1055
1107
|
const announce = () => {
|
|
1056
1108
|
if( window._wio.connected ){
|
|
1057
|
-
console.
|
|
1109
|
+
console.debug('[EMBEDDED] Connected, stopping announcements')
|
|
1058
1110
|
return
|
|
1059
1111
|
}
|
|
1060
1112
|
|
|
1061
1113
|
attempts++
|
|
1062
1114
|
if( attempts > maxAttempts ){
|
|
1063
|
-
console.
|
|
1115
|
+
console.debug('[EMBEDDED] Max announcement attempts reached')
|
|
1064
1116
|
return
|
|
1065
1117
|
}
|
|
1066
1118
|
|
|
1067
|
-
console.
|
|
1119
|
+
console.debug('[EMBEDDED] Announcing ready (attempt', attempts + '/' + maxAttempts + ')')
|
|
1068
1120
|
window._wio.emit('__embedded_ready')
|
|
1069
1121
|
|
|
1070
1122
|
setTimeout( announce, interval )
|
|
@@ -1074,7 +1126,7 @@ export default class WIO {
|
|
|
1074
1126
|
}
|
|
1075
1127
|
}
|
|
1076
1128
|
|
|
1077
|
-
console.
|
|
1129
|
+
console.debug('[EMBEDDED] WIO bridge ready. Call window._wio.listen() to connect.')
|
|
1078
1130
|
}
|
|
1079
1131
|
catch( error ){
|
|
1080
1132
|
console.error('[EMBEDDED] Setup failed:', error )
|