@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.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
 
@@ -51,10 +52,10 @@ var BRIDGE_KEYS = {
51
52
  dfu: "dfu",
52
53
  systemStatus: "systemStatus",
53
54
  /**
54
- * Native → client: o back do SO foi pressionado e o cliente reportou
55
- * ter histórico interno (`reportNavigation(true)`). O cliente deve voltar
56
- * UMA página interna. Emitido só quando o nativo decidiu delegar o back
57
- * ao cliente. Vide `BridgeClient.onBack`.
55
+ * Native → client: the OS back button was pressed and the client had
56
+ * reported internal history via `reportNavigation(true)`. Go back exactly
57
+ * ONE internal page. Emitted only when the native side decided to delegate
58
+ * the back gesture. See `BridgeClient.onBack`.
58
59
  */
59
60
  navigationBack: "navigationBack",
60
61
  /**
@@ -62,7 +63,7 @@ var BRIDGE_KEYS = {
62
63
  * AND the language explicitly chosen in the app Settings, so the client can
63
64
  * apply its own precedence rule — including rendering a language the native
64
65
  * chrome does not support. Emitted on attach, after `handshake-ack`, and on
65
- * every change in Settings. Vide `LanguagePayload`.
66
+ * every change in Settings. See `LanguagePayload`.
66
67
  */
67
68
  language: "language"
68
69
  };
@@ -469,8 +470,23 @@ 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
+ // Single channel: `handlerName` does not travel — the native side routes
483
+ // on `type`.
484
+ callHandler(_handlerName, payload) {
485
+ channel.postMessage(JSON.stringify(payload));
486
+ }
487
+ };
488
+ }
489
+ function createLegacyAdapter(host) {
474
490
  return {
475
491
  isMock: false,
476
492
  callHandler(handlerName, payload) {
@@ -482,9 +498,13 @@ function detectAdapter() {
482
498
  if (typeof window === "undefined") {
483
499
  return createMockAdapter();
484
500
  }
501
+ const channel = window.CondorNative;
502
+ if (channel && typeof channel.postMessage === "function") {
503
+ return createChannelAdapter(channel);
504
+ }
485
505
  const host = window.flutter_inappwebview;
486
506
  if (host && typeof host.callHandler === "function") {
487
- return createRealAdapter(host);
507
+ return createLegacyAdapter(host);
488
508
  }
489
509
  return createMockAdapter();
490
510
  }
@@ -495,7 +515,8 @@ function getActiveAdapter() {
495
515
  return activeAdapter;
496
516
  }
497
517
  function callNative(handlerName, payload) {
498
- getActiveAdapter().callHandler(handlerName, payload);
518
+ const stamped = outgoingStamp ? outgoingStamp(payload) : payload;
519
+ getActiveAdapter().callHandler(handlerName, stamped);
499
520
  }
500
521
 
501
522
  // src/internal/subscribers.ts
@@ -611,6 +632,9 @@ var Bridge = {
611
632
  openSettings() {
612
633
  this.send("openSettings");
613
634
  },
635
+ setOutgoingStamp(stamp) {
636
+ setOutgoingStamp(stamp);
637
+ },
614
638
  mockEmit(key, payload) {
615
639
  requireMockAdapter().triggerEmit(key, payload ?? {});
616
640
  },
@@ -671,7 +695,7 @@ function onMessage(event) {
671
695
  pendingHandshake2 = null;
672
696
  resolve({
673
697
  shellVersion: msg.ack?.shellVersion ?? "unknown",
674
- // `?? null` cobre o shell anterior à 0.17.0, que não mandava o campo.
698
+ // `?? null` covers shells older than 0.17.0, which omitted the field.
675
699
  nativeVersion: msg.ack?.nativeVersion ?? null,
676
700
  accepted: msg.ack?.accepted ?? false
677
701
  });
@@ -686,7 +710,8 @@ function onMessage(event) {
686
710
  postToShell({
687
711
  type: "sandbox-verify-response",
688
712
  flutterBridgeReachable: typeof w.flutter_inappwebview !== "undefined",
689
- condorBridgeReachable: typeof w.CondorBridge !== "undefined"
713
+ condorBridgeReachable: typeof w.CondorBridge !== "undefined",
714
+ nativeChannelReachable: typeof w.CondorNative !== "undefined"
690
715
  });
691
716
  break;
692
717
  }
@@ -812,9 +837,11 @@ var JsErrorMessage = v.object({
812
837
  var SandboxVerifyResponseMessage = v.object({
813
838
  type: v.literal("sandbox-verify-response"),
814
839
  flutterBridgeReachable: v.boolean(),
815
- condorBridgeReachable: v.boolean()
840
+ condorBridgeReachable: v.boolean(),
841
+ // Optional: clients on SDK < 0.18.0 do not send it.
842
+ nativeChannelReachable: v.optional(v.boolean())
816
843
  });
817
- var ClientToShellMessage = v.variant("type", [
844
+ var ClientToShellMessageSchema = v.variant("type", [
818
845
  SubscribeMessage,
819
846
  UnsubscribeMessage,
820
847
  SendMessage,
@@ -832,12 +859,12 @@ var HandshakeAckMessage = v.object({
832
859
  ack: v.object({
833
860
  shellVersion: v.string(),
834
861
  /**
835
- * Versão do schema declarada pelo **nativo**, repassada pelo shell.
862
+ * Schema version declared by the **native** side, relayed by the shell.
836
863
  *
837
- * `null` quando o nativo ainda não respondeu (ou não existe, como no
838
- * mock de browser). O cliente roda dentro de um iframe sandboxed e não
839
- * fala com o nativo diretamente, então este é o **único** caminho pelo
840
- * qual ele descobre de qual versão são as capacidades disponíveis.
864
+ * `null` when the native side has not answered, or does not exist (browser
865
+ * mock mode). The client runs inside a sandboxed iframe and never talks to
866
+ * the native side directly, so this is the **only** way it can learn which
867
+ * capabilities are available.
841
868
  */
842
869
  nativeVersion: v.nullable(v.string()),
843
870
  accepted: v.boolean()
@@ -852,7 +879,7 @@ var NackMessage = v.object({
852
879
  var VerifySandboxMessage = v.object({
853
880
  type: v.literal("verify-sandbox")
854
881
  });
855
- var ShellToClientMessage = v.variant("type", [
882
+ var ShellToClientMessageSchema = v.variant("type", [
856
883
  EventMessage,
857
884
  HandshakeAckMessage,
858
885
  NackMessage,
@@ -867,10 +894,18 @@ var OVERRIDE_PREFIX = `<script>
867
894
  (function(){
868
895
  try { delete window.flutter_inappwebview; } catch (_) {}
869
896
  try { delete window.CondorBridge; } catch (_) {}
897
+ try { delete window.CondorNative; } catch (_) {}
870
898
  try {
871
899
  Object.defineProperty(window, 'flutter_inappwebview', {
872
900
  get: function(){ return undefined; },
873
- set: function(_){ /* no-op: bloqueia inje\xE7\xE3o do plugin no iframe */ },
901
+ set: function(_){ /* no-op: blocks the plugin injecting into the iframe */ },
902
+ configurable: false,
903
+ });
904
+ } catch (_) {}
905
+ try {
906
+ Object.defineProperty(window, 'CondorNative', {
907
+ get: function(){ return undefined; },
908
+ set: function(_){ /* no-op: bloqueia o canal do webview_flutter no iframe */ },
874
909
  configurable: false,
875
910
  });
876
911
  } catch (_) {}
@@ -882,11 +917,11 @@ var OVERRIDE_PREFIX = `<script>
882
917
  });
883
918
  } catch (_) {}
884
919
  try {
885
- // Neutraliza navigator.sendBeacon: canal de exfil que a CSP connect-src
886
- // n\xE3o barra de forma confi\xE1vel em alguns WebViews (pentest 2026-06-24,
887
- // PT-10). O cliente comunica s\xF3 via BridgeClient/postMessage \u2014 n\xE3o tem
888
- // uso leg\xEDtimo de sendBeacon. Getter no-op retorna fun\xE7\xE3o que devolve
889
- // false; setter no-op ignora tentativa de restaurar.
920
+ // Neutralize navigator.sendBeacon: an exfiltration channel that CSP
921
+ // connect-src does not reliably block in some WebViews (pen test PT-10).
922
+ // The client communicates only through BridgeClient/postMessage, so it has
923
+ // no legitimate use for sendBeacon. The no-op getter returns a function
924
+ // that answers false; the no-op setter ignores attempts to restore it.
890
925
  Object.defineProperty(navigator, 'sendBeacon', {
891
926
  get: function(){ return function(){ return false; }; },
892
927
  set: function(_){ /* no-op */ },
@@ -900,18 +935,18 @@ var BridgeShell = class {
900
935
  this.subscriptions = /* @__PURE__ */ new Map();
901
936
  this.detached = false;
902
937
  /**
903
- * Versão do nativo, obtida uma vez e cacheada. `null` até o nativo
904
- * responder — e permanentemente `null` num browser sem nativo.
938
+ * The native version, fetched once and cached. `null` until the native side
939
+ * answers, and permanently `null` in a browser with no native host.
905
940
  */
906
941
  this.nativeVersion = null;
907
- /** Handshake com o nativo em voo; garante uma única chamada. */
942
+ /** In-flight native handshake; guarantees a single call. */
908
943
  this.nativeHandshake = null;
909
- // Map de `verify-sandbox` round-trips pendentes — key gerada por nonce.
944
+ // Pending `verify-sandbox` round trips, keyed by a generated nonce.
910
945
  this.pendingVerifies = /* @__PURE__ */ new Map();
911
946
  this.onMessage = async (event) => {
912
947
  if (this.detached) return;
913
948
  if (event.source !== this.iframe.contentWindow) return;
914
- const parsed = v2.safeParse(ClientToShellMessage, event.data);
949
+ const parsed = v2.safeParse(ClientToShellMessageSchema, event.data);
915
950
  if (!parsed.success) {
916
951
  if (typeof console !== "undefined") {
917
952
  console.debug("[BridgeShell] dropped malformed message:", parsed.issues?.[0]?.message);
@@ -954,10 +989,12 @@ var BridgeShell = class {
954
989
  if (pending) {
955
990
  clearTimeout(pending.timer);
956
991
  this.pendingVerifies.delete(first);
957
- const ok = !msg.flutterBridgeReachable && !msg.condorBridgeReachable;
992
+ const nativeChannelReachable = msg.nativeChannelReachable ?? false;
993
+ const ok = !msg.flutterBridgeReachable && !msg.condorBridgeReachable && !nativeChannelReachable;
958
994
  pending.resolve({
959
995
  flutterBridgeReachable: msg.flutterBridgeReachable,
960
996
  condorBridgeReachable: msg.condorBridgeReachable,
997
+ nativeChannelReachable,
961
998
  ok
962
999
  });
963
1000
  }
@@ -974,41 +1011,40 @@ var BridgeShell = class {
974
1011
  }
975
1012
  }
976
1013
  /**
977
- * **Camada 1 do Risco 1.** Bootstrap via fetch de URL.
1014
+ * **Layer 1.** Bootstraps by fetching a URL.
978
1015
  *
979
- * Atalho para o caso comum HTTP/HTTPS: faz `fetch(clientPath)` e
980
- * delega para `bootstrapClientIframeWithHtml`.
1016
+ * A shortcut for the common HTTP/HTTPS case: it runs `fetch(clientPath)` and
1017
+ * delegates to `bootstrapClientIframeWithHtml`.
981
1018
  *
982
- * **Não funciona em scheme `file://`** — `fetch` em WebView Android
983
- * (Chromium) bloqueia o scheme com "URL scheme 'file' is not supported".
984
- * Para asset bundled em APK ou bundle inlined no build-time, use
985
- * `bootstrapClientIframeWithHtml(htmlString)`.
1019
+ * **It does not work on the `file://` scheme** — Android's Chromium WebView
1020
+ * blocks it with "URL scheme 'file' is not supported". For an asset bundled
1021
+ * in the APK, or a bundle inlined at build time, use
1022
+ * `bootstrapClientIframeWithHtml(htmlString)` instead.
986
1023
  *
987
- * **Devo ser chamado uma vez no boot do shell**, antes de qualquer
988
- * outra interação. Após retornar, `verifySandbox()` deve ser invocado
989
- * para atestation (Camada 2).
1024
+ * **Call it once at shell boot**, before any other interaction. After it
1025
+ * returns, invoke `verifySandbox()` for attestation (Layer 2).
990
1026
  */
991
1027
  async bootstrapClientIframe(clientPath = "client.html") {
992
1028
  const res = await fetch(clientPath);
993
- if (!res.ok) throw new Error(`BridgeShell: falha ao carregar ${clientPath} (${res.status})`);
1029
+ if (!res.ok) throw new Error(`BridgeShell: failed to load ${clientPath} (${res.status})`);
994
1030
  const clientHtml = await res.text();
995
1031
  await this.bootstrapClientIframeWithHtml(clientHtml);
996
1032
  }
997
1033
  /**
998
- * **Camada 1 do Risco 1.** Bootstrap a partir de HTML string.
1034
+ * **Layer 1.** Bootstraps from an HTML string.
999
1035
  *
1000
- * Aceita HTML do cliente diretamente (já lido de arquivo, embedado em
1001
- * build-time via `esbuild.define`, lido de IndexedDB, etc.) e seta
1002
- * como `srcdoc` do iframe prefixado pelo override script. Override
1003
- * roda em parse-time, antes de qualquer outro script do cliente.
1036
+ * Takes the client HTML directly — already read from a file, embedded at
1037
+ * build time, read from storage — and sets it as the iframe's `srcdoc`,
1038
+ * prefixed by the override script, which runs at parse time before any
1039
+ * client script.
1004
1040
  *
1005
- * Use quando `fetch` não é aplicável (asset bundled em `file://`,
1006
- * bundle inlined no build, qualquer cenário sem rede). Para HTTP
1007
- * use `bootstrapClientIframe(clientPath)`.
1041
+ * Use this when `fetch` does not apply: an asset bundled under `file://`, a
1042
+ * bundle inlined at build time, any scenario without network. Android
1043
+ * WebView blocks `fetch` on the `file://` scheme, so this is the path the
1044
+ * app actually takes. For HTTP, use `bootstrapClientIframe(clientPath)`.
1008
1045
  *
1009
- * **Devo ser chamado uma vez no boot do shell**, antes de qualquer
1010
- * outra interação. Após retornar, `verifySandbox()` deve ser invocado
1011
- * para atestation (Camada 2).
1046
+ * **Call it once at shell boot**, before any other interaction. After it
1047
+ * returns, invoke `verifySandbox()` for attestation (Layer 2).
1012
1048
  */
1013
1049
  async bootstrapClientIframeWithHtml(clientHtml) {
1014
1050
  this.iframe.srcdoc = OVERRIDE_PREFIX + clientHtml;
@@ -1024,31 +1060,31 @@ var BridgeShell = class {
1024
1060
  });
1025
1061
  }
1026
1062
  /**
1027
- * **Camada 2 do Risco 1.** Round-trip `postMessage` que pede ao iframe
1028
- * atestation autoritativa do estado de `window.flutter_inappwebview`
1029
- * e `window.CondorBridge`. Timeout em 2s.
1063
+ * **Layer 2.** A `postMessage` round trip asking the iframe for an
1064
+ * authoritative attestation of whether the native globals are reachable.
1065
+ * Times out after 2 s.
1030
1066
  *
1031
- * `ok` é `true` apenas se ambos forem `undefined` no iframe. Se vier
1032
- * `false`, **shell deve abortar o boot do app** — Camada 1 falhou e
1033
- * Camada 4 (dual WebView) precisa ser acionada manualmente.
1067
+ * `ok` is `true` only when none of them is reachable. If it comes back
1068
+ * `false`, **the shell must abort the app boot**: Layer 1 failed, and client
1069
+ * code would run with a path to the native layer.
1034
1070
  */
1035
1071
  verifySandbox() {
1036
1072
  if (!this.iframe.contentWindow) {
1037
- return Promise.reject(new Error("BridgeShell: iframe sem contentWindow"));
1073
+ return Promise.reject(new Error("BridgeShell: iframe has no contentWindow"));
1038
1074
  }
1039
1075
  return new Promise((resolve, reject) => {
1040
1076
  const nonce = Math.random().toString(36).slice(2);
1041
1077
  const timer = setTimeout(() => {
1042
1078
  this.pendingVerifies.delete(nonce);
1043
- reject(new Error(`BridgeShell: verify-sandbox timeout ap\xF3s ${VERIFY_TIMEOUT_MS}ms`));
1079
+ reject(new Error(`BridgeShell: verify-sandbox timed out after ${VERIFY_TIMEOUT_MS}ms`));
1044
1080
  }, VERIFY_TIMEOUT_MS);
1045
1081
  this.pendingVerifies.set(nonce, { resolve, reject, timer });
1046
1082
  this.postToClient({ type: "verify-sandbox" });
1047
1083
  });
1048
1084
  }
1049
1085
  /**
1050
- * Encerra o shell — remove listener `message`, cancela subscriptions
1051
- * pendentes no `Bridge` nativo, limpa estado interno.
1086
+ * Tears the shell down: removes the `message` listener, cancels pending
1087
+ * subscriptions on the native `Bridge`, and clears internal state.
1052
1088
  */
1053
1089
  detach() {
1054
1090
  if (this.detached) return;
@@ -1114,13 +1150,13 @@ var BridgeShell = class {
1114
1150
  Bridge.send(command, args);
1115
1151
  }
1116
1152
  /**
1117
- * Obtém o `nativeVersion`, disparando o handshake com o nativo na primeira
1118
- * vez e cacheando o resultado.
1153
+ * Resolves `nativeVersion`, firing the native handshake the first time and
1154
+ * caching the result.
1119
1155
  *
1120
- * ⚠️ `Bridge.handshake` **não tem timeout**: a Promise só resolve quando o
1121
- * nativo emite `handshake-ack`. Num browser sem nativo ela nunca resolve,
1122
- * então o `race` abaixo é o que garante que o cliente sempre receba
1123
- * resposta — com `null` — em vez de ficar pendurado.
1156
+ * ⚠️ `Bridge.handshake` has **no timeout**: its promise only settles when the
1157
+ * native side emits `handshake-ack`. In a browser with no native host it
1158
+ * never settles, so the race below is what guarantees the client always gets
1159
+ * an answer — `null` — instead of hanging.
1124
1160
  */
1125
1161
  async resolveNativeVersion() {
1126
1162
  if (this.nativeVersion !== null) return this.nativeVersion;
@@ -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