@oxyhq/core 3.4.4 → 3.4.6
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/OxyServices.base.js +39 -0
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/utils/ssoBounce.js +32 -0
- package/dist/cjs/utils/ssoReturn.js +4 -1
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/OxyServices.base.js +39 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/utils/ssoBounce.js +30 -0
- package/dist/esm/utils/ssoReturn.js +5 -2
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +1 -1
- package/dist/types/OxyServices.base.d.ts +14 -0
- package/dist/types/OxyServices.d.ts +2 -1
- package/dist/types/index.d.ts +2 -1
- package/dist/types/mixins/OxyServices.analytics.d.ts +1 -0
- package/dist/types/mixins/OxyServices.appData.d.ts +1 -0
- package/dist/types/mixins/OxyServices.applications.d.ts +1 -0
- package/dist/types/mixins/OxyServices.assets.d.ts +1 -0
- package/dist/types/mixins/OxyServices.auth.d.ts +1 -0
- package/dist/types/mixins/OxyServices.contacts.d.ts +1 -0
- package/dist/types/mixins/OxyServices.devices.d.ts +1 -0
- package/dist/types/mixins/OxyServices.features.d.ts +1 -0
- package/dist/types/mixins/OxyServices.fedcm.d.ts +1 -0
- package/dist/types/mixins/OxyServices.language.d.ts +1 -0
- package/dist/types/mixins/OxyServices.location.d.ts +1 -0
- package/dist/types/mixins/OxyServices.managedAccounts.d.ts +1 -0
- package/dist/types/mixins/OxyServices.payment.d.ts +1 -0
- package/dist/types/mixins/OxyServices.privacy.d.ts +1 -0
- package/dist/types/mixins/OxyServices.redirect.d.ts +1 -0
- package/dist/types/mixins/OxyServices.reputation.d.ts +1 -0
- package/dist/types/mixins/OxyServices.security.d.ts +1 -0
- package/dist/types/mixins/OxyServices.silent.d.ts +1 -0
- package/dist/types/mixins/OxyServices.sso.d.ts +1 -0
- package/dist/types/mixins/OxyServices.topics.d.ts +1 -0
- package/dist/types/mixins/OxyServices.user.d.ts +1 -0
- package/dist/types/mixins/OxyServices.utility.d.ts +1 -0
- package/dist/types/mixins/OxyServices.workspaces.d.ts +1 -0
- package/dist/types/utils/ssoBounce.d.ts +23 -0
- package/package.json +1 -1
- package/src/HttpService.ts +1 -1
- package/src/OxyServices.base.ts +50 -1
- package/src/OxyServices.ts +3 -1
- package/src/__tests__/linkedClient.test.ts +64 -0
- package/src/index.ts +3 -0
- package/src/utils/__tests__/ssoReturn.test.ts +132 -1
- package/src/utils/ssoBounce.ts +33 -0
- package/src/utils/ssoReturn.ts +5 -1
|
@@ -85,6 +85,45 @@ export class OxyServicesBase {
|
|
|
85
85
|
getClient() {
|
|
86
86
|
return this.httpService;
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Create an app/backend HTTP client linked to this Oxy session.
|
|
90
|
+
*
|
|
91
|
+
* Use this when an app has its own API origin (for example
|
|
92
|
+
* `https://api.syra.fm`) but authentication is owned by the canonical
|
|
93
|
+
* OxyServices instance mounted in OxyProvider. The returned client has its own
|
|
94
|
+
* base URL, cache and request queue, but its bearer token is kept in lockstep
|
|
95
|
+
* with this session and its 401 refresh path delegates back to this session.
|
|
96
|
+
*/
|
|
97
|
+
createLinkedClient(config) {
|
|
98
|
+
const client = new HttpService(config);
|
|
99
|
+
const syncToken = (accessToken) => {
|
|
100
|
+
const currentAccessToken = client.getAccessToken();
|
|
101
|
+
if (accessToken) {
|
|
102
|
+
if (currentAccessToken !== accessToken) {
|
|
103
|
+
client.setTokens(accessToken);
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (currentAccessToken) {
|
|
108
|
+
client.clearTokens();
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
syncToken(this.getAccessToken());
|
|
112
|
+
const unsubscribe = this.onTokensChanged(syncToken);
|
|
113
|
+
client.setAuthRefreshHandler(async (reason) => {
|
|
114
|
+
const refreshed = await this.httpService.refreshAccessToken(reason);
|
|
115
|
+
syncToken(refreshed);
|
|
116
|
+
return refreshed;
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
client,
|
|
120
|
+
dispose: () => {
|
|
121
|
+
unsubscribe();
|
|
122
|
+
client.setAuthRefreshHandler(null);
|
|
123
|
+
client.clearTokens();
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
88
127
|
/**
|
|
89
128
|
* Get performance metrics
|
|
90
129
|
*/
|
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
|
-
|
|
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));
|