@varde-flyt/vfac 0.14.0 → 0.15.0

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 (3) hide show
  1. package/README.md +12 -0
  2. package/dist/vfac.mjs +992 -386
  3. package/package.json +1 -1
package/dist/vfac.mjs CHANGED
@@ -12081,11 +12081,13 @@ function leafShapes(bounds) {
12081
12081
  * particular value could not say so, so `vfac plan`'s `requiredSecrets`
12082
12082
  * could not warn before a deploy, and the refusal arrived at apply.
12083
12083
  *
12084
- * ON A SECRET ONLY, and that is narrower than the mechanism could be. A
12085
- * value-conditioned requirement for an ORDINARY setting is a real gap too,
12086
- * but it changes what every door accepts for every property; this changes
12087
- * what `requiredSecrets` reports, which is one answer with one reader.
12088
- * Opening it wider is a separate decision with its own blast radius.
12084
+ * IT BEGAN ON A ROOT SECRET ONLY, and was widened twice, each time as a
12085
+ * decision of its own: to every ROOT setting, which the number shape below
12086
+ * records, and then to a setting in a GROUP at any depth, which is where a
12087
+ * credential belonging to one sign-in method actually lives. It is still
12088
+ * refused on a setting BELOW A LIST, structurally — the leaf set the chain
12089
+ * uses there does not declare the key — because a setting there has one
12090
+ * path per row, and a condition makes one setting required.
12089
12091
  */
12090
12092
  "x-required-when": ValueConditionSchema.optional(),
12091
12093
  /**
@@ -12109,11 +12111,12 @@ function leafShapes(bounds) {
12109
12111
  maximum: external_exports.number(),
12110
12112
  default: external_exports.number().optional(),
12111
12113
  /**
12112
- * WIDENED FROM SECRETS TO EVERY ROOT SETTING, which this shape's own
12113
- * comment on the string leaf used to record as a known gap: "a
12114
- * value-conditioned requirement for an ORDINARY setting is a real gap
12115
- * too". It is the same keyword, the same evaluator and the same publish
12116
- * rules; what changes is which settings may carry it.
12114
+ * WIDENED FROM SECRETS TO EVERY ROOT SETTING, which the string leaf's
12115
+ * comment used to record as a known gap: "a value-conditioned requirement
12116
+ * for an ORDINARY setting is a real gap too". It is the same keyword, the
12117
+ * same evaluator and the same publish rules; what changes is which
12118
+ * settings may carry it — and a setting in a group may too, since the
12119
+ * chain's group leaf set keeps this key and only its list set omits it.
12117
12120
  */
12118
12121
  "x-required-when": ValueConditionSchema.optional()
12119
12122
  }).strict();
@@ -12158,14 +12161,33 @@ var ConfigurationPropertySchema = external_exports.discriminatedUnion("type", [
12158
12161
  BooleanPropertySchema,
12159
12162
  ArrayPropertySchema
12160
12163
  ]);
12161
- var NestedStringPropertySchema = V2_LEAVES.string.omit({ "x-secret": true, writeOnly: true, "x-required-when": true }).extend({ group: ConfigurationGroupSchema.optional() });
12162
- var V2_NESTED_LEAVES = {
12163
- string: NestedStringPropertySchema,
12164
- number: V2_LEAVES.number.omit({ "x-required-when": true }).extend({ group: ConfigurationGroupSchema.optional() }),
12165
- boolean: V2_LEAVES.boolean.omit({ "x-required-when": true }).extend({ group: ConfigurationGroupSchema.optional() }),
12166
- array: V2_LEAVES.array.omit({ "x-required-when": true }).extend({ group: ConfigurationGroupSchema.optional() }),
12164
+ var INHERITED_GROUP = { group: ConfigurationGroupSchema.optional() };
12165
+ var V2_GROUP_LEAVES = {
12166
+ string: withJsonSchemaAnnex(V2_LEAVES.string.extend(INHERITED_GROUP), SECRET_COUPLING_ANNEX),
12167
+ number: V2_LEAVES.number.extend(INHERITED_GROUP),
12168
+ boolean: V2_LEAVES.boolean.extend(INHERITED_GROUP),
12169
+ array: V2_LEAVES.array.extend(INHERITED_GROUP),
12167
12170
  items: V2_LEAVES.items
12168
12171
  };
12172
+ var V2_LIST_LEAVES = {
12173
+ string: withJsonSchemaAnnex(V2_LEAVES.string.omit({ "x-required-when": true }).extend(INHERITED_GROUP), SECRET_COUPLING_ANNEX),
12174
+ number: V2_LEAVES.number.omit({ "x-required-when": true }).extend(INHERITED_GROUP),
12175
+ boolean: V2_LEAVES.boolean.omit({ "x-required-when": true }).extend(INHERITED_GROUP),
12176
+ array: V2_LEAVES.array.omit({ "x-required-when": true }).extend(INHERITED_GROUP),
12177
+ items: V2_LEAVES.items
12178
+ };
12179
+ var LIST_OF_GROUPS_CONDITION_ANNEX = {
12180
+ allOf: [
12181
+ {
12182
+ $comment: "A list of groups inside a group may not carry x-required-when: the condition makes a setting required, and a list of groups is not one.",
12183
+ if: {
12184
+ properties: { items: { properties: { type: { const: "object" } }, required: ["type"] } },
12185
+ required: ["items"]
12186
+ },
12187
+ then: { not: { required: ["x-required-when"] } }
12188
+ }
12189
+ ]
12190
+ };
12169
12191
  function childBagShape(child) {
12170
12192
  return {
12171
12193
  properties: external_exports.record(PropertyNameSchema, child),
@@ -12190,41 +12212,49 @@ function childBagShape(child) {
12190
12212
  additionalProperties: external_exports.literal(false)
12191
12213
  };
12192
12214
  }
12193
- function propertyUnion(levelsBelow, leafSet) {
12215
+ var LEAF_SETS = { root: V2_LEAVES, group: V2_GROUP_LEAVES, list: V2_LIST_LEAVES };
12216
+ var LEVELS = /* @__PURE__ */ new Map();
12217
+ function levelUnion(levelsBelow, position) {
12218
+ const key = `${position}:${levelsBelow}`;
12219
+ const built = LEVELS.get(key);
12220
+ if (built !== void 0)
12221
+ return built;
12222
+ const level = MAX_CONFIGURATION_DEPTH - levelsBelow;
12223
+ const union = withJsonSchemaDefinition(external_exports.discriminatedUnion("type", propertyUnion(levelsBelow, position)), position === "group" ? `configurationPropertyLevel${level}` : `configurationListPropertyLevel${level}`);
12224
+ LEVELS.set(key, union);
12225
+ return union;
12226
+ }
12227
+ function propertyUnion(levelsBelow, position) {
12228
+ const leafSet = LEAF_SETS[position];
12194
12229
  const leaves = [leafSet.string, leafSet.number, leafSet.boolean];
12195
12230
  if (levelsBelow <= 0) {
12196
12231
  return [...leaves, leafSet.array];
12197
12232
  }
12198
- const level = MAX_CONFIGURATION_DEPTH - levelsBelow + 1;
12199
- const child = withJsonSchemaDefinition(external_exports.discriminatedUnion("type", propertyUnion(levelsBelow - 1, V2_NESTED_LEAVES)), `configurationPropertyLevel${level}`);
12200
- const bag = childBagShape(child);
12201
- const groupItems = external_exports.object({ type: external_exports.literal("object"), ...bag }).strict();
12233
+ const groupChild = levelUnion(levelsBelow - 1, position === "list" ? "list" : "group");
12234
+ const rowChild = levelUnion(levelsBelow - 1, "list");
12235
+ const groupItems = external_exports.object({ type: external_exports.literal("object"), ...childBagShape(rowChild) }).strict();
12236
+ const list = leafSet.array.extend({
12237
+ items: external_exports.discriminatedUnion("type", [V2_LEAVES.items, groupItems]),
12238
+ /**
12239
+ * Rules over the ROWS of this list — see {@link RowRulesSchema}.
12240
+ *
12241
+ * HERE RATHER THAN ON THE LEAF SHAPE, and that placement is the whole
12242
+ * enforcement. `leafShapes().array` is also a v1 list and a last-level
12243
+ * list, both of which hold STRINGS: a rule naming a field of a row would
12244
+ * name nothing there, and `uniqueItems` is already the right keyword for
12245
+ * comparing whole scalar entries. This branch is the only one in the chain
12246
+ * where a list can hold groups, so declaring the keyword here makes "rows
12247
+ * have fields" a property of the shape rather than a rule to remember.
12248
+ *
12249
+ * The refinement still has to refuse a rule on a list of STRINGS, because
12250
+ * `items` is a discriminated union INSIDE this branch — the shape narrows
12251
+ * where a row can exist, not whether this particular list has one.
12252
+ */
12253
+ "x-row-rules": RowRulesSchema.optional()
12254
+ });
12202
12255
  return [
12203
12256
  ...leaves,
12204
- // ONE `array` BRANCH, NOT TWO, because a discriminated union may not carry
12205
- // two options with the same discriminator value ("duplicate value array" —
12206
- // zod refuses it at construction). The item shape is the inner
12207
- // discriminator, which is also what keeps a v1 document's
12208
- // `items: {type: 'string'}` reading exactly as it always did.
12209
- leafSet.array.extend({
12210
- items: external_exports.discriminatedUnion("type", [V2_LEAVES.items, groupItems]),
12211
- /**
12212
- * Rules over the ROWS of this list — see {@link RowRulesSchema}.
12213
- *
12214
- * HERE RATHER THAN ON THE LEAF SHAPE, and that placement is the whole
12215
- * enforcement. `leafShapes().array` is also a v1 list and a last-level
12216
- * list, both of which hold STRINGS: a rule naming a field of a row would
12217
- * name nothing there, and `uniqueItems` is already the right keyword for
12218
- * comparing whole scalar entries. This branch is the only one in the chain
12219
- * where a list can hold groups, so declaring the keyword here makes "rows
12220
- * have fields" a property of the shape rather than a rule to remember.
12221
- *
12222
- * The refinement still has to refuse a rule on a list of STRINGS, because
12223
- * `items` is a discriminated union INSIDE this branch — the shape narrows
12224
- * where a row can exist, not whether this particular list has one.
12225
- */
12226
- "x-row-rules": RowRulesSchema.optional()
12227
- }),
12257
+ position === "group" ? withJsonSchemaAnnex(list, LIST_OF_GROUPS_CONDITION_ANNEX) : list,
12228
12258
  /**
12229
12259
  * A group of related settings.
12230
12260
  *
@@ -12235,20 +12265,23 @@ function propertyUnion(levelsBelow, leafSet) {
12235
12265
  * load-bearing "a defaulted setting is never absent", and what
12236
12266
  * `seedConfiguration` should write into a served example and a file on
12237
12267
  * disk. Defaults are a LEAF concept here.
12268
+ *
12269
+ * NO `x-required-when` EITHER, at any position: it makes a setting required,
12270
+ * and a group is not a setting.
12238
12271
  */
12239
12272
  external_exports.object({
12240
12273
  type: external_exports.literal("object"),
12241
12274
  title: TitleSchema,
12242
12275
  description: helpTextSchema(V2_LEAF_BOUNDS).optional(),
12243
- // INHERITED BELOW THE ROOT, for the reason `V2_NESTED_LEAVES` states: a
12276
+ // INHERITED BELOW THE ROOT, for the reason `INHERITED_GROUP` states: a
12244
12277
  // group is a bucket for a whole subtree, and repeating it is the only
12245
12278
  // value that could be written.
12246
- group: levelsBelow === MAX_CONFIGURATION_DEPTH - 1 ? ConfigurationGroupSchema : ConfigurationGroupSchema.optional(),
12247
- ...bag
12279
+ group: position === "root" ? ConfigurationGroupSchema : ConfigurationGroupSchema.optional(),
12280
+ ...childBagShape(groupChild)
12248
12281
  }).strict()
12249
12282
  ];
12250
12283
  }
12251
- var RootConfigurationPropertySchema = external_exports.discriminatedUnion("type", propertyUnion(MAX_CONFIGURATION_DEPTH - 1, V2_LEAVES));
12284
+ var RootConfigurationPropertySchema = external_exports.discriminatedUnion("type", propertyUnion(MAX_CONFIGURATION_DEPTH - 1, "root"));
12252
12285
  var CustomerConfigurationObjectSchema = external_exports.object({
12253
12286
  type: external_exports.literal("object"),
12254
12287
  properties: external_exports.record(PropertyNameSchema, ConfigurationPropertySchema).describe("Customer-editable settings, keyed by camelCase property name."),
@@ -12314,7 +12347,7 @@ var CustomerConfigurationSchema = CustomerConfigurationObjectSchema.superRefine(
12314
12347
  }
12315
12348
  checkDependentRequired(config, ctx);
12316
12349
  });
12317
- function checkDeclaredProperty(name, property, path, ctx, atRoot) {
12350
+ function checkDeclaredProperty(name, property, path, ctx, atRoot, underList2 = false) {
12318
12351
  const infrastructure = infrastructureVocabularyViolation(name);
12319
12352
  if (infrastructure) {
12320
12353
  ctx.addIssue({
@@ -12348,13 +12381,20 @@ function checkDeclaredProperty(name, property, path, ctx, atRoot) {
12348
12381
  ctx.addIssue({
12349
12382
  code: external_exports.ZodIssueCode.custom,
12350
12383
  path,
12351
- // THE REMEDIATION HAS TO BE ONE THE CONTRACT ALLOWS. At the root, the
12352
- // answer is to declare the two markers. Below it, that answer is
12353
- // IMPOSSIBLE — the nested shape omits both keys and `.strict()` refuses
12354
- // them — so telling a publisher to add them sends them to write a
12355
- // second invalid document, and an agent following the validator loops.
12356
- // A validator's errors are part of the agent-facing contract.
12357
- message: atRoot ? `${credential} and must be declared "x-secret": true with "writeOnly": true, or renamed if it is not one.` : `${credential}, and a credential must be a TOP-LEVEL setting in this contract format \u2014 move it out of the group and declare it "x-secret": true with "writeOnly": true, or rename it if it is not one.`
12384
+ // THE REMEDIATION HAS TO BE ONE THE CONTRACT ALLOWS. In a group, as at
12385
+ // the root, the answer is to declare the two markers. Below a LIST that
12386
+ // answer is impossible — a secret is refused there, below — so telling a
12387
+ // publisher to add them sends them to write a second invalid document,
12388
+ // and an agent following the validator loops. A validator's errors are
12389
+ // part of the agent-facing contract.
12390
+ message: underList2 ? `${credential}, and a secret cannot be declared inside a list in this contract format \u2014 move it out of the list and declare it "x-secret": true with "writeOnly": true, or rename it if it is not one.` : `${credential} and must be declared "x-secret": true with "writeOnly": true, or renamed if it is not one.`
12391
+ });
12392
+ }
12393
+ if (underList2 && (secret || property.writeOnly === true)) {
12394
+ ctx.addIssue({
12395
+ code: external_exports.ZodIssueCode.custom,
12396
+ path,
12397
+ message: `"${name}": a secret cannot be declared inside a list in this contract format. Declare it in a group or at the root.`
12358
12398
  });
12359
12399
  }
12360
12400
  if (secret && property.writeOnly !== true) {
@@ -12549,9 +12589,9 @@ function checkDependentRequired(config, ctx, at = []) {
12549
12589
  }
12550
12590
  }
12551
12591
  }
12552
- checkOneSecretPerGroup(config, ctx);
12592
+ checkOneSecretPerGroup(config, ctx, at);
12553
12593
  }
12554
- function checkOneSecretPerGroup(config, ctx) {
12594
+ function checkOneSecretPerGroup(config, ctx, at = []) {
12555
12595
  const clauses = Object.entries(config.dependentRequired ?? {});
12556
12596
  if (clauses.length === 0)
12557
12597
  return;
@@ -12595,11 +12635,238 @@ function checkOneSecretPerGroup(config, ctx) {
12595
12635
  code: external_exports.ZodIssueCode.custom,
12596
12636
  // The component has no single name, so the issue is reported against the
12597
12637
  // map rather than against whichever member happened to be visited first.
12598
- path: ["dependentRequired"],
12638
+ path: [...at, "dependentRequired"],
12599
12639
  message: `These settings are configured together and include more than one secret (${secrets.join(", ")}). Only one secret can be supplied per change, so a group must ask for at most one \u2014 otherwise completing it on a running resource takes several, in an order the customer has to work out.`
12600
12640
  });
12601
12641
  }
12602
12642
  }
12643
+ function checkOptionalGroupActivation(config, ctx) {
12644
+ const context = { root: config, acceptsOutside: /* @__PURE__ */ new Map() };
12645
+ const check = (container, at, segments) => {
12646
+ const required = new Set(container.required ?? []);
12647
+ for (const [name, property] of Object.entries(container.properties)) {
12648
+ if (property.type !== "object")
12649
+ continue;
12650
+ const path = [...at, "properties", name];
12651
+ const inner = [...segments, name];
12652
+ if (!required.has(name)) {
12653
+ const { secrets, conditional } = activationSecrets(context, property, inner);
12654
+ if (secrets.length > 1) {
12655
+ ctx.addIssue({
12656
+ code: external_exports.ZodIssueCode.custom,
12657
+ path,
12658
+ message: `"${name}" is optional, and adding it to a resource that already exists requires more than one secret (${secrets.join(", ")}). Only one secret can be supplied per change, so an optional group must ask for at most one \u2014 otherwise a resource created without it can never add it. Require at most one of them, or make the group required, with every group above it, so it is supplied when the resource is created.` + conditionalActivationSentence(conditional)
12659
+ });
12660
+ }
12661
+ }
12662
+ check(property, path, inner);
12663
+ }
12664
+ };
12665
+ check(config, [], []);
12666
+ }
12667
+ function conditionalActivationSentence(conditional) {
12668
+ if (conditional.length === 0)
12669
+ return "";
12670
+ const one = conditional.length === 1;
12671
+ return ` ${conditional.join(", ")} ${one ? "is" : "are"} counted although ${one ? "it is" : "they are"} conditional: adding the group always turns ${one ? "its" : "their"} "x-required-when" on, because the setting ${one ? "that condition reads" : "each condition reads"} is then present and no value it accepts outside "anyOf" was found. Let ${one ? "that" : "such a"} setting be left out, or declare an "enum" for it that includes a value outside "anyOf".`;
12672
+ }
12673
+ function declarationKey(segments) {
12674
+ return `/${segments.join("/")}`;
12675
+ }
12676
+ function activationSecrets(context, group, segments) {
12677
+ const carriers = conditionCarriersBelow(group, segments);
12678
+ const unavoidable = /* @__PURE__ */ new Set();
12679
+ const unconditioned = walkActivation(group, segments, unavoidable);
12680
+ let walk = unconditioned;
12681
+ for (; ; ) {
12682
+ const found = carriers.filter((carrier) => !unavoidable.has(carrier.key) && conditionUnavoidable(context, segments, carrier.condition, walk));
12683
+ if (found.length === 0)
12684
+ break;
12685
+ for (const carrier of found)
12686
+ unavoidable.add(carrier.key);
12687
+ walk = walkActivation(group, segments, unavoidable);
12688
+ }
12689
+ const secrets = [...walk.secrets].sort();
12690
+ return {
12691
+ secrets,
12692
+ conditional: secrets.filter((path) => unavoidable.has(path) && !unconditioned.secrets.has(path))
12693
+ };
12694
+ }
12695
+ function walkActivation(group, segments, unavoidable) {
12696
+ const secrets = /* @__PURE__ */ new Set();
12697
+ const demanded = /* @__PURE__ */ new Set();
12698
+ const sent = /* @__PURE__ */ new Set();
12699
+ const onTheWay = /* @__PURE__ */ new Set();
12700
+ for (const carrier of unavoidable) {
12701
+ const parts = carrier.slice(1).split("/");
12702
+ for (let depth = segments.length + 1; depth <= parts.length; depth += 1) {
12703
+ onTheWay.add(declarationKey(parts.slice(0, depth)));
12704
+ }
12705
+ }
12706
+ const visit = (node, nodeSegments) => {
12707
+ sent.add(declarationKey(nodeSegments));
12708
+ const pending = [
12709
+ ...node.required ?? [],
12710
+ ...Object.keys(node.properties).filter((name) => onTheWay.has(declarationKey([...nodeSegments, name])))
12711
+ ];
12712
+ const seen = /* @__PURE__ */ new Set();
12713
+ while (pending.length > 0) {
12714
+ const name = pending.pop();
12715
+ if (seen.has(name))
12716
+ continue;
12717
+ seen.add(name);
12718
+ if (!Object.hasOwn(node.properties, name))
12719
+ continue;
12720
+ const property = node.properties[name];
12721
+ const key = declarationKey([...nodeSegments, name]);
12722
+ demanded.add(key);
12723
+ if (isSecretProperty(property)) {
12724
+ secrets.add(key);
12725
+ } else if (property.type === "object") {
12726
+ visit(property, [...nodeSegments, name]);
12727
+ }
12728
+ const clauses = node.dependentRequired;
12729
+ if (clauses !== void 0 && Object.hasOwn(clauses, name))
12730
+ pending.push(...clauses[name]);
12731
+ }
12732
+ };
12733
+ visit(group, segments);
12734
+ return { secrets, demanded, sent };
12735
+ }
12736
+ function conditionCarriersBelow(group, segments) {
12737
+ const out = [];
12738
+ const collect = (node, nodeSegments) => {
12739
+ for (const [name, property] of Object.entries(node.properties)) {
12740
+ const here = [...nodeSegments, name];
12741
+ if (property.type === "object") {
12742
+ collect(property, here);
12743
+ continue;
12744
+ }
12745
+ const condition = property["x-required-when"];
12746
+ if (condition !== void 0)
12747
+ out.push({ key: declarationKey(here), condition });
12748
+ }
12749
+ };
12750
+ collect(group, segments);
12751
+ return out;
12752
+ }
12753
+ function conditionUnavoidable(context, group, condition, walk) {
12754
+ const target = resolveConditionTarget(context.root, condition.path);
12755
+ if (target === null)
12756
+ return false;
12757
+ const { property } = target;
12758
+ if (property.type !== "string" || isSecretProperty(property))
12759
+ return false;
12760
+ const groupOrAbove = (segments) => segments.length <= group.length && segments.every((segment, i) => group[i] === segment);
12761
+ const inside = (segments) => segments.length > group.length && group.every((segment, i) => segments[i] === segment);
12762
+ let belowList = false;
12763
+ for (const container of target.containers) {
12764
+ if (container.kind === "group" && groupOrAbove(container.segments))
12765
+ continue;
12766
+ if (container.kind === "list" && condition.anyRow !== true)
12767
+ return false;
12768
+ const key = declarationKey(container.segments);
12769
+ const present2 = !belowList && inside(container.segments) ? container.kind === "group" ? walk.sent.has(key) : walk.demanded.has(key) : container.required;
12770
+ if (!present2)
12771
+ return false;
12772
+ if (container.kind === "list") {
12773
+ if (container.minItems < 1)
12774
+ return false;
12775
+ belowList = true;
12776
+ }
12777
+ }
12778
+ const defaulted = property.default !== void 0;
12779
+ const present = !belowList && inside(target.segments) ? walk.demanded.has(declarationKey(target.segments)) || defaulted && walk.sent.has(declarationKey(target.segments.slice(0, -1))) : target.required || defaulted;
12780
+ if (!present)
12781
+ return false;
12782
+ let outside = context.acceptsOutside.get(condition);
12783
+ if (outside === void 0) {
12784
+ outside = acceptsValueOutside(property, condition.anyOf);
12785
+ context.acceptsOutside.set(condition, outside);
12786
+ }
12787
+ return !outside;
12788
+ }
12789
+ function resolveConditionTarget(root, path) {
12790
+ const pointer = path.startsWith("/");
12791
+ const segments = pointer ? path.slice(1).split("/") : [path];
12792
+ if (pointer && segments.length < 2)
12793
+ return null;
12794
+ if (!segments.every((segment) => CONFIGURATION_PROPERTY_NAME_REGEX.test(segment)))
12795
+ return null;
12796
+ const containers = [];
12797
+ let holder = root;
12798
+ for (const [index, name] of segments.entries()) {
12799
+ if (!Object.hasOwn(holder.properties, name))
12800
+ return null;
12801
+ const property = holder.properties[name];
12802
+ const here = segments.slice(0, index + 1);
12803
+ const required = (holder.required ?? []).includes(name);
12804
+ const last = index === segments.length - 1;
12805
+ if (property.type === "object") {
12806
+ if (last)
12807
+ return null;
12808
+ containers.push({ segments: here, kind: "group", required, minItems: 0 });
12809
+ holder = property;
12810
+ } else if (property.type === "array" && property.items.type === "object") {
12811
+ if (last)
12812
+ return null;
12813
+ containers.push({ segments: here, kind: "list", required, minItems: property.minItems ?? 0 });
12814
+ holder = property.items;
12815
+ } else {
12816
+ return last ? { segments, containers, property, required } : null;
12817
+ }
12818
+ }
12819
+ return null;
12820
+ }
12821
+ function acceptsValueOutside(property, anyOf) {
12822
+ let pattern = null;
12823
+ if (property.pattern !== void 0) {
12824
+ try {
12825
+ pattern = new RegExp(property.pattern);
12826
+ } catch {
12827
+ return false;
12828
+ }
12829
+ }
12830
+ const accepts = (value) => !anyOf.includes(value) && value.trim().length > 0 && (property.enum === void 0 || property.enum.includes(value)) && (property.minLength === void 0 || value.length >= property.minLength) && (property.maxLength === void 0 || value.length <= property.maxLength) && (pattern === null || pattern.test(value));
12831
+ if (property.enum !== void 0)
12832
+ return property.enum.some(accepts);
12833
+ if (property.default !== void 0 && accepts(property.default))
12834
+ return true;
12835
+ const floor = property.minLength ?? 0;
12836
+ const ceiling = Math.min(property.maxLength ?? floor + 3, floor + 3);
12837
+ for (let length = Math.max(floor, 1); length <= ceiling; length += 1) {
12838
+ for (const fill of ["x", "a", "0"]) {
12839
+ if (accepts(fill.repeat(length)))
12840
+ return true;
12841
+ }
12842
+ }
12843
+ for (const word of property.pattern?.match(/[A-Za-z0-9_-]+/g) ?? []) {
12844
+ if (accepts(word))
12845
+ return true;
12846
+ }
12847
+ for (const value of anyOf) {
12848
+ for (let index = 0; index < value.length; index += 1) {
12849
+ const variant = value.slice(0, index) + sameClassCharacter(value.charAt(index)) + value.slice(index + 1);
12850
+ if (accepts(variant))
12851
+ return true;
12852
+ }
12853
+ }
12854
+ return false;
12855
+ }
12856
+ function sameClassCharacter(character) {
12857
+ const code = character.charCodeAt(0);
12858
+ for (const [first, last] of [
12859
+ ["0", "9"],
12860
+ ["a", "z"],
12861
+ ["A", "Z"]
12862
+ ]) {
12863
+ const start = first.charCodeAt(0);
12864
+ const end = last.charCodeAt(0);
12865
+ if (code >= start && code <= end)
12866
+ return String.fromCharCode(code === end ? start : code + 1);
12867
+ }
12868
+ return "x";
12869
+ }
12603
12870
  var CustomerConfigurationV2ObjectSchema = external_exports.object({
12604
12871
  type: external_exports.literal("object"),
12605
12872
  properties: external_exports.record(PropertyNameSchema, RootConfigurationPropertySchema).describe("Customer-editable settings, keyed by camelCase property name."),
@@ -12683,10 +12950,10 @@ function minimumPropertyBytes(property) {
12683
12950
  function declaredLeafDefault(property) {
12684
12951
  return property.type === "object" ? void 0 : property.default;
12685
12952
  }
12686
- function checkPropertyDeclaration(name, property, path, ctx, atRoot) {
12953
+ function checkPropertyDeclaration(name, property, path, ctx, atRoot, underList2 = false) {
12687
12954
  const children = childrenOf(property);
12688
12955
  if (children === null) {
12689
- checkDeclaredProperty(name, property, path, ctx, atRoot);
12956
+ checkDeclaredProperty(name, property, path, ctx, atRoot, underList2);
12690
12957
  return;
12691
12958
  }
12692
12959
  const infrastructure = infrastructureVocabularyViolation(name);
@@ -12732,8 +12999,15 @@ function checkPropertyDeclaration(name, property, path, ctx, atRoot) {
12732
12999
  message: `"${name}" has minItems greater than maxItems.`
12733
13000
  });
12734
13001
  }
13002
+ if (!atRoot && property.type === "array" && property["x-required-when"] !== void 0) {
13003
+ ctx.addIssue({
13004
+ code: external_exports.ZodIssueCode.custom,
13005
+ path: [...path, "x-required-when"],
13006
+ message: `"${name}" is a list of groups, and "x-required-when" may not be declared on one inside a group. The condition makes one setting required, and a list of groups is not a setting \u2014 declare the condition on a setting instead.`
13007
+ });
13008
+ }
12735
13009
  for (const childName of childNames) {
12736
- checkPropertyDeclaration(childName, children.properties[childName], [...path, "properties", childName], ctx, false);
13010
+ checkPropertyDeclaration(childName, children.properties[childName], [...path, "properties", childName], ctx, false, underList2 || property.type === "array");
12737
13011
  }
12738
13012
  for (const required of children.required ?? []) {
12739
13013
  if (!Object.hasOwn(children.properties, required)) {
@@ -12816,6 +13090,7 @@ var CustomerConfigurationV2Schema = CustomerConfigurationV2ObjectSchema.superRef
12816
13090
  }
12817
13091
  checkDependentRequired(config, ctx);
12818
13092
  checkOneSecretPerGroup(config, ctx);
13093
+ checkOptionalGroupActivation(config, ctx);
12819
13094
  });
12820
13095
  function enumerableValues(property) {
12821
13096
  const out = [];
@@ -12876,13 +13151,16 @@ function parseConfigurationPath(raw) {
12876
13151
  return null;
12877
13152
  return configurationPath(...segments);
12878
13153
  }
13154
+ function secretPathOf(raw) {
13155
+ const path = parseConfigurationPath(raw);
13156
+ if (path === null)
13157
+ return null;
13158
+ return pathSegments(path).some((segment) => typeof segment === "number") ? null : path;
13159
+ }
12879
13160
  function pathSegments(path) {
12880
13161
  const raw = path.startsWith("/") ? path.slice(1).split("/") : [path];
12881
13162
  return raw.map((segment) => isIndexSegment(segment) ? Number(segment) : segment);
12882
13163
  }
12883
- function rootSegment(path) {
12884
- return String(pathSegments(path)[0]);
12885
- }
12886
13164
  function declarationPathOf(path) {
12887
13165
  const segments = pathSegments(path).filter((segment) => typeof segment !== "number");
12888
13166
  return configurationPath(...segments) ?? path;
@@ -12932,6 +13210,18 @@ function immediateContainer(node) {
12932
13210
  function underList(node) {
12933
13211
  return node.containers.some((container) => container.kind === "array");
12934
13212
  }
13213
+ function containersRequiredToRoot(schema, node) {
13214
+ const rootRequired = schema.required ?? [];
13215
+ return node.containers.every((container, index) => {
13216
+ const parent = node.containers[index - 1];
13217
+ return (parent === void 0 ? rootRequired : parent.required).includes(container.name);
13218
+ });
13219
+ }
13220
+ function requiredToRoot(schema, node) {
13221
+ const parent = immediateContainer(node);
13222
+ const declared = parent === null ? schema.required ?? [] : parent.required;
13223
+ return declared.includes(node.name) && containersRequiredToRoot(schema, node);
13224
+ }
12935
13225
  function containerChildren(property) {
12936
13226
  if (property.type === "object" && property.properties !== void 0) {
12937
13227
  return {
@@ -12990,6 +13280,9 @@ function walkDeclarations(properties, ancestors, base, onLeaf, onContainer = ()
12990
13280
  walkDeclarations(children.properties, [...ancestors, container], segments, onLeaf, onContainer);
12991
13281
  }
12992
13282
  }
13283
+ function declaredSecretPaths(schema) {
13284
+ return configurationLeaves(schema).filter((leaf) => isSecretProperty(leaf.property) && !underList(leaf)).map((leaf) => leaf.path).sort();
13285
+ }
12993
13286
  function declaredDefault(property) {
12994
13287
  if (property.type === "object")
12995
13288
  return void 0;
@@ -13025,7 +13318,8 @@ function expandDeclarationPath(configuration, declaration) {
13025
13318
  const [head, ...rest] = remaining;
13026
13319
  if (head === void 0)
13027
13320
  return;
13028
- walk(cursor[String(head)], rest, [...soFar, head]);
13321
+ const key = String(head);
13322
+ walk(Object.hasOwn(cursor, key) ? cursor[key] : void 0, rest, [...soFar, head]);
13029
13323
  };
13030
13324
  walk(configuration, segmentsOf(declaration), []);
13031
13325
  return out;
@@ -13039,7 +13333,7 @@ function valueAt(configuration, path) {
13039
13333
  cursor = cursor[segment];
13040
13334
  continue;
13041
13335
  }
13042
- if (!isPlainObject(cursor))
13336
+ if (!isPlainObject(cursor) || !Object.hasOwn(cursor, segment))
13043
13337
  return void 0;
13044
13338
  cursor = cursor[segment];
13045
13339
  }
@@ -13055,7 +13349,7 @@ function valuesAt(configuration, path) {
13055
13349
  next.push(cursor[segment]);
13056
13350
  continue;
13057
13351
  }
13058
- if (!isPlainObject(cursor))
13352
+ if (!isPlainObject(cursor) || !Object.hasOwn(cursor, segment))
13059
13353
  continue;
13060
13354
  const value = cursor[segment];
13061
13355
  if (Array.isArray(value))
@@ -13076,10 +13370,6 @@ function segmentsOf(path) {
13076
13370
  function conditionSetting(condition) {
13077
13371
  return "path" in condition ? condition.path : condition.property;
13078
13372
  }
13079
- function conditionRoot(condition) {
13080
- const path = parseConfigurationPath(conditionSetting(condition));
13081
- return path === null ? conditionSetting(condition) : rootSegment(path);
13082
- }
13083
13373
  function conditionHolds(condition, configuration) {
13084
13374
  const path = parseConfigurationPath(conditionSetting(condition));
13085
13375
  if (path === null)
@@ -13087,6 +13377,28 @@ function conditionHolds(condition, configuration) {
13087
13377
  const matches2 = (held) => typeof held === "string" && condition.anyOf.includes(held);
13088
13378
  return "anyRow" in condition && condition.anyRow === true ? valuesAt(configuration, path).some(matches2) : matches2(valueAt(configuration, path));
13089
13379
  }
13380
+ function conditionOf(property) {
13381
+ return property?.["x-required-when"];
13382
+ }
13383
+ function conditionCarriers(schema) {
13384
+ const settings = configurationLeaves(schema).flatMap((leaf) => {
13385
+ const condition = conditionOf(leaf.property);
13386
+ if (condition === void 0 || underList(leaf))
13387
+ return [];
13388
+ const { path, name, containers, property } = leaf;
13389
+ return [{ kind: "leaf", path, name, containers, property, condition }];
13390
+ });
13391
+ const lists = configurationContainers(schema).flatMap((container) => {
13392
+ const condition = conditionOf(container.property);
13393
+ if (condition === void 0 || container.kind !== "array" || container.containers.length > 0)
13394
+ return [];
13395
+ const { path, name, containers, property } = container;
13396
+ return [{ kind: "container", path, name, containers, property, condition }];
13397
+ });
13398
+ const roots = Object.keys(schema.properties);
13399
+ const rootIndex = (carrier) => roots.indexOf(carrier.containers[0]?.name ?? carrier.name);
13400
+ return [...settings, ...lists].sort((a, b) => rootIndex(a) - rootIndex(b));
13401
+ }
13090
13402
 
13091
13403
  // ../product-configuration/dist/customer-configuration-input.js
13092
13404
  var ABSOLUTE_MAX_STRING = MAX_DECLARABLE_STRING_LENGTH;
@@ -13096,6 +13408,7 @@ function isPlainObject2(value) {
13096
13408
  return typeof value === "object" && value !== null && !Array.isArray(value);
13097
13409
  }
13098
13410
  function report(walk, field, message) {
13411
+ walk.reported.add(field);
13099
13412
  if (walk.issues.length >= MAX_CONFIGURATION_ISSUES) {
13100
13413
  walk.suppressed += 1;
13101
13414
  return;
@@ -13111,7 +13424,10 @@ function validateSubmittedConfiguration(schema, submitted, presence = {}) {
13111
13424
  issues: [],
13112
13425
  suppressed: 0,
13113
13426
  leaves: 0,
13114
- referenced: new Set(presence.referenced ?? [])
13427
+ referenced: new Set(presence.referenced ?? []),
13428
+ present: new Set(presence.present ?? []),
13429
+ unknown: new Set(presence.unknown ?? []),
13430
+ reported: /* @__PURE__ */ new Set()
13115
13431
  };
13116
13432
  const supplied = /* @__PURE__ */ new Set();
13117
13433
  if (submitted !== void 0 && submitted !== null && !isPlainObject2(submitted)) {
@@ -13124,7 +13440,7 @@ function validateSubmittedConfiguration(schema, submitted, presence = {}) {
13124
13440
  const input = isPlainObject2(submitted) ? submitted : {};
13125
13441
  const secrets = {};
13126
13442
  const configuration = validateObject({ properties: schema.properties, required: schema.required }, input, null, walk, { secrets, supplied });
13127
- drain(walk, crossFieldIssues(schema, supplied, presence, configuration));
13443
+ drain(walk, crossFieldIssues(schema, supplied, presence, configuration, walk.reported));
13128
13444
  drain(walk, rowAndReferenceIssues(schema, presence, configuration));
13129
13445
  if (walk.suppressed > 0) {
13130
13446
  walk.issues.push({
@@ -13149,13 +13465,17 @@ function declaresContainer(schema) {
13149
13465
  return Object.values(schema.properties).some((property) => isConfigurationContainer(property));
13150
13466
  }
13151
13467
  function containsReferenced(walk, container) {
13152
- if (walk.referenced.size === 0)
13153
- return false;
13468
+ return containsAny(walk.referenced, container);
13469
+ }
13470
+ function referencedAtOrInside(walk, path) {
13471
+ return walk.referenced.has(path) || containsReferenced(walk, path);
13472
+ }
13473
+ function containsAny(candidates, container) {
13154
13474
  const parsedOuter = parseConfigurationPath(container);
13155
13475
  if (parsedOuter === null)
13156
13476
  return false;
13157
13477
  const outer = pathSegments(parsedOuter);
13158
- for (const candidate of walk.referenced) {
13478
+ for (const candidate of candidates) {
13159
13479
  const parsed = parseConfigurationPath(candidate);
13160
13480
  if (parsed === null)
13161
13481
  continue;
@@ -13167,7 +13487,7 @@ function containsReferenced(walk, container) {
13167
13487
  }
13168
13488
  return false;
13169
13489
  }
13170
- function validateObject(declaration, input, container, walk, root) {
13490
+ function validateObject(declaration, input, container, walk, sink) {
13171
13491
  const required = new Set(declaration.required ?? []);
13172
13492
  const out = {};
13173
13493
  const fieldFor = (name) => {
@@ -13185,11 +13505,13 @@ function validateObject(declaration, input, container, walk, root) {
13185
13505
  const field = fieldFor(name);
13186
13506
  const children = containerDeclaration(property);
13187
13507
  if (children !== null) {
13188
- const value = input[name];
13189
- const filled = validateContainer(name, property, children, value, field, walk);
13508
+ const value = Object.hasOwn(input, name) ? input[name] : void 0;
13509
+ const filled = validateContainer(name, property, children, value, field, walk, sink);
13510
+ if (value === void 0)
13511
+ absentGroupRuleIssues(children, field, walk, sink, required.has(name));
13190
13512
  if (filled !== void 0)
13191
13513
  out[name] = filled;
13192
- else if (required.has(name) && !containsReferenced(walk, field))
13514
+ else if (required.has(name) && !((containsReferenced(walk, field) || containsAny(walk.present, field) || sink !== null && containsAny(Object.keys(sink.secrets), field)) && (value !== void 0 || absentGroupAnswered(children, field, walk))))
13193
13515
  report(walk, field, "This setting is required.");
13194
13516
  continue;
13195
13517
  }
@@ -13204,9 +13526,13 @@ function validateObject(declaration, input, container, walk, root) {
13204
13526
  const raw = input[name];
13205
13527
  const blank = typeof raw === "string" && raw.trim().length === 0;
13206
13528
  const present = Object.hasOwn(input, name) && raw !== void 0 && !blank;
13207
- const secret = root !== null && isSecretProperty(leaf);
13208
- if (present && root !== null && container === null)
13209
- root.supplied.add(name);
13529
+ const secret = isSecretProperty(leaf);
13530
+ if (secret && sink === null) {
13531
+ report(walk, field, "This Product declares a secret inside a list, which this contract format does not support.");
13532
+ continue;
13533
+ }
13534
+ if (present && sink !== null && field !== "")
13535
+ sink.supplied.add(field);
13210
13536
  if (!present) {
13211
13537
  const fallback = secret ? void 0 : defaultOf(leaf);
13212
13538
  if (fallback !== void 0) {
@@ -13225,27 +13551,96 @@ function validateObject(declaration, input, container, walk, root) {
13225
13551
  continue;
13226
13552
  }
13227
13553
  if (secret) {
13228
- if (typeof checked === "string" && checked.length > 0)
13229
- root.secrets[name] = checked;
13554
+ if (typeof checked === "string" && checked.length > 0 && field !== "")
13555
+ sink.secrets[field] = checked;
13230
13556
  else if (required.has(name))
13231
13557
  report(walk, field, "This setting is required.");
13232
13558
  continue;
13233
13559
  }
13234
13560
  out[name] = checked;
13235
13561
  }
13236
- if (declaration.dependentRequired !== void 0) {
13237
- const suppliedHere = new Set(Object.keys(out));
13238
- for (const [trigger, dependents] of Object.entries(declaration.dependentRequired)) {
13239
- if (!suppliedHere.has(trigger) && !containsReferenced(walk, fieldFor(trigger)))
13562
+ groupRuleIssues(declaration, new Set(Object.keys(out)), container, walk, sink);
13563
+ return out;
13564
+ }
13565
+ function groupRuleIssues(declaration, suppliedHere, container, walk, sink) {
13566
+ if (declaration.dependentRequired === void 0)
13567
+ return;
13568
+ const fieldFor = (name) => (container === null ? configurationPath(name) : configurationPath(...pathSegments(container), name)) ?? "";
13569
+ const answered = (name) => {
13570
+ const path = fieldFor(name);
13571
+ return suppliedHere.has(name) || // A MEMBER, NOT A CONTAINER: a referenced leaf is recorded at this very
13572
+ // path, and a member group is answered by a reference inside it. Asking
13573
+ // only "inside" never counted a referenced leaf, so a referenced trigger
13574
+ // turned nothing on and a referenced dependent was reported missing.
13575
+ referencedAtOrInside(walk, path) || walk.present.has(path) || sink !== null && Object.hasOwn(sink.secrets, path);
13576
+ };
13577
+ const unseen = (name) => walk.unknown.has(fieldFor(name));
13578
+ const reported = /* @__PURE__ */ new Set();
13579
+ for (const [trigger, dependents] of Object.entries(declaration.dependentRequired)) {
13580
+ if (unseen(trigger) || !answered(trigger))
13581
+ continue;
13582
+ for (const dependent of dependents) {
13583
+ if (unseen(dependent) || answered(dependent) || reported.has(dependent))
13240
13584
  continue;
13241
- for (const dependent of dependents) {
13242
- if (suppliedHere.has(dependent) || containsReferenced(walk, fieldFor(dependent)))
13243
- continue;
13244
- report(walk, fieldFor(dependent), `This setting is required when ${labelOf(declaration, trigger)} is configured.`);
13245
- }
13585
+ reported.add(dependent);
13586
+ report(walk, fieldFor(dependent), `This setting is required when ${labelOf(declaration, trigger)} is configured.`);
13246
13587
  }
13247
13588
  }
13248
- return out;
13589
+ }
13590
+ function absentGroupRuleIssues(children, field, walk, sink, coveredByParent) {
13591
+ if (children.kind !== "object" || sink === null)
13592
+ return;
13593
+ const path = parseConfigurationPath(field);
13594
+ if (path === null)
13595
+ return;
13596
+ groupRuleIssues(children, /* @__PURE__ */ new Set(), path, walk, sink);
13597
+ const configured = containsReferenced(walk, field) || containsAny(walk.present, field);
13598
+ const required = new Set(children.required ?? []);
13599
+ for (const [name, property] of Object.entries(children.properties)) {
13600
+ const memberField = configurationPath(...pathSegments(path), name);
13601
+ if (memberField === null)
13602
+ continue;
13603
+ const demanded = configured && !coveredByParent && required.has(name) && !walk.unknown.has(memberField);
13604
+ const inner = containerDeclaration(property);
13605
+ if (inner === null) {
13606
+ if (!demanded || walk.referenced.has(memberField) || walk.present.has(memberField))
13607
+ continue;
13608
+ if (!isSecretProperty(property) && defaultOf(property) !== void 0)
13609
+ continue;
13610
+ report(walk, memberField, "This setting is required.");
13611
+ continue;
13612
+ }
13613
+ if (demanded && inner.kind === "object" && !(containsReferenced(walk, memberField) || containsAny(walk.present, memberField))) {
13614
+ report(walk, memberField, "This setting is required.");
13615
+ continue;
13616
+ }
13617
+ absentGroupRuleIssues(inner, memberField, walk, sink, coveredByParent && required.has(name));
13618
+ }
13619
+ }
13620
+ function absentGroupAnswered(children, field, walk) {
13621
+ if (children.kind !== "object")
13622
+ return true;
13623
+ const parent = parseConfigurationPath(field);
13624
+ if (parent === null)
13625
+ return false;
13626
+ for (const name of children.required ?? []) {
13627
+ const property = children.properties[name];
13628
+ const path = configurationPath(...pathSegments(parent), name);
13629
+ if (property === void 0 || path === null)
13630
+ return false;
13631
+ const inner = containerDeclaration(property);
13632
+ if (inner !== null) {
13633
+ if (!(containsReferenced(walk, path) || containsAny(walk.present, path)) || !absentGroupAnswered(inner, path, walk))
13634
+ return false;
13635
+ continue;
13636
+ }
13637
+ if (walk.referenced.has(path) || walk.present.has(path))
13638
+ continue;
13639
+ if (!isSecretProperty(property) && defaultOf(property) !== void 0)
13640
+ continue;
13641
+ return false;
13642
+ }
13643
+ return true;
13249
13644
  }
13250
13645
  function containerDeclaration(property) {
13251
13646
  if (property.type === "object" && property.properties !== void 0) {
@@ -13275,7 +13670,7 @@ function containerDeclaration(property) {
13275
13670
  }
13276
13671
  return null;
13277
13672
  }
13278
- function validateContainer(name, property, children, value, field, walk) {
13673
+ function validateContainer(name, property, children, value, field, walk, sink) {
13279
13674
  const path = parseConfigurationPath(field);
13280
13675
  if (children.kind === "object") {
13281
13676
  if (value === void 0)
@@ -13284,7 +13679,7 @@ function validateContainer(name, property, children, value, field, walk) {
13284
13679
  report(walk, field, "Expected a group of settings.");
13285
13680
  return void 0;
13286
13681
  }
13287
- const filled = validateObject(children, value, path, walk, null);
13682
+ const filled = validateObject(children, value, path, walk, sink);
13288
13683
  return Object.keys(filled).length === 0 ? void 0 : filled;
13289
13684
  }
13290
13685
  if (value === void 0)
@@ -13323,10 +13718,10 @@ function validateContainer(name, property, children, value, field, walk) {
13323
13718
  }
13324
13719
  return out;
13325
13720
  }
13326
- function crossFieldIssues(schema, supplied, presence, configuration = {}) {
13721
+ function crossFieldIssues(schema, supplied, presence, configuration, reportedByWalk) {
13327
13722
  const clauses = Object.entries(schema.dependentRequired ?? {});
13328
- const conditional = Object.entries(schema.properties).filter(([, property]) => conditionOf(property) !== void 0);
13329
- if (clauses.length === 0 && conditional.length === 0)
13723
+ const carriers = conditionCarriers(schema);
13724
+ if (clauses.length === 0 && carriers.length === 0)
13330
13725
  return [];
13331
13726
  const alsoPresent = /* @__PURE__ */ new Set([...presence.present ?? [], ...presence.referenced ?? []]);
13332
13727
  const unknown = new Set(presence.unknown ?? []);
@@ -13349,22 +13744,28 @@ function crossFieldIssues(schema, supplied, presence, configuration = {}) {
13349
13744
  });
13350
13745
  }
13351
13746
  }
13352
- for (const [name, property] of conditional) {
13353
- const condition = conditionOf(property);
13354
- if (condition === void 0 || !known(name) || held(name))
13747
+ for (const { path, condition } of carriers) {
13748
+ if (!known(path) || held(path))
13355
13749
  continue;
13356
- if (!conditionHolds(condition, configuration))
13750
+ const target = parseConfigurationPath(conditionSetting(condition));
13751
+ if (target === null || !conditionHolds(condition, configuration))
13357
13752
  continue;
13358
- if (reported.has(name))
13753
+ if (reported.has(path) || reportedByWalk.has(path))
13359
13754
  continue;
13360
- reported.add(name);
13755
+ reported.add(path);
13361
13756
  issues.push({
13362
- field: name,
13757
+ field: path,
13363
13758
  // THE SETTING THAT MADE IT NECESSARY, never the value it holds. A message
13364
13759
  // naming the value would be fine here and wrong one refactor later, when
13365
13760
  // somebody conditions a secret on another secret — which the publish rules
13366
13761
  // refuse precisely because nothing may read one.
13367
- message: `This setting is required because of what ${labelOf(schema, conditionRoot(condition))} is set to.`
13762
+ //
13763
+ // THE SETTING THE CONDITION READS, not the root it descends from. The two
13764
+ // were one string while a condition could only name a root setting or a
13765
+ // row of a root list; naming a setting in a group by its root would tell
13766
+ // the customer to look at a whole section. A root scalar reads exactly as
13767
+ // it always did, and a row's setting now names its list beside it.
13768
+ message: `This setting is required because of what ${labelOfPath(schema, target)} is set to.`
13368
13769
  });
13369
13770
  }
13370
13771
  return issues;
@@ -13469,9 +13870,6 @@ function labelOfChild(container, name) {
13469
13870
  const title = items?.properties?.[name]?.title;
13470
13871
  return title === void 0 ? `"${name}"` : `"${title}"`;
13471
13872
  }
13472
- function conditionOf(property) {
13473
- return property?.["x-required-when"];
13474
- }
13475
13873
  function labelOf(schema, name) {
13476
13874
  const property = schema.properties[name];
13477
13875
  return property?.title === void 0 ? `"${name}"` : `"${property.title}"`;
@@ -13516,9 +13914,6 @@ function withoutPropertyRules(properties) {
13516
13914
  }
13517
13915
  return out;
13518
13916
  }
13519
- function declaredSecretNames(schema) {
13520
- return Object.entries(schema.properties).filter(([, property]) => isSecretProperty(property)).map(([name]) => name).sort();
13521
- }
13522
13917
  function defaultOf(property) {
13523
13918
  switch (property.type) {
13524
13919
  case "string":
@@ -13675,23 +14070,52 @@ function seedConfiguration(configuration) {
13675
14070
  const required = new Set(configuration?.required ?? []);
13676
14071
  const open = [];
13677
14072
  const requiredSecrets = [];
13678
- const groups = Object.entries(configuration?.dependentRequired ?? {}).map(([trigger, dependents]) => {
14073
+ const groups = [];
14074
+ collectGroups(configuration ?? {}, [], groups);
14075
+ const values = seedProperties(properties, required, [], open, requiredSecrets);
14076
+ return { values, open, requiredSecrets, groups };
14077
+ }
14078
+ function collectGroups(node, ancestors, groups) {
14079
+ const properties = node.properties ?? {};
14080
+ const pathOf = (name) => ancestors.length === 0 ? name : configurationPath(...ancestors, name) ?? name;
14081
+ const labelOf2 = (name) => {
14082
+ const path = configurationPath(...ancestors, name);
14083
+ return path === null ? name : printableConfigurationPath(path);
14084
+ };
14085
+ const container = ancestors.length === 0 ? null : configurationPath(...ancestors);
14086
+ const within = container === null ? null : printableConfigurationPath(container);
14087
+ const clauses = Object.entries(node.dependentRequired ?? {}).map(([trigger, dependents]) => {
13679
14088
  const members = [trigger, ...dependents];
14089
+ const settings = members.filter((name) => !isSecret(properties[name] ?? {}));
13680
14090
  return {
13681
14091
  trigger,
13682
- settings: members.filter((name) => !isSecret(properties[name] ?? {})),
13683
- secrets: members.filter((name) => isSecret(properties[name] ?? {}))
14092
+ group: {
14093
+ trigger: pathOf(trigger),
14094
+ settings: settings.map(pathOf),
14095
+ labels: settings.map(labelOf2),
14096
+ secrets: members.filter((name) => isSecret(properties[name] ?? {})).map(pathOf),
14097
+ within
14098
+ }
13684
14099
  };
13685
14100
  }).sort((a, b) => a.trigger.localeCompare(b.trigger));
13686
- const values = seedProperties(properties, required, [], open, requiredSecrets);
13687
- return { values, open, requiredSecrets, groups };
14101
+ groups.push(...clauses.map((clause) => clause.group));
14102
+ for (const [name, declaration] of Object.entries(properties)) {
14103
+ if (isSecret(declaration))
14104
+ continue;
14105
+ const children = containerChildren2(declaration);
14106
+ if (children === null)
14107
+ continue;
14108
+ collectGroups(children, [...ancestors, name], groups);
14109
+ }
13688
14110
  }
13689
14111
  function seedProperties(properties, required, ancestors, open, requiredSecrets) {
13690
14112
  const values = {};
13691
14113
  for (const [name, declaration] of Object.entries(properties)) {
13692
- if (requiredSecrets !== null && isSecret(declaration)) {
13693
- if (required.has(name))
13694
- requiredSecrets.push(name);
14114
+ if (isSecret(declaration)) {
14115
+ const path = ancestors.length === 0 ? name : configurationPath(...ancestors, name);
14116
+ if (requiredSecrets !== null && required.has(name) && path !== null) {
14117
+ requiredSecrets.push(path);
14118
+ }
13695
14119
  continue;
13696
14120
  }
13697
14121
  const children = containerChildren2(declaration);
@@ -13701,7 +14125,7 @@ function seedProperties(properties, required, ancestors, open, requiredSecrets)
13701
14125
  const childRequired = new Set(children.required);
13702
14126
  const inner = [...ancestors, name];
13703
14127
  if (children.kind === "object") {
13704
- values[name] = seedProperties(children.properties, childRequired, inner, open, null);
14128
+ values[name] = seedProperties(children.properties, childRequired, inner, open, requiredSecrets);
13705
14129
  continue;
13706
14130
  }
13707
14131
  const count = children.minItems ?? 0;
@@ -13732,7 +14156,8 @@ function containerChildren2(declaration) {
13732
14156
  return {
13733
14157
  kind: "object",
13734
14158
  properties: node.properties,
13735
- required: node.required ?? []
14159
+ required: node.required ?? [],
14160
+ ...dependentRequiredOf(node.dependentRequired)
13736
14161
  };
13737
14162
  }
13738
14163
  if (node.type === "array" && node.items !== void 0) {
@@ -13743,11 +14168,15 @@ function containerChildren2(declaration) {
13743
14168
  kind: "array",
13744
14169
  properties: items.properties,
13745
14170
  required: items.required ?? [],
14171
+ ...dependentRequiredOf(items.dependentRequired),
13746
14172
  ...typeof node.minItems === "number" ? { minItems: node.minItems } : {}
13747
14173
  };
13748
14174
  }
13749
14175
  return null;
13750
14176
  }
14177
+ function dependentRequiredOf(raw) {
14178
+ return typeof raw === "object" && raw !== null && !Array.isArray(raw) ? { dependentRequired: raw } : {};
14179
+ }
13751
14180
  function dedupeOpen(open) {
13752
14181
  const seen = /* @__PURE__ */ new Set();
13753
14182
  const unique = open.filter((entry) => {
@@ -13779,6 +14208,7 @@ function exampleResourceKey(name) {
13779
14208
  const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^[^a-z]+/, "").replace(/-+$/, "").slice(0, 63);
13780
14209
  return RESOURCE_KEY_PATTERN.test(slug) ? slug : "";
13781
14210
  }
14211
+ var DEPLOYMENT_EXAMPLE_GUARANTEE = "Product-owned configuration is complete and E2E-proven; tenant-specific secrets and Resource/provider selections are supplied by the tenant.";
13782
14212
 
13783
14213
  // ../platform-schemas/dist/index.js
13784
14214
  var TENANT_KEY_REGEX = /^[a-z][a-z0-9-]{2,23}[a-z0-9]$/;
@@ -13921,6 +14351,53 @@ function isBlockedHost(hostname) {
13921
14351
  return BLOCKED_HOSTS.some((pattern) => pattern.test(hostname));
13922
14352
  }
13923
14353
 
14354
+ // ../product-contracts/dist/deployment-example.schema.js
14355
+ var DeploymentExampleSchema = external_exports.object({
14356
+ /** The heading a customer reads above the example. */
14357
+ title: external_exports.string().min(3).max(80),
14358
+ /** What this deployment gives the customer, in a sentence or two. */
14359
+ description: external_exports.string().min(20).max(400),
14360
+ /**
14361
+ * A PLAIN STRING, for the reason the Product Instance Template's is: an
14362
+ * unsupported value then reaches the check that names the regions THIS
14363
+ * Product offers, rather than a zod enum error naming the platform's.
14364
+ */
14365
+ region: external_exports.string().min(1).max(32),
14366
+ /** Plain string, same reasoning as `region`. */
14367
+ profile: external_exports.string().min(1).max(40),
14368
+ /**
14369
+ * The Product's own settings, in the shape `spec.configuration` takes —
14370
+ * nested for a v2 Product. Validated at publish against this release's
14371
+ * configuration schema by the same function a customer's manifest meets.
14372
+ */
14373
+ configuration: external_exports.record(external_exports.unknown()),
14374
+ /**
14375
+ * The secret settings a deployment of this example supplies, BY NAME.
14376
+ *
14377
+ * Every secret the example's configuration makes required must be named
14378
+ * here, so a reader knows every `--secret` flag the create needs before
14379
+ * running it. The values are the tenant's — sent when the Resource is
14380
+ * created and again only to set a new one — and never written into a
14381
+ * manifest. A secret resent on an unchanged re-apply is written again.
14382
+ */
14383
+ secretInputs: external_exports.array(external_exports.string().regex(CONFIGURATION_PROPERTY_NAME_REGEX)).max(MAX_CONFIGURATION_CHILDREN).optional()
14384
+ }).strict().describe("The recommended deployment for this release. Required for a NEW release; optional in the schema because releases published before the field existed are immutable. " + DEPLOYMENT_EXAMPLE_GUARANTEE + " Secret settings are named in secretInputs and never given a value here.");
14385
+ var SECRET_INPUT_SEGMENT = CONFIGURATION_PROPERTY_NAME_REGEX.source.slice(1, -1);
14386
+ var SecretInputV2Schema = external_exports.string().regex(new RegExp(`^(?:${SECRET_INPUT_SEGMENT}|(?:/${SECRET_INPUT_SEGMENT}){2,${MAX_CONFIGURATION_DEPTH}})$`)).refine((raw) => secretPathOf(raw) !== null, {
14387
+ message: 'A secret input is a root setting name, or a configuration path such as "/group/setting" with no list index.'
14388
+ });
14389
+ var DeploymentExampleV2Schema = DeploymentExampleSchema.extend({
14390
+ /**
14391
+ * The secret settings a deployment of this example supplies, BY CANONICAL
14392
+ * PATH — a bare name at the root, a pointer inside a group.
14393
+ *
14394
+ * Every secret the example's configuration makes required must be named
14395
+ * here, so a reader knows every `--secret` flag the create needs before
14396
+ * running it. The values are the tenant's and never written into a manifest.
14397
+ */
14398
+ secretInputs: external_exports.array(SecretInputV2Schema).max(MAX_CONFIGURATION_CHILDREN).optional()
14399
+ });
14400
+
13924
14401
  // ../product-contracts/dist/portfolio.js
13925
14402
  var PORTFOLIO_CATEGORY_IDS = [
13926
14403
  "clients",
@@ -14507,6 +14984,15 @@ var ProductDefinitionObjectSchema = external_exports.object({
14507
14984
  // --- Customer choices -------------------------------------------------
14508
14985
  profiles: external_exports.array(ServiceProfileSchema).min(1).max(SERVICE_PROFILE_IDS.length),
14509
14986
  configuration: CustomerConfigurationSchema,
14987
+ /**
14988
+ * The publisher's recommended deployment of this release. See
14989
+ * `deployment-example.schema.ts`.
14990
+ *
14991
+ * `.optional()`, NEVER `.default(...)`, for the digest reason `conditions`
14992
+ * gives below — and required for a NEW release by `publish-rules.ts`, which
14993
+ * is the only place a rule added after releases were frozen may live.
14994
+ */
14995
+ deploymentExample: DeploymentExampleSchema.optional(),
14510
14996
  // --- Results, relationships, operations -------------------------------
14511
14997
  outputs: external_exports.array(ProductOutputSchema).min(1).max(MAX_PRODUCT_OUTPUTS),
14512
14998
  dependencies: external_exports.array(ProductDependencySchema).max(8),
@@ -14543,7 +15029,10 @@ var ProductDefinitionSchema = ProductDefinitionObjectSchema.superRefine(checkPro
14543
15029
  var ProductDefinitionV2ObjectSchema = ProductDefinitionObjectSchema.extend({
14544
15030
  apiVersion: ProductApiVersionV2Schema,
14545
15031
  configuration: CustomerConfigurationV2Schema,
14546
- dependencies: external_exports.array(ProductDependencyV2Schema).max(8)
15032
+ dependencies: external_exports.array(ProductDependencyV2Schema).max(8),
15033
+ // A v2 secret may sit inside a group, so the example names it by PATH. The v1
15034
+ // example keeps its root-name rule, and the v1 artifact its bytes.
15035
+ deploymentExample: DeploymentExampleV2Schema.optional()
14547
15036
  });
14548
15037
  var ProductDefinitionV2Schema = ProductDefinitionV2ObjectSchema.superRefine(checkProductDefinition);
14549
15038
  function checkProductDefinition(definition, ctx) {
@@ -14720,12 +15209,13 @@ function checkProductDefinition(definition, ctx) {
14720
15209
  });
14721
15210
  }
14722
15211
  });
14723
- for (const [name, property] of Object.entries(definition.configuration.properties ?? {})) {
14724
- const condition = property["x-required-when"];
14725
- if (condition === void 0)
14726
- continue;
14727
- const at = ["configuration", "properties", name, "x-required-when"];
14728
- if ((definition.configuration.required ?? []).includes(name)) {
15212
+ for (const carrier of conditionCarriers(definition.configuration)) {
15213
+ const { condition } = carrier;
15214
+ const name = printableConfigurationPath(carrier.path);
15215
+ const named = conditionSetting(condition);
15216
+ const anyRow = "anyRow" in condition && condition.anyRow === true;
15217
+ const at = [...declarationIssuePath(carrier), "x-required-when"];
15218
+ if (requiredToRoot(definition.configuration, carrier)) {
14729
15219
  ctx.addIssue({
14730
15220
  code: external_exports.ZodIssueCode.custom,
14731
15221
  path: [...at],
@@ -14733,7 +15223,7 @@ function checkProductDefinition(definition, ctx) {
14733
15223
  });
14734
15224
  continue;
14735
15225
  }
14736
- if (declaredDefault(property) !== void 0) {
15226
+ if (declaredDefault(carrier.property) !== void 0) {
14737
15227
  ctx.addIssue({
14738
15228
  code: external_exports.ZodIssueCode.custom,
14739
15229
  path: [...at],
@@ -14748,7 +15238,7 @@ function checkProductDefinition(definition, ctx) {
14748
15238
  message: "Repeated values in `anyOf`."
14749
15239
  });
14750
15240
  }
14751
- if (condition.path === name) {
15241
+ if (named === carrier.path) {
14752
15242
  ctx.addIssue({
14753
15243
  code: external_exports.ZodIssueCode.custom,
14754
15244
  path: [...at, "path"],
@@ -14756,30 +15246,30 @@ function checkProductDefinition(definition, ctx) {
14756
15246
  });
14757
15247
  continue;
14758
15248
  }
14759
- const parsed = parseConfigurationPath(condition.path);
15249
+ const parsed = parseConfigurationPath(named);
14760
15250
  const leaf = parsed === null ? null : leafAt(definition.configuration, parsed);
14761
15251
  if (leaf === null) {
14762
15252
  const isGroup = parsed !== null && declaresPath(definition.configuration, parsed);
14763
15253
  ctx.addIssue({
14764
15254
  code: external_exports.ZodIssueCode.custom,
14765
15255
  path: [...at, "path"],
14766
- message: isGroup ? `"${condition.path}" is a group of settings, which holds no single value to compare. Name one setting inside it.` : `"${condition.path}" is not a declared customer configuration property.`
15256
+ message: isGroup ? `"${named}" is a group of settings, which holds no single value to compare. Name one setting inside it.` : `"${named}" is not a declared customer configuration property.`
14767
15257
  });
14768
15258
  continue;
14769
15259
  }
14770
- if (underList(leaf) && condition.anyRow !== true) {
15260
+ if (underList(leaf) && !anyRow) {
14771
15261
  ctx.addIssue({
14772
15262
  code: external_exports.ZodIssueCode.custom,
14773
15263
  path: [...at, "path"],
14774
- message: `"${condition.path}" is inside a list, so it names one value per item rather than one value. Add "anyRow": true to require the secret when ANY row chose one of these values, or name a setting the instance holds exactly once.`
15264
+ message: `"${named}" is inside a list, so it names one value per item rather than one value. Add "anyRow": true to require the secret when ANY row chose one of these values, or name a setting the instance holds exactly once.`
14775
15265
  });
14776
15266
  continue;
14777
15267
  }
14778
- if (!underList(leaf) && condition.anyRow === true) {
15268
+ if (!underList(leaf) && anyRow) {
14779
15269
  ctx.addIssue({
14780
15270
  code: external_exports.ZodIssueCode.custom,
14781
15271
  path: [...at, "anyRow"],
14782
- message: `"${condition.path}" is not inside a list, so there are no rows to quantify over.`
15272
+ message: `"${named}" is not inside a list, so there are no rows to quantify over.`
14783
15273
  });
14784
15274
  }
14785
15275
  const target = leaf.property;
@@ -14787,7 +15277,7 @@ function checkProductDefinition(definition, ctx) {
14787
15277
  ctx.addIssue({
14788
15278
  code: external_exports.ZodIssueCode.custom,
14789
15279
  path: [...at, "path"],
14790
- message: `"${condition.path}" is a ${target.type} setting; a condition compares string values.`
15280
+ message: `"${named}" is a ${target.type} setting; a condition compares string values.`
14791
15281
  });
14792
15282
  continue;
14793
15283
  }
@@ -14795,21 +15285,16 @@ function checkProductDefinition(definition, ctx) {
14795
15285
  ctx.addIssue({
14796
15286
  code: external_exports.ZodIssueCode.custom,
14797
15287
  path: [...at, "path"],
14798
- message: `"${condition.path}" is a secret. The platform never reads a secret's value, so a condition on one could never be evaluated.`
15288
+ message: `"${named}" is a secret. The platform never reads a secret's value, so a condition on one could never be evaluated.`
14799
15289
  });
14800
15290
  continue;
14801
15291
  }
14802
- const single = withoutCrossFieldRules({
14803
- ...definition.configuration,
14804
- properties: { [leaf.name]: target },
14805
- required: [leaf.name]
14806
- });
14807
- const outside = condition.anyOf.filter((value) => !validateSubmittedConfiguration(single, { [leaf.name]: value }).ok);
15292
+ const outside = valuesOutsideSpace(definition.configuration, leaf, condition.anyOf);
14808
15293
  if (outside.length > 0) {
14809
15294
  ctx.addIssue({
14810
15295
  code: external_exports.ZodIssueCode.custom,
14811
15296
  path: [...at, "anyOf"],
14812
- message: `"${condition.path}" never accepts ${outside.map((v) => `"${v}"`).join(", ")}, so this condition could never make the secret required.`
15297
+ message: `"${named}" never accepts ${outside.map((v) => `"${v}"`).join(", ")}, so this condition could never make the secret required.`
14813
15298
  });
14814
15299
  }
14815
15300
  }
@@ -15120,27 +15605,246 @@ function checkRowRules(configuration, ctx) {
15120
15605
  message: `"${rule.property}" never accepts ${outside.map((value) => `"${value}"`).join(", ")}, so this rule could never count a row.`
15121
15606
  });
15122
15607
  }
15123
- });
15608
+ });
15609
+ }
15610
+ }
15611
+
15612
+ // ../product-contracts/dist/announcements.js
15613
+ var ANNOUNCED_PRODUCTS = Object.freeze([
15614
+ {
15615
+ productId: "hosted-models",
15616
+ displayName: "Hosted Models",
15617
+ summary: "Build, customize and deploy your own AI models",
15618
+ category: "ai-models",
15619
+ order: 0
15620
+ },
15621
+ {
15622
+ productId: "vern",
15623
+ displayName: "Vern",
15624
+ summary: "Protect sensitive data in AI workloads",
15625
+ category: "data-security",
15626
+ order: 0
15627
+ }
15628
+ ]);
15629
+
15630
+ // ../product-contracts/dist/contract-issue.js
15631
+ var PATH_SEGMENT = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/;
15632
+ var UNPRINTABLE_SEGMENT = "<unprintable>";
15633
+ var MAX_SEGMENTS = MAX_CONFIGURATION_PATH_SEGMENTS + 2;
15634
+ function printableContractPath(path) {
15635
+ const printable = path.slice(0, MAX_SEGMENTS).map((segment) => typeof segment === "number" || PATH_SEGMENT.test(segment) ? String(segment) : UNPRINTABLE_SEGMENT);
15636
+ return printable.join(".") || "(document)";
15637
+ }
15638
+ function manifestConfigurationField(field) {
15639
+ if (field === "")
15640
+ return "spec.configuration";
15641
+ const parsed = parseConfigurationPath(field);
15642
+ return printableContractPath([
15643
+ "spec",
15644
+ "configuration",
15645
+ ...parsed === null ? [field] : pathSegments(parsed)
15646
+ ]);
15647
+ }
15648
+
15649
+ // ../product-contracts/dist/resource-output-reference.js
15650
+ var OUTPUT_KEY_RULE = "Output keys are camelCase.";
15651
+ var ResourceOutputReferenceSchema = external_exports.object({
15652
+ valueFrom: external_exports.object({
15653
+ resourceOutput: external_exports.object({
15654
+ /** The producing Resource's `metadata.key`, in the same Project. */
15655
+ resourceKey: ResourceKeySchema,
15656
+ /** The key of an output that Resource's Product declares. */
15657
+ output: external_exports.string().regex(/^[a-z][a-zA-Z0-9]{1,47}$/, OUTPUT_KEY_RULE)
15658
+ }).strict()
15659
+ }).strict()
15660
+ }).strict();
15661
+ function looksLikeReference(value) {
15662
+ return typeof value === "object" && value !== null && !Array.isArray(value) && "valueFrom" in value;
15663
+ }
15664
+
15665
+ // ../product-contracts/dist/dependency-binding.js
15666
+ var DependencyBindingSelectorSchema = external_exports.object({
15667
+ /** The providing Resource's `metadata.key`. */
15668
+ resourceKey: ResourceKeySchema,
15669
+ /**
15670
+ * The Project that Resource lives in. Absent means the consumer's own.
15671
+ *
15672
+ * PRESENT BECAUSE A BINDING IS TENANT-SCOPED AND A PROJECT IS NOT A RUNTIME
15673
+ * BOUNDARY: a provider anywhere in the Tenant can satisfy a consumer
15674
+ * anywhere in it, and that has been true since bindings existed. What the
15675
+ * field does NOT do is grant anything — the Control Plane opens that
15676
+ * Project from the caller's own assertion, and a Project the caller holds
15677
+ * nothing in answers exactly as a Project that does not exist.
15678
+ *
15679
+ * A plain bounded string, exactly as `spec.projectId` is: an id that does
15680
+ * not name a readable Project is refused where the refusal can be phrased
15681
+ * without leaking, which is not here.
15682
+ */
15683
+ projectId: external_exports.string().min(1).max(64).optional()
15684
+ }).strict();
15685
+ var DependencyBindingsSchema = external_exports.record(ServiceBindingKeySchema, DependencyBindingSelectorSchema);
15686
+
15687
+ // ../product-contracts/dist/product-instance-template.schema.js
15688
+ var ProductInstanceTemplateSchema = external_exports.object({
15689
+ apiVersion: ResourceApiVersionSchema,
15690
+ kind: kindSchema(PRODUCT_INSTANCE_KIND),
15691
+ metadata: external_exports.object({
15692
+ /**
15693
+ * The customer-facing Resource name — "Local service".
15694
+ *
15695
+ * Maps to `ProductInstance.displayName`, and deliberately introduces NO
15696
+ * new identity concept. `pri_…` remains the stable identity and
15697
+ * `instanceKey` remains the cloud resource key; adding a third name here
15698
+ * would mean a fourth thing to keep unique and a fifth to explain.
15699
+ * Bounded exactly as the create envelope bounds `displayName`.
15700
+ */
15701
+ // NOT `.trim()`: this schema is PUBLISHED as JSON Schema so a customer
15702
+ // can validate the file before sending it, and a trim is a transform
15703
+ // that JSON Schema cannot express. The value is trimmed when it becomes
15704
+ // a create command instead, so the published document and the platform
15705
+ // agree on what is valid.
15706
+ name: external_exports.string().min(1).max(80).regex(/\S/, "The resource name must contain a non-whitespace character. A name of only spaces trims to nothing, and an empty display name is not a name."),
15707
+ /**
15708
+ * THE STABLE IDENTITY OF THIS RESOURCE INSIDE ITS PROJECT — and the one
15709
+ * thing that makes applying a document twice different from creating
15710
+ * two resources.
15711
+ *
15712
+ * THIS IS THE FOURTH NAME, AND THE HEADER ABOVE ARGUES AGAINST ONE.
15713
+ * That argument stands and is answered rather than ignored: a
15714
+ * declarative apply is not expressible without a key the AUTHOR knows
15715
+ * before the platform has created anything, and nothing that already
15716
+ * exists is one.
15717
+ *
15718
+ * `metadata.name` mutable, not unique, and rule 1 above says it is
15719
+ * display copy rather than identity.
15720
+ * `pri_…` minted by the server. It does not exist when the
15721
+ * first apply is written, which is the only moment
15722
+ * that matters.
15723
+ * `instanceKey` derived from `pri_…` and then frozen — the same
15724
+ * problem one step removed.
15725
+ * idempotency key a de-duplication token that "names nothing,
15726
+ * grants nothing, and selects nothing". Making it
15727
+ * select a resource would falsify the security
15728
+ * argument the signed-request layer rests on.
15729
+ *
15730
+ * So it gets exactly ONE job: to say which resource in this Project the
15731
+ * document is about. It is NEVER authorization — the Project comes from
15732
+ * the request, the row is loaded inside it, and a key naming nothing
15733
+ * there is a not-found rather than a create. And it NEVER becomes a
15734
+ * provider resource name; `instanceKey` owns that, and coupling the two
15735
+ * would let a customer's chosen string reach Scaleway.
15736
+ *
15737
+ * OPTIONAL HERE, REQUIRED AT APPLY. The document is a published `v1`
15738
+ * contract and validating one on its own is a legitimate thing to do
15739
+ * with no Project in sight — the same asymmetry `resolveTemplate`
15740
+ * already has for `routeProjectId`. What refuses a document without a
15741
+ * key is the apply path, because a create that cannot be run again is
15742
+ * not deployment as code.
15743
+ *
15744
+ * Shaped like a DNS label: it becomes a database predicate and a log
15745
+ * field, and neither wants free text.
15746
+ */
15747
+ key: ResourceKeySchema.optional()
15748
+ }).strict(),
15749
+ spec: external_exports.object({
15750
+ productId: external_exports.string().min(1).max(64),
15751
+ /**
15752
+ * REQUIRED, unlike the create envelope's, which may be omitted to take
15753
+ * the newest deployable release.
15754
+ *
15755
+ * A template is a record of a decision, and "newest" is not one. An
15756
+ * optional version would mean the same document produced 1.0.0 today and
15757
+ * 1.1.0 after an unrelated promotion — so re-applying an exported
15758
+ * template would not reproduce what was exported, which is the single
15759
+ * property that makes templates worth having.
15760
+ */
15761
+ productVersion: external_exports.string().min(1).max(64),
15762
+ /** Round-trip only — see rule 3 above. Never authorization. */
15763
+ projectId: external_exports.string().min(1).max(64).optional(),
15764
+ region: external_exports.string().min(1).max(32),
15765
+ profile: external_exports.string().min(1).max(40),
15766
+ /**
15767
+ * The Product's own settings.
15768
+ *
15769
+ * An OBJECT with product-specific keys, not `unknown`: the published
15770
+ * JSON Schema has to say at least that much, and the converter refuses
15771
+ * `unknown` precisely so a published contract cannot silently widen.
15772
+ * The keys themselves are validated against the pinned Definition's
15773
+ * configuration schema by `resolveTemplate`, which is where the real
15774
+ * shape lives — it differs per Product, so it cannot be stated here.
15775
+ */
15776
+ configuration: external_exports.record(external_exports.unknown()).optional(),
15777
+ /**
15778
+ * Which Resource satisfies each declared service dependency.
15779
+ *
15780
+ * A RESOURCE RELATION, NOT A SETTING, which is why it is a sibling of
15781
+ * `configuration` rather than a key inside it. `configuration` is the
15782
+ * Product's own settings and is validated against the Definition's
15783
+ * schema; this names another Resource, and the create envelope made the
15784
+ * identical split for the identical reason.
15785
+ *
15786
+ * OPTIONAL, PERMANENTLY. Every manifest written before this field
15787
+ * existed omits it, and absence has to keep meaning "leave the
15788
+ * connections alone" — otherwise re-applying an unchanged file would
15789
+ * release a binding somebody made in the portal.
15790
+ *
15791
+ * The keys the platform will ACCEPT here are narrower than the shape:
15792
+ * only a dependency whose interface the registry calls
15793
+ * `tenant-multiple` is one a customer chooses. `resolveTemplate` says
15794
+ * so against the pinned Definition, so the refusal names the field.
15795
+ */
15796
+ dependencyBindings: DependencyBindingsSchema.optional()
15797
+ }).strict()
15798
+ }).strict();
15799
+ function secretValuesInDocument(schema, configuration) {
15800
+ if (!isPlainRecord(configuration) || !isPlainRecord(schema.properties))
15801
+ return [];
15802
+ const declarations = plainDeclarations(schema.properties);
15803
+ return declaredSecretPaths({
15804
+ properties: declarations
15805
+ }).filter((path) => {
15806
+ const segments = pathSegments(path).map(String);
15807
+ let parent = configuration;
15808
+ for (const segment of segments.slice(0, -1)) {
15809
+ parent = isPlainRecord(parent) ? parent[segment] : void 0;
15810
+ }
15811
+ const name = segments[segments.length - 1];
15812
+ return isPlainRecord(parent) && Object.hasOwn(parent, name) && !looksLikeReference(parent[name]);
15813
+ });
15814
+ }
15815
+ function plainDeclarations(properties) {
15816
+ const out = {};
15817
+ for (const [name, property] of Object.entries(properties)) {
15818
+ if (!isPlainRecord(property) || typeof property["type"] !== "string")
15819
+ continue;
15820
+ const copy = { ...property };
15821
+ if (Object.hasOwn(copy, "properties")) {
15822
+ if (isPlainRecord(copy["properties"]))
15823
+ copy["properties"] = plainDeclarations(copy["properties"]);
15824
+ else
15825
+ delete copy["properties"];
15826
+ }
15827
+ if (Object.hasOwn(copy, "items")) {
15828
+ const items = copy["items"];
15829
+ if (!isPlainRecord(items) || typeof items["type"] !== "string")
15830
+ delete copy["items"];
15831
+ else if (Object.hasOwn(items, "properties")) {
15832
+ copy["items"] = isPlainRecord(items["properties"]) ? { ...items, properties: plainDeclarations(items["properties"]) } : (({ properties: _dropped, ...rest }) => rest)(items);
15833
+ }
15834
+ }
15835
+ out[name] = copy;
15124
15836
  }
15837
+ return out;
15838
+ }
15839
+ function secretValueIssue(name) {
15840
+ return {
15841
+ field: manifestConfigurationField(name),
15842
+ message: `"${name}" is secret input and must never appear in a template. Supply it separately when applying, so it goes to the secret manager rather than into a document.`
15843
+ };
15844
+ }
15845
+ function isPlainRecord(value) {
15846
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15125
15847
  }
15126
-
15127
- // ../product-contracts/dist/announcements.js
15128
- var ANNOUNCED_PRODUCTS = Object.freeze([
15129
- {
15130
- productId: "hosted-models",
15131
- displayName: "Hosted Models",
15132
- summary: "Build, customize and deploy your own AI models",
15133
- category: "ai-models",
15134
- order: 0
15135
- },
15136
- {
15137
- productId: "vern",
15138
- displayName: "Vern",
15139
- summary: "Protect sensitive data in AI workloads",
15140
- category: "data-security",
15141
- order: 0
15142
- }
15143
- ]);
15144
15848
 
15145
15849
  // ../product-contracts/dist/publish-rules.js
15146
15850
  var PRODUCT_DISPLAY_NAME_PATTERN = "^[^\\u2014\\s](?:[^\\u2014]*[^\\u2014\\s])? \\u2014 [^\\u2014\\s](?:[^\\u2014]*[^\\u2014\\s])?$";
@@ -15464,8 +16168,12 @@ var EnvBindingV2Schema = external_exports.discriminatedUnion("source", [
15464
16168
  *
15465
16169
  * WHAT IT CARRIES: validated, non-secret configuration with resolved
15466
16170
  * ORDINARY resource references materialised in place. Never a secret
15467
- * value — secrets are root-only and travel through `secrets[]`, and a
15468
- * subtree cannot contain one. Serialised with `canonicalJson`, so the same
16171
+ * value — a secret travels through `secrets[]`, and its value is split out
16172
+ * of the configuration before anything is stored. A secret may sit inside
16173
+ * a group, so `validateProductContracts` refuses a `path` naming a secret
16174
+ * or a group that contains one anywhere below it: a component bound to
16175
+ * that group would read it as complete while its credential is absent.
16176
+ * Serialised with `canonicalJson`, so the same
15469
16177
  * logical configuration is the same bytes and the environment fingerprint
15470
16178
  * does not move on key order alone.
15471
16179
  *
@@ -15815,6 +16523,20 @@ var SecretWiringSchema = external_exports.object({
15815
16523
  envVar: EnvVarNameSchema
15816
16524
  }).strict()).min(1).max(12)
15817
16525
  }).strict();
16526
+ var SecretWiringV2Schema = SecretWiringSchema.extend({
16527
+ /**
16528
+ * LEGACY SPELLING of a ROOT secret's path, kept so released v2 Blueprints
16529
+ * still read. Described here rather than in the declaration table, whose note
16530
+ * is what the v1 artifact publishes for the same field.
16531
+ */
16532
+ configurationProperty: external_exports.string().regex(/^[a-z][a-zA-Z0-9]{1,47}$/).describe("Accepted only so releases published before `configurationPath` existed still read. It names a root secret and means the same as `configurationPath` with that name; a new release must use `configurationPath`, and a Blueprint may not name both.").optional(),
16533
+ /**
16534
+ * Set only for `customer-configuration`: the canonical path of the `x-secret`
16535
+ * setting in the Product Definition whose write-only submission populates
16536
+ * this secret — a bare name at the root, a pointer inside a group.
16537
+ */
16538
+ configurationPath: ConfigurationBindingPathSchema.optional()
16539
+ }).strict();
15818
16540
  var DataCategorySchema = external_exports.enum([
15819
16541
  "prompt-content",
15820
16542
  "model-response",
@@ -16244,7 +16966,8 @@ var DeploymentBlueprintV1ObjectSchema = DeploymentBlueprintObjectSchema.extend({
16244
16966
  });
16245
16967
  var DeploymentBlueprintV2ObjectSchema = DeploymentBlueprintObjectSchema.extend({
16246
16968
  apiVersion: ProductApiVersionV2Schema,
16247
- components: external_exports.array(RuntimeComponentV2Schema).min(1).max(MAX_RUNTIME_COMPONENTS)
16969
+ components: external_exports.array(RuntimeComponentV2Schema).min(1).max(MAX_RUNTIME_COMPONENTS),
16970
+ secrets: external_exports.array(SecretWiringV2Schema).max(20)
16248
16971
  });
16249
16972
  var DeploymentBlueprintV1Schema = DeploymentBlueprintV1ObjectSchema.superRefine(checkDeploymentBlueprint);
16250
16973
  var DeploymentBlueprintV2Schema = DeploymentBlueprintV2ObjectSchema.superRefine(checkDeploymentBlueprint);
@@ -16628,18 +17351,28 @@ function checkDeploymentBlueprint(blueprint, ctx) {
16628
17351
  });
16629
17352
  }
16630
17353
  }
16631
- if (secret.source === "customer-configuration" && secret.configurationProperty === void 0) {
17354
+ const pathField = "configurationPath" in secret ? secret.configurationPath : void 0;
17355
+ const namesSetting = secret.configurationProperty !== void 0 || pathField !== void 0;
17356
+ const settingField = blueprint.apiVersion === PRODUCT_API_VERSION ? "configurationProperty" : "configurationPath";
17357
+ if (secret.configurationProperty !== void 0 && pathField !== void 0) {
17358
+ ctx.addIssue({
17359
+ code: external_exports.ZodIssueCode.custom,
17360
+ path,
17361
+ message: `Secret "${secret.key}" names both configurationProperty and configurationPath. Name the setting once, with configurationPath.`
17362
+ });
17363
+ }
17364
+ if (secret.source === "customer-configuration" && !namesSetting) {
16632
17365
  ctx.addIssue({
16633
17366
  code: external_exports.ZodIssueCode.custom,
16634
17367
  path,
16635
- message: `Secret "${secret.key}" is sourced from customer configuration but names no configurationProperty.`
17368
+ message: `Secret "${secret.key}" is sourced from customer configuration but names no ${settingField}.`
16636
17369
  });
16637
17370
  }
16638
- if (secret.source !== "customer-configuration" && secret.configurationProperty !== void 0) {
17371
+ if (secret.source !== "customer-configuration" && namesSetting) {
16639
17372
  ctx.addIssue({
16640
17373
  code: external_exports.ZodIssueCode.custom,
16641
17374
  path,
16642
- message: `Secret "${secret.key}" names a configurationProperty but is not customer-sourced.`
17375
+ message: `Secret "${secret.key}" names a ${pathField !== void 0 ? "configurationPath" : "configurationProperty"} but is not customer-sourced.`
16643
17376
  });
16644
17377
  }
16645
17378
  if (secret.source === "service-binding" && secret.serviceBinding === void 0) {
@@ -17017,193 +17750,6 @@ function isTerminalAction(action) {
17017
17750
  }
17018
17751
  var ManifestActionSchema = external_exports.enum(MANIFEST_ACTIONS);
17019
17752
 
17020
- // ../product-contracts/dist/contract-issue.js
17021
- var PATH_SEGMENT = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/;
17022
- var UNPRINTABLE_SEGMENT = "<unprintable>";
17023
- var MAX_SEGMENTS = MAX_CONFIGURATION_PATH_SEGMENTS + 2;
17024
- function printableContractPath(path) {
17025
- const printable = path.slice(0, MAX_SEGMENTS).map((segment) => typeof segment === "number" || PATH_SEGMENT.test(segment) ? String(segment) : UNPRINTABLE_SEGMENT);
17026
- return printable.join(".") || "(document)";
17027
- }
17028
- function manifestConfigurationField(field) {
17029
- if (field === "")
17030
- return "spec.configuration";
17031
- const parsed = parseConfigurationPath(field);
17032
- return printableContractPath([
17033
- "spec",
17034
- "configuration",
17035
- ...parsed === null ? [field] : pathSegments(parsed)
17036
- ]);
17037
- }
17038
-
17039
- // ../product-contracts/dist/resource-output-reference.js
17040
- var OUTPUT_KEY_RULE = "Output keys are camelCase.";
17041
- var ResourceOutputReferenceSchema = external_exports.object({
17042
- valueFrom: external_exports.object({
17043
- resourceOutput: external_exports.object({
17044
- /** The producing Resource's `metadata.key`, in the same Project. */
17045
- resourceKey: ResourceKeySchema,
17046
- /** The key of an output that Resource's Product declares. */
17047
- output: external_exports.string().regex(/^[a-z][a-zA-Z0-9]{1,47}$/, OUTPUT_KEY_RULE)
17048
- }).strict()
17049
- }).strict()
17050
- }).strict();
17051
- function looksLikeReference(value) {
17052
- return typeof value === "object" && value !== null && !Array.isArray(value) && "valueFrom" in value;
17053
- }
17054
-
17055
- // ../product-contracts/dist/dependency-binding.js
17056
- var DependencyBindingSelectorSchema = external_exports.object({
17057
- /** The providing Resource's `metadata.key`. */
17058
- resourceKey: ResourceKeySchema,
17059
- /**
17060
- * The Project that Resource lives in. Absent means the consumer's own.
17061
- *
17062
- * PRESENT BECAUSE A BINDING IS TENANT-SCOPED AND A PROJECT IS NOT A RUNTIME
17063
- * BOUNDARY: a provider anywhere in the Tenant can satisfy a consumer
17064
- * anywhere in it, and that has been true since bindings existed. What the
17065
- * field does NOT do is grant anything — the Control Plane opens that
17066
- * Project from the caller's own assertion, and a Project the caller holds
17067
- * nothing in answers exactly as a Project that does not exist.
17068
- *
17069
- * A plain bounded string, exactly as `spec.projectId` is: an id that does
17070
- * not name a readable Project is refused where the refusal can be phrased
17071
- * without leaking, which is not here.
17072
- */
17073
- projectId: external_exports.string().min(1).max(64).optional()
17074
- }).strict();
17075
- var DependencyBindingsSchema = external_exports.record(ServiceBindingKeySchema, DependencyBindingSelectorSchema);
17076
-
17077
- // ../product-contracts/dist/product-instance-template.schema.js
17078
- var ProductInstanceTemplateSchema = external_exports.object({
17079
- apiVersion: ResourceApiVersionSchema,
17080
- kind: kindSchema(PRODUCT_INSTANCE_KIND),
17081
- metadata: external_exports.object({
17082
- /**
17083
- * The customer-facing Resource name — "Local service".
17084
- *
17085
- * Maps to `ProductInstance.displayName`, and deliberately introduces NO
17086
- * new identity concept. `pri_…` remains the stable identity and
17087
- * `instanceKey` remains the cloud resource key; adding a third name here
17088
- * would mean a fourth thing to keep unique and a fifth to explain.
17089
- * Bounded exactly as the create envelope bounds `displayName`.
17090
- */
17091
- // NOT `.trim()`: this schema is PUBLISHED as JSON Schema so a customer
17092
- // can validate the file before sending it, and a trim is a transform
17093
- // that JSON Schema cannot express. The value is trimmed when it becomes
17094
- // a create command instead, so the published document and the platform
17095
- // agree on what is valid.
17096
- name: external_exports.string().min(1).max(80).regex(/\S/, "The resource name must contain a non-whitespace character. A name of only spaces trims to nothing, and an empty display name is not a name."),
17097
- /**
17098
- * THE STABLE IDENTITY OF THIS RESOURCE INSIDE ITS PROJECT — and the one
17099
- * thing that makes applying a document twice different from creating
17100
- * two resources.
17101
- *
17102
- * THIS IS THE FOURTH NAME, AND THE HEADER ABOVE ARGUES AGAINST ONE.
17103
- * That argument stands and is answered rather than ignored: a
17104
- * declarative apply is not expressible without a key the AUTHOR knows
17105
- * before the platform has created anything, and nothing that already
17106
- * exists is one.
17107
- *
17108
- * `metadata.name` mutable, not unique, and rule 1 above says it is
17109
- * display copy rather than identity.
17110
- * `pri_…` minted by the server. It does not exist when the
17111
- * first apply is written, which is the only moment
17112
- * that matters.
17113
- * `instanceKey` derived from `pri_…` and then frozen — the same
17114
- * problem one step removed.
17115
- * idempotency key a de-duplication token that "names nothing,
17116
- * grants nothing, and selects nothing". Making it
17117
- * select a resource would falsify the security
17118
- * argument the signed-request layer rests on.
17119
- *
17120
- * So it gets exactly ONE job: to say which resource in this Project the
17121
- * document is about. It is NEVER authorization — the Project comes from
17122
- * the request, the row is loaded inside it, and a key naming nothing
17123
- * there is a not-found rather than a create. And it NEVER becomes a
17124
- * provider resource name; `instanceKey` owns that, and coupling the two
17125
- * would let a customer's chosen string reach Scaleway.
17126
- *
17127
- * OPTIONAL HERE, REQUIRED AT APPLY. The document is a published `v1`
17128
- * contract and validating one on its own is a legitimate thing to do
17129
- * with no Project in sight — the same asymmetry `resolveTemplate`
17130
- * already has for `routeProjectId`. What refuses a document without a
17131
- * key is the apply path, because a create that cannot be run again is
17132
- * not deployment as code.
17133
- *
17134
- * Shaped like a DNS label: it becomes a database predicate and a log
17135
- * field, and neither wants free text.
17136
- */
17137
- key: ResourceKeySchema.optional()
17138
- }).strict(),
17139
- spec: external_exports.object({
17140
- productId: external_exports.string().min(1).max(64),
17141
- /**
17142
- * REQUIRED, unlike the create envelope's, which may be omitted to take
17143
- * the newest deployable release.
17144
- *
17145
- * A template is a record of a decision, and "newest" is not one. An
17146
- * optional version would mean the same document produced 1.0.0 today and
17147
- * 1.1.0 after an unrelated promotion — so re-applying an exported
17148
- * template would not reproduce what was exported, which is the single
17149
- * property that makes templates worth having.
17150
- */
17151
- productVersion: external_exports.string().min(1).max(64),
17152
- /** Round-trip only — see rule 3 above. Never authorization. */
17153
- projectId: external_exports.string().min(1).max(64).optional(),
17154
- region: external_exports.string().min(1).max(32),
17155
- profile: external_exports.string().min(1).max(40),
17156
- /**
17157
- * The Product's own settings.
17158
- *
17159
- * An OBJECT with product-specific keys, not `unknown`: the published
17160
- * JSON Schema has to say at least that much, and the converter refuses
17161
- * `unknown` precisely so a published contract cannot silently widen.
17162
- * The keys themselves are validated against the pinned Definition's
17163
- * configuration schema by `resolveTemplate`, which is where the real
17164
- * shape lives — it differs per Product, so it cannot be stated here.
17165
- */
17166
- configuration: external_exports.record(external_exports.unknown()).optional(),
17167
- /**
17168
- * Which Resource satisfies each declared service dependency.
17169
- *
17170
- * A RESOURCE RELATION, NOT A SETTING, which is why it is a sibling of
17171
- * `configuration` rather than a key inside it. `configuration` is the
17172
- * Product's own settings and is validated against the Definition's
17173
- * schema; this names another Resource, and the create envelope made the
17174
- * identical split for the identical reason.
17175
- *
17176
- * OPTIONAL, PERMANENTLY. Every manifest written before this field
17177
- * existed omits it, and absence has to keep meaning "leave the
17178
- * connections alone" — otherwise re-applying an unchanged file would
17179
- * release a binding somebody made in the portal.
17180
- *
17181
- * The keys the platform will ACCEPT here are narrower than the shape:
17182
- * only a dependency whose interface the registry calls
17183
- * `tenant-multiple` is one a customer chooses. `resolveTemplate` says
17184
- * so against the pinned Definition, so the refusal names the field.
17185
- */
17186
- dependencyBindings: DependencyBindingsSchema.optional()
17187
- }).strict()
17188
- }).strict();
17189
- function secretValuesInDocument(schema, configuration) {
17190
- if (!isPlainRecord(configuration) || !isPlainRecord(schema.properties))
17191
- return [];
17192
- const declarations = Object.fromEntries(Object.entries(schema.properties).filter(([, property]) => isPlainRecord(property)));
17193
- return declaredSecretNames({
17194
- properties: declarations
17195
- }).filter((name) => Object.hasOwn(configuration, name) && !looksLikeReference(configuration[name]));
17196
- }
17197
- function secretValueIssue(name) {
17198
- return {
17199
- field: manifestConfigurationField(name),
17200
- message: `"${name}" is secret input and must never appear in a template. Supply it separately when applying, so it goes to the secret manager rather than into a document.`
17201
- };
17202
- }
17203
- function isPlainRecord(value) {
17204
- return typeof value === "object" && value !== null && !Array.isArray(value);
17205
- }
17206
-
17207
17753
  // ../product-contracts/dist/product-submission.schema.js
17208
17754
  var PRODUCT_SUBMISSION_KIND = "ProductSubmission";
17209
17755
  var SubmissionPublisherSchema = external_exports.object({
@@ -17297,10 +17843,29 @@ var PUBLISH_RULE_ANNEX = {
17297
17843
  }
17298
17844
  }
17299
17845
  }
17846
+ },
17847
+ {
17848
+ // THE SAME ASYMMETRY, a third time. Presence and shape are all JSON Schema
17849
+ // can say: whether the region and profile are this Product's, whether the
17850
+ // configuration fills every required setting and whether every required
17851
+ // secret is named are questions about the rest of the document, answered
17852
+ // by preflight.
17853
+ $comment: "A new release must declare definition.deploymentExample: one recommended deployment, with region, profile and configuration. Preflight also checks the region and profile are this Product's, the configuration is complete against its schema, no secret carries a value, and every required secret is named in secretInputs.",
17854
+ properties: {
17855
+ definition: {
17856
+ required: ["deploymentExample"],
17857
+ properties: {
17858
+ deploymentExample: {
17859
+ required: ["title", "description", "region", "profile", "configuration"]
17860
+ }
17861
+ }
17862
+ }
17863
+ }
17300
17864
  }
17301
17865
  ]
17302
17866
  };
17303
17867
  withJsonSchemaAnnex(ProductSubmissionSchema, PUBLISH_RULE_ANNEX);
17868
+ withJsonSchemaAnnex(ProductSubmissionV2Schema, PUBLISH_RULE_ANNEX);
17304
17869
 
17305
17870
  // src/manifest.ts
17306
17871
  var import_yaml = __toESM(require_dist(), 1);
@@ -17333,15 +17898,21 @@ function resolveSecrets(pairs, environment = process.env) {
17333
17898
  if (separator <= 0) {
17334
17899
  return {
17335
17900
  ok: false,
17336
- message: `${label} expects name=env:VARIABLE, and there is no name before "=".`
17901
+ message: `${label} expects NAME=env:VARIABLE, and there is no name before "=".`
17337
17902
  };
17338
17903
  }
17339
- const name = pair.slice(0, separator);
17904
+ const name = secretPathOf(pair.slice(0, separator));
17340
17905
  const source = pair.slice(separator + 1);
17906
+ if (name === null) {
17907
+ return {
17908
+ ok: false,
17909
+ message: `${label} expects a setting name before "=", or a /group/setting path for a setting inside a group. A path names no list item and does not end in "/".`
17910
+ };
17911
+ }
17341
17912
  if (!source.startsWith("env:")) {
17342
17913
  return {
17343
17914
  ok: false,
17344
- message: `${label} must read from the environment: --secret <name>=env:VARIABLE. A value on the command line is in your shell history and in the process table.`
17915
+ message: `${label} must read from the environment: --secret <name-or-path>=env:VARIABLE. A value on the command line is in your shell history and in the process table.`
17345
17916
  };
17346
17917
  }
17347
17918
  const variable = source.slice("env:".length);
@@ -17349,10 +17920,10 @@ function resolveSecrets(pairs, environment = process.env) {
17349
17920
  return { ok: false, message: `${label} names no variable after "env:".` };
17350
17921
  }
17351
17922
  const value = environment[variable];
17352
- if (value === void 0 || value === "") {
17923
+ if (value === void 0 || value.trim() === "") {
17353
17924
  return {
17354
17925
  ok: false,
17355
- message: `${label} reads an environment variable that is not set, or is empty.`
17926
+ message: `${label} reads an environment variable that is not set, or holds nothing but whitespace.`
17356
17927
  };
17357
17928
  }
17358
17929
  secrets[name] = value;
@@ -17650,6 +18221,9 @@ function secretWriteConsequence(plan) {
17650
18221
  }
17651
18222
  return lines;
17652
18223
  }
18224
+ function requiredSecretLines(names) {
18225
+ return names.length === 0 ? [] : [" required secrets:", ...names.map((name) => ` ${name}`)];
18226
+ }
17653
18227
  function printPlan(plan, json) {
17654
18228
  if (json) {
17655
18229
  process.stdout.write(`${JSON.stringify(plan)}
@@ -17682,11 +18256,7 @@ function printPlan(plan, json) {
17682
18256
  }
17683
18257
  }
17684
18258
  }
17685
- const required = plan.requiredSecrets ?? [];
17686
- if (required.length > 0) {
17687
- lines.push(" required secrets:");
17688
- for (const name of required) lines.push(` ${name}`);
17689
- }
18259
+ lines.push(...requiredSecretLines(plan.requiredSecrets ?? []));
17690
18260
  if ((plan.secretsToWrite ?? []).length > 0) {
17691
18261
  lines.push(` secrets to set: ${plan.secretsToWrite.join(", ")}`);
17692
18262
  for (const line of secretWriteConsequence(plan)) lines.push(` ${line}`);
@@ -17770,10 +18340,15 @@ async function runApply(verb, parsed, deps = {}) {
17770
18340
  body: { verb: "validate", payload: manifest2.document }
17771
18341
  });
17772
18342
  if (!result.ok) return printError(result.error, json);
18343
+ if (json) {
18344
+ process.stdout.write(`${JSON.stringify(result.data)}
18345
+ `);
18346
+ return 0;
18347
+ }
18348
+ const required = result.data["requiredSecrets"];
18349
+ const names = Array.isArray(required) ? required.filter((name) => typeof name === "string") : [];
17773
18350
  process.stdout.write(
17774
- json ? `${JSON.stringify(result.data)}
17775
- ` : `${file} is a valid manifest.
17776
- `
18351
+ [`${file} is a valid manifest.`, ...requiredSecretLines(names)].join("\n") + "\n"
17777
18352
  );
17778
18353
  return 0;
17779
18354
  }
@@ -18162,21 +18737,32 @@ async function runDoctor(parsed) {
18162
18737
  const mutating = access.projects.filter(
18163
18738
  (project) => project.permissions.some((permission) => !permission.endsWith(".read"))
18164
18739
  );
18740
+ const organizationWide = access.tenantPermissions.includes("project.read");
18741
+ const alsoOrganizationWide = access.tenantPermissions.length > 0 ? `Organization-wide it holds ${access.tenantPermissions.join(", ")}, which reaches no Project. ` : "";
18742
+ const perProject = projects.length === 0 ? "" : `. Granted per Project: ${projects.join(", ")}` + (mutating.length > 0 ? ` \u2014 you can deploy in ${mutating.map((project) => project.projectId).join(", ")}` : " \u2014 read-only there");
18165
18743
  checks.push(
18166
- access.tenantWide ? {
18744
+ organizationWide ? {
18167
18745
  name: "Access",
18168
18746
  ok: true,
18169
- detail: `the whole organization \u2014 ${access.tenantPermissions.join(", ")}`,
18170
- facts: { scope: "organization", grantedProjectIds: null }
18747
+ detail: `the whole organization \u2014 ${access.tenantPermissions.join(", ")}${perProject}`,
18748
+ facts: {
18749
+ scope: "organization",
18750
+ grantedProjectIds: projects.length > 0 ? projects.join(",") : null
18751
+ }
18171
18752
  } : projects.length === 0 ? {
18172
18753
  // A FAILURE, NOT A NOTE. This credential authenticates and can do
18173
18754
  // nothing at all — and the check said so in words while `report`
18174
18755
  // computed the exit code from `ok` alone, so `doctor` printed
18175
18756
  // "nothing else will work" and exited 0. A pipeline reads the code,
18176
18757
  // not the sentence.
18758
+ //
18759
+ // STILL A FAILURE WITH KNOWLEDGE READ AND NO PROJECT. That gate is a
18760
+ // legitimate one — it serves an MCP client — but every `vfac`
18761
+ // command that reads a Project is refused under it, and a pipeline
18762
+ // pointed at it has the wrong credential.
18177
18763
  name: "Access",
18178
18764
  ok: false,
18179
- detail: "none. This credential holds no Project and nothing organization-wide, so every command below it will be refused. Ask an organization administrator for access."
18765
+ detail: access.tenantPermissions.length > 0 ? `no Project. ${alsoOrganizationWide}Every command below that reads a Project will be refused. A gate holding only that serves an MCP client, not vfac; ask an organization administrator for a Project role on the gate vfac uses.` : "none. This credential holds no Project and nothing organization-wide, so every command below it will be refused. Ask an organization administrator for access."
18180
18766
  } : {
18181
18767
  name: "Access",
18182
18768
  ok: true,
@@ -18205,7 +18791,7 @@ async function runDoctor(parsed) {
18205
18791
  // names are the difference.
18206
18792
  grantedProjectIds: projects.join(",")
18207
18793
  },
18208
- detail: `${projects.join(", ")} \u2014 and nothing organization-wide. ` + (mutating.length > 0 ? "You can deploy in those Projects, read their catalogue, and list them. " : "Read-only there: catalogue, manifests, resources and operations. An apply or a lifecycle command is refused, and that is what was granted rather than a fault. ") + "Anything ACROSS the organization \u2014 a Project you hold nothing in, or its members \u2014 is refused."
18794
+ detail: `${projects.join(", ")}` + (access.tenantPermissions.length > 0 ? ". " : " \u2014 and nothing organization-wide. ") + alsoOrganizationWide + (mutating.length > 0 ? "You can deploy in those Projects, read their catalogue, and list them. " : "Read-only there: catalogue, manifests, resources and operations. An apply or a lifecycle command is refused, and that is what was granted rather than a fault. ") + "Anything ACROSS the organization \u2014 a Project you hold nothing in, or its members \u2014 is refused."
18209
18795
  }
18210
18796
  );
18211
18797
  checks.push({
@@ -18762,6 +19348,21 @@ function renderProduct(product) {
18762
19348
  lines.push(` ${setting.label} ${setting.type ?? ""}`.trimEnd());
18763
19349
  }
18764
19350
  }
19351
+ const together = seed.groups.filter((group) => group.within !== null);
19352
+ if (together.length > 0) {
19353
+ lines.push("", " Settings that go together");
19354
+ for (const group of together) {
19355
+ const index = group.settings.indexOf(group.trigger);
19356
+ const trigger = index === -1 ? `${group.trigger} (secret)` : group.labels[index] ?? group.trigger;
19357
+ const needs = [
19358
+ ...group.labels.filter((_, at) => at !== index),
19359
+ ...group.secrets.filter((path) => path !== group.trigger).map((path) => `--secret ${path}=env:VAR`)
19360
+ ];
19361
+ lines.push(
19362
+ ` ${trigger} also needs ${needs.join(" + ")} (if ${group.within} is configured)`
19363
+ );
19364
+ }
19365
+ }
18765
19366
  const dependencies = product.dependencies ?? [];
18766
19367
  if (dependencies.length > 0) {
18767
19368
  lines.push("", " Connections to another resource");
@@ -19063,7 +19664,7 @@ async function runManifestInit(parsed) {
19063
19664
  );
19064
19665
  }
19065
19666
  for (const group of seed.groups) {
19066
- const settings = group.settings.map((name2) => `spec.configuration.${name2}`);
19667
+ const settings = group.labels.map((label) => `spec.configuration.${label}`);
19067
19668
  const secrets = group.secrets.map((name2) => `--secret ${name2}=env:VAR`);
19068
19669
  notes.push(
19069
19670
  `${[...settings, ...secrets].join(" + ")} \u2014 all of these together, or none of them`
@@ -19216,10 +19817,13 @@ async function runWhoami(parsed) {
19216
19817
  }
19217
19818
  const { principal, access } = result.data;
19218
19819
  const lines = [`${principal.name} ${principal.id} (${principal.type})`];
19219
- if (access.tenantWide) {
19820
+ if (access.tenantPermissions.length > 0) {
19220
19821
  lines.push("", "Granted across the whole organization");
19221
19822
  lines.push(` ${access.tenantPermissions.join(", ")}`);
19222
- } else {
19823
+ }
19824
+ if (access.projects.length > 0 && access.tenantPermissions.length > 0) {
19825
+ lines.push("", "Granted in these Projects");
19826
+ } else if (!access.tenantWide) {
19223
19827
  lines.push("", "Granted in these Projects only \u2014 nothing organization-wide.");
19224
19828
  lines.push(" Listing these works. Reading ACROSS the organization does not.");
19225
19829
  }
@@ -19227,7 +19831,7 @@ async function runWhoami(parsed) {
19227
19831
  lines.push("", ` ${project.projectId}`);
19228
19832
  lines.push(` ${project.permissions.join(", ")}`);
19229
19833
  }
19230
- if (!access.tenantWide && access.projects.length === 0) {
19834
+ if (access.projects.length === 0 && !access.tenantPermissions.includes("project.read")) {
19231
19835
  lines.push("", " No Projects. Ask an organization administrator for access.");
19232
19836
  }
19233
19837
  lines.push(
@@ -19276,7 +19880,9 @@ Options
19276
19880
  --endpoint your organization's portal. Defaults to VFAC_ENDPOINT, then the
19277
19881
  stored context
19278
19882
  --secret plan and apply: NAME=env:VARIABLE \u2014 the value is read from the
19279
- environment. Plan a run that sets a secret with the same
19883
+ environment. NAME is the setting's name, or its path when it
19884
+ sits inside a group: --secret /upstreamAuth/clientSecret=env:VAR.
19885
+ Plan a run that sets a secret with the same
19280
19886
  --secret inputs the apply will carry; without them the plan
19281
19887
  answers about a different request. A value on the command line
19282
19888
  is in your shell history and in the process table, so it is