@constructive-io/express-context 0.2.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.
Files changed (51) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +95 -0
  3. package/context.d.ts +61 -0
  4. package/context.js +119 -0
  5. package/esm/context.js +115 -0
  6. package/esm/index.js +45 -0
  7. package/esm/loaders/auth-settings.js +62 -0
  8. package/esm/loaders/cors.js +55 -0
  9. package/esm/loaders/create-loader.js +53 -0
  10. package/esm/loaders/database-settings.js +52 -0
  11. package/esm/loaders/index.js +56 -0
  12. package/esm/loaders/pubkey.js +69 -0
  13. package/esm/loaders/registry.js +69 -0
  14. package/esm/loaders/rls.js +93 -0
  15. package/esm/loaders/types.js +12 -0
  16. package/esm/loaders/webauthn.js +54 -0
  17. package/esm/pg-client.js +8 -0
  18. package/esm/pg-settings.js +44 -0
  19. package/esm/request-id.js +19 -0
  20. package/esm/types.js +9 -0
  21. package/index.d.ts +46 -0
  22. package/index.js +62 -0
  23. package/loaders/auth-settings.d.ts +14 -0
  24. package/loaders/auth-settings.js +65 -0
  25. package/loaders/cors.d.ts +9 -0
  26. package/loaders/cors.js +58 -0
  27. package/loaders/create-loader.d.ts +19 -0
  28. package/loaders/create-loader.js +56 -0
  29. package/loaders/database-settings.d.ts +10 -0
  30. package/loaders/database-settings.js +55 -0
  31. package/loaders/index.d.ts +39 -0
  32. package/loaders/index.js +68 -0
  33. package/loaders/pubkey.d.ts +10 -0
  34. package/loaders/pubkey.js +72 -0
  35. package/loaders/registry.d.ts +37 -0
  36. package/loaders/registry.js +72 -0
  37. package/loaders/rls.d.ts +10 -0
  38. package/loaders/rls.js +96 -0
  39. package/loaders/types.d.ts +43 -0
  40. package/loaders/types.js +13 -0
  41. package/loaders/webauthn.d.ts +9 -0
  42. package/loaders/webauthn.js +57 -0
  43. package/package.json +63 -0
  44. package/pg-client.d.ts +8 -0
  45. package/pg-client.js +12 -0
  46. package/pg-settings.d.ts +29 -0
  47. package/pg-settings.js +47 -0
  48. package/request-id.d.ts +14 -0
  49. package/request-id.js +22 -0
  50. package/types.d.ts +197 -0
  51. package/types.js +10 -0
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ /**
3
+ * Auth Settings Loader (Tier 2 — tenant DB)
4
+ *
5
+ * Two-step discovery:
6
+ * 1. Query metaschema_modules_public.sessions_module to find the
7
+ * schema name and table name for auth settings
8
+ * 2. Query the discovered <schema>.<table> in the tenant DB
9
+ *
10
+ * This is the pattern for any module whose config lives in the tenant
11
+ * database rather than the services database.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.authSettingsLoader = void 0;
15
+ const create_loader_1 = require("./create-loader");
16
+ // ─── SQL ────────────────────────────────────────────────────────────────────
17
+ const AUTH_SETTINGS_DISCOVERY_SQL = `
18
+ SELECT s.schema_name, sm.auth_settings_table AS table_name
19
+ FROM metaschema_modules_public.sessions_module sm
20
+ JOIN metaschema_public.schema s ON s.id = sm.schema_id
21
+ LIMIT 1
22
+ `;
23
+ const buildAuthSettingsQuery = (schemaName, tableName) => `
24
+ SELECT
25
+ cookie_secure,
26
+ cookie_samesite,
27
+ cookie_domain,
28
+ cookie_httponly,
29
+ cookie_max_age,
30
+ cookie_path,
31
+ remember_me_duration,
32
+ enable_captcha,
33
+ captcha_site_key
34
+ FROM "${schemaName}"."${tableName}"
35
+ LIMIT 1
36
+ `;
37
+ // ─── Loader ─────────────────────────────────────────────────────────────────
38
+ exports.authSettingsLoader = (0, create_loader_1.createModuleLoader)({
39
+ name: 'authSettings',
40
+ ttlMs: 5 * 60_000,
41
+ async resolve(ctx) {
42
+ const { tenantPool } = ctx;
43
+ // Step 1: Discover schema + table from sessions_module
44
+ const discovery = await tenantPool.query(AUTH_SETTINGS_DISCOVERY_SQL);
45
+ const resolved = discovery.rows[0];
46
+ if (!resolved)
47
+ return undefined;
48
+ // Step 2: Query the actual auth settings table
49
+ const result = await tenantPool.query(buildAuthSettingsQuery(resolved.schema_name, resolved.table_name));
50
+ const row = result.rows[0];
51
+ if (!row)
52
+ return undefined;
53
+ return {
54
+ cookieSecure: row.cookie_secure,
55
+ cookieSamesite: row.cookie_samesite,
56
+ cookieDomain: row.cookie_domain,
57
+ cookieHttponly: row.cookie_httponly,
58
+ cookieMaxAge: row.cookie_max_age,
59
+ cookiePath: row.cookie_path,
60
+ rememberMeDuration: row.remember_me_duration,
61
+ enableCaptcha: row.enable_captcha,
62
+ captchaSiteKey: row.captcha_site_key,
63
+ };
64
+ },
65
+ });
@@ -0,0 +1,9 @@
1
+ /**
2
+ * CORS Origins Loader
3
+ *
4
+ * Resolves allowed CORS origins for a database+API combination.
5
+ * Checks per-API settings first, falls back to database-level default,
6
+ * then to the legacy api_modules approach.
7
+ */
8
+ import type { ModuleLoader } from './types';
9
+ export declare const corsLoader: ModuleLoader<string[]>;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ /**
3
+ * CORS Origins Loader
4
+ *
5
+ * Resolves allowed CORS origins for a database+API combination.
6
+ * Checks per-API settings first, falls back to database-level default,
7
+ * then to the legacy api_modules approach.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.corsLoader = void 0;
11
+ const create_loader_1 = require("./create-loader");
12
+ // ─── SQL ────────────────────────────────────────────────────────────────────
13
+ const CORS_SETTINGS_SQL = `
14
+ SELECT allowed_origins
15
+ FROM services_public.cors_settings
16
+ WHERE database_id = $1 AND api_id = $2
17
+ LIMIT 1
18
+ `;
19
+ const CORS_SETTINGS_DB_DEFAULT_SQL = `
20
+ SELECT allowed_origins
21
+ FROM services_public.cors_settings
22
+ WHERE database_id = $1 AND api_id IS NULL
23
+ LIMIT 1
24
+ `;
25
+ const CORS_MODULE_SQL = `
26
+ SELECT data
27
+ FROM services_public.api_modules
28
+ WHERE api_id = $1 AND name = 'cors'
29
+ LIMIT 1
30
+ `;
31
+ // ─── Loader ─────────────────────────────────────────────────────────────────
32
+ exports.corsLoader = (0, create_loader_1.createModuleLoader)({
33
+ name: 'corsOrigins',
34
+ ttlMs: 5 * 60_000,
35
+ async resolve(ctx) {
36
+ const { servicesPool, databaseId, apiId } = ctx;
37
+ // Try per-API cors_settings first
38
+ try {
39
+ if (apiId) {
40
+ const perApi = await servicesPool.query(CORS_SETTINGS_SQL, [databaseId, apiId]);
41
+ if (perApi.rows[0])
42
+ return perApi.rows[0].allowed_origins;
43
+ }
44
+ const dbDefault = await servicesPool.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 legacy api_modules
52
+ if (apiId) {
53
+ const result = await servicesPool.query(CORS_MODULE_SQL, [apiId]);
54
+ return result.rows[0]?.data?.urls;
55
+ }
56
+ return undefined;
57
+ },
58
+ });
@@ -0,0 +1,19 @@
1
+ /**
2
+ * create-loader — Factory for building cached ModuleLoader instances.
3
+ *
4
+ * Wraps a raw resolve function with an LRU cache keyed by databaseId.
5
+ * Each loader gets its own independent cache with configurable TTL and
6
+ * max entries.
7
+ */
8
+ import type { LoaderContext, ModuleLoader } from './types';
9
+ export interface CreateLoaderOptions<T> {
10
+ /** Unique loader name (used in log prefix and modules map key) */
11
+ name: string;
12
+ /** TTL in milliseconds (default: 60_000 — 1 minute) */
13
+ ttlMs?: number;
14
+ /** Max cache entries before LRU eviction (default: 100) */
15
+ max?: number;
16
+ /** The actual resolution function. Called on cache miss. */
17
+ resolve: (ctx: LoaderContext) => Promise<T | undefined>;
18
+ }
19
+ export declare function createModuleLoader<T>(opts: CreateLoaderOptions<T>): ModuleLoader<T>;
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ /**
3
+ * create-loader — Factory for building cached ModuleLoader instances.
4
+ *
5
+ * Wraps a raw resolve function with an LRU cache keyed by databaseId.
6
+ * Each loader gets its own independent cache with configurable TTL and
7
+ * max entries.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.createModuleLoader = createModuleLoader;
11
+ const lru_cache_1 = require("lru-cache");
12
+ const logger_1 = require("@pgpmjs/logger");
13
+ const DEFAULT_TTL_MS = 60_000;
14
+ const DEFAULT_MAX = 100;
15
+ function createModuleLoader(opts) {
16
+ const log = new logger_1.Logger(`loader:${opts.name}`);
17
+ const cache = new lru_cache_1.LRUCache({
18
+ max: opts.max ?? DEFAULT_MAX,
19
+ ttl: opts.ttlMs ?? DEFAULT_TTL_MS,
20
+ updateAgeOnGet: true,
21
+ allowStale: false,
22
+ });
23
+ return {
24
+ name: opts.name,
25
+ async resolve(ctx) {
26
+ const key = ctx.databaseId;
27
+ if (cache.has(key)) {
28
+ log.debug(`Cache HIT databaseId=${key}`);
29
+ return cache.get(key);
30
+ }
31
+ log.debug(`Cache MISS databaseId=${key}, resolving`);
32
+ try {
33
+ const value = await opts.resolve(ctx);
34
+ cache.set(key, value);
35
+ return value;
36
+ }
37
+ catch (e) {
38
+ log.warn(`Failed to resolve databaseId=${key}: ${e.message}`);
39
+ return undefined;
40
+ }
41
+ },
42
+ invalidate(databaseId) {
43
+ if (databaseId) {
44
+ cache.delete(databaseId);
45
+ log.debug(`Invalidated databaseId=${databaseId}`);
46
+ }
47
+ else {
48
+ cache.clear();
49
+ log.debug(`Invalidated all entries (was size=${cache.size})`);
50
+ }
51
+ },
52
+ get cacheSize() {
53
+ return cache.size;
54
+ },
55
+ };
56
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Database Settings Loader
3
+ *
4
+ * Resolves per-database feature flags (aggregates, postgis, search,
5
+ * uploads, many-to-many, connection filters, ltree, llm, realtime, bulk).
6
+ * Merges database-level defaults with optional per-API overrides.
7
+ */
8
+ import type { DatabaseSettings } from '../types';
9
+ import type { ModuleLoader } from './types';
10
+ export declare const databaseSettingsLoader: ModuleLoader<DatabaseSettings>;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ /**
3
+ * Database Settings Loader
4
+ *
5
+ * Resolves per-database feature flags (aggregates, postgis, search,
6
+ * uploads, many-to-many, connection filters, ltree, llm, realtime, bulk).
7
+ * Merges database-level defaults with optional per-API overrides.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.databaseSettingsLoader = void 0;
11
+ const create_loader_1 = require("./create-loader");
12
+ // ─── SQL ────────────────────────────────────────────────────────────────────
13
+ const DATABASE_SETTINGS_SQL = `
14
+ SELECT
15
+ COALESCE(aps.enable_aggregates, ds.enable_aggregates) AS resolved_enable_aggregates,
16
+ COALESCE(aps.enable_postgis, ds.enable_postgis) AS resolved_enable_postgis,
17
+ COALESCE(aps.enable_search, ds.enable_search) AS resolved_enable_search,
18
+ COALESCE(aps.enable_direct_uploads, ds.enable_direct_uploads) AS resolved_enable_direct_uploads,
19
+ COALESCE(aps.enable_presigned_uploads, ds.enable_presigned_uploads) AS resolved_enable_presigned_uploads,
20
+ COALESCE(aps.enable_many_to_many, ds.enable_many_to_many) AS resolved_enable_many_to_many,
21
+ COALESCE(aps.enable_connection_filter, ds.enable_connection_filter) AS resolved_enable_connection_filter,
22
+ COALESCE(aps.enable_ltree, ds.enable_ltree) AS resolved_enable_ltree,
23
+ COALESCE(aps.enable_llm, ds.enable_llm) AS resolved_enable_llm,
24
+ COALESCE(aps.enable_realtime, ds.enable_realtime) AS resolved_enable_realtime,
25
+ COALESCE(aps.enable_bulk, ds.enable_bulk) AS resolved_enable_bulk
26
+ FROM services_public.database_settings ds
27
+ LEFT JOIN services_public.api_settings aps ON ds.database_id = aps.database_id AND aps.api_id = $2
28
+ WHERE ds.database_id = $1
29
+ LIMIT 1
30
+ `;
31
+ // ─── Loader ─────────────────────────────────────────────────────────────────
32
+ exports.databaseSettingsLoader = (0, create_loader_1.createModuleLoader)({
33
+ name: 'databaseSettings',
34
+ ttlMs: 5 * 60_000,
35
+ async resolve(ctx) {
36
+ const { servicesPool, databaseId, apiId } = ctx;
37
+ const result = await servicesPool.query(DATABASE_SETTINGS_SQL, [databaseId, apiId ?? null]);
38
+ const row = result.rows[0];
39
+ if (!row)
40
+ return undefined;
41
+ return {
42
+ enableAggregates: row.resolved_enable_aggregates,
43
+ enablePostgis: row.resolved_enable_postgis,
44
+ enableSearch: row.resolved_enable_search,
45
+ enableDirectUploads: row.resolved_enable_direct_uploads,
46
+ enablePresignedUploads: row.resolved_enable_presigned_uploads,
47
+ enableManyToMany: row.resolved_enable_many_to_many,
48
+ enableConnectionFilter: row.resolved_enable_connection_filter,
49
+ enableLtree: row.resolved_enable_ltree,
50
+ enableLlm: row.resolved_enable_llm,
51
+ enableRealtime: row.resolved_enable_realtime,
52
+ enableBulk: row.resolved_enable_bulk,
53
+ };
54
+ },
55
+ });
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Module Loaders — pluggable per-database cached lookups.
3
+ *
4
+ * Each loader encapsulates a SQL query + type transform + LRU cache
5
+ * for one piece of per-database configuration. Register loaders in
6
+ * a LoaderRegistry and pass it to createContextMiddleware().
7
+ *
8
+ * Built-in loaders cover the standard Constructive modules:
9
+ * - rlsModule (services_public.rls_settings)
10
+ * - corsOrigins (services_public.cors_settings)
11
+ * - databaseSettings(services_public.database_settings)
12
+ * - pubkeyChallengeSettings (services_public.pubkey_settings)
13
+ * - webauthnSettings(services_public.webauthn_settings)
14
+ * - authSettings (metaschema_modules_public.sessions_module → tenant DB)
15
+ *
16
+ * To add a new per-db lookup, implement a ModuleLoader and register it:
17
+ *
18
+ * const myLoader = createModuleLoader({
19
+ * name: 'myModule',
20
+ * ttlMs: 60_000,
21
+ * async resolve(ctx) {
22
+ * const { rows } = await ctx.tenantPool.query(MY_SQL, [ctx.databaseId]);
23
+ * return rows[0] ? transform(rows[0]) : undefined;
24
+ * },
25
+ * });
26
+ * registry.register(myLoader);
27
+ */
28
+ export type { LoaderContext, ModuleLoader } from './types';
29
+ export type { CreateLoaderOptions } from './create-loader';
30
+ export { createModuleLoader } from './create-loader';
31
+ export type { LoaderRegistry } from './registry';
32
+ export { createLoaderRegistry } from './registry';
33
+ export { rlsLoader } from './rls';
34
+ export { corsLoader } from './cors';
35
+ export { databaseSettingsLoader } from './database-settings';
36
+ export { pubkeyLoader } from './pubkey';
37
+ export { webauthnLoader } from './webauthn';
38
+ export { authSettingsLoader } from './auth-settings';
39
+ export declare function createDefaultRegistry(): import("./registry").LoaderRegistry;
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ /**
3
+ * Module Loaders — pluggable per-database cached lookups.
4
+ *
5
+ * Each loader encapsulates a SQL query + type transform + LRU cache
6
+ * for one piece of per-database configuration. Register loaders in
7
+ * a LoaderRegistry and pass it to createContextMiddleware().
8
+ *
9
+ * Built-in loaders cover the standard Constructive modules:
10
+ * - rlsModule (services_public.rls_settings)
11
+ * - corsOrigins (services_public.cors_settings)
12
+ * - databaseSettings(services_public.database_settings)
13
+ * - pubkeyChallengeSettings (services_public.pubkey_settings)
14
+ * - webauthnSettings(services_public.webauthn_settings)
15
+ * - authSettings (metaschema_modules_public.sessions_module → tenant DB)
16
+ *
17
+ * To add a new per-db lookup, implement a ModuleLoader and register it:
18
+ *
19
+ * const myLoader = createModuleLoader({
20
+ * name: 'myModule',
21
+ * ttlMs: 60_000,
22
+ * async resolve(ctx) {
23
+ * const { rows } = await ctx.tenantPool.query(MY_SQL, [ctx.databaseId]);
24
+ * return rows[0] ? transform(rows[0]) : undefined;
25
+ * },
26
+ * });
27
+ * registry.register(myLoader);
28
+ */
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.authSettingsLoader = exports.webauthnLoader = exports.pubkeyLoader = exports.databaseSettingsLoader = exports.corsLoader = exports.rlsLoader = exports.createLoaderRegistry = exports.createModuleLoader = void 0;
31
+ exports.createDefaultRegistry = createDefaultRegistry;
32
+ var create_loader_1 = require("./create-loader");
33
+ Object.defineProperty(exports, "createModuleLoader", { enumerable: true, get: function () { return create_loader_1.createModuleLoader; } });
34
+ var registry_1 = require("./registry");
35
+ Object.defineProperty(exports, "createLoaderRegistry", { enumerable: true, get: function () { return registry_1.createLoaderRegistry; } });
36
+ // Built-in loaders
37
+ var rls_1 = require("./rls");
38
+ Object.defineProperty(exports, "rlsLoader", { enumerable: true, get: function () { return rls_1.rlsLoader; } });
39
+ var cors_1 = require("./cors");
40
+ Object.defineProperty(exports, "corsLoader", { enumerable: true, get: function () { return cors_1.corsLoader; } });
41
+ var database_settings_1 = require("./database-settings");
42
+ Object.defineProperty(exports, "databaseSettingsLoader", { enumerable: true, get: function () { return database_settings_1.databaseSettingsLoader; } });
43
+ var pubkey_1 = require("./pubkey");
44
+ Object.defineProperty(exports, "pubkeyLoader", { enumerable: true, get: function () { return pubkey_1.pubkeyLoader; } });
45
+ var webauthn_1 = require("./webauthn");
46
+ Object.defineProperty(exports, "webauthnLoader", { enumerable: true, get: function () { return webauthn_1.webauthnLoader; } });
47
+ var auth_settings_1 = require("./auth-settings");
48
+ Object.defineProperty(exports, "authSettingsLoader", { enumerable: true, get: function () { return auth_settings_1.authSettingsLoader; } });
49
+ /**
50
+ * Convenience: create a registry pre-loaded with all built-in loaders.
51
+ */
52
+ const registry_2 = require("./registry");
53
+ const rls_2 = require("./rls");
54
+ const cors_2 = require("./cors");
55
+ const database_settings_2 = require("./database-settings");
56
+ const pubkey_2 = require("./pubkey");
57
+ const webauthn_2 = require("./webauthn");
58
+ const auth_settings_2 = require("./auth-settings");
59
+ function createDefaultRegistry() {
60
+ const registry = (0, registry_2.createLoaderRegistry)();
61
+ registry.register(rls_2.rlsLoader);
62
+ registry.register(cors_2.corsLoader);
63
+ registry.register(database_settings_2.databaseSettingsLoader);
64
+ registry.register(pubkey_2.pubkeyLoader);
65
+ registry.register(webauthn_2.webauthnLoader);
66
+ registry.register(auth_settings_2.authSettingsLoader);
67
+ return registry;
68
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Pubkey Challenge Settings Loader
3
+ *
4
+ * Resolves public-key challenge auth config (crypto network, sign-in/sign-up
5
+ * function names). Tries the new pubkey_settings table first, falls back
6
+ * to the legacy api_modules approach.
7
+ */
8
+ import type { PubkeyChallengeSettings } from '../types';
9
+ import type { ModuleLoader } from './types';
10
+ export declare const pubkeyLoader: ModuleLoader<PubkeyChallengeSettings>;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ /**
3
+ * Pubkey Challenge Settings Loader
4
+ *
5
+ * Resolves public-key challenge auth config (crypto network, sign-in/sign-up
6
+ * function names). Tries the new pubkey_settings table first, falls back
7
+ * to the legacy api_modules approach.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.pubkeyLoader = void 0;
11
+ const create_loader_1 = require("./create-loader");
12
+ // ─── SQL ────────────────────────────────────────────────────────────────────
13
+ const PUBKEY_SETTINGS_SQL = `
14
+ SELECT
15
+ s.schema_name AS schema,
16
+ ps.crypto_network,
17
+ sign_up_fn.name AS sign_up_with_key,
18
+ sign_in_req_fn.name AS sign_in_request_challenge,
19
+ sign_in_fail_fn.name AS sign_in_record_failure,
20
+ sign_in_fn.name AS sign_in_with_challenge
21
+ FROM services_public.pubkey_settings ps
22
+ LEFT JOIN metaschema_public.schema s ON ps.schema_id = s.id
23
+ LEFT JOIN metaschema_public.function sign_up_fn ON ps.sign_up_with_key_function_id = sign_up_fn.id
24
+ LEFT JOIN metaschema_public.function sign_in_req_fn ON ps.sign_in_request_challenge_function_id = sign_in_req_fn.id
25
+ LEFT JOIN metaschema_public.function sign_in_fail_fn ON ps.sign_in_record_failure_function_id = sign_in_fail_fn.id
26
+ LEFT JOIN metaschema_public.function sign_in_fn ON ps.sign_in_with_challenge_function_id = sign_in_fn.id
27
+ WHERE ps.database_id = $1
28
+ LIMIT 1
29
+ `;
30
+ const PUBKEY_MODULE_SQL = `
31
+ SELECT data
32
+ FROM services_public.api_modules
33
+ WHERE api_id = $1 AND name = 'pubkey_challenge'
34
+ LIMIT 1
35
+ `;
36
+ // ─── Transforms ─────────────────────────────────────────────────────────────
37
+ function fromRow(row) {
38
+ if (!row?.schema || !row?.sign_up_with_key)
39
+ return undefined;
40
+ return {
41
+ schema: row.schema,
42
+ cryptoNetwork: row.crypto_network,
43
+ signUpWithKey: row.sign_up_with_key,
44
+ signInRequestChallenge: row.sign_in_request_challenge,
45
+ signInRecordFailure: row.sign_in_record_failure,
46
+ signInWithChallenge: row.sign_in_with_challenge,
47
+ };
48
+ }
49
+ // ─── Loader ─────────────────────────────────────────────────────────────────
50
+ exports.pubkeyLoader = (0, create_loader_1.createModuleLoader)({
51
+ name: 'pubkeyChallengeSettings',
52
+ ttlMs: 5 * 60_000,
53
+ async resolve(ctx) {
54
+ const { servicesPool, databaseId, apiId } = ctx;
55
+ // Try new pubkey_settings table first
56
+ try {
57
+ const result = await servicesPool.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 legacy api_modules
66
+ if (apiId) {
67
+ const result = await servicesPool.query(PUBKEY_MODULE_SQL, [apiId]);
68
+ return fromRow(result.rows[0]?.data ?? null);
69
+ }
70
+ return undefined;
71
+ },
72
+ });
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Loader Registry — manages a set of ModuleLoader instances.
3
+ *
4
+ * Supports two resolution modes:
5
+ * - Lazy (preferred): `resolve(name, ctx)` resolves a single loader
6
+ * on-demand. Only fires the SQL query if the cache misses. Middleware
7
+ * requests only the loaders it actually needs.
8
+ * - Eager: `resolveAll(ctx)` resolves every registered loader in
9
+ * parallel. Useful for pre-warming or migration from the monolithic
10
+ * svcCache pattern.
11
+ *
12
+ * Each loader's result is independently cached per databaseId — resolving
13
+ * one module never invalidates another.
14
+ */
15
+ import type { LoaderContext, ModuleLoader } from './types';
16
+ export interface LoaderRegistry {
17
+ /** Register a loader. Throws if a loader with the same name already exists. */
18
+ register(loader: ModuleLoader): void;
19
+ /**
20
+ * Resolve a single loader by name (lazy, on-demand).
21
+ * Returns undefined if the loader isn't registered or the module
22
+ * isn't provisioned for this database. Results are cached per databaseId
23
+ * inside the loader's own LRU — repeated calls are cheap.
24
+ */
25
+ resolve<T = unknown>(name: string, ctx: LoaderContext): Promise<T | undefined>;
26
+ /** Resolve all registered loaders in parallel (eager, for migration/pre-warm). */
27
+ resolveAll(ctx: LoaderContext): Promise<Record<string, unknown>>;
28
+ /** Get a specific loader by name (for direct access / advanced usage). */
29
+ get<T = unknown>(name: string): ModuleLoader<T> | undefined;
30
+ /** Check whether a loader is registered. */
31
+ has(name: string): boolean;
32
+ /** Invalidate caches for one database (or all databases if omitted). */
33
+ invalidate(databaseId?: string): void;
34
+ /** List all registered loader names. */
35
+ readonly names: string[];
36
+ }
37
+ export declare function createLoaderRegistry(): LoaderRegistry;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ /**
3
+ * Loader Registry — manages a set of ModuleLoader instances.
4
+ *
5
+ * Supports two resolution modes:
6
+ * - Lazy (preferred): `resolve(name, ctx)` resolves a single loader
7
+ * on-demand. Only fires the SQL query if the cache misses. Middleware
8
+ * requests only the loaders it actually needs.
9
+ * - Eager: `resolveAll(ctx)` resolves every registered loader in
10
+ * parallel. Useful for pre-warming or migration from the monolithic
11
+ * svcCache pattern.
12
+ *
13
+ * Each loader's result is independently cached per databaseId — resolving
14
+ * one module never invalidates another.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.createLoaderRegistry = createLoaderRegistry;
18
+ const logger_1 = require("@pgpmjs/logger");
19
+ const log = new logger_1.Logger('loader-registry');
20
+ function createLoaderRegistry() {
21
+ const loaders = new Map();
22
+ return {
23
+ register(loader) {
24
+ if (loaders.has(loader.name)) {
25
+ throw new Error(`Loader "${loader.name}" is already registered`);
26
+ }
27
+ loaders.set(loader.name, loader);
28
+ log.debug(`Registered loader: ${loader.name}`);
29
+ },
30
+ async resolve(name, ctx) {
31
+ const loader = loaders.get(name);
32
+ if (!loader) {
33
+ log.debug(`Loader "${name}" not registered, returning undefined`);
34
+ return undefined;
35
+ }
36
+ return loader.resolve(ctx);
37
+ },
38
+ async resolveAll(ctx) {
39
+ if (loaders.size === 0)
40
+ return {};
41
+ const entries = Array.from(loaders.entries());
42
+ const results = await Promise.all(entries.map(async ([name, loader]) => {
43
+ const value = await loader.resolve(ctx);
44
+ return [name, value];
45
+ }));
46
+ const modules = {};
47
+ for (const [name, value] of results) {
48
+ if (value !== undefined) {
49
+ modules[name] = value;
50
+ }
51
+ }
52
+ return modules;
53
+ },
54
+ get(name) {
55
+ return loaders.get(name);
56
+ },
57
+ has(name) {
58
+ return loaders.has(name);
59
+ },
60
+ invalidate(databaseId) {
61
+ for (const loader of loaders.values()) {
62
+ loader.invalidate(databaseId);
63
+ }
64
+ log.debug(databaseId
65
+ ? `Invalidated all loaders for databaseId=${databaseId}`
66
+ : 'Invalidated all loaders');
67
+ },
68
+ get names() {
69
+ return Array.from(loaders.keys());
70
+ },
71
+ };
72
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * RLS Module Loader
3
+ *
4
+ * Resolves RLS authentication function names and schema references for
5
+ * a given database. Tries the new rls_settings table first, falls back
6
+ * to the legacy api_modules approach.
7
+ */
8
+ import type { RlsModule } from '../types';
9
+ import type { ModuleLoader } from './types';
10
+ export declare const rlsLoader: ModuleLoader<RlsModule>;