@aifabrix/builder 2.22.1 → 2.31.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 (65) hide show
  1. package/jest.config.coverage.js +37 -0
  2. package/lib/api/pipeline.api.js +10 -9
  3. package/lib/app-deploy.js +36 -14
  4. package/lib/app-list.js +191 -71
  5. package/lib/app-prompts.js +77 -26
  6. package/lib/app-readme.js +123 -5
  7. package/lib/app-rotate-secret.js +101 -57
  8. package/lib/app-run-helpers.js +200 -172
  9. package/lib/app-run.js +137 -68
  10. package/lib/audit-logger.js +8 -7
  11. package/lib/build.js +161 -250
  12. package/lib/cli.js +73 -65
  13. package/lib/commands/login.js +45 -31
  14. package/lib/commands/logout.js +181 -0
  15. package/lib/commands/secrets-set.js +2 -2
  16. package/lib/commands/secure.js +61 -26
  17. package/lib/config.js +79 -45
  18. package/lib/datasource-deploy.js +89 -29
  19. package/lib/deployer.js +164 -129
  20. package/lib/diff.js +63 -21
  21. package/lib/environment-deploy.js +36 -19
  22. package/lib/external-system-deploy.js +134 -66
  23. package/lib/external-system-download.js +244 -171
  24. package/lib/external-system-test.js +199 -164
  25. package/lib/generator-external.js +145 -72
  26. package/lib/generator-helpers.js +49 -17
  27. package/lib/generator-split.js +105 -58
  28. package/lib/infra.js +101 -131
  29. package/lib/schema/application-schema.json +895 -896
  30. package/lib/schema/env-config.yaml +11 -4
  31. package/lib/template-validator.js +13 -4
  32. package/lib/utils/api.js +8 -8
  33. package/lib/utils/app-register-auth.js +36 -18
  34. package/lib/utils/app-run-containers.js +140 -0
  35. package/lib/utils/auth-headers.js +6 -6
  36. package/lib/utils/build-copy.js +60 -2
  37. package/lib/utils/build-helpers.js +94 -0
  38. package/lib/utils/cli-utils.js +177 -76
  39. package/lib/utils/compose-generator.js +12 -2
  40. package/lib/utils/config-tokens.js +151 -9
  41. package/lib/utils/deployment-errors.js +137 -69
  42. package/lib/utils/deployment-validation-helpers.js +103 -0
  43. package/lib/utils/docker-build.js +57 -0
  44. package/lib/utils/dockerfile-utils.js +13 -3
  45. package/lib/utils/env-copy.js +163 -94
  46. package/lib/utils/env-map.js +226 -86
  47. package/lib/utils/environment-checker.js +2 -2
  48. package/lib/utils/error-formatters/network-errors.js +0 -1
  49. package/lib/utils/external-system-display.js +14 -19
  50. package/lib/utils/external-system-env-helpers.js +107 -0
  51. package/lib/utils/external-system-test-helpers.js +144 -0
  52. package/lib/utils/health-check.js +10 -8
  53. package/lib/utils/infra-status.js +123 -0
  54. package/lib/utils/local-secrets.js +3 -2
  55. package/lib/utils/paths.js +228 -49
  56. package/lib/utils/schema-loader.js +125 -57
  57. package/lib/utils/token-manager.js +10 -7
  58. package/lib/utils/yaml-preserve.js +55 -16
  59. package/lib/validate.js +87 -89
  60. package/package.json +4 -4
  61. package/scripts/ci-fix.sh +19 -0
  62. package/scripts/ci-simulate.sh +19 -0
  63. package/templates/applications/miso-controller/test.yaml +1 -0
  64. package/templates/python/Dockerfile.hbs +8 -45
  65. package/templates/typescript/Dockerfile.hbs +8 -42
@@ -57,6 +57,153 @@ function getAifabrixHome() {
57
57
  return path.join(safeHomedir(), '.aifabrix');
58
58
  }
59
59
 
60
+ // Cache project root to avoid repeated filesystem lookups
61
+ let cachedProjectRoot = null;
62
+
63
+ /**
64
+ * Clears the cached project root
65
+ * Useful in tests when global.PROJECT_ROOT changes
66
+ */
67
+ function clearProjectRootCache() {
68
+ cachedProjectRoot = null;
69
+ }
70
+
71
+ /**
72
+ * Checks if a directory contains package.json
73
+ * @param {string} dirPath - Directory path to check
74
+ * @returns {boolean} True if package.json exists
75
+ */
76
+ function hasPackageJson(dirPath) {
77
+ const packageJsonPath = path.join(dirPath, 'package.json');
78
+ return fs.existsSync(packageJsonPath);
79
+ }
80
+
81
+ /**
82
+ * Strategy 1: Walk up from a starting directory to find package.json
83
+ * @param {string} startDir - Starting directory
84
+ * @param {number} maxDepth - Maximum depth to search
85
+ * @returns {string|null} Project root path or null if not found
86
+ */
87
+ function findProjectRootByWalkingUp(startDir, maxDepth = 10) {
88
+ let currentDir = startDir;
89
+ for (let i = 0; i < maxDepth; i++) {
90
+ if (hasPackageJson(currentDir)) {
91
+ return currentDir;
92
+ }
93
+ const parentDir = path.dirname(currentDir);
94
+ if (parentDir === currentDir) {
95
+ // Reached filesystem root
96
+ break;
97
+ }
98
+ currentDir = parentDir;
99
+ }
100
+ return null;
101
+ }
102
+
103
+ /**
104
+ * Strategy 2: Try from process.cwd()
105
+ * @returns {string|null} Project root path or null if not found
106
+ */
107
+ function findProjectRootFromCwd() {
108
+ try {
109
+ const cwd = process.cwd();
110
+ if (cwd && cwd !== '/' && hasPackageJson(cwd)) {
111
+ return cwd;
112
+ }
113
+ } catch {
114
+ // Ignore errors
115
+ }
116
+ return null;
117
+ }
118
+
119
+ /**
120
+ * Strategy 3: Check if Jest rootDir is available
121
+ * @returns {string|null} Project root path or null if not found
122
+ */
123
+ // eslint-disable-next-line no-unused-vars
124
+ function _findProjectRootFromJest() {
125
+ if (typeof jest !== 'undefined' && jest.config && jest.config.rootDir) {
126
+ const jestRoot = jest.config.rootDir;
127
+ if (hasPackageJson(jestRoot)) {
128
+ return jestRoot;
129
+ }
130
+ }
131
+ return null;
132
+ }
133
+
134
+ /**
135
+ * Strategy 4: Fallback to __dirname relative path
136
+ * @returns {string} Fallback project root path
137
+ */
138
+ function getFallbackProjectRoot() {
139
+ return path.resolve(__dirname, '..', '..');
140
+ }
141
+
142
+ /**
143
+ * Gets the project root directory by finding package.json
144
+ * Works reliably in all environments including Jest tests and CI
145
+ * @returns {string} Absolute path to project root
146
+ */
147
+ function getProjectRoot() {
148
+ // SIMPLIFIED: Always use __dirname walking - most reliable in all environments
149
+ // This works in: local dev, CI, tests, temp directories, etc.
150
+ // __dirname is lib/utils/, so we walk up to find package.json
151
+
152
+ // Return cached value if available and valid
153
+ if (cachedProjectRoot && hasPackageJson(cachedProjectRoot)) {
154
+ return cachedProjectRoot;
155
+ }
156
+
157
+ // Strategy 1: Check global.PROJECT_ROOT if set and valid (for test isolation)
158
+ // BUT: In CI simulation, the project is copied, so global.PROJECT_ROOT might point to original
159
+ // We need to verify it's actually the correct root by checking if __dirname is within it
160
+ if (typeof global !== 'undefined' && global.PROJECT_ROOT) {
161
+ const globalRoot = global.PROJECT_ROOT;
162
+ if (hasPackageJson(globalRoot)) {
163
+ // Verify that __dirname is actually within globalRoot (or they're the same)
164
+ // This ensures we're using the correct project root in CI simulation
165
+ const dirnameNormalized = path.resolve(__dirname);
166
+ const globalRootNormalized = path.resolve(globalRoot);
167
+ const isWithinGlobalRoot = dirnameNormalized.startsWith(globalRootNormalized + path.sep) ||
168
+ dirnameNormalized === globalRootNormalized;
169
+
170
+ if (isWithinGlobalRoot) {
171
+ cachedProjectRoot = globalRoot;
172
+ return cachedProjectRoot;
173
+ }
174
+ // If global.PROJECT_ROOT doesn't contain __dirname, it's wrong (e.g., original project in CI)
175
+ // Clear it and continue with other strategies
176
+ }
177
+ }
178
+
179
+ // Strategy 2: Walk up from __dirname (lib/utils/) - MOST RELIABLE
180
+ // This always works because __dirname is always correct relative to the code
181
+ const foundRoot = findProjectRootByWalkingUp(__dirname);
182
+ if (foundRoot && hasPackageJson(foundRoot)) {
183
+ cachedProjectRoot = foundRoot;
184
+ return cachedProjectRoot;
185
+ }
186
+
187
+ // Strategy 3: Try process.cwd() (works in most cases)
188
+ const cwdRoot = findProjectRootFromCwd();
189
+ if (cwdRoot && hasPackageJson(cwdRoot)) {
190
+ cachedProjectRoot = cwdRoot;
191
+ return cachedProjectRoot;
192
+ }
193
+
194
+ // Strategy 4: Fallback to __dirname relative (lib/utils/ -> project root)
195
+ const fallbackRoot = getFallbackProjectRoot();
196
+ if (hasPackageJson(fallbackRoot)) {
197
+ cachedProjectRoot = fallbackRoot;
198
+ return cachedProjectRoot;
199
+ }
200
+
201
+ // Last resort: return fallback even if package.json not found
202
+ // This prevents crashes but should rarely happen
203
+ cachedProjectRoot = fallbackRoot;
204
+ return cachedProjectRoot;
205
+ }
206
+
60
207
  /**
61
208
  * Returns the applications base directory for a developer.
62
209
  * Dev 0: <home>/applications
@@ -169,65 +316,73 @@ function getDeployJsonPath(appName, appType, preferNew = false) {
169
316
  }
170
317
 
171
318
  /**
172
- * Detects if an app is external type by checking variables.yaml
173
- * Checks both integration/ and builder/ folders for backward compatibility
174
- *
175
- * @param {string} appName - Application name
176
- * @returns {Promise<{isExternal: boolean, appPath: string, appType: string}>}
319
+ * Reads and parses variables.yaml file
320
+ * @param {string} variablesPath - Path to variables.yaml file
321
+ * @returns {Object|null} Parsed variables object or null if error
177
322
  */
178
- async function detectAppType(appName) {
179
- if (!appName || typeof appName !== 'string') {
180
- throw new Error('App name is required and must be a string');
323
+ function readVariablesFile(variablesPath) {
324
+ try {
325
+ if (!fs.existsSync(variablesPath)) {
326
+ return null;
327
+ }
328
+ const content = fs.readFileSync(variablesPath, 'utf8');
329
+ return yaml.load(content);
330
+ } catch {
331
+ return null;
181
332
  }
333
+ }
182
334
 
183
- // Check integration folder first (new structure)
335
+ /**
336
+ * Checks if app type is external from variables object
337
+ * @param {Object} variables - Parsed variables.yaml object
338
+ * @returns {boolean} True if app type is external
339
+ */
340
+ function isExternalAppType(variables) {
341
+ return variables && variables.app && variables.app.type === 'external';
342
+ }
343
+
344
+ /**
345
+ * Checks integration folder for external app type
346
+ * @param {string} appName - Application name
347
+ * @returns {Object|null} App type info or null if not found
348
+ */
349
+ function checkIntegrationFolder(appName) {
184
350
  const integrationPath = getIntegrationPath(appName);
185
- const integrationVariablesPath = path.join(integrationPath, 'variables.yaml');
351
+ const variablesPath = path.join(integrationPath, 'variables.yaml');
352
+ const variables = readVariablesFile(variablesPath);
186
353
 
187
- if (fs.existsSync(integrationVariablesPath)) {
188
- try {
189
- const content = fs.readFileSync(integrationVariablesPath, 'utf8');
190
- const variables = yaml.load(content);
191
- if (variables.app && variables.app.type === 'external') {
192
- return {
193
- isExternal: true,
194
- appPath: integrationPath,
195
- appType: 'external',
196
- baseDir: 'integration'
197
- };
198
- }
199
- } catch {
200
- // Ignore errors, continue to check builder folder
201
- }
354
+ if (variables && isExternalAppType(variables)) {
355
+ return {
356
+ isExternal: true,
357
+ appPath: integrationPath,
358
+ appType: 'external',
359
+ baseDir: 'integration'
360
+ };
202
361
  }
362
+ return null;
363
+ }
203
364
 
204
- // Check builder folder (backward compatibility)
365
+ /**
366
+ * Checks builder folder for app type
367
+ * @param {string} appName - Application name
368
+ * @returns {Object} App type info
369
+ */
370
+ function checkBuilderFolder(appName) {
205
371
  const builderPath = getBuilderPath(appName);
206
- const builderVariablesPath = path.join(builderPath, 'variables.yaml');
372
+ const variablesPath = path.join(builderPath, 'variables.yaml');
373
+ const variables = readVariablesFile(variablesPath);
207
374
 
208
- if (fs.existsSync(builderVariablesPath)) {
209
- try {
210
- const content = fs.readFileSync(builderVariablesPath, 'utf8');
211
- const variables = yaml.load(content);
212
- const isExternal = variables.app && variables.app.type === 'external';
213
- return {
214
- isExternal,
215
- appPath: builderPath,
216
- appType: isExternal ? 'external' : 'regular',
217
- baseDir: 'builder'
218
- };
219
- } catch {
220
- // If we can't read it, assume regular app in builder folder
221
- return {
222
- isExternal: false,
223
- appPath: builderPath,
224
- appType: 'regular',
225
- baseDir: 'builder'
226
- };
227
- }
375
+ if (variables) {
376
+ const isExternal = isExternalAppType(variables);
377
+ return {
378
+ isExternal,
379
+ appPath: builderPath,
380
+ appType: isExternal ? 'external' : 'regular',
381
+ baseDir: 'builder'
382
+ };
228
383
  }
229
384
 
230
- // Default to builder folder if neither exists
385
+ // Default to regular app in builder folder
231
386
  return {
232
387
  isExternal: false,
233
388
  appPath: builderPath,
@@ -236,14 +391,38 @@ async function detectAppType(appName) {
236
391
  };
237
392
  }
238
393
 
394
+ /**
395
+ * Detects if an app is external type by checking variables.yaml
396
+ * Checks both integration/ and builder/ folders for backward compatibility
397
+ *
398
+ * @param {string} appName - Application name
399
+ * @returns {Promise<{isExternal: boolean, appPath: string, appType: string}>}
400
+ */
401
+ async function detectAppType(appName) {
402
+ if (!appName || typeof appName !== 'string') {
403
+ throw new Error('App name is required and must be a string');
404
+ }
405
+
406
+ // Check integration folder first (new structure)
407
+ const integrationResult = checkIntegrationFolder(appName);
408
+ if (integrationResult) {
409
+ return integrationResult;
410
+ }
411
+
412
+ // Check builder folder (backward compatibility)
413
+ return checkBuilderFolder(appName);
414
+ }
415
+
239
416
  module.exports = {
240
417
  getAifabrixHome,
241
418
  getApplicationsBaseDir,
242
419
  getDevDirectory,
243
420
  getAppPath,
421
+ getProjectRoot,
244
422
  getIntegrationPath,
245
423
  getBuilderPath,
246
424
  getDeployJsonPath,
247
- detectAppType
425
+ detectAppType,
426
+ clearProjectRootCache
248
427
  };
249
428
 
@@ -111,6 +111,118 @@ function loadExternalDataSourceSchema() {
111
111
  return externalDataSourceValidator;
112
112
  }
113
113
 
114
+ /**
115
+ * Detect schema type from $id field
116
+ * @param {Object} parsed - Parsed JSON object
117
+ * @returns {string|null} Schema type or null if not detected
118
+ */
119
+ function detectFromId(parsed) {
120
+ if (!parsed.$id) {
121
+ return null;
122
+ }
123
+
124
+ if (parsed.$id.includes('external-system')) {
125
+ return 'external-system';
126
+ }
127
+ if (parsed.$id.includes('external-datasource')) {
128
+ return 'external-datasource';
129
+ }
130
+ if (parsed.$id.includes('application-schema')) {
131
+ return 'application';
132
+ }
133
+
134
+ return null;
135
+ }
136
+
137
+ /**
138
+ * Detect schema type from title field
139
+ * @param {Object} parsed - Parsed JSON object
140
+ * @returns {string|null} Schema type or null if not detected
141
+ */
142
+ function detectFromTitle(parsed) {
143
+ if (!parsed.title) {
144
+ return null;
145
+ }
146
+
147
+ const titleLower = parsed.title.toLowerCase();
148
+ if (titleLower.includes('external system') || titleLower.includes('external-system') || titleLower.includes('external system configuration')) {
149
+ return 'external-system';
150
+ }
151
+ if (titleLower.includes('external data source') || titleLower.includes('external datasource') || titleLower.includes('external-datasource')) {
152
+ return 'external-datasource';
153
+ }
154
+ if (titleLower.includes('application')) {
155
+ return 'application';
156
+ }
157
+
158
+ return null;
159
+ }
160
+
161
+ /**
162
+ * Detect schema type from required fields
163
+ * @param {Object} parsed - Parsed JSON object
164
+ * @returns {string|null} Schema type or null if not detected
165
+ */
166
+ function detectFromRequiredFields(parsed) {
167
+ if (!parsed.key || !parsed.displayName || !parsed.type || !parsed.authentication) {
168
+ return null;
169
+ }
170
+
171
+ // Check if it has systemKey (datasource) or not (system)
172
+ if (parsed.systemKey) {
173
+ return 'external-datasource';
174
+ }
175
+ // Check if type is one of external-system types
176
+ if (['openapi', 'mcp', 'custom'].includes(parsed.type)) {
177
+ return 'external-system';
178
+ }
179
+
180
+ return null;
181
+ }
182
+
183
+ /**
184
+ * Detect schema type from datasource-specific fields
185
+ * @param {Object} parsed - Parsed JSON object
186
+ * @returns {string|null} Schema type or null if not detected
187
+ */
188
+ function detectFromDatasourceFields(parsed) {
189
+ if (parsed.systemKey && parsed.entityKey && parsed.fieldMappings) {
190
+ return 'external-datasource';
191
+ }
192
+ return null;
193
+ }
194
+
195
+ /**
196
+ * Detect schema type from application-specific fields
197
+ * @param {Object} parsed - Parsed JSON object
198
+ * @returns {string|null} Schema type or null if not detected
199
+ */
200
+ function detectFromApplicationFields(parsed) {
201
+ if (parsed.deploymentKey || (parsed.image && parsed.registryMode && parsed.port)) {
202
+ return 'application';
203
+ }
204
+ return null;
205
+ }
206
+
207
+ /**
208
+ * Detect schema type from filename pattern
209
+ * @param {string} filePath - File path
210
+ * @returns {string|null} Schema type or null if not detected
211
+ */
212
+ function detectFromFilename(filePath) {
213
+ const fileName = path.basename(filePath).toLowerCase();
214
+ if (fileName.includes('external-system') || fileName.includes('external_system')) {
215
+ return 'external-system';
216
+ }
217
+ if (fileName.includes('external-datasource') || fileName.includes('external_datasource') || fileName.includes('datasource')) {
218
+ return 'external-datasource';
219
+ }
220
+ if (fileName.includes('application') || fileName.includes('variables') || fileName.includes('deploy')) {
221
+ return 'application';
222
+ }
223
+ return null;
224
+ }
225
+
114
226
  /**
115
227
  * Detects schema type from file content or path
116
228
  * Attempts to identify if file is application, external-system, or external-datasource
@@ -144,68 +256,24 @@ function detectSchemaType(filePath, content) {
144
256
  throw new Error(`Invalid JSON in file: ${error.message}`);
145
257
  }
146
258
 
147
- // Check for schema type indicators
148
- // Check $id for schema type
149
- if (parsed.$id) {
150
- if (parsed.$id.includes('external-system')) {
151
- return 'external-system';
152
- }
153
- if (parsed.$id.includes('external-datasource')) {
154
- return 'external-datasource';
155
- }
156
- if (parsed.$id.includes('application-schema')) {
157
- return 'application';
158
- }
159
- }
259
+ // Try different detection methods in order
260
+ const idType = detectFromId(parsed);
261
+ if (idType) return idType;
160
262
 
161
- // Check title for schema type (works even without $id or $schema)
162
- if (parsed.title) {
163
- const titleLower = parsed.title.toLowerCase();
164
- if (titleLower.includes('external system') || titleLower.includes('external-system') || titleLower.includes('external system configuration')) {
165
- return 'external-system';
166
- }
167
- if (titleLower.includes('external data source') || titleLower.includes('external datasource') || titleLower.includes('external-datasource')) {
168
- return 'external-datasource';
169
- }
170
- if (titleLower.includes('application')) {
171
- return 'application';
172
- }
173
- }
263
+ const titleType = detectFromTitle(parsed);
264
+ if (titleType) return titleType;
174
265
 
175
- // Check for required fields to determine type
176
- // External system requires: key, displayName, description, type, authentication
177
- if (parsed.key && parsed.displayName && parsed.type && parsed.authentication) {
178
- // Check if it has systemKey (datasource) or not (system)
179
- if (parsed.systemKey) {
180
- return 'external-datasource';
181
- }
182
- // Check if type is one of external-system types
183
- if (['openapi', 'mcp', 'custom'].includes(parsed.type)) {
184
- return 'external-system';
185
- }
186
- }
266
+ const requiredFieldsType = detectFromRequiredFields(parsed);
267
+ if (requiredFieldsType) return requiredFieldsType;
187
268
 
188
- // Check for datasource-specific fields
189
- if (parsed.systemKey && parsed.entityKey && parsed.fieldMappings) {
190
- return 'external-datasource';
191
- }
269
+ const datasourceType = detectFromDatasourceFields(parsed);
270
+ if (datasourceType) return datasourceType;
192
271
 
193
- // Check for application-specific fields
194
- if (parsed.deploymentKey || (parsed.image && parsed.registryMode && parsed.port)) {
195
- return 'application';
196
- }
272
+ const applicationType = detectFromApplicationFields(parsed);
273
+ if (applicationType) return applicationType;
197
274
 
198
- // Fallback: check filename pattern
199
- const fileName = path.basename(filePath).toLowerCase();
200
- if (fileName.includes('external-system') || fileName.includes('external_system')) {
201
- return 'external-system';
202
- }
203
- if (fileName.includes('external-datasource') || fileName.includes('external_datasource') || fileName.includes('datasource')) {
204
- return 'external-datasource';
205
- }
206
- if (fileName.includes('application') || fileName.includes('variables') || fileName.includes('deploy')) {
207
- return 'application';
208
- }
275
+ const filenameType = detectFromFilename(filePath);
276
+ if (filenameType) return filenameType;
209
277
 
210
278
  // Default to application if cannot determine
211
279
  return 'application';
@@ -11,13 +11,15 @@
11
11
 
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
- const os = require('os');
15
14
  const yaml = require('js-yaml');
16
15
  const config = require('../config');
17
16
  const { makeApiCall, refreshDeviceToken: apiRefreshDeviceToken } = require('./api');
18
17
  const logger = require('./logger');
18
+ const pathsUtil = require('./paths');
19
19
 
20
- const SECRETS_FILE = path.join(os.homedir(), '.aifabrix', 'secrets.local.yaml');
20
+ function getSecretsFilePath() {
21
+ return path.join(pathsUtil.getAifabrixHome(), 'secrets.local.yaml');
22
+ }
21
23
 
22
24
  /**
23
25
  * Load client credentials from secrets.local.yaml
@@ -31,11 +33,12 @@ async function loadClientCredentials(appName) {
31
33
  }
32
34
 
33
35
  try {
34
- if (!fs.existsSync(SECRETS_FILE)) {
36
+ const secretsFile = getSecretsFilePath();
37
+ if (!fs.existsSync(secretsFile)) {
35
38
  return null;
36
39
  }
37
40
 
38
- const content = fs.readFileSync(SECRETS_FILE, 'utf8');
41
+ const content = fs.readFileSync(secretsFile, 'utf8');
39
42
  const secrets = yaml.load(content) || {};
40
43
 
41
44
  const clientIdKey = `${appName}-client-idKeyVault`;
@@ -298,7 +301,7 @@ async function getOrRefreshDeviceToken(controllerUrl) {
298
301
  * @param {string} controllerUrl - Controller URL
299
302
  * @param {string} environment - Environment key
300
303
  * @param {string} appName - Application name
301
- * @returns {Promise<{type: 'bearer'|'credentials', token?: string, clientId?: string, clientSecret?: string, controller: string}>} Auth configuration
304
+ * @returns {Promise<{type: 'bearer'|'client-credentials', token?: string, clientId?: string, clientSecret?: string, controller: string}>} Auth configuration
302
305
  * @throws {Error} If no authentication method is available
303
306
  */
304
307
  async function getDeploymentAuth(controllerUrl, environment, appName) {
@@ -341,7 +344,7 @@ async function getDeploymentAuth(controllerUrl, environment, appName) {
341
344
  const credentials = await loadClientCredentials(appName);
342
345
  if (credentials && credentials.clientId && credentials.clientSecret) {
343
346
  return {
344
- type: 'credentials',
347
+ type: 'client-credentials',
345
348
  clientId: credentials.clientId,
346
349
  clientSecret: credentials.clientSecret,
347
350
  controller: controllerUrl
@@ -363,7 +366,7 @@ async function getDeploymentAuth(controllerUrl, environment, appName) {
363
366
  * @throws {Error} If credentials cannot be obtained
364
367
  */
365
368
  async function extractClientCredentials(authConfig, appKey, envKey, _options = {}) {
366
- if (authConfig.type === 'credentials') {
369
+ if (authConfig.type === 'client-credentials') {
367
370
  if (!authConfig.clientId || !authConfig.clientSecret) {
368
371
  throw new Error('Client ID and Client Secret are required');
369
372
  }
@@ -11,6 +11,57 @@
11
11
 
12
12
  const { encryptSecret, isEncrypted } = require('./secrets-encryption');
13
13
 
14
+ /**
15
+ * Check if value is null
16
+ * @param {string} trimmed - Trimmed value
17
+ * @returns {boolean} True if null
18
+ */
19
+ function isNullValue(trimmed) {
20
+ return trimmed === 'null' || trimmed === '~' || trimmed === '';
21
+ }
22
+
23
+ /**
24
+ * Check if value is boolean
25
+ * @param {string} trimmed - Trimmed value
26
+ * @returns {boolean} True if boolean
27
+ */
28
+ function isBooleanValue(trimmed) {
29
+ const booleanValues = [
30
+ 'true', 'false', 'True', 'False', 'TRUE', 'FALSE',
31
+ 'yes', 'no', 'Yes', 'No', 'YES', 'NO',
32
+ 'on', 'off', 'On', 'Off', 'ON', 'OFF'
33
+ ];
34
+ return booleanValues.includes(trimmed);
35
+ }
36
+
37
+ /**
38
+ * Check if value is number
39
+ * @param {string} trimmed - Trimmed value
40
+ * @returns {boolean} True if number
41
+ */
42
+ function isNumberValue(trimmed) {
43
+ return /^[+-]?\d+$/.test(trimmed) ||
44
+ /^[+-]?\d*\.\d+([eE][+-]?\d+)?$/.test(trimmed) ||
45
+ /^[+-]?\.\d+([eE][+-]?\d+)?$/.test(trimmed) ||
46
+ /^0x[0-9a-fA-F]+$/.test(trimmed) ||
47
+ /^0o[0-7]+$/.test(trimmed) ||
48
+ /^0b[01]+$/.test(trimmed);
49
+ }
50
+
51
+ /**
52
+ * Check if value is YAML special value
53
+ * @param {string} trimmed - Trimmed value
54
+ * @returns {boolean} True if YAML special value
55
+ */
56
+ function isYamlSpecialValue(trimmed) {
57
+ const specialValues = [
58
+ '.inf', '.Inf', '.INF',
59
+ '-.inf', '-.Inf', '-.INF',
60
+ '.nan', '.NaN', '.NAN'
61
+ ];
62
+ return specialValues.includes(trimmed);
63
+ }
64
+
14
65
  /**
15
66
  * Checks if a string value represents a YAML primitive (number, boolean, null)
16
67
  * When parsing line-by-line, these appear as strings but should not be encrypted
@@ -22,31 +73,19 @@ const { encryptSecret, isEncrypted } = require('./secrets-encryption');
22
73
  function isYamlPrimitive(value) {
23
74
  const trimmed = value.trim();
24
75
 
25
- // Check for null
26
- if (trimmed === 'null' || trimmed === '~' || trimmed === '') {
76
+ if (isNullValue(trimmed)) {
27
77
  return true;
28
78
  }
29
79
 
30
- // Check for boolean
31
- if (trimmed === 'true' || trimmed === 'false' || trimmed === 'True' || trimmed === 'False' ||
32
- trimmed === 'TRUE' || trimmed === 'FALSE' || trimmed === 'yes' || trimmed === 'no' ||
33
- trimmed === 'Yes' || trimmed === 'No' || trimmed === 'YES' || trimmed === 'NO' ||
34
- trimmed === 'on' || trimmed === 'off' || trimmed === 'On' || trimmed === 'Off' ||
35
- trimmed === 'ON' || trimmed === 'OFF') {
80
+ if (isBooleanValue(trimmed)) {
36
81
  return true;
37
82
  }
38
83
 
39
- // Check for number (integer or float, with optional sign)
40
- if (/^[+-]?\d+$/.test(trimmed) || /^[+-]?\d*\.\d+([eE][+-]?\d+)?$/.test(trimmed) ||
41
- /^[+-]?\.\d+([eE][+-]?\d+)?$/.test(trimmed) || /^0x[0-9a-fA-F]+$/.test(trimmed) ||
42
- /^0o[0-7]+$/.test(trimmed) || /^0b[01]+$/.test(trimmed)) {
84
+ if (isNumberValue(trimmed)) {
43
85
  return true;
44
86
  }
45
87
 
46
- // Check for YAML special values
47
- if (trimmed === '.inf' || trimmed === '.Inf' || trimmed === '.INF' ||
48
- trimmed === '-.inf' || trimmed === '-.Inf' || trimmed === '-.INF' ||
49
- trimmed === '.nan' || trimmed === '.NaN' || trimmed === '.NAN') {
88
+ if (isYamlSpecialValue(trimmed)) {
50
89
  return true;
51
90
  }
52
91