@aifabrix/builder 2.42.0 → 2.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/aifabrix.js +1 -1
- package/integration/hubspot-test/README.md +126 -0
- package/integration/{hubspot → hubspot-test}/application.json +6 -6
- package/integration/{hubspot → hubspot-test}/create-hubspot.js +5 -5
- package/integration/hubspot-test/env.template +4 -0
- package/integration/{hubspot/hubspot-datasource-company.json → hubspot-test/hubspot-test-datasource-company.json} +3 -2
- package/integration/{hubspot/hubspot-datasource-contact.json → hubspot-test/hubspot-test-datasource-contact.json} +3 -2
- package/integration/{hubspot/hubspot-datasource-deal.json → hubspot-test/hubspot-test-datasource-deal.json} +3 -2
- package/integration/{hubspot/hubspot-datasource-users.json → hubspot-test/hubspot-test-datasource-users.json} +3 -2
- package/integration/{hubspot/hubspot-deploy.json → hubspot-test/hubspot-test-deploy.json} +198 -21
- package/integration/{hubspot/hubspot-system.json → hubspot-test/hubspot-test-system.json} +8 -7
- package/integration/hubspot-test/rbac.json +166 -0
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-hubspot-credential-real.yaml +3 -3
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-hubspot-env-vars.yaml +2 -2
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-add-datasource.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-credential-create.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-credential-select.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-known-platform.yaml +1 -1
- package/integration/hubspot-test/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-mode.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-openapi-file.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-openapi-url.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-source.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-dimension-array-test.yaml +1 -1
- package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
- package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-dimension-test.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-rbac-test.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test.js +102 -59
- package/integration/{hubspot → hubspot-test}/wizard-hubspot-e2e.yaml +2 -2
- package/integration/{hubspot → hubspot-test}/wizard-hubspot-platform.yaml +1 -1
- package/lib/api/external-test.api.js +1 -1
- package/lib/api/service-users.api.js +111 -2
- package/lib/api/types/service-users.types.js +41 -0
- package/lib/api/wizard.api.js +2 -1
- package/lib/app/index.js +2 -2
- package/lib/app/prompts.js +2 -2
- package/lib/app/readme.js +3 -1
- package/lib/app/register.js +3 -1
- package/lib/app/rotate-secret.js +3 -0
- package/lib/cli/setup-app.js +5 -5
- package/lib/cli/setup-auth.js +19 -11
- package/lib/cli/setup-dev.js +62 -32
- package/lib/cli/setup-environment.js +6 -21
- package/lib/cli/setup-infra.js +13 -0
- package/lib/cli/setup-secrets.js +45 -6
- package/lib/cli/setup-service-user.js +146 -20
- package/lib/cli/setup-utility.js +12 -0
- package/lib/commands/auth-config.js +25 -19
- package/lib/commands/datasource.js +46 -1
- package/lib/commands/dev-init.js +1 -1
- package/lib/commands/repair-env-template.js +14 -8
- package/lib/commands/repair-rbac.js +25 -19
- package/lib/commands/repair.js +108 -31
- package/lib/commands/secrets-remove.js +1 -1
- package/lib/commands/secrets-set.js +6 -0
- package/lib/commands/secrets-validate.js +17 -4
- package/lib/commands/service-user.js +231 -2
- package/lib/commands/up-common.js +25 -0
- package/lib/commands/up-dataplane.js +91 -7
- package/lib/commands/wizard-core-helpers.js +5 -2
- package/lib/commands/wizard-core.js +2 -1
- package/lib/commands/wizard-headless.js +6 -1
- package/lib/commands/wizard.js +13 -6
- package/lib/core/admin-secrets.js +2 -0
- package/lib/core/config.js +7 -5
- package/lib/core/ensure-encryption-key.js +1 -3
- package/lib/core/secrets.js +32 -9
- package/lib/core/templates.js +1 -1
- package/lib/datasource/abac-validator.js +157 -0
- package/lib/datasource/field-reference-validator.js +74 -36
- package/lib/datasource/log-viewer.js +221 -0
- package/lib/datasource/resolve-app.js +109 -0
- package/lib/datasource/test-e2e.js +11 -20
- package/lib/datasource/test-integration.js +42 -22
- package/lib/datasource/validate.js +5 -2
- package/lib/external-system/download-helpers.js +3 -1
- package/lib/external-system/generator.js +12 -8
- package/lib/external-system/test-system-level.js +1 -1
- package/lib/generator/external-controller-manifest.js +3 -3
- package/lib/generator/external-schema-utils.js +3 -1
- package/lib/generator/external.js +7 -7
- package/lib/generator/helpers.js +13 -9
- package/lib/generator/index.js +4 -4
- package/lib/generator/split.js +45 -10
- package/lib/generator/wizard-prompts-secondary.js +39 -7
- package/lib/generator/wizard-readme.js +4 -1
- package/lib/generator/wizard.js +68 -53
- package/lib/infrastructure/helpers.js +50 -35
- package/lib/infrastructure/index.js +39 -23
- package/lib/schema/env-config.yaml +19 -2
- package/lib/schema/external-datasource.schema.json +11 -1
- package/lib/schema/wizard-config.schema.json +7 -1
- package/lib/utils/app-config-resolver.js +23 -1
- package/lib/utils/config-paths.js +48 -4
- package/lib/utils/credential-secrets-env.js +16 -1
- package/lib/utils/env-map.js +7 -3
- package/lib/utils/error-formatter.js +37 -0
- package/lib/utils/external-env-template.js +180 -0
- package/lib/utils/external-readme.js +33 -1
- package/lib/utils/external-system-display.js +43 -0
- package/lib/utils/external-system-validators.js +2 -2
- package/lib/utils/help-builder.js +3 -5
- package/lib/utils/local-secrets.js +26 -3
- package/lib/utils/paths.js +2 -1
- package/lib/utils/secrets-generator.js +2 -2
- package/lib/utils/secrets-utils.js +4 -0
- package/lib/utils/secure-file-permissions.js +91 -0
- package/lib/utils/token-manager.js +36 -3
- package/lib/utils/yaml-preserve.js +59 -1
- package/lib/validation/env-template-auth.js +50 -2
- package/lib/validation/external-manifest-validator.js +8 -0
- package/lib/validation/validate.js +8 -0
- package/lib/validation/validator.js +10 -13
- package/package.json +6 -2
- package/templates/applications/dataplane/env.template +5 -1
- package/templates/applications/miso-controller/application.yaml +1 -1
- package/templates/applications/miso-controller/env.template +13 -2
- package/templates/external-system/README.md.hbs +18 -5
- package/templates/external-system/env.template.hbs +22 -0
- package/integration/hubspot/README.md +0 -100
- package/integration/hubspot/env.template +0 -4
- package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +0 -2
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +0 -5
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +0 -5
- /package/integration/{hubspot → hubspot-test}/companies.json +0 -0
- /package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-app-name.yaml +0 -0
- /package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-missing-app.yaml +0 -0
- /package/integration/{hubspot → hubspot-test}/test-dataplane-down-helpers.js +0 -0
- /package/integration/{hubspot → hubspot-test}/test-dataplane-down-tests.js +0 -0
- /package/integration/{hubspot → hubspot-test}/test-dataplane-down.js +0 -0
|
@@ -91,6 +91,33 @@ function normalizeDatasources(datasources, systemKey, fileExt = '.json') {
|
|
|
91
91
|
);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Builds secret path entries for README "Secrets" section per auth type.
|
|
96
|
+
* Path is the key for `aifabrix secret set <key> <value>` (no kv:// prefix; key format systemKey/secretKey in camelCase).
|
|
97
|
+
* @param {string} systemKey - System key
|
|
98
|
+
* @param {string} [authType] - Authentication type (oauth2, aad, apikey, basic, queryParam, hmac, bearer, token, none)
|
|
99
|
+
* @returns {Array<{path: string, description: string}>} secretPaths for template (path = key for secret set, no kv://)
|
|
100
|
+
*/
|
|
101
|
+
function buildSecretPaths(systemKey, authType) {
|
|
102
|
+
if (!systemKey || typeof systemKey !== 'string') return [];
|
|
103
|
+
const t = (authType && typeof authType === 'string') ? authType.toLowerCase() : 'apikey';
|
|
104
|
+
const map = {
|
|
105
|
+
oauth2: [{ path: `${systemKey}/clientId`, description: 'Client ID' }, { path: `${systemKey}/clientSecret`, description: 'Client Secret' }],
|
|
106
|
+
oauth: [{ path: `${systemKey}/clientId`, description: 'Client ID' }, { path: `${systemKey}/clientSecret`, description: 'Client Secret' }],
|
|
107
|
+
aad: [{ path: `${systemKey}/clientId`, description: 'Client ID' }, { path: `${systemKey}/clientSecret`, description: 'Client Secret' }],
|
|
108
|
+
apikey: [{ path: `${systemKey}/apiKey`, description: 'API Key' }],
|
|
109
|
+
apiKey: [{ path: `${systemKey}/apiKey`, description: 'API Key' }],
|
|
110
|
+
basic: [{ path: `${systemKey}/username`, description: 'Username' }, { path: `${systemKey}/password`, description: 'Password' }],
|
|
111
|
+
queryparam: [{ path: `${systemKey}/paramValue`, description: 'Query parameter value' }],
|
|
112
|
+
hmac: [{ path: `${systemKey}/signingSecret`, description: 'Signing secret' }],
|
|
113
|
+
bearer: [{ path: `${systemKey}/bearerToken`, description: 'Bearer token' }],
|
|
114
|
+
token: [{ path: `${systemKey}/bearerToken`, description: 'Bearer token' }],
|
|
115
|
+
oidc: [],
|
|
116
|
+
none: []
|
|
117
|
+
};
|
|
118
|
+
return map[t] || map.apikey;
|
|
119
|
+
}
|
|
120
|
+
|
|
94
121
|
/**
|
|
95
122
|
* Builds the external system README template context
|
|
96
123
|
* @function buildExternalReadmeContext
|
|
@@ -102,6 +129,8 @@ function normalizeDatasources(datasources, systemKey, fileExt = '.json') {
|
|
|
102
129
|
* @param {string} [params.description] - Description
|
|
103
130
|
* @param {Array} [params.datasources] - Datasource objects
|
|
104
131
|
* @param {string} [params.fileExt] - File extension for config files (e.g. '.json', '.yaml'); default '.json'
|
|
132
|
+
* @param {string} [params.authType] - Authentication type for Secrets section (oauth2, aad, apikey, basic, etc.)
|
|
133
|
+
* @param {Object} [params.authentication] - Full authentication object (authType used if authType not set)
|
|
105
134
|
* @returns {Object} Template context
|
|
106
135
|
*/
|
|
107
136
|
function buildExternalReadmeContext(params = {}) {
|
|
@@ -112,6 +141,8 @@ function buildExternalReadmeContext(params = {}) {
|
|
|
112
141
|
const systemType = params.systemType || 'openapi';
|
|
113
142
|
const fileExt = params.fileExt !== undefined ? params.fileExt : '.json';
|
|
114
143
|
const datasources = normalizeDatasources(params.datasources, systemKey, fileExt);
|
|
144
|
+
const authType = params.authType || params.authentication?.type || params.authentication?.method || params.authentication?.authType;
|
|
145
|
+
const secretPaths = buildSecretPaths(systemKey, authType);
|
|
115
146
|
|
|
116
147
|
return {
|
|
117
148
|
appName,
|
|
@@ -122,7 +153,8 @@ function buildExternalReadmeContext(params = {}) {
|
|
|
122
153
|
fileExt: fileExt && fileExt.startsWith('.') ? fileExt : `.${fileExt || 'json'}`,
|
|
123
154
|
datasourceCount: datasources.length,
|
|
124
155
|
hasDatasources: datasources.length > 0,
|
|
125
|
-
datasources
|
|
156
|
+
datasources,
|
|
157
|
+
secretPaths
|
|
126
158
|
};
|
|
127
159
|
}
|
|
128
160
|
|
|
@@ -289,6 +289,20 @@ function displayE2EResults(data, verbose = false) {
|
|
|
289
289
|
logger.log(` ${ok ? chalk.green('✓') : chalk.red('✗')} ${name}`);
|
|
290
290
|
if (!ok && (step.error || step.message)) logger.log(chalk.red(` ${step.error || step.message}`));
|
|
291
291
|
if (verbose && step.message && ok) logger.log(chalk.gray(` ${step.message}`));
|
|
292
|
+
if (verbose && ok && (name === 'sync' || step.step === 'sync') && step.evidence && step.evidence.jobs) {
|
|
293
|
+
formatSyncStepEvidence(step.evidence.jobs);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (verbose && data.auditLog && Array.isArray(data.auditLog) && data.auditLog.length > 0) {
|
|
297
|
+
const n = data.auditLog.length;
|
|
298
|
+
const first = data.auditLog[0];
|
|
299
|
+
const execId = (first && (first.executionId || first.id || first.traceId)) ? String(first.executionId || first.id || first.traceId) : null;
|
|
300
|
+
if (execId) {
|
|
301
|
+
const short = execId.length > 10 ? `${execId.slice(0, 8)}…` : execId;
|
|
302
|
+
logger.log(chalk.gray(` CIP execution trace(s): ${n} (executionId: ${short})`));
|
|
303
|
+
} else {
|
|
304
|
+
logger.log(chalk.gray(` CIP execution trace(s): ${n}`));
|
|
305
|
+
}
|
|
292
306
|
}
|
|
293
307
|
if (isRunning) {
|
|
294
308
|
return;
|
|
@@ -297,6 +311,35 @@ function displayE2EResults(data, verbose = false) {
|
|
|
297
311
|
logger.log(allPassed ? chalk.green('\n✅ E2E test passed!') : chalk.red('\n❌ E2E test failed'));
|
|
298
312
|
}
|
|
299
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Log sync step job evidence (record counts) in verbose E2E output
|
|
316
|
+
* @param {Object[]} jobs - evidence.jobs from sync step
|
|
317
|
+
*/
|
|
318
|
+
function formatSyncStepEvidence(jobs) {
|
|
319
|
+
for (const job of jobs) {
|
|
320
|
+
const rec = job.recordsProcessed ?? job.totalProcessed;
|
|
321
|
+
const total = job.totalRecords ?? (job.audit && job.audit.totalProcessed);
|
|
322
|
+
const parts = [];
|
|
323
|
+
if (rec !== undefined && rec !== null) parts.push(`${rec} processed`);
|
|
324
|
+
if (total !== undefined && total !== null) parts.push(`total: ${total}`);
|
|
325
|
+
const audit = job.audit || {};
|
|
326
|
+
const ins = audit.inserted ?? job.insertedCount;
|
|
327
|
+
const upd = audit.updated ?? job.updatedCount;
|
|
328
|
+
const del = audit.deleted ?? job.deletedCount;
|
|
329
|
+
const tot = audit.totalProcessed ?? total;
|
|
330
|
+
if (ins !== undefined || upd !== undefined || del !== undefined || tot !== undefined) {
|
|
331
|
+
const a = [`inserted: ${ins ?? 0}`, `updated: ${upd ?? 0}`, `deleted: ${del ?? 0}`];
|
|
332
|
+
if (tot !== undefined) a.push(`totalProcessed: ${tot}`);
|
|
333
|
+
parts.push(`(${a.join(', ')})`);
|
|
334
|
+
}
|
|
335
|
+
if (job.skippedCount !== undefined) parts.push(`skipped: ${job.skippedCount}`);
|
|
336
|
+
if (job.rejectedByQualityCount !== undefined) parts.push(`rejectedByQuality: ${job.rejectedByQualityCount}`);
|
|
337
|
+
if (parts.length > 0) {
|
|
338
|
+
logger.log(chalk.gray(` Managed records: ${parts.join(' ')}`));
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
300
343
|
module.exports = {
|
|
301
344
|
displayTestResults,
|
|
302
345
|
displayIntegrationTestResults,
|
|
@@ -139,11 +139,11 @@ function validateDimensions(dimensions, results) {
|
|
|
139
139
|
// Validate dimension keys and values
|
|
140
140
|
for (const [dimensionKey, attributePath] of Object.entries(dimensions)) {
|
|
141
141
|
if (!/^[a-zA-Z0-9_]+$/.test(dimensionKey)) {
|
|
142
|
-
results.errors.push(`Invalid dimension key '${dimensionKey}': must
|
|
142
|
+
results.errors.push(`Invalid dimension key '${dimensionKey}': dimension key must contain only letters, numbers, and underscores`);
|
|
143
143
|
results.valid = false;
|
|
144
144
|
}
|
|
145
145
|
if (typeof attributePath !== 'string' || !/^[a-zA-Z0-9_.]+$/.test(attributePath)) {
|
|
146
|
-
results.errors.push(`Invalid attribute path '${attributePath}' for dimension '${dimensionKey}': must
|
|
146
|
+
results.errors.push(`Invalid attribute path '${attributePath}' for dimension '${dimensionKey}': attribute path must contain only letters, numbers, underscores, and dots`);
|
|
147
147
|
results.valid = false;
|
|
148
148
|
}
|
|
149
149
|
}
|
|
@@ -46,9 +46,7 @@ const CATEGORIES = [
|
|
|
46
46
|
{ name: 'build', term: 'build <app>' },
|
|
47
47
|
{ name: 'run', term: 'run <app>' },
|
|
48
48
|
{ name: 'shell', term: 'shell <app>' },
|
|
49
|
-
{ name: 'test', term: 'test <app>' },
|
|
50
49
|
{ name: 'install', term: 'install <app>' },
|
|
51
|
-
{ name: 'test-e2e', term: 'test-e2e <app>' },
|
|
52
50
|
{ name: 'lint', term: 'lint <app>' },
|
|
53
51
|
{ name: 'logs', term: 'logs <app>' },
|
|
54
52
|
{ name: 'stop', term: 'stop <app>' },
|
|
@@ -65,15 +63,13 @@ const CATEGORIES = [
|
|
|
65
63
|
{
|
|
66
64
|
name: 'Environments',
|
|
67
65
|
commands: [
|
|
68
|
-
{ name: 'environment' },
|
|
69
66
|
{ name: 'env' }
|
|
70
67
|
]
|
|
71
68
|
},
|
|
72
69
|
{
|
|
73
|
-
name: 'Application &
|
|
70
|
+
name: 'Application & Management',
|
|
74
71
|
commands: [
|
|
75
72
|
{ name: 'app' },
|
|
76
|
-
{ name: 'datasource' },
|
|
77
73
|
{ name: 'credential' },
|
|
78
74
|
{ name: 'deployment' },
|
|
79
75
|
{ name: 'service-user' }
|
|
@@ -98,7 +94,9 @@ const CATEGORIES = [
|
|
|
98
94
|
{ name: 'upload', term: 'upload <system-key>' },
|
|
99
95
|
{ name: 'delete', term: 'delete <system-key>' },
|
|
100
96
|
{ name: 'repair', term: 'repair <app>' },
|
|
97
|
+
{ name: 'datasource' },
|
|
101
98
|
{ name: 'test', term: 'test <app>' },
|
|
99
|
+
{ name: 'test-e2e', term: 'test-e2e <app>' },
|
|
102
100
|
{ name: 'test-integration', term: 'test-integration <app>' }
|
|
103
101
|
]
|
|
104
102
|
},
|
|
@@ -15,10 +15,31 @@ const logger = require('../utils/logger');
|
|
|
15
15
|
const pathsUtil = require('./paths');
|
|
16
16
|
const { mergeSecretsIntoFile } = require('./secrets-generator');
|
|
17
17
|
|
|
18
|
+
/** Bootstrap key name; never encrypt this key's value when writing (key is stored in config). */
|
|
19
|
+
const ENCRYPTION_KEY_VAULT = 'secrets-encryptionKeyVault';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolves value to write: encrypted (secure://) when encryption key is set and key is not the bootstrap key.
|
|
23
|
+
* @async
|
|
24
|
+
* @param {string} key - Secret key name
|
|
25
|
+
* @param {string} value - Secret value
|
|
26
|
+
* @returns {Promise<string>} Value to write (plaintext or secure://...)
|
|
27
|
+
*/
|
|
28
|
+
async function resolveValueForWrite(key, value) {
|
|
29
|
+
const config = require('../core/config');
|
|
30
|
+
const encryptionKey = await config.getSecretsEncryptionKey();
|
|
31
|
+
if (!encryptionKey || key === ENCRYPTION_KEY_VAULT) {
|
|
32
|
+
return typeof value === 'string' ? value : String(value);
|
|
33
|
+
}
|
|
34
|
+
const { encryptSecret } = require('./secrets-encryption');
|
|
35
|
+
return encryptSecret(typeof value === 'string' ? value : String(value), encryptionKey);
|
|
36
|
+
}
|
|
37
|
+
|
|
18
38
|
/**
|
|
19
39
|
* Saves a secret to ~/.aifabrix/secrets.local.yaml
|
|
20
40
|
* Uses paths.getAifabrixHome() to respect config.yaml aifabrix-home override
|
|
21
|
-
* Merges the key into the file (updates in place if key already exists, e.g. after rotate-secret)
|
|
41
|
+
* Merges the key into the file (updates in place if key already exists, e.g. after rotate-secret).
|
|
42
|
+
* Encrypts the value when a secrets-encryption key is configured (except for the bootstrap key).
|
|
22
43
|
*
|
|
23
44
|
* @async
|
|
24
45
|
* @function saveLocalSecret
|
|
@@ -39,8 +60,9 @@ async function saveLocalSecret(key, value) {
|
|
|
39
60
|
throw new Error('Secret value is required');
|
|
40
61
|
}
|
|
41
62
|
|
|
63
|
+
const valueToWrite = await resolveValueForWrite(key, value);
|
|
42
64
|
const secretsPath = path.join(pathsUtil.getAifabrixHome(), 'secrets.local.yaml');
|
|
43
|
-
mergeSecretsIntoFile(secretsPath, { [key]:
|
|
65
|
+
mergeSecretsIntoFile(secretsPath, { [key]: valueToWrite });
|
|
44
66
|
}
|
|
45
67
|
|
|
46
68
|
/**
|
|
@@ -121,8 +143,9 @@ function _loadExistingSecrets(resolvedPath) {
|
|
|
121
143
|
async function saveSecret(key, value, secretsPath) {
|
|
122
144
|
validateSaveSecretParams(key, value, secretsPath);
|
|
123
145
|
|
|
146
|
+
const valueToWrite = await resolveValueForWrite(key, value);
|
|
124
147
|
const resolvedPath = resolveAndPrepareSecretsPath(secretsPath);
|
|
125
|
-
mergeSecretsIntoFile(resolvedPath, { [key]:
|
|
148
|
+
mergeSecretsIntoFile(resolvedPath, { [key]: valueToWrite });
|
|
126
149
|
}
|
|
127
150
|
|
|
128
151
|
/**
|
package/lib/utils/paths.js
CHANGED
|
@@ -430,7 +430,7 @@ function getDeployJsonPath(appName, appType, preferNew = false) {
|
|
|
430
430
|
// If neither exists, return new naming (for generation)
|
|
431
431
|
return newPath;
|
|
432
432
|
}
|
|
433
|
-
const { resolveApplicationConfigPath } = require('./app-config-resolver');
|
|
433
|
+
const { resolveApplicationConfigPath, resolveRbacPath } = require('./app-config-resolver');
|
|
434
434
|
const { loadConfigFile } = require('./config-format');
|
|
435
435
|
/**
|
|
436
436
|
* Checks if app type is external from variables object
|
|
@@ -562,6 +562,7 @@ module.exports = {
|
|
|
562
562
|
resolveBuildContext,
|
|
563
563
|
getDeployJsonPath,
|
|
564
564
|
resolveApplicationConfigPath,
|
|
565
|
+
resolveRbacPath,
|
|
565
566
|
detectAppType,
|
|
566
567
|
getResolveAppPath,
|
|
567
568
|
resolveIntegrationAppKeyFromCwd,
|
|
@@ -198,7 +198,7 @@ function saveSecretsFile(resolvedPath, secrets) {
|
|
|
198
198
|
|
|
199
199
|
const yamlContent = yaml.dump(secrets, {
|
|
200
200
|
indent: 2,
|
|
201
|
-
lineWidth:
|
|
201
|
+
lineWidth: -1,
|
|
202
202
|
noRefs: true,
|
|
203
203
|
sortKeys: false
|
|
204
204
|
});
|
|
@@ -206,7 +206,7 @@ function saveSecretsFile(resolvedPath, secrets) {
|
|
|
206
206
|
fs.writeFileSync(resolvedPath, yamlContent, { mode: 0o600 });
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
const YAML_DUMP_OPTS = { indent: 2, lineWidth:
|
|
209
|
+
const YAML_DUMP_OPTS = { indent: 2, lineWidth: -1, noRefs: true, sortKeys: false };
|
|
210
210
|
|
|
211
211
|
/**
|
|
212
212
|
* Merges secret keys into the secrets file (load existing, merge, overwrite file).
|
|
@@ -14,6 +14,7 @@ const path = require('path');
|
|
|
14
14
|
const yaml = require('js-yaml');
|
|
15
15
|
const logger = require('./logger');
|
|
16
16
|
const pathsUtil = require('./paths');
|
|
17
|
+
const { ensureSecureFilePermissions } = require('./secure-file-permissions');
|
|
17
18
|
const { getContainerPort } = require('./port-resolver');
|
|
18
19
|
const { loadYamlTolerantOfDuplicateKeys } = require('./secrets-generator');
|
|
19
20
|
|
|
@@ -78,6 +79,7 @@ function loadPrimaryUserSecrets() {
|
|
|
78
79
|
if (!fs.existsSync(userSecretsPath)) {
|
|
79
80
|
return {};
|
|
80
81
|
}
|
|
82
|
+
ensureSecureFilePermissions(userSecretsPath);
|
|
81
83
|
|
|
82
84
|
try {
|
|
83
85
|
const content = fs.readFileSync(userSecretsPath, 'utf8');
|
|
@@ -106,6 +108,7 @@ function loadUserSecrets() {
|
|
|
106
108
|
if (!fs.existsSync(userSecretsPath)) {
|
|
107
109
|
return {};
|
|
108
110
|
}
|
|
111
|
+
ensureSecureFilePermissions(userSecretsPath);
|
|
109
112
|
|
|
110
113
|
try {
|
|
111
114
|
const content = fs.readFileSync(userSecretsPath, 'utf8');
|
|
@@ -134,6 +137,7 @@ function loadDefaultSecrets() {
|
|
|
134
137
|
if (!fs.existsSync(defaultPath)) {
|
|
135
138
|
return {};
|
|
136
139
|
}
|
|
140
|
+
ensureSecureFilePermissions(defaultPath);
|
|
137
141
|
|
|
138
142
|
try {
|
|
139
143
|
const content = fs.readFileSync(defaultPath, 'utf8');
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secure file permissions for secrets and config (ISO 27001).
|
|
3
|
+
* Ensures sensitive files are restricted to owner-only (0o600) when read or written.
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview Enforce restrictive permissions on secrets and config files
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 2.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
/** Mode for secrets and admin files: owner read/write only (no group/other). */
|
|
16
|
+
const SECRET_FILE_MODE = 0o600;
|
|
17
|
+
|
|
18
|
+
/** Mode for config file (may contain tokens): owner read/write only. */
|
|
19
|
+
const CONFIG_FILE_MODE = 0o600;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Ensures a file has restrictive permissions (0o600) when it exists.
|
|
23
|
+
* If the file has group or other read/write/execute bits set, chmods to owner-only.
|
|
24
|
+
* Safe to call on every read path; no-op when file is missing or already 0o600.
|
|
25
|
+
* On Windows, chmod restricts write access; mode bits are not fully supported.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} filePath - Absolute or relative path to the file
|
|
28
|
+
* @param {number} [mode=0o600] - Desired mode (default SECRET_FILE_MODE)
|
|
29
|
+
* @returns {boolean} True if file existed and permissions were (or are now) secure
|
|
30
|
+
*/
|
|
31
|
+
function ensureSecureFilePermissions(filePath, mode = SECRET_FILE_MODE) {
|
|
32
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const resolved = path.isAbsolute(filePath) ? filePath : path.resolve(filePath);
|
|
36
|
+
try {
|
|
37
|
+
if (!fs.existsSync(resolved)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const stat = fs.statSync(resolved);
|
|
41
|
+
if (!stat.isFile()) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const currentMode = stat.mode & 0o777;
|
|
45
|
+
if ((currentMode & 0o77) === 0) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
fs.chmodSync(resolved, mode);
|
|
49
|
+
return true;
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Ensures a directory has restrictive permissions (0o700) when it exists.
|
|
57
|
+
* No-op when directory is missing or already 0o700 (no group/other).
|
|
58
|
+
*
|
|
59
|
+
* @param {string} dirPath - Absolute or relative path to the directory
|
|
60
|
+
* @returns {boolean} True if directory existed and permissions were (or are now) secure
|
|
61
|
+
*/
|
|
62
|
+
function ensureSecureDirPermissions(dirPath) {
|
|
63
|
+
if (!dirPath || typeof dirPath !== 'string') {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const resolved = path.isAbsolute(dirPath) ? dirPath : path.resolve(dirPath);
|
|
67
|
+
try {
|
|
68
|
+
if (!fs.existsSync(resolved)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const stat = fs.statSync(resolved);
|
|
72
|
+
if (!stat.isDirectory()) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const currentMode = stat.mode & 0o777;
|
|
76
|
+
if ((currentMode & 0o77) === 0) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
fs.chmodSync(resolved, 0o700);
|
|
80
|
+
return true;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = {
|
|
87
|
+
ensureSecureFilePermissions,
|
|
88
|
+
ensureSecureDirPermissions,
|
|
89
|
+
SECRET_FILE_MODE,
|
|
90
|
+
CONFIG_FILE_MODE
|
|
91
|
+
};
|
|
@@ -21,12 +21,43 @@ const {
|
|
|
21
21
|
} = require('./token-manager-refresh');
|
|
22
22
|
const { warnRefreshFailureOnce, warnRefreshTokenExpiredOnce } = require('./token-manager-messages');
|
|
23
23
|
|
|
24
|
+
/** App key used for dataplane client credentials in secrets.local.yaml */
|
|
25
|
+
const DATAPLANE_APP_KEY = 'dataplane';
|
|
26
|
+
|
|
24
27
|
function getSecretsFilePath() {
|
|
25
28
|
return path.join(pathsUtil.getAifabrixHome(), 'secrets.local.yaml');
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
/**
|
|
29
|
-
*
|
|
32
|
+
* Validate that secrets.local.yaml contains dataplane client credentials.
|
|
33
|
+
* If missing, the developer should run: aifabrix app rotate-secret dataplane
|
|
34
|
+
* @param {string} [secretsFilePath] - Path to secrets file; defaults to ~/.aifabrix/secrets.local.yaml
|
|
35
|
+
* @returns {{ valid: boolean, hint?: string }}
|
|
36
|
+
*/
|
|
37
|
+
function validateDataplaneSecrets(secretsFilePath) {
|
|
38
|
+
const filePath = secretsFilePath || getSecretsFilePath();
|
|
39
|
+
const hint = 'Dataplane credentials are missing. Run: aifabrix app rotate-secret dataplane';
|
|
40
|
+
try {
|
|
41
|
+
if (!fs.existsSync(filePath)) {
|
|
42
|
+
return { valid: false, hint };
|
|
43
|
+
}
|
|
44
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
45
|
+
const secrets = yaml.load(content) || {};
|
|
46
|
+
const clientIdKey = `${DATAPLANE_APP_KEY}-client-idKeyVault`;
|
|
47
|
+
const clientSecretKey = `${DATAPLANE_APP_KEY}-client-secretKeyVault`;
|
|
48
|
+
const hasId = secrets[clientIdKey] !== null && secrets[clientIdKey] !== undefined && String(secrets[clientIdKey]).trim() !== '';
|
|
49
|
+
const hasSecret = secrets[clientSecretKey] !== null && secrets[clientSecretKey] !== undefined && String(secrets[clientSecretKey]).trim() !== '';
|
|
50
|
+
if (hasId && hasSecret) {
|
|
51
|
+
return { valid: true };
|
|
52
|
+
}
|
|
53
|
+
return { valid: false, hint };
|
|
54
|
+
} catch {
|
|
55
|
+
return { valid: false, hint };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Load client credentials from secrets.local.yaml or process.env (e.g. integration/hubspot-test/.env).
|
|
30
61
|
* Reads secrets file using pattern: <app-name>-client-idKeyVault and <app-name>-client-secretKeyVault.
|
|
31
62
|
* If not found, checks process.env.CLIENTID and process.env.CLIENTSECRET (set when .env is loaded).
|
|
32
63
|
* @param {string} appName - Application name
|
|
@@ -60,7 +91,7 @@ async function loadClientCredentials(appName) {
|
|
|
60
91
|
logger.warn(`Failed to load credentials from secrets.local.yaml: ${error.message}`);
|
|
61
92
|
}
|
|
62
93
|
|
|
63
|
-
// Fallback: use CLIENTID/CLIENTSECRET from process.env (e.g. from integration/hubspot/.env)
|
|
94
|
+
// Fallback: use CLIENTID/CLIENTSECRET from process.env (e.g. from integration/hubspot-test/.env)
|
|
64
95
|
const envClientId = process.env.CLIENTID || process.env.CLIENT_ID;
|
|
65
96
|
const envClientSecret = process.env.CLIENTSECRET || process.env.CLIENT_SECRET;
|
|
66
97
|
if (envClientId && envClientSecret) {
|
|
@@ -439,6 +470,7 @@ function requireBearerForDataplanePipeline(authConfig) {
|
|
|
439
470
|
}
|
|
440
471
|
|
|
441
472
|
module.exports = {
|
|
473
|
+
DATAPLANE_APP_KEY,
|
|
442
474
|
getDeviceToken,
|
|
443
475
|
getClientToken,
|
|
444
476
|
isTokenExpired,
|
|
@@ -452,5 +484,6 @@ module.exports = {
|
|
|
452
484
|
getDeploymentAuth,
|
|
453
485
|
getDeviceOnlyAuth,
|
|
454
486
|
extractClientCredentials,
|
|
455
|
-
requireBearerForDataplanePipeline
|
|
487
|
+
requireBearerForDataplanePipeline,
|
|
488
|
+
validateDataplaneSecrets
|
|
456
489
|
};
|
|
@@ -185,6 +185,9 @@ function formatValue(value, quoted, quoteChar) {
|
|
|
185
185
|
* const result = encryptYamlValues(yamlContent, encryptionKey);
|
|
186
186
|
* // Returns: { content: '...', encrypted: 5, total: 10 }
|
|
187
187
|
*/
|
|
188
|
+
/** Pattern for YAML block scalar indicator (e.g. key: >- or key: |) */
|
|
189
|
+
const BLOCK_SCALAR_PATTERN = /^(\s*)([^#:\n]+?):\s*(\|[-+]?|>[-+]?)(\s*)(#.*)?$/;
|
|
190
|
+
|
|
188
191
|
/**
|
|
189
192
|
* Processes a single line for encryption
|
|
190
193
|
* @function processLineForEncryption
|
|
@@ -221,13 +224,68 @@ function processLineForEncryption(line, encryptionKey, stats) {
|
|
|
221
224
|
return line;
|
|
222
225
|
}
|
|
223
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Collects continuation lines for a YAML block scalar (value on next lines).
|
|
229
|
+
* @param {string[]} lines - All lines
|
|
230
|
+
* @param {number} startIndex - Index of line after the key: >- line
|
|
231
|
+
* @param {number} keyIndentLen - Number of leading spaces on the key line
|
|
232
|
+
* @returns {{ value: string, endIndex: number }} Combined value and index after last continuation line
|
|
233
|
+
*/
|
|
234
|
+
function collectBlockScalarLines(lines, startIndex, keyIndentLen) {
|
|
235
|
+
const parts = [];
|
|
236
|
+
let i = startIndex;
|
|
237
|
+
while (i < lines.length) {
|
|
238
|
+
const line = lines[i];
|
|
239
|
+
const contentTrimmed = line.trim();
|
|
240
|
+
if (contentTrimmed === '' || contentTrimmed.startsWith('#')) {
|
|
241
|
+
i++;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const indentLen = line.length - line.trimStart().length;
|
|
245
|
+
if (indentLen <= keyIndentLen) {
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
parts.push(contentTrimmed);
|
|
249
|
+
i++;
|
|
250
|
+
}
|
|
251
|
+
return { value: parts.join(' '), endIndex: i };
|
|
252
|
+
}
|
|
253
|
+
|
|
224
254
|
function encryptYamlValues(content, encryptionKey) {
|
|
225
255
|
const lines = content.split(/\r?\n/);
|
|
226
256
|
const encryptedLines = [];
|
|
227
257
|
const stats = { encrypted: 0, total: 0 };
|
|
258
|
+
let i = 0;
|
|
259
|
+
|
|
260
|
+
while (i < lines.length) {
|
|
261
|
+
const line = lines[i];
|
|
262
|
+
const trimmed = line.trim();
|
|
263
|
+
|
|
264
|
+
if (trimmed === '' || trimmed.startsWith('#')) {
|
|
265
|
+
encryptedLines.push(line);
|
|
266
|
+
i++;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const blockMatch = line.match(BLOCK_SCALAR_PATTERN);
|
|
271
|
+
if (blockMatch) {
|
|
272
|
+
const [, indent, key, blockIndicator, trailingWhitespace, comment] = blockMatch;
|
|
273
|
+
const keyIndentLen = indent.length;
|
|
274
|
+
const folded = blockIndicator.startsWith('>');
|
|
275
|
+
const { value: rawValue, endIndex } = collectBlockScalarLines(lines, i + 1, keyIndentLen);
|
|
276
|
+
const value = folded ? rawValue.replace(/\s+/g, ' ').trim() : rawValue;
|
|
277
|
+
stats.total++;
|
|
278
|
+
const outValue = shouldEncryptValue(value) ? encryptSecret(value, encryptionKey) : value;
|
|
279
|
+
if (shouldEncryptValue(value)) {
|
|
280
|
+
stats.encrypted++;
|
|
281
|
+
}
|
|
282
|
+
encryptedLines.push(`${indent}${key}: ${outValue}${trailingWhitespace || ''}${comment || ''}`);
|
|
283
|
+
i = endIndex;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
228
286
|
|
|
229
|
-
for (const line of lines) {
|
|
230
287
|
encryptedLines.push(processLineForEncryption(line, encryptionKey, stats));
|
|
288
|
+
i++;
|
|
231
289
|
}
|
|
232
290
|
|
|
233
291
|
return {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const { loadExternalIntegrationConfig, loadSystemFile } = require('../generator/external');
|
|
13
|
+
const { getKvPathSegmentForSecurityKey } = require('../utils/credential-secrets-env');
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Extracts all kv:// paths from env.template content (RHS of VAR=value lines).
|
|
@@ -93,7 +94,7 @@ function setHasPathIgnoreCase(pathSet, requiredPath) {
|
|
|
93
94
|
* @returns {Promise<{ requiredPaths: Set<string>, warning?: string }>} Required kv paths and optional warning
|
|
94
95
|
*
|
|
95
96
|
* @example
|
|
96
|
-
* const { requiredPaths } = await collectRequiredAuthKvPaths('/path/to/integration/hubspot');
|
|
97
|
+
* const { requiredPaths } = await collectRequiredAuthKvPaths('/path/to/integration/hubspot-test');
|
|
97
98
|
* // requiredPaths has kv:// paths from authentication.security
|
|
98
99
|
*/
|
|
99
100
|
async function collectRequiredAuthKvPaths(appPath, _options = {}) {
|
|
@@ -148,10 +149,57 @@ async function validateAuthKvCoverage(appPath, content, errors, warnings, option
|
|
|
148
149
|
}
|
|
149
150
|
}
|
|
150
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Derives system key from system file name (e.g. hubspot-system.yaml -> hubspot).
|
|
154
|
+
* @param {string} systemFileName - System file name
|
|
155
|
+
* @returns {string}
|
|
156
|
+
*/
|
|
157
|
+
function systemKeyFromFileName(systemFileName) {
|
|
158
|
+
if (!systemFileName || typeof systemFileName !== 'string') return '';
|
|
159
|
+
return systemFileName.replace(/-system\.(yaml|yml|json)$/i, '');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Validates that authentication.security paths in system files match the canonical path
|
|
164
|
+
* (kv://<systemKey>/<getKvPathSegmentForSecurityKey(securityKey)>). Pushes errors when they differ.
|
|
165
|
+
*
|
|
166
|
+
* @async
|
|
167
|
+
* @function validateAuthSecurityPathConsistency
|
|
168
|
+
* @param {string} appPath - Application path (integration or builder dir)
|
|
169
|
+
* @param {string[]} errors - Errors array to push to
|
|
170
|
+
* @param {string[]} warnings - Warnings array to push to (unused; for API consistency)
|
|
171
|
+
*/
|
|
172
|
+
async function validateAuthSecurityPathConsistency(appPath, errors, _warnings) {
|
|
173
|
+
try {
|
|
174
|
+
const { schemaBasePath, systemFiles } = await loadExternalIntegrationConfig(appPath);
|
|
175
|
+
for (const systemFileName of systemFiles) {
|
|
176
|
+
const systemKey = systemKeyFromFileName(systemFileName);
|
|
177
|
+
if (!systemKey) continue;
|
|
178
|
+
const systemJson = await loadSystemFile(appPath, schemaBasePath, systemFileName);
|
|
179
|
+
const security = systemJson.authentication?.security;
|
|
180
|
+
if (!security || typeof security !== 'object') continue;
|
|
181
|
+
for (const [key, value] of Object.entries(security)) {
|
|
182
|
+
if (typeof value !== 'string' || !/^kv:\/\/.+/.test(value)) continue;
|
|
183
|
+
const canonicalSegment = getKvPathSegmentForSecurityKey(key);
|
|
184
|
+
const canonicalPath = canonicalSegment ? `kv://${systemKey}/${canonicalSegment}` : null;
|
|
185
|
+
if (canonicalPath && value !== canonicalPath) {
|
|
186
|
+
errors.push(
|
|
187
|
+
`authentication.security.${key} has path ${value}; canonical path is ${canonicalPath}. Run \`aifabrix repair <app>\` to normalize.`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
_warnings.push(`Could not validate auth path consistency: ${error.message}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
151
197
|
module.exports = {
|
|
152
198
|
extractKvPathsFromEnvTemplate,
|
|
153
199
|
extractKvPathsFromCommentedLines,
|
|
154
200
|
setHasPathIgnoreCase,
|
|
155
201
|
collectRequiredAuthKvPaths,
|
|
156
|
-
validateAuthKvCoverage
|
|
202
|
+
validateAuthKvCoverage,
|
|
203
|
+
validateAuthSecurityPathConsistency,
|
|
204
|
+
systemKeyFromFileName
|
|
157
205
|
};
|
|
@@ -13,6 +13,8 @@ const Ajv = require('ajv');
|
|
|
13
13
|
const fs = require('fs');
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const { formatValidationErrors } = require('../utils/error-formatter');
|
|
16
|
+
const { validateFieldReferences } = require('../datasource/field-reference-validator');
|
|
17
|
+
const { validateAbac } = require('../datasource/abac-validator');
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* Sets up AJV validator with external schemas
|
|
@@ -113,6 +115,12 @@ function validateDatasources(manifest, ajv, externalDatasourceSchema, errors, wa
|
|
|
113
115
|
if (!datasourceValid) {
|
|
114
116
|
const datasourceErrors = formatValidationErrors(validateDatasource.errors);
|
|
115
117
|
errors.push(...datasourceErrors.map(err => `Datasource ${index + 1} (${datasource.key || 'unknown'}): ${err}`));
|
|
118
|
+
} else {
|
|
119
|
+
const fieldRefErrors = validateFieldReferences(datasource);
|
|
120
|
+
const abacErrors = validateAbac(datasource);
|
|
121
|
+
const prefix = `Datasource ${index + 1} (${datasource.key || 'unknown'}): `;
|
|
122
|
+
fieldRefErrors.forEach(e => errors.push(prefix + e));
|
|
123
|
+
abacErrors.forEach(e => errors.push(prefix + e));
|
|
116
124
|
}
|
|
117
125
|
});
|
|
118
126
|
}
|
|
@@ -15,6 +15,8 @@ const validator = require('./validator');
|
|
|
15
15
|
const { resolveExternalFiles } = require('../utils/schema-resolver');
|
|
16
16
|
const { loadExternalSystemSchema, loadExternalDataSourceSchema, detectSchemaType } = require('../utils/schema-loader');
|
|
17
17
|
const { formatValidationErrors } = require('../utils/error-formatter');
|
|
18
|
+
const { validateFieldReferences } = require('../datasource/field-reference-validator');
|
|
19
|
+
const { validateAbac } = require('../datasource/abac-validator');
|
|
18
20
|
const { detectAppType } = require('../utils/paths');
|
|
19
21
|
const batch = require('./validate-batch');
|
|
20
22
|
const { logOfflinePathWhenType } = require('../utils/cli-utils');
|
|
@@ -200,6 +202,12 @@ async function validateExternalFile(filePath, type) {
|
|
|
200
202
|
validateConfigurationNoStandardAuthVariables(parseResult.parsed, errors);
|
|
201
203
|
}
|
|
202
204
|
|
|
205
|
+
if (normalizedType === 'datasource') {
|
|
206
|
+
const fieldRefErrors = validateFieldReferences(parseResult.parsed);
|
|
207
|
+
const abacErrors = validateAbac(parseResult.parsed);
|
|
208
|
+
errors.push(...fieldRefErrors, ...abacErrors);
|
|
209
|
+
}
|
|
210
|
+
|
|
203
211
|
return {
|
|
204
212
|
valid: errors.length === 0,
|
|
205
213
|
errors,
|