Skip to content

Commit 28884c2

Browse files
committed
fix: discover auth settings via metaschema modules, remove CORS migration
- Replace hardcoded app_settings_auth table name with dynamic discovery via metaschema_modules_public.sessions_module -> metaschema.schema_and_table() - Remove allowed_origins from AuthSettings (CORS migration deferred) - Revert cors.ts to legacy api_modules-only approach
1 parent a23123f commit 28884c2

3 files changed

Lines changed: 52 additions & 30 deletions

File tree

graphql/server/src/middleware/api.ts

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,27 @@ const RLS_MODULE_SQL = `
8787
`;
8888

8989
/**
90-
* Query auth settings from the tenant DB private schema.
91-
* The table name follows the pattern: {privateSchema}.app_settings_auth
92-
* We select only the server-relevant columns.
90+
* Discover auth settings table location via metaschema modules.
91+
* Step 1: Get auth_settings_table_id from sessions_module
92+
* Step 2: Resolve the actual schema + table name via metaschema.schema_and_table()
9393
*/
94-
const AUTH_SETTINGS_SQL = (privateSchema: string) => `
94+
const AUTH_SETTINGS_TABLE_ID_SQL = `
95+
SELECT auth_settings_table_id
96+
FROM metaschema_modules_public.sessions_module
97+
LIMIT 1
98+
`;
99+
100+
const AUTH_SETTINGS_SCHEMA_AND_TABLE_SQL = `
101+
SELECT schema_name, table_name
102+
FROM metaschema.schema_and_table($1)
103+
`;
104+
105+
/**
106+
* Query auth settings from the discovered table.
107+
* Schema and table name are resolved dynamically from metaschema modules.
108+
*/
109+
const AUTH_SETTINGS_SQL = (schemaName: string, tableName: string) => `
95110
SELECT
96-
allowed_origins,
97111
cookie_secure,
98112
cookie_samesite,
99113
cookie_domain,
@@ -102,7 +116,7 @@ const AUTH_SETTINGS_SQL = (privateSchema: string) => `
102116
cookie_path,
103117
enable_captcha,
104118
captcha_site_key
105-
FROM "${privateSchema}".app_settings_auth
119+
FROM "${schemaName}"."${tableName}"
106120
LIMIT 1
107121
`;
108122

@@ -132,7 +146,6 @@ interface RlsModuleData {
132146
}
133147

134148
interface AuthSettingsRow {
135-
allowed_origins: string[] | null;
136149
cookie_secure: boolean;
137150
cookie_samesite: string;
138151
cookie_domain: string | null;
@@ -243,7 +256,6 @@ const toRlsModule = (row: RlsModuleRow | null): RlsModule | undefined => {
243256
const toAuthSettings = (row: AuthSettingsRow | null): AuthSettings | undefined => {
244257
if (!row) return undefined;
245258
return {
246-
allowedOrigins: row.allowed_origins,
247259
cookieSecure: row.cookie_secure,
248260
cookieSamesite: row.cookie_samesite,
249261
cookieDomain: row.cookie_domain,
@@ -327,23 +339,41 @@ const queryRlsModule = async (pool: Pool, apiId: string): Promise<RlsModuleRow |
327339
};
328340

329341
/**
330-
* Load server-relevant auth settings from the tenant DB private schema.
331-
* Fails gracefully if the table doesn't exist yet (pre-migration).
342+
* Load server-relevant auth settings from the tenant DB.
343+
* Discovers the auth settings table dynamically by querying
344+
* metaschema_modules_public.sessions_module for the table ID,
345+
* then resolving the actual schema + table name via metaschema.schema_and_table().
346+
* Fails gracefully if modules or table don't exist yet (pre-migration).
332347
*/
333348
const queryAuthSettings = async (
334349
opts: ApiOptions,
335-
dbname: string,
336-
rlsModuleRow: RlsModuleRow | null
350+
dbname: string
337351
): Promise<AuthSettingsRow | null> => {
338-
if (!rlsModuleRow?.data?.authenticate_schema) return null;
339-
const privateSchema = rlsModuleRow.data.authenticate_schema;
340352
try {
341353
const tenantPool = getPgPool({ ...opts.pg, database: dbname });
342-
const result = await tenantPool.query<AuthSettingsRow>(AUTH_SETTINGS_SQL(privateSchema));
354+
355+
// Step 1: Get auth_settings_table_id from sessions_module
356+
const modResult = await tenantPool.query<{ auth_settings_table_id: string }>(AUTH_SETTINGS_TABLE_ID_SQL);
357+
const tableId = modResult.rows[0]?.auth_settings_table_id;
358+
if (!tableId) {
359+
log.debug('[auth-settings] No sessions_module row found in tenant DB');
360+
return null;
361+
}
362+
363+
// Step 2: Resolve actual schema + table name
364+
const stResult = await tenantPool.query<{ schema_name: string; table_name: string }>(AUTH_SETTINGS_SCHEMA_AND_TABLE_SQL, [tableId]);
365+
const resolved = stResult.rows[0];
366+
if (!resolved) {
367+
log.debug(`[auth-settings] Could not resolve schema_and_table for table_id=${tableId}`);
368+
return null;
369+
}
370+
371+
// Step 3: Query the actual auth settings table
372+
const result = await tenantPool.query<AuthSettingsRow>(AUTH_SETTINGS_SQL(resolved.schema_name, resolved.table_name));
343373
return result.rows[0] ?? null;
344374
} catch (e: any) {
345-
// Table may not exist yet if the 2FA migration hasn't been applied
346-
log.debug(`[auth-settings] Failed to load auth settings from ${privateSchema}.app_settings_auth: ${e.message}`);
375+
// Table/module may not exist yet if the 2FA migration hasn't been applied
376+
log.debug(`[auth-settings] Failed to load auth settings: ${e.message}`);
347377
return null;
348378
}
349379
};
@@ -407,7 +437,7 @@ const resolveApiNameHeader = async (ctx: ResolveContext): Promise<ApiStructure |
407437
}
408438

409439
const rlsModule = await queryRlsModule(pool, row.api_id);
410-
const authSettings = await queryAuthSettings(opts, row.dbname, rlsModule);
440+
const authSettings = await queryAuthSettings(opts, row.dbname);
411441
log.debug(`[api-name-lookup] resolved schemas: [${row.schemas?.join(', ')}], rlsModule: ${rlsModule ? 'found' : 'none'}, authSettings: ${authSettings ? 'found' : 'none'}`);
412442
return toApiStructure(row, opts, rlsModule, authSettings);
413443
};
@@ -433,7 +463,7 @@ const resolveDomainLookup = async (ctx: ResolveContext): Promise<ApiStructure |
433463
}
434464

435465
const rlsModule = await queryRlsModule(pool, row.api_id);
436-
const authSettings = await queryAuthSettings(opts, row.dbname, rlsModule);
466+
const authSettings = await queryAuthSettings(opts, row.dbname);
437467
log.debug(`[domain-lookup] resolved schemas: [${row.schemas?.join(', ')}], rlsModule: ${rlsModule ? 'found' : 'none'}, authSettings: ${authSettings ? 'found' : 'none'}`);
438468
return toApiStructure(row, opts, rlsModule, authSettings);
439469
};

graphql/server/src/middleware/cors.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,11 @@ export const cors = (fallbackOrigin?: string): RequestHandler => {
3333

3434
// 2) Per-API allowlist sourced from req.api (if available)
3535
// createApiMiddleware runs before this in server.ts, so req.api should be set
36-
const api = (req as any).api as { apiModules?: any[]; domains?: string[]; authSettings?: { allowedOrigins?: string[] | null } } | undefined;
36+
const api = (req as any).api as { apiModules?: any[]; domains?: string[] } | undefined;
3737
if (api) {
38-
// Preferred: app_auth_settings.allowed_origins (new approach)
39-
const settingsOrigins = api.authSettings?.allowedOrigins || [];
40-
// Legacy: api_modules CORS entries (backward compat)
4138
const corsModules = (api.apiModules || []).filter((m: any) => m.name === 'cors') as { name: 'cors'; data: CorsModuleData }[];
4239
const siteUrls = api.domains || [];
43-
const listOfDomains = [
44-
...settingsOrigins,
45-
...corsModules.reduce<string[]>((m, mod) => [...mod.data.urls, ...m], []),
46-
...siteUrls,
47-
];
40+
const listOfDomains = corsModules.reduce<string[]>((m, mod) => [...mod.data.urls, ...m], siteUrls);
4841

4942
if (origin && listOfDomains.includes(origin)) {
5043
return callback(null, true);

graphql/server/src/types.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,10 @@ export interface RlsModule {
4040

4141
/**
4242
* Server-visible subset of app_auth_settings (lives in the tenant DB private schema).
43+
* Discovered dynamically via metaschema_modules_public.sessions_module.
4344
* Loaded once per API resolution and cached alongside the ApiStructure.
4445
*/
4546
export interface AuthSettings {
46-
/** CORS allowed origins from app_auth_settings.allowed_origins */
47-
allowedOrigins?: string[] | null;
4847
/** Cookie configuration */
4948
cookieSecure?: boolean;
5049
cookieSamesite?: string;

0 commit comments

Comments
 (0)