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