@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,146 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthBrowserLogin,
|
|
3
|
+
AuthKeycloakConfig,
|
|
4
|
+
AuthOnboardingConfig,
|
|
5
|
+
AuthSignInParams,
|
|
6
|
+
AuthSignInResult,
|
|
7
|
+
AuthTokens,
|
|
8
|
+
AuthTokensChanged,
|
|
9
|
+
} from "./authTypes";
|
|
10
|
+
import type { BridgeClient, RequestOptions } from "./BridgeClient";
|
|
11
|
+
import { queryParams } from "./hosting";
|
|
12
|
+
import { BridgeErrorCode, BridgeRequestError } from "./protocol";
|
|
13
|
+
|
|
14
|
+
export interface AuthBridgeOptions {
|
|
15
|
+
/** Opens a URL in the system browser: `Linking.openURL` in React Native. */
|
|
16
|
+
openUrl(url: string): unknown;
|
|
17
|
+
/** Used when a call leaves `keycloak` or `onboarding` out. */
|
|
18
|
+
defaults?: { keycloak?: AuthKeycloakConfig; onboarding?: AuthOnboardingConfig };
|
|
19
|
+
/** Sign-in waits on the user in a wallet app. Default 10 minutes. */
|
|
20
|
+
signInTimeoutMs?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type AuthSignInOutcome = AuthSignInResult & {
|
|
24
|
+
/** Set on `profile-incomplete`: whether this call opened the onboarding page. */
|
|
25
|
+
onboardingOpened?: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export interface AuthSignInOptions extends RequestOptions {
|
|
29
|
+
/** Open Keycloak's onboarding in the system browser on `profile-incomplete`. Default true. */
|
|
30
|
+
openOnboarding?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Native side of the `auth` namespace. Sign-in runs in the hosted WebView
|
|
35
|
+
* page, so the Keycloak nonce cookie and the message domain belong to that
|
|
36
|
+
* page's host; native gets the tokens back.
|
|
37
|
+
*
|
|
38
|
+
* A direct grant cannot run Keycloak's required actions (a new user's profile
|
|
39
|
+
* form). The page then answers `profile-incomplete` with Keycloak's browser
|
|
40
|
+
* login, and `signIn` opens it in the system browser, once per call and never
|
|
41
|
+
* in a loop. After the user finishes there, the next `signIn` succeeds.
|
|
42
|
+
*/
|
|
43
|
+
export class AuthBridge {
|
|
44
|
+
private pending: AuthBrowserLogin | null = null;
|
|
45
|
+
|
|
46
|
+
constructor(
|
|
47
|
+
private readonly bridge: BridgeClient,
|
|
48
|
+
private readonly options: AuthBridgeOptions,
|
|
49
|
+
) {}
|
|
50
|
+
|
|
51
|
+
async signIn(
|
|
52
|
+
params: Partial<AuthSignInParams> = {},
|
|
53
|
+
options: AuthSignInOptions = {},
|
|
54
|
+
): Promise<AuthSignInOutcome> {
|
|
55
|
+
const { openOnboarding = true, ...request } = options;
|
|
56
|
+
const onboarding = params.onboarding ?? this.options.defaults?.onboarding;
|
|
57
|
+
const keycloak = this.keycloak(params.keycloak);
|
|
58
|
+
const result = (await this.bridge.request(
|
|
59
|
+
"auth",
|
|
60
|
+
"signIn",
|
|
61
|
+
{
|
|
62
|
+
family: params.family ?? "eth",
|
|
63
|
+
keycloak,
|
|
64
|
+
...(onboarding ? { onboarding } : {}),
|
|
65
|
+
},
|
|
66
|
+
{ timeoutMs: this.options.signInTimeoutMs ?? 10 * 60_000, ...request },
|
|
67
|
+
)) as AuthSignInResult;
|
|
68
|
+
if (result.status !== "profile-incomplete") {
|
|
69
|
+
this.pending = null;
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
// The page picks the URL the system browser opens, so it has to be this
|
|
73
|
+
// realm's authorization endpoint and nothing else (no intent:, no tel:).
|
|
74
|
+
if (!isAuthorizationUrl(result.browserLogin?.url, keycloak)) {
|
|
75
|
+
throw new BridgeRequestError(
|
|
76
|
+
BridgeErrorCode.internal,
|
|
77
|
+
"auth.signIn: the page's browser login is not the realm's authorization endpoint",
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
this.pending = result.browserLogin;
|
|
81
|
+
if (!openOnboarding) return { ...result, onboardingOpened: false };
|
|
82
|
+
await this.options.openUrl(result.browserLogin.url);
|
|
83
|
+
return { ...result, onboardingOpened: true };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async refresh(
|
|
87
|
+
params: { keycloak?: AuthKeycloakConfig; refreshToken?: string } = {},
|
|
88
|
+
): Promise<AuthTokens> {
|
|
89
|
+
const res = (await this.bridge.request("auth", "refresh", {
|
|
90
|
+
keycloak: this.keycloak(params.keycloak),
|
|
91
|
+
...(params.refreshToken ? { refreshToken: params.refreshToken } : {}),
|
|
92
|
+
})) as { tokens: AuthTokens };
|
|
93
|
+
return res.tokens;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async logout(
|
|
97
|
+
params: { keycloak?: AuthKeycloakConfig; refreshToken?: string } = {},
|
|
98
|
+
): Promise<void> {
|
|
99
|
+
await this.bridge.request("auth", "logout", {
|
|
100
|
+
keycloak: this.keycloak(params.keycloak),
|
|
101
|
+
...(params.refreshToken ? { refreshToken: params.refreshToken } : {}),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The browser login the last `profile-incomplete` answer carried, until a sign-in succeeds. */
|
|
106
|
+
get pendingOnboarding(): AuthBrowserLogin | null {
|
|
107
|
+
return this.pending;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* True when `url` is Keycloak sending the browser back from that onboarding
|
|
112
|
+
* (its `state` matches) with no `error`. A matching return, cancelled or
|
|
113
|
+
* not, ends the pending onboarding, so the same return never counts twice.
|
|
114
|
+
*/
|
|
115
|
+
isOnboardingReturn(url: string): boolean {
|
|
116
|
+
if (this.pending === null) return false;
|
|
117
|
+
const params = queryParams(url);
|
|
118
|
+
if (params.state !== this.pending.state) return false;
|
|
119
|
+
this.pending = null;
|
|
120
|
+
return params.error === undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** `tokensChanged` fires whenever the page's tokens for a realm and client change. */
|
|
124
|
+
onTokensChanged(handler: (change: AuthTokensChanged) => void): () => void {
|
|
125
|
+
return this.bridge.on((ns, method, params) => {
|
|
126
|
+
if (ns === "auth" && method === "tokensChanged") handler(params as AuthTokensChanged);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private keycloak(keycloak: AuthKeycloakConfig | undefined): AuthKeycloakConfig {
|
|
131
|
+
const resolved = keycloak ?? this.options.defaults?.keycloak;
|
|
132
|
+
if (!resolved) {
|
|
133
|
+
throw new BridgeRequestError(
|
|
134
|
+
BridgeErrorCode.invalidParams,
|
|
135
|
+
"auth: pass keycloak { baseUrl, realm, clientId } or set it in the provider defaults",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return resolved;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isAuthorizationUrl(url: unknown, keycloak: AuthKeycloakConfig): boolean {
|
|
143
|
+
if (typeof url !== "string") return false;
|
|
144
|
+
const realmUrl = `${keycloak.baseUrl.replace(/\/+$/, "")}/realms/${encodeURIComponent(keycloak.realm)}`;
|
|
145
|
+
return url.startsWith(`${realmUrl}/protocol/openid-connect/auth?`);
|
|
146
|
+
}
|
|
@@ -71,6 +71,18 @@ describe("BridgeClient events", () => {
|
|
|
71
71
|
]);
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
it("rejectPending() fails in-flight requests and keeps the client working", async () => {
|
|
75
|
+
const { a, b } = createMemoryTransportPair();
|
|
76
|
+
const client = new BridgeClient(a);
|
|
77
|
+
const lost = client.request("evm", "eth_requestAccounts");
|
|
78
|
+
client.rejectPending(new Error("page reloaded"));
|
|
79
|
+
await expect(lost).rejects.toThrow("page reloaded");
|
|
80
|
+
b.subscribe((m) => {
|
|
81
|
+
if (m.kind === "req") b.post({ id: m.id, ns: m.ns, kind: "res", result: "0x1" });
|
|
82
|
+
});
|
|
83
|
+
await expect(client.request("evm", "eth_chainId")).resolves.toBe("0x1");
|
|
84
|
+
});
|
|
85
|
+
|
|
74
86
|
it("dispose() rejects in-flight requests and stops events", async () => {
|
|
75
87
|
const { a, b } = createMemoryTransportPair();
|
|
76
88
|
const client = new BridgeClient(a);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
BridgeRequestError,
|
|
2
3
|
isBridgeMessage,
|
|
3
4
|
newRequestId,
|
|
4
5
|
type BridgeError,
|
|
@@ -79,15 +80,21 @@ export class BridgeClient {
|
|
|
79
80
|
};
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
/**
|
|
83
|
+
/** Fail every request in flight and keep the client usable: the page they went to is gone. */
|
|
84
|
+
rejectPending(reason: Error): void {
|
|
85
|
+
const pending = Array.from(this.pending.values());
|
|
86
|
+
this.pending.clear();
|
|
87
|
+
for (const p of pending) {
|
|
88
|
+
p.cleanup();
|
|
89
|
+
p.reject(reason);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Reject all in-flight requests and stop listening. */
|
|
83
94
|
dispose(): void {
|
|
84
95
|
this.disposed = true;
|
|
85
96
|
this.unsubscribe();
|
|
86
|
-
|
|
87
|
-
p.cleanup();
|
|
88
|
-
p.reject(new Error("Bridge disposed"));
|
|
89
|
-
}
|
|
90
|
-
this.pending.clear();
|
|
97
|
+
this.rejectPending(new Error("Bridge disposed"));
|
|
91
98
|
this.listeners.clear();
|
|
92
99
|
}
|
|
93
100
|
|
|
@@ -119,8 +126,6 @@ export class BridgeClient {
|
|
|
119
126
|
};
|
|
120
127
|
}
|
|
121
128
|
|
|
122
|
-
function toError(e: BridgeError):
|
|
123
|
-
|
|
124
|
-
err.code = e.code;
|
|
125
|
-
return err;
|
|
129
|
+
function toError(e: BridgeError): BridgeRequestError {
|
|
130
|
+
return new BridgeRequestError(e.code, e.message, e.data);
|
|
126
131
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { BridgeClient } from "./BridgeClient";
|
|
3
3
|
import { BridgeHost } from "./BridgeHost";
|
|
4
|
+
import { BridgeRequestError } from "./protocol";
|
|
4
5
|
import { createMemoryTransportPair } from "./transport";
|
|
5
6
|
|
|
6
7
|
/** Pair a native client (a) with a webview host (b) over the in-memory transport. */
|
|
@@ -60,13 +61,28 @@ describe("BridgeHost", () => {
|
|
|
60
61
|
expect(seen).toEqual([["evm", "accountsChanged", ["0xabc"]]]);
|
|
61
62
|
});
|
|
62
63
|
|
|
63
|
-
it("ready() emits a sys ready event", () => {
|
|
64
|
+
it("ready() emits a sys ready event with the protocol version and namespaces", () => {
|
|
64
65
|
const { client, host } = pair();
|
|
65
66
|
const onReady = vi.fn();
|
|
66
|
-
client.on((ns, method) => {
|
|
67
|
-
if (ns === "sys" && method === "ready") onReady();
|
|
67
|
+
client.on((ns, method, params) => {
|
|
68
|
+
if (ns === "sys" && method === "ready") onReady(params);
|
|
68
69
|
});
|
|
70
|
+
host.register("evm", () => null);
|
|
71
|
+
host.register("auth", () => null);
|
|
69
72
|
host.ready();
|
|
70
|
-
expect(onReady).
|
|
73
|
+
expect(onReady).toHaveBeenCalledExactlyOnceWith({
|
|
74
|
+
protocol: 1,
|
|
75
|
+
namespaces: ["evm", "auth", "sys"],
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("carries an error's data to the client", async () => {
|
|
80
|
+
const { client, host } = pair();
|
|
81
|
+
host.register("auth", () => {
|
|
82
|
+
throw new BridgeRequestError(-32002, "Invalid user credentials", { status: 401 });
|
|
83
|
+
});
|
|
84
|
+
const error = await client.request("auth", "signIn").catch((e: unknown) => e);
|
|
85
|
+
expect(error).toBeInstanceOf(BridgeRequestError);
|
|
86
|
+
expect(error).toMatchObject({ code: -32002, data: { status: 401 } });
|
|
71
87
|
});
|
|
72
88
|
});
|
package/src/bridge/BridgeHost.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
BRIDGE_PROTOCOL_VERSION,
|
|
3
|
+
isBridgeMessage,
|
|
4
|
+
type BridgeError,
|
|
5
|
+
type BridgeMessage,
|
|
6
|
+
type BridgeReady,
|
|
7
|
+
type Namespace,
|
|
8
|
+
} from "./protocol";
|
|
2
9
|
import type { Transport } from "./transport";
|
|
3
10
|
|
|
4
11
|
/**
|
|
@@ -31,9 +38,11 @@ export class BridgeHost {
|
|
|
31
38
|
this.transport.post({ ns, kind: "event", method, params });
|
|
32
39
|
}
|
|
33
40
|
|
|
34
|
-
/** Tell native the host is initialized. */
|
|
41
|
+
/** Tell native the host is initialized, which protocol it speaks and what it serves. */
|
|
35
42
|
ready(): void {
|
|
36
|
-
this.
|
|
43
|
+
const namespaces = Array.from(new Set<Namespace>([...this.handlers.keys(), "sys"]));
|
|
44
|
+
const params: BridgeReady = { protocol: BRIDGE_PROTOCOL_VERSION, namespaces };
|
|
45
|
+
this.transport.post({ ns: "sys", kind: "event", method: "ready", params });
|
|
37
46
|
}
|
|
38
47
|
|
|
39
48
|
dispose(): void {
|
|
@@ -63,11 +72,13 @@ export class BridgeHost {
|
|
|
63
72
|
|
|
64
73
|
function toBridgeError(e: unknown): BridgeError {
|
|
65
74
|
if (e && typeof e === "object") {
|
|
66
|
-
const o = e as { code?: unknown; message?: unknown };
|
|
67
|
-
|
|
75
|
+
const o = e as { code?: unknown; message?: unknown; data?: unknown };
|
|
76
|
+
const error: BridgeError = {
|
|
68
77
|
code: typeof o.code === "number" ? o.code : -32000,
|
|
69
78
|
message: typeof o.message === "string" ? o.message : String(e),
|
|
70
79
|
};
|
|
80
|
+
if (o.data !== undefined) error.data = o.data;
|
|
81
|
+
return error;
|
|
71
82
|
}
|
|
72
83
|
return { code: -32000, message: String(e) };
|
|
73
84
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { BridgeClient, RequestOptions } from "./BridgeClient";
|
|
2
|
+
|
|
3
|
+
/** A signature the WebView is waiting for: the message and the terminal command that signs it. */
|
|
4
|
+
export interface GpgPendingRequest {
|
|
5
|
+
message: string;
|
|
6
|
+
command: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// The user signs in a terminal and pastes the output back, which takes longer
|
|
10
|
+
// than a wallet tap.
|
|
11
|
+
const GPG_TIMEOUT_MS = 15 * 60_000;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Native-side proxy for the WebView's copy/paste GPG signer. `signMessage`
|
|
15
|
+
* parks a request in the page, which shows the command and a paste box; a
|
|
16
|
+
* native host that draws its own paste box calls `submit` instead.
|
|
17
|
+
*/
|
|
18
|
+
export class GpgBridge {
|
|
19
|
+
constructor(private readonly bridge: BridgeClient) {}
|
|
20
|
+
|
|
21
|
+
/** Resolves with the armored signature followed by the armored public key. */
|
|
22
|
+
async signMessage(message: string, options: RequestOptions = {}): Promise<string> {
|
|
23
|
+
const res = (await this.bridge.request(
|
|
24
|
+
"gpg",
|
|
25
|
+
"signMessage",
|
|
26
|
+
{ message },
|
|
27
|
+
{ timeoutMs: GPG_TIMEOUT_MS, ...options },
|
|
28
|
+
)) as { signature: string };
|
|
29
|
+
return res.signature;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Hand pasted terminal output to the pending request. Rejects when it does not cover its message. */
|
|
33
|
+
async submit(signature: string): Promise<void> {
|
|
34
|
+
await this.bridge.request("gpg", "submit", { signature });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async cancel(): Promise<void> {
|
|
38
|
+
await this.bridge.request("gpg", "cancel");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Called with the pending request whenever one is parked, and with null when it settles. */
|
|
42
|
+
onPendingChanged(handler: (request: GpgPendingRequest | null) => void): () => void {
|
|
43
|
+
return this.bridge.on((ns, method, params) => {
|
|
44
|
+
if (ns === "gpg" && method === "pendingChanged") {
|
|
45
|
+
handler((params as { request: GpgPendingRequest | null } | undefined)?.request ?? null);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { BridgeClient, RequestOptions } from "./BridgeClient";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Native-side proxy for the WebView's Sui wallet. Messages are UTF-8 text; the
|
|
5
|
+
* signature is Sui's serialized signature (flag || signature || public key),
|
|
6
|
+
* base64, which is what the Keycloak Sui verifier takes as the login username.
|
|
7
|
+
*/
|
|
8
|
+
export class SuiBridge {
|
|
9
|
+
constructor(private readonly bridge: BridgeClient) {}
|
|
10
|
+
|
|
11
|
+
async connect(options?: RequestOptions): Promise<{ address: string }> {
|
|
12
|
+
return (await this.bridge.request("sui", "connect", undefined, options)) as {
|
|
13
|
+
address: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async disconnect(): Promise<void> {
|
|
18
|
+
await this.bridge.request("sui", "disconnect");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async signMessage(message: string, options?: RequestOptions): Promise<string> {
|
|
22
|
+
const res = (await this.bridge.request("sui", "signMessage", { message }, options)) as {
|
|
23
|
+
signature: string;
|
|
24
|
+
};
|
|
25
|
+
return res.signature;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Subscribe to sui events (`accountChanged`). */
|
|
29
|
+
on(handler: (event: string, params: unknown) => void): () => void {
|
|
30
|
+
return this.bridge.on((ns, method, params) => {
|
|
31
|
+
if (ns === "sui") handler(method, params);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** Who signs an `auth.signIn`: the message families the Keycloak wallet SPIs verify. */
|
|
2
|
+
export type AuthFamily = "eth" | "sol" | "sui" | "gpg";
|
|
3
|
+
|
|
4
|
+
export const AUTH_FAMILIES: readonly AuthFamily[] = ["eth", "sol", "sui", "gpg"];
|
|
5
|
+
|
|
6
|
+
export interface AuthKeycloakConfig {
|
|
7
|
+
/** e.g. `https://account.example.com`, or `http://localhost:8080/auth` in dev. */
|
|
8
|
+
baseUrl: string;
|
|
9
|
+
realm: string;
|
|
10
|
+
/** The wallet SPI's REST resource: `siww` on the SHC Keycloak (default), `wallet` on mpo's. */
|
|
11
|
+
resource?: "siww" | "wallet";
|
|
12
|
+
/** A public client with direct access grants enabled. */
|
|
13
|
+
clientId: string;
|
|
14
|
+
/** Default `openid`. */
|
|
15
|
+
scope?: string | string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The client Keycloak's own browser login runs under when the account still
|
|
20
|
+
* has required actions: standard flow on, `redirectUri` registered exactly.
|
|
21
|
+
*/
|
|
22
|
+
export interface AuthOnboardingConfig {
|
|
23
|
+
clientId: string;
|
|
24
|
+
/** Where Keycloak sends the system browser back, usually the app's deep link. */
|
|
25
|
+
redirectUri: string;
|
|
26
|
+
scope?: string | string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface AuthTokens {
|
|
30
|
+
accessToken: string;
|
|
31
|
+
refreshToken?: string;
|
|
32
|
+
idToken?: string;
|
|
33
|
+
tokenType: string;
|
|
34
|
+
scope?: string;
|
|
35
|
+
/** Epoch ms. */
|
|
36
|
+
expiresAt: number;
|
|
37
|
+
/** Epoch ms; absent for offline or non-expiring refresh tokens. */
|
|
38
|
+
refreshExpiresAt?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface AuthSignInParams {
|
|
42
|
+
/** Default `eth`. */
|
|
43
|
+
family?: AuthFamily;
|
|
44
|
+
keycloak: AuthKeycloakConfig;
|
|
45
|
+
onboarding?: AuthOnboardingConfig;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Keycloak's authorization-code login with a fresh `state` and PKCE S256 verifier. */
|
|
49
|
+
export interface AuthBrowserLogin {
|
|
50
|
+
url: string;
|
|
51
|
+
state: string;
|
|
52
|
+
codeVerifier: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type AuthSignInResult =
|
|
56
|
+
| { status: "signed-in"; family: AuthFamily; address: string | null; tokens: AuthTokens }
|
|
57
|
+
| {
|
|
58
|
+
status: "profile-incomplete";
|
|
59
|
+
family: AuthFamily;
|
|
60
|
+
address: string | null;
|
|
61
|
+
browserLogin: AuthBrowserLogin;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export interface AuthSessionParams {
|
|
65
|
+
keycloak: AuthKeycloakConfig;
|
|
66
|
+
/** Defaults to the refresh token of the page's last sign-in for this realm and client. */
|
|
67
|
+
refreshToken?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** `auth.tokensChanged` event params. */
|
|
71
|
+
export interface AuthTokensChanged {
|
|
72
|
+
baseUrl: string;
|
|
73
|
+
realm: string;
|
|
74
|
+
clientId: string;
|
|
75
|
+
tokens: AuthTokens | null;
|
|
76
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// The Hermes-safe bridge surface, without the wagmi connector: what a React
|
|
2
|
+
// Native host needs to drive the hosted wallet page (`@multiplatform.one/web3/bridge`).
|
|
3
|
+
export * from "./protocol";
|
|
4
|
+
export * from "./transport";
|
|
5
|
+
export * from "./authTypes";
|
|
6
|
+
export { createPostMessageTransport, type PostMessageTransport } from "./postMessageTransport";
|
|
7
|
+
export { BridgeClient, type EventHandler, type RequestOptions } from "./BridgeClient";
|
|
8
|
+
export { BridgeHost, type RequestHandler } from "./BridgeHost";
|
|
9
|
+
export { createBridgeProvider, type BridgeProvider, type Eip1193RequestArgs } from "./eip1193";
|
|
10
|
+
export { bytesToBase64, base64ToBytes } from "./base64";
|
|
11
|
+
export { SolanaBridge } from "./SolanaBridge";
|
|
12
|
+
export { SuiBridge } from "./SuiBridge";
|
|
13
|
+
export { GpgBridge, type GpgPendingRequest } from "./GpgBridge";
|
|
14
|
+
export {
|
|
15
|
+
AuthBridge,
|
|
16
|
+
type AuthBridgeOptions,
|
|
17
|
+
type AuthSignInOptions,
|
|
18
|
+
type AuthSignInOutcome,
|
|
19
|
+
} from "./AuthBridge";
|
|
20
|
+
export {
|
|
21
|
+
DEV_HOSTNAMES,
|
|
22
|
+
hostedPageOrigin,
|
|
23
|
+
isHostedPageUrl,
|
|
24
|
+
parseOrigin,
|
|
25
|
+
queryParams,
|
|
26
|
+
type ParsedOrigin,
|
|
27
|
+
} from "./hosting";
|
|
28
|
+
export {
|
|
29
|
+
buildInjectScript,
|
|
30
|
+
createNativeBridge,
|
|
31
|
+
type NativeBridge,
|
|
32
|
+
type NativeBridgeOptions,
|
|
33
|
+
} from "./nativeHost";
|
|
34
|
+
export {
|
|
35
|
+
decideNavigation,
|
|
36
|
+
decideNewWindow,
|
|
37
|
+
parseWalletReturn,
|
|
38
|
+
shouldOpenExternally,
|
|
39
|
+
WALLET_SCHEMES,
|
|
40
|
+
WALLET_UNIVERSAL_HOSTS,
|
|
41
|
+
type NavigationDecision,
|
|
42
|
+
} from "../providers/native/deepLink";
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { hostedPageOrigin, isHostedPageUrl, parseOrigin, queryParams } from "./hosting";
|
|
3
|
+
|
|
4
|
+
describe("parseOrigin", () => {
|
|
5
|
+
it("lowercases and drops the default port", () => {
|
|
6
|
+
expect(parseOrigin("HTTPS://Wallet.Example.com:443/webview.html?x=1")).toEqual({
|
|
7
|
+
scheme: "https",
|
|
8
|
+
host: "wallet.example.com",
|
|
9
|
+
hostname: "wallet.example.com",
|
|
10
|
+
origin: "https://wallet.example.com",
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("keeps a non-default port, strips userinfo and handles IPv6", () => {
|
|
15
|
+
expect(parseOrigin("http://user:pw@localhost:8099/webview.html")?.origin).toBe(
|
|
16
|
+
"http://localhost:8099",
|
|
17
|
+
);
|
|
18
|
+
expect(parseOrigin("http://[::1]:8099/")?.hostname).toBe("[::1]");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("ends the authority at a backslash, as browsers do", () => {
|
|
22
|
+
expect(parseOrigin("http://evil.com\\@localhost:8099/")?.origin).toBe("http://evil.com");
|
|
23
|
+
expect(isHostedPageUrl("http://evil.com\\@localhost:8099/", { allowDevHosts: true })).toBe(
|
|
24
|
+
false,
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("gives schemes without an authority an empty host", () => {
|
|
29
|
+
expect(parseOrigin("about:blank")).toEqual({
|
|
30
|
+
scheme: "about",
|
|
31
|
+
host: "",
|
|
32
|
+
hostname: "",
|
|
33
|
+
origin: "about:",
|
|
34
|
+
});
|
|
35
|
+
expect(parseOrigin("metamask://connect")?.hostname).toBe("connect");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("returns null for text that is not a URL", () => {
|
|
39
|
+
expect(parseOrigin("not a url")).toBeNull();
|
|
40
|
+
expect(parseOrigin("")).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("isHostedPageUrl", () => {
|
|
45
|
+
it("accepts https pages", () => {
|
|
46
|
+
expect(isHostedPageUrl("https://wallet.example.com/webview.html")).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("accepts plain http only on development hosts, and only when asked to", () => {
|
|
50
|
+
const dev = { allowDevHosts: true };
|
|
51
|
+
expect(isHostedPageUrl("http://localhost:8099/webview.html", dev)).toBe(true);
|
|
52
|
+
expect(isHostedPageUrl("http://127.0.0.1:8099/webview.html", dev)).toBe(true);
|
|
53
|
+
expect(isHostedPageUrl("http://10.0.2.2:8099/webview.html", dev)).toBe(true);
|
|
54
|
+
expect(isHostedPageUrl("http://wallet.example.com/webview.html", dev)).toBe(false);
|
|
55
|
+
expect(isHostedPageUrl("http://192.168.1.20:8099/webview.html", dev)).toBe(false);
|
|
56
|
+
expect(isHostedPageUrl("http://localhost:8099/webview.html")).toBe(false);
|
|
57
|
+
expect(isHostedPageUrl("http://10.0.2.2:8099/webview.html")).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("refuses pages with no host", () => {
|
|
61
|
+
expect(isHostedPageUrl("file:///android_asset/webview.html")).toBe(false);
|
|
62
|
+
expect(isHostedPageUrl("data:text/html,<p>hi</p>")).toBe(false);
|
|
63
|
+
expect(isHostedPageUrl("about:blank")).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("hostedPageOrigin", () => {
|
|
68
|
+
it("returns the origin of a hosted page", () => {
|
|
69
|
+
expect(hostedPageOrigin("https://wallet.example.com/a/webview.html")).toBe(
|
|
70
|
+
"https://wallet.example.com",
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("throws naming the allowlist for anything else", () => {
|
|
75
|
+
expect(() => hostedPageOrigin("file:///webview.html")).toThrow(/siww\.allowedDomains/);
|
|
76
|
+
expect(() => hostedPageOrigin("http://localhost:8099/webview.html")).toThrow(
|
|
77
|
+
/only in development builds/,
|
|
78
|
+
);
|
|
79
|
+
expect(hostedPageOrigin("http://localhost:8099/webview.html", { allowDevHosts: true })).toBe(
|
|
80
|
+
"http://localhost:8099",
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("queryParams", () => {
|
|
86
|
+
it("decodes keys and values and keeps the first of a repeated name", () => {
|
|
87
|
+
expect(queryParams("app://cb?state=a%2Fb&code=x+y&state=later#frag")).toEqual({
|
|
88
|
+
state: "a/b",
|
|
89
|
+
code: "x y",
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("does not let a key reach the prototype", () => {
|
|
94
|
+
const params = queryParams("app://cb?__proto__=x&constructor=y");
|
|
95
|
+
expect(Object.getPrototypeOf(params)).toBe(Object.prototype);
|
|
96
|
+
expect(params.constructor).toBe("y");
|
|
97
|
+
});
|
|
98
|
+
});
|