@constructive-io/graphql-server 4.33.0 → 4.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/middleware/api.js CHANGED
@@ -8,14 +8,18 @@ const env_1 = require("@pgpmjs/env");
8
8
  const logger_1 = require("@pgpmjs/logger");
9
9
  const server_utils_1 = require("@pgpmjs/server-utils");
10
10
  const url_domains_1 = require("@constructive-io/url-domains");
11
+ const express_context_1 = require("@constructive-io/express-context");
11
12
  const pg_cache_1 = require("pg-cache");
12
13
  const _50x_1 = __importDefault(require("../errors/50x"));
13
14
  const _404_message_1 = __importDefault(require("../errors/404-message"));
14
15
  require("./types");
15
16
  const log = new logger_1.Logger('api');
16
- const isDev = () => (0, env_1.getNodeEnv)() === 'development';
17
17
  // =============================================================================
18
- // SQL Queries
18
+ // Module Loader Registry (replaces inline SQL queries for per-db config)
19
+ // =============================================================================
20
+ const defaultRegistry = (0, express_context_1.createDefaultRegistry)();
21
+ // =============================================================================
22
+ // SQL Queries (API resolution only — module queries now live in loaders)
19
23
  // =============================================================================
20
24
  const DOMAIN_LOOKUP_SQL = `
21
25
  SELECT
@@ -75,153 +79,39 @@ const API_LIST_SQL = `
75
79
  GROUP BY a.id, a.database_id, a.name, a.dbname, a.role_name, a.anon_role, a.is_public
76
80
  LIMIT 100
77
81
  `;
78
- const RLS_MODULE_SQL = `
79
- SELECT data
80
- FROM services_public.api_modules
81
- WHERE api_id = $1 AND name = 'rls_module'
82
- LIMIT 1
83
- `;
84
- const RLS_SETTINGS_SQL = `
85
- SELECT
86
- auth_schema.schema_name AS authenticate_schema,
87
- role_schema.schema_name AS role_schema,
88
- auth_fn.name AS authenticate,
89
- auth_strict_fn.name AS authenticate_strict,
90
- role_fn.name AS current_role,
91
- role_id_fn.name AS current_role_id,
92
- ua_fn.name AS current_user_agent,
93
- ip_fn.name AS current_ip_address
94
- FROM services_public.rls_settings rs
95
- LEFT JOIN metaschema_public.schema auth_schema ON rs.authenticate_schema_id = auth_schema.id
96
- LEFT JOIN metaschema_public.schema role_schema ON rs.role_schema_id = role_schema.id
97
- LEFT JOIN metaschema_public.function auth_fn ON rs.authenticate_function_id = auth_fn.id
98
- LEFT JOIN metaschema_public.function auth_strict_fn ON rs.authenticate_strict_function_id = auth_strict_fn.id
99
- LEFT JOIN metaschema_public.function role_fn ON rs.current_role_function_id = role_fn.id
100
- LEFT JOIN metaschema_public.function role_id_fn ON rs.current_role_id_function_id = role_id_fn.id
101
- LEFT JOIN metaschema_public.function ua_fn ON rs.current_user_agent_function_id = ua_fn.id
102
- LEFT JOIN metaschema_public.function ip_fn ON rs.current_ip_address_function_id = ip_fn.id
103
- WHERE rs.database_id = $1
104
- LIMIT 1
105
- `;
106
82
  /**
107
- * Discover auth settings table location via public metaschema tables.
108
- * Joins sessions_module with metaschema_public.schema to resolve
109
- * the schema name + table name without touching private schemas.
83
+ * Build a LoaderContext from the API row and options.
84
+ * This is used to resolve per-database module settings via the loader registry.
110
85
  */
111
- const AUTH_SETTINGS_DISCOVERY_SQL = `
112
- SELECT s.schema_name, sm.auth_settings_table AS table_name
113
- FROM metaschema_modules_public.sessions_module sm
114
- JOIN metaschema_public.schema s ON s.id = sm.schema_id
115
- LIMIT 1
116
- `;
86
+ const buildLoaderContext = (servicesPool, opts, row) => ({
87
+ servicesPool,
88
+ tenantPool: (0, pg_cache_1.getPgPool)({ ...opts.pg, database: row.dbname }),
89
+ databaseId: row.database_id,
90
+ apiId: row.api_id,
91
+ dbname: row.dbname,
92
+ });
117
93
  /**
118
- * Query auth settings from the discovered table.
119
- * Schema and table name are resolved dynamically from metaschema modules.
94
+ * Resolve all per-database module settings in parallel via the loader registry.
95
+ * Each loader independently caches by databaseId repeated calls are cheap.
120
96
  */
121
- const AUTH_SETTINGS_SQL = (schemaName, tableName) => `
122
- SELECT
123
- cookie_secure,
124
- cookie_samesite,
125
- cookie_domain,
126
- cookie_httponly,
127
- cookie_max_age,
128
- cookie_path,
129
- remember_me_duration,
130
- enable_captcha,
131
- captcha_site_key
132
- FROM "${schemaName}"."${tableName}"
133
- LIMIT 1
134
- `;
135
- const CORS_SETTINGS_SQL = `
136
- SELECT allowed_origins
137
- FROM services_public.cors_settings
138
- WHERE database_id = $1 AND api_id = $2
139
- LIMIT 1
140
- `;
141
- const CORS_SETTINGS_DB_DEFAULT_SQL = `
142
- SELECT allowed_origins
143
- FROM services_public.cors_settings
144
- WHERE database_id = $1 AND api_id IS NULL
145
- LIMIT 1
146
- `;
147
- const CORS_MODULE_SQL = `
148
- SELECT data
149
- FROM services_public.api_modules
150
- WHERE api_id = $1 AND name = 'cors'
151
- LIMIT 1
152
- `;
153
- const PUBKEY_SETTINGS_SQL = `
154
- SELECT
155
- s.schema_name AS schema,
156
- ps.crypto_network,
157
- sign_up_fn.name AS sign_up_with_key,
158
- sign_in_req_fn.name AS sign_in_request_challenge,
159
- sign_in_fail_fn.name AS sign_in_record_failure,
160
- sign_in_fn.name AS sign_in_with_challenge
161
- FROM services_public.pubkey_settings ps
162
- LEFT JOIN metaschema_public.schema s ON ps.schema_id = s.id
163
- LEFT JOIN metaschema_public.function sign_up_fn ON ps.sign_up_with_key_function_id = sign_up_fn.id
164
- LEFT JOIN metaschema_public.function sign_in_req_fn ON ps.sign_in_request_challenge_function_id = sign_in_req_fn.id
165
- LEFT JOIN metaschema_public.function sign_in_fail_fn ON ps.sign_in_record_failure_function_id = sign_in_fail_fn.id
166
- LEFT JOIN metaschema_public.function sign_in_fn ON ps.sign_in_with_challenge_function_id = sign_in_fn.id
167
- WHERE ps.database_id = $1
168
- LIMIT 1
169
- `;
170
- const PUBKEY_MODULE_SQL = `
171
- SELECT data
172
- FROM services_public.api_modules
173
- WHERE api_id = $1 AND name = 'pubkey_challenge'
174
- LIMIT 1
175
- `;
176
- const WEBAUTHN_SETTINGS_SQL = `
177
- SELECT
178
- s.schema_name AS schema,
179
- cred_s.schema_name AS credentials_schema,
180
- sess_s.schema_name AS sessions_schema,
181
- sec_s.schema_name AS session_secrets_schema,
182
- ws.rp_id,
183
- ws.rp_name,
184
- ws.origin_allowlist,
185
- ws.attestation_type,
186
- ws.require_user_verification,
187
- ws.resident_key,
188
- ws.challenge_expiry_seconds
189
- FROM services_public.webauthn_settings ws
190
- LEFT JOIN metaschema_public.schema s ON ws.schema_id = s.id
191
- LEFT JOIN metaschema_public.schema cred_s ON ws.credentials_schema_id = cred_s.id
192
- LEFT JOIN metaschema_public.schema sess_s ON ws.sessions_schema_id = sess_s.id
193
- LEFT JOIN metaschema_public.schema sec_s ON ws.session_secrets_schema_id = sec_s.id
194
- WHERE ws.database_id = $1
195
- LIMIT 1
196
- `;
197
- const DATABASE_SETTINGS_SQL = `
198
- SELECT
199
- ds.enable_aggregates,
200
- ds.enable_postgis,
201
- ds.enable_search,
202
- ds.enable_direct_uploads,
203
- ds.enable_presigned_uploads,
204
- ds.enable_many_to_many,
205
- ds.enable_connection_filter,
206
- ds.enable_ltree,
207
- ds.enable_llm,
208
- ds.enable_bulk,
209
- COALESCE(aps.enable_aggregates, ds.enable_aggregates) AS resolved_enable_aggregates,
210
- COALESCE(aps.enable_postgis, ds.enable_postgis) AS resolved_enable_postgis,
211
- COALESCE(aps.enable_search, ds.enable_search) AS resolved_enable_search,
212
- COALESCE(aps.enable_direct_uploads, ds.enable_direct_uploads) AS resolved_enable_direct_uploads,
213
- COALESCE(aps.enable_presigned_uploads, ds.enable_presigned_uploads) AS resolved_enable_presigned_uploads,
214
- COALESCE(aps.enable_many_to_many, ds.enable_many_to_many) AS resolved_enable_many_to_many,
215
- COALESCE(aps.enable_connection_filter, ds.enable_connection_filter) AS resolved_enable_connection_filter,
216
- COALESCE(aps.enable_ltree, ds.enable_ltree) AS resolved_enable_ltree,
217
- COALESCE(aps.enable_llm, ds.enable_llm) AS resolved_enable_llm,
218
- COALESCE(aps.enable_realtime, ds.enable_realtime) AS resolved_enable_realtime,
219
- COALESCE(aps.enable_bulk, ds.enable_bulk) AS resolved_enable_bulk
220
- FROM services_public.database_settings ds
221
- LEFT JOIN services_public.api_settings aps ON ds.database_id = aps.database_id AND aps.api_id = $2
222
- WHERE ds.database_id = $1
223
- LIMIT 1
224
- `;
97
+ const resolveModuleSettings = async (registry, ctx) => {
98
+ const [rlsModule, authSettings, corsOrigins, databaseSettings, pubkeyChallengeSettings, webauthnSettings,] = await Promise.all([
99
+ registry.resolve('rlsModule', ctx),
100
+ registry.resolve('authSettings', ctx),
101
+ registry.resolve('corsOrigins', ctx),
102
+ registry.resolve('databaseSettings', ctx),
103
+ registry.resolve('pubkeyChallengeSettings', ctx),
104
+ registry.resolve('webauthnSettings', ctx),
105
+ ]);
106
+ return {
107
+ rlsModule,
108
+ authSettings,
109
+ corsOrigins,
110
+ databaseSettings,
111
+ pubkeyChallengeSettings,
112
+ webauthnSettings,
113
+ };
114
+ };
225
115
  // =============================================================================
226
116
  // Helpers
227
117
  // =============================================================================
@@ -257,63 +147,6 @@ const getSvcKey = (opts, req) => {
257
147
  return baseKey;
258
148
  };
259
149
  exports.getSvcKey = getSvcKey;
260
- const toRlsModule = (row) => {
261
- if (!row?.data)
262
- return undefined;
263
- const d = row.data;
264
- return {
265
- authenticate: d.authenticate,
266
- authenticateStrict: d.authenticate_strict,
267
- privateSchema: {
268
- schemaName: d.authenticate_schema,
269
- },
270
- publicSchema: {
271
- schemaName: d.role_schema,
272
- },
273
- currentRole: d.current_role,
274
- currentRoleId: d.current_role_id,
275
- currentIpAddress: d.current_ip_address,
276
- currentUserAgent: d.current_user_agent,
277
- };
278
- };
279
- const toRlsModuleFromSettings = (row) => {
280
- if (!row)
281
- return undefined;
282
- // If metaschema_public.function rows are missing (e.g. trigger was skipped
283
- // during migration), the LEFT JOINs resolve NULL. Return undefined so the
284
- // caller falls back to the legacy api_modules lookup.
285
- if (!row.authenticate || !row.authenticate_schema)
286
- return undefined;
287
- return {
288
- authenticate: row.authenticate,
289
- authenticateStrict: row.authenticate_strict,
290
- privateSchema: {
291
- schemaName: row.authenticate_schema,
292
- },
293
- publicSchema: {
294
- schemaName: row.role_schema,
295
- },
296
- currentRole: row.current_role,
297
- currentRoleId: row.current_role_id,
298
- currentIpAddress: row.current_ip_address,
299
- currentUserAgent: row.current_user_agent,
300
- };
301
- };
302
- const toAuthSettings = (row) => {
303
- if (!row)
304
- return undefined;
305
- return {
306
- cookieSecure: row.cookie_secure,
307
- cookieSamesite: row.cookie_samesite,
308
- cookieDomain: row.cookie_domain,
309
- cookieHttponly: row.cookie_httponly,
310
- cookieMaxAge: row.cookie_max_age,
311
- cookiePath: row.cookie_path,
312
- rememberMeDuration: row.remember_me_duration,
313
- enableCaptcha: row.enable_captcha,
314
- captchaSiteKey: row.captcha_site_key,
315
- };
316
- };
317
150
  const toApiStructure = (row, opts, settings = {}) => ({
318
151
  apiId: row.api_id,
319
152
  dbname: row.dbname || opts.pg?.database || '',
@@ -325,7 +158,7 @@ const toApiStructure = (row, opts, settings = {}) => ({
325
158
  domains: [],
326
159
  databaseId: row.database_id,
327
160
  isPublic: row.is_public,
328
- authSettings: toAuthSettings(settings.authSettingsRow ?? null),
161
+ authSettings: settings.authSettings,
329
162
  corsOrigins: settings.corsOrigins,
330
163
  databaseSettings: settings.databaseSettings,
331
164
  pubkeyChallengeSettings: settings.pubkeyChallengeSettings,
@@ -342,7 +175,7 @@ const createAdminStructure = (opts, schemas, databaseId) => ({
342
175
  isPublic: false,
343
176
  });
344
177
  // =============================================================================
345
- // Database Queries
178
+ // Database Queries (API resolution only)
346
179
  // =============================================================================
347
180
  const validateSchemata = async (pool, schemas) => {
348
181
  const result = await pool.query(`SELECT schema_name FROM information_schema.schemata WHERE schema_name = ANY($1::text[])`, [schemas]);
@@ -360,184 +193,6 @@ const queryApiList = async (pool, isPublic) => {
360
193
  const result = await pool.query(API_LIST_SQL, [isPublic]);
361
194
  return result.rows;
362
195
  };
363
- const queryRlsSettings = async (pool, databaseId) => {
364
- try {
365
- const result = await pool.query(RLS_SETTINGS_SQL, [databaseId]);
366
- return toRlsModuleFromSettings(result.rows[0] ?? null);
367
- }
368
- catch (e) {
369
- log.warn(`[rls-settings] Failed to load RLS settings: ${e.message}`);
370
- return undefined;
371
- }
372
- };
373
- const queryRlsModuleLegacy = async (pool, apiId) => {
374
- const result = await pool.query(RLS_MODULE_SQL, [apiId]);
375
- return toRlsModule(result.rows[0] ?? null);
376
- };
377
- const queryRlsModule = async (pool, databaseId, apiId) => {
378
- const fromSettings = await queryRlsSettings(pool, databaseId);
379
- if (fromSettings)
380
- return fromSettings;
381
- return queryRlsModuleLegacy(pool, apiId);
382
- };
383
- // -- CORS --
384
- const queryCorsSettings = async (pool, databaseId, apiId) => {
385
- try {
386
- if (apiId) {
387
- const perApi = await pool.query(CORS_SETTINGS_SQL, [databaseId, apiId]);
388
- if (perApi.rows[0])
389
- return perApi.rows[0].allowed_origins;
390
- }
391
- const dbDefault = await pool.query(CORS_SETTINGS_DB_DEFAULT_SQL, [databaseId]);
392
- return dbDefault.rows[0]?.allowed_origins;
393
- }
394
- catch (e) {
395
- log.warn(`[cors-settings] Failed to load CORS settings: ${e.message}`);
396
- return undefined;
397
- }
398
- };
399
- const queryCorsModuleLegacy = async (pool, apiId) => {
400
- const result = await pool.query(CORS_MODULE_SQL, [apiId]);
401
- return result.rows[0]?.data?.urls;
402
- };
403
- const queryCorsOrigins = async (pool, databaseId, apiId) => {
404
- const fromSettings = await queryCorsSettings(pool, databaseId, apiId);
405
- if (fromSettings)
406
- return fromSettings;
407
- if (apiId)
408
- return queryCorsModuleLegacy(pool, apiId);
409
- return undefined;
410
- };
411
- // -- Pubkey --
412
- const toPubkeyChallengeSettings = (row) => {
413
- if (!row?.schema || !row?.sign_up_with_key)
414
- return undefined;
415
- return {
416
- schema: row.schema,
417
- cryptoNetwork: row.crypto_network,
418
- signUpWithKey: row.sign_up_with_key,
419
- signInRequestChallenge: row.sign_in_request_challenge,
420
- signInRecordFailure: row.sign_in_record_failure,
421
- signInWithChallenge: row.sign_in_with_challenge,
422
- };
423
- };
424
- const toPubkeyChallengeFromModule = (row) => {
425
- if (!row?.data?.schema)
426
- return undefined;
427
- const d = row.data;
428
- return {
429
- schema: d.schema,
430
- cryptoNetwork: d.crypto_network,
431
- signUpWithKey: d.sign_up_with_key,
432
- signInRequestChallenge: d.sign_in_request_challenge,
433
- signInRecordFailure: d.sign_in_record_failure,
434
- signInWithChallenge: d.sign_in_with_challenge,
435
- };
436
- };
437
- const queryPubkeySettings = async (pool, databaseId) => {
438
- try {
439
- const result = await pool.query(PUBKEY_SETTINGS_SQL, [databaseId]);
440
- return toPubkeyChallengeSettings(result.rows[0] ?? null);
441
- }
442
- catch (e) {
443
- log.warn(`[pubkey-settings] Failed to load pubkey challenge settings: ${e.message}`);
444
- return undefined;
445
- }
446
- };
447
- const queryPubkeyModuleLegacy = async (pool, apiId) => {
448
- const result = await pool.query(PUBKEY_MODULE_SQL, [apiId]);
449
- return toPubkeyChallengeFromModule(result.rows[0] ?? null);
450
- };
451
- const queryPubkeyChallenge = async (pool, databaseId, apiId) => {
452
- const fromSettings = await queryPubkeySettings(pool, databaseId);
453
- if (fromSettings)
454
- return fromSettings;
455
- if (apiId)
456
- return queryPubkeyModuleLegacy(pool, apiId);
457
- return undefined;
458
- };
459
- // -- WebAuthn --
460
- const toWebauthnSettings = (row) => {
461
- if (!row?.schema)
462
- return undefined;
463
- return {
464
- schema: row.schema,
465
- credentialsSchema: row.credentials_schema,
466
- sessionsSchema: row.sessions_schema,
467
- sessionSecretsSchema: row.session_secrets_schema,
468
- rpId: row.rp_id,
469
- rpName: row.rp_name,
470
- originAllowlist: row.origin_allowlist,
471
- attestationType: row.attestation_type,
472
- requireUserVerification: row.require_user_verification,
473
- residentKey: row.resident_key,
474
- challengeExpirySeconds: row.challenge_expiry_seconds,
475
- };
476
- };
477
- const queryWebauthnSettings = async (pool, databaseId) => {
478
- try {
479
- const result = await pool.query(WEBAUTHN_SETTINGS_SQL, [databaseId]);
480
- return toWebauthnSettings(result.rows[0] ?? null);
481
- }
482
- catch (e) {
483
- log.warn(`[webauthn-settings] Failed to load webauthn settings: ${e.message}`);
484
- return undefined;
485
- }
486
- };
487
- // -- Database Settings (feature flags) --
488
- const toDatabaseSettings = (row) => {
489
- if (!row)
490
- return undefined;
491
- return {
492
- enableAggregates: row.resolved_enable_aggregates,
493
- enablePostgis: row.resolved_enable_postgis,
494
- enableSearch: row.resolved_enable_search,
495
- enableDirectUploads: row.resolved_enable_direct_uploads,
496
- enablePresignedUploads: row.resolved_enable_presigned_uploads,
497
- enableManyToMany: row.resolved_enable_many_to_many,
498
- enableConnectionFilter: row.resolved_enable_connection_filter,
499
- enableLtree: row.resolved_enable_ltree,
500
- enableLlm: row.resolved_enable_llm,
501
- enableRealtime: row.resolved_enable_realtime,
502
- enableBulk: row.resolved_enable_bulk,
503
- };
504
- };
505
- const queryDatabaseSettings = async (pool, databaseId, apiId) => {
506
- try {
507
- const result = await pool.query(DATABASE_SETTINGS_SQL, [databaseId, apiId ?? null]);
508
- return toDatabaseSettings(result.rows[0] ?? null);
509
- }
510
- catch (e) {
511
- log.warn(`[database-settings] Failed to load database settings: ${e.message}`);
512
- return undefined;
513
- }
514
- };
515
- /**
516
- * Load server-relevant auth settings from the tenant DB.
517
- * Discovers the auth settings table dynamically by joining
518
- * metaschema_modules_public.sessions_module with metaschema_public.schema
519
- * (both public schemas). Fails gracefully if modules or table don't exist yet.
520
- */
521
- const queryAuthSettings = async (opts, dbname) => {
522
- try {
523
- const tenantPool = (0, pg_cache_1.getPgPool)({ ...opts.pg, database: dbname });
524
- // Discover the auth settings schema + table name from public metaschema tables
525
- const discovery = await tenantPool.query(AUTH_SETTINGS_DISCOVERY_SQL);
526
- const resolved = discovery.rows[0];
527
- if (!resolved) {
528
- log.debug('[auth-settings] No sessions_module row found in tenant DB');
529
- return null;
530
- }
531
- // Query the discovered auth settings table
532
- const result = await tenantPool.query(AUTH_SETTINGS_SQL(resolved.schema_name, resolved.table_name));
533
- return result.rows[0] ?? null;
534
- }
535
- catch (e) {
536
- // Table/module may not exist yet if the 2FA migration hasn't been applied
537
- log.debug(`[auth-settings] Failed to load auth settings: ${e.message}`);
538
- return null;
539
- }
540
- };
541
196
  // =============================================================================
542
197
  // Resolution Logic
543
198
  // =============================================================================
@@ -588,16 +243,10 @@ const resolveApiNameHeader = async (ctx) => {
588
243
  log.debug(`[api-name-lookup] No API found for databaseId=${headers.databaseId} name=${headers.apiName}`);
589
244
  return null;
590
245
  }
591
- const [rlsModule, authSettingsRow, corsOrigins, databaseSettings, pubkeyChallengeSettings, webauthnSettings] = await Promise.all([
592
- queryRlsModule(pool, row.database_id, row.api_id),
593
- queryAuthSettings(opts, row.dbname),
594
- queryCorsOrigins(pool, row.database_id, row.api_id),
595
- queryDatabaseSettings(pool, row.database_id, row.api_id),
596
- queryPubkeyChallenge(pool, row.database_id, row.api_id),
597
- queryWebauthnSettings(pool, row.database_id),
598
- ]);
599
- log.debug(`[api-name-lookup] resolved schemas: [${row.schemas?.join(', ')}], rlsModule: ${rlsModule ? 'found' : 'none'}, authSettings: ${authSettingsRow ? 'found' : 'none'}`);
600
- return toApiStructure(row, opts, { rlsModule, authSettingsRow, corsOrigins, databaseSettings, pubkeyChallengeSettings, webauthnSettings });
246
+ const loaderCtx = buildLoaderContext(pool, opts, row);
247
+ const settings = await resolveModuleSettings(defaultRegistry, loaderCtx);
248
+ log.debug(`[api-name-lookup] resolved schemas: [${row.schemas?.join(', ')}], rlsModule: ${settings.rlsModule ? 'found' : 'none'}, authSettings: ${settings.authSettings ? 'found' : 'none'}`);
249
+ return toApiStructure(row, opts, settings);
601
250
  };
602
251
  const resolveMetaSchemaHeader = (ctx, validatedSchemas) => {
603
252
  return createAdminStructure(ctx.opts, validatedSchemas, ctx.headers.databaseId);
@@ -611,16 +260,10 @@ const resolveDomainLookup = async (ctx) => {
611
260
  log.debug(`[domain-lookup] No API found for domain=${domain} subdomain=${subdomain}`);
612
261
  return null;
613
262
  }
614
- const [rlsModule, authSettingsRow, corsOrigins, databaseSettings, pubkeyChallengeSettings, webauthnSettings] = await Promise.all([
615
- queryRlsModule(pool, row.database_id, row.api_id),
616
- queryAuthSettings(opts, row.dbname),
617
- queryCorsOrigins(pool, row.database_id, row.api_id),
618
- queryDatabaseSettings(pool, row.database_id, row.api_id),
619
- queryPubkeyChallenge(pool, row.database_id, row.api_id),
620
- queryWebauthnSettings(pool, row.database_id),
621
- ]);
622
- log.debug(`[domain-lookup] resolved schemas: [${row.schemas?.join(', ')}], rlsModule: ${rlsModule ? 'found' : 'none'}, authSettings: ${authSettingsRow ? 'found' : 'none'}`);
623
- return toApiStructure(row, opts, { rlsModule, authSettingsRow, corsOrigins, databaseSettings, pubkeyChallengeSettings, webauthnSettings });
263
+ const loaderCtx = buildLoaderContext(pool, opts, row);
264
+ const settings = await resolveModuleSettings(defaultRegistry, loaderCtx);
265
+ log.debug(`[domain-lookup] resolved schemas: [${row.schemas?.join(', ')}], rlsModule: ${settings.rlsModule ? 'found' : 'none'}, authSettings: ${settings.authSettings ? 'found' : 'none'}`);
266
+ return toApiStructure(row, opts, settings);
624
267
  };
625
268
  const buildDevFallbackError = async (ctx, req) => {
626
269
  if ((0, env_1.getNodeEnv)() !== 'development')
@@ -19,6 +19,9 @@
19
19
  *
20
20
  * Auth: JWT from the auth middleware (req.token) → pg SET LOCAL context for RLS
21
21
  * Metering: check_billing_quota → LLM call → record_usage with real token counts
22
+ *
23
+ * Context: Uses `req.constructive` from @constructive-io/express-context
24
+ * for tenant-scoped database access, pgSettings, and withPgClient.
22
25
  */
23
26
  import { Router } from 'express';
24
27
  export declare function createLlmApiRouter(): Router;