@aifabrix/builder 2.40.2 → 2.42.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 (198) hide show
  1. package/.cursor/rules/docs-rules.mdc +30 -0
  2. package/README.md +7 -5
  3. package/integration/hubspot/README.md +8 -4
  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/integration/hubspot/test.js +1 -1
  16. package/jest.config.manual.js +2 -1
  17. package/lib/api/credential.api.js +40 -0
  18. package/lib/api/dev.api.js +423 -0
  19. package/lib/api/external-test.api.js +111 -0
  20. package/lib/api/index.js +42 -19
  21. package/lib/api/pipeline.api.js +66 -120
  22. package/lib/api/types/credential.types.js +23 -0
  23. package/lib/api/types/dev.types.js +140 -0
  24. package/lib/api/types/pipeline.types.js +37 -0
  25. package/lib/api/wizard-platform.api.js +61 -0
  26. package/lib/api/wizard.api.js +34 -1
  27. package/lib/app/config.js +44 -11
  28. package/lib/app/down.js +2 -1
  29. package/lib/app/index.js +12 -1
  30. package/lib/app/prompts.js +44 -29
  31. package/lib/app/push.js +36 -12
  32. package/lib/app/readme.js +9 -6
  33. package/lib/app/run-env-compose.js +264 -0
  34. package/lib/app/run-helpers.js +121 -118
  35. package/lib/app/run.js +148 -28
  36. package/lib/app/show-display.js +1 -1
  37. package/lib/app/show.js +5 -2
  38. package/lib/build/index.js +11 -3
  39. package/lib/cli/setup-app.js +172 -15
  40. package/lib/cli/setup-credential-deployment.js +31 -6
  41. package/lib/cli/setup-dev.js +206 -16
  42. package/lib/cli/setup-environment.js +16 -6
  43. package/lib/cli/setup-external-system.js +89 -24
  44. package/lib/cli/setup-infra.js +82 -15
  45. package/lib/cli/setup-secrets.js +52 -5
  46. package/lib/cli/setup-utility.js +129 -24
  47. package/lib/commands/app-install.js +172 -0
  48. package/lib/commands/app-shell.js +75 -0
  49. package/lib/commands/app-test.js +282 -0
  50. package/lib/commands/app.js +1 -1
  51. package/lib/commands/credential-env.js +162 -0
  52. package/lib/commands/credential-list.js +17 -22
  53. package/lib/commands/credential-push.js +96 -0
  54. package/lib/commands/datasource.js +77 -6
  55. package/lib/commands/dev-cli-handlers.js +141 -0
  56. package/lib/commands/dev-down.js +114 -0
  57. package/lib/commands/dev-init.js +347 -0
  58. package/lib/commands/repair-auth-config.js +99 -0
  59. package/lib/commands/repair-datasource-keys.js +208 -0
  60. package/lib/commands/repair-datasource.js +235 -0
  61. package/lib/commands/repair-env-template.js +348 -0
  62. package/lib/commands/repair-internal.js +85 -0
  63. package/lib/commands/repair-rbac.js +158 -0
  64. package/lib/commands/repair.js +507 -0
  65. package/lib/commands/secrets-list.js +118 -0
  66. package/lib/commands/secrets-remove.js +97 -0
  67. package/lib/commands/secrets-set.js +30 -17
  68. package/lib/commands/secrets-validate.js +50 -0
  69. package/lib/commands/test-e2e-external.js +165 -0
  70. package/lib/commands/up-dataplane.js +2 -2
  71. package/lib/commands/up-miso.js +0 -25
  72. package/lib/commands/upload.js +96 -40
  73. package/lib/commands/wizard-core-helpers.js +226 -4
  74. package/lib/commands/wizard-core.js +67 -29
  75. package/lib/commands/wizard-dataplane.js +1 -1
  76. package/lib/commands/wizard-entity-selection.js +43 -0
  77. package/lib/commands/wizard-headless.js +44 -5
  78. package/lib/commands/wizard-helpers.js +7 -3
  79. package/lib/commands/wizard.js +86 -64
  80. package/lib/core/admin-secrets.js +96 -0
  81. package/lib/core/config.js +7 -1
  82. package/lib/core/secrets-ensure.js +378 -0
  83. package/lib/core/secrets-env-write.js +157 -0
  84. package/lib/core/secrets.js +176 -89
  85. package/lib/datasource/deploy.js +12 -3
  86. package/lib/datasource/field-reference-validator.js +91 -0
  87. package/lib/datasource/test-e2e.js +219 -0
  88. package/lib/datasource/test-integration.js +154 -0
  89. package/lib/datasource/validate.js +21 -3
  90. package/lib/deployment/deployer.js +7 -5
  91. package/lib/deployment/environment-config.js +137 -0
  92. package/lib/deployment/environment.js +21 -98
  93. package/lib/deployment/push.js +32 -2
  94. package/lib/external-system/download.js +188 -203
  95. package/lib/external-system/generator.js +204 -56
  96. package/lib/external-system/test-auth.js +7 -3
  97. package/lib/external-system/test-execution.js +2 -1
  98. package/lib/external-system/test-system-level.js +73 -0
  99. package/lib/external-system/test.js +56 -19
  100. package/lib/generator/external-controller-manifest.js +29 -2
  101. package/lib/generator/external-schema-utils.js +1 -1
  102. package/lib/generator/external.js +10 -3
  103. package/lib/generator/index.js +177 -25
  104. package/lib/generator/split-readme.js +1 -0
  105. package/lib/generator/split-variables.js +7 -1
  106. package/lib/generator/split.js +194 -54
  107. package/lib/generator/wizard-prompts-secondary.js +294 -0
  108. package/lib/generator/wizard-prompts.js +105 -106
  109. package/lib/generator/wizard-readme.js +88 -0
  110. package/lib/generator/wizard.js +155 -158
  111. package/lib/infrastructure/compose.js +11 -1
  112. package/lib/infrastructure/helpers.js +103 -20
  113. package/lib/infrastructure/index.js +98 -12
  114. package/lib/infrastructure/services.js +88 -22
  115. package/lib/schema/application-schema.json +32 -8
  116. package/lib/schema/external-datasource.schema.json +49 -26
  117. package/lib/schema/external-system.schema.json +509 -411
  118. package/lib/schema/wizard-config.schema.json +16 -0
  119. package/lib/utils/api.js +41 -13
  120. package/lib/utils/app-register-auth.js +25 -3
  121. package/lib/utils/auth-headers.js +8 -7
  122. package/lib/utils/cli-utils.js +20 -0
  123. package/lib/utils/compose-generator.js +77 -76
  124. package/lib/utils/compose-handlebars-helpers.js +54 -0
  125. package/lib/utils/compose-vector-helper.js +18 -0
  126. package/lib/utils/config-format-preference.js +51 -0
  127. package/lib/utils/config-format.js +36 -0
  128. package/lib/utils/config-paths.js +127 -2
  129. package/lib/utils/configuration-env-resolver.js +179 -0
  130. package/lib/utils/credential-display.js +83 -0
  131. package/lib/utils/credential-secrets-env.js +357 -0
  132. package/lib/utils/dataplane-pipeline-warning.js +28 -0
  133. package/lib/utils/deployment-validation-helpers.js +4 -4
  134. package/lib/utils/dev-ca-install.js +139 -0
  135. package/lib/utils/dev-cert-helper.js +122 -0
  136. package/lib/utils/device-code-helpers.js +224 -0
  137. package/lib/utils/device-code.js +37 -336
  138. package/lib/utils/docker-build.js +40 -8
  139. package/lib/utils/env-copy.js +103 -13
  140. package/lib/utils/env-map.js +35 -5
  141. package/lib/utils/env-template.js +6 -5
  142. package/lib/utils/error-formatters/http-status-errors.js +20 -2
  143. package/lib/utils/error-formatters/permission-errors.js +0 -1
  144. package/lib/utils/error-formatters/validation-errors.js +0 -1
  145. package/lib/utils/external-readme.js +56 -29
  146. package/lib/utils/external-system-display.js +59 -1
  147. package/lib/utils/external-system-test-helpers.js +21 -8
  148. package/lib/utils/external-system-validators.js +3 -0
  149. package/lib/utils/file-upload.js +20 -50
  150. package/lib/utils/help-builder.js +16 -2
  151. package/lib/utils/infra-status.js +80 -45
  152. package/lib/utils/local-secrets.js +7 -52
  153. package/lib/utils/mutagen-install.js +195 -0
  154. package/lib/utils/mutagen.js +146 -0
  155. package/lib/utils/paths.js +128 -37
  156. package/lib/utils/port-resolver.js +28 -16
  157. package/lib/utils/remote-dev-auth.js +38 -0
  158. package/lib/utils/remote-docker-env.js +43 -0
  159. package/lib/utils/remote-secrets-loader.js +60 -0
  160. package/lib/utils/secrets-canonical.js +93 -0
  161. package/lib/utils/secrets-generator.js +114 -6
  162. package/lib/utils/secrets-helpers.js +108 -114
  163. package/lib/utils/secrets-path.js +2 -2
  164. package/lib/utils/secrets-utils.js +52 -1
  165. package/lib/utils/secrets-validation.js +84 -0
  166. package/lib/utils/ssh-key-helper.js +116 -0
  167. package/lib/utils/test-log-writer.js +56 -0
  168. package/lib/utils/token-manager-messages.js +90 -0
  169. package/lib/utils/token-manager.js +29 -36
  170. package/lib/utils/variable-transformer.js +3 -3
  171. package/lib/validation/env-template-auth.js +157 -0
  172. package/lib/validation/env-template-kv.js +41 -0
  173. package/lib/validation/external-manifest-validator.js +25 -0
  174. package/lib/validation/external-system-auth-rules.js +86 -0
  175. package/lib/validation/validate-batch.js +149 -0
  176. package/lib/validation/validate-datasource-keys-api.js +33 -0
  177. package/lib/validation/validate-display.js +94 -16
  178. package/lib/validation/validate.js +25 -12
  179. package/lib/validation/validator.js +72 -9
  180. package/lib/validation/wizard-datasource-validation.js +50 -0
  181. package/package.json +8 -3
  182. package/scripts/install-local.js +34 -15
  183. package/templates/README.md +0 -1
  184. package/templates/applications/README.md.hbs +4 -4
  185. package/templates/applications/dataplane/application.yaml +6 -5
  186. package/templates/applications/dataplane/env.template +15 -10
  187. package/templates/applications/dataplane/rbac.yaml +2 -2
  188. package/templates/applications/keycloak/env.template +2 -0
  189. package/templates/applications/miso-controller/application.yaml +1 -0
  190. package/templates/applications/miso-controller/env.template +12 -10
  191. package/templates/external-system/README.md.hbs +65 -25
  192. package/templates/external-system/deploy.js.hbs +4 -2
  193. package/templates/external-system/external-datasource.yaml.hbs +217 -0
  194. package/templates/external-system/external-system.json.hbs +1 -18
  195. package/templates/infra/compose.yaml.hbs +6 -0
  196. package/templates/python/docker-compose.hbs +49 -23
  197. package/templates/typescript/docker-compose.hbs +48 -22
  198. package/integration/hubspot/application.yaml +0 -37
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Token refresh failure message formatting and once-per-URL warning (used by token-manager).
3
+ * @fileoverview Token manager message helpers
4
+ * @author AI Fabrix Team
5
+ * @version 2.0.0
6
+ */
7
+
8
+ const config = require('../core/config');
9
+ const logger = require('./logger');
10
+
11
+ /** Network-style error messages that indicate controller unreachable (not token expiry). */
12
+ const NETWORK_ERROR_PATTERNS = [
13
+ 'fetch failed',
14
+ 'econnrefused',
15
+ 'enotfound',
16
+ 'etimedout',
17
+ 'network',
18
+ 'unreachable',
19
+ 'timed out'
20
+ ];
21
+
22
+ /** Controller URLs we have already logged a refresh-failure warning for this process. */
23
+ const refreshFailureWarnedUrls = new Set();
24
+
25
+ /** Controller URLs we have already logged a refresh-token-expired warning for this process. */
26
+ const refreshTokenExpiredWarnedUrls = new Set();
27
+
28
+ /**
29
+ * Reset warned state (for test isolation only). Not for production use.
30
+ */
31
+ function resetRefreshWarnedUrlsForTesting() {
32
+ refreshFailureWarnedUrls.clear();
33
+ refreshTokenExpiredWarnedUrls.clear();
34
+ }
35
+
36
+ /**
37
+ * Log "refresh token expired" once per controller URL per process (avoids duplicate messages when auth is tried multiple times).
38
+ * @param {string} controllerUrl - Controller URL (for dedupe key)
39
+ * @param {string} errorMessage - Full error message to log
40
+ */
41
+ function warnRefreshTokenExpiredOnce(controllerUrl, errorMessage) {
42
+ const key = (controllerUrl && typeof controllerUrl === 'string' && controllerUrl.trim())
43
+ ? config.normalizeControllerUrl(controllerUrl)
44
+ : '__no_url__';
45
+ if (refreshTokenExpiredWarnedUrls.has(key)) {
46
+ return;
47
+ }
48
+ refreshTokenExpiredWarnedUrls.add(key);
49
+ logger.warn(`Refresh token expired: ${errorMessage}`);
50
+ }
51
+
52
+ /**
53
+ * Returns a user-facing message for token refresh failure; adds a hint when the error looks like a connectivity issue.
54
+ * @param {string} errorMessage - Raw error message
55
+ * @param {string} [controllerUrl] - Controller URL for the hint
56
+ * @returns {string} Message to log
57
+ */
58
+ function formatRefreshFailureMessage(errorMessage, controllerUrl) {
59
+ const lower = (errorMessage || '').toLowerCase();
60
+ const isNetwork = NETWORK_ERROR_PATTERNS.some(p => lower.includes(p));
61
+ const hint = isNetwork
62
+ ? (controllerUrl
63
+ ? ` The controller at ${controllerUrl} may be unreachable—ensure it is running and try again, or run 'aifabrix login' once it is available.`
64
+ : ' The controller may be unreachable—ensure it is running and try again, or run \'aifabrix login\' once it is available.')
65
+ : '';
66
+ return `${errorMessage}${hint}`;
67
+ }
68
+
69
+ /**
70
+ * Log device token refresh failure once per controller URL per process.
71
+ * @param {string} controllerUrl - Controller URL (for dedupe key and message)
72
+ * @param {string} errorMessage - Raw error message
73
+ */
74
+ function warnRefreshFailureOnce(controllerUrl, errorMessage) {
75
+ const key = (controllerUrl && typeof controllerUrl === 'string' && controllerUrl.trim())
76
+ ? config.normalizeControllerUrl(controllerUrl)
77
+ : '__no_url__';
78
+ if (refreshFailureWarnedUrls.has(key)) {
79
+ return;
80
+ }
81
+ refreshFailureWarnedUrls.add(key);
82
+ logger.warn(`Failed to refresh device token: ${formatRefreshFailureMessage(errorMessage, controllerUrl)}`);
83
+ }
84
+
85
+ module.exports = {
86
+ formatRefreshFailureMessage,
87
+ warnRefreshFailureOnce,
88
+ warnRefreshTokenExpiredOnce,
89
+ resetRefreshWarnedUrlsForTesting
90
+ };
@@ -19,6 +19,7 @@ const {
19
19
  refreshClientToken,
20
20
  refreshDeviceToken
21
21
  } = require('./token-manager-refresh');
22
+ const { warnRefreshFailureOnce, warnRefreshTokenExpiredOnce } = require('./token-manager-messages');
22
23
 
23
24
  function getSecretsFilePath() {
24
25
  return path.join(pathsUtil.getAifabrixHome(), 'secrets.local.yaml');
@@ -185,9 +186,9 @@ async function getOrRefreshDeviceToken(controllerUrl) {
185
186
  // Refresh failed - check if it's a refresh token expiry
186
187
  const errorMessage = error.message || String(error);
187
188
  if (errorMessage.includes('Refresh token has expired')) {
188
- logger.warn(`Refresh token expired: ${errorMessage}`);
189
+ warnRefreshTokenExpiredOnce(controllerUrl, errorMessage);
189
190
  } else {
190
- logger.warn(`Failed to refresh device token: ${errorMessage}`);
191
+ warnRefreshFailureOnce(controllerUrl, errorMessage);
191
192
  }
192
193
  return null;
193
194
  }
@@ -246,48 +247,29 @@ async function tryClientTokenAuth(environment, appName, controllerUrl) {
246
247
  const clientToken = await getOrRefreshClientToken(environment, appName, controllerUrl);
247
248
  if (clientToken && clientToken.token) {
248
249
  return {
249
- type: 'bearer',
250
+ type: 'client-token',
250
251
  token: clientToken.token,
251
252
  controller: clientToken.controller
252
253
  };
253
254
  }
254
255
  } catch {
255
- // Client token unavailable; getDeploymentAuth will try client credentials next (no warning here to avoid misleading output when env credentials succeed)
256
- }
257
- return null;
258
- }
259
-
260
- /**
261
- * Tries to get client credentials for deployment auth
262
- * @async
263
- * @function tryClientCredentialsAuth
264
- * @param {string} appName - Application name
265
- * @param {string} controllerUrl - Controller URL
266
- * @returns {Promise<Object|null>} Auth config with client credentials or null
267
- */
268
- async function tryClientCredentialsAuth(appName, controllerUrl) {
269
- const credentials = await loadClientCredentials(appName);
270
- if (credentials && credentials.clientId && credentials.clientSecret) {
271
- return {
272
- type: 'client-credentials',
273
- clientId: credentials.clientId,
274
- clientSecret: credentials.clientSecret,
275
- controller: controllerUrl
276
- };
256
+ // Client token unavailable; getDeploymentAuth will try exchanging credentials for token (no warning here to avoid misleading output when refresh succeeds)
277
257
  }
278
258
  return null;
279
259
  }
280
260
 
281
261
  /**
282
262
  * Get deployment authentication configuration with priority:
283
- * 1. Device token (Bearer) - for user-level audit tracking (preferred)
284
- * 2. Client token (Bearer) - for application-level authentication
285
- * 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).
286
268
  *
287
269
  * @param {string} controllerUrl - Controller URL
288
270
  * @param {string} environment - Environment key
289
271
  * @param {string} appName - Application name
290
- * @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)
291
273
  * @throws {Error} If no authentication method is available
292
274
  */
293
275
  async function getDeploymentAuth(controllerUrl, environment, appName) {
@@ -305,10 +287,21 @@ async function getDeploymentAuth(controllerUrl, environment, appName) {
305
287
  return clientTokenAuth;
306
288
  }
307
289
 
308
- // Priority 3: Use client credentials directly
309
- const credentialsAuth = await tryClientCredentialsAuth(appName, controllerUrl);
310
- if (credentialsAuth) {
311
- 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
+ }
312
305
  }
313
306
 
314
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'`);
@@ -336,7 +329,7 @@ async function extractClientCredentials(authConfig, appKey, envKey, _options = {
336
329
  };
337
330
  }
338
331
 
339
- if (authConfig.type === 'bearer') {
332
+ if (authConfig.type === 'bearer' || authConfig.type === 'client-token') {
340
333
  if (authConfig.clientId && authConfig.clientSecret) {
341
334
  return {
342
335
  clientId: authConfig.clientId,
@@ -398,9 +391,9 @@ async function forceRefreshDeviceToken(controllerUrl) {
398
391
  } catch (error) {
399
392
  const errorMessage = error.message || String(error);
400
393
  if (errorMessage.includes('Refresh token has expired')) {
401
- logger.warn(`Refresh token expired: ${errorMessage}`);
394
+ warnRefreshTokenExpiredOnce(controllerUrl, errorMessage);
402
395
  } else {
403
- logger.warn(`Failed to refresh device token: ${errorMessage}`);
396
+ warnRefreshFailureOnce(controllerUrl, errorMessage);
404
397
  }
405
398
  return null;
406
399
  }
@@ -181,9 +181,6 @@ function validateBuildConfig(build) {
181
181
  if (build.envOutputPath) {
182
182
  buildConfig.envOutputPath = build.envOutputPath;
183
183
  }
184
- if (build.localPort) {
185
- buildConfig.localPort = build.localPort;
186
- }
187
184
  if (build.language) {
188
185
  buildConfig.language = build.language;
189
186
  }
@@ -193,6 +190,9 @@ function validateBuildConfig(build) {
193
190
  if (build.dockerfile && build.dockerfile.trim() !== '') {
194
191
  buildConfig.dockerfile = build.dockerfile;
195
192
  }
193
+ if (build.localPort !== undefined && build.localPort !== null) {
194
+ buildConfig.localPort = build.localPort;
195
+ }
196
196
 
197
197
  return Object.keys(buildConfig).length > 0 ? buildConfig : null;
198
198
  }
@@ -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
+ };