@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.
- package/README.md +27 -15
- package/dist/vfac.mjs +521 -76
- 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
|
|
30
|
-
|
|
31
|
-
terminal is signed in and what that credential
|
|
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. `
|
|
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
|
|
92
|
-
file you commit.
|
|
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`
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
233
|
-
|
|
234
|
-
the
|
|
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
|
-
// ../
|
|
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
|
-
*
|
|
15972
|
-
*
|
|
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:
|
|
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:
|
|
16004
|
-
pathPrefix:
|
|
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
|
|
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:
|
|
16606
|
-
*
|
|
16607
|
-
*
|
|
16608
|
-
*
|
|
16609
|
-
*
|
|
16610
|
-
*
|
|
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
|
|
16615
|
-
* `_TENANT_ID
|
|
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
|
|
17130
|
-
|
|
17131
|
-
|
|
17132
|
-
|
|
17133
|
-
|
|
17134
|
-
|
|
17135
|
-
|
|
17136
|
-
|
|
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:
|
|
17142
|
-
message:
|
|
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
|
-
|
|
17146
|
-
|
|
17147
|
-
|
|
17148
|
-
|
|
17149
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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:
|
|
18700
|
-
detail:
|
|
18701
|
-
facts: {
|
|
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((
|
|
19091
|
+
const projects = access.projects.map((project2) => project2.projectId);
|
|
18737
19092
|
const mutating = access.projects.filter(
|
|
18738
|
-
(
|
|
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((
|
|
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
|
-
` : (
|
|
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
|
|
19342
|
-
if (
|
|
19343
|
-
lines.push("",
|
|
19344
|
-
for (const
|
|
19345
|
-
lines.push(
|
|
19346
|
-
}
|
|
19347
|
-
|
|
19348
|
-
|
|
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("", "
|
|
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(
|
|
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
|
-
` : (
|
|
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
|
|
19905
|
-
|
|
19906
|
-
|
|
19907
|
-
|
|
19908
|
-
|
|
19909
|
-
|
|
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.
|