@condorinst/mobile-bridge 0.19.2 → 0.20.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/CHANGELOG.md CHANGED
@@ -7,6 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.20.0] — 2026-10-13
11
+
12
+ ### Added
13
+
14
+ - **Address one device by `deviceId` when two share a `serialNo`.** An ActLumus
15
+ and an ActLumus Plus have separate serial sequences, so one account can hold
16
+ both with the same `serialNo`. `realTime`, `syncTime`, `deleteMemory` and
17
+ `deviceConnection` accept an optional `deviceId` next to `serialNo`; keep
18
+ sending `serialNo` as well, because an older app drops in silence a command
19
+ that lacks it. Requires `nativeVersion` 0.17.0, which the contract now lists
20
+ as the `deviceIdAddressing` capability.
21
+ - **`deviceId` in the device events.** `deviceConnection`, `deviceNotification`
22
+ and `deviceLightSensors` carry an optional `deviceId`, present only when the
23
+ `serialNo` is shared by more than one device. The acks of `realTime`,
24
+ `syncTime` and `deleteMemory` echo the `deviceId` you sent and always keep
25
+ `serialNo`.
26
+ - **`systemStatus.deviceSyncErrorById`**, the same errors keyed by `deviceId`
27
+ for every device. `deviceSyncError` leaves out a `serialNo` that two devices
28
+ share, since no single device owns it.
29
+ - The contract and `docs/bridge-contract.md` document the rule in a new section,
30
+ and list `DEVICE_SERIAL_AMBIGUOUS` among the `sensorLog` error codes: the
31
+ backend answers it for a shared `serialNo`, and `sensorLog` takes no
32
+ `deviceId` yet.
33
+
34
+ ### Notes
35
+
36
+ - Additive and backward-compatible: every new field is optional and older
37
+ clients ignore it. The model of a device is **not** carried by these events;
38
+ read `modelId` from the `userDevice` list.
39
+
10
40
  ## [0.19.2] — 2026-09-23
11
41
 
12
42
  ### Fixed
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "contractVersion": "0.19.2",
3
+ "contractVersion": "0.20.0",
4
4
  "description": "Machine-readable contract of the Condor Canvas JS<->Native bridge. It states what the TypeScript declarations in dist/index.d.ts cannot express: which keys and commands a sandboxed client can actually reach, which ones need human confirmation, where messages are dropped in silence, the hard limits, and which capability each native version carries. The TypeScript types remain canonical for payload shapes.",
5
5
  "audience": "Developers and AI agents building the client bundle that renders inside the Canvas Home.",
6
6
 
7
7
  "versions": {
8
8
  "note": "Four independent numbers coexist. Only nativeVersion tells you what the device can do.",
9
- "package": "0.19.2",
9
+ "package": "0.20.0",
10
10
  "shellVersion": "0.6.0",
11
11
  "clientProtocolVersion": "0.5.0",
12
- "nativeVersion": "0.16.0",
12
+ "nativeVersion": "0.17.0",
13
13
  "capabilityDetection": {
14
14
  "how": "await BridgeClient.handshake({ clientVersion }) resolves to { shellVersion, nativeVersion, accepted }. Compare ack.nativeVersion against the minNativeVersion in capabilities[].",
15
15
  "acceptedIsAlwaysTrue": true,
@@ -101,8 +101,8 @@
101
101
  "entries": {
102
102
  "ping": { "args": {}, "requiresConfirmation": false, "repliesOn": "pingPong" },
103
103
  "userDevice": { "args": { "getFromServer": "boolean, default false" }, "requiresConfirmation": false, "repliesOn": "userDevice" },
104
- "realTime": { "args": { "serialNo": "int", "value": "boolean" }, "requiresConfirmation": false, "repliesOn": "realTime", "note": "Off by default. The deviceLightSensors stream only flows while this is on." },
105
- "deviceConnection": { "args": { "type": "'add' | 'remove'", "serialNo": "int" }, "requiresConfirmation": false, "repliesOn": "deviceConnection" },
104
+ "realTime": { "args": { "serialNo": "int", "deviceId": "int, optional, minNativeVersion 0.17.0", "value": "boolean" }, "requiresConfirmation": false, "repliesOn": "realTime", "note": "Off by default. The deviceLightSensors stream only flows while this is on. Always send serialNo; add deviceId to address one device when two share a serialNo." },
105
+ "deviceConnection": { "args": { "type": "'add' | 'remove'", "serialNo": "int", "deviceId": "int, optional, minNativeVersion 0.17.0" }, "requiresConfirmation": false, "repliesOn": "deviceConnection" },
106
106
  "sensorLog": { "args": { "requestId": "string", "serialNo": "int", "bodyPart": "'wrist' | 'necklace', optional", "limit": "1..1000, optional", "startingAfter": "opaque cursor, optional", "endingBefore": "opaque cursor, optional", "startTime": "timestamp, optional, minNativeVersion 0.15.0", "endTime": "timestamp, optional, minNativeVersion 0.15.0" }, "requiresConfirmation": false, "repliesOn": "sensorLog", "prefer": "BridgeClient.fetchSensorLog(query), which correlates requestId and applies a timeout for you." },
107
107
  "openSettings": { "args": {}, "requiresConfirmation": false, "repliesOn": null },
108
108
  "requestPermission": { "args": { "permission": "string, must match a native AppPermission name" }, "requiresConfirmation": false, "repliesOn": null, "note": "There is no permission-result key. The OS shows its own prompt and the outcome reaches you as a fresh systemStatus emission." },
@@ -110,8 +110,8 @@
110
110
  "navigationState": { "args": { "canGoBack": "boolean" }, "requiresConfirmation": false, "repliesOn": null, "prefer": "BridgeClient.reportNavigation(canGoBack)" },
111
111
  "language": { "args": {}, "requiresConfirmation": false, "repliesOn": "language", "note": "Pull. The native push happens before window.CondorBridge exists, so a client that mounts later must ask.", "minNativeVersion": "0.14.0" },
112
112
  "jsError": { "args": { "message": "string", "stack": "optional", "source": "optional", "lineno": "optional", "colno": "optional" }, "requiresConfirmation": false, "repliesOn": null, "note": "Rate limited: 20 per 10 s, fields truncated at 4 KB." },
113
- "syncTime": { "args": { "serialNo": "int" }, "requiresConfirmation": true, "repliesOn": "syncTime" },
114
- "deleteMemory": { "args": { "serialNo": "int" }, "requiresConfirmation": true, "repliesOn": "deleteMemory" },
113
+ "syncTime": { "args": { "serialNo": "int", "deviceId": "int, optional, minNativeVersion 0.17.0" }, "requiresConfirmation": true, "repliesOn": "syncTime" },
114
+ "deleteMemory": { "args": { "serialNo": "int", "deviceId": "int, optional, minNativeVersion 0.17.0" }, "requiresConfirmation": true, "repliesOn": "deleteMemory" },
115
115
  "logout": { "args": {}, "requiresConfirmation": true, "repliesOn": null },
116
116
  "dfu": { "args": {}, "denied": true, "repliesOn": null }
117
117
  },
@@ -130,7 +130,7 @@
130
130
  "name": "sensorLog failure",
131
131
  "channel": "sensorLog key",
132
132
  "shape": { "requestId": "string", "success": false, "error": "string code" },
133
- "codes": ["CONFLICTING_CURSORS", "INVALID_BODY_PART", "INVALID_LIMIT", "INVALID_START_TIME", "INVALID_END_TIME", "INVALID_TIME_RANGE", "MISSING_DEVICE_SERIAL_NO", "invalid session", "device not found for this user"],
133
+ "codes": ["CONFLICTING_CURSORS", "INVALID_BODY_PART", "INVALID_LIMIT", "INVALID_START_TIME", "INVALID_END_TIME", "INVALID_TIME_RANGE", "MISSING_DEVICE_SERIAL_NO", "DEVICE_SERIAL_AMBIGUOUS", "invalid session", "device not found for this user"],
134
134
  "visibility": "fetchSensorLog RESOLVES with success false. It only rejects on timeout.",
135
135
  "warning": "error is a diagnostic code, never user-facing text. Map it to your own copy."
136
136
  },
@@ -155,6 +155,7 @@
155
155
  { "feature": "languageKey", "minNativeVersion": "0.14.0", "description": "The language key and the language pull command exist." },
156
156
  { "feature": "sensorLogTimeRange", "minNativeVersion": "0.15.0", "description": "sensorLog accepts startTime and endTime." },
157
157
  { "feature": "lightSensorRawChannels", "minNativeVersion": "0.16.0", "description": "deviceLightSensors carries the raw light_sensor_* channels and the actlumus2 variant." },
158
+ { "feature": "deviceIdAddressing", "minNativeVersion": "0.17.0", "description": "realTime, syncTime, deleteMemory and deviceConnection accept an optional deviceId, so one device can be addressed when an ActLumus and a Plus share a serialNo; the acks echo it; deviceConnection, deviceNotification and deviceLightSensors carry it when the serialNo is shared; systemStatus adds deviceSyncErrorById." },
158
159
  { "feature": "nativeVersionRelay", "minShellVersion": "0.6.0", "description": "The shell relays nativeVersion into the sandboxed client. Below this, handshake-ack carried only shellVersion and capability detection was impossible from inside the iframe." }
159
160
  ],
160
161
 
@@ -179,23 +180,23 @@
179
180
  "verifiedAgainst": {
180
181
  "monorepo": "condorinstprogdev/mobile_applications",
181
182
  "branch": "dev_canvas",
182
- "commit": "89fe1c792202ca9f066b84a63af44a6f2b993b52",
183
- "verifiedOn": "2026-09-23",
183
+ "commit": "7a40a779d23d1ee49433982d37ebf4979444bf95",
184
+ "verifiedOn": "2026-10-13",
184
185
  "sources": {
185
- "nativeVersion": "apps/canvas/lib/ui/pages/home/bridge/condor_bridge.dart:32",
186
- "jsToNativePayloadBytes": "apps/canvas/lib/ui/pages/home/bridge/condor_bridge.dart:88",
187
- "jsErrorPerWindow": "apps/canvas/lib/ui/pages/home/bridge/condor_bridge.dart:93-94",
188
- "jsErrorFieldBytes": "apps/canvas/lib/ui/pages/home/bridge/condor_bridge.dart:97",
189
- "acceptedIsAlwaysTrue": "apps/canvas/lib/ui/pages/home/bridge/condor_bridge.dart:417-425",
190
- "silentDropPaths": "apps/canvas/lib/ui/pages/home/bridge/condor_bridge.dart:218-238,291-295,452-467,529-536",
186
+ "nativeVersion": "apps/canvas/lib/ui/pages/home/bridge/condor_bridge.dart:38",
187
+ "jsToNativePayloadBytes": "apps/canvas/lib/ui/pages/home/bridge/condor_bridge.dart:94",
188
+ "jsErrorPerWindow": "apps/canvas/lib/ui/pages/home/bridge/condor_bridge.dart:99-100",
189
+ "jsErrorFieldBytes": "apps/canvas/lib/ui/pages/home/bridge/condor_bridge.dart:103",
190
+ "acceptedIsAlwaysTrue": "apps/canvas/lib/ui/pages/home/bridge/condor_bridge.dart:486-494",
191
+ "silentDropPaths": "apps/canvas/lib/ui/pages/home/bridge/condor_bridge.dart:246-266,319-323,521-536,606-613",
191
192
  "bundleZipBytes": "apps/canvas/lib/core/config/whitelabel_bundle_limits.dart:14",
192
193
  "bundleGuards": "apps/canvas/lib/data/services/whitelabel_bundle_service.dart:227-398",
193
194
  "integrity": "apps/canvas/lib/data/services/whitelabel_bundle_service.dart:244-259",
194
195
  "sensorLogValidation": "apps/canvas/lib/domain/entities/sensor_log_entity.dart:70-85",
195
196
  "csp": "apps/canvas/web-mock-src/shell.template.html:28",
196
197
  "shellPolicy": "apps/canvas/web-mock-src/src/shell.ts:48-98",
197
- "webViewPermissionDeny": "apps/canvas/lib/ui/pages/home/home_page.dart:260-268",
198
- "opaqueOrigin": "apps/canvas/lib/ui/pages/home/home_page.dart:296-298"
198
+ "webViewPermissionDeny": "apps/canvas/lib/ui/pages/home/home_page.dart:263-271",
199
+ "opaqueOrigin": "apps/canvas/lib/ui/pages/home/home_page.dart:299-301"
199
200
  },
200
201
  "maintenance": "Re-verify every entry in sources before cutting a release. A published contract that drifts from the app is worse than no contract."
201
202
  }
package/dist/index.d.cts CHANGED
@@ -341,7 +341,12 @@ type LightSensorsActlumus2Data = LightSensorsRawChannels & {
341
341
  };
342
342
  /**
343
343
  * Real-time light-sensor stream. Shape:
344
- * `{ serialNo, type, data }`.
344
+ * `{ serialNo, deviceId?, type, data }`.
345
+ *
346
+ * `deviceId` is the server id of the device and is **optional**: the app only
347
+ * sends it when more than one device on the account shares that `serialNo` (an
348
+ * ActLumus and an ActLumus Plus can). With a unique `serialNo` it is absent, so
349
+ * keep matching on `serialNo` in that case. Present from `nativeVersion` 0.17.0.
345
350
  *
346
351
  * A union discriminated by `type`, which is the `DeviceType.name` on the Dart
347
352
  * side. `data` carries **both the derived spectrum and the raw channels it came
@@ -353,27 +358,35 @@ type LightSensorsActlumus2Data = LightSensorsRawChannels & {
353
358
  */
354
359
  type DeviceLightSensorsPayload = {
355
360
  serialNo: number;
361
+ deviceId?: number;
356
362
  type: 'actlumus';
357
363
  data: LightSensorsActlumus1Data;
358
364
  } | {
359
365
  serialNo: number;
366
+ deviceId?: number;
360
367
  type: 'actlumus2';
361
368
  data: LightSensorsActlumus2Data;
362
369
  };
363
- /** Ack de comando `syncTime`. */
370
+ /**
371
+ * Ack de comando `syncTime`. `deviceId` echoes the one you sent (see
372
+ * `DeviceLightSensorsPayload`); `serialNo` is always present.
373
+ */
364
374
  interface SyncTimeAckPayload {
365
375
  serialNo: number;
376
+ deviceId?: number;
366
377
  success: boolean;
367
378
  }
368
- /** Ack/state do toggle `realTime`. */
379
+ /** Ack/state do toggle `realTime`. `deviceId` as in `SyncTimeAckPayload`. */
369
380
  interface RealTimeAckPayload {
370
381
  serialNo: number;
382
+ deviceId?: number;
371
383
  success: boolean;
372
384
  value: boolean;
373
385
  }
374
- /** Ack de comando `deleteMemory`. */
386
+ /** Ack de comando `deleteMemory`. `deviceId` as in `SyncTimeAckPayload`. */
375
387
  interface DeleteMemoryAckPayload {
376
388
  serialNo: number;
389
+ deviceId?: number;
377
390
  success: boolean;
378
391
  }
379
392
  /**
@@ -382,6 +395,8 @@ interface DeleteMemoryAckPayload {
382
395
  */
383
396
  interface DeviceConnectionPayload {
384
397
  serialNo: number;
398
+ /** Only when the `serialNo` is shared by more than one device. */
399
+ deviceId?: number;
385
400
  isConnect: boolean;
386
401
  }
387
402
  /**
@@ -391,6 +406,8 @@ interface DeviceConnectionPayload {
391
406
  */
392
407
  interface DeviceNotificationPayload {
393
408
  serialNo: number;
409
+ /** Only when the `serialNo` is shared by more than one device. */
410
+ deviceId?: number;
394
411
  isConnect: boolean;
395
412
  message: string | null;
396
413
  messageCode: string | null;
@@ -461,7 +478,9 @@ interface SensorLogPayload {
461
478
  *
462
479
  * Emitted on attach (before your bundle exists), again right after
463
480
  * `handshake-ack`, and on every change. `deviceSyncError` maps `serialNo` to an
464
- * error message the app has already localized.
481
+ * error message the app has already localized. A `serialNo` shared by two
482
+ * devices has no single owner, so it is **left out** of `deviceSyncError` and
483
+ * only appears in `deviceSyncErrorById`.
465
484
  */
466
485
  interface SystemStatusPayload {
467
486
  bluetooth: boolean;
@@ -485,6 +504,11 @@ interface SystemStatusPayload {
485
504
  */
486
505
  missingEssentialPermissions: string[];
487
506
  deviceSyncError: Record<number, string>;
507
+ /**
508
+ * Same errors keyed by the server `deviceId`, for **every** device, shared
509
+ * `serialNo` included. Absent below `nativeVersion` 0.17.0, so guard it.
510
+ */
511
+ deviceSyncErrorById?: Record<number, string>;
488
512
  }
489
513
  /**
490
514
  * Arguments of the `requestPermission` command (JS → native).
package/dist/index.d.ts CHANGED
@@ -341,7 +341,12 @@ type LightSensorsActlumus2Data = LightSensorsRawChannels & {
341
341
  };
342
342
  /**
343
343
  * Real-time light-sensor stream. Shape:
344
- * `{ serialNo, type, data }`.
344
+ * `{ serialNo, deviceId?, type, data }`.
345
+ *
346
+ * `deviceId` is the server id of the device and is **optional**: the app only
347
+ * sends it when more than one device on the account shares that `serialNo` (an
348
+ * ActLumus and an ActLumus Plus can). With a unique `serialNo` it is absent, so
349
+ * keep matching on `serialNo` in that case. Present from `nativeVersion` 0.17.0.
345
350
  *
346
351
  * A union discriminated by `type`, which is the `DeviceType.name` on the Dart
347
352
  * side. `data` carries **both the derived spectrum and the raw channels it came
@@ -353,27 +358,35 @@ type LightSensorsActlumus2Data = LightSensorsRawChannels & {
353
358
  */
354
359
  type DeviceLightSensorsPayload = {
355
360
  serialNo: number;
361
+ deviceId?: number;
356
362
  type: 'actlumus';
357
363
  data: LightSensorsActlumus1Data;
358
364
  } | {
359
365
  serialNo: number;
366
+ deviceId?: number;
360
367
  type: 'actlumus2';
361
368
  data: LightSensorsActlumus2Data;
362
369
  };
363
- /** Ack de comando `syncTime`. */
370
+ /**
371
+ * Ack de comando `syncTime`. `deviceId` echoes the one you sent (see
372
+ * `DeviceLightSensorsPayload`); `serialNo` is always present.
373
+ */
364
374
  interface SyncTimeAckPayload {
365
375
  serialNo: number;
376
+ deviceId?: number;
366
377
  success: boolean;
367
378
  }
368
- /** Ack/state do toggle `realTime`. */
379
+ /** Ack/state do toggle `realTime`. `deviceId` as in `SyncTimeAckPayload`. */
369
380
  interface RealTimeAckPayload {
370
381
  serialNo: number;
382
+ deviceId?: number;
371
383
  success: boolean;
372
384
  value: boolean;
373
385
  }
374
- /** Ack de comando `deleteMemory`. */
386
+ /** Ack de comando `deleteMemory`. `deviceId` as in `SyncTimeAckPayload`. */
375
387
  interface DeleteMemoryAckPayload {
376
388
  serialNo: number;
389
+ deviceId?: number;
377
390
  success: boolean;
378
391
  }
379
392
  /**
@@ -382,6 +395,8 @@ interface DeleteMemoryAckPayload {
382
395
  */
383
396
  interface DeviceConnectionPayload {
384
397
  serialNo: number;
398
+ /** Only when the `serialNo` is shared by more than one device. */
399
+ deviceId?: number;
385
400
  isConnect: boolean;
386
401
  }
387
402
  /**
@@ -391,6 +406,8 @@ interface DeviceConnectionPayload {
391
406
  */
392
407
  interface DeviceNotificationPayload {
393
408
  serialNo: number;
409
+ /** Only when the `serialNo` is shared by more than one device. */
410
+ deviceId?: number;
394
411
  isConnect: boolean;
395
412
  message: string | null;
396
413
  messageCode: string | null;
@@ -461,7 +478,9 @@ interface SensorLogPayload {
461
478
  *
462
479
  * Emitted on attach (before your bundle exists), again right after
463
480
  * `handshake-ack`, and on every change. `deviceSyncError` maps `serialNo` to an
464
- * error message the app has already localized.
481
+ * error message the app has already localized. A `serialNo` shared by two
482
+ * devices has no single owner, so it is **left out** of `deviceSyncError` and
483
+ * only appears in `deviceSyncErrorById`.
465
484
  */
466
485
  interface SystemStatusPayload {
467
486
  bluetooth: boolean;
@@ -485,6 +504,11 @@ interface SystemStatusPayload {
485
504
  */
486
505
  missingEssentialPermissions: string[];
487
506
  deviceSyncError: Record<number, string>;
507
+ /**
508
+ * Same errors keyed by the server `deviceId`, for **every** device, shared
509
+ * `serialNo` included. Absent below `nativeVersion` 0.17.0, so guard it.
510
+ */
511
+ deviceSyncErrorById?: Record<number, string>;
488
512
  }
489
513
  /**
490
514
  * Arguments of the `requestPermission` command (JS → native).
@@ -130,6 +130,12 @@ BridgeClient.send('realTime', { serialNo, value: true });
130
130
 
131
131
  `serialNo` is a **number**. A string is dropped in silence.
132
132
 
133
+ Two devices can share a `serialNo` (an ActLumus and an ActLumus Plus). To act on
134
+ one of them, send its `deviceId` **together with** `serialNo`, taken from the
135
+ `userDevice` list: `BridgeClient.send('realTime', { serialNo, deviceId, value })`.
136
+ A command with only the shared `serialNo` is ignored on purpose. Gate this on
137
+ `nativeVersion` ≥ 0.17.0; details in `docs/bridge-contract.md`.
138
+
133
139
  In a raw channel, `null` means the channel does not exist on that hardware
134
140
  generation. It does not mean zero — `0.0` is a valid reading. Coercing `null`
135
141
  to `0` invents data, and has previously led to a confident, wrong conclusion
@@ -191,7 +197,8 @@ necessary, not sufficient.
191
197
  (`syncTime`, `deleteMemory`, `logout`) may never ack, because the person
192
198
  can decline.
193
199
  - [ ] No `localStorage`, no `fetch`, no `eval`. Not even behind a `try`.
194
- - [ ] `serialNo` is a number everywhere.
200
+ - [ ] `serialNo` is a number everywhere, and a command for a device whose `serialNo`
201
+ repeats on the account also carries its `deviceId` (`nativeVersion` ≥ 0.17.0).
195
202
  - [ ] Errors are reported through `BridgeClient.send('jsError', …)`, the only
196
203
  channel out.
197
204
  - [ ] Tested in the browser mock with `nativeVersion === null`.
@@ -29,10 +29,10 @@ Four numbers coexist. Only one tells you what the device can do.
29
29
 
30
30
  | Number | Current | What it means |
31
31
  | --- | --- | --- |
32
- | `nativeVersion` | `0.16.0` | **The capability signal.** The bridge schema the app on this device implements. |
32
+ | `nativeVersion` | `0.17.0` | **The capability signal.** The bridge schema the app on this device implements. |
33
33
  | `shellVersion` | `0.6.0` | The Condor shell wrapping your iframe. |
34
34
  | client protocol | `0.5.0` | The postMessage protocol `BridgeClient` speaks. |
35
- | package | `0.19.0` | The npm version you compiled against. |
35
+ | package | `0.20.0` | The npm version you compiled against. |
36
36
 
37
37
  ```ts
38
38
  const ack = await BridgeClient.handshake({ clientVersion: '1.0.0' });
@@ -58,6 +58,7 @@ the one you see most.
58
58
  | `language` key and the `language` pull command | `nativeVersion` ≥ 0.14.0 |
59
59
  | `sensorLog` accepts `startTime` / `endTime` | `nativeVersion` ≥ 0.15.0 |
60
60
  | `deviceLightSensors` carries raw `light_sensor_*` channels, and the `actlumus2` variant | `nativeVersion` ≥ 0.16.0 |
61
+ | Address one device by `deviceId` when two share a `serialNo` (`realTime`, `syncTime`, `deleteMemory`, `deviceConnection`); `deviceId` in the device events; `systemStatus.deviceSyncErrorById` | `nativeVersion` ≥ 0.17.0 |
61
62
  | `nativeVersion` reaches the client at all | `shellVersion` ≥ 0.6.0 |
62
63
 
63
64
  `CHANGELOG.md` is the precise record of when each of these landed.
@@ -115,7 +116,8 @@ conclusions before.
115
116
  bluetooth: boolean; internet: boolean; permission: boolean; hasDevices: boolean;
116
117
  missingEssentialPermissions: string[]; // e.g. ["bluetoothScan", "bluetoothConnect"]
117
118
  missingOptionalPermissions: string[]; // e.g. ["notification", "backgroundLocation"]
118
- deviceSyncError: Record<number, string>; // serialNo → message, already localized
119
+ deviceSyncError: Record<number, string>; // serialNo → message, already localized; a shared serialNo is left out
120
+ deviceSyncErrorById?: Record<number, string>; // deviceId → message, every device (nativeVersion ≥ 0.17.0)
119
121
  }
120
122
  ```
121
123
 
@@ -133,8 +135,8 @@ column.
133
135
  | --- | --- | --- | --- |
134
136
  | `ping` | — | no | `pingPong` |
135
137
  | `userDevice` | `{ getFromServer?: boolean }` | no | `userDevice` |
136
- | `realTime` | `{ serialNo: number, value: boolean }` | no | `realTime` |
137
- | `deviceConnection` | `{ type: 'add' \| 'remove', serialNo: number }` | no | `deviceConnection` |
138
+ | `realTime` | `{ serialNo: number, deviceId?: number, value: boolean }` | no | `realTime` |
139
+ | `deviceConnection` | `{ type: 'add' \| 'remove', serialNo: number, deviceId?: number }` | no | `deviceConnection` |
138
140
  | `sensorLog` | see below | no | `sensorLog` |
139
141
  | `openSettings` | — | no | — |
140
142
  | `requestPermission` | `{ permission: string }` | no | — (re-emits `systemStatus`) |
@@ -142,14 +144,40 @@ column.
142
144
  | `navigationState` | `{ canGoBack: boolean }` | no | — |
143
145
  | `language` | — | no | `language` |
144
146
  | `jsError` | `{ message, stack?, source?, lineno?, colno? }` | no | — |
145
- | `syncTime` | `{ serialNo: number }` | **yes** | `syncTime` |
146
- | `deleteMemory` | `{ serialNo: number }` | **yes** | `deleteMemory` |
147
+ | `syncTime` | `{ serialNo: number, deviceId?: number }` | **yes** | `syncTime` |
148
+ | `deleteMemory` | `{ serialNo: number, deviceId?: number }` | **yes** | `deleteMemory` |
147
149
  | `logout` | — | **yes** | — |
148
150
  | `dfu` | — | denied | — |
149
151
 
150
152
  `serialNo` is an integer. Passing a string makes the native side drop the
151
153
  message in silence.
152
154
 
155
+ ### Two devices with the same `serialNo`
156
+
157
+ An ActLumus and an ActLumus Plus have separate serial sequences, so one account
158
+ can hold both with the **same** `serialNo` (the server `deviceId` is always
159
+ unique). A command that carries only `serialNo` cannot say which one you mean:
160
+ the app **ignores it** instead of guessing, because guessing wrong would act on
161
+ the wrong device (erasing the wrong memory, for instance).
162
+
163
+ - **Send `deviceId` together with `serialNo`**, taken from the `userDevice`
164
+ list (`devicesConfig[].deviceId`). Always keep `serialNo` in the message: an
165
+ app older than 0.17.0 drops in silence a command that lacks it.
166
+ - **Read `deviceId` when it is there.** `deviceConnection`,
167
+ `deviceNotification` and `deviceLightSensors` carry it **only when the
168
+ `serialNo` is shared**; with a unique `serialNo` it is absent and `serialNo`
169
+ alone identifies the device. Never treat a missing `deviceId` as "unknown
170
+ device".
171
+ - **Acks always carry `serialNo`**, plus the `deviceId` you sent.
172
+ - **`systemStatus.deviceSyncError` leaves a shared `serialNo` out**, because no
173
+ single device owns it. Read `deviceSyncErrorById` for every device.
174
+ - **Gate on `nativeVersion` ≥ 0.17.0** before relying on any of this, and
175
+ degrade to `serialNo` only below it (where a shared `serialNo` was already
176
+ unaddressable).
177
+ - `sensorLog` takes no `deviceId` yet: for a shared `serialNo` the backend
178
+ answers with the error code `DEVICE_SERIAL_AMBIGUOUS`. Show your own copy for
179
+ it; do not retry.
180
+
153
181
  ### Commands that need human confirmation
154
182
 
155
183
  `syncTime`, `deleteMemory` and `logout` open a modal owned by the Condor shell.
@@ -30,6 +30,10 @@ in the field can be several versions behind your development device.
30
30
  payload natively and drop it on mismatch. `serialNo` is an **integer**, not a
31
31
  string; `value` and `canGoBack` are **booleans**, not strings.
32
32
 
33
+ **Also check:** if the account holds an ActLumus and an ActLumus Plus with the same
34
+ `serialNo`, a command without `deviceId` is ignored (never guessed). Send both
35
+ (`nativeVersion` ≥ 0.17.0).
36
+
33
37
  **Check:** `typeof` at the call site. `serialNo: "1234"` fails; `serialNo: 1234`
34
38
  works.
35
39
 
package/llms.txt CHANGED
@@ -7,7 +7,7 @@
7
7
  > pagination, permissions, system status and language.
8
8
 
9
9
  Every URL below is plain text over HTTPS, pinned to a released version, and
10
- needs no authentication. Replace `0.19.2` with the version you depend on; the
10
+ needs no authentication. Replace `0.20.0` with the version you depend on; the
11
11
  contract changes between minors while the package is below 1.0.0.
12
12
 
13
13
  The same files ship inside the npm tarball, so if you have run
@@ -16,27 +16,27 @@ The same files ship inside the npm tarball, so if you have run
16
16
 
17
17
  ## Start here
18
18
 
19
- - [AGENTS.md](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.19.2/AGENTS.md): The contract in one screen — hard rules, what is unreachable, and the validation loop. Read before writing code.
20
- - [Agent guide](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.19.2/docs/agent-guide.md): Step-by-step construction of a client Home, with the checks to run at each stage.
19
+ - [AGENTS.md](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.20.0/AGENTS.md): The contract in one screen — hard rules, what is unreachable, and the validation loop. Read before writing code.
20
+ - [Agent guide](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.20.0/docs/agent-guide.md): Step-by-step construction of a client Home, with the checks to run at each stage.
21
21
 
22
22
  ## Reference
23
23
 
24
- - [Bridge contract](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.19.2/docs/bridge-contract.md): Every observable key, every command, the three error shapes, and the capability matrix by native version.
25
- - [Machine-readable contract](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.19.2/contract/bridge-contract.json): The same contract as JSON. Also importable as `@condorinst/mobile-bridge/contract`.
26
- - [Limits](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.19.2/docs/limits.md): Every hard limit — payload size, rate limits, bundle size, timeouts — with what happens when you cross it.
27
- - [Troubleshooting](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.19.2/docs/troubleshooting.md): Why a command did nothing. Silence is the platform's dominant failure mode.
28
- - [TypeScript declarations](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.19.2/dist/index.d.ts): Authoritative payload shapes, with documentation on every member.
24
+ - [Bridge contract](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.20.0/docs/bridge-contract.md): Every observable key, every command, the three error shapes, and the capability matrix by native version.
25
+ - [Machine-readable contract](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.20.0/contract/bridge-contract.json): The same contract as JSON. Also importable as `@condorinst/mobile-bridge/contract`.
26
+ - [Limits](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.20.0/docs/limits.md): Every hard limit — payload size, rate limits, bundle size, timeouts — with what happens when you cross it.
27
+ - [Troubleshooting](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.20.0/docs/troubleshooting.md): Why a command did nothing. Silence is the platform's dominant failure mode.
28
+ - [TypeScript declarations](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.20.0/dist/index.d.ts): Authoritative payload shapes, with documentation on every member.
29
29
 
30
30
  ## Guides
31
31
 
32
- - [Client guide](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.19.2/docs/client-guide.md): Narrative walkthrough for a developer building the Home by hand.
33
- - [Pilot onboarding](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.19.2/docs/pilot-onboarding.md): Checklist from zero to a bundle delivered to Condor.
34
- - [README](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.19.2/README.md): Package overview, install, quickstart.
35
- - [CHANGELOG](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.19.2/CHANGELOG.md): When each capability was introduced. The most precise record of what a given native version supports.
32
+ - [Client guide](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.20.0/docs/client-guide.md): Narrative walkthrough for a developer building the Home by hand.
33
+ - [Pilot onboarding](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.20.0/docs/pilot-onboarding.md): Checklist from zero to a bundle delivered to Condor.
34
+ - [README](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.20.0/README.md): Package overview, install, quickstart.
35
+ - [CHANGELOG](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.20.0/CHANGELOG.md): When each capability was introduced. The most precise record of what a given native version supports.
36
36
 
37
37
  ## Examples
38
38
 
39
- - [Minimal home](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.19.2/examples/minimal-home/src/main.ts): A complete, working client in ~160 lines. Copy this as your starting point.
39
+ - [Minimal home](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.20.0/examples/minimal-home/src/main.ts): A complete, working client in ~160 lines. Copy this as your starting point.
40
40
 
41
41
  ## Optional
42
42
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@condorinst/mobile-bridge",
3
- "version": "0.19.2",
3
+ "version": "0.20.0",
4
4
  "description": "TypeScript SDK for the JS↔Native bridge of the Condor white-label mobile app.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "private": false,