@aifabrix/builder 2.32.2 → 2.32.3

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.
@@ -22,6 +22,7 @@ const { detectAppType } = require('../utils/paths');
22
22
  const logger = require('../utils/logger');
23
23
  const { generateEnvTemplate } = require('../utils/external-system-env-helpers');
24
24
  const { generateVariablesYaml, generateReadme } = require('./download-helpers');
25
+ const { resolveControllerUrl } = require('../utils/controller-url');
25
26
 
26
27
  /**
27
28
  * Validates system type from downloaded application
@@ -126,7 +127,7 @@ function handlePartialDownload(systemKey, systemData, datasourceErrors) {
126
127
  */
127
128
  async function setupAuthenticationAndDataplane(systemKey, options, config) {
128
129
  const environment = options.environment || 'dev';
129
- const controllerUrl = options.controller || config.deployment?.controllerUrl || 'http://localhost:3000';
130
+ const controllerUrl = await resolveControllerUrl(options, config);
130
131
  const authConfig = await getDeploymentAuth(controllerUrl, environment, systemKey);
131
132
 
132
133
  if (!authConfig.token && !authConfig.clientId) {
@@ -10,6 +10,7 @@
10
10
 
11
11
  const { getDeploymentAuth } = require('../utils/token-manager');
12
12
  const { getDataplaneUrl } = require('../datasource/deploy');
13
+ const { resolveControllerUrl } = require('../utils/controller-url');
13
14
 
14
15
  /**
15
16
  * Setup authentication and get dataplane URL for integration tests
@@ -22,7 +23,7 @@ const { getDataplaneUrl } = require('../datasource/deploy');
22
23
  */
23
24
  async function setupIntegrationTestAuth(appName, options, config) {
24
25
  const environment = options.environment || 'dev';
25
- const controllerUrl = options.controller || config.deployment?.controllerUrl || 'http://localhost:3000';
26
+ const controllerUrl = await resolveControllerUrl(options, config);
26
27
  const authConfig = await getDeploymentAuth(controllerUrl, environment, appName);
27
28
 
28
29
  if (!authConfig.token && !authConfig.clientId) {
@@ -583,7 +583,7 @@
583
583
  "minLength": 1,
584
584
  "maxLength": 500
585
585
  },
586
- "Groups": {
586
+ "groups": {
587
587
  "type": "array",
588
588
  "description": "Azure AD groups mapped to this role",
589
589
  "items": {
@@ -3,21 +3,21 @@
3
3
  "$id":"https://raw.githubusercontent.com/esystemsdev/aifabrix-builder/refs/heads/main/lib/schema/external-datasource.schema.json",
4
4
  "title":"External Data Source",
5
5
  "description":"Configuration for AI Fabrix ExternalDataSource entities. Includes metadata schema, data dimensions, transformation mappings, OpenAPI/MCP exposure, execution logic, and sync behavior.",
6
- "metadata":{
7
- "key":"external-datasource-schema",
8
- "name":"External Data Source Configuration Schema",
9
- "description":"JSON schema for validating ExternalDataSource configuration files",
10
- "version":"2.0.0",
11
- "type":"schema",
12
- "category":"integration",
13
- "author":"AI Fabrix Team",
14
- "createdAt":"2024-01-01T00:00:00Z",
15
- "updatedAt":"2026-01-18T00:00:00Z",
16
- "compatibility":{
17
- "minVersion":"1.0.0",
18
- "maxVersion":"3.0.0",
19
- "deprecated":false
20
- },
6
+ "metadata":{
7
+ "key":"external-datasource-schema",
8
+ "name":"External Data Source Configuration Schema",
9
+ "description":"JSON schema for validating ExternalDataSource configuration files",
10
+ "version":"2.1.0",
11
+ "type":"schema",
12
+ "category":"integration",
13
+ "author":"AI Fabrix Team",
14
+ "createdAt":"2024-01-01T00:00:00Z",
15
+ "updatedAt":"2026-01-19T00:00:00Z",
16
+ "compatibility":{
17
+ "minVersion":"1.0.0",
18
+ "maxVersion":"3.0.0",
19
+ "deprecated":false
20
+ },
21
21
  "tags":[
22
22
  "schema",
23
23
  "external-datasource",
@@ -80,6 +80,17 @@
80
80
  "Updated descriptions to reflect dimension-first data model approach"
81
81
  ],
82
82
  "breaking":true
83
+ },
84
+ {
85
+ "version":"2.1.0",
86
+ "date":"2026-01-19T00:00:00Z",
87
+ "changes":[
88
+ "Added error semantics (onError object) to all CIP step types with error classification, retry, compensation, and failPipeline",
89
+ "Added idempotency configuration to CIP definition for execution-level replay guarantees",
90
+ "Added lineage configuration to field mappings for field-level explainability and compliance",
91
+ "Added contract versioning configuration to datasource root for CI/CD safety and agent stability"
92
+ ],
93
+ "breaking":false
83
94
  }
84
95
  ]
85
96
  },
@@ -223,6 +234,9 @@
223
234
  }
224
235
  },
225
236
  "additionalProperties":false
237
+ },
238
+ "lineage":{
239
+ "$ref":"#/$defs/fieldMappingLineage"
226
240
  }
227
241
  }
228
242
  }
@@ -846,6 +860,9 @@
846
860
  }
847
861
  },
848
862
  "additionalProperties":true
863
+ },
864
+ "contract":{
865
+ "$ref":"#/$defs/contractConfig"
849
866
  }
850
867
  },
851
868
  "$defs":{
@@ -931,6 +948,9 @@
931
948
  }
932
949
  },
933
950
  "additionalProperties":false
951
+ },
952
+ "idempotency":{
953
+ "$ref":"#/$defs/idempotencyConfig"
934
954
  }
935
955
  },
936
956
  "additionalProperties":false
@@ -1103,6 +1123,14 @@
1103
1123
  },
1104
1124
  "lineage":{
1105
1125
  "$ref":"#/$defs/cipLineage"
1126
+ },
1127
+ "stepId":{
1128
+ "type":"string",
1129
+ "pattern":"^[a-z0-9-_]+$",
1130
+ "description":"Unique identifier for this step within the operation"
1131
+ },
1132
+ "onError":{
1133
+ "$ref":"#/$defs/cipOnErrorConfig"
1106
1134
  }
1107
1135
  },
1108
1136
  "additionalProperties":false
@@ -1160,6 +1188,14 @@
1160
1188
  },
1161
1189
  "lineage":{
1162
1190
  "$ref":"#/$defs/cipLineage"
1191
+ },
1192
+ "stepId":{
1193
+ "type":"string",
1194
+ "pattern":"^[a-z0-9-_]+$",
1195
+ "description":"Unique identifier for this step within the operation"
1196
+ },
1197
+ "onError":{
1198
+ "$ref":"#/$defs/cipOnErrorConfig"
1163
1199
  }
1164
1200
  },
1165
1201
  "additionalProperties":false
@@ -1197,6 +1233,14 @@
1197
1233
  },
1198
1234
  "lineage":{
1199
1235
  "$ref":"#/$defs/cipLineage"
1236
+ },
1237
+ "stepId":{
1238
+ "type":"string",
1239
+ "pattern":"^[a-z0-9-_]+$",
1240
+ "description":"Unique identifier for this step within the operation"
1241
+ },
1242
+ "onError":{
1243
+ "$ref":"#/$defs/cipOnErrorConfig"
1200
1244
  }
1201
1245
  },
1202
1246
  "additionalProperties":false
@@ -1236,6 +1280,14 @@
1236
1280
  },
1237
1281
  "lineage":{
1238
1282
  "$ref":"#/$defs/cipLineage"
1283
+ },
1284
+ "stepId":{
1285
+ "type":"string",
1286
+ "pattern":"^[a-z0-9-_]+$",
1287
+ "description":"Unique identifier for this step within the operation"
1288
+ },
1289
+ "onError":{
1290
+ "$ref":"#/$defs/cipOnErrorConfig"
1239
1291
  }
1240
1292
  },
1241
1293
  "additionalProperties":false
@@ -1272,6 +1324,14 @@
1272
1324
  },
1273
1325
  "lineage":{
1274
1326
  "$ref":"#/$defs/cipLineage"
1327
+ },
1328
+ "stepId":{
1329
+ "type":"string",
1330
+ "pattern":"^[a-z0-9-_]+$",
1331
+ "description":"Unique identifier for this step within the operation"
1332
+ },
1333
+ "onError":{
1334
+ "$ref":"#/$defs/cipOnErrorConfig"
1275
1335
  }
1276
1336
  },
1277
1337
  "additionalProperties":false
@@ -1298,6 +1358,232 @@
1298
1358
  },
1299
1359
  "lineage":{
1300
1360
  "$ref":"#/$defs/cipLineage"
1361
+ },
1362
+ "stepId":{
1363
+ "type":"string",
1364
+ "pattern":"^[a-z0-9-_]+$",
1365
+ "description":"Unique identifier for this step within the operation"
1366
+ },
1367
+ "onError":{
1368
+ "$ref":"#/$defs/cipOnErrorConfig"
1369
+ }
1370
+ },
1371
+ "additionalProperties":false
1372
+ },
1373
+ "cipRetryConfig":{
1374
+ "type":"object",
1375
+ "description":"Retry configuration for error handling.",
1376
+ "properties":{
1377
+ "attempts":{
1378
+ "type":"integer",
1379
+ "minimum":1,
1380
+ "maximum":10,
1381
+ "default":3,
1382
+ "description":"Maximum number of retry attempts"
1383
+ },
1384
+ "backoff":{
1385
+ "type":"string",
1386
+ "enum":["exponential","linear","fixed"],
1387
+ "default":"exponential",
1388
+ "description":"Backoff strategy: exponential (2^attempt), linear (attempt * base), fixed (constant)"
1389
+ },
1390
+ "maxDelaySeconds":{
1391
+ "type":"number",
1392
+ "minimum":1.0,
1393
+ "maximum":300.0,
1394
+ "default":60.0,
1395
+ "description":"Maximum delay between retries in seconds"
1396
+ }
1397
+ },
1398
+ "additionalProperties":false
1399
+ },
1400
+ "cipCompensationConfig":{
1401
+ "type":"object",
1402
+ "description":"Compensation (rollback) configuration for error handling.",
1403
+ "properties":{
1404
+ "enabled":{
1405
+ "type":"boolean",
1406
+ "default":false,
1407
+ "description":"Whether compensation is enabled"
1408
+ },
1409
+ "stepRef":{
1410
+ "type":"string",
1411
+ "pattern":"^[a-z0-9-_]+$",
1412
+ "description":"Reference to a previously executed step by stepId to execute as compensation"
1413
+ }
1414
+ },
1415
+ "additionalProperties":false
1416
+ },
1417
+ "cipOnErrorConfig":{
1418
+ "type":"object",
1419
+ "description":"Enhanced error handling configuration for CIP steps.",
1420
+ "required":["class"],
1421
+ "properties":{
1422
+ "class":{
1423
+ "type":"string",
1424
+ "enum":["transient","permanent","validation","auth"],
1425
+ "description":"Error classification: transient (retryable), permanent (non-retryable), validation (data validation failure), auth (authentication/authorization failure)"
1426
+ },
1427
+ "retry":{
1428
+ "$ref":"#/$defs/cipRetryConfig",
1429
+ "description":"Retry configuration (only applies when class='transient')"
1430
+ },
1431
+ "compensate":{
1432
+ "$ref":"#/$defs/cipCompensationConfig",
1433
+ "description":"Compensation (rollback) configuration"
1434
+ },
1435
+ "failPipeline":{
1436
+ "type":"boolean",
1437
+ "default":true,
1438
+ "description":"Whether step failure should fail the entire pipeline (false allows partial success)"
1439
+ }
1440
+ },
1441
+ "additionalProperties":false
1442
+ },
1443
+ "idempotencyConfig":{
1444
+ "type":"object",
1445
+ "description":"Idempotency configuration for CIP execution.",
1446
+ "required":["key"],
1447
+ "properties":{
1448
+ "key":{
1449
+ "type":"string",
1450
+ "description":"Idempotency key template. Supports variables: {{actor.userId}}, {{raw.requestId}}, {{correlationId}}, {{execution.operation}}, {{execution.datasourceId}}"
1451
+ },
1452
+ "scope":{
1453
+ "type":"string",
1454
+ "enum":["datasource","system"],
1455
+ "default":"datasource",
1456
+ "description":"Idempotency scope: datasource (same key within datasource) or system (same key across all datasources)"
1457
+ },
1458
+ "ttlSeconds":{
1459
+ "type":"integer",
1460
+ "minimum":1,
1461
+ "maximum":604800,
1462
+ "default":86400,
1463
+ "description":"Time to live for idempotency records in seconds (default: 86400 = 24 hours)"
1464
+ },
1465
+ "enforcement":{
1466
+ "type":"string",
1467
+ "enum":["strict","best-effort"],
1468
+ "default":"strict",
1469
+ "description":"Enforcement mode: strict (return previous result, no execution) or best-effort (warn + continue)"
1470
+ }
1471
+ },
1472
+ "additionalProperties":false
1473
+ },
1474
+ "transformationRule":{
1475
+ "type":"object",
1476
+ "description":"Transformation rule applied to a field.",
1477
+ "required":["type"],
1478
+ "properties":{
1479
+ "type":{
1480
+ "type":"string",
1481
+ "description":"Type of transformation: 'mapping', 'function', 'filter', 'aggregate'"
1482
+ },
1483
+ "function":{
1484
+ "type":"string",
1485
+ "description":"Function name (e.g., 'mapStatus', 'toLower', 'trim')"
1486
+ },
1487
+ "rule":{
1488
+ "type":"string",
1489
+ "description":"Human-readable rule description (e.g., 'open → OPEN, closed → CLOSED')"
1490
+ }
1491
+ },
1492
+ "additionalProperties":false
1493
+ },
1494
+ "fieldMappingLineage":{
1495
+ "type":"object",
1496
+ "description":"Lineage metadata for a field mapping.",
1497
+ "required":["sourceFields"],
1498
+ "properties":{
1499
+ "sourceFields":{
1500
+ "type":"array",
1501
+ "minItems":1,
1502
+ "items":{
1503
+ "type":"string"
1504
+ },
1505
+ "description":"Source field paths that contribute to this mapped field (e.g., ['raw.fields.state'])"
1506
+ },
1507
+ "transformations":{
1508
+ "type":"array",
1509
+ "items":{
1510
+ "$ref":"#/$defs/transformationRule"
1511
+ },
1512
+ "description":"List of transformations applied to source fields"
1513
+ },
1514
+ "confidence":{
1515
+ "type":"number",
1516
+ "minimum":0.0,
1517
+ "maximum":1.0,
1518
+ "default":1.0,
1519
+ "description":"Confidence score for this mapping (0.0-1.0). Default: 1.0 for deterministic transformations."
1520
+ }
1521
+ },
1522
+ "additionalProperties":false
1523
+ },
1524
+ "compatibilityConfig":{
1525
+ "type":"object",
1526
+ "description":"Compatibility configuration for contract versioning.",
1527
+ "properties":{
1528
+ "backwardCompatible":{
1529
+ "type":"boolean",
1530
+ "default":true,
1531
+ "description":"Whether this version is backward compatible with previous versions"
1532
+ },
1533
+ "breakingChanges":{
1534
+ "type":"array",
1535
+ "items":{
1536
+ "type":"string"
1537
+ },
1538
+ "description":"List of breaking change types: removeField, changeType, changeSemantics"
1539
+ }
1540
+ },
1541
+ "additionalProperties":false
1542
+ },
1543
+ "deprecationConfig":{
1544
+ "type":"object",
1545
+ "description":"Deprecation configuration for contract versioning.",
1546
+ "properties":{
1547
+ "sunsetDate":{
1548
+ "type":"string",
1549
+ "pattern":"^\\d{4}-\\d{2}-\\d{2}$",
1550
+ "description":"Date when this contract version will be sunset (YYYY-MM-DD)"
1551
+ },
1552
+ "replacedBy":{
1553
+ "type":"string",
1554
+ "description":"Contract name/version that replaces this deprecated version"
1555
+ }
1556
+ },
1557
+ "additionalProperties":false
1558
+ },
1559
+ "contractConfig":{
1560
+ "type":"object",
1561
+ "description":"Contract versioning configuration for datasource.",
1562
+ "required":["name","version"],
1563
+ "properties":{
1564
+ "name":{
1565
+ "type":"string",
1566
+ "pattern":"^[a-z0-9.-]+$",
1567
+ "description":"Contract identifier (e.g., 'jira.issue', separate from datasource key)"
1568
+ },
1569
+ "version":{
1570
+ "type":"string",
1571
+ "pattern":"^[0-9]+\\.[0-9]+\\.[0-9]+$",
1572
+ "description":"Semantic version of the contract (e.g., '2.1.0')"
1573
+ },
1574
+ "status":{
1575
+ "type":"string",
1576
+ "enum":["stable","deprecated","experimental"],
1577
+ "default":"stable",
1578
+ "description":"Contract status: stable (production-ready), deprecated (being phased out), experimental (testing)"
1579
+ },
1580
+ "compatibility":{
1581
+ "$ref":"#/$defs/compatibilityConfig",
1582
+ "description":"Compatibility information for this contract version"
1583
+ },
1584
+ "deprecation":{
1585
+ "$ref":"#/$defs/deprecationConfig",
1586
+ "description":"Deprecation information if status is 'deprecated'"
1301
1587
  }
1302
1588
  },
1303
1589
  "additionalProperties":false
@@ -314,7 +314,7 @@
314
314
  "minLength": 1,
315
315
  "maxLength": 500
316
316
  },
317
- "Groups": {
317
+ "groups": {
318
318
  "type": "array",
319
319
  "description": "Azure AD groups mapped to this role",
320
320
  "items": {
package/lib/utils/api.js CHANGED
@@ -209,10 +209,20 @@ function extractControllerUrl(url) {
209
209
  * Automatically refreshes device token on 401 errors if refresh token is available
210
210
  * @param {string} url - API endpoint URL
211
211
  * @param {Object} options - Fetch options
212
- * @param {string} token - Bearer token
212
+ * @param {string|Object} tokenOrAuthConfig - Bearer token string or authConfig object
213
+ * @param {string} [tokenOrAuthConfig.token] - Bearer token (if object)
214
+ * @param {string} [tokenOrAuthConfig.controller] - Controller URL for token refresh (if object)
213
215
  * @returns {Promise<Object>} Response object
214
216
  */
215
- async function authenticatedApiCall(url, options = {}, token) {
217
+ async function authenticatedApiCall(url, options = {}, tokenOrAuthConfig) {
218
+ // Support both string token (backward compat) and authConfig object
219
+ const token = typeof tokenOrAuthConfig === 'string'
220
+ ? tokenOrAuthConfig
221
+ : tokenOrAuthConfig?.token;
222
+ const authControllerUrl = typeof tokenOrAuthConfig === 'object'
223
+ ? tokenOrAuthConfig?.controller
224
+ : null;
225
+
216
226
  const headers = {
217
227
  'Content-Type': 'application/json',
218
228
  ...options.headers
@@ -230,12 +240,15 @@ async function authenticatedApiCall(url, options = {}, token) {
230
240
  // Handle 401 errors with automatic token refresh for device tokens
231
241
  if (!response.success && response.status === 401) {
232
242
  try {
233
- // Extract controller URL from request URL
234
- const controllerUrl = extractControllerUrl(url);
243
+ // Use controller URL from authConfig if available, otherwise extract from request URL
244
+ // This is important when the request URL is a dataplane URL but the token
245
+ // is stored under the controller URL
246
+ const controllerUrl = authControllerUrl || extractControllerUrl(url);
235
247
 
236
- // Try to get and refresh device token
237
- const { getOrRefreshDeviceToken } = require('./token-manager');
238
- const refreshedToken = await getOrRefreshDeviceToken(controllerUrl);
248
+ // Try to force refresh device token on 401 (regardless of local expiry time)
249
+ // because the server rejected the token
250
+ const { forceRefreshDeviceToken } = require('./token-manager');
251
+ const refreshedToken = await forceRefreshDeviceToken(controllerUrl);
239
252
 
240
253
  if (refreshedToken && refreshedToken.token) {
241
254
  // Retry request with new token
@@ -48,7 +48,8 @@ function displayRegistrationResults(data, apiUrl, environment) {
48
48
  logger.log(chalk.bold('📋 Application Details:'));
49
49
  logger.log(` ID: ${data.application.id}`);
50
50
  logger.log(` Key: ${data.application.key}`);
51
- logger.log(` Display Name: ${data.application.displayName}\n`);
51
+ logger.log(` Display Name: ${data.application.displayName}`);
52
+ logger.log(` Controller: ${apiUrl}\n`);
52
53
 
53
54
  logger.log(chalk.bold.yellow('🔑 CREDENTIALS (save these immediately):'));
54
55
  logger.log(chalk.yellow(` Client ID: ${data.credentials.clientId}`));
@@ -135,7 +135,9 @@ function formatAzureError(errorMsg) {
135
135
  ' Use format: *.azurecr.io (e.g., myacr.azurecr.io)'
136
136
  ];
137
137
  }
138
- if (errorMsg.includes('authenticate') || errorMsg.includes('ACR') || errorMsg.includes('Authentication required')) {
138
+ // Only match ACR-specific authentication errors, not general authentication failures
139
+ if (errorMsg.includes('ACR') || errorMsg.includes('azurecr.io') ||
140
+ (errorMsg.includes('authenticate') && (errorMsg.includes('registry') || errorMsg.includes('container')))) {
139
141
  return [
140
142
  ' Azure Container Registry authentication failed.',
141
143
  ' Run: az acr login --name <registry-name>',
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Controller URL Resolution Utilities
3
+ *
4
+ * Provides utilities for resolving controller URLs with developer ID-based defaults
5
+ * and fallback chain support.
6
+ *
7
+ * @fileoverview Controller URL resolution utilities
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const { getDeveloperIdNumber } = require('./env-map');
13
+ const devConfig = require('./dev-config');
14
+
15
+ /**
16
+ * Calculate default controller URL based on developer ID
17
+ * Uses getDevPorts to get the app port which is adjusted by developer ID
18
+ * Developer ID 0 = http://localhost:3000
19
+ * Developer ID 1 = http://localhost:3100
20
+ * Developer ID 2 = http://localhost:3200
21
+ * @async
22
+ * @function getDefaultControllerUrl
23
+ * @returns {Promise<string>} Default controller URL
24
+ */
25
+ async function getDefaultControllerUrl() {
26
+ const developerId = await getDeveloperIdNumber(null);
27
+ const ports = devConfig.getDevPorts(developerId);
28
+ return `http://localhost:${ports.app}`;
29
+ }
30
+
31
+ /**
32
+ * Resolve controller URL with fallback chain
33
+ * Priority:
34
+ * 1. options.controller (explicit option)
35
+ * 2. config.deployment?.controllerUrl (from config)
36
+ * 3. getDefaultControllerUrl() (developer ID-based default)
37
+ * @async
38
+ * @function resolveControllerUrl
39
+ * @param {Object} options - Command options
40
+ * @param {string} [options.controller] - Explicit controller URL option
41
+ * @param {Object} config - Configuration object
42
+ * @param {Object} [config.deployment] - Deployment configuration
43
+ * @param {string} [config.deployment.controllerUrl] - Controller URL from config
44
+ * @returns {Promise<string>} Resolved controller URL
45
+ */
46
+ async function resolveControllerUrl(options, config) {
47
+ // Priority 1: Explicit option
48
+ if (options && (options.controller || options.url)) {
49
+ const explicitUrl = options.controller || options.url;
50
+ if (explicitUrl) {
51
+ return explicitUrl.replace(/\/$/, '');
52
+ }
53
+ }
54
+
55
+ // Priority 2: Config file
56
+ if (config?.deployment?.controllerUrl) {
57
+ return config.deployment.controllerUrl.replace(/\/$/, '');
58
+ }
59
+
60
+ // Priority 3: Developer ID-based default
61
+ return await getDefaultControllerUrl();
62
+ }
63
+
64
+ module.exports = {
65
+ getDefaultControllerUrl,
66
+ resolveControllerUrl
67
+ };
@@ -297,6 +297,7 @@ async function buildEnvVarMap(context, osModule = null, developerId = null) {
297
297
  }
298
298
 
299
299
  module.exports = {
300
- buildEnvVarMap
300
+ buildEnvVarMap,
301
+ getDeveloperIdNumber
301
302
  };
302
303