@aifabrix/builder 2.7.0 → 2.9.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.
- package/.cursor/rules/project-rules.mdc +680 -0
- package/integration/hubspot/README.md +136 -0
- package/integration/hubspot/env.template +9 -0
- package/integration/hubspot/hubspot-deploy-company.json +200 -0
- package/integration/hubspot/hubspot-deploy-contact.json +228 -0
- package/integration/hubspot/hubspot-deploy-deal.json +248 -0
- package/integration/hubspot/hubspot-deploy.json +91 -0
- package/integration/hubspot/variables.yaml +17 -0
- package/lib/app-config.js +13 -2
- package/lib/app-deploy.js +9 -3
- package/lib/app-dockerfile.js +14 -1
- package/lib/app-prompts.js +177 -13
- package/lib/app-push.js +16 -1
- package/lib/app-register.js +37 -5
- package/lib/app-rotate-secret.js +10 -0
- package/lib/app-run.js +19 -0
- package/lib/app.js +70 -25
- package/lib/audit-logger.js +9 -4
- package/lib/build.js +25 -13
- package/lib/cli.js +109 -2
- package/lib/commands/login.js +40 -3
- package/lib/config.js +121 -114
- package/lib/datasource-deploy.js +14 -20
- package/lib/environment-deploy.js +305 -0
- package/lib/external-system-deploy.js +345 -0
- package/lib/external-system-download.js +431 -0
- package/lib/external-system-generator.js +190 -0
- package/lib/external-system-test.js +446 -0
- package/lib/generator-builders.js +323 -0
- package/lib/generator.js +200 -292
- package/lib/schema/application-schema.json +830 -800
- package/lib/schema/external-datasource.schema.json +868 -46
- package/lib/schema/external-system.schema.json +98 -80
- package/lib/schema/infrastructure-schema.json +1 -1
- package/lib/templates.js +32 -1
- package/lib/utils/cli-utils.js +4 -4
- package/lib/utils/device-code.js +10 -2
- package/lib/utils/external-system-display.js +159 -0
- package/lib/utils/external-system-validators.js +245 -0
- package/lib/utils/paths.js +151 -1
- package/lib/utils/schema-resolver.js +7 -2
- package/lib/utils/token-encryption.js +68 -0
- package/lib/validator.js +52 -5
- package/package.json +1 -1
- package/tatus +181 -0
- package/templates/external-system/external-datasource.json.hbs +55 -0
- package/templates/external-system/external-system.json.hbs +37 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External System Validation Helpers
|
|
3
|
+
*
|
|
4
|
+
* Provides validation functions for external system field mappings and schemas.
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview Validation helpers for external system testing
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const Ajv = require('ajv');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Validates field mapping expression syntax (pipe-based DSL)
|
|
15
|
+
* @param {string} expression - Field mapping expression
|
|
16
|
+
* @returns {Object} Validation result with isValid and error message
|
|
17
|
+
*/
|
|
18
|
+
function validateFieldMappingExpression(expression) {
|
|
19
|
+
if (!expression || typeof expression !== 'string') {
|
|
20
|
+
return { isValid: false, error: 'Expression must be a non-empty string' };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Pattern: {{path.to.field}} | transformation1 | transformation2
|
|
24
|
+
const expressionPattern = /^\s*\{\{[^}]+\}\}(\s*\|\s*[a-zA-Z0-9_]+(\s*\([^)]*\))?)*\s*$/;
|
|
25
|
+
|
|
26
|
+
if (!expressionPattern.test(expression)) {
|
|
27
|
+
return {
|
|
28
|
+
isValid: false,
|
|
29
|
+
error: 'Invalid expression format. Expected: {{path.to.field}} | toUpper | trim'
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Extract path and transformations
|
|
34
|
+
const pathMatch = expression.match(/\{\{([^}]+)\}\}/);
|
|
35
|
+
if (!pathMatch) {
|
|
36
|
+
return { isValid: false, error: 'Path must be wrapped in {{}}' };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Validate transformations (optional)
|
|
40
|
+
const transformations = expression.split('|').slice(1).map(t => t.trim());
|
|
41
|
+
const validTransformations = ['toUpper', 'toLower', 'trim', 'default', 'toNumber', 'toString'];
|
|
42
|
+
for (const trans of transformations) {
|
|
43
|
+
const transName = trans.split('(')[0].trim();
|
|
44
|
+
if (!validTransformations.includes(transName)) {
|
|
45
|
+
return {
|
|
46
|
+
isValid: false,
|
|
47
|
+
error: `Unknown transformation: ${transName}. Valid: ${validTransformations.join(', ')}`
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { isValid: true, error: null };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Validates field mappings against test payload
|
|
57
|
+
* @param {Object} datasource - Datasource configuration
|
|
58
|
+
* @param {Object} testPayload - Test payload object
|
|
59
|
+
* @returns {Object} Validation results
|
|
60
|
+
*/
|
|
61
|
+
function validateFieldMappings(datasource, testPayload) {
|
|
62
|
+
const results = {
|
|
63
|
+
valid: true,
|
|
64
|
+
errors: [],
|
|
65
|
+
warnings: [],
|
|
66
|
+
mappedFields: {}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
if (!datasource.fieldMappings || !datasource.fieldMappings.fields) {
|
|
70
|
+
results.warnings.push('No field mappings defined');
|
|
71
|
+
return results;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const fields = datasource.fieldMappings.fields;
|
|
75
|
+
const payloadTemplate = testPayload.payloadTemplate || testPayload;
|
|
76
|
+
|
|
77
|
+
// Validate each field mapping expression
|
|
78
|
+
for (const [fieldName, fieldConfig] of Object.entries(fields)) {
|
|
79
|
+
if (!fieldConfig.expression) {
|
|
80
|
+
results.errors.push(`Field '${fieldName}' missing expression`);
|
|
81
|
+
results.valid = false;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Validate expression syntax
|
|
86
|
+
const exprValidation = validateFieldMappingExpression(fieldConfig.expression);
|
|
87
|
+
if (!exprValidation.isValid) {
|
|
88
|
+
results.errors.push(`Field '${fieldName}': ${exprValidation.error}`);
|
|
89
|
+
results.valid = false;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Try to extract path from expression
|
|
94
|
+
const pathMatch = fieldConfig.expression.match(/\{\{([^}]+)\}\}/);
|
|
95
|
+
if (pathMatch) {
|
|
96
|
+
const fieldPath = pathMatch[1].trim();
|
|
97
|
+
// Check if path exists in payload (simple check)
|
|
98
|
+
const pathParts = fieldPath.split('.');
|
|
99
|
+
let current = payloadTemplate;
|
|
100
|
+
let pathExists = true;
|
|
101
|
+
|
|
102
|
+
for (const part of pathParts) {
|
|
103
|
+
if (current && typeof current === 'object' && part in current) {
|
|
104
|
+
current = current[part];
|
|
105
|
+
} else {
|
|
106
|
+
pathExists = false;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!pathExists) {
|
|
112
|
+
results.warnings.push(`Field '${fieldName}': Path '${fieldPath}' may not exist in payload`);
|
|
113
|
+
} else {
|
|
114
|
+
results.mappedFields[fieldName] = fieldConfig.expression;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return results;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Validates metadata schema against test payload
|
|
124
|
+
* @param {Object} datasource - Datasource configuration
|
|
125
|
+
* @param {Object} testPayload - Test payload object
|
|
126
|
+
* @returns {Object} Validation results
|
|
127
|
+
*/
|
|
128
|
+
function validateMetadataSchema(datasource, testPayload) {
|
|
129
|
+
const results = {
|
|
130
|
+
valid: true,
|
|
131
|
+
errors: [],
|
|
132
|
+
warnings: []
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
if (!datasource.metadataSchema) {
|
|
136
|
+
results.warnings.push('No metadata schema defined');
|
|
137
|
+
return results;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const payloadTemplate = testPayload.payloadTemplate || testPayload;
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
144
|
+
const validate = ajv.compile(datasource.metadataSchema);
|
|
145
|
+
const valid = validate(payloadTemplate);
|
|
146
|
+
|
|
147
|
+
if (!valid) {
|
|
148
|
+
results.valid = false;
|
|
149
|
+
results.errors = validate.errors.map(err => {
|
|
150
|
+
const path = err.instancePath || err.schemaPath;
|
|
151
|
+
return `${path} ${err.message}`;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
results.valid = false;
|
|
156
|
+
results.errors.push(`Schema validation error: ${error.message}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return results;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Normalizes schema to handle nullable without type
|
|
164
|
+
* @param {Object} schema - Schema to normalize
|
|
165
|
+
* @returns {Object} Normalized schema
|
|
166
|
+
*/
|
|
167
|
+
function normalizeSchema(schema) {
|
|
168
|
+
if (typeof schema !== 'object' || schema === null) {
|
|
169
|
+
return schema;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const normalized = Array.isArray(schema) ? [...schema] : { ...schema };
|
|
173
|
+
|
|
174
|
+
if (normalized.nullable === true && !normalized.type) {
|
|
175
|
+
normalized.type = ['null', 'string', 'number', 'boolean', 'object', 'array'];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
for (const key in normalized) {
|
|
179
|
+
if (typeof normalized[key] === 'object' && normalized[key] !== null) {
|
|
180
|
+
normalized[key] = normalizeSchema(normalized[key]);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return normalized;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Validates JSON against schema
|
|
189
|
+
* @param {Object} data - Data to validate
|
|
190
|
+
* @param {Object} schema - JSON schema
|
|
191
|
+
* @returns {Object} Validation results
|
|
192
|
+
*/
|
|
193
|
+
function validateAgainstSchema(data, schema) {
|
|
194
|
+
const ajv = new Ajv({
|
|
195
|
+
allErrors: true,
|
|
196
|
+
strict: false,
|
|
197
|
+
allowUnionTypes: true,
|
|
198
|
+
validateSchema: false
|
|
199
|
+
});
|
|
200
|
+
// Remove $schema for draft-2020-12 to avoid AJV issues
|
|
201
|
+
const schemaCopy = { ...schema };
|
|
202
|
+
if (schemaCopy.$schema && schemaCopy.$schema.includes('2020-12')) {
|
|
203
|
+
delete schemaCopy.$schema;
|
|
204
|
+
}
|
|
205
|
+
// Normalize schema to handle nullable without type
|
|
206
|
+
const normalizedSchema = normalizeSchema(schemaCopy);
|
|
207
|
+
const validate = ajv.compile(normalizedSchema);
|
|
208
|
+
const valid = validate(data);
|
|
209
|
+
|
|
210
|
+
if (!valid) {
|
|
211
|
+
// Filter out additionalProperties errors for required properties that aren't defined in schema
|
|
212
|
+
// This handles schema inconsistencies where authentication is required but not defined in properties
|
|
213
|
+
const filteredErrors = validate.errors.filter(err => {
|
|
214
|
+
if (err.keyword === 'additionalProperties' && err.params?.additionalProperty === 'authentication') {
|
|
215
|
+
// Check if authentication is in required array
|
|
216
|
+
const required = normalizedSchema.required || [];
|
|
217
|
+
if (required.includes('authentication')) {
|
|
218
|
+
return false; // Ignore this error since authentication is required but not defined
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return true;
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
valid: filteredErrors.length === 0,
|
|
226
|
+
errors: filteredErrors.map(err => {
|
|
227
|
+
const path = err.instancePath || err.schemaPath;
|
|
228
|
+
return `${path} ${err.message}`;
|
|
229
|
+
})
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
valid: true,
|
|
235
|
+
errors: []
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
module.exports = {
|
|
240
|
+
validateFieldMappingExpression,
|
|
241
|
+
validateFieldMappings,
|
|
242
|
+
validateMetadataSchema,
|
|
243
|
+
validateAgainstSchema
|
|
244
|
+
};
|
|
245
|
+
|
package/lib/utils/paths.js
CHANGED
|
@@ -91,9 +91,159 @@ function getDevDirectory(appName, developerId) {
|
|
|
91
91
|
return baseDir;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Gets the application path (builder or integration folder)
|
|
96
|
+
* @param {string} appName - Application name
|
|
97
|
+
* @param {string} [appType] - Application type ('external' or other)
|
|
98
|
+
* @returns {string} Absolute path to application directory
|
|
99
|
+
*/
|
|
100
|
+
function getAppPath(appName, appType) {
|
|
101
|
+
if (!appName || typeof appName !== 'string') {
|
|
102
|
+
throw new Error('App name is required and must be a string');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const baseDir = appType === 'external' ? 'integration' : 'builder';
|
|
106
|
+
return path.join(process.cwd(), baseDir, appName);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Gets the integration folder path for external systems
|
|
111
|
+
* @param {string} appName - Application name
|
|
112
|
+
* @returns {string} Absolute path to integration directory
|
|
113
|
+
*/
|
|
114
|
+
function getIntegrationPath(appName) {
|
|
115
|
+
if (!appName || typeof appName !== 'string') {
|
|
116
|
+
throw new Error('App name is required and must be a string');
|
|
117
|
+
}
|
|
118
|
+
return path.join(process.cwd(), 'integration', appName);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Gets the builder folder path for regular applications
|
|
123
|
+
* @param {string} appName - Application name
|
|
124
|
+
* @returns {string} Absolute path to builder directory
|
|
125
|
+
*/
|
|
126
|
+
function getBuilderPath(appName) {
|
|
127
|
+
if (!appName || typeof appName !== 'string') {
|
|
128
|
+
throw new Error('App name is required and must be a string');
|
|
129
|
+
}
|
|
130
|
+
return path.join(process.cwd(), 'builder', appName);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Resolves the deployment JSON file path for an application
|
|
135
|
+
* Uses consistent naming: <app-name>-deploy.json
|
|
136
|
+
* Supports backward compatibility with aifabrix-deploy.json
|
|
137
|
+
*
|
|
138
|
+
* @param {string} appName - Application name
|
|
139
|
+
* @param {string} [appType] - Application type ('external' or other)
|
|
140
|
+
* @param {boolean} [preferNew] - If true, only return new naming (no backward compat)
|
|
141
|
+
* @returns {string} Absolute path to deployment JSON file
|
|
142
|
+
*/
|
|
143
|
+
function getDeployJsonPath(appName, appType, preferNew = false) {
|
|
144
|
+
if (!appName || typeof appName !== 'string') {
|
|
145
|
+
throw new Error('App name is required and must be a string');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const appPath = getAppPath(appName, appType);
|
|
149
|
+
const newPath = path.join(appPath, `${appName}-deploy.json`);
|
|
150
|
+
|
|
151
|
+
// If preferNew is true, always return new naming
|
|
152
|
+
if (preferNew) {
|
|
153
|
+
return newPath;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Check if new naming exists, otherwise fall back to old naming for backward compatibility
|
|
157
|
+
const oldPath = path.join(appPath, 'aifabrix-deploy.json');
|
|
158
|
+
if (fs.existsSync(newPath)) {
|
|
159
|
+
return newPath;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Fall back to old naming for backward compatibility
|
|
163
|
+
if (fs.existsSync(oldPath)) {
|
|
164
|
+
return oldPath;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// If neither exists, return new naming (for generation)
|
|
168
|
+
return newPath;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Detects if an app is external type by checking variables.yaml
|
|
173
|
+
* Checks both integration/ and builder/ folders for backward compatibility
|
|
174
|
+
*
|
|
175
|
+
* @param {string} appName - Application name
|
|
176
|
+
* @returns {Promise<{isExternal: boolean, appPath: string, appType: string}>}
|
|
177
|
+
*/
|
|
178
|
+
async function detectAppType(appName) {
|
|
179
|
+
if (!appName || typeof appName !== 'string') {
|
|
180
|
+
throw new Error('App name is required and must be a string');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Check integration folder first (new structure)
|
|
184
|
+
const integrationPath = getIntegrationPath(appName);
|
|
185
|
+
const integrationVariablesPath = path.join(integrationPath, 'variables.yaml');
|
|
186
|
+
|
|
187
|
+
if (fs.existsSync(integrationVariablesPath)) {
|
|
188
|
+
try {
|
|
189
|
+
const content = fs.readFileSync(integrationVariablesPath, 'utf8');
|
|
190
|
+
const variables = yaml.load(content);
|
|
191
|
+
if (variables.app && variables.app.type === 'external') {
|
|
192
|
+
return {
|
|
193
|
+
isExternal: true,
|
|
194
|
+
appPath: integrationPath,
|
|
195
|
+
appType: 'external',
|
|
196
|
+
baseDir: 'integration'
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
// Ignore errors, continue to check builder folder
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Check builder folder (backward compatibility)
|
|
205
|
+
const builderPath = getBuilderPath(appName);
|
|
206
|
+
const builderVariablesPath = path.join(builderPath, 'variables.yaml');
|
|
207
|
+
|
|
208
|
+
if (fs.existsSync(builderVariablesPath)) {
|
|
209
|
+
try {
|
|
210
|
+
const content = fs.readFileSync(builderVariablesPath, 'utf8');
|
|
211
|
+
const variables = yaml.load(content);
|
|
212
|
+
const isExternal = variables.app && variables.app.type === 'external';
|
|
213
|
+
return {
|
|
214
|
+
isExternal,
|
|
215
|
+
appPath: builderPath,
|
|
216
|
+
appType: isExternal ? 'external' : 'regular',
|
|
217
|
+
baseDir: 'builder'
|
|
218
|
+
};
|
|
219
|
+
} catch {
|
|
220
|
+
// If we can't read it, assume regular app in builder folder
|
|
221
|
+
return {
|
|
222
|
+
isExternal: false,
|
|
223
|
+
appPath: builderPath,
|
|
224
|
+
appType: 'regular',
|
|
225
|
+
baseDir: 'builder'
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Default to builder folder if neither exists
|
|
231
|
+
return {
|
|
232
|
+
isExternal: false,
|
|
233
|
+
appPath: builderPath,
|
|
234
|
+
appType: 'regular',
|
|
235
|
+
baseDir: 'builder'
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
94
239
|
module.exports = {
|
|
95
240
|
getAifabrixHome,
|
|
96
241
|
getApplicationsBaseDir,
|
|
97
|
-
getDevDirectory
|
|
242
|
+
getDevDirectory,
|
|
243
|
+
getAppPath,
|
|
244
|
+
getIntegrationPath,
|
|
245
|
+
getBuilderPath,
|
|
246
|
+
getDeployJsonPath,
|
|
247
|
+
detectAppType
|
|
98
248
|
};
|
|
99
249
|
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
const path = require('path');
|
|
14
14
|
const yaml = require('js-yaml');
|
|
15
|
+
const { detectAppType } = require('./paths');
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Resolves schemaBasePath from application variables.yaml
|
|
@@ -32,7 +33,9 @@ async function resolveSchemaBasePath(appName) {
|
|
|
32
33
|
throw new Error('App name is required and must be a string');
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
// Detect app type and get correct path (integration or builder)
|
|
37
|
+
const { appPath } = await detectAppType(appName);
|
|
38
|
+
const variablesPath = path.join(appPath, 'variables.yaml');
|
|
36
39
|
|
|
37
40
|
if (!fs.existsSync(variablesPath)) {
|
|
38
41
|
throw new Error(`variables.yaml not found: ${variablesPath}`);
|
|
@@ -104,7 +107,9 @@ async function resolveExternalFiles(appName) {
|
|
|
104
107
|
throw new Error('App name is required and must be a string');
|
|
105
108
|
}
|
|
106
109
|
|
|
107
|
-
|
|
110
|
+
// Detect app type and get correct path (integration or builder)
|
|
111
|
+
const { appPath } = await detectAppType(appName);
|
|
112
|
+
const variablesPath = path.join(appPath, 'variables.yaml');
|
|
108
113
|
|
|
109
114
|
if (!fs.existsSync(variablesPath)) {
|
|
110
115
|
throw new Error(`variables.yaml not found: ${variablesPath}`);
|
|
@@ -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
|
+
|
package/lib/validator.js
CHANGED
|
@@ -14,9 +14,12 @@ const path = require('path');
|
|
|
14
14
|
const yaml = require('js-yaml');
|
|
15
15
|
const Ajv = require('ajv');
|
|
16
16
|
const applicationSchema = require('./schema/application-schema.json');
|
|
17
|
+
const externalSystemSchema = require('./schema/external-system.schema.json');
|
|
18
|
+
const externalDataSourceSchema = require('./schema/external-datasource.schema.json');
|
|
17
19
|
const { transformVariablesForValidation } = require('./utils/variable-transformer');
|
|
18
20
|
const { checkEnvironment } = require('./utils/environment-checker');
|
|
19
21
|
const { formatValidationErrors } = require('./utils/error-formatter');
|
|
22
|
+
const { detectAppType } = require('./utils/paths');
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
25
|
* Validates variables.yaml file against application schema
|
|
@@ -37,7 +40,9 @@ async function validateVariables(appName) {
|
|
|
37
40
|
throw new Error('App name is required and must be a string');
|
|
38
41
|
}
|
|
39
42
|
|
|
40
|
-
|
|
43
|
+
// Detect app type and get correct path (integration or builder)
|
|
44
|
+
const { appPath } = await detectAppType(appName);
|
|
45
|
+
const variablesPath = path.join(appPath, 'variables.yaml');
|
|
41
46
|
|
|
42
47
|
if (!fs.existsSync(variablesPath)) {
|
|
43
48
|
throw new Error(`variables.yaml not found: ${variablesPath}`);
|
|
@@ -56,13 +61,45 @@ async function validateVariables(appName) {
|
|
|
56
61
|
const transformed = transformVariablesForValidation(variables, appName);
|
|
57
62
|
|
|
58
63
|
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
64
|
+
// Register external schemas with their $id (GitHub raw URLs)
|
|
65
|
+
// Create copies to avoid modifying the original schemas
|
|
66
|
+
const externalSystemSchemaCopy = { ...externalSystemSchema };
|
|
67
|
+
const externalDataSourceSchemaCopy = { ...externalDataSourceSchema };
|
|
68
|
+
// Remove $schema for draft-2020-12 to avoid AJV issues
|
|
69
|
+
if (externalDataSourceSchemaCopy.$schema && externalDataSourceSchemaCopy.$schema.includes('2020-12')) {
|
|
70
|
+
delete externalDataSourceSchemaCopy.$schema;
|
|
71
|
+
}
|
|
72
|
+
ajv.addSchema(externalSystemSchemaCopy, externalSystemSchema.$id);
|
|
73
|
+
ajv.addSchema(externalDataSourceSchemaCopy, externalDataSourceSchema.$id);
|
|
59
74
|
const validate = ajv.compile(applicationSchema);
|
|
60
75
|
const valid = validate(transformed);
|
|
61
76
|
|
|
77
|
+
// Additional explicit validation for external type
|
|
78
|
+
const errors = valid ? [] : formatValidationErrors(validate.errors);
|
|
79
|
+
const warnings = [];
|
|
80
|
+
|
|
81
|
+
// If type is external, perform additional checks
|
|
82
|
+
if (variables.app && variables.app.type === 'external') {
|
|
83
|
+
// Check for externalIntegration block
|
|
84
|
+
if (!variables.externalIntegration) {
|
|
85
|
+
errors.push('externalIntegration block is required when app.type is "external"');
|
|
86
|
+
} else {
|
|
87
|
+
// Validate externalIntegration structure
|
|
88
|
+
if (!variables.externalIntegration.schemaBasePath) {
|
|
89
|
+
errors.push('externalIntegration.schemaBasePath is required');
|
|
90
|
+
}
|
|
91
|
+
if (!variables.externalIntegration.systems || !Array.isArray(variables.externalIntegration.systems) || variables.externalIntegration.systems.length === 0) {
|
|
92
|
+
errors.push('externalIntegration.systems must be a non-empty array');
|
|
93
|
+
}
|
|
94
|
+
// Note: dataSources can be empty, so we don't validate that here
|
|
95
|
+
// File existence is validated during build/deploy, not during schema validation
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
62
99
|
return {
|
|
63
|
-
valid,
|
|
64
|
-
errors
|
|
65
|
-
warnings
|
|
100
|
+
valid: valid && errors.length === 0,
|
|
101
|
+
errors,
|
|
102
|
+
warnings
|
|
66
103
|
};
|
|
67
104
|
}
|
|
68
105
|
|
|
@@ -220,7 +257,7 @@ async function validateEnvTemplate(appName) {
|
|
|
220
257
|
|
|
221
258
|
/**
|
|
222
259
|
* Validates deployment JSON against application schema
|
|
223
|
-
* Ensures generated
|
|
260
|
+
* Ensures generated <app-name>-deploy.json matches the schema structure
|
|
224
261
|
*
|
|
225
262
|
* @function validateDeploymentJson
|
|
226
263
|
* @param {Object} deployment - Deployment JSON object to validate
|
|
@@ -239,6 +276,16 @@ function validateDeploymentJson(deployment) {
|
|
|
239
276
|
}
|
|
240
277
|
|
|
241
278
|
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
279
|
+
// Register external schemas with their $id (GitHub raw URLs)
|
|
280
|
+
// Create copies to avoid modifying the original schemas
|
|
281
|
+
const externalSystemSchemaCopy = { ...externalSystemSchema };
|
|
282
|
+
const externalDataSourceSchemaCopy = { ...externalDataSourceSchema };
|
|
283
|
+
// Remove $schema for draft-2020-12 to avoid AJV issues
|
|
284
|
+
if (externalDataSourceSchemaCopy.$schema && externalDataSourceSchemaCopy.$schema.includes('2020-12')) {
|
|
285
|
+
delete externalDataSourceSchemaCopy.$schema;
|
|
286
|
+
}
|
|
287
|
+
ajv.addSchema(externalSystemSchemaCopy, externalSystemSchema.$id);
|
|
288
|
+
ajv.addSchema(externalDataSourceSchemaCopy, externalDataSourceSchema.$id);
|
|
242
289
|
const validate = ajv.compile(applicationSchema);
|
|
243
290
|
const valid = validate(deployment);
|
|
244
291
|
|