@aifabrix/builder 2.2.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.
@@ -13,6 +13,7 @@ const fs = require('fs');
13
13
  const path = require('path');
14
14
  const os = require('os');
15
15
  const yaml = require('js-yaml');
16
+ const config = require('../config');
16
17
 
17
18
  /**
18
19
  * Resolves secrets file path (backward compatibility)
@@ -56,14 +57,17 @@ function resolveSecretsPath(secretsPath) {
56
57
  /**
57
58
  * Determines the actual secrets file paths that loadSecrets would use
58
59
  * Mirrors the cascading lookup logic from loadSecrets
60
+ * Checks config.yaml for general secrets-path as fallback
61
+ *
62
+ * @async
59
63
  * @function getActualSecretsPath
60
64
  * @param {string} [secretsPath] - Path to secrets file (optional)
61
65
  * @param {string} [appName] - Application name (optional, for variables.yaml lookup)
62
- * @returns {Object} Object with userPath and buildPath (if configured)
66
+ * @returns {Promise<Object>} Object with userPath and buildPath (if configured)
63
67
  * @returns {string} returns.userPath - User's secrets file path (~/.aifabrix/secrets.local.yaml)
64
- * @returns {string|null} returns.buildPath - App's build.secrets file path (if configured in variables.yaml)
68
+ * @returns {string|null} returns.buildPath - App's build.secrets file path (if configured in variables.yaml or config.yaml)
65
69
  */
66
- function getActualSecretsPath(secretsPath, appName) {
70
+ async function getActualSecretsPath(secretsPath, appName) {
67
71
  // If explicit path provided, use it (backward compatibility)
68
72
  if (secretsPath) {
69
73
  const resolvedPath = resolveSecretsPath(secretsPath);
@@ -97,6 +101,21 @@ function getActualSecretsPath(secretsPath, appName) {
97
101
  }
98
102
  }
99
103
 
104
+ // If no build.secrets found in variables.yaml, check config.yaml for general secrets-path
105
+ if (!buildSecretsPath) {
106
+ try {
107
+ const generalSecretsPath = await config.getSecretsPath();
108
+ if (generalSecretsPath) {
109
+ // Resolve relative paths from current working directory
110
+ buildSecretsPath = path.isAbsolute(generalSecretsPath)
111
+ ? generalSecretsPath
112
+ : path.resolve(process.cwd(), generalSecretsPath);
113
+ }
114
+ } catch (error) {
115
+ // Ignore errors, continue
116
+ }
117
+ }
118
+
100
119
  // Return both paths (even if files don't exist) for error messages
101
120
  return {
102
121
  userPath: userSecretsPath,
@@ -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.2.0",
3
+ "version": "2.3.2",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -32,7 +32,7 @@
32
32
  "container"
33
33
  ],
34
34
  "author": "eSystems Nordic Ltd",
35
- "license": "UNLICENSED",
35
+ "license": "MIT",
36
36
  "engines": {
37
37
  "node": ">=18.0.0"
38
38
  },