@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.cjs
CHANGED
|
@@ -52,10 +52,10 @@ var BRIDGE_KEYS = {
|
|
|
52
52
|
dfu: "dfu",
|
|
53
53
|
systemStatus: "systemStatus",
|
|
54
54
|
/**
|
|
55
|
-
* Native → client:
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
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`.
|
|
59
59
|
*/
|
|
60
60
|
navigationBack: "navigationBack",
|
|
61
61
|
/**
|
|
@@ -63,7 +63,7 @@ var BRIDGE_KEYS = {
|
|
|
63
63
|
* AND the language explicitly chosen in the app Settings, so the client can
|
|
64
64
|
* apply its own precedence rule — including rendering a language the native
|
|
65
65
|
* chrome does not support. Emitted on attach, after `handshake-ack`, and on
|
|
66
|
-
* every change in Settings.
|
|
66
|
+
* every change in Settings. See `LanguagePayload`.
|
|
67
67
|
*/
|
|
68
68
|
language: "language"
|
|
69
69
|
};
|
|
@@ -479,7 +479,8 @@ var activeAdapter = null;
|
|
|
479
479
|
function createChannelAdapter(channel) {
|
|
480
480
|
return {
|
|
481
481
|
isMock: false,
|
|
482
|
-
//
|
|
482
|
+
// Single channel: `handlerName` does not travel — the native side routes
|
|
483
|
+
// on `type`.
|
|
483
484
|
callHandler(_handlerName, payload) {
|
|
484
485
|
channel.postMessage(JSON.stringify(payload));
|
|
485
486
|
}
|
|
@@ -694,7 +695,7 @@ function onMessage(event) {
|
|
|
694
695
|
pendingHandshake2 = null;
|
|
695
696
|
resolve({
|
|
696
697
|
shellVersion: msg.ack?.shellVersion ?? "unknown",
|
|
697
|
-
// `?? null`
|
|
698
|
+
// `?? null` covers shells older than 0.17.0, which omitted the field.
|
|
698
699
|
nativeVersion: msg.ack?.nativeVersion ?? null,
|
|
699
700
|
accepted: msg.ack?.accepted ?? false
|
|
700
701
|
});
|
|
@@ -837,10 +838,10 @@ var SandboxVerifyResponseMessage = v.object({
|
|
|
837
838
|
type: v.literal("sandbox-verify-response"),
|
|
838
839
|
flutterBridgeReachable: v.boolean(),
|
|
839
840
|
condorBridgeReachable: v.boolean(),
|
|
840
|
-
//
|
|
841
|
+
// Optional: clients on SDK < 0.18.0 do not send it.
|
|
841
842
|
nativeChannelReachable: v.optional(v.boolean())
|
|
842
843
|
});
|
|
843
|
-
var
|
|
844
|
+
var ClientToShellMessageSchema = v.variant("type", [
|
|
844
845
|
SubscribeMessage,
|
|
845
846
|
UnsubscribeMessage,
|
|
846
847
|
SendMessage,
|
|
@@ -858,12 +859,12 @@ var HandshakeAckMessage = v.object({
|
|
|
858
859
|
ack: v.object({
|
|
859
860
|
shellVersion: v.string(),
|
|
860
861
|
/**
|
|
861
|
-
*
|
|
862
|
+
* Schema version declared by the **native** side, relayed by the shell.
|
|
862
863
|
*
|
|
863
|
-
* `null`
|
|
864
|
-
* mock
|
|
865
|
-
*
|
|
866
|
-
*
|
|
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.
|
|
867
868
|
*/
|
|
868
869
|
nativeVersion: v.nullable(v.string()),
|
|
869
870
|
accepted: v.boolean()
|
|
@@ -878,7 +879,7 @@ var NackMessage = v.object({
|
|
|
878
879
|
var VerifySandboxMessage = v.object({
|
|
879
880
|
type: v.literal("verify-sandbox")
|
|
880
881
|
});
|
|
881
|
-
var
|
|
882
|
+
var ShellToClientMessageSchema = v.variant("type", [
|
|
882
883
|
EventMessage,
|
|
883
884
|
HandshakeAckMessage,
|
|
884
885
|
NackMessage,
|
|
@@ -897,7 +898,7 @@ var OVERRIDE_PREFIX = `<script>
|
|
|
897
898
|
try {
|
|
898
899
|
Object.defineProperty(window, 'flutter_inappwebview', {
|
|
899
900
|
get: function(){ return undefined; },
|
|
900
|
-
set: function(_){ /* no-op:
|
|
901
|
+
set: function(_){ /* no-op: blocks the plugin injecting into the iframe */ },
|
|
901
902
|
configurable: false,
|
|
902
903
|
});
|
|
903
904
|
} catch (_) {}
|
|
@@ -916,11 +917,11 @@ var OVERRIDE_PREFIX = `<script>
|
|
|
916
917
|
});
|
|
917
918
|
} catch (_) {}
|
|
918
919
|
try {
|
|
919
|
-
//
|
|
920
|
-
//
|
|
921
|
-
//
|
|
922
|
-
//
|
|
923
|
-
// 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.
|
|
924
925
|
Object.defineProperty(navigator, 'sendBeacon', {
|
|
925
926
|
get: function(){ return function(){ return false; }; },
|
|
926
927
|
set: function(_){ /* no-op */ },
|
|
@@ -934,18 +935,18 @@ var BridgeShell = class {
|
|
|
934
935
|
this.subscriptions = /* @__PURE__ */ new Map();
|
|
935
936
|
this.detached = false;
|
|
936
937
|
/**
|
|
937
|
-
*
|
|
938
|
-
*
|
|
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.
|
|
939
940
|
*/
|
|
940
941
|
this.nativeVersion = null;
|
|
941
|
-
/**
|
|
942
|
+
/** In-flight native handshake; guarantees a single call. */
|
|
942
943
|
this.nativeHandshake = null;
|
|
943
|
-
//
|
|
944
|
+
// Pending `verify-sandbox` round trips, keyed by a generated nonce.
|
|
944
945
|
this.pendingVerifies = /* @__PURE__ */ new Map();
|
|
945
946
|
this.onMessage = async (event) => {
|
|
946
947
|
if (this.detached) return;
|
|
947
948
|
if (event.source !== this.iframe.contentWindow) return;
|
|
948
|
-
const parsed = v2.safeParse(
|
|
949
|
+
const parsed = v2.safeParse(ClientToShellMessageSchema, event.data);
|
|
949
950
|
if (!parsed.success) {
|
|
950
951
|
if (typeof console !== "undefined") {
|
|
951
952
|
console.debug("[BridgeShell] dropped malformed message:", parsed.issues?.[0]?.message);
|
|
@@ -1010,41 +1011,40 @@ var BridgeShell = class {
|
|
|
1010
1011
|
}
|
|
1011
1012
|
}
|
|
1012
1013
|
/**
|
|
1013
|
-
* **
|
|
1014
|
+
* **Layer 1.** Bootstraps by fetching a URL.
|
|
1014
1015
|
*
|
|
1015
|
-
*
|
|
1016
|
-
*
|
|
1016
|
+
* A shortcut for the common HTTP/HTTPS case: it runs `fetch(clientPath)` and
|
|
1017
|
+
* delegates to `bootstrapClientIframeWithHtml`.
|
|
1017
1018
|
*
|
|
1018
|
-
* **
|
|
1019
|
-
*
|
|
1020
|
-
*
|
|
1021
|
-
* `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.
|
|
1022
1023
|
*
|
|
1023
|
-
* **
|
|
1024
|
-
*
|
|
1025
|
-
* 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).
|
|
1026
1026
|
*/
|
|
1027
1027
|
async bootstrapClientIframe(clientPath = "client.html") {
|
|
1028
1028
|
const res = await fetch(clientPath);
|
|
1029
|
-
if (!res.ok) throw new Error(`BridgeShell:
|
|
1029
|
+
if (!res.ok) throw new Error(`BridgeShell: failed to load ${clientPath} (${res.status})`);
|
|
1030
1030
|
const clientHtml = await res.text();
|
|
1031
1031
|
await this.bootstrapClientIframeWithHtml(clientHtml);
|
|
1032
1032
|
}
|
|
1033
1033
|
/**
|
|
1034
|
-
* **
|
|
1034
|
+
* **Layer 1.** Bootstraps from an HTML string.
|
|
1035
1035
|
*
|
|
1036
|
-
*
|
|
1037
|
-
* build
|
|
1038
|
-
*
|
|
1039
|
-
*
|
|
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.
|
|
1040
1040
|
*
|
|
1041
|
-
* Use
|
|
1042
|
-
* bundle inlined
|
|
1043
|
-
*
|
|
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)`.
|
|
1044
1045
|
*
|
|
1045
|
-
* **
|
|
1046
|
-
*
|
|
1047
|
-
* 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).
|
|
1048
1048
|
*/
|
|
1049
1049
|
async bootstrapClientIframeWithHtml(clientHtml) {
|
|
1050
1050
|
this.iframe.srcdoc = OVERRIDE_PREFIX + clientHtml;
|
|
@@ -1060,31 +1060,31 @@ var BridgeShell = class {
|
|
|
1060
1060
|
});
|
|
1061
1061
|
}
|
|
1062
1062
|
/**
|
|
1063
|
-
* **
|
|
1064
|
-
*
|
|
1065
|
-
*
|
|
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.
|
|
1066
1066
|
*
|
|
1067
|
-
* `ok`
|
|
1068
|
-
* `false`, **shell
|
|
1069
|
-
*
|
|
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.
|
|
1070
1070
|
*/
|
|
1071
1071
|
verifySandbox() {
|
|
1072
1072
|
if (!this.iframe.contentWindow) {
|
|
1073
|
-
return Promise.reject(new Error("BridgeShell: iframe
|
|
1073
|
+
return Promise.reject(new Error("BridgeShell: iframe has no contentWindow"));
|
|
1074
1074
|
}
|
|
1075
1075
|
return new Promise((resolve, reject) => {
|
|
1076
1076
|
const nonce = Math.random().toString(36).slice(2);
|
|
1077
1077
|
const timer = setTimeout(() => {
|
|
1078
1078
|
this.pendingVerifies.delete(nonce);
|
|
1079
|
-
reject(new Error(`BridgeShell: verify-sandbox
|
|
1079
|
+
reject(new Error(`BridgeShell: verify-sandbox timed out after ${VERIFY_TIMEOUT_MS}ms`));
|
|
1080
1080
|
}, VERIFY_TIMEOUT_MS);
|
|
1081
1081
|
this.pendingVerifies.set(nonce, { resolve, reject, timer });
|
|
1082
1082
|
this.postToClient({ type: "verify-sandbox" });
|
|
1083
1083
|
});
|
|
1084
1084
|
}
|
|
1085
1085
|
/**
|
|
1086
|
-
*
|
|
1087
|
-
*
|
|
1086
|
+
* Tears the shell down: removes the `message` listener, cancels pending
|
|
1087
|
+
* subscriptions on the native `Bridge`, and clears internal state.
|
|
1088
1088
|
*/
|
|
1089
1089
|
detach() {
|
|
1090
1090
|
if (this.detached) return;
|
|
@@ -1150,13 +1150,13 @@ var BridgeShell = class {
|
|
|
1150
1150
|
Bridge.send(command, args);
|
|
1151
1151
|
}
|
|
1152
1152
|
/**
|
|
1153
|
-
*
|
|
1154
|
-
*
|
|
1153
|
+
* Resolves `nativeVersion`, firing the native handshake the first time and
|
|
1154
|
+
* caching the result.
|
|
1155
1155
|
*
|
|
1156
|
-
* ⚠️ `Bridge.handshake` **
|
|
1157
|
-
*
|
|
1158
|
-
*
|
|
1159
|
-
*
|
|
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.
|
|
1160
1160
|
*/
|
|
1161
1161
|
async resolveNativeVersion() {
|
|
1162
1162
|
if (this.nativeVersion !== null) return this.nativeVersion;
|