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 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
- // The bridge is automatically available as window._wio
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 // Max queued messages when disconnected
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
- - Automatically announces `__embedded_ready` when loaded
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 (new event)
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, // NEW: EMBEDDED peer readiness state
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, // NEW: Current connection attempts
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-related events
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?)`** - Listen for connection with readiness announcement (EMBEDDED peer only)
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 (NEW)
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 (handled automatically)
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. Check that `injectedJavaScript` is properly set on WebView
604
- 2. Verify `javaScriptEnabled={true}` is set
605
- 3. Check browser console for JavaScript errors in WebView
606
- 4. Ensure origin matches exactly (including protocol)
607
- 5. Increase `connectionTimeout` for slow networks
608
- 6. Check that WebView content loads successfully
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
- - No action needed from your side
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.6",
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
- return `${Date.now()}_${Math.random().toString(36).substring(2, 15)}`
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.log('[EMBEDDED] Initializing WIO bridge...')
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.log('[EMBEDDED] Setting up message listeners...')
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.log('[EMBEDDED] Setup complete, starting ready announcements')
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.log('[EMBEDDED] No listener for:', _event)
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.log('[EMBEDDED] Queued message:', _event)
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.log('[EMBEDDED] Processing', window._wio.messageQueue.length, 'queued messages')
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.log('[EMBEDDED] Received:', _event )
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.log('[EMBEDDED] WebView ready signal received')
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.log('[EMBEDDED] Received ping, sending pong')
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.log('[EMBEDDED] Connection established (received ack)')
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.log('[EMBEDDED] Starting ready announcements')
1105
+ console.debug('[EMBEDDED] Starting ready announcements')
1054
1106
 
1055
1107
  const announce = () => {
1056
1108
  if( window._wio.connected ){
1057
- console.log('[EMBEDDED] Connected, stopping announcements')
1109
+ console.debug('[EMBEDDED] Connected, stopping announcements')
1058
1110
  return
1059
1111
  }
1060
1112
 
1061
1113
  attempts++
1062
1114
  if( attempts > maxAttempts ){
1063
- console.log('[EMBEDDED] Max announcement attempts reached')
1115
+ console.debug('[EMBEDDED] Max announcement attempts reached')
1064
1116
  return
1065
1117
  }
1066
1118
 
1067
- console.log('[EMBEDDED] Announcing ready (attempt', attempts + '/' + maxAttempts + ')')
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.log('[EMBEDDED] WIO bridge ready. Call window._wio.listen() to connect.')
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 )