@condorinst/mobile-bridge 0.17.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 CHANGED
@@ -7,6 +7,42 @@ 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
+
10
46
  ## [0.17.0] — 2026-09-01
11
47
 
12
48
  ### 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 `window.flutter_inappwebview`)
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` + `window.CondorBridge` before any client script runs. |
154
- | **2** | `BridgeShell.verifySandbox` + `BridgeClient` handler | Round-trip `postMessage` to attest the iframe state. |
155
- | **3** | (Native side) | Shell nonce validated natively in the Flutter bridge. |
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.flutter_inappwebview` (via the `flutter_inappwebview` package on the Flutter side).
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 createRealAdapter(host) {
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 createRealAdapter(host);
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
- getActiveAdapter().callHandler(handlerName, payload);
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
  },
@@ -686,7 +709,8 @@ function onMessage(event) {
686
709
  postToShell({
687
710
  type: "sandbox-verify-response",
688
711
  flutterBridgeReachable: typeof w.flutter_inappwebview !== "undefined",
689
- condorBridgeReachable: typeof w.CondorBridge !== "undefined"
712
+ condorBridgeReachable: typeof w.CondorBridge !== "undefined",
713
+ nativeChannelReachable: typeof w.CondorNative !== "undefined"
690
714
  });
691
715
  break;
692
716
  }
@@ -812,7 +836,9 @@ var JsErrorMessage = v.object({
812
836
  var SandboxVerifyResponseMessage = v.object({
813
837
  type: v.literal("sandbox-verify-response"),
814
838
  flutterBridgeReachable: v.boolean(),
815
- condorBridgeReachable: v.boolean()
839
+ condorBridgeReachable: v.boolean(),
840
+ // Opcional: clientes com SDK < 0.18.0 não enviam.
841
+ nativeChannelReachable: v.optional(v.boolean())
816
842
  });
817
843
  var ClientToShellMessage = v.variant("type", [
818
844
  SubscribeMessage,
@@ -867,6 +893,7 @@ var OVERRIDE_PREFIX = `<script>
867
893
  (function(){
868
894
  try { delete window.flutter_inappwebview; } catch (_) {}
869
895
  try { delete window.CondorBridge; } catch (_) {}
896
+ try { delete window.CondorNative; } catch (_) {}
870
897
  try {
871
898
  Object.defineProperty(window, 'flutter_inappwebview', {
872
899
  get: function(){ return undefined; },
@@ -874,6 +901,13 @@ var OVERRIDE_PREFIX = `<script>
874
901
  configurable: false,
875
902
  });
876
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 (_) {}
877
911
  try {
878
912
  Object.defineProperty(window, 'CondorBridge', {
879
913
  get: function(){ return undefined; },
@@ -954,10 +988,12 @@ var BridgeShell = class {
954
988
  if (pending) {
955
989
  clearTimeout(pending.timer);
956
990
  this.pendingVerifies.delete(first);
957
- const ok = !msg.flutterBridgeReachable && !msg.condorBridgeReachable;
991
+ const nativeChannelReachable = msg.nativeChannelReachable ?? false;
992
+ const ok = !msg.flutterBridgeReachable && !msg.condorBridgeReachable && !nativeChannelReachable;
958
993
  pending.resolve({
959
994
  flutterBridgeReachable: msg.flutterBridgeReachable,
960
995
  condorBridgeReachable: msg.condorBridgeReachable,
996
+ nativeChannelReachable,
961
997
  ok
962
998
  });
963
999
  }
@@ -1146,6 +1182,7 @@ var BridgeShell = class {
1146
1182
  BRIDGE_KEYS,
1147
1183
  Bridge,
1148
1184
  BridgeClient,
1149
- BridgeShell
1185
+ BridgeShell,
1186
+ NATIVE_CHANNEL_NAME
1150
1187
  });
1151
1188
  //# sourceMappingURL=index.cjs.map