@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
package/loaders/rls.js ADDED
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ /**
3
+ * RLS Module Loader
4
+ *
5
+ * Resolves RLS authentication function names and schema references for
6
+ * a given database. Tries the new rls_settings table first, falls back
7
+ * to the legacy api_modules approach.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.rlsLoader = void 0;
11
+ const create_loader_1 = require("./create-loader");
12
+ // ─── SQL ────────────────────────────────────────────────────────────────────
13
+ const RLS_SETTINGS_SQL = `
14
+ SELECT
15
+ auth_schema.schema_name AS authenticate_schema,
16
+ role_schema.schema_name AS role_schema,
17
+ auth_fn.name AS authenticate,
18
+ auth_strict_fn.name AS authenticate_strict,
19
+ role_fn.name AS current_role,
20
+ role_id_fn.name AS current_role_id,
21
+ ua_fn.name AS current_user_agent,
22
+ ip_fn.name AS current_ip_address
23
+ FROM services_public.rls_settings rs
24
+ LEFT JOIN metaschema_public.schema auth_schema ON rs.authenticate_schema_id = auth_schema.id
25
+ LEFT JOIN metaschema_public.schema role_schema ON rs.role_schema_id = role_schema.id
26
+ LEFT JOIN metaschema_public.function auth_fn ON rs.authenticate_function_id = auth_fn.id
27
+ LEFT JOIN metaschema_public.function auth_strict_fn ON rs.authenticate_strict_function_id = auth_strict_fn.id
28
+ LEFT JOIN metaschema_public.function role_fn ON rs.current_role_function_id = role_fn.id
29
+ LEFT JOIN metaschema_public.function role_id_fn ON rs.current_role_id_function_id = role_id_fn.id
30
+ LEFT JOIN metaschema_public.function ua_fn ON rs.current_user_agent_function_id = ua_fn.id
31
+ LEFT JOIN metaschema_public.function ip_fn ON rs.current_ip_address_function_id = ip_fn.id
32
+ WHERE rs.database_id = $1
33
+ LIMIT 1
34
+ `;
35
+ const RLS_MODULE_SQL = `
36
+ SELECT data
37
+ FROM services_public.api_modules
38
+ WHERE api_id = $1 AND name = 'rls_module'
39
+ LIMIT 1
40
+ `;
41
+ // ─── Transforms ─────────────────────────────────────────────────────────────
42
+ function fromSettings(row) {
43
+ if (!row)
44
+ return undefined;
45
+ if (!row.authenticate || !row.authenticate_schema)
46
+ return undefined;
47
+ return {
48
+ authenticate: row.authenticate,
49
+ authenticateStrict: row.authenticate_strict,
50
+ privateSchema: { schemaName: row.authenticate_schema },
51
+ publicSchema: { schemaName: row.role_schema },
52
+ currentRole: row.current_role,
53
+ currentRoleId: row.current_role_id,
54
+ currentIpAddress: row.current_ip_address,
55
+ currentUserAgent: row.current_user_agent,
56
+ };
57
+ }
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
+ // ─── Loader ─────────────────────────────────────────────────────────────────
74
+ exports.rlsLoader = (0, create_loader_1.createModuleLoader)({
75
+ name: 'rlsModule',
76
+ ttlMs: 5 * 60_000,
77
+ async resolve(ctx) {
78
+ const { servicesPool, databaseId, apiId } = ctx;
79
+ // Try new rls_settings table first
80
+ try {
81
+ const result = await servicesPool.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 legacy api_modules
90
+ if (apiId) {
91
+ const result = await servicesPool.query(RLS_MODULE_SQL, [apiId]);
92
+ return fromModule(result.rows[0] ?? null);
93
+ }
94
+ return undefined;
95
+ },
96
+ });
@@ -0,0 +1,43 @@
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
+ import type { Pool } from 'pg';
13
+ /**
14
+ * Context passed to every loader's resolve function.
15
+ * Provides both pool references so the loader can query whichever
16
+ * database tier it needs.
17
+ */
18
+ export interface LoaderContext {
19
+ /** Services database pool (for services_public.* lookups) */
20
+ servicesPool: Pool;
21
+ /** Tenant database pool (for metaschema_modules_public.* lookups) */
22
+ tenantPool: Pool;
23
+ /** UUID of the database being resolved */
24
+ databaseId: string;
25
+ /** UUID of the API (if resolved from domain/api-name lookup) */
26
+ apiId?: string;
27
+ /** Tenant database name */
28
+ dbname: string;
29
+ }
30
+ /**
31
+ * A single module loader. Encapsulates the SQL query, type transform,
32
+ * and per-databaseId LRU cache for one piece of per-database config.
33
+ */
34
+ export interface ModuleLoader<T = unknown> {
35
+ /** Unique name (used in log prefix and as the key in the modules map) */
36
+ readonly name: string;
37
+ /** Resolve the module config for a given database. Returns undefined if not provisioned. */
38
+ resolve(ctx: LoaderContext): Promise<T | undefined>;
39
+ /** Invalidate the cache for one database (or all databases if omitted) */
40
+ invalidate(databaseId?: string): void;
41
+ /** Current number of cached entries */
42
+ readonly cacheSize: number;
43
+ }
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ /**
3
+ * Module Loader Types
4
+ *
5
+ * A ModuleLoader is a per-database cached lookup that resolves config
6
+ * from the services DB or tenant DB. Each loader owns its own LRU cache
7
+ * keyed by databaseId, with independent TTL and eviction.
8
+ *
9
+ * Loaders are registered in a LoaderRegistry and resolved in parallel
10
+ * during context building. The result is a typed modules map on
11
+ * `req.constructive.modules`.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,9 @@
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 type { WebauthnSettings } from '../types';
8
+ import type { ModuleLoader } from './types';
9
+ export declare const webauthnLoader: ModuleLoader<WebauthnSettings>;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ /**
3
+ * WebAuthn Settings Loader
4
+ *
5
+ * Resolves WebAuthn/passkey configuration for a database — RP identity,
6
+ * schema references, attestation policy, and challenge expiry.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.webauthnLoader = void 0;
10
+ const create_loader_1 = require("./create-loader");
11
+ // ─── SQL ────────────────────────────────────────────────────────────────────
12
+ const WEBAUTHN_SETTINGS_SQL = `
13
+ SELECT
14
+ s.schema_name AS schema,
15
+ cred_s.schema_name AS credentials_schema,
16
+ sess_s.schema_name AS sessions_schema,
17
+ sec_s.schema_name AS session_secrets_schema,
18
+ ws.rp_id,
19
+ ws.rp_name,
20
+ ws.origin_allowlist,
21
+ ws.attestation_type,
22
+ ws.require_user_verification,
23
+ ws.resident_key,
24
+ ws.challenge_expiry_seconds
25
+ FROM services_public.webauthn_settings ws
26
+ LEFT JOIN metaschema_public.schema s ON ws.schema_id = s.id
27
+ LEFT JOIN metaschema_public.schema cred_s ON ws.credentials_schema_id = cred_s.id
28
+ LEFT JOIN metaschema_public.schema sess_s ON ws.sessions_schema_id = sess_s.id
29
+ LEFT JOIN metaschema_public.schema sec_s ON ws.session_secrets_schema_id = sec_s.id
30
+ WHERE ws.database_id = $1
31
+ LIMIT 1
32
+ `;
33
+ // ─── Loader ─────────────────────────────────────────────────────────────────
34
+ exports.webauthnLoader = (0, create_loader_1.createModuleLoader)({
35
+ name: 'webauthnSettings',
36
+ ttlMs: 5 * 60_000,
37
+ async resolve(ctx) {
38
+ const { servicesPool, databaseId } = ctx;
39
+ const result = await servicesPool.query(WEBAUTHN_SETTINGS_SQL, [databaseId]);
40
+ const row = result.rows[0];
41
+ if (!row?.schema)
42
+ return undefined;
43
+ return {
44
+ schema: row.schema,
45
+ credentialsSchema: row.credentials_schema,
46
+ sessionsSchema: row.sessions_schema,
47
+ sessionSecretsSchema: row.session_secrets_schema,
48
+ rpId: row.rp_id,
49
+ rpName: row.rp_name,
50
+ originAllowlist: row.origin_allowlist,
51
+ attestationType: row.attestation_type,
52
+ requireUserVerification: row.require_user_verification,
53
+ residentKey: row.resident_key,
54
+ challengeExpirySeconds: row.challenge_expiry_seconds,
55
+ };
56
+ },
57
+ });
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@constructive-io/express-context",
3
+ "version": "0.2.0",
4
+ "description": "Extractable Express middleware for Constructive tenant context — domain resolution, JWT auth, pgSettings, and withPgClient",
5
+ "author": "Constructive <developers@constructive.io>",
6
+ "homepage": "https://github.com/constructive-io/constructive",
7
+ "license": "MIT",
8
+ "main": "index.js",
9
+ "module": "esm/index.js",
10
+ "types": "index.d.ts",
11
+ "publishConfig": {
12
+ "access": "public",
13
+ "directory": "dist"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/constructive-io/constructive"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/constructive-io/constructive/issues"
21
+ },
22
+ "scripts": {
23
+ "clean": "makage clean",
24
+ "prepack": "npm run build",
25
+ "build": "makage build",
26
+ "build:dev": "makage build --dev",
27
+ "lint": "eslint . --fix",
28
+ "test": "jest --passWithNoTests",
29
+ "test:watch": "jest --watch"
30
+ },
31
+ "dependencies": {
32
+ "@constructive-io/url-domains": "^2.16.0",
33
+ "@pgpmjs/env": "^2.24.0",
34
+ "@pgpmjs/logger": "^2.11.0",
35
+ "@pgpmjs/server-utils": "^3.12.0",
36
+ "@pgpmjs/types": "^2.28.0",
37
+ "lru-cache": "^11.2.7",
38
+ "pg": "^8.21.0",
39
+ "pg-cache": "^3.11.0",
40
+ "pg-query-context": "^2.16.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/express": "^5.0.6",
44
+ "@types/pg": "^8.20.0",
45
+ "express": "^5.2.1",
46
+ "makage": "^0.3.0"
47
+ },
48
+ "peerDependencies": {
49
+ "express": "^5.0.0"
50
+ },
51
+ "keywords": [
52
+ "constructive",
53
+ "express",
54
+ "middleware",
55
+ "context",
56
+ "tenant",
57
+ "domain",
58
+ "auth",
59
+ "jwt",
60
+ "pgSettings"
61
+ ],
62
+ "gitHead": "c0d04574f7719d92e67becb58d60791ae978c5f5"
63
+ }
package/pg-client.d.ts ADDED
@@ -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';
package/pg-client.js ADDED
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ /**
3
+ * pg-client — Re-exports withPgClient from pg-query-context
4
+ *
5
+ * This module exists for backwards compatibility. The implementation
6
+ * lives in pg-query-context which provides both the single-query API
7
+ * and the callback-based withPgClient API.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.withPgClient = void 0;
11
+ var pg_query_context_1 = require("pg-query-context");
12
+ Object.defineProperty(exports, "withPgClient", { enumerable: true, get: function () { return pg_query_context_1.withPgClient; } });
@@ -0,0 +1,29 @@
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
+ import type { ApiStructure, ConstructiveAPIToken } from './types';
13
+ export interface PgSettingsInput {
14
+ /** Resolved API config (provides role names, database_id) */
15
+ api: ApiStructure;
16
+ /** Authenticated token (null for anonymous) */
17
+ token: ConstructiveAPIToken | null;
18
+ /** Per-request correlation ID */
19
+ requestId: string;
20
+ /** Client IP address (from request-ip middleware) */
21
+ clientIp?: string;
22
+ }
23
+ /**
24
+ * Build pgSettings from the resolved API + auth token.
25
+ *
26
+ * These settings are applied via SET LOCAL in each transaction,
27
+ * making them available to RLS policies and SQL functions.
28
+ */
29
+ export declare function buildPgSettings(input: PgSettingsInput): Record<string, string>;
package/pg-settings.js ADDED
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ /**
3
+ * pg-settings — Build pgSettings from resolved API + auth token
4
+ *
5
+ * pgSettings are key-value pairs passed to PostgreSQL via SET LOCAL
6
+ * within each transaction. They carry the JWT claims, role, database_id,
7
+ * and request_id so that RLS policies and SQL functions can reference
8
+ * the current user context via `current_setting('jwt.claims.user_id')`.
9
+ *
10
+ * This module extracts the pgSettings construction so it's reusable
11
+ * across the PostGraphile server, LLM sidecar, or any Express service.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.buildPgSettings = buildPgSettings;
15
+ /**
16
+ * Build pgSettings from the resolved API + auth token.
17
+ *
18
+ * These settings are applied via SET LOCAL in each transaction,
19
+ * making them available to RLS policies and SQL functions.
20
+ */
21
+ function buildPgSettings(input) {
22
+ const { api, token, requestId, clientIp } = input;
23
+ const settings = {};
24
+ // Role: from token (authenticated) or api (anonymous fallback)
25
+ if (token?.user_id) {
26
+ settings['role'] = api.roleName || 'authenticated';
27
+ settings['jwt.claims.user_id'] = token.user_id;
28
+ }
29
+ else {
30
+ settings['role'] = api.anonRole || 'anonymous';
31
+ }
32
+ // Session claims
33
+ if (token?.session_id) {
34
+ settings['jwt.claims.session_id'] = token.session_id;
35
+ }
36
+ // Database context
37
+ if (api.databaseId) {
38
+ settings['jwt.claims.database_id'] = api.databaseId;
39
+ }
40
+ // Distributed tracing
41
+ settings['request.id'] = requestId;
42
+ // Client metadata (for audit functions)
43
+ if (clientIp) {
44
+ settings['jwt.claims.ip_address'] = clientIp;
45
+ }
46
+ return settings;
47
+ }
@@ -0,0 +1,14 @@
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 type { RequestHandler } from 'express';
9
+ /**
10
+ * Middleware that ensures every request has a correlation ID.
11
+ *
12
+ * Priority: `X-Request-Id` header → new UUID v4
13
+ */
14
+ export declare function requestIdMiddleware(): RequestHandler;
package/request-id.js ADDED
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ /**
3
+ * request-id — Per-request correlation ID middleware
4
+ *
5
+ * Generates a UUID v4 request ID (or uses an incoming X-Request-Id header)
6
+ * and attaches it to `req.requestId`. This ID propagates through pgSettings
7
+ * into billing ledger entries and inference logs for distributed tracing.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.requestIdMiddleware = requestIdMiddleware;
11
+ const crypto_1 = require("crypto");
12
+ /**
13
+ * Middleware that ensures every request has a correlation ID.
14
+ *
15
+ * Priority: `X-Request-Id` header → new UUID v4
16
+ */
17
+ function requestIdMiddleware() {
18
+ return (req, _res, next) => {
19
+ req.requestId = (req.get('X-Request-Id') || (0, crypto_1.randomUUID)());
20
+ next();
21
+ };
22
+ }
package/types.d.ts ADDED
@@ -0,0 +1,197 @@
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
+ import type { Pool, PoolClient } from 'pg';
10
+ export interface CorsModuleData {
11
+ urls: string[];
12
+ }
13
+ export interface PublicKeyChallengeData {
14
+ schema: string;
15
+ crypto_network: string;
16
+ sign_up_with_key: string;
17
+ sign_in_request_challenge: string;
18
+ sign_in_record_failure: string;
19
+ sign_in_with_challenge: string;
20
+ }
21
+ export interface GenericModuleData {
22
+ [key: string]: unknown;
23
+ }
24
+ export interface DatabaseSettings {
25
+ enableAggregates: boolean;
26
+ enablePostgis: boolean;
27
+ enableSearch: boolean;
28
+ enableDirectUploads: boolean;
29
+ enablePresignedUploads: boolean;
30
+ enableManyToMany: boolean;
31
+ enableConnectionFilter: boolean;
32
+ enableLtree: boolean;
33
+ enableLlm: boolean;
34
+ enableRealtime: boolean;
35
+ enableBulk: boolean;
36
+ }
37
+ export interface PubkeyChallengeSettings {
38
+ schema: string;
39
+ cryptoNetwork: string;
40
+ signUpWithKey: string;
41
+ signInRequestChallenge: string;
42
+ signInRecordFailure: string;
43
+ signInWithChallenge: string;
44
+ }
45
+ export interface WebauthnSettings {
46
+ schema: string;
47
+ credentialsSchema: string;
48
+ sessionsSchema: string;
49
+ sessionSecretsSchema: string;
50
+ rpId: string;
51
+ rpName: string;
52
+ originAllowlist: string[];
53
+ attestationType: string;
54
+ requireUserVerification: boolean;
55
+ residentKey: string;
56
+ challengeExpirySeconds: number;
57
+ }
58
+ export type ApiModule = {
59
+ name: 'cors';
60
+ data: CorsModuleData;
61
+ } | {
62
+ name: 'pubkey_challenge';
63
+ data: PublicKeyChallengeData;
64
+ } | {
65
+ name: string;
66
+ data?: GenericModuleData;
67
+ };
68
+ export interface RlsModule {
69
+ authenticate: string;
70
+ authenticateStrict: string;
71
+ privateSchema: {
72
+ schemaName: string;
73
+ };
74
+ publicSchema: {
75
+ schemaName: string;
76
+ };
77
+ currentRole: string;
78
+ currentRoleId: string;
79
+ currentIpAddress: string;
80
+ currentUserAgent: string;
81
+ }
82
+ export interface AuthSettings {
83
+ cookieSecure?: boolean;
84
+ cookieSamesite?: string;
85
+ cookieDomain?: string | null;
86
+ cookieHttponly?: boolean;
87
+ cookieMaxAge?: string | null;
88
+ cookiePath?: string;
89
+ rememberMeDuration?: string | null;
90
+ enableCaptcha?: boolean;
91
+ captchaSiteKey?: string | null;
92
+ }
93
+ export interface ApiStructure {
94
+ apiId?: string;
95
+ dbname: string;
96
+ anonRole: string;
97
+ roleName: string;
98
+ schema: string[];
99
+ apiModules: ApiModule[];
100
+ rlsModule?: RlsModule;
101
+ domains?: string[];
102
+ databaseId?: string;
103
+ isPublic?: boolean;
104
+ authSettings?: AuthSettings;
105
+ corsOrigins?: string[];
106
+ databaseSettings?: DatabaseSettings;
107
+ pubkeyChallengeSettings?: PubkeyChallengeSettings;
108
+ webauthnSettings?: WebauthnSettings;
109
+ }
110
+ export type ApiError = {
111
+ errorHtml: string;
112
+ };
113
+ export type ApiConfigResult = ApiStructure | ApiError | null;
114
+ export type ConstructiveAPIToken = {
115
+ id?: string;
116
+ user_id?: string;
117
+ session_id?: string;
118
+ access_level?: string;
119
+ kind?: string;
120
+ [key: string]: unknown;
121
+ };
122
+ /**
123
+ * Maps built-in loader names to their resolved types.
124
+ *
125
+ * This enables typed access via `useModule`:
126
+ * const rls = await ctx.useModule('rlsModule'); // typed as RlsModule | undefined
127
+ *
128
+ * Custom loaders (not in this map) return `unknown`.
129
+ */
130
+ export interface BuiltinModuleMap {
131
+ rlsModule: RlsModule;
132
+ corsOrigins: string[];
133
+ databaseSettings: DatabaseSettings;
134
+ authSettings: AuthSettings;
135
+ pubkeyChallengeSettings: PubkeyChallengeSettings;
136
+ webauthnSettings: WebauthnSettings;
137
+ }
138
+ /**
139
+ * Callback for executing queries within a tenant-scoped transaction
140
+ * with proper pgSettings (role, claims, request_id).
141
+ */
142
+ export type WithPgClient = <T>(fn: (client: PoolClient) => Promise<T>) => Promise<T>;
143
+ /**
144
+ * The full tenant context attached to `req.constructive` by the middleware.
145
+ * Any Express-based service can use this to interact with the tenant database.
146
+ */
147
+ export interface ConstructiveContext {
148
+ /** Resolved API structure (database, schemas, RLS, feature flags) */
149
+ api: ApiStructure;
150
+ /** Authenticated token (null for anonymous requests) */
151
+ token: ConstructiveAPIToken | null;
152
+ /** pgSettings for SET LOCAL in tenant transactions */
153
+ pgSettings: Record<string, string>;
154
+ /** Database UUID from the API resolver */
155
+ databaseId: string | null;
156
+ /** Authenticated user ID from the JWT token */
157
+ userId: string | null;
158
+ /** Per-request correlation ID for distributed tracing */
159
+ requestId: string;
160
+ /** Tenant database connection pool */
161
+ pool: Pool;
162
+ /** Execute a function within a tenant-scoped RLS transaction */
163
+ withPgClient: WithPgClient;
164
+ /**
165
+ * Resolve a per-database module on demand (lazy, cached).
166
+ *
167
+ * Only fires the SQL query on the first call per databaseId per TTL window.
168
+ * Subsequent calls return the cached result instantly.
169
+ *
170
+ * Built-in modules are typed:
171
+ * const rls = await ctx.useModule('rlsModule'); // RlsModule | undefined
172
+ * const auth = await ctx.useModule('authSettings'); // AuthSettings | undefined
173
+ *
174
+ * Custom modules return unknown:
175
+ * const custom = await ctx.useModule('myModule'); // unknown
176
+ *
177
+ * Returns undefined if:
178
+ * - No loader registry was provided to the middleware
179
+ * - The named loader isn't registered
180
+ * - The module isn't provisioned for this database
181
+ */
182
+ useModule: {
183
+ <K extends keyof BuiltinModuleMap>(name: K): Promise<BuiltinModuleMap[K] | undefined>;
184
+ (name: string): Promise<unknown>;
185
+ };
186
+ }
187
+ declare global {
188
+ namespace Express {
189
+ interface Request {
190
+ api?: ApiStructure;
191
+ clientIp?: string;
192
+ requestId?: string;
193
+ token?: ConstructiveAPIToken;
194
+ constructive?: ConstructiveContext;
195
+ }
196
+ }
197
+ }
package/types.js ADDED
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ /**
3
+ * Shared types for Constructive Express middleware.
4
+ *
5
+ * These types describe the resolved API structure, RLS module, auth settings,
6
+ * database feature flags, and other config that middleware attaches to the
7
+ * Express request. Extracted from graphql/server so any Express-based service
8
+ * (PostGraphile, LLM sidecar, etc.) can share the same context.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });