@constructive-io/express-context 0.14.2 → 0.18.0

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/README.md CHANGED
@@ -63,11 +63,11 @@ Each loader encapsulates a SQL query + type transform + per-databaseId LRU cache
63
63
 
64
64
  | Loader | Source | Description |
65
65
  |--------|--------|-------------|
66
- | `rlsLoader` | `constructive_routing_public.rls_settings` | RLS module (authenticate functions, schema refs) |
67
- | `corsLoader` | `constructive_routing_public.cors_settings` | CORS allowed origins |
68
- | `databaseSettingsLoader` | `constructive_routing_public.database_settings` | Feature flags (aggregates, search, uploads, etc.) |
69
- | `pubkeyLoader` | `constructive_routing_public.pubkey_settings` | Public key challenge auth settings |
70
- | `webauthnLoader` | `constructive_routing_public.webauthn_settings` | WebAuthn/passkey configuration |
66
+ | `rlsLoader` | `routing_public.rls_settings` | RLS module (authenticate functions, schema refs) |
67
+ | `corsLoader` | `routing_public.cors_settings` | CORS allowed origins |
68
+ | `databaseSettingsLoader` | `routing_public.database_settings` | Feature flags (aggregates, search, uploads, etc.) |
69
+ | `pubkeyLoader` | `routing_public.pubkey_settings` | Public key challenge auth settings |
70
+ | `webauthnLoader` | `routing_public.webauthn_settings` | WebAuthn/passkey configuration |
71
71
  | `authSettingsLoader` | `metaschema_modules_public.sessions_module` | Cookie/captcha settings (two-step tenant DB discovery) |
72
72
 
73
73
  ### Custom loaders
package/context.d.ts CHANGED
@@ -22,6 +22,8 @@ export interface ContextMiddlewareOptions {
22
22
  pg?: PgpmOptions['pg'];
23
23
  /** Module loader registry for per-database cached lookups */
24
24
  loaders?: LoaderRegistry;
25
+ /** Routing-plane schema loaders query (defaults to routing_public) */
26
+ routingSchema?: string;
25
27
  }
26
28
  /**
27
29
  * Build the ConstructiveContext from the current request state.
package/context.js CHANGED
@@ -67,6 +67,7 @@ function buildContext(req, opts = {}) {
67
67
  const routingPool = (0, pg_cache_1.getPgPool)(opts.pg);
68
68
  loaderCtx = {
69
69
  routingPool,
70
+ routingSchema: opts.routingSchema,
70
71
  tenantPool,
71
72
  databaseId: api.databaseId,
72
73
  apiId: api.apiId,
package/esm/context.js CHANGED
@@ -63,6 +63,7 @@ export function buildContext(req, opts = {}) {
63
63
  const routingPool = getPgPool(opts.pg);
64
64
  loaderCtx = {
65
65
  routingPool,
66
+ routingSchema: opts.routingSchema,
66
67
  tenantPool,
67
68
  databaseId: api.databaseId,
68
69
  apiId: api.apiId,
@@ -2,54 +2,39 @@
2
2
  * CORS Origins Loader
3
3
  *
4
4
  * Resolves allowed CORS origins for a database+API combination from the
5
- * scoped routing plane. Checks per-API settings first, falls back to
6
- * database-level default, then to the api_modules approach.
5
+ * scoped routing plane. Checks per-API settings first, falls back to the
6
+ * database-level default.
7
7
  */
8
8
  import { createModuleLoader } from './create-loader';
9
+ import { routingSchemaOf } from './types';
9
10
  // ─── SQL ────────────────────────────────────────────────────────────────────
10
- const CORS_SETTINGS_SQL = `
11
+ const corsSettingsSql = (schema) => `
11
12
  SELECT allowed_origins
12
- FROM constructive_routing_public.cors_settings
13
+ FROM "${schema}".cors_settings
13
14
  WHERE database_id = $1 AND api_id = $2
14
15
  LIMIT 1
15
16
  `;
16
- const CORS_SETTINGS_DB_DEFAULT_SQL = `
17
+ const corsSettingsDbDefaultSql = (schema) => `
17
18
  SELECT allowed_origins
18
- FROM constructive_routing_public.cors_settings
19
+ FROM "${schema}".cors_settings
19
20
  WHERE database_id = $1 AND api_id IS NULL
20
21
  LIMIT 1
21
22
  `;
22
- const CORS_MODULE_SQL = `
23
- SELECT data
24
- FROM constructive_routing_public.api_modules
25
- WHERE api_id = $1 AND name = 'cors'
26
- LIMIT 1
27
- `;
28
23
  // ─── Loader ─────────────────────────────────────────────────────────────────
29
24
  export const corsLoader = createModuleLoader({
30
25
  name: 'corsOrigins',
31
26
  ttlMs: 5 * 60_000,
32
27
  async resolve(ctx) {
33
28
  const { routingPool, databaseId, apiId } = ctx;
34
- // Try per-API cors_settings first
35
- try {
36
- if (apiId) {
37
- const perApi = await routingPool.query(CORS_SETTINGS_SQL, [databaseId, apiId]);
38
- if (perApi.rows[0])
39
- return perApi.rows[0].allowed_origins;
40
- }
41
- const dbDefault = await routingPool.query(CORS_SETTINGS_DB_DEFAULT_SQL, [databaseId]);
42
- if (dbDefault.rows[0])
43
- return dbDefault.rows[0].allowed_origins;
44
- }
45
- catch {
46
- // Table may not exist yet
47
- }
48
- // Fall back to api_modules
29
+ const schema = routingSchemaOf(ctx);
49
30
  if (apiId) {
50
- const result = await routingPool.query(CORS_MODULE_SQL, [apiId]);
51
- return result.rows[0]?.data?.urls;
31
+ const perApi = await routingPool.query(corsSettingsSql(schema), [databaseId, apiId]);
32
+ if (perApi.rows[0])
33
+ return perApi.rows[0].allowed_origins;
52
34
  }
35
+ const dbDefault = await routingPool.query(corsSettingsDbDefaultSql(schema), [databaseId]);
36
+ if (dbDefault.rows[0])
37
+ return dbDefault.rows[0].allowed_origins;
53
38
  return undefined;
54
39
  }
55
40
  });
@@ -6,8 +6,9 @@
6
6
  * Merges database-level defaults with optional per-API overrides.
7
7
  */
8
8
  import { createModuleLoader } from './create-loader';
9
+ import { routingSchemaOf } from './types';
9
10
  // ─── SQL ────────────────────────────────────────────────────────────────────
10
- const DATABASE_SETTINGS_SQL = `
11
+ const databaseSettingsSql = (schema) => `
11
12
  SELECT
12
13
  COALESCE(aps.enable_aggregates, ds.enable_aggregates) AS resolved_enable_aggregates,
13
14
  COALESCE(aps.enable_postgis, ds.enable_postgis) AS resolved_enable_postgis,
@@ -21,8 +22,8 @@ const DATABASE_SETTINGS_SQL = `
21
22
  COALESCE(aps.enable_realtime, ds.enable_realtime) AS resolved_enable_realtime,
22
23
  COALESCE(aps.enable_bulk, ds.enable_bulk) AS resolved_enable_bulk,
23
24
  COALESCE(aps.enable_i18n, ds.enable_i18n) AS resolved_enable_i18n
24
- FROM constructive_routing_public.database_settings ds
25
- LEFT JOIN constructive_routing_public.api_settings aps ON ds.database_id = aps.database_id AND aps.api_id = $2
25
+ FROM "${schema}".database_settings ds
26
+ LEFT JOIN "${schema}".api_settings aps ON ds.database_id = aps.database_id AND aps.api_id = $2
26
27
  WHERE ds.database_id = $1
27
28
  LIMIT 1
28
29
  `;
@@ -32,7 +33,7 @@ export const databaseSettingsLoader = createModuleLoader({
32
33
  ttlMs: 5 * 60_000,
33
34
  async resolve(ctx) {
34
35
  const { routingPool, databaseId, apiId } = ctx;
35
- const result = await routingPool.query(DATABASE_SETTINGS_SQL, [databaseId, apiId ?? null]);
36
+ const result = await routingPool.query(databaseSettingsSql(routingSchemaOf(ctx)), [databaseId, apiId ?? null]);
36
37
  const row = result.rows[0];
37
38
  if (!row)
38
39
  return undefined;
@@ -6,11 +6,11 @@
6
6
  * a LoaderRegistry and pass it to createContextMiddleware().
7
7
  *
8
8
  * Built-in loaders cover the standard Constructive modules:
9
- * - rlsModule (constructive_routing_public.rls_settings)
10
- * - corsOrigins (constructive_routing_public.cors_settings)
11
- * - databaseSettings(constructive_routing_public.database_settings)
12
- * - pubkeyChallengeSettings (constructive_routing_public.pubkey_settings)
13
- * - webauthnSettings(constructive_routing_public.webauthn_settings)
9
+ * - rlsModule (routing-plane rls_settings)
10
+ * - corsOrigins (routing-plane cors_settings)
11
+ * - databaseSettings(routing-plane database_settings)
12
+ * - pubkeyChallengeSettings (routing-plane pubkey_settings)
13
+ * - webauthnSettings(routing-plane webauthn_settings)
14
14
  * - authSettings (metaschema_modules_public.sessions_module → tenant DB)
15
15
  *
16
16
  * To add a new per-db lookup, implement a ModuleLoader and register it:
@@ -2,12 +2,13 @@
2
2
  * Pubkey Challenge Settings Loader
3
3
  *
4
4
  * Resolves public-key challenge auth config (crypto network, sign-in/sign-up
5
- * function names) from the scoped routing plane. Tries the pubkey_settings
6
- * table first, falls back to the api_modules approach.
5
+ * function names) from the scoped routing plane via the typed pubkey_settings
6
+ * table.
7
7
  */
8
8
  import { createModuleLoader } from './create-loader';
9
+ import { routingSchemaOf } from './types';
9
10
  // ─── SQL ────────────────────────────────────────────────────────────────────
10
- const PUBKEY_SETTINGS_SQL = `
11
+ const pubkeySettingsSql = (schema) => `
11
12
  SELECT
12
13
  s.schema_name AS schema,
13
14
  ps.crypto_network,
@@ -15,7 +16,7 @@ const PUBKEY_SETTINGS_SQL = `
15
16
  sign_in_req_fn.name AS sign_in_request_challenge,
16
17
  sign_in_fail_fn.name AS sign_in_record_failure,
17
18
  sign_in_fn.name AS sign_in_with_challenge
18
- FROM constructive_routing_public.pubkey_settings ps
19
+ FROM "${schema}".pubkey_settings ps
19
20
  LEFT JOIN metaschema_public.schema s ON ps.schema_id = s.id
20
21
  LEFT JOIN metaschema_public.function sign_up_fn ON ps.sign_up_with_key_function_id = sign_up_fn.id
21
22
  LEFT JOIN metaschema_public.function sign_in_req_fn ON ps.sign_in_request_challenge_function_id = sign_in_req_fn.id
@@ -24,12 +25,6 @@ const PUBKEY_SETTINGS_SQL = `
24
25
  WHERE ps.database_id = $1
25
26
  LIMIT 1
26
27
  `;
27
- const PUBKEY_MODULE_SQL = `
28
- SELECT data
29
- FROM constructive_routing_public.api_modules
30
- WHERE api_id = $1 AND name = 'pubkey_challenge'
31
- LIMIT 1
32
- `;
33
28
  // ─── Transforms ─────────────────────────────────────────────────────────────
34
29
  function fromRow(row) {
35
30
  if (!row?.schema || !row?.sign_up_with_key)
@@ -48,22 +43,8 @@ export const pubkeyLoader = createModuleLoader({
48
43
  name: 'pubkeyChallengeSettings',
49
44
  ttlMs: 5 * 60_000,
50
45
  async resolve(ctx) {
51
- const { routingPool, databaseId, apiId } = ctx;
52
- // Try new pubkey_settings table first
53
- try {
54
- const result = await routingPool.query(PUBKEY_SETTINGS_SQL, [databaseId]);
55
- const resolved = fromRow(result.rows[0] ?? null);
56
- if (resolved)
57
- return resolved;
58
- }
59
- catch {
60
- // Table may not exist yet
61
- }
62
- // Fall back to api_modules
63
- if (apiId) {
64
- const result = await routingPool.query(PUBKEY_MODULE_SQL, [apiId]);
65
- return fromRow(result.rows[0]?.data ?? null);
66
- }
67
- return undefined;
46
+ const { routingPool, databaseId } = ctx;
47
+ const result = await routingPool.query(pubkeySettingsSql(routingSchemaOf(ctx)), [databaseId]);
48
+ return fromRow(result.rows[0] ?? null);
68
49
  }
69
50
  });
@@ -2,12 +2,13 @@
2
2
  * RLS Module Loader
3
3
  *
4
4
  * Resolves RLS authentication function names and schema references for
5
- * a given database from the scoped routing plane. Tries the rls_settings
6
- * table first, falls back to the api_modules approach.
5
+ * a given database from the scoped routing plane via the typed rls_settings
6
+ * table.
7
7
  */
8
8
  import { createModuleLoader } from './create-loader';
9
+ import { routingSchemaOf } from './types';
9
10
  // ─── SQL ────────────────────────────────────────────────────────────────────
10
- const RLS_SETTINGS_SQL = `
11
+ const rlsSettingsSql = (schema) => `
11
12
  SELECT
12
13
  auth_schema.schema_name AS authenticate_schema,
13
14
  role_schema.schema_name AS role_schema,
@@ -17,7 +18,7 @@ const RLS_SETTINGS_SQL = `
17
18
  role_id_fn.name AS current_role_id,
18
19
  ua_fn.name AS current_user_agent,
19
20
  ip_fn.name AS current_ip_address
20
- FROM constructive_routing_public.rls_settings rs
21
+ FROM "${schema}".rls_settings rs
21
22
  LEFT JOIN metaschema_public.schema auth_schema ON rs.authenticate_schema_id = auth_schema.id
22
23
  LEFT JOIN metaschema_public.schema role_schema ON rs.role_schema_id = role_schema.id
23
24
  LEFT JOIN metaschema_public.function auth_fn ON rs.authenticate_function_id = auth_fn.id
@@ -29,12 +30,6 @@ const RLS_SETTINGS_SQL = `
29
30
  WHERE rs.database_id = $1
30
31
  LIMIT 1
31
32
  `;
32
- const RLS_MODULE_SQL = `
33
- SELECT data
34
- FROM constructive_routing_public.api_modules
35
- WHERE api_id = $1 AND name = 'rls_module'
36
- LIMIT 1
37
- `;
38
33
  // ─── Transforms ─────────────────────────────────────────────────────────────
39
34
  function fromSettings(row) {
40
35
  if (!row)
@@ -52,42 +47,13 @@ function fromSettings(row) {
52
47
  currentUserAgent: row.current_user_agent
53
48
  };
54
49
  }
55
- function fromModule(row) {
56
- if (!row?.data)
57
- return undefined;
58
- const d = row.data;
59
- return {
60
- authenticate: d.authenticate,
61
- authenticateStrict: d.authenticate_strict,
62
- privateSchema: { schemaName: d.authenticate_schema },
63
- publicSchema: { schemaName: d.role_schema },
64
- currentRole: d.current_role,
65
- currentRoleId: d.current_role_id,
66
- currentIpAddress: d.current_ip_address,
67
- currentUserAgent: d.current_user_agent
68
- };
69
- }
70
50
  // ─── Loader ─────────────────────────────────────────────────────────────────
71
51
  export const rlsLoader = createModuleLoader({
72
52
  name: 'rlsModule',
73
53
  ttlMs: 5 * 60_000,
74
54
  async resolve(ctx) {
75
- const { routingPool, databaseId, apiId } = ctx;
76
- // Try new rls_settings table first
77
- try {
78
- const result = await routingPool.query(RLS_SETTINGS_SQL, [databaseId]);
79
- const resolved = fromSettings(result.rows[0] ?? null);
80
- if (resolved)
81
- return resolved;
82
- }
83
- catch {
84
- // Table may not exist yet
85
- }
86
- // Fall back to api_modules
87
- if (apiId) {
88
- const result = await routingPool.query(RLS_MODULE_SQL, [apiId]);
89
- return fromModule(result.rows[0] ?? null);
90
- }
91
- return undefined;
55
+ const { routingPool, databaseId } = ctx;
56
+ const result = await routingPool.query(rlsSettingsSql(routingSchemaOf(ctx)), [databaseId]);
57
+ return fromSettings(result.rows[0] ?? null);
92
58
  }
93
59
  });
@@ -9,4 +9,17 @@
9
9
  * during context building. The result is a typed modules map on
10
10
  * `req.constructive.modules`.
11
11
  */
12
- export {};
12
+ /** Published logical name of the scoped routing plane. */
13
+ export const DEFAULT_ROUTING_SCHEMA = 'routing_public';
14
+ /**
15
+ * The routing-plane schema a loader should query: the context override or
16
+ * the published default. Throws on names unsafe to interpolate as an
17
+ * identifier.
18
+ */
19
+ export const routingSchemaOf = (ctx) => {
20
+ const schema = ctx.routingSchema ?? DEFAULT_ROUTING_SCHEMA;
21
+ if (!/^[a-z_][a-z0-9_]*$/.test(schema)) {
22
+ throw new Error(`invalid routing schema name: ${schema}`);
23
+ }
24
+ return schema;
25
+ };
@@ -5,8 +5,9 @@
5
5
  * schema references, attestation policy, and challenge expiry.
6
6
  */
7
7
  import { createModuleLoader } from './create-loader';
8
+ import { routingSchemaOf } from './types';
8
9
  // ─── SQL ────────────────────────────────────────────────────────────────────
9
- const WEBAUTHN_SETTINGS_SQL = `
10
+ const webauthnSettingsSql = (schema) => `
10
11
  SELECT
11
12
  s.schema_name AS schema,
12
13
  cred_s.schema_name AS credentials_schema,
@@ -19,7 +20,7 @@ const WEBAUTHN_SETTINGS_SQL = `
19
20
  ws.require_user_verification,
20
21
  ws.resident_key,
21
22
  ws.challenge_expiry_seconds
22
- FROM constructive_routing_public.webauthn_settings ws
23
+ FROM "${schema}".webauthn_settings ws
23
24
  LEFT JOIN metaschema_public.schema s ON ws.schema_id = s.id
24
25
  LEFT JOIN metaschema_public.schema cred_s ON ws.credentials_schema_id = cred_s.id
25
26
  LEFT JOIN metaschema_public.schema sess_s ON ws.sessions_schema_id = sess_s.id
@@ -33,7 +34,7 @@ export const webauthnLoader = createModuleLoader({
33
34
  ttlMs: 5 * 60_000,
34
35
  async resolve(ctx) {
35
36
  const { routingPool, databaseId } = ctx;
36
- const result = await routingPool.query(WEBAUTHN_SETTINGS_SQL, [databaseId]);
37
+ const result = await routingPool.query(webauthnSettingsSql(routingSchemaOf(ctx)), [databaseId]);
37
38
  const row = result.rows[0];
38
39
  if (!row?.schema)
39
40
  return undefined;
package/index.d.ts CHANGED
@@ -34,7 +34,7 @@
34
34
  * });
35
35
  * ```
36
36
  */
37
- export type { AgentChatConfig, ApiConfigResult, ApiError, ApiModule, ApiStructure, AuthSettings, BillingConfig, BuiltinModuleMap, ComputeConfig, ComputeModuleConfig, ConstructiveAPIToken, ConstructiveContext, CorsModuleData, DatabaseSettings, GenericModuleData, InferenceLogConfig, LlmConfig, PublicKeyChallengeData, PubkeyChallengeSettings, RlsModule, WebauthnSettings, WithPgClient, } from './types';
37
+ export type { AgentChatConfig, ApiConfigResult, ApiError, ApiStructure, AuthSettings, BillingConfig, BuiltinModuleMap, ComputeConfig, ComputeModuleConfig, ConstructiveAPIToken, ConstructiveContext, DatabaseSettings, InferenceLogConfig, LlmConfig, PubkeyChallengeSettings, RlsModule, WebauthnSettings, WithPgClient, } from './types';
38
38
  export type { BillingClient, InferenceLogEntry } from './billing-client';
39
39
  export { createBillingClient } from './billing-client';
40
40
  export type { PgSettingsInput } from './pg-settings';
package/loaders/cors.d.ts CHANGED
@@ -2,8 +2,8 @@
2
2
  * CORS Origins Loader
3
3
  *
4
4
  * Resolves allowed CORS origins for a database+API combination from the
5
- * scoped routing plane. Checks per-API settings first, falls back to
6
- * database-level default, then to the api_modules approach.
5
+ * scoped routing plane. Checks per-API settings first, falls back to the
6
+ * database-level default.
7
7
  */
8
8
  import type { ModuleLoader } from './types';
9
9
  export declare const corsLoader: ModuleLoader<string[]>;
package/loaders/cors.js CHANGED
@@ -3,56 +3,41 @@
3
3
  * CORS Origins Loader
4
4
  *
5
5
  * Resolves allowed CORS origins for a database+API combination from the
6
- * scoped routing plane. Checks per-API settings first, falls back to
7
- * database-level default, then to the api_modules approach.
6
+ * scoped routing plane. Checks per-API settings first, falls back to the
7
+ * database-level default.
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.corsLoader = void 0;
11
11
  const create_loader_1 = require("./create-loader");
12
+ const types_1 = require("./types");
12
13
  // ─── SQL ────────────────────────────────────────────────────────────────────
13
- const CORS_SETTINGS_SQL = `
14
+ const corsSettingsSql = (schema) => `
14
15
  SELECT allowed_origins
15
- FROM constructive_routing_public.cors_settings
16
+ FROM "${schema}".cors_settings
16
17
  WHERE database_id = $1 AND api_id = $2
17
18
  LIMIT 1
18
19
  `;
19
- const CORS_SETTINGS_DB_DEFAULT_SQL = `
20
+ const corsSettingsDbDefaultSql = (schema) => `
20
21
  SELECT allowed_origins
21
- FROM constructive_routing_public.cors_settings
22
+ FROM "${schema}".cors_settings
22
23
  WHERE database_id = $1 AND api_id IS NULL
23
24
  LIMIT 1
24
25
  `;
25
- const CORS_MODULE_SQL = `
26
- SELECT data
27
- FROM constructive_routing_public.api_modules
28
- WHERE api_id = $1 AND name = 'cors'
29
- LIMIT 1
30
- `;
31
26
  // ─── Loader ─────────────────────────────────────────────────────────────────
32
27
  exports.corsLoader = (0, create_loader_1.createModuleLoader)({
33
28
  name: 'corsOrigins',
34
29
  ttlMs: 5 * 60_000,
35
30
  async resolve(ctx) {
36
31
  const { routingPool, databaseId, apiId } = ctx;
37
- // Try per-API cors_settings first
38
- try {
39
- if (apiId) {
40
- const perApi = await routingPool.query(CORS_SETTINGS_SQL, [databaseId, apiId]);
41
- if (perApi.rows[0])
42
- return perApi.rows[0].allowed_origins;
43
- }
44
- const dbDefault = await routingPool.query(CORS_SETTINGS_DB_DEFAULT_SQL, [databaseId]);
45
- if (dbDefault.rows[0])
46
- return dbDefault.rows[0].allowed_origins;
47
- }
48
- catch {
49
- // Table may not exist yet
50
- }
51
- // Fall back to api_modules
32
+ const schema = (0, types_1.routingSchemaOf)(ctx);
52
33
  if (apiId) {
53
- const result = await routingPool.query(CORS_MODULE_SQL, [apiId]);
54
- return result.rows[0]?.data?.urls;
34
+ const perApi = await routingPool.query(corsSettingsSql(schema), [databaseId, apiId]);
35
+ if (perApi.rows[0])
36
+ return perApi.rows[0].allowed_origins;
55
37
  }
38
+ const dbDefault = await routingPool.query(corsSettingsDbDefaultSql(schema), [databaseId]);
39
+ if (dbDefault.rows[0])
40
+ return dbDefault.rows[0].allowed_origins;
56
41
  return undefined;
57
42
  }
58
43
  });
@@ -9,8 +9,9 @@
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.databaseSettingsLoader = void 0;
11
11
  const create_loader_1 = require("./create-loader");
12
+ const types_1 = require("./types");
12
13
  // ─── SQL ────────────────────────────────────────────────────────────────────
13
- const DATABASE_SETTINGS_SQL = `
14
+ const databaseSettingsSql = (schema) => `
14
15
  SELECT
15
16
  COALESCE(aps.enable_aggregates, ds.enable_aggregates) AS resolved_enable_aggregates,
16
17
  COALESCE(aps.enable_postgis, ds.enable_postgis) AS resolved_enable_postgis,
@@ -24,8 +25,8 @@ const DATABASE_SETTINGS_SQL = `
24
25
  COALESCE(aps.enable_realtime, ds.enable_realtime) AS resolved_enable_realtime,
25
26
  COALESCE(aps.enable_bulk, ds.enable_bulk) AS resolved_enable_bulk,
26
27
  COALESCE(aps.enable_i18n, ds.enable_i18n) AS resolved_enable_i18n
27
- FROM constructive_routing_public.database_settings ds
28
- LEFT JOIN constructive_routing_public.api_settings aps ON ds.database_id = aps.database_id AND aps.api_id = $2
28
+ FROM "${schema}".database_settings ds
29
+ LEFT JOIN "${schema}".api_settings aps ON ds.database_id = aps.database_id AND aps.api_id = $2
29
30
  WHERE ds.database_id = $1
30
31
  LIMIT 1
31
32
  `;
@@ -35,7 +36,7 @@ exports.databaseSettingsLoader = (0, create_loader_1.createModuleLoader)({
35
36
  ttlMs: 5 * 60_000,
36
37
  async resolve(ctx) {
37
38
  const { routingPool, databaseId, apiId } = ctx;
38
- const result = await routingPool.query(DATABASE_SETTINGS_SQL, [databaseId, apiId ?? null]);
39
+ const result = await routingPool.query(databaseSettingsSql((0, types_1.routingSchemaOf)(ctx)), [databaseId, apiId ?? null]);
39
40
  const row = result.rows[0];
40
41
  if (!row)
41
42
  return undefined;
@@ -6,11 +6,11 @@
6
6
  * a LoaderRegistry and pass it to createContextMiddleware().
7
7
  *
8
8
  * Built-in loaders cover the standard Constructive modules:
9
- * - rlsModule (constructive_routing_public.rls_settings)
10
- * - corsOrigins (constructive_routing_public.cors_settings)
11
- * - databaseSettings(constructive_routing_public.database_settings)
12
- * - pubkeyChallengeSettings (constructive_routing_public.pubkey_settings)
13
- * - webauthnSettings(constructive_routing_public.webauthn_settings)
9
+ * - rlsModule (routing-plane rls_settings)
10
+ * - corsOrigins (routing-plane cors_settings)
11
+ * - databaseSettings(routing-plane database_settings)
12
+ * - pubkeyChallengeSettings (routing-plane pubkey_settings)
13
+ * - webauthnSettings(routing-plane webauthn_settings)
14
14
  * - authSettings (metaschema_modules_public.sessions_module → tenant DB)
15
15
  *
16
16
  * To add a new per-db lookup, implement a ModuleLoader and register it:
package/loaders/index.js CHANGED
@@ -7,11 +7,11 @@
7
7
  * a LoaderRegistry and pass it to createContextMiddleware().
8
8
  *
9
9
  * Built-in loaders cover the standard Constructive modules:
10
- * - rlsModule (constructive_routing_public.rls_settings)
11
- * - corsOrigins (constructive_routing_public.cors_settings)
12
- * - databaseSettings(constructive_routing_public.database_settings)
13
- * - pubkeyChallengeSettings (constructive_routing_public.pubkey_settings)
14
- * - webauthnSettings(constructive_routing_public.webauthn_settings)
10
+ * - rlsModule (routing-plane rls_settings)
11
+ * - corsOrigins (routing-plane cors_settings)
12
+ * - databaseSettings(routing-plane database_settings)
13
+ * - pubkeyChallengeSettings (routing-plane pubkey_settings)
14
+ * - webauthnSettings(routing-plane webauthn_settings)
15
15
  * - authSettings (metaschema_modules_public.sessions_module → tenant DB)
16
16
  *
17
17
  * To add a new per-db lookup, implement a ModuleLoader and register it:
@@ -2,8 +2,8 @@
2
2
  * Pubkey Challenge Settings Loader
3
3
  *
4
4
  * Resolves public-key challenge auth config (crypto network, sign-in/sign-up
5
- * function names) from the scoped routing plane. Tries the pubkey_settings
6
- * table first, falls back to the api_modules approach.
5
+ * function names) from the scoped routing plane via the typed pubkey_settings
6
+ * table.
7
7
  */
8
8
  import type { PubkeyChallengeSettings } from '../types';
9
9
  import type { ModuleLoader } from './types';
package/loaders/pubkey.js CHANGED
@@ -3,14 +3,15 @@
3
3
  * Pubkey Challenge Settings Loader
4
4
  *
5
5
  * Resolves public-key challenge auth config (crypto network, sign-in/sign-up
6
- * function names) from the scoped routing plane. Tries the pubkey_settings
7
- * table first, falls back to the api_modules approach.
6
+ * function names) from the scoped routing plane via the typed pubkey_settings
7
+ * table.
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.pubkeyLoader = void 0;
11
11
  const create_loader_1 = require("./create-loader");
12
+ const types_1 = require("./types");
12
13
  // ─── SQL ────────────────────────────────────────────────────────────────────
13
- const PUBKEY_SETTINGS_SQL = `
14
+ const pubkeySettingsSql = (schema) => `
14
15
  SELECT
15
16
  s.schema_name AS schema,
16
17
  ps.crypto_network,
@@ -18,7 +19,7 @@ const PUBKEY_SETTINGS_SQL = `
18
19
  sign_in_req_fn.name AS sign_in_request_challenge,
19
20
  sign_in_fail_fn.name AS sign_in_record_failure,
20
21
  sign_in_fn.name AS sign_in_with_challenge
21
- FROM constructive_routing_public.pubkey_settings ps
22
+ FROM "${schema}".pubkey_settings ps
22
23
  LEFT JOIN metaschema_public.schema s ON ps.schema_id = s.id
23
24
  LEFT JOIN metaschema_public.function sign_up_fn ON ps.sign_up_with_key_function_id = sign_up_fn.id
24
25
  LEFT JOIN metaschema_public.function sign_in_req_fn ON ps.sign_in_request_challenge_function_id = sign_in_req_fn.id
@@ -27,12 +28,6 @@ const PUBKEY_SETTINGS_SQL = `
27
28
  WHERE ps.database_id = $1
28
29
  LIMIT 1
29
30
  `;
30
- const PUBKEY_MODULE_SQL = `
31
- SELECT data
32
- FROM constructive_routing_public.api_modules
33
- WHERE api_id = $1 AND name = 'pubkey_challenge'
34
- LIMIT 1
35
- `;
36
31
  // ─── Transforms ─────────────────────────────────────────────────────────────
37
32
  function fromRow(row) {
38
33
  if (!row?.schema || !row?.sign_up_with_key)
@@ -51,22 +46,8 @@ exports.pubkeyLoader = (0, create_loader_1.createModuleLoader)({
51
46
  name: 'pubkeyChallengeSettings',
52
47
  ttlMs: 5 * 60_000,
53
48
  async resolve(ctx) {
54
- const { routingPool, databaseId, apiId } = ctx;
55
- // Try new pubkey_settings table first
56
- try {
57
- const result = await routingPool.query(PUBKEY_SETTINGS_SQL, [databaseId]);
58
- const resolved = fromRow(result.rows[0] ?? null);
59
- if (resolved)
60
- return resolved;
61
- }
62
- catch {
63
- // Table may not exist yet
64
- }
65
- // Fall back to api_modules
66
- if (apiId) {
67
- const result = await routingPool.query(PUBKEY_MODULE_SQL, [apiId]);
68
- return fromRow(result.rows[0]?.data ?? null);
69
- }
70
- return undefined;
49
+ const { routingPool, databaseId } = ctx;
50
+ const result = await routingPool.query(pubkeySettingsSql((0, types_1.routingSchemaOf)(ctx)), [databaseId]);
51
+ return fromRow(result.rows[0] ?? null);
71
52
  }
72
53
  });
package/loaders/rls.d.ts CHANGED
@@ -2,8 +2,8 @@
2
2
  * RLS Module Loader
3
3
  *
4
4
  * Resolves RLS authentication function names and schema references for
5
- * a given database from the scoped routing plane. Tries the rls_settings
6
- * table first, falls back to the api_modules approach.
5
+ * a given database from the scoped routing plane via the typed rls_settings
6
+ * table.
7
7
  */
8
8
  import type { RlsModule } from '../types';
9
9
  import type { ModuleLoader } from './types';
package/loaders/rls.js CHANGED
@@ -3,14 +3,15 @@
3
3
  * RLS Module Loader
4
4
  *
5
5
  * Resolves RLS authentication function names and schema references for
6
- * a given database from the scoped routing plane. Tries the rls_settings
7
- * table first, falls back to the api_modules approach.
6
+ * a given database from the scoped routing plane via the typed rls_settings
7
+ * table.
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.rlsLoader = void 0;
11
11
  const create_loader_1 = require("./create-loader");
12
+ const types_1 = require("./types");
12
13
  // ─── SQL ────────────────────────────────────────────────────────────────────
13
- const RLS_SETTINGS_SQL = `
14
+ const rlsSettingsSql = (schema) => `
14
15
  SELECT
15
16
  auth_schema.schema_name AS authenticate_schema,
16
17
  role_schema.schema_name AS role_schema,
@@ -20,7 +21,7 @@ const RLS_SETTINGS_SQL = `
20
21
  role_id_fn.name AS current_role_id,
21
22
  ua_fn.name AS current_user_agent,
22
23
  ip_fn.name AS current_ip_address
23
- FROM constructive_routing_public.rls_settings rs
24
+ FROM "${schema}".rls_settings rs
24
25
  LEFT JOIN metaschema_public.schema auth_schema ON rs.authenticate_schema_id = auth_schema.id
25
26
  LEFT JOIN metaschema_public.schema role_schema ON rs.role_schema_id = role_schema.id
26
27
  LEFT JOIN metaschema_public.function auth_fn ON rs.authenticate_function_id = auth_fn.id
@@ -32,12 +33,6 @@ const RLS_SETTINGS_SQL = `
32
33
  WHERE rs.database_id = $1
33
34
  LIMIT 1
34
35
  `;
35
- const RLS_MODULE_SQL = `
36
- SELECT data
37
- FROM constructive_routing_public.api_modules
38
- WHERE api_id = $1 AND name = 'rls_module'
39
- LIMIT 1
40
- `;
41
36
  // ─── Transforms ─────────────────────────────────────────────────────────────
42
37
  function fromSettings(row) {
43
38
  if (!row)
@@ -55,42 +50,13 @@ function fromSettings(row) {
55
50
  currentUserAgent: row.current_user_agent
56
51
  };
57
52
  }
58
- function fromModule(row) {
59
- if (!row?.data)
60
- return undefined;
61
- const d = row.data;
62
- return {
63
- authenticate: d.authenticate,
64
- authenticateStrict: d.authenticate_strict,
65
- privateSchema: { schemaName: d.authenticate_schema },
66
- publicSchema: { schemaName: d.role_schema },
67
- currentRole: d.current_role,
68
- currentRoleId: d.current_role_id,
69
- currentIpAddress: d.current_ip_address,
70
- currentUserAgent: d.current_user_agent
71
- };
72
- }
73
53
  // ─── Loader ─────────────────────────────────────────────────────────────────
74
54
  exports.rlsLoader = (0, create_loader_1.createModuleLoader)({
75
55
  name: 'rlsModule',
76
56
  ttlMs: 5 * 60_000,
77
57
  async resolve(ctx) {
78
- const { routingPool, databaseId, apiId } = ctx;
79
- // Try new rls_settings table first
80
- try {
81
- const result = await routingPool.query(RLS_SETTINGS_SQL, [databaseId]);
82
- const resolved = fromSettings(result.rows[0] ?? null);
83
- if (resolved)
84
- return resolved;
85
- }
86
- catch {
87
- // Table may not exist yet
88
- }
89
- // Fall back to api_modules
90
- if (apiId) {
91
- const result = await routingPool.query(RLS_MODULE_SQL, [apiId]);
92
- return fromModule(result.rows[0] ?? null);
93
- }
94
- return undefined;
58
+ const { routingPool, databaseId } = ctx;
59
+ const result = await routingPool.query(rlsSettingsSql((0, types_1.routingSchemaOf)(ctx)), [databaseId]);
60
+ return fromSettings(result.rows[0] ?? null);
95
61
  }
96
62
  });
@@ -10,14 +10,24 @@
10
10
  * `req.constructive.modules`.
11
11
  */
12
12
  import type { Pool } from 'pg';
13
+ /** Published logical name of the scoped routing plane. */
14
+ export declare const DEFAULT_ROUTING_SCHEMA = "routing_public";
15
+ /**
16
+ * The routing-plane schema a loader should query: the context override or
17
+ * the published default. Throws on names unsafe to interpolate as an
18
+ * identifier.
19
+ */
20
+ export declare const routingSchemaOf: (ctx: Pick<LoaderContext, "routingSchema">) => string;
13
21
  /**
14
22
  * Context passed to every loader's resolve function.
15
23
  * Provides both pool references so the loader can query whichever
16
24
  * database tier it needs.
17
25
  */
18
26
  export interface LoaderContext {
19
- /** Routing/configuration database pool (for constructive_routing_public.* lookups) */
27
+ /** Routing/configuration database pool (for routing-plane lookups) */
20
28
  routingPool: Pool;
29
+ /** Routing-plane schema to query (defaults to the published routing_public) */
30
+ routingSchema?: string;
21
31
  /** Tenant database pool (for metaschema_modules_public.* lookups) */
22
32
  tenantPool: Pool;
23
33
  /** UUID of the database being resolved */
package/loaders/types.js CHANGED
@@ -11,3 +11,19 @@
11
11
  * `req.constructive.modules`.
12
12
  */
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.routingSchemaOf = exports.DEFAULT_ROUTING_SCHEMA = void 0;
15
+ /** Published logical name of the scoped routing plane. */
16
+ exports.DEFAULT_ROUTING_SCHEMA = 'routing_public';
17
+ /**
18
+ * The routing-plane schema a loader should query: the context override or
19
+ * the published default. Throws on names unsafe to interpolate as an
20
+ * identifier.
21
+ */
22
+ const routingSchemaOf = (ctx) => {
23
+ const schema = ctx.routingSchema ?? exports.DEFAULT_ROUTING_SCHEMA;
24
+ if (!/^[a-z_][a-z0-9_]*$/.test(schema)) {
25
+ throw new Error(`invalid routing schema name: ${schema}`);
26
+ }
27
+ return schema;
28
+ };
29
+ exports.routingSchemaOf = routingSchemaOf;
@@ -8,8 +8,9 @@
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.webauthnLoader = void 0;
10
10
  const create_loader_1 = require("./create-loader");
11
+ const types_1 = require("./types");
11
12
  // ─── SQL ────────────────────────────────────────────────────────────────────
12
- const WEBAUTHN_SETTINGS_SQL = `
13
+ const webauthnSettingsSql = (schema) => `
13
14
  SELECT
14
15
  s.schema_name AS schema,
15
16
  cred_s.schema_name AS credentials_schema,
@@ -22,7 +23,7 @@ const WEBAUTHN_SETTINGS_SQL = `
22
23
  ws.require_user_verification,
23
24
  ws.resident_key,
24
25
  ws.challenge_expiry_seconds
25
- FROM constructive_routing_public.webauthn_settings ws
26
+ FROM "${schema}".webauthn_settings ws
26
27
  LEFT JOIN metaschema_public.schema s ON ws.schema_id = s.id
27
28
  LEFT JOIN metaschema_public.schema cred_s ON ws.credentials_schema_id = cred_s.id
28
29
  LEFT JOIN metaschema_public.schema sess_s ON ws.sessions_schema_id = sess_s.id
@@ -36,7 +37,7 @@ exports.webauthnLoader = (0, create_loader_1.createModuleLoader)({
36
37
  ttlMs: 5 * 60_000,
37
38
  async resolve(ctx) {
38
39
  const { routingPool, databaseId } = ctx;
39
- const result = await routingPool.query(WEBAUTHN_SETTINGS_SQL, [databaseId]);
40
+ const result = await routingPool.query(webauthnSettingsSql((0, types_1.routingSchemaOf)(ctx)), [databaseId]);
40
41
  const row = result.rows[0];
41
42
  if (!row?.schema)
42
43
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constructive-io/express-context",
3
- "version": "0.14.2",
3
+ "version": "0.18.0",
4
4
  "description": "Extractable Express middleware for Constructive tenant context — domain resolution, JWT auth, pgSettings, and withPgClient",
5
5
  "author": "Constructive <developers@constructive.io>",
6
6
  "homepage": "https://github.com/constructive-io/constructive",
@@ -29,15 +29,15 @@
29
29
  "test:watch": "jest --watch"
30
30
  },
31
31
  "dependencies": {
32
- "@constructive-io/url-domains": "^2.20.2",
33
- "@pgpmjs/env": "^2.30.2",
34
- "@pgpmjs/logger": "^2.15.2",
35
- "@pgpmjs/server-utils": "^3.16.2",
36
- "@pgpmjs/types": "^2.37.2",
32
+ "@constructive-io/url-domains": "^2.23.0",
33
+ "@pgpmjs/env": "^2.33.0",
34
+ "@pgpmjs/logger": "^2.18.0",
35
+ "@pgpmjs/server-utils": "^3.19.0",
36
+ "@pgpmjs/types": "^2.40.0",
37
37
  "lru-cache": "^11.2.7",
38
38
  "pg": "^8.21.0",
39
- "pg-cache": "^3.16.2",
40
- "pg-query-context": "^2.20.2"
39
+ "pg-cache": "^3.19.0",
40
+ "pg-query-context": "^2.23.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/express": "^5.0.6",
@@ -59,5 +59,5 @@
59
59
  "jwt",
60
60
  "pgSettings"
61
61
  ],
62
- "gitHead": "08741ce3a6897192c2dbc7d1da48b9f456ffb59f"
62
+ "gitHead": "73f55ad24d9a92e7ef1e268526b31bfb7bb12b28"
63
63
  }
package/types.d.ts CHANGED
@@ -8,20 +8,6 @@
8
8
  */
9
9
  import type { Pool, PoolClient } from 'pg';
10
10
  import type { BillingClient } from './billing-client';
11
- export interface CorsModuleData {
12
- urls: string[];
13
- }
14
- export interface PublicKeyChallengeData {
15
- schema: string;
16
- crypto_network: string;
17
- sign_up_with_key: string;
18
- sign_in_request_challenge: string;
19
- sign_in_record_failure: string;
20
- sign_in_with_challenge: string;
21
- }
22
- export interface GenericModuleData {
23
- [key: string]: unknown;
24
- }
25
11
  export interface DatabaseSettings {
26
12
  enableAggregates: boolean;
27
13
  enablePostgis: boolean;
@@ -57,16 +43,6 @@ export interface WebauthnSettings {
57
43
  residentKey: string;
58
44
  challengeExpirySeconds: number;
59
45
  }
60
- export type ApiModule = {
61
- name: 'cors';
62
- data: CorsModuleData;
63
- } | {
64
- name: 'pubkey_challenge';
65
- data: PublicKeyChallengeData;
66
- } | {
67
- name: string;
68
- data?: GenericModuleData;
69
- };
70
46
  export interface RlsModule {
71
47
  authenticate: string;
72
48
  authenticateStrict: string;
@@ -98,7 +74,6 @@ export interface ApiStructure {
98
74
  anonRole: string;
99
75
  roleName: string;
100
76
  schema: string[];
101
- apiModules: ApiModule[];
102
77
  rlsModule?: RlsModule;
103
78
  domains?: string[];
104
79
  databaseId?: string;