@condorinst/mobile-bridge 0.18.0 → 0.19.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 +120 -0
- package/CHANGELOG.md +57 -0
- package/README.md +66 -11
- package/contract/bridge-contract.json +198 -0
- package/dist/cli.cjs +230 -17
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +231 -18
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +64 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +442 -396
- package/dist/index.d.ts +442 -396
- package/dist/index.js +64 -64
- package/dist/index.js.map +1 -1
- package/docs/agent-guide.md +209 -0
- package/docs/bridge-contract.md +249 -0
- package/docs/client-guide.md +387 -0
- package/docs/limits.md +111 -0
- package/docs/pilot-onboarding.md +103 -0
- package/docs/troubleshooting.md +150 -0
- package/examples/minimal-home/AGENTS.md +28 -0
- package/examples/minimal-home/README.md +42 -0
- package/examples/minimal-home/build.mjs +31 -0
- package/examples/minimal-home/index.html +77 -0
- package/examples/minimal-home/package.json +23 -0
- package/examples/minimal-home/src/main.ts +161 -0
- package/examples/minimal-home/tsconfig.json +14 -0
- package/llms.txt +44 -0
- package/package.json +15 -2
package/AGENTS.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# AGENTS.md — building a Condor Canvas client
|
|
2
|
+
|
|
3
|
+
You are writing the **Home screen** of a Condor Canvas app. It is a single web
|
|
4
|
+
bundle that renders inside a sandboxed iframe in a native mobile app. The app
|
|
5
|
+
owns login, settings and the Bluetooth connection to the devices; you own what
|
|
6
|
+
the person sees on the Home.
|
|
7
|
+
|
|
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
|
|
10
|
+
device, where mistakes are expensive to diagnose.
|
|
11
|
+
|
|
12
|
+
## The one thing that makes this different
|
|
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.
|
|
18
|
+
|
|
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.
|
|
22
|
+
|
|
23
|
+
## Hard rules
|
|
24
|
+
|
|
25
|
+
1. **No storage.** `localStorage`, `sessionStorage`, `indexedDB` and cookies all
|
|
26
|
+
fail — the iframe has a `null` origin. Keep state in memory and accept that
|
|
27
|
+
it is lost on reload. Re-request what you need at startup.
|
|
28
|
+
2. **No network.** `connect-src 'self'` on a null origin blocks every `fetch`,
|
|
29
|
+
`XMLHttpRequest` and `WebSocket`. All data comes through the bridge.
|
|
30
|
+
3. **No `eval`, no `new Function`.** The CSP has no `'unsafe-eval'`. Configure
|
|
31
|
+
your bundler so it does not emit eval-based sourcemaps.
|
|
32
|
+
4. **One self-contained `index.html`.** Every script inline, every stylesheet
|
|
33
|
+
inline, every image and font as a `data:` URI. A `<script src>` or a
|
|
34
|
+
`<link rel="stylesheet">` pointing anywhere else makes the app **reject the
|
|
35
|
+
whole bundle**.
|
|
36
|
+
5. **Build single-chunk.** No code splitting, no ES module imports between
|
|
37
|
+
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.
|
|
40
|
+
7. **Use `BridgeClient`, never raw `postMessage`.** Hand-rolled messages fail
|
|
41
|
+
schema validation and are dropped without a trace.
|
|
42
|
+
8. **Never assume a capability.** Read `nativeVersion` from the handshake and
|
|
43
|
+
branch on it. See the capability matrix in `docs/bridge-contract.md`.
|
|
44
|
+
|
|
45
|
+
## What you cannot reach
|
|
46
|
+
|
|
47
|
+
`window.parent` properties · `window.top` · `window.open` ·
|
|
48
|
+
`navigator.sendBeacon` (neutralized before your code runs) · service workers ·
|
|
49
|
+
camera, microphone and geolocation (denied unconditionally) · the `dfu` key and
|
|
50
|
+
command (denied by policy).
|
|
51
|
+
|
|
52
|
+
## The API
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm install @condorinst/mobile-bridge
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { BridgeClient, BRIDGE_KEYS, type UserDevicePayload } from '@condorinst/mobile-bridge';
|
|
60
|
+
|
|
61
|
+
// 1. Handshake first. It is how you learn what this device can do.
|
|
62
|
+
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.
|
|
65
|
+
|
|
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));
|
|
69
|
+
|
|
70
|
+
// 3. Commands are fire-and-forget. No return value, no throw.
|
|
71
|
+
BridgeClient.send('userDevice', { getFromServer: false });
|
|
72
|
+
|
|
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.
|
|
76
|
+
```
|
|
77
|
+
|
|
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.
|
|
81
|
+
|
|
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.
|
|
84
|
+
|
|
85
|
+
## The validation loop — run this before you call the work done
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npx condor-mobile-bridge build -i dist/index.html -o dist/bundle.zip
|
|
89
|
+
npx condor-mobile-bridge validate -i dist/bundle.zip --strict
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`validate` applies the same gates the app applies on download, plus a lint for
|
|
93
|
+
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`.
|
|
99
|
+
|
|
100
|
+
## Definition of done
|
|
101
|
+
|
|
102
|
+
- [ ] `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 (`npm run dev` in the example), where
|
|
108
|
+
`nativeVersion` is `null` — your code must survive that.
|
|
109
|
+
|
|
110
|
+
## Where to read more
|
|
111
|
+
|
|
112
|
+
| Question | File |
|
|
113
|
+
| --- | --- |
|
|
114
|
+
| Every key, command, error shape and capability | `docs/bridge-contract.md` |
|
|
115
|
+
| The same, as JSON you can parse | `contract/bridge-contract.json` |
|
|
116
|
+
| Step-by-step build of a Home | `docs/agent-guide.md` |
|
|
117
|
+
| Every hard limit, with the code that enforces it | `docs/limits.md` |
|
|
118
|
+
| "My command did nothing" | `docs/troubleshooting.md` |
|
|
119
|
+
| Payload shapes, authoritative | `dist/index.d.ts` (your editor shows them) |
|
|
120
|
+
| A working client you can copy | `examples/minimal-home/` |
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,63 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.19.0] — 2026-09-22
|
|
11
|
+
|
|
12
|
+
Documentation release: the package is now consumable by an AI agent building a
|
|
13
|
+
client bundle, and the bundle can be verified against the contract before it is
|
|
14
|
+
delivered.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`condor-mobile-bridge validate`.** New subcommand that checks a bundle
|
|
19
|
+
against every gate the Canvas app applies on download — a single `index.html`
|
|
20
|
+
at the archive root, self-contained (no external `<script src>` or stylesheet
|
|
21
|
+
`<link>`), within the 2 MB limit — and lints for the APIs the sandbox and the
|
|
22
|
+
CSP block at runtime: Web Storage, IndexedDB, cookies, `eval`/`new Function`,
|
|
23
|
+
service workers, `sendBeacon`, `window.open`, `window.top` and outbound
|
|
24
|
+
network calls. Errors are app-level rejections; warnings install fine and fail
|
|
25
|
+
on the device. `--strict` makes warnings fail too. Accepts a `.zip` or a
|
|
26
|
+
pre-build `.html`.
|
|
27
|
+
- **`AGENTS.md` and `llms.txt`** at the package root — the entry point most AI
|
|
28
|
+
coding tools look for, and an index of every document with absolute, pinned,
|
|
29
|
+
unauthenticated URLs.
|
|
30
|
+
- **`contract/bridge-contract.json`** — the bridge contract as data: every key
|
|
31
|
+
with its client reachability, every command with its confirmation
|
|
32
|
+
requirement, the hard limits, the three error shapes, the six silent-drop
|
|
33
|
+
paths, and the capability matrix by native version. Importable as
|
|
34
|
+
`@condorinst/mobile-bridge/contract`. Carries a `verifiedAgainst` block
|
|
35
|
+
citing the file and line of every constant mirrored from the app, and a test
|
|
36
|
+
locks its key set against `BRIDGE_KEYS`.
|
|
37
|
+
- **`docs/agent-guide.md`**, **`docs/bridge-contract.md`**,
|
|
38
|
+
**`docs/limits.md`** and **`docs/troubleshooting.md`**.
|
|
39
|
+
- `NavigationBackPayload` and `NavigationStatePayload` are now exported. They
|
|
40
|
+
were part of the observable contract but had no importable name.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- **`docs/`, `contract/`, `AGENTS.md`, `llms.txt` and `examples/minimal-home`
|
|
45
|
+
now ship in the npm tarball.** Until 0.18.0 only the README reached a
|
|
46
|
+
consumer, so the Client Guide was unreachable from `node_modules` — and from
|
|
47
|
+
any tool reading the installed package. They are also served as plain text by
|
|
48
|
+
jsDelivr and unpkg, pinned per version, which the public repository cannot
|
|
49
|
+
offer: its landing page is a JavaScript application that returns no readable
|
|
50
|
+
text to a fetcher.
|
|
51
|
+
- `ClientToShellMessage` and `ShellToClientMessage` are now **type-only**
|
|
52
|
+
exports. The Valibot schemas behind them were renamed to
|
|
53
|
+
`ClientToShellMessageSchema` and `ShellToClientMessageSchema`. Previously each
|
|
54
|
+
name was both a value and a type, the `.d.ts` rollup collapsed the two, and a
|
|
55
|
+
consumer importing the name as a value type-checked and then got `undefined`
|
|
56
|
+
at runtime. The schemas remain internal.
|
|
57
|
+
- README: the client-facing `BridgeClient` quickstart now comes before the
|
|
58
|
+
legacy shell-side `Bridge`; the pin recommendation moved from `^0.8.0` to
|
|
59
|
+
`^0.19.0`; a broken relative link to a file absent from the tarball was
|
|
60
|
+
fixed.
|
|
61
|
+
- README: corrected a stale claim that the client handshake resolves without
|
|
62
|
+
`nativeVersion`. It has carried `nativeVersion` since 0.17.0, and that value
|
|
63
|
+
is the only capability signal a client gets.
|
|
64
|
+
- Examples pinned to `^0.19.0` (they were on `^0.8.0` and `^0.17.0`).
|
|
65
|
+
|
|
66
|
+
|
|
10
67
|
## [0.18.0] — 2026-09-16
|
|
11
68
|
|
|
12
69
|
### Added
|
package/README.md
CHANGED
|
@@ -9,9 +9,49 @@ mobile app.
|
|
|
9
9
|
> background, commands to the foreground service, and hooks into native
|
|
10
10
|
> screens (Settings).
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
## Reading this with an AI agent?
|
|
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:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.19.0/llms.txt
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The machine-readable contract — keys, commands, limits, capability matrix — is
|
|
27
|
+
`contract/bridge-contract.json`, importable as
|
|
28
|
+
`@condorinst/mobile-bridge/contract`.
|
|
29
|
+
|
|
30
|
+
> Note for fetchers: the Bitbucket repository landing page is a JavaScript
|
|
31
|
+
> application and returns no readable text. Use the jsDelivr URLs above, or
|
|
32
|
+
> Bitbucket's `raw/` file URLs.
|
|
33
|
+
|
|
34
|
+
## New here, as a human?
|
|
35
|
+
|
|
36
|
+
You are almost certainly building the **client bundle** that renders as the
|
|
37
|
+
app's Home. That means `BridgeClient`:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { BridgeClient, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
|
|
41
|
+
|
|
42
|
+
const ack = await BridgeClient.handshake({ clientVersion: '1.0.0' });
|
|
43
|
+
// ack.nativeVersion is your capability signal. ack.accepted is always true.
|
|
44
|
+
|
|
45
|
+
BridgeClient.on(BRIDGE_KEYS.userDevice, (devices) => render(devices));
|
|
46
|
+
BridgeClient.send('userDevice', { getFromServer: false });
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Then read the [Client Guide](docs/client-guide.md) — _"Build your White Label
|
|
50
|
+
Home"_ — and copy the runnable [`examples/minimal-home`](examples/minimal-home).
|
|
51
|
+
|
|
52
|
+
The `Bridge` singleton shown under *Quickstart* below is the **legacy,
|
|
53
|
+
shell-side** API. It talks to the native layer directly and is not reachable
|
|
54
|
+
from a sandboxed client bundle. If you are the client, you want `BridgeClient`.
|
|
15
55
|
|
|
16
56
|
## Installation
|
|
17
57
|
|
|
@@ -23,8 +63,8 @@ The published package ships a prebuilt `dist/` (ESM + CJS + type
|
|
|
23
63
|
declarations) — **no build toolchain is required on your side**.
|
|
24
64
|
|
|
25
65
|
> **Versioning during the pilot.** The package is on `0.x` (beta). Pin
|
|
26
|
-
> `"@condorinst/mobile-bridge": "^0.
|
|
27
|
-
> caret range auto-updates **patches only** (`0.
|
|
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
|
|
28
68
|
> an intentional opt-in. `1.0.0` is reserved for production.
|
|
29
69
|
|
|
30
70
|
## Quickstart
|
|
@@ -87,12 +127,15 @@ BridgeClient.onBack(() => goToPreviousPage()); // fired only when you r
|
|
|
87
127
|
The `reportNavigation` / `onBack` pair lets your page own the Android back
|
|
88
128
|
button — walking your in-page navigation before the app leaves the Home. Opt-in;
|
|
89
129
|
skip it and the app just stays on Home when back is pressed. See
|
|
90
|
-
[docs/
|
|
130
|
+
[docs/bridge-contract.md](docs/bridge-contract.md).
|
|
91
131
|
|
|
92
132
|
**Differences vs `Bridge`:**
|
|
93
133
|
- Does not expose `mockEmit`, `installDevPanel`, `isMock`, `getMockState` —
|
|
94
134
|
dev/mock APIs live only on the shell.
|
|
95
|
-
- `handshake` resolves with `{ shellVersion, accepted }
|
|
135
|
+
- `handshake` resolves with `{ shellVersion, nativeVersion, accepted }`. Since
|
|
136
|
+
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.
|
|
96
139
|
- Commands not allowed by the `ShellPolicy` are silently dropped (with a
|
|
97
140
|
`console.warn`). The external client has no visibility into which commands
|
|
98
141
|
do or do not exist.
|
|
@@ -403,10 +446,22 @@ npm run build # tsup (ESM + CJS + types in dist/)
|
|
|
403
446
|
|
|
404
447
|
## Further reading
|
|
405
448
|
|
|
406
|
-
|
|
407
|
-
-
|
|
408
|
-
|
|
409
|
-
|
|
449
|
+
Every file below ships in the npm tarball, so these relative links resolve
|
|
450
|
+
inside `node_modules/@condorinst/mobile-bridge/` as well as in the repository.
|
|
451
|
+
|
|
452
|
+
| Document | What it answers |
|
|
453
|
+
| --- | --- |
|
|
454
|
+
| [`AGENTS.md`](./AGENTS.md) | The contract in one screen. Start here if you are, or are using, an AI agent. |
|
|
455
|
+
| [`llms.txt`](./llms.txt) | Index of every document, with absolute pinned URLs. |
|
|
456
|
+
| [Agent guide](./docs/agent-guide.md) | Step-by-step construction of a client Home. |
|
|
457
|
+
| [Bridge contract](./docs/bridge-contract.md) | Every key, command, error shape and capability, by native version. |
|
|
458
|
+
| [`contract/bridge-contract.json`](./contract/bridge-contract.json) | The same, as JSON. |
|
|
459
|
+
| [Limits](./docs/limits.md) | Every hard limit, and what happens when you cross it. |
|
|
460
|
+
| [Troubleshooting](./docs/troubleshooting.md) | Why a command did nothing. |
|
|
461
|
+
| [Client guide](./docs/client-guide.md) | Narrative walkthrough — build your White Label Home. |
|
|
462
|
+
| [Pilot onboarding](./docs/pilot-onboarding.md) | Checklist: zero to a delivered bundle. |
|
|
463
|
+
| [`examples/minimal-home`](./examples/minimal-home) | A runnable client in ~160 lines. |
|
|
464
|
+
| [CHANGELOG](./CHANGELOG.md) | When each capability landed — the precise record for version checks. |
|
|
410
465
|
|
|
411
466
|
## License
|
|
412
467
|
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"contractVersion": "0.19.0",
|
|
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
|
+
"audience": "Developers and AI agents building the client bundle that renders inside the Canvas Home.",
|
|
6
|
+
|
|
7
|
+
"versions": {
|
|
8
|
+
"note": "Four independent numbers coexist. Only nativeVersion tells you what the device can do.",
|
|
9
|
+
"package": "0.19.0",
|
|
10
|
+
"shellVersion": "0.6.0",
|
|
11
|
+
"clientProtocolVersion": "0.5.0",
|
|
12
|
+
"nativeVersion": "0.16.0",
|
|
13
|
+
"capabilityDetection": {
|
|
14
|
+
"how": "await BridgeClient.handshake({ clientVersion }) resolves to { shellVersion, nativeVersion, accepted }. Compare ack.nativeVersion against the minNativeVersion in capabilities[].",
|
|
15
|
+
"acceptedIsAlwaysTrue": true,
|
|
16
|
+
"acceptedWarning": "The native side never rejects a handshake, so `accepted` carries no information. Never branch on it.",
|
|
17
|
+
"nativeVersionMayBeNull": "null in a plain browser (mock mode) 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'."
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
"limits": {
|
|
22
|
+
"jsToNativePayloadBytes": {
|
|
23
|
+
"value": 65536,
|
|
24
|
+
"onExceed": "silent-drop",
|
|
25
|
+
"note": "Measured on the JSON payload. The message is discarded with no feedback to JS."
|
|
26
|
+
},
|
|
27
|
+
"jsErrorPerWindow": { "value": 20, "windowSeconds": 10, "onExceed": "silent-drop" },
|
|
28
|
+
"jsErrorFieldBytes": { "value": 4096, "onExceed": "truncate", "appliesTo": ["message", "stack"] },
|
|
29
|
+
"bundleZipBytes": { "value": 2097152, "onExceed": "app-rejects-bundle" },
|
|
30
|
+
"sensorLogLimit": { "min": 1, "max": 1000, "backendDefault": 10, "onExceed": "error-code" },
|
|
31
|
+
"timeoutsMs": {
|
|
32
|
+
"handshake": 5000,
|
|
33
|
+
"fetchSensorLog": 10000,
|
|
34
|
+
"verifySandbox": 2000,
|
|
35
|
+
"nativeVersionRelay": 2000
|
|
36
|
+
},
|
|
37
|
+
"presignedBundleUrlTtlSeconds": 86400,
|
|
38
|
+
"maxDevicesPerUser": null,
|
|
39
|
+
"maxDevicesPerUserNote": "Not enforced anywhere in the Canvas app. If a cap exists it is a backend/product rule. Do not assume one."
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
"storage": {
|
|
43
|
+
"available": false,
|
|
44
|
+
"reason": "The shell is loaded with loadHtmlString and no base URL, so its origin is opaque; the client runs in <iframe sandbox=\"allow-scripts\"> from srcdoc, so its origin is null.",
|
|
45
|
+
"localStorage": "throws SecurityError",
|
|
46
|
+
"sessionStorage": "throws SecurityError",
|
|
47
|
+
"indexedDB": "unavailable",
|
|
48
|
+
"cookies": "writes are no-ops",
|
|
49
|
+
"serviceWorker": "cannot be registered",
|
|
50
|
+
"guidance": "Hold all state in memory. It is lost whenever the WebView reloads. Re-request what you need through the bridge on startup."
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
"csp": {
|
|
54
|
+
"policy": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; frame-src 'self'; object-src 'none'; base-uri 'none'; form-action 'none';",
|
|
55
|
+
"inheritedBySrcdoc": true,
|
|
56
|
+
"cannotBeRelaxedByClient": true,
|
|
57
|
+
"consequences": [
|
|
58
|
+
"No 'unsafe-eval': eval() and new Function() are blocked.",
|
|
59
|
+
"connect-src 'self' on a null origin blocks every fetch, XHR and WebSocket. There is no network from the client.",
|
|
60
|
+
"Images and fonts must be inline or data: URIs."
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
"sandbox": {
|
|
65
|
+
"iframeAttribute": "sandbox=\"allow-scripts\"",
|
|
66
|
+
"unavailable": [
|
|
67
|
+
"window.parent property access (postMessage is the only permitted use, and BridgeClient does it for you)",
|
|
68
|
+
"window.top (no allow-top-navigation)",
|
|
69
|
+
"window.open (no allow-popups)",
|
|
70
|
+
"navigator.sendBeacon (neutralized by the shell before the client runs)",
|
|
71
|
+
"window.CondorNative, window.CondorBridge, window.flutter_inappwebview (deleted before the client runs)",
|
|
72
|
+
"every native WebView permission: camera, microphone and geolocation are denied unconditionally"
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
"keys": {
|
|
77
|
+
"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.",
|
|
78
|
+
"entries": {
|
|
79
|
+
"pingPong": { "reachableByClient": true, "payloadType": "PingPongPayload", "emittedWhen": "reply to the `ping` command" },
|
|
80
|
+
"userDevice": { "reachableByClient": true, "payloadType": "UserDevicePayload", "emittedWhen": "reply to the `userDevice` command, and on device roster changes" },
|
|
81
|
+
"deviceLightSensors": { "reachableByClient": true, "payloadType": "DeviceLightSensorsPayload", "emittedWhen": "real-time stream, only while realTime is on for that device", "discriminator": "type", "variants": ["actlumus", "actlumus2"] },
|
|
82
|
+
"syncTime": { "reachableByClient": true, "payloadType": "SyncTimeAckPayload", "emittedWhen": "ack of the `syncTime` command" },
|
|
83
|
+
"realTime": { "reachableByClient": true, "payloadType": "RealTimeAckPayload", "emittedWhen": "ack of the `realTime` command" },
|
|
84
|
+
"deleteMemory": { "reachableByClient": true, "payloadType": "DeleteMemoryAckPayload", "emittedWhen": "ack of the `deleteMemory` command" },
|
|
85
|
+
"deviceNotification": { "reachableByClient": true, "payloadType": "DeviceNotificationPayload", "emittedWhen": "device-level notice, including errors" },
|
|
86
|
+
"deviceConnection": { "reachableByClient": true, "payloadType": "DeviceConnectionPayload", "emittedWhen": "a device connects or disconnects" },
|
|
87
|
+
"sensorLog": { "reachableByClient": true, "payloadType": "SensorLogPayload", "emittedWhen": "reply to the `sensorLog` command; correlate by requestId" },
|
|
88
|
+
"systemStatus": { "reachableByClient": true, "payloadType": "SystemStatusPayload", "emittedWhen": "on attach, again right after handshake-ack, and on every change" },
|
|
89
|
+
"navigationBack": { "reachableByClient": true, "payloadType": "NavigationBackPayload", "emittedWhen": "the OS back button was pressed and you had reported canGoBack: true" },
|
|
90
|
+
"language": { "reachableByClient": true, "payloadType": "LanguagePayload", "emittedWhen": "on attach, again right after handshake-ack, and on every change in Settings", "minNativeVersion": "0.14.0" },
|
|
91
|
+
"dfu": { "reachableByClient": false, "payloadType": null, "deniedBy": "ShellPolicy.deniedCommands and skipped natively", "note": "Firmware update traffic. A subscribe attempt is answered with nack reason not-read-only." }
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
"commands": {
|
|
96
|
+
"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.",
|
|
97
|
+
"entries": {
|
|
98
|
+
"ping": { "args": {}, "requiresConfirmation": false, "repliesOn": "pingPong" },
|
|
99
|
+
"userDevice": { "args": { "getFromServer": "boolean, default false" }, "requiresConfirmation": false, "repliesOn": "userDevice" },
|
|
100
|
+
"realTime": { "args": { "serialNo": "int", "value": "boolean" }, "requiresConfirmation": false, "repliesOn": "realTime", "note": "Off by default. The deviceLightSensors stream only flows while this is on." },
|
|
101
|
+
"deviceConnection": { "args": { "type": "'add' | 'remove'", "serialNo": "int" }, "requiresConfirmation": false, "repliesOn": "deviceConnection" },
|
|
102
|
+
"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." },
|
|
103
|
+
"openSettings": { "args": {}, "requiresConfirmation": false, "repliesOn": null },
|
|
104
|
+
"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." },
|
|
105
|
+
"setStatusBarColor": { "args": { "color": "'#RRGGBB'" }, "requiresConfirmation": false, "repliesOn": null },
|
|
106
|
+
"navigationState": { "args": { "canGoBack": "boolean" }, "requiresConfirmation": false, "repliesOn": null, "prefer": "BridgeClient.reportNavigation(canGoBack)" },
|
|
107
|
+
"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" },
|
|
108
|
+
"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." },
|
|
109
|
+
"syncTime": { "args": { "serialNo": "int" }, "requiresConfirmation": true, "repliesOn": "syncTime" },
|
|
110
|
+
"deleteMemory": { "args": { "serialNo": "int" }, "requiresConfirmation": true, "repliesOn": "deleteMemory" },
|
|
111
|
+
"logout": { "args": {}, "requiresConfirmation": true, "repliesOn": null },
|
|
112
|
+
"dfu": { "args": {}, "denied": true, "repliesOn": null }
|
|
113
|
+
},
|
|
114
|
+
"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."
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
"errorShapes": [
|
|
118
|
+
{
|
|
119
|
+
"name": "nack",
|
|
120
|
+
"channel": "shell -> client postMessage",
|
|
121
|
+
"shape": { "type": "nack", "key": "string, optional", "command": "string, optional", "reason": "string" },
|
|
122
|
+
"reasons": ["not-read-only", "denied", "no-confirmation-handler", "confirmation-error", "user-rejected", "not-allowed"],
|
|
123
|
+
"visibility": "console.warn only. BridgeClient.send does not throw and does not return a status."
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"name": "sensorLog failure",
|
|
127
|
+
"channel": "sensorLog key",
|
|
128
|
+
"shape": { "requestId": "string", "success": false, "error": "string code" },
|
|
129
|
+
"codes": ["CONFLICTING_CURSORS", "INVALID_BODY_PART", "INVALID_LIMIT", "INVALID_START_TIME", "INVALID_END_TIME", "INVALID_TIME_RANGE", "MISSING_DEVICE_SERIAL_NO", "invalid session", "device not found for this user"],
|
|
130
|
+
"visibility": "fetchSensorLog RESOLVES with success false. It only rejects on timeout.",
|
|
131
|
+
"warning": "error is a diagnostic code, never user-facing text. Map it to your own copy."
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"name": "silent drop",
|
|
135
|
+
"channel": "none",
|
|
136
|
+
"shape": null,
|
|
137
|
+
"visibility": "Nothing reaches JS. This is the dominant failure mode of the platform."
|
|
138
|
+
}
|
|
139
|
+
],
|
|
140
|
+
|
|
141
|
+
"silentDropPaths": [
|
|
142
|
+
{ "cause": "shell nonce missing or wrong", "trigger": "a message reached the native channel from anywhere but the Condor shell, e.g. a nested iframe you created", "observable": "nothing" },
|
|
143
|
+
{ "cause": "payload over 65536 bytes", "trigger": "oversized command args", "observable": "nothing" },
|
|
144
|
+
{ "cause": "unknown command type", "trigger": "typo, or a command added in a newer nativeVersion than the device runs", "observable": "nothing" },
|
|
145
|
+
{ "cause": "permission name outside the native enum", "trigger": "requestPermission with an unrecognized string", "observable": "nothing" },
|
|
146
|
+
{ "cause": "malformed args", "trigger": "realTime, syncTime, deleteMemory or deviceConnection with wrong types", "observable": "nothing" },
|
|
147
|
+
{ "cause": "postMessage failing schema validation", "trigger": "hand-rolled postMessage instead of BridgeClient", "observable": "nothing" }
|
|
148
|
+
],
|
|
149
|
+
|
|
150
|
+
"capabilities": [
|
|
151
|
+
{ "feature": "languageKey", "minNativeVersion": "0.14.0", "description": "The language key and the language pull command exist." },
|
|
152
|
+
{ "feature": "sensorLogTimeRange", "minNativeVersion": "0.15.0", "description": "sensorLog accepts startTime and endTime." },
|
|
153
|
+
{ "feature": "lightSensorRawChannels", "minNativeVersion": "0.16.0", "description": "deviceLightSensors carries the raw light_sensor_* channels and the actlumus2 variant." },
|
|
154
|
+
{ "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." }
|
|
155
|
+
],
|
|
156
|
+
|
|
157
|
+
"bundle": {
|
|
158
|
+
"artifact": ".zip containing exactly one index.html at the root",
|
|
159
|
+
"rules": [
|
|
160
|
+
{ "id": "entry", "rule": "index.html at the archive root", "enforcedBy": "app", "onViolation": "reject (falls back to the first .html found, then to the default screen)" },
|
|
161
|
+
{ "id": "self-contained-js", "rule": "no <script src> pointing anywhere but a data: URI", "enforcedBy": "app", "onViolation": "reject" },
|
|
162
|
+
{ "id": "self-contained-css", "rule": "no <link rel=stylesheet href> pointing anywhere but a data: URI", "enforcedBy": "app", "onViolation": "reject" },
|
|
163
|
+
{ "id": "size", "rule": "the .zip is at most 2097152 bytes", "enforcedBy": "app", "onViolation": "reject" },
|
|
164
|
+
{ "id": "single-chunk", "rule": "build without code splitting or ES module imports between chunks", "enforcedBy": "runtime", "onViolation": "blank screen inside srcdoc" },
|
|
165
|
+
{ "id": "media", "rule": "embed images, fonts and media as data: URIs", "enforcedBy": "runtime", "onViolation": "broken assets" }
|
|
166
|
+
],
|
|
167
|
+
"integrity": { "algorithm": "sha256", "format": "sha256-<lowercase hex>", "computedOver": "the exact bytes of the .zip" },
|
|
168
|
+
"tooling": {
|
|
169
|
+
"build": "npx condor-mobile-bridge build -i dist/index.html -o dist/bundle.zip",
|
|
170
|
+
"validate": "npx condor-mobile-bridge validate -i dist/bundle.zip --strict"
|
|
171
|
+
},
|
|
172
|
+
"statusBarHook": "<meta name=\"theme-color\" content=\"#RRGGBB\"> in the bundle sets the native status bar colour. The setStatusBarColor command overrides it at runtime."
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
"verifiedAgainst": {
|
|
176
|
+
"monorepo": "condorinstprogdev/mobile_applications",
|
|
177
|
+
"branch": "dev_canvas",
|
|
178
|
+
"commit": "a6b19cd29704db241c29dd71b267f712610cb013",
|
|
179
|
+
"verifiedOn": "2026-09-22",
|
|
180
|
+
"sources": {
|
|
181
|
+
"nativeVersion": "apps/canvas/lib/ui/pages/home/bridge/condor_bridge.dart:32",
|
|
182
|
+
"jsToNativePayloadBytes": "apps/canvas/lib/ui/pages/home/bridge/condor_bridge.dart:88",
|
|
183
|
+
"jsErrorPerWindow": "apps/canvas/lib/ui/pages/home/bridge/condor_bridge.dart:93-94",
|
|
184
|
+
"jsErrorFieldBytes": "apps/canvas/lib/ui/pages/home/bridge/condor_bridge.dart:97",
|
|
185
|
+
"acceptedIsAlwaysTrue": "apps/canvas/lib/ui/pages/home/bridge/condor_bridge.dart:417-425",
|
|
186
|
+
"silentDropPaths": "apps/canvas/lib/ui/pages/home/bridge/condor_bridge.dart:218-238,291-295,452-467,529-536",
|
|
187
|
+
"bundleZipBytes": "apps/canvas/lib/core/config/whitelabel_bundle_limits.dart:14",
|
|
188
|
+
"bundleGuards": "apps/canvas/lib/data/services/whitelabel_bundle_service.dart:227-398",
|
|
189
|
+
"integrity": "apps/canvas/lib/data/services/whitelabel_bundle_service.dart:244-259",
|
|
190
|
+
"sensorLogValidation": "apps/canvas/lib/domain/entities/sensor_log_entity.dart:70-85",
|
|
191
|
+
"csp": "apps/canvas/web-mock-src/shell.template.html:28",
|
|
192
|
+
"shellPolicy": "apps/canvas/web-mock-src/src/shell.ts:48-98",
|
|
193
|
+
"webViewPermissionDeny": "apps/canvas/lib/ui/pages/home/home_page.dart:260-268",
|
|
194
|
+
"opaqueOrigin": "apps/canvas/lib/ui/pages/home/home_page.dart:296-298"
|
|
195
|
+
},
|
|
196
|
+
"maintenance": "Re-verify every entry in sources before cutting a release. A published contract that drifts from the app is worse than no contract."
|
|
197
|
+
}
|
|
198
|
+
}
|