@aifabrix/builder 2.45.5 → 2.46.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 (231) hide show
  1. package/.cursor/rules/cli-layout.mdc +2 -0
  2. package/.cursor/rules/cli-product-neutral.mdc +43 -0
  3. package/.cursor/rules/project-rules.mdc +1 -0
  4. package/README.md +43 -17
  5. package/integration/hubspot-test/test-artifacts/wizard-hubspot-credential-real.yaml +1 -1
  6. package/integration/hubspot-test/test-artifacts/wizard-hubspot-env-vars.yaml +1 -1
  7. package/jest.config.default.js +2 -1
  8. package/jest.config.isolated.js +2 -1
  9. package/jest.global-live-fabrix-hooks.js +36 -0
  10. package/jest.isolated-projects.js +130 -0
  11. package/jest.projects.js +41 -136
  12. package/lib/api/api-response-errors.js +124 -0
  13. package/lib/api/governance-scenario-pack.api.js +159 -0
  14. package/lib/api/index.js +43 -3
  15. package/lib/api/lifecycle.api.js +77 -0
  16. package/lib/api/pipeline.api.js +3 -1
  17. package/lib/api/records-bulk.api.js +14 -1
  18. package/lib/api/validation-runner.js +3 -1
  19. package/lib/app/deploy-config.js +4 -1
  20. package/lib/app/down.js +20 -1
  21. package/lib/app/push.js +4 -5
  22. package/lib/app/register.js +7 -4
  23. package/lib/app/run-env-compose.js +84 -30
  24. package/lib/build/index.js +5 -0
  25. package/lib/certification/sync-system-certification.js +13 -6
  26. package/lib/cli/index.js +4 -0
  27. package/lib/cli/infra-guided-auth.js +109 -0
  28. package/lib/cli/infra-guided.js +84 -81
  29. package/lib/cli/setup-app.help.js +0 -31
  30. package/lib/cli/setup-app.js +28 -2
  31. package/lib/cli/setup-app.test-commands.js +5 -57
  32. package/lib/cli/setup-auth.js +25 -0
  33. package/lib/cli/setup-certification-commands.js +147 -0
  34. package/lib/cli/setup-certification.help.js +141 -0
  35. package/lib/cli/setup-environment.js +19 -2
  36. package/lib/cli/setup-external-system-help.js +62 -0
  37. package/lib/cli/setup-external-system.js +25 -3
  38. package/lib/cli/setup-infra.js +9 -1
  39. package/lib/cli/setup-platform.js +6 -1
  40. package/lib/cli/setup-secrets.js +33 -1
  41. package/lib/cli/setup-utility-help-after.js +22 -2
  42. package/lib/cli/setup-utility-repair.js +198 -0
  43. package/lib/cli/setup-utility-resolve.js +40 -4
  44. package/lib/cli/setup-utility-validate.js +197 -0
  45. package/lib/cli/setup-utility.js +4 -239
  46. package/lib/commands/auth-status-client-tokens.js +214 -0
  47. package/lib/commands/auth-status-dataplane-version.js +58 -20
  48. package/lib/commands/auth-status.js +41 -47
  49. package/lib/commands/datasource-test-trust-cli.js +27 -23
  50. package/lib/commands/datasource-unified-test-cli.js +6 -3
  51. package/lib/commands/datasource-unified-test-cli.options.js +5 -1
  52. package/lib/commands/datasource.js +7 -7
  53. package/lib/commands/dev-show-display.js +8 -1
  54. package/lib/commands/governance-cli-shared.js +94 -0
  55. package/lib/commands/governance-command-action.js +132 -0
  56. package/lib/commands/governance-download-external.js +34 -0
  57. package/lib/commands/governance-generate-external.js +71 -0
  58. package/lib/commands/governance-upload-external.js +36 -0
  59. package/lib/commands/governance-verify-external.js +64 -0
  60. package/lib/commands/governance.js +144 -0
  61. package/lib/commands/lifecycle-command-action.js +123 -0
  62. package/lib/commands/lifecycle-external.js +268 -0
  63. package/lib/commands/login.js +11 -2
  64. package/lib/commands/repair-auth-apply.js +127 -0
  65. package/lib/commands/repair-changed-files.js +41 -0
  66. package/lib/commands/repair-datasource-keys.js +4 -1
  67. package/lib/commands/repair-datasource-run.js +59 -0
  68. package/lib/commands/repair-datasource-test-payload.js +259 -0
  69. package/lib/commands/repair-datasource.js +1 -51
  70. package/lib/commands/repair-env-template.js +64 -13
  71. package/lib/commands/repair-external-run.js +216 -0
  72. package/lib/commands/repair-openapi-sync.js +23 -0
  73. package/lib/commands/repair-persist.js +4 -0
  74. package/lib/commands/repair-rbac.js +52 -1
  75. package/lib/commands/repair-steps.js +145 -0
  76. package/lib/commands/repair.js +29 -296
  77. package/lib/commands/secrets-get.js +184 -0
  78. package/lib/commands/secrets-remove-all.js +3 -1
  79. package/lib/commands/setup-image-refresh.js +146 -0
  80. package/lib/commands/setup-modes.js +158 -162
  81. package/lib/commands/setup-platform-auth.js +76 -0
  82. package/lib/commands/setup-prompts-admin.js +244 -0
  83. package/lib/commands/setup-prompts-platform-mode.js +67 -0
  84. package/lib/commands/setup-prompts-validators.js +36 -0
  85. package/lib/commands/setup-prompts.js +24 -76
  86. package/lib/commands/setup-run-context.js +45 -0
  87. package/lib/commands/setup-spinners.js +39 -0
  88. package/lib/commands/setup.js +27 -5
  89. package/lib/commands/teardown.js +39 -0
  90. package/lib/commands/test-e2e-external.js +24 -8
  91. package/lib/commands/test-governance-external.js +3 -24
  92. package/lib/commands/test-trust-external.js +20 -21
  93. package/lib/commands/up-common.js +25 -13
  94. package/lib/commands/up-dataplane.js +50 -13
  95. package/lib/commands/upload-publish-context.js +75 -0
  96. package/lib/commands/upload-sidecar-followups.js +89 -0
  97. package/lib/commands/upload.js +90 -108
  98. package/lib/commands/verify-governance-combined-external.js +61 -0
  99. package/lib/commands/verify-governance-command-action.js +64 -0
  100. package/lib/commands/verify-governance-db.js +102 -0
  101. package/lib/commands/verify-governance-external.js +77 -0
  102. package/lib/commands/verify-governance-result-builders.js +71 -0
  103. package/lib/commands/verify-operations-command-action.js +68 -0
  104. package/lib/commands/verify-operations-external.js +268 -0
  105. package/lib/commands/verify-operations-persist.js +75 -0
  106. package/lib/commands/verify-operations-step-plan.js +68 -0
  107. package/lib/commands/verify-operations-steps.js +111 -0
  108. package/lib/commands/verify-trust-command-action.js +70 -0
  109. package/lib/commands/verify-trust-external.js +67 -0
  110. package/lib/commands/wizard-config-normalizer.js +43 -23
  111. package/lib/commands/wizard-core.js +2 -0
  112. package/lib/core/admin-password-generator.js +38 -0
  113. package/lib/core/admin-secrets-bundle.js +111 -0
  114. package/lib/core/admin-secrets-env-overlay.js +62 -0
  115. package/lib/core/admin-secrets.js +32 -4
  116. package/lib/core/config-attach-extensions.js +3 -0
  117. package/lib/core/config-platform-controller.js +44 -0
  118. package/lib/core/config-setup-installation-profile.js +42 -0
  119. package/lib/core/config-setup-platform-mode.js +46 -0
  120. package/lib/core/config.js +46 -36
  121. package/lib/core/secrets-admin-env.js +6 -2
  122. package/lib/core/secrets-ensure.js +4 -0
  123. package/lib/core/secrets-env-content.js +54 -48
  124. package/lib/core/secrets-env-write.js +10 -1
  125. package/lib/core/secrets-infra-placeholder-sync.js +2 -0
  126. package/lib/core/setup-installation-profile.js +43 -0
  127. package/lib/core/setup-platform-mode.js +55 -0
  128. package/lib/datasource/agent-trust-debug-log.js +1 -1
  129. package/lib/datasource/agent-trust-run.js +5 -3
  130. package/lib/datasource/audit-evidence-extract-debug.js +30 -18
  131. package/lib/datasource/audit-evidence-extract.js +7 -4
  132. package/lib/datasource/audit-evidence-verify-matrix.js +54 -26
  133. package/lib/datasource/bulk-loader-run-batches.js +15 -3
  134. package/lib/datasource/datasource-load-export-context.js +3 -0
  135. package/lib/datasource/log-cleaner.js +2 -2
  136. package/lib/datasource/log-viewer.js +8 -8
  137. package/lib/datasource/test-e2e.js +4 -1
  138. package/lib/datasource/unified-validation-run-body.js +3 -1
  139. package/lib/datasource/unified-validation-run.js +5 -2
  140. package/lib/deployment/environment-poll-status.js +138 -0
  141. package/lib/deployment/environment.js +42 -34
  142. package/lib/external-system/download.js +21 -20
  143. package/lib/external-system/generator.js +5 -1
  144. package/lib/external-system/integration-auth-context.js +45 -0
  145. package/lib/external-system/test-auth.js +12 -5
  146. package/lib/external-system/test.js +6 -6
  147. package/lib/generator/external-controller-manifest.js +15 -0
  148. package/lib/governance/governance-cli-errors.js +96 -0
  149. package/lib/governance/governance-download-display.js +38 -0
  150. package/lib/governance/governance-run-display.js +215 -25
  151. package/lib/infrastructure/admin-secrets-apply.js +84 -0
  152. package/lib/infrastructure/admin-secrets-ensure.js +130 -0
  153. package/lib/infrastructure/admin-secrets-resolve.js +95 -0
  154. package/lib/infrastructure/admin-secrets-sync.js +91 -0
  155. package/lib/infrastructure/helpers.js +23 -129
  156. package/lib/infrastructure/index.js +5 -6
  157. package/lib/infrastructure/orphan-infra-docker-teardown.js +11 -5
  158. package/lib/lifecycle/certification-progress.js +231 -0
  159. package/lib/lifecycle/dataplane-ui-url.js +145 -0
  160. package/lib/lifecycle/exit-codes.js +24 -0
  161. package/lib/lifecycle/json-envelope.js +25 -0
  162. package/lib/lifecycle/lifecycle-governance-options.js +51 -0
  163. package/lib/lifecycle/logger-quiet.js +29 -0
  164. package/lib/lifecycle/open-api-published.js +59 -0
  165. package/lib/lifecycle/operations-readiness-metrics.js +101 -0
  166. package/lib/lifecycle/product-model.js +196 -0
  167. package/lib/lifecycle/report-display.js +471 -0
  168. package/lib/lifecycle/scenario-upload.js +191 -0
  169. package/lib/lifecycle/verify-step-progress.js +87 -0
  170. package/lib/parameters/infra-kv-discovery.js +27 -2
  171. package/lib/protection/auth-context.js +10 -4
  172. package/lib/schema/application-schema.json +1 -1
  173. package/lib/schema/datasource-test-run.schema.json +89 -6
  174. package/lib/schema/external-datasource.schema.json +2636 -2622
  175. package/lib/schema/external-system.schema.json +1089 -761
  176. package/lib/schema/infra.parameter.yaml +45 -0
  177. package/lib/schema/type/credential-field-registry.json +103 -97
  178. package/lib/utils/aifabrix-test-runtime-guard.js +125 -0
  179. package/lib/utils/api.js +55 -31
  180. package/lib/utils/app-relative-path-env.js +112 -0
  181. package/lib/utils/cli-secrets-error-format.js +83 -21
  182. package/lib/utils/cli-utils.js +16 -4
  183. package/lib/utils/compose-generate-docker-compose.js +4 -0
  184. package/lib/utils/compose-traefik-ingress-base.js +16 -1
  185. package/lib/utils/config-secrets-path.js +46 -0
  186. package/lib/utils/controller-url.js +88 -1
  187. package/lib/utils/credential-secrets-env.js +137 -9
  188. package/lib/utils/dataplane-resolver.js +129 -2
  189. package/lib/utils/declarative-url-ports.js +3 -3
  190. package/lib/utils/deployment-auth-mode.js +41 -0
  191. package/lib/utils/developer-volume-id.js +33 -0
  192. package/lib/utils/env-copy.js +14 -9
  193. package/lib/utils/env-kv-orphan-lines.js +43 -0
  194. package/lib/utils/error-formatters/network-errors.js +13 -4
  195. package/lib/utils/external-env-template.js +1 -0
  196. package/lib/utils/external-system-readiness-core.js +10 -0
  197. package/lib/utils/external-system-readiness-display.js +80 -23
  198. package/lib/utils/health-check.js +4 -1
  199. package/lib/utils/help-builder.js +11 -8
  200. package/lib/utils/keycloak-realm-ready.js +101 -0
  201. package/lib/utils/load-cip-capacity-display-config.js +61 -15
  202. package/lib/utils/manifest-source-emit.js +2 -0
  203. package/lib/utils/pipeline-upload-auth-retry.js +82 -0
  204. package/lib/utils/platform-controller-url.js +156 -0
  205. package/lib/utils/postgres-platform-bootstrap.js +199 -0
  206. package/lib/utils/postgres-wipe.js +9 -2
  207. package/lib/utils/resolve-env-mode.js +47 -0
  208. package/lib/utils/secrets-generator.js +31 -25
  209. package/lib/utils/secrets-missing-error.js +164 -0
  210. package/lib/utils/service-url-label.js +81 -0
  211. package/lib/utils/setup-quiet-output.js +25 -0
  212. package/lib/utils/template-helpers.js +15 -0
  213. package/lib/utils/token-manager-client-credentials.js +201 -0
  214. package/lib/utils/token-manager-messages.js +8 -0
  215. package/lib/utils/token-manager-refresh.js +12 -2
  216. package/lib/utils/token-manager.js +86 -105
  217. package/lib/utils/upload-pipeline-ui.js +12 -5
  218. package/lib/utils/upload-sync-options.js +42 -0
  219. package/lib/utils/url-declarative-public-base.js +21 -3
  220. package/lib/utils/url-declarative-vdir-inactive-env.js +14 -4
  221. package/lib/utils/validation-poll-ui.js +8 -0
  222. package/lib/utils/validation-run-stage.js +45 -0
  223. package/package.json +1 -1
  224. package/templates/applications/dataplane/application.yaml +2 -1
  225. package/templates/applications/dataplane/env.template +24 -1
  226. package/templates/applications/miso-controller/env.template +16 -0
  227. package/templates/external-system/README.md.hbs +74 -17
  228. package/templates/python/docker-compose.hbs +19 -18
  229. package/templates/typescript/docker-compose.hbs +18 -17
  230. package/lib/commands/test-governance-command-action.js +0 -55
  231. package/lib/commands/test-trust-command-action.js +0 -118
@@ -1,4 +1,3 @@
1
- const { formatSuccessParagraph } = require('../utils/cli-test-layout-chalk');
2
1
  /**
3
2
  * CLI utility command setup (resolve, json, split-json, show, validate, diff).
4
3
  *
@@ -10,12 +9,15 @@ const { formatSuccessParagraph } = require('../utils/cli-test-layout-chalk');
10
9
  const path = require('path');
11
10
  const fs = require('fs');
12
11
  const chalk = require('chalk');
12
+ const { formatSuccessParagraph } = require('../utils/cli-test-layout-chalk');
13
13
  const generator = require('../generator');
14
14
  const logger = require('../utils/logger');
15
15
  const { handleCommandError, logOfflinePathWhenType } = require('../utils/cli-utils');
16
16
  const { detectAppType, getDeployJsonPath, getResolveAppPath } = require('../utils/paths');
17
17
  const { setupResolveCommand } = require('./setup-utility-resolve');
18
- const { JSON_HELP_AFTER, VALIDATE_HELP_AFTER, REPAIR_HELP_AFTER } = require('./setup-utility-help-after');
18
+ const { setupRepairCommand } = require('./setup-utility-repair');
19
+ const { setupValidateDiffCommands } = require('./setup-utility-validate');
20
+ const { JSON_HELP_AFTER } = require('./setup-utility-help-after');
19
21
 
20
22
  /**
21
23
  * Resolve app path and type for split-json (integration first, then builder).
@@ -136,100 +138,6 @@ function setupSplitJsonCommand(program) {
136
138
  });
137
139
  }
138
140
 
139
- function buildRepairExternalIntegrationOptions(appName, options, format) {
140
- const all = options.all === true;
141
- return {
142
- auth: options.auth,
143
- doc: options.doc || all,
144
- dryRun: options.dryRun,
145
- format,
146
- rbac: options.rbac || all,
147
- expose: options.expose || all,
148
- sync: options.sync || all,
149
- api: options.api || all,
150
- test: options.test || all,
151
- backup: options.backup,
152
- noBackup: options.noBackup
153
- };
154
- }
155
-
156
- function logRepairResult(options, result) {
157
- if (options.dryRun && result.updated && result.changes.length > 0) {
158
- logger.log(chalk.yellow('\nWould apply:'));
159
- result.changes.forEach(c => logger.log(chalk.gray(` ${c}`)));
160
- return;
161
- }
162
- if (Array.isArray(result.backupPaths) && result.backupPaths.length > 0) {
163
- result.backupPaths.forEach((p) => logger.log(chalk.gray(`Backup: ${p}`)));
164
- }
165
- if (result.updated) {
166
- logger.log(formatSuccessParagraph('Repaired external integration config.'));
167
- return;
168
- }
169
- if (Array.isArray(result.changes) && result.changes.length > 0) {
170
- logger.log(formatSuccessParagraph('Repair actions completed.'));
171
- result.changes.forEach(c => logger.log(chalk.gray(` ${c}`)));
172
- return;
173
- }
174
- if (result.readmeRegenerated) {
175
- logger.log(formatSuccessParagraph('Regenerated README.md from deployment manifest.'));
176
- return;
177
- }
178
- logger.log(chalk.gray('No changes needed; config already matches files on disk.'));
179
- }
180
-
181
- /**
182
- * Handler for `aifabrix repair <systemKey>`.
183
- * @param {string} appName
184
- * @param {object} options - Commander options
185
- * @returns {Promise<void>}
186
- */
187
- async function handleRepairCommand(appName, options) {
188
- const { repairExternalIntegration } = require('../commands/repair');
189
- const { detectAppType } = require('../utils/paths');
190
- const { appPath } = await detectAppType(appName);
191
- logOfflinePathWhenType(appPath);
192
- let format = 'yaml';
193
- try {
194
- const config = require('../core/config');
195
- format = (await config.getFormat()) || format;
196
- } catch (_) {
197
- /* default yaml when config unavailable */
198
- }
199
- const result = await repairExternalIntegration(
200
- appName,
201
- buildRepairExternalIntegrationOptions(appName, options, format)
202
- );
203
- logRepairResult(options, result);
204
- }
205
-
206
- function setupRepairCommand(program) {
207
- program.command('repair <systemKey>')
208
- .description('Fix external integration drift (files, RBAC, manifest, …)')
209
- .option('--all', 'Run all repair actions (api, doc, expose, rbac, sync, test)')
210
- .option(
211
- '--api',
212
- 'Validate and sync API contracts needed for OpenAPI/MCP (uses local OpenAPI specs at integration/<systemKey>/openapi/*.json when present)'
213
- )
214
- .option('--auth <method>', 'Set authentication method (oauth2, aad, apikey, basic, queryParam, oidc, hmac, none); updates system file and env.template')
215
- .option('--doc', 'Regenerate README.md from deployment manifest')
216
- .option('--expose', 'Set exposed.schema on each datasource from all fieldMappings.attributes keys (metadata.<key>); removes deprecated exposed.attributes if present')
217
- .option('--rbac', 'Ensure RBAC permissions per datasource and add default Admin/Reader roles if none exist')
218
- .option('--sync', 'Add default sync section to datasources that lack it')
219
- .option('--test', 'Generate testPayload.payloadTemplate and testPayload.expectedResult from attributes')
220
- .option('--no-backup', 'Skip timestamped copies under integration/<systemKey>/backup/')
221
- .option('--dry-run', 'Report changes only; do not write')
222
- .addHelpText('after', REPAIR_HELP_AFTER)
223
- .action(async(appName, options) => {
224
- try {
225
- await handleRepairCommand(appName, options);
226
- } catch (error) {
227
- handleCommandError(error, 'repair');
228
- process.exit(1);
229
- }
230
- });
231
- }
232
-
233
141
  async function resolveConvertTargetFormat(options) {
234
142
  const config = require('../core/config');
235
143
  const effectiveFormat = options.format || (await config.getFormat());
@@ -316,149 +224,6 @@ function setupSplitJsonConvertShowCommands(program) {
316
224
  setupShowCommand(program);
317
225
  }
318
226
 
319
- function emitManifestAfterValidateIfApplicable(appOrFile, result, outFormat) {
320
- if (outFormat === 'json' || !result?.appPath || !appOrFile || typeof appOrFile !== 'string') {
321
- return;
322
- }
323
- try {
324
- if (fs.existsSync(appOrFile) && fs.statSync(appOrFile).isFile()) {
325
- return;
326
- }
327
- const { emitManifestMetadataLineIfTTY } = require('../utils/manifest-source-emit');
328
- emitManifestMetadataLineIfTTY(logger, {
329
- appKey: appOrFile,
330
- appPath: result.appPath,
331
- envOnly: false,
332
- json: false
333
- });
334
- } catch {
335
- /* ignore emit failures */
336
- }
337
- }
338
-
339
- async function runValidateBatchBranch(validate, opts) {
340
- const integration = opts.integration === true;
341
- const builder = opts.builder === true;
342
- if (!integration && !builder) {
343
- return false;
344
- }
345
- const outFormat = (opts.format || 'default').toLowerCase();
346
- const batchResult = integration && builder
347
- ? await validate.validateAll(opts)
348
- : integration
349
- ? await validate.validateAllIntegrations(opts)
350
- : await validate.validateAllBuilderApps(opts);
351
- if (outFormat === 'json') {
352
- logger.log(JSON.stringify(batchResult, null, 2));
353
- } else {
354
- validate.displayBatchValidationResults(batchResult);
355
- }
356
- if (!batchResult.valid) process.exit(1);
357
- return true;
358
- }
359
-
360
- async function runProtectionValidateBranch(appOrFile, opts) {
361
- const { validateProtectionBatchCommand } = require('../protection/validate-batch');
362
- const code = await validateProtectionBatchCommand(
363
- {
364
- env: opts.env,
365
- json: (opts.format || '').toLowerCase() === 'json',
366
- warningsAsErrors: opts.warningsAsErrors === true,
367
- simulate: opts.simulate === true,
368
- verbose: opts.verbose === true
369
- },
370
- logger
371
- );
372
- if (code !== 0) {
373
- process.exit(code);
374
- }
375
- }
376
-
377
- function buildValidateJsonPayload(result, appOrFile) {
378
- let payload = result;
379
- try {
380
- if (result?.appPath && appOrFile && typeof appOrFile === 'string') {
381
- if (!(fs.existsSync(appOrFile) && fs.statSync(appOrFile).isFile())) {
382
- const { getManifestSourcePayload } = require('../utils/manifest-source-emit');
383
- payload = { ...result, manifestSource: getManifestSourcePayload(appOrFile, result.appPath) };
384
- }
385
- }
386
- } catch {
387
- /* keep plain result */
388
- }
389
- return payload;
390
- }
391
-
392
- function displaySingleValidateResult(appOrFile, result, outFormat, validate) {
393
- emitManifestAfterValidateIfApplicable(appOrFile, result, outFormat);
394
- if (outFormat === 'json') {
395
- logger.log(JSON.stringify(buildValidateJsonPayload(result, appOrFile), null, 2));
396
- return;
397
- }
398
- validate.displayValidationResults(result);
399
- }
400
-
401
- async function runValidateCommand(appOrFile, options) {
402
- const validate = require('../validation/validate');
403
- const opts = options.opts ? options.opts() : options;
404
- const { isProtectionScope } = require('../protection/scope');
405
- if (isProtectionScope(appOrFile)) {
406
- await runProtectionValidateBranch(appOrFile, opts);
407
- return;
408
- }
409
- if (await runValidateBatchBranch(validate, opts)) {
410
- return;
411
- }
412
- const outFormat = (opts.format || 'default').toLowerCase();
413
- if (!appOrFile || typeof appOrFile !== 'string') {
414
- logger.log(chalk.red('App name or file path is required, or use --integration / --builder'));
415
- process.exit(1);
416
- }
417
- const result = await validate.validateAppOrFile(appOrFile, {
418
- ...opts,
419
- certSync: opts.certSync === true
420
- });
421
- displaySingleValidateResult(appOrFile, result, outFormat, validate);
422
- if (!result.valid) process.exit(1);
423
- }
424
-
425
- function setupValidateDiffCommands(program) {
426
- program.command('validate [appKey|systemKey|file]')
427
- .description('Validate appKey, systemKey, file path, .protection batch, or all under integration/ and builder/')
428
- .addHelpText('after', VALIDATE_HELP_AFTER)
429
- .option('--format <format>', 'Output format: json | default (human-readable)')
430
- .option('--integration', 'Validate all applications under integration/')
431
- .option('--builder', 'Validate all applications under builder/')
432
- .option('--warnings-as-errors', 'For .protection: pass strict=true to dataplane validate')
433
- .option('--simulate', 'For .protection: run simulate after validate')
434
- .option(
435
- '--cert-sync',
436
- 'After successful validation of an external integration, refresh the system file certification block from the dataplane (requires login)'
437
- )
438
- .action((appOrFile, options) => {
439
- runValidateCommand(appOrFile, options).catch((error) => {
440
- handleCommandError(error, 'validate');
441
- process.exit(1);
442
- });
443
- });
444
-
445
- program.command('diff <file1> <file2>')
446
- .description('Diff two config files (optional schema validate)')
447
- .option('--no-validate', 'Skip schema validation (type check still applied)')
448
- .action(async(file1, file2, cmd) => {
449
- try {
450
- const diff = require('../core/diff');
451
- const opts = cmd.opts();
452
- const result = await diff.compareFiles(file1, file2, { validate: opts.validate !== false });
453
- diff.formatDiffOutput(result);
454
- if (!result.identical) process.exit(1);
455
- } catch (error) {
456
- handleCommandError(error, 'diff');
457
- process.exit(1);
458
- }
459
- });
460
- }
461
-
462
227
  /**
463
228
  * Sets up utility commands
464
229
  * @param {Command} program - Commander program instance
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Client token listing and proactive refresh for auth status.
3
+ *
4
+ * @fileoverview Stored app tokens, auto-refresh when credentials exist
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const chalk = require('chalk');
12
+ const config = require('../core/config');
13
+ const logger = require('../utils/logger');
14
+ const { successGlyph, failureGlyph } = require('../utils/cli-test-layout-chalk');
15
+ const {
16
+ getOrRefreshClientToken,
17
+ loadClientCredentials,
18
+ refreshClientToken,
19
+ isTokenExpired
20
+ } = require('../utils/token-manager');
21
+
22
+ /**
23
+ * @param {string} controllerUrl
24
+ * @param {string} storedController
25
+ * @returns {boolean}
26
+ */
27
+ function controllersMatch(controllerUrl, storedController) {
28
+ if (!storedController) {
29
+ return false;
30
+ }
31
+ return config.normalizeControllerUrl(storedController) === config.normalizeControllerUrl(controllerUrl);
32
+ }
33
+
34
+ /**
35
+ * @param {string} environment
36
+ * @param {string} controllerUrl
37
+ * @returns {Promise<Record<string, { controller?: string, token?: string, expiresAt?: string }>>}
38
+ */
39
+ async function getClientEntriesForController(environment, controllerUrl) {
40
+ const configData = await config.getConfig();
41
+ const clients = configData.environments?.[environment]?.clients || {};
42
+ const out = {};
43
+ for (const [appName, tokenData] of Object.entries(clients)) {
44
+ if (controllersMatch(controllerUrl, tokenData?.controller)) {
45
+ out[appName] = tokenData;
46
+ }
47
+ }
48
+ return out;
49
+ }
50
+
51
+ /**
52
+ * Refresh expired client tokens when secrets.local.yaml (or env) has client id/secret.
53
+ *
54
+ * @async
55
+ * @param {string} environment
56
+ * @param {string} controllerUrl
57
+ * @returns {Promise<number>} Count refreshed
58
+ */
59
+ async function refreshExpiredClientTokensWherePossible(environment, controllerUrl) {
60
+ const entries = await getClientEntriesForController(environment, controllerUrl);
61
+ let refreshed = 0;
62
+ for (const appName of Object.keys(entries)) {
63
+ const tokenData = entries[appName];
64
+ if (!tokenData?.token || !isTokenExpired(tokenData.expiresAt)) {
65
+ continue;
66
+ }
67
+ const credentials = await loadClientCredentials(appName);
68
+ if (!credentials?.clientId || !credentials?.clientSecret) {
69
+ continue;
70
+ }
71
+ try {
72
+ await refreshClientToken(environment, appName, controllerUrl);
73
+ refreshed += 1;
74
+ } catch {
75
+ // Skip apps without valid credentials or controller errors
76
+ }
77
+ }
78
+ return refreshed;
79
+ }
80
+
81
+ /**
82
+ * @param {string} expiresAt
83
+ * @returns {'valid'|'expired'|'unknown'}
84
+ */
85
+ function tokenExpiryState(expiresAt) {
86
+ if (!expiresAt) {
87
+ return 'unknown';
88
+ }
89
+ return isTokenExpired(expiresAt) ? 'expired' : 'valid';
90
+ }
91
+
92
+ /**
93
+ * Build rows for stored client tokens table.
94
+ *
95
+ * @async
96
+ * @param {string} environment
97
+ * @param {string} controllerUrl
98
+ * @returns {Promise<Array<{ appName: string, expiresAt: string, state: string, logoutCmd: string }>>}
99
+ */
100
+ async function listClientTokenRows(environment, controllerUrl) {
101
+ const entries = await getClientEntriesForController(environment, controllerUrl);
102
+ const rows = [];
103
+ for (const appName of Object.keys(entries).sort()) {
104
+ const tokenData = entries[appName];
105
+ const state = tokenExpiryState(tokenData.expiresAt);
106
+ const credentials = await loadClientCredentials(appName);
107
+ const canRefresh = !!(credentials?.clientId && credentials?.clientSecret);
108
+ let stateLabel = state;
109
+ if (state === 'expired' && canRefresh) {
110
+ stateLabel = 'expired (credentials available — refreshed on status if possible)';
111
+ } else if (state === 'expired' && !canRefresh) {
112
+ stateLabel = 'expired (no client id/secret in secrets — logout or login)';
113
+ }
114
+ rows.push({
115
+ appName,
116
+ expiresAt: tokenData.expiresAt || '—',
117
+ state: stateLabel,
118
+ logoutCmd: `aifabrix logout -e ${environment} -a ${appName}`
119
+ });
120
+ }
121
+ return rows;
122
+ }
123
+
124
+ /**
125
+ * Print stored application client tokens and logout commands.
126
+ *
127
+ * @async
128
+ * @param {string} environment
129
+ * @param {string} controllerUrl
130
+ * @param {number} [refreshedCount]
131
+ * @returns {Promise<void>}
132
+ */
133
+ async function displayStoredClientTokensSection(environment, controllerUrl, refreshedCount = 0) {
134
+ const rows = await listClientTokenRows(environment, controllerUrl);
135
+ if (rows.length === 0) {
136
+ return;
137
+ }
138
+
139
+ logger.log('');
140
+ logger.log(chalk.bold('Stored application client tokens:'));
141
+ if (refreshedCount > 0) {
142
+ logger.log(
143
+ chalk.gray(
144
+ ` Refreshed ${refreshedCount} expired token(s) using client id/secret from secrets.local.yaml or env.`
145
+ )
146
+ );
147
+ }
148
+ logger.log(chalk.gray(` Environment: ${environment} · Controller: ${controllerUrl}`));
149
+ logger.log('');
150
+ for (const row of rows) {
151
+ const icon = row.state.startsWith('valid') ? successGlyph() : failureGlyph();
152
+ logger.log(` ${icon} ${chalk.cyan(row.appName)}`);
153
+ logger.log(` Expires: ${chalk.gray(row.expiresAt)}`);
154
+ logger.log(` Status: ${row.state}`);
155
+ logger.log(` Clear: ${chalk.gray(row.logoutCmd)}`);
156
+ }
157
+ logger.log('');
158
+ logger.log(chalk.gray(' Clear all client tokens for this environment:'), chalk.gray(`aifabrix logout -e ${environment}`));
159
+ logger.log(chalk.gray(' Clear device login for this controller:'), chalk.gray(`aifabrix logout -c ${controllerUrl}`));
160
+ logger.log('');
161
+ }
162
+
163
+ /**
164
+ * Resolve a working client token (refresh when expired if credentials exist).
165
+ *
166
+ * @async
167
+ * @param {string} controllerUrl
168
+ * @param {string} environment
169
+ * @param {function(string, string, string, string, string): Promise<Object>} validateClientTokenFn
170
+ * @returns {Promise<Object|null>}
171
+ */
172
+ async function tryResolveClientTokenAuth(controllerUrl, environment, validateClientTokenFn) {
173
+ const entries = await getClientEntriesForController(environment, controllerUrl);
174
+ const appNames = Object.keys(entries);
175
+ if (appNames.length === 0) {
176
+ return null;
177
+ }
178
+
179
+ const ordered = ['dataplane', ...appNames.filter(a => a !== 'dataplane').sort()];
180
+ let lastFailure = null;
181
+
182
+ for (const appName of ordered) {
183
+ try {
184
+ const refreshed = await getOrRefreshClientToken(environment, appName, controllerUrl);
185
+ if (!refreshed?.token) {
186
+ continue;
187
+ }
188
+ const stored = await config.getClientToken(environment, appName);
189
+ const result = await validateClientTokenFn(
190
+ refreshed.token,
191
+ controllerUrl,
192
+ environment,
193
+ appName,
194
+ stored?.expiresAt
195
+ );
196
+ if (result.authenticated) {
197
+ return result;
198
+ }
199
+ lastFailure = result;
200
+ } catch {
201
+ // Try next app
202
+ }
203
+ }
204
+
205
+ return lastFailure;
206
+ }
207
+
208
+ module.exports = {
209
+ refreshExpiredClientTokensWherePossible,
210
+ displayStoredClientTokensSection,
211
+ tryResolveClientTokenAuth,
212
+ listClientTokenRows,
213
+ getClientEntriesForController
214
+ };
@@ -24,6 +24,28 @@ const {
24
24
  } = require('../utils/dataplane-cli-version-gate');
25
25
  const { isAtLeast, isValidSemver } = require('../utils/semver-compare');
26
26
 
27
+ /** Max wall time for post-login version cache refresh (silent after success banner). */
28
+ const LOGIN_POST_REFRESH_BUDGET_MS = 12000;
29
+
30
+ /** Per health probe during login refresh (two endpoints may be tried). */
31
+ const LOGIN_HEALTH_PROBE_TIMEOUT_MS = 6000;
32
+
33
+ /**
34
+ * Resolve when `promise` settles or when `timeoutMs` elapses (returns undefined).
35
+ * @template T
36
+ * @param {Promise<T>} promise
37
+ * @param {number} timeoutMs
38
+ * @returns {Promise<T|undefined>}
39
+ */
40
+ function raceWithTimeout(promise, timeoutMs) {
41
+ return Promise.race([
42
+ promise,
43
+ new Promise((resolve) => {
44
+ setTimeout(() => resolve(undefined), timeoutMs);
45
+ })
46
+ ]);
47
+ }
48
+
27
49
  /**
28
50
  * Per plan 142.0: invalid semver from the dataplane is logged but does **not**
29
51
  * block the CLI; missing min version means no enforcement.
@@ -72,13 +94,17 @@ async function safeReadCache(controllerUrl) {
72
94
  * compatible: boolean
73
95
  * }>}
74
96
  */
75
- async function refreshDataplaneVersionInfo(controllerUrl, dataplaneUrl) {
97
+ async function refreshDataplaneVersionInfo(controllerUrl, dataplaneUrl, options = {}) {
76
98
  const cliVersion = getInstalledCliVersion();
77
99
  let dataplaneVersion;
78
100
  let minBuilderCliVersion;
101
+ const healthOpts = {};
102
+ if (options.healthTimeoutMs !== undefined && options.healthTimeoutMs !== null) {
103
+ healthOpts.timeoutMs = options.healthTimeoutMs;
104
+ }
79
105
 
80
106
  try {
81
- const snapshot = await fetchDataplaneGeneralHealth(dataplaneUrl);
107
+ const snapshot = await fetchDataplaneGeneralHealth(dataplaneUrl, healthOpts);
82
108
  if (snapshot) {
83
109
  dataplaneVersion = snapshot.version;
84
110
  minBuilderCliVersion = snapshot.minBuilderCliVersion;
@@ -195,29 +221,38 @@ async function resolveDataplaneUrlAfterLogin(controllerUrl, environment, authCon
195
221
  * @param {string} [loginOptions.app] - App name (credentials login) for client-token auth
196
222
  * @returns {Promise<void>}
197
223
  */
224
+ async function tryRefreshDataplaneVersionAfterLoginWork(controllerUrl, loginOptions = {}) {
225
+ const { resolveEnvironment } = require('../core/config');
226
+ const environment = await resolveEnvironment();
227
+ const authConfig = await resolveAuthConfigAfterLogin(
228
+ controllerUrl,
229
+ environment,
230
+ loginOptions
231
+ );
232
+ if (!authConfig) {
233
+ return;
234
+ }
235
+ const dataplaneUrl = await resolveDataplaneUrlAfterLogin(
236
+ controllerUrl,
237
+ environment,
238
+ authConfig
239
+ );
240
+ if (dataplaneUrl) {
241
+ await refreshDataplaneVersionInfo(controllerUrl, dataplaneUrl, {
242
+ healthTimeoutMs: LOGIN_HEALTH_PROBE_TIMEOUT_MS
243
+ });
244
+ }
245
+ }
246
+
198
247
  async function tryRefreshDataplaneVersionAfterLogin(controllerUrl, loginOptions = {}) {
199
248
  if (!controllerUrl || typeof controllerUrl !== 'string') {
200
249
  return;
201
250
  }
202
251
  try {
203
- const { resolveEnvironment } = require('../core/config');
204
- const environment = await resolveEnvironment();
205
- const authConfig = await resolveAuthConfigAfterLogin(
206
- controllerUrl,
207
- environment,
208
- loginOptions
209
- );
210
- if (!authConfig) {
211
- return;
212
- }
213
- const dataplaneUrl = await resolveDataplaneUrlAfterLogin(
214
- controllerUrl,
215
- environment,
216
- authConfig
252
+ await raceWithTimeout(
253
+ tryRefreshDataplaneVersionAfterLoginWork(controllerUrl, loginOptions),
254
+ LOGIN_POST_REFRESH_BUDGET_MS
217
255
  );
218
- if (dataplaneUrl) {
219
- await refreshDataplaneVersionInfo(controllerUrl, dataplaneUrl);
220
- }
221
256
  } catch (_error) {
222
257
  // best-effort only
223
258
  }
@@ -227,5 +262,8 @@ module.exports = {
227
262
  refreshDataplaneVersionInfo,
228
263
  loadCachedVersionInfo,
229
264
  computeCompatibility,
230
- tryRefreshDataplaneVersionAfterLogin
265
+ tryRefreshDataplaneVersionAfterLogin,
266
+ LOGIN_POST_REFRESH_BUDGET_MS,
267
+ LOGIN_HEALTH_PROBE_TIMEOUT_MS,
268
+ raceWithTimeout
231
269
  };