@oxyhq/services 13.0.3 → 13.1.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/index.js +18 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/ProfileButton.js +233 -0
- package/lib/commonjs/ui/components/ProfileButton.js.map +1 -0
- package/lib/commonjs/ui/components/ProfileMenu.js +374 -0
- package/lib/commonjs/ui/components/ProfileMenu.js.map +1 -0
- package/lib/commonjs/ui/context/OxyContext.js +99 -18
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js +28 -1
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/commonjs/ui/index.js +14 -0
- package/lib/commonjs/ui/index.js.map +1 -1
- package/lib/commonjs/ui/utils/activeAuthuser.js +76 -8
- package/lib/commonjs/ui/utils/activeAuthuser.js.map +1 -1
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/ProfileButton.js +228 -0
- package/lib/module/ui/components/ProfileButton.js.map +1 -0
- package/lib/module/ui/components/ProfileMenu.js +370 -0
- package/lib/module/ui/components/ProfileMenu.js.map +1 -0
- package/lib/module/ui/context/OxyContext.js +100 -19
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/hooks/useAuthOperations.js +29 -2
- package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/module/ui/index.js +2 -0
- package/lib/module/ui/index.js.map +1 -1
- package/lib/module/ui/utils/activeAuthuser.js +74 -9
- package/lib/module/ui/utils/activeAuthuser.js.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +4 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ProfileButton.d.ts +46 -0
- package/lib/typescript/commonjs/ui/components/ProfileButton.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts +39 -0
- package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts.map +1 -0
- 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/index.d.ts +2 -0
- package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts +24 -0
- package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +4 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ProfileButton.d.ts +46 -0
- package/lib/typescript/module/ui/components/ProfileButton.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/ProfileMenu.d.ts +39 -0
- package/lib/typescript/module/ui/components/ProfileMenu.d.ts.map +1 -0
- 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/index.d.ts +2 -0
- package/lib/typescript/module/ui/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/activeAuthuser.d.ts +24 -0
- package/lib/typescript/module/ui/utils/activeAuthuser.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +8 -0
- package/src/ui/components/ProfileButton.tsx +270 -0
- package/src/ui/components/ProfileMenu.tsx +416 -0
- package/src/ui/context/OxyContext.tsx +100 -18
- package/src/ui/context/hooks/useAuthOperations.ts +35 -2
- package/src/ui/index.ts +2 -0
- package/src/ui/utils/__tests__/activeAuthuser.test.ts +75 -0
- package/src/ui/utils/activeAuthuser.ts +74 -8
|
@@ -9,7 +9,13 @@ 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 {
|
|
12
|
+
import {
|
|
13
|
+
clearActiveAuthuser,
|
|
14
|
+
clearSsoBounceState,
|
|
15
|
+
writeActiveAuthuser,
|
|
16
|
+
markSignedOut,
|
|
17
|
+
clearSignedOut,
|
|
18
|
+
} from '../../utils/activeAuthuser';
|
|
13
19
|
import { isCrossApexWeb, CrossApexDirectSignInError } from '../../../utils/crossApex';
|
|
14
20
|
|
|
15
21
|
export interface UseAuthOperationsOptions {
|
|
@@ -133,6 +139,23 @@ export const useAuthOperations = ({
|
|
|
133
139
|
deviceFingerprint,
|
|
134
140
|
);
|
|
135
141
|
|
|
142
|
+
// Deliberate sign-in re-enables automatic silent restore: clear the durable
|
|
143
|
+
// "deliberately signed out" flag so a prior sign-out no longer suppresses
|
|
144
|
+
// the `fedcm-silent` / per-apex iframe cold-boot steps.
|
|
145
|
+
clearSignedOut();
|
|
146
|
+
|
|
147
|
+
// Register this primary session in the device's first-party multi-account
|
|
148
|
+
// refresh-cookie set so a web key-based sign-in survives reload via
|
|
149
|
+
// `refresh-all` and coexists with switched accounts. `/auth/verify` mints the
|
|
150
|
+
// session but (unlike `/auth/login` / `/fedcm/exchange`) does NOT plant an
|
|
151
|
+
// `oxy_rt_<authuser>` slot, so we establish it here through the shared
|
|
152
|
+
// `POST /auth/session` primitive. No-op on native and cross-apex (returns
|
|
153
|
+
// `null`); records the active slot when one is genuinely allocated.
|
|
154
|
+
const verifyAuthuser = await oxyServices.establishDeviceRefreshSlot();
|
|
155
|
+
if (typeof verifyAuthuser === 'number') {
|
|
156
|
+
writeActiveAuthuser(verifyAuthuser);
|
|
157
|
+
}
|
|
158
|
+
|
|
136
159
|
// Get full user data
|
|
137
160
|
fullUser = await oxyServices.getUserBySession(sessionResponse.sessionId);
|
|
138
161
|
|
|
@@ -260,7 +283,14 @@ export const useAuthOperations = ({
|
|
|
260
283
|
await switchSession(filteredSessions[0].sessionId);
|
|
261
284
|
} else {
|
|
262
285
|
// Genuine FULL sign-out (no sessions remain): clear the per-origin
|
|
263
|
-
// SSO bounce state so a fresh deliberate sign-in can re-probe
|
|
286
|
+
// SSO bounce state so a fresh deliberate sign-in can re-probe, drop
|
|
287
|
+
// the persisted active device slot so the next cold boot does not
|
|
288
|
+
// prioritize a now-signed-out `oxy_active_authuser`, and SET the
|
|
289
|
+
// deliberately-signed-out flag so the silent cold-boot steps
|
|
290
|
+
// (`fedcm-silent` / per-apex iframe) do not re-mint a session from a
|
|
291
|
+
// still-live IdP session on the next reload (mirrors `logoutAll`).
|
|
292
|
+
markSignedOut();
|
|
293
|
+
clearActiveAuthuser();
|
|
264
294
|
clearSsoBounceState();
|
|
265
295
|
clearPriorSessionHintSafe(clearPriorSessionHint, logger);
|
|
266
296
|
await clearSessionState();
|
|
@@ -322,6 +352,9 @@ export const useAuthOperations = ({
|
|
|
322
352
|
if (isWebBrowser()) {
|
|
323
353
|
await oxyServices.logoutAllSessionsViaCookie();
|
|
324
354
|
clearActiveAuthuser();
|
|
355
|
+
// Deliberate full sign-out: suppress automatic silent restore on the next
|
|
356
|
+
// cold boot so a still-live IdP session does not re-mint a session.
|
|
357
|
+
markSignedOut();
|
|
325
358
|
}
|
|
326
359
|
// logoutAll is ALWAYS a full sign-out: clear the per-origin SSO bounce
|
|
327
360
|
// state (web-guarded internally) so a fresh sign-in can re-probe, and drop
|
package/src/ui/index.ts
CHANGED
|
@@ -27,6 +27,8 @@ export { FontLoader, setupFonts } from './components/FontLoader';
|
|
|
27
27
|
export { default as OxyIcon } from './components/icon/OxyIcon';
|
|
28
28
|
export { default as AccountMenu } from './components/AccountMenu';
|
|
29
29
|
export { default as AccountMenuButton } from './components/AccountMenuButton';
|
|
30
|
+
export { default as ProfileButton } from './components/ProfileButton';
|
|
31
|
+
export { default as ProfileMenu } from './components/ProfileMenu';
|
|
30
32
|
export { default as AccountSwitcher, AccountSwitcherView } from './components/AccountSwitcher';
|
|
31
33
|
|
|
32
34
|
// Context + hooks
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fail-safe guarantees for the web `activeAuthuser` storage helpers.
|
|
3
|
+
*
|
|
4
|
+
* The PROPERTY ACCESS `window.localStorage` can throw a `SecurityError`
|
|
5
|
+
* synchronously in opaque-origin / sandboxed iframes or when storage is
|
|
6
|
+
* disabled — not just `getItem`/`setItem`. These helpers run during cold boot
|
|
7
|
+
* (and feed render-phase gate values in the providers), so they MUST never
|
|
8
|
+
* propagate that throw: reads fail safe to a benign default and writes no-op.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
readActiveAuthuser,
|
|
13
|
+
writeActiveAuthuser,
|
|
14
|
+
clearActiveAuthuser,
|
|
15
|
+
markSignedOut,
|
|
16
|
+
clearSignedOut,
|
|
17
|
+
isSilentRestoreSuppressed,
|
|
18
|
+
} from '../activeAuthuser';
|
|
19
|
+
|
|
20
|
+
describe('activeAuthuser helpers — localStorage fail-safe', () => {
|
|
21
|
+
const realDescriptor = Object.getOwnPropertyDescriptor(window, 'localStorage');
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
if (realDescriptor) {
|
|
25
|
+
Object.defineProperty(window, 'localStorage', realDescriptor);
|
|
26
|
+
}
|
|
27
|
+
window.localStorage?.clear?.();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/** Replace the `localStorage` accessor so reading the property throws. */
|
|
31
|
+
function makeLocalStorageGetterThrow(): void {
|
|
32
|
+
Object.defineProperty(window, 'localStorage', {
|
|
33
|
+
configurable: true,
|
|
34
|
+
get() {
|
|
35
|
+
throw new DOMException('denied', 'SecurityError');
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
it('readActiveAuthuser returns null (never throws) when the getter throws', () => {
|
|
41
|
+
makeLocalStorageGetterThrow();
|
|
42
|
+
expect(() => readActiveAuthuser()).not.toThrow();
|
|
43
|
+
expect(readActiveAuthuser()).toBeNull();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('isSilentRestoreSuppressed returns false (never throws) when the getter throws', () => {
|
|
47
|
+
makeLocalStorageGetterThrow();
|
|
48
|
+
expect(() => isSilentRestoreSuppressed()).not.toThrow();
|
|
49
|
+
expect(isSilentRestoreSuppressed()).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('writeActiveAuthuser / clearActiveAuthuser / markSignedOut / clearSignedOut no-op when the getter throws', () => {
|
|
53
|
+
makeLocalStorageGetterThrow();
|
|
54
|
+
expect(() => {
|
|
55
|
+
writeActiveAuthuser(2);
|
|
56
|
+
clearActiveAuthuser();
|
|
57
|
+
markSignedOut();
|
|
58
|
+
clearSignedOut();
|
|
59
|
+
}).not.toThrow();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('round-trips normally when storage works (zero behavior change)', () => {
|
|
63
|
+
writeActiveAuthuser(3);
|
|
64
|
+
expect(readActiveAuthuser()).toBe(3);
|
|
65
|
+
|
|
66
|
+
expect(isSilentRestoreSuppressed()).toBe(false);
|
|
67
|
+
markSignedOut();
|
|
68
|
+
expect(isSilentRestoreSuppressed()).toBe(true);
|
|
69
|
+
clearSignedOut();
|
|
70
|
+
expect(isSilentRestoreSuppressed()).toBe(false);
|
|
71
|
+
|
|
72
|
+
clearActiveAuthuser();
|
|
73
|
+
expect(readActiveAuthuser()).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -24,12 +24,28 @@ import {
|
|
|
24
24
|
ssoGuardKey,
|
|
25
25
|
ssoStateKey,
|
|
26
26
|
ssoDestKey,
|
|
27
|
+
ssoSignedOutKey,
|
|
28
|
+
silentRestoreSuppressed,
|
|
27
29
|
} from '@oxyhq/core';
|
|
28
30
|
|
|
29
31
|
const ACTIVE_AUTHUSER_KEY = 'oxy_active_authuser';
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Safely resolve `window.localStorage`, returning `null` when it is
|
|
35
|
+
* unavailable. The PROPERTY ACCESS itself (`window.localStorage`) can throw a
|
|
36
|
+
* `SecurityError` synchronously in opaque-origin / sandboxed iframes or when
|
|
37
|
+
* storage is disabled — even `typeof window.localStorage` evaluates the getter
|
|
38
|
+
* and throws. Every read/write in this module routes through here so the getter
|
|
39
|
+
* throw is caught once, at the source, and callers stay clean. Returns `null`
|
|
40
|
+
* off-web and on any access failure (fail safe).
|
|
41
|
+
*/
|
|
42
|
+
function getLocalStorage(): Storage | null {
|
|
43
|
+
if (typeof window === 'undefined') return null;
|
|
44
|
+
try {
|
|
45
|
+
return window.localStorage ?? null;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
33
49
|
}
|
|
34
50
|
|
|
35
51
|
function getSessionStorage(): Storage | null {
|
|
@@ -49,11 +65,12 @@ function getSessionStorage(): Storage | null {
|
|
|
49
65
|
* fall back to deterministic selection (lowest authuser).
|
|
50
66
|
*/
|
|
51
67
|
export function readActiveAuthuser(): number | null {
|
|
52
|
-
|
|
68
|
+
const storage = getLocalStorage();
|
|
69
|
+
if (!storage) {
|
|
53
70
|
return null;
|
|
54
71
|
}
|
|
55
72
|
try {
|
|
56
|
-
const raw =
|
|
73
|
+
const raw = storage.getItem(ACTIVE_AUTHUSER_KEY);
|
|
57
74
|
if (raw === null) return null;
|
|
58
75
|
const parsed = Number.parseInt(raw, 10);
|
|
59
76
|
if (!Number.isFinite(parsed) || parsed < 0) return null;
|
|
@@ -69,10 +86,11 @@ export function readActiveAuthuser(): number | null {
|
|
|
69
86
|
* this succeeding — it is best-effort UX persistence, not authoritative.
|
|
70
87
|
*/
|
|
71
88
|
export function writeActiveAuthuser(authuser: number): void {
|
|
72
|
-
if (!hasLocalStorage()) return;
|
|
73
89
|
if (!Number.isFinite(authuser) || authuser < 0) return;
|
|
90
|
+
const storage = getLocalStorage();
|
|
91
|
+
if (!storage) return;
|
|
74
92
|
try {
|
|
75
|
-
|
|
93
|
+
storage.setItem(ACTIVE_AUTHUSER_KEY, String(authuser));
|
|
76
94
|
} catch {
|
|
77
95
|
// Best-effort persistence; swallow QuotaExceededError / SecurityError.
|
|
78
96
|
}
|
|
@@ -84,14 +102,62 @@ export function writeActiveAuthuser(authuser: number): void {
|
|
|
84
102
|
* cleared slot.
|
|
85
103
|
*/
|
|
86
104
|
export function clearActiveAuthuser(): void {
|
|
87
|
-
|
|
105
|
+
const storage = getLocalStorage();
|
|
106
|
+
if (!storage) return;
|
|
107
|
+
try {
|
|
108
|
+
storage.removeItem(ACTIVE_AUTHUSER_KEY);
|
|
109
|
+
} catch {
|
|
110
|
+
// Best-effort.
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Mark this origin as DELIBERATELY signed out (durable `localStorage`, via the
|
|
116
|
+
* core {@link ssoSignedOutKey}). Called ONLY on EXPLICIT full sign-out so that
|
|
117
|
+
* the next cold boot does NOT silently re-mint a session from a still-live IdP
|
|
118
|
+
* session (`fedcm-silent` / per-apex `/auth/silent` iframe). Cleared by any
|
|
119
|
+
* deliberate sign-in (see {@link clearSignedOut}). No-ops on native / storage
|
|
120
|
+
* failure (best-effort).
|
|
121
|
+
*/
|
|
122
|
+
export function markSignedOut(): void {
|
|
123
|
+
const storage = getLocalStorage();
|
|
124
|
+
if (!storage) return;
|
|
88
125
|
try {
|
|
89
|
-
window.
|
|
126
|
+
storage.setItem(ssoSignedOutKey(window.location.origin), '1');
|
|
127
|
+
} catch {
|
|
128
|
+
// Best-effort; swallow QuotaExceededError / SecurityError (private mode).
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Clear the durable deliberately-signed-out flag. Called on ANY deliberate
|
|
134
|
+
* sign-in (password, FedCM, account switch, device claim) so a real sign-in
|
|
135
|
+
* fully re-enables automatic silent restore — there is no "stuck signed out"
|
|
136
|
+
* state. No-ops on native / storage failure.
|
|
137
|
+
*/
|
|
138
|
+
export function clearSignedOut(): void {
|
|
139
|
+
const storage = getLocalStorage();
|
|
140
|
+
if (!storage) return;
|
|
141
|
+
try {
|
|
142
|
+
storage.removeItem(ssoSignedOutKey(window.location.origin));
|
|
90
143
|
} catch {
|
|
91
144
|
// Best-effort.
|
|
92
145
|
}
|
|
93
146
|
}
|
|
94
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Whether AUTOMATIC silent restore is suppressed for the current origin because
|
|
150
|
+
* the user deliberately signed out. Reads the durable flag through the core
|
|
151
|
+
* {@link silentRestoreSuppressed} predicate. Returns `false` off-web and on any
|
|
152
|
+
* storage failure (fail safe toward normal restore). Used to gate the
|
|
153
|
+
* `fedcm-silent` and `silent-iframe` cold-boot steps.
|
|
154
|
+
*/
|
|
155
|
+
export function isSilentRestoreSuppressed(): boolean {
|
|
156
|
+
const storage = getLocalStorage();
|
|
157
|
+
if (!storage) return false;
|
|
158
|
+
return silentRestoreSuppressed(storage, window.location.origin);
|
|
159
|
+
}
|
|
160
|
+
|
|
95
161
|
/**
|
|
96
162
|
* Clear all per-origin SSO bounce sessionStorage keys. Called ONLY on EXPLICIT
|
|
97
163
|
* user sign-out (logout / logoutAll) — never on a cold-boot failure path — so a
|