@oxyhq/core 3.4.4 → 3.4.5

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/esm/index.js CHANGED
@@ -105,7 +105,7 @@ export { CENTRAL_AUTH_URL, CENTRAL_IDP_APEX, resolveCentralAuthUrl } from './uti
105
105
  export { parseSsoReturnFragment, consumeSsoReturn } from './utils/ssoReturn.js';
106
106
  export { generateSsoState } from './mixins/OxyServices.sso.js';
107
107
  // SSO bounce — per-origin sessionStorage keys, bounce URL builder, predicates
108
- export { SSO_CALLBACK_PATH, SSO_GUARD_TTL_MS, ssoStateKey, ssoGuardKey, ssoDestKey, ssoNoSessionKey, ssoAttemptedKey, ssoNavigate, buildSsoBounceUrl, isCentralIdPOrigin, guardActive, } from './utils/ssoBounce.js';
108
+ export { SSO_CALLBACK_PATH, SSO_GUARD_TTL_MS, ssoStateKey, ssoGuardKey, ssoDestKey, ssoNoSessionKey, ssoAttemptedKey, ssoCallbackBootstrapKey, ssoNavigate, getSsoCallbackBootstrapScript, buildSsoBounceUrl, isCentralIdPOrigin, guardActive, } from './utils/ssoBounce.js';
109
109
  export { runColdBoot } from './utils/coldBoot.js';
110
110
  // API response contracts (request/response Zod schemas + inferred types) live in
111
111
  // `@oxyhq/contracts` — the single source of truth shared by the backend and every
@@ -63,6 +63,7 @@ const GUARD_KEY_PREFIX = 'oxy_sso_guard:';
63
63
  const DEST_KEY_PREFIX = 'oxy_sso_dest:';
64
64
  const NO_SESSION_KEY_PREFIX = 'oxy_sso_no_session:';
65
65
  const ATTEMPTED_KEY_PREFIX = 'oxy_sso_attempted:';
66
+ const CALLBACK_BOOTSTRAP_KEY_PREFIX = 'oxy_sso_callback_bootstrap:';
66
67
  /** Per-origin CSRF state key (matched on return to defeat fragment forgery). */
67
68
  export function ssoStateKey(origin) {
68
69
  return `${STATE_KEY_PREFIX}${origin}`;
@@ -95,6 +96,35 @@ export function ssoNoSessionKey(origin) {
95
96
  export function ssoAttemptedKey(origin) {
96
97
  return `${ATTEMPTED_KEY_PREFIX}${origin}`;
97
98
  }
99
+ /**
100
+ * Per-origin marker written by the pre-hydration callback bootstrap.
101
+ *
102
+ * Static Expo exports render unknown paths as `+not-found`; on
103
+ * `/__oxy/sso-callback` that can fail hydration before the React provider has a
104
+ * chance to run `consumeSsoReturn`. The bootstrap runs in the HTML head, moves
105
+ * the URL to a hydratable route while preserving the SSO fragment, and writes
106
+ * this marker so `consumeSsoReturn` still restores the original destination as
107
+ * if the page were physically on the callback path.
108
+ */
109
+ export function ssoCallbackBootstrapKey(origin) {
110
+ return `${CALLBACK_BOOTSTRAP_KEY_PREFIX}${origin}`;
111
+ }
112
+ /**
113
+ * Inline script for Expo/static web apps.
114
+ *
115
+ * Must run before the app bundle hydrates. It is intentionally tiny and
116
+ * dependency-free: if the browser lands on the internal callback route with an
117
+ * Oxy SSO fragment, it marks the handoff and rewrites the path to `/` while
118
+ * preserving `#oxy_sso=...`. The normal SDK cold-boot `sso-return` step then
119
+ * consumes the fragment from a route that can hydrate. If the internal route is
120
+ * reached without a valid SSO fragment, it leaves the route via a hard root
121
+ * navigation because there is no session material to preserve.
122
+ */
123
+ export function getSsoCallbackBootstrapScript() {
124
+ const callbackPath = JSON.stringify(SSO_CALLBACK_PATH);
125
+ const bootstrapPrefix = JSON.stringify(CALLBACK_BOOTSTRAP_KEY_PREFIX);
126
+ return `(function(){var p=${callbackPath};if(window.location.pathname!==p)return;var h=window.location.hash||"";if(!/(?:^#|&)oxy_sso=(?:ok|none|error)(?:&|$)/.test(h)){window.location.replace("/");return;}try{window.sessionStorage.setItem(${bootstrapPrefix}+window.location.origin,"1");}catch(e){window.__oxySsoCallbackBootstrapError=e instanceof Error?e.message:String(e);}try{window.history.replaceState(null,"","/"+h);}catch(e){window.__oxySsoCallbackBootstrapError=e instanceof Error?e.message:String(e);window.location.replace("/"+h);}})();`;
127
+ }
98
128
  /**
99
129
  * Perform the terminal top-level SSO bounce navigation.
100
130
  *
@@ -20,7 +20,7 @@
20
20
  * an oxy_sso fragment at all (i.e. `oxy_sso` is absent or an unrecognised
21
21
  * value), so the caller can ignore unrelated fragments without special-casing.
22
22
  */
23
- import { SSO_CALLBACK_PATH, ssoStateKey, ssoGuardKey, ssoDestKey, ssoNoSessionKey, ssoAttemptedKey, } from './ssoBounce.js';
23
+ import { SSO_CALLBACK_PATH, ssoStateKey, ssoGuardKey, ssoDestKey, ssoNoSessionKey, ssoAttemptedKey, ssoCallbackBootstrapKey, } from './ssoBounce.js';
24
24
  const VALID_KINDS = new Set(['ok', 'none', 'error']);
25
25
  /**
26
26
  * Parse an SSO return fragment.
@@ -158,6 +158,8 @@ export async function consumeSsoReturn(oxy, deps = {}) {
158
158
  return null;
159
159
  }
160
160
  const origin = location.origin;
161
+ const callbackBootstrapKey = ssoCallbackBootstrapKey(origin);
162
+ const wasCallbackBootstrapped = storage.getItem(callbackBootstrapKey) === '1';
161
163
  const expectedState = storage.getItem(ssoStateKey(origin));
162
164
  const stateOk = !!ret.state && !!expectedState && ret.state === expectedState;
163
165
  // Strip the fragment FIRST so the opaque code never lingers in the address
@@ -183,7 +185,8 @@ export async function consumeSsoReturn(oxy, deps = {}) {
183
185
  // (so it can be fed to either `history.replaceState` or a `hardRedirect`),
184
186
  // or `null` when the page is not on the callback path (nothing to leave).
185
187
  const consumeCallbackTarget = () => {
186
- if (location.pathname !== SSO_CALLBACK_PATH) {
188
+ storage.removeItem(callbackBootstrapKey);
189
+ if (location.pathname !== SSO_CALLBACK_PATH && !wasCallbackBootstrapped) {
187
190
  // Not on the callback path — still drop the dest key (consumed) but there
188
191
  // is nothing to navigate away from.
189
192
  storage.removeItem(ssoDestKey(origin));