@kumwe/studio-core 0.1.0-alpha.7 → 0.1.0-alpha.8

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.
@@ -1,34 +1,62 @@
1
+ import { canonicalStringify } from './canonical.js';
1
2
  import { compileProfileSchema } from './profile-validator.js';
2
3
  const DRAFT_2020_12 = 'https://json-schema.org/draft/2020-12/schema';
3
4
  const MAX_ALTERNATIVES = 64;
5
+ const MAX_DESCRIPTION_LENGTH = 10_000;
4
6
  const MAX_ENUM_MEMBERS = 1_024;
7
+ const MAX_EXAMPLES = 100;
5
8
  const MAX_JSON_DEPTH = 64;
6
9
  const MAX_JSON_ITEMS = 10_000;
7
10
  const MAX_JSON_PROPERTIES = 1_000;
8
11
  const MAX_REFERENCES = 128;
12
+ const MAX_REFERENCE_LENGTH = 500;
9
13
  const MAX_SCHEMA_BYTES = 262_144;
10
14
  const MAX_SCHEMA_DEPTH = 32;
11
15
  const MAX_SCHEMA_MAP_PROPERTIES = 512;
12
16
  const MAX_SCHEMA_NODES = 1_024;
13
17
  const MAX_OBJECT_KEY_LENGTH = 200;
14
- /**
15
- * The published complexity limits of the Studio Schema Profile. The
16
- * machine-readable meta-schema (`schema-profile.schema.json`) carries the
17
- * same values; a parity test keeps the two from drifting.
18
- */
18
+ const MAX_PROPERTY_NAMES = 512;
19
+ const MAX_TITLE_LENGTH = 1_000;
20
+ /** Published complexity limits, pinned to `$defs/limits` in the meta-schema. */
19
21
  export const STUDIO_SCHEMA_PROFILE_LIMITS = Object.freeze({
20
22
  maxAlternatives: MAX_ALTERNATIVES,
23
+ maxDescriptionLength: MAX_DESCRIPTION_LENGTH,
21
24
  maxEnumMembers: MAX_ENUM_MEMBERS,
25
+ maxExamples: MAX_EXAMPLES,
22
26
  maxJsonDepth: MAX_JSON_DEPTH,
23
27
  maxJsonItems: MAX_JSON_ITEMS,
24
28
  maxJsonProperties: MAX_JSON_PROPERTIES,
25
29
  maxObjectKeyLength: MAX_OBJECT_KEY_LENGTH,
30
+ maxPropertyNames: MAX_PROPERTY_NAMES,
31
+ maxReferenceLength: MAX_REFERENCE_LENGTH,
26
32
  maxReferences: MAX_REFERENCES,
27
33
  maxSchemaBytes: MAX_SCHEMA_BYTES,
28
34
  maxSchemaDepth: MAX_SCHEMA_DEPTH,
29
35
  maxSchemaMapProperties: MAX_SCHEMA_MAP_PROPERTIES,
30
36
  maxSchemaNodes: MAX_SCHEMA_NODES,
37
+ maxTitleLength: MAX_TITLE_LENGTH,
31
38
  });
39
+ export const STUDIO_SCHEMA_PROFILE_ERROR_CODES = Object.freeze([
40
+ 'invalid-root',
41
+ 'unsupported-keyword',
42
+ 'invalid-keyword-value',
43
+ 'unsafe-member',
44
+ 'limit-exceeded',
45
+ 'invalid-reference',
46
+ 'recursive-schema',
47
+ ]);
48
+ /** A deterministic admission failure suitable for cross-runtime corpus comparison. */
49
+ export class StudioSchemaProfileError extends TypeError {
50
+ code;
51
+ /** JSON Pointer to the rejected schema location; the empty string is the root. */
52
+ schemaPath;
53
+ constructor(code, schemaPath, message, options) {
54
+ super(message, options);
55
+ this.name = 'StudioSchemaProfileError';
56
+ this.code = code;
57
+ this.schemaPath = schemaPath;
58
+ }
59
+ }
32
60
  const allowedKeywords = new Set([
33
61
  '$defs',
34
62
  '$ref',
@@ -69,55 +97,83 @@ const allowedKeywords = new Set([
69
97
  'uniqueItems',
70
98
  'writeOnly',
71
99
  ]);
72
- export function assertStudioPropertySchema(schema) {
73
- const state = { references: 0, schemaNodes: 0, seen: new WeakSet() };
74
- visitSchema(schema, '$', 1, state);
75
- assertNonRecursiveSchema(schema);
76
- let serialized;
100
+ const typeNames = new Set(['array', 'boolean', 'integer', 'null', 'number', 'object', 'string']);
101
+ class SchemaByteLimitError extends RangeError {
102
+ }
103
+ class SchemaBytePreflightDeferred extends TypeError {
104
+ }
105
+ /**
106
+ * Admit and compile one contributed block property schema. The alpha profile
107
+ * is deliberately object-rooted, closed, local-reference-only, non-recursive,
108
+ * and format-free. The returned interpreter performs no code generation.
109
+ */
110
+ export function compileStudioPropertySchema(schema) {
111
+ if (!isRecord(schema)) {
112
+ reject('invalid-root', '', 'Studio property schema root must be a JSON Schema object.');
113
+ }
114
+ // Measure before sorting or recursively interpreting attacker-controlled
115
+ // maps. Canonical member order does not affect encoded length, so this
116
+ // bounded, iterative pass can fail oversized inputs without first
117
+ // allocating the canonical document or doing O(n log n) work.
77
118
  try {
78
- serialized = JSON.stringify(schema);
119
+ assertCanonicalSchemaByteBudget(schema);
79
120
  }
80
121
  catch (error) {
81
- throw new TypeError('Studio property schema must be an acyclic JSON document.', {
82
- cause: error,
83
- });
122
+ if (error instanceof SchemaByteLimitError) {
123
+ reject('limit-exceeded', '', `Studio property schema exceeds ${MAX_SCHEMA_BYTES} canonical UTF-8 bytes.`);
124
+ }
125
+ if (error instanceof SchemaBytePreflightDeferred) {
126
+ // Precise structural admission below owns diagnostics for JavaScript
127
+ // values that cannot have come from decoded JSON (cycles, aliases,
128
+ // sparse arrays, undefined, or exotic prototypes).
129
+ }
130
+ else {
131
+ reject('invalid-root', '', 'Studio property schema must be a bounded canonical JSON document.', error);
132
+ }
84
133
  }
85
- if (utf8ByteLength(serialized) > MAX_SCHEMA_BYTES) {
86
- throw new RangeError(`Studio property schema exceeds ${MAX_SCHEMA_BYTES} bytes.`);
134
+ const state = {
135
+ references: 0,
136
+ schemaNodes: 0,
137
+ seen: new WeakSet(),
138
+ };
139
+ const admissionFailures = [];
140
+ captureAdmissionFailure(() => visitSchema(schema, '', 1, state), admissionFailures);
141
+ captureAdmissionFailure(() => assertNonRecursiveSchema(schema), admissionFailures);
142
+ captureAdmissionFailure(() => assertClosedObjectRoot(schema), admissionFailures);
143
+ const admissionFailure = firstAdmissionFailure(schema, admissionFailures);
144
+ if (admissionFailure !== undefined) {
145
+ throw admissionFailure;
87
146
  }
88
147
  try {
89
- // The eval-free interpreter is the reference implementation: it validates
90
- // keyword operands, compiles bounded lexical patterns, and resolves local
91
- // references, so a schema that fails to compile is rejected atomically.
92
- compileProfileSchema(schema);
148
+ return compileProfileSchema(schema);
93
149
  }
94
150
  catch (error) {
95
- throw new TypeError('Studio property schema does not compile under the strict profile.', {
96
- cause: error,
97
- });
151
+ // All public operand checks run above. Reaching this branch means the
152
+ // interpreter found an inconsistency, so fail closed without exposing an
153
+ // implementation-specific error taxonomy.
154
+ reject('invalid-keyword-value', '', 'Studio property schema does not compile under the strict profile.', error);
98
155
  }
99
156
  }
157
+ /** Assert that a value is an admitted Studio property schema. */
158
+ export function assertStudioPropertySchema(schema) {
159
+ compileStudioPropertySchema(schema);
160
+ }
100
161
  function visitSchema(value, path, depth, state) {
101
162
  if (!isRecord(value)) {
102
- throw new TypeError(`${path} must be a JSON Schema object.`);
163
+ reject('invalid-keyword-value', path, `${displayPath(path)} must be a JSON Schema object.`);
103
164
  }
104
165
  trackObject(value, path, state);
105
- if (depth > MAX_SCHEMA_DEPTH) {
106
- throw new RangeError(`${path} exceeds the Studio Schema Profile depth limit.`);
107
- }
108
- state.schemaNodes += 1;
109
- if (state.schemaNodes > MAX_SCHEMA_NODES) {
110
- throw new RangeError(`Studio property schema exceeds ${MAX_SCHEMA_NODES} schema nodes.`);
111
- }
112
- for (const [keyword, keywordValue] of Object.entries(value)) {
166
+ trackSchemaNode(path, depth, state);
167
+ for (const [keyword, operand] of boundedSchemaEntries(value)) {
168
+ const keywordPath = appendPointer(path, keyword);
113
169
  assertSafeObjectKey(keyword, path);
114
170
  if (!allowedKeywords.has(keyword)) {
115
- throw new TypeError(`${path}.${keyword} is not allowed by the Studio Schema Profile.`);
171
+ reject('unsupported-keyword', keywordPath, `${displayPath(keywordPath)} uses keyword ${JSON.stringify(keyword)}, which is not allowed by the Studio Schema Profile.`);
116
172
  }
117
173
  switch (keyword) {
118
174
  case '$defs':
119
175
  case 'properties':
120
- visitSchemaMap(keywordValue, `${path}.${keyword}`, depth + 1, state);
176
+ visitSchemaMap(operand, keywordPath, depth + 1, state);
121
177
  break;
122
178
  case 'additionalProperties':
123
179
  case 'else':
@@ -126,72 +182,235 @@ function visitSchema(value, path, depth, state) {
126
182
  case 'not':
127
183
  case 'propertyNames':
128
184
  case 'then':
129
- visitSubschema(keywordValue, `${path}.${keyword}`, depth + 1, state);
185
+ visitSubschema(operand, keywordPath, depth + 1, state);
130
186
  break;
131
187
  case 'allOf':
132
188
  case 'anyOf':
133
189
  case 'oneOf':
134
190
  case 'prefixItems':
135
- visitSchemaArray(keywordValue, `${path}.${keyword}`, depth + 1, state);
191
+ visitSchemaArray(operand, keywordPath, depth + 1, state);
136
192
  break;
137
193
  case '$ref':
138
- visitReference(keywordValue, `${path}.${keyword}`, state);
194
+ visitReference(operand, keywordPath, state);
139
195
  break;
140
196
  case '$schema':
141
- if (keywordValue !== DRAFT_2020_12) {
142
- throw new TypeError(`${path} must declare JSON Schema Draft 2020-12.`);
197
+ if (operand !== DRAFT_2020_12) {
198
+ reject('invalid-keyword-value', keywordPath, `${displayPath(keywordPath)} must declare JSON Schema Draft 2020-12.`);
143
199
  }
144
200
  break;
145
201
  case 'enum':
146
- if (!Array.isArray(keywordValue) || keywordValue.length > MAX_ENUM_MEMBERS) {
147
- throw new RangeError(`${path}.enum exceeds ${MAX_ENUM_MEMBERS} members.`);
202
+ visitEnum(operand, keywordPath, 1, state);
203
+ break;
204
+ case 'examples':
205
+ visitExamples(operand, keywordPath, 1, state);
206
+ break;
207
+ case 'dependentRequired':
208
+ visitDependentRequired(operand, keywordPath, state);
209
+ break;
210
+ case 'required':
211
+ visitNameArray(operand, keywordPath, MAX_PROPERTY_NAMES, state);
212
+ break;
213
+ case 'type':
214
+ visitType(operand, keywordPath, state);
215
+ break;
216
+ case 'description':
217
+ visitBoundedString(operand, keywordPath, MAX_DESCRIPTION_LENGTH);
218
+ break;
219
+ case 'title':
220
+ visitBoundedString(operand, keywordPath, MAX_TITLE_LENGTH);
221
+ break;
222
+ case 'maxItems':
223
+ case 'maxLength':
224
+ case 'maxProperties':
225
+ case 'minItems':
226
+ case 'minLength':
227
+ case 'minProperties':
228
+ visitNonNegativeInteger(operand, keywordPath);
229
+ break;
230
+ case 'exclusiveMaximum':
231
+ case 'exclusiveMinimum':
232
+ case 'maximum':
233
+ case 'minimum':
234
+ visitFiniteNumber(operand, keywordPath);
235
+ break;
236
+ case 'multipleOf':
237
+ visitFiniteNumber(operand, keywordPath);
238
+ if (operand <= 0) {
239
+ reject('invalid-keyword-value', keywordPath, `${displayPath(keywordPath)} must be greater than zero.`);
148
240
  }
149
- visitJsonValue(keywordValue, `${path}.enum`, depth + 1, state);
150
241
  break;
151
- default:
152
- visitJsonValue(keywordValue, `${path}.${keyword}`, depth + 1, state);
242
+ case 'readOnly':
243
+ case 'uniqueItems':
244
+ case 'writeOnly':
245
+ if (typeof operand !== 'boolean') {
246
+ reject('invalid-keyword-value', keywordPath, `${displayPath(keywordPath)} must be a boolean.`);
247
+ }
248
+ break;
249
+ case 'const':
250
+ case 'default':
251
+ visitJsonValue(operand, keywordPath, 1, state);
252
+ break;
153
253
  }
154
254
  }
155
255
  }
156
256
  function visitSchemaMap(value, path, depth, state) {
157
257
  if (!isRecord(value)) {
158
- throw new TypeError(`${path} must be an object of schemas.`);
258
+ reject('invalid-keyword-value', path, `${displayPath(path)} must be an object of schemas.`);
159
259
  }
160
260
  trackObject(value, path, state);
161
- const entries = Object.entries(value);
162
- if (entries.length > MAX_SCHEMA_MAP_PROPERTIES) {
163
- throw new RangeError(`${path} exceeds ${MAX_SCHEMA_MAP_PROPERTIES} schema entries.`);
261
+ const keys = Object.keys(value);
262
+ if (keys.length > MAX_SCHEMA_MAP_PROPERTIES) {
263
+ reject('limit-exceeded', path, `${displayPath(path)} exceeds ${MAX_SCHEMA_MAP_PROPERTIES} schema entries.`);
164
264
  }
165
- for (const [name, schema] of entries) {
265
+ for (const name of keys.sort(compareCodeUnits)) {
166
266
  assertSafeObjectKey(name, path);
167
- visitSchema(schema, `${path}.${name}`, depth, state);
267
+ visitSchema(value[name], appendPointer(path, name), depth, state);
168
268
  }
169
269
  }
170
270
  function visitSchemaArray(value, path, depth, state) {
171
- if (!Array.isArray(value) || value.length > MAX_ALTERNATIVES) {
172
- throw new RangeError(`${path} must contain at most ${MAX_ALTERNATIVES} schemas.`);
271
+ if (!Array.isArray(value) || !isDenseArray(value)) {
272
+ reject('invalid-keyword-value', path, `${displayPath(path)} must be a dense JSON array of schemas.`);
273
+ }
274
+ if (value.length === 0) {
275
+ reject('invalid-keyword-value', path, `${displayPath(path)} must contain at least one schema.`);
276
+ }
277
+ if (value.length > MAX_ALTERNATIVES) {
278
+ reject('limit-exceeded', path, `${displayPath(path)} must contain at most ${MAX_ALTERNATIVES} schemas.`);
173
279
  }
174
280
  trackObject(value, path, state);
175
- assertDenseArray(value, path);
176
281
  for (const [index, schema] of value.entries()) {
177
- visitSubschema(schema, `${path}[${index}]`, depth, state);
282
+ visitSubschema(schema, appendPointer(path, String(index)), depth, state);
178
283
  }
179
284
  }
180
285
  function visitSubschema(value, path, depth, state) {
181
286
  if (typeof value === 'boolean') {
287
+ trackSchemaNode(path, depth, state);
182
288
  return;
183
289
  }
184
290
  visitSchema(value, path, depth, state);
185
291
  }
292
+ function trackSchemaNode(path, depth, state) {
293
+ if (depth > MAX_SCHEMA_DEPTH) {
294
+ reject('limit-exceeded', path, `${displayPath(path)} exceeds the Studio Schema Profile depth limit.`);
295
+ }
296
+ state.schemaNodes += 1;
297
+ if (state.schemaNodes > MAX_SCHEMA_NODES) {
298
+ reject('limit-exceeded', path, `Studio property schema exceeds ${MAX_SCHEMA_NODES} schema nodes.`);
299
+ }
300
+ }
186
301
  function visitReference(value, path, state) {
187
- if (typeof value !== 'string' ||
188
- (value !== '#' && !value.startsWith('#/')) ||
189
- value.length > 500) {
190
- throw new TypeError(`${path} must be a bounded local JSON Pointer reference.`);
302
+ if (!isPortableLocalReference(value)) {
303
+ reject('invalid-reference', path, `${displayPath(path)} must be a bounded local JSON Pointer reference.`);
191
304
  }
192
305
  state.references += 1;
193
306
  if (state.references > MAX_REFERENCES) {
194
- throw new RangeError(`Studio property schema exceeds ${MAX_REFERENCES} references.`);
307
+ reject('limit-exceeded', path, `Studio property schema exceeds ${MAX_REFERENCES} references.`);
308
+ }
309
+ }
310
+ function visitEnum(value, path, depth, state) {
311
+ if (!Array.isArray(value) || !isDenseArray(value)) {
312
+ reject('invalid-keyword-value', path, `${displayPath(path)} must be a dense JSON array.`);
313
+ }
314
+ if (value.length === 0) {
315
+ reject('invalid-keyword-value', path, `${displayPath(path)} must contain at least one value.`);
316
+ }
317
+ if (value.length > MAX_ENUM_MEMBERS) {
318
+ reject('limit-exceeded', path, `${displayPath(path)} exceeds ${MAX_ENUM_MEMBERS} members.`);
319
+ }
320
+ trackObject(value, path, state);
321
+ const members = new Set();
322
+ for (const [index, member] of value.entries()) {
323
+ visitJsonValue(member, appendPointer(path, String(index)), depth, state);
324
+ const canonical = canonicalStringify(member, {
325
+ maximumDepth: MAX_JSON_DEPTH + 1,
326
+ });
327
+ if (members.has(canonical)) {
328
+ reject('invalid-keyword-value', appendPointer(path, String(index)), `${displayPath(path)} must contain unique JSON values.`);
329
+ }
330
+ members.add(canonical);
331
+ }
332
+ }
333
+ function visitExamples(value, path, depth, state) {
334
+ if (!Array.isArray(value) || !isDenseArray(value)) {
335
+ reject('invalid-keyword-value', path, `${displayPath(path)} must be a dense JSON array.`);
336
+ }
337
+ if (value.length > MAX_EXAMPLES) {
338
+ reject('limit-exceeded', path, `${displayPath(path)} exceeds ${MAX_EXAMPLES} examples.`);
339
+ }
340
+ trackObject(value, path, state);
341
+ for (const [index, example] of value.entries()) {
342
+ visitJsonValue(example, appendPointer(path, String(index)), depth, state);
343
+ }
344
+ }
345
+ function visitDependentRequired(value, path, state) {
346
+ if (!isRecord(value)) {
347
+ reject('invalid-keyword-value', path, `${displayPath(path)} must be an object of property-name arrays.`);
348
+ }
349
+ trackObject(value, path, state);
350
+ const keys = Object.keys(value);
351
+ if (keys.length > MAX_SCHEMA_MAP_PROPERTIES) {
352
+ reject('limit-exceeded', path, `${displayPath(path)} exceeds ${MAX_SCHEMA_MAP_PROPERTIES} dependency entries.`);
353
+ }
354
+ for (const name of keys.sort(compareCodeUnits)) {
355
+ assertSafeObjectKey(name, path);
356
+ visitNameArray(value[name], appendPointer(path, name), MAX_PROPERTY_NAMES, state);
357
+ }
358
+ }
359
+ function visitNameArray(value, path, maximum, state) {
360
+ if (!Array.isArray(value) || !isDenseArray(value)) {
361
+ reject('invalid-keyword-value', path, `${displayPath(path)} must be a dense array of property names.`);
362
+ }
363
+ if (value.length > maximum) {
364
+ reject('limit-exceeded', path, `${displayPath(path)} exceeds ${maximum} property names.`);
365
+ }
366
+ trackObject(value, path, state);
367
+ const names = new Set();
368
+ for (const [index, name] of value.entries()) {
369
+ if (typeof name !== 'string') {
370
+ reject('invalid-keyword-value', appendPointer(path, String(index)), `${displayPath(path)} must contain only property-name strings.`);
371
+ }
372
+ assertSafeObjectKey(name, path, appendPointer(path, String(index)));
373
+ if (names.has(name)) {
374
+ reject('invalid-keyword-value', appendPointer(path, String(index)), `${displayPath(path)} must list unique property names.`);
375
+ }
376
+ names.add(name);
377
+ }
378
+ }
379
+ function visitType(value, path, state) {
380
+ if (typeof value === 'string') {
381
+ if (!typeNames.has(value)) {
382
+ reject('invalid-keyword-value', path, `${displayPath(path)} names an unknown JSON Schema type.`);
383
+ }
384
+ return;
385
+ }
386
+ if (!Array.isArray(value) || !isDenseArray(value) || value.length === 0 || value.length > 7) {
387
+ reject('invalid-keyword-value', path, `${displayPath(path)} must be a type name or a non-empty array of at most seven names.`);
388
+ }
389
+ trackObject(value, path, state);
390
+ const names = new Set();
391
+ for (const [index, name] of value.entries()) {
392
+ if (typeof name !== 'string' || !typeNames.has(name) || names.has(name)) {
393
+ reject('invalid-keyword-value', appendPointer(path, String(index)), `${displayPath(path)} must list unique, known JSON Schema type names.`);
394
+ }
395
+ names.add(name);
396
+ }
397
+ }
398
+ function visitBoundedString(value, path, maximum) {
399
+ if (typeof value !== 'string') {
400
+ reject('invalid-keyword-value', path, `${displayPath(path)} must be a string.`);
401
+ }
402
+ if (codePointLength(value) > maximum) {
403
+ reject('limit-exceeded', path, `${displayPath(path)} exceeds ${maximum} characters.`);
404
+ }
405
+ }
406
+ function visitNonNegativeInteger(value, path) {
407
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
408
+ reject('invalid-keyword-value', path, `${displayPath(path)} must be a non-negative integer.`);
409
+ }
410
+ }
411
+ function visitFiniteNumber(value, path) {
412
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
413
+ reject('invalid-keyword-value', path, `${displayPath(path)} must be a finite number.`);
195
414
  }
196
415
  }
197
416
  function visitJsonValue(value, path, depth, state) {
@@ -202,136 +421,398 @@ function visitJsonValue(value, path, depth, state) {
202
421
  return;
203
422
  }
204
423
  if (depth > MAX_JSON_DEPTH) {
205
- throw new RangeError(`${path} exceeds the Studio Schema Profile JSON depth limit.`);
424
+ reject('limit-exceeded', path, `${displayPath(path)} exceeds the Studio Schema Profile JSON depth limit.`);
206
425
  }
207
426
  if (Array.isArray(value)) {
427
+ if (!isDenseArray(value)) {
428
+ reject('invalid-keyword-value', path, `${displayPath(path)} must be a dense JSON array.`);
429
+ }
208
430
  trackObject(value, path, state);
209
- assertDenseArray(value, path);
210
431
  if (value.length > MAX_JSON_ITEMS) {
211
- throw new RangeError(`${path} exceeds ${MAX_JSON_ITEMS} JSON items.`);
432
+ reject('limit-exceeded', path, `${displayPath(path)} exceeds ${MAX_JSON_ITEMS} JSON items.`);
212
433
  }
213
434
  for (const [index, entry] of value.entries()) {
214
- visitJsonValue(entry, `${path}[${index}]`, depth + 1, state);
435
+ visitJsonValue(entry, appendPointer(path, String(index)), depth + 1, state);
215
436
  }
216
437
  return;
217
438
  }
218
439
  if (isRecord(value)) {
219
440
  trackObject(value, path, state);
220
- const entries = Object.entries(value);
221
- if (entries.length > MAX_JSON_PROPERTIES) {
222
- throw new RangeError(`${path} exceeds ${MAX_JSON_PROPERTIES} JSON properties.`);
441
+ const keys = Object.keys(value);
442
+ if (keys.length > MAX_JSON_PROPERTIES) {
443
+ reject('limit-exceeded', path, `${displayPath(path)} exceeds ${MAX_JSON_PROPERTIES} JSON properties.`);
223
444
  }
224
- for (const [key, entry] of entries) {
445
+ for (const key of keys.sort(compareCodeUnits)) {
225
446
  assertSafeObjectKey(key, path);
226
- visitJsonValue(entry, `${path}.${key}`, depth + 1, state);
447
+ visitJsonValue(value[key], appendPointer(path, key), depth + 1, state);
227
448
  }
228
449
  return;
229
450
  }
230
- throw new TypeError(`${path} is not JSON-compatible.`);
451
+ reject('invalid-keyword-value', path, `${displayPath(path)} is not JSON-compatible.`);
452
+ }
453
+ function assertClosedObjectRoot(schema) {
454
+ // Root invariants follow the same UTF-16 member precedence as the general
455
+ // admission walk: additionalProperties sorts before type.
456
+ if (schema.additionalProperties !== false) {
457
+ reject('invalid-root', '/additionalProperties', 'Studio property schema root must declare additionalProperties: false.');
458
+ }
459
+ if (schema.type !== 'object') {
460
+ reject('invalid-root', '/type', 'Studio property schema root must declare exactly type "object".');
461
+ }
231
462
  }
232
- function assertSafeObjectKey(key, path) {
463
+ function captureAdmissionFailure(action, failures) {
464
+ try {
465
+ action();
466
+ }
467
+ catch (error) {
468
+ if (error instanceof StudioSchemaProfileError) {
469
+ failures.push(error);
470
+ return;
471
+ }
472
+ throw error;
473
+ }
474
+ }
475
+ function firstAdmissionFailure(root, failures) {
476
+ let first;
477
+ for (const failure of failures) {
478
+ if (first === undefined ||
479
+ compareAdmissionPaths(root, failure.schemaPath, first.schemaPath) < 0) {
480
+ first = failure;
481
+ }
482
+ }
483
+ return first;
484
+ }
485
+ /**
486
+ * Compare two diagnostic locations in the same order the admission grammar
487
+ * visits them: object members by UTF-16 code unit, array members by numeric
488
+ * index, and a container before any descendant. Missing root invariants are
489
+ * virtual object members, so they participate without a special-case pass
490
+ * precedence.
491
+ */
492
+ function compareAdmissionPaths(root, left, right) {
493
+ const leftTokens = pointerTokens(left);
494
+ const rightTokens = pointerTokens(right);
495
+ let parent = root;
496
+ const sharedLength = Math.min(leftTokens.length, rightTokens.length);
497
+ for (let index = 0; index < sharedLength; index += 1) {
498
+ const leftToken = leftTokens[index];
499
+ const rightToken = rightTokens[index];
500
+ if (leftToken === undefined || rightToken === undefined) {
501
+ break;
502
+ }
503
+ if (leftToken !== rightToken) {
504
+ if (Array.isArray(parent)) {
505
+ const leftIndex = Number(leftToken);
506
+ const rightIndex = Number(rightToken);
507
+ if (Number.isSafeInteger(leftIndex) && Number.isSafeInteger(rightIndex)) {
508
+ return leftIndex - rightIndex;
509
+ }
510
+ }
511
+ return compareCodeUnits(leftToken, rightToken);
512
+ }
513
+ if ((isRecord(parent) || Array.isArray(parent)) && Object.hasOwn(parent, leftToken)) {
514
+ parent = parent[leftToken];
515
+ }
516
+ else {
517
+ parent = undefined;
518
+ }
519
+ }
520
+ return leftTokens.length - rightTokens.length;
521
+ }
522
+ function pointerTokens(pointer) {
523
+ if (pointer === '') {
524
+ return [];
525
+ }
526
+ return pointer
527
+ .slice(1)
528
+ .split('/')
529
+ .map((token) => token.replaceAll('~1', '/').replaceAll('~0', '~'));
530
+ }
531
+ function assertSafeObjectKey(key, path, rejectionPath = appendPointer(path, key)) {
532
+ if (codePointLength(key) > MAX_OBJECT_KEY_LENGTH) {
533
+ reject('limit-exceeded', rejectionPath, `${displayPath(path)} contains an object member name longer than ${MAX_OBJECT_KEY_LENGTH} characters.`);
534
+ }
233
535
  if (key.length === 0 ||
234
- key.length > MAX_OBJECT_KEY_LENGTH ||
235
536
  key === '__proto__' ||
236
537
  key === 'constructor' ||
237
538
  key === 'prototype' ||
238
539
  containsControlCharacter(key)) {
239
- throw new TypeError(`${path} contains forbidden object member name ${JSON.stringify(key)}.`);
540
+ reject('unsafe-member', rejectionPath, `${displayPath(path)} contains forbidden object member name ${JSON.stringify(key)}.`);
240
541
  }
241
542
  }
242
543
  function assertNonRecursiveSchema(root) {
243
- const active = new WeakSet();
244
- const finished = new WeakSet();
245
- const stack = [{ entered: false, node: root }];
544
+ const failures = [];
545
+ const indexes = new Map();
546
+ const adjacency = [];
547
+ const reverseAdjacency = [];
548
+ const referenceSites = [];
549
+ const expanded = new WeakSet();
550
+ let eligibleReferences = 0;
551
+ const appendGraphPath = (parent, token) => ({
552
+ parent,
553
+ token,
554
+ });
555
+ const graphPathPointer = (path) => {
556
+ const tokens = [];
557
+ let current = path;
558
+ while (current !== undefined) {
559
+ tokens.push(current.token);
560
+ current = current.parent;
561
+ }
562
+ let pointer = '';
563
+ for (let index = tokens.length - 1; index >= 0; index -= 1) {
564
+ const token = tokens[index];
565
+ if (token !== undefined) {
566
+ pointer = appendPointer(pointer, token);
567
+ }
568
+ }
569
+ return pointer;
570
+ };
571
+ const ensureNode = (node) => {
572
+ const existing = indexes.get(node);
573
+ if (existing !== undefined) {
574
+ return existing;
575
+ }
576
+ const index = adjacency.length;
577
+ indexes.set(node, index);
578
+ adjacency.push([]);
579
+ reverseAdjacency.push([]);
580
+ return index;
581
+ };
582
+ const connect = (source, target) => {
583
+ adjacency[source]?.push(target);
584
+ reverseAdjacency[target]?.push(source);
585
+ };
586
+ ensureNode(root);
587
+ const stack = [
588
+ { depth: 1, diagnosticsEligible: true, node: root, path: undefined },
589
+ ];
246
590
  while (stack.length > 0) {
247
591
  const frame = stack.pop();
248
- if (frame === undefined || finished.has(frame.node)) {
249
- continue;
250
- }
251
- if (frame.entered) {
252
- active.delete(frame.node);
253
- finished.add(frame.node);
592
+ if (frame === undefined || expanded.has(frame.node)) {
254
593
  continue;
255
594
  }
256
- if (active.has(frame.node)) {
257
- throw new TypeError('Recursive contributed schemas are not admitted by the alpha profile.');
258
- }
259
- active.add(frame.node);
260
- stack.push({ entered: true, node: frame.node });
261
- const dependencies = schemaDependencies(frame.node, root);
262
- for (let index = dependencies.length - 1; index >= 0; index -= 1) {
263
- const dependency = dependencies[index];
264
- if (dependency === undefined || finished.has(dependency)) {
265
- continue;
595
+ expanded.add(frame.node);
596
+ const source = ensureNode(frame.node);
597
+ const children = [];
598
+ const addChild = (value, path, diagnosticsEligible = frame.diagnosticsEligible) => {
599
+ if (!isRecord(value)) {
600
+ return;
266
601
  }
267
- if (active.has(dependency)) {
268
- throw new TypeError('Recursive contributed schemas are not admitted by the alpha profile.');
602
+ const target = ensureNode(value);
603
+ connect(source, target);
604
+ const depth = frame.depth + 1;
605
+ children.push({
606
+ depth,
607
+ diagnosticsEligible: diagnosticsEligible && depth <= MAX_SCHEMA_DEPTH,
608
+ node: value,
609
+ path,
610
+ });
611
+ };
612
+ for (const [keyword, operand] of boundedSchemaEntries(frame.node)) {
613
+ const keywordPath = appendGraphPath(frame.path, keyword);
614
+ switch (keyword) {
615
+ case '$defs':
616
+ case 'properties':
617
+ if (isRecord(operand)) {
618
+ const names = Object.keys(operand);
619
+ const childrenEligible = names.length <= MAX_SCHEMA_MAP_PROPERTIES;
620
+ if (childrenEligible) {
621
+ names.sort(compareCodeUnits);
622
+ }
623
+ for (const name of names) {
624
+ addChild(operand[name], appendGraphPath(keywordPath, name), frame.diagnosticsEligible && childrenEligible);
625
+ }
626
+ }
627
+ break;
628
+ case '$ref':
629
+ if (isPortableLocalReference(operand)) {
630
+ const reportsDiagnostic = frame.diagnosticsEligible && (eligibleReferences += 1) <= MAX_REFERENCES;
631
+ const referencePath = reportsDiagnostic ? graphPathPointer(keywordPath) : '';
632
+ try {
633
+ const target = resolveLocalReference(root, operand, referencePath);
634
+ if (!target.schemaPosition) {
635
+ if (reportsDiagnostic) {
636
+ failures.push(new StudioSchemaProfileError('invalid-reference', referencePath, `Local schema reference ${operand} does not resolve to a schema position.`));
637
+ }
638
+ }
639
+ else if (isRecord(target.value)) {
640
+ const targetIndex = ensureNode(target.value);
641
+ connect(source, targetIndex);
642
+ if (reportsDiagnostic) {
643
+ referenceSites.push({ path: referencePath, source, target: targetIndex });
644
+ }
645
+ }
646
+ }
647
+ catch (error) {
648
+ if (error instanceof StudioSchemaProfileError) {
649
+ if (reportsDiagnostic) {
650
+ failures.push(error);
651
+ }
652
+ }
653
+ else {
654
+ throw error;
655
+ }
656
+ }
657
+ }
658
+ break;
659
+ case 'additionalProperties':
660
+ case 'else':
661
+ case 'if':
662
+ case 'items':
663
+ case 'not':
664
+ case 'propertyNames':
665
+ case 'then':
666
+ addChild(operand, keywordPath);
667
+ break;
668
+ case 'allOf':
669
+ case 'anyOf':
670
+ case 'oneOf':
671
+ case 'prefixItems':
672
+ if (Array.isArray(operand)) {
673
+ const childrenEligible = operand.length > 0 && operand.length <= MAX_ALTERNATIVES && isDenseArray(operand);
674
+ for (let index = 0; index < operand.length; index += 1) {
675
+ if (Object.hasOwn(operand, index)) {
676
+ addChild(operand[index], appendGraphPath(keywordPath, String(index)), frame.diagnosticsEligible && childrenEligible);
677
+ }
678
+ }
679
+ }
680
+ break;
269
681
  }
270
- stack.push({ entered: false, node: dependency });
271
- }
272
- }
273
- }
274
- function schemaDependencies(schema, root) {
275
- const dependencies = [];
276
- for (const keyword of [
277
- 'additionalProperties',
278
- 'else',
279
- 'if',
280
- 'items',
281
- 'not',
282
- 'propertyNames',
283
- 'then',
284
- ]) {
285
- appendSchemaDependency(dependencies, schema[keyword]);
286
- }
287
- for (const keyword of ['allOf', 'anyOf', 'oneOf', 'prefixItems']) {
288
- const candidates = schema[keyword];
289
- if (Array.isArray(candidates)) {
290
- for (const candidate of candidates) {
291
- appendSchemaDependency(dependencies, candidate);
682
+ }
683
+ for (let index = children.length - 1; index >= 0; index -= 1) {
684
+ const child = children[index];
685
+ if (child !== undefined) {
686
+ stack.push(child);
292
687
  }
293
688
  }
294
689
  }
295
- for (const keyword of ['$defs', 'properties']) {
296
- const schemaMap = schema[keyword];
297
- if (isRecord(schemaMap)) {
298
- for (const candidate of Object.values(schemaMap)) {
299
- appendSchemaDependency(dependencies, candidate);
300
- }
690
+ const components = stronglyConnectedComponents(adjacency, reverseAdjacency);
691
+ for (const site of referenceSites) {
692
+ if (components[site.source] === components[site.target]) {
693
+ failures.push(new StudioSchemaProfileError('recursive-schema', site.path, 'Recursive contributed schemas are not admitted by the alpha profile.'));
301
694
  }
302
695
  }
303
- if (typeof schema.$ref === 'string') {
304
- appendSchemaDependency(dependencies, resolveLocalReference(root, schema.$ref));
696
+ const failure = firstAdmissionFailure(root, failures);
697
+ if (failure !== undefined) {
698
+ throw failure;
305
699
  }
306
- return dependencies;
307
700
  }
308
- function appendSchemaDependency(dependencies, value) {
309
- if (isRecord(value)) {
310
- dependencies.push(value);
701
+ function stronglyConnectedComponents(adjacency, reverseAdjacency) {
702
+ const visited = new Uint8Array(adjacency.length);
703
+ const finishOrder = [];
704
+ for (let start = 0; start < adjacency.length; start += 1) {
705
+ if (visited[start] !== 0) {
706
+ continue;
707
+ }
708
+ visited[start] = 1;
709
+ const stack = [{ edge: 0, node: start }];
710
+ while (stack.length > 0) {
711
+ const frame = stack[stack.length - 1];
712
+ if (frame === undefined) {
713
+ break;
714
+ }
715
+ const edges = adjacency[frame.node] ?? [];
716
+ const target = edges[frame.edge];
717
+ if (target !== undefined) {
718
+ frame.edge += 1;
719
+ if (visited[target] === 0) {
720
+ visited[target] = 1;
721
+ stack.push({ edge: 0, node: target });
722
+ }
723
+ }
724
+ else {
725
+ finishOrder.push(frame.node);
726
+ stack.pop();
727
+ }
728
+ }
311
729
  }
730
+ const components = new Int32Array(adjacency.length);
731
+ components.fill(-1);
732
+ let component = 0;
733
+ for (let order = finishOrder.length - 1; order >= 0; order -= 1) {
734
+ const start = finishOrder[order];
735
+ if (start === undefined || components[start] !== -1) {
736
+ continue;
737
+ }
738
+ components[start] = component;
739
+ const stack = [start];
740
+ while (stack.length > 0) {
741
+ const node = stack.pop();
742
+ if (node === undefined) {
743
+ continue;
744
+ }
745
+ for (const source of reverseAdjacency[node] ?? []) {
746
+ if (components[source] === -1) {
747
+ components[source] = component;
748
+ stack.push(source);
749
+ }
750
+ }
751
+ }
752
+ component += 1;
753
+ }
754
+ return components;
312
755
  }
313
- function resolveLocalReference(root, reference) {
756
+ function resolveLocalReference(root, reference, path) {
314
757
  if (reference === '#') {
315
- return root;
758
+ return { schemaPosition: true, value: root };
316
759
  }
317
760
  let current = root;
761
+ let position = 'schema';
318
762
  for (const encodedToken of reference.slice(2).split('/')) {
319
- if (/(?:~[^01]|~$)/u.test(encodedToken)) {
320
- throw new TypeError(`Local schema reference ${reference} is not a valid JSON Pointer.`);
321
- }
322
763
  const token = encodedToken.replaceAll('~1', '/').replaceAll('~0', '~');
764
+ let nextPosition = 'other';
765
+ if (position === 'schema' && isRecord(current)) {
766
+ switch (token) {
767
+ case '$defs':
768
+ case 'properties':
769
+ nextPosition = 'schema-map';
770
+ break;
771
+ case 'additionalProperties':
772
+ case 'else':
773
+ case 'if':
774
+ case 'items':
775
+ case 'not':
776
+ case 'propertyNames':
777
+ case 'then':
778
+ nextPosition = 'schema';
779
+ break;
780
+ case 'allOf':
781
+ case 'anyOf':
782
+ case 'oneOf':
783
+ case 'prefixItems':
784
+ nextPosition = 'schema-array';
785
+ break;
786
+ }
787
+ }
788
+ else if (position === 'schema-map' && isRecord(current)) {
789
+ nextPosition = 'schema';
790
+ }
791
+ else if (position === 'schema-array' && Array.isArray(current)) {
792
+ nextPosition = 'schema';
793
+ }
323
794
  if (!isRecord(current) && !Array.isArray(current)) {
324
- throw new TypeError(`Local schema reference ${reference} does not resolve to a schema.`);
795
+ reject('invalid-reference', path, `Local schema reference ${reference} does not resolve to a schema.`);
325
796
  }
326
797
  if (!Object.hasOwn(current, token)) {
327
- throw new TypeError(`Local schema reference ${reference} does not resolve to a schema.`);
798
+ reject('invalid-reference', path, `Local schema reference ${reference} does not resolve to a schema.`);
328
799
  }
329
800
  current = current[token];
801
+ position = nextPosition;
330
802
  }
331
803
  if (typeof current !== 'boolean' && !isRecord(current)) {
332
- throw new TypeError(`Local schema reference ${reference} does not resolve to a schema.`);
804
+ reject('invalid-reference', path, `Local schema reference ${reference} does not resolve to a schema.`);
333
805
  }
334
- return current;
806
+ return { schemaPosition: position === 'schema', value: current };
807
+ }
808
+ function reject(code, schemaPath, message, cause) {
809
+ throw new StudioSchemaProfileError(code, schemaPath, message, cause === undefined ? undefined : { cause });
810
+ }
811
+ function appendPointer(pointer, token) {
812
+ return `${pointer}/${token.replaceAll('~', '~0').replaceAll('/', '~1')}`;
813
+ }
814
+ function displayPath(path) {
815
+ return path === '' ? 'schema root' : path;
335
816
  }
336
817
  function containsControlCharacter(value) {
337
818
  for (let index = 0; index < value.length; index += 1) {
@@ -342,49 +823,193 @@ function containsControlCharacter(value) {
342
823
  }
343
824
  return false;
344
825
  }
345
- function assertDenseArray(value, path) {
346
- const keys = Object.keys(value);
347
- if (keys.length !== value.length || keys.some((key, index) => key !== String(index))) {
348
- throw new TypeError(`${path} must be a dense JSON array without extra properties.`);
349
- }
826
+ function isPortableLocalReference(value) {
827
+ return (typeof value === 'string' &&
828
+ codePointLength(value) <= MAX_REFERENCE_LENGTH &&
829
+ !containsControlCharacter(value) &&
830
+ /^#(?:\/(?:[A-Za-z0-9._!$&'()*+,;=:@-]|~[01])*)*$/u.test(value));
350
831
  }
351
- function trackObject(value, path, state) {
352
- if (state.seen.has(value)) {
353
- throw new TypeError(`${path} reuses or cycles a JSON object.`);
832
+ function codePointLength(value) {
833
+ let length = 0;
834
+ for (let index = 0; index < value.length; index += 1) {
835
+ length += 1;
836
+ const code = value.charCodeAt(index);
837
+ if (code >= 0xd800 && code <= 0xdbff && index + 1 < value.length) {
838
+ const next = value.charCodeAt(index + 1);
839
+ if ((next & 0xfc00) === 0xdc00) {
840
+ index += 1;
841
+ }
842
+ }
354
843
  }
355
- state.seen.add(value);
844
+ return length;
356
845
  }
357
- function isRecord(value) {
358
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
359
- return false;
846
+ /**
847
+ * Enforce the canonical byte ceiling without materialising or sorting the
848
+ * canonical document. Object order cannot change encoded length. This pass
849
+ * is iterative so an over-deep untrusted value cannot exhaust the JavaScript
850
+ * call stack before the published schema/JSON depth checks run.
851
+ */
852
+ function assertCanonicalSchemaByteBudget(root) {
853
+ const stack = [root];
854
+ const seen = new WeakSet();
855
+ let bytes = 0;
856
+ const consume = (amount) => {
857
+ bytes += amount;
858
+ if (bytes > MAX_SCHEMA_BYTES) {
859
+ throw new SchemaByteLimitError();
860
+ }
861
+ };
862
+ while (stack.length > 0) {
863
+ const value = stack.pop();
864
+ if (value === null) {
865
+ consume(4);
866
+ continue;
867
+ }
868
+ switch (typeof value) {
869
+ case 'boolean':
870
+ consume(value ? 4 : 5);
871
+ continue;
872
+ case 'number': {
873
+ if (!Number.isFinite(value)) {
874
+ throw new SchemaBytePreflightDeferred();
875
+ }
876
+ const encoded = JSON.stringify(Object.is(value, -0) ? 0 : value);
877
+ consume(encoded.length);
878
+ continue;
879
+ }
880
+ case 'string':
881
+ consumeCanonicalJsonString(value, consume);
882
+ continue;
883
+ case 'object':
884
+ break;
885
+ default:
886
+ throw new SchemaBytePreflightDeferred();
887
+ }
888
+ if (seen.has(value)) {
889
+ throw new SchemaBytePreflightDeferred();
890
+ }
891
+ seen.add(value);
892
+ if (Array.isArray(value)) {
893
+ const members = value;
894
+ // Length alone accounts for brackets and separators. Reject an
895
+ // obviously oversized array before Object.keys allocates an entry for
896
+ // every member while checking density.
897
+ consume(2 + Math.max(0, members.length - 1));
898
+ if (!isDenseArray(members)) {
899
+ throw new SchemaBytePreflightDeferred();
900
+ }
901
+ for (let index = members.length - 1; index >= 0; index -= 1) {
902
+ const member = members[index];
903
+ if (member === undefined) {
904
+ throw new SchemaBytePreflightDeferred();
905
+ }
906
+ stack.push(member);
907
+ }
908
+ continue;
909
+ }
910
+ if (!isRecord(value)) {
911
+ throw new SchemaBytePreflightDeferred();
912
+ }
913
+ const keys = Object.keys(value);
914
+ consume(2 + Math.max(0, keys.length - 1));
915
+ for (let index = keys.length - 1; index >= 0; index -= 1) {
916
+ const key = keys[index];
917
+ if (key === undefined) {
918
+ continue;
919
+ }
920
+ const member = value[key];
921
+ if (member === undefined) {
922
+ throw new SchemaBytePreflightDeferred();
923
+ }
924
+ consumeCanonicalJsonString(key, consume);
925
+ consume(1);
926
+ stack.push(member);
927
+ }
360
928
  }
361
- const prototype = Object.getPrototypeOf(value);
362
- return prototype === Object.prototype || prototype === null;
363
929
  }
364
- function utf8ByteLength(value) {
365
- let bytes = 0;
930
+ function consumeCanonicalJsonString(value, consume) {
931
+ consume(2); // opening and closing quotes
366
932
  for (let index = 0; index < value.length; index += 1) {
367
933
  const code = value.charCodeAt(index);
368
- if (code <= 0x7f) {
369
- bytes += 1;
934
+ if (code === 0x22 ||
935
+ code === 0x5c ||
936
+ code === 0x08 ||
937
+ code === 0x09 ||
938
+ code === 0x0a ||
939
+ code === 0x0c ||
940
+ code === 0x0d) {
941
+ consume(2);
942
+ }
943
+ else if (code <= 0x1f) {
944
+ consume(6);
945
+ }
946
+ else if (code <= 0x7f) {
947
+ consume(1);
370
948
  }
371
949
  else if (code <= 0x7ff) {
372
- bytes += 2;
950
+ consume(2);
373
951
  }
374
952
  else if (code >= 0xd800 && code <= 0xdbff) {
375
953
  const next = value.charCodeAt(index + 1);
376
- if (next >= 0xdc00 && next <= 0xdfff) {
377
- bytes += 4;
954
+ if (index + 1 < value.length && (next & 0xfc00) === 0xdc00) {
955
+ consume(4);
378
956
  index += 1;
379
957
  }
380
958
  else {
381
- bytes += 3;
959
+ consume(6);
382
960
  }
383
961
  }
962
+ else if (code >= 0xdc00 && code <= 0xdfff) {
963
+ consume(6);
964
+ }
384
965
  else {
385
- bytes += 3;
966
+ consume(3);
967
+ }
968
+ }
969
+ }
970
+ /**
971
+ * Sort at most the closed keyword set plus the first invalid member. This
972
+ * preserves deterministic first-error precedence without sorting an
973
+ * arbitrarily large attacker-controlled schema object.
974
+ */
975
+ function boundedSchemaEntries(value) {
976
+ const keys = Object.keys(value);
977
+ if (keys.length <= allowedKeywords.size) {
978
+ return keys.sort(compareCodeUnits).map((key) => [key, value[key]]);
979
+ }
980
+ const candidates = [];
981
+ let firstInvalid;
982
+ for (const key of keys) {
983
+ if (allowedKeywords.has(key)) {
984
+ candidates.push(key);
985
+ }
986
+ else if (firstInvalid === undefined || compareCodeUnits(key, firstInvalid) < 0) {
987
+ firstInvalid = key;
386
988
  }
387
989
  }
388
- return bytes;
990
+ if (firstInvalid !== undefined) {
991
+ candidates.push(firstInvalid);
992
+ }
993
+ return candidates.sort(compareCodeUnits).map((key) => [key, value[key]]);
994
+ }
995
+ function isDenseArray(value) {
996
+ const keys = Object.keys(value);
997
+ return keys.length === value.length && keys.every((key, index) => key === String(index));
998
+ }
999
+ function compareCodeUnits(left, right) {
1000
+ return left < right ? -1 : left > right ? 1 : 0;
1001
+ }
1002
+ function trackObject(value, path, state) {
1003
+ if (state.seen.has(value)) {
1004
+ reject('invalid-root', path, `${displayPath(path)} reuses or cycles a JSON object.`);
1005
+ }
1006
+ state.seen.add(value);
1007
+ }
1008
+ function isRecord(value) {
1009
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
1010
+ return false;
1011
+ }
1012
+ const prototype = Object.getPrototypeOf(value);
1013
+ return prototype === Object.prototype || prototype === null;
389
1014
  }
390
1015
  //# sourceMappingURL=schema-profile.js.map