@constructive-io/graphql-server 5.30.0 → 5.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/diagnostics/debug-db-snapshot.js +0 -2
- package/esm/diagnostics/debug-db-snapshot.js +0 -2
- package/esm/middleware/api.js +2 -4
- package/esm/middleware/auth.js +0 -1
- package/esm/middleware/flush.js +1 -3
- package/esm/middleware/graphile.js +3 -7
- package/esm/refusals/recorder.js +2 -5
- package/esm/server.js +1 -4
- package/middleware/api.js +2 -4
- package/middleware/auth.js +0 -1
- package/middleware/flush.js +1 -3
- package/middleware/graphile.js +3 -7
- package/middleware/routing.d.ts +2 -2
- package/package.json +17 -17
- package/refusals/recorder.d.ts +2 -2
- package/refusals/recorder.js +2 -5
- package/server.js +1 -4
- package/control-role.d.ts +0 -8
- package/control-role.js +0 -15
- package/esm/control-role.js +0 -11
package/README.md
CHANGED
|
@@ -56,6 +56,13 @@ This starts the server with env defaults from `@constructive-io/graphql-env`.
|
|
|
56
56
|
|
|
57
57
|
## What it does
|
|
58
58
|
|
|
59
|
+
Platform deployments connect through one pgpm-provisioned `NOSUPERUSER INHERIT
|
|
60
|
+
BYPASSRLS` login with direct metadata privileges, configured only with
|
|
61
|
+
`PGUSER`/`PGPASSWORD`. Startup and routing queries use that pool login directly.
|
|
62
|
+
Requests continue to switch to `authenticated` or `anonymous`, so PostgreSQL
|
|
63
|
+
enforces their RLS policies; the login's BYPASSRLS attribute is not inherited
|
|
64
|
+
after `SET ROLE`.
|
|
65
|
+
|
|
59
66
|
Runs an Express server that wires CORS, uploads, domain parsing, auth, and PostGraphile into a single GraphQL endpoint. It serves `/graphql` and `/graphiql`, injects per-request `pgSettings`, and flushes cached schemas on demand or via database notifications. When meta API is enabled, it resolves API config (schemas, roles, modules) from the meta schema using the request host and enforces `api.isPublic`, with optional header overrides in private mode; when meta API is disabled, it serves the fixed schemas and roles from `api.exposedSchemas`, `api.anonRole`, and `api.roleName`.
|
|
60
67
|
|
|
61
68
|
## Key Features
|
|
@@ -146,7 +153,6 @@ Configuration is merged from defaults, config files, and env vars via `@construc
|
|
|
146
153
|
| `API_ROLE_NAME` | Authenticated role name | `administrator` |
|
|
147
154
|
| `API_FLUSH_TOKEN` | Bearer token required by `POST /flush`; route is not mounted when unset | empty |
|
|
148
155
|
| `API_INTROSPECTION_ROLE` | Role PostGraphile introspects as; unset means the pool's connecting role. Naming a role with fewer grants removes fields from the schema | empty |
|
|
149
|
-
| `API_CONTROL_ROLE` | Role control-plane queries (routing, API lookups, module loaders, flush, refusal log) run as via `SET LOCAL ROLE`. The login role holds no privileges, so the server refuses to start without it | required |
|
|
150
156
|
| `GRAPHQL_OBSERVABILITY_ENABLED` | Master switch for debug routes and sampler | `false` |
|
|
151
157
|
| `GRAPHQL_DEBUG_SAMPLER_ENABLED` | Enables periodic NDJSON sampling when observability is on | `true` |
|
|
152
158
|
| `GRAPHQL_DEBUG_SAMPLER_INTERVAL_MS` | Sampler interval in milliseconds | `10000` |
|
|
@@ -4,7 +4,6 @@ exports.getDebugDatabaseSnapshot = exports.closeDebugDatabasePools = void 0;
|
|
|
4
4
|
const pg_1 = require("pg");
|
|
5
5
|
const pg_cache_1 = require("pg-cache");
|
|
6
6
|
const pg_env_1 = require("pg-env");
|
|
7
|
-
const control_role_1 = require("../control-role");
|
|
8
7
|
const ACTIVE_ACTIVITY_SQL = `
|
|
9
8
|
select
|
|
10
9
|
pid,
|
|
@@ -142,7 +141,6 @@ const withDiagnosticsClient = async (opts, fn) => {
|
|
|
142
141
|
const client = await diagnosticsPool.connect();
|
|
143
142
|
try {
|
|
144
143
|
await client.query('BEGIN');
|
|
145
|
-
await client.query('SELECT set_config($1, $2, true)', ['role', (0, control_role_1.getControlRole)(opts)]);
|
|
146
144
|
await client.query(`SET LOCAL statement_timeout = '${DIAGNOSTICS_STATEMENT_TIMEOUT_MS}ms'`);
|
|
147
145
|
await client.query(`SET LOCAL lock_timeout = '${DIAGNOSTICS_LOCK_TIMEOUT_MS}ms'`);
|
|
148
146
|
const result = await fn(client);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Pool } from 'pg';
|
|
2
2
|
import { buildConnectionString, getPgPool } from 'pg-cache';
|
|
3
3
|
import { getPgEnvOptions } from 'pg-env';
|
|
4
|
-
import { getControlRole } from '../control-role';
|
|
5
4
|
const ACTIVE_ACTIVITY_SQL = `
|
|
6
5
|
select
|
|
7
6
|
pid,
|
|
@@ -139,7 +138,6 @@ const withDiagnosticsClient = async (opts, fn) => {
|
|
|
139
138
|
const client = await diagnosticsPool.connect();
|
|
140
139
|
try {
|
|
141
140
|
await client.query('BEGIN');
|
|
142
|
-
await client.query('SELECT set_config($1, $2, true)', ['role', getControlRole(opts)]);
|
|
143
141
|
await client.query(`SET LOCAL statement_timeout = '${DIAGNOSTICS_STATEMENT_TIMEOUT_MS}ms'`);
|
|
144
142
|
await client.query(`SET LOCAL lock_timeout = '${DIAGNOSTICS_LOCK_TIMEOUT_MS}ms'`);
|
|
145
143
|
const result = await fn(client);
|
package/esm/middleware/api.js
CHANGED
|
@@ -4,8 +4,6 @@ import { parseUrl } from '@constructive-io/url-domains';
|
|
|
4
4
|
import { Logger } from '@pgpmjs/logger';
|
|
5
5
|
import { svcCache } from '@pgpmjs/server-utils';
|
|
6
6
|
import { getPgPool } from 'pg-cache';
|
|
7
|
-
import { controlPlanePool } from 'pg-query-context';
|
|
8
|
-
import { getControlRole } from '../control-role';
|
|
9
7
|
import errorPage50x from '../errors/50x';
|
|
10
8
|
import errorPage404Message from '../errors/404-message';
|
|
11
9
|
import { getRoutingSchema, isValidSchemaName, requireApiRole, resolveRoute, routeToApiStructure } from './routing';
|
|
@@ -50,7 +48,7 @@ const buildLoaderContext = (routingPool, opts, row) => {
|
|
|
50
48
|
return {
|
|
51
49
|
routingPool,
|
|
52
50
|
routingSchema: getRoutingSchema(opts),
|
|
53
|
-
tenantPool:
|
|
51
|
+
tenantPool: getPgPool({ ...opts.pg, database: dbname }),
|
|
54
52
|
databaseId: row.database_id,
|
|
55
53
|
apiId: row.api_id,
|
|
56
54
|
dbname
|
|
@@ -262,7 +260,7 @@ const resolveScopedRoute = async (ctx) => {
|
|
|
262
260
|
// Main Resolution Function
|
|
263
261
|
// =============================================================================
|
|
264
262
|
export const getApiConfig = async (opts, req) => {
|
|
265
|
-
const pool =
|
|
263
|
+
const pool = getPgPool(opts.pg);
|
|
266
264
|
const { domain, subdomains } = getUrlDomains(req);
|
|
267
265
|
const subdomain = getSubdomain(subdomains);
|
|
268
266
|
const cacheKey = getSvcKey(opts, req);
|
package/esm/middleware/auth.js
CHANGED
|
@@ -62,7 +62,6 @@ export const createAuthenticateMiddleware = (opts) => {
|
|
|
62
62
|
if (effectiveToken) {
|
|
63
63
|
log.info(`[auth] Processing ${tokenSource} authentication`);
|
|
64
64
|
const context = {
|
|
65
|
-
role: api.anonRole,
|
|
66
65
|
'jwt.claims.ip_address': req.clientIp,
|
|
67
66
|
};
|
|
68
67
|
if (req.get('origin')) {
|
package/esm/middleware/flush.js
CHANGED
|
@@ -4,8 +4,6 @@ import { svcCache } from '@pgpmjs/server-utils';
|
|
|
4
4
|
import { createHash, timingSafeEqual } from 'crypto';
|
|
5
5
|
import { graphileCache } from 'graphile-cache';
|
|
6
6
|
import { getPgPool } from 'pg-cache';
|
|
7
|
-
import { controlPlanePool } from 'pg-query-context';
|
|
8
|
-
import { getControlRole } from '../control-role';
|
|
9
7
|
import { getRoutingSchema, isValidSchemaName } from './routing';
|
|
10
8
|
const log = new Logger('flush');
|
|
11
9
|
const bearerToken = (req) => {
|
|
@@ -49,7 +47,7 @@ export const createFlushMiddleware = (opts) => {
|
|
|
49
47
|
};
|
|
50
48
|
};
|
|
51
49
|
export const flushService = async (opts, databaseId) => {
|
|
52
|
-
const pgPool =
|
|
50
|
+
const pgPool = getPgPool(opts.pg);
|
|
53
51
|
log.info('flushing db ' + databaseId);
|
|
54
52
|
const api = new RegExp(`^api:${databaseId}:.*`);
|
|
55
53
|
const schemata = new RegExp(`^schemata:${databaseId}:.*`);
|
|
@@ -7,8 +7,6 @@ import { createFunctionBindingsPlugin } from 'graphile-function-bindings';
|
|
|
7
7
|
import { createConstructivePreset, createGrafastCacheLimitsPreset } from 'graphile-settings';
|
|
8
8
|
import { getPgPool } from 'pg-cache';
|
|
9
9
|
import { getPgEnvOptions } from 'pg-env';
|
|
10
|
-
import { controlPlanePool } from 'pg-query-context';
|
|
11
|
-
import { getControlRole } from '../control-role';
|
|
12
10
|
import { isGraphqlObservabilityEnabled } from '../diagnostics/observability';
|
|
13
11
|
import { HandlerCreationError } from '../errors/api-errors';
|
|
14
12
|
import { respondWithGraphQLError } from '../errors/graphql-response';
|
|
@@ -58,7 +56,7 @@ const reqLabel = (req) => (req.requestId ? `[${req.requestId}]` : '[req]');
|
|
|
58
56
|
* plugin preset. Without settings the default preset is used
|
|
59
57
|
* (everything on except aggregates).
|
|
60
58
|
*/
|
|
61
|
-
const buildPreset = async (pool, schemas, anonRole, roleName, introspectionRole,
|
|
59
|
+
const buildPreset = async (pool, schemas, anonRole, roleName, introspectionRole, graphileOptions, databaseSettings, apiId, compute, webauthn) => {
|
|
62
60
|
const introspection = await makeIntrospectionWiring(pool, schemas, graphileOptions, undefined, introspectionRole);
|
|
63
61
|
const configuredPreset = graphileOptions?.preset ?? {};
|
|
64
62
|
const grafastCachePreset = createGrafastCacheLimitsPreset(graphileOptions?.grafastCache);
|
|
@@ -89,7 +87,6 @@ const buildPreset = async (pool, schemas, anonRole, roleName, introspectionRole,
|
|
|
89
87
|
? [
|
|
90
88
|
createFunctionBindingsPlugin({
|
|
91
89
|
apiId,
|
|
92
|
-
role: controlRole,
|
|
93
90
|
modules: compute.modules.map((m) => ({
|
|
94
91
|
computeSchema: m.schemaName,
|
|
95
92
|
bindingsTable: m.bindingsTableName,
|
|
@@ -314,7 +311,7 @@ export const graphile = (opts) => {
|
|
|
314
311
|
// concurrent requests coalesce while optional plugin modules load.
|
|
315
312
|
const creationPromise = (async () => {
|
|
316
313
|
const compute = api.apiId ? await req.constructive?.useModule('compute') : undefined;
|
|
317
|
-
const preset = await buildPreset(pool, schema || [], anonRole, roleName, opts.api?.introspectionRole,
|
|
314
|
+
const preset = await buildPreset(pool, schema || [], anonRole, roleName, opts.api?.introspectionRole, opts.graphile, api.databaseSettings, api.apiId, compute, Boolean(api.webauthnSettings));
|
|
318
315
|
return observeGraphileBuild({
|
|
319
316
|
cacheKey: key,
|
|
320
317
|
serviceKey: key,
|
|
@@ -322,8 +319,7 @@ export const graphile = (opts) => {
|
|
|
322
319
|
}, () => createGraphileInstance({
|
|
323
320
|
preset,
|
|
324
321
|
cacheKey: key,
|
|
325
|
-
enableRealtime: api.databaseSettings?.enableRealtime
|
|
326
|
-
realtimePool: controlPlanePool(pool, getControlRole(opts))
|
|
322
|
+
enableRealtime: api.databaseSettings?.enableRealtime
|
|
327
323
|
}), { enabled: observabilityEnabled });
|
|
328
324
|
})();
|
|
329
325
|
creating.set(key, creationPromise);
|
package/esm/refusals/recorder.js
CHANGED
|
@@ -2,8 +2,6 @@ import '../middleware/types'; // for Request type
|
|
|
2
2
|
import { clientIpFrom, createRecordRefusalsSink, RefusalRecorder, trustedProxyHops } from '@constructive-io/express-context';
|
|
3
3
|
import { Logger } from '@pgpmjs/logger';
|
|
4
4
|
import { getPgPool } from 'pg-cache';
|
|
5
|
-
import { controlPlanePool } from 'pg-query-context';
|
|
6
|
-
import { getControlRole } from '../control-role';
|
|
7
5
|
const log = new Logger('refusals');
|
|
8
6
|
/**
|
|
9
7
|
* refusals/recorder — the GraphQL lane's `RefusalRecorder`.
|
|
@@ -121,17 +119,16 @@ export const platformFlushClaims = (identity) => ({
|
|
|
121
119
|
*/
|
|
122
120
|
export const createPlatformRefusalRecorder = (opts, recorderOpts = {}) => {
|
|
123
121
|
const pool = getPgPool(opts.pg);
|
|
124
|
-
const role = getControlRole(opts);
|
|
125
122
|
let cached = null;
|
|
126
123
|
const claims = async () => {
|
|
127
124
|
if (cached)
|
|
128
125
|
return cached;
|
|
129
|
-
cached = platformFlushClaims(await resolvePlatformFlushIdentity(
|
|
126
|
+
cached = platformFlushClaims(await resolvePlatformFlushIdentity(pool, recorderOpts.principalName));
|
|
130
127
|
log.info(`[refusals] flushing as '${recorderOpts.principalName ?? PLATFORM_BOOTSTRAP_PRINCIPAL}'`);
|
|
131
128
|
return cached;
|
|
132
129
|
};
|
|
133
130
|
return new RefusalRecorder({
|
|
134
|
-
sink: createRecordRefusalsSink({ pool,
|
|
131
|
+
sink: createRecordRefusalsSink({ pool, claims }),
|
|
135
132
|
intervalMs: recorderOpts.intervalMs,
|
|
136
133
|
jitterMs: recorderOpts.jitterMs,
|
|
137
134
|
maxKeys: recorderOpts.maxKeys
|
package/esm/server.js
CHANGED
|
@@ -11,7 +11,6 @@ import graphqlUpload from 'graphql-upload';
|
|
|
11
11
|
import { getPgPool } from 'pg-cache';
|
|
12
12
|
import requestIp from 'request-ip';
|
|
13
13
|
import { createAgenticRouter } from './agentic';
|
|
14
|
-
import { getControlRole } from './control-role';
|
|
15
14
|
import { closeDebugDatabasePools } from './diagnostics/debug-db-snapshot';
|
|
16
15
|
import { startDebugSampler } from './diagnostics/debug-sampler';
|
|
17
16
|
import { isDevelopmentObservabilityMode, isGraphqlObservabilityEnabled, isGraphqlObservabilityRequested, isLoopbackHost } from './diagnostics/observability';
|
|
@@ -77,7 +76,6 @@ class Server {
|
|
|
77
76
|
constructor(opts) {
|
|
78
77
|
this.opts = getEnvOptions(opts);
|
|
79
78
|
const effectiveOpts = this.opts;
|
|
80
|
-
const controlRole = getControlRole(effectiveOpts);
|
|
81
79
|
const observabilityRequested = isGraphqlObservabilityRequested();
|
|
82
80
|
const observabilityEnabled = isGraphqlObservabilityEnabled(effectiveOpts.server?.host);
|
|
83
81
|
const app = express();
|
|
@@ -147,8 +145,7 @@ class Server {
|
|
|
147
145
|
app.use(createContextMiddleware({
|
|
148
146
|
pg: effectiveOpts.pg,
|
|
149
147
|
loaders: createDefaultRegistry(),
|
|
150
|
-
routingSchema: getRoutingSchema(effectiveOpts)
|
|
151
|
-
controlRole
|
|
148
|
+
routingSchema: getRoutingSchema(effectiveOpts)
|
|
152
149
|
}));
|
|
153
150
|
// A suspended (or unknown) database is refused before anything is spent on
|
|
154
151
|
// the request; billing and platform admins set that state, the loader reads it.
|
package/middleware/api.js
CHANGED
|
@@ -10,8 +10,6 @@ const url_domains_1 = require("@constructive-io/url-domains");
|
|
|
10
10
|
const logger_1 = require("@pgpmjs/logger");
|
|
11
11
|
const server_utils_1 = require("@pgpmjs/server-utils");
|
|
12
12
|
const pg_cache_1 = require("pg-cache");
|
|
13
|
-
const pg_query_context_1 = require("pg-query-context");
|
|
14
|
-
const control_role_1 = require("../control-role");
|
|
15
13
|
const _50x_1 = __importDefault(require("../errors/50x"));
|
|
16
14
|
const _404_message_1 = __importDefault(require("../errors/404-message"));
|
|
17
15
|
const routing_1 = require("./routing");
|
|
@@ -56,7 +54,7 @@ const buildLoaderContext = (routingPool, opts, row) => {
|
|
|
56
54
|
return {
|
|
57
55
|
routingPool,
|
|
58
56
|
routingSchema: (0, routing_1.getRoutingSchema)(opts),
|
|
59
|
-
tenantPool: (0,
|
|
57
|
+
tenantPool: (0, pg_cache_1.getPgPool)({ ...opts.pg, database: dbname }),
|
|
60
58
|
databaseId: row.database_id,
|
|
61
59
|
apiId: row.api_id,
|
|
62
60
|
dbname
|
|
@@ -270,7 +268,7 @@ const resolveScopedRoute = async (ctx) => {
|
|
|
270
268
|
// Main Resolution Function
|
|
271
269
|
// =============================================================================
|
|
272
270
|
const getApiConfig = async (opts, req) => {
|
|
273
|
-
const pool = (0,
|
|
271
|
+
const pool = (0, pg_cache_1.getPgPool)(opts.pg);
|
|
274
272
|
const { domain, subdomains } = getUrlDomains(req);
|
|
275
273
|
const subdomain = (0, exports.getSubdomain)(subdomains);
|
|
276
274
|
const cacheKey = (0, exports.getSvcKey)(opts, req);
|
package/middleware/auth.js
CHANGED
package/middleware/flush.js
CHANGED
|
@@ -7,8 +7,6 @@ const server_utils_1 = require("@pgpmjs/server-utils");
|
|
|
7
7
|
const crypto_1 = require("crypto");
|
|
8
8
|
const graphile_cache_1 = require("graphile-cache");
|
|
9
9
|
const pg_cache_1 = require("pg-cache");
|
|
10
|
-
const pg_query_context_1 = require("pg-query-context");
|
|
11
|
-
const control_role_1 = require("../control-role");
|
|
12
10
|
const routing_1 = require("./routing");
|
|
13
11
|
const log = new logger_1.Logger('flush');
|
|
14
12
|
const bearerToken = (req) => {
|
|
@@ -53,7 +51,7 @@ const createFlushMiddleware = (opts) => {
|
|
|
53
51
|
};
|
|
54
52
|
exports.createFlushMiddleware = createFlushMiddleware;
|
|
55
53
|
const flushService = async (opts, databaseId) => {
|
|
56
|
-
const pgPool = (0,
|
|
54
|
+
const pgPool = (0, pg_cache_1.getPgPool)(opts.pg);
|
|
57
55
|
log.info('flushing db ' + databaseId);
|
|
58
56
|
const api = new RegExp(`^api:${databaseId}:.*`);
|
|
59
57
|
const schemata = new RegExp(`^schemata:${databaseId}:.*`);
|
package/middleware/graphile.js
CHANGED
|
@@ -13,8 +13,6 @@ const graphile_function_bindings_1 = require("graphile-function-bindings");
|
|
|
13
13
|
const graphile_settings_1 = require("graphile-settings");
|
|
14
14
|
const pg_cache_1 = require("pg-cache");
|
|
15
15
|
const pg_env_1 = require("pg-env");
|
|
16
|
-
const pg_query_context_1 = require("pg-query-context");
|
|
17
|
-
const control_role_1 = require("../control-role");
|
|
18
16
|
const observability_1 = require("../diagnostics/observability");
|
|
19
17
|
const api_errors_1 = require("../errors/api-errors");
|
|
20
18
|
const graphql_response_1 = require("../errors/graphql-response");
|
|
@@ -64,7 +62,7 @@ const reqLabel = (req) => (req.requestId ? `[${req.requestId}]` : '[req]');
|
|
|
64
62
|
* plugin preset. Without settings the default preset is used
|
|
65
63
|
* (everything on except aggregates).
|
|
66
64
|
*/
|
|
67
|
-
const buildPreset = async (pool, schemas, anonRole, roleName, introspectionRole,
|
|
65
|
+
const buildPreset = async (pool, schemas, anonRole, roleName, introspectionRole, graphileOptions, databaseSettings, apiId, compute, webauthn) => {
|
|
68
66
|
const introspection = await (0, graphile_introspection_1.makeIntrospectionWiring)(pool, schemas, graphileOptions, undefined, introspectionRole);
|
|
69
67
|
const configuredPreset = graphileOptions?.preset ?? {};
|
|
70
68
|
const grafastCachePreset = (0, graphile_settings_1.createGrafastCacheLimitsPreset)(graphileOptions?.grafastCache);
|
|
@@ -95,7 +93,6 @@ const buildPreset = async (pool, schemas, anonRole, roleName, introspectionRole,
|
|
|
95
93
|
? [
|
|
96
94
|
(0, graphile_function_bindings_1.createFunctionBindingsPlugin)({
|
|
97
95
|
apiId,
|
|
98
|
-
role: controlRole,
|
|
99
96
|
modules: compute.modules.map((m) => ({
|
|
100
97
|
computeSchema: m.schemaName,
|
|
101
98
|
bindingsTable: m.bindingsTableName,
|
|
@@ -320,7 +317,7 @@ const graphile = (opts) => {
|
|
|
320
317
|
// concurrent requests coalesce while optional plugin modules load.
|
|
321
318
|
const creationPromise = (async () => {
|
|
322
319
|
const compute = api.apiId ? await req.constructive?.useModule('compute') : undefined;
|
|
323
|
-
const preset = await buildPreset(pool, schema || [], anonRole, roleName, opts.api?.introspectionRole,
|
|
320
|
+
const preset = await buildPreset(pool, schema || [], anonRole, roleName, opts.api?.introspectionRole, opts.graphile, api.databaseSettings, api.apiId, compute, Boolean(api.webauthnSettings));
|
|
324
321
|
return (0, graphile_build_stats_1.observeGraphileBuild)({
|
|
325
322
|
cacheKey: key,
|
|
326
323
|
serviceKey: key,
|
|
@@ -328,8 +325,7 @@ const graphile = (opts) => {
|
|
|
328
325
|
}, () => (0, graphile_cache_1.createGraphileInstance)({
|
|
329
326
|
preset,
|
|
330
327
|
cacheKey: key,
|
|
331
|
-
enableRealtime: api.databaseSettings?.enableRealtime
|
|
332
|
-
realtimePool: (0, pg_query_context_1.controlPlanePool)(pool, (0, control_role_1.getControlRole)(opts))
|
|
328
|
+
enableRealtime: api.databaseSettings?.enableRealtime
|
|
333
329
|
}), { enabled: observabilityEnabled });
|
|
334
330
|
})();
|
|
335
331
|
creating.set(key, creationPromise);
|
package/middleware/routing.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Pool } from 'pg';
|
|
2
2
|
import { ApiOptions, ApiStructure } from '../types';
|
|
3
3
|
/**
|
|
4
4
|
* The role an API row names is the role the server will SET ROLE to; a row
|
|
@@ -41,7 +41,7 @@ export declare const isValidSchemaName: (name: string) => boolean;
|
|
|
41
41
|
* resolver is not installed in the target database — in both cases the caller
|
|
42
42
|
* treats it as a hard no-match (→ 404); there is no legacy fallback.
|
|
43
43
|
*/
|
|
44
|
-
export declare const resolveRoute: (pool:
|
|
44
|
+
export declare const resolveRoute: (pool: Pool, schema: string, host: string) => Promise<ResolvedRoute | null>;
|
|
45
45
|
/**
|
|
46
46
|
* Map a resolved api-target route onto the ApiStructure shape consumed by the
|
|
47
47
|
* rest of the middleware chain. Returns null when the route target is not an
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constructive-io/graphql-server",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.31.0",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "Constructive GraphQL Server",
|
|
6
6
|
"main": "index.js",
|
|
@@ -43,18 +43,18 @@
|
|
|
43
43
|
"@agentic-kit/ollama": "2.15.0",
|
|
44
44
|
"@constructive-io/csrf": "^0.30.1",
|
|
45
45
|
"@constructive-io/errors": "^0.24.0",
|
|
46
|
-
"@constructive-io/express-context": "^0.34.
|
|
47
|
-
"@constructive-io/graphql-env": "^3.36.
|
|
48
|
-
"@constructive-io/graphql-types": "^3.35.
|
|
46
|
+
"@constructive-io/express-context": "^0.34.1",
|
|
47
|
+
"@constructive-io/graphql-env": "^3.36.1",
|
|
48
|
+
"@constructive-io/graphql-types": "^3.35.1",
|
|
49
49
|
"@constructive-io/llm-env": "^0.15.0",
|
|
50
|
-
"@constructive-io/query-builder": "^3.16.
|
|
50
|
+
"@constructive-io/query-builder": "^3.16.10",
|
|
51
51
|
"@constructive-io/s3-utils": "^2.34.0",
|
|
52
52
|
"@constructive-io/url-domains": "^2.31.0",
|
|
53
53
|
"@graphile-contrib/pg-many-to-many": "2.0.0-rc.2",
|
|
54
|
-
"@pgpmjs/env": "^2.47.
|
|
54
|
+
"@pgpmjs/env": "^2.47.1",
|
|
55
55
|
"@pgpmjs/logger": "^2.26.0",
|
|
56
|
-
"@pgpmjs/server-utils": "^3.29.
|
|
57
|
-
"@pgpmjs/types": "^2.57.
|
|
56
|
+
"@pgpmjs/server-utils": "^3.29.8",
|
|
57
|
+
"@pgpmjs/types": "^2.57.1",
|
|
58
58
|
"@pgsql/quotes": "^18.2.4",
|
|
59
59
|
"@simplewebauthn/server": "13.3.2",
|
|
60
60
|
"cors": "^2.8.6",
|
|
@@ -65,20 +65,20 @@
|
|
|
65
65
|
"grafserv": "1.0.1",
|
|
66
66
|
"graphile-build": "5.1.1",
|
|
67
67
|
"graphile-build-pg": "5.1.3",
|
|
68
|
-
"graphile-cache": "^4.15.
|
|
68
|
+
"graphile-cache": "^4.15.1",
|
|
69
69
|
"graphile-config": "1.1.0",
|
|
70
|
-
"graphile-function-bindings": "^1.17.
|
|
70
|
+
"graphile-function-bindings": "^1.17.11",
|
|
71
71
|
"graphile-plugin-utils": "^1.13.0",
|
|
72
|
-
"graphile-scoped-introspection": "^0.4.
|
|
73
|
-
"graphile-settings": "^6.25.
|
|
72
|
+
"graphile-scoped-introspection": "^0.4.11",
|
|
73
|
+
"graphile-settings": "^6.25.3",
|
|
74
74
|
"graphile-utils": "5.0.3",
|
|
75
75
|
"graphql": "16.13.0",
|
|
76
76
|
"graphql-upload": "^13.0.0",
|
|
77
77
|
"lru-cache": "^11.2.7",
|
|
78
78
|
"pg": "^8.21.0",
|
|
79
|
-
"pg-cache": "^3.29.
|
|
80
|
-
"pg-env": "^1.32.
|
|
81
|
-
"pg-query-context": "^2.32.
|
|
79
|
+
"pg-cache": "^3.29.8",
|
|
80
|
+
"pg-env": "^1.32.1",
|
|
81
|
+
"pg-query-context": "^2.32.1",
|
|
82
82
|
"pg-sql2": "5.0.1",
|
|
83
83
|
"postgraphile": "5.1.4",
|
|
84
84
|
"request-ip": "^3.3.0"
|
|
@@ -93,11 +93,11 @@
|
|
|
93
93
|
"@types/request-ip": "^0.0.41",
|
|
94
94
|
"@types/supertest": "^7.2.1",
|
|
95
95
|
"cookie-parser": "^1.4.7",
|
|
96
|
-
"graphile-test": "5.17.
|
|
96
|
+
"graphile-test": "5.17.11",
|
|
97
97
|
"makage": "^0.8.0",
|
|
98
98
|
"nodemon": "^3.1.14",
|
|
99
99
|
"supertest": "^7.2.2",
|
|
100
100
|
"ts-node": "^10.9.2"
|
|
101
101
|
},
|
|
102
|
-
"gitHead": "
|
|
102
|
+
"gitHead": "bf845a1b0701f57f6900d9e2a3c66ea48e125cc3"
|
|
103
103
|
}
|
package/refusals/recorder.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { RefusalReason, RefusalRecorderStats } from '@constructive-io/expre
|
|
|
3
3
|
import { RefusalRecorder } from '@constructive-io/express-context';
|
|
4
4
|
import type { ConstructiveOptions } from '@constructive-io/graphql-types';
|
|
5
5
|
import type { Request } from 'express';
|
|
6
|
-
import {
|
|
6
|
+
import type { Pool } from 'pg';
|
|
7
7
|
/**
|
|
8
8
|
* refusals/recorder — the GraphQL lane's `RefusalRecorder`.
|
|
9
9
|
*
|
|
@@ -61,7 +61,7 @@ interface PlatformFlushIdentity {
|
|
|
61
61
|
* `platform-bootstrap` service principal's user row. Throws — with the row
|
|
62
62
|
* that was missing named — rather than returning a partial identity.
|
|
63
63
|
*/
|
|
64
|
-
export declare const resolvePlatformFlushIdentity: (pool:
|
|
64
|
+
export declare const resolvePlatformFlushIdentity: (pool: Pool, principalName?: string) => Promise<PlatformFlushIdentity>;
|
|
65
65
|
export declare const platformFlushClaims: (identity: PlatformFlushIdentity) => Record<string, string>;
|
|
66
66
|
export interface PlatformRefusalRecorderOptions {
|
|
67
67
|
intervalMs?: number;
|
package/refusals/recorder.js
CHANGED
|
@@ -5,8 +5,6 @@ require("../middleware/types"); // for Request type
|
|
|
5
5
|
const express_context_1 = require("@constructive-io/express-context");
|
|
6
6
|
const logger_1 = require("@pgpmjs/logger");
|
|
7
7
|
const pg_cache_1 = require("pg-cache");
|
|
8
|
-
const pg_query_context_1 = require("pg-query-context");
|
|
9
|
-
const control_role_1 = require("../control-role");
|
|
10
8
|
const log = new logger_1.Logger('refusals');
|
|
11
9
|
/**
|
|
12
10
|
* refusals/recorder — the GraphQL lane's `RefusalRecorder`.
|
|
@@ -131,17 +129,16 @@ exports.platformFlushClaims = platformFlushClaims;
|
|
|
131
129
|
*/
|
|
132
130
|
const createPlatformRefusalRecorder = (opts, recorderOpts = {}) => {
|
|
133
131
|
const pool = (0, pg_cache_1.getPgPool)(opts.pg);
|
|
134
|
-
const role = (0, control_role_1.getControlRole)(opts);
|
|
135
132
|
let cached = null;
|
|
136
133
|
const claims = async () => {
|
|
137
134
|
if (cached)
|
|
138
135
|
return cached;
|
|
139
|
-
cached = (0, exports.platformFlushClaims)(await (0, exports.resolvePlatformFlushIdentity)(
|
|
136
|
+
cached = (0, exports.platformFlushClaims)(await (0, exports.resolvePlatformFlushIdentity)(pool, recorderOpts.principalName));
|
|
140
137
|
log.info(`[refusals] flushing as '${recorderOpts.principalName ?? exports.PLATFORM_BOOTSTRAP_PRINCIPAL}'`);
|
|
141
138
|
return cached;
|
|
142
139
|
};
|
|
143
140
|
return new express_context_1.RefusalRecorder({
|
|
144
|
-
sink: (0, express_context_1.createRecordRefusalsSink)({ pool,
|
|
141
|
+
sink: (0, express_context_1.createRecordRefusalsSink)({ pool, claims }),
|
|
145
142
|
intervalMs: recorderOpts.intervalMs,
|
|
146
143
|
jitterMs: recorderOpts.jitterMs,
|
|
147
144
|
maxKeys: recorderOpts.maxKeys
|
package/server.js
CHANGED
|
@@ -17,7 +17,6 @@ const graphql_upload_1 = __importDefault(require("graphql-upload"));
|
|
|
17
17
|
const pg_cache_1 = require("pg-cache");
|
|
18
18
|
const request_ip_1 = __importDefault(require("request-ip"));
|
|
19
19
|
const agentic_1 = require("./agentic");
|
|
20
|
-
const control_role_1 = require("./control-role");
|
|
21
20
|
const debug_db_snapshot_1 = require("./diagnostics/debug-db-snapshot");
|
|
22
21
|
const debug_sampler_1 = require("./diagnostics/debug-sampler");
|
|
23
22
|
const observability_1 = require("./diagnostics/observability");
|
|
@@ -84,7 +83,6 @@ class Server {
|
|
|
84
83
|
constructor(opts) {
|
|
85
84
|
this.opts = (0, graphql_env_1.getEnvOptions)(opts);
|
|
86
85
|
const effectiveOpts = this.opts;
|
|
87
|
-
const controlRole = (0, control_role_1.getControlRole)(effectiveOpts);
|
|
88
86
|
const observabilityRequested = (0, observability_1.isGraphqlObservabilityRequested)();
|
|
89
87
|
const observabilityEnabled = (0, observability_1.isGraphqlObservabilityEnabled)(effectiveOpts.server?.host);
|
|
90
88
|
const app = (0, express_1.default)();
|
|
@@ -154,8 +152,7 @@ class Server {
|
|
|
154
152
|
app.use((0, express_context_1.createContextMiddleware)({
|
|
155
153
|
pg: effectiveOpts.pg,
|
|
156
154
|
loaders: (0, express_context_1.createDefaultRegistry)(),
|
|
157
|
-
routingSchema: (0, routing_1.getRoutingSchema)(effectiveOpts)
|
|
158
|
-
controlRole
|
|
155
|
+
routingSchema: (0, routing_1.getRoutingSchema)(effectiveOpts)
|
|
159
156
|
}));
|
|
160
157
|
// A suspended (or unknown) database is refused before anything is spent on
|
|
161
158
|
// the request; billing and platform admins set that state, the loader reads it.
|
package/control-role.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { ApiOptions } from '@constructive-io/graphql-types';
|
|
2
|
-
/**
|
|
3
|
-
* The role control-plane queries run as (`api.controlRole` / `API_CONTROL_ROLE`).
|
|
4
|
-
* The login role is NOINHERIT and holds no privileges, so there is no default.
|
|
5
|
-
*/
|
|
6
|
-
export declare const getControlRole: (opts: {
|
|
7
|
-
api?: ApiOptions;
|
|
8
|
-
}) => string;
|
package/control-role.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getControlRole = void 0;
|
|
4
|
-
/**
|
|
5
|
-
* The role control-plane queries run as (`api.controlRole` / `API_CONTROL_ROLE`).
|
|
6
|
-
* The login role is NOINHERIT and holds no privileges, so there is no default.
|
|
7
|
-
*/
|
|
8
|
-
const getControlRole = (opts) => {
|
|
9
|
-
const role = opts.api?.controlRole;
|
|
10
|
-
if (!role) {
|
|
11
|
-
throw new Error('GraphQL server: api.controlRole (API_CONTROL_ROLE) is required — control-plane queries run as that role');
|
|
12
|
-
}
|
|
13
|
-
return role;
|
|
14
|
-
};
|
|
15
|
-
exports.getControlRole = getControlRole;
|
package/esm/control-role.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The role control-plane queries run as (`api.controlRole` / `API_CONTROL_ROLE`).
|
|
3
|
-
* The login role is NOINHERIT and holds no privileges, so there is no default.
|
|
4
|
-
*/
|
|
5
|
-
export const getControlRole = (opts) => {
|
|
6
|
-
const role = opts.api?.controlRole;
|
|
7
|
-
if (!role) {
|
|
8
|
-
throw new Error('GraphQL server: api.controlRole (API_CONTROL_ROLE) is required — control-plane queries run as that role');
|
|
9
|
-
}
|
|
10
|
-
return role;
|
|
11
|
-
};
|