@oxyhq/services 8.5.0 → 8.6.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/lib/commonjs/ui/context/OxyContext.js +153 -38
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +153 -38
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/ui/context/OxyContext.tsx +147 -32
|
@@ -178,6 +178,32 @@ function silentColdBootKey(oxyServices: OxyServices): string {
|
|
|
178
178
|
return `${origin}|${baseURL}`;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Per-step fail-fast budget for the cold-boot silent iframe (`silentSignIn`
|
|
183
|
+
* against the per-apex `/auth/silent` host).
|
|
184
|
+
*
|
|
185
|
+
* This step ONLY succeeds when a durable per-apex `fedcm_session` cookie exists
|
|
186
|
+
* (established by a prior `/sso` bounce). On the common reload of a logged-out
|
|
187
|
+
* tab — or a tab that restores via the now-earlier stored-session step — the
|
|
188
|
+
* iframe never posts a message, so the full wait would be dead latency in front
|
|
189
|
+
* of the terminal `/sso` bounce. `silentSignIn` already fails fast on a load
|
|
190
|
+
* error via `iframe.onerror`; this caps the no-message case. 2.5s is well above
|
|
191
|
+
* a same-origin iframe handshake yet a fraction of the legacy 5s default.
|
|
192
|
+
*/
|
|
193
|
+
const SILENT_IFRAME_TIMEOUT = 2500;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Per-step fail-fast budget for the cold-boot refresh-cookie restore
|
|
197
|
+
* (`refreshAllSessions`).
|
|
198
|
+
*
|
|
199
|
+
* On a cross-domain RP the `Domain=oxy.so` refresh cookie never reaches
|
|
200
|
+
* `api.<apex>`, so this request returns no accounts (or stalls behind a slow
|
|
201
|
+
* endpoint) with no useful answer. As one cold-boot step it must not block the
|
|
202
|
+
* fall-through to the terminal `/sso` bounce. 3s bounds the wait while leaving
|
|
203
|
+
* ample headroom for a genuine first-party `*.oxy.so` rotation round-trip.
|
|
204
|
+
*/
|
|
205
|
+
const COOKIE_RESTORE_TIMEOUT = 3000;
|
|
206
|
+
|
|
181
207
|
/**
|
|
182
208
|
* Whether `idpOrigin` is a same-site, first-party host of the current page —
|
|
183
209
|
* i.e. it shares the page's registrable apex (last two labels), so a "no
|
|
@@ -300,10 +326,14 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
300
326
|
|
|
301
327
|
const [tokenReady, setTokenReady] = useState(true);
|
|
302
328
|
// Whether the FIRST cold-boot auth restore has concluded. Starts `false`
|
|
303
|
-
// (auth undetermined) and flips to `true` exactly once
|
|
304
|
-
//
|
|
305
|
-
//
|
|
329
|
+
// (auth undetermined) and flips to `true` exactly once — monotonic, never
|
|
330
|
+
// reverts. It now flips the MOMENT a session commits (the common reload case
|
|
331
|
+
// unblocks immediately, without waiting for the rest of the cold-boot chain),
|
|
332
|
+
// with the restore `finally` as the no-session/error backstop. The ref makes
|
|
333
|
+
// the flip idempotent across both sites so the setters fire at most once. See
|
|
334
|
+
// `isAuthResolved` on the context type for the consumer contract.
|
|
306
335
|
const [authResolved, setAuthResolved] = useState(false);
|
|
336
|
+
const authResolvedRef = useRef(false);
|
|
307
337
|
const [initialized, setInitialized] = useState(false);
|
|
308
338
|
const setAuthState = useAuthStore.setState;
|
|
309
339
|
|
|
@@ -565,6 +595,26 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
565
595
|
const onAuthStateChangeRef = useRef(onAuthStateChange);
|
|
566
596
|
onAuthStateChangeRef.current = onAuthStateChange;
|
|
567
597
|
|
|
598
|
+
// Flip the auth-resolution gate (`authResolved` + `tokenReady`) the MOMENT a
|
|
599
|
+
// session commits, instead of waiting for the whole cold-boot chain to finish.
|
|
600
|
+
// Idempotent and monotonic via `authResolvedRef`: the first call wins and the
|
|
601
|
+
// setters fire at most once, so the restore `finally` backstop becomes a no-op
|
|
602
|
+
// once a commit site has already marked resolution. Called from EVERY place a
|
|
603
|
+
// user is actually committed (the FedCM/iframe/redirect/SSO path
|
|
604
|
+
// `handleWebSSOSession`, the cookie-restore path, and the stored-session path)
|
|
605
|
+
// so the common reload case unblocks the loading gate without sitting behind
|
|
606
|
+
// the remaining (now-skipped) cold-boot steps.
|
|
607
|
+
const markAuthResolved = useCallback(() => {
|
|
608
|
+
if (authResolvedRef.current) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
authResolvedRef.current = true;
|
|
612
|
+
setTokenReady(true);
|
|
613
|
+
setAuthResolved(true);
|
|
614
|
+
}, []);
|
|
615
|
+
const markAuthResolvedRef = useRef(markAuthResolved);
|
|
616
|
+
markAuthResolvedRef.current = markAuthResolved;
|
|
617
|
+
|
|
568
618
|
// `handleWebSSOSession` is declared further down (it depends on values that
|
|
569
619
|
// are only available there). The FedCM/iframe cold-boot steps need to commit
|
|
570
620
|
// a recovered session through it, so we route the call through a ref that is
|
|
@@ -598,7 +648,9 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
598
648
|
|
|
599
649
|
let snapshot;
|
|
600
650
|
try {
|
|
601
|
-
|
|
651
|
+
// Bound the refresh so a cross-domain/stalled call cannot hang the cold
|
|
652
|
+
// boot in front of the terminal `/sso` bounce (see COOKIE_RESTORE_TIMEOUT).
|
|
653
|
+
snapshot = await oxyServices.refreshAllSessions({ timeout: COOKIE_RESTORE_TIMEOUT });
|
|
602
654
|
} catch (fetchError) {
|
|
603
655
|
// Offline / network error — fall through to the cached/stored-session flow.
|
|
604
656
|
if (__DEV__) {
|
|
@@ -662,6 +714,9 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
662
714
|
await persistSessionDurably(activeAccount.sessionId);
|
|
663
715
|
|
|
664
716
|
loginSuccessRef.current(fullUser);
|
|
717
|
+
// A session is now committed — unblock the auth-resolution gate immediately
|
|
718
|
+
// rather than waiting for `runColdBoot` to return (idempotent).
|
|
719
|
+
markAuthResolvedRef.current();
|
|
665
720
|
onAuthStateChangeRef.current?.(fullUser);
|
|
666
721
|
return true;
|
|
667
722
|
}, [oxyServices, persistSessionDurably]);
|
|
@@ -739,6 +794,11 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
739
794
|
if (storedActiveSessionId) {
|
|
740
795
|
try {
|
|
741
796
|
await switchSessionRef.current(storedActiveSessionId);
|
|
797
|
+
// The stored session is committed (this is native's ONLY restore path
|
|
798
|
+
// and the common web reload winner). Unblock the auth-resolution gate
|
|
799
|
+
// immediately so the loading screen clears without waiting for the
|
|
800
|
+
// remaining cold-boot steps to be evaluated/short-circuited (idempotent).
|
|
801
|
+
markAuthResolvedRef.current();
|
|
742
802
|
return true;
|
|
743
803
|
} catch (switchError) {
|
|
744
804
|
// Silently handle expected errors (invalid sessions, timeouts, network issues)
|
|
@@ -841,11 +901,21 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
841
901
|
// web-only step is gated by `isWebBrowser()`, so on native ONLY
|
|
842
902
|
// `stored-session` runs.
|
|
843
903
|
//
|
|
844
|
-
// Order (web): redirect callback → SSO return → FedCM silent
|
|
845
|
-
// silent iframe (per-apex, the durable reload path) → cookie
|
|
846
|
-
//
|
|
847
|
-
//
|
|
848
|
-
//
|
|
904
|
+
// Order (web): redirect callback → SSO return → stored session → FedCM silent
|
|
905
|
+
// (central) → silent iframe (per-apex, the durable reload path) → cookie
|
|
906
|
+
// restore → SSO bounce (terminal).
|
|
907
|
+
//
|
|
908
|
+
// LATENCY (FIX A): `stored-session` runs BEFORE the slow no-redirect probes
|
|
909
|
+
// (`fedcm-silent`, `silent-iframe`, `cookie-restore`). On a normal reload the
|
|
910
|
+
// local bearer validates in one round-trip and wins, so `runColdBoot`
|
|
911
|
+
// short-circuits and never sits through those probes' timeouts (the prior
|
|
912
|
+
// serial sum was a ~20-30s stall). `redirect` and `sso-return` MUST stay
|
|
913
|
+
// first — they consume the URL fragment before anything can strip it. On a
|
|
914
|
+
// first visit with no local session, `stored-session` skips and the
|
|
915
|
+
// cross-domain fallback chain (fedcm → iframe → cookie → sso-bounce) runs
|
|
916
|
+
// exactly as before; the per-apex silent iframe still restores a durable
|
|
917
|
+
// cross-domain session on reload WITHOUT a top-level bounce, so when it wins
|
|
918
|
+
// `sso-bounce` never fires (no flash, no loop).
|
|
849
919
|
// Order (native): stored session only (every web-only step is disabled
|
|
850
920
|
// off-browser).
|
|
851
921
|
const restoreSessionsFromStorage = useCallback(async (): Promise<void> => {
|
|
@@ -859,6 +929,16 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
859
929
|
const silentKey = silentColdBootKey(oxyServices);
|
|
860
930
|
const fedcmSupported = isWebBrowser() && oxyServices.isFedCMSupported?.() === true;
|
|
861
931
|
|
|
932
|
+
// FIX-B precondition flag: set true the instant the (now-earlier)
|
|
933
|
+
// `stored-session` step recovers a local bearer session. The slow web-only
|
|
934
|
+
// probes (`fedcm-silent`, `silent-iframe`) AND `enabled` on `!storedSessionRestored`
|
|
935
|
+
// so they are explicitly skipped once a local session won. `runColdBoot`
|
|
936
|
+
// already short-circuits on the first `{kind:'session'}`, so on a winning
|
|
937
|
+
// reload those `enabled` bodies are never even reached — this flag makes the
|
|
938
|
+
// intent explicit and is redundant-safe. On a first-visit-no-local-session,
|
|
939
|
+
// `stored-session` skips, this stays false, and the probes run as before.
|
|
940
|
+
let storedSessionRestored = false;
|
|
941
|
+
|
|
862
942
|
try {
|
|
863
943
|
const outcome = await runColdBoot<true>({
|
|
864
944
|
steps: [
|
|
@@ -895,14 +975,47 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
895
975
|
},
|
|
896
976
|
},
|
|
897
977
|
{
|
|
898
|
-
// 2)
|
|
978
|
+
// 2) Stored-session bearer restore. NO `enabled` gate — runs on ALL
|
|
979
|
+
// platforms. This is native's ONLY restore path (every web-only step
|
|
980
|
+
// is disabled off-browser, so native reaches exactly this) AND the
|
|
981
|
+
// common WEB reload winner.
|
|
982
|
+
//
|
|
983
|
+
// ORDERING (FIX A): this step now runs BEFORE the slow web-only
|
|
984
|
+
// probes (`fedcm-silent`, `silent-iframe`, `cookie-restore`). On a
|
|
985
|
+
// normal reload the local bearer validates in one round-trip and
|
|
986
|
+
// wins; `runColdBoot` then short-circuits and never even evaluates
|
|
987
|
+
// the slow no-redirect probes that would otherwise time out (the
|
|
988
|
+
// ~20-30s serial stall). The `redirect` and `sso-return` steps stay
|
|
989
|
+
// AHEAD of this one — they must consume the URL fragment before any
|
|
990
|
+
// later step (or anything else) strips it. On a first visit with no
|
|
991
|
+
// local session this step skips and the cross-domain fallback chain
|
|
992
|
+
// (fedcm → iframe → cookie → sso-bounce) runs exactly as before.
|
|
993
|
+
id: 'stored-session',
|
|
994
|
+
run: async () => {
|
|
995
|
+
const restored = await restoreStoredSession();
|
|
996
|
+
if (restored) {
|
|
997
|
+
// FIX-B: record the win so the slow probes below explicitly skip
|
|
998
|
+
// (belt-and-suspenders; `runColdBoot` already short-circuits).
|
|
999
|
+
storedSessionRestored = true;
|
|
1000
|
+
return { kind: 'session', session: true };
|
|
1001
|
+
}
|
|
1002
|
+
return { kind: 'skip' };
|
|
1003
|
+
},
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
// 3) FedCM silent reauthn (Chrome) against the CENTRAL IdP
|
|
899
1007
|
// (auth.oxy.so). `silentSignInWithFedCM` plants the access token
|
|
900
1008
|
// internally; we commit the returned session via
|
|
901
1009
|
// `handleWebSSOSession`. Guarded so it fires at most once per page
|
|
902
1010
|
// load across remounts. This is an enhancement layered above the
|
|
903
1011
|
// opaque-code bounce: when it succeeds the bounce never fires.
|
|
1012
|
+
//
|
|
1013
|
+
// FIX-B: additionally skipped when the earlier `stored-session` step
|
|
1014
|
+
// already recovered a local session — the probe cannot improve on a
|
|
1015
|
+
// valid local bearer, and skipping it avoids the silent round-trip.
|
|
904
1016
|
id: 'fedcm-silent',
|
|
905
|
-
enabled: () =>
|
|
1017
|
+
enabled: () =>
|
|
1018
|
+
!storedSessionRestored && fedcmSupported && !servicesSilentAttempted.has(silentKey),
|
|
906
1019
|
run: async () => {
|
|
907
1020
|
servicesSilentAttempted.add(silentKey);
|
|
908
1021
|
const session = await oxyServices.silentSignInWithFedCM?.();
|
|
@@ -914,7 +1027,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
914
1027
|
},
|
|
915
1028
|
},
|
|
916
1029
|
{
|
|
917
|
-
//
|
|
1030
|
+
// 4) First-party silent iframe at the PER-APEX IdP — the DURABLE
|
|
918
1031
|
// cross-domain reload-restore path. The durable session lives as a
|
|
919
1032
|
// first-party `fedcm_session` cookie on `auth.<rp-apex>` (e.g.
|
|
920
1033
|
// `auth.mention.earth`), established during the `/sso` bounce's
|
|
@@ -933,8 +1046,12 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
933
1046
|
// equivalent. When auto-detection bails (localhost/IP/single-label)
|
|
934
1047
|
// there is no per-apex IdP and the step skips. Web only; on native
|
|
935
1048
|
// `isWebBrowser()` gates it off, so native never runs an iframe.
|
|
1049
|
+
//
|
|
1050
|
+
// FIX-B: additionally skipped when `stored-session` already won.
|
|
1051
|
+
// FIX-D: bounded by `SILENT_IFRAME_TIMEOUT` (plus `iframe.onerror`
|
|
1052
|
+
// fail-fast in core) so a no-message iframe cannot stall cold boot.
|
|
936
1053
|
id: 'silent-iframe',
|
|
937
|
-
enabled: () => isWebBrowser(),
|
|
1054
|
+
enabled: () => !storedSessionRestored && isWebBrowser(),
|
|
938
1055
|
run: async () => {
|
|
939
1056
|
const perApexAuthUrl = autoDetectAuthWebUrl();
|
|
940
1057
|
if (!perApexAuthUrl || !commitWebSession) {
|
|
@@ -942,6 +1059,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
942
1059
|
}
|
|
943
1060
|
const session = await oxyServices.silentSignIn?.({
|
|
944
1061
|
authWebUrlOverride: perApexAuthUrl,
|
|
1062
|
+
timeout: SILENT_IFRAME_TIMEOUT,
|
|
945
1063
|
});
|
|
946
1064
|
if (!session?.user || !session?.sessionId) {
|
|
947
1065
|
return { kind: 'skip' };
|
|
@@ -951,12 +1069,14 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
951
1069
|
},
|
|
952
1070
|
},
|
|
953
1071
|
{
|
|
954
|
-
//
|
|
1072
|
+
// 5) Refresh-cookie restore (first-party only). On `*.oxy.so` the
|
|
955
1073
|
// httpOnly `oxy_rt_${n}` cookies ride along and resurrect every
|
|
956
1074
|
// device-local slot. On a cross-domain RP (mention.earth, …) the
|
|
957
1075
|
// cookie is `Domain=oxy.so` so it never reaches `api.<apex>` —
|
|
958
1076
|
// `refreshAllSessions` returns `{accounts:[]}` and this skips. That
|
|
959
1077
|
// is correct; cross-domain restore is handled by the SSO bounce.
|
|
1078
|
+
// FIX-D: `restoreViaRefreshCookie` bounds the request with
|
|
1079
|
+
// `COOKIE_RESTORE_TIMEOUT` so a cross-domain stall cannot hang here.
|
|
960
1080
|
id: 'cookie-restore',
|
|
961
1081
|
enabled: () => isWebBrowser(),
|
|
962
1082
|
run: async () => {
|
|
@@ -964,16 +1084,6 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
964
1084
|
return restored ? { kind: 'session', session: true } : { kind: 'skip' };
|
|
965
1085
|
},
|
|
966
1086
|
},
|
|
967
|
-
{
|
|
968
|
-
// 5) Stored-session bearer restore. NO `enabled` gate — runs on ALL
|
|
969
|
-
// platforms. This is native's ONLY restore path (every web-only step
|
|
970
|
-
// is disabled off-browser, so native reaches exactly this).
|
|
971
|
-
id: 'stored-session',
|
|
972
|
-
run: async () => {
|
|
973
|
-
const restored = await restoreStoredSession();
|
|
974
|
-
return restored ? { kind: 'session', session: true } : { kind: 'skip' };
|
|
975
|
-
},
|
|
976
|
-
},
|
|
977
1087
|
{
|
|
978
1088
|
// 6) SSO bounce (TERMINAL, web only, at most once). No local session
|
|
979
1089
|
// was found by any step above. Top-level navigate to the central
|
|
@@ -1048,14 +1158,14 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1048
1158
|
}
|
|
1049
1159
|
await clearSessionStateRef.current();
|
|
1050
1160
|
} finally {
|
|
1051
|
-
|
|
1052
|
-
//
|
|
1053
|
-
//
|
|
1054
|
-
//
|
|
1055
|
-
//
|
|
1056
|
-
// (
|
|
1057
|
-
// `
|
|
1058
|
-
|
|
1161
|
+
// Backstop: mark auth resolved on EVERY exit path — success, no-session,
|
|
1162
|
+
// AND error→catch→finally — and on native (which only runs the
|
|
1163
|
+
// `stored-session` step), so the gate can never hang `false`. Idempotent
|
|
1164
|
+
// via `markAuthResolved`'s ref: when a commit site already flipped it
|
|
1165
|
+
// mid-chain (the common reload case), this is a no-op. When no session was
|
|
1166
|
+
// recovered (the unauthenticated/error path), this is where `tokenReady` +
|
|
1167
|
+
// `authResolved` finally flip. Monotonic — never reverts on later restores.
|
|
1168
|
+
markAuthResolved();
|
|
1059
1169
|
}
|
|
1060
1170
|
}, [
|
|
1061
1171
|
oxyServices,
|
|
@@ -1063,6 +1173,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1063
1173
|
restoreViaRefreshCookie,
|
|
1064
1174
|
restoreStoredSession,
|
|
1065
1175
|
runSsoReturn,
|
|
1176
|
+
markAuthResolved,
|
|
1066
1177
|
]);
|
|
1067
1178
|
|
|
1068
1179
|
useEffect(() => {
|
|
@@ -1216,6 +1327,10 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1216
1327
|
fullUser = session.user as unknown as User;
|
|
1217
1328
|
}
|
|
1218
1329
|
loginSuccess(fullUser);
|
|
1330
|
+
// A session is now committed (FedCM silent / per-apex iframe / redirect /
|
|
1331
|
+
// SSO-return / popup all funnel through here) — unblock the auth-resolution
|
|
1332
|
+
// gate immediately, ahead of the cold-boot chain returning (idempotent).
|
|
1333
|
+
markAuthResolvedRef.current();
|
|
1219
1334
|
onAuthStateChange?.(fullUser);
|
|
1220
1335
|
}, [oxyServices, updateSessions, setActiveSessionId, loginSuccess, onAuthStateChange, persistSessionDurably]);
|
|
1221
1336
|
|