@aifabrix/builder 2.41.0 → 2.42.1

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 (142) hide show
  1. package/.cursor/rules/docs-rules.mdc +30 -0
  2. package/README.md +2 -2
  3. package/integration/hubspot/README.md +11 -5
  4. package/integration/hubspot/application.json +54 -0
  5. package/integration/hubspot/create-hubspot.js +9 -136
  6. package/integration/hubspot/env.template +3 -4
  7. package/integration/hubspot/hubspot-datasource-company.json +343 -5
  8. package/integration/hubspot/hubspot-datasource-contact.json +413 -5
  9. package/integration/hubspot/hubspot-datasource-deal.json +341 -4
  10. package/integration/hubspot/hubspot-datasource-users.json +116 -0
  11. package/integration/hubspot/hubspot-deploy.json +1250 -108
  12. package/integration/hubspot/hubspot-system.json +15 -32
  13. package/integration/hubspot/test-dataplane-down-tests.js +17 -16
  14. package/integration/hubspot/test-dataplane-down.js +2 -2
  15. package/jest.config.manual.js +2 -1
  16. package/lib/api/external-test.api.js +111 -0
  17. package/lib/api/index.js +42 -19
  18. package/lib/api/pipeline.api.js +66 -120
  19. package/lib/api/types/pipeline.types.js +37 -0
  20. package/lib/api/wizard-platform.api.js +61 -0
  21. package/lib/api/wizard.api.js +36 -2
  22. package/lib/app/config.js +23 -11
  23. package/lib/app/index.js +5 -3
  24. package/lib/app/prompts.js +46 -31
  25. package/lib/app/readme.js +11 -4
  26. package/lib/app/run-env-compose.js +64 -1
  27. package/lib/app/run-helpers.js +1 -1
  28. package/lib/app/show-display.js +1 -1
  29. package/lib/cli/setup-app.js +45 -14
  30. package/lib/cli/setup-credential-deployment.js +31 -6
  31. package/lib/cli/setup-dev.js +27 -0
  32. package/lib/cli/setup-environment.js +12 -4
  33. package/lib/cli/setup-external-system.js +19 -4
  34. package/lib/cli/setup-infra.js +54 -14
  35. package/lib/cli/setup-utility.js +117 -21
  36. package/lib/commands/auth-config.js +22 -12
  37. package/lib/commands/credential-env.js +162 -0
  38. package/lib/commands/credential-list.js +17 -22
  39. package/lib/commands/credential-push.js +96 -0
  40. package/lib/commands/datasource.js +77 -6
  41. package/lib/commands/dev-init.js +39 -1
  42. package/lib/commands/repair-auth-config.js +99 -0
  43. package/lib/commands/repair-datasource-keys.js +208 -0
  44. package/lib/commands/repair-datasource.js +235 -0
  45. package/lib/commands/repair-env-template.js +348 -0
  46. package/lib/commands/repair-internal.js +85 -0
  47. package/lib/commands/repair-rbac.js +158 -0
  48. package/lib/commands/repair.js +518 -0
  49. package/lib/commands/secrets-set.js +6 -0
  50. package/lib/commands/test-e2e-external.js +165 -0
  51. package/lib/commands/up-dataplane.js +90 -6
  52. package/lib/commands/upload.js +71 -40
  53. package/lib/commands/wizard-core-helpers.js +230 -5
  54. package/lib/commands/wizard-core.js +68 -29
  55. package/lib/commands/wizard-dataplane.js +1 -1
  56. package/lib/commands/wizard-entity-selection.js +43 -0
  57. package/lib/commands/wizard-headless.js +49 -5
  58. package/lib/commands/wizard-helpers.js +7 -3
  59. package/lib/commands/wizard.js +93 -64
  60. package/lib/core/config.js +7 -1
  61. package/lib/core/secrets.js +33 -12
  62. package/lib/datasource/deploy.js +12 -3
  63. package/lib/datasource/test-e2e.js +219 -0
  64. package/lib/datasource/test-integration.js +154 -0
  65. package/lib/deployment/deployer.js +7 -5
  66. package/lib/external-system/download-helpers.js +3 -1
  67. package/lib/external-system/download.js +182 -204
  68. package/lib/external-system/generator.js +204 -56
  69. package/lib/external-system/test-execution.js +2 -1
  70. package/lib/external-system/test-system-level.js +73 -0
  71. package/lib/external-system/test.js +51 -18
  72. package/lib/generator/external-controller-manifest.js +29 -2
  73. package/lib/generator/external-schema-utils.js +4 -2
  74. package/lib/generator/external.js +10 -3
  75. package/lib/generator/index.js +4 -1
  76. package/lib/generator/split-readme.js +1 -0
  77. package/lib/generator/split-variables.js +7 -1
  78. package/lib/generator/split.js +194 -54
  79. package/lib/generator/wizard-prompts-secondary.js +326 -0
  80. package/lib/generator/wizard-prompts.js +105 -106
  81. package/lib/generator/wizard-readme.js +91 -0
  82. package/lib/generator/wizard.js +180 -179
  83. package/lib/infrastructure/compose.js +11 -1
  84. package/lib/infrastructure/index.js +11 -3
  85. package/lib/infrastructure/services.js +22 -11
  86. package/lib/schema/application-schema.json +8 -5
  87. package/lib/schema/external-datasource.schema.json +49 -26
  88. package/lib/schema/external-system.schema.json +82 -6
  89. package/lib/schema/wizard-config.schema.json +23 -1
  90. package/lib/utils/api.js +38 -10
  91. package/lib/utils/auth-headers.js +8 -7
  92. package/lib/utils/compose-generator.js +1 -1
  93. package/lib/utils/compose-handlebars-helpers.js +11 -0
  94. package/lib/utils/config-format-preference.js +51 -0
  95. package/lib/utils/config-format.js +36 -0
  96. package/lib/utils/configuration-env-resolver.js +179 -0
  97. package/lib/utils/credential-display.js +83 -0
  98. package/lib/utils/credential-secrets-env.js +115 -25
  99. package/lib/utils/dataplane-pipeline-warning.js +28 -0
  100. package/lib/utils/deployment-validation-helpers.js +4 -4
  101. package/lib/utils/dev-ca-install.js +139 -0
  102. package/lib/utils/env-copy.js +23 -3
  103. package/lib/utils/error-formatters/http-status-errors.js +0 -1
  104. package/lib/utils/error-formatters/permission-errors.js +0 -1
  105. package/lib/utils/error-formatters/validation-errors.js +0 -1
  106. package/lib/utils/external-readme.js +89 -30
  107. package/lib/utils/external-system-display.js +59 -1
  108. package/lib/utils/external-system-test-helpers.js +21 -8
  109. package/lib/utils/external-system-validators.js +3 -0
  110. package/lib/utils/file-upload.js +20 -50
  111. package/lib/utils/help-builder.js +1 -0
  112. package/lib/utils/infra-status.js +50 -44
  113. package/lib/utils/local-secrets.js +5 -5
  114. package/lib/utils/paths.js +85 -4
  115. package/lib/utils/secrets-canonical.js +93 -0
  116. package/lib/utils/secrets-generator.js +20 -0
  117. package/lib/utils/secrets-helpers.js +75 -89
  118. package/lib/utils/test-log-writer.js +56 -0
  119. package/lib/utils/token-manager.js +24 -32
  120. package/lib/validation/env-template-auth.js +157 -0
  121. package/lib/validation/env-template-kv.js +41 -0
  122. package/lib/validation/external-manifest-validator.js +25 -0
  123. package/lib/validation/external-system-auth-rules.js +86 -0
  124. package/lib/validation/validate-batch.js +149 -0
  125. package/lib/validation/validate-datasource-keys-api.js +33 -0
  126. package/lib/validation/validate-display.js +94 -16
  127. package/lib/validation/validate.js +25 -12
  128. package/lib/validation/validator.js +7 -9
  129. package/lib/validation/wizard-datasource-validation.js +50 -0
  130. package/package.json +7 -2
  131. package/templates/applications/dataplane/application.yaml +1 -1
  132. package/templates/applications/dataplane/env.template +5 -5
  133. package/templates/applications/dataplane/rbac.yaml +2 -2
  134. package/templates/applications/miso-controller/env.template +1 -1
  135. package/templates/external-system/README.md.hbs +75 -22
  136. package/templates/external-system/deploy.js.hbs +4 -2
  137. package/templates/external-system/external-datasource.yaml.hbs +217 -0
  138. package/templates/external-system/external-system.json.hbs +1 -18
  139. package/templates/infra/compose.yaml.hbs +6 -0
  140. package/templates/python/docker-compose.hbs +4 -4
  141. package/templates/typescript/docker-compose.hbs +4 -4
  142. package/integration/hubspot/application.yaml +0 -37
@@ -247,48 +247,29 @@ async function tryClientTokenAuth(environment, appName, controllerUrl) {
247
247
  const clientToken = await getOrRefreshClientToken(environment, appName, controllerUrl);
248
248
  if (clientToken && clientToken.token) {
249
249
  return {
250
- type: 'bearer',
250
+ type: 'client-token',
251
251
  token: clientToken.token,
252
252
  controller: clientToken.controller
253
253
  };
254
254
  }
255
255
  } catch {
256
- // Client token unavailable; getDeploymentAuth will try client credentials next (no warning here to avoid misleading output when env credentials succeed)
257
- }
258
- return null;
259
- }
260
-
261
- /**
262
- * Tries to get client credentials for deployment auth
263
- * @async
264
- * @function tryClientCredentialsAuth
265
- * @param {string} appName - Application name
266
- * @param {string} controllerUrl - Controller URL
267
- * @returns {Promise<Object|null>} Auth config with client credentials or null
268
- */
269
- async function tryClientCredentialsAuth(appName, controllerUrl) {
270
- const credentials = await loadClientCredentials(appName);
271
- if (credentials && credentials.clientId && credentials.clientSecret) {
272
- return {
273
- type: 'client-credentials',
274
- clientId: credentials.clientId,
275
- clientSecret: credentials.clientSecret,
276
- controller: controllerUrl
277
- };
256
+ // Client token unavailable; getDeploymentAuth will try exchanging credentials for token (no warning here to avoid misleading output when refresh succeeds)
278
257
  }
279
258
  return null;
280
259
  }
281
260
 
282
261
  /**
283
262
  * Get deployment authentication configuration with priority:
284
- * 1. Device token (Bearer) - for user-level audit tracking (preferred)
285
- * 2. Client token (Bearer) - for application-level authentication
286
- * 3. Client credentials (x-client-id/x-client-secret) - direct credential authentication
263
+ * 1. Device token → type 'bearer' (user token) send as Authorization: Bearer
264
+ * 2. Client token → type 'client-token' (application token) send as x-client-token header
265
+ * 3. When no token available: if client credentials exist, exchange for client token and return type 'client-token'.
266
+ *
267
+ * x-client-id/x-client-secret are used only at the token-issuing endpoint (e.g. POST /api/v1/auth/token).
287
268
  *
288
269
  * @param {string} controllerUrl - Controller URL
289
270
  * @param {string} environment - Environment key
290
271
  * @param {string} appName - Application name
291
- * @returns {Promise<{type: 'bearer'|'client-credentials', token?: string, clientId?: string, clientSecret?: string, controller: string}>} Auth configuration
272
+ * @returns {Promise<{type: 'bearer'|'client-token', token: string, controller: string}>} Auth config: bearer = user token, client-token = app token (x-client-token header)
292
273
  * @throws {Error} If no authentication method is available
293
274
  */
294
275
  async function getDeploymentAuth(controllerUrl, environment, appName) {
@@ -306,10 +287,21 @@ async function getDeploymentAuth(controllerUrl, environment, appName) {
306
287
  return clientTokenAuth;
307
288
  }
308
289
 
309
- // Priority 3: Use client credentials directly
310
- const credentialsAuth = await tryClientCredentialsAuth(appName, controllerUrl);
311
- if (credentialsAuth) {
312
- return credentialsAuth;
290
+ // Priority 3: Exchange client credentials for a token (never return client-credentials for app endpoints)
291
+ const credentials = await loadClientCredentials(appName);
292
+ if (credentials && credentials.clientId && credentials.clientSecret) {
293
+ try {
294
+ const refreshed = await refreshClientToken(environment, appName, controllerUrl);
295
+ if (refreshed && refreshed.token) {
296
+ return {
297
+ type: 'client-token',
298
+ token: refreshed.token,
299
+ controller: controllerUrl
300
+ };
301
+ }
302
+ } catch {
303
+ // Refresh failed; fall through to throw below
304
+ }
313
305
  }
314
306
 
315
307
  throw new Error(`No authentication method available. Run 'aifabrix login' for device token, or add credentials to ~/.aifabrix/secrets.local.yaml as '${appName}-client-idKeyVault' and '${appName}-client-secretKeyVault'`);
@@ -337,7 +329,7 @@ async function extractClientCredentials(authConfig, appKey, envKey, _options = {
337
329
  };
338
330
  }
339
331
 
340
- if (authConfig.type === 'bearer') {
332
+ if (authConfig.type === 'bearer' || authConfig.type === 'client-token') {
341
333
  if (authConfig.clientId && authConfig.clientSecret) {
342
334
  return {
343
335
  clientId: authConfig.clientId,
@@ -0,0 +1,157 @@
1
+ /**
2
+ * env.template authentication kv:// validation helpers
3
+ *
4
+ * Collects required kv paths from system configs and extracts kv paths from env.template
5
+ * for validating that external integrations have all authentication secrets in env.template.
6
+ *
7
+ * @fileoverview Auth kv validation for env.template
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const { loadExternalIntegrationConfig, loadSystemFile } = require('../generator/external');
13
+
14
+ /**
15
+ * Extracts all kv:// paths from env.template content (RHS of VAR=value lines).
16
+ * Uses same regex as validateKvReferencesInLines.
17
+ *
18
+ * @function extractKvPathsFromEnvTemplate
19
+ * @param {string} content - env.template file content
20
+ * @returns {Set<string>} Set of kv:// paths found (e.g. kv://hubspot/client-id)
21
+ *
22
+ * @example
23
+ * const paths = extractKvPathsFromEnvTemplate('CLIENT_ID=kv://hubspot/client-id\nPORT=3000');
24
+ * // paths has 'kv://hubspot/client-id'
25
+ */
26
+ function extractKvPathsFromEnvTemplate(content) {
27
+ const paths = new Set();
28
+ const lines = content.split('\n');
29
+ for (const line of lines) {
30
+ const trimmed = line.trim();
31
+ if (!trimmed || trimmed.startsWith('#')) continue;
32
+ if (!trimmed.includes('=')) continue;
33
+ const [_key, value] = trimmed.split('=', 2);
34
+ const val = (value || '').trim();
35
+ const matches = val.match(/kv:\/\/[^\s]*/g) || [];
36
+ for (const fullRef of matches) {
37
+ paths.add(fullRef);
38
+ }
39
+ }
40
+ return paths;
41
+ }
42
+
43
+ /**
44
+ * Extracts kv:// paths from commented lines in env.template (e.g. # KEY=kv://path or # kv://path).
45
+ * Used to treat commented-out keys as intentionally disabled for auth-coverage validation.
46
+ * Scans the whole line after '#' so both key=value and bare/commented refs are recognized.
47
+ *
48
+ * @function extractKvPathsFromCommentedLines
49
+ * @param {string} content - env.template file content
50
+ * @returns {Set<string>} Set of kv:// paths found in commented lines (e.g. kv://avoma/apikey)
51
+ */
52
+ function extractKvPathsFromCommentedLines(content) {
53
+ const paths = new Set();
54
+ const lines = content.split('\n');
55
+ for (const line of lines) {
56
+ const trimmed = line.trim();
57
+ if (!trimmed.startsWith('#')) continue;
58
+ const afterHash = trimmed.slice(1).trim();
59
+ const matches = afterHash.match(/kv:\/\/[^\s]*/g) || [];
60
+ for (const fullRef of matches) {
61
+ paths.add(fullRef);
62
+ }
63
+ }
64
+ return paths;
65
+ }
66
+
67
+ /**
68
+ * Returns true if the required path is present in the set or matches any entry case-insensitively.
69
+ * Used so commented-out keys match required paths regardless of kv path casing (e.g. apiKey vs apikey).
70
+ *
71
+ * @function setHasPathIgnoreCase
72
+ * @param {Set<string>} pathSet - Set of kv paths (e.g. from commented lines)
73
+ * @param {string} requiredPath - Required path from authentication.security
74
+ * @returns {boolean} True if requiredPath is in pathSet or matches any element when lowercased
75
+ */
76
+ function setHasPathIgnoreCase(pathSet, requiredPath) {
77
+ if (pathSet.has(requiredPath)) return true;
78
+ const requiredLower = requiredPath.toLowerCase();
79
+ for (const p of pathSet) {
80
+ if (p.toLowerCase() === requiredLower) return true;
81
+ }
82
+ return false;
83
+ }
84
+
85
+ /**
86
+ * Collects required kv:// paths from authentication.security of all system configs.
87
+ * For external integrations only. On config load failure, returns empty set and optional warning.
88
+ *
89
+ * @async
90
+ * @function collectRequiredAuthKvPaths
91
+ * @param {string} appPath - Application directory path
92
+ * @param {Object} [options] - Options (reserved)
93
+ * @returns {Promise<{ requiredPaths: Set<string>, warning?: string }>} Required kv paths and optional warning
94
+ *
95
+ * @example
96
+ * const { requiredPaths } = await collectRequiredAuthKvPaths('/path/to/integration/hubspot');
97
+ * // requiredPaths has kv:// paths from authentication.security
98
+ */
99
+ async function collectRequiredAuthKvPaths(appPath, _options = {}) {
100
+ const requiredPaths = new Set();
101
+ try {
102
+ const { schemaBasePath, systemFiles } = await loadExternalIntegrationConfig(appPath);
103
+ for (const systemFileName of systemFiles) {
104
+ const systemJson = await loadSystemFile(appPath, schemaBasePath, systemFileName);
105
+ const security = systemJson.authentication?.security;
106
+ if (!security || typeof security !== 'object') continue;
107
+ for (const val of Object.values(security)) {
108
+ if (typeof val === 'string' && /^kv:\/\/.+/.test(val)) {
109
+ requiredPaths.add(val);
110
+ }
111
+ }
112
+ }
113
+ return { requiredPaths };
114
+ } catch (error) {
115
+ return {
116
+ requiredPaths: new Set(),
117
+ warning: `Could not validate auth kv coverage (skip auth check): ${error.message}`
118
+ };
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Validates that env.template covers all authentication.security kv paths for external apps.
124
+ * Modifies errors and warnings in place.
125
+ *
126
+ * @async
127
+ * @function validateAuthKvCoverage
128
+ * @param {string} appPath - Application path
129
+ * @param {string} content - env.template content
130
+ * @param {string[]} errors - Errors array to push to
131
+ * @param {string[]} warnings - Warnings array to push to
132
+ * @param {Object} [options] - Options
133
+ */
134
+ async function validateAuthKvCoverage(appPath, content, errors, warnings, options = {}) {
135
+ const authResult = await collectRequiredAuthKvPaths(appPath, options);
136
+ if (authResult.warning) warnings.push(authResult.warning);
137
+ if (authResult.requiredPaths.size === 0) return;
138
+ const actualPaths = extractKvPathsFromEnvTemplate(content);
139
+ const commentedPaths = extractKvPathsFromCommentedLines(content);
140
+ for (const requiredPath of authResult.requiredPaths) {
141
+ const inActive = actualPaths.has(requiredPath);
142
+ const inCommented = setHasPathIgnoreCase(commentedPaths, requiredPath);
143
+ if (!inActive && !inCommented) {
144
+ errors.push(
145
+ `env.template: Missing required authentication secret (required by authentication.security): add a variable with value ${requiredPath}`
146
+ );
147
+ }
148
+ }
149
+ }
150
+
151
+ module.exports = {
152
+ extractKvPathsFromEnvTemplate,
153
+ extractKvPathsFromCommentedLines,
154
+ setHasPathIgnoreCase,
155
+ collectRequiredAuthKvPaths,
156
+ validateAuthKvCoverage
157
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * env.template kv:// reference validation (syntax only).
3
+ * Skips comment and empty lines. Used by validator.validateEnvTemplate.
4
+ *
5
+ * @fileoverview Kv reference validation for env.template lines
6
+ * @author AI Fabrix Team
7
+ * @version 2.0.0
8
+ */
9
+
10
+ /**
11
+ * Validates kv:// references in env.template lines; pushes errors into the given array.
12
+ * Skips empty and comment (#) lines.
13
+ *
14
+ * @function validateKvReferencesInLines
15
+ * @param {string[]} lines - Lines of env.template content
16
+ * @param {string[]} errors - Array to push error messages into
17
+ */
18
+ function validateKvReferencesInLines(lines, errors) {
19
+ lines.forEach((line, index) => {
20
+ const trimmed = line.trim();
21
+ if (!trimmed || trimmed.startsWith('#')) {
22
+ return;
23
+ }
24
+ const matches = line.match(/kv:\/\/[^\s]*/g) || [];
25
+ for (const fullRef of matches) {
26
+ const pathMatch = fullRef.match(/^kv:\/\/(.*)$/);
27
+ const pathStr = pathMatch ? pathMatch[1] : '';
28
+ const invalid = !pathStr || pathStr.startsWith('/') || pathStr.endsWith('/');
29
+ if (invalid) {
30
+ const hint = !pathStr
31
+ ? 'path is empty (use kv://secret-key)'
32
+ : pathStr.startsWith('/')
33
+ ? 'path must not start with / (use kv://secret-key not kv:///secret-key)'
34
+ : 'path must not end with / (use kv://secret-key not kv://secret-key/)';
35
+ errors.push(`env.template line ${index + 1}: Invalid kv:// reference "${fullRef}" - ${hint}`);
36
+ }
37
+ }
38
+ });
39
+ }
40
+
41
+ module.exports = { validateKvReferencesInLines };
@@ -156,6 +156,30 @@ function validateRequiredFields(manifest, errors) {
156
156
  });
157
157
  }
158
158
 
159
+ /**
160
+ * Validates that each datasource's systemKey matches the application system key.
161
+ * Matches dataplane upload API wording so integrators recognize the error.
162
+ *
163
+ * @function validateDatasourceSystemKeyAlignment
164
+ * @param {Object} manifest - Manifest object with system and dataSources
165
+ * @param {Array} errors - Errors array to append to
166
+ * @returns {void}
167
+ */
168
+ function validateDatasourceSystemKeyAlignment(manifest, errors) {
169
+ const systemKey = manifest.system?.key;
170
+ if (!manifest.dataSources || !Array.isArray(manifest.dataSources) || systemKey === null || systemKey === undefined || systemKey === '') {
171
+ return;
172
+ }
173
+ manifest.dataSources.forEach((datasource) => {
174
+ if (datasource.systemKey !== systemKey) {
175
+ const dsKey = datasource.key || 'unknown';
176
+ errors.push(
177
+ `Data source '${dsKey}' systemKey does not match application system key (expected '${systemKey}', got '${datasource.systemKey}')`
178
+ );
179
+ }
180
+ });
181
+ }
182
+
159
183
  /**
160
184
  * Validates controller deployment manifest for external systems
161
185
  * Validates manifest structure and inline system/dataSources against their schemas
@@ -187,6 +211,7 @@ async function validateControllerManifest(manifest) {
187
211
  validateManifestStructure(manifest, ajv, applicationSchema, errors);
188
212
  validateInlineSystem(manifest, ajv, externalSystemSchema, errors);
189
213
  validateDatasources(manifest, ajv, externalDatasourceSchema, errors, warnings);
214
+ validateDatasourceSystemKeyAlignment(manifest, errors);
190
215
  validateConditionalRequirements(manifest, errors, warnings);
191
216
  validateRequiredFields(manifest, errors);
192
217
 
@@ -0,0 +1,86 @@
1
+ /**
2
+ * External system authentication rules (OAuth2/AAD grantType, authorizationUrl, configuration).
3
+ * Used after schema validation for external system files.
4
+ *
5
+ * @fileoverview OAuth2/AAD and configuration rules for external system configs
6
+ * @author AI Fabrix Team
7
+ * @version 2.0.0
8
+ */
9
+
10
+ const VALID_GRANT_TYPES = ['client_credentials', 'authorization_code'];
11
+
12
+ /** Standard auth variable names (credential parameters supplied at runtime). Not allowed in configuration except BASEURL when auth is none. */
13
+ const STANDARD_AUTH_VAR_NAMES = new Set([
14
+ 'baseurl', 'clientid', 'clientsecret', 'tokenurl', 'apikey', 'username', 'password'
15
+ ]);
16
+
17
+ function trimVar(value) {
18
+ return (value !== undefined && value !== null ? String(value).trim() : '');
19
+ }
20
+
21
+ function isOAuth2OrAad(method) {
22
+ const m = (method && String(method).toLowerCase()) || '';
23
+ return m === 'oauth2' || m === 'aad';
24
+ }
25
+
26
+ /**
27
+ * Validates OAuth2/AAD grantType and conditional authorizationUrl for external system files.
28
+ * When method is oauth2 or aad: grantType (if present) must be client_credentials or authorization_code;
29
+ * when effective grant is authorization_code (explicit or default), authorizationUrl is required.
30
+ *
31
+ * @function validateOAuth2GrantTypeAndAuthorizationUrl
32
+ * @param {Object} parsed - Parsed external system object (must have authentication.variables when method is oauth2/aad)
33
+ * @param {string[]} errors - Array to push validation error messages into
34
+ */
35
+ function validateOAuth2GrantTypeAndAuthorizationUrl(parsed, errors) {
36
+ const auth = parsed?.authentication;
37
+ const variables = auth?.variables;
38
+ if (!variables || typeof variables !== 'object' || !isOAuth2OrAad(auth.method)) {
39
+ return;
40
+ }
41
+
42
+ const grantType = trimVar(variables.grantType);
43
+ if (grantType !== '' && !VALID_GRANT_TYPES.includes(grantType)) {
44
+ errors.push('authentication.variables.grantType must be one of: client_credentials, authorization_code');
45
+ return;
46
+ }
47
+
48
+ const effectiveGrant = grantType || 'authorization_code';
49
+ if (effectiveGrant === 'authorization_code' && trimVar(variables.authorizationUrl) === '') {
50
+ errors.push('authentication.variables.authorizationUrl is required when grantType is authorization_code or omitted');
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Validates that the external system configuration array does not contain standard auth variable names.
56
+ * BASEURL, CLIENTID, CLIENTSECRET, TOKENURL, APIKEY, USERNAME, PASSWORD are credential parameters
57
+ * supplied from the selected credential at runtime and must not be in configuration. Exception:
58
+ * BASEURL is only allowed in configuration when authentication.method is 'none'.
59
+ *
60
+ * @param {Object} parsed - Parsed external system object
61
+ * @param {string[]} errors - Array to push validation error messages into
62
+ */
63
+ function validateConfigurationNoStandardAuthVariables(parsed, errors) {
64
+ const config = parsed?.configuration;
65
+ if (!Array.isArray(config) || config.length === 0) return;
66
+ const method = (parsed?.authentication?.method && String(parsed.authentication.method).toLowerCase()) || '';
67
+ const authNone = method === 'none';
68
+ const allowedWhenNone = new Set(['baseurl']);
69
+ for (const item of config) {
70
+ const name = (item?.name && String(item.name).trim()) || '';
71
+ if (!name) continue;
72
+ const nameLower = name.toLowerCase();
73
+ if (!STANDARD_AUTH_VAR_NAMES.has(nameLower)) continue;
74
+ if (authNone && allowedWhenNone.has(nameLower)) continue;
75
+ errors.push(
76
+ `configuration must not contain standard auth variable '${name}'. ` +
77
+ 'Standard auth variables (BASEURL, CLIENTID, CLIENTSECRET, TOKENURL, APIKEY, USERNAME, PASSWORD) are supplied from the selected credential at runtime. ' +
78
+ 'BASEURL is only allowed in configuration when authentication.method is \'none\'.'
79
+ );
80
+ }
81
+ }
82
+
83
+ module.exports = {
84
+ validateOAuth2GrantTypeAndAuthorizationUrl,
85
+ validateConfigurationNoStandardAuthVariables
86
+ };
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Batch validation helpers for validate --integration / --builder.
3
+ * @fileoverview Builds batch results and runs validation across multiple apps
4
+ * @author AI Fabrix Team
5
+ * @version 2.0.0
6
+ */
7
+
8
+ const { listIntegrationAppNames, listBuilderAppNames } = require('../utils/paths');
9
+
10
+ /**
11
+ * Collects error strings from a single validation result (various shapes).
12
+ * @param {Object} result - Single-app validation result
13
+ * @returns {string[]} Error messages
14
+ */
15
+ function collectResultErrors(result) {
16
+ const errs = [];
17
+ if (result.errors && Array.isArray(result.errors)) {
18
+ errs.push(...result.errors);
19
+ }
20
+ if (result.application && result.application.errors && Array.isArray(result.application.errors)) {
21
+ errs.push(...result.application.errors);
22
+ }
23
+ if (result.steps) {
24
+ ['application', 'components', 'manifest'].forEach(step => {
25
+ const s = result.steps[step];
26
+ if (s && s.errors && Array.isArray(s.errors)) {
27
+ errs.push(...s.errors);
28
+ }
29
+ });
30
+ }
31
+ return errs;
32
+ }
33
+
34
+ /**
35
+ * Collects warning strings from a single validation result.
36
+ * @param {Object} result - Single-app validation result
37
+ * @returns {string[]} Warning messages
38
+ */
39
+ function collectResultWarnings(result) {
40
+ const w = [];
41
+ if (result.warnings && Array.isArray(result.warnings)) {
42
+ w.push(...result.warnings);
43
+ }
44
+ if (result.application && result.application.warnings && Array.isArray(result.application.warnings)) {
45
+ w.push(...result.application.warnings);
46
+ }
47
+ if (result.steps) {
48
+ ['application', 'components', 'manifest'].forEach(step => {
49
+ const s = result.steps[step];
50
+ if (s && s.warnings && Array.isArray(s.warnings)) {
51
+ w.push(...s.warnings);
52
+ }
53
+ });
54
+ }
55
+ return w;
56
+ }
57
+
58
+ /**
59
+ * Builds batch result from per-app results. Each item has appName and either result or error.
60
+ * @param {Array<{appName: string, result?: Object, error?: string}>} items - Per-app results
61
+ * @returns {{ batch: true, valid: boolean, results: Array, errors: string[], warnings: string[] }}
62
+ */
63
+ function buildBatchResult(items) {
64
+ const errors = [];
65
+ const warnings = [];
66
+ items.forEach(item => {
67
+ if (item.error) {
68
+ errors.push(`${item.appName}: ${item.error}`);
69
+ } else if (item.result) {
70
+ collectResultErrors(item.result).forEach(e => errors.push(`${item.appName}: ${e}`));
71
+ collectResultWarnings(item.result).forEach(w => warnings.push(`${item.appName}: ${w}`));
72
+ }
73
+ });
74
+ const valid = items.every(item => item.result && item.result.valid);
75
+ return {
76
+ batch: true,
77
+ valid,
78
+ results: items,
79
+ errors,
80
+ warnings
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Validates all apps under integration/ (each as external system).
86
+ * @async
87
+ * @param {Function} validateAppOrFile - validateAppOrFile(appName, options) from validate.js
88
+ * @param {Object} [options] - Validation options
89
+ * @returns {Promise<{ batch: true, valid: boolean, results: Array, errors: string[], warnings: string[] }>}
90
+ */
91
+ async function validateAllIntegrations(validateAppOrFile, options = {}) {
92
+ const names = listIntegrationAppNames();
93
+ const items = [];
94
+ for (const appName of names) {
95
+ try {
96
+ const result = await validateAppOrFile(appName, options);
97
+ items.push({ appName, result });
98
+ } catch (error) {
99
+ items.push({ appName, error: error.message || String(error) });
100
+ }
101
+ }
102
+ return buildBatchResult(items);
103
+ }
104
+
105
+ /**
106
+ * Validates all apps under builder/.
107
+ * @async
108
+ * @param {Function} validateAppOrFile - validateAppOrFile(appName, options) from validate.js
109
+ * @param {Object} [options] - Validation options
110
+ * @returns {Promise<{ batch: true, valid: boolean, results: Array, errors: string[], warnings: string[] }>}
111
+ */
112
+ async function validateAllBuilderApps(validateAppOrFile, options = {}) {
113
+ const names = listBuilderAppNames();
114
+ const items = [];
115
+ for (const appName of names) {
116
+ try {
117
+ const result = await validateAppOrFile(appName, options);
118
+ items.push({ appName, result });
119
+ } catch (error) {
120
+ items.push({ appName, error: error.message || String(error) });
121
+ }
122
+ }
123
+ return buildBatchResult(items);
124
+ }
125
+
126
+ /**
127
+ * Validates all integration and builder apps in one run.
128
+ * @async
129
+ * @param {Function} validateAppOrFile - validateAppOrFile(appName, options) from validate.js
130
+ * @param {Object} [options] - Validation options
131
+ * @returns {Promise<{ batch: true, valid: boolean, results: Array, errors: string[], warnings: string[] }>}
132
+ */
133
+ async function validateAll(validateAppOrFile, options = {}) {
134
+ const [integrationResult, builderResult] = await Promise.all([
135
+ validateAllIntegrations(validateAppOrFile, options),
136
+ validateAllBuilderApps(validateAppOrFile, options)
137
+ ]);
138
+ const mergedResults = [...integrationResult.results, ...builderResult.results];
139
+ return buildBatchResult(mergedResults);
140
+ }
141
+
142
+ module.exports = {
143
+ buildBatchResult,
144
+ collectResultErrors,
145
+ collectResultWarnings,
146
+ validateAllIntegrations,
147
+ validateAllBuilderApps,
148
+ validateAll
149
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @fileoverview Async validation of datasourceKeys against dataplane API
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ const { getPlatformDetails } = require('../api/wizard.api');
8
+ const { validateDatasourceKeysForPlatform } = require('./wizard-datasource-validation');
9
+
10
+ /**
11
+ * Validate datasourceKeys against platform's available datasources; throws if invalid
12
+ * @async
13
+ * @param {string} dataplaneUrl - Dataplane URL
14
+ * @param {Object} authConfig - Auth config
15
+ * @param {string} platformKey - Platform key
16
+ * @param {string[]} datasourceKeys - Datasource keys to validate
17
+ * @throws {Error} If any key is invalid
18
+ */
19
+ async function validateDatasourceKeysBeforePlatformConfig(dataplaneUrl, authConfig, platformKey, datasourceKeys) {
20
+ if (!Array.isArray(datasourceKeys) || datasourceKeys.length === 0) return;
21
+ const platformDetails = await getPlatformDetails(dataplaneUrl, authConfig, platformKey);
22
+ const datasources = platformDetails?.data?.datasources ?? platformDetails?.datasources ?? [];
23
+ const { valid, invalidKeys } = validateDatasourceKeysForPlatform(datasourceKeys, datasources);
24
+ if (!valid) {
25
+ const availableKeys = datasources.map(d => d.key).filter(Boolean);
26
+ throw new Error(
27
+ `Invalid datasource keys: [${invalidKeys.join(', ')}]. ` +
28
+ `Available for platform '${platformKey}': [${availableKeys.join(', ')}].`
29
+ );
30
+ }
31
+ }
32
+
33
+ module.exports = { validateDatasourceKeysBeforePlatformConfig };