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 +95 -18
- package/dist/index.d.ts +68 -0
- package/dist/index.js +435 -11
- package/package.json +3 -5
- package/src/index.ts +593 -18
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,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
|
|
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
|
-
-
|
|
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
|
|
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, //
|
|
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, //
|
|
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
|
|
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?)`** -
|
|
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
|
|
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
|
|
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.
|
|
604
|
-
2.
|
|
605
|
-
3.
|
|
606
|
-
4.
|
|
607
|
-
5.
|
|
608
|
-
6.
|
|
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
|
-
-
|
|
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;
|