@condorinst/mobile-bridge 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1005 @@
1
+ // src/keys.ts
2
+ var BRIDGE_KEYS = {
3
+ pingPong: "pingPong",
4
+ userDevice: "userDevice",
5
+ deviceLightSensors: "deviceLightSensors",
6
+ syncTime: "syncTime",
7
+ realTime: "realTime",
8
+ deleteMemory: "deleteMemory",
9
+ deviceNotification: "deviceNotification",
10
+ deviceConnection: "deviceConnection",
11
+ sensorLog: "sensorLog",
12
+ dfu: "dfu"
13
+ };
14
+
15
+ // src/internal/dev-panel.ts
16
+ var PANEL_ID = "condor-mock-panel-root";
17
+ var REFRESH_INTERVAL_MS = 500;
18
+ var DEFAULTS = {
19
+ position: "bottom-right",
20
+ maxMessages: 20,
21
+ theme: "dark",
22
+ startCollapsed: false,
23
+ startAutoRespondEnabled: false,
24
+ autoRespondDelayMs: 50
25
+ };
26
+ function resolveOptions(opts) {
27
+ return {
28
+ position: opts?.position ?? DEFAULTS.position,
29
+ maxMessages: opts?.maxMessages ?? DEFAULTS.maxMessages,
30
+ theme: opts?.theme ?? DEFAULTS.theme,
31
+ startCollapsed: opts?.startCollapsed ?? DEFAULTS.startCollapsed,
32
+ startAutoRespondEnabled: opts?.startAutoRespondEnabled ?? DEFAULTS.startAutoRespondEnabled,
33
+ autoRespondDelayMs: opts?.autoRespondDelayMs ?? DEFAULTS.autoRespondDelayMs
34
+ };
35
+ }
36
+ function applyPanelStyles(el, opts) {
37
+ const isDark = opts.theme === "dark";
38
+ const positions = {
39
+ "bottom-right": { bottom: "12px", right: "12px" },
40
+ "bottom-left": { bottom: "12px", left: "12px" },
41
+ "top-right": { top: "12px", right: "12px" },
42
+ "top-left": { top: "12px", left: "12px" }
43
+ };
44
+ Object.assign(el.style, {
45
+ position: "fixed",
46
+ width: "340px",
47
+ maxHeight: "420px",
48
+ zIndex: "2147483647",
49
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
50
+ fontSize: "12px",
51
+ color: isDark ? "#e0e0e0" : "#222",
52
+ background: isDark ? "#1a1a1aee" : "#ffffffee",
53
+ border: isDark ? "1px solid #444" : "1px solid #ccc",
54
+ borderRadius: "6px",
55
+ boxShadow: "0 4px 12px rgba(0,0,0,0.3)",
56
+ overflow: "hidden",
57
+ display: "flex",
58
+ flexDirection: "column",
59
+ ...positions[opts.position]
60
+ });
61
+ }
62
+ function buildHeader(badge, counter, toggle) {
63
+ const header = document.createElement("div");
64
+ header.className = "condor-mock-panel-header";
65
+ Object.assign(header.style, {
66
+ padding: "6px 10px",
67
+ display: "flex",
68
+ alignItems: "center",
69
+ gap: "8px",
70
+ background: "#0a84ff",
71
+ color: "#fff",
72
+ fontWeight: "600",
73
+ cursor: "pointer"
74
+ });
75
+ Object.assign(badge.style, {
76
+ background: "#fff",
77
+ color: "#0a84ff",
78
+ padding: "1px 6px",
79
+ borderRadius: "3px",
80
+ fontSize: "10px",
81
+ letterSpacing: "0.5px"
82
+ });
83
+ badge.textContent = "MOCK";
84
+ Object.assign(counter.style, {
85
+ marginLeft: "auto",
86
+ fontSize: "11px",
87
+ opacity: "0.9"
88
+ });
89
+ Object.assign(toggle.style, {
90
+ background: "transparent",
91
+ border: "1px solid rgba(255,255,255,0.5)",
92
+ color: "#fff",
93
+ cursor: "pointer",
94
+ padding: "0 6px",
95
+ borderRadius: "3px",
96
+ fontFamily: "inherit"
97
+ });
98
+ header.append(badge, document.createTextNode("condor-bridge"), counter, toggle);
99
+ return header;
100
+ }
101
+ function buildSentSection() {
102
+ const container = document.createElement("div");
103
+ container.className = "condor-mock-panel-sent";
104
+ Object.assign(container.style, {
105
+ padding: "6px 10px",
106
+ borderBottom: "1px solid rgba(255,255,255,0.08)",
107
+ maxHeight: "180px",
108
+ overflowY: "auto"
109
+ });
110
+ const title = document.createElement("div");
111
+ title.textContent = "Sent";
112
+ Object.assign(title.style, { opacity: "0.7", marginBottom: "4px", fontSize: "11px" });
113
+ const list = document.createElement("div");
114
+ list.className = "condor-mock-panel-sent-list";
115
+ container.append(title, list);
116
+ return { container, list };
117
+ }
118
+ function buildTriggerSection(bridgeKeys, onEmit, onClear) {
119
+ const container = document.createElement("div");
120
+ container.className = "condor-mock-panel-trigger";
121
+ Object.assign(container.style, {
122
+ padding: "6px 10px",
123
+ display: "flex",
124
+ flexDirection: "column",
125
+ gap: "4px"
126
+ });
127
+ const title = document.createElement("div");
128
+ title.textContent = "Trigger emit";
129
+ Object.assign(title.style, { opacity: "0.7", fontSize: "11px" });
130
+ const select = document.createElement("select");
131
+ select.className = "condor-mock-panel-key-select";
132
+ Object.assign(select.style, {
133
+ background: "transparent",
134
+ color: "inherit",
135
+ border: "1px solid rgba(255,255,255,0.2)",
136
+ padding: "4px",
137
+ borderRadius: "3px",
138
+ fontFamily: "inherit",
139
+ fontSize: "11px"
140
+ });
141
+ for (const key of bridgeKeys) {
142
+ const opt = document.createElement("option");
143
+ opt.value = key;
144
+ opt.textContent = key;
145
+ select.appendChild(opt);
146
+ }
147
+ const ackOpt = document.createElement("option");
148
+ ackOpt.value = "handshake-ack";
149
+ ackOpt.textContent = "handshake-ack";
150
+ select.appendChild(ackOpt);
151
+ const textarea = document.createElement("textarea");
152
+ textarea.className = "condor-mock-panel-payload";
153
+ textarea.placeholder = '{ "data": "pong" }';
154
+ Object.assign(textarea.style, {
155
+ background: "transparent",
156
+ color: "inherit",
157
+ border: "1px solid rgba(255,255,255,0.2)",
158
+ padding: "4px",
159
+ borderRadius: "3px",
160
+ fontFamily: "inherit",
161
+ fontSize: "11px",
162
+ minHeight: "50px",
163
+ resize: "vertical"
164
+ });
165
+ const buttons = document.createElement("div");
166
+ Object.assign(buttons.style, { display: "flex", gap: "6px" });
167
+ const emitBtn = document.createElement("button");
168
+ emitBtn.type = "button";
169
+ emitBtn.textContent = "Emit";
170
+ emitBtn.className = "condor-mock-panel-emit-btn";
171
+ Object.assign(emitBtn.style, {
172
+ background: "#0a84ff",
173
+ color: "#fff",
174
+ border: "none",
175
+ padding: "4px 10px",
176
+ borderRadius: "3px",
177
+ cursor: "pointer",
178
+ fontFamily: "inherit",
179
+ fontSize: "11px",
180
+ flex: "1"
181
+ });
182
+ emitBtn.addEventListener("click", () => {
183
+ onEmit(select.value, textarea.value);
184
+ });
185
+ const clearBtn = document.createElement("button");
186
+ clearBtn.type = "button";
187
+ clearBtn.textContent = "Clear sent";
188
+ clearBtn.className = "condor-mock-panel-clear-btn";
189
+ Object.assign(clearBtn.style, {
190
+ background: "transparent",
191
+ color: "inherit",
192
+ border: "1px solid rgba(255,255,255,0.3)",
193
+ padding: "4px 10px",
194
+ borderRadius: "3px",
195
+ cursor: "pointer",
196
+ fontFamily: "inherit",
197
+ fontSize: "11px"
198
+ });
199
+ clearBtn.addEventListener("click", onClear);
200
+ buttons.append(emitBtn, clearBtn);
201
+ container.append(title, select, textarea, buttons);
202
+ return container;
203
+ }
204
+ function buildAutoRespondSection(initialEnabled, onChange) {
205
+ const container = document.createElement("div");
206
+ container.className = "condor-mock-panel-auto-respond";
207
+ Object.assign(container.style, {
208
+ padding: "6px 10px",
209
+ borderBottom: "1px solid rgba(255,255,255,0.08)",
210
+ display: "flex",
211
+ flexDirection: "column",
212
+ gap: "2px"
213
+ });
214
+ const title = document.createElement("div");
215
+ title.textContent = "Auto-respond";
216
+ Object.assign(title.style, { opacity: "0.7", fontSize: "11px" });
217
+ const row = document.createElement("label");
218
+ Object.assign(row.style, {
219
+ display: "flex",
220
+ alignItems: "center",
221
+ gap: "8px",
222
+ cursor: "pointer",
223
+ fontSize: "12px"
224
+ });
225
+ const checkbox = document.createElement("input");
226
+ checkbox.type = "checkbox";
227
+ checkbox.className = "condor-mock-panel-auto-respond-toggle";
228
+ checkbox.checked = initialEnabled;
229
+ Object.assign(checkbox.style, {
230
+ cursor: "pointer"
231
+ });
232
+ const label = document.createElement("span");
233
+ label.textContent = "Enabled \u2014 mockEmit em resposta a cada send (policy do cliente)";
234
+ checkbox.addEventListener("change", () => {
235
+ onChange(checkbox.checked);
236
+ });
237
+ row.append(checkbox, label);
238
+ container.append(title, row);
239
+ return container;
240
+ }
241
+ function renderSentList(list, adapter, max) {
242
+ list.innerHTML = "";
243
+ const all = adapter.getMessages();
244
+ const tail = all.slice(-max).reverse();
245
+ for (const msg of tail) {
246
+ const row = document.createElement("div");
247
+ Object.assign(row.style, {
248
+ padding: "3px 0",
249
+ borderBottom: "1px solid rgba(255,255,255,0.06)",
250
+ wordBreak: "break-all"
251
+ });
252
+ const ts = new Date(msg.timestamp).toLocaleTimeString();
253
+ const payload = msg.payload;
254
+ const type = payload?.type ?? "?";
255
+ row.textContent = `${ts} \u2192 ${type} ${JSON.stringify(msg.payload).slice(0, 80)}`;
256
+ list.appendChild(row);
257
+ }
258
+ }
259
+ var currentPanel = null;
260
+ function installDevPanel(params) {
261
+ if (typeof document === "undefined") {
262
+ throw new Error("condor-mobile-bridge dev panel: document is undefined (Node-only env).");
263
+ }
264
+ if (currentPanel) {
265
+ currentPanel.uninstall();
266
+ currentPanel = null;
267
+ }
268
+ const opts = resolveOptions(params.options);
269
+ const root = document.createElement("div");
270
+ root.id = PANEL_ID;
271
+ applyPanelStyles(root, opts);
272
+ const badge = document.createElement("span");
273
+ const counter = document.createElement("span");
274
+ const toggle = document.createElement("button");
275
+ toggle.type = "button";
276
+ toggle.textContent = opts.startCollapsed ? "+" : "\u2212";
277
+ const header = buildHeader(badge, counter, toggle);
278
+ const body = document.createElement("div");
279
+ Object.assign(body.style, {
280
+ display: opts.startCollapsed ? "none" : "flex",
281
+ flexDirection: "column",
282
+ overflow: "hidden"
283
+ });
284
+ const { container: sentContainer, list: sentList } = buildSentSection();
285
+ const policy = params.options?.onSendForAutoRespond;
286
+ let autoRespondEnabled = opts.startAutoRespondEnabled;
287
+ let unsubscribeSendListener = null;
288
+ let autoRespondSection = null;
289
+ if (policy) {
290
+ autoRespondSection = buildAutoRespondSection(autoRespondEnabled, (next) => {
291
+ autoRespondEnabled = next;
292
+ });
293
+ unsubscribeSendListener = params.adapter.onCallHandler((msg) => {
294
+ if (!autoRespondEnabled) return;
295
+ let response;
296
+ try {
297
+ response = policy(msg);
298
+ } catch (err) {
299
+ console.error("[condor-mobile-bridge dev-panel] onSendForAutoRespond threw:", err);
300
+ return;
301
+ }
302
+ if (!response) return;
303
+ setTimeout(() => {
304
+ try {
305
+ params.adapter.triggerEmit(response.key, response.payload);
306
+ } catch (err) {
307
+ console.error(
308
+ "[condor-mobile-bridge dev-panel] triggerEmit failed in auto-respond:",
309
+ err
310
+ );
311
+ }
312
+ }, opts.autoRespondDelayMs);
313
+ });
314
+ }
315
+ const trigger = buildTriggerSection(
316
+ Object.values(BRIDGE_KEYS),
317
+ (key, payloadJson) => {
318
+ let payload = {};
319
+ const trimmed = payloadJson.trim();
320
+ if (trimmed.length > 0) {
321
+ try {
322
+ payload = JSON.parse(trimmed);
323
+ } catch (err) {
324
+ console.error("[condor-mobile-bridge dev-panel] invalid JSON payload", err);
325
+ return;
326
+ }
327
+ }
328
+ params.adapter.triggerEmit(key, payload);
329
+ },
330
+ () => {
331
+ params.adapter.clearMessages();
332
+ renderSentList(sentList, params.adapter, opts.maxMessages);
333
+ counter.textContent = "0 sent";
334
+ }
335
+ );
336
+ if (autoRespondSection) {
337
+ body.append(sentContainer, autoRespondSection, trigger);
338
+ } else {
339
+ body.append(sentContainer, trigger);
340
+ }
341
+ root.append(header, body);
342
+ toggle.addEventListener("click", (e) => {
343
+ e.stopPropagation();
344
+ const collapsed = body.style.display === "none";
345
+ body.style.display = collapsed ? "flex" : "none";
346
+ toggle.textContent = collapsed ? "\u2212" : "+";
347
+ });
348
+ document.body.appendChild(root);
349
+ const refresh = () => {
350
+ const count = params.adapter.getMessages().length;
351
+ counter.textContent = `${count} sent`;
352
+ renderSentList(sentList, params.adapter, opts.maxMessages);
353
+ };
354
+ refresh();
355
+ const interval = setInterval(refresh, REFRESH_INTERVAL_MS);
356
+ const panel = {
357
+ uninstall() {
358
+ clearInterval(interval);
359
+ unsubscribeSendListener?.();
360
+ unsubscribeSendListener = null;
361
+ root.remove();
362
+ if (currentPanel === panel) currentPanel = null;
363
+ },
364
+ refresh
365
+ };
366
+ currentPanel = panel;
367
+ return panel;
368
+ }
369
+
370
+ // src/internal/mock.ts
371
+ var MAX_BUFFER = 100;
372
+ function createMockAdapter() {
373
+ const messages = [];
374
+ const sendListeners = /* @__PURE__ */ new Set();
375
+ return {
376
+ isMock: true,
377
+ callHandler(handlerName, payload) {
378
+ const msg = { handlerName, payload, timestamp: Date.now() };
379
+ messages.push(msg);
380
+ if (messages.length > MAX_BUFFER) {
381
+ messages.shift();
382
+ }
383
+ console.debug("[condor-mobile-bridge mock] \u2192", handlerName, payload);
384
+ for (const listener of sendListeners) {
385
+ try {
386
+ listener(msg);
387
+ } catch (err) {
388
+ console.error("[condor-mobile-bridge mock] send listener threw:", err);
389
+ }
390
+ }
391
+ },
392
+ getMessages() {
393
+ return messages;
394
+ },
395
+ clearMessages() {
396
+ messages.length = 0;
397
+ },
398
+ triggerEmit(key, payload) {
399
+ const bridge = typeof window !== "undefined" ? window.CondorBridge : void 0;
400
+ if (!bridge || typeof bridge.emit !== "function") {
401
+ throw new Error(
402
+ "condor-mobile-bridge mock: window.CondorBridge not bootstrapped; cannot emit."
403
+ );
404
+ }
405
+ bridge.emit(key, payload);
406
+ },
407
+ onCallHandler(listener) {
408
+ sendListeners.add(listener);
409
+ return () => {
410
+ sendListeners.delete(listener);
411
+ };
412
+ }
413
+ };
414
+ }
415
+
416
+ // src/internal/native.ts
417
+ var activeAdapter = null;
418
+ function createRealAdapter(host) {
419
+ return {
420
+ isMock: false,
421
+ callHandler(handlerName, payload) {
422
+ void host.callHandler(handlerName, payload);
423
+ }
424
+ };
425
+ }
426
+ function detectAdapter() {
427
+ if (typeof window === "undefined") {
428
+ return createMockAdapter();
429
+ }
430
+ const host = window.flutter_inappwebview;
431
+ if (host && typeof host.callHandler === "function") {
432
+ return createRealAdapter(host);
433
+ }
434
+ return createMockAdapter();
435
+ }
436
+ function getActiveAdapter() {
437
+ if (!activeAdapter) {
438
+ activeAdapter = detectAdapter();
439
+ }
440
+ return activeAdapter;
441
+ }
442
+ function callNative(handlerName, payload) {
443
+ getActiveAdapter().callHandler(handlerName, payload);
444
+ }
445
+
446
+ // src/internal/subscribers.ts
447
+ var subscribers = /* @__PURE__ */ new Map();
448
+ function addSubscriber(key, cb) {
449
+ let set = subscribers.get(key);
450
+ if (!set) {
451
+ set = /* @__PURE__ */ new Set();
452
+ subscribers.set(key, set);
453
+ }
454
+ set.add(cb);
455
+ return () => {
456
+ const current = subscribers.get(key);
457
+ if (!current) return;
458
+ current.delete(cb);
459
+ if (current.size === 0) {
460
+ subscribers.delete(key);
461
+ }
462
+ };
463
+ }
464
+ function dispatchToSubscribers(key, payload) {
465
+ const set = subscribers.get(key);
466
+ if (!set) return;
467
+ for (const cb of set) {
468
+ try {
469
+ cb(payload);
470
+ } catch (err) {
471
+ console.error(`[condor-mobile-bridge] subscriber for "${key}" threw:`, err);
472
+ }
473
+ }
474
+ }
475
+ function getSubscriberKeys() {
476
+ return Array.from(subscribers.keys());
477
+ }
478
+
479
+ // src/bridge.ts
480
+ var HANDSHAKE_ACK_KEY = "handshake-ack";
481
+ var NATIVE_HANDLER = "CondorBridge";
482
+ var PROTOCOL_VERSION = "0.1.0";
483
+ var pendingHandshake = null;
484
+ var bridgeBootstrapped = false;
485
+ function emit(key, payload) {
486
+ if (key === HANDSHAKE_ACK_KEY && pendingHandshake) {
487
+ const ack = payload ?? {};
488
+ const resolver = pendingHandshake;
489
+ pendingHandshake = null;
490
+ resolver.resolve({
491
+ nativeVersion: ack.nativeVersion ?? "unknown",
492
+ accepted: ack.accepted ?? false
493
+ });
494
+ return;
495
+ }
496
+ dispatchToSubscribers(key, payload);
497
+ }
498
+ function bootstrapWindowBridge() {
499
+ if (bridgeBootstrapped) return;
500
+ if (typeof window === "undefined") return;
501
+ bridgeBootstrapped = true;
502
+ if (window.CondorBridge && typeof window.CondorBridge.emit === "function") {
503
+ window.CondorBridge.emit = emit;
504
+ } else {
505
+ window.CondorBridge = { emit };
506
+ }
507
+ }
508
+ bootstrapWindowBridge();
509
+ function requireMockAdapter() {
510
+ const adapter = getActiveAdapter();
511
+ if (!adapter.isMock) {
512
+ throw new Error(
513
+ "condor-mobile-bridge: this API is available only in mock mode (no native host detected)."
514
+ );
515
+ }
516
+ return adapter;
517
+ }
518
+ var Bridge = {
519
+ get isMock() {
520
+ return getActiveAdapter().isMock;
521
+ },
522
+ handshake(opts) {
523
+ bootstrapWindowBridge();
524
+ if (pendingHandshake) {
525
+ return Promise.reject(
526
+ new Error(
527
+ "condor-mobile-bridge: handshake already in flight. Await the previous call before issuing another."
528
+ )
529
+ );
530
+ }
531
+ return new Promise((resolve, reject) => {
532
+ pendingHandshake = { resolve, reject };
533
+ try {
534
+ callNative(NATIVE_HANDLER, {
535
+ type: "handshake",
536
+ version: PROTOCOL_VERSION,
537
+ payload: { clientVersion: opts.clientVersion }
538
+ });
539
+ } catch (err) {
540
+ pendingHandshake = null;
541
+ reject(err instanceof Error ? err : new Error(String(err)));
542
+ }
543
+ });
544
+ },
545
+ on(key, cb) {
546
+ bootstrapWindowBridge();
547
+ return addSubscriber(key, cb);
548
+ },
549
+ send(type, payload) {
550
+ callNative(NATIVE_HANDLER, {
551
+ type,
552
+ version: PROTOCOL_VERSION,
553
+ payload: payload ?? {}
554
+ });
555
+ },
556
+ openSettings() {
557
+ this.send("openSettings");
558
+ },
559
+ mockEmit(key, payload) {
560
+ requireMockAdapter().triggerEmit(key, payload ?? {});
561
+ },
562
+ getMockState() {
563
+ const adapter = requireMockAdapter();
564
+ return {
565
+ isMock: true,
566
+ messages: adapter.getMessages(),
567
+ subscriberKeys: getSubscriberKeys()
568
+ };
569
+ },
570
+ installDevPanel(options) {
571
+ const adapter = requireMockAdapter();
572
+ installDevPanel({ adapter, options });
573
+ }
574
+ };
575
+
576
+ // src/bridge-client.ts
577
+ var PROTOCOL_VERSION2 = "0.5.0";
578
+ var HANDSHAKE_TIMEOUT_MS = 5e3;
579
+ var subscribers2 = /* @__PURE__ */ new Map();
580
+ var pendingHandshake2 = null;
581
+ var messageListenerInstalled = false;
582
+ function postToShell(msg) {
583
+ if (typeof window === "undefined" || window.parent === window) {
584
+ console.warn("[BridgeClient] no parent window; message dropped:", msg.type);
585
+ return;
586
+ }
587
+ window.parent.postMessage(msg, "*");
588
+ }
589
+ function dispatchEvent(key, payload) {
590
+ const arr = subscribers2.get(key);
591
+ if (!arr || arr.length === 0) return;
592
+ for (const sub of arr) {
593
+ try {
594
+ sub.cb(payload);
595
+ } catch (err) {
596
+ console.error("[BridgeClient] subscriber error:", err);
597
+ }
598
+ }
599
+ }
600
+ function onMessage(event) {
601
+ if (event.source !== window.parent) return;
602
+ const data = event.data;
603
+ if (!data || typeof data !== "object" || typeof data.type !== "string") return;
604
+ switch (data.type) {
605
+ case "event": {
606
+ const msg = data;
607
+ if (typeof msg.key === "string") dispatchEvent(msg.key, msg.payload);
608
+ break;
609
+ }
610
+ case "handshake-ack": {
611
+ const msg = data;
612
+ if (pendingHandshake2) {
613
+ clearTimeout(pendingHandshake2.timer);
614
+ const { resolve } = pendingHandshake2;
615
+ pendingHandshake2 = null;
616
+ resolve({
617
+ shellVersion: msg.ack?.shellVersion ?? "unknown",
618
+ accepted: msg.ack?.accepted ?? false
619
+ });
620
+ }
621
+ break;
622
+ }
623
+ case "nack":
624
+ console.warn("[BridgeClient] command nacked by shell:", data);
625
+ break;
626
+ case "verify-sandbox": {
627
+ const w = window;
628
+ postToShell({
629
+ type: "sandbox-verify-response",
630
+ flutterBridgeReachable: typeof w.flutter_inappwebview !== "undefined",
631
+ condorBridgeReachable: typeof w.CondorBridge !== "undefined"
632
+ });
633
+ break;
634
+ }
635
+ }
636
+ }
637
+ function ensureMessageListener() {
638
+ if (messageListenerInstalled) return;
639
+ if (typeof window === "undefined") return;
640
+ messageListenerInstalled = true;
641
+ window.addEventListener("message", onMessage);
642
+ }
643
+ ensureMessageListener();
644
+ var BridgeClient = {
645
+ protocolVersion: PROTOCOL_VERSION2,
646
+ on(key, cb) {
647
+ ensureMessageListener();
648
+ const arr = subscribers2.get(key) ?? [];
649
+ const sub = { cb };
650
+ arr.push(sub);
651
+ subscribers2.set(key, arr);
652
+ postToShell({ type: "subscribe", key });
653
+ return () => {
654
+ const remaining = (subscribers2.get(key) ?? []).filter((s) => s !== sub);
655
+ subscribers2.set(key, remaining);
656
+ if (remaining.length === 0) postToShell({ type: "unsubscribe", key });
657
+ };
658
+ },
659
+ send(command, args = {}) {
660
+ postToShell({ type: "send", command, args });
661
+ },
662
+ openSettings() {
663
+ this.send("openSettings");
664
+ },
665
+ handshake(opts) {
666
+ ensureMessageListener();
667
+ if (pendingHandshake2) {
668
+ return Promise.reject(
669
+ new Error(
670
+ "BridgeClient: handshake already in flight. Await previous before issuing another."
671
+ )
672
+ );
673
+ }
674
+ return new Promise((resolve, reject) => {
675
+ const timer = setTimeout(() => {
676
+ pendingHandshake2 = null;
677
+ reject(new Error(`BridgeClient: handshake timeout after ${HANDSHAKE_TIMEOUT_MS}ms`));
678
+ }, HANDSHAKE_TIMEOUT_MS);
679
+ pendingHandshake2 = { resolve, reject, timer };
680
+ postToShell({
681
+ type: "handshake",
682
+ payload: { clientVersion: opts.clientVersion }
683
+ });
684
+ });
685
+ }
686
+ };
687
+
688
+ // src/bridge-shell.ts
689
+ import * as v2 from "valibot";
690
+
691
+ // src/shell-messages.ts
692
+ import * as v from "valibot";
693
+ var BridgeKeySchema = v.picklist(Object.values(BRIDGE_KEYS));
694
+ var SubscribeMessage = v.object({
695
+ type: v.literal("subscribe"),
696
+ key: BridgeKeySchema
697
+ });
698
+ var UnsubscribeMessage = v.object({
699
+ type: v.literal("unsubscribe"),
700
+ key: BridgeKeySchema
701
+ });
702
+ var SendMessage = v.object({
703
+ type: v.literal("send"),
704
+ command: v.string(),
705
+ args: v.optional(v.record(v.string(), v.unknown()), {})
706
+ });
707
+ var HandshakeMessage = v.object({
708
+ type: v.literal("handshake"),
709
+ payload: v.object({ clientVersion: v.string() })
710
+ });
711
+ var JsErrorMessage = v.object({
712
+ type: v.literal("jsError"),
713
+ payload: v.object({
714
+ message: v.string(),
715
+ stack: v.optional(v.string()),
716
+ source: v.optional(v.string()),
717
+ lineno: v.optional(v.number()),
718
+ colno: v.optional(v.number())
719
+ })
720
+ });
721
+ var SandboxVerifyResponseMessage = v.object({
722
+ type: v.literal("sandbox-verify-response"),
723
+ flutterBridgeReachable: v.boolean(),
724
+ condorBridgeReachable: v.boolean()
725
+ });
726
+ var ClientToShellMessage = v.variant("type", [
727
+ SubscribeMessage,
728
+ UnsubscribeMessage,
729
+ SendMessage,
730
+ HandshakeMessage,
731
+ JsErrorMessage,
732
+ SandboxVerifyResponseMessage
733
+ ]);
734
+ var EventMessage = v.object({
735
+ type: v.literal("event"),
736
+ key: BridgeKeySchema,
737
+ payload: v.unknown()
738
+ });
739
+ var HandshakeAckMessage = v.object({
740
+ type: v.literal("handshake-ack"),
741
+ ack: v.object({
742
+ shellVersion: v.string(),
743
+ accepted: v.boolean()
744
+ })
745
+ });
746
+ var NackMessage = v.object({
747
+ type: v.literal("nack"),
748
+ key: v.optional(v.string()),
749
+ command: v.optional(v.string()),
750
+ reason: v.string()
751
+ });
752
+ var VerifySandboxMessage = v.object({
753
+ type: v.literal("verify-sandbox")
754
+ });
755
+ var ShellToClientMessage = v.variant("type", [
756
+ EventMessage,
757
+ HandshakeAckMessage,
758
+ NackMessage,
759
+ VerifySandboxMessage
760
+ ]);
761
+
762
+ // src/bridge-shell.ts
763
+ var SHELL_VERSION = "0.5.1";
764
+ var VERIFY_TIMEOUT_MS = 2e3;
765
+ var OVERRIDE_PREFIX = `<script>
766
+ (function(){
767
+ try { delete window.flutter_inappwebview; } catch (_) {}
768
+ try { delete window.CondorBridge; } catch (_) {}
769
+ try {
770
+ Object.defineProperty(window, 'flutter_inappwebview', {
771
+ get: function(){ return undefined; },
772
+ set: function(_){ /* no-op: bloqueia inje\xE7\xE3o do plugin no iframe */ },
773
+ configurable: false,
774
+ });
775
+ } catch (_) {}
776
+ try {
777
+ Object.defineProperty(window, 'CondorBridge', {
778
+ get: function(){ return undefined; },
779
+ set: function(_){ /* no-op: bloqueia bootstrap do package legado */ },
780
+ configurable: false,
781
+ });
782
+ } catch (_) {}
783
+ })();
784
+ </script>`;
785
+ var BridgeShell = class {
786
+ constructor(opts) {
787
+ this.subscriptions = /* @__PURE__ */ new Map();
788
+ this.detached = false;
789
+ // Map de `verify-sandbox` round-trips pendentes — key gerada por nonce.
790
+ this.pendingVerifies = /* @__PURE__ */ new Map();
791
+ this.onMessage = async (event) => {
792
+ if (this.detached) return;
793
+ if (event.source !== this.iframe.contentWindow) return;
794
+ const parsed = v2.safeParse(ClientToShellMessage, event.data);
795
+ if (!parsed.success) {
796
+ if (typeof console !== "undefined") {
797
+ console.debug("[BridgeShell] dropped malformed message:", parsed.issues?.[0]?.message);
798
+ }
799
+ return;
800
+ }
801
+ const msg = parsed.output;
802
+ switch (msg.type) {
803
+ case "subscribe":
804
+ await this.handleSubscribe(msg.key);
805
+ break;
806
+ case "unsubscribe":
807
+ this.handleUnsubscribe(msg.key);
808
+ break;
809
+ case "send":
810
+ await this.handleSend(msg.command, msg.args ?? {});
811
+ break;
812
+ case "handshake":
813
+ this.postToClient({
814
+ type: "handshake-ack",
815
+ ack: { shellVersion: SHELL_VERSION, accepted: true }
816
+ });
817
+ break;
818
+ case "jsError":
819
+ Bridge.send("jsError", {
820
+ origin: "iframe",
821
+ message: msg.payload.message,
822
+ stack: msg.payload.stack,
823
+ source: msg.payload.source,
824
+ lineno: msg.payload.lineno,
825
+ colno: msg.payload.colno
826
+ });
827
+ break;
828
+ case "sandbox-verify-response": {
829
+ const first = this.pendingVerifies.keys().next().value;
830
+ if (first !== void 0) {
831
+ const pending = this.pendingVerifies.get(first);
832
+ if (pending) {
833
+ clearTimeout(pending.timer);
834
+ this.pendingVerifies.delete(first);
835
+ const ok = !msg.flutterBridgeReachable && !msg.condorBridgeReachable;
836
+ pending.resolve({
837
+ flutterBridgeReachable: msg.flutterBridgeReachable,
838
+ condorBridgeReachable: msg.condorBridgeReachable,
839
+ ok
840
+ });
841
+ }
842
+ }
843
+ break;
844
+ }
845
+ }
846
+ };
847
+ this.iframe = opts.iframe;
848
+ this.policy = opts.policy;
849
+ this.onConfirm = opts.onConfirmationRequest;
850
+ if (typeof window !== "undefined") {
851
+ window.addEventListener("message", this.onMessage);
852
+ }
853
+ }
854
+ /**
855
+ * **Camada 1 do Risco 1.** Bootstrap via fetch de URL.
856
+ *
857
+ * Atalho para o caso comum HTTP/HTTPS: faz `fetch(clientPath)` e
858
+ * delega para `bootstrapClientIframeWithHtml`.
859
+ *
860
+ * **Não funciona em scheme `file://`** — `fetch` em WebView Android
861
+ * (Chromium) bloqueia o scheme com "URL scheme 'file' is not supported".
862
+ * Para asset bundled em APK ou bundle inlined no build-time, use
863
+ * `bootstrapClientIframeWithHtml(htmlString)`.
864
+ *
865
+ * **Devo ser chamado uma vez no boot do shell**, antes de qualquer
866
+ * outra interação. Após retornar, `verifySandbox()` deve ser invocado
867
+ * para atestation (Camada 2).
868
+ */
869
+ async bootstrapClientIframe(clientPath = "client.html") {
870
+ const res = await fetch(clientPath);
871
+ if (!res.ok) throw new Error(`BridgeShell: falha ao carregar ${clientPath} (${res.status})`);
872
+ const clientHtml = await res.text();
873
+ await this.bootstrapClientIframeWithHtml(clientHtml);
874
+ }
875
+ /**
876
+ * **Camada 1 do Risco 1.** Bootstrap a partir de HTML string.
877
+ *
878
+ * Aceita HTML do cliente diretamente (já lido de arquivo, embedado em
879
+ * build-time via `esbuild.define`, lido de IndexedDB, etc.) e seta
880
+ * como `srcdoc` do iframe prefixado pelo override script. Override
881
+ * roda em parse-time, antes de qualquer outro script do cliente.
882
+ *
883
+ * Use quando `fetch` não é aplicável (asset bundled em `file://`,
884
+ * bundle inlined no build, qualquer cenário sem rede). Para HTTP
885
+ * use `bootstrapClientIframe(clientPath)`.
886
+ *
887
+ * **Devo ser chamado uma vez no boot do shell**, antes de qualquer
888
+ * outra interação. Após retornar, `verifySandbox()` deve ser invocado
889
+ * para atestation (Camada 2).
890
+ */
891
+ async bootstrapClientIframeWithHtml(clientHtml) {
892
+ this.iframe.srcdoc = OVERRIDE_PREFIX + clientHtml;
893
+ await this.waitForIframeLoad();
894
+ }
895
+ waitForIframeLoad() {
896
+ return new Promise((resolve) => {
897
+ const onLoad = () => {
898
+ this.iframe.removeEventListener("load", onLoad);
899
+ resolve();
900
+ };
901
+ this.iframe.addEventListener("load", onLoad);
902
+ });
903
+ }
904
+ /**
905
+ * **Camada 2 do Risco 1.** Round-trip `postMessage` que pede ao iframe
906
+ * atestation autoritativa do estado de `window.flutter_inappwebview`
907
+ * e `window.CondorBridge`. Timeout em 2s.
908
+ *
909
+ * `ok` é `true` apenas se ambos forem `undefined` no iframe. Se vier
910
+ * `false`, **shell deve abortar o boot do app** — Camada 1 falhou e
911
+ * Camada 4 (dual WebView) precisa ser acionada manualmente.
912
+ */
913
+ verifySandbox() {
914
+ if (!this.iframe.contentWindow) {
915
+ return Promise.reject(new Error("BridgeShell: iframe sem contentWindow"));
916
+ }
917
+ return new Promise((resolve, reject) => {
918
+ const nonce = Math.random().toString(36).slice(2);
919
+ const timer = setTimeout(() => {
920
+ this.pendingVerifies.delete(nonce);
921
+ reject(new Error(`BridgeShell: verify-sandbox timeout ap\xF3s ${VERIFY_TIMEOUT_MS}ms`));
922
+ }, VERIFY_TIMEOUT_MS);
923
+ this.pendingVerifies.set(nonce, { resolve, reject, timer });
924
+ this.postToClient({ type: "verify-sandbox" });
925
+ });
926
+ }
927
+ /**
928
+ * Encerra o shell — remove listener `message`, cancela subscriptions
929
+ * pendentes no `Bridge` nativo, limpa estado interno.
930
+ */
931
+ detach() {
932
+ if (this.detached) return;
933
+ this.detached = true;
934
+ if (typeof window !== "undefined") {
935
+ window.removeEventListener("message", this.onMessage);
936
+ }
937
+ for (const unsub of this.subscriptions.values()) unsub();
938
+ this.subscriptions.clear();
939
+ for (const pending of this.pendingVerifies.values()) {
940
+ clearTimeout(pending.timer);
941
+ pending.reject(new Error("BridgeShell: detached"));
942
+ }
943
+ this.pendingVerifies.clear();
944
+ }
945
+ async handleSubscribe(key) {
946
+ if (!this.policy.readOnlyKeys.includes(key)) {
947
+ this.postToClient({ type: "nack", key, reason: "not-read-only" });
948
+ return;
949
+ }
950
+ if (this.subscriptions.has(key)) return;
951
+ const unsub = Bridge.on(key, (payload) => {
952
+ if (this.detached) return;
953
+ this.postToClient({ type: "event", key, payload });
954
+ });
955
+ this.subscriptions.set(key, unsub);
956
+ }
957
+ handleUnsubscribe(key) {
958
+ const unsub = this.subscriptions.get(key);
959
+ if (!unsub) return;
960
+ unsub();
961
+ this.subscriptions.delete(key);
962
+ }
963
+ async handleSend(command, args) {
964
+ if (this.policy.deniedCommands.includes(command)) {
965
+ this.postToClient({ type: "nack", command, reason: "denied" });
966
+ return;
967
+ }
968
+ if (this.policy.requiresConfirmation.includes(command)) {
969
+ if (!this.onConfirm) {
970
+ this.postToClient({ type: "nack", command, reason: "no-confirmation-handler" });
971
+ return;
972
+ }
973
+ let approved = false;
974
+ try {
975
+ approved = await this.onConfirm({ command, args });
976
+ } catch (err) {
977
+ this.postToClient({
978
+ type: "nack",
979
+ command,
980
+ reason: `confirmation-error: ${err instanceof Error ? err.message : String(err)}`
981
+ });
982
+ return;
983
+ }
984
+ if (!approved) {
985
+ this.postToClient({ type: "nack", command, reason: "user-rejected" });
986
+ return;
987
+ }
988
+ } else if (!this.policy.allowedCommands.includes(command)) {
989
+ this.postToClient({ type: "nack", command, reason: "not-allowed" });
990
+ return;
991
+ }
992
+ Bridge.send(command, args);
993
+ }
994
+ postToClient(msg) {
995
+ if (this.detached) return;
996
+ this.iframe.contentWindow?.postMessage(msg, "*");
997
+ }
998
+ };
999
+ export {
1000
+ BRIDGE_KEYS,
1001
+ Bridge,
1002
+ BridgeClient,
1003
+ BridgeShell
1004
+ };
1005
+ //# sourceMappingURL=index.js.map