@condorinst/mobile-bridge 0.22.0 → 0.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -196,6 +196,10 @@ interface BridgeOutgoingEvent {
196
196
  * There is no hard semver validation — it is **always** `true`, so it carries
197
197
  * no information. Never branch on it.
198
198
  */
199
+ /**
200
+ * Ack of the legacy {@link Bridge} (`createBridge`). With `BridgeClient`, the
201
+ * ack is {@link BridgeClientHandshakeAck}, whose `nativeVersion` may be `null`.
202
+ */
199
203
  interface HandshakeAck {
200
204
  nativeVersion: string;
201
205
  accepted: boolean;
@@ -332,16 +336,35 @@ interface DeviceUserConfigItem {
332
336
  * — but only when that `serialNo` appears once in `devicesConfig`. A device may
333
337
  * have no telemetry yet: render it with empty readings, never drop it.
334
338
  *
335
- * **It is not guaranteed to answer.** Use `BridgeClient.requestUserDevices()`
336
- * instead of `send('userDevice')`; see `docs/troubleshooting.md`.
339
+ * **Below `nativeVersion` 0.18.0 it is not guaranteed to answer**: a request
340
+ * sent while the app's background service starts is lost, and a failed lookup
341
+ * gets no reply. From 0.18.0 the app re-sends the lost request itself and
342
+ * answers a failure with empty arrays and `error`. Either way, use
343
+ * `BridgeClient.requestUserDevices()` instead of `send('userDevice')`; see
344
+ * `docs/troubleshooting.md`.
337
345
  *
338
346
  * The app enforces no device cap. A commercial rule currently limits a user to
339
347
  * two linked devices, but that can change with no signal in this contract — read
340
348
  * the length of the array rather than hard-coding it.
341
349
  */
350
+ /**
351
+ * Why the device list could not be loaded (`UserDevicePayload.error`, from
352
+ * `nativeVersion` 0.18.0):
353
+ * - `NO_DEVICES` — the server says the account has no linked device;
354
+ * - `NETWORK` — no answer from the server (offline, timeout) or a 5xx;
355
+ * - `SESSION` — no session, or the server ended it;
356
+ * - `UNKNOWN` — anything else. Treat an unrecognised value like `UNKNOWN`.
357
+ */
358
+ type UserDeviceErrorCode = 'NO_DEVICES' | 'NETWORK' | 'SESSION' | 'UNKNOWN' | (string & {});
342
359
  interface UserDevicePayload {
343
360
  /** Epoch milliseconds of the snapshot. */
344
361
  lastUpdate: number;
362
+ /**
363
+ * Present only when the list could not be loaded; the three arrays are then
364
+ * empty. From `nativeVersion` 0.18.0 — older apps send no reply at all in
365
+ * that case. `BridgeClient.requestUserDevices()` turns it into a status.
366
+ */
367
+ error?: UserDeviceErrorCode;
345
368
  devicesConfig: DeviceConfigItem[];
346
369
  devicesTelemetry: DeviceTelemetryItem[];
347
370
  devicesUserConfig: DeviceUserConfigItem[];
@@ -349,6 +372,8 @@ interface UserDevicePayload {
349
372
  /**
350
373
  * Raw light-sensor channels, under the names the backend uses in
351
374
  * `user_cmd=log` — the same vocabulary for history and for real time.
375
+ * Relative values with no physical unit (as are the `wavelength*` channels);
376
+ * only `photopic` / `melanopic` (and `lux_*` in the history) are lux.
352
377
  *
353
378
  * Present from **nativeVersion 0.16.0**. A bundle compiled against this SDK can
354
379
  * run on an older native app that does not emit them: check `nativeVersion`
@@ -387,6 +412,7 @@ type LightSensorsRawChannels = {
387
412
  * change between hardware generations.
388
413
  */
389
414
  type LightSensorsActlumus1Data = LightSensorsRawChannels & {
415
+ /** When the phone received the reading: epoch ms (UTC), phone clock. */
390
416
  time: number;
391
417
  wavelength390: number;
392
418
  wavelength415: number;
@@ -419,6 +445,7 @@ type LightSensorsActlumus1Data = LightSensorsRawChannels & {
419
445
  * Y in the green, X in the red.
420
446
  */
421
447
  type LightSensorsActlumus2Data = LightSensorsRawChannels & {
448
+ /** When the phone received the reading: epoch ms (UTC), phone clock. */
422
449
  time: number;
423
450
  wavelength395: number;
424
451
  wavelength426: number;
@@ -503,9 +530,12 @@ interface DeviceConnectionPayload {
503
530
  isConnect: boolean;
504
531
  }
505
532
  /**
506
- * Device event or status notice. Mirrors `DevicesNotification.toMap()`.
507
- * `message` is already localized by the app; `messageCode` is the stable
508
- * identifier to branch on.
533
+ * One step of a device sync (BLE session). Mirrors `DevicesNotification.toMap()`.
534
+ * Branch on `messageCode` (`pipeline_done_notification` = the sync succeeded,
535
+ * `*_error` = a stage failed); `message` is the app's English log line, not
536
+ * localized and not stable. `isConnect` is `!isError` on sync steps — read
537
+ * the connection from `deviceConnection`. See `docs/bridge-contract.md`,
538
+ * "Sync progress".
509
539
  */
510
540
  interface DeviceNotificationPayload {
511
541
  serialNo: number;
@@ -536,8 +566,8 @@ interface SensorLogQueryPayload {
536
566
  /** Echoed back in the reply — correlates concurrent requests. */
537
567
  requestId: string;
538
568
  serialNo: number;
539
- /** `wrist` or `necklace`. Omit for no filter. */
540
- bodyPart?: string;
569
+ /** Omit for no filter. */
570
+ bodyPart?: 'wrist' | 'necklace';
541
571
  /** 1..1000 (backend default is 10). Outside the range: `INVALID_LIMIT`. */
542
572
  limit?: number;
543
573
  startingAfter?: string;
@@ -562,6 +592,11 @@ interface SensorLogItem {
562
592
  /** `'YYYY-MM-DD HH:MM:SS'` in **device-local time**, not UTC. */
563
593
  log_time: string;
564
594
  log_type: number;
595
+ /**
596
+ * Where the device was set to be worn (its `deviceBodyPart`) when this log was
597
+ * uploaded — a setting, not a detection. Whether it was actually worn is
598
+ * `off_wrist`.
599
+ */
565
600
  body_part: 'wrist' | 'necklace' | null;
566
601
  /** Degrees Celsius, already converted by the backend. */
567
602
  temperature: number | null;
@@ -594,6 +629,11 @@ interface SensorLogItem {
594
629
  act_data_pim?: number | null;
595
630
  act_data_tat?: number | null;
596
631
  act_data_zcm?: number | null;
632
+ /**
633
+ * The device's raw wear flag, and **the name misleads**: `1` = worn, `0` = not
634
+ * worn (on its dock). Treat any value other than `1` as not worn, and `null`
635
+ * as unknown.
636
+ */
597
637
  off_wrist?: number | null;
598
638
  cap_sensor_1?: number | null;
599
639
  cap_sensor_2?: number | null;
@@ -648,6 +688,12 @@ interface SystemStatusPayload {
648
688
  internet: boolean;
649
689
  permission: boolean;
650
690
  hasDevices: boolean;
691
+ /**
692
+ * `true` when `hasDevices` is the server's answer; `false` when the app could
693
+ * not ask (no internet, session) — then `hasDevices: false` does **not** mean
694
+ * "no device". Absent below `nativeVersion` 0.18.0: treat absent as unknown.
695
+ */
696
+ hasDevicesKnown?: boolean;
651
697
  /**
652
698
  * Optional permissions not yet granted, as `AppPermission` names
653
699
  * (e.g. `["notification", "backgroundLocation"]`). Additive and
@@ -688,6 +734,54 @@ interface RequestPermissionPayload {
688
734
  /** Name of the permission to request, e.g. `'bluetoothScan'`. */
689
735
  permission: string;
690
736
  }
737
+ /**
738
+ * Which device a command is about. `serialNo` is an **integer** and always
739
+ * goes; add `deviceId` (from `devicesConfig[].deviceId`) when
740
+ * `nativeVersion` ≥ 0.17.0 — it is the only way to address one of two devices
741
+ * that share a `serialNo`. A string `serialNo` is dropped by the app in silence.
742
+ */
743
+ interface DeviceAddress {
744
+ serialNo: number;
745
+ deviceId?: number;
746
+ }
747
+ /**
748
+ * Arguments of every command the client may send, by name. `BridgeClient.send`
749
+ * checks them at compile time: a command that is not here (or `dfu`, which the
750
+ * shell denies) has no typed arguments and is refused at run time.
751
+ */
752
+ interface CommandArgs {
753
+ ping: Record<string, never> | undefined;
754
+ userDevice: {
755
+ getFromServer?: boolean;
756
+ } | undefined;
757
+ realTime: DeviceAddress & {
758
+ value: boolean;
759
+ };
760
+ deviceConnection: DeviceAddress & {
761
+ type: 'add' | 'remove';
762
+ };
763
+ sensorLog: SensorLogQueryPayload;
764
+ openSettings: Record<string, never> | undefined;
765
+ requestPermission: RequestPermissionPayload;
766
+ setStatusBarColor: {
767
+ color: string;
768
+ };
769
+ navigationState: NavigationStatePayload;
770
+ language: Record<string, never> | undefined;
771
+ jsError: {
772
+ message: string;
773
+ stack?: string;
774
+ source?: string;
775
+ lineno?: number;
776
+ colno?: number;
777
+ };
778
+ syncTime: DeviceAddress;
779
+ deleteMemory: DeviceAddress;
780
+ logout: Record<string, never> | undefined;
781
+ }
782
+ type CommandName = keyof CommandArgs;
783
+ /** The rest-parameters of `send(command, …)`: none, optional or required. */
784
+ type CommandArgsTuple<C extends CommandName> = undefined extends CommandArgs[C] ? [args?: Exclude<CommandArgs[C], undefined>] : [args: CommandArgs[C]];
691
785
  /**
692
786
  * Payload of the `navigationBack` event (native → client). Empty — the request
693
787
  * carries no data; react by going back exactly one internal page.
@@ -906,6 +1000,16 @@ declare const Bridge: CondorBridge;
906
1000
  * native layer.
907
1001
  */
908
1002
 
1003
+ /**
1004
+ * A unique `requestId` for `sensorLog`. Built on `crypto.getRandomValues`,
1005
+ * which works in any context — unlike `crypto.randomUUID`, which exists only in
1006
+ * a secure context, and the app's sandboxed iframe is probably not one.
1007
+ */
1008
+ declare function createRequestId(): string;
1009
+ /** A `sensorLog` query for `fetchSensorLog`: `requestId` is filled in if absent. */
1010
+ type SensorLogFetchQuery = Omit<SensorLogQueryPayload, 'requestId'> & {
1011
+ requestId?: string;
1012
+ };
909
1013
  interface BridgeClientHandshakeAck {
910
1014
  shellVersion: string;
911
1015
  /**
@@ -916,7 +1020,7 @@ interface BridgeClientHandshakeAck {
916
1020
  * version negotiation, and a sandboxed client reaches the native side by no
917
1021
  * other path. For example, the raw light channels (`light_sensor_*` in
918
1022
  * `deviceLightSensors`) exist from `0.16.0`. Always keep a working branch
919
- * for `null`. The dev harness reports `0.17.0` unless a scenario says
1023
+ * for `null`. The dev harness reports `0.18.0` unless a scenario says
920
1024
  * otherwise.
921
1025
  */
922
1026
  nativeVersion: string | null;
@@ -927,6 +1031,31 @@ interface BridgeClientHandshakeAck {
927
1031
  */
928
1032
  accepted: boolean;
929
1033
  }
1034
+ /** What {@link CondorBridgeClient.requestUserDevices} resolves with. */
1035
+ type UserDevicesResult =
1036
+ /** The list arrived. It may be empty. */
1037
+ {
1038
+ status: 'ok';
1039
+ devices: UserDevicePayload;
1040
+ }
1041
+ /** The server says the account has no linked device (nativeVersion 0.18.0+). */
1042
+ | {
1043
+ status: 'no-devices';
1044
+ }
1045
+ /** The app could not load the list: `NETWORK`, `SESSION`, `UNKNOWN` (0.18.0+). */
1046
+ | {
1047
+ status: 'error';
1048
+ code: string;
1049
+ }
1050
+ /**
1051
+ * No answer before the deadline — the only failure an app below 0.18.0 can
1052
+ * produce. `hasDevices` is the last trustworthy `systemStatus.hasDevices`,
1053
+ * or `null`.
1054
+ */
1055
+ | {
1056
+ status: 'timeout';
1057
+ hasDevices: boolean | null;
1058
+ };
930
1059
  interface CondorBridgeClient {
931
1060
  /** Version of the client-side postMessage protocol. */
932
1061
  readonly protocolVersion: string;
@@ -950,7 +1079,7 @@ interface CondorBridgeClient {
950
1079
  * Replies, when a command has one, arrive on the corresponding key — see
951
1080
  * `docs/bridge-contract.md`.
952
1081
  */
953
- send(command: string, args?: Record<string, unknown>): void;
1082
+ send<C extends CommandName>(command: C, ...args: CommandArgsTuple<C>): void;
954
1083
  /** Opens the native Settings screen. Equivalent to `send('openSettings')`. */
955
1084
  openSettings(): void;
956
1085
  /**
@@ -962,9 +1091,10 @@ interface CondorBridgeClient {
962
1091
  * Saves you from keeping a `requestId → resolve` map by hand. To sweep a
963
1092
  * period, chain calls using the returned `startingAfter` until `hasMore`
964
1093
  * becomes `false`: the cursors are dependent, so pages **cannot** be fetched
965
- * in parallel.
1094
+ * in parallel. Leave `requestId` out: one is created for you
1095
+ * ({@link createRequestId}).
966
1096
  */
967
- fetchSensorLog(query: SensorLogQueryPayload, opts?: {
1097
+ fetchSensorLog(query: SensorLogFetchQuery, opts?: {
968
1098
  timeoutMs?: number;
969
1099
  }): Promise<SensorLogPayload>;
970
1100
  /**
@@ -998,19 +1128,23 @@ interface CondorBridgeClient {
998
1128
  }>;
999
1129
  /**
1000
1130
  * Requests the user's devices and **keeps asking** until they arrive. Use
1001
- * this instead of `send('userDevice')`, which may never be answered:
1131
+ * this instead of `send('userDevice')`, which may never be answered on an app
1132
+ * below `nativeVersion` 0.18.0:
1002
1133
  *
1003
1134
  * - on a cold start the first request is lost while the background service
1004
1135
  * comes up;
1005
1136
  * - an account with no linked device gets no reply at all.
1006
1137
  *
1007
1138
  * Resends with backoff (1.5 s, 3 s, 6 s, then every 10 s) and on every
1008
- * `systemStatus` while pending. Never rejects. Resolves with
1009
- * `{ status: 'ok', devices }`, or `{ status: 'timeout', hasDevices }` after
1010
- * `timeoutMs` (default 30 s), where `hasDevices` is the last
1011
- * `systemStatus.hasDevices` seen (`null` if none). `false` there **suggests**
1012
- * an account with no device — show an empty state with a retry button — but
1013
- * the app also reports `false` when it could not reach the server.
1139
+ * `systemStatus` while pending. Never rejects. Resolves a
1140
+ * {@link UserDevicesResult}:
1141
+ * - `ok` — render `devices`;
1142
+ * - `no-devices` — the server confirmed there is none (0.18.0+);
1143
+ * - `error` — the list could not load (`NETWORK`, `SESSION`, `UNKNOWN`;
1144
+ * 0.18.0+): show it with a retry;
1145
+ * - `timeout` — no answer in `timeoutMs` (default 30 s). On an older app this
1146
+ * is how both failures look; `hasDevices === false` then **suggests** an
1147
+ * account with no device, worded so it is true either way.
1014
1148
  *
1015
1149
  * `getFromServer: true` asks the app to refresh from the server; leave it
1016
1150
  * `false` (default) at boot, where the cached list is enough.
@@ -1018,13 +1152,7 @@ interface CondorBridgeClient {
1018
1152
  requestUserDevices(opts?: {
1019
1153
  timeoutMs?: number;
1020
1154
  getFromServer?: boolean;
1021
- }): Promise<{
1022
- status: 'ok';
1023
- devices: UserDevicePayload;
1024
- } | {
1025
- status: 'timeout';
1026
- hasDevices: boolean | null;
1027
- }>;
1155
+ }): Promise<UserDevicesResult>;
1028
1156
  /**
1029
1157
  * Handshakes with the shell. Resolves with
1030
1158
  * `{ shellVersion, nativeVersion, accepted }` — read `nativeVersion` to learn
@@ -1318,4 +1446,4 @@ declare class BridgeShell {
1318
1446
  */
1319
1447
  declare function versionAtLeast(version: string | null | undefined, min: string): boolean;
1320
1448
 
1321
- export { type AutoRespondResponse, BRIDGE_KEYS, Bridge, BridgeClient, type BridgeClientHandshakeAck, type BridgeIncomingMessage, type BridgeKey, type BridgeOutgoingEvent, BridgeShell, type BridgeShellOptions, type ClientToShellMessage, type CondorBridge, type CondorBridgeClient, type ConfirmationRequest, type DeleteMemoryAckPayload, type DevPanelOptions, type DeviceConfigItem, type DeviceConnectionPayload, type DeviceLightSensorsPayload, type DeviceNotificationPayload, type DeviceSyncErrorItem, type DeviceTelemetryItem, type DeviceUserConfigItem, type HandshakeAck, type HandshakeOptions, type KeyPayloads, type LanguagePayload, type LightSensorsActlumus1Data, type LightSensorsActlumus2Data, type LightSensorsRawChannels, type MockMessage, type MockState, NATIVE_CHANNEL_NAME, type NavigationBackPayload, type NavigationStatePayload, type OutgoingStamp, type PayloadOf, type PingPongPayload, type RealTimeAckPayload, type RequestPermissionPayload, type SandboxVerifyResult, type SensorLogItem, type SensorLogPayload, type SensorLogQueryPayload, type ShellPolicy, type ShellToClientMessage, type SyncTimeAckPayload, type SystemStatusPayload, type UserDevicePayload, versionAtLeast };
1449
+ export { type AutoRespondResponse, BRIDGE_KEYS, Bridge, BridgeClient, type BridgeClientHandshakeAck, type BridgeIncomingMessage, type BridgeKey, type BridgeOutgoingEvent, BridgeShell, type BridgeShellOptions, type ClientToShellMessage, type CommandArgs, type CommandArgsTuple, type CommandName, type CondorBridge, type CondorBridgeClient, type ConfirmationRequest, type DeleteMemoryAckPayload, type DevPanelOptions, type DeviceAddress, type DeviceConfigItem, type DeviceConnectionPayload, type DeviceLightSensorsPayload, type DeviceNotificationPayload, type DeviceSyncErrorItem, type DeviceTelemetryItem, type DeviceUserConfigItem, type HandshakeAck, type HandshakeOptions, type KeyPayloads, type LanguagePayload, type LightSensorsActlumus1Data, type LightSensorsActlumus2Data, type LightSensorsRawChannels, type MockMessage, type MockState, NATIVE_CHANNEL_NAME, type NavigationBackPayload, type NavigationStatePayload, type OutgoingStamp, type PayloadOf, type PingPongPayload, type RealTimeAckPayload, type RequestPermissionPayload, type SandboxVerifyResult, type SensorLogFetchQuery, type SensorLogItem, type SensorLogPayload, type SensorLogQueryPayload, type ShellPolicy, type ShellToClientMessage, type SyncTimeAckPayload, type SystemStatusPayload, type UserDeviceErrorCode, type UserDevicePayload, type UserDevicesResult, createRequestId, versionAtLeast };
package/dist/index.d.ts CHANGED
@@ -196,6 +196,10 @@ interface BridgeOutgoingEvent {
196
196
  * There is no hard semver validation — it is **always** `true`, so it carries
197
197
  * no information. Never branch on it.
198
198
  */
199
+ /**
200
+ * Ack of the legacy {@link Bridge} (`createBridge`). With `BridgeClient`, the
201
+ * ack is {@link BridgeClientHandshakeAck}, whose `nativeVersion` may be `null`.
202
+ */
199
203
  interface HandshakeAck {
200
204
  nativeVersion: string;
201
205
  accepted: boolean;
@@ -332,16 +336,35 @@ interface DeviceUserConfigItem {
332
336
  * — but only when that `serialNo` appears once in `devicesConfig`. A device may
333
337
  * have no telemetry yet: render it with empty readings, never drop it.
334
338
  *
335
- * **It is not guaranteed to answer.** Use `BridgeClient.requestUserDevices()`
336
- * instead of `send('userDevice')`; see `docs/troubleshooting.md`.
339
+ * **Below `nativeVersion` 0.18.0 it is not guaranteed to answer**: a request
340
+ * sent while the app's background service starts is lost, and a failed lookup
341
+ * gets no reply. From 0.18.0 the app re-sends the lost request itself and
342
+ * answers a failure with empty arrays and `error`. Either way, use
343
+ * `BridgeClient.requestUserDevices()` instead of `send('userDevice')`; see
344
+ * `docs/troubleshooting.md`.
337
345
  *
338
346
  * The app enforces no device cap. A commercial rule currently limits a user to
339
347
  * two linked devices, but that can change with no signal in this contract — read
340
348
  * the length of the array rather than hard-coding it.
341
349
  */
350
+ /**
351
+ * Why the device list could not be loaded (`UserDevicePayload.error`, from
352
+ * `nativeVersion` 0.18.0):
353
+ * - `NO_DEVICES` — the server says the account has no linked device;
354
+ * - `NETWORK` — no answer from the server (offline, timeout) or a 5xx;
355
+ * - `SESSION` — no session, or the server ended it;
356
+ * - `UNKNOWN` — anything else. Treat an unrecognised value like `UNKNOWN`.
357
+ */
358
+ type UserDeviceErrorCode = 'NO_DEVICES' | 'NETWORK' | 'SESSION' | 'UNKNOWN' | (string & {});
342
359
  interface UserDevicePayload {
343
360
  /** Epoch milliseconds of the snapshot. */
344
361
  lastUpdate: number;
362
+ /**
363
+ * Present only when the list could not be loaded; the three arrays are then
364
+ * empty. From `nativeVersion` 0.18.0 — older apps send no reply at all in
365
+ * that case. `BridgeClient.requestUserDevices()` turns it into a status.
366
+ */
367
+ error?: UserDeviceErrorCode;
345
368
  devicesConfig: DeviceConfigItem[];
346
369
  devicesTelemetry: DeviceTelemetryItem[];
347
370
  devicesUserConfig: DeviceUserConfigItem[];
@@ -349,6 +372,8 @@ interface UserDevicePayload {
349
372
  /**
350
373
  * Raw light-sensor channels, under the names the backend uses in
351
374
  * `user_cmd=log` — the same vocabulary for history and for real time.
375
+ * Relative values with no physical unit (as are the `wavelength*` channels);
376
+ * only `photopic` / `melanopic` (and `lux_*` in the history) are lux.
352
377
  *
353
378
  * Present from **nativeVersion 0.16.0**. A bundle compiled against this SDK can
354
379
  * run on an older native app that does not emit them: check `nativeVersion`
@@ -387,6 +412,7 @@ type LightSensorsRawChannels = {
387
412
  * change between hardware generations.
388
413
  */
389
414
  type LightSensorsActlumus1Data = LightSensorsRawChannels & {
415
+ /** When the phone received the reading: epoch ms (UTC), phone clock. */
390
416
  time: number;
391
417
  wavelength390: number;
392
418
  wavelength415: number;
@@ -419,6 +445,7 @@ type LightSensorsActlumus1Data = LightSensorsRawChannels & {
419
445
  * Y in the green, X in the red.
420
446
  */
421
447
  type LightSensorsActlumus2Data = LightSensorsRawChannels & {
448
+ /** When the phone received the reading: epoch ms (UTC), phone clock. */
422
449
  time: number;
423
450
  wavelength395: number;
424
451
  wavelength426: number;
@@ -503,9 +530,12 @@ interface DeviceConnectionPayload {
503
530
  isConnect: boolean;
504
531
  }
505
532
  /**
506
- * Device event or status notice. Mirrors `DevicesNotification.toMap()`.
507
- * `message` is already localized by the app; `messageCode` is the stable
508
- * identifier to branch on.
533
+ * One step of a device sync (BLE session). Mirrors `DevicesNotification.toMap()`.
534
+ * Branch on `messageCode` (`pipeline_done_notification` = the sync succeeded,
535
+ * `*_error` = a stage failed); `message` is the app's English log line, not
536
+ * localized and not stable. `isConnect` is `!isError` on sync steps — read
537
+ * the connection from `deviceConnection`. See `docs/bridge-contract.md`,
538
+ * "Sync progress".
509
539
  */
510
540
  interface DeviceNotificationPayload {
511
541
  serialNo: number;
@@ -536,8 +566,8 @@ interface SensorLogQueryPayload {
536
566
  /** Echoed back in the reply — correlates concurrent requests. */
537
567
  requestId: string;
538
568
  serialNo: number;
539
- /** `wrist` or `necklace`. Omit for no filter. */
540
- bodyPart?: string;
569
+ /** Omit for no filter. */
570
+ bodyPart?: 'wrist' | 'necklace';
541
571
  /** 1..1000 (backend default is 10). Outside the range: `INVALID_LIMIT`. */
542
572
  limit?: number;
543
573
  startingAfter?: string;
@@ -562,6 +592,11 @@ interface SensorLogItem {
562
592
  /** `'YYYY-MM-DD HH:MM:SS'` in **device-local time**, not UTC. */
563
593
  log_time: string;
564
594
  log_type: number;
595
+ /**
596
+ * Where the device was set to be worn (its `deviceBodyPart`) when this log was
597
+ * uploaded — a setting, not a detection. Whether it was actually worn is
598
+ * `off_wrist`.
599
+ */
565
600
  body_part: 'wrist' | 'necklace' | null;
566
601
  /** Degrees Celsius, already converted by the backend. */
567
602
  temperature: number | null;
@@ -594,6 +629,11 @@ interface SensorLogItem {
594
629
  act_data_pim?: number | null;
595
630
  act_data_tat?: number | null;
596
631
  act_data_zcm?: number | null;
632
+ /**
633
+ * The device's raw wear flag, and **the name misleads**: `1` = worn, `0` = not
634
+ * worn (on its dock). Treat any value other than `1` as not worn, and `null`
635
+ * as unknown.
636
+ */
597
637
  off_wrist?: number | null;
598
638
  cap_sensor_1?: number | null;
599
639
  cap_sensor_2?: number | null;
@@ -648,6 +688,12 @@ interface SystemStatusPayload {
648
688
  internet: boolean;
649
689
  permission: boolean;
650
690
  hasDevices: boolean;
691
+ /**
692
+ * `true` when `hasDevices` is the server's answer; `false` when the app could
693
+ * not ask (no internet, session) — then `hasDevices: false` does **not** mean
694
+ * "no device". Absent below `nativeVersion` 0.18.0: treat absent as unknown.
695
+ */
696
+ hasDevicesKnown?: boolean;
651
697
  /**
652
698
  * Optional permissions not yet granted, as `AppPermission` names
653
699
  * (e.g. `["notification", "backgroundLocation"]`). Additive and
@@ -688,6 +734,54 @@ interface RequestPermissionPayload {
688
734
  /** Name of the permission to request, e.g. `'bluetoothScan'`. */
689
735
  permission: string;
690
736
  }
737
+ /**
738
+ * Which device a command is about. `serialNo` is an **integer** and always
739
+ * goes; add `deviceId` (from `devicesConfig[].deviceId`) when
740
+ * `nativeVersion` ≥ 0.17.0 — it is the only way to address one of two devices
741
+ * that share a `serialNo`. A string `serialNo` is dropped by the app in silence.
742
+ */
743
+ interface DeviceAddress {
744
+ serialNo: number;
745
+ deviceId?: number;
746
+ }
747
+ /**
748
+ * Arguments of every command the client may send, by name. `BridgeClient.send`
749
+ * checks them at compile time: a command that is not here (or `dfu`, which the
750
+ * shell denies) has no typed arguments and is refused at run time.
751
+ */
752
+ interface CommandArgs {
753
+ ping: Record<string, never> | undefined;
754
+ userDevice: {
755
+ getFromServer?: boolean;
756
+ } | undefined;
757
+ realTime: DeviceAddress & {
758
+ value: boolean;
759
+ };
760
+ deviceConnection: DeviceAddress & {
761
+ type: 'add' | 'remove';
762
+ };
763
+ sensorLog: SensorLogQueryPayload;
764
+ openSettings: Record<string, never> | undefined;
765
+ requestPermission: RequestPermissionPayload;
766
+ setStatusBarColor: {
767
+ color: string;
768
+ };
769
+ navigationState: NavigationStatePayload;
770
+ language: Record<string, never> | undefined;
771
+ jsError: {
772
+ message: string;
773
+ stack?: string;
774
+ source?: string;
775
+ lineno?: number;
776
+ colno?: number;
777
+ };
778
+ syncTime: DeviceAddress;
779
+ deleteMemory: DeviceAddress;
780
+ logout: Record<string, never> | undefined;
781
+ }
782
+ type CommandName = keyof CommandArgs;
783
+ /** The rest-parameters of `send(command, …)`: none, optional or required. */
784
+ type CommandArgsTuple<C extends CommandName> = undefined extends CommandArgs[C] ? [args?: Exclude<CommandArgs[C], undefined>] : [args: CommandArgs[C]];
691
785
  /**
692
786
  * Payload of the `navigationBack` event (native → client). Empty — the request
693
787
  * carries no data; react by going back exactly one internal page.
@@ -906,6 +1000,16 @@ declare const Bridge: CondorBridge;
906
1000
  * native layer.
907
1001
  */
908
1002
 
1003
+ /**
1004
+ * A unique `requestId` for `sensorLog`. Built on `crypto.getRandomValues`,
1005
+ * which works in any context — unlike `crypto.randomUUID`, which exists only in
1006
+ * a secure context, and the app's sandboxed iframe is probably not one.
1007
+ */
1008
+ declare function createRequestId(): string;
1009
+ /** A `sensorLog` query for `fetchSensorLog`: `requestId` is filled in if absent. */
1010
+ type SensorLogFetchQuery = Omit<SensorLogQueryPayload, 'requestId'> & {
1011
+ requestId?: string;
1012
+ };
909
1013
  interface BridgeClientHandshakeAck {
910
1014
  shellVersion: string;
911
1015
  /**
@@ -916,7 +1020,7 @@ interface BridgeClientHandshakeAck {
916
1020
  * version negotiation, and a sandboxed client reaches the native side by no
917
1021
  * other path. For example, the raw light channels (`light_sensor_*` in
918
1022
  * `deviceLightSensors`) exist from `0.16.0`. Always keep a working branch
919
- * for `null`. The dev harness reports `0.17.0` unless a scenario says
1023
+ * for `null`. The dev harness reports `0.18.0` unless a scenario says
920
1024
  * otherwise.
921
1025
  */
922
1026
  nativeVersion: string | null;
@@ -927,6 +1031,31 @@ interface BridgeClientHandshakeAck {
927
1031
  */
928
1032
  accepted: boolean;
929
1033
  }
1034
+ /** What {@link CondorBridgeClient.requestUserDevices} resolves with. */
1035
+ type UserDevicesResult =
1036
+ /** The list arrived. It may be empty. */
1037
+ {
1038
+ status: 'ok';
1039
+ devices: UserDevicePayload;
1040
+ }
1041
+ /** The server says the account has no linked device (nativeVersion 0.18.0+). */
1042
+ | {
1043
+ status: 'no-devices';
1044
+ }
1045
+ /** The app could not load the list: `NETWORK`, `SESSION`, `UNKNOWN` (0.18.0+). */
1046
+ | {
1047
+ status: 'error';
1048
+ code: string;
1049
+ }
1050
+ /**
1051
+ * No answer before the deadline — the only failure an app below 0.18.0 can
1052
+ * produce. `hasDevices` is the last trustworthy `systemStatus.hasDevices`,
1053
+ * or `null`.
1054
+ */
1055
+ | {
1056
+ status: 'timeout';
1057
+ hasDevices: boolean | null;
1058
+ };
930
1059
  interface CondorBridgeClient {
931
1060
  /** Version of the client-side postMessage protocol. */
932
1061
  readonly protocolVersion: string;
@@ -950,7 +1079,7 @@ interface CondorBridgeClient {
950
1079
  * Replies, when a command has one, arrive on the corresponding key — see
951
1080
  * `docs/bridge-contract.md`.
952
1081
  */
953
- send(command: string, args?: Record<string, unknown>): void;
1082
+ send<C extends CommandName>(command: C, ...args: CommandArgsTuple<C>): void;
954
1083
  /** Opens the native Settings screen. Equivalent to `send('openSettings')`. */
955
1084
  openSettings(): void;
956
1085
  /**
@@ -962,9 +1091,10 @@ interface CondorBridgeClient {
962
1091
  * Saves you from keeping a `requestId → resolve` map by hand. To sweep a
963
1092
  * period, chain calls using the returned `startingAfter` until `hasMore`
964
1093
  * becomes `false`: the cursors are dependent, so pages **cannot** be fetched
965
- * in parallel.
1094
+ * in parallel. Leave `requestId` out: one is created for you
1095
+ * ({@link createRequestId}).
966
1096
  */
967
- fetchSensorLog(query: SensorLogQueryPayload, opts?: {
1097
+ fetchSensorLog(query: SensorLogFetchQuery, opts?: {
968
1098
  timeoutMs?: number;
969
1099
  }): Promise<SensorLogPayload>;
970
1100
  /**
@@ -998,19 +1128,23 @@ interface CondorBridgeClient {
998
1128
  }>;
999
1129
  /**
1000
1130
  * Requests the user's devices and **keeps asking** until they arrive. Use
1001
- * this instead of `send('userDevice')`, which may never be answered:
1131
+ * this instead of `send('userDevice')`, which may never be answered on an app
1132
+ * below `nativeVersion` 0.18.0:
1002
1133
  *
1003
1134
  * - on a cold start the first request is lost while the background service
1004
1135
  * comes up;
1005
1136
  * - an account with no linked device gets no reply at all.
1006
1137
  *
1007
1138
  * Resends with backoff (1.5 s, 3 s, 6 s, then every 10 s) and on every
1008
- * `systemStatus` while pending. Never rejects. Resolves with
1009
- * `{ status: 'ok', devices }`, or `{ status: 'timeout', hasDevices }` after
1010
- * `timeoutMs` (default 30 s), where `hasDevices` is the last
1011
- * `systemStatus.hasDevices` seen (`null` if none). `false` there **suggests**
1012
- * an account with no device — show an empty state with a retry button — but
1013
- * the app also reports `false` when it could not reach the server.
1139
+ * `systemStatus` while pending. Never rejects. Resolves a
1140
+ * {@link UserDevicesResult}:
1141
+ * - `ok` — render `devices`;
1142
+ * - `no-devices` — the server confirmed there is none (0.18.0+);
1143
+ * - `error` — the list could not load (`NETWORK`, `SESSION`, `UNKNOWN`;
1144
+ * 0.18.0+): show it with a retry;
1145
+ * - `timeout` — no answer in `timeoutMs` (default 30 s). On an older app this
1146
+ * is how both failures look; `hasDevices === false` then **suggests** an
1147
+ * account with no device, worded so it is true either way.
1014
1148
  *
1015
1149
  * `getFromServer: true` asks the app to refresh from the server; leave it
1016
1150
  * `false` (default) at boot, where the cached list is enough.
@@ -1018,13 +1152,7 @@ interface CondorBridgeClient {
1018
1152
  requestUserDevices(opts?: {
1019
1153
  timeoutMs?: number;
1020
1154
  getFromServer?: boolean;
1021
- }): Promise<{
1022
- status: 'ok';
1023
- devices: UserDevicePayload;
1024
- } | {
1025
- status: 'timeout';
1026
- hasDevices: boolean | null;
1027
- }>;
1155
+ }): Promise<UserDevicesResult>;
1028
1156
  /**
1029
1157
  * Handshakes with the shell. Resolves with
1030
1158
  * `{ shellVersion, nativeVersion, accepted }` — read `nativeVersion` to learn
@@ -1318,4 +1446,4 @@ declare class BridgeShell {
1318
1446
  */
1319
1447
  declare function versionAtLeast(version: string | null | undefined, min: string): boolean;
1320
1448
 
1321
- export { type AutoRespondResponse, BRIDGE_KEYS, Bridge, BridgeClient, type BridgeClientHandshakeAck, type BridgeIncomingMessage, type BridgeKey, type BridgeOutgoingEvent, BridgeShell, type BridgeShellOptions, type ClientToShellMessage, type CondorBridge, type CondorBridgeClient, type ConfirmationRequest, type DeleteMemoryAckPayload, type DevPanelOptions, type DeviceConfigItem, type DeviceConnectionPayload, type DeviceLightSensorsPayload, type DeviceNotificationPayload, type DeviceSyncErrorItem, type DeviceTelemetryItem, type DeviceUserConfigItem, type HandshakeAck, type HandshakeOptions, type KeyPayloads, type LanguagePayload, type LightSensorsActlumus1Data, type LightSensorsActlumus2Data, type LightSensorsRawChannels, type MockMessage, type MockState, NATIVE_CHANNEL_NAME, type NavigationBackPayload, type NavigationStatePayload, type OutgoingStamp, type PayloadOf, type PingPongPayload, type RealTimeAckPayload, type RequestPermissionPayload, type SandboxVerifyResult, type SensorLogItem, type SensorLogPayload, type SensorLogQueryPayload, type ShellPolicy, type ShellToClientMessage, type SyncTimeAckPayload, type SystemStatusPayload, type UserDevicePayload, versionAtLeast };
1449
+ export { type AutoRespondResponse, BRIDGE_KEYS, Bridge, BridgeClient, type BridgeClientHandshakeAck, type BridgeIncomingMessage, type BridgeKey, type BridgeOutgoingEvent, BridgeShell, type BridgeShellOptions, type ClientToShellMessage, type CommandArgs, type CommandArgsTuple, type CommandName, type CondorBridge, type CondorBridgeClient, type ConfirmationRequest, type DeleteMemoryAckPayload, type DevPanelOptions, type DeviceAddress, type DeviceConfigItem, type DeviceConnectionPayload, type DeviceLightSensorsPayload, type DeviceNotificationPayload, type DeviceSyncErrorItem, type DeviceTelemetryItem, type DeviceUserConfigItem, type HandshakeAck, type HandshakeOptions, type KeyPayloads, type LanguagePayload, type LightSensorsActlumus1Data, type LightSensorsActlumus2Data, type LightSensorsRawChannels, type MockMessage, type MockState, NATIVE_CHANNEL_NAME, type NavigationBackPayload, type NavigationStatePayload, type OutgoingStamp, type PayloadOf, type PingPongPayload, type RealTimeAckPayload, type RequestPermissionPayload, type SandboxVerifyResult, type SensorLogFetchQuery, type SensorLogItem, type SensorLogPayload, type SensorLogQueryPayload, type ShellPolicy, type ShellToClientMessage, type SyncTimeAckPayload, type SystemStatusPayload, type UserDeviceErrorCode, type UserDevicePayload, type UserDevicesResult, createRequestId, versionAtLeast };