@condorinst/mobile-bridge 0.16.0 → 0.18.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 +68 -0
- package/README.md +9 -5
- package/dist/index.cjs +93 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +102 -4
- package/dist/index.d.ts +102 -4
- package/dist/index.js +91 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,74 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.18.0] — 2026-09-16
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Native transport over the `CondorNative` JavaScript channel.** The app is
|
|
15
|
+
moving from `flutter_inappwebview` to the official `webview_flutter`, whose
|
|
16
|
+
only JS → native path is a named channel that accepts a string. When
|
|
17
|
+
`window.CondorNative.postMessage` exists, the SDK uses it and sends the
|
|
18
|
+
`{type, version, payload}` envelope as JSON. It takes precedence over
|
|
19
|
+
`window.flutter_inappwebview`, which keeps working for the transition.
|
|
20
|
+
- **`Bridge.setOutgoingStamp(stamp | null)`.** A single, supported hook that
|
|
21
|
+
transforms every JS → native message (handshake included) right before it is
|
|
22
|
+
sent. It exists so the shell can stamp its nonce. Until now the shell had to
|
|
23
|
+
monkey-patch `flutter_inappwebview.callHandler`, which has no equivalent on a
|
|
24
|
+
channel. Client code should not use it.
|
|
25
|
+
- `NATIVE_CHANNEL_NAME` and the `OutgoingStamp` type are exported.
|
|
26
|
+
- `SandboxVerifyResult.nativeChannelReachable`: the client now also reports
|
|
27
|
+
whether `window.CondorNative` is visible inside the iframe, and `ok` requires
|
|
28
|
+
it to be `false`. Clients built with an older SDK do not send the field; the
|
|
29
|
+
shell treats it as `false`.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- **Layer 1 override prefix also removes and locks `window.CondorNative`.**
|
|
34
|
+
Measured on a real iPhone (iOS 26.6) and an Android device: the prefix removes
|
|
35
|
+
the channel from the iframe, but the WebView injects the channel into every
|
|
36
|
+
frame, so a child iframe created by the client gets it back, and on iOS
|
|
37
|
+
`window.webkit.messageHandlers` cannot be locked. Layer 1 is defense in depth;
|
|
38
|
+
the guarantee is the nonce validated natively (layer 3), which discarded every
|
|
39
|
+
message sent through those paths.
|
|
40
|
+
|
|
41
|
+
### Deprecated
|
|
42
|
+
|
|
43
|
+
- The `window.flutter_inappwebview` host. It will be removed in a future minor
|
|
44
|
+
once no app ships it.
|
|
45
|
+
|
|
46
|
+
## [0.17.0] — 2026-09-01
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- **The shell now relays `nativeVersion` to the sandboxed client.**
|
|
51
|
+
`BridgeClientHandshakeAck` gains `nativeVersion: string | null`, and the shell
|
|
52
|
+
performs the native handshake on the client's behalf to fill it in.
|
|
53
|
+
|
|
54
|
+
This closes a hole the 0.16.0 docs walked straight into. Those docs told
|
|
55
|
+
consumers to "check `nativeVersion` from the handshake ack" before relying on
|
|
56
|
+
the new raw light channels — but a client lives inside a sandboxed iframe and
|
|
57
|
+
handshakes with the *shell*, not the native, and the shell answered with
|
|
58
|
+
`shellVersion` alone. The check was impossible to perform. Worse, nothing in
|
|
59
|
+
the SDK ever called `Bridge.handshake`, so the native version was not merely
|
|
60
|
+
unreachable from the client: nobody obtained it at all.
|
|
61
|
+
|
|
62
|
+
`null` means the native has not answered (or does not exist, as in a browser
|
|
63
|
+
mock) and is honest information, not a placeholder. `Bridge.handshake` has no
|
|
64
|
+
timeout of its own, so the shell bounds the wait at 2s and answers the client
|
|
65
|
+
either way — without that, a client in a browser would hang forever waiting
|
|
66
|
+
for a handshake that never resolves.
|
|
67
|
+
|
|
68
|
+
### Changed
|
|
69
|
+
|
|
70
|
+
- **The shell touches the native during the client handshake.** Until 0.16.x it
|
|
71
|
+
answered immediately and deliberately did not; a test asserted exactly that.
|
|
72
|
+
That test now asserts the relay instead. Clients see one extra round-trip on
|
|
73
|
+
handshake, bounded at 2s.
|
|
74
|
+
- `SHELL_VERSION` 0.5.1 → 0.6.0.
|
|
75
|
+
- `client_demo` logs `nativeVersion` alongside `shellVersion`, which is how an
|
|
76
|
+
E2E confirms which native it is actually talking to.
|
|
77
|
+
|
|
10
78
|
## [0.16.0] — 2026-09-01
|
|
11
79
|
|
|
12
80
|
### Added
|
package/README.md
CHANGED
|
@@ -146,13 +146,13 @@ if (!verify.ok) {
|
|
|
146
146
|
// automatically. Nothing else to do at boot.
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
-
### Layered defense (propagation of
|
|
149
|
+
### Layered defense (propagation of the native channel into the iframe)
|
|
150
150
|
|
|
151
151
|
| Layer | Where | What it does |
|
|
152
152
|
|---|---|---|
|
|
153
|
-
| **1** | `BridgeShell.bootstrapClientIframe` | `srcdoc` with a prefix that deletes `window.flutter_inappwebview`
|
|
154
|
-
| **2** | `BridgeShell.verifySandbox` + `BridgeClient` handler | Round-trip `postMessage` to attest the iframe state. |
|
|
155
|
-
| **3** | (Native side) | Shell nonce validated natively
|
|
153
|
+
| **1** | `BridgeShell.bootstrapClientIframe` | `srcdoc` with a prefix that deletes `window.CondorNative`, `window.flutter_inappwebview` and `window.CondorBridge` before any client script runs. Defense in depth only: the WebView injects the channel into every frame, so a child iframe created by the client gets it back (and on iOS `window.webkit.messageHandlers` cannot be locked). |
|
|
154
|
+
| **2** | `BridgeShell.verifySandbox` + `BridgeClient` handler | Round-trip `postMessage` to attest the iframe state (`nativeChannelReachable` since 0.18.0). |
|
|
155
|
+
| **3** | (Native side) | Shell nonce, stamped on every outgoing message via `Bridge.setOutgoingStamp`, validated natively. **This is the guarantee.** |
|
|
156
156
|
| **4** | (Fallback if 1+2 fail) | Dual `InAppWebView` — shell and client in separate WebViews. |
|
|
157
157
|
|
|
158
158
|
## Build CLI — `condor-mobile-bridge build`
|
|
@@ -253,7 +253,11 @@ land once there are 2+ versions in the field.
|
|
|
253
253
|
|
|
254
254
|
The package detects automatically:
|
|
255
255
|
|
|
256
|
-
- **Android / iOS** — `window.
|
|
256
|
+
- **Android / iOS** — `window.CondorNative`, a JavaScript channel registered with
|
|
257
|
+
`webview_flutter` (`addJavaScriptChannel('CondorNative')`). Messages are sent as a
|
|
258
|
+
JSON string. Takes precedence when present.
|
|
259
|
+
- **Android / iOS (legacy)** — `window.flutter_inappwebview` (via the `flutter_inappwebview`
|
|
260
|
+
package). Still supported for the transition; will be removed in a future minor.
|
|
257
261
|
- **Browser outside the app** (dev/test): **mock mode** activates automatically.
|
|
258
262
|
|
|
259
263
|
## Mock mode
|
package/dist/index.cjs
CHANGED
|
@@ -33,7 +33,8 @@ __export(index_exports, {
|
|
|
33
33
|
BRIDGE_KEYS: () => BRIDGE_KEYS,
|
|
34
34
|
Bridge: () => Bridge,
|
|
35
35
|
BridgeClient: () => BridgeClient,
|
|
36
|
-
BridgeShell: () => BridgeShell
|
|
36
|
+
BridgeShell: () => BridgeShell,
|
|
37
|
+
NATIVE_CHANNEL_NAME: () => NATIVE_CHANNEL_NAME
|
|
37
38
|
});
|
|
38
39
|
module.exports = __toCommonJS(index_exports);
|
|
39
40
|
|
|
@@ -469,8 +470,22 @@ function createMockAdapter() {
|
|
|
469
470
|
}
|
|
470
471
|
|
|
471
472
|
// src/internal/native.ts
|
|
473
|
+
var NATIVE_CHANNEL_NAME = "CondorNative";
|
|
474
|
+
var outgoingStamp = null;
|
|
475
|
+
function setOutgoingStamp(stamp) {
|
|
476
|
+
outgoingStamp = stamp;
|
|
477
|
+
}
|
|
472
478
|
var activeAdapter = null;
|
|
473
|
-
function
|
|
479
|
+
function createChannelAdapter(channel) {
|
|
480
|
+
return {
|
|
481
|
+
isMock: false,
|
|
482
|
+
// Canal único: o `handlerName` não viaja — o nativo roteia por `type`.
|
|
483
|
+
callHandler(_handlerName, payload) {
|
|
484
|
+
channel.postMessage(JSON.stringify(payload));
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
function createLegacyAdapter(host) {
|
|
474
489
|
return {
|
|
475
490
|
isMock: false,
|
|
476
491
|
callHandler(handlerName, payload) {
|
|
@@ -482,9 +497,13 @@ function detectAdapter() {
|
|
|
482
497
|
if (typeof window === "undefined") {
|
|
483
498
|
return createMockAdapter();
|
|
484
499
|
}
|
|
500
|
+
const channel = window.CondorNative;
|
|
501
|
+
if (channel && typeof channel.postMessage === "function") {
|
|
502
|
+
return createChannelAdapter(channel);
|
|
503
|
+
}
|
|
485
504
|
const host = window.flutter_inappwebview;
|
|
486
505
|
if (host && typeof host.callHandler === "function") {
|
|
487
|
-
return
|
|
506
|
+
return createLegacyAdapter(host);
|
|
488
507
|
}
|
|
489
508
|
return createMockAdapter();
|
|
490
509
|
}
|
|
@@ -495,7 +514,8 @@ function getActiveAdapter() {
|
|
|
495
514
|
return activeAdapter;
|
|
496
515
|
}
|
|
497
516
|
function callNative(handlerName, payload) {
|
|
498
|
-
|
|
517
|
+
const stamped = outgoingStamp ? outgoingStamp(payload) : payload;
|
|
518
|
+
getActiveAdapter().callHandler(handlerName, stamped);
|
|
499
519
|
}
|
|
500
520
|
|
|
501
521
|
// src/internal/subscribers.ts
|
|
@@ -611,6 +631,9 @@ var Bridge = {
|
|
|
611
631
|
openSettings() {
|
|
612
632
|
this.send("openSettings");
|
|
613
633
|
},
|
|
634
|
+
setOutgoingStamp(stamp) {
|
|
635
|
+
setOutgoingStamp(stamp);
|
|
636
|
+
},
|
|
614
637
|
mockEmit(key, payload) {
|
|
615
638
|
requireMockAdapter().triggerEmit(key, payload ?? {});
|
|
616
639
|
},
|
|
@@ -671,6 +694,8 @@ function onMessage(event) {
|
|
|
671
694
|
pendingHandshake2 = null;
|
|
672
695
|
resolve({
|
|
673
696
|
shellVersion: msg.ack?.shellVersion ?? "unknown",
|
|
697
|
+
// `?? null` cobre o shell anterior à 0.17.0, que não mandava o campo.
|
|
698
|
+
nativeVersion: msg.ack?.nativeVersion ?? null,
|
|
674
699
|
accepted: msg.ack?.accepted ?? false
|
|
675
700
|
});
|
|
676
701
|
}
|
|
@@ -684,7 +709,8 @@ function onMessage(event) {
|
|
|
684
709
|
postToShell({
|
|
685
710
|
type: "sandbox-verify-response",
|
|
686
711
|
flutterBridgeReachable: typeof w.flutter_inappwebview !== "undefined",
|
|
687
|
-
condorBridgeReachable: typeof w.CondorBridge !== "undefined"
|
|
712
|
+
condorBridgeReachable: typeof w.CondorBridge !== "undefined",
|
|
713
|
+
nativeChannelReachable: typeof w.CondorNative !== "undefined"
|
|
688
714
|
});
|
|
689
715
|
break;
|
|
690
716
|
}
|
|
@@ -810,7 +836,9 @@ var JsErrorMessage = v.object({
|
|
|
810
836
|
var SandboxVerifyResponseMessage = v.object({
|
|
811
837
|
type: v.literal("sandbox-verify-response"),
|
|
812
838
|
flutterBridgeReachable: v.boolean(),
|
|
813
|
-
condorBridgeReachable: v.boolean()
|
|
839
|
+
condorBridgeReachable: v.boolean(),
|
|
840
|
+
// Opcional: clientes com SDK < 0.18.0 não enviam.
|
|
841
|
+
nativeChannelReachable: v.optional(v.boolean())
|
|
814
842
|
});
|
|
815
843
|
var ClientToShellMessage = v.variant("type", [
|
|
816
844
|
SubscribeMessage,
|
|
@@ -829,6 +857,15 @@ var HandshakeAckMessage = v.object({
|
|
|
829
857
|
type: v.literal("handshake-ack"),
|
|
830
858
|
ack: v.object({
|
|
831
859
|
shellVersion: v.string(),
|
|
860
|
+
/**
|
|
861
|
+
* Versão do schema declarada pelo **nativo**, repassada pelo shell.
|
|
862
|
+
*
|
|
863
|
+
* `null` quando o nativo ainda não respondeu (ou não existe, como no
|
|
864
|
+
* mock de browser). O cliente roda dentro de um iframe sandboxed e não
|
|
865
|
+
* fala com o nativo diretamente, então este é o **único** caminho pelo
|
|
866
|
+
* qual ele descobre de qual versão são as capacidades disponíveis.
|
|
867
|
+
*/
|
|
868
|
+
nativeVersion: v.nullable(v.string()),
|
|
832
869
|
accepted: v.boolean()
|
|
833
870
|
})
|
|
834
871
|
});
|
|
@@ -849,12 +886,14 @@ var ShellToClientMessage = v.variant("type", [
|
|
|
849
886
|
]);
|
|
850
887
|
|
|
851
888
|
// src/bridge-shell.ts
|
|
852
|
-
var SHELL_VERSION = "0.
|
|
889
|
+
var SHELL_VERSION = "0.6.0";
|
|
890
|
+
var NATIVE_HANDSHAKE_TIMEOUT_MS = 2e3;
|
|
853
891
|
var VERIFY_TIMEOUT_MS = 2e3;
|
|
854
892
|
var OVERRIDE_PREFIX = `<script>
|
|
855
893
|
(function(){
|
|
856
894
|
try { delete window.flutter_inappwebview; } catch (_) {}
|
|
857
895
|
try { delete window.CondorBridge; } catch (_) {}
|
|
896
|
+
try { delete window.CondorNative; } catch (_) {}
|
|
858
897
|
try {
|
|
859
898
|
Object.defineProperty(window, 'flutter_inappwebview', {
|
|
860
899
|
get: function(){ return undefined; },
|
|
@@ -862,6 +901,13 @@ var OVERRIDE_PREFIX = `<script>
|
|
|
862
901
|
configurable: false,
|
|
863
902
|
});
|
|
864
903
|
} catch (_) {}
|
|
904
|
+
try {
|
|
905
|
+
Object.defineProperty(window, 'CondorNative', {
|
|
906
|
+
get: function(){ return undefined; },
|
|
907
|
+
set: function(_){ /* no-op: bloqueia o canal do webview_flutter no iframe */ },
|
|
908
|
+
configurable: false,
|
|
909
|
+
});
|
|
910
|
+
} catch (_) {}
|
|
865
911
|
try {
|
|
866
912
|
Object.defineProperty(window, 'CondorBridge', {
|
|
867
913
|
get: function(){ return undefined; },
|
|
@@ -887,6 +933,13 @@ var BridgeShell = class {
|
|
|
887
933
|
constructor(opts) {
|
|
888
934
|
this.subscriptions = /* @__PURE__ */ new Map();
|
|
889
935
|
this.detached = false;
|
|
936
|
+
/**
|
|
937
|
+
* Versão do nativo, obtida uma vez e cacheada. `null` até o nativo
|
|
938
|
+
* responder — e permanentemente `null` num browser sem nativo.
|
|
939
|
+
*/
|
|
940
|
+
this.nativeVersion = null;
|
|
941
|
+
/** Handshake com o nativo em voo; garante uma única chamada. */
|
|
942
|
+
this.nativeHandshake = null;
|
|
890
943
|
// Map de `verify-sandbox` round-trips pendentes — key gerada por nonce.
|
|
891
944
|
this.pendingVerifies = /* @__PURE__ */ new Map();
|
|
892
945
|
this.onMessage = async (event) => {
|
|
@@ -911,9 +964,11 @@ var BridgeShell = class {
|
|
|
911
964
|
await this.handleSend(msg.command, msg.args ?? {});
|
|
912
965
|
break;
|
|
913
966
|
case "handshake":
|
|
914
|
-
this.
|
|
915
|
-
|
|
916
|
-
|
|
967
|
+
void this.resolveNativeVersion().then((nativeVersion) => {
|
|
968
|
+
this.postToClient({
|
|
969
|
+
type: "handshake-ack",
|
|
970
|
+
ack: { shellVersion: SHELL_VERSION, nativeVersion, accepted: true }
|
|
971
|
+
});
|
|
917
972
|
});
|
|
918
973
|
break;
|
|
919
974
|
case "jsError":
|
|
@@ -933,10 +988,12 @@ var BridgeShell = class {
|
|
|
933
988
|
if (pending) {
|
|
934
989
|
clearTimeout(pending.timer);
|
|
935
990
|
this.pendingVerifies.delete(first);
|
|
936
|
-
const
|
|
991
|
+
const nativeChannelReachable = msg.nativeChannelReachable ?? false;
|
|
992
|
+
const ok = !msg.flutterBridgeReachable && !msg.condorBridgeReachable && !nativeChannelReachable;
|
|
937
993
|
pending.resolve({
|
|
938
994
|
flutterBridgeReachable: msg.flutterBridgeReachable,
|
|
939
995
|
condorBridgeReachable: msg.condorBridgeReachable,
|
|
996
|
+
nativeChannelReachable,
|
|
940
997
|
ok
|
|
941
998
|
});
|
|
942
999
|
}
|
|
@@ -1092,6 +1149,29 @@ var BridgeShell = class {
|
|
|
1092
1149
|
}
|
|
1093
1150
|
Bridge.send(command, args);
|
|
1094
1151
|
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Obtém o `nativeVersion`, disparando o handshake com o nativo na primeira
|
|
1154
|
+
* vez e cacheando o resultado.
|
|
1155
|
+
*
|
|
1156
|
+
* ⚠️ `Bridge.handshake` **não tem timeout**: a Promise só resolve quando o
|
|
1157
|
+
* nativo emite `handshake-ack`. Num browser sem nativo ela nunca resolve,
|
|
1158
|
+
* então o `race` abaixo é o que garante que o cliente sempre receba
|
|
1159
|
+
* resposta — com `null` — em vez de ficar pendurado.
|
|
1160
|
+
*/
|
|
1161
|
+
async resolveNativeVersion() {
|
|
1162
|
+
if (this.nativeVersion !== null) return this.nativeVersion;
|
|
1163
|
+
if (!this.nativeHandshake) {
|
|
1164
|
+
this.nativeHandshake = Bridge.handshake({ clientVersion: SHELL_VERSION }).then((ack) => {
|
|
1165
|
+
this.nativeVersion = ack.nativeVersion;
|
|
1166
|
+
}).catch(() => {
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
await Promise.race([
|
|
1170
|
+
this.nativeHandshake,
|
|
1171
|
+
new Promise((resolve) => setTimeout(resolve, NATIVE_HANDSHAKE_TIMEOUT_MS))
|
|
1172
|
+
]);
|
|
1173
|
+
return this.nativeVersion;
|
|
1174
|
+
}
|
|
1095
1175
|
postToClient(msg) {
|
|
1096
1176
|
if (this.detached) return;
|
|
1097
1177
|
this.iframe.contentWindow?.postMessage(msg, "*");
|
|
@@ -1102,6 +1182,7 @@ var BridgeShell = class {
|
|
|
1102
1182
|
BRIDGE_KEYS,
|
|
1103
1183
|
Bridge,
|
|
1104
1184
|
BridgeClient,
|
|
1105
|
-
BridgeShell
|
|
1185
|
+
BridgeShell,
|
|
1186
|
+
NATIVE_CHANNEL_NAME
|
|
1106
1187
|
});
|
|
1107
1188
|
//# sourceMappingURL=index.cjs.map
|