@@ -60,6 +60,45 @@ async function deleteSecret(
6060 }
6161}
6262
63+ // Thrown when setup-secret validation fails; carries the HTTP status for the response.
64+ class SetupAuthError extends Error {
65+ constructor (
66+ readonly status : number ,
67+ message : string
68+ ) {
69+ super ( message )
70+ }
71+ }
72+
73+ // Validates the caller's bearer secret against the per-install setup secret in vault.
74+ // Returns only on a strict match with a non-empty stored secret; throws on every other case.
75+ async function validateSetupSecret ( callerSecret : string ) : Promise < void > {
76+ const dbUrl = Deno . env . get ( 'SUPABASE_DB_URL' )
77+ if ( ! dbUrl ) {
78+ throw new SetupAuthError ( 500 , 'SUPABASE_DB_URL not set' )
79+ }
80+
81+ let authSql : ReturnType < typeof postgres > | undefined
82+ try {
83+ authSql = postgres ( dbUrl , { max : 1 , prepare : false } )
84+ const secretResult = await authSql `
85+ SELECT decrypted_secret
86+ FROM vault.decrypted_secrets
87+ WHERE name = ${ SETUP_SECRET_NAME }
88+ `
89+ const storedSecret : unknown = secretResult [ 0 ] ?. decrypted_secret
90+ if ( typeof storedSecret !== 'string' || storedSecret . length === 0 ) {
91+ throw new SetupAuthError ( 500 , 'Setup secret not configured in vault' )
92+ }
93+ if ( callerSecret === storedSecret ) {
94+ return
95+ }
96+ throw new SetupAuthError ( 403 , 'Forbidden: Invalid setup secret' )
97+ } finally {
98+ if ( authSql ) await authSql . end ( )
99+ }
100+ }
101+
63102Deno . serve ( async ( req ) => {
64103 // Extract project ref from SUPABASE_URL (format: https://{projectRef}.{base})
65104 const supabaseUrl = Deno . env . get ( 'SUPABASE_URL' )
@@ -89,39 +128,17 @@ Deno.serve(async (req) => {
89128 // never has to trust the bearer token for privileged Management operations.
90129 const accessToken = req . headers . get ( 'x-management-api-token' ) ?? ''
91130
92- {
93- const dbUrl = Deno . env . get ( 'SUPABASE_DB_URL' )
94- if ( ! dbUrl ) {
95- return new Response ( JSON . stringify ( { error : 'SUPABASE_DB_URL not set' } ) , {
96- status : 500 ,
97- headers : { 'Content-Type' : 'application/json' } ,
98- } )
99- }
100-
101- let authSql : ReturnType < typeof postgres > | undefined
102- try {
103- authSql = postgres ( dbUrl , { max : 1 , prepare : false } )
104- const secretResult = await authSql `
105- SELECT decrypted_secret
106- FROM vault.decrypted_secrets
107- WHERE name = ${ SETUP_SECRET_NAME }
108- `
109- if ( secretResult . length === 0 ) {
110- return new Response ( 'Setup secret not configured in vault' , { status : 500 } )
111- }
112- if ( callerSecret !== secretResult [ 0 ] . decrypted_secret ) {
113- return new Response ( 'Forbidden: Invalid setup secret' , { status : 403 } )
114- }
115- } catch ( error : unknown ) {
116- const err = error as Error
117- console . error ( 'Setup secret validation error:' , error )
118- return new Response ( JSON . stringify ( { error : err . message } ) , {
119- status : 500 ,
120- headers : { 'Content-Type' : 'application/json' } ,
121- } )
122- } finally {
123- if ( authSql ) await authSql . end ( )
131+ try {
132+ await validateSetupSecret ( callerSecret )
133+ } catch ( error : unknown ) {
134+ if ( error instanceof SetupAuthError ) {
135+ return new Response ( error . message , { status : error . status } )
124136 }
137+ console . error ( 'Setup secret validation error:' , error )
138+ return new Response ( JSON . stringify ( { error : ( error as Error ) . message } ) , {
139+ status : 500 ,
140+ headers : { 'Content-Type' : 'application/json' } ,
141+ } )
125142 }
126143
127144 const supabaseAuthError = await authenticateWithSupabase ( MGMT_API_BASE , projectRef , accessToken )
0 commit comments