From 4dd518f68faee0ed5b87da568292591fc0e31ea0 Mon Sep 17 00:00:00 2001 From: Omar Al Matar Date: Wed, 20 May 2026 16:47:28 +0300 Subject: [PATCH 1/7] refactor(auth): remove navigator.locks-based mutex; introduce commit guard + dispose() Removes the `_acquireLock` mutex that wrapped every auth operation and the underlying `navigator.locks` / `processLock` machinery. Replaces it with two lighter primitives that target the specific synchronization needs each operation actually has: - `refreshingDeferred` (already existed) continues to single-flight the refresh path within an instance. - A storage-level commit guard in `_callRefreshToken` re-reads the storage refresh_token between the rotated-tokens response and `_saveSession`. If storage changed under us (e.g. a concurrent `signOut` ran `_removeSession`), the rotated tokens are discarded rather than written back. Returns `AuthRefreshDiscardedError` on the result. Cross-tab refresh races are handled server-side by GoTrue's v1 parent-of-active mechanism at `internal/tokens/service.go:376-385`, so no client-side coordination is needed. New `client.dispose()` tears down the auto-refresh interval, the `visibilitychange` listener, and the BroadcastChannel; clears registered `onAuthStateChange` subscribers. Idempotent. Call from cleanup hooks in React Strict Mode / HMR contexts to prevent stale tickers from outliving the client. The `lock` and `lockAcquireTimeout` constructor options are silently ignored for backwards compatibility; both are marked `@deprecated`. `navigatorLock`, `processLock`, and the `LockAcquireTimeoutError` family remain exported from `./lib/locks` for one major version. Stale lock references in JSDoc on `getSession`, `onAuthStateChange`, `_challengeAndVerify`, and `_listFactors` updated to match the new model. Test branch only, not for merge yet. See RFC `lockless_auth_coordination`. Resolves (test target): supabase/supabase-js#2013, #936, #2111 --- packages/core/auth-js/src/GoTrueClient.ts | 642 ++++++++---------- packages/core/auth-js/src/lib/errors.ts | 32 + packages/core/auth-js/src/lib/locks.ts | 63 +- packages/core/auth-js/src/lib/types.ts | 45 +- .../auth-js/test/GoTrueClient.browser.test.ts | 65 +- .../core/auth-js/test/GoTrueClient.test.ts | 363 ++++++++-- .../core/supabase-js/src/SupabaseClient.ts | 2 + packages/core/supabase-js/src/lib/types.ts | 20 +- pnpm-lock.yaml | 303 ++++----- 9 files changed, 798 insertions(+), 737 deletions(-) diff --git a/packages/core/auth-js/src/GoTrueClient.ts b/packages/core/auth-js/src/GoTrueClient.ts index 7e9c9f816c..41bc857758 100644 --- a/packages/core/auth-js/src/GoTrueClient.ts +++ b/packages/core/auth-js/src/GoTrueClient.ts @@ -16,11 +16,13 @@ import { AuthInvalidTokenResponseError, AuthPKCECodeVerifierMissingError, AuthPKCEGrantCodeExchangeError, + AuthRefreshDiscardedError, AuthSessionMissingError, AuthUnknownError, isAuthApiError, isAuthError, isAuthImplicitGrantRedirectError, + isAuthRefreshDiscardedError, isAuthRetryableFetchError, isAuthSessionMissingError, } from './lib/errors' @@ -54,7 +56,6 @@ import { validateExp, } from './lib/helpers' import { memoryLocalStorageAdapter } from './lib/local-storage' -import { LockAcquireTimeoutError, navigatorLock } from './lib/locks' import { polyfillGlobalThis } from './lib/polyfills' import { version } from './lib/version' @@ -91,7 +92,6 @@ import type { JWK, JwtHeader, JwtPayload, - LockFunc, MFAChallengeAndVerifyParams, MFAChallengeParams, MFAChallengePhoneParams, @@ -183,7 +183,7 @@ polyfillGlobalThis() // Make "globalThis" available const DEFAULT_OPTIONS: Omit< Required, - 'fetch' | 'storage' | 'userStorage' | 'lock' + 'fetch' | 'storage' | 'userStorage' | 'lock' | 'lockAcquireTimeout' | 'suppressLockOptionWarning' > = { url: GOTRUE_URL, storageKey: STORAGE_KEY, @@ -195,15 +195,10 @@ const DEFAULT_OPTIONS: Omit< debug: false, hasCustomAuthorizationHeader: false, throwOnError: false, - lockAcquireTimeout: 5000, // 5 seconds skipAutoInitialize: false, experimental: {}, } -async function lockNoOp(name: string, acquireTimeout: number, fn: () => Promise): Promise { - return await fn() -} - /** * Caches JWKS values for all clients created in the same environment. This is * especially useful for shared-memory execution environments such as Vercel's @@ -297,11 +292,7 @@ export default class GoTrueClient { protected hasCustomAuthorizationHeader = false protected suppressGetSessionWarning = false protected fetch: Fetch - protected lock: LockFunc - protected lockAcquired = false - protected pendingInLock: Promise[] = [] protected throwOnError: boolean - protected lockAcquireTimeout: number /** * Opt-in flags for experimental features. Defaults to an empty object. * See `GoTrueClientOptions.experimental`. @@ -371,19 +362,28 @@ export default class GoTrueClient { this.url = settings.url this.headers = settings.headers this.fetch = resolveFetch(settings.fetch) - this.lock = settings.lock || lockNoOp this.detectSessionInUrl = settings.detectSessionInUrl this.flowType = settings.flowType this.hasCustomAuthorizationHeader = settings.hasCustomAuthorizationHeader this.throwOnError = settings.throwOnError - this.lockAcquireTimeout = settings.lockAcquireTimeout - if (settings.lock) { - this.lock = settings.lock - } else if (this.persistSession && isBrowser() && globalThis?.navigator?.locks) { - this.lock = navigatorLock - } else { - this.lock = lockNoOp + // settings.lock and settings.lockAcquireTimeout are typed for backwards + // compatibility and have no effect at runtime. Warn once at construction + // when a non-null `lock` is supplied so callers who relied on a custom + // lock (typically React Native `processLock` or Node multi-process + // setups) discover the change instead of racing silently. Opt-out via + // `suppressLockOptionWarning: true`. + if (settings.lock != null && !settings.suppressLockOptionWarning) { + const lockWarning = + `${this._logPrefix()} The \`lock\` option is accepted for backwards ` + + `compatibility but is not invoked by the client. The auth client ` + + `coordinates refreshes itself and the server resolves cross-instance ` + + `races. Code that depended on a custom lock being called will not run. ` + + `Pass \`suppressLockOptionWarning: true\` to silence this warning.` + console.warn(lockWarning) + if (this.logDebugMessages) { + console.trace(lockWarning) + } } if (!this.jwks) { @@ -518,9 +518,7 @@ export default class GoTrueClient { } this.initializePromise = (async () => { - return await this._acquireLock(this.lockAcquireTimeout, async () => { - return await this._initialize() - }) + return await this._initialize() })() return await this.initializePromise @@ -1405,9 +1403,7 @@ export default class GoTrueClient { async exchangeCodeForSession(authCode: string): Promise { await this.initializePromise - return this._acquireLock(this.lockAcquireTimeout, async () => { - return this._exchangeCodeForSession(authCode) - }) + return this._exchangeCodeForSession(authCode) } /** @@ -2444,9 +2440,7 @@ export default class GoTrueClient { async reauthenticate(): Promise { await this.initializePromise - return await this._acquireLock(this.lockAcquireTimeout, async () => { - return await this._reauthenticate() - }) + return await this._reauthenticate() } private async _reauthenticate(): Promise { @@ -2593,7 +2587,7 @@ export default class GoTrueClient { * - If the session's access token is expired or is about to expire, this method will use the refresh token to refresh the session. * - When using in a browser, or you've called `startAutoRefresh()` in your environment (React Native, etc.) this function always returns a valid access token without refreshing the session itself, as this is done in the background. This function returns very fast. * - **IMPORTANT SECURITY NOTICE:** If using an insecure storage medium, such as cookies or request headers, the user object returned by this function **must not be trusted**. Always verify the JWT using `getClaims()` or your own JWT verification library to securely establish the user's identity and access. You can also use `getUser()` to fetch the user object directly from the Auth server for this purpose. - * - When using in a browser, this function is synchronized across all tabs using the [LockManager](https://developer.mozilla.org/en-US/docs/Web/API/LockManager) API. In other environments make sure you've defined a proper `lock` property, if necessary, to make sure there are no race conditions while the session is being refreshed. + * - Cross-tab refresh races are handled by the GoTrue server (the rotated token from the first tab is returned to subsequent tabs via the parent-of-active mechanism), so no client-side serialization is needed. * * @example Get the session data * ```js @@ -2661,91 +2655,15 @@ export default class GoTrueClient { async getSession() { await this.initializePromise - const result = await this._acquireLock(this.lockAcquireTimeout, async () => { - return this._useSession(async (result) => { - return result - }) + return await this._useSession(async (result) => { + return result }) - - return result } /** - * Acquires a global lock based on the storage key. - */ - private async _acquireLock(acquireTimeout: number, fn: () => Promise): Promise { - this._debug('#_acquireLock', 'begin', acquireTimeout) - - try { - if (this.lockAcquired) { - const last = this.pendingInLock.length - ? this.pendingInLock[this.pendingInLock.length - 1] - : Promise.resolve() - - const result = (async () => { - await last - return await fn() - })() - - this.pendingInLock.push( - (async () => { - try { - await result - } catch (_e) { - // we just care if it finished - } - })() - ) - - return result - } - - return await this.lock(`lock:${this.storageKey}`, acquireTimeout, async () => { - this._debug('#_acquireLock', 'lock acquired for storage key', this.storageKey) - - try { - this.lockAcquired = true - - const result = fn() - - this.pendingInLock.push( - (async () => { - try { - await result - } catch (e: any) { - // we just care if it finished - } - })() - ) - - await result - - // keep draining the queue until there's nothing to wait on - while (this.pendingInLock.length) { - const waitOn = [...this.pendingInLock] - - await Promise.all(waitOn) - - this.pendingInLock.splice(0, waitOn.length) - } - - return await result - } finally { - this._debug('#_acquireLock', 'lock released for storage key', this.storageKey) - - this.lockAcquired = false - } - }) - } finally { - this._debug('#_acquireLock', 'end') - } - } - - /** - * Use instead of {@link #getSession} inside the library. It is - * semantically usually what you want, as getting a session involves some - * processing afterwards that requires only one client operating on the - * session at once across multiple tabs or processes. + * Use instead of {@link #getSession} inside the library. Loads the session + * via `__loadSession` (which may trigger a refresh if the access token is + * within the expiry margin) and runs `fn` with the result. */ private async _useSession( fn: ( @@ -2809,10 +2727,6 @@ export default class GoTrueClient { > { this._debug('#__loadSession()', 'begin') - if (!this.lockAcquired) { - this._debug('#__loadSession()', 'used outside of an acquired lock!', new Error().stack) - } - try { let currentSession: Session | null = null @@ -2976,9 +2890,7 @@ export default class GoTrueClient { await this.initializePromise - const result = await this._acquireLock(this.lockAcquireTimeout, async () => { - return await this._getUser() - }) + const result = await this._getUser() if (result.data.user) { this.suppressGetSessionWarning = true @@ -3153,9 +3065,7 @@ export default class GoTrueClient { ): Promise { await this.initializePromise - return await this._acquireLock(this.lockAcquireTimeout, async () => { - return await this._updateUser(attributes, options) - }) + return await this._updateUser(attributes, options) } protected async _updateUser( @@ -3342,9 +3252,7 @@ export default class GoTrueClient { }): Promise { await this.initializePromise - return await this._acquireLock(this.lockAcquireTimeout, async () => { - return await this._setSession(currentSession) - }) + return await this._setSession(currentSession) } protected async _setSession(currentSession: { @@ -3533,9 +3441,7 @@ export default class GoTrueClient { async refreshSession(currentSession?: { refresh_token: string }): Promise { await this.initializePromise - return await this._acquireLock(this.lockAcquireTimeout, async () => { - return await this._refreshSession(currentSession) - }) + return await this._refreshSession(currentSession) } protected async _refreshSession(currentSession?: { @@ -3783,9 +3689,7 @@ export default class GoTrueClient { async signOut(options: SignOut = { scope: 'global' }): Promise<{ error: AuthError | null }> { await this.initializePromise - return await this._acquireLock(this.lockAcquireTimeout, async () => { - return await this._signOut(options) - }) + return await this._signOut(options) } protected async _signOut( @@ -3832,16 +3736,19 @@ export default class GoTrueClient { } /** - * Avoid using an async function inside `onAuthStateChange` as you might end - * up with a deadlock. The callback function runs inside an exclusive lock, - * so calling other Supabase Client APIs that also try to acquire the - * exclusive lock, might cause a deadlock. This behavior is observable across - * tabs. In the next major library version, this behavior will not be supported. - * - * Receive a notification every time an auth event happens. + * Receive a notification every time an auth event happens. Common reentry + * patterns (`getUser`, `setSession`, reading the session from inside a + * handler) complete normally. One hazard remains: calling `refreshSession` + * (or anything that routes through `_callRefreshToken`) from inside a + * `TOKEN_REFRESHED` handler. `refreshingDeferred` resolves only after + * `_notifyAllSubscribers` returns, so the inner refresh dedupes onto the + * outer's unresolved promise and the two wait on each other. * * @param callback A callback function to be invoked when an auth event happens. - * @deprecated Due to the possibility of deadlocks with async functions as callbacks, use the version without an async function. + * + * @deprecated Async callbacks can deadlock when they trigger a nested + * refresh from a `TOKEN_REFRESHED` event. Prefer the sync overload, or move + * refresh-triggering work outside the callback. */ onAuthStateChange(callback: (event: AuthChangeEvent, session: Session | null) => Promise): { data: { subscription: Subscription } @@ -3854,18 +3761,8 @@ export default class GoTrueClient { * - Subscribes to important events occurring on the user's session. * - Use on the frontend/client. It is less useful on the server. * - Events are emitted across tabs to keep your application's UI up-to-date. Some events can fire very frequently, based on the number of tabs open. Use a quick and efficient callback function, and defer or debounce as many operations as you can to be performed outside of the callback. - * - **Important:** A callback can be an `async` function and it runs synchronously during the processing of the changes causing the event. You can easily create a dead-lock by using `await` on a call to another method of the Supabase library. - * - Avoid using `async` functions as callbacks. - * - Limit the number of `await` calls in `async` callbacks. - * - Do not use other Supabase functions in the callback function. If you must, dispatch the functions once the callback has finished executing. Use this as a quick way to achieve this: - * ```js - * supabase.auth.onAuthStateChange((event, session) => { - * setTimeout(async () => { - * // await on other Supabase function here - * // this runs right after the callback has finished - * }, 0) - * }) - * ``` + * - Callbacks can be `async` and can safely call other Supabase auth methods (`getUser`, `setSession`, etc.) from inside the callback. + * - Keep callbacks quick. Events are awaited in order, so a slow callback delays subsequent events to subscribers in this tab. * - Emitted events: * - `INITIAL_SESSION` * - Emitted right after the Supabase client is constructed and the initial session from storage is loaded. @@ -4055,10 +3952,7 @@ export default class GoTrueClient { this.stateChangeEmitters.set(id, subscription) ;(async () => { await this.initializePromise - - await this._acquireLock(this.lockAcquireTimeout, async () => { - this._emitInitialSession(id) - }) + await this._emitInitialSession(id) })() return { data: { subscription } } @@ -4606,15 +4500,22 @@ export default class GoTrueClient { const { error } = await this._callRefreshToken(currentSession.refresh_token) if (error) { - console.error(error) - - if (!isAuthRetryableFetchError(error)) { - this._debug( - debugName, - 'refresh failed with a non-retryable error, removing the session', - error - ) - await this._removeSession() + // AuthRefreshDiscardedError means a concurrent signOut already + // cleared storage and fired SIGNED_OUT. Don't run _removeSession + // again here, or we'll emit a duplicate SIGNED_OUT. + if (isAuthRefreshDiscardedError(error)) { + this._debug(debugName, 'refresh discarded by commit guard', error) + } else { + console.error(error) + + if (!isAuthRetryableFetchError(error)) { + this._debug( + debugName, + 'refresh failed with a non-retryable error, removing the session', + error + ) + await this._removeSession() + } } } } @@ -4674,10 +4575,44 @@ export default class GoTrueClient { try { this.refreshingDeferred = new Deferred() + // Snapshot storage before the fetch. The commit guard discards the + // rotated tokens only when a non-null pre-fetch snapshot changed under + // us — typical case: a concurrent `signOut` ran `_removeSession`, or + // another tab's refresh rewrote the slot. Callers passing + // externally-sourced tokens (SSR cookie handoff, multi-account + // switching, `setSession`/`refreshSession({ refresh_token })`) may + // start from a null snapshot OR from a non-null snapshot whose + // refresh_token differs from the one they're hydrating; in both + // cases the guard fires only when storage was *modified between + // snapshots*, not when the input token disagrees with what's stored. + const storedAtStart = (await getItemAsync(this.storage, this.storageKey)) as Session | null + const { data, error } = await this._refreshAccessToken(refreshToken) if (error) throw error if (!data.session) throw new AuthSessionMissingError() + const storedAfter = (await getItemAsync(this.storage, this.storageKey)) as Session | null + const storageChangedUnderUs = + storedAtStart !== null && + (storedAfter === null || storedAfter.refresh_token !== storedAtStart.refresh_token) + + if (storageChangedUnderUs) { + this._debug( + debugName, + 'commit guard: storage changed since refresh started, discarding rotated tokens', + { + startedWith: `${storedAtStart.refresh_token?.substring(0, 5)}...`, + nowHolds: storedAfter ? `${storedAfter.refresh_token?.substring(0, 5)}...` : null, + } + ) + const discarded: CallRefreshTokenResult = { + data: null, + error: new AuthRefreshDiscardedError(), + } + this.refreshingDeferred.resolve(discarded) + return discarded + } + await this._saveSession(data.session) await this._notifyAllSubscribers('TOKEN_REFRESHED', data.session) @@ -4978,58 +4913,83 @@ export default class GoTrueClient { await this._stopAutoRefresh() } + /** + * Tears down the client's background work: stops the auto-refresh interval, + * removes the `visibilitychange` listener, closes the cross-tab + * `BroadcastChannel`, and clears registered `onAuthStateChange` subscribers. + * + * Call this from cleanup hooks when the client is being replaced before + * its JS realm is destroyed. React Strict Mode and HMR are the common + * cases. Any in-flight `fetch` calls continue to completion and may still + * write to storage; dispose doesn't abort them or erase storage. + * + * Safe to call repeatedly. + * + * @category Auth + * + * @example Cleanup on React unmount + * ```ts + * useEffect(() => { + * const client = createClient(...) + * return () => { client.auth.dispose() } + * }, []) + * ``` + */ + async dispose(): Promise { + this._removeVisibilityChangedCallback() + await this._stopAutoRefresh() + this.broadcastChannel?.close() + this.broadcastChannel = null + this.stateChangeEmitters.clear() + } + /** * Runs the auto refresh token tick. */ private async _autoRefreshTokenTick() { this._debug('#_autoRefreshTokenTick()', 'begin') + // Skip if a refresh is already in flight. `_callRefreshToken` also + // dedupes via the same field, so this is just a fast-path skip to + // avoid an unnecessary storage read. + if (this.refreshingDeferred !== null) { + this._debug('#_autoRefreshTokenTick()', 'refresh already in flight, skipping') + return + } + try { - await this._acquireLock(0, async () => { - try { - const now = Date.now() + const now = Date.now() - try { - return await this._useSession(async (result) => { - const { - data: { session }, - } = result + try { + await this._useSession(async (result) => { + const { + data: { session }, + } = result - if (!session || !session.refresh_token || !session.expires_at) { - this._debug('#_autoRefreshTokenTick()', 'no session') - return - } + if (!session || !session.refresh_token || !session.expires_at) { + this._debug('#_autoRefreshTokenTick()', 'no session') + return + } - // session will expire in this many ticks (or has already expired if <= 0) - const expiresInTicks = Math.floor( - (session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION_MS - ) + // session will expire in this many ticks (or has already expired if <= 0) + const expiresInTicks = Math.floor( + (session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION_MS + ) - this._debug( - '#_autoRefreshTokenTick()', - `access token expires in ${expiresInTicks} ticks, a tick lasts ${AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${AUTO_REFRESH_TICK_THRESHOLD} ticks` - ) + this._debug( + '#_autoRefreshTokenTick()', + `access token expires in ${expiresInTicks} ticks, a tick lasts ${AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${AUTO_REFRESH_TICK_THRESHOLD} ticks` + ) - if (expiresInTicks <= AUTO_REFRESH_TICK_THRESHOLD) { - await this._callRefreshToken(session.refresh_token) - } - }) - } catch (e) { - console.error( - 'Auto refresh tick failed with error. This is likely a transient error.', - e - ) + if (expiresInTicks <= AUTO_REFRESH_TICK_THRESHOLD) { + await this._callRefreshToken(session.refresh_token) } - } finally { - this._debug('#_autoRefreshTokenTick()', 'end') - } - }) - } catch (e) { - if (e instanceof LockAcquireTimeoutError) { - this._debug('auto refresh token tick lock not available') - } else { - throw e + }) + } catch (e) { + console.error('Auto refresh tick failed with error. This is likely a transient error.', e) } + } finally { + this._debug('#_autoRefreshTokenTick()', 'end') } } @@ -5086,24 +5046,16 @@ export default class GoTrueClient { if (!calledFromInitialize) { // called when the visibility has changed, i.e. the browser // transitioned from hidden -> visible so we need to see if the session - // should be recovered immediately... but to do that we need to acquire - // the lock first asynchronously + // should be recovered await this.initializePromise - await this._acquireLock(this.lockAcquireTimeout, async () => { - if (document.visibilityState !== 'visible') { - this._debug( - methodName, - 'acquired the lock to recover the session, but the browser visibilityState is no longer visible, aborting' - ) - - // visibility has changed while waiting for the lock, abort - return - } + if (document.visibilityState !== 'visible') { + this._debug(methodName, 'visibilityState is no longer visible, skipping recovery') + return + } - // recover the session - await this._recoverAndRefresh() - }) + // recover the session + await this._recoverAndRefresh() } } else if (document.visibilityState === 'hidden') { if (this.autoRefreshToken) { @@ -5235,78 +5187,76 @@ export default class GoTrueClient { params: MFAVerifyWebauthnParams ): Promise private async _verify(params: MFAVerifyParams): Promise { - return this._acquireLock(this.lockAcquireTimeout, async () => { - try { - return await this._useSession(async (result) => { - const { data: sessionData, error: sessionError } = result - if (sessionError) { - return this._returnResult({ data: null, error: sessionError }) - } + try { + return await this._useSession(async (result) => { + const { data: sessionData, error: sessionError } = result + if (sessionError) { + return this._returnResult({ data: null, error: sessionError }) + } - const body: StrictOmit< - | Exclude - /** Exclude out the webauthn params from here because we're going to need to serialize them in the response */ - | Prettify< - StrictOmit & { - webauthn: Prettify< - StrictOmit & { - credential_response: PublicKeyCredentialJSON - } - > - } - >, - /* Exclude challengeId because the backend expects snake_case, and exclude factorId since it's passed in the path params */ - 'challengeId' | 'factorId' - > & { - challenge_id: string - } = { - challenge_id: params.challengeId, - ...('webauthn' in params - ? { - webauthn: { - ...params.webauthn, - credential_response: - params.webauthn.type === 'create' - ? serializeCredentialCreationResponse( - params.webauthn.credential_response as RegistrationCredential - ) - : serializeCredentialRequestResponse( - params.webauthn.credential_response as AuthenticationCredential - ), - }, - } - : { code: params.code }), - } + const body: StrictOmit< + | Exclude + /** Exclude out the webauthn params from here because we're going to need to serialize them in the response */ + | Prettify< + StrictOmit & { + webauthn: Prettify< + StrictOmit & { + credential_response: PublicKeyCredentialJSON + } + > + } + >, + /* Exclude challengeId because the backend expects snake_case, and exclude factorId since it's passed in the path params */ + 'challengeId' | 'factorId' + > & { + challenge_id: string + } = { + challenge_id: params.challengeId, + ...('webauthn' in params + ? { + webauthn: { + ...params.webauthn, + credential_response: + params.webauthn.type === 'create' + ? serializeCredentialCreationResponse( + params.webauthn.credential_response as RegistrationCredential + ) + : serializeCredentialRequestResponse( + params.webauthn.credential_response as AuthenticationCredential + ), + }, + } + : { code: params.code }), + } - const { data, error } = await _request( - this.fetch, - 'POST', - `${this.url}/factors/${params.factorId}/verify`, - { - body, - headers: this.headers, - jwt: sessionData?.session?.access_token, - } - ) - if (error) { - return this._returnResult({ data: null, error }) + const { data, error } = await _request( + this.fetch, + 'POST', + `${this.url}/factors/${params.factorId}/verify`, + { + body, + headers: this.headers, + jwt: sessionData?.session?.access_token, } - - await this._saveSession({ - expires_at: Math.round(Date.now() / 1000) + data.expires_in, - ...data, - }) - await this._notifyAllSubscribers('MFA_CHALLENGE_VERIFIED', data) - - return this._returnResult({ data, error }) - }) - } catch (error) { - if (isAuthError(error)) { + ) + if (error) { return this._returnResult({ data: null, error }) } - throw error + + await this._saveSession({ + expires_at: Math.round(Date.now() / 1000) + data.expires_in, + ...data, + }) + await this._notifyAllSubscribers('MFA_CHALLENGE_VERIFIED', data) + + return this._returnResult({ data, error }) + }) + } catch (error) { + if (isAuthError(error)) { + return this._returnResult({ data: null, error }) } - }) + throw error + } } /** @@ -5322,80 +5272,78 @@ export default class GoTrueClient { params: MFAChallengeWebauthnParams ): Promise> private async _challenge(params: MFAChallengeParams): Promise { - return this._acquireLock(this.lockAcquireTimeout, async () => { - try { - return await this._useSession(async (result) => { - const { data: sessionData, error: sessionError } = result - if (sessionError) { - return this._returnResult({ data: null, error: sessionError }) - } - - const response = (await _request( - this.fetch, - 'POST', - `${this.url}/factors/${params.factorId}/challenge`, - { - body: params, - headers: this.headers, - jwt: sessionData?.session?.access_token, - } - )) as - | Exclude - /** The server will send `serialized` data, so we assert the serialized response */ - | AuthMFAChallengeWebauthnServerResponse + try { + return await this._useSession(async (result) => { + const { data: sessionData, error: sessionError } = result + if (sessionError) { + return this._returnResult({ data: null, error: sessionError }) + } - if (response.error) { - return response + const response = (await _request( + this.fetch, + 'POST', + `${this.url}/factors/${params.factorId}/challenge`, + { + body: params, + headers: this.headers, + jwt: sessionData?.session?.access_token, } + )) as + | Exclude + /** The server will send `serialized` data, so we assert the serialized response */ + | AuthMFAChallengeWebauthnServerResponse - const { data } = response + if (response.error) { + return response + } - if (data.type !== 'webauthn') { - return { data, error: null } - } + const { data } = response - switch (data.webauthn.type) { - case 'create': - return { - data: { - ...data, - webauthn: { - ...data.webauthn, - credential_options: { - ...data.webauthn.credential_options, - publicKey: deserializeCredentialCreationOptions( - data.webauthn.credential_options.publicKey - ), - }, + if (data.type !== 'webauthn') { + return { data, error: null } + } + + switch (data.webauthn.type) { + case 'create': + return { + data: { + ...data, + webauthn: { + ...data.webauthn, + credential_options: { + ...data.webauthn.credential_options, + publicKey: deserializeCredentialCreationOptions( + data.webauthn.credential_options.publicKey + ), }, }, - error: null, - } - case 'request': - return { - data: { - ...data, - webauthn: { - ...data.webauthn, - credential_options: { - ...data.webauthn.credential_options, - publicKey: deserializeCredentialRequestOptions( - data.webauthn.credential_options.publicKey - ), - }, + }, + error: null, + } + case 'request': + return { + data: { + ...data, + webauthn: { + ...data.webauthn, + credential_options: { + ...data.webauthn.credential_options, + publicKey: deserializeCredentialRequestOptions( + data.webauthn.credential_options.publicKey + ), }, }, - error: null, - } - } - }) - } catch (error) { - if (isAuthError(error)) { - return this._returnResult({ data: null, error }) + }, + error: null, + } } - throw error + }) + } catch (error) { + if (isAuthError(error)) { + return this._returnResult({ data: null, error }) } - }) + throw error + } } /** @@ -5404,9 +5352,6 @@ export default class GoTrueClient { private async _challengeAndVerify( params: MFAChallengeAndVerifyParams ): Promise { - // both _challenge and _verify independently acquire the lock, so no need - // to acquire it here - const { data: challengeData, error: challengeError } = await this._challenge({ factorId: params.factorId, }) @@ -5425,7 +5370,6 @@ export default class GoTrueClient { * {@see GoTrueMFAApi#listFactors} */ private async _listFactors(): Promise { - // use #getUser instead of #_getUser as the former acquires a lock const { data: { user }, error: userError, diff --git a/packages/core/auth-js/src/lib/errors.ts b/packages/core/auth-js/src/lib/errors.ts index 21fb7886f8..eb2520dfa1 100644 --- a/packages/core/auth-js/src/lib/errors.ts +++ b/packages/core/auth-js/src/lib/errors.ts @@ -297,6 +297,38 @@ export function isAuthRetryableFetchError(error: unknown): error is AuthRetryabl return isAuthError(error) && error.name === 'AuthRetryableFetchError' } +/** + * Returned when the server rotated a refresh token successfully but the + * client chose not to persist the rotated tokens because the local session + * changed mid-flight. Usually means a concurrent `signOut` cleared storage + * between when the refresh started and when it came back. + * + * Set on the `error` field of the refresh result so callers can tell "we + * got rotated tokens but threw them away" apart from "the refresh failed." + * The rotated session on the server will be picked up on the next refresh + * via GoTrue's parent-of-active path. + * + * @example + * ```ts + * import { isAuthRefreshDiscardedError } from '@supabase/auth-js' + * + * if (isAuthRefreshDiscardedError(error)) { + * // Concurrent signOut/sign-in raced our refresh. Treat as a no-op. + * } + * ``` + */ +export class AuthRefreshDiscardedError extends CustomAuthError { + constructor( + message = 'Refresh result discarded: session state changed mid-flight (e.g., concurrent signOut)' + ) { + super(message, 'AuthRefreshDiscardedError', 409, undefined) + } +} + +export function isAuthRefreshDiscardedError(error: unknown): error is AuthRefreshDiscardedError { + return isAuthError(error) && error.name === 'AuthRefreshDiscardedError' +} + /** * This error is thrown on certain methods when the password used is deemed * weak. Inspect the reasons to identify what password strength rules are diff --git a/packages/core/auth-js/src/lib/locks.ts b/packages/core/auth-js/src/lib/locks.ts index ece1610d0c..56b2b44ce1 100644 --- a/packages/core/auth-js/src/lib/locks.ts +++ b/packages/core/auth-js/src/lib/locks.ts @@ -1,6 +1,17 @@ +/** + * Lock primitives retained for backwards-compatible imports. The auth client + * coordinates refreshes itself (deduping in-instance callers onto a shared + * in-flight promise) and lets the GoTrue server resolve cross-instance races, + * so it does not invoke any primitive from this module. The functions still + * work for direct callers that need a navigator.locks-backed or in-process + * exclusive lock of their own. + */ + import { supportsLocalStorage } from './helpers' /** + * @deprecated Debug flag for `navigatorLock` / `processLock`. The auth + * client ignores both, so this has no client-side effect. * @experimental */ export const internals = { @@ -18,18 +29,9 @@ export const internals = { /** * An error thrown when a lock cannot be acquired after some amount of time. * - * Use the {@link #isAcquireTimeout} property instead of checking with `instanceof`. - * - * @example - * ```ts - * import { LockAcquireTimeoutError } from '@supabase/auth-js' - * - * class CustomLockError extends LockAcquireTimeoutError { - * constructor() { - * super('Lock timed out') - * } - * } - * ``` + * @deprecated The auth client doesn't acquire locks around auth operations, + * so this error never originates from `supabase.auth.*` calls. Direct callers + * of `navigatorLock` / `processLock` still receive it on acquire timeout. */ export abstract class LockAcquireTimeoutError extends Error { public readonly isAcquireTimeout = true @@ -40,25 +42,15 @@ export abstract class LockAcquireTimeoutError extends Error { } /** - * Error thrown when the browser Navigator Lock API fails to acquire a lock. - * - * @example - * ```ts - * import { NavigatorLockAcquireTimeoutError } from '@supabase/auth-js' - * - * throw new NavigatorLockAcquireTimeoutError('Lock timed out') - * ``` + * @deprecated The auth client doesn't call `navigator.locks`, so this error + * never originates from `supabase.auth.*` calls. Direct callers of + * `navigatorLock` still receive it on acquire timeout. */ export class NavigatorLockAcquireTimeoutError extends LockAcquireTimeoutError {} /** - * Error thrown when the process-level lock helper cannot acquire a lock. - * - * @example - * ```ts - * import { ProcessLockAcquireTimeoutError } from '@supabase/auth-js' - * - * throw new ProcessLockAcquireTimeoutError('Lock timed out') - * ``` + * @deprecated The auth client doesn't run `processLock`, so this error + * never originates from `supabase.auth.*` calls. Direct callers of + * `processLock` still receive it on acquire timeout. */ export class ProcessLockAcquireTimeoutError extends LockAcquireTimeoutError {} @@ -86,12 +78,10 @@ export class ProcessLockAcquireTimeoutError extends LockAcquireTimeoutError {} * will time out after so many milliseconds. An error is * a timeout if it has `isAcquireTimeout` set to true. * @param fn The operation to run once the lock is acquired. - * @example - * ```ts - * await navigatorLock('sync-user', 1000, async () => { - * await refreshSession() - * }) - * ``` + * + * @deprecated The auth client coordinates refreshes itself and the server + * resolves concurrent refresh races, so passing `{ lock: navigatorLock }` + * to it has no effect. You can safely drop the import from your client setup. */ export async function navigatorLock( name: string, @@ -320,6 +310,11 @@ const PROCESS_LOCKS: { [name: string]: Promise } = {} * will time out after so many milliseconds. An error is * a timeout if it has `isAcquireTimeout` set to true. * @param fn The operation to run once the lock is acquired. + * + * @deprecated The auth client coordinates refreshes itself and the server + * resolves concurrent refresh races, so passing `{ lock: processLock }` + * to it has no effect. You can safely drop the import from your client setup. + * * @example * ```ts * await processLock('migrate', 5000, async () => { diff --git a/packages/core/auth-js/src/lib/types.ts b/packages/core/auth-js/src/lib/types.ts index 63382a001a..212cdf9a60 100644 --- a/packages/core/auth-js/src/lib/types.ts +++ b/packages/core/auth-js/src/lib/types.ts @@ -126,14 +126,20 @@ export type GoTrueClientOptions = { /* If debug messages are emitted. Can be used to inspect the behavior of the library. If set to a function, the provided function will be used instead of `console.log()` to perform the logging. */ debug?: boolean | ((message: string, ...args: any[]) => void) /** - * Provide your own locking mechanism based on the environment. By default, - * `navigatorLock` (Web Locks API) is used in browser environments when - * `persistSession` is true. Falls back to an in-process lock for non-browser - * environments (e.g. React Native). - * - * @experimental + * @deprecated Ignored. The client coordinates refreshes itself and the + * server handles cross-tab races, so you can safely remove this from your + * constructor options. Passing a non-null value emits a one-time + * `console.warn` at construction; suppress with `suppressLockOptionWarning`. */ lock?: LockFunc + /** + * Silence the construction-time `console.warn` emitted when a custom + * `lock` is passed. Set this only after confirming the auth client's + * built-in refresh coordination is sufficient for your runtime (typical + * web/RN/Node setups) and that no code path depends on the custom lock + * being invoked. + */ + suppressLockOptionWarning?: boolean /** * Set to "true" if there is a custom authorization header set globally. * @experimental @@ -145,30 +151,9 @@ export type GoTrueClientOptions = { */ throwOnError?: boolean /** - * The maximum time in milliseconds to wait for acquiring a cross-tab synchronization lock. - * - * When multiple browser tabs or windows use the auth client simultaneously, they coordinate - * via the Web Locks API to prevent race conditions during session refresh and other operations. - * This timeout controls how long to wait before attempting lock recovery. - * - * - **Positive value**: Wait up to this many milliseconds. If the lock is still held, attempt - * automatic recovery by stealing it (the previous holder is evicted, its callback continues - * to completion without exclusive access). This recovers from orphaned locks caused by - * React Strict Mode double-mount, storage API hangs, or aborted operations. - * - **Zero (0)**: Fail immediately if the lock is unavailable; throws `LockAcquireTimeoutError` - * (check `error.isAcquireTimeout === true`). - * - **Negative value**: Wait indefinitely — can cause permanent deadlocks if the lock is orphaned. - * - * @default 5000 - * - * @example - * ```ts - * const client = createClient(url, key, { - * auth: { - * lockAcquireTimeout: 5000, // 5 seconds, then steal orphaned lock - * }, - * }) - * ``` + * @deprecated The client doesn't acquire a lock around auth operations, so + * this timeout has nothing to bound. You can safely remove it from your + * constructor options. */ lockAcquireTimeout?: number diff --git a/packages/core/auth-js/test/GoTrueClient.browser.test.ts b/packages/core/auth-js/test/GoTrueClient.browser.test.ts index 3d1c527c2b..22c72147d1 100644 --- a/packages/core/auth-js/test/GoTrueClient.browser.test.ts +++ b/packages/core/auth-js/test/GoTrueClient.browser.test.ts @@ -650,51 +650,22 @@ describe('GoTrueClient BroadcastChannel', () => { }) }) -describe('Browser locks functionality', () => { - it('should use navigator locks when available', () => { - // Mock navigator.locks - const mockLock = { name: 'test-lock' } - const mockRequest = jest - .fn() - .mockImplementation((_, __, callback) => Promise.resolve(callback(mockLock))) - +describe('Lockless coordination: navigator.locks should NOT be invoked', () => { + it('initializes without touching navigator.locks', async () => { + const mockRequest = jest.fn() Object.defineProperty(navigator, 'locks', { value: { request: mockRequest }, writable: true, }) - // Test navigator locks usage in GoTrueClient const client = getClientWithSpecificStorage({ getItem: jest.fn(), setItem: jest.fn(), removeItem: jest.fn(), }) - - expect(client).toBeDefined() - }) - - it('should handle _acquireLock with empty pendingInLock', async () => { - const client = getClientWithSpecificStorage({ - getItem: jest.fn(), - setItem: jest.fn(), - removeItem: jest.fn(), - }) - - // Mock navigator.locks - const mockLock = { name: 'test-lock' } - const mockRequest = jest - .fn() - .mockImplementation((_, __, callback) => Promise.resolve(callback(mockLock))) - - Object.defineProperty(navigator, 'locks', { - value: { request: mockRequest }, - writable: true, - }) - - // Initialize client to trigger lock acquisition await client.initialize() - expect(client).toBeDefined() + expect(mockRequest).not.toHaveBeenCalled() }) }) @@ -1225,36 +1196,18 @@ describe('Storage and User Storage Combinations', () => { }) }) -describe('Lock Mechanism Branches', () => { - it('should handle lock acquisition timeout', async () => { - const slowLock = jest - .fn() - .mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 100))) +describe('Lockless backwards-compatibility: deprecated `lock` option', () => { + it('a custom `lock` function passed to the constructor is never invoked', async () => { + const customLock = jest.fn() const client = new (require('../src/GoTrueClient').default)({ url: 'http://localhost:9999', autoRefreshToken: false, - lock: slowLock, + lock: customLock, }) await client.initialize() - expect(client).toBeDefined() - }) - - it('should handle lock release errors', async () => { - const errorLock = jest.fn().mockImplementation(() => ({ - acquire: jest.fn().mockResolvedValue(undefined), - release: jest.fn().mockRejectedValue(new Error('Lock release error')), - })) - - const client = new (require('../src/GoTrueClient').default)({ - url: 'http://localhost:9999', - autoRefreshToken: false, - lock: errorLock, - }) - - await client.initialize() - expect(client).toBeDefined() + expect(customLock).not.toHaveBeenCalled() }) }) diff --git a/packages/core/auth-js/test/GoTrueClient.test.ts b/packages/core/auth-js/test/GoTrueClient.test.ts index 26b1021f19..db1f1bb30e 100644 --- a/packages/core/auth-js/test/GoTrueClient.test.ts +++ b/packages/core/auth-js/test/GoTrueClient.test.ts @@ -3,7 +3,7 @@ import { JWK, Session } from '../src' import GoTrueClient from '../src/GoTrueClient' import { base64UrlToUint8Array } from '../src/lib/base64url' import { STORAGE_KEY } from '../src/lib/constants' -import { setItemAsync } from '../src/lib/helpers' +import { getItemAsync, setItemAsync } from '../src/lib/helpers' import { memoryLocalStorageAdapter } from '../src/lib/local-storage' import { deserializeCredentialCreationOptions, @@ -56,7 +56,7 @@ describe('GoTrueClient', () => { expect(debugSpy).toHaveBeenCalled() }) - test('should handle custom lock implementation', async () => { + test('accepts a custom lock implementation but never invokes it (deprecated)', async () => { const customLock = jest.fn().mockResolvedValue(undefined) const client = new GoTrueClient({ url: 'http://localhost:9999', @@ -64,7 +64,9 @@ describe('GoTrueClient', () => { }) await client.initialize() - expect(customLock).toHaveBeenCalled() + // The `lock` option is accepted for backwards compatibility but the + // client doesn't invoke it. + expect(customLock).not.toHaveBeenCalled() }) test('should handle userStorage configuration', async () => { @@ -3418,23 +3420,11 @@ describe('SSO Authentication', () => { }) }) -describe('Lock functionality', () => { - test('_acquireLock should execute function when lock is acquired', async () => { - const mockFn = jest.fn().mockResolvedValue('success') - // @ts-expect-error 'Allow access to private _acquireLock' - const result = await authWithSession._acquireLock(1000, mockFn) - expect(result).toBe('success') - expect(mockFn).toHaveBeenCalled() - }) - - test('_acquireLock should throw error when lock acquisition times out', async () => { - let shouldFail = false - const mockLock = jest.fn().mockImplementation(async (name, timeout, fn) => { - if (shouldFail) { - throw new Error('Lock acquisition timeout') - } - return fn() - }) +describe('Lockless coordination', () => { + test('`lock` option is accepted but not invoked by the client', async () => { + const mockLock = jest + .fn() + .mockImplementation(async (name: string, timeout: number, fn: () => Promise) => fn()) const client = new GoTrueClient({ url: GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, @@ -3443,90 +3433,321 @@ describe('Lock functionality', () => { persistSession: false, }) - // Wait for initialization to complete - await new Promise((resolve) => setTimeout(resolve, 100)) + await client.initialize() + await client.getSession() - // Now make the lock fail - shouldFail = true + // The `lock` option is accepted for backwards compatibility; the client + // doesn't route auth operations through it. + expect(mockLock).not.toHaveBeenCalled() + }) - const mockFn = jest.fn() - // @ts-expect-error 'Allow access to private _acquireLock' - await expect(client._acquireLock(1000, mockFn)).rejects.toThrow('Lock acquisition timeout') - expect(mockFn).not.toHaveBeenCalled() + test('`lockAcquireTimeout` option is accepted but ignored', async () => { + expect( + () => + new GoTrueClient({ + url: GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + autoRefreshToken: false, + persistSession: false, + lockAcquireTimeout: 12345, + }) + ).not.toThrow() }) - test('should use custom lockAcquireTimeout when provided', async () => { - const capturedTimeouts: number[] = [] - const mockLock = jest - .fn() - .mockImplementation(async (name: string, timeout: number, fn: () => Promise) => { - capturedTimeouts.push(timeout) - return fn() - }) + test('subscriber callback may call other auth methods without deadlock', async () => { + // Auth methods invoked from inside an onAuthStateChange callback complete + // without re-entering a held lock, because notification is not gated by + // one. + const observed: string[] = [] + + const { + data: { subscription }, + } = authWithSession.onAuthStateChange(async (event) => { + observed.push(event) + if (event === 'INITIAL_SESSION') { + const { data } = await authWithSession.getSession() + observed.push(`got-session:${data.session ? 'present' : 'null'}`) + } + }) + + // Allow the initial-session emit to flush. + await new Promise((resolve) => setTimeout(resolve, 100)) + + subscription.unsubscribe() - const customTimeout = 20000 // 20 seconds (different from default 10s) + expect(observed).toContain('INITIAL_SESSION') + expect(observed.some((s) => s.startsWith('got-session:'))).toBe(true) + }) +}) +describe('dispose() lifecycle', () => { + test('idempotent: safe to call repeatedly', async () => { const client = new GoTrueClient({ url: GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, - lock: mockLock, autoRefreshToken: false, persistSession: false, - lockAcquireTimeout: customTimeout, }) - await client.initialize() - // Verify that the custom timeout was passed to the lock function - expect(mockLock).toHaveBeenCalled() - expect(capturedTimeouts).toContain(customTimeout) + await expect(client.dispose()).resolves.toBeUndefined() + await expect(client.dispose()).resolves.toBeUndefined() }) - test('should use default lockAcquireTimeout (10000ms) when not provided', async () => { - const capturedTimeouts: number[] = [] - const mockLock = jest - .fn() - .mockImplementation(async (name: string, timeout: number, fn: () => Promise) => { - capturedTimeouts.push(timeout) - return fn() - }) - + test('clears registered onAuthStateChange subscribers', async () => { const client = new GoTrueClient({ url: GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, - lock: mockLock, autoRefreshToken: false, persistSession: false, - // lockAcquireTimeout not provided, should default to 5000 }) + await client.initialize() + const cb = jest.fn() + client.onAuthStateChange(cb) + + // @ts-expect-error access protected field for verification + expect(client.stateChangeEmitters.size).toBeGreaterThan(0) + + await client.dispose() + + // @ts-expect-error access protected field for verification + expect(client.stateChangeEmitters.size).toBe(0) + }) + + test('stops the auto-refresh ticker', async () => { + const client = new GoTrueClient({ + url: GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + autoRefreshToken: true, + persistSession: false, + }) await client.initialize() + await client.startAutoRefresh() + + // @ts-expect-error access protected field for verification + expect(client.autoRefreshTicker).not.toBeNull() - // Verify that the default timeout (5000 = 5 seconds) was used - expect(mockLock).toHaveBeenCalled() - expect(capturedTimeouts).toContain(5000) + await client.dispose() + + // @ts-expect-error access protected field for verification + expect(client.autoRefreshTicker).toBeNull() }) +}) - test('should pass negative timeout to lock for indefinite wait', async () => { - const capturedTimeouts: number[] = [] - const mockLock = jest - .fn() - .mockImplementation(async (name: string, timeout: number, fn: () => Promise) => { - capturedTimeouts.push(timeout) - return fn() - }) +describe('Refresh commit guard (signOut-during-refresh race)', () => { + test('discards rotated tokens when storage was cleared mid-flight', async () => { + const storage = memoryLocalStorageAdapter() + const client = new GoTrueClient({ + url: GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + storage, + autoRefreshToken: false, + persistSession: true, + }) + await client.initialize() + // Plant a session in storage with refresh_token R1. + const R1 = 'refresh-token-r1' + await setItemAsync(storage, STORAGE_KEY, { + access_token: 'jwt.accesstoken.signature', + refresh_token: R1, + token_type: 'bearer', + expires_in: 1000, + expires_at: Math.floor(Date.now() / 1000) + 1000, + user: { id: 'user-1', email: 'u@example.com' } as any, + } as Session) + + // Simulate a successful refresh that returns rotated tokens (R2) while + // storage was cleared between fetch start and continuation. + // @ts-expect-error access protected for test + client._refreshAccessToken = jest.fn(async () => { + // Concurrent signOut clears storage before our refresh continuation runs. + // @ts-expect-error access protected for test + await client._removeSession() + return { + data: { + session: { + access_token: 'jwt.new.signature', + refresh_token: 'refresh-token-r2', + token_type: 'bearer', + expires_in: 1000, + expires_at: Math.floor(Date.now() / 1000) + 1000, + user: { id: 'user-1', email: 'u@example.com' }, + } as any, + user: { id: 'user-1', email: 'u@example.com' } as any, + }, + error: null, + } as any + }) + + // @ts-expect-error access protected for test + const result = await client._callRefreshToken(R1) + + // Commit guard discards the rotated tokens. + expect(result.error).not.toBeNull() + expect((result.error as Error).name).toBe('AuthRefreshDiscardedError') + + // Storage stays cleared; rotated tokens were NOT written back. + const stored = await storage.getItem(STORAGE_KEY) + expect(stored).toBeNull() + }) + + test('accepts rotated tokens when storage was empty at fetch start', async () => { + // Regression for the false-positive guard: snapshotting "current storage" + // *after* the fetch and comparing to the input refresh_token would treat + // empty-storage as "cleared mid-flight" and reject. That broke + // setSession/refreshSession({ refresh_token }) for any caller supplying + // externally-sourced tokens against empty storage (SSR cookie-handoff, + // external-token hydration). + const storage = memoryLocalStorageAdapter() const client = new GoTrueClient({ url: GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, - lock: mockLock, + storage, autoRefreshToken: false, - persistSession: false, - lockAcquireTimeout: -1, // Indefinite wait (not recommended) + persistSession: true, }) + await client.initialize() + + // Storage starts empty — no signOut ever ran. + expect(await storage.getItem(STORAGE_KEY)).toBeNull() + + const rotatedSession = { + access_token: 'jwt.new.signature', + refresh_token: 'refresh-token-r2', + token_type: 'bearer', + expires_in: 1000, + expires_at: Math.floor(Date.now() / 1000) + 1000, + user: { id: 'user-1', email: 'u@example.com' }, + } + + // Refresh succeeds against externally-supplied refresh_token; doesn't + // touch storage itself. The guard must NOT treat the still-empty + // pre-snapshot as a mid-flight clear. + // @ts-expect-error access protected for test + client._refreshAccessToken = jest.fn(async () => ({ + data: { session: rotatedSession, user: rotatedSession.user }, + error: null, + })) + + // @ts-expect-error access protected for test + const result = await client._callRefreshToken('R-external') + + expect(result.error).toBeNull() + expect(result.data?.refresh_token).toBe('refresh-token-r2') + + // Rotated tokens are persisted because storage wasn't cleared under us. + // Read via `getItemAsync` to JSON-parse the stringified Session that + // `_saveSession` wrote through `setItemAsync`. + const stored = (await getItemAsync(storage, STORAGE_KEY)) as Session | null + expect(stored?.refresh_token).toBe('refresh-token-r2') + }) + + test('accepts rotated tokens when storage holds a different session than the input refresh_token', async () => { + // Covers the SSR-hydration / multi-account-switch path: storage already + // holds session A, caller invokes setSession (or refreshSession) with an + // externally-sourced refresh_token that doesn't match A. The guard must + // not interpret "input token differs from stored token" as a mid-flight + // race — nothing was modified between snapshots, so the rotated tokens + // should persist and replace A. + const storage = memoryLocalStorageAdapter() + const client = new GoTrueClient({ + url: GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + storage, + autoRefreshToken: false, + persistSession: true, + }) + await client.initialize() + + // Plant session A (refresh_token RA) — represents "user already signed in". + const RA = 'refresh-token-a' + await setItemAsync(storage, STORAGE_KEY, { + access_token: 'jwt.a.signature', + refresh_token: RA, + token_type: 'bearer', + expires_in: 1000, + expires_at: Math.floor(Date.now() / 1000) + 1000, + user: { id: 'user-a', email: 'a@example.com' } as any, + } as Session) + + // Caller hydrates a different session (refresh_token RB, e.g. from SSR cookie). + // Mock the refresh to return rotated tokens for RB without touching storage. + const rotatedB = { + access_token: 'jwt.b.signature', + refresh_token: 'refresh-token-b-rotated', + token_type: 'bearer', + expires_in: 1000, + expires_at: Math.floor(Date.now() / 1000) + 1000, + user: { id: 'user-b', email: 'b@example.com' }, + } + // @ts-expect-error access protected for test + client._refreshAccessToken = jest.fn(async () => ({ + data: { session: rotatedB, user: rotatedB.user }, + error: null, + })) + // @ts-expect-error access protected for test + const result = await client._callRefreshToken('refresh-token-b') + + // The rotated tokens were accepted, not discarded by the commit guard. + expect(result.error).toBeNull() + expect(result.data?.refresh_token).toBe('refresh-token-b-rotated') + + // Storage now holds the rotated B session (replaces A). + const stored = (await getItemAsync(storage, STORAGE_KEY)) as Session | null + expect(stored?.refresh_token).toBe('refresh-token-b-rotated') + }) + + test('discards rotated tokens when a different session was written to storage mid-flight', async () => { + // Stronger guard test: storage holds session A at fetch start; while the + // refresh is in flight, *another* tab writes session B to storage; the + // refresh result for A's token must be discarded because storage no + // longer represents the state we were operating on. + const storage = memoryLocalStorageAdapter() + const client = new GoTrueClient({ + url: GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + storage, + autoRefreshToken: false, + persistSession: true, + }) await client.initialize() - // Verify that negative timeout was passed through - expect(mockLock).toHaveBeenCalled() - expect(capturedTimeouts).toContain(-1) + const RA = 'refresh-token-a' + const sessionA: Session = { + access_token: 'jwt.a.signature', + refresh_token: RA, + token_type: 'bearer', + expires_in: 1000, + expires_at: Math.floor(Date.now() / 1000) + 1000, + user: { id: 'user-a', email: 'a@example.com' } as any, + } + await setItemAsync(storage, STORAGE_KEY, sessionA) + + // Simulate another tab writing session B mid-flight. + // @ts-expect-error access protected for test + client._refreshAccessToken = jest.fn(async () => { + await setItemAsync(storage, STORAGE_KEY, { + ...sessionA, + refresh_token: 'refresh-token-b-from-other-tab', + }) + return { + data: { + session: { + ...sessionA, + access_token: 'jwt.a-rotated.signature', + refresh_token: 'refresh-token-a-rotated', + } as any, + user: sessionA.user as any, + }, + error: null, + } as any + }) + + // @ts-expect-error access protected for test + const result = await client._callRefreshToken(RA) + + expect(result.error).not.toBeNull() + expect((result.error as Error).name).toBe('AuthRefreshDiscardedError') + + // Storage still holds B (what the other tab wrote); A's rotated tokens + // were correctly discarded. + const stored = (await getItemAsync(storage, STORAGE_KEY)) as Session | null + expect(stored?.refresh_token).toBe('refresh-token-b-from-other-tab') }) }) diff --git a/packages/core/supabase-js/src/SupabaseClient.ts b/packages/core/supabase-js/src/SupabaseClient.ts index a38798d63b..5be388ba53 100644 --- a/packages/core/supabase-js/src/SupabaseClient.ts +++ b/packages/core/supabase-js/src/SupabaseClient.ts @@ -591,6 +591,7 @@ export default class SupabaseClient< throwOnError, experimental, lockAcquireTimeout, + suppressLockOptionWarning, skipAutoInitialize, }: SupabaseAuthClientOptions, headers?: Record, @@ -616,6 +617,7 @@ export default class SupabaseClient< experimental, fetch, lockAcquireTimeout, + suppressLockOptionWarning, skipAutoInitialize, // auth checks if there is a custom authorizaiton header using this flag // so it knows whether to return an error when getUser is called with no session diff --git a/packages/core/supabase-js/src/lib/types.ts b/packages/core/supabase-js/src/lib/types.ts index d19458e9b1..0f3eea89ab 100644 --- a/packages/core/supabase-js/src/lib/types.ts +++ b/packages/core/supabase-js/src/lib/types.ts @@ -182,9 +182,10 @@ export type SupabaseClientOptions = { */ debug?: SupabaseAuthClientOptions['debug'] /** - * Provide your own locking mechanism based on the environment. By default no locking is done at this time. - * - * @experimental + * @deprecated The auth client coordinates refreshes itself and the server + * resolves cross-tab races, so any value forwarded through this option + * has no effect. You can safely remove it from your `createClient` + * options. */ lock?: SupabaseAuthClientOptions['lock'] /** @@ -200,13 +201,16 @@ export type SupabaseClientOptions = { */ experimental?: SupabaseAuthClientOptions['experimental'] /** - * Maximum time in milliseconds to wait when acquiring the auth lock before - * stealing it from the previous holder. See `GoTrueClientOptions.lockAcquireTimeout` - * for full semantics (zero fails immediately, negative waits indefinitely). - * - * @default 5000 + * @deprecated The auth client doesn't acquire a lock around auth + * operations, so this timeout has nothing to bound. You can safely remove + * it from your `createClient` options. */ lockAcquireTimeout?: SupabaseAuthClientOptions['lockAcquireTimeout'] + /** + * Silence the construction-time `console.warn` emitted when a custom + * `lock` is passed. Forwarded to the underlying auth client. + */ + suppressLockOptionWarning?: SupabaseAuthClientOptions['suppressLockOptionWarning'] /** * If true, skips automatic initialization in the auth client constructor. * Useful for SSR contexts where initialization timing must be controlled to diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 67c914917d..95d2a77f2a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,7 +64,7 @@ importers: version: 10.0.1(eslint@10.3.0(jiti@2.4.2)) '@nx/eslint': specifier: 22.7.1 - version: 22.7.1(6acc8fb6600279b4ea6839117370fcf1) + version: 22.7.1(nws6jx42ess74hysb6wocorctq) '@nx/eslint-plugin': specifier: 22.7.1 version: 22.7.1(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21))(@typescript-eslint/parser@8.59.3(eslint@10.3.0(jiti@2.4.2))(typescript@5.8.3))(eslint-config-prettier@10.1.8(eslint@10.3.0(jiti@2.4.2)))(eslint@10.3.0(jiti@2.4.2))(nx@22.7.1(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21)))(typescript@5.8.3) @@ -76,7 +76,7 @@ importers: version: 22.7.1(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21))(nx@22.7.1(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21))) '@nx/playwright': specifier: 22.7.1 - version: 22.7.1(cfecd908ccdf06af45448c55a0fea9d8) + version: 22.7.1(ripdl44hm2ldxruzyi4asrenke) '@nx/vite': specifier: 22.7.1 version: 22.7.1(@babel/traverse@7.29.0)(@nx/eslint@22.7.1(6acc8fb6600279b4ea6839117370fcf1))(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21))(nx@22.7.1(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21)))(typescript@5.8.3)(vite@7.3.2(@types/node@20.19.9)(jiti@2.4.2)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.6) @@ -1695,105 +1695,89 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -1991,28 +1975,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-musl@16.2.6': resolution: {integrity: sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-x64-gnu@16.2.6': resolution: {integrity: sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-musl@16.2.6': resolution: {integrity: sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-win32-arm64-msvc@16.2.6': resolution: {integrity: sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==} @@ -2103,25 +2083,21 @@ packages: resolution: {integrity: sha512-GdgPYMfbijBRFJs1absL/9QdSNLsTAGdyKykDf9CaVxEMZ92VB+pncpX9Vn/ZBCSeeWTLF+bSK3UM5v+loIObQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@nx/nx-linux-arm64-musl@22.7.1': resolution: {integrity: sha512-HyBgPtY1hyNTk8683nt7F29jh3lVdS/zul9vS0NgKeCSoYL3GRM3nLoTPynoHUxyVP/tWYOE3ymvnk92qYwL4Q==} cpu: [arm64] os: [linux] - libc: [musl] '@nx/nx-linux-x64-gnu@22.7.1': resolution: {integrity: sha512-bQBgRiEsanNvKcDOjVAUPjvcp0iDLofYYUL2af2iuCDxreLOej+J6MeA5bWTLNly5ly1d4voKGTqa+OsouVyLg==} cpu: [x64] os: [linux] - libc: [glibc] '@nx/nx-linux-x64-musl@22.7.1': resolution: {integrity: sha512-gcco2GjcAztF/fRcAgFxtWxrWDnQdNmPaAN9FTt1+qQ9RUSLvdL8bQxKx4Kd9N9T+gXPlrWhMkBkKbbV09+X1Q==} cpu: [x64] os: [linux] - libc: [musl] '@nx/nx-win32-arm64-msvc@22.7.1': resolution: {integrity: sha512-IT9oEn0YQ83iPH7666aoPyTRsUzBIBJdBLMXeLX4I60fHPXWhUSGpfiLtIsgU2OfeOVb9hU9idwNh1wc4u9rWQ==} @@ -2194,8 +2170,8 @@ packages: '@oxc-project/types@0.103.0': resolution: {integrity: sha512-bkiYX5kaXWwUessFRSoXFkGIQTmc6dLGdxuRTrC+h8PSnIdZyuXHHlLAeTmOue5Br/a0/a7dHH0Gca6eXn9MKg==} - '@oxc-project/types@0.129.0': - resolution: {integrity: sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==} + '@oxc-project/types@0.127.0': + resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} '@oxc-resolver/binding-android-arm-eabi@11.19.1': resolution: {integrity: sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==} @@ -2236,49 +2212,41 @@ packages: resolution: {integrity: sha512-heV2+jmXyYnUrpUXSPugqWDRpnsQcDm2AX4wzTuvgdlZfoNYO0O3W2AVpJYaDn9AG4JdM6Kxom8+foE7/BcSig==} cpu: [arm64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-arm64-musl@11.19.1': resolution: {integrity: sha512-jvo2Pjs1c9KPxMuMPIeQsgu0mOJF9rEb3y3TdpsrqwxRM+AN6/nDDwv45n5ZrUnQMsdBy5gIabioMKnQfWo9ew==} cpu: [arm64] os: [linux] - libc: [musl] '@oxc-resolver/binding-linux-ppc64-gnu@11.19.1': resolution: {integrity: sha512-vLmdNxWCdN7Uo5suays6A/+ywBby2PWBBPXctWPg5V0+eVuzsJxgAn6MMB4mPlshskYbppjpN2Zg83ArHze9gQ==} cpu: [ppc64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-riscv64-gnu@11.19.1': resolution: {integrity: sha512-/b+WgR+VTSBxzgOhDO7TlMXC1ufPIMR6Vj1zN+/x+MnyXGW7prTLzU9eW85Aj7Th7CCEG9ArCbTeqxCzFWdg2w==} cpu: [riscv64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-riscv64-musl@11.19.1': resolution: {integrity: sha512-YlRdeWb9j42p29ROh+h4eg/OQ3dTJlpHSa+84pUM9+p6i3djtPz1q55yLJhgW9XfDch7FN1pQ/Vd6YP+xfRIuw==} cpu: [riscv64] os: [linux] - libc: [musl] '@oxc-resolver/binding-linux-s390x-gnu@11.19.1': resolution: {integrity: sha512-EDpafVOQWF8/MJynsjOGFThcqhRHy417sRyLfQmeiamJ8qVhSKAn2Dn2VVKUGCjVB9C46VGjhNo7nOPUi1x6uA==} cpu: [s390x] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-x64-gnu@11.19.1': resolution: {integrity: sha512-NxjZe+rqWhr+RT8/Ik+5ptA3oz7tUw361Wa5RWQXKnfqwSSHdHyrw6IdcTfYuml9dM856AlKWZIUXDmA9kkiBQ==} cpu: [x64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-x64-musl@11.19.1': resolution: {integrity: sha512-cM/hQwsO3ReJg5kR+SpI69DMfvNCp+A/eVR4b4YClE5bVZwz8rh2Nh05InhwI5HR/9cArbEkzMjcKgTHS6UaNw==} cpu: [x64] os: [linux] - libc: [musl] '@oxc-resolver/binding-openharmony-arm64@11.19.1': resolution: {integrity: sha512-QF080IowFB0+9Rh6RcD19bdgh49BpQHUW5TajG1qvWHvmrQznTZZjYlgE2ltLXyKY+qs4F/v5xuX1XS7Is+3qA==} @@ -2654,23 +2622,17 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} - '@rolldown/binding-android-arm64@1.0.0': - resolution: {integrity: sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - '@rolldown/binding-android-arm64@1.0.0-beta.57': resolution: {integrity: sha512-GoOVDy8bjw9z1K30Oo803nSzXJS/vWhFijFsW3kzvZCO8IZwFnNa6pGctmbbJstKl3Fv6UBwyjJQN6msejW0IQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0': - resolution: {integrity: sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==} + '@rolldown/binding-android-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] - os: [darwin] + os: [android] '@rolldown/binding-darwin-arm64@1.0.0-beta.57': resolution: {integrity: sha512-9c4FOhRGpl+PX7zBK5p17c5efpF9aSpTPgyigv57hXf5NjQUaJOOiejPLAtFiKNBIfm5Uu6yFkvLKzOafNvlTw==} @@ -2678,10 +2640,10 @@ packages: cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0': - resolution: {integrity: sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==} + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] + cpu: [arm64] os: [darwin] '@rolldown/binding-darwin-x64@1.0.0-beta.57': @@ -2690,11 +2652,11 @@ packages: cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0': - resolution: {integrity: sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==} + '@rolldown/binding-darwin-x64@1.0.0-rc.17': + resolution: {integrity: sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] - os: [freebsd] + os: [darwin] '@rolldown/binding-freebsd-x64@1.0.0-beta.57': resolution: {integrity: sha512-uA9kG7+MYkHTbqwv67Tx+5GV5YcKd33HCJIi0311iYBd25yuwyIqvJfBdt1VVB8tdOlyTb9cPAgfCki8nhwTQg==} @@ -2702,11 +2664,11 @@ packages: cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0': - resolution: {integrity: sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==} + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': + resolution: {integrity: sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] + cpu: [x64] + os: [freebsd] '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.57': resolution: {integrity: sha512-3KkS0cHsllT2T+Te+VZMKHNw6FPQihYsQh+8J4jkzwgvAQpbsbXmrqhkw3YU/QGRrD8qgcOvBr6z5y6Jid+rmw==} @@ -2714,81 +2676,71 @@ packages: cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0': - resolution: {integrity: sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': + resolution: {integrity: sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] + cpu: [arm] os: [linux] - libc: [glibc] '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.57': resolution: {integrity: sha512-A3/wu1RgsHhqP3rVH2+sM81bpk+Qd2XaHTl8LtX5/1LNR7QVBFBCpAoiXwjTdGnI5cMdBVi7Z1pi52euW760Fw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.0': - resolution: {integrity: sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@rolldown/binding-linux-arm64-musl@1.0.0-beta.57': resolution: {integrity: sha512-d0kIVezTQtazpyWjiJIn5to8JlwfKITDqwsFv0Xc6s31N16CD2PC/Pl2OtKgS7n8WLOJbfqgIp5ixYzTAxCqMg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.0': - resolution: {integrity: sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==} + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': + resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] + cpu: [arm64] os: [linux] - libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.0': - resolution: {integrity: sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==} + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] + cpu: [ppc64] os: [linux] - libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.0': - resolution: {integrity: sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==} + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] + cpu: [s390x] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.0.0-beta.57': resolution: {integrity: sha512-E199LPijo98yrLjPCmETx8EF43sZf9t3guSrLee/ej1rCCc3zDVTR4xFfN9BRAapGVl7/8hYqbbiQPTkv73kUg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.0': - resolution: {integrity: sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==} + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@rolldown/binding-linux-x64-musl@1.0.0-beta.57': resolution: {integrity: sha512-++EQDpk/UJ33kY/BNsh7A7/P1sr/jbMuQ8cE554ZIy+tCUWCivo9zfyjDUoiMdnxqX6HLJEqqGnbGQOvzm2OMQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.0': - resolution: {integrity: sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==} + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': + resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] + cpu: [x64] + os: [linux] '@rolldown/binding-openharmony-arm64@1.0.0-beta.57': resolution: {integrity: sha512-voDEBcNqxbUv/GeXKFtxXVWA+H45P/8Dec4Ii/SbyJyGvCqV1j+nNHfnFUIiRQ2Q40DwPe/djvgYBs9PpETiMA==} @@ -2796,21 +2748,21 @@ packages: cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0': - resolution: {integrity: sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==} + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] + cpu: [arm64] + os: [openharmony] '@rolldown/binding-wasm32-wasi@1.0.0-beta.57': resolution: {integrity: sha512-bRhcF7NLlCnpkzLVlVhrDEd0KH22VbTPkPTbMjlYvqhSmarxNIq5vtlQS8qmV7LkPKHrNLWyJW/V/sOyFba26Q==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0': - resolution: {integrity: sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==} + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': + resolution: {integrity: sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] + cpu: [wasm32] '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.57': resolution: {integrity: sha512-rnDVGRks2FQ2hgJ2g15pHtfxqkGFGjJQUDWzYznEkE8Ra2+Vag9OffxdbJMZqBWXHVM0iS4dv8qSiEn7bO+n1Q==} @@ -2818,10 +2770,10 @@ packages: cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0': - resolution: {integrity: sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': + resolution: {integrity: sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] + cpu: [arm64] os: [win32] '@rolldown/binding-win32-x64-msvc@1.0.0-beta.57': @@ -2830,12 +2782,18 @@ packages: cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.0': - resolution: {integrity: sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==} + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': + resolution: {integrity: sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] '@rolldown/pluginutils@1.0.0-beta.57': resolution: {integrity: sha512-aQNelgx14tGA+n2tNSa9x6/jeoCL9fkDeCei7nOKnHx0fEFRRMu5ReiITo+zZD5TzWDGGRjbSYCs93IfRIyTuQ==} + '@rolldown/pluginutils@1.0.0-rc.17': + resolution: {integrity: sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==} + '@rolldown/pluginutils@1.0.0-rc.3': resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} @@ -2873,79 +2831,66 @@ packages: resolution: {integrity: sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.60.3': resolution: {integrity: sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.60.3': resolution: {integrity: sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.60.3': resolution: {integrity: sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.3': resolution: {integrity: sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.3': resolution: {integrity: sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.60.3': resolution: {integrity: sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.3': resolution: {integrity: sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.60.3': resolution: {integrity: sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.60.3': resolution: {integrity: sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.60.3': resolution: {integrity: sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.3': resolution: {integrity: sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.60.3': resolution: {integrity: sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.60.3': resolution: {integrity: sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==} @@ -3057,28 +3002,24 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [glibc] '@swc/core-linux-arm64-musl@1.15.8': resolution: {integrity: sha512-koiCqL09EwOP1S2RShCI7NbsQuG6r2brTqUYE7pV7kZm9O17wZ0LSz22m6gVibpwEnw8jI3IE1yYsQTVpluALw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [musl] '@swc/core-linux-x64-gnu@1.15.8': resolution: {integrity: sha512-4p6lOMU3bC+Vd5ARtKJ/FxpIC5G8v3XLoPEZ5s7mLR8h7411HWC/LmTXDHcrSXRC55zvAVia1eldy6zDLz8iFQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [glibc] '@swc/core-linux-x64-musl@1.15.8': resolution: {integrity: sha512-z3XBnbrZAL+6xDGAhJoN4lOueIxC/8rGrJ9tg+fEaeqLEuAtHSW2QHDHxDwkxZMjuF/pZ6MUTjHjbp8wLbuRLA==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [musl] '@swc/core-win32-arm64-msvc@1.15.8': resolution: {integrity: sha512-djQPJ9Rh9vP8GTS/Df3hcc6XP6xnG5c8qsngWId/BLA9oX6C7UzCPAn74BG/wGb9a6j4w3RINuoaieJB3t+7iQ==} @@ -3157,28 +3098,24 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.3.0': resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.3.0': resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.3.0': resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.3.0': resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} @@ -3524,49 +3461,41 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] - libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -6006,28 +5935,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -7046,13 +6971,13 @@ packages: vue-tsc: optional: true - rolldown@1.0.0: - resolution: {integrity: sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==} + rolldown@1.0.0-beta.57: + resolution: {integrity: sha512-lMMxcNN71GMsSko8RyeTaFoATHkCh4IWU7pYF73ziMYjhHZWfVesC6GQ+iaJCvZmVjvgSks9Ks1aaqEkBd8udg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rolldown@1.0.0-beta.57: - resolution: {integrity: sha512-lMMxcNN71GMsSko8RyeTaFoATHkCh4IWU7pYF73ziMYjhHZWfVesC6GQ+iaJCvZmVjvgSks9Ks1aaqEkBd8udg==} + rolldown@1.0.0-rc.17: + resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -7794,9 +7719,9 @@ packages: unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} - unrun@0.2.38: - resolution: {integrity: sha512-vYFWSyX5Be408RX+CAd2Heh8egQRnGBWOQmZKwYPvMtTQNmRYoKdSyFIczYNxZEMqz0dJzSxp7xkcZGzvtkHyQ==} - engines: {node: ^22.13.0 || >=24.0.0} + unrun@0.2.39: + resolution: {integrity: sha512-h9FxYVpztY/wwq+bauLOh6Y3CWu2IVeRLq5lxzneBiIU9Tn86OGp9xiQrGhnYspAmg5dzdY0Cc8+Y70kuTARCg==} + engines: {node: '>=20.19.0'} hasBin: true peerDependencies: synckit: ^0.11.11 @@ -9870,7 +9795,7 @@ snapshots: - typescript - verdaccio - '@nx/eslint@22.7.1(6acc8fb6600279b4ea6839117370fcf1)': + '@nx/eslint@22.7.1(nws6jx42ess74hysb6wocorctq)': dependencies: '@nx/devkit': 22.7.1(nx@22.7.1(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21))) '@nx/js': 22.7.1(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21))(nx@22.7.1(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21))) @@ -9988,10 +9913,10 @@ snapshots: '@nx/nx-win32-x64-msvc@22.7.1': optional: true - '@nx/playwright@22.7.1(cfecd908ccdf06af45448c55a0fea9d8)': + '@nx/playwright@22.7.1(ripdl44hm2ldxruzyi4asrenke)': dependencies: '@nx/devkit': 22.7.1(nx@22.7.1(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21))) - '@nx/eslint': 22.7.1(6acc8fb6600279b4ea6839117370fcf1) + '@nx/eslint': 22.7.1(nws6jx42ess74hysb6wocorctq) '@nx/js': 22.7.1(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21))(nx@22.7.1(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21))) minimatch: 10.2.4 tslib: 2.8.1 @@ -10042,7 +9967,7 @@ snapshots: semver: 7.8.0 tslib: 2.8.1 optionalDependencies: - '@nx/eslint': 22.7.1(6acc8fb6600279b4ea6839117370fcf1) + '@nx/eslint': 22.7.1(nws6jx42ess74hysb6wocorctq) vite: 7.3.2(@types/node@20.19.9)(jiti@2.4.2)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.9.0) vitest: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@20.19.9)(@vitest/coverage-v8@4.1.6)(jsdom@26.1.0)(vite@7.3.2(@types/node@20.19.9)(jiti@2.4.2)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.9.0)) transitivePeerDependencies: @@ -10064,7 +9989,7 @@ snapshots: picocolors: 1.1.1 tslib: 2.8.1 optionalDependencies: - '@nx/eslint': 22.7.1(6acc8fb6600279b4ea6839117370fcf1) + '@nx/eslint': 22.7.1(nws6jx42ess74hysb6wocorctq) '@nx/jest': 22.7.1(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21))(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(nx@22.7.1(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21)))(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.21))(@types/node@20.19.9)(typescript@5.8.3))(typescript@5.8.3) '@nx/playwright': 22.7.1(cfecd908ccdf06af45448c55a0fea9d8) '@nx/vite': 22.7.1(@babel/traverse@7.29.0)(@nx/eslint@22.7.1(6acc8fb6600279b4ea6839117370fcf1))(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21))(nx@22.7.1(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21)))(typescript@5.8.3)(vite@7.3.2(@types/node@20.19.9)(jiti@2.4.2)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.6) @@ -10097,7 +10022,7 @@ snapshots: '@oxc-project/types@0.103.0': {} - '@oxc-project/types@0.129.0': {} + '@oxc-project/types@0.127.0': {} '@oxc-resolver/binding-android-arm-eabi@11.19.1': optional: true @@ -10485,77 +10410,70 @@ snapshots: '@radix-ui/rect@1.1.1': {} - '@rolldown/binding-android-arm64@1.0.0': - optional: true - '@rolldown/binding-android-arm64@1.0.0-beta.57': optional: true - '@rolldown/binding-darwin-arm64@1.0.0': + '@rolldown/binding-android-arm64@1.0.0-rc.17': optional: true '@rolldown/binding-darwin-arm64@1.0.0-beta.57': optional: true - '@rolldown/binding-darwin-x64@1.0.0': + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': optional: true '@rolldown/binding-darwin-x64@1.0.0-beta.57': optional: true - '@rolldown/binding-freebsd-x64@1.0.0': + '@rolldown/binding-darwin-x64@1.0.0-rc.17': optional: true '@rolldown/binding-freebsd-x64@1.0.0-beta.57': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0': + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': optional: true '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.57': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': optional: true '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.57': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0': + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': optional: true '@rolldown/binding-linux-arm64-musl@1.0.0-beta.57': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.0': + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.0': + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0': + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': optional: true '@rolldown/binding-linux-x64-gnu@1.0.0-beta.57': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0': + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': optional: true '@rolldown/binding-linux-x64-musl@1.0.0-beta.57': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0': + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': optional: true '@rolldown/binding-openharmony-arm64@1.0.0-beta.57': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': optional: true '@rolldown/binding-wasm32-wasi@1.0.0-beta.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': @@ -10566,22 +10484,29 @@ snapshots: - '@emnapi/runtime' optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0': + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.57': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0': + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': optional: true '@rolldown/binding-win32-x64-msvc@1.0.0-beta.57': optional: true - '@rolldown/pluginutils@1.0.0': {} + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': + optional: true '@rolldown/pluginutils@1.0.0-beta.57': {} + '@rolldown/pluginutils@1.0.0-rc.17': {} + '@rolldown/pluginutils@1.0.0-rc.3': {} '@rollup/rollup-android-arm-eabi@4.60.3': @@ -15529,27 +15454,6 @@ snapshots: transitivePeerDependencies: - oxc-resolver - rolldown@1.0.0: - dependencies: - '@oxc-project/types': 0.129.0 - '@rolldown/pluginutils': 1.0.0 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0 - '@rolldown/binding-darwin-arm64': 1.0.0 - '@rolldown/binding-darwin-x64': 1.0.0 - '@rolldown/binding-freebsd-x64': 1.0.0 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0 - '@rolldown/binding-linux-arm64-gnu': 1.0.0 - '@rolldown/binding-linux-arm64-musl': 1.0.0 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0 - '@rolldown/binding-linux-s390x-gnu': 1.0.0 - '@rolldown/binding-linux-x64-gnu': 1.0.0 - '@rolldown/binding-linux-x64-musl': 1.0.0 - '@rolldown/binding-openharmony-arm64': 1.0.0 - '@rolldown/binding-wasm32-wasi': 1.0.0 - '@rolldown/binding-win32-arm64-msvc': 1.0.0 - '@rolldown/binding-win32-x64-msvc': 1.0.0 - rolldown@1.0.0-beta.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): dependencies: '@oxc-project/types': 0.103.0 @@ -15572,6 +15476,27 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' + rolldown@1.0.0-rc.17: + dependencies: + '@oxc-project/types': 0.127.0 + '@rolldown/pluginutils': 1.0.0-rc.17 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.17 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.17 + '@rolldown/binding-darwin-x64': 1.0.0-rc.17 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.17 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.17 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.17 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.17 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.17 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.17 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17 + rollup@4.60.3: dependencies: '@types/estree': 1.0.8 @@ -16295,7 +16220,7 @@ snapshots: tinyglobby: 0.2.16 tree-kill: 1.2.2 unconfig-core: 7.5.0 - unrun: 0.2.38(synckit@0.11.12) + unrun: 0.2.39(synckit@0.11.12) optionalDependencies: '@arethetypeswrong/core': 0.18.2 typescript: 5.8.3 @@ -16488,9 +16413,9 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - unrun@0.2.38(synckit@0.11.12): + unrun@0.2.39(synckit@0.11.12): dependencies: - rolldown: 1.0.0 + rolldown: 1.0.0-rc.17 optionalDependencies: synckit: 0.11.12 From b8f13bfc1a7c6938cf148bc62c4c884930645f90 Mon Sep 17 00:00:00 2001 From: Katerina Skroumpelou Date: Mon, 25 May 2026 13:58:05 +0300 Subject: [PATCH 2/7] chore(repo): resolve conflicts and update pnpm lock --- pnpm-lock.yaml | 303 ++++++++++++++++++++++++++++++------------------- 1 file changed, 189 insertions(+), 114 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 95d2a77f2a..67c914917d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,7 +64,7 @@ importers: version: 10.0.1(eslint@10.3.0(jiti@2.4.2)) '@nx/eslint': specifier: 22.7.1 - version: 22.7.1(nws6jx42ess74hysb6wocorctq) + version: 22.7.1(6acc8fb6600279b4ea6839117370fcf1) '@nx/eslint-plugin': specifier: 22.7.1 version: 22.7.1(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21))(@typescript-eslint/parser@8.59.3(eslint@10.3.0(jiti@2.4.2))(typescript@5.8.3))(eslint-config-prettier@10.1.8(eslint@10.3.0(jiti@2.4.2)))(eslint@10.3.0(jiti@2.4.2))(nx@22.7.1(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21)))(typescript@5.8.3) @@ -76,7 +76,7 @@ importers: version: 22.7.1(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21))(nx@22.7.1(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21))) '@nx/playwright': specifier: 22.7.1 - version: 22.7.1(ripdl44hm2ldxruzyi4asrenke) + version: 22.7.1(cfecd908ccdf06af45448c55a0fea9d8) '@nx/vite': specifier: 22.7.1 version: 22.7.1(@babel/traverse@7.29.0)(@nx/eslint@22.7.1(6acc8fb6600279b4ea6839117370fcf1))(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21))(nx@22.7.1(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21)))(typescript@5.8.3)(vite@7.3.2(@types/node@20.19.9)(jiti@2.4.2)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.6) @@ -1695,89 +1695,105 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -1975,24 +1991,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@next/swc-linux-arm64-musl@16.2.6': resolution: {integrity: sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@next/swc-linux-x64-gnu@16.2.6': resolution: {integrity: sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@next/swc-linux-x64-musl@16.2.6': resolution: {integrity: sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@next/swc-win32-arm64-msvc@16.2.6': resolution: {integrity: sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==} @@ -2083,21 +2103,25 @@ packages: resolution: {integrity: sha512-GdgPYMfbijBRFJs1absL/9QdSNLsTAGdyKykDf9CaVxEMZ92VB+pncpX9Vn/ZBCSeeWTLF+bSK3UM5v+loIObQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@nx/nx-linux-arm64-musl@22.7.1': resolution: {integrity: sha512-HyBgPtY1hyNTk8683nt7F29jh3lVdS/zul9vS0NgKeCSoYL3GRM3nLoTPynoHUxyVP/tWYOE3ymvnk92qYwL4Q==} cpu: [arm64] os: [linux] + libc: [musl] '@nx/nx-linux-x64-gnu@22.7.1': resolution: {integrity: sha512-bQBgRiEsanNvKcDOjVAUPjvcp0iDLofYYUL2af2iuCDxreLOej+J6MeA5bWTLNly5ly1d4voKGTqa+OsouVyLg==} cpu: [x64] os: [linux] + libc: [glibc] '@nx/nx-linux-x64-musl@22.7.1': resolution: {integrity: sha512-gcco2GjcAztF/fRcAgFxtWxrWDnQdNmPaAN9FTt1+qQ9RUSLvdL8bQxKx4Kd9N9T+gXPlrWhMkBkKbbV09+X1Q==} cpu: [x64] os: [linux] + libc: [musl] '@nx/nx-win32-arm64-msvc@22.7.1': resolution: {integrity: sha512-IT9oEn0YQ83iPH7666aoPyTRsUzBIBJdBLMXeLX4I60fHPXWhUSGpfiLtIsgU2OfeOVb9hU9idwNh1wc4u9rWQ==} @@ -2170,8 +2194,8 @@ packages: '@oxc-project/types@0.103.0': resolution: {integrity: sha512-bkiYX5kaXWwUessFRSoXFkGIQTmc6dLGdxuRTrC+h8PSnIdZyuXHHlLAeTmOue5Br/a0/a7dHH0Gca6eXn9MKg==} - '@oxc-project/types@0.127.0': - resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + '@oxc-project/types@0.129.0': + resolution: {integrity: sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==} '@oxc-resolver/binding-android-arm-eabi@11.19.1': resolution: {integrity: sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==} @@ -2212,41 +2236,49 @@ packages: resolution: {integrity: sha512-heV2+jmXyYnUrpUXSPugqWDRpnsQcDm2AX4wzTuvgdlZfoNYO0O3W2AVpJYaDn9AG4JdM6Kxom8+foE7/BcSig==} cpu: [arm64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-arm64-musl@11.19.1': resolution: {integrity: sha512-jvo2Pjs1c9KPxMuMPIeQsgu0mOJF9rEb3y3TdpsrqwxRM+AN6/nDDwv45n5ZrUnQMsdBy5gIabioMKnQfWo9ew==} cpu: [arm64] os: [linux] + libc: [musl] '@oxc-resolver/binding-linux-ppc64-gnu@11.19.1': resolution: {integrity: sha512-vLmdNxWCdN7Uo5suays6A/+ywBby2PWBBPXctWPg5V0+eVuzsJxgAn6MMB4mPlshskYbppjpN2Zg83ArHze9gQ==} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-riscv64-gnu@11.19.1': resolution: {integrity: sha512-/b+WgR+VTSBxzgOhDO7TlMXC1ufPIMR6Vj1zN+/x+MnyXGW7prTLzU9eW85Aj7Th7CCEG9ArCbTeqxCzFWdg2w==} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-riscv64-musl@11.19.1': resolution: {integrity: sha512-YlRdeWb9j42p29ROh+h4eg/OQ3dTJlpHSa+84pUM9+p6i3djtPz1q55yLJhgW9XfDch7FN1pQ/Vd6YP+xfRIuw==} cpu: [riscv64] os: [linux] + libc: [musl] '@oxc-resolver/binding-linux-s390x-gnu@11.19.1': resolution: {integrity: sha512-EDpafVOQWF8/MJynsjOGFThcqhRHy417sRyLfQmeiamJ8qVhSKAn2Dn2VVKUGCjVB9C46VGjhNo7nOPUi1x6uA==} cpu: [s390x] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-x64-gnu@11.19.1': resolution: {integrity: sha512-NxjZe+rqWhr+RT8/Ik+5ptA3oz7tUw361Wa5RWQXKnfqwSSHdHyrw6IdcTfYuml9dM856AlKWZIUXDmA9kkiBQ==} cpu: [x64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-x64-musl@11.19.1': resolution: {integrity: sha512-cM/hQwsO3ReJg5kR+SpI69DMfvNCp+A/eVR4b4YClE5bVZwz8rh2Nh05InhwI5HR/9cArbEkzMjcKgTHS6UaNw==} cpu: [x64] os: [linux] + libc: [musl] '@oxc-resolver/binding-openharmony-arm64@11.19.1': resolution: {integrity: sha512-QF080IowFB0+9Rh6RcD19bdgh49BpQHUW5TajG1qvWHvmrQznTZZjYlgE2ltLXyKY+qs4F/v5xuX1XS7Is+3qA==} @@ -2622,17 +2654,23 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@rolldown/binding-android-arm64@1.0.0': + resolution: {integrity: sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + '@rolldown/binding-android-arm64@1.0.0-beta.57': resolution: {integrity: sha512-GoOVDy8bjw9z1K30Oo803nSzXJS/vWhFijFsW3kzvZCO8IZwFnNa6pGctmbbJstKl3Fv6UBwyjJQN6msejW0IQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-android-arm64@1.0.0-rc.17': - resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==} + '@rolldown/binding-darwin-arm64@1.0.0': + resolution: {integrity: sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] - os: [android] + os: [darwin] '@rolldown/binding-darwin-arm64@1.0.0-beta.57': resolution: {integrity: sha512-9c4FOhRGpl+PX7zBK5p17c5efpF9aSpTPgyigv57hXf5NjQUaJOOiejPLAtFiKNBIfm5Uu6yFkvLKzOafNvlTw==} @@ -2640,10 +2678,10 @@ packages: cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-arm64@1.0.0-rc.17': - resolution: {integrity: sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==} + '@rolldown/binding-darwin-x64@1.0.0': + resolution: {integrity: sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] + cpu: [x64] os: [darwin] '@rolldown/binding-darwin-x64@1.0.0-beta.57': @@ -2652,11 +2690,11 @@ packages: cpu: [x64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.17': - resolution: {integrity: sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==} + '@rolldown/binding-freebsd-x64@1.0.0': + resolution: {integrity: sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] - os: [darwin] + os: [freebsd] '@rolldown/binding-freebsd-x64@1.0.0-beta.57': resolution: {integrity: sha512-uA9kG7+MYkHTbqwv67Tx+5GV5YcKd33HCJIi0311iYBd25yuwyIqvJfBdt1VVB8tdOlyTb9cPAgfCki8nhwTQg==} @@ -2664,11 +2702,11 @@ packages: cpu: [x64] os: [freebsd] - '@rolldown/binding-freebsd-x64@1.0.0-rc.17': - resolution: {integrity: sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0': + resolution: {integrity: sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] + cpu: [arm] + os: [linux] '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.57': resolution: {integrity: sha512-3KkS0cHsllT2T+Te+VZMKHNw6FPQihYsQh+8J4jkzwgvAQpbsbXmrqhkw3YU/QGRrD8qgcOvBr6z5y6Jid+rmw==} @@ -2676,71 +2714,81 @@ packages: cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': - resolution: {integrity: sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==} + '@rolldown/binding-linux-arm64-gnu@1.0.0': + resolution: {integrity: sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] + cpu: [arm64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.57': resolution: {integrity: sha512-A3/wu1RgsHhqP3rVH2+sM81bpk+Qd2XaHTl8LtX5/1LNR7QVBFBCpAoiXwjTdGnI5cMdBVi7Z1pi52euW760Fw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': - resolution: {integrity: sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==} + '@rolldown/binding-linux-arm64-musl@1.0.0': + resolution: {integrity: sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@rolldown/binding-linux-arm64-musl@1.0.0-beta.57': resolution: {integrity: sha512-d0kIVezTQtazpyWjiJIn5to8JlwfKITDqwsFv0Xc6s31N16CD2PC/Pl2OtKgS7n8WLOJbfqgIp5ixYzTAxCqMg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': - resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==} + '@rolldown/binding-linux-ppc64-gnu@1.0.0': + resolution: {integrity: sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] + cpu: [ppc64] os: [linux] + libc: [glibc] - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': - resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==} + '@rolldown/binding-linux-s390x-gnu@1.0.0': + resolution: {integrity: sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] + cpu: [s390x] os: [linux] + libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': - resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==} + '@rolldown/binding-linux-x64-gnu@1.0.0': + resolution: {integrity: sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] + cpu: [x64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.0.0-beta.57': resolution: {integrity: sha512-E199LPijo98yrLjPCmETx8EF43sZf9t3guSrLee/ej1rCCc3zDVTR4xFfN9BRAapGVl7/8hYqbbiQPTkv73kUg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': - resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==} + '@rolldown/binding-linux-x64-musl@1.0.0': + resolution: {integrity: sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@rolldown/binding-linux-x64-musl@1.0.0-beta.57': resolution: {integrity: sha512-++EQDpk/UJ33kY/BNsh7A7/P1sr/jbMuQ8cE554ZIy+tCUWCivo9zfyjDUoiMdnxqX6HLJEqqGnbGQOvzm2OMQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': - resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==} + '@rolldown/binding-openharmony-arm64@1.0.0': + resolution: {integrity: sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] + cpu: [arm64] + os: [openharmony] '@rolldown/binding-openharmony-arm64@1.0.0-beta.57': resolution: {integrity: sha512-voDEBcNqxbUv/GeXKFtxXVWA+H45P/8Dec4Ii/SbyJyGvCqV1j+nNHfnFUIiRQ2Q40DwPe/djvgYBs9PpETiMA==} @@ -2748,21 +2796,21 @@ packages: cpu: [arm64] os: [openharmony] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': - resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==} + '@rolldown/binding-wasm32-wasi@1.0.0': + resolution: {integrity: sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] + cpu: [wasm32] '@rolldown/binding-wasm32-wasi@1.0.0-beta.57': resolution: {integrity: sha512-bRhcF7NLlCnpkzLVlVhrDEd0KH22VbTPkPTbMjlYvqhSmarxNIq5vtlQS8qmV7LkPKHrNLWyJW/V/sOyFba26Q==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': - resolution: {integrity: sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==} + '@rolldown/binding-win32-arm64-msvc@1.0.0': + resolution: {integrity: sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] + cpu: [arm64] + os: [win32] '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.57': resolution: {integrity: sha512-rnDVGRks2FQ2hgJ2g15pHtfxqkGFGjJQUDWzYznEkE8Ra2+Vag9OffxdbJMZqBWXHVM0iS4dv8qSiEn7bO+n1Q==} @@ -2770,10 +2818,10 @@ packages: cpu: [arm64] os: [win32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': - resolution: {integrity: sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==} + '@rolldown/binding-win32-x64-msvc@1.0.0': + resolution: {integrity: sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] + cpu: [x64] os: [win32] '@rolldown/binding-win32-x64-msvc@1.0.0-beta.57': @@ -2782,18 +2830,12 @@ packages: cpu: [x64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': - resolution: {integrity: sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] + '@rolldown/pluginutils@1.0.0': + resolution: {integrity: sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==} '@rolldown/pluginutils@1.0.0-beta.57': resolution: {integrity: sha512-aQNelgx14tGA+n2tNSa9x6/jeoCL9fkDeCei7nOKnHx0fEFRRMu5ReiITo+zZD5TzWDGGRjbSYCs93IfRIyTuQ==} - '@rolldown/pluginutils@1.0.0-rc.17': - resolution: {integrity: sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==} - '@rolldown/pluginutils@1.0.0-rc.3': resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} @@ -2831,66 +2873,79 @@ packages: resolution: {integrity: sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.60.3': resolution: {integrity: sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.60.3': resolution: {integrity: sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.60.3': resolution: {integrity: sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.3': resolution: {integrity: sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.3': resolution: {integrity: sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.60.3': resolution: {integrity: sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.3': resolution: {integrity: sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.60.3': resolution: {integrity: sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.60.3': resolution: {integrity: sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.60.3': resolution: {integrity: sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.3': resolution: {integrity: sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.60.3': resolution: {integrity: sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.60.3': resolution: {integrity: sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==} @@ -3002,24 +3057,28 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [glibc] '@swc/core-linux-arm64-musl@1.15.8': resolution: {integrity: sha512-koiCqL09EwOP1S2RShCI7NbsQuG6r2brTqUYE7pV7kZm9O17wZ0LSz22m6gVibpwEnw8jI3IE1yYsQTVpluALw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [musl] '@swc/core-linux-x64-gnu@1.15.8': resolution: {integrity: sha512-4p6lOMU3bC+Vd5ARtKJ/FxpIC5G8v3XLoPEZ5s7mLR8h7411HWC/LmTXDHcrSXRC55zvAVia1eldy6zDLz8iFQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [glibc] '@swc/core-linux-x64-musl@1.15.8': resolution: {integrity: sha512-z3XBnbrZAL+6xDGAhJoN4lOueIxC/8rGrJ9tg+fEaeqLEuAtHSW2QHDHxDwkxZMjuF/pZ6MUTjHjbp8wLbuRLA==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [musl] '@swc/core-win32-arm64-msvc@1.15.8': resolution: {integrity: sha512-djQPJ9Rh9vP8GTS/Df3hcc6XP6xnG5c8qsngWId/BLA9oX6C7UzCPAn74BG/wGb9a6j4w3RINuoaieJB3t+7iQ==} @@ -3098,24 +3157,28 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.3.0': resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.3.0': resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.3.0': resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.3.0': resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} @@ -3461,41 +3524,49 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -5935,24 +6006,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -6971,13 +7046,13 @@ packages: vue-tsc: optional: true - rolldown@1.0.0-beta.57: - resolution: {integrity: sha512-lMMxcNN71GMsSko8RyeTaFoATHkCh4IWU7pYF73ziMYjhHZWfVesC6GQ+iaJCvZmVjvgSks9Ks1aaqEkBd8udg==} + rolldown@1.0.0: + resolution: {integrity: sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rolldown@1.0.0-rc.17: - resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==} + rolldown@1.0.0-beta.57: + resolution: {integrity: sha512-lMMxcNN71GMsSko8RyeTaFoATHkCh4IWU7pYF73ziMYjhHZWfVesC6GQ+iaJCvZmVjvgSks9Ks1aaqEkBd8udg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -7719,9 +7794,9 @@ packages: unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} - unrun@0.2.39: - resolution: {integrity: sha512-h9FxYVpztY/wwq+bauLOh6Y3CWu2IVeRLq5lxzneBiIU9Tn86OGp9xiQrGhnYspAmg5dzdY0Cc8+Y70kuTARCg==} - engines: {node: '>=20.19.0'} + unrun@0.2.38: + resolution: {integrity: sha512-vYFWSyX5Be408RX+CAd2Heh8egQRnGBWOQmZKwYPvMtTQNmRYoKdSyFIczYNxZEMqz0dJzSxp7xkcZGzvtkHyQ==} + engines: {node: ^22.13.0 || >=24.0.0} hasBin: true peerDependencies: synckit: ^0.11.11 @@ -9795,7 +9870,7 @@ snapshots: - typescript - verdaccio - '@nx/eslint@22.7.1(nws6jx42ess74hysb6wocorctq)': + '@nx/eslint@22.7.1(6acc8fb6600279b4ea6839117370fcf1)': dependencies: '@nx/devkit': 22.7.1(nx@22.7.1(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21))) '@nx/js': 22.7.1(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21))(nx@22.7.1(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21))) @@ -9913,10 +9988,10 @@ snapshots: '@nx/nx-win32-x64-msvc@22.7.1': optional: true - '@nx/playwright@22.7.1(ripdl44hm2ldxruzyi4asrenke)': + '@nx/playwright@22.7.1(cfecd908ccdf06af45448c55a0fea9d8)': dependencies: '@nx/devkit': 22.7.1(nx@22.7.1(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21))) - '@nx/eslint': 22.7.1(nws6jx42ess74hysb6wocorctq) + '@nx/eslint': 22.7.1(6acc8fb6600279b4ea6839117370fcf1) '@nx/js': 22.7.1(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21))(nx@22.7.1(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21))) minimatch: 10.2.4 tslib: 2.8.1 @@ -9967,7 +10042,7 @@ snapshots: semver: 7.8.0 tslib: 2.8.1 optionalDependencies: - '@nx/eslint': 22.7.1(nws6jx42ess74hysb6wocorctq) + '@nx/eslint': 22.7.1(6acc8fb6600279b4ea6839117370fcf1) vite: 7.3.2(@types/node@20.19.9)(jiti@2.4.2)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.9.0) vitest: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@20.19.9)(@vitest/coverage-v8@4.1.6)(jsdom@26.1.0)(vite@7.3.2(@types/node@20.19.9)(jiti@2.4.2)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.9.0)) transitivePeerDependencies: @@ -9989,7 +10064,7 @@ snapshots: picocolors: 1.1.1 tslib: 2.8.1 optionalDependencies: - '@nx/eslint': 22.7.1(nws6jx42ess74hysb6wocorctq) + '@nx/eslint': 22.7.1(6acc8fb6600279b4ea6839117370fcf1) '@nx/jest': 22.7.1(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21))(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(nx@22.7.1(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21)))(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.21))(@types/node@20.19.9)(typescript@5.8.3))(typescript@5.8.3) '@nx/playwright': 22.7.1(cfecd908ccdf06af45448c55a0fea9d8) '@nx/vite': 22.7.1(@babel/traverse@7.29.0)(@nx/eslint@22.7.1(6acc8fb6600279b4ea6839117370fcf1))(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21))(nx@22.7.1(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.8(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.8(@swc/helpers@0.5.21)))(typescript@5.8.3)(vite@7.3.2(@types/node@20.19.9)(jiti@2.4.2)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.6) @@ -10022,7 +10097,7 @@ snapshots: '@oxc-project/types@0.103.0': {} - '@oxc-project/types@0.127.0': {} + '@oxc-project/types@0.129.0': {} '@oxc-resolver/binding-android-arm-eabi@11.19.1': optional: true @@ -10410,70 +10485,77 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@rolldown/binding-android-arm64@1.0.0': + optional: true + '@rolldown/binding-android-arm64@1.0.0-beta.57': optional: true - '@rolldown/binding-android-arm64@1.0.0-rc.17': + '@rolldown/binding-darwin-arm64@1.0.0': optional: true '@rolldown/binding-darwin-arm64@1.0.0-beta.57': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.17': + '@rolldown/binding-darwin-x64@1.0.0': optional: true '@rolldown/binding-darwin-x64@1.0.0-beta.57': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.17': + '@rolldown/binding-freebsd-x64@1.0.0': optional: true '@rolldown/binding-freebsd-x64@1.0.0-beta.57': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.17': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0': optional: true '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.57': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': + '@rolldown/binding-linux-arm64-gnu@1.0.0': optional: true '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.57': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': + '@rolldown/binding-linux-arm64-musl@1.0.0': optional: true '@rolldown/binding-linux-arm64-musl@1.0.0-beta.57': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': + '@rolldown/binding-linux-ppc64-gnu@1.0.0': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': + '@rolldown/binding-linux-s390x-gnu@1.0.0': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': + '@rolldown/binding-linux-x64-gnu@1.0.0': optional: true '@rolldown/binding-linux-x64-gnu@1.0.0-beta.57': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': + '@rolldown/binding-linux-x64-musl@1.0.0': optional: true '@rolldown/binding-linux-x64-musl@1.0.0-beta.57': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': + '@rolldown/binding-openharmony-arm64@1.0.0': optional: true '@rolldown/binding-openharmony-arm64@1.0.0-beta.57': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': + '@rolldown/binding-wasm32-wasi@1.0.0': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true '@rolldown/binding-wasm32-wasi@1.0.0-beta.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': @@ -10484,29 +10566,22 @@ snapshots: - '@emnapi/runtime' optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@rolldown/binding-win32-arm64-msvc@1.0.0': optional: true '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.57': optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': + '@rolldown/binding-win32-x64-msvc@1.0.0': optional: true '@rolldown/binding-win32-x64-msvc@1.0.0-beta.57': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': - optional: true + '@rolldown/pluginutils@1.0.0': {} '@rolldown/pluginutils@1.0.0-beta.57': {} - '@rolldown/pluginutils@1.0.0-rc.17': {} - '@rolldown/pluginutils@1.0.0-rc.3': {} '@rollup/rollup-android-arm-eabi@4.60.3': @@ -15454,6 +15529,27 @@ snapshots: transitivePeerDependencies: - oxc-resolver + rolldown@1.0.0: + dependencies: + '@oxc-project/types': 0.129.0 + '@rolldown/pluginutils': 1.0.0 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0 + '@rolldown/binding-darwin-arm64': 1.0.0 + '@rolldown/binding-darwin-x64': 1.0.0 + '@rolldown/binding-freebsd-x64': 1.0.0 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0 + '@rolldown/binding-linux-arm64-gnu': 1.0.0 + '@rolldown/binding-linux-arm64-musl': 1.0.0 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0 + '@rolldown/binding-linux-s390x-gnu': 1.0.0 + '@rolldown/binding-linux-x64-gnu': 1.0.0 + '@rolldown/binding-linux-x64-musl': 1.0.0 + '@rolldown/binding-openharmony-arm64': 1.0.0 + '@rolldown/binding-wasm32-wasi': 1.0.0 + '@rolldown/binding-win32-arm64-msvc': 1.0.0 + '@rolldown/binding-win32-x64-msvc': 1.0.0 + rolldown@1.0.0-beta.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): dependencies: '@oxc-project/types': 0.103.0 @@ -15476,27 +15572,6 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - rolldown@1.0.0-rc.17: - dependencies: - '@oxc-project/types': 0.127.0 - '@rolldown/pluginutils': 1.0.0-rc.17 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.17 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.17 - '@rolldown/binding-darwin-x64': 1.0.0-rc.17 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.17 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.17 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.17 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.17 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.17 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.17 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.17 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.17 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.17 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.17 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17 - rollup@4.60.3: dependencies: '@types/estree': 1.0.8 @@ -16220,7 +16295,7 @@ snapshots: tinyglobby: 0.2.16 tree-kill: 1.2.2 unconfig-core: 7.5.0 - unrun: 0.2.39(synckit@0.11.12) + unrun: 0.2.38(synckit@0.11.12) optionalDependencies: '@arethetypeswrong/core': 0.18.2 typescript: 5.8.3 @@ -16413,9 +16488,9 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - unrun@0.2.39(synckit@0.11.12): + unrun@0.2.38(synckit@0.11.12): dependencies: - rolldown: 1.0.0-rc.17 + rolldown: 1.0.0 optionalDependencies: synckit: 0.11.12 From f3255a2bf2e072ffbb3ec1647c5f8ab19ee2cc68 Mon Sep 17 00:00:00 2001 From: Omar Al Matar Date: Mon, 25 May 2026 15:31:14 +0400 Subject: [PATCH 3/7] refactor(auth): document concurrency invariants for _useSession and dispose() _useSession: explain that concurrent callers can both reach `__loadSession` because storage reads are idempotent and the only write path (refresh) is single-flighted downstream by `refreshingDeferred` in `_callRefreshToken`. No serialization is needed at this layer. dispose(): add a lifecycle caveat clarifying that in-flight refreshes are not aborted, so a disposed instance can still persist a rotated session to storage after `dispose()` returns. A subsequent `createClient` against the same `storageKey` will pick that session up. Notes the mitigation (await pending ops before dispose, or use a fresh `storageKey`). Doc-only changes; no runtime behaviour change. --- packages/core/auth-js/src/GoTrueClient.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/core/auth-js/src/GoTrueClient.ts b/packages/core/auth-js/src/GoTrueClient.ts index 41bc857758..a833c1960b 100644 --- a/packages/core/auth-js/src/GoTrueClient.ts +++ b/packages/core/auth-js/src/GoTrueClient.ts @@ -2691,7 +2691,10 @@ export default class GoTrueClient { this._debug('#_useSession', 'begin') try { - // the use of __loadSession here is the only correct use of the function! + // Concurrent callers may both reach __loadSession; storage reads are + // idempotent, and the only write path inside it (refresh) is + // single-flighted downstream by `refreshingDeferred` in + // `_callRefreshToken`. No serialization is needed at this layer. const result = await this.__loadSession() return await fn(result) @@ -4923,6 +4926,14 @@ export default class GoTrueClient { * cases. Any in-flight `fetch` calls continue to completion and may still * write to storage; dispose doesn't abort them or erase storage. * + * Lifecycle caveat: because in-flight refreshes are not aborted, a + * disposed instance can still persist a rotated session to storage after + * `dispose()` returns. A subsequent `createClient` against the same + * `storageKey` will pick up that session on its next read. If you need + * strict isolation between client lifecycles, await any pending auth + * operation before calling `dispose()` (or change the `storageKey` for + * the replacement client). + * * Safe to call repeatedly. * * @category Auth From 0ce6dd571205772e74ab3b9758813ff1de06e2ec Mon Sep 17 00:00:00 2001 From: Omar Al Matar Date: Mon, 25 May 2026 17:25:08 +0400 Subject: [PATCH 4/7] test(supabase): update lockAcquireTimeout tests for lockless contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two tests at `SupabaseAuthClient.test.ts:61-77` were asserting that `lockAcquireTimeout` is stored as a runtime field on the auth client (`expect((authClient as any).lockAcquireTimeout).toBe(30_000)`). That field no longer exists after the lockless refactor — the option is accepted by the type for backwards compatibility but is silently ignored at runtime because the client doesn't acquire a lock around auth operations. Rewrite both tests to verify the new contract: - `_initSupabaseAuthClient` accepts the option without throwing. - `createClient` accepts it through `auth.lockAcquireTimeout`, the auth client is still constructed, but the value is not stored as a runtime field (`toBeUndefined()`). Fixes the CI failure in `Supabase-JS Integration CI / Unit + Type Check` (2 failed, 102 passed → now 104 passed) on all three platforms. --- .../test/unit/SupabaseAuthClient.test.ts | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/core/supabase-js/test/unit/SupabaseAuthClient.test.ts b/packages/core/supabase-js/test/unit/SupabaseAuthClient.test.ts index 751057fe73..db3905d112 100644 --- a/packages/core/supabase-js/test/unit/SupabaseAuthClient.test.ts +++ b/packages/core/supabase-js/test/unit/SupabaseAuthClient.test.ts @@ -58,22 +58,26 @@ test('createClient gates passkey methods when auth.experimental.passkey is not s await expect(supa.auth.passkey.list()).rejects.toThrow(/experimental.*passkey/) }) -test('_initSupabaseAuthClient should pass through lockAcquireTimeout option', () => { +test('_initSupabaseAuthClient accepts the deprecated lockAcquireTimeout option without error', () => { const client = new SupabaseClient('https://example.supabase.com', 'supabaseKey') - const authClient = client['_initSupabaseAuthClient']( - { ...authSettings, lockAcquireTimeout: 30_000 }, - undefined, - undefined - ) - - expect((authClient as any).lockAcquireTimeout).toBe(30_000) + expect(() => + client['_initSupabaseAuthClient']( + { ...authSettings, lockAcquireTimeout: 30_000 }, + undefined, + undefined + ) + ).not.toThrow() }) -test('createClient should accept auth.lockAcquireTimeout and wire it to auth client', () => { +test('createClient accepts auth.lockAcquireTimeout for backwards compatibility but does not store it on the auth client', () => { const supa = new SupabaseClient('https://example.supabase.com', 'supabaseKey', { auth: { lockAcquireTimeout: 30_000 }, }) - expect((supa.auth as any).lockAcquireTimeout).toBe(30_000) + // The deprecated option is accepted by the type for backwards compatibility, + // but the auth client no longer acquires a lock around auth operations and + // therefore does not store this value as a runtime field. + expect((supa.auth as any).lockAcquireTimeout).toBeUndefined() + expect(supa.auth).toBeInstanceOf(SupabaseAuthClient) }) test('createClient should accept auth.skipAutoInitialize and wire it to auth client', async () => { From 3ca00998b8aef869cdd009cb3fb1ec4671dd330d Mon Sep 17 00:00:00 2001 From: Katerina Skroumpelou Date: Mon, 25 May 2026 17:22:10 +0300 Subject: [PATCH 5/7] refactor(auth): preserve custom lock as a legacy opt-in path --- packages/core/auth-js/src/GoTrueClient.ts | 560 +++++++++++++----- packages/core/auth-js/src/lib/types.ts | 34 +- .../auth-js/test/GoTrueClient.browser.test.ts | 12 +- .../core/auth-js/test/GoTrueClient.test.ts | 43 +- .../core/supabase-js/src/SupabaseClient.ts | 2 - packages/core/supabase-js/src/lib/types.ts | 27 +- .../test/unit/SupabaseAuthClient.test.ts | 34 +- 7 files changed, 501 insertions(+), 211 deletions(-) diff --git a/packages/core/auth-js/src/GoTrueClient.ts b/packages/core/auth-js/src/GoTrueClient.ts index a833c1960b..f3627276c2 100644 --- a/packages/core/auth-js/src/GoTrueClient.ts +++ b/packages/core/auth-js/src/GoTrueClient.ts @@ -56,6 +56,7 @@ import { validateExp, } from './lib/helpers' import { memoryLocalStorageAdapter } from './lib/local-storage' +import { LockAcquireTimeoutError, navigatorLock } from './lib/locks' import { polyfillGlobalThis } from './lib/polyfills' import { version } from './lib/version' @@ -92,6 +93,7 @@ import type { JWK, JwtHeader, JwtPayload, + LockFunc, MFAChallengeAndVerifyParams, MFAChallengeParams, MFAChallengePhoneParams, @@ -183,7 +185,7 @@ polyfillGlobalThis() // Make "globalThis" available const DEFAULT_OPTIONS: Omit< Required, - 'fetch' | 'storage' | 'userStorage' | 'lock' | 'lockAcquireTimeout' | 'suppressLockOptionWarning' + 'fetch' | 'storage' | 'userStorage' | 'lock' > = { url: GOTRUE_URL, storageKey: STORAGE_KEY, @@ -195,10 +197,21 @@ const DEFAULT_OPTIONS: Omit< debug: false, hasCustomAuthorizationHeader: false, throwOnError: false, + lockAcquireTimeout: 5000, // 5 seconds. Only used when a custom `lock` is supplied. TODO(v3): remove. skipAutoInitialize: false, experimental: {}, } +/** + * No-op lock used internally as a placeholder. Kept so older test setups that + * inject this exact reference do not break; new code never sees it because + * `this.lock` stays `null` when no custom lock is supplied (lockless path). + * TODO(v3): remove with the legacy lock path. + */ +async function lockNoOp(name: string, acquireTimeout: number, fn: () => Promise): Promise { + return await fn() +} + /** * Caches JWKS values for all clients created in the same environment. This is * especially useful for shared-memory execution environments such as Vercel's @@ -292,7 +305,20 @@ export default class GoTrueClient { protected hasCustomAuthorizationHeader = false protected suppressGetSessionWarning = false protected fetch: Fetch + /** + * Custom lock function passed via `settings.lock`. When non-null, every auth + * operation runs inside `_acquireLock`. When null (the default), the client + * uses its lockless coordination (refresh single-flight + commit guard). + * TODO(v3): remove along with the legacy lock path. + */ + protected lock: LockFunc | null = null + protected lockAcquired = false + protected pendingInLock: Promise[] = [] protected throwOnError: boolean + /** + * Only consulted when a custom `lock` is supplied. TODO(v3): remove. + */ + protected lockAcquireTimeout: number /** * Opt-in flags for experimental features. Defaults to an empty object. * See `GoTrueClientOptions.experimental`. @@ -367,23 +393,18 @@ export default class GoTrueClient { this.hasCustomAuthorizationHeader = settings.hasCustomAuthorizationHeader this.throwOnError = settings.throwOnError - // settings.lock and settings.lockAcquireTimeout are typed for backwards - // compatibility and have no effect at runtime. Warn once at construction - // when a non-null `lock` is supplied so callers who relied on a custom - // lock (typically React Native `processLock` or Node multi-process - // setups) discover the change instead of racing silently. Opt-out via - // `suppressLockOptionWarning: true`. - if (settings.lock != null && !settings.suppressLockOptionWarning) { - const lockWarning = - `${this._logPrefix()} The \`lock\` option is accepted for backwards ` + - `compatibility but is not invoked by the client. The auth client ` + - `coordinates refreshes itself and the server resolves cross-instance ` + - `races. Code that depended on a custom lock being called will not run. ` + - `Pass \`suppressLockOptionWarning: true\` to silence this warning.` - console.warn(lockWarning) - if (this.logDebugMessages) { - console.trace(lockWarning) - } + // Always wire `lockAcquireTimeout` even on the lockless path: consumers + // (including supabase-js tests) read it off the client to verify option + // flow-through. + this.lockAcquireTimeout = settings.lockAcquireTimeout + + // TODO(v3): remove. Legacy opt-in path preserved for backwards + // compatibility with callers passing a custom `lock` (typically React + // Native `processLock` or Node multi-process setups). When `settings.lock` + // is null the client uses its lockless coordination — no `navigator.locks` + // by default, no implicit `processLock`. + if (settings.lock != null) { + this.lock = settings.lock } if (!this.jwks) { @@ -518,6 +539,12 @@ export default class GoTrueClient { } this.initializePromise = (async () => { + if (this.lock != null) { + // TODO(v3): remove legacy lock path + return await this._acquireLock(this.lockAcquireTimeout, async () => { + return await this._initialize() + }) + } return await this._initialize() })() @@ -1403,6 +1430,13 @@ export default class GoTrueClient { async exchangeCodeForSession(authCode: string): Promise { await this.initializePromise + if (this.lock != null) { + // TODO(v3): remove legacy lock path + return this._acquireLock(this.lockAcquireTimeout, async () => { + return this._exchangeCodeForSession(authCode) + }) + } + return this._exchangeCodeForSession(authCode) } @@ -2440,6 +2474,13 @@ export default class GoTrueClient { async reauthenticate(): Promise { await this.initializePromise + if (this.lock != null) { + // TODO(v3): remove legacy lock path + return await this._acquireLock(this.lockAcquireTimeout, async () => { + return await this._reauthenticate() + }) + } + return await this._reauthenticate() } @@ -2655,11 +2696,95 @@ export default class GoTrueClient { async getSession() { await this.initializePromise + if (this.lock != null) { + // TODO(v3): remove legacy lock path + return await this._acquireLock(this.lockAcquireTimeout, async () => { + return this._useSession(async (result) => { + return result + }) + }) + } + return await this._useSession(async (result) => { return result }) } + /** + * Acquires a global lock based on the storage key. + * + * TODO(v3): remove along with the legacy lock path. Only called when + * `this.lock` is non-null (custom lock supplied via constructor). The + * default lockless path bypasses this entirely. + */ + private async _acquireLock(acquireTimeout: number, fn: () => Promise): Promise { + this._debug('#_acquireLock', 'begin', acquireTimeout) + + try { + if (this.lockAcquired) { + const last = this.pendingInLock.length + ? this.pendingInLock[this.pendingInLock.length - 1] + : Promise.resolve() + + const result = (async () => { + await last + return await fn() + })() + + this.pendingInLock.push( + (async () => { + try { + await result + } catch (_e) { + // we just care if it finished + } + })() + ) + + return result + } + + return await this.lock!(`lock:${this.storageKey}`, acquireTimeout, async () => { + this._debug('#_acquireLock', 'lock acquired for storage key', this.storageKey) + + try { + this.lockAcquired = true + + const result = fn() + + this.pendingInLock.push( + (async () => { + try { + await result + } catch (e: any) { + // we just care if it finished + } + })() + ) + + await result + + // keep draining the queue until there's nothing to wait on + while (this.pendingInLock.length) { + const waitOn = [...this.pendingInLock] + + await Promise.all(waitOn) + + this.pendingInLock.splice(0, waitOn.length) + } + + return await result + } finally { + this._debug('#_acquireLock', 'lock released for storage key', this.storageKey) + + this.lockAcquired = false + } + }) + } finally { + this._debug('#_acquireLock', 'end') + } + } + /** * Use instead of {@link #getSession} inside the library. Loads the session * via `__loadSession` (which may trigger a refresh if the access token is @@ -2730,6 +2855,11 @@ export default class GoTrueClient { > { this._debug('#__loadSession()', 'begin') + if (this.lock != null && !this.lockAcquired) { + // TODO(v3): remove. Only meaningful on the legacy lock path. + this._debug('#__loadSession()', 'used outside of an acquired lock!', new Error().stack) + } + try { let currentSession: Session | null = null @@ -2893,7 +3023,15 @@ export default class GoTrueClient { await this.initializePromise - const result = await this._getUser() + let result: UserResponse + if (this.lock != null) { + // TODO(v3): remove legacy lock path + result = await this._acquireLock(this.lockAcquireTimeout, async () => { + return await this._getUser() + }) + } else { + result = await this._getUser() + } if (result.data.user) { this.suppressGetSessionWarning = true @@ -3068,6 +3206,13 @@ export default class GoTrueClient { ): Promise { await this.initializePromise + if (this.lock != null) { + // TODO(v3): remove legacy lock path + return await this._acquireLock(this.lockAcquireTimeout, async () => { + return await this._updateUser(attributes, options) + }) + } + return await this._updateUser(attributes, options) } @@ -3255,6 +3400,13 @@ export default class GoTrueClient { }): Promise { await this.initializePromise + if (this.lock != null) { + // TODO(v3): remove legacy lock path + return await this._acquireLock(this.lockAcquireTimeout, async () => { + return await this._setSession(currentSession) + }) + } + return await this._setSession(currentSession) } @@ -3444,6 +3596,13 @@ export default class GoTrueClient { async refreshSession(currentSession?: { refresh_token: string }): Promise { await this.initializePromise + if (this.lock != null) { + // TODO(v3): remove legacy lock path + return await this._acquireLock(this.lockAcquireTimeout, async () => { + return await this._refreshSession(currentSession) + }) + } + return await this._refreshSession(currentSession) } @@ -3692,6 +3851,13 @@ export default class GoTrueClient { async signOut(options: SignOut = { scope: 'global' }): Promise<{ error: AuthError | null }> { await this.initializePromise + if (this.lock != null) { + // TODO(v3): remove legacy lock path + return await this._acquireLock(this.lockAcquireTimeout, async () => { + return await this._signOut(options) + }) + } + return await this._signOut(options) } @@ -3955,7 +4121,15 @@ export default class GoTrueClient { this.stateChangeEmitters.set(id, subscription) ;(async () => { await this.initializePromise - await this._emitInitialSession(id) + + if (this.lock != null) { + // TODO(v3): remove legacy lock path + await this._acquireLock(this.lockAcquireTimeout, async () => { + this._emitInitialSession(id) + }) + } else { + await this._emitInitialSession(id) + } })() return { data: { subscription } } @@ -4960,9 +5134,62 @@ export default class GoTrueClient { private async _autoRefreshTokenTick() { this._debug('#_autoRefreshTokenTick()', 'begin') - // Skip if a refresh is already in flight. `_callRefreshToken` also - // dedupes via the same field, so this is just a fast-path skip to - // avoid an unnecessary storage read. + if (this.lock != null) { + // TODO(v3): remove legacy lock path. Uses `_acquireLock(0, ...)` which + // throws `LockAcquireTimeoutError` immediately if the lock is held — + // that's the fail-fast skip path that lets the tick bail out instead + // of queuing behind a long-running operation. + try { + await this._acquireLock(0, async () => { + try { + const now = Date.now() + try { + return await this._useSession(async (result) => { + const { + data: { session }, + } = result + + if (!session || !session.refresh_token || !session.expires_at) { + this._debug('#_autoRefreshTokenTick()', 'no session') + return + } + + const expiresInTicks = Math.floor( + (session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION_MS + ) + + this._debug( + '#_autoRefreshTokenTick()', + `access token expires in ${expiresInTicks} ticks, a tick lasts ${AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${AUTO_REFRESH_TICK_THRESHOLD} ticks` + ) + + if (expiresInTicks <= AUTO_REFRESH_TICK_THRESHOLD) { + await this._callRefreshToken(session.refresh_token) + } + }) + } catch (e) { + console.error( + 'Auto refresh tick failed with error. This is likely a transient error.', + e + ) + } + } finally { + this._debug('#_autoRefreshTokenTick()', 'end') + } + }) + } catch (e) { + if (e instanceof LockAcquireTimeoutError) { + this._debug('auto refresh token tick lock not available') + } else { + throw e + } + } + return + } + + // Lockless default: skip if a refresh is already in flight. + // `_callRefreshToken` also dedupes via the same field; this is just a + // fast-path skip to avoid an unnecessary storage read. if (this.refreshingDeferred !== null) { this._debug('#_autoRefreshTokenTick()', 'refresh already in flight, skipping') return @@ -5060,13 +5287,26 @@ export default class GoTrueClient { // should be recovered await this.initializePromise - if (document.visibilityState !== 'visible') { - this._debug(methodName, 'visibilityState is no longer visible, skipping recovery') - return + if (this.lock != null) { + // TODO(v3): remove legacy lock path + await this._acquireLock(this.lockAcquireTimeout, async () => { + if (document.visibilityState !== 'visible') { + this._debug( + methodName, + 'acquired the lock to recover the session, but the browser visibilityState is no longer visible, aborting' + ) + return + } + await this._recoverAndRefresh() + }) + } else { + if (document.visibilityState !== 'visible') { + this._debug(methodName, 'visibilityState is no longer visible, skipping recovery') + return + } + // recover the session + await this._recoverAndRefresh() } - - // recover the session - await this._recoverAndRefresh() } } else if (document.visibilityState === 'hidden') { if (this.autoRefreshToken) { @@ -5198,76 +5438,84 @@ export default class GoTrueClient { params: MFAVerifyWebauthnParams ): Promise private async _verify(params: MFAVerifyParams): Promise { - try { - return await this._useSession(async (result) => { - const { data: sessionData, error: sessionError } = result - if (sessionError) { - return this._returnResult({ data: null, error: sessionError }) - } + const run = async (): Promise => { + try { + return await this._useSession(async (result) => { + const { data: sessionData, error: sessionError } = result + if (sessionError) { + return this._returnResult({ data: null, error: sessionError }) + } - const body: StrictOmit< - | Exclude - /** Exclude out the webauthn params from here because we're going to need to serialize them in the response */ - | Prettify< - StrictOmit & { - webauthn: Prettify< - StrictOmit & { - credential_response: PublicKeyCredentialJSON - } - > - } - >, - /* Exclude challengeId because the backend expects snake_case, and exclude factorId since it's passed in the path params */ - 'challengeId' | 'factorId' - > & { - challenge_id: string - } = { - challenge_id: params.challengeId, - ...('webauthn' in params - ? { - webauthn: { - ...params.webauthn, - credential_response: - params.webauthn.type === 'create' - ? serializeCredentialCreationResponse( - params.webauthn.credential_response as RegistrationCredential - ) - : serializeCredentialRequestResponse( - params.webauthn.credential_response as AuthenticationCredential - ), - }, - } - : { code: params.code }), - } + const body: StrictOmit< + | Exclude + /** Exclude out the webauthn params from here because we're going to need to serialize them in the response */ + | Prettify< + StrictOmit & { + webauthn: Prettify< + StrictOmit & { + credential_response: PublicKeyCredentialJSON + } + > + } + >, + /* Exclude challengeId because the backend expects snake_case, and exclude factorId since it's passed in the path params */ + 'challengeId' | 'factorId' + > & { + challenge_id: string + } = { + challenge_id: params.challengeId, + ...('webauthn' in params + ? { + webauthn: { + ...params.webauthn, + credential_response: + params.webauthn.type === 'create' + ? serializeCredentialCreationResponse( + params.webauthn.credential_response as RegistrationCredential + ) + : serializeCredentialRequestResponse( + params.webauthn.credential_response as AuthenticationCredential + ), + }, + } + : { code: params.code }), + } - const { data, error } = await _request( - this.fetch, - 'POST', - `${this.url}/factors/${params.factorId}/verify`, - { - body, - headers: this.headers, - jwt: sessionData?.session?.access_token, + const { data, error } = await _request( + this.fetch, + 'POST', + `${this.url}/factors/${params.factorId}/verify`, + { + body, + headers: this.headers, + jwt: sessionData?.session?.access_token, + } + ) + if (error) { + return this._returnResult({ data: null, error }) } - ) - if (error) { - return this._returnResult({ data: null, error }) - } - await this._saveSession({ - expires_at: Math.round(Date.now() / 1000) + data.expires_in, - ...data, - }) - await this._notifyAllSubscribers('MFA_CHALLENGE_VERIFIED', data) + await this._saveSession({ + expires_at: Math.round(Date.now() / 1000) + data.expires_in, + ...data, + }) + await this._notifyAllSubscribers('MFA_CHALLENGE_VERIFIED', data) - return this._returnResult({ data, error }) - }) - } catch (error) { - if (isAuthError(error)) { - return this._returnResult({ data: null, error }) + return this._returnResult({ data, error }) + }) + } catch (error) { + if (isAuthError(error)) { + return this._returnResult({ data: null, error }) + } + throw error } - throw error } + + if (this.lock != null) { + // TODO(v3): remove legacy lock path + return this._acquireLock(this.lockAcquireTimeout, run) + } + return run() } /** @@ -5283,78 +5531,86 @@ export default class GoTrueClient { params: MFAChallengeWebauthnParams ): Promise> private async _challenge(params: MFAChallengeParams): Promise { - try { - return await this._useSession(async (result) => { - const { data: sessionData, error: sessionError } = result - if (sessionError) { - return this._returnResult({ data: null, error: sessionError }) - } - - const response = (await _request( - this.fetch, - 'POST', - `${this.url}/factors/${params.factorId}/challenge`, - { - body: params, - headers: this.headers, - jwt: sessionData?.session?.access_token, + const run = async (): Promise => { + try { + return await this._useSession(async (result) => { + const { data: sessionData, error: sessionError } = result + if (sessionError) { + return this._returnResult({ data: null, error: sessionError }) } - )) as - | Exclude - /** The server will send `serialized` data, so we assert the serialized response */ - | AuthMFAChallengeWebauthnServerResponse - if (response.error) { - return response - } + const response = (await _request( + this.fetch, + 'POST', + `${this.url}/factors/${params.factorId}/challenge`, + { + body: params, + headers: this.headers, + jwt: sessionData?.session?.access_token, + } + )) as + | Exclude + /** The server will send `serialized` data, so we assert the serialized response */ + | AuthMFAChallengeWebauthnServerResponse - const { data } = response + if (response.error) { + return response + } - if (data.type !== 'webauthn') { - return { data, error: null } - } + const { data } = response + + if (data.type !== 'webauthn') { + return { data, error: null } + } - switch (data.webauthn.type) { - case 'create': - return { - data: { - ...data, - webauthn: { - ...data.webauthn, - credential_options: { - ...data.webauthn.credential_options, - publicKey: deserializeCredentialCreationOptions( - data.webauthn.credential_options.publicKey - ), + switch (data.webauthn.type) { + case 'create': + return { + data: { + ...data, + webauthn: { + ...data.webauthn, + credential_options: { + ...data.webauthn.credential_options, + publicKey: deserializeCredentialCreationOptions( + data.webauthn.credential_options.publicKey + ), + }, }, }, - }, - error: null, - } - case 'request': - return { - data: { - ...data, - webauthn: { - ...data.webauthn, - credential_options: { - ...data.webauthn.credential_options, - publicKey: deserializeCredentialRequestOptions( - data.webauthn.credential_options.publicKey - ), + error: null, + } + case 'request': + return { + data: { + ...data, + webauthn: { + ...data.webauthn, + credential_options: { + ...data.webauthn.credential_options, + publicKey: deserializeCredentialRequestOptions( + data.webauthn.credential_options.publicKey + ), + }, }, }, - }, - error: null, - } + error: null, + } + } + }) + } catch (error) { + if (isAuthError(error)) { + return this._returnResult({ data: null, error }) } - }) - } catch (error) { - if (isAuthError(error)) { - return this._returnResult({ data: null, error }) + throw error } - throw error } + + if (this.lock != null) { + // TODO(v3): remove legacy lock path + return this._acquireLock(this.lockAcquireTimeout, run) + } + return run() } /** diff --git a/packages/core/auth-js/src/lib/types.ts b/packages/core/auth-js/src/lib/types.ts index 212cdf9a60..2ddfdd2a2c 100644 --- a/packages/core/auth-js/src/lib/types.ts +++ b/packages/core/auth-js/src/lib/types.ts @@ -126,20 +126,19 @@ export type GoTrueClientOptions = { /* If debug messages are emitted. Can be used to inspect the behavior of the library. If set to a function, the provided function will be used instead of `console.log()` to perform the logging. */ debug?: boolean | ((message: string, ...args: any[]) => void) /** - * @deprecated Ignored. The client coordinates refreshes itself and the - * server handles cross-tab races, so you can safely remove this from your - * constructor options. Passing a non-null value emits a one-time - * `console.warn` at construction; suppress with `suppressLockOptionWarning`. + * Provide your own locking mechanism based on the environment. By default + * the client coordinates refreshes itself (single-flight via + * `refreshingDeferred` + commit guard) and relies on the GoTrue server to + * resolve cross-tab refresh races. Passing a custom lock opts into a + * legacy path that wraps every auth operation in your supplied lock — this + * path is preserved for backwards compatibility (typically React Native + * `processLock` or Node multi-process setups). + * + * @deprecated Custom locks still work in v2.x for backwards compatibility. + * The legacy lock path will be removed in v3 — drop this option from your + * constructor options before upgrading. */ lock?: LockFunc - /** - * Silence the construction-time `console.warn` emitted when a custom - * `lock` is passed. Set this only after confirming the auth client's - * built-in refresh coordination is sufficient for your runtime (typical - * web/RN/Node setups) and that no code path depends on the custom lock - * being invoked. - */ - suppressLockOptionWarning?: boolean /** * Set to "true" if there is a custom authorization header set globally. * @experimental @@ -151,9 +150,14 @@ export type GoTrueClientOptions = { */ throwOnError?: boolean /** - * @deprecated The client doesn't acquire a lock around auth operations, so - * this timeout has nothing to bound. You can safely remove it from your - * constructor options. + * The maximum time in milliseconds to wait for acquiring the custom lock + * supplied via the `lock` option. Only consulted when a custom `lock` is + * passed — the default lockless path doesn't use this timeout. + * + * @default 5000 + * + * @deprecated Only used by the legacy lock path. Will be removed in v3 + * along with the `lock` option. */ lockAcquireTimeout?: number diff --git a/packages/core/auth-js/test/GoTrueClient.browser.test.ts b/packages/core/auth-js/test/GoTrueClient.browser.test.ts index 22c72147d1..c35644016f 100644 --- a/packages/core/auth-js/test/GoTrueClient.browser.test.ts +++ b/packages/core/auth-js/test/GoTrueClient.browser.test.ts @@ -1196,9 +1196,13 @@ describe('Storage and User Storage Combinations', () => { }) }) -describe('Lockless backwards-compatibility: deprecated `lock` option', () => { - it('a custom `lock` function passed to the constructor is never invoked', async () => { - const customLock = jest.fn() +describe('Legacy lock opt-in: custom `lock` function is invoked when supplied', () => { + it('a custom `lock` function passed to the constructor IS invoked (legacy path)', async () => { + const customLock = jest + .fn() + .mockImplementation(async (_name: string, _timeout: number, fn: () => Promise) => + fn() + ) const client = new (require('../src/GoTrueClient').default)({ url: 'http://localhost:9999', @@ -1207,7 +1211,7 @@ describe('Lockless backwards-compatibility: deprecated `lock` option', () => { }) await client.initialize() - expect(customLock).not.toHaveBeenCalled() + expect(customLock).toHaveBeenCalled() }) }) diff --git a/packages/core/auth-js/test/GoTrueClient.test.ts b/packages/core/auth-js/test/GoTrueClient.test.ts index db1f1bb30e..a201d0b337 100644 --- a/packages/core/auth-js/test/GoTrueClient.test.ts +++ b/packages/core/auth-js/test/GoTrueClient.test.ts @@ -56,17 +56,19 @@ describe('GoTrueClient', () => { expect(debugSpy).toHaveBeenCalled() }) - test('accepts a custom lock implementation but never invokes it (deprecated)', async () => { - const customLock = jest.fn().mockResolvedValue(undefined) + test('should handle custom lock implementation', async () => { + const customLock = jest + .fn() + .mockImplementation(async (_name: string, _timeout: number, fn: () => Promise) => + fn() + ) const client = new GoTrueClient({ url: 'http://localhost:9999', lock: customLock, }) await client.initialize() - // The `lock` option is accepted for backwards compatibility but the - // client doesn't invoke it. - expect(customLock).not.toHaveBeenCalled() + expect(customLock).toHaveBeenCalled() }) test('should handle userStorage configuration', async () => { @@ -3420,11 +3422,29 @@ describe('SSO Authentication', () => { }) }) -describe('Lockless coordination', () => { - test('`lock` option is accepted but not invoked by the client', async () => { +describe('Lockless coordination (default) and legacy lock opt-in', () => { + test('default path: no custom `lock` supplied, _acquireLock is not invoked', async () => { + const client = new GoTrueClient({ + url: GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + autoRefreshToken: false, + persistSession: false, + }) + // @ts-expect-error access private _acquireLock for verification + const acquireSpy = jest.spyOn(client, '_acquireLock') + + await client.initialize() + await client.getSession() + + // No lock supplied → lockless coordination → _acquireLock is never called + expect(acquireSpy).not.toHaveBeenCalled() + }) + + test('legacy path: custom `lock` is invoked when supplied', async () => { const mockLock = jest .fn() - .mockImplementation(async (name: string, timeout: number, fn: () => Promise) => fn()) + .mockImplementation(async (name: string, timeout: number, fn: () => Promise) => + fn() + ) const client = new GoTrueClient({ url: GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, @@ -3436,12 +3456,11 @@ describe('Lockless coordination', () => { await client.initialize() await client.getSession() - // The `lock` option is accepted for backwards compatibility; the client - // doesn't route auth operations through it. - expect(mockLock).not.toHaveBeenCalled() + // Custom lock supplied → legacy path → mockLock IS called. + expect(mockLock).toHaveBeenCalled() }) - test('`lockAcquireTimeout` option is accepted but ignored', async () => { + test('`lockAcquireTimeout` option is accepted (lockless path ignores it; legacy uses it)', async () => { expect( () => new GoTrueClient({ diff --git a/packages/core/supabase-js/src/SupabaseClient.ts b/packages/core/supabase-js/src/SupabaseClient.ts index 5be388ba53..a38798d63b 100644 --- a/packages/core/supabase-js/src/SupabaseClient.ts +++ b/packages/core/supabase-js/src/SupabaseClient.ts @@ -591,7 +591,6 @@ export default class SupabaseClient< throwOnError, experimental, lockAcquireTimeout, - suppressLockOptionWarning, skipAutoInitialize, }: SupabaseAuthClientOptions, headers?: Record, @@ -617,7 +616,6 @@ export default class SupabaseClient< experimental, fetch, lockAcquireTimeout, - suppressLockOptionWarning, skipAutoInitialize, // auth checks if there is a custom authorizaiton header using this flag // so it knows whether to return an error when getUser is called with no session diff --git a/packages/core/supabase-js/src/lib/types.ts b/packages/core/supabase-js/src/lib/types.ts index 0f3eea89ab..c90e35e174 100644 --- a/packages/core/supabase-js/src/lib/types.ts +++ b/packages/core/supabase-js/src/lib/types.ts @@ -182,10 +182,14 @@ export type SupabaseClientOptions = { */ debug?: SupabaseAuthClientOptions['debug'] /** - * @deprecated The auth client coordinates refreshes itself and the server - * resolves cross-tab races, so any value forwarded through this option - * has no effect. You can safely remove it from your `createClient` - * options. + * Provide your own locking mechanism based on the environment. By default + * the auth client coordinates refreshes itself and the server resolves + * cross-tab races. Passing a custom `lock` opts into a legacy path that + * wraps every auth operation in your supplied lock. + * + * @deprecated Custom locks still work in v2.x for backwards compatibility. + * The legacy lock path will be removed in v3 — drop this option from your + * `createClient` options before upgrading. */ lock?: SupabaseAuthClientOptions['lock'] /** @@ -201,16 +205,15 @@ export type SupabaseClientOptions = { */ experimental?: SupabaseAuthClientOptions['experimental'] /** - * @deprecated The auth client doesn't acquire a lock around auth - * operations, so this timeout has nothing to bound. You can safely remove - * it from your `createClient` options. + * Maximum time in milliseconds to wait for acquiring the custom lock + * supplied via `lock`. Only consulted when a custom `lock` is passed. + * + * @default 5000 + * + * @deprecated Only used by the legacy lock path. Will be removed in v3 + * along with the `lock` option. */ lockAcquireTimeout?: SupabaseAuthClientOptions['lockAcquireTimeout'] - /** - * Silence the construction-time `console.warn` emitted when a custom - * `lock` is passed. Forwarded to the underlying auth client. - */ - suppressLockOptionWarning?: SupabaseAuthClientOptions['suppressLockOptionWarning'] /** * If true, skips automatic initialization in the auth client constructor. * Useful for SSR contexts where initialization timing must be controlled to diff --git a/packages/core/supabase-js/test/unit/SupabaseAuthClient.test.ts b/packages/core/supabase-js/test/unit/SupabaseAuthClient.test.ts index db3905d112..e2e02c067a 100644 --- a/packages/core/supabase-js/test/unit/SupabaseAuthClient.test.ts +++ b/packages/core/supabase-js/test/unit/SupabaseAuthClient.test.ts @@ -58,26 +58,32 @@ test('createClient gates passkey methods when auth.experimental.passkey is not s await expect(supa.auth.passkey.list()).rejects.toThrow(/experimental.*passkey/) }) -test('_initSupabaseAuthClient accepts the deprecated lockAcquireTimeout option without error', () => { +// The two tests below verify that `lockAcquireTimeout` flows from +// `createClient({ auth: { lockAcquireTimeout: ... }})` through to the +// constructed `GoTrueClient` instance. The field is `protected`, so we +// cast through `unknown` to a precise shape rather than using `as any`. +// The targeted cast is deliberate: when the legacy lock path is removed in +// v3 (see `// TODO(v3): remove …` markers in `GoTrueClient.ts` and the +// SDK Linear ticket for the v3 cleanup), `grep -rn "lockAcquireTimeout"` +// surfaces both the production code AND these tests together so the +// removal is mechanical. + +test('_initSupabaseAuthClient should pass through lockAcquireTimeout option', () => { const client = new SupabaseClient('https://example.supabase.com', 'supabaseKey') - expect(() => - client['_initSupabaseAuthClient']( - { ...authSettings, lockAcquireTimeout: 30_000 }, - undefined, - undefined - ) - ).not.toThrow() + const authClient = client['_initSupabaseAuthClient']( + { ...authSettings, lockAcquireTimeout: 30_000 }, + undefined, + undefined + ) + + expect((authClient as unknown as { lockAcquireTimeout: number }).lockAcquireTimeout).toBe(30_000) }) -test('createClient accepts auth.lockAcquireTimeout for backwards compatibility but does not store it on the auth client', () => { +test('createClient should accept auth.lockAcquireTimeout and wire it to auth client', () => { const supa = new SupabaseClient('https://example.supabase.com', 'supabaseKey', { auth: { lockAcquireTimeout: 30_000 }, }) - // The deprecated option is accepted by the type for backwards compatibility, - // but the auth client no longer acquires a lock around auth operations and - // therefore does not store this value as a runtime field. - expect((supa.auth as any).lockAcquireTimeout).toBeUndefined() - expect(supa.auth).toBeInstanceOf(SupabaseAuthClient) + expect((supa.auth as unknown as { lockAcquireTimeout: number }).lockAcquireTimeout).toBe(30_000) }) test('createClient should accept auth.skipAutoInitialize and wire it to auth client', async () => { From 80eec562ed57fe8502ce8325e8762b3f95fe22ec Mon Sep 17 00:00:00 2001 From: Katerina Skroumpelou Date: Mon, 25 May 2026 17:44:48 +0300 Subject: [PATCH 6/7] chore(repo): add lockless migration instructions --- .../migrations/lockless-coordination.md | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 packages/core/auth-js/migrations/lockless-coordination.md diff --git a/packages/core/auth-js/migrations/lockless-coordination.md b/packages/core/auth-js/migrations/lockless-coordination.md new file mode 100644 index 0000000000..6bfe4e2610 --- /dev/null +++ b/packages/core/auth-js/migrations/lockless-coordination.md @@ -0,0 +1,89 @@ +# Lockless auth coordination + +**Since:** v2.X.Y (update at release time) +**Action required by:** v3.0.0 + +`@supabase/auth-js` now coordinates session refreshes without a shared mutex by default. The legacy `lock` option is still honored when supplied, but it is deprecated and will be removed in v3. + +## What changed + +- **Default coordination is lockless.** A client constructed without a `lock` option no longer acquires `navigator.locks` or any in-process lock. In-tab concurrent refreshes are deduplicated via the pre-existing single-flight (`refreshingDeferred`); cross-tab refresh races are resolved by GoTrue's server-side parent-of-active mechanism on the v1 refresh-token path. +- **Commit guard inside `_callRefreshToken`.** The client snapshots storage before the rotated-token fetch and re-reads after. If a non-null pre-fetch snapshot was cleared between the two reads (typical case: a concurrent `signOut` ran `_removeSession`), the rotated tokens are discarded instead of being written back over the cleared storage. The discarded result resolves with `{ data: null, error: new AuthRefreshDiscardedError() }`. +- **New `AuthRefreshDiscardedError`** (with `isAuthRefreshDiscardedError` type guard). Surfaces through `refreshSession()` and `getSession()` results when the commit guard fires. Distinct from `AuthRetryableFetchError` (transient network) and `AuthApiError` (server rejection). +- **New `client.auth.dispose()`.** Tears down the auto-refresh interval, the `visibilitychange` listener, the `BroadcastChannel`, and registered `onAuthStateChange` subscribers. Idempotent. Designed for React Strict Mode and HMR cleanup hooks. In-flight `fetch` calls are not aborted — they run to completion. +- **`lock` and `lockAcquireTimeout` options.** Accepted and honored when supplied (legacy opt-in path); both are `@deprecated` and will be removed in v3. + +## Who is affected + +**Most callers: nothing to do.** If you do not pass a `lock` option to `createClient` / `GoTrueClient`, you are already on the lockless default path and will get the bug fixes and new APIs without any code change. + +**Callers passing a custom `lock`** (typically React Native `processLock`, Node multi-process setups with shared AsyncStorage, or a custom lock implementation): + +- v2.x (this release): your custom `lock` is still invoked exactly as before. The legacy `_acquireLock` machinery is preserved on an opt-in path gated by `settings.lock != null`. No code change required. +- v3.0.0 (planned): the `lock` and `lockAcquireTimeout` options will be removed entirely. Drop them from your client options before upgrading to v3. + +## New APIs worth knowing + +### `client.auth.dispose()` + +Tears down the client's background work in one call. Safe to call repeatedly. + +```ts +useEffect(() => { + const supabase = createClient(URL, KEY) + return () => { + supabase.auth.dispose() + } +}, []) +``` + +### `AuthRefreshDiscardedError` + +Returned from `refreshSession()` / `getSession()` when the commit guard discards a successfully-rotated session. + +```ts +import { isAuthRefreshDiscardedError } from '@supabase/auth-js' + +const { data, error } = await supabase.auth.refreshSession() +if (isAuthRefreshDiscardedError(error)) { + // A concurrent signOut cleared storage between fetch start and now. + // The rotated tokens were discarded; the SIGNED_OUT event already fired. + // Treat as a successful no-op — no need to retry. +} +``` + +## Behavior changes worth flagging + +- **`_autoRefreshTokenTick` may run concurrently with `signOut` / `setSession` / `getUser`** on the lockless default path. Previously the tick used `_acquireLock(0, ...)` which skipped whenever any auth op held the lock. The lockless equivalent only skips when `refreshingDeferred` is set. The commit guard keeps storage consistent under the new concurrency. The legacy lock opt-in path retains the old skip-on-any-lock behavior. +- **`onAuthStateChange` async callbacks** that call `getUser`, `setSession`, or read the session from inside the callback are now safe on the default path (previously deadlocked through the lock). One residual hazard remains: calling `refreshSession` (or anything routing through `_callRefreshToken`) from inside a `TOKEN_REFRESHED` handler still deadlocks via `refreshingDeferred`. The `@deprecated` marker on the async overload is kept with its reason updated to point at this specific case. +- **Subscriber timing on the default path:** subscribers stay awaited; same as before. What changes is that `signOut` no longer waits for an in-flight refresh's HTTP and continuation to finish before its own fetch goes out. Both fetches now run concurrently, and the commit guard keeps storage consistent. + +## Migration steps + +### If you do not pass a custom `lock` (most users) + +No action required for v2. No action required for v3. + +### If you pass a custom `lock` (e.g., React Native `processLock`) + +No action required for v2 — your lock continues to work. + +For v3 readiness: + +1. Remove `lock` and `lockAcquireTimeout` from your `createClient` / `GoTrueClient` options before upgrading to v3. +2. If you depended on cross-process serialization (e.g., Node multi-process with shared AsyncStorage), validate that the lockless coordination (in-tab single-flight + server parent-of-active) is sufficient for your runtime. The default is safe for the cases the lock was originally added to handle (cross-tab refresh races), since the server resolves them. + +```ts +// Before (v2.x, still works): +const supabase = createClient(URL, KEY, { + auth: { lock: processLock, lockAcquireTimeout: 5000 }, +}) + +// After (v3-ready): +const supabase = createClient(URL, KEY) +``` + +## Reference + +- Server-side parent-of-active mechanism: `internal/tokens/service.go:376-385` in the [supabase/auth](https://github.com/supabase/auth) repo (v1 branch, the `*models.RefreshToken` type assertion). When a request arrives with a revoked refresh token whose child is the currently-active token, the server returns the active token instead of rejecting — both tabs receive the same rotated token under DB row locking. +- `lib/locks.ts` exports (`navigatorLock`, `processLock`, `LockAcquireTimeoutError`, `NavigatorLockAcquireTimeoutError`, `ProcessLockAcquireTimeoutError`, `internals`) remain available for direct imports, marked `@deprecated`. Direct callers who use these exports outside the `GoTrueClient` constructor option are unaffected. From 1a98a572d32bfdaef9b11f95db0dfb028443e622 Mon Sep 17 00:00:00 2001 From: Katerina Skroumpelou Date: Tue, 26 May 2026 15:56:41 +0300 Subject: [PATCH 7/7] chore(auth): address comments --- packages/core/auth-js/src/GoTrueClient.ts | 57 +++++++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/packages/core/auth-js/src/GoTrueClient.ts b/packages/core/auth-js/src/GoTrueClient.ts index f3627276c2..e37231ee54 100644 --- a/packages/core/auth-js/src/GoTrueClient.ts +++ b/packages/core/auth-js/src/GoTrueClient.ts @@ -288,6 +288,14 @@ export default class GoTrueClient { protected autoRefreshTickTimeout: ReturnType | null = null protected visibilityChangedCallback: (() => Promise) | null = null protected refreshingDeferred: Deferred | null = null + /** + * Monotonic counter incremented at the top of `_removeSession`, before any + * `await`. The commit guard inside `_callRefreshToken` captures this value + * before `_saveSession` and re-checks it after, so a `signOut` that + * interleaves inside `_saveSession`'s storage-write awaits is still caught + * (the post-fetch storage snapshot alone misses that window). + */ + protected _sessionRemovalEpoch = 0 /** * Keeps track of the async client initialization. * When null or not yet resolved the auth state is `unknown` @@ -4524,7 +4532,10 @@ export default class GoTrueClient { * @param refreshToken A valid refresh token that was returned on login. */ private async _refreshAccessToken(refreshToken: string): Promise { - const debugName = `#_refreshAccessToken(${refreshToken.substring(0, 5)}...)` + // Refresh tokens are long-lived bearer credentials; do NOT include any + // fragment of the token in the debug tag, even when `debug: true` is + // enabled (logs may be forwarded to third-party services). + const debugName = `#_refreshAccessToken()` this._debug(debugName, 'begin') try { @@ -4745,7 +4756,10 @@ export default class GoTrueClient { return this.refreshingDeferred.promise } - const debugName = `#_callRefreshToken(${refreshToken.substring(0, 5)}...)` + // Refresh tokens are long-lived bearer credentials; do NOT include any + // fragment of the token in the debug tag, even when `debug: true` is + // enabled (logs may be forwarded to third-party services). + const debugName = `#_callRefreshToken()` this._debug(debugName, 'begin') @@ -4778,8 +4792,10 @@ export default class GoTrueClient { debugName, 'commit guard: storage changed since refresh started, discarding rotated tokens', { - startedWith: `${storedAtStart.refresh_token?.substring(0, 5)}...`, - nowHolds: storedAfter ? `${storedAfter.refresh_token?.substring(0, 5)}...` : null, + // Presence indicators only — never log refresh token fragments, + // even partial. Logs may be forwarded to third-party services. + startedWith: 'present', + nowHolds: storedAfter ? 'replaced' : 'cleared', } ) const discarded: CallRefreshTokenResult = { @@ -4790,7 +4806,35 @@ export default class GoTrueClient { return discarded } + // Second leg of the commit guard: close the TOCTOU window between the + // synchronous `storageChangedUnderUs` check and the actual storage + // writes inside `_saveSession`. A concurrent `signOut → _removeSession` + // can land inside `_saveSession`'s `await setItemAsync(...)` yields and + // clear storage just before we overwrite it. Capture the epoch BEFORE + // the save and re-check after; if it advanced, undo the write directly + // (do NOT call `_removeSession` — that would emit a duplicate + // SIGNED_OUT for the concurrent signOut that already fired one). + const epochBeforeSave = this._sessionRemovalEpoch + await this._saveSession(data.session) + + if (this._sessionRemovalEpoch !== epochBeforeSave) { + this._debug( + debugName, + 'commit guard (post-save): _removeSession ran during _saveSession, undoing write' + ) + await removeItemAsync(this.storage, this.storageKey) + if (this.userStorage) { + await removeItemAsync(this.userStorage, this.storageKey + '-user') + } + const discarded: CallRefreshTokenResult = { + data: null, + error: new AuthRefreshDiscardedError(), + } + this.refreshingDeferred.resolve(discarded) + return discarded + } + await this._notifyAllSubscribers('TOKEN_REFRESHED', data.session) const result = { data: data.session, error: null } @@ -4902,6 +4946,11 @@ export default class GoTrueClient { } private async _removeSession() { + // Bump synchronously, BEFORE any `await`, so that `_callRefreshToken`'s + // post-save check sees the increment whenever this method has started — + // even if it hasn't finished. Pairs with the epoch check in + // `_callRefreshToken`. See `_sessionRemovalEpoch` field doc. + this._sessionRemovalEpoch += 1 this._debug('#_removeSession()') this.suppressGetSessionWarning = false