diff --git a/src/lib/ai.ts b/src/lib/ai.ts index 3b1866f..a325386 100644 --- a/src/lib/ai.ts +++ b/src/lib/ai.ts @@ -1,6 +1,7 @@ import { decryptMessage, encryptMessage } from "./encryption"; import { getAttestation } from "./getAttestation"; import * as api from "./api"; +import { getStorage } from "./storage"; export interface CustomFetchOptions { apiKey?: string; // Optional API key to use instead of JWT token @@ -18,7 +19,7 @@ export function createCustomFetch( } // Otherwise, use the standard JWT token - const currentAccessToken = window.localStorage.getItem("access_token"); + const currentAccessToken = getStorage().persistent.getItem("access_token"); if (!currentAccessToken) { throw new Error("No access token or API key available"); } diff --git a/src/lib/api.ts b/src/lib/api.ts index a74d573..b1751c2 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -2,6 +2,7 @@ import { encode } from "@stablelib/base64"; import { authenticatedApiCall, encryptedApiCall, openAiAuthenticatedApiCall } from "./encryptedApi"; import type { Model } from "openai/resources/models.js"; import { getConfig } from "./config"; +import { getStorage } from "./storage"; export function getApiUrl(): string { return getConfig().apiUrl; @@ -50,8 +51,8 @@ export async function fetchLogin( ); // Store tokens automatically - window.localStorage.setItem("access_token", response.access_token); - window.localStorage.setItem("refresh_token", response.refresh_token); + getStorage().persistent.setItem("access_token", response.access_token); + getStorage().persistent.setItem("refresh_token", response.refresh_token); return response; } @@ -68,8 +69,8 @@ export async function fetchGuestLogin( ); // Store tokens automatically - window.localStorage.setItem("access_token", response.access_token); - window.localStorage.setItem("refresh_token", response.refresh_token); + getStorage().persistent.setItem("access_token", response.access_token); + getStorage().persistent.setItem("refresh_token", response.refresh_token); return response; } @@ -99,8 +100,8 @@ export async function fetchSignUp( }); // Store tokens automatically - window.localStorage.setItem("access_token", response.access_token); - window.localStorage.setItem("refresh_token", response.refresh_token); + getStorage().persistent.setItem("access_token", response.access_token); + getStorage().persistent.setItem("refresh_token", response.refresh_token); return response; } @@ -120,14 +121,14 @@ export async function fetchGuestSignUp( }); // Store tokens automatically - window.localStorage.setItem("access_token", response.access_token); - window.localStorage.setItem("refresh_token", response.refresh_token); + getStorage().persistent.setItem("access_token", response.access_token); + getStorage().persistent.setItem("refresh_token", response.refresh_token); return response; } export async function refreshToken(): Promise { - const refresh_token = window.localStorage.getItem("refresh_token"); + const refresh_token = getStorage().persistent.getItem("refresh_token"); if (!refresh_token) throw new Error("No refresh token available"); const refreshData = { refresh_token }; @@ -141,8 +142,8 @@ export async function refreshToken(): Promise { "Failed to refresh token" ); - window.localStorage.setItem("access_token", response.access_token); - window.localStorage.setItem("refresh_token", response.refresh_token); + getStorage().persistent.setItem("access_token", response.access_token); + getStorage().persistent.setItem("refresh_token", response.refresh_token); return response; } catch (error) { console.error("Error refreshing token:", error); @@ -211,7 +212,7 @@ export async function fetchList(): Promise { } export async function fetchLogout(): Promise { - const refresh_token = window.localStorage.getItem("refresh_token"); + const refresh_token = getStorage().persistent.getItem("refresh_token"); if (refresh_token) { try { @@ -222,10 +223,10 @@ export async function fetchLogout(): Promise { } } - localStorage.removeItem("access_token"); - localStorage.removeItem("refresh_token"); - sessionStorage.removeItem("sessionKey"); - sessionStorage.removeItem("sessionId"); + getStorage().persistent.removeItem("access_token"); + getStorage().persistent.removeItem("refresh_token"); + getStorage().session.removeItem("sessionKey"); + getStorage().session.removeItem("sessionId"); } export async function verifyEmail(code: string): Promise { @@ -374,8 +375,8 @@ export async function handleGitHubCallback( ); // Store tokens automatically - window.localStorage.setItem("access_token", response.access_token); - window.localStorage.setItem("refresh_token", response.refresh_token); + getStorage().persistent.setItem("access_token", response.access_token); + getStorage().persistent.setItem("refresh_token", response.refresh_token); return response; } catch (error) { @@ -460,8 +461,8 @@ export async function handleGoogleCallback( ); // Store tokens automatically - window.localStorage.setItem("access_token", response.access_token); - window.localStorage.setItem("refresh_token", response.refresh_token); + getStorage().persistent.setItem("access_token", response.access_token); + getStorage().persistent.setItem("refresh_token", response.refresh_token); return response; } catch (error) { @@ -555,8 +556,8 @@ export async function handleAppleCallback( ); // Store tokens automatically - window.localStorage.setItem("access_token", response.access_token); - window.localStorage.setItem("refresh_token", response.refresh_token); + getStorage().persistent.setItem("access_token", response.access_token); + getStorage().persistent.setItem("refresh_token", response.refresh_token); return response; } catch (error) { @@ -646,8 +647,8 @@ export async function handleAppleNativeSignIn( ); // Store tokens automatically - window.localStorage.setItem("access_token", response.access_token); - window.localStorage.setItem("refresh_token", response.refresh_token); + getStorage().persistent.setItem("access_token", response.access_token); + getStorage().persistent.setItem("refresh_token", response.refresh_token); return response; } catch (error) { diff --git a/src/lib/config.ts b/src/lib/config.ts index ef2c6b0..4fc3ed2 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -2,9 +2,17 @@ * Global configuration for OpenSecret SDK */ +import { setStorageProvider, resetStorage, type StorageProvider } from "./storage"; + export interface OpenSecretConfig { apiUrl: string; clientId: string; + /** + * Storage provider backing access tokens and session state. + * Browser apps can pass the exported `browserStorage` helper; + * non-browser consumers implement the interface against their own store. + */ + storage: StorageProvider; } let config: OpenSecretConfig | null = null; @@ -35,9 +43,16 @@ export function configure(options: OpenSecretConfig): void { throw new Error('OpenSecret SDK requires a non-empty clientId'); } + if (!options.storage) { + throw new Error('OpenSecret SDK requires a storage provider on configure({ storage })'); + } + + setStorageProvider(options.storage); + config = { apiUrl: options.apiUrl.replace(/\/$/, ''), // Remove trailing slash - clientId: options.clientId + clientId: options.clientId, + storage: options.storage }; } @@ -66,4 +81,5 @@ export function isConfigured(): boolean { */ export function resetConfig(): void { config = null; + resetStorage(); } diff --git a/src/lib/developer.tsx b/src/lib/developer.tsx index 3d524f0..4dda3cb 100644 --- a/src/lib/developer.tsx +++ b/src/lib/developer.tsx @@ -12,6 +12,7 @@ import { } from "./attestationForView"; import type { AttestationDocument } from "./attestation"; import { PcrConfig } from "./pcr"; +import { getStorage } from "./storage"; import type { Organization, Project, @@ -523,8 +524,8 @@ export function OpenSecretDeveloper({ }, [apiUrl]); async function fetchDeveloper() { - const access_token = window.localStorage.getItem("access_token"); - const refresh_token = window.localStorage.getItem("refresh_token"); + const access_token = getStorage().persistent.getItem("access_token"); + const refresh_token = getStorage().persistent.getItem("refresh_token"); if (!access_token || !refresh_token) { setAuth({ loading: false, @@ -552,7 +553,7 @@ export function OpenSecretDeveloper({ } const getAttestationDocument = async () => { - const nonce = window.crypto.randomUUID(); + const nonce = globalThis.crypto.randomUUID(); const response = await fetch(`${apiUrl}/attestation/${nonce}`); if (!response.ok) { throw new Error("Failed to fetch attestation document"); @@ -574,8 +575,8 @@ export function OpenSecretDeveloper({ async function signIn(email: string, password: string) { try { const { access_token, refresh_token } = await platformApi.platformLogin(email, password); - window.localStorage.setItem("access_token", access_token); - window.localStorage.setItem("refresh_token", refresh_token); + getStorage().persistent.setItem("access_token", access_token); + getStorage().persistent.setItem("refresh_token", refresh_token); await fetchDeveloper(); return { access_token, refresh_token, id: "", email }; } catch (error) { @@ -592,8 +593,8 @@ export function OpenSecretDeveloper({ invite_code, name ); - window.localStorage.setItem("access_token", access_token); - window.localStorage.setItem("refresh_token", refresh_token); + getStorage().persistent.setItem("access_token", access_token); + getStorage().persistent.setItem("refresh_token", refresh_token); await fetchDeveloper(); return { access_token, refresh_token, id: "", email, name }; } catch (error) { @@ -608,7 +609,7 @@ export function OpenSecretDeveloper({ signUp, refetchDeveloper: fetchDeveloper, signOut: async () => { - const refresh_token = window.localStorage.getItem("refresh_token"); + const refresh_token = getStorage().persistent.getItem("refresh_token"); if (refresh_token) { try { await platformApi.platformLogout(refresh_token); @@ -616,8 +617,8 @@ export function OpenSecretDeveloper({ console.error("Error during logout:", error); } } - localStorage.removeItem("access_token"); - localStorage.removeItem("refresh_token"); + getStorage().persistent.removeItem("access_token"); + getStorage().persistent.removeItem("refresh_token"); setAuth({ loading: false, developer: undefined diff --git a/src/lib/encryptedApi.ts b/src/lib/encryptedApi.ts index ee5298c..a461026 100644 --- a/src/lib/encryptedApi.ts +++ b/src/lib/encryptedApi.ts @@ -3,6 +3,7 @@ import { getAttestation } from "./getAttestation"; import { refreshToken } from "./api"; import { platformRefreshToken } from "./platformApi"; import { apiConfig } from "./apiConfig"; +import { getStorage } from "./storage"; interface EncryptedResponse { encrypted: string; @@ -37,7 +38,7 @@ export async function authenticatedApiCall( } // Always get the latest token from localStorage - const accessToken = window.localStorage.getItem("access_token"); + const accessToken = getStorage().persistent.getItem("access_token"); if (!accessToken) { throw new Error("No access token available"); } diff --git a/src/lib/getAttestation.ts b/src/lib/getAttestation.ts index a8d1921..31869ee 100644 --- a/src/lib/getAttestation.ts +++ b/src/lib/getAttestation.ts @@ -3,6 +3,7 @@ import { keyExchange } from "./api"; import nacl from "tweetnacl"; import { ChaCha20Poly1305 } from "@stablelib/chacha20poly1305"; import { encode, decode } from "@stablelib/base64"; +import { getStorage } from "./storage"; export interface Attestation { sessionKey: Uint8Array | null; @@ -30,8 +31,8 @@ export async function getAttestation( explicitApiUrl?: string ): Promise { // Check if we already have a sessionKey and sessionId in sessionstorage - const sessionKey = sessionStorage.getItem("sessionKey"); - const sessionId = sessionStorage.getItem("sessionId"); + const sessionKey = getStorage().session.getItem("sessionKey"); + const sessionId = getStorage().session.getItem("sessionId"); console.groupCollapsed("Attestation"); @@ -45,7 +46,7 @@ export async function getAttestation( // Need to get a new attestation // (Will use test nonce if provided) - const attestationNonce = window.crypto.randomUUID(); + const attestationNonce = globalThis.crypto.randomUUID(); console.log("Generated attestation nonce:", attestationNonce); const document = await verifyAttestation(attestationNonce, explicitApiUrl); @@ -76,8 +77,8 @@ export async function getAttestation( if (decryptedSessionKey) { console.log("Session key decrypted successfully"); - window.sessionStorage.setItem("sessionKey", encode(decryptedSessionKey)); - window.sessionStorage.setItem("sessionId", session_id); + getStorage().session.setItem("sessionKey", encode(decryptedSessionKey)); + getStorage().session.setItem("sessionId", session_id); return { sessionKey: decryptedSessionKey, sessionId: session_id }; } else { throw new Error("Failed to decrypt session key"); diff --git a/src/lib/index.ts b/src/lib/index.ts index f74da29..67d283f 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -53,6 +53,10 @@ export type { Model } from "openai/resources/models.js"; export { configure, getConfig, isConfigured, resetConfig } from "./config"; export type { OpenSecretConfig } from "./config"; +// Export storage abstraction +export { getStorage, setStorageProvider, resetStorage, browserStorage } from "./storage"; +export type { StorageProvider } from "./storage"; + // Export API configuration export { apiConfig, type ApiContext, type ApiEndpoint } from "./apiConfig"; diff --git a/src/lib/main.tsx b/src/lib/main.tsx index a1418cd..6269c84 100644 --- a/src/lib/main.tsx +++ b/src/lib/main.tsx @@ -14,6 +14,7 @@ import type { AttestationDocument } from "./attestation"; import type { LoginResponse, ThirdPartyTokenResponse, DocumentResponse } from "./api"; import { PcrConfig } from "./pcr"; import { configure } from "./config"; +import { browserStorage, getStorage } from "./storage"; export type OpenSecretAuthState = { loading: boolean; @@ -1027,8 +1028,9 @@ export function OpenSecretProvider({ ); } - // Configure the SDK with the provided values - configure({ apiUrl, clientId }); + // Configure the SDK with the provided values. OpenSecretProvider is a React DOM + // component, so browserStorage is always the correct backing store here. + configure({ apiUrl, clientId, storage: browserStorage }); }, [apiUrl, clientId]); // Create aiCustomFetch when API is configured (supports JWT or API key internally) @@ -1042,8 +1044,8 @@ export function OpenSecretProvider({ }, [apiUrl, apiKey]); async function fetchUser() { - const access_token = window.localStorage.getItem("access_token"); - const refresh_token = window.localStorage.getItem("refresh_token"); + const access_token = getStorage().persistent.getItem("access_token"); + const refresh_token = getStorage().persistent.getItem("refresh_token"); if (!access_token || !refresh_token) { setAuth({ loading: false, @@ -1232,7 +1234,7 @@ export function OpenSecretProvider({ }; const getAttestationDocument = async () => { - const nonce = window.crypto.randomUUID(); + const nonce = globalThis.crypto.randomUUID(); const response = await fetch(`${apiUrl}/attestation/${nonce}`); if (!response.ok) { throw new Error("Failed to fetch attestation document"); diff --git a/src/lib/platformApi.ts b/src/lib/platformApi.ts index 2814f22..b5056a1 100644 --- a/src/lib/platformApi.ts +++ b/src/lib/platformApi.ts @@ -1,4 +1,5 @@ import { encryptedApiCall, authenticatedApiCall } from "./encryptedApi"; +import { getStorage } from "./storage"; // Platform Auth Types export type PlatformLoginResponse = { @@ -182,7 +183,7 @@ export async function platformLogout(refresh_token: string): Promise { * It returns new access and refresh tokens if validation succeeds. */ export async function platformRefreshToken(): Promise { - const refresh_token = window.localStorage.getItem("refresh_token"); + const refresh_token = getStorage().persistent.getItem("refresh_token"); if (!refresh_token) throw new Error("No refresh token available"); const refreshData = { refresh_token }; @@ -196,8 +197,8 @@ export async function platformRefreshToken(): Promise { "Failed to refresh platform token" ); - window.localStorage.setItem("access_token", response.access_token); - window.localStorage.setItem("refresh_token", response.refresh_token); + getStorage().persistent.setItem("access_token", response.access_token); + getStorage().persistent.setItem("refresh_token", response.refresh_token); return response; } catch (error) { console.error("Error refreshing platform token:", error); diff --git a/src/lib/storage.ts b/src/lib/storage.ts new file mode 100644 index 0000000..e56bb56 --- /dev/null +++ b/src/lib/storage.ts @@ -0,0 +1,57 @@ +/** + * Storage abstraction for OpenSecret SDK. + * + * Consumers must provide a StorageProvider via configure({ storage }) + * before any other SDK usage. Browser apps can pass the bundled + * `browserStorage` helper; non-browser consumers (React Native, Node, + * Bun, CLI, MCP) implement the interface against their own backing store. + */ + +export type StorageProvider = { + persistent: { + getItem(key: string): string | null; + setItem(key: string, value: string): void; + removeItem(key: string): void; + }; + session: { + getItem(key: string): string | null; + setItem(key: string, value: string): void; + removeItem(key: string): void; + }; +}; + +/** + * Convenience StorageProvider that maps directly to window.localStorage and + * window.sessionStorage. Uses getters so importing this module in a non-browser + * environment is safe — `window` is only referenced if the provider is actually + * passed to configure(). + */ +export const browserStorage: StorageProvider = { + get persistent() { + return window.localStorage; + }, + get session() { + return window.sessionStorage; + } +}; + +let _provider: StorageProvider | null = null; + +export function setStorageProvider(provider: StorageProvider): void { + _provider = provider; +} + +export function getStorage(): StorageProvider { + if (!_provider) { + throw new Error( + "OpenSecret SDK: no storage provider configured. " + + "Call configure({ storage: ... }) before using the SDK. " + + "Browser apps can pass the bundled `browserStorage` helper." + ); + } + return _provider; +} + +export function resetStorage(): void { + _provider = null; +} diff --git a/src/lib/test/setup.ts b/src/lib/test/setup.ts index fff209e..9b44d3f 100644 --- a/src/lib/test/setup.ts +++ b/src/lib/test/setup.ts @@ -1,4 +1,5 @@ import { configure } from "../config"; +import { browserStorage } from "../storage"; const TEST_API_URL = process.env.VITE_OPEN_SECRET_API_URL; @@ -12,7 +13,8 @@ const TEST_CLIENT_ID = "test-client-id-for-testing"; // Configure the SDK for tests configure({ apiUrl: TEST_API_URL, - clientId: TEST_CLIENT_ID + clientId: TEST_CLIENT_ID, + storage: browserStorage }); export { TEST_API_URL, TEST_CLIENT_ID };