@aifabrix/builder 2.21.0 → 2.22.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/lib/app-list.js +60 -22
- package/lib/app-register.js +3 -2
- package/lib/app-rotate-secret.js +86 -48
- package/lib/cli.js +28 -28
- package/lib/commands/app.js +3 -0
- package/lib/config.js +40 -173
- package/lib/external-system-generator.js +3 -1
- package/lib/generator-external.js +229 -0
- package/lib/generator-helpers.js +205 -0
- package/lib/generator.js +2 -367
- package/lib/schema/external-system.schema.json +92 -1
- package/lib/utils/api-error-handler.js +9 -2
- package/lib/utils/app-register-api.js +39 -29
- package/lib/utils/app-register-auth.js +103 -39
- package/lib/utils/config-paths.js +112 -0
- package/lib/utils/config-tokens.js +233 -0
- package/lib/utils/device-code.js +28 -6
- package/lib/utils/error-formatters/http-status-errors.js +78 -5
- package/lib/utils/error-formatters/network-errors.js +24 -4
- package/lib/validate.js +67 -7
- package/lib/validator.js +3 -1
- package/package.json +1 -1
- package/templates/external-system/external-system.json.hbs +20 -1
|
@@ -13,33 +13,85 @@ const chalk = require('chalk');
|
|
|
13
13
|
/**
|
|
14
14
|
* Formats authentication error
|
|
15
15
|
* @param {Object} errorData - Error response data
|
|
16
|
+
* @param {string} [errorData.controllerUrl] - Controller URL for example command
|
|
17
|
+
* @param {string[]} [errorData.attemptedUrls] - Array of attempted controller URLs
|
|
18
|
+
* @param {string} [errorData.message] - Error message
|
|
19
|
+
* @param {string} [errorData.error] - Error text
|
|
20
|
+
* @param {string} [errorData.detail] - Error detail
|
|
21
|
+
* @param {string} [errorData.correlationId] - Correlation ID
|
|
16
22
|
* @returns {string} Formatted authentication error message
|
|
17
23
|
*/
|
|
18
24
|
function formatAuthenticationError(errorData) {
|
|
19
25
|
const lines = [];
|
|
20
26
|
lines.push(chalk.red('❌ Authentication Failed\n'));
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
lines.push(chalk.yellow(
|
|
27
|
+
|
|
28
|
+
// Show controller URL prominently if available
|
|
29
|
+
if (errorData.controllerUrl) {
|
|
30
|
+
lines.push(chalk.yellow(`Controller URL: ${errorData.controllerUrl}`));
|
|
31
|
+
lines.push('');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Show attempted URLs if multiple were tried
|
|
35
|
+
if (errorData.attemptedUrls && errorData.attemptedUrls.length > 1) {
|
|
36
|
+
lines.push(chalk.gray('Attempted controller URLs:'));
|
|
37
|
+
errorData.attemptedUrls.forEach(url => {
|
|
38
|
+
lines.push(chalk.gray(` • ${url}`));
|
|
39
|
+
});
|
|
40
|
+
lines.push('');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check if error message contains specific information
|
|
44
|
+
const errorMessage = errorData.message || errorData.error || errorData.detail || '';
|
|
45
|
+
const lowerMessage = errorMessage.toLowerCase();
|
|
46
|
+
|
|
47
|
+
// Only show error message if it provides useful information beyond generic messages
|
|
48
|
+
const isGenericMessage = lowerMessage.includes('authentication required') ||
|
|
49
|
+
lowerMessage.includes('unauthorized') ||
|
|
50
|
+
lowerMessage === '';
|
|
51
|
+
|
|
52
|
+
if (errorMessage && !isGenericMessage) {
|
|
53
|
+
// Show specific error message if it provides useful details
|
|
54
|
+
lines.push(chalk.yellow(errorMessage));
|
|
55
|
+
lines.push('');
|
|
25
56
|
}
|
|
57
|
+
|
|
58
|
+
// Always show general, actionable guidance
|
|
59
|
+
lines.push(chalk.gray('Your authentication token is invalid or has expired.'));
|
|
26
60
|
lines.push('');
|
|
27
|
-
lines.push(chalk.gray('
|
|
61
|
+
lines.push(chalk.gray('To authenticate, run:'));
|
|
62
|
+
|
|
63
|
+
// Use real controller URL if provided, otherwise show placeholder
|
|
64
|
+
const controllerUrl = errorData.controllerUrl;
|
|
65
|
+
if (controllerUrl && controllerUrl.trim()) {
|
|
66
|
+
lines.push(chalk.gray(` aifabrix login --method device --controller ${controllerUrl}`));
|
|
67
|
+
} else {
|
|
68
|
+
lines.push(chalk.gray(' aifabrix login --method device --controller <url>'));
|
|
69
|
+
}
|
|
70
|
+
|
|
28
71
|
if (errorData.correlationId) {
|
|
72
|
+
lines.push('');
|
|
29
73
|
lines.push(chalk.gray(`Correlation ID: ${errorData.correlationId}`));
|
|
30
74
|
}
|
|
75
|
+
|
|
31
76
|
return lines.join('\n');
|
|
32
77
|
}
|
|
33
78
|
|
|
34
79
|
/**
|
|
35
80
|
* Formats server error (500+)
|
|
36
81
|
* @param {Object} errorData - Error response data
|
|
82
|
+
* @param {string} [errorData.controllerUrl] - Controller URL
|
|
37
83
|
* @returns {string} Formatted server error message
|
|
38
84
|
*/
|
|
39
85
|
function formatServerError(errorData) {
|
|
40
86
|
const lines = [];
|
|
41
87
|
lines.push(chalk.red('❌ Server Error\n'));
|
|
42
88
|
|
|
89
|
+
// Show controller URL if available
|
|
90
|
+
if (errorData.controllerUrl) {
|
|
91
|
+
lines.push(chalk.yellow(`Controller URL: ${errorData.controllerUrl}`));
|
|
92
|
+
lines.push('');
|
|
93
|
+
}
|
|
94
|
+
|
|
43
95
|
// Check for RFC 7807 Problem Details format (detail field)
|
|
44
96
|
if (errorData.detail) {
|
|
45
97
|
lines.push(chalk.yellow(errorData.detail));
|
|
@@ -62,12 +114,19 @@ function formatServerError(errorData) {
|
|
|
62
114
|
/**
|
|
63
115
|
* Formats conflict error (409)
|
|
64
116
|
* @param {Object} errorData - Error response data
|
|
117
|
+
* @param {string} [errorData.controllerUrl] - Controller URL
|
|
65
118
|
* @returns {string} Formatted conflict error message
|
|
66
119
|
*/
|
|
67
120
|
function formatConflictError(errorData) {
|
|
68
121
|
const lines = [];
|
|
69
122
|
lines.push(chalk.red('❌ Conflict\n'));
|
|
70
123
|
|
|
124
|
+
// Show controller URL if available
|
|
125
|
+
if (errorData.controllerUrl) {
|
|
126
|
+
lines.push(chalk.yellow(`Controller URL: ${errorData.controllerUrl}`));
|
|
127
|
+
lines.push('');
|
|
128
|
+
}
|
|
129
|
+
|
|
71
130
|
// Check if it's an application already exists error
|
|
72
131
|
const detail = errorData.detail || errorData.message || '';
|
|
73
132
|
if (detail.toLowerCase().includes('application already exists')) {
|
|
@@ -124,12 +183,19 @@ function getNotFoundGuidance(detail) {
|
|
|
124
183
|
/**
|
|
125
184
|
* Formats not found error (404)
|
|
126
185
|
* @param {Object} errorData - Error response data
|
|
186
|
+
* @param {string} [errorData.controllerUrl] - Controller URL
|
|
127
187
|
* @returns {string} Formatted not found error message
|
|
128
188
|
*/
|
|
129
189
|
function formatNotFoundError(errorData) {
|
|
130
190
|
const lines = [];
|
|
131
191
|
lines.push(chalk.red('❌ Not Found\n'));
|
|
132
192
|
|
|
193
|
+
// Show controller URL if available
|
|
194
|
+
if (errorData.controllerUrl) {
|
|
195
|
+
lines.push(chalk.yellow(`Controller URL: ${errorData.controllerUrl}`));
|
|
196
|
+
lines.push('');
|
|
197
|
+
}
|
|
198
|
+
|
|
133
199
|
const detail = errorData.detail || errorData.message || '';
|
|
134
200
|
if (detail) {
|
|
135
201
|
lines.push(chalk.yellow(detail));
|
|
@@ -154,12 +220,19 @@ function formatNotFoundError(errorData) {
|
|
|
154
220
|
* Formats generic error
|
|
155
221
|
* @param {Object} errorData - Error response data
|
|
156
222
|
* @param {number} statusCode - HTTP status code
|
|
223
|
+
* @param {string} [errorData.controllerUrl] - Controller URL
|
|
157
224
|
* @returns {string} Formatted error message
|
|
158
225
|
*/
|
|
159
226
|
function formatGenericError(errorData, statusCode) {
|
|
160
227
|
const lines = [];
|
|
161
228
|
lines.push(chalk.red(`❌ Error (HTTP ${statusCode})\n`));
|
|
162
229
|
|
|
230
|
+
// Show controller URL if available
|
|
231
|
+
if (errorData.controllerUrl) {
|
|
232
|
+
lines.push(chalk.yellow(`Controller URL: ${errorData.controllerUrl}`));
|
|
233
|
+
lines.push('');
|
|
234
|
+
}
|
|
235
|
+
|
|
163
236
|
// Check for RFC 7807 Problem Details format (detail field)
|
|
164
237
|
if (errorData.detail) {
|
|
165
238
|
lines.push(chalk.yellow(errorData.detail));
|
|
@@ -14,26 +14,46 @@ const chalk = require('chalk');
|
|
|
14
14
|
* Formats network error
|
|
15
15
|
* @param {string} errorMessage - Error message
|
|
16
16
|
* @param {Object} errorData - Error response data (optional)
|
|
17
|
+
* @param {string} [errorData.controllerUrl] - Controller URL
|
|
17
18
|
* @returns {string} Formatted network error message
|
|
18
19
|
*/
|
|
19
20
|
function formatNetworkError(errorMessage, errorData) {
|
|
20
21
|
const lines = [];
|
|
21
22
|
lines.push(chalk.red('❌ Network Error\n'));
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
// Show controller URL prominently if available
|
|
25
|
+
if (errorData && errorData.controllerUrl) {
|
|
26
|
+
lines.push(chalk.yellow(`Controller URL: ${errorData.controllerUrl}`));
|
|
27
|
+
lines.push('');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Ensure errorMessage is a string
|
|
31
|
+
const message = typeof errorMessage === 'string' ? errorMessage : String(errorMessage || 'Network error');
|
|
32
|
+
|
|
33
|
+
if (message.includes('ECONNREFUSED') || message.includes('Cannot connect')) {
|
|
24
34
|
lines.push(chalk.yellow('Cannot connect to controller.'));
|
|
35
|
+
if (errorData && errorData.controllerUrl) {
|
|
36
|
+
lines.push(chalk.gray(`Controller URL: ${errorData.controllerUrl}`));
|
|
37
|
+
}
|
|
25
38
|
lines.push(chalk.gray('Check if the controller is running.'));
|
|
26
|
-
} else if (
|
|
39
|
+
} else if (message.includes('ENOTFOUND') || message.includes('hostname not found')) {
|
|
27
40
|
lines.push(chalk.yellow('Controller hostname not found.'));
|
|
41
|
+
if (errorData && errorData.controllerUrl) {
|
|
42
|
+
lines.push(chalk.gray(`Controller URL: ${errorData.controllerUrl}`));
|
|
43
|
+
}
|
|
28
44
|
lines.push(chalk.gray('Check your controller URL.'));
|
|
29
|
-
} else if (
|
|
45
|
+
} else if (message.includes('timeout') || message.includes('timed out')) {
|
|
30
46
|
lines.push(chalk.yellow('Request timed out.'));
|
|
47
|
+
if (errorData && errorData.controllerUrl) {
|
|
48
|
+
lines.push(chalk.gray(`Controller URL: ${errorData.controllerUrl}`));
|
|
49
|
+
}
|
|
31
50
|
lines.push(chalk.gray('The controller may be overloaded.'));
|
|
32
51
|
} else {
|
|
33
|
-
lines.push(chalk.yellow(
|
|
52
|
+
lines.push(chalk.yellow(message));
|
|
34
53
|
}
|
|
35
54
|
|
|
36
55
|
if (errorData && errorData.correlationId) {
|
|
56
|
+
lines.push('');
|
|
37
57
|
lines.push(chalk.gray(`Correlation ID: ${errorData.correlationId}`));
|
|
38
58
|
}
|
|
39
59
|
|
package/lib/validate.js
CHANGED
|
@@ -16,6 +16,7 @@ const validator = require('./validator');
|
|
|
16
16
|
const { resolveExternalFiles } = require('./utils/schema-resolver');
|
|
17
17
|
const { loadExternalSystemSchema, loadExternalDataSourceSchema, detectSchemaType } = require('./utils/schema-loader');
|
|
18
18
|
const { formatValidationErrors } = require('./utils/error-formatter');
|
|
19
|
+
const { detectAppType } = require('./utils/paths');
|
|
19
20
|
const logger = require('./utils/logger');
|
|
20
21
|
|
|
21
22
|
/**
|
|
@@ -56,10 +57,29 @@ async function validateExternalFile(filePath, type) {
|
|
|
56
57
|
|
|
57
58
|
const valid = validate(parsed);
|
|
58
59
|
|
|
60
|
+
const errors = valid ? [] : formatValidationErrors(validate.errors);
|
|
61
|
+
const warnings = [];
|
|
62
|
+
|
|
63
|
+
// Additional validation for external system files: check role references in permissions
|
|
64
|
+
if (type === 'system' && parsed.permissions && Array.isArray(parsed.permissions)) {
|
|
65
|
+
const roles = parsed.roles || [];
|
|
66
|
+
const roleValues = new Set(roles.map(r => r.value));
|
|
67
|
+
|
|
68
|
+
parsed.permissions.forEach((permission, index) => {
|
|
69
|
+
if (permission.roles && Array.isArray(permission.roles)) {
|
|
70
|
+
permission.roles.forEach(roleValue => {
|
|
71
|
+
if (!roleValues.has(roleValue)) {
|
|
72
|
+
errors.push(`Permission "${permission.name}" (index ${index}) references role "${roleValue}" which does not exist in roles array`);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
59
79
|
return {
|
|
60
|
-
valid,
|
|
61
|
-
errors
|
|
62
|
-
warnings
|
|
80
|
+
valid: errors.length === 0,
|
|
81
|
+
errors,
|
|
82
|
+
warnings
|
|
63
83
|
};
|
|
64
84
|
}
|
|
65
85
|
|
|
@@ -130,11 +150,28 @@ async function validateAppOrFile(appOrFile) {
|
|
|
130
150
|
// Treat as app name
|
|
131
151
|
const appName = appOrFile;
|
|
132
152
|
|
|
153
|
+
// Detect app type to support both builder/ and integration/ directories
|
|
154
|
+
const { appPath, isExternal } = await detectAppType(appName);
|
|
155
|
+
|
|
133
156
|
// Validate application
|
|
134
157
|
const appValidation = await validator.validateApplication(appName);
|
|
135
158
|
|
|
159
|
+
// Validate rbac.yaml for external systems
|
|
160
|
+
let rbacValidation = null;
|
|
161
|
+
if (isExternal) {
|
|
162
|
+
try {
|
|
163
|
+
rbacValidation = await validator.validateRbac(appName);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
rbacValidation = {
|
|
166
|
+
valid: false,
|
|
167
|
+
errors: [error.message],
|
|
168
|
+
warnings: []
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
136
173
|
// Check for externalIntegration block
|
|
137
|
-
const variablesPath = path.join(
|
|
174
|
+
const variablesPath = path.join(appPath, 'variables.yaml');
|
|
138
175
|
if (!fs.existsSync(variablesPath)) {
|
|
139
176
|
return {
|
|
140
177
|
valid: appValidation.valid,
|
|
@@ -193,20 +230,25 @@ async function validateAppOrFile(appOrFile) {
|
|
|
193
230
|
}
|
|
194
231
|
|
|
195
232
|
// Aggregate results
|
|
196
|
-
const
|
|
233
|
+
const rbacErrors = rbacValidation ? rbacValidation.errors : [];
|
|
234
|
+
const rbacWarnings = rbacValidation ? rbacValidation.warnings : [];
|
|
235
|
+
const allValid = appValidation.valid && externalValidations.every(v => v.valid) && (!rbacValidation || rbacValidation.valid);
|
|
197
236
|
const allErrors = [
|
|
198
237
|
...appValidation.errors,
|
|
199
|
-
...externalValidations.flatMap(v => v.errors.map(e => `${v.file}: ${e}`))
|
|
238
|
+
...externalValidations.flatMap(v => v.errors.map(e => `${v.file}: ${e}`)),
|
|
239
|
+
...rbacErrors.map(e => `rbac.yaml: ${e}`)
|
|
200
240
|
];
|
|
201
241
|
const allWarnings = [
|
|
202
242
|
...appValidation.warnings,
|
|
203
|
-
...externalValidations.flatMap(v => v.warnings)
|
|
243
|
+
...externalValidations.flatMap(v => v.warnings),
|
|
244
|
+
...rbacWarnings
|
|
204
245
|
];
|
|
205
246
|
|
|
206
247
|
return {
|
|
207
248
|
valid: allValid,
|
|
208
249
|
application: appValidation,
|
|
209
250
|
externalFiles: externalValidations,
|
|
251
|
+
rbac: rbacValidation,
|
|
210
252
|
errors: allErrors,
|
|
211
253
|
warnings: allWarnings
|
|
212
254
|
};
|
|
@@ -263,6 +305,24 @@ function displayValidationResults(result) {
|
|
|
263
305
|
});
|
|
264
306
|
}
|
|
265
307
|
|
|
308
|
+
// Display rbac validation (for external systems)
|
|
309
|
+
if (result.rbac) {
|
|
310
|
+
logger.log(chalk.blue('\nRBAC Configuration:'));
|
|
311
|
+
if (result.rbac.valid) {
|
|
312
|
+
logger.log(chalk.green(' ✓ RBAC configuration is valid'));
|
|
313
|
+
} else {
|
|
314
|
+
logger.log(chalk.red(' ✗ RBAC configuration has errors:'));
|
|
315
|
+
result.rbac.errors.forEach(error => {
|
|
316
|
+
logger.log(chalk.red(` • ${error}`));
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
if (result.rbac.warnings && result.rbac.warnings.length > 0) {
|
|
320
|
+
result.rbac.warnings.forEach(warning => {
|
|
321
|
+
logger.log(chalk.yellow(` ⚠ ${warning}`));
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
266
326
|
// Display file validation (for direct file validation)
|
|
267
327
|
if (result.file) {
|
|
268
328
|
logger.log(chalk.blue(`\nFile: ${result.file}`));
|
package/lib/validator.js
CHANGED
|
@@ -162,7 +162,9 @@ async function validateRbac(appName) {
|
|
|
162
162
|
throw new Error('App name is required and must be a string');
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
// Support both builder/ and integration/ directories using detectAppType
|
|
166
|
+
const { appPath } = await detectAppType(appName);
|
|
167
|
+
const rbacPath = path.join(appPath, 'rbac.yaml');
|
|
166
168
|
|
|
167
169
|
if (!fs.existsSync(rbacPath)) {
|
|
168
170
|
return { valid: true, errors: [], warnings: ['rbac.yaml not found - authentication disabled'] };
|
package/package.json
CHANGED
|
@@ -32,6 +32,25 @@
|
|
|
32
32
|
"serverUrl": "https://mcp.example.com",
|
|
33
33
|
"toolPrefix": "{{systemKey}}"
|
|
34
34
|
}{{/if}},
|
|
35
|
-
"tags": []
|
|
35
|
+
"tags": []{{#if roles}},
|
|
36
|
+
"roles": [
|
|
37
|
+
{{#each roles}}
|
|
38
|
+
{
|
|
39
|
+
"name": "{{name}}",
|
|
40
|
+
"value": "{{value}}",
|
|
41
|
+
"description": "{{description}}"{{#if Groups}},
|
|
42
|
+
"Groups": [{{#each Groups}}"{{this}}"{{#unless @last}}, {{/unless}}{{/each}}]{{/if}}
|
|
43
|
+
}{{#unless @last}},{{/unless}}
|
|
44
|
+
{{/each}}
|
|
45
|
+
]{{/if}}{{#if permissions}},
|
|
46
|
+
"permissions": [
|
|
47
|
+
{{#each permissions}}
|
|
48
|
+
{
|
|
49
|
+
"name": "{{name}}",
|
|
50
|
+
"roles": [{{#each roles}}"{{this}}"{{#unless @last}}, {{/unless}}{{/each}}],
|
|
51
|
+
"description": "{{description}}"
|
|
52
|
+
}{{#unless @last}},{{/unless}}
|
|
53
|
+
{{/each}}
|
|
54
|
+
]{{/if}}
|
|
36
55
|
}
|
|
37
56
|
|