@constructive-io/graphql-server 5.24.0 → 5.25.1

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.
@@ -0,0 +1,36 @@
1
+ import './types'; // for Request type
2
+ import { errors } from '@constructive-io/errors';
3
+ import { respondWithGraphQLError } from '../errors/graphql-response';
4
+ /**
5
+ * Express middleware that refuses requests pinned to a database that is not in
6
+ * good standing.
7
+ *
8
+ * The decision is the `standing` loader's (system-controlled
9
+ * `suspended_at`/`suspended_reason` on `metaschema_public.database`, cached for
10
+ * a few seconds), so an established client keeps being served for at most that
11
+ * window after a suspension. A database the plane does not know is refused the
12
+ * same way: a request pinned to it has nothing to run against. A lookup failure
13
+ * is handed to the error handler — "could not verify" is not "allowed".
14
+ *
15
+ * A request without a resolved database (no context, or a plane without
16
+ * `metaschema_public`) has nothing to be suspended and passes through.
17
+ *
18
+ * Mount after the context middleware and before the GraphQL handler.
19
+ */
20
+ export const createStandingMiddleware = () => {
21
+ return async (req, res, next) => {
22
+ let standing;
23
+ try {
24
+ standing = await req.constructive?.useModule('standing');
25
+ }
26
+ catch (e) {
27
+ next(e);
28
+ return;
29
+ }
30
+ if (standing && (!standing.exists || standing.suspended)) {
31
+ respondWithGraphQLError(res, errors.ACCESS_SUSPENDED(standing.reason ? { reason: standing.reason } : {}), { status: 403 });
32
+ return;
33
+ }
34
+ next();
35
+ };
36
+ };
@@ -16,6 +16,13 @@
16
16
  * The walk is manual rather than `visitWithTypeInfo` because fragment spreads
17
17
  * have to be followed (a document can hide its depth entirely inside
18
18
  * fragments) and the visitor does not follow them.
19
+ *
20
+ * Schema introspection (`__schema` / `__type`) is governed only by
21
+ * `enableIntrospection`. Its selections are not walked: the introspection types
22
+ * carry no connections, so there is nothing to cost, and the standard
23
+ * introspection document nests a fixed `ofType` chain deeper than a sensible
24
+ * tenant depth budget, so charging depth would make the budget decide whether
25
+ * clients can introspect at all — a decision the dedicated switch already owns.
19
26
  */
20
27
  import { errors } from '@constructive-io/errors';
21
28
  import { ASSUMED_PAGE_SIZE } from '@constructive-io/express-context';
@@ -84,6 +91,7 @@ function walkSelectionSet(walk, selectionSet, parentType, depth, multiplier) {
84
91
  if (!walk.protection.enableIntrospection) {
85
92
  reject(errors.INTROSPECTION_DISABLED());
86
93
  }
94
+ continue;
87
95
  }
88
96
  const field = parentType && isObjectType(parentType)
89
97
  ? parentType.getFields()[selection.name.value]
package/esm/server.js CHANGED
@@ -33,6 +33,7 @@ import { localObservabilityOnly } from './middleware/observability/guard';
33
33
  import { createRequestLogger } from './middleware/observability/request-logger';
34
34
  import { createRequestProtectionMiddleware } from './middleware/request-protection';
35
35
  import { getRoutingSchema } from './middleware/routing';
36
+ import { createStandingMiddleware } from './middleware/standing';
36
37
  import { createPlatformRefusalRecorder, installRefusalRecorder } from './refusals/recorder';
37
38
  const log = new Logger('server');
38
39
  /**
@@ -146,6 +147,9 @@ class Server {
146
147
  loaders: createDefaultRegistry(),
147
148
  routingSchema: getRoutingSchema(effectiveOpts)
148
149
  }));
150
+ // A suspended (or unknown) database is refused before anything is spent on
151
+ // the request; billing and platform admins set that state, the loader reads it.
152
+ app.use(createStandingMiddleware());
149
153
  // Resolve the tenant's protection bounds before anything can spend budget
150
154
  // on the request (and before the GraphQL handler reads them for pgSettings).
151
155
  app.use(createRequestProtectionMiddleware());
@@ -0,0 +1,19 @@
1
+ import './types';
2
+ import type { RequestHandler } from 'express';
3
+ /**
4
+ * Express middleware that refuses requests pinned to a database that is not in
5
+ * good standing.
6
+ *
7
+ * The decision is the `standing` loader's (system-controlled
8
+ * `suspended_at`/`suspended_reason` on `metaschema_public.database`, cached for
9
+ * a few seconds), so an established client keeps being served for at most that
10
+ * window after a suspension. A database the plane does not know is refused the
11
+ * same way: a request pinned to it has nothing to run against. A lookup failure
12
+ * is handed to the error handler — "could not verify" is not "allowed".
13
+ *
14
+ * A request without a resolved database (no context, or a plane without
15
+ * `metaschema_public`) has nothing to be suspended and passes through.
16
+ *
17
+ * Mount after the context middleware and before the GraphQL handler.
18
+ */
19
+ export declare const createStandingMiddleware: () => RequestHandler;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createStandingMiddleware = void 0;
4
+ require("./types"); // for Request type
5
+ const errors_1 = require("@constructive-io/errors");
6
+ const graphql_response_1 = require("../errors/graphql-response");
7
+ /**
8
+ * Express middleware that refuses requests pinned to a database that is not in
9
+ * good standing.
10
+ *
11
+ * The decision is the `standing` loader's (system-controlled
12
+ * `suspended_at`/`suspended_reason` on `metaschema_public.database`, cached for
13
+ * a few seconds), so an established client keeps being served for at most that
14
+ * window after a suspension. A database the plane does not know is refused the
15
+ * same way: a request pinned to it has nothing to run against. A lookup failure
16
+ * is handed to the error handler — "could not verify" is not "allowed".
17
+ *
18
+ * A request without a resolved database (no context, or a plane without
19
+ * `metaschema_public`) has nothing to be suspended and passes through.
20
+ *
21
+ * Mount after the context middleware and before the GraphQL handler.
22
+ */
23
+ const createStandingMiddleware = () => {
24
+ return async (req, res, next) => {
25
+ let standing;
26
+ try {
27
+ standing = await req.constructive?.useModule('standing');
28
+ }
29
+ catch (e) {
30
+ next(e);
31
+ return;
32
+ }
33
+ if (standing && (!standing.exists || standing.suspended)) {
34
+ (0, graphql_response_1.respondWithGraphQLError)(res, errors_1.errors.ACCESS_SUSPENDED(standing.reason ? { reason: standing.reason } : {}), { status: 403 });
35
+ return;
36
+ }
37
+ next();
38
+ };
39
+ };
40
+ exports.createStandingMiddleware = createStandingMiddleware;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constructive-io/graphql-server",
3
- "version": "5.24.0",
3
+ "version": "5.25.1",
4
4
  "author": "Constructive <developers@constructive.io>",
5
5
  "description": "Constructive GraphQL Server",
6
6
  "main": "index.js",
@@ -43,19 +43,19 @@
43
43
  "dependencies": {
44
44
  "@agentic-kit/ollama": "2.14.0",
45
45
  "@constructive-io/csrf": "^0.29.1",
46
- "@constructive-io/errors": "^0.13.0",
47
- "@constructive-io/express-context": "^0.30.0",
48
- "@constructive-io/graphql-env": "^3.32.0",
49
- "@constructive-io/graphql-types": "^3.31.0",
46
+ "@constructive-io/errors": "^0.14.0",
47
+ "@constructive-io/express-context": "^0.31.0",
48
+ "@constructive-io/graphql-env": "^3.32.1",
49
+ "@constructive-io/graphql-types": "^3.31.1",
50
50
  "@constructive-io/llm-env": "^0.14.1",
51
- "@constructive-io/query-builder": "^3.13.5",
51
+ "@constructive-io/query-builder": "^3.13.6",
52
52
  "@constructive-io/s3-utils": "^2.33.0",
53
53
  "@constructive-io/url-domains": "^2.30.1",
54
54
  "@graphile-contrib/pg-many-to-many": "2.0.0-rc.2",
55
- "@pgpmjs/env": "^2.43.4",
55
+ "@pgpmjs/env": "^2.43.5",
56
56
  "@pgpmjs/logger": "^2.25.1",
57
- "@pgpmjs/server-utils": "^3.27.4",
58
- "@pgpmjs/types": "^2.53.2",
57
+ "@pgpmjs/server-utils": "^3.27.5",
58
+ "@pgpmjs/types": "^2.53.3",
59
59
  "cors": "^2.8.6",
60
60
  "deepmerge": "^4.3.1",
61
61
  "express": "^5.2.1",
@@ -64,16 +64,16 @@
64
64
  "grafserv": "1.0.1",
65
65
  "graphile-build": "5.1.1",
66
66
  "graphile-build-pg": "5.1.3",
67
- "graphile-cache": "^4.12.4",
67
+ "graphile-cache": "^4.12.5",
68
68
  "graphile-config": "1.1.0",
69
- "graphile-function-bindings": "^1.14.5",
70
- "graphile-settings": "^6.21.7",
69
+ "graphile-function-bindings": "^1.14.6",
70
+ "graphile-settings": "^6.21.8",
71
71
  "graphile-utils": "5.0.3",
72
72
  "graphql": "16.13.0",
73
73
  "graphql-upload": "^13.0.0",
74
74
  "lru-cache": "^11.2.7",
75
75
  "pg": "^8.21.0",
76
- "pg-cache": "^3.27.4",
76
+ "pg-cache": "^3.27.5",
77
77
  "pg-env": "^1.31.1",
78
78
  "pg-query-context": "^2.30.1",
79
79
  "pg-sql2": "5.0.1",
@@ -90,11 +90,11 @@
90
90
  "@types/request-ip": "^0.0.41",
91
91
  "@types/supertest": "^7.2.1",
92
92
  "cookie-parser": "^1.4.7",
93
- "graphile-test": "5.14.5",
93
+ "graphile-test": "5.14.6",
94
94
  "makage": "^0.8.0",
95
95
  "nodemon": "^3.1.14",
96
96
  "supertest": "^7.2.2",
97
97
  "ts-node": "^10.9.2"
98
98
  },
99
- "gitHead": "10d3a11478d17a20c80da5a827f8e8da4facfc80"
99
+ "gitHead": "164f6e481961196e681eb3dee9e5f9ebcb8fb834"
100
100
  }
@@ -16,6 +16,13 @@
16
16
  * The walk is manual rather than `visitWithTypeInfo` because fragment spreads
17
17
  * have to be followed (a document can hide its depth entirely inside
18
18
  * fragments) and the visitor does not follow them.
19
+ *
20
+ * Schema introspection (`__schema` / `__type`) is governed only by
21
+ * `enableIntrospection`. Its selections are not walked: the introspection types
22
+ * carry no connections, so there is nothing to cost, and the standard
23
+ * introspection document nests a fixed `ofType` chain deeper than a sensible
24
+ * tenant depth budget, so charging depth would make the budget decide whether
25
+ * clients can introspect at all — a decision the dedicated switch already owns.
19
26
  */
20
27
  import { type RequestProtection } from '@constructive-io/express-context';
21
28
  import type { DocumentNode, GraphQLSchema } from 'graphql';
@@ -17,6 +17,13 @@
17
17
  * The walk is manual rather than `visitWithTypeInfo` because fragment spreads
18
18
  * have to be followed (a document can hide its depth entirely inside
19
19
  * fragments) and the visitor does not follow them.
20
+ *
21
+ * Schema introspection (`__schema` / `__type`) is governed only by
22
+ * `enableIntrospection`. Its selections are not walked: the introspection types
23
+ * carry no connections, so there is nothing to cost, and the standard
24
+ * introspection document nests a fixed `ofType` chain deeper than a sensible
25
+ * tenant depth budget, so charging depth would make the budget decide whether
26
+ * clients can introspect at all — a decision the dedicated switch already owns.
20
27
  */
21
28
  Object.defineProperty(exports, "__esModule", { value: true });
22
29
  exports.enforceDocumentProtection = enforceDocumentProtection;
@@ -87,6 +94,7 @@ function walkSelectionSet(walk, selectionSet, parentType, depth, multiplier) {
87
94
  if (!walk.protection.enableIntrospection) {
88
95
  reject(errors_1.errors.INTROSPECTION_DISABLED());
89
96
  }
97
+ continue;
90
98
  }
91
99
  const field = parentType && (0, graphql_1.isObjectType)(parentType)
92
100
  ? parentType.getFields()[selection.name.value]
package/server.js CHANGED
@@ -39,6 +39,7 @@ const guard_1 = require("./middleware/observability/guard");
39
39
  const request_logger_1 = require("./middleware/observability/request-logger");
40
40
  const request_protection_1 = require("./middleware/request-protection");
41
41
  const routing_1 = require("./middleware/routing");
42
+ const standing_1 = require("./middleware/standing");
42
43
  const recorder_1 = require("./refusals/recorder");
43
44
  const log = new logger_1.Logger('server');
44
45
  /**
@@ -153,6 +154,9 @@ class Server {
153
154
  loaders: (0, express_context_1.createDefaultRegistry)(),
154
155
  routingSchema: (0, routing_1.getRoutingSchema)(effectiveOpts)
155
156
  }));
157
+ // A suspended (or unknown) database is refused before anything is spent on
158
+ // the request; billing and platform admins set that state, the loader reads it.
159
+ app.use((0, standing_1.createStandingMiddleware)());
156
160
  // Resolve the tenant's protection bounds before anything can spend budget
157
161
  // on the request (and before the GraphQL handler reads them for pgSettings).
158
162
  app.use((0, request_protection_1.createRequestProtectionMiddleware)());