@oxyhq/core 5.2.0 → 5.3.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/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/CrossDomainAuth.js +32 -42
- package/dist/cjs/i18n/locales/ar-SA.json +1 -0
- package/dist/cjs/i18n/locales/ca-ES.json +1 -0
- package/dist/cjs/i18n/locales/de-DE.json +1 -0
- package/dist/cjs/i18n/locales/en-US.json +1 -0
- package/dist/cjs/i18n/locales/es-ES.json +1 -0
- package/dist/cjs/i18n/locales/fr-FR.json +1 -0
- package/dist/cjs/i18n/locales/it-IT.json +1 -0
- package/dist/cjs/i18n/locales/ja-JP.json +1 -0
- package/dist/cjs/i18n/locales/ko-KR.json +1 -0
- package/dist/cjs/i18n/locales/locales/ar-SA.json +1 -0
- package/dist/cjs/i18n/locales/locales/ca-ES.json +1 -0
- package/dist/cjs/i18n/locales/locales/de-DE.json +1 -0
- package/dist/cjs/i18n/locales/locales/en-US.json +1 -0
- package/dist/cjs/i18n/locales/locales/es-ES.json +1 -0
- package/dist/cjs/i18n/locales/locales/fr-FR.json +1 -0
- package/dist/cjs/i18n/locales/locales/it-IT.json +1 -0
- package/dist/cjs/i18n/locales/locales/ja-JP.json +1 -0
- package/dist/cjs/i18n/locales/locales/ko-KR.json +1 -0
- package/dist/cjs/i18n/locales/locales/pt-PT.json +1 -0
- package/dist/cjs/i18n/locales/locales/zh-CN.json +1 -0
- package/dist/cjs/i18n/locales/pt-PT.json +1 -0
- package/dist/cjs/i18n/locales/zh-CN.json +1 -0
- package/dist/cjs/index.js +24 -2
- package/dist/cjs/mixins/OxyServices.sso.js +36 -0
- package/dist/cjs/mixins/OxyServices.user.js +24 -0
- package/dist/cjs/server/index.js +13 -1
- package/dist/cjs/session/SessionClient.js +142 -0
- package/dist/cjs/session/createSessionClient.js +26 -0
- package/dist/cjs/session/projectSessionState.js +75 -0
- package/dist/cjs/session/sessionClientHost.js +30 -0
- package/dist/cjs/session/socketLoader.js +55 -0
- package/dist/cjs/utils/cache.js +2 -0
- package/dist/cjs/utils/ssoBounce.js +9 -9
- package/dist/cjs/utils/ssoEstablish.js +110 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/CrossDomainAuth.js +32 -42
- package/dist/esm/i18n/locales/ar-SA.json +1 -0
- package/dist/esm/i18n/locales/ca-ES.json +1 -0
- package/dist/esm/i18n/locales/de-DE.json +1 -0
- package/dist/esm/i18n/locales/en-US.json +1 -0
- package/dist/esm/i18n/locales/es-ES.json +1 -0
- package/dist/esm/i18n/locales/fr-FR.json +1 -0
- package/dist/esm/i18n/locales/it-IT.json +1 -0
- package/dist/esm/i18n/locales/ja-JP.json +1 -0
- package/dist/esm/i18n/locales/ko-KR.json +1 -0
- package/dist/esm/i18n/locales/locales/ar-SA.json +1 -0
- package/dist/esm/i18n/locales/locales/ca-ES.json +1 -0
- package/dist/esm/i18n/locales/locales/de-DE.json +1 -0
- package/dist/esm/i18n/locales/locales/en-US.json +1 -0
- package/dist/esm/i18n/locales/locales/es-ES.json +1 -0
- package/dist/esm/i18n/locales/locales/fr-FR.json +1 -0
- package/dist/esm/i18n/locales/locales/it-IT.json +1 -0
- package/dist/esm/i18n/locales/locales/ja-JP.json +1 -0
- package/dist/esm/i18n/locales/locales/ko-KR.json +1 -0
- package/dist/esm/i18n/locales/locales/pt-PT.json +1 -0
- package/dist/esm/i18n/locales/locales/zh-CN.json +1 -0
- package/dist/esm/i18n/locales/pt-PT.json +1 -0
- package/dist/esm/i18n/locales/zh-CN.json +1 -0
- package/dist/esm/index.js +14 -0
- package/dist/esm/mixins/OxyServices.sso.js +36 -0
- package/dist/esm/mixins/OxyServices.user.js +24 -0
- package/dist/esm/server/index.js +10 -0
- package/dist/esm/session/SessionClient.js +138 -0
- package/dist/esm/session/createSessionClient.js +23 -0
- package/dist/esm/session/projectSessionState.js +69 -0
- package/dist/esm/session/sessionClientHost.js +27 -0
- package/dist/esm/session/socketLoader.js +19 -0
- package/dist/esm/utils/cache.js +2 -0
- package/dist/esm/utils/ssoBounce.js +9 -9
- package/dist/esm/utils/ssoEstablish.js +107 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/CrossDomainAuth.d.ts +33 -13
- package/dist/types/index.d.ts +7 -0
- package/dist/types/mixins/OxyServices.sso.d.ts +24 -0
- package/dist/types/mixins/OxyServices.user.d.ts +14 -0
- package/dist/types/server/index.d.ts +2 -0
- package/dist/types/session/SessionClient.d.ts +55 -0
- package/dist/types/session/createSessionClient.d.ts +23 -0
- package/dist/types/session/projectSessionState.d.ts +43 -0
- package/dist/types/session/sessionClientHost.d.ts +18 -0
- package/dist/types/session/socketLoader.d.ts +9 -0
- package/dist/types/utils/ssoBounce.d.ts +9 -9
- package/dist/types/utils/ssoEstablish.d.ts +85 -0
- package/package.json +1 -1
- package/src/CrossDomainAuth.ts +33 -44
- package/src/__tests__/crossDomainAuth.test.ts +33 -16
- package/src/i18n/locales/ar-SA.json +1 -0
- package/src/i18n/locales/ca-ES.json +1 -0
- package/src/i18n/locales/de-DE.json +1 -0
- package/src/i18n/locales/en-US.json +1 -0
- package/src/i18n/locales/es-ES.json +1 -0
- package/src/i18n/locales/fr-FR.json +1 -0
- package/src/i18n/locales/it-IT.json +1 -0
- package/src/i18n/locales/ja-JP.json +1 -0
- package/src/i18n/locales/ko-KR.json +1 -0
- package/src/i18n/locales/pt-PT.json +1 -0
- package/src/i18n/locales/zh-CN.json +1 -0
- package/src/index.ts +24 -0
- package/src/mixins/OxyServices.sso.ts +55 -0
- package/src/mixins/OxyServices.user.ts +26 -0
- package/src/mixins/__tests__/sso.test.ts +41 -0
- package/src/server/index.ts +12 -0
- package/src/session/SessionClient.ts +175 -0
- package/src/session/__tests__/SessionClient.rest.test.ts +79 -0
- package/src/session/__tests__/SessionClient.socket.test.ts +132 -0
- package/src/session/__tests__/SessionClient.state.test.ts +64 -0
- package/src/session/__tests__/sessionIntegration.test.ts +202 -0
- package/src/session/createSessionClient.ts +31 -0
- package/src/session/projectSessionState.ts +83 -0
- package/src/session/sessionClientHost.ts +32 -0
- package/src/session/socketLoader.ts +28 -0
- package/src/utils/__tests__/ssoEstablish.test.ts +204 -0
- package/src/utils/cache.ts +2 -0
- package/src/utils/ssoBounce.ts +9 -9
- package/src/utils/ssoEstablish.ts +174 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure projection helpers: `DeviceSessionState` (the device-scoped
|
|
3
|
+
* multi-account session-sync state produced by `SessionClient`) -> the
|
|
4
|
+
* shapes consumers (`@oxyhq/services`, `@oxyhq/auth`) render today
|
|
5
|
+
* (`ClientSession[]`, an active session id, an active `User`).
|
|
6
|
+
*
|
|
7
|
+
* No I/O. The caller fetches profiles via
|
|
8
|
+
* `oxyServices.getUsersByIds(accountIdsOf(state))` and builds `usersById`
|
|
9
|
+
* from the result before calling `deviceStateToClientSessions` /
|
|
10
|
+
* `activeUserOf`.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Maps every `SessionAccount` in `state.accounts` to a `ClientSession`.
|
|
14
|
+
*
|
|
15
|
+
* `DeviceSessionState` carries no per-account `expiresAt` / `lastActive` —
|
|
16
|
+
* both are set to `state.updatedAt` (converted to an ISO-8601 string; the
|
|
17
|
+
* wire value is an epoch-ms number) as a provisional value.
|
|
18
|
+
*
|
|
19
|
+
* `usersById` is accepted for signature symmetry with `activeUserOf` even
|
|
20
|
+
* though `ClientSession` only stores `userId` — a session is still
|
|
21
|
+
* projected for an account whose id is absent from `usersById` (no
|
|
22
|
+
* placeholder user is fabricated).
|
|
23
|
+
*/
|
|
24
|
+
export function deviceStateToClientSessions(state, usersById) {
|
|
25
|
+
const provisionalTimestamp = new Date(state.updatedAt).toISOString();
|
|
26
|
+
return state.accounts.map((account) => ({
|
|
27
|
+
sessionId: account.sessionId,
|
|
28
|
+
deviceId: state.deviceId,
|
|
29
|
+
// provisional: expiresAt/lastActive are not carried on DeviceSessionState
|
|
30
|
+
expiresAt: provisionalTimestamp,
|
|
31
|
+
lastActive: provisionalTimestamp,
|
|
32
|
+
userId: account.accountId,
|
|
33
|
+
isCurrent: account.accountId === state.activeAccountId,
|
|
34
|
+
authuser: account.authuser,
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The active account's `sessionId`, or `null` when there is no state or no
|
|
39
|
+
* active account is set.
|
|
40
|
+
*/
|
|
41
|
+
export function activeSessionIdOf(state) {
|
|
42
|
+
if (state === null || state.activeAccountId === null) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const activeAccountId = state.activeAccountId;
|
|
46
|
+
const activeAccount = state.accounts.find((account) => account.accountId === activeAccountId);
|
|
47
|
+
return activeAccount?.sessionId ?? null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The active account's `User`, resolved from `usersById`. `null` when there
|
|
51
|
+
* is no state, no active account is set, or the active account id is absent
|
|
52
|
+
* from `usersById`.
|
|
53
|
+
*/
|
|
54
|
+
export function activeUserOf(state, usersById) {
|
|
55
|
+
if (state === null || state.activeAccountId === null) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return usersById.get(state.activeAccountId) ?? null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* All account ids in `state`, suitable for an `oxyServices.getUsersByIds(...)`
|
|
62
|
+
* fetch. `[]` for `null` state.
|
|
63
|
+
*/
|
|
64
|
+
export function accountIdsOf(state) {
|
|
65
|
+
if (state === null) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
return state.accounts.map((account) => account.accountId);
|
|
69
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin `SessionClientHost` adapter over an `OxyServices` instance.
|
|
3
|
+
*
|
|
4
|
+
* `SessionClient` is host-agnostic: it only needs a REST + token surface.
|
|
5
|
+
* `OxyServices` already exposes all of that except `getCurrentAccountId`,
|
|
6
|
+
* which has no direct equivalent — the adapter holds a mutable ref set by
|
|
7
|
+
* the caller (`OxyContext` in `@oxyhq/services`, `WebOxyProvider` in
|
|
8
|
+
* `@oxyhq/auth`) via `setCurrentAccountId`.
|
|
9
|
+
*
|
|
10
|
+
* Shared here (rather than duplicated per consumer) because it is entirely
|
|
11
|
+
* platform-agnostic: every method it calls exists identically on
|
|
12
|
+
* `OxyServices` regardless of host (web, Expo/RN, Node).
|
|
13
|
+
*/
|
|
14
|
+
export function createSessionClientHost(oxyServices) {
|
|
15
|
+
let currentAccountId = null;
|
|
16
|
+
return {
|
|
17
|
+
makeRequest: (method, url, data, options) => oxyServices.makeRequest(method, url, data, options),
|
|
18
|
+
getBaseURL: () => oxyServices.getBaseURL(),
|
|
19
|
+
getAccessToken: () => oxyServices.getAccessToken(),
|
|
20
|
+
onTokensChanged: (listener) => oxyServices.onTokensChanged(listener),
|
|
21
|
+
setTokens: (accessToken) => oxyServices.setTokens(accessToken),
|
|
22
|
+
getCurrentAccountId: () => currentAccountId,
|
|
23
|
+
setCurrentAccountId: (id) => {
|
|
24
|
+
currentAccountId = id;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { logger } from '../utils/loggerUtils.js';
|
|
2
|
+
let cachedFactory = null;
|
|
3
|
+
let loadAttempted = false;
|
|
4
|
+
export async function getSocketIO() {
|
|
5
|
+
if (cachedFactory)
|
|
6
|
+
return cachedFactory;
|
|
7
|
+
if (loadAttempted)
|
|
8
|
+
return null;
|
|
9
|
+
loadAttempted = true;
|
|
10
|
+
try {
|
|
11
|
+
const mod = (await import('socket.io-client'));
|
|
12
|
+
cachedFactory = mod.io ?? mod.default ?? null;
|
|
13
|
+
return cachedFactory;
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
logger.warn('[SessionClient] socket.io-client import failed; realtime session sync disabled', { component: 'SessionClient' }, error);
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
package/dist/esm/utils/cache.js
CHANGED
|
@@ -124,18 +124,18 @@ export function ssoPriorSessionKey(origin) {
|
|
|
124
124
|
* per-tab `sessionStorage` the loop-breaker keys use — it must survive a reload.
|
|
125
125
|
*
|
|
126
126
|
* It exists purely to suppress AUTOMATIC silent restore after a deliberate
|
|
127
|
-
* sign-out: a still-live IdP session (the central `fedcm_session`
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
127
|
+
* sign-out: a still-live IdP session (the central `fedcm_session`) would
|
|
128
|
+
* otherwise let the per-apex `/auth/silent` iframe re-mint a session on the
|
|
129
|
+
* very next cold boot, so a user who pressed "Sign out" gets silently signed
|
|
130
|
+
* back in on reload. With this flag set, that silent cold-boot step is
|
|
131
|
+
* skipped while the Gmail-style returning-account fast-path is otherwise
|
|
132
|
+
* preserved.
|
|
133
133
|
*
|
|
134
134
|
* Lifecycle (mirrors the existing gate machinery — set on a definitive event,
|
|
135
135
|
* cleared on its inverse):
|
|
136
136
|
* - SET on EXPLICIT full sign-out (alongside clearing the prior-session hint
|
|
137
137
|
* and the SSO bounce state).
|
|
138
|
-
* - CLEARED on ANY deliberate sign-in (password,
|
|
138
|
+
* - CLEARED on ANY deliberate sign-in (password, account switch, device
|
|
139
139
|
* claim) so a real sign-in fully re-enables silent restore — there is no
|
|
140
140
|
* "stuck signed out" state.
|
|
141
141
|
*
|
|
@@ -279,8 +279,8 @@ export function guardActive(storage, origin, now = Date.now()) {
|
|
|
279
279
|
* Whether AUTOMATIC silent restore is SUPPRESSED for this origin because the
|
|
280
280
|
* user deliberately signed out (the durable {@link ssoSignedOutKey} flag).
|
|
281
281
|
*
|
|
282
|
-
* When `true`, the silent cold-boot
|
|
283
|
-
* still-live IdP session WITHOUT user intent —
|
|
282
|
+
* When `true`, the silent cold-boot step that can re-mint a session from a
|
|
283
|
+
* still-live IdP session WITHOUT user intent — the per-apex
|
|
284
284
|
* `/auth/silent` iframe — MUST be skipped, so a user who pressed "Sign out" is
|
|
285
285
|
* not silently signed back in on the next reload. Interactive sign-in clears the
|
|
286
286
|
* flag, so this never blocks a deliberate re-sign-in.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-claim durable-session establish hop (web device-flow / "Sign in with
|
|
3
|
+
* Oxy" QR).
|
|
4
|
+
*
|
|
5
|
+
* A WEB device-flow claim (`claimSessionByToken`) plants only IN-MEMORY tokens:
|
|
6
|
+
* unlike a redirect/FedCM/silent sign-in, it never causes the IdP to plant a
|
|
7
|
+
* `fedcm_session` cookie. So a reload has nothing to restore from — the
|
|
8
|
+
* silent-iframe and `/sso` paths find no IdP session and the session is lost.
|
|
9
|
+
*
|
|
10
|
+
* This primitive closes that gap. AFTER the claim has committed and the session
|
|
11
|
+
* has been durably persisted, it performs ONE top-level establish hop through
|
|
12
|
+
* the RP's own per-apex IdP host (`auth.<rp-apex>`), reusing the EXISTING
|
|
13
|
+
* `/sso/establish` endpoint: the server mints a short-lived, host+audience-bound
|
|
14
|
+
* establish-token and returns a fully-formed establish URL; navigating to it
|
|
15
|
+
* plants the durable first-party `fedcm_session` cookie and bounces back to the
|
|
16
|
+
* RP callback with an opaque code the standard `sso-return` cold-boot step
|
|
17
|
+
* exchanges.
|
|
18
|
+
*
|
|
19
|
+
* It reuses the SAME per-origin `sessionStorage` bounce contract
|
|
20
|
+
* (`ssoStateKey` / `ssoGuardKey` / `ssoDestKey`) that {@link buildSsoBounceUrl}
|
|
21
|
+
* primes for the terminal `/sso` bounce, so the post-bounce `sso-return` step
|
|
22
|
+
* (`consumeSsoReturn`) validates the CSRF `state`, exchanges the code, and
|
|
23
|
+
* restores the user's real destination with no extra wiring.
|
|
24
|
+
*
|
|
25
|
+
* Contract:
|
|
26
|
+
* - WEB only — off-web / native it is a no-op returning `false`.
|
|
27
|
+
* - NEVER fires while sitting on the central IdP origin (that would loop the
|
|
28
|
+
* IdP against itself).
|
|
29
|
+
* - Bounce state is persisted ONLY after the establish-URL request succeeds, so
|
|
30
|
+
* a failed request leaves no stale state behind.
|
|
31
|
+
* - SINGLE attempt: the caller invokes this exactly once per successful claim.
|
|
32
|
+
* On ANY failure it does NOT navigate and returns `false`, leaving the
|
|
33
|
+
* committed in-memory session exactly as-is (the user is no worse off than
|
|
34
|
+
* before this hop existed).
|
|
35
|
+
* - Total: never throws. Failures are reported via {@link deps.onError} only.
|
|
36
|
+
*/
|
|
37
|
+
import { ssoStateKey, ssoGuardKey, ssoDestKey, ssoNavigate, isCentralIdPOrigin, } from './ssoBounce.js';
|
|
38
|
+
import { generateSsoState } from '../mixins/OxyServices.sso.js';
|
|
39
|
+
/**
|
|
40
|
+
* Perform the post-claim establish hop. Returns `true` when a navigation to the
|
|
41
|
+
* establish URL was initiated (the document is being torn down and replaced by
|
|
42
|
+
* the IdP), `false` on every no-op / failure path.
|
|
43
|
+
*
|
|
44
|
+
* @param client - The exchange surface (`oxyServices.requestSsoEstablishUrl`).
|
|
45
|
+
* @param deps - Injectable web seams; see {@link EstablishAfterClaimDeps}.
|
|
46
|
+
*/
|
|
47
|
+
export async function establishIdpSessionAfterClaim(client, deps = {}) {
|
|
48
|
+
const isWeb = deps.isWeb ??
|
|
49
|
+
(() => typeof window !== 'undefined' &&
|
|
50
|
+
typeof window.sessionStorage !== 'undefined');
|
|
51
|
+
if (!isWeb()) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const storage = deps.storage ?? window.sessionStorage;
|
|
55
|
+
const location = deps.location ?? window.location;
|
|
56
|
+
const navigate = deps.navigate ?? ssoNavigate;
|
|
57
|
+
const generateState = deps.generateState ?? generateSsoState;
|
|
58
|
+
const now = deps.now ?? (() => Date.now());
|
|
59
|
+
const origin = location.origin;
|
|
60
|
+
// Never establish while sitting on the central IdP itself — that would bounce
|
|
61
|
+
// the IdP against itself. (The device-flow claim can only happen on an RP.)
|
|
62
|
+
if (isCentralIdPOrigin(origin)) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
const state = generateState();
|
|
66
|
+
let establishUrl;
|
|
67
|
+
try {
|
|
68
|
+
const result = await client.requestSsoEstablishUrl(origin, state);
|
|
69
|
+
if (!result ||
|
|
70
|
+
typeof result.establishUrl !== 'string' ||
|
|
71
|
+
result.establishUrl.length === 0) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
establishUrl = result.establishUrl;
|
|
75
|
+
// Defense-in-depth before a TOP-LEVEL navigation: the establish URL is
|
|
76
|
+
// server-formed, but never follow anything that is not a well-formed https
|
|
77
|
+
// `/sso/establish` on an `auth.` host. A malformed or unexpected URL aborts
|
|
78
|
+
// silently (no navigation, no state persisted) — same total/no-throw
|
|
79
|
+
// contract — rather than navigating the document somewhere unintended.
|
|
80
|
+
let parsedEstablishUrl;
|
|
81
|
+
try {
|
|
82
|
+
parsedEstablishUrl = new URL(establishUrl);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (parsedEstablishUrl.protocol !== 'https:' ||
|
|
88
|
+
parsedEstablishUrl.pathname !== '/sso/establish' ||
|
|
89
|
+
!parsedEstablishUrl.hostname.toLowerCase().startsWith('auth.')) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
// Persist the bounce state ONLY now that the request has succeeded and the
|
|
93
|
+
// URL is validated — so a failed/rejected request never leaves stale state
|
|
94
|
+
// behind. This is the exact contract the terminal `/sso` bounce primes via
|
|
95
|
+
// `buildSsoBounceUrl`, so the post-bounce `sso-return` step (`consumeSsoReturn`)
|
|
96
|
+
// validates `state`, exchanges the returned code, and restores the dest.
|
|
97
|
+
storage.setItem(ssoStateKey(origin), state);
|
|
98
|
+
storage.setItem(ssoGuardKey(origin), String(now()));
|
|
99
|
+
storage.setItem(ssoDestKey(origin), location.href);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
deps.onError?.(error);
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
navigate(establishUrl);
|
|
106
|
+
return true;
|
|
107
|
+
}
|