@aifabrix/builder 2.37.9 → 2.38.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 (45) hide show
  1. package/README.md +19 -0
  2. package/integration/hubspot/hubspot-deploy.json +1 -2
  3. package/lib/api/applications.api.js +23 -1
  4. package/lib/api/credentials.api.js +34 -0
  5. package/lib/api/deployments.api.js +27 -0
  6. package/lib/api/types/applications.types.js +1 -1
  7. package/lib/api/types/deployments.types.js +1 -1
  8. package/lib/api/types/pipeline.types.js +1 -1
  9. package/lib/api/wizard.api.js +21 -1
  10. package/lib/app/run-helpers.js +30 -2
  11. package/lib/cli/index.js +2 -0
  12. package/lib/cli/setup-app.js +32 -0
  13. package/lib/cli/setup-credential-deployment.js +72 -0
  14. package/lib/cli/setup-utility.js +1 -25
  15. package/lib/commands/app-down.js +80 -0
  16. package/lib/commands/app-logs.js +146 -0
  17. package/lib/commands/app.js +22 -0
  18. package/lib/commands/credential-list.js +104 -0
  19. package/lib/commands/deployment-list.js +184 -0
  20. package/lib/core/templates.js +2 -1
  21. package/lib/generator/builders.js +8 -3
  22. package/lib/generator/external-controller-manifest.js +5 -4
  23. package/lib/generator/index.js +16 -14
  24. package/lib/generator/split.js +1 -0
  25. package/lib/generator/wizard.js +4 -1
  26. package/lib/schema/application-schema.json +6 -2
  27. package/lib/schema/deployment-rules.yaml +121 -0
  28. package/lib/utils/app-run-containers.js +2 -1
  29. package/lib/utils/compose-generator.js +2 -1
  30. package/lib/utils/help-builder.js +0 -1
  31. package/lib/utils/image-version.js +209 -0
  32. package/lib/utils/schema-loader.js +1 -1
  33. package/lib/utils/variable-transformer.js +1 -19
  34. package/lib/validation/external-manifest-validator.js +1 -1
  35. package/package.json +1 -1
  36. package/templates/applications/README.md.hbs +1 -3
  37. package/templates/applications/dataplane/Dockerfile +2 -2
  38. package/templates/applications/dataplane/README.md +1 -3
  39. package/templates/applications/dataplane/variables.yaml +5 -3
  40. package/templates/applications/keycloak/Dockerfile +3 -3
  41. package/templates/applications/keycloak/README.md +14 -4
  42. package/templates/applications/keycloak/env.template +14 -2
  43. package/templates/applications/keycloak/variables.yaml +1 -1
  44. package/templates/applications/miso-controller/README.md +1 -3
  45. package/templates/applications/miso-controller/env.template +64 -11
@@ -0,0 +1,209 @@
1
+ /**
2
+ * AI Fabrix Builder - Image Version Resolution
3
+ *
4
+ * Resolves application version from Docker image (OCI label or semver tag).
5
+ * When template is empty or image version is greater, uses image version.
6
+ *
7
+ * @fileoverview Image version resolution utilities
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const { exec } = require('child_process');
13
+ const { promisify } = require('util');
14
+ const fs = require('fs').promises;
15
+ const fsSync = require('fs');
16
+ const path = require('path');
17
+ const yaml = require('js-yaml');
18
+ const { getBuilderPath } = require('./paths');
19
+ const composeGenerator = require('./compose-generator');
20
+ const containerHelpers = require('./app-run-containers');
21
+
22
+ const execAsync = promisify(exec);
23
+
24
+ const OCI_VERSION_LABEL = 'org.opencontainers.image.version';
25
+ const SEMVER_REGEX = /^v?(\d+\.\d+\.\d+)(?:-[-.\w]+)?(?:\+[-.\w]+)?$/i;
26
+
27
+ /**
28
+ * Gets version from Docker image via OCI label or semver tag
29
+ * @async
30
+ * @param {string} imageName - Image name (e.g. aifabrix/dataplane)
31
+ * @param {string} imageTag - Image tag (e.g. v1.0.0, latest)
32
+ * @returns {Promise<string|null>} Version string or null if not found
33
+ */
34
+ async function getVersionFromImage(imageName, imageTag) {
35
+ if (!imageName || typeof imageName !== 'string') {
36
+ return null;
37
+ }
38
+ const tag = imageTag || 'latest';
39
+ const fullImage = `${imageName}:${tag}`;
40
+
41
+ try {
42
+ const labelFormat = `{{index .Config.Labels "${OCI_VERSION_LABEL}"}}`;
43
+ const { stdout } = await execAsync(
44
+ `docker inspect --format '${labelFormat}' "${fullImage}" 2>/dev/null || true`,
45
+ { timeout: 10000 }
46
+ );
47
+ const labelValue = (stdout || '').trim();
48
+ if (labelValue && labelValue !== '<no value>') {
49
+ return labelValue;
50
+ }
51
+
52
+ const tagMatch = tag.match(SEMVER_REGEX);
53
+ if (tagMatch) {
54
+ return tagMatch[1];
55
+ }
56
+ return null;
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Compares two semantic versions
64
+ * @param {string} a - First version
65
+ * @param {string} b - Second version
66
+ * @returns {number} -1 if a < b, 0 if a === b, 1 if a > b
67
+ */
68
+ function compareSemver(a, b) {
69
+ if (!a || !b) {
70
+ return 0;
71
+ }
72
+ const parse = (v) => {
73
+ const m = String(v).match(SEMVER_REGEX);
74
+ if (!m) return null;
75
+ const parts = m[1].split('.').map(Number);
76
+ return { major: parts[0] || 0, minor: parts[1] || 0, patch: parts[2] || 0 };
77
+ };
78
+ const pa = parse(a);
79
+ const pb = parse(b);
80
+ if (!pa || !pb) return 0;
81
+ if (pa.major !== pb.major) return pa.major > pb.major ? 1 : -1;
82
+ if (pa.minor !== pb.minor) return pa.minor > pb.minor ? 1 : -1;
83
+ if (pa.patch !== pb.patch) return pa.patch > pb.patch ? 1 : -1;
84
+ return 0;
85
+ }
86
+
87
+ /**
88
+ * Resolves version for external app (app.version or externalIntegration.version)
89
+ * @param {Object} variables - Parsed variables.yaml
90
+ * @returns {string}
91
+ */
92
+ function resolveExternalVersion(variables) {
93
+ const version =
94
+ variables.app?.version ||
95
+ variables.externalIntegration?.version ||
96
+ '1.0.0';
97
+ return String(version).trim() || '1.0.0';
98
+ }
99
+
100
+ /**
101
+ * Resolves version for regular app when image exists
102
+ * @async
103
+ * @param {string} imageName - Image name
104
+ * @param {string} imageTag - Image tag
105
+ * @param {string} templateVersion - Template version (may be empty)
106
+ * @returns {Promise<{ version: string, fromImage: boolean }>}
107
+ */
108
+ async function resolveRegularVersion(imageName, imageTag, templateVersion) {
109
+ const templateEmpty =
110
+ templateVersion === undefined ||
111
+ templateVersion === null ||
112
+ String(templateVersion).trim() === '';
113
+ const templateStr = String(templateVersion || '').trim();
114
+
115
+ const imageVersion = await getVersionFromImage(imageName, imageTag);
116
+ const useImage =
117
+ imageVersion &&
118
+ (templateEmpty || compareSemver(imageVersion, templateStr) >= 0);
119
+
120
+ const version = useImage ? imageVersion : (templateEmpty ? '1.0.0' : templateStr);
121
+ return { version, fromImage: Boolean(useImage) };
122
+ }
123
+
124
+ /**
125
+ * Resolves version for an app: from image when image exists and template empty or smaller
126
+ * @async
127
+ * @param {string} appName - Application name
128
+ * @param {Object} variables - Parsed variables.yaml
129
+ * @param {Object} [options] - Options
130
+ * @param {boolean} [options.updateBuilder] - When true, update builder/variables.yaml if fromImage
131
+ * @param {string} [options.builderPath] - Builder path (defaults to getBuilderPath(appName))
132
+ * @returns {Promise<{ version: string, fromImage: boolean, updated: boolean }>}
133
+ */
134
+ async function resolveVersionForApp(appName, variables, options = {}) {
135
+ if (!appName || typeof appName !== 'string') {
136
+ return { version: '1.0.0', fromImage: false, updated: false };
137
+ }
138
+
139
+ if (variables?.externalIntegration) {
140
+ const version = resolveExternalVersion(variables);
141
+ return { version, fromImage: false, updated: false };
142
+ }
143
+
144
+ const imageName = composeGenerator.getImageName(variables, appName);
145
+ const imageTag = variables?.image?.tag || 'latest';
146
+ const imageExists = await containerHelpers.checkImageExists(imageName, imageTag);
147
+
148
+ if (!imageExists) {
149
+ const templateVersion = variables?.app?.version;
150
+ const templateEmpty =
151
+ templateVersion === undefined ||
152
+ templateVersion === null ||
153
+ String(templateVersion).trim() === '';
154
+ const fallback = templateEmpty ? '1.0.0' : String(templateVersion).trim();
155
+ return { version: fallback, fromImage: false, updated: false };
156
+ }
157
+
158
+ const { version, fromImage } = await resolveRegularVersion(
159
+ imageName,
160
+ imageTag,
161
+ variables?.app?.version
162
+ );
163
+
164
+ let updated = false;
165
+ if (fromImage && options.updateBuilder) {
166
+ const builderPath = options.builderPath || getBuilderPath(appName);
167
+ updated = await updateAppVersionInVariablesYaml(builderPath, version);
168
+ }
169
+
170
+ return { version, fromImage, updated };
171
+ }
172
+
173
+ /**
174
+ * Updates app.version in builder variables.yaml
175
+ * @async
176
+ * @param {string} builderPath - Path to builder app directory
177
+ * @param {string} version - Version to set
178
+ * @returns {Promise<boolean>} True if file was updated
179
+ */
180
+ async function updateAppVersionInVariablesYaml(builderPath, version) {
181
+ if (!builderPath || !version || typeof version !== 'string') {
182
+ return false;
183
+ }
184
+ const variablesPath = path.join(builderPath, 'variables.yaml');
185
+ if (!fsSync.existsSync(variablesPath)) {
186
+ return false;
187
+ }
188
+
189
+ try {
190
+ const content = await fs.readFile(variablesPath, 'utf8');
191
+ const parsed = yaml.load(content) || {};
192
+ if (!parsed.app) {
193
+ parsed.app = {};
194
+ }
195
+ parsed.app.version = version;
196
+ const dumped = yaml.dump(parsed, { lineWidth: -1 });
197
+ await fs.writeFile(variablesPath, dumped, { mode: 0o644, encoding: 'utf8' });
198
+ return true;
199
+ } catch {
200
+ return false;
201
+ }
202
+ }
203
+
204
+ module.exports = {
205
+ getVersionFromImage,
206
+ compareSemver,
207
+ resolveVersionForApp,
208
+ updateAppVersionInVariablesYaml
209
+ };
@@ -199,7 +199,7 @@ function detectFromDatasourceFields(parsed) {
199
199
  * @returns {string|null} Schema type or null if not detected
200
200
  */
201
201
  function detectFromApplicationFields(parsed) {
202
- if (parsed.deploymentKey || (parsed.image && parsed.registryMode && parsed.port)) {
202
+ if (parsed.image && parsed.registryMode && parsed.port) {
203
203
  return 'application';
204
204
  }
205
205
  return null;
@@ -96,20 +96,6 @@ function handlePartialAuthentication(authentication) {
96
96
  return auth;
97
97
  }
98
98
 
99
- /**
100
- * Adds placeholder deployment key if missing
101
- * @function addPlaceholderDeploymentKey
102
- * @param {Object} result - Result object
103
- * @returns {Object} Result object with deployment key
104
- */
105
- function addPlaceholderDeploymentKey(result) {
106
- if (!result.deploymentKey) {
107
- // This is a 64-character hex string matching the SHA256 pattern
108
- result.deploymentKey = '0000000000000000000000000000000000000000000000000000000000000000';
109
- }
110
- return result;
111
- }
112
-
113
99
  /**
114
100
  * Transforms flat structure to schema-compatible format
115
101
  * @function transformFlatStructure
@@ -126,9 +112,6 @@ function transformFlatStructure(variables, appName) {
126
112
  result.authentication = handlePartialAuthentication(result.authentication);
127
113
  }
128
114
 
129
- // Add placeholder deploymentKey for validation
130
- addPlaceholderDeploymentKey(result);
131
-
132
115
  return result;
133
116
  }
134
117
 
@@ -379,8 +362,7 @@ function transformVariablesForValidation(variables, appName) {
379
362
 
380
363
  // Nested structure - transform it
381
364
  const transformed = buildBaseTransformedStructure(variables, appName);
382
- const result = transformOptionalFields(variables, transformed);
383
- return addPlaceholderDeploymentKey(result);
365
+ return transformOptionalFields(variables, transformed);
384
366
  }
385
367
 
386
368
  module.exports = {
@@ -148,7 +148,7 @@ function validateConditionalRequirements(manifest, errors, warnings) {
148
148
  * @returns {void}
149
149
  */
150
150
  function validateRequiredFields(manifest, errors) {
151
- const requiredFields = ['key', 'displayName', 'description', 'type', 'deploymentKey'];
151
+ const requiredFields = ['key', 'displayName', 'description', 'type'];
152
152
  requiredFields.forEach(field => {
153
153
  if (!manifest[field]) {
154
154
  errors.push(`Required field "${field}" is missing`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.37.9",
3
+ "version": "2.38.0",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -92,8 +92,7 @@ aifabrix dockerfile {{appName}} --force # Generate Dockerfile
92
92
  aifabrix resolve {{appName}} # Generate .env file
93
93
 
94
94
  # Deployment
95
- aifabrix json {{appName}} # Preview deployment JSON
96
- aifabrix genkey {{appName}} # Generate deployment key
95
+ aifabrix json {{appName}} # Generate deployment manifest
97
96
  aifabrix push {{appName}} --registry {{registry}} # Push to ACR
98
97
  aifabrix deploy {{appName}} # Deploy (controller/env from config)
99
98
 
@@ -176,7 +175,6 @@ Controller URL and environment (for `deploy`, `app register`, etc.) are set via
176
175
  ```bash
177
176
  aifabrix resolve {{appName}} --force
178
177
  aifabrix json {{appName}}
179
- aifabrix genkey {{appName}}
180
178
  ```
181
179
 
182
180
  ---
@@ -9,8 +9,8 @@ FROM aifabrix/dataplane:latest
9
9
  EXPOSE 3001
10
10
 
11
11
  # Health check (documentation; base image may already set this)
12
- HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
13
- CMD curl -f http://localhost:3001/health || exit 1
12
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
13
+ CMD wget --no-verbose --tries=1 --spider http://localhost:3001/health || exit 1
14
14
 
15
15
  # CMD inherited from base image; override only if needed
16
16
  # CMD inherited: uvicorn app.main:app --host 0.0.0.0 --port 3001
@@ -103,8 +103,7 @@ aifabrix dockerfile dataplane --force # Generate Dockerfile
103
103
  aifabrix resolve dataplane # Generate .env file
104
104
 
105
105
  # Deployment
106
- aifabrix json dataplane # Preview deployment JSON
107
- aifabrix genkey dataplane # Generate deployment key
106
+ aifabrix json dataplane # Generate deployment manifest
108
107
  aifabrix push dataplane --registry myacr.azurecr.io # Push to ACR
109
108
  aifabrix deploy dataplane --controller <url> # Deploy to Azure
110
109
 
@@ -184,7 +183,6 @@ export AIFABRIX_SECRETS=/path/to/secrets.yaml
184
183
  ```bash
185
184
  aifabrix resolve dataplane --force
186
185
  aifabrix json dataplane
187
- aifabrix genkey dataplane
188
186
  ```
189
187
 
190
188
  ---
@@ -45,11 +45,13 @@ authentication:
45
45
  requiredRoles: ["aifabrix-user"]
46
46
 
47
47
  # Build Configuration
48
+ # Dataplane builds from published image; context is project root (like miso-controller)
48
49
  build:
49
- context: ../.. # Relative to builder/dataplane/ config location (goes to app root)
50
- dockerfile: builder/dataplane/Dockerfile
50
+ context: ../.. # Docker build context (relative to builder/dataplane/)
51
+ dockerfile: builder/dataplane/Dockerfile # Dockerfile path (relative to project root)
51
52
  envOutputPath: ../../.env # Copy to repo root for local dev
52
- localPort: 3011 # Port for local development (different from Docker port)
53
+ localPort: 3011 # Port for local development (different from Docker port)
54
+ language: python # Runtime language for template selection (typescript or python)
53
55
 
54
56
  # =============================================================================
55
57
  # Portal Input Configuration (Deployment Wizard)
@@ -1,7 +1,7 @@
1
1
  # Keycloak Identity and Access Management with Custom Themes
2
- # This Dockerfile extends Keycloak 26.4 with custom themes
2
+ # This Dockerfile extends Keycloak 26.5.2 with custom themes
3
3
 
4
- FROM quay.io/keycloak/keycloak:26.4
4
+ FROM quay.io/keycloak/keycloak:26.5.2
5
5
 
6
6
  # Set working directory
7
7
  WORKDIR /opt/keycloak
@@ -34,4 +34,4 @@ EXPOSE 8080
34
34
 
35
35
  # Default Keycloak command (can be overridden in docker-compose.yaml)
36
36
  # Health checks are enabled via the build step above
37
- CMD ["start-dev"]
37
+ CMD ["start-dev"]
@@ -40,6 +40,18 @@ aifabrix run keycloak
40
40
 
41
41
  **Access your app:** <http://dev.aifabrix:8082>
42
42
 
43
+ **Token issuer (Docker + refresh):** Keycloak is configured with `KC_HOSTNAME=localhost` and `KC_HOSTNAME_PORT=${KEYCLOAK_PUBLIC_PORT}` so tokens always have issuer `http://localhost:<port>/realms/aifabrix`. This lets refresh work when users log in via localhost and the controller (in Docker) calls Keycloak at `http://keycloak:8080`.
44
+
45
+ **If you get "Invalid token issuer. Expected 'http://keycloak:8080/realms/aifabrix'" on refresh:**
46
+
47
+ 1. Set `KEYCLOAK_PUBLIC_PORT` to the port you use for Keycloak (e.g. if your token issuer shows `http://localhost:8682/realms/aifabrix`, use `8682`). In `.env` (in the directory where you run `aifabrix resolve keycloak`) add or set: `KEYCLOAK_PUBLIC_PORT=8682`.
48
+ 2. Regenerate Keycloak env and restart Keycloak:
49
+ ```bash
50
+ aifabrix resolve keycloak
51
+ docker restart $(docker ps -q -f name=keycloak)
52
+ ```
53
+ 3. Re-run `pnpm validate:config -- --test-refresh` from the repo root.
54
+
43
55
  **View logs:**
44
56
 
45
57
  ```bash
@@ -93,8 +105,7 @@ aifabrix dockerfile keycloak --force # Generate Dockerfile
93
105
  aifabrix resolve keycloak # Generate .env file
94
106
 
95
107
  # Deployment
96
- aifabrix json keycloak # Preview deployment JSON
97
- aifabrix genkey keycloak # Generate deployment key
108
+ aifabrix json keycloak # Generate deployment manifest
98
109
  aifabrix push keycloak --registry myacr.azurecr.io # Push to ACR
99
110
  aifabrix deploy keycloak --controller <url> # Deploy to Azure
100
111
 
@@ -128,7 +139,7 @@ aifabrix run keycloak --debug # Debug output
128
139
 
129
140
  ```bash
130
141
  aifabrix push keycloak --registry myacr.azurecr.io --tag v1.0.0
131
- aifabrix push keycloak --registry myacr.azurecr.io --tag "v1.0.0,latest,stable"
142
+ aifabrix push keycloak --registry myacr.azurecr.io --tag "v1.0.0,latest"
132
143
  ```
133
144
 
134
145
  ### Deploy Options
@@ -174,7 +185,6 @@ export AIFABRIX_SECRETS=/path/to/secrets.yaml
174
185
  ```bash
175
186
  aifabrix resolve keycloak --force
176
187
  aifabrix json keycloak
177
- aifabrix genkey keycloak
178
188
  ```
179
189
 
180
190
  ---
@@ -11,6 +11,20 @@ KEYCLOAK_ADMIN_PASSWORD=kv://keycloak-admin-passwordKeyVault
11
11
  KC_HOSTNAME_STRICT=false
12
12
  KC_HTTP_ENABLED=true
13
13
 
14
+ # =============================================================================
15
+ # HOSTNAME / ISSUER (Docker vs localhost)
16
+ # =============================================================================
17
+ # Flow: Client gets callback from Keycloak (public URL) → server does auth code
18
+ # exchange by calling Keycloak on the INTERNAL address (keycloak:8080). Keycloak
19
+ # must use the PUBLIC URL as issuer in all tokens so they match what the
20
+ # controller expects (KEYCLOAK_SERVER_URL).
21
+ # - Users log in via http://localhost:${KEYCLOAK_PUBLIC_PORT} (browser/CLI)
22
+ # - Server calls Keycloak at http://keycloak:8080 for token exchange and refresh
23
+ # Without KC_HOSTNAME/KC_HOSTNAME_PORT, Keycloak would put issuer keycloak:8080
24
+ # in tokens when the server calls internally, and validation would fail.
25
+ KC_HOSTNAME=localhost
26
+ KC_HOSTNAME_PORT=${KEYCLOAK_PUBLIC_PORT}
27
+
14
28
  # =============================================================================
15
29
  # HEALTH CHECK CONFIGURATION
16
30
  # =============================================================================
@@ -30,8 +44,6 @@ KC_HTTP_MANAGEMENT_HEALTH_ENABLED=false
30
44
  # MISO Application Client Credentials (per application)
31
45
  MISO_CLIENTID=kv://keycloak-client-idKeyVault
32
46
  MISO_CLIENTSECRET=kv://keycloak-client-secretKeyVault
33
- MISO_CONTROLLER_URL=http://${MISO_HOST}:${MISO_PORT}
34
- MISO_WEB_SERVER_URL=kv://miso-controller-web-server-url
35
47
 
36
48
  # Connects to postgres service in Docker network (postgres) or localhost (local)
37
49
 
@@ -12,7 +12,7 @@ image:
12
12
  registry: devflowiseacr.azurecr.io
13
13
  registryMode: acr
14
14
 
15
- # Port Configuration
15
+ # Port Configuration (base for host; host port = 8082 + developer_id*100 from ~/.aifabrix/config.yaml)
16
16
  port: 8082
17
17
 
18
18
  # Azure Requirements
@@ -93,8 +93,7 @@ aifabrix dockerfile miso-controller --force # Generate Dockerfile
93
93
  aifabrix resolve miso-controller # Generate .env file
94
94
 
95
95
  # Deployment
96
- aifabrix json miso-controller # Preview deployment JSON
97
- aifabrix genkey miso-controller # Generate deployment key
96
+ aifabrix json miso-controller # Generate deployment manifest
98
97
  aifabrix push miso-controller --registry myacr.azurecr.io # Push to ACR
99
98
  aifabrix deploy miso-controller --controller <url> # Deploy to Azure
100
99
 
@@ -348,7 +347,6 @@ Failed to getSecret postgres-adminPassword: Secret not found
348
347
  ```bash
349
348
  aifabrix resolve miso-controller --force
350
349
  aifabrix json miso-controller
351
- aifabrix genkey miso-controller
352
350
  ```
353
351
 
354
352
  ---
@@ -79,13 +79,23 @@ REDIS_ROLES_TTL=900
79
79
  REDIS_PERMISSIONS_TTL=900
80
80
 
81
81
  # =============================================================================
82
- # KEYCLOAK CONFIGURATION
82
+ # KEYCLOAK CONFIGURATION (single canonical block)
83
83
  # =============================================================================
84
- # Connects to external keycloak from aifabrix-setup
84
+ # Connects to external Keycloak from aifabrix-setup.
85
+ #
86
+ # URL semantics:
87
+ # KEYCLOAK_SERVER_URL = Public URL (browser, issuer, OAuth callbacks)
88
+ # KEYCLOAK_INTERNAL_SERVER_URL = Internal URL (server-to-Keycloak HTTP only, e.g. Docker)
89
+ #
90
+ # Usage: Env vars are used for bootstrap/onboarding only. Runtime config comes from DB
91
+ # (KeycloakConfiguration + Application url/internalUrl). Sync env->DB at startup via
92
+ # sync-application-urls-from-env.service.
93
+ #
94
+ # NOTE: Do NOT onboard Azure Entra SSO in Keycloak during onboarding (skipAzureEntraSso=true).
85
95
 
86
96
  KEYCLOAK_REALM=aifabrix
87
97
  KEYCLOAK_SERVER_URL=kv://keycloak-server-urlKeyVault
88
- KEYCLOAK_PUBLIC_SERVER_URL=kv://keycloak-public-server-urlKeyVault
98
+ KEYCLOAK_INTERNAL_SERVER_URL=kv://keycloak-internal-server-urlKeyVault
89
99
  KEYCLOAK_CLIENT_ID=miso-controller
90
100
  KEYCLOAK_CLIENT_SECRET=kv://keycloak-client-secretKeyVault
91
101
  KEYCLOAK_ADMIN_USERNAME=admin
@@ -120,9 +130,9 @@ AZURE_CLIENT_SECRET=kv://azure-client-secretKeyVault
120
130
  # =============================================================================
121
131
  # DEPLOYMENT TYPE CONFIGURATION
122
132
  # =============================================================================
123
- # Controls deployment behavior for Azure, mock Azure, or local Docker deployments
133
+ # Controls deployment behavior for Azure, mock Azure, local Docker, or database-only deployments
124
134
  #
125
- # DEPLOYMENT types: azure, azure-mock, local
135
+ # DEPLOYMENT types: azure, azure-mock, local, database
126
136
  #
127
137
  # -----------------------------------------------------------------------------
128
138
  # DEPLOYMENT=azure: Real Azure Operations (Production Mode)
@@ -171,6 +181,41 @@ AZURE_CLIENT_SECRET=kv://azure-client-secretKeyVault
171
181
  # - Local PostgreSQL container for database (if required)
172
182
  #
173
183
  # -----------------------------------------------------------------------------
184
+ # DEPLOYMENT=database: Database-Only Mode (DB Updates Only)
185
+ # -----------------------------------------------------------------------------
186
+ # - Performs only database operations (deployment/job/application records, RBAC sync)
187
+ # - Does NOT run Docker containers (no container startup, health checks, port mapping)
188
+ # - Does NOT use Azure SDK (no Azure SDK initialization overhead)
189
+ # - Fastest mode for CI/CD pipelines that only need DB state validation
190
+ # - Use for: Testing DB workflows, validating RBAC sync, testing deployment records
191
+ #
192
+ # Example use cases:
193
+ # - CI/CD pipelines that validate deployment workflow without containers
194
+ # - Testing RBAC sync logic without Docker/Azure dependencies
195
+ # - Validating database schema changes and migrations
196
+ # - Testing deployment job creation and status updates
197
+ # - Fast validation of DB state changes
198
+ #
199
+ # What happens:
200
+ # - Creates deployment job in database
201
+ # - Updates deployment status (DEPLOYING → COMPLETED)
202
+ # - Updates application record (status, version, repository URL)
203
+ # - Syncs roles and permissions (RBAC)
204
+ # - Marks deployment as completed
205
+ # - Cleans up expired tokens
206
+ # - Invalidates role/permission caches
207
+ #
208
+ # What does NOT happen:
209
+ # - No Docker container execution
210
+ # - No Azure SDK initialization
211
+ # - No container URLs or health check URLs
212
+ #
213
+ # Requirements:
214
+ # - Database connection (DATABASE_URL)
215
+ # - No Docker required
216
+ # - No Azure credentials required
217
+ #
218
+ # -----------------------------------------------------------------------------
174
219
  # Configuration Notes
175
220
  # -----------------------------------------------------------------------------
176
221
  # Default: azure (Real Azure) if not set or invalid value
@@ -181,13 +226,15 @@ AZURE_CLIENT_SECRET=kv://azure-client-secretKeyVault
181
226
  # - Production: DEPLOYMENT=azure
182
227
  # - Staging: DEPLOYMENT=azure
183
228
  # - Local Development: DEPLOYMENT=azure-mock or DEPLOYMENT=local
184
- # - CI/CD Testing: DEPLOYMENT=azure-mock
229
+ # - CI/CD Testing: DEPLOYMENT=azure-mock or DEPLOYMENT=database
185
230
  # - Local Docker development: DEPLOYMENT=local
231
+ # - DB-only validation/testing: DEPLOYMENT=database
186
232
  #
187
233
  # When to use each mode:
188
234
  # - Need to deploy to actual Azure resources? → DEPLOYMENT=azure
189
235
  # - Need to test deployment logic without creating resources? → DEPLOYMENT=azure-mock
190
236
  # - Want to run applications locally in Docker? → DEPLOYMENT=local
237
+ # - Only need DB updates and RBAC sync (no containers/Azure)? → DEPLOYMENT=database
191
238
  #
192
239
  DEPLOYMENT=local
193
240
 
@@ -212,16 +259,13 @@ API_KEY=kv://miso-controller-api-key-secretKeyVault
212
259
  # =============================================================================
213
260
  # MISO CONTROLLER CONFIGURATION
214
261
  # =============================================================================
215
-
216
- # MISO Controller URL
217
- MISO_CONTROLLER_URL=http://${MISO_HOST}:${MISO_PORT}
218
-
219
262
  # Web Server URL (for OpenAPI documentation server URLs and Keycloak callbacks)
220
263
  # This is the PUBLIC-FACING URL that browsers/users access (e.g., http://localhost:3100)
221
264
  # Used to generate correct server URLs in OpenAPI spec and Keycloak callback URLs
222
265
  # For Docker: use localhost with mapped port (e.g., localhost:3100)
223
266
  # For production: use public domain (e.g., https://miso.example.com)
224
- MISO_WEB_SERVER_URL=http://localhost:${MISO_PUBLIC_PORT}
267
+ MISO_WEB_SERVER_URL=kv://miso-controller-web-server-urlKeyVault
268
+ MISO_CONTROLLER_URL=kv://miso-controller-internal-server-urlKeyVault
225
269
 
226
270
  # MISO Environment Configuration (miso, dev, tst, pro)
227
271
  MISO_ENVIRONMENT=miso
@@ -234,12 +278,21 @@ MISO_CLIENTSECRET=kv://miso-controller-client-secretKeyVault
234
278
  # Use wildcards for ports: http://localhost:*
235
279
  MISO_ALLOWED_ORIGINS=http://localhost:*
236
280
 
281
+ # =============================================================================
282
+ # LICENSE CONFIGURATION
283
+ # =============================================================================
284
+ # Temporary development bypass: set LICENSE_JWT=DEVELOPMENT to skip Mori validation.
285
+ # Will be replaced by JWT license validation (see plan 131-jwt_license_offline_validation).
286
+ LICENSE_JWT=DEVELOPMENT
287
+
237
288
  # =============================================================================
238
289
  # MORI SERVICE CONFIGURATION
239
290
  # =============================================================================
240
291
 
241
292
  MORI_BASE_URL=kv://mori-controller-url
242
293
  MORI_API_KEY=kv://mori-controller-api-keyKeyVault
294
+ MORI_USERNAME=kv://mori-controller-basic-usernameKeyVault
295
+ MORI_PASSWORD=kv://mori-controller-basic-passwordKeyVault
243
296
 
244
297
  # =============================================================================
245
298
  # LOGGING CONFIGURATION