@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/dist/index.d.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import * as v from 'valibot';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Mock adapter —
|
|
5
|
-
*
|
|
4
|
+
* Mock adapter — active when no native host is detected: a plain browser,
|
|
5
|
+
* jsdom without an installed host, Storybook, and so on.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* - `callHandler`
|
|
9
|
-
*
|
|
10
|
-
* listeners
|
|
11
|
-
* - `triggerEmit`
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* - `getMessages`
|
|
15
|
-
* - `clearMessages`
|
|
16
|
-
* - `onCallHandler(listener)`
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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,55 +55,51 @@ 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.
|
|
63
|
-
*
|
|
64
|
-
* Sem esta callback, o checkbox "Auto-respond" não aparece no painel.
|
|
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.
|
|
65
61
|
*
|
|
66
|
-
*
|
|
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.
|
|
87
80
|
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* segue suportado por transição. O canal só aceita string: a mensagem vai
|
|
94
|
-
* serializada em JSON. E o gancho `setOutgoingStamp` permite ao shell
|
|
95
|
-
* carimbar toda mensagem de saída (ex.: nonce da Camada 3) sem monkey-patch
|
|
96
|
-
* do host.
|
|
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.
|
|
97
86
|
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
87
|
+
* With no host at all the mock adapter activates, which is what makes browser
|
|
88
|
+
* development possible without building an APK.
|
|
89
|
+
*
|
|
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`.
|
|
101
94
|
*/
|
|
102
95
|
|
|
103
96
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* `
|
|
97
|
+
* Name of the JavaScript channel the native side registers with
|
|
98
|
+
* `WebViewController.addJavaScriptChannel`.
|
|
99
|
+
*
|
|
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.
|
|
108
103
|
*/
|
|
109
104
|
declare const NATIVE_CHANNEL_NAME = "CondorNative";
|
|
110
105
|
interface FlutterInAppWebView {
|
|
@@ -126,16 +121,15 @@ declare global {
|
|
|
126
121
|
type OutgoingStamp = (message: Record<string, unknown>) => Record<string, unknown>;
|
|
127
122
|
|
|
128
123
|
/**
|
|
129
|
-
*
|
|
124
|
+
* Catalogue of the JS↔Native bridge keys.
|
|
130
125
|
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
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)`.
|
|
134
129
|
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
* aqui como contrato forward-looking.
|
|
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.
|
|
139
133
|
*/
|
|
140
134
|
declare const BRIDGE_KEYS: {
|
|
141
135
|
readonly pingPong: "pingPong";
|
|
@@ -150,10 +144,10 @@ declare const BRIDGE_KEYS: {
|
|
|
150
144
|
readonly dfu: "dfu";
|
|
151
145
|
readonly systemStatus: "systemStatus";
|
|
152
146
|
/**
|
|
153
|
-
* Native → client:
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
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`.
|
|
157
151
|
*/
|
|
158
152
|
readonly navigationBack: "navigationBack";
|
|
159
153
|
/**
|
|
@@ -161,22 +155,24 @@ declare const BRIDGE_KEYS: {
|
|
|
161
155
|
* AND the language explicitly chosen in the app Settings, so the client can
|
|
162
156
|
* apply its own precedence rule — including rendering a language the native
|
|
163
157
|
* chrome does not support. Emitted on attach, after `handshake-ack`, and on
|
|
164
|
-
* every change in Settings.
|
|
158
|
+
* every change in Settings. See `LanguagePayload`.
|
|
165
159
|
*/
|
|
166
160
|
readonly language: "language";
|
|
167
161
|
};
|
|
168
162
|
type BridgeKey = (typeof BRIDGE_KEYS)[keyof typeof BRIDGE_KEYS];
|
|
169
163
|
|
|
170
164
|
/**
|
|
171
|
-
*
|
|
165
|
+
* Wire messages of the bridge protocol.
|
|
172
166
|
*
|
|
173
|
-
*
|
|
174
|
-
* `
|
|
167
|
+
* Mirrors the Dart side (`BridgeIncomingMessage` / `BridgeOutgoingEvent` in
|
|
168
|
+
* the Canvas app's `bridge_message.dart`).
|
|
175
169
|
*/
|
|
176
170
|
/**
|
|
177
|
-
*
|
|
178
|
-
* `window.CondorNative.postMessage(JSON.stringify(msg))` (
|
|
179
|
-
* host
|
|
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.
|
|
180
176
|
*/
|
|
181
177
|
interface BridgeIncomingMessage {
|
|
182
178
|
type: string;
|
|
@@ -184,56 +180,60 @@ interface BridgeIncomingMessage {
|
|
|
184
180
|
payload?: Record<string, unknown>;
|
|
185
181
|
}
|
|
186
182
|
/**
|
|
187
|
-
*
|
|
188
|
-
* `window.CondorBridge.emit(key, payload)`.
|
|
183
|
+
* Native → JS event, delivered through `window.CondorBridge.emit(key, payload)`.
|
|
189
184
|
*/
|
|
190
185
|
interface BridgeOutgoingEvent {
|
|
191
186
|
key: string;
|
|
192
187
|
payload: unknown;
|
|
193
188
|
}
|
|
194
189
|
/**
|
|
195
|
-
*
|
|
190
|
+
* Handshake reply (`handshake-ack`).
|
|
196
191
|
*
|
|
197
|
-
* `nativeVersion`:
|
|
198
|
-
*
|
|
199
|
-
*
|
|
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.
|
|
200
198
|
*/
|
|
201
199
|
interface HandshakeAck {
|
|
202
200
|
nativeVersion: string;
|
|
203
201
|
accepted: boolean;
|
|
204
202
|
}
|
|
203
|
+
/** Options for {@link CondorBridge.handshake}. */
|
|
205
204
|
interface HandshakeOptions {
|
|
205
|
+
/** Your own client version, for the native logs. Not validated. */
|
|
206
206
|
clientVersion: string;
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
/**
|
|
210
|
-
*
|
|
210
|
+
* Per-key typed payload catalogue.
|
|
211
211
|
*
|
|
212
|
-
* Shapes
|
|
213
|
-
*
|
|
214
|
-
*
|
|
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.
|
|
215
215
|
*
|
|
216
|
-
* **
|
|
217
|
-
*
|
|
218
|
-
*
|
|
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.
|
|
219
219
|
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
* `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).
|
|
224
223
|
*/
|
|
225
|
-
/**
|
|
224
|
+
/** Boot / keepalive heartbeat. */
|
|
226
225
|
interface PingPongPayload {
|
|
227
226
|
data: 'ping' | 'pong';
|
|
228
227
|
}
|
|
229
228
|
/**
|
|
230
|
-
* Snapshot
|
|
231
|
-
*
|
|
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.
|
|
232
235
|
*
|
|
233
|
-
*
|
|
234
|
-
* tem seu próprio `toMap()` (DeviceConfig, DeviceTelemetry,
|
|
235
|
-
* DeviceUserConfig) cujo strict typing fica para release futura quando
|
|
236
|
-
* consumidor JS precisar.
|
|
236
|
+
* There is no device cap in the app. Do not assume this list is bounded.
|
|
237
237
|
*/
|
|
238
238
|
interface UserDevicePayload {
|
|
239
239
|
lastUpdate: number;
|
|
@@ -242,23 +242,22 @@ interface UserDevicePayload {
|
|
|
242
242
|
devicesUserConfig: Array<Record<string, unknown>>;
|
|
243
243
|
}
|
|
244
244
|
/**
|
|
245
|
-
*
|
|
246
|
-
* `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.
|
|
247
247
|
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
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.
|
|
253
253
|
*
|
|
254
|
-
* ⚠️
|
|
255
|
-
* `0.16.x`
|
|
256
|
-
*
|
|
257
|
-
* 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.
|
|
258
257
|
*
|
|
259
|
-
* `null`
|
|
260
|
-
* zero"
|
|
261
|
-
* `
|
|
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.
|
|
262
261
|
*/
|
|
263
262
|
type LightSensorsRawChannels = {
|
|
264
263
|
light_sensor_f1: number;
|
|
@@ -269,18 +268,18 @@ type LightSensorsRawChannels = {
|
|
|
269
268
|
light_sensor_f6: number;
|
|
270
269
|
light_sensor_f7: number;
|
|
271
270
|
light_sensor_f8: number;
|
|
272
|
-
/**
|
|
271
|
+
/** Total light (`visible` on the device wire format). */
|
|
273
272
|
light_sensor_clear: number;
|
|
274
|
-
/**
|
|
273
|
+
/** Near infrared. */
|
|
275
274
|
light_sensor_nir: number;
|
|
276
275
|
};
|
|
277
276
|
/**
|
|
278
|
-
*
|
|
277
|
+
* A real-time reading from an ActLumus gen1 (`type: 'actlumus'`).
|
|
279
278
|
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
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.
|
|
284
283
|
*/
|
|
285
284
|
type LightSensorsActlumus1Data = LightSensorsRawChannels & {
|
|
286
285
|
time: number;
|
|
@@ -302,17 +301,17 @@ type LightSensorsActlumus1Data = LightSensorsRawChannels & {
|
|
|
302
301
|
light_sensor_fd: null;
|
|
303
302
|
};
|
|
304
303
|
/**
|
|
305
|
-
*
|
|
304
|
+
* A real-time reading from an ActLumus Plus / gen2 (`type: 'actlumus2'`).
|
|
306
305
|
*
|
|
307
|
-
*
|
|
308
|
-
* (`packages/condor_interactors/lib/src/domain/entities/actlumus/2/light_sensors2_graphic_data_entity.dart`).
|
|
309
|
-
* ⚠️ Os comprimentos de onda **não** são os mesmos do gen1 — são 14 (395…800)
|
|
310
|
-
* contra 10 (390…750), sem nenhum valor em comum. Um gráfico que leia a lista
|
|
311
|
-
* do gen1 num device gen2 encontra `undefined` em todas as séries e renderiza
|
|
312
|
-
* vazio; discrimine sempre pelo `type`.
|
|
306
|
+
* Mirrors `LightSensorsActlumus2GraphicData.toMap()` in `condor_interactors`.
|
|
313
307
|
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
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.
|
|
316
315
|
*/
|
|
317
316
|
type LightSensorsActlumus2Data = LightSensorsRawChannels & {
|
|
318
317
|
time: number;
|
|
@@ -335,17 +334,20 @@ type LightSensorsActlumus2Data = LightSensorsRawChannels & {
|
|
|
335
334
|
light_sensor_z: number;
|
|
336
335
|
light_sensor_y: number;
|
|
337
336
|
light_sensor_x: number;
|
|
338
|
-
/** Flicker detect.
|
|
337
|
+
/** Flicker detect. Not part of the spectrum matrix. */
|
|
339
338
|
light_sensor_fd: number;
|
|
340
339
|
};
|
|
341
340
|
/**
|
|
342
|
-
*
|
|
343
|
-
* `{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`.
|
|
344
348
|
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
* até a `0.16.0` só o espectro atravessava, e uma sonda que procurasse um canal
|
|
348
|
-
* 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.
|
|
349
351
|
*/
|
|
350
352
|
type DeviceLightSensorsPayload = {
|
|
351
353
|
serialNo: number;
|
|
@@ -373,16 +375,17 @@ interface DeleteMemoryAckPayload {
|
|
|
373
375
|
success: boolean;
|
|
374
376
|
}
|
|
375
377
|
/**
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
+
* BLE connection status. Mirrors the `openConnection` subscriber in the app's
|
|
379
|
+
* device notification service.
|
|
378
380
|
*/
|
|
379
381
|
interface DeviceConnectionPayload {
|
|
380
382
|
serialNo: number;
|
|
381
383
|
isConnect: boolean;
|
|
382
384
|
}
|
|
383
385
|
/**
|
|
384
|
-
*
|
|
385
|
-
*
|
|
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.
|
|
386
389
|
*/
|
|
387
390
|
interface DeviceNotificationPayload {
|
|
388
391
|
serialNo: number;
|
|
@@ -392,46 +395,52 @@ interface DeviceNotificationPayload {
|
|
|
392
395
|
isError: boolean;
|
|
393
396
|
}
|
|
394
397
|
/**
|
|
395
|
-
*
|
|
398
|
+
* Parameters of the `sensorLog` command.
|
|
396
399
|
*
|
|
397
|
-
* `startTime
|
|
398
|
-
* `endTime` **
|
|
399
|
-
* (
|
|
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.
|
|
400
403
|
*
|
|
401
|
-
* ⚠️
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
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.
|
|
406
409
|
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
410
|
+
* The `startingAfter` / `endingBefore` cursors are **opaque** and mutually
|
|
411
|
+
* exclusive: echo back the value you received, never construct one.
|
|
409
412
|
*/
|
|
410
413
|
interface SensorLogQueryPayload {
|
|
411
|
-
/**
|
|
414
|
+
/** Echoed back in the reply — correlates concurrent requests. */
|
|
412
415
|
requestId: string;
|
|
413
416
|
serialNo: number;
|
|
414
|
-
/** `wrist`
|
|
417
|
+
/** `wrist` or `necklace`. Omit for no filter. */
|
|
415
418
|
bodyPart?: string;
|
|
416
|
-
/** 1..1000 (default
|
|
419
|
+
/** 1..1000 (backend default is 10). Outside the range: `INVALID_LIMIT`. */
|
|
417
420
|
limit?: number;
|
|
418
421
|
startingAfter?: string;
|
|
419
422
|
endingBefore?: string;
|
|
420
|
-
/**
|
|
423
|
+
/** Inclusive, `YYYY-MM-DD HH:MM:SS`, in device-local time. */
|
|
421
424
|
startTime?: string;
|
|
422
|
-
/**
|
|
425
|
+
/** Exclusive, `YYYY-MM-DD HH:MM:SS`, in device-local time. */
|
|
423
426
|
endTime?: string;
|
|
424
427
|
}
|
|
425
428
|
/**
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
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.
|
|
435
444
|
*/
|
|
436
445
|
interface SensorLogPayload {
|
|
437
446
|
requestId: string;
|
|
@@ -444,13 +453,13 @@ interface SensorLogPayload {
|
|
|
444
453
|
error?: string;
|
|
445
454
|
}
|
|
446
455
|
/**
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
* (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.
|
|
451
459
|
*
|
|
452
|
-
*
|
|
453
|
-
* `
|
|
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.
|
|
454
463
|
*/
|
|
455
464
|
interface SystemStatusPayload {
|
|
456
465
|
bluetooth: boolean;
|
|
@@ -476,29 +485,38 @@ interface SystemStatusPayload {
|
|
|
476
485
|
deviceSyncError: Record<number, string>;
|
|
477
486
|
}
|
|
478
487
|
/**
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
* Dispara o **diálogo de permissão nativo in-app** para UMA permissão
|
|
482
|
-
* (`AppPermission`), diferente de `openSettings` (que só abre as Configurações
|
|
483
|
-
* do sistema). Use para o botão "Conceder" do card de permissões Necessárias,
|
|
484
|
-
* passando um dos nomes vindos de `systemStatus.missingEssentialPermissions`
|
|
485
|
-
* (ex.: `'bluetoothScan'`).
|
|
488
|
+
* Arguments of the `requestPermission` command (JS → native).
|
|
486
489
|
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
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'`).
|
|
489
494
|
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
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.
|
|
492
499
|
*/
|
|
493
500
|
interface RequestPermissionPayload {
|
|
494
|
-
/**
|
|
501
|
+
/** Name of the permission to request, e.g. `'bluetoothScan'`. */
|
|
495
502
|
permission: string;
|
|
496
503
|
}
|
|
497
504
|
/**
|
|
498
|
-
* Payload
|
|
499
|
-
*
|
|
505
|
+
* Payload of the `navigationBack` event (native → client). Empty — the request
|
|
506
|
+
* carries no data; react by going back exactly one internal page.
|
|
500
507
|
*/
|
|
501
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
|
+
}
|
|
502
520
|
/**
|
|
503
521
|
* Language state of the native app (native → client).
|
|
504
522
|
*
|
|
@@ -557,10 +575,10 @@ interface LanguagePayload {
|
|
|
557
575
|
resolvedLanguage: string;
|
|
558
576
|
}
|
|
559
577
|
/**
|
|
560
|
-
*
|
|
561
|
-
*
|
|
578
|
+
* Discriminated map of payloads by catalogue key. Used by `PayloadOf<K>` for
|
|
579
|
+
* automatic inference in `on`.
|
|
562
580
|
*
|
|
563
|
-
* `dfu`
|
|
581
|
+
* `dfu` is hub-only and stays permissive — a sandboxed client never receives it.
|
|
564
582
|
*/
|
|
565
583
|
interface KeyPayloads {
|
|
566
584
|
pingPong: PingPongPayload;
|
|
@@ -578,15 +596,13 @@ interface KeyPayloads {
|
|
|
578
596
|
language: LanguagePayload;
|
|
579
597
|
}
|
|
580
598
|
/**
|
|
581
|
-
* Payload
|
|
582
|
-
* -
|
|
583
|
-
* -
|
|
584
|
-
*
|
|
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.
|
|
585
603
|
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
* Cliente que precisa do shape permissivo pode usar
|
|
589
|
-
* `(payload: Record<string, unknown>) => ...` explicitamente.
|
|
604
|
+
* A client that wants the permissive shape can annotate the callback
|
|
605
|
+
* explicitly: `(payload: Record<string, unknown>) => …`.
|
|
590
606
|
*/
|
|
591
607
|
type PayloadOf<K extends BridgeKey = BridgeKey> = K extends keyof KeyPayloads ? KeyPayloads[K] : Record<string, unknown>;
|
|
592
608
|
|
|
@@ -605,198 +621,222 @@ interface MockState {
|
|
|
605
621
|
}
|
|
606
622
|
interface CondorBridge {
|
|
607
623
|
/**
|
|
608
|
-
* `true`
|
|
609
|
-
* `window.CondorNative`
|
|
610
|
-
*
|
|
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.
|
|
611
627
|
*/
|
|
612
628
|
readonly isMock: boolean;
|
|
613
629
|
/**
|
|
614
|
-
*
|
|
630
|
+
* Sends a versioned handshake to the native side and awaits `handshake-ack`.
|
|
615
631
|
*
|
|
616
|
-
*
|
|
617
|
-
*
|
|
632
|
+
* Only one handshake may be in flight; a parallel call before the first ack
|
|
633
|
+
* arrives is rejected with an explicit error.
|
|
618
634
|
*
|
|
619
|
-
* **
|
|
620
|
-
* `Bridge.mockEmit('handshake-ack', {nativeVersion, accepted})`
|
|
621
|
-
*
|
|
622
|
-
*
|
|
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.
|
|
623
639
|
*/
|
|
624
640
|
handshake(opts: HandshakeOptions): Promise<HandshakeAck>;
|
|
625
641
|
/**
|
|
626
|
-
*
|
|
642
|
+
* Subscribes to an observable key. Returns an `unsubscribe` function.
|
|
627
643
|
*
|
|
628
|
-
*
|
|
629
|
-
*
|
|
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`).
|
|
630
646
|
*/
|
|
631
647
|
on<K extends BridgeKey>(key: K, cb: (payload: PayloadOf<K>) => void): () => void;
|
|
632
648
|
/**
|
|
633
|
-
*
|
|
634
|
-
*
|
|
635
|
-
*
|
|
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.
|
|
636
654
|
*
|
|
637
|
-
*
|
|
655
|
+
* In mock mode it is recorded in the internal buffer and logged with
|
|
656
|
+
* `console.debug`.
|
|
638
657
|
*/
|
|
639
658
|
send(type: string, payload?: Record<string, unknown>): void;
|
|
640
659
|
/**
|
|
641
|
-
*
|
|
660
|
+
* Opens the app's native Settings screen. Equivalent to
|
|
642
661
|
* `Bridge.send('openSettings')`.
|
|
643
662
|
*/
|
|
644
663
|
openSettings(): void;
|
|
645
664
|
/**
|
|
646
|
-
*
|
|
647
|
-
*
|
|
665
|
+
* Registers a transform applied to **every** JS → native message, the
|
|
666
|
+
* handshake included, immediately before it is sent. Pass `null` to remove.
|
|
648
667
|
*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
*
|
|
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.**
|
|
653
674
|
*/
|
|
654
675
|
setOutgoingStamp(stamp: OutgoingStamp | null): void;
|
|
655
676
|
/**
|
|
656
|
-
*
|
|
657
|
-
*
|
|
677
|
+
* Fires `window.CondorBridge.emit(key, payload)` programmatically — useful to
|
|
678
|
+
* simulate native events in tests and in browser development.
|
|
658
679
|
*
|
|
659
|
-
* **Throws**
|
|
680
|
+
* **Throws** when the active adapter is not the mock.
|
|
660
681
|
*/
|
|
661
682
|
mockEmit(key: string, payload?: unknown): void;
|
|
662
683
|
/**
|
|
663
|
-
* Snapshot
|
|
664
|
-
* subscribers
|
|
684
|
+
* Snapshot of the mock adapter state: messages sent, and keys with active
|
|
685
|
+
* subscribers. Useful for assertions in client tests.
|
|
665
686
|
*
|
|
666
|
-
* **Throws**
|
|
687
|
+
* **Throws** when the active adapter is not the mock.
|
|
667
688
|
*/
|
|
668
689
|
getMockState(): MockState;
|
|
669
690
|
/**
|
|
670
|
-
*
|
|
671
|
-
* 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.
|
|
672
693
|
*
|
|
673
|
-
* **Throws**
|
|
694
|
+
* **Throws** when the active adapter is not the mock, or `document` does not
|
|
695
|
+
* exist.
|
|
674
696
|
*/
|
|
675
697
|
installDevPanel(options?: DevPanelOptions): void;
|
|
676
698
|
}
|
|
677
699
|
declare const Bridge: CondorBridge;
|
|
678
700
|
|
|
679
701
|
/**
|
|
680
|
-
* `BridgeClient`
|
|
681
|
-
*
|
|
702
|
+
* `BridgeClient` — **the API for client code**, running inside
|
|
703
|
+
* `<iframe sandbox="allow-scripts">` in the Condor Canvas app.
|
|
682
704
|
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
*
|
|
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.
|
|
687
710
|
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
711
|
+
* It deliberately does **not** expose `installDevPanel`, `mockEmit`, `isMock`
|
|
712
|
+
* or `getMockState`: those belong to the shell (`BridgeShell`).
|
|
690
713
|
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
* ou `window.CondorBridge` (que deveriam ter sido deletados pela Camada 1).
|
|
696
|
-
* 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.
|
|
697
718
|
*/
|
|
698
719
|
|
|
699
720
|
interface BridgeClientHandshakeAck {
|
|
700
721
|
shellVersion: string;
|
|
701
722
|
/**
|
|
702
|
-
*
|
|
703
|
-
*
|
|
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.
|
|
704
726
|
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
707
|
-
*
|
|
708
|
-
*
|
|
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.
|
|
709
732
|
*/
|
|
710
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
|
+
*/
|
|
711
739
|
accepted: boolean;
|
|
712
740
|
}
|
|
713
741
|
interface CondorBridgeClient {
|
|
714
|
-
/**
|
|
715
|
-
* Versão do protocolo do client.
|
|
716
|
-
*/
|
|
742
|
+
/** Version of the client-side postMessage protocol. */
|
|
717
743
|
readonly protocolVersion: string;
|
|
718
744
|
/**
|
|
719
|
-
*
|
|
720
|
-
*
|
|
721
|
-
*
|
|
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.
|
|
722
752
|
*/
|
|
723
753
|
on<K extends BridgeKey>(key: K, cb: (payload: PayloadOf<K>) => void): () => void;
|
|
724
754
|
/**
|
|
725
|
-
*
|
|
726
|
-
*
|
|
727
|
-
*
|
|
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`.
|
|
728
763
|
*/
|
|
729
764
|
send(command: string, args?: Record<string, unknown>): void;
|
|
730
|
-
/**
|
|
731
|
-
* Abre Settings nativa. Equivalente a `send('openSettings')`.
|
|
732
|
-
*/
|
|
765
|
+
/** Opens the native Settings screen. Equivalent to `send('openSettings')`. */
|
|
733
766
|
openSettings(): void;
|
|
734
767
|
/**
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
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`.
|
|
738
772
|
*
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
*
|
|
742
|
-
*
|
|
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.
|
|
743
777
|
*/
|
|
744
778
|
fetchSensorLog(query: SensorLogQueryPayload, opts?: {
|
|
745
779
|
timeoutMs?: number;
|
|
746
780
|
}): Promise<SensorLogPayload>;
|
|
747
781
|
/**
|
|
748
|
-
*
|
|
749
|
-
*
|
|
750
|
-
*
|
|
751
|
-
*
|
|
752
|
-
*
|
|
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.
|
|
753
790
|
*/
|
|
754
791
|
requestPermission(permission: string): void;
|
|
755
792
|
/**
|
|
756
|
-
*
|
|
757
|
-
*
|
|
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`.
|
|
758
796
|
*
|
|
759
|
-
*
|
|
797
|
+
* Rejects after 5 s if the shell does not answer. Only one handshake may be
|
|
798
|
+
* in flight at a time.
|
|
760
799
|
*/
|
|
761
800
|
handshake(opts: {
|
|
762
801
|
clientVersion: string;
|
|
763
802
|
}): Promise<BridgeClientHandshakeAck>;
|
|
764
803
|
/**
|
|
765
|
-
*
|
|
766
|
-
* **
|
|
767
|
-
*
|
|
768
|
-
*
|
|
769
|
-
*
|
|
770
|
-
*
|
|
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.
|
|
771
811
|
*
|
|
772
|
-
*
|
|
812
|
+
* Equivalent to `send('navigationState', { canGoBack })`.
|
|
773
813
|
*/
|
|
774
814
|
reportNavigation(canGoBack: boolean): void;
|
|
775
815
|
/**
|
|
776
|
-
*
|
|
777
|
-
*
|
|
778
|
-
*
|
|
779
|
-
*
|
|
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.
|
|
780
820
|
*/
|
|
781
821
|
onBack(cb: () => void): () => void;
|
|
782
822
|
}
|
|
783
823
|
declare const BridgeClient: CondorBridgeClient;
|
|
784
824
|
|
|
785
825
|
/**
|
|
786
|
-
*
|
|
826
|
+
* Valibot schemas for the shell↔client `postMessage` protocol.
|
|
787
827
|
*
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
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`.
|
|
792
833
|
*
|
|
793
|
-
* **Threat model:**
|
|
794
|
-
*
|
|
795
|
-
*
|
|
796
|
-
* `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.
|
|
797
837
|
*/
|
|
798
838
|
|
|
799
|
-
declare const
|
|
839
|
+
declare const ClientToShellMessageSchema: v.VariantSchema<"type", [v.ObjectSchema<{
|
|
800
840
|
readonly type: v.LiteralSchema<"subscribe", undefined>;
|
|
801
841
|
readonly key: v.PicklistSchema<[string, ...string[]], undefined>;
|
|
802
842
|
}, undefined>, v.ObjectSchema<{
|
|
@@ -826,8 +866,9 @@ declare const ClientToShellMessage: v.VariantSchema<"type", [v.ObjectSchema<{
|
|
|
826
866
|
readonly condorBridgeReachable: v.BooleanSchema<undefined>;
|
|
827
867
|
readonly nativeChannelReachable: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
828
868
|
}, undefined>], undefined>;
|
|
829
|
-
|
|
830
|
-
|
|
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<{
|
|
831
872
|
readonly type: v.LiteralSchema<"event", undefined>;
|
|
832
873
|
readonly key: v.PicklistSchema<[string, ...string[]], undefined>;
|
|
833
874
|
readonly payload: v.UnknownSchema;
|
|
@@ -836,12 +877,12 @@ declare const ShellToClientMessage: v.VariantSchema<"type", [v.ObjectSchema<{
|
|
|
836
877
|
readonly ack: v.ObjectSchema<{
|
|
837
878
|
readonly shellVersion: v.StringSchema<undefined>;
|
|
838
879
|
/**
|
|
839
|
-
*
|
|
880
|
+
* Schema version declared by the **native** side, relayed by the shell.
|
|
840
881
|
*
|
|
841
|
-
* `null`
|
|
842
|
-
* mock
|
|
843
|
-
*
|
|
844
|
-
*
|
|
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.
|
|
845
886
|
*/
|
|
846
887
|
readonly nativeVersion: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
847
888
|
readonly accepted: v.BooleanSchema<undefined>;
|
|
@@ -854,22 +895,23 @@ declare const ShellToClientMessage: v.VariantSchema<"type", [v.ObjectSchema<{
|
|
|
854
895
|
}, undefined>, v.ObjectSchema<{
|
|
855
896
|
readonly type: v.LiteralSchema<"verify-sandbox", undefined>;
|
|
856
897
|
}, undefined>], undefined>;
|
|
857
|
-
|
|
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>;
|
|
858
900
|
/**
|
|
859
|
-
*
|
|
860
|
-
*
|
|
861
|
-
* 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.
|
|
862
904
|
*
|
|
863
|
-
* - `readOnlyKeys
|
|
864
|
-
* - `allowedCommands
|
|
865
|
-
* - `requiresConfirmation
|
|
866
|
-
*
|
|
867
|
-
* - `deniedCommands
|
|
868
|
-
*
|
|
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.
|
|
869
911
|
*
|
|
870
|
-
* **
|
|
871
|
-
*
|
|
872
|
-
*
|
|
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.
|
|
873
915
|
*/
|
|
874
916
|
interface ShellPolicy {
|
|
875
917
|
readOnlyKeys: string[];
|
|
@@ -878,68 +920,73 @@ interface ShellPolicy {
|
|
|
878
920
|
deniedCommands: string[];
|
|
879
921
|
}
|
|
880
922
|
/**
|
|
881
|
-
*
|
|
923
|
+
* Argument of the `BridgeShell` `onConfirmationRequest` callback.
|
|
882
924
|
*
|
|
883
|
-
* `approve: true`
|
|
884
|
-
* `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.
|
|
885
929
|
*/
|
|
886
930
|
interface ConfirmationRequest {
|
|
887
931
|
command: string;
|
|
888
932
|
args: Record<string, unknown>;
|
|
889
933
|
}
|
|
890
934
|
/**
|
|
891
|
-
*
|
|
892
|
-
*
|
|
893
|
-
*
|
|
894
|
-
*
|
|
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.
|
|
895
939
|
*/
|
|
896
940
|
interface SandboxVerifyResult {
|
|
897
941
|
flutterBridgeReachable: boolean;
|
|
898
942
|
condorBridgeReachable: boolean;
|
|
899
|
-
/** `window.CondorNative`
|
|
943
|
+
/** Whether `window.CondorNative` is visible inside the iframe. */
|
|
900
944
|
nativeChannelReachable: boolean;
|
|
945
|
+
/** `true` only when none of the three is reachable. */
|
|
901
946
|
ok: boolean;
|
|
902
947
|
}
|
|
903
948
|
|
|
904
949
|
/**
|
|
905
|
-
* `BridgeShell`
|
|
906
|
-
*
|
|
950
|
+
* `BridgeShell` — the API for the **Condor-controlled shell** that wraps
|
|
951
|
+
* external client content inside `<iframe sandbox="allow-scripts">`.
|
|
907
952
|
*
|
|
908
|
-
*
|
|
909
|
-
*
|
|
910
|
-
*
|
|
911
|
-
*
|
|
912
|
-
*
|
|
913
|
-
*
|
|
914
|
-
*
|
|
915
|
-
*
|
|
916
|
-
*
|
|
917
|
-
*
|
|
918
|
-
*
|
|
919
|
-
*
|
|
920
|
-
*
|
|
921
|
-
*
|
|
922
|
-
*
|
|
923
|
-
* `
|
|
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.
|
|
924
970
|
*/
|
|
925
971
|
|
|
926
972
|
interface BridgeShellOptions {
|
|
927
973
|
/**
|
|
928
|
-
*
|
|
929
|
-
*
|
|
974
|
+
* The iframe hosting the client. It may be empty at construction time —
|
|
975
|
+
* `bootstrapClientIframe` is called later to populate it.
|
|
930
976
|
*/
|
|
931
977
|
iframe: HTMLIFrameElement;
|
|
932
978
|
/**
|
|
933
|
-
*
|
|
934
|
-
*
|
|
979
|
+
* The default-deny policy. Every key and command arriving from the iframe is
|
|
980
|
+
* checked against these lists. See `ShellPolicy`.
|
|
935
981
|
*/
|
|
936
982
|
policy: ShellPolicy;
|
|
937
983
|
/**
|
|
938
|
-
*
|
|
939
|
-
*
|
|
940
|
-
*
|
|
941
|
-
*
|
|
942
|
-
*
|
|
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.
|
|
943
990
|
*/
|
|
944
991
|
onConfirmationRequest?: (req: ConfirmationRequest) => boolean | Promise<boolean>;
|
|
945
992
|
}
|
|
@@ -950,61 +997,60 @@ declare class BridgeShell {
|
|
|
950
997
|
private readonly subscriptions;
|
|
951
998
|
private detached;
|
|
952
999
|
/**
|
|
953
|
-
*
|
|
954
|
-
*
|
|
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.
|
|
955
1002
|
*/
|
|
956
1003
|
private nativeVersion;
|
|
957
|
-
/**
|
|
1004
|
+
/** In-flight native handshake; guarantees a single call. */
|
|
958
1005
|
private nativeHandshake;
|
|
959
1006
|
private pendingVerifies;
|
|
960
1007
|
constructor(opts: BridgeShellOptions);
|
|
961
1008
|
/**
|
|
962
|
-
* **
|
|
1009
|
+
* **Layer 1.** Bootstraps by fetching a URL.
|
|
963
1010
|
*
|
|
964
|
-
*
|
|
965
|
-
*
|
|
1011
|
+
* A shortcut for the common HTTP/HTTPS case: it runs `fetch(clientPath)` and
|
|
1012
|
+
* delegates to `bootstrapClientIframeWithHtml`.
|
|
966
1013
|
*
|
|
967
|
-
* **
|
|
968
|
-
*
|
|
969
|
-
*
|
|
970
|
-
* `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.
|
|
971
1018
|
*
|
|
972
|
-
* **
|
|
973
|
-
*
|
|
974
|
-
* 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).
|
|
975
1021
|
*/
|
|
976
1022
|
bootstrapClientIframe(clientPath?: string): Promise<void>;
|
|
977
1023
|
/**
|
|
978
|
-
* **
|
|
1024
|
+
* **Layer 1.** Bootstraps from an HTML string.
|
|
979
1025
|
*
|
|
980
|
-
*
|
|
981
|
-
* build
|
|
982
|
-
*
|
|
983
|
-
*
|
|
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.
|
|
984
1030
|
*
|
|
985
|
-
* Use
|
|
986
|
-
* bundle inlined
|
|
987
|
-
*
|
|
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)`.
|
|
988
1035
|
*
|
|
989
|
-
* **
|
|
990
|
-
*
|
|
991
|
-
* 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).
|
|
992
1038
|
*/
|
|
993
1039
|
bootstrapClientIframeWithHtml(clientHtml: string): Promise<void>;
|
|
994
1040
|
private waitForIframeLoad;
|
|
995
1041
|
/**
|
|
996
|
-
* **
|
|
997
|
-
*
|
|
998
|
-
*
|
|
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.
|
|
999
1045
|
*
|
|
1000
|
-
* `ok`
|
|
1001
|
-
* `false`, **shell
|
|
1002
|
-
*
|
|
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.
|
|
1003
1049
|
*/
|
|
1004
1050
|
verifySandbox(): Promise<SandboxVerifyResult>;
|
|
1005
1051
|
/**
|
|
1006
|
-
*
|
|
1007
|
-
*
|
|
1052
|
+
* Tears the shell down: removes the `message` listener, cancels pending
|
|
1053
|
+
* subscriptions on the native `Bridge`, and clears internal state.
|
|
1008
1054
|
*/
|
|
1009
1055
|
detach(): void;
|
|
1010
1056
|
private onMessage;
|
|
@@ -1012,16 +1058,16 @@ declare class BridgeShell {
|
|
|
1012
1058
|
private handleUnsubscribe;
|
|
1013
1059
|
private handleSend;
|
|
1014
1060
|
/**
|
|
1015
|
-
*
|
|
1016
|
-
*
|
|
1061
|
+
* Resolves `nativeVersion`, firing the native handshake the first time and
|
|
1062
|
+
* caching the result.
|
|
1017
1063
|
*
|
|
1018
|
-
* ⚠️ `Bridge.handshake` **
|
|
1019
|
-
*
|
|
1020
|
-
*
|
|
1021
|
-
*
|
|
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.
|
|
1022
1068
|
*/
|
|
1023
1069
|
private resolveNativeVersion;
|
|
1024
1070
|
private postToClient;
|
|
1025
1071
|
}
|
|
1026
1072
|
|
|
1027
|
-
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, NATIVE_CHANNEL_NAME, type OutgoingStamp, 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 };
|