@aifabrix/builder 2.39.2 → 2.40.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 (116) hide show
  1. package/.cursor/rules/project-rules.mdc +6 -6
  2. package/README.md +2 -2
  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/lib/api/credentials.api.js +5 -5
  10. package/lib/api/deployments.api.js +2 -2
  11. package/lib/api/pipeline.api.js +17 -17
  12. package/lib/api/wizard.api.js +2 -2
  13. package/lib/app/config.js +11 -6
  14. package/lib/app/deploy-config.js +13 -16
  15. package/lib/app/deploy.js +29 -22
  16. package/lib/app/display.js +1 -1
  17. package/lib/app/dockerfile.js +11 -12
  18. package/lib/app/helpers.js +51 -13
  19. package/lib/app/index.js +14 -2
  20. package/lib/app/prompts.js +37 -45
  21. package/lib/app/push.js +8 -11
  22. package/lib/app/readme.js +16 -12
  23. package/lib/app/register.js +3 -3
  24. package/lib/app/run-helpers.js +31 -22
  25. package/lib/app/run.js +44 -5
  26. package/lib/app/show-display.js +104 -44
  27. package/lib/app/show.js +123 -43
  28. package/lib/build/index.js +11 -18
  29. package/lib/cli/setup-app.js +38 -28
  30. package/lib/cli/setup-auth.js +18 -15
  31. package/lib/cli/setup-credential-deployment.js +3 -1
  32. package/lib/cli/setup-external-system.js +35 -16
  33. package/lib/cli/setup-infra.js +45 -23
  34. package/lib/cli/setup-utility.js +79 -31
  35. package/lib/commands/app-logs.js +165 -10
  36. package/lib/commands/app.js +30 -26
  37. package/lib/commands/convert.js +202 -0
  38. package/lib/commands/credential-list.js +78 -17
  39. package/lib/commands/datasource.js +24 -24
  40. package/lib/commands/deployment-list.js +13 -6
  41. package/lib/commands/up-common.js +80 -42
  42. package/lib/commands/up-dataplane.js +15 -14
  43. package/lib/commands/up-miso.js +15 -14
  44. package/lib/commands/upload.js +163 -0
  45. package/lib/commands/wizard-core.js +5 -4
  46. package/lib/core/diff.js +84 -9
  47. package/lib/core/key-generator.js +9 -12
  48. package/lib/core/secrets-docker-env.js +2 -2
  49. package/lib/core/secrets.js +3 -2
  50. package/lib/core/templates.js +2 -2
  51. package/lib/datasource/deploy.js +2 -1
  52. package/lib/deployment/deployer.js +76 -48
  53. package/lib/external-system/delete.js +0 -1
  54. package/lib/external-system/deploy-helpers.js +5 -6
  55. package/lib/external-system/deploy.js +7 -2
  56. package/lib/external-system/download-helpers.js +4 -4
  57. package/lib/external-system/download.js +11 -10
  58. package/lib/external-system/generator.js +19 -17
  59. package/lib/external-system/test.js +10 -15
  60. package/lib/generator/builders.js +1 -1
  61. package/lib/generator/external-controller-manifest.js +26 -29
  62. package/lib/generator/external-schema-utils.js +6 -18
  63. package/lib/generator/external.js +32 -27
  64. package/lib/generator/github.js +1 -1
  65. package/lib/generator/helpers.js +12 -19
  66. package/lib/generator/index.js +15 -15
  67. package/lib/generator/parse-image.js +35 -0
  68. package/lib/generator/split-readme.js +105 -0
  69. package/lib/generator/split-variables.js +149 -0
  70. package/lib/generator/split.js +86 -246
  71. package/lib/generator/wizard.js +46 -69
  72. package/lib/schema/application-schema.json +4 -4
  73. package/lib/schema/deployment-rules.yaml +0 -4
  74. package/lib/schema/external-datasource.schema.json +5 -0
  75. package/lib/schema/external-system.schema.json +10 -0
  76. package/lib/utils/app-config-resolver.js +52 -0
  77. package/lib/utils/app-register-api.js +1 -1
  78. package/lib/utils/app-register-auth.js +1 -1
  79. package/lib/utils/app-register-config.js +16 -23
  80. package/lib/utils/app-register-display.js +22 -3
  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 +39 -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 +33 -27
  107. package/lib/validation/validator.js +50 -30
  108. package/package.json +2 -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/integration/hubspot/variables.yaml +0 -17
  114. /package/templates/applications/dataplane/{variables.yaml → application.yaml} +0 -0
  115. /package/templates/applications/keycloak/{variables.yaml → application.yaml} +0 -0
  116. /package/templates/applications/miso-controller/{variables.yaml → application.yaml} +0 -0
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Upload external system to dataplane (upload → validate → publish).
3
+ *
4
+ * @fileoverview Upload command handler for aifabrix upload <system-key>
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const chalk = require('chalk');
10
+ const logger = require('../utils/logger');
11
+ const { resolveControllerUrl } = require('../utils/controller-url');
12
+ const { getDeploymentAuth, requireBearerForDataplanePipeline } = require('../utils/token-manager');
13
+ const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
14
+ const { validateExternalSystemComplete } = require('../validation/validate');
15
+ const { displayValidationResults } = require('../validation/validate-display');
16
+ const { generateControllerManifest } = require('../generator/external-controller-manifest');
17
+ const {
18
+ uploadApplicationViaPipeline,
19
+ validateUploadViaPipeline,
20
+ publishUploadViaPipeline
21
+ } = require('../api/pipeline.api');
22
+ const { formatApiError } = require('../utils/api-error-handler');
23
+
24
+ /**
25
+ * Validates system-key format (same as download).
26
+ * @param {string} systemKey - System key
27
+ * @throws {Error} If invalid
28
+ */
29
+ function validateSystemKeyFormat(systemKey) {
30
+ if (!systemKey || typeof systemKey !== 'string') {
31
+ throw new Error('System key is required and must be a string');
32
+ }
33
+ if (!/^[a-z0-9-_]+$/.test(systemKey)) {
34
+ throw new Error('System key must contain only lowercase letters, numbers, hyphens, and underscores');
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Builds pipeline upload payload from controller manifest.
40
+ * Payload: { version, application, dataSources }; application = system with RBAC.
41
+ * @param {Object} manifest - Controller manifest from generateControllerManifest
42
+ * @returns {Object} { version, application, dataSources }
43
+ */
44
+ function buildUploadPayload(manifest) {
45
+ return {
46
+ version: manifest.version || '1.0.0',
47
+ application: manifest.system,
48
+ dataSources: manifest.dataSources || []
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Resolves dataplane URL and auth (same pattern as download).
54
+ * @param {string} systemKey - System key
55
+ * @param {Object} options - Options with optional dataplane override
56
+ * @returns {Promise<{ dataplaneUrl: string, authConfig: Object, environment: string }>}
57
+ */
58
+ async function resolveDataplaneAndAuth(systemKey, options) {
59
+ const { resolveEnvironment } = require('../core/config');
60
+ const environment = await resolveEnvironment();
61
+ const controllerUrl = await resolveControllerUrl();
62
+ const authConfig = await getDeploymentAuth(controllerUrl, environment, systemKey);
63
+
64
+ if (!authConfig.token && !authConfig.clientId) {
65
+ throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register <system-key>" first.');
66
+ }
67
+
68
+ let dataplaneUrl;
69
+ if (options.dataplane) {
70
+ dataplaneUrl = options.dataplane.replace(/\/$/, '');
71
+ } else {
72
+ logger.log(chalk.blue('Resolving dataplane URL...'));
73
+ dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
74
+ }
75
+
76
+ return { dataplaneUrl, authConfig, environment };
77
+ }
78
+
79
+ /**
80
+ * Runs upload → validate → publish on the dataplane.
81
+ * @param {string} dataplaneUrl - Dataplane base URL
82
+ * @param {Object} authConfig - Auth config
83
+ * @param {Object} payload - { version, application, dataSources }
84
+ * @returns {Promise<{ uploadId: string }>}
85
+ */
86
+ async function runUploadValidatePublish(dataplaneUrl, authConfig, payload) {
87
+ const uploadRes = await uploadApplicationViaPipeline(dataplaneUrl, authConfig, payload);
88
+ const uploadId = uploadRes?.data?.uploadId ?? uploadRes?.data?.id ?? uploadRes?.uploadId;
89
+ if (!uploadId) {
90
+ const msg = uploadRes?.success === false
91
+ ? formatApiError(uploadRes, dataplaneUrl)
92
+ : 'Upload did not return an upload ID';
93
+ throw new Error(msg);
94
+ }
95
+
96
+ const validateRes = await validateUploadViaPipeline(dataplaneUrl, uploadId, authConfig);
97
+ if (validateRes?.success === false) {
98
+ const msg = formatApiError(validateRes, dataplaneUrl);
99
+ throw new Error(`Upload validation failed: ${msg}`);
100
+ }
101
+
102
+ await publishUploadViaPipeline(dataplaneUrl, uploadId, authConfig);
103
+ return { uploadId };
104
+ }
105
+
106
+ /**
107
+ * Throws if validation result is invalid (displays results first).
108
+ * @param {Object} validationResult - Result from validateExternalSystemComplete
109
+ * @throws {Error} If validationResult.valid is false
110
+ */
111
+ function throwIfValidationFailed(validationResult) {
112
+ if (!validationResult.valid) {
113
+ displayValidationResults(validationResult);
114
+ throw new Error('Validation failed. Fix errors before uploading.');
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Uploads external system to dataplane (upload → validate → publish). No controller deploy.
120
+ * @param {string} systemKey - External system key (integration/<system-key>/)
121
+ * @param {Object} [options] - Options
122
+ * @param {boolean} [options.dryRun] - Validate and build payload only; no API calls
123
+ * @param {string} [options.dataplane] - Override dataplane URL
124
+ * @returns {Promise<void>}
125
+ * @throws {Error} If validation or API calls fail
126
+ */
127
+ async function uploadExternalSystem(systemKey, options = {}) {
128
+ validateSystemKeyFormat(systemKey);
129
+
130
+ logger.log(chalk.blue(`\nUploading external system to dataplane: ${systemKey}`));
131
+
132
+ const validationResult = await validateExternalSystemComplete(systemKey, { type: 'external' });
133
+ throwIfValidationFailed(validationResult);
134
+ logger.log(chalk.green('Validation passed.'));
135
+
136
+ const manifest = await generateControllerManifest(systemKey, { type: 'external' });
137
+ const payload = buildUploadPayload(manifest);
138
+
139
+ if (options.dryRun) {
140
+ logger.log(chalk.yellow('Dry run: would upload payload (no API calls).'));
141
+ logger.log(chalk.gray(` System: ${manifest.key}, version: ${payload.version}, datasources: ${payload.dataSources.length}`));
142
+ return;
143
+ }
144
+
145
+ const { dataplaneUrl, authConfig, environment } = await resolveDataplaneAndAuth(systemKey, options);
146
+ requireBearerForDataplanePipeline(authConfig);
147
+ logger.log(chalk.blue(`Dataplane: ${dataplaneUrl}`));
148
+
149
+ await runUploadValidatePublish(dataplaneUrl, authConfig, payload);
150
+
151
+ logger.log(chalk.green('\nUpload validated and published to dataplane.'));
152
+ logger.log(chalk.blue(`Environment: ${environment}`));
153
+ logger.log(chalk.blue(`System: ${systemKey}`));
154
+ logger.log(chalk.blue(`Dataplane: ${dataplaneUrl}`));
155
+ }
156
+
157
+ module.exports = {
158
+ uploadExternalSystem,
159
+ buildUploadPayload,
160
+ resolveDataplaneAndAuth,
161
+ runUploadValidatePublish,
162
+ validateSystemKeyFormat
163
+ };
@@ -339,7 +339,7 @@ async function validateWizardConfiguration(dataplaneUrl, authConfig, systemConfi
339
339
  }
340
340
 
341
341
  /**
342
- * Fetches deployment docs and writes README.md when variables.yaml and deploy JSON are available.
342
+ * Fetches deployment docs and writes README.md when application config and deploy JSON are available.
343
343
  * @async
344
344
  * @param {string} appPath - Application path
345
345
  * @param {string} appName - Application name
@@ -348,12 +348,13 @@ async function validateWizardConfiguration(dataplaneUrl, authConfig, systemConfi
348
348
  * @param {string} systemKey - System key
349
349
  */
350
350
  async function tryUpdateReadmeFromDeploymentDocs(appPath, appName, dataplaneUrl, authConfig, systemKey) {
351
- const variablesPath = path.join(appPath, 'variables.yaml');
351
+ const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
352
352
  const deployPath = path.join(appPath, `${appName}-deploy.json`);
353
353
  let variablesYaml = null;
354
354
  let deployJson = null;
355
355
  try {
356
- variablesYaml = await fs.readFile(variablesPath, 'utf8');
356
+ const configPath = resolveApplicationConfigPath(appPath);
357
+ variablesYaml = await fs.readFile(configPath, 'utf8');
357
358
  } catch {
358
359
  // optional
359
360
  }
@@ -372,7 +373,7 @@ async function tryUpdateReadmeFromDeploymentDocs(appPath, appName, dataplaneUrl,
372
373
  if (content && typeof content === 'string') {
373
374
  const readmePath = path.join(appPath, 'README.md');
374
375
  await fs.writeFile(readmePath, content, 'utf8');
375
- logger.log(chalk.gray(' Updated README.md from deployment-docs API (variables.yaml + deploy JSON).'));
376
+ logger.log(chalk.gray(' Updated README.md from deployment-docs API (application config + deploy JSON).'));
376
377
  }
377
378
  }
378
379
 
package/lib/core/diff.js CHANGED
@@ -13,6 +13,10 @@ const fs = require('fs');
13
13
  const path = require('path');
14
14
  const chalk = require('chalk');
15
15
  const logger = require('../utils/logger');
16
+ const { loadConfigFile } = require('../utils/config-format');
17
+ const { detectSchemaTypeFromParsed, loadExternalSystemSchema, loadExternalDataSourceSchema } = require('../utils/schema-loader');
18
+ const { validateObjectAgainstApplicationSchema } = require('../validation/validator');
19
+ const { formatValidationErrors } = require('../utils/error-formatter');
16
20
 
17
21
  /**
18
22
  * Handle added field in comparison
@@ -169,19 +173,22 @@ function identifyBreakingChanges(comparison) {
169
173
  }
170
174
 
171
175
  /**
172
- * Compares two configuration files
173
- * Loads files, parses JSON, and performs deep comparison
176
+ * Compares two configuration files.
177
+ * Both files must be the same config type (app, system, or datasource).
178
+ * By default validates both against their schema; pass { validate: false } to skip.
174
179
  *
175
180
  * @async
176
181
  * @function compareFiles
177
182
  * @param {string} file1 - Path to first file
178
183
  * @param {string} file2 - Path to second file
184
+ * @param {Object} [options] - Options
185
+ * @param {boolean} [options.validate=true] - If true, validate both files against their schema
179
186
  * @returns {Promise<Object>} Comparison result with differences
180
- * @throws {Error} If files cannot be read or parsed
187
+ * @throws {Error} If files cannot be read, parsed, types differ, or validation fails
181
188
  *
182
189
  * @example
183
190
  * const result = await compareFiles('./old.json', './new.json');
184
- * // Returns: { identical: false, added: [...], removed: [...], changed: [...] }
191
+ * const resultNoValidate = await compareFiles('./a.yaml', './b.yaml', { validate: false });
185
192
  */
186
193
  /**
187
194
  * Validates file paths
@@ -206,21 +213,70 @@ function validateFilePaths(file1, file2) {
206
213
  }
207
214
 
208
215
  /**
209
- * Reads and parses a JSON file
216
+ * Reads and parses a config file (JSON or YAML by extension: .json, .yaml, .yml).
210
217
  * @function readAndParseFile
211
218
  * @param {string} filePath - File path
212
- * @returns {Object} Parsed JSON object
219
+ * @returns {Object} Parsed object
213
220
  * @throws {Error} If file cannot be read or parsed
214
221
  */
215
222
  function readAndParseFile(filePath) {
216
223
  try {
217
- const content = fs.readFileSync(filePath, 'utf8');
218
- return JSON.parse(content);
224
+ return loadConfigFile(filePath);
219
225
  } catch (error) {
220
226
  throw new Error(`Failed to parse ${filePath}: ${error.message}`);
221
227
  }
222
228
  }
223
229
 
230
+ /**
231
+ * Maps schema type to user-facing label (app | system | datasource).
232
+ * @param {string} schemaType - 'application' | 'external-system' | 'external-datasource'
233
+ * @returns {string} 'app' | 'system' | 'datasource'
234
+ */
235
+ function toUserFacingType(schemaType) {
236
+ const map = {
237
+ application: 'app',
238
+ 'external-system': 'system',
239
+ 'external-datasource': 'datasource'
240
+ };
241
+ return map[schemaType] || 'app';
242
+ }
243
+
244
+ /**
245
+ * Runs external schema validator and returns error messages or null.
246
+ * @param {Function} validateFn - AJV validate function
247
+ * @param {Object} parsed - Parsed config object
248
+ * @returns {string[]|null} Error messages or null if valid
249
+ */
250
+ function getValidationErrors(validateFn, parsed) {
251
+ const valid = validateFn(parsed);
252
+ if (valid) return null;
253
+ return formatValidationErrors(validateFn.errors);
254
+ }
255
+
256
+ /**
257
+ * Validates parsed object against the schema for the given type.
258
+ * @param {Object} parsed - Parsed config object
259
+ * @param {string} schemaType - 'application' | 'external-system' | 'external-datasource'
260
+ * @param {string} filePath - File path (for error messages)
261
+ * @throws {Error} If validation fails
262
+ */
263
+ function validateParsedForType(parsed, schemaType, filePath) {
264
+ let messages = [];
265
+ if (schemaType === 'application') {
266
+ const result = validateObjectAgainstApplicationSchema(parsed);
267
+ if (!result.valid) messages = result.errors;
268
+ } else if (schemaType === 'external-system') {
269
+ messages = getValidationErrors(loadExternalSystemSchema(), parsed) || [];
270
+ } else if (schemaType === 'external-datasource') {
271
+ messages = getValidationErrors(loadExternalDataSourceSchema(), parsed) || [];
272
+ } else {
273
+ throw new Error(`Unknown schema type: ${schemaType}`);
274
+ }
275
+ if (messages.length > 0) {
276
+ throw new Error(`Validation failed for ${filePath}: ${messages.join('; ')}`);
277
+ }
278
+ }
279
+
224
280
  /**
225
281
  * Extracts version from parsed object
226
282
  * @function extractVersion
@@ -267,12 +323,31 @@ function buildComparisonResult(comparison, parsed1, parsed2, file1, file2) {
267
323
  };
268
324
  }
269
325
 
270
- async function compareFiles(file1, file2) {
326
+ async function compareFiles(file1, file2, options = {}) {
327
+ const shouldValidate = options.validate !== false;
328
+
271
329
  validateFilePaths(file1, file2);
272
330
 
273
331
  const parsed1 = readAndParseFile(file1);
274
332
  const parsed2 = readAndParseFile(file2);
275
333
 
334
+ const type1 = detectSchemaTypeFromParsed(parsed1, file1);
335
+ const type2 = detectSchemaTypeFromParsed(parsed2, file2);
336
+ const userType1 = toUserFacingType(type1);
337
+ const userType2 = toUserFacingType(type2);
338
+
339
+ if (userType1 !== userType2) {
340
+ throw new Error(
341
+ `Type mismatch: ${file1} is ${userType1} config and ${file2} is ${userType2} config. ` +
342
+ 'Both files must be the same type (app, system, or datasource).'
343
+ );
344
+ }
345
+
346
+ if (shouldValidate) {
347
+ validateParsedForType(parsed1, type1, file1);
348
+ validateParsedForType(parsed2, type2, file2);
349
+ }
350
+
276
351
  const comparison = compareObjects(parsed1, parsed2);
277
352
  return buildComparisonResult(comparison, parsed1, parsed2, file1, file2);
278
353
  }
@@ -2,7 +2,7 @@
2
2
  * AI Fabrix Builder Deployment Key Generator
3
3
  *
4
4
  * This module generates SHA256-based deployment keys for controller authentication.
5
- * Keys are computed from variables.yaml content to ensure deployment integrity.
5
+ * Keys are computed from application config content to ensure deployment integrity.
6
6
  *
7
7
  * @fileoverview Deployment key generation for AI Fabrix Builder
8
8
  * @author AI Fabrix Team
@@ -14,14 +14,14 @@ const fs = require('fs');
14
14
  const path = require('path');
15
15
 
16
16
  /**
17
- * Generates deployment key from variables.yaml content
17
+ * Generates deployment key from application config content
18
18
  * Creates SHA256 hash for controller authentication and deployment integrity
19
19
  *
20
20
  * @async
21
21
  * @function generateDeploymentKey
22
22
  * @param {string} appName - Name of the application
23
- * @returns {Promise<string>} SHA256 hash of variables.yaml content
24
- * @throws {Error} If variables.yaml cannot be read
23
+ * @returns {Promise<string>} SHA256 hash of application config content
24
+ * @throws {Error} If application config cannot be read
25
25
  *
26
26
  * @example
27
27
  * const key = await generateDeploymentKey('myapp');
@@ -32,22 +32,19 @@ async function generateDeploymentKey(appName) {
32
32
  throw new Error('App name is required and must be a string');
33
33
  }
34
34
 
35
- const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
36
-
37
- if (!fs.existsSync(variablesPath)) {
38
- throw new Error(`variables.yaml not found: ${variablesPath}`);
39
- }
40
-
35
+ const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
36
+ const builderPath = path.join(process.cwd(), 'builder', appName);
37
+ const variablesPath = resolveApplicationConfigPath(builderPath);
41
38
  const content = fs.readFileSync(variablesPath, 'utf8');
42
39
  return generateDeploymentKeyFromContent(content);
43
40
  }
44
41
 
45
42
  /**
46
- * Generates deployment key from raw variables.yaml content
43
+ * Generates deployment key from raw application config content
47
44
  * Useful for testing or when content is already loaded
48
45
  *
49
46
  * @function generateDeploymentKeyFromContent
50
- * @param {string} content - Raw variables.yaml content
47
+ * @param {string} content - Raw application config content
51
48
  * @returns {string} SHA256 hash of content
52
49
  *
53
50
  * @example
@@ -60,12 +60,12 @@ function getContainerPortFromDockerEnv(dockerEnv) {
60
60
 
61
61
  /**
62
62
  * Updates PORT in resolved content for docker environment
63
- * Sets PORT to container port (build.containerPort or port from variables.yaml)
63
+ * Sets PORT to container port (build.containerPort or port from application config)
64
64
  * NOT the host port (which includes developer-id offset)
65
65
  * @async
66
66
  * @function updatePortForDocker
67
67
  * @param {string} resolved - Resolved environment content
68
- * @param {string} variablesPath - Path to variables.yaml file
68
+ * @param {string} variablesPath - Path to application config file
69
69
  * @returns {Promise<string>} Updated content with PORT set
70
70
  */
71
71
  async function updatePortForDocker(resolved, variablesPath) {
@@ -12,6 +12,7 @@
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
14
  const logger = require('../utils/logger');
15
+ const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
15
16
  const config = require('./config');
16
17
  const {
17
18
  interpolateEnvVars,
@@ -288,7 +289,7 @@ async function applyEnvironmentTransformations(resolved, environment, variablesP
288
289
  async function generateEnvContent(appName, secretsPath, environment = 'local', force = false) {
289
290
  const builderPath = pathsUtil.getBuilderPath(appName);
290
291
  const templatePath = path.join(builderPath, 'env.template');
291
- const variablesPath = path.join(builderPath, 'variables.yaml');
292
+ const variablesPath = resolveApplicationConfigPath(builderPath);
292
293
  const template = loadEnvTemplate(templatePath);
293
294
  const secretsPaths = await getActualSecretsPath(secretsPath, appName);
294
295
  if (force) {
@@ -391,7 +392,7 @@ function mergeEnvContentPreservingExisting(newContent, existingMap) {
391
392
  */
392
393
  async function generateEnvFile(appName, secretsPath, environment = 'local', force = false, skipOutputPath = false, preserveFromPath = null) {
393
394
  const builderPath = pathsUtil.getBuilderPath(appName);
394
- const variablesPath = path.join(builderPath, 'variables.yaml');
395
+ const variablesPath = resolveApplicationConfigPath(builderPath);
395
396
  const envPath = path.join(builderPath, '.env');
396
397
 
397
398
  const resolved = await generateEnvContent(appName, secretsPath, environment, force);
@@ -8,7 +8,7 @@
8
8
  const yaml = require('js-yaml');
9
9
 
10
10
  /**
11
- * Generate variables.yaml content for an application
11
+ * Generate application.yaml content for an application
12
12
  * Matches application-schema.json structure
13
13
  * @param {string} appName - Application name
14
14
  * @param {Object} config - Configuration options
@@ -166,7 +166,7 @@ function generateVariablesYaml(appName, config) {
166
166
  const displayName = appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
167
167
  const appType = config.type || 'webapp';
168
168
 
169
- // For external type, create minimal variables.yaml
169
+ // For external type, create minimal application config
170
170
  if (appType === 'external') {
171
171
  const variables = generateExternalSystemVariables(appName, displayName, config);
172
172
  return dumpVariablesToYaml(variables);
@@ -11,7 +11,7 @@
11
11
 
12
12
  const fs = require('fs');
13
13
  const chalk = require('chalk');
14
- const { getDeploymentAuth } = require('../utils/token-manager');
14
+ const { getDeploymentAuth, requireBearerForDataplanePipeline } = require('../utils/token-manager');
15
15
  const { getEnvironmentApplication } = require('../api/environments.api');
16
16
  const { publishDatasourceViaPipeline } = require('../api/pipeline.api');
17
17
  const { formatApiError } = require('../utils/api-error-handler');
@@ -152,6 +152,7 @@ async function setupDeploymentAuth(controllerUrl, environment, appKey) {
152
152
  * @throws {Error} If publish fails
153
153
  */
154
154
  async function publishDatasourceToDataplane(dataplaneUrl, systemKey, authConfig, datasourceConfig) {
155
+ requireBearerForDataplanePipeline(authConfig);
155
156
  logger.log(chalk.blue('\n🚀 Publishing datasource to dataplane...'));
156
157
 
157
158
  const publishResponse = await publishDatasourceViaPipeline(dataplaneUrl, systemKey, authConfig, datasourceConfig);
@@ -15,6 +15,7 @@ const logger = require('../utils/logger');
15
15
  const { validateControllerUrl, validateEnvironmentKey } = require('../utils/deployment-validation');
16
16
  const { handleDeploymentError, handleDeploymentErrors } = require('../utils/deployment-errors');
17
17
  const { validatePipeline, deployPipeline, getPipelineDeployment } = require('../api/pipeline.api');
18
+ const { getAuthUser } = require('../api/auth.api');
18
19
  const { handleValidationResponse } = require('../utils/deployment-validation-helpers');
19
20
  const {
20
21
  convertToPipelineAuthConfig,
@@ -36,73 +37,72 @@ function transformExternalManifestForPipeline(manifest) {
36
37
 
37
38
  /**
38
39
  * Build validation data for deployment
40
+ * When authenticated with bearer token only, clientId/clientSecret are not sent (controller uses token).
39
41
  * @async
40
42
  * @param {Object} manifest - Application manifest/config
41
43
  * @param {string} validatedEnvKey - Validated environment key
42
44
  * @param {Object} authConfig - Authentication configuration
43
45
  * @param {Object} options - Additional options
44
- * @returns {Promise<Object>} Object with validationData and pipelineAuthConfig
46
+ * @returns {Promise<Object>} Object with validationData, pipelineAuthConfig, and useBearerOnly
45
47
  */
46
48
  async function buildValidationData(manifest, validatedEnvKey, authConfig, options) {
47
- const tokenManager = require('../utils/token-manager');
48
- let clientId;
49
- let clientSecret;
50
- let pipelineAuthConfig;
49
+ const repositoryUrl = options.repositoryUrl || `https://github.com/aifabrix/${manifest.key}`;
51
50
 
52
- try {
53
- const credentials = await tokenManager.extractClientCredentials(
54
- authConfig,
55
- manifest.key,
56
- validatedEnvKey,
57
- options
58
- );
59
- clientId = credentials.clientId;
60
- clientSecret = credentials.clientSecret;
61
- pipelineAuthConfig = {
62
- type: 'client-credentials',
63
- clientId,
64
- clientSecret
51
+ if (authConfig.type === 'bearer' && authConfig.token && !authConfig.clientId) {
52
+ const pipelineAuthConfig = { type: 'bearer', token: authConfig.token };
53
+ const validationData = {
54
+ clientId: manifest.key,
55
+ repositoryUrl,
56
+ applicationConfig: manifest
65
57
  };
66
- } catch (credError) {
67
- if (authConfig.type === 'bearer' && authConfig.token) {
68
- pipelineAuthConfig = { type: 'bearer', token: authConfig.token };
69
- clientId = manifest.key;
70
- clientSecret = '';
71
- } else {
72
- throw credError;
73
- }
58
+ return { validationData, pipelineAuthConfig, useBearerOnly: true };
74
59
  }
75
60
 
76
- const repositoryUrl = options.repositoryUrl || `https://github.com/aifabrix/${manifest.key}`;
61
+ const tokenManager = require('../utils/token-manager');
62
+ const credentials = await tokenManager.extractClientCredentials(
63
+ authConfig,
64
+ manifest.key,
65
+ validatedEnvKey,
66
+ options
67
+ );
68
+ const pipelineAuthConfig = {
69
+ type: 'client-credentials',
70
+ clientId: credentials.clientId,
71
+ clientSecret: credentials.clientSecret
72
+ };
77
73
  const validationData = {
78
- clientId: clientId || '',
79
- clientSecret: clientSecret || '',
80
- repositoryUrl: repositoryUrl,
74
+ clientId: credentials.clientId || '',
75
+ clientSecret: credentials.clientSecret || '',
76
+ repositoryUrl,
81
77
  applicationConfig: manifest
82
78
  };
83
-
84
- return { validationData, pipelineAuthConfig };
79
+ return { validationData, pipelineAuthConfig, useBearerOnly: false };
85
80
  }
86
81
 
87
82
  /**
88
83
  * Handle authentication errors during validation
89
84
  * @param {Error} error - Error object
90
85
  * @param {string} appKey - Application key
86
+ * @param {boolean} [useBearerOnly] - True when auth was bearer token only (no client id/secret sent)
91
87
  * @throws {Error} Enhanced authentication error
92
88
  */
93
- function handleValidationAuthError(error, appKey) {
94
- if (error.status === 401 || (error.response && error.response.status === 401)) {
95
- const authError = new Error(
96
- `Authentication failed: Invalid or expired credentials for application '${appKey}'.\n` +
97
- 'The provided Client ID and Client Secret are incorrect or have been revoked.\n\n' +
98
- '💡 If the application already exists, rotate the secret:\n' +
99
- ` aifabrix app rotate-secret ${appKey}\n\n` +
100
- '💡 Otherwise, ensure credentials are correct in ~/.aifabrix/secrets.local.yaml or use --client-id and --client-secret flags.'
101
- );
102
- authError.status = 401;
103
- authError.originalError = error;
104
- throw authError;
89
+ function handleValidationAuthError(error, appKey, useBearerOnly) {
90
+ if (error.status !== 401 && (!error.response || error.response.status !== 401)) {
91
+ return;
105
92
  }
93
+ const authError = new Error(
94
+ useBearerOnly
95
+ ? 'Authentication failed: Your authentication token is invalid or expired.\n\n' +
96
+ 'To authenticate, run:\n aifabrix login --method device --controller <url>'
97
+ : `Authentication failed: Invalid or expired credentials for application '${appKey}'.\n` +
98
+ 'The provided Client ID and Client Secret are incorrect or have been revoked.\n\n' +
99
+ '💡 If the application already exists, rotate the secret:\n' +
100
+ ` aifabrix app rotate-secret ${appKey}\n\n` +
101
+ '💡 Otherwise, ensure credentials are correct in ~/.aifabrix/secrets.local.yaml or use --client-id and --client-secret flags.'
102
+ );
103
+ authError.status = 401;
104
+ authError.originalError = error;
105
+ throw authError;
106
106
  }
107
107
 
108
108
  /**
@@ -122,8 +122,8 @@ async function validateDeployment(url, envKey, manifest, authConfig, options = {
122
122
  const validatedEnvKey = validateEnvironmentKey(envKey);
123
123
  const maxRetries = options.maxRetries || 3;
124
124
 
125
- // Build validation data
126
- const { validationData, pipelineAuthConfig } = await buildValidationData(manifest, validatedEnvKey, authConfig, options);
125
+ // Build validation data (bearer-only: no clientId/clientSecret sent)
126
+ const { validationData, pipelineAuthConfig, useBearerOnly } = await buildValidationData(manifest, validatedEnvKey, authConfig, options);
127
127
 
128
128
  let lastError;
129
129
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
@@ -133,8 +133,8 @@ async function validateDeployment(url, envKey, manifest, authConfig, options = {
133
133
  } catch (error) {
134
134
  lastError = error;
135
135
 
136
- // Handle authentication errors (401) - credentials are invalid, not missing
137
- handleValidationAuthError(error, manifest.key);
136
+ // Handle authentication errors (401)
137
+ handleValidationAuthError(error, manifest.key, useBearerOnly);
138
138
 
139
139
  const shouldRetry = attempt < maxRetries && error.status && error.status >= 500;
140
140
  if (shouldRetry) {
@@ -310,6 +310,32 @@ async function pollDeploymentStatus(deploymentId, controllerUrl, envKey, authCon
310
310
  throw new Error('Deployment timeout: Maximum polling attempts reached');
311
311
  }
312
312
 
313
+ /**
314
+ * When using bearer token only (no client credentials), verify token is valid before deploy.
315
+ * @async
316
+ * @param {string} controllerUrl - Controller URL
317
+ * @param {Object} authConfig - Authentication configuration
318
+ * @throws {Error} If token is invalid or expired
319
+ */
320
+ async function ensureBearerTokenValid(controllerUrl, authConfig) {
321
+ if (authConfig.type !== 'bearer' || !authConfig.token || authConfig.clientId) {
322
+ return;
323
+ }
324
+ try {
325
+ const response = await getAuthUser(controllerUrl, authConfig);
326
+ if (response && response.success && response.data && response.data.authenticated !== false) {
327
+ return;
328
+ }
329
+ } catch (_) {
330
+ // Fall through to throw below
331
+ }
332
+ throw new Error(
333
+ 'Your authentication token is invalid or expired.\n\n' +
334
+ 'Run: aifabrix login --method device --controller <url>\n\n' +
335
+ 'Then run deploy again.'
336
+ );
337
+ }
338
+
313
339
  /**
314
340
  * Validates and sends deployment request to controller
315
341
  * Implements two-step process: validate then deploy
@@ -322,6 +348,8 @@ async function pollDeploymentStatus(deploymentId, controllerUrl, envKey, authCon
322
348
  * @returns {Promise<Object>} Deployment result
323
349
  */
324
350
  async function sendDeployment(url, validatedEnvKey, manifest, authConfig, options) {
351
+ await ensureBearerTokenValid(url, authConfig);
352
+
325
353
  // Step 1: Validate deployment
326
354
  logger.log(chalk.blue('🔍 Validating deployment configuration...'));
327
355
  const validateResult = await validateDeployment(url, validatedEnvKey, manifest, authConfig, {
@@ -50,7 +50,6 @@ async function getAuthAndDataplane(systemKey, _options) {
50
50
  const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
51
51
  logger.log(chalk.blue('🌐 Resolving dataplane URL...'));
52
52
  const dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
53
- logger.log(chalk.green(`✓ Dataplane URL: ${dataplaneUrl}`));
54
53
 
55
54
  return { authConfig, dataplaneUrl, environment, controllerUrl };
56
55
  }