@aifabrix/builder 2.44.5 → 2.45.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 (249) hide show
  1. package/.cursor/rules/cli-layout.mdc +8 -4
  2. package/.cursor/rules/project-rules.mdc +1 -1
  3. package/README.md +15 -23
  4. package/integration/hubspot-test/README.md +2 -0
  5. package/integration/hubspot-test/test.js +5 -3
  6. package/jest.projects.js +104 -2
  7. package/lib/api/controller-health.api.js +49 -0
  8. package/lib/api/dimension-values.api.js +82 -0
  9. package/lib/api/dimensions.api.js +114 -0
  10. package/lib/api/external-systems.api.js +1 -0
  11. package/lib/api/integration-clients.api.js +168 -0
  12. package/lib/api/types/dimension-values.types.js +28 -0
  13. package/lib/api/types/dimensions.types.js +31 -0
  14. package/lib/api/types/integration-clients.types.js +45 -0
  15. package/lib/api/validation-runner.js +46 -25
  16. package/lib/app/deploy-config.js +11 -1
  17. package/lib/app/deploy-status-display.js +3 -3
  18. package/lib/app/deploy.js +36 -14
  19. package/lib/app/display.js +15 -11
  20. package/lib/app/helpers.js +3 -3
  21. package/lib/app/index.js +3 -3
  22. package/lib/app/push.js +46 -23
  23. package/lib/app/register.js +7 -6
  24. package/lib/app/restart-display.js +126 -0
  25. package/lib/app/rotate-secret.js +7 -6
  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 +58 -19
  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 +148 -74
  32. package/lib/app/show-display.js +7 -0
  33. package/lib/app/show.js +87 -5
  34. package/lib/build/index.js +83 -49
  35. package/lib/cli/doctor-check.js +117 -0
  36. package/lib/cli/index.js +8 -2
  37. package/lib/cli/infra-guided.js +460 -0
  38. package/lib/cli/installation-log-command.js +73 -0
  39. package/lib/cli/setup-app.js +31 -3
  40. package/lib/cli/setup-auth.js +98 -27
  41. package/lib/cli/setup-dev-path-commands.js +50 -3
  42. package/lib/cli/setup-infra-up-dataplane-action.js +111 -0
  43. package/lib/cli/setup-infra-up-platform-action.js +131 -0
  44. package/lib/cli/setup-infra.js +132 -118
  45. package/lib/cli/setup-integration-client.js +182 -0
  46. package/lib/cli/setup-parameters.js +21 -2
  47. package/lib/cli/setup-platform.js +102 -0
  48. package/lib/cli/setup-secrets.js +18 -6
  49. package/lib/cli/setup-utility-resolve.js +132 -0
  50. package/lib/cli/setup-utility.js +143 -84
  51. package/lib/commands/app-logs.js +81 -33
  52. package/lib/commands/auth-config.js +116 -18
  53. package/lib/commands/datasource-capability-dimension-cli.js +128 -0
  54. package/lib/commands/datasource-capability-output.js +29 -0
  55. package/lib/commands/datasource-capability-relate-cli.js +140 -0
  56. package/lib/commands/datasource-capability.js +411 -0
  57. package/lib/commands/datasource-unified-test-cli.options.js +1 -1
  58. package/lib/commands/datasource.js +53 -13
  59. package/lib/commands/dev-down.js +3 -3
  60. package/lib/commands/dev-infra-gate.js +32 -0
  61. package/lib/commands/dev-init.js +13 -7
  62. package/lib/commands/dimension-value.js +179 -0
  63. package/lib/commands/dimension.js +330 -0
  64. package/lib/commands/integration-client.js +430 -0
  65. package/lib/commands/login-device.js +65 -30
  66. package/lib/commands/login.js +21 -10
  67. package/lib/commands/parameters-validate.js +78 -13
  68. package/lib/commands/repair-datasource-auto-rbac.js +166 -0
  69. package/lib/commands/repair-datasource-keys.js +10 -5
  70. package/lib/commands/repair-datasource.js +19 -7
  71. package/lib/commands/repair-env-template.js +4 -1
  72. package/lib/commands/repair-openapi-sync.js +172 -0
  73. package/lib/commands/repair-persist.js +102 -0
  74. package/lib/commands/repair-rbac-extract.js +27 -0
  75. package/lib/commands/repair-rbac-migrate.js +186 -0
  76. package/lib/commands/repair-rbac.js +214 -31
  77. package/lib/commands/repair-system-alignment.js +246 -0
  78. package/lib/commands/repair-system-permissions.js +168 -0
  79. package/lib/commands/repair.js +120 -338
  80. package/lib/commands/secure.js +1 -1
  81. package/lib/commands/setup-modes.js +468 -0
  82. package/lib/commands/setup-prompts.js +421 -0
  83. package/lib/commands/setup.js +254 -0
  84. package/lib/commands/teardown.js +277 -0
  85. package/lib/commands/up-common.js +113 -19
  86. package/lib/commands/up-dataplane.js +44 -19
  87. package/lib/commands/up-miso.js +18 -18
  88. package/lib/commands/upload.js +111 -23
  89. package/lib/commands/wizard-core-helpers.js +14 -11
  90. package/lib/commands/wizard-core.js +6 -5
  91. package/lib/commands/wizard-dataplane.js +2 -2
  92. package/lib/commands/wizard-entity-selection.js +4 -3
  93. package/lib/commands/wizard-headless.js +2 -1
  94. package/lib/commands/wizard.js +2 -1
  95. package/lib/constants/infra-compose-service-names.js +40 -0
  96. package/lib/core/audit-logger.js +1 -34
  97. package/lib/core/config-admin-email.js +56 -0
  98. package/lib/core/config-normalize.js +60 -0
  99. package/lib/core/config-registered-controller-urls.js +54 -0
  100. package/lib/core/config.js +33 -50
  101. package/lib/core/env-reader.js +16 -3
  102. package/lib/core/secrets-admin-env.js +101 -0
  103. package/lib/core/secrets-ensure-infra.js +34 -1
  104. package/lib/core/secrets-ensure.js +88 -66
  105. package/lib/core/secrets-env-content.js +428 -0
  106. package/lib/core/secrets-env-declarative-expand.js +170 -0
  107. package/lib/core/secrets-env-write.js +29 -1
  108. package/lib/core/secrets-load.js +252 -0
  109. package/lib/core/secrets-names.js +32 -0
  110. package/lib/core/secrets.js +17 -757
  111. package/lib/datasource/capability/basic-exposure.js +76 -0
  112. package/lib/datasource/capability/capability-diff-slice.js +41 -0
  113. package/lib/datasource/capability/capability-key.js +34 -0
  114. package/lib/datasource/capability/capability-resolve.js +172 -0
  115. package/lib/datasource/capability/capability-storage-keys.js +22 -0
  116. package/lib/datasource/capability/copy-operations.js +348 -0
  117. package/lib/datasource/capability/copy-test-payload.js +139 -0
  118. package/lib/datasource/capability/create-operations.js +235 -0
  119. package/lib/datasource/capability/dimension-operations.js +151 -0
  120. package/lib/datasource/capability/dimension-validate.js +219 -0
  121. package/lib/datasource/capability/json-pointer.js +31 -0
  122. package/lib/datasource/capability/reference-rewrite.js +51 -0
  123. package/lib/datasource/capability/relate-operations.js +325 -0
  124. package/lib/datasource/capability/relate-validate.js +219 -0
  125. package/lib/datasource/capability/remove-operations.js +275 -0
  126. package/lib/datasource/capability/run-capability-copy.js +152 -0
  127. package/lib/datasource/capability/run-capability-diff.js +135 -0
  128. package/lib/datasource/capability/run-capability-dimension.js +291 -0
  129. package/lib/datasource/capability/run-capability-edit.js +377 -0
  130. package/lib/datasource/capability/run-capability-relate.js +193 -0
  131. package/lib/datasource/capability/run-capability-remove.js +105 -0
  132. package/lib/datasource/capability/templates/minimal-fetch.json +18 -0
  133. package/lib/datasource/capability/validate-capability-slice.js +35 -0
  134. package/lib/datasource/list.js +136 -23
  135. package/lib/datasource/log-viewer.js +2 -4
  136. package/lib/datasource/unified-validation-run.js +51 -16
  137. package/lib/datasource/validate.js +53 -1
  138. package/lib/deployment/deploy-poll-ui.js +60 -0
  139. package/lib/deployment/deployer-status.js +29 -3
  140. package/lib/deployment/deployer.js +48 -30
  141. package/lib/deployment/environment.js +7 -2
  142. package/lib/deployment/poll-interval.js +72 -0
  143. package/lib/deployment/push.js +11 -9
  144. package/lib/external-system/deploy.js +9 -2
  145. package/lib/external-system/download.js +61 -32
  146. package/lib/external-system/sync-deploy-manifest.js +33 -0
  147. package/lib/infrastructure/index.js +49 -19
  148. package/lib/infrastructure/orphan-infra-docker-teardown.js +177 -0
  149. package/lib/internal/node-fs.js +2 -0
  150. package/lib/parameters/infra-kv-discovery.js +29 -4
  151. package/lib/parameters/infra-parameter-catalog.js +6 -3
  152. package/lib/parameters/infra-parameter-validate.js +67 -19
  153. package/lib/resolvers/datasource-resolver.js +53 -0
  154. package/lib/resolvers/dimension-file.js +52 -0
  155. package/lib/resolvers/manifest-resolver.js +133 -0
  156. package/lib/schema/application-schema.json +4 -0
  157. package/lib/schema/external-datasource.schema.json +183 -53
  158. package/lib/schema/external-system.schema.json +23 -10
  159. package/lib/schema/infra.parameter.yaml +26 -1
  160. package/lib/schema/wizard-config.schema.json +1 -1
  161. package/lib/utils/aifabrix-config-dir-walk.js +40 -0
  162. package/lib/utils/aifabrix-runtime-config-dir.js +26 -3
  163. package/lib/utils/app-config-resolver.js +24 -1
  164. package/lib/utils/app-run-containers.js +2 -2
  165. package/lib/utils/applications-config-defaults.js +206 -0
  166. package/lib/utils/auth-config-validator.js +2 -12
  167. package/lib/utils/bash-secret-env.js +59 -0
  168. package/lib/utils/cli-secrets-error-format.js +78 -0
  169. package/lib/utils/cli-test-layout-chalk.js +31 -9
  170. package/lib/utils/cli-utils.js +4 -36
  171. package/lib/utils/compose-generate-docker-compose.js +111 -6
  172. package/lib/utils/compose-generator.js +17 -8
  173. package/lib/utils/controller-url.js +50 -7
  174. package/lib/utils/datasource-test-run-display.js +8 -0
  175. package/lib/utils/dev-hosts-helper.js +3 -2
  176. package/lib/utils/dev-init-ssh-merge.js +2 -1
  177. package/lib/utils/docker-build.js +17 -9
  178. package/lib/utils/docker-reload-mount.js +127 -0
  179. package/lib/utils/env-copy.js +99 -14
  180. package/lib/utils/env-template.js +5 -1
  181. package/lib/utils/external-readme.js +71 -2
  182. package/lib/utils/external-system-local-test-tty.js +3 -2
  183. package/lib/utils/external-system-readiness-core.js +45 -12
  184. package/lib/utils/external-system-readiness-deploy-display.js +3 -3
  185. package/lib/utils/external-system-readiness-display-internals.js +33 -3
  186. package/lib/utils/external-system-readiness-display.js +10 -1
  187. package/lib/utils/file-upload.js +40 -3
  188. package/lib/utils/health-check-db-init.js +107 -0
  189. package/lib/utils/health-check-public-warn.js +69 -0
  190. package/lib/utils/health-check-url.js +28 -10
  191. package/lib/utils/health-check.js +139 -107
  192. package/lib/utils/help-builder.js +5 -1
  193. package/lib/utils/image-name.js +34 -7
  194. package/lib/utils/infra-optional-service-flags.js +69 -0
  195. package/lib/utils/installation-log-core.js +282 -0
  196. package/lib/utils/installation-log-record.js +237 -0
  197. package/lib/utils/installation-log.js +123 -0
  198. package/lib/utils/integration-file-backup.js +74 -0
  199. package/lib/utils/log-redaction.js +105 -0
  200. package/lib/utils/manifest-location.js +164 -0
  201. package/lib/utils/manifest-source-emit.js +162 -0
  202. package/lib/utils/mutagen-install.js +30 -3
  203. package/lib/utils/paths.js +308 -76
  204. package/lib/utils/postgres-wipe.js +212 -0
  205. package/lib/utils/register-aifabrix-shell-env.js +15 -0
  206. package/lib/utils/remote-dev-auth.js +21 -5
  207. package/lib/utils/remote-docker-env.js +9 -1
  208. package/lib/utils/remote-secrets-loader.js +49 -4
  209. package/lib/utils/resolve-docker-image-ref.js +9 -3
  210. package/lib/utils/run-cli-flags.js +29 -0
  211. package/lib/utils/secrets-ancestor-paths.js +47 -0
  212. package/lib/utils/secrets-canonical.js +10 -3
  213. package/lib/utils/secrets-helpers.js +17 -10
  214. package/lib/utils/secrets-kv-refs.js +42 -0
  215. package/lib/utils/secrets-kv-scope.js +19 -2
  216. package/lib/utils/secrets-materialize-local.js +134 -0
  217. package/lib/utils/secrets-path.js +26 -13
  218. package/lib/utils/secrets-utils.js +20 -10
  219. package/lib/utils/system-builder-root.js +42 -0
  220. package/lib/utils/url-declarative-public-base.js +80 -12
  221. package/lib/utils/url-declarative-resolve-build-urls.js +238 -0
  222. package/lib/utils/url-declarative-resolve-build.js +24 -388
  223. package/lib/utils/url-declarative-resolve-expand-token.js +189 -0
  224. package/lib/utils/url-declarative-resolve-load-doc.js +12 -3
  225. package/lib/utils/url-declarative-resolve-surface-state.js +102 -0
  226. package/lib/utils/url-declarative-resolve.js +47 -7
  227. package/lib/utils/url-declarative-runtime-base-path.js +52 -0
  228. package/lib/utils/url-declarative-vdir-inactive-env.js +2 -1
  229. package/lib/utils/urls-local-registry-scan.js +103 -0
  230. package/lib/utils/urls-local-registry.js +158 -76
  231. package/lib/utils/validation-poll-ui.js +81 -0
  232. package/lib/utils/validation-run-poll.js +29 -5
  233. package/lib/utils/with-muted-logger.js +53 -0
  234. package/package.json +3 -1
  235. package/templates/applications/dataplane/application.yaml +5 -1
  236. package/templates/applications/dataplane/rbac.yaml +10 -10
  237. package/templates/applications/keycloak/env.template +8 -6
  238. package/templates/applications/miso-controller/application.yaml +9 -0
  239. package/templates/applications/miso-controller/env.template +27 -29
  240. package/templates/applications/miso-controller/rbac.yaml +9 -9
  241. package/templates/external-system/README.md.hbs +83 -123
  242. package/.npmrc.token +0 -1
  243. package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  244. package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  245. package/.nyc_output/processinfo/index.json +0 -1
  246. package/lib/api/service-users.api.js +0 -150
  247. package/lib/api/types/service-users.types.js +0 -65
  248. package/lib/cli/setup-service-user.js +0 -187
  249. package/lib/commands/service-user.js +0 -429
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Shared RBAC extraction from external system JSON (repair / merge).
3
+ *
4
+ * @fileoverview extractRbacFromSystem for repair-rbac and migrate
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ /**
12
+ * Extracts roles and permissions from external system JSON for rbac.yaml (same rules as repair).
13
+ * @param {Object} system - Parsed system config
14
+ * @returns {Object|null} RBAC object or null
15
+ */
16
+ function extractRbacFromSystem(system) {
17
+ if (!system || typeof system !== 'object') return null;
18
+ const hasRoles = system.roles && Array.isArray(system.roles) && system.roles.length > 0;
19
+ const hasPermissions = system.permissions && Array.isArray(system.permissions) && system.permissions.length > 0;
20
+ if (!hasRoles && !hasPermissions) return null;
21
+ const rbac = {};
22
+ if (hasRoles) rbac.roles = system.roles;
23
+ if (hasPermissions) rbac.permissions = system.permissions;
24
+ return rbac;
25
+ }
26
+
27
+ module.exports = { extractRbacFromSystem };
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Moves roles/permissions from *-system.{json,yaml} into rbac.{yaml,json} when both exist.
3
+ *
4
+ * @fileoverview migrateSystemRbacIntoRbacFile
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const appConfigResolver = require('../utils/app-config-resolver');
12
+ const { loadConfigFile, writeConfigFile } = require('../utils/config-format');
13
+ const { backupIntegrationFile } = require('../utils/integration-file-backup');
14
+ const extractModule = require('./repair-rbac-extract');
15
+
16
+ function _safeString(v) {
17
+ return typeof v === 'string' && v.trim() ? v.trim() : '';
18
+ }
19
+
20
+ /**
21
+ * Merges roles from source into target; dedupes by role.value.
22
+ * @param {Object[]} targetRoles - rbac.roles (mutated)
23
+ * @param {Object[]} sourceRoles - roles to merge in
24
+ * @returns {boolean} True if targetRoles changed
25
+ */
26
+ function _mergeRoleObjectsInto(targetRoles, sourceRoles) {
27
+ if (!Array.isArray(sourceRoles) || sourceRoles.length === 0) return false;
28
+ const seen = new Set(
29
+ (targetRoles || []).map(r => _safeString(r?.value)).filter(Boolean)
30
+ );
31
+ let updated = false;
32
+ for (const r of sourceRoles) {
33
+ if (!r || typeof r !== 'object') continue;
34
+ const value = _safeString(r.value);
35
+ if (!value || seen.has(value)) continue;
36
+ targetRoles.push({
37
+ name: r.name,
38
+ value: r.value,
39
+ description: r.description,
40
+ groups: Array.isArray(r.groups) ? [...r.groups] : []
41
+ });
42
+ seen.add(value);
43
+ updated = true;
44
+ }
45
+ return updated;
46
+ }
47
+
48
+ /**
49
+ * @param {unknown[]} existingRoles - roles already on permission
50
+ * @param {unknown[]} incomingRoles - roles from merged permission
51
+ * @returns {string[]|null} New roles array if union added roles
52
+ */
53
+ function _unionPermissionRoleSets(existingRoles, incomingRoles) {
54
+ const a = new Set(Array.isArray(existingRoles) ? existingRoles : []);
55
+ let unionChanged = false;
56
+ for (const rv of incomingRoles || []) {
57
+ if (typeof rv === 'string' && rv.trim() && !a.has(rv)) {
58
+ a.add(rv);
59
+ unionChanged = true;
60
+ }
61
+ }
62
+ return unionChanged ? [...a] : null;
63
+ }
64
+
65
+ /**
66
+ * Merges permissions from source into target; dedupes by name; unions roles arrays.
67
+ * @param {Object[]} targetPerms - rbac.permissions (mutated)
68
+ * @param {Object[]} sourcePerms - permissions to merge in
69
+ * @returns {boolean} True if targetPerms changed
70
+ */
71
+ function _mergePermissionObjectsInto(targetPerms, sourcePerms) {
72
+ if (!Array.isArray(sourcePerms) || sourcePerms.length === 0) return false;
73
+ const byName = new Map(
74
+ (targetPerms || []).filter(p => p && typeof p === 'object').map(p => [p.name, p])
75
+ );
76
+ let updated = false;
77
+ for (const p of sourcePerms) {
78
+ if (!p || typeof p !== 'object') continue;
79
+ const name = _safeString(p.name);
80
+ if (!name) continue;
81
+ const existing = byName.get(name);
82
+ if (!existing) {
83
+ targetPerms.push({
84
+ name: p.name,
85
+ description: _safeString(p.description) || `Permission: ${name}`,
86
+ roles: Array.isArray(p.roles) ? [...p.roles] : []
87
+ });
88
+ byName.set(name, targetPerms[targetPerms.length - 1]);
89
+ updated = true;
90
+ continue;
91
+ }
92
+ const mergedRoles = _unionPermissionRoleSets(existing.roles, Array.isArray(p.roles) ? p.roles : []);
93
+ if (mergedRoles) {
94
+ existing.roles = mergedRoles;
95
+ updated = true;
96
+ }
97
+ }
98
+ return updated;
99
+ }
100
+
101
+ /**
102
+ * @param {string[]} changes - Repair changelog lines
103
+ * @param {boolean} rbacContentChanged - Whether rbac JSON/YAML payload changed
104
+ */
105
+ function _appendMigrateSystemRbacNotes(changes, rbacContentChanged) {
106
+ if (rbacContentChanged) {
107
+ changes.push('Merged roles/permissions from external system file into rbac file');
108
+ }
109
+ changes.push('Removed roles and permissions from external system file (canonical copy in rbac file)');
110
+ }
111
+
112
+ /**
113
+ * @param {string} rbacPath
114
+ * @param {Object} rbac
115
+ * @param {boolean} rbacContentChanged
116
+ * @param {string} systemFilePath
117
+ * @param {Object} systemParsed
118
+ * @param {Object} [backupCtx]
119
+ * @returns {void}
120
+ */
121
+ function _persistMigrateSystemRbacEdits(rbacPath, rbac, rbacContentChanged, systemFilePath, systemParsed, backupCtx) {
122
+ if (rbacContentChanged) {
123
+ backupIntegrationFile(rbacPath, backupCtx);
124
+ writeConfigFile(rbacPath, rbac);
125
+ }
126
+ delete systemParsed.roles;
127
+ delete systemParsed.permissions;
128
+ backupIntegrationFile(systemFilePath, backupCtx);
129
+ writeConfigFile(systemFilePath, systemParsed);
130
+ }
131
+
132
+ /**
133
+ * When an rbac file already exists, moves roles/permissions out of the external system JSON into that file.
134
+ * (Initial create-from-system is handled by createRbacFromSystemIfNeeded in repair.js.)
135
+ *
136
+ * @param {string} appPath - Integration directory
137
+ * @param {string} systemFilePath - Path to *-system.json (or yaml)
138
+ * @param {Object} systemParsed - Parsed system (mutated: roles/permissions removed when not dryRun)
139
+ * @param {{ dryRun: boolean, changes: string[], backupCtx?: Object }} options
140
+ * @returns {boolean} True if system had RBAC fields to relocate or rbac was merged
141
+ */
142
+ function migrateSystemRbacIntoRbacFile(appPath, systemFilePath, systemParsed, options) {
143
+ const { dryRun, changes, backupCtx } = options;
144
+ const extracted = extractModule.extractRbacFromSystem(systemParsed);
145
+ if (!extracted) return false;
146
+
147
+ const rbacPath = appConfigResolver.resolveRbacPath(appPath);
148
+ if (!rbacPath) {
149
+ return false;
150
+ }
151
+
152
+ const loaded = loadConfigFile(rbacPath);
153
+ /** Clone so merges never mutate a shared mock / cached object returned by tests or loaders. */
154
+ const rbac = {
155
+ roles: Array.isArray(loaded.roles)
156
+ ? loaded.roles.map(r => ({
157
+ name: r.name,
158
+ value: r.value,
159
+ description: r.description,
160
+ groups: Array.isArray(r.groups) ? [...r.groups] : []
161
+ }))
162
+ : [],
163
+ permissions: Array.isArray(loaded.permissions)
164
+ ? loaded.permissions.map(p => ({
165
+ name: p.name,
166
+ description: p.description,
167
+ roles: Array.isArray(p.roles) ? [...p.roles] : []
168
+ }))
169
+ : []
170
+ };
171
+
172
+ const rolesMerged = _mergeRoleObjectsInto(rbac.roles, extracted.roles);
173
+ const permsMerged = _mergePermissionObjectsInto(rbac.permissions, extracted.permissions);
174
+ const rbacContentChanged = rolesMerged || permsMerged;
175
+
176
+ _appendMigrateSystemRbacNotes(changes, rbacContentChanged);
177
+
178
+ if (!dryRun) {
179
+ _persistMigrateSystemRbacEdits(rbacPath, rbac, rbacContentChanged, systemFilePath, systemParsed, backupCtx);
180
+ }
181
+ return true;
182
+ }
183
+
184
+ module.exports = {
185
+ migrateSystemRbacIntoRbacFile
186
+ };
@@ -13,24 +13,22 @@ const fs = require('fs');
13
13
  const chalk = require('chalk');
14
14
  const logger = require('../utils/logger');
15
15
  const { loadConfigFile, writeConfigFile } = require('../utils/config-format');
16
- const { resolveRbacPath } = require('../utils/app-config-resolver');
16
+ const { backupIntegrationFile } = require('../utils/integration-file-backup');
17
+ const appConfigResolver = require('../utils/app-config-resolver');
18
+ const { extractRbacFromSystem } = require('./repair-rbac-extract');
17
19
 
18
20
  const DEFAULT_CAPABILITIES = ['list', 'get', 'create', 'update', 'delete'];
19
21
 
20
- /**
21
- * Extracts roles and permissions from external system JSON for rbac.yaml (same rules as repair).
22
- * @param {Object} system - Parsed system config
23
- * @returns {Object|null} RBAC object or null
24
- */
25
- function extractRbacFromSystem(system) {
26
- if (!system || typeof system !== 'object') return null;
27
- const hasRoles = system.roles && Array.isArray(system.roles) && system.roles.length > 0;
28
- const hasPermissions = system.permissions && Array.isArray(system.permissions) && system.permissions.length > 0;
29
- if (!hasRoles && !hasPermissions) return null;
30
- const rbac = {};
31
- if (hasRoles) rbac.roles = system.roles;
32
- if (hasPermissions) rbac.permissions = system.permissions;
33
- return rbac;
22
+ function _safeString(v) {
23
+ return typeof v === 'string' && v.trim() ? v.trim() : '';
24
+ }
25
+
26
+ function _normalizeOperationKey(opKey) {
27
+ const s = _safeString(opKey);
28
+ if (!s) return '';
29
+ const withHyphens = s.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
30
+ const cleaned = withHyphens.replace(/[^a-z0-9-]+/g, '-');
31
+ return cleaned.replace(/-+/g, '-').replace(/^-+|-+$/g, '');
34
32
  }
35
33
 
36
34
  /**
@@ -62,20 +60,122 @@ function getCapabilitiesFromDatasource(parsed) {
62
60
  function collectPermissionNames(appPath, datasourceFiles) {
63
61
  const permissionNames = new Set();
64
62
  for (const fileName of datasourceFiles || []) {
65
- const filePath = path.join(appPath, fileName);
66
- if (!fs.existsSync(filePath)) continue;
67
- try {
68
- const parsed = loadConfigFile(filePath);
69
- const resourceType = parsed?.resourceType || 'document';
70
- const caps = getCapabilitiesFromDatasource(parsed);
71
- for (const cap of caps) permissionNames.add(`${resourceType}:${cap}`);
72
- } catch (err) {
73
- logger.log(chalk.yellow(`⚠ Could not load ${fileName} for RBAC: ${err.message}`));
74
- }
63
+ const parsed = _safeLoadDatasource(appPath, fileName);
64
+ if (!parsed) continue;
65
+ _collectPermissionNamesFromDatasourceParsed(permissionNames, parsed);
75
66
  }
76
67
  return permissionNames;
77
68
  }
78
69
 
70
+ function _safeLoadDatasource(appPath, fileName) {
71
+ const filePath = path.join(appPath, fileName);
72
+ if (!fs.existsSync(filePath)) return null;
73
+ try {
74
+ return loadConfigFile(filePath);
75
+ } catch (err) {
76
+ logger.log(chalk.yellow(`⚠ Could not load ${fileName} for RBAC: ${err.message}`));
77
+ return null;
78
+ }
79
+ }
80
+
81
+ function _hasAutoRbacOps(parsed) {
82
+ const openapi = parsed?.openapi;
83
+ return (
84
+ openapi &&
85
+ typeof openapi === 'object' &&
86
+ openapi.autoRbac === true &&
87
+ openapi.operations &&
88
+ typeof openapi.operations === 'object' &&
89
+ !Array.isArray(openapi.operations)
90
+ );
91
+ }
92
+
93
+ function _collectPermissionNamesFromDatasourceParsed(permissionNames, parsed) {
94
+ const resourceType = parsed?.resourceType || 'document';
95
+
96
+ if (_hasAutoRbacOps(parsed)) {
97
+ const ops = parsed.openapi.operations;
98
+ for (const opKey of Object.keys(ops)) {
99
+ const normOpKey = _normalizeOperationKey(opKey) || _safeString(opKey);
100
+ if (!normOpKey) continue;
101
+ permissionNames.add(`${resourceType}:${normOpKey}`);
102
+ }
103
+ }
104
+ // When autoRbac is false/missing, RBAC is considered manual; do not auto-generate permissions.
105
+ }
106
+
107
+ function _collectManagedResourceTypesFromDatasources(appPath, datasourceFiles) {
108
+ const out = new Set();
109
+ for (const fileName of datasourceFiles || []) {
110
+ const parsed = _safeLoadDatasource(appPath, fileName);
111
+ if (!parsed) continue;
112
+ if (!_hasAutoRbacOps(parsed)) continue;
113
+ const rt = _safeString(parsed.resourceType) || 'document';
114
+ out.add(rt);
115
+ }
116
+ return out;
117
+ }
118
+
119
+ function _buildDesiredPermissionNames(appPath, datasourceFiles) {
120
+ const desired = collectPermissionNames(appPath, datasourceFiles);
121
+ if (desired.size === 0) return { desired, managedResourceTypes: new Set(), aliases: new Map() };
122
+
123
+ const managedResourceTypes = _collectManagedResourceTypesFromDatasources(appPath, datasourceFiles);
124
+
125
+ // Build alias map for rename support (kebab-case or case variants -> canonical).
126
+ const aliases = new Map();
127
+ for (const name of desired) {
128
+ const [rt, op] = String(name).split(':');
129
+ if (!rt || !op) continue;
130
+ const kebab = _normalizeOperationKey(op);
131
+ if (kebab && `${rt}:${kebab}` !== name) {
132
+ aliases.set(`${rt}:${kebab}`, name);
133
+ }
134
+ const lower = `${rt}:${String(op).toLowerCase()}`;
135
+ if (lower !== name) {
136
+ aliases.set(lower, name);
137
+ }
138
+ const dehyphen = `${rt}:${String(op).replace(/-/g, '').toLowerCase()}`;
139
+ if (dehyphen !== name) {
140
+ aliases.set(dehyphen, name);
141
+ }
142
+ }
143
+ return { desired, managedResourceTypes, aliases };
144
+ }
145
+
146
+ function _renameExistingPermissionIfAliasMatches(rbac, aliases, changes) {
147
+ if (!rbac || !Array.isArray(rbac.permissions) || aliases.size === 0) return false;
148
+ let updated = false;
149
+ for (const p of rbac.permissions) {
150
+ if (!p || typeof p !== 'object') continue;
151
+ const name = _safeString(p.name);
152
+ if (!name) continue;
153
+ const canonical = aliases.get(name);
154
+ if (!canonical || canonical === name) continue;
155
+ p.name = canonical;
156
+ changes.push(`Renamed RBAC permission: ${name} → ${canonical}`);
157
+ updated = true;
158
+ }
159
+ return updated;
160
+ }
161
+
162
+ function _removeExtraAutoRbacPermissions(rbac, desired, managedResourceTypes, changes) {
163
+ if (!rbac || !Array.isArray(rbac.permissions) || desired.size === 0) return false;
164
+ if (!managedResourceTypes || managedResourceTypes.size === 0) return false;
165
+ const before = rbac.permissions.length;
166
+ rbac.permissions = rbac.permissions.filter((p) => {
167
+ const name = _safeString(p?.name);
168
+ if (!name || !name.includes(':')) return true;
169
+ const [rt] = name.split(':');
170
+ if (!managedResourceTypes.has(rt)) return true;
171
+ return desired.has(name);
172
+ });
173
+ const after = rbac.permissions.length;
174
+ if (after === before) return false;
175
+ changes.push(`Removed ${before - after} extra autoRbac permission(s) not present in operations`);
176
+ return true;
177
+ }
178
+
79
179
  /**
80
180
  * Loads existing RBAC file (rbac.yaml, rbac.yml, or rbac.json) or creates empty structure.
81
181
  * Uses extractRbacFromSystem when no file exists. New file path respects format (rbac.json when format is 'json').
@@ -87,7 +187,7 @@ function collectPermissionNames(appPath, datasourceFiles) {
87
187
  * @returns {{ rbac: Object, rbacPath: string }} rbacPath is resolved path or path.join(appPath, 'rbac.{json|yaml}') for new file
88
188
  */
89
189
  function loadOrCreateRbac(appPath, systemParsed, extractRbacFromSystem, format) {
90
- const resolvedPath = resolveRbacPath(appPath);
190
+ const resolvedPath = appConfigResolver.resolveRbacPath(appPath);
91
191
  let rbac;
92
192
  let rbacPath;
93
193
  if (resolvedPath) {
@@ -153,6 +253,82 @@ function ensureDefaultRoles(rbac, systemKey, displayName, changes) {
153
253
  return true;
154
254
  }
155
255
 
256
+ function _roleValues(rbac) {
257
+ const roles = Array.isArray(rbac.roles) ? rbac.roles : [];
258
+ return roles
259
+ .map(r => r?.value)
260
+ .filter(v => typeof v === 'string' && v.trim());
261
+ }
262
+
263
+ function _pickAdminRoleValue(roleValues, systemKey) {
264
+ return (
265
+ roleValues.find(v => v === `${systemKey}-admin`) ||
266
+ roleValues.find(v => /-admin$/.test(v)) ||
267
+ roleValues[0] ||
268
+ null
269
+ );
270
+ }
271
+
272
+ function _pickReaderRoleValue(roleValues, systemKey) {
273
+ return (
274
+ roleValues.find(v => v === `${systemKey}-reader`) ||
275
+ roleValues.find(v => /-reader$/.test(v)) ||
276
+ null
277
+ );
278
+ }
279
+
280
+ function _capabilityFromPermissionName(name) {
281
+ if (typeof name !== 'string' || !name.includes(':')) return null;
282
+ return name.split(':')[1] || null;
283
+ }
284
+
285
+ function _defaultRolesForCapability(cap, { adminValue, readerValue }) {
286
+ const roles = [];
287
+ if ((cap === 'list' || cap === 'get') && readerValue) roles.push(readerValue);
288
+ if (adminValue) roles.push(adminValue);
289
+ return roles;
290
+ }
291
+
292
+ /**
293
+ * Ensures every permission has at least one role assigned.
294
+ *
295
+ * When roles already exist, new permissions may be added with empty roles (invalid for manifest).
296
+ * Apply a safe default:
297
+ * - list/get -> reader + admin when those roles exist
298
+ * - create/update/delete -> admin when it exists (otherwise first available role)
299
+ *
300
+ * Mutates rbac.
301
+ * @param {Object} rbac - RBAC object (mutated)
302
+ * @param {string} systemKey - System key
303
+ * @param {string[]} changes - Array to append to
304
+ * @returns {boolean}
305
+ */
306
+ function ensureNonEmptyPermissionRoles(rbac, systemKey, changes) {
307
+ const roleValues = _roleValues(rbac);
308
+ if (!Array.isArray(rbac.permissions) || roleValues.length === 0) return false;
309
+
310
+ const adminValue = _pickAdminRoleValue(roleValues, systemKey);
311
+ const readerValue = _pickReaderRoleValue(roleValues, systemKey);
312
+
313
+ let updated = false;
314
+ for (const p of rbac.permissions) {
315
+ if (!p || typeof p !== 'object') continue;
316
+ if (!Array.isArray(p.roles)) p.roles = [];
317
+ if (p.roles.length > 0) continue;
318
+
319
+ const cap = _capabilityFromPermissionName(p.name);
320
+ const defaults = _defaultRolesForCapability(cap, { adminValue, readerValue });
321
+ for (const rv of defaults) {
322
+ if (!p.roles.includes(rv)) p.roles.push(rv);
323
+ }
324
+ if (p.roles.length > 0) {
325
+ changes.push(`RBAC: defaulted empty roles for ${p.name}`);
326
+ updated = true;
327
+ }
328
+ }
329
+ return updated;
330
+ }
331
+
156
332
  /**
157
333
  * Merges RBAC from datasources: ensures permission per resourceType:capability, adds Admin/Reader roles if none.
158
334
  * When creating a new RBAC file, uses rbac.json if format is 'json', otherwise rbac.yaml.
@@ -161,27 +337,34 @@ function ensureDefaultRoles(rbac, systemKey, displayName, changes) {
161
337
  * @param {Object} systemParsed - Parsed system (key, displayName)
162
338
  * @param {string[]} datasourceFiles - Datasource file names
163
339
  * @param {Function} extractRbacFromSystem - (system) => rbac or null
164
- * @param {{ format?: string, dryRun: boolean, changes: string[] }} options - format ('json'|'yaml'), dryRun, changes array
340
+ * @param {{ format?: string, dryRun: boolean, changes: string[], backupCtx?: Object }} options - format ('json'|'yaml'), dryRun, changes array
165
341
  * @returns {boolean} True if rbac was updated (or would be in dry-run)
166
342
  */
167
343
  function mergeRbacFromDatasources(appPath, systemParsed, datasourceFiles, extractRbacFromSystem, options) {
168
- const { format = 'yaml', dryRun, changes } = options;
344
+ const { format = 'yaml', dryRun, changes, backupCtx } = options;
169
345
  const rbacFormat = format === 'json' ? 'json' : 'yaml';
170
- const permissionNames = collectPermissionNames(appPath, datasourceFiles);
346
+ const { desired: permissionNames, managedResourceTypes, aliases } = _buildDesiredPermissionNames(appPath, datasourceFiles);
171
347
  if (permissionNames.size === 0) return false;
172
348
  const systemKey = systemParsed?.key || 'system';
173
349
  const displayName = systemParsed?.displayName || systemKey;
174
350
  const { rbac, rbacPath } = loadOrCreateRbac(appPath, systemParsed, extractRbacFromSystem, rbacFormat);
175
- let updated = addMissingPermissions(rbac, permissionNames, changes);
351
+ let updated = _renameExistingPermissionIfAliasMatches(rbac, aliases, changes);
352
+ updated = addMissingPermissions(rbac, permissionNames, changes) || updated;
353
+ updated = _removeExtraAutoRbacPermissions(rbac, permissionNames, managedResourceTypes, changes) || updated;
176
354
  updated = ensureDefaultRoles(rbac, systemKey, displayName, changes) || updated;
355
+ updated = ensureNonEmptyPermissionRoles(rbac, systemKey, changes) || updated;
177
356
  if (updated && !dryRun) {
357
+ backupIntegrationFile(rbacPath, backupCtx);
178
358
  writeConfigFile(rbacPath, rbac);
179
359
  }
180
360
  return updated;
181
361
  }
182
362
 
363
+ const { migrateSystemRbacIntoRbacFile } = require('./repair-rbac-migrate');
364
+
183
365
  module.exports = {
184
366
  extractRbacFromSystem,
185
367
  getCapabilitiesFromDatasource,
186
- mergeRbacFromDatasources
368
+ mergeRbacFromDatasources,
369
+ migrateSystemRbacIntoRbacFile
187
370
  };