@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.
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/THIRD_PARTY_NOTICES.md +9 -0
- package/dist/binding-projection.d.ts +47 -0
- package/dist/binding-projection.d.ts.map +1 -0
- package/dist/binding-projection.js +253 -0
- package/dist/binding-projection.js.map +1 -0
- package/dist/canonical.d.ts +20 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/canonical.js +100 -0
- package/dist/canonical.js.map +1 -0
- package/dist/clone.d.ts +2 -0
- package/dist/clone.d.ts.map +1 -0
- package/dist/clone.js +4 -0
- package/dist/clone.js.map +1 -0
- package/dist/commands.d.ts +18 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +799 -0
- package/dist/commands.js.map +1 -0
- package/dist/contributions.d.ts +93 -0
- package/dist/contributions.d.ts.map +1 -0
- package/dist/contributions.js +435 -0
- package/dist/contributions.js.map +1 -0
- package/dist/entry-commands.d.ts +3 -0
- package/dist/entry-commands.d.ts.map +1 -0
- package/dist/entry-commands.js +36 -0
- package/dist/entry-commands.js.map +1 -0
- package/dist/extension-sdk.d.ts +51 -0
- package/dist/extension-sdk.d.ts.map +1 -0
- package/dist/extension-sdk.js +260 -0
- package/dist/extension-sdk.js.map +1 -0
- package/dist/history.d.ts +22 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +81 -0
- package/dist/history.js.map +1 -0
- package/dist/host-session.d.ts +74 -0
- package/dist/host-session.d.ts.map +1 -0
- package/dist/host-session.js +713 -0
- package/dist/host-session.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/layout.d.ts +66 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +240 -0
- package/dist/layout.js.map +1 -0
- package/dist/migrations.d.ts +47 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +83 -0
- package/dist/migrations.js.map +1 -0
- package/dist/model-commands.d.ts +3 -0
- package/dist/model-commands.d.ts.map +1 -0
- package/dist/model-commands.js +21 -0
- package/dist/model-commands.js.map +1 -0
- package/dist/modes.d.ts +47 -0
- package/dist/modes.d.ts.map +1 -0
- package/dist/modes.js +280 -0
- package/dist/modes.js.map +1 -0
- package/dist/negotiation.d.ts +23 -0
- package/dist/negotiation.d.ts.map +1 -0
- package/dist/negotiation.js +65 -0
- package/dist/negotiation.js.map +1 -0
- package/dist/production-values.d.ts +12 -0
- package/dist/production-values.d.ts.map +1 -0
- package/dist/production-values.js +224 -0
- package/dist/production-values.js.map +1 -0
- package/dist/production.d.ts +86 -0
- package/dist/production.d.ts.map +1 -0
- package/dist/production.js +811 -0
- package/dist/production.js.map +1 -0
- package/dist/profile-validator.d.ts +47 -0
- package/dist/profile-validator.d.ts.map +1 -0
- package/dist/profile-validator.js +887 -0
- package/dist/profile-validator.js.map +1 -0
- package/dist/recipes.d.ts +15 -0
- package/dist/recipes.d.ts.map +1 -0
- package/dist/recipes.js +34 -0
- package/dist/recipes.js.map +1 -0
- package/dist/registry.d.ts +17 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +56 -0
- package/dist/registry.js.map +1 -0
- package/dist/schema-profile.d.ts +27 -0
- package/dist/schema-profile.d.ts.map +1 -0
- package/dist/schema-profile.js +1015 -0
- package/dist/schema-profile.js.map +1 -0
- package/dist/semver.d.ts +20 -0
- package/dist/semver.d.ts.map +1 -0
- package/dist/semver.js +128 -0
- package/dist/semver.js.map +1 -0
- package/dist/session.d.ts +73 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +191 -0
- package/dist/session.js.map +1 -0
- package/dist/url-policy.d.ts +45 -0
- package/dist/url-policy.d.ts.map +1 -0
- package/dist/url-policy.js +248 -0
- package/dist/url-policy.js.map +1 -0
- package/dist/validation.d.ts +13 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +444 -0
- package/dist/validation.js.map +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1,1015 @@
|
|
|
1
|
+
import { canonicalStringify } from './canonical.js';
|
|
2
|
+
import { compileProfileSchema } from './profile-validator.js';
|
|
3
|
+
const DRAFT_2020_12 = 'https://json-schema.org/draft/2020-12/schema';
|
|
4
|
+
const MAX_ALTERNATIVES = 64;
|
|
5
|
+
const MAX_DESCRIPTION_LENGTH = 10_000;
|
|
6
|
+
const MAX_ENUM_MEMBERS = 1_024;
|
|
7
|
+
const MAX_EXAMPLES = 100;
|
|
8
|
+
const MAX_JSON_DEPTH = 64;
|
|
9
|
+
const MAX_JSON_ITEMS = 10_000;
|
|
10
|
+
const MAX_JSON_PROPERTIES = 1_000;
|
|
11
|
+
const MAX_REFERENCES = 128;
|
|
12
|
+
const MAX_REFERENCE_LENGTH = 500;
|
|
13
|
+
const MAX_SCHEMA_BYTES = 262_144;
|
|
14
|
+
const MAX_SCHEMA_DEPTH = 32;
|
|
15
|
+
const MAX_SCHEMA_MAP_PROPERTIES = 512;
|
|
16
|
+
const MAX_SCHEMA_NODES = 1_024;
|
|
17
|
+
const MAX_OBJECT_KEY_LENGTH = 200;
|
|
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. */
|
|
21
|
+
export const STUDIO_SCHEMA_PROFILE_LIMITS = Object.freeze({
|
|
22
|
+
maxAlternatives: MAX_ALTERNATIVES,
|
|
23
|
+
maxDescriptionLength: MAX_DESCRIPTION_LENGTH,
|
|
24
|
+
maxEnumMembers: MAX_ENUM_MEMBERS,
|
|
25
|
+
maxExamples: MAX_EXAMPLES,
|
|
26
|
+
maxJsonDepth: MAX_JSON_DEPTH,
|
|
27
|
+
maxJsonItems: MAX_JSON_ITEMS,
|
|
28
|
+
maxJsonProperties: MAX_JSON_PROPERTIES,
|
|
29
|
+
maxObjectKeyLength: MAX_OBJECT_KEY_LENGTH,
|
|
30
|
+
maxPropertyNames: MAX_PROPERTY_NAMES,
|
|
31
|
+
maxReferenceLength: MAX_REFERENCE_LENGTH,
|
|
32
|
+
maxReferences: MAX_REFERENCES,
|
|
33
|
+
maxSchemaBytes: MAX_SCHEMA_BYTES,
|
|
34
|
+
maxSchemaDepth: MAX_SCHEMA_DEPTH,
|
|
35
|
+
maxSchemaMapProperties: MAX_SCHEMA_MAP_PROPERTIES,
|
|
36
|
+
maxSchemaNodes: MAX_SCHEMA_NODES,
|
|
37
|
+
maxTitleLength: MAX_TITLE_LENGTH,
|
|
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
|
+
}
|
|
60
|
+
const allowedKeywords = new Set([
|
|
61
|
+
'$defs',
|
|
62
|
+
'$ref',
|
|
63
|
+
'$schema',
|
|
64
|
+
'additionalProperties',
|
|
65
|
+
'allOf',
|
|
66
|
+
'anyOf',
|
|
67
|
+
'const',
|
|
68
|
+
'default',
|
|
69
|
+
'dependentRequired',
|
|
70
|
+
'description',
|
|
71
|
+
'else',
|
|
72
|
+
'enum',
|
|
73
|
+
'examples',
|
|
74
|
+
'exclusiveMaximum',
|
|
75
|
+
'exclusiveMinimum',
|
|
76
|
+
'if',
|
|
77
|
+
'items',
|
|
78
|
+
'maxItems',
|
|
79
|
+
'maxLength',
|
|
80
|
+
'maxProperties',
|
|
81
|
+
'maximum',
|
|
82
|
+
'minItems',
|
|
83
|
+
'minLength',
|
|
84
|
+
'minProperties',
|
|
85
|
+
'minimum',
|
|
86
|
+
'multipleOf',
|
|
87
|
+
'not',
|
|
88
|
+
'oneOf',
|
|
89
|
+
'prefixItems',
|
|
90
|
+
'properties',
|
|
91
|
+
'propertyNames',
|
|
92
|
+
'readOnly',
|
|
93
|
+
'required',
|
|
94
|
+
'then',
|
|
95
|
+
'title',
|
|
96
|
+
'type',
|
|
97
|
+
'uniqueItems',
|
|
98
|
+
'writeOnly',
|
|
99
|
+
]);
|
|
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.
|
|
118
|
+
try {
|
|
119
|
+
assertCanonicalSchemaByteBudget(schema);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
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
|
+
}
|
|
133
|
+
}
|
|
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;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
return compileProfileSchema(schema);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
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);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/** Assert that a value is an admitted Studio property schema. */
|
|
158
|
+
export function assertStudioPropertySchema(schema) {
|
|
159
|
+
compileStudioPropertySchema(schema);
|
|
160
|
+
}
|
|
161
|
+
function visitSchema(value, path, depth, state) {
|
|
162
|
+
if (!isRecord(value)) {
|
|
163
|
+
reject('invalid-keyword-value', path, `${displayPath(path)} must be a JSON Schema object.`);
|
|
164
|
+
}
|
|
165
|
+
trackObject(value, path, state);
|
|
166
|
+
trackSchemaNode(path, depth, state);
|
|
167
|
+
for (const [keyword, operand] of boundedSchemaEntries(value)) {
|
|
168
|
+
const keywordPath = appendPointer(path, keyword);
|
|
169
|
+
assertSafeObjectKey(keyword, path);
|
|
170
|
+
if (!allowedKeywords.has(keyword)) {
|
|
171
|
+
reject('unsupported-keyword', keywordPath, `${displayPath(keywordPath)} uses keyword ${JSON.stringify(keyword)}, which is not allowed by the Studio Schema Profile.`);
|
|
172
|
+
}
|
|
173
|
+
switch (keyword) {
|
|
174
|
+
case '$defs':
|
|
175
|
+
case 'properties':
|
|
176
|
+
visitSchemaMap(operand, keywordPath, depth + 1, state);
|
|
177
|
+
break;
|
|
178
|
+
case 'additionalProperties':
|
|
179
|
+
case 'else':
|
|
180
|
+
case 'if':
|
|
181
|
+
case 'items':
|
|
182
|
+
case 'not':
|
|
183
|
+
case 'propertyNames':
|
|
184
|
+
case 'then':
|
|
185
|
+
visitSubschema(operand, keywordPath, depth + 1, state);
|
|
186
|
+
break;
|
|
187
|
+
case 'allOf':
|
|
188
|
+
case 'anyOf':
|
|
189
|
+
case 'oneOf':
|
|
190
|
+
case 'prefixItems':
|
|
191
|
+
visitSchemaArray(operand, keywordPath, depth + 1, state);
|
|
192
|
+
break;
|
|
193
|
+
case '$ref':
|
|
194
|
+
visitReference(operand, keywordPath, state);
|
|
195
|
+
break;
|
|
196
|
+
case '$schema':
|
|
197
|
+
if (operand !== DRAFT_2020_12) {
|
|
198
|
+
reject('invalid-keyword-value', keywordPath, `${displayPath(keywordPath)} must declare JSON Schema Draft 2020-12.`);
|
|
199
|
+
}
|
|
200
|
+
break;
|
|
201
|
+
case 'enum':
|
|
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.`);
|
|
240
|
+
}
|
|
241
|
+
break;
|
|
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;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function visitSchemaMap(value, path, depth, state) {
|
|
257
|
+
if (!isRecord(value)) {
|
|
258
|
+
reject('invalid-keyword-value', path, `${displayPath(path)} must be an object of schemas.`);
|
|
259
|
+
}
|
|
260
|
+
trackObject(value, path, state);
|
|
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.`);
|
|
264
|
+
}
|
|
265
|
+
for (const name of keys.sort(compareCodeUnits)) {
|
|
266
|
+
assertSafeObjectKey(name, path);
|
|
267
|
+
visitSchema(value[name], appendPointer(path, name), depth, state);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function visitSchemaArray(value, path, depth, state) {
|
|
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.`);
|
|
279
|
+
}
|
|
280
|
+
trackObject(value, path, state);
|
|
281
|
+
for (const [index, schema] of value.entries()) {
|
|
282
|
+
visitSubschema(schema, appendPointer(path, String(index)), depth, state);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function visitSubschema(value, path, depth, state) {
|
|
286
|
+
if (typeof value === 'boolean') {
|
|
287
|
+
trackSchemaNode(path, depth, state);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
visitSchema(value, path, depth, state);
|
|
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
|
+
}
|
|
301
|
+
function visitReference(value, path, state) {
|
|
302
|
+
if (!isPortableLocalReference(value)) {
|
|
303
|
+
reject('invalid-reference', path, `${displayPath(path)} must be a bounded local JSON Pointer reference.`);
|
|
304
|
+
}
|
|
305
|
+
state.references += 1;
|
|
306
|
+
if (state.references > MAX_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.`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
function visitJsonValue(value, path, depth, state) {
|
|
417
|
+
if (value === null ||
|
|
418
|
+
typeof value === 'boolean' ||
|
|
419
|
+
typeof value === 'string' ||
|
|
420
|
+
(typeof value === 'number' && Number.isFinite(value))) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (depth > MAX_JSON_DEPTH) {
|
|
424
|
+
reject('limit-exceeded', path, `${displayPath(path)} exceeds the Studio Schema Profile JSON depth limit.`);
|
|
425
|
+
}
|
|
426
|
+
if (Array.isArray(value)) {
|
|
427
|
+
if (!isDenseArray(value)) {
|
|
428
|
+
reject('invalid-keyword-value', path, `${displayPath(path)} must be a dense JSON array.`);
|
|
429
|
+
}
|
|
430
|
+
trackObject(value, path, state);
|
|
431
|
+
if (value.length > MAX_JSON_ITEMS) {
|
|
432
|
+
reject('limit-exceeded', path, `${displayPath(path)} exceeds ${MAX_JSON_ITEMS} JSON items.`);
|
|
433
|
+
}
|
|
434
|
+
for (const [index, entry] of value.entries()) {
|
|
435
|
+
visitJsonValue(entry, appendPointer(path, String(index)), depth + 1, state);
|
|
436
|
+
}
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (isRecord(value)) {
|
|
440
|
+
trackObject(value, path, state);
|
|
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.`);
|
|
444
|
+
}
|
|
445
|
+
for (const key of keys.sort(compareCodeUnits)) {
|
|
446
|
+
assertSafeObjectKey(key, path);
|
|
447
|
+
visitJsonValue(value[key], appendPointer(path, key), depth + 1, state);
|
|
448
|
+
}
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
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
|
+
}
|
|
462
|
+
}
|
|
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
|
+
}
|
|
535
|
+
if (key.length === 0 ||
|
|
536
|
+
key === '__proto__' ||
|
|
537
|
+
key === 'constructor' ||
|
|
538
|
+
key === 'prototype' ||
|
|
539
|
+
containsControlCharacter(key)) {
|
|
540
|
+
reject('unsafe-member', rejectionPath, `${displayPath(path)} contains forbidden object member name ${JSON.stringify(key)}.`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
function assertNonRecursiveSchema(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
|
+
];
|
|
590
|
+
while (stack.length > 0) {
|
|
591
|
+
const frame = stack.pop();
|
|
592
|
+
if (frame === undefined || expanded.has(frame.node)) {
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
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;
|
|
601
|
+
}
|
|
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;
|
|
681
|
+
}
|
|
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);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
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.'));
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
const failure = firstAdmissionFailure(root, failures);
|
|
697
|
+
if (failure !== undefined) {
|
|
698
|
+
throw failure;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
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
|
+
}
|
|
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;
|
|
755
|
+
}
|
|
756
|
+
function resolveLocalReference(root, reference, path) {
|
|
757
|
+
if (reference === '#') {
|
|
758
|
+
return { schemaPosition: true, value: root };
|
|
759
|
+
}
|
|
760
|
+
let current = root;
|
|
761
|
+
let position = 'schema';
|
|
762
|
+
for (const encodedToken of reference.slice(2).split('/')) {
|
|
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
|
+
}
|
|
794
|
+
if (!isRecord(current) && !Array.isArray(current)) {
|
|
795
|
+
reject('invalid-reference', path, `Local schema reference ${reference} does not resolve to a schema.`);
|
|
796
|
+
}
|
|
797
|
+
if (!Object.hasOwn(current, token)) {
|
|
798
|
+
reject('invalid-reference', path, `Local schema reference ${reference} does not resolve to a schema.`);
|
|
799
|
+
}
|
|
800
|
+
current = current[token];
|
|
801
|
+
position = nextPosition;
|
|
802
|
+
}
|
|
803
|
+
if (typeof current !== 'boolean' && !isRecord(current)) {
|
|
804
|
+
reject('invalid-reference', path, `Local schema reference ${reference} does not resolve to a schema.`);
|
|
805
|
+
}
|
|
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;
|
|
816
|
+
}
|
|
817
|
+
function containsControlCharacter(value) {
|
|
818
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
819
|
+
const code = value.charCodeAt(index);
|
|
820
|
+
if (code <= 0x1f || code === 0x7f) {
|
|
821
|
+
return true;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
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));
|
|
831
|
+
}
|
|
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
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return length;
|
|
845
|
+
}
|
|
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
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
function consumeCanonicalJsonString(value, consume) {
|
|
931
|
+
consume(2); // opening and closing quotes
|
|
932
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
933
|
+
const code = value.charCodeAt(index);
|
|
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);
|
|
948
|
+
}
|
|
949
|
+
else if (code <= 0x7ff) {
|
|
950
|
+
consume(2);
|
|
951
|
+
}
|
|
952
|
+
else if (code >= 0xd800 && code <= 0xdbff) {
|
|
953
|
+
const next = value.charCodeAt(index + 1);
|
|
954
|
+
if (index + 1 < value.length && (next & 0xfc00) === 0xdc00) {
|
|
955
|
+
consume(4);
|
|
956
|
+
index += 1;
|
|
957
|
+
}
|
|
958
|
+
else {
|
|
959
|
+
consume(6);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
963
|
+
consume(6);
|
|
964
|
+
}
|
|
965
|
+
else {
|
|
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;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
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;
|
|
1014
|
+
}
|
|
1015
|
+
//# sourceMappingURL=schema-profile.js.map
|