@aifabrix/builder 2.60.0 → 2.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -11
- package/docs/README.md +80 -0
- package/docs/builder-help/evidence-patterns.json +155 -0
- package/docs/builder-help/golden-examples/crm-company.json +30 -0
- package/docs/builder-help/golden-examples/crm-deal.json +29 -0
- package/docs/builder-help/golden-examples/document-storage-keyed-get.json +85 -0
- package/docs/builder-help/golden-examples/document-storage.json +30 -0
- package/docs/builder-help/golden-examples/meeting-transcript.json +29 -0
- package/docs/builder-help/golden-examples/repository-template.json +29 -0
- package/docs/builder-help/golden-examples/service-ticket.json +29 -0
- package/docs/builder-help/platform-roles.json +98 -0
- package/docs/builder-help/resource-type-catalog.json +402 -0
- package/lib/api/applications.api.js +22 -2
- package/lib/api/bootstrap-snapshot.api.js +115 -0
- package/lib/api/configuration.api.js +131 -0
- package/lib/api/system-secrets.api.js +72 -0
- package/lib/app/deploy.js +14 -5
- package/lib/app/run-docker-fallback.js +6 -1
- package/lib/app/run-env-compose.js +5 -6
- package/lib/app/run-helpers.js +2 -1
- package/lib/app/run-parameter-sync.js +142 -0
- package/lib/app/run.js +6 -4
- package/lib/build/docker-build-args.js +5 -2
- package/lib/build/index.js +3 -2
- package/lib/build/standard-docker-build.js +6 -2
- package/lib/channels/channel-artifact-generator.js +13 -3
- package/lib/cli/infra-guided-footers.js +10 -8
- package/lib/cli/infra-guided.js +30 -10
- package/lib/cli/setup-app.js +5 -0
- package/lib/cli/setup-environment.js +156 -0
- package/lib/cli/setup-infra-up-platform-action.js +14 -1
- package/lib/cli/setup-platform.js +2 -0
- package/lib/commands/auth-config.js +2 -0
- package/lib/commands/datasource-capability-upsert-cli.js +112 -0
- package/lib/commands/datasource-capability.js +4 -2
- package/lib/commands/env-secret-context.js +113 -0
- package/lib/commands/env-secret-list.js +200 -0
- package/lib/commands/env-secret-push-confirm.js +54 -0
- package/lib/commands/env-secret-push-run.js +227 -0
- package/lib/commands/env-secret-push.js +168 -0
- package/lib/commands/login.js +4 -0
- package/lib/commands/repair-datasource-apply.js +5 -1
- package/lib/commands/repair-datasource-keyed-document.js +122 -0
- package/lib/commands/repair-datasource-legacy.js +94 -0
- package/lib/commands/repair-datasource-run.js +1 -0
- package/lib/commands/setup-image-refresh.js +26 -0
- package/lib/commands/setup-modes.js +8 -9
- package/lib/commands/setup-prompts-platform-mode.js +34 -0
- package/lib/commands/setup-prompts.js +2 -180
- package/lib/commands/setup.js +3 -0
- package/lib/commands/up-builder-api.js +24 -8
- package/lib/commands/up-common.js +16 -1
- package/lib/commands/up-dataplane-bootstrap.js +70 -14
- package/lib/commands/up-dataplane-credentials.js +21 -0
- package/lib/commands/up-dataplane.js +37 -18
- package/lib/commands/up-integration-server.js +112 -0
- package/lib/commands/verify-operations-skip-e2e.js +25 -1
- package/lib/commands/verify-operations-steps.js +13 -1
- package/lib/commands/wizard-config-normalizer.js +12 -5
- package/lib/commands/wizard-core.js +5 -157
- package/lib/commands/wizard-file-saving.js +163 -0
- package/lib/core/admin-secrets-env-overlay.js +8 -3
- package/lib/core/env-platform-expand.js +5 -1
- package/lib/core/environment-access-policy.js +121 -0
- package/lib/core/environment-mode-policy.js +177 -0
- package/lib/core/local-env-overrides.js +149 -0
- package/lib/core/secrets-ensure.js +4 -2
- package/lib/core/secrets-env-content.js +17 -10
- package/lib/core/secrets-env-write.js +11 -4
- package/lib/core/secrets-load.js +4 -2
- package/lib/datasource/binary-documents-validator.js +190 -0
- package/lib/datasource/capability/capability-manifest.js +1 -0
- package/lib/datasource/capability/run-capability-upsert.js +202 -0
- package/lib/datasource/capability/upsert-ingredients.js +291 -0
- package/lib/datasource/capability/upsert-operations.js +138 -0
- package/lib/datasource/capability/upsert-test-scaffold.js +189 -0
- package/lib/datasource/validate.js +12 -5
- package/lib/deployment/installation/index.js +14 -1
- package/lib/deployment/installation/infra-catalog.js +3 -0
- package/lib/deployment/installation/local-environment-stage.js +285 -0
- package/lib/deployment/installation/local-installation.js +76 -0
- package/lib/deployment/installation/stage-input.js +5 -1
- package/lib/generator/builders.js +3 -1
- package/lib/generator/index.js +3 -0
- package/lib/generator/split-variables.js +2 -2
- package/lib/lifecycle/product-model.js +4 -3
- package/lib/lifecycle/report-display.js +3 -2
- package/lib/parameters/database-adoption.js +125 -0
- package/lib/parameters/infra-parameter-catalog.js +1 -1
- package/lib/parameters/physical-database-naming.js +136 -0
- package/lib/parameters/physical-secret-name.js +194 -0
- package/lib/parameters/rsa-secret-values.js +165 -0
- package/lib/programmatic/builder-help-enterprise-sync-fabrix.js +1 -1
- package/lib/programmatic/builder-help-governance.js +1 -1
- package/lib/programmatic/builder-help.js +1 -1
- package/lib/programmatic/run-channel-install-artifacts.js +28 -2
- package/lib/programmatic/run-channel-package.js +3 -1
- package/lib/programmatic/workspace-context.js +37 -2
- package/lib/role-assistant/test-runner-workhub-answers.js +4 -1
- package/lib/role-assistant/test-runner-workhub-missing-fields.js +42 -0
- package/lib/role-assistant/test-runner-workhub-wait-stop.js +94 -0
- package/lib/role-assistant/test-runner-workhub.js +28 -19
- package/lib/schema/application-schema.json +14 -2
- package/lib/schema/external-datasource.schema.json +23 -3
- package/lib/schema/infra.parameter.yaml +440 -56
- package/lib/schema/infrastructure-schema.json +213 -120
- package/lib/utils/compose-generate-docker-compose.js +9 -14
- package/lib/utils/compose-generator.js +4 -7
- package/lib/utils/compose-miso-env.js +2 -2
- package/lib/utils/compose-traefik-ingress-base.js +3 -4
- package/lib/utils/datasource-binary-evidence.js +92 -0
- package/lib/utils/datasource-test-run-capability-scope.js +44 -1
- package/lib/utils/datasource-test-run-debug-display.js +2 -0
- package/lib/utils/datasource-test-run-display.js +8 -2
- package/lib/utils/datasource-test-run-issue-guidance.js +176 -0
- package/lib/utils/datasource-test-run-tty-log.js +2 -0
- package/lib/utils/declarative-url-ports.js +3 -0
- package/lib/utils/env-copy.js +11 -10
- package/lib/utils/env-environment-file-paths.js +105 -0
- package/lib/utils/environment-scoped-resources.js +48 -14
- package/lib/utils/external-system-system-test-tty.js +3 -2
- package/lib/utils/image-tags.js +2 -2
- package/lib/utils/paths-system-builder-keys.js +7 -1
- package/lib/utils/paths.js +7 -2
- package/lib/utils/platform-kv-ref.js +1 -1
- package/lib/utils/postgres-platform-bootstrap.js +35 -1
- package/lib/utils/prepare-local-data-mount.js +58 -0
- package/lib/utils/redis-env-scope.js +21 -1
- package/lib/utils/registry-auth-sources.js +154 -0
- package/lib/utils/registry-credentials.js +131 -14
- package/lib/utils/secrets-generator.js +30 -21
- package/lib/utils/secrets-helpers.js +5 -8
- package/lib/utils/secrets-kv-scope.js +127 -28
- package/lib/utils/secrets-materialize-local.js +9 -0
- package/lib/utils/secrets-missing-error.js +35 -1
- package/lib/utils/system-secret-mapping.js +125 -0
- package/lib/utils/test-log-writer.js +2 -1
- package/lib/utils/token-manager.js +14 -8
- package/lib/utils/url-public-path-prefix.js +6 -10
- package/lib/validation/external-manifest-validator.js +5 -0
- package/lib/validation/openapi-contract-surface-validator.js +3 -1
- package/lib/validation/validate-external-file.js +5 -1
- package/package.json +6 -4
- package/templates/agent-kit/agent-kit.yaml +1 -1
- package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +2 -1
- package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +15 -0
- package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +3 -2
- package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +6 -2
- package/templates/agent-kit/skills/aifabrix-prove/references/evidence-lifecycle.md +22 -0
- package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +7 -1
- package/templates/agent-kit/skills/aifabrix-role-assistant/references/testing-playbook.md +117 -0
- package/templates/agent-kit/skills/shared/interaction.md +83 -0
- package/templates/agent-kit/skills/shared/status.md +3 -12
- package/templates/applications/builder-api/application.yaml +1 -1
- package/templates/applications/builder-api/env.template +5 -1
- package/templates/applications/dataplane/application.yaml +2 -2
- package/templates/applications/dataplane/env.template +10 -12
- package/templates/applications/integration-server/application.yaml +91 -0
- package/templates/applications/integration-server/env.template +215 -0
- package/templates/applications/keycloak/application.yaml +6 -1
- package/templates/applications/mirrored-templates.json +52 -0
- package/templates/applications/miso-controller/application.yaml +116 -2
- package/templates/applications/miso-controller/env.template +62 -17
- package/templates/applications/miso-controller/rbac.yaml +17 -0
- package/templates/external-system/external-datasource.yaml.hbs +7 -1
- package/templates/marketplace/createUiDefinition.json +52 -4
- package/templates/marketplace/main.json +88 -10
- package/templates/python/docker-compose.hbs +1 -1
- package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +0 -41
- /package/{lib/programmatic/help-content → docs/builder-help/content}/channel-onboarding.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/cip-overview.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/connected-system-ui.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/dimensions-guide.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/enterprise-sync-fabrix.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/overview.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/subscription-guide.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/workflow.md +0 -0
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Host-scoped container registry credentials in the primary user secrets file.
|
|
3
3
|
*
|
|
4
|
-
* Keys are
|
|
5
|
-
* `secrets.local.yaml` (never the shared store)
|
|
6
|
-
*
|
|
4
|
+
* Keys are `registry-<slug>-username` and `registry-<slug>-password` in the user's
|
|
5
|
+
* `secrets.local.yaml` (never the shared store), where the slug is the host with every
|
|
6
|
+
* character outside `[a-z0-9]` replaced by `-`. The vault naming contract accepts
|
|
7
|
+
* alphanumerics and dashes only, so the earlier `<host>-username` form — which keeps the
|
|
8
|
+
* dots of `myreg.azurecr.io` — was local-only by accident rather than by design. The
|
|
9
|
+
* `registry-` prefix keeps a host that begins with an application key distinguishable from
|
|
10
|
+
* that application's own secrets in the same flat file.
|
|
11
|
+
*
|
|
12
|
+
* Pairs written under the old keys are still read, so an existing `dev set-registry` does
|
|
13
|
+
* not silently stop working, and removal clears both forms.
|
|
14
|
+
*
|
|
15
|
+
* Values are encrypted through the existing local-secret writer and reach Docker only via
|
|
16
|
+
* `docker login --password-stdin`.
|
|
7
17
|
*
|
|
8
18
|
* @fileoverview Registry credential storage, lookup, removal, and per-command Docker login
|
|
9
19
|
* @author AI Fabrix Team
|
|
@@ -25,12 +35,41 @@ const { normalizeRegistryHost } = require('./config-registry-preference');
|
|
|
25
35
|
const authenticatedHosts = new Set();
|
|
26
36
|
|
|
27
37
|
/**
|
|
38
|
+
* Host reduced to the vault naming charset: lowercase alphanumerics and dashes.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} host - Normalized registry host
|
|
41
|
+
* @returns {string} Slug safe for a secret key
|
|
42
|
+
*/
|
|
43
|
+
function registryKeySlug(host) {
|
|
44
|
+
return String(host)
|
|
45
|
+
.toLowerCase()
|
|
46
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
47
|
+
.replace(/^-+|-+$/g, '');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Secret keys for a registry, including the previous form so an existing pair still loads.
|
|
52
|
+
*
|
|
53
|
+
* `hostKey` records which host owns the slug. Two hosts can normalise to one slug
|
|
54
|
+
* (`a.b-c` and `a-b.c` both give `a-b-c`), and silently reusing another registry's
|
|
55
|
+
* credentials is worse than refusing, so a write compares against it.
|
|
56
|
+
*
|
|
28
57
|
* @param {string} registry - Registry host
|
|
29
|
-
* @returns {{ host: string, usernameKey: string, passwordKey: string
|
|
58
|
+
* @returns {{ host: string, slug: string, usernameKey: string, passwordKey: string,
|
|
59
|
+
* hostKey: string, legacyUsernameKey: string, legacyPasswordKey: string }}
|
|
30
60
|
*/
|
|
31
61
|
function registryCredentialKeys(registry) {
|
|
32
62
|
const host = normalizeRegistryHost(registry);
|
|
33
|
-
|
|
63
|
+
const slug = registryKeySlug(host);
|
|
64
|
+
return {
|
|
65
|
+
host,
|
|
66
|
+
slug,
|
|
67
|
+
usernameKey: `registry-${slug}-username`,
|
|
68
|
+
passwordKey: `registry-${slug}-password`,
|
|
69
|
+
hostKey: `registry-${slug}-host`,
|
|
70
|
+
legacyUsernameKey: `${host}-username`,
|
|
71
|
+
legacyPasswordKey: `${host}-password`
|
|
72
|
+
};
|
|
34
73
|
}
|
|
35
74
|
|
|
36
75
|
function hasValue(value) {
|
|
@@ -40,7 +79,18 @@ function hasValue(value) {
|
|
|
40
79
|
function readRawPair(keys) {
|
|
41
80
|
const file = pathsUtil.getPrimaryUserSecretsLocalPath();
|
|
42
81
|
const raw = loadExistingSecrets(file);
|
|
43
|
-
|
|
82
|
+
const current = { username: raw[keys.usernameKey], password: raw[keys.passwordKey] };
|
|
83
|
+
if (hasValue(current.username) || hasValue(current.password)) {
|
|
84
|
+
return { file, ...current, legacy: false, owner: raw[keys.hostKey] };
|
|
85
|
+
}
|
|
86
|
+
// Written before the keys were namespaced; read it rather than prompting again.
|
|
87
|
+
return {
|
|
88
|
+
file,
|
|
89
|
+
username: raw[keys.legacyUsernameKey],
|
|
90
|
+
password: raw[keys.legacyPasswordKey],
|
|
91
|
+
legacy: true,
|
|
92
|
+
owner: undefined
|
|
93
|
+
};
|
|
44
94
|
}
|
|
45
95
|
|
|
46
96
|
function partialPairError(keys) {
|
|
@@ -106,13 +156,36 @@ function removeKeysFromFile(file, keyNames) {
|
|
|
106
156
|
return true;
|
|
107
157
|
}
|
|
108
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Refuse to overwrite a different host's credentials when two hosts share a slug.
|
|
161
|
+
*
|
|
162
|
+
* @param {Object} keys - From {@link registryCredentialKeys}
|
|
163
|
+
* @param {Object} snapshot - From readRawPair
|
|
164
|
+
* @throws {Error} When the slug already belongs to another host
|
|
165
|
+
*/
|
|
166
|
+
function assertNoSlugCollision(keys, snapshot) {
|
|
167
|
+
const owner = typeof snapshot.owner === 'string' ? snapshot.owner.trim() : '';
|
|
168
|
+
if (!owner || owner === keys.host) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
throw new Error(
|
|
172
|
+
`Registry key collision: "${keys.host}" and "${owner}" both reduce to "${keys.slug}", ` +
|
|
173
|
+
`so storing this pair would overwrite the credentials for ${owner}. ` +
|
|
174
|
+
`Remove the other registry first with "aifabrix dev set-registry ${owner} --clear-credentials".`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
109
178
|
function restorePair(keys, snapshot) {
|
|
179
|
+
// Always clear what a save writes, then put back what the snapshot actually held — which
|
|
180
|
+
// may be under the old keys, since a pair written before namespacing is read from there.
|
|
110
181
|
const restore = {};
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
182
|
+
const [usernameKey, passwordKey] = snapshot.legacy
|
|
183
|
+
? [keys.legacyUsernameKey, keys.legacyPasswordKey]
|
|
184
|
+
: [keys.usernameKey, keys.passwordKey];
|
|
185
|
+
if (snapshot.username !== undefined) restore[usernameKey] = snapshot.username;
|
|
186
|
+
if (snapshot.password !== undefined) restore[passwordKey] = snapshot.password;
|
|
187
|
+
const remove = [keys.usernameKey, keys.passwordKey, keys.hostKey]
|
|
188
|
+
.filter((key) => !Object.prototype.hasOwnProperty.call(restore, key));
|
|
116
189
|
removeKeysFromFile(snapshot.file, remove);
|
|
117
190
|
mergeSecretsIntoFile(snapshot.file, restore);
|
|
118
191
|
}
|
|
@@ -130,14 +203,21 @@ async function saveRegistryCredentials(registry, username, password) {
|
|
|
130
203
|
throw new Error('Registry username and password must both be non-empty');
|
|
131
204
|
}
|
|
132
205
|
const snapshot = readRawPair(keys);
|
|
206
|
+
assertNoSlugCollision(keys, snapshot);
|
|
133
207
|
await config.ensureSecretsEncryptionKey();
|
|
134
208
|
try {
|
|
135
209
|
await saveLocalSecret(keys.usernameKey, String(username));
|
|
136
210
|
await saveLocalSecret(keys.passwordKey, String(password));
|
|
211
|
+
// Plain, not through saveLocalSecret: the host is not a secret, and encrypting it would
|
|
212
|
+
// make every later read compare a ciphertext against the host and report a collision.
|
|
213
|
+
mergeSecretsIntoFile(snapshot.file, { [keys.hostKey]: keys.host });
|
|
137
214
|
} catch (error) {
|
|
138
215
|
restorePair(keys, snapshot);
|
|
139
216
|
throw new Error(`Could not save registry credentials for ${keys.host}; previous values were restored (${error.message})`);
|
|
140
217
|
}
|
|
218
|
+
// The pair now lives under the namespaced keys; leaving the old one behind would keep a
|
|
219
|
+
// stale password readable and let a later read pick whichever it found first.
|
|
220
|
+
removeKeysFromFile(snapshot.file, [keys.legacyUsernameKey, keys.legacyPasswordKey]);
|
|
141
221
|
authenticatedHosts.delete(keys.host);
|
|
142
222
|
return { host: keys.host, rollback: () => restorePair(keys, snapshot) };
|
|
143
223
|
}
|
|
@@ -150,12 +230,24 @@ async function saveRegistryCredentials(registry, username, password) {
|
|
|
150
230
|
function removeRegistryCredentials(registry) {
|
|
151
231
|
const keys = registryCredentialKeys(registry);
|
|
152
232
|
authenticatedHosts.delete(keys.host);
|
|
153
|
-
return removeKeysFromFile(pathsUtil.getPrimaryUserSecretsLocalPath(), [
|
|
233
|
+
return removeKeysFromFile(pathsUtil.getPrimaryUserSecretsLocalPath(), [
|
|
234
|
+
keys.usernameKey,
|
|
235
|
+
keys.passwordKey,
|
|
236
|
+
keys.hostKey,
|
|
237
|
+
keys.legacyUsernameKey,
|
|
238
|
+
keys.legacyPasswordKey
|
|
239
|
+
]);
|
|
154
240
|
}
|
|
155
241
|
|
|
156
242
|
/**
|
|
157
|
-
* Authenticates Docker to the registry once per process: stored pair first, then the
|
|
158
|
-
* fallback
|
|
243
|
+
* Authenticates Docker to the registry once per process: stored pair first, then the
|
|
244
|
+
* optional fallback. The password is sent only on stdin.
|
|
245
|
+
*
|
|
246
|
+
* There is deliberately no default fallback. Most pulls are of public images and must stay
|
|
247
|
+
* anonymous; asking for a credential there would prompt for something the pull does not
|
|
248
|
+
* need. A caller that genuinely requires a credential passes one — see
|
|
249
|
+
* {@link ensureRegistryLoginWithSources}.
|
|
250
|
+
*
|
|
159
251
|
* @param {string} registry - Registry host
|
|
160
252
|
* @param {Object} [options] - Options
|
|
161
253
|
* @param {Function} [options.fallback] - async (host) => void when no pair is stored
|
|
@@ -180,6 +272,29 @@ async function ensureRegistryLogin(registry, options = {}) {
|
|
|
180
272
|
return 'none';
|
|
181
273
|
}
|
|
182
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Authenticate a registry that a command cannot proceed without.
|
|
277
|
+
*
|
|
278
|
+
* Adds the credential-source chain on top of {@link ensureRegistryLogin}: the stored pair,
|
|
279
|
+
* then `az acr login` for an `*.azurecr.io` host, then a prompt. A non-interactive run
|
|
280
|
+
* fails naming the host instead of hanging on a question nobody can answer.
|
|
281
|
+
*
|
|
282
|
+
* @param {string} registry - Registry host
|
|
283
|
+
* @param {Object} [sources] - Overrides for {@link buildRegistryAuthFallback}
|
|
284
|
+
* @returns {Promise<'cached'|'credentials'|'acr'|'prompt'>}
|
|
285
|
+
*/
|
|
286
|
+
async function ensureRegistryLoginWithSources(registry, sources = {}) {
|
|
287
|
+
const { buildRegistryAuthFallback } = require('./registry-auth-sources');
|
|
288
|
+
const fallback = buildRegistryAuthFallback(sources);
|
|
289
|
+
let outcome = null;
|
|
290
|
+
const result = await ensureRegistryLogin(registry, {
|
|
291
|
+
fallback: async(host) => {
|
|
292
|
+
outcome = await fallback(host);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
return result === 'fallback' ? outcome : result;
|
|
296
|
+
}
|
|
297
|
+
|
|
183
298
|
/**
|
|
184
299
|
* Registry host of an image reference, or '' for Docker Hub style refs.
|
|
185
300
|
* @param {string} imageRef - Image reference (`host/repo:tag`, `repo:tag`, ...)
|
|
@@ -224,12 +339,14 @@ function resetRegistryLoginCache(registry) {
|
|
|
224
339
|
}
|
|
225
340
|
|
|
226
341
|
module.exports = {
|
|
342
|
+
registryKeySlug,
|
|
227
343
|
registryCredentialKeys,
|
|
228
344
|
getRegistryCredentialStatus,
|
|
229
345
|
loadRegistryCredentials,
|
|
230
346
|
saveRegistryCredentials,
|
|
231
347
|
removeRegistryCredentials,
|
|
232
348
|
ensureRegistryLogin,
|
|
349
|
+
ensureRegistryLoginWithSources,
|
|
233
350
|
ensureLoginForImageRefs,
|
|
234
351
|
registryHostFromImageRef,
|
|
235
352
|
resetRegistryLoginCache
|
|
@@ -16,6 +16,7 @@ const yaml = require('js-yaml');
|
|
|
16
16
|
const crypto = require('crypto');
|
|
17
17
|
const logger = require('./logger');
|
|
18
18
|
const pathsUtil = require('./paths');
|
|
19
|
+
const { generateRsaSecretValues } = require('../parameters/rsa-secret-values');
|
|
19
20
|
const { mergeFlatSecretsYamlPreservingComments } = require('./secrets-yaml-preserve');
|
|
20
21
|
const {
|
|
21
22
|
generateDatabasePasswordValueForKey,
|
|
@@ -165,32 +166,40 @@ function generateDbUrlValue(key) {
|
|
|
165
166
|
* @param {Record<string, string>|undefined} placeholderContext - Merged infra.parameter.yaml defaults + CLI overrides
|
|
166
167
|
* @returns {string|undefined} Value if catalog handled key; undefined to use legacy rules
|
|
167
168
|
*/
|
|
168
|
-
|
|
169
|
-
|
|
169
|
+
/** Generators whose value comes straight from the catalog entry. */
|
|
170
|
+
const CATALOG_DIRECT_GENERATORS = Object.freeze([
|
|
171
|
+
'randomBytes32', 'randomAlphanumeric', 'password', 'emptyString', 'emptyAllowed', 'literal'
|
|
172
|
+
]);
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The catalog entry for a key, or nothing when the catalog cannot be read.
|
|
176
|
+
*
|
|
177
|
+
* @param {string} key - Secret key
|
|
178
|
+
* @returns {Object|undefined}
|
|
179
|
+
*/
|
|
180
|
+
function catalogEntryForKey(key) {
|
|
170
181
|
try {
|
|
171
|
-
|
|
182
|
+
return infraParameterCatalogModule().getInfraParameterCatalog().findEntryForKey(key);
|
|
172
183
|
} catch (err) {
|
|
173
184
|
logger.warn(`infra parameter catalog unavailable (${err.message}); using legacy generators.`);
|
|
174
185
|
return undefined;
|
|
175
186
|
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function tryGenerateSecretValueFromCatalog(key, placeholderContext) {
|
|
190
|
+
const catalogEntry = catalogEntryForKey(key);
|
|
176
191
|
if (!catalogEntry || !catalogEntry.generator) return undefined;
|
|
177
192
|
const g = catalogEntry.generator.type;
|
|
193
|
+
if (g === 'rsaKeyPair') {
|
|
194
|
+
return require('../parameters/rsa-secret-values').generateRsaValueFromCatalog(key, catalogEntry);
|
|
195
|
+
}
|
|
178
196
|
if (g === 'databasePassword') {
|
|
179
|
-
|
|
180
|
-
return v !== null ? v : undefined;
|
|
197
|
+
return generateDbPasswordValue(key) ?? undefined;
|
|
181
198
|
}
|
|
182
199
|
if (g === 'databaseUrl') {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (
|
|
187
|
-
g === 'randomBytes32' ||
|
|
188
|
-
g === 'randomAlphanumeric' ||
|
|
189
|
-
g === 'password' ||
|
|
190
|
-
g === 'emptyString' ||
|
|
191
|
-
g === 'emptyAllowed' ||
|
|
192
|
-
g === 'literal'
|
|
193
|
-
) {
|
|
200
|
+
return generateDbUrlValue(key) ?? undefined;
|
|
201
|
+
}
|
|
202
|
+
if (CATALOG_DIRECT_GENERATORS.includes(g)) {
|
|
194
203
|
return infraParameterCatalogModule().generateValueFromCatalogEntry(key, catalogEntry, crypto, placeholderContext);
|
|
195
204
|
}
|
|
196
205
|
return undefined;
|
|
@@ -411,15 +420,16 @@ async function generateMissingSecrets(envTemplate, secretsPath) {
|
|
|
411
420
|
return [];
|
|
412
421
|
}
|
|
413
422
|
|
|
414
|
-
const newSecrets =
|
|
423
|
+
const newSecrets = generateRsaSecretValues(missingKeys, existingSecrets);
|
|
415
424
|
for (const key of missingKeys) {
|
|
416
|
-
newSecrets[key] = generateSecretValue(key);
|
|
425
|
+
if (!(key in newSecrets)) newSecrets[key] = generateSecretValue(key);
|
|
417
426
|
}
|
|
418
427
|
|
|
419
428
|
appendSecretsToFile(resolvedPath, newSecrets);
|
|
420
429
|
|
|
421
|
-
|
|
422
|
-
|
|
430
|
+
const generatedKeys = Object.keys(newSecrets);
|
|
431
|
+
logger.log(`✔ Generated ${generatedKeys.length} missing secret key(s): ${generatedKeys.join(', ')}`);
|
|
432
|
+
return generatedKeys;
|
|
423
433
|
}
|
|
424
434
|
|
|
425
435
|
/**
|
|
@@ -479,4 +489,3 @@ module.exports = {
|
|
|
479
489
|
generateMissingSecrets,
|
|
480
490
|
createDefaultSecrets
|
|
481
491
|
};
|
|
482
|
-
|
|
@@ -55,7 +55,6 @@ const { resolveBashKvFromProcessEnv } = require('./secrets-bash-kv');
|
|
|
55
55
|
const EMPTY_ALLOWED_KV_FALLBACK = new Set([
|
|
56
56
|
'redis-passwordKeyVault',
|
|
57
57
|
'azure-openaiapi-urlKeyVault',
|
|
58
|
-
'secrets-azureOpenaiApiKeyVault',
|
|
59
58
|
'miso-controller-secrets-apiKeyVault'
|
|
60
59
|
]);
|
|
61
60
|
|
|
@@ -137,7 +136,7 @@ function getValueByPath(secrets, pathStr) {
|
|
|
137
136
|
return getValueByNestedPath(secrets, pathStr.split('/'));
|
|
138
137
|
}
|
|
139
138
|
|
|
140
|
-
const { getValueByPathWithEnvScope, mergeSecretsWithPrefixedCopies } =
|
|
139
|
+
const { getValueByPathWithEnvScope, mergeSecretsWithPrefixedCopies, prepareScopedKv } =
|
|
141
140
|
require('./secrets-kv-scope').createScopedKvHelpers(getValueByPath);
|
|
142
141
|
|
|
143
142
|
function resolveKvRefValue(secretsMap, pathStr, envKey, effective) {
|
|
@@ -156,8 +155,7 @@ function resolveKvRefValue(secretsMap, pathStr, envKey, effective) {
|
|
|
156
155
|
* @returns {string[]} Array of missing kv://<path> references (unique)
|
|
157
156
|
*/
|
|
158
157
|
function collectMissingSecrets(content, secrets, scopedKv = null) {
|
|
159
|
-
const effective =
|
|
160
|
-
const secretsMap = effective ? mergeSecretsWithPrefixedCopies(secrets, scopedKv.envKey) : secrets;
|
|
158
|
+
const { secretsMap, envKey, effective } = prepareScopedKv(secrets, scopedKv);
|
|
161
159
|
const seen = new Set();
|
|
162
160
|
const missing = [];
|
|
163
161
|
for (const line of content.split('\n')) {
|
|
@@ -169,7 +167,7 @@ function collectMissingSecrets(content, secrets, scopedKv = null) {
|
|
|
169
167
|
const pathStr = match[1];
|
|
170
168
|
if (seen.has(pathStr)) continue;
|
|
171
169
|
seen.add(pathStr);
|
|
172
|
-
const value = resolveKvRefValue(secretsMap, pathStr,
|
|
170
|
+
const value = resolveKvRefValue(secretsMap, pathStr, envKey, effective);
|
|
173
171
|
if ((value === undefined || value === null) && !isKvKeyAllowedEmptyWhenAbsent(pathStr)) {
|
|
174
172
|
missing.push(`kv://${pathStr}`);
|
|
175
173
|
}
|
|
@@ -217,8 +215,7 @@ function formatMissingSecretsFileInfo(secretsFilePaths) {
|
|
|
217
215
|
* @returns {string} Content with kv:// references replaced
|
|
218
216
|
*/
|
|
219
217
|
function replaceKvInContent(content, secrets, envVars, scopedKv = null) {
|
|
220
|
-
const effective =
|
|
221
|
-
const secretsMap = effective ? mergeSecretsWithPrefixedCopies(secrets, scopedKv.envKey) : secrets;
|
|
218
|
+
const { secretsMap, envKey, effective } = prepareScopedKv(secrets, scopedKv);
|
|
222
219
|
const lines = content.split('\n');
|
|
223
220
|
const result = lines.map(line => {
|
|
224
221
|
if (isCommentOrEmptyLine(line)) return line;
|
|
@@ -226,7 +223,7 @@ function replaceKvInContent(content, secrets, envVars, scopedKv = null) {
|
|
|
226
223
|
if (!scanLine.trim()) return line;
|
|
227
224
|
const suffix = line.slice(scanLine.length);
|
|
228
225
|
const replaced = scanLine.replace(KV_REF_PATTERN, (match, pathStr) => {
|
|
229
|
-
let value = resolveKvRefValue(secretsMap, pathStr,
|
|
226
|
+
let value = resolveKvRefValue(secretsMap, pathStr, envKey, effective);
|
|
230
227
|
if ((value === undefined || value === null) && isKvKeyAllowedEmptyWhenAbsent(pathStr)) {
|
|
231
228
|
value = '';
|
|
232
229
|
}
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Env-scoped kv:// resolution helpers (prefixed keys + in-memory aliases).
|
|
3
3
|
*
|
|
4
|
+
* Two regimes share this module:
|
|
5
|
+
*
|
|
6
|
+
* - `dev` and `miso` are **lenient** (plan 117). A `<env>-<key>` secret wins when present,
|
|
7
|
+
* and an absent one falls back to the unprefixed key. Existing installations are written
|
|
8
|
+
* that way, so removing the fallback would break every one of them.
|
|
9
|
+
* - `tst` and `pro` are **strict** (plan 210.0). Only `<env>-<key>` is read. An absent key
|
|
10
|
+
* is absent — it never falls back to the unprefixed value and it is never generated.
|
|
11
|
+
*
|
|
12
|
+
* The strict rule is the whole point: these environments share one Postgres, one Redis and
|
|
13
|
+
* one secrets file with `dev`. A fallback there would hand production whatever `dev` happens
|
|
14
|
+
* to hold, which is worse than failing, because it looks like it worked. Strictness is
|
|
15
|
+
* therefore deliberately independent of `useEnvironmentScopedResources` and of the
|
|
16
|
+
* per-application `environmentScopedResources` flag: an installation that forgot to set
|
|
17
|
+
* either must not silently resolve `pro` against `dev`'s secrets.
|
|
18
|
+
*
|
|
4
19
|
* @fileoverview Split from secrets-helpers for file size limits
|
|
5
20
|
* @author AI Fabrix Team
|
|
6
21
|
* @version 1.0.0
|
|
@@ -8,6 +23,25 @@
|
|
|
8
23
|
|
|
9
24
|
'use strict';
|
|
10
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Environments where the `<env>-` prefix is mandatory rather than preferred.
|
|
28
|
+
*
|
|
29
|
+
* Not the same list as `physical-database-naming.PHYSICAL_NAME_ENVIRONMENTS`: that one is the
|
|
30
|
+
* Postgres naming vocabulary and includes `miso` and `dev`, which resolve leniently here.
|
|
31
|
+
*/
|
|
32
|
+
const STRICT_SCOPED_ENVIRONMENTS = Object.freeze(['tst', 'pro']);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {string} envKey - Run environment key
|
|
36
|
+
* @returns {boolean} True when the prefix is mandatory for this environment
|
|
37
|
+
*/
|
|
38
|
+
function isStrictScopedEnvironment(envKey) {
|
|
39
|
+
if (!envKey || typeof envKey !== 'string') {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return STRICT_SCOPED_ENVIRONMENTS.includes(envKey.trim().toLowerCase());
|
|
43
|
+
}
|
|
44
|
+
|
|
11
45
|
/**
|
|
12
46
|
* Treat undefined/null and whitespace-only strings as no value so prefixed slots
|
|
13
47
|
* cannot shadow unprefixed secrets with empty placeholders (plan 117 scoped KV).
|
|
@@ -25,9 +59,80 @@ function isKvSlotEmpty(v) {
|
|
|
25
59
|
return false;
|
|
26
60
|
}
|
|
27
61
|
|
|
62
|
+
/**
|
|
63
|
+
* The secret key a strict environment actually requires.
|
|
64
|
+
*
|
|
65
|
+
* **Not simply `<env>-<key>`.** Miso's naming rule has three cases, and only the middle one
|
|
66
|
+
* is a bare environment prefix:
|
|
67
|
+
*
|
|
68
|
+
* ```text
|
|
69
|
+
* system <key> unprefixed, deliberately
|
|
70
|
+
* environment-scoped pro-<key>
|
|
71
|
+
* application-scoped pro-<app>-<key>
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* They are not distinguishable from the key alone, so the name comes from the catalog entry's
|
|
75
|
+
* own vault-name pattern rather than being built here. A database password is therefore
|
|
76
|
+
* `pro-dataplane-databases-0-passwordKeyVault` — the application segment before the key — and
|
|
77
|
+
* `keycloak-*` and `acr-*` keep the names the Marketplace template writes.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} envKey - Run environment key
|
|
80
|
+
* @param {string} pathStr - Unprefixed kv path
|
|
81
|
+
* @returns {string}
|
|
82
|
+
*/
|
|
83
|
+
function strictScopedKeyName(envKey, pathStr) {
|
|
84
|
+
const { resolveStrictSecretName } = require('../parameters/physical-secret-name');
|
|
85
|
+
return resolveStrictSecretName(pathStr, String(envKey).toLowerCase());
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Alias every unprefixed key to its `<env>-` name so a lenient lookup finds it.
|
|
90
|
+
*
|
|
91
|
+
* Never called for a strict environment: aliasing there would reintroduce the fallback
|
|
92
|
+
* this module exists to remove.
|
|
93
|
+
*
|
|
94
|
+
* @param {Object} secrets
|
|
95
|
+
* @param {string} envKey
|
|
96
|
+
* @returns {Object}
|
|
97
|
+
*/
|
|
98
|
+
function mergeSecretsWithPrefixedCopies(secrets, envKey) {
|
|
99
|
+
if (!secrets || typeof secrets !== 'object' || !envKey) {
|
|
100
|
+
return secrets;
|
|
101
|
+
}
|
|
102
|
+
const prefix = `${String(envKey).toLowerCase()}-`;
|
|
103
|
+
const merged = { ...secrets };
|
|
104
|
+
for (const [k, v] of Object.entries(secrets)) {
|
|
105
|
+
if (typeof k !== 'string' || !k || k.startsWith(prefix)) continue;
|
|
106
|
+
const pk = prefix + k;
|
|
107
|
+
if (isKvSlotEmpty(merged[pk]) && !isKvSlotEmpty(v)) {
|
|
108
|
+
merged[pk] = v;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return merged;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Resolve the scoped-kv regime and the secrets map to read through.
|
|
116
|
+
*
|
|
117
|
+
* One helper so every caller applies the same rule; a caller that recomputed `effective`
|
|
118
|
+
* itself could leave a strict environment resolving leniently.
|
|
119
|
+
*
|
|
120
|
+
* @param {Object} secrets - Merged secrets map
|
|
121
|
+
* @param {{ effective?: boolean, envKey?: string }|null} scopedKv
|
|
122
|
+
* @returns {{ secretsMap: Object, envKey: string|undefined, effective: boolean, strict: boolean }}
|
|
123
|
+
*/
|
|
124
|
+
function prepareScopedKv(secrets, scopedKv) {
|
|
125
|
+
const envKey = scopedKv && scopedKv.envKey ? scopedKv.envKey : undefined;
|
|
126
|
+
const strict = isStrictScopedEnvironment(envKey);
|
|
127
|
+
const effective = strict || Boolean(scopedKv && scopedKv.effective && envKey);
|
|
128
|
+
const secretsMap =
|
|
129
|
+
effective && !strict ? mergeSecretsWithPrefixedCopies(secrets, envKey) : secrets;
|
|
130
|
+
return { secretsMap, envKey, effective, strict };
|
|
131
|
+
}
|
|
132
|
+
|
|
28
133
|
/**
|
|
29
134
|
* @param {Function} getValueByPath - From secrets-helpers
|
|
30
|
-
* @returns {
|
|
135
|
+
* @returns {Object} Scoped kv helpers bound to that lookup
|
|
31
136
|
*/
|
|
32
137
|
function createScopedKvHelpers(getValueByPath) {
|
|
33
138
|
/**
|
|
@@ -38,40 +143,34 @@ function createScopedKvHelpers(getValueByPath) {
|
|
|
38
143
|
* @returns {*}
|
|
39
144
|
*/
|
|
40
145
|
function getValueByPathWithEnvScope(secrets, pathStr, envKey, effective) {
|
|
41
|
-
|
|
146
|
+
const strict = isStrictScopedEnvironment(envKey);
|
|
147
|
+
if (!strict && (!effective || !envKey)) {
|
|
42
148
|
return getValueByPath(secrets, pathStr);
|
|
43
149
|
}
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
150
|
+
const fromPrefixed = getValueByPath(secrets, strictScopedKeyName(envKey, pathStr));
|
|
151
|
+
if (strict) {
|
|
152
|
+
// No fallback. Undefined rather than the empty value so the caller's
|
|
153
|
+
// allowed-empty rule still decides whether absence is an error.
|
|
154
|
+
return isKvSlotEmpty(fromPrefixed) ? undefined : fromPrefixed;
|
|
155
|
+
}
|
|
47
156
|
if (!isKvSlotEmpty(fromPrefixed)) {
|
|
48
157
|
return fromPrefixed;
|
|
49
158
|
}
|
|
50
159
|
return getValueByPath(secrets, pathStr);
|
|
51
160
|
}
|
|
52
161
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
function mergeSecretsWithPrefixedCopies(secrets, envKey) {
|
|
59
|
-
if (!secrets || typeof secrets !== 'object' || !envKey) {
|
|
60
|
-
return secrets;
|
|
61
|
-
}
|
|
62
|
-
const prefix = `${String(envKey).toLowerCase()}-`;
|
|
63
|
-
const merged = { ...secrets };
|
|
64
|
-
for (const [k, v] of Object.entries(secrets)) {
|
|
65
|
-
if (typeof k !== 'string' || !k || k.startsWith(prefix)) continue;
|
|
66
|
-
const pk = prefix + k;
|
|
67
|
-
if (isKvSlotEmpty(merged[pk]) && !isKvSlotEmpty(v)) {
|
|
68
|
-
merged[pk] = v;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return merged;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return { getValueByPathWithEnvScope, mergeSecretsWithPrefixedCopies };
|
|
162
|
+
return {
|
|
163
|
+
getValueByPathWithEnvScope,
|
|
164
|
+
mergeSecretsWithPrefixedCopies,
|
|
165
|
+
prepareScopedKv
|
|
166
|
+
};
|
|
75
167
|
}
|
|
76
168
|
|
|
77
|
-
module.exports = {
|
|
169
|
+
module.exports = {
|
|
170
|
+
STRICT_SCOPED_ENVIRONMENTS,
|
|
171
|
+
isStrictScopedEnvironment,
|
|
172
|
+
strictScopedKeyName,
|
|
173
|
+
mergeSecretsWithPrefixedCopies,
|
|
174
|
+
prepareScopedKv,
|
|
175
|
+
createScopedKvHelpers
|
|
176
|
+
};
|
|
@@ -17,6 +17,7 @@ const secretsUtils = require('./secrets-utils');
|
|
|
17
17
|
const { loadConfiguredSharedSecretsStore } = require('./remote-secrets-loader');
|
|
18
18
|
const { decryptSecretsObject } = require('../core/secrets-load');
|
|
19
19
|
const { collectUniqueKvPathStrings } = require('./secrets-kv-refs');
|
|
20
|
+
const { isStrictScopedEnvironment } = require('./secrets-kv-scope');
|
|
20
21
|
const {
|
|
21
22
|
resolveKvRefValue,
|
|
22
23
|
mergeSecretsWithPrefixedCopies,
|
|
@@ -98,6 +99,14 @@ async function materializeResolvedKvSecretsToUserLocal(templateContent, mergedSe
|
|
|
98
99
|
return [];
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
// This writes back under the UNPREFIXED key. In a strict environment that would mint an
|
|
103
|
+
// unprefixed copy of a tst or pro secret which dev would then resolve, quietly undoing the
|
|
104
|
+
// isolation. Nothing is lost by skipping: a strict environment resolves only `<env>-<key>`,
|
|
105
|
+
// so every value it resolved was already stored under the name it will be read by.
|
|
106
|
+
if (isStrictScopedEnvironment(scopedKv && scopedKv.envKey)) {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
|
|
101
110
|
try {
|
|
102
111
|
const { effective, secretsMap, localMap } = buildScopedMaps(mergedSecrets, scopedKv);
|
|
103
112
|
const sharedScoped = await resolveSharedScopedMapForMaterialize(scopedKv, effective);
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
const { formatMissingSecretsFileInfo } = require('./secrets-helpers');
|
|
12
12
|
const { isCommentOrEmptyLine } = require('./secrets-kv-refs');
|
|
13
|
+
const { isStrictScopedEnvironment, strictScopedKeyName } = require('./secrets-kv-scope');
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* @param {string} kvRef - e.g. kv://hubspot-demo/clientSecret
|
|
@@ -117,6 +118,35 @@ function buildKvNamespaceAlternateHints(missing, secrets, appName) {
|
|
|
117
118
|
return lines;
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Name the key a strict environment actually needs.
|
|
123
|
+
*
|
|
124
|
+
* The template references `kv://<key>`, but `tst` and `pro` only read `<env>-<key>`, so an
|
|
125
|
+
* error quoting the template reference alone would send someone to create the one name that
|
|
126
|
+
* will not be used. Both are printed: the reference so the template line is findable, and
|
|
127
|
+
* the required key so `aifabrix secret set` gets the right argument.
|
|
128
|
+
*
|
|
129
|
+
* @param {string[]} missing - kv:// refs
|
|
130
|
+
* @param {{ envKey?: string }|null} [scopedKv]
|
|
131
|
+
* @returns {string[]}
|
|
132
|
+
*/
|
|
133
|
+
function buildStrictScopedKeyHints(missing, scopedKv) {
|
|
134
|
+
const envKey = scopedKv && scopedKv.envKey ? scopedKv.envKey : null;
|
|
135
|
+
if (!envKey || !isStrictScopedEnvironment(envKey)) {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
const env = String(envKey).toLowerCase();
|
|
139
|
+
const lines = missing.map(
|
|
140
|
+
(ref) => `Required-key: ${strictScopedKeyName(env, kvRefToSecretSetKey(ref))}`
|
|
141
|
+
);
|
|
142
|
+
lines.push(
|
|
143
|
+
`Scoped-env-hint: ${env} reads only ${env}-prefixed secrets. The unprefixed key is ` +
|
|
144
|
+
`not a fallback and is never generated for ${env}, so another environment's value ` +
|
|
145
|
+
'cannot be used by accident.'
|
|
146
|
+
);
|
|
147
|
+
return lines;
|
|
148
|
+
}
|
|
149
|
+
|
|
120
150
|
/**
|
|
121
151
|
* @param {Object} opts
|
|
122
152
|
* @param {string[]} opts.missing
|
|
@@ -125,10 +155,11 @@ function buildKvNamespaceAlternateHints(missing, secrets, appName) {
|
|
|
125
155
|
* @param {string} [opts.envTemplatePath]
|
|
126
156
|
* @param {string} [opts.envTemplate]
|
|
127
157
|
* @param {Object} [opts.secrets]
|
|
158
|
+
* @param {{ envKey?: string }|null} [opts.scopedKv]
|
|
128
159
|
* @returns {string}
|
|
129
160
|
*/
|
|
130
161
|
function buildMissingSecretsErrorMessage(opts) {
|
|
131
|
-
const { missing, secretsFilePaths, appName, envTemplatePath, envTemplate, secrets } = opts;
|
|
162
|
+
const { missing, secretsFilePaths, appName, envTemplatePath, envTemplate, secrets, scopedKv } = opts;
|
|
132
163
|
const fileInfo = formatMissingSecretsFileInfo(secretsFilePaths);
|
|
133
164
|
const lines = [`Missing secrets: ${missing.join(', ')}`];
|
|
134
165
|
if (fileInfo) lines.push(fileInfo.trim());
|
|
@@ -138,6 +169,9 @@ function buildMissingSecretsErrorMessage(opts) {
|
|
|
138
169
|
if (templateLine) lines.push(`Env-template-line: ${ref}|${templateLine}`);
|
|
139
170
|
}
|
|
140
171
|
if (appName) lines.push(`App-name: ${appName}`);
|
|
172
|
+
for (const hint of buildStrictScopedKeyHints(missing, scopedKv)) {
|
|
173
|
+
lines.push(hint);
|
|
174
|
+
}
|
|
141
175
|
if (hasStaleOAuthTemplatePaths(missing) && appName) {
|
|
142
176
|
lines.push(`Stale-template-hint: oauth-leftover | repair ${appName}`);
|
|
143
177
|
}
|