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 +25 -1
- package/dist/index.d.ts +58 -0
- package/dist/index.js +403 -16
- package/package.json +3 -5
- package/src/index.ts +525 -2
package/dist/index.js
CHANGED
|
@@ -10,6 +10,42 @@ var __assign = (this && this.__assign) || function () {
|
|
|
10
10
|
};
|
|
11
11
|
return __assign.apply(this, arguments);
|
|
12
12
|
};
|
|
13
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
14
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
15
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
16
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
17
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
18
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
19
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
23
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
24
|
+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
25
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
26
|
+
function step(op) {
|
|
27
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
28
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
29
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
30
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
31
|
+
switch (op[0]) {
|
|
32
|
+
case 0: case 1: t = op; break;
|
|
33
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
34
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
35
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
36
|
+
default:
|
|
37
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
38
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
39
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
40
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
41
|
+
if (t[2]) _.ops.pop();
|
|
42
|
+
_.trys.pop(); continue;
|
|
43
|
+
}
|
|
44
|
+
op = body.call(thisArg, _);
|
|
45
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
46
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
47
|
+
}
|
|
48
|
+
};
|
|
13
49
|
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
14
50
|
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
15
51
|
if (ar || !(i in from)) {
|
|
@@ -20,6 +56,33 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
|
20
56
|
return to.concat(ar || Array.prototype.slice.call(from));
|
|
21
57
|
};
|
|
22
58
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
59
|
+
// Current protocol version
|
|
60
|
+
var PROTOCOL_VERSION = 1;
|
|
61
|
+
/**
|
|
62
|
+
* The exact fields, in the exact order, that a signature covers.
|
|
63
|
+
*
|
|
64
|
+
* Kept as data rather than written out at each call site because there are
|
|
65
|
+
* three implementations of this protocol — the class signing, the class
|
|
66
|
+
* verifying, and the hand-written bridge that getInjectedJavaScript() injects
|
|
67
|
+
* into the WebView — and they did not agree. The class signed over `size`; the
|
|
68
|
+
* injected bridge left it out of both its sign and its verify. Every signed
|
|
69
|
+
* message the native side sent was therefore refused by the WebView, in
|
|
70
|
+
* silence, for as long as cryptoAuth has existed.
|
|
71
|
+
*
|
|
72
|
+
* The injected bridge now interpolates this same array, so the three cannot
|
|
73
|
+
* drift apart again without changing one line.
|
|
74
|
+
*/
|
|
75
|
+
var CANONICAL_FIELDS = ['v', '_event', 'payload', 'cid', 'timestamp', 'size', 'token'];
|
|
76
|
+
function canonicalMessage(data, ts, nonce) {
|
|
77
|
+
var canonical = {};
|
|
78
|
+
for (var _i = 0, CANONICAL_FIELDS_1 = CANONICAL_FIELDS; _i < CANONICAL_FIELDS_1.length; _i++) {
|
|
79
|
+
var field = CANONICAL_FIELDS_1[_i];
|
|
80
|
+
canonical[field] = data[field];
|
|
81
|
+
}
|
|
82
|
+
canonical.ts = ts;
|
|
83
|
+
canonical.nonce = nonce;
|
|
84
|
+
return JSON.stringify(canonical);
|
|
85
|
+
}
|
|
23
86
|
function newObject(data) {
|
|
24
87
|
return JSON.parse(JSON.stringify(data));
|
|
25
88
|
}
|
|
@@ -40,6 +103,80 @@ function sanitizePayload(payload, maxSize) {
|
|
|
40
103
|
// Basic sanitization - remove functions and undefined values
|
|
41
104
|
return JSON.parse(JSON.stringify(payload));
|
|
42
105
|
}
|
|
106
|
+
function constantTimeEqual(a, b) {
|
|
107
|
+
if (a.length !== b.length)
|
|
108
|
+
return false;
|
|
109
|
+
var out = 0;
|
|
110
|
+
for (var i = 0; i < a.length; i++)
|
|
111
|
+
out |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
112
|
+
return out === 0;
|
|
113
|
+
}
|
|
114
|
+
function getGlobalCrypto() {
|
|
115
|
+
return (typeof crypto !== 'undefined'
|
|
116
|
+
? crypto
|
|
117
|
+
: (typeof window !== 'undefined' && window.crypto)
|
|
118
|
+
|| (typeof globalThis !== 'undefined' && globalThis.crypto));
|
|
119
|
+
}
|
|
120
|
+
function randomHex(bytes) {
|
|
121
|
+
try {
|
|
122
|
+
var globalCrypto = getGlobalCrypto();
|
|
123
|
+
if (globalCrypto && typeof globalCrypto.getRandomValues === 'function') {
|
|
124
|
+
var buf = new Uint8Array(bytes);
|
|
125
|
+
globalCrypto.getRandomValues(buf);
|
|
126
|
+
return Array.from(buf).map(function (b) { return b.toString(16).padStart(2, '0'); }).join('');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (_a) { }
|
|
130
|
+
// Fallback (NOT cryptographically strong)
|
|
131
|
+
return Array.from({ length: bytes }, function () { return Math.floor(Math.random() * 256).toString(16).padStart(2, '0'); }).join('');
|
|
132
|
+
}
|
|
133
|
+
function hmacSha256Base64Url(secret, message) {
|
|
134
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
135
|
+
var globalCrypto, subtle, enc, key, sig, bytes, bin, i, b64, _a, nodeCrypto, b64;
|
|
136
|
+
return __generator(this, function (_b) {
|
|
137
|
+
switch (_b.label) {
|
|
138
|
+
case 0:
|
|
139
|
+
_b.trys.push([0, 4, , 5]);
|
|
140
|
+
globalCrypto = getGlobalCrypto();
|
|
141
|
+
subtle = globalCrypto === null || globalCrypto === void 0 ? void 0 : globalCrypto.subtle;
|
|
142
|
+
if (!(subtle && typeof subtle.importKey === 'function')) return [3 /*break*/, 3];
|
|
143
|
+
enc = new TextEncoder();
|
|
144
|
+
return [4 /*yield*/, subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'])];
|
|
145
|
+
case 1:
|
|
146
|
+
key = _b.sent();
|
|
147
|
+
return [4 /*yield*/, subtle.sign('HMAC', key, enc.encode(message))];
|
|
148
|
+
case 2:
|
|
149
|
+
sig = _b.sent();
|
|
150
|
+
bytes = new Uint8Array(sig);
|
|
151
|
+
bin = '';
|
|
152
|
+
for (i = 0; i < bytes.length; i++)
|
|
153
|
+
bin += String.fromCharCode(bytes[i]);
|
|
154
|
+
b64 = btoa(bin);
|
|
155
|
+
return [2 /*return*/, b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '')];
|
|
156
|
+
case 3: return [3 /*break*/, 5];
|
|
157
|
+
case 4:
|
|
158
|
+
_a = _b.sent();
|
|
159
|
+
return [3 /*break*/, 5];
|
|
160
|
+
case 5:
|
|
161
|
+
// Node.js (commonjs) - optional (React Native metro can provide crypto polyfills in some setups)
|
|
162
|
+
try {
|
|
163
|
+
nodeCrypto = globalThis.__wio_node_crypto
|
|
164
|
+
|| (globalThis.__wio_node_crypto = (typeof globalThis.require === 'function'
|
|
165
|
+
? globalThis.require('crypto')
|
|
166
|
+
: undefined));
|
|
167
|
+
if (!nodeCrypto)
|
|
168
|
+
throw new Error('node crypto unavailable');
|
|
169
|
+
b64 = nodeCrypto.createHmac('sha256', secret).update(message).digest('base64');
|
|
170
|
+
return [2 /*return*/, b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '')];
|
|
171
|
+
}
|
|
172
|
+
catch (_c) {
|
|
173
|
+
throw new Error('No crypto implementation available for HMAC-SHA256');
|
|
174
|
+
}
|
|
175
|
+
return [2 /*return*/];
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
}
|
|
43
180
|
var ackId = function () {
|
|
44
181
|
var rmin = 100000, rmax = 999999, timestamp = Date.now(), random = Math.floor(Math.random() * (rmax - rmin + 1) + rmin);
|
|
45
182
|
return "".concat(timestamp, "_").concat(random);
|
|
@@ -80,6 +217,7 @@ var WIO = /** @class */ (function () {
|
|
|
80
217
|
this.reconnectAttempts = 0;
|
|
81
218
|
this.maxReconnectAttempts = 5;
|
|
82
219
|
this.connectionAttempts = 0;
|
|
220
|
+
this.seenNonces = new Map();
|
|
83
221
|
if (options && typeof options !== 'object')
|
|
84
222
|
throw new Error('Invalid Options');
|
|
85
223
|
this.options = __assign({ debug: false, heartbeatInterval: 30000, connectionTimeout: 10000, maxMessageSize: 1024 * 1024, maxMessagesPerSecond: 100, autoReconnect: true, messageQueueSize: 50, connectionPingInterval: 2000, maxConnectionAttempts: 5 }, options);
|
|
@@ -88,6 +226,102 @@ var WIO = /** @class */ (function () {
|
|
|
88
226
|
if (options.type)
|
|
89
227
|
this.peer.type = options.type;
|
|
90
228
|
}
|
|
229
|
+
WIO.prototype.cryptoCfg = function () {
|
|
230
|
+
var _a, _b;
|
|
231
|
+
if (!this.options.cryptoAuth)
|
|
232
|
+
return undefined;
|
|
233
|
+
return {
|
|
234
|
+
secret: this.options.cryptoAuth.secret,
|
|
235
|
+
requireSigned: !!this.options.cryptoAuth.requireSigned,
|
|
236
|
+
maxSkewMs: (_a = this.options.cryptoAuth.maxSkewMs) !== null && _a !== void 0 ? _a : 2 * 60 * 1000,
|
|
237
|
+
replayWindowSize: (_b = this.options.cryptoAuth.replayWindowSize) !== null && _b !== void 0 ? _b : 500
|
|
238
|
+
};
|
|
239
|
+
};
|
|
240
|
+
/**
|
|
241
|
+
* Forget nonces that can no longer be replayed, and only then cap the map.
|
|
242
|
+
*
|
|
243
|
+
* Age is what decides replayability: a captured message is refused once its
|
|
244
|
+
* `ts` falls outside maxSkewMs, so a nonce is only worth keeping that long.
|
|
245
|
+
* Pruning purely by count made the two defaults contradict each other — 500
|
|
246
|
+
* remembered nonces at the default 100 messages a second is five seconds of
|
|
247
|
+
* history guarding a two-minute acceptance window.
|
|
248
|
+
*/
|
|
249
|
+
WIO.prototype.pruneNonces = function (maxSize) {
|
|
250
|
+
var _this = this;
|
|
251
|
+
var _a, _b;
|
|
252
|
+
var cutoff = Date.now() - ((_b = (_a = this.cryptoCfg()) === null || _a === void 0 ? void 0 : _a.maxSkewMs) !== null && _b !== void 0 ? _b : 2 * 60 * 1000), stale = [];
|
|
253
|
+
this.seenNonces.forEach(function (ts, nonce) { ts < cutoff && stale.push(nonce); });
|
|
254
|
+
stale.forEach(function (nonce) { return _this.seenNonces.delete(nonce); });
|
|
255
|
+
if (this.seenNonces.size <= maxSize)
|
|
256
|
+
return;
|
|
257
|
+
this.fire('error', {
|
|
258
|
+
type: 'REPLAY_WINDOW_EXCEEDED',
|
|
259
|
+
remembered: this.seenNonces.size,
|
|
260
|
+
maxSize: maxSize
|
|
261
|
+
});
|
|
262
|
+
var toRemove = this.seenNonces.size - maxSize, keys = Array.from(this.seenNonces.keys());
|
|
263
|
+
for (var k = 0; k < toRemove && k < keys.length; k++)
|
|
264
|
+
this.seenNonces.delete(keys[k]);
|
|
265
|
+
};
|
|
266
|
+
WIO.prototype.signOutgoing = function (messageData) {
|
|
267
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
268
|
+
var cfg, ts, nonce, sig;
|
|
269
|
+
return __generator(this, function (_a) {
|
|
270
|
+
switch (_a.label) {
|
|
271
|
+
case 0:
|
|
272
|
+
cfg = this.cryptoCfg();
|
|
273
|
+
if (!cfg)
|
|
274
|
+
return [2 /*return*/, undefined];
|
|
275
|
+
ts = Date.now(), nonce = randomHex(16);
|
|
276
|
+
return [4 /*yield*/, hmacSha256Base64Url(cfg.secret, canonicalMessage(messageData, ts, nonce))];
|
|
277
|
+
case 1:
|
|
278
|
+
sig = _a.sent();
|
|
279
|
+
return [2 /*return*/, { alg: 'HMAC-SHA256', ts: ts, nonce: nonce, sig: sig }];
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
};
|
|
284
|
+
WIO.prototype.verifyIncomingAuth = function (data) {
|
|
285
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
286
|
+
var cfg, _a, alg, ts, nonce, sig, now, expected;
|
|
287
|
+
return __generator(this, function (_b) {
|
|
288
|
+
switch (_b.label) {
|
|
289
|
+
case 0:
|
|
290
|
+
cfg = this.cryptoCfg();
|
|
291
|
+
if (!cfg)
|
|
292
|
+
return [2 /*return*/, true];
|
|
293
|
+
if (!data.auth) {
|
|
294
|
+
return [2 /*return*/, !cfg.requireSigned];
|
|
295
|
+
}
|
|
296
|
+
_a = data.auth, alg = _a.alg, ts = _a.ts, nonce = _a.nonce, sig = _a.sig;
|
|
297
|
+
if (alg !== 'HMAC-SHA256')
|
|
298
|
+
return [2 /*return*/, false];
|
|
299
|
+
if (typeof ts !== 'number' || typeof nonce !== 'string' || typeof sig !== 'string')
|
|
300
|
+
return [2 /*return*/, false];
|
|
301
|
+
now = Date.now();
|
|
302
|
+
if (Math.abs(now - ts) > cfg.maxSkewMs)
|
|
303
|
+
return [2 /*return*/, false
|
|
304
|
+
// The nonce is recorded only once the signature is known good: burning it
|
|
305
|
+
// here let an unsigned or badly signed message consume the nonce of a
|
|
306
|
+
// legitimate one still in flight.
|
|
307
|
+
];
|
|
308
|
+
// The nonce is recorded only once the signature is known good: burning it
|
|
309
|
+
// here let an unsigned or badly signed message consume the nonce of a
|
|
310
|
+
// legitimate one still in flight.
|
|
311
|
+
if (this.seenNonces.has(nonce))
|
|
312
|
+
return [2 /*return*/, false];
|
|
313
|
+
return [4 /*yield*/, hmacSha256Base64Url(cfg.secret, canonicalMessage(data, ts, nonce))];
|
|
314
|
+
case 1:
|
|
315
|
+
expected = _b.sent();
|
|
316
|
+
if (!constantTimeEqual(expected, sig))
|
|
317
|
+
return [2 /*return*/, false];
|
|
318
|
+
this.seenNonces.set(nonce, ts);
|
|
319
|
+
this.pruneNonces(cfg.replayWindowSize);
|
|
320
|
+
return [2 /*return*/, true];
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
};
|
|
91
325
|
WIO.prototype.debug = function () {
|
|
92
326
|
var args = [];
|
|
93
327
|
for (var _i = 0; _i < arguments.length; _i++) {
|
|
@@ -340,12 +574,29 @@ var WIO = /** @class */ (function () {
|
|
|
340
574
|
* Handle incoming message from WebView
|
|
341
575
|
*/
|
|
342
576
|
WIO.prototype.handleMessage = function (event) {
|
|
577
|
+
var _this = this;
|
|
343
578
|
try {
|
|
344
579
|
var data = JSON.parse(event.nativeEvent.data);
|
|
345
580
|
// Enhanced security: check valid message structure
|
|
346
581
|
if (typeof data !== 'object' || !data.hasOwnProperty('_event'))
|
|
347
582
|
return;
|
|
348
|
-
var _a = data,
|
|
583
|
+
var _a = data, v = _a.v, _event_1 = _a._event, payload_1 = _a.payload, cid_1 = _a.cid, timestamp = _a.timestamp, token = _a.token;
|
|
584
|
+
/**
|
|
585
|
+
* A peer that predates versioning sends no `v`, so absence reads as 1
|
|
586
|
+
* rather than as a refusal. Only a peer speaking a NEWER protocol than
|
|
587
|
+
* this build understands is turned away.
|
|
588
|
+
*/
|
|
589
|
+
var messageVersion = v || 1;
|
|
590
|
+
if (messageVersion > PROTOCOL_VERSION) {
|
|
591
|
+
this.fire('error', {
|
|
592
|
+
type: 'UNSUPPORTED_VERSION',
|
|
593
|
+
received: messageVersion,
|
|
594
|
+
supported: PROTOCOL_VERSION
|
|
595
|
+
});
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
if (!this.peer.protocolVersion || this.peer.protocolVersion < messageVersion)
|
|
599
|
+
this.peer.protocolVersion = messageVersion;
|
|
349
600
|
// Validate origin if specified
|
|
350
601
|
if (this.peer.origin && event.nativeEvent && 'origin' in event.nativeEvent) {
|
|
351
602
|
var messageOrigin = event.nativeEvent.origin;
|
|
@@ -355,18 +606,18 @@ var WIO = /** @class */ (function () {
|
|
|
355
606
|
}
|
|
356
607
|
}
|
|
357
608
|
// Handle heartbeat responses
|
|
358
|
-
if (
|
|
609
|
+
if (_event_1 === '__heartbeat_response') {
|
|
359
610
|
this.peer.lastHeartbeat = Date.now();
|
|
360
611
|
return;
|
|
361
612
|
}
|
|
362
613
|
// Handle heartbeat requests
|
|
363
|
-
if (
|
|
614
|
+
if (_event_1 === '__heartbeat') {
|
|
364
615
|
this.emit('__heartbeat_response', { timestamp: Date.now() });
|
|
365
616
|
this.peer.lastHeartbeat = Date.now();
|
|
366
617
|
return;
|
|
367
618
|
}
|
|
368
619
|
// Handle embedded ready announcement
|
|
369
|
-
if (
|
|
620
|
+
if (_event_1 === '__embedded_ready') {
|
|
370
621
|
this.peer.embeddedReady = true;
|
|
371
622
|
this.debug("[".concat(this.peer.type, "] Embedded peer ready"));
|
|
372
623
|
// If we're WEBVIEW and not connected, send ping
|
|
@@ -376,13 +627,13 @@ var WIO = /** @class */ (function () {
|
|
|
376
627
|
return;
|
|
377
628
|
}
|
|
378
629
|
// Handle webview ready signal
|
|
379
|
-
if (
|
|
630
|
+
if (_event_1 === '__webview_ready') {
|
|
380
631
|
this.debug("[".concat(this.peer.type, "] WebView peer ready"));
|
|
381
632
|
return;
|
|
382
633
|
}
|
|
383
|
-
this.debug("[".concat(this.peer.type, "] Message: ").concat(
|
|
634
|
+
this.debug("[".concat(this.peer.type, "] Message: ").concat(_event_1), payload_1 || '');
|
|
384
635
|
// Handshake: ping event
|
|
385
|
-
if (
|
|
636
|
+
if (_event_1 === 'ping') {
|
|
386
637
|
// EMBEDDED receives ping from WEBVIEW
|
|
387
638
|
if (this.peer.type === 'EMBEDDED') {
|
|
388
639
|
this.connectionToken = token;
|
|
@@ -393,7 +644,7 @@ var WIO = /** @class */ (function () {
|
|
|
393
644
|
return;
|
|
394
645
|
}
|
|
395
646
|
// Handshake: pong event
|
|
396
|
-
if (
|
|
647
|
+
if (_event_1 === 'pong') {
|
|
397
648
|
// WEBVIEW receives pong from EMBEDDED
|
|
398
649
|
if (this.peer.type === 'WEBVIEW') {
|
|
399
650
|
// Validate token if provided
|
|
@@ -416,7 +667,7 @@ var WIO = /** @class */ (function () {
|
|
|
416
667
|
return;
|
|
417
668
|
}
|
|
418
669
|
// Handshake: connection ack
|
|
419
|
-
if (
|
|
670
|
+
if (_event_1 === '__connection_ack') {
|
|
420
671
|
// EMBEDDED receives ack from WEBVIEW
|
|
421
672
|
if (this.peer.type === 'EMBEDDED') {
|
|
422
673
|
// Validate token if provided
|
|
@@ -435,29 +686,63 @@ var WIO = /** @class */ (function () {
|
|
|
435
686
|
}
|
|
436
687
|
return;
|
|
437
688
|
}
|
|
689
|
+
// Cryptographic authentication (optional)
|
|
690
|
+
if (this.options.cryptoAuth) {
|
|
691
|
+
this.verifyIncomingAuth(data)
|
|
692
|
+
.then(function (ok) {
|
|
693
|
+
if (!ok) {
|
|
694
|
+
_this.fire('error', { type: 'AUTH_FAILED', event: _event_1 });
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
// Optional application-level incoming validation (non-reserved events only)
|
|
698
|
+
if (!RESERVED_EVENTS.includes(_event_1)) {
|
|
699
|
+
if (_this.options.allowedIncomingEvents
|
|
700
|
+
&& !_this.options.allowedIncomingEvents.includes(_event_1)) {
|
|
701
|
+
_this.fire('error', {
|
|
702
|
+
type: 'DISALLOWED_EVENT',
|
|
703
|
+
direction: 'incoming',
|
|
704
|
+
event: _event_1
|
|
705
|
+
});
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
if (_this.options.validateIncoming
|
|
709
|
+
&& !_this.options.validateIncoming(_event_1, payload_1)) {
|
|
710
|
+
_this.fire('error', {
|
|
711
|
+
type: 'INVALID_MESSAGE',
|
|
712
|
+
direction: 'incoming',
|
|
713
|
+
event: _event_1
|
|
714
|
+
});
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
_this.fire(_event_1, payload_1, cid_1);
|
|
719
|
+
})
|
|
720
|
+
.catch(function (error) { return _this.fire('error', { type: 'AUTH_ERROR', event: _event_1, error: String(error) }); });
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
438
723
|
// Optional application-level incoming validation (non-reserved events only)
|
|
439
|
-
if (!RESERVED_EVENTS.includes(
|
|
724
|
+
if (!RESERVED_EVENTS.includes(_event_1)) {
|
|
440
725
|
if (this.options.allowedIncomingEvents
|
|
441
|
-
&& !this.options.allowedIncomingEvents.includes(
|
|
726
|
+
&& !this.options.allowedIncomingEvents.includes(_event_1)) {
|
|
442
727
|
this.fire('error', {
|
|
443
728
|
type: 'DISALLOWED_EVENT',
|
|
444
729
|
direction: 'incoming',
|
|
445
|
-
event:
|
|
730
|
+
event: _event_1
|
|
446
731
|
});
|
|
447
732
|
return;
|
|
448
733
|
}
|
|
449
734
|
if (this.options.validateIncoming
|
|
450
|
-
&& !this.options.validateIncoming(
|
|
735
|
+
&& !this.options.validateIncoming(_event_1, payload_1)) {
|
|
451
736
|
this.fire('error', {
|
|
452
737
|
type: 'INVALID_MESSAGE',
|
|
453
738
|
direction: 'incoming',
|
|
454
|
-
event:
|
|
739
|
+
event: _event_1
|
|
455
740
|
});
|
|
456
741
|
return;
|
|
457
742
|
}
|
|
458
743
|
}
|
|
459
744
|
// Fire available event listeners
|
|
460
|
-
this.fire(
|
|
745
|
+
this.fire(_event_1, payload_1, cid_1);
|
|
461
746
|
}
|
|
462
747
|
catch (error) {
|
|
463
748
|
this.debug("[".concat(this.peer.type, "] Message handling error:"), error);
|
|
@@ -546,6 +831,7 @@ var WIO = /** @class */ (function () {
|
|
|
546
831
|
});
|
|
547
832
|
}
|
|
548
833
|
var messageData = {
|
|
834
|
+
v: PROTOCOL_VERSION,
|
|
549
835
|
_event: _event,
|
|
550
836
|
payload: sanitizedPayload,
|
|
551
837
|
cid: cid,
|
|
@@ -568,6 +854,105 @@ var WIO = /** @class */ (function () {
|
|
|
568
854
|
}
|
|
569
855
|
return this;
|
|
570
856
|
};
|
|
857
|
+
/**
|
|
858
|
+
* Send a signed message (HMAC-SHA256) when `options.cryptoAuth` is configured.
|
|
859
|
+
* This is async because WebCrypto signing is async.
|
|
860
|
+
*/
|
|
861
|
+
WIO.prototype.emitSigned = function (_event, payload, fn) {
|
|
862
|
+
var _a;
|
|
863
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
864
|
+
var sanitizedPayload, cid, ackFunction_2, unsigned, auth, messageData, error_1;
|
|
865
|
+
return __generator(this, function (_b) {
|
|
866
|
+
switch (_b.label) {
|
|
867
|
+
case 0:
|
|
868
|
+
if (!this.checkRateLimit())
|
|
869
|
+
return [2 /*return*/, this];
|
|
870
|
+
if (!this.options.cryptoAuth) {
|
|
871
|
+
this.emit(_event, payload, fn);
|
|
872
|
+
return [2 /*return*/, this];
|
|
873
|
+
}
|
|
874
|
+
if (!this.isConnected() && !RESERVED_EVENTS.includes(_event)) {
|
|
875
|
+
this.queueMessage(_event, payload, fn);
|
|
876
|
+
return [2 /*return*/, this];
|
|
877
|
+
}
|
|
878
|
+
if (!this.peer.webViewRef) {
|
|
879
|
+
this.fire('error', { type: 'NO_CONNECTION', event: _event });
|
|
880
|
+
return [2 /*return*/, this];
|
|
881
|
+
}
|
|
882
|
+
if (typeof payload == 'function') {
|
|
883
|
+
fn = payload;
|
|
884
|
+
payload = undefined;
|
|
885
|
+
}
|
|
886
|
+
_b.label = 1;
|
|
887
|
+
case 1:
|
|
888
|
+
_b.trys.push([1, 3, , 4]);
|
|
889
|
+
sanitizedPayload = payload
|
|
890
|
+
? sanitizePayload(payload, this.options.maxMessageSize)
|
|
891
|
+
: payload;
|
|
892
|
+
cid = void 0;
|
|
893
|
+
if (typeof fn === 'function') {
|
|
894
|
+
ackFunction_2 = fn;
|
|
895
|
+
cid = ackId();
|
|
896
|
+
this.once("".concat(_event, "--").concat(cid, "--@ack"), function (_a) {
|
|
897
|
+
var error = _a.error, args = _a.args;
|
|
898
|
+
return ackFunction_2.apply(void 0, __spreadArray([error], args, false));
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
unsigned = {
|
|
902
|
+
v: PROTOCOL_VERSION,
|
|
903
|
+
_event: _event,
|
|
904
|
+
payload: sanitizedPayload,
|
|
905
|
+
cid: cid,
|
|
906
|
+
timestamp: Date.now(),
|
|
907
|
+
size: getMessageSize(sanitizedPayload),
|
|
908
|
+
token: RESERVED_EVENTS.includes(_event) ? this.connectionToken : undefined
|
|
909
|
+
};
|
|
910
|
+
return [4 /*yield*/, this.signOutgoing(unsigned)];
|
|
911
|
+
case 2:
|
|
912
|
+
auth = _b.sent();
|
|
913
|
+
messageData = __assign(__assign({}, unsigned), { auth: auth });
|
|
914
|
+
(_a = this.peer.webViewRef.current) === null || _a === void 0 ? void 0 : _a.postMessage(JSON.stringify(newObject(messageData)));
|
|
915
|
+
return [3 /*break*/, 4];
|
|
916
|
+
case 3:
|
|
917
|
+
error_1 = _b.sent();
|
|
918
|
+
this.debug("[".concat(this.peer.type, "] EmitSigned error:"), error_1);
|
|
919
|
+
this.fire('error', {
|
|
920
|
+
type: 'EMIT_ERROR',
|
|
921
|
+
event: _event,
|
|
922
|
+
error: error_1 instanceof Error ? error_1.message : String(error_1)
|
|
923
|
+
});
|
|
924
|
+
typeof fn === 'function'
|
|
925
|
+
&& fn(error_1 instanceof Error ? error_1.message : String(error_1));
|
|
926
|
+
return [3 /*break*/, 4];
|
|
927
|
+
case 4: return [2 /*return*/, this];
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
});
|
|
931
|
+
};
|
|
932
|
+
WIO.prototype.emitAsyncSigned = function (_event, payload, timeout) {
|
|
933
|
+
if (timeout === void 0) { timeout = 5000; }
|
|
934
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
935
|
+
var _this = this;
|
|
936
|
+
return __generator(this, function (_a) {
|
|
937
|
+
return [2 /*return*/, new Promise(function (resolve, reject) {
|
|
938
|
+
var timeoutId = setTimeout(function () { return reject(new Error("Event '".concat(_event, "' acknowledgment timeout after ").concat(timeout, "ms"))); }, timeout);
|
|
939
|
+
_this.emitSigned(_event, payload, function (error) {
|
|
940
|
+
var args = [];
|
|
941
|
+
for (var _i = 1; _i < arguments.length; _i++) {
|
|
942
|
+
args[_i - 1] = arguments[_i];
|
|
943
|
+
}
|
|
944
|
+
clearTimeout(timeoutId);
|
|
945
|
+
error
|
|
946
|
+
? reject(new Error(typeof error === 'string' ? error : 'Ack error'))
|
|
947
|
+
: resolve(args.length === 0 ? undefined : args.length === 1 ? args[0] : args);
|
|
948
|
+
}).catch(function (err) {
|
|
949
|
+
clearTimeout(timeoutId);
|
|
950
|
+
reject(err);
|
|
951
|
+
});
|
|
952
|
+
})];
|
|
953
|
+
});
|
|
954
|
+
});
|
|
955
|
+
};
|
|
571
956
|
WIO.prototype.on = function (_event, fn) {
|
|
572
957
|
// Add Event listener
|
|
573
958
|
if (!this.Events[_event])
|
|
@@ -710,7 +1095,9 @@ var WIO = /** @class */ (function () {
|
|
|
710
1095
|
* NOTE: Does not auto-initialize - page must call window._wio.listen()
|
|
711
1096
|
*/
|
|
712
1097
|
WIO.prototype.getInjectedJavaScript = function () {
|
|
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 ";
|
|
1098
|
+
var _a, _b, _c, _d, _e;
|
|
1099
|
+
var authSecret = (_a = this.options.cryptoAuth) === null || _a === void 0 ? void 0 : _a.secret;
|
|
1100
|
+
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 authSecret: ".concat(authSecret ? JSON.stringify(authSecret) : 'null', ",\n setupComplete: false,\n seenNonces: new Map(),\n maxSkewMs: ").concat(((_c = (_b = this.options.cryptoAuth) === null || _b === void 0 ? void 0 : _b.maxSkewMs) !== null && _c !== void 0 ? _c : 2 * 60 * 1000), ",\n replayWindowSize: ").concat(((_e = (_d = this.options.cryptoAuth) === null || _d === void 0 ? void 0 : _d.replayWindowSize) !== null && _e !== void 0 ? _e : 500), ",\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 emitSigned: async 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(), signed: true })\n console.debug('[EMBEDDED] Queued signed 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 unsigned = {\n v: ").concat(PROTOCOL_VERSION, ",\n _event,\n payload,\n cid,\n timestamp: Date.now(),\n size: (function(){ try { return JSON.stringify( payload ).length } catch(e){ return 0 } })(),\n token: RESERVED_EVENTS.includes(_event) ? window._wio.connectionToken : undefined\n }\n\n const auth = await window._wio.sign(unsigned)\n const messageData = { ...unsigned, auth }\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] EmitSigned 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 {\n msg.signed\n ? window._wio.emitSigned( msg._event, msg.payload, msg.fn )\n : window._wio.emit( msg._event, msg.payload, msg.fn )\n }\n catch( error ){ console.error('[EMBEDDED] Queue process error:', error ) }\n })\n },\n\n canonicalFields: ").concat(JSON.stringify(CANONICAL_FIELDS), ",\n\n // Must produce byte-identical output to canonicalMessage() on the\n // native side \u2014 hence the shared field list above.\n canonical: function( data, ts, nonce ){\n const out = {}\n window._wio.canonicalFields.forEach(function( field ){ out[field] = data[field] })\n out.ts = ts\n out.nonce = nonce\n return JSON.stringify( out )\n },\n\n pruneNonces: function(){\n const cutoff = Date.now() - window._wio.maxSkewMs\n const stale = []\n window._wio.seenNonces.forEach(function( ts, nonce ){ if( ts < cutoff ) stale.push( nonce ) })\n stale.forEach(function( nonce ){ window._wio.seenNonces.delete( nonce ) })\n\n if( window._wio.seenNonces.size <= window._wio.replayWindowSize ) return\n\n const toRemove = window._wio.seenNonces.size - window._wio.replayWindowSize\n const keys = Array.from( window._wio.seenNonces.keys() )\n for( let k = 0; k < toRemove && k < keys.length; k++ )\n window._wio.seenNonces.delete( keys[k] )\n },\n\n constantTimeEqual: function(a, b){\n if( a.length !== b.length ) return false\n let out = 0\n for( let i = 0; i < a.length; i++ ) out |= a.charCodeAt(i) ^ b.charCodeAt(i)\n return out === 0\n },\n\n hmacSha256Base64Url: async function(secret, message){\n if( !secret ) throw new Error('Missing auth secret')\n if( !window.crypto || !window.crypto.subtle ) throw new Error('WebCrypto unavailable')\n\n const enc = new TextEncoder()\n const key = await window.crypto.subtle.importKey(\n 'raw',\n enc.encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign']\n )\n const sig = await window.crypto.subtle.sign('HMAC', key, enc.encode(message))\n const bytes = new Uint8Array(sig)\n let bin = ''\n for( let i = 0; i < bytes.length; i++ ) bin += String.fromCharCode(bytes[i])\n const b64 = btoa(bin).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/g, '')\n return b64\n },\n\n sign: async function(unsigned){\n if( !window._wio.authSecret ) return null\n const ts = Date.now()\n const nonce = (function(){\n try {\n if( window.crypto && window.crypto.getRandomValues ){\n const buf = new Uint8Array(16)\n window.crypto.getRandomValues(buf)\n let out = ''\n for( let i = 0; i < buf.length; i++ ){\n out += ('0' + buf[i].toString(16)).slice(-2)\n }\n return out\n }\n } catch(e){}\n return (Math.random().toString(16).slice(2) + Math.random().toString(16).slice(2)).slice(0, 32)\n })()\n const sig = await window._wio.hmacSha256Base64Url(window._wio.authSecret, window._wio.canonical(unsigned, ts, nonce))\n return { alg: 'HMAC-SHA256', ts, nonce, sig }\n },\n\n verify: async function(data){\n if( !window._wio.authSecret ) return true\n if( !data.auth ) return false\n const { alg, ts, nonce, sig } = data.auth\n if( alg !== 'HMAC-SHA256' ) return false\n\n const now = Date.now()\n if( Math.abs(now - ts) > window._wio.maxSkewMs ) return false\n\n if( window._wio.seenNonces.has(nonce) ) return false\n\n const expected = await window._wio.hmacSha256Base64Url(window._wio.authSecret, window._wio.canonical(data, ts, nonce))\n if( !window._wio.constantTimeEqual(expected, sig) ) return false\n\n window._wio.seenNonces.set(nonce, ts)\n window._wio.pruneNonces()\n\n return true\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 // Auth verification (optional, if authSecret is set)\n if( window._wio.authSecret ){\n window._wio.verify(data).then(ok => {\n if( !ok ){\n console.error('[EMBEDDED] Auth verification failed for', _event)\n return\n }\n window._wio.fire( _event, payload, cid )\n }).catch(err => console.error('[EMBEDDED] Auth error:', err))\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 ");
|
|
714
1101
|
};
|
|
715
1102
|
return WIO;
|
|
716
1103
|
}());
|
package/package.json
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webview.io",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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",
|
|
7
7
|
"private": false,
|
|
8
8
|
"scripts": {
|
|
9
9
|
"compile": "rimraf ./dist && tsc",
|
|
10
|
-
"test": "
|
|
11
|
-
"
|
|
12
|
-
"test:unit": "nyc mocha --require ts-node/register --reporter spec --slow 200 --bail --timeout 10000 test/webview.io.ts",
|
|
13
|
-
"prepack": "yarn run compile"
|
|
10
|
+
"test": "npm run compile && node --test \"test/*.test.mjs\"",
|
|
11
|
+
"prepack": "npm run compile"
|
|
14
12
|
},
|
|
15
13
|
"dependencies": {
|
|
16
14
|
"events": "^3.3.0"
|