@aifabrix/builder 2.63.0 → 2.63.2

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.
@@ -12,6 +12,8 @@
12
12
 
13
13
  'use strict';
14
14
 
15
+ const { buildDevUsernameForFrontDoorHost } = require('../utils/compose-traefik-ingress-base');
16
+
15
17
  /** Applications the CLI registers itself through `.../applications/bootstrap`. */
16
18
  const CLI_BOOTSTRAPPED_APP_KEYS = Object.freeze(['dataplane']);
17
19
 
@@ -43,12 +45,30 @@ const ONBOARDING_APPS_CONFIG_KEY = 'onboardingApps';
43
45
  * to everybody, which is what happened the last time it carried a value.
44
46
  */
45
47
  const MISO_CONTROLLER_LOCAL_ONLY_ENV = Object.freeze({
46
- INFRASTRUCTURE_NAME: '${DEV_USERNAME}.${DEV_ROOT_DOMAIN}',
47
48
  KEYCLOAK_PUBLIC_KEY: '',
48
49
  OTEL_SERVICE_VERSION: '',
49
50
  ACR_LOGIN_SERVER: ''
50
51
  });
51
52
 
53
+ /**
54
+ * Miso's infrastructure name on a local install: `local-devNN`, the installation name the licence
55
+ * binds to (Miso plan 266.0). Miso names the customer's Entra SSO and SCIM applications from it
56
+ * (`AIF-local-dev03-SSO`) and also looks them up by that name.
57
+ *
58
+ * A concrete value, not a placeholder. These overrides are merged after the template is
59
+ * resolved, so the previous `${DEV_USERNAME}.${DEV_ROOT_DOMAIN}` was never interpolated by
60
+ * Builder; Docker substituted the unset variables with empty strings and Miso received `.`,
61
+ * creating `AIF-.-SSO` / `AIF-.-SCIM` in the customer's directory (2026-10-06). Developer id 0
62
+ * has no `devNN`, so nothing is injected there rather than an invalid `local-`.
63
+ *
64
+ * @param {Object} [cfg] - Parsed `~/.aifabrix/config.yaml`
65
+ * @returns {Object.<string, string>} `{ INFRASTRUCTURE_NAME }` or empty
66
+ */
67
+ function localInfrastructureNameEnv(cfg) {
68
+ const devUsername = buildDevUsernameForFrontDoorHost(cfg ? cfg['developer-id'] : undefined);
69
+ return devUsername ? { INFRASTRUCTURE_NAME: `local-${devUsername}` } : {};
70
+ }
71
+
52
72
  function normalizeAppKeyList(value) {
53
73
  const raw = Array.isArray(value) ? value : String(value ?? '').split(',');
54
74
  const seen = new Set();
@@ -91,7 +111,8 @@ function localEnvOverridesForApp(appName, cfg) {
91
111
  if (key === 'miso-controller') {
92
112
  return {
93
113
  ONBOARDING_APPS: resolveLocalOnboardingApps(cfg).join(','),
94
- ...MISO_CONTROLLER_LOCAL_ONLY_ENV
114
+ ...MISO_CONTROLLER_LOCAL_ONLY_ENV,
115
+ ...localInfrastructureNameEnv(cfg)
95
116
  };
96
117
  }
97
118
  if (key === 'builder-api' || key === 'integration-server') {
@@ -29,7 +29,10 @@ async function deployInstallationStage(resourceGroupKey, options, deps = {}) {
29
29
  // install registers with Mori exactly as any other does.
30
30
  if (isLocalInstallationKey(resourceGroup) && ENVIRONMENT_STAGES.includes(validateStage(options.stage))) {
31
31
  const { runLocalEnvironmentStage } = require('./local-environment-stage');
32
- return runLocalEnvironmentStage(resourceGroup, validateStage(options.stage), deps);
32
+ return runLocalEnvironmentStage(resourceGroup, validateStage(options.stage), {
33
+ ...deps,
34
+ preset: options.preset
35
+ });
33
36
  }
34
37
  const workspace = openWorkspace(resourceGroup, { root: deps.root });
35
38
  const input = resolveStageInput(resourceGroup, options, workspace.infra?.vendor?.value);
@@ -102,8 +102,6 @@ const PARAMETER_CATALOG = Object.freeze({
102
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
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.' },
104
104
  moriLicenseIssuer: { kind: 'file', source: 'registration', required: false, description: 'Expected iss claim on a Mori licence.' },
105
- entraIdClientId: { kind: 'deferred', description: 'Not collected or sent by infra; issued/configured during later Entra provider onboarding.' },
106
- entraIdClientSecret: { kind: 'deferred', description: 'Not collected or sent by infra; issued/configured during later Entra provider onboarding.' },
107
105
  entraIdTenantId: { kind: 'deferred', description: 'Customer Entra provider tenant; distinct from the Azure subscription directory tenant and configured later.' },
108
106
  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
107
  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.' },
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Ask Miso whether a local environment may exist, with honest failure labels.
3
+ *
4
+ * Local create uses the same request shape as Azure: key, environment, and
5
+ * configuration.preset / serviceName / location. Docker values are real
6
+ * placement for that system (location local, serviceName local-devNN), not a
7
+ * stripped body. Schema rejection is not a licence decline.
8
+ *
9
+ * @fileoverview Local env deploy — Miso authorize with contract vs licence split
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const { resolveLocalInstallationName } = require('./local-installation');
15
+ const { normalizePreset } = require('../environment-config');
16
+
17
+ /** Placement location stored for Docker / local / database installs. */
18
+ const LOCAL_LOCATION = 'local';
19
+
20
+ /**
21
+ * @param {Object|Error|null|undefined} errorOrResponse
22
+ * @returns {number}
23
+ */
24
+ function statusOf(errorOrResponse) {
25
+ if (!errorOrResponse || typeof errorOrResponse !== 'object') {
26
+ return 0;
27
+ }
28
+ const raw =
29
+ errorOrResponse.status ??
30
+ errorOrResponse.statusCode ??
31
+ (errorOrResponse.errorData && errorOrResponse.errorData.statusCode);
32
+ const n = Number(raw);
33
+ return Number.isFinite(n) && n > 0 ? n : 0;
34
+ }
35
+
36
+ /**
37
+ * @param {Object|Error|null|undefined} errorOrResponse
38
+ * @returns {string}
39
+ */
40
+ function detailOf(errorOrResponse) {
41
+ if (!errorOrResponse) {
42
+ return 'request failed';
43
+ }
44
+ if (typeof errorOrResponse === 'string') {
45
+ return errorOrResponse;
46
+ }
47
+ return (
48
+ errorOrResponse.formattedError ||
49
+ errorOrResponse.detail ||
50
+ (typeof errorOrResponse.error === 'string' ? errorOrResponse.error : null) ||
51
+ errorOrResponse.message ||
52
+ 'request failed'
53
+ );
54
+ }
55
+
56
+ /**
57
+ * True when the controller refused the body as invalid, before any licence decision.
58
+ *
59
+ * @param {Object|Error|null|undefined} errorOrResponse
60
+ * @returns {boolean}
61
+ */
62
+ function isContractValidationFailure(errorOrResponse) {
63
+ const status = statusOf(errorOrResponse);
64
+ if (status === 400 || status === 422) {
65
+ return true;
66
+ }
67
+ const text = [
68
+ detailOf(errorOrResponse),
69
+ errorOrResponse && errorOrResponse.title,
70
+ errorOrResponse && errorOrResponse.type,
71
+ JSON.stringify((errorOrResponse && errorOrResponse.errorData) || {})
72
+ ]
73
+ .filter(Boolean)
74
+ .join(' ')
75
+ .toLowerCase();
76
+ return /configuration\.|["']?preset["']?|servicename|service.?name|["']?location["']?/.test(
77
+ text
78
+ );
79
+ }
80
+
81
+ /**
82
+ * @param {string} envKey
83
+ * @param {Object|Error} errorOrResponse
84
+ * @returns {Error}
85
+ */
86
+ function formatAuthorizeFailure(envKey, errorOrResponse) {
87
+ const detail = detailOf(errorOrResponse);
88
+ if (isContractValidationFailure(errorOrResponse)) {
89
+ return new Error(
90
+ `Cannot create environment ${envKey}: the controller rejected the request as invalid ` +
91
+ `(not a licence decision).\n${detail}\n` +
92
+ 'Local create uses the same configuration fields as Azure (preset, serviceName, ' +
93
+ 'location). Nothing was created locally.'
94
+ );
95
+ }
96
+ return new Error(
97
+ `Miso declined environment ${envKey}: ${detail}\n` +
98
+ 'Editions are licensed in Miso. Nothing was created locally.'
99
+ );
100
+ }
101
+
102
+ /**
103
+ * Same create body shape as Azure environment create.
104
+ *
105
+ * Docker still sends a licensed size preset (default `s`). `location` is `local`
106
+ * and `serviceName` is the installation name. Optional `--preset` overrides size.
107
+ *
108
+ * @param {string} envKey
109
+ * @param {{ serviceName: string, preset?: string, location?: string }} placement
110
+ * @returns {{ key: string, environment: string, configuration: Object }}
111
+ */
112
+ function buildLocalEnvironmentCreateBody(envKey, placement) {
113
+ const key = String(envKey || '').trim();
114
+ return {
115
+ key,
116
+ environment: key,
117
+ configuration: {
118
+ preset: normalizePreset(placement.preset),
119
+ serviceName: String(placement.serviceName || '').trim(),
120
+ location: String(placement.location || LOCAL_LOCATION).trim() || LOCAL_LOCATION
121
+ }
122
+ };
123
+ }
124
+
125
+ /**
126
+ * @async
127
+ * @param {Object} deps
128
+ * @param {string} installationKey
129
+ * @returns {Promise<string>}
130
+ */
131
+ async function resolveAuthorizeServiceName(deps, installationKey) {
132
+ if (deps.serviceName) {
133
+ return String(deps.serviceName).trim();
134
+ }
135
+ const config = deps.config || require('../../core/config');
136
+ const developerId = await config.getDeveloperId();
137
+ return resolveLocalInstallationName(installationKey || 'local', developerId);
138
+ }
139
+
140
+ /**
141
+ * @async
142
+ * @param {string} envKey
143
+ * @param {Object} deps
144
+ * @returns {Promise<{ controllerUrl: string, authConfig: Object }>}
145
+ */
146
+ async function resolveAuthorizeSession(envKey, deps) {
147
+ const resolveControllerUrl =
148
+ deps.resolveControllerUrl || require('../../utils/controller-url').resolveControllerUrl;
149
+ const getOrRefreshDeviceToken =
150
+ deps.getOrRefreshDeviceToken ||
151
+ require('../../utils/token-manager').getOrRefreshDeviceToken;
152
+ const controllerUrl = await resolveControllerUrl();
153
+ if (!controllerUrl) {
154
+ throw new Error(
155
+ `Cannot create ${envKey}: no controller is configured, so whether your licence ` +
156
+ 'permits this environment cannot be checked. Run "aifabrix login" first.'
157
+ );
158
+ }
159
+ const deviceToken = await getOrRefreshDeviceToken(controllerUrl);
160
+ if (!deviceToken || !deviceToken.token) {
161
+ throw new Error(
162
+ `Cannot create ${envKey}: not signed in, so whether your licence permits this ` +
163
+ 'environment cannot be checked. Run "aifabrix login" first.'
164
+ );
165
+ }
166
+ return {
167
+ controllerUrl,
168
+ authConfig: {
169
+ type: 'device',
170
+ token: deviceToken.token,
171
+ controller: deviceToken.controller || controllerUrl
172
+ }
173
+ };
174
+ }
175
+
176
+ /**
177
+ * @async
178
+ * @param {string} envKey
179
+ * @param {Object} [deps]
180
+ * @returns {Promise<void>}
181
+ */
182
+ async function authorizeEnvironmentWithController(envKey, deps = {}) {
183
+ if (typeof deps.authorizeEnvironment === 'function') {
184
+ await deps.authorizeEnvironment(envKey);
185
+ return;
186
+ }
187
+ const createEnvironment =
188
+ deps.createEnvironment || require('../../api/environments.api').createEnvironment;
189
+ const { controllerUrl, authConfig } = await resolveAuthorizeSession(envKey, deps);
190
+ const serviceName = await resolveAuthorizeServiceName(deps, deps.installationKey);
191
+ if (!serviceName) {
192
+ throw new Error(
193
+ `Cannot create ${envKey}: local serviceName is empty. Name the installation ` +
194
+ '(for example local-dev03) or set a developer id with "aifabrix setup".'
195
+ );
196
+ }
197
+ const body = buildLocalEnvironmentCreateBody(envKey, {
198
+ serviceName,
199
+ preset: deps.preset,
200
+ location: deps.location
201
+ });
202
+ let response;
203
+ try {
204
+ response = await createEnvironment(controllerUrl, authConfig, body);
205
+ } catch (error) {
206
+ throw formatAuthorizeFailure(envKey, error);
207
+ }
208
+ if (response && response.success === false) {
209
+ throw formatAuthorizeFailure(envKey, response);
210
+ }
211
+ }
212
+
213
+ module.exports = {
214
+ LOCAL_LOCATION,
215
+ statusOf,
216
+ detailOf,
217
+ isContractValidationFailure,
218
+ formatAuthorizeFailure,
219
+ buildLocalEnvironmentCreateBody,
220
+ authorizeEnvironmentWithController
221
+ };
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Load an environment's bootstrap snapshot after its dataplane credential exists.
3
+ *
4
+ * Each environment holds its own application credential. `pro` must never authenticate
5
+ * with `dev`'s client secret. The snapshot is the settings source; this module does not
6
+ * write a parallel env file.
7
+ *
8
+ * @fileoverview Local env deploy — per-environment bootstrap snapshot
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const { resolveStrictSecretName } = require('../../parameters/physical-secret-name');
14
+
15
+ const DATAPLANE_CLIENT_ID_KEY = 'dataplane-client-idKeyVault';
16
+ const DATAPLANE_CLIENT_SECRET_KEY = 'dataplane-client-secretKeyVault';
17
+
18
+ /**
19
+ * @param {Object} secrets
20
+ * @param {string} key
21
+ * @returns {string|null}
22
+ */
23
+ function nonEmptySecret(secrets, key) {
24
+ if (!secrets || typeof secrets !== 'object') {
25
+ return null;
26
+ }
27
+ const value = secrets[key];
28
+ if (value === null || value === undefined) {
29
+ return null;
30
+ }
31
+ const text = String(value).trim();
32
+ return text ? text : null;
33
+ }
34
+
35
+ /**
36
+ * Resolve this environment's dataplane client credentials.
37
+ *
38
+ * Uses `resolveStrictSecretName` so an application-scoped secret is never shared between
39
+ * environments, even when the catalog still pins an unprefixed literal.
40
+ *
41
+ * @param {string} envKey
42
+ * @param {Object} secrets
43
+ * @returns {{ clientId: string, clientSecret: string, clientIdKey: string, clientSecretKey: string }}
44
+ * @throws {Error} When either credential is missing for this environment
45
+ */
46
+ function resolveEnvironmentDataplaneCredentials(envKey, secrets) {
47
+ const env = String(envKey || '').trim().toLowerCase();
48
+ const clientIdKey = resolveStrictSecretName(DATAPLANE_CLIENT_ID_KEY, env);
49
+ const clientSecretKey = resolveStrictSecretName(DATAPLANE_CLIENT_SECRET_KEY, env);
50
+ let clientId = nonEmptySecret(secrets, clientIdKey);
51
+ let clientSecret = nonEmptySecret(secrets, clientSecretKey);
52
+ let resolvedIdKey = clientIdKey;
53
+ let resolvedSecretKey = clientSecretKey;
54
+ // Same adoption rule as databases: a pre-environment install's unprefixed credential
55
+ // belongs to `dev`. tst and pro never adopt — that would share isolation away.
56
+ if (env === 'dev' && (!clientId || !clientSecret)) {
57
+ const adoptedId = nonEmptySecret(secrets, DATAPLANE_CLIENT_ID_KEY);
58
+ const adoptedSecret = nonEmptySecret(secrets, DATAPLANE_CLIENT_SECRET_KEY);
59
+ if (adoptedId && adoptedSecret) {
60
+ clientId = adoptedId;
61
+ clientSecret = adoptedSecret;
62
+ resolvedIdKey = DATAPLANE_CLIENT_ID_KEY;
63
+ resolvedSecretKey = DATAPLANE_CLIENT_SECRET_KEY;
64
+ }
65
+ }
66
+ if (!clientId || !clientSecret) {
67
+ const missing = [
68
+ !clientId ? clientIdKey : null,
69
+ !clientSecret ? clientSecretKey : null
70
+ ].filter(Boolean);
71
+ throw new Error(
72
+ `Cannot load a configuration snapshot for ${env}: missing ${missing.join(' and ')}. ` +
73
+ 'Each environment uses its own dataplane credential. Add ' +
74
+ `aifabrix secret set ${missing[0]} <value>` +
75
+ (missing[1] ? ` (and ${missing[1]})` : '') +
76
+ '.'
77
+ );
78
+ }
79
+ return {
80
+ clientId,
81
+ clientSecret,
82
+ clientIdKey: resolvedIdKey,
83
+ clientSecretKey: resolvedSecretKey
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Fetch the registered dataplane's bootstrap snapshot for this environment.
89
+ *
90
+ * @async
91
+ * @param {string} envKey
92
+ * @param {Object} [deps]
93
+ * @returns {Promise<{ configuration: Object, context: Object, issuedAt?: string }>}
94
+ */
95
+ async function loadEnvironmentBootstrapSnapshot(envKey, deps = {}) {
96
+ const resolveControllerUrl =
97
+ deps.resolveControllerUrl || require('../../utils/controller-url').resolveControllerUrl;
98
+ const loadSecrets = deps.loadSecrets || require('../../core/secrets').loadSecrets;
99
+ const fetchBootstrapSnapshot =
100
+ deps.fetchBootstrapSnapshot ||
101
+ require('../../api/bootstrap-snapshot.api').fetchBootstrapSnapshot;
102
+
103
+ const controllerUrl = await resolveControllerUrl();
104
+ if (!controllerUrl) {
105
+ throw new Error(
106
+ `Cannot load a configuration snapshot for ${envKey}: no controller is configured. ` +
107
+ 'Run "aifabrix login" first.'
108
+ );
109
+ }
110
+ const secrets = deps.secrets || (await loadSecrets(undefined));
111
+ const credentials = resolveEnvironmentDataplaneCredentials(envKey, secrets);
112
+ return fetchBootstrapSnapshot(
113
+ controllerUrl,
114
+ { clientId: credentials.clientId, clientSecret: credentials.clientSecret },
115
+ deps
116
+ );
117
+ }
118
+
119
+ module.exports = {
120
+ DATAPLANE_CLIENT_ID_KEY,
121
+ DATAPLANE_CLIENT_SECRET_KEY,
122
+ resolveEnvironmentDataplaneCredentials,
123
+ loadEnvironmentBootstrapSnapshot
124
+ };
@@ -19,6 +19,10 @@ const logger = require('../../utils/logger');
19
19
  const { resolveDatabaseNamesWithAdoption } = require('../../parameters/database-adoption');
20
20
  const { parseDatabaseSecretKey } = require('../../parameters/database-secret-values');
21
21
  const { resolveLocalInstallationName } = require('./local-installation');
22
+ const {
23
+ authorizeEnvironmentWithController
24
+ } = require('./local-environment-authorize');
25
+ const { loadEnvironmentBootstrapSnapshot } = require('./local-environment-snapshot');
22
26
 
23
27
  /**
24
28
  * Applications that exist **once per environment**.
@@ -127,65 +131,6 @@ async function ensureEnvironmentDatabases(envKey, deps) {
127
131
  return ensured;
128
132
  }
129
133
 
130
- /**
131
- * Ask Miso whether this environment may exist, before anything is created.
132
- *
133
- * **Editions are Miso's, not Builder's.** Whether a licence permits `tst` or `pro` is not a
134
- * question this repository answers or caches: it asks, and Miso accepts or declines. Builder
135
- * holding its own copy of the entitlement rules would be a second answer to drift from the
136
- * licence, and a local one at that — the easiest possible thing to edit.
137
- *
138
- * This runs **first**. A decline after the databases exist would leave an environment's
139
- * objects behind for an environment that is not licensed, and nothing would later remove
140
- * them.
141
- *
142
- * @async
143
- * @param {string} envKey - Environment key
144
- * @param {Object} deps - Injected collaborators
145
- * @returns {Promise<void>}
146
- * @throws {Error} When Miso declines, or cannot be reached to ask
147
- */
148
- async function authorizeEnvironmentWithController(envKey, deps) {
149
- if (typeof deps.authorizeEnvironment === 'function') {
150
- await deps.authorizeEnvironment(envKey);
151
- return;
152
- }
153
- const { resolveControllerUrl } = require('../../utils/controller-url');
154
- const { getOrRefreshDeviceToken } = require('../../utils/token-manager');
155
- const { createEnvironment } = require('../../api/environments.api');
156
-
157
- const controllerUrl = await resolveControllerUrl();
158
- if (!controllerUrl) {
159
- throw new Error(
160
- `Cannot create ${envKey}: no controller is configured, so whether your licence ` +
161
- 'permits this environment cannot be checked. Run "aifabrix login" first.'
162
- );
163
- }
164
- const deviceToken = await getOrRefreshDeviceToken(controllerUrl);
165
- if (!deviceToken || !deviceToken.token) {
166
- throw new Error(
167
- `Cannot create ${envKey}: not signed in, so whether your licence permits this ` +
168
- 'environment cannot be checked. Run "aifabrix login" first.'
169
- );
170
- }
171
- const authConfig = {
172
- type: 'device',
173
- token: deviceToken.token,
174
- controller: deviceToken.controller || controllerUrl
175
- };
176
- try {
177
- await createEnvironment(controllerUrl, authConfig, { key: envKey, environment: envKey });
178
- } catch (error) {
179
- // Miso owns the edition rules, so its refusal is reported as given rather than
180
- // reinterpreted here. Guessing at the reason would put a second, stale explanation of
181
- // the licence in front of the operator.
182
- throw new Error(
183
- `Miso declined environment ${envKey}: ${error.message}\n` +
184
- 'Editions are licensed in Miso. Nothing was created locally.'
185
- );
186
- }
187
- }
188
-
189
134
  /**
190
135
  * Workloads `env deploy` promotes into a promotion-only environment.
191
136
  *
@@ -241,24 +186,32 @@ async function promoteWorkloadsForEnvironment(envKey, deps) {
241
186
  * @param {string} installationKey - Positional installation key (`local` or `local-…`)
242
187
  * @param {string} envKey - Validated stage, one of dev|tst|pro
243
188
  * @param {Object} [deps] - Injected collaborators
244
- * @returns {Promise<{ stage: string, vendor: string, installation: string, databases: string[], promoted: string[] }>}
189
+ * @returns {Promise<{ stage: string, vendor: string, installation: string, databases: string[], promoted: string[], snapshot: { environmentId?: string, applicationId?: string }|null }>}
245
190
  */
246
191
  async function runLocalEnvironmentStage(installationKey, envKey, deps = {}) {
247
192
  const config = deps.config || require('../../core/config');
248
193
  const policy = deps.policy || require('../../core/environment-mode-policy');
194
+ const authorize = deps.authorizeEnvironmentWithController || authorizeEnvironmentWithController;
195
+ const loadSnapshot = deps.loadEnvironmentBootstrapSnapshot || loadEnvironmentBootstrapSnapshot;
196
+
197
+ const developerId = await config.getDeveloperId();
198
+ const installation = resolveLocalInstallationName(installationKey, developerId);
249
199
 
250
200
  // Miso first: it owns editions, and it accepts or declines this environment. Everything
251
- // below creates something, so the licence answer has to come before any of it.
252
- await authorizeEnvironmentWithController(envKey, deps);
201
+ // below creates something, so the licence answer has to come before any of it. Local
202
+ // create uses the same configuration fields as Azure (preset, serviceName, location).
203
+ await authorize(envKey, {
204
+ ...deps,
205
+ installationKey,
206
+ serviceName: installation,
207
+ preset: deps.preset
208
+ });
253
209
 
254
210
  // Then the local precondition. tst and pro are unreachable outside the front-door mode,
255
211
  // so creating their databases would leave resources behind for an environment that cannot
256
212
  // serve a request.
257
213
  await policy.assertEnvironmentAllowedForCurrentMode(envKey);
258
214
 
259
- const developerId = await config.getDeveloperId();
260
- const installation = resolveLocalInstallationName(installationKey, developerId);
261
-
262
215
  logger.log(`Deploying environment ${envKey} into local installation ${installation}`);
263
216
 
264
217
  // Databases before containers: a workload that starts without its database fails on its
@@ -271,7 +224,30 @@ async function runLocalEnvironmentStage(installationKey, envKey, deps = {}) {
271
224
  logger.log(`Promoted into ${envKey}: ${promoted.join(', ')}`);
272
225
  }
273
226
 
274
- return { stage: envKey, vendor: 'DOCKER', installation, databases, promoted };
227
+ const snapshot = await summarizeEnvironmentSnapshot(envKey, loadSnapshot, deps);
228
+ return { stage: envKey, vendor: 'DOCKER', installation, databases, promoted, snapshot };
229
+ }
230
+
231
+ /**
232
+ * Load this environment's own bootstrap snapshot after the dataplane can register.
233
+ *
234
+ * @async
235
+ * @param {string} envKey
236
+ * @param {Function} loadSnapshot
237
+ * @param {Object} deps
238
+ * @returns {Promise<{ environmentId?: string, applicationId?: string }>}
239
+ */
240
+ async function summarizeEnvironmentSnapshot(envKey, loadSnapshot, deps) {
241
+ const snapshotPayload = await loadSnapshot(envKey, deps);
242
+ const context = (snapshotPayload && snapshotPayload.context) || {};
243
+ logger.log(
244
+ `Loaded configuration snapshot for ${envKey}` +
245
+ (context.environmentId ? ` (environment ${context.environmentId})` : '')
246
+ );
247
+ return {
248
+ environmentId: context.environmentId,
249
+ applicationId: context.applicationId
250
+ };
275
251
  }
276
252
 
277
253
  module.exports = {
@@ -281,5 +257,6 @@ module.exports = {
281
257
  resolveEnvironmentDatabasePlan,
282
258
  ensureEnvironmentDatabases,
283
259
  promoteWorkloadsForEnvironment,
260
+ loadEnvironmentBootstrapSnapshot,
284
261
  runLocalEnvironmentStage
285
262
  };
@@ -30,30 +30,14 @@ function readPrivateKey(value, encryptionKey, pairId) {
30
30
  /**
31
31
  * Pairs whose private half belongs to Mori, not to an installation.
32
32
  *
33
- * Builder must never mint these. Licence trust is a two-signature chain, and both links
34
- * exist so that substituting a key fails rather than silently succeeding:
33
+ * Builder must never mint these. Generating either half would hand an installation the
34
+ * private key of material it only verifies against, so it could sign a licence it then
35
+ * accepts. 227.1 pinned the verifying key "precisely because a key travelling inside the
36
+ * token it verifies proves nothing"; a self-generated key proves exactly as little.
35
37
  *
36
- * 1. Mori signs the licence. The installation verifies it against the public key held on
37
- * disk, so a licence from anywhere else does not verify.
38
- * 2. That on-disk material is itself validated against a key in Key Vault. The Key Vault is
39
- * provisioned by the Marketplace deployment and readable only by Miso's managed
40
- * identity — nobody can change it, including us — so swapping the on-disk public key for
41
- * one whose private half an attacker holds fails at the second check.
42
- *
43
- * The second link is an **Azure** guarantee. A Docker installation has no Key Vault and no
44
- * managed identity, so locally the chain is the pinned public key alone. Closing that is
45
- * online validation against Mori, which is a known gap deferred on purpose: the delivery
46
- * target is Azure, and Docker exists to validate the shape of that stack.
47
- *
48
- * It does not soften this rule. The one environment lacking the second check is the last
49
- * one that should mint its own keys, and a locally generated pair would make local
50
- * behaviour diverge from the Azure stack it exists to mirror.
51
- *
52
- * Generating the pair locally breaks both links at once: the installation would hold the
53
- * private half of the key it verifies against, so it could sign a licence it then accepts,
54
- * and the material would no longer correspond to what Key Vault validates. 227.1 pinned
55
- * this key "precisely because a key travelling inside the token it verifies proves
56
- * nothing"; a self-generated key proves exactly as little.
38
+ * How Miso pins, stores, or cross-checks that public key (Key Vault, Docker secrets file,
39
+ * online Mori check) is Miso's contract — Builder has no code path that inspects a vault
40
+ * or deployment mode. Builder's job stops at: refuse to mint, resolve what was supplied.
57
41
  *
58
42
  * The public half reaches an installation through the `moriLicensePublicKey` deployment
59
43
  * parameter, which the installation contract already classifies as registration-supplied.
@@ -120,8 +104,8 @@ function generateRsaSecretValues(keys, existing = {}, encryptionKey) {
120
104
  if (populated(existing[key]) || populated(values[key])) continue;
121
105
  const entry = catalog.findEntryForKey(key);
122
106
  if (entry?.generator?.type !== 'rsaKeyPair') continue;
123
- // Supplied by Mori at deployment and validated against Key Vault. Generating it here
124
- // would hand the installation the private half of the key it verifies licences against.
107
+ // Supplied by Mori; never generated here. Minting it would hand the installation the
108
+ // private half of the key it verifies licences against.
125
109
  if (isIssuerSuppliedPair(entry)) continue;
126
110
  const entries = catalog.data.parameters.filter(e => e.key &&
127
111
  e.generator?.type === 'rsaKeyPair' && e.generator.pairId === entry.generator.pairId);
@@ -146,12 +130,11 @@ function generateRsaSecretValues(keys, existing = {}, encryptionKey) {
146
130
  function generateRsaValueFromCatalog(key, catalogEntry) {
147
131
  if (isIssuerSuppliedPair(catalogEntry)) {
148
132
  throw new Error(
149
- `Secret "${key}" is supplied by Mori and is never generated here. Licence trust is a ` +
150
- 'two-signature chain: Mori signs the licence, which is verified against the on-disk ' +
151
- 'public key, and that material is validated against a Key Vault key readable only by ' +
152
- 'Miso\'s managed identity. The public half reaches an installation through the ' +
153
- 'moriLicensePublicKey deployment parameter. An installation with neither stays ' +
154
- 'unpinned and refuses every licence, which is the intended failure.'
133
+ `Secret "${key}" is supplied by Mori and is never generated here. An installation that ` +
134
+ 'minted it would hold the private half of the key it verifies licences against. The ' +
135
+ 'public half reaches an installation through the moriLicensePublicKey deployment ' +
136
+ 'parameter. An installation with neither stays unpinned and refuses every licence, ' +
137
+ 'which is the intended failure.'
155
138
  );
156
139
  }
157
140
  return generateRsaSecretValues([key])[key];
@@ -28,10 +28,6 @@ standardUpInfraEnsureKeys:
28
28
  - databases-dataplane-2-passwordKeyVault
29
29
  - databases-dataplane-3-urlKeyVault
30
30
  - databases-dataplane-3-passwordKeyVault
31
- # Miso-controller Entra onboarding (vendor app); required for aifabrix resolve miso-controller
32
- - entra-callback-uri
33
- - entra-client-idKeyVault
34
- - entra-client-secretKeyVault
35
31
  parameters:
36
32
  - key: postgres-passwordKeyVault
37
33
  scope: infra
@@ -351,47 +347,8 @@ parameters:
351
347
 
352
348
  # Miso-controller Entra ID onboarding (vendor multi-tenant app). Empty client/callback values
353
349
  # allow local resolve and start-dev; set real values before Entra admin-consent flows.
354
- - key: entra-callback-uri
355
- category: Microsoft Entra ID
356
- sensitive: false
357
- valueSource: optionalInput
358
- requiredOnSystemOnboarding: false
359
- scope: system
360
- generator:
361
- type: emptyString
362
- ensureOn: [upInfra, resolveApp]
363
- azure:
364
- notes: >-
365
- ENTRA_ID_CENTRAL_CALLBACK_URI — Mori web redirect (e.g. …/entra-id-concent/callback);
366
- empty locally until Mori URL is configured.
367
-
368
- - key: entra-client-idKeyVault
369
- category: Microsoft Entra ID
370
- sensitive: false
371
- valueSource: optionalInput
372
- requiredOnSystemOnboarding: false
373
- scope: system
374
- generator:
375
- type: emptyString
376
- ensureOn: [upInfra, resolveApp]
377
- azure:
378
- notes: >-
379
- Vendor Entra app client id (ENTRA_ID_CLIENT_ID); empty for local dev without Entra flows.
380
-
381
- - key: entra-client-secretKeyVault
382
- category: Microsoft Entra ID
383
- sensitive: true
384
- valueSource: optionalInput
385
- requiredOnSystemOnboarding: false
386
- scope: system
387
- generator:
388
- type: emptyString
389
- ensureOn: [upInfra, resolveApp]
390
- azure:
391
- legacyVaultSecretNames:
392
- - entra-vendor-client-secret
393
- notes: >-
394
- Vendor Entra app client secret (ENTRA_ID_CLIENT_SECRET); empty for local dev without Entra flows.
350
+ # No entra-callback-uri, entra-client-idKeyVault or entra-client-secretKeyVault: Miso holds no
351
+ # Entra client credentials; Mori owns the registration and every sign-in (plan 248.0).
395
352
 
396
353
  # Keycloak events: webhook signature secret used by miso-controller (KEYCLOAK_EVENTS_SECRET).
397
354
  # This must exist on first boot because up-platform runs containers from templates (no prior resolve).
@@ -509,8 +509,6 @@
509
509
  "acrResourceId",
510
510
  "deployRoleAssignments",
511
511
  "moriBaseUrl",
512
- "entraIdClientId",
513
- "entraIdClientSecret",
514
512
  "entraIdTenantId",
515
513
  "licenseEdition",
516
514
  "capacityProfile",
@@ -970,62 +968,6 @@
970
968
  "encryptionKey": {
971
969
  "$ref": "#/definitions/envReference"
972
970
  },
973
- "entraIdClientId": {
974
- "type": "object",
975
- "required": [
976
- "value",
977
- "source",
978
- "description"
979
- ],
980
- "additionalProperties": false,
981
- "properties": {
982
- "value": {
983
- "type": "null"
984
- },
985
- "source": {
986
- "type": "string",
987
- "minLength": 1
988
- },
989
- "description": {
990
- "type": "string",
991
- "minLength": 1
992
- },
993
- "required": {
994
- "type": "boolean"
995
- },
996
- "requiredAt": {
997
- "const": "tenantActivation"
998
- }
999
- }
1000
- },
1001
- "entraIdClientSecret": {
1002
- "type": "object",
1003
- "required": [
1004
- "value",
1005
- "source",
1006
- "description"
1007
- ],
1008
- "additionalProperties": false,
1009
- "properties": {
1010
- "value": {
1011
- "type": "null"
1012
- },
1013
- "source": {
1014
- "type": "string",
1015
- "minLength": 1
1016
- },
1017
- "description": {
1018
- "type": "string",
1019
- "minLength": 1
1020
- },
1021
- "required": {
1022
- "type": "boolean"
1023
- },
1024
- "requiredAt": {
1025
- "const": "tenantActivation"
1026
- }
1027
- }
1028
- },
1029
971
  "entraIdTenantId": {
1030
972
  "type": "object",
1031
973
  "required": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.63.0",
3
+ "version": "2.63.2",
4
4
  "description": "AI Fabrix Builder — CLI and developer scripts (pnpm + af)",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -5,7 +5,7 @@ app:
5
5
  description: 'Business Transformation services — HTTP orchestration for connected-system workspace lifecycle via Dataplane Enterprise MCP and governance APIs.'
6
6
  type: webapp
7
7
  language: typescript
8
- version: 2.63.0
8
+ version: 2.63.2
9
9
 
10
10
  # Image Configuration
11
11
  image:
@@ -5,7 +5,7 @@ app:
5
5
  description: "AI Fabrix Dataplane is a secure, in-tenant integration and automation layer that supplies governed, normalized, and explainable enterprise data to AI agents. Using CIP as a declarative standard, it enforces RBAC and ABAC, executes integrations, and exposes trusted data via MCP and OpenAPI."
6
6
  type: webapp
7
7
  language: python # Explicitly specify Python language
8
- version: 2.0.82
8
+ version: 2.0.84
9
9
  # Image Configuration
10
10
  # Set tag to match your build (example: aifabrix build dataplane -t 1.0.0).
11
11
  # Registry is required so the controller can pull the image (avoids docker-not-found on the controller host).
@@ -44,7 +44,7 @@ API_KEY_BYPASS_ROLES=full-access
44
44
 
45
45
  # API Configuration
46
46
  API_V1_STR=/api/v1
47
- VERSION=2.0.82
47
+ VERSION=2.0.84
48
48
  # Minimum supported aifabrix-builder CLI semver (advertised on /health and /api/v1/health; empty = no gate)
49
49
  # MIN_BUILDER_CLI_VERSION=2.45.0
50
50
  MIN_BUILDER_CLI_VERSION=2.45.5
@@ -6,7 +6,7 @@
6
6
  "repo": "aifabrix-miso",
7
7
  "path": "builder/miso-controller/env.template"
8
8
  },
9
- "sha256": "9b74b94d5d6e7457d22cfe213f2ec9c3dceb75c540df3953aba3b51c35df05c7"
9
+ "sha256": "c14aba1bd4588e334b89f3714714521e659d29f96bb76fc28ef17cacec47c80f"
10
10
  },
11
11
  {
12
12
  "path": "templates/applications/builder-api/env.template",
@@ -30,7 +30,7 @@
30
30
  "repo": "aifabrix-dataplane",
31
31
  "path": "builder/dataplane/env.template"
32
32
  },
33
- "sha256": "d35eda839464a65ebd018064ea7c2d292654891b41d1f56862b052720a78a3cc"
33
+ "sha256": "3f4abcd6e3b7a8cb8a6029c8841b63c2384b863ef168a2ed0128bd5856119e3f"
34
34
  },
35
35
  {
36
36
  "path": "templates/applications/integration-server/env.template",
@@ -4,7 +4,7 @@ app:
4
4
  displayName: 'Miso Controller'
5
5
  description: 'Miso is the AI Fabrix in-tenant controller and portal layer for securely operating enterprise AI apps inside a customer’s Azure tenant. It provides Entra ID SSO, RBAC, audit logs, environment/app configuration via schemas, and safe secret handling via Key Vault references—ensuring governance, traceability, and predictable UX across portal, SDK, and CLI.'
6
6
  type: webapp
7
- version: '2.0.91'
7
+ version: '2.0.95'
8
8
  # Image Configuration
9
9
  # Set tag to match your build (e.g. aifabrix build miso-controller -t 1.0.0 then tag: 1.0.0)
10
10
  # Registry is required so the controller can pull the image (avoids "docker: not found" on the controller host).
@@ -96,6 +96,11 @@ platforms:
96
96
  value: 'azure'
97
97
  - name: MORI_BASE_URL
98
98
  value: 'https://platform.aifabrix.dev/mori'
99
+ # Names the Mori policy-signing pair the licence bundle is checked against. Must match
100
+ # Mori's own POLICY_KEY_ID (aifabrix-mori builder/mori-controller/application.yaml).
101
+ # Names the Mori policy-signing pair (MORI_POLICY_KEY_ID); must match Mori's manifest.
102
+ - name: POLICY_KEY_ID
103
+ value: 'mori-policy-key-1'
99
104
 
100
105
  # Local Docker runs. No resource group is supplied, so `matchLocalPlatform` takes the FIRST entry
101
106
  # declaring the local platform (`dev`) -- this one -- and the literal `aifabrix-local` can never
@@ -135,6 +140,10 @@ platforms:
135
140
  value: 'swedencentral'
136
141
  - name: MORI_BASE_URL
137
142
  value: 'url://mori-controller-internal'
143
+ # Names the Mori policy-signing pair the licence bundle is checked against. Must match
144
+ # Mori's own POLICY_KEY_ID (aifabrix-mori builder/mori-controller/application.yaml).
145
+ - name: POLICY_KEY_ID
146
+ value: 'mori-policy-key-1'
138
147
  # Declared, because the fallback infers it and infers it wrongly here:
139
148
  # `resolveCloudVendor` returns 'azure' whenever AZURE_SUBSCRIPTION_ID is set, and every
140
149
  # developer box has it from the shared development service principal. A local stack was
@@ -186,6 +195,10 @@ platforms:
186
195
  # consumer of it was pointed at a loopback address that does not exist in the cluster.
187
196
  - name: MORI_BASE_URL
188
197
  value: 'https://platform-{platform}.aifabrix.dev/mori'
198
+ # Names the Mori policy-signing pair the licence bundle is checked against. Must match
199
+ # Mori's own POLICY_KEY_ID (aifabrix-mori builder/mori-controller/application.yaml).
200
+ - name: POLICY_KEY_ID
201
+ value: 'mori-policy-key-1'
189
202
 
190
203
  - resource: aifabrix-ce-*
191
204
  name: '*'
@@ -212,6 +225,10 @@ platforms:
212
225
  # (`platform-dev.aifabrix.dev/mori`) this entry can follow the pattern used above.
213
226
  - name: MORI_BASE_URL
214
227
  value: 'https://{resourceGroup}-miso-mori-controller.azurewebsites.net/mori'
228
+ # Names the Mori policy-signing pair the licence bundle is checked against. Must match
229
+ # Mori's own POLICY_KEY_ID (aifabrix-mori builder/mori-controller/application.yaml).
230
+ - name: POLICY_KEY_ID
231
+ value: 'mori-policy-key-1'
215
232
 
216
233
  configuration:
217
234
  # -------------------------------------------------------------------------
@@ -148,8 +148,7 @@ REDIS_PERMISSIONS_TTL=900
148
148
  # sync-application-urls-from-env.service.
149
149
  #
150
150
  # After POST /api/ide/onboarding/entra-id/configure succeeds, controller adds Microsoft Entra IdP to stock Keycloak
151
- # using the customer enterprise app (AI Fabrix Miso), not the vendor ENTRA_ID_CLIENT_ID app.
152
- # After configure: Keycloak broker redirect is on the customer enterprise app (AI Fabrix Miso), not ENTRA_ID_CLIENT_ID.
151
+ # using the customer enterprise app (AI Fabrix Miso); its broker redirect is on that app.
153
152
 
154
153
  KEYCLOAK_REALM=aifabrix
155
154
  # Public issuer / browser URL (includes /auth when Keycloak uses KC_HTTP_RELATIVE_PATH=/auth).
@@ -231,16 +230,8 @@ MISO_CLIENTSECRET=kv://miso-controller-client-secretKeyVault
231
230
  # Kept in the shared block because EITHER mode may install Entra later from the plan 219
232
231
  # provider catalogue: Azure hosting does not force Entra, and SaaS may choose it.
233
232
  #
234
- # AI Fabrix vendor app for TA-1 admin consent and Entra ID configure flows.
235
- # Same ENTRA_ID_CLIENT_ID / ENTRA_ID_CLIENT_SECRET on every deployment.
236
- # Customer SSO after configure uses a separate enterprise app (AI Fabrix Miso);
237
- # Keycloak broker redirect is on that customer app, not ENTRA_ID_CLIENT_ID.
238
- #
239
- # ENTRA_ID_CENTRAL_CALLBACK_URI — Web redirect on the vendor app; must point at
240
- # Mori (e.g. …/entra-id-concent/callback), not the SPA or controller.
241
- ENTRA_ID_CENTRAL_CALLBACK_URI=url://mori-controller-public/entra-id-concent/callback
242
- ENTRA_ID_CLIENT_ID=kv://entra-client-idKeyVault
243
- ENTRA_ID_CLIENT_SECRET=kv://entra-client-secretKeyVault
233
+ # No Entra client id, secret or callback here. Mori owns the app registration and every
234
+ # sign-in; Miso only says which scopes it needs (plan 248.0, remove-vendor-app-secret-from-miso).
244
235
 
245
236
  # =============================================================================
246
237
  # TENANT ACTIVATION (TA-3) — EXISTING LLM CATALOG — [customerOwned]
@@ -378,15 +369,6 @@ MORI_LICENSE_PUBLIC_KEY=kv://mori-license-public-keyKeyVault
378
369
  # MORI_LICENSE_PUBLIC_KEY_FILE=/run/secrets/mori-license-public-key.pem
379
370
  MORI_LICENSE_ISSUER=mori-controller
380
371
 
381
- # =============================================================================
382
- # ENTRA ID ONBOARDING (delegated Graph via Mori)
383
- # =============================================================================
384
- # Must match a Web redirect URI on the vendor multi-tenant app and Mori’s route
385
- # …/entra-graph-delegated/callback. Shared pro Mori addon (CICD *-miso-rg):
386
- ENTRA_GRAPH_DELEGATED_REDIRECT_URI=url://mori-controller-public/entra-graph-delegated/callback
387
-
388
- # Optional delegated Graph scopes for GET …/graph-auth-url (space- or comma-separated).
389
- # ENTRA_GRAPH_DELEGATED_SCOPES=https://graph.microsoft.com/Application.ReadWrite.All,https://graph.microsoft.com/Group.ReadWrite.All,https://graph.microsoft.com/Synchronization.ReadWrite.All,openid,profile,offline_access
390
372
 
391
373
  # Dev only: log full delegated Graph JWT in general_log_* as delegatedGraphJwt (default off).
392
374
  # LOG_GRAPH_TOKEN=true
@@ -91,6 +91,10 @@ permissions:
91
91
  roles: ['aifabrix-platform-admin', 'aifabrix-security-admin']
92
92
  description: 'Delete groups'
93
93
 
94
+ - name: 'user-groups:update'
95
+ roles: ['aifabrix-security-admin']
96
+ description: 'Manage user-group memberships'
97
+
94
98
  # Administrative Permissions
95
99
  - name: 'admin:read'
96
100
  roles: ['aifabrix-platform-admin']
@@ -315,15 +319,15 @@ permissions:
315
319
  description: 'View dimensions and dimension values'
316
320
 
317
321
  - name: 'dimensions:create'
318
- roles: ['aifabrix-platform-admin', 'aifabrix-security-admin', 'aifabrix-deployment-admin']
322
+ roles: ['aifabrix-security-admin']
319
323
  description: 'Create dimensions and dimension values'
320
324
 
321
325
  - name: 'dimensions:update'
322
- roles: ['aifabrix-platform-admin', 'aifabrix-security-admin', 'aifabrix-deployment-admin']
326
+ roles: ['aifabrix-security-admin']
323
327
  description: 'Update dimensions and dimension values'
324
328
 
325
329
  - name: 'dimensions:delete'
326
- roles: ['aifabrix-platform-admin', 'aifabrix-security-admin']
330
+ roles: ['aifabrix-security-admin']
327
331
  description: 'Delete dimensions and dimension values'
328
332
 
329
333
  # Resource Type Management
@@ -363,15 +367,15 @@ permissions:
363
367
  description: 'View business roles'
364
368
 
365
369
  - name: 'roles:create'
366
- roles: ['aifabrix-platform-admin', 'aifabrix-security-admin', 'aifabrix-deployment-admin']
370
+ roles: ['aifabrix-security-admin']
367
371
  description: 'Create business roles'
368
372
 
369
373
  - name: 'roles:update'
370
- roles: ['aifabrix-platform-admin', 'aifabrix-security-admin', 'aifabrix-deployment-admin']
374
+ roles: ['aifabrix-security-admin']
371
375
  description: 'Update business roles'
372
376
 
373
377
  - name: 'roles:delete'
374
- roles: ['aifabrix-platform-admin', 'aifabrix-security-admin']
378
+ roles: ['aifabrix-security-admin']
375
379
  description: 'Delete business roles'
376
380
 
377
381
  # Evidence Kind Management
@@ -5,7 +5,7 @@
5
5
  "_generator": {
6
6
  "name": "bicep",
7
7
  "version": "0.47.16.16243",
8
- "templateHash": "703508813427294425"
8
+ "templateHash": "17889093247943830402"
9
9
  }
10
10
  },
11
11
  "parameters": {
@@ -179,20 +179,6 @@
179
179
  "description": "Expected `iss` claim on a Mori licence."
180
180
  }
181
181
  },
182
- "entraIdClientId": {
183
- "type": "string",
184
- "defaultValue": "481251f2-a318-47ba-9ad8-40c36f790df1",
185
- "metadata": {
186
- "description": "Vendor Entra multi-tenant app client ID. Default matches vendor miso-controller .env (not in createUiDefinition)."
187
- }
188
- },
189
- "entraIdClientSecret": {
190
- "type": "securestring",
191
- "defaultValue": "",
192
- "metadata": {
193
- "description": "Optional vendor Entra app client secret. Configure through the Miso installation contract."
194
- }
195
- },
196
182
  "entraIdTenantId": {
197
183
  "type": "string",
198
184
  "defaultValue": "09aff594-ba37-4af2-bf40-b773b654c563",
@@ -2540,9 +2526,6 @@
2540
2526
  "encryptionKey": {
2541
2527
  "value": "[reference(resourceId('Microsoft.Resources/deployments', 'bootstrapSecrets'), '2025-04-01').outputs.encryptionKey.value]"
2542
2528
  },
2543
- "entraIdClientSecret": {
2544
- "value": "[parameters('entraIdClientSecret')]"
2545
- },
2546
2529
  "moriLicensePublicKey": {
2547
2530
  "value": "[parameters('moriLicensePublicKey')]"
2548
2531
  },
@@ -2560,7 +2543,7 @@
2560
2543
  "_generator": {
2561
2544
  "name": "bicep",
2562
2545
  "version": "0.47.16.16243",
2563
- "templateHash": "4650500285752133401"
2546
+ "templateHash": "6681447246114830480"
2564
2547
  }
2565
2548
  },
2566
2549
  "parameters": {
@@ -2629,13 +2612,6 @@
2629
2612
  "description": "MISO Client Secret (auto-generated if not provided)"
2630
2613
  }
2631
2614
  },
2632
- "entraIdClientSecret": {
2633
- "type": "securestring",
2634
- "defaultValue": "",
2635
- "metadata": {
2636
- "description": "Vendor Entra app client secret (ENTRA_ID_CLIENT_SECRET). Placeholder when empty — replace for live Entra flows."
2637
- }
2638
- },
2639
2615
  "moriLicensePublicKey": {
2640
2616
  "type": "string",
2641
2617
  "defaultValue": "",
@@ -2659,7 +2635,6 @@
2659
2635
  }
2660
2636
  },
2661
2637
  "variables": {
2662
- "resolvedEntraIdClientSecret": "[if(not(empty(parameters('entraIdClientSecret'))), parameters('entraIdClientSecret'), format('PLACEHOLDER_ENTRA_CLIENT_SECRET_MUST_BE_REPLACED_{0}', uniqueString(resourceGroup().id, parameters('prefix'))))]",
2663
2638
  "misoWebAppOrigin": "[if(endsWith(parameters('misoWebAppUrl'), '/'), substring(parameters('misoWebAppUrl'), 0, max(0, sub(length(parameters('misoWebAppUrl')), 1))), parameters('misoWebAppUrl'))]",
2664
2639
  "misoVdirSegment": "[if(or(equals(parameters('misoVdirPublic'), ''), equals(parameters('misoVdirPublic'), '/')), '', if(startsWith(parameters('misoVdirPublic'), '/'), parameters('misoVdirPublic'), format('/{0}', parameters('misoVdirPublic'))))]",
2665
2640
  "misoWebServerPublicUrl": "[if(empty(variables('misoVdirSegment')), variables('misoWebAppOrigin'), format('{0}{1}', variables('misoWebAppOrigin'), variables('misoVdirSegment')))]",
@@ -2750,14 +2725,6 @@
2750
2725
  "value": "[if(not(empty(parameters('misoClientSecret'))), parameters('misoClientSecret'), format('PLACEHOLDER_CLIENT_SECRET_MUST_BE_REPLACED_{0}', uniqueString(resourceGroup().id, parameters('prefix'))))]"
2751
2726
  }
2752
2727
  },
2753
- {
2754
- "type": "Microsoft.KeyVault/vaults/secrets",
2755
- "apiVersion": "2023-07-01",
2756
- "name": "[format('{0}/{1}', parameters('keyVaultName'), 'entra-client-secretKeyVault')]",
2757
- "properties": {
2758
- "value": "[variables('resolvedEntraIdClientSecret')]"
2759
- }
2760
- },
2761
2728
  {
2762
2729
  "condition": "[not(empty(parameters('moriLicensePublicKey')))]",
2763
2730
  "type": "Microsoft.KeyVault/vaults/secrets",
@@ -2891,9 +2858,6 @@
2891
2858
  "moriLicenseIssuer": {
2892
2859
  "value": "[parameters('moriLicenseIssuer')]"
2893
2860
  },
2894
- "entraIdClientId": {
2895
- "value": "[parameters('entraIdClientId')]"
2896
- },
2897
2861
  "entraIdTenantId": {
2898
2862
  "value": "[variables('effectiveEntraIdTenantId')]"
2899
2863
  },
@@ -2926,7 +2890,7 @@
2926
2890
  "_generator": {
2927
2891
  "name": "bicep",
2928
2892
  "version": "0.47.16.16243",
2929
- "templateHash": "9991546815810387946"
2893
+ "templateHash": "16391707537665698614"
2930
2894
  }
2931
2895
  },
2932
2896
  "parameters": {
@@ -3136,13 +3100,6 @@
3136
3100
  "description": "Expected `iss` claim on a Mori licence."
3137
3101
  }
3138
3102
  },
3139
- "entraIdClientId": {
3140
- "type": "string",
3141
- "defaultValue": "481251f2-a318-47ba-9ad8-40c36f790df1",
3142
- "metadata": {
3143
- "description": "Vendor multi-tenant Entra app client ID. No longer emitted as an app setting — Miso does not hold provider credentials; Mori does. Retained so the marketplace parameter surface is unchanged."
3144
- }
3145
- },
3146
3103
  "entraIdTenantId": {
3147
3104
  "type": "string",
3148
3105
  "defaultValue": "09aff594-ba37-4af2-bf40-b773b654c563",
@@ -3224,8 +3181,6 @@
3224
3181
  "databaseUrlAppSettingValue": "[if(not(empty(parameters('databaseUrlSecretUriWithVersion'))), format('@Microsoft.KeyVault(SecretUri={0})', parameters('databaseUrlSecretUriWithVersion')), format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-databases-0-urlKeyVault)', last(split(parameters('keyVaultId'), '/')), parameters('prefix')))]",
3225
3182
  "databaseLogUrlAppSettingValue": "[if(not(empty(parameters('databaseLogUrlSecretUriWithVersion'))), format('@Microsoft.KeyVault(SecretUri={0})', parameters('databaseLogUrlSecretUriWithVersion')), format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-databases-1-urlKeyVault)', last(split(parameters('keyVaultId'), '/')), parameters('prefix')))]",
3226
3183
  "moriBaseNormalized": "[if(and(greater(length(parameters('moriBaseUrl')), 1), endsWith(parameters('moriBaseUrl'), '/')), substring(parameters('moriBaseUrl'), 0, max(0, sub(length(parameters('moriBaseUrl')), 1))), parameters('moriBaseUrl'))]",
3227
- "moriEntraGraphDelegatedRedirectUri": "[format('{0}/entra-graph-delegated/callback', variables('moriBaseNormalized'))]",
3228
- "moriEntraCentralCallbackUri": "[format('{0}/entra-id-concent/callback', variables('moriBaseNormalized'))]",
3229
3184
  "registryCredentialAppSettings": "[if(and(parameters('registryPasswordConfigured'), not(empty(parameters('acrLoginServer')))), createArray(createObject('name', 'DOCKER_REGISTRY_SERVER_USERNAME', 'value', parameters('acrAdminUsername')), createObject('name', 'DOCKER_REGISTRY_SERVER_PASSWORD', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-acr-registry-password)', last(split(parameters('keyVaultId'), '/')), parameters('prefix')))), createArray())]"
3230
3185
  },
3231
3186
  "resources": [
@@ -3268,7 +3223,7 @@
3268
3223
  "acrUseManagedIdentityCreds": "[not(parameters('registryPasswordConfigured'))]",
3269
3224
  "healthCheckPath": "/health",
3270
3225
  "alwaysOn": true,
3271
- "appSettings": "[concat(createArray(createObject('name', 'WEBSITES_ENABLE_APP_SERVICE_STORAGE', 'value', 'false'), createObject('name', 'WEBSITES_CONTAINER_START_TIME_LIMIT', 'value', '1800'), createObject('name', 'DATABASE_URL', 'value', variables('databaseUrlAppSettingValue')), createObject('name', 'JWT_SECRET', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-secrets-jwtKeyVault)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'NODE_ENV', 'value', 'production'), createObject('name', 'LOCATION', 'value', parameters('location')), createObject('name', 'PORT', 'value', '3000'), createObject('name', 'ALLOWED_ORIGINS', 'value', format('http://localhost:*,http://127.0.0.1:*,https://{0}.azurewebsites.net', parameters('name'))), createObject('name', 'WEBSITE_VNET_ROUTE_ALL', 'value', if(parameters('enablePostgresPublicAccess'), '0', '1')), createObject('name', 'WEBSITE_DNS_SERVER', 'value', '168.63.129.16'), createObject('name', 'DATABASELOG_URL', 'value', variables('databaseLogUrlAppSettingValue')), createObject('name', 'KEY_VAULT_ENABLED', 'value', 'true'), createObject('name', 'SECURITY_STORAGE_MODE', 'value', 'keyvault'), createObject('name', 'KEY_VAULT_URL', 'value', format('https://{0}.vault.azure.net/', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'ENCRYPTION_KEY', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-encryption-keyKeyVault)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'AUTO_CREATE_TABLES', 'value', if(parameters('autoCreateTables'), 'true', 'false')), createObject('name', 'KEYCLOAK_REALM', 'value', parameters('keycloakRealm')), createObject('name', 'KEYCLOAK_CLIENT_ID', 'value', parameters('keycloakClientId')), createObject('name', 'KEYCLOAK_ADMIN_USERNAME', 'value', parameters('keycloakAdminUsername')), createObject('name', 'KEYCLOAK_ADMIN_PASSWORD', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-keycloak-admin-password)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'ONBOARDING_ADMIN_PASSWORD', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-onboarding-admin-password)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'ONBOARDING_ADMIN_EMAIL', 'value', parameters('adminEmail')), createObject('name', 'KEYCLOAK_SERVER_URL', 'value', variables('keycloakPublicUrl')), createObject('name', 'KEYCLOAK_INTERNAL_SERVER_URL', 'value', variables('keycloakInternalUrl')), createObject('name', 'KEYCLOAK_TOKEN_TIMEOUT', 'value', '5000'), createObject('name', 'KEYCLOAK_VERIFY_AUDIENCE', 'value', if(parameters('keycloakVerifyAudience'), 'true', 'false')), createObject('name', 'DISABLE_RATE_LIMIT', 'value', variables('shouldDisableRateLimit')), createObject('name', 'SKIP_DB_REDEPLOY', 'value', 'true'), createObject('name', 'MISO_WEB_SERVER_URL', 'value', variables('misoPublicUrl')), createObject('name', 'MISO_CONTROLLER_URL', 'value', variables('misoInternalUrl')), createObject('name', 'MISO_RELATIVE_PATH', 'value', variables('misoVdirValue')), createObject('name', 'CONTROLLER_BASE_URL', 'value', if(not(empty(parameters('misoControllerUrl'))), parameters('misoControllerUrl'), format('https://{0}.azurewebsites.net', parameters('name')))), createObject('name', 'AZURE_SUBSCRIPTION_ID', 'value', subscription().subscriptionId), createObject('name', 'TENANT_ID', 'value', subscription().tenantId), createObject('name', 'DEPLOYMENT', 'value', 'azure'), createObject('name', 'AZURE_RESOURCE_GROUP_NAME', 'value', parameters('resourceGroupName')), createObject('name', 'INFRASTRUCTURE_NAME', 'value', parameters('prefix')), createObject('name', 'ENTRA_ID_TENANT_ID', 'value', parameters('entraIdTenantId')), createObject('name', 'MISO_CLIENTID', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-miso-clientIdKeyVault)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'MISO_CLIENTSECRET', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName=miso-controller-client-secretKeyVault)', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'MORI_BASE_URL', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName=mori-base-urlKeyVault)', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'MORI_LICENSE_PUBLIC_KEY', 'value', if(empty(parameters('moriLicensePublicKey')), '', format('@Microsoft.KeyVault(VaultName={0};SecretName=mori-license-public-keyKeyVault)', last(split(parameters('keyVaultId'), '/'))))), createObject('name', 'MORI_LICENSE_ISSUER', 'value', parameters('moriLicenseIssuer')), createObject('name', 'ENTRA_GRAPH_DELEGATED_REDIRECT_URI', 'value', variables('moriEntraGraphDelegatedRedirectUri')), createObject('name', 'ENTRA_ID_CENTRAL_CALLBACK_URI', 'value', variables('moriEntraCentralCallbackUri')), createObject('name', 'DOCKER_ENABLE_CI', 'value', 'true'), createObject('name', 'DOCKER_REGISTRY_SERVER_URL', 'value', if(not(empty(parameters('acrLoginServer'))), format('https://{0}', parameters('acrLoginServer')), '')), createObject('name', 'ONBOARDING_CREATE_PIPELINE_CREDENTIALS', 'value', 'true'), createObject('name', 'BOOTSTRAP_APPKEYS', 'value', parameters('bootstrapAppKeys')), createObject('name', 'LICENSE_JWT', 'value', parameters('licenseJwt')), createObject('name', 'INSTALLATION_RESOURCE_MODEL', 'value', parameters('installationResourceModel')), createObject('name', 'LICENSE_EDITION', 'value', parameters('licenseEdition')), createObject('name', 'ONBOARDING_MODE', 'value', parameters('onboardingMode')), createObject('name', 'INSTALLATION_CAPACITY_PROFILE', 'value', parameters('capacityProfile'))), variables('registryCredentialAppSettings'))]",
3226
+ "appSettings": "[concat(createArray(createObject('name', 'WEBSITES_ENABLE_APP_SERVICE_STORAGE', 'value', 'false'), createObject('name', 'WEBSITES_CONTAINER_START_TIME_LIMIT', 'value', '1800'), createObject('name', 'DATABASE_URL', 'value', variables('databaseUrlAppSettingValue')), createObject('name', 'JWT_SECRET', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-secrets-jwtKeyVault)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'NODE_ENV', 'value', 'production'), createObject('name', 'LOCATION', 'value', parameters('location')), createObject('name', 'PORT', 'value', '3000'), createObject('name', 'WEBSITE_VNET_ROUTE_ALL', 'value', if(parameters('enablePostgresPublicAccess'), '0', '1')), createObject('name', 'WEBSITE_DNS_SERVER', 'value', '168.63.129.16'), createObject('name', 'DATABASELOG_URL', 'value', variables('databaseLogUrlAppSettingValue')), createObject('name', 'KEY_VAULT_ENABLED', 'value', 'true'), createObject('name', 'SECURITY_STORAGE_MODE', 'value', 'keyvault'), createObject('name', 'KEY_VAULT_URL', 'value', format('https://{0}.vault.azure.net/', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'ENCRYPTION_KEY', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-encryption-keyKeyVault)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'AUTO_CREATE_TABLES', 'value', if(parameters('autoCreateTables'), 'true', 'false')), createObject('name', 'KEYCLOAK_REALM', 'value', parameters('keycloakRealm')), createObject('name', 'KEYCLOAK_CLIENT_ID', 'value', parameters('keycloakClientId')), createObject('name', 'KEYCLOAK_ADMIN_USERNAME', 'value', parameters('keycloakAdminUsername')), createObject('name', 'KEYCLOAK_ADMIN_PASSWORD', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-keycloak-admin-password)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'ONBOARDING_ADMIN_PASSWORD', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-onboarding-admin-password)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'ONBOARDING_ADMIN_EMAIL', 'value', parameters('adminEmail')), createObject('name', 'KEYCLOAK_SERVER_URL', 'value', variables('keycloakPublicUrl')), createObject('name', 'KEYCLOAK_INTERNAL_SERVER_URL', 'value', variables('keycloakInternalUrl')), createObject('name', 'KEYCLOAK_TOKEN_TIMEOUT', 'value', '5000'), createObject('name', 'KEYCLOAK_VERIFY_AUDIENCE', 'value', if(parameters('keycloakVerifyAudience'), 'true', 'false')), createObject('name', 'DISABLE_RATE_LIMIT', 'value', variables('shouldDisableRateLimit')), createObject('name', 'SKIP_DB_REDEPLOY', 'value', 'true'), createObject('name', 'MISO_WEB_SERVER_URL', 'value', variables('misoPublicUrl')), createObject('name', 'MISO_CONTROLLER_URL', 'value', variables('misoInternalUrl')), createObject('name', 'MISO_RELATIVE_PATH', 'value', variables('misoVdirValue')), createObject('name', 'CONTROLLER_BASE_URL', 'value', if(not(empty(parameters('misoControllerUrl'))), parameters('misoControllerUrl'), format('https://{0}.azurewebsites.net', parameters('name')))), createObject('name', 'AZURE_SUBSCRIPTION_ID', 'value', subscription().subscriptionId), createObject('name', 'TENANT_ID', 'value', subscription().tenantId), createObject('name', 'DEPLOYMENT', 'value', 'azure'), createObject('name', 'AZURE_RESOURCE_GROUP_NAME', 'value', parameters('resourceGroupName')), createObject('name', 'INFRASTRUCTURE_NAME', 'value', parameters('prefix')), createObject('name', 'ENTRA_ID_TENANT_ID', 'value', parameters('entraIdTenantId')), createObject('name', 'MISO_CLIENTID', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-miso-clientIdKeyVault)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'MISO_CLIENTSECRET', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName=miso-controller-client-secretKeyVault)', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'MORI_BASE_URL', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName=mori-base-urlKeyVault)', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'MORI_LICENSE_PUBLIC_KEY', 'value', if(empty(parameters('moriLicensePublicKey')), '', format('@Microsoft.KeyVault(VaultName={0};SecretName=mori-license-public-keyKeyVault)', last(split(parameters('keyVaultId'), '/'))))), createObject('name', 'MORI_LICENSE_ISSUER', 'value', parameters('moriLicenseIssuer')), createObject('name', 'DOCKER_ENABLE_CI', 'value', 'true'), createObject('name', 'DOCKER_REGISTRY_SERVER_URL', 'value', if(not(empty(parameters('acrLoginServer'))), format('https://{0}', parameters('acrLoginServer')), '')), createObject('name', 'ONBOARDING_CREATE_PIPELINE_CREDENTIALS', 'value', 'true'), createObject('name', 'BOOTSTRAP_APPKEYS', 'value', parameters('bootstrapAppKeys')), createObject('name', 'LICENSE_JWT', 'value', parameters('licenseJwt')), createObject('name', 'INSTALLATION_RESOURCE_MODEL', 'value', parameters('installationResourceModel')), createObject('name', 'LICENSE_EDITION', 'value', parameters('licenseEdition')), createObject('name', 'ONBOARDING_MODE', 'value', parameters('onboardingMode')), createObject('name', 'INSTALLATION_CAPACITY_PROFILE', 'value', parameters('capacityProfile'))), variables('registryCredentialAppSettings'))]",
3272
3227
  "vnetRouteAllEnabled": "[not(parameters('enablePostgresPublicAccess'))]"
3273
3228
  },
3274
3229
  "vnetRouteAllEnabled": "[not(parameters('enablePostgresPublicAccess'))]"
@@ -3663,9 +3618,6 @@
3663
3618
  },
3664
3619
  "encryptionKey": {
3665
3620
  "value": "[reference(resourceId('Microsoft.Resources/deployments', 'bootstrapSecrets'), '2025-04-01').outputs.encryptionKey.value]"
3666
- },
3667
- "entraIdClientSecret": {
3668
- "value": "[parameters('entraIdClientSecret')]"
3669
3621
  }
3670
3622
  },
3671
3623
  "template": {
@@ -3675,7 +3627,7 @@
3675
3627
  "_generator": {
3676
3628
  "name": "bicep",
3677
3629
  "version": "0.47.16.16243",
3678
- "templateHash": "4650500285752133401"
3630
+ "templateHash": "6681447246114830480"
3679
3631
  }
3680
3632
  },
3681
3633
  "parameters": {
@@ -3744,13 +3696,6 @@
3744
3696
  "description": "MISO Client Secret (auto-generated if not provided)"
3745
3697
  }
3746
3698
  },
3747
- "entraIdClientSecret": {
3748
- "type": "securestring",
3749
- "defaultValue": "",
3750
- "metadata": {
3751
- "description": "Vendor Entra app client secret (ENTRA_ID_CLIENT_SECRET). Placeholder when empty — replace for live Entra flows."
3752
- }
3753
- },
3754
3699
  "moriLicensePublicKey": {
3755
3700
  "type": "string",
3756
3701
  "defaultValue": "",
@@ -3774,7 +3719,6 @@
3774
3719
  }
3775
3720
  },
3776
3721
  "variables": {
3777
- "resolvedEntraIdClientSecret": "[if(not(empty(parameters('entraIdClientSecret'))), parameters('entraIdClientSecret'), format('PLACEHOLDER_ENTRA_CLIENT_SECRET_MUST_BE_REPLACED_{0}', uniqueString(resourceGroup().id, parameters('prefix'))))]",
3778
3722
  "misoWebAppOrigin": "[if(endsWith(parameters('misoWebAppUrl'), '/'), substring(parameters('misoWebAppUrl'), 0, max(0, sub(length(parameters('misoWebAppUrl')), 1))), parameters('misoWebAppUrl'))]",
3779
3723
  "misoVdirSegment": "[if(or(equals(parameters('misoVdirPublic'), ''), equals(parameters('misoVdirPublic'), '/')), '', if(startsWith(parameters('misoVdirPublic'), '/'), parameters('misoVdirPublic'), format('/{0}', parameters('misoVdirPublic'))))]",
3780
3724
  "misoWebServerPublicUrl": "[if(empty(variables('misoVdirSegment')), variables('misoWebAppOrigin'), format('{0}{1}', variables('misoWebAppOrigin'), variables('misoVdirSegment')))]",
@@ -3865,14 +3809,6 @@
3865
3809
  "value": "[if(not(empty(parameters('misoClientSecret'))), parameters('misoClientSecret'), format('PLACEHOLDER_CLIENT_SECRET_MUST_BE_REPLACED_{0}', uniqueString(resourceGroup().id, parameters('prefix'))))]"
3866
3810
  }
3867
3811
  },
3868
- {
3869
- "type": "Microsoft.KeyVault/vaults/secrets",
3870
- "apiVersion": "2023-07-01",
3871
- "name": "[format('{0}/{1}', parameters('keyVaultName'), 'entra-client-secretKeyVault')]",
3872
- "properties": {
3873
- "value": "[variables('resolvedEntraIdClientSecret')]"
3874
- }
3875
- },
3876
3812
  {
3877
3813
  "condition": "[not(empty(parameters('moriLicensePublicKey')))]",
3878
3814
  "type": "Microsoft.KeyVault/vaults/secrets",