From d5d8d954cba99785ce1da8462ff8a6381951ea76 Mon Sep 17 00:00:00 2001 From: Han Qiao Date: Tue, 7 Oct 2025 04:11:28 +0800 Subject: [PATCH 1/2] feat: use new action status endpoint (#39159) * feat: use new action status endpoint * fix: flip incorrect conditionals * chore: add step name to badge * refactor(action-runs-query): cleaner types * style(workflow logs): improve design of action run steps --------- Co-authored-by: Charis Lam <26616127+charislam@users.noreply.github.com> --- .../BranchManagement/ActionStatusBadge.tsx | 89 +++++++++++++++++++ .../BranchManagement/WorkflowLogs.tsx | 86 ++++++++++++------ .../data/actions/action-detail-query.ts | 30 +++++++ apps/studio/data/actions/action-logs-query.ts | 36 ++++++++ apps/studio/data/actions/action-runs-query.ts | 34 +++++++ apps/studio/data/actions/keys.ts | 5 ++ packages/api-types/types/api.d.ts | 4 +- 7 files changed, 254 insertions(+), 30 deletions(-) create mode 100644 apps/studio/components/interfaces/BranchManagement/ActionStatusBadge.tsx create mode 100644 apps/studio/data/actions/action-detail-query.ts create mode 100644 apps/studio/data/actions/action-logs-query.ts create mode 100644 apps/studio/data/actions/action-runs-query.ts create mode 100644 apps/studio/data/actions/keys.ts diff --git a/apps/studio/components/interfaces/BranchManagement/ActionStatusBadge.tsx b/apps/studio/components/interfaces/BranchManagement/ActionStatusBadge.tsx new file mode 100644 index 0000000000000..36cfe1cf8ad94 --- /dev/null +++ b/apps/studio/components/interfaces/BranchManagement/ActionStatusBadge.tsx @@ -0,0 +1,89 @@ +import type { PropsWithChildren } from 'react' + +import { ActionName, ActionStatus, type ActionRunStep } from 'data/actions/action-runs-query' +import { Badge, StatusIcon, Tooltip, TooltipContent, TooltipTrigger } from 'ui' + +export interface ActionStatusBadgeProps { + name: ActionName + status: ActionStatus +} + +const UNHEALTHY_STATUES: ActionStatus[] = ['DEAD', 'REMOVING'] +const WAITING_STATUSES: ActionStatus[] = ['CREATED', 'RESTARTING', 'RUNNING'] + +export const STATUS_TO_LABEL: Record = { + CREATED: 'pending', + DEAD: 'failed', + EXITED: 'succeeded', + PAUSED: 'skipped', + REMOVING: 'failed', + RESTARTING: 'restarting', + RUNNING: 'running', +} + +const NAME_TO_LABEL: Record = { + clone: 'Cloning repo', + pull: 'Pulling data', + health: 'Health check', + configure: 'Configurations', + migrate: 'Migrations', + seed: 'Data seeding', + deploy: 'Functions deployment', +} + +export const ActionStatusBadgeCondensed = ({ + children, + status, + details, +}: PropsWithChildren<{ + status: ActionStatus + details: Array +}>) => { + if (status === 'EXITED') { + return null + } + + const isUnhealthy = UNHEALTHY_STATUES.includes(status) + const isWaiting = WAITING_STATUSES.includes(status) + + return ( + + + + {(isUnhealthy || isWaiting) && ( + + )} + {children} + + + + Additional {STATUS_TO_LABEL[status]} steps: +
    + {details.map((step) => ( +
  • + {NAME_TO_LABEL[step.name]} +
  • + ))} +
+
+
+ ) +} + +export const ActionStatusBadge = ({ name, status }: ActionStatusBadgeProps) => { + if (status === 'EXITED') { + return null + } + + const isUnhealthy = UNHEALTHY_STATUES.includes(status) + const isWaiting = WAITING_STATUSES.includes(status) + + return ( + + {(isUnhealthy || isWaiting) && ( + + )} + {NAME_TO_LABEL[name]}: {STATUS_TO_LABEL[status]} + + ) +} diff --git a/apps/studio/components/interfaces/BranchManagement/WorkflowLogs.tsx b/apps/studio/components/interfaces/BranchManagement/WorkflowLogs.tsx index fddc2017c14dc..29253f09d9b9d 100644 --- a/apps/studio/components/interfaces/BranchManagement/WorkflowLogs.tsx +++ b/apps/studio/components/interfaces/BranchManagement/WorkflowLogs.tsx @@ -1,12 +1,17 @@ -import type { Branch } from 'data/branches/branches-query' import dayjs from 'dayjs' +import { groupBy } from 'lodash' import { ArrowLeft, ArrowRight } from 'lucide-react' import { useState } from 'react' -import { StatusIcon } from 'ui' import AlertError from 'components/ui/AlertError' -import { useWorkflowRunLogsQuery } from 'data/workflow-runs/workflow-run-logs-query' -import { useWorkflowRunsQuery } from 'data/workflow-runs/workflow-runs-query' +import { ActionRunData } from 'data/actions/action-detail-query' +import { useActionRunLogsQuery } from 'data/actions/action-logs-query' +import { + useActionsQuery, + type ActionRunStep, + type ActionStatus, +} from 'data/actions/action-runs-query' +import type { Branch } from 'data/branches/branches-query' import { Button, cn, @@ -18,24 +23,21 @@ import { DialogSectionSeparator, DialogTitle, DialogTrigger, + StatusIcon, } from 'ui' import { GenericSkeletonLoader } from 'ui-patterns' +import { ActionStatusBadge, ActionStatusBadgeCondensed, STATUS_TO_LABEL } from './ActionStatusBadge' import BranchStatusBadge from './BranchStatusBadge' interface WorkflowLogsProps { projectRef: string - status?: Branch['status'] | string + status: Branch['status'] } -type StatusType = Branch['status'] | string +type StatusType = Branch['status'] -const UNHEALTHY_STATUSES: StatusType[] = [ - 'ACTIVE_UNHEALTHY', - 'INIT_FAILED', - 'UNKNOWN', - 'MIGRATIONS_FAILED', - 'FUNCTIONS_FAILED', -] +const HEALTHY_STATUSES: StatusType[] = ['FUNCTIONS_DEPLOYED', 'MIGRATIONS_PASSED'] +const UNHEALTHY_STATUSES: StatusType[] = ['MIGRATIONS_FAILED', 'FUNCTIONS_FAILED'] export const WorkflowLogs = ({ projectRef, status }: WorkflowLogsProps) => { const [isOpen, setIsOpen] = useState(false) @@ -46,9 +48,11 @@ export const WorkflowLogs = ({ projectRef, status }: WorkflowLogsProps) => { isLoading: isWorkflowRunsLoading, isError: isWorkflowRunsError, error: workflowRunsError, - } = useWorkflowRunsQuery({ projectRef }, { enabled: isOpen }) + } = useActionsQuery({ ref: projectRef }, { enabled: isOpen }) - const [selectedWorkflowRunId, setSelectedWorkflowRunId] = useState(undefined) + const [selectedWorkflowRun, setSelectedWorkflowRun] = useState( + undefined + ) const { data: workflowRunLogs, @@ -56,17 +60,13 @@ export const WorkflowLogs = ({ projectRef, status }: WorkflowLogsProps) => { isLoading: isWorkflowRunLogsLoading, isError: isWorkflowRunLogsError, error: workflowRunLogsError, - } = useWorkflowRunLogsQuery( - { workflowRunId: selectedWorkflowRunId }, - { enabled: isOpen && selectedWorkflowRunId !== undefined } + } = useActionRunLogsQuery( + { ref: projectRef, run_id: selectedWorkflowRun?.id ?? '' }, + { enabled: isOpen && Boolean(selectedWorkflowRun) } ) - const showStatusIcon = - status !== undefined && - status !== 'ACTIVE_HEALTHY' && - status !== 'FUNCTIONS_DEPLOYED' && - status !== 'MIGRATIONS_PASSED' - const isUnhealthy = status !== undefined && UNHEALTHY_STATUSES.includes(status) + const showStatusIcon = !HEALTHY_STATUSES.includes(status) + const isUnhealthy = UNHEALTHY_STATUSES.includes(status) return ( @@ -93,7 +93,7 @@ export const WorkflowLogs = ({ projectRef, status }: WorkflowLogsProps) => { - {selectedWorkflowRunId === undefined ? ( + {!selectedWorkflowRun ? ( <> {isWorkflowRunsLoading && } {isWorkflowRunsError && ( @@ -108,11 +108,15 @@ export const WorkflowLogs = ({ projectRef, status }: WorkflowLogsProps) => {
  • ) } + +function RunSteps({ steps }: { steps: Array }) { + const stepsByStatus = groupBy(steps, 'status') as Record> + const firstFailedStep = stepsByStatus.DEAD?.[0] + const numberFailedSteps = stepsByStatus.DEAD?.length ?? 0 + + return ( + <> + {firstFailedStep && ( + + )} + {numberFailedSteps > 1 && ( + + {numberFailedSteps - 1} more + + )} + {(Object.keys(stepsByStatus) as Array) + .filter((status) => status !== 'DEAD') + .map((status) => ( + + {stepsByStatus[status].length} {STATUS_TO_LABEL[status]} + + ))} + + ) +} diff --git a/apps/studio/data/actions/action-detail-query.ts b/apps/studio/data/actions/action-detail-query.ts new file mode 100644 index 0000000000000..4e34849c78faa --- /dev/null +++ b/apps/studio/data/actions/action-detail-query.ts @@ -0,0 +1,30 @@ +import { useQuery, UseQueryOptions } from '@tanstack/react-query' + +import type { operations } from 'data/api' +import { get, handleError } from 'data/fetchers' +import type { ResponseError } from 'types' +import { actionKeys } from './keys' + +export type ActionRunVariables = operations['v1-get-action-run']['parameters']['path'] + +export async function getActionRun(params: ActionRunVariables, signal?: AbortSignal) { + const { data, error } = await get('/v1/projects/{ref}/actions/{run_id}', { + params: { path: params }, + signal, + }) + if (error) handleError(error) + return data +} + +export type ActionRunData = Awaited> +export type ActionRunError = ResponseError + +export const useActionRunQuery = ( + { ref, run_id }: ActionRunVariables, + { enabled = true, ...options }: UseQueryOptions = {} +) => + useQuery( + actionKeys.detail(ref, run_id), + ({ signal }) => getActionRun({ ref, run_id }, signal), + { enabled: enabled && Boolean(ref) && Boolean(run_id), staleTime: 0, ...options } + ) diff --git a/apps/studio/data/actions/action-logs-query.ts b/apps/studio/data/actions/action-logs-query.ts new file mode 100644 index 0000000000000..218eed45a7293 --- /dev/null +++ b/apps/studio/data/actions/action-logs-query.ts @@ -0,0 +1,36 @@ +import { useQuery, UseQueryOptions } from '@tanstack/react-query' + +import { operations } from 'data/api' +import { get, handleError } from 'data/fetchers' +import type { ResponseError } from 'types' +import { actionKeys } from './keys' + +export type ActionLogsVariables = operations['v1-get-action-run-logs']['parameters']['path'] + +export async function getActionRunLogs(params: ActionLogsVariables, signal?: AbortSignal) { + const { data, error } = await get(`/v1/projects/{ref}/actions/{run_id}/logs`, { + params: { path: params }, + parseAs: 'text', + headers: { Accept: 'text/plain' }, + signal, + }) + if (error) handleError(error) + return data + .split('\n') + .flatMap((line) => line.split('\r')) + .join('\n') + .trim() +} + +export type ActionLogsData = Awaited> +export type WorkflowRunLogsError = ResponseError + +export const useActionRunLogsQuery = ( + { ref, run_id }: ActionLogsVariables, + { enabled = true, ...options }: UseQueryOptions = {} +) => + useQuery( + actionKeys.detail(ref, run_id), + ({ signal }) => getActionRunLogs({ ref, run_id }, signal), + { enabled: enabled && Boolean(ref) && Boolean(run_id), staleTime: 0, ...options } + ) diff --git a/apps/studio/data/actions/action-runs-query.ts b/apps/studio/data/actions/action-runs-query.ts new file mode 100644 index 0000000000000..d987fd3494d63 --- /dev/null +++ b/apps/studio/data/actions/action-runs-query.ts @@ -0,0 +1,34 @@ +import { useQuery, type UseQueryOptions } from '@tanstack/react-query' + +import type { operations } from 'data/api' +import { get, handleError } from 'data/fetchers' +import type { ResponseError } from 'types' +import { actionKeys } from './keys' + +export type ActionsVariables = operations['v1-list-action-runs']['parameters']['path'] + +export async function listActionRuns(params: ActionsVariables, signal?: AbortSignal) { + const { data, error } = await get(`/v1/projects/{ref}/actions`, { + params: { path: params }, + signal, + }) + if (error) handleError(error) + return data +} + +export type ActionsData = Awaited> +export type ActionsError = ResponseError + +export type ActionRunStep = ActionsData[number]['run_steps'][number] +export type ActionName = ActionRunStep['name'] +export type ActionStatus = ActionRunStep['status'] + +export const useActionsQuery = ( + { ref }: ActionsVariables, + { enabled = true, ...options }: UseQueryOptions = {} +) => + useQuery( + actionKeys.list(ref), + ({ signal }) => listActionRuns({ ref }, signal), + { enabled: enabled && Boolean(ref), staleTime: 0, ...options } + ) diff --git a/apps/studio/data/actions/keys.ts b/apps/studio/data/actions/keys.ts new file mode 100644 index 0000000000000..e2e3df04f905b --- /dev/null +++ b/apps/studio/data/actions/keys.ts @@ -0,0 +1,5 @@ +export const actionKeys = { + list: (projectRef: string | undefined) => ['projects', projectRef, 'actions'] as const, + detail: (projectRef: string | undefined, runId: string | undefined) => + ['projects', projectRef, 'actions', runId] as const, +} diff --git a/packages/api-types/types/api.d.ts b/packages/api-types/types/api.d.ts index 737101c8742b2..e15025b47591b 100644 --- a/packages/api-types/types/api.d.ts +++ b/packages/api-types/types/api.d.ts @@ -2882,7 +2882,7 @@ export interface components { * @description Resource indicator for MCP (Model Context Protocol) clients * @enum {string} */ - resource?: 'http://localhost:8080/mcp' | 'http://localhost:8080/mcp' + resource?: 'https://api.supabase.green/mcp' | 'https://mcp.supabase.green/mcp' } OAuthTokenResponse: { access_token: string @@ -4280,7 +4280,7 @@ export interface operations { organization_slug?: string redirect_uri: string /** @description Resource indicator for MCP (Model Context Protocol) clients */ - resource?: 'http://localhost:8080/mcp' | 'http://localhost:8080/mcp' + resource?: 'https://api.supabase.green/mcp' | 'https://mcp.supabase.green/mcp' response_mode?: string response_type: 'code' | 'token' | 'id_token token' scope?: string From 31b6368049a1d9f5ef409383567b3f9bdaf018e9 Mon Sep 17 00:00:00 2001 From: Matt Rossman <22670878+mattrossman@users.noreply.github.com> Date: Mon, 6 Oct 2025 16:14:27 -0400 Subject: [PATCH 2/2] feat: read-only mode for self-hosted MCP (#39041) * feat: add `crypto-js`, `encryptString` with sample key * feat: include POSTGRES_PASSWORD in generated .env.test * feat: include POSTGRES_PASSWORD in turbo.json for studio * feat: read only query support * feat: configurable `POSTGRES_HOST`, `POSTGRES_DB`, `POSTGRES_PORT` * chore: rename POSTGRES_USER to clarify write permission * feat: configurable `PG_META_CRYPTO_KEY` * chore: add `PG_META_CRYPTO_KEY` to generateLocalEnv * feat: add 'postgres-meta' to linter dictionary * feat: restore read-only toggle in local MCP URL builder --- .../content/guides/self-hosting/docker.mdx | 1 + apps/studio/lib/api/self-hosted/constants.ts | 9 ++++++++ apps/studio/lib/api/self-hosted/mcp.ts | 4 ++-- apps/studio/lib/api/self-hosted/query.ts | 14 ++++++++---- apps/studio/lib/api/self-hosted/util.ts | 20 +++++++++++++++++ apps/studio/package.json | 2 ++ apps/studio/pages/api/mcp/index.ts | 10 +++++++-- docker/.env.example | 1 + docker/docker-compose.yml | 2 ++ .../components/McpConfigurationOptions.tsx | 22 +++++++++---------- .../src/McpUrlBuilder/utils/getMcpUrl.ts | 2 +- pnpm-lock.yaml | 6 +++++ scripts/generateLocalEnv.js | 6 ++++- supa-mdx-lint/Rule003Spelling.toml | 1 + turbo.json | 5 +++++ 15 files changed, 83 insertions(+), 22 deletions(-) create mode 100644 apps/studio/lib/api/self-hosted/constants.ts diff --git a/apps/docs/content/guides/self-hosting/docker.mdx b/apps/docs/content/guides/self-hosting/docker.mdx index 6f4210253b79e..a704a2ef93ea0 100644 --- a/apps/docs/content/guides/self-hosting/docker.mdx +++ b/apps/docs/content/guides/self-hosting/docker.mdx @@ -220,6 +220,7 @@ Update the `./docker/.env` file with your own secrets. In particular, these are - `SITE_URL`: the base URL of your site. - `SMTP_*`: mail server credentials. You can use any SMTP server. - `POOLER_TENANT_ID`: the tenant-id that will be used by Supavisor pooler for your connection string +- `PG_META_CRYPTO_KEY`: encryption key for securing connection strings between Studio and postgres-meta You will need to [restart](#restarting-all-services) the services for the changes to take effect. diff --git a/apps/studio/lib/api/self-hosted/constants.ts b/apps/studio/lib/api/self-hosted/constants.ts new file mode 100644 index 0000000000000..8d0f2dfa47d6b --- /dev/null +++ b/apps/studio/lib/api/self-hosted/constants.ts @@ -0,0 +1,9 @@ +// Constants specific to self-hosted environments + +export const ENCRYPTION_KEY = process.env.PG_META_CRYPTO_KEY || 'SAMPLE_KEY' +export const POSTGRES_PORT = process.env.POSTGRES_PORT || 5432 +export const POSTGRES_HOST = process.env.POSTGRES_HOST || 'db' +export const POSTGRES_DATABASE = process.env.POSTGRES_DB || 'postgres' +export const POSTGRES_PASSWORD = process.env.POSTGRES_PASSWORD || 'postgres' +export const POSTGRES_USER_READ_WRITE = 'postgres' +export const POSTGRES_USER_READ_ONLY = 'supabase_read_only_user' diff --git a/apps/studio/lib/api/self-hosted/mcp.ts b/apps/studio/lib/api/self-hosted/mcp.ts index ea9722f5f8680..79ecae3dac7ca 100644 --- a/apps/studio/lib/api/self-hosted/mcp.ts +++ b/apps/studio/lib/api/self-hosted/mcp.ts @@ -23,8 +23,8 @@ export function getDatabaseOperations({ }: GetDatabaseOperationsOptions): DatabaseOperations { return { async executeSql(_projectRef: string, options: ExecuteSqlOptions) { - const { query } = options - const { data, error } = await executeQuery({ query, headers }) + const { query, read_only: readOnly } = options + const { data, error } = await executeQuery({ query, headers, readOnly }) if (error) { throw error diff --git a/apps/studio/lib/api/self-hosted/query.ts b/apps/studio/lib/api/self-hosted/query.ts index bcceb08ee3494..9834e52245a86 100644 --- a/apps/studio/lib/api/self-hosted/query.ts +++ b/apps/studio/lib/api/self-hosted/query.ts @@ -1,10 +1,11 @@ import { PG_META_URL } from 'lib/constants/index' import { constructHeaders } from '../apiHelpers' import { PgMetaDatabaseError, databaseErrorSchema, WrappedResult } from './types' -import { assertSelfHosted } from './util' +import { assertSelfHosted, encryptString, getConnectionString } from './util' export type QueryOptions = { query: string + readOnly?: boolean headers?: HeadersInit } @@ -15,16 +16,21 @@ export type QueryOptions = { */ export async function executeQuery({ query, + readOnly = false, headers, }: QueryOptions): Promise> { assertSelfHosted() + const connectionString = getConnectionString({ readOnly }) + const connectionStringEncrypted = encryptString(connectionString) + const response = await fetch(`${PG_META_URL}/query`, { method: 'POST', - headers: { + headers: constructHeaders({ + ...headers, 'Content-Type': 'application/json', - ...constructHeaders(headers ?? {}), - }, + 'x-connection-encrypted': connectionStringEncrypted, + }), body: JSON.stringify({ query }), }) diff --git a/apps/studio/lib/api/self-hosted/util.ts b/apps/studio/lib/api/self-hosted/util.ts index ae5ba23710528..54fd5f6d6b1c3 100644 --- a/apps/studio/lib/api/self-hosted/util.ts +++ b/apps/studio/lib/api/self-hosted/util.ts @@ -1,4 +1,14 @@ +import crypto from 'crypto-js' import { IS_PLATFORM } from 'lib/constants' +import { + ENCRYPTION_KEY, + POSTGRES_DATABASE, + POSTGRES_HOST, + POSTGRES_PASSWORD, + POSTGRES_PORT, + POSTGRES_USER_READ_WRITE, + POSTGRES_USER_READ_ONLY, +} from './constants' /** * Asserts that the current environment is self-hosted. @@ -8,3 +18,13 @@ export function assertSelfHosted() { throw new Error('This function can only be called in self-hosted environments') } } + +export function encryptString(stringToEncrypt: string): string { + return crypto.AES.encrypt(stringToEncrypt, ENCRYPTION_KEY).toString() +} + +export function getConnectionString({ readOnly }: { readOnly: boolean }) { + const postgresUser = readOnly ? POSTGRES_USER_READ_ONLY : POSTGRES_USER_READ_WRITE + + return `postgresql://${postgresUser}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DATABASE}` +} diff --git a/apps/studio/package.json b/apps/studio/package.json index 96ee7253159ea..0c9ff9aa0ad96 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -79,6 +79,7 @@ "config": "workspace:*", "cron-parser": "^4.9.0", "cronstrue": "^2.50.0", + "crypto-js": "^4.2.0", "dayjs": "^1.11.10", "dnd-core": "^16.0.1", "file-saver": "^2.0.5", @@ -159,6 +160,7 @@ "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.0.0", "@types/common-tags": "^1.8.1", + "@types/crypto-js": "^4.2.2", "@types/file-saver": "^2.0.2", "@types/json-logic-js": "^1.2.1", "@types/lodash": "^4.14.172", diff --git a/apps/studio/pages/api/mcp/index.ts b/apps/studio/pages/api/mcp/index.ts index c2ed15f8bcd63..6cf25d15437d2 100644 --- a/apps/studio/pages/api/mcp/index.ts +++ b/apps/studio/pages/api/mcp/index.ts @@ -1,7 +1,7 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' import { createSupabaseMcpServer, SupabasePlatform } from '@supabase/mcp-server-supabase' import { stripIndent } from 'common-tags' -import { commaSeparatedStringIntoArray, fromNodeHeaders } from 'lib/api/apiHelpers' +import { commaSeparatedStringIntoArray, fromNodeHeaders, zBooleanString } from 'lib/api/apiHelpers' import { getDatabaseOperations, getDevelopmentOperations } from 'lib/api/self-hosted/mcp' import { DEFAULT_PROJECT } from 'lib/constants/api' import { NextApiRequest, NextApiResponse } from 'next' @@ -22,6 +22,11 @@ const mcpQuerySchema = z.object({ ` ) .pipe(z.array(supportedFeatureGroupSchema).optional()), + read_only: zBooleanString() + .default('false') + .describe( + 'Indicates whether or not the MCP server should operate in read-only mode. This prevents write operations on any of your databases by executing SQL as a read-only Postgres user.' + ), }) const handler = async (req: NextApiRequest, res: NextApiResponse) => { @@ -41,7 +46,7 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) { return res.status(400).json({ error: error.flatten().fieldErrors }) } - const { features } = data + const { features, read_only } = data const headers = fromNodeHeaders(req.headers) const platform: SupabasePlatform = { @@ -54,6 +59,7 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) { platform, projectId: DEFAULT_PROJECT.ref, features, + readOnly: read_only, }) const transport = new StreamableHTTPServerTransport({ diff --git a/docker/.env.example b/docker/.env.example index 8ee5f75c680c3..63f2b2702a5ff 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -11,6 +11,7 @@ DASHBOARD_USERNAME=supabase DASHBOARD_PASSWORD=this_password_is_insecure_and_should_be_updated SECRET_KEY_BASE=UpNVntn3cDxHJpq99YMc1T1AQgQpc8kfYTuRgBiYa15BLrx8etQoXz3gZv1/u2oq VAULT_ENC_KEY=your-encryption-key-32-chars-min +PG_META_CRYPTO_KEY=your-encryption-key-32-chars-min ############ diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 9361659aed6fd..6d9192bd5930a 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -30,6 +30,7 @@ services: environment: STUDIO_PG_META_URL: http://meta:8080 POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + PG_META_CRYPTO_KEY: ${PG_META_CRYPTO_KEY} DEFAULT_ORGANIZATION_NAME: ${STUDIO_DEFAULT_ORGANIZATION} DEFAULT_PROJECT_NAME: ${STUDIO_DEFAULT_PROJECT} @@ -311,6 +312,7 @@ services: PG_META_DB_NAME: ${POSTGRES_DB} PG_META_DB_USER: supabase_admin PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD} + CRYPTO_KEY: ${PG_META_CRYPTO_KEY} functions: container_name: supabase-edge-functions diff --git a/packages/ui-patterns/src/McpUrlBuilder/components/McpConfigurationOptions.tsx b/packages/ui-patterns/src/McpUrlBuilder/components/McpConfigurationOptions.tsx index 7d71e8a8f91ab..ad0dccaf2ab4f 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/components/McpConfigurationOptions.tsx +++ b/packages/ui-patterns/src/McpUrlBuilder/components/McpConfigurationOptions.tsx @@ -34,19 +34,17 @@ export function McpConfigurationOptions({ return (
    {/* Readonly Mode */} - {isPlatform && ( -
    -
    - - Only allow read operations on your database -
    -
    - -
    +
    +
    + + Only allow read operations on your database +
    +
    +
    - )} +
    {/* Feature Groups */}
    diff --git a/packages/ui-patterns/src/McpUrlBuilder/utils/getMcpUrl.ts b/packages/ui-patterns/src/McpUrlBuilder/utils/getMcpUrl.ts index 94dc41f8cb7f7..e7ff07dad69eb 100644 --- a/packages/ui-patterns/src/McpUrlBuilder/utils/getMcpUrl.ts +++ b/packages/ui-patterns/src/McpUrlBuilder/utils/getMcpUrl.ts @@ -28,7 +28,7 @@ export function getMcpUrl({ if (projectRef && isPlatform) { url.searchParams.set('project_ref', projectRef) } - if (readonly && isPlatform) { + if (readonly) { url.searchParams.set('read_only', 'true') } if (features.length > 0) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39d4eabbd60ba..d8c9f943efa18 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -876,6 +876,9 @@ importers: cronstrue: specifier: ^2.50.0 version: 2.50.0 + crypto-js: + specifier: ^4.2.0 + version: 4.2.0 dayjs: specifier: ^1.11.10 version: 1.11.13 @@ -1111,6 +1114,9 @@ importers: '@types/common-tags': specifier: ^1.8.1 version: 1.8.4 + '@types/crypto-js': + specifier: ^4.2.2 + version: 4.2.2 '@types/file-saver': specifier: ^2.0.2 version: 2.0.5 diff --git a/scripts/generateLocalEnv.js b/scripts/generateLocalEnv.js index ac7a488c72d69..d4134e5d53946 100644 --- a/scripts/generateLocalEnv.js +++ b/scripts/generateLocalEnv.js @@ -8,11 +8,15 @@ const generatedEnv = require('../keys.json') */ const defaultEnv = { - // POSTGRES_PASSWORD: 'postgres', // NEXT_ANALYTICS_BACKEND_PROVIDER: 'postgres', // SUPABASE_REST_URL: 'http://127.0.0.1:54321/rest/v1/', // NEXT_PUBLIC_ENABLE_LOGS: 'false', // NEXT_PUBLIC_IS_PLATFORM: 'false', + PG_META_CRYPTO_KEY: 'SAMPLE_KEY', + POSTGRES_PASSWORD: 'postgres', + POSTGRES_HOST: 'db', + POSTGRES_DB: 'postgres', + POSTGRES_PORT: '5432', SUPABASE_ANON_KEY: '$ANON_KEY', SUPABASE_SERVICE_KEY: '$SERVICE_ROLE_KEY', SUPABASE_URL: '$API_URL', diff --git a/supa-mdx-lint/Rule003Spelling.toml b/supa-mdx-lint/Rule003Spelling.toml index 91a877b738e9c..76d95a9201060 100644 --- a/supa-mdx-lint/Rule003Spelling.toml +++ b/supa-mdx-lint/Rule003Spelling.toml @@ -246,6 +246,7 @@ allow_list = [ "PostQUEL", "PostgREST", "Postgres", + "postgres-meta", # We prefer Postgres, but check for vocabulary preference in a separate rule "PostgreSQL", "ProGuard", diff --git a/turbo.json b/turbo.json index a7c33bf553390..7dd90e5bbb26b 100644 --- a/turbo.json +++ b/turbo.json @@ -82,6 +82,11 @@ // These envs are technically passthrough env vars because they're only used on the server side of Nextjs "PLATFORM_PG_META_URL", "STUDIO_PG_META_URL", + "PG_META_CRYPTO_KEY", + "POSTGRES_PASSWORD", + "POSTGRES_HOST", + "POSTGRES_DB", + "POSTGRES_PORT", "READ_ONLY_URL", "READ_ONLY_API_KEY", "SUPABASE_SERVICE_KEY",