@aifabrix/builder 2.42.1 → 2.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +1 -1
  2. package/bin/aifabrix.js +1 -1
  3. package/integration/hubspot-test/README.md +126 -0
  4. package/integration/{hubspot → hubspot-test}/application.json +6 -6
  5. package/integration/{hubspot → hubspot-test}/create-hubspot.js +5 -5
  6. package/integration/hubspot-test/env.template +4 -0
  7. package/integration/{hubspot/hubspot-datasource-company.json → hubspot-test/hubspot-test-datasource-company.json} +3 -2
  8. package/integration/{hubspot/hubspot-datasource-contact.json → hubspot-test/hubspot-test-datasource-contact.json} +3 -2
  9. package/integration/{hubspot/hubspot-datasource-deal.json → hubspot-test/hubspot-test-datasource-deal.json} +3 -2
  10. package/integration/{hubspot/hubspot-datasource-users.json → hubspot-test/hubspot-test-datasource-users.json} +3 -2
  11. package/integration/{hubspot/hubspot-deploy.json → hubspot-test/hubspot-test-deploy.json} +198 -21
  12. package/integration/{hubspot/hubspot-system.json → hubspot-test/hubspot-test-system.json} +8 -7
  13. package/integration/hubspot-test/rbac.json +166 -0
  14. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-hubspot-credential-real.yaml +3 -3
  15. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-hubspot-env-vars.yaml +2 -2
  16. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-add-datasource.yaml +1 -1
  17. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-credential-create.yaml +1 -1
  18. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-credential-select.yaml +1 -1
  19. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-known-platform.yaml +1 -1
  20. package/integration/hubspot-test/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
  21. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-mode.yaml +1 -1
  22. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-openapi-file.yaml +1 -1
  23. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-openapi-url.yaml +1 -1
  24. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-source.yaml +1 -1
  25. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-dimension-array-test.yaml +1 -1
  26. package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
  27. package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
  28. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-dimension-test.yaml +1 -1
  29. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-rbac-test.yaml +1 -1
  30. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +1 -1
  31. package/integration/{hubspot → hubspot-test}/test.js +102 -59
  32. package/integration/{hubspot → hubspot-test}/wizard-hubspot-e2e.yaml +2 -2
  33. package/integration/{hubspot → hubspot-test}/wizard-hubspot-platform.yaml +1 -1
  34. package/lib/api/external-test.api.js +1 -1
  35. package/lib/api/service-users.api.js +111 -2
  36. package/lib/api/types/service-users.types.js +41 -0
  37. package/lib/app/register.js +3 -1
  38. package/lib/app/rotate-secret.js +3 -0
  39. package/lib/cli/setup-app.js +2 -2
  40. package/lib/cli/setup-auth.js +19 -11
  41. package/lib/cli/setup-dev.js +62 -32
  42. package/lib/cli/setup-environment.js +6 -21
  43. package/lib/cli/setup-infra.js +13 -0
  44. package/lib/cli/setup-secrets.js +45 -6
  45. package/lib/cli/setup-service-user.js +146 -20
  46. package/lib/cli/setup-utility.js +12 -0
  47. package/lib/commands/auth-config.js +4 -8
  48. package/lib/commands/datasource.js +46 -1
  49. package/lib/commands/dev-init.js +1 -1
  50. package/lib/commands/repair-env-template.js +14 -8
  51. package/lib/commands/repair-rbac.js +25 -19
  52. package/lib/commands/repair.js +96 -30
  53. package/lib/commands/secrets-remove.js +1 -1
  54. package/lib/commands/secrets-validate.js +17 -4
  55. package/lib/commands/service-user.js +231 -2
  56. package/lib/commands/up-common.js +25 -0
  57. package/lib/commands/up-dataplane.js +2 -2
  58. package/lib/core/admin-secrets.js +2 -0
  59. package/lib/core/config.js +7 -5
  60. package/lib/core/ensure-encryption-key.js +1 -3
  61. package/lib/core/secrets.js +32 -9
  62. package/lib/core/templates.js +1 -1
  63. package/lib/datasource/abac-validator.js +157 -0
  64. package/lib/datasource/field-reference-validator.js +74 -36
  65. package/lib/datasource/log-viewer.js +221 -0
  66. package/lib/datasource/resolve-app.js +109 -0
  67. package/lib/datasource/test-e2e.js +11 -20
  68. package/lib/datasource/test-integration.js +42 -22
  69. package/lib/datasource/validate.js +5 -2
  70. package/lib/external-system/generator.js +12 -8
  71. package/lib/external-system/test-system-level.js +1 -1
  72. package/lib/generator/external-controller-manifest.js +3 -3
  73. package/lib/generator/external.js +7 -7
  74. package/lib/generator/helpers.js +13 -9
  75. package/lib/generator/index.js +4 -4
  76. package/lib/generator/split.js +45 -10
  77. package/lib/generator/wizard.js +9 -6
  78. package/lib/infrastructure/helpers.js +50 -35
  79. package/lib/infrastructure/index.js +39 -23
  80. package/lib/schema/env-config.yaml +19 -2
  81. package/lib/schema/external-datasource.schema.json +11 -1
  82. package/lib/utils/app-config-resolver.js +23 -1
  83. package/lib/utils/config-paths.js +48 -4
  84. package/lib/utils/credential-secrets-env.js +16 -1
  85. package/lib/utils/env-map.js +7 -3
  86. package/lib/utils/error-formatter.js +37 -0
  87. package/lib/utils/external-env-template.js +180 -0
  88. package/lib/utils/external-system-display.js +43 -0
  89. package/lib/utils/external-system-validators.js +2 -2
  90. package/lib/utils/help-builder.js +3 -5
  91. package/lib/utils/local-secrets.js +26 -3
  92. package/lib/utils/paths.js +2 -1
  93. package/lib/utils/secrets-generator.js +2 -2
  94. package/lib/utils/secrets-utils.js +4 -0
  95. package/lib/utils/secure-file-permissions.js +91 -0
  96. package/lib/utils/token-manager.js +36 -3
  97. package/lib/utils/yaml-preserve.js +59 -1
  98. package/lib/validation/env-template-auth.js +50 -2
  99. package/lib/validation/external-manifest-validator.js +8 -0
  100. package/lib/validation/validate.js +8 -0
  101. package/lib/validation/validator.js +10 -13
  102. package/package.json +5 -1
  103. package/templates/applications/dataplane/env.template +5 -1
  104. package/templates/applications/miso-controller/application.yaml +1 -1
  105. package/templates/applications/miso-controller/env.template +13 -2
  106. package/templates/external-system/env.template.hbs +22 -0
  107. package/integration/hubspot/README.md +0 -102
  108. package/integration/hubspot/env.template +0 -4
  109. package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +0 -2
  110. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +0 -5
  111. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +0 -5
  112. /package/integration/{hubspot → hubspot-test}/companies.json +0 -0
  113. /package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-app-name.yaml +0 -0
  114. /package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-missing-app.yaml +0 -0
  115. /package/integration/{hubspot → hubspot-test}/test-dataplane-down-helpers.js +0 -0
  116. /package/integration/{hubspot → hubspot-test}/test-dataplane-down-tests.js +0 -0
  117. /package/integration/{hubspot → hubspot-test}/test-dataplane-down.js +0 -0
@@ -198,7 +198,7 @@ function saveSecretsFile(resolvedPath, secrets) {
198
198
 
199
199
  const yamlContent = yaml.dump(secrets, {
200
200
  indent: 2,
201
- lineWidth: 120,
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: 120, noRefs: true, sortKeys: false };
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
- * Load client credentials from secrets.local.yaml or process.env (e.g. integration/hubspot/.env).
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,
@@ -11,7 +11,6 @@
11
11
 
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
- const yaml = require('js-yaml');
15
14
  const Ajv = require('ajv');
16
15
  const applicationSchema = require('../schema/application-schema.json');
17
16
  const externalSystemSchema = require('../schema/external-system.schema.json');
@@ -19,9 +18,9 @@ const externalDataSourceSchema = require('../schema/external-datasource.schema.j
19
18
  const { transformVariablesForValidation } = require('../utils/variable-transformer');
20
19
  const { checkEnvironment } = require('../utils/environment-checker');
21
20
  const { formatValidationErrors } = require('../utils/error-formatter');
22
- const { detectAppType, resolveApplicationConfigPath } = require('../utils/paths');
21
+ const { detectAppType, resolveApplicationConfigPath, resolveRbacPath } = require('../utils/paths');
23
22
  const { loadConfigFile } = require('../utils/config-format');
24
- const { validateAuthKvCoverage } = require('./env-template-auth');
23
+ const { validateAuthKvCoverage, validateAuthSecurityPathConsistency } = require('./env-template-auth');
25
24
  const { validateKvReferencesInLines } = require('./env-template-kv');
26
25
 
27
26
  /**
@@ -155,7 +154,7 @@ async function validateVariables(appName, options = {}) {
155
154
  function validateRoles(roles) {
156
155
  const errors = [];
157
156
  if (!roles || !Array.isArray(roles)) {
158
- errors.push('rbac.yaml must contain a "roles" array');
157
+ errors.push('rbac file must contain a "roles" array');
159
158
  return errors;
160
159
  }
161
160
 
@@ -179,7 +178,7 @@ function validateRoles(roles) {
179
178
  function validatePermissions(permissions) {
180
179
  const errors = [];
181
180
  if (!permissions || !Array.isArray(permissions)) {
182
- errors.push('rbac.yaml must contain a "permissions" array');
181
+ errors.push('rbac file must contain a "permissions" array');
183
182
  return errors;
184
183
  }
185
184
 
@@ -203,21 +202,18 @@ async function validateRbac(appName, options = {}) {
203
202
 
204
203
  // Support both builder/ and integration/ directories using detectAppType
205
204
  const { appPath } = await detectAppType(appName, options);
206
- const rbacYaml = path.join(appPath, 'rbac.yaml');
207
- const rbacYml = path.join(appPath, 'rbac.yml');
208
- const rbacPath = fs.existsSync(rbacYaml) ? rbacYaml : (fs.existsSync(rbacYml) ? rbacYml : null);
205
+ const rbacPath = resolveRbacPath(appPath);
209
206
 
210
207
  if (!rbacPath) {
211
- return { valid: true, errors: [], warnings: ['rbac.yaml not found - authentication disabled'] };
208
+ return { valid: true, errors: [], warnings: ['rbac file not found - authentication disabled'] };
212
209
  }
213
210
 
214
- const content = fs.readFileSync(rbacPath, 'utf8');
215
211
  let rbac;
216
-
217
212
  try {
218
- rbac = yaml.load(content);
213
+ rbac = loadConfigFile(rbacPath);
219
214
  } catch (error) {
220
- throw new Error(`Invalid YAML syntax in rbac.yaml: ${error.message}`);
215
+ const basename = path.basename(rbacPath);
216
+ throw new Error(`Invalid syntax in ${basename}: ${error.message}`);
221
217
  }
222
218
 
223
219
  const errors = [
@@ -287,6 +283,7 @@ async function validateEnvTemplate(appName, options = {}) {
287
283
 
288
284
  if (isExternal) {
289
285
  await validateAuthKvCoverage(appPath, content, errors, warnings, options);
286
+ await validateAuthSecurityPathConsistency(appPath, errors, warnings);
290
287
  }
291
288
 
292
289
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.42.1",
3
+ "version": "2.43.0",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -17,6 +17,10 @@
17
17
  "test:integration": "jest --config jest.config.integration.js --runInBand",
18
18
  "test:integration:python": "cross-env TEST_LANGUAGE=python jest --config jest.config.integration.js --runInBand",
19
19
  "test:integration:typescript": "cross-env TEST_LANGUAGE=typescript jest --config jest.config.integration.js --runInBand",
20
+ "test:hubspot-wizard": "node integration/hubspot-test/test.js",
21
+ "test:hubspot-wizard:negative": "node integration/hubspot-test/test.js --type negative",
22
+ "test:hubspot-wizard:positive": "node integration/hubspot-test/test.js --type positive",
23
+ "test:hubspot-dataplane-down": "node integration/hubspot-test/test-dataplane-down.js",
20
24
  "test:manual": "jest --config jest.config.manual.js --runInBand",
21
25
  "lint": "eslint . --ext .js",
22
26
  "lint:fix": "eslint . --ext .js --fix",
@@ -54,10 +54,14 @@ DATABASE_URL=kv://databases-dataplane-0-urlKeyVault
54
54
  DB_0_PASSWORD=kv://databases-dataplane-0-passwordKeyVault
55
55
 
56
56
  # Vector and document store DB: chunks, embeddings, vector indexes (pgvector).
57
- # Binaries path: config.processing.fileStoragePath or /data/documents.
58
57
  VECTOR_DATABASE_URL=kv://databases-dataplane-1-urlKeyVault
59
58
  DB_1_PASSWORD=kv://databases-dataplane-1-passwordKeyVault
60
59
 
60
+ # Base path for document binary storage (used when datasource config has no processing.fileStoragePath).
61
+ # Dataplane creates subdirs per datasource key (e.g. DOCUMENT_STORAGE_BASE_PATH/test-e2e-sharepoint-documents).
62
+ # Production: use a writable path (e.g. /data/documents) and mount a volume. Local/Docker: use /tmp/documents or /app/data/documents.
63
+ DOCUMENT_STORAGE_BASE_PATH=/mnt/data/documents
64
+
61
65
  # Logs Database Configuration (for execution, audit, ABAC traces)
62
66
  LOGS_DATABASE_URL=kv://databases-dataplane-2-urlKeyVault
63
67
  DB_2_PASSWORD=kv://databases-dataplane-2-passwordKeyVault
@@ -4,7 +4,7 @@ app:
4
4
  displayName: 'Miso Controller'
5
5
  description: 'Miso is the AI Fabrix in-tenant controller and portal layer for securely operating enterprise AI apps inside a customer’s Azure tenant. It provides Entra ID SSO, RBAC, audit logs, environment/app configuration via schemas, and safe secret handling via Key Vault references—ensuring governance, traceability, and predictable UX across portal, SDK, and CLI.'
6
6
  type: webapp
7
- version: '1.8.0'
7
+ version: '1.9.0'
8
8
 
9
9
  # Image Configuration
10
10
  image:
@@ -111,6 +111,10 @@ REDIS_PERMISSIONS_TTL=900
111
111
  KEYCLOAK_REALM=aifabrix
112
112
  KEYCLOAK_SERVER_URL=kv://keycloak-server-url
113
113
  KEYCLOAK_INTERNAL_SERVER_URL=kv://keycloak-internal-server-url
114
+ # Docker/internal host and port: used when config from DB has localhost (getDockerKeycloakInternalUrl).
115
+ # Resolved from env-config (e.g. KEYCLOAK_HOST=keycloak, KEYCLOAK_PORT=8080 for docker).
116
+ KEYCLOAK_HOST=${KEYCLOAK_HOST}
117
+ KEYCLOAK_PORT=${KEYCLOAK_PORT}
114
118
  KEYCLOAK_CLIENT_ID=miso-controller
115
119
  KEYCLOAK_CLIENT_SECRET=kv://keycloak-client-secretKeyVault
116
120
  KEYCLOAK_ADMIN_USERNAME=admin
@@ -306,8 +310,14 @@ MISO_ALLOWED_ORIGINS=http://localhost:*
306
310
  # =============================================================================
307
311
  # LICENSE CONFIGURATION
308
312
  # =============================================================================
309
- # Temporary development bypass: set LICENSE_JWT=DEVELOPMENT to skip Mori validation.
310
- # Will be replaced by JWT license validation (see plan 131-jwt_license_offline_validation).
313
+ # Offline JWT license (optional):
314
+ # - If set, controller validates license offline (RS256) without Mori subscription status call.
315
+ # - Value can be literal JWT or kv:// reference.
316
+ # - If not set, controller falls back to existing Mori subscription validation flow.
317
+ #
318
+ # Development: set to DEVELOPMENT to disable license validation (no Mori/JWT required):
319
+ # LICENSE_JWT=DEVELOPMENT
320
+ # - Use only for local development; do not use in production.
311
321
  LICENSE_JWT=DEVELOPMENT
312
322
 
313
323
  # =============================================================================
@@ -315,6 +325,7 @@ LICENSE_JWT=DEVELOPMENT
315
325
  # =============================================================================
316
326
 
317
327
  MORI_BASE_URL=kv://mori-controller-url
328
+ MORI_AUTH_METHOD=apiKey
318
329
  MORI_API_KEY=kv://mori-controller-api-keyKeyVault
319
330
  MORI_USERNAME=kv://mori-controller-basic-usernameKeyVault
320
331
  MORI_PASSWORD=kv://mori-controller-basic-passwordKeyVault
@@ -0,0 +1,22 @@
1
+ # Environment variables for external system integration
2
+ # Use kv:// (or aifabrix secret set) for sensitive values; plain values for non-sensitive configuration.
3
+ #
4
+
5
+ {{#if authMethod}}
6
+ # Authentication
7
+ # Type: {{authMethod}}
8
+ {{#each authSecureVars}}
9
+ {{name}}={{value}}
10
+ {{/each}}
11
+ {{#if authNonSecureVarNames}}
12
+ # Non-secure (e.g. URLs): {{#each authNonSecureVarNames}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}
13
+ {{/if}}
14
+
15
+ {{/if}}
16
+ {{#if configuration.length}}
17
+ # Configuration
18
+ {{#each configuration}}
19
+ # {{comment}}
20
+ {{name}}={{value}}
21
+ {{/each}}
22
+ {{/if}}