Skip to content

Commit e9e44a3

Browse files
authored
fix(auth): use Symbols for callback IDs to resolve Next.js 16 compatibility (#1847)
Thanks to @7ttp and @BOXNYC
1 parent 5a3820d commit e9e44a3

File tree

4 files changed

+63
-12
lines changed

4 files changed

+63
-12
lines changed

packages/core/auth-js/src/GoTrueClient.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
decodeJWT,
3636
deepClone,
3737
Deferred,
38+
generateCallbackId,
3839
getAlgorithm,
3940
getCodeChallengeAndMethod,
4041
getItemAsync,
@@ -48,7 +49,6 @@ import {
4849
sleep,
4950
supportsLocalStorage,
5051
userNotAvailableProxy,
51-
uuid,
5252
validateExp,
5353
} from './lib/helpers'
5454
import { memoryLocalStorageAdapter } from './lib/local-storage'
@@ -241,7 +241,7 @@ export default class GoTrueClient {
241241
*/
242242
protected userStorage: SupportedStorage | null = null
243243
protected memoryStorage: { [key: string]: string } | null = null
244-
protected stateChangeEmitters: Map<string, Subscription> = new Map()
244+
protected stateChangeEmitters: Map<string | symbol, Subscription> = new Map()
245245
protected autoRefreshTicker: ReturnType<typeof setInterval> | null = null
246246
protected visibilityChangedCallback: (() => Promise<any>) | null = null
247247
protected refreshingDeferred: Deferred<CallRefreshTokenResult> | null = null
@@ -2161,7 +2161,7 @@ export default class GoTrueClient {
21612161
): {
21622162
data: { subscription: Subscription }
21632163
} {
2164-
const id: string = uuid()
2164+
const id: string | symbol = generateCallbackId()
21652165
const subscription: Subscription = {
21662166
id,
21672167
callback,
@@ -2186,7 +2186,7 @@ export default class GoTrueClient {
21862186
return { data: { subscription } }
21872187
}
21882188

2189-
private async _emitInitialSession(id: string): Promise<void> {
2189+
private async _emitInitialSession(id: string | symbol): Promise<void> {
21902190
return await this._useSession(async (result) => {
21912191
try {
21922192
const {

packages/core/auth-js/src/lib/helpers.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,21 @@ export function expiresAt(expiresIn: number) {
99
return timeNow + expiresIn
1010
}
1111

12-
export function uuid() {
13-
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
14-
const r = (Math.random() * 16) | 0,
15-
v = c == 'x' ? r : (r & 0x3) | 0x8
16-
return v.toString(16)
17-
})
12+
/**
13+
* Generates a unique identifier for internal callback subscriptions.
14+
*
15+
* This function uses JavaScript Symbols to create guaranteed-unique identifiers
16+
* for auth state change callbacks. Symbols are ideal for this use case because:
17+
* - They are guaranteed unique by the JavaScript runtime
18+
* - They work in all environments (browser, SSR, Node.js)
19+
* - They avoid issues with Next.js 16 deterministic rendering requirements
20+
* - They are perfect for internal, non-serializable identifiers
21+
*
22+
* Note: This function is only used for internal subscription management,
23+
* not for security-critical operations like session tokens.
24+
*/
25+
export function generateCallbackId(): symbol {
26+
return Symbol('auth-callback')
1827
}
1928

2029
export const isBrowser = () => typeof window !== 'undefined' && typeof document !== 'undefined'

packages/core/auth-js/src/lib/types.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -503,9 +503,11 @@ export interface AdminUserAttributes extends Omit<UserAttributes, 'data'> {
503503

504504
export interface Subscription {
505505
/**
506-
* The subscriber UUID. This will be set by the client.
506+
* A unique identifier for this subscription, set by the client.
507+
* This is an internal identifier used for managing callbacks and should not be
508+
* relied upon by application code. Use the unsubscribe() method to remove listeners.
507509
*/
508-
id: string
510+
id: string | symbol
509511
/**
510512
* The function to call every time there is an event. eg: (eventName) => {}
511513
*/

packages/core/auth-js/test/helpers.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,53 @@
11
import { AuthInvalidJwtError } from '../src'
22
import {
33
decodeJWT,
4+
generateCallbackId,
45
getAlgorithm,
56
parseParametersFromURL,
67
parseResponseAPIVersion,
78
getCodeChallengeAndMethod,
89
validateUUID,
910
} from '../src/lib/helpers'
1011

12+
describe('generateCallbackId', () => {
13+
it('should return a Symbol', () => {
14+
const id = generateCallbackId()
15+
expect(typeof id).toBe('symbol')
16+
})
17+
18+
it('should return unique Symbols on each call', () => {
19+
const id1 = generateCallbackId()
20+
const id2 = generateCallbackId()
21+
const id3 = generateCallbackId()
22+
23+
expect(id1).not.toBe(id2)
24+
expect(id2).not.toBe(id3)
25+
expect(id1).not.toBe(id3)
26+
})
27+
28+
it('should work as Map keys', () => {
29+
const id1 = generateCallbackId()
30+
const id2 = generateCallbackId()
31+
32+
const map = new Map()
33+
map.set(id1, 'callback1')
34+
map.set(id2, 'callback2')
35+
36+
expect(map.get(id1)).toBe('callback1')
37+
expect(map.get(id2)).toBe('callback2')
38+
expect(map.size).toBe(2)
39+
40+
map.delete(id1)
41+
expect(map.has(id1)).toBe(false)
42+
expect(map.has(id2)).toBe(true)
43+
})
44+
45+
it('should have a description for debugging', () => {
46+
const id = generateCallbackId()
47+
expect(id.toString()).toBe('Symbol(auth-callback)')
48+
})
49+
})
50+
1151
describe('parseParametersFromURL', () => {
1252
it('should parse parameters from a URL with query params only', () => {
1353
const url = new URL('https://supabase.com')

0 commit comments

Comments
 (0)