@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/dist/dev.d.cts ADDED
@@ -0,0 +1,683 @@
1
+ /**
2
+ * One linked device, as configured on the server. Mirrors
3
+ * `DeviceConfig.toMap()` in `condor_core`
4
+ * (`domain/entities/device_config_entity.dart`).
5
+ *
6
+ * `deviceId` is the server id and is unique. `serialNo` is printed on the
7
+ * device and **can repeat**: an ActLumus and an ActLumus Plus may share one.
8
+ */
9
+ interface DeviceConfigItem {
10
+ /** Server id. Unique — the join key across the three arrays. */
11
+ deviceId: number;
12
+ /** Printed serial number. Not unique: an ActLumus and a Plus may share one. */
13
+ serialNo: number;
14
+ /** Where the device is worn. `null` when not set. */
15
+ deviceBodyPart: 'wrist' | 'necklace' | null;
16
+ /**
17
+ * Device model id. Absent when the server did not send one, which means an
18
+ * ActLumus (gen1); the Plus always carries it. Never a display name.
19
+ */
20
+ modelId?: 'actlumus' | 'actlumus_plus' | (string & {});
21
+ /** BLE advertising period, in minutes, set by the Condor web system. */
22
+ bleConnectionPeriod: number;
23
+ autosync: boolean;
24
+ eraseDeviceMemory: boolean;
25
+ autoEraseDeviceMemory: boolean;
26
+ autoEraseDeviceMemoryThreshold: number;
27
+ readSensorsMemory: boolean;
28
+ startDevice: boolean;
29
+ readFullMemory: boolean;
30
+ allowFirmwareUpdate: boolean;
31
+ }
32
+ /**
33
+ * One failed sync attempt. Mirrors `DeviceSyncError.toMap()`
34
+ * (`domain/entities/device_sync_error_entity.dart`).
35
+ */
36
+ interface DeviceSyncErrorItem {
37
+ serialNo: number;
38
+ /** Epoch milliseconds. */
39
+ attemptDate: number;
40
+ /** Already localized by the app. Show it; do not branch on it. */
41
+ message: string;
42
+ /** Stable identifier to branch on. `null` for legacy errors. */
43
+ messageCode: string | null;
44
+ }
45
+ /**
46
+ * Last known state of one device, as read over BLE. Mirrors
47
+ * `DeviceTelemetry.toMap()` (`domain/entities/device_telemetry_entity.dart`).
48
+ *
49
+ * Every reading is `null` until the device has synced at least once.
50
+ */
51
+ interface DeviceTelemetryItem {
52
+ serialNo: number;
53
+ /**
54
+ * Server id. **Omitted** on legacy data the app could not attribute, so join
55
+ * by `deviceId` when present and fall back to `serialNo` only when that
56
+ * `serialNo` is unique in `devicesConfig`.
57
+ */
58
+ deviceId?: number;
59
+ /**
60
+ * Last successful sync, epoch milliseconds. The misspelling (`updata`) is the
61
+ * real field name — do not "fix" it.
62
+ */
63
+ updataDateTime: number | null;
64
+ /** Display name reported by the device. Not in `devicesConfig`. */
65
+ deviceName: string | null;
66
+ /** Firmware version. */
67
+ deviceVersion: string | null;
68
+ hardwareVersion: string | null;
69
+ /** Device clock at the last sync, epoch milliseconds. */
70
+ deviceDateTime: number | null;
71
+ /** Data memory used, percent 0–100. */
72
+ memoryDataUsage: number | null;
73
+ /** System memory used, percent 0–100. */
74
+ memorySystemUsage: number | null;
75
+ /** Battery charge, percent 0–100. */
76
+ batteryLevel: number | null;
77
+ /** Battery voltage, volts. */
78
+ batteryVoltage: number | null;
79
+ isCharging: boolean | null;
80
+ isOnDock: boolean | null;
81
+ /** Sync failures of this device. Empty when there are none. */
82
+ syncErrors: DeviceSyncErrorItem[];
83
+ }
84
+ /**
85
+ * Per-device toggles of the user. Mirrors `DeviceUserConfig.toMap()`
86
+ * (`domain/entities/device_user_config_entity.dart`).
87
+ */
88
+ interface DeviceUserConfigItem {
89
+ serialNo: number;
90
+ /** Server id. Omitted on legacy data — see `DeviceTelemetryItem.deviceId`. */
91
+ deviceId?: number;
92
+ syncTime: boolean;
93
+ realTime: boolean;
94
+ deleteMemory: boolean;
95
+ readAllMemory: boolean;
96
+ readAllSystemLog: boolean;
97
+ }
98
+ /**
99
+ * Snapshot of the user's device store. Mirrors `UserDevices.toMap()` in
100
+ * `condor_core`.
101
+ *
102
+ * **Joining the three arrays.** `devicesConfig` lists the devices; the other
103
+ * two describe them. Join on `deviceId`. When an item of `devicesTelemetry` or
104
+ * `devicesUserConfig` has no `deviceId` (legacy data), fall back to `serialNo`
105
+ * — but only when that `serialNo` appears once in `devicesConfig`. A device may
106
+ * have no telemetry yet: render it with empty readings, never drop it.
107
+ *
108
+ * **It is not guaranteed to answer.** Use `BridgeClient.requestUserDevices()`
109
+ * instead of `send('userDevice')`; see `docs/troubleshooting.md`.
110
+ *
111
+ * The app enforces no device cap. A commercial rule currently limits a user to
112
+ * two linked devices, but that can change with no signal in this contract — read
113
+ * the length of the array rather than hard-coding it.
114
+ */
115
+ interface UserDevicePayload {
116
+ /** Epoch milliseconds of the snapshot. */
117
+ lastUpdate: number;
118
+ devicesConfig: DeviceConfigItem[];
119
+ devicesTelemetry: DeviceTelemetryItem[];
120
+ devicesUserConfig: DeviceUserConfigItem[];
121
+ }
122
+ /**
123
+ * Parameters of the `sensorLog` command.
124
+ *
125
+ * `startTime` / `endTime` delimit a period — `startTime` is **inclusive**,
126
+ * `endTime` **exclusive**, both in the strict format `YYYY-MM-DD HH:MM:SS`
127
+ * (no `T`, no offset, no date-only form). Requires nativeVersion >= 0.15.0.
128
+ *
129
+ * ⚠️ These timestamps are **device-local time, not UTC**: `log_time` passes
130
+ * through from the device RTC. Build the range with local `Date` getters
131
+ * (`getFullYear()`, `getHours()`, …) and **never** with `toISOString()` —
132
+ * converting to UTC shifts every series by the timezone offset, producing
133
+ * plausible data and no visible error.
134
+ *
135
+ * The `startingAfter` / `endingBefore` cursors are **opaque** and mutually
136
+ * exclusive: echo back the value you received, never construct one.
137
+ */
138
+ interface SensorLogQueryPayload {
139
+ /** Echoed back in the reply — correlates concurrent requests. */
140
+ requestId: string;
141
+ serialNo: number;
142
+ /** `wrist` or `necklace`. Omit for no filter. */
143
+ bodyPart?: string;
144
+ /** 1..1000 (backend default is 10). Outside the range: `INVALID_LIMIT`. */
145
+ limit?: number;
146
+ startingAfter?: string;
147
+ endingBefore?: string;
148
+ /** Inclusive, `YYYY-MM-DD HH:MM:SS`, in device-local time. */
149
+ startTime?: string;
150
+ /** Exclusive, `YYYY-MM-DD HH:MM:SS`, in device-local time. */
151
+ endTime?: string;
152
+ }
153
+ /**
154
+ * One sensor log, passed through from the backend `get_log` contract.
155
+ *
156
+ * **Cadence.** The device writes one log per period, which is programmable per
157
+ * device (default 60 s). Never assume one per minute: size pagination from the
158
+ * gaps between `log_time` values you actually receive.
159
+ *
160
+ * `null` on a channel means the hardware does not have it, never zero.
161
+ */
162
+ interface SensorLogItem {
163
+ /** Stable id of the log. */
164
+ id: number;
165
+ /** `'YYYY-MM-DD HH:MM:SS'` in **device-local time**, not UTC. */
166
+ log_time: string;
167
+ log_type: number;
168
+ body_part: 'wrist' | 'necklace' | null;
169
+ /** Degrees Celsius, already converted by the backend. */
170
+ temperature: number | null;
171
+ /** Photopic illuminance, lux. */
172
+ lux_photopic: number | null;
173
+ /** Melanopic illuminance (melanopic EDI), lux. */
174
+ lux_melanopic: number | null;
175
+ company_id?: number;
176
+ user_id?: number;
177
+ device_id?: number;
178
+ orientation?: number | null;
179
+ light_sensor_f1?: number | null;
180
+ light_sensor_f2?: number | null;
181
+ light_sensor_f3?: number | null;
182
+ light_sensor_f4?: number | null;
183
+ light_sensor_f5?: number | null;
184
+ light_sensor_f6?: number | null;
185
+ light_sensor_f7?: number | null;
186
+ light_sensor_f8?: number | null;
187
+ light_sensor_clear?: number | null;
188
+ light_sensor_nir?: number | null;
189
+ /** ActLumus Plus only (`null` on gen1). Approximates CIE X; not an exact CIE value. */
190
+ light_sensor_x?: number | null;
191
+ /** ActLumus Plus only (`null` on gen1). */
192
+ light_sensor_y?: number | null;
193
+ /** ActLumus Plus only (`null` on gen1). */
194
+ light_sensor_z?: number | null;
195
+ /** ActLumus Plus only (`null` on gen1). Reserved by the firmware; do not interpret. */
196
+ light_sensor_fd?: number | null;
197
+ act_data_pim?: number | null;
198
+ act_data_tat?: number | null;
199
+ act_data_zcm?: number | null;
200
+ off_wrist?: number | null;
201
+ cap_sensor_1?: number | null;
202
+ cap_sensor_2?: number | null;
203
+ event?: number | null;
204
+ state?: number | null;
205
+ from_service?: boolean;
206
+ /** Batch markers of the upload, not per-log timestamps. Do not plot them. */
207
+ device_log_time?: string | null;
208
+ app_log_time?: string | null;
209
+ }
210
+ /**
211
+ * Reply to the `sensorLog` command.
212
+ *
213
+ * An envelope carrying the echoed `requestId` for correlation (so concurrent
214
+ * keyset pagination works) plus `success` / `error`. On success it brings the
215
+ * page of logs — chronological ascending, temperature in °C, naive timestamps
216
+ * in **device-local time** — and the opaque cursors.
217
+ *
218
+ * With a time range, `hasMore` / `hasPrevious` are computed **within the
219
+ * period**, so `hasMore: false` means "this period is complete", not "no more
220
+ * data exists".
221
+ *
222
+ * On failure, `error` carries the contract code (`CONFLICTING_CURSORS`,
223
+ * `INVALID_TIME_RANGE`, `invalid session`, …) — a **diagnostic code, never
224
+ * text to show a user**. Note that `fetchSensorLog` RESOLVES with
225
+ * `success: false`; it only rejects on timeout.
226
+ */
227
+ interface SensorLogPayload {
228
+ requestId: string;
229
+ success: boolean;
230
+ /** Chronological ascending. Without a cursor or range: the most recent page. */
231
+ logs?: SensorLogItem[];
232
+ startingAfter?: string | null;
233
+ endingBefore?: string | null;
234
+ hasMore?: boolean;
235
+ hasPrevious?: boolean;
236
+ error?: string;
237
+ }
238
+ /**
239
+ * Aggregate environment status (native → JS). Mirrors `SysNotification` in the
240
+ * app: the flags a client uses to explain why things are not working — for
241
+ * instance, Bluetooth being off prevents scanning and connecting to devices.
242
+ *
243
+ * Emitted on attach (before your bundle exists), again right after
244
+ * `handshake-ack`, and on every change. `deviceSyncError` maps `serialNo` to an
245
+ * error message the app has already localized. A `serialNo` shared by two
246
+ * devices has no single owner, so it is **left out** of `deviceSyncError` and
247
+ * only appears in `deviceSyncErrorById`.
248
+ */
249
+ interface SystemStatusPayload {
250
+ bluetooth: boolean;
251
+ internet: boolean;
252
+ permission: boolean;
253
+ hasDevices: boolean;
254
+ /**
255
+ * Optional permissions not yet granted, as `AppPermission` names
256
+ * (e.g. `["notification", "backgroundLocation"]`). Additive and
257
+ * backward-compatible; render your own optional-permissions nudge from it.
258
+ * Empty array when nothing is missing. Battery is intentionally excluded
259
+ * (covered by the app's native OEM guidance, not this channel).
260
+ */
261
+ missingOptionalPermissions: string[];
262
+ /**
263
+ * Essential permissions not yet granted, as `AppPermission` names
264
+ * (e.g. `["bluetoothScan", "bluetoothConnect"]`). These are REQUIRED for the
265
+ * app to work — background collection stays off without them. Render your own
266
+ * blocking "required permissions" card from it. Additive and
267
+ * backward-compatible. Empty array when all essentials are granted.
268
+ */
269
+ missingEssentialPermissions: string[];
270
+ deviceSyncError: Record<number, string>;
271
+ /**
272
+ * Same errors keyed by the server `deviceId`, for **every** device, shared
273
+ * `serialNo` included. Absent below `nativeVersion` 0.17.0, so guard it.
274
+ */
275
+ deviceSyncErrorById?: Record<number, string>;
276
+ }
277
+
278
+ /**
279
+ * Realistic fixture data for the development harness.
280
+ *
281
+ * Shapes follow the typed payloads in `../payloads`, which mirror the Dart
282
+ * `toMap()` of `condor_core`. Timestamps that the device produces (`log_time`)
283
+ * are **device-local** wall-clock strings, built with local `Date` getters —
284
+ * never `toISOString()`.
285
+ */
286
+
287
+ /** Default log period of an ActLumus, in seconds. Programmable per device. */
288
+ declare const DEFAULT_LOG_PERIOD_S = 60;
289
+ /** `YYYY-MM-DD HH:MM:SS` in local time — the format of `log_time`. */
290
+ declare function formatLocalTimestamp(date: Date): string;
291
+ /** Parses a local `YYYY-MM-DD HH:MM:SS` back to epoch ms. */
292
+ declare function parseLocalTimestamp(value: string): number;
293
+ /** Two devices with distinct serial numbers: the common case. */
294
+ declare function twoDevices(now?: number): UserDevicePayload;
295
+ /**
296
+ * An ActLumus and an ActLumus Plus sharing serial number 7047 — real data
297
+ * looked like this for the first pilot account. Only `deviceId` tells them
298
+ * apart.
299
+ */
300
+ declare function sharedSerial(now?: number): UserDevicePayload;
301
+ declare function systemStatus(overrides?: Partial<SystemStatusPayload>): SystemStatusPayload;
302
+ /**
303
+ * Answers a `sensorLog` query with the backend's paging semantics:
304
+ * - no range: the most recent `limit` logs; older pages through `endingBefore`;
305
+ * - with `startTime`: the oldest logs of the range first; newer pages through
306
+ * `startingAfter` (with only `endTime`: the most recent before it);
307
+ * - `logs` always chronological ascending; `hasMore` / `hasPrevious` computed
308
+ * within the range.
309
+ */
310
+ declare function sensorLogPage(query: SensorLogQueryPayload, opts?: {
311
+ now?: number;
312
+ periodS?: number;
313
+ deviceId?: number;
314
+ }): SensorLogPayload;
315
+
316
+ /**
317
+ * Valibot schemas for the shell↔client `postMessage` protocol.
318
+ *
319
+ * Client content runs inside `<iframe sandbox="allow-scripts">` with a `null`
320
+ * origin, so it talks to the Condor-controlled shell over `window.postMessage`.
321
+ * These schemas are the **only accepted contract** — any malformed message is
322
+ * discarded by the shell in silence, which is why hand-rolled `postMessage`
323
+ * calls appear to do nothing. Use `BridgeClient`.
324
+ *
325
+ * **Threat model:** the iframe may be hostile (a compromised supply chain, XSS
326
+ * in the client code). These schemas plus the `ShellPolicy` enforced by
327
+ * `BridgeShell` form the structural boundary.
328
+ */
329
+
330
+ /**
331
+ * Default-deny policy enforced by `BridgeShell`. Every key and command coming
332
+ * from the iframe is checked against these lists before it touches the native
333
+ * bridge.
334
+ *
335
+ * - `readOnlyKeys` — keys the client may subscribe to.
336
+ * - `allowedCommands` — pass-through commands, no human confirmation.
337
+ * - `requiresConfirmation` — commands that fire the shell's
338
+ * `onConfirmationRequest` callback, so a person approves or declines.
339
+ * - `deniedCommands` — explicit block, defence in depth: destructive commands
340
+ * such as `dfu` that must never flow from the iframe.
341
+ *
342
+ * **Not embedded in the package** — the `BridgeShell` host supplies the
343
+ * concrete policy. That keeps the package agnostic to the catalogue and lets
344
+ * the policy evolve without a release.
345
+ */
346
+ interface ShellPolicy {
347
+ readOnlyKeys: string[];
348
+ allowedCommands: string[];
349
+ requiresConfirmation: string[];
350
+ deniedCommands: string[];
351
+ }
352
+ /**
353
+ * Argument of the `BridgeShell` `onConfirmationRequest` callback.
354
+ *
355
+ * Resolving `approve: true` lets the command flow to the native side.
356
+ * Resolving `approve: false` makes the shell send the iframe a `nack` with
357
+ * `reason: 'user-rejected'` — which client code only ever sees as a console
358
+ * warning, so the ack it was waiting for simply never arrives.
359
+ */
360
+ interface ConfirmationRequest {
361
+ command: string;
362
+ args: Record<string, unknown>;
363
+ }
364
+ /**
365
+ * Result of `BridgeShell.verifySandbox()` after a round trip with the iframe.
366
+ * If any `*Reachable` flag comes back `true`, the boundary is leaking: the
367
+ * override prefix injected by `bootstrapClientIframe` failed, and the app must
368
+ * not render client code with a path to the native layer.
369
+ */
370
+ interface SandboxVerifyResult {
371
+ flutterBridgeReachable: boolean;
372
+ condorBridgeReachable: boolean;
373
+ /** Whether `window.CondorNative` is visible inside the iframe. */
374
+ nativeChannelReachable: boolean;
375
+ /** `true` only when none of the three is reachable. */
376
+ ok: boolean;
377
+ }
378
+
379
+ /**
380
+ * `BridgeShell` — the API for the **Condor-controlled shell** that wraps
381
+ * external client content inside `<iframe sandbox="allow-scripts">`.
382
+ *
383
+ * This is not client-facing. If you are writing the bundle that renders as the
384
+ * app's Home, you want `BridgeClient`.
385
+ *
386
+ * Responsibilities:
387
+ * 1. **Layer 1 — `bootstrapClientIframe(iframe, clientPath)`:** fetches the
388
+ * client HTML, prepends an override script that removes the native globals
389
+ * from the iframe, and sets it as `srcdoc`. The override runs at parse time,
390
+ * before any other script.
391
+ * 2. **Layer 2 — `verifySandbox(iframe)`:** a `postMessage` round trip asking
392
+ * the iframe to attest that the native globals really are `undefined`.
393
+ * 3. **Routing between iframe and native:** listens for `message` events from
394
+ * the iframe, validates them with Valibot, applies the default-deny
395
+ * `ShellPolicy`, and forwards to `Bridge`. Native events go back to the
396
+ * iframe over `postMessage`.
397
+ * 4. **Human confirmation for sensitive commands:** commands listed in
398
+ * `policy.requiresConfirmation` fire the `onConfirmationRequest` callback
399
+ * before touching the native side, so the shell can present UI.
400
+ */
401
+
402
+ interface BridgeShellOptions {
403
+ /**
404
+ * The iframe hosting the client. It may be empty at construction time —
405
+ * `bootstrapClientIframe` is called later to populate it.
406
+ */
407
+ iframe: HTMLIFrameElement;
408
+ /**
409
+ * The default-deny policy. Every key and command arriving from the iframe is
410
+ * checked against these lists. See `ShellPolicy`.
411
+ */
412
+ policy: ShellPolicy;
413
+ /**
414
+ * Synchronous or asynchronous callback invoked when the client issues a
415
+ * command listed in `policy.requiresConfirmation`. The shell presents its
416
+ * confirmation UI and resolves `true` to approve or `false` to decline.
417
+ *
418
+ * If it is not provided, such commands are **denied** automatically —
419
+ * fail-safe.
420
+ */
421
+ onConfirmationRequest?: (req: ConfirmationRequest) => boolean | Promise<boolean>;
422
+ }
423
+ declare class BridgeShell {
424
+ private readonly iframe;
425
+ private readonly policy;
426
+ private readonly onConfirm?;
427
+ private readonly subscriptions;
428
+ private detached;
429
+ /**
430
+ * The native version, fetched once and cached. `null` until the native side
431
+ * answers, and permanently `null` in a browser with no native host.
432
+ */
433
+ private nativeVersion;
434
+ /** In-flight native handshake; guarantees a single call. */
435
+ private nativeHandshake;
436
+ private pendingVerifies;
437
+ constructor(opts: BridgeShellOptions);
438
+ /**
439
+ * **Layer 1.** Bootstraps by fetching a URL.
440
+ *
441
+ * A shortcut for the common HTTP/HTTPS case: it runs `fetch(clientPath)` and
442
+ * delegates to `bootstrapClientIframeWithHtml`.
443
+ *
444
+ * **It does not work on the `file://` scheme** — Android's Chromium WebView
445
+ * blocks it with "URL scheme 'file' is not supported". For an asset bundled
446
+ * in the APK, or a bundle inlined at build time, use
447
+ * `bootstrapClientIframeWithHtml(htmlString)` instead.
448
+ *
449
+ * **Call it once at shell boot**, before any other interaction. After it
450
+ * returns, invoke `verifySandbox()` for attestation (Layer 2).
451
+ */
452
+ bootstrapClientIframe(clientPath?: string): Promise<void>;
453
+ /**
454
+ * **Layer 1.** Bootstraps from an HTML string.
455
+ *
456
+ * Takes the client HTML directly — already read from a file, embedded at
457
+ * build time, read from storage — and sets it as the iframe's `srcdoc`,
458
+ * prefixed by the override script, which runs at parse time before any
459
+ * client script.
460
+ *
461
+ * Use this when `fetch` does not apply: an asset bundled under `file://`, a
462
+ * bundle inlined at build time, any scenario without network. Android
463
+ * WebView blocks `fetch` on the `file://` scheme, so this is the path the
464
+ * app actually takes. For HTTP, use `bootstrapClientIframe(clientPath)`.
465
+ *
466
+ * **Call it once at shell boot**, before any other interaction. After it
467
+ * returns, invoke `verifySandbox()` for attestation (Layer 2).
468
+ */
469
+ bootstrapClientIframeWithHtml(clientHtml: string): Promise<void>;
470
+ private waitForIframeLoad;
471
+ /**
472
+ * **Layer 2.** A `postMessage` round trip asking the iframe for an
473
+ * authoritative attestation of whether the native globals are reachable.
474
+ * Times out after 2 s.
475
+ *
476
+ * `ok` is `true` only when none of them is reachable. If it comes back
477
+ * `false`, **the shell must abort the app boot**: Layer 1 failed, and client
478
+ * code would run with a path to the native layer.
479
+ */
480
+ verifySandbox(): Promise<SandboxVerifyResult>;
481
+ /**
482
+ * Tears the shell down: removes the `message` listener, cancels pending
483
+ * subscriptions on the native `Bridge`, and clears internal state.
484
+ */
485
+ detach(): void;
486
+ private onMessage;
487
+ private handleSubscribe;
488
+ private handleUnsubscribe;
489
+ private handleSend;
490
+ /**
491
+ * Resolves `nativeVersion`, firing the native handshake the first time and
492
+ * caching the result.
493
+ *
494
+ * ⚠️ `Bridge.handshake` has **no timeout**: its promise only settles when the
495
+ * native side emits `handshake-ack`. In a browser with no native host it
496
+ * never settles, so the race below is what guarantees the client always gets
497
+ * an answer — `null` — instead of hanging.
498
+ */
499
+ private resolveNativeVersion;
500
+ private postToClient;
501
+ }
502
+
503
+ /**
504
+ * Mock adapter — active when no native host is detected: a plain browser,
505
+ * jsdom without an installed host, Storybook, and so on.
506
+ *
507
+ * Behaviour:
508
+ * - `callHandler` records `{ handlerName, payload, timestamp }` in an internal
509
+ * buffer (capped at 100, FIFO), logs through `console.debug`, and dispatches
510
+ * to listeners registered with `onCallHandler`.
511
+ * - `triggerEmit` dispatches through `window.CondorBridge.emit`, so the real
512
+ * bridge treats it as if it came from the native side: `handshake-ack`
513
+ * resolves the handshake, every other key reaches its subscribers.
514
+ * - `getMessages` returns a read-only snapshot of the buffer.
515
+ * - `clearMessages` empties it, which is useful in tests.
516
+ * - `onCallHandler(listener)` registers a listener invoked on every send and
517
+ * returns an unsubscribe function — used by the dev panel's auto-respond
518
+ * toggle, and for assertions in tests.
519
+ */
520
+ interface MockMessage {
521
+ handlerName: string;
522
+ payload: unknown;
523
+ timestamp: number;
524
+ }
525
+ interface MockAdapter {
526
+ readonly isMock: true;
527
+ callHandler(handlerName: string, payload: unknown): void;
528
+ getMessages(): readonly MockMessage[];
529
+ clearMessages(): void;
530
+ triggerEmit(key: string, payload: unknown): void;
531
+ /**
532
+ * Registers a listener invoked on every `callHandler` (a JS → native
533
+ * message). Returns an unsubscribe function.
534
+ *
535
+ * Errors thrown by a listener are caught and logged; they affect neither the
536
+ * other listeners nor the bridge pipeline.
537
+ */
538
+ onCallHandler(listener: (msg: MockMessage) => void): () => void;
539
+ }
540
+
541
+ /**
542
+ * Scenarios for the development harness — the device behaviours that a plain
543
+ * "answer everything at once" mock hides.
544
+ *
545
+ * Each one reproduces something the Condor Canvas app really does, so a bundle
546
+ * that survives every scenario in the browser survives them on a device.
547
+ */
548
+
549
+ /** Native schema version the harness reports by default. */
550
+ declare const HARNESS_NATIVE_VERSION = "0.17.0";
551
+ interface Scenario {
552
+ /** Shown in the harness toolbar. */
553
+ description: string;
554
+ /**
555
+ * Relayed to the client by the handshake. `null` reproduces a shell that
556
+ * could not reach the native side: the client receives `nativeVersion: null`.
557
+ */
558
+ nativeVersion: string | null;
559
+ /** Delay of every native reply, in ms. A pair draws uniformly from the range. */
560
+ latencyMs: number | [min: number, max: number];
561
+ /**
562
+ * Time until the app's background service is up, in ms after the page loads.
563
+ * Until then `ping` and `userDevice` are **dropped without a reply** — on
564
+ * the device they travel to that service, and a message sent before it runs
565
+ * is lost.
566
+ */
567
+ serviceStartMs: number;
568
+ /**
569
+ * `userDevice` never answers. Today the app replies only when the device
570
+ * list loads, so an account with no linked device gets silence.
571
+ */
572
+ userDeviceSilent: boolean;
573
+ /** When set, every `sensorLog` reply fails with this `error`. */
574
+ sensorLogError?: string;
575
+ /** Log period of the devices, in seconds (programmable; default 60). */
576
+ logPeriodS: number;
577
+ systemStatus: SystemStatusPayload;
578
+ devices: () => UserDevicePayload;
579
+ }
580
+ type ScenarioName = 'default' | 'cold-start' | 'no-devices' | 'shared-serial' | 'offline';
581
+ declare const SCENARIOS: Record<ScenarioName, Scenario>;
582
+ declare function resolveScenario(scenario: ScenarioName | Scenario | undefined): Scenario;
583
+
584
+ /**
585
+ * Development harness — runs a client bundle in the browser the way the Condor
586
+ * Canvas app runs it on a device.
587
+ *
588
+ * The bundle goes into a real `<iframe sandbox="allow-scripts">` through the
589
+ * real `BridgeShell`, under the app's policy. Behind the shell, a simulated
590
+ * native side answers commands as the app does — including the parts that
591
+ * hurt: replies lost while the background service starts, silence for an
592
+ * account with no device, latency. Opening `dist/index.html` directly gives
593
+ * none of this, and `BridgeClient` refuses to run there.
594
+ *
595
+ * Use it through the CLI (`npx condor-mobile-bridge dev -i dist/index.html`)
596
+ * or, with your own dev server, from `@condorinst/mobile-bridge/dev`:
597
+ *
598
+ * ```ts
599
+ * import { createDevHarness } from '@condorinst/mobile-bridge/dev';
600
+ * const html = await (await fetch('/index.html')).text();
601
+ * await createDevHarness({ html, scenario: 'cold-start' });
602
+ * ```
603
+ *
604
+ * The harness page is the **top** frame. Never import this module into the
605
+ * bundle itself: `condor-mobile-bridge validate` rejects a bundle that does.
606
+ */
607
+
608
+ /** Marker the `validate` CLI looks for: the harness must never ship in a bundle. */
609
+ declare const DEV_HARNESS_MARKER = "__condorDevHarness__";
610
+ interface HarnessLogEntry {
611
+ at: number;
612
+ /** `sent`: the bundle's command reached the simulated native side. */
613
+ kind: 'sent' | 'reply' | 'dropped' | 'info';
614
+ command?: string;
615
+ key?: string;
616
+ detail: string;
617
+ }
618
+ interface DevHarnessOptions {
619
+ /** The bundle's `index.html`, self-contained, as a string. */
620
+ html: string;
621
+ /** A preset name or a full scenario. Defaults to `'default'`. */
622
+ scenario?: ScenarioName | Scenario;
623
+ /** Where the iframe goes. Defaults to `document.body`. */
624
+ mount?: HTMLElement;
625
+ /** Shows the floating dev panel (sent log + manual emit). Defaults to `true`. */
626
+ devPanel?: boolean;
627
+ /**
628
+ * Answers the confirmation the app asks for `syncTime`, `deleteMemory` and
629
+ * `logout`. Defaults to an in-page dialog.
630
+ */
631
+ onConfirmationRequest?: (req: ConfirmationRequest) => boolean | Promise<boolean>;
632
+ /** Called for every harness log entry, besides `console.info`. */
633
+ onLog?: (entry: HarnessLogEntry) => void;
634
+ }
635
+ interface DevHarness {
636
+ readonly scenario: Scenario;
637
+ readonly iframe: HTMLIFrameElement;
638
+ readonly shell: BridgeShell;
639
+ readonly log: readonly HarnessLogEntry[];
640
+ /** Emits a native event to the bundle, as the app would. */
641
+ emit(key: string, payload: unknown): void;
642
+ /**
643
+ * Presses the OS back button. Emits `navigationBack` only when the bundle
644
+ * last reported `canGoBack: true`, as the app does; otherwise the app keeps
645
+ * the Home and the bundle gets nothing.
646
+ */
647
+ pressBack(): void;
648
+ /** Tears everything down. */
649
+ destroy(): void;
650
+ }
651
+ /**
652
+ * The simulated native side. Exported for tests; most callers want
653
+ * {@link createDevHarness}.
654
+ */
655
+ declare function createSimulatedNative(adapter: MockAdapter, scenario: Scenario, hooks?: {
656
+ onLog?: (entry: HarnessLogEntry) => void;
657
+ now?: () => number;
658
+ }): {
659
+ readonly log: readonly HarnessLogEntry[];
660
+ pressBack(): void;
661
+ dispose(): void;
662
+ };
663
+ /**
664
+ * Mounts the bundle in a sandboxed iframe behind the real `BridgeShell` and a
665
+ * simulated native side. See the module comment.
666
+ */
667
+ declare function createDevHarness(opts: DevHarnessOptions): Promise<DevHarness>;
668
+
669
+ /**
670
+ * The shell policy of the Condor Canvas app, reproduced for the development
671
+ * harness so a command the app refuses is refused in the browser too.
672
+ *
673
+ * Mirrors `apps/canvas/web-mock-src/src/shell.ts` (see
674
+ * `contract/bridge-contract.json` → `verifiedAgainst.sources.shellPolicy`).
675
+ * `tests/dev-harness.test.ts` derives the same lists from the contract and
676
+ * fails if they drift.
677
+ */
678
+
679
+ declare const CANVAS_POLICY: ShellPolicy;
680
+ /** CSP of the app shell, inherited by the client's `srcdoc` iframe. */
681
+ declare const CANVAS_CSP: string;
682
+
683
+ export { CANVAS_CSP, CANVAS_POLICY, DEFAULT_LOG_PERIOD_S, DEV_HARNESS_MARKER, type DevHarness, type DevHarnessOptions, HARNESS_NATIVE_VERSION, type HarnessLogEntry, SCENARIOS, type Scenario, type ScenarioName, createDevHarness, createSimulatedNative, formatLocalTimestamp, parseLocalTimestamp, resolveScenario, sensorLogPage, sharedSerial, systemStatus, twoDevices };