@aifabrix/builder 2.6.3 → 2.8.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 (43) hide show
  1. package/.cursor/rules/project-rules.mdc +680 -0
  2. package/bin/aifabrix.js +4 -0
  3. package/lib/app-config.js +10 -0
  4. package/lib/app-deploy.js +18 -0
  5. package/lib/app-dockerfile.js +15 -0
  6. package/lib/app-prompts.js +172 -9
  7. package/lib/app-push.js +15 -0
  8. package/lib/app-register.js +14 -0
  9. package/lib/app-run.js +25 -0
  10. package/lib/app.js +30 -13
  11. package/lib/audit-logger.js +9 -4
  12. package/lib/build.js +8 -0
  13. package/lib/cli.js +99 -2
  14. package/lib/commands/datasource.js +94 -0
  15. package/lib/commands/login.js +40 -3
  16. package/lib/config.js +121 -114
  17. package/lib/datasource-deploy.js +182 -0
  18. package/lib/datasource-diff.js +73 -0
  19. package/lib/datasource-list.js +138 -0
  20. package/lib/datasource-validate.js +63 -0
  21. package/lib/diff.js +266 -0
  22. package/lib/environment-deploy.js +305 -0
  23. package/lib/external-system-deploy.js +262 -0
  24. package/lib/external-system-generator.js +187 -0
  25. package/lib/schema/application-schema.json +869 -698
  26. package/lib/schema/external-datasource.schema.json +512 -0
  27. package/lib/schema/external-system.schema.json +262 -0
  28. package/lib/schema/infrastructure-schema.json +1 -1
  29. package/lib/secrets.js +20 -1
  30. package/lib/templates.js +32 -1
  31. package/lib/utils/device-code.js +10 -2
  32. package/lib/utils/env-copy.js +24 -0
  33. package/lib/utils/env-endpoints.js +50 -11
  34. package/lib/utils/schema-loader.js +220 -0
  35. package/lib/utils/schema-resolver.js +174 -0
  36. package/lib/utils/secrets-helpers.js +65 -17
  37. package/lib/utils/token-encryption.js +68 -0
  38. package/lib/validate.js +299 -0
  39. package/lib/validator.js +47 -3
  40. package/package.json +1 -1
  41. package/tatus +181 -0
  42. package/templates/external-system/external-datasource.json.hbs +55 -0
  43. package/templates/external-system/external-system.json.hbs +37 -0
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Schema Loading Utilities
3
+ *
4
+ * Loads and compiles JSON schemas for validation.
5
+ * Provides schema type detection and cached validators.
6
+ *
7
+ * @fileoverview Schema loading utilities for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const Ajv = require('ajv');
15
+
16
+ // Cache for compiled validators
17
+ // These are reset when module is reloaded (for testing)
18
+ let externalSystemValidator = null;
19
+ let externalDataSourceValidator = null;
20
+
21
+ /**
22
+ * Reset validators cache (for testing)
23
+ * @function resetValidators
24
+ */
25
+ function resetValidators() {
26
+ externalSystemValidator = null;
27
+ externalDataSourceValidator = null;
28
+ }
29
+
30
+ /**
31
+ * Loads and compiles external-system schema
32
+ * Caches the compiled validator for performance
33
+ *
34
+ * @function loadExternalSystemSchema
35
+ * @returns {Function} Compiled AJV validator function
36
+ * @throws {Error} If schema file cannot be loaded or compiled
37
+ *
38
+ * @example
39
+ * const validate = loadExternalSystemSchema();
40
+ * const valid = validate(data);
41
+ */
42
+ function loadExternalSystemSchema() {
43
+ if (externalSystemValidator) {
44
+ return externalSystemValidator;
45
+ }
46
+
47
+ const schemaPath = path.join(__dirname, '..', 'schema', 'external-system.schema.json');
48
+
49
+ if (!fs.existsSync(schemaPath)) {
50
+ throw new Error(`External system schema not found: ${schemaPath}`);
51
+ }
52
+
53
+ const schemaContent = fs.readFileSync(schemaPath, 'utf8');
54
+ let schema;
55
+
56
+ try {
57
+ schema = JSON.parse(schemaContent);
58
+ } catch (error) {
59
+ throw new Error(`Invalid JSON in external-system.schema.json: ${error.message}`);
60
+ }
61
+
62
+ const ajv = new Ajv({ allErrors: true, strict: false });
63
+ externalSystemValidator = ajv.compile(schema);
64
+
65
+ return externalSystemValidator;
66
+ }
67
+
68
+ /**
69
+ * Loads and compiles external-datasource schema
70
+ * Caches the compiled validator for performance
71
+ *
72
+ * @function loadExternalDataSourceSchema
73
+ * @returns {Function} Compiled AJV validator function
74
+ * @throws {Error} If schema file cannot be loaded or compiled
75
+ *
76
+ * @example
77
+ * const validate = loadExternalDataSourceSchema();
78
+ * const valid = validate(data);
79
+ */
80
+ function loadExternalDataSourceSchema() {
81
+ if (externalDataSourceValidator) {
82
+ return externalDataSourceValidator;
83
+ }
84
+
85
+ const schemaPath = path.join(__dirname, '..', 'schema', 'external-datasource.schema.json');
86
+
87
+ if (!fs.existsSync(schemaPath)) {
88
+ throw new Error(`External datasource schema not found: ${schemaPath}`);
89
+ }
90
+
91
+ const schemaContent = fs.readFileSync(schemaPath, 'utf8');
92
+ let schema;
93
+
94
+ try {
95
+ schema = JSON.parse(schemaContent);
96
+ } catch (error) {
97
+ throw new Error(`Invalid JSON in external-datasource.schema.json: ${error.message}`);
98
+ }
99
+
100
+ // For draft-2020-12 schemas, we need to handle $schema differently
101
+ // Remove $schema if it's draft-2020-12 to avoid AJV issues
102
+ const schemaToCompile = { ...schema };
103
+ if (schemaToCompile.$schema && schemaToCompile.$schema.includes('2020-12')) {
104
+ // AJV v8 supports draft-2020-12 but may need the schema without $schema for compilation
105
+ delete schemaToCompile.$schema;
106
+ }
107
+
108
+ const ajv = new Ajv({ allErrors: true, strict: false, strictSchema: false });
109
+ externalDataSourceValidator = ajv.compile(schemaToCompile);
110
+
111
+ return externalDataSourceValidator;
112
+ }
113
+
114
+ /**
115
+ * Detects schema type from file content or path
116
+ * Attempts to identify if file is application, external-system, or external-datasource
117
+ *
118
+ * @function detectSchemaType
119
+ * @param {string} filePath - Path to the file
120
+ * @param {string} [content] - Optional file content (if not provided, will be read from file)
121
+ * @returns {string} Schema type: 'application' | 'external-system' | 'external-datasource'
122
+ * @throws {Error} If file cannot be read or parsed
123
+ *
124
+ * @example
125
+ * const type = detectSchemaType('./hubspot.json');
126
+ * // Returns: 'external-system'
127
+ */
128
+ function detectSchemaType(filePath, content) {
129
+ let fileContent = content;
130
+
131
+ // Read file if content not provided
132
+ if (!fileContent) {
133
+ if (!fs.existsSync(filePath)) {
134
+ throw new Error(`File not found: ${filePath}`);
135
+ }
136
+ fileContent = fs.readFileSync(filePath, 'utf8');
137
+ }
138
+
139
+ // Try to parse JSON
140
+ let parsed;
141
+ try {
142
+ parsed = JSON.parse(fileContent);
143
+ } catch (error) {
144
+ throw new Error(`Invalid JSON in file: ${error.message}`);
145
+ }
146
+
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
+ }
160
+
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
+ }
174
+
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
+ }
187
+
188
+ // Check for datasource-specific fields
189
+ if (parsed.systemKey && parsed.entityKey && parsed.fieldMappings) {
190
+ return 'external-datasource';
191
+ }
192
+
193
+ // Check for application-specific fields
194
+ if (parsed.deploymentKey || (parsed.image && parsed.registryMode && parsed.port)) {
195
+ return 'application';
196
+ }
197
+
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
+ }
209
+
210
+ // Default to application if cannot determine
211
+ return 'application';
212
+ }
213
+
214
+ module.exports = {
215
+ loadExternalSystemSchema,
216
+ loadExternalDataSourceSchema,
217
+ detectSchemaType,
218
+ resetValidators
219
+ };
220
+
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Schema Resolution Utilities
3
+ *
4
+ * Resolves paths for external integration schemas from application configuration.
5
+ * Handles schemaBasePath resolution and external file discovery.
6
+ *
7
+ * @fileoverview Schema path resolution utilities for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const yaml = require('js-yaml');
15
+
16
+ /**
17
+ * Resolves schemaBasePath from application variables.yaml
18
+ * Supports both absolute and relative paths
19
+ *
20
+ * @async
21
+ * @function resolveSchemaBasePath
22
+ * @param {string} appName - Application name
23
+ * @returns {Promise<string>} Resolved absolute path to schema base directory
24
+ * @throws {Error} If variables.yaml not found, externalIntegration missing, or path invalid
25
+ *
26
+ * @example
27
+ * const basePath = await resolveSchemaBasePath('myapp');
28
+ * // Returns: '/path/to/builder/myapp/schemas'
29
+ */
30
+ async function resolveSchemaBasePath(appName) {
31
+ if (!appName || typeof appName !== 'string') {
32
+ throw new Error('App name is required and must be a string');
33
+ }
34
+
35
+ const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
36
+
37
+ if (!fs.existsSync(variablesPath)) {
38
+ throw new Error(`variables.yaml not found: ${variablesPath}`);
39
+ }
40
+
41
+ const content = fs.readFileSync(variablesPath, 'utf8');
42
+ let variables;
43
+
44
+ try {
45
+ variables = yaml.load(content);
46
+ } catch (error) {
47
+ throw new Error(`Invalid YAML syntax in variables.yaml: ${error.message}`);
48
+ }
49
+
50
+ // Check if externalIntegration block exists
51
+ if (!variables.externalIntegration) {
52
+ throw new Error(`externalIntegration block not found in variables.yaml for app: ${appName}`);
53
+ }
54
+
55
+ if (!variables.externalIntegration.schemaBasePath) {
56
+ throw new Error(`schemaBasePath not found in externalIntegration block for app: ${appName}`);
57
+ }
58
+
59
+ const schemaBasePath = variables.externalIntegration.schemaBasePath;
60
+ const variablesDir = path.dirname(variablesPath);
61
+
62
+ // Resolve path (absolute or relative to variables.yaml location)
63
+ let resolvedPath;
64
+ if (path.isAbsolute(schemaBasePath)) {
65
+ resolvedPath = schemaBasePath;
66
+ } else {
67
+ resolvedPath = path.resolve(variablesDir, schemaBasePath);
68
+ }
69
+
70
+ // Normalize path
71
+ resolvedPath = path.normalize(resolvedPath);
72
+
73
+ // Validate path exists
74
+ if (!fs.existsSync(resolvedPath)) {
75
+ throw new Error(`Schema base path does not exist: ${resolvedPath}`);
76
+ }
77
+
78
+ if (!fs.statSync(resolvedPath).isDirectory()) {
79
+ throw new Error(`Schema base path is not a directory: ${resolvedPath}`);
80
+ }
81
+
82
+ return resolvedPath;
83
+ }
84
+
85
+ /**
86
+ * Resolves all external system and datasource files from application configuration
87
+ * Returns array of file paths with metadata
88
+ *
89
+ * @async
90
+ * @function resolveExternalFiles
91
+ * @param {string} appName - Application name
92
+ * @returns {Promise<Array<{path: string, type: 'system'|'datasource', fileName: string}>>} Array of resolved file paths with metadata
93
+ * @throws {Error} If files cannot be resolved or do not exist
94
+ *
95
+ * @example
96
+ * const files = await resolveExternalFiles('myapp');
97
+ * // Returns: [
98
+ * // { path: '/path/to/hubspot.json', type: 'system', fileName: 'hubspot.json' },
99
+ * // { path: '/path/to/hubspot-deal.json', type: 'datasource', fileName: 'hubspot-deal.json' }
100
+ * // ]
101
+ */
102
+ async function resolveExternalFiles(appName) {
103
+ if (!appName || typeof appName !== 'string') {
104
+ throw new Error('App name is required and must be a string');
105
+ }
106
+
107
+ const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
108
+
109
+ if (!fs.existsSync(variablesPath)) {
110
+ throw new Error(`variables.yaml not found: ${variablesPath}`);
111
+ }
112
+
113
+ const content = fs.readFileSync(variablesPath, 'utf8');
114
+ let variables;
115
+
116
+ try {
117
+ variables = yaml.load(content);
118
+ } catch (error) {
119
+ throw new Error(`Invalid YAML syntax in variables.yaml: ${error.message}`);
120
+ }
121
+
122
+ // Check if externalIntegration block exists
123
+ if (!variables.externalIntegration) {
124
+ return []; // No external integration, return empty array
125
+ }
126
+
127
+ // Resolve schema base path
128
+ const schemaBasePath = await resolveSchemaBasePath(appName);
129
+ const resolvedFiles = [];
130
+
131
+ // Resolve systems files
132
+ if (variables.externalIntegration.systems && Array.isArray(variables.externalIntegration.systems)) {
133
+ for (const systemFile of variables.externalIntegration.systems) {
134
+ const systemPath = path.join(schemaBasePath, systemFile);
135
+ const normalizedPath = path.normalize(systemPath);
136
+
137
+ if (!fs.existsSync(normalizedPath)) {
138
+ throw new Error(`External system file not found: ${normalizedPath}`);
139
+ }
140
+
141
+ resolvedFiles.push({
142
+ path: normalizedPath,
143
+ type: 'system',
144
+ fileName: systemFile
145
+ });
146
+ }
147
+ }
148
+
149
+ // Resolve datasources files
150
+ if (variables.externalIntegration.dataSources && Array.isArray(variables.externalIntegration.dataSources)) {
151
+ for (const datasourceFile of variables.externalIntegration.dataSources) {
152
+ const datasourcePath = path.join(schemaBasePath, datasourceFile);
153
+ const normalizedPath = path.normalize(datasourcePath);
154
+
155
+ if (!fs.existsSync(normalizedPath)) {
156
+ throw new Error(`External datasource file not found: ${normalizedPath}`);
157
+ }
158
+
159
+ resolvedFiles.push({
160
+ path: normalizedPath,
161
+ type: 'datasource',
162
+ fileName: datasourceFile
163
+ });
164
+ }
165
+ }
166
+
167
+ return resolvedFiles;
168
+ }
169
+
170
+ module.exports = {
171
+ resolveSchemaBasePath,
172
+ resolveExternalFiles
173
+ };
174
+
@@ -13,10 +13,10 @@ const path = require('path');
13
13
  const yaml = require('js-yaml');
14
14
  const config = require('../config');
15
15
  const { buildHostnameToServiceMap, resolveUrlPort } = require('./secrets-utils');
16
- const { rewriteInfraEndpoints, getEnvHosts } = require('./env-endpoints');
16
+ const { rewriteInfraEndpoints, getEnvHosts, getServicePort, getServiceHost, getLocalhostOverride } = require('./env-endpoints');
17
17
  const { loadEnvConfig } = require('./env-config-loader');
18
- const { processEnvVariables } = require('./env-copy');
19
18
  const { updateContainerPortInEnvFile } = require('./env-ports');
19
+ const { buildEnvVarMap } = require('./env-map');
20
20
 
21
21
  /**
22
22
  * Interpolate ${VAR} occurrences with values from envVars map
@@ -262,6 +262,24 @@ async function adjustLocalEnvPortsInContent(envContent, variablesPath) {
262
262
  // Update infra endpoints with developer-id adjusted ports for local context
263
263
  updated = await rewriteInfraEndpoints(updated, 'local');
264
264
 
265
+ // Interpolate ${VAR} references created by rewriteInfraEndpoints
266
+ // Get the ports that were just set by rewriteInfraEndpoints for interpolation
267
+ const hostsForPorts = await getEnvHosts('local');
268
+ const redisPort = await getServicePort('REDIS_PORT', 'redis', hostsForPorts, 'local');
269
+ const dbPort = await getServicePort('DB_PORT', 'postgres', hostsForPorts, 'local');
270
+ const localhostOverride = getLocalhostOverride('local');
271
+ const redisHost = getServiceHost(hostsForPorts.REDIS_HOST, 'local', 'localhost', localhostOverride);
272
+ const dbHost = getServiceHost(hostsForPorts.DB_HOST, 'local', 'localhost', localhostOverride);
273
+
274
+ // Build envVars map and ensure it has the correct values
275
+ const envVars = await buildEnvVarMap('local', null, devIdNum);
276
+ // Override with the actual values that were just set by rewriteInfraEndpoints
277
+ envVars.REDIS_HOST = redisHost;
278
+ envVars.REDIS_PORT = String(redisPort);
279
+ envVars.DB_HOST = dbHost;
280
+ envVars.DB_PORT = String(dbPort);
281
+ updated = interpolateEnvVars(updated, envVars);
282
+
265
283
  return updated;
266
284
  }
267
285
 
@@ -276,6 +294,29 @@ function readYamlAtPath(filePath) {
276
294
  return yaml.load(content);
277
295
  }
278
296
 
297
+ /**
298
+ * Merge a single secret value from canonical into result
299
+ * @function mergeSecretValue
300
+ * @param {Object} result - Result object to merge into
301
+ * @param {string} key - Secret key
302
+ * @param {*} canonicalValue - Value from canonical secrets
303
+ */
304
+ function mergeSecretValue(result, key, canonicalValue) {
305
+ const currentValue = result[key];
306
+ // Fill missing, empty, or undefined values
307
+ if (!(key in result) || currentValue === undefined || currentValue === null || currentValue === '') {
308
+ result[key] = canonicalValue;
309
+ return;
310
+ }
311
+ // Only replace values that are encrypted (have secure:// prefix)
312
+ // Plaintext values (no secure://) are used as-is
313
+ if (typeof currentValue === 'string' && typeof canonicalValue === 'string') {
314
+ if (currentValue.startsWith('secure://')) {
315
+ result[key] = canonicalValue;
316
+ }
317
+ }
318
+ }
319
+
279
320
  /**
280
321
  * Apply canonical secrets path override if configured and file exists
281
322
  * @async
@@ -287,21 +328,29 @@ async function applyCanonicalSecretsOverride(currentSecrets) {
287
328
  let mergedSecrets = currentSecrets || {};
288
329
  try {
289
330
  const canonicalPath = await config.getSecretsPath();
290
- if (canonicalPath) {
291
- const resolvedCanonical = path.isAbsolute(canonicalPath)
292
- ? canonicalPath
293
- : path.resolve(process.cwd(), canonicalPath);
294
- if (fs.existsSync(resolvedCanonical)) {
295
- const configSecrets = readYamlAtPath(resolvedCanonical);
296
- // Apply canonical secrets as a fallback source:
297
- // - Do NOT override any existing keys from user/build
298
- // - Add only missing keys from canonical path
299
- if (configSecrets && typeof configSecrets === 'object') {
300
- const result = { ...configSecrets, ...mergedSecrets };
301
- mergedSecrets = result;
302
- }
303
- }
331
+ if (!canonicalPath) {
332
+ return mergedSecrets;
333
+ }
334
+ const resolvedCanonical = path.isAbsolute(canonicalPath)
335
+ ? canonicalPath
336
+ : path.resolve(process.cwd(), canonicalPath);
337
+ if (!fs.existsSync(resolvedCanonical)) {
338
+ return mergedSecrets;
339
+ }
340
+ const configSecrets = readYamlAtPath(resolvedCanonical);
341
+ if (!configSecrets || typeof configSecrets !== 'object') {
342
+ return mergedSecrets;
343
+ }
344
+ // Apply canonical secrets as a fallback source:
345
+ // - Do NOT override any existing keys from user/build
346
+ // - Add only missing keys from canonical path
347
+ // - Also fill in empty/undefined values from canonical path
348
+ // - Replace encrypted values (secure://) with canonical plaintext
349
+ const result = { ...mergedSecrets };
350
+ for (const [key, canonicalValue] of Object.entries(configSecrets)) {
351
+ mergeSecretValue(result, key, canonicalValue);
304
352
  }
353
+ mergedSecrets = result;
305
354
  } catch {
306
355
  // ignore and fall through
307
356
  }
@@ -351,7 +400,6 @@ module.exports = {
351
400
  replaceKvInContent,
352
401
  resolveServicePortsInEnvContent,
353
402
  loadEnvTemplate,
354
- processEnvVariables,
355
403
  updateContainerPortInEnvFile,
356
404
  adjustLocalEnvPortsInContent,
357
405
  readYamlAtPath,
@@ -0,0 +1,68 @@
1
+ /**
2
+ * AI Fabrix Builder Token Encryption Utilities
3
+ *
4
+ * This module provides encryption and decryption functions for authentication tokens
5
+ * using AES-256-GCM algorithm for ISO 27001 compliance.
6
+ * Reuses the same encryption infrastructure as secrets encryption.
7
+ *
8
+ * @fileoverview Token encryption utilities for AI Fabrix Builder
9
+ * @author AI Fabrix Team
10
+ * @version 2.0.0
11
+ */
12
+
13
+ const { encryptSecret, decryptSecret, isEncrypted } = require('./secrets-encryption');
14
+
15
+ /**
16
+ * Encrypts a token value using AES-256-GCM
17
+ * Returns encrypted value in format: secure://<iv>:<ciphertext>:<authTag>
18
+ * All components are base64 encoded
19
+ *
20
+ * @function encryptToken
21
+ * @param {string} value - Plaintext token value to encrypt
22
+ * @param {string} key - Encryption key (hex or base64, 32 bytes)
23
+ * @returns {string} Encrypted value with secure:// prefix
24
+ * @throws {Error} If encryption fails or key is invalid
25
+ *
26
+ * @example
27
+ * const encrypted = encryptToken('my-token', 'a1b2c3...');
28
+ * // Returns: 'secure://<iv>:<ciphertext>:<authTag>'
29
+ */
30
+ function encryptToken(value, key) {
31
+ return encryptSecret(value, key);
32
+ }
33
+
34
+ /**
35
+ * Decrypts an encrypted token value
36
+ * Handles secure:// prefixed values and extracts IV, ciphertext, and auth tag
37
+ *
38
+ * @function decryptToken
39
+ * @param {string} encryptedValue - Encrypted value with secure:// prefix
40
+ * @param {string} key - Encryption key (hex or base64, 32 bytes)
41
+ * @returns {string} Decrypted plaintext value
42
+ * @throws {Error} If decryption fails, key is invalid, or format is incorrect
43
+ *
44
+ * @example
45
+ * const decrypted = decryptToken('secure://<iv>:<ciphertext>:<authTag>', 'a1b2c3...');
46
+ * // Returns: 'my-token'
47
+ */
48
+ function decryptToken(encryptedValue, key) {
49
+ return decryptSecret(encryptedValue, key);
50
+ }
51
+
52
+ /**
53
+ * Checks if a token value is encrypted (starts with secure://)
54
+ *
55
+ * @function isTokenEncrypted
56
+ * @param {string} value - Value to check
57
+ * @returns {boolean} True if value is encrypted
58
+ */
59
+ function isTokenEncrypted(value) {
60
+ return isEncrypted(value);
61
+ }
62
+
63
+ module.exports = {
64
+ encryptToken,
65
+ decryptToken,
66
+ isTokenEncrypted
67
+ };
68
+