@lesliechan721/aw-plugin-core 0.1.0-beta.38 → 0.1.0-beta.39

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.
@@ -154,7 +154,8 @@ function withArrayPath(issue, index) {
154
154
  ...issue,
155
155
  path: {
156
156
  ...issue.path,
157
- segments: [issue.path.field, index, ...(issue.path.property === undefined ? [] : [issue.path.property])],
157
+ segments: [issue.path.field, index, ...(issue.path.segments?.slice(1)
158
+ ?? (issue.path.property === undefined ? [] : [issue.path.property]))],
158
159
  },
159
160
  };
160
161
  }
@@ -249,15 +250,53 @@ function validateScalarField(fieldDefinition, value, field, property, checkChoic
249
250
  && matchPluginConfigPreset(fieldDefinition, value) === undefined) {
250
251
  issues.push(createIssue('choice', field, 'Expected a declared preset value.', property));
251
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
+ }
252
256
  return issues;
253
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
+ }
254
292
  function validateObjectField(fieldDefinition, value, field, checkChoices = true) {
255
293
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
256
294
  return [createIssue('type', field, 'Expected an object.')];
257
295
  }
258
296
  const input = value;
259
297
  const keys = Object.keys(input);
260
- const issues = [];
298
+ const resolved = resolvePluginConfigObjectProperties(fieldDefinition, value, field);
299
+ const issues = [...resolved.issues];
261
300
  if (keys.length > MAX_OBJECT_PROPERTIES) {
262
301
  issues.push(createIssue('maxProperties', field, `Expected at most ${MAX_OBJECT_PROPERTIES} properties.`));
263
302
  }
@@ -269,7 +308,7 @@ function validateObjectField(fieldDefinition, value, field, checkChoices = true)
269
308
  && keys.length > fieldDefinition.validation.maxProperties) {
270
309
  issues.push(createIssue('maxProperties', field, `Expected at most ${fieldDefinition.validation.maxProperties} properties.`));
271
310
  }
272
- for (const [property, child] of Object.entries(fieldDefinition.properties ?? {})) {
311
+ for (const [property, child] of Object.entries(resolved.properties)) {
273
312
  if (child.required === true && !Object.hasOwn(input, property)) {
274
313
  issues.push(createIssue('required', field, 'Required property is missing.', property));
275
314
  }
@@ -279,9 +318,21 @@ function validateObjectField(fieldDefinition, value, field, checkChoices = true)
279
318
  issues.push(createIssue('dangerousKey', field, 'Dangerous config key is not allowed.', property));
280
319
  continue;
281
320
  }
282
- const child = ownProperty(fieldDefinition.properties, property);
321
+ const child = ownProperty(resolved.properties, property);
283
322
  if (child !== undefined) {
284
- issues.push(...validateScalarField(child, childValue, field, property));
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));
285
336
  continue;
286
337
  }
287
338
  const dynamicChild = fieldDefinition.additionalProperties;
@@ -322,6 +373,7 @@ function validateArrayField(fieldDefinition, value, field, checkChoices = true)
322
373
  if (count > max)
323
374
  issues.push(createIssue('maxItems', field, `Expected at most ${max} items.`));
324
375
  const seen = new Set();
376
+ const seenProperties = new Set();
325
377
  for (const [index, item] of value.entries()) {
326
378
  const itemIssues = fieldDefinition.items.type === 'object'
327
379
  ? isSafeJsonRecord(item)
@@ -329,6 +381,19 @@ function validateArrayField(fieldDefinition, value, field, checkChoices = true)
329
381
  : [createIssue('type', field, 'Expected a JSON object.')]
330
382
  : validateScalarField(fieldDefinition.items, item, field);
331
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
+ }
332
397
  if (fieldDefinition.validation?.uniqueItems === true && itemIssues.length === 0) {
333
398
  const key = stablePluginConfigStringify(item);
334
399
  if (seen.has(key)) {
@@ -397,14 +462,164 @@ export function validatePluginConfigDocument(definition, value) {
397
462
  issues.push(createIssue('unknownField', field, 'Unknown config field.'));
398
463
  }
399
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
+ }
400
578
  return issues;
401
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
+ }
402
616
  export function canonicalizePluginConfigValue(value) {
403
617
  if (typeof value !== 'object')
404
618
  return value;
405
619
  if (Array.isArray(value))
406
620
  return value.map((item) => canonicalizePluginConfigValue(item));
407
- return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0));
621
+ return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
622
+ .map(([key, child]) => [key, canonicalizePluginConfigValue(child)]));
408
623
  }
409
624
  export function canonicalizePluginConfigValues(values) {
410
625
  return Object.fromEntries(Object.entries(values)
@@ -481,7 +696,9 @@ function normalizeScalarField(value, path, options) {
481
696
  allowed.push('required');
482
697
  if (options.allowSensitive && type === 'string')
483
698
  allowed.push('sensitive');
484
- allowed.push('choices');
699
+ allowed.push('choices', 'valueRef');
700
+ if (options.allowConst)
701
+ allowed.push('const');
485
702
  if (type !== 'boolean')
486
703
  allowed.push('validation');
487
704
  assertAllowedKeys(input, path, allowed);
@@ -494,10 +711,51 @@ function normalizeScalarField(value, path, options) {
494
711
  if (sensitive === true && input.choices !== undefined) {
495
712
  throw new Error(`Invalid ${path}.choices: sensitive fields cannot declare choices.`);
496
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
+ }
497
754
  const common = {
498
755
  title: optionalText(input.title, `${path}.title`),
499
756
  description: optionalText(input.description, `${path}.description`),
500
757
  ...(options.allowRequired ? { required: optionalBoolean(input.required, `${path}.required`) } : {}),
758
+ ...(valueRef === undefined ? {} : { valueRef }),
501
759
  };
502
760
  const defaultValue = options.allowDefault && input.default !== undefined
503
761
  ? input.default
@@ -516,6 +774,7 @@ function normalizeScalarField(value, path, options) {
516
774
  return {
517
775
  type,
518
776
  ...common,
777
+ ...(Object.hasOwn(input, 'const') ? { const: input.const } : {}),
519
778
  ...(options.allowSensitive ? { sensitive } : {}),
520
779
  ...(options.allowDefault ? { default: defaultValue } : {}),
521
780
  choices: input.choices === undefined
@@ -530,6 +789,7 @@ function normalizeScalarField(value, path, options) {
530
789
  return {
531
790
  type,
532
791
  ...common,
792
+ ...(Object.hasOwn(input, 'const') ? { const: input.const } : {}),
533
793
  ...(options.allowDefault ? { default: defaultValue } : {}),
534
794
  choices: input.choices === undefined
535
795
  ? undefined
@@ -542,6 +802,7 @@ function normalizeScalarField(value, path, options) {
542
802
  return {
543
803
  type,
544
804
  ...common,
805
+ ...(Object.hasOwn(input, 'const') ? { const: input.const } : {}),
545
806
  ...(options.allowDefault ? { default: defaultValue } : {}),
546
807
  choices: input.choices === undefined
547
808
  ? undefined
@@ -578,7 +839,79 @@ function assertChoices(definition, path) {
578
839
  throw new Error(`Invalid ${path}.choices: expected at most one recommended preset.`);
579
840
  }
580
841
  }
581
- function normalizeObjectField(value, path) {
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) {
582
915
  const input = asObject(value, path);
583
916
  assertAllowedKeys(input, path, [
584
917
  'type',
@@ -590,6 +923,7 @@ function normalizeObjectField(value, path) {
590
923
  'properties',
591
924
  'additionalProperties',
592
925
  'validation',
926
+ ...(v4 ? ['variants'] : []),
593
927
  ]);
594
928
  if (input.properties === undefined && (input.additionalProperties === undefined || input.additionalProperties === false)) {
595
929
  throw new Error(`Invalid ${path}: key-value object config requires a scalar additionalProperties field.`);
@@ -608,12 +942,7 @@ function normalizeObjectField(value, path) {
608
942
  if (isDangerousConfigKey(property)) {
609
943
  throw new Error(`Invalid ${path}.properties: dangerous config key "${property}" is not allowed.`);
610
944
  }
611
- properties[property] = normalizeScalarField(child, `${path}.properties.${property}`, {
612
- allowDefault: false,
613
- allowRequired: true,
614
- allowSensitive: false,
615
- });
616
- assertChoices(properties[property], `${path}.properties.${property}`);
945
+ properties[property] = normalizeObjectProperty(child, `${path}.properties.${property}`, v4);
617
946
  }
618
947
  }
619
948
  let additionalProperties;
@@ -621,8 +950,8 @@ function normalizeObjectField(value, path) {
621
950
  additionalProperties = false;
622
951
  }
623
952
  else {
624
- additionalProperties = normalizeScalarField(input.additionalProperties, `${path}.additionalProperties`, { allowDefault: false, allowRequired: false, allowSensitive: false });
625
- assertChoices(additionalProperties, `${path}.additionalProperties`);
953
+ additionalProperties = normalizeScalarField(input.additionalProperties, `${path}.additionalProperties`, { allowDefault: false, allowRequired: false, allowSensitive: false, allowConst: v4 });
954
+ assertFieldConstraints(additionalProperties, `${path}.additionalProperties`);
626
955
  }
627
956
  let validation;
628
957
  if (input.validation !== undefined) {
@@ -651,6 +980,10 @@ function normalizeObjectField(value, path) {
651
980
  : normalizeStringValidation(validationInput.key, `${path}.validation.key`),
652
981
  };
653
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`);
654
987
  return {
655
988
  type: 'object',
656
989
  title: optionalText(input.title, `${path}.title`),
@@ -662,11 +995,11 @@ function normalizeObjectField(value, path) {
662
995
  : normalizeChoices(input.choices, `${path}.choices`),
663
996
  ...(properties === undefined
664
997
  ? { additionalProperties: additionalProperties }
665
- : { properties, additionalProperties: false }),
998
+ : { properties, additionalProperties: false, ...(variants === undefined ? {} : { variants }) }),
666
999
  validation,
667
1000
  };
668
1001
  }
669
- function normalizeArrayField(value, path) {
1002
+ function normalizeArrayField(value, path, v4 = false, scalarItemsOnly = false) {
670
1003
  const input = asObject(value, path);
671
1004
  assertAllowedKeys(input, path, ['type', 'title', 'description', 'default', 'required', 'items', 'choices', 'validation']);
672
1005
  const itemPath = `${path}.items`;
@@ -677,20 +1010,20 @@ function normalizeArrayField(value, path) {
677
1010
  }
678
1011
  }
679
1012
  let items;
680
- if (item.type === 'object') {
681
- const { default: _default, required: _required, ...objectItem } = normalizeObjectField(item, itemPath);
1013
+ if (item.type === 'object' && !scalarItemsOnly) {
1014
+ const { default: _default, required: _required, ...objectItem } = normalizeObjectField(item, itemPath, v4);
682
1015
  items = objectItem;
683
1016
  }
684
1017
  else {
685
1018
  items = normalizeScalarField(item, itemPath, {
686
- allowDefault: false, allowRequired: false, allowSensitive: false,
1019
+ allowDefault: false, allowRequired: false, allowSensitive: false, allowConst: v4,
687
1020
  });
688
1021
  }
689
- assertChoices(items, itemPath);
1022
+ assertFieldConstraints(items, itemPath);
690
1023
  let validation;
691
1024
  if (input.validation !== undefined) {
692
1025
  const rawValidation = asObject(input.validation, `${path}.validation`);
693
- assertAllowedKeys(rawValidation, `${path}.validation`, ['minItems', 'maxItems', 'uniqueItems']);
1026
+ assertAllowedKeys(rawValidation, `${path}.validation`, ['minItems', 'maxItems', 'uniqueItems', 'uniqueBy']);
694
1027
  const minItems = rawValidation.minItems === undefined
695
1028
  ? undefined
696
1029
  : nonNegativeInteger(rawValidation.minItems, `${path}.validation.minItems`);
@@ -703,10 +1036,17 @@ function normalizeArrayField(value, path) {
703
1036
  if (minItems !== undefined && minItems > (maxItems ?? PLUGIN_CONFIG_ARRAY_MAX_ITEMS)) {
704
1037
  throw new Error(`Invalid ${path}.validation.minItems: must not exceed effective maxItems.`);
705
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
+ }
706
1045
  validation = {
707
1046
  minItems,
708
1047
  maxItems,
709
1048
  uniqueItems: optionalBoolean(rawValidation.uniqueItems, `${path}.validation.uniqueItems`),
1049
+ ...(uniqueBy === undefined ? {} : { uniqueBy }),
710
1050
  };
711
1051
  }
712
1052
  return {
@@ -732,7 +1072,7 @@ export function decodePluginConfigSummaryPath(value) {
732
1072
  throw new Error('Invalid summary path: dangerous config key.');
733
1073
  return property;
734
1074
  }
735
- function expandConfigReferences(input, path) {
1075
+ function expandConfigReferences(input, path, v4 = false) {
736
1076
  const definitions = input.definitions === undefined ? {} : asObject(input.definitions, `${path}.definitions`);
737
1077
  if (Object.keys(definitions).length > PLUGIN_CONFIG_V3_MAX_DEFINITIONS) {
738
1078
  throw new Error(`Invalid ${path}.definitions: expected at most ${PLUGIN_CONFIG_V3_MAX_DEFINITIONS} definitions.`);
@@ -742,7 +1082,7 @@ function expandConfigReferences(input, path) {
742
1082
  throw new Error(`Invalid ${path}.definitions: invalid definition name "${name}".`);
743
1083
  }
744
1084
  }
745
- const expand = (value, fieldPath, chain, child = false) => {
1085
+ const expand = (value, fieldPath, chain, context = 'field') => {
746
1086
  const field = asObject(value, fieldPath);
747
1087
  if (Object.hasOwn(field, '$ref')) {
748
1088
  assertAllowedKeys(field, fieldPath, ['$ref', 'title', 'description', 'default', 'required']);
@@ -757,7 +1097,7 @@ function expandConfigReferences(input, path) {
757
1097
  if (chain.length >= PLUGIN_CONFIG_V3_MAX_REF_DEPTH) {
758
1098
  throw new Error(`Invalid ${fieldPath}.$ref: expected at most ${PLUGIN_CONFIG_V3_MAX_REF_DEPTH} reference edges.`);
759
1099
  }
760
- const result = expand(definitions[name], `${path}.definitions.${name}`, [...chain, name], child);
1100
+ const result = expand(definitions[name], `${path}.definitions.${name}`, [...chain, name], context);
761
1101
  for (const key of ['title', 'description', 'default', 'required']) {
762
1102
  if (Object.hasOwn(field, key))
763
1103
  result[key] = field[key];
@@ -765,43 +1105,67 @@ function expandConfigReferences(input, path) {
765
1105
  return result;
766
1106
  }
767
1107
  const result = { ...field };
768
- if (child)
1108
+ if (context === 'scalar' || (!v4 && context === 'property'))
769
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
+ }
770
1113
  if (field.type === 'array' && field.items !== undefined) {
771
- result.items = expand(field.items, `${fieldPath}.items`, chain);
1114
+ result.items = expand(field.items, `${fieldPath}.items`, chain, context === 'property' ? 'scalar' : 'item');
772
1115
  }
773
1116
  if (field.properties !== undefined) {
774
1117
  const properties = asObject(field.properties, `${fieldPath}.properties`);
775
1118
  if (Object.keys(properties).length > MAX_OBJECT_PROPERTIES) {
776
1119
  throw new Error(`Invalid ${fieldPath}.properties: expected at most ${MAX_OBJECT_PROPERTIES} properties.`);
777
1120
  }
778
- result.properties = Object.fromEntries(Object.entries(properties).map(([key, child]) => [key, expand(child, `${fieldPath}.properties.${key}`, chain, true)]));
1121
+ result.properties = Object.fromEntries(Object.entries(properties).map(([key, child]) => [key, expand(child, `${fieldPath}.properties.${key}`, chain, 'property')]));
779
1122
  }
780
1123
  if (field.additionalProperties !== undefined && field.additionalProperties !== false) {
781
- result.additionalProperties = expand(field.additionalProperties, `${fieldPath}.additionalProperties`, chain, true);
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`) }) };
782
1140
  }
783
1141
  return result;
784
1142
  };
785
1143
  // Validate unused definitions too, through the same field/default/choice validator.
1144
+ const normalizedDefinitions = [];
786
1145
  for (const [name, value] of Object.entries(definitions)) {
787
- normalizeConfigField(expand(value, `${path}.definitions.${name}`, []), `${path}.definitions.${name}`, true);
1146
+ normalizedDefinitions.push(normalizeConfigField(expand(value, `${path}.definitions.${name}`, []), `${path}.definitions.${name}`, true, v4));
788
1147
  }
789
1148
  const fields = asObject(input.fields, `${path}.fields`);
790
1149
  if (Object.keys(fields).length > PLUGIN_CONFIG_V3_MAX_FIELDS) {
791
1150
  throw new Error(`Invalid ${path}.fields: expected at most ${PLUGIN_CONFIG_V3_MAX_FIELDS} fields.`);
792
1151
  }
793
- return Object.fromEntries(Object.entries(fields).map(([key, value]) => [key, expand(value, `${path}.fields.${key}`, [])]));
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;
794
1158
  }
795
- function normalizeConfigField(rawDefinition, path, allowArray) {
1159
+ function normalizeConfigField(rawDefinition, path, allowArray, v4 = false) {
796
1160
  const rawField = asObject(rawDefinition, path);
797
1161
  const definition = rawField.type === 'object'
798
- ? normalizeObjectField(rawField, path)
1162
+ ? normalizeObjectField(rawField, path, v4)
799
1163
  : rawField.type === 'array' && allowArray
800
- ? normalizeArrayField(rawField, path)
1164
+ ? normalizeArrayField(rawField, path, v4)
801
1165
  : normalizeScalarField(rawField, path, {
802
- allowDefault: true, allowRequired: true, allowSensitive: true,
1166
+ allowDefault: true, allowRequired: true, allowSensitive: true, allowConst: v4,
803
1167
  });
804
- assertChoices(definition, path);
1168
+ assertFieldConstraints(definition, path);
805
1169
  if (definition.default !== undefined) {
806
1170
  const issues = validatePluginConfigValue(definition, definition.default, path);
807
1171
  if (issues.length > 0)
@@ -868,7 +1232,15 @@ function normalizeConfigSummaries(input, fields, path) {
868
1232
  if (!summary)
869
1233
  throw new Error(`Invalid ${viewPath}.${itemSummary ? 'itemSummary' : 'summary'}: unknown summary.`);
870
1234
  for (const part of summary.parts) {
871
- if (!ownProperty(objectField.properties, decodePluginConfigSummaryPath(part.path))) {
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')) {
872
1244
  throw new Error(`Invalid ${viewPath}: summary path "${part.path}" must name a declared scalar property.`);
873
1245
  }
874
1246
  }
@@ -881,15 +1253,23 @@ function normalizeConfigSummaries(input, fields, path) {
881
1253
  }
882
1254
  export function normalizePluginConfigDefinition(value, path = 'config') {
883
1255
  const input = asObject(value, path);
884
- const v3 = input.schemaVersion === 3;
1256
+ const v4 = input.schemaVersion === 4;
1257
+ const v3 = input.schemaVersion === 3 || v4;
885
1258
  assertAllowedKeys(input, path, v3 ? ['schemaVersion', 'definitions', 'fields', 'presentation'] : ['fields', 'presentation']);
886
- const fieldsInput = v3 ? expandConfigReferences(input, path) : asObject(input.fields, `${path}.fields`);
1259
+ const fieldsInput = v3 ? expandConfigReferences(input, path, v4) : asObject(input.fields, `${path}.fields`);
887
1260
  const fields = createPropertyMap();
888
1261
  for (const [field, rawDefinition] of Object.entries(fieldsInput)) {
889
1262
  if (!CONFIG_KEY_PATTERN.test(field) || isDangerousConfigKey(field)) {
890
1263
  throw new Error(`Invalid ${path}.fields: config key "${field}" is not valid.`);
891
1264
  }
892
- fields[field] = normalizeConfigField(rawDefinition, `${path}.fields.${field}`, v3);
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])}`);
893
1273
  }
894
1274
  let presentation;
895
1275
  if (input.presentation !== undefined) {
@@ -935,6 +1315,6 @@ export function normalizePluginConfigDefinition(value, path = 'config') {
935
1315
  }),
936
1316
  };
937
1317
  }
938
- return { ...(v3 ? { schemaVersion: 3 } : {}), fields, presentation };
1318
+ return { ...(v3 ? { schemaVersion: v4 ? 4 : 3 } : {}), fields, presentation };
939
1319
  }
940
1320
  //# sourceMappingURL=plugin-config.js.map