Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions packages/core/auth-js/src/GoTrueClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
decodeJWT,
deepClone,
Deferred,
generateCallbackId,
getAlgorithm,
getCodeChallengeAndMethod,
getItemAsync,
Expand All @@ -48,7 +49,6 @@ import {
sleep,
supportsLocalStorage,
userNotAvailableProxy,
uuid,
validateExp,
} from './lib/helpers'
import { memoryLocalStorageAdapter } from './lib/local-storage'
Expand Down Expand Up @@ -241,7 +241,7 @@ export default class GoTrueClient {
*/
protected userStorage: SupportedStorage | null = null
protected memoryStorage: { [key: string]: string } | null = null
protected stateChangeEmitters: Map<string, Subscription> = new Map()
protected stateChangeEmitters: Map<string | symbol, Subscription> = new Map()
protected autoRefreshTicker: ReturnType<typeof setInterval> | null = null
protected visibilityChangedCallback: (() => Promise<any>) | null = null
protected refreshingDeferred: Deferred<CallRefreshTokenResult> | null = null
Expand Down Expand Up @@ -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,
Expand All @@ -2186,7 +2186,7 @@ export default class GoTrueClient {
return { data: { subscription } }
}

private async _emitInitialSession(id: string): Promise<void> {
private async _emitInitialSession(id: string | symbol): Promise<void> {
return await this._useSession(async (result) => {
try {
const {
Expand Down
21 changes: 15 additions & 6 deletions packages/core/auth-js/src/lib/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
6 changes: 4 additions & 2 deletions packages/core/auth-js/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -503,9 +503,11 @@ export interface AdminUserAttributes extends Omit<UserAttributes, 'data'> {

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) => {}
*/
Expand Down
40 changes: 40 additions & 0 deletions packages/core/auth-js/test/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,53 @@
import { AuthInvalidJwtError } from '../src'
import {
decodeJWT,
generateCallbackId,
getAlgorithm,
parseParametersFromURL,
parseResponseAPIVersion,
getCodeChallengeAndMethod,
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')
Expand Down