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