@aifabrix/builder 2.3.0 → 2.3.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.
package/lib/config.js CHANGED
@@ -35,10 +35,24 @@ async function getConfig() {
35
35
  config = {};
36
36
  }
37
37
 
38
- // Ensure developerId defaults to 0 if not set
39
- if (typeof config['developer-id'] === 'undefined') {
40
- config['developer-id'] = 0;
38
+ // Ensure developer-id exists as a digit-only string (default "0") and validate
39
+ const DEV_ID_DIGITS_REGEX = /^[0-9]+$/;
40
+ if (typeof config['developer-id'] === 'undefined' || config['developer-id'] === null) {
41
+ config['developer-id'] = '0';
42
+ } else if (typeof config['developer-id'] === 'number') {
43
+ // Convert numeric to string to preserve type consistency
44
+ if (config['developer-id'] < 0 || !Number.isFinite(config['developer-id'])) {
45
+ throw new Error(`Invalid developer-id value: "${config['developer-id']}". Must be a non-negative digit string.`);
46
+ }
47
+ config['developer-id'] = String(config['developer-id']);
48
+ } else if (typeof config['developer-id'] === 'string') {
49
+ if (!DEV_ID_DIGITS_REGEX.test(config['developer-id'])) {
50
+ throw new Error(`Invalid developer-id value: "${config['developer-id']}". Must contain only digits 0-9.`);
51
+ }
52
+ } else {
53
+ throw new Error(`Invalid developer-id value type: ${typeof config['developer-id']}. Must be a non-negative digit string.`);
41
54
  }
55
+
42
56
  // Ensure environment defaults to 'dev' if not set
43
57
  if (typeof config.environment === 'undefined') {
44
58
  config.environment = 'dev';
@@ -51,15 +65,15 @@ async function getConfig() {
51
65
  if (typeof config.device !== 'object' || config.device === null) {
52
66
  config.device = {};
53
67
  }
54
- // Cache developer ID as property for easy access (use nullish coalescing to allow 0)
55
- cachedDeveloperId = config['developer-id'] !== undefined ? config['developer-id'] : 0;
68
+ // Cache developer ID as property for easy access (string, default "0")
69
+ cachedDeveloperId = config['developer-id'] !== undefined ? config['developer-id'] : '0';
56
70
  return config;
57
71
  } catch (error) {
58
72
  if (error.code === 'ENOENT') {
59
- // Default developer ID is 0, default environment is 'dev'
60
- cachedDeveloperId = 0;
73
+ // Default developer ID is "0", default environment is 'dev'
74
+ cachedDeveloperId = '0';
61
75
  return {
62
- 'developer-id': 0,
76
+ 'developer-id': '0',
63
77
  environment: 'dev',
64
78
  environments: {},
65
79
  device: {}
@@ -80,7 +94,8 @@ async function saveConfig(data) {
80
94
  await fs.mkdir(CONFIG_DIR, { recursive: true });
81
95
 
82
96
  // Set secure permissions
83
- const configContent = yaml.dump(data);
97
+ // Force quotes to ensure numeric-like strings (e.g., "01") remain strings in YAML
98
+ const configContent = yaml.dump(data, { forceQuotes: true });
84
99
  // Write file first
85
100
  await fs.writeFile(CONFIG_FILE, configContent, {
86
101
  mode: 0o600,
@@ -117,7 +132,7 @@ async function clearConfig() {
117
132
  * Get developer ID from configuration
118
133
  * Loads config if not already cached, then returns cached developer ID
119
134
  * Developer ID: 0 = default infra, > 0 = developer-specific
120
- * @returns {Promise<number>} Developer ID (defaults to 0)
135
+ * @returns {Promise<string>} Developer ID as string (defaults to "0")
121
136
  */
122
137
  async function getDeveloperId() {
123
138
  // Always reload from file to ensure we have the latest value
@@ -130,21 +145,33 @@ async function getDeveloperId() {
130
145
 
131
146
  /**
132
147
  * Set developer ID in configuration
133
- * @param {number} developerId - Developer ID to set (0 = default infra, > 0 = developer-specific)
148
+ * @param {number|string} developerId - Developer ID to set (digit-only string preserved, or number). "0" = default infra, > "0" = developer-specific
134
149
  * @returns {Promise<void>}
135
150
  */
136
151
  async function setDeveloperId(developerId) {
137
- if (typeof developerId !== 'number' || developerId < 0) {
138
- throw new Error('Developer ID must be a non-negative number (0 = default infra, > 0 = developer-specific)');
152
+ const DEV_ID_DIGITS_REGEX = /^[0-9]+$/;
153
+ let devIdString;
154
+ if (typeof developerId === 'number') {
155
+ if (!Number.isFinite(developerId) || developerId < 0) {
156
+ throw new Error('Developer ID must be a non-negative digit string or number (0 = default infra, > 0 = developer-specific)');
157
+ }
158
+ devIdString = String(developerId);
159
+ } else if (typeof developerId === 'string') {
160
+ if (!DEV_ID_DIGITS_REGEX.test(developerId)) {
161
+ throw new Error('Developer ID must be a non-negative digit string or number (0 = default infra, > 0 = developer-specific)');
162
+ }
163
+ devIdString = developerId;
164
+ } else {
165
+ throw new Error('Developer ID must be a non-negative digit string or number (0 = default infra, > 0 = developer-specific)');
139
166
  }
140
167
  // Clear cache first to ensure we get fresh data from file
141
168
  cachedDeveloperId = null;
142
169
  // Read file directly to avoid any caching issues
143
170
  const config = await getConfig();
144
171
  // Update developer ID
145
- config['developer-id'] = developerId;
172
+ config['developer-id'] = devIdString;
146
173
  // Update cache before saving
147
- cachedDeveloperId = developerId;
174
+ cachedDeveloperId = devIdString;
148
175
  // Save the entire config object to ensure all fields are preserved
149
176
  await saveConfig(config);
150
177
  // Verify the file was saved correctly by reading it back
@@ -154,8 +181,10 @@ async function setDeveloperId(developerId) {
154
181
  // Read file again with fresh file handle to avoid OS caching
155
182
  const savedContent = await fs.readFile(CONFIG_FILE, 'utf8');
156
183
  const savedConfig = yaml.load(savedContent);
157
- if (savedConfig['developer-id'] !== developerId) {
158
- throw new Error(`Failed to save developer ID: expected ${developerId}, got ${savedConfig['developer-id']}. File content: ${savedContent.substring(0, 200)}`);
184
+ // YAML may parse numbers as numbers, so convert to string for comparison
185
+ const savedDevIdString = String(savedConfig['developer-id']);
186
+ if (savedDevIdString !== devIdString) {
187
+ throw new Error(`Failed to save developer ID: expected ${devIdString}, got ${savedDevIdString}. File content: ${savedContent.substring(0, 200)}`);
159
188
  }
160
189
  // Clear the cache to force reload from file on next getDeveloperId() call
161
190
  // This ensures we get the value that was actually saved to disk
@@ -288,7 +317,7 @@ async function saveClientToken(environment, appName, controllerUrl, token, expir
288
317
  /**
289
318
  * Initialize and load developer ID
290
319
  * Call this to ensure developerId is loaded and cached
291
- * @returns {Promise<number>} Developer ID
320
+ * @returns {Promise<string>} Developer ID (string)
292
321
  */
293
322
  async function loadDeveloperId() {
294
323
  if (cachedDeveloperId === null) {
@@ -379,7 +408,7 @@ const exportsObj = {
379
408
  // Developer ID: 0 = default infra, > 0 = developer-specific
380
409
  Object.defineProperty(exportsObj, 'developerId', {
381
410
  get() {
382
- return cachedDeveloperId !== null ? cachedDeveloperId : 0; // Default to 0 if not loaded yet
411
+ return cachedDeveloperId !== null ? cachedDeveloperId : '0'; // Default to "0" if not loaded yet
383
412
  },
384
413
  enumerable: true,
385
414
  configurable: true
package/lib/infra.js CHANGED
@@ -28,21 +28,23 @@ const execAsync = promisify(exec);
28
28
  /**
29
29
  * Gets infrastructure directory name based on developer ID
30
30
  * Dev 0: infra (no dev-0 suffix), Dev > 0: infra-dev{id}
31
- * @param {number} devId - Developer ID
31
+ * @param {number|string} devId - Developer ID
32
32
  * @returns {string} Infrastructure directory name
33
33
  */
34
34
  function getInfraDirName(devId) {
35
- return devId === 0 ? 'infra' : `infra-dev${devId}`;
35
+ const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
36
+ return idNum === 0 ? 'infra' : `infra-dev${devId}`;
36
37
  }
37
38
 
38
39
  /**
39
40
  * Gets Docker Compose project name based on developer ID
40
41
  * Dev 0: infra (no dev-0 suffix), Dev > 0: infra-dev{id}
41
- * @param {number} devId - Developer ID
42
+ * @param {number|string} devId - Developer ID
42
43
  * @returns {string} Docker Compose project name
43
44
  */
44
45
  function getInfraProjectName(devId) {
45
- return devId === 0 ? 'infra' : `infra-dev${devId}`;
46
+ const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
47
+ return idNum === 0 ? 'infra' : `infra-dev${devId}`;
46
48
  }
47
49
 
48
50
  // Wrapper to support cwd option
@@ -96,7 +98,10 @@ async function startInfra(developerId = null) {
96
98
 
97
99
  // Get developer ID from parameter or config
98
100
  const devId = developerId || await config.getDeveloperId();
99
- const ports = devConfig.getDevPorts(devId);
101
+ // Convert to number for getDevPorts (it expects numbers)
102
+ const devIdNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
103
+ const ports = devConfig.getDevPorts(devIdNum);
104
+ const idNum = devIdNum;
100
105
 
101
106
  // Load compose template (Handlebars)
102
107
  const templatePath = path.join(__dirname, '..', 'templates', 'infra', 'compose.yaml.hbs');
@@ -116,7 +121,7 @@ async function startInfra(developerId = null) {
116
121
  const templateContent = fs.readFileSync(templatePath, 'utf8');
117
122
  const template = handlebars.compile(templateContent);
118
123
  // Dev 0: infra-aifabrix-network, Dev > 0: infra-dev{id}-aifabrix-network
119
- const networkName = devId === 0 ? 'infra-aifabrix-network' : `infra-dev${devId}-aifabrix-network`;
124
+ const networkName = idNum === 0 ? 'infra-aifabrix-network' : `infra-dev${devId}-aifabrix-network`;
120
125
  const composeContent = template({
121
126
  devId: devId,
122
127
  postgresPort: ports.postgres,
@@ -261,7 +266,9 @@ async function checkInfraHealth(devId = null) {
261
266
  */
262
267
  async function getInfraStatus() {
263
268
  const devId = await config.getDeveloperId();
264
- const ports = devConfig.getDevPorts(devId);
269
+ // Convert string developer ID to number for getDevPorts
270
+ const devIdNum = parseInt(devId, 10);
271
+ const ports = devConfig.getDevPorts(devIdNum);
265
272
  const services = {
266
273
  postgres: { port: ports.postgres, url: `localhost:${ports.postgres}` },
267
274
  redis: { port: ports.redis, url: `localhost:${ports.redis}` },
package/lib/secrets.js CHANGED
@@ -357,7 +357,9 @@ async function generateEnvFile(appName, secretsPath, environment = 'local', forc
357
357
  // For local environment, update infrastructure ports to use dev-specific ports
358
358
  if (environment === 'local') {
359
359
  const devId = await config.getDeveloperId();
360
- const ports = devConfig.getDevPorts(devId);
360
+ // Convert string developer ID to number for getDevPorts
361
+ const devIdNum = parseInt(devId, 10);
362
+ const ports = devConfig.getDevPorts(devIdNum);
361
363
 
362
364
  // Update DATABASE_PORT if present
363
365
  resolved = resolved.replace(/^DATABASE_PORT\s*=\s*.*$/m, `DATABASE_PORT=${ports.postgres}`);
@@ -22,7 +22,7 @@ const os = require('os');
22
22
  * @async
23
23
  * @function copyBuilderToDevDirectory
24
24
  * @param {string} appName - Application name
25
- * @param {number} developerId - Developer ID
25
+ * @param {number|string} developerId - Developer ID
26
26
  * @returns {Promise<string>} Path to developer-specific directory
27
27
  * @throws {Error} If copying fails
28
28
  *
@@ -39,7 +39,8 @@ async function copyBuilderToDevDirectory(appName, developerId) {
39
39
  }
40
40
 
41
41
  // Get base directory (applications or applications-dev-{id})
42
- const baseDir = developerId === 0
42
+ const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
43
+ const baseDir = idNum === 0
43
44
  ? path.join(os.homedir(), '.aifabrix', 'applications')
44
45
  : path.join(os.homedir(), '.aifabrix', `applications-dev-${developerId}`);
45
46
 
@@ -64,7 +65,7 @@ async function copyBuilderToDevDirectory(appName, developerId) {
64
65
  await fs.mkdir(devDir, { recursive: true });
65
66
 
66
67
  // Copy files based on developer ID
67
- if (developerId === 0) {
68
+ if (idNum === 0) {
68
69
  // Dev 0: Copy contents from builder/{appName}/ directly to applications/
69
70
  await copyDirectory(builderPath, devDir);
70
71
  } else {
@@ -112,11 +113,12 @@ async function copyDirectory(sourceDir, targetDir) {
112
113
  /**
113
114
  * Gets developer-specific directory path for an application
114
115
  * @param {string} appName - Application name
115
- * @param {number} developerId - Developer ID
116
+ * @param {number|string} developerId - Developer ID
116
117
  * @returns {string} Path to developer-specific directory
117
118
  */
118
119
  function getDevDirectory(appName, developerId) {
119
- if (developerId === 0) {
120
+ const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
121
+ if (idNum === 0) {
120
122
  // Dev 0: all apps go directly to applications/ (no subdirectory)
121
123
  return path.join(os.homedir(), '.aifabrix', 'applications');
122
124
  }
@@ -307,12 +307,13 @@ async function generateDockerCompose(appName, appConfig, options) {
307
307
 
308
308
  // Get developer ID and network name
309
309
  const devId = await config.getDeveloperId();
310
+ const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
310
311
  // Dev 0: infra-aifabrix-network (no dev-0 suffix)
311
312
  // Dev > 0: infra-dev{id}-aifabrix-network
312
- const networkName = devId === 0 ? 'infra-aifabrix-network' : `infra-dev${devId}-aifabrix-network`;
313
+ const networkName = idNum === 0 ? 'infra-aifabrix-network' : `infra-dev${devId}-aifabrix-network`;
313
314
  // Dev 0: aifabrix-{appName} (no dev-0 suffix)
314
315
  // Dev > 0: aifabrix-dev{id}-{appName}
315
- const containerName = devId === 0 ? `aifabrix-${appName}` : `aifabrix-dev${devId}-${appName}`;
316
+ const containerName = idNum === 0 ? `aifabrix-${appName}` : `aifabrix-dev${devId}-${appName}`;
316
317
 
317
318
  const serviceConfig = buildServiceConfig(appName, appConfig, port);
318
319
  const volumesConfig = buildVolumesConfig(appName);
@@ -27,7 +27,7 @@ const BASE_PORTS = {
27
27
  * Developer ID: 0 = default infra (base ports), > 0 = developer-specific (offset ports)
28
28
  *
29
29
  * @function getDevPorts
30
- * @param {number} developerId - Developer ID (0 = default infra, 1, 2, 3, etc. = developer-specific)
30
+ * @param {number} developerId - Developer ID (0 = default infra, 1, 2, 3, etc. = developer-specific). Must be a number.
31
31
  * @returns {Object} Object with calculated ports for all services
32
32
  *
33
33
  * @example
@@ -37,27 +37,28 @@ const BASE_PORTS = {
37
37
  * // Returns: { app: 3100, postgres: 5532, redis: 6479, pgadmin: 5150, redisCommander: 8181 }
38
38
  */
39
39
  function getDevPorts(developerId) {
40
- // Validate type first - must be a number
40
+ // Only accept numbers, reject strings and other types
41
41
  if (typeof developerId !== 'number') {
42
42
  throw new Error('Developer ID must be a positive number');
43
43
  }
44
44
 
45
- // Handle NaN, undefined, null - throw error (don't default)
46
- if (isNaN(developerId) || developerId === undefined || developerId === null) {
45
+ // Handle invalids
46
+ if (developerId === undefined || developerId === null || Number.isNaN(developerId)) {
47
47
  throw new Error('Developer ID must be a positive number');
48
48
  }
49
-
50
49
  if (developerId < 0 || !Number.isInteger(developerId)) {
51
50
  throw new Error('Developer ID must be a positive number');
52
51
  }
53
52
 
53
+ const idNum = developerId;
54
+
54
55
  // Developer ID 0 = default infra (base ports, no offset)
55
- if (developerId === 0) {
56
+ if (idNum === 0) {
56
57
  return { ...BASE_PORTS };
57
58
  }
58
59
 
59
60
  // Developer ID > 0 = developer-specific (add offset)
60
- const offset = developerId * 100;
61
+ const offset = idNum * 100;
61
62
 
62
63
  return {
63
64
  app: BASE_PORTS.app + offset,
@@ -20,14 +20,15 @@ const execAsync = promisify(exec);
20
20
  * @private
21
21
  * @async
22
22
  * @param {string} serviceName - Service name
23
- * @param {number} [devId] - Developer ID (optional, will be loaded from config if not provided)
23
+ * @param {number|string} [devId] - Developer ID (optional, will be loaded from config if not provided)
24
24
  * @returns {Promise<string|null>} Container name or null if not found
25
25
  */
26
26
  async function findContainer(serviceName, devId = null) {
27
27
  try {
28
28
  const developerId = devId || await config.getDeveloperId();
29
+ const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
29
30
  // Dev 0: aifabrix-{serviceName}, Dev > 0: aifabrix-dev{id}-{serviceName}
30
- const containerNamePattern = developerId === 0
31
+ const containerNamePattern = idNum === 0
31
32
  ? `aifabrix-${serviceName}`
32
33
  : `aifabrix-dev${developerId}-${serviceName}`;
33
34
  let { stdout } = await execAsync(`docker ps --filter "name=${containerNamePattern}" --format "{{.Names}}"`);
@@ -0,0 +1,214 @@
1
+ /**
2
+ * AI Fabrix Builder YAML Preservation Utilities
3
+ *
4
+ * This module provides line-by-line YAML parsing that preserves comments,
5
+ * formatting, and structure while encrypting values.
6
+ *
7
+ * @fileoverview YAML preservation utilities for secure command
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const { encryptSecret, isEncrypted } = require('./secrets-encryption');
13
+
14
+ /**
15
+ * Checks if a string value represents a YAML primitive (number, boolean, null)
16
+ * When parsing line-by-line, these appear as strings but should not be encrypted
17
+ *
18
+ * @function isYamlPrimitive
19
+ * @param {string} value - Value to check
20
+ * @returns {boolean} True if value is a YAML primitive
21
+ */
22
+ function isYamlPrimitive(value) {
23
+ const trimmed = value.trim();
24
+
25
+ // Check for null
26
+ if (trimmed === 'null' || trimmed === '~' || trimmed === '') {
27
+ return true;
28
+ }
29
+
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') {
36
+ return true;
37
+ }
38
+
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)) {
43
+ return true;
44
+ }
45
+
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') {
50
+ return true;
51
+ }
52
+
53
+ return false;
54
+ }
55
+
56
+ /**
57
+ * Checks if a value should be encrypted
58
+ * URLs (http:// and https://) are not encrypted as they are not secrets
59
+ * YAML primitives (numbers, booleans, null) are not encrypted
60
+ *
61
+ * @function shouldEncryptValue
62
+ * @param {string} value - Value to check
63
+ * @returns {boolean} True if value should be encrypted
64
+ */
65
+ function shouldEncryptValue(value) {
66
+ if (typeof value !== 'string') {
67
+ return false;
68
+ }
69
+
70
+ const trimmed = value.trim();
71
+
72
+ // Skip empty or whitespace-only values
73
+ if (trimmed.length === 0) {
74
+ return false;
75
+ }
76
+
77
+ // Skip YAML primitives (numbers, booleans, null)
78
+ if (isYamlPrimitive(trimmed)) {
79
+ return false;
80
+ }
81
+
82
+ // Skip already encrypted values
83
+ if (isEncrypted(trimmed)) {
84
+ return false;
85
+ }
86
+
87
+ // Skip URLs - they are not secrets
88
+ if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
89
+ return false;
90
+ }
91
+
92
+ return true;
93
+ }
94
+
95
+ /**
96
+ * Extracts value from YAML line, handling quotes
97
+ * Removes quotes but remembers if they were present
98
+ *
99
+ * @function extractValue
100
+ * @param {string} valuePart - The value portion of a YAML line
101
+ * @returns {Object} Object with value and quote info
102
+ */
103
+ function extractValue(valuePart) {
104
+ const trimmed = valuePart.trim();
105
+
106
+ // Check for quoted strings
107
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
108
+ (trimmed.startsWith('\'') && trimmed.endsWith('\''))) {
109
+ const quote = trimmed[0];
110
+ const unquoted = trimmed.slice(1, -1);
111
+ return { value: unquoted, quoted: true, quoteChar: quote };
112
+ }
113
+
114
+ return { value: trimmed, quoted: false, quoteChar: null };
115
+ }
116
+
117
+ /**
118
+ * Formats value back with quotes if needed
119
+ *
120
+ * @function formatValue
121
+ * @param {string} value - Value to format
122
+ * @param {boolean} quoted - Whether value was originally quoted
123
+ * @param {string|null} quoteChar - Original quote character
124
+ * @returns {string} Formatted value
125
+ */
126
+ function formatValue(value, quoted, quoteChar) {
127
+ if (quoted && quoteChar) {
128
+ return `${quoteChar}${value}${quoteChar}`;
129
+ }
130
+ return value;
131
+ }
132
+
133
+ /**
134
+ * Encrypts YAML values while preserving comments and formatting
135
+ * Processes file line-by-line to maintain structure
136
+ *
137
+ * @function encryptYamlValues
138
+ * @param {string} content - Original YAML file content
139
+ * @param {string} encryptionKey - Encryption key
140
+ * @returns {Object} Object with encrypted content and statistics
141
+ * @returns {string} returns.content - Encrypted YAML content
142
+ * @returns {number} returns.encrypted - Count of encrypted values
143
+ * @returns {number} returns.total - Total count of values processed
144
+ *
145
+ * @example
146
+ * const result = encryptYamlValues(yamlContent, encryptionKey);
147
+ * // Returns: { content: '...', encrypted: 5, total: 10 }
148
+ */
149
+ function encryptYamlValues(content, encryptionKey) {
150
+ const lines = content.split(/\r?\n/);
151
+ const encryptedLines = [];
152
+ let encryptedCount = 0;
153
+ let totalCount = 0;
154
+
155
+ // Pattern to match key-value pairs with optional comments
156
+ // Matches: indentation, key, colon, value, optional whitespace, optional comment
157
+ // Handles: key: value, key: "value", key: value # comment, etc.
158
+ const kvPattern = /^(\s*)([^#:\n]+?):\s*(.+?)(\s*)(#.*)?$/;
159
+
160
+ for (let i = 0; i < lines.length; i++) {
161
+ const line = lines[i];
162
+ const trimmed = line.trim();
163
+
164
+ // Preserve empty lines and comment-only lines
165
+ if (trimmed === '' || trimmed.startsWith('#')) {
166
+ encryptedLines.push(line);
167
+ continue;
168
+ }
169
+
170
+ // Try to match key-value pattern
171
+ const match = line.match(kvPattern);
172
+ if (match) {
173
+ totalCount++;
174
+ const [, indent, key, valuePart, trailingWhitespace, comment] = match;
175
+
176
+ // Extract value (handle quotes)
177
+ const { value, quoted, quoteChar } = extractValue(valuePart);
178
+
179
+ // Check if value should be encrypted
180
+ if (shouldEncryptValue(value)) {
181
+ // Encrypt the value
182
+ const encryptedValue = encryptSecret(value, encryptionKey);
183
+ const formattedValue = formatValue(encryptedValue, quoted, quoteChar);
184
+
185
+ // Reconstruct line with encrypted value
186
+ const encryptedLine = `${indent}${key}: ${formattedValue}${trailingWhitespace}${comment || ''}`;
187
+ encryptedLines.push(encryptedLine);
188
+ encryptedCount++;
189
+ } else {
190
+ // Keep original line (already encrypted, URL, or non-string)
191
+ encryptedLines.push(line);
192
+ }
193
+ } else {
194
+ // Line doesn't match pattern (multiline value, complex structure, etc.)
195
+ // Preserve as-is
196
+ encryptedLines.push(line);
197
+ }
198
+ }
199
+
200
+ return {
201
+ content: encryptedLines.join('\n'),
202
+ encrypted: encryptedCount,
203
+ total: totalCount
204
+ };
205
+ }
206
+
207
+ module.exports = {
208
+ encryptYamlValues,
209
+ shouldEncryptValue,
210
+ isYamlPrimitive,
211
+ extractValue,
212
+ formatValue
213
+ };
214
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.3.0",
3
+ "version": "2.3.2",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {