@condorinst/mobile-bridge 0.21.0 → 0.23.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.
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "contractVersion": "0.21.0",
3
+ "contractVersion": "0.23.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.21.0",
9
+ "package": "0.23.0",
10
10
  "shellVersion": "0.6.0",
11
11
  "clientProtocolVersion": "0.5.0",
12
- "nativeVersion": "0.17.0",
12
+ "nativeVersion": "0.18.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,
16
16
  "acceptedWarning": "The native side never rejects a handshake, so `accepted` carries no information. Never branch on it.",
17
- "nativeVersionMayBeNull": "null when the shell could not reach the native side, and in bundles compiled against SDK <= 0.16.x, where the shell did not relay it. Treat null as 'assume nothing beyond the 0.13.0 baseline'. The dev harness reports 0.17.0 unless a scenario sets it to null."
17
+ "nativeVersionMayBeNull": "null when the shell could not reach the native side, and in bundles compiled against SDK <= 0.16.x, where the shell did not relay it. Treat null as 'assume nothing beyond the 0.13.0 baseline'. The dev harness reports 0.18.0 unless --native-version says otherwise."
18
18
  }
19
19
  },
20
20
 
@@ -81,12 +81,12 @@
81
81
  "note": "13 keys exist; 12 are reachable by the sandboxed client. Subscribe with BridgeClient.on(key, cb). Payload shapes are canonical in dist/index.d.ts under the listed type.",
82
82
  "entries": {
83
83
  "pingPong": { "reachableByClient": true, "payloadType": "PingPongPayload", "emittedWhen": "reply to the `ping` command, once the app's background service is up" },
84
- "userDevice": { "reachableByClient": true, "payloadType": "UserDevicePayload", "emittedWhen": "reply to the `userDevice` command when the device list loads, and on device roster changes. Not guaranteed: see silentDropPaths and prefer BridgeClient.requestUserDevices()." },
85
- "deviceLightSensors": { "reachableByClient": true, "payloadType": "DeviceLightSensorsPayload", "emittedWhen": "real-time stream, only while realTime is on for that device", "discriminator": "type", "variants": ["actlumus", "actlumus2"] },
84
+ "userDevice": { "reachableByClient": true, "payloadType": "UserDevicePayload", "emittedWhen": "reply to the `userDevice` command, and on device roster changes. From nativeVersion 0.18.0 always answered: with empty arrays and `error` when the list could not load. Below it, not guaranteed (see silentDropPaths). Prefer BridgeClient.requestUserDevices()." },
85
+ "deviceLightSensors": { "reachableByClient": true, "payloadType": "DeviceLightSensorsPayload", "emittedWhen": "real-time stream, only while realTime is on for that device", "discriminator": "type", "variants": ["actlumus", "actlumus2"], "units": { "photopic": "lux", "melanopic": "lux (melanopic EDI)", "wavelength*": "relative, no physical unit", "light_sensor_*": "relative, no physical unit" } },
86
86
  "syncTime": { "reachableByClient": true, "payloadType": "SyncTimeAckPayload", "emittedWhen": "ack of the `syncTime` command" },
87
87
  "realTime": { "reachableByClient": true, "payloadType": "RealTimeAckPayload", "emittedWhen": "ack of the `realTime` command" },
88
88
  "deleteMemory": { "reachableByClient": true, "payloadType": "DeleteMemoryAckPayload", "emittedWhen": "ack of the `deleteMemory` command" },
89
- "deviceNotification": { "reachableByClient": true, "payloadType": "DeviceNotificationPayload", "emittedWhen": "device-level notice, including errors" },
89
+ "deviceNotification": { "reachableByClient": true, "payloadType": "DeviceNotificationPayload", "emittedWhen": "each step of a device sync (BLE session), for every linked device; success = messageCode pipeline_done_notification, failure = a *_error without it; message is an English log line, not localized" },
90
90
  "deviceConnection": { "reachableByClient": true, "payloadType": "DeviceConnectionPayload", "emittedWhen": "a device connects or disconnects" },
91
91
  "sensorLog": { "reachableByClient": true, "payloadType": "SensorLogPayload", "emittedWhen": "reply to the `sensorLog` command; correlate by requestId" },
92
92
  "systemStatus": { "reachableByClient": true, "payloadType": "SystemStatusPayload", "emittedWhen": "on attach, again right after handshake-ack, and on every change" },
@@ -100,9 +100,9 @@
100
100
  "note": "Send with BridgeClient.send(command, args). It is fire-and-forget: there is no return value and no throw. Replies arrive on the key named in repliesOn.",
101
101
  "entries": {
102
102
  "ping": { "args": {}, "requiresConfirmation": false, "repliesOn": "pingPong", "note": "Handled by the app's background service: lost without a reply if sent before the service is up. BridgeClient.waitForService() resends it until a pingPong arrives." },
103
- "userDevice": { "args": { "getFromServer": "boolean, default false. false: memory or cache while fresh, else the server (use at boot). true: the server first, cache on failure (use for a refresh button)." }, "requiresConfirmation": false, "repliesOn": "userDevice", "prefer": "BridgeClient.requestUserDevices({ getFromServer }), which resends with backoff and resolves { status: 'timeout', hasDevices } instead of waiting forever.", "note": "Handled by the app's background service. A request sent before it is up is lost, and no reply is sent when the list fails to load — including an account with no linked device. An empty list is never sent." },
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", "note": "Pairs nothing. 'add' subscribes to the BLE connection state of a device from the userDevice list; the state arrives on the deviceConnection key on every change. 'remove' stops it." },
103
+ "userDevice": { "args": { "getFromServer": "boolean, default false. false: memory or cache while fresh, else the server (use at boot). true: the server first, cache on failure (use for a refresh button)." }, "requiresConfirmation": false, "repliesOn": "userDevice", "prefer": "BridgeClient.requestUserDevices({ getFromServer }), which resends with backoff and resolves { status: 'timeout', hasDevices } instead of waiting forever.", "note": "From nativeVersion 0.18.0 the app re-sends a request lost while its background service starts, and answers a failed lookup with empty arrays and `error` (NO_DEVICES | NETWORK | SESSION | UNKNOWN). Below 0.18.0 a request sent before the service is up is lost, and a failed lookup gets no reply." },
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 and the device is in a BLE session; while it is on, the app keeps the session open after the sync (it ends when realTime goes off or the link drops); the stream stops when the session ends and resumes at the next one. The app switches it off for every device when it restarts. The ack means the wish is recorded; it does not require a connected device. 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", "note": "Pairs nothing. 'add' subscribes to the BLE connection state of a device from the userDevice list; the state arrives on the deviceConnection key on every CHANGE only — the current state is not replayed (except isConnect false at once for a device the app does not know). Start every device as 'unknown'. An ActLumus is between sessions most of the time (bleConnectionPeriod, minutes). 'remove' stops it." },
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.", "paging": { "orderWithinPage": "chronological ascending", "noCursorNoRange": "the most recent `limit` logs; older pages with endingBefore while hasPrevious", "withStartTime": "the oldest logs of the range first; newer pages with startingAfter while hasMore", "onlyEndTime": "the most recent logs before endTime", "defaultLimit": 10, "maxLimit": 1000, "logPeriod": "one log per period, programmable per device, default 60 s; derive it from consecutive log_time values" } },
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,9 +110,9 @@
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", "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
- "logout": { "args": {}, "requiresConfirmation": true, "repliesOn": null },
113
+ "syncTime": { "args": { "serialNo": "int", "deviceId": "int, optional, minNativeVersion 0.17.0" }, "requiresConfirmation": true, "repliesOn": "syncTime", "ackMeans": "recorded; the clock is set at the device's next BLE session. Does not require a connected device." },
114
+ "deleteMemory": { "args": { "serialNo": "int", "deviceId": "int, optional, minNativeVersion 0.17.0" }, "requiresConfirmation": true, "repliesOn": "deleteMemory", "ackMeans": "recorded; the memory is erased at the end of the next sync, only after its data was uploaded (a failed upload keeps it pending). Unsent data is never erased. Not reversible once done. Does not require a connected device." },
115
+ "logout": { "args": {}, "requiresConfirmation": true, "repliesOn": null, "note": "No reply: once confirmed, the app goes to its login screen and unloads the page." },
116
116
  "dfu": { "args": {}, "denied": true, "repliesOn": null }
117
117
  },
118
118
  "confirmationSemantics": "A command with requiresConfirmation true opens a modal owned by the Condor shell. If the person declines, the shell answers nack with reason user-rejected, which your code only sees as a console warning. The ack simply never arrives. Never block your UI waiting for it: drive the screen from the ack key when it comes, and keep a timeout."
@@ -149,8 +149,8 @@
149
149
  { "cause": "permission name outside the native enum", "trigger": "requestPermission with an unrecognized string", "observable": "nothing" },
150
150
  { "cause": "malformed args", "trigger": "realTime, syncTime, deleteMemory or deviceConnection with wrong types", "observable": "nothing" },
151
151
  { "cause": "postMessage failing schema validation", "trigger": "hand-rolled postMessage instead of BridgeClient", "observable": "nothing" },
152
- { "cause": "background service not running yet", "trigger": "userDevice or ping sent before the app's background service is up, typically right after a cold start; the app does not re-send the list when the service comes up", "observable": "nothing (the handshake, systemStatus and language already work)", "mitigation": "BridgeClient.requestUserDevices() / waitForService()" },
153
- { "cause": "device list did not load", "trigger": "userDevice for an account with no linked device, or when the server request fails (no internet, session problem); the app replies only on success", "observable": "nothing; systemStatus.hasDevices stays false", "mitigation": "BridgeClient.requestUserDevices() resolves { status: 'timeout', hasDevices } after 30 s: render an empty state with a retry" }
152
+ { "cause": "background service not running yet", "untilNativeVersion": "0.18.0", "trigger": "userDevice or ping sent before the app's background service is up, typically right after a cold start; the app does not re-send the list when the service comes up", "observable": "nothing (the handshake, systemStatus and language already work)", "mitigation": "BridgeClient.requestUserDevices() / waitForService()" },
153
+ { "cause": "device list did not load", "untilNativeVersion": "0.18.0", "trigger": "userDevice for an account with no linked device, or when the server request fails (no internet, session problem); the app replies only on success", "observable": "nothing; systemStatus.hasDevices stays false", "mitigation": "BridgeClient.requestUserDevices() resolves { status: 'timeout', hasDevices } after 30 s: render an empty state with a retry" }
154
154
  ],
155
155
 
156
156
  "capabilities": [
@@ -158,6 +158,9 @@
158
158
  { "feature": "sensorLogTimeRange", "minNativeVersion": "0.15.0", "description": "sensorLog accepts startTime and endTime." },
159
159
  { "feature": "lightSensorRawChannels", "minNativeVersion": "0.16.0", "description": "deviceLightSensors carries the raw light_sensor_* channels and the actlumus2 variant." },
160
160
  { "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." },
161
+ { "feature": "userDeviceAlwaysReplies", "minNativeVersion": "0.18.0", "description": "userDevice is always answered: the app re-sends a request lost while its background service starts, and a failed lookup arrives as empty arrays plus error (NO_DEVICES | NETWORK | SESSION | UNKNOWN). requestUserDevices() resolves no-devices / error." },
162
+ { "feature": "hasDevicesKnown", "minNativeVersion": "0.18.0", "description": "systemStatus.hasDevicesKnown tells whether hasDevices is the server's answer (true) or the lookup failed (false)." },
163
+ { "feature": "userDevicePayloadAllowlist", "minNativeVersion": "0.18.0", "description": "The app sends only the fields listed in payloadItems; anything else (patient data included) is stripped before it reaches the bundle." },
161
164
  { "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." }
162
165
  ],
163
166
 
@@ -174,7 +177,8 @@
174
177
  "integrity": { "algorithm": "sha256", "format": "sha256-<lowercase hex>", "computedOver": "the exact bytes of the .zip" },
175
178
  "tooling": {
176
179
  "build": "npx condor-mobile-bridge build -i dist/index.html -o dist/bundle.zip",
177
- "validate": "npx condor-mobile-bridge validate -i dist/bundle.zip --strict"
180
+ "validate": "npx condor-mobile-bridge validate -i dist/bundle.zip --strict",
181
+ "publish": "npx condor-mobile-bridge canvas publish dist/index.html --version <v> — interactive (company id, user id, password, 2FA code); live at once for every user of the company; needs a manager user. Never run it from an agent."
178
182
  },
179
183
  "statusBarHook": "<meta name=\"theme-color\" content=\"#RRGGBB\"> in the bundle sets the native status bar colour. The setStatusBarColor command overrides it at runtime."
180
184
  },
@@ -182,7 +186,8 @@
182
186
  "payloadItems": {
183
187
  "note": "Item shapes of userDevice and sensorLog. Canonical types in dist/index.d.ts. Fields not listed here are not part of the public contract — do not read them.",
184
188
  "joinKey": "deviceId. An item of devicesTelemetry or devicesUserConfig without deviceId is legacy data: fall back to serialNo only when that serialNo appears once in devicesConfig.",
185
- "hasDevices": "systemStatus.hasDevices is true only after the app loaded a non-empty list from the server; false means no device OR the lookup failed. A hint, never a verdict.",
189
+ "hasDevices": "systemStatus.hasDevices is true only after the app loaded a non-empty list from the server. From nativeVersion 0.18.0, systemStatus.hasDevicesKnown says whether it is the server's answer; below it (or when hasDevicesKnown is false), false means no device OR the lookup failed.",
190
+ "error": "UserDevicePayload.error (nativeVersion 0.18.0+): present only when the list could not load, with the three arrays empty. NO_DEVICES | NETWORK | SESSION | UNKNOWN; treat any other value as UNKNOWN.",
186
191
  "devicesConfig": {
187
192
  "type": "DeviceConfigItem",
188
193
  "role": "the list of linked devices to render",
@@ -200,7 +205,7 @@
200
205
  },
201
206
  "sensorLogItem": {
202
207
  "type": "SensorLogItem",
203
- "fields": { "id": "int", "log_time": "'YYYY-MM-DD HH:MM:SS', device-LOCAL time, not UTC", "log_type": "int", "body_part": "'wrist' | 'necklace' | null", "temperature": "°C | null", "lux_photopic": "lux | null", "lux_melanopic": "lux | null", "light_sensor_f1..f8, light_sensor_clear, light_sensor_nir": "raw channels | null (null = the hardware has no such channel)", "act_data_pim, act_data_tat, act_data_zcm": "int | null", "device_log_time, app_log_time": "upload batch markers, not per-log timestamps" }
208
+ "fields": { "id": "int", "log_time": "'YYYY-MM-DD HH:MM:SS', device-LOCAL time, not UTC", "log_type": "int", "body_part": "'wrist' | 'necklace' | null — where the device was SET to be worn when the log was uploaded, not a detection", "off_wrist": "raw wear flag, the name misleads: 1 = worn, 0 = not worn (dock); anything else = not worn; null = unknown", "temperature": "°C | null", "lux_photopic": "lux | null", "lux_melanopic": "lux | null", "light_sensor_f1..f8, light_sensor_clear, light_sensor_nir": "raw channels | null (null = the hardware has no such channel)", "act_data_pim, act_data_tat, act_data_zcm": "int | null", "device_log_time, app_log_time": "upload batch markers, not per-log timestamps" }
204
209
  }
205
210
  },
206
211
 
@@ -214,8 +219,8 @@
214
219
  "verifiedAgainst": {
215
220
  "monorepo": "condorinstprogdev/mobile_applications",
216
221
  "branch": "dev_canvas",
217
- "commit": "a1a524fd8491e4c266c7ea7bbcd9b1aa334620db",
218
- "verifiedOn": "2026-10-20",
222
+ "commit": "7b8ca8a1e736689c7abbb0cee5af63a24f7ebc23",
223
+ "verifiedOn": "2026-10-21",
219
224
  "sources": {
220
225
  "nativeVersion": "apps/canvas/lib/ui/pages/home/bridge/condor_bridge.dart:38",
221
226
  "jsToNativePayloadBytes": "apps/canvas/lib/ui/pages/home/bridge/condor_bridge.dart:94",
@@ -234,13 +239,20 @@
234
239
  "userDeviceToBackgroundService": "apps/canvas/lib/ui/pages/home/bridge/condor_bridge.dart:671-683",
235
240
  "userDeviceRepliesOnlyOnSuccess": "packages/condor_core/lib/src/data/services/user_devices_service.dart:28-39",
236
241
  "getFromServerSemantics": "packages/condor_core/lib/src/data/repositories/user_devices_repository.dart:49-85",
237
- "hasDevices": "apps/canvas/lib/data/services/sys_notification_service.dart:224-237",
242
+ "hasDevices": "apps/canvas/lib/data/services/sys_notification_service.dart:225-238",
238
243
  "deviceConfigItem": "packages/condor_core/lib/src/domain/entities/device_config_entity.dart:61-77",
239
244
  "deviceTelemetryItem": "packages/condor_core/lib/src/domain/entities/device_telemetry_entity.dart:100-117",
240
245
  "deviceUserConfigItem": "packages/condor_core/lib/src/domain/entities/device_user_config_entity.dart:78-88",
241
246
  "deviceSyncErrorItem": "packages/condor_core/lib/src/domain/entities/device_sync_error_entity.dart:16-23",
242
247
  "userDevicesEnvelope": "packages/condor_core/lib/src/domain/entities/user_devices_entity.dart:282-289",
243
- "bleConnectionPeriodMinutes": "packages/condor_core/lib/src/domain/entities/device_session_record_entity.dart:25"
248
+ "bleConnectionPeriodMinutes": "packages/condor_core/lib/src/domain/entities/device_session_record_entity.dart:25",
249
+ "nativeVersion018": "apps/canvas/lib/ui/pages/home/bridge/condor_bridge.dart:45",
250
+ "userDeviceResendOnPong": "apps/canvas/lib/ui/pages/home/bridge/condor_bridge.dart:741-755",
251
+ "userDeviceErrorReply": "apps/canvas/lib/ui/pages/home/bridge/condor_bridge.dart:433-447",
252
+ "userDeviceErrorFromBackground": "apps/canvas/lib/foreground/handler/main_handler.dart:98-116",
253
+ "userDeviceErrorCodes": "apps/canvas/lib/core/user_device_error.dart:41-61",
254
+ "userDevicePayloadAllowlist": "apps/canvas/lib/ui/pages/home/bridge/condor_bridge.dart:965-982",
255
+ "hasDevicesKnown": "apps/canvas/lib/data/services/sys_notification_service.dart:239,252-266"
244
256
  },
245
257
  "maintenance": "Re-verify every entry in sources before cutting a release. A published contract that drifts from the app is worse than no contract."
246
258
  }