@condorinst/mobile-bridge 0.18.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 +57 -0
- package/README.md +66 -11
- 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 +64 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +442 -396
- package/dist/index.d.ts +442 -396
- package/dist/index.js +64 -64
- 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
|
};
|
|
@@ -439,7 +439,8 @@ var activeAdapter = null;
|
|
|
439
439
|
function createChannelAdapter(channel) {
|
|
440
440
|
return {
|
|
441
441
|
isMock: false,
|
|
442
|
-
//
|
|
442
|
+
// Single channel: `handlerName` does not travel — the native side routes
|
|
443
|
+
// on `type`.
|
|
443
444
|
callHandler(_handlerName, payload) {
|
|
444
445
|
channel.postMessage(JSON.stringify(payload));
|
|
445
446
|
}
|
|
@@ -654,7 +655,7 @@ function onMessage(event) {
|
|
|
654
655
|
pendingHandshake2 = null;
|
|
655
656
|
resolve({
|
|
656
657
|
shellVersion: msg.ack?.shellVersion ?? "unknown",
|
|
657
|
-
// `?? null`
|
|
658
|
+
// `?? null` covers shells older than 0.17.0, which omitted the field.
|
|
658
659
|
nativeVersion: msg.ack?.nativeVersion ?? null,
|
|
659
660
|
accepted: msg.ack?.accepted ?? false
|
|
660
661
|
});
|
|
@@ -797,10 +798,10 @@ var SandboxVerifyResponseMessage = v.object({
|
|
|
797
798
|
type: v.literal("sandbox-verify-response"),
|
|
798
799
|
flutterBridgeReachable: v.boolean(),
|
|
799
800
|
condorBridgeReachable: v.boolean(),
|
|
800
|
-
//
|
|
801
|
+
// Optional: clients on SDK < 0.18.0 do not send it.
|
|
801
802
|
nativeChannelReachable: v.optional(v.boolean())
|
|
802
803
|
});
|
|
803
|
-
var
|
|
804
|
+
var ClientToShellMessageSchema = v.variant("type", [
|
|
804
805
|
SubscribeMessage,
|
|
805
806
|
UnsubscribeMessage,
|
|
806
807
|
SendMessage,
|
|
@@ -818,12 +819,12 @@ var HandshakeAckMessage = v.object({
|
|
|
818
819
|
ack: v.object({
|
|
819
820
|
shellVersion: v.string(),
|
|
820
821
|
/**
|
|
821
|
-
*
|
|
822
|
+
* Schema version declared by the **native** side, relayed by the shell.
|
|
822
823
|
*
|
|
823
|
-
* `null`
|
|
824
|
-
* mock
|
|
825
|
-
*
|
|
826
|
-
*
|
|
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.
|
|
827
828
|
*/
|
|
828
829
|
nativeVersion: v.nullable(v.string()),
|
|
829
830
|
accepted: v.boolean()
|
|
@@ -838,7 +839,7 @@ var NackMessage = v.object({
|
|
|
838
839
|
var VerifySandboxMessage = v.object({
|
|
839
840
|
type: v.literal("verify-sandbox")
|
|
840
841
|
});
|
|
841
|
-
var
|
|
842
|
+
var ShellToClientMessageSchema = v.variant("type", [
|
|
842
843
|
EventMessage,
|
|
843
844
|
HandshakeAckMessage,
|
|
844
845
|
NackMessage,
|
|
@@ -857,7 +858,7 @@ var OVERRIDE_PREFIX = `<script>
|
|
|
857
858
|
try {
|
|
858
859
|
Object.defineProperty(window, 'flutter_inappwebview', {
|
|
859
860
|
get: function(){ return undefined; },
|
|
860
|
-
set: function(_){ /* no-op:
|
|
861
|
+
set: function(_){ /* no-op: blocks the plugin injecting into the iframe */ },
|
|
861
862
|
configurable: false,
|
|
862
863
|
});
|
|
863
864
|
} catch (_) {}
|
|
@@ -876,11 +877,11 @@ var OVERRIDE_PREFIX = `<script>
|
|
|
876
877
|
});
|
|
877
878
|
} catch (_) {}
|
|
878
879
|
try {
|
|
879
|
-
//
|
|
880
|
-
//
|
|
881
|
-
//
|
|
882
|
-
//
|
|
883
|
-
// 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.
|
|
884
885
|
Object.defineProperty(navigator, 'sendBeacon', {
|
|
885
886
|
get: function(){ return function(){ return false; }; },
|
|
886
887
|
set: function(_){ /* no-op */ },
|
|
@@ -894,18 +895,18 @@ var BridgeShell = class {
|
|
|
894
895
|
this.subscriptions = /* @__PURE__ */ new Map();
|
|
895
896
|
this.detached = false;
|
|
896
897
|
/**
|
|
897
|
-
*
|
|
898
|
-
*
|
|
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.
|
|
899
900
|
*/
|
|
900
901
|
this.nativeVersion = null;
|
|
901
|
-
/**
|
|
902
|
+
/** In-flight native handshake; guarantees a single call. */
|
|
902
903
|
this.nativeHandshake = null;
|
|
903
|
-
//
|
|
904
|
+
// Pending `verify-sandbox` round trips, keyed by a generated nonce.
|
|
904
905
|
this.pendingVerifies = /* @__PURE__ */ new Map();
|
|
905
906
|
this.onMessage = async (event) => {
|
|
906
907
|
if (this.detached) return;
|
|
907
908
|
if (event.source !== this.iframe.contentWindow) return;
|
|
908
|
-
const parsed = v2.safeParse(
|
|
909
|
+
const parsed = v2.safeParse(ClientToShellMessageSchema, event.data);
|
|
909
910
|
if (!parsed.success) {
|
|
910
911
|
if (typeof console !== "undefined") {
|
|
911
912
|
console.debug("[BridgeShell] dropped malformed message:", parsed.issues?.[0]?.message);
|
|
@@ -970,41 +971,40 @@ var BridgeShell = class {
|
|
|
970
971
|
}
|
|
971
972
|
}
|
|
972
973
|
/**
|
|
973
|
-
* **
|
|
974
|
+
* **Layer 1.** Bootstraps by fetching a URL.
|
|
974
975
|
*
|
|
975
|
-
*
|
|
976
|
-
*
|
|
976
|
+
* A shortcut for the common HTTP/HTTPS case: it runs `fetch(clientPath)` and
|
|
977
|
+
* delegates to `bootstrapClientIframeWithHtml`.
|
|
977
978
|
*
|
|
978
|
-
* **
|
|
979
|
-
*
|
|
980
|
-
*
|
|
981
|
-
* `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.
|
|
982
983
|
*
|
|
983
|
-
* **
|
|
984
|
-
*
|
|
985
|
-
* 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).
|
|
986
986
|
*/
|
|
987
987
|
async bootstrapClientIframe(clientPath = "client.html") {
|
|
988
988
|
const res = await fetch(clientPath);
|
|
989
|
-
if (!res.ok) throw new Error(`BridgeShell:
|
|
989
|
+
if (!res.ok) throw new Error(`BridgeShell: failed to load ${clientPath} (${res.status})`);
|
|
990
990
|
const clientHtml = await res.text();
|
|
991
991
|
await this.bootstrapClientIframeWithHtml(clientHtml);
|
|
992
992
|
}
|
|
993
993
|
/**
|
|
994
|
-
* **
|
|
994
|
+
* **Layer 1.** Bootstraps from an HTML string.
|
|
995
995
|
*
|
|
996
|
-
*
|
|
997
|
-
* build
|
|
998
|
-
*
|
|
999
|
-
*
|
|
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.
|
|
1000
1000
|
*
|
|
1001
|
-
* Use
|
|
1002
|
-
* bundle inlined
|
|
1003
|
-
*
|
|
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)`.
|
|
1004
1005
|
*
|
|
1005
|
-
* **
|
|
1006
|
-
*
|
|
1007
|
-
* 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).
|
|
1008
1008
|
*/
|
|
1009
1009
|
async bootstrapClientIframeWithHtml(clientHtml) {
|
|
1010
1010
|
this.iframe.srcdoc = OVERRIDE_PREFIX + clientHtml;
|
|
@@ -1020,31 +1020,31 @@ var BridgeShell = class {
|
|
|
1020
1020
|
});
|
|
1021
1021
|
}
|
|
1022
1022
|
/**
|
|
1023
|
-
* **
|
|
1024
|
-
*
|
|
1025
|
-
*
|
|
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.
|
|
1026
1026
|
*
|
|
1027
|
-
* `ok`
|
|
1028
|
-
* `false`, **shell
|
|
1029
|
-
*
|
|
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.
|
|
1030
1030
|
*/
|
|
1031
1031
|
verifySandbox() {
|
|
1032
1032
|
if (!this.iframe.contentWindow) {
|
|
1033
|
-
return Promise.reject(new Error("BridgeShell: iframe
|
|
1033
|
+
return Promise.reject(new Error("BridgeShell: iframe has no contentWindow"));
|
|
1034
1034
|
}
|
|
1035
1035
|
return new Promise((resolve, reject) => {
|
|
1036
1036
|
const nonce = Math.random().toString(36).slice(2);
|
|
1037
1037
|
const timer = setTimeout(() => {
|
|
1038
1038
|
this.pendingVerifies.delete(nonce);
|
|
1039
|
-
reject(new Error(`BridgeShell: verify-sandbox
|
|
1039
|
+
reject(new Error(`BridgeShell: verify-sandbox timed out after ${VERIFY_TIMEOUT_MS}ms`));
|
|
1040
1040
|
}, VERIFY_TIMEOUT_MS);
|
|
1041
1041
|
this.pendingVerifies.set(nonce, { resolve, reject, timer });
|
|
1042
1042
|
this.postToClient({ type: "verify-sandbox" });
|
|
1043
1043
|
});
|
|
1044
1044
|
}
|
|
1045
1045
|
/**
|
|
1046
|
-
*
|
|
1047
|
-
*
|
|
1046
|
+
* Tears the shell down: removes the `message` listener, cancels pending
|
|
1047
|
+
* subscriptions on the native `Bridge`, and clears internal state.
|
|
1048
1048
|
*/
|
|
1049
1049
|
detach() {
|
|
1050
1050
|
if (this.detached) return;
|
|
@@ -1110,13 +1110,13 @@ var BridgeShell = class {
|
|
|
1110
1110
|
Bridge.send(command, args);
|
|
1111
1111
|
}
|
|
1112
1112
|
/**
|
|
1113
|
-
*
|
|
1114
|
-
*
|
|
1113
|
+
* Resolves `nativeVersion`, firing the native handshake the first time and
|
|
1114
|
+
* caching the result.
|
|
1115
1115
|
*
|
|
1116
|
-
* ⚠️ `Bridge.handshake` **
|
|
1117
|
-
*
|
|
1118
|
-
*
|
|
1119
|
-
*
|
|
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.
|
|
1120
1120
|
*/
|
|
1121
1121
|
async resolveNativeVersion() {
|
|
1122
1122
|
if (this.nativeVersion !== null) return this.nativeVersion;
|