@@ -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 , RlsModule } from '../types' ;
11+ import { ApiConfigResult , ApiError , ApiOptions , ApiStructure , AuthSettings , RlsModule } from '../types' ;
1212import './types' ;
1313
1414const log = new Logger ( 'api' ) ;
@@ -86,6 +86,36 @@ const RLS_MODULE_SQL = `
8686 LIMIT 1
8787` ;
8888
89+ /**
90+ * Discover auth settings table location via public metaschema tables.
91+ * Joins sessions_module with metaschema_public.schema to resolve
92+ * the schema name + table name without touching private schemas.
93+ */
94+ const AUTH_SETTINGS_DISCOVERY_SQL = `
95+ SELECT s.schema_name, sm.auth_settings_table AS table_name
96+ FROM metaschema_modules_public.sessions_module sm
97+ JOIN metaschema_public.schema s ON s.id = sm.schema_id
98+ LIMIT 1
99+ ` ;
100+
101+ /**
102+ * Query auth settings from the discovered table.
103+ * Schema and table name are resolved dynamically from metaschema modules.
104+ */
105+ const AUTH_SETTINGS_SQL = ( schemaName : string , tableName : string ) => `
106+ SELECT
107+ cookie_secure,
108+ cookie_samesite,
109+ cookie_domain,
110+ cookie_httponly,
111+ cookie_max_age,
112+ cookie_path,
113+ enable_captcha,
114+ captcha_site_key
115+ FROM "${ schemaName } "."${ tableName } "
116+ LIMIT 1
117+ ` ;
118+
89119// =============================================================================
90120// Types
91121// =============================================================================
@@ -111,6 +141,17 @@ interface RlsModuleData {
111141 current_user_agent : string ;
112142}
113143
144+ interface AuthSettingsRow {
145+ cookie_secure : boolean ;
146+ cookie_samesite : string ;
147+ cookie_domain : string | null ;
148+ cookie_httponly : boolean ;
149+ cookie_max_age : string | null ;
150+ cookie_path : string ;
151+ enable_captcha : boolean ;
152+ captcha_site_key : string | null ;
153+ }
154+
114155interface RlsModuleRow {
115156 data : RlsModuleData | null ;
116157}
@@ -208,7 +249,21 @@ const toRlsModule = (row: RlsModuleRow | null): RlsModule | undefined => {
208249 } ;
209250} ;
210251
211- const toApiStructure = ( row : ApiRow , opts : ApiOptions , rlsModuleRow ?: RlsModuleRow | null ) : ApiStructure => ( {
252+ const toAuthSettings = ( row : AuthSettingsRow | null ) : AuthSettings | undefined => {
253+ if ( ! row ) return undefined ;
254+ return {
255+ cookieSecure : row . cookie_secure ,
256+ cookieSamesite : row . cookie_samesite ,
257+ cookieDomain : row . cookie_domain ,
258+ cookieHttponly : row . cookie_httponly ,
259+ cookieMaxAge : row . cookie_max_age ,
260+ cookiePath : row . cookie_path ,
261+ enableCaptcha : row . enable_captcha ,
262+ captchaSiteKey : row . captcha_site_key ,
263+ } ;
264+ } ;
265+
266+ const toApiStructure = ( row : ApiRow , opts : ApiOptions , rlsModuleRow ?: RlsModuleRow | null , authSettingsRow ?: AuthSettingsRow | null ) : ApiStructure => ( {
212267 apiId : row . api_id ,
213268 dbname : row . dbname || opts . pg ?. database || '' ,
214269 anonRole : row . anon_role || 'anon' ,
@@ -219,6 +274,7 @@ const toApiStructure = (row: ApiRow, opts: ApiOptions, rlsModuleRow?: RlsModuleR
219274 domains : [ ] ,
220275 databaseId : row . database_id ,
221276 isPublic : row . is_public ,
277+ authSettings : toAuthSettings ( authSettingsRow ?? null ) ,
222278} ) ;
223279
224280const createAdminStructure = (
@@ -278,6 +334,37 @@ const queryRlsModule = async (pool: Pool, apiId: string): Promise<RlsModuleRow |
278334 return result . rows [ 0 ] ?? null ;
279335} ;
280336
337+ /**
338+ * Load server-relevant auth settings from the tenant DB.
339+ * Discovers the auth settings table dynamically by joining
340+ * metaschema_modules_public.sessions_module with metaschema_public.schema
341+ * (both public schemas). Fails gracefully if modules or table don't exist yet.
342+ */
343+ const queryAuthSettings = async (
344+ opts : ApiOptions ,
345+ dbname : string
346+ ) : Promise < AuthSettingsRow | null > => {
347+ try {
348+ const tenantPool = getPgPool ( { ...opts . pg , database : dbname } ) ;
349+
350+ // Discover the auth settings schema + table name from public metaschema tables
351+ const discovery = await tenantPool . query < { schema_name : string ; table_name : string } > ( AUTH_SETTINGS_DISCOVERY_SQL ) ;
352+ const resolved = discovery . rows [ 0 ] ;
353+ if ( ! resolved ) {
354+ log . debug ( '[auth-settings] No sessions_module row found in tenant DB' ) ;
355+ return null ;
356+ }
357+
358+ // Query the discovered auth settings table
359+ const result = await tenantPool . query < AuthSettingsRow > ( AUTH_SETTINGS_SQL ( resolved . schema_name , resolved . table_name ) ) ;
360+ return result . rows [ 0 ] ?? null ;
361+ } catch ( e : any ) {
362+ // Table/module may not exist yet if the 2FA migration hasn't been applied
363+ log . debug ( `[auth-settings] Failed to load auth settings: ${ e . message } ` ) ;
364+ return null ;
365+ }
366+ } ;
367+
281368// =============================================================================
282369// Resolution Logic
283370// =============================================================================
@@ -337,8 +424,9 @@ const resolveApiNameHeader = async (ctx: ResolveContext): Promise<ApiStructure |
337424 }
338425
339426 const rlsModule = await queryRlsModule ( pool , row . api_id ) ;
340- log . debug ( `[api-name-lookup] resolved schemas: [${ row . schemas ?. join ( ', ' ) } ], rlsModule: ${ rlsModule ? 'found' : 'none' } ` ) ;
341- return toApiStructure ( row , opts , rlsModule ) ;
427+ const authSettings = await queryAuthSettings ( opts , row . dbname ) ;
428+ log . debug ( `[api-name-lookup] resolved schemas: [${ row . schemas ?. join ( ', ' ) } ], rlsModule: ${ rlsModule ? 'found' : 'none' } , authSettings: ${ authSettings ? 'found' : 'none' } ` ) ;
429+ return toApiStructure ( row , opts , rlsModule , authSettings ) ;
342430} ;
343431
344432const resolveMetaSchemaHeader = (
@@ -362,8 +450,9 @@ const resolveDomainLookup = async (ctx: ResolveContext): Promise<ApiStructure |
362450 }
363451
364452 const rlsModule = await queryRlsModule ( pool , row . api_id ) ;
365- log . debug ( `[domain-lookup] resolved schemas: [${ row . schemas ?. join ( ', ' ) } ], rlsModule: ${ rlsModule ? 'found' : 'none' } ` ) ;
366- return toApiStructure ( row , opts , rlsModule ) ;
453+ const authSettings = await queryAuthSettings ( opts , row . dbname ) ;
454+ log . debug ( `[domain-lookup] resolved schemas: [${ row . schemas ?. join ( ', ' ) } ], rlsModule: ${ rlsModule ? 'found' : 'none' } , authSettings: ${ authSettings ? 'found' : 'none' } ` ) ;
455+ return toApiStructure ( row , opts , rlsModule , authSettings ) ;
367456} ;
368457
369458const buildDevFallbackError = async (
0 commit comments