@aifabrix/builder 2.32.3 → 2.33.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 +8 -0
- package/README.md +36 -8
- package/bin/aifabrix.js +6 -8
- package/integration/hubspot/README.md +8 -7
- package/integration/hubspot/companies.json +2048 -0
- package/integration/hubspot/create-hubspot.js +665 -0
- package/integration/hubspot/{hubspot-deploy-company.json → hubspot-datasource-company.json} +1 -1
- package/integration/hubspot/{hubspot-deploy-contact.json → hubspot-datasource-contact.json} +1 -1
- package/integration/hubspot/{hubspot-deploy-deal.json → hubspot-datasource-deal.json} +1 -1
- package/integration/hubspot/hubspot-deploy.json +832 -81
- package/integration/hubspot/hubspot-system.json +99 -0
- package/integration/hubspot/test-artifacts/wizard-hubspot-credential-real.yaml +20 -0
- package/integration/hubspot/test-artifacts/wizard-hubspot-env-vars.yaml +9 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-add-datasource.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-app-name.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-credential-create.yaml +7 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-credential-select.yaml +7 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-known-platform.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-missing-app.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-mode.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-openapi-file.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-openapi-url.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-source.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-array-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +5 -0
- package/integration/hubspot/test-dataplane-down-helpers.js +246 -0
- package/integration/hubspot/test-dataplane-down-tests.js +419 -0
- package/integration/hubspot/test-dataplane-down.js +157 -0
- package/integration/hubspot/test.js +1517 -0
- package/integration/hubspot/variables.yaml +4 -4
- package/integration/hubspot/wizard-hubspot-e2e.yaml +16 -0
- package/integration/hubspot/wizard-hubspot-platform.yaml +8 -0
- package/lib/api/applications.api.js +1 -0
- package/lib/api/types/wizard.types.js +176 -38
- package/lib/api/wizard.api.js +161 -23
- package/lib/app/deploy.js +116 -54
- package/lib/app/display.js +6 -5
- package/lib/app/dockerfile.js +2 -1
- package/lib/app/list.js +17 -10
- package/lib/app/readme.js +41 -112
- package/lib/app/register.js +44 -9
- package/lib/app/rotate-secret.js +48 -31
- package/lib/cli.js +219 -70
- package/lib/commands/app.js +4 -9
- package/lib/commands/auth-config.js +125 -0
- package/lib/commands/auth-status.js +7 -8
- package/lib/commands/datasource.js +3 -6
- package/lib/commands/login-credentials.js +4 -4
- package/lib/commands/login-device.js +26 -17
- package/lib/commands/login.js +12 -10
- package/lib/commands/wizard-config-normalizer.js +92 -0
- package/lib/commands/wizard-core.js +515 -0
- package/lib/commands/wizard-dataplane.js +122 -0
- package/lib/commands/wizard-headless.js +115 -0
- package/lib/commands/wizard.js +110 -332
- package/lib/core/config.js +46 -0
- package/lib/core/secrets.js +3 -22
- package/lib/core/templates-env.js +1 -1
- package/lib/datasource/deploy.js +29 -21
- package/lib/datasource/list.js +8 -6
- package/lib/deployment/deployer.js +25 -0
- package/lib/deployment/environment.js +10 -13
- package/lib/external-system/delete.js +151 -0
- package/lib/external-system/deploy.js +53 -378
- package/lib/external-system/download-helpers.js +45 -65
- package/lib/external-system/download.js +33 -13
- package/lib/external-system/generator.js +11 -7
- package/lib/external-system/test-auth.js +4 -3
- package/lib/generator/builders.js +3 -1
- package/lib/generator/external-controller-manifest.js +157 -0
- package/lib/generator/external-schema-utils.js +236 -0
- package/lib/generator/external.js +55 -3
- package/lib/generator/index.js +22 -10
- package/lib/generator/wizard-prompts.js +33 -10
- package/lib/generator/wizard.js +69 -86
- package/lib/infrastructure/compose.js +100 -0
- package/lib/infrastructure/helpers.js +139 -0
- package/lib/infrastructure/index.js +52 -311
- package/lib/infrastructure/services.js +168 -0
- package/lib/schema/application-schema.json +23 -4
- package/lib/schema/external-datasource.schema.json +2 -2
- package/lib/schema/wizard-config.schema.json +234 -0
- package/lib/utils/api.js +32 -50
- package/lib/utils/app-existence.js +42 -0
- package/lib/utils/app-register-config.js +7 -2
- package/lib/utils/auth-config-validator.js +92 -0
- package/lib/utils/command-header.js +43 -0
- package/lib/utils/compose-generator.js +113 -70
- package/lib/utils/controller-url.js +65 -17
- package/lib/utils/dataplane-health.js +115 -0
- package/lib/utils/dataplane-resolver.js +29 -0
- package/lib/utils/dev-config.js +6 -2
- package/lib/utils/env-copy.js +2 -1
- package/lib/utils/env-ports.js +2 -1
- package/lib/utils/env-template.js +1 -1
- package/lib/utils/error-formatter.js +49 -0
- package/lib/utils/external-readme.js +125 -0
- package/lib/utils/help-builder.js +190 -0
- package/lib/utils/infra-status.js +13 -3
- package/lib/utils/paths.js +17 -2
- package/lib/utils/port-resolver.js +111 -0
- package/lib/utils/secrets-helpers.js +3 -15
- package/lib/utils/secrets-utils.js +2 -2
- package/lib/utils/token-manager.js +9 -4
- package/lib/utils/variable-transformer.js +7 -2
- package/lib/validation/external-manifest-validator.js +202 -0
- package/lib/validation/validate-display.js +406 -0
- package/lib/validation/validate.js +159 -123
- package/lib/validation/validator.js +36 -3
- package/lib/validation/wizard-config-validator.js +267 -0
- package/package.json +4 -2
- package/templates/applications/README.md.hbs +18 -16
- package/templates/applications/miso-controller/env.template +1 -1
- package/templates/applications/miso-controller/rbac.yaml +7 -7
- package/templates/external-system/README.md.hbs +99 -0
- package/templates/infra/compose.yaml.hbs +35 -0
- package/templates/python/docker-compose.hbs +26 -0
- package/templates/typescript/docker-compose.hbs +26 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/esystemsdev/aifabrix-builder/refs/heads/main/lib/schema/wizard-config.schema.json",
|
|
4
|
+
"title": "AI Fabrix Wizard Configuration Schema",
|
|
5
|
+
"description": "Schema for validating wizard.yaml configuration files for headless external system creation",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["appName", "mode", "source"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"appName": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "Application name (must be lowercase alphanumeric with hyphens and underscores)",
|
|
12
|
+
"pattern": "^[a-z0-9-_]+$",
|
|
13
|
+
"minLength": 1,
|
|
14
|
+
"maxLength": 50
|
|
15
|
+
},
|
|
16
|
+
"mode": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "Wizard mode",
|
|
19
|
+
"enum": ["create-system", "add-datasource"]
|
|
20
|
+
},
|
|
21
|
+
"systemIdOrKey": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "Existing system ID or key (required when mode='add-datasource')",
|
|
24
|
+
"pattern": "^[a-z0-9-]+$",
|
|
25
|
+
"minLength": 1,
|
|
26
|
+
"maxLength": 50
|
|
27
|
+
},
|
|
28
|
+
"source": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"description": "Source configuration for the wizard",
|
|
31
|
+
"required": ["type"],
|
|
32
|
+
"properties": {
|
|
33
|
+
"type": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"description": "Source type",
|
|
36
|
+
"enum": ["openapi-file", "openapi-url", "mcp-server", "known-platform"]
|
|
37
|
+
},
|
|
38
|
+
"filePath": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"description": "Path to OpenAPI file (for openapi-file type)",
|
|
41
|
+
"minLength": 1
|
|
42
|
+
},
|
|
43
|
+
"url": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"description": "OpenAPI URL (for openapi-url type)",
|
|
46
|
+
"pattern": "^https?://.*$"
|
|
47
|
+
},
|
|
48
|
+
"serverUrl": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"description": "MCP server URL (for mcp-server type)",
|
|
51
|
+
"pattern": "^https?://.*$"
|
|
52
|
+
},
|
|
53
|
+
"token": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"description": "MCP server authentication token (supports ${ENV_VAR} syntax)",
|
|
56
|
+
"minLength": 1
|
|
57
|
+
},
|
|
58
|
+
"platform": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"description": "Known platform identifier (for known-platform type)",
|
|
61
|
+
"enum": ["hubspot", "salesforce", "zendesk", "slack", "microsoft365"]
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"allOf": [
|
|
65
|
+
{
|
|
66
|
+
"if": {
|
|
67
|
+
"properties": { "type": { "const": "openapi-file" } }
|
|
68
|
+
},
|
|
69
|
+
"then": {
|
|
70
|
+
"required": ["filePath"]
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"if": {
|
|
75
|
+
"properties": { "type": { "const": "openapi-url" } }
|
|
76
|
+
},
|
|
77
|
+
"then": {
|
|
78
|
+
"required": ["url"]
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"if": {
|
|
83
|
+
"properties": { "type": { "const": "mcp-server" } }
|
|
84
|
+
},
|
|
85
|
+
"then": {
|
|
86
|
+
"required": ["serverUrl", "token"]
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"if": {
|
|
91
|
+
"properties": { "type": { "const": "known-platform" } }
|
|
92
|
+
},
|
|
93
|
+
"then": {
|
|
94
|
+
"required": ["platform"]
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
},
|
|
99
|
+
"credential": {
|
|
100
|
+
"type": "object",
|
|
101
|
+
"description": "Credential configuration for the wizard (optional)",
|
|
102
|
+
"required": ["action"],
|
|
103
|
+
"properties": {
|
|
104
|
+
"action": {
|
|
105
|
+
"type": "string",
|
|
106
|
+
"description": "Credential action",
|
|
107
|
+
"enum": ["create", "select", "skip"]
|
|
108
|
+
},
|
|
109
|
+
"credentialIdOrKey": {
|
|
110
|
+
"type": "string",
|
|
111
|
+
"description": "Credential ID or key (required when action='select')",
|
|
112
|
+
"minLength": 1
|
|
113
|
+
},
|
|
114
|
+
"config": {
|
|
115
|
+
"type": "object",
|
|
116
|
+
"description": "Credential configuration (required when action='create')",
|
|
117
|
+
"properties": {
|
|
118
|
+
"key": {
|
|
119
|
+
"type": "string",
|
|
120
|
+
"description": "Credential key",
|
|
121
|
+
"pattern": "^[a-z0-9-]+$",
|
|
122
|
+
"minLength": 1
|
|
123
|
+
},
|
|
124
|
+
"displayName": {
|
|
125
|
+
"type": "string",
|
|
126
|
+
"description": "Credential display name",
|
|
127
|
+
"minLength": 1
|
|
128
|
+
},
|
|
129
|
+
"type": {
|
|
130
|
+
"type": "string",
|
|
131
|
+
"description": "Credential type",
|
|
132
|
+
"enum": ["OAUTH2", "API_KEY", "BASIC", "BEARER"]
|
|
133
|
+
},
|
|
134
|
+
"config": {
|
|
135
|
+
"type": "object",
|
|
136
|
+
"description": "Credential-specific configuration",
|
|
137
|
+
"additionalProperties": true
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
"required": ["key", "displayName", "type"]
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
"allOf": [
|
|
144
|
+
{
|
|
145
|
+
"if": {
|
|
146
|
+
"properties": { "action": { "const": "select" } }
|
|
147
|
+
},
|
|
148
|
+
"then": {
|
|
149
|
+
"required": ["credentialIdOrKey"]
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"if": {
|
|
154
|
+
"properties": { "action": { "const": "create" } }
|
|
155
|
+
},
|
|
156
|
+
"then": {
|
|
157
|
+
"required": ["config"]
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
},
|
|
162
|
+
"preferences": {
|
|
163
|
+
"type": "object",
|
|
164
|
+
"description": "Generation preferences",
|
|
165
|
+
"properties": {
|
|
166
|
+
"intent": {
|
|
167
|
+
"type": "string",
|
|
168
|
+
"description": "User intent (any descriptive text, e.g., 'sales-focused CRM integration')",
|
|
169
|
+
"minLength": 1,
|
|
170
|
+
"maxLength": 500
|
|
171
|
+
},
|
|
172
|
+
"fieldOnboardingLevel": {
|
|
173
|
+
"type": "string",
|
|
174
|
+
"description": "Field onboarding level",
|
|
175
|
+
"enum": ["full", "standard", "minimal"],
|
|
176
|
+
"default": "full"
|
|
177
|
+
},
|
|
178
|
+
"enableOpenAPIGeneration": {
|
|
179
|
+
"type": "boolean",
|
|
180
|
+
"description": "Enable OpenAPI operation generation",
|
|
181
|
+
"default": true
|
|
182
|
+
},
|
|
183
|
+
"enableMCP": {
|
|
184
|
+
"type": "boolean",
|
|
185
|
+
"description": "Enable Model Context Protocol",
|
|
186
|
+
"default": false
|
|
187
|
+
},
|
|
188
|
+
"enableABAC": {
|
|
189
|
+
"type": "boolean",
|
|
190
|
+
"description": "Enable Attribute-Based Access Control",
|
|
191
|
+
"default": false
|
|
192
|
+
},
|
|
193
|
+
"enableRBAC": {
|
|
194
|
+
"type": "boolean",
|
|
195
|
+
"description": "Enable Role-Based Access Control",
|
|
196
|
+
"default": false
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
"deployment": {
|
|
201
|
+
"type": "object",
|
|
202
|
+
"description": "Deployment settings (optional overrides)",
|
|
203
|
+
"properties": {
|
|
204
|
+
"controller": {
|
|
205
|
+
"type": "string",
|
|
206
|
+
"description": "Controller URL",
|
|
207
|
+
"pattern": "^https?://.*$"
|
|
208
|
+
},
|
|
209
|
+
"environment": {
|
|
210
|
+
"type": "string",
|
|
211
|
+
"description": "Environment key",
|
|
212
|
+
"enum": ["dev", "tst", "pro", "miso"],
|
|
213
|
+
"default": "dev"
|
|
214
|
+
},
|
|
215
|
+
"dataplane": {
|
|
216
|
+
"type": "string",
|
|
217
|
+
"description": "Dataplane URL (overrides controller lookup)",
|
|
218
|
+
"pattern": "^https?://.*$"
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
"allOf": [
|
|
224
|
+
{
|
|
225
|
+
"if": {
|
|
226
|
+
"properties": { "mode": { "const": "add-datasource" } }
|
|
227
|
+
},
|
|
228
|
+
"then": {
|
|
229
|
+
"required": ["systemIdOrKey"]
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
],
|
|
233
|
+
"additionalProperties": false
|
|
234
|
+
}
|
package/lib/utils/api.js
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
const { parseErrorResponse } = require('./api-error-handler');
|
|
14
14
|
const auditLogger = require('../core/audit-logger');
|
|
15
15
|
|
|
16
|
+
/** Default timeout for HTTP requests (ms). Prevents hanging when the controller is unreachable. */
|
|
17
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 5000;
|
|
18
|
+
|
|
16
19
|
/**
|
|
17
20
|
* Logs API request performance metrics and errors to audit log
|
|
18
21
|
* @param {Object} params - Performance logging parameters
|
|
@@ -166,15 +169,20 @@ async function handleNetworkError(error, url, options, duration) {
|
|
|
166
169
|
|
|
167
170
|
/**
|
|
168
171
|
* Make an API call with proper error handling
|
|
172
|
+
* Uses a 15s timeout to avoid hanging when the controller is unreachable.
|
|
169
173
|
* @param {string} url - API endpoint URL
|
|
170
|
-
* @param {Object} options - Fetch options
|
|
174
|
+
* @param {Object} options - Fetch options (signal, method, headers, body, etc.)
|
|
171
175
|
* @returns {Promise<Object>} Response object with success flag
|
|
172
176
|
*/
|
|
173
177
|
async function makeApiCall(url, options = {}) {
|
|
174
178
|
const startTime = Date.now();
|
|
179
|
+
const fetchOptions = { ...options };
|
|
180
|
+
if (!fetchOptions.signal) {
|
|
181
|
+
fetchOptions.signal = AbortSignal.timeout(DEFAULT_REQUEST_TIMEOUT_MS);
|
|
182
|
+
}
|
|
175
183
|
|
|
176
184
|
try {
|
|
177
|
-
const response = await fetch(url,
|
|
185
|
+
const response = await fetch(url, fetchOptions);
|
|
178
186
|
const duration = Date.now() - startTime;
|
|
179
187
|
|
|
180
188
|
if (!response.ok) {
|
|
@@ -182,8 +190,13 @@ async function makeApiCall(url, options = {}) {
|
|
|
182
190
|
}
|
|
183
191
|
|
|
184
192
|
return await handleSuccessResponse(response, url, options, duration);
|
|
185
|
-
} catch (
|
|
193
|
+
} catch (err) {
|
|
186
194
|
const duration = Date.now() - startTime;
|
|
195
|
+
const error = err?.name === 'AbortError'
|
|
196
|
+
? new Error(
|
|
197
|
+
`Request timed out after ${DEFAULT_REQUEST_TIMEOUT_MS / 1000} seconds. The controller may be unreachable. Check the URL and network.`
|
|
198
|
+
)
|
|
199
|
+
: err;
|
|
187
200
|
return await handleNetworkError(error, url, options, duration);
|
|
188
201
|
}
|
|
189
202
|
}
|
|
@@ -214,20 +227,16 @@ function extractControllerUrl(url) {
|
|
|
214
227
|
* @param {string} [tokenOrAuthConfig.controller] - Controller URL for token refresh (if object)
|
|
215
228
|
* @returns {Promise<Object>} Response object
|
|
216
229
|
*/
|
|
230
|
+
// eslint-disable-next-line max-statements
|
|
217
231
|
async function authenticatedApiCall(url, options = {}, tokenOrAuthConfig) {
|
|
218
|
-
|
|
219
|
-
const token =
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const headers = {
|
|
227
|
-
'Content-Type': 'application/json',
|
|
228
|
-
...options.headers
|
|
229
|
-
};
|
|
230
|
-
|
|
232
|
+
const isStringToken = typeof tokenOrAuthConfig === 'string';
|
|
233
|
+
const token = isStringToken ? tokenOrAuthConfig : tokenOrAuthConfig?.token;
|
|
234
|
+
const authControllerUrl = isStringToken ? null : tokenOrAuthConfig?.controller;
|
|
235
|
+
const isFormData = typeof FormData !== 'undefined' && options.body instanceof FormData;
|
|
236
|
+
const headers = { ...options.headers };
|
|
237
|
+
if (!isFormData && !headers['Content-Type']) {
|
|
238
|
+
headers['Content-Type'] = 'application/json';
|
|
239
|
+
}
|
|
231
240
|
if (token) {
|
|
232
241
|
headers['Authorization'] = `Bearer ${token}`;
|
|
233
242
|
}
|
|
@@ -237,46 +246,19 @@ async function authenticatedApiCall(url, options = {}, tokenOrAuthConfig) {
|
|
|
237
246
|
headers
|
|
238
247
|
});
|
|
239
248
|
|
|
240
|
-
// Handle 401 errors with automatic token refresh for device tokens
|
|
241
249
|
if (!response.success && response.status === 401) {
|
|
242
250
|
try {
|
|
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);
|
|
247
|
-
|
|
248
|
-
// Try to force refresh device token on 401 (regardless of local expiry time)
|
|
249
|
-
// because the server rejected the token
|
|
250
251
|
const { forceRefreshDeviceToken } = require('./token-manager');
|
|
251
|
-
const refreshedToken = await forceRefreshDeviceToken(
|
|
252
|
-
|
|
253
|
-
if (refreshedToken && refreshedToken.token) {
|
|
254
|
-
// Retry request with new token
|
|
252
|
+
const refreshedToken = await forceRefreshDeviceToken(authControllerUrl || extractControllerUrl(url));
|
|
253
|
+
if (refreshedToken?.token) {
|
|
255
254
|
headers['Authorization'] = `Bearer ${refreshedToken.token}`;
|
|
256
|
-
|
|
257
|
-
...options,
|
|
258
|
-
headers
|
|
259
|
-
});
|
|
260
|
-
return retryResponse;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Token refresh failed or no refresh token available
|
|
264
|
-
// Return a more helpful error message
|
|
265
|
-
if (!refreshedToken) {
|
|
266
|
-
return {
|
|
267
|
-
...response,
|
|
268
|
-
error: 'Authentication failed: Token expired and refresh failed. Please login again using: aifabrix login',
|
|
269
|
-
formattedError: 'Authentication failed: Token expired and refresh failed. Please login again using: aifabrix login'
|
|
270
|
-
};
|
|
255
|
+
return await makeApiCall(url, { ...options, headers });
|
|
271
256
|
}
|
|
257
|
+
const authError = 'Authentication failed: Token expired and refresh failed. Please login again using: aifabrix login';
|
|
258
|
+
return { ...response, error: authError, formattedError: authError };
|
|
272
259
|
} catch (refreshError) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
return {
|
|
276
|
-
...response,
|
|
277
|
-
error: `Authentication failed: ${errorMessage}. Please login again using: aifabrix login`,
|
|
278
|
-
formattedError: `Authentication failed: ${errorMessage}. Please login again using: aifabrix login`
|
|
279
|
-
};
|
|
260
|
+
const authError = `Authentication failed: ${refreshError.message || String(refreshError)}. Please login again using: aifabrix login`;
|
|
261
|
+
return { ...response, error: authError, formattedError: authError };
|
|
280
262
|
}
|
|
281
263
|
}
|
|
282
264
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application Existence Check Utility
|
|
3
|
+
*
|
|
4
|
+
* Checks if an application exists in an environment before deployment.
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview Application existence checking for AI Fabrix Builder
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { getEnvironmentApplication } = require('../api/environments.api');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Check if application exists in the environment
|
|
15
|
+
* Uses device token auth to check existence (doesn't require app credentials)
|
|
16
|
+
* @async
|
|
17
|
+
* @function checkApplicationExists
|
|
18
|
+
* @param {string} appKey - Application key
|
|
19
|
+
* @param {string} controllerUrl - Controller URL
|
|
20
|
+
* @param {string} envKey - Environment key
|
|
21
|
+
* @param {Object} authConfig - Authentication configuration (device token)
|
|
22
|
+
* @returns {Promise<boolean>} True if application exists, false otherwise
|
|
23
|
+
*/
|
|
24
|
+
async function checkApplicationExists(appKey, controllerUrl, envKey, authConfig) {
|
|
25
|
+
try {
|
|
26
|
+
// Use device token auth (bearer token) to check if app exists
|
|
27
|
+
// This doesn't require app credentials, so it works even if credentials are wrong
|
|
28
|
+
const deviceAuthConfig = { type: 'bearer', token: authConfig.token };
|
|
29
|
+
const response = await getEnvironmentApplication(controllerUrl, envKey, appKey, deviceAuthConfig);
|
|
30
|
+
return response.success && response.data !== null && response.data !== undefined;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
// If 404, application doesn't exist
|
|
33
|
+
if (error.status === 404 || (error.response && error.response.status === 404)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
// For other errors (including 401 if device token is invalid), we can't determine existence
|
|
37
|
+
// Return false to avoid blocking deployment - the validation step will catch credential issues
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { checkApplicationExists };
|
|
@@ -14,6 +14,7 @@ const chalk = require('chalk');
|
|
|
14
14
|
const yaml = require('js-yaml');
|
|
15
15
|
const logger = require('./logger');
|
|
16
16
|
const { detectAppType } = require('./paths');
|
|
17
|
+
const { getContainerPort, getLocalPort } = require('./port-resolver');
|
|
17
18
|
|
|
18
19
|
// createApp is imported dynamically in createMinimalAppIfNeeded to handle test mocking
|
|
19
20
|
|
|
@@ -238,9 +239,11 @@ async function extractExternalAppConfiguration(appKey, variables, appKeyFromFile
|
|
|
238
239
|
function extractWebappConfiguration(variables, appKeyFromFile, displayName, description, options) {
|
|
239
240
|
const appType = variables.build?.language === 'typescript' ? 'webapp' : 'service';
|
|
240
241
|
const registryMode = variables.image?.registryMode || 'external';
|
|
241
|
-
const port =
|
|
242
|
+
const port = options.port ?? getContainerPort(variables, 3000);
|
|
243
|
+
const localPort = getLocalPort(variables, port);
|
|
242
244
|
const language = variables.build?.language || 'typescript';
|
|
243
245
|
const image = buildImageReference(variables, appKeyFromFile);
|
|
246
|
+
const url = variables.app?.url || variables.deployment?.dataplaneUrl || variables.deployment?.appUrl || null;
|
|
244
247
|
|
|
245
248
|
return {
|
|
246
249
|
appKey: appKeyFromFile,
|
|
@@ -249,8 +252,10 @@ function extractWebappConfiguration(variables, appKeyFromFile, displayName, desc
|
|
|
249
252
|
appType,
|
|
250
253
|
registryMode,
|
|
251
254
|
port,
|
|
255
|
+
localPort,
|
|
252
256
|
image,
|
|
253
|
-
language
|
|
257
|
+
language,
|
|
258
|
+
url
|
|
254
259
|
};
|
|
255
260
|
}
|
|
256
261
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication Configuration Validators
|
|
3
|
+
*
|
|
4
|
+
* Provides validation functions for authentication configuration commands
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview Authentication configuration validators
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { getControllerUrlFromLoggedInUser } = require('./controller-url');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Validate controller URL format
|
|
15
|
+
* @function validateControllerUrl
|
|
16
|
+
* @param {string} url - Controller URL to validate
|
|
17
|
+
* @throws {Error} If URL format is invalid
|
|
18
|
+
*/
|
|
19
|
+
function validateControllerUrl(url) {
|
|
20
|
+
if (!url || typeof url !== 'string') {
|
|
21
|
+
throw new Error('Controller URL is required and must be a string');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const trimmed = url.trim();
|
|
25
|
+
if (trimmed.length === 0) {
|
|
26
|
+
throw new Error('Controller URL cannot be empty');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Basic URL validation - must start with http:// or https://
|
|
30
|
+
if (!trimmed.match(/^https?:\/\//)) {
|
|
31
|
+
throw new Error('Controller URL must start with http:// or https://');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
// Use URL constructor for more thorough validation
|
|
36
|
+
new URL(trimmed);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
throw new Error(`Invalid controller URL format: ${error.message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validate environment key format
|
|
44
|
+
* @function validateEnvironment
|
|
45
|
+
* @param {string} env - Environment key to validate
|
|
46
|
+
* @throws {Error} If environment format is invalid
|
|
47
|
+
*/
|
|
48
|
+
function validateEnvironment(env) {
|
|
49
|
+
if (!env || typeof env !== 'string') {
|
|
50
|
+
throw new Error('Environment is required and must be a string');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const trimmed = env.trim();
|
|
54
|
+
if (trimmed.length === 0) {
|
|
55
|
+
throw new Error('Environment cannot be empty');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Environment key must contain only letters, numbers, hyphens, and underscores
|
|
59
|
+
if (!/^[a-z0-9-_]+$/i.test(trimmed)) {
|
|
60
|
+
throw new Error('Environment must contain only letters, numbers, hyphens, and underscores');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if user is logged in to a controller
|
|
66
|
+
* @async
|
|
67
|
+
* @function checkUserLoggedIn
|
|
68
|
+
* @param {string} controllerUrl - Controller URL to check
|
|
69
|
+
* @returns {Promise<boolean>} True if user has device token for this controller
|
|
70
|
+
*/
|
|
71
|
+
async function checkUserLoggedIn(controllerUrl) {
|
|
72
|
+
if (!controllerUrl) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const normalizedUrl = controllerUrl.trim().replace(/\/+$/, '');
|
|
77
|
+
const loggedInControllerUrl = await getControllerUrlFromLoggedInUser();
|
|
78
|
+
|
|
79
|
+
if (!loggedInControllerUrl) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Normalize both URLs for comparison
|
|
84
|
+
const normalizedLoggedIn = loggedInControllerUrl.trim().replace(/\/+$/, '');
|
|
85
|
+
return normalizedLoggedIn === normalizedUrl;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = {
|
|
89
|
+
validateControllerUrl,
|
|
90
|
+
validateEnvironment,
|
|
91
|
+
checkUserLoggedIn
|
|
92
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Header Display Utility
|
|
3
|
+
*
|
|
4
|
+
* Displays active configuration (controller, environment, dataplane) at top of commands
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview Command header display utility
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const chalk = require('chalk');
|
|
12
|
+
const logger = require('./logger');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Display command header with active configuration
|
|
16
|
+
* @function displayCommandHeader
|
|
17
|
+
* @param {string} controllerUrl - Controller URL
|
|
18
|
+
* @param {string} environment - Environment key
|
|
19
|
+
* @param {string} [dataplaneUrl] - Dataplane URL (optional)
|
|
20
|
+
*/
|
|
21
|
+
function displayCommandHeader(controllerUrl, environment, dataplaneUrl) {
|
|
22
|
+
const parts = [];
|
|
23
|
+
|
|
24
|
+
if (controllerUrl) {
|
|
25
|
+
parts.push(`Controller: ${chalk.cyan(controllerUrl)}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (environment) {
|
|
29
|
+
parts.push(`Environment: ${chalk.cyan(environment)}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (dataplaneUrl) {
|
|
33
|
+
parts.push(`Dataplane: ${chalk.cyan(dataplaneUrl)}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (parts.length > 0) {
|
|
37
|
+
logger.log(chalk.gray(`\n${parts.join(' | ')}\n`));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
displayCommandHeader
|
|
43
|
+
};
|