@blamejs/core 0.18.50 → 0.18.53

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/lib/guard-yaml.js CHANGED
@@ -68,6 +68,7 @@
68
68
  */
69
69
 
70
70
  var codepointClass = require("./codepoint-class");
71
+ var yamlLex = require("./yaml-lex");
71
72
  var lazyRequire = require("./lazy-require");
72
73
  var gateContract = require("./gate-contract");
73
74
  var C = require("./constants");
@@ -95,11 +96,11 @@ var SAFE_CORE_TAGS = Object.freeze([
95
96
  "!!binary", "!!timestamp", "!!merge",
96
97
  ]);
97
98
 
98
- // Characters that may precede an anchor declaration (`&name`) or an alias
99
- // reference (`*name`) the start of the document, whitespace, or one of the
100
- // structural characters a name can follow.
101
- var ANCHOR_LEAD_CHARS = ":-";
102
- var ALIAS_LEAD_CHARS = ":-[{,";
99
+ // There is no table of characters an anchor or alias is allowed to follow any
100
+ // more. Two of them existed, one per sigil, and each was a guess at YAML's
101
+ // grammar written as a character class: they admitted a sigil in the middle of
102
+ // a plain scalar and excluded the compact flow form `{"a":&anchor v}` that YAML
103
+ // permits. Position is decided once, by the shared lexer's mask.
103
104
 
104
105
  // The YAML 1.1 boolean-shaped tokens that make an unquoted scalar change type.
105
106
  // `true` and `false` are valid YAML 1.2 booleans and are not flagged; these
@@ -119,14 +120,17 @@ function _isSpace(cc) {
119
120
 
120
121
  // Every `&name` / `*name` in the document, given the sigil and the characters
121
122
  // that may precede it. Returns the names, in order.
122
- function _collectSigilNames(text, sigil, leadChars) {
123
+ // `text` is the MASK, in which a sigil survives only where it opens a node, so
124
+ // there is no test on the preceding character here at all. That judgement used
125
+ // to be a hand-written list of characters a sigil was allowed to follow, kept
126
+ // separately for anchors and for aliases, and it was wrong in both directions:
127
+ // it admitted a `&` in the middle of a plain scalar, and it had no way to allow
128
+ // `{"a":&anchor v}`, where YAML's JSON compatibility lets a quoted key take its
129
+ // colon with no space after it. One scanner decides position now.
130
+ function _collectSigilNames(text, sigil) {
123
131
  var out = [];
124
132
  for (var i = 0; i < text.length; i += 1) {
125
133
  if (text.charAt(i) !== sigil) continue;
126
- if (i > 0) {
127
- var lead = text.charCodeAt(i - 1);
128
- if (!_isSpace(lead) && leadChars.indexOf(text.charAt(i - 1)) === -1) continue;
129
- }
130
134
  if (!_isNameStart(text.charCodeAt(i + 1))) continue;
131
135
  var end = i + 2;
132
136
  while (end < text.length && _isNameChar(text.charCodeAt(end))) end += 1;
@@ -325,9 +329,11 @@ function _scanTags(text) {
325
329
  var tags = [];
326
330
  for (var i = 0; i < text.length; i += 1) {
327
331
  if (text.charAt(i) !== "!") continue;
328
- var atStart = i === 0 ||
329
- codepointClass.inRanges(text.charCodeAt(i - 1), codepointClass.WHITESPACE_RANGES);
330
- if (!atStart) continue;
332
+ // No "preceded by whitespace" test. That WAS the original defect, and once
333
+ // the mask decides position it is not merely redundant but harmful: in
334
+ // `{"a":!!python/object x}` the character before the tag is the colon of a
335
+ // JSON-style key, so the test skipped it and a deserialization tag reached
336
+ // both screens unreported. Position is the mask's question now.
331
337
  var nameAt = text.charAt(i + 1) === "!" ? i + 2 : i + 1;
332
338
  if (!codepointClass.isAsciiLetter(text.charCodeAt(nameAt))) continue;
333
339
  var end = nameAt + 1;
@@ -354,8 +360,22 @@ function _detectIssues(input, opts) {
354
360
  if (pre.done) return pre.issues;
355
361
  var issues = pre.issues;
356
362
 
363
+ // Sigil scans run against the MASK, not the source. A `!`, `&` or `*` means
364
+ // what it looks like only at a node start, and the previous rule — "after
365
+ // whitespace" — cannot tell a node start from the middle of a scalar. It
366
+ // reported a tag for the bang in a comment, in a quoted string, in a block
367
+ // scalar's shell script, and in ordinary prose: `x: 1 # note !bang` was
368
+ // refused. The mask is index-aligned with the source and the same length, so
369
+ // every location and line number reported below is still the source's.
370
+ //
371
+ // Only the three sigil scans use it. The value-shaped detectors further down
372
+ // (the Norway problem, leading zeros, merge keys) are asking about scalar
373
+ // CONTENT, which is exactly what the mask removes, so they keep reading the
374
+ // source.
375
+ var masked = yamlLex.maskNonStructural(input);
376
+
357
377
  // 1. Tag-injection scan.
358
- var tagHits = _scanTags(input);
378
+ var tagHits = _scanTags(masked);
359
379
  for (var ti = 0; ti < tagHits.length; ti += 1) {
360
380
  var t = tagHits[ti];
361
381
  if (t.kind === "dangerous") {
@@ -392,8 +412,8 @@ function _detectIssues(input, opts) {
392
412
  }
393
413
 
394
414
  // 2. Anchor / alias recursion scan.
395
- var anchors = _collectSigilNames(input, "&", ANCHOR_LEAD_CHARS);
396
- var aliases = _collectSigilNames(input, "*", ALIAS_LEAD_CHARS);
415
+ var anchors = _collectSigilNames(masked, "&");
416
+ var aliases = _collectSigilNames(masked, "*");
397
417
  if (anchors.length > opts.maxAnchors) {
398
418
  issues.push({
399
419
  kind: "anchor-cap", severity: "high",
@@ -531,6 +551,24 @@ function _mappingEntryAt(line) {
531
551
  return null;
532
552
  }
533
553
 
554
+ // The column the line's content starts in.
555
+ function _indentOfLine(line) {
556
+ var i = 0;
557
+ while (i < line.length && _isSpace(line.charCodeAt(i))) i += 1;
558
+ return i;
559
+ }
560
+
561
+ // The indent of a sequence dash opening this line, or -1 when the line does not
562
+ // open a sequence item. A dash counts only when it stands alone as a token: `-`
563
+ // at end of line, or followed by a space. `-quux` and `-1` are scalars.
564
+ function _sequenceDashIndent(line) {
565
+ var i = 0;
566
+ while (i < line.length && _isSpace(line.charCodeAt(i))) i += 1;
567
+ if (i >= line.length || line.charAt(i) !== "-") return -1;
568
+ if (i + 1 < line.length && !_isSpace(line.charCodeAt(i + 1))) return -1;
569
+ return i;
570
+ }
571
+
534
572
  // Is the line blank, or a comment?
535
573
  function _isCommentLine(line) {
536
574
  var i = 0;
@@ -544,19 +582,117 @@ function _detectDuplicateKeysYaml(text) {
544
582
  var dups = Object.create(null);
545
583
  var lines = _splitLines(text);
546
584
  var indentScopes = Object.create(null);
585
+ // The sequence items currently open, outermost first, each with the column
586
+ // its first key sat in. Every key of an item at its top level is filed under
587
+ // one scope whatever column it is written in, because it is one mapping.
588
+ //
589
+ // A STACK, because sequences nest. Holding the innermost item in a pair of
590
+ // variables let a nested sequence overwrite its parent's scope and never give
591
+ // it back, so a key repeated in the OUTER item after the nested one closed
592
+ // was filed somewhere else and went unreported:
593
+ //
594
+ // - a: 1
595
+ // inner:
596
+ // - x: 1
597
+ // a: 2 <- a duplicate of the first `a`, and it was missed
598
+ //
599
+ // The same shape as the defect this detector reports, one level up: a single
600
+ // slot standing in for something there can be several of.
601
+ var itemStack = [];
547
602
  for (var i = 0; i < lines.length; i += 1) {
548
603
  var line = lines[i];
549
604
  if (line.length === 0 || _isCommentLine(line)) continue;
605
+ // A sequence item OPENS A NEW MAPPING, so it ends the previous item's and
606
+ // everything nested inside it. That is decided from the DASH, before
607
+ // anything else, because a line may be a sequence item and carry no mapping
608
+ // entry of its own:
609
+ //
610
+ // steps:
611
+ // -
612
+ // p: 1
613
+ // -
614
+ // p: 2
615
+ //
616
+ // A dash alone has no `key: value` on it, so a reset that waited for one
617
+ // never ran and the second item's `p` was still read as a duplicate of the
618
+ // first's. The boundary is the dash; whether the item writes its first key
619
+ // beside it or underneath it is a matter of layout.
620
+ var dashAt = _sequenceDashIndent(line);
621
+ // An item is over once the document comes back out to its dash's column or
622
+ // further left. That is true of the next item's own dash as much as of a
623
+ // key belonging to the enclosing mapping, so it is measured here, before
624
+ // this line is classified at all.
625
+ var lineIndent = dashAt >= 0 ? dashAt : _indentOfLine(line);
626
+ while (itemStack.length &&
627
+ itemStack[itemStack.length - 1].dash >= lineIndent) itemStack.pop();
628
+ if (dashAt >= 0) {
629
+ Object.keys(indentScopes).forEach(function (k) {
630
+ if (Number(k) > dashAt) delete indentScopes[k];
631
+ });
632
+ // keyIndent is set by this item's first key, wherever it is written.
633
+ itemStack.push({ dash: dashAt, keyIndent: -1 });
634
+ }
550
635
  var entry = _mappingEntryAt(line);
551
636
  if (!entry) continue;
552
637
  var indent = entry.indent;
553
638
  var key = entry.key.trim();
554
- if (key.charAt(0) === "-" || key.charAt(0) === "[" || key.charAt(0) === "{") continue;
555
- if (!indentScopes[indent]) indentScopes[indent] = Object.create(null);
556
- if (indentScopes[indent][key]) dups[key] = true;
557
- else indentScopes[indent][key] = true;
639
+ if (key.charAt(0) === "[" || key.charAt(0) === "{") continue;
640
+ // The key written INLINE with the dash belongs to the item's mapping, and
641
+ // sits at the indent AFTER the dash and its space — so that is the scope it
642
+ // is registered in, alongside the keys written underneath it. It used to be
643
+ // skipped entirely, which meant repeating it went unreported.
644
+ var dash = key.charAt(0) === "-" &&
645
+ (key.length === 1 || _isSpace(key.charCodeAt(1)));
646
+ var scopeAt = indent;
647
+ if (dash) {
648
+ var after = 1;
649
+ while (after < key.length && _isSpace(key.charCodeAt(after))) after += 1;
650
+ key = key.slice(after).trim();
651
+ // `- ` alone, or `- - x`: no inline key of this item's own to register.
652
+ if (!key || key.charAt(0) === "-" || key.charAt(0) === "[" ||
653
+ key.charAt(0) === "{") continue;
654
+ scopeAt = indent + after;
655
+ }
656
+ // The item's mapping is ONE mapping however its keys are laid out, so the
657
+ // scope it is tracked under must not depend on spacing. `- a: 1` puts its
658
+ // inline key at column 4 while the key written under it sits at column 2,
659
+ // and keying on the raw column filed them separately — so a repeat across
660
+ // those two lines went unreported, which is exactly the smuggling shape
661
+ // this detector exists for (one parser reads two keys, another reads one).
662
+ //
663
+ // The item's top level is every key deeper than the dash and no deeper than
664
+ // the first key it saw. Anything past that is genuinely nested and keeps
665
+ // its own column, so `- a: 1` / ` b:` / ` a: 2` is still not a
666
+ // duplicate.
667
+ // Half a column past the dash: a number, so the pruning comparisons below
668
+ // and at the dash keep working unchanged, and one that sorts between the
669
+ // dash and anything nested inside the item.
670
+ var item = itemStack.length ? itemStack[itemStack.length - 1] : null;
671
+ if (item && scopeAt > item.dash) {
672
+ // The item's top level is every key no deeper than the SHALLOWEST key it
673
+ // has shown, and the bound moves down as shallower ones appear. Two
674
+ // failures pinned this from opposite sides:
675
+ //
676
+ // - a: 1 the inline key sits at column 4 because of the extra
677
+ // b: spacing, but the item's mapping is written at 2. If
678
+ // a: 2 4 is taken as the bound, the NESTED `a` at 4 counts
679
+ // as top level and reads as a duplicate.
680
+ //
681
+ // - a: here the inline key IS the bound, at 2. If it sets
682
+ // x: 1 nothing, the first nested key at 4 becomes the bound
683
+ // a: 2 and the nested `a` reads as a duplicate instead.
684
+ //
685
+ // So the inline key establishes the bound and a later, shallower key
686
+ // lowers it. Extra spacing after the indicator is presentation; the
687
+ // shallowest key is the structure.
688
+ if (item.keyIndent === -1 || scopeAt < item.keyIndent) item.keyIndent = scopeAt;
689
+ if (scopeAt <= item.keyIndent) scopeAt = item.dash + 0.5;
690
+ }
691
+ if (!indentScopes[scopeAt]) indentScopes[scopeAt] = Object.create(null);
692
+ if (indentScopes[scopeAt][key]) dups[key] = true;
693
+ else indentScopes[scopeAt][key] = true;
558
694
  Object.keys(indentScopes).forEach(function (k) {
559
- if (Number(k) > indent) delete indentScopes[k];
695
+ if (Number(k) > scopeAt) delete indentScopes[k];
560
696
  });
561
697
  }
562
698
  return Object.keys(dups);
@@ -141,10 +141,52 @@ function _unescapePointerToken(t) { return t.replace(/~1/g, "/").replace(/~0/g,
141
141
 
142
142
  // --- registry: indexes every subschema by canonical URI + anchors ---
143
143
 
144
- function _Registry() { this.schemas = {}; this.dynamicAnchors = {}; this.baseByNode = new Map(); }
144
+ // A ceiling on how many subschema POSITIONS one registry will index, across
145
+ // every document added to it.
146
+ //
147
+ // This is the total-work half of the bound, and it is a different guarantee
148
+ // from a depth ceiling: depth limits how far one path runs, and says nothing
149
+ // about how many paths there are. The walk below cannot use identity tracking
150
+ // instead, because it indexes each node by `base#pointer` and a shared object
151
+ // legitimately has one entry per pointer that reaches it — skipping the second
152
+ // visit would drop a `$ref` target rather than save work. So what gets bounded
153
+ // is the number of entries, which is the thing that actually grows.
154
+ //
155
+ // A schema graph that reaches one object through a branching position —
156
+ // `anyOf: [shared, shared]`, which is what building a schema out of reused
157
+ // JavaScript objects produces, and which needs no `$ref` — has 2^depth pointers
158
+ // over a handful of objects. A cyclic one has no end at all. Neither is refused
159
+ // by a depth cap.
160
+ //
161
+ // Far above any authored schema: the JSON Schema meta-schema is on the order of
162
+ // a hundred positions, and an OpenAPI document of a thousand endpoints is tens
163
+ // of thousands. A schema that reaches this is not one someone wrote out.
164
+ var MAX_REGISTRY_NODES = 100000; // subschema positions, not bytes
165
+
166
+ // How deep a schema may nest before it is refused. This is the OTHER half of
167
+ // the bound, and it is not the node ceiling in disguise: a chain ten thousand
168
+ // levels deep is ten thousand positions, far under that ceiling, and still runs
169
+ // the JavaScript stack out. What comes back then is `RangeError: Maximum call
170
+ // stack size exceeded` — the engine's error, which says nothing about the
171
+ // schema and is not catchable as a JsonSchemaError.
172
+ //
173
+ // It THROWS rather than stopping the walk. A walk that silently returns at the
174
+ // ceiling leaves a node looking examined when its descendants were skipped,
175
+ // which is how a pattern below one escapes screening entirely. Refusing is the
176
+ // only reading that cannot be mistaken for completion.
177
+ //
178
+ // Well above anything authored — the JSON Schema meta-schema nests about a
179
+ // dozen levels and an OpenAPI document a few dozen — and well below where the
180
+ // stack gives out.
181
+ var MAX_SCHEMA_NESTING = 1000; // nesting levels, not bytes
182
+
183
+ function _Registry() {
184
+ this.schemas = {}; this.dynamicAnchors = {}; this.baseByNode = new Map();
185
+ this.nodesIndexed = 0;
186
+ }
145
187
 
146
188
  _Registry.prototype.add = function (schema, baseUri) {
147
- this._walk(schema, baseUri || "", "");
189
+ this._walk(schema, baseUri || "", "", null, 0);
148
190
  // A document retrieved from URI X is addressable by X even when its own
149
191
  // $id is a different (canonical) URI — register the retrieval URI too.
150
192
  if (baseUri && (_isObject(schema) || typeof schema === "boolean")) {
@@ -156,10 +198,43 @@ _Registry.prototype.add = function (schema, baseUri) {
156
198
  // Walk a schema document, registering $id base changes, $anchor and
157
199
  // $dynamicAnchor names, and indexing every subschema by its base URI +
158
200
  // JSON-pointer fragment.
159
- _Registry.prototype._walk = function (node, baseUri, pointer) {
201
+ _Registry.prototype._walk = function (node, baseUri, pointer, path, depth) {
160
202
  if (!_isObject(node) && typeof node !== "boolean") return;
203
+ if (depth > MAX_SCHEMA_NESTING) {
204
+ throw new JsonSchemaError("json-schema/schema-too-deep",
205
+ "jsonSchema: schema nests deeper than " + MAX_SCHEMA_NESTING +
206
+ " levels — deeper than the walk can index without exhausting the stack");
207
+ }
208
+ this.nodesIndexed += 1;
209
+ if (this.nodesIndexed > MAX_REGISTRY_NODES) {
210
+ throw new JsonSchemaError("json-schema/schema-too-large",
211
+ "jsonSchema: schema indexes more than " + MAX_REGISTRY_NODES +
212
+ " subschema positions — a shared or cyclic schema graph reaches one " +
213
+ "object through many pointers; give the reused subschema an $id and " +
214
+ "reference it with $ref instead of embedding the same object twice");
215
+ }
161
216
  if (typeof node === "boolean") { this.schemas[baseUri + "#" + pointer] = node; return; }
162
217
 
218
+ // A node already on the ACTIVE PATH is a cycle, and a cycle with one
219
+ // reference per level never reaches the ceiling above: it does not branch, so
220
+ // it recurses straight down and exhausts the JavaScript stack while the count
221
+ // is still in the thousands, surfacing as a RangeError from the engine rather
222
+ // than the refusal this validator documents.
223
+ //
224
+ // The test is the path and not a depth cap, because deep is not the same as
225
+ // cyclic: a schema nesting four hundred levels with no cycle and no `$ref`
226
+ // compiles today, and a depth cap would refuse it — under a `ref-loop` code
227
+ // naming a reference chain it does not have. The path set answers the
228
+ // question actually being asked, and leaves depth alone.
229
+ path = path || new Set();
230
+ var isOwnAncestor = path.has(node);
231
+ if (isOwnAncestor) {
232
+ throw new JsonSchemaError("json-schema/ref-loop",
233
+ "jsonSchema: schema contains a cycle — a subschema is its own ancestor, " +
234
+ "so indexing it has no end; break the cycle with $id + $ref");
235
+ }
236
+ path.add(node);
237
+
163
238
  var thisBase = baseUri;
164
239
  if (typeof node.$id === "string") {
165
240
  thisBase = _resolveUri(node.$id, baseUri);
@@ -191,7 +266,7 @@ _Registry.prototype._walk = function (node, baseUri, pointer) {
191
266
  // Recurse. Keywords whose values are schemas vs maps-of-schemas vs
192
267
  // arrays-of-schemas are walked with the right shape.
193
268
  var self = this;
194
- function child(key, sub, ptr) { self._walk(sub, thisBase, ptr); }
269
+ function child(key, sub, ptr) { self._walk(sub, thisBase, ptr, path, depth + 1); }
195
270
  SCHEMA_KEYWORDS.forEach(function (k) {
196
271
  if (node[k] !== undefined) child(k, node[k], pointer + "/" + k);
197
272
  });
@@ -207,6 +282,12 @@ _Registry.prototype._walk = function (node, baseUri, pointer) {
207
282
  node[k].forEach(function (sub, idx) { child(k, sub, pointer + "/" + k + "/" + idx); });
208
283
  }
209
284
  });
285
+
286
+ // Off the active path on the way out. A node reached again through a SIBLING
287
+ // branch is shared rather than cyclic, and indexing it under its second
288
+ // pointer is the correct thing to do — leaving it in the set would call that
289
+ // a cycle and refuse a schema that merely reuses a subschema.
290
+ path.delete(node);
210
291
  };
211
292
 
212
293
  _Registry.prototype.resolve = function (uri) {
@@ -255,7 +336,13 @@ var SCHEMA_KEYWORDS = ["additionalProperties", "propertyNames", "items",
255
336
  "unevaluatedProperties"];
256
337
  var SCHEMA_MAP_KEYWORDS = ["$defs", "definitions", "properties",
257
338
  "patternProperties", "dependentSchemas"];
258
- var SCHEMA_ARRAY_KEYWORDS = ["allOf", "anyOf", "oneOf", "prefixItems"];
339
+ // `items` appears in BOTH lists on purpose. Draft 2020-12 makes it a single
340
+ // schema, and the legacy tuple form makes it an array of them; each branch
341
+ // checks the type it wants, so exactly one of them walks any given `items`.
342
+ // Listed only as a single schema, an array-valued one was dropped at the
343
+ // `_isObject` test at the top of the walk and never indexed at all — so the
344
+ // subschemas inside a tuple were unreachable by `$ref`, and unbudgeted.
345
+ var SCHEMA_ARRAY_KEYWORDS = ["allOf", "anyOf", "oneOf", "prefixItems", "items"];
259
346
 
260
347
  module.exports = _buildModule();
261
348
 
@@ -315,25 +402,55 @@ var _SUBSCHEMA_MAP_KEYS = ["$defs", "definitions", "dependentSchemas",
315
402
 
316
403
  // Walk the subschema positions for the two keywords that carry a regular
317
404
  // expression, and hand each to the same screen the compile site uses.
318
- // Depth-bounded by the ceiling the validator applies to `$ref` chains, so a
319
- // deeply nested schema cannot spend the walk instead of the match.
320
- function _screenPatterns(node, depth) {
321
- depth = depth || 0;
322
- if (depth > MAX_REF_DEPTH || !_isObject(node)) return;
405
+ //
406
+ // Every object is visited ONCE, tracked by identity. The depth ceiling below is
407
+ // not a substitute for that and never was: it limits how far a single path runs
408
+ // and says nothing about how many paths there are. A schema graph reaching one
409
+ // object through a branching position — `anyOf: [shared, shared]`, which is what
410
+ // building a schema in JavaScript out of reused subschema objects produces, and
411
+ // which needs no `$ref` — was walked once per path. Twenty-one objects cost
412
+ // 2.6 seconds, doubling per added level, against a cap that allows 256; a cyclic
413
+ // schema never returned. This screen exists so a pattern cannot make the
414
+ // validator hang, and SECURITY.md promises a screen costs the length of its
415
+ // input and never a function of its shape, so a walk that is itself a function
416
+ // of shape contradicts the thing it was added to guarantee.
417
+ //
418
+ // Identity tracking bounds the total work by the number of distinct objects,
419
+ // which is the length of the schema — so the promise holds without a separate
420
+ // node budget.
421
+ //
422
+ // The nesting ceiling here THROWS, and that distinction is the whole point. A
423
+ // ceiling that silently stopped walking would leave a node looking examined
424
+ // when its descendants were skipped, so a later and shallower path would find
425
+ // it already marked and skip it too — and a catastrophic pattern below it would
426
+ // never be screened at all, which is the opposite of what this walk is for.
427
+ // Refusing cannot be mistaken for completion.
428
+ function _screenPatterns(node, seen, depth) {
429
+ if (!_isObject(node)) return;
430
+ if (depth > MAX_SCHEMA_NESTING) {
431
+ throw new JsonSchemaError("json-schema/schema-too-deep",
432
+ "jsonSchema: schema nests deeper than " + MAX_SCHEMA_NESTING +
433
+ " levels — deeper than the pattern screen can walk without exhausting " +
434
+ "the stack");
435
+ }
436
+ seen = seen || new WeakSet();
437
+ if (seen.has(node)) return;
438
+ seen.add(node);
323
439
 
324
440
  if (typeof node.pattern === "string") _compileRegex(node.pattern);
325
441
  if (_isObject(node.patternProperties)) {
326
442
  Object.keys(node.patternProperties).forEach(function (p) { _compileRegex(p); });
327
443
  }
328
444
 
329
- _SUBSCHEMA_KEYS.forEach(function (k) { _screenPatterns(node[k], depth + 1); });
445
+ var next = (depth || 0) + 1;
446
+ _SUBSCHEMA_KEYS.forEach(function (k) { _screenPatterns(node[k], seen, next); });
330
447
  _SUBSCHEMA_LIST_KEYS.forEach(function (k) {
331
448
  if (!Array.isArray(node[k])) return;
332
- node[k].forEach(function (sub) { _screenPatterns(sub, depth + 1); });
449
+ node[k].forEach(function (sub) { _screenPatterns(sub, seen, next); });
333
450
  });
334
451
  _SUBSCHEMA_MAP_KEYS.forEach(function (k) {
335
452
  if (!_isObject(node[k])) return;
336
- Object.keys(node[k]).forEach(function (name) { _screenPatterns(node[k][name], depth + 1); });
453
+ Object.keys(node[k]).forEach(function (name) { _screenPatterns(node[k][name], seen, next); });
337
454
  });
338
455
  }
339
456