@aifabrix/builder 2.57.0 → 2.58.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 (33) hide show
  1. package/lib/app/push.js +98 -41
  2. package/lib/build/standard-docker-build.js +13 -11
  3. package/lib/cli/setup-app.js +16 -5
  4. package/lib/cli/setup-dev-path-commands.js +49 -2
  5. package/lib/cli/setup-environment.js +17 -4
  6. package/lib/commands/dev-registry.js +155 -0
  7. package/lib/commands/dev-show-display.js +10 -2
  8. package/lib/commands/setup-image-refresh.js +26 -0
  9. package/lib/core/config-attach-extensions.js +4 -1
  10. package/lib/deployment/installation/azure-infra-stage.js +40 -9
  11. package/lib/deployment/installation/azure-preflight.js +53 -8
  12. package/lib/deployment/installation/azure-readiness.js +35 -5
  13. package/lib/deployment/installation/infra-catalog.js +50 -4
  14. package/lib/deployment/installation/infra-manifest.js +17 -2
  15. package/lib/deployment/installation/infra-projection.js +14 -3
  16. package/lib/deployment/installation/registry-auth-mode.js +98 -0
  17. package/lib/deployment/installation/stage-input.js +21 -6
  18. package/lib/generator/builders.js +8 -9
  19. package/lib/schema/infrastructure-schema.json +55 -2
  20. package/lib/utils/app-register-config.js +2 -4
  21. package/lib/utils/build-resolve-image.js +2 -21
  22. package/lib/utils/cli-utils.js +8 -1
  23. package/lib/utils/config-registry-preference.js +106 -0
  24. package/lib/utils/docker-build.js +53 -19
  25. package/lib/utils/env-map.js +23 -6
  26. package/lib/utils/image-tags.js +88 -0
  27. package/lib/utils/promote-dev-docker-image.js +6 -0
  28. package/lib/utils/registry-credentials.js +236 -0
  29. package/lib/utils/resolve-docker-image-ref.js +78 -9
  30. package/lib/utils/secrets-helpers.js +9 -1
  31. package/lib/utils/variable-transformer.js +2 -4
  32. package/package.json +2 -1
  33. package/templates/marketplace/main.json +2 -2
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Host-scoped container registry credentials in the primary user secrets file.
3
+ *
4
+ * Keys are exactly `<registry>-username` and `<registry>-password` in the user's
5
+ * `secrets.local.yaml` (never the shared store). Values are encrypted through the existing
6
+ * local-secret writer and reach Docker only via `docker login --password-stdin`.
7
+ *
8
+ * @fileoverview Registry credential storage, lookup, removal, and per-command Docker login
9
+ * @author AI Fabrix Team
10
+ * @version 1.0.0
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const fs = require('fs');
16
+ const yaml = require('js-yaml');
17
+ const config = require('../core/config');
18
+ const pathsUtil = require('./paths');
19
+ const { loadExistingSecrets, mergeSecretsIntoFile } = require('./secrets-generator');
20
+ const { saveLocalSecret } = require('./local-secrets');
21
+ const { decryptSecret, isEncrypted } = require('./secrets-encryption');
22
+ const { normalizeRegistryHost } = require('./config-registry-preference');
23
+
24
+ /** Hosts already authenticated during this CLI process. */
25
+ const authenticatedHosts = new Set();
26
+
27
+ /**
28
+ * @param {string} registry - Registry host
29
+ * @returns {{ host: string, usernameKey: string, passwordKey: string }}
30
+ */
31
+ function registryCredentialKeys(registry) {
32
+ const host = normalizeRegistryHost(registry);
33
+ return { host, usernameKey: `${host}-username`, passwordKey: `${host}-password` };
34
+ }
35
+
36
+ function hasValue(value) {
37
+ return typeof value === 'string' ? value.trim() !== '' : value !== undefined && value !== null;
38
+ }
39
+
40
+ function readRawPair(keys) {
41
+ const file = pathsUtil.getPrimaryUserSecretsLocalPath();
42
+ const raw = loadExistingSecrets(file);
43
+ return { file, username: raw[keys.usernameKey], password: raw[keys.passwordKey] };
44
+ }
45
+
46
+ function partialPairError(keys) {
47
+ return new Error(
48
+ `Registry credentials for ${keys.host} are incomplete: both ${keys.usernameKey} and ${keys.passwordKey} ` +
49
+ `must be set. Run "aifabrix dev set-registry ${keys.host} --username <username> --password-stdin" ` +
50
+ `or "aifabrix dev set-registry ${keys.host} --clear-credentials".`
51
+ );
52
+ }
53
+
54
+ /**
55
+ * Credential presence for a registry without decrypting anything.
56
+ * @param {string} registry - Registry host
57
+ * @returns {'complete'|'absent'|'partial'}
58
+ */
59
+ function getRegistryCredentialStatus(registry) {
60
+ const keys = registryCredentialKeys(registry);
61
+ const pair = readRawPair(keys);
62
+ const count = [pair.username, pair.password].filter(hasValue).length;
63
+ if (count === 2) return 'complete';
64
+ return count === 0 ? 'absent' : 'partial';
65
+ }
66
+
67
+ async function decryptStoredValue(value, label) {
68
+ const text = String(value);
69
+ if (!isEncrypted(text)) return text;
70
+ const key = await config.getSecretsEncryptionKey();
71
+ if (!key) {
72
+ throw new Error(`Cannot decrypt the stored ${label}: the local secrets encryption key is not configured`);
73
+ }
74
+ return decryptSecret(text, key);
75
+ }
76
+
77
+ /**
78
+ * Loads a complete credential pair for the registry.
79
+ * @param {string} registry - Registry host
80
+ * @returns {Promise<{ username: string, password: string }|null>} null when no pair is stored
81
+ * @throws {Error} When only one of the two keys is stored
82
+ */
83
+ async function loadRegistryCredentials(registry) {
84
+ const keys = registryCredentialKeys(registry);
85
+ const pair = readRawPair(keys);
86
+ const present = [pair.username, pair.password].filter(hasValue).length;
87
+ if (present === 0) return null;
88
+ if (present === 1) throw partialPairError(keys);
89
+ return {
90
+ username: await decryptStoredValue(pair.username, `${keys.host} username`),
91
+ password: await decryptStoredValue(pair.password, `${keys.host} password`)
92
+ };
93
+ }
94
+
95
+ function removeKeysFromFile(file, keyNames) {
96
+ if (!fs.existsSync(file)) return false;
97
+ const data = loadExistingSecrets(file);
98
+ const present = keyNames.filter((key) => Object.prototype.hasOwnProperty.call(data, key));
99
+ if (present.length === 0) return false;
100
+ for (const key of present) delete data[key];
101
+ const { assertWritableSecretsPathForTests } = require('./aifabrix-test-runtime-guard');
102
+ assertWritableSecretsPathForTests(file);
103
+ fs.writeFileSync(file, yaml.dump(data, { indent: 2, lineWidth: -1, noRefs: true, sortKeys: false }), { mode: 0o600 });
104
+ // `mode` applies only on creation; keep an existing file owner-only too.
105
+ fs.chmodSync(file, 0o600);
106
+ return true;
107
+ }
108
+
109
+ function restorePair(keys, snapshot) {
110
+ const restore = {};
111
+ const remove = [];
112
+ for (const [key, value] of [[keys.usernameKey, snapshot.username], [keys.passwordKey, snapshot.password]]) {
113
+ if (value === undefined) remove.push(key);
114
+ else restore[key] = value;
115
+ }
116
+ removeKeysFromFile(snapshot.file, remove);
117
+ mergeSecretsIntoFile(snapshot.file, restore);
118
+ }
119
+
120
+ /**
121
+ * Saves a complete pair. On any write failure the previous entries are restored.
122
+ * @param {string} registry - Registry host
123
+ * @param {string} username - Registry username
124
+ * @param {string} password - Registry password or token
125
+ * @returns {Promise<{ host: string, rollback: Function }>} rollback restores the prior entries
126
+ */
127
+ async function saveRegistryCredentials(registry, username, password) {
128
+ const keys = registryCredentialKeys(registry);
129
+ if (!hasValue(username) || !hasValue(password)) {
130
+ throw new Error('Registry username and password must both be non-empty');
131
+ }
132
+ const snapshot = readRawPair(keys);
133
+ await config.ensureSecretsEncryptionKey();
134
+ try {
135
+ await saveLocalSecret(keys.usernameKey, String(username));
136
+ await saveLocalSecret(keys.passwordKey, String(password));
137
+ } catch (error) {
138
+ restorePair(keys, snapshot);
139
+ throw new Error(`Could not save registry credentials for ${keys.host}; previous values were restored (${error.message})`);
140
+ }
141
+ authenticatedHosts.delete(keys.host);
142
+ return { host: keys.host, rollback: () => restorePair(keys, snapshot) };
143
+ }
144
+
145
+ /**
146
+ * Removes both credential keys for the registry (idempotent).
147
+ * @param {string} registry - Registry host
148
+ * @returns {boolean} true when at least one key was removed
149
+ */
150
+ function removeRegistryCredentials(registry) {
151
+ const keys = registryCredentialKeys(registry);
152
+ authenticatedHosts.delete(keys.host);
153
+ return removeKeysFromFile(pathsUtil.getPrimaryUserSecretsLocalPath(), [keys.usernameKey, keys.passwordKey]);
154
+ }
155
+
156
+ /**
157
+ * Authenticates Docker to the registry once per process: stored pair first, then the optional
158
+ * fallback (for example Azure CLI for ACR). The password is sent only on stdin.
159
+ * @param {string} registry - Registry host
160
+ * @param {Object} [options] - Options
161
+ * @param {Function} [options.fallback] - async (host) => void when no pair is stored
162
+ * @param {Function} [options.login] - async (host, username, password) => void (tests)
163
+ * @returns {Promise<'cached'|'credentials'|'fallback'|'none'>}
164
+ */
165
+ async function ensureRegistryLogin(registry, options = {}) {
166
+ const host = normalizeRegistryHost(registry);
167
+ if (authenticatedHosts.has(host)) return 'cached';
168
+ const credentials = await loadRegistryCredentials(host);
169
+ if (credentials) {
170
+ const login = options.login || require('../deployment/push').authenticateExternalRegistry;
171
+ await login(host, credentials.username, credentials.password);
172
+ authenticatedHosts.add(host);
173
+ return 'credentials';
174
+ }
175
+ if (typeof options.fallback === 'function') {
176
+ await options.fallback(host);
177
+ authenticatedHosts.add(host);
178
+ return 'fallback';
179
+ }
180
+ return 'none';
181
+ }
182
+
183
+ /**
184
+ * Registry host of an image reference, or '' for Docker Hub style refs.
185
+ * @param {string} imageRef - Image reference (`host/repo:tag`, `repo:tag`, ...)
186
+ * @returns {string}
187
+ */
188
+ function registryHostFromImageRef(imageRef) {
189
+ const ref = String(imageRef || '').trim();
190
+ const slash = ref.indexOf('/');
191
+ if (slash <= 0) return '';
192
+ const first = ref.slice(0, slash);
193
+ if (!(first.includes('.') || first.includes(':') || first === 'localhost')) return '';
194
+ try {
195
+ return normalizeRegistryHost(first);
196
+ } catch {
197
+ return '';
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Logs in once per distinct registry host of the given image refs when a stored pair exists.
203
+ * Hosts without stored credentials are pulled anonymously (or with the user's own Docker login).
204
+ * @param {string[]} imageRefs - Image references
205
+ * @returns {Promise<string[]>} Hosts logged in with stored credentials
206
+ */
207
+ async function ensureLoginForImageRefs(imageRefs) {
208
+ const hosts = [...new Set((imageRefs || []).map(registryHostFromImageRef).filter(Boolean))];
209
+ const used = [];
210
+ for (const host of hosts) {
211
+ const outcome = await ensureRegistryLogin(host);
212
+ if (outcome === 'credentials') used.push(host);
213
+ }
214
+ return used;
215
+ }
216
+
217
+ /**
218
+ * Forgets cached logins (used before re-authentication and in tests).
219
+ * @param {string} [registry] - Host to forget; all when omitted
220
+ */
221
+ function resetRegistryLoginCache(registry) {
222
+ if (registry) authenticatedHosts.delete(normalizeRegistryHost(registry));
223
+ else authenticatedHosts.clear();
224
+ }
225
+
226
+ module.exports = {
227
+ registryCredentialKeys,
228
+ getRegistryCredentialStatus,
229
+ loadRegistryCredentials,
230
+ saveRegistryCredentials,
231
+ removeRegistryCredentials,
232
+ ensureRegistryLogin,
233
+ ensureLoginForImageRefs,
234
+ registryHostFromImageRef,
235
+ resetRegistryLoginCache
236
+ };
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Resolve Docker repository path and tag from application config and optional CLI overrides.
3
- * Precedence: --image (full ref), --registry CLI, image.registry in manifest, else unqualified name.
3
+ * Precedence: --image (full ref), --registry CLI, installation `registry` in config.yaml,
4
+ * image.registry in manifest, else unqualified name.
4
5
  *
5
6
  * For refs like localhost:5000/repo without an explicit tag, prefer --image with :tag (parse ambiguity).
6
7
  *
@@ -12,6 +13,7 @@
12
13
  'use strict';
13
14
 
14
15
  const { parseImageOverride } = require('./parse-image-ref');
16
+ const { readInstallationRegistrySync } = require('./config-registry-preference');
15
17
  const DOCKER_REPOSITORY_RE = /^[a-z0-9][a-z0-9._/-]*$/i;
16
18
  const DOCKER_TAG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
17
19
 
@@ -92,6 +94,73 @@ function normalizeDockerRegistryPrefix(registry) {
92
94
  return t.slice(0, end);
93
95
  }
94
96
 
97
+ /**
98
+ * Remove a registry host prefix from a Docker repository path.
99
+ * The first segment is a host when it contains `.` or `:` or equals `localhost`.
100
+ * @param {string} repository
101
+ * @returns {string}
102
+ */
103
+ function stripRegistryHostFromRepository(repository) {
104
+ if (!repository || typeof repository !== 'string') {
105
+ return repository;
106
+ }
107
+ const parts = repository.split('/');
108
+ if (parts.length <= 1) {
109
+ return repository;
110
+ }
111
+ const host = parts[0];
112
+ if (host.includes('.') || host.includes(':') || host === 'localhost') {
113
+ return parts.slice(1).join('/');
114
+ }
115
+ return repository;
116
+ }
117
+
118
+ /**
119
+ * Effective registry for an application image.
120
+ * Precedence: `options.registry` (CLI) > installation `registry` > manifest `image.registry`.
121
+ * @param {Object} [appConfig] - Application manifest
122
+ * @param {Object} [options] - `registry` (CLI) and optional `installationRegistry` (preloaded; '' = none)
123
+ * @returns {{ registry: string, source: 'cli'|'installation'|'manifest'|'none' }}
124
+ */
125
+ function resolveEffectiveRegistry(appConfig, options = {}) {
126
+ const opts = options || {};
127
+ const cli = normalizeDockerRegistryPrefix(opts.registry);
128
+ if (cli) return { registry: cli, source: 'cli' };
129
+ const installation = opts.installationRegistry !== undefined
130
+ ? normalizeDockerRegistryPrefix(opts.installationRegistry)
131
+ : readInstallationRegistrySync();
132
+ if (installation) return { registry: installation, source: 'installation' };
133
+ const manifest = normalizeDockerRegistryPrefix(appConfig?.image?.registry ?? '');
134
+ if (manifest) return { registry: manifest, source: 'manifest' };
135
+ return { registry: '', source: 'none' };
136
+ }
137
+
138
+ /**
139
+ * Prefix a repository with a registry exactly once (replacing any existing host).
140
+ * @param {string} registry - Registry prefix ('' keeps the repository unchanged)
141
+ * @param {string} repository - Repository path, possibly already qualified
142
+ * @returns {string}
143
+ */
144
+ function qualifyRepository(registry, repository) {
145
+ const prefix = normalizeDockerRegistryPrefix(registry);
146
+ if (!prefix) return repository;
147
+ return `${prefix}/${stripRegistryHostFromRepository(repository)}`;
148
+ }
149
+
150
+ /**
151
+ * Deployment/registration image reference (`registry/name:tag`) using the shared precedence.
152
+ * @param {Object} variables - Application manifest
153
+ * @param {string} fallbackName - Name when the manifest has no image name or app key
154
+ * @param {Object} [options] - Same options as {@link resolveEffectiveRegistry}
155
+ * @returns {string}
156
+ */
157
+ function buildManifestImageReference(variables, fallbackName, options = {}) {
158
+ const imageName = variables?.image?.name || variables?.app?.key || fallbackName;
159
+ const tag = variables?.image?.tag || 'latest';
160
+ const { registry } = resolveEffectiveRegistry(variables, options);
161
+ return `${qualifyRepository(registry, imageName)}:${tag}`;
162
+ }
163
+
95
164
  /**
96
165
  * Effective image repository (may include registry prefix) and tag for Docker.
97
166
  * @param {string} appName - Application name
@@ -118,13 +187,8 @@ function resolveDockerImageRef(appName, appConfig, runOptions = {}) {
118
187
 
119
188
  const baseRepo = assertSafeRepository(getRepositoryPathFromConfig(appConfig, appName));
120
189
  const imageTag = assertSafeTag(tagFromOpts || imageTagFromConfig(appConfig));
121
- const prefix =
122
- normalizeDockerRegistryPrefix(opts.registry) ||
123
- normalizeDockerRegistryPrefix(appConfig?.image?.registry ?? '');
124
- if (prefix) {
125
- return { imageName: assertSafeRepository(`${prefix}/${baseRepo}`), imageTag };
126
- }
127
- return { imageName: baseRepo, imageTag };
190
+ const { registry } = resolveEffectiveRegistry(appConfig, opts);
191
+ return { imageName: assertSafeRepository(qualifyRepository(registry, baseRepo)), imageTag };
128
192
  }
129
193
 
130
194
  /**
@@ -137,7 +201,7 @@ function resolveDockerImageRef(appName, appConfig, runOptions = {}) {
137
201
  function resolveComposeImageOverrideString(appName, appConfig, options = {}) {
138
202
  if (options.image) return options.image;
139
203
  if (options.imageOverride) return options.imageOverride;
140
- const runOpts = { registry: options.registry, image: undefined };
204
+ const runOpts = { registry: options.registry, installationRegistry: options.installationRegistry, image: undefined };
141
205
  if (options.tag) {
142
206
  const { imageName } = resolveDockerImageRef(appName, appConfig, runOpts);
143
207
  return `${imageName}:${options.tag}`;
@@ -154,6 +218,11 @@ function resolveComposeImageOverrideString(appName, appConfig, options = {}) {
154
218
  module.exports = {
155
219
  resolveDockerImageRef,
156
220
  resolveComposeImageOverrideString,
221
+ resolveEffectiveRegistry,
222
+ qualifyRepository,
223
+ buildManifestImageReference,
224
+ stripRegistryHostFromRepository,
225
+ assertSafeTag,
157
226
  normalizeDockerRegistryPrefix,
158
227
  getRepositoryPathFromConfig,
159
228
  imageTagFromConfig
@@ -29,7 +29,15 @@ const { collectUniqueKvPathStrings, isCommentOrEmptyLine, activeEnvLineForKvScan
29
29
  * @returns {string} Interpolated content
30
30
  */
31
31
  function interpolateEnvVars(content, envVars) {
32
- return content.replace(/\$\{([A-Z_]+)\}/g, (match, envVar) => {
32
+ const hasOwn = (key) => Object.prototype.hasOwnProperty.call(envVars, key);
33
+ const interpolated = content.replace(
34
+ /\$\{DEV_USERNAME\}\.\$\{REMOTE_HOST\}/g,
35
+ (match) => {
36
+ if (!hasOwn('DEV_USERNAME') || !hasOwn('REMOTE_HOST')) return match;
37
+ return [envVars.DEV_USERNAME, envVars.REMOTE_HOST].filter(Boolean).join('.');
38
+ }
39
+ );
40
+ return interpolated.replace(/\$\{([A-Z_]+)\}/g, (match, envVar) => {
33
41
  return envVars[envVar] || match;
34
42
  });
35
43
  }
@@ -126,10 +126,8 @@ function transformFlatStructure(variables, appName) {
126
126
  * @returns {string} Image reference string
127
127
  */
128
128
  function buildImageReference(variables, appName) {
129
- const imageName = variables.image?.name || variables.app?.key || appName;
130
- const registry = variables.image?.registry;
131
- const tag = variables.image?.tag || 'latest';
132
- return registry ? `${registry}/${imageName}:${tag}` : `${imageName}:${tag}`;
129
+ const { buildManifestImageReference } = require('./resolve-docker-image-ref');
130
+ return buildManifestImageReference(variables, appName);
133
131
  }
134
132
 
135
133
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.57.0",
3
+ "version": "2.58.0",
4
4
  "description": "AI Fabrix Builder — CLI and developer scripts (pnpm + af)",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -28,6 +28,7 @@
28
28
  "agent-skills:check": "node scripts/pnpm/agent-skills-sync.mjs --check",
29
29
  "agent-skills:sync:codex": "node scripts/pnpm/agent-skills-sync.mjs --codex-home",
30
30
  "agent-skills:test": "node --test scripts/pnpm/agent-skills-sync.test.mjs",
31
+ "dev-deploy:test": "node --test scripts/pnpm/dev-deploy.test.mjs",
31
32
  "check": "npm run lint && npm run test",
32
33
  "lint": "eslint . --ext .js",
33
34
  "lint:fix": "eslint . --ext .js --fix",
@@ -2379,7 +2379,7 @@
2379
2379
  "virtualNetworkSubnetId": "[if(not(empty(parameters('vnetSubnetId'))), parameters('vnetSubnetId'), null())]",
2380
2380
  "siteConfig": {
2381
2381
  "linuxFxVersion": "[format('DOCKER|{0}', parameters('image'))]",
2382
- "acrUseManagedIdentityCreds": true,
2382
+ "acrUseManagedIdentityCreds": "[not(variables('registryPasswordConfigured'))]",
2383
2383
  "appCommandLine": "start --optimized=false",
2384
2384
  "healthCheckPath": "[variables('keycloakHealthCheckPath')]",
2385
2385
  "alwaysOn": true,
@@ -3083,7 +3083,7 @@
3083
3083
  "virtualNetworkSubnetId": "[if(not(empty(parameters('vnetSubnetId'))), parameters('vnetSubnetId'), null())]",
3084
3084
  "siteConfig": {
3085
3085
  "linuxFxVersion": "[format('DOCKER|{0}', parameters('image'))]",
3086
- "acrUseManagedIdentityCreds": true,
3086
+ "acrUseManagedIdentityCreds": "[not(parameters('registryPasswordConfigured'))]",
3087
3087
  "healthCheckPath": "/health",
3088
3088
  "alwaysOn": true,
3089
3089
  "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', 'CORS_ORIGINS', 'value', format('https://{0}.azurewebsites.net', parameters('name'))), 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', 'KEYCLOAK_SERVER_URL', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName=keycloak-web-server-url)', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'KEYCLOAK_INTERNAL_SERVER_URL', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName=keycloak-internal-server-url)', last(split(parameters('keyVaultId'), '/')))), 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', format('@Microsoft.KeyVault(VaultName={0};SecretName=miso-controller-web-server-url)', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'MISO_CONTROLLER_URL', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName=miso-controller-internal-server-urlKeyVault)', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'MISO_RELATIVE_PATH', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName=miso-controller-vdir-public)', last(split(parameters('keyVaultId'), '/')))), 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', 'AZURE_RESOURCE_GROUP_NAME', 'value', parameters('resourceGroupName')), createObject('name', 'INFRASTRUCTURE_NAME', 'value', parameters('prefix')), createObject('name', 'ENTRA_ID_CLIENT_ID', 'value', parameters('entraIdClientId')), createObject('name', 'ENTRA_ID_CLIENT_SECRET', 'value', parameters('entraIdClientSecret')), 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', variables('moriBaseNormalized')), createObject('name', 'MORI_CALLBACK_API_KEY', 'value', parameters('moriCallbackApiKey')), createObject('name', 'ENTRA_GRAPH_DELEGATED_REDIRECT_URI', 'value', variables('moriEntraGraphDelegatedRedirectUri')), createObject('name', 'ENTRA_ID_CENTRAL_CALLBACK_URI', 'value', variables('moriEntraCentralCallbackUri')), createObject('name', 'ENTRA_CALLBACK_STATE_SECRET', 'value', parameters('entraCallbackStateSecret')), 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', 'LICENSE_JWT', 'value', parameters('licenseJwt')), createObject('name', 'LICENSE_EDITION', 'value', parameters('licenseEdition')), createObject('name', 'ONBOARDING_MODE', 'value', parameters('onboardingMode'))), variables('registryCredentialAppSettings'))]",