@lesliechan721/aw-plugin-core 0.1.0-beta.5 → 0.1.0-beta.51

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 (49) hide show
  1. package/dist/aw-capabilities.d.ts +27 -0
  2. package/dist/aw-capabilities.js +114 -0
  3. package/dist/aw-capabilities.js.map +1 -0
  4. package/dist/catalog-file-modes.d.ts +5 -0
  5. package/dist/catalog-file-modes.js +67 -0
  6. package/dist/catalog-file-modes.js.map +1 -0
  7. package/dist/catalog-workspace-resolution.d.ts +3 -0
  8. package/dist/catalog-workspace-resolution.js +34 -0
  9. package/dist/catalog-workspace-resolution.js.map +1 -0
  10. package/dist/generated/plugin-manifest-v3.d.ts +1350 -0
  11. package/dist/generated/{plugin-manifest-v1.js → plugin-manifest-v3.js} +1 -1
  12. package/dist/generated/plugin-manifest-v3.js.map +1 -0
  13. package/dist/index.d.ts +3 -2
  14. package/dist/index.js +2 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/manifest-contract.d.ts +3 -6
  17. package/dist/manifest-contract.js +224 -398
  18. package/dist/manifest-contract.js.map +1 -1
  19. package/dist/manifest-type-contract.d.ts +2 -2
  20. package/dist/manifest.d.ts +10 -8
  21. package/dist/manifest.js +16 -7
  22. package/dist/manifest.js.map +1 -1
  23. package/dist/plugin-authoring-capabilities.d.ts +1 -1
  24. package/dist/plugin-authoring-capabilities.js +60 -26
  25. package/dist/plugin-authoring-capabilities.js.map +1 -1
  26. package/dist/plugin-config.d.ts +39 -0
  27. package/dist/plugin-config.js +1320 -0
  28. package/dist/plugin-config.js.map +1 -0
  29. package/dist/plugin-requirements.d.ts +34 -0
  30. package/dist/plugin-requirements.js +289 -0
  31. package/dist/plugin-requirements.js.map +1 -0
  32. package/dist/plugin-script-contract.d.ts +11 -12
  33. package/dist/plugin-script-contract.js +44 -71
  34. package/dist/plugin-script-contract.js.map +1 -1
  35. package/dist/runtime-config.d.ts +38 -0
  36. package/dist/runtime-config.js +31 -0
  37. package/dist/runtime-config.js.map +1 -0
  38. package/dist/types.d.ts +249 -61
  39. package/dist/workspace.d.ts +37 -9
  40. package/dist/workspace.js +419 -191
  41. package/dist/workspace.js.map +1 -1
  42. package/package.json +13 -4
  43. package/schemas/plugin-manifest-v3.json +2122 -0
  44. package/dist/generated/plugin-manifest-v1.d.ts +0 -368
  45. package/dist/generated/plugin-manifest-v1.js.map +0 -1
  46. package/dist/plugin-release.d.ts +0 -30
  47. package/dist/plugin-release.js +0 -187
  48. package/dist/plugin-release.js.map +0 -1
  49. package/schemas/plugin-manifest-v1.json +0 -329
@@ -0,0 +1,1320 @@
1
+ import { RE2 } from 're2-wasm';
2
+ const CONFIG_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/u;
3
+ const IDENTIFIER_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/u;
4
+ const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
5
+ const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))(?:\.(?:0|[1-9]\d*|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/u;
6
+ const HOSTNAME_PATTERN = /^(?=.{1,253}$)(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)(?:\.(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?))*$/u;
7
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/u;
8
+ const STRING_FORMATS = new Set([
9
+ 'identifier',
10
+ 'slug',
11
+ 'semver',
12
+ 'url',
13
+ 'hostname',
14
+ 'email',
15
+ ]);
16
+ export const PLUGIN_CONFIG_V3_MAX_DEFINITIONS = 64;
17
+ export const PLUGIN_CONFIG_V3_MAX_FIELDS = 256;
18
+ /** Count each followed $ref edge, including edges from object properties; inline objects add zero. */
19
+ export const PLUGIN_CONFIG_V3_MAX_REF_DEPTH = 8;
20
+ export const PLUGIN_CONFIG_SUMMARY_MAX_PARTS = 4;
21
+ export const PLUGIN_CONFIG_SUMMARY_MAX_SEPARATOR_CODE_POINTS = 8;
22
+ export const PLUGIN_CONFIG_SUMMARY_MAX_PATH_LENGTH = 128;
23
+ export const PLUGIN_CONFIG_SUMMARY_MAX_CODE_POINTS = 256;
24
+ export const PLUGIN_CONFIG_ARRAY_MAX_ITEMS = 128;
25
+ export const PLUGIN_CONFIG_ARRAY_MAX_BYTES = 262144;
26
+ const MAX_OBJECT_PROPERTIES = 32;
27
+ const MAX_PATTERN_CODE_POINTS = 512;
28
+ const MAX_STRING_CODE_POINTS = 4096;
29
+ const DANGEROUS_CONFIG_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
30
+ function createPropertyMap() {
31
+ return Object.create(null);
32
+ }
33
+ function ownProperty(value, property) {
34
+ return value !== undefined && Object.hasOwn(value, property) ? value[property] : undefined;
35
+ }
36
+ function isDangerousConfigKey(value) {
37
+ return DANGEROUS_CONFIG_KEYS.has(value);
38
+ }
39
+ function asObject(value, path) {
40
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
41
+ throw new Error(`Invalid ${path}: expected an object.`);
42
+ }
43
+ return value;
44
+ }
45
+ function assertAllowedKeys(value, path, allowed) {
46
+ for (const key of Object.keys(value)) {
47
+ if (!allowed.includes(key)) {
48
+ throw new Error(`Invalid ${path}: unknown field "${key}".`);
49
+ }
50
+ }
51
+ }
52
+ function nonEmptyString(value, path) {
53
+ if (typeof value !== 'string' || value.trim() === '') {
54
+ throw new Error(`Invalid ${path}: expected a non-empty string.`);
55
+ }
56
+ return value;
57
+ }
58
+ function optionalText(value, path) {
59
+ return value === undefined ? undefined : nonEmptyString(value, path);
60
+ }
61
+ function optionalBoolean(value, path) {
62
+ if (value === undefined)
63
+ return undefined;
64
+ if (typeof value !== 'boolean') {
65
+ throw new Error(`Invalid ${path}: expected a boolean.`);
66
+ }
67
+ return value;
68
+ }
69
+ function finiteNumber(value, path) {
70
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
71
+ throw new Error(`Invalid ${path}: expected a finite number.`);
72
+ }
73
+ return value;
74
+ }
75
+ function nonNegativeInteger(value, path) {
76
+ const result = finiteNumber(value, path);
77
+ if (!Number.isInteger(result) || result < 0) {
78
+ throw new Error(`Invalid ${path}: expected a non-negative integer.`);
79
+ }
80
+ return result;
81
+ }
82
+ function normalizeStringValidation(value, path) {
83
+ const input = asObject(value, path);
84
+ assertAllowedKeys(input, path, ['minLength', 'maxLength', 'pattern', 'format']);
85
+ const minLength = input.minLength === undefined
86
+ ? undefined
87
+ : nonNegativeInteger(input.minLength, `${path}.minLength`);
88
+ const maxLength = input.maxLength === undefined
89
+ ? undefined
90
+ : nonNegativeInteger(input.maxLength, `${path}.maxLength`);
91
+ if (maxLength !== undefined && maxLength > MAX_STRING_CODE_POINTS) {
92
+ throw new Error(`Invalid ${path}.maxLength: expected at most ${MAX_STRING_CODE_POINTS}.`);
93
+ }
94
+ if (minLength !== undefined && maxLength !== undefined && minLength > maxLength) {
95
+ throw new Error(`Invalid ${path}: minLength must be less than or equal to maxLength.`);
96
+ }
97
+ if (input.pattern !== undefined && typeof input.pattern !== 'string') {
98
+ throw new Error(`Invalid ${path}.pattern: expected a string.`);
99
+ }
100
+ const pattern = input.pattern;
101
+ if (pattern !== undefined) {
102
+ if ([...pattern].length > MAX_PATTERN_CODE_POINTS) {
103
+ throw new Error(`Invalid ${path}.pattern: expected at most ${MAX_PATTERN_CODE_POINTS} Unicode code points.`);
104
+ }
105
+ if (maxLength === undefined) {
106
+ throw new Error(`Invalid ${path}: maxLength is required when pattern is declared.`);
107
+ }
108
+ try {
109
+ new RE2(pattern, 'u');
110
+ }
111
+ catch {
112
+ throw new Error(`Invalid ${path}.pattern: expected a valid RE2 regular expression.`);
113
+ }
114
+ }
115
+ const format = input.format === undefined
116
+ ? undefined
117
+ : nonEmptyString(input.format, `${path}.format`);
118
+ if (format !== undefined && !STRING_FORMATS.has(format)) {
119
+ throw new Error(`Invalid ${path}.format: unsupported string format "${format}".`);
120
+ }
121
+ return { minLength, maxLength, pattern, format };
122
+ }
123
+ function normalizeNumberValidation(value, path) {
124
+ const input = asObject(value, path);
125
+ assertAllowedKeys(input, path, ['integer', 'minimum', 'maximum', 'multipleOf']);
126
+ const integer = optionalBoolean(input.integer, `${path}.integer`);
127
+ const minimum = input.minimum === undefined
128
+ ? undefined
129
+ : finiteNumber(input.minimum, `${path}.minimum`);
130
+ const maximum = input.maximum === undefined
131
+ ? undefined
132
+ : finiteNumber(input.maximum, `${path}.maximum`);
133
+ const multipleOf = input.multipleOf === undefined
134
+ ? undefined
135
+ : finiteNumber(input.multipleOf, `${path}.multipleOf`);
136
+ if (minimum !== undefined && maximum !== undefined && minimum > maximum) {
137
+ throw new Error(`Invalid ${path}: minimum must be less than or equal to maximum.`);
138
+ }
139
+ if (multipleOf !== undefined && multipleOf <= 0) {
140
+ throw new Error(`Invalid ${path}.multipleOf: expected a number greater than zero.`);
141
+ }
142
+ return { integer, minimum, maximum, multipleOf };
143
+ }
144
+ function createIssue(code, field, message, property) {
145
+ return {
146
+ code,
147
+ path: property === undefined ? { field } : { field, property },
148
+ message,
149
+ recoverable: true,
150
+ };
151
+ }
152
+ function withArrayPath(issue, index) {
153
+ return {
154
+ ...issue,
155
+ path: {
156
+ ...issue.path,
157
+ segments: [issue.path.field, index, ...(issue.path.segments?.slice(1)
158
+ ?? (issue.path.property === undefined ? [] : [issue.path.property]))],
159
+ },
160
+ };
161
+ }
162
+ function isSafeJsonRecord(value) {
163
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
164
+ return false;
165
+ const prototype = Object.getPrototypeOf(value);
166
+ if (prototype !== Object.prototype && prototype !== null)
167
+ return false;
168
+ return Reflect.ownKeys(value).every((key) => typeof key === 'string' && Object.hasOwn(Object.getOwnPropertyDescriptor(value, key) ?? {}, 'value'));
169
+ }
170
+ function isDenseDataArray(value) {
171
+ if (Object.getPrototypeOf(value) !== Array.prototype)
172
+ return false;
173
+ if (Reflect.ownKeys(value).length !== value.length + 1)
174
+ return false;
175
+ for (let index = 0; index < value.length; index += 1) {
176
+ if (!Object.hasOwn(Object.getOwnPropertyDescriptor(value, String(index)) ?? {}, 'value'))
177
+ return false;
178
+ }
179
+ return true;
180
+ }
181
+ function stringFormatMatches(value, format) {
182
+ if (format === 'identifier')
183
+ return IDENTIFIER_PATTERN.test(value);
184
+ if (format === 'slug')
185
+ return SLUG_PATTERN.test(value);
186
+ if (format === 'semver')
187
+ return SEMVER_PATTERN.test(value);
188
+ if (format === 'hostname')
189
+ return HOSTNAME_PATTERN.test(value);
190
+ if (format === 'email')
191
+ return EMAIL_PATTERN.test(value);
192
+ try {
193
+ const url = new URL(value);
194
+ return url.protocol.length > 1;
195
+ }
196
+ catch {
197
+ return false;
198
+ }
199
+ }
200
+ function validateString(value, validation, field, property) {
201
+ if (typeof value !== 'string') {
202
+ return [createIssue('type', field, 'Expected a string.', property)];
203
+ }
204
+ const issues = [];
205
+ if (validation?.minLength !== undefined && [...value].length < validation.minLength) {
206
+ issues.push(createIssue('minLength', field, `Expected at least ${validation.minLength} characters.`, property));
207
+ }
208
+ if (validation?.maxLength !== undefined && [...value].length > validation.maxLength) {
209
+ issues.push(createIssue('maxLength', field, `Expected at most ${validation.maxLength} characters.`, property));
210
+ }
211
+ if (validation?.pattern !== undefined && !new RE2(validation.pattern, 'u').test(value)) {
212
+ issues.push(createIssue('pattern', field, `Expected a value that matches ${validation.pattern}.`, property));
213
+ }
214
+ if (validation?.format !== undefined && !stringFormatMatches(value, validation.format)) {
215
+ issues.push(createIssue('format', field, `Expected ${validation.format} format.`, property));
216
+ }
217
+ return issues;
218
+ }
219
+ function validateNumber(value, validation, field, property) {
220
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
221
+ return [createIssue('type', field, 'Expected a finite number.', property)];
222
+ }
223
+ const issues = [];
224
+ if (validation?.integer === true && !Number.isInteger(value)) {
225
+ issues.push(createIssue('integer', field, 'Expected an integer.', property));
226
+ }
227
+ if (validation?.minimum !== undefined && value < validation.minimum) {
228
+ issues.push(createIssue('minimum', field, `Expected a number greater than or equal to ${validation.minimum}.`, property));
229
+ }
230
+ if (validation?.maximum !== undefined && value > validation.maximum) {
231
+ issues.push(createIssue('maximum', field, `Expected a number less than or equal to ${validation.maximum}.`, property));
232
+ }
233
+ if (validation?.multipleOf !== undefined
234
+ && Math.abs(value / validation.multipleOf - Math.round(value / validation.multipleOf)) > 1e-9) {
235
+ issues.push(createIssue('multipleOf', field, `Expected a multiple of ${validation.multipleOf}.`, property));
236
+ }
237
+ return issues;
238
+ }
239
+ function validateScalarField(fieldDefinition, value, field, property, checkChoices = true) {
240
+ const issues = fieldDefinition.type === 'string'
241
+ ? validateString(value, fieldDefinition.validation, field, property)
242
+ : fieldDefinition.type === 'number'
243
+ ? validateNumber(value, fieldDefinition.validation, field, property)
244
+ : typeof value === 'boolean'
245
+ ? []
246
+ : [createIssue('type', field, 'Expected a boolean.', property)];
247
+ if (checkChoices
248
+ && issues.length === 0
249
+ && fieldDefinition.choices?.allowCustom === false
250
+ && matchPluginConfigPreset(fieldDefinition, value) === undefined) {
251
+ issues.push(createIssue('choice', field, 'Expected a declared preset value.', property));
252
+ }
253
+ if (issues.length === 0 && fieldDefinition.const !== undefined && value !== fieldDefinition.const) {
254
+ issues.push(createIssue('const', field, 'Expected the declared constant value.', property));
255
+ }
256
+ return issues;
257
+ }
258
+ /** Resolve visible and allowed own properties without changing the supplied value. */
259
+ export function resolvePluginConfigObjectProperties(definition, value, field = 'value') {
260
+ const properties = { ...definition.properties };
261
+ if (definition.properties === undefined || definition.variants === undefined)
262
+ return { properties, issues: [] };
263
+ const variants = definition.variants;
264
+ const input = isSafeJsonRecord(value) ? value : undefined;
265
+ const missingSelector = input === undefined || !Object.hasOwn(input, variants.discriminator);
266
+ const selector = ownProperty(input, variants.discriminator);
267
+ const selectorField = definition.properties[variants.discriminator];
268
+ const issues = missingSelector ? [] : validateScalarField(selectorField, selector, field, variants.discriminator);
269
+ if (issues.length > 0)
270
+ return { properties, issues };
271
+ const caseName = missingSelector ? variants.missing : selector;
272
+ const selected = caseName === undefined ? undefined : ownProperty(variants.cases, caseName);
273
+ if (selected !== undefined) {
274
+ return { properties: { ...properties, ...selected.properties }, variant: { kind: 'case', case: caseName }, issues };
275
+ }
276
+ if (!missingSelector && variants.otherwise !== undefined) {
277
+ return { properties: { ...properties, ...variants.otherwise.properties }, variant: { kind: 'otherwise' }, issues };
278
+ }
279
+ return { properties, issues: [createIssue('variant', field, 'No object variant matches the discriminator.', variants.discriminator)] };
280
+ }
281
+ function withPropertyPath(issue, property) {
282
+ return {
283
+ ...issue,
284
+ path: {
285
+ field: issue.path.field,
286
+ property,
287
+ segments: [issue.path.field, property, ...(issue.path.segments?.slice(1)
288
+ ?? (issue.path.property === undefined ? [] : [issue.path.property]))],
289
+ },
290
+ };
291
+ }
292
+ function validateObjectField(fieldDefinition, value, field, checkChoices = true) {
293
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
294
+ return [createIssue('type', field, 'Expected an object.')];
295
+ }
296
+ const input = value;
297
+ const keys = Object.keys(input);
298
+ const resolved = resolvePluginConfigObjectProperties(fieldDefinition, value, field);
299
+ const issues = [...resolved.issues];
300
+ if (keys.length > MAX_OBJECT_PROPERTIES) {
301
+ issues.push(createIssue('maxProperties', field, `Expected at most ${MAX_OBJECT_PROPERTIES} properties.`));
302
+ }
303
+ if (fieldDefinition.validation?.minProperties !== undefined
304
+ && keys.length < fieldDefinition.validation.minProperties) {
305
+ issues.push(createIssue('minProperties', field, `Expected at least ${fieldDefinition.validation.minProperties} properties.`));
306
+ }
307
+ if (fieldDefinition.validation?.maxProperties !== undefined
308
+ && keys.length > fieldDefinition.validation.maxProperties) {
309
+ issues.push(createIssue('maxProperties', field, `Expected at most ${fieldDefinition.validation.maxProperties} properties.`));
310
+ }
311
+ for (const [property, child] of Object.entries(resolved.properties)) {
312
+ if (child.required === true && !Object.hasOwn(input, property)) {
313
+ issues.push(createIssue('required', field, 'Required property is missing.', property));
314
+ }
315
+ }
316
+ for (const [property, childValue] of Object.entries(input)) {
317
+ if (isDangerousConfigKey(property)) {
318
+ issues.push(createIssue('dangerousKey', field, 'Dangerous config key is not allowed.', property));
319
+ continue;
320
+ }
321
+ const child = ownProperty(resolved.properties, property);
322
+ if (child !== undefined) {
323
+ if (fieldDefinition.variants?.discriminator === property && resolved.issues.length > 0)
324
+ continue;
325
+ if (child.type === 'array' || child.type === 'object') {
326
+ if (child.type === 'object' && !isSafeJsonRecord(childValue)) {
327
+ issues.push(withPropertyPath(createIssue('type', field, 'Expected a JSON object.'), property));
328
+ }
329
+ else {
330
+ issues.push(...validatePluginConfigValue(child, childValue, field)
331
+ .map((issue) => withPropertyPath(issue, property)));
332
+ }
333
+ }
334
+ else
335
+ issues.push(...validateScalarField(child, childValue, field, property));
336
+ continue;
337
+ }
338
+ const dynamicChild = fieldDefinition.additionalProperties;
339
+ if (dynamicChild === undefined || dynamicChild === false) {
340
+ issues.push(createIssue('additionalProperty', field, 'Additional property is not allowed.', property));
341
+ continue;
342
+ }
343
+ if (fieldDefinition.validation?.key !== undefined) {
344
+ issues.push(...validateString(property, fieldDefinition.validation.key, field, property));
345
+ }
346
+ issues.push(...validateScalarField(dynamicChild, childValue, field, property));
347
+ }
348
+ if (checkChoices
349
+ && issues.length === 0
350
+ && fieldDefinition.choices?.allowCustom === false
351
+ && matchPluginConfigPreset(fieldDefinition, input) === undefined) {
352
+ issues.push(createIssue('choice', field, 'Expected a declared preset value.'));
353
+ }
354
+ return issues;
355
+ }
356
+ function validateArrayField(fieldDefinition, value, field, checkChoices = true) {
357
+ if (!Array.isArray(value)) {
358
+ return [{ ...createIssue('type', field, 'Expected a dense array of JSON values.'), path: { field, segments: [field] } }];
359
+ }
360
+ if (value.length > PLUGIN_CONFIG_ARRAY_MAX_ITEMS) {
361
+ return [{ ...createIssue('maxItems', field, `Expected at most ${PLUGIN_CONFIG_ARRAY_MAX_ITEMS} items.`),
362
+ path: { field, segments: [field] } }];
363
+ }
364
+ if (!isDenseDataArray(value)) {
365
+ return [{ ...createIssue('type', field, 'Expected a dense array of JSON values.'), path: { field, segments: [field] } }];
366
+ }
367
+ const issues = [];
368
+ const count = value.length;
369
+ const min = fieldDefinition.validation?.minItems;
370
+ const max = fieldDefinition.validation?.maxItems ?? PLUGIN_CONFIG_ARRAY_MAX_ITEMS;
371
+ if (count < (min ?? 0))
372
+ issues.push(createIssue('minItems', field, `Expected at least ${min} items.`));
373
+ if (count > max)
374
+ issues.push(createIssue('maxItems', field, `Expected at most ${max} items.`));
375
+ const seen = new Set();
376
+ const seenProperties = new Set();
377
+ for (const [index, item] of value.entries()) {
378
+ const itemIssues = fieldDefinition.items.type === 'object'
379
+ ? isSafeJsonRecord(item)
380
+ ? validateObjectField(fieldDefinition.items, item, field)
381
+ : [createIssue('type', field, 'Expected a JSON object.')]
382
+ : validateScalarField(fieldDefinition.items, item, field);
383
+ issues.push(...itemIssues.map((issue) => withArrayPath(issue, index)));
384
+ const uniqueBy = fieldDefinition.validation?.uniqueBy;
385
+ if (uniqueBy !== undefined && itemIssues.length === 0 && isSafeJsonRecord(item)) {
386
+ if (!Object.hasOwn(item, uniqueBy)) {
387
+ issues.push(withArrayPath(createIssue('uniqueBy', field, 'Unique property is missing.', uniqueBy), index));
388
+ }
389
+ else {
390
+ const key = item[uniqueBy];
391
+ if (seenProperties.has(key)) {
392
+ issues.push(withArrayPath(createIssue('uniqueBy', field, 'Expected a unique property value.', uniqueBy), index));
393
+ }
394
+ seenProperties.add(key);
395
+ }
396
+ }
397
+ if (fieldDefinition.validation?.uniqueItems === true && itemIssues.length === 0) {
398
+ const key = stablePluginConfigStringify(item);
399
+ if (seen.has(key)) {
400
+ issues.push(withArrayPath(createIssue('uniqueItems', field, 'Expected unique items.'), index));
401
+ }
402
+ seen.add(key);
403
+ }
404
+ }
405
+ if (issues.length === 0) {
406
+ const bytes = Buffer.byteLength(stablePluginConfigStringify(value), 'utf8');
407
+ if (bytes > PLUGIN_CONFIG_ARRAY_MAX_BYTES) {
408
+ issues.push(createIssue('maxBytes', field, `Expected at most ${PLUGIN_CONFIG_ARRAY_MAX_BYTES} UTF-8 JSON bytes.`));
409
+ }
410
+ }
411
+ if (checkChoices && issues.length === 0 && fieldDefinition.choices?.allowCustom === false
412
+ && matchPluginConfigPreset(fieldDefinition, value) === undefined) {
413
+ issues.push(createIssue('choice', field, 'Expected a declared preset value.'));
414
+ }
415
+ return issues.map((issue) => issue.path.segments === undefined
416
+ ? { ...issue, path: { ...issue.path, segments: [field] } }
417
+ : issue);
418
+ }
419
+ function resolveValueArguments(first, second, third) {
420
+ if (typeof first === 'string') {
421
+ return {
422
+ field: first,
423
+ definition: second,
424
+ value: third,
425
+ };
426
+ }
427
+ return {
428
+ definition: first,
429
+ value: second,
430
+ field: typeof third === 'string' ? third : 'value',
431
+ };
432
+ }
433
+ export function validatePluginConfigValue(first, second, third) {
434
+ const resolved = resolveValueArguments(first, second, third);
435
+ return resolved.definition.type === 'object'
436
+ ? validateObjectField(resolved.definition, resolved.value, resolved.field)
437
+ : resolved.definition.type === 'array'
438
+ ? validateArrayField(resolved.definition, resolved.value, resolved.field)
439
+ : validateScalarField(resolved.definition, resolved.value, resolved.field);
440
+ }
441
+ export function validatePluginConfigDocument(definition, value) {
442
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
443
+ return [createIssue('type', '$document', 'Expected a config object.')];
444
+ }
445
+ const input = value;
446
+ const issues = [];
447
+ for (const [field, fieldDefinition] of Object.entries(definition.fields)) {
448
+ if (!Object.hasOwn(input, field)) {
449
+ if (fieldDefinition.required === true) {
450
+ issues.push(createIssue('required', field, 'Required field is missing.'));
451
+ }
452
+ continue;
453
+ }
454
+ issues.push(...validatePluginConfigValue(fieldDefinition, input[field], field));
455
+ }
456
+ for (const field of Object.keys(input)) {
457
+ if (isDangerousConfigKey(field)) {
458
+ issues.push(createIssue('dangerousKey', field, 'Dangerous config key is not allowed.'));
459
+ continue;
460
+ }
461
+ if (!Object.hasOwn(definition.fields, field)) {
462
+ issues.push(createIssue('unknownField', field, 'Unknown config field.'));
463
+ }
464
+ }
465
+ return [...issues, ...validatePluginConfigReferences(definition, input, Object.keys(definition.fields))];
466
+ }
467
+ function scalarSchemas(definition) {
468
+ if (definition.type === 'array')
469
+ return scalarSchemas(definition.items);
470
+ if (definition.type !== 'object')
471
+ return [definition];
472
+ return definition.properties === undefined
473
+ ? [definition.additionalProperties]
474
+ : [definition.properties, ...Object.values(definition.variants?.cases ?? {}).map((variant) => variant.properties),
475
+ ...(definition.variants?.otherwise === undefined ? [] : [definition.variants.otherwise.properties])]
476
+ .flatMap((properties) => Object.values(properties).flatMap((child) => scalarSchemas(child)));
477
+ }
478
+ /** Returns validation dependencies only; callers still project their original allowed keys. */
479
+ export function getPluginConfigDependencyKeys(definition, keys) {
480
+ const result = new Set();
481
+ const visit = (key) => {
482
+ if (result.has(key))
483
+ return;
484
+ const field = ownProperty(definition.fields, key);
485
+ if (field === undefined)
486
+ throw new Error(`Unknown config key "${key}".`);
487
+ result.add(key);
488
+ for (const scalar of scalarSchemas(field)) {
489
+ if ('valueRef' in scalar && scalar.valueRef !== undefined)
490
+ visit(scalar.valueRef.field);
491
+ }
492
+ };
493
+ keys.forEach(visit);
494
+ return [...result];
495
+ }
496
+ function validatePluginConfigReferences(definition, input, keys) {
497
+ const issues = [];
498
+ const reservedPaths = new Set();
499
+ const sources = new Map();
500
+ const allowedValues = new Map();
501
+ const check = (scalar, value, field, segments = []) => {
502
+ if (!('valueRef' in scalar) || scalar.valueRef === undefined)
503
+ return;
504
+ const ref = scalar.valueRef;
505
+ let source = sources.get(ref.field);
506
+ if (source === undefined) {
507
+ const sourceSchema = definition.fields[ref.field];
508
+ const sourceValue = ownProperty(input, ref.field);
509
+ const valid = sourceValue === undefined
510
+ ? sourceSchema.required !== true
511
+ : validatePluginConfigValue(sourceSchema, sourceValue, ref.field).length === 0;
512
+ source = { valid, entries: valid ? (sourceValue ?? []) : [] };
513
+ sources.set(ref.field, source);
514
+ }
515
+ const add = (code, message) => {
516
+ const property = segments.find((segment) => typeof segment === 'string');
517
+ const issue = createIssue(code, field, message, property);
518
+ issues.push(segments.length < 2 && typeof segments[0] !== 'number'
519
+ ? issue : { ...issue, path: { ...issue.path, segments: [field, ...segments] } });
520
+ };
521
+ if (!source.valid) {
522
+ add('valueRefSource', `Referenced config "${ref.field}" is invalid; fix its complete array value.`);
523
+ return;
524
+ }
525
+ let allowed = allowedValues.get(ref);
526
+ if (allowed === undefined) {
527
+ allowed = new Set(ref.include);
528
+ for (const [sourceIndex, entry] of source.entries.entries()) {
529
+ if (ref.include?.includes(entry[ref.key]) === true) {
530
+ const pathKey = `${ref.field}\0${sourceIndex}\0${ref.key}`;
531
+ if (!reservedPaths.has(pathKey)) {
532
+ reservedPaths.add(pathKey);
533
+ issues.push(withArrayPath(createIssue('valueRefReserved', ref.field, 'Referenced property collides with a reserved included value.', ref.key), sourceIndex));
534
+ }
535
+ }
536
+ allowed.add(entry[ref.key]);
537
+ }
538
+ allowedValues.set(ref, allowed);
539
+ }
540
+ if (value === undefined || validateScalarField(scalar, value, field).length > 0)
541
+ return;
542
+ if (!allowed.has(value)) {
543
+ add('valueRef', `Expected an included value or a key from config "${ref.field}". Add that key first, update dependent fields, then remove the old key.`);
544
+ }
545
+ };
546
+ const walk = (schema, value, field, segments = []) => {
547
+ if (schema.type === 'array') {
548
+ // Reserved source values must remain invalid when a dependent array is absent.
549
+ for (const scalar of scalarSchemas(schema))
550
+ check(scalar, undefined, field, segments);
551
+ if (Array.isArray(value) && validatePluginConfigValue(schema, value, field).length === 0) {
552
+ value.forEach((item, index) => walk(schema.items, item, field, [...segments, index]));
553
+ }
554
+ }
555
+ else if (schema.type === 'object') {
556
+ if (schema.properties !== undefined) {
557
+ const resolved = resolvePluginConfigObjectProperties(schema, value, field);
558
+ for (const [property, child] of Object.entries(resolved.properties)) {
559
+ walk(child, isSafeJsonRecord(value) ? ownProperty(value, property) : undefined, field, [...segments, property]);
560
+ }
561
+ }
562
+ else {
563
+ check(schema.additionalProperties, undefined, field, segments);
564
+ if (isSafeJsonRecord(value)) {
565
+ for (const [property, child] of Object.entries(value)) {
566
+ walk(schema.additionalProperties, child, field, [...segments, property]);
567
+ }
568
+ }
569
+ }
570
+ }
571
+ else
572
+ check(schema, value, field, segments);
573
+ };
574
+ for (const field of keys) {
575
+ const schema = definition.fields[field];
576
+ walk(schema, ownProperty(input, field), field);
577
+ }
578
+ return issues;
579
+ }
580
+ /** Validate one effective candidate, including complete sources required by selected fields. */
581
+ export function validatePluginConfigCandidate(definition, values, keys) {
582
+ if (!isSafeJsonRecord(values))
583
+ return [createIssue('type', '$document', 'Expected a config JSON object.')];
584
+ if (keys === undefined)
585
+ return validatePluginConfigDocument(definition, values);
586
+ const closure = getPluginConfigDependencyKeys(definition, keys);
587
+ const projected = Object.fromEntries(closure.flatMap((key) => Object.hasOwn(values, key) ? [[key, values[key]]] : []));
588
+ return validatePluginConfigDocument({ ...definition, fields: Object.fromEntries(closure.map((key) => [key, definition.fields[key]])) }, projected);
589
+ }
590
+ function assertValueReferenceSchemas(fields, definitions, path, allowReferences = true) {
591
+ for (const definition of definitions) {
592
+ for (const scalar of scalarSchemas(definition)) {
593
+ if (!('valueRef' in scalar) || scalar.valueRef === undefined)
594
+ continue;
595
+ if (!allowReferences)
596
+ throw new Error(`Invalid ${path}: valueRef requires Config Definition v3.`);
597
+ const ref = scalar.valueRef;
598
+ const source = ownProperty(fields, ref.field);
599
+ if (source?.type !== 'array' || source.items.type !== 'object' || source.items.properties === undefined) {
600
+ throw new Error(`Invalid ${path}.valueRef.field: expected a declared fixed-object array.`);
601
+ }
602
+ const key = ownProperty(source.items.properties, ref.key);
603
+ if (key === undefined || key.type !== scalar.type || source.validation?.uniqueBy !== ref.key) {
604
+ throw new Error(`Invalid ${path}.valueRef.key: expected a scalar of the same type with matching uniqueBy.`);
605
+ }
606
+ if (ref.label !== undefined && ownProperty(source.items.properties, ref.label) === undefined) {
607
+ throw new Error(`Invalid ${path}.valueRef.label: expected a declared scalar property.`);
608
+ }
609
+ const label = ref.label === undefined ? undefined : ownProperty(source.items.properties, ref.label);
610
+ if (label?.type === 'array' || label?.type === 'object') {
611
+ throw new Error(`Invalid ${path}.valueRef.label: expected a declared scalar property.`);
612
+ }
613
+ }
614
+ }
615
+ }
616
+ export function canonicalizePluginConfigValue(value) {
617
+ if (typeof value !== 'object')
618
+ return value;
619
+ if (Array.isArray(value))
620
+ return value.map((item) => canonicalizePluginConfigValue(item));
621
+ return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
622
+ .map(([key, child]) => [key, canonicalizePluginConfigValue(child)]));
623
+ }
624
+ export function canonicalizePluginConfigValues(values) {
625
+ return Object.fromEntries(Object.entries(values)
626
+ .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
627
+ .map(([key, value]) => [key, canonicalizePluginConfigValue(value)]));
628
+ }
629
+ export function stablePluginConfigStringify(value) {
630
+ return JSON.stringify(canonicalizePluginConfigValue(value));
631
+ }
632
+ export function matchPluginConfigPreset(definition, value) {
633
+ return definition.choices?.presets.find((preset) => stablePluginConfigStringify(preset.value) === stablePluginConfigStringify(value));
634
+ }
635
+ export function validatePluginConfigPreset(definition, preset, field = 'value') {
636
+ return definition.type === 'object'
637
+ ? validateObjectField(definition, preset.value, field, false)
638
+ : definition.type === 'array'
639
+ ? validateArrayField(definition, preset.value, field, false)
640
+ : validateScalarField(definition, preset.value, field, undefined, false);
641
+ }
642
+ export function redactPluginConfigValue(definition, value) {
643
+ if (definition.type === 'string' && definition.sensitive === true)
644
+ return '[redacted]';
645
+ return value;
646
+ }
647
+ export function formatPluginConfigIssue(issue) {
648
+ const path = issue.path.segments !== undefined
649
+ ? issue.path.segments.map((segment, index) => typeof segment === 'number' ? `[${segment}]` : index === 0 ? segment : `.${segment}`).join('')
650
+ : issue.path.property === undefined
651
+ ? issue.path.field
652
+ : `${issue.path.field}.${issue.path.property}`;
653
+ return `${path}: ${issue.message}`;
654
+ }
655
+ function normalizePreset(value, path) {
656
+ const input = asObject(value, path);
657
+ assertAllowedKeys(input, path, ['id', 'title', 'description', 'value', 'recommended']);
658
+ if (!Object.hasOwn(input, 'value')) {
659
+ throw new Error(`Invalid ${path}.value: value is required.`);
660
+ }
661
+ if (input.recommended !== undefined && input.recommended !== true) {
662
+ throw new Error(`Invalid ${path}.recommended: expected true.`);
663
+ }
664
+ return {
665
+ id: nonEmptyString(input.id, `${path}.id`),
666
+ title: nonEmptyString(input.title, `${path}.title`),
667
+ description: optionalText(input.description, `${path}.description`),
668
+ value: input.value,
669
+ recommended: input.recommended,
670
+ };
671
+ }
672
+ function normalizeChoices(value, path) {
673
+ const input = asObject(value, path);
674
+ assertAllowedKeys(input, path, ['allowCustom', 'presets']);
675
+ if (typeof input.allowCustom !== 'boolean') {
676
+ throw new Error(`Invalid ${path}.allowCustom: expected a boolean.`);
677
+ }
678
+ if (!Array.isArray(input.presets)) {
679
+ throw new Error(`Invalid ${path}.presets: expected an array.`);
680
+ }
681
+ return {
682
+ allowCustom: input.allowCustom,
683
+ presets: input.presets.map((preset, index) => normalizePreset(preset, `${path}.presets[${index}]`)),
684
+ };
685
+ }
686
+ function normalizeScalarField(value, path, options) {
687
+ const input = asObject(value, path);
688
+ const type = nonEmptyString(input.type, `${path}.type`);
689
+ if (type !== 'string' && type !== 'number' && type !== 'boolean') {
690
+ throw new Error(`Invalid ${path}.type: expected string, number, or boolean.`);
691
+ }
692
+ const allowed = ['type', 'title', 'description'];
693
+ if (options.allowDefault)
694
+ allowed.push('default');
695
+ if (options.allowRequired)
696
+ allowed.push('required');
697
+ if (options.allowSensitive && type === 'string')
698
+ allowed.push('sensitive');
699
+ allowed.push('choices', 'valueRef');
700
+ if (options.allowConst)
701
+ allowed.push('const');
702
+ if (type !== 'boolean')
703
+ allowed.push('validation');
704
+ assertAllowedKeys(input, path, allowed);
705
+ const sensitive = options.allowSensitive && type === 'string'
706
+ ? optionalBoolean(input.sensitive, `${path}.sensitive`)
707
+ : undefined;
708
+ if (sensitive === true && input.default !== undefined) {
709
+ throw new Error(`Invalid ${path}.default: sensitive fields cannot declare a default.`);
710
+ }
711
+ if (sensitive === true && input.choices !== undefined) {
712
+ throw new Error(`Invalid ${path}.choices: sensitive fields cannot declare choices.`);
713
+ }
714
+ if (sensitive === true && input.valueRef !== undefined) {
715
+ throw new Error(`Invalid ${path}.valueRef: sensitive fields cannot declare value references.`);
716
+ }
717
+ if (sensitive === true && Object.hasOwn(input, 'const')) {
718
+ throw new Error(`Invalid ${path}.const: sensitive fields cannot declare a constant.`);
719
+ }
720
+ if (Object.hasOwn(input, 'const') && (typeof input.const !== type
721
+ || (type === 'number' && !Number.isFinite(input.const)))) {
722
+ throw new Error(`Invalid ${path}.const: expected a finite scalar of type ${type}.`);
723
+ }
724
+ let valueRef;
725
+ if (input.valueRef !== undefined) {
726
+ const rawRef = asObject(input.valueRef, `${path}.valueRef`);
727
+ assertAllowedKeys(rawRef, `${path}.valueRef`, ['field', 'key', 'label', 'include']);
728
+ let include;
729
+ if (rawRef.include !== undefined) {
730
+ if (!Array.isArray(rawRef.include) || !isDenseDataArray(rawRef.include)
731
+ || rawRef.include.length > PLUGIN_CONFIG_ARRAY_MAX_ITEMS) {
732
+ throw new Error(`Invalid ${path}.valueRef.include: expected at most ${PLUGIN_CONFIG_ARRAY_MAX_ITEMS} scalar values.`);
733
+ }
734
+ include = rawRef.include.map((entry) => {
735
+ if (typeof entry !== type || (type === 'number' && !Number.isFinite(entry))) {
736
+ throw new Error(`Invalid ${path}.valueRef.include: expected values of type ${type}.`);
737
+ }
738
+ return entry;
739
+ });
740
+ if (new Set(include).size !== include.length) {
741
+ throw new Error(`Invalid ${path}.valueRef.include: expected unique scalar values.`);
742
+ }
743
+ if (Buffer.byteLength(JSON.stringify(include), 'utf8') > PLUGIN_CONFIG_ARRAY_MAX_BYTES) {
744
+ throw new Error(`Invalid ${path}.valueRef.include: expected at most ${PLUGIN_CONFIG_ARRAY_MAX_BYTES} UTF-8 JSON bytes.`);
745
+ }
746
+ }
747
+ valueRef = {
748
+ field: nonEmptyString(rawRef.field, `${path}.valueRef.field`),
749
+ key: nonEmptyString(rawRef.key, `${path}.valueRef.key`),
750
+ label: optionalText(rawRef.label, `${path}.valueRef.label`),
751
+ include,
752
+ };
753
+ }
754
+ const common = {
755
+ title: optionalText(input.title, `${path}.title`),
756
+ description: optionalText(input.description, `${path}.description`),
757
+ ...(options.allowRequired ? { required: optionalBoolean(input.required, `${path}.required`) } : {}),
758
+ ...(valueRef === undefined ? {} : { valueRef }),
759
+ };
760
+ const defaultValue = options.allowDefault && input.default !== undefined
761
+ ? input.default
762
+ : undefined;
763
+ if (type === 'string') {
764
+ if (sensitive === true) {
765
+ return {
766
+ type,
767
+ ...common,
768
+ sensitive: true,
769
+ validation: input.validation === undefined
770
+ ? undefined
771
+ : normalizeStringValidation(input.validation, `${path}.validation`),
772
+ };
773
+ }
774
+ return {
775
+ type,
776
+ ...common,
777
+ ...(Object.hasOwn(input, 'const') ? { const: input.const } : {}),
778
+ ...(options.allowSensitive ? { sensitive } : {}),
779
+ ...(options.allowDefault ? { default: defaultValue } : {}),
780
+ choices: input.choices === undefined
781
+ ? undefined
782
+ : normalizeChoices(input.choices, `${path}.choices`),
783
+ validation: input.validation === undefined
784
+ ? undefined
785
+ : normalizeStringValidation(input.validation, `${path}.validation`),
786
+ };
787
+ }
788
+ if (type === 'number') {
789
+ return {
790
+ type,
791
+ ...common,
792
+ ...(Object.hasOwn(input, 'const') ? { const: input.const } : {}),
793
+ ...(options.allowDefault ? { default: defaultValue } : {}),
794
+ choices: input.choices === undefined
795
+ ? undefined
796
+ : normalizeChoices(input.choices, `${path}.choices`),
797
+ validation: input.validation === undefined
798
+ ? undefined
799
+ : normalizeNumberValidation(input.validation, `${path}.validation`),
800
+ };
801
+ }
802
+ return {
803
+ type,
804
+ ...common,
805
+ ...(Object.hasOwn(input, 'const') ? { const: input.const } : {}),
806
+ ...(options.allowDefault ? { default: defaultValue } : {}),
807
+ choices: input.choices === undefined
808
+ ? undefined
809
+ : normalizeChoices(input.choices, `${path}.choices`),
810
+ };
811
+ }
812
+ function assertChoices(definition, path) {
813
+ if (definition.choices === undefined)
814
+ return;
815
+ if (definition.choices.allowCustom === false && definition.choices.presets.length === 0) {
816
+ throw new Error(`Invalid ${path}.choices.presets: expected at least one preset when allowCustom is false.`);
817
+ }
818
+ const ids = new Set();
819
+ const values = new Set();
820
+ let recommendedCount = 0;
821
+ for (const [index, preset] of definition.choices.presets.entries()) {
822
+ if (ids.has(preset.id)) {
823
+ throw new Error(`Invalid ${path}.choices.presets[${index}].id: duplicate preset id "${preset.id}".`);
824
+ }
825
+ ids.add(preset.id);
826
+ const canonicalValue = stablePluginConfigStringify(preset.value);
827
+ if (values.has(canonicalValue)) {
828
+ throw new Error(`Invalid ${path}.choices.presets[${index}].value: duplicate preset value.`);
829
+ }
830
+ values.add(canonicalValue);
831
+ if (preset.recommended === true)
832
+ recommendedCount += 1;
833
+ const issues = validatePluginConfigPreset(definition, preset, path);
834
+ if (issues.length > 0) {
835
+ throw new Error(`Invalid ${path}.choices.presets[${index}].value: ${formatPluginConfigIssue(issues[0])}`);
836
+ }
837
+ }
838
+ if (recommendedCount > 1) {
839
+ throw new Error(`Invalid ${path}.choices: expected at most one recommended preset.`);
840
+ }
841
+ }
842
+ function assertFieldConstraints(definition, path) {
843
+ assertChoices(definition, path);
844
+ if (definition.type !== 'object' && definition.type !== 'array' && definition.const !== undefined) {
845
+ const issues = validateScalarField(definition, definition.const, path);
846
+ if (issues.length > 0)
847
+ throw new Error(`Invalid ${path}.const: ${formatPluginConfigIssue(issues[0])}`);
848
+ }
849
+ }
850
+ function normalizeObjectProperty(value, path, v4) {
851
+ const input = asObject(value, path);
852
+ let definition;
853
+ if (v4 && (input.type === 'array' || input.type === 'object')) {
854
+ if (Object.hasOwn(input, 'default') || Object.hasOwn(input, 'sensitive')) {
855
+ throw new Error(`Invalid ${path}: object properties cannot declare default or sensitive.`);
856
+ }
857
+ if (input.type === 'object' && (Object.hasOwn(input, 'properties') || Object.hasOwn(input, 'variants'))) {
858
+ throw new Error(`Invalid ${path}: nested objects must be scalar key-value objects.`);
859
+ }
860
+ definition = (input.type === 'array'
861
+ ? normalizeArrayField(input, path, true, true)
862
+ : normalizeObjectField(input, path, true));
863
+ delete definition.default;
864
+ }
865
+ else {
866
+ definition = normalizeScalarField(input, path, {
867
+ allowDefault: false, allowRequired: true, allowSensitive: false, allowConst: v4,
868
+ });
869
+ }
870
+ assertFieldConstraints(definition, path);
871
+ return definition;
872
+ }
873
+ function normalizeObjectVariants(value, properties, path) {
874
+ const input = asObject(value, path);
875
+ assertAllowedKeys(input, path, ['discriminator', 'cases', 'missing', 'otherwise']);
876
+ const discriminator = nonEmptyString(input.discriminator, `${path}.discriminator`);
877
+ if (ownProperty(properties, discriminator)?.type !== 'string') {
878
+ throw new Error(`Invalid ${path}.discriminator: expected a declared own string property.`);
879
+ }
880
+ const casesInput = asObject(input.cases, `${path}.cases`);
881
+ if (Object.keys(casesInput).length < 1 || Object.keys(casesInput).length > MAX_OBJECT_PROPERTIES) {
882
+ throw new Error(`Invalid ${path}.cases: expected 1..${MAX_OBJECT_PROPERTIES} cases.`);
883
+ }
884
+ const normalizeVariant = (value, variantPath) => {
885
+ const variant = asObject(value, variantPath);
886
+ assertAllowedKeys(variant, variantPath, ['properties']);
887
+ const children = asObject(variant.properties, `${variantPath}.properties`);
888
+ if (Object.keys(children).length + Object.keys(properties).length > MAX_OBJECT_PROPERTIES) {
889
+ throw new Error(`Invalid ${variantPath}.properties: common and case properties must total at most ${MAX_OBJECT_PROPERTIES}.`);
890
+ }
891
+ const result = createPropertyMap();
892
+ for (const [key, child] of Object.entries(children)) {
893
+ if (isDangerousConfigKey(key))
894
+ throw new Error(`Invalid ${variantPath}.properties: dangerous config key.`);
895
+ if (Object.hasOwn(properties, key))
896
+ throw new Error(`Invalid ${variantPath}.properties: case cannot override common property "${key}".`);
897
+ result[key] = normalizeObjectProperty(child, `${variantPath}.properties.${key}`, true);
898
+ }
899
+ return { properties: result };
900
+ };
901
+ const cases = createPropertyMap();
902
+ for (const [name, variant] of Object.entries(casesInput)) {
903
+ if (isDangerousConfigKey(name))
904
+ throw new Error(`Invalid ${path}.cases: dangerous case key.`);
905
+ cases[name] = normalizeVariant(variant, `${path}.cases.${name}`);
906
+ }
907
+ const missing = optionalText(input.missing, `${path}.missing`);
908
+ if (missing !== undefined && !Object.hasOwn(cases, missing)) {
909
+ throw new Error(`Invalid ${path}.missing: expected a declared case.`);
910
+ }
911
+ return { discriminator, cases, ...(missing === undefined ? {} : { missing }),
912
+ ...(input.otherwise === undefined ? {} : { otherwise: normalizeVariant(input.otherwise, `${path}.otherwise`) }) };
913
+ }
914
+ function normalizeObjectField(value, path, v4 = false) {
915
+ const input = asObject(value, path);
916
+ assertAllowedKeys(input, path, [
917
+ 'type',
918
+ 'title',
919
+ 'description',
920
+ 'default',
921
+ 'required',
922
+ 'choices',
923
+ 'properties',
924
+ 'additionalProperties',
925
+ 'validation',
926
+ ...(v4 ? ['variants'] : []),
927
+ ]);
928
+ if (input.properties === undefined && (input.additionalProperties === undefined || input.additionalProperties === false)) {
929
+ throw new Error(`Invalid ${path}: key-value object config requires a scalar additionalProperties field.`);
930
+ }
931
+ if (input.properties !== undefined && input.additionalProperties !== undefined && input.additionalProperties !== false) {
932
+ throw new Error(`Invalid ${path}: fixed and key-value object config modes cannot be mixed.`);
933
+ }
934
+ let properties;
935
+ if (input.properties !== undefined) {
936
+ const propertiesInput = asObject(input.properties, `${path}.properties`);
937
+ if (Object.keys(propertiesInput).length > MAX_OBJECT_PROPERTIES) {
938
+ throw new Error(`Invalid ${path}.properties: expected at most ${MAX_OBJECT_PROPERTIES} properties.`);
939
+ }
940
+ properties = createPropertyMap();
941
+ for (const [property, child] of Object.entries(propertiesInput)) {
942
+ if (isDangerousConfigKey(property)) {
943
+ throw new Error(`Invalid ${path}.properties: dangerous config key "${property}" is not allowed.`);
944
+ }
945
+ properties[property] = normalizeObjectProperty(child, `${path}.properties.${property}`, v4);
946
+ }
947
+ }
948
+ let additionalProperties;
949
+ if (input.additionalProperties === undefined || input.additionalProperties === false) {
950
+ additionalProperties = false;
951
+ }
952
+ else {
953
+ additionalProperties = normalizeScalarField(input.additionalProperties, `${path}.additionalProperties`, { allowDefault: false, allowRequired: false, allowSensitive: false, allowConst: v4 });
954
+ assertFieldConstraints(additionalProperties, `${path}.additionalProperties`);
955
+ }
956
+ let validation;
957
+ if (input.validation !== undefined) {
958
+ const validationInput = asObject(input.validation, `${path}.validation`);
959
+ assertAllowedKeys(validationInput, `${path}.validation`, ['minProperties', 'maxProperties', 'key']);
960
+ const minProperties = validationInput.minProperties === undefined
961
+ ? undefined
962
+ : nonNegativeInteger(validationInput.minProperties, `${path}.validation.minProperties`);
963
+ const maxProperties = validationInput.maxProperties === undefined
964
+ ? undefined
965
+ : nonNegativeInteger(validationInput.maxProperties, `${path}.validation.maxProperties`);
966
+ if (minProperties !== undefined && minProperties > MAX_OBJECT_PROPERTIES) {
967
+ throw new Error(`Invalid ${path}.validation.minProperties: expected at most ${MAX_OBJECT_PROPERTIES}.`);
968
+ }
969
+ if (maxProperties !== undefined && maxProperties > MAX_OBJECT_PROPERTIES) {
970
+ throw new Error(`Invalid ${path}.validation.maxProperties: expected at most ${MAX_OBJECT_PROPERTIES}.`);
971
+ }
972
+ if (minProperties !== undefined && maxProperties !== undefined && minProperties > maxProperties) {
973
+ throw new Error(`Invalid ${path}.validation: minProperties must be less than or equal to maxProperties.`);
974
+ }
975
+ validation = {
976
+ minProperties,
977
+ maxProperties,
978
+ key: validationInput.key === undefined
979
+ ? undefined
980
+ : normalizeStringValidation(validationInput.key, `${path}.validation.key`),
981
+ };
982
+ }
983
+ if (input.variants !== undefined && properties === undefined) {
984
+ throw new Error(`Invalid ${path}.variants: variants require a fixed object.`);
985
+ }
986
+ const variants = input.variants === undefined ? undefined : normalizeObjectVariants(input.variants, properties, `${path}.variants`);
987
+ return {
988
+ type: 'object',
989
+ title: optionalText(input.title, `${path}.title`),
990
+ description: optionalText(input.description, `${path}.description`),
991
+ default: input.default,
992
+ required: optionalBoolean(input.required, `${path}.required`),
993
+ choices: input.choices === undefined
994
+ ? undefined
995
+ : normalizeChoices(input.choices, `${path}.choices`),
996
+ ...(properties === undefined
997
+ ? { additionalProperties: additionalProperties }
998
+ : { properties, additionalProperties: false, ...(variants === undefined ? {} : { variants }) }),
999
+ validation,
1000
+ };
1001
+ }
1002
+ function normalizeArrayField(value, path, v4 = false, scalarItemsOnly = false) {
1003
+ const input = asObject(value, path);
1004
+ assertAllowedKeys(input, path, ['type', 'title', 'description', 'default', 'required', 'items', 'choices', 'validation']);
1005
+ const itemPath = `${path}.items`;
1006
+ const item = asObject(input.items, itemPath);
1007
+ for (const forbidden of ['default', 'required', 'sensitive']) {
1008
+ if (Object.hasOwn(item, forbidden)) {
1009
+ throw new Error(`Invalid ${itemPath}.${forbidden}: array items cannot declare ${forbidden}.`);
1010
+ }
1011
+ }
1012
+ let items;
1013
+ if (item.type === 'object' && !scalarItemsOnly) {
1014
+ const { default: _default, required: _required, ...objectItem } = normalizeObjectField(item, itemPath, v4);
1015
+ items = objectItem;
1016
+ }
1017
+ else {
1018
+ items = normalizeScalarField(item, itemPath, {
1019
+ allowDefault: false, allowRequired: false, allowSensitive: false, allowConst: v4,
1020
+ });
1021
+ }
1022
+ assertFieldConstraints(items, itemPath);
1023
+ let validation;
1024
+ if (input.validation !== undefined) {
1025
+ const rawValidation = asObject(input.validation, `${path}.validation`);
1026
+ assertAllowedKeys(rawValidation, `${path}.validation`, ['minItems', 'maxItems', 'uniqueItems', 'uniqueBy']);
1027
+ const minItems = rawValidation.minItems === undefined
1028
+ ? undefined
1029
+ : nonNegativeInteger(rawValidation.minItems, `${path}.validation.minItems`);
1030
+ const maxItems = rawValidation.maxItems === undefined
1031
+ ? undefined
1032
+ : nonNegativeInteger(rawValidation.maxItems, `${path}.validation.maxItems`);
1033
+ if (maxItems !== undefined && maxItems > PLUGIN_CONFIG_ARRAY_MAX_ITEMS) {
1034
+ throw new Error(`Invalid ${path}.validation.maxItems: expected at most ${PLUGIN_CONFIG_ARRAY_MAX_ITEMS}.`);
1035
+ }
1036
+ if (minItems !== undefined && minItems > (maxItems ?? PLUGIN_CONFIG_ARRAY_MAX_ITEMS)) {
1037
+ throw new Error(`Invalid ${path}.validation.minItems: must not exceed effective maxItems.`);
1038
+ }
1039
+ const uniqueBy = optionalText(rawValidation.uniqueBy, `${path}.validation.uniqueBy`);
1040
+ const uniqueProperty = items.type === 'object' && uniqueBy !== undefined ? ownProperty(items.properties, uniqueBy) : undefined;
1041
+ if (uniqueBy !== undefined && (uniqueProperty === undefined
1042
+ || uniqueProperty.type === 'array' || uniqueProperty.type === 'object')) {
1043
+ throw new Error(`Invalid ${path}.validation.uniqueBy: expected a declared fixed scalar property.`);
1044
+ }
1045
+ validation = {
1046
+ minItems,
1047
+ maxItems,
1048
+ uniqueItems: optionalBoolean(rawValidation.uniqueItems, `${path}.validation.uniqueItems`),
1049
+ ...(uniqueBy === undefined ? {} : { uniqueBy }),
1050
+ };
1051
+ }
1052
+ return {
1053
+ type: 'array',
1054
+ title: optionalText(input.title, `${path}.title`),
1055
+ description: optionalText(input.description, `${path}.description`),
1056
+ default: input.default,
1057
+ required: optionalBoolean(input.required, `${path}.required`),
1058
+ items,
1059
+ choices: input.choices === undefined
1060
+ ? undefined
1061
+ : normalizeChoices(input.choices, `${path}.choices`),
1062
+ validation,
1063
+ };
1064
+ }
1065
+ /** Decode exactly one local property token, never a traversal path. */
1066
+ export function decodePluginConfigSummaryPath(value) {
1067
+ if ([...value].length > PLUGIN_CONFIG_SUMMARY_MAX_PATH_LENGTH || !/^\/(?:[^~/]|~[01])*$/u.test(value)) {
1068
+ throw new Error('Invalid summary path: expected one local JSON Pointer property token (at most 128 characters).');
1069
+ }
1070
+ const property = value.slice(1).replace(/~1/gu, '/').replace(/~0/gu, '~');
1071
+ if (isDangerousConfigKey(property))
1072
+ throw new Error('Invalid summary path: dangerous config key.');
1073
+ return property;
1074
+ }
1075
+ function expandConfigReferences(input, path, v4 = false) {
1076
+ const definitions = input.definitions === undefined ? {} : asObject(input.definitions, `${path}.definitions`);
1077
+ if (Object.keys(definitions).length > PLUGIN_CONFIG_V3_MAX_DEFINITIONS) {
1078
+ throw new Error(`Invalid ${path}.definitions: expected at most ${PLUGIN_CONFIG_V3_MAX_DEFINITIONS} definitions.`);
1079
+ }
1080
+ for (const name of Object.keys(definitions)) {
1081
+ if (!CONFIG_KEY_PATTERN.test(name) || isDangerousConfigKey(name)) {
1082
+ throw new Error(`Invalid ${path}.definitions: invalid definition name "${name}".`);
1083
+ }
1084
+ }
1085
+ const expand = (value, fieldPath, chain, context = 'field') => {
1086
+ const field = asObject(value, fieldPath);
1087
+ if (Object.hasOwn(field, '$ref')) {
1088
+ assertAllowedKeys(field, fieldPath, ['$ref', 'title', 'description', 'default', 'required']);
1089
+ const ref = nonEmptyString(field.$ref, `${fieldPath}.$ref`);
1090
+ const match = /^#\/config\/definitions\/([A-Za-z][A-Za-z0-9_]*)$/u.exec(ref);
1091
+ const name = match?.[1];
1092
+ if (name === undefined || isDangerousConfigKey(name) || !Object.hasOwn(definitions, name)) {
1093
+ throw new Error(`Invalid ${fieldPath}.$ref: unknown or unsupported local definition reference.`);
1094
+ }
1095
+ if (chain.includes(name))
1096
+ throw new Error(`Invalid ${fieldPath}.$ref: circular definition reference.`);
1097
+ if (chain.length >= PLUGIN_CONFIG_V3_MAX_REF_DEPTH) {
1098
+ throw new Error(`Invalid ${fieldPath}.$ref: expected at most ${PLUGIN_CONFIG_V3_MAX_REF_DEPTH} reference edges.`);
1099
+ }
1100
+ const result = expand(definitions[name], `${path}.definitions.${name}`, [...chain, name], context);
1101
+ for (const key of ['title', 'description', 'default', 'required']) {
1102
+ if (Object.hasOwn(field, key))
1103
+ result[key] = field[key];
1104
+ }
1105
+ return result;
1106
+ }
1107
+ const result = { ...field };
1108
+ if (context === 'scalar' || (!v4 && context === 'property'))
1109
+ return result;
1110
+ if (context === 'property' && field.type === 'object' && (field.properties !== undefined || field.variants !== undefined)) {
1111
+ throw new Error(`Invalid ${fieldPath}: nested objects must be scalar key-value objects.`);
1112
+ }
1113
+ if (field.type === 'array' && field.items !== undefined) {
1114
+ result.items = expand(field.items, `${fieldPath}.items`, chain, context === 'property' ? 'scalar' : 'item');
1115
+ }
1116
+ if (field.properties !== undefined) {
1117
+ const properties = asObject(field.properties, `${fieldPath}.properties`);
1118
+ if (Object.keys(properties).length > MAX_OBJECT_PROPERTIES) {
1119
+ throw new Error(`Invalid ${fieldPath}.properties: expected at most ${MAX_OBJECT_PROPERTIES} properties.`);
1120
+ }
1121
+ result.properties = Object.fromEntries(Object.entries(properties).map(([key, child]) => [key, expand(child, `${fieldPath}.properties.${key}`, chain, 'property')]));
1122
+ }
1123
+ if (field.additionalProperties !== undefined && field.additionalProperties !== false) {
1124
+ result.additionalProperties = expand(field.additionalProperties, `${fieldPath}.additionalProperties`, chain, 'scalar');
1125
+ }
1126
+ if (v4 && field.variants !== undefined) {
1127
+ const variants = asObject(field.variants, `${fieldPath}.variants`);
1128
+ const cases = asObject(variants.cases, `${fieldPath}.variants.cases`);
1129
+ if (Object.keys(cases).length > MAX_OBJECT_PROPERTIES)
1130
+ throw new Error(`Invalid ${fieldPath}.variants.cases: expected at most ${MAX_OBJECT_PROPERTIES} cases.`);
1131
+ const expandVariant = (value, variantPath) => {
1132
+ const variant = asObject(value, variantPath);
1133
+ const properties = asObject(variant.properties, `${variantPath}.properties`);
1134
+ if (Object.keys(properties).length > MAX_OBJECT_PROPERTIES)
1135
+ throw new Error(`Invalid ${variantPath}.properties: expected at most ${MAX_OBJECT_PROPERTIES} properties.`);
1136
+ return { ...variant, properties: Object.fromEntries(Object.entries(properties).map(([key, child]) => [key, expand(child, `${variantPath}.properties.${key}`, chain, 'property')])) };
1137
+ };
1138
+ result.variants = { ...variants, cases: Object.fromEntries(Object.entries(cases).map(([name, variant]) => [name, expandVariant(variant, `${fieldPath}.variants.cases.${name}`)])),
1139
+ ...(variants.otherwise === undefined ? {} : { otherwise: expandVariant(variants.otherwise, `${fieldPath}.variants.otherwise`) }) };
1140
+ }
1141
+ return result;
1142
+ };
1143
+ // Validate unused definitions too, through the same field/default/choice validator.
1144
+ const normalizedDefinitions = [];
1145
+ for (const [name, value] of Object.entries(definitions)) {
1146
+ normalizedDefinitions.push(normalizeConfigField(expand(value, `${path}.definitions.${name}`, []), `${path}.definitions.${name}`, true, v4));
1147
+ }
1148
+ const fields = asObject(input.fields, `${path}.fields`);
1149
+ if (Object.keys(fields).length > PLUGIN_CONFIG_V3_MAX_FIELDS) {
1150
+ throw new Error(`Invalid ${path}.fields: expected at most ${PLUGIN_CONFIG_V3_MAX_FIELDS} fields.`);
1151
+ }
1152
+ const expanded = Object.fromEntries(Object.entries(fields).map(([key, value]) => [key, expand(value, `${path}.fields.${key}`, [])]));
1153
+ if (normalizedDefinitions.some((definition) => scalarSchemas(definition).some((scalar) => 'valueRef' in scalar && scalar.valueRef !== undefined))) {
1154
+ const normalizedFields = Object.fromEntries(Object.entries(expanded).map(([key, value]) => [key, normalizeConfigField(value, `${path}.fields.${key}`, true, v4)]));
1155
+ assertValueReferenceSchemas(normalizedFields, normalizedDefinitions, `${path}.definitions`);
1156
+ }
1157
+ return expanded;
1158
+ }
1159
+ function normalizeConfigField(rawDefinition, path, allowArray, v4 = false) {
1160
+ const rawField = asObject(rawDefinition, path);
1161
+ const definition = rawField.type === 'object'
1162
+ ? normalizeObjectField(rawField, path, v4)
1163
+ : rawField.type === 'array' && allowArray
1164
+ ? normalizeArrayField(rawField, path, v4)
1165
+ : normalizeScalarField(rawField, path, {
1166
+ allowDefault: true, allowRequired: true, allowSensitive: true, allowConst: v4,
1167
+ });
1168
+ assertFieldConstraints(definition, path);
1169
+ if (definition.default !== undefined) {
1170
+ const issues = validatePluginConfigValue(definition, definition.default, path);
1171
+ if (issues.length > 0)
1172
+ throw new Error(`Invalid ${path}.default: ${formatPluginConfigIssue(issues[0])}`);
1173
+ }
1174
+ return definition;
1175
+ }
1176
+ function normalizeConfigSummaries(input, fields, path) {
1177
+ const summaries = createPropertyMap();
1178
+ for (const [name, raw] of Object.entries(input.summaries === undefined ? {} : asObject(input.summaries, `${path}.summaries`))) {
1179
+ const summaryPath = `${path}.summaries.${name}`;
1180
+ if (!CONFIG_KEY_PATTERN.test(name) || isDangerousConfigKey(name))
1181
+ throw new Error(`Invalid ${summaryPath}: invalid summary name.`);
1182
+ const summary = asObject(raw, summaryPath);
1183
+ assertAllowedKeys(summary, summaryPath, ['kind', 'parts', 'separator', 'missing']);
1184
+ if (summary.kind !== 'join')
1185
+ throw new Error(`Invalid ${summaryPath}.kind: expected join.`);
1186
+ if (!Array.isArray(summary.parts) || summary.parts.length < 1 || summary.parts.length > PLUGIN_CONFIG_SUMMARY_MAX_PARTS) {
1187
+ throw new Error(`Invalid ${summaryPath}.parts: expected 1..${PLUGIN_CONFIG_SUMMARY_MAX_PARTS} parts.`);
1188
+ }
1189
+ if (typeof summary.separator !== 'string' || [...summary.separator].length > PLUGIN_CONFIG_SUMMARY_MAX_SEPARATOR_CODE_POINTS) {
1190
+ throw new Error(`Invalid ${summaryPath}.separator: expected at most 8 Unicode code points.`);
1191
+ }
1192
+ if (typeof summary.missing !== 'string')
1193
+ throw new Error(`Invalid ${summaryPath}.missing: expected a string.`);
1194
+ summaries[name] = {
1195
+ kind: 'join', separator: summary.separator, missing: summary.missing,
1196
+ parts: summary.parts.map((rawPart, index) => {
1197
+ const partPath = `${summaryPath}.parts[${index}]`;
1198
+ const part = asObject(rawPart, partPath);
1199
+ assertAllowedKeys(part, partPath, ['path', 'format']);
1200
+ const propertyPath = nonEmptyString(part.path, `${partPath}.path`);
1201
+ try {
1202
+ decodePluginConfigSummaryPath(propertyPath);
1203
+ }
1204
+ catch (error) {
1205
+ throw new Error(`Invalid ${partPath}.path: ${error.message}`);
1206
+ }
1207
+ if (part.format !== undefined && part.format !== 'raw' && part.format !== 'choiceTitle') {
1208
+ throw new Error(`Invalid ${partPath}.format: expected raw or choiceTitle.`);
1209
+ }
1210
+ return { path: propertyPath, ...(part.format === undefined ? {} : { format: part.format }) };
1211
+ }),
1212
+ };
1213
+ }
1214
+ const fieldViews = createPropertyMap();
1215
+ for (const [key, raw] of Object.entries(input.fieldViews === undefined ? {} : asObject(input.fieldViews, `${path}.fieldViews`))) {
1216
+ const viewPath = `${path}.fieldViews.${key}`;
1217
+ const field = ownProperty(fields, key);
1218
+ const view = asObject(raw, viewPath);
1219
+ assertAllowedKeys(view, viewPath, ['summary', 'itemSummary']);
1220
+ const itemSummary = Object.hasOwn(view, 'itemSummary');
1221
+ if (itemSummary === Object.hasOwn(view, 'summary')) {
1222
+ throw new Error(`Invalid ${viewPath}: expected exactly one summary reference.`);
1223
+ }
1224
+ const objectField = itemSummary
1225
+ ? field?.type === 'array' && field.items.type === 'object' ? field.items : undefined
1226
+ : field?.type === 'object' ? field : undefined;
1227
+ if (!objectField?.properties) {
1228
+ throw new Error(`Invalid ${viewPath}: summary requires a declared fixed object${itemSummary ? ' array item' : ' field'}.`);
1229
+ }
1230
+ const name = nonEmptyString(itemSummary ? view.itemSummary : view.summary, `${viewPath}.${itemSummary ? 'itemSummary' : 'summary'}`);
1231
+ const summary = ownProperty(summaries, name);
1232
+ if (!summary)
1233
+ throw new Error(`Invalid ${viewPath}.${itemSummary ? 'itemSummary' : 'summary'}: unknown summary.`);
1234
+ for (const part of summary.parts) {
1235
+ const propertyName = decodePluginConfigSummaryPath(part.path);
1236
+ const declarations = [objectField.properties,
1237
+ ...Object.values(objectField.variants?.cases ?? {}).map((variant) => variant.properties),
1238
+ ...(objectField.variants?.otherwise === undefined ? [] : [objectField.variants.otherwise.properties])]
1239
+ .flatMap((properties) => {
1240
+ const property = ownProperty(properties, propertyName);
1241
+ return property === undefined ? [] : [property];
1242
+ });
1243
+ if (declarations.length === 0 || declarations.some((property) => property.type === 'array' || property.type === 'object')) {
1244
+ throw new Error(`Invalid ${viewPath}: summary path "${part.path}" must name a declared scalar property.`);
1245
+ }
1246
+ }
1247
+ fieldViews[key] = itemSummary ? { itemSummary: name } : { summary: name };
1248
+ }
1249
+ return {
1250
+ ...(input.summaries === undefined ? {} : { summaries }),
1251
+ ...(input.fieldViews === undefined ? {} : { fieldViews }),
1252
+ };
1253
+ }
1254
+ export function normalizePluginConfigDefinition(value, path = 'config') {
1255
+ const input = asObject(value, path);
1256
+ const v4 = input.schemaVersion === 4;
1257
+ const v3 = input.schemaVersion === 3 || v4;
1258
+ assertAllowedKeys(input, path, v3 ? ['schemaVersion', 'definitions', 'fields', 'presentation'] : ['fields', 'presentation']);
1259
+ const fieldsInput = v3 ? expandConfigReferences(input, path, v4) : asObject(input.fields, `${path}.fields`);
1260
+ const fields = createPropertyMap();
1261
+ for (const [field, rawDefinition] of Object.entries(fieldsInput)) {
1262
+ if (!CONFIG_KEY_PATTERN.test(field) || isDangerousConfigKey(field)) {
1263
+ throw new Error(`Invalid ${path}.fields: config key "${field}" is not valid.`);
1264
+ }
1265
+ fields[field] = normalizeConfigField(rawDefinition, `${path}.fields.${field}`, v3, v4);
1266
+ }
1267
+ assertValueReferenceSchemas(fields, Object.values(fields), path, v3);
1268
+ const defaultValues = Object.fromEntries(Object.entries(fields).flatMap(([field, definition]) => definition.default === undefined ? [] : [[field, definition.default]]));
1269
+ const defaultIssues = validatePluginConfigReferences({ schemaVersion: 4, fields }, defaultValues, Object.keys(fields))
1270
+ .filter((issue) => Object.hasOwn(defaultValues, issue.path.field));
1271
+ if (defaultIssues.length > 0) {
1272
+ throw new Error(`Invalid ${path}.default: ${formatPluginConfigIssue(defaultIssues[0])}`);
1273
+ }
1274
+ let presentation;
1275
+ if (input.presentation !== undefined) {
1276
+ const presentationInput = asObject(input.presentation, `${path}.presentation`);
1277
+ assertAllowedKeys(presentationInput, `${path}.presentation`, v3 ? ['groups', 'summaries', 'fieldViews'] : ['groups']);
1278
+ if (presentationInput.groups !== undefined && !Array.isArray(presentationInput.groups)) {
1279
+ throw new Error(`Invalid ${path}.presentation.groups: expected an array.`);
1280
+ }
1281
+ const groupIds = new Set();
1282
+ const groupedFields = new Set();
1283
+ presentation = {
1284
+ ...(v3 ? normalizeConfigSummaries(presentationInput, fields, `${path}.presentation`) : {}),
1285
+ groups: presentationInput.groups?.map((rawGroup, index) => {
1286
+ const groupPath = `${path}.presentation.groups[${index}]`;
1287
+ const group = asObject(rawGroup, groupPath);
1288
+ assertAllowedKeys(group, groupPath, ['id', 'title', 'description', 'fields', 'advanced']);
1289
+ const id = nonEmptyString(group.id, `${groupPath}.id`);
1290
+ if (groupIds.has(id)) {
1291
+ throw new Error(`Invalid ${groupPath}.id: duplicate group id "${id}".`);
1292
+ }
1293
+ groupIds.add(id);
1294
+ if (!Array.isArray(group.fields)) {
1295
+ throw new Error(`Invalid ${groupPath}.fields: expected an array.`);
1296
+ }
1297
+ const groupFields = group.fields.map((entry, fieldIndex) => {
1298
+ const field = nonEmptyString(entry, `${groupPath}.fields[${fieldIndex}]`);
1299
+ if (!Object.hasOwn(fields, field)) {
1300
+ throw new Error(`Invalid ${groupPath}.fields[${fieldIndex}]: unknown config key "${field}".`);
1301
+ }
1302
+ if (groupedFields.has(field)) {
1303
+ throw new Error(`Invalid ${groupPath}.fields[${fieldIndex}]: config key "${field}" is assigned to more than one group.`);
1304
+ }
1305
+ groupedFields.add(field);
1306
+ return field;
1307
+ });
1308
+ return {
1309
+ id,
1310
+ title: nonEmptyString(group.title, `${groupPath}.title`),
1311
+ description: optionalText(group.description, `${groupPath}.description`),
1312
+ fields: groupFields,
1313
+ advanced: optionalBoolean(group.advanced, `${groupPath}.advanced`),
1314
+ };
1315
+ }),
1316
+ };
1317
+ }
1318
+ return { ...(v3 ? { schemaVersion: v4 ? 4 : 3 } : {}), fields, presentation };
1319
+ }
1320
+ //# sourceMappingURL=plugin-config.js.map