@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/AGENTS.md +120 -0
- package/CHANGELOG.md +93 -0
- package/README.md +75 -16
- package/contract/bridge-contract.json +198 -0
- package/dist/cli.cjs +230 -17
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +231 -18
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +107 -70
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +476 -373
- package/dist/index.d.ts +476 -373
- package/dist/index.js +105 -69
- package/dist/index.js.map +1 -1
- package/docs/agent-guide.md +209 -0
- package/docs/bridge-contract.md +249 -0
- package/docs/client-guide.md +387 -0
- package/docs/limits.md +111 -0
- package/docs/pilot-onboarding.md +103 -0
- package/docs/troubleshooting.md +150 -0
- package/examples/minimal-home/AGENTS.md +28 -0
- package/examples/minimal-home/README.md +42 -0
- package/examples/minimal-home/build.mjs +31 -0
- package/examples/minimal-home/index.html +77 -0
- package/examples/minimal-home/package.json +23 -0
- package/examples/minimal-home/src/main.ts +161 -0
- package/examples/minimal-home/tsconfig.json +14 -0
- package/llms.txt +44 -0
- package/package.json +15 -2
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:
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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`
|
|
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
|
|
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
|
-
*
|
|
862
|
+
* Schema version declared by the **native** side, relayed by the shell.
|
|
836
863
|
*
|
|
837
|
-
* `null`
|
|
838
|
-
* mock
|
|
839
|
-
*
|
|
840
|
-
*
|
|
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
|
|
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:
|
|
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
|
-
//
|
|
886
|
-
//
|
|
887
|
-
//
|
|
888
|
-
//
|
|
889
|
-
// false;
|
|
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
|
-
*
|
|
904
|
-
*
|
|
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
|
-
/**
|
|
942
|
+
/** In-flight native handshake; guarantees a single call. */
|
|
908
943
|
this.nativeHandshake = null;
|
|
909
|
-
//
|
|
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(
|
|
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
|
|
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
|
-
* **
|
|
1014
|
+
* **Layer 1.** Bootstraps by fetching a URL.
|
|
978
1015
|
*
|
|
979
|
-
*
|
|
980
|
-
*
|
|
1016
|
+
* A shortcut for the common HTTP/HTTPS case: it runs `fetch(clientPath)` and
|
|
1017
|
+
* delegates to `bootstrapClientIframeWithHtml`.
|
|
981
1018
|
*
|
|
982
|
-
* **
|
|
983
|
-
*
|
|
984
|
-
*
|
|
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
|
-
* **
|
|
988
|
-
*
|
|
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:
|
|
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
|
-
* **
|
|
1034
|
+
* **Layer 1.** Bootstraps from an HTML string.
|
|
999
1035
|
*
|
|
1000
|
-
*
|
|
1001
|
-
* build
|
|
1002
|
-
*
|
|
1003
|
-
*
|
|
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
|
|
1006
|
-
* bundle inlined
|
|
1007
|
-
*
|
|
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
|
-
* **
|
|
1010
|
-
*
|
|
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
|
-
* **
|
|
1028
|
-
*
|
|
1029
|
-
*
|
|
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`
|
|
1032
|
-
* `false`, **shell
|
|
1033
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
1051
|
-
*
|
|
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
|
-
*
|
|
1118
|
-
*
|
|
1153
|
+
* Resolves `nativeVersion`, firing the native handshake the first time and
|
|
1154
|
+
* caching the result.
|
|
1119
1155
|
*
|
|
1120
|
-
* ⚠️ `Bridge.handshake` **
|
|
1121
|
-
*
|
|
1122
|
-
*
|
|
1123
|
-
*
|
|
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
|