@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.js
CHANGED
|
@@ -12,10 +12,10 @@ var BRIDGE_KEYS = {
|
|
|
12
12
|
dfu: "dfu",
|
|
13
13
|
systemStatus: "systemStatus",
|
|
14
14
|
/**
|
|
15
|
-
* Native → client:
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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`
|
|
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
|
|
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
|
-
*
|
|
822
|
+
* Schema version declared by the **native** side, relayed by the shell.
|
|
797
823
|
*
|
|
798
|
-
* `null`
|
|
799
|
-
* mock
|
|
800
|
-
*
|
|
801
|
-
*
|
|
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
|
|
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:
|
|
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
|
-
//
|
|
847
|
-
//
|
|
848
|
-
//
|
|
849
|
-
//
|
|
850
|
-
// false;
|
|
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
|
-
*
|
|
865
|
-
*
|
|
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
|
-
/**
|
|
902
|
+
/** In-flight native handshake; guarantees a single call. */
|
|
869
903
|
this.nativeHandshake = null;
|
|
870
|
-
//
|
|
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(
|
|
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
|
|
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
|
-
* **
|
|
974
|
+
* **Layer 1.** Bootstraps by fetching a URL.
|
|
939
975
|
*
|
|
940
|
-
*
|
|
941
|
-
*
|
|
976
|
+
* A shortcut for the common HTTP/HTTPS case: it runs `fetch(clientPath)` and
|
|
977
|
+
* delegates to `bootstrapClientIframeWithHtml`.
|
|
942
978
|
*
|
|
943
|
-
* **
|
|
944
|
-
*
|
|
945
|
-
*
|
|
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
|
-
* **
|
|
949
|
-
*
|
|
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:
|
|
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
|
-
* **
|
|
994
|
+
* **Layer 1.** Bootstraps from an HTML string.
|
|
960
995
|
*
|
|
961
|
-
*
|
|
962
|
-
* build
|
|
963
|
-
*
|
|
964
|
-
*
|
|
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
|
|
967
|
-
* bundle inlined
|
|
968
|
-
*
|
|
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
|
-
* **
|
|
971
|
-
*
|
|
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
|
-
* **
|
|
989
|
-
*
|
|
990
|
-
*
|
|
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`
|
|
993
|
-
* `false`, **shell
|
|
994
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
1012
|
-
*
|
|
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
|
-
*
|
|
1079
|
-
*
|
|
1113
|
+
* Resolves `nativeVersion`, firing the native handshake the first time and
|
|
1114
|
+
* caching the result.
|
|
1080
1115
|
*
|
|
1081
|
-
* ⚠️ `Bridge.handshake` **
|
|
1082
|
-
*
|
|
1083
|
-
*
|
|
1084
|
-
*
|
|
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
|