@aifabrix/builder 2.33.1 → 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 (42) hide show
  1. package/README.md +13 -0
  2. package/lib/app/deploy-config.js +161 -0
  3. package/lib/app/deploy.js +28 -153
  4. package/lib/app/register.js +6 -5
  5. package/lib/app/run-helpers.js +23 -17
  6. package/lib/cli.js +31 -1
  7. package/lib/commands/logout.js +3 -4
  8. package/lib/commands/up-common.js +72 -0
  9. package/lib/commands/up-dataplane.js +109 -0
  10. package/lib/commands/up-miso.js +134 -0
  11. package/lib/core/config.js +32 -9
  12. package/lib/core/secrets-docker-env.js +88 -0
  13. package/lib/core/secrets.js +142 -115
  14. package/lib/infrastructure/helpers.js +82 -1
  15. package/lib/infrastructure/index.js +2 -0
  16. package/lib/schema/env-config.yaml +7 -0
  17. package/lib/utils/compose-generator.js +13 -13
  18. package/lib/utils/config-paths.js +13 -0
  19. package/lib/utils/device-code.js +2 -2
  20. package/lib/utils/env-endpoints.js +2 -5
  21. package/lib/utils/env-map.js +4 -5
  22. package/lib/utils/parse-image-ref.js +27 -0
  23. package/lib/utils/paths.js +28 -4
  24. package/lib/utils/secrets-generator.js +34 -12
  25. package/lib/utils/secrets-helpers.js +1 -2
  26. package/lib/utils/token-manager-refresh.js +5 -0
  27. package/package.json +1 -1
  28. package/templates/applications/dataplane/Dockerfile +16 -0
  29. package/templates/applications/dataplane/README.md +205 -0
  30. package/templates/applications/dataplane/env.template +143 -0
  31. package/templates/applications/dataplane/rbac.yaml +283 -0
  32. package/templates/applications/dataplane/variables.yaml +143 -0
  33. package/templates/applications/keycloak/Dockerfile +1 -1
  34. package/templates/applications/keycloak/README.md +193 -0
  35. package/templates/applications/keycloak/variables.yaml +5 -6
  36. package/templates/applications/miso-controller/Dockerfile +8 -8
  37. package/templates/applications/miso-controller/README.md +369 -0
  38. package/templates/applications/miso-controller/env.template +114 -6
  39. package/templates/applications/miso-controller/rbac.yaml +74 -0
  40. package/templates/applications/miso-controller/variables.yaml +93 -5
  41. package/templates/infra/compose.yaml.hbs +2 -1
  42. package/templates/applications/miso-controller/test.yaml +0 -1
@@ -0,0 +1,109 @@
1
+ /**
2
+ * AI Fabrix Builder - Up Dataplane Command
3
+ *
4
+ * Registers or rotates dataplane app in dev, then deploys. Miso-controller runs
5
+ * the dataplane container; this command does not run the image locally.
6
+ * If app is already registered, uses rotate-secret; otherwise registers.
7
+ *
8
+ * @fileoverview up-dataplane command implementation
9
+ * @author AI Fabrix Team
10
+ * @version 2.0.0
11
+ */
12
+
13
+ const path = require('path');
14
+ const fs = require('fs');
15
+ const yaml = require('js-yaml');
16
+ const chalk = require('chalk');
17
+ const logger = require('../utils/logger');
18
+ const config = require('../core/config');
19
+ const { checkAuthentication } = require('../utils/app-register-auth');
20
+ const { resolveControllerUrl } = require('../utils/controller-url');
21
+ const { resolveEnvironment } = require('../core/config');
22
+ const { registerApplication } = require('../app/register');
23
+ const { rotateSecret } = require('../app/rotate-secret');
24
+ const { checkApplicationExists } = require('../utils/app-existence');
25
+ const app = require('../app');
26
+ const { ensureAppFromTemplate } = require('./up-common');
27
+
28
+ /**
29
+ * Register or rotate dataplane: if app exists in controller, rotate secret; otherwise register.
30
+ * @async
31
+ * @param {Object} options - Commander options
32
+ * @param {string} controllerUrl - Controller URL
33
+ * @param {string} environmentKey - Environment key
34
+ * @param {Object} authConfig - Auth config with token
35
+ */
36
+ async function registerOrRotateDataplane(options, controllerUrl, environmentKey, authConfig) {
37
+ const appExists = await checkApplicationExists('dataplane', controllerUrl, environmentKey, authConfig);
38
+ if (appExists) {
39
+ logger.log(chalk.blue('Dataplane already registered; rotating secret...'));
40
+ await rotateSecret('dataplane', options);
41
+ } else {
42
+ const imageOverride = options.image || (options.registry ? buildDataplaneImageRef(options.registry) : undefined);
43
+ const registerOpts = { imageOverride, image: imageOverride, registryMode: options.registryMode };
44
+ await registerApplication('dataplane', registerOpts);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Build full image ref from registry and dataplane variables (registry/name:tag)
50
+ * @param {string} registry - Registry URL
51
+ * @returns {string|undefined} Full image reference or undefined
52
+ */
53
+ function buildDataplaneImageRef(registry) {
54
+ const pathsUtil = require('../utils/paths');
55
+ const variablesPath = path.join(pathsUtil.getBuilderPath('dataplane'), 'variables.yaml');
56
+ if (!fs.existsSync(variablesPath)) return undefined;
57
+ const content = fs.readFileSync(variablesPath, 'utf8');
58
+ const variables = yaml.load(content);
59
+ const name = variables?.image?.name || variables?.app?.key || 'dataplane';
60
+ const tag = variables?.image?.tag || 'latest';
61
+ const base = (registry || '').replace(/\/+$/, '');
62
+ return base ? `${base}/${name}:${tag}` : undefined;
63
+ }
64
+
65
+ /**
66
+ * Handle up-dataplane command: ensure logged in, environment dev, ensure dataplane,
67
+ * register or rotate (if already registered), then deploy (miso-controller runs the container).
68
+ *
69
+ * @async
70
+ * @function handleUpDataplane
71
+ * @param {Object} options - Commander options
72
+ * @param {string} [options.registry] - Override registry for dataplane
73
+ * @param {string} [options.registryMode] - Override registry mode (acr|external)
74
+ * @param {string} [options.image] - Override image reference for dataplane
75
+ * @returns {Promise<void>}
76
+ * @throws {Error} If not logged in, environment not dev, or any step fails
77
+ */
78
+ async function handleUpDataplane(options = {}) {
79
+ const builderDir = await config.getAifabrixBuilderDir();
80
+ if (builderDir) {
81
+ process.env.AIFABRIX_BUILDER_DIR = builderDir;
82
+ }
83
+ logger.log(chalk.blue('Starting up-dataplane (register/rotate, deploy dataplane in dev)...\n'));
84
+
85
+ const [controllerUrl, environmentKey] = await Promise.all([resolveControllerUrl(), resolveEnvironment()]);
86
+ const authConfig = await checkAuthentication(controllerUrl, environmentKey);
87
+
88
+ const cfg = await config.getConfig();
89
+ const environment = (cfg && cfg.environment) ? cfg.environment : 'dev';
90
+ if (environment !== 'dev') {
91
+ throw new Error(
92
+ 'Dataplane is only supported in dev environment. Set with: aifabrix auth config --set-environment dev.'
93
+ );
94
+ }
95
+ logger.log(chalk.green('✓ Logged in and environment is dev'));
96
+
97
+ await ensureAppFromTemplate('dataplane');
98
+
99
+ await registerOrRotateDataplane(options, controllerUrl, environmentKey, authConfig);
100
+
101
+ const imageOverride = options.image || (options.registry ? buildDataplaneImageRef(options.registry) : undefined);
102
+ const deployOpts = { imageOverride, image: imageOverride, registryMode: options.registryMode };
103
+
104
+ await app.deployApp('dataplane', deployOpts);
105
+
106
+ logger.log(chalk.green('\n✓ up-dataplane complete. Dataplane is registered and deployed in dev (miso-controller runs the container).'));
107
+ }
108
+
109
+ module.exports = { handleUpDataplane, buildDataplaneImageRef };
@@ -0,0 +1,134 @@
1
+ /**
2
+ * AI Fabrix Builder - Up Miso Command
3
+ *
4
+ * Installs miso-controller and keycloak from images (no build).
5
+ * Assumes infra is up; sets dev secrets and resolves (no force; existing .env values preserved).
6
+ *
7
+ * @fileoverview up-miso command implementation
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const path = require('path');
13
+ const fs = require('fs');
14
+ const yaml = require('js-yaml');
15
+ const chalk = require('chalk');
16
+ const logger = require('../utils/logger');
17
+ const config = require('../core/config');
18
+ const secrets = require('../core/secrets');
19
+ const infra = require('../infrastructure');
20
+ const app = require('../app');
21
+ const { saveLocalSecret } = require('../utils/local-secrets');
22
+ const { ensureAppFromTemplate } = require('./up-common');
23
+
24
+ /** Keycloak base port (from templates/applications/keycloak/variables.yaml) */
25
+ const KEYCLOAK_BASE_PORT = 8082;
26
+ /** Miso controller base port (dev-config app base) */
27
+ const MISO_BASE_PORT = 3000;
28
+
29
+ /**
30
+ * Parse --image options array into map { keycloak?: string, 'miso-controller'?: string }
31
+ * @param {string[]|string} imageOpts - Option value(s) e.g. ['keycloak=reg/k:v1', 'miso-controller=reg/m:v1']
32
+ * @returns {{ keycloak?: string, 'miso-controller'?: string }}
33
+ */
34
+ function parseImageOptions(imageOpts) {
35
+ const map = {};
36
+ const arr = Array.isArray(imageOpts) ? imageOpts : (imageOpts ? [imageOpts] : []);
37
+ for (const item of arr) {
38
+ if (typeof item !== 'string') continue;
39
+ const eq = item.indexOf('=');
40
+ if (eq > 0) {
41
+ const key = item.substring(0, eq).trim();
42
+ const value = item.substring(eq + 1).trim();
43
+ if (key && value) map[key] = value;
44
+ }
45
+ }
46
+ return map;
47
+ }
48
+
49
+ /**
50
+ * Build full image ref from registry and app variables (registry/name:tag)
51
+ * @param {string} appName - keycloak or miso-controller
52
+ * @param {string} registry - Registry URL
53
+ * @returns {string} Full image reference
54
+ */
55
+ function buildImageRefFromRegistry(appName, registry) {
56
+ const pathsUtil = require('../utils/paths');
57
+ const variablesPath = path.join(pathsUtil.getBuilderPath(appName), 'variables.yaml');
58
+ if (!fs.existsSync(variablesPath)) return undefined;
59
+ const content = fs.readFileSync(variablesPath, 'utf8');
60
+ const variables = yaml.load(content);
61
+ const name = variables?.image?.name || variables?.app?.key || appName;
62
+ const tag = variables?.image?.tag || 'latest';
63
+ const base = (registry || '').replace(/\/+$/, '');
64
+ return base ? `${base}/${name}:${tag}` : undefined;
65
+ }
66
+
67
+ /**
68
+ * Set URL secrets and resolve keycloak + miso-controller (no force; existing .env preserved)
69
+ * @async
70
+ * @param {number} devIdNum - Developer ID number
71
+ */
72
+ async function setMisoSecretsAndResolve(devIdNum) {
73
+ const keycloakPort = KEYCLOAK_BASE_PORT + (devIdNum === 0 ? 0 : devIdNum * 100);
74
+ const misoPort = MISO_BASE_PORT + (devIdNum === 0 ? 0 : devIdNum * 100);
75
+ await saveLocalSecret('keycloak-public-server-urlKeyVault', `http://localhost:${keycloakPort}`);
76
+ await saveLocalSecret('miso-controller-web-server-url', `http://localhost:${misoPort}`);
77
+ logger.log(chalk.green('✓ Set keycloak and miso-controller URL secrets'));
78
+ await secrets.generateEnvFile('keycloak', undefined, 'docker', false, true);
79
+ await secrets.generateEnvFile('miso-controller', undefined, 'docker', false, true);
80
+ logger.log(chalk.green('✓ Resolved keycloak and miso-controller'));
81
+ }
82
+
83
+ /**
84
+ * Build run options and run keycloak then miso-controller
85
+ * @async
86
+ * @param {Object} options - Commander options (image, registry, registryMode)
87
+ */
88
+ async function runMisoApps(options) {
89
+ const imageMap = parseImageOptions(options.image);
90
+ const keycloakImage = imageMap.keycloak || (options.registry ? buildImageRefFromRegistry('keycloak', options.registry) : undefined);
91
+ const misoImage = imageMap['miso-controller'] || (options.registry ? buildImageRefFromRegistry('miso-controller', options.registry) : undefined);
92
+ const keycloakRunOpts = { image: keycloakImage, registry: options.registry, registryMode: options.registryMode, skipEnvOutputPath: true };
93
+ const misoRunOpts = { image: misoImage, registry: options.registry, registryMode: options.registryMode, skipEnvOutputPath: true };
94
+ logger.log(chalk.blue('Starting keycloak...'));
95
+ await app.runApp('keycloak', keycloakRunOpts);
96
+ logger.log(chalk.blue('Starting miso-controller...'));
97
+ await app.runApp('miso-controller', misoRunOpts);
98
+ }
99
+
100
+ /**
101
+ * Handle up-miso command: ensure infra, ensure app dirs, set secrets, resolve (preserve existing .env), run keycloak then miso-controller.
102
+ *
103
+ * @async
104
+ * @function handleUpMiso
105
+ * @param {Object} options - Commander options
106
+ * @param {string} [options.registry] - Override registry for both apps
107
+ * @param {string} [options.registryMode] - Override registry mode (acr|external)
108
+ * @param {string[]|string} [options.image] - Override images e.g. keycloak=reg/k:v1 or miso-controller=reg/m:v1
109
+ * @returns {Promise<void>}
110
+ * @throws {Error} If infra not up or any step fails
111
+ */
112
+ async function handleUpMiso(options = {}) {
113
+ const builderDir = await config.getAifabrixBuilderDir();
114
+ if (builderDir) {
115
+ process.env.AIFABRIX_BUILDER_DIR = builderDir;
116
+ }
117
+ logger.log(chalk.blue('Starting up-miso (keycloak + miso-controller from images)...\n'));
118
+ const health = await infra.checkInfraHealth();
119
+ const allHealthy = Object.values(health).every(status => status === 'healthy');
120
+ if (!allHealthy) {
121
+ throw new Error('Infrastructure is not up. Run \'aifabrix up\' first.');
122
+ }
123
+ logger.log(chalk.green('✓ Infrastructure is up'));
124
+ await ensureAppFromTemplate('keycloak');
125
+ await ensureAppFromTemplate('miso-controller');
126
+ const developerId = await config.getDeveloperId();
127
+ const devIdNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
128
+ await setMisoSecretsAndResolve(devIdNum);
129
+ await runMisoApps(options);
130
+ logger.log(chalk.green('\n✓ up-miso complete. Keycloak and miso-controller are running.'));
131
+ logger.log(chalk.gray(' Run onboarding and register Keycloak from the miso-controller repo if needed.'));
132
+ }
133
+
134
+ module.exports = { handleUpMiso, parseImageOptions };
@@ -14,16 +14,31 @@ const yaml = require('js-yaml');
14
14
  const os = require('os');
15
15
  const { encryptToken, decryptToken, isTokenEncrypted } = require('../utils/token-encryption');
16
16
  // Avoid importing paths here to prevent circular dependency.
17
- // Config location is always under OS home at ~/.aifabrix/config.yaml
18
-
19
- // Default (for tests and constants): always reflects OS home
20
- const CONFIG_DIR = path.join(os.homedir(), '.aifabrix');
21
- const CONFIG_FILE = path.join(CONFIG_DIR, 'config.yaml');
17
+ // Config location (first match wins):
18
+ // 1. AIFABRIX_CONFIG env = full path to config.yaml
19
+ // 2. AIFABRIX_HOME env = directory containing config.yaml
20
+ // 3. ~/.aifabrix
21
+ // Set AIFABRIX_HOME=/workspace/.aifabrix or AIFABRIX_CONFIG=/workspace/.aifabrix/config.yaml when config is not in default home.
22
+
23
+ function getConfigDir() {
24
+ const configFile = process.env.AIFABRIX_CONFIG && typeof process.env.AIFABRIX_CONFIG === 'string';
25
+ if (configFile) {
26
+ return path.dirname(path.resolve(process.env.AIFABRIX_CONFIG.trim()));
27
+ }
28
+ if (process.env.AIFABRIX_HOME && typeof process.env.AIFABRIX_HOME === 'string') {
29
+ return path.resolve(process.env.AIFABRIX_HOME.trim());
30
+ }
31
+ return path.join(os.homedir(), '.aifabrix');
32
+ }
22
33
 
23
- // Runtime config directory (always under OS home)
24
- const RUNTIME_CONFIG_DIR = path.join(os.homedir(), '.aifabrix');
34
+ // Runtime config directory and file (respect AIFABRIX_HOME)
35
+ const RUNTIME_CONFIG_DIR = getConfigDir();
25
36
  const RUNTIME_CONFIG_FILE = path.join(RUNTIME_CONFIG_DIR, 'config.yaml');
26
37
 
38
+ // Legacy exports (same as runtime when module loads)
39
+ const CONFIG_DIR = RUNTIME_CONFIG_DIR;
40
+ const CONFIG_FILE = RUNTIME_CONFIG_FILE;
41
+
27
42
  // Cache for developer ID - loaded when getConfig() is first called
28
43
  let cachedDeveloperId = null;
29
44
 
@@ -325,9 +340,17 @@ async function decryptTokenValue(value) {
325
340
  if (!isTokenEncrypted(value)) return value;
326
341
  const decrypted = decryptToken(value, encryptionKey);
327
342
  // Ensure we never return undefined for valid inputs
328
- return decrypted !== undefined && decrypted !== null ? decrypted : value;
343
+ if (decrypted !== undefined && decrypted !== null) {
344
+ return decrypted;
345
+ }
346
+ // Encrypted value but decryption produced nothing - do not return encrypted string to callers (e.g. refresh API)
347
+ throw new Error('Could not decrypt stored token. If you changed the secrets-encryption key, run "aifabrix login" again.');
329
348
  } catch (error) {
330
- return value;
349
+ if (error.message && error.message.includes('Could not decrypt stored token')) {
350
+ throw error;
351
+ }
352
+ // Decryption failed (wrong key, corrupted data, etc.) - do not pass encrypted value to callers
353
+ throw new Error('Could not decrypt stored token. If you changed the secrets-encryption key, run "aifabrix login" again.');
331
354
  }
332
355
  }
333
356
  // Token management functions moved to lib/utils/config-tokens.js to reduce file size
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Docker environment helpers for secrets/env generation.
3
+ *
4
+ * @fileoverview Base docker env, config overrides, and PORT handling for container env
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const yaml = require('js-yaml');
11
+ const config = require('./config');
12
+ const { getEnvHosts } = require('../utils/env-endpoints');
13
+ const { getContainerPortFromPath } = require('../utils/port-resolver');
14
+
15
+ /**
16
+ * Gets base docker environment config
17
+ * @async
18
+ * @function getBaseDockerEnv
19
+ * @returns {Promise<Object>} Docker environment config
20
+ */
21
+ async function getBaseDockerEnv() {
22
+ return await getEnvHosts('docker');
23
+ }
24
+
25
+ /**
26
+ * Applies config.yaml override to docker environment
27
+ * @function applyDockerEnvOverride
28
+ * @param {Object} dockerEnv - Base docker environment config
29
+ * @returns {Object} Updated docker environment config
30
+ */
31
+ function applyDockerEnvOverride(dockerEnv) {
32
+ try {
33
+ const cfgPath = config.CONFIG_FILE;
34
+ if (fs.existsSync(cfgPath)) {
35
+ const cfgContent = fs.readFileSync(cfgPath, 'utf8');
36
+ const cfg = yaml.load(cfgContent) || {};
37
+ if (cfg && cfg.environments && cfg.environments.docker) {
38
+ return { ...dockerEnv, ...cfg.environments.docker };
39
+ }
40
+ }
41
+ } catch {
42
+ // Ignore config.yaml read errors, continue with env-config values
43
+ }
44
+ return dockerEnv;
45
+ }
46
+
47
+ /**
48
+ * Gets container port from docker environment config
49
+ * @function getContainerPortFromDockerEnv
50
+ * @param {Object} dockerEnv - Docker environment config
51
+ * @returns {number} Container port (defaults to 3000)
52
+ */
53
+ function getContainerPortFromDockerEnv(dockerEnv) {
54
+ if (dockerEnv.PORT === undefined || dockerEnv.PORT === null) {
55
+ return 3000;
56
+ }
57
+ const portVal = typeof dockerEnv.PORT === 'number' ? dockerEnv.PORT : parseInt(dockerEnv.PORT, 10);
58
+ return Number.isNaN(portVal) ? 3000 : portVal;
59
+ }
60
+
61
+ /**
62
+ * Updates PORT in resolved content for docker environment
63
+ * Sets PORT to container port (build.containerPort or port from variables.yaml)
64
+ * NOT the host port (which includes developer-id offset)
65
+ * @async
66
+ * @function updatePortForDocker
67
+ * @param {string} resolved - Resolved environment content
68
+ * @param {string} variablesPath - Path to variables.yaml file
69
+ * @returns {Promise<string>} Updated content with PORT set
70
+ */
71
+ async function updatePortForDocker(resolved, variablesPath) {
72
+ let dockerEnv = await getBaseDockerEnv();
73
+ dockerEnv = applyDockerEnvOverride(dockerEnv);
74
+
75
+ let containerPort = getContainerPortFromPath(variablesPath);
76
+ if (containerPort === null) {
77
+ containerPort = getContainerPortFromDockerEnv(dockerEnv);
78
+ }
79
+
80
+ return resolved.replace(/^PORT\s*=\s*.*$/m, `PORT=${containerPort}`);
81
+ }
82
+
83
+ module.exports = {
84
+ getBaseDockerEnv,
85
+ applyDockerEnvOverride,
86
+ getContainerPortFromDockerEnv,
87
+ updatePortForDocker
88
+ };