@varde-flyt/vfac 0.13.0 → 0.14.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 +17 -3
  2. package/dist/vfac.mjs +534 -310
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -80,8 +80,18 @@ vfac get operation <operationId>
80
80
  vfac get product-instance <productInstanceId>
81
81
  ```
82
82
 
83
+ Lost the operation id — a terminal that closed, a CI log nobody kept? The
84
+ operations of a resource are listed newest first, one page at a time; the
85
+ `--cursor` for older ones is printed, and is `nextCursor` in JSON:
86
+
87
+ ```sh
88
+ vfac get operations --resource <productInstanceId>
89
+ ```
90
+
83
91
  Secrets are supplied per apply and never belong in the manifest — a manifest is a
84
- file you commit. **Plan the run that sets one with the same `--secret` inputs the
92
+ file you commit. A value for a setting the Product declares secret is refused
93
+ at every door by the platform, and by `vfac` before the file is sent when the
94
+ file pins the release the catalog currently offers. **Plan the run that sets one with the same `--secret` inputs the
85
95
  apply will carry**: `plan` takes the flag exactly as `apply` does, and without it
86
96
  the plan describes a different request.
87
97
 
@@ -182,7 +192,9 @@ spec:
182
192
 
183
193
  `vfac manifest init` names the exact property for every dependency you have to
184
194
  choose, and `vfac plan` prints the connections an apply would establish before
185
- you approve it. The dependency key comes from the Product, not from you —
195
+ you approve it. A resource in this Project that cannot provide the connection is
196
+ refused on `spec.dependencyBindings.<dependency-key>`, saying why; one named in
197
+ another Project answers only that it could not be used. The dependency key comes from the Product, not from you —
186
198
  `vfac product show <productId>` lists every dependency with its key, its
187
199
  capability, and whether a setting is what turns it on.
188
200
 
@@ -214,6 +226,7 @@ vfac product show <productId> --output json
214
226
  vfac plan -f resource.yaml --output json
215
227
  vfac get product-instance <productInstanceId> --output json
216
228
  vfac get product-instances --output json
229
+ vfac get operations --resource <productInstanceId> --output json
217
230
  ```
218
231
 
219
232
  **Decide from the action, not from the exit code.** A plan that REFUSES still
@@ -235,8 +248,9 @@ The envelopes are stable at the top level:
235
248
 
236
249
  | Command | Answers with |
237
250
  | --- | --- |
238
- | `vfac get product-instance <id> --output json` | `resource` and `lifecycle` — `status`, `outputs` and `conditions` are under `resource` |
251
+ | `vfac get product-instance <id> --output json` | `resource` and `lifecycle` — `status`, `outputs` and `conditions` are under `resource`; each condition carries `state` and `freshness` (`FRESH`, or `STALE` when not re-confirmed recently), and `lifecycle.refused[]` carries a `reason` |
239
252
  | `vfac get product-instances --output json` | `productInstances` and `nextCursor` |
253
+ | `vfac get operations --resource <productInstanceId> --output json` | `operations` and `nextCursor` — one page, newest first, and `nextCursor` reads older ones; each line is `operationId`, `type`, `status`, `phase` and its timestamps |
240
254
 
241
255
  `vfac guide` is authoritative for what is inside them.
242
256
 
package/dist/vfac.mjs CHANGED
@@ -7378,6 +7378,8 @@ var VALUED = /* @__PURE__ */ new Set([
7378
7378
  // an organization and part of a catalogue, silently, once either passed 25.
7379
7379
  "cursor",
7380
7380
  "page-size",
7381
+ // `get operations --resource pri_…`: the history of one resource.
7382
+ "resource",
7381
7383
  "product",
7382
7384
  "name",
7383
7385
  "key",
@@ -7700,283 +7702,7 @@ async function probeMachineApi(args) {
7700
7702
  }
7701
7703
 
7702
7704
  // src/manifest.ts
7703
- var import_yaml = __toESM(require_dist(), 1);
7704
7705
  import { readFileSync as readFileSync2 } from "node:fs";
7705
- function readManifest(path) {
7706
- let text;
7707
- try {
7708
- text = readFileSync2(path, "utf8");
7709
- } catch {
7710
- return { ok: false, message: `Could not read ${path}.` };
7711
- }
7712
- let parsed;
7713
- try {
7714
- parsed = (0, import_yaml.parse)(text);
7715
- } catch (error) {
7716
- return {
7717
- ok: false,
7718
- message: `${path} is not valid YAML or JSON: ${error instanceof Error ? error.message : "unknown error"}`
7719
- };
7720
- }
7721
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
7722
- return { ok: false, message: `${path} does not contain a manifest document.` };
7723
- }
7724
- return { ok: true, document: parsed };
7725
- }
7726
- function resolveSecrets(pairs, environment = process.env) {
7727
- const secrets = {};
7728
- for (const pair of pairs) {
7729
- const separator = pair.indexOf("=");
7730
- if (separator <= 0) {
7731
- return { ok: false, message: `--secret expects name=env:VARIABLE, got "${pair}".` };
7732
- }
7733
- const name = pair.slice(0, separator);
7734
- const source = pair.slice(separator + 1);
7735
- if (!source.startsWith("env:")) {
7736
- return {
7737
- ok: false,
7738
- message: `--secret ${name} 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.`
7739
- };
7740
- }
7741
- const variable = source.slice("env:".length);
7742
- if (variable.length === 0) {
7743
- return { ok: false, message: `--secret ${name}=env: names no variable.` };
7744
- }
7745
- const value = environment[variable];
7746
- if (value === void 0 || value === "") {
7747
- return { ok: false, message: `${variable} is not set, so ${name} has no value.` };
7748
- }
7749
- secrets[name] = value;
7750
- }
7751
- return { ok: true, secrets };
7752
- }
7753
- function manifestKeyOf(document) {
7754
- const metadata = document["metadata"];
7755
- if (typeof metadata !== "object" || metadata === null) return null;
7756
- const key = metadata["key"];
7757
- return typeof key === "string" ? key : null;
7758
- }
7759
-
7760
- // src/endpoint.ts
7761
- function isLoopback(hostname) {
7762
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
7763
- }
7764
- function validateEndpoint(raw, source) {
7765
- let url;
7766
- try {
7767
- url = new URL(raw);
7768
- } catch {
7769
- return { ok: false, message: `${source} is not a URL: "${raw}".` };
7770
- }
7771
- if (url.protocol === "https:") return { ok: true, endpoint: url.origin };
7772
- if (url.protocol === "http:" && isLoopback(url.hostname)) {
7773
- return { ok: true, endpoint: url.origin };
7774
- }
7775
- return {
7776
- ok: false,
7777
- message: `${source} must be https \u2014 a credential is sent to it, and "${url.protocol}//" would send it in the clear. Plain http is accepted for localhost only.`
7778
- };
7779
- }
7780
-
7781
- // src/session.ts
7782
- async function ensureSession(args) {
7783
- const stored = readStored();
7784
- const resolved = resolveEndpoint(args.endpointFlag, stored);
7785
- if (!resolved) {
7786
- return {
7787
- ok: false,
7788
- message: "No endpoint. Run `vfac context set --endpoint https://your-organization.cloud.vardeflyt.no`, or set VFAC_ENDPOINT."
7789
- };
7790
- }
7791
- const checked = validateEndpoint(resolved.value, resolved.source);
7792
- if (!checked.ok) return { ok: false, message: checked.message };
7793
- const endpoint = checked.endpoint;
7794
- const credential = credentialFromEnvironment();
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, via: "stored" } };
7797
- }
7798
- const signedIn = await signIn({
7799
- endpoint,
7800
- ...args.fetchImpl ? { fetchImpl: args.fetchImpl } : {}
7801
- });
7802
- if (!signedIn.ok) return { ok: false, message: signedIn.message };
7803
- const next = { ...stored, session: signedIn.session };
7804
- writeStored(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
- };
7815
- }
7816
- function resolveProject(flag, stored) {
7817
- return flag ?? stored.context?.projectId ?? null;
7818
- }
7819
-
7820
- // src/operations.ts
7821
- var OPERATION_STATUSES = [
7822
- "PENDING",
7823
- "RUNNING",
7824
- "SUCCEEDED",
7825
- "FAILED",
7826
- "CANCELLED"
7827
- ];
7828
- var TERMINAL_STATUSES = ["SUCCEEDED", "FAILED", "CANCELLED"];
7829
- var POLL_INTERVAL_MS = 5e3;
7830
- var MAX_WAIT_MS = 60 * 60 * 1e3;
7831
- function withProject(path, projectId) {
7832
- return projectId === null ? path : `${path}?project=${encodeURIComponent(projectId)}`;
7833
- }
7834
- function operationPath(operationId, projectId) {
7835
- return withProject(`/api/v1/operations/${encodeURIComponent(operationId)}`, projectId);
7836
- }
7837
- function classify(data) {
7838
- const status = data["status"];
7839
- if (typeof status !== "string") return "unknown";
7840
- if (TERMINAL_STATUSES.includes(status)) return "terminal";
7841
- if (OPERATION_STATUSES.includes(status)) return "working";
7842
- return "unknown";
7843
- }
7844
- async function readOperation(args) {
7845
- const result = await call({
7846
- endpoint: args.endpoint,
7847
- path: operationPath(args.operationId, args.projectId),
7848
- token: args.token,
7849
- ...args.fetchImpl ? { fetchImpl: args.fetchImpl } : {}
7850
- });
7851
- return result.ok ? { ok: true, data: result.data } : { ok: false, error: result.error };
7852
- }
7853
- async function waitForOperation(args) {
7854
- const deps = args.deps ?? {};
7855
- const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
7856
- const now = deps.now ?? (() => Date.now());
7857
- const deadline = now() + MAX_WAIT_MS;
7858
- let token = args.token;
7859
- let refreshed = false;
7860
- let last = null;
7861
- for (; ; ) {
7862
- const read = await readOperation({
7863
- endpoint: args.endpoint,
7864
- token,
7865
- operationId: args.operationId,
7866
- projectId: args.projectId,
7867
- ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
7868
- });
7869
- if (!read.ok) {
7870
- if (read.error.status === 401 && !refreshed && deps.refreshToken) {
7871
- refreshed = true;
7872
- const next = await deps.refreshToken();
7873
- if (next !== null) {
7874
- token = next;
7875
- continue;
7876
- }
7877
- }
7878
- return { kind: "read-failed", error: read.error };
7879
- }
7880
- last = read.data;
7881
- const verdict = classify(read.data);
7882
- if (verdict === "terminal") {
7883
- return { kind: "settled", status: String(read.data["status"]), data: read.data };
7884
- }
7885
- if (verdict === "unknown") {
7886
- return { kind: "unknown-status", status: String(read.data["status"]), data: read.data };
7887
- }
7888
- args.onProgress?.(read.data);
7889
- if (now() + POLL_INTERVAL_MS >= deadline) return { kind: "timed-out", data: last };
7890
- await sleep(POLL_INTERVAL_MS);
7891
- }
7892
- }
7893
- function renderOperation(operationId, data) {
7894
- const status = String(data["status"] ?? "UNKNOWN");
7895
- const phase = data["phase"];
7896
- const lines = [
7897
- typeof phase === "string" && phase !== "" ? `${operationId} ${status} (${phase})` : `${operationId} ${status}`
7898
- ];
7899
- const type = data["type"];
7900
- if (typeof type === "string") lines[0] += ` ${type}`;
7901
- const steps = Array.isArray(data["steps"]) ? data["steps"] : [];
7902
- if (steps.length > 0) {
7903
- lines.push("");
7904
- for (const step of steps) {
7905
- const state = String(step["state"] ?? "");
7906
- lines.push(` ${markerFor(state)} ${String(step["label"] ?? step["key"] ?? "")}`);
7907
- }
7908
- }
7909
- const failure = asRecord(data["failure"]);
7910
- if (failure) {
7911
- lines.push("");
7912
- const stage = failure["stage"];
7913
- lines.push(` Failed${typeof stage === "string" ? ` at ${stage}` : ""}`);
7914
- const summary = failure["summary"];
7915
- if (typeof summary === "string") lines.push(` ${summary}`);
7916
- const cause = failure["cause"];
7917
- if (typeof cause === "string") lines.push(` cause: ${cause}`);
7918
- const services = failure["services"];
7919
- if (Array.isArray(services)) {
7920
- for (const entry of services) {
7921
- const one = asRecord(entry);
7922
- if (one === null) continue;
7923
- const dependency = one["dependency"];
7924
- if (typeof dependency !== "string" || dependency === "") continue;
7925
- const code = one["code"];
7926
- lines.push(
7927
- typeof code === "string" && code !== "" ? ` ${dependency} \u2014 the service reported: ${code}` : ` ${dependency} \u2014 the service reported nothing usable`
7928
- );
7929
- }
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
- }
7950
- const retryable = failure["retryable"];
7951
- if (typeof retryable === "boolean") {
7952
- const again = data["type"] === "PRODUCT_INSTANCE_UPGRADE" ? "`vfac lifecycle upgrade <pri_\u2026>`" : "`vfac lifecycle reconcile <pri_\u2026>`";
7953
- const changeIt = data["type"] === "PRODUCT_INSTANCE_UPGRADE" ? "" : ", or `vfac apply` a corrected manifest";
7954
- const cause2 = failure["cause"];
7955
- const named = reportedSettings[0];
7956
- lines.push(
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}.`
7958
- );
7959
- }
7960
- }
7961
- return lines.join("\n");
7962
- }
7963
- function markerFor(state) {
7964
- switch (state) {
7965
- case "completed":
7966
- return "\u2713";
7967
- case "failed":
7968
- return "\u2717";
7969
- case "active":
7970
- return "\u203A";
7971
- case "pending":
7972
- return "\xB7";
7973
- default:
7974
- return "-";
7975
- }
7976
- }
7977
- function asRecord(value) {
7978
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
7979
- }
7980
7706
 
7981
7707
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
7982
7708
  var external_exports = {};
@@ -13790,6 +13516,9 @@ function withoutPropertyRules(properties) {
13790
13516
  }
13791
13517
  return out;
13792
13518
  }
13519
+ function declaredSecretNames(schema) {
13520
+ return Object.entries(schema.properties).filter(([, property]) => isSecretProperty(property)).map(([name]) => name).sort();
13521
+ }
13793
13522
  function defaultOf(property) {
13794
13523
  switch (property.type) {
13795
13524
  case "string":
@@ -17289,7 +17018,23 @@ function isTerminalAction(action) {
17289
17018
  var ManifestActionSchema = external_exports.enum(MANIFEST_ACTIONS);
17290
17019
 
17291
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>";
17292
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
+ }
17293
17038
 
17294
17039
  // ../product-contracts/dist/resource-output-reference.js
17295
17040
  var OUTPUT_KEY_RULE = "Output keys are camelCase.";
@@ -17303,6 +17048,9 @@ var ResourceOutputReferenceSchema = external_exports.object({
17303
17048
  }).strict()
17304
17049
  }).strict()
17305
17050
  }).strict();
17051
+ function looksLikeReference(value) {
17052
+ return typeof value === "object" && value !== null && !Array.isArray(value) && "valueFrom" in value;
17053
+ }
17306
17054
 
17307
17055
  // ../product-contracts/dist/dependency-binding.js
17308
17056
  var DependencyBindingSelectorSchema = external_exports.object({
@@ -17438,6 +17186,23 @@ var ProductInstanceTemplateSchema = external_exports.object({
17438
17186
  dependencyBindings: DependencyBindingsSchema.optional()
17439
17187
  }).strict()
17440
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
+ }
17441
17206
 
17442
17207
  // ../product-contracts/dist/product-submission.schema.js
17443
17208
  var PRODUCT_SUBMISSION_KIND = "ProductSubmission";
@@ -17506,36 +17271,369 @@ function checkProductSubmission(submission, ctx) {
17506
17271
  });
17507
17272
  }
17508
17273
  }
17509
- var PUBLISH_RULE_ANNEX = {
17510
- allOf: [
17511
- {
17512
- $comment: 'displayName reads "<Product name> \u2014 <Short description>": an em dash (U+2014) with one space on each side. A hyphen and an en dash are different characters and are refused.',
17513
- properties: {
17514
- definition: { properties: { displayName: { pattern: PRODUCT_DISPLAY_NAME_PATTERN } } }
17274
+ var PUBLISH_RULE_ANNEX = {
17275
+ allOf: [
17276
+ {
17277
+ $comment: 'displayName reads "<Product name> \u2014 <Short description>": an em dash (U+2014) with one space on each side. A hyphen and an en dash are different characters and are refused.',
17278
+ properties: {
17279
+ definition: { properties: { displayName: { pattern: PRODUCT_DISPLAY_NAME_PATTERN } } }
17280
+ }
17281
+ },
17282
+ {
17283
+ // THE SAME ASYMMETRY, one field along. `portfolio` is optional on a
17284
+ // Definition because frozen releases carry none; a Submission is by
17285
+ // construction a new release, so it must say where the Product belongs.
17286
+ // The `enum` is the LIVE list — narrower than the Definition's, which
17287
+ // still admits the retired ids a stored document may name.
17288
+ $comment: "A new release must declare portfolio.category, the catalog section a customer finds this Product under. Retired section ids still parse in a published Definition but may not be published again.",
17289
+ properties: {
17290
+ definition: {
17291
+ required: ["portfolio"],
17292
+ properties: {
17293
+ portfolio: {
17294
+ required: ["category"],
17295
+ properties: { category: { enum: [...PORTFOLIO_CATEGORY_IDS] } }
17296
+ }
17297
+ }
17298
+ }
17299
+ }
17300
+ }
17301
+ ]
17302
+ };
17303
+ withJsonSchemaAnnex(ProductSubmissionSchema, PUBLISH_RULE_ANNEX);
17304
+
17305
+ // src/manifest.ts
17306
+ var import_yaml = __toESM(require_dist(), 1);
17307
+ function readManifest(path) {
17308
+ let text;
17309
+ try {
17310
+ text = readFileSync2(path, "utf8");
17311
+ } catch {
17312
+ return { ok: false, message: `Could not read ${path}.` };
17313
+ }
17314
+ let parsed;
17315
+ try {
17316
+ parsed = (0, import_yaml.parse)(text, { prettyErrors: false });
17317
+ } catch (error) {
17318
+ return {
17319
+ ok: false,
17320
+ message: `${path} is not valid YAML or JSON${whereItFailed(text, error)}.`
17321
+ };
17322
+ }
17323
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
17324
+ return { ok: false, message: `${path} does not contain a manifest document.` };
17325
+ }
17326
+ return { ok: true, document: parsed };
17327
+ }
17328
+ function resolveSecrets(pairs, environment = process.env) {
17329
+ const secrets = {};
17330
+ for (const [index, pair] of pairs.entries()) {
17331
+ const label = pairs.length === 1 ? "--secret" : `--secret #${index + 1}`;
17332
+ const separator = pair.indexOf("=");
17333
+ if (separator <= 0) {
17334
+ return {
17335
+ ok: false,
17336
+ message: `${label} expects name=env:VARIABLE, and there is no name before "=".`
17337
+ };
17338
+ }
17339
+ const name = pair.slice(0, separator);
17340
+ const source = pair.slice(separator + 1);
17341
+ if (!source.startsWith("env:")) {
17342
+ return {
17343
+ 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.`
17345
+ };
17346
+ }
17347
+ const variable = source.slice("env:".length);
17348
+ if (variable.length === 0) {
17349
+ return { ok: false, message: `${label} names no variable after "env:".` };
17350
+ }
17351
+ const value = environment[variable];
17352
+ if (value === void 0 || value === "") {
17353
+ return {
17354
+ ok: false,
17355
+ message: `${label} reads an environment variable that is not set, or is empty.`
17356
+ };
17357
+ }
17358
+ secrets[name] = value;
17359
+ }
17360
+ return { ok: true, secrets };
17361
+ }
17362
+ function manifestKeyOf(document) {
17363
+ const metadata = document["metadata"];
17364
+ if (typeof metadata !== "object" || metadata === null) return null;
17365
+ const key = metadata["key"];
17366
+ return typeof key === "string" ? key : null;
17367
+ }
17368
+ function whereItFailed(text, error) {
17369
+ if (typeof error !== "object" || error === null) return "";
17370
+ const code = error.code;
17371
+ const pos = error.pos;
17372
+ const kind = typeof code === "string" && /^[A-Z_]+$/.test(code) ? code : null;
17373
+ const offset = Array.isArray(pos) && typeof pos[0] === "number" ? pos[0] : null;
17374
+ if (offset === null) return kind ? ` (${kind})` : "";
17375
+ const before = text.slice(0, offset);
17376
+ const line = before.split("\n").length;
17377
+ const column = offset - before.lastIndexOf("\n");
17378
+ return ` (${kind ?? "error"} at line ${line}, column ${column})`;
17379
+ }
17380
+ function pinnedRelease(document) {
17381
+ const spec = document["spec"];
17382
+ if (typeof spec !== "object" || spec === null || Array.isArray(spec)) return null;
17383
+ const { productId, productVersion } = spec;
17384
+ if (typeof productId !== "string" || productId === "") return null;
17385
+ if (typeof productVersion !== "string" || productVersion === "") return null;
17386
+ return { productId, productVersion };
17387
+ }
17388
+ function carriesLiteralSettings(document) {
17389
+ const configuration = configurationOf(document);
17390
+ return configuration !== null && Object.values(configuration).some((value) => !looksLikeReference(value));
17391
+ }
17392
+ function pastedSecretSettings(document, view) {
17393
+ const pinned = pinnedRelease(document);
17394
+ if (!pinned || typeof view !== "object" || view === null) return [];
17395
+ const { productId, version, configuration } = view;
17396
+ if (productId !== pinned.productId || version !== pinned.productVersion) return [];
17397
+ if (typeof configuration !== "object" || configuration === null) return [];
17398
+ try {
17399
+ return secretValuesInDocument(
17400
+ configuration,
17401
+ configurationOf(document)
17402
+ );
17403
+ } catch {
17404
+ return [];
17405
+ }
17406
+ }
17407
+ function configurationOf(document) {
17408
+ const spec = document["spec"];
17409
+ if (typeof spec !== "object" || spec === null || Array.isArray(spec)) return null;
17410
+ const configuration = spec["configuration"];
17411
+ if (typeof configuration !== "object" || configuration === null || Array.isArray(configuration)) {
17412
+ return null;
17413
+ }
17414
+ return configuration;
17415
+ }
17416
+
17417
+ // src/endpoint.ts
17418
+ function isLoopback(hostname) {
17419
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
17420
+ }
17421
+ function validateEndpoint(raw, source) {
17422
+ let url;
17423
+ try {
17424
+ url = new URL(raw);
17425
+ } catch {
17426
+ return { ok: false, message: `${source} is not a URL: "${raw}".` };
17427
+ }
17428
+ if (url.protocol === "https:") return { ok: true, endpoint: url.origin };
17429
+ if (url.protocol === "http:" && isLoopback(url.hostname)) {
17430
+ return { ok: true, endpoint: url.origin };
17431
+ }
17432
+ return {
17433
+ ok: false,
17434
+ message: `${source} must be https \u2014 a credential is sent to it, and "${url.protocol}//" would send it in the clear. Plain http is accepted for localhost only.`
17435
+ };
17436
+ }
17437
+
17438
+ // src/session.ts
17439
+ async function ensureSession(args) {
17440
+ const stored = readStored();
17441
+ const resolved = resolveEndpoint(args.endpointFlag, stored);
17442
+ if (!resolved) {
17443
+ return {
17444
+ ok: false,
17445
+ message: "No endpoint. Run `vfac context set --endpoint https://your-organization.cloud.vardeflyt.no`, or set VFAC_ENDPOINT."
17446
+ };
17447
+ }
17448
+ const checked = validateEndpoint(resolved.value, resolved.source);
17449
+ if (!checked.ok) return { ok: false, message: checked.message };
17450
+ const endpoint = checked.endpoint;
17451
+ const credential = credentialFromEnvironment();
17452
+ if (args.forceExchange !== true && sessionUsable(stored.session, endpoint, args.now ?? /* @__PURE__ */ new Date(), credential?.keyId)) {
17453
+ return { ok: true, ready: { endpoint, token: stored.session.token, stored, via: "stored" } };
17454
+ }
17455
+ const signedIn = await signIn({
17456
+ endpoint,
17457
+ ...args.fetchImpl ? { fetchImpl: args.fetchImpl } : {}
17458
+ });
17459
+ if (!signedIn.ok) return { ok: false, message: signedIn.message };
17460
+ const next = { ...stored, session: signedIn.session };
17461
+ writeStored(next);
17462
+ return {
17463
+ ok: true,
17464
+ ready: {
17465
+ endpoint,
17466
+ token: signedIn.session.token,
17467
+ stored: next,
17468
+ via: "exchanged",
17469
+ ...signedIn.gate ? { gate: signedIn.gate } : {}
17470
+ }
17471
+ };
17472
+ }
17473
+ function resolveProject(flag, stored) {
17474
+ return flag ?? stored.context?.projectId ?? null;
17475
+ }
17476
+
17477
+ // src/operations.ts
17478
+ var OPERATION_STATUSES = [
17479
+ "PENDING",
17480
+ "RUNNING",
17481
+ "SUCCEEDED",
17482
+ "FAILED",
17483
+ "CANCELLED"
17484
+ ];
17485
+ var TERMINAL_STATUSES = ["SUCCEEDED", "FAILED", "CANCELLED"];
17486
+ var POLL_INTERVAL_MS = 5e3;
17487
+ var MAX_WAIT_MS = 60 * 60 * 1e3;
17488
+ function withProject(path, projectId) {
17489
+ return projectId === null ? path : `${path}?project=${encodeURIComponent(projectId)}`;
17490
+ }
17491
+ function operationPath(operationId, projectId) {
17492
+ return withProject(`/api/v1/operations/${encodeURIComponent(operationId)}`, projectId);
17493
+ }
17494
+ function classify(data) {
17495
+ const status = data["status"];
17496
+ if (typeof status !== "string") return "unknown";
17497
+ if (TERMINAL_STATUSES.includes(status)) return "terminal";
17498
+ if (OPERATION_STATUSES.includes(status)) return "working";
17499
+ return "unknown";
17500
+ }
17501
+ async function readOperation(args) {
17502
+ const result = await call({
17503
+ endpoint: args.endpoint,
17504
+ path: operationPath(args.operationId, args.projectId),
17505
+ token: args.token,
17506
+ ...args.fetchImpl ? { fetchImpl: args.fetchImpl } : {}
17507
+ });
17508
+ return result.ok ? { ok: true, data: result.data } : { ok: false, error: result.error };
17509
+ }
17510
+ async function waitForOperation(args) {
17511
+ const deps = args.deps ?? {};
17512
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
17513
+ const now = deps.now ?? (() => Date.now());
17514
+ const deadline = now() + MAX_WAIT_MS;
17515
+ let token = args.token;
17516
+ let refreshed = false;
17517
+ let last = null;
17518
+ for (; ; ) {
17519
+ const read = await readOperation({
17520
+ endpoint: args.endpoint,
17521
+ token,
17522
+ operationId: args.operationId,
17523
+ projectId: args.projectId,
17524
+ ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
17525
+ });
17526
+ if (!read.ok) {
17527
+ if (read.error.status === 401 && !refreshed && deps.refreshToken) {
17528
+ refreshed = true;
17529
+ const next = await deps.refreshToken();
17530
+ if (next !== null) {
17531
+ token = next;
17532
+ continue;
17533
+ }
17534
+ }
17535
+ return { kind: "read-failed", error: read.error };
17536
+ }
17537
+ last = read.data;
17538
+ const verdict = classify(read.data);
17539
+ if (verdict === "terminal") {
17540
+ return { kind: "settled", status: String(read.data["status"]), data: read.data };
17541
+ }
17542
+ if (verdict === "unknown") {
17543
+ return { kind: "unknown-status", status: String(read.data["status"]), data: read.data };
17544
+ }
17545
+ args.onProgress?.(read.data);
17546
+ if (now() + POLL_INTERVAL_MS >= deadline) return { kind: "timed-out", data: last };
17547
+ await sleep(POLL_INTERVAL_MS);
17548
+ }
17549
+ }
17550
+ function renderOperation(operationId, data) {
17551
+ const status = String(data["status"] ?? "UNKNOWN");
17552
+ const phase = data["phase"];
17553
+ const lines = [
17554
+ typeof phase === "string" && phase !== "" ? `${operationId} ${status} (${phase})` : `${operationId} ${status}`
17555
+ ];
17556
+ const type = data["type"];
17557
+ if (typeof type === "string") lines[0] += ` ${type}`;
17558
+ const steps = Array.isArray(data["steps"]) ? data["steps"] : [];
17559
+ if (steps.length > 0) {
17560
+ lines.push("");
17561
+ for (const step of steps) {
17562
+ const state = String(step["state"] ?? "");
17563
+ lines.push(` ${markerFor(state)} ${String(step["label"] ?? step["key"] ?? "")}`);
17564
+ }
17565
+ }
17566
+ const failure = asRecord(data["failure"]);
17567
+ if (failure) {
17568
+ lines.push("");
17569
+ const stage = failure["stage"];
17570
+ lines.push(` Failed${typeof stage === "string" ? ` at ${stage}` : ""}`);
17571
+ const summary = failure["summary"];
17572
+ if (typeof summary === "string") lines.push(` ${summary}`);
17573
+ const cause = failure["cause"];
17574
+ if (typeof cause === "string") lines.push(` cause: ${cause}`);
17575
+ const services = failure["services"];
17576
+ if (Array.isArray(services)) {
17577
+ for (const entry of services) {
17578
+ const one = asRecord(entry);
17579
+ if (one === null) continue;
17580
+ const dependency = one["dependency"];
17581
+ if (typeof dependency !== "string" || dependency === "") continue;
17582
+ const code = one["code"];
17583
+ lines.push(
17584
+ typeof code === "string" && code !== "" ? ` ${dependency} \u2014 the service reported: ${code}` : ` ${dependency} \u2014 the service reported nothing usable`
17585
+ );
17515
17586
  }
17516
- },
17517
- {
17518
- // THE SAME ASYMMETRY, one field along. `portfolio` is optional on a
17519
- // Definition because frozen releases carry none; a Submission is by
17520
- // construction a new release, so it must say where the Product belongs.
17521
- // The `enum` is the LIVE list — narrower than the Definition's, which
17522
- // still admits the retired ids a stored document may name.
17523
- $comment: "A new release must declare portfolio.category, the catalog section a customer finds this Product under. Retired section ids still parse in a published Definition but may not be published again.",
17524
- properties: {
17525
- definition: {
17526
- required: ["portfolio"],
17527
- properties: {
17528
- portfolio: {
17529
- required: ["category"],
17530
- properties: { category: { enum: [...PORTFOLIO_CATEGORY_IDS] } }
17531
- }
17532
- }
17587
+ }
17588
+ const product = failure["product"];
17589
+ const reportedSettings = [];
17590
+ if (Array.isArray(product)) {
17591
+ for (const entry of product) {
17592
+ const one = asRecord(entry);
17593
+ if (one === null) continue;
17594
+ const component = one["component"];
17595
+ const code = one["code"];
17596
+ if (typeof component !== "string" || typeof code !== "string") continue;
17597
+ if (component === "" || code === "") continue;
17598
+ const setting = one["setting"];
17599
+ if (typeof setting === "string" && setting !== "") {
17600
+ reportedSettings.push(setting);
17601
+ lines.push(` ${component} refused to start: ${code} (setting: ${setting})`);
17602
+ } else {
17603
+ lines.push(` ${component} refused to start: ${code}`);
17533
17604
  }
17534
17605
  }
17535
17606
  }
17536
- ]
17537
- };
17538
- withJsonSchemaAnnex(ProductSubmissionSchema, PUBLISH_RULE_ANNEX);
17607
+ const retryable = failure["retryable"];
17608
+ if (typeof retryable === "boolean") {
17609
+ const again = data["type"] === "PRODUCT_INSTANCE_UPGRADE" ? "`vfac lifecycle upgrade <pri_\u2026>`" : "`vfac lifecycle reconcile <pri_\u2026>`";
17610
+ const changeIt = data["type"] === "PRODUCT_INSTANCE_UPGRADE" ? "" : ", or `vfac apply` a corrected manifest";
17611
+ const cause2 = failure["cause"];
17612
+ const named = reportedSettings[0];
17613
+ lines.push(
17614
+ 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}.`
17615
+ );
17616
+ }
17617
+ }
17618
+ return lines.join("\n");
17619
+ }
17620
+ function markerFor(state) {
17621
+ switch (state) {
17622
+ case "completed":
17623
+ return "\u2713";
17624
+ case "failed":
17625
+ return "\u2717";
17626
+ case "active":
17627
+ return "\u203A";
17628
+ case "pending":
17629
+ return "\xB7";
17630
+ default:
17631
+ return "-";
17632
+ }
17633
+ }
17634
+ function asRecord(value) {
17635
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
17636
+ }
17539
17637
 
17540
17638
  // src/render.ts
17541
17639
  function isUnconverged(plan) {
@@ -17649,6 +17747,12 @@ async function runApply(verb, parsed, deps = {}) {
17649
17747
  return 1;
17650
17748
  }
17651
17749
  const json = parsed.flags["output"] === "json";
17750
+ const pasted = await pastedSecretsIn(manifest2.document, {
17751
+ endpoint: session.ready.endpoint,
17752
+ token: session.ready.token,
17753
+ projectId
17754
+ });
17755
+ if (pasted.length > 0) return refusePastedSecrets(file, pasted, json);
17652
17756
  const path = `/api/v1/projects/${encodeURIComponent(projectId)}/manifest`;
17653
17757
  const envelope = {
17654
17758
  template: manifest2.document,
@@ -17842,6 +17946,39 @@ Read the operation that failed: \`vfac get operation <id>\`, or \`vfac get produ
17842
17946
  }
17843
17947
  }
17844
17948
  }
17949
+ async function pastedSecretsIn(document, target) {
17950
+ const pinned = pinnedRelease(document);
17951
+ if (!pinned || !carriesLiteralSettings(document)) return [];
17952
+ const view = await call({
17953
+ endpoint: target.endpoint,
17954
+ path: `/api/v1/projects/${encodeURIComponent(target.projectId)}/products/${encodeURIComponent(pinned.productId)}`,
17955
+ token: target.token
17956
+ });
17957
+ return view.ok ? pastedSecretSettings(document, view.data) : [];
17958
+ }
17959
+ function refusePastedSecrets(file, names, json) {
17960
+ const fields = names.map(secretValueIssue);
17961
+ if (json) {
17962
+ return printError(
17963
+ {
17964
+ status: 400,
17965
+ code: "REQUEST_INVALID",
17966
+ message: `${file} contains a secret value, so it was not sent. Nothing was changed.`,
17967
+ fields
17968
+ },
17969
+ true
17970
+ );
17971
+ }
17972
+ for (const field of fields) {
17973
+ process.stderr.write(`${field.field} contains a secret value.
17974
+ `);
17975
+ }
17976
+ process.stderr.write(
17977
+ "\nSecrets must not be stored in a manifest, so this file was not sent and nothing was changed.\nRemove the value and supply it with:\n" + names.map((name) => ` --secret ${name}=env:VARIABLE
17978
+ `).join("") + "\nIf the value was a real credential and this file has been committed or shared,\nrotate it.\n"
17979
+ );
17980
+ return 1;
17981
+ }
17845
17982
 
17846
17983
  // src/commands/context.ts
17847
17984
  function runContext(parsed) {
@@ -18223,9 +18360,9 @@ var MAX_PAGES = 50;
18223
18360
  async function readAllPages(args) {
18224
18361
  const items = [];
18225
18362
  let cursor = args.cursor;
18226
- const onePage = args.cursor !== void 0 || args.pageSize !== void 0;
18363
+ const onePage = args.onePage === true || args.cursor !== void 0 || args.pageSize !== void 0;
18227
18364
  for (let page = 0; page < MAX_PAGES; page += 1) {
18228
- const query = new URLSearchParams();
18365
+ const query = new URLSearchParams(args.query ?? {});
18229
18366
  if (cursor !== void 0) query.set("cursor", cursor);
18230
18367
  if (args.pageSize !== void 0) query.set("pageSize", args.pageSize);
18231
18368
  const suffix = query.toString();
@@ -18260,7 +18397,7 @@ async function readAllPages(args) {
18260
18397
  }
18261
18398
 
18262
18399
  // src/commands/get.ts
18263
- async function runGet(parsed) {
18400
+ async function runGet(parsed, deps = {}) {
18264
18401
  const noun = parsed.command[1];
18265
18402
  const json = parsed.flags["output"] === "json";
18266
18403
  const session = await ensureSession({ endpointFlag: parsed.flags["endpoint"] });
@@ -18337,8 +18474,10 @@ async function runGet(parsed) {
18337
18474
  const label = String(one["displayName"] ?? one["key"] ?? "");
18338
18475
  const state = String(one["state"] ?? "UNKNOWN");
18339
18476
  const reason = one["reason"];
18477
+ const observedAt = one["observedAt"];
18478
+ const stale = one["freshness"] === "STALE" && typeof observedAt === "string" ? ` [stale \u2014 last observed ${ageOf(observedAt, (deps.now ?? (() => /* @__PURE__ */ new Date()))())} ago]` : "";
18340
18479
  lines.push(
18341
- ` ${label.padEnd(width)} ${state}${typeof reason === "string" && reason !== "" ? ` (${reason})` : ""}`
18480
+ ` ${label.padEnd(width)} ${state}${typeof reason === "string" && reason !== "" ? ` (${reason})` : ""}${stale}`
18342
18481
  );
18343
18482
  const description = one["description"];
18344
18483
  if (typeof description === "string") {
@@ -18375,6 +18514,10 @@ async function runGet(parsed) {
18375
18514
  if (allowed.length > 0) {
18376
18515
  lines.push("", ` You may: ${allowed.join(", ")} (vfac lifecycle <command> ${instanceId})`);
18377
18516
  }
18517
+ const notNow = (lifecycle["refused"] ?? []).filter(
18518
+ (entry) => typeof entry["command"] === "string" && typeof entry["reason"] === "string" && entry["reason"] !== "permission_required"
18519
+ ).map((entry) => `${String(entry["command"])} (${String(entry["reason"])})`);
18520
+ if (notNow.length > 0) lines.push(` Not now: ${notNow.join(", ")}`);
18378
18521
  process.stdout.write(`${lines.join("\n")}
18379
18522
  `);
18380
18523
  return 0;
@@ -18421,7 +18564,75 @@ This is one page. Continue with --cursor ${result.nextCursor ?? ""}
18421
18564
  }
18422
18565
  return 0;
18423
18566
  }
18424
- process.stderr.write("Usage: vfac get product-instances | vfac get operation op_\u2026\n");
18567
+ if (noun === "operations") {
18568
+ const projectId = resolveProject(parsed.flags["project"], session.ready.stored);
18569
+ if (!projectId) {
18570
+ process.stderr.write("No Project. Use --project prj_\u2026 or set one in your context.\n");
18571
+ return 2;
18572
+ }
18573
+ const resource = parsed.flags["resource"];
18574
+ const result = await readAllPages({
18575
+ endpoint: session.ready.endpoint,
18576
+ token: session.ready.token,
18577
+ path: `/api/v1/projects/${encodeURIComponent(projectId)}/operations`,
18578
+ key: "operations",
18579
+ cursor: parsed.flags["cursor"],
18580
+ pageSize: parsed.flags["page-size"],
18581
+ onePage: true,
18582
+ ...resource === void 0 ? {} : { query: { productInstanceId: resource } }
18583
+ });
18584
+ if (!result.ok) {
18585
+ if (result.error.status === 404 && result.error.code === "NOT_FOUND") {
18586
+ process.stderr.write(
18587
+ "This Tenant Portal does not list operations yet \u2014 it is running an older build.\n"
18588
+ );
18589
+ return 1;
18590
+ }
18591
+ return printError(result.error, json);
18592
+ }
18593
+ if (json) {
18594
+ process.stdout.write(
18595
+ `${JSON.stringify({ operations: result.items, nextCursor: result.nextCursor })}
18596
+ `
18597
+ );
18598
+ return 0;
18599
+ }
18600
+ if (result.items.length === 0) {
18601
+ process.stdout.write(
18602
+ resource === void 0 ? "No operations in this project.\n" : "No operations for this resource in this project.\n"
18603
+ );
18604
+ return 0;
18605
+ }
18606
+ const rows = result.items.map((operation) => [
18607
+ operation.operationId ?? "(no id)",
18608
+ // `PRODUCT_INSTANCE_UPDATE` is every line's prefix and says nothing a
18609
+ // reader scanning the column needs. JSON keeps the platform's word.
18610
+ (operation.type ?? "").replace(/^PRODUCT_INSTANCE_/, ""),
18611
+ operation.status ?? "",
18612
+ operation.productInstanceId ?? "-",
18613
+ operation.createdAt ?? ""
18614
+ ]);
18615
+ const widths = rows[0].map(
18616
+ (_, column) => Math.max(...rows.map((row) => (row[column] ?? "").length))
18617
+ );
18618
+ for (const row of rows) {
18619
+ process.stdout.write(
18620
+ `${row.map((cell, column) => column === row.length - 1 ? cell : cell.padEnd(widths[column])).join(" ")}
18621
+ `
18622
+ );
18623
+ }
18624
+ if (!result.complete) {
18625
+ process.stderr.write(
18626
+ `
18627
+ This is one page. Continue with --cursor ${result.nextCursor ?? ""}
18628
+ `
18629
+ );
18630
+ }
18631
+ return 0;
18632
+ }
18633
+ process.stderr.write(
18634
+ "Usage: vfac get product-instances | vfac get operation op_\u2026 | vfac get operations [--resource pri_\u2026]\n"
18635
+ );
18425
18636
  return 2;
18426
18637
  }
18427
18638
  function wrapOutput(text, width) {
@@ -18438,6 +18649,13 @@ function wrapOutput(text, width) {
18438
18649
  if (current !== "") out.push(current);
18439
18650
  return out;
18440
18651
  }
18652
+ function ageOf(iso, now) {
18653
+ const seconds = Math.max(0, Math.floor((now.getTime() - Date.parse(iso)) / 1e3));
18654
+ if (Number.isNaN(seconds)) return "an unknown time";
18655
+ if (seconds < 3600) return `${Math.max(1, Math.floor(seconds / 60))}m`;
18656
+ if (seconds < 72 * 3600) return `${Math.floor(seconds / 3600)}h`;
18657
+ return `${Math.floor(seconds / 86400)}d`;
18658
+ }
18441
18659
 
18442
18660
  // src/commands/product.ts
18443
18661
  async function runProduct(parsed) {
@@ -19044,6 +19262,7 @@ var USAGE = `vfac \u2014 deploy Varde Flyt resources from a manifest
19044
19262
  vfac get product-instances
19045
19263
  vfac get product-instance pri_\u2026 what it is, and its outputs
19046
19264
  vfac get operation op_\u2026
19265
+ vfac get operations [--resource pri_\u2026] newest first \u2014 find an operation id again
19047
19266
  vfac lifecycle <suspend|resume|upgrade|reconcile|delete> pri_\u2026
19048
19267
  delete is irreversible and needs --yes
19049
19268
  vfac export product-instance pri_\u2026
@@ -19061,7 +19280,10 @@ Options
19061
19280
  --secret inputs the apply will carry; without them the plan
19062
19281
  answers about a different request. A value on the command line
19063
19282
  is in your shell history and in the process table, so it is
19064
- refused
19283
+ refused. A value for a secret setting written into the
19284
+ manifest itself is refused too: before the file is sent when it
19285
+ pins the release the catalog offers, by the platform otherwise
19286
+ --resource get operations: only that resource's history
19065
19287
  --product which Product to write a starting manifest for
19066
19288
  --name what a person will call the resource
19067
19289
  --key its stable identity. Derived from --name when omitted
@@ -19089,8 +19311,10 @@ Reading the catalogue
19089
19311
  Products are read UNDER a Project, because that is the scope a credential is
19090
19312
  granted in. Pass --project, or store one with: vfac context set --project prj_\u2026
19091
19313
 
19092
- project list and product list read the WHOLE list, following the server's
19093
- cursor. Pass --cursor or --page-size only when you want a single page.
19314
+ project list, product list and get product-instances read the WHOLE list,
19315
+ following the server's cursor. Pass --cursor or --page-size only when you want
19316
+ a single page. get operations reads ONE page, newest first \u2014 a history only
19317
+ grows \u2014 and prints the --cursor that continues into older operations.
19094
19318
 
19095
19319
  Authentication
19096
19320
  As yourself: vfac login \u2014 approve the code in your browser. The terminal then
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@varde-flyt/vfac",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Deploy and manage Varde Flyt Products from a manifest.",
5
5
  "repository": {
6
6
  "type": "git",