@oxyhq/core 2.3.0 → 2.3.2
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/index.js +2 -1
- package/dist/cjs/utils/ssoBounce.js +19 -1
- package/dist/cjs/utils/ssoReturn.js +60 -26
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/utils/ssoBounce.js +18 -1
- package/dist/esm/utils/ssoReturn.js +61 -27
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/utils/ssoBounce.d.ts +15 -1
- package/dist/types/utils/ssoReturn.d.ts +20 -6
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/utils/__tests__/consumeSsoReturn.test.ts +226 -3
- package/src/utils/__tests__/ssoBounce.test.ts +2 -0
- package/src/utils/ssoBounce.ts +19 -1
- package/src/utils/ssoReturn.ts +74 -29
package/dist/cjs/index.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
21
|
exports.normalizeColorScheme = exports.normalizeTheme = exports.getContrastTextColor = exports.isLightColor = exports.withOpacity = exports.rgbToHex = exports.hexToRgb = exports.lightenColor = exports.darkenColor = exports.isAndroid = exports.isIOS = exports.isNative = exports.isWeb = exports.setPlatformOS = exports.getPlatformOS = exports.isRTLLocale = exports.normalizeLanguageCode = exports.getNativeLanguageName = exports.getLanguageName = exports.getLanguageMetadata = exports.SUPPORTED_LANGUAGES = exports.TopicSource = exports.TopicType = exports.SECURITY_EVENT_SEVERITY_MAP = exports.DeviceManager = exports.RecoveryPhraseService = exports.SignatureService = exports.IdentityPersistError = exports.IdentityAlreadyExistsError = exports.KeyManager = exports.sessionsArraysEqual = exports.normalizeAndSortSessions = exports.mergeSessions = exports.authenticatedApiCall = exports.withAuthErrorHandling = exports.isAuthenticationError = exports.ensureValidToken = exports.AuthenticationFailedError = exports.SessionSyncRequiredError = exports.OxyAppDataIdentifierError = exports.ServiceCredentialMismatchError = exports.createCrossDomainAuth = exports.CrossDomainAuth = exports.createAuthManager = exports.AuthManager = exports.oxyClient = exports.OXY_CLOUD_URL = exports.OxyAuthenticationTimeoutError = exports.OxyAuthenticationError = exports.OxyServices = void 0;
|
|
22
22
|
exports.isValidURL = exports.isValidUUID = exports.isValidObject = exports.isValidArray = exports.isRequiredBoolean = exports.isRequiredNumber = exports.isRequiredString = exports.isValidPassword = exports.isValidUsername = exports.isValidEmail = exports.PASSWORD_REGEX = exports.USERNAME_REGEX = exports.EMAIL_REGEX = exports.retryAsync = exports.validateRequiredFields = exports.handleHttpError = exports.createApiError = exports.ErrorCodes = exports.safeJsonParse = exports.buildPaginationParams = exports.buildUrl = exports.buildSearchParams = exports.translate = exports.createDebugLogger = exports.debugError = exports.debugWarn = exports.debugLog = exports.isDev = exports.withRetry = exports.delay = exports.shouldAllowRequest = exports.recordSuccess = exports.recordFailure = exports.calculateBackoffInterval = exports.createCircuitBreakerState = exports.DEFAULT_CIRCUIT_BREAKER_CONFIG = exports.isRetryableError = exports.isNetworkError = exports.isServerError = exports.isRateLimitError = exports.isNotFoundError = exports.isForbiddenError = exports.isUnauthorizedError = exports.isAlreadyRegisteredError = exports.getErrorMessage = exports.getErrorStatus = exports.HttpStatus = exports.getSystemColorScheme = exports.systemPrefersDarkMode = exports.getOppositeTheme = void 0;
|
|
23
|
-
exports.packageInfo = exports.runColdBoot = exports.guardActive = exports.isCentralIdPOrigin = exports.buildSsoBounceUrl = exports.ssoNavigate = exports.ssoNoSessionKey = exports.ssoDestKey = exports.ssoGuardKey = exports.ssoStateKey = exports.SSO_GUARD_TTL_MS = exports.SSO_CALLBACK_PATH = exports.generateSsoState = exports.consumeSsoReturn = exports.parseSsoReturnFragment = exports.resolveCentralAuthUrl = exports.CENTRAL_IDP_APEX = exports.CENTRAL_AUTH_URL = exports.MULTIPART_TLDS = exports.registrableApex = exports.autoDetectAuthWebUrl = exports.getAccountColor = exports.mergeAccountsFromRefreshAll = exports.formatPublicKeyHandle = exports.getAccountFallbackHandle = exports.getAccountDisplayName = exports.createQuickAccount = exports.buildAccountsArray = exports.updateAvatarVisibility = exports.logPerformance = exports.logPayment = exports.logDevice = exports.logUser = exports.logSession = exports.logApi = exports.logAuth = exports.LogLevel = exports.logger = exports.validateAndSanitizeUserInput = exports.isValidObjectId = exports.sanitizeHTML = exports.sanitizeString = exports.isValidFileType = exports.isValidFileSize = exports.isValidDate = void 0;
|
|
23
|
+
exports.packageInfo = exports.runColdBoot = exports.guardActive = exports.isCentralIdPOrigin = exports.buildSsoBounceUrl = exports.ssoNavigate = exports.ssoAttemptedKey = exports.ssoNoSessionKey = exports.ssoDestKey = exports.ssoGuardKey = exports.ssoStateKey = exports.SSO_GUARD_TTL_MS = exports.SSO_CALLBACK_PATH = exports.generateSsoState = exports.consumeSsoReturn = exports.parseSsoReturnFragment = exports.resolveCentralAuthUrl = exports.CENTRAL_IDP_APEX = exports.CENTRAL_AUTH_URL = exports.MULTIPART_TLDS = exports.registrableApex = exports.autoDetectAuthWebUrl = exports.getAccountColor = exports.mergeAccountsFromRefreshAll = exports.formatPublicKeyHandle = exports.getAccountFallbackHandle = exports.getAccountDisplayName = exports.createQuickAccount = exports.buildAccountsArray = exports.updateAvatarVisibility = exports.logPerformance = exports.logPayment = exports.logDevice = exports.logUser = exports.logSession = exports.logApi = exports.logAuth = exports.LogLevel = exports.logger = exports.validateAndSanitizeUserInput = exports.isValidObjectId = exports.sanitizeHTML = exports.sanitizeString = exports.isValidFileType = exports.isValidFileSize = exports.isValidDate = void 0;
|
|
24
24
|
// Ensure crypto polyfills are loaded before anything else
|
|
25
25
|
require("./crypto/polyfill");
|
|
26
26
|
// ---------------------------------------------------------------------------
|
|
@@ -250,6 +250,7 @@ Object.defineProperty(exports, "ssoStateKey", { enumerable: true, get: function
|
|
|
250
250
|
Object.defineProperty(exports, "ssoGuardKey", { enumerable: true, get: function () { return ssoBounce_1.ssoGuardKey; } });
|
|
251
251
|
Object.defineProperty(exports, "ssoDestKey", { enumerable: true, get: function () { return ssoBounce_1.ssoDestKey; } });
|
|
252
252
|
Object.defineProperty(exports, "ssoNoSessionKey", { enumerable: true, get: function () { return ssoBounce_1.ssoNoSessionKey; } });
|
|
253
|
+
Object.defineProperty(exports, "ssoAttemptedKey", { enumerable: true, get: function () { return ssoBounce_1.ssoAttemptedKey; } });
|
|
253
254
|
Object.defineProperty(exports, "ssoNavigate", { enumerable: true, get: function () { return ssoBounce_1.ssoNavigate; } });
|
|
254
255
|
Object.defineProperty(exports, "buildSsoBounceUrl", { enumerable: true, get: function () { return ssoBounce_1.buildSsoBounceUrl; } });
|
|
255
256
|
Object.defineProperty(exports, "isCentralIdPOrigin", { enumerable: true, get: function () { return ssoBounce_1.isCentralIdPOrigin; } });
|
|
@@ -27,11 +27,15 @@
|
|
|
27
27
|
* the session, then restores the original destination.
|
|
28
28
|
*
|
|
29
29
|
* Loop proof (logged-out): first load all steps skip → `sso-bounce` sets
|
|
30
|
-
* guard/state/dest
|
|
30
|
+
* guard/state/dest + the outcome-independent attempted-flag
|
|
31
|
+
* ({@link ssoAttemptedKey}) and navigates; the IdP (no central session) returns
|
|
31
32
|
* `#oxy_sso=none`; the callback load's `sso-return` sees `none`, sets the
|
|
32
33
|
* NO_SESSION flag ({@link ssoNoSessionKey}), and `sso-bounce` is then disabled.
|
|
33
34
|
* Exactly ONE bounce, no loop. An interrupted bounce (user hit back
|
|
34
35
|
* mid-redirect) self-heals once the {@link SSO_GUARD_TTL_MS} guard TTL lapses.
|
|
36
|
+
* The attempted-flag is the definitive, outcome-INDEPENDENT loop breaker: it is
|
|
37
|
+
* set pre-bounce so even if the return-side NO_SESSION write never lands, the
|
|
38
|
+
* bounce can never re-fire this tab after the self-heal TTL lapses.
|
|
35
39
|
*
|
|
36
40
|
* All state lives in `sessionStorage` (per tab, cleared on tab close) and is
|
|
37
41
|
* keyed per-origin so two RPs hosted in the same browser never collide. The
|
|
@@ -44,6 +48,7 @@ exports.ssoStateKey = ssoStateKey;
|
|
|
44
48
|
exports.ssoGuardKey = ssoGuardKey;
|
|
45
49
|
exports.ssoDestKey = ssoDestKey;
|
|
46
50
|
exports.ssoNoSessionKey = ssoNoSessionKey;
|
|
51
|
+
exports.ssoAttemptedKey = ssoAttemptedKey;
|
|
47
52
|
exports.ssoNavigate = ssoNavigate;
|
|
48
53
|
exports.buildSsoBounceUrl = buildSsoBounceUrl;
|
|
49
54
|
exports.isCentralIdPOrigin = isCentralIdPOrigin;
|
|
@@ -69,6 +74,7 @@ const STATE_KEY_PREFIX = 'oxy_sso_state:';
|
|
|
69
74
|
const GUARD_KEY_PREFIX = 'oxy_sso_guard:';
|
|
70
75
|
const DEST_KEY_PREFIX = 'oxy_sso_dest:';
|
|
71
76
|
const NO_SESSION_KEY_PREFIX = 'oxy_sso_no_session:';
|
|
77
|
+
const ATTEMPTED_KEY_PREFIX = 'oxy_sso_attempted:';
|
|
72
78
|
/** Per-origin CSRF state key (matched on return to defeat fragment forgery). */
|
|
73
79
|
function ssoStateKey(origin) {
|
|
74
80
|
return `${STATE_KEY_PREFIX}${origin}`;
|
|
@@ -89,6 +95,18 @@ function ssoDestKey(origin) {
|
|
|
89
95
|
function ssoNoSessionKey(origin) {
|
|
90
96
|
return `${NO_SESSION_KEY_PREFIX}${origin}`;
|
|
91
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Per-origin, OUTCOME-INDEPENDENT once-guard. Set in `sessionStorage` BEFORE
|
|
100
|
+
* the terminal SSO bounce navigates. Gates the bounce so the silent
|
|
101
|
+
* cross-domain probe fires AT MOST ONCE per tab session — independent of
|
|
102
|
+
* whether the return-side NO_SESSION flag ever lands. The definitive loop
|
|
103
|
+
* breaker; survives the 30s self-heal `ssoGuardKey` TTL. Cleared only on an
|
|
104
|
+
* explicit sign-out/clear so a later cold boot (after the user signs in
|
|
105
|
+
* centrally) can probe again.
|
|
106
|
+
*/
|
|
107
|
+
function ssoAttemptedKey(origin) {
|
|
108
|
+
return `${ATTEMPTED_KEY_PREFIX}${origin}`;
|
|
109
|
+
}
|
|
92
110
|
/**
|
|
93
111
|
* Perform the terminal top-level SSO bounce navigation.
|
|
94
112
|
*
|
|
@@ -90,14 +90,20 @@ function parseSsoReturnFragment(hash) {
|
|
|
90
90
|
* or a `Referer` header even if a later step throws.
|
|
91
91
|
* - `state` must match (CSRF). A mismatch or a missing code sets the
|
|
92
92
|
* NO_SESSION flag so `sso-bounce` is disabled (no rebounce loop).
|
|
93
|
-
* - `none`/`error` outcomes set the NO_SESSION flag
|
|
94
|
-
* loop proof).
|
|
93
|
+
* - `none`/`error` outcomes set BOTH the NO_SESSION flag and the
|
|
94
|
+
* outcome-independent attempted-flag (the load2 half of the loop proof).
|
|
95
95
|
* - A throwing exchange is caught, reported via `onExchangeError`, and
|
|
96
96
|
* treated exactly like "no session" (never loops, never rethrows).
|
|
97
|
-
* -
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* DEST key is
|
|
97
|
+
* - On EVERY consumed outcome (ok, none, error, state-mismatch, no-code,
|
|
98
|
+
* failed-exchange, no-sessionId) — not just ok — if the page landed on
|
|
99
|
+
* {@link SSO_CALLBACK_PATH}, the real pre-bounce destination is restored
|
|
100
|
+
* from the DEST key so the user is never stranded on the internal callback
|
|
101
|
+
* path. Same-origin only (an attacker-planted cross-origin or relative-evil
|
|
102
|
+
* dest is rejected). The DEST key is removed unconditionally.
|
|
103
|
+
* - After a same-origin dest restore (which uses `history.replaceState`, that
|
|
104
|
+
* does NOT itself emit `popstate`), a synthetic `popstate` is dispatched so
|
|
105
|
+
* URL-driven routers (Expo Router / React Navigation web) re-sync to the
|
|
106
|
+
* restored route. It is NOT dispatched when the dest is rejected/absent.
|
|
101
107
|
*
|
|
102
108
|
* Total: this function NEVER throws. Off-web it is a no-op returning `null`.
|
|
103
109
|
*
|
|
@@ -116,6 +122,21 @@ async function consumeSsoReturn(oxy, deps = {}) {
|
|
|
116
122
|
const location = deps.location ?? window.location;
|
|
117
123
|
const history = deps.history ?? window.history;
|
|
118
124
|
const onExchangeError = deps.onExchangeError;
|
|
125
|
+
// Default: emit a synthetic `popstate` so URL-driven routers re-sync after a
|
|
126
|
+
// `history.replaceState` (which does NOT emit `popstate` on its own). Feature-
|
|
127
|
+
// detected end to end so it never throws in any environment.
|
|
128
|
+
const dispatchPopState = deps.dispatchPopState ??
|
|
129
|
+
(() => {
|
|
130
|
+
if (typeof window === 'undefined' || typeof window.dispatchEvent !== 'function') {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (typeof PopStateEvent !== 'undefined') {
|
|
134
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
135
|
+
}
|
|
136
|
+
else if (typeof Event !== 'undefined') {
|
|
137
|
+
window.dispatchEvent(new Event('popstate'));
|
|
138
|
+
}
|
|
139
|
+
});
|
|
119
140
|
const ret = parseSsoReturnFragment(location.hash);
|
|
120
141
|
if (!ret) {
|
|
121
142
|
// Not an oxy_sso fragment — nothing to do (do NOT touch any flags).
|
|
@@ -133,17 +154,47 @@ async function consumeSsoReturn(oxy, deps = {}) {
|
|
|
133
154
|
storage.removeItem((0, ssoBounce_1.ssoGuardKey)(origin));
|
|
134
155
|
const markNoSession = () => {
|
|
135
156
|
storage.setItem((0, ssoBounce_1.ssoNoSessionKey)(origin), '1');
|
|
157
|
+
// A return was consumed, so the probe definitively happened. Set the
|
|
158
|
+
// outcome-independent attempted-flag too so the bounce can never re-fire
|
|
159
|
+
// even if some consumer path skipped setting it pre-bounce.
|
|
160
|
+
storage.setItem((0, ssoBounce_1.ssoAttemptedKey)(origin), '1');
|
|
161
|
+
};
|
|
162
|
+
// Restore the user's real pre-bounce destination so they are never stranded
|
|
163
|
+
// on the internal callback path — invoked on EVERY consumed outcome, not just
|
|
164
|
+
// success. Same-origin only — never honour a cross-origin/protocol-relative
|
|
165
|
+
// dest that could have been planted to redirect the user. The DEST key is
|
|
166
|
+
// removed unconditionally. After a successful same-origin restore a synthetic
|
|
167
|
+
// `popstate` is dispatched so URL-driven routers re-sync.
|
|
168
|
+
const restoreDest = () => {
|
|
169
|
+
if (location.pathname === ssoBounce_1.SSO_CALLBACK_PATH) {
|
|
170
|
+
const dest = storage.getItem((0, ssoBounce_1.ssoDestKey)(origin));
|
|
171
|
+
if (dest) {
|
|
172
|
+
try {
|
|
173
|
+
const destUrl = new URL(dest, origin);
|
|
174
|
+
if (destUrl.origin === origin) {
|
|
175
|
+
history.replaceState(null, '', destUrl.pathname + destUrl.search + destUrl.hash);
|
|
176
|
+
dispatchPopState();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// Malformed stored destination — leave the URL on the callback path.
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
storage.removeItem((0, ssoBounce_1.ssoDestKey)(origin));
|
|
136
185
|
};
|
|
137
186
|
if (ret.kind === 'none' || ret.kind === 'error') {
|
|
138
187
|
// The central IdP had no session (or the bounce failed). Record it so we do
|
|
139
188
|
// not bounce again this tab — the definitive loop breaker.
|
|
140
189
|
markNoSession();
|
|
190
|
+
restoreDest();
|
|
141
191
|
return null;
|
|
142
192
|
}
|
|
143
193
|
if (!stateOk || !ret.code) {
|
|
144
194
|
// Forged / replayed / stale fragment, or a malformed ok with no code. Treat
|
|
145
195
|
// exactly like "no session": never exchange, never loop.
|
|
146
196
|
markNoSession();
|
|
197
|
+
restoreDest();
|
|
147
198
|
return null;
|
|
148
199
|
}
|
|
149
200
|
let session;
|
|
@@ -153,31 +204,14 @@ async function consumeSsoReturn(oxy, deps = {}) {
|
|
|
153
204
|
catch (error) {
|
|
154
205
|
onExchangeError?.(error);
|
|
155
206
|
markNoSession();
|
|
207
|
+
restoreDest();
|
|
156
208
|
return null;
|
|
157
209
|
}
|
|
158
210
|
if (!session?.sessionId) {
|
|
159
211
|
markNoSession();
|
|
212
|
+
restoreDest();
|
|
160
213
|
return null;
|
|
161
214
|
}
|
|
162
|
-
|
|
163
|
-
// destination (captured at bounce time). Same-origin only — never honour a
|
|
164
|
-
// cross-origin destination that could have been planted to redirect the
|
|
165
|
-
// freshly signed-in user. `new URL(dest, origin)` tolerates relative dests
|
|
166
|
-
// and is still re-checked against the page origin.
|
|
167
|
-
if (location.pathname === ssoBounce_1.SSO_CALLBACK_PATH) {
|
|
168
|
-
const dest = storage.getItem((0, ssoBounce_1.ssoDestKey)(origin));
|
|
169
|
-
if (dest) {
|
|
170
|
-
try {
|
|
171
|
-
const destUrl = new URL(dest, origin);
|
|
172
|
-
if (destUrl.origin === origin) {
|
|
173
|
-
history.replaceState(null, '', destUrl.pathname + destUrl.search + destUrl.hash);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
catch {
|
|
177
|
-
// Malformed stored destination — leave the URL on the callback path.
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
storage.removeItem((0, ssoBounce_1.ssoDestKey)(origin));
|
|
215
|
+
restoreDest();
|
|
182
216
|
return session;
|
|
183
217
|
}
|