@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,212 @@
1
+ /**
2
+ * Postgres data wipe helper for `aifabrix setup` Mode 2 (Wipe data).
3
+ *
4
+ * Drops every non-template database and every non-superuser role in the
5
+ * developer's running Postgres container, while preserving the volume,
6
+ * the `postgres` superuser, and the admin password (so the post-wipe
7
+ * `up-infra` and platform bootstrap recreate schemas + service users).
8
+ *
9
+ * Uses `docker exec` with the admin password passed via the container's
10
+ * environment (PGPASSWORD) so it is not visible in `ps`.
11
+ *
12
+ * @fileoverview Drop all DBs and roles in dev Postgres for setup Mode 2
13
+ * @author AI Fabrix Team
14
+ * @version 2.0.0
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const config = require('../core/config');
20
+ const adminSecrets = require('../core/admin-secrets');
21
+ const dockerExec = require('./docker-exec');
22
+ const logger = require('./logger');
23
+ const chalk = require('chalk');
24
+ const { successGlyph } = require('./cli-test-layout-chalk');
25
+
26
+ /** Roles that must never be dropped (Postgres-managed predefined roles). */
27
+ const PROTECTED_ROLES = new Set([
28
+ // In this project, infra starts Postgres with POSTGRES_USER=pgadmin (see templates/infra/compose.yaml.hbs),
29
+ // so that role is the superuser we must preserve.
30
+ 'pgadmin',
31
+ // Legacy / default superuser name for official Postgres images.
32
+ 'postgres',
33
+ 'pg_signal_backend',
34
+ 'pg_read_server_files',
35
+ 'pg_write_server_files',
36
+ 'pg_execute_server_program',
37
+ 'pg_monitor',
38
+ 'pg_read_all_settings',
39
+ 'pg_read_all_stats',
40
+ 'pg_stat_scan_tables',
41
+ 'pg_database_owner',
42
+ 'pg_read_all_data',
43
+ 'pg_write_all_data',
44
+ 'pg_checkpoint',
45
+ 'pg_use_reserved_connections',
46
+ 'pg_create_subscription'
47
+ ]);
48
+
49
+ /** Databases that must never be dropped. */
50
+ const PROTECTED_DATABASES = new Set(['postgres', 'template0', 'template1']);
51
+
52
+ /** Superuser role used inside the infra Postgres container. */
53
+ const SUPERUSER_ROLE = 'pgadmin';
54
+
55
+ /**
56
+ * Compute the developer-scoped Postgres container name.
57
+ * Mirrors `getInfraContainerNames` in `lib/utils/infra-status.js`.
58
+ * @param {number|string} devId - Developer ID
59
+ * @returns {string}
60
+ */
61
+ function getPostgresContainerName(devId) {
62
+ const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
63
+ if (idNum === 0) return 'aifabrix-postgres';
64
+ return `aifabrix-dev${devId}-postgres`;
65
+ }
66
+
67
+ /**
68
+ * Run `psql -t -A -c <sql>` inside the Postgres container with PGPASSWORD set
69
+ * via `-e PGPASSWORD=...` so the secret never appears on the command line.
70
+ *
71
+ * @async
72
+ * @param {string} container - Container name
73
+ * @param {string} sql - Single SQL statement (no shell metacharacters)
74
+ * @param {string} adminPassword - Postgres superuser password
75
+ * @returns {Promise<string>} stdout (trimmed)
76
+ * @throws {Error} If the docker exec invocation fails
77
+ */
78
+ async function runPsql(container, sql, adminPassword) {
79
+ if (!container || typeof container !== 'string') {
80
+ throw new Error('Postgres container name is required');
81
+ }
82
+ if (!sql || typeof sql !== 'string') {
83
+ throw new Error('SQL statement is required');
84
+ }
85
+ if (!adminPassword || typeof adminPassword !== 'string') {
86
+ throw new Error('Admin password is required');
87
+ }
88
+ const escapedSql = sql.replace(/"/g, '\\"');
89
+ const cmd = `docker exec -e PGPASSWORD -i ${container} psql -U ${SUPERUSER_ROLE} -d postgres -tAc "${escapedSql}"`;
90
+ const env = { PGPASSWORD: adminPassword };
91
+ const result = (await dockerExec.execWithDockerEnv(cmd, { env })) || {};
92
+ return String(result.stdout || '').trim();
93
+ }
94
+
95
+ /**
96
+ * List user databases (non-template, not in PROTECTED_DATABASES).
97
+ * @async
98
+ * @param {string} container - Container name
99
+ * @param {string} adminPassword - Postgres superuser password
100
+ * @returns {Promise<string[]>} database names
101
+ */
102
+ async function listUserDatabases(container, adminPassword) {
103
+ const out = await runPsql(
104
+ container,
105
+ 'SELECT datname FROM pg_database WHERE datistemplate = false;',
106
+ adminPassword
107
+ );
108
+ return out
109
+ .split('\n')
110
+ .map(s => s.trim())
111
+ .filter(name => name && !PROTECTED_DATABASES.has(name));
112
+ }
113
+
114
+ /**
115
+ * List dropable roles (non-superuser, not in PROTECTED_ROLES).
116
+ * @async
117
+ * @param {string} container - Container name
118
+ * @param {string} adminPassword - Postgres superuser password
119
+ * @returns {Promise<string[]>} role names
120
+ */
121
+ async function listDropableRoles(container, adminPassword) {
122
+ const out = await runPsql(
123
+ container,
124
+ 'SELECT rolname FROM pg_roles WHERE rolsuper = false;',
125
+ adminPassword
126
+ );
127
+ return out
128
+ .split('\n')
129
+ .map(s => s.trim())
130
+ .filter(name => name && !PROTECTED_ROLES.has(name) && !name.startsWith('pg_'));
131
+ }
132
+
133
+ /**
134
+ * Drop a single database (forces disconnection of active sessions).
135
+ * @async
136
+ * @param {string} container
137
+ * @param {string} dbName
138
+ * @param {string} adminPassword
139
+ * @returns {Promise<void>}
140
+ */
141
+ async function dropDatabase(container, dbName, adminPassword) {
142
+ if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(dbName)) {
143
+ throw new Error(`Refusing to drop database with unsafe name: ${dbName}`);
144
+ }
145
+ await runPsql(
146
+ container,
147
+ `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${dbName}' AND pid <> pg_backend_pid();`,
148
+ adminPassword
149
+ );
150
+ await runPsql(container, `DROP DATABASE IF EXISTS "${dbName}";`, adminPassword);
151
+ }
152
+
153
+ /**
154
+ * Drop a single role (REASSIGN OWNED + DROP OWNED first to clear dependencies).
155
+ * @async
156
+ * @param {string} container
157
+ * @param {string} role
158
+ * @param {string} adminPassword
159
+ * @returns {Promise<void>}
160
+ */
161
+ async function dropRole(container, role, adminPassword) {
162
+ if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(role)) {
163
+ throw new Error(`Refusing to drop role with unsafe name: ${role}`);
164
+ }
165
+ await runPsql(
166
+ container,
167
+ `REASSIGN OWNED BY "${role}" TO ${SUPERUSER_ROLE};`,
168
+ adminPassword
169
+ ).catch(() => undefined);
170
+ await runPsql(container, `DROP OWNED BY "${role}" CASCADE;`, adminPassword).catch(() => undefined);
171
+ await runPsql(container, `DROP ROLE IF EXISTS "${role}";`, adminPassword);
172
+ }
173
+
174
+ /**
175
+ * Drop every non-template database and every non-superuser role in the
176
+ * developer's running Postgres container. Caller must ensure infra is up.
177
+ *
178
+ * @async
179
+ * @function wipePostgresData
180
+ * @returns {Promise<{ databases: string[], roles: string[] }>} dropped names
181
+ * @throws {Error} If admin secrets are missing or psql calls fail
182
+ */
183
+ async function wipePostgresData() {
184
+ const devId = await config.getDeveloperId();
185
+ const container = getPostgresContainerName(devId);
186
+ const admin = await adminSecrets.readAndDecryptAdminSecrets();
187
+ const password = admin && admin.POSTGRES_PASSWORD;
188
+ if (!password) {
189
+ throw new Error('POSTGRES_PASSWORD not found in admin-secrets.env. Run "aifabrix up-infra" first.');
190
+ }
191
+
192
+ const databases = await listUserDatabases(container, password);
193
+ for (const dbName of databases) {
194
+ await dropDatabase(container, dbName, password);
195
+ logger.log(chalk.gray(` ${successGlyph()} Dropped database ${dbName}`));
196
+ }
197
+
198
+ const roles = await listDropableRoles(container, password);
199
+ for (const role of roles) {
200
+ await dropRole(container, role, password);
201
+ logger.log(chalk.gray(` ${successGlyph()} Dropped role ${role}`));
202
+ }
203
+
204
+ return { databases, roles };
205
+ }
206
+
207
+ module.exports = {
208
+ wipePostgresData,
209
+ getPostgresContainerName,
210
+ PROTECTED_DATABASES,
211
+ PROTECTED_ROLES
212
+ };
@@ -191,8 +191,23 @@ async function registerAifabrixShellEnvFromConfig(getConfigFn, overrides = {}) {
191
191
  await applyPosixShellEnv(homeAbs, workAbs, overrides);
192
192
  }
193
193
 
194
+ /**
195
+ * Build sh export lines for AIFABRIX_HOME / AIFABRIX_WORK from current config (stdout for eval).
196
+ *
197
+ * @async
198
+ * @param {function(): Promise<object>} getConfigFn - Same as config.getConfig
199
+ * @returns {Promise<string>} Lines suitable for `eval "$(aifabrix dev shell-env)"` on bash/zsh
200
+ */
201
+ async function buildShellEnvExportsFromConfig(getConfigFn) {
202
+ const config = await getConfigFn();
203
+ const homeAbs = absFromConfigRaw(config['aifabrix-home']);
204
+ const workAbs = absFromConfigRaw(config['aifabrix-work']);
205
+ return buildPosixShellEnvBody(homeAbs, workAbs);
206
+ }
207
+
194
208
  module.exports = {
195
209
  registerAifabrixShellEnvFromConfig,
210
+ buildShellEnvExportsFromConfig,
196
211
  buildPosixShellEnvBody,
197
212
  buildProfileBlock,
198
213
  shSingleQuoted,
@@ -5,10 +5,25 @@
5
5
  */
6
6
 
7
7
  const path = require('path');
8
+ const os = require('os');
8
9
  const config = require('../core/config');
9
10
  const { getCertDir, readClientCertPem, readServerCaPem } = require('./dev-cert-helper');
10
11
  const { getConfigDirForPaths, getAifabrixHome, getAifabrixWork } = require('./paths');
11
12
 
13
+ /**
14
+ * Same rules as {@link module:lib/core/config.expandTilde} / {@link module:lib/core/config.getSecretsPath}.
15
+ * Inline here so `resolveSharedSecretsEndpoint` stays aligned with `getSecretsPath()` even when `getAifabrixSecretsPath()`
16
+ * returns the raw config string (tilde not expanded).
17
+ */
18
+ function expandConfiguredSecretsTilde(filePath) {
19
+ if (!filePath || typeof filePath !== 'string') return filePath;
20
+ if (filePath === '~') return os.homedir();
21
+ if (filePath.startsWith('~/') || filePath.startsWith('~' + path.sep)) {
22
+ return path.join(os.homedir(), filePath.slice(2));
23
+ }
24
+ return filePath;
25
+ }
26
+
12
27
  /**
13
28
  * Single API object so resolveSharedSecretsEndpoint and callers share one getRemoteDevAuth
14
29
  * (Jest spies and partial mocks work reliably).
@@ -54,16 +69,17 @@ const remoteDevAuth = {
54
69
  const trimmed = configuredPath.trim();
55
70
  if (!trimmed) return configuredPath;
56
71
  if (remoteDevAuth.isRemoteSecretsUrl(trimmed)) return trimmed.replace(/\/+$/, '');
72
+ const expanded = expandConfiguredSecretsTilde(trimmed);
57
73
  const auth = await remoteDevAuth.getRemoteDevAuth();
58
- if (!auth) return configuredPath;
59
- const abs = normalizeSharedSecretsFilePath(trimmed);
60
- if (!abs) return configuredPath;
74
+ if (!auth) return expanded;
75
+ const abs = normalizeSharedSecretsFilePath(expanded);
76
+ if (!abs) return expanded;
61
77
  const home = path.normalize(getAifabrixHome());
62
- if (isPathUnderDir(abs, home)) return configuredPath;
78
+ if (isPathUnderDir(abs, home)) return expanded;
63
79
  const work = getAifabrixWork();
64
80
  if (work) {
65
81
  const w = path.normalize(work);
66
- if (isPathUnderDir(abs, w)) return configuredPath;
82
+ if (isPathUnderDir(abs, w)) return expanded;
67
83
  }
68
84
  const base = String(auth.serverUrl).replace(/\/+$/, '');
69
85
  return `${base}/api/dev/secrets`;
@@ -89,7 +89,15 @@ async function getDockerExecEnv() {
89
89
  if (overlay.DOCKER_HOST && !Object.prototype.hasOwnProperty.call(overlay, 'DOCKER_CERT_PATH')) {
90
90
  delete merged.DOCKER_CERT_PATH;
91
91
  }
92
- return { ...merged, ...overlay };
92
+ let out = { ...merged, ...overlay };
93
+ try {
94
+ const { getBashPrefixedProcessEnvOverlay } = require('./bash-secret-env');
95
+ const bash = await getBashPrefixedProcessEnvOverlay();
96
+ out = { ...out, ...bash };
97
+ } catch {
98
+ /* ignore secrets load failures; Docker CLI still runs with process + remote env */
99
+ }
100
+ return out;
93
101
  }
94
102
 
95
103
  module.exports = { getRemoteDockerEnv, getDockerExecEnv };
@@ -1,13 +1,17 @@
1
1
  /**
2
- * Load shared secrets from Builder Server when aifabrix-secrets is an http(s) URL.
3
- * Used for .env resolution only; values are never persisted to disk.
2
+ * Load shared secrets from Builder Server when aifabrix-secrets is an http(s) URL,
3
+ * or from the configured shared YAML path when it targets a local file.
4
4
  *
5
5
  * @fileoverview Remote shared secrets loader for .env generation
6
6
  * @author AI Fabrix Team
7
7
  * @version 2.0.0
8
8
  */
9
9
 
10
+ const fs = require('fs');
11
+ const path = require('path');
10
12
  const config = require('../core/config');
13
+ const { readYamlAtPath } = require('./secrets-canonical');
14
+ const { ensureSecureFilePermissions } = require('./secure-file-permissions');
11
15
 
12
16
  /**
13
17
  * Fetches shared secrets from Builder Server when aifabrix-secrets is an http(s) URL.
@@ -50,20 +54,61 @@ async function loadRemoteSharedSecrets() {
50
54
  * Merges remote shared secrets with user secrets. User wins on same key.
51
55
  * @param {Object} userSecrets - User secrets object
52
56
  * @param {Object} remoteSecrets - Remote API secrets (key-value)
57
+ * @param {Record<string, string>} [keySources] - Mutated: winning file/API label per key (decrypt hints)
58
+ * @param {string} [remoteSourceLabel] - Human-readable source for keys taken from remote
53
59
  * @returns {Object} Merged object
54
60
  */
55
- function mergeUserWithRemoteSecrets(userSecrets, remoteSecrets) {
61
+ function mergeUserWithRemoteSecrets(userSecrets, remoteSecrets, keySources, remoteSourceLabel) {
56
62
  const merged = { ...userSecrets };
57
63
  if (!remoteSecrets || typeof remoteSecrets !== 'object') return merged;
64
+ const label = remoteSourceLabel || 'shared secrets API';
58
65
  for (const key of Object.keys(remoteSecrets)) {
59
66
  if (!(key in merged) || merged[key] === undefined || merged[key] === null || merged[key] === '') {
60
67
  merged[key] = remoteSecrets[key];
68
+ if (keySources) {
69
+ keySources[key] = label;
70
+ }
61
71
  }
62
72
  }
63
73
  return merged;
64
74
  }
65
75
 
76
+ /**
77
+ * Raw secrets from the configured shared store only (`aifabrix-secrets`): remote Builder API or shared YAML file.
78
+ * Does not include primary user secrets or builder merges. Used to avoid duplicating shared keys into ~/.aifabrix.
79
+ *
80
+ * @returns {Promise<Object|null>} Key-value map or null when unavailable / not configured
81
+ */
82
+ async function loadConfiguredSharedSecretsStore() {
83
+ const remoteDevAuth = require('./remote-dev-auth');
84
+ const configSecretsPath = await config.getSecretsPath();
85
+ if (!configSecretsPath) {
86
+ return null;
87
+ }
88
+ const endpoint = await remoteDevAuth.resolveSharedSecretsEndpoint(configSecretsPath);
89
+ if (remoteDevAuth.isRemoteSecretsUrl(endpoint)) {
90
+ return loadRemoteSharedSecrets();
91
+ }
92
+ const resolvedFile = path.isAbsolute(endpoint)
93
+ ? endpoint
94
+ : path.resolve(process.cwd(), endpoint);
95
+ if (!fs.existsSync(resolvedFile)) {
96
+ return null;
97
+ }
98
+ try {
99
+ ensureSecureFilePermissions(resolvedFile);
100
+ const data = readYamlAtPath(resolvedFile);
101
+ if (!data || typeof data !== 'object') {
102
+ return null;
103
+ }
104
+ return data;
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+
66
110
  module.exports = {
67
111
  loadRemoteSharedSecrets,
68
- mergeUserWithRemoteSecrets
112
+ mergeUserWithRemoteSecrets,
113
+ loadConfiguredSharedSecretsStore
69
114
  };
@@ -73,16 +73,21 @@ function normalizeDockerRegistryPrefix(registry) {
73
73
  */
74
74
  function resolveDockerImageRef(appName, appConfig, runOptions = {}) {
75
75
  const opts = runOptions || {};
76
+ const tagFromOpts =
77
+ opts.tag !== undefined && opts.tag !== null && String(opts.tag).trim() !== ''
78
+ ? String(opts.tag).trim()
79
+ : null;
80
+
76
81
  if (opts.image) {
77
82
  const parsed = parseImageOverride(opts.image);
78
83
  return {
79
84
  imageName: parsed ? parsed.name : getRepositoryPathFromConfig(appConfig, appName),
80
- imageTag: parsed ? parsed.tag : imageTagFromConfig(appConfig)
85
+ imageTag: parsed ? parsed.tag : tagFromOpts || imageTagFromConfig(appConfig)
81
86
  };
82
87
  }
83
88
 
84
89
  const baseRepo = getRepositoryPathFromConfig(appConfig, appName);
85
- const imageTag = imageTagFromConfig(appConfig);
90
+ const imageTag = tagFromOpts || imageTagFromConfig(appConfig);
86
91
  const prefix =
87
92
  normalizeDockerRegistryPrefix(opts.registry) ||
88
93
  normalizeDockerRegistryPrefix(appConfig?.image?.registry ?? '');
@@ -120,5 +125,6 @@ module.exports = {
120
125
  resolveDockerImageRef,
121
126
  resolveComposeImageOverrideString,
122
127
  normalizeDockerRegistryPrefix,
123
- getRepositoryPathFromConfig
128
+ getRepositoryPathFromConfig,
129
+ imageTagFromConfig
124
130
  };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Normalize Commander flags for `aifabrix run`.
3
+ *
4
+ * @fileoverview Commander v11 pairs `--no-proxy` with a default-true `--proxy` flag as `options.proxy === false`.
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ /**
12
+ * True when the user disabled proxy hints: explicit `--no-proxy` or `proxy === false` when supported.
13
+ *
14
+ * @param {Object} [options] - Commander action options
15
+ * @returns {boolean}
16
+ */
17
+ function isRunCliNoProxy(options) {
18
+ if (!options || typeof options !== 'object') {
19
+ return false;
20
+ }
21
+ if (options.proxy === false) {
22
+ return true;
23
+ }
24
+ return options.noProxy === true;
25
+ }
26
+
27
+ module.exports = {
28
+ isRunCliNoProxy
29
+ };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Collect `secrets.local.yaml` paths along the cwd → root walk so parent workspace
3
+ * secrets (e.g. `/workspace/.aifabrix/`) merge when the active config is nested
4
+ * (e.g. `repo/.aifabrix/` from cwd).
5
+ *
6
+ * @fileoverview Ancestor secrets.local.yaml discovery for loadSecrets cascade
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const path = require('path');
12
+
13
+ const MAX_ANCESTOR_STEPS = 64;
14
+
15
+ /**
16
+ * @param {string} startDir - Directory to start from (typically process.cwd())
17
+ * @param {(p: string) => boolean} existsSyncFn - Sync existence check
18
+ * @returns {string[]} Absolute paths, nearest ancestor first (cwd-side), then parents
19
+ */
20
+ function collectAncestorAifabrixSecretsLocalYamlPaths(startDir, existsSyncFn) {
21
+ if (!startDir || typeof startDir !== 'string' || typeof existsSyncFn !== 'function') {
22
+ return [];
23
+ }
24
+ const out = [];
25
+ const seen = new Set();
26
+ let dir = path.resolve(startDir);
27
+ for (let i = 0; i < MAX_ANCESTOR_STEPS; i += 1) {
28
+ const secretsPath = path.join(dir, '.aifabrix', 'secrets.local.yaml');
29
+ if (existsSyncFn(secretsPath)) {
30
+ const abs = path.resolve(secretsPath);
31
+ if (!seen.has(abs)) {
32
+ seen.add(abs);
33
+ out.push(abs);
34
+ }
35
+ }
36
+ const parent = path.dirname(dir);
37
+ if (parent === dir) {
38
+ break;
39
+ }
40
+ dir = parent;
41
+ }
42
+ return out;
43
+ }
44
+
45
+ module.exports = {
46
+ collectAncestorAifabrixSecretsLocalYamlPaths
47
+ };
@@ -31,11 +31,14 @@ function readYamlAtPath(filePath) {
31
31
  * @param {string} key - Secret key
32
32
  * @param {*} canonicalValue - Value from canonical secrets
33
33
  */
34
- function mergeSecretValue(result, key, canonicalValue) {
34
+ function mergeSecretValue(result, key, canonicalValue, keySources, canonicalSourcePath) {
35
35
  const currentValue = result[key];
36
36
  // Fill missing, empty, or undefined values
37
37
  if (!(key in result) || currentValue === undefined || currentValue === null || currentValue === '') {
38
38
  result[key] = canonicalValue;
39
+ if (keySources && canonicalSourcePath) {
40
+ keySources[key] = canonicalSourcePath;
41
+ }
39
42
  return;
40
43
  }
41
44
  // Only replace values that are encrypted (have secure:// prefix)
@@ -43,6 +46,9 @@ function mergeSecretValue(result, key, canonicalValue) {
43
46
  if (typeof currentValue === 'string' && typeof canonicalValue === 'string') {
44
47
  if (currentValue.startsWith('secure://')) {
45
48
  result[key] = canonicalValue;
49
+ if (keySources && canonicalSourcePath) {
50
+ keySources[key] = canonicalSourcePath;
51
+ }
46
52
  }
47
53
  }
48
54
  }
@@ -52,9 +58,10 @@ function mergeSecretValue(result, key, canonicalValue) {
52
58
  * @async
53
59
  * @function applyCanonicalSecretsOverride
54
60
  * @param {Object} currentSecrets - Current secrets map
61
+ * @param {Record<string, string>} [keySources] - Mutated: per-key source path when a value is taken from canonical YAML
55
62
  * @returns {Promise<Object>} Possibly overridden secrets
56
63
  */
57
- async function applyCanonicalSecretsOverride(currentSecrets) {
64
+ async function applyCanonicalSecretsOverride(currentSecrets, keySources) {
58
65
  let mergedSecrets = currentSecrets || {};
59
66
  try {
60
67
  const canonicalPath = await config.getSecretsPath();
@@ -78,7 +85,7 @@ async function applyCanonicalSecretsOverride(currentSecrets) {
78
85
  // - Replace encrypted values (secure://) with canonical plaintext
79
86
  const result = { ...mergedSecrets };
80
87
  for (const [key, canonicalValue] of Object.entries(configSecrets)) {
81
- mergeSecretValue(result, key, canonicalValue);
88
+ mergeSecretValue(result, key, canonicalValue, keySources, resolvedCanonical);
82
89
  }
83
90
  mergedSecrets = result;
84
91
  } catch {
@@ -19,6 +19,7 @@ const { updateContainerPortInEnvFile } = require('./env-ports');
19
19
  const { buildEnvVarMap } = require('./env-map');
20
20
  const { getLocalPortFromPath } = require('./port-resolver');
21
21
  const { readYamlAtPath, applyCanonicalSecretsOverride } = require('./secrets-canonical');
22
+ const { collectUniqueKvPathStrings } = require('./secrets-kv-refs');
22
23
 
23
24
  /**
24
25
  * Interpolate ${VAR} occurrences with values from envVars map
@@ -51,9 +52,13 @@ const { resolveBashKvFromProcessEnv } = require('./secrets-bash-kv');
51
52
  /**
52
53
  * Last-resort when infra.parameter.yaml cannot be read (e.g. Jest suites that mock `fs`;
53
54
  * catalog uses `node:fs`, which is often the same mocked instance). Must stay in sync with
54
- * `generator.type: emptyAllowed` keys in lib/schema/infra.parameter.yaml.
55
+ * `generator.type: emptyAllowed` keys in lib/schema/infra.parameter.yaml; plus optional Azure/OpenAI kv names.
55
56
  */
56
- const EMPTY_ALLOWED_KV_FALLBACK = new Set(['redis-passwordKeyVault']);
57
+ const EMPTY_ALLOWED_KV_FALLBACK = new Set([
58
+ 'redis-passwordKeyVault',
59
+ 'azure-openaiapi-urlKeyVault',
60
+ 'secrets-azureOpenaiApiKeyVault'
61
+ ]);
57
62
 
58
63
  /**
59
64
  * Infra catalog keys with generator `emptyAllowed` may be absent from the secrets file;
@@ -191,7 +196,12 @@ function formatMissingSecretsFileInfo(secretsFilePaths) {
191
196
  if (secretsFilePaths.buildPath) {
192
197
  paths.push(secretsFilePaths.buildPath);
193
198
  }
194
- return `\n\nSecrets file location: ${paths.join(' and ')}`;
199
+ let msg = `\n\nSecrets file location: ${paths.join(' and ')}`;
200
+ if (secretsFilePaths.sharedSecretsApiUrl && typeof secretsFilePaths.sharedSecretsApiUrl === 'string') {
201
+ msg +=
202
+ `\n(Shared secrets API: ${secretsFilePaths.sharedSecretsApiUrl.trim()}. Keys from that API are merged when resolving secrets; if a key is still missing, add it via "aifabrix secret set" / shared store or check "aifabrix secret list --shared".)`;
203
+ }
204
+ return msg;
195
205
  }
196
206
  return '';
197
207
  }
@@ -459,13 +469,7 @@ async function adjustLocalEnvPortsInContent(envContent, variablesPath) {
459
469
  return updated;
460
470
  }
461
471
 
462
- /**
463
- * Validate secrets against the env template (skips commented and empty lines)
464
- * @function validateSecrets
465
- * @param {string} envTemplate - Environment template content
466
- * @param {Object} secrets - Available secrets
467
- * @returns {Object} Validation result
468
- */
472
+ /** Validate secrets vs env template (commented/empty lines skipped). */
469
473
  function validateSecrets(envTemplate, secrets) {
470
474
  const missing = collectMissingSecrets(envTemplate, secrets);
471
475
  return { valid: missing.length === 0, missing };
@@ -475,6 +479,9 @@ module.exports = {
475
479
  loadEnvConfig,
476
480
  interpolateEnvVars,
477
481
  collectMissingSecrets,
482
+ collectUniqueKvPathStrings,
483
+ resolveKvRefValue,
484
+ isKvKeyAllowedEmptyWhenAbsent,
478
485
  resolveBashKvFromProcessEnv,
479
486
  mergeSecretsWithPrefixedCopies,
480
487
  formatMissingSecretsFileInfo,
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Scan env-style content for unique kv:// path segments (comments skipped).
3
+ * @fileoverview Keeps secrets-helpers under max-lines
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const KV_REF_PATTERN = /kv:\/\/([a-zA-Z0-9_\-/]+)/g;
9
+
10
+ function isCommentOrEmptyLine(line) {
11
+ const t = line.trim();
12
+ return t === '' || t.startsWith('#');
13
+ }
14
+
15
+ /**
16
+ * @param {string} content
17
+ * @returns {string[]}
18
+ */
19
+ function collectUniqueKvPathStrings(content) {
20
+ const seen = new Set();
21
+ const out = [];
22
+ if (!content || typeof content !== 'string') {
23
+ return out;
24
+ }
25
+ for (const line of content.split('\n')) {
26
+ if (isCommentOrEmptyLine(line)) continue;
27
+ let match;
28
+ KV_REF_PATTERN.lastIndex = 0;
29
+ while ((match = KV_REF_PATTERN.exec(line)) !== null) {
30
+ const pathStr = match[1];
31
+ if (!seen.has(pathStr)) {
32
+ seen.add(pathStr);
33
+ out.push(pathStr);
34
+ }
35
+ }
36
+ }
37
+ return out;
38
+ }
39
+
40
+ module.exports = {
41
+ collectUniqueKvPathStrings
42
+ };