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/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,11 +103,101 @@ 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);
|
|
46
183
|
};
|
|
47
184
|
var generateToken = function () {
|
|
185
|
+
// Prefer cryptographically strong randomness when available
|
|
186
|
+
try {
|
|
187
|
+
var globalCrypto = (typeof crypto !== 'undefined'
|
|
188
|
+
? crypto
|
|
189
|
+
: (typeof window !== 'undefined' && window.crypto)
|
|
190
|
+
|| (typeof globalThis !== 'undefined' && globalThis.crypto));
|
|
191
|
+
if (globalCrypto && typeof globalCrypto.getRandomValues === 'function') {
|
|
192
|
+
var buffer = new Uint32Array(4);
|
|
193
|
+
globalCrypto.getRandomValues(buffer);
|
|
194
|
+
var randomPart = Array.from(buffer).map(function (n) { return n.toString(16); }).join('');
|
|
195
|
+
return "".concat(Date.now(), "_").concat(randomPart);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch (_a) {
|
|
199
|
+
// Fall back to Math.random-based implementation below
|
|
200
|
+
}
|
|
48
201
|
return "".concat(Date.now(), "_").concat(Math.random().toString(36).substring(2, 15));
|
|
49
202
|
};
|
|
50
203
|
var RESERVED_EVENTS = [
|
|
@@ -64,6 +217,7 @@ var WIO = /** @class */ (function () {
|
|
|
64
217
|
this.reconnectAttempts = 0;
|
|
65
218
|
this.maxReconnectAttempts = 5;
|
|
66
219
|
this.connectionAttempts = 0;
|
|
220
|
+
this.seenNonces = new Map();
|
|
67
221
|
if (options && typeof options !== 'object')
|
|
68
222
|
throw new Error('Invalid Options');
|
|
69
223
|
this.options = __assign({ debug: false, heartbeatInterval: 30000, connectionTimeout: 10000, maxMessageSize: 1024 * 1024, maxMessagesPerSecond: 100, autoReconnect: true, messageQueueSize: 50, connectionPingInterval: 2000, maxConnectionAttempts: 5 }, options);
|
|
@@ -72,6 +226,102 @@ var WIO = /** @class */ (function () {
|
|
|
72
226
|
if (options.type)
|
|
73
227
|
this.peer.type = options.type;
|
|
74
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
|
+
};
|
|
75
325
|
WIO.prototype.debug = function () {
|
|
76
326
|
var args = [];
|
|
77
327
|
for (var _i = 0; _i < arguments.length; _i++) {
|
|
@@ -324,12 +574,29 @@ var WIO = /** @class */ (function () {
|
|
|
324
574
|
* Handle incoming message from WebView
|
|
325
575
|
*/
|
|
326
576
|
WIO.prototype.handleMessage = function (event) {
|
|
577
|
+
var _this = this;
|
|
327
578
|
try {
|
|
328
579
|
var data = JSON.parse(event.nativeEvent.data);
|
|
329
580
|
// Enhanced security: check valid message structure
|
|
330
581
|
if (typeof data !== 'object' || !data.hasOwnProperty('_event'))
|
|
331
582
|
return;
|
|
332
|
-
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;
|
|
333
600
|
// Validate origin if specified
|
|
334
601
|
if (this.peer.origin && event.nativeEvent && 'origin' in event.nativeEvent) {
|
|
335
602
|
var messageOrigin = event.nativeEvent.origin;
|
|
@@ -339,18 +606,18 @@ var WIO = /** @class */ (function () {
|
|
|
339
606
|
}
|
|
340
607
|
}
|
|
341
608
|
// Handle heartbeat responses
|
|
342
|
-
if (
|
|
609
|
+
if (_event_1 === '__heartbeat_response') {
|
|
343
610
|
this.peer.lastHeartbeat = Date.now();
|
|
344
611
|
return;
|
|
345
612
|
}
|
|
346
613
|
// Handle heartbeat requests
|
|
347
|
-
if (
|
|
614
|
+
if (_event_1 === '__heartbeat') {
|
|
348
615
|
this.emit('__heartbeat_response', { timestamp: Date.now() });
|
|
349
616
|
this.peer.lastHeartbeat = Date.now();
|
|
350
617
|
return;
|
|
351
618
|
}
|
|
352
619
|
// Handle embedded ready announcement
|
|
353
|
-
if (
|
|
620
|
+
if (_event_1 === '__embedded_ready') {
|
|
354
621
|
this.peer.embeddedReady = true;
|
|
355
622
|
this.debug("[".concat(this.peer.type, "] Embedded peer ready"));
|
|
356
623
|
// If we're WEBVIEW and not connected, send ping
|
|
@@ -360,13 +627,13 @@ var WIO = /** @class */ (function () {
|
|
|
360
627
|
return;
|
|
361
628
|
}
|
|
362
629
|
// Handle webview ready signal
|
|
363
|
-
if (
|
|
630
|
+
if (_event_1 === '__webview_ready') {
|
|
364
631
|
this.debug("[".concat(this.peer.type, "] WebView peer ready"));
|
|
365
632
|
return;
|
|
366
633
|
}
|
|
367
|
-
this.debug("[".concat(this.peer.type, "] Message: ").concat(
|
|
634
|
+
this.debug("[".concat(this.peer.type, "] Message: ").concat(_event_1), payload_1 || '');
|
|
368
635
|
// Handshake: ping event
|
|
369
|
-
if (
|
|
636
|
+
if (_event_1 === 'ping') {
|
|
370
637
|
// EMBEDDED receives ping from WEBVIEW
|
|
371
638
|
if (this.peer.type === 'EMBEDDED') {
|
|
372
639
|
this.connectionToken = token;
|
|
@@ -377,7 +644,7 @@ var WIO = /** @class */ (function () {
|
|
|
377
644
|
return;
|
|
378
645
|
}
|
|
379
646
|
// Handshake: pong event
|
|
380
|
-
if (
|
|
647
|
+
if (_event_1 === 'pong') {
|
|
381
648
|
// WEBVIEW receives pong from EMBEDDED
|
|
382
649
|
if (this.peer.type === 'WEBVIEW') {
|
|
383
650
|
// Validate token if provided
|
|
@@ -400,7 +667,7 @@ var WIO = /** @class */ (function () {
|
|
|
400
667
|
return;
|
|
401
668
|
}
|
|
402
669
|
// Handshake: connection ack
|
|
403
|
-
if (
|
|
670
|
+
if (_event_1 === '__connection_ack') {
|
|
404
671
|
// EMBEDDED receives ack from WEBVIEW
|
|
405
672
|
if (this.peer.type === 'EMBEDDED') {
|
|
406
673
|
// Validate token if provided
|
|
@@ -419,8 +686,63 @@ var WIO = /** @class */ (function () {
|
|
|
419
686
|
}
|
|
420
687
|
return;
|
|
421
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
|
+
}
|
|
723
|
+
// Optional application-level incoming validation (non-reserved events only)
|
|
724
|
+
if (!RESERVED_EVENTS.includes(_event_1)) {
|
|
725
|
+
if (this.options.allowedIncomingEvents
|
|
726
|
+
&& !this.options.allowedIncomingEvents.includes(_event_1)) {
|
|
727
|
+
this.fire('error', {
|
|
728
|
+
type: 'DISALLOWED_EVENT',
|
|
729
|
+
direction: 'incoming',
|
|
730
|
+
event: _event_1
|
|
731
|
+
});
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
if (this.options.validateIncoming
|
|
735
|
+
&& !this.options.validateIncoming(_event_1, payload_1)) {
|
|
736
|
+
this.fire('error', {
|
|
737
|
+
type: 'INVALID_MESSAGE',
|
|
738
|
+
direction: 'incoming',
|
|
739
|
+
event: _event_1
|
|
740
|
+
});
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
422
744
|
// Fire available event listeners
|
|
423
|
-
this.fire(
|
|
745
|
+
this.fire(_event_1, payload_1, cid_1);
|
|
424
746
|
}
|
|
425
747
|
catch (error) {
|
|
426
748
|
this.debug("[".concat(this.peer.type, "] Message handling error:"), error);
|
|
@@ -509,6 +831,7 @@ var WIO = /** @class */ (function () {
|
|
|
509
831
|
});
|
|
510
832
|
}
|
|
511
833
|
var messageData = {
|
|
834
|
+
v: PROTOCOL_VERSION,
|
|
512
835
|
_event: _event,
|
|
513
836
|
payload: sanitizedPayload,
|
|
514
837
|
cid: cid,
|
|
@@ -531,6 +854,105 @@ var WIO = /** @class */ (function () {
|
|
|
531
854
|
}
|
|
532
855
|
return this;
|
|
533
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
|
+
};
|
|
534
956
|
WIO.prototype.on = function (_event, fn) {
|
|
535
957
|
// Add Event listener
|
|
536
958
|
if (!this.Events[_event])
|
|
@@ -673,7 +1095,9 @@ var WIO = /** @class */ (function () {
|
|
|
673
1095
|
* NOTE: Does not auto-initialize - page must call window._wio.listen()
|
|
674
1096
|
*/
|
|
675
1097
|
WIO.prototype.getInjectedJavaScript = function () {
|
|
676
|
-
return "\n (function() {\n try {\n console.log('[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.log('[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.log('[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.log('[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.log('[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.log('[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.log('[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.log('[EMBEDDED] WebView ready signal received')\n return\n }\n \n // Handle ping from WEBVIEW\n if( _event === 'ping' ){\n console.log('[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.log('[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.log('[EMBEDDED] Starting ready announcements')\n \n const announce = () => {\n if( window._wio.connected ){\n console.log('[EMBEDDED] Connected, stopping announcements')\n return\n }\n \n attempts++\n if( attempts > maxAttempts ){\n console.log('[EMBEDDED] Max announcement attempts reached')\n return\n }\n \n console.log('[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.log('[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 ");
|
|
677
1101
|
};
|
|
678
1102
|
return WIO;
|
|
679
1103
|
}());
|
package/package.json
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webview.io",
|
|
3
|
-
"version": "1.0
|
|
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"
|