@aifabrix/builder 2.31.0 → 2.32.1

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 (119) hide show
  1. package/README.md +9 -9
  2. package/integration/hubspot/README.md +2 -2
  3. package/integration/hubspot/hubspot-deploy-company.json +17 -14
  4. package/integration/hubspot/hubspot-deploy-contact.json +19 -16
  5. package/integration/hubspot/hubspot-deploy-deal.json +21 -18
  6. package/lib/api/types/datasources.types.js +31 -5
  7. package/lib/api/types/wizard.types.js +142 -0
  8. package/lib/api/wizard.api.js +177 -0
  9. package/lib/{app-config.js → app/config.js} +4 -4
  10. package/lib/{app-deploy.js → app/deploy.js} +8 -8
  11. package/lib/app/display.js +90 -0
  12. package/lib/{app-dockerfile.js → app/dockerfile.js} +4 -4
  13. package/lib/{app-down.js → app/down.js} +4 -4
  14. package/lib/app/helpers.js +218 -0
  15. package/lib/app/index.js +298 -0
  16. package/lib/{app-list.js → app/list.js} +6 -6
  17. package/lib/{app-push.js → app/push.js} +4 -4
  18. package/lib/{app-readme.js → app/readme.js} +34 -13
  19. package/lib/{app-register.js → app/register.js} +9 -9
  20. package/lib/{app-rotate-secret.js → app/rotate-secret.js} +123 -37
  21. package/lib/{app-run-helpers.js → app/run-helpers.js} +10 -10
  22. package/lib/{app-run.js → app/run.js} +6 -6
  23. package/lib/{build.js → build/index.js} +59 -32
  24. package/lib/build/package.json +7 -0
  25. package/lib/cli.js +245 -179
  26. package/lib/commands/app.js +3 -3
  27. package/lib/commands/datasource.js +4 -4
  28. package/lib/commands/login-credentials.js +209 -0
  29. package/lib/commands/login-device.js +254 -0
  30. package/lib/commands/login.js +67 -378
  31. package/lib/commands/logout.js +1 -1
  32. package/lib/commands/secrets-set.js +1 -1
  33. package/lib/commands/secure.js +2 -2
  34. package/lib/commands/wizard.js +498 -0
  35. package/lib/{audit-logger.js → core/audit-logger.js} +1 -1
  36. package/lib/{config.js → core/config.js} +28 -26
  37. package/lib/{diff.js → core/diff.js} +157 -72
  38. package/lib/{secrets.js → core/secrets.js} +86 -49
  39. package/lib/{templates.js → core/templates-env.js} +14 -222
  40. package/lib/core/templates.js +279 -0
  41. package/lib/{datasource-deploy.js → datasource/deploy.js} +6 -6
  42. package/lib/{datasource-diff.js → datasource/diff.js} +2 -2
  43. package/lib/datasource/list.js +223 -0
  44. package/lib/{datasource-validate.js → datasource/validate.js} +2 -2
  45. package/lib/{deployer.js → deployment/deployer.js} +48 -18
  46. package/lib/{environment-deploy.js → deployment/environment.js} +163 -84
  47. package/lib/{push.js → deployment/push.js} +1 -1
  48. package/lib/external-system/deploy-helpers.js +145 -0
  49. package/lib/{external-system-deploy.js → external-system/deploy.js} +156 -111
  50. package/lib/external-system/download-helpers.js +114 -0
  51. package/lib/{external-system-download.js → external-system/download.js} +92 -135
  52. package/lib/{external-system-generator.js → external-system/generator.js} +15 -11
  53. package/lib/external-system/test-auth.js +40 -0
  54. package/lib/external-system/test-execution.js +84 -0
  55. package/lib/external-system/test-helpers.js +109 -0
  56. package/lib/{external-system-test.js → external-system/test.js} +174 -192
  57. package/lib/{generator-builders.js → generator/builders.js} +87 -10
  58. package/lib/{generator-external.js → generator/external.js} +115 -52
  59. package/lib/{github-generator.js → generator/github.js} +116 -15
  60. package/lib/{generator-helpers.js → generator/helpers.js} +92 -42
  61. package/lib/{generator.js → generator/index.js} +49 -22
  62. package/lib/{generator-split.js → generator/split.js} +108 -55
  63. package/lib/generator/wizard-prompts.js +357 -0
  64. package/lib/generator/wizard.js +490 -0
  65. package/lib/{infra.js → infrastructure/index.js} +49 -22
  66. package/lib/schema/external-datasource.schema.json +145 -133
  67. package/lib/schema/external-system.schema.json +42 -0
  68. package/lib/utils/api.js +9 -5
  69. package/lib/utils/app-register-api.js +60 -32
  70. package/lib/utils/app-register-auth.js +172 -47
  71. package/lib/utils/app-register-config.js +130 -59
  72. package/lib/utils/app-run-containers.js +29 -8
  73. package/lib/utils/build-helpers.js +1 -1
  74. package/lib/utils/cli-utils.js +78 -30
  75. package/lib/utils/compose-generator.js +145 -65
  76. package/lib/utils/config-paths.js +2 -0
  77. package/lib/utils/deployment-errors.js +1 -1
  78. package/lib/utils/device-code.js +99 -41
  79. package/lib/utils/env-config-loader.js +1 -1
  80. package/lib/utils/env-copy.js +21 -18
  81. package/lib/utils/env-endpoints.js +115 -67
  82. package/lib/utils/env-map.js +13 -14
  83. package/lib/utils/env-ports.js +45 -25
  84. package/lib/utils/env-template.js +84 -42
  85. package/lib/utils/error-formatter.js +26 -9
  86. package/lib/utils/error-formatters/error-parser.js +90 -4
  87. package/lib/utils/error-formatters/http-status-errors.js +54 -17
  88. package/lib/utils/error-formatters/network-errors.js +103 -26
  89. package/lib/utils/external-system-display.js +184 -90
  90. package/lib/utils/external-system-validators.js +164 -42
  91. package/lib/utils/file-upload.js +109 -0
  92. package/lib/utils/health-check.js +199 -83
  93. package/lib/utils/infra-containers.js +1 -1
  94. package/lib/utils/infra-status.js +66 -15
  95. package/lib/utils/local-secrets.js +45 -25
  96. package/lib/utils/paths.js +45 -33
  97. package/lib/utils/schema-loader.js +42 -25
  98. package/lib/utils/schema-resolver.js +123 -74
  99. package/lib/utils/secrets-encryption.js +62 -25
  100. package/lib/utils/secrets-helpers.js +126 -63
  101. package/lib/utils/secrets-path.js +1 -1
  102. package/lib/utils/secrets-url.js +1 -1
  103. package/lib/utils/token-manager-refresh.js +181 -0
  104. package/lib/utils/token-manager.js +76 -123
  105. package/lib/utils/variable-transformer.js +154 -77
  106. package/lib/utils/yaml-preserve.js +41 -47
  107. package/lib/{template-validator.js → validation/template.js} +54 -23
  108. package/lib/{validate.js → validation/validate.js} +205 -125
  109. package/lib/{validator.js → validation/validator.js} +58 -39
  110. package/package.json +34 -3
  111. package/scripts/install-local.js +210 -0
  112. package/templates/external-system/deploy.ps1.hbs +34 -0
  113. package/templates/external-system/deploy.sh.hbs +34 -0
  114. package/templates/external-system/external-datasource.json.hbs +31 -12
  115. package/lib/app.js +0 -467
  116. package/lib/datasource-list.js +0 -141
  117. /package/lib/{app-prompts.js → app/prompts.js} +0 -0
  118. /package/lib/{env-reader.js → core/env-reader.js} +0 -0
  119. /package/lib/{key-generator.js → core/key-generator.js} +0 -0
@@ -186,7 +186,8 @@ function detectFromRequiredFields(parsed) {
186
186
  * @returns {string|null} Schema type or null if not detected
187
187
  */
188
188
  function detectFromDatasourceFields(parsed) {
189
- if (parsed.systemKey && parsed.entityKey && parsed.fieldMappings) {
189
+ // Support both entityType (new) and entityKey (old) for backward compatibility during migration
190
+ if (parsed.systemKey && (parsed.entityType || parsed.entityKey) && parsed.fieldMappings) {
190
191
  return 'external-datasource';
191
192
  }
192
193
  return null;
@@ -237,10 +238,17 @@ function detectFromFilename(filePath) {
237
238
  * const type = detectSchemaType('./hubspot.json');
238
239
  * // Returns: 'external-system'
239
240
  */
240
- function detectSchemaType(filePath, content) {
241
+ /**
242
+ * Reads and parses file content
243
+ * @function readAndParseFileContent
244
+ * @param {string} filePath - File path
245
+ * @param {string|undefined} content - Optional file content
246
+ * @returns {Object} Parsed JSON object
247
+ * @throws {Error} If file not found or invalid JSON
248
+ */
249
+ function readAndParseFileContent(filePath, content) {
241
250
  let fileContent = content;
242
251
 
243
- // Read file if content not provided
244
252
  if (!fileContent) {
245
253
  if (!fs.existsSync(filePath)) {
246
254
  throw new Error(`File not found: ${filePath}`);
@@ -248,35 +256,44 @@ function detectSchemaType(filePath, content) {
248
256
  fileContent = fs.readFileSync(filePath, 'utf8');
249
257
  }
250
258
 
251
- // Try to parse JSON
252
- let parsed;
253
259
  try {
254
- parsed = JSON.parse(fileContent);
260
+ return JSON.parse(fileContent);
255
261
  } catch (error) {
256
262
  throw new Error(`Invalid JSON in file: ${error.message}`);
257
263
  }
264
+ }
258
265
 
259
- // Try different detection methods in order
260
- const idType = detectFromId(parsed);
261
- if (idType) return idType;
262
-
263
- const titleType = detectFromTitle(parsed);
264
- if (titleType) return titleType;
265
-
266
- const requiredFieldsType = detectFromRequiredFields(parsed);
267
- if (requiredFieldsType) return requiredFieldsType;
268
-
269
- const datasourceType = detectFromDatasourceFields(parsed);
270
- if (datasourceType) return datasourceType;
271
-
272
- const applicationType = detectFromApplicationFields(parsed);
273
- if (applicationType) return applicationType;
266
+ /**
267
+ * Tries detection methods in order
268
+ * @function tryDetectionMethods
269
+ * @param {Object} parsed - Parsed JSON object
270
+ * @param {string} filePath - File path
271
+ * @returns {string|null} Detected type or null
272
+ */
273
+ function tryDetectionMethods(parsed, filePath) {
274
+ const detectionMethods = [
275
+ () => detectFromId(parsed),
276
+ () => detectFromTitle(parsed),
277
+ () => detectFromRequiredFields(parsed),
278
+ () => detectFromDatasourceFields(parsed),
279
+ () => detectFromApplicationFields(parsed),
280
+ () => detectFromFilename(filePath)
281
+ ];
282
+
283
+ for (const method of detectionMethods) {
284
+ const result = method();
285
+ if (result) {
286
+ return result;
287
+ }
288
+ }
274
289
 
275
- const filenameType = detectFromFilename(filePath);
276
- if (filenameType) return filenameType;
290
+ return null;
291
+ }
277
292
 
278
- // Default to application if cannot determine
279
- return 'application';
293
+ function detectSchemaType(filePath, content) {
294
+ const parsed = readAndParseFileContent(filePath, content);
295
+ const detectedType = tryDetectionMethods(parsed, filePath);
296
+ return detectedType || 'application';
280
297
  }
281
298
 
282
299
  module.exports = {
@@ -28,56 +28,66 @@ const { detectAppType } = require('./paths');
28
28
  * const basePath = await resolveSchemaBasePath('myapp');
29
29
  * // Returns: '/path/to/builder/myapp/schemas'
30
30
  */
31
- async function resolveSchemaBasePath(appName) {
32
- if (!appName || typeof appName !== 'string') {
33
- throw new Error('App name is required and must be a string');
34
- }
35
-
36
- // Detect app type and get correct path (integration or builder)
37
- const { appPath } = await detectAppType(appName);
31
+ /**
32
+ * Loads and validates variables.yaml
33
+ * @async
34
+ * @function loadAndValidateVariablesForSchema
35
+ * @param {string} appName - Application name
36
+ * @param {string} appPath - Application path
37
+ * @returns {Promise<Object>} Variables object
38
+ * @throws {Error} If file not found or invalid
39
+ */
40
+ async function loadAndValidateVariablesForSchema(appName, appPath) {
38
41
  const variablesPath = path.join(appPath, 'variables.yaml');
39
-
40
42
  if (!fs.existsSync(variablesPath)) {
41
43
  throw new Error(`variables.yaml not found: ${variablesPath}`);
42
44
  }
43
45
 
44
46
  const content = fs.readFileSync(variablesPath, 'utf8');
45
- let variables;
46
-
47
47
  try {
48
- variables = yaml.load(content);
48
+ return yaml.load(content);
49
49
  } catch (error) {
50
50
  throw new Error(`Invalid YAML syntax in variables.yaml: ${error.message}`);
51
51
  }
52
+ }
52
53
 
53
- // Check if externalIntegration block exists
54
+ /**
55
+ * Validates externalIntegration block
56
+ * @function validateExternalIntegrationBlock
57
+ * @param {Object} variables - Variables object
58
+ * @param {string} appName - Application name
59
+ * @returns {string} Schema base path
60
+ * @throws {Error} If block is missing or invalid
61
+ */
62
+ function validateExternalIntegrationBlock(variables, appName) {
54
63
  if (!variables.externalIntegration) {
55
64
  throw new Error(`externalIntegration block not found in variables.yaml for app: ${appName}`);
56
65
  }
57
-
58
66
  if (!variables.externalIntegration.schemaBasePath) {
59
67
  throw new Error(`schemaBasePath not found in externalIntegration block for app: ${appName}`);
60
68
  }
69
+ return variables.externalIntegration.schemaBasePath;
70
+ }
61
71
 
62
- const schemaBasePath = variables.externalIntegration.schemaBasePath;
72
+ /**
73
+ * Resolves and validates schema base path
74
+ * @function resolveAndValidateSchemaPath
75
+ * @param {string} schemaBasePath - Schema base path from config
76
+ * @param {string} variablesPath - Path to variables.yaml
77
+ * @returns {string} Resolved and validated path
78
+ * @throws {Error} If path is invalid
79
+ */
80
+ function resolveAndValidateSchemaPath(schemaBasePath, variablesPath) {
63
81
  const variablesDir = path.dirname(variablesPath);
82
+ let resolvedPath = path.isAbsolute(schemaBasePath)
83
+ ? schemaBasePath
84
+ : path.resolve(variablesDir, schemaBasePath);
64
85
 
65
- // Resolve path (absolute or relative to variables.yaml location)
66
- let resolvedPath;
67
- if (path.isAbsolute(schemaBasePath)) {
68
- resolvedPath = schemaBasePath;
69
- } else {
70
- resolvedPath = path.resolve(variablesDir, schemaBasePath);
71
- }
72
-
73
- // Normalize path
74
86
  resolvedPath = path.normalize(resolvedPath);
75
87
 
76
- // Validate path exists
77
88
  if (!fs.existsSync(resolvedPath)) {
78
89
  throw new Error(`Schema base path does not exist: ${resolvedPath}`);
79
90
  }
80
-
81
91
  if (!fs.statSync(resolvedPath).isDirectory()) {
82
92
  throw new Error(`Schema base path is not a directory: ${resolvedPath}`);
83
93
  }
@@ -85,6 +95,19 @@ async function resolveSchemaBasePath(appName) {
85
95
  return resolvedPath;
86
96
  }
87
97
 
98
+ async function resolveSchemaBasePath(appName) {
99
+ if (!appName || typeof appName !== 'string') {
100
+ throw new Error('App name is required and must be a string');
101
+ }
102
+
103
+ const { appPath } = await detectAppType(appName);
104
+ const variables = await loadAndValidateVariablesForSchema(appName, appPath);
105
+ const schemaBasePath = validateExternalIntegrationBlock(variables, appName);
106
+ const variablesPath = path.join(appPath, 'variables.yaml');
107
+
108
+ return resolveAndValidateSchemaPath(schemaBasePath, variablesPath);
109
+ }
110
+
88
111
  /**
89
112
  * Resolves all external system and datasource files from application configuration
90
113
  * Returns array of file paths with metadata
@@ -102,13 +125,69 @@ async function resolveSchemaBasePath(appName) {
102
125
  * // { path: '/path/to/hubspot-deal.json', type: 'datasource', fileName: 'hubspot-deal.json' }
103
126
  * // ]
104
127
  */
105
- async function resolveExternalFiles(appName) {
106
- if (!appName || typeof appName !== 'string') {
107
- throw new Error('App name is required and must be a string');
128
+ /**
129
+ * Resolves a single external file
130
+ * @function resolveSingleExternalFile
131
+ * @param {string} schemaBasePath - Schema base path
132
+ * @param {string} fileName - File name
133
+ * @param {string} type - File type ('system' or 'datasource')
134
+ * @returns {Object} Resolved file object
135
+ * @throws {Error} If file not found
136
+ */
137
+ function resolveSingleExternalFile(schemaBasePath, fileName, type) {
138
+ const filePath = path.join(schemaBasePath, fileName);
139
+ const normalizedPath = path.normalize(filePath);
140
+
141
+ if (!fs.existsSync(normalizedPath)) {
142
+ throw new Error(`External ${type} file not found: ${normalizedPath}`);
108
143
  }
109
144
 
110
- // Detect app type and get correct path (integration or builder)
111
- const { appPath } = await detectAppType(appName);
145
+ return {
146
+ path: normalizedPath,
147
+ type: type,
148
+ fileName: fileName
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Resolves system files
154
+ * @function resolveSystemFiles
155
+ * @param {string} schemaBasePath - Schema base path
156
+ * @param {string[]} systemFiles - Array of system file names
157
+ * @returns {Object[]} Array of resolved file objects
158
+ */
159
+ function resolveSystemFiles(schemaBasePath, systemFiles) {
160
+ if (!systemFiles || !Array.isArray(systemFiles)) {
161
+ return [];
162
+ }
163
+
164
+ return systemFiles.map(fileName => resolveSingleExternalFile(schemaBasePath, fileName, 'system'));
165
+ }
166
+
167
+ /**
168
+ * Resolves datasource files
169
+ * @function resolveDatasourceFiles
170
+ * @param {string} schemaBasePath - Schema base path
171
+ * @param {string[]} datasourceFiles - Array of datasource file names
172
+ * @returns {Object[]} Array of resolved file objects
173
+ */
174
+ function resolveDatasourceFiles(schemaBasePath, datasourceFiles) {
175
+ if (!datasourceFiles || !Array.isArray(datasourceFiles)) {
176
+ return [];
177
+ }
178
+
179
+ return datasourceFiles.map(fileName => resolveSingleExternalFile(schemaBasePath, fileName, 'datasource'));
180
+ }
181
+
182
+ /**
183
+ * Loads and validates variables.yaml
184
+ * @async
185
+ * @function loadAndValidateVariables
186
+ * @param {string} appPath - Application path
187
+ * @returns {Promise<Object>} Variables object
188
+ * @throws {Error} If file not found or invalid
189
+ */
190
+ async function loadAndValidateVariables(appPath) {
112
191
  const variablesPath = path.join(appPath, 'variables.yaml');
113
192
 
114
193
  if (!fs.existsSync(variablesPath)) {
@@ -116,60 +195,30 @@ async function resolveExternalFiles(appName) {
116
195
  }
117
196
 
118
197
  const content = fs.readFileSync(variablesPath, 'utf8');
119
- let variables;
120
-
121
198
  try {
122
- variables = yaml.load(content);
199
+ return yaml.load(content);
123
200
  } catch (error) {
124
201
  throw new Error(`Invalid YAML syntax in variables.yaml: ${error.message}`);
125
202
  }
203
+ }
126
204
 
127
- // Check if externalIntegration block exists
128
- if (!variables.externalIntegration) {
129
- return []; // No external integration, return empty array
205
+ async function resolveExternalFiles(appName) {
206
+ if (!appName || typeof appName !== 'string') {
207
+ throw new Error('App name is required and must be a string');
130
208
  }
131
209
 
132
- // Resolve schema base path
133
- const schemaBasePath = await resolveSchemaBasePath(appName);
134
- const resolvedFiles = [];
135
-
136
- // Resolve systems files
137
- if (variables.externalIntegration.systems && Array.isArray(variables.externalIntegration.systems)) {
138
- for (const systemFile of variables.externalIntegration.systems) {
139
- const systemPath = path.join(schemaBasePath, systemFile);
140
- const normalizedPath = path.normalize(systemPath);
141
-
142
- if (!fs.existsSync(normalizedPath)) {
143
- throw new Error(`External system file not found: ${normalizedPath}`);
144
- }
145
-
146
- resolvedFiles.push({
147
- path: normalizedPath,
148
- type: 'system',
149
- fileName: systemFile
150
- });
151
- }
152
- }
210
+ const { appPath } = await detectAppType(appName);
211
+ const variables = await loadAndValidateVariables(appPath);
153
212
 
154
- // Resolve datasources files
155
- if (variables.externalIntegration.dataSources && Array.isArray(variables.externalIntegration.dataSources)) {
156
- for (const datasourceFile of variables.externalIntegration.dataSources) {
157
- const datasourcePath = path.join(schemaBasePath, datasourceFile);
158
- const normalizedPath = path.normalize(datasourcePath);
159
-
160
- if (!fs.existsSync(normalizedPath)) {
161
- throw new Error(`External datasource file not found: ${normalizedPath}`);
162
- }
163
-
164
- resolvedFiles.push({
165
- path: normalizedPath,
166
- type: 'datasource',
167
- fileName: datasourceFile
168
- });
169
- }
213
+ if (!variables.externalIntegration) {
214
+ return [];
170
215
  }
171
216
 
172
- return resolvedFiles;
217
+ const schemaBasePath = await resolveSchemaBasePath(appName);
218
+ const systemFiles = resolveSystemFiles(schemaBasePath, variables.externalIntegration.systems);
219
+ const datasourceFiles = resolveDatasourceFiles(schemaBasePath, variables.externalIntegration.dataSources);
220
+
221
+ return [...systemFiles, ...datasourceFiles];
173
222
  }
174
223
 
175
224
  module.exports = {
@@ -131,48 +131,85 @@ function encryptSecret(value, key) {
131
131
  * const decrypted = decryptSecret('secure://<iv>:<ciphertext>:<authTag>', 'a1b2c3...');
132
132
  * // Returns: 'my-secret'
133
133
  */
134
- function decryptSecret(encryptedValue, key) {
134
+ /**
135
+ * Validates encrypted value format
136
+ * @function validateEncryptedValueFormat
137
+ * @param {string} encryptedValue - Encrypted value
138
+ * @throws {Error} If format is invalid
139
+ */
140
+ function validateEncryptedValueFormat(encryptedValue) {
135
141
  if (!encryptedValue || typeof encryptedValue !== 'string') {
136
142
  throw new Error('Encrypted value is required and must be a string');
137
143
  }
138
-
139
144
  if (!encryptedValue.startsWith('secure://')) {
140
145
  throw new Error('Encrypted value must start with secure:// prefix');
141
146
  }
147
+ }
142
148
 
143
- const keyBuffer = normalizeKey(key);
144
-
145
- // Remove secure:// prefix
149
+ /**
150
+ * Parses encrypted value parts
151
+ * @function parseEncryptedValueParts
152
+ * @param {string} encryptedValue - Encrypted value
153
+ * @returns {Object} Object with ivBase64, ciphertext, and authTagBase64
154
+ * @throws {Error} If format is invalid
155
+ */
156
+ function parseEncryptedValueParts(encryptedValue) {
146
157
  const parts = encryptedValue.substring(9).split(':');
147
158
  if (parts.length !== 3) {
148
159
  throw new Error('Invalid encrypted value format. Expected: secure://<iv>:<ciphertext>:<authTag>');
149
160
  }
161
+ return {
162
+ ivBase64: parts[0],
163
+ ciphertext: parts[1],
164
+ authTagBase64: parts[2]
165
+ };
166
+ }
150
167
 
151
- const [ivBase64, ciphertext, authTagBase64] = parts;
152
-
153
- try {
154
- // Decode IV and auth tag
155
- const iv = Buffer.from(ivBase64, 'base64');
156
- const authTag = Buffer.from(authTagBase64, 'base64');
168
+ /**
169
+ * Validates IV and auth tag lengths
170
+ * @function validateDecryptionComponents
171
+ * @param {Buffer} iv - Initialization vector
172
+ * @param {Buffer} authTag - Authentication tag
173
+ * @throws {Error} If lengths are invalid
174
+ */
175
+ function validateDecryptionComponents(iv, authTag) {
176
+ if (iv.length !== IV_LENGTH) {
177
+ throw new Error(`Invalid IV length: expected ${IV_LENGTH} bytes, got ${iv.length}`);
178
+ }
179
+ if (authTag.length !== AUTH_TAG_LENGTH) {
180
+ throw new Error(`Invalid auth tag length: expected ${AUTH_TAG_LENGTH} bytes, got ${authTag.length}`);
181
+ }
182
+ }
157
183
 
158
- // Validate lengths
159
- if (iv.length !== IV_LENGTH) {
160
- throw new Error(`Invalid IV length: expected ${IV_LENGTH} bytes, got ${iv.length}`);
161
- }
184
+ /**
185
+ * Performs decryption
186
+ * @function performDecryption
187
+ * @param {Buffer} keyBuffer - Key buffer
188
+ * @param {Buffer} iv - Initialization vector
189
+ * @param {string} ciphertext - Ciphertext
190
+ * @param {Buffer} authTag - Authentication tag
191
+ * @returns {string} Decrypted plaintext
192
+ */
193
+ function performDecryption(keyBuffer, iv, ciphertext, authTag) {
194
+ const decipher = crypto.createDecipheriv(ALGORITHM, keyBuffer, iv);
195
+ decipher.setAuthTag(authTag);
196
+ let plaintext = decipher.update(ciphertext, 'base64', 'utf8');
197
+ plaintext += decipher.final('utf8');
198
+ return plaintext;
199
+ }
162
200
 
163
- if (authTag.length !== AUTH_TAG_LENGTH) {
164
- throw new Error(`Invalid auth tag length: expected ${AUTH_TAG_LENGTH} bytes, got ${authTag.length}`);
165
- }
201
+ function decryptSecret(encryptedValue, key) {
202
+ validateEncryptedValueFormat(encryptedValue);
166
203
 
167
- // Create decipher
168
- const decipher = crypto.createDecipheriv(ALGORITHM, keyBuffer, iv);
169
- decipher.setAuthTag(authTag);
204
+ const keyBuffer = normalizeKey(key);
205
+ const { ivBase64, ciphertext, authTagBase64 } = parseEncryptedValueParts(encryptedValue);
170
206
 
171
- // Decrypt
172
- let plaintext = decipher.update(ciphertext, 'base64', 'utf8');
173
- plaintext += decipher.final('utf8');
207
+ try {
208
+ const iv = Buffer.from(ivBase64, 'base64');
209
+ const authTag = Buffer.from(authTagBase64, 'base64');
174
210
 
175
- return plaintext;
211
+ validateDecryptionComponents(iv, authTag);
212
+ return performDecryption(keyBuffer, iv, ciphertext, authTag);
176
213
  } catch (error) {
177
214
  // Don't expose sensitive details in error messages
178
215
  if (error.message.includes('Unsupported state') || error.message.includes('bad decrypt')) {