@condorinst/mobile-bridge 0.19.2 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -6,19 +6,18 @@ owns login, settings and the Bluetooth connection to the devices; you own what
6
6
  the person sees on the Home.
7
7
 
8
8
  This file is the contract. Read it before writing code. Everything here is
9
- enforced — either by a gate in the app, or by the browser runtime on a real
9
+ enforced, either by a gate in the app or by the browser runtime on a real
10
10
  device, where mistakes are expensive to diagnose.
11
11
 
12
12
  ## The one thing that makes this different
13
13
 
14
- **Failures are silent.** If you send a malformed command, an oversized payload,
15
- or a command this device's firmware does not know, nothing happens. No
16
- exception, no rejected promise, no console error. The native side logs it and
17
- moves on.
14
+ **Failures are silent.** A malformed command, an oversized payload, a command
15
+ sent before the app is ready, a request for an account with no device: nothing
16
+ happens. No exception, no rejected promise, no console error.
18
17
 
19
- So: never write code whose correctness you infer from the absence of an error.
20
- Drive your UI from the events that arrive, and put a timeout on anything you
21
- wait for.
18
+ So never write code whose correctness you infer from the absence of an error.
19
+ Drive the UI from the events that arrive, give every wait a deadline, and
20
+ always have a screen for "it did not come".
22
21
 
23
22
  ## Hard rules
24
23
 
@@ -35,12 +34,14 @@ wait for.
35
34
  whole bundle**.
36
35
  5. **Build single-chunk.** No code splitting, no ES module imports between
37
36
  chunks — `srcdoc` cannot resolve them. Vite: `vite-plugin-singlefile`.
38
- 6. **The `.zip` must be at most 2 MB.** Remember that `data:` URIs inflate
39
- assets by about a third.
37
+ 6. **The `.zip` must be at most 2 MB.** `data:` URIs inflate assets by about a
38
+ third.
40
39
  7. **Use `BridgeClient`, never raw `postMessage`.** Hand-rolled messages fail
41
40
  schema validation and are dropped without a trace.
42
41
  8. **Never assume a capability.** Read `nativeVersion` from the handshake and
43
42
  branch on it. See the capability matrix in `docs/bridge-contract.md`.
43
+ 9. **Never ask once and wait.** Use `BridgeClient.requestUserDevices()` for the
44
+ device list; a single `send('userDevice')` can be lost (see below).
44
45
 
45
46
  ## What you cannot reach
46
47
 
@@ -49,38 +50,92 @@ wait for.
49
50
  camera, microphone and geolocation (denied unconditionally) · the `dfu` key and
50
51
  command (denied by policy).
51
52
 
52
- ## The API
53
+ ## The golden path
53
54
 
54
55
  ```bash
55
- npm install @condorinst/mobile-bridge
56
+ npm install @condorinst/mobile-bridge@^0.21.0
56
57
  ```
57
58
 
58
59
  ```ts
59
- import { BridgeClient, BRIDGE_KEYS, type UserDevicePayload } from '@condorinst/mobile-bridge';
60
+ import {
61
+ BridgeClient, BRIDGE_KEYS, versionAtLeast, type UserDevicePayload,
62
+ } from '@condorinst/mobile-bridge';
60
63
 
61
- // 1. Handshake first. It is how you learn what this device can do.
64
+ // 1. Subscribe before you ask for anything. Roster changes arrive here too.
65
+ BridgeClient.on(BRIDGE_KEYS.userDevice, (devices: UserDevicePayload) => renderDevices(devices));
66
+ BridgeClient.on(BRIDGE_KEYS.systemStatus, (s) => renderEnvironment(s)); // bluetooth, internet, permissions
67
+
68
+ // 2. Handshake. nativeVersion is the ONLY capability signal; ack.accepted is
69
+ // always true and carries no information — never branch on it.
62
70
  const ack = await BridgeClient.handshake({ clientVersion: '1.0.0' });
63
- // ack.nativeVersion is the ONLY capability signal. ack.accepted is always
64
- // true and carries no information — never branch on it.
71
+ const canAddressById = versionAtLeast(ack.nativeVersion, '0.17.0'); // null → false
72
+
73
+ // 3. Ask for the devices with the helper. It resends until they arrive.
74
+ const result = await BridgeClient.requestUserDevices();
75
+ if (result.status === 'timeout') {
76
+ // No reply in 30 s. hasDevices === false suggests an account with no device;
77
+ // otherwise the app could not load the list. Either way: show an empty state
78
+ // with a "Try again" button that calls requestUserDevices({ getFromServer: true }).
79
+ renderEmpty(result.hasDevices === false);
80
+ } else {
81
+ // 4. Subscribe to the connection state of each device (`add` pairs nothing).
82
+ for (const d of result.devices.devicesConfig) {
83
+ BridgeClient.send('deviceConnection', {
84
+ type: 'add', serialNo: d.serialNo, ...(canAddressById ? { deviceId: d.deviceId } : {}),
85
+ });
86
+ }
87
+ }
88
+
89
+ // 5. History: one page per call; it resolves { success: false, error } on
90
+ // failure and only rejects on timeout.
91
+ const page = await BridgeClient.fetchSensorLog({ requestId: crypto.randomUUID(), serialNo });
92
+ ```
65
93
 
66
- // 2. Subscribe before you ask for anything.
67
- BridgeClient.on(BRIDGE_KEYS.userDevice, (payload: UserDevicePayload) => render(payload));
68
- BridgeClient.on(BRIDGE_KEYS.systemStatus, (s) => renderEnvironment(s));
94
+ **Why the helper.** Device data comes from a background service that the app
95
+ starts *after* your bundle is already running. Anything you send to it before it
96
+ is up is lost without a reply — while the handshake, `systemStatus` and
97
+ `language` already work, so the bridge *looks* ready. And an account with no
98
+ linked device gets no `userDevice` reply at all.
99
+
100
+ **Identify devices by `deviceId`.** `serialNo` can repeat: an ActLumus and an
101
+ ActLumus Plus may share one. Join `devicesConfig`, `devicesTelemetry` and
102
+ `devicesUserConfig` on `deviceId`; send `serialNo` in every command, plus
103
+ `deviceId` when `nativeVersion` ≥ 0.17.0 (`versionAtLeast` does the comparison).
104
+ The typed shapes (`DeviceConfigItem`, `DeviceTelemetryItem`, …) show every field
105
+ and its unit — `devicesConfig` has no name or battery; those are in the
106
+ telemetry.
107
+
108
+ **Timestamps are device-local.** `log_time` is `'YYYY-MM-DD HH:MM:SS'` in the
109
+ device's wall-clock time. Parse and build it with local `Date` getters, never
110
+ with `toISOString()` or a `'Z'` suffix.
111
+
112
+ Three commands open a confirmation dialog owned by Condor: `syncTime`,
113
+ `deleteMemory` and `logout`. If the person declines, you get nothing — the ack
114
+ never arrives. Do not block your UI on them.
69
115
 
70
- // 3. Commands are fire-and-forget. No return value, no throw.
71
- BridgeClient.send('userDevice', { getFromServer: false });
116
+ ## Develop in the harness, not in a bare browser tab
72
117
 
73
- // 4. The one request/response helper: it correlates the id and times out.
74
- const page = await BridgeClient.fetchSensorLog({ requestId: crypto.randomUUID(), serialNo });
75
- // Resolves with { success: false, error } on failure — it only REJECTS on timeout.
118
+ ```bash
119
+ npx condor-mobile-bridge dev -i dist/index.html --scenario cold-start
76
120
  ```
77
121
 
78
- Three commands open a confirmation modal owned by Condor: `syncTime`,
79
- `deleteMemory` and `logout`. If the person declines, you get nothing — the ack
80
- simply never arrives. Do not block your UI on them.
122
+ It serves your bundle at `http://localhost:5173` inside the **same sandbox,
123
+ shell and policy as the app**, with a simulated native side. Opening
124
+ `dist/index.html` directly does not work: `BridgeClient.handshake()` rejects at
125
+ once outside the shell. Rebuild and reload; the harness re-reads the file.
81
126
 
82
- `systemStatus` and `language` are emitted on attach, again right after the
83
- handshake, and on every change. Render from the latest one you received.
127
+ | Scenario | What it reproduces |
128
+ | --- | --- |
129
+ | `default` | two devices, realistic latency |
130
+ | `cold-start` | the background service takes 4 s; earlier `ping`/`userDevice` are lost |
131
+ | `no-devices` | `userDevice` never answers; `hasDevices: false` |
132
+ | `shared-serial` | an ActLumus and a Plus share serial 7047 |
133
+ | `offline` | no internet; `sensorLog` fails |
134
+
135
+ The harness console (`[condor dev]`) says what was sent (with its arguments),
136
+ answered and dropped. The **◀ OS back** button at the top presses the OS back
137
+ button. `--native-version null` (or `0.15.0`, …) makes the handshake report that
138
+ version, to test your capability branches.
84
139
 
85
140
  ## The validation loop — run this before you call the work done
86
141
 
@@ -91,22 +146,23 @@ npx condor-mobile-bridge validate -i dist/bundle.zip --strict
91
146
 
92
147
  `validate` applies the same gates the app applies on download, plus a lint for
93
148
  the APIs the sandbox blocks at runtime. Under `--strict` it exits non-zero on
94
- warnings too. **Treat a non-zero exit as a build failure.**
95
-
96
- It is a textual check, so it cannot see through minification: a clean run is
97
- necessary, not sufficient. It will not catch a capability you assumed without
98
- checking `nativeVersion`.
149
+ warnings too. **Treat a non-zero exit as a build failure.** It is a textual
150
+ check, so a clean run is necessary, not sufficient.
99
151
 
100
152
  ## Definition of done
101
153
 
102
154
  - [ ] `validate --strict` exits 0.
103
- - [ ] The UI renders correctly before any event arrives — first paint must not
104
- depend on the bridge.
105
- - [ ] Every capability beyond the baseline is guarded by a `nativeVersion` check.
106
- - [ ] Nothing waits forever on an ack that may never come.
107
- - [ ] Tested in the browser with the mock (in the example: `npm run build`, then
108
- open `dist/index.html`), where `nativeVersion` is `null` — your code must
109
- survive that.
155
+ - [ ] The bundle works in **every** harness scenario: it leaves the loading
156
+ screen in `cold-start`, shows an empty state in `no-devices`, keeps two
157
+ cards apart in `shared-serial`, and explains the failure in `offline`.
158
+ - [ ] The UI renders before any event arrives — first paint must not depend on
159
+ the bridge.
160
+ - [ ] Every capability beyond the baseline is guarded by a `nativeVersion` check
161
+ (`versionAtLeast`), and the page still renders with
162
+ `--native-version null`.
163
+ - [ ] Nothing waits forever on a reply that may never come.
164
+
165
+ To put the bundle on a phone, read `docs/device-testing.md`.
110
166
 
111
167
  ## Where to read more
112
168
 
@@ -115,7 +171,9 @@ checking `nativeVersion`.
115
171
  | Every key, command, error shape and capability | `docs/bridge-contract.md` |
116
172
  | The same, as JSON you can parse | `contract/bridge-contract.json` |
117
173
  | Step-by-step build of a Home | `docs/agent-guide.md` |
174
+ | "My command did nothing" / "the device list never came" | `docs/troubleshooting.md` |
175
+ | Getting the bundle onto a device, and seeing its errors | `docs/device-testing.md` |
118
176
  | Every hard limit, with the code that enforces it | `docs/limits.md` |
119
- | "My command did nothing" | `docs/troubleshooting.md` |
120
177
  | Payload shapes, authoritative | `dist/index.d.ts` (your editor shows them) |
121
178
  | A working client you can copy | `examples/minimal-home/` |
179
+ | All of the above in one file | `llms-full.txt` |
package/CHANGELOG.md CHANGED
@@ -7,6 +7,112 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.21.0] — 2026-10-20
11
+
12
+ A client built from the 0.20.0 docs worked in the browser and stalled on the
13
+ first real device: the device list never arrived. This release makes the
14
+ browser behave like the device, gives the boot sequence a helper, and types the
15
+ device payloads.
16
+
17
+ ### Added
18
+
19
+ - **`condor-mobile-bridge dev`** — serves a bundle (`index.html`, `dist/` or
20
+ `.zip`) in a browser harness that reproduces the app: the bundle runs in
21
+ `<iframe sandbox="allow-scripts">` behind the real `BridgeShell`, with the
22
+ app's policy and CSP and a simulated native side. Scenarios: `default`,
23
+ `cold-start` (requests lost while the app's background service starts),
24
+ `no-devices` (`userDevice` never answers), `shared-serial` and `offline`.
25
+ The page logs every command, reply and dropped request.
26
+ - **`@condorinst/mobile-bridge/dev`** — the same harness as a library
27
+ (`createDevHarness`, `createSimulatedNative`, scenarios and fixtures) for a
28
+ host page on your own dev server.
29
+ - **`BridgeClient.requestUserDevices({ getFromServer })`** — requests the device
30
+ list and resends it with backoff (1.5 s, 3 s, 6 s, then every 10 s, and on
31
+ every `systemStatus`) until it arrives. Never rejects: resolves
32
+ `{ status: 'timeout', hasDevices }` after 30 s, with `hasDevices` from the
33
+ latest `systemStatus` received — also one that arrived before the call.
34
+ - **`BridgeClient.waitForService()`** — resends `ping` until the app's
35
+ background service answers.
36
+ - **Typed device payloads**: `DeviceConfigItem`, `DeviceTelemetryItem`,
37
+ `DeviceUserConfigItem`, `DeviceSyncErrorItem` and `SensorLogItem`, with units
38
+ (battery and memory in %, `bleConnectionPeriod` in minutes, epoch ms) and the
39
+ join rule across the three arrays of `userDevice` (`deviceId`).
40
+ - **`docs/device-testing.md`** — how a bundle reaches a phone, development vs
41
+ production, inspecting the WebView, and where errors go.
42
+ - **`llms-full.txt`** — `AGENTS.md`, the docs and the reference example in one
43
+ file, generated on build.
44
+ - Contract: `payloadItems`, `devTools`, `sensorLog` paging rules, and two
45
+ silent-drop paths for `userDevice` (request before the background service is
46
+ up; a list that failed to load, including an account with no device).
47
+ - `validate` fails a bundle that includes the dev harness
48
+ (`dev-harness-bundled`).
49
+ - `dev --native-version <v|null>` sets the `nativeVersion` the harness
50
+ reports; the harness page has an **◀ OS back** button (emits
51
+ `navigationBack` only after the bundle reported `canGoBack: true`), and its
52
+ log shows the arguments of every command.
53
+ - `versionAtLeast(version, min)` — numeric capability check; `null` → `false`.
54
+ - `SensorLogItem` declares the ActLumus Plus channels `light_sensor_x/_y/_z/_fd`
55
+ (`null` on gen1).
56
+
57
+ ### Changed
58
+
59
+ - **`BridgeClient.handshake()` rejects at once outside the shell** (a page
60
+ opened directly in a browser), with a message pointing to the harness. It
61
+ used to wait 5 s and time out.
62
+ - `UserDevicePayload` arrays and `SensorLogPayload.logs` are typed instead of
63
+ `Record<string, unknown>`. Code that cast items to `Record<string, unknown>`
64
+ still compiles.
65
+ - `AGENTS.md` is rewritten around the boot sequence that survives a cold start
66
+ and an account with no device; the docs no longer describe a browser mock for
67
+ `BridgeClient`, which never existed.
68
+ - `examples/minimal-home` uses the helpers and the harness, and reads the device
69
+ name from the telemetry (it read a field `devicesConfig` does not have).
70
+ `examples/client_demo` drops its private mock and reads `log_time` as
71
+ device-local time.
72
+
73
+ ### Documented
74
+
75
+ - `userDevice` is not guaranteed to answer; `systemStatus.hasDevices` is a hint
76
+ (`false` also when the lookup failed); what `getFromServer` does.
77
+ - `deviceConnection` `type: 'add'` subscribes to a device's connection state and
78
+ pairs nothing.
79
+ - `sensorLog` error codes are matched by prefix; transport failures carry free
80
+ text. A full 7-day sweep recipe (keep the range on every page).
81
+ - The back button example keeps navigation in memory instead of `history`.
82
+ - `sensorLog`: without a range the first page is the most recent; with
83
+ `startTime` the oldest of the range. The log period is programmable per
84
+ device (default 60 s).
85
+
86
+ ## [0.20.0] — 2026-10-13
87
+
88
+ ### Added
89
+
90
+ - **Address one device by `deviceId` when two share a `serialNo`.** An ActLumus
91
+ and an ActLumus Plus have separate serial sequences, so one account can hold
92
+ both with the same `serialNo`. `realTime`, `syncTime`, `deleteMemory` and
93
+ `deviceConnection` accept an optional `deviceId` next to `serialNo`; keep
94
+ sending `serialNo` as well, because an older app drops in silence a command
95
+ that lacks it. Requires `nativeVersion` 0.17.0, which the contract now lists
96
+ as the `deviceIdAddressing` capability.
97
+ - **`deviceId` in the device events.** `deviceConnection`, `deviceNotification`
98
+ and `deviceLightSensors` carry an optional `deviceId`, present only when the
99
+ `serialNo` is shared by more than one device. The acks of `realTime`,
100
+ `syncTime` and `deleteMemory` echo the `deviceId` you sent and always keep
101
+ `serialNo`.
102
+ - **`systemStatus.deviceSyncErrorById`**, the same errors keyed by `deviceId`
103
+ for every device. `deviceSyncError` leaves out a `serialNo` that two devices
104
+ share, since no single device owns it.
105
+ - The contract and `docs/bridge-contract.md` document the rule in a new section,
106
+ and list `DEVICE_SERIAL_AMBIGUOUS` among the `sensorLog` error codes: the
107
+ backend answers it for a shared `serialNo`, and `sensorLog` takes no
108
+ `deviceId` yet.
109
+
110
+ ### Notes
111
+
112
+ - Additive and backward-compatible: every new field is optional and older
113
+ clients ignore it. The model of a device is **not** carried by these events;
114
+ read `modelId` from the `userDevice` list.
115
+
10
116
  ## [0.19.2] — 2026-09-23
11
117
 
12
118
  ### Fixed
package/README.md CHANGED
@@ -20,7 +20,8 @@ Over the network, every document is plain text at a pinned, unauthenticated
20
20
  URL. `llms.txt` is the index:
21
21
 
22
22
  ```
23
- https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.19.0/llms.txt
23
+ https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.21.0/llms.txt
24
+ https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.21.0/llms-full.txt # every doc in one file
24
25
  ```
25
26
 
26
27
  The machine-readable contract — keys, commands, limits, capability matrix — is
@@ -43,7 +44,15 @@ const ack = await BridgeClient.handshake({ clientVersion: '1.0.0' });
43
44
  // ack.nativeVersion is your capability signal. ack.accepted is always true.
44
45
 
45
46
  BridgeClient.on(BRIDGE_KEYS.userDevice, (devices) => render(devices));
46
- BridgeClient.send('userDevice', { getFromServer: false });
47
+ const result = await BridgeClient.requestUserDevices(); // resends until the list arrives
48
+ if (result.status === 'timeout') renderEmptyState();
49
+ ```
50
+
51
+ Develop it in the browser with the harness, which runs the bundle under the
52
+ same sandbox and shell as the app:
53
+
54
+ ```bash
55
+ npx condor-mobile-bridge dev -i dist/index.html --scenario cold-start
47
56
  ```
48
57
 
49
58
  Then read the [Client Guide](docs/client-guide.md) — _"Build your White Label
@@ -63,11 +72,15 @@ The published package ships a prebuilt `dist/` (ESM + CJS + type
63
72
  declarations) — **no build toolchain is required on your side**.
64
73
 
65
74
  > **Versioning during the pilot.** The package is on `0.x` (beta). Pin
66
- > `"@condorinst/mobile-bridge": "^0.19.0"`. Under semver, while on `0.x` a
67
- > caret range auto-updates **patches only** (`0.19.x`); moving to `0.20.0` is
75
+ > `"@condorinst/mobile-bridge": "^0.21.0"`. Under semver, while on `0.x` a
76
+ > caret range auto-updates **patches only** (`0.21.x`); moving to `0.22.0` is
68
77
  > an intentional opt-in. `1.0.0` is reserved for production.
69
78
 
70
- ## Quickstart
79
+ ## Quickstart (legacy, shell-side `Bridge`)
80
+
81
+ > **Not for client bundles.** This is the API the Condor shell uses to talk to
82
+ > the native layer. A client bundle cannot reach it from the sandbox — use
83
+ > `BridgeClient` (above, and in the next section).
71
84
 
72
85
  ```typescript
73
86
  import { Bridge, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
@@ -82,8 +95,8 @@ const unsubscribe = Bridge.on(BRIDGE_KEYS.deviceConnection, (payload) => {
82
95
  console.log('Device:', payload);
83
96
  });
84
97
 
85
- // Command: enable real-time
86
- Bridge.send('realTime', { enabled: true });
98
+ // Command: enable real-time for one device
99
+ Bridge.send('realTime', { serialNo: 1234, value: true });
87
100
 
88
101
  // Native hook: open the app's native Settings screen
89
102
  Bridge.openSettings();
@@ -134,8 +147,8 @@ skip it and the app just stays on Home when back is pressed. See
134
147
  dev/mock APIs live only on the shell.
135
148
  - `handshake` resolves with `{ shellVersion, nativeVersion, accepted }`. Since
136
149
  0.17.0 the shell relays `nativeVersion` into the iframe — it is the only
137
- capability signal you get, and it is `null` in mock mode and against shells
138
- older than 0.6.0. `accepted` is always `true`; never branch on it.
150
+ capability signal you get, and it is `null` when the shell could not reach
151
+ the native side and against shells older than 0.6.0. `accepted` is always `true`; never branch on it.
139
152
  - Commands not allowed by the `ShellPolicy` are silently dropped (with a
140
153
  `console.warn`). The external client has no visibility into which commands
141
154
  do or do not exist.
@@ -198,6 +211,34 @@ if (!verify.ok) {
198
211
  | **3** | (Native side) | Shell nonce, stamped on every outgoing message via `Bridge.setOutgoingStamp`, validated natively. **This is the guarantee.** |
199
212
  | **4** | (Fallback if 1+2 fail) | Dual `InAppWebView` — shell and client in separate WebViews. |
200
213
 
214
+ ## Dev harness — `condor-mobile-bridge dev`
215
+
216
+ ```bash
217
+ npx condor-mobile-bridge dev -i dist/index.html [--scenario cold-start] [--port 5173]
218
+ ```
219
+
220
+ Serves the bundle (an `index.html`, a `dist/` folder or a `.zip`) at
221
+ `http://localhost:5173` inside a sandboxed iframe, behind the real
222
+ `BridgeShell` with the app's policy and CSP, and a simulated native side. The
223
+ input is re-read on every reload.
224
+
225
+ Scenarios reproduce device behaviour a naive mock hides: `default`,
226
+ `cold-start` (requests lost while the app's background service starts),
227
+ `no-devices` (`userDevice` never answers), `shared-serial` (two devices, one
228
+ `serialNo`) and `offline`. The page's console (`[condor dev]`) logs what was
229
+ sent, answered and dropped.
230
+
231
+ With your own dev server, mount the same harness from the **host** page:
232
+
233
+ ```ts
234
+ import { createDevHarness } from '@condorinst/mobile-bridge/dev';
235
+ const html = await (await fetch('/bundle/index.html')).text();
236
+ await createDevHarness({ html, scenario: 'no-devices', mount: document.getElementById('device')! });
237
+ ```
238
+
239
+ Never import `@condorinst/mobile-bridge/dev` into the bundle itself: `validate`
240
+ fails it with `dev-harness-bundled`.
241
+
201
242
  ## Build CLI — `condor-mobile-bridge build`
202
243
 
203
244
  Generates the **self-contained `.zip` bundle** that the white-label app
@@ -280,10 +321,10 @@ The client passes `Record<string, unknown>`.
280
321
 
281
322
  ## Versioning policy (semver)
282
323
 
283
- - **PATCH** (`0.8.x`): bugfix with no API change.
324
+ - **PATCH** (`0.21.x`): bugfix with no API change.
284
325
  - **MINOR** (`0.x.0`): new observable key, new command, backward-compatible API.
285
326
  > While on `0.x`, a minor bump may carry breaking changes (semver treats
286
- > pre-1.0 minors as breaking). Pin `^0.8.0` to stay on safe patches.
327
+ > pre-1.0 minors as breaking). Pin `^0.21.0` to stay on safe patches.
287
328
  - **MAJOR** (`x.0.0`): incompatible schema change (key rename, payload shape
288
329
  change, public API change). `1.0.0` marks the production-stable contract.
289
330
 
@@ -301,9 +342,15 @@ The package detects automatically:
301
342
  JSON string. Takes precedence when present.
302
343
  - **Android / iOS (legacy)** — `window.flutter_inappwebview` (via the `flutter_inappwebview`
303
344
  package). Still supported for the transition; will be removed in a future minor.
304
- - **Browser outside the app** (dev/test): **mock mode** activates automatically.
345
+ - **Browser outside the app** (dev/test): **mock mode** activates automatically
346
+ for `Bridge`. A client bundle (`BridgeClient`) runs in the browser only
347
+ through the `dev` harness.
305
348
 
306
- ## Mock mode
349
+ ## Mock mode (shell-side `Bridge`)
350
+
351
+ > For code that uses the legacy `Bridge` directly — the shell, tests,
352
+ > Storybook. A client bundle uses `BridgeClient`, which has no mock of its own:
353
+ > develop it with `condor-mobile-bridge dev` (above).
307
354
 
308
355
  When the package runs outside the Flutter WebView (regular browser, Vite dev
309
356
  server, Storybook), the adapter activates mock mode automatically. You can
@@ -361,7 +408,10 @@ console.log(state.subscriberKeys);
361
408
  // → ['deviceConnection']
362
409
  ```
363
410
 
364
- ## Dev panel
411
+ ## Dev panel (shell-side `Bridge`)
412
+
413
+ > The `dev` harness installs this panel for you. Call `installDevPanel` yourself
414
+ > only when hosting `Bridge` directly.
365
415
 
366
416
  An opt-in HTML panel to inspect the bridge in the browser during development.
367
417