Skip to content

Commit 826e195

Browse files
committed
fix(auth): use Symbols for callback IDs to resolve Next.js 16 compatibility
1 parent 0580949 commit 826e195

File tree

3 files changed

+19
-42
lines changed

3 files changed

+19
-42
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: 11 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,46 +9,21 @@ export function expiresAt(expiresIn: number) {
99
return timeNow + expiresIn
1010
}
1111

12-
// Counter for SSR-safe UUID generation
13-
let ssrUuidCounter = 0
14-
1512
/**
16-
* Generates a UUID v4 string.
13+
* Generates a unique identifier for internal callback subscriptions.
1714
*
18-
* This function is SSR-aware to handle Next.js 16 pre-rendering constraints:
19-
* - In browsers: Uses crypto.randomUUID() or crypto.getRandomValues() for cryptographic randomness
20-
* - During SSR: Uses a deterministic fallback (timestamp + counter)
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
2121
*
22-
* Note: The SSR fallback is safe because:
23-
* 1. UUIDs from this function are only used for internal subscription IDs, not security-critical operations
24-
* 2. During SSR/pre-rendering, auth callbacks don't actually fire
25-
* 3. Once in the browser, proper cryptographic APIs are always used
22+
* Note: This function is only used for internal subscription management,
23+
* not for security-critical operations like session tokens.
2624
*/
27-
export function uuid() {
28-
// Modern browsers and Node.js 19+ - use native crypto.randomUUID()
29-
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
30-
return crypto.randomUUID()
31-
}
32-
33-
// Browsers with crypto.getRandomValues() support
34-
if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
35-
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
36-
const array = new Uint8Array(1)
37-
crypto.getRandomValues(array)
38-
const r = array[0] % 16
39-
const v = c == 'x' ? r : (r & 0x3) | 0x8
40-
return v.toString(16)
41-
})
42-
}
43-
44-
// SSR/pre-render fallback - deterministic but unique within session
45-
// This only generates subscription IDs during pre-render; real UUIDs use crypto in browser
46-
const timestamp = Date.now()
47-
const counter = ssrUuidCounter++
48-
const random1 = Math.floor(timestamp / 1000000) % 10000
49-
const random2 = (timestamp % 1000000) % 10000
50-
51-
return `ssr-${timestamp.toString(16)}-${counter.toString(16)}-${random1.toString(16)}-${random2.toString(16)}`
25+
export function generateCallbackId(): symbol {
26+
return Symbol('auth-callback')
5227
}
5328

5429
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
*/

0 commit comments

Comments
 (0)