@condorinst/mobile-bridge 0.17.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 +93 -0
- package/README.md +75 -16
- 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 +107 -70
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +476 -373
- package/dist/index.d.ts +476 -373
- package/dist/index.js +105 -69
- 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/dist/index.d.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import * as v from 'valibot';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Mock adapter —
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* - `callHandler`
|
|
9
|
-
*
|
|
10
|
-
* listeners
|
|
11
|
-
* - `triggerEmit`
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* - `getMessages`
|
|
15
|
-
* - `clearMessages`
|
|
16
|
-
* - `onCallHandler(listener)`
|
|
17
|
-
*
|
|
18
|
-
*
|
|
4
|
+
* Mock adapter — active when no native host is detected: a plain browser,
|
|
5
|
+
* jsdom without an installed host, Storybook, and so on.
|
|
6
|
+
*
|
|
7
|
+
* Behaviour:
|
|
8
|
+
* - `callHandler` records `{ handlerName, payload, timestamp }` in an internal
|
|
9
|
+
* buffer (capped at 100, FIFO), logs through `console.debug`, and dispatches
|
|
10
|
+
* to listeners registered with `onCallHandler`.
|
|
11
|
+
* - `triggerEmit` dispatches through `window.CondorBridge.emit`, so the real
|
|
12
|
+
* bridge treats it as if it came from the native side: `handshake-ack`
|
|
13
|
+
* resolves the handshake, every other key reaches its subscribers.
|
|
14
|
+
* - `getMessages` returns a read-only snapshot of the buffer.
|
|
15
|
+
* - `clearMessages` empties it, which is useful in tests.
|
|
16
|
+
* - `onCallHandler(listener)` registers a listener invoked on every send and
|
|
17
|
+
* returns an unsubscribe function — used by the dev panel's auto-respond
|
|
18
|
+
* toggle, and for assertions in tests.
|
|
19
19
|
*/
|
|
20
20
|
interface MockMessage {
|
|
21
21
|
handlerName: string;
|
|
@@ -24,27 +24,26 @@ interface MockMessage {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* Dev panel — UI
|
|
27
|
+
* Dev panel — a floating UI for inspecting the bridge in mock mode.
|
|
28
28
|
*
|
|
29
|
-
* **Opt-in:**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* **Opt-in:** call `Bridge.installDevPanel(opts?)` when you want it. It only
|
|
30
|
+
* works in mock mode and throws in real mode. It injects into `document.body`
|
|
31
|
+
* and is idempotent — re-installing removes the previous panel.
|
|
32
32
|
*
|
|
33
|
-
*
|
|
34
|
-
* -
|
|
35
|
-
* -
|
|
36
|
-
* -
|
|
33
|
+
* Contents:
|
|
34
|
+
* - a header with a "MOCK" badge, a send counter, and a minimise button;
|
|
35
|
+
* - a sent log with the last N messages (timestamp, type, truncated payload);
|
|
36
|
+
* - a trigger-emit control: a `BRIDGE_KEYS` dropdown, a payload textarea, and
|
|
37
|
+
* a button.
|
|
37
38
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
39
|
+
* Styles are inline and classes are prefixed `condor-mock-panel-*`, to reduce
|
|
40
|
+
* collisions with the client's own CSS.
|
|
40
41
|
*/
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* @since v0.3.0
|
|
44
|
+
* A reply emitted automatically in reaction to a `Bridge.send`, returned by the
|
|
45
|
+
* client's `onSendForAutoRespond` policy. `null` means "do not answer this
|
|
46
|
+
* send".
|
|
48
47
|
*/
|
|
49
48
|
interface AutoRespondResponse {
|
|
50
49
|
key: string;
|
|
@@ -56,43 +55,81 @@ interface DevPanelOptions {
|
|
|
56
55
|
theme?: 'dark' | 'light';
|
|
57
56
|
startCollapsed?: boolean;
|
|
58
57
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* `
|
|
62
|
-
* após delay, ou `null` para não responder.
|
|
58
|
+
* Auto-respond policy: invoked on every `Bridge.send` while the panel's
|
|
59
|
+
* "Auto-respond" toggle is on. Return `{ key, payload }` to fire
|
|
60
|
+
* `Bridge.mockEmit(key, payload)` after the delay, or `null` to stay silent.
|
|
63
61
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* @since v0.3.0
|
|
62
|
+
* Without this callback the "Auto-respond" checkbox does not appear.
|
|
67
63
|
*/
|
|
68
64
|
onSendForAutoRespond?: (msg: MockMessage) => AutoRespondResponse | null;
|
|
69
65
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* @since v0.3.0
|
|
66
|
+
* Initial state of the "Auto-respond" toggle. Defaults to `false` — manual
|
|
67
|
+
* stays the default, and the client decides when to turn it on.
|
|
74
68
|
*/
|
|
75
69
|
startAutoRespondEnabled?: boolean;
|
|
76
70
|
/**
|
|
77
|
-
* Delay
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* @since v0.3.0
|
|
71
|
+
* Delay in milliseconds between the `send` and the auto-response `mockEmit`.
|
|
72
|
+
* Defaults to `50`, which approximates the IPC latency of the real native
|
|
73
|
+
* bridge.
|
|
81
74
|
*/
|
|
82
75
|
autoRespondDelayMs?: number;
|
|
83
76
|
}
|
|
84
77
|
|
|
85
78
|
/**
|
|
86
|
-
*
|
|
79
|
+
* Native host detection, behind an adapter.
|
|
80
|
+
*
|
|
81
|
+
* Detection order: the `window.CondorNative` JavaScript channel
|
|
82
|
+
* (`webview_flutter`) first, then the legacy `window.flutter_inappwebview`,
|
|
83
|
+
* then the mock. The channel only accepts strings, so the message travels as
|
|
84
|
+
* JSON. `setOutgoingStamp` lets the shell stamp every outgoing message — the
|
|
85
|
+
* nonce — without monkey-patching the host.
|
|
86
|
+
*
|
|
87
|
+
* With no host at all the mock adapter activates, which is what makes browser
|
|
88
|
+
* development possible without building an APK.
|
|
87
89
|
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
90
|
+
* The active adapter is cached on the first `getActiveAdapter()` call. Tests
|
|
91
|
+
* should call `__resetAdapterForTests()` in `beforeEach` to force
|
|
92
|
+
* re-detection, for instance after installing or removing
|
|
93
|
+
* `window.CondorNative`.
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Name of the JavaScript channel the native side registers with
|
|
98
|
+
* `WebViewController.addJavaScriptChannel`.
|
|
91
99
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
|
|
100
|
+
* It cannot be `CondorBridge`: the plugin publishes the channel as
|
|
101
|
+
* `window.<name>`, which would collide with the `window.CondorBridge` emitter
|
|
102
|
+
* this SDK installs.
|
|
103
|
+
*/
|
|
104
|
+
declare const NATIVE_CHANNEL_NAME = "CondorNative";
|
|
105
|
+
interface FlutterInAppWebView {
|
|
106
|
+
callHandler(name: string, ...args: unknown[]): Promise<unknown>;
|
|
107
|
+
}
|
|
108
|
+
interface NativeChannel {
|
|
109
|
+
postMessage(message: string): void;
|
|
110
|
+
}
|
|
111
|
+
declare global {
|
|
112
|
+
interface Window {
|
|
113
|
+
flutter_inappwebview?: FlutterInAppWebView;
|
|
114
|
+
CondorNative?: NativeChannel;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Transforma toda mensagem JS → Native imediatamente antes do envio.
|
|
119
|
+
* Recebe e devolve o envelope `{type, version, payload}`.
|
|
120
|
+
*/
|
|
121
|
+
type OutgoingStamp = (message: Record<string, unknown>) => Record<string, unknown>;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Catalogue of the JS↔Native bridge keys.
|
|
125
|
+
*
|
|
126
|
+
* Mirrors the JS-facing side of `ForegroundComCoreKeys` in `condor_core`.
|
|
127
|
+
* The strings here are what JavaScript receives through
|
|
128
|
+
* `window.CondorBridge.emit(key, payload)`.
|
|
129
|
+
*
|
|
130
|
+
* Thirteen keys exist. A sandboxed client can subscribe to twelve of them:
|
|
131
|
+
* `dfu` is denied by the shell policy and skipped natively. See
|
|
132
|
+
* `docs/bridge-contract.md` for what each one carries and when it is emitted.
|
|
96
133
|
*/
|
|
97
134
|
declare const BRIDGE_KEYS: {
|
|
98
135
|
readonly pingPong: "pingPong";
|
|
@@ -107,10 +144,10 @@ declare const BRIDGE_KEYS: {
|
|
|
107
144
|
readonly dfu: "dfu";
|
|
108
145
|
readonly systemStatus: "systemStatus";
|
|
109
146
|
/**
|
|
110
|
-
* Native → client:
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
147
|
+
* Native → client: the OS back button was pressed and the client had
|
|
148
|
+
* reported internal history via `reportNavigation(true)`. Go back exactly
|
|
149
|
+
* ONE internal page. Emitted only when the native side decided to delegate
|
|
150
|
+
* the back gesture. See `BridgeClient.onBack`.
|
|
114
151
|
*/
|
|
115
152
|
readonly navigationBack: "navigationBack";
|
|
116
153
|
/**
|
|
@@ -118,21 +155,24 @@ declare const BRIDGE_KEYS: {
|
|
|
118
155
|
* AND the language explicitly chosen in the app Settings, so the client can
|
|
119
156
|
* apply its own precedence rule — including rendering a language the native
|
|
120
157
|
* chrome does not support. Emitted on attach, after `handshake-ack`, and on
|
|
121
|
-
* every change in Settings.
|
|
158
|
+
* every change in Settings. See `LanguagePayload`.
|
|
122
159
|
*/
|
|
123
160
|
readonly language: "language";
|
|
124
161
|
};
|
|
125
162
|
type BridgeKey = (typeof BRIDGE_KEYS)[keyof typeof BRIDGE_KEYS];
|
|
126
163
|
|
|
127
164
|
/**
|
|
128
|
-
*
|
|
165
|
+
* Wire messages of the bridge protocol.
|
|
129
166
|
*
|
|
130
|
-
*
|
|
131
|
-
* `
|
|
167
|
+
* Mirrors the Dart side (`BridgeIncomingMessage` / `BridgeOutgoingEvent` in
|
|
168
|
+
* the Canvas app's `bridge_message.dart`).
|
|
132
169
|
*/
|
|
133
170
|
/**
|
|
134
|
-
*
|
|
135
|
-
* `window.
|
|
171
|
+
* JS → native message, sent over
|
|
172
|
+
* `window.CondorNative.postMessage(JSON.stringify(msg))` (0.18.0+) or, on a
|
|
173
|
+
* legacy host, `window.flutter_inappwebview.callHandler('CondorBridge', msg)`.
|
|
174
|
+
*
|
|
175
|
+
* Anything above 64 KB is discarded by the native side without a reply.
|
|
136
176
|
*/
|
|
137
177
|
interface BridgeIncomingMessage {
|
|
138
178
|
type: string;
|
|
@@ -140,56 +180,60 @@ interface BridgeIncomingMessage {
|
|
|
140
180
|
payload?: Record<string, unknown>;
|
|
141
181
|
}
|
|
142
182
|
/**
|
|
143
|
-
*
|
|
144
|
-
* `window.CondorBridge.emit(key, payload)`.
|
|
183
|
+
* Native → JS event, delivered through `window.CondorBridge.emit(key, payload)`.
|
|
145
184
|
*/
|
|
146
185
|
interface BridgeOutgoingEvent {
|
|
147
186
|
key: string;
|
|
148
187
|
payload: unknown;
|
|
149
188
|
}
|
|
150
189
|
/**
|
|
151
|
-
*
|
|
190
|
+
* Handshake reply (`handshake-ack`).
|
|
152
191
|
*
|
|
153
|
-
* `nativeVersion`:
|
|
154
|
-
*
|
|
155
|
-
*
|
|
192
|
+
* - `nativeVersion`: the semver schema version the native side declares. This
|
|
193
|
+
* is the ONLY capability signal available; compare it against the minimum
|
|
194
|
+
* version of the feature you want to use.
|
|
195
|
+
* - `accepted`: whether the native side accepted the `clientVersion` you sent.
|
|
196
|
+
* There is no hard semver validation — it is **always** `true`, so it carries
|
|
197
|
+
* no information. Never branch on it.
|
|
156
198
|
*/
|
|
157
199
|
interface HandshakeAck {
|
|
158
200
|
nativeVersion: string;
|
|
159
201
|
accepted: boolean;
|
|
160
202
|
}
|
|
203
|
+
/** Options for {@link CondorBridge.handshake}. */
|
|
161
204
|
interface HandshakeOptions {
|
|
205
|
+
/** Your own client version, for the native logs. Not validated. */
|
|
162
206
|
clientVersion: string;
|
|
163
207
|
}
|
|
164
208
|
|
|
165
209
|
/**
|
|
166
|
-
*
|
|
210
|
+
* Per-key typed payload catalogue.
|
|
167
211
|
*
|
|
168
|
-
* Shapes
|
|
169
|
-
*
|
|
170
|
-
*
|
|
212
|
+
* Shapes are derived from the `condor_core` source in the
|
|
213
|
+
* `mobile_applications` monorepo, and are authoritative for what arrives on
|
|
214
|
+
* each key.
|
|
171
215
|
*
|
|
172
|
-
* **
|
|
173
|
-
*
|
|
174
|
-
*
|
|
216
|
+
* **Direction:** native → JS (observable events). JS → native commands
|
|
217
|
+
* (`send`) stay permissive (`Record<string, unknown>`); the typed helpers on
|
|
218
|
+
* `BridgeClient` are the ergonomic path for the ones that matter.
|
|
175
219
|
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
* `Record<string, unknown>` — hub-only, reservado).
|
|
220
|
+
* `PayloadOf<K>` maps a catalogued key to its payload, with a permissive
|
|
221
|
+
* fallback for uncatalogued keys (`dfu` stays `Record<string, unknown>` — it
|
|
222
|
+
* is hub-only and a sandboxed client never receives it).
|
|
180
223
|
*/
|
|
181
|
-
/**
|
|
224
|
+
/** Boot / keepalive heartbeat. */
|
|
182
225
|
interface PingPongPayload {
|
|
183
226
|
data: 'ping' | 'pong';
|
|
184
227
|
}
|
|
185
228
|
/**
|
|
186
|
-
* Snapshot
|
|
187
|
-
*
|
|
229
|
+
* Snapshot of the user's device store. Mirrors `UserDevices.toMap()` in
|
|
230
|
+
* `condor_core`.
|
|
231
|
+
*
|
|
232
|
+
* The inner arrays are `Record<string, unknown>`: each item has its own
|
|
233
|
+
* `toMap()` on the Dart side (device config, telemetry, user config), and
|
|
234
|
+
* strict typing for them is deferred until a client needs it.
|
|
188
235
|
*
|
|
189
|
-
*
|
|
190
|
-
* tem seu próprio `toMap()` (DeviceConfig, DeviceTelemetry,
|
|
191
|
-
* DeviceUserConfig) cujo strict typing fica para release futura quando
|
|
192
|
-
* consumidor JS precisar.
|
|
236
|
+
* There is no device cap in the app. Do not assume this list is bounded.
|
|
193
237
|
*/
|
|
194
238
|
interface UserDevicePayload {
|
|
195
239
|
lastUpdate: number;
|
|
@@ -198,23 +242,22 @@ interface UserDevicePayload {
|
|
|
198
242
|
devicesUserConfig: Array<Record<string, unknown>>;
|
|
199
243
|
}
|
|
200
244
|
/**
|
|
201
|
-
*
|
|
202
|
-
* `user_cmd=log` —
|
|
245
|
+
* Raw light-sensor channels, under the names the backend uses in
|
|
246
|
+
* `user_cmd=log` — the same vocabulary for history and for real time.
|
|
203
247
|
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
248
|
+
* Present from **nativeVersion 0.16.0**. A bundle compiled against this SDK can
|
|
249
|
+
* run on an older native app that does not emit them: check `nativeVersion`
|
|
250
|
+
* from the handshake ack before depending on them, or read defensively. They
|
|
251
|
+
* are deliberately **not** optional in the type — optional would hide the
|
|
252
|
+
* version skew instead of making it explicit.
|
|
209
253
|
*
|
|
210
|
-
* ⚠️
|
|
211
|
-
* `0.16.x`
|
|
212
|
-
*
|
|
213
|
-
* iframe.
|
|
254
|
+
* ⚠️ `nativeVersion` only reaches the client from **SDK 0.17.0**: up to
|
|
255
|
+
* `0.16.x` the shell answered the client handshake with `shellVersion` alone,
|
|
256
|
+
* so this check was impossible to satisfy from inside the iframe.
|
|
214
257
|
*
|
|
215
|
-
* `null`
|
|
216
|
-
* zero"
|
|
217
|
-
* `
|
|
258
|
+
* `null` means **the channel does not exist on this hardware generation**,
|
|
259
|
+
* never "the reading was zero" — `0.0` is a valid reading. Coercing `null` to
|
|
260
|
+
* `0` fabricates data.
|
|
218
261
|
*/
|
|
219
262
|
type LightSensorsRawChannels = {
|
|
220
263
|
light_sensor_f1: number;
|
|
@@ -225,18 +268,18 @@ type LightSensorsRawChannels = {
|
|
|
225
268
|
light_sensor_f6: number;
|
|
226
269
|
light_sensor_f7: number;
|
|
227
270
|
light_sensor_f8: number;
|
|
228
|
-
/**
|
|
271
|
+
/** Total light (`visible` on the device wire format). */
|
|
229
272
|
light_sensor_clear: number;
|
|
230
|
-
/**
|
|
273
|
+
/** Near infrared. */
|
|
231
274
|
light_sensor_nir: number;
|
|
232
275
|
};
|
|
233
276
|
/**
|
|
234
|
-
*
|
|
277
|
+
* A real-time reading from an ActLumus gen1 (`type: 'actlumus'`).
|
|
235
278
|
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
279
|
+
* Mirrors `LightSensorsActlumus1GraphicData.toMap()` in `condor_interactors`.
|
|
280
|
+
* Gen1 has neither the tristimulus channels nor flicker detect — those four
|
|
281
|
+
* keys arrive as `null`, present in the schema so that its shape does not
|
|
282
|
+
* change between hardware generations.
|
|
240
283
|
*/
|
|
241
284
|
type LightSensorsActlumus1Data = LightSensorsRawChannels & {
|
|
242
285
|
time: number;
|
|
@@ -258,17 +301,17 @@ type LightSensorsActlumus1Data = LightSensorsRawChannels & {
|
|
|
258
301
|
light_sensor_fd: null;
|
|
259
302
|
};
|
|
260
303
|
/**
|
|
261
|
-
*
|
|
304
|
+
* A real-time reading from an ActLumus Plus / gen2 (`type: 'actlumus2'`).
|
|
262
305
|
*
|
|
263
|
-
*
|
|
264
|
-
* (`packages/condor_interactors/lib/src/domain/entities/actlumus/2/light_sensors2_graphic_data_entity.dart`).
|
|
265
|
-
* ⚠️ Os comprimentos de onda **não** são os mesmos do gen1 — são 14 (395…800)
|
|
266
|
-
* contra 10 (390…750), sem nenhum valor em comum. Um gráfico que leia a lista
|
|
267
|
-
* do gen1 num device gen2 encontra `undefined` em todas as séries e renderiza
|
|
268
|
-
* vazio; discrimine sempre pelo `type`.
|
|
306
|
+
* Mirrors `LightSensorsActlumus2GraphicData.toMap()` in `condor_interactors`.
|
|
269
307
|
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
308
|
+
* ⚠️ The wavelengths are **not** the gen1 ones: 14 of them (395…800) against
|
|
309
|
+
* 10 (390…750), with no value in common. A chart that reads the gen1 list on a
|
|
310
|
+
* gen2 device finds `undefined` in every series and renders empty. Always
|
|
311
|
+
* discriminate on `type`.
|
|
312
|
+
*
|
|
313
|
+
* `light_sensor_z/_y/_x` are the CIE 1931 tristimulus values — Z in the blue,
|
|
314
|
+
* Y in the green, X in the red.
|
|
272
315
|
*/
|
|
273
316
|
type LightSensorsActlumus2Data = LightSensorsRawChannels & {
|
|
274
317
|
time: number;
|
|
@@ -291,17 +334,20 @@ type LightSensorsActlumus2Data = LightSensorsRawChannels & {
|
|
|
291
334
|
light_sensor_z: number;
|
|
292
335
|
light_sensor_y: number;
|
|
293
336
|
light_sensor_x: number;
|
|
294
|
-
/** Flicker detect.
|
|
337
|
+
/** Flicker detect. Not part of the spectrum matrix. */
|
|
295
338
|
light_sensor_fd: number;
|
|
296
339
|
};
|
|
297
340
|
/**
|
|
298
|
-
*
|
|
299
|
-
* `{serialNo, type, data
|
|
341
|
+
* Real-time light-sensor stream. Shape:
|
|
342
|
+
* `{ serialNo, type, data }`.
|
|
343
|
+
*
|
|
344
|
+
* A union discriminated by `type`, which is the `DeviceType.name` on the Dart
|
|
345
|
+
* side. `data` carries **both the derived spectrum and the raw channels it came
|
|
346
|
+
* from** — before 0.16.0 only the spectrum crossed the bridge, and a probe
|
|
347
|
+
* looking up a raw channel by name read `undefined`.
|
|
300
348
|
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
* até a `0.16.0` só o espectro atravessava, e uma sonda que procurasse um canal
|
|
304
|
-
* cru pelo nome lia `undefined`.
|
|
349
|
+
* This stream only flows while real-time is on for that device; it is off by
|
|
350
|
+
* default. See the `realTime` command.
|
|
305
351
|
*/
|
|
306
352
|
type DeviceLightSensorsPayload = {
|
|
307
353
|
serialNo: number;
|
|
@@ -329,16 +375,17 @@ interface DeleteMemoryAckPayload {
|
|
|
329
375
|
success: boolean;
|
|
330
376
|
}
|
|
331
377
|
/**
|
|
332
|
-
*
|
|
333
|
-
*
|
|
378
|
+
* BLE connection status. Mirrors the `openConnection` subscriber in the app's
|
|
379
|
+
* device notification service.
|
|
334
380
|
*/
|
|
335
381
|
interface DeviceConnectionPayload {
|
|
336
382
|
serialNo: number;
|
|
337
383
|
isConnect: boolean;
|
|
338
384
|
}
|
|
339
385
|
/**
|
|
340
|
-
*
|
|
341
|
-
*
|
|
386
|
+
* Device event or status notice. Mirrors `DevicesNotification.toMap()`.
|
|
387
|
+
* `message` is already localized by the app; `messageCode` is the stable
|
|
388
|
+
* identifier to branch on.
|
|
342
389
|
*/
|
|
343
390
|
interface DeviceNotificationPayload {
|
|
344
391
|
serialNo: number;
|
|
@@ -348,46 +395,52 @@ interface DeviceNotificationPayload {
|
|
|
348
395
|
isError: boolean;
|
|
349
396
|
}
|
|
350
397
|
/**
|
|
351
|
-
*
|
|
398
|
+
* Parameters of the `sensorLog` command.
|
|
352
399
|
*
|
|
353
|
-
* `startTime
|
|
354
|
-
* `endTime` **
|
|
355
|
-
* (
|
|
400
|
+
* `startTime` / `endTime` delimit a period — `startTime` is **inclusive**,
|
|
401
|
+
* `endTime` **exclusive**, both in the strict format `YYYY-MM-DD HH:MM:SS`
|
|
402
|
+
* (no `T`, no offset, no date-only form). Requires nativeVersion >= 0.15.0.
|
|
356
403
|
*
|
|
357
|
-
* ⚠️
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
404
|
+
* ⚠️ These timestamps are **device-local time, not UTC**: `log_time` passes
|
|
405
|
+
* through from the device RTC. Build the range with local `Date` getters
|
|
406
|
+
* (`getFullYear()`, `getHours()`, …) and **never** with `toISOString()` —
|
|
407
|
+
* converting to UTC shifts every series by the timezone offset, producing
|
|
408
|
+
* plausible data and no visible error.
|
|
362
409
|
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
410
|
+
* The `startingAfter` / `endingBefore` cursors are **opaque** and mutually
|
|
411
|
+
* exclusive: echo back the value you received, never construct one.
|
|
365
412
|
*/
|
|
366
413
|
interface SensorLogQueryPayload {
|
|
367
|
-
/**
|
|
414
|
+
/** Echoed back in the reply — correlates concurrent requests. */
|
|
368
415
|
requestId: string;
|
|
369
416
|
serialNo: number;
|
|
370
|
-
/** `wrist`
|
|
417
|
+
/** `wrist` or `necklace`. Omit for no filter. */
|
|
371
418
|
bodyPart?: string;
|
|
372
|
-
/** 1..1000 (default
|
|
419
|
+
/** 1..1000 (backend default is 10). Outside the range: `INVALID_LIMIT`. */
|
|
373
420
|
limit?: number;
|
|
374
421
|
startingAfter?: string;
|
|
375
422
|
endingBefore?: string;
|
|
376
|
-
/**
|
|
423
|
+
/** Inclusive, `YYYY-MM-DD HH:MM:SS`, in device-local time. */
|
|
377
424
|
startTime?: string;
|
|
378
|
-
/**
|
|
425
|
+
/** Exclusive, `YYYY-MM-DD HH:MM:SS`, in device-local time. */
|
|
379
426
|
endTime?: string;
|
|
380
427
|
}
|
|
381
428
|
/**
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
429
|
+
* Reply to the `sensorLog` command.
|
|
430
|
+
*
|
|
431
|
+
* An envelope carrying the echoed `requestId` for correlation (so concurrent
|
|
432
|
+
* keyset pagination works) plus `success` / `error`. On success it brings the
|
|
433
|
+
* page of logs — chronological ascending, temperature in °C, naive timestamps
|
|
434
|
+
* in **device-local time** — and the opaque cursors.
|
|
435
|
+
*
|
|
436
|
+
* With a time range, `hasMore` / `hasPrevious` are computed **within the
|
|
437
|
+
* period**, so `hasMore: false` means "this period is complete", not "no more
|
|
438
|
+
* data exists".
|
|
439
|
+
*
|
|
440
|
+
* On failure, `error` carries the contract code (`CONFLICTING_CURSORS`,
|
|
441
|
+
* `INVALID_TIME_RANGE`, `invalid session`, …) — a **diagnostic code, never
|
|
442
|
+
* text to show a user**. Note that `fetchSensorLog` RESOLVES with
|
|
443
|
+
* `success: false`; it only rejects on timeout.
|
|
391
444
|
*/
|
|
392
445
|
interface SensorLogPayload {
|
|
393
446
|
requestId: string;
|
|
@@ -400,13 +453,13 @@ interface SensorLogPayload {
|
|
|
400
453
|
error?: string;
|
|
401
454
|
}
|
|
402
455
|
/**
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
* (ex.: Bluetooth desligado impede o scan/conexão dos devices).
|
|
456
|
+
* Aggregate environment status (native → JS). Mirrors `SysNotification` in the
|
|
457
|
+
* app: the flags a client uses to explain why things are not working — for
|
|
458
|
+
* instance, Bluetooth being off prevents scanning and connecting to devices.
|
|
407
459
|
*
|
|
408
|
-
*
|
|
409
|
-
* `
|
|
460
|
+
* Emitted on attach (before your bundle exists), again right after
|
|
461
|
+
* `handshake-ack`, and on every change. `deviceSyncError` maps `serialNo` to an
|
|
462
|
+
* error message the app has already localized.
|
|
410
463
|
*/
|
|
411
464
|
interface SystemStatusPayload {
|
|
412
465
|
bluetooth: boolean;
|
|
@@ -432,29 +485,38 @@ interface SystemStatusPayload {
|
|
|
432
485
|
deviceSyncError: Record<number, string>;
|
|
433
486
|
}
|
|
434
487
|
/**
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
* Dispara o **diálogo de permissão nativo in-app** para UMA permissão
|
|
438
|
-
* (`AppPermission`), diferente de `openSettings` (que só abre as Configurações
|
|
439
|
-
* do sistema). Use para o botão "Conceder" do card de permissões Necessárias,
|
|
440
|
-
* passando um dos nomes vindos de `systemStatus.missingEssentialPermissions`
|
|
441
|
-
* (ex.: `'bluetoothScan'`).
|
|
488
|
+
* Arguments of the `requestPermission` command (JS → native).
|
|
442
489
|
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
490
|
+
* Triggers the **in-app native permission dialog** for ONE permission, unlike
|
|
491
|
+
* `openSettings`, which only opens the system Settings. Use it for the "Grant"
|
|
492
|
+
* button of your required-permissions card, passing a name taken from
|
|
493
|
+
* `systemStatus.missingEssentialPermissions` (for example `'bluetoothScan'`).
|
|
445
494
|
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
495
|
+
* **There is no result key.** The native side re-emits `systemStatus` after the
|
|
496
|
+
* person answers, so your card disappears when the array empties. Waiting for a
|
|
497
|
+
* dedicated ack means waiting forever. A name outside the native enum is
|
|
498
|
+
* dropped in silence.
|
|
448
499
|
*/
|
|
449
500
|
interface RequestPermissionPayload {
|
|
450
|
-
/**
|
|
501
|
+
/** Name of the permission to request, e.g. `'bluetoothScan'`. */
|
|
451
502
|
permission: string;
|
|
452
503
|
}
|
|
453
504
|
/**
|
|
454
|
-
* Payload
|
|
455
|
-
*
|
|
505
|
+
* Payload of the `navigationBack` event (native → client). Empty — the request
|
|
506
|
+
* carries no data; react by going back exactly one internal page.
|
|
456
507
|
*/
|
|
457
508
|
type NavigationBackPayload = Record<string, never>;
|
|
509
|
+
/**
|
|
510
|
+
* Arguments of the `navigationState` command (client → native). The client
|
|
511
|
+
* reports whether it has internal history to go back to; the native side caches
|
|
512
|
+
* it and uses it to decide what the OS back button does. See
|
|
513
|
+
* `BridgeClient.reportNavigation`, and call it on every internal navigation —
|
|
514
|
+
* not once at startup.
|
|
515
|
+
*/
|
|
516
|
+
interface NavigationStatePayload {
|
|
517
|
+
/** `true` when the client has a previous internal page to return to. */
|
|
518
|
+
canGoBack: boolean;
|
|
519
|
+
}
|
|
458
520
|
/**
|
|
459
521
|
* Language state of the native app (native → client).
|
|
460
522
|
*
|
|
@@ -513,10 +575,10 @@ interface LanguagePayload {
|
|
|
513
575
|
resolvedLanguage: string;
|
|
514
576
|
}
|
|
515
577
|
/**
|
|
516
|
-
*
|
|
517
|
-
*
|
|
578
|
+
* Discriminated map of payloads by catalogue key. Used by `PayloadOf<K>` for
|
|
579
|
+
* automatic inference in `on`.
|
|
518
580
|
*
|
|
519
|
-
* `dfu`
|
|
581
|
+
* `dfu` is hub-only and stays permissive — a sandboxed client never receives it.
|
|
520
582
|
*/
|
|
521
583
|
interface KeyPayloads {
|
|
522
584
|
pingPong: PingPongPayload;
|
|
@@ -534,15 +596,13 @@ interface KeyPayloads {
|
|
|
534
596
|
language: LanguagePayload;
|
|
535
597
|
}
|
|
536
598
|
/**
|
|
537
|
-
* Payload
|
|
538
|
-
* -
|
|
539
|
-
* -
|
|
540
|
-
*
|
|
599
|
+
* Payload typed by key. Distributive:
|
|
600
|
+
* - keys catalogued in `KeyPayloads` resolve to their strict payload;
|
|
601
|
+
* - any other key resolves to a permissive `Record<string, unknown>`, to keep
|
|
602
|
+
* the catalogue extensible.
|
|
541
603
|
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
* Cliente que precisa do shape permissivo pode usar
|
|
545
|
-
* `(payload: Record<string, unknown>) => ...` explicitamente.
|
|
604
|
+
* A client that wants the permissive shape can annotate the callback
|
|
605
|
+
* explicitly: `(payload: Record<string, unknown>) => …`.
|
|
546
606
|
*/
|
|
547
607
|
type PayloadOf<K extends BridgeKey = BridgeKey> = K extends keyof KeyPayloads ? KeyPayloads[K] : Record<string, unknown>;
|
|
548
608
|
|
|
@@ -561,188 +621,222 @@ interface MockState {
|
|
|
561
621
|
}
|
|
562
622
|
interface CondorBridge {
|
|
563
623
|
/**
|
|
564
|
-
* `true`
|
|
565
|
-
* `window.
|
|
566
|
-
* WebView
|
|
624
|
+
* `true` when the active adapter is the mock — a plain browser, with neither
|
|
625
|
+
* `window.CondorNative` nor `window.flutter_inappwebview`. `false` inside the
|
|
626
|
+
* Flutter WebView.
|
|
567
627
|
*/
|
|
568
628
|
readonly isMock: boolean;
|
|
569
629
|
/**
|
|
570
|
-
*
|
|
630
|
+
* Sends a versioned handshake to the native side and awaits `handshake-ack`.
|
|
571
631
|
*
|
|
572
|
-
*
|
|
573
|
-
*
|
|
632
|
+
* Only one handshake may be in flight; a parallel call before the first ack
|
|
633
|
+
* arrives is rejected with an explicit error.
|
|
574
634
|
*
|
|
575
|
-
* **
|
|
576
|
-
* `Bridge.mockEmit('handshake-ack', {nativeVersion, accepted})`
|
|
577
|
-
*
|
|
578
|
-
*
|
|
635
|
+
* **In mock mode it does not resolve on its own.** The caller must emit
|
|
636
|
+
* `Bridge.mockEmit('handshake-ack', { nativeVersion, accepted })` to unblock
|
|
637
|
+
* the promise. That is deliberate: it forces tests to be explicit about the
|
|
638
|
+
* native state they are simulating.
|
|
579
639
|
*/
|
|
580
640
|
handshake(opts: HandshakeOptions): Promise<HandshakeAck>;
|
|
581
641
|
/**
|
|
582
|
-
*
|
|
642
|
+
* Subscribes to an observable key. Returns an `unsubscribe` function.
|
|
583
643
|
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
644
|
+
* Multiple subscribers per key are allowed; a throw in one does not affect
|
|
645
|
+
* the others (it is caught and logged with `console.error`).
|
|
586
646
|
*/
|
|
587
647
|
on<K extends BridgeKey>(key: K, cb: (payload: PayloadOf<K>) => void): () => void;
|
|
588
648
|
/**
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
*
|
|
649
|
+
* Sends a generic message to the native side, for commands without a
|
|
650
|
+
* dedicated helper. `type` is the string the native side routes on.
|
|
651
|
+
*
|
|
652
|
+
* Fire-and-forget: no return value and no delivery confirmation. A payload
|
|
653
|
+
* above 64 KB, or an unknown `type`, is dropped natively without a reply.
|
|
592
654
|
*
|
|
593
|
-
*
|
|
655
|
+
* In mock mode it is recorded in the internal buffer and logged with
|
|
656
|
+
* `console.debug`.
|
|
594
657
|
*/
|
|
595
658
|
send(type: string, payload?: Record<string, unknown>): void;
|
|
596
659
|
/**
|
|
597
|
-
*
|
|
660
|
+
* Opens the app's native Settings screen. Equivalent to
|
|
598
661
|
* `Bridge.send('openSettings')`.
|
|
599
662
|
*/
|
|
600
663
|
openSettings(): void;
|
|
601
664
|
/**
|
|
602
|
-
*
|
|
603
|
-
*
|
|
665
|
+
* Registers a transform applied to **every** JS → native message, the
|
|
666
|
+
* handshake included, immediately before it is sent. Pass `null` to remove.
|
|
667
|
+
*
|
|
668
|
+
* It exists so the shell can stamp its nonce (`payload._shellNonce`) at a
|
|
669
|
+
* single chokepoint. Wrapping `send` alone is not enough — `handshake` calls
|
|
670
|
+
* the native layer directly, and a message without the nonce is dropped and
|
|
671
|
+
* logged as a security event.
|
|
672
|
+
*
|
|
673
|
+
* **Client code must not use this.**
|
|
674
|
+
*/
|
|
675
|
+
setOutgoingStamp(stamp: OutgoingStamp | null): void;
|
|
676
|
+
/**
|
|
677
|
+
* Fires `window.CondorBridge.emit(key, payload)` programmatically — useful to
|
|
678
|
+
* simulate native events in tests and in browser development.
|
|
604
679
|
*
|
|
605
|
-
* **Throws**
|
|
680
|
+
* **Throws** when the active adapter is not the mock.
|
|
606
681
|
*/
|
|
607
682
|
mockEmit(key: string, payload?: unknown): void;
|
|
608
683
|
/**
|
|
609
|
-
* Snapshot
|
|
610
|
-
* subscribers
|
|
684
|
+
* Snapshot of the mock adapter state: messages sent, and keys with active
|
|
685
|
+
* subscribers. Useful for assertions in client tests.
|
|
611
686
|
*
|
|
612
|
-
* **Throws**
|
|
687
|
+
* **Throws** when the active adapter is not the mock.
|
|
613
688
|
*/
|
|
614
689
|
getMockState(): MockState;
|
|
615
690
|
/**
|
|
616
|
-
*
|
|
617
|
-
* bridge
|
|
691
|
+
* Injects a floating HTML panel into `document.body` for inspecting the
|
|
692
|
+
* bridge in mock mode. Idempotent — re-installing replaces the previous one.
|
|
618
693
|
*
|
|
619
|
-
* **Throws**
|
|
694
|
+
* **Throws** when the active adapter is not the mock, or `document` does not
|
|
695
|
+
* exist.
|
|
620
696
|
*/
|
|
621
697
|
installDevPanel(options?: DevPanelOptions): void;
|
|
622
698
|
}
|
|
623
699
|
declare const Bridge: CondorBridge;
|
|
624
700
|
|
|
625
701
|
/**
|
|
626
|
-
* `BridgeClient`
|
|
627
|
-
*
|
|
702
|
+
* `BridgeClient` — **the API for client code**, running inside
|
|
703
|
+
* `<iframe sandbox="allow-scripts">` in the Condor Canvas app.
|
|
628
704
|
*
|
|
629
|
-
*
|
|
630
|
-
*
|
|
631
|
-
*
|
|
632
|
-
*
|
|
705
|
+
* This is what you want if you are building the bundle that renders as the
|
|
706
|
+
* app's Home. Every call becomes a `window.parent.postMessage(...)` that the
|
|
707
|
+
* Condor shell validates against a schema and a default-deny policy before it
|
|
708
|
+
* reaches the native layer. You never touch the native globals — the shell
|
|
709
|
+
* deletes them before your bundle runs.
|
|
633
710
|
*
|
|
634
|
-
*
|
|
635
|
-
*
|
|
711
|
+
* It deliberately does **not** expose `installDevPanel`, `mockEmit`, `isMock`
|
|
712
|
+
* or `getMockState`: those belong to the shell (`BridgeShell`).
|
|
636
713
|
*
|
|
637
|
-
*
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
* ou `window.CondorBridge` (que deveriam ter sido deletados pela Camada 1).
|
|
642
|
-
* Se vier `true` em algum, o shell aborta o boot do app.
|
|
714
|
+
* The module also answers the shell's `verify-sandbox` probe — the iframe
|
|
715
|
+
* attesting whether it can still see the native globals. If any is reachable,
|
|
716
|
+
* the shell aborts the boot rather than render client code with a path to the
|
|
717
|
+
* native layer.
|
|
643
718
|
*/
|
|
644
719
|
|
|
645
720
|
interface BridgeClientHandshakeAck {
|
|
646
721
|
shellVersion: string;
|
|
647
722
|
/**
|
|
648
|
-
*
|
|
649
|
-
*
|
|
723
|
+
* Schema version of the **native** side, relayed by the shell. `null` when
|
|
724
|
+
* the native side has not answered, does not exist (browser mock mode), or
|
|
725
|
+
* the shell is older than 0.6.0.
|
|
650
726
|
*
|
|
651
|
-
*
|
|
652
|
-
*
|
|
653
|
-
*
|
|
654
|
-
*
|
|
727
|
+
* **This number is the only capability signal a client gets.** There is no
|
|
728
|
+
* version negotiation, and a sandboxed client reaches the native side by no
|
|
729
|
+
* other path. For example, the raw light channels (`light_sensor_*` in
|
|
730
|
+
* `deviceLightSensors`) exist from `0.16.0`. Always keep a working branch
|
|
731
|
+
* for `null`: your browser development environment always hits it.
|
|
655
732
|
*/
|
|
656
733
|
nativeVersion: string | null;
|
|
734
|
+
/**
|
|
735
|
+
* Whether the native side accepted your `clientVersion`. It is **always
|
|
736
|
+
* `true`** — there is no hard validation — so it carries no information.
|
|
737
|
+
* Never branch on it; branch on {@link nativeVersion}.
|
|
738
|
+
*/
|
|
657
739
|
accepted: boolean;
|
|
658
740
|
}
|
|
659
741
|
interface CondorBridgeClient {
|
|
660
|
-
/**
|
|
661
|
-
* Versão do protocolo do client.
|
|
662
|
-
*/
|
|
742
|
+
/** Version of the client-side postMessage protocol. */
|
|
663
743
|
readonly protocolVersion: string;
|
|
664
744
|
/**
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
745
|
+
* Subscribes to an observable key. Returns an `unsubscribe` function —
|
|
746
|
+
* call it when a view unmounts, or handlers accumulate and every one of them
|
|
747
|
+
* re-renders.
|
|
748
|
+
*
|
|
749
|
+
* Multiple subscribers per key are allowed, and a throw in one does not
|
|
750
|
+
* affect the others. Subscribe *before* sending the command that triggers
|
|
751
|
+
* the reply: the reply can arrive before your next statement runs.
|
|
668
752
|
*/
|
|
669
753
|
on<K extends BridgeKey>(key: K, cb: (payload: PayloadOf<K>) => void): () => void;
|
|
670
754
|
/**
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
755
|
+
* Sends a command to the shell, which validates it against the policy before
|
|
756
|
+
* it touches the native layer.
|
|
757
|
+
*
|
|
758
|
+
* **Fire-and-forget: no return value, no throw, no delivery confirmation.**
|
|
759
|
+
* A command that is not allowed produces a `nack` that surfaces only as a
|
|
760
|
+
* `console.warn`. A malformed one is dropped in silence by the native side.
|
|
761
|
+
* Replies, when a command has one, arrive on the corresponding key — see
|
|
762
|
+
* `docs/bridge-contract.md`.
|
|
674
763
|
*/
|
|
675
764
|
send(command: string, args?: Record<string, unknown>): void;
|
|
676
|
-
/**
|
|
677
|
-
* Abre Settings nativa. Equivalente a `send('openSettings')`.
|
|
678
|
-
*/
|
|
765
|
+
/** Opens the native Settings screen. Equivalent to `send('openSettings')`. */
|
|
679
766
|
openSettings(): void;
|
|
680
767
|
/**
|
|
681
|
-
*
|
|
682
|
-
*
|
|
683
|
-
*
|
|
768
|
+
* Requests ONE page of sensor logs and resolves with the reply correlated by
|
|
769
|
+
* `requestId` — **including when it carries `success: false`**, which is an
|
|
770
|
+
* answer, not a transport failure. The promise only **rejects** on timeout,
|
|
771
|
+
* so always check `success` before reading `logs`.
|
|
684
772
|
*
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
773
|
+
* Saves you from keeping a `requestId → resolve` map by hand. To sweep a
|
|
774
|
+
* period, chain calls using the returned `startingAfter` until `hasMore`
|
|
775
|
+
* becomes `false`: the cursors are dependent, so pages **cannot** be fetched
|
|
776
|
+
* in parallel.
|
|
689
777
|
*/
|
|
690
778
|
fetchSensorLog(query: SensorLogQueryPayload, opts?: {
|
|
691
779
|
timeoutMs?: number;
|
|
692
780
|
}): Promise<SensorLogPayload>;
|
|
693
781
|
/**
|
|
694
|
-
*
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
782
|
+
* Requests ONE permission through the **in-app native dialog** (it does not
|
|
783
|
+
* open Settings). Use it for the "Grant" button of your required-permissions
|
|
784
|
+
* card, with a name taken from `systemStatus.missingEssentialPermissions`,
|
|
785
|
+
* e.g. `'bluetoothScan'`.
|
|
786
|
+
*
|
|
787
|
+
* **There is no result key.** The outcome arrives as a fresh `systemStatus`
|
|
788
|
+
* emission; your card disappears when the array empties. A name outside the
|
|
789
|
+
* native enum is dropped in silence.
|
|
699
790
|
*/
|
|
700
791
|
requestPermission(permission: string): void;
|
|
701
792
|
/**
|
|
702
|
-
*
|
|
703
|
-
*
|
|
793
|
+
* Handshakes with the shell. Resolves with
|
|
794
|
+
* `{ shellVersion, nativeVersion, accepted }` — read `nativeVersion` to learn
|
|
795
|
+
* what this device can do, and ignore `accepted`, which is always `true`.
|
|
704
796
|
*
|
|
705
|
-
*
|
|
797
|
+
* Rejects after 5 s if the shell does not answer. Only one handshake may be
|
|
798
|
+
* in flight at a time.
|
|
706
799
|
*/
|
|
707
800
|
handshake(opts: {
|
|
708
801
|
clientVersion: string;
|
|
709
802
|
}): Promise<BridgeClientHandshakeAck>;
|
|
710
803
|
/**
|
|
711
|
-
*
|
|
712
|
-
* **
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
*
|
|
804
|
+
* Reports to the native side whether the client has internal history to go
|
|
805
|
+
* back to. **Call it on every internal navigation**, and at boot — not once.
|
|
806
|
+
* The native side caches the value and uses it to decide what the OS back
|
|
807
|
+
* button does:
|
|
808
|
+
* - `true` — back asks you to go back one page (the `navigationBack` event,
|
|
809
|
+
* see {@link onBack});
|
|
810
|
+
* - `false` — back stays with the app and does not leave the Home.
|
|
717
811
|
*
|
|
718
|
-
*
|
|
812
|
+
* Equivalent to `send('navigationState', { canGoBack })`.
|
|
719
813
|
*/
|
|
720
814
|
reportNavigation(canGoBack: boolean): void;
|
|
721
815
|
/**
|
|
722
|
-
*
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
*
|
|
816
|
+
* Subscribes to the OS back request (the `navigationBack` event). The
|
|
817
|
+
* callback must go back **one** internal page and then call
|
|
818
|
+
* {@link reportNavigation} with the new state. It only fires when you had
|
|
819
|
+
* reported `reportNavigation(true)`. Returns an `unsubscribe` function.
|
|
726
820
|
*/
|
|
727
821
|
onBack(cb: () => void): () => void;
|
|
728
822
|
}
|
|
729
823
|
declare const BridgeClient: CondorBridgeClient;
|
|
730
824
|
|
|
731
825
|
/**
|
|
732
|
-
*
|
|
826
|
+
* Valibot schemas for the shell↔client `postMessage` protocol.
|
|
733
827
|
*
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
828
|
+
* Client content runs inside `<iframe sandbox="allow-scripts">` with a `null`
|
|
829
|
+
* origin, so it talks to the Condor-controlled shell over `window.postMessage`.
|
|
830
|
+
* These schemas are the **only accepted contract** — any malformed message is
|
|
831
|
+
* discarded by the shell in silence, which is why hand-rolled `postMessage`
|
|
832
|
+
* calls appear to do nothing. Use `BridgeClient`.
|
|
738
833
|
*
|
|
739
|
-
* **Threat model:**
|
|
740
|
-
*
|
|
741
|
-
*
|
|
742
|
-
* `condor_wiki/cross-cutting/sleep-white-label-content-transport-and-security.md`.
|
|
834
|
+
* **Threat model:** the iframe may be hostile (a compromised supply chain, XSS
|
|
835
|
+
* in the client code). These schemas plus the `ShellPolicy` enforced by
|
|
836
|
+
* `BridgeShell` form the structural boundary.
|
|
743
837
|
*/
|
|
744
838
|
|
|
745
|
-
declare const
|
|
839
|
+
declare const ClientToShellMessageSchema: v.VariantSchema<"type", [v.ObjectSchema<{
|
|
746
840
|
readonly type: v.LiteralSchema<"subscribe", undefined>;
|
|
747
841
|
readonly key: v.PicklistSchema<[string, ...string[]], undefined>;
|
|
748
842
|
}, undefined>, v.ObjectSchema<{
|
|
@@ -770,9 +864,11 @@ declare const ClientToShellMessage: v.VariantSchema<"type", [v.ObjectSchema<{
|
|
|
770
864
|
readonly type: v.LiteralSchema<"sandbox-verify-response", undefined>;
|
|
771
865
|
readonly flutterBridgeReachable: v.BooleanSchema<undefined>;
|
|
772
866
|
readonly condorBridgeReachable: v.BooleanSchema<undefined>;
|
|
867
|
+
readonly nativeChannelReachable: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
773
868
|
}, undefined>], undefined>;
|
|
774
|
-
|
|
775
|
-
|
|
869
|
+
/** Union of every client → shell message. Type only; the runtime schema is `ClientToShellMessageSchema`, which is not part of the public API. */
|
|
870
|
+
type ClientToShellMessage = v.InferOutput<typeof ClientToShellMessageSchema>;
|
|
871
|
+
declare const ShellToClientMessageSchema: v.VariantSchema<"type", [v.ObjectSchema<{
|
|
776
872
|
readonly type: v.LiteralSchema<"event", undefined>;
|
|
777
873
|
readonly key: v.PicklistSchema<[string, ...string[]], undefined>;
|
|
778
874
|
readonly payload: v.UnknownSchema;
|
|
@@ -781,12 +877,12 @@ declare const ShellToClientMessage: v.VariantSchema<"type", [v.ObjectSchema<{
|
|
|
781
877
|
readonly ack: v.ObjectSchema<{
|
|
782
878
|
readonly shellVersion: v.StringSchema<undefined>;
|
|
783
879
|
/**
|
|
784
|
-
*
|
|
880
|
+
* Schema version declared by the **native** side, relayed by the shell.
|
|
785
881
|
*
|
|
786
|
-
* `null`
|
|
787
|
-
* mock
|
|
788
|
-
*
|
|
789
|
-
*
|
|
882
|
+
* `null` when the native side has not answered, or does not exist (browser
|
|
883
|
+
* mock mode). The client runs inside a sandboxed iframe and never talks to
|
|
884
|
+
* the native side directly, so this is the **only** way it can learn which
|
|
885
|
+
* capabilities are available.
|
|
790
886
|
*/
|
|
791
887
|
readonly nativeVersion: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
792
888
|
readonly accepted: v.BooleanSchema<undefined>;
|
|
@@ -799,22 +895,23 @@ declare const ShellToClientMessage: v.VariantSchema<"type", [v.ObjectSchema<{
|
|
|
799
895
|
}, undefined>, v.ObjectSchema<{
|
|
800
896
|
readonly type: v.LiteralSchema<"verify-sandbox", undefined>;
|
|
801
897
|
}, undefined>], undefined>;
|
|
802
|
-
|
|
898
|
+
/** Union of every shell → client message. Type only; the runtime schema is `ShellToClientMessageSchema`, which is not part of the public API. */
|
|
899
|
+
type ShellToClientMessage = v.InferOutput<typeof ShellToClientMessageSchema>;
|
|
803
900
|
/**
|
|
804
|
-
*
|
|
805
|
-
*
|
|
806
|
-
* bridge
|
|
901
|
+
* Default-deny policy enforced by `BridgeShell`. Every key and command coming
|
|
902
|
+
* from the iframe is checked against these lists before it touches the native
|
|
903
|
+
* bridge.
|
|
807
904
|
*
|
|
808
|
-
* - `readOnlyKeys
|
|
809
|
-
* - `allowedCommands
|
|
810
|
-
* - `requiresConfirmation
|
|
811
|
-
*
|
|
812
|
-
* - `deniedCommands
|
|
813
|
-
*
|
|
905
|
+
* - `readOnlyKeys` — keys the client may subscribe to.
|
|
906
|
+
* - `allowedCommands` — pass-through commands, no human confirmation.
|
|
907
|
+
* - `requiresConfirmation` — commands that fire the shell's
|
|
908
|
+
* `onConfirmationRequest` callback, so a person approves or declines.
|
|
909
|
+
* - `deniedCommands` — explicit block, defence in depth: destructive commands
|
|
910
|
+
* such as `dfu` that must never flow from the iframe.
|
|
814
911
|
*
|
|
815
|
-
* **
|
|
816
|
-
*
|
|
817
|
-
*
|
|
912
|
+
* **Not embedded in the package** — the `BridgeShell` host supplies the
|
|
913
|
+
* concrete policy. That keeps the package agnostic to the catalogue and lets
|
|
914
|
+
* the policy evolve without a release.
|
|
818
915
|
*/
|
|
819
916
|
interface ShellPolicy {
|
|
820
917
|
readOnlyKeys: string[];
|
|
@@ -823,66 +920,73 @@ interface ShellPolicy {
|
|
|
823
920
|
deniedCommands: string[];
|
|
824
921
|
}
|
|
825
922
|
/**
|
|
826
|
-
*
|
|
923
|
+
* Argument of the `BridgeShell` `onConfirmationRequest` callback.
|
|
827
924
|
*
|
|
828
|
-
* `approve: true`
|
|
829
|
-
* `approve: false`
|
|
925
|
+
* Resolving `approve: true` lets the command flow to the native side.
|
|
926
|
+
* Resolving `approve: false` makes the shell send the iframe a `nack` with
|
|
927
|
+
* `reason: 'user-rejected'` — which client code only ever sees as a console
|
|
928
|
+
* warning, so the ack it was waiting for simply never arrives.
|
|
830
929
|
*/
|
|
831
930
|
interface ConfirmationRequest {
|
|
832
931
|
command: string;
|
|
833
932
|
args: Record<string, unknown>;
|
|
834
933
|
}
|
|
835
934
|
/**
|
|
836
|
-
*
|
|
837
|
-
*
|
|
838
|
-
*
|
|
839
|
-
*
|
|
935
|
+
* Result of `BridgeShell.verifySandbox()` after a round trip with the iframe.
|
|
936
|
+
* If any `*Reachable` flag comes back `true`, the boundary is leaking: the
|
|
937
|
+
* override prefix injected by `bootstrapClientIframe` failed, and the app must
|
|
938
|
+
* not render client code with a path to the native layer.
|
|
840
939
|
*/
|
|
841
940
|
interface SandboxVerifyResult {
|
|
842
941
|
flutterBridgeReachable: boolean;
|
|
843
942
|
condorBridgeReachable: boolean;
|
|
943
|
+
/** Whether `window.CondorNative` is visible inside the iframe. */
|
|
944
|
+
nativeChannelReachable: boolean;
|
|
945
|
+
/** `true` only when none of the three is reachable. */
|
|
844
946
|
ok: boolean;
|
|
845
947
|
}
|
|
846
948
|
|
|
847
949
|
/**
|
|
848
|
-
* `BridgeShell`
|
|
849
|
-
*
|
|
850
|
-
*
|
|
851
|
-
*
|
|
852
|
-
*
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
*
|
|
856
|
-
*
|
|
857
|
-
*
|
|
858
|
-
*
|
|
859
|
-
*
|
|
860
|
-
*
|
|
861
|
-
*
|
|
862
|
-
*
|
|
863
|
-
*
|
|
864
|
-
*
|
|
865
|
-
*
|
|
866
|
-
* `
|
|
950
|
+
* `BridgeShell` — the API for the **Condor-controlled shell** that wraps
|
|
951
|
+
* external client content inside `<iframe sandbox="allow-scripts">`.
|
|
952
|
+
*
|
|
953
|
+
* This is not client-facing. If you are writing the bundle that renders as the
|
|
954
|
+
* app's Home, you want `BridgeClient`.
|
|
955
|
+
*
|
|
956
|
+
* Responsibilities:
|
|
957
|
+
* 1. **Layer 1 — `bootstrapClientIframe(iframe, clientPath)`:** fetches the
|
|
958
|
+
* client HTML, prepends an override script that removes the native globals
|
|
959
|
+
* from the iframe, and sets it as `srcdoc`. The override runs at parse time,
|
|
960
|
+
* before any other script.
|
|
961
|
+
* 2. **Layer 2 — `verifySandbox(iframe)`:** a `postMessage` round trip asking
|
|
962
|
+
* the iframe to attest that the native globals really are `undefined`.
|
|
963
|
+
* 3. **Routing between iframe and native:** listens for `message` events from
|
|
964
|
+
* the iframe, validates them with Valibot, applies the default-deny
|
|
965
|
+
* `ShellPolicy`, and forwards to `Bridge`. Native events go back to the
|
|
966
|
+
* iframe over `postMessage`.
|
|
967
|
+
* 4. **Human confirmation for sensitive commands:** commands listed in
|
|
968
|
+
* `policy.requiresConfirmation` fire the `onConfirmationRequest` callback
|
|
969
|
+
* before touching the native side, so the shell can present UI.
|
|
867
970
|
*/
|
|
868
971
|
|
|
869
972
|
interface BridgeShellOptions {
|
|
870
973
|
/**
|
|
871
|
-
*
|
|
872
|
-
*
|
|
974
|
+
* The iframe hosting the client. It may be empty at construction time —
|
|
975
|
+
* `bootstrapClientIframe` is called later to populate it.
|
|
873
976
|
*/
|
|
874
977
|
iframe: HTMLIFrameElement;
|
|
875
978
|
/**
|
|
876
|
-
*
|
|
877
|
-
*
|
|
979
|
+
* The default-deny policy. Every key and command arriving from the iframe is
|
|
980
|
+
* checked against these lists. See `ShellPolicy`.
|
|
878
981
|
*/
|
|
879
982
|
policy: ShellPolicy;
|
|
880
983
|
/**
|
|
881
|
-
*
|
|
882
|
-
*
|
|
883
|
-
*
|
|
884
|
-
*
|
|
885
|
-
*
|
|
984
|
+
* Synchronous or asynchronous callback invoked when the client issues a
|
|
985
|
+
* command listed in `policy.requiresConfirmation`. The shell presents its
|
|
986
|
+
* confirmation UI and resolves `true` to approve or `false` to decline.
|
|
987
|
+
*
|
|
988
|
+
* If it is not provided, such commands are **denied** automatically —
|
|
989
|
+
* fail-safe.
|
|
886
990
|
*/
|
|
887
991
|
onConfirmationRequest?: (req: ConfirmationRequest) => boolean | Promise<boolean>;
|
|
888
992
|
}
|
|
@@ -893,61 +997,60 @@ declare class BridgeShell {
|
|
|
893
997
|
private readonly subscriptions;
|
|
894
998
|
private detached;
|
|
895
999
|
/**
|
|
896
|
-
*
|
|
897
|
-
*
|
|
1000
|
+
* The native version, fetched once and cached. `null` until the native side
|
|
1001
|
+
* answers, and permanently `null` in a browser with no native host.
|
|
898
1002
|
*/
|
|
899
1003
|
private nativeVersion;
|
|
900
|
-
/**
|
|
1004
|
+
/** In-flight native handshake; guarantees a single call. */
|
|
901
1005
|
private nativeHandshake;
|
|
902
1006
|
private pendingVerifies;
|
|
903
1007
|
constructor(opts: BridgeShellOptions);
|
|
904
1008
|
/**
|
|
905
|
-
* **
|
|
1009
|
+
* **Layer 1.** Bootstraps by fetching a URL.
|
|
906
1010
|
*
|
|
907
|
-
*
|
|
908
|
-
*
|
|
1011
|
+
* A shortcut for the common HTTP/HTTPS case: it runs `fetch(clientPath)` and
|
|
1012
|
+
* delegates to `bootstrapClientIframeWithHtml`.
|
|
909
1013
|
*
|
|
910
|
-
* **
|
|
911
|
-
*
|
|
912
|
-
*
|
|
913
|
-
* `bootstrapClientIframeWithHtml(htmlString)
|
|
1014
|
+
* **It does not work on the `file://` scheme** — Android's Chromium WebView
|
|
1015
|
+
* blocks it with "URL scheme 'file' is not supported". For an asset bundled
|
|
1016
|
+
* in the APK, or a bundle inlined at build time, use
|
|
1017
|
+
* `bootstrapClientIframeWithHtml(htmlString)` instead.
|
|
914
1018
|
*
|
|
915
|
-
* **
|
|
916
|
-
*
|
|
917
|
-
* para atestation (Camada 2).
|
|
1019
|
+
* **Call it once at shell boot**, before any other interaction. After it
|
|
1020
|
+
* returns, invoke `verifySandbox()` for attestation (Layer 2).
|
|
918
1021
|
*/
|
|
919
1022
|
bootstrapClientIframe(clientPath?: string): Promise<void>;
|
|
920
1023
|
/**
|
|
921
|
-
* **
|
|
1024
|
+
* **Layer 1.** Bootstraps from an HTML string.
|
|
922
1025
|
*
|
|
923
|
-
*
|
|
924
|
-
* build
|
|
925
|
-
*
|
|
926
|
-
*
|
|
1026
|
+
* Takes the client HTML directly — already read from a file, embedded at
|
|
1027
|
+
* build time, read from storage — and sets it as the iframe's `srcdoc`,
|
|
1028
|
+
* prefixed by the override script, which runs at parse time before any
|
|
1029
|
+
* client script.
|
|
927
1030
|
*
|
|
928
|
-
* Use
|
|
929
|
-
* bundle inlined
|
|
930
|
-
*
|
|
1031
|
+
* Use this when `fetch` does not apply: an asset bundled under `file://`, a
|
|
1032
|
+
* bundle inlined at build time, any scenario without network. Android
|
|
1033
|
+
* WebView blocks `fetch` on the `file://` scheme, so this is the path the
|
|
1034
|
+
* app actually takes. For HTTP, use `bootstrapClientIframe(clientPath)`.
|
|
931
1035
|
*
|
|
932
|
-
* **
|
|
933
|
-
*
|
|
934
|
-
* para atestation (Camada 2).
|
|
1036
|
+
* **Call it once at shell boot**, before any other interaction. After it
|
|
1037
|
+
* returns, invoke `verifySandbox()` for attestation (Layer 2).
|
|
935
1038
|
*/
|
|
936
1039
|
bootstrapClientIframeWithHtml(clientHtml: string): Promise<void>;
|
|
937
1040
|
private waitForIframeLoad;
|
|
938
1041
|
/**
|
|
939
|
-
* **
|
|
940
|
-
*
|
|
941
|
-
*
|
|
1042
|
+
* **Layer 2.** A `postMessage` round trip asking the iframe for an
|
|
1043
|
+
* authoritative attestation of whether the native globals are reachable.
|
|
1044
|
+
* Times out after 2 s.
|
|
942
1045
|
*
|
|
943
|
-
* `ok`
|
|
944
|
-
* `false`, **shell
|
|
945
|
-
*
|
|
1046
|
+
* `ok` is `true` only when none of them is reachable. If it comes back
|
|
1047
|
+
* `false`, **the shell must abort the app boot**: Layer 1 failed, and client
|
|
1048
|
+
* code would run with a path to the native layer.
|
|
946
1049
|
*/
|
|
947
1050
|
verifySandbox(): Promise<SandboxVerifyResult>;
|
|
948
1051
|
/**
|
|
949
|
-
*
|
|
950
|
-
*
|
|
1052
|
+
* Tears the shell down: removes the `message` listener, cancels pending
|
|
1053
|
+
* subscriptions on the native `Bridge`, and clears internal state.
|
|
951
1054
|
*/
|
|
952
1055
|
detach(): void;
|
|
953
1056
|
private onMessage;
|
|
@@ -955,16 +1058,16 @@ declare class BridgeShell {
|
|
|
955
1058
|
private handleUnsubscribe;
|
|
956
1059
|
private handleSend;
|
|
957
1060
|
/**
|
|
958
|
-
*
|
|
959
|
-
*
|
|
1061
|
+
* Resolves `nativeVersion`, firing the native handshake the first time and
|
|
1062
|
+
* caching the result.
|
|
960
1063
|
*
|
|
961
|
-
* ⚠️ `Bridge.handshake` **
|
|
962
|
-
*
|
|
963
|
-
*
|
|
964
|
-
*
|
|
1064
|
+
* ⚠️ `Bridge.handshake` has **no timeout**: its promise only settles when the
|
|
1065
|
+
* native side emits `handshake-ack`. In a browser with no native host it
|
|
1066
|
+
* never settles, so the race below is what guarantees the client always gets
|
|
1067
|
+
* an answer — `null` — instead of hanging.
|
|
965
1068
|
*/
|
|
966
1069
|
private resolveNativeVersion;
|
|
967
1070
|
private postToClient;
|
|
968
1071
|
}
|
|
969
1072
|
|
|
970
|
-
export { type AutoRespondResponse, BRIDGE_KEYS, Bridge, BridgeClient, type BridgeClientHandshakeAck, type BridgeIncomingMessage, type BridgeKey, type BridgeOutgoingEvent, BridgeShell, type BridgeShellOptions, ClientToShellMessage, type CondorBridge, type CondorBridgeClient, type ConfirmationRequest, type DeleteMemoryAckPayload, type DevPanelOptions, type DeviceConnectionPayload, type DeviceLightSensorsPayload, type DeviceNotificationPayload, type HandshakeAck, type HandshakeOptions, type KeyPayloads, type LanguagePayload, type LightSensorsActlumus1Data, type LightSensorsActlumus2Data, type LightSensorsRawChannels, type MockMessage, type MockState, type PayloadOf, type PingPongPayload, type RealTimeAckPayload, type RequestPermissionPayload, type SandboxVerifyResult, type SensorLogPayload, type SensorLogQueryPayload, type ShellPolicy, ShellToClientMessage, type SyncTimeAckPayload, type SystemStatusPayload, type UserDevicePayload };
|
|
1073
|
+
export { type AutoRespondResponse, BRIDGE_KEYS, Bridge, BridgeClient, type BridgeClientHandshakeAck, type BridgeIncomingMessage, type BridgeKey, type BridgeOutgoingEvent, BridgeShell, type BridgeShellOptions, type ClientToShellMessage, type CondorBridge, type CondorBridgeClient, type ConfirmationRequest, type DeleteMemoryAckPayload, type DevPanelOptions, type DeviceConnectionPayload, type DeviceLightSensorsPayload, type DeviceNotificationPayload, type HandshakeAck, type HandshakeOptions, type KeyPayloads, type LanguagePayload, type LightSensorsActlumus1Data, type LightSensorsActlumus2Data, type LightSensorsRawChannels, type MockMessage, type MockState, NATIVE_CHANNEL_NAME, type NavigationBackPayload, type NavigationStatePayload, type OutgoingStamp, type PayloadOf, type PingPongPayload, type RealTimeAckPayload, type RequestPermissionPayload, type SandboxVerifyResult, type SensorLogPayload, type SensorLogQueryPayload, type ShellPolicy, type ShellToClientMessage, type SyncTimeAckPayload, type SystemStatusPayload, type UserDevicePayload };
|