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
## 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