@openfort/react 1.0.6 → 1.0.8
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/build/assets/icons.js +1 -1
- package/build/components/Common/WalletRecoveryIcon/index.d.ts +1 -0
- package/build/components/Common/WalletRecoveryIcon/index.js +6 -1
- package/build/components/Common/WalletRecoveryIcon/index.js.map +1 -1
- package/build/components/ConnectModal/ConnectWithInjector/index.js +4 -4
- package/build/components/ConnectModal/ConnectWithMobile.js +10 -8
- package/build/components/ConnectModal/ConnectWithMobile.js.map +1 -1
- package/build/components/ConnectModal/ConnectWithOAuth.js +5 -1
- package/build/components/ConnectModal/ConnectWithOAuth.js.map +1 -1
- package/build/components/ConnectModal/index.js +3 -3
- package/build/components/Openfort/OpenfortProvider.js +4 -0
- package/build/components/Openfort/OpenfortProvider.js.map +1 -1
- package/build/components/Pages/Connected/EthereumConnected.js +7 -4
- package/build/components/Pages/Connected/EthereumConnected.js.map +1 -1
- package/build/components/Pages/Connected/SolanaConnected.js +1 -1
- package/build/components/Pages/Connectors/index.js +1 -55
- package/build/components/Pages/Connectors/index.js.map +1 -1
- package/build/components/Pages/LinkedProvider/index.js +7 -2
- package/build/components/Pages/LinkedProvider/index.js.map +1 -1
- package/build/components/Pages/LoadWallets/index.js +35 -28
- package/build/components/Pages/LoadWallets/index.js.map +1 -1
- package/build/components/Pages/RemoveLinkedProvider/index.js +5 -1
- package/build/components/Pages/RemoveLinkedProvider/index.js.map +1 -1
- package/build/components/Pages/SelectWalletToRecover/index.js +41 -3
- package/build/components/Pages/SelectWalletToRecover/index.js.map +1 -1
- package/build/ethereum/hooks/useEthereumEmbeddedWallet.js +5 -1
- package/build/ethereum/hooks/useEthereumEmbeddedWallet.js.map +1 -1
- package/build/hooks/openfort/auth/useAuthCallback.d.ts +1 -1
- package/build/hooks/openfort/auth/useAuthCallback.js +24 -8
- package/build/hooks/openfort/auth/useAuthCallback.js.map +1 -1
- package/build/hooks/useResolvedIdentity.js +4 -1
- package/build/hooks/useResolvedIdentity.js.map +1 -1
- package/build/openfort/CoreOpenfortProvider.js +1 -0
- package/build/openfort/CoreOpenfortProvider.js.map +1 -1
- package/build/openfort/hooks/useActiveAddressSync.d.ts +7 -3
- package/build/openfort/hooks/useActiveAddressSync.js +37 -7
- package/build/openfort/hooks/useActiveAddressSync.js.map +1 -1
- package/build/solana/hooks/useSolanaEmbeddedWallet.js +1 -1
- package/build/utils/rpc.d.ts +0 -4
- package/build/utils/rpc.js +5 -1
- package/build/utils/rpc.js.map +1 -1
- package/build/utils/urlSecurity.d.ts +28 -0
- package/build/utils/urlSecurity.js +56 -0
- package/build/utils/urlSecurity.js.map +1 -0
- package/build/version.d.ts +1 -1
- package/build/version.js +1 -1
- package/build/wagmi/useConnectWithSiwe.js +35 -29
- package/build/wagmi/useConnectWithSiwe.js.map +1 -1
- package/build/wagmi/useEmbeddedWalletWagmiSync.js +21 -1
- package/build/wagmi/useEmbeddedWalletWagmiSync.js.map +1 -1
- package/build/wagmi/useWalletAuth.js +7 -0
- package/build/wagmi/useWalletAuth.js.map +1 -1
- package/package.json +2 -3
|
@@ -6,19 +6,50 @@ import { toSolanaUserWallet } from '../../../hooks/openfort/walletTypes.js';
|
|
|
6
6
|
import { useResolvedIdentity } from '../../../hooks/useResolvedIdentity.js';
|
|
7
7
|
import { useOpenfortCore } from '../../../openfort/useOpenfort.js';
|
|
8
8
|
import { useSolanaEmbeddedWallet } from '../../../solana/hooks/useSolanaEmbeddedWallet.js';
|
|
9
|
+
import styled from '../../../styles/styled/index.js';
|
|
9
10
|
import 'detect-browser';
|
|
10
11
|
import 'react';
|
|
11
12
|
import { truncateEthAddress } from '../../../utils/format.js';
|
|
12
13
|
import { walletConfigs } from '../../../wallets/walletConfigs.js';
|
|
13
14
|
import Button from '../../Common/Button/index.js';
|
|
14
15
|
import { ModalHeading } from '../../Common/Modal/styles.js';
|
|
15
|
-
import { WalletRecoveryIcon } from '../../Common/WalletRecoveryIcon/index.js';
|
|
16
|
+
import { RECOVERY_METHOD_LABEL, WalletRecoveryIcon } from '../../Common/WalletRecoveryIcon/index.js';
|
|
16
17
|
import { recoverRoute } from '../../Openfort/routeHelpers.js';
|
|
17
18
|
import { routes } from '../../Openfort/types.js';
|
|
18
19
|
import { useOpenfort } from '../../Openfort/useOpenfort.js';
|
|
19
20
|
import { PageContent } from '../../PageContent/index.js';
|
|
20
21
|
import { ProvidersButton, ProviderLabel, ProviderIcon } from '../Providers/styles.js';
|
|
21
22
|
|
|
23
|
+
const RecoveryTag = styled.span `
|
|
24
|
+
display: inline-flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
padding: 2px 8px;
|
|
27
|
+
border-radius: 6px;
|
|
28
|
+
font-size: 11px;
|
|
29
|
+
font-weight: 600;
|
|
30
|
+
line-height: 16px;
|
|
31
|
+
white-space: nowrap;
|
|
32
|
+
background: var(--ck-body-background-secondary, #f0f0f0);
|
|
33
|
+
color: var(--ck-body-color-muted, #999);
|
|
34
|
+
`;
|
|
35
|
+
const WalletListScroll = styled.div `
|
|
36
|
+
max-height: min(400px, 50vh);
|
|
37
|
+
overflow-x: hidden;
|
|
38
|
+
overflow-y: auto;
|
|
39
|
+
|
|
40
|
+
&::-webkit-scrollbar {
|
|
41
|
+
width: 4px;
|
|
42
|
+
}
|
|
43
|
+
&::-webkit-scrollbar-track {
|
|
44
|
+
background: transparent;
|
|
45
|
+
}
|
|
46
|
+
&::-webkit-scrollbar-thumb {
|
|
47
|
+
background: var(--ck-body-color-muted, #c47a2a);
|
|
48
|
+
border-radius: 4px;
|
|
49
|
+
}
|
|
50
|
+
scrollbar-width: thin;
|
|
51
|
+
scrollbar-color: var(--ck-body-color-muted, #c47a2a) transparent;
|
|
52
|
+
`;
|
|
22
53
|
function WalletRow({ chainType, wallet, }) {
|
|
23
54
|
var _a;
|
|
24
55
|
const { setRoute } = useOpenfort();
|
|
@@ -54,16 +85,23 @@ function WalletRow({ chainType, wallet, }) {
|
|
|
54
85
|
}
|
|
55
86
|
setRoute(recoverRoute(chainType, wallet));
|
|
56
87
|
};
|
|
57
|
-
|
|
88
|
+
const tag = wallet.recoveryMethod != null ? RECOVERY_METHOD_LABEL[wallet.recoveryMethod] : undefined;
|
|
89
|
+
return (jsx(ProvidersButton, { children: jsxs(Button, { onClick: handleClick, children: [jsxs(ProviderLabel, { children: [display, tag && jsx(RecoveryTag, { children: tag })] }), jsx(ProviderIcon, { children: walletIcon() })] }) }));
|
|
58
90
|
}
|
|
91
|
+
/** Connected-page routes that indicate the user navigated here from "Manage wallets" */
|
|
92
|
+
const CONNECTED_ROUTES = new Set([routes.CONNECTED, routes.ETH_CONNECTED, routes.SOL_CONNECTED]);
|
|
59
93
|
function SelectWalletToRecover() {
|
|
60
94
|
const { chainType } = useOpenfortCore();
|
|
95
|
+
const { previousRoute } = useOpenfort();
|
|
61
96
|
const ethereumWallet = useEthereumEmbeddedWallet();
|
|
62
97
|
const solanaWallet = useSolanaEmbeddedWallet();
|
|
63
98
|
const embeddedWallet = chainType === ChainTypeEnum.EVM ? ethereumWallet : solanaWallet;
|
|
64
99
|
const wallets = embeddedWallet.wallets;
|
|
65
100
|
const list = wallets.map((wallet) => jsx(WalletRow, { chainType: chainType, wallet: wallet }, wallet.id));
|
|
66
|
-
|
|
101
|
+
// When arriving from a connected page (Manage wallets), go back there without logging out.
|
|
102
|
+
// When arriving from the login flow, go back to providers and log out.
|
|
103
|
+
const fromConnected = previousRoute != null && CONNECTED_ROUTES.has(previousRoute.route);
|
|
104
|
+
return (jsxs(PageContent, { onBack: fromConnected ? 'back' : routes.PROVIDERS, logoutOnBack: !fromConnected, children: [jsx(ModalHeading, { children: "Select a wallet" }), jsx(WalletListScroll, { children: list })] }));
|
|
67
105
|
}
|
|
68
106
|
|
|
69
107
|
export { SelectWalletToRecover as default };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -139,6 +139,11 @@ function useEthereumEmbeddedWallet(options) {
|
|
|
139
139
|
...(accountType !== DEFAULT_ACCOUNT_TYPE && { chainId: (_d = createOptions === null || createOptions === void 0 ? void 0 : createOptions.chainId) !== null && _d !== void 0 ? _d : creationChainId }),
|
|
140
140
|
recoveryParams,
|
|
141
141
|
});
|
|
142
|
+
// Set the address before fetching accounts or setting connected state.
|
|
143
|
+
// This prevents a race where the sync effect sees status='connected' but
|
|
144
|
+
// no activeEmbeddedAddress and disconnects (especially for the first wallet
|
|
145
|
+
// when embeddedAccounts is still empty).
|
|
146
|
+
setActiveEmbeddedAddress(account.address);
|
|
142
147
|
await updateEmbeddedAccounts({ silent: true });
|
|
143
148
|
const provider = await getEmbeddedEthereumProvider();
|
|
144
149
|
const connectedWallet = buildConnectedWallet(account, 0, async () => provider, {
|
|
@@ -151,7 +156,6 @@ function useEthereumEmbeddedWallet(options) {
|
|
|
151
156
|
provider,
|
|
152
157
|
error: null,
|
|
153
158
|
});
|
|
154
|
-
setActiveEmbeddedAddress(account.address);
|
|
155
159
|
(_e = createOptions === null || createOptions === void 0 ? void 0 : createOptions.onSuccess) === null || _e === void 0 ? void 0 : _e.call(createOptions, { account });
|
|
156
160
|
return account;
|
|
157
161
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useEthereumEmbeddedWallet.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useEthereumEmbeddedWallet.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { UIAuthProvider } from '../../../components/Openfort/types';
|
|
2
2
|
import { OpenfortError } from '../../../core/errors';
|
|
3
3
|
import type { OpenfortHookOptions } from '../../../types';
|
|
4
4
|
import type { CreateWalletPostAuthOptions } from './useConnectToWalletPostAuth';
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { UIAuthProvider } from '../../../components/Openfort/types.js';
|
|
2
3
|
import { OpenfortError, OpenfortReactErrorType } from '../../../types.js';
|
|
3
4
|
import { logger } from '../../../utils/logger.js';
|
|
5
|
+
import { parseCallbackUrl, suppressReferrer } from '../../../utils/urlSecurity.js';
|
|
4
6
|
import { useEmailAuth } from './useEmailAuth.js';
|
|
5
7
|
import { useOAuth } from './useOAuth.js';
|
|
6
8
|
|
|
@@ -79,17 +81,27 @@ const useAuthCallback = ({ enabled = true, // Automatically handle OAuth and ema
|
|
|
79
81
|
if (callbackProcessedRef.current)
|
|
80
82
|
return;
|
|
81
83
|
callbackProcessedRef.current = true;
|
|
84
|
+
// Parse callback URL (fixes OF-1013 duplicate `?` issue)
|
|
85
|
+
const url = parseCallbackUrl(window.location.href);
|
|
86
|
+
const rawProvider = url.searchParams.get('openfortAuthProvider');
|
|
87
|
+
// Allowlist: UIAuthProvider values + callback-only providers set by buildCallbackUrl
|
|
88
|
+
const validProviders = new Set([
|
|
89
|
+
...Object.values(UIAuthProvider),
|
|
90
|
+
'email', // set by buildCallbackUrl for email verification
|
|
91
|
+
'password', // set by buildCallbackUrl for password reset
|
|
92
|
+
]);
|
|
93
|
+
if (!rawProvider || !validProviders.has(rawProvider)) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// Validated against the allowlist above
|
|
97
|
+
const openfortAuthProvider = rawProvider;
|
|
98
|
+
// Suppress Referer SYNCHRONOUSLY — before any async work — so that
|
|
99
|
+
// subresource requests cannot leak access_token to third parties.
|
|
100
|
+
const restoreReferrer = suppressReferrer();
|
|
82
101
|
(async () => {
|
|
83
102
|
var _a, _b, _c;
|
|
84
|
-
// redirectUrl is not working with query params OF-1013
|
|
85
|
-
const fixedUrl = window.location.href.replace('?state=', '&state=');
|
|
86
|
-
const url = new URL(fixedUrl);
|
|
87
|
-
const openfortAuthProvider = url.searchParams.get('openfortAuthProvider');
|
|
88
|
-
if (!openfortAuthProvider) {
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
103
|
setProvider(openfortAuthProvider);
|
|
92
|
-
if (openfortAuthProvider === 'email') {
|
|
104
|
+
if (openfortAuthProvider === 'email' || openfortAuthProvider === 'password') {
|
|
93
105
|
// Email verification flow
|
|
94
106
|
// The backend verifies the email server-side via /auth/verify-email?token=...
|
|
95
107
|
// and then redirects here. If a `state` token is present we verify client-side
|
|
@@ -101,6 +113,7 @@ const useAuthCallback = ({ enabled = true, // Automatically handle OAuth and ema
|
|
|
101
113
|
url.searchParams.delete(key);
|
|
102
114
|
});
|
|
103
115
|
window.history.replaceState({}, document.title, url.toString());
|
|
116
|
+
restoreReferrer();
|
|
104
117
|
};
|
|
105
118
|
if (state && email) {
|
|
106
119
|
// State present — verify client-side as well
|
|
@@ -130,6 +143,7 @@ const useAuthCallback = ({ enabled = true, // Automatically handle OAuth and ema
|
|
|
130
143
|
removeParams();
|
|
131
144
|
}
|
|
132
145
|
else {
|
|
146
|
+
restoreReferrer();
|
|
133
147
|
const err = new OpenfortError('No email found in URL', OpenfortReactErrorType.AUTHENTICATION_ERROR);
|
|
134
148
|
logger.error('No email found in URL');
|
|
135
149
|
(_b = hookOptions.onError) === null || _b === void 0 ? void 0 : _b.call(hookOptions, err);
|
|
@@ -142,6 +156,7 @@ const useAuthCallback = ({ enabled = true, // Automatically handle OAuth and ema
|
|
|
142
156
|
const userId = url.searchParams.get('user_id');
|
|
143
157
|
const token = url.searchParams.get('access_token');
|
|
144
158
|
if (!userId || !token) {
|
|
159
|
+
restoreReferrer();
|
|
145
160
|
logger.error(`Missing user id or access token`, {
|
|
146
161
|
hasUserId: !!userId,
|
|
147
162
|
hasToken: !!token,
|
|
@@ -157,6 +172,7 @@ const useAuthCallback = ({ enabled = true, // Automatically handle OAuth and ema
|
|
|
157
172
|
url.searchParams.delete(key);
|
|
158
173
|
});
|
|
159
174
|
window.history.replaceState({}, document.title, url.toString());
|
|
175
|
+
restoreReferrer();
|
|
160
176
|
};
|
|
161
177
|
logger.log('callback', { userId });
|
|
162
178
|
const options = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAuthCallback.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useAuthCallback.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -61,7 +61,10 @@ function useResolvedIdentity(options) {
|
|
|
61
61
|
var _a, _b, _c, _d, _e;
|
|
62
62
|
const { address, chainType = ChainTypeEnum.EVM, ensChainId = 0, enabled = true } = options;
|
|
63
63
|
const { walletConfig } = useOpenfort();
|
|
64
|
-
|
|
64
|
+
// Only resolve RPC URL for mainnet (ensChainId === 1) — chainId 0 means "do not resolve"
|
|
65
|
+
const rpcUrl = ensChainId === 1
|
|
66
|
+
? ((_c = (_b = (_a = walletConfig === null || walletConfig === void 0 ? void 0 : walletConfig.ethereum) === null || _a === void 0 ? void 0 : _a.rpcUrls) === null || _b === void 0 ? void 0 : _b[ensChainId]) !== null && _c !== void 0 ? _c : getDefaultEthereumRpcUrl(ensChainId))
|
|
67
|
+
: undefined;
|
|
65
68
|
const isEnabled = enabled && !!address && address.length > 0 && ensChainId === 1 && !!rpcUrl;
|
|
66
69
|
const { data, error, isLoading } = useAsyncData({
|
|
67
70
|
queryKey: ['identity', chainType, address, ensChainId],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useResolvedIdentity.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useResolvedIdentity.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -219,6 +219,7 @@ const CoreOpenfortProvider = ({ children, onConnect, onDisconnect, openfortConfi
|
|
|
219
219
|
storeActiveEmbeddedAddress,
|
|
220
220
|
chainType,
|
|
221
221
|
store,
|
|
222
|
+
walletConfig,
|
|
222
223
|
});
|
|
223
224
|
// Current chain for EVM provider reconfiguration
|
|
224
225
|
const evmChainId = (strategy === null || strategy === void 0 ? void 0 : strategy.chainType) === ChainTypeEnum.EVM ? (bridge ? bridge.chainId : strategy === null || strategy === void 0 ? void 0 : strategy.getChainId()) : undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CoreOpenfortProvider.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"CoreOpenfortProvider.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ChainTypeEnum, type Openfort } from '@openfort/openfort-js';
|
|
2
2
|
import type { StoreApi } from 'zustand/vanilla';
|
|
3
|
+
import type { OpenfortWalletConfig } from '../../components/Openfort/types';
|
|
3
4
|
import type { OpenfortStore } from '../store';
|
|
4
5
|
type Params = {
|
|
5
6
|
openfort: Openfort;
|
|
@@ -8,13 +9,16 @@ type Params = {
|
|
|
8
9
|
storeActiveEmbeddedAddress: OpenfortStore['activeEmbeddedAddress'];
|
|
9
10
|
chainType: ChainTypeEnum;
|
|
10
11
|
store: StoreApi<OpenfortStore>;
|
|
12
|
+
walletConfig: OpenfortWalletConfig | undefined;
|
|
11
13
|
};
|
|
12
14
|
/**
|
|
13
15
|
* Syncs the active embedded address into the store.
|
|
14
16
|
*
|
|
15
17
|
* - Clears address when there are no accounts or user is logged out.
|
|
16
|
-
* - When
|
|
17
|
-
*
|
|
18
|
+
* - When connectOnLogin is false and the session went through
|
|
19
|
+
* EMBEDDED_SIGNER_NOT_CONFIGURED (login flow), skips auto-seeding at READY
|
|
20
|
+
* so the user must pick a wallet explicitly.
|
|
21
|
+
* - Returning sessions (SDK starts directly at READY) always auto-seed.
|
|
18
22
|
*/
|
|
19
|
-
export declare function useActiveAddressSync({ openfort, storeEmbeddedAccounts, storeEmbeddedState, storeActiveEmbeddedAddress, chainType, store, }: Params): void;
|
|
23
|
+
export declare function useActiveAddressSync({ openfort, storeEmbeddedAccounts, storeEmbeddedState, storeActiveEmbeddedAddress, chainType, store, walletConfig, }: Params): void;
|
|
20
24
|
export {};
|
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
import { EmbeddedState } from '@openfort/openfort-js';
|
|
2
|
-
import { useEffect } from 'react';
|
|
2
|
+
import { useRef, useEffect } from 'react';
|
|
3
3
|
import { firstEmbeddedAddress } from '../../core/strategyUtils.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Syncs the active embedded address into the store.
|
|
7
7
|
*
|
|
8
8
|
* - Clears address when there are no accounts or user is logged out.
|
|
9
|
-
* - When
|
|
10
|
-
*
|
|
9
|
+
* - When connectOnLogin is false and the session went through
|
|
10
|
+
* EMBEDDED_SIGNER_NOT_CONFIGURED (login flow), skips auto-seeding at READY
|
|
11
|
+
* so the user must pick a wallet explicitly.
|
|
12
|
+
* - Returning sessions (SDK starts directly at READY) always auto-seed.
|
|
11
13
|
*/
|
|
12
|
-
function useActiveAddressSync({ openfort, storeEmbeddedAccounts, storeEmbeddedState, storeActiveEmbeddedAddress, chainType, store, }) {
|
|
14
|
+
function useActiveAddressSync({ openfort, storeEmbeddedAccounts, storeEmbeddedState, storeActiveEmbeddedAddress, chainType, store, walletConfig, }) {
|
|
15
|
+
var _a;
|
|
16
|
+
const connectOnLogin = (_a = walletConfig === null || walletConfig === void 0 ? void 0 : walletConfig.connectOnLogin) !== null && _a !== void 0 ? _a : true;
|
|
17
|
+
// Track whether the current session went through EMBEDDED_SIGNER_NOT_CONFIGURED
|
|
18
|
+
// (i.e. a login flow, not a returning session). When connectOnLogin is false, we
|
|
19
|
+
// skip auto-seeding at READY only for login flows — returning sessions (where the
|
|
20
|
+
// SDK starts directly at READY with a configured signer) should always auto-seed.
|
|
21
|
+
const sawSignerNotConfiguredRef = useRef(false);
|
|
13
22
|
useEffect(() => {
|
|
14
23
|
if (!openfort || !(storeEmbeddedAccounts === null || storeEmbeddedAccounts === void 0 ? void 0 : storeEmbeddedAccounts.length)) {
|
|
15
|
-
|
|
24
|
+
// Only clear the address on genuine logout (no user). During wallet creation
|
|
25
|
+
// the accounts list is briefly empty while updateEmbeddedAccounts refetches —
|
|
26
|
+
// clearing here would undo the address that create() just set.
|
|
27
|
+
if (!(storeEmbeddedAccounts === null || storeEmbeddedAccounts === void 0 ? void 0 : storeEmbeddedAccounts.length) && !store.getState().user) {
|
|
16
28
|
store.getState().setActiveEmbeddedAddress(undefined);
|
|
17
29
|
}
|
|
18
30
|
return;
|
|
@@ -23,13 +35,16 @@ function useActiveAddressSync({ openfort, storeEmbeddedAccounts, storeEmbeddedSt
|
|
|
23
35
|
if (storeEmbeddedState === EmbeddedState.UNAUTHENTICATED || storeEmbeddedState === EmbeddedState.NONE) {
|
|
24
36
|
if (!store.getState().user) {
|
|
25
37
|
store.getState().setActiveEmbeddedAddress(undefined);
|
|
38
|
+
sawSignerNotConfiguredRef.current = false;
|
|
26
39
|
}
|
|
27
40
|
return;
|
|
28
41
|
}
|
|
29
42
|
// Bootstrap recovery: when signer is not yet configured and no address is set,
|
|
30
43
|
// seed the active address so useAutoRecovery can trigger and drive state → READY.
|
|
44
|
+
// Skip when connectOnLogin is false — the user should explicitly choose a wallet.
|
|
31
45
|
if (storeEmbeddedState === EmbeddedState.EMBEDDED_SIGNER_NOT_CONFIGURED) {
|
|
32
|
-
|
|
46
|
+
sawSignerNotConfiguredRef.current = true;
|
|
47
|
+
if (connectOnLogin && storeActiveEmbeddedAddress === undefined) {
|
|
33
48
|
const first = firstEmbeddedAddress(storeEmbeddedAccounts, chainType);
|
|
34
49
|
if (first)
|
|
35
50
|
store.getState().setActiveEmbeddedAddress(first);
|
|
@@ -42,6 +57,13 @@ function useActiveAddressSync({ openfort, storeEmbeddedAccounts, storeEmbeddedSt
|
|
|
42
57
|
// Already have an address — nothing to resolve
|
|
43
58
|
if (storeActiveEmbeddedAddress !== undefined)
|
|
44
59
|
return;
|
|
60
|
+
// Login flow with connectOnLogin=false: the session went through
|
|
61
|
+
// EMBEDDED_SIGNER_NOT_CONFIGURED, so don't auto-seed. The user must pick
|
|
62
|
+
// a wallet via the UI. create() and setActive() set the address directly,
|
|
63
|
+
// so they bypass this check.
|
|
64
|
+
if (!connectOnLogin && sawSignerNotConfiguredRef.current) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
45
67
|
// Priority 1: ask the SDK for its active wallet
|
|
46
68
|
let cancelled = false;
|
|
47
69
|
openfort.embeddedWallet
|
|
@@ -69,7 +91,15 @@ function useActiveAddressSync({ openfort, storeEmbeddedAccounts, storeEmbeddedSt
|
|
|
69
91
|
return () => {
|
|
70
92
|
cancelled = true;
|
|
71
93
|
};
|
|
72
|
-
}, [
|
|
94
|
+
}, [
|
|
95
|
+
openfort,
|
|
96
|
+
storeEmbeddedAccounts,
|
|
97
|
+
storeEmbeddedState,
|
|
98
|
+
storeActiveEmbeddedAddress,
|
|
99
|
+
chainType,
|
|
100
|
+
store,
|
|
101
|
+
connectOnLogin,
|
|
102
|
+
]);
|
|
73
103
|
}
|
|
74
104
|
|
|
75
105
|
export { useActiveAddressSync };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useActiveAddressSync.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useActiveAddressSync.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -123,6 +123,7 @@ function useSolanaEmbeddedWallet(options) {
|
|
|
123
123
|
accountType: AccountTypeEnum.EOA,
|
|
124
124
|
recoveryParams,
|
|
125
125
|
});
|
|
126
|
+
setActiveEmbeddedAddress(account.address);
|
|
126
127
|
await updateEmbeddedAccounts({ silent: true });
|
|
127
128
|
const provider = createProviderForAccount(account);
|
|
128
129
|
const connectedWallet = {
|
|
@@ -139,7 +140,6 @@ function useSolanaEmbeddedWallet(options) {
|
|
|
139
140
|
provider,
|
|
140
141
|
error: null,
|
|
141
142
|
});
|
|
142
|
-
setActiveEmbeddedAddress(account.address);
|
|
143
143
|
(_a = createOptions === null || createOptions === void 0 ? void 0 : createOptions.onSuccess) === null || _a === void 0 ? void 0 : _a.call(createOptions, { account });
|
|
144
144
|
return account;
|
|
145
145
|
}
|
package/build/utils/rpc.d.ts
CHANGED
|
@@ -6,10 +6,6 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { Chain } from 'viem';
|
|
8
8
|
import type { SolanaCluster } from '../solana/types';
|
|
9
|
-
/**
|
|
10
|
-
* Get default Ethereum RPC URL for a chain ID.
|
|
11
|
-
* Returns the viem/chains default RPC when known, falls back to Sepolia.
|
|
12
|
-
*/
|
|
13
9
|
export declare function getDefaultEthereumRpcUrl(chainId: number): string;
|
|
14
10
|
/**
|
|
15
11
|
* Get default Solana RPC URL for a cluster.
|
package/build/utils/rpc.js
CHANGED
|
@@ -30,11 +30,15 @@ const DEFAULT_SOLANA_RPC_URLS = {
|
|
|
30
30
|
* Get default Ethereum RPC URL for a chain ID.
|
|
31
31
|
* Returns the viem/chains default RPC when known, falls back to Sepolia.
|
|
32
32
|
*/
|
|
33
|
+
const warnedChainIds = new Set();
|
|
33
34
|
function getDefaultEthereumRpcUrl(chainId) {
|
|
34
35
|
const chain = KNOWN_CHAINS[chainId];
|
|
35
36
|
const rpcUrl = chain === null || chain === void 0 ? void 0 : chain.rpcUrls.default.http[0];
|
|
36
37
|
if (!rpcUrl) {
|
|
37
|
-
|
|
38
|
+
if (!warnedChainIds.has(chainId)) {
|
|
39
|
+
warnedChainIds.add(chainId);
|
|
40
|
+
logger.warn(`No default Ethereum RPC URL found for chain ${chainId}. Configure rpcUrls in OpenfortProvider for better reliability and rate limits.`);
|
|
41
|
+
}
|
|
38
42
|
return sepolia.rpcUrls.default.http[0];
|
|
39
43
|
}
|
|
40
44
|
return rpcUrl;
|
package/build/utils/rpc.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpc.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"rpc.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for secure handling of OAuth callback URLs containing tokens.
|
|
3
|
+
*
|
|
4
|
+
* Prevents Referer-header leakage of access tokens and provides robust
|
|
5
|
+
* URL parsing for the OF-1013 workaround (duplicate `?` in redirect URLs).
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Injects a `<meta name="referrer" content="no-referrer">` tag so that
|
|
9
|
+
* any subresource request fired before `history.replaceState` strips the
|
|
10
|
+
* tokens will NOT leak the full URL (including access_token) via the
|
|
11
|
+
* Referer header.
|
|
12
|
+
*
|
|
13
|
+
* Call this **synchronously** — before any `await` — when the URL
|
|
14
|
+
* contains sensitive query parameters.
|
|
15
|
+
*
|
|
16
|
+
* @returns A cleanup function that removes the meta tag.
|
|
17
|
+
*/
|
|
18
|
+
export declare function suppressReferrer(): () => void;
|
|
19
|
+
/**
|
|
20
|
+
* Parses the current `window.location.href`, fixing the OF-1013 issue
|
|
21
|
+
* where the server redirect produces a URL with a duplicate `?`, e.g.
|
|
22
|
+
* `https://example.com/callback?existing=1?access_token=xxx&user_id=yyy`.
|
|
23
|
+
*
|
|
24
|
+
* Instead of a fragile `.replace('?access_token=', '&access_token=')`
|
|
25
|
+
* that can mangle values containing the same substring, this finds the
|
|
26
|
+
* *second* `?` (if any) and replaces it with `&`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseCallbackUrl(href: string): URL;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for secure handling of OAuth callback URLs containing tokens.
|
|
3
|
+
*
|
|
4
|
+
* Prevents Referer-header leakage of access tokens and provides robust
|
|
5
|
+
* URL parsing for the OF-1013 workaround (duplicate `?` in redirect URLs).
|
|
6
|
+
*/
|
|
7
|
+
const REFERRER_META_ID = '__openfort_no_referrer';
|
|
8
|
+
/**
|
|
9
|
+
* Injects a `<meta name="referrer" content="no-referrer">` tag so that
|
|
10
|
+
* any subresource request fired before `history.replaceState` strips the
|
|
11
|
+
* tokens will NOT leak the full URL (including access_token) via the
|
|
12
|
+
* Referer header.
|
|
13
|
+
*
|
|
14
|
+
* Call this **synchronously** — before any `await` — when the URL
|
|
15
|
+
* contains sensitive query parameters.
|
|
16
|
+
*
|
|
17
|
+
* @returns A cleanup function that removes the meta tag.
|
|
18
|
+
*/
|
|
19
|
+
function suppressReferrer() {
|
|
20
|
+
if (typeof document === 'undefined')
|
|
21
|
+
return () => { };
|
|
22
|
+
// Avoid duplicates if called more than once
|
|
23
|
+
if (document.getElementById(REFERRER_META_ID))
|
|
24
|
+
return () => { };
|
|
25
|
+
const meta = document.createElement('meta');
|
|
26
|
+
meta.id = REFERRER_META_ID;
|
|
27
|
+
meta.name = 'referrer';
|
|
28
|
+
meta.content = 'no-referrer';
|
|
29
|
+
document.head.appendChild(meta);
|
|
30
|
+
return () => {
|
|
31
|
+
meta.remove();
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Parses the current `window.location.href`, fixing the OF-1013 issue
|
|
36
|
+
* where the server redirect produces a URL with a duplicate `?`, e.g.
|
|
37
|
+
* `https://example.com/callback?existing=1?access_token=xxx&user_id=yyy`.
|
|
38
|
+
*
|
|
39
|
+
* Instead of a fragile `.replace('?access_token=', '&access_token=')`
|
|
40
|
+
* that can mangle values containing the same substring, this finds the
|
|
41
|
+
* *second* `?` (if any) and replaces it with `&`.
|
|
42
|
+
*/
|
|
43
|
+
function parseCallbackUrl(href) {
|
|
44
|
+
const firstQ = href.indexOf('?');
|
|
45
|
+
if (firstQ === -1)
|
|
46
|
+
return new URL(href);
|
|
47
|
+
const secondQ = href.indexOf('?', firstQ + 1);
|
|
48
|
+
if (secondQ === -1)
|
|
49
|
+
return new URL(href);
|
|
50
|
+
// Replace only the second `?` with `&`
|
|
51
|
+
const fixed = `${href.slice(0, secondQ)}&${href.slice(secondQ + 1)}`;
|
|
52
|
+
return new URL(fixed);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { parseCallbackUrl, suppressReferrer };
|
|
56
|
+
//# sourceMappingURL=urlSecurity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"urlSecurity.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/build/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const OPENFORT_VERSION = "1.0.
|
|
1
|
+
export declare const OPENFORT_VERSION = "1.0.8";
|
package/build/version.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { OpenfortError } from '@openfort/openfort-js';
|
|
2
|
-
import { useCallback } from 'react';
|
|
2
|
+
import { useRef, useCallback } from 'react';
|
|
3
3
|
import { useEthereumBridge } from '../ethereum/OpenfortEthereumBridgeContext.js';
|
|
4
4
|
import { useOpenfortCore } from '../openfort/useOpenfort.js';
|
|
5
5
|
import { createSIWEMessage } from '../siwe/create-siwe-message.js';
|
|
@@ -17,27 +17,35 @@ import { logger } from '../utils/logger.js';
|
|
|
17
17
|
* ```
|
|
18
18
|
*/
|
|
19
19
|
function useConnectWithSiwe() {
|
|
20
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
21
20
|
const { client, user, updateUser } = useOpenfortCore();
|
|
22
21
|
const bridge = useEthereumBridge();
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
22
|
+
// Use a ref so the callback always reads the latest bridge state,
|
|
23
|
+
// not a stale closure from the last render (critical after connectAsync changes the active connector).
|
|
24
|
+
const bridgeRef = useRef(bridge);
|
|
25
|
+
bridgeRef.current = bridge;
|
|
26
|
+
const userRef = useRef(user);
|
|
27
|
+
userRef.current = user;
|
|
28
|
+
const connectWithSiwe = useCallback(async ({ onError, onConnect, address: propsAddress, connectorType: propsConnectorType, walletClientType: propsWalletClientType, link, } = {}) => {
|
|
29
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
30
|
+
// Read fresh values from the bridge ref — NOT from the stale render closure
|
|
31
|
+
const b = bridgeRef.current;
|
|
32
|
+
const currentUser = userRef.current;
|
|
33
|
+
const shouldLink = link !== null && link !== void 0 ? link : !!currentUser;
|
|
34
|
+
const address = propsAddress !== null && propsAddress !== void 0 ? propsAddress : (_a = b === null || b === void 0 ? void 0 : b.account) === null || _a === void 0 ? void 0 : _a.address;
|
|
35
|
+
const connectorType = propsConnectorType !== null && propsConnectorType !== void 0 ? propsConnectorType : (_c = (_b = b === null || b === void 0 ? void 0 : b.account) === null || _b === void 0 ? void 0 : _b.connector) === null || _c === void 0 ? void 0 : _c.type;
|
|
36
|
+
const walletClientType = propsWalletClientType !== null && propsWalletClientType !== void 0 ? propsWalletClientType : (_e = (_d = b === null || b === void 0 ? void 0 : b.account) === null || _d === void 0 ? void 0 : _d.connector) === null || _e === void 0 ? void 0 : _e.id;
|
|
37
|
+
const chainId = (_f = b === null || b === void 0 ? void 0 : b.chainId) !== null && _f !== void 0 ? _f : 0;
|
|
38
|
+
const accountChainId = (_j = (_h = (_g = b === null || b === void 0 ? void 0 : b.account) === null || _g === void 0 ? void 0 : _g.chain) === null || _h === void 0 ? void 0 : _h.id) !== null && _j !== void 0 ? _j : b === null || b === void 0 ? void 0 : b.chainId;
|
|
39
|
+
const chainName = (_l = (_k = b === null || b === void 0 ? void 0 : b.account) === null || _k === void 0 ? void 0 : _k.chain) === null || _l === void 0 ? void 0 : _l.name;
|
|
40
|
+
const switchChainAsync = (_m = b === null || b === void 0 ? void 0 : b.switchChain) === null || _m === void 0 ? void 0 : _m.switchChainAsync;
|
|
41
|
+
const signMessage = b === null || b === void 0 ? void 0 : b.signMessage;
|
|
42
|
+
if (!address || !connectorType || !walletClientType) {
|
|
43
|
+
logger.warn('[useConnectWithSiwe] Missing params', { address, connectorType, walletClientType });
|
|
37
44
|
onError === null || onError === void 0 ? void 0 : onError('No address found');
|
|
38
45
|
return;
|
|
39
46
|
}
|
|
40
47
|
if (!signMessage) {
|
|
48
|
+
logger.warn('[useConnectWithSiwe] No signMessage on bridge');
|
|
41
49
|
onError === null || onError === void 0 ? void 0 : onError('EVM bridge not available (signMessage)');
|
|
42
50
|
return;
|
|
43
51
|
}
|
|
@@ -46,46 +54,44 @@ function useConnectWithSiwe() {
|
|
|
46
54
|
await switchChainAsync({ chainId });
|
|
47
55
|
}
|
|
48
56
|
let nonce;
|
|
49
|
-
if (
|
|
50
|
-
const resp = await client.auth.initLinkSiwe({ address
|
|
57
|
+
if (shouldLink) {
|
|
58
|
+
const resp = await client.auth.initLinkSiwe({ address });
|
|
51
59
|
nonce = resp.nonce;
|
|
52
60
|
}
|
|
53
61
|
else {
|
|
54
|
-
const resp = await client.auth.initSiwe({ address
|
|
62
|
+
const resp = await client.auth.initSiwe({ address });
|
|
55
63
|
nonce = resp.nonce;
|
|
56
64
|
}
|
|
57
|
-
const SIWEMessage = createSIWEMessage(
|
|
65
|
+
const SIWEMessage = createSIWEMessage(address, nonce, chainId);
|
|
58
66
|
if (!SIWEMessage)
|
|
59
67
|
throw new Error('SIWE message creation failed (window not available)');
|
|
60
68
|
const signature = await signMessage({ message: SIWEMessage });
|
|
61
|
-
if (
|
|
62
|
-
logger.log('Linking wallet to user');
|
|
69
|
+
if (shouldLink) {
|
|
63
70
|
await client.auth.linkWithSiwe({
|
|
64
71
|
signature,
|
|
65
72
|
message: SIWEMessage,
|
|
66
73
|
connectorType,
|
|
67
74
|
walletClientType,
|
|
68
|
-
address
|
|
75
|
+
address,
|
|
69
76
|
chainId,
|
|
70
77
|
});
|
|
71
78
|
}
|
|
72
79
|
else {
|
|
73
|
-
logger.log('Authenticating with SIWE');
|
|
74
80
|
await client.auth.loginWithSiwe({
|
|
75
81
|
signature,
|
|
76
82
|
message: SIWEMessage,
|
|
77
83
|
connectorType,
|
|
78
84
|
walletClientType,
|
|
79
|
-
address
|
|
85
|
+
address,
|
|
80
86
|
});
|
|
81
87
|
}
|
|
82
88
|
await updateUser();
|
|
83
89
|
await Promise.resolve(onConnect === null || onConnect === void 0 ? void 0 : onConnect());
|
|
84
90
|
}
|
|
85
91
|
catch (err) {
|
|
86
|
-
logger.
|
|
87
|
-
|
|
88
|
-
status: (
|
|
92
|
+
logger.error('[useConnectWithSiwe] SIWE failed', {
|
|
93
|
+
message: err instanceof Error ? err.message : String(err),
|
|
94
|
+
status: (_o = err === null || err === void 0 ? void 0 : err.response) === null || _o === void 0 ? void 0 : _o.status,
|
|
89
95
|
});
|
|
90
96
|
if (!onError)
|
|
91
97
|
return;
|
|
@@ -107,7 +113,7 @@ function useConnectWithSiwe() {
|
|
|
107
113
|
}
|
|
108
114
|
onError(message, err instanceof OpenfortError ? err : undefined);
|
|
109
115
|
}
|
|
110
|
-
}, [client,
|
|
116
|
+
}, [client, updateUser]);
|
|
111
117
|
return { connectWithSiwe };
|
|
112
118
|
}
|
|
113
119
|
|