@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,52 @@
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 { createModuleLoader } from './create-loader';
9
+ // ─── SQL ────────────────────────────────────────────────────────────────────
10
+ const DATABASE_SETTINGS_SQL = `
11
+ SELECT
12
+ COALESCE(aps.enable_aggregates, ds.enable_aggregates) AS resolved_enable_aggregates,
13
+ COALESCE(aps.enable_postgis, ds.enable_postgis) AS resolved_enable_postgis,
14
+ COALESCE(aps.enable_search, ds.enable_search) AS resolved_enable_search,
15
+ COALESCE(aps.enable_direct_uploads, ds.enable_direct_uploads) AS resolved_enable_direct_uploads,
16
+ COALESCE(aps.enable_presigned_uploads, ds.enable_presigned_uploads) AS resolved_enable_presigned_uploads,
17
+ COALESCE(aps.enable_many_to_many, ds.enable_many_to_many) AS resolved_enable_many_to_many,
18
+ COALESCE(aps.enable_connection_filter, ds.enable_connection_filter) AS resolved_enable_connection_filter,
19
+ COALESCE(aps.enable_ltree, ds.enable_ltree) AS resolved_enable_ltree,
20
+ COALESCE(aps.enable_llm, ds.enable_llm) AS resolved_enable_llm,
21
+ COALESCE(aps.enable_realtime, ds.enable_realtime) AS resolved_enable_realtime,
22
+ COALESCE(aps.enable_bulk, ds.enable_bulk) AS resolved_enable_bulk
23
+ FROM services_public.database_settings ds
24
+ LEFT JOIN services_public.api_settings aps ON ds.database_id = aps.database_id AND aps.api_id = $2
25
+ WHERE ds.database_id = $1
26
+ LIMIT 1
27
+ `;
28
+ // ─── Loader ─────────────────────────────────────────────────────────────────
29
+ export const databaseSettingsLoader = createModuleLoader({
30
+ name: 'databaseSettings',
31
+ ttlMs: 5 * 60_000,
32
+ async resolve(ctx) {
33
+ const { servicesPool, databaseId, apiId } = ctx;
34
+ const result = await servicesPool.query(DATABASE_SETTINGS_SQL, [databaseId, apiId ?? null]);
35
+ const row = result.rows[0];
36
+ if (!row)
37
+ return undefined;
38
+ return {
39
+ enableAggregates: row.resolved_enable_aggregates,
40
+ enablePostgis: row.resolved_enable_postgis,
41
+ enableSearch: row.resolved_enable_search,
42
+ enableDirectUploads: row.resolved_enable_direct_uploads,
43
+ enablePresignedUploads: row.resolved_enable_presigned_uploads,
44
+ enableManyToMany: row.resolved_enable_many_to_many,
45
+ enableConnectionFilter: row.resolved_enable_connection_filter,
46
+ enableLtree: row.resolved_enable_ltree,
47
+ enableLlm: row.resolved_enable_llm,
48
+ enableRealtime: row.resolved_enable_realtime,
49
+ enableBulk: row.resolved_enable_bulk,
50
+ };
51
+ },
52
+ });
@@ -0,0 +1,56 @@
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 { createModuleLoader } from './create-loader';
29
+ export { createLoaderRegistry } from './registry';
30
+ // Built-in loaders
31
+ export { rlsLoader } from './rls';
32
+ export { corsLoader } from './cors';
33
+ export { databaseSettingsLoader } from './database-settings';
34
+ export { pubkeyLoader } from './pubkey';
35
+ export { webauthnLoader } from './webauthn';
36
+ export { authSettingsLoader } from './auth-settings';
37
+ /**
38
+ * Convenience: create a registry pre-loaded with all built-in loaders.
39
+ */
40
+ import { createLoaderRegistry } from './registry';
41
+ import { rlsLoader } from './rls';
42
+ import { corsLoader } from './cors';
43
+ import { databaseSettingsLoader } from './database-settings';
44
+ import { pubkeyLoader } from './pubkey';
45
+ import { webauthnLoader } from './webauthn';
46
+ import { authSettingsLoader } from './auth-settings';
47
+ export function createDefaultRegistry() {
48
+ const registry = createLoaderRegistry();
49
+ registry.register(rlsLoader);
50
+ registry.register(corsLoader);
51
+ registry.register(databaseSettingsLoader);
52
+ registry.register(pubkeyLoader);
53
+ registry.register(webauthnLoader);
54
+ registry.register(authSettingsLoader);
55
+ return registry;
56
+ }
@@ -0,0 +1,69 @@
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 { createModuleLoader } from './create-loader';
9
+ // ─── SQL ────────────────────────────────────────────────────────────────────
10
+ const PUBKEY_SETTINGS_SQL = `
11
+ SELECT
12
+ s.schema_name AS schema,
13
+ ps.crypto_network,
14
+ sign_up_fn.name AS sign_up_with_key,
15
+ sign_in_req_fn.name AS sign_in_request_challenge,
16
+ sign_in_fail_fn.name AS sign_in_record_failure,
17
+ sign_in_fn.name AS sign_in_with_challenge
18
+ FROM services_public.pubkey_settings ps
19
+ LEFT JOIN metaschema_public.schema s ON ps.schema_id = s.id
20
+ LEFT JOIN metaschema_public.function sign_up_fn ON ps.sign_up_with_key_function_id = sign_up_fn.id
21
+ LEFT JOIN metaschema_public.function sign_in_req_fn ON ps.sign_in_request_challenge_function_id = sign_in_req_fn.id
22
+ LEFT JOIN metaschema_public.function sign_in_fail_fn ON ps.sign_in_record_failure_function_id = sign_in_fail_fn.id
23
+ LEFT JOIN metaschema_public.function sign_in_fn ON ps.sign_in_with_challenge_function_id = sign_in_fn.id
24
+ WHERE ps.database_id = $1
25
+ LIMIT 1
26
+ `;
27
+ const PUBKEY_MODULE_SQL = `
28
+ SELECT data
29
+ FROM services_public.api_modules
30
+ WHERE api_id = $1 AND name = 'pubkey_challenge'
31
+ LIMIT 1
32
+ `;
33
+ // ─── Transforms ─────────────────────────────────────────────────────────────
34
+ function fromRow(row) {
35
+ if (!row?.schema || !row?.sign_up_with_key)
36
+ return undefined;
37
+ return {
38
+ schema: row.schema,
39
+ cryptoNetwork: row.crypto_network,
40
+ signUpWithKey: row.sign_up_with_key,
41
+ signInRequestChallenge: row.sign_in_request_challenge,
42
+ signInRecordFailure: row.sign_in_record_failure,
43
+ signInWithChallenge: row.sign_in_with_challenge,
44
+ };
45
+ }
46
+ // ─── Loader ─────────────────────────────────────────────────────────────────
47
+ export const pubkeyLoader = createModuleLoader({
48
+ name: 'pubkeyChallengeSettings',
49
+ ttlMs: 5 * 60_000,
50
+ async resolve(ctx) {
51
+ const { servicesPool, databaseId, apiId } = ctx;
52
+ // Try new pubkey_settings table first
53
+ try {
54
+ const result = await servicesPool.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 legacy api_modules
63
+ if (apiId) {
64
+ const result = await servicesPool.query(PUBKEY_MODULE_SQL, [apiId]);
65
+ return fromRow(result.rows[0]?.data ?? null);
66
+ }
67
+ return undefined;
68
+ },
69
+ });
@@ -0,0 +1,69 @@
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 { Logger } from '@pgpmjs/logger';
16
+ const log = new Logger('loader-registry');
17
+ export function createLoaderRegistry() {
18
+ const loaders = new Map();
19
+ return {
20
+ register(loader) {
21
+ if (loaders.has(loader.name)) {
22
+ throw new Error(`Loader "${loader.name}" is already registered`);
23
+ }
24
+ loaders.set(loader.name, loader);
25
+ log.debug(`Registered loader: ${loader.name}`);
26
+ },
27
+ async resolve(name, ctx) {
28
+ const loader = loaders.get(name);
29
+ if (!loader) {
30
+ log.debug(`Loader "${name}" not registered, returning undefined`);
31
+ return undefined;
32
+ }
33
+ return loader.resolve(ctx);
34
+ },
35
+ async resolveAll(ctx) {
36
+ if (loaders.size === 0)
37
+ return {};
38
+ const entries = Array.from(loaders.entries());
39
+ const results = await Promise.all(entries.map(async ([name, loader]) => {
40
+ const value = await loader.resolve(ctx);
41
+ return [name, value];
42
+ }));
43
+ const modules = {};
44
+ for (const [name, value] of results) {
45
+ if (value !== undefined) {
46
+ modules[name] = value;
47
+ }
48
+ }
49
+ return modules;
50
+ },
51
+ get(name) {
52
+ return loaders.get(name);
53
+ },
54
+ has(name) {
55
+ return loaders.has(name);
56
+ },
57
+ invalidate(databaseId) {
58
+ for (const loader of loaders.values()) {
59
+ loader.invalidate(databaseId);
60
+ }
61
+ log.debug(databaseId
62
+ ? `Invalidated all loaders for databaseId=${databaseId}`
63
+ : 'Invalidated all loaders');
64
+ },
65
+ get names() {
66
+ return Array.from(loaders.keys());
67
+ },
68
+ };
69
+ }
@@ -0,0 +1,93 @@
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 { createModuleLoader } from './create-loader';
9
+ // ─── SQL ────────────────────────────────────────────────────────────────────
10
+ const RLS_SETTINGS_SQL = `
11
+ SELECT
12
+ auth_schema.schema_name AS authenticate_schema,
13
+ role_schema.schema_name AS role_schema,
14
+ auth_fn.name AS authenticate,
15
+ auth_strict_fn.name AS authenticate_strict,
16
+ role_fn.name AS current_role,
17
+ role_id_fn.name AS current_role_id,
18
+ ua_fn.name AS current_user_agent,
19
+ ip_fn.name AS current_ip_address
20
+ FROM services_public.rls_settings rs
21
+ LEFT JOIN metaschema_public.schema auth_schema ON rs.authenticate_schema_id = auth_schema.id
22
+ LEFT JOIN metaschema_public.schema role_schema ON rs.role_schema_id = role_schema.id
23
+ LEFT JOIN metaschema_public.function auth_fn ON rs.authenticate_function_id = auth_fn.id
24
+ LEFT JOIN metaschema_public.function auth_strict_fn ON rs.authenticate_strict_function_id = auth_strict_fn.id
25
+ LEFT JOIN metaschema_public.function role_fn ON rs.current_role_function_id = role_fn.id
26
+ LEFT JOIN metaschema_public.function role_id_fn ON rs.current_role_id_function_id = role_id_fn.id
27
+ LEFT JOIN metaschema_public.function ua_fn ON rs.current_user_agent_function_id = ua_fn.id
28
+ LEFT JOIN metaschema_public.function ip_fn ON rs.current_ip_address_function_id = ip_fn.id
29
+ WHERE rs.database_id = $1
30
+ LIMIT 1
31
+ `;
32
+ const RLS_MODULE_SQL = `
33
+ SELECT data
34
+ FROM services_public.api_modules
35
+ WHERE api_id = $1 AND name = 'rls_module'
36
+ LIMIT 1
37
+ `;
38
+ // ─── Transforms ─────────────────────────────────────────────────────────────
39
+ function fromSettings(row) {
40
+ if (!row)
41
+ return undefined;
42
+ if (!row.authenticate || !row.authenticate_schema)
43
+ return undefined;
44
+ return {
45
+ authenticate: row.authenticate,
46
+ authenticateStrict: row.authenticate_strict,
47
+ privateSchema: { schemaName: row.authenticate_schema },
48
+ publicSchema: { schemaName: row.role_schema },
49
+ currentRole: row.current_role,
50
+ currentRoleId: row.current_role_id,
51
+ currentIpAddress: row.current_ip_address,
52
+ currentUserAgent: row.current_user_agent,
53
+ };
54
+ }
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
+ // ─── Loader ─────────────────────────────────────────────────────────────────
71
+ export const rlsLoader = createModuleLoader({
72
+ name: 'rlsModule',
73
+ ttlMs: 5 * 60_000,
74
+ async resolve(ctx) {
75
+ const { servicesPool, databaseId, apiId } = ctx;
76
+ // Try new rls_settings table first
77
+ try {
78
+ const result = await servicesPool.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 legacy api_modules
87
+ if (apiId) {
88
+ const result = await servicesPool.query(RLS_MODULE_SQL, [apiId]);
89
+ return fromModule(result.rows[0] ?? null);
90
+ }
91
+ return undefined;
92
+ },
93
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Module Loader Types
3
+ *
4
+ * A ModuleLoader is a per-database cached lookup that resolves config
5
+ * from the services DB or tenant DB. Each loader owns its own LRU cache
6
+ * keyed by databaseId, with independent TTL and eviction.
7
+ *
8
+ * Loaders are registered in a LoaderRegistry and resolved in parallel
9
+ * during context building. The result is a typed modules map on
10
+ * `req.constructive.modules`.
11
+ */
12
+ export {};
@@ -0,0 +1,54 @@
1
+ /**
2
+ * WebAuthn Settings Loader
3
+ *
4
+ * Resolves WebAuthn/passkey configuration for a database — RP identity,
5
+ * schema references, attestation policy, and challenge expiry.
6
+ */
7
+ import { createModuleLoader } from './create-loader';
8
+ // ─── SQL ────────────────────────────────────────────────────────────────────
9
+ const WEBAUTHN_SETTINGS_SQL = `
10
+ SELECT
11
+ s.schema_name AS schema,
12
+ cred_s.schema_name AS credentials_schema,
13
+ sess_s.schema_name AS sessions_schema,
14
+ sec_s.schema_name AS session_secrets_schema,
15
+ ws.rp_id,
16
+ ws.rp_name,
17
+ ws.origin_allowlist,
18
+ ws.attestation_type,
19
+ ws.require_user_verification,
20
+ ws.resident_key,
21
+ ws.challenge_expiry_seconds
22
+ FROM services_public.webauthn_settings ws
23
+ LEFT JOIN metaschema_public.schema s ON ws.schema_id = s.id
24
+ LEFT JOIN metaschema_public.schema cred_s ON ws.credentials_schema_id = cred_s.id
25
+ LEFT JOIN metaschema_public.schema sess_s ON ws.sessions_schema_id = sess_s.id
26
+ LEFT JOIN metaschema_public.schema sec_s ON ws.session_secrets_schema_id = sec_s.id
27
+ WHERE ws.database_id = $1
28
+ LIMIT 1
29
+ `;
30
+ // ─── Loader ─────────────────────────────────────────────────────────────────
31
+ export const webauthnLoader = createModuleLoader({
32
+ name: 'webauthnSettings',
33
+ ttlMs: 5 * 60_000,
34
+ async resolve(ctx) {
35
+ const { servicesPool, databaseId } = ctx;
36
+ const result = await servicesPool.query(WEBAUTHN_SETTINGS_SQL, [databaseId]);
37
+ const row = result.rows[0];
38
+ if (!row?.schema)
39
+ return undefined;
40
+ return {
41
+ schema: row.schema,
42
+ credentialsSchema: row.credentials_schema,
43
+ sessionsSchema: row.sessions_schema,
44
+ sessionSecretsSchema: row.session_secrets_schema,
45
+ rpId: row.rp_id,
46
+ rpName: row.rp_name,
47
+ originAllowlist: row.origin_allowlist,
48
+ attestationType: row.attestation_type,
49
+ requireUserVerification: row.require_user_verification,
50
+ residentKey: row.resident_key,
51
+ challengeExpirySeconds: row.challenge_expiry_seconds,
52
+ };
53
+ },
54
+ });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * pg-client — Re-exports withPgClient from pg-query-context
3
+ *
4
+ * This module exists for backwards compatibility. The implementation
5
+ * lives in pg-query-context which provides both the single-query API
6
+ * and the callback-based withPgClient API.
7
+ */
8
+ export { withPgClient } from 'pg-query-context';
@@ -0,0 +1,44 @@
1
+ /**
2
+ * pg-settings — Build pgSettings from resolved API + auth token
3
+ *
4
+ * pgSettings are key-value pairs passed to PostgreSQL via SET LOCAL
5
+ * within each transaction. They carry the JWT claims, role, database_id,
6
+ * and request_id so that RLS policies and SQL functions can reference
7
+ * the current user context via `current_setting('jwt.claims.user_id')`.
8
+ *
9
+ * This module extracts the pgSettings construction so it's reusable
10
+ * across the PostGraphile server, LLM sidecar, or any Express service.
11
+ */
12
+ /**
13
+ * Build pgSettings from the resolved API + auth token.
14
+ *
15
+ * These settings are applied via SET LOCAL in each transaction,
16
+ * making them available to RLS policies and SQL functions.
17
+ */
18
+ export function buildPgSettings(input) {
19
+ const { api, token, requestId, clientIp } = input;
20
+ const settings = {};
21
+ // Role: from token (authenticated) or api (anonymous fallback)
22
+ if (token?.user_id) {
23
+ settings['role'] = api.roleName || 'authenticated';
24
+ settings['jwt.claims.user_id'] = token.user_id;
25
+ }
26
+ else {
27
+ settings['role'] = api.anonRole || 'anonymous';
28
+ }
29
+ // Session claims
30
+ if (token?.session_id) {
31
+ settings['jwt.claims.session_id'] = token.session_id;
32
+ }
33
+ // Database context
34
+ if (api.databaseId) {
35
+ settings['jwt.claims.database_id'] = api.databaseId;
36
+ }
37
+ // Distributed tracing
38
+ settings['request.id'] = requestId;
39
+ // Client metadata (for audit functions)
40
+ if (clientIp) {
41
+ settings['jwt.claims.ip_address'] = clientIp;
42
+ }
43
+ return settings;
44
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * request-id — Per-request correlation ID middleware
3
+ *
4
+ * Generates a UUID v4 request ID (or uses an incoming X-Request-Id header)
5
+ * and attaches it to `req.requestId`. This ID propagates through pgSettings
6
+ * into billing ledger entries and inference logs for distributed tracing.
7
+ */
8
+ import { randomUUID } from 'crypto';
9
+ /**
10
+ * Middleware that ensures every request has a correlation ID.
11
+ *
12
+ * Priority: `X-Request-Id` header → new UUID v4
13
+ */
14
+ export function requestIdMiddleware() {
15
+ return (req, _res, next) => {
16
+ req.requestId = (req.get('X-Request-Id') || randomUUID());
17
+ next();
18
+ };
19
+ }
package/esm/types.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Shared types for Constructive Express middleware.
3
+ *
4
+ * These types describe the resolved API structure, RLS module, auth settings,
5
+ * database feature flags, and other config that middleware attaches to the
6
+ * Express request. Extracted from graphql/server so any Express-based service
7
+ * (PostGraphile, LLM sidecar, etc.) can share the same context.
8
+ */
9
+ export {};
package/index.d.ts ADDED
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @constructive-io/express-context
3
+ *
4
+ * Extractable Express middleware for Constructive tenant context.
5
+ *
6
+ * Provides:
7
+ * - Shared types (ApiStructure, RlsModule, AuthSettings, etc.)
8
+ * - pgSettings builder (role, JWT claims, request_id, database_id)
9
+ * - withPgClient (tenant-scoped RLS transaction helper)
10
+ * - requestId middleware (UUID correlation ID)
11
+ * - Context middleware (composes all of the above into req.constructive)
12
+ * - Module loaders (pluggable per-database cached lookups)
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * import {
17
+ * createContextMiddleware,
18
+ * requestIdMiddleware,
19
+ * createDefaultRegistry,
20
+ * } from '@constructive-io/express-context';
21
+ *
22
+ * const loaders = createDefaultRegistry();
23
+ *
24
+ * app.use(requestIdMiddleware());
25
+ * app.use(apiMiddleware); // sets req.api (your domain resolver)
26
+ * app.use(authMiddleware); // sets req.token (your JWT verifier)
27
+ * app.use(createContextMiddleware({ loaders })); // builds req.constructive
28
+ *
29
+ * app.post('/v1/chat', async (req, res) => {
30
+ * const ctx = req.constructive;
31
+ * const rls = await ctx.useModule('rlsModule'); // only fires if not cached
32
+ * const auth = await ctx.useModule('authSettings'); // only fires if not cached
33
+ * // webauthnSettings loader never fires if nobody asks for it
34
+ * });
35
+ * ```
36
+ */
37
+ export type { ApiConfigResult, ApiError, ApiModule, ApiStructure, AuthSettings, ConstructiveAPIToken, ConstructiveContext, CorsModuleData, DatabaseSettings, GenericModuleData, PublicKeyChallengeData, PubkeyChallengeSettings, BuiltinModuleMap, RlsModule, WebauthnSettings, WithPgClient, } from './types';
38
+ export type { PgSettingsInput } from './pg-settings';
39
+ export { buildPgSettings } from './pg-settings';
40
+ export { withPgClient } from './pg-client';
41
+ export { requestIdMiddleware } from './request-id';
42
+ export type { ContextMiddlewareOptions } from './context';
43
+ export { buildContext, createContextMiddleware } from './context';
44
+ export type { CreateLoaderOptions, LoaderContext, LoaderRegistry, ModuleLoader, } from './loaders';
45
+ export { authSettingsLoader, corsLoader, createDefaultRegistry, createLoaderRegistry, createModuleLoader, databaseSettingsLoader, pubkeyLoader, rlsLoader, webauthnLoader, } from './loaders';
46
+ import './types';
package/index.js ADDED
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ /**
3
+ * @constructive-io/express-context
4
+ *
5
+ * Extractable Express middleware for Constructive tenant context.
6
+ *
7
+ * Provides:
8
+ * - Shared types (ApiStructure, RlsModule, AuthSettings, etc.)
9
+ * - pgSettings builder (role, JWT claims, request_id, database_id)
10
+ * - withPgClient (tenant-scoped RLS transaction helper)
11
+ * - requestId middleware (UUID correlation ID)
12
+ * - Context middleware (composes all of the above into req.constructive)
13
+ * - Module loaders (pluggable per-database cached lookups)
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import {
18
+ * createContextMiddleware,
19
+ * requestIdMiddleware,
20
+ * createDefaultRegistry,
21
+ * } from '@constructive-io/express-context';
22
+ *
23
+ * const loaders = createDefaultRegistry();
24
+ *
25
+ * app.use(requestIdMiddleware());
26
+ * app.use(apiMiddleware); // sets req.api (your domain resolver)
27
+ * app.use(authMiddleware); // sets req.token (your JWT verifier)
28
+ * app.use(createContextMiddleware({ loaders })); // builds req.constructive
29
+ *
30
+ * app.post('/v1/chat', async (req, res) => {
31
+ * const ctx = req.constructive;
32
+ * const rls = await ctx.useModule('rlsModule'); // only fires if not cached
33
+ * const auth = await ctx.useModule('authSettings'); // only fires if not cached
34
+ * // webauthnSettings loader never fires if nobody asks for it
35
+ * });
36
+ * ```
37
+ */
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.webauthnLoader = exports.rlsLoader = exports.pubkeyLoader = exports.databaseSettingsLoader = exports.createModuleLoader = exports.createLoaderRegistry = exports.createDefaultRegistry = exports.corsLoader = exports.authSettingsLoader = exports.createContextMiddleware = exports.buildContext = exports.requestIdMiddleware = exports.withPgClient = exports.buildPgSettings = void 0;
40
+ var pg_settings_1 = require("./pg-settings");
41
+ Object.defineProperty(exports, "buildPgSettings", { enumerable: true, get: function () { return pg_settings_1.buildPgSettings; } });
42
+ // withPgClient helper
43
+ var pg_client_1 = require("./pg-client");
44
+ Object.defineProperty(exports, "withPgClient", { enumerable: true, get: function () { return pg_client_1.withPgClient; } });
45
+ // Request ID middleware
46
+ var request_id_1 = require("./request-id");
47
+ Object.defineProperty(exports, "requestIdMiddleware", { enumerable: true, get: function () { return request_id_1.requestIdMiddleware; } });
48
+ var context_1 = require("./context");
49
+ Object.defineProperty(exports, "buildContext", { enumerable: true, get: function () { return context_1.buildContext; } });
50
+ Object.defineProperty(exports, "createContextMiddleware", { enumerable: true, get: function () { return context_1.createContextMiddleware; } });
51
+ var loaders_1 = require("./loaders");
52
+ Object.defineProperty(exports, "authSettingsLoader", { enumerable: true, get: function () { return loaders_1.authSettingsLoader; } });
53
+ Object.defineProperty(exports, "corsLoader", { enumerable: true, get: function () { return loaders_1.corsLoader; } });
54
+ Object.defineProperty(exports, "createDefaultRegistry", { enumerable: true, get: function () { return loaders_1.createDefaultRegistry; } });
55
+ Object.defineProperty(exports, "createLoaderRegistry", { enumerable: true, get: function () { return loaders_1.createLoaderRegistry; } });
56
+ Object.defineProperty(exports, "createModuleLoader", { enumerable: true, get: function () { return loaders_1.createModuleLoader; } });
57
+ Object.defineProperty(exports, "databaseSettingsLoader", { enumerable: true, get: function () { return loaders_1.databaseSettingsLoader; } });
58
+ Object.defineProperty(exports, "pubkeyLoader", { enumerable: true, get: function () { return loaders_1.pubkeyLoader; } });
59
+ Object.defineProperty(exports, "rlsLoader", { enumerable: true, get: function () { return loaders_1.rlsLoader; } });
60
+ Object.defineProperty(exports, "webauthnLoader", { enumerable: true, get: function () { return loaders_1.webauthnLoader; } });
61
+ // Side-effect: Express type augmentation
62
+ require("./types");
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Auth Settings Loader (Tier 2 — tenant DB)
3
+ *
4
+ * Two-step discovery:
5
+ * 1. Query metaschema_modules_public.sessions_module to find the
6
+ * schema name and table name for auth settings
7
+ * 2. Query the discovered <schema>.<table> in the tenant DB
8
+ *
9
+ * This is the pattern for any module whose config lives in the tenant
10
+ * database rather than the services database.
11
+ */
12
+ import type { AuthSettings } from '../types';
13
+ import type { ModuleLoader } from './types';
14
+ export declare const authSettingsLoader: ModuleLoader<AuthSettings>;