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 +41 -11
- package/dist/index.cjs +149 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -8
- package/dist/index.d.ts +41 -8
- package/dist/index.js +147 -31
- package/dist/index.js.map +1 -1
- package/package.json +12 -7
package/README.md
CHANGED
|
@@ -6,11 +6,24 @@
|
|
|
6
6
|
[](https://www.npmjs.com/package/graphql-query-depth-limit-esm)
|
|
7
7
|
[](https://www.npmjs.com/package/graphql-query-depth-limit-esm)
|
|
8
8
|
[](LICENSE)
|
|
9
|
-
[](https://nodejs.org/)
|
|
10
|
+
[](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
|
-
|
|
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
|
-
|
|
44
|
+
npm install graphql-query-depth-limit-esm graphql
|
|
32
45
|
```
|
|
33
46
|
|
|
34
47
|
```bash
|
|
35
|
-
|
|
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
|
-
##
|
|
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, [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ?
|
|
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
|
|
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
|
-
|
|
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
|