@oxyhq/services 8.3.1 → 8.4.1
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/lib/commonjs/ui/context/OxyContext.js +41 -89
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js +8 -0
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/commonjs/ui/utils/activeAuthuser.js +27 -0
- package/lib/commonjs/ui/utils/activeAuthuser.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +36 -83
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/hooks/useAuthOperations.js +9 -1
- package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/module/ui/utils/activeAuthuser.js +26 -0
- package/lib/module/ui/utils/activeAuthuser.js.map +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts +9 -0
- package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/activeAuthuser.d.ts +9 -0
- package/lib/typescript/module/ui/utils/activeAuthuser.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/ui/context/OxyContext.tsx +45 -95
- package/src/ui/context/hooks/useAuthOperations.ts +9 -1
- package/src/ui/utils/activeAuthuser.ts +34 -0
- package/lib/commonjs/ui/utils/ssoBounce.js +0 -158
- package/lib/commonjs/ui/utils/ssoBounce.js.map +0 -1
- package/lib/module/ui/utils/ssoBounce.js +0 -148
- package/lib/module/ui/utils/ssoBounce.js.map +0 -1
- package/lib/typescript/commonjs/ui/utils/ssoBounce.d.ts +0 -89
- package/lib/typescript/commonjs/ui/utils/ssoBounce.d.ts.map +0 -1
- package/lib/typescript/module/ui/utils/ssoBounce.d.ts +0 -89
- package/lib/typescript/module/ui/utils/ssoBounce.d.ts.map +0 -1
- package/src/ui/utils/ssoBounce.ts +0 -146
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/services",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.4.1",
|
|
4
4
|
"description": "OxyHQ Expo/React Native SDK — UI components, screens, and native features",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
"peerDependencies": {
|
|
161
161
|
"@expo/vector-icons": "^15.0.3",
|
|
162
162
|
"@oxyhq/bloom": ">=0.5.0",
|
|
163
|
-
"@oxyhq/core": "^2.
|
|
163
|
+
"@oxyhq/core": "^2.3.0",
|
|
164
164
|
"@react-native-community/netinfo": "^11.4.1",
|
|
165
165
|
"@tanstack/query-async-storage-persister": "^5.100",
|
|
166
166
|
"@tanstack/query-sync-storage-persister": "^5.100",
|
|
@@ -14,18 +14,22 @@ import type { User, ApiError, SessionLoginResponse } from '@oxyhq/core';
|
|
|
14
14
|
import type { ManagedAccount, CreateManagedAccountInput } from '@oxyhq/core';
|
|
15
15
|
import { KeyManager } from '@oxyhq/core';
|
|
16
16
|
import type { ClientSession } from '@oxyhq/core';
|
|
17
|
-
import { runColdBoot, resolveCentralAuthUrl, parseSsoReturnFragment, autoDetectAuthWebUrl } from '@oxyhq/core';
|
|
18
|
-
import { toast } from '@oxyhq/bloom';
|
|
19
17
|
import {
|
|
20
|
-
|
|
18
|
+
runColdBoot,
|
|
19
|
+
resolveCentralAuthUrl,
|
|
20
|
+
autoDetectAuthWebUrl,
|
|
21
21
|
ssoStateKey,
|
|
22
22
|
ssoGuardKey,
|
|
23
23
|
ssoDestKey,
|
|
24
24
|
ssoNoSessionKey,
|
|
25
|
+
ssoAttemptedKey,
|
|
25
26
|
isCentralIdPOrigin,
|
|
26
27
|
guardActive,
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
ssoNavigate,
|
|
29
|
+
buildSsoBounceUrl,
|
|
30
|
+
consumeSsoReturn,
|
|
31
|
+
} from '@oxyhq/core';
|
|
32
|
+
import { toast } from '@oxyhq/bloom';
|
|
29
33
|
import { useAuthStore, type AuthState } from '../stores/authStore';
|
|
30
34
|
import { useShallow } from 'zustand/react/shallow';
|
|
31
35
|
import { useSessionSocket } from '../hooks/useSessionSocket';
|
|
@@ -745,99 +749,42 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
745
749
|
storageKeys.sessionIds,
|
|
746
750
|
]);
|
|
747
751
|
|
|
748
|
-
// Central cross-domain SSO return handler (web).
|
|
749
|
-
//
|
|
750
|
-
//
|
|
751
|
-
//
|
|
752
|
-
//
|
|
753
|
-
//
|
|
752
|
+
// Central cross-domain SSO return handler (web). A THIN wrapper over core's
|
|
753
|
+
// `consumeSsoReturn`, which performs the entire security-critical kernel —
|
|
754
|
+
// parse the IdP redirect fragment, validate the CSRF `state`, strip the
|
|
755
|
+
// fragment FIRST, exchange the opaque single-use code, restore the user's
|
|
756
|
+
// pre-bounce destination (same-origin only), and set the per-origin
|
|
757
|
+
// NO_SESSION loop breaker on every non-ok outcome — and RETURNS the exchanged
|
|
758
|
+
// session (or `null`) WITHOUT committing. We preserve services' contract by
|
|
759
|
+
// committing the returned session here via `handleWebSSOSession`. Shared by
|
|
760
|
+
// the `sso-return` cold-boot step AND the bfcache `pageshow` re-evaluation, so
|
|
761
|
+
// the same kernel runs exactly once per delivered fragment regardless of how
|
|
762
|
+
// the page was (re)shown.
|
|
754
763
|
//
|
|
755
764
|
// Returns `true` when a session was committed (caller short-circuits), `false`
|
|
756
|
-
// otherwise.
|
|
757
|
-
//
|
|
758
|
-
// so `sso-bounce` is disabled and the page cannot loop. Off-browser it is a
|
|
759
|
-
// no-op returning `false` (native never reaches it).
|
|
765
|
+
// otherwise. Off-browser `consumeSsoReturn` is a no-op returning `null`, so
|
|
766
|
+
// this returns `false` (native never reaches it).
|
|
760
767
|
const runSsoReturn = useCallback(async (): Promise<boolean> => {
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
// Strip the fragment FIRST so the opaque code never lingers in the address
|
|
776
|
-
// bar, history, or a copy-paste — even if a later step throws.
|
|
777
|
-
window.history.replaceState(null, '', window.location.pathname + window.location.search);
|
|
778
|
-
window.sessionStorage.removeItem(ssoStateKey(origin));
|
|
779
|
-
|
|
780
|
-
const markNoSession = () => {
|
|
781
|
-
window.sessionStorage.setItem(ssoNoSessionKey(origin), '1');
|
|
782
|
-
};
|
|
783
|
-
|
|
784
|
-
if (ret.kind === 'none' || ret.kind === 'error') {
|
|
785
|
-
// The central IdP had no session (or the bounce failed). Record it so we
|
|
786
|
-
// do not bounce again this tab — the definitive loop breaker.
|
|
787
|
-
markNoSession();
|
|
788
|
-
return false;
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
if (!stateOk || !ret.code) {
|
|
792
|
-
// Forged / replayed / stale fragment, or a malformed ok with no code.
|
|
793
|
-
// Treat exactly like "no session": never exchange, never loop.
|
|
794
|
-
markNoSession();
|
|
768
|
+
const session = await consumeSsoReturn(oxyServices, {
|
|
769
|
+
isWeb: isWebBrowser,
|
|
770
|
+
onExchangeError: (error) => {
|
|
771
|
+
if (__DEV__) {
|
|
772
|
+
loggerUtil.debug(
|
|
773
|
+
'SSO code exchange failed (treating as no session)',
|
|
774
|
+
{ component: 'OxyContext', method: 'runSsoReturn' },
|
|
775
|
+
error,
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
});
|
|
780
|
+
if (!session) {
|
|
795
781
|
return false;
|
|
796
782
|
}
|
|
797
|
-
|
|
798
783
|
const commitWebSession = handleWebSSOSessionRef.current;
|
|
799
|
-
|
|
800
|
-
try {
|
|
801
|
-
session = await oxyServices.exchangeSsoCode(ret.code);
|
|
802
|
-
} catch (error) {
|
|
803
|
-
if (__DEV__) {
|
|
804
|
-
loggerUtil.debug(
|
|
805
|
-
'SSO code exchange failed (treating as no session)',
|
|
806
|
-
{ component: 'OxyContext', method: 'runSsoReturn' },
|
|
807
|
-
error,
|
|
808
|
-
);
|
|
809
|
-
}
|
|
810
|
-
markNoSession();
|
|
811
|
-
return false;
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
if (!session?.sessionId || !commitWebSession) {
|
|
815
|
-
markNoSession();
|
|
784
|
+
if (!commitWebSession) {
|
|
816
785
|
return false;
|
|
817
786
|
}
|
|
818
|
-
|
|
819
787
|
await commitWebSession(session);
|
|
820
|
-
|
|
821
|
-
// Restore the user's real destination captured before the bounce. We only
|
|
822
|
-
// rewrite the URL when we are sitting on the callback path — otherwise the
|
|
823
|
-
// current URL is already the destination.
|
|
824
|
-
if (window.location.pathname === SSO_CALLBACK_PATH) {
|
|
825
|
-
const dest = window.sessionStorage.getItem(ssoDestKey(origin));
|
|
826
|
-
if (dest) {
|
|
827
|
-
try {
|
|
828
|
-
const destUrl = new URL(dest);
|
|
829
|
-
// Same-origin only — never honour a cross-origin destination that
|
|
830
|
-
// could have been planted to redirect the freshly signed-in user.
|
|
831
|
-
if (destUrl.origin === origin) {
|
|
832
|
-
window.history.replaceState(null, '', destUrl.pathname + destUrl.search + destUrl.hash);
|
|
833
|
-
}
|
|
834
|
-
} catch {
|
|
835
|
-
// Malformed stored destination — leave the URL on the callback path.
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
window.sessionStorage.removeItem(ssoDestKey(origin));
|
|
840
|
-
|
|
841
788
|
return true;
|
|
842
789
|
}, [oxyServices]);
|
|
843
790
|
|
|
@@ -1001,6 +948,9 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1001
948
|
if (window.sessionStorage.getItem(ssoNoSessionKey(origin)) === '1') {
|
|
1002
949
|
return false;
|
|
1003
950
|
}
|
|
951
|
+
if (window.sessionStorage.getItem(ssoAttemptedKey(origin)) === '1') {
|
|
952
|
+
return false;
|
|
953
|
+
}
|
|
1004
954
|
if (guardActive(window.sessionStorage, origin, Date.now())) {
|
|
1005
955
|
return false;
|
|
1006
956
|
}
|
|
@@ -1012,18 +962,18 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1012
962
|
window.sessionStorage.setItem(ssoStateKey(origin), state);
|
|
1013
963
|
window.sessionStorage.setItem(ssoGuardKey(origin), String(Date.now()));
|
|
1014
964
|
window.sessionStorage.setItem(ssoDestKey(origin), window.location.href);
|
|
965
|
+
// OUTCOME-INDEPENDENT once-guard: mark the probe attempted the instant we
|
|
966
|
+
// commit to the bounce, so even if the callback never lands cleanly no
|
|
967
|
+
// second bounce can ever fire this tab (the definitive loop breaker).
|
|
968
|
+
window.sessionStorage.setItem(ssoAttemptedKey(origin), '1');
|
|
1015
969
|
|
|
1016
|
-
const url =
|
|
1017
|
-
url.searchParams.set('prompt', 'none');
|
|
1018
|
-
url.searchParams.set('client_id', origin);
|
|
1019
|
-
url.searchParams.set('return_to', origin + SSO_CALLBACK_PATH);
|
|
1020
|
-
url.searchParams.set('state', state);
|
|
970
|
+
const url = buildSsoBounceUrl(origin, state, oxyServices.config?.authWebUrl);
|
|
1021
971
|
|
|
1022
972
|
// TERMINAL: the document is torn down by this navigation. The
|
|
1023
973
|
// `skip` below is only reached if `assign` is a no-op (e.g. the
|
|
1024
974
|
// navigation is blocked); in that case we fall through
|
|
1025
975
|
// unauthenticated, which is correct.
|
|
1026
|
-
|
|
976
|
+
ssoNavigate(url);
|
|
1027
977
|
return { kind: 'skip' };
|
|
1028
978
|
},
|
|
1029
979
|
},
|
|
@@ -9,7 +9,7 @@ import type { StorageInterface } from '../../utils/storageHelpers';
|
|
|
9
9
|
import type { OxyServices } from '@oxyhq/core';
|
|
10
10
|
import { SignatureService } from '@oxyhq/core';
|
|
11
11
|
import { isWebBrowser } from '../../hooks/useWebSSO';
|
|
12
|
-
import { clearActiveAuthuser } from '../../utils/activeAuthuser';
|
|
12
|
+
import { clearActiveAuthuser, clearSsoBounceState } from '../../utils/activeAuthuser';
|
|
13
13
|
|
|
14
14
|
/** Type guard for error objects with optional code and status properties */
|
|
15
15
|
function isErrorWithCodeOrStatus(error: unknown): error is { code?: string; status?: number; message?: string } {
|
|
@@ -323,6 +323,9 @@ export const useAuthOperations = ({
|
|
|
323
323
|
if (filteredSessions.length > 0) {
|
|
324
324
|
await switchSession(filteredSessions[0].sessionId);
|
|
325
325
|
} else {
|
|
326
|
+
// Genuine FULL sign-out (no sessions remain): clear the per-origin
|
|
327
|
+
// SSO bounce state so a fresh deliberate sign-in can re-probe.
|
|
328
|
+
clearSsoBounceState();
|
|
326
329
|
await clearSessionState();
|
|
327
330
|
return;
|
|
328
331
|
}
|
|
@@ -331,6 +334,8 @@ export const useAuthOperations = ({
|
|
|
331
334
|
const isInvalid = isInvalidSessionError(error);
|
|
332
335
|
|
|
333
336
|
if (isInvalid && targetSessionId === activeSessionId) {
|
|
337
|
+
// The active session is invalid → full sign-out; clear SSO state too.
|
|
338
|
+
clearSsoBounceState();
|
|
334
339
|
await clearSessionState();
|
|
335
340
|
return;
|
|
336
341
|
}
|
|
@@ -390,6 +395,9 @@ export const useAuthOperations = ({
|
|
|
390
395
|
} else {
|
|
391
396
|
await oxyServices.logoutAllSessions(activeSessionId);
|
|
392
397
|
}
|
|
398
|
+
// logoutAll is ALWAYS a full sign-out: clear the per-origin SSO bounce
|
|
399
|
+
// state (web-guarded internally) so a fresh sign-in can re-probe.
|
|
400
|
+
clearSsoBounceState();
|
|
393
401
|
await clearSessionState();
|
|
394
402
|
} catch (error) {
|
|
395
403
|
handleAuthError(error, {
|
|
@@ -18,12 +18,24 @@
|
|
|
18
18
|
* outside the browser.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
import {
|
|
22
|
+
ssoAttemptedKey,
|
|
23
|
+
ssoNoSessionKey,
|
|
24
|
+
ssoGuardKey,
|
|
25
|
+
ssoStateKey,
|
|
26
|
+
ssoDestKey,
|
|
27
|
+
} from '@oxyhq/core';
|
|
28
|
+
|
|
21
29
|
const ACTIVE_AUTHUSER_KEY = 'oxy_active_authuser';
|
|
22
30
|
|
|
23
31
|
function hasLocalStorage(): boolean {
|
|
24
32
|
return typeof window !== 'undefined' && typeof window.localStorage !== 'undefined';
|
|
25
33
|
}
|
|
26
34
|
|
|
35
|
+
function hasSessionStorage(): boolean {
|
|
36
|
+
return typeof window !== 'undefined' && typeof window.sessionStorage !== 'undefined';
|
|
37
|
+
}
|
|
38
|
+
|
|
27
39
|
/**
|
|
28
40
|
* Read the persisted active `authuser` slot index.
|
|
29
41
|
*
|
|
@@ -75,4 +87,26 @@ export function clearActiveAuthuser(): void {
|
|
|
75
87
|
}
|
|
76
88
|
}
|
|
77
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Clear all per-origin SSO bounce sessionStorage keys. Called ONLY on EXPLICIT
|
|
92
|
+
* user sign-out (logout / logoutAll) — never on a cold-boot failure path — so a
|
|
93
|
+
* fresh deliberate sign-in can re-probe the central IdP. Clearing on cold-boot
|
|
94
|
+
* failure would reintroduce the redirect loop.
|
|
95
|
+
*
|
|
96
|
+
* No-ops on native and on any storage failure (best-effort).
|
|
97
|
+
*/
|
|
98
|
+
export function clearSsoBounceState(): void {
|
|
99
|
+
if (!hasSessionStorage()) return;
|
|
100
|
+
const origin = window.location.origin;
|
|
101
|
+
try {
|
|
102
|
+
window.sessionStorage.removeItem(ssoAttemptedKey(origin));
|
|
103
|
+
window.sessionStorage.removeItem(ssoNoSessionKey(origin));
|
|
104
|
+
window.sessionStorage.removeItem(ssoGuardKey(origin));
|
|
105
|
+
window.sessionStorage.removeItem(ssoStateKey(origin));
|
|
106
|
+
window.sessionStorage.removeItem(ssoDestKey(origin));
|
|
107
|
+
} catch {
|
|
108
|
+
// Best-effort; swallow SecurityError (e.g. Safari private mode).
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
78
112
|
export { ACTIVE_AUTHUSER_KEY };
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.SSO_GUARD_TTL_MS = exports.SSO_CALLBACK_PATH = void 0;
|
|
7
|
-
exports.guardActive = guardActive;
|
|
8
|
-
exports.isCentralIdPOrigin = isCentralIdPOrigin;
|
|
9
|
-
exports.ssoDestKey = ssoDestKey;
|
|
10
|
-
exports.ssoGuardKey = ssoGuardKey;
|
|
11
|
-
exports.ssoNavigate = ssoNavigate;
|
|
12
|
-
exports.ssoNoSessionKey = ssoNoSessionKey;
|
|
13
|
-
exports.ssoStateKey = ssoStateKey;
|
|
14
|
-
var _core = require("@oxyhq/core");
|
|
15
|
-
/**
|
|
16
|
-
* Central cross-domain SSO bounce — per-origin sessionStorage keys and small
|
|
17
|
-
* pure predicates shared by the cold-boot `sso-return` / `sso-bounce` steps and
|
|
18
|
-
* the bfcache `pageshow` re-evaluation.
|
|
19
|
-
*
|
|
20
|
-
* TRUE central SSO (Google/Meta/Clerk style) works like this for a Relying
|
|
21
|
-
* Party (mention.earth, homiio.com, alia.onl, …) with no local session:
|
|
22
|
-
*
|
|
23
|
-
* 1. `sso-bounce` (terminal, once): top-level navigate to
|
|
24
|
-
* `auth.oxy.so/sso?prompt=none&client_id=<origin>&return_to=<origin>/__oxy/sso-callback&state=<s>`.
|
|
25
|
-
* Before navigating it records, in this origin's `sessionStorage`, the CSRF
|
|
26
|
-
* `state`, a guard timestamp (loop breaker), and the real destination URL
|
|
27
|
-
* to restore after the callback.
|
|
28
|
-
* 2. The central IdP worker reads its first-party `fedcm_session`, mints a
|
|
29
|
-
* session, stores it under an opaque single-use `code`, and 303-redirects
|
|
30
|
-
* back to `<origin>/__oxy/sso-callback#oxy_sso=ok&code=<code>&state=<s>`
|
|
31
|
-
* (or `#oxy_sso=none` / `#oxy_sso=error`).
|
|
32
|
-
* 3. `sso-return` parses the fragment (`parseSsoReturnFragment` from core),
|
|
33
|
-
* validates `state`, exchanges the `code` via `oxyServices.exchangeSsoCode`,
|
|
34
|
-
* and commits the session — then restores the original destination.
|
|
35
|
-
*
|
|
36
|
-
* Loop proof (logged-out): first load all steps skip → `sso-bounce` sets
|
|
37
|
-
* guard/state/dest and navigates; the IdP (no central session) returns
|
|
38
|
-
* `#oxy_sso=none`; the callback load's `sso-return` sees `none`, sets the
|
|
39
|
-
* no-session flag, and `sso-bounce` is then disabled. Exactly ONE bounce, no
|
|
40
|
-
* loop. An interrupted bounce (user hit back mid-redirect) self-heals once the
|
|
41
|
-
* 30s guard TTL lapses.
|
|
42
|
-
*
|
|
43
|
-
* All state lives in `sessionStorage` (per tab, cleared on tab close) and is
|
|
44
|
-
* keyed per-origin so two RPs hosted in the same browser never collide. This
|
|
45
|
-
* module is pure with respect to navigation: it only reads/writes
|
|
46
|
-
* `sessionStorage` and parses URLs; it performs no redirects itself.
|
|
47
|
-
*/
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* The RP callback path the central IdP redirects back to. The SSO result is
|
|
51
|
-
* delivered in the fragment of this URL; `sso-return` consumes it and then
|
|
52
|
-
* restores the user's real destination.
|
|
53
|
-
*/
|
|
54
|
-
const SSO_CALLBACK_PATH = exports.SSO_CALLBACK_PATH = '/__oxy/sso-callback';
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Self-healing TTL (ms) for the bounce guard. If a bounce is interrupted before
|
|
58
|
-
* the callback lands (e.g. the user navigates back mid-redirect), the guard
|
|
59
|
-
* would otherwise pin the RP signed-out forever. After this window the guard is
|
|
60
|
-
* treated as stale and a fresh single bounce is permitted.
|
|
61
|
-
*/
|
|
62
|
-
const SSO_GUARD_TTL_MS = exports.SSO_GUARD_TTL_MS = 30_000;
|
|
63
|
-
const STATE_KEY_PREFIX = 'oxy_sso_state:';
|
|
64
|
-
const GUARD_KEY_PREFIX = 'oxy_sso_guard:';
|
|
65
|
-
const DEST_KEY_PREFIX = 'oxy_sso_dest:';
|
|
66
|
-
const NO_SESSION_KEY_PREFIX = 'oxy_sso_no_session:';
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Perform the terminal top-level SSO bounce navigation.
|
|
70
|
-
*
|
|
71
|
-
* A thin wrapper over `window.location.assign(url)` so the single navigation
|
|
72
|
-
* seam lives in one place (and stays mockable in tests, where jsdom's
|
|
73
|
-
* `Location.assign` is a non-configurable native method). In production this is
|
|
74
|
-
* exactly `window.location.assign` — the document is torn down and replaced by
|
|
75
|
-
* the central IdP page. Off-browser it is a no-op (native never bounces).
|
|
76
|
-
*/
|
|
77
|
-
function ssoNavigate(url) {
|
|
78
|
-
if (typeof window === 'undefined' || typeof window.location === 'undefined') {
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
window.location.assign(url);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Per-origin CSRF state key (matched on return to defeat fragment forgery). */
|
|
85
|
-
function ssoStateKey(origin) {
|
|
86
|
-
return `${STATE_KEY_PREFIX}${origin}`;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Per-origin bounce guard key (a timestamp; loop breaker + self-heal TTL). */
|
|
90
|
-
function ssoGuardKey(origin) {
|
|
91
|
-
return `${GUARD_KEY_PREFIX}${origin}`;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Per-origin destination key (the real URL to restore after the callback). */
|
|
95
|
-
function ssoDestKey(origin) {
|
|
96
|
-
return `${DEST_KEY_PREFIX}${origin}`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Per-origin "the central IdP has no session for me" key. Set after a
|
|
101
|
-
* `none`/`error` return (or a failed/forged exchange) so `sso-bounce` does not
|
|
102
|
-
* fire again this tab — the definitive loop breaker.
|
|
103
|
-
*/
|
|
104
|
-
function ssoNoSessionKey(origin) {
|
|
105
|
-
return `${NO_SESSION_KEY_PREFIX}${origin}`;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Whether `origin` IS the central IdP origin. We must never bounce while on
|
|
110
|
-
* `auth.oxy.so` itself (it would bounce to itself). Compared by URL origin so a
|
|
111
|
-
* trailing-slash / path difference never defeats the guard.
|
|
112
|
-
*/
|
|
113
|
-
function isCentralIdPOrigin(origin) {
|
|
114
|
-
let centralOrigin;
|
|
115
|
-
try {
|
|
116
|
-
centralOrigin = new URL(_core.CENTRAL_AUTH_URL).origin;
|
|
117
|
-
} catch {
|
|
118
|
-
return false;
|
|
119
|
-
}
|
|
120
|
-
let candidateOrigin;
|
|
121
|
-
try {
|
|
122
|
-
candidateOrigin = new URL(origin).origin;
|
|
123
|
-
} catch {
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
126
|
-
return candidateOrigin === centralOrigin;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Read the bounce guard and decide whether it is still ACTIVE.
|
|
131
|
-
*
|
|
132
|
-
* Active means: a guard value is present AND it parses to a finite timestamp AND
|
|
133
|
-
* less than {@link SSO_GUARD_TTL_MS} has elapsed since it was set. An active
|
|
134
|
-
* guard disables `sso-bounce` (a bounce is already in flight this tab). A
|
|
135
|
-
* missing, malformed, or expired guard is NOT active, so a fresh bounce may
|
|
136
|
-
* proceed (this is the 30s self-heal for an interrupted bounce).
|
|
137
|
-
*
|
|
138
|
-
* @param storage - The session storage to read (injected for testability).
|
|
139
|
-
* @param origin - The page origin whose guard to evaluate.
|
|
140
|
-
* @param now - Current epoch ms (injected for deterministic tests).
|
|
141
|
-
*/
|
|
142
|
-
function guardActive(storage, origin, now) {
|
|
143
|
-
let raw;
|
|
144
|
-
try {
|
|
145
|
-
raw = storage.getItem(ssoGuardKey(origin));
|
|
146
|
-
} catch {
|
|
147
|
-
return false;
|
|
148
|
-
}
|
|
149
|
-
if (raw === null || raw.length === 0) {
|
|
150
|
-
return false;
|
|
151
|
-
}
|
|
152
|
-
const stamp = Number(raw);
|
|
153
|
-
if (!Number.isFinite(stamp)) {
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
return now - stamp < SSO_GUARD_TTL_MS;
|
|
157
|
-
}
|
|
158
|
-
//# sourceMappingURL=ssoBounce.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"names":["_core","require","SSO_CALLBACK_PATH","exports","SSO_GUARD_TTL_MS","STATE_KEY_PREFIX","GUARD_KEY_PREFIX","DEST_KEY_PREFIX","NO_SESSION_KEY_PREFIX","ssoNavigate","url","window","location","assign","ssoStateKey","origin","ssoGuardKey","ssoDestKey","ssoNoSessionKey","isCentralIdPOrigin","centralOrigin","URL","CENTRAL_AUTH_URL","candidateOrigin","guardActive","storage","now","raw","getItem","length","stamp","Number","isFinite"],"sourceRoot":"../../../../src","sources":["ui/utils/ssoBounce.ts"],"mappings":";;;;;;;;;;;;;AAkCA,IAAAA,KAAA,GAAAC,OAAA;AAlCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAIA;AACA;AACA;AACA;AACA;AACO,MAAMC,iBAAiB,GAAAC,OAAA,CAAAD,iBAAA,GAAG,qBAAqB;;AAEtD;AACA;AACA;AACA;AACA;AACA;AACO,MAAME,gBAAgB,GAAAD,OAAA,CAAAC,gBAAA,GAAG,MAAM;AAEtC,MAAMC,gBAAgB,GAAG,gBAAgB;AACzC,MAAMC,gBAAgB,GAAG,gBAAgB;AACzC,MAAMC,eAAe,GAAG,eAAe;AACvC,MAAMC,qBAAqB,GAAG,qBAAqB;;AAEnD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASC,WAAWA,CAACC,GAAW,EAAQ;EAC7C,IAAI,OAAOC,MAAM,KAAK,WAAW,IAAI,OAAOA,MAAM,CAACC,QAAQ,KAAK,WAAW,EAAE;IAC3E;EACF;EACAD,MAAM,CAACC,QAAQ,CAACC,MAAM,CAACH,GAAG,CAAC;AAC7B;;AAEA;AACO,SAASI,WAAWA,CAACC,MAAc,EAAU;EAClD,OAAO,GAAGV,gBAAgB,GAAGU,MAAM,EAAE;AACvC;;AAEA;AACO,SAASC,WAAWA,CAACD,MAAc,EAAU;EAClD,OAAO,GAAGT,gBAAgB,GAAGS,MAAM,EAAE;AACvC;;AAEA;AACO,SAASE,UAAUA,CAACF,MAAc,EAAU;EACjD,OAAO,GAAGR,eAAe,GAAGQ,MAAM,EAAE;AACtC;;AAEA;AACA;AACA;AACA;AACA;AACO,SAASG,eAAeA,CAACH,MAAc,EAAU;EACtD,OAAO,GAAGP,qBAAqB,GAAGO,MAAM,EAAE;AAC5C;;AAEA;AACA;AACA;AACA;AACA;AACO,SAASI,kBAAkBA,CAACJ,MAAc,EAAW;EAC1D,IAAIK,aAAqB;EACzB,IAAI;IACFA,aAAa,GAAG,IAAIC,GAAG,CAACC,sBAAgB,CAAC,CAACP,MAAM;EAClD,CAAC,CAAC,MAAM;IACN,OAAO,KAAK;EACd;EACA,IAAIQ,eAAuB;EAC3B,IAAI;IACFA,eAAe,GAAG,IAAIF,GAAG,CAACN,MAAM,CAAC,CAACA,MAAM;EAC1C,CAAC,CAAC,MAAM;IACN,OAAO,KAAK;EACd;EACA,OAAOQ,eAAe,KAAKH,aAAa;AAC1C;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASI,WAAWA,CAACC,OAAgB,EAAEV,MAAc,EAAEW,GAAW,EAAW;EAClF,IAAIC,GAAkB;EACtB,IAAI;IACFA,GAAG,GAAGF,OAAO,CAACG,OAAO,CAACZ,WAAW,CAACD,MAAM,CAAC,CAAC;EAC5C,CAAC,CAAC,MAAM;IACN,OAAO,KAAK;EACd;EACA,IAAIY,GAAG,KAAK,IAAI,IAAIA,GAAG,CAACE,MAAM,KAAK,CAAC,EAAE;IACpC,OAAO,KAAK;EACd;EACA,MAAMC,KAAK,GAAGC,MAAM,CAACJ,GAAG,CAAC;EACzB,IAAI,CAACI,MAAM,CAACC,QAAQ,CAACF,KAAK,CAAC,EAAE;IAC3B,OAAO,KAAK;EACd;EACA,OAAOJ,GAAG,GAAGI,KAAK,GAAG1B,gBAAgB;AACvC","ignoreList":[]}
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Central cross-domain SSO bounce — per-origin sessionStorage keys and small
|
|
5
|
-
* pure predicates shared by the cold-boot `sso-return` / `sso-bounce` steps and
|
|
6
|
-
* the bfcache `pageshow` re-evaluation.
|
|
7
|
-
*
|
|
8
|
-
* TRUE central SSO (Google/Meta/Clerk style) works like this for a Relying
|
|
9
|
-
* Party (mention.earth, homiio.com, alia.onl, …) with no local session:
|
|
10
|
-
*
|
|
11
|
-
* 1. `sso-bounce` (terminal, once): top-level navigate to
|
|
12
|
-
* `auth.oxy.so/sso?prompt=none&client_id=<origin>&return_to=<origin>/__oxy/sso-callback&state=<s>`.
|
|
13
|
-
* Before navigating it records, in this origin's `sessionStorage`, the CSRF
|
|
14
|
-
* `state`, a guard timestamp (loop breaker), and the real destination URL
|
|
15
|
-
* to restore after the callback.
|
|
16
|
-
* 2. The central IdP worker reads its first-party `fedcm_session`, mints a
|
|
17
|
-
* session, stores it under an opaque single-use `code`, and 303-redirects
|
|
18
|
-
* back to `<origin>/__oxy/sso-callback#oxy_sso=ok&code=<code>&state=<s>`
|
|
19
|
-
* (or `#oxy_sso=none` / `#oxy_sso=error`).
|
|
20
|
-
* 3. `sso-return` parses the fragment (`parseSsoReturnFragment` from core),
|
|
21
|
-
* validates `state`, exchanges the `code` via `oxyServices.exchangeSsoCode`,
|
|
22
|
-
* and commits the session — then restores the original destination.
|
|
23
|
-
*
|
|
24
|
-
* Loop proof (logged-out): first load all steps skip → `sso-bounce` sets
|
|
25
|
-
* guard/state/dest and navigates; the IdP (no central session) returns
|
|
26
|
-
* `#oxy_sso=none`; the callback load's `sso-return` sees `none`, sets the
|
|
27
|
-
* no-session flag, and `sso-bounce` is then disabled. Exactly ONE bounce, no
|
|
28
|
-
* loop. An interrupted bounce (user hit back mid-redirect) self-heals once the
|
|
29
|
-
* 30s guard TTL lapses.
|
|
30
|
-
*
|
|
31
|
-
* All state lives in `sessionStorage` (per tab, cleared on tab close) and is
|
|
32
|
-
* keyed per-origin so two RPs hosted in the same browser never collide. This
|
|
33
|
-
* module is pure with respect to navigation: it only reads/writes
|
|
34
|
-
* `sessionStorage` and parses URLs; it performs no redirects itself.
|
|
35
|
-
*/
|
|
36
|
-
|
|
37
|
-
import { CENTRAL_AUTH_URL } from '@oxyhq/core';
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* The RP callback path the central IdP redirects back to. The SSO result is
|
|
41
|
-
* delivered in the fragment of this URL; `sso-return` consumes it and then
|
|
42
|
-
* restores the user's real destination.
|
|
43
|
-
*/
|
|
44
|
-
export const SSO_CALLBACK_PATH = '/__oxy/sso-callback';
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Self-healing TTL (ms) for the bounce guard. If a bounce is interrupted before
|
|
48
|
-
* the callback lands (e.g. the user navigates back mid-redirect), the guard
|
|
49
|
-
* would otherwise pin the RP signed-out forever. After this window the guard is
|
|
50
|
-
* treated as stale and a fresh single bounce is permitted.
|
|
51
|
-
*/
|
|
52
|
-
export const SSO_GUARD_TTL_MS = 30_000;
|
|
53
|
-
const STATE_KEY_PREFIX = 'oxy_sso_state:';
|
|
54
|
-
const GUARD_KEY_PREFIX = 'oxy_sso_guard:';
|
|
55
|
-
const DEST_KEY_PREFIX = 'oxy_sso_dest:';
|
|
56
|
-
const NO_SESSION_KEY_PREFIX = 'oxy_sso_no_session:';
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Perform the terminal top-level SSO bounce navigation.
|
|
60
|
-
*
|
|
61
|
-
* A thin wrapper over `window.location.assign(url)` so the single navigation
|
|
62
|
-
* seam lives in one place (and stays mockable in tests, where jsdom's
|
|
63
|
-
* `Location.assign` is a non-configurable native method). In production this is
|
|
64
|
-
* exactly `window.location.assign` — the document is torn down and replaced by
|
|
65
|
-
* the central IdP page. Off-browser it is a no-op (native never bounces).
|
|
66
|
-
*/
|
|
67
|
-
export function ssoNavigate(url) {
|
|
68
|
-
if (typeof window === 'undefined' || typeof window.location === 'undefined') {
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
window.location.assign(url);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Per-origin CSRF state key (matched on return to defeat fragment forgery). */
|
|
75
|
-
export function ssoStateKey(origin) {
|
|
76
|
-
return `${STATE_KEY_PREFIX}${origin}`;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Per-origin bounce guard key (a timestamp; loop breaker + self-heal TTL). */
|
|
80
|
-
export function ssoGuardKey(origin) {
|
|
81
|
-
return `${GUARD_KEY_PREFIX}${origin}`;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Per-origin destination key (the real URL to restore after the callback). */
|
|
85
|
-
export function ssoDestKey(origin) {
|
|
86
|
-
return `${DEST_KEY_PREFIX}${origin}`;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Per-origin "the central IdP has no session for me" key. Set after a
|
|
91
|
-
* `none`/`error` return (or a failed/forged exchange) so `sso-bounce` does not
|
|
92
|
-
* fire again this tab — the definitive loop breaker.
|
|
93
|
-
*/
|
|
94
|
-
export function ssoNoSessionKey(origin) {
|
|
95
|
-
return `${NO_SESSION_KEY_PREFIX}${origin}`;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Whether `origin` IS the central IdP origin. We must never bounce while on
|
|
100
|
-
* `auth.oxy.so` itself (it would bounce to itself). Compared by URL origin so a
|
|
101
|
-
* trailing-slash / path difference never defeats the guard.
|
|
102
|
-
*/
|
|
103
|
-
export function isCentralIdPOrigin(origin) {
|
|
104
|
-
let centralOrigin;
|
|
105
|
-
try {
|
|
106
|
-
centralOrigin = new URL(CENTRAL_AUTH_URL).origin;
|
|
107
|
-
} catch {
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
let candidateOrigin;
|
|
111
|
-
try {
|
|
112
|
-
candidateOrigin = new URL(origin).origin;
|
|
113
|
-
} catch {
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
return candidateOrigin === centralOrigin;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Read the bounce guard and decide whether it is still ACTIVE.
|
|
121
|
-
*
|
|
122
|
-
* Active means: a guard value is present AND it parses to a finite timestamp AND
|
|
123
|
-
* less than {@link SSO_GUARD_TTL_MS} has elapsed since it was set. An active
|
|
124
|
-
* guard disables `sso-bounce` (a bounce is already in flight this tab). A
|
|
125
|
-
* missing, malformed, or expired guard is NOT active, so a fresh bounce may
|
|
126
|
-
* proceed (this is the 30s self-heal for an interrupted bounce).
|
|
127
|
-
*
|
|
128
|
-
* @param storage - The session storage to read (injected for testability).
|
|
129
|
-
* @param origin - The page origin whose guard to evaluate.
|
|
130
|
-
* @param now - Current epoch ms (injected for deterministic tests).
|
|
131
|
-
*/
|
|
132
|
-
export function guardActive(storage, origin, now) {
|
|
133
|
-
let raw;
|
|
134
|
-
try {
|
|
135
|
-
raw = storage.getItem(ssoGuardKey(origin));
|
|
136
|
-
} catch {
|
|
137
|
-
return false;
|
|
138
|
-
}
|
|
139
|
-
if (raw === null || raw.length === 0) {
|
|
140
|
-
return false;
|
|
141
|
-
}
|
|
142
|
-
const stamp = Number(raw);
|
|
143
|
-
if (!Number.isFinite(stamp)) {
|
|
144
|
-
return false;
|
|
145
|
-
}
|
|
146
|
-
return now - stamp < SSO_GUARD_TTL_MS;
|
|
147
|
-
}
|
|
148
|
-
//# sourceMappingURL=ssoBounce.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"names":["CENTRAL_AUTH_URL","SSO_CALLBACK_PATH","SSO_GUARD_TTL_MS","STATE_KEY_PREFIX","GUARD_KEY_PREFIX","DEST_KEY_PREFIX","NO_SESSION_KEY_PREFIX","ssoNavigate","url","window","location","assign","ssoStateKey","origin","ssoGuardKey","ssoDestKey","ssoNoSessionKey","isCentralIdPOrigin","centralOrigin","URL","candidateOrigin","guardActive","storage","now","raw","getItem","length","stamp","Number","isFinite"],"sourceRoot":"../../../../src","sources":["ui/utils/ssoBounce.ts"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,SAASA,gBAAgB,QAAQ,aAAa;;AAE9C;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMC,iBAAiB,GAAG,qBAAqB;;AAEtD;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMC,gBAAgB,GAAG,MAAM;AAEtC,MAAMC,gBAAgB,GAAG,gBAAgB;AACzC,MAAMC,gBAAgB,GAAG,gBAAgB;AACzC,MAAMC,eAAe,GAAG,eAAe;AACvC,MAAMC,qBAAqB,GAAG,qBAAqB;;AAEnD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,WAAWA,CAACC,GAAW,EAAQ;EAC7C,IAAI,OAAOC,MAAM,KAAK,WAAW,IAAI,OAAOA,MAAM,CAACC,QAAQ,KAAK,WAAW,EAAE;IAC3E;EACF;EACAD,MAAM,CAACC,QAAQ,CAACC,MAAM,CAACH,GAAG,CAAC;AAC7B;;AAEA;AACA,OAAO,SAASI,WAAWA,CAACC,MAAc,EAAU;EAClD,OAAO,GAAGV,gBAAgB,GAAGU,MAAM,EAAE;AACvC;;AAEA;AACA,OAAO,SAASC,WAAWA,CAACD,MAAc,EAAU;EAClD,OAAO,GAAGT,gBAAgB,GAAGS,MAAM,EAAE;AACvC;;AAEA;AACA,OAAO,SAASE,UAAUA,CAACF,MAAc,EAAU;EACjD,OAAO,GAAGR,eAAe,GAAGQ,MAAM,EAAE;AACtC;;AAEA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASG,eAAeA,CAACH,MAAc,EAAU;EACtD,OAAO,GAAGP,qBAAqB,GAAGO,MAAM,EAAE;AAC5C;;AAEA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASI,kBAAkBA,CAACJ,MAAc,EAAW;EAC1D,IAAIK,aAAqB;EACzB,IAAI;IACFA,aAAa,GAAG,IAAIC,GAAG,CAACnB,gBAAgB,CAAC,CAACa,MAAM;EAClD,CAAC,CAAC,MAAM;IACN,OAAO,KAAK;EACd;EACA,IAAIO,eAAuB;EAC3B,IAAI;IACFA,eAAe,GAAG,IAAID,GAAG,CAACN,MAAM,CAAC,CAACA,MAAM;EAC1C,CAAC,CAAC,MAAM;IACN,OAAO,KAAK;EACd;EACA,OAAOO,eAAe,KAAKF,aAAa;AAC1C;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASG,WAAWA,CAACC,OAAgB,EAAET,MAAc,EAAEU,GAAW,EAAW;EAClF,IAAIC,GAAkB;EACtB,IAAI;IACFA,GAAG,GAAGF,OAAO,CAACG,OAAO,CAACX,WAAW,CAACD,MAAM,CAAC,CAAC;EAC5C,CAAC,CAAC,MAAM;IACN,OAAO,KAAK;EACd;EACA,IAAIW,GAAG,KAAK,IAAI,IAAIA,GAAG,CAACE,MAAM,KAAK,CAAC,EAAE;IACpC,OAAO,KAAK;EACd;EACA,MAAMC,KAAK,GAAGC,MAAM,CAACJ,GAAG,CAAC;EACzB,IAAI,CAACI,MAAM,CAACC,QAAQ,CAACF,KAAK,CAAC,EAAE;IAC3B,OAAO,KAAK;EACd;EACA,OAAOJ,GAAG,GAAGI,KAAK,GAAGzB,gBAAgB;AACvC","ignoreList":[]}
|