From 0580949db60aec9a10871a237802035925d591e0 Mon Sep 17 00:00:00 2001 From: Katerina Skroumpelou Date: Mon, 10 Nov 2025 19:16:27 +0200 Subject: [PATCH 1/3] fix(auth): make uuid() SSR-aware for Next.js 16 compatibility --- packages/core/auth-js/src/lib/helpers.ts | 44 +++++++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/packages/core/auth-js/src/lib/helpers.ts b/packages/core/auth-js/src/lib/helpers.ts index 132839ab1..1cffe53cb 100644 --- a/packages/core/auth-js/src/lib/helpers.ts +++ b/packages/core/auth-js/src/lib/helpers.ts @@ -9,12 +9,46 @@ export function expiresAt(expiresIn: number) { return timeNow + expiresIn } +// Counter for SSR-safe UUID generation +let ssrUuidCounter = 0 + +/** + * Generates a UUID v4 string. + * + * This function is SSR-aware to handle Next.js 16 pre-rendering constraints: + * - In browsers: Uses crypto.randomUUID() or crypto.getRandomValues() for cryptographic randomness + * - During SSR: Uses a deterministic fallback (timestamp + counter) + * + * Note: The SSR fallback is safe because: + * 1. UUIDs from this function are only used for internal subscription IDs, not security-critical operations + * 2. During SSR/pre-rendering, auth callbacks don't actually fire + * 3. Once in the browser, proper cryptographic APIs are always used + */ export function uuid() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { - const r = (Math.random() * 16) | 0, - v = c == 'x' ? r : (r & 0x3) | 0x8 - return v.toString(16) - }) + // Modern browsers and Node.js 19+ - use native crypto.randomUUID() + if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { + return crypto.randomUUID() + } + + // Browsers with crypto.getRandomValues() support + if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + const array = new Uint8Array(1) + crypto.getRandomValues(array) + const r = array[0] % 16 + const v = c == 'x' ? r : (r & 0x3) | 0x8 + return v.toString(16) + }) + } + + // SSR/pre-render fallback - deterministic but unique within session + // This only generates subscription IDs during pre-render; real UUIDs use crypto in browser + const timestamp = Date.now() + const counter = ssrUuidCounter++ + const random1 = Math.floor(timestamp / 1000000) % 10000 + const random2 = (timestamp % 1000000) % 10000 + + return `ssr-${timestamp.toString(16)}-${counter.toString(16)}-${random1.toString(16)}-${random2.toString(16)}` } export const isBrowser = () => typeof window !== 'undefined' && typeof document !== 'undefined' From 826e1956c66903f039c0eef18a2b0de5eca4668c Mon Sep 17 00:00:00 2001 From: Katerina Skroumpelou Date: Tue, 11 Nov 2025 11:48:51 +0200 Subject: [PATCH 2/3] fix(auth): use Symbols for callback IDs to resolve Next.js 16 compatibility --- packages/core/auth-js/src/GoTrueClient.ts | 8 ++-- packages/core/auth-js/src/lib/helpers.ts | 47 ++++++----------------- packages/core/auth-js/src/lib/types.ts | 6 ++- 3 files changed, 19 insertions(+), 42 deletions(-) diff --git a/packages/core/auth-js/src/GoTrueClient.ts b/packages/core/auth-js/src/GoTrueClient.ts index a62f6bd30..42bbbcfef 100644 --- a/packages/core/auth-js/src/GoTrueClient.ts +++ b/packages/core/auth-js/src/GoTrueClient.ts @@ -35,6 +35,7 @@ import { decodeJWT, deepClone, Deferred, + generateCallbackId, getAlgorithm, getCodeChallengeAndMethod, getItemAsync, @@ -48,7 +49,6 @@ import { sleep, supportsLocalStorage, userNotAvailableProxy, - uuid, validateExp, } from './lib/helpers' import { memoryLocalStorageAdapter } from './lib/local-storage' @@ -241,7 +241,7 @@ export default class GoTrueClient { */ protected userStorage: SupportedStorage | null = null protected memoryStorage: { [key: string]: string } | null = null - protected stateChangeEmitters: Map = new Map() + protected stateChangeEmitters: Map = new Map() protected autoRefreshTicker: ReturnType | null = null protected visibilityChangedCallback: (() => Promise) | null = null protected refreshingDeferred: Deferred | null = null @@ -2161,7 +2161,7 @@ export default class GoTrueClient { ): { data: { subscription: Subscription } } { - const id: string = uuid() + const id: string | symbol = generateCallbackId() const subscription: Subscription = { id, callback, @@ -2186,7 +2186,7 @@ export default class GoTrueClient { return { data: { subscription } } } - private async _emitInitialSession(id: string): Promise { + private async _emitInitialSession(id: string | symbol): Promise { return await this._useSession(async (result) => { try { const { diff --git a/packages/core/auth-js/src/lib/helpers.ts b/packages/core/auth-js/src/lib/helpers.ts index 1cffe53cb..659e82d65 100644 --- a/packages/core/auth-js/src/lib/helpers.ts +++ b/packages/core/auth-js/src/lib/helpers.ts @@ -9,46 +9,21 @@ export function expiresAt(expiresIn: number) { return timeNow + expiresIn } -// Counter for SSR-safe UUID generation -let ssrUuidCounter = 0 - /** - * Generates a UUID v4 string. + * Generates a unique identifier for internal callback subscriptions. * - * This function is SSR-aware to handle Next.js 16 pre-rendering constraints: - * - In browsers: Uses crypto.randomUUID() or crypto.getRandomValues() for cryptographic randomness - * - During SSR: Uses a deterministic fallback (timestamp + counter) + * This function uses JavaScript Symbols to create guaranteed-unique identifiers + * for auth state change callbacks. Symbols are ideal for this use case because: + * - They are guaranteed unique by the JavaScript runtime + * - They work in all environments (browser, SSR, Node.js) + * - They avoid issues with Next.js 16 deterministic rendering requirements + * - They are perfect for internal, non-serializable identifiers * - * Note: The SSR fallback is safe because: - * 1. UUIDs from this function are only used for internal subscription IDs, not security-critical operations - * 2. During SSR/pre-rendering, auth callbacks don't actually fire - * 3. Once in the browser, proper cryptographic APIs are always used + * Note: This function is only used for internal subscription management, + * not for security-critical operations like session tokens. */ -export function uuid() { - // Modern browsers and Node.js 19+ - use native crypto.randomUUID() - if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { - return crypto.randomUUID() - } - - // Browsers with crypto.getRandomValues() support - if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { - const array = new Uint8Array(1) - crypto.getRandomValues(array) - const r = array[0] % 16 - const v = c == 'x' ? r : (r & 0x3) | 0x8 - return v.toString(16) - }) - } - - // SSR/pre-render fallback - deterministic but unique within session - // This only generates subscription IDs during pre-render; real UUIDs use crypto in browser - const timestamp = Date.now() - const counter = ssrUuidCounter++ - const random1 = Math.floor(timestamp / 1000000) % 10000 - const random2 = (timestamp % 1000000) % 10000 - - return `ssr-${timestamp.toString(16)}-${counter.toString(16)}-${random1.toString(16)}-${random2.toString(16)}` +export function generateCallbackId(): symbol { + return Symbol('auth-callback') } export const isBrowser = () => typeof window !== 'undefined' && typeof document !== 'undefined' diff --git a/packages/core/auth-js/src/lib/types.ts b/packages/core/auth-js/src/lib/types.ts index af23147da..3c108cdd4 100644 --- a/packages/core/auth-js/src/lib/types.ts +++ b/packages/core/auth-js/src/lib/types.ts @@ -503,9 +503,11 @@ export interface AdminUserAttributes extends Omit { export interface Subscription { /** - * The subscriber UUID. This will be set by the client. + * A unique identifier for this subscription, set by the client. + * This is an internal identifier used for managing callbacks and should not be + * relied upon by application code. Use the unsubscribe() method to remove listeners. */ - id: string + id: string | symbol /** * The function to call every time there is an event. eg: (eventName) => {} */ From 390ce3c7854c35f6d4e47f579c02e5ce3ed001bf Mon Sep 17 00:00:00 2001 From: Katerina Skroumpelou Date: Tue, 11 Nov 2025 11:58:18 +0200 Subject: [PATCH 3/3] test(auth): added tests for the new subscription id method --- packages/core/auth-js/test/helpers.test.ts | 40 ++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/packages/core/auth-js/test/helpers.test.ts b/packages/core/auth-js/test/helpers.test.ts index e98fee282..7cd035b98 100644 --- a/packages/core/auth-js/test/helpers.test.ts +++ b/packages/core/auth-js/test/helpers.test.ts @@ -1,6 +1,7 @@ import { AuthInvalidJwtError } from '../src' import { decodeJWT, + generateCallbackId, getAlgorithm, parseParametersFromURL, parseResponseAPIVersion, @@ -8,6 +9,45 @@ import { validateUUID, } from '../src/lib/helpers' +describe('generateCallbackId', () => { + it('should return a Symbol', () => { + const id = generateCallbackId() + expect(typeof id).toBe('symbol') + }) + + it('should return unique Symbols on each call', () => { + const id1 = generateCallbackId() + const id2 = generateCallbackId() + const id3 = generateCallbackId() + + expect(id1).not.toBe(id2) + expect(id2).not.toBe(id3) + expect(id1).not.toBe(id3) + }) + + it('should work as Map keys', () => { + const id1 = generateCallbackId() + const id2 = generateCallbackId() + + const map = new Map() + map.set(id1, 'callback1') + map.set(id2, 'callback2') + + expect(map.get(id1)).toBe('callback1') + expect(map.get(id2)).toBe('callback2') + expect(map.size).toBe(2) + + map.delete(id1) + expect(map.has(id1)).toBe(false) + expect(map.has(id2)).toBe(true) + }) + + it('should have a description for debugging', () => { + const id = generateCallbackId() + expect(id.toString()).toBe('Symbol(auth-callback)') + }) +}) + describe('parseParametersFromURL', () => { it('should parse parameters from a URL with query params only', () => { const url = new URL('https://supabase.com')