webview.io 1.0.6 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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,13 @@ 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
148
+ cryptoAuth: { // Optional cryptographic message authentication (HMAC-SHA256)
149
+ secret: 'replace-with-shared-secret',
150
+ requireSigned: false
151
+ }
144
152
  })
145
153
  ```
146
154
 
@@ -180,12 +188,28 @@ This prevents:
180
188
  - Fires `connect_timeout` event if all attempts fail
181
189
 
182
190
  **EMBEDDED Side:**
183
- - Automatically announces `__embedded_ready` when loaded
191
+ - Bridge is available as `window._wio` after injection
192
+ - **Must manually call `window._wio.listen()` to start connection**
193
+ - Automatically announces `__embedded_ready` when `listen()` is called
184
194
  - Retries announcement every 2 seconds until connected
185
195
  - Handles race condition if loaded before React Native
186
196
  - Responds to `ping` with `pong` containing same token
187
197
  - Confirms connection upon receiving `__connection_ack`
188
198
 
199
+ ## Reserved Events
200
+
201
+ The library uses several internal events for connection management. These events are handled automatically and **bypass normal message queuing** to ensure reliable connection establishment:
202
+
203
+ - **`ping`** - Initial connection request from WEBVIEW
204
+ - **`pong`** - Connection response from EMBEDDED
205
+ - **`__heartbeat`** - Periodic health check request
206
+ - **`__heartbeat_response`** - Health check response
207
+ - **`__embedded_ready`** - EMBEDDED announces it's ready to connect
208
+ - **`__connection_ack`** - Final handshake acknowledgment from WEBVIEW
209
+ - **`__webview_ready`** - Signal that WEBVIEW peer is ready (currently logged but not actively used)
210
+
211
+ **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.
212
+
189
213
  ## Async/Await Support
190
214
 
191
215
  ### Send Messages with Acknowledgments
@@ -231,7 +255,7 @@ console.log('User data received:', userData)
231
255
  ### Connection Timeout Handling
232
256
 
233
257
  ```javascript
234
- // Handle connection timeout (new event)
258
+ // Handle connection timeout
235
259
  wio.on('connect_timeout', ({ attempts }) => {
236
260
  console.log(`Failed to connect after ${attempts} attempts`)
237
261
  // Optionally retry manually or show error to user
@@ -267,13 +291,13 @@ const stats = wio.getStats()
267
291
  console.log(stats)
268
292
  // {
269
293
  // connected: true,
270
- // embeddedReady: true, // NEW: EMBEDDED peer readiness state
294
+ // embeddedReady: true, // EMBEDDED peer readiness state
271
295
  // peerType: 'WEBVIEW',
272
296
  // origin: 'https://example.com',
273
297
  // lastHeartbeat: 1609459200000,
274
298
  // queuedMessages: 0,
275
299
  // reconnectAttempts: 0,
276
- // connectionAttempts: 0, // NEW: Current connection attempts
300
+ // connectionAttempts: 0, // Current connection attempts
277
301
  // activeListeners: 5,
278
302
  // messageRate: 2
279
303
  // }
@@ -288,7 +312,7 @@ Each connection uses a unique token for validation:
288
312
  ```javascript
289
313
  // Automatically generated and validated during handshake
290
314
  // Prevents accepting messages from previous connections
291
- // Tokens are only used for connection-related events
315
+ // Tokens are only used for reserved connection events
292
316
  ```
293
317
 
294
318
  ### Origin Validation
@@ -312,6 +336,47 @@ wio.emit('data', {
312
336
  })
313
337
  ```
314
338
 
339
+ ### Incoming Event Allowlist & Validation
340
+
341
+ 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.
342
+
343
+ ```javascript
344
+ const wio = new WIO({
345
+ type: 'WEBVIEW',
346
+ debug: true,
347
+ allowedIncomingEvents: ['get:location', 'location:picked'],
348
+ validateIncoming: (event, payload) => {
349
+ // Example: simple checks
350
+ if (event === 'location:picked') return payload && typeof payload.lat === 'number'
351
+ return true
352
+ }
353
+ })
354
+
355
+ wio.on('error', (error) => {
356
+ if (error.type === 'DISALLOWED_EVENT' || error.type === 'INVALID_MESSAGE') {
357
+ console.warn('Dropped incoming message:', error)
358
+ }
359
+ })
360
+ ```
361
+
362
+ ### Cryptographic Message Authentication (HMAC)
363
+
364
+ If you need **message integrity/authenticity** beyond the default transport guarantees, enable `cryptoAuth` and use the signed APIs. This adds an HMAC-SHA256 signature + timestamp + nonce (with basic replay protection).
365
+
366
+ **Important security note:** if your WebView loads untrusted content, that content can read the shared secret. This protects against some injection/misrouting scenarios—but not against a fully compromised WebView page.
367
+
368
+ ```javascript
369
+ // React Native side
370
+ const wio = new WIO({
371
+ type: 'WEBVIEW',
372
+ cryptoAuth: { secret: 'replace-with-shared-secret', requireSigned: true }
373
+ })
374
+
375
+ // Send signed
376
+ await wio.emitSigned('hello', { msg: 'signed' })
377
+ const reply = await wio.emitAsyncSigned('getData', { id: 123 }, 5000)
378
+ ```
379
+
315
380
  ### Rate Limiting
316
381
 
317
382
  ```javascript
@@ -356,6 +421,7 @@ wio.on('error', (error) => {
356
421
 
357
422
  ```javascript
358
423
  // Messages are automatically queued when disconnected
424
+ // Reserved events bypass the queue for reliable connection handling
359
425
  wio.emit('important-data', { data: 'This will be queued if disconnected' })
360
426
 
361
427
  // Clear queue manually if needed
@@ -382,7 +448,7 @@ console.log(`${stats.queuedMessages} messages queued`)
382
448
 
383
449
  #### Connection Methods
384
450
  - **`initiate(webViewRef, origin)`** - Establish connection with retry logic (WEBVIEW peer only)
385
- - **`listen(hostOrigin?)`** - Listen for connection with readiness announcement (EMBEDDED peer only)
451
+ - **`listen(hostOrigin?)`** - Start listening for connection (EMBEDDED peer only). The `hostOrigin` parameter is informational only and used for logging - no validation is performed.
386
452
  - **`handleMessage(event)`** - Handle incoming message from WebView
387
453
  - **`disconnect(callback?)`** - Disconnect and cleanup all resources
388
454
  - **`isConnected()`** - Check connection status
@@ -399,15 +465,19 @@ console.log(`${stats.queuedMessages} messages queued`)
399
465
  #### Connection Events
400
466
  - **`connect`** - Connection established (after 3-way handshake)
401
467
  - **`disconnect`** - Connection lost with reason
402
- - **`connect_timeout`** - Initial connection failed after all attempts (NEW)
468
+ - **`connect_timeout`** - Initial connection failed after all attempts
403
469
  - **`reconnecting`** - Reconnection attempt started
404
470
  - **`reconnection_failed`** - All reconnection attempts failed
405
471
 
406
- #### Internal Events (handled automatically)
472
+ #### Reserved Internal Events
473
+ These events are handled automatically by the library. Do not use these names for your application events:
474
+ - **`ping`** - Connection initiation from WEBVIEW
475
+ - **`pong`** - Connection response from EMBEDDED
407
476
  - **`__embedded_ready`** - EMBEDDED peer announces readiness
408
477
  - **`__connection_ack`** - Final acknowledgment in 3-way handshake
409
- - **`__heartbeat`** - Connection health check
478
+ - **`__heartbeat`** - Connection health check request
410
479
  - **`__heartbeat_response`** - Heartbeat response
480
+ - **`__webview_ready`** - WEBVIEW ready signal (informational)
411
481
 
412
482
  #### Error Events
413
483
  - **`error`** - Various error conditions with detailed error objects
@@ -575,6 +645,10 @@ const response = await wio.emitAsync<{ query: string }, ApiResponse>(
575
645
  | `MESSAGE_HANDLING_ERROR` | Error processing incoming message |
576
646
  | `EMIT_ERROR` | Error sending message |
577
647
  | `LISTENER_ERROR` | Error in event listener |
648
+ | `DISALLOWED_EVENT` | Incoming event rejected by `allowedIncomingEvents` |
649
+ | `INVALID_MESSAGE` | Incoming message rejected by `validateIncoming` |
650
+ | `AUTH_FAILED` | Incoming message failed cryptographic authentication |
651
+ | `AUTH_ERROR` | Cryptographic verification errored (missing crypto, etc.) |
578
652
  | `RATE_LIMIT_EXCEEDED` | Too many messages sent |
579
653
  | `NO_CONNECTION` | Attempted to send without connection |
580
654
 
@@ -600,12 +674,13 @@ const response = await wio.emitAsync<{ query: string }, ApiResponse>(
600
674
  **Symptoms:** `connect_timeout` event fires, connection never succeeds
601
675
 
602
676
  **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
677
+ 1. **Verify `listen()` is called** - The EMBEDDED side must manually call `window._wio.listen()` to start the connection
678
+ 2. Check that `injectedJavaScript` is properly set on WebView
679
+ 3. Verify `javaScriptEnabled={true}` is set
680
+ 4. Check browser console for JavaScript errors in WebView
681
+ 5. Ensure origin matches exactly (including protocol)
682
+ 6. Increase `connectionTimeout` for slow networks
683
+ 7. Check that WebView content loads successfully
609
684
 
610
685
  ### Messages Not Received
611
686
 
@@ -617,6 +692,7 @@ const response = await wio.emitAsync<{ query: string }, ApiResponse>(
617
692
  3. Look for errors in `error` event listener
618
693
  4. Check if rate limiting is being exceeded
619
694
  5. Verify message size is under `maxMessageSize`
695
+ 6. Ensure you're not using reserved event names
620
696
 
621
697
  ### Frequent Disconnections
622
698
 
@@ -637,7 +713,7 @@ const response = await wio.emitAsync<{ query: string }, ApiResponse>(
637
713
  - Already handled! The three-way handshake with readiness announcements prevents this
638
714
  - EMBEDDED announces readiness periodically until connected
639
715
  - WEBVIEW retries connection attempts automatically
640
- - No action needed from your side
716
+ - Just ensure `window._wio.listen()` is called in your WebView code
641
717
 
642
718
  ## Differences from iframe.io
643
719
 
@@ -647,6 +723,7 @@ const response = await wio.emitAsync<{ query: string }, ApiResponse>(
647
723
  - **Initialization**: Uses `RefObject<WebView>` instead of `Window` object
648
724
  - **Message Handling**: Requires explicit `handleMessage()` call in `onMessage` prop
649
725
  - **Injected Script**: Uses `getInjectedJavaScript()` to setup bridge in WebView
726
+ - **Manual Connection**: EMBEDDED side must call `listen()` to start connection
650
727
  - **No DOM Dependencies**: Works in React Native environment without DOM APIs
651
728
  - **Enhanced Handshake**: Three-way handshake with token validation for mobile reliability
652
729
  - **Connection Retry**: Built-in retry logic for spotty mobile connections
package/dist/index.d.ts CHANGED
@@ -3,6 +3,30 @@ import type { WebView } from 'react-native-webview';
3
3
  export type PeerType = 'WEBVIEW' | 'EMBEDDED';
4
4
  export type AckFunction = (error: boolean | string, ...args: any[]) => void;
5
5
  export type Listener = (payload?: any, ack?: AckFunction) => void;
6
+ export type CryptoAuthOptions = {
7
+ /**
8
+ * Shared secret used for HMAC-SHA256 signing.
9
+ *
10
+ * IMPORTANT: If an attacker can execute JS in either peer, they can read the secret.
11
+ * This is for authenticity/integrity between cooperating peers, not a sandbox boundary.
12
+ */
13
+ secret: string;
14
+ /**
15
+ * If true, drop any incoming message that doesn't carry valid auth.
16
+ * Default: false (accept unsigned messages)
17
+ */
18
+ requireSigned?: boolean;
19
+ /**
20
+ * Maximum allowed clock skew for signed messages (ms).
21
+ * Default: 2 minutes
22
+ */
23
+ maxSkewMs?: number;
24
+ /**
25
+ * Replay window size (max number of nonces kept in memory).
26
+ * Default: 500
27
+ */
28
+ replayWindowSize?: number;
29
+ };
6
30
  export type Options = {
7
31
  type?: PeerType;
8
32
  debug?: boolean;
@@ -14,12 +38,29 @@ export type Options = {
14
38
  messageQueueSize?: number;
15
39
  connectionPingInterval?: number;
16
40
  maxConnectionAttempts?: number;
41
+ /**
42
+ * Optional allowlist of incoming application-level events.
43
+ * Reserved internal events (ping/pong/heartbeat/handshake) are always allowed.
44
+ */
45
+ allowedIncomingEvents?: string[];
46
+ /**
47
+ * Optional custom validator for incoming messages.
48
+ * Return false to drop a message; an 'error' event will be emitted.
49
+ */
50
+ validateIncoming?: (event: string, payload: any) => boolean;
51
+ /**
52
+ * Optional cryptographic message authentication (HMAC-SHA256).
53
+ * When enabled, use `emitSigned` / `emitAsyncSigned` to send signed messages.
54
+ * For EMBEDDED (WebView content) you must set the secret in injected bridge too (handled by `getInjectedJavaScript()` when configured).
55
+ */
56
+ cryptoAuth?: CryptoAuthOptions;
17
57
  };
18
58
  export interface RegisteredEvents {
19
59
  [index: string]: Listener[];
20
60
  }
21
61
  export type Peer = {
22
62
  type: PeerType;
63
+ protocolVersion?: number;
23
64
  webViewRef?: RefObject<WebView>;
24
65
  origin?: string;
25
66
  connected?: boolean;
@@ -27,12 +68,19 @@ export type Peer = {
27
68
  embeddedReady?: boolean;
28
69
  };
29
70
  export type MessageData = {
71
+ v?: number;
30
72
  _event: string;
31
73
  payload: any;
32
74
  cid: string | undefined;
33
75
  timestamp?: number;
34
76
  size?: number;
35
77
  token?: string;
78
+ auth?: {
79
+ alg: 'HMAC-SHA256';
80
+ ts: number;
81
+ nonce: string;
82
+ sig: string;
83
+ };
36
84
  };
37
85
  export type Message = {
38
86
  data: MessageData;
@@ -58,7 +106,21 @@ export default class WIO {
58
106
  private maxReconnectAttempts;
59
107
  private connectionToken?;
60
108
  private connectionAttempts;
109
+ private seenNonces;
61
110
  constructor(options?: Options);
111
+ private cryptoCfg;
112
+ /**
113
+ * Forget nonces that can no longer be replayed, and only then cap the map.
114
+ *
115
+ * Age is what decides replayability: a captured message is refused once its
116
+ * `ts` falls outside maxSkewMs, so a nonce is only worth keeping that long.
117
+ * Pruning purely by count made the two defaults contradict each other — 500
118
+ * remembered nonces at the default 100 messages a second is five seconds of
119
+ * history guarding a two-minute acceptance window.
120
+ */
121
+ private pruneNonces;
122
+ private signOutgoing;
123
+ private verifyIncomingAuth;
62
124
  debug(...args: any[]): void;
63
125
  isConnected(): boolean;
64
126
  private startHeartbeat;
@@ -93,6 +155,12 @@ export default class WIO {
93
155
  }): void;
94
156
  fire(_event: string, payload?: MessageData['payload'], cid?: string): void;
95
157
  emit<T = any>(_event: string, payload?: T | AckFunction, fn?: AckFunction): this;
158
+ /**
159
+ * Send a signed message (HMAC-SHA256) when `options.cryptoAuth` is configured.
160
+ * This is async because WebCrypto signing is async.
161
+ */
162
+ emitSigned<T = any>(_event: string, payload?: T | AckFunction, fn?: AckFunction): Promise<this>;
163
+ emitAsyncSigned<T = any, R = any>(_event: string, payload?: T, timeout?: number): Promise<R>;
96
164
  on(_event: string, fn: Listener): this;
97
165
  once(_event: string, fn: Listener): this;
98
166
  off(_event: string, fn?: Listener): this;