@condorinst/mobile-bridge 0.8.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/CHANGELOG.md +350 -0
- package/LICENSE +14 -0
- package/README.md +396 -0
- package/dist/cli.cjs +194 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.js +193 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +1045 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +624 -0
- package/dist/index.d.ts +624 -0
- package/dist/index.js +1005 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
import * as v from 'valibot';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mock adapter — ativo quando `window.flutter_inappwebview` não é
|
|
5
|
+
* detectado (browser dev, jsdom sem host instalado, Storybook etc.).
|
|
6
|
+
*
|
|
7
|
+
* Comportamento:
|
|
8
|
+
* - `callHandler` grava `{handlerName, payload, timestamp}` em buffer
|
|
9
|
+
* interno (cap 100, FIFO), loga via `console.debug` e dispatcha
|
|
10
|
+
* listeners registrados via `onCallHandler`.
|
|
11
|
+
* - `triggerEmit` dispatcha via `window.CondorBridge.emit` — bridge real
|
|
12
|
+
* trata como se viesse do nativo (handshake-ack resolve handshake;
|
|
13
|
+
* demais chaves vão pros subscribers).
|
|
14
|
+
* - `getMessages` retorna snapshot read-only do buffer.
|
|
15
|
+
* - `clearMessages` esvazia o buffer (útil em testes).
|
|
16
|
+
* - `onCallHandler(listener)` — registra listener invocado a cada send.
|
|
17
|
+
* Retorna função de unsubscribe. Útil para dev panel reagir a sends
|
|
18
|
+
* (v0.3.0+ auto-respond toggle) ou análise em testes.
|
|
19
|
+
*/
|
|
20
|
+
interface MockMessage {
|
|
21
|
+
handlerName: string;
|
|
22
|
+
payload: unknown;
|
|
23
|
+
timestamp: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Dev panel — UI flutuante para inspecionar o bridge em mock mode.
|
|
28
|
+
*
|
|
29
|
+
* **Opt-in:** chamar `Bridge.installDevPanel(opts?)` quando quiser ver
|
|
30
|
+
* o painel. Só funciona em mock mode (throws em modo real). Injeta no
|
|
31
|
+
* `document.body`; idempotente (re-install remove o anterior).
|
|
32
|
+
*
|
|
33
|
+
* Conteúdo:
|
|
34
|
+
* - Header com badge "MOCK", contador de sends, botão minimizar/maximizar.
|
|
35
|
+
* - Sent log — últimas N mensagens (timestamp, type, payload truncado).
|
|
36
|
+
* - Trigger emit — dropdown com `BRIDGE_KEYS` + textarea payload + botão.
|
|
37
|
+
*
|
|
38
|
+
* Estilos inline + classes prefixadas `condor-mock-panel-*` para reduzir
|
|
39
|
+
* conflito com CSS do cliente.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resposta auto-emitida em reação a um `Bridge.send`, retornada pela
|
|
44
|
+
* policy `onSendForAutoRespond` do cliente. `null` significa "não
|
|
45
|
+
* responder a este send".
|
|
46
|
+
*
|
|
47
|
+
* @since v0.3.0
|
|
48
|
+
*/
|
|
49
|
+
interface AutoRespondResponse {
|
|
50
|
+
key: string;
|
|
51
|
+
payload: unknown;
|
|
52
|
+
}
|
|
53
|
+
interface DevPanelOptions {
|
|
54
|
+
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
|
|
55
|
+
maxMessages?: number;
|
|
56
|
+
theme?: 'dark' | 'light';
|
|
57
|
+
startCollapsed?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Policy de auto-respond: invocada a cada `Bridge.send` quando o
|
|
60
|
+
* toggle "Auto-respond" está ligado no painel. Retorna
|
|
61
|
+
* `{key, payload}` para disparar `Bridge.mockEmit(key, payload)`
|
|
62
|
+
* após delay, ou `null` para não responder.
|
|
63
|
+
*
|
|
64
|
+
* Sem esta callback, o checkbox "Auto-respond" não aparece no painel.
|
|
65
|
+
*
|
|
66
|
+
* @since v0.3.0
|
|
67
|
+
*/
|
|
68
|
+
onSendForAutoRespond?: (msg: MockMessage) => AutoRespondResponse | null;
|
|
69
|
+
/**
|
|
70
|
+
* Estado inicial do toggle "Auto-respond". Default: `false`
|
|
71
|
+
* (manual permanece padrão; cliente decide quando ligar).
|
|
72
|
+
*
|
|
73
|
+
* @since v0.3.0
|
|
74
|
+
*/
|
|
75
|
+
startAutoRespondEnabled?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Delay em ms entre o `send` e o `mockEmit` da auto-resposta.
|
|
78
|
+
* Default: `50` — aproxima latência de IPC real de native bridge.
|
|
79
|
+
*
|
|
80
|
+
* @since v0.3.0
|
|
81
|
+
*/
|
|
82
|
+
autoRespondDelayMs?: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Catálogo das chaves do bridge JS↔Native.
|
|
87
|
+
*
|
|
88
|
+
* Espelha o lado JS-facing de `ForegroundComCoreKeys` do `condor_core`
|
|
89
|
+
* (monorepo `mobile_applications`). As strings aqui são o que o JS recebe
|
|
90
|
+
* via `window.CondorBridge.emit(key, payload)`.
|
|
91
|
+
*
|
|
92
|
+
* Camada 1 (v0.1.0): catálogo declarativo. Apenas `pingPong` está
|
|
93
|
+
* efetivamente emitida pelo lado Dart no Trio B; as outras 8 são
|
|
94
|
+
* reservadas para o Trio C (`sleep-whitelabel-bridge-keys`) e ficam
|
|
95
|
+
* aqui como contrato forward-looking.
|
|
96
|
+
*/
|
|
97
|
+
declare const BRIDGE_KEYS: {
|
|
98
|
+
readonly pingPong: "pingPong";
|
|
99
|
+
readonly userDevice: "userDevice";
|
|
100
|
+
readonly deviceLightSensors: "deviceLightSensors";
|
|
101
|
+
readonly syncTime: "syncTime";
|
|
102
|
+
readonly realTime: "realTime";
|
|
103
|
+
readonly deleteMemory: "deleteMemory";
|
|
104
|
+
readonly deviceNotification: "deviceNotification";
|
|
105
|
+
readonly deviceConnection: "deviceConnection";
|
|
106
|
+
readonly sensorLog: "sensorLog";
|
|
107
|
+
readonly dfu: "dfu";
|
|
108
|
+
};
|
|
109
|
+
type BridgeKey = (typeof BRIDGE_KEYS)[keyof typeof BRIDGE_KEYS];
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Mensagens do protocolo do bridge.
|
|
113
|
+
*
|
|
114
|
+
* Espelha o lado Dart (`BridgeIncomingMessage`/`BridgeOutgoingEvent` em
|
|
115
|
+
* `apps/sleep_whitelabel/lib/ui/pages/home/bridge/bridge_message.dart`).
|
|
116
|
+
*/
|
|
117
|
+
/**
|
|
118
|
+
* Mensagem JS → Native, enviada via
|
|
119
|
+
* `window.flutter_inappwebview.callHandler('CondorBridge', msg)`.
|
|
120
|
+
*/
|
|
121
|
+
interface BridgeIncomingMessage {
|
|
122
|
+
type: string;
|
|
123
|
+
version: string;
|
|
124
|
+
payload?: Record<string, unknown>;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Evento Native → JS, recebido via
|
|
128
|
+
* `window.CondorBridge.emit(key, payload)`.
|
|
129
|
+
*/
|
|
130
|
+
interface BridgeOutgoingEvent {
|
|
131
|
+
key: string;
|
|
132
|
+
payload: unknown;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Resposta do handshake (`handshake-ack`).
|
|
136
|
+
*
|
|
137
|
+
* `nativeVersion`: versão semver do schema declarada pelo nativo.
|
|
138
|
+
* `accepted`: nativo aceita o `clientVersion` enviado. v0.1.0 não impõe
|
|
139
|
+
* validação semver dura — nativo retorna `accepted: true` sempre.
|
|
140
|
+
*/
|
|
141
|
+
interface HandshakeAck {
|
|
142
|
+
nativeVersion: string;
|
|
143
|
+
accepted: boolean;
|
|
144
|
+
}
|
|
145
|
+
interface HandshakeOptions {
|
|
146
|
+
clientVersion: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Catálogo de payloads tipados por chave (v0.4.0+).
|
|
151
|
+
*
|
|
152
|
+
* Shapes derivadas autoritativamente do source code do `condor_core` no
|
|
153
|
+
* monorepo `mobile_applications` (ver §"Shapes derivadas" no execution
|
|
154
|
+
* log do step2 do trio `2026-06-04_sleep-whitelabel-bridge-keys`).
|
|
155
|
+
*
|
|
156
|
+
* **Direção:** BG → JS (observáveis). Os comandos JS → BG (`Bridge.send`)
|
|
157
|
+
* continuam permissivos (`Record<string, unknown>`) na v0.4.0 — sua
|
|
158
|
+
* tipagem estrita é trabalho separado e fora do MVP atual.
|
|
159
|
+
*
|
|
160
|
+
* **Migração da v0.3.0:** o tipo `PayloadOf<K>` era `Record<string, unknown>`
|
|
161
|
+
* para qualquer K. Agora é mapeado pelas chaves do catálogo, com
|
|
162
|
+
* fallback permissivo para chaves não-catalogadas (e.g., `dfu` continua
|
|
163
|
+
* `Record<string, unknown>` — hub-only, reservado).
|
|
164
|
+
*/
|
|
165
|
+
/** Loop heartbeat boot/keepalive. */
|
|
166
|
+
interface PingPongPayload {
|
|
167
|
+
data: 'ping' | 'pong';
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Snapshot do device store do usuário. Espelha `UserDevices.toMap()`
|
|
171
|
+
* (`packages/condor_core/lib/src/domain/entities/user_devices_entity.dart:144-151`).
|
|
172
|
+
*
|
|
173
|
+
* Os arrays internos são `Record<string, unknown>` no MVP — cada item
|
|
174
|
+
* tem seu próprio `toMap()` (DeviceConfig, DeviceTelemetry,
|
|
175
|
+
* DeviceUserConfig) cujo strict typing fica para release futura quando
|
|
176
|
+
* consumidor JS precisar.
|
|
177
|
+
*/
|
|
178
|
+
interface UserDevicePayload {
|
|
179
|
+
lastUpdate: number;
|
|
180
|
+
devicesConfig: Array<Record<string, unknown>>;
|
|
181
|
+
devicesTelemetry: Array<Record<string, unknown>>;
|
|
182
|
+
devicesUserConfig: Array<Record<string, unknown>>;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Stream real-time de sensores de luz (FEST). Shape:
|
|
186
|
+
* `{serialNo, type, data: <lightSensors.toMap()>}`. O subobjeto `data`
|
|
187
|
+
* varia por device type — permissivo no MVP.
|
|
188
|
+
*/
|
|
189
|
+
interface DeviceLightSensorsPayload {
|
|
190
|
+
serialNo: number;
|
|
191
|
+
type: string;
|
|
192
|
+
data: Record<string, unknown>;
|
|
193
|
+
}
|
|
194
|
+
/** Ack de comando `syncTime`. */
|
|
195
|
+
interface SyncTimeAckPayload {
|
|
196
|
+
serialNo: number;
|
|
197
|
+
success: boolean;
|
|
198
|
+
}
|
|
199
|
+
/** Ack/state do toggle `realTime`. */
|
|
200
|
+
interface RealTimeAckPayload {
|
|
201
|
+
serialNo: number;
|
|
202
|
+
success: boolean;
|
|
203
|
+
value: boolean;
|
|
204
|
+
}
|
|
205
|
+
/** Ack de comando `deleteMemory`. */
|
|
206
|
+
interface DeleteMemoryAckPayload {
|
|
207
|
+
serialNo: number;
|
|
208
|
+
success: boolean;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Status de conexão BLE. Espelha `_userDevicesService.openConnection`
|
|
212
|
+
* subscriber (`devices_notification_service.dart:38-44`).
|
|
213
|
+
*/
|
|
214
|
+
interface DeviceConnectionPayload {
|
|
215
|
+
serialNo: number;
|
|
216
|
+
isConnect: boolean;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Evento/status de device (logs). Espelha `DevicesNotification.toMap()`
|
|
220
|
+
* (`devices_notification_entity.dart:33-41`).
|
|
221
|
+
*/
|
|
222
|
+
interface DeviceNotificationPayload {
|
|
223
|
+
serialNo: number;
|
|
224
|
+
isConnect: boolean;
|
|
225
|
+
message: string | null;
|
|
226
|
+
messageCode: string | null;
|
|
227
|
+
isError: boolean;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Resposta do comando `sensorLog` (GET `user_cmd=log` do Mobile Lambda).
|
|
231
|
+
* Envelope com `requestId` ecoado para correlação JS (suporta paginação
|
|
232
|
+
* keyset concorrente) + `success`/`error`. No sucesso traz a página de logs
|
|
233
|
+
* (ordem cronológica ASC, temperatura em °C, timestamps UTC naive) e os
|
|
234
|
+
* cursores opacos. No erro, `error` carrega o código do contrato (ex.:
|
|
235
|
+
* `CONFLICTING_CURSORS`, `invalid session`).
|
|
236
|
+
*/
|
|
237
|
+
interface SensorLogPayload {
|
|
238
|
+
requestId: string;
|
|
239
|
+
success: boolean;
|
|
240
|
+
logs?: Array<Record<string, unknown>>;
|
|
241
|
+
startingAfter?: string | null;
|
|
242
|
+
endingBefore?: string | null;
|
|
243
|
+
hasMore?: boolean;
|
|
244
|
+
hasPrevious?: boolean;
|
|
245
|
+
error?: string;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Mapa discriminado de payloads por chave do catálogo. Usado por
|
|
249
|
+
* `PayloadOf<K>` para inferência automática no `Bridge.on`.
|
|
250
|
+
*
|
|
251
|
+
* `dfu` é hub-only — sleep_whitelabel não trata; mantido permissivo.
|
|
252
|
+
*/
|
|
253
|
+
interface KeyPayloads {
|
|
254
|
+
pingPong: PingPongPayload;
|
|
255
|
+
userDevice: UserDevicePayload;
|
|
256
|
+
deviceLightSensors: DeviceLightSensorsPayload;
|
|
257
|
+
syncTime: SyncTimeAckPayload;
|
|
258
|
+
realTime: RealTimeAckPayload;
|
|
259
|
+
deleteMemory: DeleteMemoryAckPayload;
|
|
260
|
+
deviceNotification: DeviceNotificationPayload;
|
|
261
|
+
deviceConnection: DeviceConnectionPayload;
|
|
262
|
+
sensorLog: SensorLogPayload;
|
|
263
|
+
dfu: Record<string, unknown>;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Payload tipado por chave. Distributivo:
|
|
267
|
+
* - Chaves catalogadas em `KeyPayloads` → payload estrito.
|
|
268
|
+
* - Demais chaves (futuras / extras do cliente) → `Record<string, unknown>`
|
|
269
|
+
* permissivo para preservar extensibilidade.
|
|
270
|
+
*
|
|
271
|
+
* **v0.3.0 → v0.4.0:** quebra build-time (não runtime) para clientes
|
|
272
|
+
* que usavam payload `unknown` em `Bridge.on` — agora vem tipado.
|
|
273
|
+
* Cliente que precisa do shape permissivo pode usar
|
|
274
|
+
* `(payload: Record<string, unknown>) => ...` explicitamente.
|
|
275
|
+
*/
|
|
276
|
+
type PayloadOf<K extends BridgeKey = BridgeKey> = K extends keyof KeyPayloads ? KeyPayloads[K] : Record<string, unknown>;
|
|
277
|
+
|
|
278
|
+
interface WindowCondorBridge {
|
|
279
|
+
emit(key: string, payload: unknown): void;
|
|
280
|
+
}
|
|
281
|
+
declare global {
|
|
282
|
+
interface Window {
|
|
283
|
+
CondorBridge?: WindowCondorBridge;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
interface MockState {
|
|
287
|
+
readonly isMock: true;
|
|
288
|
+
readonly messages: readonly MockMessage[];
|
|
289
|
+
readonly subscriberKeys: readonly string[];
|
|
290
|
+
}
|
|
291
|
+
interface CondorBridge {
|
|
292
|
+
/**
|
|
293
|
+
* `true` quando o adapter ativo é mock (browser dev, sem
|
|
294
|
+
* `window.flutter_inappwebview`). `false` quando rodando dentro do
|
|
295
|
+
* WebView Flutter.
|
|
296
|
+
*/
|
|
297
|
+
readonly isMock: boolean;
|
|
298
|
+
/**
|
|
299
|
+
* Envia handshake versionado ao nativo e aguarda `handshake-ack`.
|
|
300
|
+
*
|
|
301
|
+
* Apenas um handshake pode estar pendente por vez. Chamadas paralelas
|
|
302
|
+
* antes do primeiro `ack` chegar são rejeitadas com erro claro.
|
|
303
|
+
*
|
|
304
|
+
* **Em mock mode:** NÃO resolve sozinho. Cliente/teste deve chamar
|
|
305
|
+
* `Bridge.mockEmit('handshake-ack', {nativeVersion, accepted})` para
|
|
306
|
+
* destravar a Promise. Esta decisão (`Explícito`) força testes a serem
|
|
307
|
+
* intencionais sobre o estado nativo.
|
|
308
|
+
*/
|
|
309
|
+
handshake(opts: HandshakeOptions): Promise<HandshakeAck>;
|
|
310
|
+
/**
|
|
311
|
+
* Subscreve a uma chave observável. Retorna função `unsubscribe`.
|
|
312
|
+
*
|
|
313
|
+
* Múltiplos subscribers por chave são permitidos. Erro em um subscriber
|
|
314
|
+
* não afeta os outros (capturado e logado em `console.error`).
|
|
315
|
+
*/
|
|
316
|
+
on<K extends BridgeKey>(key: K, cb: (payload: PayloadOf<K>) => void): () => void;
|
|
317
|
+
/**
|
|
318
|
+
* Envia mensagem genérica ao nativo. Use para comandos UI→BG que não
|
|
319
|
+
* tenham helper dedicado. `type` é a string que o native usa para
|
|
320
|
+
* roteamento no `_onJsMessage`.
|
|
321
|
+
*
|
|
322
|
+
* Em mock mode: grava no buffer interno + `console.debug`.
|
|
323
|
+
*/
|
|
324
|
+
send(type: string, payload?: Record<string, unknown>): void;
|
|
325
|
+
/**
|
|
326
|
+
* Abre a tela nativa de Settings do app. Equivalente a
|
|
327
|
+
* `Bridge.send('openSettings')`.
|
|
328
|
+
*/
|
|
329
|
+
openSettings(): void;
|
|
330
|
+
/**
|
|
331
|
+
* Dispara `window.CondorBridge.emit(key, payload)` programaticamente —
|
|
332
|
+
* útil para simular eventos nativos em testes e dev em browser.
|
|
333
|
+
*
|
|
334
|
+
* **Throws** se o adapter ativo não for mock.
|
|
335
|
+
*/
|
|
336
|
+
mockEmit(key: string, payload?: unknown): void;
|
|
337
|
+
/**
|
|
338
|
+
* Snapshot do estado do mock adapter (mensagens enviadas + chaves com
|
|
339
|
+
* subscribers ativos). Useful para asserts em testes de cliente.
|
|
340
|
+
*
|
|
341
|
+
* **Throws** se o adapter ativo não for mock.
|
|
342
|
+
*/
|
|
343
|
+
getMockState(): MockState;
|
|
344
|
+
/**
|
|
345
|
+
* Injeta painel HTML flutuante no `document.body` para inspecionar o
|
|
346
|
+
* bridge em mock mode. Idempotente — re-install substitui o anterior.
|
|
347
|
+
*
|
|
348
|
+
* **Throws** se o adapter ativo não for mock OU se `document` não existir.
|
|
349
|
+
*/
|
|
350
|
+
installDevPanel(options?: DevPanelOptions): void;
|
|
351
|
+
}
|
|
352
|
+
declare const Bridge: CondorBridge;
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* `BridgeClient` (v0.5.0) — API para código do cliente externo rodando
|
|
356
|
+
* dentro de `<iframe sandbox="allow-scripts">` no `sleep_whitelabel`.
|
|
357
|
+
*
|
|
358
|
+
* Surface idêntica ao `Bridge` legado (`on`/`send`/`openSettings`/`handshake`)
|
|
359
|
+
* — cliente importa e não percebe que está em sandbox. Internamente,
|
|
360
|
+
* todas as chamadas viram `window.parent.postMessage(...)` com schema
|
|
361
|
+
* validado pelo shell antes de chegar ao nativo.
|
|
362
|
+
*
|
|
363
|
+
* **Não expõe** `installDevPanel`, `mockEmit`, `isMock`, `getMockState` —
|
|
364
|
+
* essas APIs são responsabilidade do shell (`BridgeShell`).
|
|
365
|
+
*
|
|
366
|
+
* **Threat model & Camada 2 do Risco 1** (vide
|
|
367
|
+
* `sleep-white-label-content-transport-and-security.md`): este módulo
|
|
368
|
+
* também responde ao `verify-sandbox` do shell — função de attestation
|
|
369
|
+
* do próprio iframe sobre se ainda enxerga `window.flutter_inappwebview`
|
|
370
|
+
* ou `window.CondorBridge` (que deveriam ter sido deletados pela Camada 1).
|
|
371
|
+
* Se vier `true` em algum, o shell aborta o boot do app.
|
|
372
|
+
*/
|
|
373
|
+
|
|
374
|
+
interface BridgeClientHandshakeAck {
|
|
375
|
+
shellVersion: string;
|
|
376
|
+
accepted: boolean;
|
|
377
|
+
}
|
|
378
|
+
interface CondorBridgeClient {
|
|
379
|
+
/**
|
|
380
|
+
* Versão do protocolo do client.
|
|
381
|
+
*/
|
|
382
|
+
readonly protocolVersion: string;
|
|
383
|
+
/**
|
|
384
|
+
* Subscreve a uma chave observável. Retorna função `unsubscribe`.
|
|
385
|
+
* Múltiplos subscribers por chave permitidos. Erro em um subscriber
|
|
386
|
+
* não afeta os outros.
|
|
387
|
+
*/
|
|
388
|
+
on<K extends BridgeKey>(key: K, cb: (payload: PayloadOf<K>) => void): () => void;
|
|
389
|
+
/**
|
|
390
|
+
* Envia comando para o shell. Shell valida contra `ShellPolicy` antes
|
|
391
|
+
* de tocar o nativo. Comandos não permitidos → `nack` (logado em
|
|
392
|
+
* `console.warn` na v0.5.0).
|
|
393
|
+
*/
|
|
394
|
+
send(command: string, args?: Record<string, unknown>): void;
|
|
395
|
+
/**
|
|
396
|
+
* Abre Settings nativa. Equivalente a `send('openSettings')`.
|
|
397
|
+
*/
|
|
398
|
+
openSettings(): void;
|
|
399
|
+
/**
|
|
400
|
+
* Handshake com o shell. Resolve com `{ shellVersion, accepted }`.
|
|
401
|
+
* Timeout em 5s se shell não responder.
|
|
402
|
+
*
|
|
403
|
+
* Apenas um handshake pode estar pendente por vez.
|
|
404
|
+
*/
|
|
405
|
+
handshake(opts: {
|
|
406
|
+
clientVersion: string;
|
|
407
|
+
}): Promise<BridgeClientHandshakeAck>;
|
|
408
|
+
}
|
|
409
|
+
declare const BridgeClient: CondorBridgeClient;
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Schemas Valibot dos `postMessage` shell↔client (v0.5.0).
|
|
413
|
+
*
|
|
414
|
+
* Quando o conteúdo do cliente roda dentro de `<iframe sandbox="allow-scripts">`
|
|
415
|
+
* (origem null), a comunicação com o shell Condor-controlado acontece via
|
|
416
|
+
* `window.postMessage`. Estes schemas são o **único contrato aceito** —
|
|
417
|
+
* qualquer mensagem mal-formada é descartada silenciosamente pelo shell.
|
|
418
|
+
*
|
|
419
|
+
* **Threat model:** o iframe pode ser hostil (supply-chain comprometida,
|
|
420
|
+
* XSS no código do cliente). Estes schemas + a `ShellPolicy` aplicada
|
|
421
|
+
* no `BridgeShell` formam o boundary estrutural — vide
|
|
422
|
+
* `condor_wiki/cross-cutting/sleep-white-label-content-transport-and-security.md`.
|
|
423
|
+
*/
|
|
424
|
+
|
|
425
|
+
declare const ClientToShellMessage: v.VariantSchema<"type", [v.ObjectSchema<{
|
|
426
|
+
readonly type: v.LiteralSchema<"subscribe", undefined>;
|
|
427
|
+
readonly key: v.PicklistSchema<[string, ...string[]], undefined>;
|
|
428
|
+
}, undefined>, v.ObjectSchema<{
|
|
429
|
+
readonly type: v.LiteralSchema<"unsubscribe", undefined>;
|
|
430
|
+
readonly key: v.PicklistSchema<[string, ...string[]], undefined>;
|
|
431
|
+
}, undefined>, v.ObjectSchema<{
|
|
432
|
+
readonly type: v.LiteralSchema<"send", undefined>;
|
|
433
|
+
readonly command: v.StringSchema<undefined>;
|
|
434
|
+
readonly args: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.UnknownSchema, undefined>, {}>;
|
|
435
|
+
}, undefined>, v.ObjectSchema<{
|
|
436
|
+
readonly type: v.LiteralSchema<"handshake", undefined>;
|
|
437
|
+
readonly payload: v.ObjectSchema<{
|
|
438
|
+
readonly clientVersion: v.StringSchema<undefined>;
|
|
439
|
+
}, undefined>;
|
|
440
|
+
}, undefined>, v.ObjectSchema<{
|
|
441
|
+
readonly type: v.LiteralSchema<"jsError", undefined>;
|
|
442
|
+
readonly payload: v.ObjectSchema<{
|
|
443
|
+
readonly message: v.StringSchema<undefined>;
|
|
444
|
+
readonly stack: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
445
|
+
readonly source: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
446
|
+
readonly lineno: v.OptionalSchema<v.NumberSchema<undefined>, undefined>;
|
|
447
|
+
readonly colno: v.OptionalSchema<v.NumberSchema<undefined>, undefined>;
|
|
448
|
+
}, undefined>;
|
|
449
|
+
}, undefined>, v.ObjectSchema<{
|
|
450
|
+
readonly type: v.LiteralSchema<"sandbox-verify-response", undefined>;
|
|
451
|
+
readonly flutterBridgeReachable: v.BooleanSchema<undefined>;
|
|
452
|
+
readonly condorBridgeReachable: v.BooleanSchema<undefined>;
|
|
453
|
+
}, undefined>], undefined>;
|
|
454
|
+
type ClientToShellMessage = v.InferOutput<typeof ClientToShellMessage>;
|
|
455
|
+
declare const ShellToClientMessage: v.VariantSchema<"type", [v.ObjectSchema<{
|
|
456
|
+
readonly type: v.LiteralSchema<"event", undefined>;
|
|
457
|
+
readonly key: v.PicklistSchema<[string, ...string[]], undefined>;
|
|
458
|
+
readonly payload: v.UnknownSchema;
|
|
459
|
+
}, undefined>, v.ObjectSchema<{
|
|
460
|
+
readonly type: v.LiteralSchema<"handshake-ack", undefined>;
|
|
461
|
+
readonly ack: v.ObjectSchema<{
|
|
462
|
+
readonly shellVersion: v.StringSchema<undefined>;
|
|
463
|
+
readonly accepted: v.BooleanSchema<undefined>;
|
|
464
|
+
}, undefined>;
|
|
465
|
+
}, undefined>, v.ObjectSchema<{
|
|
466
|
+
readonly type: v.LiteralSchema<"nack", undefined>;
|
|
467
|
+
readonly key: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
468
|
+
readonly command: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
469
|
+
readonly reason: v.StringSchema<undefined>;
|
|
470
|
+
}, undefined>, v.ObjectSchema<{
|
|
471
|
+
readonly type: v.LiteralSchema<"verify-sandbox", undefined>;
|
|
472
|
+
}, undefined>], undefined>;
|
|
473
|
+
type ShellToClientMessage = v.InferOutput<typeof ShellToClientMessage>;
|
|
474
|
+
/**
|
|
475
|
+
* Policy default-deny aplicada pelo `BridgeShell`. Toda chave/comando
|
|
476
|
+
* vindo do iframe é validado contra estas listas antes de tocar o
|
|
477
|
+
* bridge nativo.
|
|
478
|
+
*
|
|
479
|
+
* - `readOnlyKeys`: chaves que o cliente pode subscrever (observáveis).
|
|
480
|
+
* - `allowedCommands`: comandos pass-through (sem confirmação humana).
|
|
481
|
+
* - `requiresConfirmation`: comandos que disparam callback `onConfirmationRequest`
|
|
482
|
+
* no shell — usuário decide aprovar/recusar via UI nativa do shell.
|
|
483
|
+
* - `deniedCommands`: bloqueio explícito (defesa em profundidade — comandos
|
|
484
|
+
* destrutivos como `dfu` que nunca devem fluir do iframe).
|
|
485
|
+
*
|
|
486
|
+
* **Não embedada no package** — cliente do `BridgeShell` (no monorepo
|
|
487
|
+
* `sleep_whitelabel`) provê a policy concreta. Mantém o package agnóstico
|
|
488
|
+
* ao catálogo e permite policy evoluir sem novo release.
|
|
489
|
+
*/
|
|
490
|
+
interface ShellPolicy {
|
|
491
|
+
readOnlyKeys: string[];
|
|
492
|
+
allowedCommands: string[];
|
|
493
|
+
requiresConfirmation: string[];
|
|
494
|
+
deniedCommands: string[];
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Resposta do callback `onConfirmationRequest` do `BridgeShell`.
|
|
498
|
+
*
|
|
499
|
+
* `approve: true` → comando flui para o nativo.
|
|
500
|
+
* `approve: false` → shell envia `nack` ao iframe com `reason: 'user-rejected'`.
|
|
501
|
+
*/
|
|
502
|
+
interface ConfirmationRequest {
|
|
503
|
+
command: string;
|
|
504
|
+
args: Record<string, unknown>;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Estado retornado pelo `verifySandbox()` do `BridgeShell` após
|
|
508
|
+
* round-trip com o iframe. Se algum dos `*Reachable` vier `true`, o
|
|
509
|
+
* boundary está furado — Camada 1 (`bootstrapClientIframe` com override
|
|
510
|
+
* prefix) falhou e a Camada 4 (dual WebView) deve ser acionada.
|
|
511
|
+
*/
|
|
512
|
+
interface SandboxVerifyResult {
|
|
513
|
+
flutterBridgeReachable: boolean;
|
|
514
|
+
condorBridgeReachable: boolean;
|
|
515
|
+
ok: boolean;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* `BridgeShell` (v0.5.0) — API para o shell Condor-controlado que envolve
|
|
520
|
+
* o conteúdo do cliente externo dentro de `<iframe sandbox="allow-scripts">`.
|
|
521
|
+
*
|
|
522
|
+
* Responsabilidades:
|
|
523
|
+
* 1. **Camada 1 do Risco 1** — `bootstrapClientIframe(iframe, clientPath)`:
|
|
524
|
+
* busca HTML do cliente via `fetch`, prepende script de override que
|
|
525
|
+
* deleta `window.flutter_inappwebview` e `window.CondorBridge` do
|
|
526
|
+
* iframe, e seta como `srcdoc`. Override roda em parse-time, antes
|
|
527
|
+
* de qualquer outro script.
|
|
528
|
+
* 2. **Camada 2 do Risco 1** — `verifySandbox(iframe)`: round-trip
|
|
529
|
+
* `postMessage` que pede ao iframe atestation autoritativa de que
|
|
530
|
+
* `window.flutter_inappwebview` e `window.CondorBridge` são `undefined`.
|
|
531
|
+
* 3. **Roteamento postMessage iframe ↔ nativo:** escuta `message` events
|
|
532
|
+
* do iframe, valida com Valibot, aplica `ShellPolicy` default-deny,
|
|
533
|
+
* e propaga para o `Bridge` legado (que fala com o nativo). Eventos
|
|
534
|
+
* nativos voltam para o iframe via `postMessage`.
|
|
535
|
+
* 4. **Confirmação humana para comandos sensíveis:** comandos em
|
|
536
|
+
* `policy.requiresConfirmation` disparam callback
|
|
537
|
+
* `onConfirmationRequest` antes de tocar o nativo — shell apresenta UI.
|
|
538
|
+
*/
|
|
539
|
+
|
|
540
|
+
interface BridgeShellOptions {
|
|
541
|
+
/**
|
|
542
|
+
* Iframe que hospeda o cliente. Pode estar vazio no momento da
|
|
543
|
+
* construção — `bootstrapClientIframe` será chamado depois para popular.
|
|
544
|
+
*/
|
|
545
|
+
iframe: HTMLIFrameElement;
|
|
546
|
+
/**
|
|
547
|
+
* Policy default-deny. Toda chave/comando vindo do iframe é validado
|
|
548
|
+
* contra estas listas. Vide `ShellPolicy`.
|
|
549
|
+
*/
|
|
550
|
+
policy: ShellPolicy;
|
|
551
|
+
/**
|
|
552
|
+
* Callback síncrono ou assíncrono chamado quando o cliente pede um
|
|
553
|
+
* comando em `policy.requiresConfirmation`. Shell apresenta UI de
|
|
554
|
+
* confirmação; resolve com `true` (aprovar) ou `false` (recusar).
|
|
555
|
+
* Se não fornecido, comandos `requiresConfirmation` são **negados**
|
|
556
|
+
* automaticamente — fail-safe.
|
|
557
|
+
*/
|
|
558
|
+
onConfirmationRequest?: (req: ConfirmationRequest) => boolean | Promise<boolean>;
|
|
559
|
+
}
|
|
560
|
+
declare class BridgeShell {
|
|
561
|
+
private readonly iframe;
|
|
562
|
+
private readonly policy;
|
|
563
|
+
private readonly onConfirm?;
|
|
564
|
+
private readonly subscriptions;
|
|
565
|
+
private detached;
|
|
566
|
+
private pendingVerifies;
|
|
567
|
+
constructor(opts: BridgeShellOptions);
|
|
568
|
+
/**
|
|
569
|
+
* **Camada 1 do Risco 1.** Bootstrap via fetch de URL.
|
|
570
|
+
*
|
|
571
|
+
* Atalho para o caso comum HTTP/HTTPS: faz `fetch(clientPath)` e
|
|
572
|
+
* delega para `bootstrapClientIframeWithHtml`.
|
|
573
|
+
*
|
|
574
|
+
* **Não funciona em scheme `file://`** — `fetch` em WebView Android
|
|
575
|
+
* (Chromium) bloqueia o scheme com "URL scheme 'file' is not supported".
|
|
576
|
+
* Para asset bundled em APK ou bundle inlined no build-time, use
|
|
577
|
+
* `bootstrapClientIframeWithHtml(htmlString)`.
|
|
578
|
+
*
|
|
579
|
+
* **Devo ser chamado uma vez no boot do shell**, antes de qualquer
|
|
580
|
+
* outra interação. Após retornar, `verifySandbox()` deve ser invocado
|
|
581
|
+
* para atestation (Camada 2).
|
|
582
|
+
*/
|
|
583
|
+
bootstrapClientIframe(clientPath?: string): Promise<void>;
|
|
584
|
+
/**
|
|
585
|
+
* **Camada 1 do Risco 1.** Bootstrap a partir de HTML string.
|
|
586
|
+
*
|
|
587
|
+
* Aceita HTML do cliente diretamente (já lido de arquivo, embedado em
|
|
588
|
+
* build-time via `esbuild.define`, lido de IndexedDB, etc.) e seta
|
|
589
|
+
* como `srcdoc` do iframe prefixado pelo override script. Override
|
|
590
|
+
* roda em parse-time, antes de qualquer outro script do cliente.
|
|
591
|
+
*
|
|
592
|
+
* Use quando `fetch` não é aplicável (asset bundled em `file://`,
|
|
593
|
+
* bundle inlined no build, qualquer cenário sem rede). Para HTTP
|
|
594
|
+
* use `bootstrapClientIframe(clientPath)`.
|
|
595
|
+
*
|
|
596
|
+
* **Devo ser chamado uma vez no boot do shell**, antes de qualquer
|
|
597
|
+
* outra interação. Após retornar, `verifySandbox()` deve ser invocado
|
|
598
|
+
* para atestation (Camada 2).
|
|
599
|
+
*/
|
|
600
|
+
bootstrapClientIframeWithHtml(clientHtml: string): Promise<void>;
|
|
601
|
+
private waitForIframeLoad;
|
|
602
|
+
/**
|
|
603
|
+
* **Camada 2 do Risco 1.** Round-trip `postMessage` que pede ao iframe
|
|
604
|
+
* atestation autoritativa do estado de `window.flutter_inappwebview`
|
|
605
|
+
* e `window.CondorBridge`. Timeout em 2s.
|
|
606
|
+
*
|
|
607
|
+
* `ok` é `true` apenas se ambos forem `undefined` no iframe. Se vier
|
|
608
|
+
* `false`, **shell deve abortar o boot do app** — Camada 1 falhou e
|
|
609
|
+
* Camada 4 (dual WebView) precisa ser acionada manualmente.
|
|
610
|
+
*/
|
|
611
|
+
verifySandbox(): Promise<SandboxVerifyResult>;
|
|
612
|
+
/**
|
|
613
|
+
* Encerra o shell — remove listener `message`, cancela subscriptions
|
|
614
|
+
* pendentes no `Bridge` nativo, limpa estado interno.
|
|
615
|
+
*/
|
|
616
|
+
detach(): void;
|
|
617
|
+
private onMessage;
|
|
618
|
+
private handleSubscribe;
|
|
619
|
+
private handleUnsubscribe;
|
|
620
|
+
private handleSend;
|
|
621
|
+
private postToClient;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
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 MockMessage, type MockState, type PayloadOf, type PingPongPayload, type RealTimeAckPayload, type SandboxVerifyResult, type SensorLogPayload, type ShellPolicy, ShellToClientMessage, type SyncTimeAckPayload, type UserDevicePayload };
|