From 63bedef77f85d7799f3bee63e42cf8d52cc9169e Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Tue, 15 Sep 2026 11:16:47 +0200 Subject: [PATCH] MFA Recovery codes: allow users to download their recovery codes (#50267) ## What kind of change does this PR introduce? After users have set up a new MFA (first or not), we must: - check whether recovery codes have already been generated - if there are none, generate recovery codes and display them, "forcing" users to copy them - if already generated, show them how many are still available > [!NOTE] > The _Delete my recovery codes_ button in last screenshot only appear on local and staging environments ## How to test - On an account that doesn't have recovery codes generated yet and has an MFA added - You should see an admonition suggesting to generate the codes ## Screenshots image image image ## Summary by CodeRabbit - **New Features** - Added support for generating, copying, and confirming MFA recovery codes. - Added recovery-code status visibility, including remaining and exhausted codes. - Added the ability to delete recovery codes with confirmation. - Added clear loading, success, and error states for recovery-code actions. - Recovery-code status refreshes after codes are generated or deleted. - **Bug Fixes** - Recovery-code notices now remain visible when all codes have been used. - Recovery-code dialogs can now be closed after generation errors. --- .../GenerateRecoveryCodesModal.test.tsx | 88 +++++++++ .../GenerateRecoveryCodesModal.tsx | 175 ++++++++++++++++++ .../UnenrollRecoveryCodesModal.tsx | 36 ++++ .../interfaces/Account/TOTPFactors/index.tsx | 23 ++- .../oauth-custom-providers-query.ts | 8 +- apps/studio/data/recovery-codes/keys.ts | 3 + .../recovery-codes-generate-mutation.ts | 50 +++++ .../recovery-codes-status-query.ts | 33 ++++ .../recovery-codes/recovery-codes-unenroll.ts | 48 +++++ packages/common/gotrue.ts | 3 + pnpm-lock.yaml | 102 +++++----- pnpm-workspace.yaml | 8 +- 12 files changed, 516 insertions(+), 61 deletions(-) create mode 100644 apps/studio/components/interfaces/Account/TOTPFactors/GenerateRecoveryCodesModal.test.tsx create mode 100644 apps/studio/components/interfaces/Account/TOTPFactors/GenerateRecoveryCodesModal.tsx create mode 100644 apps/studio/components/interfaces/Account/TOTPFactors/UnenrollRecoveryCodesModal.tsx create mode 100644 apps/studio/data/recovery-codes/keys.ts create mode 100644 apps/studio/data/recovery-codes/recovery-codes-generate-mutation.ts create mode 100644 apps/studio/data/recovery-codes/recovery-codes-status-query.ts create mode 100644 apps/studio/data/recovery-codes/recovery-codes-unenroll.ts diff --git a/apps/studio/components/interfaces/Account/TOTPFactors/GenerateRecoveryCodesModal.test.tsx b/apps/studio/components/interfaces/Account/TOTPFactors/GenerateRecoveryCodesModal.test.tsx new file mode 100644 index 00000000000..4ac820f96a1 --- /dev/null +++ b/apps/studio/components/interfaces/Account/TOTPFactors/GenerateRecoveryCodesModal.test.tsx @@ -0,0 +1,88 @@ +import { AuthError } from '@supabase/auth-js' +import { fireEvent, screen, waitFor } from '@testing-library/react' +import { describe, expect, test, vi } from 'vitest' + +import { GenerateRecoveryCodesModal } from './GenerateRecoveryCodesModal' +import { auth } from '@/lib/gotrue' +import { customRender } from '@/tests/lib/custom-render' + +const { mockCopyToClipboard } = vi.hoisted(() => ({ + mockCopyToClipboard: vi.fn((_value: string, callback: () => void) => { + return callback() + }), +})) +vi.mock('ui', async (importOriginal) => ({ + ...(await importOriginal()), + copyToClipboard: mockCopyToClipboard, +})) + +const codes = Array.from(Array(10).keys()).map((i) => `code_${i}`) + +describe('GenerateRecoveryCodesModal', () => { + test('generate the recovery codes and allow users to copy them', async () => { + vi.spyOn(auth.mfa.recoveryCodes, 'generate').mockResolvedValue({ + data: { + id: 'some_id', + total: 10, + codes, + type: 'recovery_code', + }, + error: null, + }) + customRender() + fireEvent.click(await screen.findByRole('button', { name: 'Generate recovery codes' })) + + // Codes are generated + await screen.findByText('Save your recovery codes') + await screen.findByText('code_0') + await screen.findByText('code_9') + + // Users have to copy the codes to close the modal, next click should fail if they managed to close it + expect(await screen.findAllByRole('button', { name: 'Close' })).toHaveLength(1) + fireEvent.click(await screen.findByRole('button', { name: 'Close' })) + fireEvent.click(await screen.findByRole('button', { name: 'Copy to clipboard' })) + await waitFor(() => + expect(screen.getByRole('checkbox', { name: 'I have copied the codes' })).toBeChecked() + ) + expect(mockCopyToClipboard).toHaveBeenCalledWith(codes.join('\n'), expect.any(Function)) + + // We should have 2 close buttons (header icon and a standard button) + expect(await screen.findAllByRole('button', { name: 'Close' })).toHaveLength(2) + fireEvent.click((await screen.findAllByRole('button', { name: 'Close' })).at(1)!) + await waitFor(() => expect(screen.queryByText('Save your recovery codes')).toBeNull()) + }) + + test('allow users to retry in case of error', async () => { + vi.spyOn(auth.mfa.recoveryCodes, 'generate') + .mockRejectedValueOnce({ + data: null, + error: new AuthError('boom'), + }) + .mockResolvedValue({ + data: { + id: 'some_id', + total: 10, + codes, + type: 'recovery_code', + }, + error: null, + }) + customRender() + fireEvent.click(await screen.findByRole('button', { name: 'Generate recovery codes' })) + await screen.findByText( + "We couldn't generate your recovery code. Please try again later or contact support if the problem persists." + ) + + // We should have 2 close buttons (header icon and a standard button) + expect(await screen.findAllByRole('button', { name: 'Close' })).toHaveLength(2) + fireEvent.click((await screen.findAllByRole('button', { name: 'Close' })).at(1)!) + await waitFor(() => expect(screen.queryByText('Save your recovery codes')).toBeNull()) + + // Retry + fireEvent.click(await screen.findByRole('button', { name: 'Generate recovery codes' })) + // Codes are generated + await screen.findByText('Save your recovery codes') + await screen.findByText('code_0') + await screen.findByText('code_9') + }) +}) diff --git a/apps/studio/components/interfaces/Account/TOTPFactors/GenerateRecoveryCodesModal.tsx b/apps/studio/components/interfaces/Account/TOTPFactors/GenerateRecoveryCodesModal.tsx new file mode 100644 index 00000000000..c9355a62c3e --- /dev/null +++ b/apps/studio/components/interfaces/Account/TOTPFactors/GenerateRecoveryCodesModal.tsx @@ -0,0 +1,175 @@ +import { AuthMFARecoveryCodesGenerateResponseData } from '@supabase/auth-js' +import { MutationStatus, useQueryClient } from '@tanstack/react-query' +import { useState } from 'react' +import { + Button, + Checkbox, + copyToClipboard, + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from 'ui' +import { Admonition } from 'ui-patterns/Admonition' + +import { recoveryCodeKeys } from '@/data/recovery-codes/keys' +import { useRecoveryCodesGenerateMutation } from '@/data/recovery-codes/recovery-codes-generate-mutation' + +export const GenerateRecoveryCodesModal = () => { + const queryClient = useQueryClient() + const recoveryCodesGenerateMutation = useRecoveryCodesGenerateMutation() + const [open, setOpen] = useState(false) + const [copied, setCopied] = useState(false) + const [copiedToClipboard, setCopiedToClipboard] = useState(false) + + return ( + { + // Prevent users from closing the dialog until they copied the codes + if (!open && !copied && recoveryCodesGenerateMutation.isSuccess) return + // Prevent users from closing the dialog while the mutation is running + if (recoveryCodesGenerateMutation.isPending) return + + setOpen(open) + if (!open) { + // Reset state + setCopied(false) + setCopiedToClipboard(false) + recoveryCodesGenerateMutation.reset() + queryClient.invalidateQueries({ queryKey: recoveryCodeKeys.status() }) + } + }} + > + + + + + + + + + +
+ setCopied(copied)} + /> +
+
+
+ {!recoveryCodesGenerateMutation.isPending ? ( + + {copiedToClipboard ? ( + + Codes copied to your clipboard. + + ) : null} + {copied || recoveryCodesGenerateMutation.isError ? ( + + + + ) : null} + + + + ) : null} +
+ + } + /> + ) +} + +const GenerateRecoveryCodesModalTitle = ({ status }: { status: MutationStatus }) => { + if (status === 'pending') { + return 'Generating your recovery codes...' + } + if (status === 'error') { + return 'An error occurred while generating your recovery code' + } + + return 'Save your recovery codes' +} + +const GenerateRecoveryCodesModalContent = ({ + codes, + copied, + status, + onCodesCopied, +}: { + codes: AuthMFARecoveryCodesGenerateResponseData['codes'] | undefined + copied: boolean + status: MutationStatus + onCodesCopied: (copied: boolean) => void +}) => { + if (status === 'error') { + return ( +

+ We couldn't generate your recovery code. Please try again later or contact support if the + problem persists. +

+ ) + } + + if (status === 'success') { + return ( +
+

Save your recovery codes somewhere safe.

+
+          
+            {codes?.map((code) => (
+              {code}
+            ))}
+          
+        
+
+ onCodesCopied(checked === true)} + /> + +
+
+ ) + } + + return null +} diff --git a/apps/studio/components/interfaces/Account/TOTPFactors/UnenrollRecoveryCodesModal.tsx b/apps/studio/components/interfaces/Account/TOTPFactors/UnenrollRecoveryCodesModal.tsx new file mode 100644 index 00000000000..a86f4e914bc --- /dev/null +++ b/apps/studio/components/interfaces/Account/TOTPFactors/UnenrollRecoveryCodesModal.tsx @@ -0,0 +1,36 @@ +import { useState } from 'react' +import { Button } from 'ui' + +import { TextConfirmModal } from '@/components/ui/TextConfirmModalWrapper' +import { useRecoveryCodesUnenrollMutation } from '@/data/recovery-codes/recovery-codes-unenroll' + +export const UnenrollRecoveryCodesModal = () => { + const [showConfirm, setShowConfirm] = useState(false) + const recoveryCodesGenerateMutation = useRecoveryCodesUnenrollMutation({ + onSuccess: () => { + setShowConfirm(false) + }, + }) + + return ( + <> + + recoveryCodesGenerateMutation.mutate()} + onCancel={() => setShowConfirm(false)} + > +

+ Your recovery codes won't work anymore. Make sure to regenerate them if needed. +

+
+ + ) +} diff --git a/apps/studio/components/interfaces/Account/TOTPFactors/index.tsx b/apps/studio/components/interfaces/Account/TOTPFactors/index.tsx index 83023b5c6f1..8adc187289f 100644 --- a/apps/studio/components/interfaces/Account/TOTPFactors/index.tsx +++ b/apps/studio/components/interfaces/Account/TOTPFactors/index.tsx @@ -1,3 +1,4 @@ +import { useFlag } from 'common' import dayjs from 'dayjs' import { Plus } from 'lucide-react' import { useState } from 'react' @@ -16,18 +17,27 @@ import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { AddNewFactorModal } from './AddNewFactorModal' import DeleteFactorModal from './DeleteFactorModal' +import { GenerateRecoveryCodesModal } from './GenerateRecoveryCodesModal' +import { UnenrollRecoveryCodesModal } from './UnenrollRecoveryCodesModal' import { AlertError } from '@/components/ui/AlertError' import { useMfaListFactorsQuery } from '@/data/profile/mfa-list-factors-query' -import { DATETIME_FORMAT } from '@/lib/constants' +import { useRecoveryCodesStatusQuery } from '@/data/recovery-codes/recovery-codes-status-query' +import { DATETIME_FORMAT, IS_STAGING_OR_LOCAL } from '@/lib/constants' export const TOTPFactors = () => { const [isAddNewFactorOpen, setIsAddNewFactorOpen] = useState(false) const [factorToBeDeleted, setFactorToBeDeleted] = useState(null) const { data, isPending: isLoading, isError, isSuccess, error } = useMfaListFactorsQuery() + const enableAuthRecoveryCodes = useFlag('enableAuthRecoveryCodes') const totpFactors = data?.totp ?? [] const canAddApp = isSuccess && totpFactors.length < 2 const shouldShowLockoutWarning = isSuccess && totpFactors.length === 1 + const shouldVerifyRecoveryCodes = enableAuthRecoveryCodes && totpFactors.length === 1 + + const { data: recoveryCodesStatus } = useRecoveryCodesStatusQuery({ + enabled: shouldVerifyRecoveryCodes, + }) const handleAddNewApp = () => setIsAddNewFactorOpen(true) @@ -51,6 +61,15 @@ export const TOTPFactors = () => { )} + {recoveryCodesStatus?.status === 'unenrolled' && } + {recoveryCodesStatus?.status === 'available' && ( + } + /> + )} {shouldShowLockoutWarning && ( {

))} diff --git a/apps/studio/data/oauth-custom-providers/oauth-custom-providers-query.ts b/apps/studio/data/oauth-custom-providers/oauth-custom-providers-query.ts index 0221a01d0a2..f34b91ef5f3 100644 --- a/apps/studio/data/oauth-custom-providers/oauth-custom-providers-query.ts +++ b/apps/studio/data/oauth-custom-providers/oauth-custom-providers-query.ts @@ -25,14 +25,14 @@ export async function getOAuthCustomProviders({ const { data, error } = await supabaseClient.auth.admin.customProviders.listProviders() if (error) { - let newError = error // Non-JSON responses from the API indicate custom providers aren't enabled. // Different browsers/SDK versions produce different JSON parse error messages, // so we check broadly for JSON parse indicators. - if (/JSON\.parse|Unexpected token|unexpected.*character/i.test(newError.message)) { - newError = new AuthError('Custom providers are not enabled for this project') + if (/JSON\.parse|Unexpected token|unexpected.*character/i.test(error.message)) { + handleError(new AuthError('Custom providers are not enabled for this project')) + } else { + handleError(error) } - handleError(newError) } return data.providers as CustomOAuthProvider[] } diff --git a/apps/studio/data/recovery-codes/keys.ts b/apps/studio/data/recovery-codes/keys.ts new file mode 100644 index 00000000000..17c81f78324 --- /dev/null +++ b/apps/studio/data/recovery-codes/keys.ts @@ -0,0 +1,3 @@ +export const recoveryCodeKeys = { + status: () => ['recovery-code-status-query'] as const, +} diff --git a/apps/studio/data/recovery-codes/recovery-codes-generate-mutation.ts b/apps/studio/data/recovery-codes/recovery-codes-generate-mutation.ts new file mode 100644 index 00000000000..336ff378600 --- /dev/null +++ b/apps/studio/data/recovery-codes/recovery-codes-generate-mutation.ts @@ -0,0 +1,50 @@ +import type { + AuthMFARecoveryCodesGenerateResponse, + MFARecoveryCodesGenerateParams, +} from '@supabase/auth-js' +import { useMutation } from '@tanstack/react-query' +import { toast } from 'sonner' + +import { captureCriticalError } from '@/lib/error-reporting' +import { auth } from '@/lib/gotrue' +import { UseCustomMutationOptions } from '@/types' + +export const recoveryCodesGenerate = async (params: MFARecoveryCodesGenerateParams) => { + const { error, data } = await auth.mfa.recoveryCodes.generate(params) + if (error) throw error + return data +} + +type RecoveryCodesGenerateResponse = NonNullable +type RecoveryCodesGenerateError = NonNullable + +export const useRecoveryCodesGenerateMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseCustomMutationOptions< + RecoveryCodesGenerateResponse, + RecoveryCodesGenerateError, + MFARecoveryCodesGenerateParams + >, + 'mutationFn' +> = {}) => { + return useMutation({ + mutationFn: (vars) => { + return recoveryCodesGenerate(vars) + }, + async onSuccess(data, variables, context) { + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to get recovery codes: ${data.message}`) + } else { + onError(data, variables, context) + } + captureCriticalError(data, 'recovery codes status') + }, + ...options, + }) +} diff --git a/apps/studio/data/recovery-codes/recovery-codes-status-query.ts b/apps/studio/data/recovery-codes/recovery-codes-status-query.ts new file mode 100644 index 00000000000..2fc38d31515 --- /dev/null +++ b/apps/studio/data/recovery-codes/recovery-codes-status-query.ts @@ -0,0 +1,33 @@ +import { useQuery } from '@tanstack/react-query' + +import { recoveryCodeKeys } from './keys' +import { handleError } from '@/data/fetchers' +import { auth } from '@/lib/gotrue' +import type { ResponseError, UseCustomQueryOptions } from '@/types' + +export async function getRecoveryCodesStatus() { + const { data, error } = await auth.mfa.recoveryCodes.getStatus() + // If users haven't enrolled yet, just return null so that UI does not retry the request and + // offer to enroll + if (error?.code === 'mfa_factor_not_found') { + return { status: 'unenrolled', data: null } as const + } + if (error) handleError(error) + + return { status: 'available', data } as const +} + +export type RecoveryCodesStatusData = Awaited> +export type RecoveryCodesError = ResponseError + +export const useRecoveryCodesStatusQuery = ({ + enabled = true, + ...options +}: UseCustomQueryOptions = {}) => + useQuery({ + queryKey: recoveryCodeKeys.status(), + queryFn: () => getRecoveryCodesStatus(), + staleTime: 1000 * 60 * 30, + enabled, + ...options, + }) diff --git a/apps/studio/data/recovery-codes/recovery-codes-unenroll.ts b/apps/studio/data/recovery-codes/recovery-codes-unenroll.ts new file mode 100644 index 00000000000..5bdc0b97981 --- /dev/null +++ b/apps/studio/data/recovery-codes/recovery-codes-unenroll.ts @@ -0,0 +1,48 @@ +import type { AuthMFAUnenrollResponse } from '@supabase/auth-js' +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { toast } from 'sonner' + +import { recoveryCodeKeys } from './keys' +import { captureCriticalError } from '@/lib/error-reporting' +import { auth } from '@/lib/gotrue' +import { UseCustomMutationOptions } from '@/types' + +export const recoveryCodesUnenroll = async () => { + const { error, data } = await auth.mfa.recoveryCodes.unenroll() + if (error) throw error + return data +} + +type RecoveryCodesUnenrollResponse = AuthMFAUnenrollResponse['data'] +type RecoveryCodesUnenrollError = NonNullable + +export const useRecoveryCodesUnenrollMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseCustomMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: () => { + return recoveryCodesUnenroll() + }, + async onSuccess(data, variables, context) { + await queryClient.invalidateQueries({ queryKey: recoveryCodeKeys.status() }) + + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to unenroll recovery codes: ${data.message}`) + } else { + onError(data, variables, context) + } + captureCriticalError(data, 'recovery codes unenroll') + }, + ...options, + }) +} diff --git a/packages/common/gotrue.ts b/packages/common/gotrue.ts index 49c16ad105e..9330099b52a 100644 --- a/packages/common/gotrue.ts +++ b/packages/common/gotrue.ts @@ -1,4 +1,5 @@ import { AuthClient, navigatorLock, User } from '@supabase/auth-js' + import { isBrowser } from './helpers' export const STORAGE_KEY = process.env.NEXT_PUBLIC_STORAGE_KEY || 'supabase.dashboard.auth.token' @@ -185,6 +186,8 @@ export const gotrueClient = new AuthClient({ detectSessionInUrl: shouldDetectSessionInUrl, debug: debug ? (persistedDebug ? logIndexedDB : true) : false, lock: navigatorLockEnabled ? debuggableNavigatorLock : undefined, + // TODO: Should this be enabled with a feature flag? + experimental: { recoveryCodes: true }, ...('localStorage' in globalThis ? { storage: globalThis.localStorage, userStorage: globalThis.localStorage } : null), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a68665a6fad..31c72d8db12 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,20 +16,20 @@ catalogs: specifier: ^10.59.0 version: 10.59.0 '@supabase/auth-js': - specifier: 2.112.4 - version: 2.112.4 + specifier: 2.116.0 + version: 2.116.0 '@supabase/postgrest-js': - specifier: 2.112.4 - version: 2.112.4 + specifier: 2.116.0 + version: 2.116.0 '@supabase/realtime-js': - specifier: 2.112.4 - version: 2.112.4 + specifier: 2.116.0 + version: 2.116.0 '@supabase/ssr': specifier: 0.10.2 version: 0.10.2 '@supabase/supabase-js': - specifier: 2.112.4 - version: 2.112.4 + specifier: 2.116.0 + version: 2.116.0 '@tanstack/react-router': specifier: ^1.169.2 version: 1.170.10 @@ -379,7 +379,7 @@ importers: version: 10.59.0(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(encoding@0.1.13)(next@15.5.25(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(@types/node@22.13.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react@19.2.6)(supports-color@8.1.1)(vite@8.2.1(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1)) '@supabase/supabase-js': specifier: 'catalog:' - version: 2.112.4(@opentelemetry/api@1.9.1) + version: 2.116.0(@opentelemetry/api@1.9.1) '@tanstack/react-query': specifier: ~5.83.0 version: 5.83.0(react@19.2.6) @@ -1040,7 +1040,7 @@ importers: version: 1.0.32(@types/node@22.13.14)(supports-color@8.1.1) '@supabase/auth-js': specifier: 'catalog:' - version: 2.112.4 + version: 2.116.0 '@supabase/mcp-server-supabase': specifier: ^0.12.0 version: 0.12.0(@modelcontextprotocol/server@2.0.0)(zod@3.25.76) @@ -1049,13 +1049,13 @@ importers: version: link:../../packages/pg-meta '@supabase/realtime-js': specifier: 'catalog:' - version: 2.112.4 + version: 2.116.0 '@supabase/shared-types': specifier: 0.1.95 version: 0.1.95 '@supabase/supabase-js': specifier: 'catalog:' - version: 2.112.4(@opentelemetry/api@1.9.1) + version: 2.116.0(@opentelemetry/api@1.9.1) '@tanstack/react-devtools': specifier: ^0.10.3 version: 0.10.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(solid-js@1.9.13) @@ -1494,7 +1494,7 @@ importers: version: 0.1.0(@opentelemetry/api@1.9.1) '@supabase/postgrest-js': specifier: 'catalog:' - version: 2.112.4 + version: 2.116.0 '@supabase/supa-mdx-lint': specifier: 0.2.6-alpha version: 0.2.6-alpha @@ -1636,10 +1636,10 @@ importers: version: 1.6.0 '@supabase/ssr': specifier: 'catalog:' - version: 0.10.2(@supabase/supabase-js@2.112.4(@opentelemetry/api@1.9.1)) + version: 0.10.2(@supabase/supabase-js@2.116.0(@opentelemetry/api@1.9.1)) '@supabase/supabase-js': specifier: 'catalog:' - version: 2.112.4(@opentelemetry/api@1.9.1) + version: 2.116.0(@opentelemetry/api@1.9.1) '@tanstack/react-router': specifier: 'catalog:' version: 1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -1786,10 +1786,10 @@ importers: version: 10.59.0(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(encoding@0.1.13)(next@15.5.25(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(@types/node@22.13.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react@19.2.6)(supports-color@8.1.1)(vite@8.2.1(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1)) '@supabase/ssr': specifier: 'catalog:' - version: 0.10.2(@supabase/supabase-js@2.112.4(@opentelemetry/api@1.9.1)) + version: 0.10.2(@supabase/supabase-js@2.116.0(@opentelemetry/api@1.9.1)) '@supabase/supabase-js': specifier: 'catalog:' - version: 2.112.4(@opentelemetry/api@1.9.1) + version: 2.116.0(@opentelemetry/api@1.9.1) '@typescript/native': specifier: 'catalog:' version: typescript@7.0.2 @@ -2066,13 +2066,13 @@ importers: dependencies: '@supabase/postgrest-js': specifier: 'catalog:' - version: 2.112.4 + version: 2.116.0 '@supabase/ssr': specifier: 'catalog:' - version: 0.10.2(@supabase/supabase-js@2.112.4(@opentelemetry/api@1.9.1)) + version: 0.10.2(@supabase/supabase-js@2.116.0(@opentelemetry/api@1.9.1)) '@supabase/supabase-js': specifier: 'catalog:' - version: 2.112.4(@opentelemetry/api@1.9.1) + version: 2.116.0(@opentelemetry/api@1.9.1) '@vueuse/core': specifier: ^14.1.0 version: 14.1.0(vue@3.5.35(typescript@6.0.2)) @@ -2156,7 +2156,7 @@ importers: version: 1.59.1 '@supabase/supabase-js': specifier: 'catalog:' - version: 2.112.4(@opentelemetry/api@1.9.1) + version: 2.116.0(@opentelemetry/api@1.9.1) cross-fetch: specifier: ^4.1.0 version: 4.1.0(encoding@0.1.13) @@ -2203,7 +2203,7 @@ importers: version: 0.18.5 '@supabase/supabase-js': specifier: 'catalog:' - version: 2.112.4(@opentelemetry/api@1.9.1) + version: 2.116.0(@opentelemetry/api@1.9.1) ai: specifier: 5.0.52 version: 5.0.52(zod@3.25.76) @@ -2300,10 +2300,10 @@ importers: dependencies: '@supabase/auth-js': specifier: 'catalog:' - version: 2.112.4 + version: 2.116.0 '@supabase/supabase-js': specifier: 'catalog:' - version: 2.112.4(@opentelemetry/api@1.9.1) + version: 2.116.0(@opentelemetry/api@1.9.1) '@types/dat.gui': specifier: ^0.7.12 version: 0.7.12 @@ -2781,7 +2781,7 @@ importers: version: 0.1.6(encoding@0.1.13)(supports-color@8.1.1) '@supabase/supabase-js': specifier: 'catalog:' - version: 2.112.4(@opentelemetry/api@1.9.1) + version: 2.116.0(@opentelemetry/api@1.9.1) '@tanstack/react-table': specifier: 'catalog:' version: 8.21.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -7958,8 +7958,8 @@ packages: '@supabase-labs/y-supabase@0.1.0': resolution: {integrity: sha512-1ItaKbcImgy7ueYfJcyVYihqCKi9RAee9MKBV8wcCqSMNsrWvw4ibhW8ai91kWsewsiWzIDXVPONgcUcxagk9g==} - '@supabase/auth-js@2.112.4': - resolution: {integrity: sha512-z8DesgwLzKM5PiT0yNmJU8VJyh1zAhYi+20Z7drdJQLXg/wWW4yGt/un+He5ERYUo94Vz66t5aeyr1DIDemI5A==} + '@supabase/auth-js@2.116.0': + resolution: {integrity: sha512-Cmosty12gyKGK9N3bQb+lMmuAFev5nmUzaR1AsmZHqKOAGzqX1VQzmp49CNPwOx/pw0H9Qqk4rs9yhwTlKpfDg==} engines: {node: '>=22.0.0'} '@supabase/cli-darwin-arm64@2.114.0': @@ -8006,8 +8006,8 @@ packages: cpu: [x64] os: [win32] - '@supabase/functions-js@2.112.4': - resolution: {integrity: sha512-DQ0aVH8wSQAccVqNoEkec62qCu2QRNyoGN53RqsVZ1k6F1zq4/v8scrlR6LNT2RJmT97apiTmORijPVhErCS2g==} + '@supabase/functions-js@2.116.0': + resolution: {integrity: sha512-E+VOc2QDcni/fySqkBFiZhnoB3SGydEdZgFI6/dEAGAHx6yEhB46TN9qb2wXs+E+RSzOBV0R6dasiSlw4xlZAA==} engines: {node: '>=22.0.0'} '@supabase/mcp-server-supabase@0.12.0': @@ -8026,12 +8026,12 @@ packages: '@supabase/phoenix@0.4.5': resolution: {integrity: sha512-aAn9H9ovVyeApKy11OWOrrOGq8DV68yWeH4ud2lN9fzn4aO8Zb5GLL9m1pUg9nLqIcT+ZDfAcsZe0E/nqdv2lw==} - '@supabase/postgrest-js@2.112.4': - resolution: {integrity: sha512-uaubtPSeg2TR4wrtfQoQWgkTAe+a0qWX2KhmwvTfNl5mGN9+U7owiJt6abk3o/V6O899PSRD1yzxs5RlF4xTug==} + '@supabase/postgrest-js@2.116.0': + resolution: {integrity: sha512-kGpVZTDHxFTJS3tu+rU0iTAZ+4U0bcLVjxwCk8f3gRhjw3qdCZjTBlgYvc4kGH2XccmAzbkKwXL/mrNHMGSc+A==} engines: {node: '>=22.0.0'} - '@supabase/realtime-js@2.112.4': - resolution: {integrity: sha512-vZ+j079SKrM0Xiq7MJCvQKLDpaH2kfKfLY68xuQE1sqsCsMmx1CyrDBJHsxZ3cX01VOs5SI9igmoZAF3BmdZxw==} + '@supabase/realtime-js@2.116.0': + resolution: {integrity: sha512-MHAnlXxi2s6yiJsZsQMfs2B3RFxeVfQWxerqYhIMqcCQV/FuY3LIeouPEkXw/ah7wUWMLYwempF9MOCUScyddg==} engines: {node: '>=22.0.0'} '@supabase/shared-types@0.1.95': @@ -8045,8 +8045,8 @@ packages: peerDependencies: '@supabase/supabase-js': ^2.102.1 - '@supabase/storage-js@2.112.4': - resolution: {integrity: sha512-lQ0JemuTlMIXVKgSci1qez8yPnM5hyDngeAfEBjZS2Om4D+Cus0EE5BE6glFobrxdyii1OF4UzWfF0zcQgDq5A==} + '@supabase/storage-js@2.116.0': + resolution: {integrity: sha512-6/3hR6vccBP6oGM5B6RfbwZcTCKmQOodd/ZWQdsw8yJsU5zO/a//oBL6yLnmgxcjnHSrelW8rsO7hL5DPybyUQ==} engines: {node: '>=22.0.0'} '@supabase/supa-mdx-lint-darwin@0.2.6-alpha': @@ -8184,8 +8184,8 @@ packages: resolution: {integrity: sha512-iqJHDk/ToyxFMa/um9A5gsp9jYN7iI0cIabNq9nyBpK/Yat/J9I2IIMueQwIMy3TsG6a2qdPyUkZkuPC37SdRg==} hasBin: true - '@supabase/supabase-js@2.112.4': - resolution: {integrity: sha512-UiCX1udlFY1fQQrO7Z3GU7obQsju0w5Vk9mOOwalfo/+Gy+tahWVenSSuu5E/GTy/q//HxvGv2IrCdW66/61kw==} + '@supabase/supabase-js@2.116.0': + resolution: {integrity: sha512-YyWmKXt2NspV9iO8FPnlswUFJIRnrLd3oTCb+3ZyYRuKZtBH0xCUDgnUqoyA0fGUxpM/UhfwDjYf/dht/9bp7g==} engines: {node: '>=22.0.0'} peerDependencies: '@opentelemetry/api': '>=1.0.0' @@ -24548,13 +24548,13 @@ snapshots: '@supabase-labs/y-supabase@0.1.0(@opentelemetry/api@1.9.1)': dependencies: - '@supabase/supabase-js': 2.112.4(@opentelemetry/api@1.9.1) + '@supabase/supabase-js': 2.116.0(@opentelemetry/api@1.9.1) y-protocols: 1.0.7(yjs@13.6.29) yjs: 13.6.29 transitivePeerDependencies: - '@opentelemetry/api' - '@supabase/auth-js@2.112.4': + '@supabase/auth-js@2.116.0': dependencies: tslib: 2.8.1 @@ -24582,7 +24582,7 @@ snapshots: '@supabase/cli-windows-x64@2.114.0': optional: true - '@supabase/functions-js@2.112.4': + '@supabase/functions-js@2.116.0': dependencies: tslib: 2.8.1 @@ -24604,11 +24604,11 @@ snapshots: '@supabase/phoenix@0.4.5': {} - '@supabase/postgrest-js@2.112.4': + '@supabase/postgrest-js@2.116.0': dependencies: tslib: 2.8.1 - '@supabase/realtime-js@2.112.4': + '@supabase/realtime-js@2.116.0': dependencies: '@supabase/phoenix': 0.4.5 tslib: 2.8.1 @@ -24624,12 +24624,12 @@ snapshots: - encoding - supports-color - '@supabase/ssr@0.10.2(@supabase/supabase-js@2.112.4(@opentelemetry/api@1.9.1))': + '@supabase/ssr@0.10.2(@supabase/supabase-js@2.116.0(@opentelemetry/api@1.9.1))': dependencies: - '@supabase/supabase-js': 2.112.4(@opentelemetry/api@1.9.1) + '@supabase/supabase-js': 2.116.0(@opentelemetry/api@1.9.1) cookie: 1.0.2 - '@supabase/storage-js@2.112.4': + '@supabase/storage-js@2.116.0': dependencies: iceberg-js: 0.8.1 tslib: 2.8.1 @@ -24730,13 +24730,13 @@ snapshots: '@supabase/supa-mdx-lint-win32-x64': 0.3.2 node-pty: 1.0.0 - '@supabase/supabase-js@2.112.4(@opentelemetry/api@1.9.1)': + '@supabase/supabase-js@2.116.0(@opentelemetry/api@1.9.1)': dependencies: - '@supabase/auth-js': 2.112.4 - '@supabase/functions-js': 2.112.4 - '@supabase/postgrest-js': 2.112.4 - '@supabase/realtime-js': 2.112.4 - '@supabase/storage-js': 2.112.4 + '@supabase/auth-js': 2.116.0 + '@supabase/functions-js': 2.116.0 + '@supabase/postgrest-js': 2.116.0 + '@supabase/realtime-js': 2.116.0 + '@supabase/storage-js': 2.116.0 optionalDependencies: '@opentelemetry/api': 1.9.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9b72087a05b..ac63c8989d3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -14,11 +14,11 @@ catalog: '@monaco-editor/react': 4.8.0-rc.3 '@sentry/nextjs': ^10.59.0 '@sentry/tanstackstart-react': ^10.59.0 - '@supabase/auth-js': 2.112.4 - '@supabase/postgrest-js': 2.112.4 - '@supabase/realtime-js': 2.112.4 + '@supabase/auth-js': 2.116.0 + '@supabase/postgrest-js': 2.116.0 + '@supabase/realtime-js': 2.116.0 '@supabase/ssr': 0.10.2 - '@supabase/supabase-js': 2.112.4 + '@supabase/supabase-js': 2.116.0 '@tanstack/react-router': ^1.169.2 '@tanstack/react-start': ^1.167.65 '@tanstack/react-table': ^8.21.3