iframe.io 1.2.0 → 1.3.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/dist/index.d.ts CHANGED
@@ -1,6 +1,50 @@
1
1
  export type PeerType = 'WINDOW' | 'IFRAME';
2
2
  export type AckFunction = (error: boolean | string, ...args: any[]) => void;
3
3
  export type Listener = (payload?: any, ack?: AckFunction) => void;
4
+ export type CryptoAuthOptions = {
5
+ /**
6
+ * Shared secret used for HMAC-SHA256 signing.
7
+ *
8
+ * IMPORTANT: If an attacker can execute JS in either peer, they can read the secret.
9
+ * This is for authenticity/integrity between cooperating peers, not a sandbox boundary.
10
+ */
11
+ secret: string;
12
+ /**
13
+ * If true, drop any incoming message that doesn't carry valid auth.
14
+ * Default: false (accept unsigned messages)
15
+ */
16
+ requireSigned?: boolean;
17
+ /**
18
+ * Maximum allowed clock skew for signed messages (ms).
19
+ * Default: 2 minutes
20
+ */
21
+ maxSkewMs?: number;
22
+ /**
23
+ * Replay window size (max number of nonces kept in memory).
24
+ * Default: 500
25
+ */
26
+ replayWindowSize?: number;
27
+ /**
28
+ * Enable session-derived keys for enhanced security.
29
+ * When enabled, a unique session key is derived from the master secret
30
+ * and exchanged session IDs during connection handshake.
31
+ * Recommended for long-lived connections and high-security applications.
32
+ * Default: false
33
+ */
34
+ enableSessionKeys?: boolean;
35
+ /**
36
+ * How often to rotate session keys (in milliseconds).
37
+ * Only applies when enableSessionKeys is true.
38
+ * Default: 3600000 (1 hour)
39
+ */
40
+ sessionKeyRotationInterval?: number;
41
+ };
42
+ export type SessionKeyInfo = {
43
+ keyId: string;
44
+ key: string;
45
+ createdAt: number;
46
+ expiresAt: number;
47
+ };
4
48
  export type Options = {
5
49
  type?: PeerType;
6
50
  debug?: boolean;
@@ -20,6 +64,11 @@ export type Options = {
20
64
  * Return false to drop a message; an 'error' event will be emitted.
21
65
  */
22
66
  validateIncoming?: (event: string, payload: any, origin: string) => boolean;
67
+ /**
68
+ * Optional cryptographic message authentication (HMAC-SHA256).
69
+ * When enabled, use `emitSigned` / `emitAsyncSigned` to send signed messages.
70
+ */
71
+ cryptoAuth?: CryptoAuthOptions;
23
72
  };
24
73
  export interface RegisteredEvents {
25
74
  [index: string]: Listener[];
@@ -30,13 +79,24 @@ export type Peer = {
30
79
  origin?: string;
31
80
  connected?: boolean;
32
81
  lastHeartbeat?: number;
82
+ protocolVersion?: number;
83
+ sessionId?: string;
33
84
  };
34
85
  export type MessageData = {
86
+ v: number;
35
87
  _event: string;
36
88
  payload: any;
37
89
  cid: string | undefined;
38
90
  timestamp?: number;
39
91
  size?: number;
92
+ auth?: {
93
+ alg: 'HMAC-SHA256';
94
+ ts: number;
95
+ nonce: string;
96
+ sig: string;
97
+ keyId?: string;
98
+ };
99
+ sessionId?: string;
40
100
  };
41
101
  export type Message = {
42
102
  origin: string;
@@ -56,11 +116,93 @@ export default class IOF {
56
116
  private messageListener?;
57
117
  private heartbeatTimer?;
58
118
  private reconnectTimer?;
119
+ private sessionKeyRotationTimer?;
59
120
  private messageQueue;
60
121
  private messageRateTracker;
61
122
  private reconnectAttempts;
62
123
  private maxReconnectAttempts;
124
+ private seenNonces;
125
+ private currentSessionKey?;
126
+ private pendingSessionKey?;
127
+ private previousSessionKey?;
128
+ private mySessionId?;
63
129
  constructor(options?: Options);
130
+ private cryptoCfg;
131
+ /**
132
+ * Forget nonces that can no longer be replayed, and only then cap the map.
133
+ *
134
+ * Age is what actually decides replayability: a captured message is refused
135
+ * once its `ts` falls outside maxSkewMs, so a nonce is only worth keeping
136
+ * that long. Pruning purely by count — as this did — made the two defaults
137
+ * contradict each other: 500 remembered nonces at the default 100 messages a
138
+ * second is five seconds of history guarding a two-minute acceptance window,
139
+ * so anything captured could simply be replayed after five seconds.
140
+ *
141
+ * The count cap stays as a memory bound. Reaching it means the rate limiter
142
+ * is admitting more traffic than the window can remember, so it is reported
143
+ * rather than applied in silence.
144
+ */
145
+ private pruneNonces;
146
+ /**
147
+ * Get the appropriate secret for signing messages
148
+ * Uses session key if available, otherwise falls back to master secret
149
+ */
150
+ private getSigningSecret;
151
+ /**
152
+ * Get the appropriate secret for verifying incoming messages
153
+ * Tries current key, then pending, then previous, then master
154
+ */
155
+ private getVerificationSecrets;
156
+ /**
157
+ * Application-level admission check for one incoming message.
158
+ *
159
+ * Reserved events bypass it deliberately: the handshake and the heartbeats
160
+ * must survive an allow-list that does not name them, or configuring one
161
+ * would silently sever the connection.
162
+ *
163
+ * This lives in a method because it used to be written out at each place a
164
+ * message can arrive — the authenticated and unauthenticated branches of
165
+ * `initiate()` and of `listen()` — and `listen()`'s unauthenticated branch
166
+ * never got a copy. An embedded bridge configured with an allow-list and no
167
+ * cryptoAuth, which is exactly how de.eui runs it, therefore accepted every
168
+ * event name a host cared to send.
169
+ */
170
+ private acceptIncoming;
171
+ /**
172
+ * Initialize session key exchange
173
+ * Called after connection is established if enableSessionKeys is true
174
+ */
175
+ private initiateSessionKeyExchange;
176
+ /**
177
+ * Handle incoming session key initialization
178
+ */
179
+ private handleSessionKeyInit;
180
+ /**
181
+ * Handle session key acknowledgment
182
+ */
183
+ private handleSessionKeyAck;
184
+ /**
185
+ * Derive and store a session key
186
+ */
187
+ private deriveAndStoreSessionKey;
188
+ /**
189
+ * Start session key rotation timer
190
+ */
191
+ private startSessionKeyRotation;
192
+ /**
193
+ * Rotate session key
194
+ */
195
+ private rotateSessionKey;
196
+ /**
197
+ * Handle incoming session key rotation
198
+ */
199
+ private handleSessionKeyRotate;
200
+ /**
201
+ * Stop session key rotation timer
202
+ */
203
+ private stopSessionKeyRotation;
204
+ private signOutgoing;
205
+ private verifyIncomingAuth;
64
206
  debug(...args: any[]): void;
65
207
  isConnected(): boolean;
66
208
  private startHeartbeat;
@@ -81,6 +223,12 @@ export default class IOF {
81
223
  listen(hostOrigin?: string): this;
82
224
  fire(_event: string, payload?: MessageData['payload'], cid?: string): void;
83
225
  emit<T = any>(_event: string, payload?: T | AckFunction, fn?: AckFunction): this;
226
+ /**
227
+ * Send a signed message (HMAC-SHA256) when `options.cryptoAuth` is configured.
228
+ * This is async because WebCrypto signing is async.
229
+ */
230
+ emitSigned<T = any>(_event: string, payload?: T | AckFunction, fn?: AckFunction): Promise<this>;
231
+ emitAsyncSigned<T = any, R = any>(_event: string, payload?: T, timeout?: number): Promise<R>;
84
232
  on(_event: string, fn: Listener): this;
85
233
  once(_event: string, fn: Listener): this;
86
234
  off(_event: string, fn?: Listener): this;
@@ -99,6 +247,10 @@ export default class IOF {
99
247
  reconnectAttempts: number;
100
248
  activeListeners: number;
101
249
  messageRate: number;
250
+ protocolVersion: number;
251
+ peerProtocolVersion: number | undefined;
252
+ sessionKeyActive: boolean;
253
+ sessionKeyId: string | undefined;
102
254
  };
103
255
  clearQueue(): this;
104
256
  }