UNPKG

@nhost/nhost-js

Version:

Nhost JavaScript SDK

1 lines 16.5 kB
{"version":3,"file":"session-MPM1dzMp.cjs","names":[],"sources":["../src/session/session.ts","../src/session/storageBackend.ts","../src/session/storage.ts"],"sourcesContent":["import type { Session as AuthSession } from '../auth';\n\n/**\n * Decoded JWT token payload with processed timestamps and Hasura claims\n */\nexport interface DecodedToken {\n /** Token expiration time as Date object */\n exp?: number;\n /** Token issued at time as Date object */\n iat?: number;\n /** Token issuer */\n iss?: string;\n /** Token subject (user ID) */\n sub?: string;\n /** Hasura JWT claims with PostgreSQL arrays converted to JavaScript arrays */\n 'https://hasura.io/jwt/claims'?: Record<string, unknown>;\n /** Any other JWT claims */\n [key: string]: unknown;\n}\n\n/**\n * The enriched session stored and managed by the Nhost SDK client.\n *\n * This is a superset of the raw `Session` returned by the auth API\n * (importable from `@nhost/nhost-js/auth`). In addition to the standard\n * auth fields, it includes a `decodedToken` with the parsed JWT payload,\n * making it easy to inspect Hasura claims, roles, and session variables\n * without manually decoding the access token.\n *\n * This is the type you must use when implementing a custom\n * {@link SessionStorageBackend} for `createServerClient`.\n *\n * @see {@link SessionStorageBackend}\n */\nexport interface StoredSession extends AuthSession {\n /** Decoded JWT token payload with processed timestamps and Hasura claims */\n decodedToken: DecodedToken;\n}\n\n/**\n * @deprecated Use {@link StoredSession} instead. Both the auth module and the\n * session module previously exported a type named `Session`, but they are\n * different: `auth.Session` is the raw API response, while `session.Session`\n * is the enriched client-side session that includes `decodedToken`.\n * `StoredSession` is the unambiguous name for the latter.\n */\nexport type Session = StoredSession;\n\n/**\n * Decodes a base64url-encoded string (RFC 4648 Section 5) to a UTF-8 string.\n *\n * JWTs use base64url encoding, which differs from standard base64 by using\n * `-` and `_` instead of `+` and `/`, and omitting padding. The browser's\n * native `atob()` does not support base64url, so we must handle the conversion.\n */\nconst decodeBase64Url = (input: string): string => {\n // Convert base64url to standard base64\n let base64 = input.replace(/-/g, '+').replace(/_/g, '/');\n const pad = base64.length % 4;\n if (pad) {\n base64 += '='.repeat(4 - pad);\n }\n\n // Use TextDecoder for proper UTF-8 support (atob alone mangles multi-byte characters)\n const binaryString = atob(base64);\n const bytes = Uint8Array.from(binaryString, (c) => c.charCodeAt(0));\n return new TextDecoder().decode(bytes);\n};\n\nexport const decodeUserSession = (accessToken: string): DecodedToken => {\n const s = accessToken.split('.');\n if (s.length !== 3 || !s[1]) {\n throw new Error('Invalid access token format');\n }\n\n const decodedToken = JSON.parse(decodeBase64Url(s[1])) as Record<\n string,\n unknown\n >;\n\n // Convert iat and exp to Date objects\n const iat =\n typeof decodedToken['iat'] === 'number'\n ? decodedToken['iat'] * 1000 // Convert seconds to milliseconds\n : undefined;\n const exp =\n typeof decodedToken['exp'] === 'number'\n ? decodedToken['exp'] * 1000 // Convert seconds to milliseconds\n : undefined;\n\n // Process Hasura claims - dynamically convert PostgreSQL array notation to arrays\n const hasuraClaims = decodedToken['https://hasura.io/jwt/claims'] as\n | Record<string, unknown>\n | undefined;\n const processedClaims = hasuraClaims\n ? Object.entries(hasuraClaims).reduce(\n (acc, [key, value]) => {\n if (typeof value === 'string' && isPostgresArray(value)) {\n acc[key] = parsePostgresArray(value);\n } else {\n acc[key] = value;\n }\n return acc;\n },\n {} as Record<string, unknown>,\n )\n : undefined;\n\n return {\n ...decodedToken,\n iat,\n exp,\n 'https://hasura.io/jwt/claims': processedClaims,\n };\n};\n\nconst isPostgresArray = (value: string): boolean => {\n return value.startsWith('{') && value.endsWith('}');\n};\n\nconst parsePostgresArray = (value: string): string[] => {\n if (!value || value === '{}') return [];\n // Remove curly braces and split by comma, handling quoted values\n return value\n .slice(1, -1)\n .split(',')\n .map((item) => item.trim().replace(/^\"(.*)\"$/, '$1'));\n};\n","/**\n * Storage implementations for session persistence in different environments.\n *\n * This module provides different storage adapters for persisting authentication sessions\n * across page reloads and browser sessions.\n */\n\nimport type { StoredSession } from './session';\n\n/**\n * Session storage interface for session persistence.\n * This interface can be implemented to provide custom storage solutions.\n *\n * **Important:** The methods here operate on {@link StoredSession}, which is\n * the enriched client-side session managed by the Nhost SDK. It extends the\n * raw `Session` type from `@nhost/nhost-js/auth` by adding a `decodedToken`\n * field with the parsed JWT payload. Do **not** use `auth.Session` here —\n * it is missing `decodedToken` and will cause a TypeScript error.\n *\n * @example\n * ```ts\n * import { type StoredSession, type SessionStorageBackend } from '@nhost/nhost-js/session';\n *\n * class MyCustomStorage implements SessionStorageBackend {\n * get(): StoredSession | null { ... }\n * set(value: StoredSession): void { ... }\n * remove(): void { ... }\n * }\n * ```\n */\nexport interface SessionStorageBackend {\n /**\n * Get the current session from storage\n * @returns The stored session or null if not found\n */\n get(): StoredSession | null;\n\n /**\n * Set the session in storage\n * @param value - The session to store\n */\n set(value: StoredSession): void;\n\n /**\n * Remove the session from storage\n */\n remove(): void;\n}\n\n/**\n * Default storage key used for storing the Nhost session\n */\nexport const DEFAULT_SESSION_KEY = 'nhostSession';\n\n/**\n * Browser localStorage implementation of StorageInterface.\n * Persists the session across page reloads and browser restarts.\n */\nexport class LocalStorage implements SessionStorageBackend {\n private readonly storageKey: string;\n\n /**\n * Creates a new LocalStorage instance\n * @param options - Configuration options\n * @param options.storageKey - The key to use in localStorage (defaults to \"nhostSession\")\n */\n constructor(options?: { storageKey?: string }) {\n this.storageKey = options?.storageKey || DEFAULT_SESSION_KEY;\n }\n\n /**\n * Gets the session from localStorage\n * @returns The stored session or null if not found\n */\n get(): StoredSession | null {\n try {\n const value = window.localStorage.getItem(this.storageKey);\n return value ? (JSON.parse(value) as StoredSession) : null;\n } catch {\n this.remove();\n return null;\n }\n }\n\n /**\n * Sets the session in localStorage\n * @param value - The session to store\n */\n set(value: StoredSession): void {\n window.localStorage.setItem(this.storageKey, JSON.stringify(value));\n }\n\n /**\n * Removes the session from localStorage\n */\n remove(): void {\n window.localStorage.removeItem(this.storageKey);\n }\n}\n\n/**\n * In-memory storage implementation for non-browser environments or when\n * persistent storage is not available or desirable.\n */\nexport class MemoryStorage implements SessionStorageBackend {\n private session: StoredSession | null = null;\n\n /**\n * Gets the session from memory\n * @returns The stored session or null if not set\n */\n get(): StoredSession | null {\n return this.session;\n }\n\n /**\n * Sets the session in memory\n * @param value - The session to store\n */\n set(value: StoredSession): void {\n this.session = value;\n }\n\n /**\n * Clears the session from memory\n */\n remove(): void {\n this.session = null;\n }\n}\n\n/**\n * Cookie-based storage implementation.\n * This storage uses web browser cookies to store the session so it's not\n * available in server-side environments. It is useful though for synchronizing\n * sessions between client and server environments.\n */\nexport class CookieStorage implements SessionStorageBackend {\n private readonly cookieName: string;\n private readonly expirationDays: number;\n private readonly secure: boolean;\n private readonly sameSite: 'strict' | 'lax' | 'none';\n\n /**\n * Creates a new CookieStorage instance\n * @param options - Configuration options\n * @param options.cookieName - Name of the cookie to use (defaults to \"nhostSession\")\n * @param options.expirationDays - Number of days until the cookie expires (defaults to 30)\n * @param options.secure - Whether to set the Secure flag on the cookie (defaults to true)\n * @param options.sameSite - SameSite policy for the cookie (defaults to \"lax\")\n */\n constructor(options?: {\n cookieName?: string;\n expirationDays?: number;\n secure?: boolean;\n sameSite?: 'strict' | 'lax' | 'none';\n }) {\n this.cookieName = options?.cookieName || DEFAULT_SESSION_KEY;\n this.expirationDays = options?.expirationDays ?? 30;\n this.secure = options?.secure ?? true;\n this.sameSite = options?.sameSite || 'lax';\n }\n\n /**\n * Gets the session from cookies\n * @returns The stored session or null if not found\n */\n get(): StoredSession | null {\n const cookies = document.cookie.split(';');\n for (const cookie of cookies) {\n const [name, value] = cookie.trim().split('=');\n if (name === this.cookieName) {\n try {\n return JSON.parse(decodeURIComponent(value || '')) as StoredSession;\n } catch {\n this.remove();\n return null;\n }\n }\n }\n return null;\n }\n\n /**\n * Sets the session in a cookie\n * @param value - The session to store\n */\n set(value: StoredSession): void {\n const expires = new Date();\n expires.setTime(\n expires.getTime() + this.expirationDays * 24 * 60 * 60 * 1000,\n );\n\n const cookieValue = encodeURIComponent(JSON.stringify(value));\n const cookieString = `${this.cookieName}=${cookieValue}; expires=${expires.toUTCString()}; path=/; ${this.secure ? 'secure; ' : ''}SameSite=${this.sameSite}`;\n\n // biome-ignore lint/suspicious/noDocumentCookie: this is unnecessary\n document.cookie = cookieString;\n }\n\n /**\n * Removes the session cookie\n */\n remove(): void {\n // biome-ignore lint/suspicious/noDocumentCookie: this is unnecessary\n document.cookie = `${this.cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; ${this.secure ? 'secure; ' : ''}SameSite=${this.sameSite}`;\n }\n}\n","/**\n * Storage implementations for session persistence in different environments.\n *\n * This module provides different storage adapters for persisting authentication sessions\n * across page reloads and browser sessions.\n */\n\nimport type { Session as AuthSession } from '../auth';\nimport { decodeUserSession, type StoredSession } from './session';\nimport {\n LocalStorage,\n MemoryStorage,\n type SessionStorageBackend,\n} from './storageBackend';\n\n/**\n * Callback function type for session change subscriptions\n */\nexport type SessionChangeCallback = (session: StoredSession | null) => void;\n\n/**\n * A wrapper around any SessionStorageInterface implementation that adds\n * the ability to subscribe to session changes.\n */\nexport class SessionStorage {\n private readonly storage: SessionStorageBackend;\n private subscribers = new Set<SessionChangeCallback>();\n\n /**\n * Creates a new SessionStorage instance\n * @param storage - The underlying storage implementation to use\n */\n constructor(storage: SessionStorageBackend) {\n this.storage = storage;\n }\n\n /**\n * Gets the session from the underlying storage\n * @returns The stored session or null if not found\n */\n get(): StoredSession | null {\n return this.storage.get();\n }\n\n /**\n * Sets the session in the underlying storage and notifies subscribers\n * @param value - The session to store\n */\n set(value: AuthSession): void {\n const decodedToken = decodeUserSession(value.accessToken);\n const decodedSession = {\n ...value,\n decodedToken: decodedToken,\n };\n\n this.storage.set(decodedSession);\n this.notifySubscribers(decodedSession);\n }\n\n /**\n * Removes the session from the underlying storage and notifies subscribers\n */\n remove(): void {\n this.storage.remove();\n this.notifySubscribers(null);\n }\n\n /**\n * Subscribe to session changes\n * @param callback - Function that will be called when the session changes\n * @returns An unsubscribe function to remove this subscription\n */\n onChange(callback: SessionChangeCallback) {\n this.subscribers.add(callback);\n\n return () => {\n this.subscribers.delete(callback);\n };\n }\n\n /**\n * Notify all subscribers of a session change\n * @param session - The new session value or null if removed\n */\n private notifySubscribers(session: StoredSession | null): void {\n for (const subscriber of this.subscribers) {\n try {\n subscriber(session);\n } catch (error) {\n console.error('Error notifying subscriber:', error);\n }\n }\n }\n}\n\n/**\n * Detects the best available storage implementation for the current environment.\n *\n * The detection process follows this order:\n * 1. Try to use localStorage if we're in a browser environment\n * 2. Fall back to in-memory storage if localStorage isn't available\n *\n * @returns The best available storage implementation as a SessionStorageBackend\n */\nexport const detectStorage = (): SessionStorageBackend => {\n if (typeof window !== 'undefined') {\n return new LocalStorage();\n }\n return new MemoryStorage();\n};\n"],"mappings":"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"}