@aifabrix/builder 2.39.3 → 2.40.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.cursor/rules/project-rules.mdc +6 -6
  2. package/README.md +3 -3
  3. package/babel.config.js +6 -0
  4. package/integration/hubspot/README.md +53 -141
  5. package/integration/hubspot/application.yaml +37 -0
  6. package/integration/hubspot/env.template +2 -11
  7. package/integration/hubspot/hubspot-deploy.json +1 -0
  8. package/integration/hubspot/test.js +5 -5
  9. package/jest.config.manual.js +29 -0
  10. package/lib/api/credentials.api.js +5 -5
  11. package/lib/api/deployments.api.js +2 -2
  12. package/lib/api/pipeline.api.js +17 -17
  13. package/lib/api/wizard.api.js +2 -2
  14. package/lib/app/config.js +11 -6
  15. package/lib/app/deploy-config.js +13 -16
  16. package/lib/app/deploy.js +29 -22
  17. package/lib/app/display.js +1 -1
  18. package/lib/app/dockerfile.js +11 -12
  19. package/lib/app/helpers.js +51 -13
  20. package/lib/app/index.js +14 -2
  21. package/lib/app/prompts.js +37 -45
  22. package/lib/app/push.js +8 -11
  23. package/lib/app/readme.js +16 -12
  24. package/lib/app/register.js +1 -1
  25. package/lib/app/run-helpers.js +31 -22
  26. package/lib/app/run.js +44 -5
  27. package/lib/app/show-display.js +104 -44
  28. package/lib/app/show.js +123 -43
  29. package/lib/build/index.js +11 -18
  30. package/lib/cli/setup-app.js +36 -29
  31. package/lib/cli/setup-auth.js +19 -15
  32. package/lib/cli/setup-credential-deployment.js +3 -1
  33. package/lib/cli/setup-external-system.js +35 -16
  34. package/lib/cli/setup-infra.js +45 -23
  35. package/lib/cli/setup-utility.js +85 -31
  36. package/lib/commands/app-logs.js +28 -20
  37. package/lib/commands/app.js +30 -26
  38. package/lib/commands/auth-status.js +36 -3
  39. package/lib/commands/convert.js +202 -0
  40. package/lib/commands/credential-list.js +78 -17
  41. package/lib/commands/datasource.js +24 -24
  42. package/lib/commands/deployment-list.js +13 -6
  43. package/lib/commands/up-common.js +80 -42
  44. package/lib/commands/up-dataplane.js +15 -14
  45. package/lib/commands/up-miso.js +15 -14
  46. package/lib/commands/upload.js +163 -0
  47. package/lib/commands/wizard-core.js +5 -4
  48. package/lib/core/diff.js +84 -9
  49. package/lib/core/key-generator.js +9 -12
  50. package/lib/core/secrets-docker-env.js +2 -2
  51. package/lib/core/secrets.js +3 -2
  52. package/lib/core/templates.js +2 -2
  53. package/lib/datasource/deploy.js +2 -1
  54. package/lib/deployment/deployer.js +76 -48
  55. package/lib/external-system/delete.js +0 -1
  56. package/lib/external-system/deploy-helpers.js +5 -6
  57. package/lib/external-system/deploy.js +7 -2
  58. package/lib/external-system/download-helpers.js +4 -4
  59. package/lib/external-system/download.js +11 -10
  60. package/lib/external-system/generator.js +19 -17
  61. package/lib/external-system/test.js +10 -15
  62. package/lib/generator/builders.js +1 -1
  63. package/lib/generator/external-controller-manifest.js +26 -29
  64. package/lib/generator/external-schema-utils.js +6 -18
  65. package/lib/generator/external.js +32 -27
  66. package/lib/generator/github.js +1 -1
  67. package/lib/generator/helpers.js +12 -19
  68. package/lib/generator/index.js +15 -15
  69. package/lib/generator/parse-image.js +35 -0
  70. package/lib/generator/split-readme.js +105 -0
  71. package/lib/generator/split-variables.js +149 -0
  72. package/lib/generator/split.js +86 -246
  73. package/lib/generator/wizard.js +51 -70
  74. package/lib/schema/application-schema.json +4 -4
  75. package/lib/schema/external-datasource.schema.json +5 -0
  76. package/lib/schema/external-system.schema.json +10 -0
  77. package/lib/utils/app-config-resolver.js +52 -0
  78. package/lib/utils/app-register-api.js +1 -1
  79. package/lib/utils/app-register-auth.js +1 -1
  80. package/lib/utils/app-register-config.js +16 -23
  81. package/lib/utils/app-register-validator.js +2 -2
  82. package/lib/utils/cli-utils.js +47 -3
  83. package/lib/utils/config-format.js +154 -0
  84. package/lib/utils/config-paths.js +19 -52
  85. package/lib/utils/config-tokens.js +1 -0
  86. package/lib/utils/docker-build.js +71 -94
  87. package/lib/utils/dockerfile-utils.js +1 -1
  88. package/lib/utils/env-copy.js +4 -4
  89. package/lib/utils/env-ports.js +2 -2
  90. package/lib/utils/error-formatter.js +1 -1
  91. package/lib/utils/error-formatters/validation-errors.js +1 -1
  92. package/lib/utils/external-readme.js +12 -5
  93. package/lib/utils/external-system-test-helpers.js +2 -0
  94. package/lib/utils/health-check.js +55 -66
  95. package/lib/utils/image-version.js +12 -21
  96. package/lib/utils/paths.js +45 -66
  97. package/lib/utils/port-resolver.js +8 -8
  98. package/lib/utils/schema-loader.js +22 -0
  99. package/lib/utils/schema-resolver.js +23 -33
  100. package/lib/utils/secrets-helpers.js +7 -7
  101. package/lib/utils/secrets-utils.js +10 -12
  102. package/lib/utils/template-helpers.js +13 -13
  103. package/lib/utils/token-manager.js +20 -2
  104. package/lib/utils/variable-transformer.js +2 -2
  105. package/lib/validation/validate-display.js +3 -4
  106. package/lib/validation/validate.js +34 -28
  107. package/lib/validation/validator.js +50 -30
  108. package/package.json +4 -1
  109. package/templates/README.md +1 -1
  110. package/templates/applications/README.md.hbs +3 -3
  111. package/templates/applications/miso-controller/env.template +3 -1
  112. package/templates/external-system/README.md.hbs +4 -4
  113. package/templates/external-system/external-system.json.hbs +1 -16
  114. package/integration/hubspot/variables.yaml +0 -17
  115. /package/templates/applications/dataplane/{variables.yaml → application.yaml} +0 -0
  116. /package/templates/applications/keycloak/{variables.yaml → application.yaml} +0 -0
  117. /package/templates/applications/miso-controller/{variables.yaml → application.yaml} +0 -0
@@ -119,7 +119,7 @@ function buildHostnameToServiceMap(dockerHosts) {
119
119
  }
120
120
 
121
121
  /**
122
- * Resolves port for a single URL by looking up service's variables.yaml
122
+ * Resolves port for a single URL by looking up service's application config
123
123
  * @function resolveUrlPort
124
124
  * @param {string} protocol - URL protocol (http:// or https://)
125
125
  * @param {string} hostname - Service hostname
@@ -135,24 +135,22 @@ function resolveUrlPort(protocol, hostname, port, urlPath, hostnameToService) {
135
135
  return `${protocol}${hostname}:${port}${urlPath}`;
136
136
  }
137
137
 
138
- // Try to load service's variables.yaml
139
- const serviceVariablesPath = path.join(process.cwd(), 'builder', serviceName, 'variables.yaml');
140
- if (!fs.existsSync(serviceVariablesPath)) {
141
- // Service variables.yaml not found, keep original port
138
+ const { resolveApplicationConfigPath } = require('./app-config-resolver');
139
+ const { loadConfigFile } = require('./config-format');
140
+ const builderPath = path.join(process.cwd(), 'builder', serviceName);
141
+ let serviceVariablesPath;
142
+ try {
143
+ serviceVariablesPath = resolveApplicationConfigPath(builderPath);
144
+ } catch {
142
145
  return `${protocol}${hostname}:${port}${urlPath}`;
143
146
  }
144
147
 
145
148
  try {
146
- const variablesContent = fs.readFileSync(serviceVariablesPath, 'utf8');
147
- const variables = yaml.load(variablesContent);
148
-
149
+ const variables = loadConfigFile(serviceVariablesPath);
149
150
  const containerPort = getContainerPort(variables, port);
150
-
151
- // Replace port in URL
152
151
  return `${protocol}${hostname}:${containerPort}${urlPath}`;
153
152
  } catch (error) {
154
- // Error loading variables.yaml, keep original port
155
- logger.warn(`Warning: Could not load variables.yaml for service ${serviceName}: ${error.message}`);
153
+ logger.warn(`Warning: Could not load application config for service ${serviceName}: ${error.message}`);
156
154
  return `${protocol}${hostname}:${port}${urlPath}`;
157
155
  }
158
156
  }
@@ -14,7 +14,7 @@ const chalk = require('chalk');
14
14
  const logger = require('./logger');
15
15
 
16
16
  /**
17
- * Loads template variables from template's variables.yaml file
17
+ * Loads template variables from template's application.yaml file
18
18
  * @async
19
19
  * @function loadTemplateVariables
20
20
  * @param {string} templateName - Template name
@@ -27,15 +27,15 @@ async function loadTemplateVariables(templateName) {
27
27
 
28
28
  const yaml = require('js-yaml');
29
29
  const templatePath = path.join(__dirname, '..', '..', 'templates', 'applications', templateName);
30
- const templateVariablesPath = path.join(templatePath, 'variables.yaml');
30
+ const templateVariablesPath = path.join(templatePath, 'application.yaml');
31
31
 
32
32
  try {
33
33
  const templateContent = await fs.readFile(templateVariablesPath, 'utf8');
34
34
  return yaml.load(templateContent);
35
35
  } catch (error) {
36
- // Template variables.yaml not found or invalid, continue without it
36
+ // Template application.yaml not found or invalid, continue without it
37
37
  if (error.code !== 'ENOENT') {
38
- logger.warn(chalk.yellow(`⚠️ Warning: Could not load template variables.yaml: ${error.message}`));
38
+ logger.warn(chalk.yellow(`⚠️ Warning: Could not load template application.yaml: ${error.message}`));
39
39
  }
40
40
  return null;
41
41
  }
@@ -101,7 +101,7 @@ function updateDatabaseConfig(variables, options, appName) {
101
101
  }
102
102
 
103
103
  /**
104
- * Updates variables.yaml file after copying from template
104
+ * Updates application config file after copying from template
105
105
  * Updates app.key, displayName, and port with actual values
106
106
  * @async
107
107
  * @function updateTemplateVariables
@@ -111,21 +111,21 @@ function updateDatabaseConfig(variables, options, appName) {
111
111
  * @param {Object} config - Final configuration
112
112
  */
113
113
  async function updateTemplateVariables(appPath, appName, options, config) {
114
- const variablesPath = path.join(appPath, 'variables.yaml');
114
+ const { resolveApplicationConfigPath } = require('./app-config-resolver');
115
+ const { loadConfigFile, writeConfigFile } = require('./config-format');
115
116
  try {
116
- const yaml = require('js-yaml');
117
- const variablesContent = await fs.readFile(variablesPath, 'utf8');
118
- const variables = yaml.load(variablesContent);
117
+ const variablesPath = resolveApplicationConfigPath(appPath);
118
+ const variables = loadConfigFile(variablesPath) || {};
119
119
 
120
120
  updateAppMetadata(variables, appName);
121
121
  updatePort(variables, options, config);
122
122
  updateBuildConfig(variables);
123
123
  updateDatabaseConfig(variables, options, appName);
124
124
 
125
- await fs.writeFile(variablesPath, yaml.dump(variables, { indent: 2, lineWidth: 120, noRefs: true }));
125
+ writeConfigFile(variablesPath, variables);
126
126
  } catch (error) {
127
- if (error.code !== 'ENOENT') {
128
- logger.warn(chalk.yellow(`⚠️ Warning: Could not update variables.yaml: ${error.message}`));
127
+ if (error.message && !error.message.includes('not found')) {
128
+ logger.warn(chalk.yellow(`⚠️ Warning: Could not update application config: ${error.message}`));
129
129
  }
130
130
  }
131
131
  }
@@ -197,7 +197,7 @@ function mergeAuthentication(merged, templateVariables) {
197
197
  * Merges template variables into options
198
198
  * @function mergeTemplateVariables
199
199
  * @param {Object} options - User-provided options
200
- * @param {Object} templateVariables - Template variables from variables.yaml
200
+ * @param {Object} templateVariables - Template variables from application.yaml
201
201
  * @returns {Object} Merged options object
202
202
  */
203
203
  function mergeTemplateVariables(options, templateVariables) {
@@ -427,6 +427,24 @@ async function getDeviceOnlyAuth(controllerUrl) {
427
427
  throw new Error('Device token authentication required. Run "aifabrix login" to authenticate.');
428
428
  }
429
429
 
430
+ /**
431
+ * Ensures auth config has a Bearer token for Dataplane pipeline endpoints.
432
+ * Dataplane pipeline (upload, validate, publish, test) accepts OAuth2 (Bearer) or API_KEY only;
433
+ * client id/secret are rejected. Call this before invoking pipeline API functions.
434
+ * @param {Object} authConfig - Authentication configuration (may have token, clientId, clientSecret)
435
+ * @throws {Error} If authConfig has only client id/secret (no token)
436
+ */
437
+ function requireBearerForDataplanePipeline(authConfig) {
438
+ if (authConfig.token) {
439
+ return;
440
+ }
441
+ if (authConfig.clientId || authConfig.clientSecret) {
442
+ throw new Error(
443
+ 'Dataplane pipeline endpoints require OAuth2 (Bearer token). Client id/secret are not accepted. Run "aifabrix login" to authenticate, then retry.'
444
+ );
445
+ }
446
+ }
447
+
430
448
  module.exports = {
431
449
  getDeviceToken,
432
450
  getClientToken,
@@ -440,6 +458,6 @@ module.exports = {
440
458
  forceRefreshDeviceToken,
441
459
  getDeploymentAuth,
442
460
  getDeviceOnlyAuth,
443
- extractClientCredentials
461
+ extractClientCredentials,
462
+ requireBearerForDataplanePipeline
444
463
  };
445
-
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Variable Transformation Utilities
3
3
  *
4
- * Transforms nested variables.yaml structure to flat schema format
4
+ * Transforms nested application config structure to flat schema format
5
5
  * Converts app.*, image.*, requires.* to schema-compatible structure
6
6
  *
7
7
  * @fileoverview Variable transformation utilities for AI Fabrix Builder
@@ -309,7 +309,7 @@ function transformOptionalFields(variables, transformed) {
309
309
  }
310
310
 
311
311
  /**
312
- * Transforms nested variables.yaml structure to flat schema format
312
+ * Transforms nested application config structure to flat schema format
313
313
  * Converts app.*, image.*, requires.* to schema-compatible structure
314
314
  * Handles both flat and nested structures
315
315
  *
@@ -8,9 +8,9 @@
8
8
  * @version 2.0.0
9
9
  */
10
10
 
11
- const fs = require('fs');
12
11
  const chalk = require('chalk');
13
12
  const logger = require('../utils/logger');
13
+ const { loadConfigFile } = require('../utils/config-format');
14
14
 
15
15
  /**
16
16
  * Displays application validation results
@@ -44,12 +44,11 @@ function displayApplicationValidation(application) {
44
44
  * Extracts dimensions from a datasource file
45
45
  * @function extractDimensionsFromDatasource
46
46
  * @param {string} filePath - Path to datasource file
47
- * @returns {Object} Dimensions info { dimensions: Object, hasDimensions: boolean }
47
+ * @returns {Object} Dimensions info { dimensions: Object, dimensionKeys: string[], hasDimensions: boolean }
48
48
  */
49
49
  function extractDimensionsFromDatasource(filePath) {
50
50
  try {
51
- const content = fs.readFileSync(filePath, 'utf8');
52
- const parsed = JSON.parse(content);
51
+ const parsed = loadConfigFile(filePath);
53
52
 
54
53
  // Check fieldMappings.dimensions (primary location)
55
54
  const dimensions = parsed.fieldMappings?.dimensions || {};
@@ -10,12 +10,14 @@
10
10
  */
11
11
 
12
12
  const fs = require('fs');
13
- const path = require('path');
13
+ const yaml = require('js-yaml');
14
14
  const validator = require('./validator');
15
15
  const { resolveExternalFiles } = require('../utils/schema-resolver');
16
16
  const { loadExternalSystemSchema, loadExternalDataSourceSchema, detectSchemaType } = require('../utils/schema-loader');
17
17
  const { formatValidationErrors } = require('../utils/error-formatter');
18
18
  const { detectAppType } = require('../utils/paths');
19
+ const { logOfflinePathWhenType } = require('../utils/cli-utils');
20
+ const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
19
21
  const { displayValidationResults } = require('./validate-display');
20
22
  const { generateControllerManifest } = require('../generator/external-controller-manifest');
21
23
  const { validateControllerManifest } = require('./external-manifest-validator');
@@ -43,8 +45,8 @@ async function validateFilePath(filePath) {
43
45
  * @param {string} appName - Application name
44
46
  * @returns {Promise<Array>} Array of validation results
45
47
  */
46
- async function validateExternalFilesForApp(appName) {
47
- const files = await resolveExternalFiles(appName);
48
+ async function validateExternalFilesForApp(appName, options = {}) {
49
+ const files = await resolveExternalFiles(appName, options);
48
50
  const validations = [];
49
51
 
50
52
  for (const file of files) {
@@ -105,21 +107,23 @@ function aggregateValidationResults(appValidation, externalValidations, rbacVali
105
107
  * @returns {Promise<Object>} Validation result
106
108
  */
107
109
  /**
108
- * Parses JSON file content
110
+ * Parses external system/datasource file content (JSON or YAML).
109
111
  * @function parseJsonFileContent
110
- * @param {string} filePath - File path
112
+ * @param {string} filePath - File path (.json, .yaml, or .yml)
111
113
  * @returns {Object} Parse result with parsed object or error
112
114
  */
113
115
  function parseJsonFileContent(filePath) {
114
116
  const content = fs.readFileSync(filePath, 'utf8');
117
+ const isYaml = /\.(yaml|yml)$/i.test(filePath);
115
118
  try {
116
- return { parsed: JSON.parse(content), error: null };
119
+ const parsed = isYaml ? yaml.load(content) : JSON.parse(content);
120
+ return { parsed: parsed || {}, error: null };
117
121
  } catch (error) {
118
122
  return {
119
123
  parsed: null,
120
124
  error: {
121
125
  valid: false,
122
- errors: [`Invalid JSON syntax: ${error.message}`],
126
+ errors: [isYaml ? `Invalid YAML syntax: ${error.message}` : `Invalid JSON syntax: ${error.message}`],
123
127
  warnings: []
124
128
  }
125
129
  };
@@ -206,7 +210,7 @@ async function validateExternalFile(filePath, type) {
206
210
  * @function validateAppOrFile
207
211
  * @param {string} appOrFile - Application name or file path
208
212
  * @param {Object} [options] - Validation options
209
- * @param {string} [options.type] - Forced application type (external)
213
+ *
210
214
  * @returns {Promise<Object>} Validation result with aggregated results
211
215
  * @throws {Error} If validation fails
212
216
  *
@@ -239,9 +243,9 @@ async function validateRbacForExternalSystem(isExternal, appName) {
239
243
  }
240
244
 
241
245
  /**
242
- * Loads and checks variables.yaml for externalIntegration
246
+ * Loads and checks application config for externalIntegration
243
247
  * @function loadVariablesAndCheckExternalIntegration
244
- * @param {string} variablesPath - Path to variables.yaml
248
+ * @param {string} variablesPath - Path to application config
245
249
  * @param {Object} appValidation - Application validation result
246
250
  * @returns {Object|null} Variables object or null if no externalIntegration
247
251
  */
@@ -265,7 +269,7 @@ function loadVariablesAndCheckExternalIntegration(variablesPath, appValidation)
265
269
  valid: appValidation.valid,
266
270
  application: appValidation,
267
271
  externalFiles: [],
268
- warnings: [`Could not parse variables.yaml to check externalIntegration: ${error.message}`]
272
+ warnings: [`Could not parse application config to check externalIntegration: ${error.message}`]
269
273
  };
270
274
  }
271
275
 
@@ -287,9 +291,9 @@ function loadVariablesAndCheckExternalIntegration(variablesPath, appValidation)
287
291
  * @param {string} appName - Application name
288
292
  * @returns {Promise<Object>} Application validation result
289
293
  */
290
- async function validateApplicationStep(appName) {
294
+ async function validateApplicationStep(appName, options = {}) {
291
295
  try {
292
- const appValidation = await validator.validateApplication(appName);
296
+ const appValidation = await validator.validateApplication(appName, options);
293
297
  return {
294
298
  valid: appValidation.valid,
295
299
  errors: appValidation.errors || [],
@@ -311,9 +315,9 @@ async function validateApplicationStep(appName) {
311
315
  * @param {string} appName - Application name
312
316
  * @returns {Promise<Object>} Components validation result
313
317
  */
314
- async function validateComponentsStep(appName) {
318
+ async function validateComponentsStep(appName, options = {}) {
315
319
  try {
316
- const externalValidations = await validateExternalFilesForApp(appName);
320
+ const externalValidations = await validateExternalFilesForApp(appName, options);
317
321
  const componentErrors = [];
318
322
  const componentWarnings = [];
319
323
  const componentFiles = [];
@@ -357,9 +361,9 @@ async function validateComponentsStep(appName) {
357
361
  * @param {string} appName - Application name
358
362
  * @returns {Promise<Object>} Manifest validation result
359
363
  */
360
- async function validateManifestStep(appName) {
364
+ async function validateManifestStep(appName, options = {}) {
361
365
  try {
362
- const manifest = await generateControllerManifest(appName);
366
+ const manifest = await generateControllerManifest(appName, options);
363
367
  const manifestValidation = await validateControllerManifest(manifest);
364
368
  return {
365
369
  valid: manifestValidation.valid,
@@ -389,7 +393,7 @@ async function validateManifestStep(appName) {
389
393
  * const result = await validateExternalSystemComplete('my-hubspot');
390
394
  * // Returns: { valid: true, errors: [], warnings: [], steps: {...} }
391
395
  */
392
- async function validateExternalSystemComplete(appName) {
396
+ async function validateExternalSystemComplete(appName, options = {}) {
393
397
  if (!appName || typeof appName !== 'string') {
394
398
  throw new Error('App name is required and must be a string');
395
399
  }
@@ -400,11 +404,13 @@ async function validateExternalSystemComplete(appName) {
400
404
  manifest: { valid: false, errors: [], warnings: [] }
401
405
  };
402
406
 
407
+ const externalOptions = { ...options };
408
+
403
409
  // Step 1: Validate Application Config
404
- steps.application = await validateApplicationStep(appName);
410
+ steps.application = await validateApplicationStep(appName, externalOptions);
405
411
 
406
412
  // Step 2: Validate Individual Components
407
- steps.components = await validateComponentsStep(appName);
413
+ steps.components = await validateComponentsStep(appName, externalOptions);
408
414
 
409
415
  // If components have errors, return early (don't validate manifest)
410
416
  if (!steps.components.valid) {
@@ -417,7 +423,7 @@ async function validateExternalSystemComplete(appName) {
417
423
  }
418
424
 
419
425
  // Step 3 & 4: Generate and Validate Full Manifest (only if Step 2 passes)
420
- steps.manifest = await validateManifestStep(appName);
426
+ steps.manifest = await validateManifestStep(appName, externalOptions);
421
427
 
422
428
  // Aggregate Results
423
429
  const allErrors = [...steps.application.errors, ...steps.components.errors, ...steps.manifest.errors];
@@ -442,23 +448,23 @@ async function validateAppOrFile(appOrFile, options = {}) {
442
448
  }
443
449
 
444
450
  const appName = appOrFile;
445
- const { appPath, isExternal } = await detectAppType(appName, options);
451
+ const { appPath, isExternal } = await detectAppType(appName);
452
+ logOfflinePathWhenType(appPath);
446
453
 
447
- // For external systems with --type external flag, use new unified validation
448
- if (isExternal && options.type === 'external') {
449
- return await validateExternalSystemComplete(appName);
454
+ if (isExternal) {
455
+ return await validateExternalSystemComplete(appName, options);
450
456
  }
451
457
 
452
- const appValidation = await validator.validateApplication(appName);
458
+ const appValidation = await validator.validateApplication(appName, options);
453
459
  const rbacValidation = await validateRbacForExternalSystem(isExternal, appName);
454
460
 
455
- const variablesPath = path.join(appPath, 'variables.yaml');
461
+ const variablesPath = resolveApplicationConfigPath(appPath);
456
462
  const earlyReturn = loadVariablesAndCheckExternalIntegration(variablesPath, appValidation);
457
463
  if (earlyReturn) {
458
464
  return earlyReturn;
459
465
  }
460
466
 
461
- const externalValidations = await validateExternalFilesForApp(appName);
467
+ const externalValidations = await validateExternalFilesForApp(appName, options);
462
468
  return aggregateValidationResults(appValidation, externalValidations, rbacValidation);
463
469
  }
464
470
 
@@ -2,7 +2,7 @@
2
2
  * AI Fabrix Builder Schema Validation
3
3
  *
4
4
  * This module provides schema validation with developer-friendly error messages.
5
- * Validates variables.yaml, rbac.yaml, and env.template files.
5
+ * Validates application.yaml, rbac.yaml, and env.template files.
6
6
  *
7
7
  * @fileoverview Schema validation with friendly error messages for AI Fabrix Builder
8
8
  * @author AI Fabrix Team
@@ -19,10 +19,11 @@ const externalDataSourceSchema = require('../schema/external-datasource.schema.j
19
19
  const { transformVariablesForValidation } = require('../utils/variable-transformer');
20
20
  const { checkEnvironment } = require('../utils/environment-checker');
21
21
  const { formatValidationErrors } = require('../utils/error-formatter');
22
- const { detectAppType } = require('../utils/paths');
22
+ const { detectAppType, resolveApplicationConfigPath } = require('../utils/paths');
23
+ const { loadConfigFile } = require('../utils/config-format');
23
24
 
24
25
  /**
25
- * Validates variables.yaml file against application schema
26
+ * Validates application config file against application schema
26
27
  * Provides detailed error messages for configuration issues
27
28
  *
28
29
  * @async
@@ -36,27 +37,17 @@ const { detectAppType } = require('../utils/paths');
36
37
  * // Returns: { valid: true, errors: [], warnings: [] }
37
38
  */
38
39
  /**
39
- * Loads and parses variables.yaml
40
+ * Loads and parses application config (application.yaml or application.json) via resolver + converter.
40
41
  * @async
41
42
  * @function loadVariablesYaml
42
43
  * @param {string} appName - Application name
43
44
  * @returns {Promise<Object>} Variables object
44
45
  * @throws {Error} If file not found or invalid
45
46
  */
46
- async function loadVariablesYaml(appName) {
47
- const { appPath } = await detectAppType(appName);
48
- const variablesPath = path.join(appPath, 'variables.yaml');
49
-
50
- if (!fs.existsSync(variablesPath)) {
51
- throw new Error(`variables.yaml not found: ${variablesPath}`);
52
- }
53
-
54
- const content = fs.readFileSync(variablesPath, 'utf8');
55
- try {
56
- return yaml.load(content);
57
- } catch (error) {
58
- throw new Error(`Invalid YAML syntax in variables.yaml: ${error.message}`);
59
- }
47
+ async function loadVariablesYaml(appName, options = {}) {
48
+ const { appPath } = await detectAppType(appName, options);
49
+ const configPath = resolveApplicationConfigPath(appPath);
50
+ return loadConfigFile(configPath);
60
51
  }
61
52
 
62
53
  /**
@@ -119,12 +110,12 @@ function validateFrontDoorRouting(variables, errors) {
119
110
  }
120
111
  }
121
112
 
122
- async function validateVariables(appName) {
113
+ async function validateVariables(appName, options = {}) {
123
114
  if (!appName || typeof appName !== 'string') {
124
115
  throw new Error('App name is required and must be a string');
125
116
  }
126
117
 
127
- const variables = await loadVariablesYaml(appName);
118
+ const variables = await loadVariablesYaml(appName, options);
128
119
  const transformed = transformVariablesForValidation(variables, appName);
129
120
  const validate = setupAjvValidator();
130
121
  const valid = validate(transformed);
@@ -203,16 +194,18 @@ function validatePermissions(permissions) {
203
194
  return errors;
204
195
  }
205
196
 
206
- async function validateRbac(appName) {
197
+ async function validateRbac(appName, options = {}) {
207
198
  if (!appName || typeof appName !== 'string') {
208
199
  throw new Error('App name is required and must be a string');
209
200
  }
210
201
 
211
202
  // Support both builder/ and integration/ directories using detectAppType
212
- const { appPath } = await detectAppType(appName);
213
- const rbacPath = path.join(appPath, 'rbac.yaml');
203
+ const { appPath } = await detectAppType(appName, options);
204
+ const rbacYaml = path.join(appPath, 'rbac.yaml');
205
+ const rbacYml = path.join(appPath, 'rbac.yml');
206
+ const rbacPath = fs.existsSync(rbacYaml) ? rbacYaml : (fs.existsSync(rbacYml) ? rbacYml : null);
214
207
 
215
- if (!fs.existsSync(rbacPath)) {
208
+ if (!rbacPath) {
216
209
  return { valid: true, errors: [], warnings: ['rbac.yaml not found - authentication disabled'] };
217
210
  }
218
211
 
@@ -251,13 +244,13 @@ async function validateRbac(appName) {
251
244
  * const result = await validateEnvTemplate('myapp');
252
245
  * // Returns: { valid: true, errors: [], warnings: [] }
253
246
  */
254
- async function validateEnvTemplate(appName) {
247
+ async function validateEnvTemplate(appName, options = {}) {
255
248
  if (!appName || typeof appName !== 'string') {
256
249
  throw new Error('App name is required and must be a string');
257
250
  }
258
251
 
259
252
  // Support both builder/ and integration/ directories using detectAppType
260
- const { appPath } = await detectAppType(appName);
253
+ const { appPath } = await detectAppType(appName, options);
261
254
  const templatePath = path.join(appPath, 'env.template');
262
255
 
263
256
  if (!fs.existsSync(templatePath)) {
@@ -305,6 +298,32 @@ async function validateEnvTemplate(appName) {
305
298
  };
306
299
  }
307
300
 
301
+ /**
302
+ * Validates a single object against the application schema (no app-name resolution).
303
+ * Supports both flat root (key, displayName, ...) and nested shape (app: {...}).
304
+ * Used by diff and other callers that have already parsed the config.
305
+ *
306
+ * @function validateObjectAgainstApplicationSchema
307
+ * @param {Object} obj - Parsed config object (root or { app, deployment })
308
+ * @returns {{ valid: boolean, errors: string[] }} Validation result
309
+ *
310
+ * @example
311
+ * const result = validateObjectAgainstApplicationSchema(parsed);
312
+ * if (!result.valid) throw new Error(result.errors.join('; '));
313
+ */
314
+ function validateObjectAgainstApplicationSchema(obj) {
315
+ if (!obj || typeof obj !== 'object') {
316
+ return { valid: false, errors: ['Config must be an object'] };
317
+ }
318
+ const toValidate = obj.app && typeof obj.app === 'object' && !obj.key ? obj.app : obj;
319
+ const validate = setupAjvValidator();
320
+ const valid = validate(toValidate);
321
+ return {
322
+ valid: !!valid,
323
+ errors: valid ? [] : formatValidationErrors(validate.errors)
324
+ };
325
+ }
326
+
308
327
  /**
309
328
  * Validates deployment JSON against application schema
310
329
  * Ensures generated <app-name>-deploy.json matches the schema structure
@@ -360,14 +379,14 @@ function validateDeploymentJson(deployment) {
360
379
  * const result = await validateApplication('myapp');
361
380
  * // Returns: { valid: true, variables: {...}, rbac: {...}, env: {...} }
362
381
  */
363
- async function validateApplication(appName) {
382
+ async function validateApplication(appName, options = {}) {
364
383
  if (!appName || typeof appName !== 'string') {
365
384
  throw new Error('App name is required and must be a string');
366
385
  }
367
386
 
368
- const variables = await validateVariables(appName);
369
- const rbac = await validateRbac(appName);
370
- const env = await validateEnvTemplate(appName);
387
+ const variables = await validateVariables(appName, options);
388
+ const rbac = await validateRbac(appName, options);
389
+ const env = await validateEnvTemplate(appName, options);
371
390
 
372
391
  const valid = variables.valid && rbac.valid && env.valid;
373
392
  const errors = [...(variables.errors || []), ...(rbac.errors || []), ...(env.errors || [])];
@@ -392,6 +411,7 @@ module.exports = {
392
411
  validateRbac,
393
412
  validateEnvTemplate,
394
413
  validateDeploymentJson,
414
+ validateObjectAgainstApplicationSchema,
395
415
  checkEnvironment,
396
416
  formatValidationErrors,
397
417
  validateApplication
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.39.3",
3
+ "version": "2.40.2",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -16,11 +16,13 @@
16
16
  "test:integration": "jest --config jest.config.integration.js --runInBand",
17
17
  "test:integration:python": "cross-env TEST_LANGUAGE=python jest --config jest.config.integration.js --runInBand",
18
18
  "test:integration:typescript": "cross-env TEST_LANGUAGE=typescript jest --config jest.config.integration.js --runInBand",
19
+ "test:manual": "jest --config jest.config.manual.js --runInBand",
19
20
  "lint": "eslint . --ext .js",
20
21
  "lint:fix": "eslint . --ext .js --fix",
21
22
  "lint:ci": "eslint . --ext .js --format json --output-file eslint-report.json",
22
23
  "dev": "node bin/aifabrix.js",
23
24
  "build": "npm run lint && npm run test",
25
+ "build:ci": "npm run lint && npm run test:ci",
24
26
  "pack": "npm run build && npm pack",
25
27
  "validate": "npm run build",
26
28
  "prepublishOnly": "npm run validate",
@@ -78,6 +80,7 @@
78
80
  "ora": "^5.4.1"
79
81
  },
80
82
  "devDependencies": {
83
+ "@babel/preset-env": "^7.29.0",
81
84
  "@bcoe/v8-coverage": "^1.0.2",
82
85
  "@types/node": "^20.10.0",
83
86
  "cross-env": "^10.1.0",
@@ -106,7 +106,7 @@ Create your own step templates in `templates/github/steps/{your-step}.hbs` and r
106
106
 
107
107
  ### Language and Infrastructure Templates
108
108
 
109
- These templates are processed automatically by the AI Fabrix Builder SDK based on the application schema defined in `variables.yaml`. The generated files will be valid Docker files after template processing.
109
+ These templates are processed automatically by the AI Fabrix Builder SDK based on the application schema defined in `application.yaml`. The generated files will be valid Docker files after template processing.
110
110
 
111
111
  ## VS Code Configuration
112
112
 
@@ -61,7 +61,7 @@ aifabrix build {{appName}} --tag v1.0.0
61
61
  aifabrix push {{appName}} --registry {{registry}} --tag "v1.0.0,latest"
62
62
 
63
63
  # Deploy (controller and environment from config; set via aifabrix login or aifabrix auth config)
64
- aifabrix deploy {{appName}} --deployment local
64
+ aifabrix deploy {{appName}} --local
65
65
  ```
66
66
 
67
67
  ---
@@ -132,7 +132,7 @@ aifabrix push {{appName}} --registry {{registry}} --tag "v1.0.0,latest,stable"
132
132
  ### Deploy Options
133
133
 
134
134
  ```bash
135
- aifabrix deploy {{appName}} --deployment local # Uses controller and environment from config
135
+ aifabrix deploy {{appName}} --local # Uses controller and environment from config
136
136
  aifabrix deploy {{appName}} --no-poll # Deploy without polling for status
137
137
  ```
138
138
 
@@ -166,7 +166,7 @@ Controller URL and environment (for `deploy`, `app register`, etc.) are set via
166
166
 
167
167
  - **"Docker not running"** → Start Docker Desktop
168
168
  - **"Not logged in"** → Run `aifabrix login` first
169
- - **"Port already in use"** → Use `aifabrix run {{appName}} --port <port>` or set `build.localPort` in `variables.yaml` (default: {{localPort}})
169
+ - **"Port already in use"** → Use `aifabrix run {{appName}} --port <port>` or set `build.localPort` in `application.yaml` (default: {{localPort}})
170
170
  - **"Authentication failed"** → Run `aifabrix login` again
171
171
  - **"Build fails"** → Check Docker is running and `aifabrix-secrets` in `config.yaml` is configured correctly
172
172
  - **"Can't connect"** → Verify infrastructure is running{{#if hasDatabase}} and PostgreSQL is accessible{{/if}}
@@ -105,7 +105,9 @@ REDIS_PERMISSIONS_TTL=900
105
105
  # (KeycloakConfiguration + Application url/internalUrl). Sync env->DB at startup via
106
106
  # sync-application-urls-from-env.service.
107
107
  #
108
- # NOTE: Do NOT onboard Azure Entra SSO in Keycloak during onboarding (skipAzureEntraSso=true).
108
+ # Azure Entra SSO during onboarding: true = skip (default), false = onboard Azure Entra SSO client in Keycloak
109
+ # (onboarding Azure SSO requires Entra admin consent and Azure app credentials).
110
+ KEYCLOAK_SKIP_AZURE_ENTRA_SSO=false
109
111
 
110
112
  KEYCLOAK_REALM=aifabrix
111
113
  KEYCLOAK_SERVER_URL=kv://keycloak-server-urlKeyVault
@@ -10,8 +10,8 @@
10
10
 
11
11
  ## Files
12
12
 
13
- - `variables.yaml` – Application configuration with `app` and `externalIntegration` blocks
14
- - `{{systemKey}}-system.json` – External system definition (authentication, OpenAPI/MCP, RBAC)
13
+ - `application.yaml` – Application configuration with `app` and `externalIntegration` blocks
14
+ - `{{systemKey}}-system.yaml` – External system definition (authentication, OpenAPI/MCP, RBAC)
15
15
  {{#each datasources}}
16
16
  - `{{fileName}}` – Datasource: {{displayName}}
17
17
  {{/each}}
@@ -38,8 +38,8 @@ aifabrix wizard --app {{appName}}
38
38
 
39
39
  Edit files in `integration/{{appName}}/`:
40
40
 
41
- - **Authentication**: `{{systemKey}}-system.json` (auth type, credentials placeholders)
42
- - **Field mappings**: `{{systemKey}}-datasource-*.json` (dimensions, attributes, operations)
41
+ - **Authentication**: `{{systemKey}}-system.yaml` (auth type, credentials placeholders)
42
+ - **Field mappings**: `{{systemKey}}-datasource-*.yaml` (dimensions, attributes, operations)
43
43
 
44
44
  ### 3. Validate Configuration
45
45