@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/app-run-helpers.js +381 -0
- package/lib/app-run.js +17 -392
- package/lib/build.js +2 -1
- package/lib/cli.js +13 -10
- package/lib/commands/secure.js +17 -35
- package/lib/config.js +48 -19
- package/lib/infra.js +14 -7
- package/lib/secrets.js +3 -1
- package/lib/utils/build-copy.js +7 -5
- package/lib/utils/compose-generator.js +3 -2
- package/lib/utils/dev-config.js +8 -7
- package/lib/utils/infra-containers.js +3 -2
- package/lib/utils/yaml-preserve.js +214 -0
- package/package.json +1 -1
package/lib/config.js
CHANGED
|
@@ -35,10 +35,24 @@ async function getConfig() {
|
|
|
35
35
|
config = {};
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
// Ensure
|
|
39
|
-
|
|
40
|
-
|
|
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 (
|
|
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
|
-
|
|
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<
|
|
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
|
-
|
|
138
|
-
|
|
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'] =
|
|
172
|
+
config['developer-id'] = devIdString;
|
|
146
173
|
// Update cache before saving
|
|
147
|
-
cachedDeveloperId =
|
|
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
|
-
|
|
158
|
-
|
|
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<
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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}`);
|
package/lib/utils/build-copy.js
CHANGED
|
@@ -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
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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);
|
package/lib/utils/dev-config.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
46
|
-
if (
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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
|
+
|