@varde-flyt/vfac 0.15.0 → 0.15.3

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 +27 -15
  2. package/dist/vfac.mjs +521 -76
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -26,10 +26,10 @@ vfac guide
26
26
  vfac whoami
27
27
  ```
28
28
 
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, 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
29
+ `doctor` checks the endpoint, which Project this terminal has selected and which
30
+ one this command will act in, the API, whether this client is new enough for the
31
+ platform it is pointed at, how this terminal is signed in and what that credential
32
+ reaches, in that order, and reports the first thing that is actually wrong. Both ways of signing in count: a Context
33
33
  Gate exchanged from `VFAC_CREDENTIAL_ID` and `VFAC_CREDENTIAL_SECRET`, or a
34
34
  sign-in from `vfac login` that is still valid. A platform declares the oldest client it supports;
35
35
  below that, `doctor` fails and says so before sending your credential anywhere.
@@ -37,7 +37,10 @@ below that, `doctor` fails and says so before sending your credential anywhere.
37
37
  The selected Project and the Projects your credential reaches are two different
38
38
  lines, and reading the second as the first is the mistake worth naming: a command
39
39
  with no `--project` uses the SELECTED one, and having access to a Project is not
40
- the same as being in it. `doctor` also says so when it has just started a fresh
40
+ the same as being in it. `--project` overrides the selection for one command and
41
+ writes nothing, so `vfac doctor --project prj_…` reports the Project it would act
42
+ in, says the flag is where that came from, and still names what your context has
43
+ selected. `doctor` also says so when it has just started a fresh
41
44
  config because the old one could not be read — the endpoint and Project you had
42
45
  set are gone in that case, and the unreadable file is kept beside it rather than
43
46
  discarded.
@@ -68,7 +71,7 @@ vfac context set --project <projectId>
68
71
 
69
72
  ```sh
70
73
  vfac product list
71
- vfac product show <productId>
74
+ vfac product show <productId> # every setting, and when you must answer it
72
75
  vfac manifest init --product <productId> --name "My resource" > resource.yaml
73
76
  ```
74
77
 
@@ -88,8 +91,10 @@ operations of a resource are listed newest first, one page at a time; the
88
91
  vfac get operations --resource <productInstanceId>
89
92
  ```
90
93
 
91
- Secrets are supplied per apply and never belong in the manifest — a manifest is a
92
- file you commit. A value for a setting the Product declares secret is refused
94
+ Secrets are passed as `--secret` inputs, beside the manifest and never in it — a manifest is a
95
+ file you commit. Pass one when a resource is created, and afterwards only on a run that sets or
96
+ changes one: a secret sent is a secret written, so a run that repeats one plans `UPDATE` rather
97
+ than `NO_CHANGE`. A value for a setting the Product declares secret is refused
93
98
  at every door by the platform, and by `vfac` before the file is sent when the
94
99
  file pins the release the catalog currently offers. **Plan the run that sets one with the same `--secret` inputs the
95
100
  apply will carry**: `plan` takes the flag exactly as `apply` does, and without it
@@ -97,9 +102,12 @@ the plan describes a different request.
97
102
 
98
103
  `--secret` names a root setting by its name, and a setting inside a group by its
99
104
  path — `--secret /upstreamAuth/clientSecret=env:UPSTREAM_CLIENT_SECRET` for the
100
- `clientSecret` under `upstreamAuth`. `vfac product show` and `vfac manifest init`
101
- print each required secret in that form. A path never names an item of a list:
102
- no secret is declared there.
105
+ `clientSecret` under `upstreamAuth`. `vfac product show` prints that form beside
106
+ EVERY credential the Product declares, with a line saying whether it is required
107
+ of everyone, required when another setting holds a particular value, or required
108
+ because another setting was answered — `dependentRequired` is directional, and
109
+ the line says which way it runs; `vfac manifest init` names the ones required
110
+ outright. A path never names an item of a list: no secret is declared there.
103
111
 
104
112
  `requiredSecrets` in a plan is what the Product requires for the configuration in
105
113
  your document — not a list of what the platform is missing. It names a credential
@@ -109,7 +117,9 @@ act on. `secretsToWrite` is the field that says what this apply would write.
109
117
  A credential the Product requires for your document — outright, or because of
110
118
  what a setting is set to — that the run neither supplies nor the resource already
111
119
  holds is refused by `plan` and `apply` alike, before anything is written: the
112
- error names every one, by the path `--secret` takes, and the command exits 1.
120
+ error names every one, by the path `--secret` takes, and the command exits 1. So
121
+ is a credential the run DOES supply whose value the Product's own declaration
122
+ refuses: the error names the setting and the bound it missed, never the value.
113
123
  `vfac validate` carries no secrets and refuses none; it lists them.
114
124
 
115
125
  ### `metadata.key` is the name you choose
@@ -229,9 +239,11 @@ it.
229
239
  ## For an agent or a pipeline
230
240
 
231
241
  Every command takes `--output json`, and it is the same data rather than a
232
- prettier subset — the human format of `vfac product show` prints a readable
233
- selection on purpose and says so at the foot of its output, and the JSON carries
234
- the whole Product contract.
242
+ prettier subset. The human format of `vfac product show` lists every setting the
243
+ Product declares and says when each must be answered; what it leaves to the JSON
244
+ form is the prose and the value constraints — descriptions, patterns, bounds —
245
+ and it says so at the foot of its output. The JSON carries the whole Product
246
+ contract.
235
247
 
236
248
  ```sh
237
249
  vfac product show <productId> --output json
package/dist/vfac.mjs CHANGED
@@ -13207,6 +13207,14 @@ function canonicalJson(value) {
13207
13207
  function immediateContainer(node) {
13208
13208
  return node.containers[node.containers.length - 1] ?? null;
13209
13209
  }
13210
+ function resolvedGroup(node) {
13211
+ for (const container of node.containers) {
13212
+ const declared = container.property.group;
13213
+ if (declared !== void 0)
13214
+ return declared;
13215
+ }
13216
+ return node.property.group;
13217
+ }
13210
13218
  function underList(node) {
13211
13219
  return node.containers.some((container) => container.kind === "array");
13212
13220
  }
@@ -14210,6 +14218,162 @@ function exampleResourceKey(name) {
14210
14218
  }
14211
14219
  var DEPLOYMENT_EXAMPLE_GUARANTEE = "Product-owned configuration is complete and E2E-proven; tenant-specific secrets and Resource/provider selections are supplied by the tenant.";
14212
14220
 
14221
+ // ../product-configuration/dist/configuration-requirements.js
14222
+ function configurationRequirements(configuration) {
14223
+ if (configuration === void 0 || configuration === null)
14224
+ return [];
14225
+ const schema = configuration;
14226
+ const rootRequired = new Set(schema.required ?? []);
14227
+ const carriers = new Map(conditionCarriers(schema).map((carrier) => [carrier.path, carrier]));
14228
+ const secretPaths = new Set(configurationLeaves(schema).filter((leaf) => isSecretProperty(leaf.property)).map((leaf) => String(leaf.path)));
14229
+ return configurationLeaves(schema).map((leaf) => {
14230
+ const parent = immediateContainer(leaf);
14231
+ const immediatelyRequired = parent === null ? rootRequired.has(leaf.name) : parent.required.includes(leaf.name);
14232
+ const chainRequired = containersRequiredToRoot(schema, leaf);
14233
+ const always = requiredToRoot(schema, leaf);
14234
+ const secret = isSecretProperty(leaf.property);
14235
+ const condition = conditionFor(schema, leaf.path, leaf.containers, carriers);
14236
+ const clause = clauseEdges(schema, leaf, secretPaths);
14237
+ const requiredness = always ? "always" : condition !== null && condition.carrier === "setting" ? "when-value" : immediatelyRequired && !chainRequired ? "when-group" : clause.requiredBy.length > 0 ? "with-siblings" : "optional";
14238
+ const containers = leaf.containers.map((container, index) => settingContainer(container, index === 0 ? rootRequired : new Set(leaf.containers[index - 1]?.required ?? [])));
14239
+ const list = [...containers].reverse().find((container) => container.kind === "array") ?? null;
14240
+ return {
14241
+ path: leaf.path,
14242
+ printablePath: printableConfigurationPath(leaf.path),
14243
+ title: leaf.property.title,
14244
+ description: leaf.property.description,
14245
+ type: leaf.property.type,
14246
+ secret,
14247
+ requiredness,
14248
+ // NEVER A SECRET'S, even though the declaration rules already refuse one.
14249
+ // Being wrong in that direction prints a credential, which is the same
14250
+ // paranoia `product-instance-example.ts` documents for its own reader.
14251
+ defaultValue: secret ? void 0 : declaredDefault(leaf.property),
14252
+ allowed: secret ? [] : allowedValues(leaf.property),
14253
+ condition,
14254
+ requires: clause.requires,
14255
+ requiredBy: clause.requiredBy,
14256
+ containers,
14257
+ perRow: underList(leaf),
14258
+ list,
14259
+ group: resolvedGroup(leaf)
14260
+ };
14261
+ });
14262
+ }
14263
+ function allowedValues(property) {
14264
+ const leaf = property;
14265
+ return [...leaf.enum ?? leaf.items?.enum ?? []];
14266
+ }
14267
+ function conditionFor(schema, path, containers, carriers) {
14268
+ const own = carriers.get(path);
14269
+ const ancestor = [...containers].reverse().map((container) => carriers.get(container.path)).find((carrier2) => carrier2 !== void 0 && carrier2.kind === "container");
14270
+ const carrier = own?.kind === "leaf" ? own : ancestor;
14271
+ if (carrier === void 0)
14272
+ return null;
14273
+ const setting = parseConfigurationPath(conditionSetting(carrier.condition));
14274
+ if (setting === null)
14275
+ return null;
14276
+ return {
14277
+ carrier: carrier.kind === "leaf" ? "setting" : "container",
14278
+ carrierPath: carrier.path,
14279
+ setting,
14280
+ printableSetting: printableConfigurationPath(setting),
14281
+ settingLabel: labelOfPath(schema, setting),
14282
+ anyOf: [...carrier.condition.anyOf],
14283
+ anyRow: "path" in carrier.condition && carrier.condition.anyRow === true
14284
+ };
14285
+ }
14286
+ function clauseEdges(schema, leaf, secretPaths) {
14287
+ const parent = immediateContainer(leaf);
14288
+ const scope = parent === null ? schema : parent.kind === "array" ? parent.property.items ?? parent.property : parent.property;
14289
+ const requires = /* @__PURE__ */ new Set();
14290
+ const requiredBy = /* @__PURE__ */ new Set();
14291
+ for (const [trigger, dependents] of Object.entries(scope.dependentRequired ?? {})) {
14292
+ if (trigger === leaf.name)
14293
+ for (const dependent of dependents)
14294
+ requires.add(dependent);
14295
+ else if (dependents.includes(leaf.name))
14296
+ requiredBy.add(trigger);
14297
+ }
14298
+ const order = Object.keys(scope.properties ?? {});
14299
+ const members = (names) => [...names].sort((a, b) => order.indexOf(a) - order.indexOf(b)).flatMap((name) => {
14300
+ const path = memberPath(leaf, name);
14301
+ return path === null ? [] : [
14302
+ {
14303
+ path,
14304
+ printablePath: printableConfigurationPath(path),
14305
+ secret: secretPaths.has(String(path))
14306
+ }
14307
+ ];
14308
+ });
14309
+ return { requires: members(requires), requiredBy: members(requiredBy) };
14310
+ }
14311
+ function memberPath(leaf, name) {
14312
+ const parent = immediateContainer(leaf);
14313
+ return parent === null ? configurationPath(name) : configurationPath(...pathSegments(parent.path), name);
14314
+ }
14315
+ function settingContainer(container, requiredByParent) {
14316
+ return {
14317
+ path: container.path,
14318
+ printablePath: printableConfigurationPath(container.path),
14319
+ title: container.property.title,
14320
+ kind: container.kind,
14321
+ required: requiredByParent.has(container.name)
14322
+ };
14323
+ }
14324
+
14325
+ // ../product-configuration/dist/manifest-yaml.js
14326
+ var import_yaml = __toESM(require_dist(), 1);
14327
+ function manifestYaml(document) {
14328
+ return (0, import_yaml.stringify)(document, { compat: "yaml-1.1" });
14329
+ }
14330
+
14331
+ // ../platform-schemas/dist/governance-feed.js
14332
+ var GOVERNANCE_FEED_CATEGORIES = ["connections", "resources"];
14333
+ var OPAQUE_ID = /^[a-z]{2,4}_[0-9a-z]{10,40}$/;
14334
+ var opaqueId = external_exports.string().regex(OPAQUE_ID);
14335
+ var GovernanceFeedActorSchema = external_exports.discriminatedUnion("type", [
14336
+ external_exports.object({ type: external_exports.literal("person"), ref: external_exports.string().min(1).max(128) }),
14337
+ external_exports.object({ type: external_exports.literal("platform") }),
14338
+ external_exports.object({ type: external_exports.literal("resource"), id: opaqueId }),
14339
+ external_exports.object({ type: external_exports.literal("system") })
14340
+ ]);
14341
+ var GovernanceFeedEventSchema = external_exports.object({
14342
+ /** The audit row's id. Stable, so a workspace can recognise a re-delivery. */
14343
+ id: opaqueId,
14344
+ /** The Control Plane's own event name, from its allowlist. */
14345
+ event: external_exports.string().regex(/^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*){1,4}$/).max(80),
14346
+ category: external_exports.enum(GOVERNANCE_FEED_CATEGORIES),
14347
+ /** RFC 3339: when it happened. */
14348
+ occurredAt: external_exports.string().datetime({ offset: true }),
14349
+ actor: GovernanceFeedActorSchema,
14350
+ action: external_exports.string().regex(/^[a-z][a-z0-9_]{0,31}$/),
14351
+ outcome: external_exports.enum(["success", "failure"]),
14352
+ projectId: opaqueId.optional(),
14353
+ /** The Resource the event is about — for a connection, the PROVIDER. */
14354
+ resourceId: opaqueId.optional(),
14355
+ /** For a connection between two Resources, the one that consumes it. */
14356
+ consumerResourceId: opaqueId.optional(),
14357
+ operationId: opaqueId.optional(),
14358
+ target: external_exports.object({
14359
+ type: external_exports.enum(["service_binding", "resource"]),
14360
+ id: opaqueId.optional()
14361
+ })
14362
+ });
14363
+ var GovernanceFeedPositionSchema = external_exports.object({
14364
+ at: external_exports.string().datetime({ offset: true }),
14365
+ id: external_exports.union([external_exports.literal(""), opaqueId])
14366
+ });
14367
+ var GOVERNANCE_FEED_MAX_LIMIT = 100;
14368
+ var GovernanceFeedRequestSchema = external_exports.object({
14369
+ after: GovernanceFeedPositionSchema,
14370
+ limit: external_exports.number().int().min(1).max(GOVERNANCE_FEED_MAX_LIMIT).optional()
14371
+ }).strict();
14372
+ var GovernanceFeedResponseSchema = external_exports.object({
14373
+ events: external_exports.array(GovernanceFeedEventSchema).max(GOVERNANCE_FEED_MAX_LIMIT),
14374
+ position: GovernanceFeedPositionSchema
14375
+ });
14376
+
14213
14377
  // ../platform-schemas/dist/index.js
14214
14378
  var TENANT_KEY_REGEX = /^[a-z][a-z0-9-]{2,23}[a-z0-9]$/;
14215
14379
  var RESERVED_TENANT_KEYS = /* @__PURE__ */ new Set([
@@ -14335,7 +14499,7 @@ var kindSchema = (kind) => external_exports.literal(kind, {
14335
14499
  errorMap: () => ({ message: `kind must be "${kind}".` })
14336
14500
  });
14337
14501
 
14338
- // ../product-contracts/dist/blocked-hosts.js
14502
+ // ../public-fetch/dist/index.js
14339
14503
  var BLOCKED_HOSTS = [
14340
14504
  /^localhost$/i,
14341
14505
  /^169\.254\./,
@@ -14568,6 +14732,7 @@ var IDENTITY_OIDC_CLIENT_V1 = {
14568
14732
  }
14569
14733
  };
14570
14734
  var DIRECTORY_SCOPES = [
14735
+ "directory.group-members.list",
14571
14736
  "directory.groups.search",
14572
14737
  "directory.membership.check",
14573
14738
  "directory.principals.resolve",
@@ -14619,9 +14784,58 @@ var IDENTITY_DIRECTORY_V1 = {
14619
14784
  }
14620
14785
  }
14621
14786
  };
14787
+ var OBSERVABILITY_LOGS_SCOPES = ["logs.read", "logs.write"];
14788
+ var OBSERVABILITY_LOGS_V1 = {
14789
+ kind: "service-api",
14790
+ id: "observability.logs",
14791
+ version: "v1",
14792
+ // TENANT-MULTIPLE, for the reason the two identity interfaces are: an
14793
+ // organization may run more than one log workspace — one per environment, one
14794
+ // per department — and which one a Product emits to is the customer's answer
14795
+ // rather than the platform's. There is deliberately no fallback to "the only
14796
+ // one" when a Tenant happens to have exactly one, because that rule would
14797
+ // change the day a second workspace is deployed and bind existing consumers
14798
+ // by accident.
14799
+ cardinality: "tenant-multiple",
14800
+ summary: "Send operational log records to a log workspace in this organization, and optionally query them back. The provider returns an ingest address and a credential; the consumer calls it directly.",
14801
+ maxRequestedScopes: OBSERVABILITY_LOGS_SCOPES.length,
14802
+ scopes: OBSERVABILITY_LOGS_SCOPES,
14803
+ // `logs.write` IS REQUIRED, and it is the only required scope on any
14804
+ // interface in this file. The reason is that the alternative is incoherent
14805
+ // rather than merely narrow: a binding to a log workspace that may not write
14806
+ // to it has asked for a connection it cannot use for the thing the connection
14807
+ // is for. A consumer that only wants to READ somebody else's records is not
14808
+ // this interface's case — it is a query surface the providing Product
14809
+ // exposes in its own right.
14810
+ requiredScopes: ["logs.write"],
14811
+ protocol: {
14812
+ authScheme: "bearer",
14813
+ credentialTransport: "authorization-header"
14814
+ },
14815
+ fields: {
14816
+ ingestUrl: {
14817
+ kind: "attribute",
14818
+ always: true,
14819
+ format: "https-url",
14820
+ description: 'The base address records are sent to: `POST {ingestUrl}/records` with `{"records": [...]}`, each record `{message, severity, observedAt?, fields?}`. Operations are relative to it, so the provider can move them without a new binding.'
14821
+ },
14822
+ grantedScopes: {
14823
+ kind: "attribute",
14824
+ always: true,
14825
+ format: "space-separated-tokens",
14826
+ description: "Space-separated scopes actually granted, which may be narrower than those requested. Read this rather than assuming the request was honoured."
14827
+ },
14828
+ apiToken: {
14829
+ kind: "secret",
14830
+ always: true,
14831
+ description: "The credential for the address above, presented as a bearer token. Delivered as a secret reference, never as an environment value, and rotatable without a new binding."
14832
+ }
14833
+ }
14834
+ };
14622
14835
  var SERVICE_INTERFACE_CONTRACTS = {
14623
14836
  "identity.oidc-client/v1": IDENTITY_OIDC_CLIENT_V1,
14624
- "identity.directory/v1": IDENTITY_DIRECTORY_V1
14837
+ "identity.directory/v1": IDENTITY_DIRECTORY_V1,
14838
+ "observability.logs/v1": OBSERVABILITY_LOGS_V1
14625
14839
  };
14626
14840
  var SERVICE_INTERFACE_KEYS = Object.keys(SERVICE_INTERFACE_CONTRACTS).sort();
14627
14841
  var SERVICE_INTERFACE_IDS = [
@@ -14880,11 +15094,35 @@ var ProductDependencySchema = external_exports.object({
14880
15094
  var ProductDependencyV2Schema = ProductDependencySchema.extend({
14881
15095
  requiredWhen: RequiredWhenV2Schema.optional()
14882
15096
  });
15097
+ function hasControlCharacter(value) {
15098
+ for (let i = 0; i < value.length; i += 1) {
15099
+ const code = value.charCodeAt(i);
15100
+ if (code < 32 || code === 127)
15101
+ return true;
15102
+ }
15103
+ return false;
15104
+ }
15105
+ var GuidanceTextSchema = (min, max) => external_exports.string().min(min).max(max).refine((value) => !hasControlCharacter(value), {
15106
+ message: "Guidance is one line, with no control characters."
15107
+ });
15108
+ var ConsumerGuidanceSchema = external_exports.object({
15109
+ /** One paragraph: what this provider does with what a consumer sends or asks. */
15110
+ summary: GuidanceTextSchema(20, 600),
15111
+ /** Concrete, provider-specific practices, one per entry. */
15112
+ notes: external_exports.array(GuidanceTextSchema(10, 400)).min(1).max(12).optional()
15113
+ }).strict();
14883
15114
  var ServiceInterfaceProvideSchema = external_exports.object({
14884
15115
  key: ServiceBindingKeySchema,
14885
15116
  interfaceId: ServiceInterfaceIdSchema,
14886
15117
  interfaceVersion: ServiceInterfaceVersionSchema,
14887
- cardinality: ServiceInterfaceCardinalitySchema
15118
+ cardinality: ServiceInterfaceCardinalitySchema,
15119
+ /**
15120
+ * `.optional()`, NEVER `.default()`. A default materialises the key,
15121
+ * `contractDigest` digests the parsed object, and every release published
15122
+ * before this field existed would be refused on republish as
15123
+ * `RELEASED_CONTRACT_MUTATED`.
15124
+ */
15125
+ consumerGuidance: ConsumerGuidanceSchema.optional()
14888
15126
  }).strict();
14889
15127
  var LifecycleCapabilitiesSchema = external_exports.object({
14890
15128
  upgrade: external_exports.boolean(),
@@ -15944,10 +16182,38 @@ var ComponentHealthSchema = external_exports.object({
15944
16182
  readiness: ProbeSchema,
15945
16183
  liveness: ProbeSchema
15946
16184
  }).strict();
16185
+ var PUBLIC_INGRESS_AUTHENTICATION = external_exports.enum([
16186
+ "api-key",
16187
+ "oauth2",
16188
+ "mtls",
16189
+ "signed-request",
16190
+ "tenant-human-session"
16191
+ ]);
16192
+ var HUMAN_SESSION_MECHANISMS = external_exports.array(external_exports.enum(["federated-oidc", "email-one-time-link", "local-password"])).min(1).max(3);
16193
+ var INGRESS_PATH_PREFIX = external_exports.string().regex(/^\/[a-z0-9\-/]*$/).max(64);
16194
+ var IngressSurfaceSchema = external_exports.object({
16195
+ pathPrefix: INGRESS_PATH_PREFIX,
16196
+ authentication: PUBLIC_INGRESS_AUTHENTICATION,
16197
+ /** Required and refused exactly as the component-level field is. */
16198
+ humanSessionMechanisms: HUMAN_SESSION_MECHANISMS.optional(),
16199
+ /** Who calls this part of the component, for the reviewer. */
16200
+ purpose: external_exports.string().min(20).max(300)
16201
+ }).strict();
16202
+ var MAX_INGRESS_SURFACES = 8;
15947
16203
  var ComponentIngressSchema = external_exports.discriminatedUnion("exposure", [
15948
16204
  external_exports.object({
15949
16205
  exposure: external_exports.literal("public"),
15950
16206
  /**
16207
+ * `oauth2`: the routes it names accept OAuth access tokens. The
16208
+ * Authorization Server that issues them may be an external authority, or
16209
+ * the Product may implement it itself — an MCP server that runs its own
16210
+ * OAuth 2.1 discovery, authorization, consent, PKCE and token issuance
16211
+ * declares `oauth2` for the endpoint those tokens reach. Either way
16212
+ * the Product must truthfully implement and expose the flow it declares.
16213
+ * The platform provides none of it implicitly: it runs no Authorization
16214
+ * Server, registers no client, issues no token and validates none in
16215
+ * front of the container.
16216
+ *
15951
16217
  * `tenant-human-session` is the one that needs explaining: a console the
15952
16218
  * Product itself signs humans into, rather than delegating to a caller's
15953
16219
  * credential.
@@ -15968,16 +16234,18 @@ var ComponentIngressSchema = external_exports.discriminatedUnion("exposure", [
15968
16234
  * defaulted to, it runs with mail disabled entirely, and the temporary
15969
16235
  * credential it bootstraps has its own list of properties in that section.
15970
16236
  *
15971
- * `oauth2` remains correct for a component that accepts a token somebody
15972
- * else minted. Declare this one when the Product owns the session.
16237
+ * WHERE THE TWO MEET: a Product that is its own Authorization Server. The
16238
+ * person approving a client must first be signed in through the Product's
16239
+ * ordinary `tenant-human-session` — approving is something a signed-in
16240
+ * person does, never a sign-in of its own — so the routes serving that
16241
+ * sign-in and the approval declare `tenant-human-session` and its
16242
+ * mechanisms, as the component's own `authentication` or as a surface,
16243
+ * and the routes accepting the resulting tokens declare `oauth2`. `oauth2`
16244
+ * describes how a token is PRESENTED, never how a person signs in:
16245
+ * whoever issued the token, a sign-in the Product runs carries the
16246
+ * obligation above.
15973
16247
  */
15974
- authentication: external_exports.enum([
15975
- "api-key",
15976
- "oauth2",
15977
- "mtls",
15978
- "signed-request",
15979
- "tenant-human-session"
15980
- ]),
16248
+ authentication: PUBLIC_INGRESS_AUTHENTICATION,
15981
16249
  /**
15982
16250
  * WHICH of those three mechanisms this Product actually offers.
15983
16251
  *
@@ -16000,8 +16268,16 @@ var ComponentIngressSchema = external_exports.discriminatedUnion("exposure", [
16000
16268
  * which — never a failure of another. Listing all three means all three
16001
16269
  * are implemented, not that the Product will fall back through them.
16002
16270
  */
16003
- humanSessionMechanisms: external_exports.array(external_exports.enum(["federated-oidc", "email-one-time-link", "local-password"])).min(1).max(3).optional(),
16004
- pathPrefix: external_exports.string().regex(/^\/[a-z0-9\-/]*$/).max(64),
16271
+ humanSessionMechanisms: HUMAN_SESSION_MECHANISMS.optional(),
16272
+ pathPrefix: INGRESS_PATH_PREFIX,
16273
+ /**
16274
+ * Parts of this component's routes that authenticate differently from
16275
+ * `authentication` above. Optional, and `.optional()` rather than a
16276
+ * default: a released Blueprint is re-parsed on the read path and its
16277
+ * digest is taken over the parsed object, so a materialised empty list
16278
+ * would refuse a republish of unchanged bytes.
16279
+ */
16280
+ surfaces: external_exports.array(IngressSurfaceSchema).min(1).max(MAX_INGRESS_SURFACES).optional(),
16005
16281
  /**
16006
16282
  * Why this component is public at all. docs/SECURITY.md §8 makes adding
16007
16283
  * public ingress a security-approval trigger; requiring the author to
@@ -16560,6 +16836,20 @@ var DataStoreSchema = external_exports.object({
16560
16836
  * or push it outside the contract — where nothing governs its retention,
16561
16837
  * its data categories or its deletion.
16562
16838
  *
16839
+ * `log-store` was added for the same reason one level along: a Product that
16840
+ * emits structured operational records about its own running — requests
16841
+ * served, durations, outcomes — had nowhere in this contract to say so, and
16842
+ * a store it cannot name is a store whose retention, data categories and
16843
+ * deletion nothing governs. It is LOGS, deliberately and only. Metrics and
16844
+ * traces are different capabilities with different shapes, and naming this
16845
+ * one after the signal it carries is what keeps a later `metric-store` from
16846
+ * being a silent widening of this one.
16847
+ *
16848
+ * PROVIDER-NEUTRAL, like every other name here. Nothing in this enum names
16849
+ * an engine, a vendor or a wire format, and `log-store` must not become the
16850
+ * exception — which implementation backs it is a platform promise published
16851
+ * in `DATA_STORE_INTERFACES`, not a choice a publisher makes.
16852
+ *
16563
16853
  * Naming a kind is a CONTRACT question. Whether this Control Plane can
16564
16854
  * provision it is a DEPLOYABILITY question, answered separately by
16565
16855
  * `validateDeployability` — see deployability.ts.
@@ -16571,7 +16861,8 @@ var DataStoreSchema = external_exports.object({
16571
16861
  "vector-index",
16572
16862
  "queue",
16573
16863
  "document-database",
16574
- "search-index"
16864
+ "search-index",
16865
+ "log-store"
16575
16866
  ]),
16576
16867
  purpose: external_exports.string().min(10).max(300),
16577
16868
  /**
@@ -16593,7 +16884,7 @@ var DataStoreSchema = external_exports.object({
16593
16884
  }).strict(),
16594
16885
  deleteBehavior: external_exports.enum(["purge-with-instance", "purge-after-retention"]),
16595
16886
  /**
16596
- * Ask for RUNTIME access to the store's own objects.
16887
+ * Ask for RUNTIME access to the store's own contents.
16597
16888
  *
16598
16889
  * OPT-IN, AND PRESENT-OR-ABSENT RATHER THAN AN ENUM. A `mode` field would
16599
16890
  * have to be optional with a default to keep published documents parseable,
@@ -16602,17 +16893,31 @@ var DataStoreSchema = external_exports.object({
16602
16893
  * and a legitimate republish would be refused as an immutability conflict.
16603
16894
  * Presence is a cleaner discriminator and reads better in a diff.
16604
16895
  *
16605
- * WHAT IT DOES NOT GRANT: S3 credentials. The component receives an address,
16606
- * its own key prefix and a signing key for the platform's storage broker,
16607
- * which checks every key against the INSTANCE the signature proved. A
16608
- * Scaleway key cannot be scoped to a prefix, and the provider's quota is 50
16609
- * API keys per Organization, so one per Product Instance would cap the
16610
- * platform at roughly 45 of them.
16896
+ * WHAT IT DOES NOT GRANT: a provider credential, on either kind that
16897
+ * accepts it. The component receives an address, what identifies it, and a
16898
+ * signing key for the platform service that brokers the access — and that
16899
+ * service checks every request against the INSTANCE the signature proved.
16900
+ *
16901
+ * For `object-storage` the reason is that a Scaleway key cannot be scoped
16902
+ * to a prefix or a bucket, and the provider's quota is 50 API keys per
16903
+ * Organization, so one per Product Instance would cap the platform at
16904
+ * roughly 45 of them. For `log-store` the reason is sharper: a Cockpit
16905
+ * token carries a Project and a scope and has NO data-source field, and the
16906
+ * whole platform lives in one Scaleway Project — so a token delivered to
16907
+ * one Tenant's component would read and write every other Tenant's logs.
16908
+ * Neither is a limit to work around; both are why the brokering service
16909
+ * exists.
16910
+ *
16911
+ * WHICH KINDS MAY DECLARE IT is `RUNTIME_ACCESS_KINDS`, and the variables
16912
+ * each one then receives are `RUNTIME_ACCESS_SUFFIXES`. The cross-field
16913
+ * check below reads both.
16611
16914
  */
16612
16915
  runtimeAccess: external_exports.object({
16613
16916
  /**
16614
- * Names the variables the platform injects — `{PREFIX}_ENDPOINT_URL`,
16615
- * `_TENANT_ID`, `_PREFIX` and `_SIGNING_KEY`.
16917
+ * Names the variables the platform injects: `{PREFIX}_ENDPOINT_URL`,
16918
+ * `_TENANT_ID` and `_SIGNING_KEY` on every kind that accepts runtime
16919
+ * access, plus `_PREFIX` on `object-storage`, which is the only one
16920
+ * addressed by a key prefix.
16616
16921
  *
16617
16922
  * Bounded at 32 so every derived name fits `EnvVarNameSchema`'s 64, and
16618
16923
  * REFUSED at authoring time rather than silently truncated at deploy —
@@ -16631,6 +16936,17 @@ var DataStoreSchema = external_exports.object({
16631
16936
  access: external_exports.enum(["read-write", "read-only"])
16632
16937
  }).strict()).min(1).max(12)
16633
16938
  }).strict();
16939
+ var RUNTIME_ACCESS_SUFFIXES = {
16940
+ "object-storage": ["_ENDPOINT_URL", "_TENANT_ID", "_PREFIX", "_SIGNING_KEY"],
16941
+ "log-store": ["_ENDPOINT_URL", "_TENANT_ID", "_SIGNING_KEY"]
16942
+ };
16943
+ var RUNTIME_ACCESS_KINDS = new Set(Object.keys(RUNTIME_ACCESS_SUFFIXES));
16944
+ function runtimeAccessEnvNames(kind, envPrefix) {
16945
+ const suffixes = RUNTIME_ACCESS_SUFFIXES[kind];
16946
+ if (suffixes === void 0)
16947
+ return [];
16948
+ return suffixes.map((suffix) => `${envPrefix}${suffix}`);
16949
+ }
16634
16950
  var EgressRuleSchema = external_exports.object({
16635
16951
  componentId: ComponentIdSchema,
16636
16952
  destination: external_exports.discriminatedUnion("kind", [
@@ -17126,29 +17442,34 @@ function checkDeploymentBlueprint(blueprint, ctx) {
17126
17442
  });
17127
17443
  blueprint.components.forEach((component, index) => {
17128
17444
  if (component.ingress.exposure === "public") {
17129
- const mechanismPath = ["components", index, "ingress", "humanSessionMechanisms"];
17130
- const declared = component.ingress.humanSessionMechanisms;
17131
- if (component.ingress.authentication === "tenant-human-session") {
17132
- if (declared === void 0) {
17133
- ctx.addIssue({
17134
- code: external_exports.ZodIssueCode.custom,
17135
- path: mechanismPath,
17136
- message: "A component declaring `tenant-human-session` must name which approved mechanisms it implements, so a customer can tell whether it fits their organization before they deploy it."
17137
- });
17138
- } else if (new Set(declared).size !== declared.length) {
17445
+ const ingress = component.ingress;
17446
+ checkHumanSessionMechanisms(ctx, ["components", index, "ingress"], ingress.authentication, ingress.humanSessionMechanisms);
17447
+ const surfaces = ingress.surfaces ?? [];
17448
+ const base = ingress.pathPrefix.replace(/\/+$/, "");
17449
+ surfaces.forEach((surface, surfaceIndex) => {
17450
+ const surfacePath = ["components", index, "ingress", "surfaces", surfaceIndex];
17451
+ checkHumanSessionMechanisms(ctx, surfacePath, surface.authentication, surface.humanSessionMechanisms);
17452
+ const prefix = surface.pathPrefix.replace(/\/+$/, "");
17453
+ if (!(prefix.startsWith(`${base}/`) && prefix.length > base.length + 1)) {
17139
17454
  ctx.addIssue({
17140
17455
  code: external_exports.ZodIssueCode.custom,
17141
- path: mechanismPath,
17142
- message: "Each mechanism may be named once."
17456
+ path: [...surfacePath, "pathPrefix"],
17457
+ message: `A surface covers part of the component's routes, so its prefix must lie under "${ingress.pathPrefix}" and differ from it.`
17143
17458
  });
17144
17459
  }
17145
- } else if (declared !== void 0) {
17146
- ctx.addIssue({
17147
- code: external_exports.ZodIssueCode.custom,
17148
- path: mechanismPath,
17149
- message: "Only a component whose ingress authentication is `tenant-human-session` signs people in itself; remove this, or declare that authentication."
17460
+ surfaces.forEach((other, otherIndex) => {
17461
+ if (otherIndex <= surfaceIndex)
17462
+ return;
17463
+ const otherPrefix = other.pathPrefix.replace(/\/+$/, "");
17464
+ if (otherPrefix === prefix || otherPrefix.startsWith(`${prefix}/`) || prefix.startsWith(`${otherPrefix}/`)) {
17465
+ ctx.addIssue({
17466
+ code: external_exports.ZodIssueCode.custom,
17467
+ path: ["components", index, "ingress", "surfaces", otherIndex, "pathPrefix"],
17468
+ message: `Surfaces "${surface.pathPrefix}" and "${other.pathPrefix}" overlap; a path may be claimed by one surface only.`
17469
+ });
17470
+ }
17150
17471
  });
17151
- }
17472
+ });
17152
17473
  }
17153
17474
  const envNames = component.env.map((binding) => binding.name);
17154
17475
  const duplicateEnv = envNames.filter((name, i) => envNames.indexOf(name) !== i);
@@ -17478,11 +17799,11 @@ function checkDeploymentBlueprint(blueprint, ctx) {
17478
17799
  if (!access)
17479
17800
  return;
17480
17801
  const path = ["dataStores", index, "runtimeAccess"];
17481
- if (store.kind !== "object-storage") {
17802
+ if (!RUNTIME_ACCESS_KINDS.has(store.kind)) {
17482
17803
  ctx.addIssue({
17483
17804
  code: external_exports.ZodIssueCode.custom,
17484
17805
  path,
17485
- message: `Runtime access is only available for an object-storage store; "${store.id}" is a ${store.kind}.`
17806
+ message: `Runtime access is only available for a ${[...RUNTIME_ACCESS_KINDS].sort().join(" or ")} store; "${store.id}" is a ${store.kind}.`
17486
17807
  });
17487
17808
  }
17488
17809
  const first = usedPrefixes.get(access.envPrefix);
@@ -17494,7 +17815,7 @@ function checkDeploymentBlueprint(blueprint, ctx) {
17494
17815
  });
17495
17816
  }
17496
17817
  usedPrefixes.set(access.envPrefix, index);
17497
- const derived = new Set(["_ENDPOINT_URL", "_TENANT_ID", "_PREFIX", "_SIGNING_KEY"].map((suffix) => `${access.envPrefix}${suffix}`));
17818
+ const derived = new Set(runtimeAccessEnvNames(store.kind, access.envPrefix));
17498
17819
  const binders = new Set(store.bindings.map((binding) => binding.componentId));
17499
17820
  for (const component of blueprint.components) {
17500
17821
  if (!binders.has(component.id))
@@ -17695,6 +18016,30 @@ function checkDeploymentBlueprint(blueprint, ctx) {
17695
18016
  }
17696
18017
  }
17697
18018
  }
18019
+ function checkHumanSessionMechanisms(ctx, at, authentication, declared) {
18020
+ const mechanismPath = [...at, "humanSessionMechanisms"];
18021
+ if (authentication === "tenant-human-session") {
18022
+ if (declared === void 0) {
18023
+ ctx.addIssue({
18024
+ code: external_exports.ZodIssueCode.custom,
18025
+ path: mechanismPath,
18026
+ message: "A component declaring `tenant-human-session` must name which approved mechanisms it implements, so a customer can tell whether it fits their organization before they deploy it."
18027
+ });
18028
+ } else if (new Set(declared).size !== declared.length) {
18029
+ ctx.addIssue({
18030
+ code: external_exports.ZodIssueCode.custom,
18031
+ path: mechanismPath,
18032
+ message: "Each mechanism may be named once."
18033
+ });
18034
+ }
18035
+ } else if (declared !== void 0) {
18036
+ ctx.addIssue({
18037
+ code: external_exports.ZodIssueCode.custom,
18038
+ path: mechanismPath,
18039
+ message: "Only a component whose ingress authentication is `tenant-human-session` signs people in itself; remove this, or declare that authentication."
18040
+ });
18041
+ }
18042
+ }
17698
18043
 
17699
18044
  // ../product-contracts/dist/deployability.js
17700
18045
  var VIRTUAL_MACHINE_PROFILE_INTERFACE = {
@@ -17868,7 +18213,7 @@ withJsonSchemaAnnex(ProductSubmissionSchema, PUBLISH_RULE_ANNEX);
17868
18213
  withJsonSchemaAnnex(ProductSubmissionV2Schema, PUBLISH_RULE_ANNEX);
17869
18214
 
17870
18215
  // src/manifest.ts
17871
- var import_yaml = __toESM(require_dist(), 1);
18216
+ var import_yaml2 = __toESM(require_dist(), 1);
17872
18217
  function readManifest(path) {
17873
18218
  let text;
17874
18219
  try {
@@ -17878,7 +18223,7 @@ function readManifest(path) {
17878
18223
  }
17879
18224
  let parsed;
17880
18225
  try {
17881
- parsed = (0, import_yaml.parse)(text, { prettyErrors: false });
18226
+ parsed = (0, import_yaml2.parse)(text, { prettyErrors: false });
17882
18227
  } catch (error) {
17883
18228
  return {
17884
18229
  ok: false,
@@ -18041,8 +18386,14 @@ async function ensureSession(args) {
18041
18386
  }
18042
18387
  };
18043
18388
  }
18389
+ function resolveProjectWithSource(flag, stored) {
18390
+ if (flag !== void 0) return { value: flag, source: "--project" };
18391
+ const selected = stored.context?.projectId;
18392
+ if (selected !== void 0) return { value: selected, source: "the stored context" };
18393
+ return null;
18394
+ }
18044
18395
  function resolveProject(flag, stored) {
18045
- return flag ?? stored.context?.projectId ?? null;
18396
+ return resolveProjectWithSource(flag, stored)?.value ?? null;
18046
18397
  }
18047
18398
 
18048
18399
  // src/operations.ts
@@ -18256,7 +18607,6 @@ function printPlan(plan, json) {
18256
18607
  }
18257
18608
  }
18258
18609
  }
18259
- lines.push(...requiredSecretLines(plan.requiredSecrets ?? []));
18260
18610
  if ((plan.secretsToWrite ?? []).length > 0) {
18261
18611
  lines.push(` secrets to set: ${plan.secretsToWrite.join(", ")}`);
18262
18612
  for (const line of secretWriteConsequence(plan)) lines.push(` ${line}`);
@@ -18692,13 +19042,18 @@ async function runDoctor(parsed) {
18692
19042
  detail: `${endpoint} (from ${resolved.source})`,
18693
19043
  facts: { endpoint, source: resolved.source }
18694
19044
  });
18695
- const selected = stored.context?.projectId ?? null;
19045
+ const project = resolveProjectWithSource(parsed.flags["project"], stored);
19046
+ const storedProjectId = stored.context?.projectId ?? null;
18696
19047
  checks.push({
18697
19048
  name: "Project",
18698
19049
  ok: true,
18699
- note: selected === null,
18700
- detail: selected === null ? "none selected. Commands need `--project prj_\u2026`, or run `vfac context set --project prj_\u2026` once. This is not a fault \u2014 it is what a pipeline that names the Project per command looks like \u2014 but it is NOT the same thing as which Projects your credential reaches." : `${selected} (selected in this context; which Projects the credential reaches is a separate question)`,
18701
- facts: { selectedProjectId: selected }
19050
+ note: storedProjectId === null,
19051
+ detail: project === null ? "none selected. Commands need `--project prj_\u2026`, or run `vfac context set --project prj_\u2026` once. This is not a fault \u2014 it is what a pipeline that names the Project per command looks like \u2014 but it is NOT the same thing as which Projects your credential reaches." : project.source === "--project" ? `${project.value} (from --project, for this command only). ` + (storedProjectId === null ? "Nothing is selected in this context, so every other command needs the flag too. " : `The stored context still selects ${storedProjectId}, which is what a command with no --project uses. `) + "Which Projects the credential reaches is a separate question." : `${project.value} (from the stored context; a command with no --project uses it, and which Projects the credential reaches is a separate question)`,
19052
+ facts: {
19053
+ selectedProjectId: project?.value ?? null,
19054
+ source: project?.source ?? null,
19055
+ storedProjectId
19056
+ }
18702
19057
  });
18703
19058
  const probe = await probeMachineApi({ endpoint });
18704
19059
  if (!probe.ok) {
@@ -18733,13 +19088,13 @@ async function runDoctor(parsed) {
18733
19088
  return report2(checks, json);
18734
19089
  }
18735
19090
  const access = who.data.access;
18736
- const projects = access.projects.map((project) => project.projectId);
19091
+ const projects = access.projects.map((project2) => project2.projectId);
18737
19092
  const mutating = access.projects.filter(
18738
- (project) => project.permissions.some((permission) => !permission.endsWith(".read"))
19093
+ (project2) => project2.permissions.some((permission) => !permission.endsWith(".read"))
18739
19094
  );
18740
19095
  const organizationWide = access.tenantPermissions.includes("project.read");
18741
19096
  const alsoOrganizationWide = access.tenantPermissions.length > 0 ? `Organization-wide it holds ${access.tenantPermissions.join(", ")}, which reaches no Project. ` : "";
18742
- const perProject = projects.length === 0 ? "" : `. Granted per Project: ${projects.join(", ")}` + (mutating.length > 0 ? ` \u2014 you can deploy in ${mutating.map((project) => project.projectId).join(", ")}` : " \u2014 read-only there");
19097
+ const perProject = projects.length === 0 ? "" : `. Granted per Project: ${projects.join(", ")}` + (mutating.length > 0 ? ` \u2014 you can deploy in ${mutating.map((project2) => project2.projectId).join(", ")}` : " \u2014 read-only there");
18743
19098
  checks.push(
18744
19099
  organizationWide ? {
18745
19100
  name: "Access",
@@ -18898,7 +19253,6 @@ function report2(checks, json) {
18898
19253
  }
18899
19254
 
18900
19255
  // src/commands/export.ts
18901
- var import_yaml2 = __toESM(require_dist(), 1);
18902
19256
  async function runExport(parsed) {
18903
19257
  if (parsed.command[1] !== "product-instance" && parsed.command[1] !== "resource") {
18904
19258
  process.stderr.write("Usage: vfac export product-instance pri_\u2026\n");
@@ -18926,7 +19280,7 @@ async function runExport(parsed) {
18926
19280
  if (!result.ok) return printError(result.error, parsed.flags["output"] === "json");
18927
19281
  process.stdout.write(
18928
19282
  parsed.flags["output"] === "json" ? `${JSON.stringify(result.data, null, 2)}
18929
- ` : (0, import_yaml2.stringify)(result.data)
19283
+ ` : manifestYaml(result.data)
18930
19284
  );
18931
19285
  const metadata = result.data["metadata"];
18932
19286
  const hasKey = typeof metadata === "object" && metadata !== null && "key" in metadata;
@@ -19338,16 +19692,44 @@ function renderProduct(product) {
19338
19692
  (profile) => `${profile.id}${profile.default ? " (default)" : ""}`
19339
19693
  );
19340
19694
  lines.push(` profile ${profiles.join(", ") || "(none declared)"}`);
19341
- const seed = seedConfiguration(product.configuration);
19342
- if (seed.open.length > 0 || seed.requiredSecrets.length > 0) {
19343
- lines.push("", " Required configuration");
19344
- for (const name of seed.requiredSecrets) {
19345
- lines.push(` ${name} (secret \u2014 pass --secret ${name}=env:VAR)`);
19346
- }
19347
- for (const setting of seed.open) {
19348
- lines.push(` ${setting.label} ${setting.type ?? ""}`.trimEnd());
19695
+ const settings = configurationRequirements(product.configuration);
19696
+ if (settings.length > 0) {
19697
+ lines.push("", ` Settings (${settings.length})`);
19698
+ for (const setting of settings) {
19699
+ lines.push(
19700
+ ` ${identityOf(setting)} ${setting.secret ? "secret" : setting.type} ${TOKEN[setting.requiredness]}`
19701
+ );
19702
+ const requirement = requirementOf(setting);
19703
+ if (requirement !== null) lines.push(...continuation(requirement));
19704
+ if (setting.requires.length > 0 && !mutualClause(setting)) {
19705
+ lines.push(
19706
+ ...continuation(
19707
+ `answering it also requires ${listOf(
19708
+ setting.requires.map((member) => identityOf(member)),
19709
+ "and"
19710
+ )}`
19711
+ )
19712
+ );
19713
+ }
19714
+ if (setting.condition?.carrier === "container") {
19715
+ lines.push(
19716
+ ...continuation(
19717
+ `${printableConfigurationPath(setting.condition.carrierPath)} is required when ${setting.condition.printableSetting} is ${valueList(setting.condition.anyOf)}`
19718
+ )
19719
+ );
19720
+ }
19721
+ if (setting.secret) {
19722
+ lines.push(...continuation(`--secret ${String(setting.path)}=env:VAR`));
19723
+ }
19724
+ if (setting.allowed.length > 0) {
19725
+ lines.push(...continuation(`one of ${valueList(setting.allowed)}`));
19726
+ }
19727
+ if (setting.defaultValue !== void 0) {
19728
+ lines.push(...continuation(`default ${JSON.stringify(setting.defaultValue)}`));
19729
+ }
19349
19730
  }
19350
19731
  }
19732
+ const seed = seedConfiguration(product.configuration);
19351
19733
  const together = seed.groups.filter((group) => group.within !== null);
19352
19734
  if (together.length > 0) {
19353
19735
  lines.push("", " Settings that go together");
@@ -19392,14 +19774,74 @@ function renderProduct(product) {
19392
19774
  }
19393
19775
  const conditions = product.conditions ?? [];
19394
19776
  if (conditions.length > 0) {
19395
- lines.push("", " Needs setting up outside the platform");
19777
+ lines.push("", " Conditions");
19396
19778
  for (const condition of conditions) {
19397
19779
  lines.push(` ${condition.key ?? ""} ${condition.displayName ?? ""}`.trimEnd());
19398
19780
  }
19399
19781
  }
19400
- lines.push("", " `--output json` carries the whole declaration, this is a readable subset.");
19782
+ lines.push(
19783
+ "",
19784
+ " `--output json` carries the whole declaration. Every setting is listed above;",
19785
+ " descriptions and value constraints are in the JSON form."
19786
+ );
19787
+ if (settings.some((setting) => setting.secret)) {
19788
+ lines.push(
19789
+ " A credential is passed with --secret when a resource is created, and afterwards only",
19790
+ " when you set or change one \u2014 never written into a manifest."
19791
+ );
19792
+ }
19401
19793
  return lines.join("\n");
19402
19794
  }
19795
+ var TOKEN = {
19796
+ always: "required",
19797
+ "when-value": "required-when",
19798
+ "when-group": "required-if",
19799
+ "with-siblings": "required-with",
19800
+ optional: "optional"
19801
+ };
19802
+ var MAX_LISTED_VALUES = 8;
19803
+ var identityOf = (setting) => setting.secret ? String(setting.path) : setting.printablePath;
19804
+ function valueList(values) {
19805
+ const shown = values.slice(0, MAX_LISTED_VALUES).map((value) => JSON.stringify(value));
19806
+ const rest = values.length - shown.length;
19807
+ const joined = shown.length === 2 ? shown.join(" or ") : shown.join(", ");
19808
+ return rest > 0 ? `${joined} and ${rest} more` : joined;
19809
+ }
19810
+ function requirementOf(setting) {
19811
+ const list = setting.list === null ? "" : ` ${setting.list.printablePath}`;
19812
+ switch (setting.requiredness) {
19813
+ case "always":
19814
+ return setting.perRow ? `in every${list} entry` : null;
19815
+ case "when-value": {
19816
+ const condition = setting.condition;
19817
+ if (condition === null) return null;
19818
+ const rows = condition.anyRow ? " in any entry" : "";
19819
+ return `when ${condition.printableSetting} is ${valueList(condition.anyOf)}${rows}`;
19820
+ }
19821
+ case "when-group": {
19822
+ const owner = setting.containers[setting.containers.length - 1];
19823
+ const base = `once ${owner?.printablePath ?? ""} is configured`;
19824
+ return setting.perRow ? `${base}, in every${list} entry` : base;
19825
+ }
19826
+ case "with-siblings": {
19827
+ const names = (members) => members.map((member) => identityOf(member));
19828
+ const triggers = names(setting.requiredBy);
19829
+ return mutualClause(setting) ? `with ${names(setting.requires).join(", ")}` : `once ${listOf(triggers, "or")} is set`;
19830
+ }
19831
+ case "optional":
19832
+ return setting.perRow ? `one per${list} entry` : null;
19833
+ }
19834
+ }
19835
+ function mutualClause(setting) {
19836
+ const requires = setting.requires.map((member) => String(member.path)).sort();
19837
+ const requiredBy = setting.requiredBy.map((member) => String(member.path)).sort();
19838
+ return requires.length > 0 && requires.join("\0") === requiredBy.join("\0");
19839
+ }
19840
+ function listOf(items, conjunction) {
19841
+ if (items.length <= 1) return items[0] ?? "";
19842
+ return `${items.slice(0, -1).join(", ")} ${conjunction} ${items[items.length - 1]}`;
19843
+ }
19844
+ var continuation = (sentence) => wrap(sentence, 72).map((line) => ` ${line}`);
19403
19845
  function wrap(text, width) {
19404
19846
  const lines = [];
19405
19847
  for (const paragraph of text.split(/\n\s*\n/)) {
@@ -19582,7 +20024,6 @@ Only approve a code you are looking at right now. Waiting\u2026
19582
20024
  }
19583
20025
 
19584
20026
  // src/commands/manifest-init.ts
19585
- var import_yaml3 = __toESM(require_dist(), 1);
19586
20027
  async function runManifestInit(parsed) {
19587
20028
  const verb = parsed.command[1];
19588
20029
  if (verb !== "init") {
@@ -19687,7 +20128,7 @@ ${where}`);
19687
20128
  }
19688
20129
  process.stdout.write(
19689
20130
  parsed.flags["output"] === "json" ? `${JSON.stringify(document, null, 2)}
19690
- ` : (0, import_yaml3.stringify)(document)
20131
+ ` : manifestYaml(document)
19691
20132
  );
19692
20133
  if (notes.length > 0) {
19693
20134
  process.stderr.write("\nStill yours to fill in:\n");
@@ -19901,12 +20342,16 @@ Options
19901
20342
  --output json, for a pipeline that reads the answer
19902
20343
 
19903
20344
  Diagnosing
19904
- vfac doctor checks the endpoint, the machine API, whether this client is new
19905
- enough for that platform, the credential and what that credential
19906
- can reach \u2014 in that order, so the first thing that is actually
19907
- wrong is the thing it reports. It ends by asking npm whether a
19908
- newer vfac has been published, which is a note and never a
19909
- failure. Set VFAC_NO_UPDATE_CHECK to skip that one request
20345
+ vfac doctor checks the config it could read, the endpoint, which Project this
20346
+ terminal has selected and which one the command will act in, the
20347
+ machine API, whether this client is new enough for that platform,
20348
+ how this terminal is signed in and what that credential can reach
20349
+ \u2014 in that order, so the first thing that is actually wrong is the
20350
+ thing it reports. Pass --project to ask about the Project a
20351
+ command would name, which changes nothing on disk. It ends by
20352
+ asking npm whether a newer vfac has been published, which is a
20353
+ note and never a failure. Set VFAC_NO_UPDATE_CHECK to skip that
20354
+ one request
19910
20355
  vfac whoami what this credential is, and which Project scopes it has been
19911
20356
  GRANTED. A credential scoped to one Project deploys fine and
19912
20357
  cannot make organization-wide reads \u2014 this is where that is said.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@varde-flyt/vfac",
3
- "version": "0.15.0",
3
+ "version": "0.15.3",
4
4
  "description": "Deploy and manage Varde Flyt Products from a manifest.",
5
5
  "repository": {
6
6
  "type": "git",