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/llm-agent-parity.md b/.agents/skills/ask-the-docs/reference/llm-agent-parity.md index 207e2dc9f00aa..6c66a4caf3d4e 100644 --- a/.agents/skills/ask-the-docs/reference/llm-agent-parity.md +++ b/.agents/skills/ask-the-docs/reference/llm-agent-parity.md @@ -80,10 +80,6 @@ runtime (`AiSkills.utils.ts`). See [`federated-docs.md`](./federated-docs.md). ## In-flux / stale wiring -- `apps/docs/.gitignore` lists `public/llms/` as generated by - `build:llms`, but **`build:llms` is not in `package.json`** today. - Per-source `llms/*.txt` links in `/llms.txt` may point at assets whose - generation path is unclear — verify before depending on them. - `llms-full.txt` fetches reference markdown from `${NEXT_PUBLIC_DOCS_URL}/markdown/reference/.md` at runtime. 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/.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/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/registry/default/example/form-patterns-pagelayout.tsx b/apps/design-system/registry/default/example/form-patterns-pagelayout.tsx index 42deb0f33d0e4..59ba2b740fd6d 100644 --- a/apps/design-system/registry/default/example/form-patterns-pagelayout.tsx +++ b/apps/design-system/registry/default/example/form-patterns-pagelayout.tsx @@ -580,18 +580,18 @@ export default function FormPatternsPageLayout() { label="Select (Dropdown)" description="Single selection from a list of options" > - - + - - US East (N. Virginia) - US West (Oregon) - EU West (Ireland) - - - + + + US East (N. Virginia) + US West (Oregon) + EU West (Ireland) + + )} /> diff --git a/apps/design-system/registry/default/example/form-patterns-sidepanel.tsx b/apps/design-system/registry/default/example/form-patterns-sidepanel.tsx index 1b3a146e357f8..c3e734a6d7e29 100644 --- a/apps/design-system/registry/default/example/form-patterns-sidepanel.tsx +++ b/apps/design-system/registry/default/example/form-patterns-sidepanel.tsx @@ -573,18 +573,18 @@ export default function FormPatternsSidePanel() { label="Select (Dropdown)" description="Single selection from a list of options" > - - + - - US East (N. Virginia) - US West (Oregon) - EU West (Ireland) - - - + + + US East (N. Virginia) + US West (Oregon) + EU West (Ireland) + + )} /> 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/.gitignore b/apps/docs/.gitignore index abe7aa2494f0e..3a32bfc38706b 100644 --- a/apps/docs/.gitignore +++ b/apps/docs/.gitignore @@ -27,8 +27,6 @@ yarn-error.log* # Sitemap public/sitemap.xml -# Per-source llms files (generated by build:llms, served by www) -public/llms/ # Generated guide and reference markdown files. manifest.json is committed # with a placeholder empty array so middleware.ts's import always resolves, # even when build:markdown hasn't run (e.g. in local dev, which skips it). 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/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/local-development/declarative-database-schemas.mdx b/apps/docs/content/guides/local-development/declarative-database-schemas.mdx index 513d129fd515e..404648f57b0d3 100644 --- a/apps/docs/content/guides/local-development/declarative-database-schemas.mdx +++ b/apps/docs/content/guides/local-development/declarative-database-schemas.mdx @@ -28,8 +28,6 @@ Schema migrations are SQL statements written in Data Definition Language. They a -<$CodeTabs> - ```sql name=supabase/schemas/employees.sql create table "employees" ( "id" integer not null, @@ -37,8 +35,6 @@ create table "employees" ( ); ``` - - @@ -53,14 +49,10 @@ create table "employees" ( -<$CodeTabs> - ```bash name=Terminal supabase db diff -f create_employees_table ``` - - @@ -75,15 +67,11 @@ supabase db diff -f create_employees_table -<$CodeTabs> - ```bash name=Terminal supabase start supabase migration up ``` - - @@ -106,8 +94,6 @@ With declarative schemas, the files in `supabase/schemas/` are the source of tru -<$CodeTabs> - ```sql name=supabase/schemas/employees.sql create table "employees" ( "id" integer not null, @@ -116,8 +102,6 @@ create table "employees" ( ); ``` - - @@ -138,14 +122,10 @@ Some entities like views and enums expect columns to be declared in a specific o -<$CodeTabs> - ```bash name=Terminal supabase db diff -f add_age ``` - - @@ -160,14 +140,10 @@ supabase db diff -f add_age -<$CodeTabs> - ```sql name=supabase/migrations/_add_age.sql alter table "public"."employees" add column "age" smallint not null; ``` - - @@ -181,14 +157,10 @@ alter table "public"."employees" add column "age" smallint not null; -<$CodeTabs> - ```bash name=Terminal supabase migration up ``` - - @@ -205,14 +177,10 @@ supabase migration up -<$CodeTabs> - ```bash name=Terminal supabase login ``` - - @@ -224,14 +192,10 @@ supabase login -<$CodeTabs> - ```bash name=Terminal supabase link ``` - - @@ -243,14 +207,10 @@ supabase link -<$CodeTabs> - ```bash name=Terminal supabase db push ``` - - @@ -260,8 +220,6 @@ supabase db push As your database schema evolves, you will probably start using more advanced entities like views and functions. These entities are notoriously verbose to manage using plain migrations because the entire body must be recreated whenever there is a change. Using declarative schema, you can now edit them in-place so it’s much easier to review. -<$CodeTabs> - ```sql name=supabase/schemas/employees.sql create table "employees" ( "id" integer not null, @@ -281,8 +239,6 @@ AS $$ $$; ``` - - Your schema files are run in lexicographic order by default. The order is important when you have foreign keys between multiple tables as the parent table must be created first. For example, your `supabase` directory may end up with the following structure. ```bash @@ -299,8 +255,6 @@ Your schema files are run in lexicographic order by default. The order is import For small projects with only a few tables, the default schema order may be sufficient. However, as your project grows, you might need more control over the order in which schemas are applied. To specify a custom order for applying the schemas, you can declare them explicitly in `config.toml`. Any glob patterns will evaluated, deduplicated, and sorted in lexicographic order. For example, the following pattern ensures `employees.sql` is always executed first. -<$CodeTabs> - ```toml name=supabase/config.toml [db.migrations] schema_paths = [ @@ -309,34 +263,24 @@ schema_paths = [ ] ``` - - ### Pulling in your production schema To set up declarative schemas on a existing project, you can pull in your production schema by running: -<$CodeTabs> - ```bash name=Terminal supabase db dump > supabase/schemas/prod.sql ``` - - From there, you can start breaking down your schema into smaller files and generate migrations. You can do this all at once, or incrementally as you make changes to your schema. ### Rolling back a schema change During development, you may want to rollback a migration to keep your new schema changes in a single migration file. This can be done by resetting your local database to a previous version. -<$CodeTabs> - ```bash name=Terminal supabase db reset --version 20241005112233 ``` - - After a reset, you can [edit the schema](#updating-your-schema) and regenerate a new migration file. Note that you should not reset a version that's already deployed to production. If you need to rollback a migration that's already deployed, you should first revert changes to the schema files. Then you can generate a new migration file containing the down migration. This ensures your production migrations are always rolling forward. 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/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/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/reference/README.md b/apps/docs/spec/reference/README.md index 9602a13174e4e..f5228a8824b0f 100644 --- a/apps/docs/spec/reference/README.md +++ b/apps/docs/spec/reference/README.md @@ -426,7 +426,7 @@ No other call sites need to change. > ⚠️ Don't move the constant. `Reference.utils.ts` transitively pulls in > `next/navigation`, which crashes `tsx --conditions=react-server` (used by -> `pnpm build:llms`). The constant lives in its own no-dep file +> `pnpm embeddings`). The constant lives in its own no-dep file > (`Reference.constants.ts`) so server-only scripts can import it without > dragging the Next runtime in. 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/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/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/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/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/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/Account/AccessTokens/Classic/NewTokenDialog.tsx b/apps/studio/components/interfaces/Account/AccessTokens/Classic/NewTokenDialog.tsx index 350802b852c8e..2a5568fdf5a97 100644 --- a/apps/studio/components/interfaces/Account/AccessTokens/Classic/NewTokenDialog.tsx +++ b/apps/studio/components/interfaces/Account/AccessTokens/Classic/NewTokenDialog.tsx @@ -183,13 +183,9 @@ export const NewTokenDialog = ({ name="tokenName" control={form.control} render={({ field }) => ( - + - + )} @@ -199,24 +195,24 @@ export const NewTokenDialog = ({ name="expiresAt" control={form.control} render={({ field }) => ( - +
- - + - - {Object.values(EXPIRES_AT_OPTIONS).map( - (option: { value: string; label: string }) => ( - - {option.label} - - ) - )} - - - + + + {Object.values(EXPIRES_AT_OPTIONS).map( + (option: { value: string; label: string }) => ( + + {option.label} + + ) + )} + + {isCustomExpiry && ( 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/Form/TokenDetails.tsx b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/TokenDetails.tsx index a57127f6b65be..7ef6accc758c5 100644 --- a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/TokenDetails.tsx +++ b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/TokenDetails.tsx @@ -59,7 +59,7 @@ export const TokenDetails = ({ control, setValue }: TokenDetailsProps) => { name="tokenName" control={control} render={({ field }) => ( - + 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/apps/studio/components/interfaces/Account/TOTPFactors/AddNewFactorModal.tsx b/apps/studio/components/interfaces/Account/TOTPFactors/AddNewFactorModal.tsx index 8b6ce19fb05ab..5f3accd726dc0 100644 --- a/apps/studio/components/interfaces/Account/TOTPFactors/AddNewFactorModal.tsx +++ b/apps/studio/components/interfaces/Account/TOTPFactors/AddNewFactorModal.tsx @@ -245,15 +245,9 @@ const SecondStep = ({ name="code" control={form.control} render={({ field }) => ( - + - + )} 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/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/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/EnumeratedTypes/EnumeratedTypeValueRow.tsx b/apps/studio/components/interfaces/Database/EnumeratedTypes/EnumeratedTypeValueRow.tsx index 884ca841a2da3..ab4f843df769a 100644 --- a/apps/studio/components/interfaces/Database/EnumeratedTypes/EnumeratedTypeValueRow.tsx +++ b/apps/studio/components/interfaces/Database/EnumeratedTypes/EnumeratedTypeValueRow.tsx @@ -2,7 +2,18 @@ import { useSortable } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import { GripVertical, Trash } from 'lucide-react' import type { Control, FieldPath, FieldValues } from 'react-hook-form' -import { Button, FormControl, FormField, FormItem, FormLabel, FormMessage, Input } from 'ui' +import { + Button, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + Input, + Tooltip, + TooltipContent, + TooltipTrigger, +} from 'ui' interface EnumeratedTypeValueRowProps< TFieldValues extends FieldValues = FieldValues, @@ -58,14 +69,22 @@ const EnumeratedTypeValueRow = ({ -
diff --git a/apps/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx b/apps/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx index 74458383ac0e5..69a981d0bc722 100644 --- a/apps/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx +++ b/apps/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx @@ -239,7 +239,7 @@ export const EnableExtensionModal = ({ name="name" control={form.control} render={({ field }) => ( - + diff --git a/apps/studio/components/interfaces/Database/Functions/CreateFunction/FunctionEditor.tsx b/apps/studio/components/interfaces/Database/Functions/CreateFunction/FunctionEditor.tsx index 5543dc30f6ac5..efefb13a2b7fa 100644 --- a/apps/studio/components/interfaces/Database/Functions/CreateFunction/FunctionEditor.tsx +++ b/apps/studio/components/interfaces/Database/Functions/CreateFunction/FunctionEditor.tsx @@ -44,6 +44,9 @@ export const FunctionEditor = ({ )} onClick={() => setFocused(!focused)} icon={focused ? : } + aria-label={focused ? 'Minimize editor' : 'Maximize editor'} + // Tooltip repeats the label; the description would read the name twice + aria-describedby={undefined} /> diff --git a/apps/studio/components/interfaces/Database/Functions/CreateFunction/index.tsx b/apps/studio/components/interfaces/Database/Functions/CreateFunction/index.tsx index ba9e7d8b7fd0f..26b7cd98c545c 100644 --- a/apps/studio/components/interfaces/Database/Functions/CreateFunction/index.tsx +++ b/apps/studio/components/interfaces/Database/Functions/CreateFunction/index.tsx @@ -30,6 +30,9 @@ import { SheetFooter, SheetSection, Switch, + Tooltip, + TooltipContent, + TooltipTrigger, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import z from 'zod' @@ -536,12 +539,20 @@ const FormFieldArgs = ({ readonly }: FormFieldConfigParamsProps) => { /> {!readonly && ( -