From 95a482472fcdfbde9490aea38ad5be119aee7672 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 21 Mar 2025 22:26:18 +0200 Subject: [PATCH 1/7] chore(clerk-js): Use local environment on outage --- .changeset/cool-times-divide.md | 5 ++ packages/clerk-js/src/core/clerk.ts | 22 +++++++ packages/clerk-js/src/core/events.ts | 2 + .../src/core/resources/Environment.ts | 8 ++- packages/clerk-js/src/ui/hooks/index.ts | 1 - .../clerk-js/src/ui/hooks/useLocalStorage.ts | 33 ---------- .../clerk-js/src/ui/utils/localStorage.ts | 63 +++++++++++++++++++ 7 files changed, 99 insertions(+), 35 deletions(-) create mode 100644 .changeset/cool-times-divide.md delete mode 100644 packages/clerk-js/src/ui/hooks/useLocalStorage.ts create mode 100644 packages/clerk-js/src/ui/utils/localStorage.ts diff --git a/.changeset/cool-times-divide.md b/.changeset/cool-times-divide.md new file mode 100644 index 00000000000..fe59aceafbf --- /dev/null +++ b/.changeset/cool-times-divide.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Fallback to locally stored environment during an outage. diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 4434a203161..5cb1426acbf 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -76,6 +76,7 @@ import type { } from '@clerk/types'; import type { MountComponentRenderer } from '../ui/Components'; +import { SafeLocalStorage } from '../ui/utils/localStorage'; import { ALLOWED_PROTOCOLS, buildURL, @@ -2087,6 +2088,19 @@ export class Clerk implements ClerkInterface { .fetch({ touch: shouldTouchEnv }) .then(res => { this.updateEnvironment(res); + }) + .catch(e => { + const environmentSnapshot = SafeLocalStorage.getItem( + 'environment_snapshot', + null, + ); + + if (environmentSnapshot) { + this.updateEnvironment(new Environment(environmentSnapshot)); + return; + } + + throw e; }); const initClient = async () => { @@ -2126,6 +2140,7 @@ export class Clerk implements ClerkInterface { }; const [envResult, clientResult] = await Promise.allSettled([initEnvironmentPromise, initClient()]); + if (clientResult.status === 'rejected') { const e = clientResult.reason; @@ -2140,6 +2155,8 @@ export class Clerk implements ClerkInterface { } } + await initEnvironmentPromise; + this.#authService?.setClientUatCookieForDevelopmentInstances(); if (await this.#redirectFAPIInitiatedFlow()) { @@ -2265,6 +2282,11 @@ export class Clerk implements ClerkInterface { eventBus.on(events.UserSignOut, () => { this.#broadcastChannel?.postMessage({ type: 'signout' }); }); + + eventBus.on(events.EnvironmentUpdate, () => { + // Cache the environment snapshot for 24 hours + SafeLocalStorage.setItem('environment_snapshot', this.environment?.__internal_toSnapshot(), 24 * 60 * 60 * 1_000); + }); }; // TODO: Be more conservative about touches. Throttle, don't touch when only one user, etc diff --git a/packages/clerk-js/src/core/events.ts b/packages/clerk-js/src/core/events.ts index 7401dd91370..42181706221 100644 --- a/packages/clerk-js/src/core/events.ts +++ b/packages/clerk-js/src/core/events.ts @@ -3,6 +3,7 @@ import type { TokenResource } from '@clerk/types'; export const events = { TokenUpdate: 'token:update', UserSignOut: 'user:signOut', + EnvironmentUpdate: 'environment:update', } as const; type ClerkEvent = (typeof events)[keyof typeof events]; @@ -13,6 +14,7 @@ type TokenUpdatePayload = { token: TokenResource | null }; type EventPayload = { [events.TokenUpdate]: TokenUpdatePayload; [events.UserSignOut]: null; + [events.EnvironmentUpdate]: null; }; const createEventBus = () => { diff --git a/packages/clerk-js/src/core/resources/Environment.ts b/packages/clerk-js/src/core/resources/Environment.ts index f589088a749..af180374ec9 100644 --- a/packages/clerk-js/src/core/resources/Environment.ts +++ b/packages/clerk-js/src/core/resources/Environment.ts @@ -9,6 +9,7 @@ import type { UserSettingsResource, } from '@clerk/types'; +import { eventBus, events } from '../../core/events'; import { __experimental_CommerceSettings, AuthConfig, BaseResource, DisplayConfig, UserSettings } from './internal'; import { OrganizationSettings } from './OrganizationSettings'; @@ -53,7 +54,12 @@ export class Environment extends BaseResource implements EnvironmentResource { } fetch({ touch, fetchMaxTries }: { touch: boolean; fetchMaxTries?: number } = { touch: false }): Promise { - return touch ? this._basePatch({}) : this._baseGet({ fetchMaxTries }); + const promise = touch ? this._basePatch({}) : this._baseGet({ fetchMaxTries }); + + return promise.then(data => { + eventBus.dispatch(events.EnvironmentUpdate, null); + return data; + }); } isDevelopmentOrStaging = (): boolean => { diff --git a/packages/clerk-js/src/ui/hooks/index.ts b/packages/clerk-js/src/ui/hooks/index.ts index 92dc877a1de..c60cc21955a 100644 --- a/packages/clerk-js/src/ui/hooks/index.ts +++ b/packages/clerk-js/src/ui/hooks/index.ts @@ -9,7 +9,6 @@ export * from './useEnterpriseSSOLink'; export * from './useFetch'; export * from './useInView'; export * from './useLoadingStatus'; -export * from './useLocalStorage'; export * from './useNavigateToFlowStart'; export * from './usePassword'; export * from './usePasswordComplexity'; diff --git a/packages/clerk-js/src/ui/hooks/useLocalStorage.ts b/packages/clerk-js/src/ui/hooks/useLocalStorage.ts deleted file mode 100644 index 9b0e68e49a7..00000000000 --- a/packages/clerk-js/src/ui/hooks/useLocalStorage.ts +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; - -export function useLocalStorage(key: string, initialValue: T) { - key = 'clerk:' + key; - const [storedValue, setStoredValue] = React.useState(() => { - if (typeof window === 'undefined') { - return initialValue; - } - - try { - const item = window.localStorage.getItem(key); - return item ? JSON.parse(item) : initialValue; - } catch { - return initialValue; - } - }); - - const setValue = React.useCallback((value: ((stored: T) => T) | T) => { - if (typeof window === 'undefined') { - console.warn(`Tried setting localStorage key "${key}" even though environment is not a client`); - } - - try { - const valueToStore = value instanceof Function ? value(storedValue) : value; - setStoredValue(valueToStore); - window.localStorage.setItem(key, JSON.stringify(valueToStore)); - } catch (error) { - console.error(error); - } - }, []); - - return [storedValue, setValue] as const; -} diff --git a/packages/clerk-js/src/ui/utils/localStorage.ts b/packages/clerk-js/src/ui/utils/localStorage.ts new file mode 100644 index 00000000000..f9bf590a4ac --- /dev/null +++ b/packages/clerk-js/src/ui/utils/localStorage.ts @@ -0,0 +1,63 @@ +const CLERK_PREFIX = 'clerk_'; + +interface StorageEntry { + value: T; + exp?: number; +} + +/** + * Safe wrapper around localStorage that automatically prefixes keys with 'clerk_' + * and handles potential errors and entry expiration + */ +export class SafeLocalStorage { + private static _key(key: string): string { + return `${CLERK_PREFIX}${key}`; + } + + private static isExpired(entry: StorageEntry): boolean { + return !!entry.exp && Date.now() > entry.exp; + } + + static setItem(key: string, value: unknown, expiresInMs?: number): void { + try { + const entry: StorageEntry = { + value, + ...(expiresInMs && { exp: Date.now() + expiresInMs }), + }; + window.localStorage.setItem(this._key(key), JSON.stringify(entry)); + } catch { + // noop + } + } + + static getItem(key: string, defaultValue: T): T { + try { + const item = window.localStorage.getItem(this._key(key)); + if (!item) return defaultValue; + + const entry = JSON.parse(item) as StorageEntry; + + // Handle legacy items that weren't stored with expiration metadata + if (!('value' in entry)) { + return entry as T; + } + + if (this.isExpired(entry)) { + this.removeItem(key); + return defaultValue; + } + + return entry.value; + } catch { + return defaultValue; + } + } + + static removeItem(key: string): void { + try { + window.localStorage.removeItem(this._key(key)); + } catch { + // noop + } + } +} From dd6b7d230de58ba450285f280961e532be59df9a Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 21 Mar 2025 22:33:54 +0200 Subject: [PATCH 2/7] use variable instead of string --- packages/clerk-js/src/core/clerk.ts | 17 ++++++++++------- packages/clerk-js/src/ui/utils/localStorage.ts | 2 ++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 5cb1426acbf..b890fcd42fe 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -76,7 +76,7 @@ import type { } from '@clerk/types'; import type { MountComponentRenderer } from '../ui/Components'; -import { SafeLocalStorage } from '../ui/utils/localStorage'; +import { CLERK_ENVIRONMENT_STORAGE_ENTRY, SafeLocalStorage } from '../ui/utils/localStorage'; import { ALLOWED_PROTOCOLS, buildURL, @@ -2091,16 +2091,15 @@ export class Clerk implements ClerkInterface { }) .catch(e => { const environmentSnapshot = SafeLocalStorage.getItem( - 'environment_snapshot', + CLERK_ENVIRONMENT_STORAGE_ENTRY, null, ); - if (environmentSnapshot) { - this.updateEnvironment(new Environment(environmentSnapshot)); - return; + if (!environmentSnapshot) { + throw e; } - throw e; + this.updateEnvironment(new Environment(environmentSnapshot)); }); const initClient = async () => { @@ -2285,7 +2284,11 @@ export class Clerk implements ClerkInterface { eventBus.on(events.EnvironmentUpdate, () => { // Cache the environment snapshot for 24 hours - SafeLocalStorage.setItem('environment_snapshot', this.environment?.__internal_toSnapshot(), 24 * 60 * 60 * 1_000); + SafeLocalStorage.setItem( + CLERK_ENVIRONMENT_STORAGE_ENTRY, + this.environment?.__internal_toSnapshot(), + 24 * 60 * 60 * 1_000, + ); }); }; diff --git a/packages/clerk-js/src/ui/utils/localStorage.ts b/packages/clerk-js/src/ui/utils/localStorage.ts index f9bf590a4ac..83d374ce70b 100644 --- a/packages/clerk-js/src/ui/utils/localStorage.ts +++ b/packages/clerk-js/src/ui/utils/localStorage.ts @@ -1,5 +1,7 @@ const CLERK_PREFIX = 'clerk_'; +export const CLERK_ENVIRONMENT_STORAGE_ENTRY = 'environment_snapshot'; + interface StorageEntry { value: T; exp?: number; From 91212b3c82b99194cf16761ff412284256e6861b Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 21 Mar 2025 22:56:22 +0200 Subject: [PATCH 3/7] minor change --- packages/clerk-js/src/ui/utils/localStorage.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/clerk-js/src/ui/utils/localStorage.ts b/packages/clerk-js/src/ui/utils/localStorage.ts index 83d374ce70b..8c59b7e70f4 100644 --- a/packages/clerk-js/src/ui/utils/localStorage.ts +++ b/packages/clerk-js/src/ui/utils/localStorage.ts @@ -1,12 +1,14 @@ const CLERK_PREFIX = 'clerk_'; -export const CLERK_ENVIRONMENT_STORAGE_ENTRY = 'environment_snapshot'; +export const CLERK_ENVIRONMENT_STORAGE_ENTRY = 'environment'; interface StorageEntry { value: T; exp?: number; } +const serialize = JSON.stringify; + /** * Safe wrapper around localStorage that automatically prefixes keys with 'clerk_' * and handles potential errors and entry expiration @@ -26,7 +28,7 @@ export class SafeLocalStorage { value, ...(expiresInMs && { exp: Date.now() + expiresInMs }), }; - window.localStorage.setItem(this._key(key), JSON.stringify(entry)); + window.localStorage.setItem(this._key(key), serialize(entry)); } catch { // noop } @@ -37,7 +39,7 @@ export class SafeLocalStorage { const item = window.localStorage.getItem(this._key(key)); if (!item) return defaultValue; - const entry = JSON.parse(item) as StorageEntry; + const entry = serialize(item) as unknown as StorageEntry; // Handle legacy items that weren't stored with expiration metadata if (!('value' in entry)) { From c1e8697648d455906492ae49e35d68d39e57790c Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 21 Mar 2025 23:06:17 +0200 Subject: [PATCH 4/7] minor change --- packages/clerk-js/bundlewatch.config.json | 4 ++-- packages/clerk-js/src/core/clerk.ts | 2 +- packages/clerk-js/src/{ui => }/utils/localStorage.ts | 0 3 files changed, 3 insertions(+), 3 deletions(-) rename packages/clerk-js/src/{ui => }/utils/localStorage.ts (100%) diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index a81ab917af1..1ab3f60f95c 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -1,8 +1,8 @@ { "files": [ { "path": "./dist/clerk.js", "maxSize": "580kB" }, - { "path": "./dist/clerk.browser.js", "maxSize": "78.6kB" }, - { "path": "./dist/clerk.headless.js", "maxSize": "55KB" }, + { "path": "./dist/clerk.browser.js", "maxSize": "78.9kB" }, + { "path": "./dist/clerk.headless*.js", "maxSize": "55KB" }, { "path": "./dist/ui-common*.js", "maxSize": "94KB" }, { "path": "./dist/vendors*.js", "maxSize": "30KB" }, { "path": "./dist/coinbase*.js", "maxSize": "35.5KB" }, diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index b890fcd42fe..00dd0e6fef0 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -76,7 +76,7 @@ import type { } from '@clerk/types'; import type { MountComponentRenderer } from '../ui/Components'; -import { CLERK_ENVIRONMENT_STORAGE_ENTRY, SafeLocalStorage } from '../ui/utils/localStorage'; +import { CLERK_ENVIRONMENT_STORAGE_ENTRY, SafeLocalStorage } from '../utils/localStorage'; import { ALLOWED_PROTOCOLS, buildURL, diff --git a/packages/clerk-js/src/ui/utils/localStorage.ts b/packages/clerk-js/src/utils/localStorage.ts similarity index 100% rename from packages/clerk-js/src/ui/utils/localStorage.ts rename to packages/clerk-js/src/utils/localStorage.ts From 124c17798826b80c5bd0a5a093c25629c3df569f Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 21 Mar 2025 23:12:46 +0200 Subject: [PATCH 5/7] fix lint --- packages/clerk-js/src/core/clerk.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 00dd0e6fef0..d72ddbfa118 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -76,7 +76,6 @@ import type { } from '@clerk/types'; import type { MountComponentRenderer } from '../ui/Components'; -import { CLERK_ENVIRONMENT_STORAGE_ENTRY, SafeLocalStorage } from '../utils/localStorage'; import { ALLOWED_PROTOCOLS, buildURL, @@ -107,6 +106,7 @@ import { windowNavigate, } from '../utils'; import { assertNoLegacyProp } from '../utils/assertNoLegacyProp'; +import { CLERK_ENVIRONMENT_STORAGE_ENTRY, SafeLocalStorage } from '../utils/localStorage'; import { memoizeListenerCallback } from '../utils/memoizeStateListenerCallback'; import { RedirectUrls } from '../utils/redirectUrls'; import { AuthCookieService } from './auth/AuthCookieService'; From 9bad073c4e9880abf6cf9f0c1cf1563ad8c462d9 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Wed, 26 Mar 2025 22:17:29 +0200 Subject: [PATCH 6/7] update bundle watch --- packages/clerk-js/bundlewatch.config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index 6ab6375095d..ca1c4c99940 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -1,7 +1,7 @@ { "files": [ - { "path": "./dist/clerk.js", "maxSize": "581.5kB" }, - { "path": "./dist/clerk.browser.js", "maxSize": "79.30kB" }, + { "path": "./dist/clerk.js", "maxSize": "581.65kB" }, + { "path": "./dist/clerk.browser.js", "maxSize": "79.6kB" }, { "path": "./dist/clerk.headless*.js", "maxSize": "55KB" }, { "path": "./dist/ui-common*.js", "maxSize": "96KB" }, { "path": "./dist/vendors*.js", "maxSize": "30KB" }, From 24c830d89f285058d1ad1f377c9b00466a892f87 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Wed, 26 Mar 2025 23:07:34 +0200 Subject: [PATCH 7/7] update logic --- .../src/utils/__tests__/localStorage.test.ts | 169 ++++++++++++++++++ packages/clerk-js/src/utils/localStorage.ts | 13 +- 2 files changed, 175 insertions(+), 7 deletions(-) create mode 100644 packages/clerk-js/src/utils/__tests__/localStorage.test.ts diff --git a/packages/clerk-js/src/utils/__tests__/localStorage.test.ts b/packages/clerk-js/src/utils/__tests__/localStorage.test.ts new file mode 100644 index 00000000000..792f0acfa7f --- /dev/null +++ b/packages/clerk-js/src/utils/__tests__/localStorage.test.ts @@ -0,0 +1,169 @@ +import { SafeLocalStorage } from '../localStorage'; + +describe('SafeLocalStorage', () => { + let mockStorage: { [key: string]: string } = {}; + + beforeEach(() => { + mockStorage = {}; + + // Create a mock implementation of localStorage + const localStorageMock = { + getItem: (key: string) => mockStorage[key] || null, + setItem: (key: string, value: string) => { + mockStorage[key] = value; + }, + removeItem: (key: string) => { + delete mockStorage[key]; + }, + }; + + // Replace window.localStorage with our mock + Object.defineProperty(window, 'localStorage', { + value: localStorageMock, + writable: true, + }); + }); + + afterEach(() => { + mockStorage = {}; + jest.restoreAllMocks(); + }); + + describe('setItem', () => { + it('stores value with clerk prefix', () => { + SafeLocalStorage.setItem('test', 'value'); + expect(mockStorage['__clerk_test']).toBeDefined(); + const parsed = JSON.parse(mockStorage['__clerk_test']); + expect(parsed.value).toBe('value'); + }); + + it('handles localStorage errors gracefully', () => { + Object.defineProperty(window, 'localStorage', { + value: { + setItem: () => { + throw new Error('Storage full'); + }, + }, + writable: true, + }); + + expect(() => { + SafeLocalStorage.setItem('test', 'value'); + }).not.toThrow(); + }); + + it('sets expiration when provided', () => { + jest.useFakeTimers(); + const now = Date.now(); + SafeLocalStorage.setItem('test', 'value', 1000); + + const stored = JSON.parse(mockStorage['__clerk_test']); + expect(stored.exp).toBe(now + 1000); + jest.useRealTimers(); + }); + + it('stores complex objects correctly', () => { + const complexObject = { foo: 'bar', nested: { value: 42 } }; + SafeLocalStorage.setItem('complex', complexObject); + const stored = JSON.parse(mockStorage['__clerk_complex']); + expect(stored.value).toEqual(complexObject); + }); + + it('does not set expiration when not provided', () => { + SafeLocalStorage.setItem('test', 'value'); + const stored = JSON.parse(mockStorage['__clerk_test']); + expect(stored.exp).toBeUndefined(); + }); + }); + + describe('getItem', () => { + it('retrieves stored value', () => { + SafeLocalStorage.setItem('test', 'value'); + expect(SafeLocalStorage.getItem('test', 'default')).toBe('value'); + }); + + it('retrieves stored value when not expired', () => { + SafeLocalStorage.setItem('test', 'value', 1_000); + expect(SafeLocalStorage.getItem('test', 'default')).toBe('value'); + }); + + it('returns default value when key not found', () => { + expect(SafeLocalStorage.getItem('nonexistent', 'default')).toBe('default'); + }); + + it('handles localStorage errors by returning default value', () => { + Object.defineProperty(window, 'localStorage', { + value: { + getItem: () => { + throw new Error('Storage error'); + }, + }, + writable: true, + }); + + expect(SafeLocalStorage.getItem('test', 'default')).toBe('default'); + }); + + it('returns default value and removes item when expired', () => { + jest.useFakeTimers(); + SafeLocalStorage.setItem('test', 'value', 1_000); + + // Advance time beyond expiration + jest.advanceTimersByTime(1_001); + + expect(SafeLocalStorage.getItem('test', 'default')).toBe('default'); + expect(mockStorage['__clerk_test']).toBeUndefined(); + jest.useRealTimers(); + }); + + it('handles malformed JSON data by returning default value', () => { + mockStorage['__clerk_malformed'] = 'not-json-data'; + expect(SafeLocalStorage.getItem('malformed', 'default')).toBe('default'); + }); + + it('handles empty stored value by returning default', () => { + mockStorage['__clerk_empty'] = JSON.stringify({ value: null }); + expect(SafeLocalStorage.getItem('empty', 'default')).toBe('default'); + }); + + it('retrieves complex objects correctly', () => { + const complexObject = { foo: 'bar', nested: { value: 42 } }; + SafeLocalStorage.setItem('complex', complexObject); + expect(SafeLocalStorage.getItem('complex', {})).toEqual(complexObject); + }); + + it('handles edge case with zero as stored value', () => { + SafeLocalStorage.setItem('zero', 0); + expect(SafeLocalStorage.getItem('zero', 1)).toBe(0); + }); + }); + + describe('removeItem', () => { + it('removes item with clerk prefix', () => { + SafeLocalStorage.setItem('test', 'value'); + expect(mockStorage['__clerk_test']).toBeDefined(); + SafeLocalStorage.removeItem('test'); + expect(mockStorage['__clerk_test']).toBeUndefined(); + }); + + it('handles localStorage errors gracefully', () => { + Object.defineProperty(window, 'localStorage', { + value: { + removeItem: () => { + throw new Error('Storage error'); + }, + }, + writable: true, + }); + + expect(() => { + SafeLocalStorage.removeItem('test'); + }).not.toThrow(); + }); + + it('does nothing when removing non-existent item', () => { + SafeLocalStorage.removeItem('nonexistent'); + expect(mockStorage['__clerk_nonexistent']).toBeUndefined(); + }); + }); +}); diff --git a/packages/clerk-js/src/utils/localStorage.ts b/packages/clerk-js/src/utils/localStorage.ts index 8c59b7e70f4..9b517967e36 100644 --- a/packages/clerk-js/src/utils/localStorage.ts +++ b/packages/clerk-js/src/utils/localStorage.ts @@ -1,4 +1,4 @@ -const CLERK_PREFIX = 'clerk_'; +const CLERK_PREFIX = '__clerk_'; export const CLERK_ENVIRONMENT_STORAGE_ENTRY = 'environment'; @@ -8,6 +8,7 @@ interface StorageEntry { } const serialize = JSON.stringify; +const parse = JSON.parse; /** * Safe wrapper around localStorage that automatically prefixes keys with 'clerk_' @@ -38,12 +39,10 @@ export class SafeLocalStorage { try { const item = window.localStorage.getItem(this._key(key)); if (!item) return defaultValue; + const entry = parse(item) as unknown as StorageEntry | undefined | null; - const entry = serialize(item) as unknown as StorageEntry; - - // Handle legacy items that weren't stored with expiration metadata - if (!('value' in entry)) { - return entry as T; + if (!entry) { + return defaultValue; } if (this.isExpired(entry)) { @@ -51,7 +50,7 @@ export class SafeLocalStorage { return defaultValue; } - return entry.value; + return entry?.value ?? defaultValue; } catch { return defaultValue; }