@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.
- package/LICENSE +23 -0
- package/README.md +95 -0
- package/context.d.ts +61 -0
- package/context.js +119 -0
- package/esm/context.js +115 -0
- package/esm/index.js +45 -0
- package/esm/loaders/auth-settings.js +62 -0
- package/esm/loaders/cors.js +55 -0
- package/esm/loaders/create-loader.js +53 -0
- package/esm/loaders/database-settings.js +52 -0
- package/esm/loaders/index.js +56 -0
- package/esm/loaders/pubkey.js +69 -0
- package/esm/loaders/registry.js +69 -0
- package/esm/loaders/rls.js +93 -0
- package/esm/loaders/types.js +12 -0
- package/esm/loaders/webauthn.js +54 -0
- package/esm/pg-client.js +8 -0
- package/esm/pg-settings.js +44 -0
- package/esm/request-id.js +19 -0
- package/esm/types.js +9 -0
- package/index.d.ts +46 -0
- package/index.js +62 -0
- package/loaders/auth-settings.d.ts +14 -0
- package/loaders/auth-settings.js +65 -0
- package/loaders/cors.d.ts +9 -0
- package/loaders/cors.js +58 -0
- package/loaders/create-loader.d.ts +19 -0
- package/loaders/create-loader.js +56 -0
- package/loaders/database-settings.d.ts +10 -0
- package/loaders/database-settings.js +55 -0
- package/loaders/index.d.ts +39 -0
- package/loaders/index.js +68 -0
- package/loaders/pubkey.d.ts +10 -0
- package/loaders/pubkey.js +72 -0
- package/loaders/registry.d.ts +37 -0
- package/loaders/registry.js +72 -0
- package/loaders/rls.d.ts +10 -0
- package/loaders/rls.js +96 -0
- package/loaders/types.d.ts +43 -0
- package/loaders/types.js +13 -0
- package/loaders/webauthn.d.ts +9 -0
- package/loaders/webauthn.js +57 -0
- package/package.json +63 -0
- package/pg-client.d.ts +8 -0
- package/pg-client.js +12 -0
- package/pg-settings.d.ts +29 -0
- package/pg-settings.js +47 -0
- package/request-id.d.ts +14 -0
- package/request-id.js +22 -0
- package/types.d.ts +197 -0
- 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
|
+
}
|
package/loaders/types.js
ADDED
|
@@ -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; } });
|
package/pg-settings.d.ts
ADDED
|
@@ -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
|
+
}
|
package/request-id.d.ts
ADDED
|
@@ -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 });
|