@kumwe/studio-core 0.1.0-alpha.10

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.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -0
  3. package/THIRD_PARTY_NOTICES.md +9 -0
  4. package/dist/binding-projection.d.ts +47 -0
  5. package/dist/binding-projection.d.ts.map +1 -0
  6. package/dist/binding-projection.js +253 -0
  7. package/dist/binding-projection.js.map +1 -0
  8. package/dist/canonical.d.ts +20 -0
  9. package/dist/canonical.d.ts.map +1 -0
  10. package/dist/canonical.js +100 -0
  11. package/dist/canonical.js.map +1 -0
  12. package/dist/clone.d.ts +2 -0
  13. package/dist/clone.d.ts.map +1 -0
  14. package/dist/clone.js +4 -0
  15. package/dist/clone.js.map +1 -0
  16. package/dist/commands.d.ts +18 -0
  17. package/dist/commands.d.ts.map +1 -0
  18. package/dist/commands.js +799 -0
  19. package/dist/commands.js.map +1 -0
  20. package/dist/contributions.d.ts +93 -0
  21. package/dist/contributions.d.ts.map +1 -0
  22. package/dist/contributions.js +435 -0
  23. package/dist/contributions.js.map +1 -0
  24. package/dist/entry-commands.d.ts +3 -0
  25. package/dist/entry-commands.d.ts.map +1 -0
  26. package/dist/entry-commands.js +36 -0
  27. package/dist/entry-commands.js.map +1 -0
  28. package/dist/extension-sdk.d.ts +51 -0
  29. package/dist/extension-sdk.d.ts.map +1 -0
  30. package/dist/extension-sdk.js +260 -0
  31. package/dist/extension-sdk.js.map +1 -0
  32. package/dist/history.d.ts +22 -0
  33. package/dist/history.d.ts.map +1 -0
  34. package/dist/history.js +81 -0
  35. package/dist/history.js.map +1 -0
  36. package/dist/host-session.d.ts +74 -0
  37. package/dist/host-session.d.ts.map +1 -0
  38. package/dist/host-session.js +713 -0
  39. package/dist/host-session.js.map +1 -0
  40. package/dist/index.d.ts +23 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +23 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/layout.d.ts +66 -0
  45. package/dist/layout.d.ts.map +1 -0
  46. package/dist/layout.js +240 -0
  47. package/dist/layout.js.map +1 -0
  48. package/dist/migrations.d.ts +47 -0
  49. package/dist/migrations.d.ts.map +1 -0
  50. package/dist/migrations.js +83 -0
  51. package/dist/migrations.js.map +1 -0
  52. package/dist/model-commands.d.ts +3 -0
  53. package/dist/model-commands.d.ts.map +1 -0
  54. package/dist/model-commands.js +21 -0
  55. package/dist/model-commands.js.map +1 -0
  56. package/dist/modes.d.ts +47 -0
  57. package/dist/modes.d.ts.map +1 -0
  58. package/dist/modes.js +280 -0
  59. package/dist/modes.js.map +1 -0
  60. package/dist/negotiation.d.ts +23 -0
  61. package/dist/negotiation.d.ts.map +1 -0
  62. package/dist/negotiation.js +65 -0
  63. package/dist/negotiation.js.map +1 -0
  64. package/dist/production-values.d.ts +12 -0
  65. package/dist/production-values.d.ts.map +1 -0
  66. package/dist/production-values.js +224 -0
  67. package/dist/production-values.js.map +1 -0
  68. package/dist/production.d.ts +86 -0
  69. package/dist/production.d.ts.map +1 -0
  70. package/dist/production.js +811 -0
  71. package/dist/production.js.map +1 -0
  72. package/dist/profile-validator.d.ts +47 -0
  73. package/dist/profile-validator.d.ts.map +1 -0
  74. package/dist/profile-validator.js +887 -0
  75. package/dist/profile-validator.js.map +1 -0
  76. package/dist/recipes.d.ts +15 -0
  77. package/dist/recipes.d.ts.map +1 -0
  78. package/dist/recipes.js +34 -0
  79. package/dist/recipes.js.map +1 -0
  80. package/dist/registry.d.ts +17 -0
  81. package/dist/registry.d.ts.map +1 -0
  82. package/dist/registry.js +56 -0
  83. package/dist/registry.js.map +1 -0
  84. package/dist/schema-profile.d.ts +27 -0
  85. package/dist/schema-profile.d.ts.map +1 -0
  86. package/dist/schema-profile.js +1015 -0
  87. package/dist/schema-profile.js.map +1 -0
  88. package/dist/semver.d.ts +20 -0
  89. package/dist/semver.d.ts.map +1 -0
  90. package/dist/semver.js +128 -0
  91. package/dist/semver.js.map +1 -0
  92. package/dist/session.d.ts +73 -0
  93. package/dist/session.d.ts.map +1 -0
  94. package/dist/session.js +191 -0
  95. package/dist/session.js.map +1 -0
  96. package/dist/url-policy.d.ts +45 -0
  97. package/dist/url-policy.d.ts.map +1 -0
  98. package/dist/url-policy.js +248 -0
  99. package/dist/url-policy.js.map +1 -0
  100. package/dist/validation.d.ts +13 -0
  101. package/dist/validation.d.ts.map +1 -0
  102. package/dist/validation.js +444 -0
  103. package/dist/validation.js.map +1 -0
  104. package/package.json +41 -0
@@ -0,0 +1,887 @@
1
+ /**
2
+ * An eval-free interpreting validator for the Studio Schema Profile.
3
+ *
4
+ * The Studio Schema Profile (docs/contracts/schema-profile.md,
5
+ * schemas/schema-profile.schema.json) deliberately bounds schemas to a closed
6
+ * keyword allowlist with complexity limits, which makes the profile small
7
+ * enough to interpret directly. This module walks the raw schema document at
8
+ * validation time instead of generating JavaScript, so validation runs under
9
+ * a Content-Security-Policy that forbids string-to-code compilation
10
+ * (`script-src 'self'` without `unsafe-eval`) and under Trusted Types.
11
+ *
12
+ * Supported keywords are exactly the profile's closed set — types, `enum`,
13
+ * `const`, `required`/`properties`/`additionalProperties`/`propertyNames`/
14
+ * `dependentRequired`, `items`/`prefixItems` and array bounds, string and
15
+ * number bounds, `allOf`/`anyOf`/`oneOf`/`not`/`if`/`then`/`else`,
16
+ * within-registry `$defs`/`$ref`, and the profile's annotations — plus two
17
+ * canonical-schema affordances the profile contract reserves for reviewed
18
+ * Studio schemas: `pattern` (see the ReDoS bound below) and a document-root
19
+ * `$id` so canonical documents can reference each other through the
20
+ * in-memory registry. Formats are not interpreted: the profile publishes no
21
+ * runtime `format` assertions, so the keyword is rejected at compile time
22
+ * exactly like every other keyword outside the allowlist.
23
+ *
24
+ * ReDoS bound: contributed schemas cannot carry `pattern` at all — the
25
+ * profile prohibits the keyword and `assertStudioPropertySchema` rejects it
26
+ * before this module ever sees a contributed document. Every regular
27
+ * expression interpreted here therefore comes from a reviewed lexical
28
+ * pattern in a canonical Studio schema. As defence in depth the compiler
29
+ * still enforces the profile's 500-character bound on lexical source text
30
+ * (the same bound the meta-schema places on `$ref`) and the profile's
31
+ * instance limits bound every string a pattern can be asked to match.
32
+ *
33
+ * The interpreter is pure and deterministic: no DOM, no Node APIs, no
34
+ * `Function` constructor, no shared mutable state beyond the per-validator
35
+ * error buffer that mirrors the previous compiled-validator shape. Public
36
+ * diagnostics are the ordered set of distinct failures; exact duplicates
37
+ * are collapsed so repeated reference fan-out cannot amplify output size.
38
+ */
39
+ const DRAFT_2020_12 = 'https://json-schema.org/draft/2020-12/schema';
40
+ const MAX_PATTERN_SOURCE_LENGTH = 500;
41
+ const MAX_REFERENCE_LENGTH = 500;
42
+ const TYPE_NAMES = new Set(['array', 'boolean', 'integer', 'null', 'number', 'object', 'string']);
43
+ const SUPPORTED_KEYWORDS = new Set([
44
+ '$defs',
45
+ '$id',
46
+ '$ref',
47
+ '$schema',
48
+ 'additionalProperties',
49
+ 'allOf',
50
+ 'anyOf',
51
+ 'const',
52
+ 'default',
53
+ 'dependentRequired',
54
+ 'description',
55
+ 'else',
56
+ 'enum',
57
+ 'examples',
58
+ 'exclusiveMaximum',
59
+ 'exclusiveMinimum',
60
+ 'if',
61
+ 'items',
62
+ 'maxItems',
63
+ 'maxLength',
64
+ 'maxProperties',
65
+ 'maximum',
66
+ 'minItems',
67
+ 'minLength',
68
+ 'minProperties',
69
+ 'minimum',
70
+ 'multipleOf',
71
+ 'not',
72
+ 'oneOf',
73
+ 'pattern',
74
+ 'prefixItems',
75
+ 'properties',
76
+ 'propertyNames',
77
+ 'readOnly',
78
+ 'required',
79
+ 'then',
80
+ 'title',
81
+ 'type',
82
+ 'uniqueItems',
83
+ 'writeOnly',
84
+ ]);
85
+ /**
86
+ * A compiled (pre-walked, pre-resolved) profile schema. `validate` mirrors
87
+ * the verdict-plus-`errors` shape of the code-generating validator it
88
+ * replaces: `errors` is `null` after a passing run and carries the ordered,
89
+ * distinct failures of the most recent failing run otherwise.
90
+ */
91
+ export class CompiledSchemaValidator {
92
+ errors = null;
93
+ #program;
94
+ constructor(program) {
95
+ this.#program = program;
96
+ }
97
+ validate(instance) {
98
+ const errors = [];
99
+ const valid = validateSubschema(this.#program.root, instance, '', errors, this.#program, new Set(), new WeakMap());
100
+ const diagnostics = uniqueDiagnostics(errors);
101
+ if (valid === diagnostics.length > 0) {
102
+ throw new TypeError('Schema validation verdict and diagnostics disagree.');
103
+ }
104
+ this.errors = diagnostics.length > 0 ? diagnostics : null;
105
+ return valid;
106
+ }
107
+ }
108
+ /**
109
+ * Compiles a schema for interpretation: validates every keyword against the
110
+ * profile's closed allowlist and operand grammar, compiles bounded lexical
111
+ * patterns, and resolves every `$ref` against the in-memory registry. All
112
+ * structural errors surface here, at compile time, so validation itself is
113
+ * total. Throws `TypeError`/`RangeError` on any schema outside the profile.
114
+ */
115
+ export function compileProfileSchema(schema, options = {}) {
116
+ if (!isSchemaNode(schema)) {
117
+ throw new TypeError('Schema root must be a plain JSON Schema object.');
118
+ }
119
+ const documents = [];
120
+ const documentsByUri = new Map();
121
+ const registerDocument = (root, requireId) => {
122
+ const baseUri = root.$id;
123
+ if (baseUri !== undefined && typeof baseUri !== 'string') {
124
+ throw new TypeError('Schema $id must be a string.');
125
+ }
126
+ if (requireId && baseUri === undefined) {
127
+ throw new TypeError('Registry schema documents must declare a root $id.');
128
+ }
129
+ const document = { baseUri, root, schemaPointers: new Set() };
130
+ if (baseUri !== undefined) {
131
+ if (documentsByUri.has(baseUri)) {
132
+ throw new TypeError(`Schema registry declares ${baseUri} more than once.`);
133
+ }
134
+ documentsByUri.set(baseUri, document);
135
+ }
136
+ documents.push(document);
137
+ return document;
138
+ };
139
+ registerDocument(schema, false);
140
+ for (const registered of options.schemas ?? []) {
141
+ if (!isSchemaNode(registered)) {
142
+ throw new TypeError('Registry schema documents must be plain JSON Schema objects.');
143
+ }
144
+ registerDocument(registered, true);
145
+ }
146
+ const patterns = new WeakMap();
147
+ const references = new WeakMap();
148
+ const sites = [];
149
+ for (const document of documents) {
150
+ walkDocument(document, patterns, sites);
151
+ }
152
+ for (const site of sites) {
153
+ references.set(site.node, resolveReferenceSite(site, documentsByUri));
154
+ }
155
+ return new CompiledSchemaValidator({ patterns, references, root: schema });
156
+ }
157
+ // ---------------------------------------------------------------------------
158
+ // Compilation: keyword allowlist, operand grammar, patterns, references.
159
+ // ---------------------------------------------------------------------------
160
+ function walkDocument(document, patterns, sites) {
161
+ const seen = new WeakSet();
162
+ const walkSchema = (value, pointer) => {
163
+ const location = describeLocation(document, pointer);
164
+ if (!isSchemaNode(value)) {
165
+ throw new TypeError(`${location} must be a plain JSON Schema object.`);
166
+ }
167
+ if (seen.has(value)) {
168
+ throw new TypeError(`${location} reuses or cycles a schema object.`);
169
+ }
170
+ seen.add(value);
171
+ document.schemaPointers.add(pointer);
172
+ for (const [keyword, operand] of sortedEntries(value)) {
173
+ const keywordLocation = describeLocation(document, appendPointer(pointer, keyword));
174
+ if (!SUPPORTED_KEYWORDS.has(keyword)) {
175
+ throw new TypeError(`${keywordLocation} uses keyword ${JSON.stringify(keyword)}, which the Studio schema interpreter does not support.`);
176
+ }
177
+ switch (keyword) {
178
+ case '$id':
179
+ if (pointer !== '') {
180
+ throw new TypeError(`${keywordLocation} may only appear at the document root.`);
181
+ }
182
+ break;
183
+ case '$schema':
184
+ if (operand !== DRAFT_2020_12) {
185
+ throw new TypeError(`${keywordLocation} must declare JSON Schema Draft 2020-12.`);
186
+ }
187
+ break;
188
+ case '$ref':
189
+ if (typeof operand !== 'string' || codePointLength(operand) > MAX_REFERENCE_LENGTH) {
190
+ throw new TypeError(`${keywordLocation} must be a string of at most ${MAX_REFERENCE_LENGTH} characters.`);
191
+ }
192
+ sites.push({ document, node: value, pointer, reference: operand });
193
+ break;
194
+ case '$defs':
195
+ case 'properties':
196
+ walkSchemaMap(operand, appendPointer(pointer, keyword));
197
+ break;
198
+ case 'additionalProperties':
199
+ case 'else':
200
+ case 'if':
201
+ case 'items':
202
+ case 'not':
203
+ case 'propertyNames':
204
+ case 'then':
205
+ walkSubschema(operand, appendPointer(pointer, keyword));
206
+ break;
207
+ case 'allOf':
208
+ case 'anyOf':
209
+ case 'oneOf':
210
+ case 'prefixItems':
211
+ walkSchemaArray(operand, appendPointer(pointer, keyword));
212
+ break;
213
+ case 'type':
214
+ assertTypeOperand(operand, keywordLocation);
215
+ break;
216
+ case 'enum':
217
+ if (!isDenseArray(operand) || operand.length === 0) {
218
+ throw new TypeError(`${keywordLocation} must be a dense, non-empty JSON array.`);
219
+ }
220
+ for (let index = 0; index < operand.length; index += 1) {
221
+ if (operand.slice(0, index).some((member) => deepEqual(member, operand[index]))) {
222
+ throw new TypeError(`${keywordLocation} must contain unique JSON values.`);
223
+ }
224
+ }
225
+ break;
226
+ case 'examples':
227
+ if (!isDenseArray(operand)) {
228
+ throw new TypeError(`${keywordLocation} must be a dense JSON array.`);
229
+ }
230
+ break;
231
+ case 'const':
232
+ case 'default':
233
+ break;
234
+ case 'required':
235
+ assertNameArray(operand, keywordLocation);
236
+ break;
237
+ case 'dependentRequired':
238
+ if (!isSchemaNode(operand)) {
239
+ throw new TypeError(`${keywordLocation} must be an object of property-name arrays.`);
240
+ }
241
+ for (const [name, dependents] of sortedEntries(operand)) {
242
+ assertNameArray(dependents, `${keywordLocation}.${name}`);
243
+ }
244
+ break;
245
+ case 'maxItems':
246
+ case 'maxLength':
247
+ case 'maxProperties':
248
+ case 'minItems':
249
+ case 'minLength':
250
+ case 'minProperties':
251
+ if (typeof operand !== 'number' || !Number.isInteger(operand) || operand < 0) {
252
+ throw new TypeError(`${keywordLocation} must be a non-negative integer.`);
253
+ }
254
+ break;
255
+ case 'exclusiveMaximum':
256
+ case 'exclusiveMinimum':
257
+ case 'maximum':
258
+ case 'minimum':
259
+ if (typeof operand !== 'number' || !Number.isFinite(operand)) {
260
+ throw new TypeError(`${keywordLocation} must be a finite number.`);
261
+ }
262
+ break;
263
+ case 'multipleOf':
264
+ if (typeof operand !== 'number' || !Number.isFinite(operand) || operand <= 0) {
265
+ throw new TypeError(`${keywordLocation} must be a finite number greater than zero.`);
266
+ }
267
+ break;
268
+ case 'pattern':
269
+ patterns.set(value, compilePattern(operand, keywordLocation));
270
+ break;
271
+ case 'readOnly':
272
+ case 'uniqueItems':
273
+ case 'writeOnly':
274
+ if (typeof operand !== 'boolean') {
275
+ throw new TypeError(`${keywordLocation} must be a boolean.`);
276
+ }
277
+ break;
278
+ case 'description':
279
+ case 'title':
280
+ if (typeof operand !== 'string') {
281
+ throw new TypeError(`${keywordLocation} must be a string.`);
282
+ }
283
+ break;
284
+ default:
285
+ throw new TypeError(`${keywordLocation} is not interpretable.`);
286
+ }
287
+ }
288
+ };
289
+ const walkSubschema = (value, pointer) => {
290
+ if (typeof value === 'boolean') {
291
+ document.schemaPointers.add(pointer);
292
+ return;
293
+ }
294
+ walkSchema(value, pointer);
295
+ };
296
+ const walkSchemaMap = (value, pointer) => {
297
+ if (!isSchemaNode(value)) {
298
+ throw new TypeError(`${describeLocation(document, pointer)} must be an object of schemas.`);
299
+ }
300
+ for (const [name, member] of sortedEntries(value)) {
301
+ walkSubschema(member, appendPointer(pointer, name));
302
+ }
303
+ };
304
+ const walkSchemaArray = (value, pointer) => {
305
+ if (!isDenseArray(value) || value.length === 0) {
306
+ throw new TypeError(`${describeLocation(document, pointer)} must be a dense, non-empty array of schemas.`);
307
+ }
308
+ for (const [index, member] of value.entries()) {
309
+ walkSubschema(member, appendPointer(pointer, String(index)));
310
+ }
311
+ };
312
+ walkSchema(document.root, '');
313
+ }
314
+ function compilePattern(operand, location) {
315
+ if (typeof operand !== 'string' || codePointLength(operand) > MAX_PATTERN_SOURCE_LENGTH) {
316
+ throw new TypeError(`${location} must be a lexical pattern of at most ${MAX_PATTERN_SOURCE_LENGTH} characters.`);
317
+ }
318
+ try {
319
+ return new RegExp(operand, 'u');
320
+ }
321
+ catch (error) {
322
+ throw new TypeError(`${location} is not a valid Unicode regular expression.`, {
323
+ cause: error,
324
+ });
325
+ }
326
+ }
327
+ function assertTypeOperand(operand, location) {
328
+ if (typeof operand === 'string') {
329
+ if (!TYPE_NAMES.has(operand)) {
330
+ throw new TypeError(`${location} names an unknown JSON Schema type.`);
331
+ }
332
+ return;
333
+ }
334
+ if (!isDenseArray(operand) || operand.length === 0) {
335
+ throw new TypeError(`${location} must be a type name or a dense, non-empty array of them.`);
336
+ }
337
+ const names = new Set();
338
+ for (const member of operand) {
339
+ if (typeof member !== 'string' || !TYPE_NAMES.has(member) || names.has(member)) {
340
+ throw new TypeError(`${location} must list unique, known JSON Schema type names.`);
341
+ }
342
+ names.add(member);
343
+ }
344
+ }
345
+ function assertNameArray(operand, location) {
346
+ if (!isDenseArray(operand)) {
347
+ throw new TypeError(`${location} must be a dense array of property names.`);
348
+ }
349
+ const names = new Set();
350
+ for (const member of operand) {
351
+ if (typeof member !== 'string' || names.has(member)) {
352
+ throw new TypeError(`${location} must list unique property-name strings.`);
353
+ }
354
+ names.add(member);
355
+ }
356
+ }
357
+ function resolveReferenceSite(site, documentsByUri) {
358
+ const location = `${describeLocation(site.document, site.pointer)}/$ref`;
359
+ const hashIndex = site.reference.indexOf('#');
360
+ const uriPart = hashIndex === -1 ? site.reference : site.reference.slice(0, hashIndex);
361
+ const fragment = hashIndex === -1 ? '' : site.reference.slice(hashIndex + 1);
362
+ let target;
363
+ if (uriPart === '') {
364
+ target = site.document;
365
+ }
366
+ else {
367
+ const resolved = resolveDocumentUri(site.document.baseUri, uriPart, location);
368
+ const found = documentsByUri.get(resolved);
369
+ if (found === undefined) {
370
+ throw new TypeError(`${location} references ${resolved}, which is not in the registry.`);
371
+ }
372
+ target = found;
373
+ }
374
+ if (fragment !== '' && !fragment.startsWith('/')) {
375
+ throw new TypeError(`${location} must use a JSON Pointer fragment.`);
376
+ }
377
+ const tokens = fragment === ''
378
+ ? []
379
+ : fragment
380
+ .slice(1)
381
+ .split('/')
382
+ .map((token) => unescapeToken(token, location));
383
+ const canonical = tokens.map((token) => `/${escapeToken(token)}`).join('');
384
+ if (canonical !== '' && !target.schemaPointers.has(canonical)) {
385
+ throw new TypeError(`${location} does not reference a schema position.`);
386
+ }
387
+ let current = target.root;
388
+ for (const token of tokens) {
389
+ if (Array.isArray(current)) {
390
+ const index = Number(token);
391
+ if (!Number.isInteger(index) || index < 0 || index >= current.length) {
392
+ throw new TypeError(`${location} does not resolve to a schema.`);
393
+ }
394
+ current = current[index];
395
+ }
396
+ else if (isSchemaNode(current) && Object.hasOwn(current, token)) {
397
+ current = current[token];
398
+ }
399
+ else {
400
+ throw new TypeError(`${location} does not resolve to a schema.`);
401
+ }
402
+ }
403
+ if (typeof current === 'boolean' || isSchemaNode(current)) {
404
+ return current;
405
+ }
406
+ throw new TypeError(`${location} does not resolve to a schema.`);
407
+ }
408
+ function resolveDocumentUri(base, uriPart, location) {
409
+ if (/^[A-Za-z][A-Za-z0-9+.-]*:/u.test(uriPart)) {
410
+ return uriPart;
411
+ }
412
+ if (base === undefined) {
413
+ throw new TypeError(`${location} uses a relative reference without a document base URI.`);
414
+ }
415
+ if (uriPart.startsWith('/') ||
416
+ uriPart.split('/').some((segment) => segment === '..' || segment === '.')) {
417
+ throw new TypeError(`${location} must stay within the schema registry root.`);
418
+ }
419
+ return base.slice(0, base.lastIndexOf('/') + 1) + uriPart;
420
+ }
421
+ // ---------------------------------------------------------------------------
422
+ // Interpretation.
423
+ // ---------------------------------------------------------------------------
424
+ function validateSubschema(schema, instance, path, errors, program, active, memo) {
425
+ if (typeof schema === 'boolean') {
426
+ if (!schema) {
427
+ errors.push({ instancePath: path, keyword: 'false', message: 'boolean schema is false' });
428
+ }
429
+ return schema;
430
+ }
431
+ const cached = memoizedVerdict(memo, schema, path, instance);
432
+ if (cached !== undefined) {
433
+ for (const diagnostic of cached.diagnostics) {
434
+ errors.push({ ...diagnostic });
435
+ }
436
+ return cached.valid;
437
+ }
438
+ if (active.has(schema)) {
439
+ // Unreachable for admitted schemas: contributed schemas are checked to be
440
+ // non-recursive, and canonical recursion always descends into a child
441
+ // instance value (which resets this set). Guarded anyway so a
442
+ // hypothetical non-progressing reference cycle fails closed instead of
443
+ // looping.
444
+ throw new RangeError('Schema evaluation cycled without consuming instance input.');
445
+ }
446
+ active.add(schema);
447
+ const firstNewError = errors.length;
448
+ let valid;
449
+ try {
450
+ valid = validateSchemaNode(schema, instance, path, errors, program, active, memo);
451
+ }
452
+ finally {
453
+ active.delete(schema);
454
+ }
455
+ const diagnostics = uniqueDiagnostics(errors.slice(firstNewError));
456
+ if (valid === diagnostics.length > 0) {
457
+ throw new TypeError('Subschema validation verdict and diagnostics disagree.');
458
+ }
459
+ memoizeVerdict(memo, schema, path, instance, { diagnostics, valid });
460
+ return valid;
461
+ }
462
+ function validateSchemaNode(schema, instance, path, errors, program, active, memo) {
463
+ let valid = true;
464
+ const fail = (keyword, message, at = path) => {
465
+ valid = false;
466
+ errors.push({ instancePath: at, keyword, message });
467
+ };
468
+ if (schema.$ref !== undefined) {
469
+ const target = program.references.get(schema);
470
+ if (target === undefined) {
471
+ throw new TypeError('Schema reference was not resolved at compile time.');
472
+ }
473
+ if (!validateSubschema(target, instance, path, errors, program, active, memo)) {
474
+ valid = false;
475
+ }
476
+ }
477
+ const typeOperand = schema.type;
478
+ if (typeof typeOperand === 'string') {
479
+ if (!matchesType(typeOperand, instance)) {
480
+ fail('type', `must be ${typeOperand}`);
481
+ }
482
+ }
483
+ else if (Array.isArray(typeOperand)) {
484
+ if (!typeOperand.some((name) => typeof name === 'string' && matchesType(name, instance))) {
485
+ fail('type', `must be ${typeOperand.join(',')}`);
486
+ }
487
+ }
488
+ if (schema.enum !== undefined && Array.isArray(schema.enum)) {
489
+ if (!schema.enum.some((member) => deepEqual(member, instance))) {
490
+ fail('enum', 'must be equal to one of the allowed values');
491
+ }
492
+ }
493
+ if (Object.hasOwn(schema, 'const') && !deepEqual(schema.const, instance)) {
494
+ fail('const', 'must be equal to constant');
495
+ }
496
+ validateCombinators(schema, instance, path, errors, program, active, memo, fail);
497
+ if (typeof instance === 'string') {
498
+ validateStringKeywords(schema, instance, fail, program);
499
+ }
500
+ else if (typeof instance === 'number' && Number.isFinite(instance)) {
501
+ validateNumberKeywords(schema, instance, fail);
502
+ }
503
+ else if (Array.isArray(instance)) {
504
+ if (!validateArrayKeywords(schema, instance, path, errors, program, memo, fail)) {
505
+ valid = false;
506
+ }
507
+ }
508
+ else if (isObjectInstance(instance)) {
509
+ if (!validateObjectKeywords(schema, instance, path, errors, program, memo, fail)) {
510
+ valid = false;
511
+ }
512
+ }
513
+ return valid;
514
+ }
515
+ function validateCombinators(schema, instance, path, errors, program, active, memo, fail) {
516
+ const speculate = (subschema) => {
517
+ const scratch = [];
518
+ return validateSubschema(subschema, instance, path, scratch, program, active, memo);
519
+ };
520
+ if (Array.isArray(schema.allOf)) {
521
+ for (const member of schema.allOf) {
522
+ if (!validateSubschema(member, instance, path, errors, program, active, memo)) {
523
+ fail('allOf', 'must match all schemas in allOf');
524
+ }
525
+ }
526
+ }
527
+ if (Array.isArray(schema.anyOf)) {
528
+ if (!schema.anyOf.some((member) => speculate(member))) {
529
+ fail('anyOf', 'must match a schema in anyOf');
530
+ }
531
+ }
532
+ if (Array.isArray(schema.oneOf)) {
533
+ let matches = 0;
534
+ for (const member of schema.oneOf) {
535
+ if (speculate(member) && (matches += 1) > 1) {
536
+ break;
537
+ }
538
+ }
539
+ if (matches !== 1) {
540
+ fail('oneOf', 'must match exactly one schema in oneOf');
541
+ }
542
+ }
543
+ if (schema.not !== undefined && speculate(schema.not)) {
544
+ fail('not', 'must NOT be valid');
545
+ }
546
+ if (schema.if !== undefined) {
547
+ const branch = speculate(schema.if) ? schema.then : schema.else;
548
+ if (branch !== undefined &&
549
+ !validateSubschema(branch, instance, path, errors, program, active, memo)) {
550
+ fail('if', 'must match the conditional schema');
551
+ }
552
+ }
553
+ }
554
+ function validateStringKeywords(schema, instance, fail, program) {
555
+ const minLength = schema.minLength;
556
+ const maxLength = schema.maxLength;
557
+ if (typeof minLength === 'number' || typeof maxLength === 'number') {
558
+ const length = codePointLength(instance);
559
+ if (typeof minLength === 'number' && length < minLength) {
560
+ fail('minLength', `must NOT have fewer than ${minLength} characters`);
561
+ }
562
+ if (typeof maxLength === 'number' && length > maxLength) {
563
+ fail('maxLength', `must NOT have more than ${maxLength} characters`);
564
+ }
565
+ }
566
+ if (typeof schema.pattern === 'string') {
567
+ const pattern = program.patterns.get(schema);
568
+ if (pattern === undefined) {
569
+ throw new TypeError('Schema pattern was not compiled.');
570
+ }
571
+ if (!pattern.test(instance)) {
572
+ fail('pattern', `must match pattern "${schema.pattern}"`);
573
+ }
574
+ }
575
+ }
576
+ function validateNumberKeywords(schema, instance, fail) {
577
+ if (typeof schema.minimum === 'number' && instance < schema.minimum) {
578
+ fail('minimum', `must be >= ${schema.minimum}`);
579
+ }
580
+ if (typeof schema.maximum === 'number' && instance > schema.maximum) {
581
+ fail('maximum', `must be <= ${schema.maximum}`);
582
+ }
583
+ if (typeof schema.exclusiveMinimum === 'number' && instance <= schema.exclusiveMinimum) {
584
+ fail('exclusiveMinimum', `must be > ${schema.exclusiveMinimum}`);
585
+ }
586
+ if (typeof schema.exclusiveMaximum === 'number' && instance >= schema.exclusiveMaximum) {
587
+ fail('exclusiveMaximum', `must be < ${schema.exclusiveMaximum}`);
588
+ }
589
+ if (typeof schema.multipleOf === 'number') {
590
+ // Studio compares the canonical base-10 coefficients exactly. Binary
591
+ // division and epsilon tolerances are both runtime-dependent at decimal
592
+ // boundaries (for example 4.02 / 0.01), so neither is a portable rule.
593
+ if (!isCanonicalDecimalMultiple(instance, schema.multipleOf)) {
594
+ fail('multipleOf', `must be multiple of ${schema.multipleOf}`);
595
+ }
596
+ }
597
+ }
598
+ function isCanonicalDecimalMultiple(instance, divisor) {
599
+ const value = canonicalDecimal(instance);
600
+ const multiple = canonicalDecimal(divisor);
601
+ const exponentDifference = value.exponent - multiple.exponent;
602
+ if (exponentDifference >= 0) {
603
+ return (value.coefficient * 10n ** BigInt(exponentDifference)) % multiple.coefficient === 0n;
604
+ }
605
+ return value.coefficient % (multiple.coefficient * 10n ** BigInt(-exponentDifference)) === 0n;
606
+ }
607
+ function canonicalDecimal(value) {
608
+ const source = JSON.stringify(Object.is(value, -0) ? 0 : value);
609
+ const match = /^(-?)(\d+)(?:\.(\d+))?(?:e([+-]?\d+))?$/u.exec(source);
610
+ if (match === null) {
611
+ throw new TypeError('Canonical decimal conversion requires a finite number.');
612
+ }
613
+ const fraction = match[3] ?? '';
614
+ let coefficient = BigInt(`${match[1] ?? ''}${match[2]}${fraction}`);
615
+ let exponent = Number(match[4] ?? 0) - fraction.length;
616
+ while (coefficient !== 0n && coefficient % 10n === 0n) {
617
+ coefficient /= 10n;
618
+ exponent += 1;
619
+ }
620
+ return { coefficient, exponent };
621
+ }
622
+ function validateArrayKeywords(schema, instance, path, errors, program, memo, fail) {
623
+ let valid = true;
624
+ const child = (subschema, index) => {
625
+ if (!validateSubschema(subschema, instance[index], `${path}/${index}`, errors, program, new Set(), memo)) {
626
+ valid = false;
627
+ }
628
+ };
629
+ const prefixItems = Array.isArray(schema.prefixItems) ? schema.prefixItems : undefined;
630
+ const prefixLength = prefixItems?.length ?? 0;
631
+ if (prefixItems !== undefined) {
632
+ for (let index = 0; index < Math.min(prefixLength, instance.length); index += 1) {
633
+ child(prefixItems[index], index);
634
+ }
635
+ }
636
+ const items = schema.items;
637
+ if (items !== undefined && instance.length > prefixLength) {
638
+ if (items === false) {
639
+ fail('items', `must NOT have more than ${prefixLength} items`);
640
+ }
641
+ else if (items !== true) {
642
+ for (let index = prefixLength; index < instance.length; index += 1) {
643
+ child(items, index);
644
+ }
645
+ }
646
+ }
647
+ if (typeof schema.minItems === 'number' && instance.length < schema.minItems) {
648
+ fail('minItems', `must NOT have fewer than ${schema.minItems} items`);
649
+ }
650
+ if (typeof schema.maxItems === 'number' && instance.length > schema.maxItems) {
651
+ fail('maxItems', `must NOT have more than ${schema.maxItems} items`);
652
+ }
653
+ if (schema.uniqueItems === true) {
654
+ const duplicate = findDuplicateIndexes(instance);
655
+ if (duplicate !== undefined) {
656
+ fail('uniqueItems', `must NOT have duplicate items (items ## ${duplicate[0]} and ${duplicate[1]} are identical)`);
657
+ }
658
+ }
659
+ return valid;
660
+ }
661
+ function validateObjectKeywords(schema, instance, path, errors, program, memo, fail) {
662
+ let valid = true;
663
+ // A member whose value is `undefined` is not JSON and is treated as absent,
664
+ // matching how the previous validator saw JSON-decoded instances.
665
+ const memberNames = Object.keys(instance)
666
+ .filter((name) => instance[name] !== undefined)
667
+ .sort(compareCodeUnits);
668
+ const present = (name) => Object.hasOwn(instance, name) && instance[name] !== undefined;
669
+ const properties = isSchemaNode(schema.properties) ? schema.properties : undefined;
670
+ if (properties !== undefined) {
671
+ for (const [name, subschema] of sortedEntries(properties)) {
672
+ if (present(name) &&
673
+ !validateSubschema(subschema, instance[name], `${path}/${escapeToken(name)}`, errors, program, new Set(), memo)) {
674
+ valid = false;
675
+ }
676
+ }
677
+ }
678
+ if (Array.isArray(schema.required)) {
679
+ for (const name of sortedStrings(schema.required)) {
680
+ if (!present(name)) {
681
+ fail('required', `must have required property '${name}'`);
682
+ }
683
+ }
684
+ }
685
+ const additional = schema.additionalProperties;
686
+ if (additional !== undefined) {
687
+ for (const name of memberNames) {
688
+ if (properties !== undefined && Object.hasOwn(properties, name)) {
689
+ continue;
690
+ }
691
+ if (additional === false) {
692
+ fail('additionalProperties', 'must NOT have additional properties');
693
+ }
694
+ else if (additional !== true &&
695
+ !validateSubschema(additional, instance[name], `${path}/${escapeToken(name)}`, errors, program, new Set(), memo)) {
696
+ valid = false;
697
+ }
698
+ }
699
+ }
700
+ const propertyNames = schema.propertyNames;
701
+ if (propertyNames !== undefined) {
702
+ for (const name of memberNames) {
703
+ const scratch = [];
704
+ if (!validateSubschema(propertyNames, name, path, scratch, program, new Set(), memo)) {
705
+ fail('propertyNames', `property name '${name}' is invalid`);
706
+ }
707
+ }
708
+ }
709
+ const dependentRequired = schema.dependentRequired;
710
+ if (isSchemaNode(dependentRequired)) {
711
+ for (const [trigger, dependents] of sortedEntries(dependentRequired)) {
712
+ if (!present(trigger) || !Array.isArray(dependents)) {
713
+ continue;
714
+ }
715
+ for (const dependent of sortedStrings(dependents)) {
716
+ if (!present(dependent)) {
717
+ fail('dependentRequired', `must have property ${dependent} when property ${trigger} is present`);
718
+ }
719
+ }
720
+ }
721
+ }
722
+ if (typeof schema.minProperties === 'number' && memberNames.length < schema.minProperties) {
723
+ fail('minProperties', `must NOT have fewer than ${schema.minProperties} properties`);
724
+ }
725
+ if (typeof schema.maxProperties === 'number' && memberNames.length > schema.maxProperties) {
726
+ fail('maxProperties', `must NOT have more than ${schema.maxProperties} properties`);
727
+ }
728
+ return valid;
729
+ }
730
+ // ---------------------------------------------------------------------------
731
+ // Shared helpers.
732
+ // ---------------------------------------------------------------------------
733
+ function memoizedVerdict(memo, schema, path, instance) {
734
+ const instances = memo.get(schema)?.get(path);
735
+ return instances?.get(instance);
736
+ }
737
+ function memoizeVerdict(memo, schema, path, instance, result) {
738
+ let locations = memo.get(schema);
739
+ if (locations === undefined) {
740
+ locations = new Map();
741
+ memo.set(schema, locations);
742
+ }
743
+ let instances = locations.get(path);
744
+ if (instances === undefined) {
745
+ instances = new Map();
746
+ locations.set(path, instances);
747
+ }
748
+ instances.set(instance, result);
749
+ }
750
+ function uniqueDiagnostics(errors) {
751
+ const keys = new Set();
752
+ const diagnostics = [];
753
+ for (const error of errors) {
754
+ const key = JSON.stringify([error.instancePath, error.keyword, error.message]);
755
+ if (keys.has(key)) {
756
+ continue;
757
+ }
758
+ keys.add(key);
759
+ diagnostics.push({ ...error });
760
+ }
761
+ return diagnostics;
762
+ }
763
+ function matchesType(name, instance) {
764
+ switch (name) {
765
+ case 'array':
766
+ return Array.isArray(instance);
767
+ case 'boolean':
768
+ return typeof instance === 'boolean';
769
+ case 'integer':
770
+ return typeof instance === 'number' && Number.isFinite(instance) && instance % 1 === 0;
771
+ case 'null':
772
+ return instance === null;
773
+ case 'number':
774
+ return typeof instance === 'number' && Number.isFinite(instance);
775
+ case 'object':
776
+ return isObjectInstance(instance);
777
+ case 'string':
778
+ return typeof instance === 'string';
779
+ default:
780
+ return false;
781
+ }
782
+ }
783
+ function findDuplicateIndexes(instance) {
784
+ for (let second = 1; second < instance.length; second += 1) {
785
+ for (let first = 0; first < second; first += 1) {
786
+ if (deepEqual(instance[second], instance[first])) {
787
+ return [first, second];
788
+ }
789
+ }
790
+ }
791
+ return undefined;
792
+ }
793
+ /** JSON-value deep equality (order-insensitive for objects). */
794
+ function deepEqual(a, b) {
795
+ if (a === b) {
796
+ return true;
797
+ }
798
+ if (Array.isArray(a) && Array.isArray(b)) {
799
+ if (a.length !== b.length) {
800
+ return false;
801
+ }
802
+ for (let index = 0; index < a.length; index += 1) {
803
+ if (!deepEqual(a[index], b[index])) {
804
+ return false;
805
+ }
806
+ }
807
+ return true;
808
+ }
809
+ if (typeof a === 'object' &&
810
+ typeof b === 'object' &&
811
+ a !== null &&
812
+ b !== null &&
813
+ !Array.isArray(a) &&
814
+ !Array.isArray(b)) {
815
+ const aKeys = Object.keys(a);
816
+ const bRecord = b;
817
+ if (aKeys.length !== Object.keys(b).length) {
818
+ return false;
819
+ }
820
+ for (const key of aKeys) {
821
+ if (!Object.hasOwn(b, key) || !deepEqual(a[key], bRecord[key])) {
822
+ return false;
823
+ }
824
+ }
825
+ return true;
826
+ }
827
+ return false;
828
+ }
829
+ /** String length in Unicode code points, as JSON Schema requires. */
830
+ function codePointLength(value) {
831
+ let length = 0;
832
+ for (let index = 0; index < value.length; index += 1) {
833
+ length += 1;
834
+ const code = value.charCodeAt(index);
835
+ if (code >= 0xd800 && code <= 0xdbff && index + 1 < value.length) {
836
+ const next = value.charCodeAt(index + 1);
837
+ if ((next & 0xfc00) === 0xdc00) {
838
+ index += 1;
839
+ }
840
+ }
841
+ }
842
+ return length;
843
+ }
844
+ function isObjectInstance(value) {
845
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
846
+ }
847
+ function isSchemaNode(value) {
848
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
849
+ }
850
+ function isDenseArray(value) {
851
+ if (!Array.isArray(value)) {
852
+ return false;
853
+ }
854
+ const keys = Object.keys(value);
855
+ return keys.length === value.length && keys.every((key, index) => key === String(index));
856
+ }
857
+ function sortedEntries(value) {
858
+ return Object.entries(value).sort(([left], [right]) => compareCodeUnits(left, right));
859
+ }
860
+ function sortedStrings(values) {
861
+ const strings = [];
862
+ for (const value of values) {
863
+ if (typeof value === 'string') {
864
+ strings.push(value);
865
+ }
866
+ }
867
+ return strings.sort(compareCodeUnits);
868
+ }
869
+ function compareCodeUnits(left, right) {
870
+ return left < right ? -1 : left > right ? 1 : 0;
871
+ }
872
+ function appendPointer(pointer, token) {
873
+ return `${pointer}/${escapeToken(token)}`;
874
+ }
875
+ function escapeToken(token) {
876
+ return token.replaceAll('~', '~0').replaceAll('/', '~1');
877
+ }
878
+ function unescapeToken(token, location) {
879
+ if (/(?:~[^01]|~$)/u.test(token)) {
880
+ throw new TypeError(`${location} is not a valid JSON Pointer reference.`);
881
+ }
882
+ return token.replaceAll('~1', '/').replaceAll('~0', '~');
883
+ }
884
+ function describeLocation(document, pointer) {
885
+ return `${document.baseUri ?? 'schema'}#${pointer}`;
886
+ }
887
+ //# sourceMappingURL=profile-validator.js.map