diff --git a/apps/docs/content/guides/deployment/branching.mdx b/apps/docs/content/guides/deployment/branching.mdx index 23342dd4c89..d4f3ed3ca11 100644 --- a/apps/docs/content/guides/deployment/branching.mdx +++ b/apps/docs/content/guides/deployment/branching.mdx @@ -15,7 +15,8 @@ Supabase branches create separate environments that spin off from your main proj - **Preview Branches**: Preview branches are ephemeral and best suited for focused testing. They are automatically deleted when a PR is merged or closed. - **Persistent Branches**: Persistent branches are long-lived and recommended for environments like staging, QA, or development. They aren't automatically paused or deleted due to inactivity or when a PR is merged or closed. - **Managing Branches**: You can create, review, and merge branches either automatically via our [GitHub integration](/docs/guides/deployment/branching/github-integration) or directly [through the dashboard](/docs/guides/deployment/branching/dashboard) (currently in beta). All branches show up in the branches page in the dashboard, regardless of how they were created. -- **Data-less**: New branches do not start with any data from your main project. This is meant to better protect your sensitive production data. To start your branches with data, you can use a [seed file](/docs/guides/deployment/branching/github-integration#seeding) if using the GitHub integration. +- **Cloned from your main project**: Each new branch is created as a clone of your main project. It starts with that project's Edge Functions deployed and the configuration set. +- **Data-less by default**: By default, new branches do not start with any data or storage objects from your main project. This is meant to better protect your sensitive production data. To start your branches with data, you can use a [seed file](/docs/guides/deployment/branching/github-integration#seeding) if using the GitHub integration, or the [Include data](/docs/guides/deployment/branching/dashboard#include-production-data) option if you create the branch from the dashboard. ## Deploying to production diff --git a/apps/docs/content/guides/deployment/branching/dashboard.mdx b/apps/docs/content/guides/deployment/branching/dashboard.mdx index ad1ae7021c8..e10efb1c2ce 100644 --- a/apps/docs/content/guides/deployment/branching/dashboard.mdx +++ b/apps/docs/content/guides/deployment/branching/dashboard.mdx @@ -41,6 +41,24 @@ Once you've enabled the feature, you can create a new branch: 1. Click the arrows next to the branch name in the top menu bar. (The top menu bar has the format `YOUR_ORGANIZATION / YOUR_PROJECT / CURRENT_BRANCH_NAME`.) 2. Click `Create branch`. +The new branch is a clone of your base project. It starts with the project's schema, Edge Functions, and the configuration set. + +### Include production data + +By default, a branch starts without any of your production data or storage objects. If your project has the Point-in-Time Recovery add-on, you can turn on **Include data** when you create the branch to copy your production data into it. + + + +A branch created with **Include data** holds a copy of your production data, treat it with the same care as production. + + + + + +A branch uses a larger disk and matches the compute size of your project, which increases its cost. + + + ## Making changes to a branch Use the branch selector in the top bar to change to your branch. Any changes you make (including SQL run in the SQL editor, table editor changes, and configuration changes) are now made against the currently selected branch. diff --git a/apps/docs/content/guides/deployment/branching/github-integration.mdx b/apps/docs/content/guides/deployment/branching/github-integration.mdx index c04cb14f4fe..cb81e3bff72 100644 --- a/apps/docs/content/guides/deployment/branching/github-integration.mdx +++ b/apps/docs/content/guides/deployment/branching/github-integration.mdx @@ -83,19 +83,25 @@ Enable the **Automatic branching** option in your GitHub Integration configurati When a new branch is created in GitHub, a corresponding branch is created in Supabase. (You can enable the **Supabase changes only** option to only create Supabase branches when Supabase files change.) +Every Supabase branch, preview or persistent, is created as a clone of your base project. The new branch starts with the Edge Functions and configuration of that project. Its database schema is not cloned. Instead, it is built from the migrations you commit to your repository. + ### Configuration You can test configuration changes on your Preview Branch by configuring the `config.toml` file in your Supabase directory. See the [Configuration docs](/docs/guides/deployment/branching/configuration) for more information. +Your branch starts with the configuration of your base project. The settings in your `config.toml` file are applied on top of that clone. + A comment is added to your PR with the deployment status of your preview branch. ### Migrations -The migrations in the `migrations` subdirectory of your Supabase directory are automatically run. +The migrations in the `migrations` subdirectory of your Supabase directory are automatically run when the branch is created. Each later commit runs only the migrations that haven't been applied yet. + +If you want to rerun existing migrations, reset the branch from the Supabase dashboard to start from scratch. Note that existing data on your branch will also be dropped by a reset. ### Seeding -No production data is copied to your Preview branch. This is meant to protect your sensitive production data. +Cloning your base project copies its Edge Functions and configuration, but not its data or storage objects. This is meant to protect your sensitive production data. Your branch starts with the tables your migrations create, and the only rows in them are the ones your seed files add. You can seed your Preview Branch with sample data using the `seed.sql` file in your Supabase directory. See the [Seeding docs](/docs/guides/local-development/seeding-your-database) for more information. diff --git a/apps/docs/content/guides/deployment/branching/troubleshooting.mdx b/apps/docs/content/guides/deployment/branching/troubleshooting.mdx index 6e77b2c1768..1fd9194c40c 100644 --- a/apps/docs/content/guides/deployment/branching/troubleshooting.mdx +++ b/apps/docs/content/guides/deployment/branching/troubleshooting.mdx @@ -27,7 +27,9 @@ You might want to roll back changes you've made in an earlier migration change. To fix this, push the latest changes, then delete the preview branch in Supabase and reopen it. -The new preview branch is reseeded from the `./supabase/seed.sql` file by default. Any additional data changes made on the old preview branch are lost. This is equivalent to running `supabase db reset` locally. All migrations are rerun in sequential order. +The new preview branch is a fresh clone of your base project and is reseeded from the `./supabase/seed.sql` file by default. Any additional data changes made on the old preview branch are lost. + +To rerun migrations that your base project has already applied, reset the branch from the Supabase dashboard instead. A reset reruns all migrations in sequential order and drops existing data on the branch. ### Deployment failures diff --git a/apps/docs/content/guides/deployment/branching/working-with-branches.mdx b/apps/docs/content/guides/deployment/branching/working-with-branches.mdx index ecdceb08658..c007f17cf5f 100644 --- a/apps/docs/content/guides/deployment/branching/working-with-branches.mdx +++ b/apps/docs/content/guides/deployment/branching/working-with-branches.mdx @@ -181,7 +181,7 @@ After completing the steps above, you should receive a Slack message whenever an Migrations are run in sequential order. Each migration builds upon the previous one. -The preview branch has a record of which migrations have been applied, and only applies new migrations for each commit. This can create an issue when rolling back migrations. +The preview branch inherits the migration history of your base project, so it only applies migrations that haven't been run yet. This can create an issue when rolling back migrations. ### Using ORM or custom seed scripts @@ -237,7 +237,9 @@ You might want to roll back changes you've made in an earlier migration change. To fix this, push the latest changes, then delete the preview branch in Supabase and reopen it. -The new preview branch is reseeded from the `./supabase/seed.sql` file by default. Any additional data changes made on the old preview branch are lost. This is equivalent to running `supabase db reset` locally. All migrations are rerun in sequential order. +The new preview branch is a fresh clone of your base project and is reseeded from the `./supabase/seed.sql` file by default. Any additional data changes made on the old preview branch are lost. + +To rerun migrations that your base project has already applied, reset the branch from the Supabase dashboard instead. A reset reruns all migrations in sequential order and drops existing data on the branch. ### Seeding behavior diff --git a/apps/docs/content/guides/platform/upgrading.mdx b/apps/docs/content/guides/platform/upgrading.mdx index 1400a906e59..13f873a7228 100644 --- a/apps/docs/content/guides/platform/upgrading.mdx +++ b/apps/docs/content/guides/platform/upgrading.mdx @@ -192,7 +192,7 @@ Existing projects on pg_graphql 1.5.x are not impacted unless they choose to upg ### Ltree indexes require reindexing after upgrade -_Applies when upgrading to Postgres 15.18 or 17.10._ +_Applies when upgrading to Postgres 15.19 or 17.11._ @@ -221,29 +221,115 @@ To mitigate this issue: 2. If `reindex_required` is `true`, find the affected indexes: ```sql - select schemaname, tablename, indexname - from pg_indexes + select distinct + n.nspname as schema_name, + cls.relname as table_name, + ic.relname as index_name + from pg_index idx + join pg_class ic on idx.indexrelid = ic.oid + join pg_class cls on idx.indrelid = cls.oid + join pg_namespace n on ic.relnamespace = n.oid + join lateral unnest(idx.indclass::oid[]) with ordinality as k(opclass, pos) on true + join pg_opclass oc on oc.oid = k.opclass + join pg_type ty on ty.oid = oc.opcintype where - indexname in ( - select c.relname - from - pg_index as i - join pg_class as c on i.indexrelid = c.oid - join pg_attribute as a on a.attrelid = i.indrelid and a.attnum = ANY(i.indkey) - join pg_type as t on a.atttypid = t.oid - where t.typname in ('ltree', '_ltree') - ); + k.pos <= idx.indnkeyatts -- key columns only, excludes INCLUDE + and ty.typname in ('ltree', '_ltree'); ``` -3. Reindex each affected index. `REINDEX INDEX CONCURRENTLY` runs online with no downtime: +3. Reindex each affected index using its schema-qualified name. `REINDEX INDEX CONCURRENTLY` runs online with no downtime, but cannot run inside a transaction block: ```sql - REINDEX INDEX CONCURRENTLY ; + REINDEX INDEX CONCURRENTLY .; + ``` + +Separately from the encoding case above, this release also fixes an integer overflow in `ltree` comparisons: values with more than about 14,653 labels could compare incorrectly, which can corrupt B-tree indexes built over them, regardless of your database encoding. The following query lists only the B-tree indexes whose `ltree` column or expression actually contains such values, so they are the ones to reindex (an empty result means no action is needed): + +```sql +SELECT s.schema_name || '.' || s.index_name AS index_to_reindex +FROM ( + SELECT + n.nspname AS schema_name, + c.relname AS table_name, + ic.relname AS index_name, + min(pg_get_expr(i.indpred, i.indrelid)) AS pred, -- partial-index predicate, if any + string_agg('nlevel(' || pg_get_indexdef(i.indexrelid, k.pos::int, true) || ') > 14653', ' OR ') AS keys_cond + FROM pg_index i + JOIN pg_class ic ON ic.oid = i.indexrelid + JOIN pg_class c ON c.oid = i.indrelid + JOIN pg_namespace n ON n.oid = c.relnamespace + JOIN pg_am am ON am.oid = ic.relam + JOIN LATERAL generate_series(1, i.indnkeyatts) AS k(pos) ON true + JOIN pg_attribute ia ON ia.attrelid = i.indexrelid AND ia.attnum = k.pos + JOIN pg_type t ON t.oid = ia.atttypid + WHERE am.amname = 'btree' + AND t.typname = 'ltree' + AND n.nspname NOT IN ('pg_catalog', 'information_schema') + GROUP BY n.nspname, c.relname, ic.relname +) s +WHERE (xpath( + '/row/cnt/text()', + query_to_xml( + format('SELECT count(*) AS cnt FROM %I.%I WHERE %s(%s)', + s.schema_name, s.table_name, + CASE WHEN s.pred IS NOT NULL THEN '(' || s.pred || ') AND ' ELSE '' END, + s.keys_cond), + false, true, '' + ) + ))[1]::text::bigint > 0 +ORDER BY 1; +``` + +Reindex each index it returns, using the schema-qualified name. `REINDEX INDEX CONCURRENTLY` runs online with no downtime, but cannot run inside a transaction block: + +```sql +REINDEX INDEX CONCURRENTLY .; +``` + +### Btree_gist indexes on float columns require reindexing after upgrade + +_Applies when upgrading to Postgres 15.19 or 17.11._ + + + +You are affected only if you have `btree_gist` indexes on `float4` or `float8` columns that may contain `NaN` values. + + + +This release fixes `NaN` handling in `btree_gist`'s `float4` and `float8` operator classes. Indexes on those columns built under the previous version can return wrong results for rows containing `NaN` until the index is rebuilt. + +To mitigate this issue: + +1. Find `btree_gist` indexes on float columns: + + ```sql + select distinct + n.nspname as schema_name, + cls.relname as table_name, + ic.relname as index_name + from pg_index idx + join pg_class ic on idx.indexrelid = ic.oid + join pg_am am on ic.relam = am.oid + join pg_class cls on idx.indrelid = cls.oid + join pg_namespace n on ic.relnamespace = n.oid + join lateral unnest(idx.indclass::oid[]) with ordinality as k(opclass, pos) on true + join pg_opclass oc on oc.oid = k.opclass + join pg_type ty on ty.oid = oc.opcintype + where + am.amname = 'gist' + and k.pos <= idx.indnkeyatts + and ty.typname in ('float4', 'float8'); + ``` + +2. If any indexes are returned and those columns may contain `NaN` values, reindex them using the schema-qualified name. `REINDEX INDEX CONCURRENTLY` runs online with no downtime, but cannot run inside a transaction block: + + ```sql + REINDEX INDEX CONCURRENTLY .; ``` ### Custom operator selectivity estimators -_Applies when upgrading to Postgres 15.18 or 17.10._ +_Applies when upgrading to Postgres 15.19 or 17.11._ Attaching a non-built-in (extension- or user-provided) selectivity estimator function to an operator now requires superuser. Existing operators continue to work — the check only fires when an operator is (re)created, most commonly during `pg_dump` / `pg_restore`, a logical restore, or a branch. @@ -256,7 +342,9 @@ ERROR: must be superuser to specify a non-built-in restriction estimator functio Most projects are not affected. To check whether your database has any user-defined operators that reference a non-built-in estimator: ```sql -SELECT n.nspname AS schema, o.oprname AS operator +SELECT n.nspname AS schema, o.oprname AS operator, + o.oprrest::regproc AS restrict_estimator, + o.oprjoin::regproc AS join_estimator FROM pg_operator o JOIN pg_namespace n ON o.oprnamespace = n.oid WHERE n.nspname NOT IN ('pg_catalog', 'information_schema') diff --git a/apps/docs/content/troubleshooting/realtime-postgres-changes-troubleshooting.mdx b/apps/docs/content/troubleshooting/realtime-postgres-changes-troubleshooting.mdx new file mode 100644 index 00000000000..2d5bcfa2608 --- /dev/null +++ b/apps/docs/content/troubleshooting/realtime-postgres-changes-troubleshooting.mdx @@ -0,0 +1,225 @@ +--- +title = "Realtime: Postgres Changes Troubleshooting" +topics = [ + "database", + "realtime", +] +keywords = [ "postgres changes", "rls", "replica identity", "subscription", "websocket" ] # any strings (topics are automatically added so no need to duplicate) +--- + +A Realtime subscription connects, a row changes in the database, and nothing arrives on the client. No error, no event — silence. This is one of the more common issues developers run into with Supabase Realtime, and the cause is almost always one of a handful of things. + +The order below reflects how often each one turns out to be the actual problem. Check RLS early, even if the subscription code looks fine — it's the single biggest source of "silently missing" events, by a wide margin. + +For a broader index of Realtime-specific issues, see the [Troubleshooting](/docs/guides/troubleshooting) directory. + +## Step 1: Is the table in the Realtime publication? + +Realtime doesn't watch every table by default. Each one has to be added to a publication called `supabase_realtime`. If it isn't, Realtime has no visibility into that table at all — no matter how the client subscription is set up. + +```sql +select * from pg_publication_tables where pubname = 'supabase_realtime'; +``` + +If the table's missing from the results: + +```sql +alter publication supabase_realtime add table your_table; +``` + +Or toggle it on from **Database → Replication** in the dashboard. + +This gets overlooked on a table created recently — creating the table and enabling Realtime on it are two separate steps: + +```sql +alter publication supabase_realtime add table messages; +``` + +## Step 2: Is RLS quietly blocking the row? + +This is the most common cause, and it's the one that wastes the most time, because nothing errors. The event doesn't show up. + +Realtime enforces RLS the same way a normal query would — as the subscribing client's role. `SUBSCRIBED` only means the WebSocket connected. It says nothing about whether that client is allowed to see the data. + +Test it directly with either options: + +Using the same credentials the client uses, try to select the row that changed: + +```js +const { data, error } = await supabase.from('messages').select('*').eq('id', theRowIdThatChanged) + +console.log({ data, error }) +``` + +Or on the dashboard, open Table Editor, and try to view the row as the subscribing user's role. + +Empty `data`? That's the answer. The policy is blocking this row for this user, and Realtime is doing exactly what it's supposed to. + +A fix might look like: + +```sql +create policy "Users can view messages in their rooms" +on messages for select +using ( + exists ( + select 1 from room_members + where room_members.room_id = messages.room_id + and room_members.user_id = auth.uid() + ) +); +``` + +One trap specific to `UPDATE` events: RLS generally has to allow both the old and new row state. If a policy only permits `status = 'active'`, and an update flips the status to `archived`, the event can fail to deliver — the row was visible a second ago, but the new state no longer passes the check. + +See [Row Level Security](/docs/guides/database/postgres/row-level-security) for the underlying model, and [why a select can return an empty data array](./why-is-my-select-returning-an-empty-data-array-and-i-have-data-in-the-table-xvOPgx), which is directly relevant to the test above. + +## Step 3: Missing fields in `payload.old`? + +If events are arriving but `payload.old` is mostly empty or `null`, that's a replica identity problem, not a delivery problem. + +By default, Postgres only sends the primary key in the "old row" for `UPDATE` and `DELETE`. If your code compares old vs. new values, that's not enough: + +```sql +alter table messages replica identity full; +``` + +If the table has Row Level Security enabled, `replica identity full` isn't enough by itself: the `old` record still contains only the primary key. There's no way around this while RLS is on — don't rely on other `payload.old` fields for a policy-protected table. + +A typical case: a `status` column moves from `pending` to `completed`, and the client checks + +```js +if (payload.old.status !== payload.new.status) { + notifyUser() +} +``` + +Without `replica identity full` (or with RLS enabled), `payload.old.status` is `undefined`, and `undefined !== 'completed'` is `true` — so the check fires `notifyUser()` on every update, not just the ones where status actually changed. + +## Step 4: Check the subscription code itself + +Once publication and RLS are ruled out, look at the subscription config. Table name, schema, filter syntax — small mismatches here are common. + +- Table name matches exactly, including case +- Schema is correct (`public`, unless you're using a custom one) +- Filter syntax is right: `room_id=eq.abc123`, not `room_id = abc123` + +```js +const channel = supabase + .channel('room-messages') + .on( + 'postgres_changes', + { + event: 'UPDATE', + schema: 'public', + table: 'messages', + filter: `room_id=eq.${roomId}`, + }, + (payload) => { + console.log('Got an update:', payload.new) + } + ) + .subscribe((status) => { + console.log('Subscription status:', status) + }) +``` + +Log the status callback. Don't assume `.subscribe()` worked. + +- `SUBSCRIBED` — connected +- `CHANNEL_ERROR` — check the error payload +- `CLOSED` — channel got shut down +- `TIMED_OUT` — connection issue; see [Realtime connections giving `TIMED_OUT` errors](./realtime-connections-timed_out-status) + +A channel stuck at `CHANNEL_ERROR` is a different problem than one that connects fine but never fires. Figure out which one you're dealing with before going further. + +## Step 5: Stale or duplicate subscriptions + +This one is almost always a React/Next.js problem. + +A component re-renders, `roomId` changes, and the old channel doesn't get cleaned up. Now you've got two subscriptions running, or one listening against a stale parameter. It doesn't always look broken — sometimes it looks like duplicate events, or events for the wrong room. + +```js +useEffect(() => { + const channel = supabase + .channel(`room:${roomId}`) + .on( + 'postgres_changes', + { event: 'UPDATE', schema: 'public', table: 'messages', filter: `room_id=eq.${roomId}` }, + (payload) => console.log(payload.new) + ) + .subscribe() + + return () => { + supabase.removeChannel(channel) + } +}, [roomId]) +``` + +That cleanup line is the part people skip. Without it, the old channel keeps running against the previous `roomId` in the background, and nothing in the UI tells you it's happening. See [Next.js 13/14 stale data when changing RLS or table data](./nextjs-1314-stale-data-when-changing-rls-or-table-data-85b8oQ) for an adjacent version of the same bug. + +## Step 6: Writing right after `SUBSCRIBED` + +There's a confirmed timing gap between the client reporting `SUBSCRIBED` and the backend's replication listener being ready to stream. A write made in that gap can get missed. + +This shows up most in automated tests, where subscribe and write happen back-to-back: + +```js +// Risky — writing immediately after subscribing +const channel = supabase.channel('test-channel').on(/* ... */).subscribe() +await supabase.from('messages').insert({ text: 'hello' }) +``` + +The reliable fix isn't a fixed delay — it's waiting for the backend to confirm the `postgres_changes` extension is listening, via the `system` message it emits: + +```js +supabase + .channel('room1') + .on('system', '*', (payload) => { + if (payload.extension === 'postgres_changes' && payload.status === 'ok') { + console.log('changes are ready', payload) // safe to write now + } + }) + .on('postgres_changes', { event: '*', schema: '*' }, (payload) => { + console.log('Change received!', payload) + }) + .subscribe() +``` + +No need to `await` anything here — react to the message inside the handler (e.g. set a flag) before triggering the write. This `system` message isn't formally documented yet. + +If you can't wire this up right now, a short fixed delay after `SUBSCRIBED` is a weaker fallback: + +```js +channel.subscribe(async (status) => { + if (status === 'SUBSCRIBED') { + await new Promise((resolve) => setTimeout(resolve, 1000)) + await supabase.from('messages').insert({ text: 'hello' }) + } +}) +``` + +## Step 7: Is Realtime enabled? + +Sometimes the answer is straightforward. Check: + +- **Project Settings → Realtime** — enabled at the project level? +- **Database → Replication** — toggle on for this specific table? + +Check this again after a project's been paused and restarted — the replication slot can need to reconnect. If the project's under heavier load than usual, also check the Realtime "Concurrent Peak Connections" quota — hitting that limit can look a lot like a broken subscription. + +## Step 8: Read the Realtime logs + +Still stuck? **Logs → Realtime** in the dashboard. Look for connection drops, replication errors, rate limiting. + +For more detail than the default logs give you, see [Debug Realtime with Logger and Log Levels](./realtime-debugging-with-logger). If the connection seems to drop intermittently rather than failing outright, check [Realtime: Handling Silent Disconnections in Background Applications](./realtime-handling-silent-disconnections-in-backgrounded-applications-592794) and [Understanding and Monitoring Realtime Heartbeats](./realtime-heartbeat-messages). + +One thing to design around regardless: Realtime doesn't guarantee every message gets delivered. Network blips and reconnects can drop an event here and there. If a missed update matters, treat the Realtime event as a signal to re-fetch state — not as the only source of truth. Then a dropped event is an inconvenience, not a bug. + +## Still nothing? + +Rule out the network layer: + +- Corporate firewall or proxy blocking WebSocket connections +- Outdated `supabase-js` version — check recent release notes + +Hopefully this guide is helpful to you in resolving these types of issues. diff --git a/apps/docs/features/docs/Reference.ui.tsx b/apps/docs/features/docs/Reference.ui.tsx index 2cac82a4c0c..c8a2ee5704b 100644 --- a/apps/docs/features/docs/Reference.ui.tsx +++ b/apps/docs/features/docs/Reference.ui.tsx @@ -381,6 +381,14 @@ interface ApiOperationRequestBodyDetailsInternalProps extends HTMLAttributes { + if (Array.isArray(value)) return value + if (value && typeof value === 'object') return [value] + return [] +} + function ApiOperationRequestBodyDetailsInternal({ schema, ...props @@ -389,7 +397,7 @@ function ApiOperationRequestBodyDetailsInternal({ return ( <> All of the following: - {schema.allOf.map((option, index) => ( + {asSchemaArray(schema.allOf).map((option, index) => ( ))} @@ -398,7 +406,7 @@ function ApiOperationRequestBodyDetailsInternal({ return ( <> Any of the following: - {schema.anyOf.map((option, index) => ( + {asSchemaArray(schema.anyOf).map((option, index) => ( ))} @@ -407,7 +415,7 @@ function ApiOperationRequestBodyDetailsInternal({ return ( <> One of the following: - {schema.oneOf.map((option, index) => ( + {asSchemaArray(schema.oneOf).map((option, index) => ( ))} @@ -431,10 +439,11 @@ function ApiOperationRequestBodyDetailsInternal({ return ( <> {`Array of ${displayName}`} - {!( - 'type' in schema.items && - ['string', 'boolean', 'number', 'integer'].includes(schema.items.type) - ) && } + {schema.items && + !( + 'type' in schema.items && + ['string', 'boolean', 'number', 'integer'].includes(schema.items.type) + ) && } ) } else if (schema.type === 'object') { @@ -469,13 +478,14 @@ export function ApiSchemaParamSubdetails({ (schema.type === 'string' && !('minLength' in schema || 'maxLength' in schema || 'pattern' in schema)) || (schema.type === 'array' && + schema.items && 'type' in schema.items && ['boolean', 'number', 'integer', 'string', 'file'].includes(schema.items.type))) ) { return null } - const subContent = + const rawSubContent = 'enum' in schema ? schema.enum : 'anyOf' in schema @@ -492,6 +502,7 @@ export function ApiSchemaParamSubdetails({ value: schema[key], })) : [] + const subContent = asSchemaArray(rawSubContent) return ( @@ -530,8 +541,24 @@ export function ApiSchemaParamSubdetails({ {'type' in schema && schema.type === 'object' ? ( -
- +
+
+ +
+ +
+ ) : 'type' in schema && + schema.type === 'array' && + 'items' in schema && + schema.items && + typeof schema.items === 'object' && + 'type' in schema.items && + schema.items.type === 'object' ? ( +
+
+ +
+
) : (
    diff --git a/apps/docs/features/ui/CodeBlock/CodeBlock.client.tsx b/apps/docs/features/ui/CodeBlock/CodeBlock.client.tsx index 2db18ab15c4..4ec13d2478a 100644 --- a/apps/docs/features/ui/CodeBlock/CodeBlock.client.tsx +++ b/apps/docs/features/ui/CodeBlock/CodeBlock.client.tsx @@ -119,6 +119,8 @@ export function CodeCopyButton({ className, content }: { className?: string; con className )} aria-label="Copy code" + // Tooltip repeats the label; the description would read the name twice + aria-describedby={undefined} > {copied ? ( @@ -135,29 +137,33 @@ export function CodeCopyButton({ className, content }: { className?: string; con export function CodeBlockControls({ content }: { content: string }) { const [isWrapped, setIsWrapped] = useState(false) + // Empty until the first toggle, so nothing is announced on mount + const [wrapStatus, setWrapStatus] = useState('') const wrapperRef = useRef(null) const toggleWrap = useCallback(() => { - setIsWrapped((prev) => { - const newValue = !prev - // Find the parent code block and toggle the wrap data attribute - const codeBlock = wrapperRef.current?.closest('.shiki') - if (codeBlock) { - if (newValue) { - codeBlock.setAttribute('data-wrapped', 'true') - } else { - codeBlock.removeAttribute('data-wrapped') - } + const newValue = !isWrapped + setIsWrapped(newValue) + setWrapStatus(newValue ? 'Word wrap enabled' : 'Word wrap disabled') + + const codeBlock = wrapperRef.current?.closest('.shiki') + if (codeBlock) { + if (newValue) { + codeBlock.setAttribute('data-wrapped', 'true') + } else { + codeBlock.removeAttribute('data-wrapped') } - return newValue - }) - }, []) + } + }, [isWrapped]) return (
    + + {wrapStatus} +
    ) } diff --git a/apps/docs/features/ui/CodeBlock/CodeBlock.utils.ts b/apps/docs/features/ui/CodeBlock/CodeBlock.utils.ts index 2202c2a5193..b3046183a38 100644 --- a/apps/docs/features/ui/CodeBlock/CodeBlock.utils.ts +++ b/apps/docs/features/ui/CodeBlock/CodeBlock.utils.ts @@ -1,8 +1,6 @@ import { type CSSProperties } from 'react' -/* - * As defined in @shikijs/core/dist/chunk-tokens.d.mts - */ +// As defined in @shikijs/core/dist/chunk-tokens.d.mts enum FontStyle { NotSet = -1, None = 0, @@ -28,3 +26,26 @@ export function getFontStyle(styleFlags: number): CSSProperties { return style } + +// Fence aliases a screen reader would otherwise read letter by letter +const LANGUAGE_LABELS: Record = { + c: 'C', + html: 'HTML', + js: 'JavaScript', + json: 'JSON', + jsx: 'JavaScript', + py: 'Python', + sh: 'Shell', + shell: 'Shell', + sql: 'SQL', + toml: 'TOML', + ts: 'TypeScript', + tsx: 'TypeScript', + yaml: 'YAML', +} + +export function getCodeBlockLabel(lang: string | null, lineCount: number): string { + const lines = `${lineCount} ${lineCount === 1 ? 'line' : 'lines'}` + if (!lang) return lines + return `${LANGUAGE_LABELS[lang] ?? lang}, ${lines}` +} diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index 0f40c00761b..964214306f5 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -283,6 +283,7 @@ Shane E Shaun Newman Shardul Borhade Shreekar Shetty +Simon Tomlinson Sreyas Udayavarman Stephanie Jackson (stejacks) Stephen Morgan diff --git a/apps/docs/spec/api_v1_openapi.json b/apps/docs/spec/api_v1_openapi.json index 01b1a6ff1cf..f2e5cd59c0b 100644 --- a/apps/docs/spec/api_v1_openapi.json +++ b/apps/docs/spec/api_v1_openapi.json @@ -468,7 +468,7 @@ "summary": "List all projects", "tags": ["Projects"], "x-badges": [{ "name": "OAuth scope: projects:read", "position": "after" }], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["projects_read"]], "x-oauth-scope": "projects:read" }, @@ -496,7 +496,7 @@ "summary": "Create a project", "tags": ["Projects"], "x-badges": [{ "name": "OAuth scope: projects:write", "position": "after" }], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["organization_projects_create"]], "x-oauth-scope": "projects:write" } @@ -596,7 +596,7 @@ "summary": "List all organizations", "tags": ["Organizations"], "x-badges": [{ "name": "OAuth scope: organizations:read", "position": "after" }], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["organizations_read"]], "x-oauth-scope": "organizations:read" }, @@ -628,7 +628,7 @@ "security": [{ "bearer": [] }], "summary": "Create an organization", "tags": ["Organizations"], - "x-endpoint-owners": ["management-api", "billing"], + "x-endpoint-owners": ["control-plane", "billing"], "x-fga-permissions": [["organizations_create"]] } }, @@ -725,7 +725,7 @@ "responses": { "204": { "description": "" } }, "summary": "[Beta] Authorize user through oauth", "tags": ["OAuth"], - "x-endpoint-owners": ["auth", "management-api"] + "x-endpoint-owners": ["auth", "control-plane"] } }, "/v1/oauth/token": { @@ -753,7 +753,7 @@ }, "summary": "[Beta] Exchange auth code for user's access and refresh token", "tags": ["OAuth"], - "x-endpoint-owners": ["auth", "management-api"] + "x-endpoint-owners": ["auth", "control-plane"] } }, "/v1/oauth/revoke": { @@ -771,7 +771,7 @@ "responses": { "204": { "description": "" } }, "summary": "[Beta] Revoke oauth app authorization and it's corresponding tokens", "tags": ["OAuth"], - "x-endpoint-owners": ["auth", "management-api"] + "x-endpoint-owners": ["auth", "control-plane"] } }, "/v1/oauth/authorize/project-claim": { @@ -853,7 +853,7 @@ "security": [{ "bearer": [] }], "summary": "Authorize user through oauth and claim a project", "tags": ["OAuth"], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["organization_admin_write", "project_admin_write"]] } }, @@ -910,7 +910,7 @@ "summary": "Lists SQL snippets for the logged in user", "tags": ["Database"], "x-badges": [{ "name": "OAuth scope: database:read", "position": "after" }], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["snippets_read"]], "x-oauth-scope": "database:read" } @@ -947,7 +947,7 @@ "summary": "Gets a specific SQL snippet", "tags": ["Database"], "x-badges": [{ "name": "OAuth scope: database:read", "position": "after" }], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["snippets_read"]], "x-oauth-scope": "database:read" } @@ -967,7 +967,7 @@ "security": [{ "bearer": [] }], "summary": "Gets the user's profile", "tags": ["Profile"], - "x-endpoint-owners": ["management-api"] + "x-endpoint-owners": ["control-plane"] } }, "/v1/projects/{ref}/actions": { @@ -1256,7 +1256,7 @@ "summary": "Get project api keys", "tags": ["Secrets"], "x-badges": [{ "name": "OAuth scope: secrets:read", "position": "after" }], - "x-endpoint-owners": ["auth", "management-api"], + "x-endpoint-owners": ["auth", "control-plane"], "x-fga-permissions": [["api_gateway_keys_read"]], "x-oauth-scope": "secrets:read" }, @@ -1305,7 +1305,7 @@ "summary": "Creates a new API key for the project", "tags": ["Secrets"], "x-badges": [{ "name": "OAuth scope: secrets:write", "position": "after" }], - "x-endpoint-owners": ["auth", "management-api"], + "x-endpoint-owners": ["auth", "control-plane"], "x-fga-permissions": [["api_gateway_keys_write"]], "x-oauth-scope": "secrets:write" } @@ -1345,7 +1345,7 @@ "summary": "Check whether JWT based legacy (anon, service_role) API keys are enabled. This API endpoint will be removed in the future, check for HTTP 404 Not Found.", "tags": ["Secrets"], "x-badges": [{ "name": "OAuth scope: secrets:read", "position": "after" }], - "x-endpoint-owners": ["auth", "management-api"], + "x-endpoint-owners": ["auth", "control-plane"], "x-fga-permissions": [["api_gateway_keys_read"]], "x-oauth-scope": "secrets:read" }, @@ -1390,7 +1390,7 @@ "summary": "Disable or re-enable JWT based legacy (anon, service_role) API keys. This API endpoint will be removed in the future, check for HTTP 404 Not Found.", "tags": ["Secrets"], "x-badges": [{ "name": "OAuth scope: secrets:write", "position": "after" }], - "x-endpoint-owners": ["auth", "management-api"], + "x-endpoint-owners": ["auth", "control-plane"], "x-fga-permissions": [["api_gateway_keys_write"]], "x-oauth-scope": "secrets:write" } @@ -1452,7 +1452,7 @@ "summary": "Updates an API key for the project", "tags": ["Secrets"], "x-badges": [{ "name": "OAuth scope: secrets:write", "position": "after" }], - "x-endpoint-owners": ["auth", "management-api"], + "x-endpoint-owners": ["auth", "control-plane"], "x-fga-permissions": [["api_gateway_keys_write"]], "x-oauth-scope": "secrets:write" }, @@ -1506,7 +1506,7 @@ "summary": "Get API key", "tags": ["Secrets"], "x-badges": [{ "name": "OAuth scope: secrets:read", "position": "after" }], - "x-endpoint-owners": ["auth", "management-api"], + "x-endpoint-owners": ["auth", "control-plane"], "x-fga-permissions": [["api_gateway_keys_read"]], "x-oauth-scope": "secrets:read" }, @@ -1573,7 +1573,7 @@ "summary": "Deletes an API key for the project", "tags": ["Secrets"], "x-badges": [{ "name": "OAuth scope: secrets:write", "position": "after" }], - "x-endpoint-owners": ["auth", "management-api"], + "x-endpoint-owners": ["auth", "control-plane"], "x-fga-permissions": [["api_gateway_keys_write"]], "x-oauth-scope": "secrets:write" } @@ -1773,7 +1773,7 @@ "summary": "[Beta] Gets project's custom hostname config", "tags": ["Domains"], "x-badges": [{ "name": "OAuth scope: domains:read", "position": "after" }], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["custom_domain_read"]], "x-oauth-scope": "domains:read" }, @@ -1812,7 +1812,7 @@ "summary": "[Beta] Deletes a project's custom hostname configuration", "tags": ["Domains"], "x-badges": [{ "name": "OAuth scope: domains:write", "position": "after" }], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["custom_domain_write"]], "x-oauth-scope": "domains:write" } @@ -1861,7 +1861,7 @@ "summary": "[Beta] Updates project's custom hostname configuration", "tags": ["Domains"], "x-badges": [{ "name": "OAuth scope: domains:write", "position": "after" }], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["custom_domain_write"]], "x-oauth-scope": "domains:write" } @@ -1902,7 +1902,7 @@ "summary": "[Beta] Attempts to verify the DNS configuration for project's custom hostname configuration", "tags": ["Domains"], "x-badges": [{ "name": "OAuth scope: domains:write", "position": "after" }], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["custom_domain_write"]], "x-oauth-scope": "domains:write" } @@ -1943,7 +1943,7 @@ "summary": "[Beta] Activates a custom hostname for a project.", "tags": ["Domains"], "x-badges": [{ "name": "OAuth scope: domains:write", "position": "after" }], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["custom_domain_write"]], "x-oauth-scope": "domains:write" } @@ -2013,7 +2013,7 @@ "summary": "[Beta] Get project's temporary access configuration.", "tags": ["Database"], "x-badges": [{ "name": "OAuth scope: database:read", "position": "after" }], - "x-endpoint-owners": ["security", "management-api"], + "x-endpoint-owners": ["security", "control-plane"], "x-fga-permissions": [["project_admin_read"]], "x-oauth-scope": "database:read" }, @@ -2089,7 +2089,7 @@ "summary": "[Beta] Update project's temporary access configuration.", "tags": ["Database"], "x-badges": [{ "name": "OAuth scope: database:write", "position": "after" }], - "x-endpoint-owners": ["security", "management-api"], + "x-endpoint-owners": ["security", "control-plane"], "x-fga-permissions": [["project_admin_write"]], "x-oauth-scope": "database:write" } @@ -2130,7 +2130,7 @@ "summary": "[Beta] Gets project's network bans", "tags": ["Projects"], "x-badges": [{ "name": "OAuth scope: projects:read", "position": "after" }], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["database_network_bans_read"]], "x-oauth-scope": "projects:read" } @@ -2171,7 +2171,7 @@ "summary": "[Beta] Gets project's network bans with additional information about which databases they affect", "tags": ["Projects"], "x-badges": [{ "name": "OAuth scope: projects:read", "position": "after" }], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["database_network_bans_read"]], "x-oauth-scope": "projects:read" } @@ -2213,7 +2213,7 @@ "summary": "[Beta] Remove network bans.", "tags": ["Projects"], "x-badges": [{ "name": "OAuth scope: projects:write", "position": "after" }], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["database_network_bans_write"]], "x-oauth-scope": "projects:write" } @@ -2254,7 +2254,7 @@ "summary": "[Beta] Gets project's network restrictions", "tags": ["Projects"], "x-badges": [{ "name": "OAuth scope: projects:read", "position": "after" }], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["database_network_restrictions_read"]], "x-oauth-scope": "projects:read" }, @@ -2301,7 +2301,7 @@ "summary": "[Alpha] Updates project's network restrictions by adding or removing CIDRs", "tags": ["Projects"], "x-badges": [{ "name": "OAuth scope: projects:write", "position": "after" }], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["database_network_restrictions_write"]], "x-oauth-scope": "projects:write" } @@ -2350,7 +2350,7 @@ "summary": "[Beta] Updates project's network restrictions", "tags": ["Projects"], "x-badges": [{ "name": "OAuth scope: projects:write", "position": "after" }], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["database_network_restrictions_write"]], "x-oauth-scope": "projects:write" } @@ -2479,7 +2479,7 @@ "summary": "Gets project's postgrest config", "tags": ["Rest"], "x-badges": [{ "name": "OAuth scope: rest:read", "position": "after" }], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["data_api_config_read"]], "x-oauth-scope": "rest:read" }, @@ -2526,7 +2526,7 @@ "summary": "Updates project's postgrest config", "tags": ["Rest"], "x-badges": [{ "name": "OAuth scope: rest:write", "position": "after" }], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["data_api_config_write"]], "x-oauth-scope": "rest:write" } @@ -2567,7 +2567,7 @@ "summary": "Gets a specific project that belongs to the authenticated user", "tags": ["Projects"], "x-badges": [{ "name": "OAuth scope: projects:read", "position": "after" }], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["project_admin_read"]], "x-oauth-scope": "projects:read" }, @@ -2605,7 +2605,7 @@ "summary": "Deletes the given project", "tags": ["Projects"], "x-badges": [{ "name": "OAuth scope: projects:write", "position": "after" }], - "x-endpoint-owners": ["management-api", "infra", "dev-workflows"], + "x-endpoint-owners": ["control-plane", "infra", "dev-workflows"], "x-fga-permissions": [["project_admin_write"]], "x-oauth-scope": "projects:write" }, @@ -2650,7 +2650,7 @@ "summary": "Updates the given project", "tags": ["Projects"], "x-badges": [{ "name": "OAuth scope: projects:write", "position": "after" }], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["project_admin_write"]], "x-oauth-scope": "projects:write" } @@ -2814,7 +2814,7 @@ "summary": "[Beta] Get project's SSL enforcement configuration.", "tags": ["Database"], "x-badges": [{ "name": "OAuth scope: database:read", "position": "after" }], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["database_ssl_config_read"]], "x-oauth-scope": "database:read" }, @@ -2861,7 +2861,7 @@ "summary": "[Beta] Update project's SSL enforcement configuration.", "tags": ["Database"], "x-badges": [{ "name": "OAuth scope: database:write", "position": "after" }], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["database_ssl_config_write"]], "x-oauth-scope": "database:write" } @@ -2960,7 +2960,7 @@ { "name": "OAuth scope: domains:read", "position": "after" }, { "name": "Only available on Pro, Team, Enterprise", "position": "before" } ], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["vanity_subdomain_read"]], "x-oauth-scope": "domains:read" }, @@ -2992,7 +2992,7 @@ "summary": "[Beta] Deletes a project's vanity subdomain configuration", "tags": ["Domains"], "x-badges": [{ "name": "OAuth scope: domains:write", "position": "after" }], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["vanity_subdomain_write"]], "x-oauth-scope": "domains:write" } @@ -3049,7 +3049,7 @@ { "name": "OAuth scope: domains:write", "position": "after" }, { "name": "Only available on Pro, Team, Enterprise", "position": "before" } ], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["vanity_subdomain_write"]], "x-oauth-scope": "domains:write" } @@ -3106,7 +3106,7 @@ { "name": "OAuth scope: domains:write", "position": "after" }, { "name": "Only available on Pro, Team, Enterprise", "position": "before" } ], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["vanity_subdomain_write"]], "x-oauth-scope": "domains:write" } @@ -3153,7 +3153,7 @@ "summary": "[Beta] Upgrades the project's Postgres version", "tags": ["Projects"], "x-badges": [{ "name": "OAuth scope: projects:write", "position": "after" }], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["project_admin_write", "database_write"]], "x-oauth-scope": "projects:write" } @@ -3194,7 +3194,7 @@ "summary": "[Beta] Returns the project's eligibility for upgrades", "tags": ["Projects"], "x-badges": [{ "name": "OAuth scope: projects:read", "position": "after" }], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["project_admin_read", "database_read"]], "x-oauth-scope": "projects:read" } @@ -3241,7 +3241,7 @@ "summary": "[Beta] Gets the latest status of the project's upgrade", "tags": ["Projects"], "x-badges": [{ "name": "OAuth scope: projects:read", "position": "after" }], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["project_admin_read", "database_read"]], "x-oauth-scope": "projects:read" } @@ -3282,7 +3282,7 @@ "summary": "Returns project's readonly mode status", "tags": ["Database"], "x-badges": [{ "name": "OAuth scope: database:read", "position": "after" }], - "x-endpoint-owners": ["management-api", "infra", "support-tooling"], + "x-endpoint-owners": ["control-plane", "infra", "support-tooling"], "x-fga-permissions": [["database_readonly_config_read"]], "x-oauth-scope": "database:read" } @@ -3316,7 +3316,7 @@ "summary": "Disables project's readonly mode for the next 15 minutes", "tags": ["Database"], "x-badges": [{ "name": "OAuth scope: database:write", "position": "after" }], - "x-endpoint-owners": ["management-api", "infra", "support-tooling"], + "x-endpoint-owners": ["control-plane", "infra", "support-tooling"], "x-fga-permissions": [["database_readonly_config_write"]], "x-oauth-scope": "database:write" } @@ -3365,7 +3365,7 @@ "tags": ["Database"], "x-allowed-plans": ["Pro", "Team", "Enterprise"], "x-badges": [{ "name": "Only available on Pro, Team, Enterprise", "position": "before" }], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["infra_read_replicas_write"]] } }, @@ -3405,7 +3405,7 @@ "security": [{ "bearer": [] }], "summary": "[Beta] Remove a read replica", "tags": ["Database"], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["infra_read_replicas_write"]] } }, @@ -3488,7 +3488,7 @@ "summary": "Gets project's service health status", "tags": ["Projects"], "x-badges": [{ "name": "OAuth scope: projects:read", "position": "after" }], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["project_admin_read"]], "x-oauth-scope": "projects:read" } @@ -4111,7 +4111,7 @@ "summary": "Pauses the given project", "tags": ["Projects"], "x-badges": [{ "name": "OAuth scope: projects:write", "position": "after" }], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["project_admin_write"]], "x-oauth-scope": "projects:write" } @@ -4144,7 +4144,7 @@ "summary": "Restarts the given project", "tags": ["Projects"], "x-badges": [{ "name": "OAuth scope: projects:write", "position": "after" }], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["project_admin_write"]], "x-oauth-scope": "projects:write" } @@ -4186,7 +4186,7 @@ "summary": "Lists available restore versions for the given project", "tags": ["Projects"], "x-badges": [{ "name": "OAuth scope: projects:read", "position": "after" }], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["project_admin_read"]], "x-oauth-scope": "projects:read" }, @@ -4217,7 +4217,7 @@ "summary": "Restores the given project", "tags": ["Projects"], "x-badges": [{ "name": "OAuth scope: projects:write", "position": "after" }], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["project_admin_write"]], "x-oauth-scope": "projects:write" } @@ -4250,7 +4250,7 @@ "summary": "Cancels the given project restoration", "tags": ["Projects"], "x-badges": [{ "name": "OAuth scope: projects:write", "position": "after" }], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["project_admin_write"]], "x-oauth-scope": "projects:write" } @@ -4437,7 +4437,7 @@ "security": [{ "bearer": [] }], "summary": "Gets project claim token", "tags": ["Projects"], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["project_admin_read"]], "x-internal": true }, @@ -4474,7 +4474,7 @@ "security": [{ "bearer": [] }], "summary": "Creates project claim token", "tags": ["Projects"], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["organization_admin_write", "project_admin_write"]], "x-internal": true }, @@ -4504,7 +4504,7 @@ "security": [{ "bearer": [] }], "summary": "Revokes project claim token", "tags": ["Projects"], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["organization_admin_write", "project_admin_write"]], "x-internal": true } @@ -4546,7 +4546,7 @@ "summary": "Gets project performance advisors.", "tags": ["Advisors"], "x-badges": [{ "name": "OAuth scope: database:read", "position": "after" }], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["advisors_read"]], "x-oauth-scope": "database:read" } @@ -4594,7 +4594,7 @@ "summary": "Gets project security advisors.", "tags": ["Advisors"], "x-badges": [{ "name": "OAuth scope: database:read", "position": "after" }], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["advisors_read"]], "x-oauth-scope": "database:read" } @@ -4602,7 +4602,7 @@ "/v1/projects/{ref}/analytics/endpoints/logs.all": { "get": { "deprecated": true, - "description": "Executes a SQL query on the project's logs.\n\nEither the `iso_timestamp_start` and `iso_timestamp_end` parameters must be provided.\nIf both are not provided, only the last 1 minute of logs will be queried.\nThe timestamp range must be no more than 24 hours and is rounded to the nearest minute. If the range is more than 24 hours, a validation error will be thrown.\n\nNote: Unless the `sql` parameter is provided, only edge_logs will be queried. See the [log query docs](/docs/guides/telemetry/logs?queryGroups=product&product=postgres&queryGroups=source&source=edge_logs#querying-with-the-logs-explorer:~:text=logs%20from%20the-,Sources,-drop%2Ddown%3A) for all available sources.\n", + "description": "Executes a SQL query on the project's logs.\n\nEither the `iso_timestamp_start` and `iso_timestamp_end` parameters must be provided.\nIf both are not provided, only the last 1 minute of logs will be queried.\nThe timestamp range must be no more than 24 hours and is rounded to the nearest minute. If the range is more than 24 hours, a validation error will be thrown.\n\nNote: Unless the `sql` parameter is provided, only edge_logs will be queried. See the [log query docs](https://supabase.com/docs/guides/monitoring-and-debugging/logs#logs-explorer) for all available sources.\n", "operationId": "v1-get-project-logs-all", "parameters": [ { @@ -4622,7 +4622,7 @@ "name": "sql", "required": false, "in": "query", - "description": "Custom SQL query to execute on the logs. See [querying logs](/docs/guides/telemetry/logs?queryGroups=product&product=postgres&queryGroups=source&source=edge_logs#querying-with-the-logs-explorer) for more details.", + "description": "Custom SQL query to execute on the logs. See [querying logs](https://supabase.com/docs/guides/monitoring-and-debugging/logs#querying-with-the-logs-explorer) for more details.", "schema": { "example": "select event_message from edge_logs limit 10", "type": "string" @@ -4695,7 +4695,7 @@ "name": "sql", "required": false, "in": "query", - "description": "Custom SQL query to execute on the logs. See [querying logs](/docs/guides/telemetry/logs?queryGroups=product&product=postgres&queryGroups=source&source=edge_logs#querying-with-the-logs-explorer) for more details.", + "description": "Custom SQL query to execute on the logs. See [querying logs](https://supabase.com/docs/guides/monitoring-and-debugging/logs#querying-with-the-logs-explorer) for more details.", "schema": { "example": "select event_message from edge_logs limit 10", "type": "string" @@ -4913,6 +4913,9 @@ "application/openmetrics-text": { "schema": { "type": "string" } } } }, + "400": { + "description": "Project must be active and healthy, or metrics are not available for this project" + }, "401": { "description": "Unauthorized" }, "403": { "description": "Forbidden action" }, "429": { "description": "Rate limit exceeded" }, @@ -5315,7 +5318,7 @@ "summary": "[Beta] Run sql query", "tags": ["Database"], "x-badges": [{ "name": "OAuth scope: database:write", "position": "after" }], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["database_read"], ["database_write"]], "x-oauth-scope": "database:write" } @@ -5356,7 +5359,7 @@ "summary": "[Beta] Run a sql query as supabase_read_only_user", "tags": ["Database"], "x-badges": [{ "name": "OAuth scope: database:read", "position": "after" }], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["database_read"]], "x-oauth-scope": "database:read" } @@ -5390,7 +5393,7 @@ "summary": "[Beta] Enables Database Webhooks on the project", "tags": ["Database"], "x-badges": [{ "name": "OAuth scope: database:write", "position": "after" }], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["database_webhooks_config_write"]], "x-oauth-scope": "database:write" } @@ -5432,7 +5435,7 @@ "summary": "Gets database metadata for the given project.", "tags": ["Database"], "x-badges": [{ "name": "OAuth scope: projects:read", "position": "after" }], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["database_read"]], "x-oauth-scope": "projects:read" } @@ -5481,7 +5484,7 @@ "summary": "Updates the database password", "tags": ["Database"], "x-badges": [{ "name": "OAuth scope: database:write", "position": "after" }], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["database_config_write"]], "x-oauth-scope": "database:write" } @@ -5874,7 +5877,7 @@ "summary": "Get PostgREST OpenAPI spec", "tags": ["Database"], "x-badges": [{ "name": "OAuth scope: database:read", "position": "after" }], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["database_read"]], "x-oauth-scope": "database:read" } @@ -6450,7 +6453,7 @@ "security": [{ "bearer": [] }], "summary": "Get database disk attributes", "tags": ["Projects"], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["infra_disk_config_read"]] }, "post": { @@ -6486,7 +6489,7 @@ "security": [{ "bearer": [] }], "summary": "Modify database disk", "tags": ["Projects"], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["infra_disk_config_write"]] } }, @@ -6525,7 +6528,7 @@ "security": [{ "bearer": [] }], "summary": "Get disk utilization", "tags": ["Projects"], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["infra_disk_config_read"]] } }, @@ -6564,7 +6567,7 @@ "security": [{ "bearer": [] }], "summary": "Gets project disk autoscale config", "tags": ["Projects"], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["infra_disk_config_read"]] } }, @@ -6680,7 +6683,7 @@ "summary": "Get project's pgbouncer config", "tags": ["Database"], "x-badges": [{ "name": "OAuth scope: database:read", "position": "after" }], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["database_read"]], "x-oauth-scope": "database:read" } @@ -6812,7 +6815,7 @@ "summary": "Gets project's Postgres config", "tags": ["Database"], "x-badges": [{ "name": "OAuth scope: database:read", "position": "after" }], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["database_config_read"]], "x-oauth-scope": "database:read" }, @@ -6859,7 +6862,7 @@ "summary": "Updates project's Postgres config", "tags": ["Database"], "x-badges": [{ "name": "OAuth scope: database:write", "position": "after" }], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["database_config_write"]], "x-oauth-scope": "database:write" } @@ -7658,7 +7661,7 @@ "summary": "List members of an organization", "tags": ["Organizations"], "x-badges": [{ "name": "OAuth scope: organizations:read", "position": "after" }], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["members_read"]], "x-oauth-scope": "organizations:read" } @@ -7696,7 +7699,7 @@ "summary": "Gets information about the organization", "tags": ["Organizations"], "x-badges": [{ "name": "OAuth scope: organizations:read", "position": "after" }], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["organization_admin_read"]], "x-oauth-scope": "organizations:read" } @@ -7739,7 +7742,7 @@ "security": [{ "bearer": [] }], "summary": "Gets project details for the specified organization and claim token", "tags": ["Organizations"], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["organization_admin_write"]], "x-internal": true }, @@ -7773,7 +7776,7 @@ "security": [{ "bearer": [] }], "summary": "Claims project for the specified organization", "tags": ["Organizations"], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["organization_admin_write"]], "x-internal": true } @@ -7865,7 +7868,7 @@ "summary": "Gets all projects for the given organization", "tags": ["Projects"], "x-badges": [{ "name": "OAuth scope: projects:read", "position": "after" }], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["organization_projects_read"]], "x-oauth-scope": "projects:read" } @@ -13106,6 +13109,13 @@ "description": "Sets connection pool size for Realtime Authorization", "nullable": true }, + "postgres_changes_pool": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "Sets connection pool size used to create Postgres Changes subscriptions", + "nullable": true + }, "max_concurrent_users": { "type": "integer", "minimum": 1, @@ -13165,6 +13175,7 @@ "required": [ "private_only", "connection_pool", + "postgres_changes_pool", "max_concurrent_users", "max_events_per_second", "max_bytes_per_second", @@ -13189,6 +13200,12 @@ "maximum": 100, "description": "Sets connection pool size for Realtime Authorization" }, + "postgres_changes_pool": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "Sets connection pool size used to create Postgres Changes subscriptions" + }, "max_concurrent_users": { "type": "integer", "minimum": 1, @@ -13870,6 +13887,7 @@ "project_restore_after_expiry", "assistant.advance_model", "integrations.github_connections", + "integrations.github_push_webhooks_limit", "dedicated_pooler", "observability.dashboard_advanced_metrics", "api.members.invitations", diff --git a/apps/docs/spec/api_v2_openapi.json b/apps/docs/spec/api_v2_openapi.json index df7d06c6065..89fa22760c9 100644 --- a/apps/docs/spec/api_v2_openapi.json +++ b/apps/docs/spec/api_v2_openapi.json @@ -28,10 +28,30 @@ } } }, - "401": { "description": "Unauthorized" }, - "403": { "description": "Forbidden action" }, - "429": { "description": "Rate limit exceeded" }, - "500": { "description": "Failed to fetch log drains" } + "401": { + "description": "Unauthorized", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "403": { + "description": "Forbidden action", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "429": { + "description": "Rate limit exceeded", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "500": { + "description": "Failed to fetch log drains", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + } }, "security": [{ "bearer": [] }], "summary": "List project log drains", @@ -73,18 +93,36 @@ "application/json": { "schema": { "$ref": "#/components/schemas/LogDrainResponse" } } } }, - "401": { "description": "Unauthorized" }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, "402": { "description": "This feature requires the Pro, Team, or Enterprise organization plan.", "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/PlanGateErrorBodyV2" } - } + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } } }, - "403": { "description": "Forbidden action" }, - "429": { "description": "Rate limit exceeded" }, - "500": { "description": "Failed to create a log drain" } + "403": { + "description": "Forbidden action", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "429": { + "description": "Rate limit exceeded", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "500": { + "description": "Failed to create a log drain", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + } }, "security": [{ "bearer": [] }], "summary": "Create a log drain for a project", @@ -143,10 +181,30 @@ "application/json": { "schema": { "$ref": "#/components/schemas/LogDrainResponse" } } } }, - "401": { "description": "Unauthorized" }, - "403": { "description": "Forbidden action" }, - "429": { "description": "Rate limit exceeded" }, - "500": { "description": "Failed to update log drain" } + "401": { + "description": "Unauthorized", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "403": { + "description": "Forbidden action", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "429": { + "description": "Rate limit exceeded", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "500": { + "description": "Failed to update log drain", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + } }, "security": [{ "bearer": [] }], "summary": "Update a project log drain", @@ -186,10 +244,30 @@ ], "responses": { "204": { "description": "" }, - "401": { "description": "Unauthorized" }, - "403": { "description": "Forbidden action" }, - "429": { "description": "Rate limit exceeded" }, - "500": { "description": "Failed to delete a log drain" } + "401": { + "description": "Unauthorized", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "403": { + "description": "Forbidden action", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "429": { + "description": "Rate limit exceeded", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "500": { + "description": "Failed to delete a log drain", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + } }, "security": [{ "bearer": [] }], "summary": "Delete a project log drain", @@ -200,6 +278,71 @@ "x-oauth-scope": "analytics_config:write" } }, + "/v2/projects/{ref}/config": { + "get": { + "description": "Returns the project's database, pooler, Auth, Data API, Realtime and Storage configuration — the same configuration a branch inherits from its base project. Each is the effective config, so a setting the project has never overridden is reported at its platform default rather than as null. Auth secrets are returned as an HMAC of their value. `storage` is read live from the storage service; the rest come from this platform's own records.", + "operationId": "v2-get-project-config", + "parameters": [ + { + "name": "ref", + "required": true, + "in": "path", + "description": "Project ref", + "schema": { + "minLength": 20, + "maxLength": 20, + "pattern": "^[a-z]+$", + "example": "abcdefghijklmnopqrst", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/V2ProjectConfigResponse" } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "403": { + "description": "Forbidden action", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "429": { + "description": "Rate limit exceeded", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + } + }, + "security": [{ "bearer": [] }], + "summary": "[Alpha] Get a project's service configuration", + "tags": ["Projects"], + "x-endpoint-owners": ["control-plane", "infra"], + "x-fga-permissions": [ + [ + "database_config_read", + "database_read", + "database_ssl_config_read", + "database_network_restrictions_read", + "auth_config_read", + "data_api_config_read", + "realtime_config_read", + "storage_config_read" + ] + ] + } + }, "/v2/projects/{ref}/transfers/previews": { "post": { "operationId": "v2-preview-a-project-transfer", @@ -235,14 +378,29 @@ } } }, - "401": { "description": "Unauthorized" }, - "403": { "description": "Forbidden action" }, - "429": { "description": "Rate limit exceeded" } + "401": { + "description": "Unauthorized", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "403": { + "description": "Forbidden action", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "429": { + "description": "Rate limit exceeded", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + } }, "security": [{ "bearer": [] }], "summary": "Previews transferring a project to a different organizations, shows eligibility and impact", "tags": ["Projects"], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["project_admin_read"]] } }, @@ -274,14 +432,29 @@ }, "responses": { "200": { "description": "" }, - "401": { "description": "Unauthorized" }, - "403": { "description": "Forbidden action" }, - "429": { "description": "Rate limit exceeded" } + "401": { + "description": "Unauthorized", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "403": { + "description": "Forbidden action", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "429": { + "description": "Rate limit exceeded", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + } }, "security": [{ "bearer": [] }], "summary": "Transfers a project to a different organization", "tags": ["Projects"], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["organization_admin_write"]] } }, @@ -312,15 +485,35 @@ } } }, - "401": { "description": "Unauthorized" }, - "403": { "description": "Forbidden action" }, - "429": { "description": "Rate limit exceeded" }, - "500": { "description": "Failed to retrieve AWS accounts for project" } + "401": { + "description": "Unauthorized", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "403": { + "description": "Forbidden action", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "429": { + "description": "Rate limit exceeded", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "500": { + "description": "Failed to retrieve AWS accounts for project", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + } }, "security": [{ "bearer": [] }], "summary": "List AWS accounts attached to the project PrivateLink share", "tags": ["Projects"], - "x-endpoint-owners": ["platform-networking", "management-api"], + "x-endpoint-owners": ["platform-networking", "control-plane"], "x-fga-permissions": [["project_admin_read"]] }, "post": { @@ -358,25 +551,43 @@ } } }, - "401": { "description": "Unauthorized" }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, "402": { "description": "This feature requires the Team, or Enterprise organization plan.", "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/PlanGateErrorBodyV2" } - } + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } } }, - "403": { "description": "Forbidden action" }, - "429": { "description": "Rate limit exceeded" }, - "500": { "description": "Failed to add AWS account to PrivateLink share" } + "403": { + "description": "Forbidden action", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "429": { + "description": "Rate limit exceeded", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "500": { + "description": "Failed to add AWS account to PrivateLink share", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + } }, "security": [{ "bearer": [] }], "summary": "Add an AWS account to the project PrivateLink share", "tags": ["Projects"], "x-allowed-plans": ["Team", "Enterprise"], "x-badges": [{ "name": "Only available on Team, Enterprise", "position": "before" }], - "x-endpoint-owners": ["platform-networking", "management-api"], + "x-endpoint-owners": ["platform-networking", "control-plane"], "x-fga-permissions": [["project_admin_write"]] } }, @@ -408,15 +619,35 @@ ], "responses": { "204": { "description": "" }, - "401": { "description": "Unauthorized" }, - "403": { "description": "Forbidden action" }, - "429": { "description": "Rate limit exceeded" }, - "500": { "description": "Failed to remove AWS account from PrivateLink share" } + "401": { + "description": "Unauthorized", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "403": { + "description": "Forbidden action", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "429": { + "description": "Rate limit exceeded", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "500": { + "description": "Failed to remove AWS account from PrivateLink share", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + } }, "security": [{ "bearer": [] }], "summary": "Remove an AWS account from the project PrivateLink share", "tags": ["Projects"], - "x-endpoint-owners": ["platform-networking", "management-api"], + "x-endpoint-owners": ["platform-networking", "control-plane"], "x-fga-permissions": [["project_admin_write"]] } }, @@ -455,18 +686,353 @@ ], "responses": { "204": { "description": "" }, - "401": { "description": "Unauthorized" }, - "403": { "description": "Forbidden action" }, - "429": { "description": "Rate limit exceeded" }, - "500": { "description": "Failed to remove AWS account from PrivateLink share" } + "401": { + "description": "Unauthorized", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "403": { + "description": "Forbidden action", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "429": { + "description": "Rate limit exceeded", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "500": { + "description": "Failed to remove AWS account from PrivateLink share", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + } }, "security": [{ "bearer": [] }], "summary": "Remove an AWS account from a specific database PrivateLink share", "tags": ["Projects"], - "x-endpoint-owners": ["platform-networking", "management-api"], + "x-endpoint-owners": ["platform-networking", "control-plane"], "x-fga-permissions": [["project_admin_write"]] } }, + "/v2/projects/{ref}/workers": { + "get": { + "description": "Returns all workers you've previously deployed to the specified project.", + "operationId": "v2-list-all-workers", + "parameters": [ + { + "name": "ref", + "required": true, + "in": "path", + "description": "Project ref", + "schema": { + "minLength": 20, + "maxLength": 20, + "pattern": "^[a-z]+$", + "example": "abcdefghijklmnopqrst", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/V2ListWorkersResponse" } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "403": { + "description": "Forbidden action", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "429": { + "description": "Rate limit exceeded", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + } + }, + "security": [{ "bearer": [] }], + "summary": "[Alpha] List all workers", + "tags": ["Workers"], + "x-badges": [{ "name": "OAuth scope: edge_functions:read", "position": "after" }], + "x-endpoint-owners": ["functions"], + "x-fga-permissions": [["workers_read"]], + "x-oauth-scope": "edge_functions:read" + } + }, + "/v2/projects/{ref}/workers/{name}": { + "get": { + "description": "Returns a worker along with its instance tally. Poll this after a deploy until `build_state` leaves `building`.", + "operationId": "v2-get-a-worker", + "parameters": [ + { + "name": "ref", + "required": true, + "in": "path", + "description": "Project ref", + "schema": { + "minLength": 20, + "maxLength": 20, + "pattern": "^[a-z]+$", + "example": "abcdefghijklmnopqrst", + "type": "string" + } + }, + { + "name": "name", + "required": true, + "in": "path", + "schema": { + "pattern": "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$", + "example": "hello-world", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/V2WorkerResponse" } } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "403": { + "description": "Forbidden action", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "429": { + "description": "Rate limit exceeded", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + } + }, + "security": [{ "bearer": [] }], + "summary": "[Alpha] Retrieve a worker", + "tags": ["Workers"], + "x-badges": [{ "name": "OAuth scope: edge_functions:read", "position": "after" }], + "x-endpoint-owners": ["functions"], + "x-fga-permissions": [["workers_read"]], + "x-oauth-scope": "edge_functions:read" + }, + "delete": { + "description": "Tombstones the worker. Its instances and image are torn down asynchronously.", + "operationId": "v2-delete-a-worker", + "parameters": [ + { + "name": "ref", + "required": true, + "in": "path", + "description": "Project ref", + "schema": { + "minLength": 20, + "maxLength": 20, + "pattern": "^[a-z]+$", + "example": "abcdefghijklmnopqrst", + "type": "string" + } + }, + { + "name": "name", + "required": true, + "in": "path", + "schema": { + "pattern": "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$", + "example": "hello-world", + "type": "string" + } + } + ], + "responses": { + "204": { "description": "" }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "403": { + "description": "Forbidden action", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "429": { + "description": "Rate limit exceeded", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + } + }, + "security": [{ "bearer": [] }], + "summary": "[Alpha] Delete a worker", + "tags": ["Workers"], + "x-badges": [{ "name": "OAuth scope: edge_functions:write", "position": "after" }], + "x-endpoint-owners": ["functions"], + "x-fga-permissions": [["workers_write"]], + "x-oauth-scope": "edge_functions:write" + } + }, + "/v2/projects/{ref}/workers/{name}/uploads": { + "post": { + "description": "PUT the `.tar.gz` build context to the returned `url` before `expires_at`, then deploy with the upload id as `context_upload_id`. The bytes go straight to storage — no management API request carries them.", + "operationId": "v2-create-worker-upload", + "parameters": [ + { + "name": "ref", + "required": true, + "in": "path", + "description": "Project ref", + "schema": { + "minLength": 20, + "maxLength": 20, + "pattern": "^[a-z]+$", + "example": "abcdefghijklmnopqrst", + "type": "string" + } + }, + { + "name": "name", + "required": true, + "in": "path", + "schema": { + "pattern": "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$", + "example": "hello-world", + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/V2WorkerUploadResponse" } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "403": { + "description": "Forbidden action", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "429": { + "description": "Rate limit exceeded", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + } + }, + "security": [{ "bearer": [] }], + "summary": "[Alpha] Mint a presigned slot for a build-context upload", + "tags": ["Workers"], + "x-badges": [{ "name": "OAuth scope: edge_functions:write", "position": "after" }], + "x-endpoint-owners": ["functions"], + "x-fga-permissions": [["workers_write"]], + "x-oauth-scope": "edge_functions:write" + } + }, + "/v2/projects/{ref}/workers/{name}/deploy": { + "post": { + "description": "Creates the worker if it does not exist, building from a context staged through the uploads endpoint. The build runs asynchronously: this answers 202 and the worker reaches `build_state` `active` or `failed` later.", + "operationId": "v2-deploy-a-worker", + "parameters": [ + { + "name": "ref", + "required": true, + "in": "path", + "description": "Project ref", + "schema": { + "minLength": 20, + "maxLength": 20, + "pattern": "^[a-z]+$", + "example": "abcdefghijklmnopqrst", + "type": "string" + } + }, + { + "name": "name", + "required": true, + "in": "path", + "schema": { + "pattern": "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$", + "example": "hello-world", + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/V2DeployWorkerRequest" } + } + } + }, + "responses": { + "202": { + "description": "", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/V2WorkerResponse" } } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "403": { + "description": "Forbidden action", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "429": { + "description": "Rate limit exceeded", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + } + }, + "security": [{ "bearer": [] }], + "summary": "[Alpha] Deploy a worker", + "tags": ["Workers"], + "x-badges": [{ "name": "OAuth scope: edge_functions:write", "position": "after" }], + "x-endpoint-owners": ["functions"], + "x-fga-permissions": [["workers_write"]], + "x-oauth-scope": "edge_functions:write" + } + }, "/v2/organizations/{slug}/members": { "get": { "description": "Returns a cursor-paginated list of organization members including their roles and project-scoped permissions.", @@ -532,15 +1098,30 @@ } } }, - "401": { "description": "Unauthorized" }, - "403": { "description": "Forbidden action" }, - "429": { "description": "Rate limit exceeded" } + "401": { + "description": "Unauthorized", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "403": { + "description": "Forbidden action", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "429": { + "description": "Rate limit exceeded", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + } }, "security": [{ "bearer": [] }], "summary": "List members of an organization", "tags": ["Organizations"], "x-badges": [{ "name": "OAuth scope: organizations:read", "position": "after" }], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["members_read"]], "x-oauth-scope": "organizations:read" } @@ -589,25 +1170,43 @@ } } }, - "401": { "description": "Unauthorized" }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, "402": { "description": "This feature requires the Enterprise organization plan.", "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/PlanGateErrorBodyV2" } - } + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } } }, - "403": { "description": "Forbidden action" }, - "429": { "description": "Rate limit exceeded" }, - "500": { "description": "Failed to assign organization member role" } + "403": { + "description": "Forbidden action", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "429": { + "description": "Rate limit exceeded", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "500": { + "description": "Failed to assign organization member role", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + } }, "security": [{ "bearer": [] }], "summary": "Assign or change an organization member role", "tags": ["Organizations"], "x-allowed-plans": ["Enterprise"], "x-badges": [{ "name": "Only available on Enterprise", "position": "before" }], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["organization_admin_write"]] } }, @@ -637,15 +1236,30 @@ } } }, - "401": { "description": "Unauthorized" }, - "403": { "description": "Forbidden action" }, - "429": { "description": "Rate limit exceeded" } + "401": { + "description": "Unauthorized", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "403": { + "description": "Forbidden action", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "429": { + "description": "Rate limit exceeded", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + } }, "security": [{ "bearer": [] }], "summary": "List roles of an organization", "tags": ["Organizations"], "x-badges": [{ "name": "OAuth scope: organizations:read", "position": "after" }], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["members_read"]], "x-oauth-scope": "organizations:read" } @@ -684,17 +1298,30 @@ } } }, - "401": { "description": "Unauthorized" }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, "402": { "description": "This feature requires the Enterprise organization plan.", "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/PlanGateErrorBodyV2" } - } + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } } }, - "403": { "description": "Forbidden action" }, - "429": { "description": "Rate limit exceeded" } + "403": { + "description": "Forbidden action", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "429": { + "description": "Rate limit exceeded", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + } }, "security": [{ "bearer": [] }], "summary": "Creates organization invitations", @@ -704,7 +1331,7 @@ { "name": "OAuth scope: organizations:write", "position": "after" }, { "name": "Only available on Enterprise", "position": "before" } ], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["members_write"]], "x-oauth-scope": "organizations:write" }, @@ -741,17 +1368,30 @@ } } }, - "401": { "description": "Unauthorized" }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, "402": { "description": "This feature requires the Enterprise organization plan.", "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/PlanGateErrorBodyV2" } - } + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } } }, - "403": { "description": "Forbidden action" }, - "429": { "description": "Rate limit exceeded" } + "403": { + "description": "Forbidden action", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "429": { + "description": "Rate limit exceeded", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + } }, "security": [{ "bearer": [] }], "summary": "Deletes organization invitations by email", @@ -761,7 +1401,7 @@ { "name": "OAuth scope: organizations:write", "position": "after" }, { "name": "Only available on Enterprise", "position": "before" } ], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["members_write"]], "x-oauth-scope": "organizations:write" } @@ -824,15 +1464,30 @@ } } }, - "401": { "description": "Unauthorized" }, - "403": { "description": "Forbidden action" }, - "429": { "description": "Rate limit exceeded" } + "401": { + "description": "Unauthorized", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "403": { + "description": "Forbidden action", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "429": { + "description": "Rate limit exceeded", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + } }, "security": [{ "bearer": [] }], "summary": "List projects of an organization", "tags": ["Organizations"], "x-badges": [{ "name": "OAuth scope: projects:read", "position": "after" }], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["organization_projects_read"]], "x-oauth-scope": "projects:read" } @@ -910,22 +1565,37 @@ } } }, - "401": { "description": "Unauthorized" }, - "403": { "description": "Forbidden action" }, - "429": { "description": "Rate limit exceeded" } + "401": { + "description": "Unauthorized", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "403": { + "description": "Forbidden action", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + }, + "429": { + "description": "Rate limit exceeded", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponseBody" } } + } + } }, "security": [{ "bearer": [] }], "summary": "List GitHub connections of an organization", "tags": ["Organizations"], "x-badges": [{ "name": "OAuth scope: projects:read", "position": "after" }], - "x-endpoint-owners": ["management-api", "dev-workflows"], + "x-endpoint-owners": ["control-plane", "dev-workflows"], "x-fga-permissions": [["organization_projects_read"]], "x-oauth-scope": "projects:read" } }, "/v2/projects/{ref}/webhooks/endpoints": { "get": { - "operationId": "allV2ProjectsByRefWebhooks", + "operationId": "v2-projects-ref-webhooks-endpoints-get", "parameters": [ { "in": "path", @@ -1604,7 +2274,7 @@ "description": "List all Webhook endpoints based on a project's ref or an organization's slug." }, "post": { - "operationId": "allV2ProjectsByRefWebhooks", + "operationId": "v2-projects-ref-webhooks-endpoints-post", "parameters": [ { "in": "path", @@ -2341,7 +3011,7 @@ } }, "delete": { - "operationId": "allV2ProjectsByRefWebhooks", + "operationId": "v2-projects-ref-webhooks-endpoints-delete", "parameters": [ { "in": "path", @@ -2984,7 +3654,7 @@ }, "/v2/projects/{ref}/webhooks/endpoints/{id}": { "get": { - "operationId": "allV2ProjectsByRefWebhooks", + "operationId": "v2-projects-ref-webhooks-endpoints-id-get", "parameters": [ { "in": "path", @@ -3693,7 +4363,7 @@ "description": "Get details of a specific endpoint." }, "patch": { - "operationId": "allV2ProjectsByRefWebhooks", + "operationId": "v2-projects-ref-webhooks-endpoints-id-patch", "parameters": [ { "in": "path", @@ -4502,7 +5172,7 @@ } }, "delete": { - "operationId": "allV2ProjectsByRefWebhooks", + "operationId": "v2-projects-ref-webhooks-endpoints-id-delete", "parameters": [ { "in": "path", @@ -5213,7 +5883,7 @@ }, "/v2/projects/{ref}/webhooks/endpoints/{id}/deliveries": { "get": { - "operationId": "allV2ProjectsByRefWebhooks", + "operationId": "v2-projects-ref-webhooks-endpoints-id-deliveries-get", "parameters": [ { "in": "path", @@ -5952,7 +6622,7 @@ }, "/v2/projects/{ref}/webhooks/endpoints/{id}/test": { "post": { - "operationId": "allV2ProjectsByRefWebhooks", + "operationId": "v2-projects-ref-webhooks-endpoints-id-test-post", "parameters": [ { "in": "path", @@ -6744,7 +7414,7 @@ }, "/v2/projects/{ref}/webhooks/deliveries/{id}": { "get": { - "operationId": "allV2ProjectsByRefWebhooks", + "operationId": "v2-projects-ref-webhooks-deliveries-id-get", "parameters": [ { "in": "path", @@ -7515,7 +8185,7 @@ }, "/v2/projects/{ref}/webhooks/deliveries/{id}/retry": { "post": { - "operationId": "allV2ProjectsByRefWebhooks", + "operationId": "v2-projects-ref-webhooks-deliveries-id-retry-post", "parameters": [ { "in": "path", @@ -8121,7 +8791,7 @@ }, "/v2/organizations/{slug}/webhooks/endpoints": { "get": { - "operationId": "allV2OrganizationsBySlugWebhooks", + "operationId": "v2-organizations-slug-webhooks-endpoints-get", "parameters": [ { "in": "path", @@ -8798,7 +9468,7 @@ "description": "List all Webhook endpoints based on a project's ref or an organization's slug." }, "post": { - "operationId": "allV2OrganizationsBySlugWebhooks", + "operationId": "v2-organizations-slug-webhooks-endpoints-post", "parameters": [ { "in": "path", @@ -9533,7 +10203,7 @@ } }, "delete": { - "operationId": "allV2OrganizationsBySlugWebhooks", + "operationId": "v2-organizations-slug-webhooks-endpoints-delete", "parameters": [ { "in": "path", @@ -10174,7 +10844,7 @@ }, "/v2/organizations/{slug}/webhooks/endpoints/{id}": { "get": { - "operationId": "allV2OrganizationsBySlugWebhooks", + "operationId": "v2-organizations-slug-webhooks-endpoints-id-get", "parameters": [ { "in": "path", @@ -10881,7 +11551,7 @@ "description": "Get details of a specific endpoint." }, "patch": { - "operationId": "allV2OrganizationsBySlugWebhooks", + "operationId": "v2-organizations-slug-webhooks-endpoints-id-patch", "parameters": [ { "in": "path", @@ -11688,7 +12358,7 @@ } }, "delete": { - "operationId": "allV2OrganizationsBySlugWebhooks", + "operationId": "v2-organizations-slug-webhooks-endpoints-id-delete", "parameters": [ { "in": "path", @@ -12397,7 +13067,7 @@ }, "/v2/organizations/{slug}/webhooks/endpoints/{id}/deliveries": { "get": { - "operationId": "allV2OrganizationsBySlugWebhooks", + "operationId": "v2-organizations-slug-webhooks-endpoints-id-deliveries-get", "parameters": [ { "in": "path", @@ -13134,7 +13804,7 @@ }, "/v2/organizations/{slug}/webhooks/endpoints/{id}/test": { "post": { - "operationId": "allV2OrganizationsBySlugWebhooks", + "operationId": "v2-organizations-slug-webhooks-endpoints-id-test-post", "parameters": [ { "in": "path", @@ -13924,7 +14594,7 @@ }, "/v2/organizations/{slug}/webhooks/deliveries/{id}": { "get": { - "operationId": "allV2OrganizationsBySlugWebhooks", + "operationId": "v2-organizations-slug-webhooks-deliveries-id-get", "parameters": [ { "in": "path", @@ -14693,7 +15363,7 @@ }, "/v2/organizations/{slug}/webhooks/deliveries/{id}/retry": { "post": { - "operationId": "allV2OrganizationsBySlugWebhooks", + "operationId": "v2-organizations-slug-webhooks-deliveries-id-retry-post", "parameters": [ { "in": "path", @@ -15447,6 +16117,44 @@ }, "required": ["data"] }, + "ErrorResponseBodyAPIErrorObject": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "code": { "type": "string" }, + "message": { "type": "string" }, + "description": { "type": "string" }, + "links": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "href": { "type": "string" }, + "rel": { "type": "string" }, + "title": { "type": "string" }, + "type": { "type": "string" }, + "describedby": { "type": "string" }, + "meta": { "type": "object", "additionalProperties": {} } + }, + "required": ["href"] + } + }, + "meta": { "type": "object", "additionalProperties": {} }, + "issues": { + "type": "array", + "items": { "$ref": "#/components/schemas/ErrorResponseBodyAPIErrorObject" } + } + }, + "required": ["code", "message"], + "ref": "APIErrorObject" + }, + "ErrorResponseBody": { + "type": "object", + "properties": { + "error": { "$ref": "#/components/schemas/ErrorResponseBodyAPIErrorObject" } + }, + "required": ["error"] + }, "CreateLogDrainRequestOpenApi": { "type": "object", "properties": { @@ -15712,27 +16420,6 @@ }, "required": ["data"] }, - "PlanGateErrorBodyV2": { - "type": "object", - "properties": { - "error": { - "type": "object", - "properties": { - "code": { - "type": "string", - "description": "HTTP status-derived error code, e.g. \"payment_required\"" - }, - "message": { - "type": "string", - "description": "Human-readable explanation of the plan gate" - } - }, - "required": ["code", "message"], - "description": "Plan-gate error object" - } - }, - "required": ["error"] - }, "UpdateLogDrainRequestOpenApi": { "type": "object", "properties": { @@ -15865,6 +16552,437 @@ }, "required": ["data"] }, + "V2ProjectConfigResponse": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Resource type.", + "enum": ["project_config"] + }, + "id": { "type": "string", "description": "Project ref." }, + "attributes": { + "type": "object", + "properties": { + "database": { + "type": "object", + "properties": { + "major_version": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "description": "The major Postgres version the database runs. `17` covers both Postgres 17 and Oriole on 17, since Oriole is a storage engine rather than a version." + }, + "ssl_enforced": { + "type": "boolean", + "description": "Whether the database rejects plaintext connections" + }, + "network_restrictions": { + "type": "object", + "properties": { + "entitlement": { "type": "string", "enum": ["disallowed", "allowed"] }, + "status": { + "type": "string", + "enum": ["stored", "applied"], + "description": "Whether the allowlist below is applied to the project or only stored." + }, + "allowed_cidrs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "address": { "type": "string" }, + "type": { "type": "string", "enum": ["v4", "v6"] } + }, + "required": ["address", "type"] + } + }, + "updated_at": { "type": "string" }, + "applied_at": { "type": "string" } + }, + "required": ["entitlement", "status", "allowed_cidrs"] + }, + "postgres_settings": { + "type": "object", + "properties": { + "effective_cache_size": { "type": "string" }, + "logical_decoding_work_mem": { "type": "string" }, + "log_autovacuum_min_duration": { + "type": "string", + "description": "Default unit: ms", + "pattern": "^(-?[0-9]+(?:\\.[0-9]+)?)(us|ms|s|min|h|d)?$" + }, + "log_checkpoints": { "type": "boolean" }, + "log_connections": { "type": "boolean" }, + "log_disconnections": { "type": "boolean" }, + "log_duration": { "type": "boolean" }, + "log_lock_waits": { "type": "boolean" }, + "log_recovery_conflict_waits": { "type": "boolean" }, + "log_replication_commands": { "type": "boolean" }, + "log_startup_progress_interval": { + "type": "string", + "description": "Default unit: ms", + "pattern": "^(-?[0-9]+(?:\\.[0-9]+)?)(us|ms|s|min|h|d)?$" + }, + "log_temp_files": { "type": "string" }, + "maintenance_work_mem": { "type": "string" }, + "track_activity_query_size": { "type": "string" }, + "max_connections": { "type": "integer", "minimum": 1, "maximum": 262143 }, + "max_locks_per_transaction": { + "type": "integer", + "minimum": 10, + "maximum": 2147483640 + }, + "max_logical_replication_workers": { + "type": "integer", + "minimum": 0, + "maximum": 262143 + }, + "max_parallel_maintenance_workers": { + "type": "integer", + "minimum": 0, + "maximum": 1024 + }, + "max_parallel_workers": { + "type": "integer", + "minimum": 0, + "maximum": 1024 + }, + "max_parallel_workers_per_gather": { + "type": "integer", + "minimum": 0, + "maximum": 1024 + }, + "max_replication_slots": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "max_slot_wal_keep_size": { "type": "string" }, + "max_standby_archive_delay": { "type": "string" }, + "max_standby_streaming_delay": { "type": "string" }, + "max_sync_workers_per_subscription": { + "type": "integer", + "minimum": 0, + "maximum": 262143 + }, + "max_wal_size": { "type": "string" }, + "max_wal_senders": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "max_worker_processes": { + "type": "integer", + "minimum": 0, + "maximum": 262143 + }, + "session_replication_role": { + "type": "string", + "enum": ["origin", "replica", "local"] + }, + "shared_buffers": { "type": "string" }, + "statement_timeout": { + "type": "string", + "description": "Default unit: ms", + "pattern": "^(-?[0-9]+(?:\\.[0-9]+)?)(us|ms|s|min|h|d)?$" + }, + "track_commit_timestamp": { "type": "boolean" }, + "wal_keep_size": { "type": "string" }, + "wal_sender_timeout": { + "type": "string", + "description": "Default unit: ms", + "pattern": "^(-?[0-9]+(?:\\.[0-9]+)?)(us|ms|s|min|h|d)?$" + }, + "work_mem": { "type": "string" }, + "checkpoint_timeout": { + "type": "string", + "description": "Default unit: s", + "pattern": "^(-?[0-9]+(?:\\.[0-9]+)?)(us|ms|s|min|h|d)?$" + }, + "hot_standby_feedback": { "type": "boolean" }, + "cron_log_statement": { "type": "boolean" } + }, + "description": "Postgres parameter overrides. Empty when the project runs entirely on defaults." + } + }, + "required": [ + "major_version", + "ssl_enforced", + "network_restrictions", + "postgres_settings" + ] + }, + "pooler": { + "type": "object", + "properties": { + "pool_mode": { + "type": "string", + "enum": ["transaction", "session", "statement"] + }, + "ignore_startup_parameters": { "type": "string" }, + "server_idle_timeout": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "server_lifetime": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "query_wait_timeout": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "reserve_pool_size": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "default_pool_size": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "description": "Defaults to the pooler's size for the project's compute when not overridden." + }, + "max_client_conn": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "description": "Defaults to the pooler's size for the project's compute when not overridden." + } + }, + "required": [ + "pool_mode", + "ignore_startup_parameters", + "server_idle_timeout", + "server_lifetime", + "query_wait_timeout", + "reserve_pool_size", + "default_pool_size", + "max_client_conn" + ] + }, + "auth": { + "type": "object", + "additionalProperties": {}, + "description": "Effective Auth config, keyed by lowercased GoTrue setting name and resolved through the `gotrue_config` view, so a setting the project has never overridden is reported at its platform default. Secrets are returned as an HMAC of their value, never in plaintext." + }, + "api": { + "type": "object", + "properties": { + "db_schema": { + "type": "string", + "description": "Schemas exposed through the Data API" + }, + "db_extra_search_path": { "type": "string" }, + "max_rows": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "db_pool_acquisition_timeout": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "db_pool": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "description": "If `null`, no pool size is written to the project's PostgREST config and PostgREST's own default applies. The platform does not pick a value here.", + "nullable": true + } + }, + "required": [ + "db_schema", + "db_extra_search_path", + "max_rows", + "db_pool_acquisition_timeout", + "db_pool" + ] + }, + "realtime": { + "type": "object", + "properties": { + "private_only": { "type": "boolean" }, + "max_concurrent_users": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "max_events_per_second": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "max_bytes_per_second": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "max_channels_per_client": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "max_joins_per_second": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "max_presence_events_per_second": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "max_payload_size_in_kb": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "presence_enabled": { "type": "boolean" }, + "suspend": { "type": "boolean" }, + "connection_pool": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "description": "Defaults to Realtime's pool size for the project's compute when not overridden." + }, + "postgres_changes_pool": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "description": "If `null`, no override is stored and Realtime applies its own default.", + "nullable": true + } + }, + "required": [ + "private_only", + "max_concurrent_users", + "max_events_per_second", + "max_bytes_per_second", + "max_channels_per_client", + "max_joins_per_second", + "max_presence_events_per_second", + "max_payload_size_in_kb", + "presence_enabled", + "suspend", + "connection_pool", + "postgres_changes_pool" + ] + }, + "storage": { + "type": "object", + "properties": { + "file_size_limit": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "format": "int64" + }, + "features": { + "type": "object", + "properties": { + "image_transformation": { + "type": "object", + "properties": { "enabled": { "type": "boolean" } }, + "required": ["enabled"] + }, + "s3_protocol": { + "type": "object", + "properties": { "enabled": { "type": "boolean" } }, + "required": ["enabled"] + }, + "purge_cache": { + "type": "object", + "properties": { "enabled": { "type": "boolean" } }, + "required": ["enabled"] + }, + "iceberg_catalog": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "max_namespaces": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "max_tables": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "max_catalogs": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + } + }, + "required": ["enabled", "max_namespaces", "max_tables", "max_catalogs"] + }, + "vector_buckets": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "max_buckets": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "max_indexes": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + } + }, + "required": ["enabled", "max_buckets", "max_indexes"] + } + }, + "required": [ + "image_transformation", + "s3_protocol", + "purge_cache", + "iceberg_catalog", + "vector_buckets" + ] + }, + "capabilities": { + "type": "object", + "properties": { + "list_v2": { "type": "boolean" }, + "iceberg_catalog": { "type": "boolean" } + }, + "required": ["list_v2", "iceberg_catalog"] + }, + "upstream_target": { "type": "string", "enum": ["main", "canary"] }, + "migration_version": { "type": "string" }, + "database_pool_mode": { "type": "string" } + }, + "required": [ + "file_size_limit", + "features", + "capabilities", + "upstream_target", + "migration_version", + "database_pool_mode" + ], + "description": "Read from the storage service's admin API rather than the middleware DB, so unlike the rest of this resource it reflects the tenant's live config." + } + }, + "required": ["database", "pooler", "auth", "api", "realtime", "storage"] + } + }, + "required": ["type", "id", "attributes"] + } + }, + "required": ["data"] + }, "V2TransferProjectBody": { "type": "object", "properties": { @@ -15999,6 +17117,18 @@ "database_identifier": { "type": "string", "description": "Identifier of the database this PrivateLink share targets - the project ref for the primary, or the read replica identifier." + }, + "resource_access_manager_resource_config_id": { + "description": "ID of the AWS VPC Lattice resource configuration backing this PrivateLink share.", + "type": "string" + }, + "resource_access_manager_resource_config_arn": { + "description": "ARN of the AWS VPC Lattice resource configuration backing this PrivateLink share.", + "type": "string" + }, + "resource_access_manager_share_arn": { + "description": "ARN of the AWS Resource Access Manager resource share for this association.", + "type": "string" } }, "required": [ @@ -16108,6 +17238,18 @@ "database_identifier": { "type": "string", "description": "Identifier of the database this PrivateLink share targets - the project ref for the primary, or the read replica identifier." + }, + "resource_access_manager_resource_config_id": { + "description": "ID of the AWS VPC Lattice resource configuration backing this PrivateLink share.", + "type": "string" + }, + "resource_access_manager_resource_config_arn": { + "description": "ARN of the AWS VPC Lattice resource configuration backing this PrivateLink share.", + "type": "string" + }, + "resource_access_manager_share_arn": { + "description": "ARN of the AWS Resource Access Manager resource share for this association.", + "type": "string" } }, "required": [ @@ -16124,6 +17266,230 @@ }, "required": ["data"] }, + "V2ListWorkersResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Resource type.", + "enum": ["project_worker"] + }, + "id": { "type": "string", "description": "Worker name.", "example": "hello-world" }, + "attributes": { + "type": "object", + "properties": { + "spec": { + "type": "object", + "properties": { + "runtime": { "example": "node", "type": "string" }, + "size": { "type": "string", "example": "2gb-1vcpu" }, + "exposure": { "type": "string", "example": "public" }, + "instances": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "example": 1 + } + }, + "required": ["size", "exposure", "instances"] + }, + "build_state": { "type": "string", "enum": ["building", "active", "failed"] }, + "secret_generation": { "type": "string" }, + "state_reason": { "type": "string" }, + "image_version": { "type": "string" }, + "deleting": { "type": "boolean" }, + "instances": { + "type": "object", + "properties": { + "declared": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "live": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "ready": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "stale": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + } + }, + "required": ["declared", "live", "ready", "stale"] + }, + "instances_error": { "type": "string" } + }, + "required": ["spec", "build_state", "secret_generation"] + } + }, + "required": ["type", "id", "attributes"] + } + } + }, + "required": ["data"] + }, + "V2WorkerResponse": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Resource type.", + "enum": ["project_worker"] + }, + "id": { "type": "string", "description": "Worker name.", "example": "hello-world" }, + "attributes": { + "type": "object", + "properties": { + "spec": { + "type": "object", + "properties": { + "runtime": { "example": "node", "type": "string" }, + "size": { "type": "string", "example": "2gb-1vcpu" }, + "exposure": { "type": "string", "example": "public" }, + "instances": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "example": 1 + } + }, + "required": ["size", "exposure", "instances"] + }, + "build_state": { "type": "string", "enum": ["building", "active", "failed"] }, + "secret_generation": { "type": "string" }, + "state_reason": { "type": "string" }, + "image_version": { "type": "string" }, + "deleting": { "type": "boolean" }, + "instances": { + "type": "object", + "properties": { + "declared": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "live": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "ready": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "stale": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + } + }, + "required": ["declared", "live", "ready", "stale"] + }, + "instances_error": { "type": "string" } + }, + "required": ["spec", "build_state", "secret_generation"] + } + }, + "required": ["type", "id", "attributes"] + } + }, + "required": ["data"] + }, + "V2WorkerUploadResponse": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Resource type.", + "enum": ["project_worker_upload"] + }, + "id": { + "type": "string", + "description": "Upload id to pass to the deploy endpoint as `context_upload_id`.", + "example": "cafe0000000000000000000000000000" + }, + "attributes": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "Presigned destination for the `.tar.gz` build context." + }, + "method": { "type": "string", "example": "PUT" }, + "expires_at": { + "type": "string", + "description": "When the slot stops accepting the upload." + } + }, + "required": ["url", "method", "expires_at"] + } + }, + "required": ["type", "id", "attributes"] + } + }, + "required": ["data"] + }, + "V2DeployWorkerRequest": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Resource type.", + "enum": ["project_worker"] + }, + "attributes": { + "type": "object", + "properties": { + "spec": { + "type": "object", + "properties": { + "runtime": { "example": "node", "type": "string" }, + "size": { "type": "string", "example": "2gb-1vcpu" }, + "exposure": { "type": "string", "example": "public" }, + "instances": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "example": 1 + } + }, + "required": ["size", "exposure", "instances"] + }, + "context_upload_id": { + "description": "Id of a build context staged through the uploads endpoint. Required unless `runtime` is set.", + "type": "string" + } + }, + "required": ["spec"] + } + }, + "required": ["type", "attributes"] + } + }, + "required": ["data"] + }, "V2ListMembersResponse": { "type": "object", "properties": { @@ -16386,7 +17752,9 @@ "email": { "type": "string", "format": "email", - "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$" + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$", + "description": "Email address of the invitation receipient.", + "example": "hello@example.com" }, "role": { "type": "string", @@ -16396,6 +17764,7 @@ }, "projects": { "description": "The projects to limit a user to. If omitted, user will have org-wide access with the provided role.", + "examples": [{ "ref": "abcjuqabhgwjjutfvtpa" }], "minItems": 1, "type": "array", "items": { @@ -16477,7 +17846,9 @@ "email": { "type": "string", "format": "email", - "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$" + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$", + "description": "Email address of the invitation receipient.", + "example": "hello@example.com" } }, "required": ["email"] @@ -16505,7 +17876,9 @@ "email": { "type": "string", "format": "email", - "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$" + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$", + "description": "Email address of the invitation receipient.", + "example": "hello@example.com" } }, "required": ["email"] @@ -16538,7 +17911,9 @@ "email": { "type": "string", "format": "email", - "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$" + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$", + "description": "Email address of the invitation receipient.", + "example": "hello@example.com" } }, "required": ["email"] @@ -16569,7 +17944,9 @@ "email": { "type": "string", "format": "email", - "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$" + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$", + "description": "Email address of the invitation receipient.", + "example": "hello@example.com" } }, "required": ["email"] diff --git a/apps/docs/spec/common-api-sections.json b/apps/docs/spec/common-api-sections.json index 9421f19a16b..8fbecc4a95c 100644 --- a/apps/docs/spec/common-api-sections.json +++ b/apps/docs/spec/common-api-sections.json @@ -732,6 +732,72 @@ } ] }, + { + "type": "category", + "title": "Organization webhooks", + "items": [ + { + "id": "v2-organizations-slug-webhooks-deliveries-id-get", + "title": "Organizations slug webhooks deliveries id get", + "slug": "v2-organizations-slug-webhooks-deliveries-id-get", + "type": "operation" + }, + { + "id": "v2-organizations-slug-webhooks-deliveries-id-retry-post", + "title": "Organizations slug webhooks deliveries id retry post", + "slug": "v2-organizations-slug-webhooks-deliveries-id-retry-post", + "type": "operation" + }, + { + "id": "v2-organizations-slug-webhooks-endpoints-delete", + "title": "Organizations slug webhooks endpoints delete", + "slug": "v2-organizations-slug-webhooks-endpoints-delete", + "type": "operation" + }, + { + "id": "v2-organizations-slug-webhooks-endpoints-get", + "title": "Organizations slug webhooks endpoints get", + "slug": "v2-organizations-slug-webhooks-endpoints-get", + "type": "operation" + }, + { + "id": "v2-organizations-slug-webhooks-endpoints-id-delete", + "title": "Organizations slug webhooks endpoints id delete", + "slug": "v2-organizations-slug-webhooks-endpoints-id-delete", + "type": "operation" + }, + { + "id": "v2-organizations-slug-webhooks-endpoints-id-deliveries-get", + "title": "Organizations slug webhooks endpoints id deliveries get", + "slug": "v2-organizations-slug-webhooks-endpoints-id-deliveries-get", + "type": "operation" + }, + { + "id": "v2-organizations-slug-webhooks-endpoints-id-get", + "title": "Organizations slug webhooks endpoints id get", + "slug": "v2-organizations-slug-webhooks-endpoints-id-get", + "type": "operation" + }, + { + "id": "v2-organizations-slug-webhooks-endpoints-id-patch", + "title": "Organizations slug webhooks endpoints id patch", + "slug": "v2-organizations-slug-webhooks-endpoints-id-patch", + "type": "operation" + }, + { + "id": "v2-organizations-slug-webhooks-endpoints-id-test-post", + "title": "Organizations slug webhooks endpoints id test post", + "slug": "v2-organizations-slug-webhooks-endpoints-id-test-post", + "type": "operation" + }, + { + "id": "v2-organizations-slug-webhooks-endpoints-post", + "title": "Organizations slug webhooks endpoints post", + "slug": "v2-organizations-slug-webhooks-endpoints-post", + "type": "operation" + } + ] + }, { "type": "category", "title": "Organizations", @@ -828,6 +894,72 @@ } ] }, + { + "type": "category", + "title": "Project webhooks", + "items": [ + { + "id": "v2-projects-ref-webhooks-deliveries-id-get", + "title": "Projects ref webhooks deliveries id get", + "slug": "v2-projects-ref-webhooks-deliveries-id-get", + "type": "operation" + }, + { + "id": "v2-projects-ref-webhooks-deliveries-id-retry-post", + "title": "Projects ref webhooks deliveries id retry post", + "slug": "v2-projects-ref-webhooks-deliveries-id-retry-post", + "type": "operation" + }, + { + "id": "v2-projects-ref-webhooks-endpoints-delete", + "title": "Projects ref webhooks endpoints delete", + "slug": "v2-projects-ref-webhooks-endpoints-delete", + "type": "operation" + }, + { + "id": "v2-projects-ref-webhooks-endpoints-get", + "title": "Projects ref webhooks endpoints get", + "slug": "v2-projects-ref-webhooks-endpoints-get", + "type": "operation" + }, + { + "id": "v2-projects-ref-webhooks-endpoints-id-delete", + "title": "Projects ref webhooks endpoints id delete", + "slug": "v2-projects-ref-webhooks-endpoints-id-delete", + "type": "operation" + }, + { + "id": "v2-projects-ref-webhooks-endpoints-id-deliveries-get", + "title": "Projects ref webhooks endpoints id deliveries get", + "slug": "v2-projects-ref-webhooks-endpoints-id-deliveries-get", + "type": "operation" + }, + { + "id": "v2-projects-ref-webhooks-endpoints-id-get", + "title": "Projects ref webhooks endpoints id get", + "slug": "v2-projects-ref-webhooks-endpoints-id-get", + "type": "operation" + }, + { + "id": "v2-projects-ref-webhooks-endpoints-id-patch", + "title": "Projects ref webhooks endpoints id patch", + "slug": "v2-projects-ref-webhooks-endpoints-id-patch", + "type": "operation" + }, + { + "id": "v2-projects-ref-webhooks-endpoints-id-test-post", + "title": "Projects ref webhooks endpoints id test post", + "slug": "v2-projects-ref-webhooks-endpoints-id-test-post", + "type": "operation" + }, + { + "id": "v2-projects-ref-webhooks-endpoints-post", + "title": "Projects ref webhooks endpoints post", + "slug": "v2-projects-ref-webhooks-endpoints-post", + "type": "operation" + } + ] + }, { "type": "category", "title": "Projects", @@ -922,6 +1054,12 @@ "slug": "v1-get-project", "type": "operation" }, + { + "id": "v2-get-project-config", + "title": "Get project config", + "slug": "v2-get-project-config", + "type": "operation" + }, { "id": "v1-get-project-disk-autoscale-config", "title": "Get project disk autoscale config", @@ -1169,5 +1307,41 @@ "type": "operation" } ] + }, + { + "type": "category", + "title": "Workers", + "items": [ + { + "id": "v2-create-worker-upload", + "title": "Create worker upload", + "slug": "v2-create-worker-upload", + "type": "operation" + }, + { + "id": "v2-delete-a-worker", + "title": "Delete a worker", + "slug": "v2-delete-a-worker", + "type": "operation" + }, + { + "id": "v2-deploy-a-worker", + "title": "Deploy a worker", + "slug": "v2-deploy-a-worker", + "type": "operation" + }, + { + "id": "v2-get-a-worker", + "title": "Get a worker", + "slug": "v2-get-a-worker", + "type": "operation" + }, + { + "id": "v2-list-all-workers", + "title": "List all workers", + "slug": "v2-list-all-workers", + "type": "operation" + } + ] } ] diff --git a/apps/docs/spec/transforms/api_v1_openapi_deparsed.json b/apps/docs/spec/transforms/api_v1_openapi_deparsed.json index 15a255feb54..831388c88cf 100644 --- a/apps/docs/spec/transforms/api_v1_openapi_deparsed.json +++ b/apps/docs/spec/transforms/api_v1_openapi_deparsed.json @@ -685,7 +685,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["projects_read"]], "x-oauth-scope": "projects:read" }, @@ -736,7 +736,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["organization_projects_create"]], "x-oauth-scope": "projects:write" } @@ -871,7 +871,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["organizations_read"]], "x-oauth-scope": "organizations:read" }, @@ -919,7 +919,7 @@ ], "summary": "Create an organization", "tags": ["Organizations"], - "x-endpoint-owners": ["management-api", "billing"], + "x-endpoint-owners": ["control-plane", "billing"], "x-fga-permissions": [["organizations_create"]] } }, @@ -1041,7 +1041,7 @@ }, "summary": "[Beta] Authorize user through oauth", "tags": ["OAuth"], - "x-endpoint-owners": ["auth", "management-api"] + "x-endpoint-owners": ["auth", "control-plane"] } }, "/v1/oauth/token": { @@ -1073,7 +1073,7 @@ }, "summary": "[Beta] Exchange auth code for user's access and refresh token", "tags": ["OAuth"], - "x-endpoint-owners": ["auth", "management-api"] + "x-endpoint-owners": ["auth", "control-plane"] } }, "/v1/oauth/revoke": { @@ -1097,7 +1097,7 @@ }, "summary": "[Beta] Revoke oauth app authorization and it's corresponding tokens", "tags": ["OAuth"], - "x-endpoint-owners": ["auth", "management-api"] + "x-endpoint-owners": ["auth", "control-plane"] } }, "/v1/oauth/authorize/project-claim": { @@ -1207,7 +1207,7 @@ ], "summary": "Authorize user through oauth and claim a project", "tags": ["OAuth"], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["organization_admin_write", "project_admin_write"]] } }, @@ -1302,7 +1302,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["snippets_read"]], "x-oauth-scope": "database:read" } @@ -1360,7 +1360,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["snippets_read"]], "x-oauth-scope": "database:read" } @@ -1388,7 +1388,7 @@ ], "summary": "Gets the user's profile", "tags": ["Profile"], - "x-endpoint-owners": ["management-api"] + "x-endpoint-owners": ["control-plane"] } }, "/v1/projects/{ref}/actions": { @@ -1821,7 +1821,7 @@ "position": "after" } ], - "x-endpoint-owners": ["auth", "management-api"], + "x-endpoint-owners": ["auth", "control-plane"], "x-fga-permissions": [["api_gateway_keys_read"]], "x-oauth-scope": "secrets:read" }, @@ -1896,7 +1896,7 @@ "position": "after" } ], - "x-endpoint-owners": ["auth", "management-api"], + "x-endpoint-owners": ["auth", "control-plane"], "x-fga-permissions": [["api_gateway_keys_write"]], "x-oauth-scope": "secrets:write" } @@ -1953,7 +1953,7 @@ "position": "after" } ], - "x-endpoint-owners": ["auth", "management-api"], + "x-endpoint-owners": ["auth", "control-plane"], "x-fga-permissions": [["api_gateway_keys_read"]], "x-oauth-scope": "secrets:read" }, @@ -2018,7 +2018,7 @@ "position": "after" } ], - "x-endpoint-owners": ["auth", "management-api"], + "x-endpoint-owners": ["auth", "control-plane"], "x-fga-permissions": [["api_gateway_keys_write"]], "x-oauth-scope": "secrets:write" } @@ -2106,7 +2106,7 @@ "position": "after" } ], - "x-endpoint-owners": ["auth", "management-api"], + "x-endpoint-owners": ["auth", "control-plane"], "x-fga-permissions": [["api_gateway_keys_write"]], "x-oauth-scope": "secrets:write" }, @@ -2182,7 +2182,7 @@ "position": "after" } ], - "x-endpoint-owners": ["auth", "management-api"], + "x-endpoint-owners": ["auth", "control-plane"], "x-fga-permissions": [["api_gateway_keys_read"]], "x-oauth-scope": "secrets:read" }, @@ -2277,7 +2277,7 @@ "position": "after" } ], - "x-endpoint-owners": ["auth", "management-api"], + "x-endpoint-owners": ["auth", "control-plane"], "x-fga-permissions": [["api_gateway_keys_write"]], "x-oauth-scope": "secrets:write" } @@ -2565,7 +2565,7 @@ "position": "after" } ], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["custom_domain_read"]], "x-oauth-scope": "domains:read" }, @@ -2625,7 +2625,7 @@ "position": "after" } ], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["custom_domain_write"]], "x-oauth-scope": "domains:write" } @@ -2695,7 +2695,7 @@ "position": "after" } ], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["custom_domain_write"]], "x-oauth-scope": "domains:write" } @@ -2755,7 +2755,7 @@ "position": "after" } ], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["custom_domain_write"]], "x-oauth-scope": "domains:write" } @@ -2815,7 +2815,7 @@ "position": "after" } ], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["custom_domain_write"]], "x-oauth-scope": "domains:write" } @@ -2910,7 +2910,7 @@ "position": "after" } ], - "x-endpoint-owners": ["security", "management-api"], + "x-endpoint-owners": ["security", "control-plane"], "x-fga-permissions": [["project_admin_read"]], "x-oauth-scope": "database:read" }, @@ -3013,7 +3013,7 @@ "position": "after" } ], - "x-endpoint-owners": ["security", "management-api"], + "x-endpoint-owners": ["security", "control-plane"], "x-fga-permissions": [["project_admin_write"]], "x-oauth-scope": "database:write" } @@ -3073,7 +3073,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["database_network_bans_read"]], "x-oauth-scope": "projects:read" } @@ -3133,7 +3133,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["database_network_bans_read"]], "x-oauth-scope": "projects:read" } @@ -3196,7 +3196,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["database_network_bans_write"]], "x-oauth-scope": "projects:write" } @@ -3256,7 +3256,7 @@ "position": "after" } ], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["database_network_restrictions_read"]], "x-oauth-scope": "projects:read" }, @@ -3324,7 +3324,7 @@ "position": "after" } ], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["database_network_restrictions_write"]], "x-oauth-scope": "projects:write" } @@ -3394,7 +3394,7 @@ "position": "after" } ], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["database_network_restrictions_write"]], "x-oauth-scope": "projects:write" } @@ -3582,7 +3582,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["data_api_config_read"]], "x-oauth-scope": "rest:read" }, @@ -3650,7 +3650,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["data_api_config_write"]], "x-oauth-scope": "rest:write" } @@ -3710,7 +3710,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["project_admin_read"]], "x-oauth-scope": "projects:read" }, @@ -3765,7 +3765,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api", "infra", "dev-workflows"], + "x-endpoint-owners": ["control-plane", "infra", "dev-workflows"], "x-fga-permissions": [["project_admin_write"]], "x-oauth-scope": "projects:write" }, @@ -3833,7 +3833,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["project_admin_write"]], "x-oauth-scope": "projects:write" } @@ -4081,7 +4081,7 @@ "position": "after" } ], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["database_ssl_config_read"]], "x-oauth-scope": "database:read" }, @@ -4149,7 +4149,7 @@ "position": "after" } ], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["database_ssl_config_write"]], "x-oauth-scope": "database:write" } @@ -4295,7 +4295,7 @@ "position": "before" } ], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["vanity_subdomain_read"]], "x-oauth-scope": "domains:read" }, @@ -4346,7 +4346,7 @@ "position": "after" } ], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["vanity_subdomain_write"]], "x-oauth-scope": "domains:write" } @@ -4431,7 +4431,7 @@ "position": "before" } ], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["vanity_subdomain_write"]], "x-oauth-scope": "domains:write" } @@ -4516,7 +4516,7 @@ "position": "before" } ], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["vanity_subdomain_write"]], "x-oauth-scope": "domains:write" } @@ -4586,7 +4586,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["project_admin_write", "database_write"]], "x-oauth-scope": "projects:write" } @@ -4646,7 +4646,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["project_admin_read", "database_read"]], "x-oauth-scope": "projects:read" } @@ -4715,7 +4715,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["project_admin_read", "database_read"]], "x-oauth-scope": "projects:read" } @@ -4775,7 +4775,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api", "infra", "support-tooling"], + "x-endpoint-owners": ["control-plane", "infra", "support-tooling"], "x-fga-permissions": [["database_readonly_config_read"]], "x-oauth-scope": "database:read" } @@ -4828,7 +4828,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api", "infra", "support-tooling"], + "x-endpoint-owners": ["control-plane", "infra", "support-tooling"], "x-fga-permissions": [["database_readonly_config_write"]], "x-oauth-scope": "database:write" } @@ -4902,7 +4902,7 @@ "position": "before" } ], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["infra_read_replicas_write"]] } }, @@ -4958,7 +4958,7 @@ ], "summary": "[Beta] Remove a read replica", "tags": ["Database"], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["infra_read_replicas_write"]] } }, @@ -5065,7 +5065,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["project_admin_read"]], "x-oauth-scope": "projects:read" } @@ -5939,7 +5939,7 @@ "position": "after" } ], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["project_admin_write"]], "x-oauth-scope": "projects:write" } @@ -5989,7 +5989,7 @@ "position": "after" } ], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["project_admin_write"]], "x-oauth-scope": "projects:write" } @@ -6046,7 +6046,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["project_admin_read"]], "x-oauth-scope": "projects:read" }, @@ -6094,7 +6094,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["project_admin_write"]], "x-oauth-scope": "projects:write" } @@ -6144,7 +6144,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["project_admin_write"]], "x-oauth-scope": "projects:write" } @@ -6396,7 +6396,7 @@ ], "summary": "Gets project claim token", "tags": ["Projects"], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["project_admin_read"]], "x-internal": true }, @@ -6445,7 +6445,7 @@ ], "summary": "Creates project claim token", "tags": ["Projects"], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["organization_admin_write", "project_admin_write"]], "x-internal": true }, @@ -6487,7 +6487,7 @@ ], "summary": "Revokes project claim token", "tags": ["Projects"], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["organization_admin_write", "project_admin_write"]], "x-internal": true } @@ -6546,7 +6546,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["advisors_read"]], "x-oauth-scope": "database:read" } @@ -6615,7 +6615,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["advisors_read"]], "x-oauth-scope": "database:read" } @@ -6623,7 +6623,7 @@ "/v1/projects/{ref}/analytics/endpoints/logs.all": { "get": { "deprecated": true, - "description": "Executes a SQL query on the project's logs.\n\nEither the `iso_timestamp_start` and `iso_timestamp_end` parameters must be provided.\nIf both are not provided, only the last 1 minute of logs will be queried.\nThe timestamp range must be no more than 24 hours and is rounded to the nearest minute. If the range is more than 24 hours, a validation error will be thrown.\n\nNote: Unless the `sql` parameter is provided, only edge_logs will be queried. See the [log query docs](/docs/guides/telemetry/logs?queryGroups=product&product=postgres&queryGroups=source&source=edge_logs#querying-with-the-logs-explorer:~:text=logs%20from%20the-,Sources,-drop%2Ddown%3A) for all available sources.\n", + "description": "Executes a SQL query on the project's logs.\n\nEither the `iso_timestamp_start` and `iso_timestamp_end` parameters must be provided.\nIf both are not provided, only the last 1 minute of logs will be queried.\nThe timestamp range must be no more than 24 hours and is rounded to the nearest minute. If the range is more than 24 hours, a validation error will be thrown.\n\nNote: Unless the `sql` parameter is provided, only edge_logs will be queried. See the [log query docs](https://supabase.com/docs/guides/monitoring-and-debugging/logs#logs-explorer) for all available sources.\n", "operationId": "v1-get-project-logs-all", "parameters": [ { @@ -6643,7 +6643,7 @@ "name": "sql", "required": false, "in": "query", - "description": "Custom SQL query to execute on the logs. See [querying logs](/docs/guides/telemetry/logs?queryGroups=product&product=postgres&queryGroups=source&source=edge_logs#querying-with-the-logs-explorer) for more details.", + "description": "Custom SQL query to execute on the logs. See [querying logs](https://supabase.com/docs/guides/monitoring-and-debugging/logs#querying-with-the-logs-explorer) for more details.", "schema": { "example": "select event_message from edge_logs limit 10", "type": "string" @@ -6737,7 +6737,7 @@ "name": "sql", "required": false, "in": "query", - "description": "Custom SQL query to execute on the logs. See [querying logs](/docs/guides/telemetry/logs?queryGroups=product&product=postgres&queryGroups=source&source=edge_logs#querying-with-the-logs-explorer) for more details.", + "description": "Custom SQL query to execute on the logs. See [querying logs](https://supabase.com/docs/guides/monitoring-and-debugging/logs#querying-with-the-logs-explorer) for more details.", "schema": { "example": "select event_message from edge_logs limit 10", "type": "string" @@ -7031,6 +7031,9 @@ } } }, + "400": { + "description": "Project must be active and healthy, or metrics are not available for this project" + }, "401": { "description": "Unauthorized" }, @@ -7651,7 +7654,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["database_read"], ["database_write"]], "x-oauth-scope": "database:write" } @@ -7715,7 +7718,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["database_read"]], "x-oauth-scope": "database:read" } @@ -7768,7 +7771,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["database_webhooks_config_write"]], "x-oauth-scope": "database:write" } @@ -7827,7 +7830,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["database_read"]], "x-oauth-scope": "projects:read" } @@ -7897,7 +7900,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["database_config_write"]], "x-oauth-scope": "database:write" } @@ -8448,7 +8451,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["database_read"]], "x-oauth-scope": "database:read" } @@ -9294,7 +9297,7 @@ ], "summary": "Get database disk attributes", "tags": ["Projects"], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["infra_disk_config_read"]] }, "post": { @@ -9348,7 +9351,7 @@ ], "summary": "Modify database disk", "tags": ["Projects"], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["infra_disk_config_write"]] } }, @@ -9401,7 +9404,7 @@ ], "summary": "Get disk utilization", "tags": ["Projects"], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["infra_disk_config_read"]] } }, @@ -9454,7 +9457,7 @@ ], "summary": "Gets project disk autoscale config", "tags": ["Projects"], - "x-endpoint-owners": ["management-api", "infra"], + "x-endpoint-owners": ["control-plane", "infra"], "x-fga-permissions": [["infra_disk_config_read"]] } }, @@ -9615,7 +9618,7 @@ "position": "after" } ], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["database_read"]], "x-oauth-scope": "database:read" } @@ -9806,7 +9809,7 @@ "position": "after" } ], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["database_config_read"]], "x-oauth-scope": "database:read" }, @@ -9874,7 +9877,7 @@ "position": "after" } ], - "x-endpoint-owners": ["infra", "management-api"], + "x-endpoint-owners": ["infra", "control-plane"], "x-fga-permissions": [["database_config_write"]], "x-oauth-scope": "database:write" } @@ -11019,7 +11022,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["members_read"]], "x-oauth-scope": "organizations:read" } @@ -11074,7 +11077,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["organization_admin_read"]], "x-oauth-scope": "organizations:read" } @@ -11132,7 +11135,7 @@ ], "summary": "Gets project details for the specified organization and claim token", "tags": ["Organizations"], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["organization_admin_write"]], "x-internal": true }, @@ -11181,7 +11184,7 @@ ], "summary": "Claims project for the specified organization", "tags": ["Organizations"], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["organization_admin_write"]], "x-internal": true } @@ -11298,7 +11301,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["organization_projects_read"]], "x-oauth-scope": "projects:read" } @@ -19487,6 +19490,13 @@ "description": "Sets connection pool size for Realtime Authorization", "nullable": true }, + "postgres_changes_pool": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "Sets connection pool size used to create Postgres Changes subscriptions", + "nullable": true + }, "max_concurrent_users": { "type": "integer", "minimum": 1, @@ -19549,6 +19559,7 @@ "required": [ "private_only", "connection_pool", + "postgres_changes_pool", "max_concurrent_users", "max_events_per_second", "max_bytes_per_second", @@ -19573,6 +19584,12 @@ "maximum": 100, "description": "Sets connection pool size for Realtime Authorization" }, + "postgres_changes_pool": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "Sets connection pool size used to create Postgres Changes subscriptions" + }, "max_concurrent_users": { "type": "integer", "minimum": 1, @@ -20536,6 +20553,7 @@ "project_restore_after_expiry", "assistant.advance_model", "integrations.github_connections", + "integrations.github_push_webhooks_limit", "dedicated_pooler", "observability.dashboard_advanced_metrics", "api.members.invitations", diff --git a/apps/docs/spec/transforms/api_v2_openapi_deparsed.json b/apps/docs/spec/transforms/api_v2_openapi_deparsed.json index c99d176c4b0..6a476ef664c 100644 --- a/apps/docs/spec/transforms/api_v2_openapi_deparsed.json +++ b/apps/docs/spec/transforms/api_v2_openapi_deparsed.json @@ -39,16 +39,44 @@ } }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "403": { - "description": "Forbidden action" + "description": "Forbidden action", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "429": { - "description": "Rate limit exceeded" + "description": "Rate limit exceeded", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "500": { - "description": "Failed to fetch log drains" + "description": "Failed to fetch log drains", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } } }, "security": [ @@ -107,26 +135,54 @@ } }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "402": { "description": "This feature requires the Pro, Team, or Enterprise organization plan.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PlanGateErrorBodyV2" + "$ref": "#/components/schemas/ErrorResponseBody" } } } }, "403": { - "description": "Forbidden action" + "description": "Forbidden action", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "429": { - "description": "Rate limit exceeded" + "description": "Rate limit exceeded", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "500": { - "description": "Failed to create a log drain" + "description": "Failed to create a log drain", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } } }, "security": [ @@ -203,16 +259,44 @@ } }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "403": { - "description": "Forbidden action" + "description": "Forbidden action", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "429": { - "description": "Rate limit exceeded" + "description": "Rate limit exceeded", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "500": { - "description": "Failed to update log drain" + "description": "Failed to update log drain", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } } }, "security": [ @@ -265,16 +349,44 @@ "description": "" }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "403": { - "description": "Forbidden action" + "description": "Forbidden action", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "429": { - "description": "Rate limit exceeded" + "description": "Rate limit exceeded", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "500": { - "description": "Failed to delete a log drain" + "description": "Failed to delete a log drain", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } } }, "security": [ @@ -295,6 +407,89 @@ "x-oauth-scope": "analytics_config:write" } }, + "/v2/projects/{ref}/config": { + "get": { + "description": "Returns the project's database, pooler, Auth, Data API, Realtime and Storage configuration — the same configuration a branch inherits from its base project. Each is the effective config, so a setting the project has never overridden is reported at its platform default rather than as null. Auth secrets are returned as an HMAC of their value. `storage` is read live from the storage service; the rest come from this platform's own records.", + "operationId": "v2-get-project-config", + "parameters": [ + { + "name": "ref", + "required": true, + "in": "path", + "description": "Project ref", + "schema": { + "minLength": 20, + "maxLength": 20, + "pattern": "^[a-z]+$", + "example": "abcdefghijklmnopqrst", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/V2ProjectConfigResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } + }, + "403": { + "description": "Forbidden action", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } + }, + "429": { + "description": "Rate limit exceeded", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } + } + }, + "security": [ + { + "bearer": [] + } + ], + "summary": "[Alpha] Get a project's service configuration", + "tags": ["Projects"], + "x-endpoint-owners": ["control-plane", "infra"], + "x-fga-permissions": [ + [ + "database_config_read", + "database_read", + "database_ssl_config_read", + "database_network_restrictions_read", + "auth_config_read", + "data_api_config_read", + "realtime_config_read", + "storage_config_read" + ] + ] + } + }, "/v2/projects/{ref}/transfers/previews": { "post": { "operationId": "v2-preview-a-project-transfer", @@ -335,13 +530,34 @@ } }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "403": { - "description": "Forbidden action" + "description": "Forbidden action", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "429": { - "description": "Rate limit exceeded" + "description": "Rate limit exceeded", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } } }, "security": [ @@ -351,7 +567,7 @@ ], "summary": "Previews transferring a project to a different organizations, shows eligibility and impact", "tags": ["Projects"], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["project_admin_read"]] } }, @@ -388,13 +604,34 @@ "description": "" }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "403": { - "description": "Forbidden action" + "description": "Forbidden action", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "429": { - "description": "Rate limit exceeded" + "description": "Rate limit exceeded", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } } }, "security": [ @@ -404,7 +641,7 @@ ], "summary": "Transfers a project to a different organization", "tags": ["Projects"], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["organization_admin_write"]] } }, @@ -438,16 +675,44 @@ } }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "403": { - "description": "Forbidden action" + "description": "Forbidden action", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "429": { - "description": "Rate limit exceeded" + "description": "Rate limit exceeded", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "500": { - "description": "Failed to retrieve AWS accounts for project" + "description": "Failed to retrieve AWS accounts for project", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } } }, "security": [ @@ -457,7 +722,7 @@ ], "summary": "List AWS accounts attached to the project PrivateLink share", "tags": ["Projects"], - "x-endpoint-owners": ["platform-networking", "management-api"], + "x-endpoint-owners": ["platform-networking", "control-plane"], "x-fga-permissions": [["project_admin_read"]] }, "post": { @@ -500,26 +765,54 @@ } }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "402": { "description": "This feature requires the Team, or Enterprise organization plan.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PlanGateErrorBodyV2" + "$ref": "#/components/schemas/ErrorResponseBody" } } } }, "403": { - "description": "Forbidden action" + "description": "Forbidden action", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "429": { - "description": "Rate limit exceeded" + "description": "Rate limit exceeded", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "500": { - "description": "Failed to add AWS account to PrivateLink share" + "description": "Failed to add AWS account to PrivateLink share", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } } }, "security": [ @@ -536,7 +829,7 @@ "position": "before" } ], - "x-endpoint-owners": ["platform-networking", "management-api"], + "x-endpoint-owners": ["platform-networking", "control-plane"], "x-fga-permissions": [["project_admin_write"]] } }, @@ -573,16 +866,44 @@ "description": "" }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "403": { - "description": "Forbidden action" + "description": "Forbidden action", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "429": { - "description": "Rate limit exceeded" + "description": "Rate limit exceeded", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "500": { - "description": "Failed to remove AWS account from PrivateLink share" + "description": "Failed to remove AWS account from PrivateLink share", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } } }, "security": [ @@ -592,7 +913,7 @@ ], "summary": "Remove an AWS account from the project PrivateLink share", "tags": ["Projects"], - "x-endpoint-owners": ["platform-networking", "management-api"], + "x-endpoint-owners": ["platform-networking", "control-plane"], "x-fga-permissions": [["project_admin_write"]] } }, @@ -638,16 +959,44 @@ "description": "" }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "403": { - "description": "Forbidden action" + "description": "Forbidden action", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "429": { - "description": "Rate limit exceeded" + "description": "Rate limit exceeded", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "500": { - "description": "Failed to remove AWS account from PrivateLink share" + "description": "Failed to remove AWS account from PrivateLink share", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } } }, "security": [ @@ -657,10 +1006,446 @@ ], "summary": "Remove an AWS account from a specific database PrivateLink share", "tags": ["Projects"], - "x-endpoint-owners": ["platform-networking", "management-api"], + "x-endpoint-owners": ["platform-networking", "control-plane"], "x-fga-permissions": [["project_admin_write"]] } }, + "/v2/projects/{ref}/workers": { + "get": { + "description": "Returns all workers you've previously deployed to the specified project.", + "operationId": "v2-list-all-workers", + "parameters": [ + { + "name": "ref", + "required": true, + "in": "path", + "description": "Project ref", + "schema": { + "minLength": 20, + "maxLength": 20, + "pattern": "^[a-z]+$", + "example": "abcdefghijklmnopqrst", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/V2ListWorkersResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } + }, + "403": { + "description": "Forbidden action", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } + }, + "429": { + "description": "Rate limit exceeded", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } + } + }, + "security": [ + { + "bearer": [] + } + ], + "summary": "[Alpha] List all workers", + "tags": ["Workers"], + "x-badges": [ + { + "name": "OAuth scope: edge_functions:read", + "position": "after" + } + ], + "x-endpoint-owners": ["functions"], + "x-fga-permissions": [["workers_read"]], + "x-oauth-scope": "edge_functions:read" + } + }, + "/v2/projects/{ref}/workers/{name}": { + "get": { + "description": "Returns a worker along with its instance tally. Poll this after a deploy until `build_state` leaves `building`.", + "operationId": "v2-get-a-worker", + "parameters": [ + { + "name": "ref", + "required": true, + "in": "path", + "description": "Project ref", + "schema": { + "minLength": 20, + "maxLength": 20, + "pattern": "^[a-z]+$", + "example": "abcdefghijklmnopqrst", + "type": "string" + } + }, + { + "name": "name", + "required": true, + "in": "path", + "schema": { + "pattern": "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$", + "example": "hello-world", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/V2WorkerResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } + }, + "403": { + "description": "Forbidden action", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } + }, + "429": { + "description": "Rate limit exceeded", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } + } + }, + "security": [ + { + "bearer": [] + } + ], + "summary": "[Alpha] Retrieve a worker", + "tags": ["Workers"], + "x-badges": [ + { + "name": "OAuth scope: edge_functions:read", + "position": "after" + } + ], + "x-endpoint-owners": ["functions"], + "x-fga-permissions": [["workers_read"]], + "x-oauth-scope": "edge_functions:read" + }, + "delete": { + "description": "Tombstones the worker. Its instances and image are torn down asynchronously.", + "operationId": "v2-delete-a-worker", + "parameters": [ + { + "name": "ref", + "required": true, + "in": "path", + "description": "Project ref", + "schema": { + "minLength": 20, + "maxLength": 20, + "pattern": "^[a-z]+$", + "example": "abcdefghijklmnopqrst", + "type": "string" + } + }, + { + "name": "name", + "required": true, + "in": "path", + "schema": { + "pattern": "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$", + "example": "hello-world", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "" + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } + }, + "403": { + "description": "Forbidden action", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } + }, + "429": { + "description": "Rate limit exceeded", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } + } + }, + "security": [ + { + "bearer": [] + } + ], + "summary": "[Alpha] Delete a worker", + "tags": ["Workers"], + "x-badges": [ + { + "name": "OAuth scope: edge_functions:write", + "position": "after" + } + ], + "x-endpoint-owners": ["functions"], + "x-fga-permissions": [["workers_write"]], + "x-oauth-scope": "edge_functions:write" + } + }, + "/v2/projects/{ref}/workers/{name}/uploads": { + "post": { + "description": "PUT the `.tar.gz` build context to the returned `url` before `expires_at`, then deploy with the upload id as `context_upload_id`. The bytes go straight to storage — no management API request carries them.", + "operationId": "v2-create-worker-upload", + "parameters": [ + { + "name": "ref", + "required": true, + "in": "path", + "description": "Project ref", + "schema": { + "minLength": 20, + "maxLength": 20, + "pattern": "^[a-z]+$", + "example": "abcdefghijklmnopqrst", + "type": "string" + } + }, + { + "name": "name", + "required": true, + "in": "path", + "schema": { + "pattern": "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$", + "example": "hello-world", + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/V2WorkerUploadResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } + }, + "403": { + "description": "Forbidden action", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } + }, + "429": { + "description": "Rate limit exceeded", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } + } + }, + "security": [ + { + "bearer": [] + } + ], + "summary": "[Alpha] Mint a presigned slot for a build-context upload", + "tags": ["Workers"], + "x-badges": [ + { + "name": "OAuth scope: edge_functions:write", + "position": "after" + } + ], + "x-endpoint-owners": ["functions"], + "x-fga-permissions": [["workers_write"]], + "x-oauth-scope": "edge_functions:write" + } + }, + "/v2/projects/{ref}/workers/{name}/deploy": { + "post": { + "description": "Creates the worker if it does not exist, building from a context staged through the uploads endpoint. The build runs asynchronously: this answers 202 and the worker reaches `build_state` `active` or `failed` later.", + "operationId": "v2-deploy-a-worker", + "parameters": [ + { + "name": "ref", + "required": true, + "in": "path", + "description": "Project ref", + "schema": { + "minLength": 20, + "maxLength": 20, + "pattern": "^[a-z]+$", + "example": "abcdefghijklmnopqrst", + "type": "string" + } + }, + { + "name": "name", + "required": true, + "in": "path", + "schema": { + "pattern": "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$", + "example": "hello-world", + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/V2DeployWorkerRequest" + } + } + } + }, + "responses": { + "202": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/V2WorkerResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } + }, + "403": { + "description": "Forbidden action", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } + }, + "429": { + "description": "Rate limit exceeded", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } + } + }, + "security": [ + { + "bearer": [] + } + ], + "summary": "[Alpha] Deploy a worker", + "tags": ["Workers"], + "x-badges": [ + { + "name": "OAuth scope: edge_functions:write", + "position": "after" + } + ], + "x-endpoint-owners": ["functions"], + "x-fga-permissions": [["workers_write"]], + "x-oauth-scope": "edge_functions:write" + } + }, "/v2/organizations/{slug}/members": { "get": { "description": "Returns a cursor-paginated list of organization members including their roles and project-scoped permissions.", @@ -735,13 +1520,34 @@ } }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "403": { - "description": "Forbidden action" + "description": "Forbidden action", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "429": { - "description": "Rate limit exceeded" + "description": "Rate limit exceeded", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } } }, "security": [ @@ -757,7 +1563,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["members_read"]], "x-oauth-scope": "organizations:read" } @@ -811,26 +1617,54 @@ } }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "402": { "description": "This feature requires the Enterprise organization plan.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PlanGateErrorBodyV2" + "$ref": "#/components/schemas/ErrorResponseBody" } } } }, "403": { - "description": "Forbidden action" + "description": "Forbidden action", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "429": { - "description": "Rate limit exceeded" + "description": "Rate limit exceeded", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "500": { - "description": "Failed to assign organization member role" + "description": "Failed to assign organization member role", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } } }, "security": [ @@ -847,7 +1681,7 @@ "position": "before" } ], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["organization_admin_write"]] } }, @@ -880,13 +1714,34 @@ } }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "403": { - "description": "Forbidden action" + "description": "Forbidden action", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "429": { - "description": "Rate limit exceeded" + "description": "Rate limit exceeded", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } } }, "security": [ @@ -902,7 +1757,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["members_read"]], "x-oauth-scope": "organizations:read" } @@ -946,23 +1801,44 @@ } }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "402": { "description": "This feature requires the Enterprise organization plan.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PlanGateErrorBodyV2" + "$ref": "#/components/schemas/ErrorResponseBody" } } } }, "403": { - "description": "Forbidden action" + "description": "Forbidden action", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "429": { - "description": "Rate limit exceeded" + "description": "Rate limit exceeded", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } } }, "security": [ @@ -983,7 +1859,7 @@ "position": "before" } ], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["members_write"]], "x-oauth-scope": "organizations:write" }, @@ -1025,23 +1901,44 @@ } }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "402": { "description": "This feature requires the Enterprise organization plan.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PlanGateErrorBodyV2" + "$ref": "#/components/schemas/ErrorResponseBody" } } } }, "403": { - "description": "Forbidden action" + "description": "Forbidden action", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "429": { - "description": "Rate limit exceeded" + "description": "Rate limit exceeded", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } } }, "security": [ @@ -1062,7 +1959,7 @@ "position": "before" } ], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["members_write"]], "x-oauth-scope": "organizations:write" } @@ -1141,13 +2038,34 @@ } }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "403": { - "description": "Forbidden action" + "description": "Forbidden action", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "429": { - "description": "Rate limit exceeded" + "description": "Rate limit exceeded", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } } }, "security": [ @@ -1163,7 +2081,7 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api"], + "x-endpoint-owners": ["control-plane"], "x-fga-permissions": [["organization_projects_read"]], "x-oauth-scope": "projects:read" } @@ -1248,13 +2166,34 @@ } }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "403": { - "description": "Forbidden action" + "description": "Forbidden action", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } }, "429": { - "description": "Rate limit exceeded" + "description": "Rate limit exceeded", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseBody" + } + } + } } }, "security": [ @@ -1270,14 +2209,14 @@ "position": "after" } ], - "x-endpoint-owners": ["management-api", "dev-workflows"], + "x-endpoint-owners": ["control-plane", "dev-workflows"], "x-fga-permissions": [["organization_projects_read"]], "x-oauth-scope": "projects:read" } }, "/v2/projects/{ref}/webhooks/endpoints": { "get": { - "operationId": "allV2ProjectsByRefWebhooks", + "operationId": "v2-projects-ref-webhooks-endpoints-get", "parameters": [ { "in": "path", @@ -2257,7 +3196,7 @@ "description": "List all Webhook endpoints based on a project's ref or an organization's slug." }, "post": { - "operationId": "allV2ProjectsByRefWebhooks", + "operationId": "v2-projects-ref-webhooks-endpoints-post", "parameters": [ { "in": "path", @@ -3283,7 +4222,7 @@ } }, "delete": { - "operationId": "allV2ProjectsByRefWebhooks", + "operationId": "v2-projects-ref-webhooks-endpoints-delete", "parameters": [ { "in": "path", @@ -4191,7 +5130,7 @@ }, "/v2/projects/{ref}/webhooks/endpoints/{id}": { "get": { - "operationId": "allV2ProjectsByRefWebhooks", + "operationId": "v2-projects-ref-webhooks-endpoints-id-get", "parameters": [ { "in": "path", @@ -5200,7 +6139,7 @@ "description": "Get details of a specific endpoint." }, "patch": { - "operationId": "allV2ProjectsByRefWebhooks", + "operationId": "v2-projects-ref-webhooks-endpoints-id-patch", "parameters": [ { "in": "path", @@ -6330,7 +7269,7 @@ } }, "delete": { - "operationId": "allV2ProjectsByRefWebhooks", + "operationId": "v2-projects-ref-webhooks-endpoints-id-delete", "parameters": [ { "in": "path", @@ -7341,7 +8280,7 @@ }, "/v2/projects/{ref}/webhooks/endpoints/{id}/deliveries": { "get": { - "operationId": "allV2ProjectsByRefWebhooks", + "operationId": "v2-projects-ref-webhooks-endpoints-id-deliveries-get", "parameters": [ { "in": "path", @@ -8406,7 +9345,7 @@ }, "/v2/projects/{ref}/webhooks/endpoints/{id}/test": { "post": { - "operationId": "allV2ProjectsByRefWebhooks", + "operationId": "v2-projects-ref-webhooks-endpoints-id-test-post", "parameters": [ { "in": "path", @@ -9526,7 +10465,7 @@ }, "/v2/projects/{ref}/webhooks/deliveries/{id}": { "get": { - "operationId": "allV2ProjectsByRefWebhooks", + "operationId": "v2-projects-ref-webhooks-deliveries-id-get", "parameters": [ { "in": "path", @@ -10593,7 +11532,7 @@ }, "/v2/projects/{ref}/webhooks/deliveries/{id}/retry": { "post": { - "operationId": "allV2ProjectsByRefWebhooks", + "operationId": "v2-projects-ref-webhooks-deliveries-id-retry-post", "parameters": [ { "in": "path", @@ -11483,7 +12422,7 @@ }, "/v2/organizations/{slug}/webhooks/endpoints": { "get": { - "operationId": "allV2OrganizationsBySlugWebhooks", + "operationId": "v2-organizations-slug-webhooks-endpoints-get", "parameters": [ { "in": "path", @@ -12461,7 +13400,7 @@ "description": "List all Webhook endpoints based on a project's ref or an organization's slug." }, "post": { - "operationId": "allV2OrganizationsBySlugWebhooks", + "operationId": "v2-organizations-slug-webhooks-endpoints-post", "parameters": [ { "in": "path", @@ -13485,7 +14424,7 @@ } }, "delete": { - "operationId": "allV2OrganizationsBySlugWebhooks", + "operationId": "v2-organizations-slug-webhooks-endpoints-delete", "parameters": [ { "in": "path", @@ -14391,7 +15330,7 @@ }, "/v2/organizations/{slug}/webhooks/endpoints/{id}": { "get": { - "operationId": "allV2OrganizationsBySlugWebhooks", + "operationId": "v2-organizations-slug-webhooks-endpoints-id-get", "parameters": [ { "in": "path", @@ -15398,7 +16337,7 @@ "description": "Get details of a specific endpoint." }, "patch": { - "operationId": "allV2OrganizationsBySlugWebhooks", + "operationId": "v2-organizations-slug-webhooks-endpoints-id-patch", "parameters": [ { "in": "path", @@ -16526,7 +17465,7 @@ } }, "delete": { - "operationId": "allV2OrganizationsBySlugWebhooks", + "operationId": "v2-organizations-slug-webhooks-endpoints-id-delete", "parameters": [ { "in": "path", @@ -17535,7 +18474,7 @@ }, "/v2/organizations/{slug}/webhooks/endpoints/{id}/deliveries": { "get": { - "operationId": "allV2OrganizationsBySlugWebhooks", + "operationId": "v2-organizations-slug-webhooks-endpoints-id-deliveries-get", "parameters": [ { "in": "path", @@ -18598,7 +19537,7 @@ }, "/v2/organizations/{slug}/webhooks/endpoints/{id}/test": { "post": { - "operationId": "allV2OrganizationsBySlugWebhooks", + "operationId": "v2-organizations-slug-webhooks-endpoints-id-test-post", "parameters": [ { "in": "path", @@ -19716,7 +20655,7 @@ }, "/v2/organizations/{slug}/webhooks/deliveries/{id}": { "get": { - "operationId": "allV2OrganizationsBySlugWebhooks", + "operationId": "v2-organizations-slug-webhooks-deliveries-id-get", "parameters": [ { "in": "path", @@ -20781,7 +21720,7 @@ }, "/v2/organizations/{slug}/webhooks/deliveries/{id}/retry": { "post": { - "operationId": "allV2OrganizationsBySlugWebhooks", + "operationId": "v2-organizations-slug-webhooks-deliveries-id-retry-post", "parameters": [ { "in": "path", @@ -21895,6 +22834,72 @@ }, "required": ["data"] }, + "ErrorResponseBodyAPIErrorObject": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "description": { + "type": "string" + }, + "links": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "href": { + "type": "string" + }, + "rel": { + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "type": "string" + }, + "describedby": { + "type": "string" + }, + "meta": { + "type": "object", + "additionalProperties": {} + } + }, + "required": ["href"] + } + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "issues": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ErrorResponseBodyAPIErrorObject" + } + } + }, + "required": ["code", "message"], + "ref": "APIErrorObject" + }, + "ErrorResponseBody": { + "type": "object", + "properties": { + "error": { + "$ref": "#/components/schemas/ErrorResponseBodyAPIErrorObject" + } + }, + "required": ["error"] + }, "CreateLogDrainRequestOpenApi": { "type": "object", "properties": { @@ -22322,27 +23327,6 @@ }, "required": ["data"] }, - "PlanGateErrorBodyV2": { - "type": "object", - "properties": { - "error": { - "type": "object", - "properties": { - "code": { - "type": "string", - "description": "HTTP status-derived error code, e.g. \"payment_required\"" - }, - "message": { - "type": "string", - "description": "Human-readable explanation of the plan gate" - } - }, - "required": ["code", "message"], - "description": "Plan-gate error object" - } - }, - "required": ["error"] - }, "UpdateLogDrainRequestOpenApi": { "type": "object", "properties": { @@ -22555,6 +23539,537 @@ }, "required": ["data"] }, + "V2ProjectConfigResponse": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Resource type.", + "enum": ["project_config"] + }, + "id": { + "type": "string", + "description": "Project ref." + }, + "attributes": { + "type": "object", + "properties": { + "database": { + "type": "object", + "properties": { + "major_version": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "description": "The major Postgres version the database runs. `17` covers both Postgres 17 and Oriole on 17, since Oriole is a storage engine rather than a version." + }, + "ssl_enforced": { + "type": "boolean", + "description": "Whether the database rejects plaintext connections" + }, + "network_restrictions": { + "type": "object", + "properties": { + "entitlement": { + "type": "string", + "enum": ["disallowed", "allowed"] + }, + "status": { + "type": "string", + "enum": ["stored", "applied"], + "description": "Whether the allowlist below is applied to the project or only stored." + }, + "allowed_cidrs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["v4", "v6"] + } + }, + "required": ["address", "type"] + } + }, + "updated_at": { + "type": "string" + }, + "applied_at": { + "type": "string" + } + }, + "required": ["entitlement", "status", "allowed_cidrs"] + }, + "postgres_settings": { + "type": "object", + "properties": { + "effective_cache_size": { + "type": "string" + }, + "logical_decoding_work_mem": { + "type": "string" + }, + "log_autovacuum_min_duration": { + "type": "string", + "description": "Default unit: ms", + "pattern": "^(-?[0-9]+(?:\\.[0-9]+)?)(us|ms|s|min|h|d)?$" + }, + "log_checkpoints": { + "type": "boolean" + }, + "log_connections": { + "type": "boolean" + }, + "log_disconnections": { + "type": "boolean" + }, + "log_duration": { + "type": "boolean" + }, + "log_lock_waits": { + "type": "boolean" + }, + "log_recovery_conflict_waits": { + "type": "boolean" + }, + "log_replication_commands": { + "type": "boolean" + }, + "log_startup_progress_interval": { + "type": "string", + "description": "Default unit: ms", + "pattern": "^(-?[0-9]+(?:\\.[0-9]+)?)(us|ms|s|min|h|d)?$" + }, + "log_temp_files": { + "type": "string" + }, + "maintenance_work_mem": { + "type": "string" + }, + "track_activity_query_size": { + "type": "string" + }, + "max_connections": { + "type": "integer", + "minimum": 1, + "maximum": 262143 + }, + "max_locks_per_transaction": { + "type": "integer", + "minimum": 10, + "maximum": 2147483640 + }, + "max_logical_replication_workers": { + "type": "integer", + "minimum": 0, + "maximum": 262143 + }, + "max_parallel_maintenance_workers": { + "type": "integer", + "minimum": 0, + "maximum": 1024 + }, + "max_parallel_workers": { + "type": "integer", + "minimum": 0, + "maximum": 1024 + }, + "max_parallel_workers_per_gather": { + "type": "integer", + "minimum": 0, + "maximum": 1024 + }, + "max_replication_slots": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "max_slot_wal_keep_size": { + "type": "string" + }, + "max_standby_archive_delay": { + "type": "string" + }, + "max_standby_streaming_delay": { + "type": "string" + }, + "max_sync_workers_per_subscription": { + "type": "integer", + "minimum": 0, + "maximum": 262143 + }, + "max_wal_size": { + "type": "string" + }, + "max_wal_senders": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "max_worker_processes": { + "type": "integer", + "minimum": 0, + "maximum": 262143 + }, + "session_replication_role": { + "type": "string", + "enum": ["origin", "replica", "local"] + }, + "shared_buffers": { + "type": "string" + }, + "statement_timeout": { + "type": "string", + "description": "Default unit: ms", + "pattern": "^(-?[0-9]+(?:\\.[0-9]+)?)(us|ms|s|min|h|d)?$" + }, + "track_commit_timestamp": { + "type": "boolean" + }, + "wal_keep_size": { + "type": "string" + }, + "wal_sender_timeout": { + "type": "string", + "description": "Default unit: ms", + "pattern": "^(-?[0-9]+(?:\\.[0-9]+)?)(us|ms|s|min|h|d)?$" + }, + "work_mem": { + "type": "string" + }, + "checkpoint_timeout": { + "type": "string", + "description": "Default unit: s", + "pattern": "^(-?[0-9]+(?:\\.[0-9]+)?)(us|ms|s|min|h|d)?$" + }, + "hot_standby_feedback": { + "type": "boolean" + }, + "cron_log_statement": { + "type": "boolean" + } + }, + "description": "Postgres parameter overrides. Empty when the project runs entirely on defaults." + } + }, + "required": [ + "major_version", + "ssl_enforced", + "network_restrictions", + "postgres_settings" + ] + }, + "pooler": { + "type": "object", + "properties": { + "pool_mode": { + "type": "string", + "enum": ["transaction", "session", "statement"] + }, + "ignore_startup_parameters": { + "type": "string" + }, + "server_idle_timeout": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "server_lifetime": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "query_wait_timeout": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "reserve_pool_size": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "default_pool_size": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "description": "Defaults to the pooler's size for the project's compute when not overridden." + }, + "max_client_conn": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "description": "Defaults to the pooler's size for the project's compute when not overridden." + } + }, + "required": [ + "pool_mode", + "ignore_startup_parameters", + "server_idle_timeout", + "server_lifetime", + "query_wait_timeout", + "reserve_pool_size", + "default_pool_size", + "max_client_conn" + ] + }, + "auth": { + "type": "object", + "additionalProperties": {}, + "description": "Effective Auth config, keyed by lowercased GoTrue setting name and resolved through the `gotrue_config` view, so a setting the project has never overridden is reported at its platform default. Secrets are returned as an HMAC of their value, never in plaintext." + }, + "api": { + "type": "object", + "properties": { + "db_schema": { + "type": "string", + "description": "Schemas exposed through the Data API" + }, + "db_extra_search_path": { + "type": "string" + }, + "max_rows": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "db_pool_acquisition_timeout": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "db_pool": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "description": "If `null`, no pool size is written to the project's PostgREST config and PostgREST's own default applies. The platform does not pick a value here.", + "nullable": true + } + }, + "required": [ + "db_schema", + "db_extra_search_path", + "max_rows", + "db_pool_acquisition_timeout", + "db_pool" + ] + }, + "realtime": { + "type": "object", + "properties": { + "private_only": { + "type": "boolean" + }, + "max_concurrent_users": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "max_events_per_second": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "max_bytes_per_second": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "max_channels_per_client": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "max_joins_per_second": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "max_presence_events_per_second": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "max_payload_size_in_kb": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "presence_enabled": { + "type": "boolean" + }, + "suspend": { + "type": "boolean" + }, + "connection_pool": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "description": "Defaults to Realtime's pool size for the project's compute when not overridden." + }, + "postgres_changes_pool": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "description": "If `null`, no override is stored and Realtime applies its own default.", + "nullable": true + } + }, + "required": [ + "private_only", + "max_concurrent_users", + "max_events_per_second", + "max_bytes_per_second", + "max_channels_per_client", + "max_joins_per_second", + "max_presence_events_per_second", + "max_payload_size_in_kb", + "presence_enabled", + "suspend", + "connection_pool", + "postgres_changes_pool" + ] + }, + "storage": { + "type": "object", + "properties": { + "file_size_limit": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "format": "int64" + }, + "features": { + "type": "object", + "properties": { + "image_transformation": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + }, + "required": ["enabled"] + }, + "s3_protocol": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + }, + "required": ["enabled"] + }, + "purge_cache": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + }, + "required": ["enabled"] + }, + "iceberg_catalog": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "max_namespaces": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "max_tables": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "max_catalogs": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + } + }, + "required": ["enabled", "max_namespaces", "max_tables", "max_catalogs"] + }, + "vector_buckets": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "max_buckets": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "max_indexes": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + } + }, + "required": ["enabled", "max_buckets", "max_indexes"] + } + }, + "required": [ + "image_transformation", + "s3_protocol", + "purge_cache", + "iceberg_catalog", + "vector_buckets" + ] + }, + "capabilities": { + "type": "object", + "properties": { + "list_v2": { + "type": "boolean" + }, + "iceberg_catalog": { + "type": "boolean" + } + }, + "required": ["list_v2", "iceberg_catalog"] + }, + "upstream_target": { + "type": "string", + "enum": ["main", "canary"] + }, + "migration_version": { + "type": "string" + }, + "database_pool_mode": { + "type": "string" + } + }, + "required": [ + "file_size_limit", + "features", + "capabilities", + "upstream_target", + "migration_version", + "database_pool_mode" + ], + "description": "Read from the storage service's admin API rather than the middleware DB, so unlike the rest of this resource it reflects the tenant's live config." + } + }, + "required": ["database", "pooler", "auth", "api", "realtime", "storage"] + } + }, + "required": ["type", "id", "attributes"] + } + }, + "required": ["data"] + }, "V2TransferProjectBody": { "type": "object", "properties": { @@ -22709,6 +24224,18 @@ "database_identifier": { "type": "string", "description": "Identifier of the database this PrivateLink share targets - the project ref for the primary, or the read replica identifier." + }, + "resource_access_manager_resource_config_id": { + "description": "ID of the AWS VPC Lattice resource configuration backing this PrivateLink share.", + "type": "string" + }, + "resource_access_manager_resource_config_arn": { + "description": "ARN of the AWS VPC Lattice resource configuration backing this PrivateLink share.", + "type": "string" + }, + "resource_access_manager_share_arn": { + "description": "ARN of the AWS Resource Access Manager resource share for this association.", + "type": "string" } }, "required": [ @@ -22820,6 +24347,18 @@ "database_identifier": { "type": "string", "description": "Identifier of the database this PrivateLink share targets - the project ref for the primary, or the read replica identifier." + }, + "resource_access_manager_resource_config_id": { + "description": "ID of the AWS VPC Lattice resource configuration backing this PrivateLink share.", + "type": "string" + }, + "resource_access_manager_resource_config_arn": { + "description": "ARN of the AWS VPC Lattice resource configuration backing this PrivateLink share.", + "type": "string" + }, + "resource_access_manager_share_arn": { + "description": "ARN of the AWS Resource Access Manager resource share for this association.", + "type": "string" } }, "required": [ @@ -22836,6 +24375,294 @@ }, "required": ["data"] }, + "V2ListWorkersResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Resource type.", + "enum": ["project_worker"] + }, + "id": { + "type": "string", + "description": "Worker name.", + "example": "hello-world" + }, + "attributes": { + "type": "object", + "properties": { + "spec": { + "type": "object", + "properties": { + "runtime": { + "example": "node", + "type": "string" + }, + "size": { + "type": "string", + "example": "2gb-1vcpu" + }, + "exposure": { + "type": "string", + "example": "public" + }, + "instances": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "example": 1 + } + }, + "required": ["size", "exposure", "instances"] + }, + "build_state": { + "type": "string", + "enum": ["building", "active", "failed"] + }, + "secret_generation": { + "type": "string" + }, + "state_reason": { + "type": "string" + }, + "image_version": { + "type": "string" + }, + "deleting": { + "type": "boolean" + }, + "instances": { + "type": "object", + "properties": { + "declared": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "live": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "ready": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "stale": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + } + }, + "required": ["declared", "live", "ready", "stale"] + }, + "instances_error": { + "type": "string" + } + }, + "required": ["spec", "build_state", "secret_generation"] + } + }, + "required": ["type", "id", "attributes"] + } + } + }, + "required": ["data"] + }, + "V2WorkerResponse": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Resource type.", + "enum": ["project_worker"] + }, + "id": { + "type": "string", + "description": "Worker name.", + "example": "hello-world" + }, + "attributes": { + "type": "object", + "properties": { + "spec": { + "type": "object", + "properties": { + "runtime": { + "example": "node", + "type": "string" + }, + "size": { + "type": "string", + "example": "2gb-1vcpu" + }, + "exposure": { + "type": "string", + "example": "public" + }, + "instances": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "example": 1 + } + }, + "required": ["size", "exposure", "instances"] + }, + "build_state": { + "type": "string", + "enum": ["building", "active", "failed"] + }, + "secret_generation": { + "type": "string" + }, + "state_reason": { + "type": "string" + }, + "image_version": { + "type": "string" + }, + "deleting": { + "type": "boolean" + }, + "instances": { + "type": "object", + "properties": { + "declared": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "live": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "ready": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "stale": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + } + }, + "required": ["declared", "live", "ready", "stale"] + }, + "instances_error": { + "type": "string" + } + }, + "required": ["spec", "build_state", "secret_generation"] + } + }, + "required": ["type", "id", "attributes"] + } + }, + "required": ["data"] + }, + "V2WorkerUploadResponse": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Resource type.", + "enum": ["project_worker_upload"] + }, + "id": { + "type": "string", + "description": "Upload id to pass to the deploy endpoint as `context_upload_id`.", + "example": "cafe0000000000000000000000000000" + }, + "attributes": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "Presigned destination for the `.tar.gz` build context." + }, + "method": { + "type": "string", + "example": "PUT" + }, + "expires_at": { + "type": "string", + "description": "When the slot stops accepting the upload." + } + }, + "required": ["url", "method", "expires_at"] + } + }, + "required": ["type", "id", "attributes"] + } + }, + "required": ["data"] + }, + "V2DeployWorkerRequest": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Resource type.", + "enum": ["project_worker"] + }, + "attributes": { + "type": "object", + "properties": { + "spec": { + "type": "object", + "properties": { + "runtime": { + "example": "node", + "type": "string" + }, + "size": { + "type": "string", + "example": "2gb-1vcpu" + }, + "exposure": { + "type": "string", + "example": "public" + }, + "instances": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "example": 1 + } + }, + "required": ["size", "exposure", "instances"] + }, + "context_upload_id": { + "description": "Id of a build context staged through the uploads endpoint. Required unless `runtime` is set.", + "type": "string" + } + }, + "required": ["spec"] + } + }, + "required": ["type", "attributes"] + } + }, + "required": ["data"] + }, "V2ListMembersResponse": { "type": "object", "properties": { @@ -23109,7 +24936,9 @@ "email": { "type": "string", "format": "email", - "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$" + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$", + "description": "Email address of the invitation receipient.", + "example": "hello@example.com" }, "role": { "type": "string", @@ -23119,6 +24948,11 @@ }, "projects": { "description": "The projects to limit a user to. If omitted, user will have org-wide access with the provided role.", + "examples": [ + { + "ref": "abcjuqabhgwjjutfvtpa" + } + ], "minItems": 1, "type": "array", "items": { @@ -23247,7 +25081,9 @@ "email": { "type": "string", "format": "email", - "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$" + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$", + "description": "Email address of the invitation receipient.", + "example": "hello@example.com" } }, "required": ["email"] @@ -23275,7 +25111,9 @@ "email": { "type": "string", "format": "email", - "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$" + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$", + "description": "Email address of the invitation receipient.", + "example": "hello@example.com" } }, "required": ["email"] @@ -23308,7 +25146,9 @@ "email": { "type": "string", "format": "email", - "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$" + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$", + "description": "Email address of the invitation receipient.", + "example": "hello@example.com" } }, "required": ["email"] @@ -23339,7 +25179,9 @@ "email": { "type": "string", "format": "email", - "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$" + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$", + "description": "Email address of the invitation receipient.", + "example": "hello@example.com" } }, "required": ["email"] diff --git a/apps/docs/styles/globals.css b/apps/docs/styles/globals.css index 4064aaa2199..42266e52e40 100644 --- a/apps/docs/styles/globals.css +++ b/apps/docs/styles/globals.css @@ -452,7 +452,7 @@ th code { } /* Word wrap styles for code blocks */ -.shiki[data-wrapped='true'] { +.shiki[data-wrapped='true'] .code-scroll { overflow-x: hidden !important; } diff --git a/apps/studio/TANSTACK_MIGRATION.md b/apps/studio/TANSTACK_MIGRATION.md index fb8ba8cc114..13644b1c7ea 100644 --- a/apps/studio/TANSTACK_MIGRATION.md +++ b/apps/studio/TANSTACK_MIGRATION.md @@ -231,6 +231,7 @@ These are the layout-only TanStack files. Most hold a single product layout comp ### Project shell — `/workers/*` - [x] A `routes/project/$ref/workers/index.tsx` ← `pages/project/[ref]/workers/index.tsx` +- [x] A `routes/project/$ref/workers/$name.tsx` ← `pages/project/[ref]/workers/[name].tsx` ### Project shell — `/functions/*` diff --git a/apps/studio/app/api/scoped-access-token-permissions/MCPToolScopeMappings.ts b/apps/studio/app/api/scoped-access-token-permissions/MCPToolScopeMappings.ts deleted file mode 100644 index 7e2b8da4823..00000000000 --- a/apps/studio/app/api/scoped-access-token-permissions/MCPToolScopeMappings.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { constants, permissions } from '@supabase/shared-types' - -import { McpMap } from '@/data/scoped-access-tokens/permission-scope-map-query' - -const { OAuthScope } = constants - -type OAuthScopeValue = (typeof OAuthScope)[keyof typeof OAuthScope] - -// Manually extracted from platform mcp controller code. Each tool maps to alternative OAuth-scope -// groups with the same semantics as ScopeGroupAlternatives: a token can call the tool when it -// holds ALL scopes of at least ONE group (OR between groups, AND within a group). Most tools -// assert a single scope; execute_sql asserts database:read OR database:write depending on the -// session's read_only mode (mcp.controller.ts), so it carries two alternatives. -const MCPToolOAuthScopeMapping: Record = { - apply_migration: [[OAuthScope.DATABASE_WRITE]], - // Computes a local confirmation hash without calling the platform — no scope gates it. - confirm_cost: [[]], - create_branch: [[OAuthScope.ENVIRONMENT_WRITE]], - create_project: [[OAuthScope.PROJECTS_WRITE]], - delete_branch: [[OAuthScope.ENVIRONMENT_WRITE]], - deploy_edge_function: [[OAuthScope.EDGE_FUNCTIONS_WRITE]], - execute_sql: [[OAuthScope.DATABASE_READ], [OAuthScope.DATABASE_WRITE]], - generate_typescript_types: [[OAuthScope.DATABASE_READ]], - get_advisors: [[OAuthScope.DATABASE_READ]], - // Calls getOrganization + listProjects to price a project, so it needs both read scopes. - // (The type=branch path returns a constant with no platform call; gating on the project - // path's scopes fails closed for branch-only pricing, which is fine for advisory display.) - get_cost: [[OAuthScope.ORGANIZATIONS_READ, OAuthScope.PROJECTS_READ]], - get_edge_function: [[OAuthScope.EDGE_FUNCTIONS_READ]], - get_logs: [[OAuthScope.ANALYTICS_READ]], - get_organization: [[OAuthScope.ORGANIZATIONS_READ]], - get_project: [[OAuthScope.PROJECTS_READ]], - get_project_url: [[OAuthScope.PROJECTS_READ]], - get_publishable_keys: [[OAuthScope.SECRETS_READ]], - get_storage_config: [[OAuthScope.STORAGE_READ]], - list_branches: [[OAuthScope.ENVIRONMENT_READ]], - list_edge_functions: [[OAuthScope.EDGE_FUNCTIONS_READ]], - // Runs through executeSql with read_only forced true. - list_extensions: [[OAuthScope.DATABASE_READ]], - list_migrations: [[OAuthScope.DATABASE_READ]], - list_organizations: [[OAuthScope.ORGANIZATIONS_READ]], - list_projects: [[OAuthScope.PROJECTS_READ]], - list_storage_buckets: [[OAuthScope.STORAGE_READ]], - // Runs through executeSql with read_only forced true. - list_tables: [[OAuthScope.DATABASE_READ]], - merge_branch: [[OAuthScope.ENVIRONMENT_WRITE]], - pause_project: [[OAuthScope.PROJECTS_WRITE]], - rebase_branch: [[OAuthScope.ENVIRONMENT_WRITE]], - reset_branch: [[OAuthScope.ENVIRONMENT_WRITE]], - restore_project: [[OAuthScope.PROJECTS_WRITE]], - // Queries the public content API — no scope gates it. - search_docs: [[]], - update_storage_config: [[OAuthScope.STORAGE_WRITE]], -} - -type ExtractIds = { - [K in keyof T]: { - [P in keyof T[K]]: T[K][P] extends { id: infer I } ? I : never - } -} -const FGA_PERMISSIONS = Object.fromEntries( - Object.entries(permissions.FgaPermissions).map(([group, permissions]) => [ - group, - Object.fromEntries(Object.entries(permissions).map(([key, { id }]) => [key, id])), - ]) -) as ExtractIds - -// Duplicated from platform (packages/api-core/src/lib/permissions/fga-permissions.ts) -// Ideally, this could be exported from @supabase/shared-types -export const legacyOauthScopeToFgaPermissionMap: Record = { - 'analytics:read': [ - FGA_PERMISSIONS.PROJECT.ANALYTICS_LOGS_READ, - FGA_PERMISSIONS.PROJECT.ANALYTICS_USAGE_READ, - ], - 'analytics:write': [], - 'analytics_config:read': [FGA_PERMISSIONS.PROJECT.ANALYTICS_CONFIG_READ], - 'analytics_config:write': [FGA_PERMISSIONS.PROJECT.ANALYTICS_CONFIG_WRITE], - 'auth:read': [FGA_PERMISSIONS.PROJECT.AUTH_CONFIG_READ], - // Note(Hieu) Auth:write scope grants access to all auth config endpoints. - // However, one endpoint requires minimum administrator role, so this oauth scope must also include the FGA PROJECT.ADMIN_WRITE permission - 'auth:write': [FGA_PERMISSIONS.PROJECT.ADMIN_WRITE, FGA_PERMISSIONS.PROJECT.AUTH_CONFIG_WRITE], - 'database:read': [ - FGA_PERMISSIONS.USER.SNIPPETS_READ, - FGA_PERMISSIONS.PROJECT.ADVISORS_READ, - FGA_PERMISSIONS.PROJECT.BACKUPS_READ, - FGA_PERMISSIONS.PROJECT.DATABASE_READ, - FGA_PERMISSIONS.PROJECT.DATABASE_CONFIG_READ, - FGA_PERMISSIONS.PROJECT.DATABASE_JIT_READ, - FGA_PERMISSIONS.PROJECT.DATABASE_MIGRATIONS_READ, - FGA_PERMISSIONS.PROJECT.DATABASE_POOLING_CONFIG_READ, - FGA_PERMISSIONS.PROJECT.DATABASE_READONLY_CONFIG_READ, - FGA_PERMISSIONS.PROJECT.DATABASE_SSL_CONFIG_READ, - FGA_PERMISSIONS.PROJECT.SNIPPETS_READ, - ], - 'database:write': [ - FGA_PERMISSIONS.PROJECT.ADMIN_WRITE, - FGA_PERMISSIONS.PROJECT.BACKUPS_WRITE, - // Note(Hieu): Include database read permission here to align with the project query endpoint. - // RLS and FGA guard this endpoint with database read first, then perform an additional check for write queries. - // The OAuth guard requires database write directly, which causes a discrepancy error if we don't include read here. - FGA_PERMISSIONS.PROJECT.DATABASE_READ, - FGA_PERMISSIONS.PROJECT.DATABASE_WRITE, - FGA_PERMISSIONS.PROJECT.DATABASE_CONFIG_WRITE, - FGA_PERMISSIONS.PROJECT.DATABASE_MIGRATIONS_WRITE, - FGA_PERMISSIONS.PROJECT.DATABASE_POOLING_CONFIG_WRITE, - FGA_PERMISSIONS.PROJECT.DATABASE_READONLY_CONFIG_WRITE, - FGA_PERMISSIONS.PROJECT.DATABASE_SSL_CONFIG_WRITE, - FGA_PERMISSIONS.PROJECT.DATABASE_WEBHOOKS_CONFIG_WRITE, - ], - 'domains:read': [ - FGA_PERMISSIONS.PROJECT.CUSTOM_DOMAIN_READ, - FGA_PERMISSIONS.PROJECT.VANITY_SUBDOMAIN_READ, - ], - 'domains:write': [ - FGA_PERMISSIONS.PROJECT.CUSTOM_DOMAIN_WRITE, - FGA_PERMISSIONS.PROJECT.VANITY_SUBDOMAIN_WRITE, - ], - 'edge_functions:read': [FGA_PERMISSIONS.PROJECT.EDGE_FUNCTIONS_READ], - 'edge_functions:write': [FGA_PERMISSIONS.PROJECT.EDGE_FUNCTIONS_WRITE], - 'environment:read': [ - FGA_PERMISSIONS.PROJECT.ACTION_RUNS_READ, - FGA_PERMISSIONS.PROJECT.BRANCHING_DEVELOPMENT_READ, - FGA_PERMISSIONS.PROJECT.BRANCHING_PRODUCTION_READ, - ], - 'environment:write': [ - FGA_PERMISSIONS.PROJECT.ACTION_RUNS_WRITE, - FGA_PERMISSIONS.PROJECT.BRANCHING_DEVELOPMENT_CREATE, - FGA_PERMISSIONS.PROJECT.BRANCHING_DEVELOPMENT_DELETE, - FGA_PERMISSIONS.PROJECT.BRANCHING_DEVELOPMENT_WRITE, - FGA_PERMISSIONS.PROJECT.BRANCHING_PRODUCTION_CREATE, - FGA_PERMISSIONS.PROJECT.BRANCHING_PRODUCTION_DELETE, - FGA_PERMISSIONS.PROJECT.BRANCHING_PRODUCTION_WRITE, - ], - 'organizations:read': [ - FGA_PERMISSIONS.USER.ORGANIZATIONS_READ, - FGA_PERMISSIONS.ORGANIZATION.ADMIN_READ, - FGA_PERMISSIONS.ORGANIZATION.MEMBERS_READ, - ], - 'organizations:write': [], - 'projects:read': [ - FGA_PERMISSIONS.USER.PROJECTS_READ, - FGA_PERMISSIONS.ORGANIZATION.PROJECTS_READ, - FGA_PERMISSIONS.PROJECT.ADMIN_READ, - FGA_PERMISSIONS.PROJECT.DATABASE_NETWORK_BANS_READ, - FGA_PERMISSIONS.PROJECT.DATABASE_NETWORK_RESTRICTIONS_READ, - ], - 'projects:write': [ - FGA_PERMISSIONS.ORGANIZATION.ADMIN_WRITE, - FGA_PERMISSIONS.ORGANIZATION.PROJECTS_CREATE, - FGA_PERMISSIONS.PROJECT.ADMIN_WRITE, - FGA_PERMISSIONS.PROJECT.DATABASE_NETWORK_BANS_WRITE, - FGA_PERMISSIONS.PROJECT.DATABASE_NETWORK_RESTRICTIONS_WRITE, - ], - 'rest:read': [FGA_PERMISSIONS.PROJECT.DATA_API_CONFIG_READ], - 'rest:write': [FGA_PERMISSIONS.PROJECT.DATA_API_CONFIG_WRITE], - 'secrets:read': [ - FGA_PERMISSIONS.PROJECT.API_GATEWAY_KEYS_READ, - FGA_PERMISSIONS.PROJECT.AUTH_SIGNING_KEYS_READ, - FGA_PERMISSIONS.PROJECT.EDGE_FUNCTIONS_SECRETS_READ, - ], - 'secrets:write': [ - FGA_PERMISSIONS.PROJECT.API_GATEWAY_KEYS_WRITE, - FGA_PERMISSIONS.PROJECT.AUTH_SIGNING_KEYS_WRITE, - FGA_PERMISSIONS.PROJECT.EDGE_FUNCTIONS_SECRETS_WRITE, - ], - 'storage:read': [ - FGA_PERMISSIONS.PROJECT.STORAGE_READ, - FGA_PERMISSIONS.PROJECT.STORAGE_CONFIG_READ, - ], - 'storage:write': [ - FGA_PERMISSIONS.PROJECT.STORAGE_WRITE, - FGA_PERMISSIONS.PROJECT.STORAGE_CONFIG_WRITE, - ], -} - -/* - * Build a map of MCP tools/FGA permissions by expanding each OAuth-scope group to the FGA - * permissions it implies: - * { - * execute_sql: [["snippets_read", "database_read", ...], ["project_admin_write", ...]] - * } - * Groups are expanded independently, preserving the OR-of-AND structure. A group is an AND, so it - * is kept only when every one of its scopes maps to at least one FGA permission — a partial - * expansion would weaken the requirement (e.g. [ORGANIZATIONS_READ, PROJECTS_READ] shrinking to - * projects_read alone). A group with any unmapped scope is dropped whole, so the tool stays gated - * rather than becoming ungated; an explicitly empty group ([]) is the deliberate ungated marker - * and is vacuously kept. - * The code is duplicated from platform until we find a better way to share those mappings - */ -export const expandOAuthScopeGroups = ( - oAuthScopeGroups: string[][], - fgaPermissionMap: Record -): string[][] => - oAuthScopeGroups - .filter((group) => group.every((oAuthScope) => (fgaPermissionMap[oAuthScope] ?? []).length > 0)) - .map((group) => group.flatMap((oAuthScope) => fgaPermissionMap[oAuthScope] ?? [])) - -export const MCPToolScopeMappings = Object.entries(MCPToolOAuthScopeMapping).reduce( - (acc, [mcpTool, oAuthScopeGroups]) => { - acc[mcpTool] = expandOAuthScopeGroups(oAuthScopeGroups, legacyOauthScopeToFgaPermissionMap) - return acc - }, - {} as McpMap -) diff --git a/apps/studio/app/api/scoped-access-token-permissions/buildAPIPermissionScopeMap.test.ts b/apps/studio/app/api/scoped-access-token-permissions/buildAPIPermissionScopeMap.test.ts index 65a5ee62fc1..d550f2b5683 100644 --- a/apps/studio/app/api/scoped-access-token-permissions/buildAPIPermissionScopeMap.test.ts +++ b/apps/studio/app/api/scoped-access-token-permissions/buildAPIPermissionScopeMap.test.ts @@ -6,7 +6,6 @@ import { buildAPIPermissionScopeMap, getScopesAndEndpointsForAPI, } from './buildAPIPermissionScopeMap' -import { expandOAuthScopeGroups, MCPToolScopeMappings } from './MCPToolScopeMappings' import { type ScopeMap } from '@/data/scoped-access-tokens/permission-scope-map-query' import { mswServer } from '@/tests/lib/msw' @@ -135,115 +134,24 @@ describe('addMCPToolsToScopes', () => { }) }) -describe('MCPToolScopeMappings', () => { - // Platform gates execute_sql on database:read OR database:write depending on the MCP session's - // read_only mode (mcp.controller.ts), so the derived requirement must be two alternatives — a - // single conjunctive group would hide the tool from read-only tokens the platform accepts. - test('execute_sql derives the database:read bundle OR the database:write bundle', () => { - expect(MCPToolScopeMappings.execute_sql).toHaveLength(2) - const [readGroup, writeGroup] = MCPToolScopeMappings.execute_sql - expect(readGroup).toContain('database_read') - expect(readGroup).not.toContain('database_write') - expect(writeGroup).toContain('database_write') - }) - - test('single-scope tools derive a single conjunctive group', () => { - expect(MCPToolScopeMappings.apply_migration).toHaveLength(1) - expect(MCPToolScopeMappings.apply_migration[0]).toContain('database_write') - }) - - test('tools without a platform scope gate stay ungated ([[]]), not disabled ([])', () => { - expect(MCPToolScopeMappings.confirm_cost).toEqual([[]]) - expect(MCPToolScopeMappings.search_docs).toEqual([[]]) - }) - - // A group is an AND: expanding only its mapped scopes would weaken the requirement (e.g. - // [organizations:read, projects:read] shrinking to projects_read alone) and report the tool - // enabled for an incomplete grant. - test('a group with any unmapped scope is dropped whole, not partially expanded', () => { - const map = { 'projects:read': ['projects_read'] } - - expect(expandOAuthScopeGroups([['organizations:read', 'projects:read']], map)).toEqual([]) - // Other alternatives and the ungated marker survive the drop untouched. - expect(expandOAuthScopeGroups([['organizations:read'], ['projects:read'], []], map)).toEqual([ - ['projects_read'], - [], - ]) - }) - - // Guards the OAuth-scope -> legacy-map join: a scope key drifting out of the legacy map must - // not inject undefined into the payload (flatMap doesn't flatten it) or silently disable a - // gated tool by dropping all its groups. - test('every derived group is non-empty strings, and only the ungated tools lack scopes', () => { - const ungated = ['confirm_cost', 'search_docs'] - for (const [tool, groups] of Object.entries(MCPToolScopeMappings)) { - expect(groups.length, `${tool} lost all its alternatives`).toBeGreaterThan(0) - for (const group of groups) { - if (!ungated.includes(tool)) - expect(group.length, `${tool} has an empty group`).toBeGreaterThan(0) - for (const scope of group) - expect(typeof scope, `${tool} leaked a non-string scope`).toBe('string') - } - } - }) - - test('get_cost requires both organization and project read bundles together', () => { - expect(MCPToolScopeMappings.get_cost).toHaveLength(1) - expect(MCPToolScopeMappings.get_cost[0]).toEqual( - expect.arrayContaining(['organizations_read', 'projects_read']) - ) - }) - - test('covers exactly the tool registry of the deployed MCP server', () => { - expect(Object.keys(MCPToolScopeMappings).sort()).toEqual([ - 'apply_migration', - 'confirm_cost', - 'create_branch', - 'create_project', - 'delete_branch', - 'deploy_edge_function', - 'execute_sql', - 'generate_typescript_types', - 'get_advisors', - 'get_cost', - 'get_edge_function', - 'get_logs', - 'get_organization', - 'get_project', - 'get_project_url', - 'get_publishable_keys', - 'get_storage_config', - 'list_branches', - 'list_edge_functions', - 'list_extensions', - 'list_migrations', - 'list_organizations', - 'list_projects', - 'list_storage_buckets', - 'list_tables', - 'merge_branch', - 'pause_project', - 'rebase_branch', - 'reset_branch', - 'restore_project', - 'search_docs', - 'update_storage_config', - ]) - }) -}) - describe('buildAPIPermissionScopeMap', () => { // vitestSetup starts mswServer with `onUnhandledRequest: 'error'` and resets handlers between - // tests, so mocking here keeps that guard instead of replacing global fetch. - const stubSpecs = (v1: Record, v2: Record) => { + // tests, so mocking here keeps that guard instead of replacing global fetch. All three live + // sources (v1 spec, v2 spec, the MCP tool-permissions endpoint) are stubbed. + const stubSources = ( + v1: Record, + v2: Record, + mcpTools: Record = { execute_sql: [['database_read']] } + ) => { mswServer.use( http.get('*/api/v1-json', () => HttpResponse.json(v1)), - http.get('*/api/v2-json', () => HttpResponse.json(v2)) + http.get('*/api/v2-json', () => HttpResponse.json(v2)), + http.get('*/platform/mcp-tools-permissions', () => HttpResponse.json(mcpTools)) ) } test('merges both specs, attaching each MCP tool to a shared scope exactly once', async () => { - stubSpecs( + stubSources( { paths: { '/v1/projects/{ref}/database/query': { @@ -274,7 +182,7 @@ describe('buildAPIPermissionScopeMap', () => { // Path items may legally carry non-operation members; the specs are fetched live, so a benign // upstream swagger change must not start 500ing this route. test('tolerates path items with non-method OpenAPI members', async () => { - stubSpecs( + stubSources( { paths: { '/v1/projects/{ref}': { @@ -293,15 +201,33 @@ describe('buildAPIPermissionScopeMap', () => { expect(Object.keys(map.endpoints)).toHaveLength(1) }) - test('returns a copy of the tool mapping so callers cannot corrupt the module singleton', async () => { - stubSpecs({ paths: {} }, { paths: {} }) + test('returns the MCP tool map fetched from the endpoint', async () => { + stubSources( + { paths: {} }, + { paths: {} }, + { + apply_migration: [['database_migrations_write']], + search_docs: [[]], + } + ) const map = await buildAPIPermissionScopeMap() - expect(map.mcp_tools).toEqual(MCPToolScopeMappings) - expect(map.mcp_tools).not.toBe(MCPToolScopeMappings) - const before = structuredClone(MCPToolScopeMappings.execute_sql) - map.mcp_tools.execute_sql.push(['tampered']) - expect(MCPToolScopeMappings.execute_sql).toEqual(before) + expect(map.mcp_tools).toEqual({ + apply_migration: [['database_migrations_write']], + search_docs: [[]], + }) + // The gated tool is indexed under its permission; the ungated one is not. + expect(map.scopes.database_migrations_write.mcp_tools).toEqual(['apply_migration']) + }) + + test('throws when the MCP tool-permissions endpoint is unavailable', async () => { + mswServer.use( + http.get('*/api/v1-json', () => HttpResponse.json({ paths: {} })), + http.get('*/api/v2-json', () => HttpResponse.json({ paths: {} })), + http.get('*/platform/mcp-tools-permissions', () => new HttpResponse(null, { status: 503 })) + ) + + await expect(buildAPIPermissionScopeMap()).rejects.toThrow() }) }) diff --git a/apps/studio/app/api/scoped-access-token-permissions/buildAPIPermissionScopeMap.ts b/apps/studio/app/api/scoped-access-token-permissions/buildAPIPermissionScopeMap.ts index b6b568e6022..95655c5884e 100644 --- a/apps/studio/app/api/scoped-access-token-permissions/buildAPIPermissionScopeMap.ts +++ b/apps/studio/app/api/scoped-access-token-permissions/buildAPIPermissionScopeMap.ts @@ -1,9 +1,5 @@ -import { cloneDeep } from 'lodash' import z from 'zod' -// We don't have an OpenAPI that describes mcp tools security requirements so -// we have this hard coded file that must be updated when they change -import { MCPToolScopeMappings } from './MCPToolScopeMappings' import { EndpointMap, McpMap, @@ -13,32 +9,29 @@ import { import { InternalServerError } from '@/lib/api/apiHelpers' /* - * Builds the permissions/endpoint mapping by fetching the OpenAPI specs for our v1 and v2 APIs. - * The two specs are indexed together rather than merged afterwards: every v1 path starts with - * `/v1/` and every v2 path with `/v2/`, so they can't collide, and one pass de-duplicates a - * scope's endpoint list by construction. - * @throws InternalServerError when it can't fetch the OpenAPI specs + * Builds the permissions/endpoint mapping from three live sources: the v1 and v2 OpenAPI specs + * (endpoint -> FGA via `x-fga-permissions`) and the mgmt-api MCP-tool-permissions endpoint + * (tool -> FGA). The MCP map is owned by Control Plane — it's projected from the same MCP_TOOL_AUTH + * descriptor that drives enforcement — so Studio fetches it exactly like the OpenAPI spec instead of + * hand-maintaining or importing a copy. + * @throws InternalServerError when it can't fetch the specs or the MCP map */ export const buildAPIPermissionScopeMap = async (): Promise => { - const [apiV1SpecsJSON, apiV2SpecsJSON] = await Promise.all([ + const [apiV1SpecsJSON, apiV2SpecsJSON, mcpToolsJSON] = await Promise.all([ fetchAPIPermissionScope('v1'), fetchAPIPermissionScope('v2'), + fetchMcpToolPermissions(), ]) const apiV1Specs = API_SPECS_SCHEMA.parse(apiV1SpecsJSON) const apiV2Specs = API_SPECS_SCHEMA.parse(apiV2SpecsJSON) + const mcpTools = MCP_TOOLS_SCHEMA.parse(mcpToolsJSON) const { scopes, endpoints } = getScopesAndEndpointsForAPI({ paths: { ...apiV1Specs.paths, ...apiV2Specs.paths }, }) - addMCPToolsToScopes(scopes, MCPToolScopeMappings) + addMCPToolsToScopes(scopes, mcpTools) - return { - scopes, - endpoints, - // Deep copy so a caller mutating the response can't corrupt the module-level mapping, which - // outlives every request in a long-running server. - mcp_tools: cloneDeep(MCPToolScopeMappings), - } + return { scopes, endpoints, mcp_tools: mcpTools } } // OPEN API specs look like this (only kept the parts we're interested in): @@ -63,7 +56,7 @@ export const buildAPIPermissionScopeMap = async (): Promise // KNOWN DIVERGENCE: annotations are trusted verbatim, and the one on // POST /v1/projects/{ref}/database/query overstates access — the spec publishes // `[[database_read], [database_write]]`, but the route's guard requires database_read outright and -// the write group is doc-only (see the execute_sql entry in MCPToolScopeMappings.ts). A token +// the write group is doc-only (the MCP endpoint reports execute_sql under database_read only). A token // granted only database_write is therefore shown this endpoint as callable when the guard would // reject it. Studio-created tokens can't hit this (write mode always grants the read scopes too), // so this stays a display inaccuracy for API-created tokens; the fix is correcting the annotation @@ -147,6 +140,38 @@ const fetchAPIPermissionScope = async (version: 'v1' | 'v2') => { } } +// The mgmt-api endpoint that projects the MCP_TOOL_AUTH descriptor (which also drives enforcement) +// to tool -> FGA permission groups. Fetched like the OpenAPI spec above. +const fetchMcpToolPermissions = async () => { + try { + const response = await fetch(`${NEXT_PUBLIC_API_DOMAIN}/platform/mcp-tools-permissions`, { + method: 'get', + headers: { + 'Content-Type': 'application/json', + }, + }) + if (response.ok) { + return response.json() + } + const responseText = await response.text() + + const retryAfter = response.headers.get('Retry-After') ?? undefined + throw new InternalServerError(`MCP tool permissions responded with ${response.status}`, { + status: response.status, + body: responseText, + ...(retryAfter !== undefined && { retryAfter }), + }) + } catch (error: unknown) { + if (error instanceof InternalServerError) { + throw error + } + + if (error instanceof Error) { + throw new InternalServerError(error.message) + } + } +} + // Simplified OPEN API specs schemas that only defines what we care about for scoped tokens const OPEN_API_PATH_METHOD_SCHEMA = z.object({ @@ -172,3 +197,6 @@ const OPEN_API_PATH_ITEM_SCHEMA = z.preprocess( const API_SPECS_SCHEMA = z.object({ paths: z.record(z.string(), OPEN_API_PATH_ITEM_SCHEMA), }) + +// tool name -> OR-of-AND FGA permission groups, as served by GET /platform/mcp-tools-permissions. +const MCP_TOOLS_SCHEMA: z.ZodType = z.record(z.string(), z.array(z.array(z.string()))) diff --git a/apps/studio/components/interfaces/App/AppBannerWrapper.tsx b/apps/studio/components/interfaces/App/AppBannerWrapper.tsx index a0e19ea5d92..2c05bd85b65 100644 --- a/apps/studio/components/interfaces/App/AppBannerWrapper.tsx +++ b/apps/studio/components/interfaces/App/AppBannerWrapper.tsx @@ -2,6 +2,10 @@ import { IS_PLATFORM, LOCAL_STORAGE_KEYS, useFlag } from 'common' import dayjs from 'dayjs' import { usePathname } from 'next/navigation' import { PropsWithChildren, useEffect, useRef, useState } from 'react' +import { + SELECT_26_STUDIO_DISMISSAL_KEY, + useSelect26PromotionActive, +} from 'ui-patterns/Banners/Select26Promotion' import { OrganizationResourceBanner } from '../Organization/HeaderBanner' import { isLogsOrObservabilityPath } from './AppBannerWrapper.utils' @@ -9,6 +13,11 @@ import { ClockSkewBanner } from '@/components/layouts/AppLayout/ClockSkewBanner' import { NoticeBanner } from '@/components/layouts/AppLayout/NoticeBanner' import { StatusPageBanner } from '@/components/layouts/AppLayout/StatusPageBanner' import { BannerLogsAllDeprecation } from '@/components/ui/BannerStack/Banners/BannerLogsAllDeprecation' +import { BannerSelect2026 } from '@/components/ui/BannerStack/Banners/BannerSelect2026' +import { + SELECT_26_BANNER_PRIORITY, + shouldShowSelect26Banner, +} from '@/components/ui/BannerStack/Banners/BannerSelect2026.utils' import { BannerTOSUpdate } from '@/components/ui/BannerStack/Banners/BannerTOSUpdate' import { BANNER_ID, useBannerStack } from '@/components/ui/BannerStack/BannerStackProvider' import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' @@ -36,6 +45,38 @@ export const AppBannerWrapper = ({ children }: PropsWithChildren<{}>) => { false ) + const [isSelect26BannerDismissed, , { isSuccess: isSelect26DismissalLoaded }] = + useLocalStorageQuery(SELECT_26_STUDIO_DISMISSAL_KEY, false) + const isSelect26PromotionActive = useSelect26PromotionActive() + + useEffect(() => { + if (!isSelect26DismissalLoaded) return + + const shouldShow = shouldShowSelect26Banner({ + isPlatform: IS_PLATFORM, + dismissalLoaded: isSelect26DismissalLoaded, + isActive: isSelect26PromotionActive, + isDismissed: isSelect26BannerDismissed, + }) + + if (shouldShow) { + addBanner({ + id: BANNER_ID.SELECT_26, + isDismissed: false, + content: , + priority: SELECT_26_BANNER_PRIORITY, + }) + } else { + dismissBanner(BANNER_ID.SELECT_26) + } + }, [ + isSelect26DismissalLoaded, + isSelect26PromotionActive, + isSelect26BannerDismissed, + addBanner, + dismissBanner, + ]) + useEffect(() => { if (Date.now() >= TOSUpdateExpiry.getTime()) return diff --git a/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.test.tsx b/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.test.tsx new file mode 100644 index 00000000000..409d8374608 --- /dev/null +++ b/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.test.tsx @@ -0,0 +1,116 @@ +import { screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import type { components } from 'api-types' +import { HttpResponse } from 'msw' +import { Button } from 'ui' +import { useCurrentPage, useSetPage } from 'ui-patterns/CommandMenu' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { useApiKeysCommands } from './ApiKeys' +import { customRender } from '@/tests/lib/custom-render' +import { addAPIMock } from '@/tests/lib/msw' + +type ApiKeyResponse = components['schemas']['ApiKeyResponse'] + +const { mockUseAsyncCheckPermissions, mockUseHighAvailability, mockUseSelectedProjectQuery } = + vi.hoisted(() => ({ + mockUseAsyncCheckPermissions: vi.fn(), + mockUseHighAvailability: vi.fn(), + mockUseSelectedProjectQuery: vi.fn(), + })) + +vi.mock('@/hooks/misc/useCheckPermissions', () => ({ + useAsyncCheckPermissions: mockUseAsyncCheckPermissions, +})) + +vi.mock('@/hooks/misc/useHighAvailability', () => ({ + useHighAvailability: mockUseHighAvailability, +})) + +vi.mock('@/hooks/misc/useSelectedProject', () => ({ + useSelectedProjectQuery: mockUseSelectedProjectQuery, +})) + +const API_KEYS: ApiKeyResponse[] = [ + { api_key: 'anon-key', name: 'anon', type: 'legacy' }, + { api_key: 'service-key', name: 'service_role', type: 'legacy' }, + { + api_key: 'publishable-key', + hash: 'hash', + id: 'publishable-id', + inserted_at: '2025-02-16T22:24:42.115195Z', + name: 'default', + type: 'publishable', + }, + { + api_key: 'secret-key', + hash: 'hash', + id: 'secret-id', + inserted_at: '2025-02-16T22:24:42.115195Z', + name: 'sb_secret', + type: 'secret', + }, +] + +/** Renders the API keys command page so its commands can be asserted on. */ +const CommandPageHarness = () => { + useApiKeysCommands() + const setPage = useSetPage() + const page = useCurrentPage() + const commands = + page && 'sections' in page ? page.sections.flatMap((section) => section.commands) : [] + + return ( + <> + +
      + {commands.map((command) => ( +
    • {command.name}
    • + ))} +
    + + ) +} + +async function renderCommandPage() { + customRender() + + await userEvent.click(screen.getByRole('button', { name: 'Open API keys page' })) +} + +describe('useApiKeysCommands', () => { + beforeEach(() => { + vi.clearAllMocks() + + mockUseAsyncCheckPermissions.mockReturnValue({ can: true }) + mockUseSelectedProjectQuery.mockReturnValue({ + data: { id: 1, ref: 'default', name: 'default' }, + }) + mockUseHighAvailability.mockReturnValue({ isHighAvailability: false, isPending: false }) + addAPIMock({ + method: 'get', + path: '/v1/projects/:ref/api-keys', + response: () => HttpResponse.json(API_KEYS), + }) + }) + + it('omits the legacy key commands on High Availability projects', async () => { + mockUseHighAvailability.mockReturnValue({ isHighAvailability: true, isPending: false }) + + await renderCommandPage() + + expect(await screen.findByText('Copy publishable key')).toBeInTheDocument() + expect(screen.getByText('Copy secret key (sb_secret)')).toBeInTheDocument() + expect(screen.queryByText('Copy anonymous API key')).not.toBeInTheDocument() + expect(screen.queryByText('Copy service API key')).not.toBeInTheDocument() + }) + + it('includes the legacy key commands on other projects', async () => { + await renderCommandPage() + + expect(await screen.findByText('Copy anonymous API key')).toBeInTheDocument() + expect(screen.getByText('Copy service API key')).toBeInTheDocument() + expect(screen.getByText('Copy publishable key')).toBeInTheDocument() + expect(screen.getByText('Copy secret key (sb_secret)')).toBeInTheDocument() + }) +}) diff --git a/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.tsx b/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.tsx index d8ad22c0763..7788073e507 100644 --- a/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.tsx +++ b/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.tsx @@ -17,6 +17,7 @@ import { COMMAND_MENU_SECTIONS } from './CommandMenu.utils' import { orderCommandSectionsByPriority } from './ordering' import { useAPIKeys } from '@/data/api-keys/api-keys-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' +import { useHighAvailability } from '@/hooks/misc/useHighAvailability' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' const API_KEYS_PAGE_NAME = 'API Keys' @@ -30,15 +31,22 @@ export function useApiKeysCommands() { const ref = project?.ref || '_' const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*') + const { isHighAvailability } = useHighAvailability() const { data: apiKeysData } = useAPIKeys( { projectRef: project?.ref, reveal: true }, { enabled: canReadAPIKeys } ) const commands = useMemo(() => { - const { anonKey, serviceKey, publishableKey, allSecretKeys } = canReadAPIKeys - ? (apiKeysData ?? {}) - : {} + const { + anonKey: legacyAnonKey, + serviceKey: legacyServiceKey, + publishableKey, + allSecretKeys, + } = canReadAPIKeys ? (apiKeysData ?? {}) : {} + + const anonKey = isHighAvailability ? undefined : legacyAnonKey + const serviceKey = isHighAvailability ? undefined : legacyServiceKey return [ project && @@ -127,7 +135,7 @@ export function useApiKeysCommands() { icon: () => , }, ].filter(Boolean) as ICommand[] - }, [canReadAPIKeys, apiKeysData, project, ref, resetCommandMenu, setIsOpen]) + }, [canReadAPIKeys, apiKeysData, isHighAvailability, project, ref, resetCommandMenu, setIsOpen]) useRegisterPage( API_KEYS_PAGE_NAME, diff --git a/apps/studio/components/interfaces/App/FeaturePreview/ExplorerPreview.tsx b/apps/studio/components/interfaces/App/FeaturePreview/ExplorerPreview.tsx new file mode 100644 index 00000000000..f64d5fef769 --- /dev/null +++ b/apps/studio/components/interfaces/App/FeaturePreview/ExplorerPreview.tsx @@ -0,0 +1,55 @@ +import { useParams } from 'common' +import Image from 'next/image' + +import { useIsExplorerEnabled } from './FeaturePreviewContext' +import { InlineLink } from '@/components/ui/InlineLink' +import { BASE_PATH } from '@/lib/constants' + +export const ExplorerPreview = () => { + const { ref } = useParams() + const isExplorerEnabled = useIsExplorerEnabled() + + return ( +
    +

    + The Explorer is a new unified workspace for querying your data and chatting with the + Assistant, and is an early preview of where we're heading with the SQL Editor. +

    +

    + Notebooks are the first new feature of the Explorer — mix query cells and markdown notes in + a single document, so your queries and context stay together. Use them to write runbooks, + document incidents, build reusable reports, and more! +

    + + explorer-preview + +
    +

    Enabling this preview will:

    +
      +
    • + Replace the existing SQL Editor with the new{' '} + + Explorer + + . +
        +
      • + We're looking to replace the SQL Editor with the Explorer in the long term, but for + now it lives alongside the SQL Editor, toggleable via this feature preview. +
      • +
      +
    • +
    • Enable managing of Notebooks through both the dashboard and the Assistant.
    • +
    +
    +
    + ) +} diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx index 2eeb77a318f..739b178761e 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx @@ -165,6 +165,12 @@ export const useIsDatabaseConnectionsEnabled = () => { } } +export const useIsExplorerEnabled = () => { + const { flags } = useFeaturePreviewContext() + const isExplorerEnabled = useFlag('explorer') + return isExplorerEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_EXPLORER] +} + export const useFeaturePreviewModal = () => { const featurePreviews = useFeaturePreviews() const [featurePreviewModal, setFeaturePreviewModal] = useQueryState('featurePreviewModal') diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx index 24ad9662f92..606e6198125 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx @@ -34,6 +34,7 @@ import { import { AdvisorRulesPreview } from './AdvisorRulesPreview' import { CLSPreview } from './CLSPreview' import { DatabaseConnectionsPreview } from './DatabaseConnectionsPreview' +import { ExplorerPreview } from './ExplorerPreview' import { useFeaturePreviewContext, useFeaturePreviewModal } from './FeaturePreviewContext' import { IntegrationsLayoutPreview } from './IntegrationsLayoutPreview' import { JitDbAccessPreview } from './JitDbAccessPreview' @@ -59,6 +60,7 @@ const FEATURE_PREVIEW_KEY_TO_CONTENT: { [LOCAL_STORAGE_KEYS.UI_PREVIEW_SQL_EDITOR_MANUAL_SAVE]: , [LOCAL_STORAGE_KEYS.UI_PREVIEW_MARKETPLACE]: , [LOCAL_STORAGE_KEYS.UI_PREVIEW_DATABASE_CONNECTIONS]: , + [LOCAL_STORAGE_KEYS.UI_PREVIEW_EXPLORER]: , } export const FeaturePreviewModal = () => { @@ -148,12 +150,8 @@ export const FeaturePreviewModal = () => { ? allFeaturePreviews.filter((x) => x.category === undefined) : allFeaturePreviews.filter((x) => x.category === category) return ( - - + + {category} @@ -318,8 +316,10 @@ const FeaturePreviewItem = ({ key={feature.key} onClick={() => selectFeaturePreview(feature.key)} className={cn( - 'w-full! flex-1 flex items-center justify-between p-4 border-b cursor-pointer bg transition', - selectedFeature?.key === feature.key ? 'bg-accent' : 'bg-card', + 'w-full! flex-1 flex items-center justify-between p-4 cursor-pointer bg transition', + selectedFeature?.key === feature.key + ? 'bg-muted dark:bg-accent text-foreground' + : 'bg-card text-foreground-light', className )} > diff --git a/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts b/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts index 607337c5384..fb766a922ff 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts +++ b/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts @@ -21,7 +21,7 @@ export type FeaturePreview = { */ isForced?: boolean /** Optional category that the feature preview falls under, defaults to "Others" in the UI otherwise */ - category?: 'observability' | 'database' + category?: 'observability' | 'database' | 'editors' /** * Where to send the user after enabling, to try the feature out. Omit if the * feature has no single destination (e.g. a global layout change). @@ -35,11 +35,24 @@ export const useFeaturePreviews = (): FeaturePreview[] => { const jitDbAccessEnabled = useFlag('jitDbAccess') const isMarketplaceEnabled = useFlag('marketplaceIntegrations') const isDatabaseConnectionsEnabled = useFlag('topForPostgres') + const isExplorerEnabled = useFlag('explorer') const isSqlEditorManualSaveForced = useFlag('sqlEditorManualSaveForced') return useMemo(() => { const previews: FeaturePreview[] = [ + { + key: LOCAL_STORAGE_KEYS.UI_PREVIEW_EXPLORER, + name: 'Explorer & Notebooks', + category: 'editors', + // [Joshen TODO] Update with proper URL once discussion is up + discussionsUrl: undefined, + enabled: isExplorerEnabled, + isNew: true, + isPlatformOnly: true, + isDefaultOptIn: false, + getRoute: (ref?: string) => `/project/${ref}/explorer`, + }, { key: LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS, name: 'Updated Logs interface', @@ -114,6 +127,7 @@ export const useFeaturePreviews = (): FeaturePreview[] => { }, { key: LOCAL_STORAGE_KEYS.UI_PREVIEW_SQL_EDITOR_MANUAL_SAVE, + category: 'editors', name: 'Disable snippet auto-saving', discussionsUrl: undefined, isNew: true, @@ -146,5 +160,6 @@ export const useFeaturePreviews = (): FeaturePreview[] => { jitDbAccessEnabled, isMarketplaceEnabled, isDatabaseConnectionsEnabled, + isExplorerEnabled, ]) } diff --git a/apps/studio/components/interfaces/ConfigDrift/ConfigurationDriftPage.test.tsx b/apps/studio/components/interfaces/ConfigDrift/ConfigurationDriftPage.test.tsx index 36c6f00a3ec..7fd9669319e 100644 --- a/apps/studio/components/interfaces/ConfigDrift/ConfigurationDriftPage.test.tsx +++ b/apps/studio/components/interfaces/ConfigDrift/ConfigurationDriftPage.test.tsx @@ -15,7 +15,7 @@ type ListGitHubConnectionsResponse = platformComponents['schemas']['ListGitHubCo type GetGitHubConnectionConfigResponse = platformComponents['schemas']['GetGitHubConnectionConfigResponse'] type BranchResponse = apiV1Components['schemas']['BranchResponse'] -type V2ProjectConfigResponse = apiV2Components['schemas']['V2ProjectConfigResponse'] +type V2ProjectConfigResponse = apiV2Components['schemas']['V2ProjectConfigResponse_Output'] const PROJECT_REF = 'default' const ORGANIZATION_ID = 1 @@ -120,6 +120,7 @@ function createProjectConfigResponse(auth: Record): V2ProjectCo }, auth, database: { + major_version: 17, network_restrictions: { allowed_cidrs: [], entitlement: 'disallowed', diff --git a/apps/studio/components/interfaces/ConnectSheet/Connect.constants.ts b/apps/studio/components/interfaces/ConnectSheet/Connect.constants.ts index 86071ccaef9..a588f3320dd 100644 --- a/apps/studio/components/interfaces/ConnectSheet/Connect.constants.ts +++ b/apps/studio/components/interfaces/ConnectSheet/Connect.constants.ts @@ -386,6 +386,8 @@ export const PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT = 'Purchase IPv4 add-on or use Shared Pooler if on a IPv4 network' export const IPV4_ADDON_TEXT = 'Connections are IPv4 proxied with IPv4 add-on' +export const CONNECTION_SOURCE_LOAD_BALANCER = 'load-balancer' + export type ConnectionStringMethod = 'direct' | 'transaction' | 'session' export const connectionStringMethodOptions: Record< diff --git a/apps/studio/components/interfaces/ConnectSheet/ConnectConfigSection.tsx b/apps/studio/components/interfaces/ConnectSheet/ConnectConfigSection.tsx index bb21ae25718..9f96b39af66 100644 --- a/apps/studio/components/interfaces/ConnectSheet/ConnectConfigSection.tsx +++ b/apps/studio/components/interfaces/ConnectSheet/ConnectConfigSection.tsx @@ -126,12 +126,14 @@ export function ConnectConfigSection({ layout="horizontal" label={field.label} description={field.description} + name={`connect-${field.id}`} > - {field.value ?? 'error'} + + {INVALIDATED_SLOT_BEHAVIOR_LABELS[field.value ?? 'error']} + -

    Error

    +

    Block startup

    Blocks startup for manual recovery.

    -

    Recreate

    +

    Recreate slot

    Replaces destination tables and runs a new, billable initial sync.

    @@ -222,7 +229,7 @@ export const AdvancedSettings = ({
} layout="horizontal" - description="How old query results can be while BigQuery applies ongoing changes." + description="Set the maximum age of query results while BigQuery applies ongoing changes, or leave blank for the freshest results." > @@ -233,7 +240,6 @@ export const AdvancedSettings = ({ step={1} value={field.value ?? ''} onChange={handleNumberChange(field)} - placeholder="Default: None (Freshest results)" /> minutes diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/AnalyticsBucket/AnalyticsBucket.utils.ts b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/AnalyticsBucket/AnalyticsBucket.utils.ts index 3aeb5cd9060..d443262582e 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/AnalyticsBucket/AnalyticsBucket.utils.ts +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/AnalyticsBucket/AnalyticsBucket.utils.ts @@ -21,9 +21,9 @@ type AnalyticsBucketValidationOptions = { // Fields that are always required regardless of the namespace / access key selections. const ANALYTICS_BUCKET_REQUIRED_FIELDS: AnalyticsBucketValidationIssue[] = [ - { path: 'warehouseName', message: 'Bucket is required' }, - { path: 's3Region', message: 'S3 region is required' }, - { path: 's3AccessKeyId', message: 'S3 access key ID is required' }, + { path: 'warehouseName', message: 'Bucket is required.' }, + { path: 's3Region', message: 'S3 region is required.' }, + { path: 's3AccessKeyId', message: 'S3 access key ID is required.' }, ] export const getAnalyticsBucketValidationIssues = ( @@ -43,8 +43,8 @@ export const getAnalyticsBucketValidationIssues = ( if (!hasValidNamespace) { issues.push( isCreatingNewNamespace - ? { path: 'newNamespaceName', message: 'Namespace name is required' } - : { path: 'namespace', message: 'Namespace is required' } + ? { path: 'newNamespaceName', message: 'Namespace name is required.' } + : { path: 'namespace', message: 'Namespace is required.' } ) } @@ -53,7 +53,7 @@ export const getAnalyticsBucketValidationIssues = ( data.s3SecretAccessKey?.trim().length && !data.s3AccessKeyId?.trim().length ) { - issues.push({ path: 's3AccessKeyId', message: 'S3 access key ID is required' }) + issues.push({ path: 's3AccessKeyId', message: 'S3 access key ID is required.' }) } const currentS3AccessKeyId = data.s3AccessKeyId?.trim() @@ -69,7 +69,7 @@ export const getAnalyticsBucketValidationIssues = ( (!options.secretsOptional || hasChangedStoredS3AccessKey) && !data.s3SecretAccessKey?.trim().length ) { - issues.push({ path: 's3SecretAccessKey', message: 'S3 secret access key is required' }) + issues.push({ path: 's3SecretAccessKey', message: 'S3 secret access key is required.' }) } return issues diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/BigQuery/BigQuery.schema.ts b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/BigQuery/BigQuery.schema.ts index abd86b79faf..b6fa2279c49 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/BigQuery/BigQuery.schema.ts +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/BigQuery/BigQuery.schema.ts @@ -7,11 +7,11 @@ export const BigQueryFormSchema = z.object({ connectionPoolSize: z .number() .int() - .min(1, 'Connection pool size must be greater than 0') + .min(1, 'Connection pool size must be greater than 0.') .optional(), maxStalenessMins: z .number() - .int('Maximum staleness must be a whole number of minutes') - .min(0, 'Maximum staleness must be 0 or greater') + .int('Maximum staleness must be a whole number of minutes.') + .min(0, 'Maximum staleness must be 0 or greater.') .optional(), }) diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/BigQuery/BigQuery.utils.ts b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/BigQuery/BigQuery.utils.ts index 7b9e1c25a36..423632f5c4f 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/BigQuery/BigQuery.utils.ts +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/BigQuery/BigQuery.utils.ts @@ -7,18 +7,51 @@ export type BigQueryValidationIssue = { message: string } -const BIGQUERY_REQUIRED_FIELDS: { path: BigQueryFieldPath; message: string }[] = [ - { path: 'projectId', message: 'Project ID is required' }, - { path: 'datasetId', message: 'Dataset ID is required' }, - { path: 'serviceAccountKey', message: 'Service account key is required' }, +export const BIGQUERY_SERVICE_ACCOUNT_JSON_MESSAGE = 'Service account key must be valid JSON.' + +const BIGQUERY_REQUIRED_FIELDS: { + path: Exclude + message: string +}[] = [ + { path: 'projectId', message: 'Project ID is required.' }, + { path: 'datasetId', message: 'Dataset ID is required.' }, ] +const isValidJsonString = (value: string) => { + try { + JSON.parse(value) + return true + } catch { + return false + } +} + export const getBigQueryValidationIssues = ( data: Pick, - options: { secretsOptional?: boolean } = {} -): BigQueryValidationIssue[] => - BIGQUERY_REQUIRED_FIELDS.filter(({ path }) => { - if (options.secretsOptional && path === 'serviceAccountKey') return false + options: { secretsOptional?: boolean; validateJson?: boolean } = {} +): BigQueryValidationIssue[] => { + const { secretsOptional = false, validateJson = true } = options + const issues: BigQueryValidationIssue[] = BIGQUERY_REQUIRED_FIELDS.filter( + ({ path }) => !data[path]?.trim().length + ).map(({ path, message }) => ({ path, message })) - return !data[path]?.trim().length - }) + const serviceAccountKey = data.serviceAccountKey?.trim() ?? '' + + if (!serviceAccountKey) { + if (!secretsOptional) { + issues.push({ path: 'serviceAccountKey', message: 'Service account key is required.' }) + } + return issues + } + + // JSON shape is checked on submit only. Live onChange validation would fail on every + // keystroke while the user is still pasting or typing a key. + if (validateJson && !isValidJsonString(serviceAccountKey)) { + issues.push({ + path: 'serviceAccountKey', + message: BIGQUERY_SERVICE_ACCOUNT_JSON_MESSAGE, + }) + } + + return issues +} diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/BigQuery/Fields.test.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/BigQuery/Fields.test.tsx new file mode 100644 index 00000000000..6322f20270d --- /dev/null +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/BigQuery/Fields.test.tsx @@ -0,0 +1,90 @@ +import { fireEvent, screen, waitFor } from '@testing-library/react' +import { useForm } from 'react-hook-form' +import { Form } from 'ui' +import { describe, expect, it, vi } from 'vitest' + +import type { DestinationPanelSchemaType } from '../DestinationForm.schema' +import { BigQueryFields } from './Fields' +import { customRender } from '@/tests/lib/custom-render' + +const TestForm = ({ serviceAccountKey = '' }: { serviceAccountKey?: string }) => { + const form = useForm({ + defaultValues: { projectId: '', datasetId: '', serviceAccountKey }, + }) + + return ( +
+ + + ) +} + +describe('BigQueryFields', () => { + it('imports a JSON file into an editable service account key field', async () => { + const { container } = customRender() + const contents = '{"type":"service_account"}' + const file = new File([contents], 'service-account.json', { type: 'application/json' }) + Object.defineProperty(file, 'text', { value: vi.fn().mockResolvedValue(contents) }) + + const fileInput = container.querySelector('input[type="file"]') + expect(fileInput).not.toBeNull() + fireEvent.change(fileInput!, { target: { files: [file] } }) + + const textarea = screen.getByPlaceholderText( + '{"type": "service_account", "project_id": "...", ...}' + ) + await waitFor(() => expect(textarea).toHaveValue(contents)) + + fireEvent.change(textarea, { target: { value: `${contents}\n` } }) + expect(textarea).toHaveValue(`${contents}\n`) + expect(textarea).toHaveClass('max-h-[calc(13lh+1rem)]') + }) + + it('imports a dropped JSON file into the service account key field', async () => { + customRender() + const contents = '{"type":"service_account"}' + const file = new File([contents], 'service-account.json', { type: 'application/json' }) + Object.defineProperty(file, 'text', { value: vi.fn().mockResolvedValue(contents) }) + + const textarea = screen.getByPlaceholderText( + '{"type": "service_account", "project_id": "...", ...}' + ) + fireEvent.drop(textarea.closest('div')!, { + dataTransfer: { files: [file] }, + }) + + await waitFor(() => expect(textarea).toHaveValue(contents)) + }) + + it('rejects an oversized file without replacing the existing key', async () => { + const { container } = customRender() + const file = new File(['x'.repeat(5001)], 'service-account.json', { + type: 'application/json', + }) + const readFile = vi.fn() + Object.defineProperty(file, 'text', { value: readFile }) + + const fileInput = container.querySelector('input[type="file"]') + fireEvent.change(fileInput!, { target: { files: [file] } }) + + expect( + await screen.findByText('Service account key must be 5,000 characters or fewer.') + ).toBeInTheDocument() + expect(readFile).not.toHaveBeenCalled() + expect(screen.getByDisplayValue('existing-key')).toBeInTheDocument() + }) + + it('preserves the existing key when the selected file cannot be read', async () => { + const { container } = customRender() + const file = new File(['{}'], 'service-account.json', { type: 'application/json' }) + Object.defineProperty(file, 'text', { + value: vi.fn().mockRejectedValue(new Error('File read failed')), + }) + + const fileInput = container.querySelector('input[type="file"]') + fireEvent.change(fileInput!, { target: { files: [file] } }) + + expect(await screen.findByText('Could not read the selected JSON file.')).toBeInTheDocument() + expect(screen.getByDisplayValue('existing-key')).toBeInTheDocument() + }) +}) diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/BigQuery/Fields.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/BigQuery/Fields.tsx index 9e7db5d75cf..bb4e4397d95 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/BigQuery/Fields.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/BigQuery/Fields.tsx @@ -1,10 +1,54 @@ +import { Upload } from 'lucide-react' +import { useRef, useState, type ChangeEvent, type DragEvent } from 'react' import type { UseFormReturn } from 'react-hook-form' -import { FormControl, FormField, Input, TextArea } from 'ui' +import { Button, cn, FormControl, FormField, Input, TextArea } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { STORED_SECRET_PLACEHOLDER } from '../DestinationForm.constants' import type { DestinationPanelSchemaType } from '../DestinationForm.schema' +const MAX_SERVICE_ACCOUNT_KEY_LENGTH = 5000 + +const readServiceAccountFile = async ( + file: File, + form: UseFormReturn, + isCurrentRequest: () => boolean +) => { + if (file.size > MAX_SERVICE_ACCOUNT_KEY_LENGTH) { + if (isCurrentRequest()) { + form.setError('serviceAccountKey', { + message: 'Service account key must be 5,000 characters or fewer.', + }) + } + return + } + + try { + const contents = await file.text() + if (!isCurrentRequest()) return + + if (contents.length > MAX_SERVICE_ACCOUNT_KEY_LENGTH) { + form.setError('serviceAccountKey', { + message: 'Service account key must be 5,000 characters or fewer.', + }) + return + } + + form.setValue('serviceAccountKey', contents, { + shouldDirty: true, + shouldTouch: true, + shouldValidate: true, + }) + form.clearErrors('serviceAccountKey') + } catch { + if (isCurrentRequest()) { + form.setError('serviceAccountKey', { + message: 'Could not read the selected JSON file.', + }) + } + } +} + export const BigQueryFields = ({ form, editMode, @@ -12,6 +56,41 @@ export const BigQueryFields = ({ form: UseFormReturn editMode: boolean }) => { + const serviceAccountFileInputRef = useRef(null) + const fileReadRequestIdRef = useRef(0) + const [isDraggingFile, setIsDraggingFile] = useState(false) + + const handleServiceAccountFile = async (file: File | undefined) => { + if (!file) return + const requestId = ++fileReadRequestIdRef.current + await readServiceAccountFile(file, form, () => requestId === fileReadRequestIdRef.current) + } + + const handleServiceAccountFileInputChange = async (event: ChangeEvent) => { + const file = event.target.files?.[0] + event.target.value = '' + await handleServiceAccountFile(file) + } + + const handleDragOver = (event: DragEvent) => { + event.preventDefault() + event.stopPropagation() + setIsDraggingFile(true) + } + + const handleDragLeave = (event: DragEvent) => { + event.preventDefault() + event.stopPropagation() + setIsDraggingFile(false) + } + + const handleDrop = async (event: DragEvent) => { + event.preventDefault() + event.stopPropagation() + setIsDraggingFile(false) + await handleServiceAccountFile(event.dataTransfer.files?.[0]) + } + return (

BigQuery settings

@@ -57,23 +136,58 @@ export const BigQueryFields = ({ label="Service account key" description={ editMode - ? 'Stored credentials are hidden. Enter new credentials to replace them.' - : 'Service account credentials JSON for authenticating with BigQuery' + ? 'Stored credentials are hidden. Paste or upload new credentials to replace them.' + : 'Paste or upload your service account credentials JSON file for authenticating with BigQuery.' } > - -