@aifabrix/builder 2.6.2 → 2.7.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.
@@ -0,0 +1,262 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://aifabrix.ai/schemas/external-system.schema.json",
4
+ "title": "AI Fabrix External System Configuration Schema",
5
+ "description": "Schema for configuring an external system connected to the AI Fabrix Dataplane. This defines authentication, OpenAPI/MCP bindings, field mappings defaults, metadata handling and portal inputs.",
6
+
7
+ "metadata": {
8
+ "key": "external-system-schema",
9
+ "name": "External System Configuration Schema",
10
+ "description": "JSON schema for validating ExternalSystem configuration files",
11
+ "version": "1.0.0",
12
+ "type": "schema",
13
+ "category": "integration",
14
+ "author": "AI Fabrix Team",
15
+ "createdAt": "2024-01-01T00:00:00Z",
16
+ "updatedAt": "2024-01-01T00:00:00Z",
17
+ "compatibility": {
18
+ "minVersion": "1.0.0",
19
+ "maxVersion": "2.0.0",
20
+ "deprecated": false
21
+ },
22
+ "tags": ["schema", "external-system", "dataplane", "integration", "validation"],
23
+ "dependencies": [],
24
+ "changelog": [
25
+ {
26
+ "version": "1.0.0",
27
+ "date": "2024-01-01T00:00:00Z",
28
+ "changes": [
29
+ "Initial schema for External Systems",
30
+ "Added OpenAPI and MCP contract configuration",
31
+ "Added authentication and environment variables",
32
+ "Added portalInput for UI-driven onboarding",
33
+ "Compatible with field-mappings.schema and security-model.schema"
34
+ ],
35
+ "breaking": false
36
+ }
37
+ ]
38
+ },
39
+
40
+ "type": "object",
41
+
42
+ "required": [
43
+ "key",
44
+ "displayName",
45
+ "description",
46
+ "type",
47
+ "authentication"
48
+ ],
49
+
50
+ "properties": {
51
+ "key": {
52
+ "type": "string",
53
+ "description": "Unique external system identifier (cannot be changed after creation). Example: 'hubspot', 'salesforce', 'teams'.",
54
+ "pattern": "^[a-z0-9-]+$",
55
+ "minLength": 3,
56
+ "maxLength": 40
57
+ },
58
+
59
+ "displayName": {
60
+ "type": "string",
61
+ "description": "Human-readable name for the external system",
62
+ "minLength": 1,
63
+ "maxLength": 100
64
+ },
65
+
66
+ "description": {
67
+ "type": "string",
68
+ "description": "Description of this external system integration",
69
+ "minLength": 1,
70
+ "maxLength": 500
71
+ },
72
+
73
+ "type": {
74
+ "type": "string",
75
+ "enum": ["openapi", "mcp", "custom"],
76
+ "description": "Integration type: OpenAPI-driven, MCP-driven, or custom Python connector."
77
+ },
78
+
79
+ "enabled": {
80
+ "type": "boolean",
81
+ "default": true
82
+ },
83
+
84
+ "environment": {
85
+ "type": "object",
86
+ "description": "Environment-level configuration values used by dataplane and external data sources.",
87
+ "properties": {
88
+ "baseUrl": {
89
+ "type": "string",
90
+ "description": "Base API URL or MCP server URL",
91
+ "pattern": "^(http|https)://.*$"
92
+ },
93
+ "region": {
94
+ "type": "string",
95
+ "description": "Optional region setting for API routing"
96
+ }
97
+ },
98
+ "additionalProperties": true
99
+ },
100
+
101
+ "authentication": {
102
+ "type": "object",
103
+ "description": "Authentication configuration for the external system.",
104
+ "required": ["mode"],
105
+ "properties": {
106
+ "mode": {
107
+ "type": "string",
108
+ "enum": ["oauth2", "apikey", "basic", "aad", "none"],
109
+ "description": "Authentication method used for API access."
110
+ },
111
+
112
+ "oauth2": {
113
+ "type": "object",
114
+ "description": "OAuth2 configuration used when mode == 'oauth2'. All secrets are stored in Key Vault.",
115
+ "properties": {
116
+ "tokenUrl": {
117
+ "type": "string",
118
+ "pattern": "^(http|https)://.*$"
119
+ },
120
+ "clientId": {
121
+ "type": "string"
122
+ },
123
+ "clientSecret": {
124
+ "type": "string"
125
+ },
126
+ "scopes": {
127
+ "type": "array",
128
+ "items": { "type": "string" }
129
+ }
130
+ },
131
+ "additionalProperties": false
132
+ },
133
+
134
+ "apikey": {
135
+ "type": "object",
136
+ "properties": {
137
+ "headerName": { "type": "string" },
138
+ "key": { "type": "string" }
139
+ },
140
+ "additionalProperties": false
141
+ },
142
+
143
+ "basic": {
144
+ "type": "object",
145
+ "properties": {
146
+ "username": { "type": "string" },
147
+ "password": { "type": "string" }
148
+ },
149
+ "additionalProperties": false
150
+ }
151
+ },
152
+ "additionalProperties": false
153
+ },
154
+
155
+ "openapi": {
156
+ "type": "object",
157
+ "description": "OpenAPI integration configuration.",
158
+ "properties": {
159
+ "documentKey": {
160
+ "type": "string",
161
+ "description": "Reference to OpenAPI spec registered via builder. Example: 'hubspot-v3'."
162
+ },
163
+ "autoDiscoverEntities": {
164
+ "type": "boolean",
165
+ "default": false,
166
+ "description": "Automatically discover resources/entities from OpenAPI schema."
167
+ }
168
+ },
169
+ "additionalProperties": false
170
+ },
171
+
172
+ "mcp": {
173
+ "type": "object",
174
+ "description": "Model Context Protocol integration config.",
175
+ "properties": {
176
+ "serverUrl": {
177
+ "type": "string",
178
+ "pattern": "^(http|https)://.*$"
179
+ },
180
+ "toolPrefix": {
181
+ "type": "string",
182
+ "pattern": "^[a-z0-9-]+$",
183
+ "description": "Prefix for MCP tool names generated from this system."
184
+ }
185
+ },
186
+ "additionalProperties": false
187
+ },
188
+
189
+ "dataSources": {
190
+ "type": "array",
191
+ "description": "List of data source keys belonging to this external system. Each must match external-datasource.schema.json.",
192
+ "items": {
193
+ "type": "string",
194
+ "pattern": "^[a-z0-9-]+$"
195
+ },
196
+ "uniqueItems": true
197
+ },
198
+
199
+ "configuration": {
200
+ "type": "array",
201
+ "description": "External system configuration variables (same pattern as application-schema).",
202
+ "items": {
203
+ "type": "object",
204
+ "required": ["name", "value", "location", "required"],
205
+ "properties": {
206
+ "name": {
207
+ "type": "string",
208
+ "pattern": "^[A-Z_][A-Z0-9_]*$"
209
+ },
210
+ "value": {
211
+ "type": "string",
212
+ "description": "Literal value or parameter reference ({{parameter}})"
213
+ },
214
+ "location": {
215
+ "type": "string",
216
+ "enum": ["variable", "keyvault"]
217
+ },
218
+ "required": {
219
+ "type": "boolean"
220
+ },
221
+ "portalInput": {
222
+ "type": "object",
223
+ "required": ["field", "label"],
224
+ "properties": {
225
+ "field": {
226
+ "type": "string",
227
+ "enum": ["password", "text", "textarea", "select", "json"]
228
+ },
229
+ "label": { "type": "string" },
230
+ "placeholder": { "type": "string" },
231
+ "options": {
232
+ "type": "array",
233
+ "items": { "type": "string" }
234
+ },
235
+ "masked": { "type": "boolean" },
236
+ "validation": {
237
+ "type": "object",
238
+ "properties": {
239
+ "minLength": { "type": "integer" },
240
+ "maxLength": { "type": "integer" },
241
+ "pattern": { "type": "string" },
242
+ "required": { "type": "boolean" }
243
+ },
244
+ "additionalProperties": false
245
+ }
246
+ },
247
+ "additionalProperties": false
248
+ }
249
+ },
250
+ "additionalProperties": false
251
+ }
252
+ },
253
+
254
+ "tags": {
255
+ "type": "array",
256
+ "description": "Optional tags for search / filtering",
257
+ "items": { "type": "string" }
258
+ }
259
+ },
260
+
261
+ "additionalProperties": false
262
+ }
package/lib/secrets.js CHANGED
@@ -20,7 +20,6 @@ const {
20
20
  formatMissingSecretsFileInfo,
21
21
  replaceKvInContent,
22
22
  loadEnvTemplate,
23
- processEnvVariables,
24
23
  adjustLocalEnvPortsInContent,
25
24
  rewriteInfraEndpoints,
26
25
  readYamlAtPath,
@@ -28,6 +27,7 @@ const {
28
27
  ensureNonEmptySecrets,
29
28
  validateSecrets
30
29
  } = require('./utils/secrets-helpers');
30
+ const { processEnvVariables } = require('./utils/env-copy');
31
31
  const { buildEnvVarMap } = require('./utils/env-map');
32
32
  const { resolveServicePortsInEnvContent } = require('./utils/secrets-url');
33
33
  const {
@@ -291,6 +291,25 @@ async function applyEnvironmentTransformations(resolved, environment, variablesP
291
291
  if (environment === 'docker') {
292
292
  resolved = await resolveServicePortsInEnvContent(resolved, environment);
293
293
  resolved = await rewriteInfraEndpoints(resolved, 'docker');
294
+ // Interpolate ${VAR} references created by rewriteInfraEndpoints
295
+ // Get the actual host and port values from env-endpoints.js directly
296
+ // to ensure they are correctly populated in envVars for interpolation
297
+ const { getEnvHosts, getServiceHost, getServicePort, getLocalhostOverride } = require('./utils/env-endpoints');
298
+ const hosts = await getEnvHosts('docker');
299
+ const localhostOverride = getLocalhostOverride('docker');
300
+ const redisHost = getServiceHost(hosts.REDIS_HOST, 'docker', 'redis', localhostOverride);
301
+ const redisPort = await getServicePort('REDIS_PORT', 'redis', hosts, 'docker', null);
302
+ const dbHost = getServiceHost(hosts.DB_HOST, 'docker', 'postgres', localhostOverride);
303
+ const dbPort = await getServicePort('DB_PORT', 'postgres', hosts, 'docker', null);
304
+
305
+ // Build envVars map and ensure it has the correct values
306
+ const envVars = await buildEnvVarMap('docker');
307
+ // Override with the actual values that were just set by rewriteInfraEndpoints
308
+ envVars.REDIS_HOST = redisHost;
309
+ envVars.REDIS_PORT = String(redisPort);
310
+ envVars.DB_HOST = dbHost;
311
+ envVars.DB_PORT = String(dbPort);
312
+ resolved = interpolateEnvVars(resolved, envVars);
294
313
  resolved = await updatePortForDocker(resolved, variablesPath);
295
314
  } else if (environment === 'local') {
296
315
  // adjustLocalEnvPortsInContent handles both PORT and infra endpoints
package/lib/utils/api.js CHANGED
@@ -236,14 +236,30 @@ async function authenticatedApiCall(url, options = {}, token) {
236
236
  if (refreshedToken && refreshedToken.token) {
237
237
  // Retry request with new token
238
238
  headers['Authorization'] = `Bearer ${refreshedToken.token}`;
239
- return makeApiCall(url, {
239
+ const retryResponse = await makeApiCall(url, {
240
240
  ...options,
241
241
  headers
242
242
  });
243
+ return retryResponse;
244
+ }
245
+
246
+ // Token refresh failed or no refresh token available
247
+ // Return a more helpful error message
248
+ if (!refreshedToken) {
249
+ return {
250
+ ...response,
251
+ error: 'Authentication failed: Token expired and refresh failed. Please login again using: aifabrix login',
252
+ formattedError: 'Authentication failed: Token expired and refresh failed. Please login again using: aifabrix login'
253
+ };
243
254
  }
244
255
  } catch (refreshError) {
245
- // Refresh failed, return original 401 error
246
- // This allows the caller to handle the authentication error
256
+ // Refresh failed, return original 401 error with additional context
257
+ const errorMessage = refreshError.message || String(refreshError);
258
+ return {
259
+ ...response,
260
+ error: `Authentication failed: ${errorMessage}. Please login again using: aifabrix login`,
261
+ formattedError: `Authentication failed: ${errorMessage}. Please login again using: aifabrix login`
262
+ };
247
263
  }
248
264
  }
249
265
 
@@ -16,6 +16,8 @@ const logger = require('./logger');
16
16
  const config = require('../config');
17
17
  const devConfig = require('../utils/dev-config');
18
18
  const { rewriteInfraEndpoints } = require('./env-endpoints');
19
+ const { buildEnvVarMap } = require('./env-map');
20
+ const { interpolateEnvVars } = require('./secrets-helpers');
19
21
 
20
22
  /**
21
23
  * Process and optionally copy env file to envOutputPath if configured
@@ -120,6 +122,28 @@ async function processEnvVariables(envPath, variablesPath, appName, secretsPath)
120
122
  });
121
123
  // Rewrite infra endpoints using env-config mapping for local context
122
124
  envContent = await rewriteInfraEndpoints(envContent, 'local', infraPorts);
125
+ // Interpolate ${VAR} references created by rewriteInfraEndpoints
126
+ // Extract actual values from updated content to use for interpolation
127
+ const envVars = await buildEnvVarMap('local', null, devIdNum);
128
+ // Extract REDIS_HOST, REDIS_PORT, DB_HOST, DB_PORT from updated content if present
129
+ // Only extract if the value doesn't contain ${VAR} references (to avoid circular interpolation)
130
+ const redisHostMatch = envContent.match(/^REDIS_HOST\s*=\s*([^\r\n$]+)/m);
131
+ const redisPortMatch = envContent.match(/^REDIS_PORT\s*=\s*([^\r\n$]+)/m);
132
+ const dbHostMatch = envContent.match(/^DB_HOST\s*=\s*([^\r\n$]+)/m);
133
+ const dbPortMatch = envContent.match(/^DB_PORT\s*=\s*([^\r\n$]+)/m);
134
+ if (redisHostMatch && redisHostMatch[1] && !redisHostMatch[1].includes('${')) {
135
+ envVars.REDIS_HOST = redisHostMatch[1].trim();
136
+ }
137
+ if (redisPortMatch && redisPortMatch[1] && !redisPortMatch[1].includes('${')) {
138
+ envVars.REDIS_PORT = redisPortMatch[1].trim();
139
+ }
140
+ if (dbHostMatch && dbHostMatch[1] && !dbHostMatch[1].includes('${')) {
141
+ envVars.DB_HOST = dbHostMatch[1].trim();
142
+ }
143
+ if (dbPortMatch && dbPortMatch[1] && !dbPortMatch[1].includes('${')) {
144
+ envVars.DB_PORT = dbPortMatch[1].trim();
145
+ }
146
+ envContent = interpolateEnvVars(envContent, envVars);
123
147
  fs.writeFileSync(outputPath, envContent, { mode: 0o600 });
124
148
  logger.log(chalk.green(`✓ Copied .env to: ${variables.build.envOutputPath}`));
125
149
  }
@@ -149,27 +149,26 @@ function updateEndpointVariables(envContent, redisHost, redisPort, dbHost, dbPor
149
149
 
150
150
  // Update REDIS_URL if present
151
151
  if (/^REDIS_URL\s*=.*$/m.test(updated)) {
152
- const m = updated.match(/^REDIS_URL\s*=\s*redis:\/\/([^:\s]+):\d+/m);
153
- const currentHost = m && m[1] ? m[1] : null;
154
- const targetHost = redisHost || currentHost;
155
- if (targetHost) {
156
- updated = updated.replace(
157
- /^REDIS_URL\s*=\s*.*$/m,
158
- `REDIS_URL=redis://${targetHost}:${redisPort}`
159
- );
160
- }
152
+ updated = updated.replace(
153
+ /^REDIS_URL\s*=\s*.*$/m,
154
+ 'REDIS_URL=redis://${REDIS_HOST}:${REDIS_PORT}'
155
+ );
161
156
  }
162
157
 
163
158
  // Update REDIS_HOST if present
159
+ // If the original has host:port pattern, use ${VAR} references
160
+ // Otherwise, just set the host value
164
161
  if (/^REDIS_HOST\s*=.*$/m.test(updated)) {
165
162
  const hostPortMatch = updated.match(/^REDIS_HOST\s*=\s*([a-zA-Z0-9_.-]+):\d+$/m);
166
163
  const hasPortPattern = !!hostPortMatch;
167
164
  if (hasPortPattern) {
165
+ // Original had host:port pattern, use ${VAR} references
168
166
  updated = updated.replace(
169
167
  /^REDIS_HOST\s*=\s*.*$/m,
170
- `REDIS_HOST=${redisHost}:${redisPort}`
168
+ 'REDIS_HOST=${REDIS_HOST}:${REDIS_PORT}'
171
169
  );
172
170
  } else {
171
+ // Just host, set actual value
173
172
  updated = updated.replace(/^REDIS_HOST\s*=\s*.*$/m, `REDIS_HOST=${redisHost}`);
174
173
  }
175
174
  }
@@ -195,6 +194,31 @@ function updateEndpointVariables(envContent, redisHost, redisPort, dbHost, dbPor
195
194
  );
196
195
  }
197
196
 
197
+ // Update DATABASE_URL and other database URL variables (postgresql:// or postgres:// URLs)
198
+ // Handles DATABASE_URL, DATABASELOG_URL, and any other *_URL variables with postgresql:// or postgres://
199
+ // First, collect all matches to avoid issues with modifying string during iteration
200
+ const dbUrlPattern = /^(DATABASE[^=]*_URL)\s*=\s*(postgresql?:\/\/)([^:]+):([^@]+)@([^:/]+):(\d+)(\/[^\s]*)?/gm;
201
+ const matches = [];
202
+ let dbUrlMatch;
203
+ // Reset regex lastIndex to ensure we start from beginning
204
+ dbUrlPattern.lastIndex = 0;
205
+ while ((dbUrlMatch = dbUrlPattern.exec(updated)) !== null) {
206
+ matches.push({
207
+ varName: dbUrlMatch[1], // DATABASE_URL, DATABASELOG_URL, etc.
208
+ protocol: dbUrlMatch[2], // postgresql:// or postgres://
209
+ user: dbUrlMatch[3], // username
210
+ password: dbUrlMatch[4], // password
211
+ path: dbUrlMatch[7] || '' // /database path
212
+ });
213
+ }
214
+ // Now apply all replacements
215
+ for (const match of matches) {
216
+ updated = updated.replace(
217
+ new RegExp(`^${match.varName}\\s*=\\s*.*$`, 'm'),
218
+ `${match.varName}=${match.protocol}${match.user}:${match.password}@\${DB_HOST}:\${DB_PORT}${match.path}`
219
+ );
220
+ }
221
+
198
222
  // Update DATABASE_PORT if present (some templates use DATABASE_PORT instead of DB_PORT)
199
223
  if (/^DATABASE_PORT\s*=.*$/m.test(updated)) {
200
224
  updated = updated.replace(
@@ -203,6 +227,20 @@ function updateEndpointVariables(envContent, redisHost, redisPort, dbHost, dbPor
203
227
  );
204
228
  }
205
229
 
230
+ // Update Keycloak database variables if present
231
+ // KC_DB_URL_HOST is used by Keycloak to connect to the database
232
+ if (/^KC_DB_URL_HOST\s*=.*$/m.test(updated)) {
233
+ updated = updated.replace(/^KC_DB_URL_HOST\s*=\s*.*$/m, `KC_DB_URL_HOST=${dbHost}`);
234
+ }
235
+
236
+ // KC_DB_URL_PORT is used by Keycloak for database port
237
+ if (/^KC_DB_URL_PORT\s*=.*$/m.test(updated)) {
238
+ updated = updated.replace(
239
+ /^KC_DB_URL_PORT\s*=\s*.*$/m,
240
+ `KC_DB_URL_PORT=${dbPort}`
241
+ );
242
+ }
243
+
206
244
  return updated;
207
245
  }
208
246
 
@@ -259,6 +297,7 @@ module.exports = {
259
297
  splitHost,
260
298
  getServicePort,
261
299
  getServiceHost,
262
- updateEndpointVariables
300
+ updateEndpointVariables,
301
+ getLocalhostOverride
263
302
  };
264
303