webview.io 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -144,7 +144,11 @@ const wio = new WIO({
144
144
  autoReconnect: true, // Enable automatic reconnection
145
145
  messageQueueSize: 50, // Max queued messages when disconnected
146
146
  allowedIncomingEvents: ['hello', 'response'], // Optional incoming event allowlist (non-reserved events)
147
- validateIncoming: (event, payload) => true // Optional custom incoming validator
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
+ }
148
152
  })
149
153
  ```
150
154
 
@@ -355,6 +359,24 @@ wio.on('error', (error) => {
355
359
  })
356
360
  ```
357
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
+
358
380
  ### Rate Limiting
359
381
 
360
382
  ```javascript
@@ -625,6 +647,8 @@ const response = await wio.emitAsync<{ query: string }, ApiResponse>(
625
647
  | `LISTENER_ERROR` | Error in event listener |
626
648
  | `DISALLOWED_EVENT` | Incoming event rejected by `allowedIncomingEvents` |
627
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.) |
628
652
  | `RATE_LIMIT_EXCEEDED` | Too many messages sent |
629
653
  | `NO_CONNECTION` | Attempted to send without connection |
630
654
 
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;
@@ -24,12 +48,19 @@ export type Options = {
24
48
  * Return false to drop a message; an 'error' event will be emitted.
25
49
  */
26
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;
27
57
  };
28
58
  export interface RegisteredEvents {
29
59
  [index: string]: Listener[];
30
60
  }
31
61
  export type Peer = {
32
62
  type: PeerType;
63
+ protocolVersion?: number;
33
64
  webViewRef?: RefObject<WebView>;
34
65
  origin?: string;
35
66
  connected?: boolean;
@@ -37,12 +68,19 @@ export type Peer = {
37
68
  embeddedReady?: boolean;
38
69
  };
39
70
  export type MessageData = {
71
+ v?: number;
40
72
  _event: string;
41
73
  payload: any;
42
74
  cid: string | undefined;
43
75
  timestamp?: number;
44
76
  size?: number;
45
77
  token?: string;
78
+ auth?: {
79
+ alg: 'HMAC-SHA256';
80
+ ts: number;
81
+ nonce: string;
82
+ sig: string;
83
+ };
46
84
  };
47
85
  export type Message = {
48
86
  data: MessageData;
@@ -68,7 +106,21 @@ export default class WIO {
68
106
  private maxReconnectAttempts;
69
107
  private connectionToken?;
70
108
  private connectionAttempts;
109
+ private seenNonces;
71
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;
72
124
  debug(...args: any[]): void;
73
125
  isConnected(): boolean;
74
126
  private startHeartbeat;
@@ -103,6 +155,12 @@ export default class WIO {
103
155
  }): void;
104
156
  fire(_event: string, payload?: MessageData['payload'], cid?: string): void;
105
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>;
106
164
  on(_event: string, fn: Listener): this;
107
165
  once(_event: string, fn: Listener): this;
108
166
  off(_event: string, fn?: Listener): this;