@multiplatform.one/web3 7.24.0 → 7.25.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/README.md +33 -0
- package/dist/cjs/bridge/AuthBridge.cjs +124 -0
- package/dist/cjs/bridge/AuthBridge.native.js +186 -0
- package/dist/cjs/bridge/AuthBridge.native.js.map +1 -0
- package/dist/cjs/bridge/BridgeClient.cjs +12 -9
- package/dist/cjs/bridge/BridgeClient.native.js +17 -12
- package/dist/cjs/bridge/BridgeClient.native.js.map +1 -1
- package/dist/cjs/bridge/BridgeHost.cjs +11 -3
- package/dist/cjs/bridge/BridgeHost.native.js +11 -3
- package/dist/cjs/bridge/BridgeHost.native.js.map +1 -1
- package/dist/cjs/bridge/GpgBridge.cjs +61 -0
- package/dist/cjs/bridge/GpgBridge.native.js +111 -0
- package/dist/cjs/bridge/GpgBridge.native.js.map +1 -0
- package/dist/cjs/bridge/SuiBridge.cjs +51 -0
- package/dist/cjs/bridge/SuiBridge.native.js +99 -0
- package/dist/cjs/bridge/SuiBridge.native.js.map +1 -0
- package/dist/cjs/bridge/authTypes.cjs +28 -0
- package/dist/cjs/bridge/authTypes.native.js +31 -0
- package/dist/cjs/bridge/authTypes.native.js.map +1 -0
- package/dist/cjs/bridge/core.cjs +65 -0
- package/dist/cjs/bridge/core.native.js +68 -0
- package/dist/cjs/bridge/core.native.js.map +1 -0
- package/dist/cjs/bridge/hosting.cjs +91 -0
- package/dist/cjs/bridge/hosting.native.js +117 -0
- package/dist/cjs/bridge/hosting.native.js.map +1 -0
- package/dist/cjs/bridge/index.cjs +3 -17
- package/dist/cjs/bridge/index.native.js +2 -16
- package/dist/cjs/bridge/index.native.js.map +1 -1
- package/dist/cjs/bridge/nativeHost.cjs +91 -0
- package/dist/cjs/bridge/nativeHost.native.js +118 -0
- package/dist/cjs/bridge/nativeHost.native.js.map +1 -0
- package/dist/cjs/bridge/protocol.cjs +30 -1
- package/dist/cjs/bridge/protocol.native.js +147 -1
- package/dist/cjs/bridge/protocol.native.js.map +1 -1
- package/dist/cjs/index.cjs +1 -0
- package/dist/cjs/index.native.js +1 -0
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/providers/WalletProvider.cjs +2 -0
- package/dist/cjs/providers/WalletProvider.native.js +123 -90
- package/dist/cjs/providers/WalletProvider.native.js.map +1 -1
- package/dist/cjs/providers/index.cjs +1 -0
- package/dist/cjs/providers/index.native.js +1 -0
- package/dist/cjs/providers/index.native.js.map +1 -1
- package/dist/cjs/providers/native/deepLink.cjs +40 -23
- package/dist/cjs/providers/native/deepLink.native.js +43 -25
- package/dist/cjs/providers/native/deepLink.native.js.map +1 -1
- package/dist/cjs/providers/native/inject.cjs +2 -4
- package/dist/cjs/providers/native/inject.native.js +2 -4
- package/dist/cjs/providers/native/inject.native.js.map +1 -1
- package/dist/cjs/providers/walletBridge.cjs +35 -0
- package/dist/cjs/providers/walletBridge.native.js +38 -0
- package/dist/cjs/providers/walletBridge.native.js.map +1 -0
- package/dist/cjs/providers/walletConfig.native.js.map +1 -1
- package/dist/esm/bridge/AuthBridge.mjs +99 -0
- package/dist/esm/bridge/AuthBridge.mjs.map +1 -0
- package/dist/esm/bridge/AuthBridge.native.js +158 -0
- package/dist/esm/bridge/AuthBridge.native.js.map +1 -0
- package/dist/esm/bridge/BridgeClient.mjs +13 -10
- package/dist/esm/bridge/BridgeClient.mjs.map +1 -1
- package/dist/esm/bridge/BridgeClient.native.js +18 -13
- package/dist/esm/bridge/BridgeClient.native.js.map +1 -1
- package/dist/esm/bridge/BridgeHost.mjs +12 -4
- package/dist/esm/bridge/BridgeHost.mjs.map +1 -1
- package/dist/esm/bridge/BridgeHost.native.js +12 -4
- package/dist/esm/bridge/BridgeHost.native.js.map +1 -1
- package/dist/esm/bridge/GpgBridge.mjs +36 -0
- package/dist/esm/bridge/GpgBridge.mjs.map +1 -0
- package/dist/esm/bridge/GpgBridge.native.js +83 -0
- package/dist/esm/bridge/GpgBridge.native.js.map +1 -0
- package/dist/esm/bridge/SuiBridge.mjs +26 -0
- package/dist/esm/bridge/SuiBridge.mjs.map +1 -0
- package/dist/esm/bridge/SuiBridge.native.js +71 -0
- package/dist/esm/bridge/SuiBridge.native.js.map +1 -0
- package/dist/esm/bridge/authTypes.mjs +3 -0
- package/dist/esm/bridge/authTypes.mjs.map +1 -0
- package/dist/esm/bridge/authTypes.native.js +3 -0
- package/dist/esm/bridge/authTypes.native.js.map +1 -0
- package/dist/esm/bridge/core.mjs +17 -0
- package/dist/esm/bridge/core.mjs.map +1 -0
- package/dist/esm/bridge/core.native.js +17 -0
- package/dist/esm/bridge/core.native.js.map +1 -0
- package/dist/esm/bridge/hosting.mjs +62 -0
- package/dist/esm/bridge/hosting.mjs.map +1 -0
- package/dist/esm/bridge/hosting.native.js +85 -0
- package/dist/esm/bridge/hosting.native.js.map +1 -0
- package/dist/esm/bridge/index.mjs +2 -9
- package/dist/esm/bridge/index.mjs.map +1 -1
- package/dist/esm/bridge/index.native.js +2 -9
- package/dist/esm/bridge/index.native.js.map +1 -1
- package/dist/esm/bridge/nativeHost.mjs +65 -0
- package/dist/esm/bridge/nativeHost.mjs.map +1 -0
- package/dist/esm/bridge/nativeHost.native.js +89 -0
- package/dist/esm/bridge/nativeHost.native.js.map +1 -0
- package/dist/esm/bridge/protocol.mjs +28 -2
- package/dist/esm/bridge/protocol.mjs.map +1 -1
- package/dist/esm/bridge/protocol.native.js +145 -2
- package/dist/esm/bridge/protocol.native.js.map +1 -1
- package/dist/esm/index.mjs +2 -2
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +2 -2
- package/dist/esm/index.native.js.map +1 -1
- package/dist/esm/providers/WalletProvider.mjs +2 -1
- package/dist/esm/providers/WalletProvider.mjs.map +1 -1
- package/dist/esm/providers/WalletProvider.native.js +125 -93
- package/dist/esm/providers/WalletProvider.native.js.map +1 -1
- package/dist/esm/providers/index.mjs +2 -2
- package/dist/esm/providers/index.mjs.map +1 -1
- package/dist/esm/providers/index.native.js +2 -2
- package/dist/esm/providers/index.native.js.map +1 -1
- package/dist/esm/providers/native/deepLink.mjs +36 -24
- package/dist/esm/providers/native/deepLink.mjs.map +1 -1
- package/dist/esm/providers/native/deepLink.native.js +39 -26
- package/dist/esm/providers/native/deepLink.native.js.map +1 -1
- package/dist/esm/providers/native/inject.mjs +1 -3
- package/dist/esm/providers/native/inject.mjs.map +1 -1
- package/dist/esm/providers/native/inject.native.js +1 -3
- package/dist/esm/providers/native/inject.native.js.map +1 -1
- package/dist/esm/providers/walletBridge.mjs +9 -0
- package/dist/esm/providers/walletBridge.mjs.map +1 -0
- package/dist/esm/providers/walletBridge.native.js +9 -0
- package/dist/esm/providers/walletBridge.native.js.map +1 -0
- package/dist/esm/providers/walletConfig.mjs.map +1 -1
- package/dist/esm/providers/walletConfig.native.js.map +1 -1
- package/dist/jsx/bridge/AuthBridge.mjs +99 -0
- package/dist/jsx/bridge/AuthBridge.mjs.map +1 -0
- package/dist/jsx/bridge/AuthBridge.native.js +186 -0
- package/dist/jsx/bridge/AuthBridge.native.js.map +1 -0
- package/dist/jsx/bridge/BridgeClient.mjs +13 -10
- package/dist/jsx/bridge/BridgeClient.mjs.map +1 -1
- package/dist/jsx/bridge/BridgeClient.native.js +17 -12
- package/dist/jsx/bridge/BridgeClient.native.js.map +1 -1
- package/dist/jsx/bridge/BridgeHost.mjs +12 -4
- package/dist/jsx/bridge/BridgeHost.mjs.map +1 -1
- package/dist/jsx/bridge/BridgeHost.native.js +11 -3
- package/dist/jsx/bridge/BridgeHost.native.js.map +1 -1
- package/dist/jsx/bridge/GpgBridge.mjs +36 -0
- package/dist/jsx/bridge/GpgBridge.mjs.map +1 -0
- package/dist/jsx/bridge/GpgBridge.native.js +111 -0
- package/dist/jsx/bridge/GpgBridge.native.js.map +1 -0
- package/dist/jsx/bridge/SuiBridge.mjs +26 -0
- package/dist/jsx/bridge/SuiBridge.mjs.map +1 -0
- package/dist/jsx/bridge/SuiBridge.native.js +99 -0
- package/dist/jsx/bridge/SuiBridge.native.js.map +1 -0
- package/dist/jsx/bridge/authTypes.mjs +3 -0
- package/dist/jsx/bridge/authTypes.mjs.map +1 -0
- package/dist/jsx/bridge/authTypes.native.js +31 -0
- package/dist/jsx/bridge/authTypes.native.js.map +1 -0
- package/dist/jsx/bridge/core.mjs +17 -0
- package/dist/jsx/bridge/core.mjs.map +1 -0
- package/dist/jsx/bridge/core.native.js +68 -0
- package/dist/jsx/bridge/core.native.js.map +1 -0
- package/dist/jsx/bridge/hosting.mjs +62 -0
- package/dist/jsx/bridge/hosting.mjs.map +1 -0
- package/dist/jsx/bridge/hosting.native.js +117 -0
- package/dist/jsx/bridge/hosting.native.js.map +1 -0
- package/dist/jsx/bridge/index.mjs +2 -9
- package/dist/jsx/bridge/index.mjs.map +1 -1
- package/dist/jsx/bridge/index.native.js +2 -16
- package/dist/jsx/bridge/index.native.js.map +1 -1
- package/dist/jsx/bridge/nativeHost.mjs +65 -0
- package/dist/jsx/bridge/nativeHost.mjs.map +1 -0
- package/dist/jsx/bridge/nativeHost.native.js +118 -0
- package/dist/jsx/bridge/nativeHost.native.js.map +1 -0
- package/dist/jsx/bridge/protocol.mjs +28 -2
- package/dist/jsx/bridge/protocol.mjs.map +1 -1
- package/dist/jsx/bridge/protocol.native.js +147 -1
- package/dist/jsx/bridge/protocol.native.js.map +1 -1
- package/dist/jsx/index.js +2 -2
- package/dist/jsx/index.js.map +1 -1
- package/dist/jsx/index.mjs +2 -2
- package/dist/jsx/index.mjs.map +1 -1
- package/dist/jsx/index.native.js +1 -0
- package/dist/jsx/index.native.js.map +1 -1
- package/dist/jsx/providers/WalletProvider.mjs +2 -1
- package/dist/jsx/providers/WalletProvider.mjs.map +1 -1
- package/dist/jsx/providers/WalletProvider.native.js +123 -90
- package/dist/jsx/providers/WalletProvider.native.js.map +1 -1
- package/dist/jsx/providers/index.mjs +2 -2
- package/dist/jsx/providers/index.mjs.map +1 -1
- package/dist/jsx/providers/index.native.js +1 -0
- package/dist/jsx/providers/index.native.js.map +1 -1
- package/dist/jsx/providers/native/deepLink.mjs +36 -24
- package/dist/jsx/providers/native/deepLink.mjs.map +1 -1
- package/dist/jsx/providers/native/deepLink.native.js +43 -25
- package/dist/jsx/providers/native/deepLink.native.js.map +1 -1
- package/dist/jsx/providers/native/inject.mjs +1 -3
- package/dist/jsx/providers/native/inject.mjs.map +1 -1
- package/dist/jsx/providers/native/inject.native.js +2 -4
- package/dist/jsx/providers/native/inject.native.js.map +1 -1
- package/dist/jsx/providers/walletBridge.mjs +9 -0
- package/dist/jsx/providers/walletBridge.mjs.map +1 -0
- package/dist/jsx/providers/walletBridge.native.js +38 -0
- package/dist/jsx/providers/walletBridge.native.js.map +1 -0
- package/dist/jsx/providers/walletConfig.mjs.map +1 -1
- package/dist/jsx/providers/walletConfig.native.js.map +1 -1
- package/docs/bridge-protocol.md +485 -0
- package/package.json +14 -6
- package/src/bridge/AuthBridge.ts +146 -0
- package/src/bridge/BridgeClient.spec.ts +12 -0
- package/src/bridge/BridgeClient.ts +15 -10
- package/src/bridge/BridgeHost.spec.ts +20 -4
- package/src/bridge/BridgeHost.ts +16 -5
- package/src/bridge/GpgBridge.ts +49 -0
- package/src/bridge/SuiBridge.ts +34 -0
- package/src/bridge/authTypes.ts +76 -0
- package/src/bridge/core.ts +42 -0
- package/src/bridge/hosting.spec.ts +98 -0
- package/src/bridge/hosting.ts +99 -0
- package/src/bridge/index.ts +1 -8
- package/src/bridge/nativeHost.spec.ts +165 -0
- package/src/bridge/nativeHost.ts +120 -0
- package/src/bridge/protocol.spec.ts +24 -1
- package/src/bridge/protocol.ts +44 -1
- package/src/index.ts +2 -0
- package/src/providers/WalletProvider.native.tsx +139 -85
- package/src/providers/WalletProvider.tsx +1 -1
- package/src/providers/index.ts +2 -0
- package/src/providers/native/deepLink.spec.ts +73 -1
- package/src/providers/native/deepLink.ts +76 -28
- package/src/providers/native/inject.ts +1 -9
- package/src/providers/walletBridge.ts +24 -0
- package/src/providers/walletConfig.ts +15 -1
- package/types/bridge/AuthBridge.d.ts +58 -0
- package/types/bridge/AuthBridge.d.ts.map +1 -0
- package/types/bridge/BridgeClient.d.ts +3 -1
- package/types/bridge/BridgeClient.d.ts.map +1 -1
- package/types/bridge/BridgeHost.d.ts +1 -1
- package/types/bridge/BridgeHost.d.ts.map +1 -1
- package/types/bridge/GpgBridge.d.ts +23 -0
- package/types/bridge/GpgBridge.d.ts.map +1 -0
- package/types/bridge/SuiBridge.d.ts +18 -0
- package/types/bridge/SuiBridge.d.ts.map +1 -0
- package/types/bridge/authTypes.d.ts +71 -0
- package/types/bridge/authTypes.d.ts.map +1 -0
- package/types/bridge/core.d.ts +16 -0
- package/types/bridge/core.d.ts.map +1 -0
- package/types/bridge/hosting.d.ts +34 -0
- package/types/bridge/hosting.d.ts.map +1 -0
- package/types/bridge/index.d.ts +1 -8
- package/types/bridge/index.d.ts.map +1 -1
- package/types/bridge/nativeHost.d.ts +55 -0
- package/types/bridge/nativeHost.d.ts.map +1 -0
- package/types/bridge/protocol.d.ts +34 -1
- package/types/bridge/protocol.d.ts.map +1 -1
- package/types/index.d.ts +1 -1
- package/types/index.d.ts.map +1 -1
- package/types/providers/WalletProvider.d.ts +1 -0
- package/types/providers/WalletProvider.d.ts.map +1 -1
- package/types/providers/WalletProvider.native.d.ts +2 -1
- package/types/providers/WalletProvider.native.d.ts.map +1 -1
- package/types/providers/index.d.ts +1 -1
- package/types/providers/index.d.ts.map +1 -1
- package/types/providers/native/deepLink.d.ts +29 -4
- package/types/providers/native/deepLink.d.ts.map +1 -1
- package/types/providers/native/inject.d.ts +1 -7
- package/types/providers/native/inject.d.ts.map +1 -1
- package/types/providers/walletBridge.d.ts +17 -0
- package/types/providers/walletBridge.d.ts.map +1 -0
- package/types/providers/walletConfig.d.ts +18 -1
- package/types/providers/walletConfig.d.ts.map +1 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// URL handling without the URL global: React Native's own URL throws on
|
|
2
|
+
// `hostname` and `searchParams` outside Expo, and native hosts run this code.
|
|
3
|
+
|
|
4
|
+
export interface ParsedOrigin {
|
|
5
|
+
/** Lowercased, without the colon. */
|
|
6
|
+
scheme: string;
|
|
7
|
+
/** Lowercased hostname with a non-default port, e.g. `example.com:8443`; empty for `about:`. */
|
|
8
|
+
host: string;
|
|
9
|
+
/** Lowercased hostname, IPv6 in brackets. */
|
|
10
|
+
hostname: string;
|
|
11
|
+
/** `scheme://host`, or `scheme:` for a URL without an authority. */
|
|
12
|
+
origin: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// A backslash ends the authority, as browsers read http(s) URLs:
|
|
16
|
+
// `http://evil.com\@localhost/` is evil.com, not localhost.
|
|
17
|
+
const schemeAndAuthority = /^([a-z][a-z0-9+.-]*):(?:\/\/([^/?#\\]*))?/i;
|
|
18
|
+
const hostAndPort = /^(\[[^\]]*\]|[^:]*)(?::(\d*))?$/;
|
|
19
|
+
const defaultPorts: Record<string, string> = { http: "80", https: "443" };
|
|
20
|
+
|
|
21
|
+
export function parseOrigin(url: string): ParsedOrigin | null {
|
|
22
|
+
const match = url.trim().match(schemeAndAuthority);
|
|
23
|
+
if (!match) return null;
|
|
24
|
+
const scheme = match[1].toLowerCase();
|
|
25
|
+
if (match[2] === undefined) return { scheme, host: "", hostname: "", origin: `${scheme}:` };
|
|
26
|
+
const authority = match[2].slice(match[2].lastIndexOf("@") + 1).toLowerCase();
|
|
27
|
+
const hostPort = authority.match(hostAndPort);
|
|
28
|
+
if (!hostPort) return null;
|
|
29
|
+
const hostname = hostPort[1];
|
|
30
|
+
const port = hostPort[2] && hostPort[2] !== defaultPorts[scheme] ? hostPort[2] : "";
|
|
31
|
+
const host = port ? `${hostname}:${port}` : hostname;
|
|
32
|
+
return { scheme, host, hostname, origin: `${scheme}://${host}` };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The query parameters of `url`, decoded; a repeated name keeps its first value. */
|
|
36
|
+
export function queryParams(url: string): Record<string, string> {
|
|
37
|
+
const params = new Map<string, string>();
|
|
38
|
+
const query = url.match(/\?([^#]*)/)?.[1] ?? "";
|
|
39
|
+
for (const pair of query.split("&")) {
|
|
40
|
+
if (!pair) continue;
|
|
41
|
+
const eq = pair.indexOf("=");
|
|
42
|
+
const key = safeDecode(eq === -1 ? pair : pair.slice(0, eq));
|
|
43
|
+
if (!params.has(key)) params.set(key, eq === -1 ? "" : safeDecode(pair.slice(eq + 1)));
|
|
44
|
+
}
|
|
45
|
+
return Object.fromEntries(params);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function safeDecode(value: string): string {
|
|
49
|
+
try {
|
|
50
|
+
return decodeURIComponent(value.replace(/\+/g, " "));
|
|
51
|
+
} catch {
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Plain http is allowed only where the device itself is the server: loopback,
|
|
58
|
+
* reached from an emulator through `adb reverse`, and 10.0.2.2, the Android
|
|
59
|
+
* emulator's alias for the host machine. Development builds only: on a phone
|
|
60
|
+
* any app can bind a loopback port, and 10.0.2.2 is a LAN address there.
|
|
61
|
+
*/
|
|
62
|
+
export const DEV_HOSTNAMES: readonly string[] = ["localhost", "127.0.0.1", "[::1]", "10.0.2.2"];
|
|
63
|
+
|
|
64
|
+
export interface HostedPageOptions {
|
|
65
|
+
/** Accept plain http on `DEV_HOSTNAMES`. Default false; a React Native host passes `__DEV__`. */
|
|
66
|
+
allowDevHosts?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* True when a page at `url` can run the wallet bridge: https, or http on a
|
|
71
|
+
* development host. The sign-in message's domain is the page's host and the
|
|
72
|
+
* Keycloak nonce cookie is set for it, so a `file:`, `data:` or inline page,
|
|
73
|
+
* whose host is empty, can never sign in.
|
|
74
|
+
*/
|
|
75
|
+
export function isHostedPageUrl(url: string, options: HostedPageOptions = {}): boolean {
|
|
76
|
+
const parsed = parseOrigin(url);
|
|
77
|
+
if (!parsed || !parsed.hostname) return false;
|
|
78
|
+
if (parsed.scheme === "https") return true;
|
|
79
|
+
return (
|
|
80
|
+
options.allowDevHosts === true &&
|
|
81
|
+
parsed.scheme === "http" &&
|
|
82
|
+
DEV_HOSTNAMES.includes(parsed.hostname)
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The origin of a hosted wallet page, or a thrown error naming what is wrong with `url`. */
|
|
87
|
+
export function hostedPageOrigin(url: string, options: HostedPageOptions = {}): string {
|
|
88
|
+
if (!isHostedPageUrl(url, options)) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`The wallet WebView page must be served over https (got ${JSON.stringify(url)}). ` +
|
|
91
|
+
"Serve @multiplatform.one/web3/assets/webview.html from a host listed in the realm's " +
|
|
92
|
+
"siww.allowedDomains / wallet.allowedDomains; " +
|
|
93
|
+
(options.allowDevHosts
|
|
94
|
+
? `plain http is accepted only on ${DEV_HOSTNAMES.join(", ")} for development.`
|
|
95
|
+
: "plain http is accepted only in development builds."),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return (parseOrigin(url) as ParsedOrigin).origin;
|
|
99
|
+
}
|
package/src/bridge/index.ts
CHANGED
|
@@ -1,9 +1,2 @@
|
|
|
1
|
-
export * from "./
|
|
2
|
-
export * from "./transport";
|
|
3
|
-
export { createPostMessageTransport, type PostMessageTransport } from "./postMessageTransport";
|
|
4
|
-
export { BridgeClient, type EventHandler, type RequestOptions } from "./BridgeClient";
|
|
5
|
-
export { BridgeHost, type RequestHandler } from "./BridgeHost";
|
|
6
|
-
export { createBridgeProvider, type BridgeProvider, type Eip1193RequestArgs } from "./eip1193";
|
|
1
|
+
export * from "./core";
|
|
7
2
|
export { createBridgeConnector } from "./bridgeConnector";
|
|
8
|
-
export { bytesToBase64, base64ToBytes } from "./base64";
|
|
9
|
-
export { SolanaBridge } from "./SolanaBridge";
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { BridgeHost } from "./BridgeHost";
|
|
3
|
+
import { buildInjectScript, createNativeBridge } from "./nativeHost";
|
|
4
|
+
import { createPostMessageTransport } from "./postMessageTransport";
|
|
5
|
+
import { BRIDGE_PROTOCOL_VERSION } from "./protocol";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A fake page: `inject` runs the native side's script against a fake window
|
|
9
|
+
* whose `__bridgeReceive` feeds a BridgeHost, and the host's replies come back
|
|
10
|
+
* through `native.receive` with the page's URL.
|
|
11
|
+
*/
|
|
12
|
+
function setup(pageUrl = "https://wallet.example.com/webview.html") {
|
|
13
|
+
const injected: string[] = [];
|
|
14
|
+
const win: { location: { origin: string }; __bridgeReceive?: (raw: string) => void } = {
|
|
15
|
+
location: { origin: new URL(pageUrl).origin },
|
|
16
|
+
};
|
|
17
|
+
let postedFrom = pageUrl;
|
|
18
|
+
const native = createNativeBridge({
|
|
19
|
+
pageUrl,
|
|
20
|
+
allowDevHosts: true,
|
|
21
|
+
inject(script) {
|
|
22
|
+
injected.push(script);
|
|
23
|
+
new Function("window", script)(win);
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
const transport = createPostMessageTransport((raw) => native.receive(raw, postedFrom));
|
|
27
|
+
win.__bridgeReceive = (raw) => transport.receive(raw);
|
|
28
|
+
const host = new BridgeHost(transport);
|
|
29
|
+
return {
|
|
30
|
+
native,
|
|
31
|
+
host,
|
|
32
|
+
injected,
|
|
33
|
+
win,
|
|
34
|
+
postFrom(url: string) {
|
|
35
|
+
postedFrom = url;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("createNativeBridge", () => {
|
|
41
|
+
it("refuses a page that is not hosted", () => {
|
|
42
|
+
expect(() =>
|
|
43
|
+
createNativeBridge({ pageUrl: "file:///webview.html", inject: () => undefined }),
|
|
44
|
+
).toThrow(/https/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("refuses a plain-http dev page unless dev hosts are allowed", () => {
|
|
48
|
+
const pageUrl = "http://localhost:8099/webview.html";
|
|
49
|
+
expect(() => createNativeBridge({ pageUrl, inject: () => undefined })).toThrow(
|
|
50
|
+
/development builds/,
|
|
51
|
+
);
|
|
52
|
+
expect(
|
|
53
|
+
createNativeBridge({ pageUrl, inject: () => undefined, allowDevHosts: true }).pageOrigin,
|
|
54
|
+
).toBe("http://localhost:8099");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("queues requests until sys.ready, then flushes them", async () => {
|
|
58
|
+
const { native, host, injected } = setup();
|
|
59
|
+
host.register("evm", (method) => (method === "eth_chainId" ? "0x1" : null));
|
|
60
|
+
const pending = native.client.request("evm", "eth_chainId");
|
|
61
|
+
expect(injected).toHaveLength(0);
|
|
62
|
+
host.ready();
|
|
63
|
+
await expect(pending).resolves.toBe("0x1");
|
|
64
|
+
expect(injected).toHaveLength(1);
|
|
65
|
+
expect(native.peer).toEqual({ protocol: BRIDGE_PROTOCOL_VERSION, namespaces: ["evm", "sys"] });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("drops messages posted by a page on another origin", async () => {
|
|
69
|
+
const { native, host, postFrom } = setup();
|
|
70
|
+
host.register("evm", () => "0x1");
|
|
71
|
+
postFrom("https://evil.example/");
|
|
72
|
+
host.ready();
|
|
73
|
+
expect(native.peer).toBeNull();
|
|
74
|
+
postFrom("https://wallet.example.com/other");
|
|
75
|
+
host.ready();
|
|
76
|
+
expect(native.peer).not.toBeNull();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("drops messages that name no sender", () => {
|
|
80
|
+
const { native, postFrom, host } = setup();
|
|
81
|
+
postFrom("");
|
|
82
|
+
host.ready();
|
|
83
|
+
expect(native.peer).toBeNull();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("delivers nothing into a document on another origin", async () => {
|
|
87
|
+
const { native, host, injected, win } = setup();
|
|
88
|
+
host.register("evm", () => "0x1");
|
|
89
|
+
host.ready();
|
|
90
|
+
win.location.origin = "https://evil.example";
|
|
91
|
+
const lost = native.client.request("evm", "eth_chainId", undefined, { timeoutMs: 20 });
|
|
92
|
+
expect(injected).toHaveLength(1);
|
|
93
|
+
await expect(lost).rejects.toThrow();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("a top-frame load on another origin fails what is in flight and stops delivery", async () => {
|
|
97
|
+
const { native, host, injected } = setup();
|
|
98
|
+
host.register("evm", (method) =>
|
|
99
|
+
method === "eth_chainId" ? "0x1" : new Promise(() => undefined),
|
|
100
|
+
);
|
|
101
|
+
host.ready();
|
|
102
|
+
const lost = native.client.request("evm", "eth_requestAccounts");
|
|
103
|
+
expect(native.navigated("https://wallet.example.com/webview.html")).toBe(false);
|
|
104
|
+
expect(native.navigated("about:blank")).toBe(false);
|
|
105
|
+
expect(native.peer).not.toBeNull();
|
|
106
|
+
expect(native.navigated("https://evil.example/form")).toBe(true);
|
|
107
|
+
await expect(lost).rejects.toThrow(/went away/);
|
|
108
|
+
expect(native.peer).toBeNull();
|
|
109
|
+
const before = injected.length;
|
|
110
|
+
const next = native.client.request("evm", "eth_chainId");
|
|
111
|
+
expect(injected).toHaveLength(before);
|
|
112
|
+
host.ready();
|
|
113
|
+
await expect(next).resolves.toBe("0x1");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("fails requests in flight when the page announces itself again", async () => {
|
|
117
|
+
const { native, host } = setup();
|
|
118
|
+
host.register("evm", () => new Promise(() => undefined));
|
|
119
|
+
host.ready();
|
|
120
|
+
const lost = native.client.request("evm", "eth_requestAccounts");
|
|
121
|
+
host.ready();
|
|
122
|
+
await expect(lost).rejects.toThrow(/reloaded/);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("pageGone fails what is in flight and queues until the next ready", async () => {
|
|
126
|
+
const { native, host, injected } = setup();
|
|
127
|
+
host.register("evm", (method) =>
|
|
128
|
+
method === "eth_chainId" ? "0x1" : new Promise(() => undefined),
|
|
129
|
+
);
|
|
130
|
+
host.ready();
|
|
131
|
+
const lost = native.client.request("evm", "eth_requestAccounts");
|
|
132
|
+
native.pageGone();
|
|
133
|
+
await expect(lost).rejects.toThrow(/went away/);
|
|
134
|
+
const before = injected.length;
|
|
135
|
+
const next = native.client.request("evm", "eth_chainId");
|
|
136
|
+
expect(injected).toHaveLength(before);
|
|
137
|
+
host.ready();
|
|
138
|
+
await expect(next).resolves.toBe("0x1");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("asks for the page back twice at most until it announces itself again", () => {
|
|
142
|
+
const { native, host } = setup();
|
|
143
|
+
host.ready();
|
|
144
|
+
expect(native.navigated("https://evil.example/")).toBe(true);
|
|
145
|
+
expect(native.navigated("https://evil.example/")).toBe(true);
|
|
146
|
+
expect(native.navigated("https://evil.example/")).toBe(false);
|
|
147
|
+
host.ready();
|
|
148
|
+
expect(native.navigated("https://evil.example/")).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("exposes the page origin", () => {
|
|
152
|
+
expect(setup("http://localhost:8099/webview.html").native.pageOrigin).toBe(
|
|
153
|
+
"http://localhost:8099",
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("buildInjectScript survives quotes and newlines", () => {
|
|
158
|
+
const received: string[] = [];
|
|
159
|
+
const raw = 'he said "hi"\nline2\\end </script>';
|
|
160
|
+
new Function("window", buildInjectScript(raw))({
|
|
161
|
+
__bridgeReceive: (s: string) => received.push(s),
|
|
162
|
+
});
|
|
163
|
+
expect(received).toEqual([raw]);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { BridgeClient } from "./BridgeClient";
|
|
2
|
+
import { hostedPageOrigin, parseOrigin } from "./hosting";
|
|
3
|
+
import { createPostMessageTransport } from "./postMessageTransport";
|
|
4
|
+
import { BRIDGE_PROTOCOL_VERSION, type BridgeReady } from "./protocol";
|
|
5
|
+
|
|
6
|
+
export interface NativeBridgeOptions {
|
|
7
|
+
/** The hosted wallet page. Must pass `isHostedPageUrl`. */
|
|
8
|
+
pageUrl: string;
|
|
9
|
+
/** Accept plain http on the development hosts. Pass `__DEV__`; never true in a release build. */
|
|
10
|
+
allowDevHosts?: boolean;
|
|
11
|
+
/** Runs a script in the page: react-native-webview's `injectJavaScript`. */
|
|
12
|
+
inject(script: string): void;
|
|
13
|
+
/** Default request timeout, see BridgeClient. */
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
/** Called once the page reports `sys.ready`. */
|
|
16
|
+
onReady?(ready: BridgeReady): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface NativeBridge {
|
|
20
|
+
readonly client: BridgeClient;
|
|
21
|
+
/** Origin of the hosted page; messages from any other origin are dropped. */
|
|
22
|
+
readonly pageOrigin: string;
|
|
23
|
+
/** What the page announced in `sys.ready`, or null before it has. */
|
|
24
|
+
readonly peer: BridgeReady | null;
|
|
25
|
+
/**
|
|
26
|
+
* Feed a WebView message in: `receive(e.nativeEvent.data, e.nativeEvent.url)`.
|
|
27
|
+
* A message whose `fromUrl` is missing or on another origin is dropped.
|
|
28
|
+
*/
|
|
29
|
+
receive(data: string, fromUrl: string): void;
|
|
30
|
+
/**
|
|
31
|
+
* The top frame started loading `url` (react-native-webview's `onLoadStart`).
|
|
32
|
+
* A document on another origin is not the page: what is in flight fails,
|
|
33
|
+
* nothing more is delivered until the page announces itself again, and the
|
|
34
|
+
* answer is true so the host loads `pageUrl` back. Twice at most between two
|
|
35
|
+
* `sys.ready`s: a page that never comes back stays down instead of looping.
|
|
36
|
+
*/
|
|
37
|
+
navigated(url: string): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The page died (WebView content process gone) or is being reloaded: fail
|
|
40
|
+
* the requests in flight and queue new ones until the next `sys.ready`.
|
|
41
|
+
*/
|
|
42
|
+
pageGone(): void;
|
|
43
|
+
dispose(): void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The script that delivers one message into the page. The raw string is
|
|
48
|
+
* embedded with JSON.stringify so quotes, newlines and backslashes survive;
|
|
49
|
+
* the trailing `true;` is react-native-webview's injectJavaScript contract.
|
|
50
|
+
* With `pageOrigin` the script delivers only while the top frame is on that
|
|
51
|
+
* origin, so a document that replaced the page never sees a message.
|
|
52
|
+
*/
|
|
53
|
+
export function buildInjectScript(raw: string, pageOrigin?: string): string {
|
|
54
|
+
const guard = pageOrigin
|
|
55
|
+
? `window.location && window.location.origin === ${JSON.stringify(pageOrigin)} && `
|
|
56
|
+
: "";
|
|
57
|
+
return `${guard}window.__bridgeReceive && window.__bridgeReceive(${JSON.stringify(raw)});true;`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The native end of the bridge for any React Native WebView host. Requests
|
|
62
|
+
* wait in a queue until the page says `sys.ready`, because the first request
|
|
63
|
+
* usually fires before the page has loaded.
|
|
64
|
+
*/
|
|
65
|
+
export function createNativeBridge(options: NativeBridgeOptions): NativeBridge {
|
|
66
|
+
const pageOrigin = hostedPageOrigin(options.pageUrl, { allowDevHosts: options.allowDevHosts });
|
|
67
|
+
const queue: string[] = [];
|
|
68
|
+
let peer: BridgeReady | null = null;
|
|
69
|
+
let reloads = 0;
|
|
70
|
+
|
|
71
|
+
const transport = createPostMessageTransport((raw) => {
|
|
72
|
+
if (peer) options.inject(buildInjectScript(raw, pageOrigin));
|
|
73
|
+
else queue.push(raw);
|
|
74
|
+
});
|
|
75
|
+
const offReady = transport.subscribe((message) => {
|
|
76
|
+
if (message.ns !== "sys" || message.kind !== "event" || message.method !== "ready") return;
|
|
77
|
+
// A second ready is a new document: whatever the old one was answering is lost.
|
|
78
|
+
if (peer) client.rejectPending(new Error("The wallet page reloaded"));
|
|
79
|
+
reloads = 0;
|
|
80
|
+
const params = message.params as Partial<BridgeReady> | undefined;
|
|
81
|
+
peer = {
|
|
82
|
+
protocol: typeof params?.protocol === "number" ? params.protocol : BRIDGE_PROTOCOL_VERSION,
|
|
83
|
+
namespaces: Array.isArray(params?.namespaces) ? params.namespaces : [],
|
|
84
|
+
};
|
|
85
|
+
for (const raw of queue.splice(0)) options.inject(buildInjectScript(raw, pageOrigin));
|
|
86
|
+
options.onReady?.(peer);
|
|
87
|
+
});
|
|
88
|
+
const client = new BridgeClient(transport, { timeoutMs: options.timeoutMs });
|
|
89
|
+
|
|
90
|
+
const pageGone = () => {
|
|
91
|
+
peer = null;
|
|
92
|
+
queue.length = 0;
|
|
93
|
+
client.rejectPending(new Error("The wallet page went away"));
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
client,
|
|
98
|
+
pageOrigin,
|
|
99
|
+
get peer() {
|
|
100
|
+
return peer;
|
|
101
|
+
},
|
|
102
|
+
receive(data, fromUrl) {
|
|
103
|
+
if (!fromUrl || parseOrigin(fromUrl)?.origin !== pageOrigin) return;
|
|
104
|
+
transport.receive(data);
|
|
105
|
+
},
|
|
106
|
+
navigated(url) {
|
|
107
|
+
if (url.trim().toLowerCase() === "about:blank") return false;
|
|
108
|
+
if (parseOrigin(url)?.origin === pageOrigin) return false;
|
|
109
|
+
pageGone();
|
|
110
|
+
reloads += 1;
|
|
111
|
+
return reloads <= 2;
|
|
112
|
+
},
|
|
113
|
+
pageGone,
|
|
114
|
+
dispose() {
|
|
115
|
+
offReady();
|
|
116
|
+
client.dispose();
|
|
117
|
+
queue.length = 0;
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
BridgeRequestError,
|
|
4
|
+
NAMESPACES,
|
|
5
|
+
isBridgeMessage,
|
|
6
|
+
newRequestId,
|
|
7
|
+
type BridgeMessage,
|
|
8
|
+
} from "./protocol";
|
|
3
9
|
|
|
4
10
|
describe("protocol", () => {
|
|
5
11
|
it("accepts a well-formed req", () => {
|
|
@@ -9,6 +15,23 @@ describe("protocol", () => {
|
|
|
9
15
|
it("accepts an event without id", () => {
|
|
10
16
|
expect(isBridgeMessage({ ns: "solana", kind: "event", method: "connect" })).toBe(true);
|
|
11
17
|
});
|
|
18
|
+
it("accepts the sui, gpg and auth namespaces", () => {
|
|
19
|
+
expect(NAMESPACES).toEqual(["evm", "solana", "sui", "gpg", "auth", "sys"]);
|
|
20
|
+
for (const ns of ["sui", "gpg", "auth"]) {
|
|
21
|
+
expect(isBridgeMessage({ id: "1", ns, kind: "req", method: "signMessage" })).toBe(true);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
it("BridgeRequestError keeps code and data", () => {
|
|
25
|
+
const e = new BridgeRequestError(-32002, "refused", { status: 401 });
|
|
26
|
+
expect(e).toBeInstanceOf(Error);
|
|
27
|
+
expect([e.name, e.code, e.message, e.data]).toEqual([
|
|
28
|
+
"BridgeRequestError",
|
|
29
|
+
-32002,
|
|
30
|
+
"refused",
|
|
31
|
+
{ status: 401 },
|
|
32
|
+
]);
|
|
33
|
+
expect("data" in new BridgeRequestError(1, "x")).toBe(false);
|
|
34
|
+
});
|
|
12
35
|
it("rejects unknown namespace", () => {
|
|
13
36
|
expect(isBridgeMessage({ ns: "btc", kind: "req", method: "x" })).toBe(false);
|
|
14
37
|
});
|
package/src/bridge/protocol.ts
CHANGED
|
@@ -1,12 +1,36 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Version of the wire protocol in docs/bridge-protocol.md. The WebView sends it
|
|
3
|
+
* in `sys.ready`; bump it only for a change an existing host cannot ignore.
|
|
4
|
+
*/
|
|
5
|
+
export const BRIDGE_PROTOCOL_VERSION = 1;
|
|
6
|
+
|
|
7
|
+
export const NAMESPACES = ["evm", "solana", "sui", "gpg", "auth", "sys"] as const;
|
|
2
8
|
export type Namespace = (typeof NAMESPACES)[number];
|
|
3
9
|
|
|
4
10
|
export const KINDS = ["req", "res", "event"] as const;
|
|
5
11
|
export type Kind = (typeof KINDS)[number];
|
|
6
12
|
|
|
13
|
+
/** Error codes a response can carry. EIP-1193 and JSON-RPC codes keep their meaning. */
|
|
14
|
+
export const BridgeErrorCode = {
|
|
15
|
+
userRejected: 4001,
|
|
16
|
+
unsupportedMethod: 4200,
|
|
17
|
+
methodNotFound: -32601,
|
|
18
|
+
invalidParams: -32602,
|
|
19
|
+
internal: -32000,
|
|
20
|
+
/** The page is not served over https (or loopback http), so it cannot sign in. */
|
|
21
|
+
pageNotHosted: -32001,
|
|
22
|
+
/** Keycloak refused the request; `data` carries `status`, `error`, `errorDescription`. */
|
|
23
|
+
keycloakRefused: -32002,
|
|
24
|
+
/** No wallet or signer for the requested family is available in the page. */
|
|
25
|
+
walletUnavailable: -32003,
|
|
26
|
+
/** The account has required actions left and no onboarding client was given. */
|
|
27
|
+
profileIncomplete: -32004,
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
7
30
|
export interface BridgeError {
|
|
8
31
|
code: number;
|
|
9
32
|
message: string;
|
|
33
|
+
data?: unknown;
|
|
10
34
|
}
|
|
11
35
|
|
|
12
36
|
export interface BridgeMessage {
|
|
@@ -20,6 +44,25 @@ export interface BridgeMessage {
|
|
|
20
44
|
error?: BridgeError;
|
|
21
45
|
}
|
|
22
46
|
|
|
47
|
+
/** What the WebView announces in `sys.ready`. */
|
|
48
|
+
export interface BridgeReady {
|
|
49
|
+
protocol: number;
|
|
50
|
+
namespaces: Namespace[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** An error that crosses the bridge with its code and data intact, in either direction. */
|
|
54
|
+
export class BridgeRequestError extends Error {
|
|
55
|
+
readonly code: number;
|
|
56
|
+
readonly data?: unknown;
|
|
57
|
+
|
|
58
|
+
constructor(code: number, message: string, data?: unknown) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.name = "BridgeRequestError";
|
|
61
|
+
this.code = code;
|
|
62
|
+
if (data !== undefined) this.data = data;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
23
66
|
export function isBridgeMessage(value: unknown): value is BridgeMessage {
|
|
24
67
|
if (typeof value !== "object" || value === null) return false;
|
|
25
68
|
const m = value as Record<string, unknown>;
|