@condorinst/mobile-bridge 0.18.0 → 0.19.1
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 +78 -0
- package/README.md +66 -11
- package/contract/bridge-contract.json +202 -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 +444 -396
- package/dist/index.d.ts +444 -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 +120 -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,62 @@ 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
|
-
*
|
|
235
|
-
*
|
|
236
|
-
* consumidor JS precisar.
|
|
236
|
+
* The app enforces no device cap. A commercial rule currently limits a user to
|
|
237
|
+
* two linked devices, but that can change with no signal in this contract — read
|
|
238
|
+
* the length of the array rather than hard-coding it.
|
|
237
239
|
*/
|
|
238
240
|
interface UserDevicePayload {
|
|
239
241
|
lastUpdate: number;
|
|
@@ -242,23 +244,22 @@ interface UserDevicePayload {
|
|
|
242
244
|
devicesUserConfig: Array<Record<string, unknown>>;
|
|
243
245
|
}
|
|
244
246
|
/**
|
|
245
|
-
*
|
|
246
|
-
* `user_cmd=log` —
|
|
247
|
+
* Raw light-sensor channels, under the names the backend uses in
|
|
248
|
+
* `user_cmd=log` — the same vocabulary for history and for real time.
|
|
247
249
|
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
250
|
+
* Present from **nativeVersion 0.16.0**. A bundle compiled against this SDK can
|
|
251
|
+
* run on an older native app that does not emit them: check `nativeVersion`
|
|
252
|
+
* from the handshake ack before depending on them, or read defensively. They
|
|
253
|
+
* are deliberately **not** optional in the type — optional would hide the
|
|
254
|
+
* version skew instead of making it explicit.
|
|
253
255
|
*
|
|
254
|
-
* ⚠️
|
|
255
|
-
* `0.16.x`
|
|
256
|
-
*
|
|
257
|
-
* iframe.
|
|
256
|
+
* ⚠️ `nativeVersion` only reaches the client from **SDK 0.17.0**: up to
|
|
257
|
+
* `0.16.x` the shell answered the client handshake with `shellVersion` alone,
|
|
258
|
+
* so this check was impossible to satisfy from inside the iframe.
|
|
258
259
|
*
|
|
259
|
-
* `null`
|
|
260
|
-
* zero"
|
|
261
|
-
* `
|
|
260
|
+
* `null` means **the channel does not exist on this hardware generation**,
|
|
261
|
+
* never "the reading was zero" — `0.0` is a valid reading. Coercing `null` to
|
|
262
|
+
* `0` fabricates data.
|
|
262
263
|
*/
|
|
263
264
|
type LightSensorsRawChannels = {
|
|
264
265
|
light_sensor_f1: number;
|
|
@@ -269,18 +270,18 @@ type LightSensorsRawChannels = {
|
|
|
269
270
|
light_sensor_f6: number;
|
|
270
271
|
light_sensor_f7: number;
|
|
271
272
|
light_sensor_f8: number;
|
|
272
|
-
/**
|
|
273
|
+
/** Total light (`visible` on the device wire format). */
|
|
273
274
|
light_sensor_clear: number;
|
|
274
|
-
/**
|
|
275
|
+
/** Near infrared. */
|
|
275
276
|
light_sensor_nir: number;
|
|
276
277
|
};
|
|
277
278
|
/**
|
|
278
|
-
*
|
|
279
|
+
* A real-time reading from an ActLumus gen1 (`type: 'actlumus'`).
|
|
279
280
|
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
281
|
+
* Mirrors `LightSensorsActlumus1GraphicData.toMap()` in `condor_interactors`.
|
|
282
|
+
* Gen1 has neither the tristimulus channels nor flicker detect — those four
|
|
283
|
+
* keys arrive as `null`, present in the schema so that its shape does not
|
|
284
|
+
* change between hardware generations.
|
|
284
285
|
*/
|
|
285
286
|
type LightSensorsActlumus1Data = LightSensorsRawChannels & {
|
|
286
287
|
time: number;
|
|
@@ -302,17 +303,17 @@ type LightSensorsActlumus1Data = LightSensorsRawChannels & {
|
|
|
302
303
|
light_sensor_fd: null;
|
|
303
304
|
};
|
|
304
305
|
/**
|
|
305
|
-
*
|
|
306
|
+
* A real-time reading from an ActLumus Plus / gen2 (`type: 'actlumus2'`).
|
|
306
307
|
*
|
|
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`.
|
|
308
|
+
* Mirrors `LightSensorsActlumus2GraphicData.toMap()` in `condor_interactors`.
|
|
313
309
|
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
310
|
+
* ⚠️ The wavelengths are **not** the gen1 ones: 14 of them (395…800) against
|
|
311
|
+
* 10 (390…750), with no value in common. A chart that reads the gen1 list on a
|
|
312
|
+
* gen2 device finds `undefined` in every series and renders empty. Always
|
|
313
|
+
* discriminate on `type`.
|
|
314
|
+
*
|
|
315
|
+
* `light_sensor_z/_y/_x` are the CIE 1931 tristimulus values — Z in the blue,
|
|
316
|
+
* Y in the green, X in the red.
|
|
316
317
|
*/
|
|
317
318
|
type LightSensorsActlumus2Data = LightSensorsRawChannels & {
|
|
318
319
|
time: number;
|
|
@@ -335,17 +336,20 @@ type LightSensorsActlumus2Data = LightSensorsRawChannels & {
|
|
|
335
336
|
light_sensor_z: number;
|
|
336
337
|
light_sensor_y: number;
|
|
337
338
|
light_sensor_x: number;
|
|
338
|
-
/** Flicker detect.
|
|
339
|
+
/** Flicker detect. Not part of the spectrum matrix. */
|
|
339
340
|
light_sensor_fd: number;
|
|
340
341
|
};
|
|
341
342
|
/**
|
|
342
|
-
*
|
|
343
|
-
* `{serialNo, type, data
|
|
343
|
+
* Real-time light-sensor stream. Shape:
|
|
344
|
+
* `{ serialNo, type, data }`.
|
|
345
|
+
*
|
|
346
|
+
* A union discriminated by `type`, which is the `DeviceType.name` on the Dart
|
|
347
|
+
* side. `data` carries **both the derived spectrum and the raw channels it came
|
|
348
|
+
* from** — before 0.16.0 only the spectrum crossed the bridge, and a probe
|
|
349
|
+
* looking up a raw channel by name read `undefined`.
|
|
344
350
|
*
|
|
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`.
|
|
351
|
+
* This stream only flows while real-time is on for that device; it is off by
|
|
352
|
+
* default. See the `realTime` command.
|
|
349
353
|
*/
|
|
350
354
|
type DeviceLightSensorsPayload = {
|
|
351
355
|
serialNo: number;
|
|
@@ -373,16 +377,17 @@ interface DeleteMemoryAckPayload {
|
|
|
373
377
|
success: boolean;
|
|
374
378
|
}
|
|
375
379
|
/**
|
|
376
|
-
*
|
|
377
|
-
*
|
|
380
|
+
* BLE connection status. Mirrors the `openConnection` subscriber in the app's
|
|
381
|
+
* device notification service.
|
|
378
382
|
*/
|
|
379
383
|
interface DeviceConnectionPayload {
|
|
380
384
|
serialNo: number;
|
|
381
385
|
isConnect: boolean;
|
|
382
386
|
}
|
|
383
387
|
/**
|
|
384
|
-
*
|
|
385
|
-
*
|
|
388
|
+
* Device event or status notice. Mirrors `DevicesNotification.toMap()`.
|
|
389
|
+
* `message` is already localized by the app; `messageCode` is the stable
|
|
390
|
+
* identifier to branch on.
|
|
386
391
|
*/
|
|
387
392
|
interface DeviceNotificationPayload {
|
|
388
393
|
serialNo: number;
|
|
@@ -392,46 +397,52 @@ interface DeviceNotificationPayload {
|
|
|
392
397
|
isError: boolean;
|
|
393
398
|
}
|
|
394
399
|
/**
|
|
395
|
-
*
|
|
400
|
+
* Parameters of the `sensorLog` command.
|
|
396
401
|
*
|
|
397
|
-
* `startTime
|
|
398
|
-
* `endTime` **
|
|
399
|
-
* (
|
|
402
|
+
* `startTime` / `endTime` delimit a period — `startTime` is **inclusive**,
|
|
403
|
+
* `endTime` **exclusive**, both in the strict format `YYYY-MM-DD HH:MM:SS`
|
|
404
|
+
* (no `T`, no offset, no date-only form). Requires nativeVersion >= 0.15.0.
|
|
400
405
|
*
|
|
401
|
-
* ⚠️
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
+
* ⚠️ These timestamps are **device-local time, not UTC**: `log_time` passes
|
|
407
|
+
* through from the device RTC. Build the range with local `Date` getters
|
|
408
|
+
* (`getFullYear()`, `getHours()`, …) and **never** with `toISOString()` —
|
|
409
|
+
* converting to UTC shifts every series by the timezone offset, producing
|
|
410
|
+
* plausible data and no visible error.
|
|
406
411
|
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
412
|
+
* The `startingAfter` / `endingBefore` cursors are **opaque** and mutually
|
|
413
|
+
* exclusive: echo back the value you received, never construct one.
|
|
409
414
|
*/
|
|
410
415
|
interface SensorLogQueryPayload {
|
|
411
|
-
/**
|
|
416
|
+
/** Echoed back in the reply — correlates concurrent requests. */
|
|
412
417
|
requestId: string;
|
|
413
418
|
serialNo: number;
|
|
414
|
-
/** `wrist`
|
|
419
|
+
/** `wrist` or `necklace`. Omit for no filter. */
|
|
415
420
|
bodyPart?: string;
|
|
416
|
-
/** 1..1000 (default
|
|
421
|
+
/** 1..1000 (backend default is 10). Outside the range: `INVALID_LIMIT`. */
|
|
417
422
|
limit?: number;
|
|
418
423
|
startingAfter?: string;
|
|
419
424
|
endingBefore?: string;
|
|
420
|
-
/**
|
|
425
|
+
/** Inclusive, `YYYY-MM-DD HH:MM:SS`, in device-local time. */
|
|
421
426
|
startTime?: string;
|
|
422
|
-
/**
|
|
427
|
+
/** Exclusive, `YYYY-MM-DD HH:MM:SS`, in device-local time. */
|
|
423
428
|
endTime?: string;
|
|
424
429
|
}
|
|
425
430
|
/**
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
431
|
+
* Reply to the `sensorLog` command.
|
|
432
|
+
*
|
|
433
|
+
* An envelope carrying the echoed `requestId` for correlation (so concurrent
|
|
434
|
+
* keyset pagination works) plus `success` / `error`. On success it brings the
|
|
435
|
+
* page of logs — chronological ascending, temperature in °C, naive timestamps
|
|
436
|
+
* in **device-local time** — and the opaque cursors.
|
|
437
|
+
*
|
|
438
|
+
* With a time range, `hasMore` / `hasPrevious` are computed **within the
|
|
439
|
+
* period**, so `hasMore: false` means "this period is complete", not "no more
|
|
440
|
+
* data exists".
|
|
441
|
+
*
|
|
442
|
+
* On failure, `error` carries the contract code (`CONFLICTING_CURSORS`,
|
|
443
|
+
* `INVALID_TIME_RANGE`, `invalid session`, …) — a **diagnostic code, never
|
|
444
|
+
* text to show a user**. Note that `fetchSensorLog` RESOLVES with
|
|
445
|
+
* `success: false`; it only rejects on timeout.
|
|
435
446
|
*/
|
|
436
447
|
interface SensorLogPayload {
|
|
437
448
|
requestId: string;
|
|
@@ -444,13 +455,13 @@ interface SensorLogPayload {
|
|
|
444
455
|
error?: string;
|
|
445
456
|
}
|
|
446
457
|
/**
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
* (ex.: Bluetooth desligado impede o scan/conexão dos devices).
|
|
458
|
+
* Aggregate environment status (native → JS). Mirrors `SysNotification` in the
|
|
459
|
+
* app: the flags a client uses to explain why things are not working — for
|
|
460
|
+
* instance, Bluetooth being off prevents scanning and connecting to devices.
|
|
451
461
|
*
|
|
452
|
-
*
|
|
453
|
-
* `
|
|
462
|
+
* Emitted on attach (before your bundle exists), again right after
|
|
463
|
+
* `handshake-ack`, and on every change. `deviceSyncError` maps `serialNo` to an
|
|
464
|
+
* error message the app has already localized.
|
|
454
465
|
*/
|
|
455
466
|
interface SystemStatusPayload {
|
|
456
467
|
bluetooth: boolean;
|
|
@@ -476,29 +487,38 @@ interface SystemStatusPayload {
|
|
|
476
487
|
deviceSyncError: Record<number, string>;
|
|
477
488
|
}
|
|
478
489
|
/**
|
|
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'`).
|
|
490
|
+
* Arguments of the `requestPermission` command (JS → native).
|
|
486
491
|
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
492
|
+
* Triggers the **in-app native permission dialog** for ONE permission, unlike
|
|
493
|
+
* `openSettings`, which only opens the system Settings. Use it for the "Grant"
|
|
494
|
+
* button of your required-permissions card, passing a name taken from
|
|
495
|
+
* `systemStatus.missingEssentialPermissions` (for example `'bluetoothScan'`).
|
|
489
496
|
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
497
|
+
* **There is no result key.** The native side re-emits `systemStatus` after the
|
|
498
|
+
* person answers, so your card disappears when the array empties. Waiting for a
|
|
499
|
+
* dedicated ack means waiting forever. A name outside the native enum is
|
|
500
|
+
* dropped in silence.
|
|
492
501
|
*/
|
|
493
502
|
interface RequestPermissionPayload {
|
|
494
|
-
/**
|
|
503
|
+
/** Name of the permission to request, e.g. `'bluetoothScan'`. */
|
|
495
504
|
permission: string;
|
|
496
505
|
}
|
|
497
506
|
/**
|
|
498
|
-
* Payload
|
|
499
|
-
*
|
|
507
|
+
* Payload of the `navigationBack` event (native → client). Empty — the request
|
|
508
|
+
* carries no data; react by going back exactly one internal page.
|
|
500
509
|
*/
|
|
501
510
|
type NavigationBackPayload = Record<string, never>;
|
|
511
|
+
/**
|
|
512
|
+
* Arguments of the `navigationState` command (client → native). The client
|
|
513
|
+
* reports whether it has internal history to go back to; the native side caches
|
|
514
|
+
* it and uses it to decide what the OS back button does. See
|
|
515
|
+
* `BridgeClient.reportNavigation`, and call it on every internal navigation —
|
|
516
|
+
* not once at startup.
|
|
517
|
+
*/
|
|
518
|
+
interface NavigationStatePayload {
|
|
519
|
+
/** `true` when the client has a previous internal page to return to. */
|
|
520
|
+
canGoBack: boolean;
|
|
521
|
+
}
|
|
502
522
|
/**
|
|
503
523
|
* Language state of the native app (native → client).
|
|
504
524
|
*
|
|
@@ -557,10 +577,10 @@ interface LanguagePayload {
|
|
|
557
577
|
resolvedLanguage: string;
|
|
558
578
|
}
|
|
559
579
|
/**
|
|
560
|
-
*
|
|
561
|
-
*
|
|
580
|
+
* Discriminated map of payloads by catalogue key. Used by `PayloadOf<K>` for
|
|
581
|
+
* automatic inference in `on`.
|
|
562
582
|
*
|
|
563
|
-
* `dfu`
|
|
583
|
+
* `dfu` is hub-only and stays permissive — a sandboxed client never receives it.
|
|
564
584
|
*/
|
|
565
585
|
interface KeyPayloads {
|
|
566
586
|
pingPong: PingPongPayload;
|
|
@@ -578,15 +598,13 @@ interface KeyPayloads {
|
|
|
578
598
|
language: LanguagePayload;
|
|
579
599
|
}
|
|
580
600
|
/**
|
|
581
|
-
* Payload
|
|
582
|
-
* -
|
|
583
|
-
* -
|
|
584
|
-
*
|
|
601
|
+
* Payload typed by key. Distributive:
|
|
602
|
+
* - keys catalogued in `KeyPayloads` resolve to their strict payload;
|
|
603
|
+
* - any other key resolves to a permissive `Record<string, unknown>`, to keep
|
|
604
|
+
* the catalogue extensible.
|
|
585
605
|
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
* Cliente que precisa do shape permissivo pode usar
|
|
589
|
-
* `(payload: Record<string, unknown>) => ...` explicitamente.
|
|
606
|
+
* A client that wants the permissive shape can annotate the callback
|
|
607
|
+
* explicitly: `(payload: Record<string, unknown>) => …`.
|
|
590
608
|
*/
|
|
591
609
|
type PayloadOf<K extends BridgeKey = BridgeKey> = K extends keyof KeyPayloads ? KeyPayloads[K] : Record<string, unknown>;
|
|
592
610
|
|
|
@@ -605,198 +623,222 @@ interface MockState {
|
|
|
605
623
|
}
|
|
606
624
|
interface CondorBridge {
|
|
607
625
|
/**
|
|
608
|
-
* `true`
|
|
609
|
-
* `window.CondorNative`
|
|
610
|
-
*
|
|
626
|
+
* `true` when the active adapter is the mock — a plain browser, with neither
|
|
627
|
+
* `window.CondorNative` nor `window.flutter_inappwebview`. `false` inside the
|
|
628
|
+
* Flutter WebView.
|
|
611
629
|
*/
|
|
612
630
|
readonly isMock: boolean;
|
|
613
631
|
/**
|
|
614
|
-
*
|
|
632
|
+
* Sends a versioned handshake to the native side and awaits `handshake-ack`.
|
|
615
633
|
*
|
|
616
|
-
*
|
|
617
|
-
*
|
|
634
|
+
* Only one handshake may be in flight; a parallel call before the first ack
|
|
635
|
+
* arrives is rejected with an explicit error.
|
|
618
636
|
*
|
|
619
|
-
* **
|
|
620
|
-
* `Bridge.mockEmit('handshake-ack', {nativeVersion, accepted})`
|
|
621
|
-
*
|
|
622
|
-
*
|
|
637
|
+
* **In mock mode it does not resolve on its own.** The caller must emit
|
|
638
|
+
* `Bridge.mockEmit('handshake-ack', { nativeVersion, accepted })` to unblock
|
|
639
|
+
* the promise. That is deliberate: it forces tests to be explicit about the
|
|
640
|
+
* native state they are simulating.
|
|
623
641
|
*/
|
|
624
642
|
handshake(opts: HandshakeOptions): Promise<HandshakeAck>;
|
|
625
643
|
/**
|
|
626
|
-
*
|
|
644
|
+
* Subscribes to an observable key. Returns an `unsubscribe` function.
|
|
627
645
|
*
|
|
628
|
-
*
|
|
629
|
-
*
|
|
646
|
+
* Multiple subscribers per key are allowed; a throw in one does not affect
|
|
647
|
+
* the others (it is caught and logged with `console.error`).
|
|
630
648
|
*/
|
|
631
649
|
on<K extends BridgeKey>(key: K, cb: (payload: PayloadOf<K>) => void): () => void;
|
|
632
650
|
/**
|
|
633
|
-
*
|
|
634
|
-
*
|
|
635
|
-
*
|
|
651
|
+
* Sends a generic message to the native side, for commands without a
|
|
652
|
+
* dedicated helper. `type` is the string the native side routes on.
|
|
653
|
+
*
|
|
654
|
+
* Fire-and-forget: no return value and no delivery confirmation. A payload
|
|
655
|
+
* above 64 KB, or an unknown `type`, is dropped natively without a reply.
|
|
636
656
|
*
|
|
637
|
-
*
|
|
657
|
+
* In mock mode it is recorded in the internal buffer and logged with
|
|
658
|
+
* `console.debug`.
|
|
638
659
|
*/
|
|
639
660
|
send(type: string, payload?: Record<string, unknown>): void;
|
|
640
661
|
/**
|
|
641
|
-
*
|
|
662
|
+
* Opens the app's native Settings screen. Equivalent to
|
|
642
663
|
* `Bridge.send('openSettings')`.
|
|
643
664
|
*/
|
|
644
665
|
openSettings(): void;
|
|
645
666
|
/**
|
|
646
|
-
*
|
|
647
|
-
*
|
|
667
|
+
* Registers a transform applied to **every** JS → native message, the
|
|
668
|
+
* handshake included, immediately before it is sent. Pass `null` to remove.
|
|
648
669
|
*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
*
|
|
670
|
+
* It exists so the shell can stamp its nonce (`payload._shellNonce`) at a
|
|
671
|
+
* single chokepoint. Wrapping `send` alone is not enough — `handshake` calls
|
|
672
|
+
* the native layer directly, and a message without the nonce is dropped and
|
|
673
|
+
* logged as a security event.
|
|
674
|
+
*
|
|
675
|
+
* **Client code must not use this.**
|
|
653
676
|
*/
|
|
654
677
|
setOutgoingStamp(stamp: OutgoingStamp | null): void;
|
|
655
678
|
/**
|
|
656
|
-
*
|
|
657
|
-
*
|
|
679
|
+
* Fires `window.CondorBridge.emit(key, payload)` programmatically — useful to
|
|
680
|
+
* simulate native events in tests and in browser development.
|
|
658
681
|
*
|
|
659
|
-
* **Throws**
|
|
682
|
+
* **Throws** when the active adapter is not the mock.
|
|
660
683
|
*/
|
|
661
684
|
mockEmit(key: string, payload?: unknown): void;
|
|
662
685
|
/**
|
|
663
|
-
* Snapshot
|
|
664
|
-
* subscribers
|
|
686
|
+
* Snapshot of the mock adapter state: messages sent, and keys with active
|
|
687
|
+
* subscribers. Useful for assertions in client tests.
|
|
665
688
|
*
|
|
666
|
-
* **Throws**
|
|
689
|
+
* **Throws** when the active adapter is not the mock.
|
|
667
690
|
*/
|
|
668
691
|
getMockState(): MockState;
|
|
669
692
|
/**
|
|
670
|
-
*
|
|
671
|
-
* bridge
|
|
693
|
+
* Injects a floating HTML panel into `document.body` for inspecting the
|
|
694
|
+
* bridge in mock mode. Idempotent — re-installing replaces the previous one.
|
|
672
695
|
*
|
|
673
|
-
* **Throws**
|
|
696
|
+
* **Throws** when the active adapter is not the mock, or `document` does not
|
|
697
|
+
* exist.
|
|
674
698
|
*/
|
|
675
699
|
installDevPanel(options?: DevPanelOptions): void;
|
|
676
700
|
}
|
|
677
701
|
declare const Bridge: CondorBridge;
|
|
678
702
|
|
|
679
703
|
/**
|
|
680
|
-
* `BridgeClient`
|
|
681
|
-
*
|
|
704
|
+
* `BridgeClient` — **the API for client code**, running inside
|
|
705
|
+
* `<iframe sandbox="allow-scripts">` in the Condor Canvas app.
|
|
682
706
|
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
*
|
|
707
|
+
* This is what you want if you are building the bundle that renders as the
|
|
708
|
+
* app's Home. Every call becomes a `window.parent.postMessage(...)` that the
|
|
709
|
+
* Condor shell validates against a schema and a default-deny policy before it
|
|
710
|
+
* reaches the native layer. You never touch the native globals — the shell
|
|
711
|
+
* deletes them before your bundle runs.
|
|
687
712
|
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
713
|
+
* It deliberately does **not** expose `installDevPanel`, `mockEmit`, `isMock`
|
|
714
|
+
* or `getMockState`: those belong to the shell (`BridgeShell`).
|
|
690
715
|
*
|
|
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.
|
|
716
|
+
* The module also answers the shell's `verify-sandbox` probe — the iframe
|
|
717
|
+
* attesting whether it can still see the native globals. If any is reachable,
|
|
718
|
+
* the shell aborts the boot rather than render client code with a path to the
|
|
719
|
+
* native layer.
|
|
697
720
|
*/
|
|
698
721
|
|
|
699
722
|
interface BridgeClientHandshakeAck {
|
|
700
723
|
shellVersion: string;
|
|
701
724
|
/**
|
|
702
|
-
*
|
|
703
|
-
*
|
|
725
|
+
* Schema version of the **native** side, relayed by the shell. `null` when
|
|
726
|
+
* the native side has not answered, does not exist (browser mock mode), or
|
|
727
|
+
* the shell is older than 0.6.0.
|
|
704
728
|
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
707
|
-
*
|
|
708
|
-
*
|
|
729
|
+
* **This number is the only capability signal a client gets.** There is no
|
|
730
|
+
* version negotiation, and a sandboxed client reaches the native side by no
|
|
731
|
+
* other path. For example, the raw light channels (`light_sensor_*` in
|
|
732
|
+
* `deviceLightSensors`) exist from `0.16.0`. Always keep a working branch
|
|
733
|
+
* for `null`: your browser development environment always hits it.
|
|
709
734
|
*/
|
|
710
735
|
nativeVersion: string | null;
|
|
736
|
+
/**
|
|
737
|
+
* Whether the native side accepted your `clientVersion`. It is **always
|
|
738
|
+
* `true`** — there is no hard validation — so it carries no information.
|
|
739
|
+
* Never branch on it; branch on {@link nativeVersion}.
|
|
740
|
+
*/
|
|
711
741
|
accepted: boolean;
|
|
712
742
|
}
|
|
713
743
|
interface CondorBridgeClient {
|
|
714
|
-
/**
|
|
715
|
-
* Versão do protocolo do client.
|
|
716
|
-
*/
|
|
744
|
+
/** Version of the client-side postMessage protocol. */
|
|
717
745
|
readonly protocolVersion: string;
|
|
718
746
|
/**
|
|
719
|
-
*
|
|
720
|
-
*
|
|
721
|
-
*
|
|
747
|
+
* Subscribes to an observable key. Returns an `unsubscribe` function —
|
|
748
|
+
* call it when a view unmounts, or handlers accumulate and every one of them
|
|
749
|
+
* re-renders.
|
|
750
|
+
*
|
|
751
|
+
* Multiple subscribers per key are allowed, and a throw in one does not
|
|
752
|
+
* affect the others. Subscribe *before* sending the command that triggers
|
|
753
|
+
* the reply: the reply can arrive before your next statement runs.
|
|
722
754
|
*/
|
|
723
755
|
on<K extends BridgeKey>(key: K, cb: (payload: PayloadOf<K>) => void): () => void;
|
|
724
756
|
/**
|
|
725
|
-
*
|
|
726
|
-
*
|
|
727
|
-
*
|
|
757
|
+
* Sends a command to the shell, which validates it against the policy before
|
|
758
|
+
* it touches the native layer.
|
|
759
|
+
*
|
|
760
|
+
* **Fire-and-forget: no return value, no throw, no delivery confirmation.**
|
|
761
|
+
* A command that is not allowed produces a `nack` that surfaces only as a
|
|
762
|
+
* `console.warn`. A malformed one is dropped in silence by the native side.
|
|
763
|
+
* Replies, when a command has one, arrive on the corresponding key — see
|
|
764
|
+
* `docs/bridge-contract.md`.
|
|
728
765
|
*/
|
|
729
766
|
send(command: string, args?: Record<string, unknown>): void;
|
|
730
|
-
/**
|
|
731
|
-
* Abre Settings nativa. Equivalente a `send('openSettings')`.
|
|
732
|
-
*/
|
|
767
|
+
/** Opens the native Settings screen. Equivalent to `send('openSettings')`. */
|
|
733
768
|
openSettings(): void;
|
|
734
769
|
/**
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
770
|
+
* Requests ONE page of sensor logs and resolves with the reply correlated by
|
|
771
|
+
* `requestId` — **including when it carries `success: false`**, which is an
|
|
772
|
+
* answer, not a transport failure. The promise only **rejects** on timeout,
|
|
773
|
+
* so always check `success` before reading `logs`.
|
|
738
774
|
*
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
*
|
|
742
|
-
*
|
|
775
|
+
* Saves you from keeping a `requestId → resolve` map by hand. To sweep a
|
|
776
|
+
* period, chain calls using the returned `startingAfter` until `hasMore`
|
|
777
|
+
* becomes `false`: the cursors are dependent, so pages **cannot** be fetched
|
|
778
|
+
* in parallel.
|
|
743
779
|
*/
|
|
744
780
|
fetchSensorLog(query: SensorLogQueryPayload, opts?: {
|
|
745
781
|
timeoutMs?: number;
|
|
746
782
|
}): Promise<SensorLogPayload>;
|
|
747
783
|
/**
|
|
748
|
-
*
|
|
749
|
-
*
|
|
750
|
-
*
|
|
751
|
-
*
|
|
752
|
-
*
|
|
784
|
+
* Requests ONE permission through the **in-app native dialog** (it does not
|
|
785
|
+
* open Settings). Use it for the "Grant" button of your required-permissions
|
|
786
|
+
* card, with a name taken from `systemStatus.missingEssentialPermissions`,
|
|
787
|
+
* e.g. `'bluetoothScan'`.
|
|
788
|
+
*
|
|
789
|
+
* **There is no result key.** The outcome arrives as a fresh `systemStatus`
|
|
790
|
+
* emission; your card disappears when the array empties. A name outside the
|
|
791
|
+
* native enum is dropped in silence.
|
|
753
792
|
*/
|
|
754
793
|
requestPermission(permission: string): void;
|
|
755
794
|
/**
|
|
756
|
-
*
|
|
757
|
-
*
|
|
795
|
+
* Handshakes with the shell. Resolves with
|
|
796
|
+
* `{ shellVersion, nativeVersion, accepted }` — read `nativeVersion` to learn
|
|
797
|
+
* what this device can do, and ignore `accepted`, which is always `true`.
|
|
758
798
|
*
|
|
759
|
-
*
|
|
799
|
+
* Rejects after 5 s if the shell does not answer. Only one handshake may be
|
|
800
|
+
* in flight at a time.
|
|
760
801
|
*/
|
|
761
802
|
handshake(opts: {
|
|
762
803
|
clientVersion: string;
|
|
763
804
|
}): Promise<BridgeClientHandshakeAck>;
|
|
764
805
|
/**
|
|
765
|
-
*
|
|
766
|
-
* **
|
|
767
|
-
*
|
|
768
|
-
*
|
|
769
|
-
*
|
|
770
|
-
*
|
|
806
|
+
* Reports to the native side whether the client has internal history to go
|
|
807
|
+
* back to. **Call it on every internal navigation**, and at boot — not once.
|
|
808
|
+
* The native side caches the value and uses it to decide what the OS back
|
|
809
|
+
* button does:
|
|
810
|
+
* - `true` — back asks you to go back one page (the `navigationBack` event,
|
|
811
|
+
* see {@link onBack});
|
|
812
|
+
* - `false` — back stays with the app and does not leave the Home.
|
|
771
813
|
*
|
|
772
|
-
*
|
|
814
|
+
* Equivalent to `send('navigationState', { canGoBack })`.
|
|
773
815
|
*/
|
|
774
816
|
reportNavigation(canGoBack: boolean): void;
|
|
775
817
|
/**
|
|
776
|
-
*
|
|
777
|
-
*
|
|
778
|
-
*
|
|
779
|
-
*
|
|
818
|
+
* Subscribes to the OS back request (the `navigationBack` event). The
|
|
819
|
+
* callback must go back **one** internal page and then call
|
|
820
|
+
* {@link reportNavigation} with the new state. It only fires when you had
|
|
821
|
+
* reported `reportNavigation(true)`. Returns an `unsubscribe` function.
|
|
780
822
|
*/
|
|
781
823
|
onBack(cb: () => void): () => void;
|
|
782
824
|
}
|
|
783
825
|
declare const BridgeClient: CondorBridgeClient;
|
|
784
826
|
|
|
785
827
|
/**
|
|
786
|
-
*
|
|
828
|
+
* Valibot schemas for the shell↔client `postMessage` protocol.
|
|
787
829
|
*
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
830
|
+
* Client content runs inside `<iframe sandbox="allow-scripts">` with a `null`
|
|
831
|
+
* origin, so it talks to the Condor-controlled shell over `window.postMessage`.
|
|
832
|
+
* These schemas are the **only accepted contract** — any malformed message is
|
|
833
|
+
* discarded by the shell in silence, which is why hand-rolled `postMessage`
|
|
834
|
+
* calls appear to do nothing. Use `BridgeClient`.
|
|
792
835
|
*
|
|
793
|
-
* **Threat model:**
|
|
794
|
-
*
|
|
795
|
-
*
|
|
796
|
-
* `condor_wiki/cross-cutting/sleep-white-label-content-transport-and-security.md`.
|
|
836
|
+
* **Threat model:** the iframe may be hostile (a compromised supply chain, XSS
|
|
837
|
+
* in the client code). These schemas plus the `ShellPolicy` enforced by
|
|
838
|
+
* `BridgeShell` form the structural boundary.
|
|
797
839
|
*/
|
|
798
840
|
|
|
799
|
-
declare const
|
|
841
|
+
declare const ClientToShellMessageSchema: v.VariantSchema<"type", [v.ObjectSchema<{
|
|
800
842
|
readonly type: v.LiteralSchema<"subscribe", undefined>;
|
|
801
843
|
readonly key: v.PicklistSchema<[string, ...string[]], undefined>;
|
|
802
844
|
}, undefined>, v.ObjectSchema<{
|
|
@@ -826,8 +868,9 @@ declare const ClientToShellMessage: v.VariantSchema<"type", [v.ObjectSchema<{
|
|
|
826
868
|
readonly condorBridgeReachable: v.BooleanSchema<undefined>;
|
|
827
869
|
readonly nativeChannelReachable: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
828
870
|
}, undefined>], undefined>;
|
|
829
|
-
|
|
830
|
-
|
|
871
|
+
/** Union of every client → shell message. Type only; the runtime schema is `ClientToShellMessageSchema`, which is not part of the public API. */
|
|
872
|
+
type ClientToShellMessage = v.InferOutput<typeof ClientToShellMessageSchema>;
|
|
873
|
+
declare const ShellToClientMessageSchema: v.VariantSchema<"type", [v.ObjectSchema<{
|
|
831
874
|
readonly type: v.LiteralSchema<"event", undefined>;
|
|
832
875
|
readonly key: v.PicklistSchema<[string, ...string[]], undefined>;
|
|
833
876
|
readonly payload: v.UnknownSchema;
|
|
@@ -836,12 +879,12 @@ declare const ShellToClientMessage: v.VariantSchema<"type", [v.ObjectSchema<{
|
|
|
836
879
|
readonly ack: v.ObjectSchema<{
|
|
837
880
|
readonly shellVersion: v.StringSchema<undefined>;
|
|
838
881
|
/**
|
|
839
|
-
*
|
|
882
|
+
* Schema version declared by the **native** side, relayed by the shell.
|
|
840
883
|
*
|
|
841
|
-
* `null`
|
|
842
|
-
* mock
|
|
843
|
-
*
|
|
844
|
-
*
|
|
884
|
+
* `null` when the native side has not answered, or does not exist (browser
|
|
885
|
+
* mock mode). The client runs inside a sandboxed iframe and never talks to
|
|
886
|
+
* the native side directly, so this is the **only** way it can learn which
|
|
887
|
+
* capabilities are available.
|
|
845
888
|
*/
|
|
846
889
|
readonly nativeVersion: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
847
890
|
readonly accepted: v.BooleanSchema<undefined>;
|
|
@@ -854,22 +897,23 @@ declare const ShellToClientMessage: v.VariantSchema<"type", [v.ObjectSchema<{
|
|
|
854
897
|
}, undefined>, v.ObjectSchema<{
|
|
855
898
|
readonly type: v.LiteralSchema<"verify-sandbox", undefined>;
|
|
856
899
|
}, undefined>], undefined>;
|
|
857
|
-
|
|
900
|
+
/** Union of every shell → client message. Type only; the runtime schema is `ShellToClientMessageSchema`, which is not part of the public API. */
|
|
901
|
+
type ShellToClientMessage = v.InferOutput<typeof ShellToClientMessageSchema>;
|
|
858
902
|
/**
|
|
859
|
-
*
|
|
860
|
-
*
|
|
861
|
-
* bridge
|
|
903
|
+
* Default-deny policy enforced by `BridgeShell`. Every key and command coming
|
|
904
|
+
* from the iframe is checked against these lists before it touches the native
|
|
905
|
+
* bridge.
|
|
862
906
|
*
|
|
863
|
-
* - `readOnlyKeys
|
|
864
|
-
* - `allowedCommands
|
|
865
|
-
* - `requiresConfirmation
|
|
866
|
-
*
|
|
867
|
-
* - `deniedCommands
|
|
868
|
-
*
|
|
907
|
+
* - `readOnlyKeys` — keys the client may subscribe to.
|
|
908
|
+
* - `allowedCommands` — pass-through commands, no human confirmation.
|
|
909
|
+
* - `requiresConfirmation` — commands that fire the shell's
|
|
910
|
+
* `onConfirmationRequest` callback, so a person approves or declines.
|
|
911
|
+
* - `deniedCommands` — explicit block, defence in depth: destructive commands
|
|
912
|
+
* such as `dfu` that must never flow from the iframe.
|
|
869
913
|
*
|
|
870
|
-
* **
|
|
871
|
-
*
|
|
872
|
-
*
|
|
914
|
+
* **Not embedded in the package** — the `BridgeShell` host supplies the
|
|
915
|
+
* concrete policy. That keeps the package agnostic to the catalogue and lets
|
|
916
|
+
* the policy evolve without a release.
|
|
873
917
|
*/
|
|
874
918
|
interface ShellPolicy {
|
|
875
919
|
readOnlyKeys: string[];
|
|
@@ -878,68 +922,73 @@ interface ShellPolicy {
|
|
|
878
922
|
deniedCommands: string[];
|
|
879
923
|
}
|
|
880
924
|
/**
|
|
881
|
-
*
|
|
925
|
+
* Argument of the `BridgeShell` `onConfirmationRequest` callback.
|
|
882
926
|
*
|
|
883
|
-
* `approve: true`
|
|
884
|
-
* `approve: false`
|
|
927
|
+
* Resolving `approve: true` lets the command flow to the native side.
|
|
928
|
+
* Resolving `approve: false` makes the shell send the iframe a `nack` with
|
|
929
|
+
* `reason: 'user-rejected'` — which client code only ever sees as a console
|
|
930
|
+
* warning, so the ack it was waiting for simply never arrives.
|
|
885
931
|
*/
|
|
886
932
|
interface ConfirmationRequest {
|
|
887
933
|
command: string;
|
|
888
934
|
args: Record<string, unknown>;
|
|
889
935
|
}
|
|
890
936
|
/**
|
|
891
|
-
*
|
|
892
|
-
*
|
|
893
|
-
*
|
|
894
|
-
*
|
|
937
|
+
* Result of `BridgeShell.verifySandbox()` after a round trip with the iframe.
|
|
938
|
+
* If any `*Reachable` flag comes back `true`, the boundary is leaking: the
|
|
939
|
+
* override prefix injected by `bootstrapClientIframe` failed, and the app must
|
|
940
|
+
* not render client code with a path to the native layer.
|
|
895
941
|
*/
|
|
896
942
|
interface SandboxVerifyResult {
|
|
897
943
|
flutterBridgeReachable: boolean;
|
|
898
944
|
condorBridgeReachable: boolean;
|
|
899
|
-
/** `window.CondorNative`
|
|
945
|
+
/** Whether `window.CondorNative` is visible inside the iframe. */
|
|
900
946
|
nativeChannelReachable: boolean;
|
|
947
|
+
/** `true` only when none of the three is reachable. */
|
|
901
948
|
ok: boolean;
|
|
902
949
|
}
|
|
903
950
|
|
|
904
951
|
/**
|
|
905
|
-
* `BridgeShell`
|
|
906
|
-
*
|
|
952
|
+
* `BridgeShell` — the API for the **Condor-controlled shell** that wraps
|
|
953
|
+
* external client content inside `<iframe sandbox="allow-scripts">`.
|
|
907
954
|
*
|
|
908
|
-
*
|
|
909
|
-
*
|
|
910
|
-
*
|
|
911
|
-
*
|
|
912
|
-
*
|
|
913
|
-
*
|
|
914
|
-
*
|
|
915
|
-
*
|
|
916
|
-
*
|
|
917
|
-
*
|
|
918
|
-
*
|
|
919
|
-
*
|
|
920
|
-
*
|
|
921
|
-
*
|
|
922
|
-
*
|
|
923
|
-
* `
|
|
955
|
+
* This is not client-facing. If you are writing the bundle that renders as the
|
|
956
|
+
* app's Home, you want `BridgeClient`.
|
|
957
|
+
*
|
|
958
|
+
* Responsibilities:
|
|
959
|
+
* 1. **Layer 1 — `bootstrapClientIframe(iframe, clientPath)`:** fetches the
|
|
960
|
+
* client HTML, prepends an override script that removes the native globals
|
|
961
|
+
* from the iframe, and sets it as `srcdoc`. The override runs at parse time,
|
|
962
|
+
* before any other script.
|
|
963
|
+
* 2. **Layer 2 — `verifySandbox(iframe)`:** a `postMessage` round trip asking
|
|
964
|
+
* the iframe to attest that the native globals really are `undefined`.
|
|
965
|
+
* 3. **Routing between iframe and native:** listens for `message` events from
|
|
966
|
+
* the iframe, validates them with Valibot, applies the default-deny
|
|
967
|
+
* `ShellPolicy`, and forwards to `Bridge`. Native events go back to the
|
|
968
|
+
* iframe over `postMessage`.
|
|
969
|
+
* 4. **Human confirmation for sensitive commands:** commands listed in
|
|
970
|
+
* `policy.requiresConfirmation` fire the `onConfirmationRequest` callback
|
|
971
|
+
* before touching the native side, so the shell can present UI.
|
|
924
972
|
*/
|
|
925
973
|
|
|
926
974
|
interface BridgeShellOptions {
|
|
927
975
|
/**
|
|
928
|
-
*
|
|
929
|
-
*
|
|
976
|
+
* The iframe hosting the client. It may be empty at construction time —
|
|
977
|
+
* `bootstrapClientIframe` is called later to populate it.
|
|
930
978
|
*/
|
|
931
979
|
iframe: HTMLIFrameElement;
|
|
932
980
|
/**
|
|
933
|
-
*
|
|
934
|
-
*
|
|
981
|
+
* The default-deny policy. Every key and command arriving from the iframe is
|
|
982
|
+
* checked against these lists. See `ShellPolicy`.
|
|
935
983
|
*/
|
|
936
984
|
policy: ShellPolicy;
|
|
937
985
|
/**
|
|
938
|
-
*
|
|
939
|
-
*
|
|
940
|
-
*
|
|
941
|
-
*
|
|
942
|
-
*
|
|
986
|
+
* Synchronous or asynchronous callback invoked when the client issues a
|
|
987
|
+
* command listed in `policy.requiresConfirmation`. The shell presents its
|
|
988
|
+
* confirmation UI and resolves `true` to approve or `false` to decline.
|
|
989
|
+
*
|
|
990
|
+
* If it is not provided, such commands are **denied** automatically —
|
|
991
|
+
* fail-safe.
|
|
943
992
|
*/
|
|
944
993
|
onConfirmationRequest?: (req: ConfirmationRequest) => boolean | Promise<boolean>;
|
|
945
994
|
}
|
|
@@ -950,61 +999,60 @@ declare class BridgeShell {
|
|
|
950
999
|
private readonly subscriptions;
|
|
951
1000
|
private detached;
|
|
952
1001
|
/**
|
|
953
|
-
*
|
|
954
|
-
*
|
|
1002
|
+
* The native version, fetched once and cached. `null` until the native side
|
|
1003
|
+
* answers, and permanently `null` in a browser with no native host.
|
|
955
1004
|
*/
|
|
956
1005
|
private nativeVersion;
|
|
957
|
-
/**
|
|
1006
|
+
/** In-flight native handshake; guarantees a single call. */
|
|
958
1007
|
private nativeHandshake;
|
|
959
1008
|
private pendingVerifies;
|
|
960
1009
|
constructor(opts: BridgeShellOptions);
|
|
961
1010
|
/**
|
|
962
|
-
* **
|
|
1011
|
+
* **Layer 1.** Bootstraps by fetching a URL.
|
|
963
1012
|
*
|
|
964
|
-
*
|
|
965
|
-
*
|
|
1013
|
+
* A shortcut for the common HTTP/HTTPS case: it runs `fetch(clientPath)` and
|
|
1014
|
+
* delegates to `bootstrapClientIframeWithHtml`.
|
|
966
1015
|
*
|
|
967
|
-
* **
|
|
968
|
-
*
|
|
969
|
-
*
|
|
970
|
-
* `bootstrapClientIframeWithHtml(htmlString)
|
|
1016
|
+
* **It does not work on the `file://` scheme** — Android's Chromium WebView
|
|
1017
|
+
* blocks it with "URL scheme 'file' is not supported". For an asset bundled
|
|
1018
|
+
* in the APK, or a bundle inlined at build time, use
|
|
1019
|
+
* `bootstrapClientIframeWithHtml(htmlString)` instead.
|
|
971
1020
|
*
|
|
972
|
-
* **
|
|
973
|
-
*
|
|
974
|
-
* para atestation (Camada 2).
|
|
1021
|
+
* **Call it once at shell boot**, before any other interaction. After it
|
|
1022
|
+
* returns, invoke `verifySandbox()` for attestation (Layer 2).
|
|
975
1023
|
*/
|
|
976
1024
|
bootstrapClientIframe(clientPath?: string): Promise<void>;
|
|
977
1025
|
/**
|
|
978
|
-
* **
|
|
1026
|
+
* **Layer 1.** Bootstraps from an HTML string.
|
|
979
1027
|
*
|
|
980
|
-
*
|
|
981
|
-
* build
|
|
982
|
-
*
|
|
983
|
-
*
|
|
1028
|
+
* Takes the client HTML directly — already read from a file, embedded at
|
|
1029
|
+
* build time, read from storage — and sets it as the iframe's `srcdoc`,
|
|
1030
|
+
* prefixed by the override script, which runs at parse time before any
|
|
1031
|
+
* client script.
|
|
984
1032
|
*
|
|
985
|
-
* Use
|
|
986
|
-
* bundle inlined
|
|
987
|
-
*
|
|
1033
|
+
* Use this when `fetch` does not apply: an asset bundled under `file://`, a
|
|
1034
|
+
* bundle inlined at build time, any scenario without network. Android
|
|
1035
|
+
* WebView blocks `fetch` on the `file://` scheme, so this is the path the
|
|
1036
|
+
* app actually takes. For HTTP, use `bootstrapClientIframe(clientPath)`.
|
|
988
1037
|
*
|
|
989
|
-
* **
|
|
990
|
-
*
|
|
991
|
-
* para atestation (Camada 2).
|
|
1038
|
+
* **Call it once at shell boot**, before any other interaction. After it
|
|
1039
|
+
* returns, invoke `verifySandbox()` for attestation (Layer 2).
|
|
992
1040
|
*/
|
|
993
1041
|
bootstrapClientIframeWithHtml(clientHtml: string): Promise<void>;
|
|
994
1042
|
private waitForIframeLoad;
|
|
995
1043
|
/**
|
|
996
|
-
* **
|
|
997
|
-
*
|
|
998
|
-
*
|
|
1044
|
+
* **Layer 2.** A `postMessage` round trip asking the iframe for an
|
|
1045
|
+
* authoritative attestation of whether the native globals are reachable.
|
|
1046
|
+
* Times out after 2 s.
|
|
999
1047
|
*
|
|
1000
|
-
* `ok`
|
|
1001
|
-
* `false`, **shell
|
|
1002
|
-
*
|
|
1048
|
+
* `ok` is `true` only when none of them is reachable. If it comes back
|
|
1049
|
+
* `false`, **the shell must abort the app boot**: Layer 1 failed, and client
|
|
1050
|
+
* code would run with a path to the native layer.
|
|
1003
1051
|
*/
|
|
1004
1052
|
verifySandbox(): Promise<SandboxVerifyResult>;
|
|
1005
1053
|
/**
|
|
1006
|
-
*
|
|
1007
|
-
*
|
|
1054
|
+
* Tears the shell down: removes the `message` listener, cancels pending
|
|
1055
|
+
* subscriptions on the native `Bridge`, and clears internal state.
|
|
1008
1056
|
*/
|
|
1009
1057
|
detach(): void;
|
|
1010
1058
|
private onMessage;
|
|
@@ -1012,16 +1060,16 @@ declare class BridgeShell {
|
|
|
1012
1060
|
private handleUnsubscribe;
|
|
1013
1061
|
private handleSend;
|
|
1014
1062
|
/**
|
|
1015
|
-
*
|
|
1016
|
-
*
|
|
1063
|
+
* Resolves `nativeVersion`, firing the native handshake the first time and
|
|
1064
|
+
* caching the result.
|
|
1017
1065
|
*
|
|
1018
|
-
* ⚠️ `Bridge.handshake` **
|
|
1019
|
-
*
|
|
1020
|
-
*
|
|
1021
|
-
*
|
|
1066
|
+
* ⚠️ `Bridge.handshake` has **no timeout**: its promise only settles when the
|
|
1067
|
+
* native side emits `handshake-ack`. In a browser with no native host it
|
|
1068
|
+
* never settles, so the race below is what guarantees the client always gets
|
|
1069
|
+
* an answer — `null` — instead of hanging.
|
|
1022
1070
|
*/
|
|
1023
1071
|
private resolveNativeVersion;
|
|
1024
1072
|
private postToClient;
|
|
1025
1073
|
}
|
|
1026
1074
|
|
|
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 };
|
|
1075
|
+
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 };
|