diff --git a/apps/studio/components/interfaces/Database/Hooks/FormContents.tsx b/apps/studio/components/interfaces/Database/Hooks/FormContents.tsx
index 84fdc1d909e..00b586b202b 100644
--- a/apps/studio/components/interfaces/Database/Hooks/FormContents.tsx
+++ b/apps/studio/components/interfaces/Database/Hooks/FormContents.tsx
@@ -27,12 +27,13 @@ import { AVAILABLE_WEBHOOK_TYPES, HOOK_EVENTS } from './Hooks.constants'
import { HTTPHeaders } from './HTTPHeaders'
import { HTTPParameters } from './HTTPParameters'
import { HTTPRequestConfig } from './HTTPRequestConfig'
+import { ensureEdgeFunctionAuthorizationHeader } from '@/components/interfaces/Functions/httpHeaderAddActions'
import {
FormSection,
FormSectionContent,
FormSectionLabel,
} from '@/components/ui/Forms/FormSection'
-import { useAPIKeysQuery } from '@/data/api-keys/api-keys-query'
+import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query'
import { useEdgeFunctionsQuery } from '@/data/edge-functions/edge-functions-query'
import { useTableNamesQuery } from '@/data/tables/table-names-query'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
@@ -60,7 +61,8 @@ export const FormContents = ({ form, selectedHook }: FormContentsProps) => {
projectRef: ref,
})
- const legacyServiceRole = keys.find((x) => x.name === 'service_role')?.api_key ?? '[YOUR API KEY]'
+ const { serviceKey } = getKeys(keys)
+ const serviceRoleKey = serviceKey?.api_key
const httpUrl = useWatch({ control: form.control, name: 'http_url' })
const httpHeaders = useWatch({ control: form.control, name: 'httpHeaders' })
@@ -79,34 +81,18 @@ export const FormContents = ({ form, selectedHook }: FormContentsProps) => {
if (httpUrl && isEdgeFunctionSelected) {
const fnSlug = httpUrl.split('/').at(-1)
const fn = functions.find((x) => x.slug === fnSlug)
- const authorizationHeader = httpHeaders.find((x) => x.name === 'Authorization')
- const edgeFunctionAuthHeaderVal = `Bearer ${legacyServiceRole}`
+ const updatedHttpHeaders = ensureEdgeFunctionAuthorizationHeader({
+ headers: httpHeaders,
+ serviceRoleKey,
+ verifyJwt: fn?.verify_jwt,
+ createRow: (name, value) => ({ id: uuidv4(), name, value }),
+ })
- if (fn?.verify_jwt && authorizationHeader == null) {
- const newAuthHeader = {
- id: uuidv4(),
- name: 'Authorization',
- value: edgeFunctionAuthHeaderVal,
- }
- form.setValue('httpHeaders', [...httpHeaders, newAuthHeader])
- } else if (fn?.verify_jwt && authorizationHeader?.value !== edgeFunctionAuthHeaderVal) {
- const updatedHttpHeaders = httpHeaders.map((x) => {
- if (x.name === 'Authorization') return { ...x, value: edgeFunctionAuthHeaderVal }
- else return x
- })
+ if (updatedHttpHeaders !== httpHeaders) {
form.setValue('httpHeaders', updatedHttpHeaders)
}
}
- }, [
- form,
- functions,
- httpHeaders,
- httpUrl,
- isSuccessEdgeFunctions,
- legacyServiceRole,
- ref,
- restUrl,
- ])
+ }, [form, functions, httpHeaders, httpUrl, isSuccessEdgeFunctions, serviceRoleKey, ref, restUrl])
return (
diff --git a/apps/studio/components/interfaces/Database/Hooks/HTTPHeaders.tsx b/apps/studio/components/interfaces/Database/Hooks/HTTPHeaders.tsx
index b22344bbec2..a0bf26bd380 100644
--- a/apps/studio/components/interfaces/Database/Hooks/HTTPHeaders.tsx
+++ b/apps/studio/components/interfaces/Database/Hooks/HTTPHeaders.tsx
@@ -38,7 +38,6 @@ export const HTTPHeaders = ({ form }: HTTPHeadersProps) => {
functionType === 'supabase_function'
? buildEdgeFunctionHeaderAddActions({
apiKey,
- includeApiKeyHeader: serviceKey?.type === 'secret',
createRow: (name: string, value: string) => ({ id: uuidv4(), name, value }),
})
: []
@@ -56,7 +55,7 @@ export const HTTPHeaders = ({ form }: HTTPHeadersProps) => {
createEmptyRow={() => ({ id: uuidv4(), name: '', value: '' })}
keyPlaceholder="Header name"
valuePlaceholder="Header value"
- addLabel="Add a new header"
+ addLabel="Add header"
addActions={addActions}
/>
diff --git a/apps/studio/components/interfaces/Database/Hooks/HTTPParameters.test.tsx b/apps/studio/components/interfaces/Database/Hooks/HTTPParameters.test.tsx
index 81c69533117..03c87619bed 100644
--- a/apps/studio/components/interfaces/Database/Hooks/HTTPParameters.test.tsx
+++ b/apps/studio/components/interfaces/Database/Hooks/HTTPParameters.test.tsx
@@ -35,7 +35,7 @@ describe('HTTPParameters', () => {
render()
- await user.click(screen.getByRole('button', { name: 'Add a new parameter' }))
+ await user.click(screen.getByRole('button', { name: 'Add parameter' }))
expect(screen.getAllByPlaceholderText('Parameter name')).toHaveLength(2)
expect(screen.getAllByPlaceholderText('Parameter value')).toHaveLength(2)
diff --git a/apps/studio/components/interfaces/Database/Hooks/HTTPParameters.tsx b/apps/studio/components/interfaces/Database/Hooks/HTTPParameters.tsx
index 50d0e9308f9..6b53ac43b02 100644
--- a/apps/studio/components/interfaces/Database/Hooks/HTTPParameters.tsx
+++ b/apps/studio/components/interfaces/Database/Hooks/HTTPParameters.tsx
@@ -27,7 +27,7 @@ export const HTTPParameters = ({ form }: HTTPParametersProps) => {
createEmptyRow={() => ({ id: uuidv4(), name: '', value: '' })}
keyPlaceholder="Parameter name"
valuePlaceholder="Parameter value"
- addLabel="Add a new parameter"
+ addLabel="Add parameter"
removeLabel="Remove parameter"
/>
diff --git a/apps/studio/components/interfaces/Functions/httpHeaderAddActions.test.ts b/apps/studio/components/interfaces/Functions/httpHeaderAddActions.test.ts
index ceb81d01648..f9089b9c680 100644
--- a/apps/studio/components/interfaces/Functions/httpHeaderAddActions.test.ts
+++ b/apps/studio/components/interfaces/Functions/httpHeaderAddActions.test.ts
@@ -1,30 +1,159 @@
import { describe, expect, it } from 'vitest'
-import { buildEdgeFunctionHeaderAddActions } from './httpHeaderAddActions'
+import {
+ buildEdgeFunctionHeaderAddActions,
+ ensureEdgeFunctionAuthorizationHeader,
+ getEdgeFunctionAuthHeader,
+} from './httpHeaderAddActions'
describe('buildEdgeFunctionHeaderAddActions', () => {
- it('includes the apikey header when requested', () => {
+ it('includes the apikey header for secret keys', () => {
const [authAction] = buildEdgeFunctionHeaderAddActions({
- apiKey: 'secret-key',
- includeApiKeyHeader: true,
+ apiKey: 'sb_secret_123',
createRow: (name, value) => ({ name, value }),
})
- expect(authAction.createRows()).toEqual([
- { name: 'Authorization', value: 'Bearer secret-key' },
- { name: 'apikey', value: 'secret-key' },
- ])
+ expect(authAction.label).toBe('Add secret key')
+ expect(authAction.description).toBe(
+ 'Requires JWT verification to be disabled and authorization handled by the function'
+ )
+ expect(authAction.createRows()).toEqual([{ name: 'apikey', value: 'sb_secret_123' }])
})
- it('omits the apikey header when not requested', () => {
+ it('falls back to the Authorization header for legacy keys', () => {
const [authAction] = buildEdgeFunctionHeaderAddActions({
- apiKey: 'service-key',
- includeApiKeyHeader: false,
+ apiKey: 'legacy-service-role-jwt',
createRow: (name, value) => ({ name, value }),
})
+ expect(authAction.label).toBe('Add secret key')
+ expect(authAction.description).toBe('Required for edge functions that enforce JWT verification')
expect(authAction.createRows()).toEqual([
- { name: 'Authorization', value: 'Bearer service-key' },
+ { name: 'Authorization', value: 'Bearer legacy-service-role-jwt' },
])
})
})
+
+describe('getEdgeFunctionAuthHeader', () => {
+ it('returns the apikey header for publishable keys', () => {
+ expect(getEdgeFunctionAuthHeader('sb_publishable_123')).toEqual({
+ name: 'apikey',
+ value: 'sb_publishable_123',
+ })
+ })
+
+ it('returns the Authorization header for non-prefixed keys', () => {
+ expect(getEdgeFunctionAuthHeader('legacy-service-role-jwt')).toEqual({
+ name: 'Authorization',
+ value: 'Bearer legacy-service-role-jwt',
+ })
+ })
+
+ it.each(['sb_secretly_123', 'sb_publishableish_123'])(
+ 'does not treat %s as a new API key',
+ (apiKey) => {
+ expect(getEdgeFunctionAuthHeader(apiKey)).toEqual({
+ name: 'Authorization',
+ value: `Bearer ${apiKey}`,
+ })
+ }
+ )
+})
+
+describe('ensureEdgeFunctionAuthorizationHeader', () => {
+ const createRow = (name: string, value: string) => ({ id: 'new', name, value })
+
+ it('preserves apikey while normalizing Authorization rows', () => {
+ const headers = [
+ { id: 'custom-before', name: 'X-Before', value: 'before' },
+ { id: 'authorization', name: ' authorization ', value: 'Bearer old-key' },
+ { id: 'apikey', name: 'apikey', value: 'sb_secret_123' },
+ { id: 'duplicate', name: 'AUTHORIZATION', value: 'Bearer stale-key' },
+ { id: 'custom-after', name: 'X-After', value: 'after' },
+ ]
+
+ expect(
+ ensureEdgeFunctionAuthorizationHeader({
+ headers,
+ serviceRoleKey: 'legacy-service-role-jwt',
+ verifyJwt: true,
+ createRow,
+ })
+ ).toEqual([
+ { id: 'custom-before', name: 'X-Before', value: 'before' },
+ {
+ id: 'authorization',
+ name: 'Authorization',
+ value: 'Bearer old-key',
+ },
+ { id: 'apikey', name: 'apikey', value: 'sb_secret_123' },
+ { id: 'custom-after', name: 'X-After', value: 'after' },
+ ])
+ })
+
+ it('adds Authorization alongside apikey when JWT verification is enabled', () => {
+ const headers = [{ id: 'apikey', name: 'apikey', value: 'sb_secret_123' }]
+
+ expect(
+ ensureEdgeFunctionAuthorizationHeader({
+ headers,
+ serviceRoleKey: 'legacy-service-role-jwt',
+ verifyJwt: true,
+ createRow,
+ })
+ ).toEqual([
+ { id: 'apikey', name: 'apikey', value: 'sb_secret_123' },
+ { id: 'new', name: 'Authorization', value: 'Bearer legacy-service-role-jwt' },
+ ])
+ })
+
+ it('does not change headers without an applicable service role key', () => {
+ const headers = [
+ { id: 'authorization', name: 'Authorization', value: 'Bearer existing-key' },
+ { id: 'apikey', name: 'apikey', value: 'sb_secret_123' },
+ ]
+
+ expect(
+ ensureEdgeFunctionAuthorizationHeader({
+ headers,
+ serviceRoleKey: 'legacy-service-role-jwt',
+ verifyJwt: false,
+ createRow,
+ })
+ ).toBe(headers)
+ expect(ensureEdgeFunctionAuthorizationHeader({ headers, verifyJwt: true, createRow })).toBe(
+ headers
+ )
+ expect(
+ ensureEdgeFunctionAuthorizationHeader({
+ headers,
+ serviceRoleKey: 'sb_secret_456',
+ verifyJwt: true,
+ createRow,
+ })
+ ).toBe(headers)
+ })
+
+ it.each(['Bearer user-entered-token', ''])(
+ 'preserves a user-entered Authorization value: %j',
+ (value) => {
+ const headers = [
+ {
+ id: 'authorization',
+ name: 'Authorization',
+ value,
+ },
+ { id: 'apikey', name: 'apikey', value: 'sb_secret_123' },
+ ]
+
+ expect(
+ ensureEdgeFunctionAuthorizationHeader({
+ headers,
+ serviceRoleKey: 'legacy-service-role-jwt',
+ verifyJwt: true,
+ createRow,
+ })
+ ).toBe(headers)
+ }
+ )
+})
diff --git a/apps/studio/components/interfaces/Functions/httpHeaderAddActions.ts b/apps/studio/components/interfaces/Functions/httpHeaderAddActions.ts
index 62a6cf3fcff..75e446c5e39 100644
--- a/apps/studio/components/interfaces/Functions/httpHeaderAddActions.ts
+++ b/apps/studio/components/interfaces/Functions/httpHeaderAddActions.ts
@@ -2,29 +2,83 @@ import type { KeyValueFieldArrayAction } from 'ui-patterns/form/KeyValueFieldArr
interface BuildEdgeFunctionHeaderAddActionsParams {
apiKey: string
- includeApiKeyHeader?: boolean
createRow: (name: string, value: string) => TRow
}
+interface HTTPHeader {
+ name: string
+ value: string
+}
+
+interface EnsureEdgeFunctionAuthorizationHeaderParams {
+ headers: TRow[]
+ serviceRoleKey?: string
+ verifyJwt?: boolean
+ createRow: (name: string, value: string) => TRow
+}
+
+const isNewApiKey = (apiKey: string) =>
+ apiKey.startsWith('sb_secret_') || apiKey.startsWith('sb_publishable_')
+
+export const getEdgeFunctionAuthHeader = (apiKey: string) =>
+ isNewApiKey(apiKey)
+ ? { name: 'apikey', value: apiKey }
+ : { name: 'Authorization', value: `Bearer ${apiKey}` }
+
+export const ensureEdgeFunctionAuthorizationHeader = ({
+ headers,
+ serviceRoleKey,
+ verifyJwt,
+ createRow,
+}: EnsureEdgeFunctionAuthorizationHeaderParams): TRow[] => {
+ if (!verifyJwt || !serviceRoleKey || isNewApiKey(serviceRoleKey)) return headers
+
+ const isAuthorization = (header: HTTPHeader) =>
+ header.name.trim().toLowerCase() === 'authorization'
+ const authorizationIndex = headers.findIndex(isAuthorization)
+
+ if (authorizationIndex === -1) {
+ return [...headers, createRow('Authorization', `Bearer ${serviceRoleKey}`)]
+ }
+
+ const authorizationHeader = headers[authorizationIndex]
+ const normalizedHeaders = headers.filter(
+ (header, index) => index === authorizationIndex || !isAuthorization(header)
+ )
+
+ if (normalizedHeaders.length === headers.length && authorizationHeader.name === 'Authorization') {
+ return headers
+ }
+
+ normalizedHeaders[authorizationIndex] = {
+ ...authorizationHeader,
+ name: 'Authorization',
+ }
+ return normalizedHeaders
+}
+
export const buildEdgeFunctionHeaderAddActions = ({
apiKey,
- includeApiKeyHeader = false,
createRow,
-}: BuildEdgeFunctionHeaderAddActionsParams): KeyValueFieldArrayAction[] => [
- {
- key: 'add-auth-header',
- label: 'Add auth header with secret key',
- description: 'Required if your edge function enforces JWT verification',
- createRows: () => [
- createRow('Authorization', `Bearer ${apiKey}`),
- ...(includeApiKeyHeader ? [createRow('apikey', apiKey)] : []),
- ],
- },
- {
- key: 'add-source-header',
- label: 'Add custom source header',
- description: 'Useful to verify that the edge function was triggered from this webhook',
- createRows: () => createRow('x-supabase-webhook-source', '[Use a secret value]'),
- separatorAbove: true,
- },
-]
+}: BuildEdgeFunctionHeaderAddActionsParams): KeyValueFieldArrayAction[] => {
+ const authHeader = getEdgeFunctionAuthHeader(apiKey)
+
+ return [
+ {
+ key: 'add-auth-header',
+ label: 'Add secret key',
+ description:
+ authHeader.name === 'apikey'
+ ? 'Requires JWT verification to be disabled and authorization handled by the function'
+ : 'Required for edge functions that enforce JWT verification',
+ createRows: () => [createRow(authHeader.name, authHeader.value)],
+ },
+ {
+ key: 'add-source-header',
+ label: 'Add custom source',
+ description: 'Useful to verify that the edge function was triggered from this webhook',
+ createRows: () => createRow('x-supabase-webhook-source', '[Use a secret value]'),
+ separatorAbove: true,
+ },
+ ]
+}
diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/HttpHeaderFieldsSection.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/HttpHeaderFieldsSection.tsx
index 5212f876416..6d6fdb10263 100644
--- a/apps/studio/components/interfaces/Integrations/CronJobs/HttpHeaderFieldsSection.tsx
+++ b/apps/studio/components/interfaces/Integrations/CronJobs/HttpHeaderFieldsSection.tsx
@@ -29,7 +29,6 @@ export const HTTPHeaderFieldsSection = ({ variant }: HTTPHeaderFieldsSectionProp
variant === 'edge_function'
? buildEdgeFunctionHeaderAddActions({
apiKey,
- includeApiKeyHeader: serviceKey?.type === 'secret',
createRow: (name: string, value: string) => ({ name, value }),
})
: []
@@ -45,7 +44,7 @@ export const HTTPHeaderFieldsSection = ({ variant }: HTTPHeaderFieldsSectionProp
createEmptyRow={() => ({ name: '', value: '' })}
keyPlaceholder="Header name"
valuePlaceholder="Header value"
- addLabel="Add a new header"
+ addLabel="Add header"
addActions={addActions}
/>
diff --git a/e2e/studio/features/database-webhooks.spec.ts b/e2e/studio/features/database-webhooks.spec.ts
index 04849884fc5..b6bd2e523fe 100644
--- a/e2e/studio/features/database-webhooks.spec.ts
+++ b/e2e/studio/features/database-webhooks.spec.ts
@@ -56,7 +56,7 @@ const openWebhookEditor = async (page: Page, hookName: string) => {
}
const addCustomHeader = async (page: Page, name: string, value: string) => {
- await page.getByRole('button', { name: 'Add a new header' }).click()
+ await page.getByRole('button', { name: 'Add header' }).click()
await page.getByPlaceholder('Header name').last().fill(name)
await page.getByPlaceholder('Header value').last().fill(value)
}