@condorinst/mobile-bridge 0.22.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.
package/README.md CHANGED
@@ -1,27 +1,24 @@
1
1
  # @condorinst/mobile-bridge
2
2
 
3
- Canonical TypeScript SDK for the JS↔Native bridge of the Condor white-label
4
- mobile app.
3
+ The SDK for the **Home of the Condor Canvas app**: a self-contained web page
4
+ that the app shows inside a sandboxed iframe after login. The page talks to the
5
+ app — device list, live light readings, history, permissions, language — with
6
+ `BridgeClient`, and the package's CLI builds, checks, tests and publishes it.
5
7
 
6
- > **Purpose:** give the JavaScript code that runs inside the white-label app's
7
- > WebView a typed, runtime-validated, testable API to talk to the native
8
- > Flutter side — versioned handshake, observation of keys emitted by the
9
- > background, commands to the foreground service, and hooks into native
10
- > screens (Settings).
8
+ ## Building a Home — person or AI agent
11
9
 
12
- ## Reading this with an AI agent?
10
+ Start at **[`AGENTS.md`](AGENTS.md)**: the rules, the boot sequence to copy, the
11
+ browser harness, the validation loop, and which document owns each subject. It
12
+ is written once for both audiences.
13
13
 
14
- Start at **[`AGENTS.md`](AGENTS.md)** — the contract in one screen — then
15
- `docs/agent-guide.md`. Both ship inside the npm tarball, so after
16
- `npm install` they are on disk at
17
- `node_modules/@condorinst/mobile-bridge/`, readable with no network access.
18
-
19
- Over the network, every document is plain text at a pinned, unauthenticated
20
- URL. `llms.txt` is the index:
14
+ Everything ships inside the npm tarball, so after `npm install` it is on disk at
15
+ `node_modules/@condorinst/mobile-bridge/`, readable with no network. Over the
16
+ network, every document is plain text at a pinned, unauthenticated URL;
17
+ `llms.txt` is the index and `llms-full.txt` has every document in one file:
21
18
 
22
19
  ```
23
- https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.22.0/llms.txt
24
- https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.22.0/llms-full.txt # every doc in one file
20
+ https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.23.0/llms.txt
21
+ https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.23.0/llms-full.txt
25
22
  ```
26
23
 
27
24
  The machine-readable contract — keys, commands, limits, capability matrix — is
@@ -32,315 +29,30 @@ The machine-readable contract — keys, commands, limits, capability matrix —
32
29
  > application and returns no readable text. Use the jsDelivr URLs above, or
33
30
  > Bitbucket's `raw/` file URLs.
34
31
 
35
- ## New here, as a human?
36
-
37
- You are almost certainly building the **client bundle** that renders as the
38
- app's Home. That means `BridgeClient`:
39
-
40
- ```typescript
41
- import { BridgeClient, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
42
-
43
- const ack = await BridgeClient.handshake({ clientVersion: '1.0.0' });
44
- // ack.nativeVersion is your capability signal. ack.accepted is always true.
45
-
46
- BridgeClient.on(BRIDGE_KEYS.userDevice, (devices) => render(devices));
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
56
- ```
57
-
58
- Then read the [Client Guide](docs/client-guide.md) — _"Build your White Label
59
- Home"_ — and copy the runnable [`examples/minimal-home`](examples/minimal-home).
60
-
61
- The `Bridge` singleton shown under *Quickstart* below is the **legacy,
62
- shell-side** API. It talks to the native layer directly and is not reachable
63
- from a sandboxed client bundle. If you are the client, you want `BridgeClient`.
64
-
65
32
  ## Installation
66
33
 
67
34
  ```bash
68
- npm install @condorinst/mobile-bridge
69
- ```
70
-
71
- The published package ships a prebuilt `dist/` (ESM + CJS + type
72
- declarations) — **no build toolchain is required on your side**.
73
-
74
- > **Versioning during the pilot.** The package is on `0.x` (beta). Pin
75
- > `"@condorinst/mobile-bridge": "^0.22.0"`. Under semver, while on `0.x` a
76
- > caret range auto-updates **patches only** (`0.22.x`); moving to `0.23.0` is
77
- > an intentional opt-in. `1.0.0` is reserved for production.
78
-
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).
84
-
85
- ```typescript
86
- import { Bridge, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
87
-
88
- // Versioned handshake with the native side (can be called at any time;
89
- // recommended on the client's DOMContentLoaded)
90
- const ack = await Bridge.handshake({ clientVersion: '1.0.0' });
91
- console.log('Native bridge:', ack.nativeVersion, 'accepted:', ack.accepted);
92
-
93
- // Subscribe to an observable key coming from the background
94
- const unsubscribe = Bridge.on(BRIDGE_KEYS.deviceConnection, (payload) => {
95
- console.log('Device:', payload);
96
- });
97
-
98
- // Command: enable real-time for one device
99
- Bridge.send('realTime', { serialNo: 1234, value: true });
100
-
101
- // Native hook: open the app's native Settings screen
102
- Bridge.openSettings();
103
-
104
- // Cleanup
105
- unsubscribe();
106
- ```
107
-
108
- > The recommended model for white-label clients is **`BridgeClient`** (sandbox
109
- > iframe), not the legacy `Bridge`. See below.
110
-
111
- ## Sandbox iframe — `BridgeClient` + `BridgeShell`
112
-
113
- The recommended model isolates the external client code inside an
114
- `<iframe sandbox="allow-scripts">`, communicating over `window.postMessage`
115
- with schema-validated messages and a default-deny policy.
116
-
117
- ### For the external client — `BridgeClient`
118
-
119
- API **identical** to the legacy `Bridge`, but over a postMessage transport.
120
- The client imports it and never notices it is sandboxed:
121
-
122
- ```typescript
123
- import { BridgeClient, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
124
-
125
- const ack = await BridgeClient.handshake({ clientVersion: '1.0.0' });
126
- console.log('Shell:', ack.shellVersion, 'accepted:', ack.accepted);
127
-
128
- const unsubscribe = BridgeClient.on(BRIDGE_KEYS.userDevice, (payload) => {
129
- console.log('Devices:', payload);
130
- });
131
-
132
- BridgeClient.send('realTime', { serialNo: 1234, value: true });
133
- BridgeClient.openSettings();
134
-
135
- // System back button: report whether you can go back, and react to the OS back.
136
- BridgeClient.reportNavigation(currentView !== 'home'); // call on every route change
137
- BridgeClient.onBack(() => goToPreviousPage()); // fired only when you reported true
138
- ```
139
-
140
- The `reportNavigation` / `onBack` pair lets your page own the Android back
141
- button — walking your in-page navigation before the app leaves the Home. Opt-in;
142
- skip it and the app just stays on Home when back is pressed. See
143
- [docs/bridge-contract.md](docs/bridge-contract.md).
144
-
145
- **Differences vs `Bridge`:**
146
- - Does not expose `mockEmit`, `installDevPanel`, `isMock`, `getMockState` —
147
- dev/mock APIs live only on the shell.
148
- - `handshake` resolves with `{ shellVersion, nativeVersion, accepted }`. Since
149
- 0.17.0 the shell relays `nativeVersion` into the iframe — it is the only
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.
152
- - Commands not allowed by the `ShellPolicy` are silently dropped (with a
153
- `console.warn`). The external client has no visibility into which commands
154
- do or do not exist.
155
-
156
- ### For the Condor-controlled shell — `BridgeShell`
157
-
158
- ```typescript
159
- import { BridgeShell, BRIDGE_KEYS, type ShellPolicy } from '@condorinst/mobile-bridge';
160
-
161
- const iframe = document.getElementById('client-frame') as HTMLIFrameElement;
162
-
163
- const policy: ShellPolicy = {
164
- readOnlyKeys: [
165
- BRIDGE_KEYS.userDevice,
166
- BRIDGE_KEYS.deviceConnection,
167
- BRIDGE_KEYS.deviceLightSensors,
168
- BRIDGE_KEYS.realTime,
169
- ],
170
- allowedCommands: ['userDevice', 'realTime', 'openSettings'],
171
- requiresConfirmation: ['syncTime', 'deleteMemory'],
172
- deniedCommands: ['dfu'],
173
- };
174
-
175
- const shell = new BridgeShell({
176
- iframe,
177
- policy,
178
- onConfirmationRequest: async ({ command, args }) => {
179
- return await showConfirmationModal(command, args); // shell UI
180
- },
181
- });
182
-
183
- // Layer 1 — bootstrap with an override prefix.
184
- //
185
- // Use bootstrapClientIframe(clientPath) when HTML comes over HTTP:
186
- await shell.bootstrapClientIframe('https://api.condor.example/whitelabel/bundle.html');
187
- //
188
- // Use bootstrapClientIframeWithHtml(htmlString) when the HTML is already in
189
- // memory — required for an asset bundled at file:// (Android WebView cannot
190
- // fetch over file://), a build-time inlined bundle, etc:
191
- declare const __CLIENT_HTML__: string; // e.g. via esbuild.define
192
- await shell.bootstrapClientIframeWithHtml(__CLIENT_HTML__);
193
-
194
- // Layer 2 — iframe attestation.
195
- const verify = await shell.verifySandbox();
196
- if (!verify.ok) {
197
- // Abort the boot — Layer 1 failed.
198
- throw new Error('Sandbox compromised: bridges still reachable from iframe.');
199
- }
200
-
201
- // From here on, BridgeShell routes postMessage ↔ native Bridge
202
- // automatically. Nothing else to do at boot.
203
- ```
204
-
205
- ### Layered defense (propagation of the native channel into the iframe)
206
-
207
- | Layer | Where | What it does |
208
- |---|---|---|
209
- | **1** | `BridgeShell.bootstrapClientIframe` | `srcdoc` with a prefix that deletes `window.CondorNative`, `window.flutter_inappwebview` and `window.CondorBridge` before any client script runs. Defense in depth only: the WebView injects the channel into every frame, so a child iframe created by the client gets it back (and on iOS `window.webkit.messageHandlers` cannot be locked). |
210
- | **2** | `BridgeShell.verifySandbox` + `BridgeClient` handler | Round-trip `postMessage` to attest the iframe state (`nativeChannelReachable` since 0.18.0). |
211
- | **3** | (Native side) | Shell nonce, stamped on every outgoing message via `Bridge.setOutgoingStamp`, validated natively. **This is the guarantee.** |
212
- | **4** | (Fallback if 1+2 fail) | Dual `InAppWebView` — shell and client in separate WebViews. |
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
-
242
- ## Publish CLI — `condor-mobile-bridge canvas`
243
-
244
- ```bash
245
- npx condor-mobile-bridge canvas publish dist/index.html --version 1.4.0 # validate, confirm, publish
246
- npx condor-mobile-bridge canvas list
247
- npx condor-mobile-bridge canvas revoke <bundleId> --fallback-version 1.3.0
248
- ```
249
-
250
- Publishes the bundle of your company in the Condor Canvas app, through the same
251
- endpoints and login as the Condor web admin: company id, user id, password
252
- (prompted without echo, never stored) and the two-factor code when your company
253
- uses one. It needs a **manager** user and an interactive terminal. A publish goes
254
- **live at once** for every user of the company — use a test company first. The
255
- API is production by default (`--env dev` is for Condor staff; `--base-url`
256
- overrides). Details: `docs/device-testing.md`.
257
-
258
- ## Build CLI — `condor-mobile-bridge build`
259
-
260
- Generates the **self-contained `.zip` bundle** that the white-label app
261
- consumes. The app injects the client HTML via `iframe.srcdoc` (origin `null`,
262
- no base URL) → **relative references do not resolve** and the CSP blocks
263
- external fetches; therefore the `.zip` must contain **a single `index.html`**
264
- with inline JS/CSS and media as `data:` URIs.
265
-
266
- The CLI does this from your build's `dist/`: it reads `index.html`, **inlines**
267
- `<script src>` (escaping `</script>`) and `<link rel=stylesheet>`, resolves CSS
268
- `url()` and local `<img>`/icons to `data:` URIs, and packs a `.zip` with
269
- `index.html` at the root. External refs (`https://…`) are **preserved** (and
270
- blocked by the app's CSP at runtime).
271
-
272
- ```bash
273
- npx condor-mobile-bridge build -i dist/index.html -o dist/bundle.zip
274
- ```
275
-
276
- | Option | Description |
277
- |---|---|
278
- | `-i, --html <path>` | Input HTML (e.g. `dist/index.html`). Required. |
279
- | `-o, --out <path>` | Output `.zip` (default: `dist/whitelabel-bundle.zip`). |
280
- | `--max-bytes <n>` | **Warning** threshold in bytes (default: `2097152` = 2 MB). |
281
-
282
- Output (for registering the bundle with the backend — `bundleSize`/`bundleHash`):
283
-
284
- ```
285
- Built dist/bundle.zip
286
- entry: index.html (123.4 KB self-contained)
287
- bundleSize (zip): 45678 bytes
288
- bundleHash (zip): sha256-<hex>
35
+ npm install @condorinst/mobile-bridge@^0.23.0
289
36
  ```
290
37
 
291
- The hash is **deterministic** (fixed mtime on the zip entry) — the same input
292
- produces the same `bundleHash`. Above the threshold, the CLI **warns** (does
293
- not block): remember that **the app rejects** a `.zip` larger than 2 MB and
294
- falls back to the mock.
38
+ The package ships a prebuilt `dist/` (ESM + CJS + type declarations); no build
39
+ toolchain of ours is needed on your side.
295
40
 
296
- > ⚠️ **Constraint: single-chunk build.** The inliner is post-build and assumes
297
- > a `dist` with **one JS + one CSS + assets**. Builds with **code-splitting /
298
- > ES modules** that `import` across chunks **break** in `srcdoc` (the imports
299
- > do not resolve). Configure your build to emit a single file:
300
- > - **Vite:** `vite-plugin-singlefile` (or `build.rollupOptions.output.inlineDynamicImports: true` + `manualChunks: undefined`).
301
- > - **CRA:** build without code-splitting (no `React.lazy`/dynamic `import()` in routes), single bundle.
302
- >
303
- > Inlining inflates `data:` media by ~33% (base64) — size against the 2 MB limit.
41
+ ## The CLI
304
42
 
305
- ## Key catalog (strictly typed)
306
-
307
- Mirrors the JS-facing side of the native foreground com keys. The strings here
308
- are what the JS receives via `window.CondorBridge.emit(key, payload)`.
309
-
310
- | `BRIDGE_KEYS.*` | Wire (JS) | Typed payload | Typical direction |
311
- |---|---|---|---|
312
- | `pingPong` | `'pingPong'` | `PingPongPayload = { data: 'ping' \| 'pong' }` | bidirectional |
313
- | `userDevice` | `'userDevice'` | `UserDevicePayload = { lastUpdate, devicesConfig[], devicesTelemetry[], devicesUserConfig[] }` | BG → JS |
314
- | `deviceLightSensors` | `'deviceLightSensors'` | `DeviceLightSensorsPayload` — discriminated by `type`: `{ serialNo, type: 'actlumus', data: LightSensorsActlumus1Data }` \| `{ serialNo, type: 'actlumus2', data: LightSensorsActlumus2Data }`. `data` carries the derived spectrum **and** the raw channels (`light_sensor_*`, native ≥ 0.16.0) | BG → JS |
315
- | `syncTime` | `'syncTime'` | `SyncTimeAckPayload = { serialNo, success }` | JS → BG (cmd) / BG → JS (ack) |
316
- | `realTime` | `'realTime'` | `RealTimeAckPayload = { serialNo, success, value }` | JS → BG (cmd) / BG → JS (state) |
317
- | `deleteMemory` | `'deleteMemory'` | `DeleteMemoryAckPayload = { serialNo, success }` | JS → BG (cmd) / BG → JS (ack) |
318
- | `deviceNotification` | `'deviceNotification'` | `DeviceNotificationPayload = { serialNo, isConnect, message, messageCode, isError }` | BG → JS |
319
- | `deviceConnection` | `'deviceConnection'` | `DeviceConnectionPayload = { serialNo, isConnect }` | BG → JS (status); JS → BG (`{type, serialNo}`, permissive) |
320
- | `sensorLog` | `'sensorLog'` | `SensorLogPayload` | BG → JS |
321
- | `dfu` | `'dfu'` | `Record<string, unknown>` (fallback) | BG → JS |
322
- | `systemStatus` | `'systemStatus'` | `SystemStatusPayload = { bluetooth, internet, permission, hasDevices, missingOptionalPermissions[], missingEssentialPermissions[], deviceSyncError }` | native → JS (on change + snapshot after handshake) |
323
- | `navigationBack` | `'navigationBack'` | `NavigationBackPayload = {}` (OS back pressed) | BG → JS (via `BridgeClient.onBack`) |
324
- | `language` | `'language'` | `LanguagePayload = { deviceLocale, appLanguage, resolvedLanguage }` | native → JS (on change + snapshot after handshake) |
325
-
326
- **Automatic inference on `Bridge.on`:**
327
-
328
- ```typescript
329
- Bridge.on(BRIDGE_KEYS.realTime, (payload) => {
330
- // payload: RealTimeAckPayload — type-safe!
331
- console.log(payload.serialNo, payload.success, payload.value);
332
- });
333
- ```
334
-
335
- **`Bridge.send` commands** remain permissive (heterogeneous shapes per type).
336
- The client passes `Record<string, unknown>`.
43
+ | Command | Does | Documented in |
44
+ | --- | --- | --- |
45
+ | `npx condor-mobile-bridge dev -i dist/index.html` | runs your page in a browser harness that behaves like the app | `docs/device-testing.md` |
46
+ | `npx condor-mobile-bridge build -i dist/index.html -o dist/bundle.zip` | inlines your build into one `index.html` and zips it | `docs/device-testing.md` |
47
+ | `npx condor-mobile-bridge validate -i dist/bundle.zip --strict` | applies the app's gates and lints for blocked APIs | `docs/device-testing.md` |
48
+ | `npx condor-mobile-bridge canvas publish \| list \| revoke` | publishes your bundle to your company (interactive login) | `docs/device-testing.md` |
337
49
 
338
50
  ## Versioning policy (semver)
339
51
 
340
- - **PATCH** (`0.22.x`): bugfix with no API change.
52
+ - **PATCH** (`0.23.x`): bugfix with no API change.
341
53
  - **MINOR** (`0.x.0`): new observable key, new command, backward-compatible API.
342
54
  > While on `0.x`, a minor bump may carry breaking changes (semver treats
343
- > pre-1.0 minors as breaking). Pin `^0.22.0` to stay on safe patches.
55
+ > pre-1.0 minors as breaking). Pin `^0.23.0` to stay on safe patches.
344
56
  - **MAJOR** (`x.0.0`): incompatible schema change (key rename, payload shape
345
57
  change, public API change). `1.0.0` marks the production-stable contract.
346
58
 
@@ -349,155 +61,11 @@ declares `clientVersion`. This release does **not** enforce hard semver
349
61
  validation — the native side accepts any `clientVersion`. Real validation will
350
62
  land once there are 2+ versions in the field.
351
63
 
352
- ## Host detection
353
-
354
- The package detects automatically:
355
-
356
- - **Android / iOS** — `window.CondorNative`, a JavaScript channel registered with
357
- `webview_flutter` (`addJavaScriptChannel('CondorNative')`). Messages are sent as a
358
- JSON string. Takes precedence when present.
359
- - **Android / iOS (legacy)** — `window.flutter_inappwebview` (via the `flutter_inappwebview`
360
- package). Still supported for the transition; will be removed in a future minor.
361
- - **Browser outside the app** (dev/test): **mock mode** activates automatically
362
- for `Bridge`. A client bundle (`BridgeClient`) runs in the browser only
363
- through the `dev` harness.
364
-
365
- ## Mock mode (shell-side `Bridge`)
366
-
367
- > For code that uses the legacy `Bridge` directly — the shell, tests,
368
- > Storybook. A client bundle uses `BridgeClient`, which has no mock of its own:
369
- > develop it with `condor-mobile-bridge dev` (above).
370
-
371
- When the package runs outside the Flutter WebView (regular browser, Vite dev
372
- server, Storybook), the adapter activates mock mode automatically. You can
373
- develop in the browser without building the APK.
374
-
375
- ```typescript
376
- import { Bridge, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
377
-
378
- if (Bridge.isMock) {
379
- // Subscribe to a listener as usual
380
- Bridge.on(BRIDGE_KEYS.deviceConnection, (payload) => {
381
- console.log('Device:', payload);
382
- });
383
-
384
- // Simulate the native side sending an event
385
- Bridge.mockEmit(BRIDGE_KEYS.deviceConnection, {
386
- serialNo: 1234,
387
- connected: true,
388
- });
389
- }
390
- ```
64
+ ## Shell side (Condor)
391
65
 
392
- ### Handshake in mock is EXPLICIT
393
-
394
- ```typescript
395
- // In mock, handshake does NOT resolve by itself — the client/test responds.
396
- const ackPromise = Bridge.handshake({ clientVersion: '1.0.0' });
397
-
398
- if (Bridge.isMock) {
399
- Bridge.mockEmit('handshake-ack', {
400
- nativeVersion: '0.1.0',
401
- accepted: true,
402
- });
403
- }
404
-
405
- const ack = await ackPromise;
406
- ```
407
-
408
- Design decision: it forces tests to be intentional about the native state. In
409
- real mode (inside the Flutter WebView), the ack arrives naturally from native.
410
-
411
- ### `Bridge.send` in mock
412
-
413
- Messages sent via `Bridge.send` (or helpers like `openSettings`) are recorded
414
- in an internal FIFO buffer (cap 100) + `console.debug`. Inspect via
415
- `Bridge.getMockState()`:
416
-
417
- ```typescript
418
- Bridge.send('realTime', { enabled: true });
419
-
420
- const state = Bridge.getMockState();
421
- console.log(state.messages);
422
- // → [{ handlerName: 'CondorBridge', payload: {...}, timestamp: ... }]
423
- console.log(state.subscriberKeys);
424
- // → ['deviceConnection']
425
- ```
426
-
427
- ## Dev panel (shell-side `Bridge`)
428
-
429
- > The `dev` harness installs this panel for you. Call `installDevPanel` yourself
430
- > only when hosting `Bridge` directly.
431
-
432
- An opt-in HTML panel to inspect the bridge in the browser during development.
433
-
434
- ```typescript
435
- import { Bridge } from '@condorinst/mobile-bridge';
436
-
437
- if (Bridge.isMock) {
438
- Bridge.installDevPanel({
439
- position: 'bottom-right', // 'bottom-right' | 'bottom-left' | 'top-*'
440
- maxMessages: 20, // last N shown
441
- theme: 'dark', // 'dark' | 'light'
442
- startCollapsed: false,
443
- });
444
- }
445
- ```
446
-
447
- Contents:
448
-
449
- - Header with a "MOCK" badge, sent counter, minimize button.
450
- - Sent log — last N messages (timestamp, type, truncated payload).
451
- - Trigger emit — dropdown of `BRIDGE_KEYS` + JSON textarea + "Emit" button
452
- (fires `Bridge.mockEmit`) + "Clear sent" button.
453
-
454
- Idempotent — calling `installDevPanel` again replaces the previous panel.
455
- Throws if called in real mode (`!Bridge.isMock`).
456
-
457
- ### Auto-respond toggle
458
-
459
- To close the full loop in the browser (client clicks a button → native
460
- responds → subscriber fires), pass a **policy** that maps `send` to `emit`.
461
- The package provides the **mechanism** (checkbox UI + hook); you provide the
462
- **policy** (app-specific mappings).
463
-
464
- ```typescript
465
- import { Bridge, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
466
-
467
- if (Bridge.isMock) {
468
- Bridge.installDevPanel({
469
- startAutoRespondEnabled: true, // default false
470
- autoRespondDelayMs: 50, // default 50 (simulates IPC)
471
- onSendForAutoRespond(msg) {
472
- const payload = msg.payload as { type?: string };
473
- switch (payload?.type) {
474
- case 'handshake':
475
- return {
476
- key: 'handshake-ack',
477
- payload: { nativeVersion: '0.1.0 (mock)', accepted: true },
478
- };
479
- case 'ping':
480
- return {
481
- key: BRIDGE_KEYS.pingPong,
482
- payload: { data: 'pong' },
483
- };
484
- default:
485
- return null; // do not auto-respond to this send
486
- }
487
- },
488
- });
489
- }
490
- ```
491
-
492
- Behavior:
493
-
494
- - The toggle appears in the panel **only when** `onSendForAutoRespond` is
495
- passed — without a policy, the panel keeps the manual-emit UX.
496
- - The toggle state persists in memory for the panel's lifetime. Re-install
497
- resets to `startAutoRespondEnabled`.
498
- - Errors thrown by the policy or the internal `triggerEmit` are caught and
499
- logged (`console.error`) — they do not destabilize the bridge.
500
- - Use `BRIDGE_KEYS` to avoid string-magic in the mappings.
66
+ `BridgeShell`, the legacy `Bridge` singleton, mock mode and the dev panel are
67
+ for Condor's app shell, not for client bundles:
68
+ [`docs/shell-integration.md`](docs/shell-integration.md).
501
69
 
502
70
  ## Local development (SDK contributors)
503
71
 
@@ -510,25 +78,6 @@ npm run test # vitest run
510
78
  npm run build # tsup (ESM + CJS + types in dist/)
511
79
  ```
512
80
 
513
- ## Further reading
514
-
515
- Every file below ships in the npm tarball, so these relative links resolve
516
- inside `node_modules/@condorinst/mobile-bridge/` as well as in the repository.
517
-
518
- | Document | What it answers |
519
- | --- | --- |
520
- | [`AGENTS.md`](./AGENTS.md) | The contract in one screen. Start here if you are, or are using, an AI agent. |
521
- | [`llms.txt`](./llms.txt) | Index of every document, with absolute pinned URLs. |
522
- | [Agent guide](./docs/agent-guide.md) | Step-by-step construction of a client Home. |
523
- | [Bridge contract](./docs/bridge-contract.md) | Every key, command, error shape and capability, by native version. |
524
- | [`contract/bridge-contract.json`](./contract/bridge-contract.json) | The same, as JSON. |
525
- | [Limits](./docs/limits.md) | Every hard limit, and what happens when you cross it. |
526
- | [Troubleshooting](./docs/troubleshooting.md) | Why a command did nothing. |
527
- | [Client guide](./docs/client-guide.md) | Narrative walkthrough — build your White Label Home. |
528
- | [Pilot onboarding](./docs/pilot-onboarding.md) | Checklist: zero to a delivered bundle. |
529
- | [`examples/minimal-home`](./examples/minimal-home) | A runnable client in ~160 lines. |
530
- | [CHANGELOG](./CHANGELOG.md) | When each capability landed — the precise record for version checks. |
531
-
532
81
  ## License
533
82
 
534
83
  Proprietary — Condor Instruments. See [LICENSE](./LICENSE).