@aifabrix/builder 2.44.5 → 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 (207) 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 +48 -2
  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/validation-runner.js +46 -25
  17. package/lib/app/deploy-config.js +11 -1
  18. package/lib/app/deploy-status-display.js +3 -3
  19. package/lib/app/deploy.js +36 -14
  20. package/lib/app/display.js +15 -11
  21. package/lib/app/push.js +46 -23
  22. package/lib/app/register.js +1 -1
  23. package/lib/app/restart-display.js +95 -0
  24. package/lib/app/rotate-secret.js +1 -1
  25. package/lib/app/run-container-start.js +12 -6
  26. package/lib/app/run-env-compose.js +30 -1
  27. package/lib/app/run-helpers.js +44 -12
  28. package/lib/app/run-reload-sync.js +148 -0
  29. package/lib/app/run-resolve-image.js +51 -1
  30. package/lib/app/run.js +99 -73
  31. package/lib/build/index.js +75 -45
  32. package/lib/cli/doctor-check.js +117 -0
  33. package/lib/cli/index.js +8 -2
  34. package/lib/cli/infra-guided.js +445 -0
  35. package/lib/cli/setup-app.js +20 -2
  36. package/lib/cli/setup-auth.js +26 -0
  37. package/lib/cli/setup-dev-path-commands.js +50 -3
  38. package/lib/cli/setup-infra.js +134 -61
  39. package/lib/cli/setup-integration-client.js +182 -0
  40. package/lib/cli/setup-parameters.js +21 -2
  41. package/lib/cli/setup-platform.js +102 -0
  42. package/lib/cli/setup-secrets.js +18 -6
  43. package/lib/cli/setup-utility.js +78 -33
  44. package/lib/commands/datasource-capability-dimension-cli.js +128 -0
  45. package/lib/commands/datasource-capability-output.js +29 -0
  46. package/lib/commands/datasource-capability-relate-cli.js +140 -0
  47. package/lib/commands/datasource-capability.js +411 -0
  48. package/lib/commands/datasource-unified-test-cli.options.js +1 -1
  49. package/lib/commands/datasource.js +53 -13
  50. package/lib/commands/dev-down.js +3 -3
  51. package/lib/commands/dev-infra-gate.js +32 -0
  52. package/lib/commands/dev-init.js +13 -7
  53. package/lib/commands/dimension-value.js +179 -0
  54. package/lib/commands/dimension.js +330 -0
  55. package/lib/commands/integration-client.js +430 -0
  56. package/lib/commands/login-device.js +65 -30
  57. package/lib/commands/login.js +21 -10
  58. package/lib/commands/parameters-validate.js +78 -13
  59. package/lib/commands/repair-datasource-auto-rbac.js +166 -0
  60. package/lib/commands/repair-datasource-keys.js +10 -5
  61. package/lib/commands/repair-datasource.js +19 -7
  62. package/lib/commands/repair-env-template.js +4 -1
  63. package/lib/commands/repair-openapi-sync.js +172 -0
  64. package/lib/commands/repair-persist.js +102 -0
  65. package/lib/commands/repair-rbac-extract.js +27 -0
  66. package/lib/commands/repair-rbac-migrate.js +186 -0
  67. package/lib/commands/repair-rbac.js +214 -31
  68. package/lib/commands/repair-system-alignment.js +246 -0
  69. package/lib/commands/repair-system-permissions.js +168 -0
  70. package/lib/commands/repair.js +120 -338
  71. package/lib/commands/secure.js +1 -1
  72. package/lib/commands/setup-modes.js +455 -0
  73. package/lib/commands/setup-prompts.js +388 -0
  74. package/lib/commands/setup.js +149 -0
  75. package/lib/commands/teardown.js +228 -0
  76. package/lib/commands/up-common.js +79 -19
  77. package/lib/commands/up-dataplane.js +33 -11
  78. package/lib/commands/up-miso.js +7 -11
  79. package/lib/commands/upload.js +109 -23
  80. package/lib/commands/wizard-core-helpers.js +14 -11
  81. package/lib/commands/wizard-core.js +6 -5
  82. package/lib/commands/wizard-dataplane.js +2 -2
  83. package/lib/commands/wizard-entity-selection.js +4 -3
  84. package/lib/commands/wizard-headless.js +2 -1
  85. package/lib/commands/wizard.js +2 -1
  86. package/lib/constants/infra-compose-service-names.js +40 -0
  87. package/lib/core/env-reader.js +16 -3
  88. package/lib/core/secrets-admin-env.js +101 -0
  89. package/lib/core/secrets-ensure-infra.js +34 -1
  90. package/lib/core/secrets-ensure.js +88 -66
  91. package/lib/core/secrets-env-content.js +432 -0
  92. package/lib/core/secrets-env-write.js +27 -1
  93. package/lib/core/secrets-load.js +248 -0
  94. package/lib/core/secrets-names.js +32 -0
  95. package/lib/core/secrets.js +17 -757
  96. package/lib/datasource/capability/basic-exposure.js +76 -0
  97. package/lib/datasource/capability/capability-diff-slice.js +41 -0
  98. package/lib/datasource/capability/capability-key.js +34 -0
  99. package/lib/datasource/capability/capability-resolve.js +172 -0
  100. package/lib/datasource/capability/capability-storage-keys.js +22 -0
  101. package/lib/datasource/capability/copy-operations.js +348 -0
  102. package/lib/datasource/capability/copy-test-payload.js +139 -0
  103. package/lib/datasource/capability/create-operations.js +235 -0
  104. package/lib/datasource/capability/dimension-operations.js +151 -0
  105. package/lib/datasource/capability/dimension-validate.js +219 -0
  106. package/lib/datasource/capability/json-pointer.js +31 -0
  107. package/lib/datasource/capability/reference-rewrite.js +51 -0
  108. package/lib/datasource/capability/relate-operations.js +325 -0
  109. package/lib/datasource/capability/relate-validate.js +219 -0
  110. package/lib/datasource/capability/remove-operations.js +275 -0
  111. package/lib/datasource/capability/run-capability-copy.js +152 -0
  112. package/lib/datasource/capability/run-capability-diff.js +135 -0
  113. package/lib/datasource/capability/run-capability-dimension.js +291 -0
  114. package/lib/datasource/capability/run-capability-edit.js +377 -0
  115. package/lib/datasource/capability/run-capability-relate.js +193 -0
  116. package/lib/datasource/capability/run-capability-remove.js +105 -0
  117. package/lib/datasource/capability/templates/minimal-fetch.json +18 -0
  118. package/lib/datasource/capability/validate-capability-slice.js +35 -0
  119. package/lib/datasource/list.js +136 -23
  120. package/lib/datasource/log-viewer.js +2 -4
  121. package/lib/datasource/unified-validation-run.js +51 -16
  122. package/lib/datasource/validate.js +53 -1
  123. package/lib/deployment/deploy-poll-ui.js +60 -0
  124. package/lib/deployment/deployer-status.js +29 -3
  125. package/lib/deployment/deployer.js +48 -30
  126. package/lib/deployment/environment.js +7 -2
  127. package/lib/deployment/poll-interval.js +72 -0
  128. package/lib/deployment/push.js +11 -9
  129. package/lib/external-system/deploy.js +4 -1
  130. package/lib/external-system/download.js +61 -32
  131. package/lib/external-system/sync-deploy-manifest.js +33 -0
  132. package/lib/infrastructure/index.js +49 -19
  133. package/lib/infrastructure/orphan-infra-docker-teardown.js +177 -0
  134. package/lib/parameters/infra-kv-discovery.js +29 -4
  135. package/lib/parameters/infra-parameter-catalog.js +6 -3
  136. package/lib/parameters/infra-parameter-validate.js +67 -19
  137. package/lib/resolvers/datasource-resolver.js +53 -0
  138. package/lib/resolvers/dimension-file.js +52 -0
  139. package/lib/resolvers/manifest-resolver.js +133 -0
  140. package/lib/schema/external-datasource.schema.json +183 -53
  141. package/lib/schema/external-system.schema.json +23 -10
  142. package/lib/schema/infra.parameter.yaml +26 -11
  143. package/lib/schema/wizard-config.schema.json +1 -1
  144. package/lib/utils/aifabrix-config-dir-walk.js +40 -0
  145. package/lib/utils/aifabrix-runtime-config-dir.js +26 -3
  146. package/lib/utils/app-run-containers.js +2 -2
  147. package/lib/utils/bash-secret-env.js +59 -0
  148. package/lib/utils/cli-secrets-error-format.js +78 -0
  149. package/lib/utils/cli-test-layout-chalk.js +31 -9
  150. package/lib/utils/cli-utils.js +4 -36
  151. package/lib/utils/datasource-test-run-display.js +8 -0
  152. package/lib/utils/dev-hosts-helper.js +3 -2
  153. package/lib/utils/dev-init-ssh-merge.js +2 -1
  154. package/lib/utils/docker-build.js +17 -9
  155. package/lib/utils/docker-reload-mount.js +127 -0
  156. package/lib/utils/external-readme.js +71 -2
  157. package/lib/utils/external-system-local-test-tty.js +3 -2
  158. package/lib/utils/external-system-readiness-core.js +45 -12
  159. package/lib/utils/external-system-readiness-deploy-display.js +3 -3
  160. package/lib/utils/external-system-readiness-display-internals.js +33 -3
  161. package/lib/utils/external-system-readiness-display.js +10 -1
  162. package/lib/utils/file-upload.js +40 -3
  163. package/lib/utils/health-check-db-init.js +107 -0
  164. package/lib/utils/health-check-public-warn.js +69 -0
  165. package/lib/utils/health-check-url.js +19 -4
  166. package/lib/utils/health-check.js +135 -105
  167. package/lib/utils/help-builder.js +5 -1
  168. package/lib/utils/image-name.js +34 -7
  169. package/lib/utils/integration-file-backup.js +74 -0
  170. package/lib/utils/mutagen-install.js +30 -3
  171. package/lib/utils/paths.js +108 -25
  172. package/lib/utils/postgres-wipe.js +212 -0
  173. package/lib/utils/register-aifabrix-shell-env.js +15 -0
  174. package/lib/utils/remote-dev-auth.js +21 -5
  175. package/lib/utils/remote-docker-env.js +9 -1
  176. package/lib/utils/remote-secrets-loader.js +42 -3
  177. package/lib/utils/resolve-docker-image-ref.js +9 -3
  178. package/lib/utils/secrets-ancestor-paths.js +47 -0
  179. package/lib/utils/secrets-helpers.js +17 -10
  180. package/lib/utils/secrets-kv-refs.js +42 -0
  181. package/lib/utils/secrets-kv-scope.js +19 -2
  182. package/lib/utils/secrets-materialize-local.js +134 -0
  183. package/lib/utils/secrets-path.js +24 -10
  184. package/lib/utils/secrets-utils.js +2 -2
  185. package/lib/utils/system-builder-root.js +34 -0
  186. package/lib/utils/url-declarative-resolve-build.js +6 -1
  187. package/lib/utils/url-declarative-runtime-base-path.js +32 -0
  188. package/lib/utils/url-declarative-vdir-inactive-env.js +2 -1
  189. package/lib/utils/urls-local-registry.js +23 -12
  190. package/lib/utils/validation-poll-ui.js +81 -0
  191. package/lib/utils/validation-run-poll.js +29 -5
  192. package/lib/utils/with-muted-logger.js +53 -0
  193. package/package.json +1 -1
  194. package/templates/applications/dataplane/application.yaml +1 -1
  195. package/templates/applications/dataplane/rbac.yaml +10 -10
  196. package/templates/applications/keycloak/env.template +8 -6
  197. package/templates/applications/miso-controller/application.yaml +7 -0
  198. package/templates/applications/miso-controller/env.template +1 -1
  199. package/templates/applications/miso-controller/rbac.yaml +9 -9
  200. package/templates/external-system/README.md.hbs +83 -123
  201. package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  202. package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  203. package/.nyc_output/processinfo/index.json +0 -1
  204. package/lib/api/service-users.api.js +0 -150
  205. package/lib/api/types/service-users.types.js +0 -65
  206. package/lib/cli/setup-service-user.js +0 -187
  207. package/lib/commands/service-user.js +0 -429
@@ -17,6 +17,7 @@ const {
17
17
  getAifabrixRuntimeConfigDir,
18
18
  resolveAifabrixHomeLikePath
19
19
  } = require('./aifabrix-runtime-config-dir');
20
+ const { resolveSystemBuilderParentDir } = require('./system-builder-root');
20
21
 
21
22
  function safeHomedir() {
22
23
  try {
@@ -43,14 +44,13 @@ function getConfigDirForPaths() {
43
44
  }
44
45
 
45
46
  /**
46
- * User-owned `secrets.local.yaml` beside effective `config.yaml` (see {@link getConfigDirForPaths}).
47
- * When `AIFABRIX_HOME` is `$HOME` but config is only at `$HOME/.aifabrix/config.yaml`, uses that folder.
48
- * Keeps `secret list` / `secret set` aligned with resolve merge (`loadPrimaryUserSecrets`).
47
+ * User-owned `secrets.local.yaml` under {@link getAifabrixHome} (same rule as `aifabrix-home` in
48
+ * `config.yaml`, else `~/.aifabrix`). Keeps `secret set` / merge loaders aligned with declared home.
49
49
  *
50
50
  * @returns {string} Absolute path to secrets.local.yaml
51
51
  */
52
52
  function getPrimaryUserSecretsLocalPath() {
53
- return path.join(getConfigDirForPaths(), 'secrets.local.yaml');
53
+ return path.join(getAifabrixHome(), 'secrets.local.yaml');
54
54
  }
55
55
 
56
56
  /**
@@ -327,7 +327,7 @@ function getAppPath(appName, appType) {
327
327
  if (appType === 'external') {
328
328
  return getIntegrationPath(appName);
329
329
  }
330
- return path.join(getBuilderRoot(), appName);
330
+ return getBuilderPath(appName);
331
331
  }
332
332
 
333
333
  /**
@@ -342,7 +342,7 @@ function getIntegrationBuilderBaseDir() {
342
342
  const workNorm = path.resolve(work);
343
343
  const integrationUnderWork = path.join(workNorm, 'integration');
344
344
  try {
345
- if (fs.existsSync(integrationUnderWork)) {
345
+ if (nodeFs().existsSync(integrationUnderWork)) {
346
346
  return workNorm;
347
347
  }
348
348
  } catch {
@@ -380,6 +380,60 @@ function getBuilderRoot() {
380
380
  return path.join(getIntegrationBuilderBaseDir(), 'builder');
381
381
  }
382
382
 
383
+ /**
384
+ * Platform system apps: project `builder/<app>` when present; else `builder/<app>` under the
385
+ * resolved system-builder parent (config dir, or `aifabrix-home` when outside config — see
386
+ * {@link getSystemBuilderRoot}).
387
+ * @readonly
388
+ */
389
+ const SYSTEM_BUILDER_APP_KEYS = Object.freeze(['keycloak', 'miso-controller', 'dataplane']);
390
+
391
+ /**
392
+ * @param {string} appName
393
+ * @returns {boolean}
394
+ */
395
+ function isSystemBuilderAppName(appName) {
396
+ if (!appName || typeof appName !== 'string') return false;
397
+ return SYSTEM_BUILDER_APP_KEYS.includes(appName);
398
+ }
399
+
400
+ /**
401
+ * Project/cwd `builder/<appName>` (no existence check).
402
+ * @param {string} appName
403
+ * @returns {string}
404
+ */
405
+ function getProjectBuilderAppPath(appName) {
406
+ return path.join(getIntegrationBuilderBaseDir(), 'builder', appName);
407
+ }
408
+
409
+ /**
410
+ * Directory containing default materialization for platform apps: `<parent>/builder/<app>`.
411
+ * Parent is {@link getAifabrixSystemDir} when that path lies under {@link getAifabrixHome};
412
+ * otherwise {@link getAifabrixHome} (honours `aifabrix-home` / `AIFABRIX_HOME` vs config location).
413
+ *
414
+ * @returns {string}
415
+ */
416
+ function getSystemBuilderRoot() {
417
+ return path.join(
418
+ resolveSystemBuilderParentDir(getAifabrixSystemDir(), getAifabrixHome()),
419
+ 'builder'
420
+ );
421
+ }
422
+
423
+ /**
424
+ * @param {string} projectAppPath - Absolute `.../builder/<app>`
425
+ * @returns {boolean}
426
+ */
427
+ function isProjectBuilderAppDirectory(projectAppPath) {
428
+ try {
429
+ if (!projectAppPath || !nodeFs().existsSync(projectAppPath)) return false;
430
+ const st = nodeFs().statSync(projectAppPath);
431
+ return Boolean(st && typeof st.isDirectory === 'function' && st.isDirectory());
432
+ } catch {
433
+ return false;
434
+ }
435
+ }
436
+
383
437
  /**
384
438
  * True when name under root is a real directory (follows symlinks). False for broken symlinks, files, ENOENT.
385
439
  * @param {string} root - Parent directory (must exist)
@@ -422,27 +476,44 @@ function listIntegrationAppNames() {
422
476
  }
423
477
 
424
478
  /**
425
- * Lists app names (directories) under builder root. Excludes dot-prefixed entries.
426
- * Returns [] if root does not exist.
427
- * @returns {string[]} Sorted list of app directory names
479
+ * Adds immediate subdirectory names under a builder root into `names` (real disk via {@link nodeFs}).
480
+ * @param {ReturnType<typeof nodeFs>} disk
481
+ * @param {string} builderRootDir
482
+ * @param {Set<string>} names
483
+ * @param {(name: string) => boolean} [nameFilter] - When set, only names passing this filter (after dir check)
428
484
  */
429
- function listBuilderAppNames() {
430
- const disk = nodeFs();
431
- const root = getBuilderRoot();
432
- if (!disk.existsSync(root)) {
433
- return [];
434
- }
435
- let rootStat;
485
+ function addBuilderSubdirNamesToSet(disk, builderRootDir, names, nameFilter) {
486
+ if (!builderRootDir || !disk.existsSync(builderRootDir)) return;
487
+ let st;
436
488
  try {
437
- rootStat = disk.statSync(root);
489
+ st = disk.statSync(builderRootDir);
438
490
  } catch {
439
- return [];
491
+ return;
440
492
  }
441
- if (!rootStat || typeof rootStat.isDirectory !== 'function' || !rootStat.isDirectory()) {
442
- return [];
493
+ if (!st || typeof st.isDirectory !== 'function' || !st.isDirectory()) return;
494
+ try {
495
+ for (const name of disk.readdirSync(builderRootDir)) {
496
+ if (!isAppSubdirSync(builderRootDir, name)) continue;
497
+ if (nameFilter && !nameFilter(name)) continue;
498
+ names.add(name);
499
+ }
500
+ } catch {
501
+ // ignore
443
502
  }
444
- const entries = disk.readdirSync(root);
445
- return entries.filter((name) => isAppSubdirSync(root, name)).sort();
503
+ }
504
+
505
+ /**
506
+ * Lists app names (directories) under builder root. Excludes dot-prefixed entries.
507
+ * Merges project `builder/` with system builder root (platform keys only under the latter).
508
+ * @returns {string[]} Sorted list of app directory names
509
+ */
510
+ function listBuilderAppNames() {
511
+ const disk = nodeFs();
512
+ const names = new Set();
513
+ const projectBuilder = path.join(getIntegrationBuilderBaseDir(), 'builder');
514
+ addBuilderSubdirNamesToSet(disk, projectBuilder, names, null);
515
+ addBuilderSubdirNamesToSet(disk, getSystemBuilderRoot(), names, isSystemBuilderAppName);
516
+ return [...names].sort();
446
517
  }
447
518
 
448
519
  /**
@@ -481,11 +552,18 @@ function getBuilderPath(appName) {
481
552
  if (!appName || typeof appName !== 'string') {
482
553
  throw new Error('App name is required and must be a string');
483
554
  }
484
- const builderRoot = process.env.AIFABRIX_BUILDER_DIR && typeof process.env.AIFABRIX_BUILDER_DIR === 'string'
555
+ const envBuilderRoot = process.env.AIFABRIX_BUILDER_DIR && typeof process.env.AIFABRIX_BUILDER_DIR === 'string'
485
556
  ? process.env.AIFABRIX_BUILDER_DIR.trim()
486
557
  : null;
487
- if (builderRoot) {
488
- return path.join(path.resolve(builderRoot), appName);
558
+ if (envBuilderRoot) {
559
+ return path.join(path.resolve(envBuilderRoot), appName);
560
+ }
561
+ if (isSystemBuilderAppName(appName)) {
562
+ const projectAppPath = getProjectBuilderAppPath(appName);
563
+ if (isProjectBuilderAppDirectory(projectAppPath)) {
564
+ return projectAppPath;
565
+ }
566
+ return path.join(getSystemBuilderRoot(), appName);
489
567
  }
490
568
  const base = getIntegrationBuilderBaseDir();
491
569
  return path.join(base, 'builder', appName);
@@ -665,6 +743,11 @@ module.exports = {
665
743
  getBuilderPath,
666
744
  getIntegrationRoot,
667
745
  getBuilderRoot,
746
+ SYSTEM_BUILDER_APP_KEYS,
747
+ isSystemBuilderAppName,
748
+ getProjectBuilderAppPath,
749
+ getSystemBuilderRoot,
750
+ resolveSystemBuilderParentDir,
668
751
  listIntegrationAppNames,
669
752
  listBuilderAppNames,
670
753
  resolveBuildContext,
@@ -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.
@@ -63,7 +67,42 @@ function mergeUserWithRemoteSecrets(userSecrets, remoteSecrets) {
63
67
  return merged;
64
68
  }
65
69
 
70
+ /**
71
+ * Raw secrets from the configured shared store only (`aifabrix-secrets`): remote Builder API or shared YAML file.
72
+ * Does not include primary user secrets or builder merges. Used to avoid duplicating shared keys into ~/.aifabrix.
73
+ *
74
+ * @returns {Promise<Object|null>} Key-value map or null when unavailable / not configured
75
+ */
76
+ async function loadConfiguredSharedSecretsStore() {
77
+ const remoteDevAuth = require('./remote-dev-auth');
78
+ const configSecretsPath = await config.getSecretsPath();
79
+ if (!configSecretsPath) {
80
+ return null;
81
+ }
82
+ const endpoint = await remoteDevAuth.resolveSharedSecretsEndpoint(configSecretsPath);
83
+ if (remoteDevAuth.isRemoteSecretsUrl(endpoint)) {
84
+ return loadRemoteSharedSecrets();
85
+ }
86
+ const resolvedFile = path.isAbsolute(endpoint)
87
+ ? endpoint
88
+ : path.resolve(process.cwd(), endpoint);
89
+ if (!fs.existsSync(resolvedFile)) {
90
+ return null;
91
+ }
92
+ try {
93
+ ensureSecureFilePermissions(resolvedFile);
94
+ const data = readYamlAtPath(resolvedFile);
95
+ if (!data || typeof data !== 'object') {
96
+ return null;
97
+ }
98
+ return data;
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+
66
104
  module.exports = {
67
105
  loadRemoteSharedSecrets,
68
- mergeUserWithRemoteSecrets
106
+ mergeUserWithRemoteSecrets,
107
+ loadConfiguredSharedSecretsStore
69
108
  };
@@ -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,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
+ };