graphql-query-depth-limit-esm 2.0.0 → 2.0.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/README.md CHANGED
@@ -6,11 +6,24 @@
6
6
  [![npm version](https://img.shields.io/npm/v/graphql-query-depth-limit-esm?logo=npm)](https://www.npmjs.com/package/graphql-query-depth-limit-esm)
7
7
  [![npm downloads](https://img.shields.io/npm/dm/graphql-query-depth-limit-esm?logo=npm)](https://www.npmjs.com/package/graphql-query-depth-limit-esm)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
9
- [![Node >=20](https://img.shields.io/badge/node-%3E%3D20-339933?logo=node.js&logoColor=white)](https://nodejs.org/)
9
+ [![Node >=22](https://img.shields.io/badge/node-%3E%3D22-339933?logo=node.js&logoColor=white)](https://nodejs.org/)
10
+ [![Provenance](https://img.shields.io/badge/provenance-verified-brightgreen?logo=npm)](https://www.npmjs.com/package/graphql-query-depth-limit-esm)
10
11
 
11
12
  Production-ready GraphQL query depth limiting as a validation rule. Prevents denial-of-service attacks from deeply nested queries by enforcing a configurable maximum depth.
12
13
 
13
- Explore the library's internal function architecture with the **[interactive node-based visualization](https://lafittemehdy.github.io/graphql-query-depth-limit-esm/architecture.html)**.
14
+ **[Interactive demo](https://lafittemehdy.github.io/graphql-query-depth-limit-esm/)** — see depth analysis in action with preset queries, an AST tree viewer, and a depth gauge.
15
+
16
+ ## Interactive Demo
17
+
18
+ **[Try it live](https://lafittemehdy.github.io/graphql-query-depth-limit-esm/)** or run locally:
19
+
20
+ ```bash
21
+ cd examples/visualization
22
+ npm install
23
+ npm run dev
24
+ ```
25
+
26
+ Includes preset queries (simple lookups through deeply nested attacks), an AST tree viewer with depth badges, and a depth gauge with pass/fail verdict.
14
27
 
15
28
  ## Features
16
29
 
@@ -28,26 +41,31 @@ Explore the library's internal function architecture with the **[interactive nod
28
41
  ## Installation
29
42
 
30
43
  ```bash
31
- pnpm add graphql-query-depth-limit-esm graphql
44
+ npm install graphql-query-depth-limit-esm graphql
32
45
  ```
33
46
 
34
47
  ```bash
35
- npm install graphql-query-depth-limit-esm graphql
48
+ pnpm add graphql-query-depth-limit-esm graphql
36
49
  ```
37
50
 
38
51
  ```bash
39
52
  yarn add graphql-query-depth-limit-esm graphql
40
53
  ```
41
54
 
42
- ## Quick Start
55
+ ## Quickstart
43
56
 
44
57
  ```ts
45
- import { validate } from "graphql";
58
+ import { specifiedRules, validate } from "graphql";
46
59
  import { depthLimit } from "graphql-query-depth-limit-esm";
47
60
 
48
- const errors = validate(schema, document, [depthLimit(7, { useDirective: true })]);
61
+ const errors = validate(schema, document, [
62
+ ...specifiedRules,
63
+ depthLimit(7, { useDirective: true }),
64
+ ]);
49
65
  ```
50
66
 
67
+ When calling `validate()` directly, include `specifiedRules` unless you intentionally want to replace GraphQL's default validation rules.
68
+
51
69
  The recommended way to use `graphql-query-depth-limit-esm` is the `@depth` directive — a global maximum protects your API, while per-field overrides can tighten limits (and can opt into deeper nesting when `directiveMode: "override"` is enabled).
52
70
 
53
71
  ### Global Limit with Per-Field Overrides
@@ -85,7 +103,7 @@ Build the schema with the directive type defs, then enable directive support via
85
103
  ```ts
86
104
  import { makeExecutableSchema } from "@graphql-tools/schema";
87
105
  import { depthDirectiveTypeDefs, depthLimit } from "graphql-query-depth-limit-esm";
88
- import { validate } from "graphql";
106
+ import { specifiedRules, validate } from "graphql";
89
107
 
90
108
  const schema = makeExecutableSchema({
91
109
  typeDefs: [depthDirectiveTypeDefs, yourTypeDefs],
@@ -94,6 +112,7 @@ const schema = makeExecutableSchema({
94
112
 
95
113
  // Global max of 7 — @depth directives can tighten but never exceed this limit
96
114
  const errors = validate(schema, document, [
115
+ ...specifiedRules,
97
116
  depthLimit(7, { useDirective: true }),
98
117
  ]);
99
118
  ```
@@ -152,11 +171,11 @@ For straightforward global limiting without per-field overrides, call [`depthLim
152
171
 
153
172
  ```ts
154
173
  import { depthLimit } from "graphql-query-depth-limit-esm";
155
- import { validate } from "graphql";
174
+ import { specifiedRules, validate } from "graphql";
156
175
 
157
176
  // Reject queries deeper than 10 levels
158
177
  const rule = depthLimit(10);
159
- const errors = validate(schema, document, [rule]);
178
+ const errors = validate(schema, document, [...specifiedRules, rule]);
160
179
  ```
161
180
 
162
181
  ### With Apollo Server
@@ -295,7 +314,7 @@ Monitor query depths with an optional [`callback`](#depthcallback) that receives
295
314
 
296
315
  ```ts
297
316
  const rule = depthLimit(10, {}, (depths) => {
298
- // { "GetUser": 3, "ListPosts": 5, "anonymous": 2 }
317
+ // { "GetUser": 3, "ListPosts": 5, "[anonymous]": 2 }
299
318
  for (const [operation, depth] of Object.entries(depths)) {
300
319
  console.log(`Operation "${operation}" has depth ${depth}`);
301
320
  }
@@ -511,6 +530,17 @@ depthLimit(10, { directiveMode: "override", useDirective: true });
511
530
 
512
531
  **Impact:** This is transparent to most users. The only observable difference is that error messages may report the depth at the first violation rather than the deepest violation when multiple branches exceed the limit. If you need the true maximum depth, provide a callback.
513
532
 
533
+ ## Related Packages
534
+
535
+ This package is part of a suite of GraphQL security tools that work independently or together to protect your API:
536
+
537
+ | Package | Purpose |
538
+ |---|---|
539
+ | [`graphql-query-complexity-esm`](https://github.com/lafittemehdy/graphql-query-complexity-esm) | Complexity analysis — assign cost scores to fields and reject expensive queries |
540
+ | [`graphql-rate-limit-redis-esm`](https://github.com/lafittemehdy/graphql-rate-limit-redis-esm) | Rate limiting — Redis-backed per-field rate limiting via `@rateLimit` directive |
541
+
542
+ **Recommended layering:** Use depth limiting as a fast, cheap first gate, complexity analysis for fine-grained cost control, and rate limiting for per-client throttling.
543
+
514
544
  ## License
515
545
 
516
546
  [MIT](LICENSE)
package/dist/index.cjs CHANGED
@@ -22,15 +22,16 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  ERROR_CODES: () => ERROR_CODES,
24
24
  depthDirectiveTypeDefs: () => depthDirectiveTypeDefs,
25
- depthLimit: () => depthLimit
25
+ depthLimit: () => depthLimit,
26
+ isUnsafeRegExp: () => isUnsafeRegExp
26
27
  });
27
28
  module.exports = __toCommonJS(index_exports);
28
29
 
29
30
  // src/constants.ts
30
- var ERROR_CODES = {
31
+ var ERROR_CODES = Object.freeze({
31
32
  IGNORE_RULE_ERROR: "IGNORE_RULE_ERROR",
32
33
  QUERY_TOO_DEEP: "QUERY_TOO_DEEP"
33
- };
34
+ });
34
35
 
35
36
  // src/depth-limit.ts
36
37
  var import_graphql3 = require("graphql");
@@ -40,9 +41,10 @@ var import_graphql2 = require("graphql");
40
41
 
41
42
  // src/directives.ts
42
43
  var import_graphql = require("graphql");
43
- var depthDirectiveTypeDefs = `
44
- directive @depth(max: Int!) on FIELD_DEFINITION
45
- `;
44
+ var depthDirectiveTypeDefs = (
45
+ /* GraphQL */
46
+ `directive @depth(max: Int!) on FIELD_DEFINITION`
47
+ );
46
48
  function getDepthFromDirective(field) {
47
49
  if (!field?.astNode?.directives) {
48
50
  return void 0;
@@ -62,6 +64,83 @@ function getDepthFromDirective(field) {
62
64
  }
63
65
 
64
66
  // src/ignore.ts
67
+ var QUANTIFIER_CHARS = /* @__PURE__ */ new Set(["+", "*", "?"]);
68
+ var BRACE_QUANTIFIER = /^\{(\d+)(?:,(\d*))?\}/;
69
+ function isUnsafeRegExp(regex) {
70
+ const source = regex.source;
71
+ const length = source.length;
72
+ const groupStack = [];
73
+ for (let i = 0; i < length; i++) {
74
+ const char = source.charAt(i);
75
+ if (char === "\\") {
76
+ i++;
77
+ continue;
78
+ }
79
+ if (char === "[") {
80
+ while (i < length && source[i] !== "]") {
81
+ if (source[i] === "\\") i++;
82
+ i++;
83
+ }
84
+ continue;
85
+ }
86
+ if (char === "(") {
87
+ groupStack.push(false);
88
+ if (source[i + 1] === "?") {
89
+ i++;
90
+ if (source[i + 1] === "<" && (source[i + 2] === "=" || source[i + 2] === "!")) {
91
+ i += 2;
92
+ } else if (source[i + 1] === ":" || source[i + 1] === "=" || source[i + 1] === "!") {
93
+ i++;
94
+ }
95
+ }
96
+ continue;
97
+ }
98
+ if (char === ")") {
99
+ const groupHadQuantifier = groupStack.pop() ?? false;
100
+ if (groupHadQuantifier && isFollowedByQuantifier(source, i + 1, length)) {
101
+ return "nested quantifier: group with inner quantifier followed by outer quantifier";
102
+ }
103
+ if (groupStack.length > 0 && isFollowedByQuantifier(source, i + 1, length)) {
104
+ groupStack[groupStack.length - 1] = true;
105
+ }
106
+ continue;
107
+ }
108
+ if (QUANTIFIER_CHARS.has(char) || char === "{") {
109
+ if (char === "{") {
110
+ const match = BRACE_QUANTIFIER.exec(source.slice(i));
111
+ if (!match) continue;
112
+ const minStr = match[1] ?? "0";
113
+ const min = Number.parseInt(minStr, 10);
114
+ const fullMatch = match[0] ?? "";
115
+ const hasComma = fullMatch.includes(",");
116
+ if (!hasComma && min <= 1) continue;
117
+ if (hasComma && match[2] !== void 0 && match[2] !== "" && Number.parseInt(match[2], 10) <= 1)
118
+ continue;
119
+ }
120
+ if (groupStack.length > 0) {
121
+ groupStack[groupStack.length - 1] = true;
122
+ }
123
+ }
124
+ }
125
+ return null;
126
+ }
127
+ function isFollowedByQuantifier(source, pos, length) {
128
+ if (pos >= length) return false;
129
+ const char = source[pos];
130
+ if (char === "+" || char === "*") return true;
131
+ if (char === "{") {
132
+ const match = BRACE_QUANTIFIER.exec(source.slice(pos));
133
+ if (match) {
134
+ const minStr = match[1] ?? "0";
135
+ const min = Number.parseInt(minStr, 10);
136
+ const fullMatch = match[0] ?? "";
137
+ const hasComma = fullMatch.includes(",");
138
+ if (!hasComma && min <= 1) return false;
139
+ return true;
140
+ }
141
+ }
142
+ return false;
143
+ }
65
144
  function shouldIgnoreField(fieldName, ignore, caseInsensitive = false, introspectionMode = "typename") {
66
145
  if (introspectionMode === "all" && fieldName.startsWith("__")) {
67
146
  return true;
@@ -132,6 +211,7 @@ function calculateDepth(caches, config, fragments, maxDepth, node, parentType, s
132
211
  if (!frame.node.selectionSet) {
133
212
  continue;
134
213
  }
214
+ const nextFrames = [];
135
215
  for (const selection of frame.node.selectionSet.selections) {
136
216
  switch (selection.kind) {
137
217
  case import_graphql2.Kind.FIELD: {
@@ -213,7 +293,7 @@ function calculateDepth(caches, config, fragments, maxDepth, node, parentType, s
213
293
  deepestViolation = violation;
214
294
  }
215
295
  }
216
- stack.push({
296
+ nextFrames.push({
217
297
  currentDepth: newDepth,
218
298
  hasDirectiveLimit,
219
299
  ignoredFieldsOnPath,
@@ -237,7 +317,7 @@ function calculateDepth(caches, config, fragments, maxDepth, node, parentType, s
237
317
  const fragmentVisited = new Set(frame.visitedFragments);
238
318
  fragmentVisited.add(fragmentName);
239
319
  const parentType2 = fragment.typeCondition ? resolveTypeCondition(fragment.typeCondition.name.value, schema, frame.parentType) : frame.parentType;
240
- stack.push({
320
+ nextFrames.push({
241
321
  currentDepth: frame.currentDepth,
242
322
  hasDirectiveLimit: frame.hasDirectiveLimit,
243
323
  ignoredFieldsOnPath: frame.ignoredFieldsOnPath,
@@ -251,7 +331,7 @@ function calculateDepth(caches, config, fragments, maxDepth, node, parentType, s
251
331
  }
252
332
  case import_graphql2.Kind.INLINE_FRAGMENT: {
253
333
  const parentType2 = selection.typeCondition ? resolveTypeCondition(selection.typeCondition.name.value, schema, frame.parentType) : frame.parentType;
254
- stack.push({
334
+ nextFrames.push({
255
335
  currentDepth: frame.currentDepth,
256
336
  hasDirectiveLimit: frame.hasDirectiveLimit,
257
337
  ignoredFieldsOnPath: frame.ignoredFieldsOnPath,
@@ -265,13 +345,22 @@ function calculateDepth(caches, config, fragments, maxDepth, node, parentType, s
265
345
  }
266
346
  default: {
267
347
  const exhaustiveCheck = selection;
268
- throw new Error(`Unhandled selection kind: ${exhaustiveCheck.kind}`);
348
+ throwUnhandledSelectionKind(exhaustiveCheck);
269
349
  }
270
350
  }
271
351
  }
352
+ for (let index = nextFrames.length - 1; index >= 0; index--) {
353
+ const nextFrame = nextFrames[index];
354
+ if (nextFrame) {
355
+ stack.push(nextFrame);
356
+ }
357
+ }
272
358
  }
273
359
  return { depth: globalMaxDepth, violation: deepestViolation };
274
360
  }
361
+ function throwUnhandledSelectionKind(selection) {
362
+ throw new Error(`Unhandled selection kind: ${selection.kind}`);
363
+ }
275
364
  function collectInterfaces(type) {
276
365
  const interfaces = [];
277
366
  const seen = /* @__PURE__ */ new Set();
@@ -385,19 +474,13 @@ function assertBooleanOption(name, value) {
385
474
  function createValidationRule(maxDepth, options, callback) {
386
475
  const shortCircuit = options?.shortCircuit ?? callback == null;
387
476
  return function depthLimitValidationRule(context) {
388
- let anonymousCount = 0;
389
477
  const caches = createTraversalCaches();
390
478
  const document = context.getDocument();
391
- const depths = callback ? {} : void 0;
479
+ const depths = callback ? createDepthResultRecord() : void 0;
392
480
  const { fragments, operations } = extractDefinitions(document.definitions);
393
481
  const schema = context.getSchema() ?? void 0;
394
482
  const useDirective = Boolean(schema) && (options?.useDirective ?? false);
395
- const namedOperationNames = /* @__PURE__ */ new Set();
396
- for (const op of operations) {
397
- if (op.name?.value) {
398
- namedOperationNames.add(op.name.value);
399
- }
400
- }
483
+ const nextOperationName = createOperationNameAllocator(operations);
401
484
  const config = {
402
485
  caseInsensitiveIgnore: options?.caseInsensitiveIgnore ?? false,
403
486
  directiveMode: options?.directiveMode ?? "cap",
@@ -418,18 +501,7 @@ function createValidationRule(maxDepth, options, callback) {
418
501
  subscription: void 0
419
502
  };
420
503
  for (const operation of operations) {
421
- let operationName;
422
- if (operation.name?.value) {
423
- operationName = operation.name.value;
424
- } else {
425
- let candidate = anonymousCount === 0 ? "anonymous" : `anonymous_${anonymousCount}`;
426
- while (namedOperationNames.has(candidate)) {
427
- anonymousCount++;
428
- candidate = `anonymous_${anonymousCount}`;
429
- }
430
- operationName = candidate;
431
- anonymousCount++;
432
- }
504
+ const operationName = nextOperationName(operation);
433
505
  const rootType = rootTypeMap[operation.operation];
434
506
  let result;
435
507
  try {
@@ -475,7 +547,7 @@ function createValidationRule(maxDepth, options, callback) {
475
547
  }
476
548
  }
477
549
  if (callback && depths) {
478
- callback(depths);
550
+ callback({ ...depths });
479
551
  }
480
552
  return {};
481
553
  };
@@ -537,9 +609,53 @@ function normalizeIgnoreRules(ignore) {
537
609
  `Invalid ignore rule at index ${index}: expected string, RegExp, or function, received ${receivedType}.`
538
610
  );
539
611
  }
612
+ if (rule instanceof RegExp) {
613
+ const reason = isUnsafeRegExp(rule);
614
+ if (reason) {
615
+ throw new TypeError(
616
+ `Unsafe RegExp ignore rule at index ${index}: /${rule.source}/${rule.flags} \u2014 ${reason}. Use a simpler pattern to avoid catastrophic backtracking.`
617
+ );
618
+ }
619
+ }
540
620
  }
541
621
  return rules;
542
622
  }
623
+ function createDepthResultRecord() {
624
+ return /* @__PURE__ */ Object.create(null);
625
+ }
626
+ function createOperationNameAllocator(operations) {
627
+ const explicitNames = /* @__PURE__ */ new Set();
628
+ for (const operation of operations) {
629
+ if (operation.name?.value) {
630
+ explicitNames.add(operation.name.value);
631
+ }
632
+ }
633
+ const usedNames = /* @__PURE__ */ new Set();
634
+ const namedCounts = /* @__PURE__ */ new Map();
635
+ let anonymousCount = 0;
636
+ return (operation) => {
637
+ const explicitName = operation.name?.value;
638
+ if (explicitName) {
639
+ let suffix = namedCounts.get(explicitName) ?? 0;
640
+ let candidate2 = suffix === 0 ? explicitName : `${explicitName}_${suffix}`;
641
+ while (usedNames.has(candidate2) || suffix > 0 && explicitNames.has(candidate2)) {
642
+ suffix++;
643
+ candidate2 = `${explicitName}_${suffix}`;
644
+ }
645
+ namedCounts.set(explicitName, suffix + 1);
646
+ usedNames.add(candidate2);
647
+ return candidate2;
648
+ }
649
+ anonymousCount++;
650
+ let candidate = anonymousCount === 1 ? "[anonymous]" : `[anonymous:${anonymousCount}]`;
651
+ while (usedNames.has(candidate) || explicitNames.has(candidate)) {
652
+ anonymousCount++;
653
+ candidate = `[anonymous:${anonymousCount}]`;
654
+ }
655
+ usedNames.add(candidate);
656
+ return candidate;
657
+ };
658
+ }
543
659
  function setDepthResult(target, key, value) {
544
660
  Object.defineProperty(target, key, {
545
661
  configurable: true,
@@ -552,6 +668,7 @@ function setDepthResult(target, key, value) {
552
668
  0 && (module.exports = {
553
669
  ERROR_CODES,
554
670
  depthDirectiveTypeDefs,
555
- depthLimit
671
+ depthLimit,
672
+ isUnsafeRegExp
556
673
  });
557
674
  //# sourceMappingURL=index.cjs.map