From 02cf09212edfd045501eeab90a2840c1761b1b26 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Tue, 1 Sep 2026 13:13:30 +0200 Subject: [PATCH 01/16] chore: Remove tsconfig paths (#49770) This PR removes all `paths` in `tsconfig.json` for all apps and packages. They were added previosly because some of the components had a `_Shadcn` suffix because of an ongoing migration. How that the migration is done, the paths can be removed. ## Summary by CodeRabbit * **Refactor** * Standardized shared UI component, utility, and icon imports across design-system examples and application screens. * Simplified shared component access and project configuration. * Added shared access to anchor-link helpers and animation styles. * **Compatibility** * Updated component exports and imports without changing existing behavior. * No changes to user-facing workflows, screens, or functionality. --- apps/design-system/app/(app)/page.tsx | 2 +- apps/design-system/components/colors.tsx | 5 +++-- .../content/docs/components/alert.mdx | 4 ++-- .../content/docs/components/collapsible.mdx | 6 +----- .../default/example/commandmenu-badge.tsx | 2 +- .../example/commandmenu-conditional.tsx | 2 +- .../default/example/commandmenu-demo.tsx | 2 +- .../default/example/commandmenu-force.tsx | 2 +- .../default/example/commandmenu-hidden.tsx | 2 +- .../default/example/commandmenu-icon.tsx | 2 +- .../example/commandmenu-subpage-custom.tsx | 2 +- .../default/example/commandmenu-subpage.tsx | 2 +- .../default/example/field-checkbox.tsx | 4 ++-- .../default/example/field-choice-card.tsx | 5 +++-- .../registry/default/example/field-demo.tsx | 9 +++++---- .../default/example/field-fieldset.tsx | 10 +--------- .../registry/default/example/field-group.tsx | 4 ++-- .../registry/default/example/field-hear.tsx | 6 ++++-- .../registry/default/example/field-input.tsx | 9 +-------- .../registry/default/example/field-radio.tsx | 3 +-- .../default/example/field-responsive.tsx | 6 ++++-- .../registry/default/example/field-select.tsx | 6 ++++-- .../registry/default/example/field-slider.tsx | 3 +-- .../registry/default/example/field-switch.tsx | 8 +------- .../default/example/field-textarea.tsx | 9 +-------- apps/design-system/tsconfig.json | 1 - .../ai-skills/AiSkillsIndex.tsx | 2 +- .../local-development/cli/config/page.tsx | 2 +- .../ContentListings.client.tsx | 3 +-- apps/docs/features/docs/MdxBase.shared.tsx | 3 +-- apps/docs/layouts/ref/RefSubLayout.tsx | 11 +++++----- apps/docs/layouts/ref/RefSubLayoutNonFunc.tsx | 8 ++++---- apps/learn/tsconfig.json | 1 - .../interfaces/APIKeys/ApiKeyPill.tsx | 3 +-- .../interfaces/APIKeys/SecretAPIKeys.tsx | 9 +-------- .../Account/AccessTokens/AccessTokenList.tsx | 3 ++- .../AccessTokenTable/RowLoading.tsx | 3 +-- .../AccessTokenTable/TableContainer.tsx | 6 ++++-- .../FeaturePreview/FeaturePreviewModal.tsx | 10 ++++------ .../Auth/RedirectUrls/AddNewURLModal.tsx | 2 +- .../NewPaymentMethodElement.tsx | 2 +- .../Policies/PolicyEditorPanel/QueryError.tsx | 2 +- .../EdgeFunctionDetails.utils.tsx | 2 +- .../CreateCronJobSheet/CreateCronJobSheet.tsx | 2 +- .../Integrations/Wrappers/InputField.tsx | 3 +-- .../BillingSettings/BillingEmail.tsx | 3 +-- .../NewAwsMarketplaceOrgModal.tsx | 8 ++++---- .../QueryPerformance/QueryIndexes.tsx | 2 +- .../interfaces/Reports/ReportFilterBar.tsx | 4 +++- .../Reports/v2/ReportsNumericFilter.tsx | 15 ++++++++------ .../Reports/v2/ReportsSelectFilter.tsx | 20 ++++++++++++++----- .../interfaces/Settings/Logs/LogTable.tsx | 2 +- .../Settings/Logs/Logs.DatePickers.tsx | 5 +++-- .../Settings/Logs/LogsFilterPopover.tsx | 5 +---- .../Storage/StorageSettings/S3Connection.tsx | 2 +- .../ui/AIAssistantPanel/AssistantChatForm.tsx | 3 +-- .../studio/components/ui/DatePicker/index.tsx | 2 +- apps/studio/components/ui/Shortcut.tsx | 2 +- apps/studio/components/ui/ShortcutTooltip.tsx | 3 +-- apps/studio/tsconfig.json | 3 +-- .../components/Forms/ApplyToSupaSquadForm.tsx | 19 +++++++++--------- .../www/components/Forms/RequestADemoForm.tsx | 3 +-- .../Forms/TalkToPartnershipTeamForm.tsx | 3 +-- apps/www/components/Nav/index.tsx | 10 ++++++---- .../Pricing/ComputePricingCalculator.tsx | 3 +-- .../Pricing/PricingComputeSection.tsx | 3 +-- .../src/AssistantChat/AssistantChatForm.tsx | 3 +-- .../AssistantCommandsPopover.tsx | 8 +++++--- .../ui-patterns/src/Banners/LW15Banner.tsx | 2 +- .../prepackaged/DocsAi/DocsAiPage.tsx | 3 +-- .../prepackaged/DocsSearch/DocsSearchPage.tsx | 3 +-- .../ui-patterns/src/CountdownWidget/index.tsx | 2 +- .../src/Dialogs/ConfirmationModal.tsx | 3 ++- .../src/Dialogs/TextConfirmModal.tsx | 2 +- packages/ui-patterns/src/Markdown/index.tsx | 3 +-- .../components/McpConfigurationOptions.tsx | 3 +-- .../src/MobileSheetNav/MobileSheetNav.tsx | 3 +-- .../ui-patterns/src/PromoToast/PromoBg.tsx | 2 +- .../ui-patterns/src/PromoToast/PromoToast.tsx | 3 +-- .../src/SqlToRest/base-url-dialog.tsx | 6 +++--- packages/ui-patterns/src/Toc/toc.tsx | 3 +-- packages/ui-patterns/src/TweetCard/index.tsx | 2 +- .../ui-patterns/src/collapsible-alert.tsx | 8 +------- .../src/form/Layout/FormLayout.tsx | 3 +-- .../src/multi-select/multi-select.tsx | 3 ++- packages/ui/index.tsx | 11 +++++++++- .../components/CustomHTMLElements/index.tsx | 3 +-- 87 files changed, 178 insertions(+), 214 deletions(-) diff --git a/apps/design-system/app/(app)/page.tsx b/apps/design-system/app/(app)/page.tsx index 14b69506f7865..c8a6e7d3d78ba 100644 --- a/apps/design-system/app/(app)/page.tsx +++ b/apps/design-system/app/(app)/page.tsx @@ -1,4 +1,4 @@ -import { Auth, Database, Realtime } from 'icons/src/icons' +import { Auth, Database, Realtime } from 'icons' import { Paintbrush } from 'lucide-react' import Link from 'next/link' diff --git a/apps/design-system/components/colors.tsx b/apps/design-system/components/colors.tsx index 5a0660ac63a91..23948a3802ab4 100644 --- a/apps/design-system/components/colors.tsx +++ b/apps/design-system/components/colors.tsx @@ -1,9 +1,10 @@ import { useState } from 'react' -import { cn } from 'ui' -import color from 'ui/src/lib/tailwind-demo-classes' +import { cn, colors } from 'ui' import { Grid, GridItem } from './grid' +const color = colors['default'] + const Colors = ({ definition, }: { diff --git a/apps/design-system/content/docs/components/alert.mdx b/apps/design-system/content/docs/components/alert.mdx index f5c277e7c2175..f688e7e79c449 100644 --- a/apps/design-system/content/docs/components/alert.mdx +++ b/apps/design-system/content/docs/components/alert.mdx @@ -18,7 +18,7 @@ Use [Collapsible Alert](../fragments/collapsible-alert) when a callout also need ## Usage ```tsx -import { Alert, AlertDescription, AlertTitle } from 'ui/src/components/shadcn/ui/alert' +import { Alert, AlertDescription, AlertTitle } from 'ui' ``` ```tsx @@ -36,5 +36,5 @@ import { Alert, AlertDescription, AlertTitle } from 'ui/src/components/shadcn/ui Use `alertVariants` when building shared wrappers around the primitive. ```tsx -import { alertVariants } from 'ui/src/components/shadcn/ui/alert' +import { alertVariants } from 'ui' ``` diff --git a/apps/design-system/content/docs/components/collapsible.mdx b/apps/design-system/content/docs/components/collapsible.mdx index 7eb271478ed9f..3b6b31bd1ffd0 100644 --- a/apps/design-system/content/docs/components/collapsible.mdx +++ b/apps/design-system/content/docs/components/collapsible.mdx @@ -13,11 +13,7 @@ Use Collapsible for generic disclosure behaviour: rows, sections, menus, tool de ## Usage ```tsx -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from 'ui/src/components/shadcn/ui/collapsible' +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from 'ui' ``` ```tsx diff --git a/apps/design-system/registry/default/example/commandmenu-badge.tsx b/apps/design-system/registry/default/example/commandmenu-badge.tsx index edeb2f3df2f58..d048bba3104f0 100644 --- a/apps/design-system/registry/default/example/commandmenu-badge.tsx +++ b/apps/design-system/registry/default/example/commandmenu-badge.tsx @@ -1,4 +1,4 @@ -import { Button } from '@ui/components/shadcn/ui/button' +import { Button } from 'ui' import { BadgeExperimental, CommandMenu, diff --git a/apps/design-system/registry/default/example/commandmenu-conditional.tsx b/apps/design-system/registry/default/example/commandmenu-conditional.tsx index 2be767247be66..711c8e6e3555d 100644 --- a/apps/design-system/registry/default/example/commandmenu-conditional.tsx +++ b/apps/design-system/registry/default/example/commandmenu-conditional.tsx @@ -1,5 +1,5 @@ -import { Button } from '@ui/components/shadcn/ui/button' import { useMemo, useState } from 'react' +import { Button } from 'ui' import { CommandMenu, CommandMenuInput, diff --git a/apps/design-system/registry/default/example/commandmenu-demo.tsx b/apps/design-system/registry/default/example/commandmenu-demo.tsx index e4df0a4385016..1f8bd002fccca 100644 --- a/apps/design-system/registry/default/example/commandmenu-demo.tsx +++ b/apps/design-system/registry/default/example/commandmenu-demo.tsx @@ -1,4 +1,4 @@ -import { Button } from '@ui/components/shadcn/ui/button' +import { Button } from 'ui' import { CommandMenu, CommandMenuInput, diff --git a/apps/design-system/registry/default/example/commandmenu-force.tsx b/apps/design-system/registry/default/example/commandmenu-force.tsx index b7f297d3ba2f7..9ddee6bf75492 100644 --- a/apps/design-system/registry/default/example/commandmenu-force.tsx +++ b/apps/design-system/registry/default/example/commandmenu-force.tsx @@ -1,4 +1,4 @@ -import { Button } from '@ui/components/shadcn/ui/button' +import { Button } from 'ui' import { CommandMenu, CommandMenuInput, diff --git a/apps/design-system/registry/default/example/commandmenu-hidden.tsx b/apps/design-system/registry/default/example/commandmenu-hidden.tsx index 3e693b32b60a2..ebecc1325933d 100644 --- a/apps/design-system/registry/default/example/commandmenu-hidden.tsx +++ b/apps/design-system/registry/default/example/commandmenu-hidden.tsx @@ -1,4 +1,4 @@ -import { Button } from '@ui/components/shadcn/ui/button' +import { Button } from 'ui' import { CommandMenu, CommandMenuInput, diff --git a/apps/design-system/registry/default/example/commandmenu-icon.tsx b/apps/design-system/registry/default/example/commandmenu-icon.tsx index 7de6da43f0cd4..407888b2144b5 100644 --- a/apps/design-system/registry/default/example/commandmenu-icon.tsx +++ b/apps/design-system/registry/default/example/commandmenu-icon.tsx @@ -1,5 +1,5 @@ -import { Button } from '@ui/components/shadcn/ui/button' import { Github } from 'lucide-react' +import { Button } from 'ui' import { CommandMenu, CommandMenuInput, diff --git a/apps/design-system/registry/default/example/commandmenu-subpage-custom.tsx b/apps/design-system/registry/default/example/commandmenu-subpage-custom.tsx index 93f833dbae5df..8c74449289d62 100644 --- a/apps/design-system/registry/default/example/commandmenu-subpage-custom.tsx +++ b/apps/design-system/registry/default/example/commandmenu-subpage-custom.tsx @@ -1,4 +1,4 @@ -import { Button } from '@ui/components/shadcn/ui/button' +import { Button } from 'ui' import { Breadcrumb, CommandMenu, diff --git a/apps/design-system/registry/default/example/commandmenu-subpage.tsx b/apps/design-system/registry/default/example/commandmenu-subpage.tsx index 3bc2112c66980..49530f45b62b1 100644 --- a/apps/design-system/registry/default/example/commandmenu-subpage.tsx +++ b/apps/design-system/registry/default/example/commandmenu-subpage.tsx @@ -1,5 +1,5 @@ -import { Button } from '@ui/components/shadcn/ui/button' import { useMemo } from 'react' +import { Button } from 'ui' import { CommandMenu, CommandMenuInput, diff --git a/apps/design-system/registry/default/example/field-checkbox.tsx b/apps/design-system/registry/default/example/field-checkbox.tsx index 0d222ae78c979..9eac860240f73 100644 --- a/apps/design-system/registry/default/example/field-checkbox.tsx +++ b/apps/design-system/registry/default/example/field-checkbox.tsx @@ -1,5 +1,5 @@ -import { Checkbox } from 'ui' import { + Checkbox, Field, FieldContent, FieldDescription, @@ -8,7 +8,7 @@ import { FieldLegend, FieldSeparator, FieldSet, -} from 'ui/src/components/shadcn/ui/field' +} from 'ui' export default function FieldCheckbox() { return ( diff --git a/apps/design-system/registry/default/example/field-choice-card.tsx b/apps/design-system/registry/default/example/field-choice-card.tsx index 26cf6af26ed0d..b806462644a3a 100644 --- a/apps/design-system/registry/default/example/field-choice-card.tsx +++ b/apps/design-system/registry/default/example/field-choice-card.tsx @@ -6,8 +6,9 @@ import { FieldLabel, FieldSet, FieldTitle, -} from 'ui/src/components/shadcn/ui/field' -import { RadioGroup, RadioGroupItem } from 'ui/src/components/shadcn/ui/radio-group' + RadioGroup, + RadioGroupItem, +} from 'ui' export default function FieldChoiceCard() { return ( diff --git a/apps/design-system/registry/default/example/field-demo.tsx b/apps/design-system/registry/default/example/field-demo.tsx index 7f73f3481584c..45cad3c1f1825 100644 --- a/apps/design-system/registry/default/example/field-demo.tsx +++ b/apps/design-system/registry/default/example/field-demo.tsx @@ -1,5 +1,6 @@ -import { Button, Checkbox, Input, Textarea } from 'ui' import { + Button, + Checkbox, Field, FieldDescription, FieldGroup, @@ -7,14 +8,14 @@ import { FieldLegend, FieldSeparator, FieldSet, -} from 'ui/src/components/shadcn/ui/field' -import { + Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from 'ui/src/components/shadcn/ui/select' + Textarea, +} from 'ui' export default function FieldDemo() { return ( diff --git a/apps/design-system/registry/default/example/field-fieldset.tsx b/apps/design-system/registry/default/example/field-fieldset.tsx index a91c1d4d86e58..7d3cb96a02e06 100644 --- a/apps/design-system/registry/default/example/field-fieldset.tsx +++ b/apps/design-system/registry/default/example/field-fieldset.tsx @@ -1,12 +1,4 @@ -import { Input } from 'ui' -import { - Field, - FieldDescription, - FieldGroup, - FieldLabel, - FieldLegend, - FieldSet, -} from 'ui/src/components/shadcn/ui/field' +import { Field, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSet, Input } from 'ui' export default function FieldFieldset() { return ( diff --git a/apps/design-system/registry/default/example/field-group.tsx b/apps/design-system/registry/default/example/field-group.tsx index 73b66eaf8e018..f8a2d7a55d832 100644 --- a/apps/design-system/registry/default/example/field-group.tsx +++ b/apps/design-system/registry/default/example/field-group.tsx @@ -1,12 +1,12 @@ -import { Checkbox } from 'ui' import { + Checkbox, Field, FieldDescription, FieldGroup, FieldLabel, FieldSeparator, FieldSet, -} from 'ui/src/components/shadcn/ui/field' +} from 'ui' export default function FieldGroupExample() { return ( diff --git a/apps/design-system/registry/default/example/field-hear.tsx b/apps/design-system/registry/default/example/field-hear.tsx index 80bee715127d6..87bdd25843e33 100644 --- a/apps/design-system/registry/default/example/field-hear.tsx +++ b/apps/design-system/registry/default/example/field-hear.tsx @@ -1,5 +1,7 @@ -import { Card, CardContent, Checkbox } from 'ui' import { + Card, + CardContent, + Checkbox, Field, FieldDescription, FieldGroup, @@ -7,7 +9,7 @@ import { FieldLegend, FieldSet, FieldTitle, -} from 'ui/src/components/shadcn/ui/field' +} from 'ui' const options = [ { diff --git a/apps/design-system/registry/default/example/field-input.tsx b/apps/design-system/registry/default/example/field-input.tsx index 17eee8e9b2bcf..f027cc5325ad7 100644 --- a/apps/design-system/registry/default/example/field-input.tsx +++ b/apps/design-system/registry/default/example/field-input.tsx @@ -1,11 +1,4 @@ -import { Input } from 'ui' -import { - Field, - FieldDescription, - FieldGroup, - FieldLabel, - FieldSet, -} from 'ui/src/components/shadcn/ui/field' +import { Field, FieldDescription, FieldGroup, FieldLabel, FieldSet, Input } from 'ui' export default function FieldInput() { return ( diff --git a/apps/design-system/registry/default/example/field-radio.tsx b/apps/design-system/registry/default/example/field-radio.tsx index 59d703a015d90..d96420a05ce5e 100644 --- a/apps/design-system/registry/default/example/field-radio.tsx +++ b/apps/design-system/registry/default/example/field-radio.tsx @@ -1,5 +1,4 @@ -import { Field, FieldDescription, FieldLabel, FieldSet } from 'ui/src/components/shadcn/ui/field' -import { RadioGroup, RadioGroupItem } from 'ui/src/components/shadcn/ui/radio-group' +import { Field, FieldDescription, FieldLabel, FieldSet, RadioGroup, RadioGroupItem } from 'ui' export default function FieldRadio() { return ( diff --git a/apps/design-system/registry/default/example/field-responsive.tsx b/apps/design-system/registry/default/example/field-responsive.tsx index 568e583485d7a..c59bbbeb254e6 100644 --- a/apps/design-system/registry/default/example/field-responsive.tsx +++ b/apps/design-system/registry/default/example/field-responsive.tsx @@ -1,5 +1,5 @@ -import { Button, Input, Textarea } from 'ui' import { + Button, Field, FieldContent, FieldDescription, @@ -8,7 +8,9 @@ import { FieldLegend, FieldSeparator, FieldSet, -} from 'ui/src/components/shadcn/ui/field' + Input, + Textarea, +} from 'ui' export default function FieldResponsive() { return ( diff --git a/apps/design-system/registry/default/example/field-select.tsx b/apps/design-system/registry/default/example/field-select.tsx index be6996b554cc5..7d9a56c4c1c90 100644 --- a/apps/design-system/registry/default/example/field-select.tsx +++ b/apps/design-system/registry/default/example/field-select.tsx @@ -1,11 +1,13 @@ -import { Field, FieldDescription, FieldLabel } from 'ui/src/components/shadcn/ui/field' import { + Field, + FieldDescription, + FieldLabel, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from 'ui/src/components/shadcn/ui/select' +} from 'ui' export default function FieldSelect() { return ( diff --git a/apps/design-system/registry/default/example/field-slider.tsx b/apps/design-system/registry/default/example/field-slider.tsx index 6f7fb54d7ab12..26822ce0781f0 100644 --- a/apps/design-system/registry/default/example/field-slider.tsx +++ b/apps/design-system/registry/default/example/field-slider.tsx @@ -1,8 +1,7 @@ 'use client' import { useState } from 'react' -import { Slider } from 'ui' -import { Field, FieldDescription, FieldTitle } from 'ui/src/components/shadcn/ui/field' +import { Field, FieldDescription, FieldTitle, Slider } from 'ui' export default function FieldSlider() { const [value, setValue] = useState([200, 800]) diff --git a/apps/design-system/registry/default/example/field-switch.tsx b/apps/design-system/registry/default/example/field-switch.tsx index ba08cec8f4d97..9601fe5df768d 100644 --- a/apps/design-system/registry/default/example/field-switch.tsx +++ b/apps/design-system/registry/default/example/field-switch.tsx @@ -1,10 +1,4 @@ -import { Switch } from 'ui' -import { - Field, - FieldContent, - FieldDescription, - FieldLabel, -} from 'ui/src/components/shadcn/ui/field' +import { Field, FieldContent, FieldDescription, FieldLabel, Switch } from 'ui' export default function FieldSwitch() { return ( diff --git a/apps/design-system/registry/default/example/field-textarea.tsx b/apps/design-system/registry/default/example/field-textarea.tsx index 91980c1d3fd5e..df43668d62535 100644 --- a/apps/design-system/registry/default/example/field-textarea.tsx +++ b/apps/design-system/registry/default/example/field-textarea.tsx @@ -1,11 +1,4 @@ -import { Textarea } from 'ui' -import { - Field, - FieldDescription, - FieldGroup, - FieldLabel, - FieldSet, -} from 'ui/src/components/shadcn/ui/field' +import { Field, FieldDescription, FieldGroup, FieldLabel, FieldSet, Textarea } from 'ui' export default function FieldTextarea() { return ( diff --git a/apps/design-system/tsconfig.json b/apps/design-system/tsconfig.json index d96c6722dbff8..c52f02adc4bce 100644 --- a/apps/design-system/tsconfig.json +++ b/apps/design-system/tsconfig.json @@ -6,7 +6,6 @@ "lib": ["dom", "dom.iterable", "esnext"], "paths": { "@/*": ["./*"], - "@ui/*": ["./../../packages/ui/src/*"], // handle ui package paths "contentlayer/generated": ["./.contentlayer/generated"], "icons/*": ["./../../packages/icons/*"] }, diff --git a/apps/docs/app/guides/getting-started/ai-skills/AiSkillsIndex.tsx b/apps/docs/app/guides/getting-started/ai-skills/AiSkillsIndex.tsx index 0d04fd700cc96..d9a58e69a4697 100644 --- a/apps/docs/app/guides/getting-started/ai-skills/AiSkillsIndex.tsx +++ b/apps/docs/app/guides/getting-started/ai-skills/AiSkillsIndex.tsx @@ -1,5 +1,5 @@ +import { Heading } from 'ui' import { CodeBlock } from 'ui-patterns/CodeBlock' -import Heading from 'ui/src/components/CustomHTMLElements/Heading' import { getAiSkills } from './AiSkills.utils' diff --git a/apps/docs/app/guides/local-development/cli/config/page.tsx b/apps/docs/app/guides/local-development/cli/config/page.tsx index 8461b6baeb44d..f01fe0562a1ab 100644 --- a/apps/docs/app/guides/local-development/cli/config/page.tsx +++ b/apps/docs/app/guides/local-development/cli/config/page.tsx @@ -4,8 +4,8 @@ import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' import type { Parameter } from '~/lib/refGenerator/refTypes' import specFile from '~/spec/cli_v1_config.yaml' with { type: 'yml' } import ReactMarkdown from 'react-markdown' +import { Heading } from 'ui' import { CodeBlock } from 'ui-patterns/CodeBlock' -import { Heading } from 'ui/src/components/CustomHTMLElements' const meta = { title: 'Supabase CLI config', diff --git a/apps/docs/components/ContentListings/ContentListings.client.tsx b/apps/docs/components/ContentListings/ContentListings.client.tsx index 2e699cbd2d6f5..fc9e817f27830 100644 --- a/apps/docs/components/ContentListings/ContentListings.client.tsx +++ b/apps/docs/components/ContentListings/ContentListings.client.tsx @@ -11,9 +11,8 @@ import { useSendTelemetryEvent } from '~/lib/telemetry' import Link from 'next/link' import { useCallback, useMemo } from 'react' import ReactMarkdown from 'react-markdown' -import { Badge } from 'ui' +import { Badge, Heading } from 'ui' import { GlassPanel } from 'ui-patterns/GlassPanel' -import { Heading } from 'ui/src/components/CustomHTMLElements' import { resolveContentListingIcon } from './iconChip' diff --git a/apps/docs/features/docs/MdxBase.shared.tsx b/apps/docs/features/docs/MdxBase.shared.tsx index 78d5347b57337..a7124288ec4c0 100644 --- a/apps/docs/features/docs/MdxBase.shared.tsx +++ b/apps/docs/features/docs/MdxBase.shared.tsx @@ -33,11 +33,10 @@ import { TabPanel, Tabs } from '~/features/ui/Tabs' import { ArrowDown, Check, X } from 'lucide-react' import Link from 'next/link' import { type ComponentPropsWithoutRef } from 'react' -import { Badge, Button } from 'ui' +import { Badge, Button, Heading } from 'ui' import { Admonition, type AdmonitionProps } from 'ui-patterns/Admonition' import { GlassPanel } from 'ui-patterns/GlassPanel' import SqlToRest from 'ui-patterns/SqlToRest' -import { Heading } from 'ui/src/components/CustomHTMLElements' import { AgentPluginsPanel } from '../ui/AgentPluginsPanel' import { AiPrompt } from '../ui/AiPrompt' diff --git a/apps/docs/layouts/ref/RefSubLayout.tsx b/apps/docs/layouts/ref/RefSubLayout.tsx index 4aa5101ff71b4..e30ed7989fe82 100644 --- a/apps/docs/layouts/ref/RefSubLayout.tsx +++ b/apps/docs/layouts/ref/RefSubLayout.tsx @@ -1,12 +1,11 @@ -import { useInView } from 'react-intersection-observer' -import { FC, PropsWithChildren } from 'react' -import { highlightSelectedNavItem } from 'ui/src/components/CustomHTMLElements/CustomHTMLElements.utils' -import { useRouter } from 'next/compat/router' import { useNavigationMenuContext } from '~/components/Navigation/NavigationMenu/NavigationMenu.Context' import { menuState } from '~/hooks/useMenuState' -import Image from 'next/legacy/image' -import { cn } from 'ui' import { safeHistoryReplaceState } from '~/lib/historyUtils' +import { useRouter } from 'next/compat/router' +import Image from 'next/legacy/image' +import { FC, PropsWithChildren } from 'react' +import { useInView } from 'react-intersection-observer' +import { cn, highlightSelectedNavItem } from 'ui' interface ISectionContainer { id: string diff --git a/apps/docs/layouts/ref/RefSubLayoutNonFunc.tsx b/apps/docs/layouts/ref/RefSubLayoutNonFunc.tsx index c6f466e32f5d6..0c6e465315a30 100644 --- a/apps/docs/layouts/ref/RefSubLayoutNonFunc.tsx +++ b/apps/docs/layouts/ref/RefSubLayoutNonFunc.tsx @@ -1,10 +1,10 @@ -import { useInView } from 'react-intersection-observer' -import { FC, PropsWithChildren } from 'react' -import { highlightSelectedNavItem } from 'ui/src/components/CustomHTMLElements/CustomHTMLElements.utils' -import { useRouter } from 'next/compat/router' import { useNavigationMenuContext } from '~/components/Navigation/NavigationMenu/NavigationMenu.Context' import { menuState } from '~/hooks/useMenuState' import { safeHistoryReplaceState } from '~/lib/historyUtils' +import { useRouter } from 'next/compat/router' +import { FC, PropsWithChildren } from 'react' +import { useInView } from 'react-intersection-observer' +import { highlightSelectedNavItem } from 'ui' interface ISectionContainer { id: string diff --git a/apps/learn/tsconfig.json b/apps/learn/tsconfig.json index 3ea68ca96d84d..2a94aacb8e935 100644 --- a/apps/learn/tsconfig.json +++ b/apps/learn/tsconfig.json @@ -6,7 +6,6 @@ // @deprecated: Don't import directly without suffix. Use @/ always. "*": ["./*"], "@/*": ["./*"], - "@ui/*": ["./../../packages/ui/src/*"], // handle ui package paths "contentlayer/generated": ["./.contentlayer/generated"] }, "plugins": [{ "name": "next" }], diff --git a/apps/studio/components/interfaces/APIKeys/ApiKeyPill.tsx b/apps/studio/components/interfaces/APIKeys/ApiKeyPill.tsx index b46c07b230d23..f4b6f6e6c4391 100644 --- a/apps/studio/components/interfaces/APIKeys/ApiKeyPill.tsx +++ b/apps/studio/components/interfaces/APIKeys/ApiKeyPill.tsx @@ -1,10 +1,9 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { InputVariants } from '@ui/components/shadcn/ui/input' import { useParams } from 'common' import { Eye, EyeOff } from 'lucide-react' import { useEffect, useState } from 'react' import { toast } from 'sonner' -import { Button, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui' +import { Button, cn, InputVariants, Tooltip, TooltipContent, TooltipTrigger } from 'ui' import { useRevealedSecret } from './useRevealedSecret' import CopyButton from '@/components/ui/CopyButton' diff --git a/apps/studio/components/interfaces/APIKeys/SecretAPIKeys.tsx b/apps/studio/components/interfaces/APIKeys/SecretAPIKeys.tsx index 366fa02005a0c..a863818bf6239 100644 --- a/apps/studio/components/interfaces/APIKeys/SecretAPIKeys.tsx +++ b/apps/studio/components/interfaces/APIKeys/SecretAPIKeys.tsx @@ -3,15 +3,8 @@ import { IS_PLATFORM, useParams } from 'common' import { parseAsString, useQueryState } from 'nuqs' import { useEffect, useMemo } from 'react' import { toast } from 'sonner' -import { Card } from 'ui' +import { Card, Table, TableBody, TableHead, TableHeader, TableRow } from 'ui' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' -import { - Table, - TableBody, - TableHead, - TableHeader, - TableRow, -} from 'ui/src/components/shadcn/ui/table' import { APIKeyRow } from './APIKeyRow' import { CreateSecretAPIKeyDialog } from './CreateSecretAPIKeyDialog' diff --git a/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenList.tsx b/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenList.tsx index 72de6b8416305..b95ba903350c1 100644 --- a/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenList.tsx +++ b/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenList.tsx @@ -8,9 +8,10 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, + TableCell, + TableRow, } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' -import { TableCell, TableRow } from 'ui/src/components/shadcn/ui/table' import { ACCESS_TOKEN_SORT_VALUES, diff --git a/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenTable/RowLoading.tsx b/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenTable/RowLoading.tsx index 53f799da494f3..4ef0d8b165f04 100644 --- a/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenTable/RowLoading.tsx +++ b/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenTable/RowLoading.tsx @@ -1,5 +1,4 @@ -import { Skeleton } from 'ui' -import { TableCell, TableRow } from 'ui/src/components/shadcn/ui/table' +import { Skeleton, TableCell, TableRow } from 'ui' export const RowLoading = () => ( diff --git a/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenTable/TableContainer.tsx b/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenTable/TableContainer.tsx index e684a587677a5..0415a1fb74522 100644 --- a/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenTable/TableContainer.tsx +++ b/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenTable/TableContainer.tsx @@ -1,12 +1,14 @@ -import { Card, CardContent, cn } from 'ui' import { + Card, + CardContent, + cn, Table, TableBody, TableHead, TableHeader, TableHeadSort, TableRow, -} from 'ui/src/components/shadcn/ui/table' +} from 'ui' import { AccessTokenSort, AccessTokenSortColumn } from '../AccessToken.types' diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx index 606e6198125c1..794d865cd2ff6 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx @@ -20,16 +20,14 @@ import { DialogSectionSeparator, DialogTitle, ScrollArea, - Tooltip, - TooltipContent, - TooltipTrigger, -} from 'ui' -import { Select, SelectContent, SelectItem, SelectTrigger, -} from 'ui/src/components/shadcn/ui/select' + Tooltip, + TooltipContent, + TooltipTrigger, +} from 'ui' import { AdvisorRulesPreview } from './AdvisorRulesPreview' import { CLSPreview } from './CLSPreview' diff --git a/apps/studio/components/interfaces/Auth/RedirectUrls/AddNewURLModal.tsx b/apps/studio/components/interfaces/Auth/RedirectUrls/AddNewURLModal.tsx index a468dbc6c9664..43eee80ea0809 100644 --- a/apps/studio/components/interfaces/Auth/RedirectUrls/AddNewURLModal.tsx +++ b/apps/studio/components/interfaces/Auth/RedirectUrls/AddNewURLModal.tsx @@ -1,5 +1,4 @@ import { zodResolver } from '@hookform/resolvers/zod' -import { Label } from '@ui/components/shadcn/ui/label' import { useParams } from 'common' import { useEffect } from 'react' import { useForm, useWatch } from 'react-hook-form' @@ -16,6 +15,7 @@ import { DialogSectionSeparator, DialogTitle, Form, + Label, ScrollArea, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' diff --git a/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/NewPaymentMethodElement.tsx b/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/NewPaymentMethodElement.tsx index c2b2e72f3c342..2aac6747753e4 100644 --- a/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/NewPaymentMethodElement.tsx +++ b/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/NewPaymentMethodElement.tsx @@ -11,7 +11,6 @@ import { StripeAddressElementOptions, type SetupIntent, } from '@stripe/stripe-js' -import { Form } from '@ui/components/shadcn/ui/form' import { Check, ChevronsUpDown, HelpCircle } from 'lucide-react' import { forwardRef, useEffect, useId, useImperativeHandle, useMemo, useRef, useState } from 'react' import { useForm, useWatch } from 'react-hook-form' @@ -26,6 +25,7 @@ import { CommandInput, CommandItem, CommandList, + Form, FormControl, FormField, FormItem, diff --git a/apps/studio/components/interfaces/Database/Policies/PolicyEditorPanel/QueryError.tsx b/apps/studio/components/interfaces/Database/Policies/PolicyEditorPanel/QueryError.tsx index 3edd410b0d83f..735e5124014bb 100644 --- a/apps/studio/components/interfaces/Database/Policies/PolicyEditorPanel/QueryError.tsx +++ b/apps/studio/components/interfaces/Database/Policies/PolicyEditorPanel/QueryError.tsx @@ -1,4 +1,3 @@ -import styles from '@ui/layout/ai-icon-animation/ai-icon-animation-style.module.css' import { initial, last } from 'lodash' import { Dispatch, SetStateAction } from 'react' import { @@ -9,6 +8,7 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger, + aiIconAnimationStyles as styles, } from 'ui' import { QueryResponseError } from '@/data/sql/execute-sql-mutation' diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.utils.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.utils.tsx index 21b02412ecb1a..689faa5e1d6dd 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.utils.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.utils.tsx @@ -1,4 +1,4 @@ -import { getAnchor } from '@ui/components/CustomHTMLElements/CustomHTMLElements.utils' +import { getAnchor } from 'ui' import { EdgeFunction } from '@/data/edge-functions/edge-function-query' import { DOCS_URL } from '@/lib/constants' diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.tsx index 1e98e0e3d25aa..1ff0eed0e06a6 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.tsx @@ -1,6 +1,5 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useWatch } from '@ui/components/shadcn/ui/form' import { useParams } from 'common' import { parseAsString, useQueryState } from 'nuqs' import { useEffect, useState } from 'react' @@ -21,6 +20,7 @@ import { SheetHeader, SheetSection, SheetTitle, + useWatch, WarningIcon, } from 'ui' import { Admonition } from 'ui-patterns/Admonition' diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/InputField.tsx b/apps/studio/components/interfaces/Integrations/Wrappers/InputField.tsx index 664fb36f4dfb0..c34eb91a6522f 100644 --- a/apps/studio/components/interfaces/Integrations/Wrappers/InputField.tsx +++ b/apps/studio/components/interfaces/Integrations/Wrappers/InputField.tsx @@ -1,8 +1,7 @@ -import { InputVariants } from '@ui/components/shadcn/ui/input' import { HelpCircle } from 'lucide-react' import Link from 'next/link' import type { Control, FieldPath, FieldValues } from 'react-hook-form' -import { cn, FormControl, FormField, Input, Textarea } from 'ui' +import { cn, FormControl, FormField, Input, InputVariants, Textarea } from 'ui' import { Input as PasswordInput } from 'ui-patterns/DataInputs/Input' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingEmail.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingEmail.tsx index a035f9a4650a1..f8248859abfd2 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingEmail.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingEmail.tsx @@ -1,12 +1,11 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { Form, FormControl, FormField } from '@ui/components/shadcn/ui/form' import { useParams } from 'common' import { useEffect } from 'react' import { useForm, useWatch } from 'react-hook-form' import { useInView } from 'react-intersection-observer' import { toast } from 'sonner' -import { FormMessage, Input } from 'ui' +import { Form, FormControl, FormField, FormMessage, Input } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { InfoTooltip } from 'ui-patterns/info-tooltip' import { diff --git a/apps/studio/components/interfaces/Organization/CloudMarketplace/NewAwsMarketplaceOrgModal.tsx b/apps/studio/components/interfaces/Organization/CloudMarketplace/NewAwsMarketplaceOrgModal.tsx index e5fd1c0942426..11c34f1ea1c75 100644 --- a/apps/studio/components/interfaces/Organization/CloudMarketplace/NewAwsMarketplaceOrgModal.tsx +++ b/apps/studio/components/interfaces/Organization/CloudMarketplace/NewAwsMarketplaceOrgModal.tsx @@ -1,4 +1,7 @@ +import { SubmitHandler } from 'react-hook-form' +import { toast } from 'sonner' import { + Button, Dialog, DialogContent, DialogDescription, @@ -7,10 +10,7 @@ import { DialogSection, DialogSectionSeparator, DialogTitle, -} from '@ui/components/shadcn/ui/dialog' -import { SubmitHandler } from 'react-hook-form' -import { toast } from 'sonner' -import { Button } from 'ui' +} from 'ui' import { CREATE_AWS_MANAGED_ORG_FORM_ID, diff --git a/apps/studio/components/interfaces/QueryPerformance/QueryIndexes.tsx b/apps/studio/components/interfaces/QueryPerformance/QueryIndexes.tsx index 5902c8753f1df..1fe97d0dcc12b 100644 --- a/apps/studio/components/interfaces/QueryPerformance/QueryIndexes.tsx +++ b/apps/studio/components/interfaces/QueryPerformance/QueryIndexes.tsx @@ -1,10 +1,10 @@ -import { AccordionTrigger } from '@ui/components/shadcn/ui/accordion' import { Check, Lightbulb, Table2 } from 'lucide-react' import { useEffect, useState } from 'react' import { Accordion, AccordionContent, AccordionItem, + AccordionTrigger, Alert, AlertDescription, AlertTitle, diff --git a/apps/studio/components/interfaces/Reports/ReportFilterBar.tsx b/apps/studio/components/interfaces/Reports/ReportFilterBar.tsx index 4c3aaba51c5d3..b3d8aded4366c 100644 --- a/apps/studio/components/interfaces/Reports/ReportFilterBar.tsx +++ b/apps/studio/components/interfaces/Reports/ReportFilterBar.tsx @@ -1,4 +1,3 @@ -import { Popover, PopoverContent, PopoverTrigger } from '@ui/components/shadcn/ui/popover' import { useParams } from 'common' import { Auth, Realtime, Storage } from 'icons' import { ChevronDown, Database, Network, Plus, RefreshCw, X } from 'lucide-react' @@ -13,6 +12,9 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, Input, + Popover, + PopoverContent, + PopoverTrigger, Select, SelectContent, SelectGroup, diff --git a/apps/studio/components/interfaces/Reports/v2/ReportsNumericFilter.tsx b/apps/studio/components/interfaces/Reports/v2/ReportsNumericFilter.tsx index f102e68f3385a..86efb5b81fe66 100644 --- a/apps/studio/components/interfaces/Reports/v2/ReportsNumericFilter.tsx +++ b/apps/studio/components/interfaces/Reports/v2/ReportsNumericFilter.tsx @@ -1,15 +1,18 @@ -import { Label } from '@ui/components/shadcn/ui/label' -import { Popover, PopoverContent, PopoverTrigger } from '@ui/components/shadcn/ui/popover' +import { ChevronDown } from 'lucide-react' +import { useEffect, useState } from 'react' import { + Button, + cn, + Label, + Popover, + PopoverContent, + PopoverTrigger, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from '@ui/components/shadcn/ui/select' -import { ChevronDown } from 'lucide-react' -import { useEffect, useState } from 'react' -import { Button, cn } from 'ui' +} from 'ui' import { Input } from 'ui-patterns/DataInputs/Input' import { z } from 'zod' diff --git a/apps/studio/components/interfaces/Reports/v2/ReportsSelectFilter.tsx b/apps/studio/components/interfaces/Reports/v2/ReportsSelectFilter.tsx index 4727ce55c6a8d..dbb142abc1472 100644 --- a/apps/studio/components/interfaces/Reports/v2/ReportsSelectFilter.tsx +++ b/apps/studio/components/interfaces/Reports/v2/ReportsSelectFilter.tsx @@ -1,10 +1,20 @@ -import { Checkbox } from '@ui/components/shadcn/ui/checkbox' -import { CommandGroup } from '@ui/components/shadcn/ui/command' -import { Label } from '@ui/components/shadcn/ui/label' -import { Popover, PopoverContent, PopoverTrigger } from '@ui/components/shadcn/ui/popover' import { ChevronDown } from 'lucide-react' import { useEffect, useState } from 'react' -import { Button, cn, Command, CommandEmpty, CommandInput, CommandItem, CommandList } from 'ui' +import { + Button, + Checkbox, + cn, + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + Label, + Popover, + PopoverContent, + PopoverTrigger, +} from 'ui' import { z } from 'zod' export interface ReportSelectOption { diff --git a/apps/studio/components/interfaces/Settings/Logs/LogTable.tsx b/apps/studio/components/interfaces/Settings/Logs/LogTable.tsx index 89f17a3f8ded0..fe73a36671fe3 100644 --- a/apps/studio/components/interfaces/Settings/Logs/LogTable.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/LogTable.tsx @@ -1,5 +1,4 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { ContextMenuContent } from '@ui/components/shadcn/ui/context-menu' import { IS_PLATFORM, useParams } from 'common' import { Copy, Eye, EyeOff, Play } from 'lucide-react' import { Key, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -10,6 +9,7 @@ import { Checkbox, cn, ContextMenu, + ContextMenuContent, ContextMenuItem, ContextMenuTrigger, copyToClipboard, diff --git a/apps/studio/components/interfaces/Settings/Logs/Logs.DatePickers.tsx b/apps/studio/components/interfaces/Settings/Logs/Logs.DatePickers.tsx index 811191d918a2c..bceb9af95fb40 100644 --- a/apps/studio/components/interfaces/Settings/Logs/Logs.DatePickers.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/Logs.DatePickers.tsx @@ -1,5 +1,3 @@ -import { Label } from '@ui/components/shadcn/ui/label' -import { RadioGroup, RadioGroupItem } from '@ui/components/shadcn/ui/radio-group' import dayjs from 'dayjs' import { Clock, HistoryIcon, Lock } from 'lucide-react' import type { PropsWithChildren } from 'react' @@ -11,9 +9,12 @@ import { cn, copyToClipboard, Input, + Label, Popover, PopoverContent, PopoverTrigger, + RadioGroup, + RadioGroupItem, } from 'ui' import { LOGS_LARGE_DATE_RANGE_DAYS_THRESHOLD } from './Logs.constants' diff --git a/apps/studio/components/interfaces/Settings/Logs/LogsFilterPopover.tsx b/apps/studio/components/interfaces/Settings/Logs/LogsFilterPopover.tsx index bd606e42e9a51..df7de4a14807b 100644 --- a/apps/studio/components/interfaces/Settings/Logs/LogsFilterPopover.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/LogsFilterPopover.tsx @@ -1,8 +1,5 @@ -import { Checkbox } from '@ui/components/shadcn/ui/checkbox' -import { Label } from '@ui/components/shadcn/ui/label' -import { Popover, PopoverContent, PopoverTrigger } from '@ui/components/shadcn/ui/popover' import { useEffect, useMemo, useState } from 'react' -import { Button, cn } from 'ui' +import { Button, Checkbox, cn, Label, Popover, PopoverContent, PopoverTrigger } from 'ui' import type { Filters, FilterSet } from './Logs.types' diff --git a/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx b/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx index f4f54e73b91bd..725327e230165 100644 --- a/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx +++ b/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx @@ -1,6 +1,5 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { AlertTitle } from '@ui/components/shadcn/ui/alert' import { useParams } from 'common' import Link from 'next/link' import { useEffect, useState } from 'react' @@ -9,6 +8,7 @@ import { toast } from 'sonner' import { Alert, AlertDescription, + AlertTitle, Button, Card, CardContent, diff --git a/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx b/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx index 9db5c0488e571..eb92e6c157085 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx @@ -1,8 +1,7 @@ import { useBreakpoint } from 'common' import { ArrowUp, Loader2, Square } from 'lucide-react' import { ChangeEvent, FormEvent, forwardRef, KeyboardEvent, memo, useRef } from 'react' -import { ExpandingTextArea } from 'ui' -import { cn } from 'ui/src/lib/utils' +import { cn, ExpandingTextArea } from 'ui' import { ButtonTooltip } from '../ButtonTooltip' import { formatAttachedSnippets } from './AIAssistant.utils' diff --git a/apps/studio/components/ui/DatePicker/index.tsx b/apps/studio/components/ui/DatePicker/index.tsx index df04c8977dd8a..bf42d1be0abcc 100644 --- a/apps/studio/components/ui/DatePicker/index.tsx +++ b/apps/studio/components/ui/DatePicker/index.tsx @@ -4,13 +4,13 @@ import { ArrowRight, Calendar } from 'lucide-react' import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react' import { Button, + ButtonProps, Calendar as CalendarPicker, Popover, PopoverContent, PopoverSeparator, PopoverTrigger, } from 'ui' -import { ButtonProps } from 'ui/src/components/Button/Button' import { TimeSplitInput } from './TimeSplitInput' import type { DatePickerToFrom } from '@/components/interfaces/Settings/Logs/Logs.types' diff --git a/apps/studio/components/ui/Shortcut.tsx b/apps/studio/components/ui/Shortcut.tsx index 7cf020951f615..e166e9c821089 100644 --- a/apps/studio/components/ui/Shortcut.tsx +++ b/apps/studio/components/ui/Shortcut.tsx @@ -1,5 +1,5 @@ -import { TooltipContentProps } from '@ui/components/shadcn/ui/tooltip' import type { ReactNode } from 'react' +import { TooltipContentProps } from 'ui' import { ShortcutTooltip } from './ShortcutTooltip' import type { ShortcutId } from '@/state/shortcuts/registry' diff --git a/apps/studio/components/ui/ShortcutTooltip.tsx b/apps/studio/components/ui/ShortcutTooltip.tsx index 23bc80df5b7aa..0a1b7c68f876d 100644 --- a/apps/studio/components/ui/ShortcutTooltip.tsx +++ b/apps/studio/components/ui/ShortcutTooltip.tsx @@ -1,7 +1,6 @@ import { HotkeySequence } from '@tanstack/react-hotkeys' -import { TooltipContentProps } from '@ui/components/shadcn/ui/tooltip' import { Fragment, useState, type ReactNode } from 'react' -import { KeyboardShortcut, Tooltip, TooltipContent, TooltipTrigger } from 'ui' +import { KeyboardShortcut, Tooltip, TooltipContent, TooltipContentProps, TooltipTrigger } from 'ui' import { hotkeyToKeys } from '@/state/shortcuts/formatShortcut' import { SHORTCUT_DEFINITIONS, type ShortcutId } from '@/state/shortcuts/registry' diff --git a/apps/studio/tsconfig.json b/apps/studio/tsconfig.json index ab88b97192411..0a9beb902834d 100644 --- a/apps/studio/tsconfig.json +++ b/apps/studio/tsconfig.json @@ -7,8 +7,7 @@ "noUnusedParameters": true, "lib": ["dom", "dom.iterable", "esnext"], "paths": { - "@/*": ["./*"], - "@ui/*": ["./../../packages/ui/src/*"] // handle ui package paths + "@/*": ["./*"] }, "plugins": [{ "name": "next" }], "jsx": "react-jsx", diff --git a/apps/www/components/Forms/ApplyToSupaSquadForm.tsx b/apps/www/components/Forms/ApplyToSupaSquadForm.tsx index 45470d7621694..564ed774f315c 100644 --- a/apps/www/components/Forms/ApplyToSupaSquadForm.tsx +++ b/apps/www/components/Forms/ApplyToSupaSquadForm.tsx @@ -7,6 +7,15 @@ import { AlertCircle } from 'lucide-react' import { FC, memo, useEffect, useState } from 'react' import { useForm } from 'react-hook-form' import { + Alert, + AlertDescription, + AlertDialog, + AlertDialogAction, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, Button, Form, FormControl, @@ -26,16 +35,6 @@ import { MultiSelectorList, MultiSelectorTrigger, } from 'ui-patterns/multi-select' -import { Alert, AlertDescription } from 'ui/src/components/shadcn/ui/alert' -import { - AlertDialog, - AlertDialogAction, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from 'ui/src/components/shadcn/ui/alert-dialog' import { CountrySelector } from '../Supasquad/CountrySelector' diff --git a/apps/www/components/Forms/RequestADemoForm.tsx b/apps/www/components/Forms/RequestADemoForm.tsx index ebe80dc4eb7e8..67656f9cf9d49 100644 --- a/apps/www/components/Forms/RequestADemoForm.tsx +++ b/apps/www/components/Forms/RequestADemoForm.tsx @@ -2,8 +2,7 @@ import { useSendTelemetryEvent } from '~/lib/telemetry' import { CircleAlert } from 'lucide-react' import Link from 'next/link' import { FC, useEffect, useState } from 'react' -import { Button, cn, Input, Label, Separator, TextArea } from 'ui' -import { Alert } from 'ui/src/components/shadcn/ui/alert' +import { Alert, Button, cn, Input, Label, Separator, TextArea } from 'ui' interface FormData { firstName: string diff --git a/apps/www/components/Forms/TalkToPartnershipTeamForm.tsx b/apps/www/components/Forms/TalkToPartnershipTeamForm.tsx index 64a24982746c5..4c0dfaaa905b0 100644 --- a/apps/www/components/Forms/TalkToPartnershipTeamForm.tsx +++ b/apps/www/components/Forms/TalkToPartnershipTeamForm.tsx @@ -2,8 +2,7 @@ import { useSendTelemetryEvent } from '~/lib/telemetry' import { CircleAlert } from 'lucide-react' import Link from 'next/link' import { FC, useEffect, useState } from 'react' -import { Button, cn, Input, Label, Separator, TextArea } from 'ui' -import { Alert } from 'ui/src/components/shadcn/ui/alert' +import { Alert, Button, cn, Input, Label, Separator, TextArea } from 'ui' interface FormData { firstName: string diff --git a/apps/www/components/Nav/index.tsx b/apps/www/components/Nav/index.tsx index 5c20e1fa310f2..1f45ce23f2e3d 100644 --- a/apps/www/components/Nav/index.tsx +++ b/apps/www/components/Nav/index.tsx @@ -9,17 +9,19 @@ import Link from 'next/link' import { usePathname } from 'next/navigation' import React, { useState } from 'react' import { useWindowSize } from 'react-use' -import { Button, buttonVariants, cn } from 'ui' -import { AuthenticatedDropdownMenu } from 'ui-patterns/AuthenticatedDropdownMenu' -import { AnnouncementBanner } from 'ui-patterns/Banners/AnnouncementBanner' import { + Button, + buttonVariants, + cn, NavigationMenu, NavigationMenuContent, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, -} from 'ui/src/components/shadcn/ui/navigation-menu' +} from 'ui' +import { AuthenticatedDropdownMenu } from 'ui-patterns/AuthenticatedDropdownMenu' +import { AnnouncementBanner } from 'ui-patterns/Banners/AnnouncementBanner' import GitHubButton from './GitHubButton' import HamburgerButton from './HamburgerMenu' diff --git a/apps/www/components/Pricing/ComputePricingCalculator.tsx b/apps/www/components/Pricing/ComputePricingCalculator.tsx index 130d4f6b523d4..99c4cc43370a4 100644 --- a/apps/www/components/Pricing/ComputePricingCalculator.tsx +++ b/apps/www/components/Pricing/ComputePricingCalculator.tsx @@ -2,10 +2,9 @@ import pricingAddOn from '~/data/PricingAddOnTable.json' import { Plus, Trash2 } from 'lucide-react' import { useEffect, useState } from 'react' import { plans as allPlans } from 'shared-data/plans' -import { Button, cn, Slider } from 'ui' +import { Button, cn, Slider, ToggleGroup, ToggleGroupItem } from 'ui' import { ComputeBadge } from 'ui-patterns/ComputeBadge' import { InfoTooltip } from 'ui-patterns/info-tooltip' -import { ToggleGroup, ToggleGroupItem } from 'ui/src/components/shadcn/ui/toggle-group' const STANDALONE_PLANS = allPlans .filter((plan) => plan.planId === 'pro' || plan.planId === 'team') diff --git a/apps/www/components/Pricing/PricingComputeSection.tsx b/apps/www/components/Pricing/PricingComputeSection.tsx index 861dc6454bc16..1d3ff037f7f2d 100644 --- a/apps/www/components/Pricing/PricingComputeSection.tsx +++ b/apps/www/components/Pricing/PricingComputeSection.tsx @@ -5,8 +5,7 @@ import Link from 'next/link' import React, { useEffect, useRef, useState } from 'react' import { useWindowSize } from 'react-use' import { plans as allPlans } from 'shared-data/plans' -import { Button, cn } from 'ui' -import { ToggleGroup, ToggleGroupItem } from 'ui/src/components/shadcn/ui/toggle-group' +import { Button, cn, ToggleGroup, ToggleGroupItem } from 'ui' import Panel from '../Panel' import ComputePricingCalculator from './ComputePricingCalculator' diff --git a/packages/ui-patterns/src/AssistantChat/AssistantChatForm.tsx b/packages/ui-patterns/src/AssistantChat/AssistantChatForm.tsx index 53d8313dface6..b7a25b85cd84c 100644 --- a/packages/ui-patterns/src/AssistantChat/AssistantChatForm.tsx +++ b/packages/ui-patterns/src/AssistantChat/AssistantChatForm.tsx @@ -3,8 +3,7 @@ import { useBreakpoint } from 'common' import { Loader2 } from 'lucide-react' import React, { ChangeEvent, memo, useRef } from 'react' -import { ExpandingTextArea } from 'ui' -import { cn } from 'ui/src/lib/utils' +import { cn, ExpandingTextArea } from 'ui' export interface FormProps extends React.FormHTMLAttributes { /* The ref for the textarea, optional. Exposed for the CommandsPopover to attach events. */ diff --git a/packages/ui-patterns/src/AssistantChat/AssistantCommandsPopover.tsx b/packages/ui-patterns/src/AssistantChat/AssistantCommandsPopover.tsx index b4a1226f278be..8ef147cb66cd0 100644 --- a/packages/ui-patterns/src/AssistantChat/AssistantCommandsPopover.tsx +++ b/packages/ui-patterns/src/AssistantChat/AssistantCommandsPopover.tsx @@ -12,6 +12,7 @@ import { useRef, } from 'react' import { + cn, Command, CommandEmpty, CommandGroup, @@ -19,9 +20,10 @@ import { CommandItem, CommandList, CommandSeparator, -} from 'ui/src/components/shadcn/ui/command' -import { Popover, PopoverAnchor, PopoverContent } from 'ui/src/components/shadcn/ui/popover' -import { cn } from 'ui/src/lib/utils/cn' + Popover, + PopoverAnchor, + PopoverContent, +} from 'ui' const AssistantCommandsPopover = forwardRef< ComponentRef, diff --git a/packages/ui-patterns/src/Banners/LW15Banner.tsx b/packages/ui-patterns/src/Banners/LW15Banner.tsx index b5eca5bcd0153..2959329860932 100644 --- a/packages/ui-patterns/src/Banners/LW15Banner.tsx +++ b/packages/ui-patterns/src/Banners/LW15Banner.tsx @@ -3,7 +3,7 @@ import Link from 'next/link' import { usePathname } from 'next/navigation' import { useEffect, useRef } from 'react' -import { Button } from 'ui/src/components/Button' +import { Button } from 'ui' import announcement from '../Banners/data.json' diff --git a/packages/ui-patterns/src/CommandMenu/prepackaged/DocsAi/DocsAiPage.tsx b/packages/ui-patterns/src/CommandMenu/prepackaged/DocsAi/DocsAiPage.tsx index d9bbf5d750711..113409ae90d8a 100644 --- a/packages/ui-patterns/src/CommandMenu/prepackaged/DocsAi/DocsAiPage.tsx +++ b/packages/ui-patterns/src/CommandMenu/prepackaged/DocsAi/DocsAiPage.tsx @@ -5,9 +5,8 @@ import { User } from 'lucide-react' import { Fragment, useCallback, useEffect, useRef, useState } from 'react' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' -import { AiIconAnimation, Button, cn, CommandGroup, CommandItem, CommandList } from 'ui' +import { AiIconAnimation, Button, cn, CommandGroup, CommandItem, CommandList, StatusIcon } from 'ui' import { markdownComponents } from 'ui-patterns/Markdown' -import { StatusIcon } from 'ui/src/components/StatusIcon' import { Breadcrumb, diff --git a/packages/ui-patterns/src/CommandMenu/prepackaged/DocsSearch/DocsSearchPage.tsx b/packages/ui-patterns/src/CommandMenu/prepackaged/DocsSearch/DocsSearchPage.tsx index 0f27b16def4dd..378b2fedbdcd4 100644 --- a/packages/ui-patterns/src/CommandMenu/prepackaged/DocsSearch/DocsSearchPage.tsx +++ b/packages/ui-patterns/src/CommandMenu/prepackaged/DocsSearch/DocsSearchPage.tsx @@ -8,8 +8,7 @@ import { } from 'common' import { Book, ChevronRight, Github, Hash, Loader2, MessageSquare, Search } from 'lucide-react' import { useCallback, useEffect, useRef } from 'react' -import { Button, cn, CommandGroup, CommandItem, CommandList } from 'ui' -import { StatusIcon } from 'ui/src/components/StatusIcon' +import { Button, cn, CommandGroup, CommandItem, CommandList, StatusIcon } from 'ui' import { Breadcrumb, diff --git a/packages/ui-patterns/src/CountdownWidget/index.tsx b/packages/ui-patterns/src/CountdownWidget/index.tsx index 744fe7e701cae..83420ca70005a 100644 --- a/packages/ui-patterns/src/CountdownWidget/index.tsx +++ b/packages/ui-patterns/src/CountdownWidget/index.tsx @@ -1,4 +1,4 @@ -import { cn } from 'ui/src/lib/utils' +import { cn } from 'ui' import CountdownStep from './CountdownStep' diff --git a/packages/ui-patterns/src/Dialogs/ConfirmationModal.tsx b/packages/ui-patterns/src/Dialogs/ConfirmationModal.tsx index 89460076fbd10..5e246f02c9b88 100644 --- a/packages/ui-patterns/src/Dialogs/ConfirmationModal.tsx +++ b/packages/ui-patterns/src/Dialogs/ConfirmationModal.tsx @@ -10,11 +10,12 @@ import { cn, Dialog, DialogContent, + DialogDescription, + DialogHeader, DialogSection, DialogSectionSeparator, DialogTitle, } from 'ui' -import { DialogDescription, DialogHeader } from 'ui/src/components/shadcn/ui/dialog' import { Admonition } from '../Admonition' diff --git a/packages/ui-patterns/src/Dialogs/TextConfirmModal.tsx b/packages/ui-patterns/src/Dialogs/TextConfirmModal.tsx index c3e156a071c6f..498178245f8b5 100644 --- a/packages/ui-patterns/src/Dialogs/TextConfirmModal.tsx +++ b/packages/ui-patterns/src/Dialogs/TextConfirmModal.tsx @@ -14,6 +14,7 @@ import { copyToClipboard, Dialog, DialogContent, + DialogHeader, DialogSection, DialogSectionSeparator, DialogTitle, @@ -26,7 +27,6 @@ import { FormMessage, Input, } from 'ui' -import { DialogHeader } from 'ui/src/components/shadcn/ui/dialog' import { z } from 'zod' import { Admonition } from '../Admonition' diff --git a/packages/ui-patterns/src/Markdown/index.tsx b/packages/ui-patterns/src/Markdown/index.tsx index 7074a36646f08..5223dae6646ef 100644 --- a/packages/ui-patterns/src/Markdown/index.tsx +++ b/packages/ui-patterns/src/Markdown/index.tsx @@ -3,8 +3,7 @@ import React from 'react' import ReactMarkdown, { type Components, type Options } from 'react-markdown' import remarkGfm from 'remark-gfm' -import { cn } from 'ui' -import { Heading } from 'ui/src/components/CustomHTMLElements' +import { cn, Heading } from 'ui' import { Avatar, diff --git a/packages/ui-patterns/src/McpUrlBuilder/components/McpConfigurationOptions.tsx b/packages/ui-patterns/src/McpUrlBuilder/components/McpConfigurationOptions.tsx index 695cea5834b44..3729a0c7dc094 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/components/McpConfigurationOptions.tsx +++ b/packages/ui-patterns/src/McpUrlBuilder/components/McpConfigurationOptions.tsx @@ -1,7 +1,6 @@ 'use client' -import { cn, Switch } from 'ui' -import { Label } from 'ui/src/components/shadcn/ui/label' +import { cn, Label, Switch } from 'ui' import { InfoTooltip } from '../../info-tooltip' import { diff --git a/packages/ui-patterns/src/MobileSheetNav/MobileSheetNav.tsx b/packages/ui-patterns/src/MobileSheetNav/MobileSheetNav.tsx index 47ebc96ae86ce..89a3d220866a0 100644 --- a/packages/ui-patterns/src/MobileSheetNav/MobileSheetNav.tsx +++ b/packages/ui-patterns/src/MobileSheetNav/MobileSheetNav.tsx @@ -4,8 +4,7 @@ import { useRouter } from 'next/router' import { ComponentProps, useEffect } from 'react' import { ErrorBoundary } from 'react-error-boundary' import { useWindowSize } from 'react-use' -import { CommandEmpty, Sheet, SheetContent } from 'ui' -import { cn } from 'ui/src/lib/utils' +import { cn, CommandEmpty, Sheet, SheetContent } from 'ui' const MobileSheetNav: React.FC<{ children: React.ReactNode diff --git a/packages/ui-patterns/src/PromoToast/PromoBg.tsx b/packages/ui-patterns/src/PromoToast/PromoBg.tsx index c30b0fd62202d..79564da96127d 100644 --- a/packages/ui-patterns/src/PromoToast/PromoBg.tsx +++ b/packages/ui-patterns/src/PromoToast/PromoBg.tsx @@ -1,4 +1,4 @@ -import { cn } from 'ui/src/lib/utils/cn' +import { cn } from 'ui' interface Props { className?: string diff --git a/packages/ui-patterns/src/PromoToast/PromoToast.tsx b/packages/ui-patterns/src/PromoToast/PromoToast.tsx index ac2db741c212a..7f9be18587f0b 100644 --- a/packages/ui-patterns/src/PromoToast/PromoToast.tsx +++ b/packages/ui-patterns/src/PromoToast/PromoToast.tsx @@ -3,8 +3,7 @@ import { hasConsented, LOCAL_STORAGE_KEYS } from 'common' import Link from 'next/link' import { useEffect, useState } from 'react' -import { Button } from 'ui/src/components/Button/Button' -import { cn } from 'ui/src/lib/utils/cn' +import { Button, cn } from 'ui' import announcement from '../Banners/data.json' diff --git a/packages/ui-patterns/src/SqlToRest/base-url-dialog.tsx b/packages/ui-patterns/src/SqlToRest/base-url-dialog.tsx index b0d422defc786..fc1a743b299b6 100644 --- a/packages/ui-patterns/src/SqlToRest/base-url-dialog.tsx +++ b/packages/ui-patterns/src/SqlToRest/base-url-dialog.tsx @@ -1,14 +1,14 @@ import { useState } from 'react' -import { Alert } from 'ui/src/components/shadcn/ui/alert' -import { Button } from 'ui/src/components/shadcn/ui/button' import { + Alert, + Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, -} from 'ui/src/components/shadcn/ui/dialog' +} from 'ui' import { Input } from '../DataInputs/Input' diff --git a/packages/ui-patterns/src/Toc/toc.tsx b/packages/ui-patterns/src/Toc/toc.tsx index 7297ba2b9ff5a..442b2d3963dc8 100644 --- a/packages/ui-patterns/src/Toc/toc.tsx +++ b/packages/ui-patterns/src/Toc/toc.tsx @@ -1,8 +1,7 @@ 'use client' import { Fragment, useRef, type ComponentProps, type HTMLAttributes, type ReactNode } from 'react' -import { cn, ScrollArea, ScrollViewport } from 'ui' -import { removeAnchor } from 'ui/src/components/CustomHTMLElements/CustomHTMLElements.utils' +import { cn, removeAnchor, ScrollArea, ScrollViewport } from 'ui' import type { TOCItemType } from './server/get-toc' import * as Primitive from './toc-primitive' diff --git a/packages/ui-patterns/src/TweetCard/index.tsx b/packages/ui-patterns/src/TweetCard/index.tsx index 806d57178bd27..1d1f16e7e62f6 100644 --- a/packages/ui-patterns/src/TweetCard/index.tsx +++ b/packages/ui-patterns/src/TweetCard/index.tsx @@ -1,5 +1,5 @@ import Image from 'next/image' -import { cn } from 'ui/src/lib/utils' +import { cn } from 'ui' interface TweetCard { handle: string diff --git a/packages/ui-patterns/src/collapsible-alert.tsx b/packages/ui-patterns/src/collapsible-alert.tsx index 0b17745e98b2d..b83b8bc27d0e0 100644 --- a/packages/ui-patterns/src/collapsible-alert.tsx +++ b/packages/ui-patterns/src/collapsible-alert.tsx @@ -3,13 +3,7 @@ import { type VariantProps } from 'class-variance-authority' import { ChevronDown } from 'lucide-react' import * as React from 'react' -import { Button, cn } from 'ui' -import { alertVariants } from 'ui/src/components/shadcn/ui/alert' -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from 'ui/src/components/shadcn/ui/collapsible' +import { alertVariants, Button, cn, Collapsible, CollapsibleContent, CollapsibleTrigger } from 'ui' export interface CollapsibleAlertProps extends Omit, 'title'>, VariantProps { diff --git a/packages/ui-patterns/src/form/Layout/FormLayout.tsx b/packages/ui-patterns/src/form/Layout/FormLayout.tsx index 2b2db87dca161..284e6a9faaf00 100644 --- a/packages/ui-patterns/src/form/Layout/FormLayout.tsx +++ b/packages/ui-patterns/src/form/Layout/FormLayout.tsx @@ -1,7 +1,6 @@ import { cva } from 'class-variance-authority' import React from 'react' -import { cn, FormDescription, FormLabel, FormMessage, Label } from 'ui' -import { SIZE } from 'ui/src/lib/constants' +import { cn, FormDescription, FormLabel, FormMessage, Label, SIZE } from 'ui' type Props = { align?: 'left' | 'right' diff --git a/packages/ui-patterns/src/multi-select/multi-select.tsx b/packages/ui-patterns/src/multi-select/multi-select.tsx index 9e5e63eca9ce5..b74627751c21b 100644 --- a/packages/ui-patterns/src/multi-select/multi-select.tsx +++ b/packages/ui-patterns/src/multi-select/multi-select.tsx @@ -17,8 +17,9 @@ import { PopoverAnchor, PopoverContent, PopoverContentProps, + SIZE_VARIANTS, + SIZE_VARIANTS_DEFAULT, } from 'ui' -import { SIZE_VARIANTS, SIZE_VARIANTS_DEFAULT } from 'ui/src/lib/constants' interface MultiSelectContextProps { id: string diff --git a/packages/ui/index.tsx b/packages/ui/index.tsx index ebbc9499e849e..8630256e6a676 100644 --- a/packages/ui/index.tsx +++ b/packages/ui/index.tsx @@ -14,7 +14,12 @@ export * from './src/components/SidePanel' // HTML -export { Heading } from './src/components/CustomHTMLElements' +export { + Heading, + getAnchor, + removeAnchor, + highlightSelectedNavItem, +} from './src/components/CustomHTMLElements' // UTILITIES @@ -31,6 +36,7 @@ export * from './src/components/LoadingLine' // ai icon export * from './src/layout/ai-icon-animation' +export { default as aiIconAnimationStyles } from './src/layout/ai-icon-animation/ai-icon-animation-style.module.css' // theme switcher export * from './src/components/ThemeProvider/singleThemes' @@ -160,3 +166,6 @@ export * from './src/lib/Hooks' export * from './src/components/hooks/use-mobile' export * from './src/components/KeyboardShortcut/KeyboardShortcut' + +export * as colors from './src/lib/tailwind-demo-classes' +export * from './src/lib/constants' diff --git a/packages/ui/src/components/CustomHTMLElements/index.tsx b/packages/ui/src/components/CustomHTMLElements/index.tsx index 51b2839dcc131..14440ef444752 100644 --- a/packages/ui/src/components/CustomHTMLElements/index.tsx +++ b/packages/ui/src/components/CustomHTMLElements/index.tsx @@ -1,5 +1,4 @@ import Heading from './Heading' export { Heading } - -export default Heading +export { getAnchor, removeAnchor, highlightSelectedNavItem } from './CustomHTMLElements.utils' From b278b1ec8a173158fa3dde096fed4bc7dc1a7744 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Tue, 1 Sep 2026 08:27:38 -0400 Subject: [PATCH 02/16] fix(studio): Debug with Assistant and Copy prompt work (#49690) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary * Resolved hanging buttons in Explorer's QueryResultError panel that were wired to stub no-ops (`buildPrompt={() => ''}`, `onOpenAssistant={() => {}}`). * "Debug with Assistant" now opens a new chat seeded with a real prompt combining the SQL query and error context using existing `buildDebugPromptText` util and `useCreateChat` hook. * "Copy prompt" now copies the same real debug prompt text to clipboard. * Threaded `sql` and query `source` props down through `QueryEditor` → `QueryResultRenderer` → `QueryResultError` while keeping them optional for backward compatibility with other callers like `AssistantNotebookPreviewCell`. ## Test plan - [X] Run `pnpm typecheck` — passes - [X] Run `pnpm lint --filter=studio` — passes - [X] Run `pnpm test:studio --run apps/studio/components/interfaces/Explorer/QueryEditor` — Explorer vitest suite (99 tests across 13 files) passes with no regressions - [X] Manually verify in Explorer: trigger an ad-hoc SQL query that fails, confirm "Debug with Assistant" opens a new chat with the error prompt seeded, and "Copy prompt" copies the prompt to clipboard ## Summary by CodeRabbit - **New Features** - Added “Debug with Assistant” to query errors using the submitted SQL and error details. - Added an option to copy the debugging prompt for easier troubleshooting. - Assistant actions are hidden when query details are unavailable or restricted. - **Bug Fixes** - Ensured query errors reference the SQL that produced them, rather than later editor changes. - **Tests** - Added coverage for assistant debugging, prompt copying, conditional visibility, and self-hosted behavior. --- .../QueryResultError.selfhosted.test.tsx | 82 +++++++++ .../QueryEditor/QueryResultError.test.tsx | 161 ++++++++++++++++++ .../Explorer/QueryEditor/QueryResultError.tsx | 50 +++++- .../QueryEditor/QueryResultRenderer.tsx | 14 +- .../interfaces/Explorer/QueryEditor/index.tsx | 35 +++- .../components/interfaces/Explorer/types.ts | 4 + 6 files changed, 327 insertions(+), 19 deletions(-) create mode 100644 apps/studio/components/interfaces/Explorer/QueryEditor/QueryResultError.selfhosted.test.tsx create mode 100644 apps/studio/components/interfaces/Explorer/QueryEditor/QueryResultError.test.tsx diff --git a/apps/studio/components/interfaces/Explorer/QueryEditor/QueryResultError.selfhosted.test.tsx b/apps/studio/components/interfaces/Explorer/QueryEditor/QueryResultError.selfhosted.test.tsx new file mode 100644 index 0000000000000..7ec4302a2fa38 --- /dev/null +++ b/apps/studio/components/interfaces/Explorer/QueryEditor/QueryResultError.selfhosted.test.tsx @@ -0,0 +1,82 @@ +import { screen } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { QueryResultError } from './QueryResultError' +import { customRender } from '@/tests/lib/custom-render' +import { addAPIMock } from '@/tests/lib/msw' + +const mocks = vi.hoisted(() => ({ + createChat: vi.fn(), + useParams: vi.fn(), +})) + +vi.mock('@/lib/constants', async () => { + const actual = await vi.importActual>('@/lib/constants') + return { ...actual, IS_PLATFORM: false } +}) + +vi.mock('common', async (importOriginal) => { + const actual = await importOriginal() + return { ...actual, useParams: () => mocks.useParams() } +}) + +vi.mock('../hooks', () => ({ + useCreateChat: () => ({ createChat: mocks.createChat, isCreating: false }), +})) + +vi.mock('@/hooks/misc/useSelectedOrganization', () => ({ + useSelectedOrganizationQuery: () => ({ data: undefined }), +})) + +// Self-hosted has no orgs/billing, so these eligibility queries are expected to never +// resolve (disabled or failing) - the dropdown must not stay hidden waiting on them. +vi.mock('@/data/subscriptions/org-subscription-query', () => ({ + useOrgSubscriptionQuery: () => ({ data: undefined, isSuccess: false }), +})) + +vi.mock('@/data/config/project-settings-v2-query', () => ({ + useProjectSettingsV2Query: () => ({ data: undefined, isSuccess: false }), +})) + +describe('QueryResultError (self-hosted)', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.useParams.mockReturnValue({ ref: 'default' }) + addAPIMock({ + method: 'get', + path: '/platform/projects/:ref', + response: { + id: 1, + ref: 'default', + organization_id: 1, + name: 'Test Project', + status: 'ACTIVE_HEALTHY', + cloud_provider: 'AWS', + region: 'us-east-1', + db_host: 'db.default.supabase.co', + restUrl: 'https://default.supabase.co/rest/v1/', + inserted_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', + subscription_id: 'sub_123', + is_branch_enabled: false, + is_physical_backups_enabled: false, + high_availability: false, + integration_source: null, + connectionString: 'postgresql://postgres@localhost:5432/postgres', + is_hibernating: false, + }, + }) + }) + + it('renders the assistant dropdown without waiting on HIPAA eligibility queries', () => { + customRender( + + ) + + expect(screen.getByRole('button', { name: 'Debug with Assistant' })).toBeInTheDocument() + }) +}) diff --git a/apps/studio/components/interfaces/Explorer/QueryEditor/QueryResultError.test.tsx b/apps/studio/components/interfaces/Explorer/QueryEditor/QueryResultError.test.tsx new file mode 100644 index 0000000000000..574860d4a9f3a --- /dev/null +++ b/apps/studio/components/interfaces/Explorer/QueryEditor/QueryResultError.test.tsx @@ -0,0 +1,161 @@ +import { fireEvent, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { QueryResultError } from './QueryResultError' +import { customRender } from '@/tests/lib/custom-render' +import { addAPIMock } from '@/tests/lib/msw' + +const mocks = vi.hoisted(() => ({ + createChat: vi.fn(), + useParams: vi.fn(), + mockCopyToClipboard: vi.fn(), + useOrgSubscriptionQuery: vi.fn(), + useProjectSettingsV2Query: vi.fn(), +})) + +vi.mock('common', async (importOriginal) => { + const actual = await importOriginal() + return { ...actual, useParams: () => mocks.useParams() } +}) + +// This file covers the platform-mode HIPAA eligibility gate; the self-hosted bypass is +// covered separately in QueryResultError.selfhosted.test.tsx. +vi.mock('@/lib/constants', async () => { + const actual = await vi.importActual>('@/lib/constants') + return { ...actual, IS_PLATFORM: true } +}) + +// CopyButton and AiAssistantDropdown write via copyToClipboard from 'ui'. Stub just that +// export so we can assert the value handed to the clipboard without depending on jsdom's +// document.hasFocus() / navigator.clipboard. Everything else in 'ui' stays real. +vi.mock('ui', async (importOriginal) => ({ + ...(await importOriginal()), + copyToClipboard: mocks.mockCopyToClipboard, +})) + +vi.mock('../hooks', () => ({ + useCreateChat: () => ({ createChat: mocks.createChat, isCreating: false }), +})) + +vi.mock('@/hooks/misc/useSelectedOrganization', () => ({ + useSelectedOrganizationQuery: () => ({ data: { slug: 'default-org' } }), +})) + +vi.mock('@/data/subscriptions/org-subscription-query', () => ({ + useOrgSubscriptionQuery: () => mocks.useOrgSubscriptionQuery(), +})) + +vi.mock('@/data/config/project-settings-v2-query', () => ({ + useProjectSettingsV2Query: () => mocks.useProjectSettingsV2Query(), +})) + +describe('QueryResultError', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.useParams.mockReturnValue({ ref: 'default' }) + mocks.useOrgSubscriptionQuery.mockReturnValue({ data: undefined, isSuccess: true }) + mocks.useProjectSettingsV2Query.mockReturnValue({ data: undefined, isSuccess: true }) + // useTrack() (invoked by AiAssistantDropdown) reads the selected project to attach + // telemetry context, so the platform project fetch needs a handler even though this + // component doesn't read project data itself. + addAPIMock({ + method: 'get', + path: '/platform/projects/:ref', + response: { + id: 1, + ref: 'default', + organization_id: 1, + name: 'Test Project', + status: 'ACTIVE_HEALTHY', + cloud_provider: 'AWS', + region: 'us-east-1', + db_host: 'db.default.supabase.co', + restUrl: 'https://default.supabase.co/rest/v1/', + inserted_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', + subscription_id: 'sub_123', + is_branch_enabled: false, + is_physical_backups_enabled: false, + high_availability: false, + integration_source: null, + connectionString: 'postgresql://postgres@localhost:5432/postgres', + is_hibernating: false, + }, + }) + }) + + it('opens a new assistant chat seeded with the query and error when debugging', () => { + customRender( + + ) + + fireEvent.click(screen.getByRole('button', { name: 'Debug with Assistant' })) + + expect(mocks.createChat).toHaveBeenCalledWith({ + name: 'Debug SQL snippet', + initialMessage: expect.stringContaining('select * from foo;'), + }) + expect(mocks.createChat.mock.calls[0][0].initialMessage).toContain( + 'relation "foo" does not exist' + ) + }) + + it('copies the same debug prompt text via the dropdown', async () => { + const user = userEvent.setup() + customRender( + + ) + + await user.click(screen.getByRole('button', { name: 'More actions' })) + await user.click(await screen.findByText('Copy prompt')) + + expect(mocks.mockCopyToClipboard).toHaveBeenCalledWith( + expect.stringContaining('select * from foo;') + ) + }) + + it('does not render the assistant dropdown when the query is unavailable', () => { + customRender() + + expect(screen.queryByRole('button', { name: 'Debug with Assistant' })).not.toBeInTheDocument() + }) + + it('does not render the assistant dropdown while HIPAA eligibility is still resolving', () => { + mocks.useOrgSubscriptionQuery.mockReturnValue({ data: undefined, isSuccess: false }) + + customRender( + + ) + + expect(screen.queryByRole('button', { name: 'Debug with Assistant' })).not.toBeInTheDocument() + }) + + it('does not render the assistant dropdown when an eligibility query is disabled or failed', () => { + // A disabled or failed query also settles with isSuccess: false forever - same as + // still-loading from this component's point of view, so it stays denied. + mocks.useProjectSettingsV2Query.mockReturnValue({ data: undefined, isSuccess: false }) + + customRender( + + ) + + expect(screen.queryByRole('button', { name: 'Debug with Assistant' })).not.toBeInTheDocument() + }) +}) diff --git a/apps/studio/components/interfaces/Explorer/QueryEditor/QueryResultError.tsx b/apps/studio/components/interfaces/Explorer/QueryEditor/QueryResultError.tsx index 83cc108148964..d83b46ed85faa 100644 --- a/apps/studio/components/interfaces/Explorer/QueryEditor/QueryResultError.tsx +++ b/apps/studio/components/interfaces/Explorer/QueryEditor/QueryResultError.tsx @@ -1,9 +1,13 @@ import { useParams } from 'common' import { ExternalLink } from 'lucide-react' import { parseAsBoolean, useQueryState } from 'nuqs' +import { useCallback } from 'react' import { Button, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui' import { subscriptionHasHipaaAddon } from '../../Billing/Subscription/Subscription.utils' +import { type SqlSnippetSource } from '../../SQLEditor/querySource' +import { buildDebugPromptText } from '../../SQLEditor/SQLEditor.utils' +import { useCreateChat } from '../hooks' import { type QueryResult } from '../types' import { AiAssistantDropdown } from '@/components/ui/AiAssistantDropdown' import CopyButton from '@/components/ui/CopyButton' @@ -12,24 +16,53 @@ import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-que import { getSqlErrorLines } from '@/data/sql/utils' import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' -import { DOCS_URL } from '@/lib/constants' +import { DOCS_URL, IS_PLATFORM } from '@/lib/constants' export const QueryResultError = ({ error, autoLimit, + sql, + source, }: { error: NonNullable autoLimit?: QueryResult['autoLimit'] + sql?: string + source?: SqlSnippetSource }) => { const { ref } = useParams() const { data: org } = useSelectedOrganizationQuery() - const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: org?.slug }) - const { data: projectSettings } = useProjectSettingsV2Query({ projectRef: ref }) + const { data: subscription, isSuccess: isSubscriptionResolved } = useOrgSubscriptionQuery({ + orgSlug: org?.slug, + }) + const { data: projectSettings, isSuccess: isProjectSettingsResolved } = useProjectSettingsV2Query( + { + projectRef: ref, + } + ) const hasHipaaAddon = subscriptionHasHipaaAddon(subscription) && projectSettings?.is_sensitive + // Default deny until both eligibility queries have actually succeeded - a disabled or + // failed query also reports isLoading: false, so isLoading can't tell "confirmed no + // addon" apart from "don't know yet", and the assistant sends the SQL and error to an + // LLM. Self-hosted has no HIPAA concept at all (subscriptionHasHipaaAddon short-circuits + // to false there), so there's nothing to wait on outside of platform. + const isCheckingHipaaEligibility = + IS_PLATFORM && (!isSubscriptionResolved || !isProjectSettingsResolved) + + const { createChat, isCreating } = useCreateChat() const [, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false)) + const canDebug = sql !== undefined && source !== undefined + + const buildDebugPrompt = useCallback( + () => (canDebug ? buildDebugPromptText(sql, error.message, source) : ''), + [canDebug, sql, error.message, source] + ) + + const handleDebug = () => + createChat({ name: 'Debug SQL snippet', initialMessage: buildDebugPrompt() }) + const isTimeout = error.message?.includes('canceling statement due to statement timeout') || error.message?.includes('upstream request timeout') || @@ -136,15 +169,14 @@ export const QueryResultError = ({ )} - {!hasHipaaAddon && ( - // [Joshen] TODO + {!hasHipaaAddon && !isCheckingHipaaEligibility && canDebug && ( ''} - onOpenAssistant={() => {}} - disabled={false} - loading={false} + buildPrompt={buildDebugPrompt} + onOpenAssistant={handleDebug} + disabled={isCreating} + loading={isCreating} /> )} diff --git a/apps/studio/components/interfaces/Explorer/QueryEditor/QueryResultRenderer.tsx b/apps/studio/components/interfaces/Explorer/QueryEditor/QueryResultRenderer.tsx index 79fbc5a407f5c..52442928cfb33 100644 --- a/apps/studio/components/interfaces/Explorer/QueryEditor/QueryResultRenderer.tsx +++ b/apps/studio/components/interfaces/Explorer/QueryEditor/QueryResultRenderer.tsx @@ -1,3 +1,4 @@ +import { type SqlSnippetSource } from '../../SQLEditor/querySource' import { type QueryResult } from '../types' import { QueryResultChart } from './QueryResultChart' import { QueryResultError } from './QueryResultError' @@ -8,9 +9,18 @@ interface QueryResultRendererProps { result?: QueryResult view?: 'table' | 'chart' chart?: ChartConfig + /** The query that produced `result`, used to build the "Debug with Assistant" prompt on error. */ + sql?: string + source?: SqlSnippetSource } -export const QueryResultRenderer = ({ result, view, chart }: QueryResultRendererProps) => { +export const QueryResultRenderer = ({ + result, + view, + chart, + sql, + source, +}: QueryResultRendererProps) => { const { rows, error, autoLimit } = result ?? {} if (!result) { @@ -18,7 +28,7 @@ export const QueryResultRenderer = ({ result, view, chart }: QueryResultRenderer } if (error) { - return + return } if ((rows ?? []).length === 0) { diff --git a/apps/studio/components/interfaces/Explorer/QueryEditor/index.tsx b/apps/studio/components/interfaces/Explorer/QueryEditor/index.tsx index 11cfe9b4f2c22..7c729cee4e31c 100644 --- a/apps/studio/components/interfaces/Explorer/QueryEditor/index.tsx +++ b/apps/studio/components/interfaces/Explorer/QueryEditor/index.tsx @@ -227,13 +227,11 @@ export const QueryEditor = forwardRef(funct ) const { mutateAsync: executeSql, isPending: isExecutingSql } = useExecuteSqlMutation({ - onSuccess: (data) => onResultChange({ rows: data.result }), - onError: (error) => onResultChange({ error }), + onError: () => {}, }) const { mutateAsync: executeLogsSql, isPending: isExecutingLogs } = useExecuteLogsSqlMutation({ - onSuccess: (data) => onResultChange({ rows: data.rows as readonly Record[] }), - onError: (error) => onResultChange({ error }), + onError: () => {}, }) const isResolvingDatabase = @@ -259,6 +257,7 @@ export const QueryEditor = forwardRef(funct } onRun?.() + const querySnapshot = { sql: rawSql, source: query._tag } // [Joshen] This is deliberate to commit the sql, rather than the passed rawSql // As we want to save the cell's content into the store, rather than what's getting run onSqlCommit?.(sql) @@ -267,6 +266,7 @@ export const QueryEditor = forwardRef(funct if (!isOtelLogsEnabled) { onResultChange({ error: { message: "Querying logs isn't available for this project yet." }, + ...querySnapshot, }) return } @@ -276,7 +276,14 @@ export const QueryEditor = forwardRef(funct sql: acceptUntrustedLogsSql(untrustedLogSql(rawSql)), range: resolveLogTimeRange(query.time_range), endpoint: QUERY_SOURCE_REGISTRY.logs.endpoint, - }).catch(() => {}) + }).then( + (data) => + onResultChange({ + rows: data.rows as readonly Record[], + ...querySnapshot, + }), + (error) => onResultChange({ error, ...querySnapshot }) + ) return } @@ -284,7 +291,10 @@ export const QueryEditor = forwardRef(funct const limitedSql = applyAutoLimit(safeSql, rowLimit) if (!isValidConnString(connectionString)) { - onResultChange({ error: { message: 'Unable to run query: Connection string is missing' } }) + onResultChange({ + error: { message: 'Unable to run query: Connection string is missing' }, + ...querySnapshot, + }) return } @@ -296,7 +306,10 @@ export const QueryEditor = forwardRef(funct contextualInvalidation: true, isStatementTimeoutDisabled: true, isRoleImpersonationEnabled: isRoleImpersonationEnabled(roleImpersonationState?.role), - }).catch(() => {}) + }).then( + (data) => onResultChange({ rows: data.result, ...querySnapshot }), + (error) => onResultChange({ error, ...querySnapshot }) + ) } const handleConfirmPendingRun = () => { @@ -584,7 +597,13 @@ export const QueryEditor = forwardRef(funct : 'overflow-x-auto' )} > - + diff --git a/apps/studio/components/interfaces/Explorer/types.ts b/apps/studio/components/interfaces/Explorer/types.ts index 4109350cbae4c..b1f8898562624 100644 --- a/apps/studio/components/interfaces/Explorer/types.ts +++ b/apps/studio/components/interfaces/Explorer/types.ts @@ -1,9 +1,13 @@ +import { type SqlSnippetSource } from '../SQLEditor/querySource' import { type ChartConfig } from '@/data/content/notebooks/notebook-schema' export type QueryResult = { rows?: readonly Record[] error?: { message: string; formattedError?: string } autoLimit?: number + /** The query that was submitted to produce this result, snapshotted at run time. */ + sql?: string + source?: SqlSnippetSource } /** From 2681a21f5c6f06836ff10b91eadb9f13cae3cae7 Mon Sep 17 00:00:00 2001 From: Wen Bo Xie Date: Tue, 1 Sep 2026 20:30:56 +0800 Subject: [PATCH 03/16] docs: add Personal Access Tokens guide with generated permission tables (#49732) Add a guide that compares classic and scoped personal access tokens, explains how account roles constrain token permissions, and walks through creating and testing a project-scoped token. Include generated tables mapping permissions to Management API endpoints and MCP tools, and link the guide from docs navigation and Studio token sheets. Move the scoped-token permission catalog from Studio into shared-data. Studio and docs generation now share permission names, categories, descriptions, risk metadata, modes, scopes, and display order. Generate the tables from the shared catalog, OpenAPI x-fga-permissions, and the downloaded MCP permission map. Exclude Workers permissions until the feature is live. Run regeneration through the docs Makefile, verify checked-in output in CI, and refresh it in the weekly Management API workflow. Add Dashboard and Docs ownership plus contributor guidance so permission changes stay synchronized. --- .agents/skills/ask-the-docs/SKILL.md | 2 +- .../ask-the-docs/reference/ci-and-lint.md | 9 +- .../reference/management-api-reference.md | 9 + .claude/CLAUDE.md | 2 +- .github/CODEOWNERS | 1 + .github/workflows/docs-mgmt-api-update.yml | 33 +- .github/workflows/docs-tests.yml | 12 + .../NavigationMenu.constants.ts | 5 + .../access-control/scoped_pat_mcp_tools.mdx | 37 ++ .../access-control/scoped_pat_permissions.mdx | 236 +++++++ .../platform/personal-access-tokens.mdx | 61 ++ apps/docs/spec/Makefile | 21 +- apps/docs/spec/mcp_tools_permissions.json | 35 + .../generateAccessControlPartials.mts | 334 ++++++++++ apps/studio/CLAUDE.md | 1 + .../AccessTokens/AccessToken.constants.ts | 48 +- .../AccessTokens/AccessToken.permissions.ts | 583 +--------------- .../Scoped/Form/PermissionsAccordion.tsx | 8 +- .../Scoped/NewScopedTokenSheet.tsx | 4 +- .../AccessTokens/Scoped/TokenDocsButtons.tsx | 20 + .../AccessTokens/Scoped/ViewTokenSheet.tsx | 16 +- packages/shared-data/package.json | 1 + .../scoped-access-token-permissions.ts | 625 ++++++++++++++++++ pnpm-lock.yaml | 3 + 24 files changed, 1473 insertions(+), 633 deletions(-) create mode 100644 apps/docs/content/_partials/access-control/scoped_pat_mcp_tools.mdx create mode 100644 apps/docs/content/_partials/access-control/scoped_pat_permissions.mdx create mode 100644 apps/docs/content/guides/platform/personal-access-tokens.mdx create mode 100644 apps/docs/spec/mcp_tools_permissions.json create mode 100644 apps/docs/spec/sections/generateAccessControlPartials.mts create mode 100644 apps/studio/components/interfaces/Account/AccessTokens/Scoped/TokenDocsButtons.tsx create mode 100644 packages/shared-data/scoped-access-token-permissions.ts diff --git a/.agents/skills/ask-the-docs/SKILL.md b/.agents/skills/ask-the-docs/SKILL.md index d214049957a54..0de0ac2ec2a70 100644 --- a/.agents/skills/ask-the-docs/SKILL.md +++ b/.agents/skills/ask-the-docs/SKILL.md @@ -87,7 +87,7 @@ at hand — they cite each other where context matters. | [`reference/llm-agent-parity.md`](./reference/llm-agent-parity.md) | HTML↔markdown fidelity (e.g. AI prompts), search caveat, agent onboarding guides, in-flux wiring. | | [`reference/federated-docs.md`](./reference/federated-docs.md) | How docs pulls markdown from external repos at build time. Routes, `pageMap`, remark/rehype plugins, link transforms, known failure modes. | | [`reference/ci-and-lint.md`](./reference/ci-and-lint.md) | GitHub Actions on every PR — `docs_lint`, `Docs Tests`, typecheck, prettier, Vercel preview gate. Where to add a check before creating a new one. | -| [`reference/management-api-reference.md`](./reference/management-api-reference.md) | Management API OpenAPI download → Redocly bundle → codegen → `ApiEndpointSection`; why not to swap in Scalar/Redoc. | +| [`reference/management-api-reference.md`](./reference/management-api-reference.md) | Management API OpenAPI → reference generation, including scoped PAT permission tables; why not to swap in Scalar/Redoc. | | [`reference/gotchas.md`](./reference/gotchas.md) | Specific traps to watch for. One-liner per item. | ## How to use during a chat diff --git a/.agents/skills/ask-the-docs/reference/ci-and-lint.md b/.agents/skills/ask-the-docs/reference/ci-and-lint.md index fc8c039f48ec0..19e3705ea56e5 100644 --- a/.agents/skills/ask-the-docs/reference/ci-and-lint.md +++ b/.agents/skills/ask-the-docs/reference/ci-and-lint.md @@ -14,7 +14,7 @@ PR opened / updated │ ├── docs_lint (MDX/content linting; required) ├── docs_lint_comment_external (posts results as PR comments for external PRs) - ├── Docs Tests (pnpm test:docs on .ts* file changes) + ├── Docs Tests (pnpm test:docs on relevant docs code/spec changes) ├── TypeScript & Lint (tsc + eslint) ├── Prettier (format check) ├── reviewdog (inline annotations) @@ -36,15 +36,16 @@ check before merging.** Backed by `supa-mdx-lint`. ### 2. Docs Tests (`docs-tests.yml`) -Triggered on PRs to master when files matching `apps/docs/**/*.ts*` or -`apps/docs/spec/**/*.json` change. Runs on a Blacksmith 4-vCPU Ubuntu runner -with concurrency controls to cancel stale builds. +Triggered on relevant docs code/spec changes, including the generated scoped +PAT partials and their shared permission catalog. Runs on a Blacksmith 4-vCPU +Ubuntu runner with concurrency controls to cancel stale builds. - Sparse checkout of `apps/docs`, `examples`, `packages`, `supabase`, and `patches`. - Install pnpm (pinned hash). - Set up Node.js from `.nvmrc`. - `pnpm install --frozen-lockfile`. +- Regenerate the scoped PAT partials and fail if the committed output drifts. - Run `pnpm run test:docs` (with dummy GitHub OAuth env vars to prevent local Supabase startup errors). diff --git a/.agents/skills/ask-the-docs/reference/management-api-reference.md b/.agents/skills/ask-the-docs/reference/management-api-reference.md index 98f0b1f009d72..f4707a8af61fd 100644 --- a/.agents/skills/ask-the-docs/reference/management-api-reference.md +++ b/.agents/skills/ask-the-docs/reference/management-api-reference.md @@ -92,6 +92,15 @@ the custom cycle-safe resolve remains required. | `apps/docs/features/docs/Reference.sections.tsx` | `ApiEndpointSection` UI | | `apps/docs/internals/generate-reference-markdown.ts` | agent markdown export | +## Scoped personal access token permission tables + +The "Personal Access Tokens" guide's permission and MCP tables are generated +from the Management API specs, MCP permission map, and the same shared catalog +Studio uses. Regenerate `content/_partials/access-control/scoped_pat_*.mdx` with +`make -C apps/docs/spec generate.partials.access-control`. Docs Tests runs this +on relevant pull requests and fails if the partials drift; the weekly Management +API update also runs it after refreshing the live inputs. + ## Related - [`build-pipeline.md`](./build-pipeline.md) — where `codegen:references` diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 5ad8d8d0bf72b..144e60df965cb 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -41,7 +41,7 @@ pnpm api:codegen # platform Management API types → packages/api-ty Every PR must pass typecheck + lint (one workflow), Prettier, and a typos check. Other checks are path-filtered: Studio unit tests/build and the lint ratchet (ESLint warning count must not increase) run on `apps/studio/**` changes; app-specific test suites run on their own paths. -Never hand-edit generated files: `packages/api-types/types/**`, `**/routeTree.gen.ts`, `**/__generated__/**`, `apps/docs/features/docs/generated/**`, `apps/www/.generated/**`, `supabase/functions/common/database-types.ts`. +Never hand-edit generated files: `packages/api-types/types/**`, `**/routeTree.gen.ts`, `**/__generated__/**`, `apps/docs/features/docs/generated/**`, `apps/www/.generated/**`, `supabase/functions/common/database-types.ts`, `apps/docs/content/_partials/access-control/scoped_pat_*.mdx` (run `make -C apps/docs/spec generate.partials.access-control`). ## Conventions diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 79e3919a77122..a72095d25c908 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,6 +1,7 @@ /packages/ui/ @supabase/design /packages/shared-data/pricing.ts @supabase/billing /packages/shared-data/plans.ts @supabase/billing +/packages/shared-data/scoped-access-token-permissions.ts @supabase/Dashboard @supabase/docs /packages/common/telemetry-constants.ts @supabase/growth-eng /packages/dev-tools/ @supabase/growth-eng # /packages/pg-meta @supabase/postgres @avallete diff --git a/.github/workflows/docs-mgmt-api-update.yml b/.github/workflows/docs-mgmt-api-update.yml index 6b868b1b37b29..c0f3add857f7e 100644 --- a/.github/workflows/docs-mgmt-api-update.yml +++ b/.github/workflows/docs-mgmt-api-update.yml @@ -3,7 +3,7 @@ name: Update Mgmt Api Docs on: schedule: # Run at 00:00 UTC every Monday - - cron: "0 0 * * 1" + - cron: '0 0 * * 1' workflow_dispatch: permissions: @@ -19,10 +19,13 @@ jobs: with: persist-credentials: false ref: ${{ github.ref }} + # The PAT tables generator imports the shared catalog and its base tsconfig. sparse-checkout: | apps/docs patches packages/generator + packages/shared-data + packages/tsconfig - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 name: Install pnpm @@ -32,15 +35,15 @@ jobs: - name: Use Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: - node-version-file: ".nvmrc" - cache: "pnpm" + node-version-file: '.nvmrc' + cache: 'pnpm' - name: Install deps run: pnpm install --frozen-lockfile - - name: Change to apps/docs/spec directory and run make command + - name: Refresh Management API docs working-directory: apps/docs/spec - run: make download.api.v1 dereference.api.v1 generate.sections.api.v1 format + run: make download.api.v1 download.mcp-tools-permissions dereference.api.v1 generate.sections.api.v1 generate.partials.access-control format - name: Generate token id: app-token @@ -55,8 +58,18 @@ jobs: uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0 with: token: ${{ steps.app-token.outputs.token }} - commit-message: "feat: update mgmt api docs" - title: "feat: update mgmt api docs" - body: "This PR updates mgmt api docs automatically." - branch: "gha/auto-update-mgmt-api-docs" - base: "master" + commit-message: 'feat: update mgmt api docs' + title: 'feat: update mgmt api docs' + body: | + This PR updates Management API docs automatically. + + This regenerates: + + - Management API specs and sections + - Personal Access Tokens permission-to-endpoint table + - Personal Access Tokens MCP tool permissions table + + Sources include the live Management API specs, MCP permission map, + and Studio's shared permission catalog. + branch: 'gha/auto-update-mgmt-api-docs' + base: 'master' diff --git a/.github/workflows/docs-tests.yml b/.github/workflows/docs-tests.yml index 51c6b3a277ad6..967b2875f0181 100644 --- a/.github/workflows/docs-tests.yml +++ b/.github/workflows/docs-tests.yml @@ -6,8 +6,13 @@ on: paths: - 'apps/docs/**/*.ts*' - 'apps/docs/spec/**/*.json' + - 'apps/docs/spec/Makefile' + - 'apps/docs/spec/sections/generateAccessControlPartials.mts' + - 'apps/docs/content/_partials/access-control/**' - 'apps/docs/.env.development' - 'apps/docs/package.json' + - 'packages/shared-data/package.json' + - 'packages/shared-data/scoped-access-token-permissions.ts' - 'e2e/docs/local-smoke/**' - 'e2e/docs/playwright.local-smoke.config.ts' - 'e2e/docs/package.json' @@ -52,6 +57,13 @@ jobs: - name: Install deps run: pnpm install --frozen-lockfile + - name: Check access-control partials + run: | + make -C apps/docs/spec generate.partials.access-control + git diff --exit-code -- \ + apps/docs/content/_partials/access-control/scoped_pat_permissions.mdx \ + apps/docs/content/_partials/access-control/scoped_pat_mcp_tools.mdx + - name: Download JS reference TypeDoc dumps # The source dumps under apps/docs/spec/reference///*.json are # gitignored — `make download.tsdoc.v2` re-fetches them from diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index ecfa09baf287d..28501353fc0f4 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -2728,6 +2728,11 @@ export const platform: NavMenuConstant = { name: 'Access Control', url: '/guides/platform/access-control' as `/${string}`, }, + { + name: 'Personal Access Tokens', + url: '/guides/platform/personal-access-tokens' as `/${string}`, + enabled: fullPlatformEnabled, + }, { name: 'Multi-factor Authentication', url: '/guides/platform/multi-factor-authentication', diff --git a/apps/docs/content/_partials/access-control/scoped_pat_mcp_tools.mdx b/apps/docs/content/_partials/access-control/scoped_pat_mcp_tools.mdx new file mode 100644 index 0000000000000..acc7a1134867f --- /dev/null +++ b/apps/docs/content/_partials/access-control/scoped_pat_mcp_tools.mdx @@ -0,0 +1,37 @@ +{/* Generated by `make -C apps/docs/spec generate.partials.access-control`. Do not hand-edit; see supabase/platform#37175 and apps/docs/spec/Makefile. */} + +| MCP tool | Required permission | +| --------------------------- | ----------------------------------------------------------------------------- | +| `apply_migration` | **Migrations** (Read-write) | +| `confirm_cost` | None (always available) | +| `create_branch` | **Development Branches** (Read-write) or **Production Branches** (Read-write) | +| `create_project` | **Organization Projects** (Read-write) | +| `delete_branch` | **Production Branches** (Read-write) or **Development Branches** (Read-write) | +| `deploy_edge_function` | **Edge Functions** (Read-write) | +| `execute_sql` | **Database** (Read) | +| `generate_typescript_types` | **Database** (Read) | +| `get_advisors` | **Advisors** (Read) | +| `get_cost` | **Organization Settings** (Read) and **Projects (account-wide)** (Read) | +| `get_edge_function` | **Edge Functions** (Read) | +| `get_logs` | **Logs** (Read) | +| `get_organization` | **Organization Settings** (Read) | +| `get_project` | **Project Settings** (Read) | +| `get_project_url` | **Project Settings** (Read) | +| `get_publishable_keys` | **API Keys** (Read) | +| `get_storage_config` | **Storage Config** (Read) | +| `list_branches` | **Development Branches** (Read) or **Production Branches** (Read) | +| `list_edge_functions` | **Edge Functions** (Read) | +| `list_extensions` | **Database** (Read) | +| `list_migrations` | **Migrations** (Read) | +| `list_organizations` | **Organizations** (Read) | +| `list_projects` | **Projects (account-wide)** (Read) | +| `list_storage_buckets` | **Storage** (Read) | +| `list_tables` | **Database** (Read) | +| `merge_branch` | **Production Branches** (Read-write) or **Development Branches** (Read-write) | +| `pause_project` | **Project Settings** (Read-write) | +| `query_logs` | **Logs** (Read) | +| `rebase_branch` | **Production Branches** (Read-write) or **Development Branches** (Read-write) | +| `reset_branch` | **Production Branches** (Read-write) or **Development Branches** (Read-write) | +| `restore_project` | **Project Settings** (Read-write) | +| `search_docs` | None (always available) | +| `update_storage_config` | **Storage Config** (Read-write) | diff --git a/apps/docs/content/_partials/access-control/scoped_pat_permissions.mdx b/apps/docs/content/_partials/access-control/scoped_pat_permissions.mdx new file mode 100644 index 0000000000000..e674bde3f4e27 --- /dev/null +++ b/apps/docs/content/_partials/access-control/scoped_pat_permissions.mdx @@ -0,0 +1,236 @@ +{/* Generated by `make -C apps/docs/spec generate.partials.access-control`. Do not hand-edit; see supabase/platform#37175 and apps/docs/spec/Makefile. */} + +| Permission | Access required | Management API endpoint | +| -------------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------- | +| **Project** | | | +| Project Settings | Read | [Get JIT access config](/docs/reference/api/v1-get-jit-access-config) | +| | | [Get postgres upgrade eligibility](/docs/reference/api/v1-get-postgres-upgrade-eligibility)[^1] | +| | | [Get postgres upgrade status](/docs/reference/api/v1-get-postgres-upgrade-status)[^1] | +| | | [Get project](/docs/reference/api/v1-get-project) | +| | | [Get services health](/docs/reference/api/v1-get-services-health) | +| | | [List available restore versions](/docs/reference/api/v1-list-available-restore-versions) | +| | | [List private link associations](/docs/reference/api/v2-list-private-link-associations) | +| | | [Preview a project transfer](/docs/reference/api/v2-preview-a-project-transfer) | +| | Read-write | [Cancel a project restoration](/docs/reference/api/v1-cancel-a-project-restoration) | +| | | [Create private link association](/docs/reference/api/v2-create-private-link-association) | +| | | [Delete a project](/docs/reference/api/v1-delete-a-project) | +| | | [Delete private link association](/docs/reference/api/v2-delete-private-link-association) | +| | | [Delete private link association for database](/docs/reference/api/v2-delete-private-link-association-for-database) | +| | | [Get pgsodium config](/docs/reference/api/v1-get-pgsodium-config) | +| | | [OAuth authorize project claim](/docs/reference/api/v1-oauth-authorize-project-claim)[^2] | +| | | [Pause a project](/docs/reference/api/v1-pause-a-project) | +| | | [Restart a project](/docs/reference/api/v1-restart-a-project) | +| | | [Restore a project](/docs/reference/api/v1-restore-a-project) | +| | | [Update a project](/docs/reference/api/v1-update-a-project) | +| | | [Update auth service config](/docs/reference/api/v1-update-auth-service-config)[^3] | +| | | [Update JIT access config](/docs/reference/api/v1-update-jit-access-config) | +| | | [Update pgsodium config](/docs/reference/api/v1-update-pgsodium-config) | +| | | [Upgrade postgres version](/docs/reference/api/v1-upgrade-postgres-version)[^4] | +| Action Runs | Read | [Count action runs](/docs/reference/api/v1-count-action-runs) | +| | | [Get action run](/docs/reference/api/v1-get-action-run) | +| | | [Get action run logs](/docs/reference/api/v1-get-action-run-logs) | +| | | [List action runs](/docs/reference/api/v1-list-action-runs) | +| | Read-write | [Update action run status](/docs/reference/api/v1-update-action-run-status) | +| Advisors | Read | [Get performance advisors](/docs/reference/api/v1-get-performance-advisors) | +| | | [Get security advisors](/docs/reference/api/v1-get-security-advisors) | +| Analytics Config | Read | [List log drains](/docs/reference/api/v2-list-log-drains) | +| | Read-write | [Create log drain](/docs/reference/api/v2-create-log-drain) | +| | | [Delete log drain](/docs/reference/api/v2-delete-log-drain) | +| | | [Update log drain](/docs/reference/api/v2-update-log-drain) | +| Logs | Read | [Get project logs](/docs/reference/api/v1-get-project-logs) | +| | | [Get project logs all](/docs/reference/api/v1-get-project-logs-all) | +| | | [Scrape project metrics](/docs/reference/api/v1-scrape-project-metrics) | +| Usage Analytics | Read | [Get project function combined stats](/docs/reference/api/v1-get-project-function-combined-stats) | +| | | [Get project usage API count](/docs/reference/api/v1-get-project-usage-api-count) | +| | | [Get project usage request count](/docs/reference/api/v1-get-project-usage-request-count) | +| Platform Webhooks | Read | [Get delivery](/docs/reference/api/v2-projects-ref-webhooks-deliveries-id-get) | +| | | [Get endpoint](/docs/reference/api/v2-projects-ref-webhooks-endpoints-id-get) | +| | | [List deliveries](/docs/reference/api/v2-projects-ref-webhooks-endpoints-id-deliveries-get) | +| | | [List endpoints](/docs/reference/api/v2-projects-ref-webhooks-endpoints-get) | +| | Read-write | [Create endpoint](/docs/reference/api/v2-projects-ref-webhooks-endpoints-post) | +| | | [Delete all endpoints](/docs/reference/api/v2-projects-ref-webhooks-endpoints-delete) | +| | | [Delete endpoint](/docs/reference/api/v2-projects-ref-webhooks-endpoints-id-delete) | +| | | [Retry delivery](/docs/reference/api/v2-projects-ref-webhooks-deliveries-id-retry-post) | +| | | [Send test event](/docs/reference/api/v2-projects-ref-webhooks-endpoints-id-test-post) | +| | | [Update endpoint](/docs/reference/api/v2-projects-ref-webhooks-endpoints-id-patch) | +| **Database** | | | +| Backups | Read | [Get backup schedule](/docs/reference/api/v1-get-backup-schedule) | +| | | [List all backups](/docs/reference/api/v1-list-all-backups) | +| | Read-write | [Restore PITR backup](/docs/reference/api/v1-restore-pitr-backup) | +| | | [Update backup schedule](/docs/reference/api/v1-update-backup-schedule) | +| Database | Read | [Generate typescript types](/docs/reference/api/v1-generate-typescript-types) | +| | | [Get database metadata](/docs/reference/api/v1-get-database-metadata) | +| | | [Get database openapi](/docs/reference/api/v1-get-database-openapi) | +| | | [Get postgres upgrade eligibility](/docs/reference/api/v1-get-postgres-upgrade-eligibility)[^1] | +| | | [Get postgres upgrade status](/docs/reference/api/v1-get-postgres-upgrade-status)[^1] | +| | | [Get project config](/docs/reference/api/v2-get-project-config)[^5] | +| | | [Get project PgBouncer config](/docs/reference/api/v1-get-project-pgbouncer-config) | +| | | [Read only query](/docs/reference/api/v1-read-only-query) | +| | | [Run a query](/docs/reference/api/v1-run-a-query) | +| | Read-write | [Create login role](/docs/reference/api/v1-create-login-role) | +| | | [Delete login roles](/docs/reference/api/v1-delete-login-roles) | +| | | [Run a query](/docs/reference/api/v1-run-a-query) | +| | | [Upgrade postgres version](/docs/reference/api/v1-upgrade-postgres-version)[^4] | +| Database Config | Read | [Get postgres config](/docs/reference/api/v1-get-postgres-config) | +| | | [Get project config](/docs/reference/api/v2-get-project-config)[^5] | +| | Read-write | [Update database password](/docs/reference/api/v1-update-database-password) | +| | | [Update postgres config](/docs/reference/api/v1-update-postgres-config) | +| Database JIT | Read | [Authorize JIT access](/docs/reference/api/v1-authorize-jit-access) | +| | | [Get JIT access](/docs/reference/api/v1-get-jit-access) | +| | Read-write | [Delete invite external JIT access](/docs/reference/api/v1-delete-invite-external-jit-access) | +| | | [Delete JIT access](/docs/reference/api/v1-delete-jit-access) | +| | | [Invite external JIT access](/docs/reference/api/v1-invite-external-jit-access) | +| | | [List JIT access](/docs/reference/api/v1-list-jit-access) | +| | | [Update JIT access](/docs/reference/api/v1-update-jit-access) | +| Network Bans | Read | [List all network bans](/docs/reference/api/v1-list-all-network-bans) | +| | | [List all network bans enriched](/docs/reference/api/v1-list-all-network-bans-enriched) | +| | Read-write | [Delete network bans](/docs/reference/api/v1-delete-network-bans) | +| Network Restrictions | Read | [Get network restrictions](/docs/reference/api/v1-get-network-restrictions) | +| | | [Get project config](/docs/reference/api/v2-get-project-config)[^5] | +| | Read-write | [Patch network restrictions](/docs/reference/api/v1-patch-network-restrictions) | +| | | [Update network restrictions](/docs/reference/api/v1-update-network-restrictions) | +| Migrations | Read | [Get a migration](/docs/reference/api/v1-get-a-migration) | +| | | [List migration history](/docs/reference/api/v1-list-migration-history) | +| | Read-write | [Apply a migration](/docs/reference/api/v1-apply-a-migration) | +| | | [Patch a migration](/docs/reference/api/v1-patch-a-migration) | +| | | [Rollback migrations](/docs/reference/api/v1-rollback-migrations) | +| | | [Upsert a migration](/docs/reference/api/v1-upsert-a-migration) | +| Connection Pooling | Read | [Get pooler config](/docs/reference/api/v1-get-pooler-config) | +| | Read-write | [Update pooler config](/docs/reference/api/v1-update-pooler-config) | +| Read-only Mode | Read | [Get read-only mode status](/docs/reference/api/v1-get-readonly-mode-status) | +| | Read-write | [Disable read-only mode temporarily](/docs/reference/api/v1-disable-readonly-mode-temporarily) | +| SSL Enforcement | Read | [Get project config](/docs/reference/api/v2-get-project-config)[^5] | +| | | [Get SSL enforcement config](/docs/reference/api/v1-get-ssl-enforcement-config) | +| | Read-write | [Update SSL enforcement config](/docs/reference/api/v1-update-ssl-enforcement-config) | +| Database Webhooks | Read-write | [Enable database webhook](/docs/reference/api/v1-enable-database-webhook) | +| **Application services** | | | +| API Keys | Read | [Get project API key](/docs/reference/api/v1-get-project-api-key) | +| | | [Get project API keys](/docs/reference/api/v1-get-project-api-keys) | +| | | [Get project legacy API keys](/docs/reference/api/v1-get-project-legacy-api-keys) | +| | Read-write | [Create project API key](/docs/reference/api/v1-create-project-api-key) | +| | | [Delete project API key](/docs/reference/api/v1-delete-project-api-key) | +| | | [Update project API key](/docs/reference/api/v1-update-project-api-key) | +| | | [Update project legacy API keys](/docs/reference/api/v1-update-project-legacy-api-keys) | +| Auth Config | Read | [Get a SSO provider](/docs/reference/api/v1-get-a-sso-provider) | +| | | [Get auth service config](/docs/reference/api/v1-get-auth-service-config) | +| | | [Get project config](/docs/reference/api/v2-get-project-config)[^5] | +| | | [Get project TPA integration](/docs/reference/api/v1-get-project-tpa-integration) | +| | | [List all SSO provider](/docs/reference/api/v1-list-all-sso-provider) | +| | | [List project TPA integrations](/docs/reference/api/v1-list-project-tpa-integrations) | +| | Read-write | [Create a SSO provider](/docs/reference/api/v1-create-a-sso-provider) | +| | | [Create project TPA integration](/docs/reference/api/v1-create-project-tpa-integration) | +| | | [Delete a SSO provider](/docs/reference/api/v1-delete-a-sso-provider) | +| | | [Delete project TPA integration](/docs/reference/api/v1-delete-project-tpa-integration) | +| | | [Update a SSO provider](/docs/reference/api/v1-update-a-sso-provider) | +| | | [Update auth service config](/docs/reference/api/v1-update-auth-service-config)[^3] | +| Auth Signing Keys | Read | [Get legacy signing key](/docs/reference/api/v1-get-legacy-signing-key) | +| | | [Get project signing key](/docs/reference/api/v1-get-project-signing-key) | +| | | [Get project signing keys](/docs/reference/api/v1-get-project-signing-keys) | +| | Read-write | [Create legacy signing key](/docs/reference/api/v1-create-legacy-signing-key) | +| | | [Create project signing key](/docs/reference/api/v1-create-project-signing-key) | +| | | [Remove project signing key](/docs/reference/api/v1-remove-project-signing-key) | +| | | [Update project signing key](/docs/reference/api/v1-update-project-signing-key) | +| Data API Config | Read | [Get PostgREST service config](/docs/reference/api/v1-get-postgrest-service-config) | +| | | [Get project config](/docs/reference/api/v2-get-project-config)[^5] | +| | Read-write | [Update PostgREST service config](/docs/reference/api/v1-update-postgrest-service-config) | +| Edge Functions | Read | [Get a function](/docs/reference/api/v1-get-a-function) | +| | | [Get a function body](/docs/reference/api/v1-get-a-function-body) | +| | | [List all functions](/docs/reference/api/v1-list-all-functions) | +| | Read-write | [Bulk update functions](/docs/reference/api/v1-bulk-update-functions) | +| | | [Create a function](/docs/reference/api/v1-create-a-function) | +| | | [Delete a function](/docs/reference/api/v1-delete-a-function) | +| | | [Deploy a function](/docs/reference/api/v1-deploy-a-function) | +| | | [Update a function](/docs/reference/api/v1-update-a-function) | +| Edge Function Secrets | Read | [List all secrets](/docs/reference/api/v1-list-all-secrets) | +| | Read-write | [Bulk create secrets](/docs/reference/api/v1-bulk-create-secrets) | +| | | [Bulk delete secrets](/docs/reference/api/v1-bulk-delete-secrets) | +| Realtime Config | Read | [Get project config](/docs/reference/api/v2-get-project-config)[^5] | +| | | [Get realtime config](/docs/reference/api/v1-get-realtime-config) | +| | Read-write | [Shutdown realtime](/docs/reference/api/v1-shutdown-realtime) | +| | | [Update realtime config](/docs/reference/api/v1-update-realtime-config) | +| Storage | Read | [List all buckets](/docs/reference/api/v1-list-all-buckets) | +| Storage Config | Read | [Get project config](/docs/reference/api/v2-get-project-config)[^5] | +| | | [Get storage config](/docs/reference/api/v1-get-storage-config) | +| | Read-write | [Update storage config](/docs/reference/api/v1-update-storage-config) | +| **Infrastructure and delivery** | | | +| Development Branches | Read | [Get a branch](/docs/reference/api/v1-get-a-branch) | +| | | [Get a branch config](/docs/reference/api/v1-get-a-branch-config) | +| | | [List all branches](/docs/reference/api/v1-list-all-branches) | +| | Read-write | [Create a branch](/docs/reference/api/v1-create-a-branch) | +| | | [Delete a branch](/docs/reference/api/v1-delete-a-branch) | +| | | [Diff a branch](/docs/reference/api/v1-diff-a-branch) | +| | | [Merge a branch](/docs/reference/api/v1-merge-a-branch) | +| | | [Push a branch](/docs/reference/api/v1-push-a-branch) | +| | | [Reset a branch](/docs/reference/api/v1-reset-a-branch) | +| | | [Restore a branch](/docs/reference/api/v1-restore-a-branch) | +| | | [Update a branch config](/docs/reference/api/v1-update-a-branch-config) | +| Production Branches | Read | [Get a branch](/docs/reference/api/v1-get-a-branch) | +| | | [Get a branch config](/docs/reference/api/v1-get-a-branch-config) | +| | | [List all branches](/docs/reference/api/v1-list-all-branches) | +| | Read-write | [Create a branch](/docs/reference/api/v1-create-a-branch) | +| | | [Delete a branch](/docs/reference/api/v1-delete-a-branch) | +| | | [Diff a branch](/docs/reference/api/v1-diff-a-branch) | +| | | [Disable preview branching](/docs/reference/api/v1-disable-preview-branching) | +| | | [Merge a branch](/docs/reference/api/v1-merge-a-branch) | +| | | [Push a branch](/docs/reference/api/v1-push-a-branch) | +| | | [Reset a branch](/docs/reference/api/v1-reset-a-branch) | +| | | [Restore a branch](/docs/reference/api/v1-restore-a-branch) | +| | | [Update a branch config](/docs/reference/api/v1-update-a-branch-config) | +| Custom Domains | Read | [Get hostname config](/docs/reference/api/v1-get-hostname-config) | +| | Read-write | [Activate custom hostname](/docs/reference/api/v1-activate-custom-hostname) | +| | | Delete hostname config | +| | | [Update hostname config](/docs/reference/api/v1-update-hostname-config) | +| | | [Verify DNS config](/docs/reference/api/v1-verify-dns-config) | +| Add-ons | Read | [List project add-ons](/docs/reference/api/v1-list-project-addons) | +| | Read-write | [Apply project add-on](/docs/reference/api/v1-apply-project-addon) | +| | | [Remove project add-on](/docs/reference/api/v1-remove-project-addon) | +| Disk Config | Read | [Get database disk](/docs/reference/api/v1-get-database-disk) | +| | | [Get disk utilization](/docs/reference/api/v1-get-disk-utilization) | +| | | [Get project disk auto-scaling config](/docs/reference/api/v1-get-project-disk-autoscale-config) | +| | Read-write | [Modify database disk](/docs/reference/api/v1-modify-database-disk) | +| Read Replicas | Read-write | [Remove a read replica](/docs/reference/api/v1-remove-a-read-replica) | +| | | [Setup a read replica](/docs/reference/api/v1-setup-a-read-replica) | +| Vanity Subdomain | Read | [Get vanity subdomain config](/docs/reference/api/v1-get-vanity-subdomain-config) | +| | Read-write | [Activate vanity subdomain config](/docs/reference/api/v1-activate-vanity-subdomain-config) | +| | | [Check vanity subdomain availability](/docs/reference/api/v1-check-vanity-subdomain-availability) | +| | | [Deactivate vanity subdomain config](/docs/reference/api/v1-deactivate-vanity-subdomain-config) | +| **Account and organization** | | | +| Organizations | Read | [List all organizations](/docs/reference/api/v1-list-all-organizations) | +| | Read-write | [Create an organization](/docs/reference/api/v1-create-an-organization) | +| Projects (account-wide) | Read | [List all projects](/docs/reference/api/v1-list-all-projects) | +| SQL Snippets (account-wide) | Read | [Get a snippet](/docs/reference/api/v1-get-a-snippet) | +| | | [List all snippets](/docs/reference/api/v1-list-all-snippets) | +| Organization Settings | Read | [Get an organization](/docs/reference/api/v1-get-an-organization) | +| | | [Get organization entitlements](/docs/reference/api/v1-get-organization-entitlements) | +| | Read-write | [Assign organization member role](/docs/reference/api/v2-assign-organization-member-role) | +| | | [OAuth authorize project claim](/docs/reference/api/v1-oauth-authorize-project-claim)[^2] | +| | | [Transfer a project](/docs/reference/api/v2-transfer-a-project) | +| Organization Members | Read | [List organization members](/docs/reference/api/v1-list-organization-members) | +| | | [List organization members](/docs/reference/api/v2-list-organization-members) | +| | | [List organization roles](/docs/reference/api/v2-list-organization-roles) | +| | Read-write | [Create organization invitations](/docs/reference/api/v2-create-organization-invitations) | +| | | [Delete organization invitations](/docs/reference/api/v2-delete-organization-invitations) | +| Organization Projects | Read | [Get all projects for organization](/docs/reference/api/v1-get-all-projects-for-organization) | +| | | [List organization GitHub connections](/docs/reference/api/v2-list-organization-github-connections) | +| | | [List organization projects](/docs/reference/api/v2-list-organization-projects) | +| | Read-write | [Create a project](/docs/reference/api/v1-create-a-project) | +| Platform Webhooks (organization) | Read | [Get delivery](/docs/reference/api/v2-organizations-slug-webhooks-deliveries-id-get) | +| | | [Get endpoint](/docs/reference/api/v2-organizations-slug-webhooks-endpoints-id-get) | +| | | [List deliveries](/docs/reference/api/v2-organizations-slug-webhooks-endpoints-id-deliveries-get) | +| | | [List endpoints](/docs/reference/api/v2-organizations-slug-webhooks-endpoints-get) | +| | Read-write | [Create endpoint](/docs/reference/api/v2-organizations-slug-webhooks-endpoints-post) | +| | | [Delete all endpoints](/docs/reference/api/v2-organizations-slug-webhooks-endpoints-delete) | +| | | [Delete endpoint](/docs/reference/api/v2-organizations-slug-webhooks-endpoints-id-delete) | +| | | [Retry delivery](/docs/reference/api/v2-organizations-slug-webhooks-deliveries-id-retry-post) | +| | | [Send test event](/docs/reference/api/v2-organizations-slug-webhooks-endpoints-id-test-post) | +| | | [Update endpoint](/docs/reference/api/v2-organizations-slug-webhooks-endpoints-id-patch) | + +[^1]: Requires **Project Settings** (Read) and **Database** (Read). + +[^2]: Requires **Organization Settings** (Read-write) and **Project Settings** (Read-write). + +[^3]: Requires **Auth Config** (Read-write) and **Project Settings** (Read-write). + +[^4]: Requires **Project Settings** (Read-write) and **Database** (Read-write). + +[^5]: Requires **Database Config** (Read), **Database** (Read), **SSL Enforcement** (Read), **Network Restrictions** (Read), **Auth Config** (Read), **Data API Config** (Read), **Realtime Config** (Read), and **Storage Config** (Read). diff --git a/apps/docs/content/guides/platform/personal-access-tokens.mdx b/apps/docs/content/guides/platform/personal-access-tokens.mdx new file mode 100644 index 0000000000000..9710b5d77f405 --- /dev/null +++ b/apps/docs/content/guides/platform/personal-access-tokens.mdx @@ -0,0 +1,61 @@ +--- +title: 'Personal Access Tokens' +description: 'Scope personal access tokens to specific organizations, projects, and permissions' +--- + + + +Scoped personal access tokens are in **public alpha** and rolling out gradually. If you don't see the option to choose permissions when creating a token, your account doesn't have access yet. File a [support ticket](https://supabase.help) to get early access. + + + +Personal access tokens (PATs) authenticate you to the [Management API](/docs/reference/api/introduction) and the tools built on it, like the Supabase CLI and the [MCP server](/docs/guides/ai-tools/mcp). They come in two flavors: + +- **Classic tokens** carry your account's full access. That means every permission, on every organization and every project you belong to today, and on every one you create or join in the future. A classic token created a year ago can touch a project you created today. +- **Scoped tokens** carry only the organizations, projects, and permissions you choose. For example: read one project's database and view its logs, with no access to billing or organization settings. + + + +We recommend scoped tokens for everything, especially AI agents, automation scripts, and CI environments. If a token leaks, the blast radius stays small. + + + +To use scoped personal access tokens you need a Supabase account with a role on the organization or project you want the token to reach. A scoped personal access token's permissions only ever narrow what your account can already do. They never grant more. If your role doesn't include a permission (see [Access Control](/docs/guides/platform/access-control)), granting that permission to a token has no effect: the token still can't do it. + +You create scoped tokens the same way as classic tokens, from your [access tokens](/dashboard/account/tokens) settings. Choose which permissions to grant during creation instead of leaving the token with full access. + +## Create and use a scoped personal access token + +This example creates a token that can only read one project's settings, then calls an endpoint outside that scope. Both calls are reads available to every organization role, including Read-Only, so anyone can run it. + +1. Go to your [access tokens](/dashboard/account/tokens) settings and generate a new token. +2. While creating it, scope the token to one project and grant only the **Project Settings** permission with **Read** access. +3. Copy the token (scoped personal access tokens start with `sbp_fc`) and use it against the Management API, replacing `your-project-ref` with the project's ref: + +```bash +export SUPABASE_ACCESS_TOKEN="sbp_fc..." + +# Allowed: Project Settings Read unlocks this endpoint +curl "https://api.supabase.com/v1/projects/your-project-ref" \ + -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" +# Returns the project's details + +# Denied: this endpoint needs the Database permission, which the token lacks +curl -i "https://api.supabase.com/v1/projects/your-project-ref/types/typescript" \ + -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" +# Returns HTTP 403 +``` + +The tables below list which permission unlocks which endpoints, and which permission each [MCP tool](#mcp-tools) requires, so you can grant exactly what a workflow needs. + +## Permission scopes + +Each permission controls read or read-write access to one resource, for example **Database**, **Edge Functions**, or **Production Branches**. The permissions and names below match those shown when creating a scoped personal access token. Permissions that currently unlock neither a public Management API endpoint nor an MCP tool are omitted. A few endpoints need more than one permission. The footnotes call these out. + +<$Partial path="access-control/scoped_pat_permissions.mdx" /> + +## MCP tools + +A scoped personal access token used to authenticate the [MCP server](/docs/guides/ai-tools/mcp) can only call the tools its granted permissions unlock. See [Available tools](/docs/guides/ai-tools/mcp#available-tools) for what each tool does. + +<$Partial path="access-control/scoped_pat_mcp_tools.mdx" /> diff --git a/apps/docs/spec/Makefile b/apps/docs/spec/Makefile index 7551510304585..2110c1d0b4b0e 100644 --- a/apps/docs/spec/Makefile +++ b/apps/docs/spec/Makefile @@ -1,7 +1,7 @@ REPO_DIR=$(shell pwd) GENERATOR_DIR=../../../packages/generator -.PHONY: run download download.api.v1 download.storage.v1 download.tsdoc.v2 download.server.v1 transform dereference.api.v1 dereference.auth.v1 dereference.storage.v0 generate generate.sections.api.v1 format +.PHONY: run download download.api.v1 download.mcp-tools-permissions download.storage.v1 download.tsdoc.v2 download.server.v1 transform dereference.api.v1 dereference.auth.v1 dereference.storage.v0 generate generate.sections.api.v1 generate.partials.access-control format run: download transform generate format @@ -11,12 +11,17 @@ run: download transform generate format ############################################################################### # comment out download.auth.v1 temporarily, we're manually creating the file # download: download.api.v1 download.auth.v1 download.storage.v1 download.tsdoc.v2 -download: download.api.v1 download.storage.v1 download.tsdoc.v2 download.server.v1 +download: download.api.v1 download.mcp-tools-permissions download.storage.v1 download.tsdoc.v2 download.server.v1 download.api.v1: curl -sS https://api.supabase.com/api/v1-json > $(REPO_DIR)/api_v1_openapi.json curl -sS https://api.supabase.com/api/v2-json > $(REPO_DIR)/api_v2_openapi.json +# Download tool permissions from the public Management API projection of its enforcement rules. +download.mcp-tools-permissions: + curl -sSf https://api.supabase.com/platform/mcp-tools-permissions > $(REPO_DIR)/mcp_tools_permissions.json + npx prettier --write $(REPO_DIR)/mcp_tools_permissions.json + # This flow needs to be updated, so we'l comment out for the moment # Manual flow for now: # — get swagger.json (https://supabase.github.io/gotrue/swagger.json) @@ -81,7 +86,7 @@ dereference.analytics.v0: ############################################################################### # Generate sections from OpenAPI 3.0 ############################################################################### -generate: generate.sections.api.v1 +generate: generate.sections.api.v1 generate.partials.access-control generate.sections.api.v1: npx tsx $(REPO_DIR)/sections/generateMgmtApiSections.cts \ @@ -89,6 +94,16 @@ generate.sections.api.v1: $(REPO_DIR)/transforms/api_v2_openapi_deparsed.json \ $(REPO_DIR)/common-api-sections.json +# Generate PAT guide tables from Management API inputs and Studio's shared permission catalog. +generate.partials.access-control: + npx tsx $(REPO_DIR)/sections/generateAccessControlPartials.mts \ + $(REPO_DIR)/transforms/api_v1_openapi_deparsed.json \ + $(REPO_DIR)/transforms/api_v2_openapi_deparsed.json \ + $(REPO_DIR)/mcp_tools_permissions.json \ + $(REPO_DIR)/../content/_partials/access-control/scoped_pat_permissions.mdx \ + $(REPO_DIR)/../content/_partials/access-control/scoped_pat_mcp_tools.mdx + npx prettier --write $(REPO_DIR)/../content/_partials/access-control/*.mdx + ############################################################################### # Validate OpenAPI 3.0 ############################################################################### diff --git a/apps/docs/spec/mcp_tools_permissions.json b/apps/docs/spec/mcp_tools_permissions.json new file mode 100644 index 0000000000000..82b8c4dfef954 --- /dev/null +++ b/apps/docs/spec/mcp_tools_permissions.json @@ -0,0 +1,35 @@ +{ + "list_organizations": [["organizations_read"]], + "get_organization": [["organization_admin_read"]], + "list_projects": [["projects_read"]], + "get_project": [["project_admin_read"]], + "create_project": [["organization_projects_create"]], + "pause_project": [["project_admin_write"]], + "restore_project": [["project_admin_write"]], + "list_branches": [["branching_development_read"], ["branching_production_read"]], + "create_branch": [["branching_development_create"], ["branching_production_create"]], + "delete_branch": [["branching_production_delete"], ["branching_development_delete"]], + "merge_branch": [["branching_production_write"], ["branching_development_write"]], + "reset_branch": [["branching_production_write"], ["branching_development_write"]], + "rebase_branch": [["branching_production_write"], ["branching_development_write"]], + "execute_sql": [["database_read"]], + "list_tables": [["database_read"]], + "list_extensions": [["database_read"]], + "list_migrations": [["database_migrations_read"]], + "apply_migration": [["database_migrations_write"]], + "get_logs": [["analytics_logs_read"]], + "query_logs": [["analytics_logs_read"]], + "get_advisors": [["advisors_read"]], + "generate_typescript_types": [["database_read"]], + "get_project_url": [["project_admin_read"]], + "get_publishable_keys": [["api_gateway_keys_read"]], + "list_edge_functions": [["edge_functions_read"]], + "get_edge_function": [["edge_functions_read"]], + "deploy_edge_function": [["edge_functions_write"]], + "get_storage_config": [["storage_config_read"]], + "update_storage_config": [["storage_config_write"]], + "list_storage_buckets": [["storage_read"]], + "get_cost": [["organization_admin_read", "projects_read"]], + "confirm_cost": [[]], + "search_docs": [[]] +} diff --git a/apps/docs/spec/sections/generateAccessControlPartials.mts b/apps/docs/spec/sections/generateAccessControlPartials.mts new file mode 100644 index 0000000000000..9c706b7a94545 --- /dev/null +++ b/apps/docs/spec/sections/generateAccessControlPartials.mts @@ -0,0 +1,334 @@ +import fs from 'node:fs' +import { createRequire } from 'node:module' +import path from 'node:path' + +const require = createRequire(import.meta.url) +const { PERMISSION_CATALOG_BY_CATEGORY, PERMISSION_MODE_LABEL } = + require('shared-data/scoped-access-token-permissions') as typeof import('shared-data/scoped-access-token-permissions') + +type ScopeGroupAlternatives = string[][] +type McpMap = Record + +type Operation = { + operationId?: string + summary?: string + 'x-fga-permissions'?: ScopeGroupAlternatives + 'x-internal'?: boolean +} + +type Endpoint = { + operationId: string + label: string + groups: ScopeGroupAlternatives +} + +type PermissionRow = { + resource: string + access: string + category: string + scopes: string[] +} + +const GENERATED_NOTICE = + '{/* Generated by `make -C apps/docs/spec generate.partials.access-control`. Do not hand-edit; see supabase/platform#37175 and apps/docs/spec/Makefile. */}\n' + +const WORD_FIXES: Record = { + api: 'API', + sso: 'SSO', + tpa: 'TPA', + pitr: 'PITR', + dns: 'DNS', + jit: 'JIT', + ssl: 'SSL', + oauth: 'OAuth', + github: 'GitHub', + postgrest: 'PostgREST', + pgbouncer: 'PgBouncer', + readonly: 'read-only', + addon: 'add-on', + addons: 'add-ons', + autoscale: 'auto-scaling', +} + +const readJson = (filePath: string) => JSON.parse(fs.readFileSync(filePath, 'utf8')) + +function endpointLabel(operationId: string) { + const words = operationId + .replace(/^v\d+-?/, '') + .split('-') + .filter(Boolean) + .map((word) => WORD_FIXES[word] ?? word) + .join(' ') + return words.charAt(0).toUpperCase() + words.slice(1) +} + +const permissionRows: PermissionRow[] = PERMISSION_CATALOG_BY_CATEGORY.flatMap((category) => + category.entries.flatMap((entry) => [ + ...(entry.readScopes.length > 0 + ? [ + { + resource: entry.name, + access: PERMISSION_MODE_LABEL.read, + category: category.name, + scopes: entry.readScopes, + }, + ] + : []), + ...(entry.writeScopes.length > 0 + ? [ + { + resource: entry.name, + access: PERMISSION_MODE_LABEL.readwrite, + category: category.name, + scopes: entry.writeScopes, + }, + ] + : []), + ]) +) + +const rowByScope = new Map(permissionRows.flatMap((row) => row.scopes.map((scope) => [scope, row]))) + +// Workers permissions are present in the API spec but are not live for scoped PATs yet. +const EXCLUDED_SCOPES = new Set(['workers_read', 'workers_write']) + +// The public v2 webhook operations currently omit x-fga-permissions from the OpenAPI projection. +// Keep this fallback narrow so the generated table can still link those endpoints, and fail below +// if any other public operation has not been classified for the scoped-PAT table. +const WEBHOOK_PERMISSION_SCOPES = [ + { + routePrefix: '/v2/projects/{ref}/webhooks/', + read: 'platform_webhooks_projects_read', + write: 'platform_webhooks_projects_write', + }, + { + routePrefix: '/v2/organizations/{slug}/webhooks/', + read: 'platform_webhooks_organization_read', + write: 'platform_webhooks_organization_write', + }, +] + +// These public operations sit outside the scoped-PAT permission table. +const OPERATIONS_OUTSIDE_SCOPED_PAT_TABLE = new Set([ + 'v1-accept-invite-external-jit-access', + 'v1-authorize-user', + 'v1-exchange-oauth-token', + 'v1-get-available-regions', + 'v1-get-profile', + 'v1-revoke-token', +]) + +function webhookPermissionGroups( + route: string, + method: string +): ScopeGroupAlternatives | undefined { + const scopes = WEBHOOK_PERMISSION_SCOPES.find(({ routePrefix }) => route.startsWith(routePrefix)) + if (!scopes) return undefined + + const access = ['get', 'head'].includes(method) + ? 'read' + : ['post', 'put', 'patch', 'delete'].includes(method) + ? 'write' + : undefined + if (!access) return undefined + return [[scopes[access]]] +} + +function knownGroups(groups: ScopeGroupAlternatives, missing: Set) { + return groups.filter((group) => { + if (group.some((scope) => EXCLUDED_SCOPES.has(scope))) return false + const unknown = group.filter((scope) => !rowByScope.has(scope)) + unknown.forEach((scope) => missing.add(scope)) + return unknown.length === 0 + }) +} + +function joinList(items: string[]) { + if (items.length < 2) return items[0] ?? '' + if (items.length === 2) return `${items[0]} and ${items[1]}` + return `${items.slice(0, -1).join(', ')}, and ${items.at(-1)}` +} + +function formatRequirement(groups: ScopeGroupAlternatives) { + const alternatives = Array.from( + new Set( + groups.map((group) => + joinList( + Array.from( + new Set( + group.map((scope) => { + const row = rowByScope.get(scope)! + return `**${row.resource}** (${row.access})` + }) + ) + ) + ) + ) + ) + ) + return alternatives.join(alternatives.some((item) => item.includes(' and ')) ? ', or ' : ' or ') +} + +function missingScopesNotice(missing: Set) { + if (missing.size === 0) return [] + return [ + '', + `{/* Not documented, missing from the shared permission catalog ` + + `(packages/shared-data/scoped-access-token-permissions.ts): ${[...missing].sort().join(', ')} */}`, + ] +} + +function collectEndpoints(specPaths: string[]) { + const endpoints = new Map() + const unclassifiedOperations: string[] = [] + + for (const specPath of specPaths) { + const spec = readJson(specPath) + for (const [route, methods] of Object.entries>(spec.paths ?? {})) { + for (const [method, operation] of Object.entries(methods)) { + if (!operation?.operationId || operation['x-internal']) continue + + const key = `${method.toUpperCase()} ${route}` + const fallbackGroups = webhookPermissionGroups(route, method) + const groups = operation['x-fga-permissions'] ?? fallbackGroups ?? [] + if (groups.length === 0) { + if (!OPERATIONS_OUTSIDE_SCOPED_PAT_TABLE.has(operation.operationId)) { + unclassifiedOperations.push(`${key} (${operation.operationId})`) + } + continue + } + + endpoints.set(key, { + operationId: operation.operationId, + label: fallbackGroups + ? (operation.summary ?? endpointLabel(operation.operationId)) + : endpointLabel(operation.operationId), + groups, + }) + } + } + } + + if (unclassifiedOperations.length > 0) { + throw new Error( + `Public Management API operations are not classified for the scoped-PAT table:\n${unclassifiedOperations.join('\n')}` + ) + } + + return [...endpoints.values()] +} + +function generatePermissionsPartial(specPaths: string[], tools: McpMap, outputPath: string) { + const missing = new Set() + const endpoints = collectEndpoints(specPaths).map((endpoint) => ({ + ...endpoint, + groups: knownGroups(endpoint.groups, missing), + })) + const mcpToolScopes = new Set( + Object.values(tools).flatMap((groups) => knownGroups(groups, missing).flat()) + ) + const footnotes = new Map() + const lines = [ + GENERATED_NOTICE, + '| Permission | Access required | Management API endpoint |', + '| ---------- | --------------- | ----------------------- |', + ] + let previousCategory = '' + let previousResource = '' + + for (const row of permissionRows) { + const rowScopes = new Set(row.scopes) + const rowEndpoints = endpoints + .filter((endpoint) => + endpoint.groups.some((group) => group.some((scope) => rowScopes.has(scope))) + ) + .sort((a, b) => a.label.localeCompare(b.label) || a.operationId.localeCompare(b.operationId)) + + if (rowEndpoints.length === 0 && !row.scopes.some((scope) => mcpToolScopes.has(scope))) continue + + if (row.category !== previousCategory) { + lines.push(`| **${row.category}** | | |`) + previousCategory = row.category + previousResource = '' + } + + const permissionCell = row.resource === previousResource ? '' : row.resource + previousResource = row.resource + + if (rowEndpoints.length === 0) { + lines.push(`| ${permissionCell} | ${row.access} | No public Management API endpoints |`) + continue + } + + rowEndpoints.forEach((endpoint, index) => { + const label = endpoint.label.replace(/\\/g, '\\\\').replace(/\|/g, '\\|').replace(/\s+/g, ' ') + const link = /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(endpoint.operationId) + ? `[${label}](/docs/reference/api/${endpoint.operationId})` + : label + const unlocksAlone = endpoint.groups.some((group) => + group.every((scope) => rowScopes.has(scope)) + ) + let requirement = '' + if (!unlocksAlone) { + const text = `Requires ${formatRequirement(endpoint.groups)}.` + const id = footnotes.get(text) ?? String(footnotes.size + 1) + footnotes.set(text, id) + requirement = `[^${id}]` + } + lines.push( + `| ${index === 0 ? permissionCell : ''} | ${index === 0 ? row.access : ''} | ${link}${requirement} |` + ) + }) + } + + const definitions = Array.from(footnotes, ([text, id]) => `[^${id}]: ${text}`) + writeOutput(outputPath, [ + ...lines, + ...(definitions.length > 0 ? ['', ...definitions] : []), + ...missingScopesNotice(missing), + '', + ]) +} + +function generateMcpToolsPartial(tools: McpMap, outputPath: string) { + const missing = new Set() + const rows = Object.entries(tools) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([tool, groups]) => { + const publishable = knownGroups(groups, missing) + const requirement = publishable.some((group) => group.length === 0) + ? 'None (always available)' + : publishable.length === 0 + ? 'Not available to scoped personal access tokens' + : formatRequirement(publishable) + return `| \`${tool}\` | ${requirement} |` + }) + + writeOutput(outputPath, [ + GENERATED_NOTICE, + '| MCP tool | Required permission |', + '| -------- | ------------------- |', + ...rows, + ...missingScopesNotice(missing), + '', + ]) +} + +function writeOutput(outputPath: string, lines: string[]) { + fs.mkdirSync(path.dirname(outputPath), { recursive: true }) + fs.writeFileSync(outputPath, lines.join('\n'), 'utf8') + console.log(`Wrote ${outputPath}`) +} + +const args = process.argv.slice(2).map((arg) => path.resolve(arg)) +if (args.length !== 5) { + console.error( + 'Usage: generateAccessControlPartials.mts ' + + ' ' + ) + process.exit(1) +} + +const tools: McpMap = readJson(args[2]) +generatePermissionsPartial(args.slice(0, 2), tools, args[3]) +generateMcpToolsPartial(tools, args[4]) diff --git a/apps/studio/CLAUDE.md b/apps/studio/CLAUDE.md index f1f084c3192f1..3e058c3b557cf 100644 --- a/apps/studio/CLAUDE.md +++ b/apps/studio/CLAUDE.md @@ -37,6 +37,7 @@ Studio is migrating from the Next.js pages router (`pages/**`) to TanStack Start - **Telemetry** — `useTrack()` from `lib/telemetry/track`; event types live in `packages/common/telemetry-constants.ts`. - **Tests** — default to including relevant tests with any change: a couple of unit tests for extracted logic, component tests for UI behavior, E2E only when the scope demands it (`studio-testing` has the decision tree). Not every PR needs them, but "no tests" should be a considered choice, not the default. Tooling: vitest + MSW; component tests use `customRender` + `addAPIMock` from `tests/lib/`; unhandled network requests fail tests. Don't `vi.mock('@/data/...')`. - **Shortcuts** — use the registry in `state/shortcuts/` and `components/ui/Shortcut*.tsx`; keep `G then …` chords for navigation; no one-off keyboard listeners. +- **Scoped PAT catalog** — `packages/shared-data/scoped-access-token-permissions.ts` feeds Studio and the generated Personal Access Tokens guide. After changing it, run `make -C apps/docs/spec generate.partials.access-control`; Docs Tests rejects stale tables. - **Reuse first** — before writing a new hook or helper, search for an existing one (`hooks/`, `lib/`, `packages/common`, `packages/ui-patterns`). If you do need a new one, make it as reusable as possible: general naming, no page-specific coupling, placed where other callers can find it. - Co-locate sub-components with their parent; avoid barrel re-export files. diff --git a/apps/studio/components/interfaces/Account/AccessTokens/AccessToken.constants.ts b/apps/studio/components/interfaces/Account/AccessTokens/AccessToken.constants.ts index c3f765e8bb305..cc5bdee8a8000 100644 --- a/apps/studio/components/interfaces/Account/AccessTokens/AccessToken.constants.ts +++ b/apps/studio/components/interfaces/Account/AccessTokens/AccessToken.constants.ts @@ -1,5 +1,11 @@ import { permissions } from '@supabase/shared-types' import { components } from 'api-types' +import { + getAction, + getResource, + PERMISSION_CATALOG, + type FgaAction, +} from 'shared-data/scoped-access-token-permissions' export type ScopedAccessTokenPermission = components['schemas']['CreateScopedAccessTokenBody']['permissions'][number] @@ -26,25 +32,12 @@ export const EXPIRES_AT_OPTIONS = { const FGA = permissions.FgaPermissions -const getAction = (key: string): string => { - if (key.endsWith('_READ')) return 'read' - if (key.endsWith('_WRITE')) return 'write' - if (key.endsWith('_CREATE')) return 'create' - if (key.endsWith('_DELETE')) return 'delete' - return 'read' -} - -const getResource = (key: string): string => { - return key.replace(/_(READ|WRITE|CREATE|DELETE)$/, '').toLowerCase() -} - const buildPermissionList = () => { const list: Array<{ scope: string resource: string - action: string + action: FgaAction id: string - title: string }> = [] for (const [scope, scopePerms] of Object.entries(FGA)) { @@ -54,7 +47,6 @@ const buildPermissionList = () => { resource: getResource(key), action: getAction(key), id: perm.id, - title: perm.title, }) } } @@ -64,20 +56,12 @@ const buildPermissionList = () => { export const PERMISSION_LIST = buildPermissionList() -export const ACCESS_TOKEN_RESOURCES = (() => { - const resourceMap = new Map() - - for (const p of PERMISSION_LIST) { - const key = `${p.scope}:${p.resource}` - if (!resourceMap.has(key)) { - const cleanTitle = p.title.replace(/^(Read|Manage|Create|Delete)\s+/i, '') - resourceMap.set(key, { resource: key, title: cleanTitle, actions: [] }) - } - const entry = resourceMap.get(key)! - if (!entry.actions.includes(p.action)) { - entry.actions.push(p.action) - } - } - - return Array.from(resourceMap.values()) -})() +/** + * Resources shown in token permission summaries (e.g. the post-creation banner). + * Titles come from the shared permission catalog so they match the creation form + * and the generated docs tables. + */ +export const ACCESS_TOKEN_RESOURCES = PERMISSION_CATALOG.map((entry) => ({ + resource: entry.key, + title: entry.name, +})) diff --git a/apps/studio/components/interfaces/Account/AccessTokens/AccessToken.permissions.ts b/apps/studio/components/interfaces/Account/AccessTokens/AccessToken.permissions.ts index 405b96e3d61cc..33caa07b9545c 100644 --- a/apps/studio/components/interfaces/Account/AccessTokens/AccessToken.permissions.ts +++ b/apps/studio/components/interfaces/Account/AccessTokens/AccessToken.permissions.ts @@ -1,562 +1,31 @@ -import { permissions } from '@supabase/shared-types' +import { + getCatalogEntry, + PERMISSION_CATALOG, + PERMISSION_CATALOG_BY_CATEGORY, + PERMISSION_MODE_LABEL, + type PermissionCatalogEntry, + type PermissionCategoryKey, + type PermissionMode, + type RiskLevel, +} from 'shared-data/scoped-access-token-permissions' import type { ScopedAccessTokenPermission } from './AccessToken.constants' -/** - * Data model for the scoped access-token creation flow. - * - * The real permission scopes come from `@supabase/shared-types` (`FgaPermissions`). Those scopes - * carry no category or risk metadata, so this file layers editable presentation data on top: - * - PERMISSION_CATEGORIES groups every scope into one of five UI categories. - * - RESOURCE_METADATA assigns each resource a display name, description, category and risk. - * - * TODO(product): the risk levels, reasons and "Allows" copy below are proposed defaults — review - * and adjust. Where a resource has no explicit metadata entry we fall back to a heuristic. - */ - -const FGA = permissions.FgaPermissions - -export type PermissionMode = 'none' | 'read' | 'readwrite' -export type RiskLevel = 'low' | 'medium' | 'high' -export type PermissionCategoryKey = 'account' | 'project' | 'database' | 'appsvc' | 'infra' - -export interface PermissionCategory { - key: PermissionCategoryKey - name: string - description: string -} - -/** Display order matches the accordion, where every category starts collapsed. */ -export const PERMISSION_CATEGORIES: PermissionCategory[] = [ - { - key: 'project', - name: 'Project', - description: 'Core project visibility, settings, and diagnostics.', - }, - { - key: 'database', - name: 'Database', - description: 'SQL access, migrations, backups, and data operations.', - }, - { - key: 'appsvc', - name: 'Application Services', - description: 'Auth, storage, realtime, edge functions, and service configuration.', - }, - { - key: 'infra', - name: 'Infrastructure & Delivery', - description: 'Branch automation, domains, add-ons, and network.', - }, - { - key: 'account', - name: 'Account & Organization', - description: 'Account-wide and organization-level access that spans projects.', - }, -] - -interface ResourceMeta { - category: PermissionCategoryKey - name: string - description: string - risk: RiskLevel - riskReason: string - allowsRead?: string[] - allowsWrite?: string[] -} - -/** - * Per-resource presentation metadata, keyed by the derived `scope:resource` key (see - * AccessToken.constants → ACCESS_TOKEN_RESOURCES). Every resource returned from FgaPermissions - * should have an entry; RESOURCE_METADATA_FALLBACK covers anything that slips through. - */ -const RESOURCE_METADATA: Record = { - // --- Account & Organization --- - 'user:organizations': { - category: 'account', - name: 'Organizations', - description: 'Organizations you belong to.', - risk: 'medium', - riskReason: 'Read-write can create new organizations under your account.', - allowsRead: ['List your organizations'], - allowsWrite: ['Create organizations'], - }, - 'user:projects': { - category: 'account', - name: 'Projects (account-wide)', - description: 'Projects across all your organizations.', - risk: 'low', - riskReason: 'Read-only listing of the projects you can access.', - allowsRead: ['List your projects'], - }, - 'user:snippets': { - category: 'account', - name: 'SQL Snippets (account-wide)', - description: 'Saved SQL snippets across your account.', - risk: 'low', - riskReason: 'Read-only access to your saved snippets.', - allowsRead: ['Read your SQL snippets'], - }, - 'organization:admin': { - category: 'account', - name: 'Organization Settings', - description: 'Organization settings and project transfers.', - risk: 'high', - riskReason: 'Read-write grants elevated access to organization settings and project transfers.', - allowsRead: ['Read organization settings'], - allowsWrite: ['Manage organization settings', 'Transfer projects'], - }, - 'organization:members': { - category: 'account', - name: 'Organization Members', - description: 'Members and roles within the organization.', - risk: 'high', - riskReason: 'Read-write can add or remove members and change roles across your organization.', - allowsRead: ['Read organization members'], - allowsWrite: ['Add or remove members', 'Change member roles'], - }, - 'organization:projects': { - category: 'account', - name: 'Organization Projects', - description: 'Projects within the organization.', - risk: 'medium', - riskReason: 'Read-write can create new projects in the organization.', - allowsRead: ['List organization projects'], - allowsWrite: ['Create organization projects'], - }, - - // --- Project --- - 'project:admin': { - category: 'project', - name: 'Project Settings', - description: 'Project metadata and settings.', - risk: 'high', - riskReason: 'Read-write grants elevated access to change project settings and configuration.', - allowsRead: ['Read project metadata'], - allowsWrite: ['Update project settings'], - }, - 'project:action_runs': { - category: 'project', - name: 'Action Runs', - description: 'Project action run status and logs.', - risk: 'medium', - riskReason: 'Read-write can trigger action runs that execute project workflows.', - allowsRead: ['Read action run status', 'Read run logs'], - allowsWrite: ['Trigger action runs'], - }, - 'project:advisors': { - category: 'project', - name: 'Advisors', - description: 'Security and performance advisor results.', - risk: 'low', - riskReason: 'Read-only access to advisor findings — no changes possible.', - allowsRead: ['Read security advisors', 'Read performance advisors'], - }, - 'project:analytics_logs': { - category: 'project', - name: 'Logs', - description: 'Operational logs and log analytics.', - risk: 'low', - riskReason: 'Read-only access to project logs.', - allowsRead: ['Read project logs'], - }, - 'project:analytics_usage': { - category: 'project', - name: 'Usage Analytics', - description: 'Project usage and analytics data.', - risk: 'low', - riskReason: 'Read-only access to usage analytics.', - allowsRead: ['Read usage analytics'], - }, - 'project:snippets': { - category: 'project', - name: 'SQL Snippets', - description: 'Saved SQL snippets for the project.', - risk: 'low', - riskReason: 'Read-write can create and edit saved SQL snippets.', - allowsRead: ['Read project SQL snippets'], - allowsWrite: ['Manage project SQL snippets'], - }, - - // --- Database --- - 'project:database': { - category: 'database', - name: 'Database', - description: 'Database access and data operations.', - risk: 'high', - riskReason: - 'Read-write lets this token run arbitrary SQL, so it can modify or delete any data in your database.', - allowsRead: ['Read tables and schema', 'Run read-only queries'], - allowsWrite: ['Run arbitrary SQL'], - }, - 'project:database_migrations': { - category: 'database', - name: 'Migrations', - description: 'Database migration history and application.', - risk: 'high', - riskReason: - 'Read-write can apply schema changes that alter or drop tables across your database.', - allowsRead: ['Read migration history'], - allowsWrite: ['Apply migrations'], - }, - 'project:backups': { - category: 'database', - name: 'Backups', - description: 'Database backups, restore points, and restore.', - risk: 'high', - riskReason: - 'Read-write can trigger restores that overwrite current data with an earlier snapshot.', - allowsRead: ['Read backups and restore points'], - allowsWrite: ['Trigger restores'], - }, - 'project:database_config': { - category: 'database', - name: 'Database Config', - description: 'Database configuration.', - risk: 'medium', - riskReason: 'Read-write can change database configuration.', - allowsRead: ['Read database configuration'], - allowsWrite: ['Update database configuration'], - }, - 'project:database_jit': { - category: 'database', - name: 'Database JIT', - description: 'Just-in-time database access settings.', - risk: 'medium', - riskReason: 'Read-write can change just-in-time database access settings.', - allowsRead: ['Read JIT settings'], - allowsWrite: ['Manage JIT settings'], - }, - 'project:database_pooling_config': { - category: 'database', - name: 'Connection Pooling', - description: 'Database connection pooling.', - risk: 'medium', - riskReason: 'Read-write can change connection pooling behavior.', - allowsRead: ['Read pooling configuration'], - allowsWrite: ['Update pooling configuration'], - }, - 'project:database_readonly_config': { - category: 'database', - name: 'Read-only Mode', - description: 'Database read-only mode.', - risk: 'medium', - riskReason: 'Read-write can toggle the database into or out of read-only mode.', - allowsRead: ['Read read-only mode status'], - allowsWrite: ['Toggle read-only mode'], - }, - 'project:database_ssl_config': { - category: 'database', - name: 'SSL Enforcement', - description: 'Database SSL configuration.', - risk: 'medium', - riskReason: 'Read-write can change SSL enforcement for database connections.', - allowsRead: ['Read SSL configuration'], - allowsWrite: ['Manage SSL enforcement'], - }, - 'project:database_webhooks_config': { - category: 'database', - name: 'Database Webhooks', - description: 'Webhooks triggered from the database.', - risk: 'medium', - riskReason: 'Read-write can change database webhook configuration.', - allowsRead: ['Read webhook configuration'], - allowsWrite: ['Manage database webhooks'], - }, - 'project:database_network_bans': { - category: 'database', - name: 'Network Bans', - description: 'Banned IPs for the database.', - risk: 'medium', - riskReason: 'Read-write can ban or unban IP addresses from reaching the database.', - allowsRead: ['Read banned IPs'], - allowsWrite: ['Manage banned IPs'], - }, - 'project:database_network_restrictions': { - category: 'database', - name: 'Network Restrictions', - description: 'Network restrictions for the database.', - risk: 'high', - riskReason: 'Read-write can change which networks are allowed to reach the database.', - allowsRead: ['Read network restrictions'], - allowsWrite: ['Manage network restrictions'], - }, - - // --- Application Services --- - 'project:auth_config': { - category: 'appsvc', - name: 'Auth Config', - description: 'Authentication provider and settings.', - risk: 'high', - riskReason: - 'Read-write can change authentication providers and settings, affecting how users sign in.', - allowsRead: ['Read auth configuration'], - allowsWrite: ['Update auth providers and settings'], - }, - 'project:auth_signing_keys': { - category: 'appsvc', - name: 'Auth Signing Keys', - description: 'Authentication signing keys.', - risk: 'high', - riskReason: 'Read-write can rotate signing keys, invalidating existing sessions and tokens.', - allowsRead: ['Read signing keys'], - allowsWrite: ['Manage signing keys'], - }, - 'project:api_gateway_keys': { - category: 'appsvc', - name: 'API Keys', - description: 'Project API keys.', - risk: 'high', - riskReason: 'Read exposes API keys; read-write grants elevated access to create new keys.', - allowsRead: ['Read project API keys'], - allowsWrite: ['Create and revoke API keys'], - }, - 'project:edge_functions': { - category: 'appsvc', - name: 'Edge Functions', - description: 'Edge functions.', - risk: 'medium', - riskReason: 'Read-write can deploy or delete edge functions.', - allowsRead: ['Read edge functions'], - allowsWrite: ['Deploy and delete edge functions'], - }, - 'project:edge_functions_secrets': { - category: 'appsvc', - name: 'Edge Function Secrets', - description: 'Secrets available to edge functions.', - risk: 'high', - riskReason: 'Read exposes function secrets; read-write can set new secret values.', - allowsRead: ['Read edge function secrets'], - allowsWrite: ['Set edge function secrets'], - }, - 'project:realtime_config': { - category: 'appsvc', - name: 'Realtime Config', - description: 'Realtime configuration.', - risk: 'medium', - riskReason: 'Read-write can change realtime settings and shut down active connections.', - allowsRead: ['Read realtime configuration'], - allowsWrite: ['Update realtime settings'], - }, - 'project:storage': { - category: 'appsvc', - name: 'Storage', - description: 'File storage buckets and objects.', - risk: 'medium', - riskReason: 'Read-write can modify or delete stored files.', - allowsRead: ['Read storage buckets and objects'], - allowsWrite: ['Manage storage buckets and objects'], - }, - 'project:storage_config': { - category: 'appsvc', - name: 'Storage Config', - description: 'Storage bucket configuration.', - risk: 'medium', - riskReason: 'Read-write can change storage configuration.', - allowsRead: ['Read storage configuration'], - allowsWrite: ['Update storage configuration'], - }, - 'project:data_api_config': { - category: 'appsvc', - name: 'Data API Config', - description: 'PostgREST behavior and settings.', - risk: 'medium', - riskReason: 'Read-write can change how the auto-generated Data API behaves.', - allowsRead: ['Read Data API configuration'], - allowsWrite: ['Update Data API configuration'], - }, - - // --- Infrastructure & Delivery --- - 'project:branching_development': { - category: 'infra', - name: 'Development Branches', - description: 'Development branch automation.', - risk: 'low', - riskReason: 'Branch automation for development workflows — limited blast radius.', - allowsRead: ['Read development branches'], - allowsWrite: ['Create, update, and delete development branches'], - }, - 'project:branching_production': { - category: 'infra', - name: 'Production Branches', - description: 'Production branch automation.', - risk: 'high', - riskReason: - 'Read-write grants elevated access to create, merge, or delete production branches.', - allowsRead: ['Read production branches'], - allowsWrite: ['Create, merge, and delete production branches'], - }, - 'project:custom_domain': { - category: 'infra', - name: 'Custom Domains', - description: 'Custom hostnames.', - risk: 'medium', - riskReason: 'Read-write can change custom hostnames, affecting how your project is reached.', - allowsRead: ['Read custom domain configuration'], - allowsWrite: ['Set custom hostnames'], - }, - 'project:vanity_subdomain': { - category: 'infra', - name: 'Vanity Subdomain', - description: 'Project vanity subdomain.', - risk: 'medium', - riskReason: 'Read-write can change the project vanity subdomain.', - allowsRead: ['Read vanity subdomain'], - allowsWrite: ['Manage vanity subdomain'], - }, - 'project:infra_addons': { - category: 'infra', - name: 'Add-ons', - description: 'Infrastructure add-ons.', - risk: 'medium', - riskReason: 'Read-write can enable or change paid infrastructure add-ons.', - allowsRead: ['Read infrastructure add-ons'], - allowsWrite: ['Manage infrastructure add-ons'], - }, - 'project:infra_disk_config': { - category: 'infra', - name: 'Disk Config', - description: 'Disk configuration.', - risk: 'medium', - riskReason: 'Read-write can change disk size and configuration, which may incur cost.', - allowsRead: ['Read disk configuration'], - allowsWrite: ['Manage disk configuration'], - }, - 'project:read_replicas': { - category: 'infra', - name: 'Read Replicas', - description: 'Read replica configuration.', - risk: 'medium', - riskReason: 'Read-write can provision or remove read replicas, which may incur cost.', - allowsRead: ['Read read-replica configuration'], - allowsWrite: ['Manage read replicas'], - }, -} - -const RESOURCE_METADATA_FALLBACK = ( - resourceKey: string, - title: string, - hasWrite: boolean -): ResourceMeta => ({ - category: resourceKey.startsWith('project:') ? 'project' : 'account', - name: title.replace(/^(Read|Manage|Create|Delete)\s+/i, ''), - description: title, - risk: hasWrite ? 'medium' : 'low', - riskReason: hasWrite - ? 'Read-write can modify this resource.' - : 'Read-only access to this resource.', -}) - -const PERMISSION_LEVELS = ['user', 'organization', 'project'] as const - -export type PermissionLevel = (typeof PERMISSION_LEVELS)[number] - -/** - * Runtime guard for the FGA namespaces: role evaluation branches on the level, so an unrecognized - * namespace must fail loudly (at module load, caught by any test importing the catalog) rather - * than silently evaluate as project-level. - */ -const toPermissionLevel = (scope: string): PermissionLevel => { - const level = scope.toLowerCase() - const match = PERMISSION_LEVELS.find((candidate) => candidate === level) - if (match === undefined) throw new Error(`Unknown FGA namespace: ${scope}`) - return match +export { + getCatalogEntry, + PERMISSION_CATALOG, + PERMISSION_CATALOG_BY_CATEGORY, + PERMISSION_MODE_LABEL, } - -export interface PermissionCatalogEntry { - /** Derived resource key, e.g. "project:database" */ - key: string - /** Which FGA namespace the resource lives in — decides which role (org vs project) governs it. */ - level: PermissionLevel - category: PermissionCategoryKey - name: string - description: string - risk: RiskLevel - riskReason: string - allowsRead: string[] - allowsWrite: string[] - /** Whether a Read-write mode is offered (false => read-only resource). */ - writable: boolean - /** FGA scope ids granted at Read (and above). */ - readScopes: ScopedAccessTokenPermission[] - /** Additional FGA scope ids granted at Read-write (write / create / delete). */ - writeScopes: ScopedAccessTokenPermission[] -} - -const getAction = (key: string): 'read' | 'write' | 'create' | 'delete' => { - if (key.endsWith('_WRITE')) return 'write' - if (key.endsWith('_CREATE')) return 'create' - if (key.endsWith('_DELETE')) return 'delete' - return 'read' -} - -const getResource = (key: string): string => - key.replace(/_(READ|WRITE|CREATE|DELETE)$/, '').toLowerCase() +export type { PermissionCatalogEntry, PermissionMode, RiskLevel } /** - * Builds the permission catalog from the real FgaPermissions. Each unique `scope:resource` becomes - * one row; its read scope maps to Read mode and its write/create/delete scopes to Read-write mode. + * Selection model for the scoped personal access token creation flow: none/read/readwrite modes per + * catalog entry, and the conversions between a selection and concrete FGA scope ids. + * + * The catalog itself (resources, categories, risk metadata, scopes) lives in + * shared-data/scoped-access-token-permissions.ts so the docs generator can consume it too. */ -const buildCatalog = (): PermissionCatalogEntry[] => { - const byResource = new Map< - string, - { level: PermissionLevel; title: string; readScopes: string[]; writeScopes: string[] } - >() - - for (const [scope, scopePerms] of Object.entries(FGA)) { - const level = toPermissionLevel(scope) - for (const [permKey, perm] of Object.entries(scopePerms)) { - const resourceKey = `${level}:${getResource(permKey)}` - const action = getAction(permKey) - if (!byResource.has(resourceKey)) { - byResource.set(resourceKey, { level, title: perm.title, readScopes: [], writeScopes: [] }) - } - const entry = byResource.get(resourceKey)! - if (action === 'read') entry.readScopes.push(perm.id) - else entry.writeScopes.push(perm.id) - } - } - - const catalog: PermissionCatalogEntry[] = [] - for (const [key, { level, title, readScopes, writeScopes }] of byResource.entries()) { - const meta = - RESOURCE_METADATA[key] ?? RESOURCE_METADATA_FALLBACK(key, title, writeScopes.length > 0) - catalog.push({ - key, - level, - category: meta.category, - name: meta.name, - description: meta.description, - risk: meta.risk, - riskReason: meta.riskReason, - allowsRead: meta.allowsRead ?? [`Read ${meta.name.toLowerCase()}`], - allowsWrite: - meta.allowsWrite ?? (writeScopes.length > 0 ? [`Modify ${meta.name.toLowerCase()}`] : []), - writable: writeScopes.length > 0, - readScopes: readScopes as ScopedAccessTokenPermission[], - writeScopes: writeScopes as ScopedAccessTokenPermission[], - }) - } - return catalog -} - -export const PERMISSION_CATALOG = buildCatalog() - -const CATALOG_BY_KEY = new Map(PERMISSION_CATALOG.map((entry) => [entry.key, entry])) - -export const getCatalogEntry = (key: string) => CATALOG_BY_KEY.get(key) - -export interface CategoryWithEntries extends PermissionCategory { - entries: PermissionCatalogEntry[] -} - -/** Catalog grouped by category, in category display order, dropping empty categories. */ -export const PERMISSION_CATALOG_BY_CATEGORY: CategoryWithEntries[] = PERMISSION_CATEGORIES.map( - (category) => ({ - ...category, - entries: PERMISSION_CATALOG.filter((entry) => entry.category === category.key), - }) -).filter((category) => category.entries.length > 0) /** Map of resource key -> selected mode. Absent keys are treated as 'none'. */ export type PermissionSelection = Record @@ -577,7 +46,7 @@ export const selectionToScopes = ( ): ScopedAccessTokenPermission[] => { const scopes: ScopedAccessTokenPermission[] = [] for (const [key, mode] of Object.entries(selection)) { - const entry = CATALOG_BY_KEY.get(key) + const entry = getCatalogEntry(key) if (!entry) continue scopes.push(...getEntryScopes(entry, mode)) } @@ -590,7 +59,7 @@ export const selectionToScopes = ( * Tokens created through the Management API can hold arbitrary scope subsets that the * none/read/readwrite modes cannot represent exactly (e.g. a lone branching_development_create). * Any granted scope of an entry marks it at the corresponding mode, so a partial grant is never - * dropped — the mode is an upper bound and may name specific operations the token lacks, but it + * dropped. The mode is an upper bound and may name specific operations the token lacks, but it * never understates the token's authority or risk. The endpoint and MCP-tool lists, computed * from the actual granted scopes, remain the precise view. */ @@ -623,12 +92,6 @@ export const RISK_LEVEL_LABEL: Record = { high: 'High risk', } -export const PERMISSION_MODE_LABEL: Record = { - none: 'None', - read: 'Read', - readwrite: 'Read-write', -} - export type ResourceAccessMode = 'project' | 'organization' | 'account' export const RISK_TONE_VARIANT: Record = { diff --git a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/PermissionsAccordion.tsx b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/PermissionsAccordion.tsx index b96ba33e95ebc..2fd628ebc002d 100644 --- a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/PermissionsAccordion.tsx +++ b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/PermissionsAccordion.tsx @@ -13,8 +13,6 @@ import { getActivePreset, type PermissionPreset } from '../../AccessToken.preset import type { TokenAccessEvaluation } from '../../AccessToken.roles' import { PermissionPresetSelect } from './PermissionPresetSelect' import { PermissionRow } from './PermissionRow' -import { InlineLink } from '@/components/ui/InlineLink' -import { DOCS_URL } from '@/lib/constants' interface PermissionsAccordionProps { selection: PermissionSelection @@ -42,11 +40,7 @@ export const PermissionsAccordion = ({ description={

Grant the minimum access this token needs. Everything defaults to None. Permissions - follow your role in the organizations and projects you're a member of — see{' '} - - access control - {' '} - for how roles work. + follow your role in the organizations and projects you're a member of.

} > diff --git a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/NewScopedTokenSheet.tsx b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/NewScopedTokenSheet.tsx index 302388d8060d6..b78759675420a 100644 --- a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/NewScopedTokenSheet.tsx +++ b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/NewScopedTokenSheet.tsx @@ -15,6 +15,7 @@ import { ExperimentalTokenDropdown } from '../Classic/ExperimentalTokenDropdown' import { NewScopedTokenForm } from './Form/NewScopedTokenForm' import { getExpiryDate, type TokenFormValues } from './Form/NewScopedTokenForm.utils' import { NewScopedTokenSuccess } from './Form/NewScopedTokenSuccess' +import { TokenDocsButtons } from './TokenDocsButtons' import { useAccessTokenCreateMutation, type NewAccessToken, @@ -124,11 +125,12 @@ export const NewScopedTokenSheet = ({ onCreateExperimentalToken }: NewScopedToke size="default" className="flex h-full flex-col gap-0 sm:w-[656px] lg:w-[800px]" > - + {step === 'success' ? 'Token created' : 'Generate token'} Configure and create a new access token. + {step !== 'success' && } {step === 'success' && createdToken ? ( { + return ( +
+ + +
+ ) +} diff --git a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/ViewTokenSheet.tsx b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/ViewTokenSheet.tsx index 533c774403450..7ac97feadf3ed 100644 --- a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/ViewTokenSheet.tsx +++ b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/ViewTokenSheet.tsx @@ -21,7 +21,7 @@ import { getCapabilityDensityTier, type CapabilityLevelFilter, } from './TokenCapabilities/TokenCapabilities.utils' -import { DocsButton } from '@/components/ui/DocsButton' +import { TokenDocsButtons } from './TokenDocsButtons' import { useOrganizationsQuery } from '@/data/organizations/organizations-query' import { useProjectsInfiniteQuery } from '@/data/projects/projects-infinite-query' import { @@ -29,7 +29,6 @@ import { useGetEnabledEndpointsForCapability, } from '@/data/scoped-access-tokens/permission-scope-map-query' import { useScopedAccessTokenQuery } from '@/data/scoped-access-tokens/scoped-access-token-query' -import { DOCS_URL } from '@/lib/constants' interface ViewTokenSheetProps { visible: boolean @@ -148,18 +147,7 @@ export function ViewTokenSheet({ visible, tokenId, onClose }: ViewTokenSheetProp

View access for {token?.name}

-
- - -
+
{/* Radix wraps viewport children in an inline-styled display:table div that grows to fit the widest child, which would let one long endpoint path expand the sheet instead of diff --git a/packages/shared-data/package.json b/packages/shared-data/package.json index bc39f07660007..68e503b0df545 100644 --- a/packages/shared-data/package.json +++ b/packages/shared-data/package.json @@ -15,6 +15,7 @@ "author": "", "license": "MIT", "dependencies": { + "@supabase/shared-types": "0.1.91", "zod": "catalog:" } } diff --git a/packages/shared-data/scoped-access-token-permissions.ts b/packages/shared-data/scoped-access-token-permissions.ts new file mode 100644 index 0000000000000..2c8e2934abb2f --- /dev/null +++ b/packages/shared-data/scoped-access-token-permissions.ts @@ -0,0 +1,625 @@ +import { permissions } from '@supabase/shared-types' + +/** + * The permission catalog for scoped personal access tokens: every FGA scope grouped into resources with + * display names, categories, and risk metadata. + * + * The real permission scopes come from `@supabase/shared-types` (`FgaPermissions`). Those scopes + * carry no category or risk metadata, so this file layers editable presentation data on top: + * - PERMISSION_CATEGORIES groups every scope into a display category. + * - RESOURCE_METADATA assigns each resource a display name, description, category and risk. + * + * Lives in shared-data (not apps/studio) because two apps consume it: Studio's scoped personal + * access token creation form, and the docs generator that renders the "Personal Access Tokens" + * guide's permission tables (apps/docs/spec/sections/generateAccessControlPartials.mts). + * The docs must show the same names, categories, and order as the form. + * + * The `name` and `category` fields are published copy: they are the permission and section labels + * in both the form and the docs guide, so renaming one renames both on the next regeneration. + * + * TODO(product): the risk levels, risk reasons, and "Allows" copy below are proposed defaults and + * still need review. They render in the form only, not in the docs. Where a resource has no + * explicit metadata entry we fall back to a heuristic. + */ + +const FGA = permissions.FgaPermissions + +type PermissionsOf = T extends Record ? P : never +/** Union of every FGA scope id literal published by @supabase/shared-types. */ +export type FgaScopeId = + PermissionsOf<(typeof FGA)[keyof typeof FGA]> extends { id: infer Id } ? Id : never + +export type RiskLevel = 'low' | 'medium' | 'high' + +/** + * Display labels for permission modes. Part of the form<->docs contract like resource names: + * Studio's form and the docs guide's tables must label modes identically. + */ +export const PERMISSION_MODE_LABEL = { + none: 'None', + read: 'Read', + readwrite: 'Read-write', +} as const + +/** Selection modes for a catalog entry. The label map's keys are the single source of truth. */ +export type PermissionMode = keyof typeof PERMISSION_MODE_LABEL + +export type PermissionCategoryKey = 'account' | 'project' | 'database' | 'appsvc' | 'infra' + +export interface PermissionCategory { + key: PermissionCategoryKey + name: string + description: string +} + +/** Display order. The form's accordion sections and the docs guide's sections both follow it. */ +const PERMISSION_CATEGORIES: PermissionCategory[] = [ + { + key: 'project', + name: 'Project', + description: 'Core project visibility, settings, and diagnostics.', + }, + { + key: 'database', + name: 'Database', + description: 'SQL access, migrations, backups, and data operations.', + }, + { + key: 'appsvc', + name: 'Application services', + description: 'Auth, storage, realtime, edge functions, and service configuration.', + }, + { + key: 'infra', + name: 'Infrastructure and delivery', + description: 'Branch automation, domains, add-ons, and network.', + }, + { + key: 'account', + name: 'Account and organization', + description: 'Account-wide and organization-level access that spans projects.', + }, +] + +interface ResourceMeta { + category: PermissionCategoryKey + name: string + description: string + risk: RiskLevel + riskReason: string + allowsRead?: string[] + allowsWrite?: string[] +} + +/** + * Per-resource presentation metadata, keyed by the derived `scope:resource` key (derived in + * `buildCatalog` below). Every resource returned from FgaPermissions should have an entry; + * RESOURCE_METADATA_FALLBACK covers anything that slips through. + */ +const RESOURCE_METADATA: Record = { + // --- Account and organization --- + 'user:organizations': { + category: 'account', + name: 'Organizations', + description: 'Organizations you belong to.', + risk: 'medium', + riskReason: 'Read-write can create new organizations under your account.', + allowsRead: ['List your organizations'], + allowsWrite: ['Create organizations'], + }, + 'user:projects': { + category: 'account', + name: 'Projects (account-wide)', + description: 'Projects across all your organizations.', + risk: 'low', + riskReason: 'Read-only listing of the projects you can access.', + allowsRead: ['List your projects'], + }, + 'user:snippets': { + category: 'account', + name: 'SQL Snippets (account-wide)', + description: 'Saved SQL snippets across your account.', + risk: 'low', + riskReason: 'Read-only access to your saved snippets.', + allowsRead: ['Read your SQL snippets'], + }, + 'organization:admin': { + category: 'account', + name: 'Organization Settings', + description: 'Organization settings and project transfers.', + risk: 'high', + riskReason: 'Read-write grants elevated access to organization settings and project transfers.', + allowsRead: ['Read organization settings'], + allowsWrite: ['Manage organization settings', 'Transfer projects'], + }, + 'organization:members': { + category: 'account', + name: 'Organization Members', + description: 'Members and roles within the organization.', + risk: 'high', + riskReason: 'Read-write can add or remove members and change roles across your organization.', + allowsRead: ['Read organization members'], + allowsWrite: ['Add or remove members', 'Change member roles'], + }, + 'organization:projects': { + category: 'account', + name: 'Organization Projects', + description: 'Projects within the organization.', + risk: 'medium', + riskReason: 'Read-write can create new projects in the organization.', + allowsRead: ['List organization projects'], + allowsWrite: ['Create organization projects'], + }, + 'organization:platform_webhooks': { + category: 'account', + name: 'Platform Webhooks (organization)', + description: 'Platform webhook endpoints and deliveries for the organization.', + risk: 'medium', + riskReason: 'Read-write can create webhook endpoints that receive organization events.', + allowsRead: ['Read webhook endpoints and deliveries'], + allowsWrite: ['Manage webhook endpoints'], + }, + + // --- Project --- + 'project:admin': { + category: 'project', + name: 'Project Settings', + description: 'Project metadata and settings.', + risk: 'high', + riskReason: 'Read-write grants elevated access to change project settings and configuration.', + allowsRead: ['Read project metadata'], + allowsWrite: ['Update project settings'], + }, + 'project:action_runs': { + category: 'project', + name: 'Action Runs', + description: 'Project action run status and logs.', + risk: 'medium', + riskReason: 'Read-write can trigger action runs that execute project workflows.', + allowsRead: ['Read action run status', 'Read run logs'], + allowsWrite: ['Trigger action runs'], + }, + 'project:advisors': { + category: 'project', + name: 'Advisors', + description: 'Security and performance advisor results.', + risk: 'low', + riskReason: 'Read-only access to advisor findings. No changes possible.', + allowsRead: ['Read security advisors', 'Read performance advisors'], + }, + 'project:analytics_logs': { + category: 'project', + name: 'Logs', + description: 'Operational logs and log analytics.', + risk: 'low', + riskReason: 'Read-only access to project logs.', + allowsRead: ['Read project logs'], + }, + 'project:analytics_usage': { + category: 'project', + name: 'Usage Analytics', + description: 'Project usage and analytics data.', + risk: 'low', + riskReason: 'Read-only access to usage analytics.', + allowsRead: ['Read usage analytics'], + }, + 'project:analytics_config': { + category: 'project', + name: 'Analytics Config', + description: 'Log drains and analytics configuration.', + risk: 'medium', + riskReason: 'Read-write can create log drains that export project logs.', + allowsRead: ['Read analytics configuration'], + allowsWrite: ['Manage log drains'], + }, + 'project:platform_webhooks': { + category: 'project', + name: 'Platform Webhooks', + description: 'Platform webhook endpoints and deliveries for the project.', + risk: 'medium', + riskReason: 'Read-write can create webhook endpoints that receive project events.', + allowsRead: ['Read webhook endpoints and deliveries'], + allowsWrite: ['Manage webhook endpoints'], + }, + 'project:snippets': { + category: 'project', + name: 'SQL Snippets', + description: 'Saved SQL snippets for the project.', + risk: 'low', + riskReason: 'Read-write can create and edit saved SQL snippets.', + allowsRead: ['Read project SQL snippets'], + allowsWrite: ['Manage project SQL snippets'], + }, + + // --- Database --- + 'project:database': { + category: 'database', + name: 'Database', + description: 'Database access and data operations.', + risk: 'high', + riskReason: + 'Read-write lets this token run arbitrary SQL, so it can modify or delete any data in your database.', + allowsRead: ['Read tables and schema', 'Run read-only queries'], + allowsWrite: ['Run arbitrary SQL'], + }, + 'project:database_migrations': { + category: 'database', + name: 'Migrations', + description: 'Database migration history and application.', + risk: 'high', + riskReason: + 'Read-write can apply schema changes that alter or drop tables across your database.', + allowsRead: ['Read migration history'], + allowsWrite: ['Apply migrations'], + }, + 'project:backups': { + category: 'database', + name: 'Backups', + description: 'Database backups, restore points, and restore.', + risk: 'high', + riskReason: + 'Read-write can trigger restores that overwrite current data with an earlier snapshot.', + allowsRead: ['Read backups and restore points'], + allowsWrite: ['Trigger restores'], + }, + 'project:database_config': { + category: 'database', + name: 'Database Config', + description: 'Database configuration.', + risk: 'medium', + riskReason: 'Read-write can change database configuration.', + allowsRead: ['Read database configuration'], + allowsWrite: ['Update database configuration'], + }, + 'project:database_jit': { + category: 'database', + name: 'Database JIT', + description: 'Just-in-time database access settings.', + risk: 'medium', + riskReason: 'Read-write can change just-in-time database access settings.', + allowsRead: ['Read JIT settings'], + allowsWrite: ['Manage JIT settings'], + }, + 'project:database_pooling_config': { + category: 'database', + name: 'Connection Pooling', + description: 'Database connection pooling.', + risk: 'medium', + riskReason: 'Read-write can change connection pooling behavior.', + allowsRead: ['Read pooling configuration'], + allowsWrite: ['Update pooling configuration'], + }, + 'project:database_readonly_config': { + category: 'database', + name: 'Read-only Mode', + description: 'Database read-only mode.', + risk: 'medium', + riskReason: 'Read-write can toggle the database into or out of read-only mode.', + allowsRead: ['Read read-only mode status'], + allowsWrite: ['Toggle read-only mode'], + }, + 'project:database_ssl_config': { + category: 'database', + name: 'SSL Enforcement', + description: 'Database SSL configuration.', + risk: 'medium', + riskReason: 'Read-write can change SSL enforcement for database connections.', + allowsRead: ['Read SSL configuration'], + allowsWrite: ['Manage SSL enforcement'], + }, + 'project:database_webhooks_config': { + category: 'database', + name: 'Database Webhooks', + description: 'Webhooks triggered from the database.', + risk: 'medium', + riskReason: 'Read-write can change database webhook configuration.', + allowsRead: ['Read webhook configuration'], + allowsWrite: ['Manage database webhooks'], + }, + 'project:database_network_bans': { + category: 'database', + name: 'Network Bans', + description: 'Banned IPs for the database.', + risk: 'medium', + riskReason: 'Read-write can ban or unban IP addresses from reaching the database.', + allowsRead: ['Read banned IPs'], + allowsWrite: ['Manage banned IPs'], + }, + 'project:database_network_restrictions': { + category: 'database', + name: 'Network Restrictions', + description: 'Network restrictions for the database.', + risk: 'high', + riskReason: 'Read-write can change which networks are allowed to reach the database.', + allowsRead: ['Read network restrictions'], + allowsWrite: ['Manage network restrictions'], + }, + + // --- Application services --- + 'project:auth_config': { + category: 'appsvc', + name: 'Auth Config', + description: 'Authentication provider and settings.', + risk: 'high', + riskReason: + 'Read-write can change authentication providers and settings, affecting how users sign in.', + allowsRead: ['Read auth configuration'], + allowsWrite: ['Update auth providers and settings'], + }, + 'project:auth_signing_keys': { + category: 'appsvc', + name: 'Auth Signing Keys', + description: 'Authentication signing keys.', + risk: 'high', + riskReason: 'Read-write can rotate signing keys, invalidating existing sessions and tokens.', + allowsRead: ['Read signing keys'], + allowsWrite: ['Manage signing keys'], + }, + 'project:api_gateway_keys': { + category: 'appsvc', + name: 'API Keys', + description: 'Project API keys.', + risk: 'high', + riskReason: 'Read exposes API keys; read-write grants elevated access to create new keys.', + allowsRead: ['Read project API keys'], + allowsWrite: ['Create and revoke API keys'], + }, + 'project:edge_functions': { + category: 'appsvc', + name: 'Edge Functions', + description: 'Edge functions.', + risk: 'medium', + riskReason: 'Read-write can deploy or delete edge functions.', + allowsRead: ['Read edge functions'], + allowsWrite: ['Deploy and delete edge functions'], + }, + 'project:edge_functions_secrets': { + category: 'appsvc', + name: 'Edge Function Secrets', + description: 'Secrets available to edge functions.', + risk: 'high', + riskReason: 'Read exposes function secrets; read-write can set new secret values.', + allowsRead: ['Read edge function secrets'], + allowsWrite: ['Set edge function secrets'], + }, + 'project:realtime_config': { + category: 'appsvc', + name: 'Realtime Config', + description: 'Realtime configuration.', + risk: 'medium', + riskReason: 'Read-write can change realtime settings and shut down active connections.', + allowsRead: ['Read realtime configuration'], + allowsWrite: ['Update realtime settings'], + }, + 'project:storage': { + category: 'appsvc', + name: 'Storage', + description: 'File storage buckets and objects.', + risk: 'medium', + riskReason: 'Read-write can modify or delete stored files.', + allowsRead: ['Read storage buckets and objects'], + allowsWrite: ['Manage storage buckets and objects'], + }, + 'project:storage_config': { + category: 'appsvc', + name: 'Storage Config', + description: 'Storage bucket configuration.', + risk: 'medium', + riskReason: 'Read-write can change storage configuration.', + allowsRead: ['Read storage configuration'], + allowsWrite: ['Update storage configuration'], + }, + 'project:data_api_config': { + category: 'appsvc', + name: 'Data API Config', + description: 'PostgREST behavior and settings.', + risk: 'medium', + riskReason: 'Read-write can change how the auto-generated Data API behaves.', + allowsRead: ['Read Data API configuration'], + allowsWrite: ['Update Data API configuration'], + }, + + // --- Infrastructure and delivery --- + 'project:branching_development': { + category: 'infra', + name: 'Development Branches', + description: 'Development branch automation.', + risk: 'low', + riskReason: 'Branch automation for development workflows with limited blast radius.', + allowsRead: ['Read development branches'], + allowsWrite: ['Create, update, and delete development branches'], + }, + 'project:branching_production': { + category: 'infra', + name: 'Production Branches', + description: 'Production branch automation.', + risk: 'high', + riskReason: + 'Read-write grants elevated access to create, merge, or delete production branches.', + allowsRead: ['Read production branches'], + allowsWrite: ['Create, merge, and delete production branches'], + }, + 'project:custom_domain': { + category: 'infra', + name: 'Custom Domains', + description: 'Custom hostnames.', + risk: 'medium', + riskReason: 'Read-write can change custom hostnames, affecting how your project is reached.', + allowsRead: ['Read custom domain configuration'], + allowsWrite: ['Set custom hostnames'], + }, + 'project:vanity_subdomain': { + category: 'infra', + name: 'Vanity Subdomain', + description: 'Project vanity subdomain.', + risk: 'medium', + riskReason: 'Read-write can change the project vanity subdomain.', + allowsRead: ['Read vanity subdomain'], + allowsWrite: ['Manage vanity subdomain'], + }, + 'project:infra_addons': { + category: 'infra', + name: 'Add-ons', + description: 'Infrastructure add-ons.', + risk: 'medium', + riskReason: 'Read-write can enable or change paid infrastructure add-ons.', + allowsRead: ['Read infrastructure add-ons'], + allowsWrite: ['Manage infrastructure add-ons'], + }, + 'project:infra_disk_config': { + category: 'infra', + name: 'Disk Config', + description: 'Disk configuration.', + risk: 'medium', + riskReason: 'Read-write can change disk size and configuration, which may incur cost.', + allowsRead: ['Read disk configuration'], + allowsWrite: ['Manage disk configuration'], + }, + 'project:read_replicas': { + category: 'infra', + name: 'Read Replicas', + description: 'Read replica configuration.', + risk: 'medium', + riskReason: 'Read-write can provision or remove read replicas, which may incur cost.', + allowsRead: ['Read read-replica configuration'], + allowsWrite: ['Manage read replicas'], + }, +} + +const RESOURCE_METADATA_FALLBACK = ( + resourceKey: string, + title: string, + hasWrite: boolean +): ResourceMeta => ({ + category: resourceKey.startsWith('project:') ? 'project' : 'account', + // Title-cased so an uncurated resource doesn't ship a lowercase FGA-derived name + // (e.g. "project analytics configurations") next to the curated Title Case entries. + name: title + .replace(/^(Read|Manage|Create|Delete)\s+/i, '') + .replace(/\b[a-z]/g, (char) => char.toUpperCase()), + description: title, + risk: hasWrite ? 'medium' : 'low', + riskReason: hasWrite + ? 'Read-write can modify this resource.' + : 'Read-only access to this resource.', +}) + +const PERMISSION_LEVELS = ['user', 'organization', 'project'] as const + +export type PermissionLevel = (typeof PERMISSION_LEVELS)[number] + +/** + * Runtime guard for the FGA namespaces: role evaluation branches on the level, so an unrecognized + * namespace must fail loudly (at module load, caught by any test importing the catalog) rather + * than silently evaluate as project-level. + */ +const toPermissionLevel = (scope: string): PermissionLevel => { + const level = scope.toLowerCase() + const match = PERMISSION_LEVELS.find((candidate) => candidate === level) + if (match === undefined) throw new Error(`Unknown FGA namespace: ${scope}`) + return match +} + +export interface PermissionCatalogEntry { + /** Derived resource key, e.g. "project:database" */ + key: string + /** Which FGA namespace the resource lives in. Decides which role (org vs project) governs it. */ + level: PermissionLevel + category: PermissionCategoryKey + name: string + description: string + risk: RiskLevel + riskReason: string + allowsRead: string[] + allowsWrite: string[] + /** Whether a Read-write mode is offered (false => read-only resource). */ + writable: boolean + /** FGA scope ids granted at Read (and above). */ + readScopes: FgaScopeId[] + /** Additional FGA scope ids granted at Read-write (write / create / delete). */ + writeScopes: FgaScopeId[] +} + +/** Action classes an FGA permission key's suffix can map to. */ +export type FgaAction = 'read' | 'write' | 'create' | 'delete' + +/** + * Classifies an FGA permission key by its action suffix: "PROJECTS_READ" -> "read". + * Keys without a write, create, or delete suffix are treated as read. + */ +export const getAction = (key: string): FgaAction => { + if (key.endsWith('_WRITE')) return 'write' + if (key.endsWith('_CREATE')) return 'create' + if (key.endsWith('_DELETE')) return 'delete' + return 'read' +} + +const isReadScope = (key: string): boolean => getAction(key) === 'read' + +/** Strips the FGA action suffix off a permission key: "PROJECTS_READ" -> "projects". */ +export const getResource = (key: string): string => + key.replace(/_(READ|WRITE|CREATE|DELETE)$/, '').toLowerCase() + +/** + * Builds the permission catalog from the real FgaPermissions. Each unique `scope:resource` becomes + * one row; its read scope maps to Read mode and its write/create/delete scopes to Read-write mode. + */ +const buildCatalog = (): PermissionCatalogEntry[] => { + const byResource = new Map< + string, + { level: PermissionLevel; title: string; readScopes: string[]; writeScopes: string[] } + >() + + for (const [scope, scopePerms] of Object.entries(FGA)) { + const level = toPermissionLevel(scope) + for (const [permKey, perm] of Object.entries(scopePerms)) { + const resourceKey = `${level}:${getResource(permKey)}` + if (!byResource.has(resourceKey)) { + byResource.set(resourceKey, { level, title: perm.title, readScopes: [], writeScopes: [] }) + } + const entry = byResource.get(resourceKey)! + if (isReadScope(permKey)) entry.readScopes.push(perm.id) + else entry.writeScopes.push(perm.id) + } + } + + const catalog: PermissionCatalogEntry[] = [] + for (const [key, { level, title, readScopes, writeScopes }] of byResource.entries()) { + const meta = + RESOURCE_METADATA[key] ?? RESOURCE_METADATA_FALLBACK(key, title, writeScopes.length > 0) + catalog.push({ + key, + level, + category: meta.category, + name: meta.name, + description: meta.description, + risk: meta.risk, + riskReason: meta.riskReason, + allowsRead: meta.allowsRead ?? [`Read ${meta.name.toLowerCase()}`], + allowsWrite: + meta.allowsWrite ?? (writeScopes.length > 0 ? [`Modify ${meta.name.toLowerCase()}`] : []), + writable: writeScopes.length > 0, + readScopes: readScopes as FgaScopeId[], + writeScopes: writeScopes as FgaScopeId[], + }) + } + return catalog +} + +export const PERMISSION_CATALOG = buildCatalog() + +const CATALOG_BY_KEY = new Map(PERMISSION_CATALOG.map((entry) => [entry.key, entry])) + +export const getCatalogEntry = (key: string) => CATALOG_BY_KEY.get(key) + +export interface CategoryWithEntries extends PermissionCategory { + entries: PermissionCatalogEntry[] +} + +/** Catalog grouped by category, in category display order, dropping empty categories. */ +export const PERMISSION_CATALOG_BY_CATEGORY: CategoryWithEntries[] = PERMISSION_CATEGORIES.map( + (category) => ({ + ...category, + entries: PERMISSION_CATALOG.filter((entry) => entry.category === category.key), + }) +).filter((category) => category.entries.length > 0) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eb78095c2ce11..ca4cb7e898141 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2592,6 +2592,9 @@ importers: packages/shared-data: dependencies: + '@supabase/shared-types': + specifier: 0.1.91 + version: 0.1.91 zod: specifier: 'catalog:' version: 3.25.76 From b917b0e1bfc303e9f5165e3dbfacbca84cb604db Mon Sep 17 00:00:00 2001 From: Kanishk Dudeja Date: Tue, 1 Sep 2026 18:04:25 +0530 Subject: [PATCH 04/16] feat(billing): adds non-dismissable modal for indirect tax declaration (#49643) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Summary This PR adds a blocking dashboard modal for affected Australian customers to confirm their GST registration and business use of Supabase. KPMG requires us to collect this declaration from certain existing Australian customers. The backend now identifies organizations that still need to respond using `requires_indirect_tax_declaration` and stores their `yes` or `no` response in Orb customer metadata. It also supports email links with `submit_indirect_tax_declaration=true` and shows a dismissible confirmation when the organization has already responded. ### Testing #### Manual testing - Confirmed the modal appears for an affected organization without an existing response and cannot be dismissed. - Submitted both `yes` and `no` and confirmed the modal remains closed after a refresh. - Confirmed the declaration is stored without changing the customer's Tax ID. - Confirmed the modal does not appear for non admins/owners or organizations that do not require a declaration. - Confirmed the email-link parameter shows the already-submitted confirmation only for organizations that have responded, and is removed when dismissed. ## Summary by CodeRabbit * **New Features** * Added an indirect tax declaration dialog for eligible Australian organizations. * Users with billing permissions can select “Yes” or “No” and submit their declaration. * Added a dismissible confirmation for declarations submitted through a linked prompt. * The dialog requires an explicit response and provides guidance when no option is selected. * **Bug Fixes** * Declaration prompts remain visible through submission confirmation and close when dismissed. * Users without billing permissions do not see the dialog. * Success notifications no longer overlap with the confirmation dialog. --------- Co-authored-by: Julian Domke <68325451+juleswritescode@users.noreply.github.com> --- .../App/IndirectTaxDeclarationModal.tsx | 169 +++++++++++++++ .../LayoutSidebar/index.test.tsx | 1 + ...zation-customer-profile-update-mutation.ts | 25 ++- .../organization-mappers.test.ts | 2 +- apps/studio/pages/_app.tsx | 2 + apps/studio/routes/__root.tsx | 2 + .../App/IndirectTaxDeclarationModal.test.tsx | 201 ++++++++++++++++++ .../tests/lib/sql-editor-test-utils.tsx | 2 +- 8 files changed, 401 insertions(+), 3 deletions(-) create mode 100644 apps/studio/components/interfaces/App/IndirectTaxDeclarationModal.tsx create mode 100644 apps/studio/tests/components/interfaces/App/IndirectTaxDeclarationModal.test.tsx diff --git a/apps/studio/components/interfaces/App/IndirectTaxDeclarationModal.tsx b/apps/studio/components/interfaces/App/IndirectTaxDeclarationModal.tsx new file mode 100644 index 0000000000000..2f00d14e96fd5 --- /dev/null +++ b/apps/studio/components/interfaces/App/IndirectTaxDeclarationModal.tsx @@ -0,0 +1,169 @@ +import { PermissionAction } from '@supabase/shared-types/out/constants' +import { parseAsBoolean, useQueryState } from 'nuqs' +import { useEffect, useState } from 'react' +import { toast } from 'sonner' +import { + Button, + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogSection, + DialogSectionSeparator, + DialogTitle, + RadioGroupStacked, + RadioGroupStackedItem, +} from 'ui' + +import { ButtonTooltip } from '@/components/ui/ButtonTooltip' +import { useOrganizationCustomerProfileUpdateMutation } from '@/data/organizations/organization-customer-profile-update-mutation' +import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' +import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' +import { IS_PLATFORM } from '@/lib/constants' + +type IndirectTaxDeclaration = 'yes' | 'no' +type DeclarationModal = 'declaration-form' | 'submission-confirmation' | null + +export const IndirectTaxDeclarationModal = () => { + const { data: organization } = useSelectedOrganizationQuery({ enabled: IS_PLATFORM }) + + const [response, setResponse] = useState('') + const [failedSubmissionSlug, setFailedSubmissionSlug] = useState() + + const [shouldShowDeclarationConfirmation, setShouldShowDeclarationConfirmation] = useQueryState( + 'submit_indirect_tax_declaration', + parseAsBoolean.withDefault(false) + ) + + useEffect(() => { + setResponse('') + }, [organization?.slug]) + + const { can: canUpdateBillingInfo, isSuccess: permissionsLoaded } = useAsyncCheckPermissions( + PermissionAction.BILLING_WRITE, + 'stripe.customer' + ) + + const { mutate: updateCustomerProfile, isPending } = useOrganizationCustomerProfileUpdateMutation( + { + onSuccess: () => { + if (!shouldShowDeclarationConfirmation) { + toast.success('GST declaration submitted') + } + }, + onError: (_error, variables) => { + setFailedSubmissionSlug(variables.slug) + toast.error("We couldn't submit your GST declaration. Reload the page and try again.", { + duration: Infinity, + }) + }, + } + ) + + const canViewDeclaration = + IS_PLATFORM && organization !== undefined && permissionsLoaded && canUpdateBillingInfo + + let declarationModal: DeclarationModal = null + + if (canViewDeclaration) { + if (organization.requires_indirect_tax_declaration) { + if (organization.slug !== failedSubmissionSlug) { + declarationModal = 'declaration-form' + } + } else if (shouldShowDeclarationConfirmation) { + declarationModal = 'submission-confirmation' + } + } + + const onSubmit = () => { + if (organization?.slug === undefined || response === '') return + + updateCustomerProfile({ + slug: organization.slug, + indirect_tax_registration_declaration: response, + }) + } + + const closeSubmissionConfirmation = () => { + setShouldShowDeclarationConfirmation(null) + } + + return ( + <> + + event.preventDefault()} + onEscapeKeyDown={(event) => event.preventDefault()} + > + + Confirm your Australian GST status + + Confirm the following for your organization {organization?.name} + + + + + + { + if (value === 'yes' || value === 'no') setResponse(value) + }} + > + + + + + + + + Submit declaration + + + + + + { + if (!open) closeSubmissionConfirmation() + }} + > + + + GST declaration submitted + + The GST declaration for {organization?.name} has been submitted. No further action is + required. + + + + + + + + + ) +} diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutSidebar/index.test.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutSidebar/index.test.tsx index 05cd28f6e204e..18937deb5b410 100644 --- a/apps/studio/components/layouts/ProjectLayout/LayoutSidebar/index.test.tsx +++ b/apps/studio/components/layouts/ProjectLayout/LayoutSidebar/index.test.tsx @@ -75,6 +75,7 @@ vi.mock('@/hooks/misc/useSelectedOrganization', () => ({ restriction_status: null, restriction_data: null, organization_missing_address: false, + requires_indirect_tax_declaration: false, }, }), })) diff --git a/apps/studio/data/organizations/organization-customer-profile-update-mutation.ts b/apps/studio/data/organizations/organization-customer-profile-update-mutation.ts index d5344c308b337..af9e63db9d445 100644 --- a/apps/studio/data/organizations/organization-customer-profile-update-mutation.ts +++ b/apps/studio/data/organizations/organization-customer-profile-update-mutation.ts @@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' import { organizationKeys } from './keys' +import type { OrganizationsData } from './organizations-query' import type { CustomerAddress, CustomerTaxId } from './types' import { handleError, put } from '@/data/fetchers' import type { ResponseError, UseCustomMutationOptions } from '@/types' @@ -14,6 +15,7 @@ export type OrganizationCustomerProfileUpdateVariables = { tax_id?: CustomerTaxId | null email?: string additional_emails?: string[] + indirect_tax_registration_declaration?: 'yes' | 'no' /** When true, validates the request without persisting changes */ dry_run?: boolean } @@ -25,6 +27,7 @@ export async function updateOrganizationCustomerProfile({ tax_id, email, additional_emails, + indirect_tax_registration_declaration, dry_run, }: OrganizationCustomerProfileUpdateVariables) { if (!slug) return console.error('Slug is required') @@ -45,6 +48,7 @@ export async function updateOrganizationCustomerProfile({ : {}), email, additional_emails, + indirect_tax_registration_declaration, ...(dry_run ? { dry_run } : {}), }, }) @@ -77,7 +81,16 @@ export const useOrganizationCustomerProfileUpdateMutation = ({ >({ mutationFn: (vars) => updateOrganizationCustomerProfile(vars), async onSuccess(data, variables, context) { - const { address, slug, billing_name, tax_id, email, additional_emails, dry_run } = variables + const { + address, + slug, + billing_name, + tax_id, + email, + additional_emails, + indirect_tax_registration_declaration, + dry_run, + } = variables if (dry_run) { await onSuccess?.(data, variables, context) @@ -106,6 +119,16 @@ export const useOrganizationCustomerProfileUpdateMutation = ({ queryClient.setQueryData(organizationKeys.taxId(slug), tax_id) } + if (indirect_tax_registration_declaration !== undefined) { + queryClient.setQueryData(organizationKeys.list(), (previous) => + previous?.map((organization) => + organization.slug === slug + ? { ...organization, requires_indirect_tax_declaration: false } + : organization + ) + ) + } + // Refetch after a delay to pick up server-canonical values (e.g. normalized tax IDs). // The GET endpoint can be stale for 1-2 seconds after an update. setTimeout(() => { diff --git a/apps/studio/data/organizations/organization-mappers.test.ts b/apps/studio/data/organizations/organization-mappers.test.ts index 6b334622c6897..41aaf38be1556 100644 --- a/apps/studio/data/organizations/organization-mappers.test.ts +++ b/apps/studio/data/organizations/organization-mappers.test.ts @@ -20,8 +20,8 @@ const baseOrganizationResponse = { organization_missing_address: false, organization_missing_tax_id: false, organization_requires_mfa: false, - plan: { id: 'pro' as const, name: 'Pro' }, requires_indirect_tax_declaration: false, + plan: { id: 'pro' as const, name: 'Pro' }, restriction_data: null, restriction_status: null, slug: 'toolshed', diff --git a/apps/studio/pages/_app.tsx b/apps/studio/pages/_app.tsx index f7b7474a30749..618085cdba4e0 100644 --- a/apps/studio/pages/_app.tsx +++ b/apps/studio/pages/_app.tsx @@ -42,6 +42,7 @@ import { StudioCommandMenu } from '@/components/interfaces/App/CommandMenu' import { StudioCommandProvider as CommandProvider } from '@/components/interfaces/App/CommandMenu/StudioCommandProvider' import { FeaturePreviewContextProvider } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { FeaturePreviewModal } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewModal' +import { IndirectTaxDeclarationModal } from '@/components/interfaces/App/IndirectTaxDeclarationModal' import { MonacoThemeProvider } from '@/components/interfaces/App/MonacoThemeProvider' import { RouteValidationWrapper } from '@/components/interfaces/App/RouteValidationWrapper' import { MainScrollContainerProvider } from '@/components/layouts/MainScrollContainerContext' @@ -218,6 +219,7 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) { + diff --git a/apps/studio/routes/__root.tsx b/apps/studio/routes/__root.tsx index 9c2e251ef9e81..3609593df9d9d 100644 --- a/apps/studio/routes/__root.tsx +++ b/apps/studio/routes/__root.tsx @@ -64,6 +64,7 @@ import { StudioCommandMenu } from '@/components/interfaces/App/CommandMenu' import { StudioCommandProvider as CommandProvider } from '@/components/interfaces/App/CommandMenu/StudioCommandProvider' import { FeaturePreviewContextProvider } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { FeaturePreviewModal } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewModal' +import { IndirectTaxDeclarationModal } from '@/components/interfaces/App/IndirectTaxDeclarationModal' import { MonacoThemeProvider } from '@/components/interfaces/App/MonacoThemeProvider' import { RouteValidationWrapper } from '@/components/interfaces/App/RouteValidationWrapper' import { ShellFallback } from '@/components/interfaces/App/ShellFallback' @@ -394,6 +395,7 @@ function RootComponent() { + diff --git a/apps/studio/tests/components/interfaces/App/IndirectTaxDeclarationModal.test.tsx b/apps/studio/tests/components/interfaces/App/IndirectTaxDeclarationModal.test.tsx new file mode 100644 index 0000000000000..1c1b6d6f59c2d --- /dev/null +++ b/apps/studio/tests/components/interfaces/App/IndirectTaxDeclarationModal.test.tsx @@ -0,0 +1,201 @@ +import { PermissionAction } from '@supabase/shared-types/out/constants' +import { screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { HttpResponse } from 'msw' +import { toast } from 'sonner' +import { describe, expect, test, vi } from 'vitest' + +import { IndirectTaxDeclarationModal } from '@/components/interfaces/App/IndirectTaxDeclarationModal' +import { createMockOrganizationResponse } from '@/tests/helpers' +import { customRender } from '@/tests/lib/custom-render' +import { addAPIMock } from '@/tests/lib/msw' +import { createMockProfileContext } from '@/tests/lib/profile-helpers' + +const ORG_SLUG = 'test-org' +const PROFILE_CONTEXT = createMockProfileContext() + +vi.mock('common', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + useParams: () => ({ slug: ORG_SLUG }), + useIsLoggedIn: () => true, + } +}) + +vi.mock('@/lib/constants', async (importOriginal) => { + const actual = await importOriginal>() + return { ...actual, IS_PLATFORM: true } +}) + +vi.mock('sonner', () => ({ + toast: { + error: vi.fn(), + success: vi.fn(), + }, +})) + +const billingWritePermission = { + actions: [PermissionAction.BILLING_WRITE], + condition: null, + organization_id: 1, + organization_slug: ORG_SLUG, + project_ids: [], + project_refs: [], + resources: ['stripe.customer'], + restrictive: false, +} + +function setupMocks({ + canUpdateBilling = true, + requiresDeclaration = true, +}: { + canUpdateBilling?: boolean + requiresDeclaration?: boolean +} = {}) { + addAPIMock({ + method: 'get', + path: '/platform/organizations', + response: [ + createMockOrganizationResponse({ + slug: ORG_SLUG, + name: 'Test Org', + requires_indirect_tax_declaration: requiresDeclaration, + }), + ], + }) + + addAPIMock({ + method: 'get', + path: '/platform/profile/permissions', + response: canUpdateBilling ? [billingWritePermission] : [], + }) +} + +describe('IndirectTaxDeclarationModal', () => { + test('is non-dismissible and requires an explicit response', async () => { + setupMocks() + customRender(, { profileContext: PROFILE_CONTEXT }) + + const dialog = await screen.findByRole('dialog') + expect(dialog).toHaveTextContent('Confirm your Australian GST status') + expect(screen.queryByRole('button', { name: /close/i })).not.toBeInTheDocument() + + await userEvent.keyboard('{Escape}') + expect(screen.getByRole('dialog')).toBeInTheDocument() + + const submitButton = screen.getByRole('button', { name: 'Submit declaration' }) + expect(submitButton).toBeDisabled() + + await userEvent.hover(submitButton) + expect(await screen.findByRole('tooltip')).toHaveTextContent('Select Yes or No to continue') + }) + + test('submits the declaration', async () => { + setupMocks() + const requestBodies: unknown[] = [] + + addAPIMock({ + method: 'put', + path: '/platform/organizations/:slug/customer', + response: async ({ request }) => { + requestBodies.push(await request.json()) + return new HttpResponse(null, { status: 204 }) + }, + }) + + customRender(, { profileContext: PROFILE_CONTEXT }) + + await userEvent.click(await screen.findByRole('radio', { name: /Yes, I confirm/i })) + await userEvent.click(screen.getByRole('button', { name: 'Submit declaration' })) + + await waitFor(() => { + expect(requestBodies).toEqual([{ indirect_tax_registration_declaration: 'yes' }]) + }) + await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument()) + }) + + test('transitions to confirmation after submitting from the declaration link', async () => { + setupMocks() + + addAPIMock({ + method: 'put', + path: '/platform/organizations/:slug/customer', + response: () => new HttpResponse(null, { status: 204 }), + }) + + customRender(, { + profileContext: PROFILE_CONTEXT, + nuqs: { searchParams: { submit_indirect_tax_declaration: 'true' } }, + }) + + await userEvent.click(await screen.findByRole('radio', { name: /Yes, I confirm/i })) + await userEvent.click(screen.getByRole('button', { name: 'Submit declaration' })) + + const dialog = await screen.findByRole('dialog') + expect(dialog).toHaveTextContent('GST declaration submitted') + expect(dialog).toHaveTextContent( + 'The GST declaration for Test Org has been submitted. No further action is required.' + ) + }) + + test('closes after a failed submission without showing the submitted confirmation', async () => { + setupMocks() + + addAPIMock({ + method: 'put', + path: '/platform/organizations/:slug/customer', + response: () => HttpResponse.json({ message: 'Orb update failed' }, { status: 500 }), + }) + + customRender(, { + profileContext: PROFILE_CONTEXT, + nuqs: { searchParams: { submit_indirect_tax_declaration: 'true' } }, + }) + + await userEvent.click(await screen.findByRole('radio', { name: /Yes, I confirm/i })) + await userEvent.click(screen.getByRole('button', { name: 'Submit declaration' })) + + await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument()) + expect(screen.queryByText('GST declaration submitted')).not.toBeInTheDocument() + expect(toast.error).toHaveBeenCalledWith( + "We couldn't submit your GST declaration. Reload the page and try again.", + { duration: Infinity } + ) + }) + + test('does not show for members without billing write permission', async () => { + setupMocks({ canUpdateBilling: false }) + customRender(, { profileContext: PROFILE_CONTEXT }) + + await waitFor(() => { + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + }) + }) + + test('shows a dismissible confirmation when linked after a previous submission', async () => { + setupMocks({ requiresDeclaration: false }) + customRender(, { + profileContext: PROFILE_CONTEXT, + nuqs: { searchParams: { submit_indirect_tax_declaration: 'true' } }, + }) + + const dialog = await screen.findByRole('dialog') + expect(dialog).toHaveTextContent('GST declaration submitted') + expect(dialog).toHaveTextContent( + 'The GST declaration for Test Org has been submitted. No further action is required.' + ) + + await userEvent.click(screen.getAllByRole('button', { name: 'Close' })[0]) + await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument()) + }) + + test('does not show the submitted confirmation without the URL parameter', async () => { + setupMocks({ requiresDeclaration: false }) + customRender(, { profileContext: PROFILE_CONTEXT }) + + await waitFor(() => { + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + }) + }) +}) diff --git a/apps/studio/tests/lib/sql-editor-test-utils.tsx b/apps/studio/tests/lib/sql-editor-test-utils.tsx index 22d0180eebc6f..e332c1977d7a6 100644 --- a/apps/studio/tests/lib/sql-editor-test-utils.tsx +++ b/apps/studio/tests/lib/sql-editor-test-utils.tsx @@ -227,8 +227,8 @@ export function setupSqlEditorMocks({ organization_missing_address: false, organization_missing_tax_id: false, organization_requires_mfa: false, - plan: { id: 'free', name: 'Free' }, requires_indirect_tax_declaration: false, + plan: { id: 'free', name: 'Free' }, restriction_data: null, restriction_status: null, stripe_customer_id: null, From 0c8dc73bf403885cfd81f6f4a2ec7effa1a6f428 Mon Sep 17 00:00:00 2001 From: Utkarash Kumar Singh Date: Tue, 1 Sep 2026 13:42:06 +0100 Subject: [PATCH 05/16] feat(upgrade): surface btree_gist NaN reindex warning (#49684) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Feature — studio counterpart of supabase/platform#37696 (PG 15.19/17.11 release, PSQL-1247). ## What is the current behavior? The pre-upgrade warnings panel handles `ltree_reindex_required` and `operator_estimator_gate` only. ## What is the new behavior? Adds the `btree_gist_nan_reindex` warning (title, description, docs link) emitted by the eligibility endpoint when a project has btree_gist indexes on float columns and the upgrade crosses the 15.19/17.11 NaN-handling fix. Non-blocking, same pattern as #47003. `api-v1.d.ts` union extended to match the platform spec. ## Additional context Docs anchor targets the section added in #49621 — merge that first (or together). Refs PSQL-1247. ## Summary by CodeRabbit * **New Features** * Added an upgrade warning for the `btree_gist_nan_reindex` requirement. * Included a description and link to relevant documentation for guidance. --- .../interfaces/Settings/Infrastructure/UpgradeWarnings.tsx | 6 ++++++ packages/api-types/types/api-v1.d.ts | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/UpgradeWarnings.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/UpgradeWarnings.tsx index a5204a403db44..7ebf1c69721dc 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/UpgradeWarnings.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/UpgradeWarnings.tsx @@ -164,6 +164,8 @@ const getWarningTitle = (warning: ProjectUpgradeEligibilityWarning): string => { return 'ltree indexes must be reindexed after this upgrade' case 'operator_estimator_gate': return 'Custom operators may need attention after this upgrade' + case 'btree_gist_nan_reindex': + return 'btree_gist indexes on float columns must be reindexed after this upgrade' } } @@ -175,6 +177,8 @@ const getWarningDescription = (warning: ProjectUpgradeEligibilityWarning): strin return 'After upgrading, ltree indexes on this database can return incomplete results until they are rebuilt. Run `REINDEX INDEX CONCURRENTLY` on the affected indexes — this runs online with no downtime.' case 'operator_estimator_gate': return 'After upgrading, recreating an operator that references a non-built-in selectivity estimator (for example during a restore or branch) requires superuser and may fail. Most projects are not affected.' + case 'btree_gist_nan_reindex': + return 'After upgrading, btree_gist indexes on float columns can return wrong results for rows containing NaN until they are rebuilt. If those columns may contain NaN values, run `REINDEX INDEX CONCURRENTLY` on the affected indexes — this runs online with no downtime.' } } @@ -186,6 +190,8 @@ const getWarningLink = (warning: ProjectUpgradeEligibilityWarning): string => { return `${DOCS_URL}/guides/platform/upgrading#ltree-indexes-require-reindexing-after-upgrade` case 'operator_estimator_gate': return `${DOCS_URL}/guides/platform/upgrading#custom-operator-selectivity-estimators` + case 'btree_gist_nan_reindex': + return `${DOCS_URL}/guides/platform/upgrading#btree_gist-indexes-on-float-columns-require-reindexing-after-upgrade` } } diff --git a/packages/api-types/types/api-v1.d.ts b/packages/api-types/types/api-v1.d.ts index b7d26dcb49d16..1a8ef0636fbda 100644 --- a/packages/api-types/types/api-v1.d.ts +++ b/packages/api-types/types/api-v1.d.ts @@ -4045,6 +4045,10 @@ export interface components { /** @enum {string} */ type: 'operator_estimator_gate' } + | { + /** @enum {string} */ + type: 'btree_gist_nan_reindex' + } )[] } ProjectUpgradeInitiateResponse: { From 3dddb60149b2671b34da45d8150e8ac48502af66 Mon Sep 17 00:00:00 2001 From: Pamela Chia Date: Tue, 1 Sep 2026 21:07:46 +0800 Subject: [PATCH 06/16] feat(www): publish agent discovery catalog and complete json-ld (#49768) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I added the agent-discovery surfaces the www app was missing: a resource catalog at `/.well-known/ard.json` plus completed structured data on the homepage. I scoped this from the agent-readiness gaps that are truthfully closable on the www side; the catalog lists only resources that already exist and serve 200 (MCP OAuth metadata, Management API OpenAPI spec, llms.txt, agent-skills index). **Changed:** - **Agents can discover our machine-readable resources from one document**: new static catalog at `/.well-known/ard.json` (Agentic Resource Discovery format); the legacy `/.well-known/ai-catalog.json` path serves the same file via rewrite, keeping a single source artifact. - **Organization JSON-LD carries verifiable company details**: adds `legalName`, a support `contactPoint`, and the registered address already public on our Terms of Service. - **Homepage declares the product as an application entity**: emits `SoftwareApplication` JSON-LD via the existing `softwareApplicationSchema` builder, same pattern as the vector module page. ## To test Tested on Vercel preview: - [ ] `curl /.well-known/ard.json`: expect 200 with a JSON catalog of 5 entries - [ ] `curl /.well-known/ai-catalog.json`: expect the same document with status 200 (rewrite, not a redirect) - [ ] View homepage page source: expect three `application/ld+json` scripts: Organization now includes `address` and `contactPoint`, and a `SoftwareApplication` block is present ## Linear - fixes GROWTH-1164 ## Summary by CodeRabbit - **New Features** - Added an Agent Resource Description catalog listing Supabase’s MCP, API, documentation, and agent skill resources. - Added support for the legacy AI Catalog URL through a canonical redirect. - Enhanced website structured data with software application details, legal information, support contact details, and business address. - **Tests** - Added validation ensuring discoverable `.well-known` resources are cataloged and resolve correctly. - **Chores** - Updated marketing site test coverage for `.well-known` resource changes. --- .github/workflows/www-tests.yml | 1 + apps/www/app/(home)/page.tsx | 21 +++++++- apps/www/ard-catalog.test.ts | 80 ++++++++++++++++++++++++++++ apps/www/lib/json-ld.ts | 13 +++++ apps/www/lib/rewrites.js | 5 ++ apps/www/public/.well-known/ard.json | 72 +++++++++++++++++++++++++ 6 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 apps/www/ard-catalog.test.ts create mode 100644 apps/www/public/.well-known/ard.json diff --git a/.github/workflows/www-tests.yml b/.github/workflows/www-tests.yml index a29e1b4765cc3..96f2737cccf43 100644 --- a/.github/workflows/www-tests.yml +++ b/.github/workflows/www-tests.yml @@ -10,6 +10,7 @@ on: - 'apps/www/lib/**/*.js' - 'apps/www/content/md/**' - 'apps/www/scripts/**/*.mjs' + - 'apps/www/public/.well-known/**' # Cancel old builds on new commit for same workflow + branch/PR concurrency: diff --git a/apps/www/app/(home)/page.tsx b/apps/www/app/(home)/page.tsx index 8aa34bd438c2f..d9c2094bff7b7 100644 --- a/apps/www/app/(home)/page.tsx +++ b/apps/www/app/(home)/page.tsx @@ -2,7 +2,13 @@ import type { Metadata } from 'next' import { FrameworksSection } from './_components/FrameworksSection' import { HomeContent } from './_components/HomeContent' -import { organizationSchema, serializeJsonLd, websiteSchema } from '@/lib/json-ld' +import { DEFAULT_META_DESCRIPTION } from '@/lib/constants' +import { + organizationSchema, + serializeJsonLd, + softwareApplicationSchema, + websiteSchema, +} from '@/lib/json-ld' import { mdAlternates } from '@/lib/md-alternates' export const metadata: Metadata = { @@ -23,6 +29,19 @@ export default function HomePage() { type="application/ld+json" dangerouslySetInnerHTML={{ __html: serializeJsonLd(websiteSchema()) }} /> +