@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.
- package/.cursor/rules/docs-rules.mdc +30 -0
- package/README.md +7 -5
- package/integration/hubspot/README.md +8 -4
- package/integration/hubspot/application.json +54 -0
- package/integration/hubspot/create-hubspot.js +9 -136
- package/integration/hubspot/env.template +3 -4
- package/integration/hubspot/hubspot-datasource-company.json +343 -5
- package/integration/hubspot/hubspot-datasource-contact.json +413 -5
- package/integration/hubspot/hubspot-datasource-deal.json +341 -4
- package/integration/hubspot/hubspot-datasource-users.json +116 -0
- package/integration/hubspot/hubspot-deploy.json +1250 -108
- package/integration/hubspot/hubspot-system.json +15 -32
- package/integration/hubspot/test-dataplane-down-tests.js +17 -16
- package/integration/hubspot/test-dataplane-down.js +2 -2
- package/integration/hubspot/test.js +1 -1
- package/jest.config.manual.js +2 -1
- package/lib/api/credential.api.js +40 -0
- package/lib/api/dev.api.js +423 -0
- package/lib/api/external-test.api.js +111 -0
- package/lib/api/index.js +42 -19
- package/lib/api/pipeline.api.js +66 -120
- package/lib/api/types/credential.types.js +23 -0
- package/lib/api/types/dev.types.js +140 -0
- package/lib/api/types/pipeline.types.js +37 -0
- package/lib/api/wizard-platform.api.js +61 -0
- package/lib/api/wizard.api.js +34 -1
- package/lib/app/config.js +44 -11
- package/lib/app/down.js +2 -1
- package/lib/app/index.js +12 -1
- package/lib/app/prompts.js +44 -29
- package/lib/app/push.js +36 -12
- package/lib/app/readme.js +9 -6
- package/lib/app/run-env-compose.js +264 -0
- package/lib/app/run-helpers.js +121 -118
- package/lib/app/run.js +148 -28
- package/lib/app/show-display.js +1 -1
- package/lib/app/show.js +5 -2
- package/lib/build/index.js +11 -3
- package/lib/cli/setup-app.js +172 -15
- package/lib/cli/setup-credential-deployment.js +31 -6
- package/lib/cli/setup-dev.js +206 -16
- package/lib/cli/setup-environment.js +16 -6
- package/lib/cli/setup-external-system.js +89 -24
- package/lib/cli/setup-infra.js +82 -15
- package/lib/cli/setup-secrets.js +52 -5
- package/lib/cli/setup-utility.js +129 -24
- package/lib/commands/app-install.js +172 -0
- package/lib/commands/app-shell.js +75 -0
- package/lib/commands/app-test.js +282 -0
- package/lib/commands/app.js +1 -1
- package/lib/commands/credential-env.js +162 -0
- package/lib/commands/credential-list.js +17 -22
- package/lib/commands/credential-push.js +96 -0
- package/lib/commands/datasource.js +77 -6
- package/lib/commands/dev-cli-handlers.js +141 -0
- package/lib/commands/dev-down.js +114 -0
- package/lib/commands/dev-init.js +347 -0
- package/lib/commands/repair-auth-config.js +99 -0
- package/lib/commands/repair-datasource-keys.js +208 -0
- package/lib/commands/repair-datasource.js +235 -0
- package/lib/commands/repair-env-template.js +348 -0
- package/lib/commands/repair-internal.js +85 -0
- package/lib/commands/repair-rbac.js +158 -0
- package/lib/commands/repair.js +507 -0
- package/lib/commands/secrets-list.js +118 -0
- package/lib/commands/secrets-remove.js +97 -0
- package/lib/commands/secrets-set.js +30 -17
- package/lib/commands/secrets-validate.js +50 -0
- package/lib/commands/test-e2e-external.js +165 -0
- package/lib/commands/up-dataplane.js +2 -2
- package/lib/commands/up-miso.js +0 -25
- package/lib/commands/upload.js +96 -40
- package/lib/commands/wizard-core-helpers.js +226 -4
- package/lib/commands/wizard-core.js +67 -29
- package/lib/commands/wizard-dataplane.js +1 -1
- package/lib/commands/wizard-entity-selection.js +43 -0
- package/lib/commands/wizard-headless.js +44 -5
- package/lib/commands/wizard-helpers.js +7 -3
- package/lib/commands/wizard.js +86 -64
- package/lib/core/admin-secrets.js +96 -0
- package/lib/core/config.js +7 -1
- package/lib/core/secrets-ensure.js +378 -0
- package/lib/core/secrets-env-write.js +157 -0
- package/lib/core/secrets.js +176 -89
- package/lib/datasource/deploy.js +12 -3
- package/lib/datasource/field-reference-validator.js +91 -0
- package/lib/datasource/test-e2e.js +219 -0
- package/lib/datasource/test-integration.js +154 -0
- package/lib/datasource/validate.js +21 -3
- package/lib/deployment/deployer.js +7 -5
- package/lib/deployment/environment-config.js +137 -0
- package/lib/deployment/environment.js +21 -98
- package/lib/deployment/push.js +32 -2
- package/lib/external-system/download.js +188 -203
- package/lib/external-system/generator.js +204 -56
- package/lib/external-system/test-auth.js +7 -3
- package/lib/external-system/test-execution.js +2 -1
- package/lib/external-system/test-system-level.js +73 -0
- package/lib/external-system/test.js +56 -19
- package/lib/generator/external-controller-manifest.js +29 -2
- package/lib/generator/external-schema-utils.js +1 -1
- package/lib/generator/external.js +10 -3
- package/lib/generator/index.js +177 -25
- package/lib/generator/split-readme.js +1 -0
- package/lib/generator/split-variables.js +7 -1
- package/lib/generator/split.js +194 -54
- package/lib/generator/wizard-prompts-secondary.js +294 -0
- package/lib/generator/wizard-prompts.js +105 -106
- package/lib/generator/wizard-readme.js +88 -0
- package/lib/generator/wizard.js +155 -158
- package/lib/infrastructure/compose.js +11 -1
- package/lib/infrastructure/helpers.js +103 -20
- package/lib/infrastructure/index.js +98 -12
- package/lib/infrastructure/services.js +88 -22
- package/lib/schema/application-schema.json +32 -8
- package/lib/schema/external-datasource.schema.json +49 -26
- package/lib/schema/external-system.schema.json +509 -411
- package/lib/schema/wizard-config.schema.json +16 -0
- package/lib/utils/api.js +41 -13
- package/lib/utils/app-register-auth.js +25 -3
- package/lib/utils/auth-headers.js +8 -7
- package/lib/utils/cli-utils.js +20 -0
- package/lib/utils/compose-generator.js +77 -76
- package/lib/utils/compose-handlebars-helpers.js +54 -0
- package/lib/utils/compose-vector-helper.js +18 -0
- package/lib/utils/config-format-preference.js +51 -0
- package/lib/utils/config-format.js +36 -0
- package/lib/utils/config-paths.js +127 -2
- package/lib/utils/configuration-env-resolver.js +179 -0
- package/lib/utils/credential-display.js +83 -0
- package/lib/utils/credential-secrets-env.js +357 -0
- package/lib/utils/dataplane-pipeline-warning.js +28 -0
- package/lib/utils/deployment-validation-helpers.js +4 -4
- package/lib/utils/dev-ca-install.js +139 -0
- package/lib/utils/dev-cert-helper.js +122 -0
- package/lib/utils/device-code-helpers.js +224 -0
- package/lib/utils/device-code.js +37 -336
- package/lib/utils/docker-build.js +40 -8
- package/lib/utils/env-copy.js +103 -13
- package/lib/utils/env-map.js +35 -5
- package/lib/utils/env-template.js +6 -5
- package/lib/utils/error-formatters/http-status-errors.js +20 -2
- package/lib/utils/error-formatters/permission-errors.js +0 -1
- package/lib/utils/error-formatters/validation-errors.js +0 -1
- package/lib/utils/external-readme.js +56 -29
- package/lib/utils/external-system-display.js +59 -1
- package/lib/utils/external-system-test-helpers.js +21 -8
- package/lib/utils/external-system-validators.js +3 -0
- package/lib/utils/file-upload.js +20 -50
- package/lib/utils/help-builder.js +16 -2
- package/lib/utils/infra-status.js +80 -45
- package/lib/utils/local-secrets.js +7 -52
- package/lib/utils/mutagen-install.js +195 -0
- package/lib/utils/mutagen.js +146 -0
- package/lib/utils/paths.js +128 -37
- package/lib/utils/port-resolver.js +28 -16
- package/lib/utils/remote-dev-auth.js +38 -0
- package/lib/utils/remote-docker-env.js +43 -0
- package/lib/utils/remote-secrets-loader.js +60 -0
- package/lib/utils/secrets-canonical.js +93 -0
- package/lib/utils/secrets-generator.js +114 -6
- package/lib/utils/secrets-helpers.js +108 -114
- package/lib/utils/secrets-path.js +2 -2
- package/lib/utils/secrets-utils.js +52 -1
- package/lib/utils/secrets-validation.js +84 -0
- package/lib/utils/ssh-key-helper.js +116 -0
- package/lib/utils/test-log-writer.js +56 -0
- package/lib/utils/token-manager-messages.js +90 -0
- package/lib/utils/token-manager.js +29 -36
- package/lib/utils/variable-transformer.js +3 -3
- package/lib/validation/env-template-auth.js +157 -0
- package/lib/validation/env-template-kv.js +41 -0
- package/lib/validation/external-manifest-validator.js +25 -0
- package/lib/validation/external-system-auth-rules.js +86 -0
- package/lib/validation/validate-batch.js +149 -0
- package/lib/validation/validate-datasource-keys-api.js +33 -0
- package/lib/validation/validate-display.js +94 -16
- package/lib/validation/validate.js +25 -12
- package/lib/validation/validator.js +72 -9
- package/lib/validation/wizard-datasource-validation.js +50 -0
- package/package.json +8 -3
- package/scripts/install-local.js +34 -15
- package/templates/README.md +0 -1
- package/templates/applications/README.md.hbs +4 -4
- package/templates/applications/dataplane/application.yaml +6 -5
- package/templates/applications/dataplane/env.template +15 -10
- package/templates/applications/dataplane/rbac.yaml +2 -2
- package/templates/applications/keycloak/env.template +2 -0
- package/templates/applications/miso-controller/application.yaml +1 -0
- package/templates/applications/miso-controller/env.template +12 -10
- package/templates/external-system/README.md.hbs +65 -25
- package/templates/external-system/deploy.js.hbs +4 -2
- package/templates/external-system/external-datasource.yaml.hbs +217 -0
- package/templates/external-system/external-system.json.hbs +1 -18
- package/templates/infra/compose.yaml.hbs +6 -0
- package/templates/python/docker-compose.hbs +49 -23
- package/templates/typescript/docker-compose.hbs +48 -22
- 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
|
-
|
|
189
|
+
warnRefreshTokenExpiredOnce(controllerUrl, errorMessage);
|
|
189
190
|
} else {
|
|
190
|
-
|
|
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: '
|
|
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
|
|
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 (
|
|
284
|
-
* 2. Client token (
|
|
285
|
-
* 3.
|
|
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-
|
|
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:
|
|
309
|
-
const
|
|
310
|
-
if (
|
|
311
|
-
|
|
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
|
-
|
|
394
|
+
warnRefreshTokenExpiredOnce(controllerUrl, errorMessage);
|
|
402
395
|
} else {
|
|
403
|
-
|
|
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
|
+
};
|