@varde-flyt/vfac 0.11.0 → 0.13.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 +19 -7
  2. package/dist/vfac.mjs +534 -39
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -74,14 +74,21 @@ vfac manifest init --product <productId> --name "My resource" > resource.yaml
74
74
 
75
75
  ```sh
76
76
  vfac validate -f resource.yaml
77
- vfac plan -f resource.yaml
77
+ vfac plan -f resource.yaml --secret SETTING=env:VARIABLE
78
78
  vfac apply -f resource.yaml --secret SETTING=env:VARIABLE
79
79
  vfac get operation <operationId>
80
80
  vfac get product-instance <productInstanceId>
81
81
  ```
82
82
 
83
83
  Secrets are supplied per apply and never belong in the manifest — a manifest is a
84
- file you commit. `vfac plan` lists the ones an apply will still need.
84
+ file you commit. **Plan the run that sets one with the same `--secret` inputs the
85
+ apply will carry**: `plan` takes the flag exactly as `apply` does, and without it
86
+ the plan describes a different request.
87
+
88
+ `requiredSecrets` in a plan is what the Product requires for the configuration in
89
+ your document — not a list of what the platform is missing. It names a credential
90
+ whether or not this resource already holds one, so it is not a shopping list to
91
+ act on. `secretsToWrite` is the field that says what this apply would write.
85
92
 
86
93
  ### `metadata.key` is the name you choose
87
94
 
@@ -176,7 +183,8 @@ spec:
176
183
  `vfac manifest init` names the exact property for every dependency you have to
177
184
  choose, and `vfac plan` prints the connections an apply would establish before
178
185
  you approve it. The dependency key comes from the Product, not from you —
179
- `vfac product show <productId>` lists them.
186
+ `vfac product show <productId>` lists every dependency with its key, its
187
+ capability, and whether a setting is what turns it on.
180
188
 
181
189
  This is not the same thing as the section above, and the difference is worth
182
190
  holding on to:
@@ -198,7 +206,8 @@ it.
198
206
 
199
207
  Every command takes `--output json`, and it is the same data rather than a
200
208
  prettier subset — the human format of `vfac product show` prints a readable
201
- selection on purpose, and the JSON carries the whole Product contract.
209
+ selection on purpose and says so at the foot of its output, and the JSON carries
210
+ the whole Product contract.
202
211
 
203
212
  ```sh
204
213
  vfac product show <productId> --output json
@@ -261,12 +270,15 @@ so this answers `UPDATE` and redeploys, even when the value is the value already
261
270
  stored:
262
271
 
263
272
  ```sh
273
+ vfac plan -f resource.yaml --secret SETTING=env:VARIABLE
264
274
  vfac apply -f resource.yaml --secret SETTING=env:VARIABLE
265
275
  ```
266
276
 
267
- Send secrets on the apply that creates the resource and on the run that rotates
268
- one, and on no other run. A pipeline that passes its secrets every run restarts
269
- your service every time somebody pushes a README.
277
+ Three runs carry a secret: the apply that creates the resource, the first time a
278
+ setting needs a credential on a resource that already exists, and a rotation. Send
279
+ none on any other run — a pipeline that passes its secrets every run restarts your
280
+ service every time somebody pushes a README. Plan each of those three with the
281
+ same `--secret` inputs: with no secret to write, a rotation plans as `NO_CHANGE`.
270
282
 
271
283
  ## Where the Gate Secret goes
272
284
 
package/dist/vfac.mjs CHANGED
@@ -7928,12 +7928,33 @@ function renderOperation(operationId, data) {
7928
7928
  );
7929
7929
  }
7930
7930
  }
7931
+ const product = failure["product"];
7932
+ const reportedSettings = [];
7933
+ if (Array.isArray(product)) {
7934
+ for (const entry of product) {
7935
+ const one = asRecord(entry);
7936
+ if (one === null) continue;
7937
+ const component = one["component"];
7938
+ const code = one["code"];
7939
+ if (typeof component !== "string" || typeof code !== "string") continue;
7940
+ if (component === "" || code === "") continue;
7941
+ const setting = one["setting"];
7942
+ if (typeof setting === "string" && setting !== "") {
7943
+ reportedSettings.push(setting);
7944
+ lines.push(` ${component} refused to start: ${code} (setting: ${setting})`);
7945
+ } else {
7946
+ lines.push(` ${component} refused to start: ${code}`);
7947
+ }
7948
+ }
7949
+ }
7931
7950
  const retryable = failure["retryable"];
7932
7951
  if (typeof retryable === "boolean") {
7933
7952
  const again = data["type"] === "PRODUCT_INSTANCE_UPGRADE" ? "`vfac lifecycle upgrade <pri_\u2026>`" : "`vfac lifecycle reconcile <pri_\u2026>`";
7934
7953
  const changeIt = data["type"] === "PRODUCT_INSTANCE_UPGRADE" ? "" : ", or `vfac apply` a corrected manifest";
7954
+ const cause2 = failure["cause"];
7955
+ const named = reportedSettings[0];
7935
7956
  lines.push(
7936
- retryable ? ` Another attempt can succeed: ${again}.` : ` Repeating this unchanged fails again \u2014 fix what the lines above name, then ${again}${changeIt}.`
7957
+ retryable ? ` Another attempt can succeed: ${again}.` : named !== void 0 ? ` Correct \`${named}\` in your manifest, then \`vfac apply\` it${data["type"] === "PRODUCT_INSTANCE_UPGRADE" ? ` and ${again}` : ""}. Repeating it unchanged fails the same way.` : cause2 === "unknown" ? ` The platform could not classify this, so there is nothing here to correct. Read the whole record with \`vfac get operation ${operationId} --output json\`, and quote that id if you ask for help. Repeating it unchanged fails the same way.` : ` Repeating this unchanged fails again \u2014 fix what the lines above name, then ${again}${changeIt}.`
7937
7958
  );
7938
7959
  }
7939
7960
  }
@@ -12244,6 +12265,29 @@ var ValueConditionSchema = external_exports.object({
12244
12265
  anyOf: external_exports.array(external_exports.string().min(1).max(200)).min(1).max(20),
12245
12266
  anyRow: external_exports.boolean().optional()
12246
12267
  }).strict();
12268
+ var ValueReferenceSchema = external_exports.object({
12269
+ /**
12270
+ * The setting whose VALUES form the allowed set — a canonical declaration
12271
+ * path, or a bare root key, which is the same string at depth 1.
12272
+ */
12273
+ path: external_exports.string().min(1).max(400)
12274
+ }).strict();
12275
+ var RowCardinalitySchema = external_exports.object({
12276
+ /** A setting declared DIRECTLY on the row. */
12277
+ property: PropertyNameSchema,
12278
+ /** The answers that count toward the limit. Non-empty. */
12279
+ anyOf: external_exports.array(external_exports.string().min(1).max(200)).min(1).max(20),
12280
+ /**
12281
+ * How many rows may answer that way. Zero is meaningful and allowed — "no
12282
+ * row may, under this release" is a thing an upgrade needs to be able to
12283
+ * say.
12284
+ */
12285
+ max: external_exports.number().int().min(0).max(99)
12286
+ }).strict();
12287
+ var RowRulesSchema = external_exports.object({
12288
+ unique: external_exports.array(PropertyNameSchema).min(1).max(MAX_CONFIGURATION_CHILDREN).optional(),
12289
+ atMost: external_exports.array(RowCardinalitySchema).min(1).max(10).optional()
12290
+ }).strict();
12247
12291
  var TitleSchema = external_exports.string().min(2).max(80).describe("Customer-facing field label.");
12248
12292
  var V1_LEAF_BOUNDS = {
12249
12293
  help: 400,
@@ -12317,7 +12361,16 @@ function leafShapes(bounds) {
12317
12361
  * what `requiredSecrets` reports, which is one answer with one reader.
12318
12362
  * Opening it wider is a separate decision with its own blast radius.
12319
12363
  */
12320
- "x-required-when": ValueConditionSchema.optional()
12364
+ "x-required-when": ValueConditionSchema.optional(),
12365
+ /**
12366
+ * The values this one must be chosen from — see {@link ValueReferenceSchema}.
12367
+ *
12368
+ * ON A STRING ONLY, and not on a secret: the platform never reads a
12369
+ * secret's value, so a membership test on one could never be evaluated.
12370
+ * The publish rules say so rather than leaving it to be discovered, the
12371
+ * same way they already do for a condition pointed at a secret.
12372
+ */
12373
+ "x-value-in": ValueReferenceSchema.optional()
12321
12374
  }).strict();
12322
12375
  withJsonSchemaAnnex(string, SECRET_COUPLING_ANNEX);
12323
12376
  const number = external_exports.object({
@@ -12328,14 +12381,23 @@ function leafShapes(bounds) {
12328
12381
  /** Both bounds required — "numeric limits" in the guide is not optional. */
12329
12382
  minimum: external_exports.number(),
12330
12383
  maximum: external_exports.number(),
12331
- default: external_exports.number().optional()
12384
+ default: external_exports.number().optional(),
12385
+ /**
12386
+ * WIDENED FROM SECRETS TO EVERY ROOT SETTING, which this shape's own
12387
+ * comment on the string leaf used to record as a known gap: "a
12388
+ * value-conditioned requirement for an ORDINARY setting is a real gap
12389
+ * too". It is the same keyword, the same evaluator and the same publish
12390
+ * rules; what changes is which settings may carry it.
12391
+ */
12392
+ "x-required-when": ValueConditionSchema.optional()
12332
12393
  }).strict();
12333
12394
  const boolean = external_exports.object({
12334
12395
  type: external_exports.literal("boolean"),
12335
12396
  title: TitleSchema,
12336
12397
  description: help.optional(),
12337
12398
  group: ConfigurationGroupSchema,
12338
- default: external_exports.boolean().optional()
12399
+ default: external_exports.boolean().optional(),
12400
+ "x-required-when": ValueConditionSchema.optional()
12339
12401
  }).strict();
12340
12402
  const items = external_exports.object({
12341
12403
  type: external_exports.literal("string"),
@@ -12352,7 +12414,8 @@ function leafShapes(bounds) {
12352
12414
  /** Required: an unbounded list is an unbounded write. */
12353
12415
  maxItems: external_exports.number().int().min(1).max(100),
12354
12416
  uniqueItems: external_exports.boolean().optional(),
12355
- default: external_exports.array(external_exports.string().max(200)).max(100).optional()
12417
+ default: external_exports.array(external_exports.string().max(200)).max(100).optional(),
12418
+ "x-required-when": ValueConditionSchema.optional()
12356
12419
  }).strict();
12357
12420
  return { string, number, boolean, items, array };
12358
12421
  }
@@ -12372,9 +12435,9 @@ var ConfigurationPropertySchema = external_exports.discriminatedUnion("type", [
12372
12435
  var NestedStringPropertySchema = V2_LEAVES.string.omit({ "x-secret": true, writeOnly: true, "x-required-when": true }).extend({ group: ConfigurationGroupSchema.optional() });
12373
12436
  var V2_NESTED_LEAVES = {
12374
12437
  string: NestedStringPropertySchema,
12375
- number: V2_LEAVES.number.extend({ group: ConfigurationGroupSchema.optional() }),
12376
- boolean: V2_LEAVES.boolean.extend({ group: ConfigurationGroupSchema.optional() }),
12377
- array: V2_LEAVES.array.extend({ group: ConfigurationGroupSchema.optional() }),
12438
+ number: V2_LEAVES.number.omit({ "x-required-when": true }).extend({ group: ConfigurationGroupSchema.optional() }),
12439
+ boolean: V2_LEAVES.boolean.omit({ "x-required-when": true }).extend({ group: ConfigurationGroupSchema.optional() }),
12440
+ array: V2_LEAVES.array.omit({ "x-required-when": true }).extend({ group: ConfigurationGroupSchema.optional() }),
12378
12441
  items: V2_LEAVES.items
12379
12442
  };
12380
12443
  function childBagShape(child) {
@@ -12418,7 +12481,23 @@ function propertyUnion(levelsBelow, leafSet) {
12418
12481
  // discriminator, which is also what keeps a v1 document's
12419
12482
  // `items: {type: 'string'}` reading exactly as it always did.
12420
12483
  leafSet.array.extend({
12421
- items: external_exports.discriminatedUnion("type", [V2_LEAVES.items, groupItems])
12484
+ items: external_exports.discriminatedUnion("type", [V2_LEAVES.items, groupItems]),
12485
+ /**
12486
+ * Rules over the ROWS of this list — see {@link RowRulesSchema}.
12487
+ *
12488
+ * HERE RATHER THAN ON THE LEAF SHAPE, and that placement is the whole
12489
+ * enforcement. `leafShapes().array` is also a v1 list and a last-level
12490
+ * list, both of which hold STRINGS: a rule naming a field of a row would
12491
+ * name nothing there, and `uniqueItems` is already the right keyword for
12492
+ * comparing whole scalar entries. This branch is the only one in the chain
12493
+ * where a list can hold groups, so declaring the keyword here makes "rows
12494
+ * have fields" a property of the shape rather than a rule to remember.
12495
+ *
12496
+ * The refinement still has to refuse a rule on a list of STRINGS, because
12497
+ * `items` is a discriminated union INSIDE this branch — the shape narrows
12498
+ * where a row can exist, not whether this particular list has one.
12499
+ */
12500
+ "x-row-rules": RowRulesSchema.optional()
12422
12501
  }),
12423
12502
  /**
12424
12503
  * A group of related settings.
@@ -13121,6 +13200,9 @@ function canonicalJson(value) {
13121
13200
  }
13122
13201
 
13123
13202
  // ../product-configuration/dist/configuration-tree.js
13203
+ function immediateContainer(node) {
13204
+ return node.containers[node.containers.length - 1] ?? null;
13205
+ }
13124
13206
  function underList(node) {
13125
13207
  return node.containers.some((container) => container.kind === "array");
13126
13208
  }
@@ -13182,6 +13264,11 @@ function walkDeclarations(properties, ancestors, base, onLeaf, onContainer = ()
13182
13264
  walkDeclarations(children.properties, [...ancestors, container], segments, onLeaf, onContainer);
13183
13265
  }
13184
13266
  }
13267
+ function declaredDefault(property) {
13268
+ if (property.type === "object")
13269
+ return void 0;
13270
+ return property.default;
13271
+ }
13185
13272
  function leafAt(schema, path) {
13186
13273
  const declaration = declarationPathOf(path);
13187
13274
  return configurationLeaves(schema).find((leaf) => leaf.path === declaration) ?? null;
@@ -13194,6 +13281,29 @@ function declaresPath(schema, path) {
13194
13281
  function isPlainObject(value) {
13195
13282
  return typeof value === "object" && value !== null && !Array.isArray(value);
13196
13283
  }
13284
+ function expandDeclarationPath(configuration, declaration) {
13285
+ const out = [];
13286
+ const walk = (cursor, remaining, soFar) => {
13287
+ if (remaining.length === 0) {
13288
+ const path = configurationPath(...soFar);
13289
+ if (path !== null)
13290
+ out.push(path);
13291
+ return;
13292
+ }
13293
+ if (Array.isArray(cursor)) {
13294
+ cursor.forEach((element, index) => walk(element, remaining, [...soFar, index]));
13295
+ return;
13296
+ }
13297
+ if (!isPlainObject(cursor))
13298
+ return;
13299
+ const [head, ...rest] = remaining;
13300
+ if (head === void 0)
13301
+ return;
13302
+ walk(cursor[String(head)], rest, [...soFar, head]);
13303
+ };
13304
+ walk(configuration, segmentsOf(declaration), []);
13305
+ return out;
13306
+ }
13197
13307
  function valueAt(configuration, path) {
13198
13308
  let cursor = configuration;
13199
13309
  for (const segment of segmentsOf(path)) {
@@ -13255,6 +13365,7 @@ function conditionHolds(condition, configuration) {
13255
13365
  // ../product-configuration/dist/customer-configuration-input.js
13256
13366
  var ABSOLUTE_MAX_STRING = MAX_DECLARABLE_STRING_LENGTH;
13257
13367
  var MAX_REPORTED_UNDECLARED_KEYS = 10;
13368
+ var UNDECLARED_SETTING_MESSAGE = "This Product does not declare a setting with that name.";
13258
13369
  function isPlainObject2(value) {
13259
13370
  return typeof value === "object" && value !== null && !Array.isArray(value);
13260
13371
  }
@@ -13288,6 +13399,7 @@ function validateSubmittedConfiguration(schema, submitted, presence = {}) {
13288
13399
  const secrets = {};
13289
13400
  const configuration = validateObject({ properties: schema.properties, required: schema.required }, input, null, walk, { secrets, supplied });
13290
13401
  drain(walk, crossFieldIssues(schema, supplied, presence, configuration));
13402
+ drain(walk, rowAndReferenceIssues(schema, presence, configuration));
13291
13403
  if (walk.suppressed > 0) {
13292
13404
  walk.issues.push({
13293
13405
  field: "",
@@ -13338,7 +13450,7 @@ function validateObject(declaration, input, container, walk, root) {
13338
13450
  };
13339
13451
  const undeclared = Object.keys(input).filter((name) => !Object.hasOwn(declaration.properties, name));
13340
13452
  for (const name of undeclared.slice(0, MAX_REPORTED_UNDECLARED_KEYS)) {
13341
- report(walk, fieldFor(name), "This Product does not declare a setting with that name.");
13453
+ report(walk, fieldFor(name), UNDECLARED_SETTING_MESSAGE);
13342
13454
  }
13343
13455
  if (undeclared.length > MAX_REPORTED_UNDECLARED_KEYS) {
13344
13456
  report(walk, container === null ? "" : container, `${undeclared.length - MAX_REPORTED_UNDECLARED_KEYS} further settings are not declared by this Product.`);
@@ -13531,6 +13643,106 @@ function crossFieldIssues(schema, supplied, presence, configuration = {}) {
13531
13643
  }
13532
13644
  return issues;
13533
13645
  }
13646
+ function referenceOf(property) {
13647
+ return property?.["x-value-in"];
13648
+ }
13649
+ function rowRulesOf(property) {
13650
+ return property?.["x-row-rules"];
13651
+ }
13652
+ function comparableValue(value) {
13653
+ if (value === void 0 || value === null)
13654
+ return null;
13655
+ if (typeof value === "string")
13656
+ return value === "" ? null : JSON.stringify(value);
13657
+ if (typeof value === "number" || typeof value === "boolean")
13658
+ return JSON.stringify(value);
13659
+ return null;
13660
+ }
13661
+ function rowAndReferenceIssues(schema, presence, configuration) {
13662
+ const issues = [];
13663
+ const referenced = new Set(presence.referenced ?? []);
13664
+ for (const leaf of configurationLeaves(schema)) {
13665
+ const reference = referenceOf(leaf.property);
13666
+ if (reference === void 0)
13667
+ continue;
13668
+ const targetPath = parseConfigurationPath(reference.path);
13669
+ if (targetPath === null)
13670
+ continue;
13671
+ const allowed = new Set(valuesAt(configuration, targetPath).filter((value) => typeof value === "string"));
13672
+ for (const valuePath of expandDeclarationPath(configuration, leaf.path)) {
13673
+ if (referenced.has(valuePath))
13674
+ continue;
13675
+ const held = valueAt(configuration, valuePath);
13676
+ if (typeof held !== "string" || held === "")
13677
+ continue;
13678
+ if (allowed.has(held))
13679
+ continue;
13680
+ issues.push({
13681
+ field: valuePath,
13682
+ message: `This must be one of the values configured under ${labelOfPath(schema, targetPath)}. ` + (allowed.size === 0 ? "Nothing is configured there yet." : "No entry there currently holds this one.")
13683
+ });
13684
+ }
13685
+ }
13686
+ for (const container of configurationContainers(schema)) {
13687
+ if (container.kind !== "array")
13688
+ continue;
13689
+ const rules = rowRulesOf(container.property);
13690
+ if (rules === void 0)
13691
+ continue;
13692
+ for (const listPath of expandDeclarationPath(configuration, container.path)) {
13693
+ const rows = valueAt(configuration, listPath);
13694
+ if (!Array.isArray(rows))
13695
+ continue;
13696
+ const fieldOf = (index, name) => configurationPath(...pathSegments(listPath), index, name) ?? listPath;
13697
+ for (const name of rules.unique ?? []) {
13698
+ const firstAt = /* @__PURE__ */ new Map();
13699
+ rows.forEach((row, index) => {
13700
+ const held = comparableValue(isPlainObject2(row) ? row[name] : void 0);
13701
+ if (held === null)
13702
+ return;
13703
+ const earlier = firstAt.get(held);
13704
+ if (earlier === void 0) {
13705
+ firstAt.set(held, index);
13706
+ return;
13707
+ }
13708
+ issues.push({
13709
+ field: fieldOf(index, name),
13710
+ message: `${labelOfChild(container, name)} must be different in every entry; entry ${earlier + 1} already uses this one.`
13711
+ });
13712
+ });
13713
+ }
13714
+ for (const rule of rules.atMost ?? []) {
13715
+ let counted = 0;
13716
+ rows.forEach((row, index) => {
13717
+ const held = isPlainObject2(row) ? row[rule.property] : void 0;
13718
+ if (typeof held !== "string" || !rule.anyOf.includes(held))
13719
+ return;
13720
+ counted += 1;
13721
+ if (counted <= rule.max)
13722
+ return;
13723
+ issues.push({
13724
+ field: fieldOf(index, rule.property),
13725
+ message: rule.max === 0 ? `No entry may be set to this.` : `At most ${rule.max} ${rule.max === 1 ? "entry" : "entries"} may be set to this, and ${counted - 1} already ${counted - 1 === 1 ? "is" : "are"}.`
13726
+ });
13727
+ });
13728
+ }
13729
+ }
13730
+ }
13731
+ return issues;
13732
+ }
13733
+ function labelOfPath(schema, path) {
13734
+ const leaf = leafAt(schema, path);
13735
+ const titleOf = (node) => node?.property.title;
13736
+ const field = titleOf(leaf) ?? printableConfigurationPath(path);
13737
+ const container = leaf?.containers[0];
13738
+ const outer = container === void 0 ? void 0 : titleOf(container);
13739
+ return outer === void 0 ? `"${field}"` : `"${field}" in "${outer}"`;
13740
+ }
13741
+ function labelOfChild(container, name) {
13742
+ const items = container.property.items;
13743
+ const title = items?.properties?.[name]?.title;
13744
+ return title === void 0 ? `"${name}"` : `"${title}"`;
13745
+ }
13534
13746
  function conditionOf(property) {
13535
13747
  return property?.["x-required-when"];
13536
13748
  }
@@ -13539,19 +13751,45 @@ function labelOf(schema, name) {
13539
13751
  return property?.title === void 0 ? `"${name}"` : `"${property.title}"`;
13540
13752
  }
13541
13753
  function withoutCrossFieldRules(schema) {
13542
- const conditioned = Object.entries(schema.properties).some(([, property]) => property["x-required-when"] !== void 0);
13543
- if (conditioned) {
13544
- const properties = Object.fromEntries(Object.entries(schema.properties).map(([name, property]) => {
13545
- const { "x-required-when": _dropped, ...bare } = property;
13546
- return [name, bare];
13547
- }));
13548
- return withoutCrossFieldRules({ ...schema, properties });
13549
- }
13550
- if (schema.dependentRequired === void 0)
13551
- return schema;
13552
- const { dependentRequired: _removed, ...rest } = schema;
13754
+ const properties = withoutPropertyRules(schema.properties);
13755
+ const stripped = properties === null ? schema : { ...schema, properties };
13756
+ if (stripped.dependentRequired === void 0)
13757
+ return stripped;
13758
+ const { dependentRequired: _removed, ...rest } = stripped;
13553
13759
  return rest;
13554
13760
  }
13761
+ var PROPERTY_RULE_KEYWORDS = ["x-required-when", "x-value-in", "x-row-rules"];
13762
+ function withoutPropertyRules(properties) {
13763
+ let out = null;
13764
+ const edit = (name) => {
13765
+ out ??= { ...properties };
13766
+ const copy = { ...out[name] };
13767
+ out[name] = copy;
13768
+ return copy;
13769
+ };
13770
+ for (const [name, property] of Object.entries(properties)) {
13771
+ if (!isPlainObject2(property))
13772
+ continue;
13773
+ for (const keyword of PROPERTY_RULE_KEYWORDS) {
13774
+ if (property[keyword] === void 0)
13775
+ continue;
13776
+ delete edit(name)[keyword];
13777
+ }
13778
+ const children = out?.[name] ?? property;
13779
+ if (isPlainObject2(children["properties"])) {
13780
+ const bare = withoutPropertyRules(children["properties"]);
13781
+ if (bare !== null)
13782
+ edit(name)["properties"] = bare;
13783
+ }
13784
+ const items = children["items"];
13785
+ if (isPlainObject2(items) && isPlainObject2(items["properties"])) {
13786
+ const bare = withoutPropertyRules(items["properties"]);
13787
+ if (bare !== null)
13788
+ edit(name)["items"] = { ...items, properties: bare };
13789
+ }
13790
+ }
13791
+ return out;
13792
+ }
13555
13793
  function defaultOf(property) {
13556
13794
  switch (property.type) {
13557
13795
  case "string":
@@ -13604,7 +13842,11 @@ function checkString(field, property, value, issues) {
13604
13842
  return void 0;
13605
13843
  }
13606
13844
  if (property.pattern !== void 0 && !matches(property.pattern, value)) {
13607
- issues.push({ field, message: "Value is not in the required format." });
13845
+ const expectation = property.description;
13846
+ issues.push({
13847
+ field,
13848
+ message: expectation === void 0 ? "Value is not in the required format." : `Value is not in the required format. ${expectation.replace(/\s+$/, "")}`
13849
+ });
13608
13850
  return void 0;
13609
13851
  }
13610
13852
  return value;
@@ -14754,11 +14996,19 @@ function checkProductDefinition(definition, ctx) {
14754
14996
  if (condition === void 0)
14755
14997
  continue;
14756
14998
  const at = ["configuration", "properties", name, "x-required-when"];
14757
- if (!isSecretProperty(property)) {
14999
+ if ((definition.configuration.required ?? []).includes(name)) {
15000
+ ctx.addIssue({
15001
+ code: external_exports.ZodIssueCode.custom,
15002
+ path: [...at],
15003
+ message: `"${name}" is already required, so "x-required-when" can only narrow a rule that is already unconditional. Drop one: \`required\` is always, \`x-required-when\` is the condition.`
15004
+ });
15005
+ continue;
15006
+ }
15007
+ if (declaredDefault(property) !== void 0) {
14758
15008
  ctx.addIssue({
14759
15009
  code: external_exports.ZodIssueCode.custom,
14760
15010
  path: [...at],
14761
- message: `"${name}" is not a secret, so "x-required-when" has nothing to report through. Declare it "x-secret": true with "writeOnly": true, or use "dependentRequired" to require one ordinary setting alongside another.`
15011
+ message: `"${name}" declares a default, so it is never absent and this condition could never make it required. Remove the default, or the condition.`
14762
15012
  });
14763
15013
  continue;
14764
15014
  }
@@ -14834,6 +15084,8 @@ function checkProductDefinition(definition, ctx) {
14834
15084
  });
14835
15085
  }
14836
15086
  }
15087
+ checkValueReferences(definition.configuration, ctx);
15088
+ checkRowRules(definition.configuration, ctx);
14837
15089
  const provides = definition.serviceInterfaces?.provides ?? [];
14838
15090
  const provideKeys = provides.map((provide) => provide.key);
14839
15091
  const duplicateProvideKeys = provideKeys.filter((key, i) => provideKeys.indexOf(key) !== i);
@@ -14945,6 +15197,203 @@ function checkProductDefinition(definition, ctx) {
14945
15197
  });
14946
15198
  }
14947
15199
  }
15200
+ function declarationIssuePath(node) {
15201
+ const out = ["configuration"];
15202
+ for (const container of node.containers) {
15203
+ out.push("properties", container.name);
15204
+ if (container.kind === "array")
15205
+ out.push("items");
15206
+ }
15207
+ out.push("properties", node.name);
15208
+ return out;
15209
+ }
15210
+ function directChildren(leaves, container) {
15211
+ return leaves.filter((leaf) => immediateContainer(leaf)?.path === container.path);
15212
+ }
15213
+ function valuesOutsideSpace(configuration, leaf, values) {
15214
+ const single = withoutCrossFieldRules({
15215
+ ...configuration,
15216
+ properties: { [leaf.name]: leaf.property },
15217
+ required: [leaf.name]
15218
+ });
15219
+ return values.filter((value) => !validateSubmittedConfiguration(single, { [leaf.name]: value }).ok);
15220
+ }
15221
+ function checkValueReferences(configuration, ctx) {
15222
+ const leaves = configurationLeaves(configuration);
15223
+ for (const leaf of leaves) {
15224
+ const reference = leaf.property["x-value-in"];
15225
+ if (reference === void 0)
15226
+ continue;
15227
+ const at = [...declarationIssuePath(leaf), "x-value-in"];
15228
+ const here = printableConfigurationPath(leaf.path);
15229
+ if (isSecretProperty(leaf.property)) {
15230
+ ctx.addIssue({
15231
+ code: external_exports.ZodIssueCode.custom,
15232
+ path: at,
15233
+ message: `"${here}" is a secret. The platform never reads a secret's value, so it could never check one against a set of values.`
15234
+ });
15235
+ continue;
15236
+ }
15237
+ if (leaf.property.enum !== void 0) {
15238
+ ctx.addIssue({
15239
+ code: external_exports.ZodIssueCode.custom,
15240
+ path: at,
15241
+ message: `"${here}" already fixes its values with "enum". A setting takes its values from the contract or from another setting, not from both.`
15242
+ });
15243
+ continue;
15244
+ }
15245
+ if (declaredDefault(leaf.property) !== void 0) {
15246
+ ctx.addIssue({
15247
+ code: external_exports.ZodIssueCode.custom,
15248
+ path: at,
15249
+ message: `"${here}" declares a default, which would be written into every row and then refused until the setting it points at happens to hold the same value. Remove the default.`
15250
+ });
15251
+ continue;
15252
+ }
15253
+ const parsed = parseConfigurationPath(reference.path);
15254
+ const target = parsed === null ? null : leafAt(configuration, parsed);
15255
+ if (target === null) {
15256
+ const isGroup = parsed !== null && declaresPath(configuration, parsed);
15257
+ ctx.addIssue({
15258
+ code: external_exports.ZodIssueCode.custom,
15259
+ path: [...at, "path"],
15260
+ message: isGroup ? `"${reference.path}" is a group of settings, which holds no value to choose from. Name one setting inside it.` : `"${reference.path}" is not a declared customer configuration property.`
15261
+ });
15262
+ continue;
15263
+ }
15264
+ if (target.path === leaf.path) {
15265
+ ctx.addIssue({
15266
+ code: external_exports.ZodIssueCode.custom,
15267
+ path: [...at, "path"],
15268
+ message: `"${here}" cannot take its values from itself.`
15269
+ });
15270
+ continue;
15271
+ }
15272
+ if (target.property.type !== "string") {
15273
+ ctx.addIssue({
15274
+ code: external_exports.ZodIssueCode.custom,
15275
+ path: [...at, "path"],
15276
+ message: `"${reference.path}" is a ${target.property.type} setting; a reference compares string values.`
15277
+ });
15278
+ continue;
15279
+ }
15280
+ if (isSecretProperty(target.property)) {
15281
+ ctx.addIssue({
15282
+ code: external_exports.ZodIssueCode.custom,
15283
+ path: [...at, "path"],
15284
+ message: `"${reference.path}" is a secret. The platform never reads a secret's value, so it could never be the set a reference is checked against.`
15285
+ });
15286
+ continue;
15287
+ }
15288
+ }
15289
+ }
15290
+ function checkRowRules(configuration, ctx) {
15291
+ const leaves = configurationLeaves(configuration);
15292
+ for (const leaf of leaves) {
15293
+ if (leaf.property["x-row-rules"] === void 0)
15294
+ continue;
15295
+ ctx.addIssue({
15296
+ code: external_exports.ZodIssueCode.custom,
15297
+ path: [...declarationIssuePath(leaf), "x-row-rules"],
15298
+ message: `"${printableConfigurationPath(leaf.path)}" is a list of text entries, whose rows have no fields to name. Use "uniqueItems" to compare whole entries, or declare "items" as a group.`
15299
+ });
15300
+ }
15301
+ for (const container of configurationContainers(configuration)) {
15302
+ const rules = container.property["x-row-rules"];
15303
+ if (rules === void 0)
15304
+ continue;
15305
+ const at = [...declarationIssuePath(container), "x-row-rules"];
15306
+ const here = printableConfigurationPath(container.path);
15307
+ if (rules.unique === void 0 && rules.atMost === void 0) {
15308
+ ctx.addIssue({
15309
+ code: external_exports.ZodIssueCode.custom,
15310
+ path: at,
15311
+ message: `"${here}" declares "x-row-rules" with no rule in it. Declare "unique", "atMost", or both.`
15312
+ });
15313
+ continue;
15314
+ }
15315
+ const children = directChildren(leaves, container);
15316
+ const childAt = (name) => children.find((child) => child.name === name);
15317
+ const maxItems = container.property.maxItems;
15318
+ const seen = /* @__PURE__ */ new Set();
15319
+ (rules.unique ?? []).forEach((name, index) => {
15320
+ if (seen.has(name)) {
15321
+ ctx.addIssue({
15322
+ code: external_exports.ZodIssueCode.custom,
15323
+ path: [...at, "unique", index],
15324
+ message: `"${name}" is named twice.`
15325
+ });
15326
+ return;
15327
+ }
15328
+ seen.add(name);
15329
+ const child = childAt(name);
15330
+ if (child === void 0) {
15331
+ ctx.addIssue({
15332
+ code: external_exports.ZodIssueCode.custom,
15333
+ path: [...at, "unique", index],
15334
+ message: `"${name}" is not a setting declared on a row of "${here}".`
15335
+ });
15336
+ return;
15337
+ }
15338
+ if (child.property.type === "array") {
15339
+ ctx.addIssue({
15340
+ code: external_exports.ZodIssueCode.custom,
15341
+ path: [...at, "unique", index],
15342
+ message: `"${name}" is a list, which holds no single value to compare across rows.`
15343
+ });
15344
+ }
15345
+ if (child.property.type === "boolean") {
15346
+ ctx.addIssue({
15347
+ code: external_exports.ZodIssueCode.custom,
15348
+ path: [...at, "unique", index],
15349
+ message: `"${name}" is a true/false setting, so requiring it to be unique would cap "${here}" at two rows rather than make anything distinct. Use "atMost" to limit how many rows may answer one way.`
15350
+ });
15351
+ }
15352
+ });
15353
+ (rules.atMost ?? []).forEach((rule, index) => {
15354
+ const ruleAt = [...at, "atMost", index];
15355
+ const child = childAt(rule.property);
15356
+ if (child === void 0) {
15357
+ ctx.addIssue({
15358
+ code: external_exports.ZodIssueCode.custom,
15359
+ path: [...ruleAt, "property"],
15360
+ message: `"${rule.property}" is not a setting declared on a row of "${here}".`
15361
+ });
15362
+ return;
15363
+ }
15364
+ if (child.property.type !== "string") {
15365
+ ctx.addIssue({
15366
+ code: external_exports.ZodIssueCode.custom,
15367
+ path: [...ruleAt, "property"],
15368
+ message: `"${rule.property}" is a ${child.property.type} setting; a row rule compares string values.`
15369
+ });
15370
+ return;
15371
+ }
15372
+ if (new Set(rule.anyOf).size !== rule.anyOf.length) {
15373
+ ctx.addIssue({
15374
+ code: external_exports.ZodIssueCode.custom,
15375
+ path: [...ruleAt, "anyOf"],
15376
+ message: "Repeated values in `anyOf`."
15377
+ });
15378
+ }
15379
+ if (maxItems !== void 0 && rule.max >= maxItems) {
15380
+ ctx.addIssue({
15381
+ code: external_exports.ZodIssueCode.custom,
15382
+ path: [...ruleAt, "max"],
15383
+ message: `"${here}" holds at most ${maxItems} rows, so a limit of ${rule.max} could never be exceeded. Lower it, or remove the rule.`
15384
+ });
15385
+ }
15386
+ const outside = valuesOutsideSpace(configuration, child, rule.anyOf);
15387
+ if (outside.length > 0) {
15388
+ ctx.addIssue({
15389
+ code: external_exports.ZodIssueCode.custom,
15390
+ path: [...ruleAt, "anyOf"],
15391
+ message: `"${rule.property}" never accepts ${outside.map((value) => `"${value}"`).join(", ")}, so this rule could never count a row.`
15392
+ });
15393
+ }
15394
+ });
15395
+ }
15396
+ }
14948
15397
 
14949
15398
  // ../product-contracts/dist/announcements.js
14950
15399
  var ANNOUNCED_PRODUCTS = Object.freeze([
@@ -16801,8 +17250,16 @@ var MANIFEST_ACTIONS = [
16801
17250
  "ADOPT",
16802
17251
  /** The key names a resource and something in the document differs. */
16803
17252
  "UPDATE",
16804
- /** The key names a resource and the document matches it. A pipeline that runs
16805
- * on every push and changes nothing reports this, and it is SUCCESS. */
17253
+ /** The key names a resource and this REQUEST asks for nothing: no field
17254
+ * differs and no secret would be written.
17255
+ *
17256
+ * NOT "the document matches it", which is how it read and is a stronger claim
17257
+ * than the planner makes. What a document OMITS is left alone rather than
17258
+ * compared — a manifest naming no connections answers this beside a resource
17259
+ * that holds several — so this never means the file mirrors stored state.
17260
+ *
17261
+ * A pipeline that runs on every push and changes nothing reports it, and it
17262
+ * is SUCCESS. */
16806
17263
  "NO_CHANGE",
16807
17264
  /** `adopt` points at a resource that was DELETED. Its own verb, because both
16808
17265
  * alternatives are wrong: `NO_CHANGE` would call a missing resource fine, and
@@ -17089,7 +17546,9 @@ function secretWriteConsequence(plan) {
17089
17546
  const lines = ["the value you send REPLACES the stored one, and the resource redeploys."];
17090
17547
  if (plan.action === "UPDATE" && (plan.changes ?? []).length === 0) {
17091
17548
  lines.push("Nothing else changed \u2014 sending a secret is itself the change.");
17092
- lines.push("Omit --secret on runs that are not rotating one to get NO_CHANGE.");
17549
+ lines.push(
17550
+ "Omit --secret on a run that is not setting or changing one; that is what answers NO_CHANGE."
17551
+ );
17093
17552
  }
17094
17553
  return lines;
17095
17554
  }
@@ -17893,9 +18352,9 @@ async function runGet(parsed) {
17893
18352
  lines.push("", " Outputs");
17894
18353
  const width = Math.max(...names.map((name) => name.length));
17895
18354
  for (const name of names) {
17896
- lines.push(
17897
- ` ${name.padEnd(width)} ${isSecret2(name) ? "[secret]" : String(outputs[name] ?? "")}`
17898
- );
18355
+ const value = outputs[name];
18356
+ const shown = isSecret2(name) ? "[secret]" : value === void 0 || value === null ? "[not available]" : String(value);
18357
+ lines.push(` ${name.padEnd(width)} ${shown}`);
17899
18358
  const meaning = declared.find((entry) => entry["key"] === name);
17900
18359
  const description = meaning?.["description"];
17901
18360
  if (typeof description === "string") {
@@ -18085,14 +18544,41 @@ function renderProduct(product) {
18085
18544
  lines.push(` ${setting.label} ${setting.type ?? ""}`.trimEnd());
18086
18545
  }
18087
18546
  }
18088
- const needed = (product.dependencies ?? []).filter((dependency) => dependency.required);
18089
- if (needed.length > 0) {
18090
- lines.push("", " Requires a connection to another resource:");
18091
- for (const dependency of needed) {
18092
- lines.push(` ${dependency.productId ?? "(a Product providing a service)"}`);
18547
+ const dependencies = product.dependencies ?? [];
18548
+ if (dependencies.length > 0) {
18549
+ lines.push("", " Connections to another resource");
18550
+ for (const dependency of dependencies) {
18551
+ const when = dependency.requiredWhen?.property;
18552
+ const need = dependency.required ? "required" : when === void 0 ? "optional" : `required when ${when} is ${(dependency.requiredWhen?.anyOf ?? []).join(" or ")}`;
18553
+ lines.push(` ${dependency.key ?? "(no key)"} ${need}`);
18554
+ const capability = dependency.interface?.id;
18555
+ if (capability !== void 0) {
18556
+ lines.push(
18557
+ ` ${capability}${dependency.interface?.version ? `/${dependency.interface.version}` : ""}`
18558
+ );
18559
+ } else if (dependency.productId !== void 0) {
18560
+ lines.push(` ${dependency.productId}`);
18561
+ }
18093
18562
  if (dependency.reason) lines.push(` ${dependency.reason}`);
18094
18563
  }
18095
18564
  }
18565
+ const outputs = product.outputs ?? [];
18566
+ if (outputs.length > 0) {
18567
+ lines.push("", " Publishes");
18568
+ for (const output of outputs) {
18569
+ lines.push(
18570
+ ` ${output.key ?? ""} ${output.secret === true ? "secret" : output.type ?? ""}`.trimEnd()
18571
+ );
18572
+ }
18573
+ }
18574
+ const conditions = product.conditions ?? [];
18575
+ if (conditions.length > 0) {
18576
+ lines.push("", " Needs setting up outside the platform");
18577
+ for (const condition of conditions) {
18578
+ lines.push(` ${condition.key ?? ""} ${condition.displayName ?? ""}`.trimEnd());
18579
+ }
18580
+ }
18581
+ lines.push("", " `--output json` carries the whole declaration, this is a readable subset.");
18096
18582
  return lines.join("\n");
18097
18583
  }
18098
18584
  function wrap(text, width) {
@@ -18441,6 +18927,12 @@ Run \`vfac get product-instance pri_\u2026\` to see which ones this credential m
18441
18927
  `);
18442
18928
  return 2;
18443
18929
  }
18930
+ if (parsed.booleans.has("wait")) {
18931
+ process.stderr.write(
18932
+ "`--wait` belongs to `vfac apply`; `vfac lifecycle` does not support it.\nThis command answers with an operation id. Follow it with:\n vfac get operation op_\u2026\nand branch on the status it reports: SUCCEEDED, FAILED or CANCELLED. That\ncommand's exit code says whether the READ worked, never whether the operation\ndid \u2014 so a script that only checks the exit code reports a failed deploy as a\nsuccess.\n"
18933
+ );
18934
+ return 2;
18935
+ }
18444
18936
  if (command === "delete" && !parsed.booleans.has("yes")) {
18445
18937
  process.stderr.write(
18446
18938
  `delete removes ${instanceId} and everything it is serving. This cannot be undone.
@@ -18547,7 +19039,7 @@ var USAGE = `vfac \u2014 deploy Varde Flyt resources from a manifest
18547
19039
  vfac manifest init --product <productId> --name <name> [--key <key>]
18548
19040
  [--region <region>] [--profile <profile>]
18549
19041
  vfac validate -f resource.yaml
18550
- vfac plan -f resource.yaml
19042
+ vfac plan -f resource.yaml [--secret NAME=env:VAR]
18551
19043
  vfac apply -f resource.yaml [--secret NAME=env:VAR] [--adopt pri_\u2026] [--wait]
18552
19044
  vfac get product-instances
18553
19045
  vfac get product-instance pri_\u2026 what it is, and its outputs
@@ -18564,9 +19056,12 @@ Options
18564
19056
  --project the Project to act in. Defaults to the stored context
18565
19057
  --endpoint your organization's portal. Defaults to VFAC_ENDPOINT, then the
18566
19058
  stored context
18567
- --secret NAME=env:VARIABLE \u2014 the value is read from the environment.
18568
- A value on the command line is in your shell history and in the
18569
- process table, so it is refused
19059
+ --secret plan and apply: NAME=env:VARIABLE \u2014 the value is read from the
19060
+ environment. Plan a run that sets a secret with the same
19061
+ --secret inputs the apply will carry; without them the plan
19062
+ answers about a different request. A value on the command line
19063
+ is in your shell history and in the process table, so it is
19064
+ refused
18570
19065
  --product which Product to write a starting manifest for
18571
19066
  --name what a person will call the resource
18572
19067
  --key its stable identity. Derived from --name when omitted
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@varde-flyt/vfac",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Deploy and manage Varde Flyt Products from a manifest.",
5
5
  "repository": {
6
6
  "type": "git",