@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.
Files changed (48) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/OxyServices.base.js +39 -0
  3. package/dist/cjs/index.js +3 -1
  4. package/dist/cjs/utils/ssoBounce.js +32 -0
  5. package/dist/cjs/utils/ssoReturn.js +4 -1
  6. package/dist/esm/.tsbuildinfo +1 -1
  7. package/dist/esm/OxyServices.base.js +39 -0
  8. package/dist/esm/index.js +1 -1
  9. package/dist/esm/utils/ssoBounce.js +30 -0
  10. package/dist/esm/utils/ssoReturn.js +5 -2
  11. package/dist/types/.tsbuildinfo +1 -1
  12. package/dist/types/HttpService.d.ts +1 -1
  13. package/dist/types/OxyServices.base.d.ts +14 -0
  14. package/dist/types/OxyServices.d.ts +2 -1
  15. package/dist/types/index.d.ts +2 -1
  16. package/dist/types/mixins/OxyServices.analytics.d.ts +1 -0
  17. package/dist/types/mixins/OxyServices.appData.d.ts +1 -0
  18. package/dist/types/mixins/OxyServices.applications.d.ts +1 -0
  19. package/dist/types/mixins/OxyServices.assets.d.ts +1 -0
  20. package/dist/types/mixins/OxyServices.auth.d.ts +1 -0
  21. package/dist/types/mixins/OxyServices.contacts.d.ts +1 -0
  22. package/dist/types/mixins/OxyServices.devices.d.ts +1 -0
  23. package/dist/types/mixins/OxyServices.features.d.ts +1 -0
  24. package/dist/types/mixins/OxyServices.fedcm.d.ts +1 -0
  25. package/dist/types/mixins/OxyServices.language.d.ts +1 -0
  26. package/dist/types/mixins/OxyServices.location.d.ts +1 -0
  27. package/dist/types/mixins/OxyServices.managedAccounts.d.ts +1 -0
  28. package/dist/types/mixins/OxyServices.payment.d.ts +1 -0
  29. package/dist/types/mixins/OxyServices.privacy.d.ts +1 -0
  30. package/dist/types/mixins/OxyServices.redirect.d.ts +1 -0
  31. package/dist/types/mixins/OxyServices.reputation.d.ts +1 -0
  32. package/dist/types/mixins/OxyServices.security.d.ts +1 -0
  33. package/dist/types/mixins/OxyServices.silent.d.ts +1 -0
  34. package/dist/types/mixins/OxyServices.sso.d.ts +1 -0
  35. package/dist/types/mixins/OxyServices.topics.d.ts +1 -0
  36. package/dist/types/mixins/OxyServices.user.d.ts +1 -0
  37. package/dist/types/mixins/OxyServices.utility.d.ts +1 -0
  38. package/dist/types/mixins/OxyServices.workspaces.d.ts +1 -0
  39. package/dist/types/utils/ssoBounce.d.ts +23 -0
  40. package/package.json +1 -1
  41. package/src/HttpService.ts +1 -1
  42. package/src/OxyServices.base.ts +50 -1
  43. package/src/OxyServices.ts +3 -1
  44. package/src/__tests__/linkedClient.test.ts +64 -0
  45. package/src/index.ts +3 -0
  46. package/src/utils/__tests__/ssoReturn.test.ts +132 -1
  47. package/src/utils/ssoBounce.ts +33 -0
  48. package/src/utils/ssoReturn.ts +5 -1
@@ -88,6 +88,45 @@ class OxyServicesBase {
88
88
  getClient() {
89
89
  return this.httpService;
90
90
  }
91
+ /**
92
+ * Create an app/backend HTTP client linked to this Oxy session.
93
+ *
94
+ * Use this when an app has its own API origin (for example
95
+ * `https://api.syra.fm`) but authentication is owned by the canonical
96
+ * OxyServices instance mounted in OxyProvider. The returned client has its own
97
+ * base URL, cache and request queue, but its bearer token is kept in lockstep
98
+ * with this session and its 401 refresh path delegates back to this session.
99
+ */
100
+ createLinkedClient(config) {
101
+ const client = new HttpService_1.HttpService(config);
102
+ const syncToken = (accessToken) => {
103
+ const currentAccessToken = client.getAccessToken();
104
+ if (accessToken) {
105
+ if (currentAccessToken !== accessToken) {
106
+ client.setTokens(accessToken);
107
+ }
108
+ return;
109
+ }
110
+ if (currentAccessToken) {
111
+ client.clearTokens();
112
+ }
113
+ };
114
+ syncToken(this.getAccessToken());
115
+ const unsubscribe = this.onTokensChanged(syncToken);
116
+ client.setAuthRefreshHandler(async (reason) => {
117
+ const refreshed = await this.httpService.refreshAccessToken(reason);
118
+ syncToken(refreshed);
119
+ return refreshed;
120
+ });
121
+ return {
122
+ client,
123
+ dispose: () => {
124
+ unsubscribe();
125
+ client.setAuthRefreshHandler(null);
126
+ client.clearTokens();
127
+ },
128
+ };
129
+ }
91
130
  /**
92
131
  * Get performance metrics
93
132
  */
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.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;
23
+ exports.packageInfo = exports.runColdBoot = exports.guardActive = exports.isCentralIdPOrigin = exports.buildSsoBounceUrl = exports.getSsoCallbackBootstrapScript = exports.ssoNavigate = exports.ssoCallbackBootstrapKey = 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
  // ---------------------------------------------------------------------------
@@ -251,7 +251,9 @@ Object.defineProperty(exports, "ssoGuardKey", { enumerable: true, get: function
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
253
  Object.defineProperty(exports, "ssoAttemptedKey", { enumerable: true, get: function () { return ssoBounce_1.ssoAttemptedKey; } });
254
+ Object.defineProperty(exports, "ssoCallbackBootstrapKey", { enumerable: true, get: function () { return ssoBounce_1.ssoCallbackBootstrapKey; } });
254
255
  Object.defineProperty(exports, "ssoNavigate", { enumerable: true, get: function () { return ssoBounce_1.ssoNavigate; } });
256
+ Object.defineProperty(exports, "getSsoCallbackBootstrapScript", { enumerable: true, get: function () { return ssoBounce_1.getSsoCallbackBootstrapScript; } });
255
257
  Object.defineProperty(exports, "buildSsoBounceUrl", { enumerable: true, get: function () { return ssoBounce_1.buildSsoBounceUrl; } });
256
258
  Object.defineProperty(exports, "isCentralIdPOrigin", { enumerable: true, get: function () { return ssoBounce_1.isCentralIdPOrigin; } });
257
259
  Object.defineProperty(exports, "guardActive", { enumerable: true, get: function () { return ssoBounce_1.guardActive; } });
@@ -49,6 +49,8 @@ exports.ssoGuardKey = ssoGuardKey;
49
49
  exports.ssoDestKey = ssoDestKey;
50
50
  exports.ssoNoSessionKey = ssoNoSessionKey;
51
51
  exports.ssoAttemptedKey = ssoAttemptedKey;
52
+ exports.ssoCallbackBootstrapKey = ssoCallbackBootstrapKey;
53
+ exports.getSsoCallbackBootstrapScript = getSsoCallbackBootstrapScript;
52
54
  exports.ssoNavigate = ssoNavigate;
53
55
  exports.buildSsoBounceUrl = buildSsoBounceUrl;
54
56
  exports.isCentralIdPOrigin = isCentralIdPOrigin;
@@ -75,6 +77,7 @@ const GUARD_KEY_PREFIX = 'oxy_sso_guard:';
75
77
  const DEST_KEY_PREFIX = 'oxy_sso_dest:';
76
78
  const NO_SESSION_KEY_PREFIX = 'oxy_sso_no_session:';
77
79
  const ATTEMPTED_KEY_PREFIX = 'oxy_sso_attempted:';
80
+ const CALLBACK_BOOTSTRAP_KEY_PREFIX = 'oxy_sso_callback_bootstrap:';
78
81
  /** Per-origin CSRF state key (matched on return to defeat fragment forgery). */
79
82
  function ssoStateKey(origin) {
80
83
  return `${STATE_KEY_PREFIX}${origin}`;
@@ -107,6 +110,35 @@ function ssoNoSessionKey(origin) {
107
110
  function ssoAttemptedKey(origin) {
108
111
  return `${ATTEMPTED_KEY_PREFIX}${origin}`;
109
112
  }
113
+ /**
114
+ * Per-origin marker written by the pre-hydration callback bootstrap.
115
+ *
116
+ * Static Expo exports render unknown paths as `+not-found`; on
117
+ * `/__oxy/sso-callback` that can fail hydration before the React provider has a
118
+ * chance to run `consumeSsoReturn`. The bootstrap runs in the HTML head, moves
119
+ * the URL to a hydratable route while preserving the SSO fragment, and writes
120
+ * this marker so `consumeSsoReturn` still restores the original destination as
121
+ * if the page were physically on the callback path.
122
+ */
123
+ function ssoCallbackBootstrapKey(origin) {
124
+ return `${CALLBACK_BOOTSTRAP_KEY_PREFIX}${origin}`;
125
+ }
126
+ /**
127
+ * Inline script for Expo/static web apps.
128
+ *
129
+ * Must run before the app bundle hydrates. It is intentionally tiny and
130
+ * dependency-free: if the browser lands on the internal callback route with an
131
+ * Oxy SSO fragment, it marks the handoff and rewrites the path to `/` while
132
+ * preserving `#oxy_sso=...`. The normal SDK cold-boot `sso-return` step then
133
+ * consumes the fragment from a route that can hydrate. If the internal route is
134
+ * reached without a valid SSO fragment, it leaves the route via a hard root
135
+ * navigation because there is no session material to preserve.
136
+ */
137
+ function getSsoCallbackBootstrapScript() {
138
+ const callbackPath = JSON.stringify(exports.SSO_CALLBACK_PATH);
139
+ const bootstrapPrefix = JSON.stringify(CALLBACK_BOOTSTRAP_KEY_PREFIX);
140
+ 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);}})();`;
141
+ }
110
142
  /**
111
143
  * Perform the terminal top-level SSO bounce navigation.
112
144
  *
@@ -162,6 +162,8 @@ async function consumeSsoReturn(oxy, deps = {}) {
162
162
  return null;
163
163
  }
164
164
  const origin = location.origin;
165
+ const callbackBootstrapKey = (0, ssoBounce_1.ssoCallbackBootstrapKey)(origin);
166
+ const wasCallbackBootstrapped = storage.getItem(callbackBootstrapKey) === '1';
165
167
  const expectedState = storage.getItem((0, ssoBounce_1.ssoStateKey)(origin));
166
168
  const stateOk = !!ret.state && !!expectedState && ret.state === expectedState;
167
169
  // Strip the fragment FIRST so the opaque code never lingers in the address
@@ -187,7 +189,8 @@ async function consumeSsoReturn(oxy, deps = {}) {
187
189
  // (so it can be fed to either `history.replaceState` or a `hardRedirect`),
188
190
  // or `null` when the page is not on the callback path (nothing to leave).
189
191
  const consumeCallbackTarget = () => {
190
- if (location.pathname !== ssoBounce_1.SSO_CALLBACK_PATH) {
192
+ storage.removeItem(callbackBootstrapKey);
193
+ if (location.pathname !== ssoBounce_1.SSO_CALLBACK_PATH && !wasCallbackBootstrapped) {
191
194
  // Not on the callback path — still drop the dest key (consumed) but there
192
195
  // is nothing to navigate away from.
193
196
  storage.removeItem((0, ssoBounce_1.ssoDestKey)(origin));