@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
@@ -1,6 +1,14 @@
1
- const { formatSuccessLine } = require('../utils/cli-test-layout-chalk');
1
+ const {
2
+ formatSuccessLine,
3
+ formatWarningLine,
4
+ formatProgress,
5
+ sectionTitle,
6
+ headerKeyValue,
7
+ metadata
8
+ } = require('../utils/cli-test-layout-chalk');
2
9
  /**
3
- * Upload external system to dataplane (single pipeline upload: validate → publish → controller register).
10
+ * Upload external system to dataplane (sync local `integration/<systemKey>/openapi/*.json` when present,
11
+ * then single pipeline upload: validate → publish → controller register).
4
12
  *
5
13
  * @fileoverview Upload command handler for aifabrix upload <systemKey>
6
14
  * @author AI Fabrix Team
@@ -10,6 +18,8 @@ const { formatSuccessLine } = require('../utils/cli-test-layout-chalk');
10
18
  const path = require('path');
11
19
  const chalk = require('chalk');
12
20
  const logger = require('../utils/logger');
21
+
22
+ const SEP = chalk.gray('────────────────────────────────────────');
13
23
  const { resolveControllerUrl } = require('../utils/controller-url');
14
24
  const { getDeploymentAuth, requireBearerForDataplanePipeline } = require('../utils/token-manager');
15
25
  const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
@@ -38,6 +48,7 @@ const {
38
48
  } = require('../utils/external-system-readiness-display');
39
49
  const { maybeSyncSystemCertificationFromDataplane } = require('../certification/sync-system-certification');
40
50
  const { cliOptsSkipCertSync } = require('../certification/cli-cert-sync-skip');
51
+ const { maybeSyncOpenApiFilesForMcp } = require('./repair-openapi-sync');
41
52
 
42
53
  /**
43
54
  * Validates system-key format (same as download).
@@ -86,7 +97,7 @@ async function resolveDataplaneAndAuth(systemKey, opts = {}) {
86
97
  }
87
98
 
88
99
  if (!silent) {
89
- logger.log(chalk.gray('Resolving dataplane URL...'));
100
+ logger.log(metadata('Resolving dataplane URL...'));
90
101
  }
91
102
  const dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig, { silent });
92
103
  return { dataplaneUrl, authConfig, environment };
@@ -168,7 +179,9 @@ async function pushAndLogCredentialSecrets(dataplaneUrl, authConfig, systemKey,
168
179
  });
169
180
  if (pushResult.pushed > 0) {
170
181
  const keyList = pushResult.keys?.length ? ` (${pushResult.keys.join(', ')})` : '';
171
- logger.log(chalk.green(`Pushed ${pushResult.pushed} credential secret(s) to dataplane${keyList}.`));
182
+ logger.log(
183
+ formatSuccessLine(`Pushed ${pushResult.pushed} credential secret(s) to dataplane${keyList}.`)
184
+ );
172
185
  } else {
173
186
  logger.log(chalk.yellow('Secret push skipped'));
174
187
  }
@@ -223,7 +236,10 @@ const UPLOAD_PROBE_TEST_DATA = {};
223
236
  * @param {number|undefined} probeTimeoutMs
224
237
  */
225
238
  async function maybeRunUploadProbe(dataplaneUrl, systemKey, authConfig, probeTimeoutMs) {
226
- logger.log(chalk.blue('\nRunning runtime checks (--probe)...'));
239
+ logger.log('');
240
+ logger.log(sectionTitle('Runtime checks (--probe)'));
241
+ logger.log(SEP);
242
+ logger.log(formatProgress('Running runtime checks...'));
227
243
  const timeoutMs = probeTimeoutMs === undefined || probeTimeoutMs === null ? 120000 : probeTimeoutMs;
228
244
  try {
229
245
  const pr = await testSystemViaPipeline(
@@ -246,10 +262,13 @@ async function maybeRunUploadProbe(dataplaneUrl, systemKey, authConfig, probeTim
246
262
 
247
263
  /**
248
264
  * Local validation, manifest, payload, and configuration resolution.
265
+ * Does not write *-deploy.json; run `aifabrix json <systemKey>` to regenerate the deployment manifest from sources.
266
+ *
249
267
  * @param {string} systemKey
268
+ * @param {{ dryRun?: boolean }} [_opts] - Reserved for callers (e.g. dry-run); manifest is always built from current disk files
250
269
  * @returns {Promise<{ manifest: Object, payload: Object }>}
251
270
  */
252
- async function buildValidatedUploadManifestPayload(systemKey) {
271
+ async function buildValidatedUploadManifestPayload(systemKey, _opts = {}) {
253
272
  const validationResult = await validateExternalSystemComplete(systemKey, { type: 'external' });
254
273
  throwIfValidationFailed(validationResult);
255
274
  logger.log(formatSuccessLine('Local validation passed'));
@@ -260,24 +279,54 @@ async function buildValidatedUploadManifestPayload(systemKey) {
260
279
  }
261
280
 
262
281
  /**
263
- * Upload path after dry-run check.
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.
286
+ *
264
287
  * @param {string} systemKey
265
- * @param {Object} options
266
288
  * @param {Object} manifest
267
- * @param {Object} payload
289
+ * @returns {Promise<void>}
268
290
  */
269
- async function runUploadPublishAndSummary(systemKey, options, manifest, payload) {
270
- const { dataplaneUrl, authConfig, environment } = await resolveDataplaneAndAuth(systemKey);
271
- requireBearerForDataplanePipeline(authConfig);
272
- logger.log(chalk.gray('Target:'));
273
- logger.log(chalk.gray(`Environment: ${environment}`));
274
- logger.log(chalk.gray(`Dataplane: ${dataplaneUrl}`));
275
- logDataplanePipelineWarning();
276
- if (options.verbose) {
277
- await maybeRunVerboseServerValidation(dataplaneUrl, authConfig, payload);
291
+ async function logAndSyncLocalOpenApiForUpload(systemKey, manifest) {
292
+ const appPath = getIntegrationPath(systemKey);
293
+ const ei = manifest && manifest.externalIntegration;
294
+ const datasourceFiles = ei && Array.isArray(ei.dataSources) ? ei.dataSources : [];
295
+ const openapiSyncLines = await maybeSyncOpenApiFilesForMcp({
296
+ enabled: true,
297
+ dryRun: false,
298
+ appPath,
299
+ systemKey,
300
+ datasourceFiles
301
+ });
302
+ for (const line of openapiSyncLines) {
303
+ logger.log(line.startsWith('Skipped') ? formatWarningLine(line) : formatSuccessLine(line));
278
304
  }
279
- await pushAndLogCredentialSecrets(dataplaneUrl, authConfig, systemKey, payload);
280
- const rawRes = await runUploadValidatePublish(dataplaneUrl, authConfig, payload);
305
+ }
306
+
307
+ /**
308
+ * @param {Object} ctx
309
+ * @param {string} ctx.systemKey
310
+ * @param {Object} ctx.options
311
+ * @param {Object} ctx.manifest
312
+ * @param {Object} ctx.payload
313
+ * @param {string} ctx.environment
314
+ * @param {string} ctx.dataplaneUrl
315
+ * @param {Object} ctx.authConfig
316
+ * @param {Object} ctx.rawRes
317
+ * @returns {Promise<void>}
318
+ */
319
+ async function handlePublicationAndFollowups(ctx) {
320
+ const {
321
+ systemKey,
322
+ options,
323
+ manifest,
324
+ payload,
325
+ environment,
326
+ dataplaneUrl,
327
+ authConfig,
328
+ rawRes
329
+ } = ctx;
281
330
  const publication = unwrapPublicationResult(rawRes);
282
331
  if (!publication) {
283
332
  throw new Error(
@@ -296,7 +345,6 @@ async function runUploadPublishAndSummary(systemKey, options, manifest, payload)
296
345
  if (options.probe) {
297
346
  await maybeRunUploadProbe(dataplaneUrl, systemKey, authConfig, options.probeTimeout);
298
347
  }
299
-
300
348
  const dsKeys = (payload.dataSources || []).map((ds) => ds && ds.key).filter(Boolean);
301
349
  await maybeSyncSystemCertificationFromDataplane({
302
350
  label: 'upload',
@@ -308,6 +356,41 @@ async function runUploadPublishAndSummary(systemKey, options, manifest, payload)
308
356
  });
309
357
  }
310
358
 
359
+ /**
360
+ * Upload path after dry-run check.
361
+ * @param {string} systemKey
362
+ * @param {Object} options
363
+ * @param {Object} manifest
364
+ * @param {Object} payload
365
+ */
366
+ async function runUploadPublishAndSummary(systemKey, options, manifest, payload) {
367
+ const { dataplaneUrl, authConfig, environment } = await resolveDataplaneAndAuth(systemKey);
368
+ requireBearerForDataplanePipeline(authConfig);
369
+ logger.log('');
370
+ logger.log(sectionTitle('Target'));
371
+ logger.log(SEP);
372
+ logger.log(headerKeyValue('Environment:', environment));
373
+ logger.log(headerKeyValue('Dataplane:', dataplaneUrl));
374
+ logDataplanePipelineWarning();
375
+ if (options.verbose) {
376
+ await maybeRunVerboseServerValidation(dataplaneUrl, authConfig, payload);
377
+ }
378
+ await pushAndLogCredentialSecrets(dataplaneUrl, authConfig, systemKey, payload);
379
+
380
+ const rawRes = await runUploadValidatePublish(dataplaneUrl, authConfig, payload);
381
+ await logAndSyncLocalOpenApiForUpload(systemKey, manifest);
382
+ await handlePublicationAndFollowups({
383
+ systemKey,
384
+ options,
385
+ manifest,
386
+ payload,
387
+ environment,
388
+ dataplaneUrl,
389
+ authConfig,
390
+ rawRes
391
+ });
392
+ }
393
+
311
394
  /**
312
395
  * Uploads external system: publishes to dataplane and registers with controller (draft).
313
396
  * @param {string} systemKey - External system key (integration/<systemKey>/)
@@ -322,8 +405,13 @@ async function runUploadPublishAndSummary(systemKey, options, manifest, payload)
322
405
  */
323
406
  async function uploadExternalSystem(systemKey, options = {}) {
324
407
  validateSystemKeyFormat(systemKey);
325
- logger.log(chalk.blue(`\nUploading external system: ${chalk.bold(systemKey)}`));
326
- const { manifest, payload } = await buildValidatedUploadManifestPayload(systemKey);
408
+ logger.log('');
409
+ logger.log(sectionTitle('Upload external system'));
410
+ logger.log(SEP);
411
+ logger.log(headerKeyValue('System:', systemKey));
412
+ const { manifest, payload } = await buildValidatedUploadManifestPayload(systemKey, {
413
+ dryRun: !!options.dryRun
414
+ });
327
415
  if (options.dryRun) {
328
416
  logger.log(chalk.yellow('Dry run: would upload payload (no API calls).'));
329
417
  logger.log(
@@ -15,6 +15,7 @@ const { getIntegrationPath } = require('../utils/paths');
15
15
  const { parseOpenApi, testMcpConnection, credentialSelection } = require('../api/wizard.api');
16
16
  const { listCredentials } = require('../api/credentials.api');
17
17
  const { listExternalSystems, getExternalSystem } = require('../api/external-systems.api');
18
+ const { formatSuccessLine } = require('../utils/cli-layout-chalk');
18
19
 
19
20
  /**
20
21
  * Parse OpenAPI file or URL
@@ -35,7 +36,7 @@ async function parseOpenApiSource(dataplaneUrl, authConfig, sourceType, sourceDa
35
36
  if (!parseResponse.success) {
36
37
  throw new Error(`OpenAPI parsing failed: ${parseResponse.error || parseResponse.formattedError}`);
37
38
  }
38
- logger.log(chalk.green(`\u2713 OpenAPI ${isUrl ? 'URL' : 'file'} parsed successfully`));
39
+ logger.log(formatSuccessLine(`OpenAPI ${isUrl ? 'URL' : 'file'} parsed successfully`));
39
40
  return parseResponse.data?.spec;
40
41
  } catch (error) {
41
42
  spinner.stop();
@@ -61,7 +62,7 @@ async function testMcpServerConnection(dataplaneUrl, authConfig, sourceData) {
61
62
  if (!testResponse.success || !testResponse.data?.connected) {
62
63
  throw new Error(`MCP connection failed: ${testResponse.data?.error || 'Unable to connect'}`);
63
64
  }
64
- logger.log(chalk.green('\u2713 MCP server connection successful'));
65
+ logger.log(formatSuccessLine('MCP server connection successful'));
65
66
  } catch (error) {
66
67
  spinner.stop();
67
68
  throw error;
@@ -145,7 +146,7 @@ async function runCredentialSelectionLoop(dataplaneUrl, authConfig, selectionDat
145
146
  }
146
147
  if (response.success) {
147
148
  const actionText = selectionData.action === 'create' ? 'created' : 'selected';
148
- logger.log(chalk.green(`\u2713 Credential ${actionText}`));
149
+ logger.log(formatSuccessLine(`Credential ${actionText}`));
149
150
  return response.data?.credentialIdOrKey || null;
150
151
  }
151
152
  const errorMsg = response.error || response.formattedError || response.message || 'Unknown error';
@@ -308,18 +309,19 @@ function throwConfigGenerationError(generateResponse, options = {}) {
308
309
  }
309
310
 
310
311
  /**
311
- * Write debug log to integration/<systemKey>/debug.log
312
+ * Write debug log to integration/<systemKey>/logs/debug.log
312
313
  * @async
313
314
  * @param {string} appName - Application name
314
315
  * @param {string} content - Debug log content
315
316
  */
316
317
  async function writeDebugLog(appName, content) {
317
318
  try {
318
- const dir = getIntegrationPath(appName);
319
+ const appDir = getIntegrationPath(appName);
320
+ const dir = path.join(appDir, 'logs');
319
321
  await fs.mkdir(dir, { recursive: true });
320
322
  const debugPath = path.join(dir, 'debug.log');
321
323
  await fs.writeFile(debugPath, content, 'utf8');
322
- logger.log(chalk.gray(` Debug log saved to integration/${appName}/debug.log`));
324
+ logger.log(chalk.gray(` Debug log saved to integration/${appName}/logs/debug.log`));
323
325
  } catch (e) {
324
326
  logger.warn(`Could not save debug.log: ${e.message}`);
325
327
  }
@@ -336,13 +338,14 @@ async function writeDebugLog(appName, content) {
336
338
  async function writeDebugManifest(appName, systemConfig, datasourceConfig) {
337
339
  const saved = [];
338
340
  try {
339
- const dir = getIntegrationPath(appName);
341
+ const appDir = getIntegrationPath(appName);
342
+ const dir = path.join(appDir, 'logs');
340
343
  await fs.mkdir(dir, { recursive: true });
341
344
  if (systemConfig && typeof systemConfig === 'object') {
342
345
  const systemPath = path.join(dir, 'debug-system.yaml');
343
346
  await fs.writeFile(systemPath, yaml.dump(systemConfig, { lineWidth: -1 }), 'utf8');
344
347
  saved.push('debug-system.yaml');
345
- logger.log(chalk.gray(` Debug manifest saved to integration/${appName}/debug-system.yaml`));
348
+ logger.log(chalk.gray(` Debug manifest saved to integration/${appName}/logs/debug-system.yaml`));
346
349
  }
347
350
  if (datasourceConfig !== undefined && datasourceConfig !== null) {
348
351
  const configs = Array.isArray(datasourceConfig) ? datasourceConfig : [datasourceConfig];
@@ -351,7 +354,7 @@ async function writeDebugManifest(appName, systemConfig, datasourceConfig) {
351
354
  const toWrite = configs.length === 1 ? configs[0] : configs;
352
355
  await fs.writeFile(datasourcePath, yaml.dump(toWrite, { lineWidth: -1 }), 'utf8');
353
356
  saved.push('debug-datasource.yaml');
354
- logger.log(chalk.gray(` Debug manifest saved to integration/${appName}/debug-datasource.yaml`));
357
+ logger.log(chalk.gray(` Debug manifest saved to integration/${appName}/logs/debug-datasource.yaml`));
355
358
  }
356
359
  }
357
360
  } catch (e) {
@@ -381,7 +384,7 @@ async function saveDebugManifestOnErrorAndThrow(generateResponse, opts) {
381
384
  const savedManifest = await writeDebugManifest(appName, systemConfig, datasourceConfig);
382
385
  if (debugLog || savedManifest.length > 0) {
383
386
  const files = [debugLog && 'debug.log', ...savedManifest].filter(Boolean).join(', ');
384
- debugManifestHint = `Debug manifest saved to integration/${appName}/ (${files}). Review the log and fix the manifest manually, then run: aifabrix wizard ${appName}`;
387
+ debugManifestHint = `Debug manifest saved to integration/${appName}/logs/ (${files}). Review the log and fix the manifest manually, then run: aifabrix wizard ${appName}`;
385
388
  }
386
389
  }
387
390
  throwConfigGenerationError(generateResponse, { debugManifestHint });
@@ -398,7 +401,7 @@ async function throwValidationFailureWithDebug(validateResponse, systemConfig, c
398
401
  );
399
402
  if (!debugLog && savedManifest.length === 0) throw new Error(`Configuration validation failed: ${errorMsg}`);
400
403
  const files = [debugLog && 'debug.log', ...savedManifest].filter(Boolean).join(', ');
401
- throw new Error(`Configuration validation failed: ${errorMsg}\n\nDebug manifest saved to integration/${options.appName}/ (${files}). Review the log and fix the manifest manually, then run: aifabrix wizard ${options.appName}`);
404
+ throw new Error(`Configuration validation failed: ${errorMsg}\n\nDebug manifest saved to integration/${options.appName}/logs/ (${files}). Review the log and fix the manifest manually, then run: aifabrix wizard ${options.appName}`);
402
405
  }
403
406
 
404
407
  /**
@@ -13,6 +13,7 @@ const logger = require('../utils/logger');
13
13
  const { getDeploymentAuth, requireBearerForDataplanePipeline } = require('../utils/token-manager');
14
14
  const { resolveControllerUrl } = require('../utils/controller-url');
15
15
  const { normalizeWizardConfigs } = require('./wizard-config-normalizer');
16
+ const { formatSuccessLine, formatSuccessParagraph } = require('../utils/cli-layout-chalk');
16
17
  const {
17
18
  createWizardSession,
18
19
  updateWizardSession,
@@ -143,7 +144,7 @@ async function handleModeSelection(dataplaneUrl, authConfig, configMode = null,
143
144
  throw new Error(fullMsg);
144
145
  }
145
146
  const sessionId = extractSessionId(sessionResponse.data);
146
- logger.log(chalk.green(`\u2713 Session created: ${sessionId}`));
147
+ logger.log(formatSuccessLine(`Session created: ${sessionId}`));
147
148
  return { mode, sessionId };
148
149
  }
149
150
 
@@ -250,7 +251,7 @@ async function handleTypeDetection(dataplaneUrl, authConfig, openapiSpec) {
250
251
  if (detectResponse.success && detectResponse.data) {
251
252
  const detectedType = detectResponse.data;
252
253
  const recommendedType = detectedType.recommendedType || detectedType.apiType || 'unknown';
253
- logger.log(chalk.green(`\u2713 API type detected: ${recommendedType}`));
254
+ logger.log(formatSuccessLine(`API type detected: ${recommendedType}`));
254
255
  return detectedType;
255
256
  }
256
257
  } catch (error) {
@@ -329,7 +330,7 @@ async function handleConfigurationGeneration(dataplaneUrl, authConfig, options)
329
330
  await writeDebugLog(options.appName, debugLog);
330
331
  }
331
332
  }
332
- logger.log(chalk.green('\u2713 Configuration generated successfully'));
333
+ logger.log(formatSuccessLine('Configuration generated successfully'));
333
334
  return { systemConfig: normalized.systemConfig, datasourceConfigs: normalized.datasourceConfigs, systemKey: result.systemKey };
334
335
  } catch (error) {
335
336
  spinner.stop();
@@ -368,7 +369,7 @@ async function validateWizardConfiguration(dataplaneUrl, authConfig, systemConfi
368
369
  if (validateResponse.data?.warnings?.length > 0) warnings.push(...validateResponse.data.warnings);
369
370
  }
370
371
  spinner.stop();
371
- logger.log(chalk.green('\u2713 Configuration validated successfully'));
372
+ logger.log(formatSuccessLine('Configuration validated successfully'));
372
373
  if (warnings.length > 0) logger.log(chalk.yellow('\n\u26A0 Warnings:\n' + warnings.map(w => ` - ${w.message || w}`).join('\n')));
373
374
  } catch (error) {
374
375
  spinner.stop();
@@ -423,7 +424,7 @@ async function tryUpdateReadmeFromDeploymentDocs(appPath, appName, dataplaneUrl,
423
424
  * @param {string} format - Project format: yaml | json
424
425
  */
425
426
  function logWizardFileSaveFooter(appName, generatedFiles) {
426
- logger.log(chalk.green('\n\u2713 Wizard completed successfully!'));
427
+ logger.log(formatSuccessParagraph('Wizard completed successfully!'));
427
428
  logger.log(chalk.green(`\nFiles created in: ${generatedFiles.appPath}`));
428
429
  logger.log(chalk.blue('\nNext steps:'));
429
430
  logger.log(chalk.gray(` 1. Review the generated files in integration/${appName}/`));
@@ -6,7 +6,7 @@
6
6
 
7
7
  const chalk = require('chalk');
8
8
  const logger = require('../utils/logger');
9
- const { infoLine, formatSuccessLine } = require('../utils/cli-test-layout-chalk');
9
+ const { formatProgress, formatSuccessLine } = require('../utils/cli-test-layout-chalk');
10
10
  const { getDataplaneUrl } = require('../datasource/deploy');
11
11
  const { listEnvironmentApplications } = require('../api/environments.api');
12
12
 
@@ -102,7 +102,7 @@ async function tryFallbackDataplaneUrl(controllerUrl, environment, authConfig, s
102
102
  async function discoverDataplaneUrl(controllerUrl, environment, authConfig, opts = {}) {
103
103
  const silent = opts.silent === true;
104
104
  if (!silent) {
105
- logger.log(infoLine('🌐 Getting dataplane URL from controller...'));
105
+ logger.log(formatProgress('Getting dataplane URL from controller...'));
106
106
  }
107
107
  try {
108
108
  const dataplaneAppKey = await findDataplaneServiceAppKey(controllerUrl, environment, authConfig);
@@ -9,6 +9,7 @@ const logger = require('../utils/logger');
9
9
  const { discoverEntities } = require('../api/wizard.api');
10
10
  const { validateEntityNameForOpenApi } = require('../validation/wizard-datasource-validation');
11
11
  const { promptForEntitySelection } = require('../generator/wizard-prompts');
12
+ const { formatSuccessLine } = require('../utils/cli-layout-chalk');
12
13
 
13
14
  /**
14
15
  * If wizard.yaml entity name matches discover-entities list, use it; else warn.
@@ -22,7 +23,7 @@ function resolvePrefillEntityName(trimmed, entities) {
22
23
  logger.log(chalk.gray(
23
24
  `Using entity from wizard.yaml (${trimmed}). Skipping entity prompts.`
24
25
  ));
25
- logger.log(chalk.green(`\u2713 Selected entity: ${trimmed}`));
26
+ logger.log(formatSuccessLine(`Selected entity: ${trimmed}`));
26
27
  return trimmed;
27
28
  }
28
29
  logger.log(chalk.yellow(
@@ -42,7 +43,7 @@ async function promptForValidatedEntity(entities) {
42
43
  if (!validation.valid) {
43
44
  throw new Error(`Invalid entity '${entityName}'. Available: ${entities.map(e => e.name).join(', ')}`);
44
45
  }
45
- logger.log(chalk.green(`\u2713 Selected entity: ${entityName}`));
46
+ logger.log(formatSuccessLine(`Selected entity: ${entityName}`));
46
47
  return entityName;
47
48
  }
48
49
 
@@ -63,7 +64,7 @@ async function discoverAndSelectEntity(dataplaneUrl, authConfig, openapiSpec, pr
63
64
 
64
65
  if (entities.length === 1) {
65
66
  const only = entities[0].name;
66
- logger.log(chalk.green(`\u2713 Only one entity discovered; using: ${only}`));
67
+ logger.log(formatSuccessLine(`Only one entity discovered; using: ${only}`));
67
68
  return only;
68
69
  }
69
70
 
@@ -6,6 +6,7 @@
6
6
 
7
7
  const chalk = require('chalk');
8
8
  const logger = require('../utils/logger');
9
+ const { formatSuccessLine } = require('../utils/cli-layout-chalk');
9
10
  const {
10
11
  validateWizardConfig: validateWizardConfigFile,
11
12
  displayValidationResults
@@ -141,7 +142,7 @@ async function handleWizardHeadless(options) {
141
142
  displayValidationResults(validationResult);
142
143
  throw new Error('Wizard configuration validation failed');
143
144
  }
144
- logger.log(chalk.green('\u2713 Configuration file validated'));
145
+ logger.log(formatSuccessLine('Configuration file validated'));
145
146
 
146
147
  const wizardConfig = validationResult.config;
147
148
  const appName = wizardConfig.appName;
@@ -50,6 +50,7 @@ const {
50
50
  ensureIntegrationDir
51
51
  } = require('./wizard-helpers');
52
52
  const { humanizeAppKey } = require('../generator/wizard-prompts-secondary');
53
+ const { formatSuccessLine } = require('../utils/cli-layout-chalk');
53
54
 
54
55
  /**
55
56
  * Map resolved source type/data onto wizard state.source.
@@ -455,7 +456,7 @@ async function executeWizardFlow(appKey, dataplaneUrl, authConfig, flowOpts = {}
455
456
  }
456
457
  logger.log(chalk.blue('\n\uD83D\uDCCB Step 1: Create Session'));
457
458
  const sessionId = await createSessionFromParams(dataplaneUrl, authConfig, mode, systemIdOrKey, appKey);
458
- logger.log(chalk.green('\u2713 Session created'));
459
+ logger.log(formatSuccessLine('Session created'));
459
460
 
460
461
  const platforms = mode === 'add-datasource' ? [] : await getWizardPlatforms(dataplaneUrl, authConfig);
461
462
  const state = await runWizardStepsAfterSession(appKey, dataplaneUrl, authConfig, sessionId, {
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Infra compose service names accepted by `aifabrix restart` and {@link restartService}.
3
+ *
4
+ * @fileoverview Single source of truth for CLI help + validation
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ /** @type {ReadonlyArray<{ name: string, description: string }>} */
12
+ const RESTARTABLE_INFRA_SERVICES = Object.freeze([
13
+ { name: 'postgres', description: 'PostgreSQL database' },
14
+ { name: 'redis', description: 'Redis' },
15
+ { name: 'pgadmin', description: 'pgAdmin 4 web UI (only if enabled when you ran up-infra)' },
16
+ { name: 'redis-commander', description: 'Redis Commander web UI (only if enabled when you ran up-infra)' },
17
+ { name: 'traefik', description: 'Traefik reverse proxy (only if enabled when you ran up-infra)' }
18
+ ]);
19
+
20
+ /**
21
+ * @returns {string[]} service names in compose order
22
+ */
23
+ function getRestartableInfraServiceNames() {
24
+ return RESTARTABLE_INFRA_SERVICES.map((s) => s.name);
25
+ }
26
+
27
+ /**
28
+ * Aligned lines for Commander `addHelpText('after', …)`.
29
+ * @returns {string}
30
+ */
31
+ function buildRestartInfraHelpLines() {
32
+ const col = 22;
33
+ return RESTARTABLE_INFRA_SERVICES.map((s) => ` ${s.name.padEnd(col)}${s.description}`).join('\n');
34
+ }
35
+
36
+ module.exports = {
37
+ RESTARTABLE_INFRA_SERVICES,
38
+ getRestartableInfraServiceNames,
39
+ buildRestartInfraHelpLines
40
+ };
@@ -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
+ };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * URL and developer-id normalization for runtime config.
3
+ *
4
+ * @fileoverview Shared normalizers used by lib/core/config.js
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ /**
12
+ * Normalize controller URL for consistent storage and lookup
13
+ * @param {string} url - Controller URL to normalize
14
+ * @returns {string|undefined|null} Normalized URL or original falsy
15
+ */
16
+ function normalizeControllerUrl(url) {
17
+ if (!url || typeof url !== 'string') {
18
+ return url;
19
+ }
20
+ let normalized = url.trim().replace(/\/+$/, '');
21
+ if (!normalized.match(/^https?:\/\//)) {
22
+ normalized = `http://${normalized}`;
23
+ }
24
+ return normalized;
25
+ }
26
+
27
+ /**
28
+ * Validate and normalize developer ID
29
+ * @param {*} developerId - Developer ID value (can be string, number, undefined, or null)
30
+ * @returns {string} Normalized developer ID as string
31
+ * @throws {Error} If developer ID is invalid
32
+ */
33
+ function validateAndNormalizeDeveloperId(developerId) {
34
+ const DEV_ID_DIGITS_REGEX = /^[0-9]+$/;
35
+
36
+ if (typeof developerId === 'undefined' || developerId === null) {
37
+ return '0';
38
+ }
39
+
40
+ if (typeof developerId === 'number') {
41
+ if (developerId < 0 || !Number.isFinite(developerId)) {
42
+ throw new Error('Developer ID must be a non-negative digit string or number');
43
+ }
44
+ return String(developerId);
45
+ }
46
+
47
+ if (typeof developerId === 'string') {
48
+ if (!DEV_ID_DIGITS_REGEX.test(developerId)) {
49
+ throw new Error('Developer ID must be a non-negative digit string or number');
50
+ }
51
+ return developerId;
52
+ }
53
+
54
+ throw new Error('Developer ID must be a non-negative digit string or number');
55
+ }
56
+
57
+ module.exports = {
58
+ normalizeControllerUrl,
59
+ validateAndNormalizeDeveloperId
60
+ };