@aifabrix/builder 2.42.0 → 2.43.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.
Files changed (133) hide show
  1. package/README.md +1 -1
  2. package/bin/aifabrix.js +1 -1
  3. package/integration/hubspot-test/README.md +126 -0
  4. package/integration/{hubspot → hubspot-test}/application.json +6 -6
  5. package/integration/{hubspot → hubspot-test}/create-hubspot.js +5 -5
  6. package/integration/hubspot-test/env.template +4 -0
  7. package/integration/{hubspot/hubspot-datasource-company.json → hubspot-test/hubspot-test-datasource-company.json} +3 -2
  8. package/integration/{hubspot/hubspot-datasource-contact.json → hubspot-test/hubspot-test-datasource-contact.json} +3 -2
  9. package/integration/{hubspot/hubspot-datasource-deal.json → hubspot-test/hubspot-test-datasource-deal.json} +3 -2
  10. package/integration/{hubspot/hubspot-datasource-users.json → hubspot-test/hubspot-test-datasource-users.json} +3 -2
  11. package/integration/{hubspot/hubspot-deploy.json → hubspot-test/hubspot-test-deploy.json} +198 -21
  12. package/integration/{hubspot/hubspot-system.json → hubspot-test/hubspot-test-system.json} +8 -7
  13. package/integration/hubspot-test/rbac.json +166 -0
  14. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-hubspot-credential-real.yaml +3 -3
  15. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-hubspot-env-vars.yaml +2 -2
  16. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-add-datasource.yaml +1 -1
  17. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-credential-create.yaml +1 -1
  18. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-credential-select.yaml +1 -1
  19. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-known-platform.yaml +1 -1
  20. package/integration/hubspot-test/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
  21. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-mode.yaml +1 -1
  22. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-openapi-file.yaml +1 -1
  23. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-openapi-url.yaml +1 -1
  24. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-source.yaml +1 -1
  25. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-dimension-array-test.yaml +1 -1
  26. package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
  27. package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
  28. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-dimension-test.yaml +1 -1
  29. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-rbac-test.yaml +1 -1
  30. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +1 -1
  31. package/integration/{hubspot → hubspot-test}/test.js +102 -59
  32. package/integration/{hubspot → hubspot-test}/wizard-hubspot-e2e.yaml +2 -2
  33. package/integration/{hubspot → hubspot-test}/wizard-hubspot-platform.yaml +1 -1
  34. package/lib/api/external-test.api.js +1 -1
  35. package/lib/api/service-users.api.js +111 -2
  36. package/lib/api/types/service-users.types.js +41 -0
  37. package/lib/api/wizard.api.js +2 -1
  38. package/lib/app/index.js +2 -2
  39. package/lib/app/prompts.js +2 -2
  40. package/lib/app/readme.js +3 -1
  41. package/lib/app/register.js +3 -1
  42. package/lib/app/rotate-secret.js +3 -0
  43. package/lib/cli/setup-app.js +5 -5
  44. package/lib/cli/setup-auth.js +19 -11
  45. package/lib/cli/setup-dev.js +62 -32
  46. package/lib/cli/setup-environment.js +6 -21
  47. package/lib/cli/setup-infra.js +13 -0
  48. package/lib/cli/setup-secrets.js +45 -6
  49. package/lib/cli/setup-service-user.js +146 -20
  50. package/lib/cli/setup-utility.js +12 -0
  51. package/lib/commands/auth-config.js +25 -19
  52. package/lib/commands/datasource.js +46 -1
  53. package/lib/commands/dev-init.js +1 -1
  54. package/lib/commands/repair-env-template.js +14 -8
  55. package/lib/commands/repair-rbac.js +25 -19
  56. package/lib/commands/repair.js +108 -31
  57. package/lib/commands/secrets-remove.js +1 -1
  58. package/lib/commands/secrets-set.js +6 -0
  59. package/lib/commands/secrets-validate.js +17 -4
  60. package/lib/commands/service-user.js +231 -2
  61. package/lib/commands/up-common.js +25 -0
  62. package/lib/commands/up-dataplane.js +91 -7
  63. package/lib/commands/wizard-core-helpers.js +5 -2
  64. package/lib/commands/wizard-core.js +2 -1
  65. package/lib/commands/wizard-headless.js +6 -1
  66. package/lib/commands/wizard.js +13 -6
  67. package/lib/core/admin-secrets.js +2 -0
  68. package/lib/core/config.js +7 -5
  69. package/lib/core/ensure-encryption-key.js +1 -3
  70. package/lib/core/secrets.js +32 -9
  71. package/lib/core/templates.js +1 -1
  72. package/lib/datasource/abac-validator.js +157 -0
  73. package/lib/datasource/field-reference-validator.js +74 -36
  74. package/lib/datasource/log-viewer.js +221 -0
  75. package/lib/datasource/resolve-app.js +109 -0
  76. package/lib/datasource/test-e2e.js +11 -20
  77. package/lib/datasource/test-integration.js +42 -22
  78. package/lib/datasource/validate.js +5 -2
  79. package/lib/external-system/download-helpers.js +3 -1
  80. package/lib/external-system/generator.js +12 -8
  81. package/lib/external-system/test-system-level.js +1 -1
  82. package/lib/generator/external-controller-manifest.js +3 -3
  83. package/lib/generator/external-schema-utils.js +3 -1
  84. package/lib/generator/external.js +7 -7
  85. package/lib/generator/helpers.js +13 -9
  86. package/lib/generator/index.js +4 -4
  87. package/lib/generator/split.js +45 -10
  88. package/lib/generator/wizard-prompts-secondary.js +39 -7
  89. package/lib/generator/wizard-readme.js +4 -1
  90. package/lib/generator/wizard.js +68 -53
  91. package/lib/infrastructure/helpers.js +50 -35
  92. package/lib/infrastructure/index.js +39 -23
  93. package/lib/schema/env-config.yaml +19 -2
  94. package/lib/schema/external-datasource.schema.json +11 -1
  95. package/lib/schema/wizard-config.schema.json +7 -1
  96. package/lib/utils/app-config-resolver.js +23 -1
  97. package/lib/utils/config-paths.js +48 -4
  98. package/lib/utils/credential-secrets-env.js +16 -1
  99. package/lib/utils/env-map.js +7 -3
  100. package/lib/utils/error-formatter.js +37 -0
  101. package/lib/utils/external-env-template.js +180 -0
  102. package/lib/utils/external-readme.js +33 -1
  103. package/lib/utils/external-system-display.js +43 -0
  104. package/lib/utils/external-system-validators.js +2 -2
  105. package/lib/utils/help-builder.js +3 -5
  106. package/lib/utils/local-secrets.js +26 -3
  107. package/lib/utils/paths.js +2 -1
  108. package/lib/utils/secrets-generator.js +2 -2
  109. package/lib/utils/secrets-utils.js +4 -0
  110. package/lib/utils/secure-file-permissions.js +91 -0
  111. package/lib/utils/token-manager.js +36 -3
  112. package/lib/utils/yaml-preserve.js +59 -1
  113. package/lib/validation/env-template-auth.js +50 -2
  114. package/lib/validation/external-manifest-validator.js +8 -0
  115. package/lib/validation/validate.js +8 -0
  116. package/lib/validation/validator.js +10 -13
  117. package/package.json +6 -2
  118. package/templates/applications/dataplane/env.template +5 -1
  119. package/templates/applications/miso-controller/application.yaml +1 -1
  120. package/templates/applications/miso-controller/env.template +13 -2
  121. package/templates/external-system/README.md.hbs +18 -5
  122. package/templates/external-system/env.template.hbs +22 -0
  123. package/integration/hubspot/README.md +0 -100
  124. package/integration/hubspot/env.template +0 -4
  125. package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +0 -2
  126. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +0 -5
  127. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +0 -5
  128. /package/integration/{hubspot → hubspot-test}/companies.json +0 -0
  129. /package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-app-name.yaml +0 -0
  130. /package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-missing-app.yaml +0 -0
  131. /package/integration/{hubspot → hubspot-test}/test-dataplane-down-helpers.js +0 -0
  132. /package/integration/{hubspot → hubspot-test}/test-dataplane-down-tests.js +0 -0
  133. /package/integration/{hubspot → hubspot-test}/test-dataplane-down.js +0 -0
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Log viewer for E2E and integration test logs - format and display JSON logs.
3
+ * @fileoverview Read and format test-e2e / test-integration debug logs for terminal
4
+ * @author AI Fabrix Team
5
+ * @version 2.0.0
6
+ */
7
+ /* eslint-disable max-statements, complexity, max-depth -- Formatter functions; display branches by design */
8
+
9
+ const path = require('path');
10
+ const fs = require('fs').promises;
11
+ const chalk = require('chalk');
12
+ const logger = require('../utils/logger');
13
+ const { resolveAppKeyForDatasource } = require('./resolve-app');
14
+ const { getIntegrationPath } = require('../utils/paths');
15
+
16
+ /**
17
+ * Get the path to the latest log file in a directory matching a glob-like pattern
18
+ * @param {string} logsDir - Directory containing log files
19
+ * @param {string} pattern - Prefix pattern (e.g. 'test-e2e' matches test-e2e-*.json)
20
+ * @returns {Promise<string|null>} Full path to latest file or null if none
21
+ */
22
+ async function getLatestLogPath(logsDir, pattern) {
23
+ let entries;
24
+ try {
25
+ entries = await fs.readdir(logsDir, { withFileTypes: true });
26
+ } catch (err) {
27
+ if (err.code === 'ENOENT') return null;
28
+ throw err;
29
+ }
30
+ const prefix = pattern.replace(/\*$/, '');
31
+ const files = entries
32
+ .filter(e => e.isFile() && e.name.startsWith(prefix) && e.name.endsWith('.json'))
33
+ .map(e => path.join(logsDir, e.name));
34
+ if (files.length === 0) return null;
35
+ const withStats = await Promise.all(
36
+ files.map(async f => ({ path: f, mtime: (await fs.stat(f)).mtimeMs }))
37
+ );
38
+ withStats.sort((a, b) => b.mtime - a.mtime);
39
+ return withStats[0].path;
40
+ }
41
+
42
+ /**
43
+ * Truncate string for display
44
+ * @param {string} s - String
45
+ * @param {number} maxLen - Max length
46
+ * @returns {string}
47
+ */
48
+ function truncate(s, maxLen = 60) {
49
+ if (typeof s !== 'string') return String(s);
50
+ return s.length <= maxLen ? s : `${s.slice(0, maxLen - 1)}…`;
51
+ }
52
+
53
+ /**
54
+ * Format E2E log content for terminal display
55
+ * @param {Object} data - Parsed log JSON (request, response, error)
56
+ * @param {string} [fileName] - Log file name for header
57
+ */
58
+ function formatE2ELog(data, fileName) {
59
+ logger.log(chalk.blue('\n——— E2E Log') + (fileName ? chalk.gray(` ${fileName}`) : ''));
60
+ const req = data.request || {};
61
+ logger.log(chalk.cyan('Request:'));
62
+ logger.log(chalk.gray(` sourceIdOrKey: ${req.sourceIdOrKey ?? '—'}`));
63
+ if (req.includeDebug !== undefined) logger.log(chalk.gray(` includeDebug: ${req.includeDebug}`));
64
+ if (req.cleanup !== undefined) logger.log(chalk.gray(` cleanup: ${req.cleanup}`));
65
+ if (req.primaryKeyValue !== undefined) logger.log(chalk.gray(` primaryKeyValue: ${truncate(JSON.stringify(req.primaryKeyValue))}`));
66
+ if (data.error) {
67
+ logger.log(chalk.red('Error: ') + data.error);
68
+ return;
69
+ }
70
+ const res = data.response || {};
71
+ logger.log(chalk.cyan('Response:'));
72
+ logger.log(chalk.gray(` success: ${res.success}`));
73
+ if (res.status) logger.log(chalk.gray(` status: ${res.status}`));
74
+ if (res.error) logger.log(chalk.red(` error: ${res.error}`));
75
+ const steps = res.steps || res.completedActions || [];
76
+ if (steps.length > 0) {
77
+ logger.log(chalk.cyan('Steps:'));
78
+ for (const step of steps) {
79
+ const name = step.name || step.step || 'unknown';
80
+ const ok = step.success !== false && !step.error;
81
+ logger.log(` ${ok ? chalk.green('✓') : chalk.red('✗')} ${name}`);
82
+ if (step.error) logger.log(chalk.red(` ${step.error}`));
83
+ if (step.message && ok) logger.log(chalk.gray(` ${step.message}`));
84
+ if ((name === 'sync' || step.step === 'sync') && step.evidence && step.evidence.jobs) {
85
+ for (const job of step.evidence.jobs) {
86
+ const audit = job.audit || {};
87
+ const parts = [];
88
+ if (job.recordsProcessed !== undefined && job.recordsProcessed !== null) parts.push(`${job.recordsProcessed} processed`);
89
+ if (job.totalRecords !== undefined && job.totalRecords !== null) parts.push(`total: ${job.totalRecords}`);
90
+ if (audit.inserted !== undefined && audit.inserted !== null || audit.updated !== undefined && audit.updated !== null || audit.deleted !== undefined && audit.deleted !== null) {
91
+ parts.push(`(inserted: ${audit.inserted ?? 0}, updated: ${audit.updated ?? 0}, deleted: ${audit.deleted ?? 0})`);
92
+ }
93
+ if (parts.length) logger.log(chalk.gray(` Job: ${parts.join(' ')}`));
94
+ }
95
+ }
96
+ }
97
+ }
98
+ if (res.auditLog && Array.isArray(res.auditLog) && res.auditLog.length > 0) {
99
+ logger.log(chalk.cyan('CIP execution trace(s): ') + chalk.gray(`${res.auditLog.length}`));
100
+ const baseUrl = (req.dataplaneUrl || '').toString().replace(/\/$/, '');
101
+ const sourceIdOrKey = req.sourceIdOrKey || '';
102
+ res.auditLog.slice(0, 3).forEach((trace, i) => {
103
+ const id = trace.executionId || trace.id || trace.traceId;
104
+ if (id) {
105
+ const idStr = String(id);
106
+ logger.log(chalk.gray(` ${i + 1}. executionId: ${idStr}`));
107
+ if (baseUrl && sourceIdOrKey) {
108
+ const executionUrl = `${baseUrl}/api/v1/external/${sourceIdOrKey}/executions/${idStr}`;
109
+ logger.log(chalk.gray(` Link: ${executionUrl}`));
110
+ }
111
+ }
112
+ });
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Format integration test log content for terminal display
118
+ * @param {Object} data - Parsed log JSON
119
+ * @param {string} [fileName] - Log file name for header
120
+ */
121
+ function formatIntegrationLog(data, fileName) {
122
+ logger.log(chalk.blue('\n——— Integration Log') + (fileName ? chalk.gray(` ${fileName}`) : ''));
123
+ const req = data.request || {};
124
+ logger.log(chalk.cyan('Request:'));
125
+ logger.log(chalk.gray(` systemKey: ${req.systemKey ?? '—'}, datasourceKey: ${req.datasourceKey ?? '—'}`));
126
+ if (req.includeDebug !== undefined) logger.log(chalk.gray(` includeDebug: ${req.includeDebug}`));
127
+ if (data.error) {
128
+ logger.log(chalk.red('Error: ') + data.error);
129
+ return;
130
+ }
131
+ const res = data.response || {};
132
+ logger.log(chalk.cyan('Response:'));
133
+ logger.log(chalk.gray(` success: ${res.success}`));
134
+ if (res.error) logger.log(chalk.red(` error: ${res.error}`));
135
+ const vr = res.validationResults || {};
136
+ logger.log(chalk.cyan('Validation:'));
137
+ logger.log(chalk.gray(` isValid: ${vr.isValid}`));
138
+ if (vr.errors && vr.errors.length) vr.errors.forEach(e => logger.log(chalk.red(` - ${e}`)));
139
+ const fmr = res.fieldMappingResults || {};
140
+ if (Object.keys(fmr).length) {
141
+ logger.log(chalk.cyan('Field mapping:'));
142
+ logger.log(chalk.gray(` mappingCount: ${fmr.mappingCount ?? '—'}`));
143
+ if (fmr.dimensions) logger.log(chalk.gray(` dimensions: ${Object.keys(fmr.dimensions).join(', ')}`));
144
+ }
145
+ const etr = res.endpointTestResults || {};
146
+ if (Object.keys(etr).length) {
147
+ logger.log(chalk.cyan('Endpoint:'));
148
+ logger.log(chalk.gray(` endpointConfigured: ${etr.endpointConfigured}`));
149
+ }
150
+ if (res.normalizedOutput || res.normalizedMetadata) {
151
+ const out = res.normalizedOutput || res.normalizedMetadata;
152
+ const keys = typeof out === 'object' && out !== null ? Object.keys(out) : [];
153
+ logger.log(chalk.cyan('Normalized output: ') + chalk.gray(keys.length ? `${keys.length} fields` : '—'));
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Format log content by type
159
+ * @param {Object} parsed - Parsed JSON log
160
+ * @param {'test-e2e'|'test-integration'} logType - Log type
161
+ * @param {string} [fileName] - File name for header
162
+ */
163
+ function formatLogContent(parsed, logType, fileName) {
164
+ if (logType === 'test-e2e') {
165
+ formatE2ELog(parsed, fileName);
166
+ } else {
167
+ formatIntegrationLog(parsed, fileName);
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Run log viewer: resolve log file, read, parse, format and print
173
+ * @async
174
+ * @param {string} datasourceKey - Datasource key (used when no --file)
175
+ * @param {Object} options - Options
176
+ * @param {string} [options.app] - App key (optional, resolved from key if omitted)
177
+ * @param {string} [options.file] - Path to log file (overrides app resolution)
178
+ * @param {'test-e2e'|'test-integration'} options.logType - Log type
179
+ * @throws {Error} When file not found or invalid JSON
180
+ */
181
+ /* eslint-disable-next-line max-statements -- Resolve path, read, parse, format */
182
+ async function runLogViewer(datasourceKey, options) {
183
+ const { app, file, logType } = options;
184
+ let logPath;
185
+ let fileName;
186
+ if (file && typeof file === 'string' && file.trim()) {
187
+ logPath = path.isAbsolute(file) ? file : path.resolve(process.cwd(), file.trim());
188
+ fileName = path.basename(logPath);
189
+ } else {
190
+ if (!datasourceKey || typeof datasourceKey !== 'string') {
191
+ throw new Error('Datasource key is required when --file is not provided');
192
+ }
193
+ const { appKey } = await resolveAppKeyForDatasource(datasourceKey.trim(), app);
194
+ const appPath = getIntegrationPath(appKey);
195
+ const logsDir = path.join(appPath, 'logs');
196
+ const pattern = logType === 'test-e2e' ? 'test-e2e-' : 'test-integration-';
197
+ logPath = await getLatestLogPath(logsDir, pattern);
198
+ if (!logPath) {
199
+ throw new Error(
200
+ `No ${logType} log found in ${logsDir}. Run the test with --debug first.`
201
+ );
202
+ }
203
+ fileName = path.basename(logPath);
204
+ }
205
+ const content = await fs.readFile(logPath, 'utf8');
206
+ let parsed;
207
+ try {
208
+ parsed = JSON.parse(content);
209
+ } catch (err) {
210
+ throw new Error(`Invalid JSON in ${logPath}: ${err.message}`);
211
+ }
212
+ formatLogContent(parsed, logType, fileName);
213
+ }
214
+
215
+ module.exports = {
216
+ getLatestLogPath,
217
+ formatLogContent,
218
+ formatE2ELog,
219
+ formatIntegrationLog,
220
+ runLogViewer
221
+ };
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Resolve app key for datasource commands from explicit --app, cwd, scan, or key parse.
3
+ * @fileoverview App resolution for datasource test-e2e, test-integration, log-e2e, log-integration
4
+ * @author AI Fabrix Team
5
+ * @version 2.0.0
6
+ */
7
+
8
+ const path = require('path');
9
+ const fs = require('fs');
10
+ const {
11
+ getIntegrationPath,
12
+ listIntegrationAppNames,
13
+ resolveIntegrationAppKeyFromCwd
14
+ } = require('../utils/paths');
15
+ const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
16
+ const { loadConfigFile } = require('../utils/config-format');
17
+
18
+ /**
19
+ * For one app, check if any of its datasource files has the given key
20
+ * @param {string} appKey - Integration app key
21
+ * @param {string} datasourceKey - Datasource key to match
22
+ * @returns {boolean} True if this app has a datasource with that key
23
+ */
24
+ function appHasDatasourceKey(appKey, datasourceKey) {
25
+ const appPath = getIntegrationPath(appKey);
26
+ let config;
27
+ try {
28
+ const configPath = resolveApplicationConfigPath(appPath);
29
+ config = loadConfigFile(configPath);
30
+ } catch {
31
+ return false;
32
+ }
33
+ const schemaBasePath = config.externalIntegration?.schemaBasePath || './';
34
+ const datasourceFiles = config.externalIntegration?.dataSources || [];
35
+ for (const f of datasourceFiles) {
36
+ if (!f || typeof f !== 'string') continue;
37
+ const fullPath = path.isAbsolute(schemaBasePath)
38
+ ? path.join(schemaBasePath, f)
39
+ : path.join(appPath, schemaBasePath, f);
40
+ if (!fs.existsSync(fullPath)) continue;
41
+ try {
42
+ const parsed = loadConfigFile(fullPath);
43
+ if (parsed && parsed.key === datasourceKey) return true;
44
+ } catch {
45
+ // skip unreadable or invalid files
46
+ }
47
+ }
48
+ return false;
49
+ }
50
+
51
+ /**
52
+ * Resolve app key for a datasource: explicit --app, cwd, scan by key, or parse key convention.
53
+ * @async
54
+ * @param {string} datasourceKey - Datasource key (e.g. hubspot-test-company)
55
+ * @param {string} [explicitApp] - Explicit app key from --app
56
+ * @returns {Promise<{appKey: string}>} Resolved app key
57
+ * @throws {Error} When app cannot be determined or multiple apps match
58
+ */
59
+ /* eslint-disable-next-line max-statements -- Resolution order: explicit, cwd, scan, parse */
60
+ async function resolveAppKeyForDatasource(datasourceKey, explicitApp) {
61
+ if (!datasourceKey || typeof datasourceKey !== 'string') {
62
+ throw new Error('Datasource key is required');
63
+ }
64
+ const key = String(datasourceKey).trim();
65
+ if (key.length === 0) {
66
+ throw new Error('Datasource key cannot be empty');
67
+ }
68
+
69
+ if (explicitApp && typeof explicitApp === 'string' && explicitApp.trim()) {
70
+ const appPath = getIntegrationPath(explicitApp.trim());
71
+ if (fs.existsSync(appPath)) {
72
+ return { appKey: explicitApp.trim() };
73
+ }
74
+ }
75
+
76
+ const fromCwd = resolveIntegrationAppKeyFromCwd();
77
+ if (fromCwd && appHasDatasourceKey(fromCwd, key)) {
78
+ return { appKey: fromCwd };
79
+ }
80
+
81
+ const appNames = listIntegrationAppNames();
82
+ const matches = appNames.filter(appName => appHasDatasourceKey(appName, key));
83
+ if (matches.length === 1) {
84
+ return { appKey: matches[0] };
85
+ }
86
+ if (matches.length > 1) {
87
+ throw new Error(
88
+ `More than one app has this datasource; add --app <appKey>. Apps: ${matches.join(', ')}`
89
+ );
90
+ }
91
+
92
+ const segments = key.split('-');
93
+ if (segments.length >= 2) {
94
+ const candidate = segments.slice(0, -1).join('-');
95
+ const candidatePath = getIntegrationPath(candidate);
96
+ if (fs.existsSync(candidatePath) && appHasDatasourceKey(candidate, key)) {
97
+ return { appKey: candidate };
98
+ }
99
+ }
100
+
101
+ throw new Error(
102
+ 'Could not determine app context. Use --app <appKey> or run from integration/<appKey>/ directory.'
103
+ );
104
+ }
105
+
106
+ module.exports = {
107
+ resolveAppKeyForDatasource,
108
+ appHasDatasourceKey
109
+ };
@@ -10,7 +10,8 @@ const path = require('path');
10
10
  const fs = require('fs').promises;
11
11
  const chalk = require('chalk');
12
12
  const logger = require('../utils/logger');
13
- const { getIntegrationPath, resolveIntegrationAppKeyFromCwd } = require('../utils/paths');
13
+ const { getIntegrationPath } = require('../utils/paths');
14
+ const { resolveAppKeyForDatasource } = require('./resolve-app');
14
15
  const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
15
16
  const { resolveControllerUrl } = require('../utils/controller-url');
16
17
  const { getDeviceOnlyAuth } = require('../utils/token-manager');
@@ -20,20 +21,6 @@ const { writeTestLog } = require('../utils/test-log-writer');
20
21
  const DEFAULT_POLL_INTERVAL_MS = 2500;
21
22
  const DEFAULT_POLL_TIMEOUT_MS = 15 * 60 * 1000;
22
23
 
23
- /**
24
- * Resolve appKey for datasource test-e2e
25
- * @param {string} [appKey] - Explicit app key from --app
26
- * @returns {string}
27
- */
28
- function resolveAppKey(appKey) {
29
- if (appKey) return appKey;
30
- const fromCwd = resolveIntegrationAppKeyFromCwd();
31
- if (fromCwd) return fromCwd;
32
- throw new Error(
33
- 'Could not determine app context. Use --app <appKey> or run from integration/<appKey>/ directory.'
34
- );
35
- }
36
-
37
24
  /**
38
25
  * Resolve primaryKeyValue for request body: string as-is, or read and parse JSON from @path
39
26
  * @param {string} [value] - Literal value or path prefixed with @ (e.g. @pk.json)
@@ -58,6 +45,7 @@ async function resolvePrimaryKeyValue(value) {
58
45
  async function buildE2EBody(options) {
59
46
  const body = {};
60
47
  if (options.debug) body.includeDebug = true;
48
+ if (options.verbose) body.audit = true;
61
49
  if (options.testCrud === true) body.testCrud = true;
62
50
  if (options.recordId !== undefined && options.recordId !== null && options.recordId !== '') body.recordId = String(options.recordId);
63
51
  if (options.cleanup === false) body.cleanup = false;
@@ -125,7 +113,7 @@ async function runDatasourceTestE2E(datasourceKey, options = {}) {
125
113
  if (!datasourceKey || typeof datasourceKey !== 'string') {
126
114
  throw new Error('Datasource key is required');
127
115
  }
128
- const appKey = resolveAppKey(options.app);
116
+ const { appKey } = await resolveAppKeyForDatasource(datasourceKey, options.app);
129
117
  const controllerUrl = await resolveControllerUrl();
130
118
  const { resolveEnvironment } = require('../core/config');
131
119
  const environment = options.environment || await resolveEnvironment();
@@ -138,6 +126,7 @@ async function runDatasourceTestE2E(datasourceKey, options = {}) {
138
126
  const useAsync = options.async !== false;
139
127
  const requestMeta = {
140
128
  sourceIdOrKey: datasourceKey,
129
+ dataplaneUrl,
141
130
  includeDebug: options.debug,
142
131
  testCrud: options.testCrud,
143
132
  recordId: options.recordId,
@@ -197,11 +186,14 @@ async function executeE2EWithOptionalPoll(opts) {
197
186
  asyncRun: useAsync
198
187
  });
199
188
  let data = response.data || response;
200
- if (useAsync && data !== null && data !== undefined && data.testRunId) {
189
+ const runId = (data?.testRunId !== null && data?.testRunId !== undefined)
190
+ ? (typeof data.testRunId === 'string' ? data.testRunId : data.testRunId.id || data.testRunId.key)
191
+ : null;
192
+ if (useAsync && runId) {
201
193
  data = await pollE2ETestRun(
202
194
  dataplaneUrl,
203
195
  datasourceKey,
204
- data.testRunId,
196
+ runId,
205
197
  authConfig,
206
198
  {
207
199
  intervalMs: pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
@@ -214,6 +206,5 @@ async function executeE2EWithOptionalPoll(opts) {
214
206
  }
215
207
 
216
208
  module.exports = {
217
- runDatasourceTestE2E,
218
- resolveAppKey
209
+ runDatasourceTestE2E
219
210
  };
@@ -9,7 +9,8 @@
9
9
  const path = require('path');
10
10
  const chalk = require('chalk');
11
11
  const logger = require('../utils/logger');
12
- const { getIntegrationPath, resolveIntegrationAppKeyFromCwd } = require('../utils/paths');
12
+ const { getIntegrationPath } = require('../utils/paths');
13
+ const { resolveAppKeyForDatasource } = require('./resolve-app');
13
14
  const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
14
15
  const { loadConfigFile } = require('../utils/config-format');
15
16
  const { setupIntegrationTestAuth } = require('../external-system/test-auth');
@@ -20,21 +21,12 @@ const testHelpers = require('../utils/external-system-test-helpers');
20
21
  const fs = require('fs').promises;
21
22
 
22
23
  /**
23
- * Resolve systemKey and appKey for datasource test-integration
24
- * @param {string} [appKey] - Explicit app key from --app
25
- * @returns {Promise<{appKey: string, systemKey: string}>}
24
+ * Get systemKey for an integration app (from application config and first system file)
25
+ * @param {string} appKey - Integration app key
26
+ * @returns {Promise<string>} systemKey
26
27
  */
27
- async function resolveSystemKey(appKey) {
28
- let resolvedAppKey = appKey;
29
- if (!resolvedAppKey) {
30
- resolvedAppKey = resolveIntegrationAppKeyFromCwd();
31
- }
32
- if (!resolvedAppKey) {
33
- throw new Error(
34
- 'Could not determine app context. Use --app <appKey> or run from integration/<appKey>/ directory.'
35
- );
36
- }
37
- const appPath = getIntegrationPath(resolvedAppKey);
28
+ async function getSystemKeyFromAppKey(appKey) {
29
+ const appPath = getIntegrationPath(appKey);
38
30
  const configPath = resolveApplicationConfigPath(appPath);
39
31
  const config = loadConfigFile(configPath);
40
32
  if (!config.externalIntegration || !config.externalIntegration.systems || config.externalIntegration.systems.length === 0) {
@@ -47,8 +39,33 @@ async function resolveSystemKey(appKey) {
47
39
  const systemContent = await fs.readFile(systemPath, 'utf8');
48
40
  const yaml = require('js-yaml');
49
41
  const systemConfig = yaml.load(systemContent);
50
- const systemKey = systemConfig?.key || path.basename(systemFile, '-system.yaml').replace('-system', '');
51
- return { appKey: resolvedAppKey, systemKey };
42
+ return systemConfig?.key || path.basename(systemFile, '-system.yaml').replace('-system', '');
43
+ }
44
+
45
+ /**
46
+ * Find a datasource filename by matching the key inside the file (fallback when filename-base match fails).
47
+ * @param {string} appPath - Integration app directory path
48
+ * @param {string} schemaBasePath - Schema base path (relative or absolute)
49
+ * @param {string[]} datasourceFiles - List of datasource filenames from application config
50
+ * @param {string} datasourceKey - Datasource key to find
51
+ * @returns {string|null} Filename if found, null otherwise
52
+ */
53
+ function findDatasourceFileByKey(appPath, schemaBasePath, datasourceFiles, datasourceKey) {
54
+ const fsSync = require('fs');
55
+ for (const f of datasourceFiles) {
56
+ if (!f || typeof f !== 'string') continue;
57
+ const fullPath = path.isAbsolute(schemaBasePath)
58
+ ? path.join(schemaBasePath, f)
59
+ : path.join(appPath, schemaBasePath, f);
60
+ if (!fsSync.existsSync(fullPath)) continue;
61
+ try {
62
+ const parsed = loadConfigFile(fullPath);
63
+ if (parsed && parsed.key === datasourceKey) return f;
64
+ } catch {
65
+ // skip unreadable or invalid files
66
+ }
67
+ }
68
+ return null;
52
69
  }
53
70
 
54
71
  /**
@@ -67,23 +84,26 @@ async function runDatasourceTestIntegration(datasourceKey, options = {}) {
67
84
  if (!datasourceKey || typeof datasourceKey !== 'string') {
68
85
  throw new Error('Datasource key is required');
69
86
  }
70
- const { appKey, systemKey } = await resolveSystemKey(options.app);
87
+ const { appKey } = await resolveAppKeyForDatasource(datasourceKey, options.app);
88
+ const systemKey = await getSystemKeyFromAppKey(appKey);
71
89
  const appPath = getIntegrationPath(appKey);
72
90
  const config = loadConfigFile(resolveApplicationConfigPath(appPath));
73
91
  const schemaBasePath = config.externalIntegration?.schemaBasePath || './';
74
92
  const datasourceFiles = config.externalIntegration?.dataSources || [];
75
- const datasourceFile = datasourceFiles.find(f => {
93
+ let datasourceFile = datasourceFiles.find(f => {
76
94
  const base = path.basename(f, path.extname(f));
77
95
  return base === datasourceKey || base.includes(datasourceKey);
78
96
  });
97
+ if (!datasourceFile) {
98
+ datasourceFile = findDatasourceFileByKey(appPath, schemaBasePath, datasourceFiles, datasourceKey);
99
+ }
79
100
  if (!datasourceFile) {
80
101
  throw new Error(`Datasource '${datasourceKey}' not found in application config`);
81
102
  }
82
103
  const datasourcePath = path.isAbsolute(schemaBasePath)
83
104
  ? path.join(schemaBasePath, datasourceFile)
84
105
  : path.join(appPath, schemaBasePath, datasourceFile);
85
- const datasourceContent = await fs.readFile(datasourcePath, 'utf8');
86
- const datasource = JSON.parse(datasourceContent);
106
+ const datasource = loadConfigFile(datasourcePath);
87
107
  if (datasource.key !== datasourceKey) {
88
108
  throw new Error(`Datasource key mismatch: file has '${datasource.key}', expected '${datasourceKey}'`);
89
109
  }
@@ -150,5 +170,5 @@ async function runDatasourceTestIntegration(datasourceKey, options = {}) {
150
170
 
151
171
  module.exports = {
152
172
  runDatasourceTestIntegration,
153
- resolveSystemKey
173
+ getSystemKeyFromAppKey
154
174
  };
@@ -12,6 +12,7 @@ const fs = require('fs');
12
12
  const { loadExternalDataSourceSchema } = require('../utils/schema-loader');
13
13
  const { formatValidationErrors } = require('../utils/error-formatter');
14
14
  const { validateFieldReferences } = require('./field-reference-validator');
15
+ const { validateAbac } = require('./abac-validator');
15
16
 
16
17
  /**
17
18
  * Validates a datasource file against external-datasource schema
@@ -60,10 +61,12 @@ async function validateDatasourceFile(filePath) {
60
61
  }
61
62
 
62
63
  const fieldRefErrors = validateFieldReferences(parsed);
63
- if (fieldRefErrors.length > 0) {
64
+ const abacErrors = validateAbac(parsed);
65
+ const postSchemaErrors = [...fieldRefErrors, ...abacErrors];
66
+ if (postSchemaErrors.length > 0) {
64
67
  return {
65
68
  valid: false,
66
- errors: fieldRefErrors,
69
+ errors: postSchemaErrors,
67
70
  warnings: []
68
71
  };
69
72
  }
@@ -77,13 +77,15 @@ function generateReadme(systemKey, application, dataSources) {
77
77
  };
78
78
  });
79
79
 
80
+ const authType = application.authentication?.type || application.authentication?.method;
80
81
  return generateExternalReadmeContent({
81
82
  appName: systemKey,
82
83
  systemKey,
83
84
  systemType: application.type,
84
85
  displayName: application.displayName,
85
86
  description: application.description,
86
- datasources
87
+ datasources,
88
+ authType
87
89
  });
88
90
  }
89
91
 
@@ -16,6 +16,7 @@ const chalk = require('chalk');
16
16
  const logger = require('../utils/logger');
17
17
  const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
18
18
  const { loadConfigFile, writeConfigFile } = require('../utils/config-format');
19
+ const { getKvPathSegmentForSecurityKey } = require('../utils/credential-secrets-env');
19
20
 
20
21
  // Register Handlebars helper for equality check
21
22
  handlebars.registerHelper('eq', (a, b) => a === b);
@@ -23,36 +24,39 @@ handlebars.registerHelper('json', (obj) => JSON.stringify(obj));
23
24
 
24
25
  /**
25
26
  * Build authentication object per schema authenticationVariablesByMethod.
26
- * Security values use kv://<systemKey>/<key> pattern.
27
+ * Security values use canonical kv://<systemKey>/<segment> paths (segment from getKvPathSegmentForSecurityKey).
27
28
  * @param {string} systemKey - External system key
28
29
  * @param {string} authType - Auth method (oauth2, aad, apikey, basic, queryParam, oidc, hmac, none)
29
30
  * @returns {{ method: string, variables: Object, security: Object }} Authentication object
30
31
  */
31
32
  function buildAuthenticationFromMethod(systemKey, authType) {
32
- const kv = (key) => `kv://${systemKey}/${key}`;
33
+ const kvPath = (securityKey) => {
34
+ const segment = getKvPathSegmentForSecurityKey(securityKey);
35
+ return segment ? `kv://${systemKey}/${segment}` : null;
36
+ };
33
37
  const method = authType || 'apikey';
34
38
  const base = 'https://api.example.com';
35
39
 
36
40
  const authMap = {
37
41
  oauth2: {
38
42
  variables: { baseUrl: base, tokenUrl: `${base}/oauth/token`, authorizationUrl: `${base}/oauth/authorize` },
39
- security: { clientId: kv('clientid'), clientSecret: kv('clientsecret') }
43
+ security: { clientId: kvPath('clientId'), clientSecret: kvPath('clientSecret') }
40
44
  },
41
45
  aad: {
42
46
  variables: { baseUrl: base, tokenUrl: 'https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token', tenantId: '{tenant-id}' },
43
- security: { clientId: kv('clientid'), clientSecret: kv('clientsecret') }
47
+ security: { clientId: kvPath('clientId'), clientSecret: kvPath('clientSecret') }
44
48
  },
45
49
  apikey: {
46
50
  variables: { baseUrl: base, headerName: 'X-API-Key' },
47
- security: { apiKey: kv('apikey') }
51
+ security: { apiKey: kvPath('apiKey') }
48
52
  },
49
53
  basic: {
50
54
  variables: { baseUrl: base },
51
- security: { username: kv('username'), password: kv('password') }
55
+ security: { username: kvPath('username'), password: kvPath('password') }
52
56
  },
53
57
  queryParam: {
54
58
  variables: { baseUrl: base, paramName: 'api_key' },
55
- security: { paramValue: kv('paramvalue') }
59
+ security: { paramValue: kvPath('paramValue') }
56
60
  },
57
61
  oidc: {
58
62
  variables: { openIdConfigUrl: 'https://example.com/.well-known/openid-configuration', clientId: 'app-id' },
@@ -60,7 +64,7 @@ function buildAuthenticationFromMethod(systemKey, authType) {
60
64
  },
61
65
  hmac: {
62
66
  variables: { baseUrl: base, algorithm: 'sha256', signatureHeader: 'X-Signature' },
63
- security: { signingSecret: kv('signingsecret') }
67
+ security: { signingSecret: kvPath('signingSecret') }
64
68
  },
65
69
  none: {
66
70
  variables: {},
@@ -42,7 +42,7 @@ async function runSystemLevelTest({ appName, systemKey, authConfig, dataplaneUrl
42
42
  let success = true;
43
43
 
44
44
  for (const r of rawResults) {
45
- const dsKey = r.key || r.datasourceKey;
45
+ const dsKey = r.key || r.sourceKey || r.name || r.datasourceKey;
46
46
  const dsResult = {
47
47
  key: dsKey,
48
48
  success: r.success !== false,
@@ -11,7 +11,7 @@
11
11
 
12
12
  const path = require('path');
13
13
  const { detectAppType } = require('../utils/paths');
14
- const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
14
+ const { resolveApplicationConfigPath, resolveRbacPath } = require('../utils/app-config-resolver');
15
15
  const { loadSystemFile, loadDatasourceFiles } = require('./external');
16
16
  const { loadVariables, loadRbac } = require('./helpers');
17
17
 
@@ -77,8 +77,8 @@ function extractAppMetadata(variables, appName) {
77
77
  */
78
78
  async function loadSystemWithRbac(appPath, schemaBasePath, systemFile) {
79
79
  const systemJson = await loadSystemFile(appPath, schemaBasePath, systemFile);
80
- const rbacPath = path.join(appPath, 'rbac.yaml');
81
- const rbac = loadRbac(rbacPath);
80
+ const rbacPath = resolveRbacPath(appPath);
81
+ const rbac = rbacPath ? loadRbac(rbacPath) : null;
82
82
  mergeRbacIntoSystemJson(systemJson, rbac);
83
83
  return systemJson;
84
84
  }
@@ -196,13 +196,15 @@ async function writeSplitExternalSchemaFiles({ outputDir, systemKey, application
196
196
  await writeYamlFile(rbacPath, rbac, { indent: 2, lineWidth: -1 });
197
197
  }
198
198
 
199
+ const authType = application.authentication?.type || application.authentication?.method;
199
200
  const readmeContent = generateExternalReadmeContent({
200
201
  appName: systemKey,
201
202
  systemKey,
202
203
  systemType: application.type,
203
204
  displayName: application.displayName,
204
205
  description: application.description,
205
- datasources: dataSources
206
+ datasources: dataSources,
207
+ authType
206
208
  });
207
209
  const readmePath = path.join(outputDir, 'README.md');
208
210
  await fs.promises.writeFile(readmePath, readmeContent, 'utf8');