@constructive-io/graphql-server 5.25.0 → 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.
|
@@ -16,6 +16,16 @@
|
|
|
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 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.
|
|
19
29
|
*/
|
|
20
30
|
import { errors } from '@constructive-io/errors';
|
|
21
31
|
import { ASSUMED_PAGE_SIZE } from '@constructive-io/express-context';
|
|
@@ -36,6 +46,14 @@ const reject = (error) => {
|
|
|
36
46
|
statusCode: error.http
|
|
37
47
|
});
|
|
38
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;
|
|
39
57
|
/** Arguments a connection field uses to size its page. */
|
|
40
58
|
const PAGE_SIZE_ARGS = ['first', 'last'];
|
|
41
59
|
/** A connection is any object type that carries Relay's `pageInfo`. */
|
|
@@ -71,19 +89,28 @@ const requestedPageSize = (args, variableValues) => {
|
|
|
71
89
|
* the walk only stops charging cost rather than raising its own error)
|
|
72
90
|
* @param depth - nesting level of these selections
|
|
73
91
|
* @param multiplier - rows the enclosing connections can return
|
|
92
|
+
* @param bound - which depth limit applies to these selections
|
|
74
93
|
*/
|
|
75
|
-
function walkSelectionSet(walk, selectionSet, parentType, depth, multiplier) {
|
|
76
|
-
if (
|
|
77
|
-
walk.maxDepth
|
|
78
|
-
|
|
79
|
-
|
|
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 }));
|
|
80
104
|
}
|
|
81
105
|
for (const selection of selectionSet.selections) {
|
|
82
106
|
if (selection.kind === Kind.FIELD) {
|
|
107
|
+
let childBound = bound;
|
|
83
108
|
if (selection.name.value === '__schema' || selection.name.value === '__type') {
|
|
84
109
|
if (!walk.protection.enableIntrospection) {
|
|
85
110
|
reject(errors.INTROSPECTION_DISABLED());
|
|
86
111
|
}
|
|
112
|
+
if (bound.kind === 'query')
|
|
113
|
+
childBound = { kind: 'introspection', base: depth };
|
|
87
114
|
}
|
|
88
115
|
const field = parentType && isObjectType(parentType)
|
|
89
116
|
? parentType.getFields()[selection.name.value]
|
|
@@ -110,7 +137,7 @@ function walkSelectionSet(walk, selectionSet, parentType, depth, multiplier) {
|
|
|
110
137
|
}
|
|
111
138
|
}
|
|
112
139
|
if (selection.selectionSet) {
|
|
113
|
-
walkSelectionSet(walk, selection.selectionSet, fieldType, depth + 1, childMultiplier);
|
|
140
|
+
walkSelectionSet(walk, selection.selectionSet, fieldType, depth + 1, childMultiplier, childBound);
|
|
114
141
|
}
|
|
115
142
|
continue;
|
|
116
143
|
}
|
|
@@ -119,7 +146,7 @@ function walkSelectionSet(walk, selectionSet, parentType, depth, multiplier) {
|
|
|
119
146
|
? typeFromAST(walk.schema, selection.typeCondition)
|
|
120
147
|
: parentType;
|
|
121
148
|
// An inline fragment is not a level of nesting of its own.
|
|
122
|
-
walkSelectionSet(walk, selection.selectionSet, onType ?? null, depth, multiplier);
|
|
149
|
+
walkSelectionSet(walk, selection.selectionSet, onType ?? null, depth, multiplier, bound);
|
|
123
150
|
continue;
|
|
124
151
|
}
|
|
125
152
|
if (selection.kind === Kind.FRAGMENT_SPREAD) {
|
|
@@ -135,7 +162,7 @@ function walkSelectionSet(walk, selectionSet, parentType, depth, multiplier) {
|
|
|
135
162
|
const onType = typeFromAST(walk.schema, fragment.typeCondition);
|
|
136
163
|
walk.activeFragments.add(name);
|
|
137
164
|
try {
|
|
138
|
-
walkSelectionSet(walk, fragment.selectionSet, onType ?? null, depth, multiplier);
|
|
165
|
+
walkSelectionSet(walk, fragment.selectionSet, onType ?? null, depth, multiplier, bound);
|
|
139
166
|
}
|
|
140
167
|
finally {
|
|
141
168
|
walk.activeFragments.delete(name);
|
|
@@ -177,6 +204,6 @@ export function enforceDocumentProtection(schema, document, variableValues, prot
|
|
|
177
204
|
maxDepth: 0,
|
|
178
205
|
cost: 0
|
|
179
206
|
};
|
|
180
|
-
walkSelectionSet(walk, operation.selectionSet, rootType ?? null, 1, 1);
|
|
207
|
+
walkSelectionSet(walk, operation.selectionSet, rootType ?? null, 1, 1, { kind: 'query' });
|
|
181
208
|
return { depth: walk.maxDepth, cost: walk.cost };
|
|
182
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
|
}
|
|
@@ -16,9 +16,27 @@
|
|
|
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 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.
|
|
19
29
|
*/
|
|
20
30
|
import { type RequestProtection } from '@constructive-io/express-context';
|
|
21
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;
|
|
22
40
|
export interface DocumentAnalysis {
|
|
23
41
|
/** Deepest field nesting in the operation. */
|
|
24
42
|
depth: number;
|
|
@@ -17,8 +17,19 @@
|
|
|
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 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.
|
|
20
30
|
*/
|
|
21
31
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.INTROSPECTION_MAX_DEPTH = void 0;
|
|
22
33
|
exports.enforceDocumentProtection = enforceDocumentProtection;
|
|
23
34
|
const errors_1 = require("@constructive-io/errors");
|
|
24
35
|
const express_context_1 = require("@constructive-io/express-context");
|
|
@@ -39,6 +50,14 @@ const reject = (error) => {
|
|
|
39
50
|
statusCode: error.http
|
|
40
51
|
});
|
|
41
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;
|
|
42
61
|
/** Arguments a connection field uses to size its page. */
|
|
43
62
|
const PAGE_SIZE_ARGS = ['first', 'last'];
|
|
44
63
|
/** A connection is any object type that carries Relay's `pageInfo`. */
|
|
@@ -74,19 +93,28 @@ const requestedPageSize = (args, variableValues) => {
|
|
|
74
93
|
* the walk only stops charging cost rather than raising its own error)
|
|
75
94
|
* @param depth - nesting level of these selections
|
|
76
95
|
* @param multiplier - rows the enclosing connections can return
|
|
96
|
+
* @param bound - which depth limit applies to these selections
|
|
77
97
|
*/
|
|
78
|
-
function walkSelectionSet(walk, selectionSet, parentType, depth, multiplier) {
|
|
79
|
-
if (
|
|
80
|
-
walk.maxDepth
|
|
81
|
-
|
|
82
|
-
|
|
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 }));
|
|
83
108
|
}
|
|
84
109
|
for (const selection of selectionSet.selections) {
|
|
85
110
|
if (selection.kind === graphql_1.Kind.FIELD) {
|
|
111
|
+
let childBound = bound;
|
|
86
112
|
if (selection.name.value === '__schema' || selection.name.value === '__type') {
|
|
87
113
|
if (!walk.protection.enableIntrospection) {
|
|
88
114
|
reject(errors_1.errors.INTROSPECTION_DISABLED());
|
|
89
115
|
}
|
|
116
|
+
if (bound.kind === 'query')
|
|
117
|
+
childBound = { kind: 'introspection', base: depth };
|
|
90
118
|
}
|
|
91
119
|
const field = parentType && (0, graphql_1.isObjectType)(parentType)
|
|
92
120
|
? parentType.getFields()[selection.name.value]
|
|
@@ -113,7 +141,7 @@ function walkSelectionSet(walk, selectionSet, parentType, depth, multiplier) {
|
|
|
113
141
|
}
|
|
114
142
|
}
|
|
115
143
|
if (selection.selectionSet) {
|
|
116
|
-
walkSelectionSet(walk, selection.selectionSet, fieldType, depth + 1, childMultiplier);
|
|
144
|
+
walkSelectionSet(walk, selection.selectionSet, fieldType, depth + 1, childMultiplier, childBound);
|
|
117
145
|
}
|
|
118
146
|
continue;
|
|
119
147
|
}
|
|
@@ -122,7 +150,7 @@ function walkSelectionSet(walk, selectionSet, parentType, depth, multiplier) {
|
|
|
122
150
|
? (0, graphql_1.typeFromAST)(walk.schema, selection.typeCondition)
|
|
123
151
|
: parentType;
|
|
124
152
|
// An inline fragment is not a level of nesting of its own.
|
|
125
|
-
walkSelectionSet(walk, selection.selectionSet, onType ?? null, depth, multiplier);
|
|
153
|
+
walkSelectionSet(walk, selection.selectionSet, onType ?? null, depth, multiplier, bound);
|
|
126
154
|
continue;
|
|
127
155
|
}
|
|
128
156
|
if (selection.kind === graphql_1.Kind.FRAGMENT_SPREAD) {
|
|
@@ -138,7 +166,7 @@ function walkSelectionSet(walk, selectionSet, parentType, depth, multiplier) {
|
|
|
138
166
|
const onType = (0, graphql_1.typeFromAST)(walk.schema, fragment.typeCondition);
|
|
139
167
|
walk.activeFragments.add(name);
|
|
140
168
|
try {
|
|
141
|
-
walkSelectionSet(walk, fragment.selectionSet, onType ?? null, depth, multiplier);
|
|
169
|
+
walkSelectionSet(walk, fragment.selectionSet, onType ?? null, depth, multiplier, bound);
|
|
142
170
|
}
|
|
143
171
|
finally {
|
|
144
172
|
walk.activeFragments.delete(name);
|
|
@@ -180,6 +208,6 @@ function enforceDocumentProtection(schema, document, variableValues, protection,
|
|
|
180
208
|
maxDepth: 0,
|
|
181
209
|
cost: 0
|
|
182
210
|
};
|
|
183
|
-
walkSelectionSet(walk, operation.selectionSet, rootType ?? null, 1, 1);
|
|
211
|
+
walkSelectionSet(walk, operation.selectionSet, rootType ?? null, 1, 1, { kind: 'query' });
|
|
184
212
|
return { depth: walk.maxDepth, cost: walk.cost };
|
|
185
213
|
}
|