@aifabrix/builder 2.54.1 → 2.55.2

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 (253) hide show
  1. package/README.md +8 -0
  2. package/lib/agent-kit/apply-ops.js +82 -0
  3. package/lib/agent-kit/checksums.js +28 -0
  4. package/lib/agent-kit/classify.js +113 -0
  5. package/lib/agent-kit/claude-shim.js +61 -0
  6. package/lib/agent-kit/constants.js +55 -0
  7. package/lib/agent-kit/file-list.js +103 -0
  8. package/lib/agent-kit/host-approve.js +172 -0
  9. package/lib/agent-kit/hosting/github.js +63 -0
  10. package/lib/agent-kit/hosts.js +71 -0
  11. package/lib/agent-kit/init.js +319 -0
  12. package/lib/agent-kit/kit-root.js +17 -0
  13. package/lib/agent-kit/managed-section.js +79 -0
  14. package/lib/agent-kit/package-load.js +45 -0
  15. package/lib/agent-kit/plan-ops.js +69 -0
  16. package/lib/agent-kit/remote-prompts.js +272 -0
  17. package/lib/agent-kit/render-host-markdown.js +40 -0
  18. package/lib/agent-kit/repo-guard.js +47 -0
  19. package/lib/agent-kit/report.js +107 -0
  20. package/lib/agent-kit/run.js +82 -0
  21. package/lib/agent-kit/scm/git.js +122 -0
  22. package/lib/agent-kit/start.js +310 -0
  23. package/lib/agent-kit/state-file.js +83 -0
  24. package/lib/agent-kit/status.js +50 -0
  25. package/lib/agent-kit/v1-detect.js +81 -0
  26. package/lib/agent-kit/validate-kit.js +87 -0
  27. package/lib/agent-kit/workspace-name.js +40 -0
  28. package/lib/agent-kit/workspace-skeleton.js +103 -0
  29. package/lib/api/role-assistant-test-pack.api.js +86 -0
  30. package/lib/api/work-search.api.js +31 -4
  31. package/lib/api/work.api.js +19 -12
  32. package/lib/app/deploy.js +7 -4
  33. package/lib/app/list-external-systems.js +2 -1
  34. package/lib/app/offer-create-from-template.js +184 -0
  35. package/lib/app/show-display.js +33 -6
  36. package/lib/app/show.js +25 -2
  37. package/lib/channels/add-channel.js +1 -0
  38. package/lib/channels/approval-guides/claude.js +2 -2
  39. package/lib/channels/channel-add-failure.js +1 -0
  40. package/lib/channels/channel-add-remaining.js +15 -9
  41. package/lib/channels/channel-definition-status.js +115 -0
  42. package/lib/channels/channel-description.js +2 -1
  43. package/lib/channels/channel-install-envelope.js +4 -2
  44. package/lib/channels/channel-integration-generator.js +2 -0
  45. package/lib/channels/channel-runtime-resolver.js +14 -0
  46. package/lib/channels/channel-targets.js +31 -5
  47. package/lib/channels/claude-setup-values.js +4 -1
  48. package/lib/channels/slack-channel-add.js +59 -6
  49. package/lib/channels/slack-mcp-callback.js +51 -0
  50. package/lib/channels/slack-mcp-runtime.js +156 -0
  51. package/lib/channels/slack-provisioning/install-cli.js +14 -2
  52. package/lib/channels/slack-provisioning/with-curl-on-path.js +6 -1
  53. package/lib/cli/index.js +4 -0
  54. package/lib/cli/setup-app.js +8 -1
  55. package/lib/cli/setup-auth.js +11 -2
  56. package/lib/cli/setup-certification-commands.js +2 -2
  57. package/lib/cli/setup-certification.help.js +3 -1
  58. package/lib/commands/agent-kit-help.js +149 -0
  59. package/lib/commands/agent-kit.js +417 -0
  60. package/lib/commands/auth-status-user.js +99 -0
  61. package/lib/commands/auth-status.js +14 -4
  62. package/lib/commands/channel-add-help.js +5 -5
  63. package/lib/commands/datasource-capability-relate-cli.js +5 -4
  64. package/lib/commands/datasource-load-export-cli.js +7 -2
  65. package/lib/commands/datasource-test-trust-cli.js +5 -1
  66. package/lib/commands/governance-command-action.js +5 -4
  67. package/lib/commands/identity-membership-role-cache.js +6 -1
  68. package/lib/commands/lifecycle-command-action.js +16 -15
  69. package/lib/commands/login-credentials.js +8 -3
  70. package/lib/commands/login-device.js +14 -5
  71. package/lib/commands/login.js +14 -1
  72. package/lib/commands/logout.js +50 -15
  73. package/lib/commands/repair-auth-apply.js +1 -1
  74. package/lib/commands/repair-datasource-key-skip.js +39 -0
  75. package/lib/commands/repair-datasource-keys.js +24 -12
  76. package/lib/commands/repair-rbac.js +16 -10
  77. package/lib/commands/role-assistant-copy-output.js +61 -0
  78. package/lib/commands/role-assistant-copy.js +155 -0
  79. package/lib/commands/role-assistant-test-output.js +8 -9
  80. package/lib/commands/role-assistant-test-result-rows.js +42 -0
  81. package/lib/commands/role-assistant.js +140 -59
  82. package/lib/commands/search.js +143 -0
  83. package/lib/commands/test-trust-external.js +7 -5
  84. package/lib/commands/upload-sidecar-followups.js +4 -1
  85. package/lib/commands/verify-governance-command-action.js +9 -7
  86. package/lib/commands/verify-operations-command-action.js +9 -8
  87. package/lib/commands/verify-operations-skip-e2e.js +131 -0
  88. package/lib/commands/verify-operations-step-plan.js +6 -33
  89. package/lib/commands/verify-operations-steps.js +3 -1
  90. package/lib/commands/verify-trust-command-action.js +6 -5
  91. package/lib/datasource/capability/relate-operations.js +28 -10
  92. package/lib/datasource/capability/run-capability-relate.js +48 -25
  93. package/lib/datasource/datasource-identifier.js +13 -5
  94. package/lib/deployment/deployer-poll-run.js +117 -35
  95. package/lib/external-system/deploy-dataplane-published.js +70 -0
  96. package/lib/external-system/deploy.js +12 -2
  97. package/lib/external-system/download-role-assistant.js +41 -15
  98. package/lib/external-system/download.js +8 -3
  99. package/lib/generator/helpers.js +12 -1
  100. package/lib/generator/wizard-datasource-key.js +54 -0
  101. package/lib/generator/wizard.js +55 -54
  102. package/lib/integration-definition/apply-datasource-keys.js +75 -0
  103. package/lib/integration-definition/apply-relationships.js +195 -0
  104. package/lib/integration-definition/apply-static-enterprise.js +232 -0
  105. package/lib/integration-definition/apply-system-file.js +71 -0
  106. package/lib/integration-definition/apply-test-endpoint.js +89 -0
  107. package/lib/integration-definition/apply-viewpoint-defaults.js +157 -0
  108. package/lib/integration-definition/apply.js +78 -59
  109. package/lib/integration-definition/enrich-openapi-operations.js +36 -10
  110. package/lib/integration-definition/enrich-openapi-pick.js +87 -0
  111. package/lib/lifecycle/role-assistant-governance.js +56 -21
  112. package/lib/programmatic/capability-registry.js +1 -0
  113. package/lib/programmatic/index.js +2 -0
  114. package/lib/programmatic/openapi-apply-result-schemas.js +8 -0
  115. package/lib/programmatic/openapi-channel-install-schemas.js +4 -1
  116. package/lib/programmatic/openapi-component-schemas.js +1 -0
  117. package/lib/programmatic/openapi-descriptions.js +9 -3
  118. package/lib/programmatic/openapi-envelope-examples.js +7 -0
  119. package/lib/programmatic/openapi-operation-bindings.js +5 -0
  120. package/lib/programmatic/openapi-request-schemas.js +5 -0
  121. package/lib/programmatic/openapi-schema-descriptions-channel.js +8 -5
  122. package/lib/programmatic/openapi-schema-descriptions-operations.js +19 -0
  123. package/lib/programmatic/openapi-spec.js +1 -0
  124. package/lib/programmatic/request-timeout.js +3 -0
  125. package/lib/programmatic/route-handler-map.js +7 -0
  126. package/lib/programmatic/run-channel-catalog.js +50 -8
  127. package/lib/programmatic/run-channel-install-options.js +5 -1
  128. package/lib/programmatic/run-download.js +41 -0
  129. package/lib/protection/auth-context.js +3 -1
  130. package/lib/protection/protection-display.js +3 -2
  131. package/lib/protection/report-exit.js +50 -0
  132. package/lib/protection/run-commands.js +2 -0
  133. package/lib/role-assistant/knowledge-sync-settings-push.js +172 -0
  134. package/lib/role-assistant/knowledge-sync.js +9 -126
  135. package/lib/role-assistant/package-validate.js +175 -0
  136. package/lib/role-assistant/test-case-assertion-row.js +36 -0
  137. package/lib/role-assistant/test-case-copy-bind.js +77 -0
  138. package/lib/role-assistant/test-case-copy-expected.js +127 -0
  139. package/lib/role-assistant/test-case-copy-from-work.js +90 -0
  140. package/lib/role-assistant/test-case-copy-interactions.js +67 -0
  141. package/lib/role-assistant/test-case-copy.js +119 -0
  142. package/lib/role-assistant/test-case-expected-catalog.js +135 -0
  143. package/lib/role-assistant/test-case-validate.js +88 -0
  144. package/lib/role-assistant/test-cases-bid.js +117 -4
  145. package/lib/role-assistant/test-cases-evaluate.js +149 -31
  146. package/lib/role-assistant/test-cases-evidence.js +44 -5
  147. package/lib/role-assistant/test-cases-honesty.js +155 -6
  148. package/lib/role-assistant/test-cases-lifecycle.js +297 -0
  149. package/lib/role-assistant/test-cases-search-graph.js +122 -0
  150. package/lib/role-assistant/test-cases-search.js +20 -2
  151. package/lib/role-assistant/test-cases.js +34 -168
  152. package/lib/role-assistant/test-pack-map.js +180 -0
  153. package/lib/role-assistant/test-pack-sync.js +187 -0
  154. package/lib/role-assistant/test-runner-debug.js +54 -0
  155. package/lib/role-assistant/test-runner-respond-body.js +36 -0
  156. package/lib/role-assistant/test-runner-search.js +20 -5
  157. package/lib/role-assistant/test-runner-start-payload.js +63 -10
  158. package/lib/role-assistant/test-runner-workhub-answers.js +45 -16
  159. package/lib/role-assistant/test-runner-workhub.js +45 -21
  160. package/lib/role-assistant/test-runner.js +53 -10
  161. package/lib/schema/external-datasource.schema.json +36 -5
  162. package/lib/schema/external-system.schema.json +20 -2
  163. package/lib/schema/infra-parameter.schema.json +51 -3
  164. package/lib/schema/infra.parameter.yaml +476 -40
  165. package/lib/schema/role-assistant-test-case.schema.json +408 -0
  166. package/lib/schema/type/document-storage.json +1 -1
  167. package/lib/search/dataplane-context.js +23 -0
  168. package/lib/search/search-args.js +211 -0
  169. package/lib/search/search-display.js +238 -0
  170. package/lib/search/search-errors.js +50 -0
  171. package/lib/search/search-prompts.js +119 -0
  172. package/lib/search/search-roles.js +85 -0
  173. package/lib/search/search-run.js +241 -0
  174. package/lib/search/search-skills.js +198 -0
  175. package/lib/utils/api-required-permissions.js +271 -0
  176. package/lib/utils/api.js +16 -13
  177. package/lib/utils/authentication-slot-show.js +153 -0
  178. package/lib/utils/cli-json-exit.js +41 -0
  179. package/lib/utils/cli-utils.js +23 -5
  180. package/lib/utils/controller-url.js +78 -4
  181. package/lib/utils/credential-display.js +43 -1
  182. package/lib/utils/deployment-errors.js +15 -0
  183. package/lib/utils/deployment-validation-helpers.js +4 -1
  184. package/lib/utils/device-code.js +31 -10
  185. package/lib/utils/error-formatters/error-parser.js +16 -1
  186. package/lib/utils/error-formatters/permission-errors.js +19 -5
  187. package/lib/utils/external-system-readiness-compact-blocks.js +5 -1
  188. package/lib/utils/external-system-readiness-compact-display.js +30 -1
  189. package/lib/utils/external-system-validators.js +2 -1
  190. package/lib/utils/help-builder.js +3 -1
  191. package/lib/utils/keycloak-realm-ready.js +5 -2
  192. package/lib/utils/logger.js +1 -0
  193. package/lib/utils/paths.js +26 -5
  194. package/lib/utils/schema-loader.js +37 -0
  195. package/lib/utils/secrets-helpers.js +2 -2
  196. package/lib/utils/write-stream-and-wait.js +63 -0
  197. package/lib/validation/validate-external-file.js +5 -1
  198. package/lib/validation/validate.js +3 -2
  199. package/package.json +20 -6
  200. package/templates/agent-kit/adapters/claude/nested.CLAUDE.md +1 -0
  201. package/templates/agent-kit/adapters/claude/root.CLAUDE.md +5 -0
  202. package/templates/agent-kit/adapters/cursor/commands/cust-0-plan.md +5 -0
  203. package/templates/agent-kit/adapters/cursor/commands/cust-1-validate.md +5 -0
  204. package/templates/agent-kit/adapters/cursor/commands/cust-2-build.md +5 -0
  205. package/templates/agent-kit/adapters/cursor/commands/cust-3-push.md +5 -0
  206. package/templates/agent-kit/adapters/cursor/commands/cust-biz-1-validate.md +5 -0
  207. package/templates/agent-kit/adapters/cursor/commands/cust-biz-2-build.md +5 -0
  208. package/templates/agent-kit/adapters/cursor/commands/cust-biz-3-prove.md +5 -0
  209. package/templates/agent-kit/agent-kit.yaml +45 -0
  210. package/templates/agent-kit/instructions/AGENTKIT.md +104 -0
  211. package/templates/agent-kit/instructions/integration.AGENTS.md +25 -0
  212. package/templates/agent-kit/instructions/role-assistant.AGENTS.md +22 -0
  213. package/templates/agent-kit/instructions/root.AGENTS.md +37 -0
  214. package/templates/agent-kit/parity-inventory.yaml +188 -0
  215. package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +59 -0
  216. package/templates/agent-kit/skills/aifabrix-connected-system/references/contract-checklist.md +40 -0
  217. package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +54 -0
  218. package/templates/agent-kit/skills/aifabrix-connected-system/references/production-safety.md +35 -0
  219. package/templates/agent-kit/skills/aifabrix-connected-system/scripts/reserved-system-key.js +19 -0
  220. package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +52 -0
  221. package/templates/agent-kit/skills/aifabrix-plan/references/challenge-checklist.md +108 -0
  222. package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +34 -0
  223. package/templates/agent-kit/skills/aifabrix-plan/references/plan-template.md +182 -0
  224. package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +51 -0
  225. package/templates/agent-kit/skills/aifabrix-prove/references/demo-template.md +65 -0
  226. package/templates/agent-kit/skills/aifabrix-prove/references/evidence-lifecycle.md +68 -0
  227. package/templates/agent-kit/skills/aifabrix-prove/references/learning-template.md +66 -0
  228. package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +43 -0
  229. package/templates/agent-kit/skills/aifabrix-role-assistant/references/package-boundary.md +21 -0
  230. package/templates/agent-kit/skills/shared/hosts.md +65 -0
  231. package/templates/agent-kit/skills/shared/login.md +16 -0
  232. package/templates/agent-kit/workspace/.cursor/plans/README.md +22 -0
  233. package/templates/agent-kit/workspace/LICENSE.mit +21 -0
  234. package/templates/agent-kit/workspace/README.md +89 -0
  235. package/templates/applications/builder-api/Dockerfile +3 -1
  236. package/templates/applications/dataplane/application.yaml +2 -3
  237. package/templates/applications/dataplane/env.template +5 -5
  238. package/templates/applications/dataplane/rbac.yaml +28 -0
  239. package/templates/applications/keycloak/env.template +3 -3
  240. package/templates/applications/miso-controller/application.yaml +21 -2
  241. package/templates/applications/miso-controller/env.template +96 -25
  242. package/templates/applications/miso-controller/rbac.yaml +5 -0
  243. package/templates/channels/claude/TROUBLESHOOTING.md +3 -0
  244. package/templates/channels/claude/channel.json +1 -1
  245. package/templates/channels/claude/install.template.md.hbs +1 -0
  246. package/templates/channels/claude/integration/claude-system.json +1 -1
  247. package/templates/channels/slack/channel.json +11 -0
  248. package/templates/channels/slack/install.template.md.hbs +9 -0
  249. package/templates/channels/slack/integration/slack-system.json +11 -0
  250. package/templates/channels/slack/manifest.template.json.hbs +6 -3
  251. package/templates/channels/teams-bot/channel.json +1 -0
  252. package/templates/python/docker-compose.hbs +14 -0
  253. package/templates/typescript/docker-compose.hbs +14 -0
package/lib/utils/api.js CHANGED
@@ -98,7 +98,12 @@ function parseErrorText(errorText, status, statusText) {
98
98
  async function handleErrorResponse(response, url, options, duration) {
99
99
  const errorText = await response.text();
100
100
  const errorData = parseErrorText(errorText, response.status, response.statusText);
101
- const parsedError = parseErrorResponse(errorData, response.status, false);
101
+ const { enrichForbiddenErrorData } = require('./api-required-permissions');
102
+ const parsedError = parseErrorResponse(enrichForbiddenErrorData(errorData, {
103
+ status: response.status,
104
+ method: options && options.method,
105
+ url
106
+ }), response.status, false);
102
107
 
103
108
  await logApiPerformance({
104
109
  url,
@@ -406,21 +411,14 @@ function setAuthHeader(headers, token, authType) {
406
411
  * @param {string} [tokenOrAuthConfig.controller] - Controller URL for token refresh (if object)
407
412
  * @returns {Promise<Object>} Response object
408
413
  */
409
- /**
410
- * Retry once after device-token refresh on 401 (Bearer only; never for client-token/preserveBearer).
411
- * @param {Object} response
412
- * @param {string} url
413
- * @param {Object} options
414
- * @param {Object} headers
415
- * @param {string|null} authControllerUrl
416
- * @returns {Promise<Object>}
417
- */
414
+ /** Retry once after device-token refresh on 401 (Bearer only). */
418
415
  async function retryApiCallAfterDeviceTokenRefresh(
419
416
  response,
420
417
  url,
421
418
  options,
422
419
  headers,
423
- authControllerUrl
420
+ authControllerUrl,
421
+ authConfig
424
422
  ) {
425
423
  try {
426
424
  const { forceRefreshDeviceToken } = require('./token-manager');
@@ -429,6 +427,11 @@ async function retryApiCallAfterDeviceTokenRefresh(
429
427
  );
430
428
  if (refreshedToken?.token) {
431
429
  headers.Authorization = `Bearer ${refreshedToken.token}`;
430
+ // Keep long-running commands current instead of reusing the expired bearer.
431
+ if (authConfig && typeof authConfig === 'object') {
432
+ authConfig.token = refreshedToken.token;
433
+ authConfig.controller = refreshedToken.controller || authControllerUrl;
434
+ }
432
435
  return await makeApiCall(url, { ...options, headers });
433
436
  }
434
437
  const authError =
@@ -471,7 +474,8 @@ async function authenticatedApiCall(url, options = {}, tokenOrAuthConfig) {
471
474
  url,
472
475
  options,
473
476
  headers,
474
- authControllerUrl
477
+ authControllerUrl,
478
+ isStringToken ? null : tokenOrAuthConfig
475
479
  );
476
480
  }
477
481
 
@@ -492,4 +496,3 @@ module.exports = {
492
496
  displayDeviceCodeInfo,
493
497
  refreshDeviceToken
494
498
  };
495
-
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Connected System credential line for `aifabrix show` (usage slots + online/offline).
3
+ *
4
+ * @fileoverview TTY formatter for authentications[] / legacy authentication
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const chalk = require('chalk');
12
+ const { getAuthMethodFromAuthentication } = require('./authentication-shape');
13
+
14
+ /** Same labels as the dataplane Connected System authentication tab. */
15
+ const AUTHENTICATION_USAGE_LABELS = Object.freeze({
16
+ api: 'API calls',
17
+ webhookInbound: 'Webhook inbound',
18
+ syncExecution: 'Sync execution',
19
+ messageInbound: 'Message inbound',
20
+ messageOutbound: 'Message outbound',
21
+ callback: 'Callback',
22
+ provisioning: 'Provisioning'
23
+ });
24
+
25
+ /**
26
+ * @param {string} method
27
+ * @param {string} glyph
28
+ * @returns {string}
29
+ */
30
+ function chalkGlyph(method, glyph) {
31
+ const fn = chalk[method];
32
+ return typeof fn === 'function' ? fn(glyph) : glyph;
33
+ }
34
+
35
+ /**
36
+ * @param {string|null|undefined} status
37
+ * @returns {string}
38
+ */
39
+ function formatOnlineOfflineIcon(status) {
40
+ const raw = String(status || 'offline').trim().toLowerCase();
41
+ if (raw === 'online') return chalkGlyph('green', '✔');
42
+ if (raw === 'partially') return chalkGlyph('yellow', '⚠');
43
+ return chalkGlyph('red', '✖');
44
+ }
45
+
46
+ /**
47
+ * @param {unknown} value
48
+ * @returns {Object|null}
49
+ */
50
+ function asObject(value) {
51
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
52
+ }
53
+
54
+ /**
55
+ * @param {Object} slot
56
+ * @returns {string}
57
+ */
58
+ function formatSlotType(slot) {
59
+ const authType = String(slot.authType || '').trim().toLowerCase();
60
+ if (authType === 'system' || authType === 'none') return authType;
61
+ if (authType === 'apiprovider') return 'apiProvider';
62
+ const method = getAuthMethodFromAuthentication(slot);
63
+ if (method) return method;
64
+ const nested = asObject(slot.credential);
65
+ const nestedMethod = nested && typeof nested.method === 'string' ? nested.method.trim() : '';
66
+ return nestedMethod || authType || '—';
67
+ }
68
+
69
+ /**
70
+ * @param {Object} slot
71
+ * @param {string|null|undefined} systemStatus
72
+ * @returns {string}
73
+ */
74
+ function slotStatus(slot, systemStatus) {
75
+ const authType = String(slot.authType || '').trim().toLowerCase();
76
+ if (authType === 'system' || authType === 'none') return 'online';
77
+ return systemStatus || 'offline';
78
+ }
79
+
80
+ /**
81
+ * @param {Object} slot
82
+ * @param {string|null|undefined} systemStatus
83
+ * @param {boolean} includeUsage
84
+ * @returns {string}
85
+ */
86
+ function formatOneSlot(slot, systemStatus, includeUsage) {
87
+ const type = formatSlotType(slot);
88
+ const icon = formatOnlineOfflineIcon(slotStatus(slot, systemStatus));
89
+ if (!includeUsage) return `${type} ${icon}`;
90
+ const usage = String(slot.usage || 'api').trim() || 'api';
91
+ const label = AUTHENTICATION_USAGE_LABELS[usage] || usage;
92
+ return `${label}: ${type} ${icon}`;
93
+ }
94
+
95
+ /**
96
+ * @param {unknown} entry
97
+ * @returns {Object|null}
98
+ */
99
+ function normalizeSlot(entry) {
100
+ const obj = asObject(entry);
101
+ if (!obj) return null;
102
+ const key = typeof obj.key === 'string' && obj.key.trim() ? obj.key.trim() : 'default';
103
+ const usage = typeof obj.usage === 'string' && obj.usage.trim() ? obj.usage.trim() : 'api';
104
+ return { ...obj, key, usage };
105
+ }
106
+
107
+ /**
108
+ * @param {Object|null|undefined} config
109
+ * @param {Object|null|undefined} system
110
+ * @returns {Object[]}
111
+ */
112
+ function resolveAuthenticationSlots(config, system) {
113
+ const cfg = asObject(config) || {};
114
+ const sys = asObject(system) || {};
115
+ const fromCfg = Array.isArray(cfg.authentications) && cfg.authentications.length > 0
116
+ ? cfg.authentications
117
+ : null;
118
+ const fromSys = Array.isArray(sys.authentications) && sys.authentications.length > 0
119
+ ? sys.authentications
120
+ : null;
121
+ const rawSlots = fromCfg || fromSys;
122
+ if (rawSlots) return rawSlots.map(normalizeSlot).filter(Boolean);
123
+ const legacy = asObject(cfg.authentication) || asObject(sys.authentication);
124
+ if (!legacy) return [];
125
+ return [normalizeSlot({ ...legacy, key: 'default', usage: 'api' })];
126
+ }
127
+
128
+ /**
129
+ * One slot: `oauth2 ✔`. Several: `API calls: oauth2 ✔, Webhook inbound: hmac ✔`.
130
+ * @param {Object} ext
131
+ * @returns {string}
132
+ */
133
+ function formatCredentialShowLine(ext) {
134
+ if (!ext || typeof ext !== 'object') return '—';
135
+ const slots = Array.isArray(ext.authenticationSlots) ? ext.authenticationSlots : [];
136
+ const status = ext.authenticationStatus;
137
+ if (slots.length === 1) return formatOneSlot(slots[0], status, false);
138
+ if (slots.length > 1) {
139
+ return slots.map((slot) => formatOneSlot(slot, status, true)).join(', ');
140
+ }
141
+ const method = typeof ext.credentialMethod === 'string' ? ext.credentialMethod.trim() : '';
142
+ if (method && method.toLowerCase() !== 'unknown') {
143
+ return `${method} ${formatOnlineOfflineIcon(status)}`;
144
+ }
145
+ return '—';
146
+ }
147
+
148
+ module.exports = {
149
+ AUTHENTICATION_USAGE_LABELS,
150
+ resolveAuthenticationSlots,
151
+ formatCredentialShowLine,
152
+ formatOnlineOfflineIcon
153
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Truncation-safe JSON stdout write + exit for CLI commands.
3
+ *
4
+ * @fileoverview Shared helper for `--json` command output followed by a
5
+ * non-zero process exit.
6
+ * @author AI Fabrix Team
7
+ * @version 1.0.0
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const logger = require('./logger');
13
+ const { writeStreamAndWait } = require('./write-stream-and-wait');
14
+
15
+ /**
16
+ * Write a JSON envelope to stdout and exit with the given code, without
17
+ * truncating the output.
18
+ *
19
+ * Writes to a piped (non-TTY) stdout are asynchronous on Linux; calling
20
+ * `process.exit()` immediately after `logger.log()`/`console.log()` can
21
+ * kill the process before the OS finishes flushing a large write, silently
22
+ * truncating the JSON. Waiting for the `stdout.write` callback before
23
+ * exiting avoids that. Applies `logger.sanitizeValue` first, when available,
24
+ * so token/secret masking still happens (bypassing `logger.log()` skips it
25
+ * otherwise) — falls back to an identity function for test doubles that
26
+ * mock `logger` without it.
27
+ *
28
+ * @param {unknown} payload - Value to serialize as the JSON envelope.
29
+ * @param {number} exitCode - Exit code; 0 resolves without exiting.
30
+ * @returns {Promise<void>}
31
+ */
32
+ async function writeJsonEnvelopeAndExit(payload, exitCode) {
33
+ const sanitize = typeof logger.sanitizeValue === 'function' ? logger.sanitizeValue : v => v;
34
+ const json = `${JSON.stringify(sanitize(payload), null, 2)}\n`;
35
+ await writeStreamAndWait(process.stdout, json);
36
+ if (exitCode !== 0) {
37
+ process.exit(exitCode);
38
+ }
39
+ }
40
+
41
+ module.exports = { writeJsonEnvelopeAndExit };
@@ -83,9 +83,11 @@ function isPortConflictError(errorMsg) {
83
83
  * @returns {boolean} True if permission denied error
84
84
  */
85
85
  function isPermissionDeniedError(errorMsg) {
86
- return (errorMsg.includes('permission denied') || errorMsg.includes('EACCES') || errorMsg.includes('Permission denied')) &&
87
- !errorMsg.includes('permissions/') &&
88
- !errorMsg.includes('Field "permissions');
86
+ const text = String(errorMsg || '');
87
+ const lower = text.toLowerCase();
88
+ return (lower.includes('permission denied') || lower.includes('eacces')) &&
89
+ !text.includes('permissions/') &&
90
+ !text.includes('Field "permissions');
89
91
  }
90
92
 
91
93
  /**
@@ -287,7 +289,7 @@ function formatApiPermissionError(errorMsg) {
287
289
  if (isLocalFilesystemPermissionDeniedError(errorMsg)) return null;
288
290
  return [
289
291
  ` ${errorMsg}`,
290
- ' Ensure your token has the required permission (e.g. external-system:delete for delete).'
292
+ ' Assign the required permission on the Controller or Dataplane for this environment (roles and groups).'
291
293
  ];
292
294
  }
293
295
 
@@ -418,11 +420,27 @@ function isAuthenticationError(error) {
418
420
  * @param {Error} error - The error that occurred
419
421
  * @param {string} command - Command that failed
420
422
  */
423
+ function appendCommandPermissionHint(errorMessages, command) {
424
+ const blob = Array.isArray(errorMessages) ? errorMessages.join('\n') : '';
425
+ if (!isPermissionDeniedError(blob)) return errorMessages;
426
+ if (/Required permissions:|Missing permissions:|Required permission \(/.test(blob)) {
427
+ return errorMessages;
428
+ }
429
+ const { lookupCommandRequiredPermission } = require('./api-required-permissions');
430
+ const hit = lookupCommandRequiredPermission(command);
431
+ if (!hit) return errorMessages;
432
+ return [
433
+ ...errorMessages,
434
+ ` Required permission (${hit.service}): ${hit.permissions.join(', ')}`,
435
+ ` Assign it on the ${hit.service} for this environment (roles and groups).`
436
+ ];
437
+ }
438
+
421
439
  function handleCommandError(error, command) {
422
440
  if (error && error.compactGateAbortLogged === true) {
423
441
  return;
424
442
  }
425
- const errorMessages = formatError(error);
443
+ const errorMessages = appendCommandPermissionHint(formatError(error), command);
426
444
  const skipDoctor = isMissingSecretsErrorMessage(error && error.message);
427
445
  logError(command, errorMessages, { skipDoctor });
428
446
  if (error.wizardResumeMessage) {
@@ -14,6 +14,10 @@ const devConfig = require('./dev-config');
14
14
  const config = require('../core/config');
15
15
  const { getDeviceToken, isTokenExpired } = require('./token-manager');
16
16
 
17
+ /** Default Front Door / Traefik mount for Miso Controller on public hosts. */
18
+ const DEFAULT_CONTROLLER_VDIR = '/miso';
19
+ const CONTROLLER_HEALTH_TIMEOUT_MS = 2500;
20
+
17
21
  /**
18
22
  * Calculate default controller URL based on developer ID
19
23
  * Uses getDevPorts to get the app port which is adjusted by developer ID
@@ -132,14 +136,58 @@ async function isPlatformAuthValidForController(controllerUrl) {
132
136
  }
133
137
 
134
138
  /**
135
- * Best-effort GET /health on the controller (no auth). Used to avoid token refresh HTTP
136
- * during setup before Miso Controller is running.
139
+ * True for local controller ports (no Front Door /miso vdir).
140
+ * @param {string} hostname
141
+ * @returns {boolean}
142
+ */
143
+ function isLoopbackControllerHost(hostname) {
144
+ const host = String(hostname || '').toLowerCase();
145
+ return host === 'localhost' || host === '127.0.0.1' || host === '::1';
146
+ }
147
+
148
+ /**
149
+ * @param {string} pathname
150
+ * @returns {boolean}
151
+ */
152
+ function isMisoVdirPath(pathname) {
153
+ return pathname === DEFAULT_CONTROLLER_VDIR
154
+ || pathname.startsWith(`${DEFAULT_CONTROLLER_VDIR}/`);
155
+ }
156
+
157
+ /**
158
+ * Strip extra path segments. Public hosts always use origin + /miso.
159
+ * Loopback keeps origin unless the path is already /miso.
137
160
  *
161
+ * @param {string} url
162
+ * @returns {string}
163
+ */
164
+ function canonicalizeControllerLoginUrl(url) {
165
+ const base = normalizeUrl(url);
166
+ if (!base) {
167
+ return url;
168
+ }
169
+ let parsed;
170
+ try {
171
+ parsed = new URL(base);
172
+ } catch {
173
+ return base;
174
+ }
175
+ const origin = `${parsed.protocol}//${parsed.host}`;
176
+ const pathname = (parsed.pathname || '/').replace(/\/+$/, '') || '/';
177
+ if (isLoopbackControllerHost(parsed.hostname)) {
178
+ return isMisoVdirPath(pathname) ? `${origin}${DEFAULT_CONTROLLER_VDIR}` : origin;
179
+ }
180
+ return `${origin}${DEFAULT_CONTROLLER_VDIR}`;
181
+ }
182
+
183
+ /**
184
+ * Best-effort GET /health on the controller (no auth).
138
185
  * @async
139
186
  * @param {string} controllerUrl
187
+ * @param {number} timeoutMs
140
188
  * @returns {Promise<boolean>}
141
189
  */
142
- async function isControllerHealthReachable(controllerUrl) {
190
+ async function probeControllerHealth(controllerUrl, timeoutMs) {
143
191
  const base = normalizeUrl(controllerUrl);
144
192
  if (!base) {
145
193
  return false;
@@ -147,7 +195,7 @@ async function isControllerHealthReachable(controllerUrl) {
147
195
  try {
148
196
  const { fetchWithOptionalDevPlatformTls } = require('./dev-platform-tls');
149
197
  const res = await fetchWithOptionalDevPlatformTls(`${base}/health`, {
150
- signal: AbortSignal.timeout(2500)
198
+ signal: AbortSignal.timeout(timeoutMs)
151
199
  });
152
200
  return res.ok;
153
201
  } catch {
@@ -155,6 +203,29 @@ async function isControllerHealthReachable(controllerUrl) {
155
203
  }
156
204
  }
157
205
 
206
+ /**
207
+ * Best-effort GET /health on the controller (no auth). Used to avoid token refresh HTTP
208
+ * during setup before Miso Controller is running.
209
+ *
210
+ * @async
211
+ * @param {string} controllerUrl
212
+ * @returns {Promise<boolean>}
213
+ */
214
+ async function isControllerHealthReachable(controllerUrl) {
215
+ return probeControllerHealth(controllerUrl, CONTROLLER_HEALTH_TIMEOUT_MS);
216
+ }
217
+
218
+ /**
219
+ * Resolve a controller base URL for login: public hosts always origin + /miso.
220
+ *
221
+ * @async
222
+ * @param {string} controllerUrl
223
+ * @returns {Promise<string>} Canonical controller URL
224
+ */
225
+ async function discoverControllerBaseUrl(controllerUrl) {
226
+ return canonicalizeControllerLoginUrl(controllerUrl);
227
+ }
228
+
158
229
  /**
159
230
  * Get controller URL from logged-in user's device tokens.
160
231
  * Prefers the entry under {@link config.controller} when it matches a `device` key; otherwise a
@@ -237,11 +308,14 @@ async function resolveControllerUrl() {
237
308
  }
238
309
 
239
310
  module.exports = {
311
+ DEFAULT_CONTROLLER_VDIR,
240
312
  getDefaultControllerUrl,
241
313
  hasStoredDeviceTokenForController,
242
314
  isDeviceTokenUsableForController,
243
315
  isPlatformAuthValidForController,
244
316
  isControllerHealthReachable,
317
+ canonicalizeControllerLoginUrl,
318
+ discoverControllerBaseUrl,
245
319
  getControllerUrlFromLoggedInUser,
246
320
  getControllerFromConfig,
247
321
  resolveControllerUrl
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  const chalk = require('chalk');
11
+ const { formatCredentialShowLine } = require('./authentication-slot-show');
11
12
 
12
13
  /** @type {{ verified: string, pending: string, failed: string, expired: string }} */
13
14
  const STATUS_ICONS = {
@@ -74,10 +75,51 @@ function formatCredentialWithStatus(credential) {
74
75
  return { key, name, statusFormatted, statusLabel };
75
76
  }
76
77
 
78
+ /**
79
+ * @param {unknown[]} values
80
+ * @returns {string|null}
81
+ */
82
+ function firstNonEmptyScalar(values) {
83
+ for (const value of values) {
84
+ if (typeof value === 'number') return String(value);
85
+ if (typeof value === 'string' && value.trim()) return value.trim();
86
+ }
87
+ return null;
88
+ }
89
+
90
+ /**
91
+ * Identity from a dataplane credential id or ForeignKeyReference. Never `[object Object]`.
92
+ * @param {unknown} value
93
+ * @returns {string|null}
94
+ */
95
+ function formatCredentialIdentity(value) {
96
+ if (value === null || value === undefined || value === '') return null;
97
+ if (typeof value !== 'object') return String(value);
98
+ return firstNonEmptyScalar([
99
+ value.name,
100
+ value.displayName,
101
+ value.key,
102
+ value.id,
103
+ value.credentialKey,
104
+ value.credentialId
105
+ ]);
106
+ }
107
+
108
+ /**
109
+ * Scalar credential id/key for show output. Objects must not print as [object Object].
110
+ * @param {unknown} value
111
+ * @returns {string}
112
+ */
113
+ function formatCredentialIdValue(value) {
114
+ return formatCredentialIdentity(value) || '—';
115
+ }
116
+
77
117
  module.exports = {
78
118
  STATUS_ICONS,
79
119
  STATUS_LABELS,
80
120
  STATUS_CHALK,
81
121
  formatCredentialStatus,
82
- formatCredentialWithStatus
122
+ formatCredentialWithStatus,
123
+ formatCredentialIdValue,
124
+ formatCredentialShowLine
83
125
  };
@@ -193,6 +193,17 @@ function createFormattedError(parsedError, safeError, originalError) {
193
193
  return formattedError;
194
194
  }
195
195
 
196
+ function createPreservedFormattedError(error, safeError) {
197
+ if (!error || typeof error.formatted !== 'string' || !error.formatted.trim()) {
198
+ return null;
199
+ }
200
+ return createFormattedError({
201
+ message: error.message || error.formatted,
202
+ formatted: error.formatted,
203
+ data: error.data
204
+ }, safeError, error);
205
+ }
206
+
196
207
  /**
197
208
  * Unified error handler for deployment errors
198
209
  * Handles audit logging, error formatting, and user-friendly messages
@@ -227,6 +238,10 @@ async function handleDeploymentErrors(error, appName, url, alreadyLogged = false
227
238
  }
228
239
 
229
240
  const safeError = handleDeploymentError(error);
241
+ const preserved = createPreservedFormattedError(error, safeError);
242
+ if (preserved) {
243
+ throw preserved;
244
+ }
230
245
  const errorResponse = extractErrorResponse(safeError, error);
231
246
  const parsedError = parseErrorResponseSafely(errorResponse, safeError);
232
247
 
@@ -49,7 +49,10 @@ function processValidationFailure(responseData) {
49
49
  function processValidationError(response) {
50
50
  const error = new Error(`Validation request failed: ${response.formattedError || response.error || 'Unknown error'}`);
51
51
  error.status = response.status || 400;
52
- error.data = response.data;
52
+ error.data = response.errorData || response.data;
53
+ if (response.formattedError) {
54
+ error.formatted = response.formattedError;
55
+ }
53
56
  throw error;
54
57
  }
55
58
 
@@ -128,25 +128,45 @@ function buildVerificationUrlWithUserCode(verificationUri, userCode) {
128
128
  }
129
129
 
130
130
  /**
131
- * Displays device code information to the user
132
- * Formats user code and verification URL for easy reading. Uses a URL with user_code in the query
133
- * so the device page can pre-fill the code and the user does not need to type it.
131
+ * Full visit URL for device login (complete URI when the controller sends one).
132
+ *
133
+ * @param {Object} deviceCodeResponse
134
+ * @param {string} [deviceCodeResponse.verification_uri_complete]
135
+ * @param {string} [deviceCodeResponse.verification_uri]
136
+ * @param {string} [deviceCodeResponse.user_code]
137
+ * @returns {string}
138
+ */
139
+ function resolveDeviceVisitUrl(deviceCodeResponse) {
140
+ const complete = deviceCodeResponse && deviceCodeResponse.verification_uri_complete;
141
+ if (complete) {
142
+ return complete;
143
+ }
144
+ return buildVerificationUrlWithUserCode(
145
+ deviceCodeResponse && deviceCodeResponse.verification_uri,
146
+ deviceCodeResponse && deviceCodeResponse.user_code
147
+ );
148
+ }
149
+
150
+ /**
151
+ * Displays device code information to the user.
152
+ * Prints a visit URL with user_code so the device page can pre-fill the code.
134
153
  *
135
154
  * @function displayDeviceCodeInfo
136
155
  * @param {string} userCode - User code to display
137
156
  * @param {string} verificationUri - Verification URL (base, without user_code)
138
157
  * @param {Object} logger - Logger instance with log method
139
158
  * @param {Object} chalk - Chalk instance for colored output
159
+ * @param {string} [verificationUriComplete] - Full verification URL when provided by the controller
140
160
  */
141
- function displayDeviceCodeInfo(userCode, verificationUri, logger, chalk) {
142
- const visitUrl = buildVerificationUrlWithUserCode(verificationUri, userCode);
161
+ function displayDeviceCodeInfo(userCode, verificationUri, logger, chalk, verificationUriComplete) {
162
+ const visitUrl = verificationUriComplete
163
+ || buildVerificationUrlWithUserCode(verificationUri, userCode);
143
164
  logger.log(chalk.cyan('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
144
165
  logger.log(chalk.cyan(' Device Code Flow Authentication'));
145
166
  logger.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
146
- logger.log(chalk.yellow('To complete authentication:'));
147
- logger.log(chalk.gray(' 1. Visit (code is in the URL): ') + chalk.blue.underline(visitUrl));
148
- logger.log(chalk.gray(' 2. Approve the request\n'));
149
- logger.log(chalk.gray('Waiting for approval...'));
167
+ logger.log(chalk.yellow('Open this URL to authenticate:'));
168
+ logger.log(chalk.blue.underline(visitUrl));
169
+ logger.log(chalk.gray('\nApprove the request in the browser. Waiting for approval...'));
150
170
  }
151
171
 
152
172
  /** Timeout for token refresh request (ms). Longer than default to allow for slow controller/Keycloak. */
@@ -197,5 +217,6 @@ module.exports = {
197
217
  displayDeviceCodeInfo,
198
218
  refreshDeviceToken,
199
219
  parseTokenResponse,
200
- buildVerificationUrlWithUserCode
220
+ buildVerificationUrlWithUserCode,
221
+ resolveDeviceVisitUrl
201
222
  };
@@ -177,6 +177,21 @@ function handleGenericError(errorData, statusCode) {
177
177
  return createErrorResult('generic', errorData.message || errorData.error || 'Unknown error', formatGenericError(errorData, statusCode), errorData);
178
178
  }
179
179
 
180
+ function unwrapNestedErrorData(errorData) {
181
+ if (!errorData || !errorData.data || typeof errorData.data !== 'object') {
182
+ return errorData;
183
+ }
184
+ const nested = errorData.data;
185
+ return {
186
+ ...nested,
187
+ cliPermissionService: errorData.cliPermissionService || nested.cliPermissionService,
188
+ requiredPermissions: errorData.requiredPermissions || nested.requiredPermissions,
189
+ method: errorData.method || nested.method,
190
+ url: nested.instance || nested.url || errorData.url,
191
+ instance: nested.instance || errorData.instance
192
+ };
193
+ }
194
+
180
195
  /**
181
196
  * Parses error response and determines error type
182
197
  * @param {string|Object} errorResponse - Error response (string or parsed JSON)
@@ -200,7 +215,7 @@ function parseErrorResponse(errorResponse, statusCode, isNetworkError) {
200
215
 
201
216
  // Handle nested response structure (some APIs wrap errors in data field)
202
217
  if (errorData.data && typeof errorData.data === 'object') {
203
- errorData = errorData.data;
218
+ errorData = unwrapNestedErrorData(errorData);
204
219
  }
205
220
 
206
221
  // Handle network errors
@@ -42,6 +42,9 @@ function extractRequiredPermissions(errorData) {
42
42
  if (errorData.requiredPermissions && Array.isArray(errorData.requiredPermissions)) {
43
43
  return errorData.requiredPermissions;
44
44
  }
45
+ if (typeof errorData.requiredPermission === 'string' && errorData.requiredPermission.trim()) {
46
+ return [errorData.requiredPermission.trim()];
47
+ }
45
48
  if (errorData.required && errorData.required.permissions && Array.isArray(errorData.required.permissions)) {
46
49
  return errorData.required.permissions;
47
50
  }
@@ -97,6 +100,19 @@ function getPermissionDetailLines(errorData) {
97
100
  return lines;
98
101
  }
99
102
 
103
+ function appendPermissionAssignHint(lines, errorData) {
104
+ const service = errorData.cliPermissionService;
105
+ if (service) {
106
+ lines.push(chalk.gray(`Required on: ${service}`));
107
+ }
108
+ lines.push(chalk.gray(
109
+ service
110
+ ? `Assign this permission on the ${service} for this environment (roles and groups).`
111
+ : 'Assign the required permission on the Controller or Dataplane for this environment (roles and groups).'
112
+ ));
113
+ lines.push('');
114
+ }
115
+
100
116
  /**
101
117
  * Formats permission error with missing and required permissions
102
118
  * @param {Object} errorData - Error response data
@@ -111,11 +127,9 @@ function formatPermissionError(errorData) {
111
127
  lines.push('');
112
128
  }
113
129
 
114
- const missingPerms = extractMissingPermissions(errorData);
115
- addPermissionList(lines, missingPerms, 'Missing permissions');
116
-
117
- const requiredPerms = extractRequiredPermissions(errorData);
118
- addPermissionList(lines, requiredPerms, 'Required permissions');
130
+ addPermissionList(lines, extractMissingPermissions(errorData), 'Missing permissions');
131
+ addPermissionList(lines, extractRequiredPermissions(errorData), 'Required permissions');
132
+ appendPermissionAssignHint(lines, errorData);
119
133
 
120
134
  const requestUrl = errorData.instance || errorData.url;
121
135
  const method = errorData.method || 'POST';