diff --git a/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts b/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts index 81a27c36e..76929a086 100644 --- a/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts +++ b/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts @@ -12,6 +12,95 @@ const MGMT_API_BASE = MGMT_API_BASE_RAW.match(/^https?:\/\//) ? MGMT_API_BASE_RAW : `https://${MGMT_API_BASE_RAW}` +// Returns true if the projectRef passed is a non-branch project, false otherwise +async function canAccessProject(projectRef: string, accessToken: string): Promise { + const url = `${MGMT_API_BASE}/v1/projects/${projectRef}` + const response = await fetch(url, { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }) + return response.ok +} + +// Returns all projects refs +async function getAllProjects(accessToken: string): Promise { + const url = `${MGMT_API_BASE}/v1/projects` + const response = await fetch(url, { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }) + if (response.ok) { + const projects = (await response.json()) as Array<{ ref?: string }> + return Array.isArray(projects) + ? projects + .map((project) => project.ref) + .filter((projectRef): projectRef is string => Boolean(projectRef)) + : [] + } else { + return [] + } +} + +// Returns all branch project refs of a give projectRef +async function getAllBranches(projectRef: string, accessToken: string): Promise { + const url = `${MGMT_API_BASE}/v1/projects/${projectRef}/branches` + const response = await fetch(url, { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }) + if (response.ok) { + const branches = (await response.json()) as Array<{ project_ref?: string }> + return Array.isArray(branches) + ? branches + .map((branch) => branch.project_ref) + .filter((projectRef): projectRef is string => Boolean(projectRef)) + : [] + } else { + return [] + } +} + +// Authenticates with Supabase management API. Returns null if the accessToken has +// access to the projectRef project, other returns an error string +async function authenticateWithSupabase( + projectRef: string, + accessToken: string | null +): Promise { + if (!accessToken) { + return 'Unauthorized: Invalid credentials' + } + + // First we check if the token can return the project directly + // If it does then the token is valid for this project and + // we authenticate the request + const tokenCanAccesProject = await canAccessProject(projectRef, accessToken) + if (tokenCanAccesProject) { + return null + } + + // If the token does not return a project then projectRef could be a branch + // project, in which case we enumerate branch projects of all projects + const allProjectRefs = await getAllProjects(accessToken) + for (const ref of allProjectRefs) { + const branches = await getAllBranches(ref, accessToken) + if (branches.includes(projectRef)) { + return null + } + } + + // It's not even a branch project + return 'Unauthorized: Invalid credentials' +} + // Helper to delete edge function via Management API async function deleteEdgeFunction( projectRef: string, @@ -74,6 +163,11 @@ Deno.serve(async (req) => { const accessToken = authHeader.substring(7) // Remove 'Bearer ' + const supabaseAuthError = await authenticateWithSupabase(projectRef, accessToken) + if (supabaseAuthError) { + return new Response('Unauthorized', { status: 401 }) + } + // Handle GET requests for status if (req.method === 'GET') { const dbUrl = Deno.env.get('SUPABASE_DB_URL')