@aifabrix/builder 2.61.2 → 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.
Files changed (81) hide show
  1. package/lib/api/applications.api.js +22 -2
  2. package/lib/api/bootstrap-snapshot.api.js +115 -0
  3. package/lib/app/deploy.js +14 -5
  4. package/lib/app/run-env-compose.js +5 -6
  5. package/lib/app/run.js +6 -4
  6. package/lib/build/standard-docker-build.js +4 -0
  7. package/lib/channels/channel-artifact-generator.js +13 -3
  8. package/lib/cli/infra-guided-footers.js +10 -8
  9. package/lib/cli/infra-guided.js +30 -10
  10. package/lib/cli/setup-infra-up-platform-action.js +14 -1
  11. package/lib/cli/setup-platform.js +2 -0
  12. package/lib/commands/auth-config.js +2 -0
  13. package/lib/commands/login.js +4 -0
  14. package/lib/commands/repair-datasource-apply.js +3 -1
  15. package/lib/commands/repair-datasource-legacy.js +94 -0
  16. package/lib/commands/setup-image-refresh.js +26 -0
  17. package/lib/commands/setup-modes.js +7 -1
  18. package/lib/commands/setup-prompts-platform-mode.js +34 -0
  19. package/lib/commands/setup.js +3 -0
  20. package/lib/commands/up-builder-api.js +24 -8
  21. package/lib/commands/up-common.js +16 -1
  22. package/lib/commands/up-dataplane-bootstrap.js +70 -14
  23. package/lib/commands/up-dataplane-credentials.js +21 -0
  24. package/lib/commands/up-dataplane.js +37 -18
  25. package/lib/commands/up-integration-server.js +112 -0
  26. package/lib/commands/wizard-config-normalizer.js +5 -1
  27. package/lib/core/admin-secrets-env-overlay.js +8 -3
  28. package/lib/core/environment-access-policy.js +121 -0
  29. package/lib/core/environment-mode-policy.js +177 -0
  30. package/lib/core/local-env-overrides.js +149 -0
  31. package/lib/core/secrets-ensure.js +4 -2
  32. package/lib/core/secrets-env-content.js +12 -9
  33. package/lib/core/secrets-env-write.js +1 -1
  34. package/lib/datasource/capability/capability-manifest.js +1 -0
  35. package/lib/deployment/installation/index.js +14 -1
  36. package/lib/deployment/installation/infra-catalog.js +3 -0
  37. package/lib/deployment/installation/local-environment-stage.js +285 -0
  38. package/lib/deployment/installation/local-installation.js +76 -0
  39. package/lib/deployment/installation/stage-input.js +5 -1
  40. package/lib/generator/builders.js +3 -1
  41. package/lib/generator/split-variables.js +2 -2
  42. package/lib/parameters/database-adoption.js +125 -0
  43. package/lib/parameters/physical-database-naming.js +136 -0
  44. package/lib/parameters/physical-secret-name.js +194 -0
  45. package/lib/parameters/rsa-secret-values.js +165 -0
  46. package/lib/programmatic/run-channel-install-artifacts.js +28 -2
  47. package/lib/programmatic/run-channel-package.js +3 -1
  48. package/lib/programmatic/workspace-context.js +37 -2
  49. package/lib/schema/infra.parameter.yaml +61 -27
  50. package/lib/schema/infrastructure-schema.json +213 -120
  51. package/lib/utils/compose-generate-docker-compose.js +2 -2
  52. package/lib/utils/compose-generator.js +4 -7
  53. package/lib/utils/compose-miso-env.js +2 -2
  54. package/lib/utils/compose-traefik-ingress-base.js +3 -4
  55. package/lib/utils/declarative-url-ports.js +3 -0
  56. package/lib/utils/env-environment-file-paths.js +105 -0
  57. package/lib/utils/environment-scoped-resources.js +48 -14
  58. package/lib/utils/paths-system-builder-keys.js +7 -1
  59. package/lib/utils/paths.js +7 -2
  60. package/lib/utils/postgres-platform-bootstrap.js +35 -1
  61. package/lib/utils/redis-env-scope.js +21 -1
  62. package/lib/utils/registry-auth-sources.js +154 -0
  63. package/lib/utils/registry-credentials.js +131 -14
  64. package/lib/utils/secrets-generator.js +30 -21
  65. package/lib/utils/secrets-helpers.js +5 -7
  66. package/lib/utils/secrets-kv-scope.js +127 -28
  67. package/lib/utils/secrets-materialize-local.js +9 -0
  68. package/lib/utils/secrets-missing-error.js +35 -1
  69. package/lib/utils/token-manager.js +14 -8
  70. package/lib/utils/url-public-path-prefix.js +6 -10
  71. package/package.json +3 -2
  72. package/templates/applications/builder-api/application.yaml +1 -1
  73. package/templates/applications/dataplane/application.yaml +2 -2
  74. package/templates/applications/dataplane/env.template +1 -1
  75. package/templates/applications/integration-server/application.yaml +91 -0
  76. package/templates/applications/integration-server/env.template +215 -0
  77. package/templates/applications/mirrored-templates.json +52 -0
  78. package/templates/applications/miso-controller/application.yaml +43 -1
  79. package/templates/applications/miso-controller/env.template +45 -39
  80. package/templates/marketplace/createUiDefinition.json +52 -4
  81. package/templates/marketplace/main.json +85 -7
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Who may act on which environment (plan 210.0).
3
+ *
4
+ * Cross-environment management is gated by the **signed-in user's** access, never by a
5
+ * standing service identity. The distinction matters because the bootstrap credential that
6
+ * makes a silent `dev` install possible would otherwise also reach `pro`: one shared secret
7
+ * on a developer host would become a route into production, and nothing in the audit trail
8
+ * would name a person.
9
+ *
10
+ * The entitled set therefore has to come from the user's own token. That is a property of
11
+ * the caller rather than of this module, so the rule is made mechanical: a caller states
12
+ * where the list came from, and anything other than a signed-in user is refused here rather
13
+ * than trusted to have checked.
14
+ *
15
+ * **A request touching several environments is all-or-nothing, decided before anything
16
+ * runs.** A partly applied cross-environment operation leaves the installation in a state
17
+ * nobody asked for, and the exit code does not say which half happened; refusing the whole
18
+ * request is recoverable, half-doing it is not.
19
+ *
20
+ * Pure: no I/O, no token parsing. The caller supplies what it learned.
21
+ *
22
+ * @fileoverview Environment entitlement checks for cross-environment commands
23
+ */
24
+
25
+ 'use strict';
26
+
27
+ /** The only acceptable provenance for an entitled-environment list. */
28
+ const USER_ACCESS_SOURCE = 'user';
29
+
30
+ /**
31
+ * @param {unknown} value - Environment key
32
+ * @returns {string}
33
+ */
34
+ function normalizeEnvKey(value) {
35
+ return String(value ?? '').trim().toLowerCase();
36
+ }
37
+
38
+ /**
39
+ * Normalise an entitled-environment list to comparable keys.
40
+ *
41
+ * @param {Array<unknown>|null|undefined} entitled - Environments the user may act on
42
+ * @returns {string[]} Unique, normalised, in the order given
43
+ */
44
+ function normalizeEntitledEnvironments(entitled) {
45
+ if (!Array.isArray(entitled)) {
46
+ return [];
47
+ }
48
+ const seen = new Set();
49
+ const out = [];
50
+ for (const item of entitled) {
51
+ const key = normalizeEnvKey(typeof item === 'object' && item ? item.key ?? item.name : item);
52
+ if (!key || seen.has(key)) continue;
53
+ seen.add(key);
54
+ out.push(key);
55
+ }
56
+ return out;
57
+ }
58
+
59
+ /**
60
+ * Split a request into what this user may and may not act on.
61
+ *
62
+ * @param {Array<unknown>} requested - Environments the command would touch
63
+ * @param {Array<unknown>} entitled - Environments the user may act on
64
+ * @returns {{ allowed: string[], refused: string[] }}
65
+ */
66
+ function partitionByEntitlement(requested, entitled) {
67
+ const permitted = new Set(normalizeEntitledEnvironments(entitled));
68
+ const allowed = [];
69
+ const refused = [];
70
+ for (const item of Array.isArray(requested) ? requested : [requested]) {
71
+ const key = normalizeEnvKey(item);
72
+ if (!key) continue;
73
+ (permitted.has(key) ? allowed : refused).push(key);
74
+ }
75
+ return { allowed, refused };
76
+ }
77
+
78
+ /**
79
+ * Refuse a request that reaches an environment this user is not entitled to.
80
+ *
81
+ * @param {Array<unknown>|string} requested - Environments the command would touch
82
+ * @param {Object} access - What the caller learned about the signed-in user
83
+ * @param {Array<unknown>} access.entitled - Environments the user may act on
84
+ * @param {string} access.source - Provenance; must be `user`
85
+ * @returns {string[]} The requested environments, normalised, when every one is permitted
86
+ * @throws {Error} When the list is not user-scoped, or any environment is not permitted
87
+ */
88
+ function assertEnvironmentsEntitled(requested, access = {}) {
89
+ if (normalizeEnvKey(access.source) !== USER_ACCESS_SOURCE) {
90
+ throw new Error(
91
+ 'Environment access must come from the signed-in user, not a service identity. ' +
92
+ 'Run "aifabrix login" and retry.'
93
+ );
94
+ }
95
+ const entitled = normalizeEntitledEnvironments(access.entitled);
96
+ const { allowed, refused } = partitionByEntitlement(requested, access.entitled);
97
+
98
+ if (entitled.length === 0) {
99
+ throw new Error(
100
+ 'You are signed in but entitled to no environment. Ask an administrator for access, ' +
101
+ 'or check the controller with: aifabrix env secret list'
102
+ );
103
+ }
104
+ if (refused.length > 0) {
105
+ // Named in full, and nothing is run. Reporting only the first would hide the rest
106
+ // behind a retry loop, and running the entitled ones first would half-apply the request.
107
+ throw new Error(
108
+ `Not entitled to ${refused.join(', ')}. You may act on ${entitled.join(', ')}.\n` +
109
+ 'Nothing was changed: a request spanning environments is refused as a whole so it ' +
110
+ 'cannot be left half applied.'
111
+ );
112
+ }
113
+ return allowed;
114
+ }
115
+
116
+ module.exports = {
117
+ USER_ACCESS_SOURCE,
118
+ normalizeEntitledEnvironments,
119
+ partitionByEntitlement,
120
+ assertEnvironmentsEntitled
121
+ };
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Which environments a local installation may build in and select (plan 210.0).
3
+ *
4
+ * Two rules, kept together because they describe one position: `tst` and `pro` are
5
+ * **promotion targets**, not working environments.
6
+ *
7
+ * **Build is a dev activity.** `tst` and `pro` receive a promoted image — the same image run
8
+ * as another container under an environment-suffixed name. Building into them would mean the
9
+ * artifact in production was never the artifact that was tested, which is the property
10
+ * promotion exists to guarantee. Miso does not police this; a build leaves no trace of where
11
+ * it happened, so the refusal has to be here.
12
+ *
13
+ * **`tst` and `pro` need the front door.** They exist only in `full` platform mode, where
14
+ * Traefik routes each environment under its own path. `single-traefik` is a front door for
15
+ * **one** environment, and `single` is direct ports: several environments on one host collide
16
+ * on ports there. Allowing the selection in either would produce an environment that is
17
+ * unreachable rather than one that failed to start, so it is refused up front.
18
+ *
19
+ * Pure except for `resolveEffectiveBuildEnvironment`, which reads the configured environment.
20
+ *
21
+ * @fileoverview Build and platform-mode environment policy for local installations
22
+ */
23
+
24
+ 'use strict';
25
+
26
+ const {
27
+ SETUP_PLATFORM_MODE,
28
+ normalizeSetupPlatformMode
29
+ } = require('./setup-platform-mode');
30
+
31
+ /** Environments that receive a promoted artifact and are never built into. */
32
+ const PROMOTION_ONLY_ENVIRONMENTS = Object.freeze(['tst', 'pro']);
33
+
34
+ /**
35
+ * @param {unknown} envKey - Environment key
36
+ * @returns {string} Lowercase trimmed key
37
+ */
38
+ function normalizeEnvKey(envKey) {
39
+ return String(envKey ?? '').trim().toLowerCase();
40
+ }
41
+
42
+ /**
43
+ * @param {unknown} envKey - Environment key
44
+ * @returns {boolean} True when the environment only ever receives a promoted artifact
45
+ */
46
+ function isPromotionOnlyEnvironment(envKey) {
47
+ return PROMOTION_ONLY_ENVIRONMENTS.includes(normalizeEnvKey(envKey));
48
+ }
49
+
50
+ /**
51
+ * Refuse a build that would target a promotion-only environment.
52
+ *
53
+ * `--env` has no default, so an omitted flag silently takes the configured environment. The
54
+ * message distinguishes the two cases: being told "build cannot target pro" after typing no
55
+ * environment at all is not actionable on its own.
56
+ *
57
+ * @param {unknown} envKey - Resolved environment key
58
+ * @param {Object} [opts] - Context
59
+ * @param {boolean} [opts.explicit] - Whether the caller passed --env
60
+ * @throws {Error} When the environment is tst or pro
61
+ */
62
+ function assertBuildEnvironmentAllowed(envKey, opts = {}) {
63
+ const key = normalizeEnvKey(envKey);
64
+ if (!isPromotionOnlyEnvironment(key)) {
65
+ return;
66
+ }
67
+ const cause = opts.explicit
68
+ ? `--env ${key}`
69
+ : `the current environment is ${key}`;
70
+ throw new Error(
71
+ `Cannot build into ${key} (${cause}). ${key} receives a promoted image, never a locally ` +
72
+ 'built one, so that what runs there is what was tested in dev.\n' +
73
+ 'Build in dev with: aifabrix build <appKey> --env dev\n' +
74
+ `Then promote into ${key} with: aifabrix env`
75
+ );
76
+ }
77
+
78
+ /**
79
+ * Refuse selecting a promotion-only environment outside the front-door platform mode.
80
+ *
81
+ * @param {unknown} envKey - Environment key being selected
82
+ * @param {unknown} mode - setupPlatformMode value
83
+ * @throws {Error} When the environment needs full mode and the install is not in it
84
+ */
85
+ function assertEnvironmentAllowedForPlatformMode(envKey, mode) {
86
+ const key = normalizeEnvKey(envKey);
87
+ if (!isPromotionOnlyEnvironment(key)) {
88
+ return;
89
+ }
90
+ const normalized = normalizeSetupPlatformMode(mode);
91
+ if (normalized === SETUP_PLATFORM_MODE.FULL) {
92
+ return;
93
+ }
94
+ const reason =
95
+ normalized === SETUP_PLATFORM_MODE.SINGLE_TRAEFIK
96
+ ? 'single-traefik is a front door for one environment only'
97
+ : 'single mode publishes direct ports, and several environments on one host collide on ports';
98
+ throw new Error(
99
+ `Environment ${key} requires platform mode "full" (HTTPS front door with per-environment ` +
100
+ `paths). This installation is "${normalized}": ${reason}.\n` +
101
+ 'Switch with: aifabrix setup --platform full'
102
+ );
103
+ }
104
+
105
+ /**
106
+ * Refuse a promotion-only environment against the installation's configured mode.
107
+ *
108
+ * @param {unknown} envKey - Environment key being selected
109
+ * @returns {Promise<void>}
110
+ * @throws {Error} When the environment needs full mode and the install is not in it
111
+ */
112
+ async function assertEnvironmentAllowedForCurrentMode(envKey) {
113
+ if (!isPromotionOnlyEnvironment(envKey)) {
114
+ return;
115
+ }
116
+ const config = require('./config');
117
+ const mode =
118
+ typeof config.getSetupPlatformMode === 'function'
119
+ ? await config.getSetupPlatformMode()
120
+ : SETUP_PLATFORM_MODE.SINGLE;
121
+ assertEnvironmentAllowedForPlatformMode(envKey, mode);
122
+ }
123
+
124
+ /**
125
+ * The environment a build will actually target.
126
+ *
127
+ * Mirrors how the secret path resolves it (`buildScopedKvContext`): an omitted `--env` falls
128
+ * through to the configured environment, so a guard that only inspected the flag would let
129
+ * `aifabrix build` run against `pro` on a machine configured for it.
130
+ *
131
+ * @param {Object} [options] - Build options carrying `env`
132
+ * @returns {Promise<{ envKey: string, explicit: boolean }>}
133
+ */
134
+ async function resolveEffectiveBuildEnvironment(options = {}) {
135
+ if (options && options.env) {
136
+ return { envKey: normalizeEnvKey(options.env), explicit: true };
137
+ }
138
+ const config = require('./config');
139
+ if (typeof config.getCurrentEnvironment !== 'function') {
140
+ return { envKey: 'dev', explicit: false };
141
+ }
142
+ let current;
143
+ try {
144
+ current = await config.getCurrentEnvironment();
145
+ } catch {
146
+ // An unreadable config is a different failure, reported properly by whatever next needs
147
+ // it — secrets resolution cannot proceed without it either. Turning it into "cannot
148
+ // build" would stop every build on a machine whose config has an unrelated problem, and
149
+ // it protects nothing: reaching tst or pro requires either an explicit --env, which
150
+ // never reads config, or a readable config naming one.
151
+ return { envKey: 'dev', explicit: false };
152
+ }
153
+ return { envKey: normalizeEnvKey(current) || 'dev', explicit: false };
154
+ }
155
+
156
+ /**
157
+ * Resolve the build environment and refuse it when it is promotion-only.
158
+ *
159
+ * @param {Object} [options] - Build options carrying `env`
160
+ * @returns {Promise<string>} The allowed environment key
161
+ * @throws {Error} When the environment is tst or pro
162
+ */
163
+ async function assertBuildEnvironmentForOptions(options = {}) {
164
+ const { envKey, explicit } = await resolveEffectiveBuildEnvironment(options);
165
+ assertBuildEnvironmentAllowed(envKey, { explicit });
166
+ return envKey;
167
+ }
168
+
169
+ module.exports = {
170
+ PROMOTION_ONLY_ENVIRONMENTS,
171
+ isPromotionOnlyEnvironment,
172
+ assertBuildEnvironmentAllowed,
173
+ assertEnvironmentAllowedForPlatformMode,
174
+ assertEnvironmentAllowedForCurrentMode,
175
+ resolveEffectiveBuildEnvironment,
176
+ assertBuildEnvironmentForOptions
177
+ };
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Builder-owned env values for locally installed platform applications.
3
+ *
4
+ * `templates/applications/{miso-controller,builder-api,keycloak}/env.template` are
5
+ * byte-identical mirrors of the Miso master, and `dataplane` mirrors the dataplane
6
+ * repository. An edit to a mirror survives until the next `chore(templates): sync`
7
+ * commit and then disappears without a diff anyone reads, so a value Builder decides
8
+ * cannot live there. It is applied here instead, after the template is resolved.
9
+ *
10
+ * @fileoverview Local-install env overrides owned by Builder, not by a mirrored template
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ /** Applications the CLI registers itself through `.../applications/bootstrap`. */
16
+ const CLI_BOOTSTRAPPED_APP_KEYS = Object.freeze(['dataplane']);
17
+
18
+ /**
19
+ * Controller onboarding owns the miso-only platform applications. Dataplane remains a
20
+ * CLI-owned workload and must never be inserted into this list.
21
+ */
22
+ const DEFAULT_LOCAL_ONBOARDING_APPS = Object.freeze(['builder-api']);
23
+
24
+ /** Config key an installation may set to choose its own onboarding list. */
25
+ const ONBOARDING_APPS_CONFIG_KEY = 'onboardingApps';
26
+
27
+ /**
28
+ * Keys Builder's mirror carried that the Miso master does not.
29
+ *
30
+ * They were edits to a mirrored file, which is the thing this module exists to prevent: the
31
+ * mirror has since fallen 57 lines behind its master, and re-syncing it would have dropped
32
+ * them silently. They are declared here instead, so the next sync cannot lose them.
33
+ *
34
+ * - `INFRASTRUCTURE_NAME` is the only one carrying a value, used by controller and onboarding
35
+ * flows. Losing it would change behaviour, not merely remove a blank line.
36
+ * - The other three shipped empty. They are kept so a local install's env has exactly the keys
37
+ * it had before this sync: an absent key and an empty one are usually the same thing, and
38
+ * "usually" is not a basis for changing every installation at once.
39
+ *
40
+ * `ACR_LOGIN_SERVER` is deliberately empty. Miso removed it with the note that its proper home
41
+ * is Builder's run tooling, which knows the per-developer answer — so an installation that
42
+ * needs it should have it injected there rather than shipped one test installation's registry
43
+ * to everybody, which is what happened the last time it carried a value.
44
+ */
45
+ const MISO_CONTROLLER_LOCAL_ONLY_ENV = Object.freeze({
46
+ INFRASTRUCTURE_NAME: '${DEV_USERNAME}.${DEV_ROOT_DOMAIN}',
47
+ KEYCLOAK_PUBLIC_KEY: '',
48
+ OTEL_SERVICE_VERSION: '',
49
+ ACR_LOGIN_SERVER: ''
50
+ });
51
+
52
+ function normalizeAppKeyList(value) {
53
+ const raw = Array.isArray(value) ? value : String(value ?? '').split(',');
54
+ const seen = new Set();
55
+ for (const entry of raw) {
56
+ const key = String(entry ?? '').trim().toLowerCase();
57
+ if (key) {
58
+ seen.add(key);
59
+ }
60
+ }
61
+ return [...seen];
62
+ }
63
+
64
+ /**
65
+ * Applications controller onboarding may register on this installation.
66
+ *
67
+ * Builder API is always retained because Controller onboarding owns that miso-only
68
+ * application. This also migrates older explicit-empty setup values safely.
69
+ *
70
+ * @param {Object} [cfg] - Parsed `~/.aifabrix/config.yaml`
71
+ * @returns {string[]} Application keys, never including one the CLI bootstraps
72
+ */
73
+ function resolveLocalOnboardingApps(cfg) {
74
+ const configured = cfg ? cfg[ONBOARDING_APPS_CONFIG_KEY] : undefined;
75
+ const list = configured === undefined || configured === null
76
+ ? [...DEFAULT_LOCAL_ONBOARDING_APPS]
77
+ : normalizeAppKeyList(configured);
78
+ const allowed = list.filter((key) => !CLI_BOOTSTRAPPED_APP_KEYS.includes(key));
79
+ return ['builder-api', ...allowed.filter((key) => key !== 'builder-api')];
80
+ }
81
+
82
+ /**
83
+ * Env values Builder owns for a locally installed application.
84
+ *
85
+ * @param {string} appName - Application key
86
+ * @param {Object} [cfg] - Parsed `~/.aifabrix/config.yaml`
87
+ * @returns {Object.<string, string>} Map to merge into resolved env content; empty when none apply
88
+ */
89
+ function localEnvOverridesForApp(appName, cfg) {
90
+ const key = String(appName ?? '').trim().toLowerCase();
91
+ if (key === 'miso-controller') {
92
+ return {
93
+ ONBOARDING_APPS: resolveLocalOnboardingApps(cfg).join(','),
94
+ ...MISO_CONTROLLER_LOCAL_ONLY_ENV
95
+ };
96
+ }
97
+ if (key === 'builder-api' || key === 'integration-server') {
98
+ return { MISO_ENVIRONMENT: 'miso' };
99
+ }
100
+ return {};
101
+ }
102
+
103
+ /**
104
+ * Merge Builder-owned values into resolved env content for a locally installed application.
105
+ *
106
+ * Applied after the template is resolved, so the installation's choice is the authority for
107
+ * these keys. The merge function is passed in rather than imported, because the only caller
108
+ * owns it and importing it here would close a require cycle.
109
+ *
110
+ * @param {string} content - Resolved env content
111
+ * @param {string} appName - Application key
112
+ * @param {(content: string, map: Object) => string} mergeEnvMapIntoContent - Merge helper
113
+ * @returns {Promise<string>} Content with overrides merged, unchanged when none apply
114
+ */
115
+ async function applyLocalEnvOverrides(content, appName, mergeEnvMapIntoContent) {
116
+ let cfg = null;
117
+ try {
118
+ cfg = await require('./config').getConfig();
119
+ } catch {
120
+ // No readable config (fresh machine, or resolve outside an installation): use defaults.
121
+ }
122
+ const overrides = localEnvOverridesForApp(appName, cfg);
123
+ if (!overrides || Object.keys(overrides).length === 0) {
124
+ return content;
125
+ }
126
+ return mergeEnvMapIntoContent(content, overrides);
127
+ }
128
+
129
+ /**
130
+ * The onboarding list for an installation that does or does not carry the integration
131
+ * server. Written to config so the choice survives, rather than being asked on every run.
132
+ *
133
+ * @param {boolean} integrationServer - Whether the installation carries it
134
+ * @returns {string[]} Value for the `onboardingApps` config key
135
+ */
136
+ function onboardingAppsForIntegrationServer(integrationServer) {
137
+ return integrationServer ? ['builder-api', 'integration-server'] : ['builder-api'];
138
+ }
139
+
140
+ module.exports = {
141
+ CLI_BOOTSTRAPPED_APP_KEYS,
142
+ DEFAULT_LOCAL_ONBOARDING_APPS,
143
+ ONBOARDING_APPS_CONFIG_KEY,
144
+ MISO_CONTROLLER_LOCAL_ONLY_ENV,
145
+ resolveLocalOnboardingApps,
146
+ localEnvOverridesForApp,
147
+ applyLocalEnvOverrides,
148
+ onboardingAppsForIntegrationServer
149
+ };
@@ -29,6 +29,7 @@ const {
29
29
  saveSecretsFile
30
30
  } = require('../utils/secrets-generator');
31
31
  const { encryptSecret } = require('../utils/secrets-encryption');
32
+ const { generateRsaSecretValues } = require('../parameters/rsa-secret-values');
32
33
  const { loadEnvTemplate } = require('../utils/secrets-helpers');
33
34
  const {
34
35
  buildInfraPlaceholderContext,
@@ -287,12 +288,13 @@ async function ensureSecretsForKeys(keys, options = {}) {
287
288
  if (toAdd.length === 0) return [];
288
289
 
289
290
  const encryptionKey = await config.getSecretsEncryptionKey();
291
+ const rsaValues = emptyForCredentials ? {} : generateRsaSecretValues(toAdd, { ...existing, ...suggested }, encryptionKey);
290
292
  const added = [];
291
293
 
292
294
  return addSecretsToFile({
293
295
  filePath: target.filePath,
294
- toAdd,
295
- suggested,
296
+ toAdd: [...new Set([...toAdd, ...Object.keys(rsaValues)])],
297
+ suggested: { ...rsaValues, ...suggested },
296
298
  emptyForCredentials,
297
299
  encryptionKey,
298
300
  added,
@@ -12,6 +12,7 @@ const fs = require('fs');
12
12
  const path = require('path');
13
13
  const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
14
14
  const config = require('./config');
15
+ const { applyLocalEnvOverrides } = require('./local-env-overrides');
15
16
  const {
16
17
  interpolateEnvVars,
17
18
  collectMissingSecrets,
@@ -40,8 +41,9 @@ const secretsEnsure = require('./secrets-ensure');
40
41
  const { resolveSecretsPath, getActualSecretsPath } = require('../utils/secrets-path');
41
42
  const pathsUtil = require('../utils/paths');
42
43
  const { readAppEnvironmentScopedFlagForAppPath } = require('../utils/app-scoped-config');
43
- const { computeEffectiveEnvironmentScopedResources, redisDbIndexForScopedRunEnv } = require('../utils/environment-scoped-resources');
44
- const { applyRedisDbIndexToEnvContent } = require('../utils/redis-env-scope');
44
+ const { computeEffectiveEnvironmentScopedResources } = require('../utils/environment-scoped-resources');
45
+ const { isStrictScopedEnvironment } = require('../utils/secrets-kv-scope');
46
+ const { applyScopedRedisDbIndex } = require('../utils/redis-env-scope');
45
47
  const { expandDeclarativeUrlsIfPresent } = require('./secrets-env-declarative-expand');
46
48
  const {
47
49
  mergeAdditionalPublishedPorts,
@@ -106,7 +108,8 @@ async function resolveKvReferences(
106
108
  secretsFilePaths,
107
109
  appName,
108
110
  envTemplatePath,
109
- envTemplate
111
+ envTemplate,
112
+ scopedKv
110
113
  })
111
114
  );
112
115
  }
@@ -237,12 +240,14 @@ async function generateEnvContent(appName, secretsPath, environment = 'local', f
237
240
  const variablesPath = (options && options.envOnly) ? null : resolveApplicationConfigPath(appPath);
238
241
  const template = loadEnvTemplate(templatePath);
239
242
  const secretsPaths = await getActualSecretsPath(secretsPath, appName);
240
- if (force) {
243
+ const { runEnvKey, effective } = await buildScopedKvContext(appPath, options);
244
+ // --force backfills a missing secret by generating one. A strict environment must not be
245
+ // handed a value the Controller has never seen, so it errors on the missing key instead.
246
+ if (force && !isStrictScopedEnvironment(runEnvKey)) {
241
247
  const preferredPath = secretsPath ? resolveSecretsPath(secretsPath) : secretsPaths.userPath;
242
248
  await secretsEnsure.ensureSecretsFromEnvTemplate(templatePath, { preferredFilePath: preferredPath });
243
249
  }
244
250
  const secrets = await loadSecrets(secretsPath, appName);
245
- const { runEnvKey, effective } = await buildScopedKvContext(appPath, options);
246
251
  let resolved = await resolveKvReferences(template, secrets, environment, secretsPaths, appName, {
247
252
  envKey: runEnvKey,
248
253
  effective,
@@ -262,10 +267,8 @@ async function generateEnvContent(appName, secretsPath, environment = 'local', f
262
267
  Boolean(options.envOnly)
263
268
  );
264
269
  resolved = await applyEnvironmentTransformations(resolved, environment, variablesPath);
265
- if (effective) {
266
- const idx = redisDbIndexForScopedRunEnv(runEnvKey);
267
- resolved = applyRedisDbIndexToEnvContent(resolved, idx);
268
- }
270
+ resolved = applyScopedRedisDbIndex(resolved, runEnvKey, effective);
271
+ resolved = await applyLocalEnvOverrides(resolved, appName, mergeEnvMapIntoContent);
269
272
 
270
273
  return resolved;
271
274
  }
@@ -208,7 +208,7 @@ async function resolveAndGetEnvMap(appName, options = {}) {
208
208
  const adminSecrets = require('./admin-secrets');
209
209
  const { applyAdminSecretsPlatformOverlay } = require('./admin-secrets-env-overlay');
210
210
  const adminObj = await adminSecrets.readAndDecryptAdminSecrets();
211
- applyAdminSecretsPlatformOverlay(map, adminObj);
211
+ applyAdminSecretsPlatformOverlay(map, adminObj, appName);
212
212
  } catch {
213
213
  // admin-secrets.env optional until up-infra/setup
214
214
  }
@@ -9,6 +9,7 @@
9
9
  'use strict';
10
10
 
11
11
  const FORBIDDEN_ROOT_KEYS = new Set([
12
+ '_comManifestRoleCatalog',
12
13
  'context',
13
14
  'origin',
14
15
  'generatedBy',
@@ -6,8 +6,14 @@
6
6
  * All input validation happens before any filesystem, network or cloud mutation.
7
7
  */
8
8
 
9
- const { resolveStageInput, validateResourceGroupKey, ENVIRONMENT_STAGES } = require('./stage-input');
9
+ const {
10
+ resolveStageInput,
11
+ validateResourceGroupKey,
12
+ validateStage,
13
+ ENVIRONMENT_STAGES
14
+ } = require('./stage-input');
10
15
  const { openWorkspace } = require('./workspace');
16
+ const { isLocalInstallationKey } = require('./local-installation');
11
17
 
12
18
  /**
13
19
  * Runs one installation workspace stage.
@@ -18,6 +24,13 @@ const { openWorkspace } = require('./workspace');
18
24
  */
19
25
  async function deployInstallationStage(resourceGroupKey, options, deps = {}) {
20
26
  const resourceGroup = validateResourceGroupKey(resourceGroupKey);
27
+ // A local installation owns its environments itself: no workspace, no Azure template and
28
+ // no Controller round trip. `register` still goes the shared route, because a local
29
+ // install registers with Mori exactly as any other does.
30
+ if (isLocalInstallationKey(resourceGroup) && ENVIRONMENT_STAGES.includes(validateStage(options.stage))) {
31
+ const { runLocalEnvironmentStage } = require('./local-environment-stage');
32
+ return runLocalEnvironmentStage(resourceGroup, validateStage(options.stage), deps);
33
+ }
21
34
  const workspace = openWorkspace(resourceGroup, { root: deps.root });
22
35
  const input = resolveStageInput(resourceGroup, options, workspace.infra?.vendor?.value);
23
36
  const base = { stage: input.stage, vendor: input.vendor, workspaceDir: workspace.dir };
@@ -88,6 +88,7 @@ const PARAMETER_CATALOG = Object.freeze({
88
88
  enablePostgresPublicAccess: { kind: 'allowMyIp', source: 'allowMyIp', description: 'Default off; enabled only while allowMyIp grants the validated caller IPv4 /32 for testing.' },
89
89
  allowedIPs: { kind: 'allowMyIp', source: 'allowMyIpOrUser', description: 'Explicit CIDRs for PostgreSQL, Key Vault, and Storage; allowMyIp merges the resolved caller IPv4 /32.' },
90
90
  adminPassword: { kind: 'secretEnv', env: 'ADMIN_PASSWORD', required: true, description: 'Installation administrator password.' },
91
+ adminEmail: { kind: 'file', source: 'user', required: true, description: 'Email of the initial platform administrator, used for the installation welcome. Required: the template sets no default and enforces minLength.' },
91
92
  deploymentEnvironment: { kind: 'file', source: 'templateDefault', description: 'Vendor deployment profile.' },
92
93
  keycloakVerifyAudience: { kind: 'file', source: 'templateDefault', description: 'Enable Keycloak audience verification.' },
93
94
  encryptionKey: { kind: 'secretEnv', env: 'ENCRYPTION_KEY', required: true, description: 'Application/data encryption key.' },
@@ -99,11 +100,13 @@ const PARAMETER_CATALOG = Object.freeze({
99
100
  deployRegistryRoleAssignment: { kind: 'parameterDefault', defaultFrom: 'deployRoleAssignments', source: 'templateDefault', description: 'Deploy the AcrPull grant on the container registry. False while credential image pull is in effect, because that grant lives in the registry resource group, which a customer installation cannot reach; restored for managed identity.' },
100
101
  moriBaseUrl: { kind: 'file', source: 'registration', required: true, description: 'Mori service base URL.' },
101
102
  moriLicensePublicKey: { kind: 'file', source: 'registration', required: false, description: 'Mori licence issuer public key (SPKI PEM); empty leaves the installation unpinned and it then refuses every licence.' },
103
+ moriPolicyPublicKey: { kind: 'file', source: 'registration', required: false, description: 'Mori policy signing anchor (SPKI PEM), a different pair from the licence key; empty leaves the policy bundle checkable only against the key it carries, which proves nothing.' },
102
104
  moriLicenseIssuer: { kind: 'file', source: 'registration', required: false, description: 'Expected iss claim on a Mori licence.' },
103
105
  entraIdClientId: { kind: 'deferred', description: 'Not collected or sent by infra; issued/configured during later Entra provider onboarding.' },
104
106
  entraIdClientSecret: { kind: 'deferred', description: 'Not collected or sent by infra; issued/configured during later Entra provider onboarding.' },
105
107
  entraIdTenantId: { kind: 'deferred', description: 'Customer Entra provider tenant; distinct from the Azure subscription directory tenant and configured later.' },
106
108
  licenseEdition: { kind: 'license', source: 'cliOrFile', description: 'License requested from Mori during tenant activation: COMMUNITY, STANDARD or ENTERPRISE, or an Azure Marketplace subscription ID. Standard/Enterprise by name is a 30-day evaluation.' },
109
+ installationResourceModel: { kind: 'file', source: 'templateDefault', description: 'Initial installation topology: shared for Community and Standard, dedicated for Enterprise DEV. Defaults to shared and is protected by Azure application configuration. It records the topology the installation starts in and grants no later licence entitlement, so it is not a licence input.' },
107
110
  onboardingMode: { kind: 'onboarding', source: 'workspacePolicy', description: 'Miso onboarding mode (ONBOARDING_MODE); Builder installations use saasDirect, Marketplace customer installations use customerOwned.' },
108
111
  capacityProfile: { kind: 'capacity', source: 'workspacePreset', description: 'Infrastructure sizing profile (s, m, l, xl) selecting compute and PostgreSQL SKUs. Derived from the workspace preset so the two cannot drift; set it with --preset.' },
109
112
  misoImageTag: { kind: 'file', source: 'templateDefault', description: 'Miso container image tag.' },