@condorinst/mobile-bridge 0.19.2 → 0.21.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.
@@ -0,0 +1,2110 @@
1
+ "use strict";
2
+ var CondorDevHarness = (() => {
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/dev/index.ts
22
+ var dev_exports = {};
23
+ __export(dev_exports, {
24
+ CANVAS_CSP: () => CANVAS_CSP,
25
+ CANVAS_POLICY: () => CANVAS_POLICY,
26
+ DEFAULT_LOG_PERIOD_S: () => DEFAULT_LOG_PERIOD_S,
27
+ DEV_HARNESS_MARKER: () => DEV_HARNESS_MARKER,
28
+ HARNESS_NATIVE_VERSION: () => HARNESS_NATIVE_VERSION,
29
+ SCENARIOS: () => SCENARIOS,
30
+ createDevHarness: () => createDevHarness,
31
+ createSimulatedNative: () => createSimulatedNative,
32
+ formatLocalTimestamp: () => formatLocalTimestamp,
33
+ parseLocalTimestamp: () => parseLocalTimestamp,
34
+ resolveScenario: () => resolveScenario,
35
+ sensorLogPage: () => sensorLogPage,
36
+ sharedSerial: () => sharedSerial,
37
+ systemStatus: () => systemStatus,
38
+ twoDevices: () => twoDevices
39
+ });
40
+
41
+ // src/dev/fixtures.ts
42
+ var DEFAULT_LOG_PERIOD_S = 60;
43
+ var pad = (n) => String(n).padStart(2, "0");
44
+ function formatLocalTimestamp(date) {
45
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
46
+ }
47
+ function parseLocalTimestamp(value) {
48
+ const m = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/.exec(value);
49
+ if (!m) return Number.NaN;
50
+ const [, y, mo, d, h, mi, s] = m.map(Number);
51
+ return new Date(y ?? 0, (mo ?? 1) - 1, d ?? 1, h ?? 0, mi ?? 0, s ?? 0).getTime();
52
+ }
53
+ function config(spec) {
54
+ const item = {
55
+ deviceId: spec.deviceId,
56
+ serialNo: spec.serialNo,
57
+ deviceBodyPart: spec.bodyPart,
58
+ bleConnectionPeriod: 5,
59
+ autosync: true,
60
+ eraseDeviceMemory: false,
61
+ autoEraseDeviceMemory: false,
62
+ autoEraseDeviceMemoryThreshold: 80,
63
+ readSensorsMemory: false,
64
+ startDevice: false,
65
+ readFullMemory: false,
66
+ allowFirmwareUpdate: true
67
+ };
68
+ if (spec.modelId) item.modelId = spec.modelId;
69
+ return item;
70
+ }
71
+ function telemetry(spec, now) {
72
+ return {
73
+ serialNo: spec.serialNo,
74
+ deviceId: spec.deviceId,
75
+ updataDateTime: now - spec.syncedMinutesAgo * 6e4,
76
+ deviceName: spec.name,
77
+ deviceVersion: spec.modelId === "actlumus_plus" ? "3.0.2" : "2.1.0",
78
+ hardwareVersion: spec.modelId === "actlumus_plus" ? "B" : "A",
79
+ deviceDateTime: now - spec.syncedMinutesAgo * 6e4,
80
+ memoryDataUsage: 37.5,
81
+ memorySystemUsage: 4.2,
82
+ batteryLevel: spec.battery,
83
+ batteryVoltage: 3.7 + spec.battery / 1e3,
84
+ isCharging: spec.charging,
85
+ isOnDock: spec.charging,
86
+ syncErrors: spec.syncError ? [
87
+ {
88
+ serialNo: spec.serialNo,
89
+ attemptDate: now - 10 * 6e4,
90
+ message: spec.syncError,
91
+ messageCode: "SYNC_FAILED"
92
+ }
93
+ ] : []
94
+ };
95
+ }
96
+ function userConfig(spec) {
97
+ return {
98
+ serialNo: spec.serialNo,
99
+ deviceId: spec.deviceId,
100
+ syncTime: false,
101
+ realTime: false,
102
+ deleteMemory: false,
103
+ readAllMemory: false,
104
+ readAllSystemLog: false
105
+ };
106
+ }
107
+ function payloadOf(specs, now = Date.now()) {
108
+ return {
109
+ lastUpdate: now,
110
+ devicesConfig: specs.map(config),
111
+ devicesTelemetry: specs.map((s) => telemetry(s, now)),
112
+ devicesUserConfig: specs.map(userConfig)
113
+ };
114
+ }
115
+ function twoDevices(now = Date.now()) {
116
+ return payloadOf(
117
+ [
118
+ {
119
+ deviceId: 9001,
120
+ serialNo: 4534,
121
+ modelId: "actlumus",
122
+ bodyPart: "wrist",
123
+ name: "ActLumus 4534",
124
+ battery: 82,
125
+ charging: false,
126
+ syncedMinutesAgo: 12
127
+ },
128
+ {
129
+ deviceId: 9002,
130
+ serialNo: 5120,
131
+ modelId: "actlumus_plus",
132
+ bodyPart: "necklace",
133
+ name: "ActLumus Plus 5120",
134
+ battery: 41,
135
+ charging: true,
136
+ syncedMinutesAgo: 190,
137
+ syncError: "Could not read the device memory."
138
+ }
139
+ ],
140
+ now
141
+ );
142
+ }
143
+ function sharedSerial(now = Date.now()) {
144
+ return payloadOf(
145
+ [
146
+ {
147
+ deviceId: 9101,
148
+ serialNo: 7047,
149
+ bodyPart: "wrist",
150
+ name: "ActLumus 7047",
151
+ battery: 67,
152
+ charging: false,
153
+ syncedMinutesAgo: 30
154
+ },
155
+ {
156
+ deviceId: 9102,
157
+ serialNo: 7047,
158
+ modelId: "actlumus_plus",
159
+ bodyPart: "necklace",
160
+ name: "ActLumus Plus 7047",
161
+ battery: 93,
162
+ charging: false,
163
+ syncedMinutesAgo: 8
164
+ }
165
+ ],
166
+ now
167
+ );
168
+ }
169
+ function systemStatus(overrides = {}) {
170
+ return {
171
+ bluetooth: true,
172
+ internet: true,
173
+ permission: true,
174
+ hasDevices: true,
175
+ missingOptionalPermissions: [],
176
+ missingEssentialPermissions: [],
177
+ deviceSyncError: {},
178
+ deviceSyncErrorById: {},
179
+ ...overrides
180
+ };
181
+ }
182
+ function lightAt(epochMs, serialNo) {
183
+ const d = new Date(epochMs);
184
+ const hour = d.getHours() + d.getMinutes() / 60;
185
+ const daylight = Math.max(0, Math.sin((hour - 6) / 12 * Math.PI));
186
+ const noise = (epochMs / 6e4 + serialNo) % 7 / 7;
187
+ const photopic = Math.round((daylight * 1800 + noise * 40) * 10) / 10;
188
+ return { photopic, melanopic: Math.round(photopic * 0.62 * 10) / 10 };
189
+ }
190
+ function logAt(epochMs, serialNo, deviceId) {
191
+ const { photopic, melanopic } = lightAt(epochMs, serialNo);
192
+ return {
193
+ id: Math.floor(epochMs / 1e3) * 10 + serialNo % 10,
194
+ log_time: formatLocalTimestamp(new Date(epochMs)),
195
+ log_type: 1,
196
+ body_part: "wrist",
197
+ temperature: 31.5,
198
+ lux_photopic: photopic,
199
+ lux_melanopic: melanopic,
200
+ device_id: deviceId,
201
+ act_data_pim: 120
202
+ };
203
+ }
204
+ var HISTORY_DAYS = 14;
205
+ var encodeCursor = (epochMs) => `mock:${epochMs}`;
206
+ var decodeCursor = (cursor) => Number(cursor.slice("mock:".length));
207
+ function sensorLogPage(query, opts = {}) {
208
+ const now = opts.now ?? Date.now();
209
+ const periodMs = (opts.periodS ?? DEFAULT_LOG_PERIOD_S) * 1e3;
210
+ const limit = query.limit ?? 10;
211
+ if (limit < 1 || limit > 1e3) {
212
+ return { requestId: query.requestId, success: false, error: "INVALID_LIMIT" };
213
+ }
214
+ if (query.startingAfter && query.endingBefore) {
215
+ return { requestId: query.requestId, success: false, error: "CONFLICTING_CURSORS" };
216
+ }
217
+ const newest = Math.floor(now / periodMs) * periodMs;
218
+ const oldest = newest - HISTORY_DAYS * 864e5;
219
+ const rangeStart = query.startTime ? parseLocalTimestamp(query.startTime) : oldest;
220
+ const rangeEnd = query.endTime ? parseLocalTimestamp(query.endTime) - 1 : newest;
221
+ if (Number.isNaN(rangeStart) || Number.isNaN(rangeEnd) || rangeStart > rangeEnd + 1) {
222
+ return { requestId: query.requestId, success: false, error: "INVALID_TIME_RANGE" };
223
+ }
224
+ const first = Math.max(oldest, Math.ceil(rangeStart / periodMs) * periodMs);
225
+ const last = Math.min(newest, Math.floor(rangeEnd / periodMs) * periodMs);
226
+ let from;
227
+ let to;
228
+ if (query.endingBefore) {
229
+ to = Math.min(last, decodeCursor(query.endingBefore) - periodMs);
230
+ from = Math.max(first, to - (limit - 1) * periodMs);
231
+ } else if (query.startingAfter) {
232
+ from = Math.max(first, decodeCursor(query.startingAfter) + periodMs);
233
+ to = Math.min(last, from + (limit - 1) * periodMs);
234
+ } else if (query.startTime) {
235
+ from = first;
236
+ to = Math.min(last, from + (limit - 1) * periodMs);
237
+ } else {
238
+ to = last;
239
+ from = Math.max(first, to - (limit - 1) * periodMs);
240
+ }
241
+ const logs = [];
242
+ for (let t = from; t <= to; t += periodMs) {
243
+ logs.push(logAt(t, query.serialNo, opts.deviceId ?? 0));
244
+ }
245
+ return {
246
+ requestId: query.requestId,
247
+ success: true,
248
+ logs,
249
+ startingAfter: logs.length ? encodeCursor(to) : null,
250
+ endingBefore: logs.length ? encodeCursor(from) : null,
251
+ hasMore: logs.length > 0 && to < last,
252
+ hasPrevious: logs.length > 0 && from > first
253
+ };
254
+ }
255
+
256
+ // src/keys.ts
257
+ var BRIDGE_KEYS = {
258
+ pingPong: "pingPong",
259
+ userDevice: "userDevice",
260
+ deviceLightSensors: "deviceLightSensors",
261
+ syncTime: "syncTime",
262
+ realTime: "realTime",
263
+ deleteMemory: "deleteMemory",
264
+ deviceNotification: "deviceNotification",
265
+ deviceConnection: "deviceConnection",
266
+ sensorLog: "sensorLog",
267
+ dfu: "dfu",
268
+ systemStatus: "systemStatus",
269
+ /**
270
+ * Native → client: the OS back button was pressed and the client had
271
+ * reported internal history via `reportNavigation(true)`. Go back exactly
272
+ * ONE internal page. Emitted only when the native side decided to delegate
273
+ * the back gesture. See `BridgeClient.onBack`.
274
+ */
275
+ navigationBack: "navigationBack",
276
+ /**
277
+ * Native → client: the app's language state. Carries the raw device locale
278
+ * AND the language explicitly chosen in the app Settings, so the client can
279
+ * apply its own precedence rule — including rendering a language the native
280
+ * chrome does not support. Emitted on attach, after `handshake-ack`, and on
281
+ * every change in Settings. See `LanguagePayload`.
282
+ */
283
+ language: "language"
284
+ };
285
+
286
+ // src/internal/dev-panel.ts
287
+ var PANEL_ID = "condor-mock-panel-root";
288
+ var REFRESH_INTERVAL_MS = 500;
289
+ var DEFAULTS = {
290
+ position: "bottom-right",
291
+ maxMessages: 20,
292
+ theme: "dark",
293
+ startCollapsed: false,
294
+ startAutoRespondEnabled: false,
295
+ autoRespondDelayMs: 50
296
+ };
297
+ function resolveOptions(opts) {
298
+ return {
299
+ position: opts?.position ?? DEFAULTS.position,
300
+ maxMessages: opts?.maxMessages ?? DEFAULTS.maxMessages,
301
+ theme: opts?.theme ?? DEFAULTS.theme,
302
+ startCollapsed: opts?.startCollapsed ?? DEFAULTS.startCollapsed,
303
+ startAutoRespondEnabled: opts?.startAutoRespondEnabled ?? DEFAULTS.startAutoRespondEnabled,
304
+ autoRespondDelayMs: opts?.autoRespondDelayMs ?? DEFAULTS.autoRespondDelayMs
305
+ };
306
+ }
307
+ function applyPanelStyles(el, opts) {
308
+ const isDark = opts.theme === "dark";
309
+ const positions = {
310
+ "bottom-right": { bottom: "12px", right: "12px" },
311
+ "bottom-left": { bottom: "12px", left: "12px" },
312
+ "top-right": { top: "12px", right: "12px" },
313
+ "top-left": { top: "12px", left: "12px" }
314
+ };
315
+ Object.assign(el.style, {
316
+ position: "fixed",
317
+ width: "340px",
318
+ maxHeight: "420px",
319
+ zIndex: "2147483647",
320
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
321
+ fontSize: "12px",
322
+ color: isDark ? "#e0e0e0" : "#222",
323
+ background: isDark ? "#1a1a1aee" : "#ffffffee",
324
+ border: isDark ? "1px solid #444" : "1px solid #ccc",
325
+ borderRadius: "6px",
326
+ boxShadow: "0 4px 12px rgba(0,0,0,0.3)",
327
+ overflow: "hidden",
328
+ display: "flex",
329
+ flexDirection: "column",
330
+ ...positions[opts.position]
331
+ });
332
+ }
333
+ function buildHeader(badge, counter, toggle) {
334
+ const header = document.createElement("div");
335
+ header.className = "condor-mock-panel-header";
336
+ Object.assign(header.style, {
337
+ padding: "6px 10px",
338
+ display: "flex",
339
+ alignItems: "center",
340
+ gap: "8px",
341
+ background: "#0a84ff",
342
+ color: "#fff",
343
+ fontWeight: "600",
344
+ cursor: "pointer"
345
+ });
346
+ Object.assign(badge.style, {
347
+ background: "#fff",
348
+ color: "#0a84ff",
349
+ padding: "1px 6px",
350
+ borderRadius: "3px",
351
+ fontSize: "10px",
352
+ letterSpacing: "0.5px"
353
+ });
354
+ badge.textContent = "MOCK";
355
+ Object.assign(counter.style, {
356
+ marginLeft: "auto",
357
+ fontSize: "11px",
358
+ opacity: "0.9"
359
+ });
360
+ Object.assign(toggle.style, {
361
+ background: "transparent",
362
+ border: "1px solid rgba(255,255,255,0.5)",
363
+ color: "#fff",
364
+ cursor: "pointer",
365
+ padding: "0 6px",
366
+ borderRadius: "3px",
367
+ fontFamily: "inherit"
368
+ });
369
+ header.append(badge, document.createTextNode("condor-bridge"), counter, toggle);
370
+ return header;
371
+ }
372
+ function buildSentSection() {
373
+ const container = document.createElement("div");
374
+ container.className = "condor-mock-panel-sent";
375
+ Object.assign(container.style, {
376
+ padding: "6px 10px",
377
+ borderBottom: "1px solid rgba(255,255,255,0.08)",
378
+ maxHeight: "180px",
379
+ overflowY: "auto"
380
+ });
381
+ const title = document.createElement("div");
382
+ title.textContent = "Sent";
383
+ Object.assign(title.style, { opacity: "0.7", marginBottom: "4px", fontSize: "11px" });
384
+ const list = document.createElement("div");
385
+ list.className = "condor-mock-panel-sent-list";
386
+ container.append(title, list);
387
+ return { container, list };
388
+ }
389
+ function buildTriggerSection(bridgeKeys, onEmit, onClear) {
390
+ const container = document.createElement("div");
391
+ container.className = "condor-mock-panel-trigger";
392
+ Object.assign(container.style, {
393
+ padding: "6px 10px",
394
+ display: "flex",
395
+ flexDirection: "column",
396
+ gap: "4px"
397
+ });
398
+ const title = document.createElement("div");
399
+ title.textContent = "Trigger emit";
400
+ Object.assign(title.style, { opacity: "0.7", fontSize: "11px" });
401
+ const select = document.createElement("select");
402
+ select.className = "condor-mock-panel-key-select";
403
+ Object.assign(select.style, {
404
+ background: "transparent",
405
+ color: "inherit",
406
+ border: "1px solid rgba(255,255,255,0.2)",
407
+ padding: "4px",
408
+ borderRadius: "3px",
409
+ fontFamily: "inherit",
410
+ fontSize: "11px"
411
+ });
412
+ for (const key of bridgeKeys) {
413
+ const opt = document.createElement("option");
414
+ opt.value = key;
415
+ opt.textContent = key;
416
+ select.appendChild(opt);
417
+ }
418
+ const ackOpt = document.createElement("option");
419
+ ackOpt.value = "handshake-ack";
420
+ ackOpt.textContent = "handshake-ack";
421
+ select.appendChild(ackOpt);
422
+ const textarea = document.createElement("textarea");
423
+ textarea.className = "condor-mock-panel-payload";
424
+ textarea.placeholder = '{ "data": "pong" }';
425
+ Object.assign(textarea.style, {
426
+ background: "transparent",
427
+ color: "inherit",
428
+ border: "1px solid rgba(255,255,255,0.2)",
429
+ padding: "4px",
430
+ borderRadius: "3px",
431
+ fontFamily: "inherit",
432
+ fontSize: "11px",
433
+ minHeight: "50px",
434
+ resize: "vertical"
435
+ });
436
+ const buttons = document.createElement("div");
437
+ Object.assign(buttons.style, { display: "flex", gap: "6px" });
438
+ const emitBtn = document.createElement("button");
439
+ emitBtn.type = "button";
440
+ emitBtn.textContent = "Emit";
441
+ emitBtn.className = "condor-mock-panel-emit-btn";
442
+ Object.assign(emitBtn.style, {
443
+ background: "#0a84ff",
444
+ color: "#fff",
445
+ border: "none",
446
+ padding: "4px 10px",
447
+ borderRadius: "3px",
448
+ cursor: "pointer",
449
+ fontFamily: "inherit",
450
+ fontSize: "11px",
451
+ flex: "1"
452
+ });
453
+ emitBtn.addEventListener("click", () => {
454
+ onEmit(select.value, textarea.value);
455
+ });
456
+ const clearBtn = document.createElement("button");
457
+ clearBtn.type = "button";
458
+ clearBtn.textContent = "Clear sent";
459
+ clearBtn.className = "condor-mock-panel-clear-btn";
460
+ Object.assign(clearBtn.style, {
461
+ background: "transparent",
462
+ color: "inherit",
463
+ border: "1px solid rgba(255,255,255,0.3)",
464
+ padding: "4px 10px",
465
+ borderRadius: "3px",
466
+ cursor: "pointer",
467
+ fontFamily: "inherit",
468
+ fontSize: "11px"
469
+ });
470
+ clearBtn.addEventListener("click", onClear);
471
+ buttons.append(emitBtn, clearBtn);
472
+ container.append(title, select, textarea, buttons);
473
+ return container;
474
+ }
475
+ function buildAutoRespondSection(initialEnabled, onChange) {
476
+ const container = document.createElement("div");
477
+ container.className = "condor-mock-panel-auto-respond";
478
+ Object.assign(container.style, {
479
+ padding: "6px 10px",
480
+ borderBottom: "1px solid rgba(255,255,255,0.08)",
481
+ display: "flex",
482
+ flexDirection: "column",
483
+ gap: "2px"
484
+ });
485
+ const title = document.createElement("div");
486
+ title.textContent = "Auto-respond";
487
+ Object.assign(title.style, { opacity: "0.7", fontSize: "11px" });
488
+ const row = document.createElement("label");
489
+ Object.assign(row.style, {
490
+ display: "flex",
491
+ alignItems: "center",
492
+ gap: "8px",
493
+ cursor: "pointer",
494
+ fontSize: "12px"
495
+ });
496
+ const checkbox = document.createElement("input");
497
+ checkbox.type = "checkbox";
498
+ checkbox.className = "condor-mock-panel-auto-respond-toggle";
499
+ checkbox.checked = initialEnabled;
500
+ Object.assign(checkbox.style, {
501
+ cursor: "pointer"
502
+ });
503
+ const label = document.createElement("span");
504
+ label.textContent = "Enabled \u2014 mockEmit em resposta a cada send (policy do cliente)";
505
+ checkbox.addEventListener("change", () => {
506
+ onChange(checkbox.checked);
507
+ });
508
+ row.append(checkbox, label);
509
+ container.append(title, row);
510
+ return container;
511
+ }
512
+ function renderSentList(list, adapter, max) {
513
+ list.innerHTML = "";
514
+ const all = adapter.getMessages();
515
+ const tail = all.slice(-max).reverse();
516
+ for (const msg of tail) {
517
+ const row = document.createElement("div");
518
+ Object.assign(row.style, {
519
+ padding: "3px 0",
520
+ borderBottom: "1px solid rgba(255,255,255,0.06)",
521
+ wordBreak: "break-all"
522
+ });
523
+ const ts = new Date(msg.timestamp).toLocaleTimeString();
524
+ const payload = msg.payload;
525
+ const type = payload?.type ?? "?";
526
+ row.textContent = `${ts} \u2192 ${type} ${JSON.stringify(msg.payload).slice(0, 80)}`;
527
+ list.appendChild(row);
528
+ }
529
+ }
530
+ var currentPanel = null;
531
+ function installDevPanel(params) {
532
+ if (typeof document === "undefined") {
533
+ throw new Error("condor-mobile-bridge dev panel: document is undefined (Node-only env).");
534
+ }
535
+ if (currentPanel) {
536
+ currentPanel.uninstall();
537
+ currentPanel = null;
538
+ }
539
+ const opts = resolveOptions(params.options);
540
+ const root = document.createElement("div");
541
+ root.id = PANEL_ID;
542
+ applyPanelStyles(root, opts);
543
+ const badge = document.createElement("span");
544
+ const counter = document.createElement("span");
545
+ const toggle = document.createElement("button");
546
+ toggle.type = "button";
547
+ toggle.textContent = opts.startCollapsed ? "+" : "\u2212";
548
+ const header = buildHeader(badge, counter, toggle);
549
+ const body = document.createElement("div");
550
+ Object.assign(body.style, {
551
+ display: opts.startCollapsed ? "none" : "flex",
552
+ flexDirection: "column",
553
+ overflow: "hidden"
554
+ });
555
+ const { container: sentContainer, list: sentList } = buildSentSection();
556
+ const policy = params.options?.onSendForAutoRespond;
557
+ let autoRespondEnabled = opts.startAutoRespondEnabled;
558
+ let unsubscribeSendListener = null;
559
+ let autoRespondSection = null;
560
+ if (policy) {
561
+ autoRespondSection = buildAutoRespondSection(autoRespondEnabled, (next) => {
562
+ autoRespondEnabled = next;
563
+ });
564
+ unsubscribeSendListener = params.adapter.onCallHandler((msg) => {
565
+ if (!autoRespondEnabled) return;
566
+ let response;
567
+ try {
568
+ response = policy(msg);
569
+ } catch (err) {
570
+ console.error("[condor-mobile-bridge dev-panel] onSendForAutoRespond threw:", err);
571
+ return;
572
+ }
573
+ if (!response) return;
574
+ setTimeout(() => {
575
+ try {
576
+ params.adapter.triggerEmit(response.key, response.payload);
577
+ } catch (err) {
578
+ console.error(
579
+ "[condor-mobile-bridge dev-panel] triggerEmit failed in auto-respond:",
580
+ err
581
+ );
582
+ }
583
+ }, opts.autoRespondDelayMs);
584
+ });
585
+ }
586
+ const trigger = buildTriggerSection(
587
+ Object.values(BRIDGE_KEYS),
588
+ (key, payloadJson) => {
589
+ let payload = {};
590
+ const trimmed = payloadJson.trim();
591
+ if (trimmed.length > 0) {
592
+ try {
593
+ payload = JSON.parse(trimmed);
594
+ } catch (err) {
595
+ console.error("[condor-mobile-bridge dev-panel] invalid JSON payload", err);
596
+ return;
597
+ }
598
+ }
599
+ params.adapter.triggerEmit(key, payload);
600
+ },
601
+ () => {
602
+ params.adapter.clearMessages();
603
+ renderSentList(sentList, params.adapter, opts.maxMessages);
604
+ counter.textContent = "0 sent";
605
+ }
606
+ );
607
+ if (autoRespondSection) {
608
+ body.append(sentContainer, autoRespondSection, trigger);
609
+ } else {
610
+ body.append(sentContainer, trigger);
611
+ }
612
+ root.append(header, body);
613
+ toggle.addEventListener("click", (e) => {
614
+ e.stopPropagation();
615
+ const collapsed = body.style.display === "none";
616
+ body.style.display = collapsed ? "flex" : "none";
617
+ toggle.textContent = collapsed ? "\u2212" : "+";
618
+ });
619
+ document.body.appendChild(root);
620
+ const refresh = () => {
621
+ const count = params.adapter.getMessages().length;
622
+ counter.textContent = `${count} sent`;
623
+ renderSentList(sentList, params.adapter, opts.maxMessages);
624
+ };
625
+ refresh();
626
+ const interval = setInterval(refresh, REFRESH_INTERVAL_MS);
627
+ const panel = {
628
+ uninstall() {
629
+ clearInterval(interval);
630
+ unsubscribeSendListener?.();
631
+ unsubscribeSendListener = null;
632
+ root.remove();
633
+ if (currentPanel === panel) currentPanel = null;
634
+ },
635
+ refresh
636
+ };
637
+ currentPanel = panel;
638
+ return panel;
639
+ }
640
+
641
+ // src/internal/mock.ts
642
+ var MAX_BUFFER = 100;
643
+ function createMockAdapter() {
644
+ const messages = [];
645
+ const sendListeners = /* @__PURE__ */ new Set();
646
+ return {
647
+ isMock: true,
648
+ callHandler(handlerName, payload) {
649
+ const msg = { handlerName, payload, timestamp: Date.now() };
650
+ messages.push(msg);
651
+ if (messages.length > MAX_BUFFER) {
652
+ messages.shift();
653
+ }
654
+ console.debug("[condor-mobile-bridge mock] \u2192", handlerName, payload);
655
+ for (const listener of sendListeners) {
656
+ try {
657
+ listener(msg);
658
+ } catch (err) {
659
+ console.error("[condor-mobile-bridge mock] send listener threw:", err);
660
+ }
661
+ }
662
+ },
663
+ getMessages() {
664
+ return messages;
665
+ },
666
+ clearMessages() {
667
+ messages.length = 0;
668
+ },
669
+ triggerEmit(key, payload) {
670
+ const bridge = typeof window !== "undefined" ? window.CondorBridge : void 0;
671
+ if (!bridge || typeof bridge.emit !== "function") {
672
+ throw new Error(
673
+ "condor-mobile-bridge mock: window.CondorBridge not bootstrapped; cannot emit."
674
+ );
675
+ }
676
+ bridge.emit(key, payload);
677
+ },
678
+ onCallHandler(listener) {
679
+ sendListeners.add(listener);
680
+ return () => {
681
+ sendListeners.delete(listener);
682
+ };
683
+ }
684
+ };
685
+ }
686
+
687
+ // src/internal/native.ts
688
+ var outgoingStamp = null;
689
+ function setOutgoingStamp(stamp) {
690
+ outgoingStamp = stamp;
691
+ }
692
+ var activeAdapter = null;
693
+ function createChannelAdapter(channel) {
694
+ return {
695
+ isMock: false,
696
+ // Single channel: `handlerName` does not travel — the native side routes
697
+ // on `type`.
698
+ callHandler(_handlerName, payload) {
699
+ channel.postMessage(JSON.stringify(payload));
700
+ }
701
+ };
702
+ }
703
+ function createLegacyAdapter(host) {
704
+ return {
705
+ isMock: false,
706
+ callHandler(handlerName, payload) {
707
+ void host.callHandler(handlerName, payload);
708
+ }
709
+ };
710
+ }
711
+ function detectAdapter() {
712
+ if (typeof window === "undefined") {
713
+ return createMockAdapter();
714
+ }
715
+ const channel = window.CondorNative;
716
+ if (channel && typeof channel.postMessage === "function") {
717
+ return createChannelAdapter(channel);
718
+ }
719
+ const host = window.flutter_inappwebview;
720
+ if (host && typeof host.callHandler === "function") {
721
+ return createLegacyAdapter(host);
722
+ }
723
+ return createMockAdapter();
724
+ }
725
+ function getActiveAdapter() {
726
+ if (!activeAdapter) {
727
+ activeAdapter = detectAdapter();
728
+ }
729
+ return activeAdapter;
730
+ }
731
+ function callNative(handlerName, payload) {
732
+ const stamped = outgoingStamp ? outgoingStamp(payload) : payload;
733
+ getActiveAdapter().callHandler(handlerName, stamped);
734
+ }
735
+
736
+ // src/internal/subscribers.ts
737
+ var subscribers = /* @__PURE__ */ new Map();
738
+ function addSubscriber(key, cb) {
739
+ let set = subscribers.get(key);
740
+ if (!set) {
741
+ set = /* @__PURE__ */ new Set();
742
+ subscribers.set(key, set);
743
+ }
744
+ set.add(cb);
745
+ return () => {
746
+ const current = subscribers.get(key);
747
+ if (!current) return;
748
+ current.delete(cb);
749
+ if (current.size === 0) {
750
+ subscribers.delete(key);
751
+ }
752
+ };
753
+ }
754
+ function dispatchToSubscribers(key, payload) {
755
+ const set = subscribers.get(key);
756
+ if (!set) return;
757
+ for (const cb of set) {
758
+ try {
759
+ cb(payload);
760
+ } catch (err) {
761
+ console.error(`[condor-mobile-bridge] subscriber for "${key}" threw:`, err);
762
+ }
763
+ }
764
+ }
765
+ function getSubscriberKeys() {
766
+ return Array.from(subscribers.keys());
767
+ }
768
+
769
+ // src/bridge.ts
770
+ var HANDSHAKE_ACK_KEY = "handshake-ack";
771
+ var NATIVE_HANDLER = "CondorBridge";
772
+ var PROTOCOL_VERSION = "0.1.0";
773
+ var pendingHandshake = null;
774
+ var bridgeBootstrapped = false;
775
+ function emit(key, payload) {
776
+ if (key === HANDSHAKE_ACK_KEY && pendingHandshake) {
777
+ const ack = payload ?? {};
778
+ const resolver = pendingHandshake;
779
+ pendingHandshake = null;
780
+ resolver.resolve({
781
+ nativeVersion: ack.nativeVersion ?? "unknown",
782
+ accepted: ack.accepted ?? false
783
+ });
784
+ return;
785
+ }
786
+ dispatchToSubscribers(key, payload);
787
+ }
788
+ function bootstrapWindowBridge() {
789
+ if (bridgeBootstrapped) return;
790
+ if (typeof window === "undefined") return;
791
+ bridgeBootstrapped = true;
792
+ if (window.CondorBridge && typeof window.CondorBridge.emit === "function") {
793
+ window.CondorBridge.emit = emit;
794
+ } else {
795
+ window.CondorBridge = { emit };
796
+ }
797
+ }
798
+ bootstrapWindowBridge();
799
+ function requireMockAdapter() {
800
+ const adapter = getActiveAdapter();
801
+ if (!adapter.isMock) {
802
+ throw new Error(
803
+ "condor-mobile-bridge: this API is available only in mock mode (no native host detected)."
804
+ );
805
+ }
806
+ return adapter;
807
+ }
808
+ var Bridge = {
809
+ get isMock() {
810
+ return getActiveAdapter().isMock;
811
+ },
812
+ handshake(opts) {
813
+ bootstrapWindowBridge();
814
+ if (pendingHandshake) {
815
+ return Promise.reject(
816
+ new Error(
817
+ "condor-mobile-bridge: handshake already in flight. Await the previous call before issuing another."
818
+ )
819
+ );
820
+ }
821
+ return new Promise((resolve, reject) => {
822
+ pendingHandshake = { resolve, reject };
823
+ try {
824
+ callNative(NATIVE_HANDLER, {
825
+ type: "handshake",
826
+ version: PROTOCOL_VERSION,
827
+ payload: { clientVersion: opts.clientVersion }
828
+ });
829
+ } catch (err) {
830
+ pendingHandshake = null;
831
+ reject(err instanceof Error ? err : new Error(String(err)));
832
+ }
833
+ });
834
+ },
835
+ on(key, cb) {
836
+ bootstrapWindowBridge();
837
+ return addSubscriber(key, cb);
838
+ },
839
+ send(type, payload) {
840
+ callNative(NATIVE_HANDLER, {
841
+ type,
842
+ version: PROTOCOL_VERSION,
843
+ payload: payload ?? {}
844
+ });
845
+ },
846
+ openSettings() {
847
+ this.send("openSettings");
848
+ },
849
+ setOutgoingStamp(stamp) {
850
+ setOutgoingStamp(stamp);
851
+ },
852
+ mockEmit(key, payload) {
853
+ requireMockAdapter().triggerEmit(key, payload ?? {});
854
+ },
855
+ getMockState() {
856
+ const adapter = requireMockAdapter();
857
+ return {
858
+ isMock: true,
859
+ messages: adapter.getMessages(),
860
+ subscriberKeys: getSubscriberKeys()
861
+ };
862
+ },
863
+ installDevPanel(options) {
864
+ const adapter = requireMockAdapter();
865
+ installDevPanel({ adapter, options });
866
+ }
867
+ };
868
+
869
+ // node_modules/valibot/dist/index.mjs
870
+ var store$4;
871
+ var DEFAULT_CONFIG = {
872
+ lang: void 0,
873
+ message: void 0,
874
+ abortEarly: void 0,
875
+ abortPipeEarly: void 0
876
+ };
877
+ // @__NO_SIDE_EFFECTS__
878
+ function getGlobalConfig(config$1) {
879
+ if (!config$1 && !store$4) return DEFAULT_CONFIG;
880
+ return {
881
+ lang: config$1?.lang ?? store$4?.lang,
882
+ message: config$1?.message,
883
+ abortEarly: config$1?.abortEarly ?? store$4?.abortEarly,
884
+ abortPipeEarly: config$1?.abortPipeEarly ?? store$4?.abortPipeEarly
885
+ };
886
+ }
887
+ var store$3;
888
+ // @__NO_SIDE_EFFECTS__
889
+ function getGlobalMessage(lang) {
890
+ return store$3?.get(lang);
891
+ }
892
+ var store$2;
893
+ // @__NO_SIDE_EFFECTS__
894
+ function getSchemaMessage(lang) {
895
+ return store$2?.get(lang);
896
+ }
897
+ var store$1;
898
+ // @__NO_SIDE_EFFECTS__
899
+ function getSpecificMessage(reference, lang) {
900
+ return store$1?.get(reference)?.get(lang);
901
+ }
902
+ // @__NO_SIDE_EFFECTS__
903
+ function _stringify(input) {
904
+ const type = typeof input;
905
+ if (type === "string") return `"${input}"`;
906
+ if (type === "number" || type === "bigint" || type === "boolean") return `${input}`;
907
+ if (type === "object" || type === "function") return (input && Object.getPrototypeOf(input)?.constructor?.name) ?? "null";
908
+ return type;
909
+ }
910
+ function _addIssue(context, label, dataset, config$1, other) {
911
+ const input = other && "input" in other ? other.input : dataset.value;
912
+ const expected = other?.expected ?? context.expects ?? null;
913
+ const received = other?.received ?? /* @__PURE__ */ _stringify(input);
914
+ const issue = {
915
+ kind: context.kind,
916
+ type: context.type,
917
+ input,
918
+ expected,
919
+ received,
920
+ message: `Invalid ${label}: ${expected ? `Expected ${expected} but r` : "R"}eceived ${received}`,
921
+ requirement: context.requirement,
922
+ path: other?.path,
923
+ issues: other?.issues,
924
+ lang: config$1.lang,
925
+ abortEarly: config$1.abortEarly,
926
+ abortPipeEarly: config$1.abortPipeEarly
927
+ };
928
+ const isSchema = context.kind === "schema";
929
+ const message$1 = other?.message ?? context.message ?? /* @__PURE__ */ getSpecificMessage(context.reference, issue.lang) ?? (isSchema ? /* @__PURE__ */ getSchemaMessage(issue.lang) : null) ?? config$1.message ?? /* @__PURE__ */ getGlobalMessage(issue.lang);
930
+ if (message$1 !== void 0) issue.message = typeof message$1 === "function" ? message$1(issue) : message$1;
931
+ if (isSchema) dataset.typed = false;
932
+ if (dataset.issues) dataset.issues.push(issue);
933
+ else dataset.issues = [issue];
934
+ }
935
+ var _standardCache = /* @__PURE__ */ new WeakMap();
936
+ // @__NO_SIDE_EFFECTS__
937
+ function _getStandardProps(context) {
938
+ let cached = _standardCache.get(context);
939
+ if (!cached) {
940
+ cached = {
941
+ version: 1,
942
+ vendor: "valibot",
943
+ validate(value$1) {
944
+ return context["~run"]({ value: value$1 }, /* @__PURE__ */ getGlobalConfig());
945
+ }
946
+ };
947
+ _standardCache.set(context, cached);
948
+ }
949
+ return cached;
950
+ }
951
+ // @__NO_SIDE_EFFECTS__
952
+ function _isValidObjectKey(object$1, key) {
953
+ return Object.prototype.hasOwnProperty.call(object$1, key) && key !== "__proto__" && key !== "prototype" && key !== "constructor";
954
+ }
955
+ // @__NO_SIDE_EFFECTS__
956
+ function _joinExpects(values$1, separator) {
957
+ const list = [...new Set(values$1)];
958
+ if (list.length > 1) return `(${list.join(` ${separator} `)})`;
959
+ return list[0] ?? "never";
960
+ }
961
+ var ABORT_EARLY_CONFIG = { abortEarly: true };
962
+ // @__NO_SIDE_EFFECTS__
963
+ function getFallback(schema, dataset, config$1) {
964
+ return typeof schema.fallback === "function" ? schema.fallback(dataset, config$1) : schema.fallback;
965
+ }
966
+ // @__NO_SIDE_EFFECTS__
967
+ function getDefault(schema, dataset, config$1) {
968
+ return typeof schema.default === "function" ? schema.default(dataset, config$1) : schema.default;
969
+ }
970
+ // @__NO_SIDE_EFFECTS__
971
+ function boolean(message$1) {
972
+ return {
973
+ kind: "schema",
974
+ type: "boolean",
975
+ reference: boolean,
976
+ expects: "boolean",
977
+ async: false,
978
+ message: message$1,
979
+ get "~standard"() {
980
+ return /* @__PURE__ */ _getStandardProps(this);
981
+ },
982
+ "~run"(dataset, config$1) {
983
+ if (typeof dataset.value === "boolean") dataset.typed = true;
984
+ else _addIssue(this, "type", dataset, config$1);
985
+ return dataset;
986
+ }
987
+ };
988
+ }
989
+ // @__NO_SIDE_EFFECTS__
990
+ function literal(literal_, message$1) {
991
+ return {
992
+ kind: "schema",
993
+ type: "literal",
994
+ reference: literal,
995
+ expects: /* @__PURE__ */ _stringify(literal_),
996
+ async: false,
997
+ literal: literal_,
998
+ message: message$1,
999
+ get "~standard"() {
1000
+ return /* @__PURE__ */ _getStandardProps(this);
1001
+ },
1002
+ "~run"(dataset, config$1) {
1003
+ if (dataset.value === this.literal) dataset.typed = true;
1004
+ else _addIssue(this, "type", dataset, config$1);
1005
+ return dataset;
1006
+ }
1007
+ };
1008
+ }
1009
+ // @__NO_SIDE_EFFECTS__
1010
+ function nullable(wrapped, default_) {
1011
+ return {
1012
+ kind: "schema",
1013
+ type: "nullable",
1014
+ reference: nullable,
1015
+ expects: `(${wrapped.expects} | null)`,
1016
+ async: false,
1017
+ wrapped,
1018
+ default: default_,
1019
+ get "~standard"() {
1020
+ return /* @__PURE__ */ _getStandardProps(this);
1021
+ },
1022
+ "~run"(dataset, config$1) {
1023
+ if (dataset.value === null) {
1024
+ if (this.default !== void 0) dataset.value = /* @__PURE__ */ getDefault(this, dataset, config$1);
1025
+ if (dataset.value === null) {
1026
+ dataset.typed = true;
1027
+ return dataset;
1028
+ }
1029
+ }
1030
+ return this.wrapped["~run"](dataset, config$1);
1031
+ }
1032
+ };
1033
+ }
1034
+ // @__NO_SIDE_EFFECTS__
1035
+ function number(message$1) {
1036
+ return {
1037
+ kind: "schema",
1038
+ type: "number",
1039
+ reference: number,
1040
+ expects: "number",
1041
+ async: false,
1042
+ message: message$1,
1043
+ get "~standard"() {
1044
+ return /* @__PURE__ */ _getStandardProps(this);
1045
+ },
1046
+ "~run"(dataset, config$1) {
1047
+ if (typeof dataset.value === "number" && !isNaN(dataset.value)) dataset.typed = true;
1048
+ else _addIssue(this, "type", dataset, config$1);
1049
+ return dataset;
1050
+ }
1051
+ };
1052
+ }
1053
+ // @__NO_SIDE_EFFECTS__
1054
+ function object(entries$1, message$1) {
1055
+ return {
1056
+ kind: "schema",
1057
+ type: "object",
1058
+ reference: object,
1059
+ expects: "Object",
1060
+ async: false,
1061
+ entries: entries$1,
1062
+ message: message$1,
1063
+ get "~standard"() {
1064
+ return /* @__PURE__ */ _getStandardProps(this);
1065
+ },
1066
+ "~run"(dataset, config$1) {
1067
+ const input = dataset.value;
1068
+ if (input && typeof input === "object") {
1069
+ dataset.typed = true;
1070
+ dataset.value = {};
1071
+ for (const key in this.entries) {
1072
+ const valueSchema = this.entries[key];
1073
+ if (key in input || (valueSchema.type === "exact_optional" || valueSchema.type === "optional" || valueSchema.type === "nullish") && valueSchema.default !== void 0) {
1074
+ const value$1 = key in input ? input[key] : /* @__PURE__ */ getDefault(valueSchema);
1075
+ const valueDataset = valueSchema["~run"]({ value: value$1 }, config$1);
1076
+ if (valueDataset.issues) {
1077
+ const pathItem = {
1078
+ type: "object",
1079
+ origin: "value",
1080
+ input,
1081
+ key,
1082
+ value: value$1
1083
+ };
1084
+ for (const issue of valueDataset.issues) {
1085
+ if (issue.path) issue.path.unshift(pathItem);
1086
+ else issue.path = [pathItem];
1087
+ dataset.issues?.push(issue);
1088
+ }
1089
+ if (!dataset.issues) dataset.issues = valueDataset.issues;
1090
+ if (config$1.abortEarly) {
1091
+ dataset.typed = false;
1092
+ break;
1093
+ }
1094
+ }
1095
+ if (!valueDataset.typed) dataset.typed = false;
1096
+ dataset.value[key] = valueDataset.value;
1097
+ } else if (valueSchema.fallback !== void 0) dataset.value[key] = /* @__PURE__ */ getFallback(valueSchema);
1098
+ else if (valueSchema.type !== "exact_optional" && valueSchema.type !== "optional" && valueSchema.type !== "nullish") {
1099
+ _addIssue(this, "key", dataset, config$1, {
1100
+ input: void 0,
1101
+ expected: `"${key}"`,
1102
+ path: [{
1103
+ type: "object",
1104
+ origin: "key",
1105
+ input,
1106
+ key,
1107
+ value: input[key]
1108
+ }]
1109
+ });
1110
+ if (config$1.abortEarly) break;
1111
+ }
1112
+ }
1113
+ } else _addIssue(this, "type", dataset, config$1);
1114
+ return dataset;
1115
+ }
1116
+ };
1117
+ }
1118
+ // @__NO_SIDE_EFFECTS__
1119
+ function optional(wrapped, default_) {
1120
+ return {
1121
+ kind: "schema",
1122
+ type: "optional",
1123
+ reference: optional,
1124
+ expects: `(${wrapped.expects} | undefined)`,
1125
+ async: false,
1126
+ wrapped,
1127
+ default: default_,
1128
+ get "~standard"() {
1129
+ return /* @__PURE__ */ _getStandardProps(this);
1130
+ },
1131
+ "~run"(dataset, config$1) {
1132
+ if (dataset.value === void 0) {
1133
+ if (this.default !== void 0) dataset.value = /* @__PURE__ */ getDefault(this, dataset, config$1);
1134
+ if (dataset.value === void 0) {
1135
+ dataset.typed = true;
1136
+ return dataset;
1137
+ }
1138
+ }
1139
+ return this.wrapped["~run"](dataset, config$1);
1140
+ }
1141
+ };
1142
+ }
1143
+ // @__NO_SIDE_EFFECTS__
1144
+ function picklist(options, message$1) {
1145
+ return {
1146
+ kind: "schema",
1147
+ type: "picklist",
1148
+ reference: picklist,
1149
+ expects: /* @__PURE__ */ _joinExpects(options.map(_stringify), "|"),
1150
+ async: false,
1151
+ options,
1152
+ message: message$1,
1153
+ get "~standard"() {
1154
+ return /* @__PURE__ */ _getStandardProps(this);
1155
+ },
1156
+ "~run"(dataset, config$1) {
1157
+ if (this.options.includes(dataset.value)) dataset.typed = true;
1158
+ else _addIssue(this, "type", dataset, config$1);
1159
+ return dataset;
1160
+ }
1161
+ };
1162
+ }
1163
+ // @__NO_SIDE_EFFECTS__
1164
+ function record(key, value$1, message$1) {
1165
+ return {
1166
+ kind: "schema",
1167
+ type: "record",
1168
+ reference: record,
1169
+ expects: "Object",
1170
+ async: false,
1171
+ key,
1172
+ value: value$1,
1173
+ message: message$1,
1174
+ get "~standard"() {
1175
+ return /* @__PURE__ */ _getStandardProps(this);
1176
+ },
1177
+ "~run"(dataset, config$1) {
1178
+ const input = dataset.value;
1179
+ if (input && typeof input === "object") {
1180
+ dataset.typed = true;
1181
+ dataset.value = {};
1182
+ for (const entryKey in input) if (/* @__PURE__ */ _isValidObjectKey(input, entryKey)) {
1183
+ const entryValue = input[entryKey];
1184
+ const keyDataset = this.key["~run"]({ value: entryKey }, config$1);
1185
+ if (keyDataset.issues) {
1186
+ const pathItem = {
1187
+ type: "object",
1188
+ origin: "key",
1189
+ input,
1190
+ key: entryKey,
1191
+ value: entryValue
1192
+ };
1193
+ for (const issue of keyDataset.issues) {
1194
+ issue.path = [pathItem];
1195
+ dataset.issues?.push(issue);
1196
+ }
1197
+ if (!dataset.issues) dataset.issues = keyDataset.issues;
1198
+ if (config$1.abortEarly) {
1199
+ dataset.typed = false;
1200
+ break;
1201
+ }
1202
+ }
1203
+ const valueDataset = this.value["~run"]({ value: entryValue }, config$1);
1204
+ if (valueDataset.issues) {
1205
+ const pathItem = {
1206
+ type: "object",
1207
+ origin: "value",
1208
+ input,
1209
+ key: entryKey,
1210
+ value: entryValue
1211
+ };
1212
+ for (const issue of valueDataset.issues) {
1213
+ if (issue.path) issue.path.unshift(pathItem);
1214
+ else issue.path = [pathItem];
1215
+ dataset.issues?.push(issue);
1216
+ }
1217
+ if (!dataset.issues) dataset.issues = valueDataset.issues;
1218
+ if (config$1.abortEarly) {
1219
+ dataset.typed = false;
1220
+ break;
1221
+ }
1222
+ }
1223
+ if (!keyDataset.typed || !valueDataset.typed) dataset.typed = false;
1224
+ if (keyDataset.typed) dataset.value[keyDataset.value] = valueDataset.value;
1225
+ }
1226
+ } else _addIssue(this, "type", dataset, config$1);
1227
+ return dataset;
1228
+ }
1229
+ };
1230
+ }
1231
+ // @__NO_SIDE_EFFECTS__
1232
+ function string(message$1) {
1233
+ return {
1234
+ kind: "schema",
1235
+ type: "string",
1236
+ reference: string,
1237
+ expects: "string",
1238
+ async: false,
1239
+ message: message$1,
1240
+ get "~standard"() {
1241
+ return /* @__PURE__ */ _getStandardProps(this);
1242
+ },
1243
+ "~run"(dataset, config$1) {
1244
+ if (typeof dataset.value === "string") dataset.typed = true;
1245
+ else _addIssue(this, "type", dataset, config$1);
1246
+ return dataset;
1247
+ }
1248
+ };
1249
+ }
1250
+ // @__NO_SIDE_EFFECTS__
1251
+ function unknown() {
1252
+ return {
1253
+ kind: "schema",
1254
+ type: "unknown",
1255
+ reference: unknown,
1256
+ expects: "unknown",
1257
+ async: false,
1258
+ get "~standard"() {
1259
+ return /* @__PURE__ */ _getStandardProps(this);
1260
+ },
1261
+ "~run"(dataset) {
1262
+ dataset.typed = true;
1263
+ return dataset;
1264
+ }
1265
+ };
1266
+ }
1267
+ // @__NO_SIDE_EFFECTS__
1268
+ function variant(key, options, message$1) {
1269
+ return {
1270
+ kind: "schema",
1271
+ type: "variant",
1272
+ reference: variant,
1273
+ expects: "Object",
1274
+ async: false,
1275
+ key,
1276
+ options,
1277
+ message: message$1,
1278
+ get "~standard"() {
1279
+ return /* @__PURE__ */ _getStandardProps(this);
1280
+ },
1281
+ "~run"(dataset, config$1) {
1282
+ const input = dataset.value;
1283
+ if (input && typeof input === "object") {
1284
+ let outputDataset;
1285
+ let maxDiscriminatorPriority = 0;
1286
+ let invalidDiscriminatorKey = this.key;
1287
+ let expectedDiscriminators = [];
1288
+ const parseOptions = (variant$1, allKeys) => {
1289
+ for (const schema of variant$1.options) {
1290
+ if (schema.type === "variant") parseOptions(schema, new Set(allKeys).add(schema.key));
1291
+ else {
1292
+ let keysAreValid = true;
1293
+ let currentPriority = 0;
1294
+ for (const currentKey of allKeys) {
1295
+ const discriminatorSchema = schema.entries[currentKey];
1296
+ if (currentKey in input ? discriminatorSchema["~run"]({
1297
+ typed: false,
1298
+ value: input[currentKey]
1299
+ }, ABORT_EARLY_CONFIG).issues : discriminatorSchema.type !== "exact_optional" && discriminatorSchema.type !== "optional" && discriminatorSchema.type !== "nullish") {
1300
+ keysAreValid = false;
1301
+ if (invalidDiscriminatorKey !== currentKey && (maxDiscriminatorPriority < currentPriority || maxDiscriminatorPriority === currentPriority && currentKey in input && !(invalidDiscriminatorKey in input))) {
1302
+ maxDiscriminatorPriority = currentPriority;
1303
+ invalidDiscriminatorKey = currentKey;
1304
+ expectedDiscriminators = [];
1305
+ }
1306
+ if (invalidDiscriminatorKey === currentKey) expectedDiscriminators.push(schema.entries[currentKey].expects);
1307
+ break;
1308
+ }
1309
+ currentPriority++;
1310
+ }
1311
+ if (keysAreValid) {
1312
+ const optionDataset = schema["~run"]({ value: input }, config$1);
1313
+ if (!outputDataset || !outputDataset.typed && optionDataset.typed) outputDataset = optionDataset;
1314
+ }
1315
+ }
1316
+ if (outputDataset && !outputDataset.issues) break;
1317
+ }
1318
+ };
1319
+ parseOptions(this, /* @__PURE__ */ new Set([this.key]));
1320
+ if (outputDataset) return outputDataset;
1321
+ _addIssue(this, "type", dataset, config$1, {
1322
+ input: input[invalidDiscriminatorKey],
1323
+ expected: /* @__PURE__ */ _joinExpects(expectedDiscriminators, "|"),
1324
+ path: [{
1325
+ type: "object",
1326
+ origin: "value",
1327
+ input,
1328
+ key: invalidDiscriminatorKey,
1329
+ value: input[invalidDiscriminatorKey]
1330
+ }]
1331
+ });
1332
+ } else _addIssue(this, "type", dataset, config$1);
1333
+ return dataset;
1334
+ }
1335
+ };
1336
+ }
1337
+ // @__NO_SIDE_EFFECTS__
1338
+ function safeParse(schema, input, config$1) {
1339
+ const dataset = schema["~run"]({ value: input }, /* @__PURE__ */ getGlobalConfig(config$1));
1340
+ return {
1341
+ typed: dataset.typed,
1342
+ success: !dataset.issues,
1343
+ output: dataset.value,
1344
+ issues: dataset.issues
1345
+ };
1346
+ }
1347
+
1348
+ // src/shell-messages.ts
1349
+ var BridgeKeySchema = picklist(Object.values(BRIDGE_KEYS));
1350
+ var SubscribeMessage = object({
1351
+ type: literal("subscribe"),
1352
+ key: BridgeKeySchema
1353
+ });
1354
+ var UnsubscribeMessage = object({
1355
+ type: literal("unsubscribe"),
1356
+ key: BridgeKeySchema
1357
+ });
1358
+ var SendMessage = object({
1359
+ type: literal("send"),
1360
+ command: string(),
1361
+ args: optional(record(string(), unknown()), {})
1362
+ });
1363
+ var HandshakeMessage = object({
1364
+ type: literal("handshake"),
1365
+ payload: object({ clientVersion: string() })
1366
+ });
1367
+ var JsErrorMessage = object({
1368
+ type: literal("jsError"),
1369
+ payload: object({
1370
+ message: string(),
1371
+ stack: optional(string()),
1372
+ source: optional(string()),
1373
+ lineno: optional(number()),
1374
+ colno: optional(number())
1375
+ })
1376
+ });
1377
+ var SandboxVerifyResponseMessage = object({
1378
+ type: literal("sandbox-verify-response"),
1379
+ flutterBridgeReachable: boolean(),
1380
+ condorBridgeReachable: boolean(),
1381
+ // Optional: clients on SDK < 0.18.0 do not send it.
1382
+ nativeChannelReachable: optional(boolean())
1383
+ });
1384
+ var ClientToShellMessageSchema = variant("type", [
1385
+ SubscribeMessage,
1386
+ UnsubscribeMessage,
1387
+ SendMessage,
1388
+ HandshakeMessage,
1389
+ JsErrorMessage,
1390
+ SandboxVerifyResponseMessage
1391
+ ]);
1392
+ var EventMessage = object({
1393
+ type: literal("event"),
1394
+ key: BridgeKeySchema,
1395
+ payload: unknown()
1396
+ });
1397
+ var HandshakeAckMessage = object({
1398
+ type: literal("handshake-ack"),
1399
+ ack: object({
1400
+ shellVersion: string(),
1401
+ /**
1402
+ * Schema version declared by the **native** side, relayed by the shell.
1403
+ *
1404
+ * `null` when the native side has not answered, or does not exist (browser
1405
+ * mock mode). The client runs inside a sandboxed iframe and never talks to
1406
+ * the native side directly, so this is the **only** way it can learn which
1407
+ * capabilities are available.
1408
+ */
1409
+ nativeVersion: nullable(string()),
1410
+ accepted: boolean()
1411
+ })
1412
+ });
1413
+ var NackMessage = object({
1414
+ type: literal("nack"),
1415
+ key: optional(string()),
1416
+ command: optional(string()),
1417
+ reason: string()
1418
+ });
1419
+ var VerifySandboxMessage = object({
1420
+ type: literal("verify-sandbox")
1421
+ });
1422
+ var ShellToClientMessageSchema = variant("type", [
1423
+ EventMessage,
1424
+ HandshakeAckMessage,
1425
+ NackMessage,
1426
+ VerifySandboxMessage
1427
+ ]);
1428
+
1429
+ // src/bridge-shell.ts
1430
+ var SHELL_VERSION = "0.6.0";
1431
+ var NATIVE_HANDSHAKE_TIMEOUT_MS = 2e3;
1432
+ var VERIFY_TIMEOUT_MS = 2e3;
1433
+ var OVERRIDE_PREFIX = `<script>
1434
+ (function(){
1435
+ try { delete window.flutter_inappwebview; } catch (_) {}
1436
+ try { delete window.CondorBridge; } catch (_) {}
1437
+ try { delete window.CondorNative; } catch (_) {}
1438
+ try {
1439
+ Object.defineProperty(window, 'flutter_inappwebview', {
1440
+ get: function(){ return undefined; },
1441
+ set: function(_){ /* no-op: blocks the plugin injecting into the iframe */ },
1442
+ configurable: false,
1443
+ });
1444
+ } catch (_) {}
1445
+ try {
1446
+ Object.defineProperty(window, 'CondorNative', {
1447
+ get: function(){ return undefined; },
1448
+ set: function(_){ /* no-op: bloqueia o canal do webview_flutter no iframe */ },
1449
+ configurable: false,
1450
+ });
1451
+ } catch (_) {}
1452
+ try {
1453
+ Object.defineProperty(window, 'CondorBridge', {
1454
+ get: function(){ return undefined; },
1455
+ set: function(_){ /* no-op: bloqueia bootstrap do package legado */ },
1456
+ configurable: false,
1457
+ });
1458
+ } catch (_) {}
1459
+ try {
1460
+ // Neutralize navigator.sendBeacon: an exfiltration channel that CSP
1461
+ // connect-src does not reliably block in some WebViews (pen test PT-10).
1462
+ // The client communicates only through BridgeClient/postMessage, so it has
1463
+ // no legitimate use for sendBeacon. The no-op getter returns a function
1464
+ // that answers false; the no-op setter ignores attempts to restore it.
1465
+ Object.defineProperty(navigator, 'sendBeacon', {
1466
+ get: function(){ return function(){ return false; }; },
1467
+ set: function(_){ /* no-op */ },
1468
+ configurable: false,
1469
+ });
1470
+ } catch (_) {}
1471
+ })();
1472
+ </script>`;
1473
+ var BridgeShell = class {
1474
+ constructor(opts) {
1475
+ this.subscriptions = /* @__PURE__ */ new Map();
1476
+ this.detached = false;
1477
+ /**
1478
+ * The native version, fetched once and cached. `null` until the native side
1479
+ * answers, and permanently `null` in a browser with no native host.
1480
+ */
1481
+ this.nativeVersion = null;
1482
+ /** In-flight native handshake; guarantees a single call. */
1483
+ this.nativeHandshake = null;
1484
+ // Pending `verify-sandbox` round trips, keyed by a generated nonce.
1485
+ this.pendingVerifies = /* @__PURE__ */ new Map();
1486
+ this.onMessage = async (event) => {
1487
+ if (this.detached) return;
1488
+ if (event.source !== this.iframe.contentWindow) return;
1489
+ const parsed = safeParse(ClientToShellMessageSchema, event.data);
1490
+ if (!parsed.success) {
1491
+ if (typeof console !== "undefined") {
1492
+ console.debug("[BridgeShell] dropped malformed message:", parsed.issues?.[0]?.message);
1493
+ }
1494
+ return;
1495
+ }
1496
+ const msg = parsed.output;
1497
+ switch (msg.type) {
1498
+ case "subscribe":
1499
+ await this.handleSubscribe(msg.key);
1500
+ break;
1501
+ case "unsubscribe":
1502
+ this.handleUnsubscribe(msg.key);
1503
+ break;
1504
+ case "send":
1505
+ await this.handleSend(msg.command, msg.args ?? {});
1506
+ break;
1507
+ case "handshake":
1508
+ void this.resolveNativeVersion().then((nativeVersion) => {
1509
+ this.postToClient({
1510
+ type: "handshake-ack",
1511
+ ack: { shellVersion: SHELL_VERSION, nativeVersion, accepted: true }
1512
+ });
1513
+ });
1514
+ break;
1515
+ case "jsError":
1516
+ Bridge.send("jsError", {
1517
+ origin: "iframe",
1518
+ message: msg.payload.message,
1519
+ stack: msg.payload.stack,
1520
+ source: msg.payload.source,
1521
+ lineno: msg.payload.lineno,
1522
+ colno: msg.payload.colno
1523
+ });
1524
+ break;
1525
+ case "sandbox-verify-response": {
1526
+ const first = this.pendingVerifies.keys().next().value;
1527
+ if (first !== void 0) {
1528
+ const pending = this.pendingVerifies.get(first);
1529
+ if (pending) {
1530
+ clearTimeout(pending.timer);
1531
+ this.pendingVerifies.delete(first);
1532
+ const nativeChannelReachable = msg.nativeChannelReachable ?? false;
1533
+ const ok = !msg.flutterBridgeReachable && !msg.condorBridgeReachable && !nativeChannelReachable;
1534
+ pending.resolve({
1535
+ flutterBridgeReachable: msg.flutterBridgeReachable,
1536
+ condorBridgeReachable: msg.condorBridgeReachable,
1537
+ nativeChannelReachable,
1538
+ ok
1539
+ });
1540
+ }
1541
+ }
1542
+ break;
1543
+ }
1544
+ }
1545
+ };
1546
+ this.iframe = opts.iframe;
1547
+ this.policy = opts.policy;
1548
+ this.onConfirm = opts.onConfirmationRequest;
1549
+ if (typeof window !== "undefined") {
1550
+ window.addEventListener("message", this.onMessage);
1551
+ }
1552
+ }
1553
+ /**
1554
+ * **Layer 1.** Bootstraps by fetching a URL.
1555
+ *
1556
+ * A shortcut for the common HTTP/HTTPS case: it runs `fetch(clientPath)` and
1557
+ * delegates to `bootstrapClientIframeWithHtml`.
1558
+ *
1559
+ * **It does not work on the `file://` scheme** — Android's Chromium WebView
1560
+ * blocks it with "URL scheme 'file' is not supported". For an asset bundled
1561
+ * in the APK, or a bundle inlined at build time, use
1562
+ * `bootstrapClientIframeWithHtml(htmlString)` instead.
1563
+ *
1564
+ * **Call it once at shell boot**, before any other interaction. After it
1565
+ * returns, invoke `verifySandbox()` for attestation (Layer 2).
1566
+ */
1567
+ async bootstrapClientIframe(clientPath = "client.html") {
1568
+ const res = await fetch(clientPath);
1569
+ if (!res.ok) throw new Error(`BridgeShell: failed to load ${clientPath} (${res.status})`);
1570
+ const clientHtml = await res.text();
1571
+ await this.bootstrapClientIframeWithHtml(clientHtml);
1572
+ }
1573
+ /**
1574
+ * **Layer 1.** Bootstraps from an HTML string.
1575
+ *
1576
+ * Takes the client HTML directly — already read from a file, embedded at
1577
+ * build time, read from storage — and sets it as the iframe's `srcdoc`,
1578
+ * prefixed by the override script, which runs at parse time before any
1579
+ * client script.
1580
+ *
1581
+ * Use this when `fetch` does not apply: an asset bundled under `file://`, a
1582
+ * bundle inlined at build time, any scenario without network. Android
1583
+ * WebView blocks `fetch` on the `file://` scheme, so this is the path the
1584
+ * app actually takes. For HTTP, use `bootstrapClientIframe(clientPath)`.
1585
+ *
1586
+ * **Call it once at shell boot**, before any other interaction. After it
1587
+ * returns, invoke `verifySandbox()` for attestation (Layer 2).
1588
+ */
1589
+ async bootstrapClientIframeWithHtml(clientHtml) {
1590
+ this.iframe.srcdoc = OVERRIDE_PREFIX + clientHtml;
1591
+ await this.waitForIframeLoad();
1592
+ }
1593
+ waitForIframeLoad() {
1594
+ return new Promise((resolve) => {
1595
+ const onLoad = () => {
1596
+ this.iframe.removeEventListener("load", onLoad);
1597
+ resolve();
1598
+ };
1599
+ this.iframe.addEventListener("load", onLoad);
1600
+ });
1601
+ }
1602
+ /**
1603
+ * **Layer 2.** A `postMessage` round trip asking the iframe for an
1604
+ * authoritative attestation of whether the native globals are reachable.
1605
+ * Times out after 2 s.
1606
+ *
1607
+ * `ok` is `true` only when none of them is reachable. If it comes back
1608
+ * `false`, **the shell must abort the app boot**: Layer 1 failed, and client
1609
+ * code would run with a path to the native layer.
1610
+ */
1611
+ verifySandbox() {
1612
+ if (!this.iframe.contentWindow) {
1613
+ return Promise.reject(new Error("BridgeShell: iframe has no contentWindow"));
1614
+ }
1615
+ return new Promise((resolve, reject) => {
1616
+ const nonce = Math.random().toString(36).slice(2);
1617
+ const timer = setTimeout(() => {
1618
+ this.pendingVerifies.delete(nonce);
1619
+ reject(new Error(`BridgeShell: verify-sandbox timed out after ${VERIFY_TIMEOUT_MS}ms`));
1620
+ }, VERIFY_TIMEOUT_MS);
1621
+ this.pendingVerifies.set(nonce, { resolve, reject, timer });
1622
+ this.postToClient({ type: "verify-sandbox" });
1623
+ });
1624
+ }
1625
+ /**
1626
+ * Tears the shell down: removes the `message` listener, cancels pending
1627
+ * subscriptions on the native `Bridge`, and clears internal state.
1628
+ */
1629
+ detach() {
1630
+ if (this.detached) return;
1631
+ this.detached = true;
1632
+ if (typeof window !== "undefined") {
1633
+ window.removeEventListener("message", this.onMessage);
1634
+ }
1635
+ for (const unsub of this.subscriptions.values()) unsub();
1636
+ this.subscriptions.clear();
1637
+ for (const pending of this.pendingVerifies.values()) {
1638
+ clearTimeout(pending.timer);
1639
+ pending.reject(new Error("BridgeShell: detached"));
1640
+ }
1641
+ this.pendingVerifies.clear();
1642
+ }
1643
+ async handleSubscribe(key) {
1644
+ if (!this.policy.readOnlyKeys.includes(key)) {
1645
+ this.postToClient({ type: "nack", key, reason: "not-read-only" });
1646
+ return;
1647
+ }
1648
+ if (this.subscriptions.has(key)) return;
1649
+ const unsub = Bridge.on(key, (payload) => {
1650
+ if (this.detached) return;
1651
+ this.postToClient({ type: "event", key, payload });
1652
+ });
1653
+ this.subscriptions.set(key, unsub);
1654
+ }
1655
+ handleUnsubscribe(key) {
1656
+ const unsub = this.subscriptions.get(key);
1657
+ if (!unsub) return;
1658
+ unsub();
1659
+ this.subscriptions.delete(key);
1660
+ }
1661
+ async handleSend(command, args) {
1662
+ if (this.policy.deniedCommands.includes(command)) {
1663
+ this.postToClient({ type: "nack", command, reason: "denied" });
1664
+ return;
1665
+ }
1666
+ if (this.policy.requiresConfirmation.includes(command)) {
1667
+ if (!this.onConfirm) {
1668
+ this.postToClient({ type: "nack", command, reason: "no-confirmation-handler" });
1669
+ return;
1670
+ }
1671
+ let approved = false;
1672
+ try {
1673
+ approved = await this.onConfirm({ command, args });
1674
+ } catch (err) {
1675
+ this.postToClient({
1676
+ type: "nack",
1677
+ command,
1678
+ reason: `confirmation-error: ${err instanceof Error ? err.message : String(err)}`
1679
+ });
1680
+ return;
1681
+ }
1682
+ if (!approved) {
1683
+ this.postToClient({ type: "nack", command, reason: "user-rejected" });
1684
+ return;
1685
+ }
1686
+ } else if (!this.policy.allowedCommands.includes(command)) {
1687
+ this.postToClient({ type: "nack", command, reason: "not-allowed" });
1688
+ return;
1689
+ }
1690
+ Bridge.send(command, args);
1691
+ }
1692
+ /**
1693
+ * Resolves `nativeVersion`, firing the native handshake the first time and
1694
+ * caching the result.
1695
+ *
1696
+ * ⚠️ `Bridge.handshake` has **no timeout**: its promise only settles when the
1697
+ * native side emits `handshake-ack`. In a browser with no native host it
1698
+ * never settles, so the race below is what guarantees the client always gets
1699
+ * an answer — `null` — instead of hanging.
1700
+ */
1701
+ async resolveNativeVersion() {
1702
+ if (this.nativeVersion !== null) return this.nativeVersion;
1703
+ if (!this.nativeHandshake) {
1704
+ this.nativeHandshake = Bridge.handshake({ clientVersion: SHELL_VERSION }).then((ack) => {
1705
+ this.nativeVersion = ack.nativeVersion;
1706
+ }).catch(() => {
1707
+ });
1708
+ }
1709
+ await Promise.race([
1710
+ this.nativeHandshake,
1711
+ new Promise((resolve) => setTimeout(resolve, NATIVE_HANDSHAKE_TIMEOUT_MS))
1712
+ ]);
1713
+ return this.nativeVersion;
1714
+ }
1715
+ postToClient(msg) {
1716
+ if (this.detached) return;
1717
+ this.iframe.contentWindow?.postMessage(msg, "*");
1718
+ }
1719
+ };
1720
+
1721
+ // src/dev/policy.ts
1722
+ var CANVAS_POLICY = {
1723
+ readOnlyKeys: [
1724
+ BRIDGE_KEYS.userDevice,
1725
+ BRIDGE_KEYS.deviceConnection,
1726
+ BRIDGE_KEYS.deviceLightSensors,
1727
+ BRIDGE_KEYS.deviceNotification,
1728
+ BRIDGE_KEYS.pingPong,
1729
+ BRIDGE_KEYS.realTime,
1730
+ BRIDGE_KEYS.syncTime,
1731
+ BRIDGE_KEYS.deleteMemory,
1732
+ BRIDGE_KEYS.sensorLog,
1733
+ BRIDGE_KEYS.systemStatus,
1734
+ BRIDGE_KEYS.navigationBack,
1735
+ BRIDGE_KEYS.language
1736
+ ],
1737
+ allowedCommands: [
1738
+ "userDevice",
1739
+ "realTime",
1740
+ "openSettings",
1741
+ "deviceConnection",
1742
+ "ping",
1743
+ "jsError",
1744
+ "sensorLog",
1745
+ "setStatusBarColor",
1746
+ "requestPermission",
1747
+ "navigationState",
1748
+ "language"
1749
+ ],
1750
+ requiresConfirmation: ["syncTime", "deleteMemory", "logout"],
1751
+ deniedCommands: ["dfu"]
1752
+ };
1753
+ var CANVAS_CSP = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; frame-src 'self'; object-src 'none'; base-uri 'none'; form-action 'none';";
1754
+
1755
+ // src/dev/scenarios.ts
1756
+ var HARNESS_NATIVE_VERSION = "0.17.0";
1757
+ var base = {
1758
+ description: "Two devices, background service already running, realistic latency.",
1759
+ nativeVersion: HARNESS_NATIVE_VERSION,
1760
+ latencyMs: [80, 250],
1761
+ serviceStartMs: 0,
1762
+ userDeviceSilent: false,
1763
+ logPeriodS: 60,
1764
+ systemStatus: systemStatus(),
1765
+ devices: () => twoDevices()
1766
+ };
1767
+ var SCENARIOS = {
1768
+ default: base,
1769
+ "cold-start": {
1770
+ ...base,
1771
+ description: "App launched from cold: the background service takes 4 s to start, and every ping/userDevice sent before that is lost. A bundle that asks once stays on its loading screen forever.",
1772
+ serviceStartMs: 4e3
1773
+ },
1774
+ "no-devices": {
1775
+ ...base,
1776
+ description: "Account with no linked device: userDevice never answers and systemStatus.hasDevices is false. Your bundle must show an empty state.",
1777
+ userDeviceSilent: true,
1778
+ systemStatus: systemStatus({ hasDevices: false })
1779
+ },
1780
+ "shared-serial": {
1781
+ ...base,
1782
+ description: "An ActLumus and an ActLumus Plus share serial number 7047. Only deviceId tells them apart; sensorLog cannot address one of them.",
1783
+ devices: () => sharedSerial()
1784
+ },
1785
+ offline: {
1786
+ ...base,
1787
+ description: "No internet. systemStatus.internet is false and sensorLog fails; userDevice answers from the cache.",
1788
+ sensorLogError: "Connection failed",
1789
+ systemStatus: systemStatus({ internet: false })
1790
+ }
1791
+ };
1792
+ function resolveScenario(scenario) {
1793
+ if (scenario === void 0) return SCENARIOS.default;
1794
+ if (typeof scenario === "string") {
1795
+ const found = SCENARIOS[scenario];
1796
+ if (!found) {
1797
+ throw new Error(
1798
+ `condor-mobile-bridge dev: unknown scenario "${scenario}". Known: ${Object.keys(SCENARIOS).join(", ")}.`
1799
+ );
1800
+ }
1801
+ return found;
1802
+ }
1803
+ return scenario;
1804
+ }
1805
+
1806
+ // src/dev/harness.ts
1807
+ var DEV_HARNESS_MARKER = "__condorDevHarness__";
1808
+ function createSimulatedNative(adapter, scenario, hooks = {}) {
1809
+ const now = hooks.now ?? Date.now;
1810
+ const startedAt = now();
1811
+ const log = [];
1812
+ const timers = /* @__PURE__ */ new Set();
1813
+ let status = { ...scenario.systemStatus };
1814
+ let canGoBack = false;
1815
+ const record2 = (entry) => {
1816
+ const full = { at: now(), ...entry };
1817
+ log.push(full);
1818
+ if (log.length > 500) log.shift();
1819
+ hooks.onLog?.(full);
1820
+ if (typeof console !== "undefined") console.info(`[condor dev] ${full.kind}: ${full.detail}`);
1821
+ };
1822
+ const latency = () => {
1823
+ const l = scenario.latencyMs;
1824
+ return Array.isArray(l) ? l[0] + Math.random() * (l[1] - l[0]) : l;
1825
+ };
1826
+ const reply = (key, payload, delay = latency()) => {
1827
+ const timer = setTimeout(() => {
1828
+ timers.delete(timer);
1829
+ record2({ kind: "reply", key, detail: `${key} \u2192 bundle` });
1830
+ adapter.triggerEmit(key, payload);
1831
+ }, delay);
1832
+ timers.add(timer);
1833
+ };
1834
+ const serviceUp = () => now() - startedAt >= scenario.serviceStartMs;
1835
+ const language = () => {
1836
+ const deviceLocale = typeof navigator !== "undefined" ? navigator.language : "en-US";
1837
+ const lang = deviceLocale.slice(0, 2);
1838
+ return {
1839
+ deviceLocale,
1840
+ appLanguage: null,
1841
+ resolvedLanguage: ["en", "pt", "de", "fr"].includes(lang) ? lang : "en"
1842
+ };
1843
+ };
1844
+ const handle = (msg) => {
1845
+ const env = msg.payload ?? {};
1846
+ const command = env.type ?? "";
1847
+ const args = env.payload ?? {};
1848
+ if (command !== "handshake") {
1849
+ const shown = JSON.stringify(args);
1850
+ const detail = shown === "{}" ? command : `${command} ${shown.length > 160 ? `${shown.slice(0, 157)}...` : shown}`;
1851
+ record2({ kind: "sent", command, detail });
1852
+ }
1853
+ switch (command) {
1854
+ case "handshake":
1855
+ if (scenario.nativeVersion === null) {
1856
+ record2({
1857
+ kind: "info",
1858
+ detail: "handshake unanswered: the bundle gets nativeVersion null"
1859
+ });
1860
+ return;
1861
+ }
1862
+ reply("handshake-ack", { nativeVersion: scenario.nativeVersion, accepted: true });
1863
+ reply("systemStatus", status, latency() + 20);
1864
+ reply("language", language(), latency() + 30);
1865
+ return;
1866
+ case "ping":
1867
+ if (!serviceUp()) {
1868
+ record2({
1869
+ kind: "dropped",
1870
+ command,
1871
+ detail: "ping lost: the background service is not running yet"
1872
+ });
1873
+ return;
1874
+ }
1875
+ reply("pingPong", { data: "pong" });
1876
+ return;
1877
+ case "userDevice":
1878
+ if (!serviceUp()) {
1879
+ record2({
1880
+ kind: "dropped",
1881
+ command,
1882
+ detail: "userDevice lost: the background service is not running yet. Ask again."
1883
+ });
1884
+ return;
1885
+ }
1886
+ if (scenario.userDeviceSilent) {
1887
+ record2({
1888
+ kind: "dropped",
1889
+ command,
1890
+ detail: "userDevice unanswered: the account has no linked device (the app replies only on success)"
1891
+ });
1892
+ return;
1893
+ }
1894
+ reply("userDevice", scenario.devices());
1895
+ return;
1896
+ case "sensorLog": {
1897
+ const query = args;
1898
+ if (scenario.sensorLogError) {
1899
+ reply("sensorLog", {
1900
+ requestId: query.requestId,
1901
+ success: false,
1902
+ error: scenario.sensorLogError
1903
+ });
1904
+ return;
1905
+ }
1906
+ const matches = scenario.devices().devicesConfig.filter((d) => d.serialNo === query.serialNo);
1907
+ if (matches.length > 1) {
1908
+ reply("sensorLog", {
1909
+ requestId: query.requestId,
1910
+ success: false,
1911
+ error: "DEVICE_SERIAL_AMBIGUOUS"
1912
+ });
1913
+ return;
1914
+ }
1915
+ const device = matches[0];
1916
+ reply(
1917
+ "sensorLog",
1918
+ sensorLogPage(query, {
1919
+ now: now(),
1920
+ periodS: scenario.logPeriodS,
1921
+ deviceId: device?.deviceId
1922
+ })
1923
+ );
1924
+ return;
1925
+ }
1926
+ case "realTime":
1927
+ reply("realTime", {
1928
+ serialNo: args.serialNo,
1929
+ ...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
1930
+ success: true,
1931
+ value: args.value === true
1932
+ });
1933
+ return;
1934
+ case "syncTime":
1935
+ case "deleteMemory":
1936
+ reply(command, {
1937
+ serialNo: args.serialNo,
1938
+ ...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
1939
+ success: true
1940
+ });
1941
+ return;
1942
+ case "deviceConnection":
1943
+ if (args.type === "add") {
1944
+ reply("deviceConnection", {
1945
+ serialNo: args.serialNo,
1946
+ ...args.deviceId !== void 0 ? { deviceId: args.deviceId } : {},
1947
+ isConnect: true
1948
+ });
1949
+ }
1950
+ return;
1951
+ case "language":
1952
+ reply("language", language());
1953
+ return;
1954
+ case "requestPermission": {
1955
+ const name = String(args.permission);
1956
+ status = {
1957
+ ...status,
1958
+ missingEssentialPermissions: status.missingEssentialPermissions.filter((p) => p !== name),
1959
+ missingOptionalPermissions: status.missingOptionalPermissions.filter((p) => p !== name)
1960
+ };
1961
+ reply("systemStatus", status, latency() + 400);
1962
+ return;
1963
+ }
1964
+ case "jsError":
1965
+ record2({
1966
+ kind: "info",
1967
+ command,
1968
+ detail: `jsError from the bundle: ${String(args.message)}`
1969
+ });
1970
+ return;
1971
+ case "logout":
1972
+ record2({
1973
+ kind: "info",
1974
+ command,
1975
+ detail: "logout confirmed: the app would return to its login screen"
1976
+ });
1977
+ return;
1978
+ case "navigationState":
1979
+ canGoBack = args.canGoBack === true;
1980
+ return;
1981
+ default:
1982
+ return;
1983
+ }
1984
+ };
1985
+ const unsubscribe = adapter.onCallHandler(handle);
1986
+ if (scenario.serviceStartMs > 0) {
1987
+ const timer = setTimeout(() => {
1988
+ timers.delete(timer);
1989
+ record2({ kind: "info", detail: "background service is up: ping and userDevice now answer" });
1990
+ }, scenario.serviceStartMs);
1991
+ timers.add(timer);
1992
+ }
1993
+ return {
1994
+ log,
1995
+ pressBack() {
1996
+ if (!canGoBack) {
1997
+ record2({
1998
+ kind: "info",
1999
+ detail: "OS back: the bundle reported canGoBack false, so the app keeps the Home"
2000
+ });
2001
+ return;
2002
+ }
2003
+ reply("navigationBack", {}, 0);
2004
+ },
2005
+ dispose() {
2006
+ unsubscribe();
2007
+ for (const t of timers) clearTimeout(t);
2008
+ timers.clear();
2009
+ }
2010
+ };
2011
+ }
2012
+ function confirmInPage(req) {
2013
+ return new Promise((resolve) => {
2014
+ const overlay = document.createElement("div");
2015
+ overlay.setAttribute(
2016
+ "style",
2017
+ "position:fixed;inset:0;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;z-index:2147483646;font:14px system-ui,sans-serif"
2018
+ );
2019
+ const box = document.createElement("div");
2020
+ box.setAttribute(
2021
+ "style",
2022
+ "background:#fff;color:#111;border-radius:12px;padding:20px;max-width:320px;box-shadow:0 8px 32px rgba(0,0,0,.3)"
2023
+ );
2024
+ const text = document.createElement("p");
2025
+ text.textContent = `The app asks the person to confirm "${req.command}". On a device this is a Condor dialog; if they decline, your bundle receives nothing.`;
2026
+ const buttons = document.createElement("div");
2027
+ buttons.setAttribute("style", "display:flex;gap:8px;justify-content:flex-end;margin-top:12px");
2028
+ const done = (value) => {
2029
+ overlay.remove();
2030
+ resolve(value);
2031
+ };
2032
+ for (const [label, value] of [
2033
+ ["Decline", false],
2034
+ ["Confirm", true]
2035
+ ]) {
2036
+ const button = document.createElement("button");
2037
+ button.textContent = label;
2038
+ button.addEventListener("click", () => done(value));
2039
+ buttons.appendChild(button);
2040
+ }
2041
+ box.append(text, buttons);
2042
+ overlay.appendChild(box);
2043
+ document.body.appendChild(overlay);
2044
+ });
2045
+ }
2046
+ async function createDevHarness(opts) {
2047
+ if (typeof document === "undefined") {
2048
+ throw new Error(
2049
+ "createDevHarness: needs a DOM (run it in the browser page that hosts the bundle)."
2050
+ );
2051
+ }
2052
+ const adapter = getActiveAdapter();
2053
+ if (!adapter.isMock) {
2054
+ throw new Error(
2055
+ "createDevHarness: a native host is present; the harness only runs in a plain browser."
2056
+ );
2057
+ }
2058
+ globalThis[DEV_HARNESS_MARKER] = true;
2059
+ const scenario = resolveScenario(opts.scenario);
2060
+ const native = createSimulatedNative(adapter, scenario, { onLog: opts.onLog });
2061
+ const iframe = document.createElement("iframe");
2062
+ iframe.setAttribute("sandbox", "allow-scripts");
2063
+ iframe.setAttribute("title", "Condor Canvas bundle");
2064
+ iframe.setAttribute("style", "border:0;width:100%;height:100%;display:block;background:#fff");
2065
+ (opts.mount ?? document.body).appendChild(iframe);
2066
+ const shell = new BridgeShell({
2067
+ iframe,
2068
+ policy: CANVAS_POLICY,
2069
+ onConfirmationRequest: opts.onConfirmationRequest ?? confirmInPage
2070
+ });
2071
+ if (opts.devPanel !== false) {
2072
+ installDevPanel({ adapter, options: { startCollapsed: true } });
2073
+ }
2074
+ await shell.bootstrapClientIframeWithHtml(opts.html);
2075
+ try {
2076
+ const result = await shell.verifySandbox();
2077
+ if (!result.ok) {
2078
+ console.error(
2079
+ "[condor dev] sandbox verification failed: the app would abort the boot.",
2080
+ result
2081
+ );
2082
+ }
2083
+ } catch (err) {
2084
+ console.warn(
2085
+ "[condor dev] the bundle did not answer the sandbox probe: its script did not run. Check the console of the iframe for errors, that the page imports BridgeClient, and that the input is self-contained (or let the harness inline it: pass the dist/index.html your bundler wrote).",
2086
+ err
2087
+ );
2088
+ }
2089
+ return {
2090
+ scenario,
2091
+ iframe,
2092
+ shell,
2093
+ get log() {
2094
+ return native.log;
2095
+ },
2096
+ emit(key, payload) {
2097
+ Bridge.mockEmit(key, payload);
2098
+ },
2099
+ pressBack() {
2100
+ native.pressBack();
2101
+ },
2102
+ destroy() {
2103
+ native.dispose();
2104
+ shell.detach();
2105
+ iframe.remove();
2106
+ }
2107
+ };
2108
+ }
2109
+ return __toCommonJS(dev_exports);
2110
+ })();