@condorinst/mobile-bridge 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -12,10 +12,10 @@ var BRIDGE_KEYS = {
12
12
  dfu: "dfu",
13
13
  systemStatus: "systemStatus",
14
14
  /**
15
- * Native → client: o back do SO foi pressionado e o cliente reportou
16
- * ter histórico interno (`reportNavigation(true)`). O cliente deve voltar
17
- * UMA página interna. Emitido só quando o nativo decidiu delegar o back
18
- * ao cliente. Vide `BridgeClient.onBack`.
15
+ * Native → client: the OS back button was pressed and the client had
16
+ * reported internal history via `reportNavigation(true)`. Go back exactly
17
+ * ONE internal page. Emitted only when the native side decided to delegate
18
+ * the back gesture. See `BridgeClient.onBack`.
19
19
  */
20
20
  navigationBack: "navigationBack",
21
21
  /**
@@ -23,7 +23,7 @@ var BRIDGE_KEYS = {
23
23
  * AND the language explicitly chosen in the app Settings, so the client can
24
24
  * apply its own precedence rule — including rendering a language the native
25
25
  * chrome does not support. Emitted on attach, after `handshake-ack`, and on
26
- * every change in Settings. Vide `LanguagePayload`.
26
+ * every change in Settings. See `LanguagePayload`.
27
27
  */
28
28
  language: "language"
29
29
  };
@@ -430,8 +430,23 @@ function createMockAdapter() {
430
430
  }
431
431
 
432
432
  // src/internal/native.ts
433
+ var NATIVE_CHANNEL_NAME = "CondorNative";
434
+ var outgoingStamp = null;
435
+ function setOutgoingStamp(stamp) {
436
+ outgoingStamp = stamp;
437
+ }
433
438
  var activeAdapter = null;
434
- function createRealAdapter(host) {
439
+ function createChannelAdapter(channel) {
440
+ return {
441
+ isMock: false,
442
+ // Single channel: `handlerName` does not travel — the native side routes
443
+ // on `type`.
444
+ callHandler(_handlerName, payload) {
445
+ channel.postMessage(JSON.stringify(payload));
446
+ }
447
+ };
448
+ }
449
+ function createLegacyAdapter(host) {
435
450
  return {
436
451
  isMock: false,
437
452
  callHandler(handlerName, payload) {
@@ -443,9 +458,13 @@ function detectAdapter() {
443
458
  if (typeof window === "undefined") {
444
459
  return createMockAdapter();
445
460
  }
461
+ const channel = window.CondorNative;
462
+ if (channel && typeof channel.postMessage === "function") {
463
+ return createChannelAdapter(channel);
464
+ }
446
465
  const host = window.flutter_inappwebview;
447
466
  if (host && typeof host.callHandler === "function") {
448
- return createRealAdapter(host);
467
+ return createLegacyAdapter(host);
449
468
  }
450
469
  return createMockAdapter();
451
470
  }
@@ -456,7 +475,8 @@ function getActiveAdapter() {
456
475
  return activeAdapter;
457
476
  }
458
477
  function callNative(handlerName, payload) {
459
- getActiveAdapter().callHandler(handlerName, payload);
478
+ const stamped = outgoingStamp ? outgoingStamp(payload) : payload;
479
+ getActiveAdapter().callHandler(handlerName, stamped);
460
480
  }
461
481
 
462
482
  // src/internal/subscribers.ts
@@ -572,6 +592,9 @@ var Bridge = {
572
592
  openSettings() {
573
593
  this.send("openSettings");
574
594
  },
595
+ setOutgoingStamp(stamp) {
596
+ setOutgoingStamp(stamp);
597
+ },
575
598
  mockEmit(key, payload) {
576
599
  requireMockAdapter().triggerEmit(key, payload ?? {});
577
600
  },
@@ -632,7 +655,7 @@ function onMessage(event) {
632
655
  pendingHandshake2 = null;
633
656
  resolve({
634
657
  shellVersion: msg.ack?.shellVersion ?? "unknown",
635
- // `?? null` cobre o shell anterior à 0.17.0, que não mandava o campo.
658
+ // `?? null` covers shells older than 0.17.0, which omitted the field.
636
659
  nativeVersion: msg.ack?.nativeVersion ?? null,
637
660
  accepted: msg.ack?.accepted ?? false
638
661
  });
@@ -647,7 +670,8 @@ function onMessage(event) {
647
670
  postToShell({
648
671
  type: "sandbox-verify-response",
649
672
  flutterBridgeReachable: typeof w.flutter_inappwebview !== "undefined",
650
- condorBridgeReachable: typeof w.CondorBridge !== "undefined"
673
+ condorBridgeReachable: typeof w.CondorBridge !== "undefined",
674
+ nativeChannelReachable: typeof w.CondorNative !== "undefined"
651
675
  });
652
676
  break;
653
677
  }
@@ -773,9 +797,11 @@ var JsErrorMessage = v.object({
773
797
  var SandboxVerifyResponseMessage = v.object({
774
798
  type: v.literal("sandbox-verify-response"),
775
799
  flutterBridgeReachable: v.boolean(),
776
- condorBridgeReachable: v.boolean()
800
+ condorBridgeReachable: v.boolean(),
801
+ // Optional: clients on SDK < 0.18.0 do not send it.
802
+ nativeChannelReachable: v.optional(v.boolean())
777
803
  });
778
- var ClientToShellMessage = v.variant("type", [
804
+ var ClientToShellMessageSchema = v.variant("type", [
779
805
  SubscribeMessage,
780
806
  UnsubscribeMessage,
781
807
  SendMessage,
@@ -793,12 +819,12 @@ var HandshakeAckMessage = v.object({
793
819
  ack: v.object({
794
820
  shellVersion: v.string(),
795
821
  /**
796
- * Versão do schema declarada pelo **nativo**, repassada pelo shell.
822
+ * Schema version declared by the **native** side, relayed by the shell.
797
823
  *
798
- * `null` quando o nativo ainda não respondeu (ou não existe, como no
799
- * mock de browser). O cliente roda dentro de um iframe sandboxed e não
800
- * fala com o nativo diretamente, então este é o **único** caminho pelo
801
- * qual ele descobre de qual versão são as capacidades disponíveis.
824
+ * `null` when the native side has not answered, or does not exist (browser
825
+ * mock mode). The client runs inside a sandboxed iframe and never talks to
826
+ * the native side directly, so this is the **only** way it can learn which
827
+ * capabilities are available.
802
828
  */
803
829
  nativeVersion: v.nullable(v.string()),
804
830
  accepted: v.boolean()
@@ -813,7 +839,7 @@ var NackMessage = v.object({
813
839
  var VerifySandboxMessage = v.object({
814
840
  type: v.literal("verify-sandbox")
815
841
  });
816
- var ShellToClientMessage = v.variant("type", [
842
+ var ShellToClientMessageSchema = v.variant("type", [
817
843
  EventMessage,
818
844
  HandshakeAckMessage,
819
845
  NackMessage,
@@ -828,10 +854,18 @@ var OVERRIDE_PREFIX = `<script>
828
854
  (function(){
829
855
  try { delete window.flutter_inappwebview; } catch (_) {}
830
856
  try { delete window.CondorBridge; } catch (_) {}
857
+ try { delete window.CondorNative; } catch (_) {}
831
858
  try {
832
859
  Object.defineProperty(window, 'flutter_inappwebview', {
833
860
  get: function(){ return undefined; },
834
- set: function(_){ /* no-op: bloqueia inje\xE7\xE3o do plugin no iframe */ },
861
+ set: function(_){ /* no-op: blocks the plugin injecting into the iframe */ },
862
+ configurable: false,
863
+ });
864
+ } catch (_) {}
865
+ try {
866
+ Object.defineProperty(window, 'CondorNative', {
867
+ get: function(){ return undefined; },
868
+ set: function(_){ /* no-op: bloqueia o canal do webview_flutter no iframe */ },
835
869
  configurable: false,
836
870
  });
837
871
  } catch (_) {}
@@ -843,11 +877,11 @@ var OVERRIDE_PREFIX = `<script>
843
877
  });
844
878
  } catch (_) {}
845
879
  try {
846
- // Neutraliza navigator.sendBeacon: canal de exfil que a CSP connect-src
847
- // n\xE3o barra de forma confi\xE1vel em alguns WebViews (pentest 2026-06-24,
848
- // PT-10). O cliente comunica s\xF3 via BridgeClient/postMessage \u2014 n\xE3o tem
849
- // uso leg\xEDtimo de sendBeacon. Getter no-op retorna fun\xE7\xE3o que devolve
850
- // false; setter no-op ignora tentativa de restaurar.
880
+ // Neutralize navigator.sendBeacon: an exfiltration channel that CSP
881
+ // connect-src does not reliably block in some WebViews (pen test PT-10).
882
+ // The client communicates only through BridgeClient/postMessage, so it has
883
+ // no legitimate use for sendBeacon. The no-op getter returns a function
884
+ // that answers false; the no-op setter ignores attempts to restore it.
851
885
  Object.defineProperty(navigator, 'sendBeacon', {
852
886
  get: function(){ return function(){ return false; }; },
853
887
  set: function(_){ /* no-op */ },
@@ -861,18 +895,18 @@ var BridgeShell = class {
861
895
  this.subscriptions = /* @__PURE__ */ new Map();
862
896
  this.detached = false;
863
897
  /**
864
- * Versão do nativo, obtida uma vez e cacheada. `null` até o nativo
865
- * responder — e permanentemente `null` num browser sem nativo.
898
+ * The native version, fetched once and cached. `null` until the native side
899
+ * answers, and permanently `null` in a browser with no native host.
866
900
  */
867
901
  this.nativeVersion = null;
868
- /** Handshake com o nativo em voo; garante uma única chamada. */
902
+ /** In-flight native handshake; guarantees a single call. */
869
903
  this.nativeHandshake = null;
870
- // Map de `verify-sandbox` round-trips pendentes — key gerada por nonce.
904
+ // Pending `verify-sandbox` round trips, keyed by a generated nonce.
871
905
  this.pendingVerifies = /* @__PURE__ */ new Map();
872
906
  this.onMessage = async (event) => {
873
907
  if (this.detached) return;
874
908
  if (event.source !== this.iframe.contentWindow) return;
875
- const parsed = v2.safeParse(ClientToShellMessage, event.data);
909
+ const parsed = v2.safeParse(ClientToShellMessageSchema, event.data);
876
910
  if (!parsed.success) {
877
911
  if (typeof console !== "undefined") {
878
912
  console.debug("[BridgeShell] dropped malformed message:", parsed.issues?.[0]?.message);
@@ -915,10 +949,12 @@ var BridgeShell = class {
915
949
  if (pending) {
916
950
  clearTimeout(pending.timer);
917
951
  this.pendingVerifies.delete(first);
918
- const ok = !msg.flutterBridgeReachable && !msg.condorBridgeReachable;
952
+ const nativeChannelReachable = msg.nativeChannelReachable ?? false;
953
+ const ok = !msg.flutterBridgeReachable && !msg.condorBridgeReachable && !nativeChannelReachable;
919
954
  pending.resolve({
920
955
  flutterBridgeReachable: msg.flutterBridgeReachable,
921
956
  condorBridgeReachable: msg.condorBridgeReachable,
957
+ nativeChannelReachable,
922
958
  ok
923
959
  });
924
960
  }
@@ -935,41 +971,40 @@ var BridgeShell = class {
935
971
  }
936
972
  }
937
973
  /**
938
- * **Camada 1 do Risco 1.** Bootstrap via fetch de URL.
974
+ * **Layer 1.** Bootstraps by fetching a URL.
939
975
  *
940
- * Atalho para o caso comum HTTP/HTTPS: faz `fetch(clientPath)` e
941
- * delega para `bootstrapClientIframeWithHtml`.
976
+ * A shortcut for the common HTTP/HTTPS case: it runs `fetch(clientPath)` and
977
+ * delegates to `bootstrapClientIframeWithHtml`.
942
978
  *
943
- * **Não funciona em scheme `file://`** — `fetch` em WebView Android
944
- * (Chromium) bloqueia o scheme com "URL scheme 'file' is not supported".
945
- * Para asset bundled em APK ou bundle inlined no build-time, use
946
- * `bootstrapClientIframeWithHtml(htmlString)`.
979
+ * **It does not work on the `file://` scheme** — Android's Chromium WebView
980
+ * blocks it with "URL scheme 'file' is not supported". For an asset bundled
981
+ * in the APK, or a bundle inlined at build time, use
982
+ * `bootstrapClientIframeWithHtml(htmlString)` instead.
947
983
  *
948
- * **Devo ser chamado uma vez no boot do shell**, antes de qualquer
949
- * outra interação. Após retornar, `verifySandbox()` deve ser invocado
950
- * para atestation (Camada 2).
984
+ * **Call it once at shell boot**, before any other interaction. After it
985
+ * returns, invoke `verifySandbox()` for attestation (Layer 2).
951
986
  */
952
987
  async bootstrapClientIframe(clientPath = "client.html") {
953
988
  const res = await fetch(clientPath);
954
- if (!res.ok) throw new Error(`BridgeShell: falha ao carregar ${clientPath} (${res.status})`);
989
+ if (!res.ok) throw new Error(`BridgeShell: failed to load ${clientPath} (${res.status})`);
955
990
  const clientHtml = await res.text();
956
991
  await this.bootstrapClientIframeWithHtml(clientHtml);
957
992
  }
958
993
  /**
959
- * **Camada 1 do Risco 1.** Bootstrap a partir de HTML string.
994
+ * **Layer 1.** Bootstraps from an HTML string.
960
995
  *
961
- * Aceita HTML do cliente diretamente (já lido de arquivo, embedado em
962
- * build-time via `esbuild.define`, lido de IndexedDB, etc.) e seta
963
- * como `srcdoc` do iframe prefixado pelo override script. Override
964
- * roda em parse-time, antes de qualquer outro script do cliente.
996
+ * Takes the client HTML directly — already read from a file, embedded at
997
+ * build time, read from storage — and sets it as the iframe's `srcdoc`,
998
+ * prefixed by the override script, which runs at parse time before any
999
+ * client script.
965
1000
  *
966
- * Use quando `fetch` não é aplicável (asset bundled em `file://`,
967
- * bundle inlined no build, qualquer cenário sem rede). Para HTTP
968
- * use `bootstrapClientIframe(clientPath)`.
1001
+ * Use this when `fetch` does not apply: an asset bundled under `file://`, a
1002
+ * bundle inlined at build time, any scenario without network. Android
1003
+ * WebView blocks `fetch` on the `file://` scheme, so this is the path the
1004
+ * app actually takes. For HTTP, use `bootstrapClientIframe(clientPath)`.
969
1005
  *
970
- * **Devo ser chamado uma vez no boot do shell**, antes de qualquer
971
- * outra interação. Após retornar, `verifySandbox()` deve ser invocado
972
- * para atestation (Camada 2).
1006
+ * **Call it once at shell boot**, before any other interaction. After it
1007
+ * returns, invoke `verifySandbox()` for attestation (Layer 2).
973
1008
  */
974
1009
  async bootstrapClientIframeWithHtml(clientHtml) {
975
1010
  this.iframe.srcdoc = OVERRIDE_PREFIX + clientHtml;
@@ -985,31 +1020,31 @@ var BridgeShell = class {
985
1020
  });
986
1021
  }
987
1022
  /**
988
- * **Camada 2 do Risco 1.** Round-trip `postMessage` que pede ao iframe
989
- * atestation autoritativa do estado de `window.flutter_inappwebview`
990
- * e `window.CondorBridge`. Timeout em 2s.
1023
+ * **Layer 2.** A `postMessage` round trip asking the iframe for an
1024
+ * authoritative attestation of whether the native globals are reachable.
1025
+ * Times out after 2 s.
991
1026
  *
992
- * `ok` é `true` apenas se ambos forem `undefined` no iframe. Se vier
993
- * `false`, **shell deve abortar o boot do app** — Camada 1 falhou e
994
- * Camada 4 (dual WebView) precisa ser acionada manualmente.
1027
+ * `ok` is `true` only when none of them is reachable. If it comes back
1028
+ * `false`, **the shell must abort the app boot**: Layer 1 failed, and client
1029
+ * code would run with a path to the native layer.
995
1030
  */
996
1031
  verifySandbox() {
997
1032
  if (!this.iframe.contentWindow) {
998
- return Promise.reject(new Error("BridgeShell: iframe sem contentWindow"));
1033
+ return Promise.reject(new Error("BridgeShell: iframe has no contentWindow"));
999
1034
  }
1000
1035
  return new Promise((resolve, reject) => {
1001
1036
  const nonce = Math.random().toString(36).slice(2);
1002
1037
  const timer = setTimeout(() => {
1003
1038
  this.pendingVerifies.delete(nonce);
1004
- reject(new Error(`BridgeShell: verify-sandbox timeout ap\xF3s ${VERIFY_TIMEOUT_MS}ms`));
1039
+ reject(new Error(`BridgeShell: verify-sandbox timed out after ${VERIFY_TIMEOUT_MS}ms`));
1005
1040
  }, VERIFY_TIMEOUT_MS);
1006
1041
  this.pendingVerifies.set(nonce, { resolve, reject, timer });
1007
1042
  this.postToClient({ type: "verify-sandbox" });
1008
1043
  });
1009
1044
  }
1010
1045
  /**
1011
- * Encerra o shell — remove listener `message`, cancela subscriptions
1012
- * pendentes no `Bridge` nativo, limpa estado interno.
1046
+ * Tears the shell down: removes the `message` listener, cancels pending
1047
+ * subscriptions on the native `Bridge`, and clears internal state.
1013
1048
  */
1014
1049
  detach() {
1015
1050
  if (this.detached) return;
@@ -1075,13 +1110,13 @@ var BridgeShell = class {
1075
1110
  Bridge.send(command, args);
1076
1111
  }
1077
1112
  /**
1078
- * Obtém o `nativeVersion`, disparando o handshake com o nativo na primeira
1079
- * vez e cacheando o resultado.
1113
+ * Resolves `nativeVersion`, firing the native handshake the first time and
1114
+ * caching the result.
1080
1115
  *
1081
- * ⚠️ `Bridge.handshake` **não tem timeout**: a Promise só resolve quando o
1082
- * nativo emite `handshake-ack`. Num browser sem nativo ela nunca resolve,
1083
- * então o `race` abaixo é o que garante que o cliente sempre receba
1084
- * resposta — com `null` — em vez de ficar pendurado.
1116
+ * ⚠️ `Bridge.handshake` has **no timeout**: its promise only settles when the
1117
+ * native side emits `handshake-ack`. In a browser with no native host it
1118
+ * never settles, so the race below is what guarantees the client always gets
1119
+ * an answer — `null` — instead of hanging.
1085
1120
  */
1086
1121
  async resolveNativeVersion() {
1087
1122
  if (this.nativeVersion !== null) return this.nativeVersion;
@@ -1106,6 +1141,7 @@ export {
1106
1141
  BRIDGE_KEYS,
1107
1142
  Bridge,
1108
1143
  BridgeClient,
1109
- BridgeShell
1144
+ BridgeShell,
1145
+ NATIVE_CHANNEL_NAME
1110
1146
  };
1111
1147
  //# sourceMappingURL=index.js.map