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 132839ab1..659e82d65 100644 --- a/packages/core/auth-js/src/lib/helpers.ts +++ b/packages/core/auth-js/src/lib/helpers.ts @@ -9,12 +9,21 @@ export function expiresAt(expiresIn: number) { return timeNow + expiresIn } -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) - }) +/** + * Generates a unique identifier for internal callback subscriptions. + * + * 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: This function is only used for internal subscription management, + * not for security-critical operations like session tokens. + */ +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) => {} */ 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')