@varde-flyt/vfac 0.9.0 → 0.10.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 +5 -3
  2. package/dist/vfac.mjs +241 -23
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -27,9 +27,11 @@ vfac whoami
27
27
  ```
28
28
 
29
29
  `doctor` checks the endpoint, which Project this terminal has selected, the API,
30
- whether this client is new enough for the platform it is pointed at, your
31
- credential and what that credential reaches, in that order, and reports the first
32
- thing that is actually wrong. A platform declares the oldest client it supports;
30
+ whether this client is new enough for the platform it is pointed at, how this
31
+ terminal is signed in and what that credential reaches, in that order, and reports
32
+ the first thing that is actually wrong. Both ways of signing in count: a Context
33
+ Gate exchanged from `VFAC_CREDENTIAL_ID` and `VFAC_CREDENTIAL_SECRET`, or a
34
+ sign-in from `vfac login` that is still valid. A platform declares the oldest client it supports;
33
35
  below that, `doctor` fails and says so before sending your credential anywhere.
34
36
 
35
37
  The selected Project and the Projects your credential reaches are two different
package/dist/vfac.mjs CHANGED
@@ -7793,7 +7793,7 @@ async function ensureSession(args) {
7793
7793
  const endpoint = checked.endpoint;
7794
7794
  const credential = credentialFromEnvironment();
7795
7795
  if (args.forceExchange !== true && sessionUsable(stored.session, endpoint, args.now ?? /* @__PURE__ */ new Date(), credential?.keyId)) {
7796
- return { ok: true, ready: { endpoint, token: stored.session.token, stored } };
7796
+ return { ok: true, ready: { endpoint, token: stored.session.token, stored, via: "stored" } };
7797
7797
  }
7798
7798
  const signedIn = await signIn({
7799
7799
  endpoint,
@@ -7802,7 +7802,16 @@ async function ensureSession(args) {
7802
7802
  if (!signedIn.ok) return { ok: false, message: signedIn.message };
7803
7803
  const next = { ...stored, session: signedIn.session };
7804
7804
  writeStored(next);
7805
- return { ok: true, ready: { endpoint, token: signedIn.session.token, stored: next } };
7805
+ return {
7806
+ ok: true,
7807
+ ready: {
7808
+ endpoint,
7809
+ token: signedIn.session.token,
7810
+ stored: next,
7811
+ via: "exchanged",
7812
+ ...signedIn.gate ? { gate: signedIn.gate } : {}
7813
+ }
7814
+ };
7806
7815
  }
7807
7816
  function resolveProject(flag, stored) {
7808
7817
  return flag ?? stored.context?.projectId ?? null;
@@ -7922,8 +7931,9 @@ function renderOperation(operationId, data) {
7922
7931
  const retryable = failure["retryable"];
7923
7932
  if (typeof retryable === "boolean") {
7924
7933
  const again = data["type"] === "PRODUCT_INSTANCE_UPGRADE" ? "`vfac lifecycle upgrade <pri_\u2026>`" : "`vfac lifecycle reconcile <pri_\u2026>`";
7934
+ const changeIt = data["type"] === "PRODUCT_INSTANCE_UPGRADE" ? "" : ", or `vfac apply` a corrected manifest";
7925
7935
  lines.push(
7926
- retryable ? ` Another attempt can succeed: ${again}.` : ` Repeating this unchanged fails again \u2014 fix what the lines above name, then ${again}.`
7936
+ retryable ? ` Another attempt can succeed: ${again}.` : ` Repeating this unchanged fails again \u2014 fix what the lines above name, then ${again}${changeIt}.`
7927
7937
  );
7928
7938
  }
7929
7939
  }
@@ -12224,6 +12234,16 @@ var ConfigurationGroupSchema = external_exports.enum([
12224
12234
  ]);
12225
12235
  var CONFIGURATION_PROPERTY_NAME_REGEX = /^[a-z][a-zA-Z0-9]{1,47}$/;
12226
12236
  var PropertyNameSchema = external_exports.string().regex(CONFIGURATION_PROPERTY_NAME_REGEX, "Configuration property names must be camelCase, start with a lowercase letter, and be 2\u201348 characters.");
12237
+ var ValueConditionSchema = external_exports.object({
12238
+ /**
12239
+ * The setting, as a canonical path — or a bare root key, which is the same
12240
+ * string at depth 1. With `anyRow`, a DECLARATION path: `/servers/kind`
12241
+ * names the setting each row answers, never one row's answer.
12242
+ */
12243
+ path: external_exports.string().min(1).max(400),
12244
+ anyOf: external_exports.array(external_exports.string().min(1).max(200)).min(1).max(20),
12245
+ anyRow: external_exports.boolean().optional()
12246
+ }).strict();
12227
12247
  var TitleSchema = external_exports.string().min(2).max(80).describe("Customer-facing field label.");
12228
12248
  var V1_LEAF_BOUNDS = {
12229
12249
  help: 400,
@@ -12280,7 +12300,24 @@ function leafShapes(bounds) {
12280
12300
  * without the other a validation error.
12281
12301
  */
12282
12302
  writeOnly: external_exports.boolean().optional(),
12283
- "x-secret": external_exports.boolean().optional()
12303
+ "x-secret": external_exports.boolean().optional(),
12304
+ /**
12305
+ * The setting VALUE that makes this credential necessary.
12306
+ *
12307
+ * WHY IT CANNOT BE `dependentRequired`. That mechanism is keyed on
12308
+ * PRESENCE — "if the author answered X, they must also answer Y" — and
12309
+ * says so in as many words: "PRESENCE, NEVER A VALUE. A caller passes
12310
+ * NAMES." A Product whose credential is needed only when a setting has one
12311
+ * particular value could not say so, so `vfac plan`'s `requiredSecrets`
12312
+ * could not warn before a deploy, and the refusal arrived at apply.
12313
+ *
12314
+ * ON A SECRET ONLY, and that is narrower than the mechanism could be. A
12315
+ * value-conditioned requirement for an ORDINARY setting is a real gap too,
12316
+ * but it changes what every door accepts for every property; this changes
12317
+ * what `requiredSecrets` reports, which is one answer with one reader.
12318
+ * Opening it wider is a separate decision with its own blast radius.
12319
+ */
12320
+ "x-required-when": ValueConditionSchema.optional()
12284
12321
  }).strict();
12285
12322
  withJsonSchemaAnnex(string, SECRET_COUPLING_ANNEX);
12286
12323
  const number = external_exports.object({
@@ -12332,7 +12369,7 @@ var ConfigurationPropertySchema = external_exports.discriminatedUnion("type", [
12332
12369
  BooleanPropertySchema,
12333
12370
  ArrayPropertySchema
12334
12371
  ]);
12335
- var NestedStringPropertySchema = V2_LEAVES.string.omit({ "x-secret": true, writeOnly: true }).extend({ group: ConfigurationGroupSchema.optional() });
12372
+ var NestedStringPropertySchema = V2_LEAVES.string.omit({ "x-secret": true, writeOnly: true, "x-required-when": true }).extend({ group: ConfigurationGroupSchema.optional() });
12336
12373
  var V2_NESTED_LEAVES = {
12337
12374
  string: NestedStringPropertySchema,
12338
12375
  number: V2_LEAVES.number.extend({ group: ConfigurationGroupSchema.optional() }),
@@ -13038,6 +13075,9 @@ function pathSegments(path) {
13038
13075
  const raw = path.startsWith("/") ? path.slice(1).split("/") : [path];
13039
13076
  return raw.map((segment) => isIndexSegment(segment) ? Number(segment) : segment);
13040
13077
  }
13078
+ function rootSegment(path) {
13079
+ return String(pathSegments(path)[0]);
13080
+ }
13041
13081
  function declarationPathOf(path) {
13042
13082
  const segments = pathSegments(path).filter((segment) => typeof segment !== "number");
13043
13083
  return configurationPath(...segments) ?? path;
@@ -13151,11 +13191,71 @@ function declaresPath(schema, path) {
13151
13191
  return true;
13152
13192
  return configurationContainers(schema).some((container) => container.path === declarationPathOf(path));
13153
13193
  }
13194
+ function isPlainObject(value) {
13195
+ return typeof value === "object" && value !== null && !Array.isArray(value);
13196
+ }
13197
+ function valueAt(configuration, path) {
13198
+ let cursor = configuration;
13199
+ for (const segment of segmentsOf(path)) {
13200
+ if (typeof segment === "number") {
13201
+ if (!Array.isArray(cursor))
13202
+ return void 0;
13203
+ cursor = cursor[segment];
13204
+ continue;
13205
+ }
13206
+ if (!isPlainObject(cursor))
13207
+ return void 0;
13208
+ cursor = cursor[segment];
13209
+ }
13210
+ return cursor;
13211
+ }
13212
+ function valuesAt(configuration, path) {
13213
+ let cursors = [configuration];
13214
+ for (const segment of segmentsOf(path)) {
13215
+ const next = [];
13216
+ for (const cursor of cursors) {
13217
+ if (typeof segment === "number") {
13218
+ if (Array.isArray(cursor))
13219
+ next.push(cursor[segment]);
13220
+ continue;
13221
+ }
13222
+ if (!isPlainObject(cursor))
13223
+ continue;
13224
+ const value = cursor[segment];
13225
+ if (Array.isArray(value))
13226
+ next.push(...value);
13227
+ else
13228
+ next.push(value);
13229
+ }
13230
+ cursors = next;
13231
+ }
13232
+ return cursors.filter((value) => value !== void 0);
13233
+ }
13234
+ function segmentsOf(path) {
13235
+ const raw = path.startsWith("/") ? path.slice(1).split("/") : [path];
13236
+ return raw.map((segment) => /^\d+$/.test(segment) ? Number(segment) : segment);
13237
+ }
13238
+
13239
+ // ../product-configuration/dist/value-condition.js
13240
+ function conditionSetting(condition) {
13241
+ return "path" in condition ? condition.path : condition.property;
13242
+ }
13243
+ function conditionRoot(condition) {
13244
+ const path = parseConfigurationPath(conditionSetting(condition));
13245
+ return path === null ? conditionSetting(condition) : rootSegment(path);
13246
+ }
13247
+ function conditionHolds(condition, configuration) {
13248
+ const path = parseConfigurationPath(conditionSetting(condition));
13249
+ if (path === null)
13250
+ return false;
13251
+ const matches2 = (held) => typeof held === "string" && condition.anyOf.includes(held);
13252
+ return "anyRow" in condition && condition.anyRow === true ? valuesAt(configuration, path).some(matches2) : matches2(valueAt(configuration, path));
13253
+ }
13154
13254
 
13155
13255
  // ../product-configuration/dist/customer-configuration-input.js
13156
13256
  var ABSOLUTE_MAX_STRING = MAX_DECLARABLE_STRING_LENGTH;
13157
13257
  var MAX_REPORTED_UNDECLARED_KEYS = 10;
13158
- function isPlainObject(value) {
13258
+ function isPlainObject2(value) {
13159
13259
  return typeof value === "object" && value !== null && !Array.isArray(value);
13160
13260
  }
13161
13261
  function report(walk, field, message) {
@@ -13177,17 +13277,17 @@ function validateSubmittedConfiguration(schema, submitted, presence = {}) {
13177
13277
  referenced: new Set(presence.referenced ?? [])
13178
13278
  };
13179
13279
  const supplied = /* @__PURE__ */ new Set();
13180
- if (submitted !== void 0 && submitted !== null && !isPlainObject(submitted)) {
13280
+ if (submitted !== void 0 && submitted !== null && !isPlainObject2(submitted)) {
13181
13281
  return {
13182
13282
  ok: false,
13183
13283
  issues: [{ field: "", message: "Configuration must be an object." }],
13184
13284
  partial: {}
13185
13285
  };
13186
13286
  }
13187
- const input = isPlainObject(submitted) ? submitted : {};
13287
+ const input = isPlainObject2(submitted) ? submitted : {};
13188
13288
  const secrets = {};
13189
13289
  const configuration = validateObject({ properties: schema.properties, required: schema.required }, input, null, walk, { secrets, supplied });
13190
- drain(walk, crossFieldIssues(schema, supplied, presence));
13290
+ drain(walk, crossFieldIssues(schema, supplied, presence, configuration));
13191
13291
  if (walk.suppressed > 0) {
13192
13292
  walk.issues.push({
13193
13293
  field: "",
@@ -13342,7 +13442,7 @@ function validateContainer(name, property, children, value, field, walk) {
13342
13442
  if (children.kind === "object") {
13343
13443
  if (value === void 0)
13344
13444
  return void 0;
13345
- if (!isPlainObject(value)) {
13445
+ if (!isPlainObject2(value)) {
13346
13446
  report(walk, field, "Expected a group of settings.");
13347
13447
  return void 0;
13348
13448
  }
@@ -13369,7 +13469,7 @@ function validateContainer(name, property, children, value, field, walk) {
13369
13469
  report(walk, field, "This configuration has too many values.");
13370
13470
  break;
13371
13471
  }
13372
- if (!isPlainObject(element)) {
13472
+ if (!isPlainObject2(element)) {
13373
13473
  report(walk, `${field}`, `Item ${index + 1} is not a group of settings.`);
13374
13474
  continue;
13375
13475
  }
@@ -13385,9 +13485,10 @@ function validateContainer(name, property, children, value, field, walk) {
13385
13485
  }
13386
13486
  return out;
13387
13487
  }
13388
- function crossFieldIssues(schema, supplied, presence) {
13488
+ function crossFieldIssues(schema, supplied, presence, configuration = {}) {
13389
13489
  const clauses = Object.entries(schema.dependentRequired ?? {});
13390
- if (clauses.length === 0)
13490
+ const conditional = Object.entries(schema.properties).filter(([, property]) => conditionOf(property) !== void 0);
13491
+ if (clauses.length === 0 && conditional.length === 0)
13391
13492
  return [];
13392
13493
  const alsoPresent = /* @__PURE__ */ new Set([...presence.present ?? [], ...presence.referenced ?? []]);
13393
13494
  const unknown = new Set(presence.unknown ?? []);
@@ -13410,13 +13511,42 @@ function crossFieldIssues(schema, supplied, presence) {
13410
13511
  });
13411
13512
  }
13412
13513
  }
13514
+ for (const [name, property] of conditional) {
13515
+ const condition = conditionOf(property);
13516
+ if (condition === void 0 || !known(name) || held(name))
13517
+ continue;
13518
+ if (!conditionHolds(condition, configuration))
13519
+ continue;
13520
+ if (reported.has(name))
13521
+ continue;
13522
+ reported.add(name);
13523
+ issues.push({
13524
+ field: name,
13525
+ // THE SETTING THAT MADE IT NECESSARY, never the value it holds. A message
13526
+ // naming the value would be fine here and wrong one refactor later, when
13527
+ // somebody conditions a secret on another secret — which the publish rules
13528
+ // refuse precisely because nothing may read one.
13529
+ message: `This setting is required because of what ${labelOf(schema, conditionRoot(condition))} is set to.`
13530
+ });
13531
+ }
13413
13532
  return issues;
13414
13533
  }
13534
+ function conditionOf(property) {
13535
+ return property?.["x-required-when"];
13536
+ }
13415
13537
  function labelOf(schema, name) {
13416
13538
  const property = schema.properties[name];
13417
13539
  return property?.title === void 0 ? `"${name}"` : `"${property.title}"`;
13418
13540
  }
13419
13541
  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
+ }
13420
13550
  if (schema.dependentRequired === void 0)
13421
13551
  return schema;
13422
13552
  const { dependentRequired: _removed, ...rest } = schema;
@@ -14619,6 +14749,91 @@ function checkProductDefinition(definition, ctx) {
14619
14749
  });
14620
14750
  }
14621
14751
  });
14752
+ for (const [name, property] of Object.entries(definition.configuration.properties ?? {})) {
14753
+ const condition = property["x-required-when"];
14754
+ if (condition === void 0)
14755
+ continue;
14756
+ const at = ["configuration", "properties", name, "x-required-when"];
14757
+ if (!isSecretProperty(property)) {
14758
+ ctx.addIssue({
14759
+ code: external_exports.ZodIssueCode.custom,
14760
+ 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.`
14762
+ });
14763
+ continue;
14764
+ }
14765
+ if (new Set(condition.anyOf).size !== condition.anyOf.length) {
14766
+ ctx.addIssue({
14767
+ code: external_exports.ZodIssueCode.custom,
14768
+ path: [...at, "anyOf"],
14769
+ message: "Repeated values in `anyOf`."
14770
+ });
14771
+ }
14772
+ if (condition.path === name) {
14773
+ ctx.addIssue({
14774
+ code: external_exports.ZodIssueCode.custom,
14775
+ path: [...at, "path"],
14776
+ message: `"${name}" cannot be conditioned on its own value.`
14777
+ });
14778
+ continue;
14779
+ }
14780
+ const parsed = parseConfigurationPath(condition.path);
14781
+ const leaf = parsed === null ? null : leafAt(definition.configuration, parsed);
14782
+ if (leaf === null) {
14783
+ const isGroup = parsed !== null && declaresPath(definition.configuration, parsed);
14784
+ ctx.addIssue({
14785
+ code: external_exports.ZodIssueCode.custom,
14786
+ path: [...at, "path"],
14787
+ 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.`
14788
+ });
14789
+ continue;
14790
+ }
14791
+ if (underList(leaf) && condition.anyRow !== true) {
14792
+ ctx.addIssue({
14793
+ code: external_exports.ZodIssueCode.custom,
14794
+ path: [...at, "path"],
14795
+ 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.`
14796
+ });
14797
+ continue;
14798
+ }
14799
+ if (!underList(leaf) && condition.anyRow === true) {
14800
+ ctx.addIssue({
14801
+ code: external_exports.ZodIssueCode.custom,
14802
+ path: [...at, "anyRow"],
14803
+ message: `"${condition.path}" is not inside a list, so there are no rows to quantify over.`
14804
+ });
14805
+ }
14806
+ const target = leaf.property;
14807
+ if (target.type !== "string") {
14808
+ ctx.addIssue({
14809
+ code: external_exports.ZodIssueCode.custom,
14810
+ path: [...at, "path"],
14811
+ message: `"${condition.path}" is a ${target.type} setting; a condition compares string values.`
14812
+ });
14813
+ continue;
14814
+ }
14815
+ if (isSecretProperty(target)) {
14816
+ ctx.addIssue({
14817
+ code: external_exports.ZodIssueCode.custom,
14818
+ path: [...at, "path"],
14819
+ message: `"${condition.path}" is a secret. The platform never reads a secret's value, so a condition on one could never be evaluated.`
14820
+ });
14821
+ continue;
14822
+ }
14823
+ const single = withoutCrossFieldRules({
14824
+ ...definition.configuration,
14825
+ properties: { [leaf.name]: target },
14826
+ required: [leaf.name]
14827
+ });
14828
+ const outside = condition.anyOf.filter((value) => !validateSubmittedConfiguration(single, { [leaf.name]: value }).ok);
14829
+ if (outside.length > 0) {
14830
+ ctx.addIssue({
14831
+ code: external_exports.ZodIssueCode.custom,
14832
+ path: [...at, "anyOf"],
14833
+ message: `"${condition.path}" never accepts ${outside.map((v) => `"${v}"`).join(", ")}, so this condition could never make the secret required.`
14834
+ });
14835
+ }
14836
+ }
14622
14837
  const provides = definition.serviceInterfaces?.provides ?? [];
14623
14838
  const provideKeys = provides.map((provide) => provide.key);
14624
14839
  const duplicateProvideKeys = provideKeys.filter((key, i) => provideKeys.indexOf(key) !== i);
@@ -17002,7 +17217,7 @@ async function runApply(verb, parsed, deps = {}) {
17002
17217
  if (!planned.ok) return printError(planned.error, json);
17003
17218
  if (verb === "plan") {
17004
17219
  printPlan(planned.data, json);
17005
- if (isUnconverged(planned.data)) {
17220
+ if (isUnconverged(planned.data) && planned.data.action === "NO_CHANGE") {
17006
17221
  if (!json) {
17007
17222
  process.stderr.write(
17008
17223
  `${planned.data.manifestKey ?? "This resource"} is ${planned.data.health?.status ?? "not serving"}. Its desired state is applied; it is not running. \`vfac get operation <id>\` says why.
@@ -17011,6 +17226,12 @@ async function runApply(verb, parsed, deps = {}) {
17011
17226
  }
17012
17227
  return 1;
17013
17228
  }
17229
+ if (isUnconverged(planned.data) && !json) {
17230
+ process.stderr.write(
17231
+ `${planned.data.manifestKey ?? "This resource"} is ${planned.data.health?.status ?? "not serving"}. This plan changes it.
17232
+ `
17233
+ );
17234
+ }
17014
17235
  return 0;
17015
17236
  }
17016
17237
  const parsedAction = ManifestActionSchema.safeParse(planned.data.action);
@@ -17310,23 +17531,20 @@ async function runDoctor(parsed) {
17310
17531
  const compatible = compatibility(cliVersion(), probe.minimumCliVersion);
17311
17532
  checks.push(compatible);
17312
17533
  if (!compatible.ok) return report2(checks, json);
17313
- const signedIn = await signIn({ endpoint });
17314
- if (!signedIn.ok) {
17315
- checks.push({ name: "Context Gate", ok: false, detail: signedIn.message });
17534
+ const session = await ensureSession({ endpointFlag: parsed.flags["endpoint"] });
17535
+ if (!session.ok) {
17536
+ checks.push({ name: "Sign-in", ok: false, detail: session.message });
17316
17537
  return report2(checks, json);
17317
17538
  }
17318
17539
  checks.push({
17319
- name: "Context Gate",
17540
+ name: "Sign-in",
17320
17541
  ok: true,
17321
- detail: signedIn.gate ? `authenticated as "${signedIn.gate.name}" (${signedIn.gate.id})` : "authenticated"
17542
+ detail: session.ready.via === "stored" ? "a stored sign-in is still valid, so no credential was spent" : session.ready.gate ? `authenticated as "${session.ready.gate.name}" (${session.ready.gate.id})` : "authenticated"
17322
17543
  });
17323
- if (!sessionUsable(stored.session, endpoint, /* @__PURE__ */ new Date())) {
17324
- writeStored({ ...stored, session: signedIn.session });
17325
- }
17326
17544
  const who = await call({
17327
17545
  endpoint,
17328
17546
  path: "/api/v1/whoami",
17329
- token: signedIn.session.token
17547
+ token: session.ready.token
17330
17548
  });
17331
17549
  if (!who.ok) {
17332
17550
  checks.push({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@varde-flyt/vfac",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Deploy and manage Varde Flyt Products from a manifest.",
5
5
  "repository": {
6
6
  "type": "git",