@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.
- package/AGENTS.md +142 -34
- package/CHANGELOG.md +198 -0
- package/README.md +30 -465
- package/contract/bridge-contract.json +34 -22
- package/dist/cli.cjs +639 -43
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +635 -39
- package/dist/cli.js.map +1 -1
- package/dist/dev-harness.global.js +388 -55
- package/dist/dev.cjs +389 -55
- package/dist/dev.cjs.map +1 -1
- package/dist/dev.d.cts +239 -9
- package/dist/dev.d.ts +239 -9
- package/dist/dev.js +388 -55
- package/dist/dev.js.map +1 -1
- package/dist/index.cjs +30 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +154 -26
- package/dist/index.d.ts +154 -26
- package/dist/index.js +29 -8
- package/dist/index.js.map +1 -1
- package/docs/agent-guide.md +9 -265
- package/docs/bridge-contract.md +313 -26
- package/docs/client-guide.md +34 -398
- package/docs/device-testing.md +286 -22
- package/docs/pilot-onboarding.md +41 -111
- package/docs/shell-integration.md +292 -0
- package/docs/troubleshooting.md +19 -11
- package/examples/minimal-home/README.md +2 -2
- package/examples/minimal-home/package.json +1 -1
- package/examples/minimal-home/src/main.ts +23 -3
- package/llms-full.txt +825 -881
- package/llms.txt +13 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,27 +1,24 @@
|
|
|
1
1
|
# @condorinst/mobile-bridge
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
15
|
-
`
|
|
16
|
-
|
|
17
|
-
`
|
|
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.
|
|
24
|
-
https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.
|
|
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,299 +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.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
|
|
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')! });
|
|
35
|
+
npm install @condorinst/mobile-bridge@^0.23.0
|
|
237
36
|
```
|
|
238
37
|
|
|
239
|
-
|
|
240
|
-
|
|
38
|
+
The package ships a prebuilt `dist/` (ESM + CJS + type declarations); no build
|
|
39
|
+
toolchain of ours is needed on your side.
|
|
241
40
|
|
|
242
|
-
##
|
|
41
|
+
## The CLI
|
|
243
42
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
The CLI does this from your build's `dist/`: it reads `index.html`, **inlines**
|
|
251
|
-
`<script src>` (escaping `</script>`) and `<link rel=stylesheet>`, resolves CSS
|
|
252
|
-
`url()` and local `<img>`/icons to `data:` URIs, and packs a `.zip` with
|
|
253
|
-
`index.html` at the root. External refs (`https://…`) are **preserved** (and
|
|
254
|
-
blocked by the app's CSP at runtime).
|
|
255
|
-
|
|
256
|
-
```bash
|
|
257
|
-
npx condor-mobile-bridge build -i dist/index.html -o dist/bundle.zip
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
| Option | Description |
|
|
261
|
-
|---|---|
|
|
262
|
-
| `-i, --html <path>` | Input HTML (e.g. `dist/index.html`). Required. |
|
|
263
|
-
| `-o, --out <path>` | Output `.zip` (default: `dist/whitelabel-bundle.zip`). |
|
|
264
|
-
| `--max-bytes <n>` | **Warning** threshold in bytes (default: `2097152` = 2 MB). |
|
|
265
|
-
|
|
266
|
-
Output (for registering the bundle with the backend — `bundleSize`/`bundleHash`):
|
|
267
|
-
|
|
268
|
-
```
|
|
269
|
-
Built dist/bundle.zip
|
|
270
|
-
entry: index.html (123.4 KB self-contained)
|
|
271
|
-
bundleSize (zip): 45678 bytes
|
|
272
|
-
bundleHash (zip): sha256-<hex>
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
The hash is **deterministic** (fixed mtime on the zip entry) — the same input
|
|
276
|
-
produces the same `bundleHash`. Above the threshold, the CLI **warns** (does
|
|
277
|
-
not block): remember that **the app rejects** a `.zip` larger than 2 MB and
|
|
278
|
-
falls back to the mock.
|
|
279
|
-
|
|
280
|
-
> ⚠️ **Constraint: single-chunk build.** The inliner is post-build and assumes
|
|
281
|
-
> a `dist` with **one JS + one CSS + assets**. Builds with **code-splitting /
|
|
282
|
-
> ES modules** that `import` across chunks **break** in `srcdoc` (the imports
|
|
283
|
-
> do not resolve). Configure your build to emit a single file:
|
|
284
|
-
> - **Vite:** `vite-plugin-singlefile` (or `build.rollupOptions.output.inlineDynamicImports: true` + `manualChunks: undefined`).
|
|
285
|
-
> - **CRA:** build without code-splitting (no `React.lazy`/dynamic `import()` in routes), single bundle.
|
|
286
|
-
>
|
|
287
|
-
> Inlining inflates `data:` media by ~33% (base64) — size against the 2 MB limit.
|
|
288
|
-
|
|
289
|
-
## Key catalog (strictly typed)
|
|
290
|
-
|
|
291
|
-
Mirrors the JS-facing side of the native foreground com keys. The strings here
|
|
292
|
-
are what the JS receives via `window.CondorBridge.emit(key, payload)`.
|
|
293
|
-
|
|
294
|
-
| `BRIDGE_KEYS.*` | Wire (JS) | Typed payload | Typical direction |
|
|
295
|
-
|---|---|---|---|
|
|
296
|
-
| `pingPong` | `'pingPong'` | `PingPongPayload = { data: 'ping' \| 'pong' }` | bidirectional |
|
|
297
|
-
| `userDevice` | `'userDevice'` | `UserDevicePayload = { lastUpdate, devicesConfig[], devicesTelemetry[], devicesUserConfig[] }` | BG → JS |
|
|
298
|
-
| `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 |
|
|
299
|
-
| `syncTime` | `'syncTime'` | `SyncTimeAckPayload = { serialNo, success }` | JS → BG (cmd) / BG → JS (ack) |
|
|
300
|
-
| `realTime` | `'realTime'` | `RealTimeAckPayload = { serialNo, success, value }` | JS → BG (cmd) / BG → JS (state) |
|
|
301
|
-
| `deleteMemory` | `'deleteMemory'` | `DeleteMemoryAckPayload = { serialNo, success }` | JS → BG (cmd) / BG → JS (ack) |
|
|
302
|
-
| `deviceNotification` | `'deviceNotification'` | `DeviceNotificationPayload = { serialNo, isConnect, message, messageCode, isError }` | BG → JS |
|
|
303
|
-
| `deviceConnection` | `'deviceConnection'` | `DeviceConnectionPayload = { serialNo, isConnect }` | BG → JS (status); JS → BG (`{type, serialNo}`, permissive) |
|
|
304
|
-
| `sensorLog` | `'sensorLog'` | `SensorLogPayload` | BG → JS |
|
|
305
|
-
| `dfu` | `'dfu'` | `Record<string, unknown>` (fallback) | BG → JS |
|
|
306
|
-
| `systemStatus` | `'systemStatus'` | `SystemStatusPayload = { bluetooth, internet, permission, hasDevices, missingOptionalPermissions[], missingEssentialPermissions[], deviceSyncError }` | native → JS (on change + snapshot after handshake) |
|
|
307
|
-
| `navigationBack` | `'navigationBack'` | `NavigationBackPayload = {}` (OS back pressed) | BG → JS (via `BridgeClient.onBack`) |
|
|
308
|
-
| `language` | `'language'` | `LanguagePayload = { deviceLocale, appLanguage, resolvedLanguage }` | native → JS (on change + snapshot after handshake) |
|
|
309
|
-
|
|
310
|
-
**Automatic inference on `Bridge.on`:**
|
|
311
|
-
|
|
312
|
-
```typescript
|
|
313
|
-
Bridge.on(BRIDGE_KEYS.realTime, (payload) => {
|
|
314
|
-
// payload: RealTimeAckPayload — type-safe!
|
|
315
|
-
console.log(payload.serialNo, payload.success, payload.value);
|
|
316
|
-
});
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
**`Bridge.send` commands** remain permissive (heterogeneous shapes per type).
|
|
320
|
-
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` |
|
|
321
49
|
|
|
322
50
|
## Versioning policy (semver)
|
|
323
51
|
|
|
324
|
-
- **PATCH** (`0.
|
|
52
|
+
- **PATCH** (`0.23.x`): bugfix with no API change.
|
|
325
53
|
- **MINOR** (`0.x.0`): new observable key, new command, backward-compatible API.
|
|
326
54
|
> While on `0.x`, a minor bump may carry breaking changes (semver treats
|
|
327
|
-
> pre-1.0 minors as breaking). Pin `^0.
|
|
55
|
+
> pre-1.0 minors as breaking). Pin `^0.23.0` to stay on safe patches.
|
|
328
56
|
- **MAJOR** (`x.0.0`): incompatible schema change (key rename, payload shape
|
|
329
57
|
change, public API change). `1.0.0` marks the production-stable contract.
|
|
330
58
|
|
|
@@ -333,155 +61,11 @@ declares `clientVersion`. This release does **not** enforce hard semver
|
|
|
333
61
|
validation — the native side accepts any `clientVersion`. Real validation will
|
|
334
62
|
land once there are 2+ versions in the field.
|
|
335
63
|
|
|
336
|
-
##
|
|
337
|
-
|
|
338
|
-
The package detects automatically:
|
|
339
|
-
|
|
340
|
-
- **Android / iOS** — `window.CondorNative`, a JavaScript channel registered with
|
|
341
|
-
`webview_flutter` (`addJavaScriptChannel('CondorNative')`). Messages are sent as a
|
|
342
|
-
JSON string. Takes precedence when present.
|
|
343
|
-
- **Android / iOS (legacy)** — `window.flutter_inappwebview` (via the `flutter_inappwebview`
|
|
344
|
-
package). Still supported for the transition; will be removed in a future minor.
|
|
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.
|
|
348
|
-
|
|
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).
|
|
354
|
-
|
|
355
|
-
When the package runs outside the Flutter WebView (regular browser, Vite dev
|
|
356
|
-
server, Storybook), the adapter activates mock mode automatically. You can
|
|
357
|
-
develop in the browser without building the APK.
|
|
358
|
-
|
|
359
|
-
```typescript
|
|
360
|
-
import { Bridge, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
|
|
361
|
-
|
|
362
|
-
if (Bridge.isMock) {
|
|
363
|
-
// Subscribe to a listener as usual
|
|
364
|
-
Bridge.on(BRIDGE_KEYS.deviceConnection, (payload) => {
|
|
365
|
-
console.log('Device:', payload);
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
// Simulate the native side sending an event
|
|
369
|
-
Bridge.mockEmit(BRIDGE_KEYS.deviceConnection, {
|
|
370
|
-
serialNo: 1234,
|
|
371
|
-
connected: true,
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
### Handshake in mock is EXPLICIT
|
|
64
|
+
## Shell side (Condor)
|
|
377
65
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if (Bridge.isMock) {
|
|
383
|
-
Bridge.mockEmit('handshake-ack', {
|
|
384
|
-
nativeVersion: '0.1.0',
|
|
385
|
-
accepted: true,
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const ack = await ackPromise;
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
Design decision: it forces tests to be intentional about the native state. In
|
|
393
|
-
real mode (inside the Flutter WebView), the ack arrives naturally from native.
|
|
394
|
-
|
|
395
|
-
### `Bridge.send` in mock
|
|
396
|
-
|
|
397
|
-
Messages sent via `Bridge.send` (or helpers like `openSettings`) are recorded
|
|
398
|
-
in an internal FIFO buffer (cap 100) + `console.debug`. Inspect via
|
|
399
|
-
`Bridge.getMockState()`:
|
|
400
|
-
|
|
401
|
-
```typescript
|
|
402
|
-
Bridge.send('realTime', { enabled: true });
|
|
403
|
-
|
|
404
|
-
const state = Bridge.getMockState();
|
|
405
|
-
console.log(state.messages);
|
|
406
|
-
// → [{ handlerName: 'CondorBridge', payload: {...}, timestamp: ... }]
|
|
407
|
-
console.log(state.subscriberKeys);
|
|
408
|
-
// → ['deviceConnection']
|
|
409
|
-
```
|
|
410
|
-
|
|
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.
|
|
415
|
-
|
|
416
|
-
An opt-in HTML panel to inspect the bridge in the browser during development.
|
|
417
|
-
|
|
418
|
-
```typescript
|
|
419
|
-
import { Bridge } from '@condorinst/mobile-bridge';
|
|
420
|
-
|
|
421
|
-
if (Bridge.isMock) {
|
|
422
|
-
Bridge.installDevPanel({
|
|
423
|
-
position: 'bottom-right', // 'bottom-right' | 'bottom-left' | 'top-*'
|
|
424
|
-
maxMessages: 20, // last N shown
|
|
425
|
-
theme: 'dark', // 'dark' | 'light'
|
|
426
|
-
startCollapsed: false,
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
```
|
|
430
|
-
|
|
431
|
-
Contents:
|
|
432
|
-
|
|
433
|
-
- Header with a "MOCK" badge, sent counter, minimize button.
|
|
434
|
-
- Sent log — last N messages (timestamp, type, truncated payload).
|
|
435
|
-
- Trigger emit — dropdown of `BRIDGE_KEYS` + JSON textarea + "Emit" button
|
|
436
|
-
(fires `Bridge.mockEmit`) + "Clear sent" button.
|
|
437
|
-
|
|
438
|
-
Idempotent — calling `installDevPanel` again replaces the previous panel.
|
|
439
|
-
Throws if called in real mode (`!Bridge.isMock`).
|
|
440
|
-
|
|
441
|
-
### Auto-respond toggle
|
|
442
|
-
|
|
443
|
-
To close the full loop in the browser (client clicks a button → native
|
|
444
|
-
responds → subscriber fires), pass a **policy** that maps `send` to `emit`.
|
|
445
|
-
The package provides the **mechanism** (checkbox UI + hook); you provide the
|
|
446
|
-
**policy** (app-specific mappings).
|
|
447
|
-
|
|
448
|
-
```typescript
|
|
449
|
-
import { Bridge, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
|
|
450
|
-
|
|
451
|
-
if (Bridge.isMock) {
|
|
452
|
-
Bridge.installDevPanel({
|
|
453
|
-
startAutoRespondEnabled: true, // default false
|
|
454
|
-
autoRespondDelayMs: 50, // default 50 (simulates IPC)
|
|
455
|
-
onSendForAutoRespond(msg) {
|
|
456
|
-
const payload = msg.payload as { type?: string };
|
|
457
|
-
switch (payload?.type) {
|
|
458
|
-
case 'handshake':
|
|
459
|
-
return {
|
|
460
|
-
key: 'handshake-ack',
|
|
461
|
-
payload: { nativeVersion: '0.1.0 (mock)', accepted: true },
|
|
462
|
-
};
|
|
463
|
-
case 'ping':
|
|
464
|
-
return {
|
|
465
|
-
key: BRIDGE_KEYS.pingPong,
|
|
466
|
-
payload: { data: 'pong' },
|
|
467
|
-
};
|
|
468
|
-
default:
|
|
469
|
-
return null; // do not auto-respond to this send
|
|
470
|
-
}
|
|
471
|
-
},
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
|
-
```
|
|
475
|
-
|
|
476
|
-
Behavior:
|
|
477
|
-
|
|
478
|
-
- The toggle appears in the panel **only when** `onSendForAutoRespond` is
|
|
479
|
-
passed — without a policy, the panel keeps the manual-emit UX.
|
|
480
|
-
- The toggle state persists in memory for the panel's lifetime. Re-install
|
|
481
|
-
resets to `startAutoRespondEnabled`.
|
|
482
|
-
- Errors thrown by the policy or the internal `triggerEmit` are caught and
|
|
483
|
-
logged (`console.error`) — they do not destabilize the bridge.
|
|
484
|
-
- 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).
|
|
485
69
|
|
|
486
70
|
## Local development (SDK contributors)
|
|
487
71
|
|
|
@@ -494,25 +78,6 @@ npm run test # vitest run
|
|
|
494
78
|
npm run build # tsup (ESM + CJS + types in dist/)
|
|
495
79
|
```
|
|
496
80
|
|
|
497
|
-
## Further reading
|
|
498
|
-
|
|
499
|
-
Every file below ships in the npm tarball, so these relative links resolve
|
|
500
|
-
inside `node_modules/@condorinst/mobile-bridge/` as well as in the repository.
|
|
501
|
-
|
|
502
|
-
| Document | What it answers |
|
|
503
|
-
| --- | --- |
|
|
504
|
-
| [`AGENTS.md`](./AGENTS.md) | The contract in one screen. Start here if you are, or are using, an AI agent. |
|
|
505
|
-
| [`llms.txt`](./llms.txt) | Index of every document, with absolute pinned URLs. |
|
|
506
|
-
| [Agent guide](./docs/agent-guide.md) | Step-by-step construction of a client Home. |
|
|
507
|
-
| [Bridge contract](./docs/bridge-contract.md) | Every key, command, error shape and capability, by native version. |
|
|
508
|
-
| [`contract/bridge-contract.json`](./contract/bridge-contract.json) | The same, as JSON. |
|
|
509
|
-
| [Limits](./docs/limits.md) | Every hard limit, and what happens when you cross it. |
|
|
510
|
-
| [Troubleshooting](./docs/troubleshooting.md) | Why a command did nothing. |
|
|
511
|
-
| [Client guide](./docs/client-guide.md) | Narrative walkthrough — build your White Label Home. |
|
|
512
|
-
| [Pilot onboarding](./docs/pilot-onboarding.md) | Checklist: zero to a delivered bundle. |
|
|
513
|
-
| [`examples/minimal-home`](./examples/minimal-home) | A runnable client in ~160 lines. |
|
|
514
|
-
| [CHANGELOG](./CHANGELOG.md) | When each capability landed — the precise record for version checks. |
|
|
515
|
-
|
|
516
81
|
## License
|
|
517
82
|
|
|
518
83
|
Proprietary — Condor Instruments. See [LICENSE](./LICENSE).
|