@aifabrix/builder 2.33.0 → 2.33.3

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 (50) hide show
  1. package/README.md +13 -0
  2. package/integration/hubspot/README.md +7 -7
  3. package/lib/api/index.js +6 -2
  4. package/lib/app/deploy-config.js +161 -0
  5. package/lib/app/deploy.js +28 -153
  6. package/lib/app/register.js +6 -5
  7. package/lib/app/run-helpers.js +23 -17
  8. package/lib/cli.js +31 -1
  9. package/lib/commands/logout.js +3 -4
  10. package/lib/commands/up-common.js +72 -0
  11. package/lib/commands/up-dataplane.js +109 -0
  12. package/lib/commands/up-miso.js +134 -0
  13. package/lib/core/config.js +32 -9
  14. package/lib/core/secrets-docker-env.js +88 -0
  15. package/lib/core/secrets.js +142 -115
  16. package/lib/datasource/deploy.js +31 -3
  17. package/lib/datasource/list.js +102 -15
  18. package/lib/infrastructure/helpers.js +82 -1
  19. package/lib/infrastructure/index.js +2 -0
  20. package/lib/schema/env-config.yaml +7 -0
  21. package/lib/utils/api.js +70 -2
  22. package/lib/utils/compose-generator.js +13 -13
  23. package/lib/utils/config-paths.js +13 -0
  24. package/lib/utils/device-code.js +2 -2
  25. package/lib/utils/env-endpoints.js +2 -5
  26. package/lib/utils/env-map.js +4 -5
  27. package/lib/utils/error-formatters/network-errors.js +13 -3
  28. package/lib/utils/parse-image-ref.js +27 -0
  29. package/lib/utils/paths.js +28 -4
  30. package/lib/utils/secrets-generator.js +34 -12
  31. package/lib/utils/secrets-helpers.js +1 -2
  32. package/lib/utils/token-manager-refresh.js +5 -0
  33. package/package.json +1 -1
  34. package/templates/applications/dataplane/Dockerfile +16 -0
  35. package/templates/applications/dataplane/README.md +205 -0
  36. package/templates/applications/dataplane/env.template +143 -0
  37. package/templates/applications/dataplane/rbac.yaml +283 -0
  38. package/templates/applications/dataplane/variables.yaml +143 -0
  39. package/templates/applications/keycloak/Dockerfile +1 -1
  40. package/templates/applications/keycloak/README.md +193 -0
  41. package/templates/applications/keycloak/variables.yaml +5 -6
  42. package/templates/applications/miso-controller/Dockerfile +8 -8
  43. package/templates/applications/miso-controller/README.md +369 -0
  44. package/templates/applications/miso-controller/env.template +114 -6
  45. package/templates/applications/miso-controller/rbac.yaml +74 -0
  46. package/templates/applications/miso-controller/variables.yaml +93 -5
  47. package/templates/github/ci.yaml.hbs +44 -1
  48. package/templates/github/release.yaml.hbs +44 -0
  49. package/templates/infra/compose.yaml.hbs +2 -1
  50. package/templates/applications/miso-controller/test.yaml +0 -1
@@ -18,6 +18,9 @@ environments:
18
18
  MISO_PORT: 3000 # Internal port (container-to-container). MISO_PUBLIC_PORT calculated automatically.
19
19
  KEYCLOAK_HOST: keycloak
20
20
  KEYCLOAK_PORT: 8082 # Internal port (container-to-container). KEYCLOAK_PUBLIC_PORT calculated automatically.
21
+ KEYCLOAK_PUBLIC_PORT: 8082
22
+ DATAPLANE_HOST: dataplane
23
+ DATAPLANE_PORT: 3001
21
24
  NODE_ENV: production
22
25
  PYTHONUNBUFFERED: 1
23
26
  PYTHONDONTWRITEBYTECODE: 1
@@ -30,8 +33,12 @@ environments:
30
33
  REDIS_PORT: 6379
31
34
  MISO_HOST: localhost
32
35
  MISO_PORT: 3010
36
+ MISO_PUBLIC_PORT: 3010
33
37
  KEYCLOAK_HOST: localhost
34
38
  KEYCLOAK_PORT: 8082
39
+ KEYCLOAK_PUBLIC_PORT: 8082
40
+ DATAPLANE_HOST: localhost
41
+ DATAPLANE_PORT: 3011
35
42
  NODE_ENV: development
36
43
  PYTHONUNBUFFERED: 1
37
44
  PYTHONDONTWRITEBYTECODE: 1
package/lib/utils/api.js CHANGED
@@ -131,6 +131,32 @@ async function handleSuccessResponse(response, url, options, duration) {
131
131
  return { success: true, data: text, status: response.status };
132
132
  }
133
133
 
134
+ /**
135
+ * Validates that a URL is not empty or missing
136
+ * @function validateUrl
137
+ * @param {string} url - URL to validate
138
+ * @param {string} [urlType='URL'] - Type of URL for error message (e.g., 'Dataplane URL', 'Controller URL')
139
+ * @returns {void}
140
+ * @throws {Error} If URL is empty, null, undefined, whitespace-only, or malformed
141
+ */
142
+ function validateUrl(url, urlType = 'URL') {
143
+ if (!url || typeof url !== 'string') {
144
+ throw new Error(`${urlType} is required and must be a string (received: ${JSON.stringify(url)})`);
145
+ }
146
+ const trimmedUrl = url.trim();
147
+ if (!trimmedUrl) {
148
+ throw new Error(`${urlType} cannot be empty. Please provide a valid URL.`);
149
+ }
150
+ // Check for common invalid URL patterns
151
+ if (trimmedUrl === 'undefined' || trimmedUrl === 'null' || trimmedUrl === 'NaN') {
152
+ throw new Error(`${urlType} is invalid: "${trimmedUrl}". Please provide a valid URL.`);
153
+ }
154
+ // Basic URL format validation - must start with http:// or https://
155
+ if (!trimmedUrl.match(/^https?:\/\//i)) {
156
+ throw new Error(`${urlType} must be a valid HTTP/HTTPS URL (received: "${trimmedUrl}")`);
157
+ }
158
+ }
159
+
134
160
  /**
135
161
  * Handles network error from API call
136
162
  * @async
@@ -142,7 +168,34 @@ async function handleSuccessResponse(response, url, options, duration) {
142
168
  * @returns {Promise<Object>} Error response object
143
169
  */
144
170
  async function handleNetworkError(error, url, options, duration) {
145
- const parsedError = parseErrorResponse(error.message, 0, true);
171
+ // Enhance error message with URL information if URL is missing or invalid
172
+ let errorMessage = error.message;
173
+ if (errorMessage && (errorMessage.includes('cannot be empty') || errorMessage.includes('is required'))) {
174
+ // Add URL context to validation errors
175
+ if (!url || !url.trim()) {
176
+ errorMessage = `${errorMessage} (URL was: ${JSON.stringify(url)})`;
177
+ } else {
178
+ errorMessage = `${errorMessage} (URL was: ${url})`;
179
+ }
180
+ } else if (!url || !url.trim()) {
181
+ // If URL is empty but error doesn't mention it, add context
182
+ errorMessage = `Invalid or missing URL. ${errorMessage} (URL was: ${JSON.stringify(url)})`;
183
+ }
184
+
185
+ const parsedError = parseErrorResponse(errorMessage, 0, true);
186
+
187
+ // Extract controller URL from full URL for error data
188
+ let controllerUrl = null;
189
+ const endpointUrl = url;
190
+ if (url && typeof url === 'string' && url.trim()) {
191
+ try {
192
+ const urlObj = new URL(url);
193
+ controllerUrl = `${urlObj.protocol}//${urlObj.host}`;
194
+ } catch {
195
+ // If URL parsing fails, use the full URL as endpoint
196
+ controllerUrl = null;
197
+ }
198
+ }
146
199
 
147
200
  await logApiPerformance({
148
201
  url,
@@ -157,10 +210,17 @@ async function handleNetworkError(error, url, options, duration) {
157
210
  }
158
211
  });
159
212
 
213
+ // Include both controller URL and full endpoint URL in error data
214
+ const errorData = {
215
+ ...parsedError.data,
216
+ controllerUrl: controllerUrl,
217
+ endpointUrl: endpointUrl
218
+ };
219
+
160
220
  return {
161
221
  success: false,
162
222
  error: parsedError.message,
163
- errorData: parsedError.data,
223
+ errorData: errorData,
164
224
  errorType: parsedError.type,
165
225
  formattedError: parsedError.formatted,
166
226
  network: true
@@ -175,6 +235,14 @@ async function handleNetworkError(error, url, options, duration) {
175
235
  * @returns {Promise<Object>} Response object with success flag
176
236
  */
177
237
  async function makeApiCall(url, options = {}) {
238
+ // Validate URL before attempting request
239
+ try {
240
+ validateUrl(url, 'API endpoint URL');
241
+ } catch (error) {
242
+ const duration = 0;
243
+ return await handleNetworkError(error, url || '', options, duration);
244
+ }
245
+
178
246
  const startTime = Date.now();
179
247
  const fetchOptions = { ...options };
180
248
  if (!fetchOptions.signal) {
@@ -17,6 +17,7 @@ const config = require('../core/config');
17
17
  const buildCopy = require('./build-copy');
18
18
  const { formatMissingDbPasswordError } = require('./error-formatter');
19
19
  const { getContainerPort } = require('./port-resolver');
20
+ const { parseImageOverride } = require('./parse-image-ref');
20
21
 
21
22
  // Register commonly used helpers
22
23
  handlebars.registerHelper('eq', (a, b) => a === b);
@@ -129,9 +130,14 @@ function buildAppConfig(appName, config) {
129
130
  * Builds image configuration section
130
131
  * @param {Object} config - Application configuration
131
132
  * @param {string} appName - Application name
133
+ * @param {string} [imageOverride] - Optional full image reference (registry/name:tag) to use instead of config
132
134
  * @returns {Object} Image configuration
133
135
  */
134
- function buildImageConfig(config, appName) {
136
+ function buildImageConfig(config, appName, imageOverride) {
137
+ const parsed = imageOverride ? parseImageOverride(imageOverride) : null;
138
+ if (parsed) {
139
+ return { name: parsed.name, tag: parsed.tag };
140
+ }
135
141
  const imageName = getImageName(config, appName);
136
142
  const imageTag = config.image?.tag || 'latest';
137
143
  return {
@@ -237,14 +243,15 @@ function buildRequiresConfig(config) {
237
243
  * @param {Object} config - Application configuration
238
244
  * @param {number} port - Application port
239
245
  * @param {string|number} devId - Developer ID
246
+ * @param {string} [imageOverride] - Optional full image reference for run (e.g. from --image)
240
247
  * @returns {Object} Service configuration
241
248
  */
242
- function buildServiceConfig(appName, config, port, devId) {
249
+ function buildServiceConfig(appName, config, port, devId, imageOverride) {
243
250
  const containerPortValue = getContainerPort(config, 3000);
244
251
  const hostPort = port;
245
252
  return {
246
253
  app: buildAppConfig(appName, config),
247
- image: buildImageConfig(config, appName),
254
+ image: buildImageConfig(config, appName, imageOverride),
248
255
  port: containerPortValue, // Container port (for health check and template)
249
256
  containerPort: containerPortValue, // Container port (always set, equals containerPort if exists, else port)
250
257
  hostPort: hostPort, // Host port (options.port if provided, else config.port)
@@ -275,14 +282,7 @@ function buildNetworksConfig(config) {
275
282
  return { databases: config.requires?.databases || config.databases || [] };
276
283
  }
277
284
 
278
- /**
279
- * Reads and parses .env file
280
- * @async
281
- * @function readEnvFile
282
- * @param {string} envPath - Path to .env file
283
- * @returns {Promise<Object>} Object with environment variables
284
- * @throws {Error} If file not found or read fails
285
- */
285
+ /** Reads and parses .env file. @param {string} envPath - Path to .env file. @returns {Promise<Object>} env vars. @throws {Error} If file not found. */
286
286
  async function readEnvFile(envPath) {
287
287
  if (!fsSync.existsSync(envPath)) {
288
288
  throw new Error(`.env file not found: ${envPath}`);
@@ -458,9 +458,10 @@ async function generateDockerCompose(appName, appConfig, options) {
458
458
  const language = appConfig.build?.language || appConfig.language || 'typescript';
459
459
  const template = loadDockerComposeTemplate(language);
460
460
  const port = options.port || appConfig.port || 3000;
461
+ const imageOverride = options.image || options.imageOverride;
461
462
  const { devId, idNum } = await getDeveloperIdAndNumeric();
462
463
  const { networkName, containerName } = buildNetworkAndContainerNames(appName, devId, idNum);
463
- const serviceConfig = buildServiceConfig(appName, appConfig, port, devId);
464
+ const serviceConfig = buildServiceConfig(appName, appConfig, port, devId, imageOverride);
464
465
  const volumesConfig = buildVolumesConfig(appName);
465
466
  const networksConfig = buildNetworksConfig(appConfig);
466
467
 
@@ -495,4 +496,3 @@ module.exports = {
495
496
  buildTraefikConfig,
496
497
  buildDevUsername
497
498
  };
498
-
@@ -8,6 +8,8 @@
8
8
  * @version 2.0.0
9
9
  */
10
10
 
11
+ const path = require('path');
12
+
11
13
  /**
12
14
  * Get path configuration value
13
15
  * @async
@@ -102,6 +104,17 @@ function createPathConfigFunctions(getConfigFn, saveConfigFn) {
102
104
  */
103
105
  async setAifabrixEnvConfigPath(envConfigPath) {
104
106
  await setPathConfig(getConfigFn, saveConfigFn, 'aifabrix-env-config', envConfigPath, 'Env config path is required and must be a string');
107
+ },
108
+
109
+ /**
110
+ * Get builder root directory (dirname of aifabrix-env-config when set).
111
+ * When set, app dirs and generated .env use this instead of cwd/builder.
112
+ * @async
113
+ * @returns {Promise<string|null>} Builder root path or null to use cwd/builder
114
+ */
115
+ async getAifabrixBuilderDir() {
116
+ const envConfigPath = await getPathConfig(getConfigFn, 'aifabrix-env-config');
117
+ return envConfigPath && typeof envConfigPath === 'string' ? path.dirname(envConfigPath) : null;
105
118
  }
106
119
  };
107
120
  }
@@ -469,12 +469,13 @@ async function refreshDeviceToken(controllerUrl, refreshToken) {
469
469
  }
470
470
 
471
471
  const url = `${controllerUrl}/api/v1/auth/login/device/refresh`;
472
+ // Send both refresh_token (OAuth2 RFC 6749 / Keycloak) and refreshToken (camelCase) so controller accepts either
472
473
  const response = await getMakeApiCall()(url, {
473
474
  method: 'POST',
474
475
  headers: {
475
476
  'Content-Type': 'application/json'
476
477
  },
477
- body: JSON.stringify({ refreshToken })
478
+ body: JSON.stringify({ refresh_token: refreshToken, refreshToken })
478
479
  });
479
480
 
480
481
  if (!response.success) {
@@ -490,7 +491,6 @@ async function refreshDeviceToken(controllerUrl, refreshToken) {
490
491
 
491
492
  return tokenResponse;
492
493
  }
493
-
494
494
  module.exports = {
495
495
  initiateDeviceCodeFlow,
496
496
  pollDeviceCodeToken,
@@ -9,7 +9,6 @@
9
9
  'use strict';
10
10
 
11
11
  const fs = require('fs');
12
- const path = require('path');
13
12
  const yaml = require('js-yaml');
14
13
  const config = require('../core/config');
15
14
  const devConfig = require('../utils/dev-config');
@@ -49,8 +48,7 @@ function splitHost(value) {
49
48
  function getLocalhostOverride(context) {
50
49
  if (context !== 'local') return null;
51
50
  try {
52
- const os = require('os');
53
- const cfgPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
51
+ const cfgPath = config.CONFIG_FILE;
54
52
  if (fs.existsSync(cfgPath)) {
55
53
  const cfgContent = fs.readFileSync(cfgPath, 'utf8');
56
54
  const cfg = yaml.load(cfgContent) || {};
@@ -310,8 +308,7 @@ async function rewriteInfraEndpoints(envContent, context, devPorts, adjustedHost
310
308
 
311
309
  // Apply config.yaml → environments.{context} override (if exists)
312
310
  try {
313
- const os = require('os');
314
- const cfgPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
311
+ const cfgPath = config.CONFIG_FILE;
315
312
  if (fs.existsSync(cfgPath)) {
316
313
  const cfgContent = fs.readFileSync(cfgPath, 'utf8');
317
314
  const cfg = yaml.load(cfgContent) || {};
@@ -9,7 +9,6 @@
9
9
  'use strict';
10
10
 
11
11
  const fs = require('fs');
12
- const path = require('path');
13
12
  const yaml = require('js-yaml');
14
13
  const { loadEnvConfig } = require('./env-config-loader');
15
14
  const config = require('../core/config');
@@ -38,9 +37,9 @@ async function loadBaseVars(context) {
38
37
  * @param {Object} os - OS module instance
39
38
  * @returns {Object} Override environment variables
40
39
  */
41
- function loadOverrideVars(context, os) {
40
+ function loadOverrideVars(context, _os) {
42
41
  try {
43
- const cfgPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
42
+ const cfgPath = config.CONFIG_FILE;
44
43
  if (fs.existsSync(cfgPath)) {
45
44
  const cfgContent = fs.readFileSync(cfgPath, 'utf8');
46
45
  const cfg = yaml.load(cfgContent) || {};
@@ -60,9 +59,9 @@ function loadOverrideVars(context, os) {
60
59
  * @param {Object} os - OS module instance
61
60
  * @returns {string|null} Localhost override value or null
62
61
  */
63
- function getLocalhostOverride(os) {
62
+ function getLocalhostOverride(_os) {
64
63
  try {
65
- const cfgPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
64
+ const cfgPath = config.CONFIG_FILE;
66
65
  if (fs.existsSync(cfgPath)) {
67
66
  const cfgContent = fs.readFileSync(cfgPath, 'utf8');
68
67
  const cfg = yaml.load(cfgContent) || {};
@@ -40,7 +40,10 @@ function addControllerUrlHeader(lines, errorData) {
40
40
  * @param {Object} errorData - Error response data
41
41
  */
42
42
  function addControllerUrlToMessage(lines, errorData) {
43
- if (errorData && errorData.controllerUrl) {
43
+ // Prefer showing full endpoint URL if available
44
+ if (errorData && errorData.endpointUrl) {
45
+ lines.push(chalk.gray(`Endpoint URL: ${errorData.endpointUrl}`));
46
+ } else if (errorData && errorData.controllerUrl) {
44
47
  lines.push(chalk.gray(`Controller URL: ${errorData.controllerUrl}`));
45
48
  }
46
49
  }
@@ -77,8 +80,15 @@ function formatHostnameNotFoundError(lines, errorData) {
77
80
  */
78
81
  function formatTimeoutError(lines, errorData) {
79
82
  lines.push(chalk.yellow('Request timed out.'));
80
- addControllerUrlToMessage(lines, errorData);
81
- lines.push(chalk.gray('The controller may be overloaded.'));
83
+
84
+ // Show full endpoint URL if available, otherwise show controller URL
85
+ if (errorData && errorData.endpointUrl) {
86
+ lines.push(chalk.gray(`Endpoint URL: ${errorData.endpointUrl}`));
87
+ } else if (errorData && errorData.controllerUrl) {
88
+ lines.push(chalk.gray(`Controller URL: ${errorData.controllerUrl}`));
89
+ }
90
+
91
+ lines.push(chalk.gray('The endpoint may not exist, the controller may be overloaded, or there may be a network issue.'));
82
92
  }
83
93
 
84
94
  /**
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Parse full image reference (registry/name:tag or name:tag) into { name, tag }
3
+ *
4
+ * @fileoverview Image reference parsing for compose and run overrides
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ /**
10
+ * Parses full image reference (registry/name:tag or name:tag) into { name, tag }
11
+ * @param {string} imageRef - Full image reference (e.g. myreg/keycloak:v1 or keycloak:latest)
12
+ * @returns {{ name: string, tag: string }|null} Parsed name and tag, or null if invalid
13
+ */
14
+ function parseImageOverride(imageRef) {
15
+ if (!imageRef || typeof imageRef !== 'string') {
16
+ return null;
17
+ }
18
+ const lastColon = imageRef.lastIndexOf(':');
19
+ if (lastColon <= 0) {
20
+ return { name: imageRef.trim(), tag: 'latest' };
21
+ }
22
+ const name = imageRef.substring(0, lastColon).trim();
23
+ const tag = imageRef.substring(lastColon + 1).trim() || 'latest';
24
+ return { name, tag };
25
+ }
26
+
27
+ module.exports = { parseImageOverride };
@@ -30,18 +30,33 @@ function safeHomedir() {
30
30
  return process.env.HOME || process.env.USERPROFILE || '/';
31
31
  }
32
32
 
33
+ /**
34
+ * Returns the path to the config file (AIFABRIX_HOME env or ~/.aifabrix).
35
+ * Used so getAifabrixHome can read from the same location as config.js.
36
+ * @returns {string} Absolute path to config directory
37
+ */
38
+ function getConfigDirForPaths() {
39
+ if (process.env.AIFABRIX_HOME && typeof process.env.AIFABRIX_HOME === 'string') {
40
+ return path.resolve(process.env.AIFABRIX_HOME.trim());
41
+ }
42
+ return path.join(safeHomedir(), '.aifabrix');
43
+ }
44
+
33
45
  /**
34
46
  * Returns the base AI Fabrix directory.
35
- * Resolved from config.yaml `aifabrix-home` (stored under OS home).
36
- * Falls back to ~/.aifabrix when not specified.
47
+ * Priority: AIFABRIX_HOME env → config.yaml `aifabrix-home` (from AIFABRIX_HOME or ~/.aifabrix) → ~/.aifabrix.
37
48
  *
38
49
  * @returns {string} Absolute path to the AI Fabrix home directory
39
50
  */
40
51
  function getAifabrixHome() {
52
+ if (process.env.AIFABRIX_HOME && typeof process.env.AIFABRIX_HOME === 'string') {
53
+ return path.resolve(process.env.AIFABRIX_HOME.trim());
54
+ }
41
55
  const isTestEnv = process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined;
42
56
  if (!isTestEnv) {
43
57
  try {
44
- const configPath = path.join(safeHomedir(), '.aifabrix', 'config.yaml');
58
+ const configDir = getConfigDirForPaths();
59
+ const configPath = path.join(configDir, 'config.yaml');
45
60
  if (fs.existsSync(configPath)) {
46
61
  const content = fs.readFileSync(configPath, 'utf8');
47
62
  const config = yaml.load(content) || {};
@@ -278,7 +293,9 @@ function getIntegrationPath(appName) {
278
293
  }
279
294
 
280
295
  /**
281
- * Gets the builder folder path for regular applications
296
+ * Gets the builder folder path for regular applications.
297
+ * When AIFABRIX_BUILDER_DIR is set (e.g. by up-miso/up-dataplane from config aifabrix-env-config),
298
+ * uses that as builder root instead of cwd/builder.
282
299
  * @param {string} appName - Application name
283
300
  * @returns {string} Absolute path to builder directory
284
301
  */
@@ -286,6 +303,12 @@ function getBuilderPath(appName) {
286
303
  if (!appName || typeof appName !== 'string') {
287
304
  throw new Error('App name is required and must be a string');
288
305
  }
306
+ const builderRoot = process.env.AIFABRIX_BUILDER_DIR && typeof process.env.AIFABRIX_BUILDER_DIR === 'string'
307
+ ? process.env.AIFABRIX_BUILDER_DIR.trim()
308
+ : null;
309
+ if (builderRoot) {
310
+ return path.join(builderRoot, appName);
311
+ }
289
312
  return path.join(process.cwd(), 'builder', appName);
290
313
  }
291
314
 
@@ -442,6 +465,7 @@ async function detectAppType(appName, options = {}) {
442
465
 
443
466
  module.exports = {
444
467
  getAifabrixHome,
468
+ getConfigDirForPaths,
445
469
  getApplicationsBaseDir,
446
470
  getDevDirectory,
447
471
  getAppPath,
@@ -41,6 +41,36 @@ function findMissingSecretKeys(envTemplate, existingSecrets) {
41
41
  return missingKeys;
42
42
  }
43
43
 
44
+ /**
45
+ * Generate database password value for a key (databases-*-passwordKeyVault)
46
+ * @param {string} key - Secret key name
47
+ * @returns {string|null} Password string or null if key does not match
48
+ */
49
+ function generateDbPasswordValue(key) {
50
+ const dbPasswordMatch = key.match(/^databases-([a-z0-9-_]+)-\d+-passwordKeyVault$/i);
51
+ if (!dbPasswordMatch) return null;
52
+ const appName = dbPasswordMatch[1];
53
+ if (appName === 'miso-controller') return 'miso_pass123';
54
+ const dbName = appName.replace(/-/g, '_');
55
+ return `${dbName}_pass123`;
56
+ }
57
+
58
+ /**
59
+ * Generate database URL value for a key (databases-*-urlKeyVault)
60
+ * @param {string} key - Secret key name
61
+ * @returns {string|null} URL string or null if key does not match
62
+ */
63
+ function generateDbUrlValue(key) {
64
+ const dbUrlMatch = key.match(/^databases-([a-z0-9-_]+)-\d+-urlKeyVault$/i);
65
+ if (!dbUrlMatch) return null;
66
+ const appName = dbUrlMatch[1];
67
+ if (appName === 'miso-controller') {
68
+ return 'postgresql://miso_user:miso_pass123@${DB_HOST}:${DB_PORT}/miso';
69
+ }
70
+ const dbName = appName.replace(/-/g, '_');
71
+ return `postgresql://${dbName}_user:${dbName}_pass123@\${DB_HOST}:\${DB_PORT}/${dbName}`;
72
+ }
73
+
44
74
  /**
45
75
  * Generates secret value based on key name
46
76
  * @function generateSecretValue
@@ -51,22 +81,14 @@ function generateSecretValue(key) {
51
81
  const keyLower = key.toLowerCase();
52
82
 
53
83
  if (keyLower.includes('password')) {
54
- const dbPasswordMatch = key.match(/^databases-([a-z0-9-_]+)-\d+-passwordKeyVault$/i);
55
- if (dbPasswordMatch) {
56
- const appName = dbPasswordMatch[1];
57
- const dbName = appName.replace(/-/g, '_');
58
- return `${dbName}_pass123`;
59
- }
84
+ const dbPassword = generateDbPasswordValue(key);
85
+ if (dbPassword !== null) return dbPassword;
60
86
  return crypto.randomBytes(32).toString('base64');
61
87
  }
62
88
 
63
89
  if (keyLower.includes('url') || keyLower.includes('uri')) {
64
- const dbUrlMatch = key.match(/^databases-([a-z0-9-_]+)-\d+-urlKeyVault$/i);
65
- if (dbUrlMatch) {
66
- const appName = dbUrlMatch[1];
67
- const dbName = appName.replace(/-/g, '_');
68
- return `postgresql://${dbName}_user:${dbName}_pass123@\${DB_HOST}:\${DB_PORT}/${dbName}`;
69
- }
90
+ const dbUrl = generateDbUrlValue(key);
91
+ if (dbUrl !== null) return dbUrl;
70
92
  return '';
71
93
  }
72
94
 
@@ -263,8 +263,7 @@ async function getLocalEnvWithOverrides() {
263
263
  let localEnv = await getEnvHosts('local');
264
264
 
265
265
  try {
266
- const os = require('os');
267
- const cfgPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
266
+ const cfgPath = config.CONFIG_FILE;
268
267
  if (fs.existsSync(cfgPath)) {
269
268
  const cfgContent = fs.readFileSync(cfgPath, 'utf8');
270
269
  const cfg = yaml.load(cfgContent) || {};
@@ -10,6 +10,7 @@
10
10
 
11
11
  const config = require('../core/config');
12
12
  const { refreshDeviceToken: apiRefreshDeviceToken } = require('./api');
13
+ const { isTokenEncrypted } = require('./token-encryption');
13
14
 
14
15
  /**
15
16
  * Validates refresh token parameters
@@ -138,6 +139,10 @@ async function refreshDeviceToken(controllerUrl, refreshToken) {
138
139
  if (!refreshToken || typeof refreshToken !== 'string') {
139
140
  throw new Error('Refresh token is required');
140
141
  }
142
+ // Never send encrypted token to the API (causes 401). Decryption should happen in getDeviceToken; this is a safeguard.
143
+ if (isTokenEncrypted(refreshToken)) {
144
+ throw new Error('Refresh token is still encrypted; decryption may have failed. Run "aifabrix login" to authenticate again.');
145
+ }
141
146
 
142
147
  try {
143
148
  // Call API refresh endpoint
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.33.0",
3
+ "version": "2.33.3",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -0,0 +1,16 @@
1
+ # AI Fabrix Dataplane - Build from base image
2
+ # This repo has no application source; use the published image as baseline.
3
+ # Build: docker build -t aifabrix/dataplane:local .
4
+ # Or use the image directly: docker run aifabrix/dataplane:latest
5
+
6
+ FROM aifabrix/dataplane:latest
7
+
8
+ # Expose port (documentation; base image may already set this)
9
+ EXPOSE 3001
10
+
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
14
+
15
+ # CMD inherited from base image; override only if needed
16
+ # CMD inherited: uvicorn app.main:app --host 0.0.0.0 --port 3001