@@ -8,7 +8,7 @@ import { getPgPool } from 'pg-cache';
88
99import errorPage50x from '../errors/50x' ;
1010import errorPage404Message from '../errors/404-message' ;
11- import { ApiConfigResult , ApiError , ApiOptions , ApiStructure , AuthSettings , RlsModule } from '../types' ;
11+ import { ApiConfigResult , ApiError , ApiOptions , ApiStructure , AuthSettings , DatabaseSettings , PubkeyChallengeSettings , RlsModule , WebauthnSettings } from '../types' ;
1212import './types' ;
1313
1414const log = new Logger ( 'api' ) ;
@@ -139,6 +139,100 @@ const AUTH_SETTINGS_SQL = (schemaName: string, tableName: string) => `
139139 LIMIT 1
140140` ;
141141
142+ const CORS_SETTINGS_SQL = `
143+ SELECT allowed_origins
144+ FROM services_public.cors_settings
145+ WHERE database_id = $1 AND api_id = $2
146+ LIMIT 1
147+ ` ;
148+
149+ const CORS_SETTINGS_DB_DEFAULT_SQL = `
150+ SELECT allowed_origins
151+ FROM services_public.cors_settings
152+ WHERE database_id = $1 AND api_id IS NULL
153+ LIMIT 1
154+ ` ;
155+
156+ const CORS_MODULE_SQL = `
157+ SELECT data
158+ FROM services_public.api_modules
159+ WHERE api_id = $1 AND name = 'cors'
160+ LIMIT 1
161+ ` ;
162+
163+ const PUBKEY_SETTINGS_SQL = `
164+ SELECT
165+ s.schema_name AS schema,
166+ ps.crypto_network,
167+ sign_up_fn.name AS sign_up_with_key,
168+ sign_in_req_fn.name AS sign_in_request_challenge,
169+ sign_in_fail_fn.name AS sign_in_record_failure,
170+ sign_in_fn.name AS sign_in_with_challenge
171+ FROM services_public.pubkey_settings ps
172+ LEFT JOIN metaschema_public.schema s ON ps.schema_id = s.id
173+ LEFT JOIN metaschema_public.function sign_up_fn ON ps.sign_up_with_key_function_id = sign_up_fn.id
174+ LEFT JOIN metaschema_public.function sign_in_req_fn ON ps.sign_in_request_challenge_function_id = sign_in_req_fn.id
175+ LEFT JOIN metaschema_public.function sign_in_fail_fn ON ps.sign_in_record_failure_function_id = sign_in_fail_fn.id
176+ LEFT JOIN metaschema_public.function sign_in_fn ON ps.sign_in_with_challenge_function_id = sign_in_fn.id
177+ WHERE ps.database_id = $1
178+ LIMIT 1
179+ ` ;
180+
181+ const PUBKEY_MODULE_SQL = `
182+ SELECT data
183+ FROM services_public.api_modules
184+ WHERE api_id = $1 AND name = 'pubkey_challenge'
185+ LIMIT 1
186+ ` ;
187+
188+ const WEBAUTHN_SETTINGS_SQL = `
189+ SELECT
190+ s.schema_name AS schema,
191+ cred_s.schema_name AS credentials_schema,
192+ sess_s.schema_name AS sessions_schema,
193+ sec_s.schema_name AS session_secrets_schema,
194+ ws.rp_id,
195+ ws.rp_name,
196+ ws.origin_allowlist,
197+ ws.attestation_type,
198+ ws.require_user_verification,
199+ ws.resident_key,
200+ ws.challenge_expiry_seconds
201+ FROM services_public.webauthn_settings ws
202+ LEFT JOIN metaschema_public.schema s ON ws.schema_id = s.id
203+ LEFT JOIN metaschema_public.schema cred_s ON ws.credentials_schema_id = cred_s.id
204+ LEFT JOIN metaschema_public.schema sess_s ON ws.sessions_schema_id = sess_s.id
205+ LEFT JOIN metaschema_public.schema sec_s ON ws.session_secrets_schema_id = sec_s.id
206+ WHERE ws.database_id = $1
207+ LIMIT 1
208+ ` ;
209+
210+ const DATABASE_SETTINGS_SQL = `
211+ SELECT
212+ ds.enable_aggregates,
213+ ds.enable_postgis,
214+ ds.enable_search,
215+ ds.enable_direct_uploads,
216+ ds.enable_presigned_uploads,
217+ ds.enable_many_to_many,
218+ ds.enable_connection_filter,
219+ ds.enable_ltree,
220+ ds.enable_llm,
221+ COALESCE(aps.enable_aggregates, ds.enable_aggregates) AS resolved_enable_aggregates,
222+ COALESCE(aps.enable_postgis, ds.enable_postgis) AS resolved_enable_postgis,
223+ COALESCE(aps.enable_search, ds.enable_search) AS resolved_enable_search,
224+ COALESCE(aps.enable_direct_uploads, ds.enable_direct_uploads) AS resolved_enable_direct_uploads,
225+ COALESCE(aps.enable_presigned_uploads, ds.enable_presigned_uploads) AS resolved_enable_presigned_uploads,
226+ COALESCE(aps.enable_many_to_many, ds.enable_many_to_many) AS resolved_enable_many_to_many,
227+ COALESCE(aps.enable_connection_filter, ds.enable_connection_filter) AS resolved_enable_connection_filter,
228+ COALESCE(aps.enable_ltree, ds.enable_ltree) AS resolved_enable_ltree,
229+ COALESCE(aps.enable_llm, ds.enable_llm) AS resolved_enable_llm
230+ FROM services_public.database_settings ds
231+ LEFT JOIN services_public.api_settings aps ON ds.database_id = aps.database_id AND aps.api_id = $2
232+ WHERE ds.database_id = $1
233+ LIMIT 1
234+ ` ;
235+
142236// =============================================================================
143237// Types
144238// =============================================================================
@@ -179,6 +273,60 @@ interface RlsModuleRow {
179273 data : RlsModuleData | null ;
180274}
181275
276+ interface CorsSettingsRow {
277+ allowed_origins : string [ ] ;
278+ }
279+
280+ interface CorsModuleRow {
281+ data : { urls : string [ ] } | null ;
282+ }
283+
284+ interface PubkeySettingsRow {
285+ schema : string ;
286+ crypto_network : string ;
287+ sign_up_with_key : string ;
288+ sign_in_request_challenge : string ;
289+ sign_in_record_failure : string ;
290+ sign_in_with_challenge : string ;
291+ }
292+
293+ interface PubkeyModuleRow {
294+ data : {
295+ schema : string ;
296+ crypto_network : string ;
297+ sign_up_with_key : string ;
298+ sign_in_request_challenge : string ;
299+ sign_in_record_failure : string ;
300+ sign_in_with_challenge : string ;
301+ } | null ;
302+ }
303+
304+ interface WebauthnSettingsRow {
305+ schema : string ;
306+ credentials_schema : string ;
307+ sessions_schema : string ;
308+ session_secrets_schema : string ;
309+ rp_id : string ;
310+ rp_name : string ;
311+ origin_allowlist : string [ ] ;
312+ attestation_type : string ;
313+ require_user_verification : boolean ;
314+ resident_key : string ;
315+ challenge_expiry_seconds : number ;
316+ }
317+
318+ interface DatabaseSettingsRow {
319+ resolved_enable_aggregates : boolean ;
320+ resolved_enable_postgis : boolean ;
321+ resolved_enable_search : boolean ;
322+ resolved_enable_direct_uploads : boolean ;
323+ resolved_enable_presigned_uploads : boolean ;
324+ resolved_enable_many_to_many : boolean ;
325+ resolved_enable_connection_filter : boolean ;
326+ resolved_enable_ltree : boolean ;
327+ resolved_enable_llm : boolean ;
328+ }
329+
182330interface ApiListRow {
183331 id : string ;
184332 database_id : string ;
@@ -304,18 +452,31 @@ const toAuthSettings = (row: AuthSettingsRow | null): AuthSettings | undefined =
304452 } ;
305453} ;
306454
307- const toApiStructure = ( row : ApiRow , opts : ApiOptions , rlsModule ?: RlsModule , authSettingsRow ?: AuthSettingsRow | null ) : ApiStructure => ( {
455+ interface ResolvedSettings {
456+ rlsModule ?: RlsModule ;
457+ authSettingsRow ?: AuthSettingsRow | null ;
458+ corsOrigins ?: string [ ] ;
459+ databaseSettings ?: DatabaseSettings ;
460+ pubkeyChallengeSettings ?: PubkeyChallengeSettings ;
461+ webauthnSettings ?: WebauthnSettings ;
462+ }
463+
464+ const toApiStructure = ( row : ApiRow , opts : ApiOptions , settings : ResolvedSettings = { } ) : ApiStructure => ( {
308465 apiId : row . api_id ,
309466 dbname : row . dbname || opts . pg ?. database || '' ,
310467 anonRole : row . anon_role || 'anon' ,
311468 roleName : row . role_name || 'authenticated' ,
312469 schema : row . schemas || [ ] ,
313470 apiModules : [ ] ,
314- rlsModule,
471+ rlsModule : settings . rlsModule ,
315472 domains : [ ] ,
316473 databaseId : row . database_id ,
317474 isPublic : row . is_public ,
318- authSettings : toAuthSettings ( authSettingsRow ?? null ) ,
475+ authSettings : toAuthSettings ( settings . authSettingsRow ?? null ) ,
476+ corsOrigins : settings . corsOrigins ,
477+ databaseSettings : settings . databaseSettings ,
478+ pubkeyChallengeSettings : settings . pubkeyChallengeSettings ,
479+ webauthnSettings : settings . webauthnSettings ,
319480} ) ;
320481
321482const createAdminStructure = (
@@ -390,6 +551,135 @@ const queryRlsModule = async (pool: Pool, databaseId: string, apiId: string): Pr
390551 return queryRlsModuleLegacy ( pool , apiId ) ;
391552} ;
392553
554+ // -- CORS --
555+
556+ const queryCorsSettings = async ( pool : Pool , databaseId : string , apiId ?: string ) : Promise < string [ ] | undefined > => {
557+ try {
558+ if ( apiId ) {
559+ const perApi = await pool . query < CorsSettingsRow > ( CORS_SETTINGS_SQL , [ databaseId , apiId ] ) ;
560+ if ( perApi . rows [ 0 ] ) return perApi . rows [ 0 ] . allowed_origins ;
561+ }
562+ const dbDefault = await pool . query < CorsSettingsRow > ( CORS_SETTINGS_DB_DEFAULT_SQL , [ databaseId ] ) ;
563+ return dbDefault . rows [ 0 ] ?. allowed_origins ;
564+ } catch {
565+ return undefined ;
566+ }
567+ } ;
568+
569+ const queryCorsModuleLegacy = async ( pool : Pool , apiId : string ) : Promise < string [ ] | undefined > => {
570+ const result = await pool . query < CorsModuleRow > ( CORS_MODULE_SQL , [ apiId ] ) ;
571+ return result . rows [ 0 ] ?. data ?. urls ;
572+ } ;
573+
574+ const queryCorsOrigins = async ( pool : Pool , databaseId : string , apiId ?: string ) : Promise < string [ ] | undefined > => {
575+ const fromSettings = await queryCorsSettings ( pool , databaseId , apiId ) ;
576+ if ( fromSettings ) return fromSettings ;
577+ if ( apiId ) return queryCorsModuleLegacy ( pool , apiId ) ;
578+ return undefined ;
579+ } ;
580+
581+ // -- Pubkey --
582+
583+ const toPubkeyChallengeSettings = ( row : PubkeySettingsRow | null ) : PubkeyChallengeSettings | undefined => {
584+ if ( ! row ?. schema || ! row ?. sign_up_with_key ) return undefined ;
585+ return {
586+ schema : row . schema ,
587+ cryptoNetwork : row . crypto_network ,
588+ signUpWithKey : row . sign_up_with_key ,
589+ signInRequestChallenge : row . sign_in_request_challenge ,
590+ signInRecordFailure : row . sign_in_record_failure ,
591+ signInWithChallenge : row . sign_in_with_challenge ,
592+ } ;
593+ } ;
594+
595+ const toPubkeyChallengeFromModule = ( row : PubkeyModuleRow | null ) : PubkeyChallengeSettings | undefined => {
596+ if ( ! row ?. data ?. schema ) return undefined ;
597+ const d = row . data ;
598+ return {
599+ schema : d . schema ,
600+ cryptoNetwork : d . crypto_network ,
601+ signUpWithKey : d . sign_up_with_key ,
602+ signInRequestChallenge : d . sign_in_request_challenge ,
603+ signInRecordFailure : d . sign_in_record_failure ,
604+ signInWithChallenge : d . sign_in_with_challenge ,
605+ } ;
606+ } ;
607+
608+ const queryPubkeySettings = async ( pool : Pool , databaseId : string ) : Promise < PubkeyChallengeSettings | undefined > => {
609+ try {
610+ const result = await pool . query < PubkeySettingsRow > ( PUBKEY_SETTINGS_SQL , [ databaseId ] ) ;
611+ return toPubkeyChallengeSettings ( result . rows [ 0 ] ?? null ) ;
612+ } catch {
613+ return undefined ;
614+ }
615+ } ;
616+
617+ const queryPubkeyModuleLegacy = async ( pool : Pool , apiId : string ) : Promise < PubkeyChallengeSettings | undefined > => {
618+ const result = await pool . query < PubkeyModuleRow > ( PUBKEY_MODULE_SQL , [ apiId ] ) ;
619+ return toPubkeyChallengeFromModule ( result . rows [ 0 ] ?? null ) ;
620+ } ;
621+
622+ const queryPubkeyChallenge = async ( pool : Pool , databaseId : string , apiId ?: string ) : Promise < PubkeyChallengeSettings | undefined > => {
623+ const fromSettings = await queryPubkeySettings ( pool , databaseId ) ;
624+ if ( fromSettings ) return fromSettings ;
625+ if ( apiId ) return queryPubkeyModuleLegacy ( pool , apiId ) ;
626+ return undefined ;
627+ } ;
628+
629+ // -- WebAuthn --
630+
631+ const toWebauthnSettings = ( row : WebauthnSettingsRow | null ) : WebauthnSettings | undefined => {
632+ if ( ! row ?. schema ) return undefined ;
633+ return {
634+ schema : row . schema ,
635+ credentialsSchema : row . credentials_schema ,
636+ sessionsSchema : row . sessions_schema ,
637+ sessionSecretsSchema : row . session_secrets_schema ,
638+ rpId : row . rp_id ,
639+ rpName : row . rp_name ,
640+ originAllowlist : row . origin_allowlist ,
641+ attestationType : row . attestation_type ,
642+ requireUserVerification : row . require_user_verification ,
643+ residentKey : row . resident_key ,
644+ challengeExpirySeconds : row . challenge_expiry_seconds ,
645+ } ;
646+ } ;
647+
648+ const queryWebauthnSettings = async ( pool : Pool , databaseId : string ) : Promise < WebauthnSettings | undefined > => {
649+ try {
650+ const result = await pool . query < WebauthnSettingsRow > ( WEBAUTHN_SETTINGS_SQL , [ databaseId ] ) ;
651+ return toWebauthnSettings ( result . rows [ 0 ] ?? null ) ;
652+ } catch {
653+ return undefined ;
654+ }
655+ } ;
656+
657+ // -- Database Settings (feature flags) --
658+
659+ const toDatabaseSettings = ( row : DatabaseSettingsRow | null ) : DatabaseSettings | undefined => {
660+ if ( ! row ) return undefined ;
661+ return {
662+ enableAggregates : row . resolved_enable_aggregates ,
663+ enablePostgis : row . resolved_enable_postgis ,
664+ enableSearch : row . resolved_enable_search ,
665+ enableDirectUploads : row . resolved_enable_direct_uploads ,
666+ enablePresignedUploads : row . resolved_enable_presigned_uploads ,
667+ enableManyToMany : row . resolved_enable_many_to_many ,
668+ enableConnectionFilter : row . resolved_enable_connection_filter ,
669+ enableLtree : row . resolved_enable_ltree ,
670+ enableLlm : row . resolved_enable_llm ,
671+ } ;
672+ } ;
673+
674+ const queryDatabaseSettings = async ( pool : Pool , databaseId : string , apiId ?: string ) : Promise < DatabaseSettings | undefined > => {
675+ try {
676+ const result = await pool . query < DatabaseSettingsRow > ( DATABASE_SETTINGS_SQL , [ databaseId , apiId ?? null ] ) ;
677+ return toDatabaseSettings ( result . rows [ 0 ] ?? null ) ;
678+ } catch {
679+ return undefined ;
680+ }
681+ } ;
682+
393683/**
394684 * Load server-relevant auth settings from the tenant DB.
395685 * Discovers the auth settings table dynamically by joining
@@ -479,10 +769,16 @@ const resolveApiNameHeader = async (ctx: ResolveContext): Promise<ApiStructure |
479769 return null ;
480770 }
481771
482- const rlsModule = await queryRlsModule ( pool , row . database_id , row . api_id ) ;
483- const authSettings = await queryAuthSettings ( opts , row . dbname ) ;
484- log . debug ( `[api-name-lookup] resolved schemas: [${ row . schemas ?. join ( ', ' ) } ], rlsModule: ${ rlsModule ? 'found' : 'none' } , authSettings: ${ authSettings ? 'found' : 'none' } ` ) ;
485- return toApiStructure ( row , opts , rlsModule , authSettings ) ;
772+ const [ rlsModule , authSettingsRow , corsOrigins , databaseSettings , pubkeyChallengeSettings , webauthnSettings ] = await Promise . all ( [
773+ queryRlsModule ( pool , row . database_id , row . api_id ) ,
774+ queryAuthSettings ( opts , row . dbname ) ,
775+ queryCorsOrigins ( pool , row . database_id , row . api_id ) ,
776+ queryDatabaseSettings ( pool , row . database_id , row . api_id ) ,
777+ queryPubkeyChallenge ( pool , row . database_id , row . api_id ) ,
778+ queryWebauthnSettings ( pool , row . database_id ) ,
779+ ] ) ;
780+ log . debug ( `[api-name-lookup] resolved schemas: [${ row . schemas ?. join ( ', ' ) } ], rlsModule: ${ rlsModule ? 'found' : 'none' } , authSettings: ${ authSettingsRow ? 'found' : 'none' } ` ) ;
781+ return toApiStructure ( row , opts , { rlsModule, authSettingsRow, corsOrigins, databaseSettings, pubkeyChallengeSettings, webauthnSettings } ) ;
486782} ;
487783
488784const resolveMetaSchemaHeader = (
@@ -505,10 +801,16 @@ const resolveDomainLookup = async (ctx: ResolveContext): Promise<ApiStructure |
505801 return null ;
506802 }
507803
508- const rlsModule = await queryRlsModule ( pool , row . database_id , row . api_id ) ;
509- const authSettings = await queryAuthSettings ( opts , row . dbname ) ;
510- log . debug ( `[domain-lookup] resolved schemas: [${ row . schemas ?. join ( ', ' ) } ], rlsModule: ${ rlsModule ? 'found' : 'none' } , authSettings: ${ authSettings ? 'found' : 'none' } ` ) ;
511- return toApiStructure ( row , opts , rlsModule , authSettings ) ;
804+ const [ rlsModule , authSettingsRow , corsOrigins , databaseSettings , pubkeyChallengeSettings , webauthnSettings ] = await Promise . all ( [
805+ queryRlsModule ( pool , row . database_id , row . api_id ) ,
806+ queryAuthSettings ( opts , row . dbname ) ,
807+ queryCorsOrigins ( pool , row . database_id , row . api_id ) ,
808+ queryDatabaseSettings ( pool , row . database_id , row . api_id ) ,
809+ queryPubkeyChallenge ( pool , row . database_id , row . api_id ) ,
810+ queryWebauthnSettings ( pool , row . database_id ) ,
811+ ] ) ;
812+ log . debug ( `[domain-lookup] resolved schemas: [${ row . schemas ?. join ( ', ' ) } ], rlsModule: ${ rlsModule ? 'found' : 'none' } , authSettings: ${ authSettingsRow ? 'found' : 'none' } ` ) ;
813+ return toApiStructure ( row , opts , { rlsModule, authSettingsRow, corsOrigins, databaseSettings, pubkeyChallengeSettings, webauthnSettings } ) ;
512814} ;
513815
514816const buildDevFallbackError = async (
0 commit comments