mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
fix: selfhost edge URLs (#47861)
This commit is contained in:
1 parent
dac5756295
commit
05d8dd356c
14 files changed
+196
-69
No files matched your search
@@ -21,22 +21,11 @@ import {
|
||||
} from '@/data/table-editor/table-editor-query'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose'
|
||||
import { isEdgeFunctionUrl } from '@/lib/api/edgeFunctions'
|
||||
import { uuidv4 } from '@/lib/helpers'
|
||||
|
||||
export type HTTPArgument = { id: string; name: string; value: string }
|
||||
|
||||
export const isEdgeFunction = ({
|
||||
ref,
|
||||
restUrlTld,
|
||||
url,
|
||||
}: {
|
||||
ref?: string
|
||||
restUrlTld?: string
|
||||
url: string
|
||||
}) =>
|
||||
url.includes(`https://${ref}.functions.supabase.${restUrlTld}/`) ||
|
||||
url.includes(`https://${ref}.supabase.${restUrlTld}/functions/`)
|
||||
|
||||
const FORM_ID = 'edit-hook-panel-form'
|
||||
|
||||
const parseHeaders = (selectedHook?: PGTrigger): HTTPArgument[] => {
|
||||
@@ -143,7 +132,6 @@ export const EditHookPanel = () => {
|
||||
const isSubmitting = isCreating || isUpdating || isLoadingTable
|
||||
|
||||
const restUrl = project?.restUrl
|
||||
const restUrlTld = restUrl ? new URL(restUrl).hostname.split('.').pop() : 'co'
|
||||
|
||||
const form = useForm<WebhookFormValues>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
@@ -152,11 +140,7 @@ export const EditHookPanel = () => {
|
||||
table_id: selectedHook?.table_id?.toString() ?? '',
|
||||
http_url: selectedHook?.function_args?.[0] ?? '',
|
||||
http_method: (selectedHook?.function_args?.[1] as 'GET' | 'POST') ?? 'POST',
|
||||
function_type: isEdgeFunction({
|
||||
ref,
|
||||
restUrlTld,
|
||||
url: selectedHook?.function_args?.[0] ?? '',
|
||||
})
|
||||
function_type: isEdgeFunctionUrl(selectedHook?.function_args?.[0] ?? '', ref ?? '', restUrl)
|
||||
? 'supabase_function'
|
||||
: 'http_request',
|
||||
timeout_ms: Number(selectedHook?.function_args?.[4] ?? 5000),
|
||||
@@ -188,11 +172,7 @@ export const EditHookPanel = () => {
|
||||
table_id: selectedHook?.table_id?.toString() ?? '',
|
||||
http_url: selectedHook?.function_args?.[0] ?? '',
|
||||
http_method: (selectedHook?.function_args?.[1] as 'GET' | 'POST') ?? 'POST',
|
||||
function_type: isEdgeFunction({
|
||||
ref,
|
||||
restUrlTld,
|
||||
url: selectedHook?.function_args?.[0] ?? '',
|
||||
})
|
||||
function_type: isEdgeFunctionUrl(selectedHook?.function_args?.[0] ?? '', ref ?? '', restUrl)
|
||||
? 'supabase_function'
|
||||
: 'http_request',
|
||||
timeout_ms: Number(selectedHook?.function_args?.[4] ?? 5000),
|
||||
@@ -201,7 +181,7 @@ export const EditHookPanel = () => {
|
||||
httpParameters: parseParameters(selectedHook),
|
||||
})
|
||||
}
|
||||
}, [visible, selectedHook, ref, restUrlTld, form])
|
||||
}, [visible, selectedHook, ref, restUrl, form])
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const onSubmit: SubmitHandler<WebhookFormValues> = async (values) => {
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
} from 'ui'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
|
||||
import { isEdgeFunction } from './EditHookPanel'
|
||||
import { WebhookFormValues } from './EditHookPanel.constants'
|
||||
import { AVAILABLE_WEBHOOK_TYPES, HOOK_EVENTS } from './Hooks.constants'
|
||||
import { HTTPHeaders } from './HTTPHeaders'
|
||||
@@ -38,6 +37,7 @@ import { useEdgeFunctionsQuery } from '@/data/edge-functions/edge-functions-quer
|
||||
import { useTableNamesQuery } from '@/data/tables/table-names-query'
|
||||
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { buildDatabaseEdgeFunctionUrl, isEdgeFunctionUrl } from '@/lib/api/edgeFunctions'
|
||||
import { uuidv4 } from '@/lib/helpers'
|
||||
|
||||
export interface FormContentsProps {
|
||||
@@ -50,7 +50,6 @@ export const FormContents = ({ form, selectedHook }: FormContentsProps) => {
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
|
||||
const restUrl = project?.restUrl
|
||||
const restUrlTld = restUrl ? new URL(restUrl).hostname.split('.').pop() : 'co'
|
||||
|
||||
const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*')
|
||||
const { data: keys = [] } = useAPIKeysQuery(
|
||||
@@ -75,7 +74,7 @@ export const FormContents = ({ form, selectedHook }: FormContentsProps) => {
|
||||
useEffect(() => {
|
||||
if (!isSuccessEdgeFunctions) return
|
||||
|
||||
const isEdgeFunctionSelected = isEdgeFunction({ ref, restUrlTld, url: httpUrl })
|
||||
const isEdgeFunctionSelected = isEdgeFunctionUrl(httpUrl, ref ?? '', restUrl)
|
||||
|
||||
if (httpUrl && isEdgeFunctionSelected) {
|
||||
const fnSlug = httpUrl.split('/').at(-1)
|
||||
@@ -98,8 +97,16 @@ export const FormContents = ({ form, selectedHook }: FormContentsProps) => {
|
||||
form.setValue('httpHeaders', updatedHttpHeaders)
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [httpUrl, isSuccessEdgeFunctions])
|
||||
}, [
|
||||
form,
|
||||
functions,
|
||||
httpHeaders,
|
||||
httpUrl,
|
||||
isSuccessEdgeFunctions,
|
||||
legacyServiceRole,
|
||||
ref,
|
||||
restUrl,
|
||||
])
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -235,9 +242,13 @@ export const FormContents = ({ form, selectedHook }: FormContentsProps) => {
|
||||
} else if (functionType === 'supabase_function') {
|
||||
// Default to first edge function in the list
|
||||
const fnSlug = functions[0]?.slug
|
||||
const defaultFunctionUrl = `https://${ref}.supabase.${restUrlTld}/functions/v1/${fnSlug}`
|
||||
const defaultFunctionUrl = buildDatabaseEdgeFunctionUrl(
|
||||
fnSlug ?? '',
|
||||
ref ?? '',
|
||||
restUrl
|
||||
)
|
||||
const currentUrl = form.getValues('http_url')
|
||||
if (!isEdgeFunction({ ref, restUrlTld, url: currentUrl })) {
|
||||
if (!isEdgeFunctionUrl(currentUrl, ref ?? '', restUrl)) {
|
||||
form.setValue('http_url', defaultFunctionUrl, { shouldDirty: false })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from '@/components/ui/Forms/FormSection'
|
||||
import { useEdgeFunctionsQuery } from '@/data/edge-functions/edge-functions-query'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { buildDatabaseEdgeFunctionUrl } from '@/lib/api/edgeFunctions'
|
||||
|
||||
interface HTTPRequestConfigProps {
|
||||
form: UseFormReturn<WebhookFormValues>
|
||||
@@ -116,8 +117,7 @@ export const HTTPRequestConfig = ({ form }: HTTPRequestConfigProps) => {
|
||||
<SelectContent>
|
||||
{edgeFunctions.map((fn) => {
|
||||
const restUrl = selectedProject?.restUrl
|
||||
const restUrlTld = restUrl ? new URL(restUrl).hostname.split('.').pop() : 'co'
|
||||
const functionUrl = `https://${ref}.supabase.${restUrlTld}/functions/v1/${fn.slug}`
|
||||
const functionUrl = buildDatabaseEdgeFunctionUrl(fn.slug, ref ?? '', restUrl)
|
||||
|
||||
return (
|
||||
<SelectItem key={fn.id} value={functionUrl}>
|
||||
|
||||
@@ -19,6 +19,7 @@ import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
|
||||
import { useDatabaseHooksQuery } from '@/data/database-triggers/database-triggers-query'
|
||||
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { isEdgeFunctionUrl } from '@/lib/api/edgeFunctions'
|
||||
import { BASE_PATH } from '@/lib/constants'
|
||||
|
||||
export interface HookListProps {
|
||||
@@ -38,7 +39,6 @@ export const HookList = ({ schema, filterString }: HookListProps) => {
|
||||
const [, setSelectedHookIdToDelete] = useQueryState('delete', parseAsString.withDefault(''))
|
||||
|
||||
const restUrl = project?.restUrl
|
||||
const restUrlTld = restUrl ? new URL(restUrl).hostname.split('.').pop() : 'co'
|
||||
|
||||
const filteredHooks = (hooks ?? []).filter(
|
||||
(x) =>
|
||||
@@ -54,10 +54,8 @@ export const HookList = ({ schema, filterString }: HookListProps) => {
|
||||
return (
|
||||
<>
|
||||
{filteredHooks.map((x) => {
|
||||
const isEdgeFunction = (url: string) =>
|
||||
url.includes(`https://${ref}.functions.supabase.${restUrlTld}/`) ||
|
||||
url.includes(`https://${ref}.supabase.${restUrlTld}/functions/`)
|
||||
const [url, method] = x.function_args
|
||||
const isEdgeFunction = isEdgeFunctionUrl(url, ref ?? '', restUrl)
|
||||
|
||||
return (
|
||||
<Table.tr key={x.id}>
|
||||
@@ -66,7 +64,7 @@ export const HookList = ({ schema, filterString }: HookListProps) => {
|
||||
<div>
|
||||
<Image
|
||||
src={
|
||||
isEdgeFunction(url)
|
||||
isEdgeFunction
|
||||
? `${BASE_PATH}/img/function-providers/supabase-severless-function.png`
|
||||
: `${BASE_PATH}/img/function-providers/http-request.png`
|
||||
}
|
||||
@@ -74,7 +72,7 @@ export const HookList = ({ schema, filterString }: HookListProps) => {
|
||||
layout="fixed"
|
||||
width="20"
|
||||
height="20"
|
||||
title={isEdgeFunction(url) ? 'Supabase Edge Function' : 'HTTP Request'}
|
||||
title={isEdgeFunction ? 'Supabase Edge Function' : 'HTTP Request'}
|
||||
/>
|
||||
</div>
|
||||
<p title={x.name} className="truncate">
|
||||
|
||||
@@ -37,10 +37,6 @@ export const TerminalInstructions = forwardRef<
|
||||
|
||||
const apiKey = publishableKey?.api_key ?? anonKey?.api_key ?? '[YOUR ANON KEY]'
|
||||
|
||||
// get the .co or .net TLD from the restUrl
|
||||
const restUrl = `https://${endpoint}`
|
||||
const restUrlTld = !!endpoint ? new URL(restUrl).hostname.split('.').pop() : 'co'
|
||||
|
||||
const commands: Commands[] = [
|
||||
{
|
||||
command: 'supabase functions new hello-world',
|
||||
@@ -68,7 +64,7 @@ export const TerminalInstructions = forwardRef<
|
||||
comment: 'Deploy your function',
|
||||
},
|
||||
{
|
||||
command: `curl -L -X POST 'https://${projectRef}.supabase.${restUrlTld}/functions/v1/hello-world' -H 'Authorization: Bearer ${apiKey}'${anonKey?.type === 'publishable' ? ` -H 'apikey: ${apiKey}'` : ''} --data '{"name":"Functions"}'`,
|
||||
command: `curl -L -X POST '${functionsEndpoint}/hello-world' -H 'Authorization: Bearer ${apiKey}'${anonKey?.type === 'publishable' ? ` -H 'apikey: ${apiKey}'` : ''} --data '{"name":"Functions"}'`,
|
||||
description: 'Invokes the hello-world function',
|
||||
jsx: () => {
|
||||
return (
|
||||
|
||||
+5
-1
@@ -122,7 +122,11 @@ export const CreateCronJobSheet = ({ open, selectedCronJob, onClose }: CreateCro
|
||||
'extensions'
|
||||
)
|
||||
|
||||
const cronJobValues = parseCronJobCommand(selectedCronJob?.command || '', project?.ref!)
|
||||
const cronJobValues = parseCronJobCommand(
|
||||
selectedCronJob?.command || '',
|
||||
project?.ref!,
|
||||
project?.restUrl
|
||||
)
|
||||
|
||||
const defaultValues = {
|
||||
name: selectedCronJob?.jobname || '',
|
||||
|
||||
@@ -54,7 +54,7 @@ export const CronJobPage = () => {
|
||||
const { data: edgeFunctions = [] } = useEdgeFunctionsQuery({ projectRef: project?.ref })
|
||||
|
||||
// Parse the cron job command to check if it's an edge function
|
||||
const cronJobValues = parseCronJobCommand(job?.command || '', project?.ref!)
|
||||
const cronJobValues = parseCronJobCommand(job?.command || '', project?.ref!, project?.restUrl)
|
||||
const edgeFunction =
|
||||
cronJobValues.type === 'edge_function' ? cronJobValues.edgeFunctionName : undefined
|
||||
const edgeFunctionSlug = edgeFunction?.split('/functions/v1/').pop()
|
||||
|
||||
@@ -151,6 +151,24 @@ describe('parseCronJobCommand', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should return an edge function config for a self-hosted function URL', () => {
|
||||
const command = `select net.http_post( url:='http://kong:8000/functions/v1/_', headers:=jsonb_build_object('Authorization', 'Bearer something'), timeout_milliseconds:=5000 );`
|
||||
expect(parseCronJobCommand(command, 'default', undefined, false)).toMatchObject({
|
||||
edgeFunctionName: 'http://kong:8000/functions/v1/_',
|
||||
type: 'edge_function',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return an edge function config for a self-hosted public function URL', () => {
|
||||
const command = `select net.http_post( url:='https://default.supabase.localhost/functions/v1/_', headers:=jsonb_build_object('Authorization', 'Bearer something'), timeout_milliseconds:=5000 );`
|
||||
expect(
|
||||
parseCronJobCommand(command, 'default', 'https://default.supabase.localhost/rest/v1/', false)
|
||||
).toMatchObject({
|
||||
edgeFunctionName: 'https://default.supabase.localhost/functions/v1/_',
|
||||
type: 'edge_function',
|
||||
})
|
||||
})
|
||||
|
||||
it("should return an HTTP request config when there's a query parameter or hash in the URL (also handles edge function)", () => {
|
||||
const command = `select net.http_post( url:='https://random_project_ref.supabase.co/functions/v1/_?first=1#second=2', headers:=jsonb_build_object('Authorization', 'Bearer something'), timeout_milliseconds:=5000 )`
|
||||
expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({
|
||||
|
||||
@@ -7,6 +7,8 @@ import { CronJobType } from './CreateCronJobSheet/CreateCronJobSheet.constants'
|
||||
import { CRON_TABLE_COLUMNS, HTTPHeader, secondsPattern } from './CronJobs.constants'
|
||||
import { CronJobTableCell } from './CronJobTableCell'
|
||||
import { CronJob } from '@/data/database-cron-jobs/database-cron-jobs-infinite-query'
|
||||
import { isEdgeFunctionUrl } from '@/lib/api/edgeFunctions'
|
||||
import { IS_PLATFORM } from '@/lib/constants'
|
||||
|
||||
const unescapeSqlLiteral = (value = '', isEscapeString = false) => {
|
||||
const unescaped = value.replaceAll("''", "'")
|
||||
@@ -129,7 +131,12 @@ const DEFAULT_CRONJOB_COMMAND = {
|
||||
httpBody: '',
|
||||
} as const
|
||||
|
||||
export const parseCronJobCommand = (originalCommand: string, projectRef: string): CronJobType => {
|
||||
export const parseCronJobCommand = (
|
||||
originalCommand: string,
|
||||
projectRef: string,
|
||||
restUrl?: string,
|
||||
isPlatform = IS_PLATFORM
|
||||
): CronJobType => {
|
||||
const command = originalCommand.replaceAll('$$', ' ').replaceAll(/\n/g, ' ').trim()
|
||||
|
||||
if (command.toLocaleLowerCase().match(/^select\s+net\./)) {
|
||||
@@ -181,8 +188,7 @@ export const parseCronJobCommand = (originalCommand: string, projectRef: string)
|
||||
} catch {}
|
||||
|
||||
if (
|
||||
url.includes(`${projectRef}.supabase.`) &&
|
||||
url.includes('/functions/v1/') &&
|
||||
isEdgeFunctionUrl(url, projectRef, restUrl, isPlatform) &&
|
||||
searchParams.length === 0 &&
|
||||
urlHash.length === 0
|
||||
) {
|
||||
|
||||
@@ -32,7 +32,7 @@ export const DeleteCronJob = () => {
|
||||
} = useDatabaseCronJobDeleteMutation({
|
||||
onSuccess: () => {
|
||||
if (cronJob && project) {
|
||||
const { type } = parseCronJobCommand(cronJob.command, project.ref)
|
||||
const { type } = parseCronJobCommand(cronJob.command, project.ref, project.restUrl)
|
||||
track('cron_job_removed', { type })
|
||||
}
|
||||
toast.success(`Successfully removed cron job`)
|
||||
|
||||
@@ -37,17 +37,12 @@ import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import { CreateCronJobForm } from './CreateCronJobSheet/CreateCronJobSheet.constants'
|
||||
import { useEdgeFunctionsQuery } from '@/data/edge-functions/edge-functions-query'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { buildDatabaseEdgeFunctionUrl } from '@/lib/api/edgeFunctions'
|
||||
|
||||
interface HTTPRequestFieldsProps {
|
||||
form: UseFormReturn<CreateCronJobForm>
|
||||
}
|
||||
|
||||
const buildFunctionUrl = (slug: string, projectRef: string, restUrl?: string) => {
|
||||
const restUrlTld = restUrl ? new URL(restUrl).hostname.split('.').pop() : 'co'
|
||||
const functionUrl = `https://${projectRef}.supabase.${restUrlTld}/functions/v1/${slug}`
|
||||
return functionUrl
|
||||
}
|
||||
|
||||
export const EdgeFunctionSection = ({ form }: HTTPRequestFieldsProps) => {
|
||||
const { ref } = useParams()
|
||||
const { data: selectedProject } = useSelectedProjectQuery()
|
||||
@@ -62,7 +57,11 @@ export const EdgeFunctionSection = ({ form }: HTTPRequestFieldsProps) => {
|
||||
() =>
|
||||
functions?.map((fn) => ({
|
||||
...fn,
|
||||
url: buildFunctionUrl(fn.slug, selectedProject?.ref || '', selectedProject?.restUrl),
|
||||
url: buildDatabaseEdgeFunctionUrl(
|
||||
fn.slug,
|
||||
selectedProject?.ref || '',
|
||||
selectedProject?.restUrl
|
||||
),
|
||||
})) ?? [],
|
||||
[functions, selectedProject]
|
||||
)
|
||||
|
||||
@@ -40,18 +40,10 @@ export const EdgeFunctionRenderer = ({
|
||||
|
||||
const functionUrl = useMemo(() => {
|
||||
const endpoint = settings?.app_config?.endpoint
|
||||
const protocol = settings?.app_config?.protocol ?? 'https'
|
||||
if (!endpoint || !ref || !functionName) return undefined
|
||||
|
||||
try {
|
||||
const url = new URL(`https://${endpoint}`)
|
||||
const restUrlTld = url.hostname.split('.').pop()
|
||||
return restUrlTld
|
||||
? `https://${ref}.supabase.${restUrlTld}/functions/v1/${functionName}`
|
||||
: undefined
|
||||
} catch (error) {
|
||||
return undefined
|
||||
}
|
||||
}, [settings?.app_config?.endpoint, ref, functionName])
|
||||
return `${protocol}://${endpoint}/functions/v1/${functionName}`
|
||||
}, [settings?.app_config?.endpoint, settings?.app_config?.protocol, ref, functionName])
|
||||
|
||||
const deploymentDetailsUrl = useMemo(() => {
|
||||
if (!ref || !functionName) return undefined
|
||||
|
||||
@@ -1,6 +1,94 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { isValidEdgeFunctionURL } from './edgeFunctions'
|
||||
import {
|
||||
buildDatabaseEdgeFunctionUrl,
|
||||
isEdgeFunctionUrl,
|
||||
isValidEdgeFunctionURL,
|
||||
} from './edgeFunctions'
|
||||
|
||||
describe('buildDatabaseEdgeFunctionUrl', () => {
|
||||
it('builds a platform edge function URL', () => {
|
||||
expect(
|
||||
buildDatabaseEdgeFunctionUrl(
|
||||
'hello-world',
|
||||
'uniquetwentychararef',
|
||||
'https://uniquetwentychararef.supabase.red/rest/v1/',
|
||||
true
|
||||
)
|
||||
).toBe('https://uniquetwentychararef.supabase.red/functions/v1/hello-world')
|
||||
})
|
||||
|
||||
it('builds a self-hosted URL reachable from Postgres', () => {
|
||||
expect(
|
||||
buildDatabaseEdgeFunctionUrl(
|
||||
'hello-world',
|
||||
'default',
|
||||
'http://localhost:8000/rest/v1/',
|
||||
false
|
||||
)
|
||||
).toBe('http://kong:8000/functions/v1/hello-world')
|
||||
})
|
||||
})
|
||||
|
||||
describe('isEdgeFunctionUrl', () => {
|
||||
it('matches platform edge function URLs for the current project', () => {
|
||||
expect(
|
||||
isEdgeFunctionUrl(
|
||||
'https://uniquetwentychararef.supabase.co/functions/v1/hello-world',
|
||||
'uniquetwentychararef',
|
||||
'https://uniquetwentychararef.supabase.co/rest/v1/',
|
||||
true
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('matches self-hosted edge function URLs', () => {
|
||||
expect(
|
||||
isEdgeFunctionUrl(
|
||||
'http://kong:8000/functions/v1/hello-world',
|
||||
'default',
|
||||
'http://localhost:8000/rest/v1/',
|
||||
false
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: 'a project URL without the functions path',
|
||||
url: 'https://uniquetwentychararef.supabase.co/rest/v1/hello-world',
|
||||
projectRef: 'uniquetwentychararef',
|
||||
restUrl: 'https://uniquetwentychararef.supabase.co/rest/v1/',
|
||||
isPlatform: true,
|
||||
},
|
||||
{
|
||||
name: "another project's edge function URL",
|
||||
url: 'https://anotherprojectref000.supabase.co/functions/v1/hello-world',
|
||||
projectRef: 'uniquetwentychararef',
|
||||
restUrl: 'https://uniquetwentychararef.supabase.co/rest/v1/',
|
||||
isPlatform: true,
|
||||
},
|
||||
{
|
||||
name: 'a lookalike project origin',
|
||||
url: 'https://uniquetwentychararef.supabase.example.com/functions/v1/hello-world',
|
||||
projectRef: 'uniquetwentychararef',
|
||||
restUrl: 'https://uniquetwentychararef.supabase.co/rest/v1/',
|
||||
isPlatform: true,
|
||||
},
|
||||
{
|
||||
name: 'a self-hosted non-function URL',
|
||||
url: 'http://kong:8000/rest/v1/hello-world',
|
||||
projectRef: 'default',
|
||||
restUrl: 'http://localhost:8000/rest/v1/',
|
||||
isPlatform: false,
|
||||
},
|
||||
])('does not match $name', ({ url, projectRef, restUrl, isPlatform }) => {
|
||||
expect(
|
||||
isEdgeFunctionUrl(url, projectRef, restUrl, isPlatform),
|
||||
`Expected ${url} not to match an edge function URL`
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isValidEdgeFunctionURL', () => {
|
||||
const validEdgeFunctionUrls = [
|
||||
|
||||
@@ -1,4 +1,39 @@
|
||||
import { IS_PLATFORM } from '@/lib/constants'
|
||||
|
||||
const NIMBUS_PROD_PROJECTS_URL = process.env.NIMBUS_PROD_PROJECTS_URL
|
||||
// Cron jobs and database hooks run inside Postgres, where Kong is available by this network alias.
|
||||
const SELF_HOSTED_EDGE_FUNCTIONS_URL = 'http://kong:8000/functions/v1'
|
||||
const PLATFORM_TLDS = ['co', 'red'] as const
|
||||
|
||||
export const buildDatabaseEdgeFunctionUrl = (
|
||||
slug: string,
|
||||
projectRef: string,
|
||||
restUrl?: string,
|
||||
isPlatform = IS_PLATFORM
|
||||
) => {
|
||||
if (!isPlatform) return `${SELF_HOSTED_EDGE_FUNCTIONS_URL}/${slug}`
|
||||
|
||||
const projectOrigin = restUrl ? new URL(restUrl).origin : `https://${projectRef}.supabase.co`
|
||||
return `${projectOrigin}/functions/v1/${slug}`
|
||||
}
|
||||
|
||||
export const isEdgeFunctionUrl = (
|
||||
url: string,
|
||||
projectRef: string,
|
||||
restUrl?: string,
|
||||
isPlatform = IS_PLATFORM
|
||||
) => {
|
||||
if (!isPlatform && url.startsWith(`${SELF_HOSTED_EDGE_FUNCTIONS_URL}/`)) return true
|
||||
|
||||
const projectOrigin = restUrl ? new URL(restUrl).origin : undefined
|
||||
if (projectOrigin && url.startsWith(`${projectOrigin}/functions/v1/`)) return true
|
||||
|
||||
return PLATFORM_TLDS.some(
|
||||
(tld) =>
|
||||
url.startsWith(`https://${projectRef}.functions.supabase.${tld}/`) ||
|
||||
url.startsWith(`https://${projectRef}.supabase.${tld}/functions/`)
|
||||
)
|
||||
}
|
||||
|
||||
export const isValidEdgeFunctionURL = (url: string, isPlatform: boolean) => {
|
||||
if (NIMBUS_PROD_PROJECTS_URL !== undefined) {
|
||||
|
||||
Reference in new issue
Block a user