@constructive-io/graphql-server 5.25.1 → 5.25.2
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/esm/protection/document-gate.js +35 -16
- package/package.json +16 -16
- package/protection/document-gate.d.ts +17 -6
- package/protection/document-gate.js +36 -16
|
@@ -17,12 +17,15 @@
|
|
|
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
19
|
*
|
|
20
|
-
* Schema introspection (`__schema` / `__type`) is
|
|
21
|
-
* `enableIntrospection
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
20
|
+
* Schema introspection (`__schema` / `__type`) is switched by
|
|
21
|
+
* `enableIntrospection` and bounded by its own fixed depth ceiling rather than
|
|
22
|
+
* the tenant's `maxQueryDepth`. The standard introspection document nests a
|
|
23
|
+
* fixed `ofType` chain deeper than a sensible tenant depth budget, so charging
|
|
24
|
+
* it against that budget would let the budget decide whether clients can
|
|
25
|
+
* introspect at all. It still needs a bound of its own, though: the
|
|
26
|
+
* introspection schema is recursive (`__Type.fields → __Field.type → __Type`),
|
|
27
|
+
* so an unbounded walk would let a client nest it arbitrarily. Introspection
|
|
28
|
+
* types carry no connections, so cost is naturally zero.
|
|
26
29
|
*/
|
|
27
30
|
import { errors } from '@constructive-io/errors';
|
|
28
31
|
import { ASSUMED_PAGE_SIZE } from '@constructive-io/express-context';
|
|
@@ -43,6 +46,14 @@ const reject = (error) => {
|
|
|
43
46
|
statusCode: error.http
|
|
44
47
|
});
|
|
45
48
|
};
|
|
49
|
+
/**
|
|
50
|
+
* Depth allowed inside a `__schema` / `__type` subtree, counted from the
|
|
51
|
+
* introspection field itself. graphql-js's `getIntrospectionQuery` with every
|
|
52
|
+
* option enabled reaches 13 (`types → fields → args → type → ofType×9`), so
|
|
53
|
+
* this leaves headroom for tooling without permitting the recursive
|
|
54
|
+
* `fields { type { fields { type … } } }` shape to run away.
|
|
55
|
+
*/
|
|
56
|
+
export const INTROSPECTION_MAX_DEPTH = 16;
|
|
46
57
|
/** Arguments a connection field uses to size its page. */
|
|
47
58
|
const PAGE_SIZE_ARGS = ['first', 'last'];
|
|
48
59
|
/** A connection is any object type that carries Relay's `pageInfo`. */
|
|
@@ -78,20 +89,28 @@ const requestedPageSize = (args, variableValues) => {
|
|
|
78
89
|
* the walk only stops charging cost rather than raising its own error)
|
|
79
90
|
* @param depth - nesting level of these selections
|
|
80
91
|
* @param multiplier - rows the enclosing connections can return
|
|
92
|
+
* @param bound - which depth limit applies to these selections
|
|
81
93
|
*/
|
|
82
|
-
function walkSelectionSet(walk, selectionSet, parentType, depth, multiplier) {
|
|
83
|
-
if (
|
|
84
|
-
walk.maxDepth
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
function walkSelectionSet(walk, selectionSet, parentType, depth, multiplier, bound) {
|
|
95
|
+
if (bound.kind === 'query') {
|
|
96
|
+
if (depth > walk.maxDepth)
|
|
97
|
+
walk.maxDepth = depth;
|
|
98
|
+
if (depth > walk.protection.maxQueryDepth) {
|
|
99
|
+
reject(errors.QUERY_TOO_DEEP({ depth, limit: walk.protection.maxQueryDepth }));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else if (depth - bound.base > INTROSPECTION_MAX_DEPTH) {
|
|
103
|
+
reject(errors.QUERY_TOO_DEEP({ depth: depth - bound.base, limit: INTROSPECTION_MAX_DEPTH }));
|
|
87
104
|
}
|
|
88
105
|
for (const selection of selectionSet.selections) {
|
|
89
106
|
if (selection.kind === Kind.FIELD) {
|
|
107
|
+
let childBound = bound;
|
|
90
108
|
if (selection.name.value === '__schema' || selection.name.value === '__type') {
|
|
91
109
|
if (!walk.protection.enableIntrospection) {
|
|
92
110
|
reject(errors.INTROSPECTION_DISABLED());
|
|
93
111
|
}
|
|
94
|
-
|
|
112
|
+
if (bound.kind === 'query')
|
|
113
|
+
childBound = { kind: 'introspection', base: depth };
|
|
95
114
|
}
|
|
96
115
|
const field = parentType && isObjectType(parentType)
|
|
97
116
|
? parentType.getFields()[selection.name.value]
|
|
@@ -118,7 +137,7 @@ function walkSelectionSet(walk, selectionSet, parentType, depth, multiplier) {
|
|
|
118
137
|
}
|
|
119
138
|
}
|
|
120
139
|
if (selection.selectionSet) {
|
|
121
|
-
walkSelectionSet(walk, selection.selectionSet, fieldType, depth + 1, childMultiplier);
|
|
140
|
+
walkSelectionSet(walk, selection.selectionSet, fieldType, depth + 1, childMultiplier, childBound);
|
|
122
141
|
}
|
|
123
142
|
continue;
|
|
124
143
|
}
|
|
@@ -127,7 +146,7 @@ function walkSelectionSet(walk, selectionSet, parentType, depth, multiplier) {
|
|
|
127
146
|
? typeFromAST(walk.schema, selection.typeCondition)
|
|
128
147
|
: parentType;
|
|
129
148
|
// An inline fragment is not a level of nesting of its own.
|
|
130
|
-
walkSelectionSet(walk, selection.selectionSet, onType ?? null, depth, multiplier);
|
|
149
|
+
walkSelectionSet(walk, selection.selectionSet, onType ?? null, depth, multiplier, bound);
|
|
131
150
|
continue;
|
|
132
151
|
}
|
|
133
152
|
if (selection.kind === Kind.FRAGMENT_SPREAD) {
|
|
@@ -143,7 +162,7 @@ function walkSelectionSet(walk, selectionSet, parentType, depth, multiplier) {
|
|
|
143
162
|
const onType = typeFromAST(walk.schema, fragment.typeCondition);
|
|
144
163
|
walk.activeFragments.add(name);
|
|
145
164
|
try {
|
|
146
|
-
walkSelectionSet(walk, fragment.selectionSet, onType ?? null, depth, multiplier);
|
|
165
|
+
walkSelectionSet(walk, fragment.selectionSet, onType ?? null, depth, multiplier, bound);
|
|
147
166
|
}
|
|
148
167
|
finally {
|
|
149
168
|
walk.activeFragments.delete(name);
|
|
@@ -185,6 +204,6 @@ export function enforceDocumentProtection(schema, document, variableValues, prot
|
|
|
185
204
|
maxDepth: 0,
|
|
186
205
|
cost: 0
|
|
187
206
|
};
|
|
188
|
-
walkSelectionSet(walk, operation.selectionSet, rootType ?? null, 1, 1);
|
|
207
|
+
walkSelectionSet(walk, operation.selectionSet, rootType ?? null, 1, 1, { kind: 'query' });
|
|
189
208
|
return { depth: walk.maxDepth, cost: walk.cost };
|
|
190
209
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constructive-io/graphql-server",
|
|
3
|
-
"version": "5.25.
|
|
3
|
+
"version": "5.25.2",
|
|
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.
|
|
47
|
-
"@constructive-io/express-context": "^0.31.
|
|
48
|
-
"@constructive-io/graphql-env": "^3.32.
|
|
49
|
-
"@constructive-io/graphql-types": "^3.31.
|
|
46
|
+
"@constructive-io/errors": "^0.16.1",
|
|
47
|
+
"@constructive-io/express-context": "^0.31.1",
|
|
48
|
+
"@constructive-io/graphql-env": "^3.32.2",
|
|
49
|
+
"@constructive-io/graphql-types": "^3.31.2",
|
|
50
50
|
"@constructive-io/llm-env": "^0.14.1",
|
|
51
|
-
"@constructive-io/query-builder": "^3.13.
|
|
52
|
-
"@constructive-io/s3-utils": "^2.33.
|
|
51
|
+
"@constructive-io/query-builder": "^3.13.7",
|
|
52
|
+
"@constructive-io/s3-utils": "^2.33.1",
|
|
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.
|
|
55
|
+
"@pgpmjs/env": "^2.43.6",
|
|
56
56
|
"@pgpmjs/logger": "^2.25.1",
|
|
57
|
-
"@pgpmjs/server-utils": "^3.27.
|
|
58
|
-
"@pgpmjs/types": "^2.53.
|
|
57
|
+
"@pgpmjs/server-utils": "^3.27.6",
|
|
58
|
+
"@pgpmjs/types": "^2.53.4",
|
|
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.
|
|
67
|
+
"graphile-cache": "^4.12.6",
|
|
68
68
|
"graphile-config": "1.1.0",
|
|
69
|
-
"graphile-function-bindings": "^1.14.
|
|
70
|
-
"graphile-settings": "^6.21.
|
|
69
|
+
"graphile-function-bindings": "^1.14.7",
|
|
70
|
+
"graphile-settings": "^6.21.9",
|
|
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.
|
|
76
|
+
"pg-cache": "^3.27.6",
|
|
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.
|
|
93
|
+
"graphile-test": "5.14.7",
|
|
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": "
|
|
99
|
+
"gitHead": "9bba983b38395277e0400294aaa380e586b049d4"
|
|
100
100
|
}
|
|
@@ -17,15 +17,26 @@
|
|
|
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
19
|
*
|
|
20
|
-
* Schema introspection (`__schema` / `__type`) is
|
|
21
|
-
* `enableIntrospection
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
20
|
+
* Schema introspection (`__schema` / `__type`) is switched by
|
|
21
|
+
* `enableIntrospection` and bounded by its own fixed depth ceiling rather than
|
|
22
|
+
* the tenant's `maxQueryDepth`. The standard introspection document nests a
|
|
23
|
+
* fixed `ofType` chain deeper than a sensible tenant depth budget, so charging
|
|
24
|
+
* it against that budget would let the budget decide whether clients can
|
|
25
|
+
* introspect at all. It still needs a bound of its own, though: the
|
|
26
|
+
* introspection schema is recursive (`__Type.fields → __Field.type → __Type`),
|
|
27
|
+
* so an unbounded walk would let a client nest it arbitrarily. Introspection
|
|
28
|
+
* types carry no connections, so cost is naturally zero.
|
|
26
29
|
*/
|
|
27
30
|
import { type RequestProtection } from '@constructive-io/express-context';
|
|
28
31
|
import type { DocumentNode, GraphQLSchema } from 'graphql';
|
|
32
|
+
/**
|
|
33
|
+
* Depth allowed inside a `__schema` / `__type` subtree, counted from the
|
|
34
|
+
* introspection field itself. graphql-js's `getIntrospectionQuery` with every
|
|
35
|
+
* option enabled reaches 13 (`types → fields → args → type → ofType×9`), so
|
|
36
|
+
* this leaves headroom for tooling without permitting the recursive
|
|
37
|
+
* `fields { type { fields { type … } } }` shape to run away.
|
|
38
|
+
*/
|
|
39
|
+
export declare const INTROSPECTION_MAX_DEPTH = 16;
|
|
29
40
|
export interface DocumentAnalysis {
|
|
30
41
|
/** Deepest field nesting in the operation. */
|
|
31
42
|
depth: number;
|
|
@@ -18,14 +18,18 @@
|
|
|
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
20
|
*
|
|
21
|
-
* Schema introspection (`__schema` / `__type`) is
|
|
22
|
-
* `enableIntrospection
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
21
|
+
* Schema introspection (`__schema` / `__type`) is switched by
|
|
22
|
+
* `enableIntrospection` and bounded by its own fixed depth ceiling rather than
|
|
23
|
+
* the tenant's `maxQueryDepth`. The standard introspection document nests a
|
|
24
|
+
* fixed `ofType` chain deeper than a sensible tenant depth budget, so charging
|
|
25
|
+
* it against that budget would let the budget decide whether clients can
|
|
26
|
+
* introspect at all. It still needs a bound of its own, though: the
|
|
27
|
+
* introspection schema is recursive (`__Type.fields → __Field.type → __Type`),
|
|
28
|
+
* so an unbounded walk would let a client nest it arbitrarily. Introspection
|
|
29
|
+
* types carry no connections, so cost is naturally zero.
|
|
27
30
|
*/
|
|
28
31
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.INTROSPECTION_MAX_DEPTH = void 0;
|
|
29
33
|
exports.enforceDocumentProtection = enforceDocumentProtection;
|
|
30
34
|
const errors_1 = require("@constructive-io/errors");
|
|
31
35
|
const express_context_1 = require("@constructive-io/express-context");
|
|
@@ -46,6 +50,14 @@ const reject = (error) => {
|
|
|
46
50
|
statusCode: error.http
|
|
47
51
|
});
|
|
48
52
|
};
|
|
53
|
+
/**
|
|
54
|
+
* Depth allowed inside a `__schema` / `__type` subtree, counted from the
|
|
55
|
+
* introspection field itself. graphql-js's `getIntrospectionQuery` with every
|
|
56
|
+
* option enabled reaches 13 (`types → fields → args → type → ofType×9`), so
|
|
57
|
+
* this leaves headroom for tooling without permitting the recursive
|
|
58
|
+
* `fields { type { fields { type … } } }` shape to run away.
|
|
59
|
+
*/
|
|
60
|
+
exports.INTROSPECTION_MAX_DEPTH = 16;
|
|
49
61
|
/** Arguments a connection field uses to size its page. */
|
|
50
62
|
const PAGE_SIZE_ARGS = ['first', 'last'];
|
|
51
63
|
/** A connection is any object type that carries Relay's `pageInfo`. */
|
|
@@ -81,20 +93,28 @@ const requestedPageSize = (args, variableValues) => {
|
|
|
81
93
|
* the walk only stops charging cost rather than raising its own error)
|
|
82
94
|
* @param depth - nesting level of these selections
|
|
83
95
|
* @param multiplier - rows the enclosing connections can return
|
|
96
|
+
* @param bound - which depth limit applies to these selections
|
|
84
97
|
*/
|
|
85
|
-
function walkSelectionSet(walk, selectionSet, parentType, depth, multiplier) {
|
|
86
|
-
if (
|
|
87
|
-
walk.maxDepth
|
|
88
|
-
|
|
89
|
-
|
|
98
|
+
function walkSelectionSet(walk, selectionSet, parentType, depth, multiplier, bound) {
|
|
99
|
+
if (bound.kind === 'query') {
|
|
100
|
+
if (depth > walk.maxDepth)
|
|
101
|
+
walk.maxDepth = depth;
|
|
102
|
+
if (depth > walk.protection.maxQueryDepth) {
|
|
103
|
+
reject(errors_1.errors.QUERY_TOO_DEEP({ depth, limit: walk.protection.maxQueryDepth }));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else if (depth - bound.base > exports.INTROSPECTION_MAX_DEPTH) {
|
|
107
|
+
reject(errors_1.errors.QUERY_TOO_DEEP({ depth: depth - bound.base, limit: exports.INTROSPECTION_MAX_DEPTH }));
|
|
90
108
|
}
|
|
91
109
|
for (const selection of selectionSet.selections) {
|
|
92
110
|
if (selection.kind === graphql_1.Kind.FIELD) {
|
|
111
|
+
let childBound = bound;
|
|
93
112
|
if (selection.name.value === '__schema' || selection.name.value === '__type') {
|
|
94
113
|
if (!walk.protection.enableIntrospection) {
|
|
95
114
|
reject(errors_1.errors.INTROSPECTION_DISABLED());
|
|
96
115
|
}
|
|
97
|
-
|
|
116
|
+
if (bound.kind === 'query')
|
|
117
|
+
childBound = { kind: 'introspection', base: depth };
|
|
98
118
|
}
|
|
99
119
|
const field = parentType && (0, graphql_1.isObjectType)(parentType)
|
|
100
120
|
? parentType.getFields()[selection.name.value]
|
|
@@ -121,7 +141,7 @@ function walkSelectionSet(walk, selectionSet, parentType, depth, multiplier) {
|
|
|
121
141
|
}
|
|
122
142
|
}
|
|
123
143
|
if (selection.selectionSet) {
|
|
124
|
-
walkSelectionSet(walk, selection.selectionSet, fieldType, depth + 1, childMultiplier);
|
|
144
|
+
walkSelectionSet(walk, selection.selectionSet, fieldType, depth + 1, childMultiplier, childBound);
|
|
125
145
|
}
|
|
126
146
|
continue;
|
|
127
147
|
}
|
|
@@ -130,7 +150,7 @@ function walkSelectionSet(walk, selectionSet, parentType, depth, multiplier) {
|
|
|
130
150
|
? (0, graphql_1.typeFromAST)(walk.schema, selection.typeCondition)
|
|
131
151
|
: parentType;
|
|
132
152
|
// An inline fragment is not a level of nesting of its own.
|
|
133
|
-
walkSelectionSet(walk, selection.selectionSet, onType ?? null, depth, multiplier);
|
|
153
|
+
walkSelectionSet(walk, selection.selectionSet, onType ?? null, depth, multiplier, bound);
|
|
134
154
|
continue;
|
|
135
155
|
}
|
|
136
156
|
if (selection.kind === graphql_1.Kind.FRAGMENT_SPREAD) {
|
|
@@ -146,7 +166,7 @@ function walkSelectionSet(walk, selectionSet, parentType, depth, multiplier) {
|
|
|
146
166
|
const onType = (0, graphql_1.typeFromAST)(walk.schema, fragment.typeCondition);
|
|
147
167
|
walk.activeFragments.add(name);
|
|
148
168
|
try {
|
|
149
|
-
walkSelectionSet(walk, fragment.selectionSet, onType ?? null, depth, multiplier);
|
|
169
|
+
walkSelectionSet(walk, fragment.selectionSet, onType ?? null, depth, multiplier, bound);
|
|
150
170
|
}
|
|
151
171
|
finally {
|
|
152
172
|
walk.activeFragments.delete(name);
|
|
@@ -188,6 +208,6 @@ function enforceDocumentProtection(schema, document, variableValues, protection,
|
|
|
188
208
|
maxDepth: 0,
|
|
189
209
|
cost: 0
|
|
190
210
|
};
|
|
191
|
-
walkSelectionSet(walk, operation.selectionSet, rootType ?? null, 1, 1);
|
|
211
|
+
walkSelectionSet(walk, operation.selectionSet, rootType ?? null, 1, 1, { kind: 'query' });
|
|
192
212
|
return { depth: walk.maxDepth, cost: walk.cost };
|
|
193
213
|
}
|