@aifabrix/builder 2.44.6 → 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 (86) hide show
  1. package/.cursor/rules/cli-layout.mdc +7 -3
  2. package/jest.projects.js +56 -0
  3. package/lib/app/helpers.js +3 -3
  4. package/lib/app/index.js +3 -3
  5. package/lib/app/register.js +7 -6
  6. package/lib/app/restart-display.js +52 -21
  7. package/lib/app/rotate-secret.js +7 -6
  8. package/lib/app/run-helpers.js +15 -8
  9. package/lib/app/run.js +57 -9
  10. package/lib/app/show-display.js +7 -0
  11. package/lib/app/show.js +87 -5
  12. package/lib/build/index.js +9 -5
  13. package/lib/cli/infra-guided.js +42 -27
  14. package/lib/cli/installation-log-command.js +73 -0
  15. package/lib/cli/setup-app.js +11 -1
  16. package/lib/cli/setup-auth.js +94 -49
  17. package/lib/cli/setup-infra-up-dataplane-action.js +111 -0
  18. package/lib/cli/setup-infra-up-platform-action.js +131 -0
  19. package/lib/cli/setup-infra.js +60 -119
  20. package/lib/cli/setup-platform.js +1 -1
  21. package/lib/cli/setup-utility-resolve.js +132 -0
  22. package/lib/cli/setup-utility.js +65 -51
  23. package/lib/commands/app-logs.js +81 -33
  24. package/lib/commands/auth-config.js +116 -18
  25. package/lib/commands/setup-modes.js +19 -6
  26. package/lib/commands/setup-prompts.js +41 -8
  27. package/lib/commands/setup.js +114 -9
  28. package/lib/commands/teardown.js +54 -5
  29. package/lib/commands/up-common.js +48 -14
  30. package/lib/commands/up-dataplane.js +21 -18
  31. package/lib/commands/up-miso.js +12 -8
  32. package/lib/commands/upload.js +5 -3
  33. package/lib/core/audit-logger.js +1 -34
  34. package/lib/core/config-admin-email.js +56 -0
  35. package/lib/core/config-normalize.js +60 -0
  36. package/lib/core/config-registered-controller-urls.js +54 -0
  37. package/lib/core/config.js +33 -50
  38. package/lib/core/secrets-ensure-infra.js +1 -1
  39. package/lib/core/secrets-env-content.js +86 -90
  40. package/lib/core/secrets-env-declarative-expand.js +170 -0
  41. package/lib/core/secrets-env-write.js +2 -0
  42. package/lib/core/secrets-load.js +106 -102
  43. package/lib/external-system/deploy.js +5 -1
  44. package/lib/internal/node-fs.js +2 -0
  45. package/lib/schema/application-schema.json +4 -0
  46. package/lib/schema/infra.parameter.yaml +10 -0
  47. package/lib/utils/app-config-resolver.js +24 -1
  48. package/lib/utils/applications-config-defaults.js +206 -0
  49. package/lib/utils/auth-config-validator.js +2 -12
  50. package/lib/utils/bash-secret-env.js +1 -1
  51. package/lib/utils/compose-generate-docker-compose.js +111 -6
  52. package/lib/utils/compose-generator.js +17 -8
  53. package/lib/utils/controller-url.js +50 -7
  54. package/lib/utils/env-copy.js +99 -14
  55. package/lib/utils/env-template.js +5 -1
  56. package/lib/utils/health-check-url.js +18 -15
  57. package/lib/utils/health-check.js +7 -5
  58. package/lib/utils/infra-optional-service-flags.js +69 -0
  59. package/lib/utils/installation-log-core.js +282 -0
  60. package/lib/utils/installation-log-record.js +237 -0
  61. package/lib/utils/installation-log.js +123 -0
  62. package/lib/utils/log-redaction.js +105 -0
  63. package/lib/utils/manifest-location.js +164 -0
  64. package/lib/utils/manifest-source-emit.js +162 -0
  65. package/lib/utils/paths.js +238 -89
  66. package/lib/utils/remote-secrets-loader.js +7 -1
  67. package/lib/utils/run-cli-flags.js +29 -0
  68. package/lib/utils/secrets-canonical.js +10 -3
  69. package/lib/utils/secrets-path.js +3 -4
  70. package/lib/utils/secrets-utils.js +20 -10
  71. package/lib/utils/system-builder-root.js +10 -2
  72. package/lib/utils/url-declarative-public-base.js +80 -12
  73. package/lib/utils/url-declarative-resolve-build-urls.js +238 -0
  74. package/lib/utils/url-declarative-resolve-build.js +24 -393
  75. package/lib/utils/url-declarative-resolve-expand-token.js +189 -0
  76. package/lib/utils/url-declarative-resolve-load-doc.js +12 -3
  77. package/lib/utils/url-declarative-resolve-surface-state.js +102 -0
  78. package/lib/utils/url-declarative-resolve.js +47 -7
  79. package/lib/utils/url-declarative-runtime-base-path.js +21 -1
  80. package/lib/utils/urls-local-registry-scan.js +103 -0
  81. package/lib/utils/urls-local-registry.js +161 -90
  82. package/package.json +3 -1
  83. package/templates/applications/dataplane/application.yaml +4 -0
  84. package/templates/applications/miso-controller/application.yaml +2 -0
  85. package/templates/applications/miso-controller/env.template +27 -29
  86. package/.npmrc.token +0 -1
@@ -29,6 +29,7 @@ const {
29
29
 
30
30
  const setupPrompts = require('./setup-prompts');
31
31
  const setupModes = require('./setup-modes');
32
+ const installationLog = require('../utils/installation-log');
32
33
 
33
34
  const MODE = setupPrompts.MODE;
34
35
 
@@ -57,19 +58,25 @@ async function runFreshInstallFlow(options) {
57
58
  await maybePinDeveloperIdForFreshInstall(options.developer);
58
59
  logger.log(infoLine('No infrastructure detected; starting fresh install.'));
59
60
  const adminCreds = await setupPrompts.promptAdminCredentials();
61
+ await config.setAdminEmail(adminCreds.adminEmail);
60
62
  await setupModes.runFreshInstall(adminCreds);
61
63
  logger.log(formatSuccessParagraph('aifabrix setup complete (fresh install).'));
62
64
  }
63
65
 
66
+ /**
67
+ * @param {boolean} assumeYes
68
+ * @returns {Promise<{ aborted: boolean, mode: string|null }>}
69
+ */
64
70
  async function runExistingInfraFlow(assumeYes) {
65
71
  const mode = await setupPrompts.promptModeSelection();
66
72
  const proceed = await setupPrompts.confirmDestructiveMode(mode, assumeYes);
67
73
  if (!proceed) {
68
74
  logger.log(chalk.yellow('Aborted by user.'));
69
- return;
75
+ return { aborted: true, mode };
70
76
  }
71
77
  await dispatchMode(mode);
72
78
  logger.log(formatSuccessParagraph(`aifabrix setup complete (mode: ${mode}).`));
79
+ return { aborted: false, mode };
73
80
  }
74
81
 
75
82
  /**
@@ -116,6 +123,89 @@ async function dispatchMode(mode) {
116
123
  }
117
124
  }
118
125
 
126
+ /**
127
+ * @param {Object} options
128
+ * @param {boolean} assumeYes
129
+ * @returns {Promise<{ outcome: string, setupMode: string|null }>}
130
+ */
131
+ async function runSetupMainPath(options, assumeYes) {
132
+ logSetupHeader();
133
+ logger.log(formatProgress('Detecting current installation state...'));
134
+ const running = await isInfraRunning();
135
+ if (!running) {
136
+ await runFreshInstallFlow(options);
137
+ return { outcome: 'success', setupMode: 'fresh' };
138
+ }
139
+ const r = await runExistingInfraFlow(assumeYes);
140
+ const outcome = r.aborted ? 'aborted' : 'success';
141
+ return { outcome, setupMode: r.mode };
142
+ }
143
+
144
+ async function readConfigSnapshotForSetupLog() {
145
+ try {
146
+ return await config.getConfig();
147
+ } catch {
148
+ return {};
149
+ }
150
+ }
151
+
152
+ function tryCollectPlatformImagesForLog(outcome) {
153
+ if (outcome !== 'success') return undefined;
154
+ try {
155
+ return installationLog.collectPlatformAppImages(['keycloak', 'miso-controller', 'dataplane'], {});
156
+ } catch {
157
+ return undefined;
158
+ }
159
+ }
160
+
161
+ async function readSetupLogUrlFields() {
162
+ let controllerUrl;
163
+ try {
164
+ controllerUrl = await installationLog.resolveControllerUrlForLog();
165
+ } catch {
166
+ controllerUrl = undefined;
167
+ }
168
+ let adminEmail;
169
+ try {
170
+ adminEmail = await installationLog.resolveAdminEmailPresence();
171
+ } catch {
172
+ adminEmail = 'unset';
173
+ }
174
+ return { controllerUrl, adminEmail };
175
+ }
176
+
177
+ /**
178
+ * @param {Object} payload
179
+ * @param {string} payload.outcome
180
+ * @param {Date} payload.startedAt
181
+ * @param {Date} payload.completedAt
182
+ * @param {Object} payload.options
183
+ * @param {string|null} payload.setupMode
184
+ * @param {Error|null} payload.err
185
+ */
186
+ async function appendSetupInstallationRecord(payload) {
187
+ const cfg = await readConfigSnapshotForSetupLog();
188
+ const platformApps = tryCollectPlatformImagesForLog(payload.outcome);
189
+ const { controllerUrl, adminEmail } = await readSetupLogUrlFields();
190
+ try {
191
+ await installationLog.appendInstallationRecord({
192
+ command: 'setup',
193
+ outcome: payload.outcome,
194
+ startedAt: payload.startedAt,
195
+ completedAt: payload.completedAt,
196
+ options: payload.options,
197
+ setupMode: payload.setupMode || undefined,
198
+ infra: cfg && typeof cfg === 'object' ? { cfg, options: {} } : undefined,
199
+ platformApps,
200
+ configExtra: { controllerUrl, adminEmail },
201
+ error: payload.err || undefined,
202
+ errorCode: payload.err && payload.err.code ? String(payload.err.code) : undefined
203
+ });
204
+ } catch {
205
+ // never block setup on log failure
206
+ }
207
+ }
208
+
119
209
  /**
120
210
  * Run the setup wizard / dispatcher.
121
211
  *
@@ -129,17 +219,32 @@ async function dispatchMode(mode) {
129
219
  */
130
220
  async function handleSetup(options = {}) {
131
221
  const assumeYes = options.yes === true || options.assumeYes === true;
222
+ const startedAt = new Date();
223
+ let outcome = 'success';
224
+ let setupMode = null;
225
+ let err = null;
132
226
 
133
- logSetupHeader();
134
-
135
- logger.log(formatProgress('Detecting current installation state...'));
136
- const running = await isInfraRunning();
227
+ try {
228
+ const main = await runSetupMainPath(options, assumeYes);
229
+ outcome = main.outcome;
230
+ setupMode = main.setupMode;
231
+ } catch (e) {
232
+ outcome = 'failure';
233
+ err = e;
234
+ } finally {
235
+ await appendSetupInstallationRecord({
236
+ outcome,
237
+ startedAt,
238
+ completedAt: new Date(),
239
+ options,
240
+ setupMode,
241
+ err
242
+ });
243
+ }
137
244
 
138
- if (!running) {
139
- await runFreshInstallFlow(options);
140
- return;
245
+ if (err) {
246
+ throw err;
141
247
  }
142
- await runExistingInfraFlow(assumeYes);
143
248
  }
144
249
 
145
250
  module.exports = {
@@ -24,6 +24,7 @@ const chalk = require('chalk');
24
24
  const ora = require('ora');
25
25
 
26
26
  const config = require('../core/config');
27
+ const installationLog = require('../utils/installation-log');
27
28
  const infra = require('../infrastructure');
28
29
  const pathsUtil = require('../utils/paths');
29
30
  const logger = require('../utils/logger');
@@ -193,6 +194,47 @@ function cleanFilesWithSpinner() {
193
194
  return result;
194
195
  }
195
196
 
197
+ async function appendTeardownAbortedLog(options) {
198
+ const t = new Date();
199
+ try {
200
+ await installationLog.appendInstallationRecord({
201
+ command: 'teardown',
202
+ outcome: 'aborted',
203
+ startedAt: t,
204
+ completedAt: t,
205
+ options
206
+ });
207
+ } catch {
208
+ // ignore
209
+ }
210
+ }
211
+
212
+ async function appendTeardownSuccessLog(options, startedAt) {
213
+ let cfg = {};
214
+ try {
215
+ cfg = await config.getConfig();
216
+ } catch {
217
+ cfg = {};
218
+ }
219
+ try {
220
+ await installationLog.appendInstallationRecord({
221
+ command: 'teardown',
222
+ outcome: 'success',
223
+ startedAt,
224
+ completedAt: new Date(),
225
+ options,
226
+ infra: { cfg, options: {} },
227
+ cleanup: { volumesRemoved: true, configPreserved: true },
228
+ configExtra: {
229
+ controllerUrl: await installationLog.resolveControllerUrlForLog(),
230
+ adminEmail: await installationLog.resolveAdminEmailPresence()
231
+ }
232
+ });
233
+ } catch {
234
+ // ignore
235
+ }
236
+ }
237
+
196
238
  /**
197
239
  * Run the teardown.
198
240
  *
@@ -208,15 +250,22 @@ async function handleTeardown(options = {}) {
208
250
 
209
251
  const ok = await confirmTeardown(assumeYes);
210
252
  if (!ok) {
253
+ await appendTeardownAbortedLog(options);
211
254
  logger.log(chalk.yellow('Aborted by user.'));
212
255
  return;
213
256
  }
214
- await stopInfraQuietly();
215
- const { removed, failed } = cleanFilesWithSpinner();
216
- const devStr = await buildDeveloperLabel();
217
- logTeardownFooter({ devStr, removedCount: removed.length, failedCount: failed.length });
218
257
 
219
- logger.log(formatSuccessParagraph('aifabrix teardown complete.'));
258
+ const startedAt = new Date();
259
+ try {
260
+ await stopInfraQuietly();
261
+ const { removed, failed } = cleanFilesWithSpinner();
262
+ const devStr = await buildDeveloperLabel();
263
+ logTeardownFooter({ devStr, removedCount: removed.length, failedCount: failed.length });
264
+
265
+ logger.log(formatSuccessParagraph('aifabrix teardown complete.'));
266
+ } finally {
267
+ await appendTeardownSuccessLog(options, startedAt);
268
+ }
220
269
  }
221
270
 
222
271
  module.exports = {
@@ -22,6 +22,23 @@ const { copyTemplateFiles } = require('../validation/template');
22
22
  const { ensureReadmeForAppPath, ensureReadmeForApp } = require('../app/readme');
23
23
  const { refreshUrlsLocalRegistryFromBuilder } = require('../utils/urls-local-registry');
24
24
 
25
+ /**
26
+ * @param {string[]} entries
27
+ * @returns {string[]}
28
+ */
29
+ function uniqueResolvedRoots(entries) {
30
+ const seen = new Set();
31
+ const out = [];
32
+ for (const e of entries) {
33
+ const r = path.resolve(e);
34
+ if (!seen.has(r)) {
35
+ seen.add(r);
36
+ out.push(r);
37
+ }
38
+ }
39
+ return out;
40
+ }
41
+
25
42
  /**
26
43
  * Copy template to a target path if application config is missing there.
27
44
  * After copy, generates README.md from templates/applications/README.md.hbs.
@@ -226,7 +243,11 @@ function isUnderAllowedBuilderRoot(resolvedAppPath, allowedRoots) {
226
243
  async function cleanBuilderAppDirs(appNames, opts = {}) {
227
244
  const silent = Boolean(opts.silent);
228
245
  if (!Array.isArray(appNames) || appNames.length === 0) return;
229
- const allowedRoots = [path.resolve(pathsUtil.getBuilderRoot()), path.resolve(pathsUtil.getSystemBuilderRoot())];
246
+ const allowedRoots = uniqueResolvedRoots([
247
+ path.join(process.cwd(), 'builder'),
248
+ pathsUtil.getBuilderRoot(),
249
+ pathsUtil.getSystemBuilderRoot()
250
+ ]);
230
251
  const cleaned = [];
231
252
  for (const appName of appNames) {
232
253
  if (!appName || typeof appName !== 'string') continue;
@@ -253,8 +274,9 @@ async function cleanBuilderAppDirs(appNames, opts = {}) {
253
274
  * Uses AIFABRIX_BUILDER_DIR when set (e.g. by up-miso/up-dataplane from config aifabrix-env-config).
254
275
  * When `process.env.AIFABRIX_BUILDER_DIR` is set and the primary app path differs from
255
276
  * `cwd/builder/<appName>`, also copies into `cwd/builder/<appName>` so the repo tree is not empty
256
- * while using a custom builder root. Skips that extra copy when no custom dir is in use (e.g.
257
- * platform apps materialized only under the system builder root).
277
+ * while using a custom builder root. Skips that extra copy for platform apps (`keycloak`,
278
+ * `miso-controller`, `dataplane`) so they materialize only under the system builder root, and when
279
+ * no custom dir is in use.
258
280
  *
259
281
  * @async
260
282
  * @function ensureAppFromTemplate
@@ -281,7 +303,8 @@ async function ensureAppFromTemplate(appName) {
281
303
  const cwdBuilderPath = path.join(process.cwd(), 'builder', appName);
282
304
  if (
283
305
  envBuilderRoot &&
284
- path.resolve(cwdBuilderPath) !== path.resolve(appPath)
306
+ path.resolve(cwdBuilderPath) !== path.resolve(appPath) &&
307
+ !pathsUtil.isSystemBuilderAppName(appName)
285
308
  ) {
286
309
  const cwdCopied = await ensureTemplateAtPath(appName, cwdBuilderPath);
287
310
  if (cwdCopied) {
@@ -295,22 +318,32 @@ async function ensureAppFromTemplate(appName) {
295
318
  }
296
319
 
297
320
  /**
298
- * For `aifabrix up-platform` only: align builder dir env with up-miso/up-dataplane, materialize all three
299
- * platform apps from templates if missing, then refresh `~/.aifabrix/urls.local.yaml` so declarative
300
- * `url://` expansion (e.g. cross-references between miso-controller, dataplane, keycloak) sees every
301
- * app's port and pattern before Keycloak or Miso resolve their `.env` files.
321
+ * Merge builder/packages `application.yaml` into `~/.aifabrix/urls.local.yaml`.
322
+ * Same scan as the end of {@link prepareUrlsLocalRegistryForUpPlatform}; call after platform app
323
+ * templates exist (e.g. `up-miso`, `up-dataplane`) so declarative `url://` and tooling see ports/patterns.
324
+ */
325
+ function refreshUrlsLocalRegistryForCurrentProject() {
326
+ try {
327
+ refreshUrlsLocalRegistryFromBuilder(pathsUtil.getProjectRoot());
328
+ } catch (error) {
329
+ logger.warn(chalk.yellow(`⚠ Could not refresh URLs registry: ${error.message}`));
330
+ }
331
+ }
332
+
333
+ /**
334
+ * For `aifabrix up-platform` only: materialize all three platform apps from templates if missing
335
+ * (paths follow cwd / material `builder/` only — no `AIFABRIX_BUILDER_DIR` override), then refresh
336
+ * `~/.aifabrix/urls.local.yaml` so declarative `url://` expansion (e.g. cross-references between
337
+ * miso-controller, dataplane, keycloak) sees every app's port and pattern before Keycloak or Miso
338
+ * resolve their `.env` files.
302
339
  *
303
340
  * @returns {Promise<void>}
304
341
  */
305
342
  async function prepareUrlsLocalRegistryForUpPlatform() {
306
- const builderDir = await config.getAifabrixBuilderDir();
307
- if (builderDir) {
308
- process.env.AIFABRIX_BUILDER_DIR = path.resolve(builderDir);
309
- }
310
343
  await ensureAppFromTemplate('keycloak');
311
344
  await ensureAppFromTemplate('miso-controller');
312
345
  await ensureAppFromTemplate('dataplane');
313
- refreshUrlsLocalRegistryFromBuilder(pathsUtil.getProjectRoot());
346
+ refreshUrlsLocalRegistryForCurrentProject();
314
347
  }
315
348
 
316
349
  module.exports = {
@@ -320,5 +353,6 @@ module.exports = {
320
353
  patchEnvOutputPathForDeployOnly,
321
354
  validateEnvOutputPathFolderOrNull,
322
355
  getEnvOutputPathFolder,
323
- prepareUrlsLocalRegistryForUpPlatform
356
+ prepareUrlsLocalRegistryForUpPlatform,
357
+ refreshUrlsLocalRegistryForCurrentProject
324
358
  };
@@ -12,7 +12,6 @@ const { formatSuccessLine, formatSuccessParagraph } = require('../utils/cli-test
12
12
  * @version 2.0.0
13
13
  */
14
14
 
15
- const path = require('path');
16
15
  const readline = require('readline');
17
16
  const chalk = require('chalk');
18
17
  const pathsUtil = require('../utils/paths');
@@ -30,7 +29,7 @@ const { checkHealthEndpoint } = require('../utils/health-check');
30
29
  const { validateControllerUrl } = require('../utils/auth-config-validator');
31
30
  const app = require('../app');
32
31
  const { assertDevInfraUp } = require('./dev-infra-gate');
33
- const { ensureAppFromTemplate, validateEnvOutputPathFolderOrNull } = require('./up-common');
32
+ const { ensureAppFromTemplate, validateEnvOutputPathFolderOrNull, refreshUrlsLocalRegistryForCurrentProject } = require('./up-common');
34
33
 
35
34
  const CONTROLLER_HEALTH_PATH = '/health';
36
35
 
@@ -168,14 +167,17 @@ function buildDataplaneImageRef(options = {}) {
168
167
  }
169
168
  }
170
169
 
171
- /**
172
- * Sets `AIFABRIX_BUILDER_DIR` when configured (shared by deploy/run paths).
173
- * @returns {Promise<void>}
174
- */
175
- async function applyAifabrixBuilderDirEnv() {
176
- const builderDir = await config.getAifabrixBuilderDir();
177
- if (builderDir) {
178
- process.env.AIFABRIX_BUILDER_DIR = path.resolve(builderDir);
170
+ function emitDataplaneManifestLineFromBuilder() {
171
+ const { emitSystemBuilderAppManifestLineIfTTY } = require('../utils/manifest-source-emit');
172
+ emitSystemBuilderAppManifestLineIfTTY(logger, 'dataplane');
173
+ }
174
+
175
+ function assertDevEnvironmentForDataplane(cfg) {
176
+ const environment = (cfg && cfg.environment) ? cfg.environment : 'dev';
177
+ if (environment !== 'dev') {
178
+ throw new Error(
179
+ 'Dataplane is only supported in dev environment. Set with: aifabrix auth --set-environment dev.'
180
+ );
179
181
  }
180
182
  }
181
183
 
@@ -195,7 +197,6 @@ async function applyAifabrixBuilderDirEnv() {
195
197
  * @throws {Error} If infra not up, controller unavailable, not logged in, environment not dev, or any step fails
196
198
  */
197
199
  async function handleUpDataplane(options = {}) {
198
- await applyAifabrixBuilderDirEnv();
199
200
  logger.log(chalk.blue('Starting up-dataplane (register/rotate, deploy, then run dataplane locally)...\n'));
200
201
 
201
202
  if (options.skipInfraCheck !== true) {
@@ -207,18 +208,19 @@ async function handleUpDataplane(options = {}) {
207
208
  const authConfig = await checkAuthentication(controllerUrl, environmentKey, { throwOnFailure: true });
208
209
 
209
210
  const cfg = await config.getConfig();
210
- const environment = (cfg && cfg.environment) ? cfg.environment : 'dev';
211
- if (environment !== 'dev') {
212
- throw new Error(
213
- 'Dataplane is only supported in dev environment. Set with: aifabrix auth --set-environment dev.'
214
- );
215
- }
211
+ assertDevEnvironmentForDataplane(cfg);
216
212
  logger.log(formatSuccessLine('Logged in and environment is dev'));
217
213
 
218
214
  await ensureAppFromTemplate('dataplane');
219
215
  // If envOutputPath target folder does not exist, set envOutputPath to null
220
216
  validateEnvOutputPathFolderOrNull('dataplane');
221
217
 
218
+ refreshUrlsLocalRegistryForCurrentProject();
219
+
220
+ if (options.platformInstall !== true) {
221
+ emitDataplaneManifestLineFromBuilder();
222
+ }
223
+
222
224
  await registerOrRotateDataplane(options, controllerUrl, environmentKey, authConfig);
223
225
 
224
226
  await deployDataplaneToController(options);
@@ -226,7 +228,8 @@ async function handleUpDataplane(options = {}) {
226
228
  await app.runApp('dataplane', {
227
229
  skipEnvOutputPath: true,
228
230
  registry: options.registry || undefined,
229
- base: options.base !== false
231
+ base: options.base !== false,
232
+ skipManifestMetadataLine: true
230
233
  });
231
234
 
232
235
  logger.log(formatSuccessParagraph('up-dataplane complete. Dataplane is registered, deployed in dev, and running locally.'));
@@ -10,13 +10,16 @@ const { formatSuccessParagraph } = require('../utils/cli-test-layout-chalk');
10
10
  * @version 2.0.0
11
11
  */
12
12
 
13
- const path = require('path');
14
13
  const chalk = require('chalk');
15
14
  const logger = require('../utils/logger');
16
- const config = require('../core/config');
17
15
  const app = require('../app');
18
16
  const { assertDevInfraUp } = require('./dev-infra-gate');
19
- const { ensureAppFromTemplate, patchEnvOutputPathForDeployOnly, validateEnvOutputPathFolderOrNull } = require('./up-common');
17
+ const {
18
+ ensureAppFromTemplate,
19
+ patchEnvOutputPathForDeployOnly,
20
+ validateEnvOutputPathFolderOrNull,
21
+ refreshUrlsLocalRegistryForCurrentProject
22
+ } = require('./up-common');
20
23
 
21
24
  /**
22
25
  * Parse --image options array into map { keycloak?: string, 'miso-controller'?: string }
@@ -51,7 +54,8 @@ async function runMisoApps(options) {
51
54
  registryMode: options.registryMode,
52
55
  skipEnvOutputPath: true,
53
56
  skipInfraCheck: true,
54
- base: useBaseImage
57
+ base: useBaseImage,
58
+ skipManifestMetadataLine: true
55
59
  };
56
60
  const keycloakRunOpts = { ...common };
57
61
  if (imageMap.keycloak) {
@@ -61,9 +65,12 @@ async function runMisoApps(options) {
61
65
  if (imageMap['miso-controller']) {
62
66
  misoRunOpts.image = imageMap['miso-controller'];
63
67
  }
68
+ const { emitSystemBuilderAppManifestLineIfTTY } = require('../utils/manifest-source-emit');
64
69
  logger.log(chalk.blue('Starting keycloak...'));
70
+ emitSystemBuilderAppManifestLineIfTTY(logger, 'keycloak');
65
71
  await app.runApp('keycloak', keycloakRunOpts);
66
72
  logger.log(chalk.blue('Starting miso-controller...'));
73
+ emitSystemBuilderAppManifestLineIfTTY(logger, 'miso-controller');
67
74
  await app.runApp('miso-controller', misoRunOpts);
68
75
  }
69
76
 
@@ -80,10 +87,6 @@ async function runMisoApps(options) {
80
87
  * @throws {Error} If infra not up or any step fails
81
88
  */
82
89
  async function handleUpMiso(options = {}) {
83
- const builderDir = await config.getAifabrixBuilderDir();
84
- if (builderDir) {
85
- process.env.AIFABRIX_BUILDER_DIR = path.resolve(builderDir);
86
- }
87
90
  logger.log(chalk.blue('Starting up-miso (keycloak + miso-controller from images)...\n'));
88
91
  await assertDevInfraUp();
89
92
  await ensureAppFromTemplate('keycloak');
@@ -94,6 +97,7 @@ async function handleUpMiso(options = {}) {
94
97
  // Deploy-only: do not copy .env to repo paths; patch variables so envOutputPath is null
95
98
  patchEnvOutputPathForDeployOnly('keycloak');
96
99
  patchEnvOutputPathForDeployOnly('miso-controller');
100
+ refreshUrlsLocalRegistryForCurrentProject();
97
101
  await runMisoApps(options);
98
102
  logger.log(formatSuccessParagraph('up-miso complete. Keycloak and miso-controller are running.') +
99
103
  chalk.gray('\n Run onboarding and register Keycloak from the miso-controller repo if needed. Use \'aifabrix up-dataplane\' for dataplane.'));
@@ -279,8 +279,10 @@ async function buildValidatedUploadManifestPayload(systemKey, _opts = {}) {
279
279
  }
280
280
 
281
281
  /**
282
- * Upload local `integration/<systemKey>/openapi/*.json` specs when present so pipeline can resolve
283
- * each datasource `openapi.documentKey` (same behavior as repair --api OpenAPI sync).
282
+ * Upload local `integration/<systemKey>/openapi/*.json` specs when present so dataplane can store
283
+ * vendor OpenAPI keyed by each datasource `openapi.documentKey` (same behavior as repair --api OpenAPI sync).
284
+ * Call this **after** a successful pipeline publish: list/upload endpoints require the external system row.
285
+ * First publish still succeeds because pipeline materializes internal specs from `openapi.operations` when needed.
284
286
  *
285
287
  * @param {string} systemKey
286
288
  * @param {Object} manifest
@@ -374,9 +376,9 @@ async function runUploadPublishAndSummary(systemKey, options, manifest, payload)
374
376
  await maybeRunVerboseServerValidation(dataplaneUrl, authConfig, payload);
375
377
  }
376
378
  await pushAndLogCredentialSecrets(dataplaneUrl, authConfig, systemKey, payload);
377
- await logAndSyncLocalOpenApiForUpload(systemKey, manifest);
378
379
 
379
380
  const rawRes = await runUploadValidatePublish(dataplaneUrl, authConfig, payload);
381
+ await logAndSyncLocalOpenApiForUpload(systemKey, manifest);
380
382
  await handlePublicationAndFollowups({
381
383
  systemKey,
382
384
  options,
@@ -15,6 +15,7 @@ const fs = require('fs').promises;
15
15
  const path = require('path');
16
16
  const os = require('os');
17
17
  const paths = require('../utils/paths');
18
+ const { maskSensitiveData } = require('../utils/log-redaction');
18
19
 
19
20
  // Audit log file path (beside config.yaml / CLI system dir for compliance)
20
21
  let auditLogPath = null;
@@ -51,40 +52,6 @@ async function getAuditLogPath() {
51
52
  return auditLogPath;
52
53
  }
53
54
 
54
- /**
55
- * Masks sensitive data in strings
56
- * Prevents secrets, keys, and passwords from appearing in logs
57
- *
58
- * @param {string} value - Value to mask
59
- * @returns {string} Masked value
60
- */
61
- function maskSensitiveData(value) {
62
- if (!value || typeof value !== 'string') {
63
- return value;
64
- }
65
-
66
- // Mask patterns: passwords, secrets, keys, tokens
67
- const sensitivePatterns = [
68
- { pattern: /password[=:]\s*([^\s]+)/gi, replacement: 'password=***' },
69
- { pattern: /secret[=:]\s*([^\s]+)/gi, replacement: 'secret=***' },
70
- { pattern: /key[=:]\s*([^\s]+)/gi, replacement: 'key=***' },
71
- { pattern: /token[=:]\s*([^\s]+)/gi, replacement: 'token=***' },
72
- { pattern: /api[_-]?key[=:]\s*([^\s]+)/gi, replacement: 'api_key=***' }
73
- ];
74
-
75
- let masked = value;
76
- for (const { pattern, replacement } of sensitivePatterns) {
77
- masked = masked.replace(pattern, replacement);
78
- }
79
-
80
- // If value looks like a hash/key (long hex string), mask it
81
- if (/^[a-f0-9]{32,}$/i.test(masked.trim())) {
82
- return '***';
83
- }
84
-
85
- return masked;
86
- }
87
-
88
55
  /**
89
56
  * Creates an audit log entry with ISO 27001 compliance
90
57
  *
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Admin email field in `config.yaml` (Keycloak / pgAdmin), written by `aifabrix setup`.
3
+ *
4
+ * @fileoverview config.yaml adminEmail helpers (keeps config.js under max-lines)
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ /**
12
+ * @param {unknown} email
13
+ * @returns {string} Trimmed valid email
14
+ * @throws {Error} When empty or not a plausible email
15
+ */
16
+ function validateAdminEmailForConfig(email) {
17
+ const value = String(email ?? '').trim();
18
+ if (!value) {
19
+ throw new Error('Admin email must be a non-empty string');
20
+ }
21
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
22
+ throw new Error('Admin email must be a valid email address');
23
+ }
24
+ return value;
25
+ }
26
+
27
+ /**
28
+ * @param {() => Promise<Object>} getConfig
29
+ * @returns {Promise<string>}
30
+ */
31
+ async function getAdminEmailFromConfig(getConfig) {
32
+ const cfg = await getConfig();
33
+ if (cfg && typeof cfg.adminEmail === 'string' && cfg.adminEmail.trim()) {
34
+ return cfg.adminEmail.trim();
35
+ }
36
+ return '';
37
+ }
38
+
39
+ /**
40
+ * @param {() => Promise<Object>} getConfig
41
+ * @param {(data: Object) => Promise<void>} saveConfig
42
+ * @param {string} email
43
+ * @returns {Promise<void>}
44
+ */
45
+ async function setAdminEmailInConfig(getConfig, saveConfig, email) {
46
+ const trimmed = validateAdminEmailForConfig(email);
47
+ const cfg = await getConfig();
48
+ cfg.adminEmail = trimmed;
49
+ await saveConfig(cfg);
50
+ }
51
+
52
+ module.exports = {
53
+ validateAdminEmailForConfig,
54
+ getAdminEmailFromConfig,
55
+ setAdminEmailInConfig
56
+ };