@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
package/lib/generator.js
CHANGED
|
@@ -11,116 +11,13 @@
|
|
|
11
11
|
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
const path = require('path');
|
|
14
|
-
const yaml = require('js-yaml');
|
|
15
|
-
const Ajv = require('ajv');
|
|
16
|
-
const _secrets = require('./secrets');
|
|
17
14
|
const _keyGenerator = require('./key-generator');
|
|
18
15
|
const _validator = require('./validator');
|
|
19
16
|
const builders = require('./generator-builders');
|
|
20
17
|
const { detectAppType, getDeployJsonPath } = require('./utils/paths');
|
|
21
18
|
const splitFunctions = require('./generator-split');
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
* Loads variables.yaml file
|
|
25
|
-
* @param {string} variablesPath - Path to variables.yaml
|
|
26
|
-
* @returns {Object} Parsed variables
|
|
27
|
-
* @throws {Error} If file not found or invalid YAML
|
|
28
|
-
*/
|
|
29
|
-
function loadVariables(variablesPath) {
|
|
30
|
-
if (!fs.existsSync(variablesPath)) {
|
|
31
|
-
throw new Error(`variables.yaml not found: ${variablesPath}`);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const variablesContent = fs.readFileSync(variablesPath, 'utf8');
|
|
35
|
-
try {
|
|
36
|
-
return { content: variablesContent, parsed: yaml.load(variablesContent) };
|
|
37
|
-
} catch (error) {
|
|
38
|
-
throw new Error(`Invalid YAML syntax in variables.yaml: ${error.message}`);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Loads env.template file
|
|
44
|
-
* @param {string} templatePath - Path to env.template
|
|
45
|
-
* @returns {string} Template content
|
|
46
|
-
* @throws {Error} If file not found
|
|
47
|
-
*/
|
|
48
|
-
function loadEnvTemplate(templatePath) {
|
|
49
|
-
if (!fs.existsSync(templatePath)) {
|
|
50
|
-
throw new Error(`env.template not found: ${templatePath}`);
|
|
51
|
-
}
|
|
52
|
-
return fs.readFileSync(templatePath, 'utf8');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Loads rbac.yaml file if it exists
|
|
57
|
-
* @param {string} rbacPath - Path to rbac.yaml
|
|
58
|
-
* @returns {Object|null} Parsed RBAC configuration or null
|
|
59
|
-
* @throws {Error} If file exists but has invalid YAML
|
|
60
|
-
*/
|
|
61
|
-
function loadRbac(rbacPath) {
|
|
62
|
-
if (!fs.existsSync(rbacPath)) {
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const rbacContent = fs.readFileSync(rbacPath, 'utf8');
|
|
67
|
-
try {
|
|
68
|
-
return yaml.load(rbacContent);
|
|
69
|
-
} catch (error) {
|
|
70
|
-
throw new Error(`Invalid YAML syntax in rbac.yaml: ${error.message}`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Generates external system <app-name>-deploy.json by loading the system JSON file
|
|
76
|
-
* For external systems, the system JSON file is already created and we just need to reference it
|
|
77
|
-
* @async
|
|
78
|
-
* @function generateExternalSystemDeployJson
|
|
79
|
-
* @param {string} appName - Name of the application
|
|
80
|
-
* @param {string} appPath - Path to application directory (integration or builder)
|
|
81
|
-
* @returns {Promise<string>} Path to generated <app-name>-deploy.json file
|
|
82
|
-
* @throws {Error} If generation fails
|
|
83
|
-
*/
|
|
84
|
-
async function generateExternalSystemDeployJson(appName, appPath) {
|
|
85
|
-
if (!appName || typeof appName !== 'string') {
|
|
86
|
-
throw new Error('App name is required and must be a string');
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const variablesPath = path.join(appPath, 'variables.yaml');
|
|
90
|
-
const { parsed: variables } = loadVariables(variablesPath);
|
|
91
|
-
|
|
92
|
-
if (!variables.externalIntegration) {
|
|
93
|
-
throw new Error('externalIntegration block not found in variables.yaml');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// For external systems, the system JSON file should be in the same folder
|
|
97
|
-
// Check if it already exists (should be <app-name>-deploy.json)
|
|
98
|
-
const deployJsonPath = getDeployJsonPath(appName, 'external', true);
|
|
99
|
-
const systemFileName = variables.externalIntegration.systems && variables.externalIntegration.systems.length > 0
|
|
100
|
-
? variables.externalIntegration.systems[0]
|
|
101
|
-
: `${appName}-deploy.json`;
|
|
102
|
-
|
|
103
|
-
// Resolve system file path (schemaBasePath is usually './' for same folder)
|
|
104
|
-
const schemaBasePath = variables.externalIntegration.schemaBasePath || './';
|
|
105
|
-
const systemFilePath = path.isAbsolute(schemaBasePath)
|
|
106
|
-
? path.join(schemaBasePath, systemFileName)
|
|
107
|
-
: path.join(appPath, schemaBasePath, systemFileName);
|
|
108
|
-
|
|
109
|
-
// If system file doesn't exist, throw error (it should be created manually or via external-system-generator)
|
|
110
|
-
if (!fs.existsSync(systemFilePath)) {
|
|
111
|
-
throw new Error(`External system file not found: ${systemFilePath}. Please create it first.`);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Read the system JSON file
|
|
115
|
-
const systemContent = await fs.promises.readFile(systemFilePath, 'utf8');
|
|
116
|
-
const systemJson = JSON.parse(systemContent);
|
|
117
|
-
|
|
118
|
-
// Write it as <app-name>-deploy.json (consistent naming)
|
|
119
|
-
const jsonContent = JSON.stringify(systemJson, null, 2);
|
|
120
|
-
await fs.promises.writeFile(deployJsonPath, jsonContent, { mode: 0o644, encoding: 'utf8' });
|
|
121
|
-
|
|
122
|
-
return deployJsonPath;
|
|
123
|
-
}
|
|
19
|
+
const { loadVariables, loadEnvTemplate, loadRbac, parseEnvironmentVariables } = require('./generator-helpers');
|
|
20
|
+
const { generateExternalSystemDeployJson, generateExternalSystemApplicationSchema } = require('./generator-external');
|
|
124
21
|
|
|
125
22
|
/**
|
|
126
23
|
* Generates deployment JSON from application configuration files
|
|
@@ -188,139 +85,6 @@ async function generateDeployJson(appName) {
|
|
|
188
85
|
return jsonPath;
|
|
189
86
|
}
|
|
190
87
|
|
|
191
|
-
/**
|
|
192
|
-
* Validates portalInput structure against schema requirements
|
|
193
|
-
* @param {Object} portalInput - Portal input configuration to validate
|
|
194
|
-
* @param {string} variableName - Variable name for error messages
|
|
195
|
-
* @throws {Error} If portalInput structure is invalid
|
|
196
|
-
*/
|
|
197
|
-
function validatePortalInput(portalInput, variableName) {
|
|
198
|
-
if (!portalInput || typeof portalInput !== 'object') {
|
|
199
|
-
throw new Error(`Invalid portalInput for variable '${variableName}': must be an object`);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Check required fields
|
|
203
|
-
if (!portalInput.field || typeof portalInput.field !== 'string') {
|
|
204
|
-
throw new Error(`Invalid portalInput for variable '${variableName}': field is required and must be a string`);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (!portalInput.label || typeof portalInput.label !== 'string') {
|
|
208
|
-
throw new Error(`Invalid portalInput for variable '${variableName}': label is required and must be a string`);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Validate field type
|
|
212
|
-
const validFieldTypes = ['password', 'text', 'textarea', 'select'];
|
|
213
|
-
if (!validFieldTypes.includes(portalInput.field)) {
|
|
214
|
-
throw new Error(`Invalid portalInput for variable '${variableName}': field must be one of: ${validFieldTypes.join(', ')}`);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Validate select field requires options
|
|
218
|
-
if (portalInput.field === 'select') {
|
|
219
|
-
if (!portalInput.options || !Array.isArray(portalInput.options) || portalInput.options.length === 0) {
|
|
220
|
-
throw new Error(`Invalid portalInput for variable '${variableName}': select field requires a non-empty options array`);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Validate optional fields
|
|
225
|
-
if (portalInput.placeholder !== undefined && typeof portalInput.placeholder !== 'string') {
|
|
226
|
-
throw new Error(`Invalid portalInput for variable '${variableName}': placeholder must be a string`);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (portalInput.masked !== undefined && typeof portalInput.masked !== 'boolean') {
|
|
230
|
-
throw new Error(`Invalid portalInput for variable '${variableName}': masked must be a boolean`);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (portalInput.validation !== undefined) {
|
|
234
|
-
if (typeof portalInput.validation !== 'object' || Array.isArray(portalInput.validation)) {
|
|
235
|
-
throw new Error(`Invalid portalInput for variable '${variableName}': validation must be an object`);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (portalInput.options !== undefined && portalInput.field !== 'select') {
|
|
240
|
-
// Options should only be present for select fields
|
|
241
|
-
if (Array.isArray(portalInput.options) && portalInput.options.length > 0) {
|
|
242
|
-
throw new Error(`Invalid portalInput for variable '${variableName}': options can only be used with select field type`);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Parses environment variables from env.template and merges portalInput from variables.yaml
|
|
249
|
-
* @param {string} envTemplate - Content of env.template file
|
|
250
|
-
* @param {Object|null} [variablesConfig=null] - Optional configuration from variables.yaml
|
|
251
|
-
* @returns {Array<Object>} Configuration array with merged portalInput
|
|
252
|
-
* @throws {Error} If portalInput structure is invalid
|
|
253
|
-
*/
|
|
254
|
-
function parseEnvironmentVariables(envTemplate, variablesConfig = null) {
|
|
255
|
-
const configuration = [];
|
|
256
|
-
const lines = envTemplate.split('\n');
|
|
257
|
-
|
|
258
|
-
// Create a map of portalInput configurations by variable name
|
|
259
|
-
const portalInputMap = new Map();
|
|
260
|
-
if (variablesConfig && variablesConfig.configuration && Array.isArray(variablesConfig.configuration)) {
|
|
261
|
-
for (const configItem of variablesConfig.configuration) {
|
|
262
|
-
if (configItem.name && configItem.portalInput) {
|
|
263
|
-
// Validate portalInput before adding to map
|
|
264
|
-
validatePortalInput(configItem.portalInput, configItem.name);
|
|
265
|
-
portalInputMap.set(configItem.name, configItem.portalInput);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
for (const line of lines) {
|
|
271
|
-
const trimmed = line.trim();
|
|
272
|
-
|
|
273
|
-
// Skip empty lines and comments
|
|
274
|
-
if (!trimmed || trimmed.startsWith('#')) {
|
|
275
|
-
continue;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Parse KEY=VALUE format
|
|
279
|
-
const equalIndex = trimmed.indexOf('=');
|
|
280
|
-
if (equalIndex === -1) {
|
|
281
|
-
continue;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const key = trimmed.substring(0, equalIndex).trim();
|
|
285
|
-
const value = trimmed.substring(equalIndex + 1).trim();
|
|
286
|
-
|
|
287
|
-
if (!key || !value) {
|
|
288
|
-
continue;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Determine location and required status
|
|
292
|
-
let location = 'variable';
|
|
293
|
-
let required = false;
|
|
294
|
-
|
|
295
|
-
if (value.startsWith('kv://')) {
|
|
296
|
-
location = 'keyvault';
|
|
297
|
-
required = true;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Check if it's a sensitive variable
|
|
301
|
-
const sensitiveKeys = ['password', 'secret', 'key', 'token', 'auth'];
|
|
302
|
-
if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive))) {
|
|
303
|
-
required = true;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const configItem = {
|
|
307
|
-
name: key,
|
|
308
|
-
value: value.replace('kv://', ''), // Remove kv:// prefix for KeyVault
|
|
309
|
-
location,
|
|
310
|
-
required
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
// Merge portalInput if it exists in variables.yaml
|
|
314
|
-
if (portalInputMap.has(key)) {
|
|
315
|
-
configItem.portalInput = portalInputMap.get(key);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
configuration.push(configItem);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
return configuration;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
88
|
async function generateDeployJsonWithValidation(appName) {
|
|
325
89
|
const jsonPath = await generateDeployJson(appName);
|
|
326
90
|
const jsonContent = fs.readFileSync(jsonPath, 'utf8');
|
|
@@ -348,135 +112,6 @@ async function generateDeployJsonWithValidation(appName) {
|
|
|
348
112
|
};
|
|
349
113
|
}
|
|
350
114
|
|
|
351
|
-
/**
|
|
352
|
-
* Generates application-schema.json structure for external systems
|
|
353
|
-
* Combines system and datasource JSONs into application-level deployment format
|
|
354
|
-
* @async
|
|
355
|
-
* @function generateExternalSystemApplicationSchema
|
|
356
|
-
* @param {string} appName - Application name
|
|
357
|
-
* @returns {Promise<Object>} Application schema object
|
|
358
|
-
* @throws {Error} If generation fails
|
|
359
|
-
*/
|
|
360
|
-
async function generateExternalSystemApplicationSchema(appName) {
|
|
361
|
-
if (!appName || typeof appName !== 'string') {
|
|
362
|
-
throw new Error('App name is required and must be a string');
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const { appPath } = await detectAppType(appName);
|
|
366
|
-
const variablesPath = path.join(appPath, 'variables.yaml');
|
|
367
|
-
|
|
368
|
-
// Load variables.yaml
|
|
369
|
-
const { parsed: variables } = loadVariables(variablesPath);
|
|
370
|
-
|
|
371
|
-
if (!variables.externalIntegration) {
|
|
372
|
-
throw new Error('externalIntegration block not found in variables.yaml');
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Load system file
|
|
376
|
-
const schemaBasePath = variables.externalIntegration.schemaBasePath || './';
|
|
377
|
-
const systemFiles = variables.externalIntegration.systems || [];
|
|
378
|
-
|
|
379
|
-
if (systemFiles.length === 0) {
|
|
380
|
-
throw new Error('No system files specified in externalIntegration.systems');
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const systemFileName = systemFiles[0];
|
|
384
|
-
const systemFilePath = path.isAbsolute(schemaBasePath)
|
|
385
|
-
? path.join(schemaBasePath, systemFileName)
|
|
386
|
-
: path.join(appPath, schemaBasePath, systemFileName);
|
|
387
|
-
|
|
388
|
-
if (!fs.existsSync(systemFilePath)) {
|
|
389
|
-
throw new Error(`System file not found: ${systemFilePath}`);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
const systemContent = await fs.promises.readFile(systemFilePath, 'utf8');
|
|
393
|
-
const systemJson = JSON.parse(systemContent);
|
|
394
|
-
|
|
395
|
-
// Load datasource files
|
|
396
|
-
const datasourceFiles = variables.externalIntegration.dataSources || [];
|
|
397
|
-
const datasourceJsons = [];
|
|
398
|
-
|
|
399
|
-
for (const datasourceFile of datasourceFiles) {
|
|
400
|
-
const datasourcePath = path.isAbsolute(schemaBasePath)
|
|
401
|
-
? path.join(schemaBasePath, datasourceFile)
|
|
402
|
-
: path.join(appPath, schemaBasePath, datasourceFile);
|
|
403
|
-
|
|
404
|
-
if (!fs.existsSync(datasourcePath)) {
|
|
405
|
-
throw new Error(`Datasource file not found: ${datasourcePath}`);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
const datasourceContent = await fs.promises.readFile(datasourcePath, 'utf8');
|
|
409
|
-
const datasourceJson = JSON.parse(datasourceContent);
|
|
410
|
-
datasourceJsons.push(datasourceJson);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Build application-schema.json structure
|
|
414
|
-
const applicationSchema = {
|
|
415
|
-
version: variables.externalIntegration.version || '1.0.0',
|
|
416
|
-
application: systemJson,
|
|
417
|
-
dataSources: datasourceJsons
|
|
418
|
-
};
|
|
419
|
-
|
|
420
|
-
// Validate individual components against their schemas
|
|
421
|
-
const externalSystemSchema = require('./schema/external-system.schema.json');
|
|
422
|
-
const externalDatasourceSchema = require('./schema/external-datasource.schema.json');
|
|
423
|
-
|
|
424
|
-
// For draft-2020-12 schemas, remove $schema to avoid AJV issues (similar to schema-loader.js)
|
|
425
|
-
const datasourceSchemaToAdd = { ...externalDatasourceSchema };
|
|
426
|
-
if (datasourceSchemaToAdd.$schema && datasourceSchemaToAdd.$schema.includes('2020-12')) {
|
|
427
|
-
delete datasourceSchemaToAdd.$schema;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const ajv = new Ajv({ allErrors: true, strict: false, removeAdditional: false });
|
|
431
|
-
|
|
432
|
-
// Validate application (system) against external-system schema
|
|
433
|
-
const externalSystemSchemaId = externalSystemSchema.$id || 'https://raw.githubusercontent.com/esystemsdev/aifabrix-builder/refs/heads/main/lib/schema/external-system.schema.json';
|
|
434
|
-
ajv.addSchema(externalSystemSchema, externalSystemSchemaId);
|
|
435
|
-
const validateSystem = ajv.compile(externalSystemSchema);
|
|
436
|
-
const systemValid = validateSystem(systemJson);
|
|
437
|
-
|
|
438
|
-
if (!systemValid) {
|
|
439
|
-
// Filter out additionalProperties errors for required properties that aren't defined in schema
|
|
440
|
-
// This handles schema inconsistencies where authentication is required but not defined in properties
|
|
441
|
-
const filteredErrors = validateSystem.errors.filter(err => {
|
|
442
|
-
if (err.keyword === 'additionalProperties' && err.params?.additionalProperty === 'authentication') {
|
|
443
|
-
// Check if authentication is in required array
|
|
444
|
-
const required = externalSystemSchema.required || [];
|
|
445
|
-
if (required.includes('authentication')) {
|
|
446
|
-
return false; // Ignore this error since authentication is required but not defined
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
return true;
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
if (filteredErrors.length > 0) {
|
|
453
|
-
const errors = filteredErrors.map(err => {
|
|
454
|
-
const path = err.instancePath || err.schemaPath;
|
|
455
|
-
return `${path} ${err.message}`;
|
|
456
|
-
}).join(', ');
|
|
457
|
-
throw new Error(`System JSON does not match external-system schema: ${errors}`);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// Validate each datasource against external-datasource schema
|
|
462
|
-
const externalDatasourceSchemaId = datasourceSchemaToAdd.$id || 'https://raw.githubusercontent.com/esystemsdev/aifabrix-builder/refs/heads/main/lib/schema/external-datasource.schema.json';
|
|
463
|
-
ajv.addSchema(datasourceSchemaToAdd, externalDatasourceSchemaId);
|
|
464
|
-
const validateDatasource = ajv.compile(datasourceSchemaToAdd);
|
|
465
|
-
|
|
466
|
-
for (let i = 0; i < datasourceJsons.length; i++) {
|
|
467
|
-
const datasourceValid = validateDatasource(datasourceJsons[i]);
|
|
468
|
-
if (!datasourceValid) {
|
|
469
|
-
const errors = validateDatasource.errors.map(err => {
|
|
470
|
-
const path = err.instancePath || err.schemaPath;
|
|
471
|
-
return `${path} ${err.message}`;
|
|
472
|
-
}).join(', ');
|
|
473
|
-
throw new Error(`Datasource ${i + 1} (${datasourceJsons[i].key || 'unknown'}) does not match external-datasource schema: ${errors}`);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
return applicationSchema;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
115
|
module.exports = {
|
|
481
116
|
generateDeployJson,
|
|
482
117
|
generateDeployJsonWithValidation,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"key": "external-system-schema",
|
|
8
8
|
"name": "External System Configuration Schema",
|
|
9
9
|
"description": "JSON schema for validating ExternalSystem configuration files",
|
|
10
|
-
"version": "1.
|
|
10
|
+
"version": "1.1.0",
|
|
11
11
|
"type": "schema",
|
|
12
12
|
"category": "integration",
|
|
13
13
|
"author": "AI Fabrix Team",
|
|
@@ -27,6 +27,17 @@
|
|
|
27
27
|
],
|
|
28
28
|
"dependencies": [],
|
|
29
29
|
"changelog": [
|
|
30
|
+
{
|
|
31
|
+
"version": "1.1.0",
|
|
32
|
+
"date": "2025-12-01T00:00:00Z",
|
|
33
|
+
"changes": [
|
|
34
|
+
"Added RBAC roles and permissions support",
|
|
35
|
+
"RBAC roles/permissions are registered with miso-controller during deployment",
|
|
36
|
+
"Roles support Azure AD group mapping via Groups property",
|
|
37
|
+
"Permissions reference roles and are used for access control"
|
|
38
|
+
],
|
|
39
|
+
"breaking": false
|
|
40
|
+
},
|
|
30
41
|
{
|
|
31
42
|
"version": "1.0.0",
|
|
32
43
|
"date": "2024-01-01T00:00:00Z",
|
|
@@ -274,6 +285,86 @@
|
|
|
274
285
|
"items": {
|
|
275
286
|
"type": "string"
|
|
276
287
|
}
|
|
288
|
+
},
|
|
289
|
+
"roles": {
|
|
290
|
+
"type": "array",
|
|
291
|
+
"description": "External system roles for Azure AD group mapping",
|
|
292
|
+
"items": {
|
|
293
|
+
"type": "object",
|
|
294
|
+
"required": [
|
|
295
|
+
"name",
|
|
296
|
+
"value",
|
|
297
|
+
"description"
|
|
298
|
+
],
|
|
299
|
+
"properties": {
|
|
300
|
+
"name": {
|
|
301
|
+
"type": "string",
|
|
302
|
+
"description": "Human-readable role name",
|
|
303
|
+
"minLength": 1,
|
|
304
|
+
"maxLength": 100
|
|
305
|
+
},
|
|
306
|
+
"value": {
|
|
307
|
+
"type": "string",
|
|
308
|
+
"description": "Role identifier (used in JWT and ACL)",
|
|
309
|
+
"pattern": "^[a-z-]+$"
|
|
310
|
+
},
|
|
311
|
+
"description": {
|
|
312
|
+
"type": "string",
|
|
313
|
+
"description": "Role description",
|
|
314
|
+
"minLength": 1,
|
|
315
|
+
"maxLength": 500
|
|
316
|
+
},
|
|
317
|
+
"Groups": {
|
|
318
|
+
"type": "array",
|
|
319
|
+
"description": "Azure AD groups mapped to this role",
|
|
320
|
+
"items": {
|
|
321
|
+
"type": "string",
|
|
322
|
+
"minLength": 1,
|
|
323
|
+
"maxLength": 100
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
"additionalProperties": false
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
"permissions": {
|
|
331
|
+
"type": "array",
|
|
332
|
+
"description": "External system permissions with role mappings for access control",
|
|
333
|
+
"items": {
|
|
334
|
+
"type": "object",
|
|
335
|
+
"required": [
|
|
336
|
+
"name",
|
|
337
|
+
"roles",
|
|
338
|
+
"description"
|
|
339
|
+
],
|
|
340
|
+
"properties": {
|
|
341
|
+
"name": {
|
|
342
|
+
"type": "string",
|
|
343
|
+
"description": "Permission identifier (e.g., 'documentstore:read', 'flowise:dev:access')",
|
|
344
|
+
"pattern": "^[a-z0-9-:]+$",
|
|
345
|
+
"minLength": 1,
|
|
346
|
+
"maxLength": 100
|
|
347
|
+
},
|
|
348
|
+
"roles": {
|
|
349
|
+
"type": "array",
|
|
350
|
+
"description": "Roles that have this permission",
|
|
351
|
+
"items": {
|
|
352
|
+
"type": "string",
|
|
353
|
+
"pattern": "^[a-z-]+$",
|
|
354
|
+
"minLength": 1,
|
|
355
|
+
"maxLength": 50
|
|
356
|
+
},
|
|
357
|
+
"minItems": 1
|
|
358
|
+
},
|
|
359
|
+
"description": {
|
|
360
|
+
"type": "string",
|
|
361
|
+
"description": "Permission description",
|
|
362
|
+
"minLength": 1,
|
|
363
|
+
"maxLength": 500
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
"additionalProperties": false
|
|
367
|
+
}
|
|
277
368
|
}
|
|
278
369
|
},
|
|
279
370
|
"additionalProperties": false
|
|
@@ -25,9 +25,10 @@ const { formatNetworkError } = require('./error-formatters/network-errors');
|
|
|
25
25
|
/**
|
|
26
26
|
* Formats error for display in CLI
|
|
27
27
|
* @param {Object} apiResponse - API response object from makeApiCall
|
|
28
|
+
* @param {string} [controllerUrl] - Controller URL (optional)
|
|
28
29
|
* @returns {string} Formatted error message
|
|
29
30
|
*/
|
|
30
|
-
function formatApiError(apiResponse) {
|
|
31
|
+
function formatApiError(apiResponse, controllerUrl = null) {
|
|
31
32
|
if (!apiResponse || apiResponse.success !== false) {
|
|
32
33
|
return chalk.red('❌ Unknown error occurred');
|
|
33
34
|
}
|
|
@@ -41,7 +42,13 @@ function formatApiError(apiResponse) {
|
|
|
41
42
|
const statusCode = apiResponse.status || 0;
|
|
42
43
|
const isNetworkError = apiResponse.network === true;
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
// Add controller URL to error data
|
|
46
|
+
// Handle both object and string error responses
|
|
47
|
+
const errorData = typeof errorResponse === 'object' && errorResponse !== null
|
|
48
|
+
? { ...errorResponse, controllerUrl: controllerUrl }
|
|
49
|
+
: { message: String(errorResponse || ''), controllerUrl: controllerUrl };
|
|
50
|
+
|
|
51
|
+
const parsed = parseErrorResponse(errorData, statusCode, isNetworkError);
|
|
45
52
|
return parsed.formatted;
|
|
46
53
|
}
|
|
47
54
|
|
|
@@ -25,41 +25,51 @@ const { formatApiError } = require('./api-error-handler');
|
|
|
25
25
|
async function callRegisterApi(apiUrl, token, environment, registrationData) {
|
|
26
26
|
// Use centralized API client
|
|
27
27
|
const authConfig = { type: 'bearer', token: token };
|
|
28
|
-
|
|
28
|
+
try {
|
|
29
|
+
const response = await registerApplication(apiUrl, environment, authConfig, registrationData);
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
if (!response.success) {
|
|
32
|
+
const formattedError = response.formattedError || formatApiError(response, apiUrl);
|
|
33
|
+
logger.error(formattedError);
|
|
34
|
+
logger.error(chalk.gray(`\nController URL: ${apiUrl}`));
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
// For validation errors (400, 422), show the request payload for debugging
|
|
37
|
+
if (response.status === 400 || response.status === 422) {
|
|
38
|
+
logger.error(chalk.gray('\nRequest payload:'));
|
|
39
|
+
logger.error(chalk.gray(JSON.stringify(registrationData, null, 2)));
|
|
40
|
+
logger.error('');
|
|
41
|
+
logger.error(chalk.gray('Check your variables.yaml file and ensure all required fields are correctly set.'));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
process.exit(1);
|
|
40
45
|
}
|
|
41
46
|
|
|
47
|
+
// Handle API response structure:
|
|
48
|
+
// registerApplication returns: { success: true, data: <API response> }
|
|
49
|
+
// API response can be:
|
|
50
|
+
// 1. Direct format: { application: {...}, credentials: {...} }
|
|
51
|
+
// 2. Wrapped format: { success: true, data: { application: {...}, credentials: {...} } }
|
|
52
|
+
const apiResponse = response.data;
|
|
53
|
+
if (apiResponse && apiResponse.data && apiResponse.data.application) {
|
|
54
|
+
// Wrapped format: use apiResponse.data
|
|
55
|
+
return apiResponse.data;
|
|
56
|
+
} else if (apiResponse && apiResponse.application) {
|
|
57
|
+
// Direct format: use apiResponse directly
|
|
58
|
+
return apiResponse;
|
|
59
|
+
}
|
|
60
|
+
// Fallback: return apiResponse as-is (shouldn't happen, but handle gracefully)
|
|
61
|
+
logger.error(chalk.red('❌ Invalid response: missing application data'));
|
|
62
|
+
logger.error(chalk.gray(`\nController URL: ${apiUrl}`));
|
|
63
|
+
logger.error(chalk.gray('\nFull response for debugging:'));
|
|
64
|
+
logger.error(chalk.gray(JSON.stringify(response, null, 2)));
|
|
42
65
|
process.exit(1);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
// Include controller URL in error context
|
|
68
|
+
logger.error(chalk.red('❌ Registration API call failed'));
|
|
69
|
+
logger.error(chalk.gray(`Controller URL: ${apiUrl}`));
|
|
70
|
+
logger.error(chalk.gray(`Error: ${error.message}`));
|
|
71
|
+
throw error;
|
|
43
72
|
}
|
|
44
|
-
|
|
45
|
-
// Handle API response structure:
|
|
46
|
-
// registerApplication returns: { success: true, data: <API response> }
|
|
47
|
-
// API response can be:
|
|
48
|
-
// 1. Direct format: { application: {...}, credentials: {...} }
|
|
49
|
-
// 2. Wrapped format: { success: true, data: { application: {...}, credentials: {...} } }
|
|
50
|
-
const apiResponse = response.data;
|
|
51
|
-
if (apiResponse && apiResponse.data && apiResponse.data.application) {
|
|
52
|
-
// Wrapped format: use apiResponse.data
|
|
53
|
-
return apiResponse.data;
|
|
54
|
-
} else if (apiResponse && apiResponse.application) {
|
|
55
|
-
// Direct format: use apiResponse directly
|
|
56
|
-
return apiResponse;
|
|
57
|
-
}
|
|
58
|
-
// Fallback: return apiResponse as-is (shouldn't happen, but handle gracefully)
|
|
59
|
-
logger.error(chalk.red('❌ Invalid response: missing application data'));
|
|
60
|
-
logger.error(chalk.gray('\nFull response for debugging:'));
|
|
61
|
-
logger.error(chalk.gray(JSON.stringify(response, null, 2)));
|
|
62
|
-
process.exit(1);
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
module.exports = { callRegisterApi };
|