@aifabrix/builder 2.36.0 → 2.36.2
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/integration/hubspot/README.md +47 -0
- package/integration/hubspot/env.template +6 -1
- package/integration/hubspot/hubspot-deploy.json +0 -6
- package/integration/hubspot/hubspot-system.json +0 -6
- package/integration/hubspot/test-artifacts/wizard-hubspot-env-vars.yaml +1 -1
- package/integration/hubspot/test-dataplane-down-helpers.js +4 -1
- package/integration/hubspot/test-dataplane-down-tests.js +5 -33
- package/integration/hubspot/test-dataplane-down.js +60 -8
- package/integration/hubspot/test.js +54 -20
- package/integration/hubspot/wizard-hubspot-e2e.yaml +1 -1
- package/lib/api/pipeline.api.js +37 -6
- package/lib/app/config.js +0 -1
- package/lib/app/list.js +1 -1
- package/lib/app/show-display.js +27 -1
- package/lib/commands/app.js +15 -0
- package/lib/commands/auth-status.js +39 -9
- package/lib/commands/wizard-helpers.js +13 -4
- package/lib/commands/wizard.js +1 -0
- package/lib/generator/wizard-prompts.js +13 -1
- package/lib/schema/env-config.yaml +1 -3
- package/lib/schema/external-system.schema.json +24 -1
- package/lib/utils/token-manager.js +32 -25
- package/package.json +1 -1
- package/integration/test-hubspot/wizard.yaml +0 -8
|
@@ -128,9 +128,56 @@ aifabrix datasource list
|
|
|
128
128
|
aifabrix datasource validate hubspot-company
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
+
## Wizard E2E tests and use case coverage
|
|
132
|
+
|
|
133
|
+
The script `integration/hubspot/test.js` runs end-to-end wizard and validation tests. It covers all wizard use cases from [Wizard Guide](../../docs/wizard.md):
|
|
134
|
+
|
|
135
|
+
| Use case | Test ID | Description |
|
|
136
|
+
|----------|---------|-------------|
|
|
137
|
+
| **Headless config (wizard.yaml)** | | |
|
|
138
|
+
| Required `appName` | 2.1 | Rejects config missing `appName` |
|
|
139
|
+
| Valid `appName` pattern (lowercase, hyphens/underscores) | 2.2 | Rejects uppercase in app name |
|
|
140
|
+
| Required `mode` | 2.5 | Rejects invalid `mode` enum |
|
|
141
|
+
| Required `source` block | 2.3 | Rejects config without `source` |
|
|
142
|
+
| Valid `source.type` | 2.4 | Rejects invalid source type |
|
|
143
|
+
| `openapi-file` requires `filePath` | 2.7 | Rejects missing/non-existent OpenAPI file |
|
|
144
|
+
| `openapi-url` requires `url` | 2.8 | Rejects `openapi-url` without `url` |
|
|
145
|
+
| `known-platform` requires `platform` | 2.6 | Rejects known-platform without `platform` |
|
|
146
|
+
| **Mode: add-datasource** | | |
|
|
147
|
+
| `systemIdOrKey` required when mode=add-datasource | 2.9 | Rejects add-datasource without systemIdOrKey |
|
|
148
|
+
| **Credential** | | |
|
|
149
|
+
| `credential.action=select` requires `credentialIdOrKey` | 2.10 | Rejects select without credentialIdOrKey |
|
|
150
|
+
| `credential.action=create` requires `config` | 2.11 | Rejects create without config |
|
|
151
|
+
| **Positive flows** | | |
|
|
152
|
+
| Full wizard with OpenAPI file | 1.1 | Complete flow with local OpenAPI file |
|
|
153
|
+
| Wizard with known platform | 1.2 | Flow using known-platform (e.g. HubSpot) |
|
|
154
|
+
| Wizard with env var substitution in deployment | 1.6 | `${CONTROLLER_URL}`, `${DATAPLANE_URL}` in wizard.yaml |
|
|
155
|
+
| Real credential creation (real-data) | 1.3 | Credential create with real OAuth2 (optional env) |
|
|
156
|
+
| **Post-wizard validation (external system)** | | |
|
|
157
|
+
| RBAC: permissions reference existing roles | 2.12 | Rejects permission referencing non-existent role |
|
|
158
|
+
| RBAC: rbac.yaml valid YAML and structure | 2.13 | Rejects invalid YAML in rbac.yaml |
|
|
159
|
+
| Datasource: dimensions required in fieldMappings | 2.14 | Rejects missing dimensions |
|
|
160
|
+
| Datasource: dimension key pattern | 2.15 | Rejects invalid dimension key |
|
|
161
|
+
| Datasource: attribute path pattern | 2.16 | Rejects invalid attribute path |
|
|
162
|
+
| Datasource: dimensions must be object | 2.17 | Rejects dimensions as array |
|
|
163
|
+
|
|
164
|
+
**Run tests:**
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
# All tests (positive may skip if dataplane/controller unavailable)
|
|
168
|
+
node integration/hubspot/test.js
|
|
169
|
+
|
|
170
|
+
# Negative only (no dataplane required)
|
|
171
|
+
node integration/hubspot/test.js --type negative
|
|
172
|
+
|
|
173
|
+
# Specific test
|
|
174
|
+
node integration/hubspot/test.js --test "2.1,2.2"
|
|
175
|
+
```
|
|
176
|
+
|
|
131
177
|
## Documentation
|
|
132
178
|
|
|
133
179
|
- [External Systems Guide](../../docs/external-systems.md) - Complete guide with examples
|
|
180
|
+
- [Wizard Guide](../../docs/wizard.md) - Wizard workflow and headless config
|
|
134
181
|
- [CLI Reference](../../docs/cli-reference.md) - All commands
|
|
135
182
|
- [Configuration Reference](../../docs/CONFIGURATION.md) - Config file details
|
|
136
183
|
|
|
@@ -5,5 +5,10 @@
|
|
|
5
5
|
CLIENTID=kv://hubspot-clientidKeyVault
|
|
6
6
|
CLIENTSECRET=kv://hubspot-clientsecretKeyVault
|
|
7
7
|
TOKENURL=https://api.hubapi.com/oauth/v1/token
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
# Optional: test runner (integration/hubspot/test.js) reads all config from this .env
|
|
10
|
+
# CONTROLLER_URL=http://localhost:3110
|
|
11
|
+
# ENVIRONMENT=miso
|
|
12
|
+
# DATAPLANE_URL=
|
|
13
|
+
# HUBSPOT_OPENAPI_FILE=integration/hubspot/companies.json
|
|
9
14
|
|
|
@@ -217,7 +217,10 @@ function validateWizardResult(result, output) {
|
|
|
217
217
|
'fetch failed',
|
|
218
218
|
'timeout',
|
|
219
219
|
'unreachable',
|
|
220
|
-
'failed to create wizard session'
|
|
220
|
+
'failed to create wizard session',
|
|
221
|
+
'authentication failed',
|
|
222
|
+
'no authentication method',
|
|
223
|
+
'client token unavailable'
|
|
221
224
|
];
|
|
222
225
|
|
|
223
226
|
const isValid = !result.success && validateError(output, expectedPatterns);
|
|
@@ -65,13 +65,7 @@ async function testWizard() {
|
|
|
65
65
|
'bin/aifabrix.js',
|
|
66
66
|
'wizard',
|
|
67
67
|
'--config',
|
|
68
|
-
configPath
|
|
69
|
-
'--controller',
|
|
70
|
-
CONTROLLER_URL,
|
|
71
|
-
'--environment',
|
|
72
|
-
ENVIRONMENT,
|
|
73
|
-
'--dataplane',
|
|
74
|
-
INVALID_DATAPLANE_URL
|
|
68
|
+
configPath
|
|
75
69
|
];
|
|
76
70
|
|
|
77
71
|
const result = await runCommand('node', args);
|
|
@@ -107,11 +101,7 @@ async function testDownload() {
|
|
|
107
101
|
const args = [
|
|
108
102
|
'bin/aifabrix.js',
|
|
109
103
|
'download',
|
|
110
|
-
'non-existent-system-that-should-fail'
|
|
111
|
-
'--environment',
|
|
112
|
-
ENVIRONMENT,
|
|
113
|
-
'--controller',
|
|
114
|
-
CONTROLLER_URL
|
|
104
|
+
'non-existent-system-that-should-fail'
|
|
115
105
|
];
|
|
116
106
|
|
|
117
107
|
const result = await runCommand('node', args);
|
|
@@ -155,10 +145,6 @@ async function testDelete() {
|
|
|
155
145
|
'non-existent-system-that-should-fail',
|
|
156
146
|
'--type',
|
|
157
147
|
'external',
|
|
158
|
-
'--environment',
|
|
159
|
-
ENVIRONMENT,
|
|
160
|
-
'--controller',
|
|
161
|
-
CONTROLLER_URL,
|
|
162
148
|
'--yes'
|
|
163
149
|
];
|
|
164
150
|
|
|
@@ -219,13 +205,7 @@ function buildDatasourceDeployArgs(datasourcePath) {
|
|
|
219
205
|
'datasource',
|
|
220
206
|
'deploy',
|
|
221
207
|
'test-app',
|
|
222
|
-
datasourcePath
|
|
223
|
-
'--environment',
|
|
224
|
-
ENVIRONMENT,
|
|
225
|
-
'--controller',
|
|
226
|
-
CONTROLLER_URL,
|
|
227
|
-
'--dataplane',
|
|
228
|
-
INVALID_DATAPLANE_URL
|
|
208
|
+
datasourcePath
|
|
229
209
|
];
|
|
230
210
|
}
|
|
231
211
|
|
|
@@ -321,11 +301,7 @@ async function testIntegration() {
|
|
|
321
301
|
const args = [
|
|
322
302
|
'bin/aifabrix.js',
|
|
323
303
|
'test-integration',
|
|
324
|
-
testApp
|
|
325
|
-
'--environment',
|
|
326
|
-
ENVIRONMENT,
|
|
327
|
-
'--controller',
|
|
328
|
-
CONTROLLER_URL
|
|
304
|
+
testApp
|
|
329
305
|
];
|
|
330
306
|
|
|
331
307
|
const result = await runCommand('node', args);
|
|
@@ -372,11 +348,7 @@ async function testDataplaneDiscovery() {
|
|
|
372
348
|
const args = [
|
|
373
349
|
'bin/aifabrix.js',
|
|
374
350
|
'download',
|
|
375
|
-
'non-existent-system-for-discovery-test'
|
|
376
|
-
'--environment',
|
|
377
|
-
ENVIRONMENT,
|
|
378
|
-
'--controller',
|
|
379
|
-
CONTROLLER_URL
|
|
351
|
+
'non-existent-system-for-discovery-test'
|
|
380
352
|
];
|
|
381
353
|
|
|
382
354
|
const result = await runCommand('node', args);
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
*/
|
|
12
12
|
'use strict';
|
|
13
13
|
|
|
14
|
+
const fs = require('fs').promises;
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const os = require('os');
|
|
14
17
|
const {
|
|
15
18
|
logInfo,
|
|
16
19
|
logSuccess,
|
|
@@ -27,9 +30,51 @@ const {
|
|
|
27
30
|
} = require('./test-dataplane-down-tests');
|
|
28
31
|
|
|
29
32
|
const INVALID_DATAPLANE_URL = 'http://localhost:9999';
|
|
30
|
-
const CONTROLLER_URL = process.env.CONTROLLER_URL || 'http://localhost:3110';
|
|
31
33
|
const ENVIRONMENT = process.env.ENVIRONMENT || 'miso';
|
|
32
34
|
|
|
35
|
+
/** Path to temp config used so CLI uses invalid controller URL and fails with connection error */
|
|
36
|
+
let tempConfigPath = null;
|
|
37
|
+
let originalAifabrixConfig = null;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create temp config pointing controller to invalid URL so commands fail with connection error
|
|
41
|
+
* @async
|
|
42
|
+
* @returns {Promise<string>} Path to temp config file
|
|
43
|
+
*/
|
|
44
|
+
async function createTempConfig() {
|
|
45
|
+
const dir = path.join(os.tmpdir(), `aifabrix-dataplane-down-${Date.now()}`);
|
|
46
|
+
await fs.mkdir(dir, { recursive: true });
|
|
47
|
+
const configPath = path.join(dir, 'config.yaml');
|
|
48
|
+
const content = `controller: "${INVALID_DATAPLANE_URL}"
|
|
49
|
+
environment: "${ENVIRONMENT}"
|
|
50
|
+
`;
|
|
51
|
+
await fs.writeFile(configPath, content, 'utf8');
|
|
52
|
+
return configPath;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Restore AIFABRIX_CONFIG and remove temp config
|
|
57
|
+
* @async
|
|
58
|
+
* @returns {Promise<void>} Resolves when cleanup is complete
|
|
59
|
+
*/
|
|
60
|
+
async function cleanupTempConfig() {
|
|
61
|
+
if (originalAifabrixConfig !== undefined) {
|
|
62
|
+
if (originalAifabrixConfig === null) {
|
|
63
|
+
delete process.env.AIFABRIX_CONFIG;
|
|
64
|
+
} else {
|
|
65
|
+
process.env.AIFABRIX_CONFIG = originalAifabrixConfig;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (tempConfigPath) {
|
|
69
|
+
try {
|
|
70
|
+
await fs.rm(path.dirname(tempConfigPath), { recursive: true, force: true }).catch(() => {});
|
|
71
|
+
} catch {
|
|
72
|
+
// Ignore cleanup errors
|
|
73
|
+
}
|
|
74
|
+
tempConfigPath = null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
33
78
|
/**
|
|
34
79
|
* Displays failed test details
|
|
35
80
|
* @function displayFailedTestDetails
|
|
@@ -137,17 +182,24 @@ async function main() {
|
|
|
137
182
|
logInfo('='.repeat(60));
|
|
138
183
|
logInfo('Dataplane Down Error Handling Test Suite');
|
|
139
184
|
logInfo('='.repeat(60));
|
|
140
|
-
logInfo(`Invalid
|
|
141
|
-
logInfo(`Controller URL: ${CONTROLLER_URL}`);
|
|
185
|
+
logInfo(`Invalid Controller URL (used for all commands): ${INVALID_DATAPLANE_URL}`);
|
|
142
186
|
logInfo(`Environment: ${ENVIRONMENT}`);
|
|
143
187
|
logInfo('='.repeat(60));
|
|
144
188
|
|
|
145
|
-
|
|
146
|
-
|
|
189
|
+
try {
|
|
190
|
+
tempConfigPath = await createTempConfig();
|
|
191
|
+
originalAifabrixConfig = process.env.AIFABRIX_CONFIG || null;
|
|
192
|
+
process.env.AIFABRIX_CONFIG = tempConfigPath;
|
|
147
193
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
194
|
+
const results = await runTests();
|
|
195
|
+
displaySummary(results);
|
|
196
|
+
|
|
197
|
+
const failed = results.filter(r => !r.success).length;
|
|
198
|
+
if (failed > 0) {
|
|
199
|
+
process.exitCode = 1;
|
|
200
|
+
}
|
|
201
|
+
} finally {
|
|
202
|
+
await cleanupTempConfig();
|
|
151
203
|
}
|
|
152
204
|
}
|
|
153
205
|
|
|
@@ -18,19 +18,14 @@ const yaml = require('js-yaml');
|
|
|
18
18
|
const chalk = require('chalk');
|
|
19
19
|
const { getDeploymentAuth } = require('../../lib/utils/token-manager');
|
|
20
20
|
const { discoverDataplaneUrl } = require('../../lib/commands/wizard-dataplane');
|
|
21
|
+
const { resolveControllerUrl } = require('../../lib/utils/controller-url');
|
|
22
|
+
const { resolveEnvironment } = require('../../lib/core/config');
|
|
21
23
|
|
|
22
24
|
const execFileAsync = promisify(execFile);
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
const DEFAULT_ENVIRONMENT = process.env.ENVIRONMENT || 'miso';
|
|
26
|
-
const DEFAULT_DATAPLANE_URL = process.env.DATAPLANE_URL || '';
|
|
27
|
-
const DEFAULT_OPENAPI_FILE = process.env.HUBSPOT_OPENAPI_FILE ||
|
|
28
|
-
'/workspace/aifabrix-dataplane/data/hubspot/openapi/companies.json';
|
|
29
|
-
const LOCAL_ENV_PATH = path.join(process.cwd(), 'integration', 'hubspot', '.env');
|
|
30
|
-
const DEFAULT_ENV_PATH = process.env.HUBSPOT_ENV_PATH ||
|
|
31
|
-
(fsSync.existsSync(LOCAL_ENV_PATH) ? LOCAL_ENV_PATH : '/workspace/aifabrix-dataplane/data/hubspot/.env');
|
|
32
|
-
|
|
26
|
+
/** Single source for test config: integration/hubspot/.env */
|
|
33
27
|
const HUBSPOT_DIR = path.join(process.cwd(), 'integration', 'hubspot');
|
|
28
|
+
const LOCAL_ENV_PATH = path.join(HUBSPOT_DIR, '.env');
|
|
34
29
|
const ARTIFACT_DIR = path.join(HUBSPOT_DIR, 'test-artifacts');
|
|
35
30
|
const MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
|
|
36
31
|
const COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
|
|
@@ -271,11 +266,54 @@ async function loadEnvFile(envPath, options) {
|
|
|
271
266
|
process.env[key] = value;
|
|
272
267
|
}
|
|
273
268
|
}
|
|
269
|
+
// Map common .env keys so tests and wizard use credentials from integration/hubspot/.env
|
|
270
|
+
if (process.env.CLIENTID && process.env.HUBSPOT_CLIENT_ID === undefined) {
|
|
271
|
+
process.env.HUBSPOT_CLIENT_ID = process.env.CLIENTID;
|
|
272
|
+
}
|
|
273
|
+
if (process.env.CLIENTSECRET && process.env.HUBSPOT_CLIENT_SECRET === undefined) {
|
|
274
|
+
process.env.HUBSPOT_CLIENT_SECRET = process.env.CLIENTSECRET;
|
|
275
|
+
}
|
|
274
276
|
if (options.verbose) {
|
|
275
277
|
logInfo(`Loaded env vars from: ${envPath}`);
|
|
276
278
|
}
|
|
277
279
|
}
|
|
278
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Load test config (controller, environment, dataplane, openapi file).
|
|
283
|
+
* Reads integration/hubspot/.env; missing CONTROLLER_URL/ENVIRONMENT fall back to
|
|
284
|
+
* the same resolution as the CLI (aifx auth status) so tests use the same controller.
|
|
285
|
+
* @async
|
|
286
|
+
* @function loadTestConfigFromEnv
|
|
287
|
+
* @returns {Promise<Object>} Context with controllerUrl, environment, dataplaneUrl, openapiFile
|
|
288
|
+
*/
|
|
289
|
+
async function loadTestConfigFromEnv() {
|
|
290
|
+
const defaults = {
|
|
291
|
+
DATAPLANE_URL: '',
|
|
292
|
+
HUBSPOT_OPENAPI_FILE: path.join(HUBSPOT_DIR, 'companies.json')
|
|
293
|
+
};
|
|
294
|
+
let parsed = {};
|
|
295
|
+
try {
|
|
296
|
+
if (fsSync.existsSync(LOCAL_ENV_PATH)) {
|
|
297
|
+
const content = await fs.readFile(LOCAL_ENV_PATH, 'utf8');
|
|
298
|
+
parsed = parseEnvFile(content);
|
|
299
|
+
}
|
|
300
|
+
} catch (error) {
|
|
301
|
+
logWarn(`Could not read ${LOCAL_ENV_PATH}: ${error.message}`);
|
|
302
|
+
}
|
|
303
|
+
const controllerUrl = parsed.CONTROLLER_URL
|
|
304
|
+
? parsed.CONTROLLER_URL.trim()
|
|
305
|
+
: await resolveControllerUrl();
|
|
306
|
+
const environment = parsed.ENVIRONMENT
|
|
307
|
+
? parsed.ENVIRONMENT.trim()
|
|
308
|
+
: await resolveEnvironment();
|
|
309
|
+
return {
|
|
310
|
+
controllerUrl,
|
|
311
|
+
environment,
|
|
312
|
+
dataplaneUrl: parsed.DATAPLANE_URL || defaults.DATAPLANE_URL,
|
|
313
|
+
openapiFile: parsed.HUBSPOT_OPENAPI_FILE || defaults.HUBSPOT_OPENAPI_FILE
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
279
317
|
/**
|
|
280
318
|
* Ensure dataplane URL is available for tests
|
|
281
319
|
* @async
|
|
@@ -392,7 +430,8 @@ async function validateAuth(context, options) {
|
|
|
392
430
|
throw new Error('Authentication check failed. Run: node bin/aifabrix.js login --controller <url> --method device');
|
|
393
431
|
}
|
|
394
432
|
const output = `${result.stdout}\n${result.stderr}`;
|
|
395
|
-
|
|
433
|
+
// Only treat "Not authenticated" as failure; "✗ Not reachable" (dataplane) is not auth failure
|
|
434
|
+
if (output.includes('Not authenticated')) {
|
|
396
435
|
throw new Error('Not authenticated. Run: node bin/aifabrix.js login --controller <url> --method device');
|
|
397
436
|
}
|
|
398
437
|
logSuccess('Authentication validated.');
|
|
@@ -689,7 +728,7 @@ async function runCommandWithErrorHandling(command, args, options, appName) {
|
|
|
689
728
|
const errorOutput = `${result.stdout}\n${result.stderr}`;
|
|
690
729
|
if (appName && (errorOutput.includes('not found') ||
|
|
691
730
|
(errorOutput.includes('External system') && errorOutput.includes('not found')))) {
|
|
692
|
-
|
|
731
|
+
logInfo(`System ${appName} not deployed on dataplane; download step omitted (optional).`);
|
|
693
732
|
return { skipped: true };
|
|
694
733
|
}
|
|
695
734
|
throw new Error(`${command} failed: ${result.stderr || result.stdout}`);
|
|
@@ -1216,7 +1255,7 @@ async function corruptSystemFileWithInvalidRole(appPath) {
|
|
|
1216
1255
|
* @returns {Promise<void>} Resolves when file is corrupted
|
|
1217
1256
|
*/
|
|
1218
1257
|
async function corruptRbacFile(appPath) {
|
|
1219
|
-
const rbacPath = path.join(appPath, 'rbac.
|
|
1258
|
+
const rbacPath = path.join(appPath, 'rbac.yaml');
|
|
1220
1259
|
await fs.writeFile(rbacPath, 'invalid: yaml: syntax: [', 'utf8');
|
|
1221
1260
|
}
|
|
1222
1261
|
|
|
@@ -1339,7 +1378,7 @@ function buildNegativeRbacTestCases(context) {
|
|
|
1339
1378
|
const appName = 'hubspot-test-negative-rbac-invalid-yaml';
|
|
1340
1379
|
const appPath = await createSystemForNegativeTest(appName, 'wizard-valid-for-rbac-yaml-test', context, options);
|
|
1341
1380
|
await corruptRbacFile(appPath);
|
|
1342
|
-
await runValidationExpectFailure(appName, context, options, '
|
|
1381
|
+
await runValidationExpectFailure(appName, context, options, 'Invalid YAML syntax in rbac.yaml');
|
|
1343
1382
|
await cleanupAppArtifacts(appName, options);
|
|
1344
1383
|
}
|
|
1345
1384
|
}
|
|
@@ -1485,13 +1524,8 @@ async function main() {
|
|
|
1485
1524
|
return;
|
|
1486
1525
|
}
|
|
1487
1526
|
await ensureDir(ARTIFACT_DIR);
|
|
1488
|
-
await loadEnvFile(
|
|
1489
|
-
const context =
|
|
1490
|
-
controllerUrl: DEFAULT_CONTROLLER_URL,
|
|
1491
|
-
environment: DEFAULT_ENVIRONMENT,
|
|
1492
|
-
dataplaneUrl: DEFAULT_DATAPLANE_URL,
|
|
1493
|
-
openapiFile: DEFAULT_OPENAPI_FILE
|
|
1494
|
-
};
|
|
1527
|
+
await loadEnvFile(LOCAL_ENV_PATH, args);
|
|
1528
|
+
const context = await loadTestConfigFromEnv();
|
|
1495
1529
|
setupTestContext(context);
|
|
1496
1530
|
await validateAuth(context, args);
|
|
1497
1531
|
const testCases = buildTestCases(context).filter(testCase => (
|
package/lib/api/pipeline.api.js
CHANGED
|
@@ -81,9 +81,33 @@ async function getPipelineHealth(controllerUrl, envKey) {
|
|
|
81
81
|
return await client.get(`/api/v1/pipeline/${envKey}/health`);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Publish one external system via dataplane pipeline endpoint
|
|
86
|
+
* POST /api/v1/pipeline/publish
|
|
87
|
+
* Request body: external system JSON (external-system.schema.json). Optional field in body:
|
|
88
|
+
* generateMcpContract (boolean, default true). Optional: generateOpenApiContract (boolean).
|
|
89
|
+
* Do not use query parameters for MCP; use the field in the body only.
|
|
90
|
+
*
|
|
91
|
+
* @async
|
|
92
|
+
* @function publishSystemViaPipeline
|
|
93
|
+
* @param {string} dataplaneUrl - Dataplane base URL
|
|
94
|
+
* @param {Object} authConfig - Authentication configuration
|
|
95
|
+
* @param {Object} systemConfig - External system configuration (conforms to external-system.schema.json)
|
|
96
|
+
* @returns {Promise<Object>} Published external system response
|
|
97
|
+
* @throws {Error} If publish fails
|
|
98
|
+
*/
|
|
99
|
+
async function publishSystemViaPipeline(dataplaneUrl, authConfig, systemConfig) {
|
|
100
|
+
const client = new ApiClient(dataplaneUrl, authConfig);
|
|
101
|
+
return await client.post('/api/v1/pipeline/publish', {
|
|
102
|
+
body: systemConfig
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
84
106
|
/**
|
|
85
107
|
* Publish datasource via dataplane pipeline endpoint
|
|
86
108
|
* POST /api/v1/pipeline/{systemKey}/publish
|
|
109
|
+
* No generateMcpContract for this endpoint; dataplane always uses default (MCP generated).
|
|
110
|
+
*
|
|
87
111
|
* @async
|
|
88
112
|
* @function publishDatasourceViaPipeline
|
|
89
113
|
* @param {string} dataplaneUrl - Dataplane base URL
|
|
@@ -169,11 +193,15 @@ async function deployDatasourceViaPipeline(dataplaneUrl, systemKey, authConfig,
|
|
|
169
193
|
/**
|
|
170
194
|
* Upload application configuration via dataplane pipeline endpoint
|
|
171
195
|
* POST /api/v1/pipeline/upload
|
|
196
|
+
* Body: { version, application, dataSources }. Include application.generateMcpContract
|
|
197
|
+
* and/or application.generateOpenApiContract to control contract generation when
|
|
198
|
+
* publishing this upload (publish reads from stored config; no query param on publish).
|
|
199
|
+
*
|
|
172
200
|
* @async
|
|
173
201
|
* @function uploadApplicationViaPipeline
|
|
174
202
|
* @param {string} dataplaneUrl - Dataplane base URL
|
|
175
203
|
* @param {Object} authConfig - Authentication configuration
|
|
176
|
-
* @param {Object} applicationSchema -
|
|
204
|
+
* @param {Object} applicationSchema - { version, application, dataSources }; application may include generateMcpContract, generateOpenApiContract
|
|
177
205
|
* @returns {Promise<Object>} Upload response with uploadId
|
|
178
206
|
* @throws {Error} If upload fails
|
|
179
207
|
*/
|
|
@@ -203,20 +231,22 @@ async function validateUploadViaPipeline(dataplaneUrl, uploadId, authConfig) {
|
|
|
203
231
|
/**
|
|
204
232
|
* Publish upload via dataplane pipeline endpoint
|
|
205
233
|
* POST /api/v1/pipeline/upload/{uploadId}/publish
|
|
234
|
+
* No body or query parameters. MCP/OpenAPI generation is taken from the application config
|
|
235
|
+
* that was uploaded (application.generateMcpContract, application.generateOpenApiContract).
|
|
236
|
+
* To control MCP/OpenAPI, include those fields in the application object when calling
|
|
237
|
+
* uploadApplicationViaPipeline.
|
|
238
|
+
*
|
|
206
239
|
* @async
|
|
207
240
|
* @function publishUploadViaPipeline
|
|
208
241
|
* @param {string} dataplaneUrl - Dataplane base URL
|
|
209
242
|
* @param {string} uploadId - Upload ID
|
|
210
243
|
* @param {Object} authConfig - Authentication configuration
|
|
211
|
-
* @param {Object} [options] - Publish options
|
|
212
|
-
* @param {boolean} [options.generateMcpContract] - Generate MCP contract (default: true)
|
|
213
244
|
* @returns {Promise<Object>} Publish response
|
|
214
245
|
* @throws {Error} If publish fails
|
|
215
246
|
*/
|
|
216
|
-
async function publishUploadViaPipeline(dataplaneUrl, uploadId, authConfig
|
|
247
|
+
async function publishUploadViaPipeline(dataplaneUrl, uploadId, authConfig) {
|
|
217
248
|
const client = new ApiClient(dataplaneUrl, authConfig);
|
|
218
|
-
|
|
219
|
-
return await client.post(`/api/v1/pipeline/upload/${uploadId}/publish?generateMcpContract=${generateMcpContract}`);
|
|
249
|
+
return await client.post(`/api/v1/pipeline/upload/${uploadId}/publish`);
|
|
220
250
|
}
|
|
221
251
|
|
|
222
252
|
module.exports = {
|
|
@@ -224,6 +254,7 @@ module.exports = {
|
|
|
224
254
|
deployPipeline,
|
|
225
255
|
getPipelineDeployment,
|
|
226
256
|
getPipelineHealth,
|
|
257
|
+
publishSystemViaPipeline,
|
|
227
258
|
publishDatasourceViaPipeline,
|
|
228
259
|
testDatasourceViaPipeline,
|
|
229
260
|
deployExternalSystemViaPipeline,
|
package/lib/app/config.js
CHANGED
|
@@ -66,7 +66,6 @@ function generateExternalSystemEnvTemplate(config, appName) {
|
|
|
66
66
|
lines.push('CLIENTID=kv://' + systemKey + '-clientidKeyVault');
|
|
67
67
|
lines.push('CLIENTSECRET=kv://' + systemKey + '-clientsecretKeyVault');
|
|
68
68
|
lines.push('TOKENURL=https://api.example.com/oauth/token');
|
|
69
|
-
lines.push('REDIRECT_URI=kv://' + systemKey + '-redirect-uriKeyVault');
|
|
70
69
|
} else if (authType === 'apikey') {
|
|
71
70
|
lines.push('API_KEY=kv://' + systemKey + '-api-keyKeyVault');
|
|
72
71
|
} else if (authType === 'basic') {
|
package/lib/app/list.js
CHANGED
|
@@ -168,7 +168,7 @@ function displayApplications(applications, environment, controllerUrl) {
|
|
|
168
168
|
const urlAndPort = formatUrlAndPort(app);
|
|
169
169
|
logger.log(`${hasPipeline} ${chalk.cyan(app.key)} - ${app.displayName} (${app.status || 'unknown'})${urlAndPort}`);
|
|
170
170
|
});
|
|
171
|
-
logger.log('');
|
|
171
|
+
logger.log(chalk.gray(' To show details for an app: aifabrix app show <appKey>\n'));
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
/**
|
package/lib/app/show-display.js
CHANGED
|
@@ -51,7 +51,7 @@ function logApplicationExternalIntegration(ei) {
|
|
|
51
51
|
logger.log(` schemaBasePath: ${ei.schemaBasePath}`);
|
|
52
52
|
logger.log(` systems: [${(ei.systems || []).join(', ')}]`);
|
|
53
53
|
logger.log(` dataSources: [${(ei.dataSources || []).join(', ')}]`);
|
|
54
|
-
logger.log(chalk.gray('\n For external system data as on dataplane, run
|
|
54
|
+
logger.log(chalk.gray('\n For external system data as on dataplane, run: aifabrix show <appKey> --online or aifabrix app show <appKey>.'));
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
function logApplicationSection(a, isExternal) {
|
|
@@ -130,6 +130,31 @@ function logExternalSystemDataSources(dataSources) {
|
|
|
130
130
|
});
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Log OpenAPI and MCP documentation links for external system (dataplane endpoints).
|
|
135
|
+
* REST: /api/v1/rest/{systemKey}/docs; MCP: /api/v1/mcp/{systemKey}/{resourceType}/docs per dataSource.
|
|
136
|
+
* @param {Object} ext - External system result (dataplaneUrl, systemKey, dataSources, openapiFiles, openapiEndpoints)
|
|
137
|
+
*/
|
|
138
|
+
function logExternalSystemServiceLinks(ext) {
|
|
139
|
+
if (!ext || !ext.dataplaneUrl || !ext.systemKey) return;
|
|
140
|
+
const base = ext.dataplaneUrl.replace(/\/$/, '');
|
|
141
|
+
const sk = ext.systemKey;
|
|
142
|
+
const hasOpenApi = (ext.openapiFiles && ext.openapiFiles.length > 0) ||
|
|
143
|
+
(ext.openapiEndpoints && ext.openapiEndpoints.length > 0);
|
|
144
|
+
const dataSources = ext.dataSources || [];
|
|
145
|
+
const hasMcp = dataSources.length > 0;
|
|
146
|
+
if (!hasOpenApi && !hasMcp) return;
|
|
147
|
+
|
|
148
|
+
logger.log(' Service links:');
|
|
149
|
+
logger.log(` REST OpenAPI: ${base}/api/v1/rest/${sk}/docs`);
|
|
150
|
+
if (hasMcp) {
|
|
151
|
+
dataSources.forEach((ds) => {
|
|
152
|
+
const resourceType = ds.key || ds.systemKey || sk;
|
|
153
|
+
logger.log(` MCP ${resourceType}: ${base}/api/v1/mcp/${sk}/${resourceType}/docs`);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
133
158
|
function logExternalSystemApplication(ap) {
|
|
134
159
|
if (!ap) return;
|
|
135
160
|
logger.log(' Application (from dataplane):');
|
|
@@ -155,6 +180,7 @@ function logExternalSystemSection(ext) {
|
|
|
155
180
|
const sample = ext.openapiEndpoints.slice(0, 3).map((e) => `${e.method || 'GET'} ${e.path || e.pathPattern || e}`).join(', ');
|
|
156
181
|
logger.log(` OpenAPI endpoints: ${ext.openapiEndpoints.length} (e.g. ${sample}${ext.openapiEndpoints.length > 3 ? ' …' : ''})`);
|
|
157
182
|
}
|
|
183
|
+
logExternalSystemServiceLinks(ext);
|
|
158
184
|
}
|
|
159
185
|
|
|
160
186
|
/**
|
package/lib/commands/app.js
CHANGED
|
@@ -14,6 +14,7 @@ const logger = require('../utils/logger');
|
|
|
14
14
|
const { listApplications } = require('../app/list');
|
|
15
15
|
const { registerApplication } = require('../app/register');
|
|
16
16
|
const { rotateSecret } = require('../app/rotate-secret');
|
|
17
|
+
const { showApp } = require('../app/show');
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Setup application management commands
|
|
@@ -66,6 +67,20 @@ function setupAppCommands(program) {
|
|
|
66
67
|
process.exit(1);
|
|
67
68
|
}
|
|
68
69
|
});
|
|
70
|
+
|
|
71
|
+
// Show command: show application from controller (online). Same as aifabrix show <appKey> --online.
|
|
72
|
+
app
|
|
73
|
+
.command('show <appKey>')
|
|
74
|
+
.description('Show application from controller (online). Same as aifabrix show <appKey> --online')
|
|
75
|
+
.option('--json', 'Output as JSON')
|
|
76
|
+
.action(async(appKey, options) => {
|
|
77
|
+
try {
|
|
78
|
+
await showApp(appKey, { online: true, json: !!options.json });
|
|
79
|
+
} catch (error) {
|
|
80
|
+
logger.error(chalk.red(`Error: ${error.message}`));
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
69
84
|
}
|
|
70
85
|
|
|
71
86
|
module.exports = { setupAppCommands };
|
|
@@ -197,15 +197,15 @@ function displayTokenInfo(tokenInfo) {
|
|
|
197
197
|
const statusIcon = tokenInfo.authenticated ? chalk.green('✓') : chalk.red('✗');
|
|
198
198
|
const statusText = tokenInfo.authenticated ? 'Authenticated' : 'Not authenticated';
|
|
199
199
|
|
|
200
|
-
logger.log(`Status: ${statusIcon} ${statusText}`);
|
|
201
|
-
logger.log(`Token Type: ${chalk.cyan(tokenInfo.type)}`);
|
|
200
|
+
logger.log(` Status: ${statusIcon} ${statusText}`);
|
|
201
|
+
logger.log(` Token Type: ${chalk.cyan(tokenInfo.type)}`);
|
|
202
202
|
|
|
203
203
|
if (tokenInfo.appName) {
|
|
204
|
-
logger.log(`Application: ${chalk.cyan(tokenInfo.appName)}`);
|
|
204
|
+
logger.log(` Application: ${chalk.cyan(tokenInfo.appName)}`);
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
if (tokenInfo.expiresAt) {
|
|
208
|
-
logger.log(`Expires: ${chalk.gray(formatExpiration(tokenInfo.expiresAt))}`);
|
|
208
|
+
logger.log(` Expires: ${chalk.gray(formatExpiration(tokenInfo.expiresAt))}`);
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
if (tokenInfo.error) {
|
|
@@ -241,14 +241,43 @@ async function resolveDataplaneUrlSilent(controllerUrl, environment, authConfig)
|
|
|
241
241
|
* @param {boolean} dataplaneConnected - Whether dataplane health check passed
|
|
242
242
|
*/
|
|
243
243
|
function displayDataplaneSection(dataplaneUrl, dataplaneConnected) {
|
|
244
|
+
logger.log('');
|
|
244
245
|
if (dataplaneUrl) {
|
|
245
246
|
logger.log(`Dataplane: ${chalk.cyan(dataplaneUrl)}`);
|
|
246
247
|
const statusIcon = dataplaneConnected ? chalk.green('✓') : chalk.red('✗');
|
|
247
248
|
const statusText = dataplaneConnected ? 'Connected' : 'Not reachable';
|
|
248
|
-
|
|
249
|
+
displayOpenApiDocs(null, dataplaneUrl);
|
|
250
|
+
logger.log('');
|
|
251
|
+
logger.log(` Status: ${statusIcon} ${statusText}`);
|
|
249
252
|
} else {
|
|
250
253
|
logger.log(`Dataplane: ${chalk.gray('—')}`);
|
|
251
|
-
logger.log(
|
|
254
|
+
logger.log('');
|
|
255
|
+
logger.log(` Status: ${chalk.gray('Not discovered')}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Normalize base URL (no trailing slash) for docs path
|
|
261
|
+
* @param {string} url - Base URL
|
|
262
|
+
* @returns {string} URL without trailing slash
|
|
263
|
+
*/
|
|
264
|
+
function normalizeBaseUrl(url) {
|
|
265
|
+
return (url || '').replace(/\/$/, '');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Display Open API documentation links (Controller and Dataplane)
|
|
270
|
+
* @param {string} controllerUrl - Controller URL
|
|
271
|
+
* @param {string|null} dataplaneUrl - Dataplane URL or null
|
|
272
|
+
*/
|
|
273
|
+
function displayOpenApiDocs(controllerUrl, dataplaneUrl) {
|
|
274
|
+
const controllerBase = normalizeBaseUrl(controllerUrl);
|
|
275
|
+
if (controllerBase) {
|
|
276
|
+
logger.log(` Open API docs: ${chalk.cyan(controllerBase + '/api/docs')}`);
|
|
277
|
+
}
|
|
278
|
+
if (dataplaneUrl) {
|
|
279
|
+
const dataplaneBase = normalizeBaseUrl(dataplaneUrl);
|
|
280
|
+
logger.log(` Open API docs: ${chalk.cyan(dataplaneBase + '/api/docs')}`);
|
|
252
281
|
}
|
|
253
282
|
}
|
|
254
283
|
|
|
@@ -264,11 +293,12 @@ function displayDataplaneSection(dataplaneUrl, dataplaneConnected) {
|
|
|
264
293
|
function displayStatus(controllerUrl, environment, tokenInfo, dataplaneInfo) {
|
|
265
294
|
logger.log(chalk.bold('\n🔐 Authentication Status\n'));
|
|
266
295
|
logger.log(`Controller: ${chalk.cyan(controllerUrl)}`);
|
|
267
|
-
|
|
296
|
+
displayOpenApiDocs(controllerUrl, null);
|
|
297
|
+
logger.log(` Environment: ${chalk.cyan(environment || 'Not specified')}\n`);
|
|
268
298
|
|
|
269
299
|
if (!tokenInfo) {
|
|
270
|
-
logger.log(`Status: ${chalk.red('✗ Not authenticated')}`);
|
|
271
|
-
logger.log(`Token Type: ${chalk.gray('None')}\n`);
|
|
300
|
+
logger.log(` Status: ${chalk.red('✗ Not authenticated')}`);
|
|
301
|
+
logger.log(` Token Type: ${chalk.gray('None')}\n`);
|
|
272
302
|
logger.log(chalk.yellow('💡 Run "aifabrix login" to authenticate\n'));
|
|
273
303
|
return;
|
|
274
304
|
}
|
|
@@ -12,13 +12,15 @@ const logger = require('../utils/logger');
|
|
|
12
12
|
/**
|
|
13
13
|
* Build preferences object for wizard.yaml (schema shape: intent, fieldOnboardingLevel, enableOpenAPIGeneration, enableMCP, enableABAC, enableRBAC)
|
|
14
14
|
* @param {string} intent - User intent
|
|
15
|
-
* @param {Object} preferences - From promptForUserPreferences (mcp, abac, rbac)
|
|
15
|
+
* @param {Object} preferences - From promptForUserPreferences (fieldOnboardingLevel, mcp, abac, rbac)
|
|
16
16
|
* @returns {Object} Preferences for wizard-config schema
|
|
17
17
|
*/
|
|
18
18
|
function buildPreferencesForSave(intent, preferences) {
|
|
19
|
+
const level = preferences?.fieldOnboardingLevel;
|
|
20
|
+
const validLevel = level === 'standard' || level === 'minimal' ? level : 'full';
|
|
19
21
|
return {
|
|
20
22
|
intent: intent || 'general integration',
|
|
21
|
-
fieldOnboardingLevel:
|
|
23
|
+
fieldOnboardingLevel: validLevel,
|
|
22
24
|
enableOpenAPIGeneration: true,
|
|
23
25
|
enableMCP: Boolean(preferences?.mcp),
|
|
24
26
|
enableABAC: Boolean(preferences?.abac),
|
|
@@ -78,11 +80,18 @@ function formatSourceLine(source) {
|
|
|
78
80
|
* @returns {string|null}
|
|
79
81
|
*/
|
|
80
82
|
function formatPreferencesLine(preferences) {
|
|
81
|
-
if (!preferences || (!preferences.intent && (preferences.enableMCP === undefined || preferences.enableMCP === null))) {
|
|
83
|
+
if (!preferences || (!preferences.intent && (preferences.enableMCP === undefined || preferences.enableMCP === null) && !preferences.fieldOnboardingLevel)) {
|
|
82
84
|
return null;
|
|
83
85
|
}
|
|
84
86
|
const p = preferences;
|
|
85
|
-
|
|
87
|
+
const parts = [
|
|
88
|
+
p.fieldOnboardingLevel && `level=${p.fieldOnboardingLevel}`,
|
|
89
|
+
p.intent && `intent=${p.intent}`,
|
|
90
|
+
p.enableMCP && 'MCP',
|
|
91
|
+
p.enableABAC && 'ABAC',
|
|
92
|
+
p.enableRBAC && 'RBAC'
|
|
93
|
+
].filter(Boolean);
|
|
94
|
+
return parts.length ? parts.join(', ') : '(defaults)';
|
|
86
95
|
}
|
|
87
96
|
|
|
88
97
|
/**
|
package/lib/commands/wizard.js
CHANGED
|
@@ -148,6 +148,7 @@ async function handleInteractiveConfigGeneration(options) {
|
|
|
148
148
|
|
|
149
149
|
const configPrefs = {
|
|
150
150
|
intent: userIntent,
|
|
151
|
+
fieldOnboardingLevel: preferences.fieldOnboardingLevel || 'full',
|
|
151
152
|
enableMCP: preferences.mcp,
|
|
152
153
|
enableABAC: preferences.abac,
|
|
153
154
|
enableRBAC: preferences.rbac
|
|
@@ -309,10 +309,21 @@ async function promptForUserIntent() {
|
|
|
309
309
|
* Prompt for user preferences
|
|
310
310
|
* @async
|
|
311
311
|
* @function promptForUserPreferences
|
|
312
|
-
* @returns {Promise<Object>} User preferences object
|
|
312
|
+
* @returns {Promise<Object>} User preferences object (fieldOnboardingLevel, mcp, abac, rbac)
|
|
313
313
|
*/
|
|
314
314
|
async function promptForUserPreferences() {
|
|
315
315
|
const answers = await inquirer.prompt([
|
|
316
|
+
{
|
|
317
|
+
type: 'list',
|
|
318
|
+
name: 'fieldOnboardingLevel',
|
|
319
|
+
message: 'Field onboarding level:',
|
|
320
|
+
choices: [
|
|
321
|
+
{ name: 'full - All fields mapped and indexed', value: 'full' },
|
|
322
|
+
{ name: 'standard - Core and important fields only', value: 'standard' },
|
|
323
|
+
{ name: 'minimal - Essential fields only', value: 'minimal' }
|
|
324
|
+
],
|
|
325
|
+
default: 'full'
|
|
326
|
+
},
|
|
316
327
|
{
|
|
317
328
|
type: 'confirm',
|
|
318
329
|
name: 'mcp',
|
|
@@ -333,6 +344,7 @@ async function promptForUserPreferences() {
|
|
|
333
344
|
}
|
|
334
345
|
]);
|
|
335
346
|
return {
|
|
347
|
+
fieldOnboardingLevel: answers.fieldOnboardingLevel,
|
|
336
348
|
mcp: answers.mcp,
|
|
337
349
|
abac: answers.abac,
|
|
338
350
|
rbac: answers.rbac
|
|
@@ -20,7 +20,7 @@ environments:
|
|
|
20
20
|
KEYCLOAK_PORT: 8082 # Internal port (container-to-container). KEYCLOAK_PUBLIC_PORT calculated automatically.
|
|
21
21
|
KEYCLOAK_PUBLIC_PORT: 8082
|
|
22
22
|
DATAPLANE_HOST: dataplane
|
|
23
|
-
DATAPLANE_PORT: 3001
|
|
23
|
+
DATAPLANE_PORT: 3001 # Internal port (container-to-container). DATAPLANE_PUBLIC_PORT calculated automatically.
|
|
24
24
|
NODE_ENV: production
|
|
25
25
|
PYTHONUNBUFFERED: 1
|
|
26
26
|
PYTHONDONTWRITEBYTECODE: 1
|
|
@@ -33,10 +33,8 @@ environments:
|
|
|
33
33
|
REDIS_PORT: 6379
|
|
34
34
|
MISO_HOST: localhost
|
|
35
35
|
MISO_PORT: 3010
|
|
36
|
-
MISO_PUBLIC_PORT: 3010
|
|
37
36
|
KEYCLOAK_HOST: localhost
|
|
38
37
|
KEYCLOAK_PORT: 8082
|
|
39
|
-
KEYCLOAK_PUBLIC_PORT: 8082
|
|
40
38
|
DATAPLANE_HOST: localhost
|
|
41
39
|
DATAPLANE_PORT: 3011
|
|
42
40
|
NODE_ENV: development
|
|
@@ -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.2.0",
|
|
11
11
|
"type": "schema",
|
|
12
12
|
"category": "integration",
|
|
13
13
|
"author": "AI Fabrix Team",
|
|
@@ -27,6 +27,15 @@
|
|
|
27
27
|
],
|
|
28
28
|
"dependencies": [],
|
|
29
29
|
"changelog": [
|
|
30
|
+
{
|
|
31
|
+
"version": "1.2.0",
|
|
32
|
+
"date": "2025-02-01T00:00:00Z",
|
|
33
|
+
"changes": [
|
|
34
|
+
"Added generateMcpContract (boolean, default true): config-only control for MCP contract generation on publish",
|
|
35
|
+
"Added generateOpenApiContract (boolean, default true): reserved for future use"
|
|
36
|
+
],
|
|
37
|
+
"breaking": false
|
|
38
|
+
},
|
|
30
39
|
{
|
|
31
40
|
"version": "1.1.0",
|
|
32
41
|
"date": "2025-12-01T00:00:00Z",
|
|
@@ -407,6 +416,20 @@
|
|
|
407
416
|
"type": "boolean",
|
|
408
417
|
"description": "Master switch for all endpoints in this system. If false, no endpoints are registered regardless of individual endpoint active flags.",
|
|
409
418
|
"default": true
|
|
419
|
+
},
|
|
420
|
+
"credentialIdOrKey": {
|
|
421
|
+
"type": "string",
|
|
422
|
+
"description": "Credential identifier (ID or key) to use for authenticating with this external system."
|
|
423
|
+
},
|
|
424
|
+
"generateMcpContract": {
|
|
425
|
+
"type": "boolean",
|
|
426
|
+
"description": "Whether to generate MCP contract on publish. Config only (no query parameter); default true when absent.",
|
|
427
|
+
"default": true
|
|
428
|
+
},
|
|
429
|
+
"generateOpenApiContract": {
|
|
430
|
+
"type": "boolean",
|
|
431
|
+
"description": "Reserved: whether to generate or expose OpenAPI contract on publish. Not yet implemented.",
|
|
432
|
+
"default": true
|
|
410
433
|
}
|
|
411
434
|
},
|
|
412
435
|
"additionalProperties": false
|
|
@@ -25,8 +25,9 @@ function getSecretsFilePath() {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* Load client credentials from secrets.local.yaml
|
|
29
|
-
* Reads using pattern: <app-name>-client-idKeyVault and <app-name>-client-secretKeyVault
|
|
28
|
+
* Load client credentials from secrets.local.yaml or process.env (e.g. integration/hubspot/.env).
|
|
29
|
+
* Reads secrets file using pattern: <app-name>-client-idKeyVault and <app-name>-client-secretKeyVault.
|
|
30
|
+
* If not found, checks process.env.CLIENTID and process.env.CLIENTSECRET (set when .env is loaded).
|
|
30
31
|
* @param {string} appName - Application name
|
|
31
32
|
* @returns {Promise<{clientId: string, clientSecret: string}|null>} Credentials or null if not found
|
|
32
33
|
*/
|
|
@@ -37,31 +38,38 @@ async function loadClientCredentials(appName) {
|
|
|
37
38
|
|
|
38
39
|
try {
|
|
39
40
|
const secretsFile = getSecretsFilePath();
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
41
|
+
if (fs.existsSync(secretsFile)) {
|
|
42
|
+
const content = fs.readFileSync(secretsFile, 'utf8');
|
|
43
|
+
const secrets = yaml.load(content) || {};
|
|
44
|
+
|
|
45
|
+
const clientIdKey = `${appName}-client-idKeyVault`;
|
|
46
|
+
const clientSecretKey = `${appName}-client-secretKeyVault`;
|
|
47
|
+
|
|
48
|
+
const clientId = secrets[clientIdKey];
|
|
49
|
+
const clientSecret = secrets[clientSecretKey];
|
|
50
|
+
|
|
51
|
+
if (clientId && clientSecret) {
|
|
52
|
+
return {
|
|
53
|
+
clientId: String(clientId),
|
|
54
|
+
clientSecret: String(clientSecret)
|
|
55
|
+
};
|
|
56
|
+
}
|
|
55
57
|
}
|
|
58
|
+
} catch (error) {
|
|
59
|
+
logger.warn(`Failed to load credentials from secrets.local.yaml: ${error.message}`);
|
|
60
|
+
}
|
|
56
61
|
|
|
62
|
+
// Fallback: use CLIENTID/CLIENTSECRET from process.env (e.g. from integration/hubspot/.env)
|
|
63
|
+
const envClientId = process.env.CLIENTID || process.env.CLIENT_ID;
|
|
64
|
+
const envClientSecret = process.env.CLIENTSECRET || process.env.CLIENT_SECRET;
|
|
65
|
+
if (envClientId && envClientSecret) {
|
|
57
66
|
return {
|
|
58
|
-
clientId:
|
|
59
|
-
clientSecret:
|
|
67
|
+
clientId: String(envClientId).trim(),
|
|
68
|
+
clientSecret: String(envClientSecret).trim()
|
|
60
69
|
};
|
|
61
|
-
} catch (error) {
|
|
62
|
-
logger.warn(`Failed to load credentials from secrets.local.yaml: ${error.message}`);
|
|
63
|
-
return null;
|
|
64
70
|
}
|
|
71
|
+
|
|
72
|
+
return null;
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
/**
|
|
@@ -243,9 +251,8 @@ async function tryClientTokenAuth(environment, appName, controllerUrl) {
|
|
|
243
251
|
controller: clientToken.controller
|
|
244
252
|
};
|
|
245
253
|
}
|
|
246
|
-
} catch
|
|
247
|
-
// Client token unavailable
|
|
248
|
-
logger.warn(`Client token unavailable: ${error.message}`);
|
|
254
|
+
} catch {
|
|
255
|
+
// Client token unavailable; getDeploymentAuth will try client credentials next (no warning here to avoid misleading output when env credentials succeed)
|
|
249
256
|
}
|
|
250
257
|
return null;
|
|
251
258
|
}
|
package/package.json
CHANGED