@aifabrix/builder 2.44.4 → 2.44.6

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 (214) hide show
  1. package/.cursor/rules/cli-layout.mdc +1 -1
  2. package/.cursor/rules/project-rules.mdc +1 -1
  3. package/.npmrc.token +1 -1
  4. package/README.md +15 -23
  5. package/integration/hubspot-test/README.md +2 -0
  6. package/integration/hubspot-test/test.js +5 -3
  7. package/jest.projects.js +68 -17
  8. package/lib/api/controller-health.api.js +49 -0
  9. package/lib/api/dimension-values.api.js +82 -0
  10. package/lib/api/dimensions.api.js +114 -0
  11. package/lib/api/external-systems.api.js +1 -0
  12. package/lib/api/integration-clients.api.js +168 -0
  13. package/lib/api/types/dimension-values.types.js +28 -0
  14. package/lib/api/types/dimensions.types.js +31 -0
  15. package/lib/api/types/integration-clients.types.js +45 -0
  16. package/lib/api/types/wizard.types.js +2 -1
  17. package/lib/api/validation-runner.js +46 -25
  18. package/lib/app/deploy-config.js +11 -1
  19. package/lib/app/deploy-status-display.js +3 -3
  20. package/lib/app/deploy.js +36 -14
  21. package/lib/app/display.js +15 -11
  22. package/lib/app/push.js +46 -23
  23. package/lib/app/register.js +1 -1
  24. package/lib/app/restart-display.js +95 -0
  25. package/lib/app/rotate-secret.js +1 -1
  26. package/lib/app/run-container-start.js +12 -6
  27. package/lib/app/run-env-compose.js +30 -1
  28. package/lib/app/run-helpers.js +44 -12
  29. package/lib/app/run-reload-sync.js +148 -0
  30. package/lib/app/run-resolve-image.js +51 -1
  31. package/lib/app/run.js +99 -73
  32. package/lib/build/index.js +75 -45
  33. package/lib/cli/doctor-check.js +117 -0
  34. package/lib/cli/index.js +8 -2
  35. package/lib/cli/infra-guided.js +445 -0
  36. package/lib/cli/setup-app.help.js +1 -1
  37. package/lib/cli/setup-app.js +20 -2
  38. package/lib/cli/setup-app.test-commands.js +9 -5
  39. package/lib/cli/setup-auth.js +26 -0
  40. package/lib/cli/setup-dev-path-commands.js +50 -3
  41. package/lib/cli/setup-infra.js +138 -61
  42. package/lib/cli/setup-integration-client.js +182 -0
  43. package/lib/cli/setup-parameters.js +21 -2
  44. package/lib/cli/setup-platform.js +102 -0
  45. package/lib/cli/setup-secrets.js +18 -6
  46. package/lib/cli/setup-utility.js +97 -33
  47. package/lib/commands/datasource-capability-dimension-cli.js +128 -0
  48. package/lib/commands/datasource-capability-output.js +29 -0
  49. package/lib/commands/datasource-capability-relate-cli.js +140 -0
  50. package/lib/commands/datasource-capability.js +411 -0
  51. package/lib/commands/datasource-unified-test-cli.options.js +1 -1
  52. package/lib/commands/datasource.js +53 -13
  53. package/lib/commands/dev-down.js +3 -3
  54. package/lib/commands/dev-infra-gate.js +32 -0
  55. package/lib/commands/dev-init.js +13 -7
  56. package/lib/commands/dimension-value.js +179 -0
  57. package/lib/commands/dimension.js +330 -0
  58. package/lib/commands/integration-client.js +430 -0
  59. package/lib/commands/login-device.js +65 -30
  60. package/lib/commands/login.js +21 -10
  61. package/lib/commands/parameters-validate.js +78 -13
  62. package/lib/commands/repair-datasource-auto-rbac.js +166 -0
  63. package/lib/commands/repair-datasource-keys.js +10 -5
  64. package/lib/commands/repair-datasource.js +19 -7
  65. package/lib/commands/repair-env-template.js +4 -1
  66. package/lib/commands/repair-openapi-sync.js +172 -0
  67. package/lib/commands/repair-persist.js +102 -0
  68. package/lib/commands/repair-rbac-extract.js +27 -0
  69. package/lib/commands/repair-rbac-migrate.js +186 -0
  70. package/lib/commands/repair-rbac.js +225 -19
  71. package/lib/commands/repair-system-alignment.js +246 -0
  72. package/lib/commands/repair-system-permissions.js +168 -0
  73. package/lib/commands/repair.js +120 -354
  74. package/lib/commands/secure.js +1 -1
  75. package/lib/commands/setup-modes.js +455 -0
  76. package/lib/commands/setup-prompts.js +388 -0
  77. package/lib/commands/setup.js +149 -0
  78. package/lib/commands/teardown.js +228 -0
  79. package/lib/commands/test-e2e-external.js +4 -3
  80. package/lib/commands/up-common.js +97 -12
  81. package/lib/commands/up-dataplane.js +33 -11
  82. package/lib/commands/up-miso.js +7 -11
  83. package/lib/commands/upload.js +109 -23
  84. package/lib/commands/wizard-core-helpers.js +14 -11
  85. package/lib/commands/wizard-core.js +58 -15
  86. package/lib/commands/wizard-dataplane.js +2 -2
  87. package/lib/commands/wizard-entity-selection.js +72 -14
  88. package/lib/commands/wizard-headless.js +7 -3
  89. package/lib/commands/wizard-helpers.js +13 -1
  90. package/lib/commands/wizard.js +210 -61
  91. package/lib/constants/infra-compose-service-names.js +40 -0
  92. package/lib/core/env-reader.js +16 -3
  93. package/lib/core/secrets-admin-env.js +101 -0
  94. package/lib/core/secrets-ensure-infra.js +34 -1
  95. package/lib/core/secrets-ensure.js +88 -66
  96. package/lib/core/secrets-env-content.js +432 -0
  97. package/lib/core/secrets-env-write.js +27 -1
  98. package/lib/core/secrets-load.js +248 -0
  99. package/lib/core/secrets-names.js +32 -0
  100. package/lib/core/secrets.js +17 -757
  101. package/lib/datasource/capability/basic-exposure.js +76 -0
  102. package/lib/datasource/capability/capability-diff-slice.js +41 -0
  103. package/lib/datasource/capability/capability-key.js +34 -0
  104. package/lib/datasource/capability/capability-resolve.js +172 -0
  105. package/lib/datasource/capability/capability-storage-keys.js +22 -0
  106. package/lib/datasource/capability/copy-operations.js +348 -0
  107. package/lib/datasource/capability/copy-test-payload.js +139 -0
  108. package/lib/datasource/capability/create-operations.js +235 -0
  109. package/lib/datasource/capability/dimension-operations.js +151 -0
  110. package/lib/datasource/capability/dimension-validate.js +219 -0
  111. package/lib/datasource/capability/json-pointer.js +31 -0
  112. package/lib/datasource/capability/reference-rewrite.js +51 -0
  113. package/lib/datasource/capability/relate-operations.js +325 -0
  114. package/lib/datasource/capability/relate-validate.js +219 -0
  115. package/lib/datasource/capability/remove-operations.js +275 -0
  116. package/lib/datasource/capability/run-capability-copy.js +152 -0
  117. package/lib/datasource/capability/run-capability-diff.js +135 -0
  118. package/lib/datasource/capability/run-capability-dimension.js +291 -0
  119. package/lib/datasource/capability/run-capability-edit.js +377 -0
  120. package/lib/datasource/capability/run-capability-relate.js +193 -0
  121. package/lib/datasource/capability/run-capability-remove.js +105 -0
  122. package/lib/datasource/capability/templates/minimal-fetch.json +18 -0
  123. package/lib/datasource/capability/validate-capability-slice.js +35 -0
  124. package/lib/datasource/list.js +136 -23
  125. package/lib/datasource/log-viewer.js +2 -4
  126. package/lib/datasource/unified-validation-run.js +51 -16
  127. package/lib/datasource/validate.js +53 -1
  128. package/lib/deployment/deploy-poll-ui.js +60 -0
  129. package/lib/deployment/deployer-status.js +29 -3
  130. package/lib/deployment/deployer.js +48 -30
  131. package/lib/deployment/environment.js +7 -2
  132. package/lib/deployment/poll-interval.js +72 -0
  133. package/lib/deployment/push.js +11 -9
  134. package/lib/external-system/deploy.js +4 -1
  135. package/lib/external-system/download.js +61 -32
  136. package/lib/external-system/sync-deploy-manifest.js +33 -0
  137. package/lib/generator/wizard-prompts.js +7 -1
  138. package/lib/generator/wizard.js +34 -0
  139. package/lib/infrastructure/index.js +49 -19
  140. package/lib/infrastructure/orphan-infra-docker-teardown.js +177 -0
  141. package/lib/parameters/infra-kv-discovery.js +29 -4
  142. package/lib/parameters/infra-parameter-catalog.js +6 -3
  143. package/lib/parameters/infra-parameter-validate.js +67 -19
  144. package/lib/resolvers/datasource-resolver.js +53 -0
  145. package/lib/resolvers/dimension-file.js +52 -0
  146. package/lib/resolvers/manifest-resolver.js +133 -0
  147. package/lib/schema/external-datasource.schema.json +183 -53
  148. package/lib/schema/external-system.schema.json +23 -10
  149. package/lib/schema/infra.parameter.yaml +26 -11
  150. package/lib/schema/wizard-config.schema.json +2 -2
  151. package/lib/utils/aifabrix-config-dir-walk.js +40 -0
  152. package/lib/utils/aifabrix-runtime-config-dir.js +26 -3
  153. package/lib/utils/app-run-containers.js +2 -2
  154. package/lib/utils/bash-secret-env.js +59 -0
  155. package/lib/utils/cli-secrets-error-format.js +78 -0
  156. package/lib/utils/cli-test-layout-chalk.js +31 -9
  157. package/lib/utils/cli-utils.js +4 -36
  158. package/lib/utils/datasource-test-run-display.js +8 -0
  159. package/lib/utils/dev-hosts-helper.js +3 -2
  160. package/lib/utils/dev-init-ssh-merge.js +2 -1
  161. package/lib/utils/docker-build.js +17 -9
  162. package/lib/utils/docker-reload-mount.js +127 -0
  163. package/lib/utils/external-readme.js +117 -4
  164. package/lib/utils/external-system-local-test-tty.js +3 -2
  165. package/lib/utils/external-system-readiness-core.js +45 -12
  166. package/lib/utils/external-system-readiness-deploy-display.js +3 -3
  167. package/lib/utils/external-system-readiness-display-internals.js +33 -3
  168. package/lib/utils/external-system-readiness-display.js +10 -1
  169. package/lib/utils/file-upload.js +40 -3
  170. package/lib/utils/health-check-db-init.js +107 -0
  171. package/lib/utils/health-check-public-warn.js +69 -0
  172. package/lib/utils/health-check-url.js +19 -4
  173. package/lib/utils/health-check.js +135 -105
  174. package/lib/utils/help-builder.js +5 -1
  175. package/lib/utils/image-name.js +34 -7
  176. package/lib/utils/integration-file-backup.js +74 -0
  177. package/lib/utils/mutagen-install.js +30 -3
  178. package/lib/utils/paths.js +108 -25
  179. package/lib/utils/postgres-wipe.js +212 -0
  180. package/lib/utils/register-aifabrix-shell-env.js +15 -0
  181. package/lib/utils/remote-dev-auth.js +21 -5
  182. package/lib/utils/remote-docker-env.js +9 -1
  183. package/lib/utils/remote-secrets-loader.js +42 -3
  184. package/lib/utils/resolve-docker-image-ref.js +9 -3
  185. package/lib/utils/secrets-ancestor-paths.js +47 -0
  186. package/lib/utils/secrets-helpers.js +17 -10
  187. package/lib/utils/secrets-kv-refs.js +42 -0
  188. package/lib/utils/secrets-kv-scope.js +19 -2
  189. package/lib/utils/secrets-materialize-local.js +134 -0
  190. package/lib/utils/secrets-path.js +24 -10
  191. package/lib/utils/secrets-utils.js +2 -2
  192. package/lib/utils/system-builder-root.js +34 -0
  193. package/lib/utils/url-declarative-resolve-build.js +6 -1
  194. package/lib/utils/url-declarative-runtime-base-path.js +32 -0
  195. package/lib/utils/url-declarative-vdir-inactive-env.js +2 -1
  196. package/lib/utils/urls-local-registry.js +73 -20
  197. package/lib/utils/validation-poll-ui.js +81 -0
  198. package/lib/utils/validation-run-poll.js +29 -5
  199. package/lib/utils/with-muted-logger.js +53 -0
  200. package/package.json +1 -1
  201. package/templates/applications/dataplane/application.yaml +1 -1
  202. package/templates/applications/dataplane/rbac.yaml +10 -10
  203. package/templates/applications/keycloak/env.template +8 -6
  204. package/templates/applications/miso-controller/application.yaml +7 -0
  205. package/templates/applications/miso-controller/env.template +7 -7
  206. package/templates/applications/miso-controller/rbac.yaml +9 -9
  207. package/templates/external-system/README.md.hbs +89 -102
  208. package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  209. package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  210. package/.nyc_output/processinfo/index.json +0 -1
  211. package/lib/api/service-users.api.js +0 -150
  212. package/lib/api/types/service-users.types.js +0 -65
  213. package/lib/cli/setup-service-user.js +0 -187
  214. package/lib/commands/service-user.js +0 -429
@@ -1,11 +1,15 @@
1
- const { formatBlockingError, formatSuccessLine } = require('../utils/cli-test-layout-chalk');
1
+ const {
2
+ formatBlockingError,
3
+ formatIssue,
4
+ formatSuccessLine,
5
+ sectionTitle
6
+ } = require('../utils/cli-test-layout-chalk');
2
7
  /**
3
8
  * @fileoverview CLI handler: parameters validate (kv:// vs infra.parameter.yaml)
4
9
  * @author AI Fabrix Team
5
10
  * @version 2.0.0
6
11
  */
7
12
 
8
- const chalk = require('chalk');
9
13
  const logger = require('../utils/logger');
10
14
  const pathsUtil = require('../utils/paths');
11
15
  const {
@@ -17,37 +21,98 @@ const {
17
21
  validateCatalogRequiredGenerators
18
22
  } = require('../parameters/infra-parameter-validate');
19
23
 
24
+ function _loadCatalog(catalogPath) {
25
+ try {
26
+ const catalog = catalogPath
27
+ ? loadInfraParameterCatalog(catalogPath)
28
+ : getInfraParameterCatalog();
29
+ return { catalog, catalogPath: catalogPath || 'lib/schema/infra.parameter.yaml' };
30
+ } catch (e) {
31
+ logger.log(formatBlockingError(`Could not load infra parameter catalog: ${e.message}`));
32
+ return null;
33
+ }
34
+ }
35
+
36
+ function _printScanSummary(kv) {
37
+ logger.log(sectionTitle('Scan summary:'));
38
+ logger.log(
39
+ ` Apps scanned: ${kv.summary.scannedApps.length} (builder/* only; integration/* is skipped)`
40
+ );
41
+ logger.log(` env.template files: ${kv.summary.scannedEnvTemplates.length}`);
42
+ logger.log(` kv:// keys (unique): ${kv.summary.kvKeysCount}`);
43
+ }
44
+
45
+ function _printCatalogHint(catalogPath) {
46
+ logger.log(
47
+ formatIssue(
48
+ `Catalog: ${catalogPath}`,
49
+ 'Use --catalog to validate against a different infra.parameter.yaml.'
50
+ )
51
+ );
52
+ }
53
+
54
+ function _printKvErrors(kv) {
55
+ logger.log(sectionTitle('Missing catalog entries:'));
56
+ kv.errors.forEach((err) => {
57
+ if (err.key === '__read_error__') {
58
+ logger.log(
59
+ formatIssue(
60
+ `Could not read ${err.envTemplatePath}`,
61
+ err.message || 'Check file permissions and retry.'
62
+ )
63
+ );
64
+ return;
65
+ }
66
+ logger.log(
67
+ formatIssue(
68
+ `Unknown kv:// key "${err.key}" in ${err.envTemplatePath}`,
69
+ 'Add a matching entry (or keyPattern) in lib/schema/infra.parameter.yaml.'
70
+ )
71
+ );
72
+ });
73
+ }
74
+
75
+ function _printSuccess(kv, catalogPath, verbose) {
76
+ logger.log(formatSuccessLine('parameters validate: catalog OK; workspace kv:// keys covered.'));
77
+ logger.log(` Catalog: ${catalogPath}`);
78
+ _printScanSummary(kv);
79
+ if (verbose) {
80
+ logger.log(sectionTitle('Files scanned:'));
81
+ kv.summary.scannedEnvTemplates.forEach((p) => logger.log(` - ${p}`));
82
+ }
83
+ }
84
+
20
85
  /**
21
86
  * Run catalog + workspace kv:// validation.
22
87
  * @param {Object} [options] - CLI options
23
88
  * @returns {Promise<{ valid: boolean }>}
24
89
  */
25
90
  async function handleParametersValidate(options = {}) {
26
- let catalog;
27
- try {
28
- catalog = options.catalogPath
29
- ? loadInfraParameterCatalog(options.catalogPath)
30
- : getInfraParameterCatalog();
31
- } catch (e) {
32
- logger.log(formatBlockingError(`Could not load infra parameter catalog: ${e.message}`));
91
+ const loaded = _loadCatalog(options.catalogPath);
92
+ if (!loaded) {
33
93
  return { valid: false };
34
94
  }
95
+ const { catalog, catalogPath } = loaded;
35
96
 
36
97
  const reqGen = validateCatalogRequiredGenerators(catalog.data);
37
98
  if (!reqGen.valid) {
38
99
  logger.log(formatBlockingError('Catalog requiredForLocal / generator issues:'));
39
- reqGen.errors.forEach((err) => logger.log(chalk.yellow(` • ${err}`)));
100
+ reqGen.errors.forEach((err) =>
101
+ logger.log(formatIssue(err, 'Fix infra.parameter.yaml generator for requiredForLocal entry.'))
102
+ );
40
103
  return { valid: false };
41
104
  }
42
105
 
43
106
  const kv = validateWorkspaceKvRefsAgainstCatalog(catalog, pathsUtil);
44
107
  if (!kv.valid) {
45
- logger.log(formatBlockingError('env.template kv:// keys not covered by infra.parameter.yaml:'));
46
- kv.errors.forEach((err) => logger.log(chalk.yellow(` • ${err}`)));
108
+ logger.log(formatBlockingError('Missing infra.parameter.yaml coverage for kv:// keys.'));
109
+ _printCatalogHint(catalogPath);
110
+ _printScanSummary(kv);
111
+ _printKvErrors(kv);
47
112
  return { valid: false };
48
113
  }
49
114
 
50
- logger.log(formatSuccessLine('parameters validate: catalog OK; workspace kv:// keys covered.'));
115
+ _printSuccess(kv, catalogPath, Boolean(options.verbose));
51
116
  return { valid: true };
52
117
  }
53
118
 
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Auto-RBAC operation key normalization for datasource OpenAPI sections.
3
+ *
4
+ * @fileoverview Normalize operation keys for RBAC safety and consistency
5
+ * @author AI Fabrix Team
6
+ * @version 2.2.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ function safeString(v) {
12
+ return typeof v === 'string' && v.trim() ? v.trim() : '';
13
+ }
14
+
15
+ function toCamelCaseKey(opKey) {
16
+ const s = safeString(opKey);
17
+ if (!s) return '';
18
+ // Split on common separators, keep only alphanumerics
19
+ const parts = s
20
+ .split(/[^a-zA-Z0-9]+/g)
21
+ .map((p) => p.trim())
22
+ .filter(Boolean);
23
+ if (parts.length === 0) return '';
24
+ const first = parts[0].charAt(0).toLowerCase() + parts[0].slice(1);
25
+ const rest = parts
26
+ .slice(1)
27
+ .map((p) => (p.charAt(0).toUpperCase() + p.slice(1)));
28
+ const out = [first, ...rest].join('');
29
+ // Must match ^[a-z][a-zA-Z0-9]*$
30
+ return /^[a-z][a-zA-Z0-9]*$/.test(out) ? out : '';
31
+ }
32
+
33
+ function buildRenameMap(operationMap) {
34
+ if (!operationMap || typeof operationMap !== 'object' || Array.isArray(operationMap)) return {};
35
+ const renameMap = {};
36
+ for (const k of Object.keys(operationMap)) {
37
+ // If it's already schema-valid but contains capitals, normalize to lowercase to align
38
+ // with RBAC permission name restrictions (external-system schema forbids A-Z).
39
+ if (/^[a-z][a-zA-Z0-9]*$/.test(k)) {
40
+ if (/[A-Z]/.test(k)) {
41
+ renameMap[k] = k.toLowerCase();
42
+ }
43
+ continue;
44
+ }
45
+ const camel = toCamelCaseKey(k);
46
+ if (!camel || camel === k) continue;
47
+ // Use lowercase key to keep RBAC permission names schema-valid.
48
+ renameMap[k] = camel.toLowerCase();
49
+ }
50
+ return renameMap;
51
+ }
52
+
53
+ function toKebabAliasKey(opKey) {
54
+ const s = safeString(opKey);
55
+ if (!s) return '';
56
+ const withHyphens = s.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
57
+ const cleaned = withHyphens.replace(/[^a-z0-9-]+/g, '-');
58
+ return cleaned.replace(/-+/g, '-').replace(/^-+|-+$/g, '');
59
+ }
60
+
61
+ function buildAliasMapFromCanonicalOps(canonicalOps) {
62
+ if (!canonicalOps || typeof canonicalOps !== 'object' || Array.isArray(canonicalOps)) return {};
63
+ const aliasMap = {};
64
+ for (const k of Object.keys(canonicalOps)) {
65
+ // If canonical is createBasic, allow alias create-basic to map back to createBasic
66
+ const alias = toKebabAliasKey(k);
67
+ if (!alias || alias === k) continue;
68
+ if (aliasMap[alias]) continue;
69
+ aliasMap[alias] = k;
70
+ }
71
+ return aliasMap;
72
+ }
73
+
74
+ function renameKeysInObject(obj, renameMap) {
75
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return false;
76
+ let updated = false;
77
+ for (const [oldKey, newKey] of Object.entries(renameMap)) {
78
+ if (!oldKey || !newKey || oldKey === newKey) continue;
79
+ if (obj[oldKey] === undefined) continue;
80
+ if (obj[newKey] !== undefined) continue; // avoid collisions
81
+ obj[newKey] = obj[oldKey];
82
+ delete obj[oldKey];
83
+ updated = true;
84
+ }
85
+ return updated;
86
+ }
87
+
88
+ function mergeAndDeleteAliasKeys(obj, renameMap) {
89
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return false;
90
+ let updated = false;
91
+ for (const [oldKey, newKey] of Object.entries(renameMap)) {
92
+ if (!oldKey || !newKey || oldKey === newKey) continue;
93
+ if (obj[oldKey] === undefined) continue;
94
+ if (obj[newKey] === undefined) continue;
95
+
96
+ const oldVal = obj[oldKey];
97
+ const newVal = obj[newKey];
98
+ if (Array.isArray(oldVal) && Array.isArray(newVal)) {
99
+ const merged = [...new Set([...newVal, ...oldVal])];
100
+ obj[newKey] = merged;
101
+ }
102
+ delete obj[oldKey];
103
+ updated = true;
104
+ }
105
+ return updated;
106
+ }
107
+
108
+ function renameScenarioOperations(parsed, renameMap) {
109
+ const scenarios = parsed?.testPayload?.scenarios;
110
+ if (!Array.isArray(scenarios)) return false;
111
+ let updated = false;
112
+ for (const sc of scenarios) {
113
+ const op = safeString(sc?.operation);
114
+ if (!op) continue;
115
+ const newOp = renameMap[op];
116
+ if (!newOp) continue;
117
+ sc.operation = newOp;
118
+ updated = true;
119
+ }
120
+ return updated;
121
+ }
122
+
123
+ /**
124
+ * When OpenAPI autoRbac is enabled, ensure operation keys are schema-valid and consistent across:
125
+ * - openapi.operations
126
+ * - execution.cip.operations
127
+ * - testPayload.scenarios[].operation
128
+ */
129
+ function normalizeAutoRbacOperationKeys(parsed, changes) {
130
+ const openapi = parsed?.openapi;
131
+ if (!openapi || typeof openapi !== 'object') return false;
132
+ if (openapi.autoRbac !== true) return false;
133
+
134
+ const renameMap = {
135
+ ...buildRenameMap(openapi.operations),
136
+ ...buildAliasMapFromCanonicalOps(openapi.operations)
137
+ };
138
+ const keysToRename = Object.keys(renameMap);
139
+ if (keysToRename.length === 0) return false;
140
+
141
+ const cipOps = parsed?.execution?.cip?.operations;
142
+ let updated = false;
143
+ updated = renameKeysInObject(openapi.operations, renameMap) || updated;
144
+ updated = renameKeysInObject(cipOps, renameMap) || updated;
145
+ updated = renameScenarioOperations(parsed, renameMap) || updated;
146
+
147
+ const wsAllowed = parsed?.fieldMappings?.writeSurface?.allowed;
148
+ updated = renameKeysInObject(wsAllowed, renameMap) || updated;
149
+ // If both alias + canonical exist, merge and drop alias (schema requires canonical camelCase keys).
150
+ updated = mergeAndDeleteAliasKeys(wsAllowed, renameMap) || updated;
151
+
152
+ if (updated && Array.isArray(changes)) {
153
+ const pairs = keysToRename.map((k) => `${k}→${renameMap[k]}`).join(', ');
154
+ changes.push(
155
+ `Normalized autoRbac operation keys (permission names are lowercase per schema; kebab aliases fold into canonical keys): ${pairs}`
156
+ );
157
+ }
158
+
159
+ return updated;
160
+ }
161
+
162
+ module.exports = {
163
+ normalizeAutoRbacOperationKeys,
164
+ toCamelCaseKey
165
+ };
166
+
@@ -15,6 +15,7 @@
15
15
  const path = require('path');
16
16
  const fs = require('fs');
17
17
  const { loadConfigFile, writeConfigFile } = require('../utils/config-format');
18
+ const { backupIntegrationFile } = require('../utils/integration-file-backup');
18
19
 
19
20
  /**
20
21
  * Returns suffix from a canonical-format filename: <systemKey>-datasource-<suffix>.<ext>.
@@ -112,13 +113,12 @@ function isFilenameAlreadyCanonical(fileName, systemKey) {
112
113
  * @param {string} appPath - Application directory path
113
114
  * @param {string[]} datasourceFiles - Current list of datasource filenames
114
115
  * @param {string} systemKey - System key
115
- * @param {Object} variables - Application variables (mutated: externalIntegration.dataSources updated)
116
- * @param {boolean} dryRun - If true, do not write or rename
117
- * @param {string[]} changes - Array to append change descriptions to
116
+ * @param {{ variables: Object, dryRun: boolean, changes: string[], backupCtx?: Object }} writeOpts
118
117
  * @returns {{ updated: boolean, datasourceFiles: string[] }} Updated flag and new list of datasource filenames
119
118
  */
120
119
  /* eslint-disable max-lines-per-function, max-statements, complexity -- Normalization loops and branching per file */
121
- function normalizeDatasourceKeysAndFilenames(appPath, datasourceFiles, systemKey, variables, dryRun, changes) {
120
+ function normalizeDatasourceKeysAndFilenames(appPath, datasourceFiles, systemKey, writeOpts) {
121
+ const { variables, dryRun, changes, backupCtx } = writeOpts;
122
122
  if (!datasourceFiles || datasourceFiles.length === 0) {
123
123
  return { updated: false, datasourceFiles: datasourceFiles || [] };
124
124
  }
@@ -174,7 +174,11 @@ function normalizeDatasourceKeysAndFilenames(appPath, datasourceFiles, systemKey
174
174
 
175
175
  if (parsed.key !== canonicalKey) {
176
176
  parsed.key = canonicalKey;
177
- if (!dryRun) writeConfigFile(path.join(appPath, fileName), parsed);
177
+ if (!dryRun) {
178
+ const fullPath = path.join(appPath, fileName);
179
+ backupIntegrationFile(fullPath, backupCtx);
180
+ writeConfigFile(fullPath, parsed);
181
+ }
178
182
  changes.push(`${fileName}: key → ${canonicalKey}`);
179
183
  updated = true;
180
184
  }
@@ -182,6 +186,7 @@ function normalizeDatasourceKeysAndFilenames(appPath, datasourceFiles, systemKey
182
186
  const oldPath = path.join(appPath, fileName);
183
187
  const newPath = path.join(appPath, canonicalFileName);
184
188
  if (!dryRun && fs.existsSync(oldPath) && !fs.existsSync(newPath)) {
189
+ backupIntegrationFile(oldPath, backupCtx);
185
190
  fs.renameSync(oldPath, newPath);
186
191
  }
187
192
  changes.push(`Renamed ${fileName} → ${canonicalFileName}`);
@@ -10,6 +10,7 @@
10
10
  'use strict';
11
11
 
12
12
  const { repairOpenapiSection } = require('./repair-datasource-openapi');
13
+ const { normalizeAutoRbacOperationKeys } = require('./repair-datasource-auto-rbac');
13
14
 
14
15
  const DEFAULT_SYNC = {
15
16
  mode: 'pull',
@@ -442,13 +443,8 @@ function repairDatasourceFile(parsed, options = {}, changes = []) {
442
443
  let updated = false;
443
444
  const none = isNoneEntityType(parsed?.entityType);
444
445
 
445
- if (!none) {
446
- updated = repairDimensionBindingShape(parsed, out) || updated;
447
- updated = repairRootDimensionsFromAttributes(parsed, out) || updated;
448
- updated = repairMetadataSchemaFromAttributes(parsed, out) || updated;
449
- }
450
-
451
- updated = repairOpenapiSection(parsed, out) || updated;
446
+ updated = applyBaseDatasourceRepairs(parsed, none, out) || updated;
447
+ updated = applyOpenapiDatasourceRepairs(parsed, out) || updated;
452
448
 
453
449
  if (options.expose) {
454
450
  updated = repairExposeFromAttributes(parsed, out) || updated;
@@ -467,6 +463,22 @@ function repairDatasourceFile(parsed, options = {}, changes = []) {
467
463
  return { updated, changes: out };
468
464
  }
469
465
 
466
+ function applyBaseDatasourceRepairs(parsed, none, changes) {
467
+ if (none) return false;
468
+ let updated = false;
469
+ updated = repairDimensionBindingShape(parsed, changes) || updated;
470
+ updated = repairRootDimensionsFromAttributes(parsed, changes) || updated;
471
+ updated = repairMetadataSchemaFromAttributes(parsed, changes) || updated;
472
+ return updated;
473
+ }
474
+
475
+ function applyOpenapiDatasourceRepairs(parsed, changes) {
476
+ let updated = false;
477
+ updated = repairOpenapiSection(parsed, changes) || updated;
478
+ updated = normalizeAutoRbacOperationKeys(parsed, changes) || updated;
479
+ return updated;
480
+ }
481
+
470
482
  module.exports = {
471
483
  getAttributeKeys,
472
484
  parsePathsFromExpressions,
@@ -9,6 +9,7 @@
9
9
 
10
10
  const path = require('path');
11
11
  const fs = require('fs');
12
+ const { backupIntegrationFile } = require('../utils/integration-file-backup');
12
13
  const { systemKeyToKvPrefix, kvEnvKeyToPath, securityKeyToVar } = require('../utils/credential-secrets-env');
13
14
  const { extractEnvTemplate } = require('../generator/split');
14
15
  const { generateExternalEnvTemplateContent } = require('../utils/external-env-template');
@@ -235,9 +236,10 @@ function mergeEnvTemplateContent(content, expectedByKey) {
235
236
  * @param {string} systemKey - System key
236
237
  * @param {boolean} dryRun - If true, do not write
237
238
  * @param {string[]} changes - Array to append change descriptions to
239
+ * @param {Object} [backupCtx] - Optional backup context for backupIntegrationFile
238
240
  * @returns {boolean} True if env.template was repaired or created
239
241
  */
240
- function repairEnvTemplate(appPath, systemParsed, systemKey, dryRun, changes) {
242
+ function repairEnvTemplate(appPath, systemParsed, systemKey, dryRun, changes, backupCtx) {
241
243
  const effective = buildEffectiveConfiguration(systemParsed, systemKey);
242
244
  const expectedByKey = buildExpectedByKey(effective);
243
245
  const envPath = path.join(appPath, 'env.template');
@@ -253,6 +255,7 @@ function repairEnvTemplate(appPath, systemParsed, systemKey, dryRun, changes) {
253
255
  const { output, changed } = mergeEnvTemplateContent(content, expectedByKey);
254
256
 
255
257
  if (changed && !dryRun) {
258
+ backupIntegrationFile(envPath, backupCtx);
256
259
  fs.writeFileSync(envPath, output, { mode: 0o644, encoding: 'utf8' });
257
260
  }
258
261
  if (changed) {
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Upload OpenAPI specs (integration/<systemKey>/openapi/*.json) to dataplane so MCP generation
3
+ * can resolve `openapi.documentKey` and store mcpContract per datasource.
4
+ *
5
+ * This is a *repair* action: upload should remain non-mutating.
6
+ *
7
+ * @fileoverview Repair action: sync OpenAPI files for MCP
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const path = require('path');
15
+ const fs = require('fs');
16
+
17
+ const { resolveControllerUrl } = require('../utils/controller-url');
18
+ const { getDeploymentAuth, requireBearerForDataplanePipeline } = require('../utils/token-manager');
19
+ const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
20
+ const { uploadFileAs } = require('../utils/file-upload');
21
+ const { listOpenAPIFiles } = require('../api/external-systems.api');
22
+
23
+ async function fileExistsAsync(filePath) {
24
+ try {
25
+ await fs.promises.access(filePath);
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ async function resolveDataplaneAndAuth(systemKey) {
33
+ const { resolveEnvironment } = require('../core/config');
34
+ const environment = await resolveEnvironment();
35
+ const controllerUrl = await resolveControllerUrl();
36
+ const authConfig = await getDeploymentAuth(controllerUrl, environment, systemKey);
37
+ requireBearerForDataplanePipeline(authConfig);
38
+ const dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig, { silent: true });
39
+ return { dataplaneUrl, authConfig };
40
+ }
41
+
42
+ function documentKeyToLocalOpenApiPath(appPath, systemKey, documentKey) {
43
+ const openapiDir = path.join(appPath, 'openapi');
44
+ const suffix = documentKey.startsWith(systemKey + '-') ? documentKey.slice(systemKey.length + 1) : documentKey;
45
+ return path.join(openapiDir, `${suffix}.json`);
46
+ }
47
+
48
+ async function uploadOneOpenApiFile(dataplaneUrl, authConfig, systemKey, localPath, documentKey) {
49
+ const url =
50
+ `${dataplaneUrl.replace(/\/$/, '')}` +
51
+ `/api/v1/specs/upload?systemIdOrKey=${encodeURIComponent(systemKey)}`;
52
+ const res = await uploadFileAs(url, localPath, `${documentKey}.json`, 'file', authConfig);
53
+ if (!res || res.success !== true) {
54
+ const msg = res && typeof res.formattedError === 'string'
55
+ ? res.formattedError
56
+ : (res && typeof res.error === 'string' ? res.error : 'OpenAPI upload failed');
57
+ throw new Error(msg);
58
+ }
59
+ }
60
+
61
+ /**
62
+ * @param {Object|null|undefined} res
63
+ * @param {string} fallback
64
+ * @returns {string}
65
+ */
66
+ function failureMessageFromApiResult(res, fallback) {
67
+ if (res && typeof res.formattedError === 'string') return res.formattedError;
68
+ if (res && typeof res.error === 'string') return res.error;
69
+ return fallback;
70
+ }
71
+
72
+ /**
73
+ * @param {Object} res
74
+ * @returns {Array<unknown>}
75
+ */
76
+ function extractOpenApiFileListItems(res) {
77
+ const data = res && res.data;
78
+ if (data && Array.isArray(data.data)) return data.data;
79
+ if (data && Array.isArray(data.items)) return data.items;
80
+ if (res && Array.isArray(res.items)) return res.items;
81
+ return [];
82
+ }
83
+
84
+ async function getExistingOpenApiKeys(dataplaneUrl, systemKey, authConfig) {
85
+ // Note: this endpoint is system-scoped and does not require guessing externalSystemId.
86
+ // Dataplane caps pageSize at 100 (OpenAPI route schema); keep within bounds.
87
+ const res = await listOpenAPIFiles(dataplaneUrl, systemKey, authConfig, { pageSize: 100 });
88
+ if (!res || res.success !== true) {
89
+ throw new Error(failureMessageFromApiResult(res, 'Failed to list OpenAPI files'));
90
+ }
91
+ const keys = new Set();
92
+ for (const it of extractOpenApiFileListItems(res)) {
93
+ if (it && typeof it.key === 'string') keys.add(it.key);
94
+ }
95
+ return keys;
96
+ }
97
+
98
+ function readDatasourceDocumentKey(appPath, datasourceFileName) {
99
+ const dsPath = path.join(appPath, datasourceFileName);
100
+ if (!fs.existsSync(dsPath)) return null;
101
+ const parsed = require('../utils/config-format').loadConfigFile(dsPath);
102
+ const openapi = parsed && typeof parsed.openapi === 'object' && !Array.isArray(parsed.openapi) ? parsed.openapi : null;
103
+ return openapi && typeof openapi.documentKey === 'string' ? openapi.documentKey : null;
104
+ }
105
+
106
+ /**
107
+ * @param {Object} opts
108
+ * @param {string} opts.appPath
109
+ * @param {string} opts.systemKey
110
+ * @param {string[]} opts.datasourceFiles
111
+ * @returns {Promise<{ uploaded: number, skipped: number }>}
112
+ */
113
+ async function syncOpenApiFilesForMcp(opts) {
114
+ const { appPath, systemKey, datasourceFiles } = opts;
115
+ const openapiDir = path.join(appPath, 'openapi');
116
+ if (!(await fileExistsAsync(openapiDir))) return { uploaded: 0, skipped: 0 };
117
+
118
+ const { dataplaneUrl, authConfig } = await resolveDataplaneAndAuth(systemKey);
119
+ const existingKeys = await getExistingOpenApiKeys(dataplaneUrl, systemKey, authConfig);
120
+ const uploadedKeys = new Set(existingKeys);
121
+ let uploaded = 0;
122
+ let skipped = 0;
123
+
124
+ for (const fileName of datasourceFiles || []) {
125
+ const documentKey = readDatasourceDocumentKey(appPath, fileName);
126
+ if (!documentKey || uploadedKeys.has(documentKey)) continue;
127
+
128
+ const localPath = documentKeyToLocalOpenApiPath(appPath, systemKey, documentKey);
129
+ if (!(await fileExistsAsync(localPath))) {
130
+ skipped += 1;
131
+ continue;
132
+ }
133
+
134
+ await uploadOneOpenApiFile(dataplaneUrl, authConfig, systemKey, localPath, documentKey);
135
+ uploadedKeys.add(documentKey);
136
+ uploaded += 1;
137
+ }
138
+
139
+ return { uploaded, skipped };
140
+ }
141
+
142
+ /**
143
+ * Repair wrapper that returns change-log lines (or []).
144
+ * @param {Object} opts
145
+ * @param {boolean} opts.enabled
146
+ * @param {boolean} opts.dryRun
147
+ * @param {string} opts.appPath
148
+ * @param {string} opts.systemKey
149
+ * @param {string[]} opts.datasourceFiles
150
+ * @returns {Promise<string[]>}
151
+ */
152
+ async function maybeSyncOpenApiFilesForMcp(opts) {
153
+ if (!opts.enabled || opts.dryRun) return [];
154
+ const r = await syncOpenApiFilesForMcp(opts);
155
+ const lines = [];
156
+ if (r.uploaded > 0) {
157
+ lines.push(`Uploaded ${r.uploaded} OpenAPI file(s) for MCP (keyed by openapi.documentKey)`);
158
+ }
159
+ if (r.skipped > 0) {
160
+ lines.push(
161
+ `Skipped ${r.skipped} OpenAPI upload(s) (missing local integration/<systemKey>/openapi/<name>.json)`
162
+ );
163
+ }
164
+ return lines;
165
+ }
166
+
167
+ module.exports = {
168
+ documentKeyToLocalOpenApiPath,
169
+ maybeSyncOpenApiFilesForMcp,
170
+ syncOpenApiFilesForMcp
171
+ };
172
+
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Persistence helpers for repair: write config files, regenerate manifest, regenerate README.
3
+ *
4
+ * Extracted from repair.js to keep file size under 500 lines.
5
+ *
6
+ * @fileoverview External integration repair helpers (persistence)
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const path = require('path');
12
+ const fs = require('fs');
13
+ const chalk = require('chalk');
14
+
15
+ const { formatSuccessLine } = require('../utils/cli-test-layout-chalk');
16
+ const { getDeployJsonPath } = require('../utils/paths');
17
+ const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
18
+ const { writeConfigFile, writeYamlPreservingComments, isYamlPath } = require('../utils/config-format');
19
+ const { backupIntegrationFile } = require('../utils/integration-file-backup');
20
+ const logger = require('../utils/logger');
21
+ const generator = require('../generator');
22
+ const { generateReadmeFromDeployJson } = require('../generator/split-readme');
23
+
24
+ /**
25
+ * README "Files" section should match integration config format on disk (YAML vs JSON).
26
+ * @param {string} appPath - Integration directory
27
+ * @returns {string} '.yaml' or '.json'
28
+ */
29
+ function inferExternalReadmeFileExt(appPath) {
30
+ try {
31
+ const configPath = resolveApplicationConfigPath(appPath);
32
+ const ext = path.extname(configPath).toLowerCase();
33
+ if (ext === '.yaml' || ext === '.yml') return '.yaml';
34
+ } catch {
35
+ /* use default */
36
+ }
37
+ return '.json';
38
+ }
39
+
40
+ async function regenerateManifest(appName, appPath, changes, backupCtx) {
41
+ try {
42
+ const deployPath = getDeployJsonPath(appName, 'external', true);
43
+ backupIntegrationFile(deployPath, backupCtx);
44
+ const outPath = await generator.generateDeployJson(appName, { appPath });
45
+ changes.push(`Regenerated ${path.basename(outPath)}`);
46
+ return true;
47
+ } catch (err) {
48
+ logger.log(chalk.yellow(`⚠ Manifest regeneration skipped: ${err.message}`));
49
+ return false;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Regenerates README.md from deployment manifest when options.doc is set.
55
+ * @param {string} appName - Application name
56
+ * @param {string} appPath - Application path
57
+ * @param {Object} options - Options (doc, dryRun)
58
+ * @param {string[]} changes - Array to append change messages to
59
+ * @returns {Promise<boolean>} True if README was regenerated
60
+ */
61
+ async function regenerateReadmeIfRequested(appName, appPath, options, changes) {
62
+ if (!options.doc) return false;
63
+ const deployJsonPath = getDeployJsonPath(appName, 'external', true);
64
+ if (!fs.existsSync(deployJsonPath) && !options.dryRun) {
65
+ await regenerateManifest(appName, appPath, changes, options.backupCtx);
66
+ }
67
+ if (!fs.existsSync(deployJsonPath)) return false;
68
+ try {
69
+ const deployment = JSON.parse(fs.readFileSync(deployJsonPath, 'utf8'));
70
+ const fileExt = inferExternalReadmeFileExt(appPath);
71
+ const readmeContent = generateReadmeFromDeployJson(deployment, { fileExt });
72
+ const readmePath = path.join(appPath, 'README.md');
73
+ if (!options.dryRun) {
74
+ backupIntegrationFile(readmePath, options.backupCtx);
75
+ fs.writeFileSync(readmePath, readmeContent, { mode: 0o644, encoding: 'utf8' });
76
+ }
77
+ changes.push('Regenerated README.md from deployment manifest');
78
+ return true;
79
+ } catch (err) {
80
+ logger.log(chalk.yellow(`⚠ Could not regenerate README: ${err.message}`));
81
+ return false;
82
+ }
83
+ }
84
+
85
+ function persistChangesAndRegenerate(opts) {
86
+ const { configPath, variables, appName, appPath, changes, originalYamlContent, backupCtx } = opts;
87
+ backupIntegrationFile(configPath, backupCtx);
88
+ if (originalYamlContent !== null && originalYamlContent !== undefined && typeof originalYamlContent === 'string' && isYamlPath(configPath)) {
89
+ writeYamlPreservingComments(configPath, originalYamlContent, variables);
90
+ } else {
91
+ writeConfigFile(configPath, variables);
92
+ }
93
+ logger.log(formatSuccessLine(`Updated ${path.basename(configPath)}`));
94
+ changes.forEach(c => logger.log(chalk.gray(` ${c}`)));
95
+ return regenerateManifest(appName, appPath, changes, backupCtx);
96
+ }
97
+
98
+ module.exports = {
99
+ persistChangesAndRegenerate,
100
+ regenerateReadmeIfRequested
101
+ };
102
+