@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.
- package/README.md +8 -0
- package/lib/agent-kit/apply-ops.js +82 -0
- package/lib/agent-kit/checksums.js +28 -0
- package/lib/agent-kit/classify.js +113 -0
- package/lib/agent-kit/claude-shim.js +61 -0
- package/lib/agent-kit/constants.js +55 -0
- package/lib/agent-kit/file-list.js +103 -0
- package/lib/agent-kit/host-approve.js +172 -0
- package/lib/agent-kit/hosting/github.js +63 -0
- package/lib/agent-kit/hosts.js +71 -0
- package/lib/agent-kit/init.js +319 -0
- package/lib/agent-kit/kit-root.js +17 -0
- package/lib/agent-kit/managed-section.js +79 -0
- package/lib/agent-kit/package-load.js +45 -0
- package/lib/agent-kit/plan-ops.js +69 -0
- package/lib/agent-kit/remote-prompts.js +272 -0
- package/lib/agent-kit/render-host-markdown.js +40 -0
- package/lib/agent-kit/repo-guard.js +47 -0
- package/lib/agent-kit/report.js +107 -0
- package/lib/agent-kit/run.js +82 -0
- package/lib/agent-kit/scm/git.js +122 -0
- package/lib/agent-kit/start.js +310 -0
- package/lib/agent-kit/state-file.js +83 -0
- package/lib/agent-kit/status.js +50 -0
- package/lib/agent-kit/v1-detect.js +81 -0
- package/lib/agent-kit/validate-kit.js +87 -0
- package/lib/agent-kit/workspace-name.js +40 -0
- package/lib/agent-kit/workspace-skeleton.js +103 -0
- package/lib/api/role-assistant-test-pack.api.js +86 -0
- package/lib/api/work-search.api.js +31 -4
- package/lib/api/work.api.js +19 -12
- package/lib/app/deploy.js +7 -4
- package/lib/app/list-external-systems.js +2 -1
- package/lib/app/offer-create-from-template.js +184 -0
- package/lib/app/show-display.js +33 -6
- package/lib/app/show.js +25 -2
- package/lib/channels/add-channel.js +1 -0
- package/lib/channels/approval-guides/claude.js +2 -2
- package/lib/channels/channel-add-failure.js +1 -0
- package/lib/channels/channel-add-remaining.js +15 -9
- package/lib/channels/channel-definition-status.js +115 -0
- package/lib/channels/channel-description.js +2 -1
- package/lib/channels/channel-install-envelope.js +4 -2
- package/lib/channels/channel-integration-generator.js +2 -0
- package/lib/channels/channel-runtime-resolver.js +14 -0
- package/lib/channels/channel-targets.js +31 -5
- package/lib/channels/claude-setup-values.js +4 -1
- package/lib/channels/slack-channel-add.js +59 -6
- package/lib/channels/slack-mcp-callback.js +51 -0
- package/lib/channels/slack-mcp-runtime.js +156 -0
- package/lib/channels/slack-provisioning/install-cli.js +14 -2
- package/lib/channels/slack-provisioning/with-curl-on-path.js +6 -1
- package/lib/cli/index.js +4 -0
- package/lib/cli/setup-app.js +8 -1
- package/lib/cli/setup-auth.js +11 -2
- package/lib/cli/setup-certification-commands.js +2 -2
- package/lib/cli/setup-certification.help.js +3 -1
- package/lib/commands/agent-kit-help.js +149 -0
- package/lib/commands/agent-kit.js +417 -0
- package/lib/commands/auth-status-user.js +99 -0
- package/lib/commands/auth-status.js +14 -4
- package/lib/commands/channel-add-help.js +5 -5
- package/lib/commands/datasource-capability-relate-cli.js +5 -4
- package/lib/commands/datasource-load-export-cli.js +7 -2
- package/lib/commands/datasource-test-trust-cli.js +5 -1
- package/lib/commands/governance-command-action.js +5 -4
- package/lib/commands/identity-membership-role-cache.js +6 -1
- package/lib/commands/lifecycle-command-action.js +16 -15
- package/lib/commands/login-credentials.js +8 -3
- package/lib/commands/login-device.js +14 -5
- package/lib/commands/login.js +14 -1
- package/lib/commands/logout.js +50 -15
- package/lib/commands/repair-auth-apply.js +1 -1
- package/lib/commands/repair-datasource-key-skip.js +39 -0
- package/lib/commands/repair-datasource-keys.js +24 -12
- package/lib/commands/repair-rbac.js +16 -10
- package/lib/commands/role-assistant-copy-output.js +61 -0
- package/lib/commands/role-assistant-copy.js +155 -0
- package/lib/commands/role-assistant-test-output.js +8 -9
- package/lib/commands/role-assistant-test-result-rows.js +42 -0
- package/lib/commands/role-assistant.js +140 -59
- package/lib/commands/search.js +143 -0
- package/lib/commands/test-trust-external.js +7 -5
- package/lib/commands/upload-sidecar-followups.js +4 -1
- package/lib/commands/verify-governance-command-action.js +9 -7
- package/lib/commands/verify-operations-command-action.js +9 -8
- package/lib/commands/verify-operations-skip-e2e.js +131 -0
- package/lib/commands/verify-operations-step-plan.js +6 -33
- package/lib/commands/verify-operations-steps.js +3 -1
- package/lib/commands/verify-trust-command-action.js +6 -5
- package/lib/datasource/capability/relate-operations.js +28 -10
- package/lib/datasource/capability/run-capability-relate.js +48 -25
- package/lib/datasource/datasource-identifier.js +13 -5
- package/lib/deployment/deployer-poll-run.js +117 -35
- package/lib/external-system/deploy-dataplane-published.js +70 -0
- package/lib/external-system/deploy.js +12 -2
- package/lib/external-system/download-role-assistant.js +41 -15
- package/lib/external-system/download.js +8 -3
- package/lib/generator/helpers.js +12 -1
- package/lib/generator/wizard-datasource-key.js +54 -0
- package/lib/generator/wizard.js +55 -54
- package/lib/integration-definition/apply-datasource-keys.js +75 -0
- package/lib/integration-definition/apply-relationships.js +195 -0
- package/lib/integration-definition/apply-static-enterprise.js +232 -0
- package/lib/integration-definition/apply-system-file.js +71 -0
- package/lib/integration-definition/apply-test-endpoint.js +89 -0
- package/lib/integration-definition/apply-viewpoint-defaults.js +157 -0
- package/lib/integration-definition/apply.js +78 -59
- package/lib/integration-definition/enrich-openapi-operations.js +36 -10
- package/lib/integration-definition/enrich-openapi-pick.js +87 -0
- package/lib/lifecycle/role-assistant-governance.js +56 -21
- package/lib/programmatic/capability-registry.js +1 -0
- package/lib/programmatic/index.js +2 -0
- package/lib/programmatic/openapi-apply-result-schemas.js +8 -0
- package/lib/programmatic/openapi-channel-install-schemas.js +4 -1
- package/lib/programmatic/openapi-component-schemas.js +1 -0
- package/lib/programmatic/openapi-descriptions.js +9 -3
- package/lib/programmatic/openapi-envelope-examples.js +7 -0
- package/lib/programmatic/openapi-operation-bindings.js +5 -0
- package/lib/programmatic/openapi-request-schemas.js +5 -0
- package/lib/programmatic/openapi-schema-descriptions-channel.js +8 -5
- package/lib/programmatic/openapi-schema-descriptions-operations.js +19 -0
- package/lib/programmatic/openapi-spec.js +1 -0
- package/lib/programmatic/request-timeout.js +3 -0
- package/lib/programmatic/route-handler-map.js +7 -0
- package/lib/programmatic/run-channel-catalog.js +50 -8
- package/lib/programmatic/run-channel-install-options.js +5 -1
- package/lib/programmatic/run-download.js +41 -0
- package/lib/protection/auth-context.js +3 -1
- package/lib/protection/protection-display.js +3 -2
- package/lib/protection/report-exit.js +50 -0
- package/lib/protection/run-commands.js +2 -0
- package/lib/role-assistant/knowledge-sync-settings-push.js +172 -0
- package/lib/role-assistant/knowledge-sync.js +9 -126
- package/lib/role-assistant/package-validate.js +175 -0
- package/lib/role-assistant/test-case-assertion-row.js +36 -0
- package/lib/role-assistant/test-case-copy-bind.js +77 -0
- package/lib/role-assistant/test-case-copy-expected.js +127 -0
- package/lib/role-assistant/test-case-copy-from-work.js +90 -0
- package/lib/role-assistant/test-case-copy-interactions.js +67 -0
- package/lib/role-assistant/test-case-copy.js +119 -0
- package/lib/role-assistant/test-case-expected-catalog.js +135 -0
- package/lib/role-assistant/test-case-validate.js +88 -0
- package/lib/role-assistant/test-cases-bid.js +117 -4
- package/lib/role-assistant/test-cases-evaluate.js +149 -31
- package/lib/role-assistant/test-cases-evidence.js +44 -5
- package/lib/role-assistant/test-cases-honesty.js +155 -6
- package/lib/role-assistant/test-cases-lifecycle.js +297 -0
- package/lib/role-assistant/test-cases-search-graph.js +122 -0
- package/lib/role-assistant/test-cases-search.js +20 -2
- package/lib/role-assistant/test-cases.js +34 -168
- package/lib/role-assistant/test-pack-map.js +180 -0
- package/lib/role-assistant/test-pack-sync.js +187 -0
- package/lib/role-assistant/test-runner-debug.js +54 -0
- package/lib/role-assistant/test-runner-respond-body.js +36 -0
- package/lib/role-assistant/test-runner-search.js +20 -5
- package/lib/role-assistant/test-runner-start-payload.js +63 -10
- package/lib/role-assistant/test-runner-workhub-answers.js +45 -16
- package/lib/role-assistant/test-runner-workhub.js +45 -21
- package/lib/role-assistant/test-runner.js +53 -10
- package/lib/schema/external-datasource.schema.json +36 -5
- package/lib/schema/external-system.schema.json +20 -2
- package/lib/schema/infra-parameter.schema.json +51 -3
- package/lib/schema/infra.parameter.yaml +476 -40
- package/lib/schema/role-assistant-test-case.schema.json +408 -0
- package/lib/schema/type/document-storage.json +1 -1
- package/lib/search/dataplane-context.js +23 -0
- package/lib/search/search-args.js +211 -0
- package/lib/search/search-display.js +238 -0
- package/lib/search/search-errors.js +50 -0
- package/lib/search/search-prompts.js +119 -0
- package/lib/search/search-roles.js +85 -0
- package/lib/search/search-run.js +241 -0
- package/lib/search/search-skills.js +198 -0
- package/lib/utils/api-required-permissions.js +271 -0
- package/lib/utils/api.js +16 -13
- package/lib/utils/authentication-slot-show.js +153 -0
- package/lib/utils/cli-json-exit.js +41 -0
- package/lib/utils/cli-utils.js +23 -5
- package/lib/utils/controller-url.js +78 -4
- package/lib/utils/credential-display.js +43 -1
- package/lib/utils/deployment-errors.js +15 -0
- package/lib/utils/deployment-validation-helpers.js +4 -1
- package/lib/utils/device-code.js +31 -10
- package/lib/utils/error-formatters/error-parser.js +16 -1
- package/lib/utils/error-formatters/permission-errors.js +19 -5
- package/lib/utils/external-system-readiness-compact-blocks.js +5 -1
- package/lib/utils/external-system-readiness-compact-display.js +30 -1
- package/lib/utils/external-system-validators.js +2 -1
- package/lib/utils/help-builder.js +3 -1
- package/lib/utils/keycloak-realm-ready.js +5 -2
- package/lib/utils/logger.js +1 -0
- package/lib/utils/paths.js +26 -5
- package/lib/utils/schema-loader.js +37 -0
- package/lib/utils/secrets-helpers.js +2 -2
- package/lib/utils/write-stream-and-wait.js +63 -0
- package/lib/validation/validate-external-file.js +5 -1
- package/lib/validation/validate.js +3 -2
- package/package.json +20 -6
- package/templates/agent-kit/adapters/claude/nested.CLAUDE.md +1 -0
- package/templates/agent-kit/adapters/claude/root.CLAUDE.md +5 -0
- package/templates/agent-kit/adapters/cursor/commands/cust-0-plan.md +5 -0
- package/templates/agent-kit/adapters/cursor/commands/cust-1-validate.md +5 -0
- package/templates/agent-kit/adapters/cursor/commands/cust-2-build.md +5 -0
- package/templates/agent-kit/adapters/cursor/commands/cust-3-push.md +5 -0
- package/templates/agent-kit/adapters/cursor/commands/cust-biz-1-validate.md +5 -0
- package/templates/agent-kit/adapters/cursor/commands/cust-biz-2-build.md +5 -0
- package/templates/agent-kit/adapters/cursor/commands/cust-biz-3-prove.md +5 -0
- package/templates/agent-kit/agent-kit.yaml +45 -0
- package/templates/agent-kit/instructions/AGENTKIT.md +104 -0
- package/templates/agent-kit/instructions/integration.AGENTS.md +25 -0
- package/templates/agent-kit/instructions/role-assistant.AGENTS.md +22 -0
- package/templates/agent-kit/instructions/root.AGENTS.md +37 -0
- package/templates/agent-kit/parity-inventory.yaml +188 -0
- package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +59 -0
- package/templates/agent-kit/skills/aifabrix-connected-system/references/contract-checklist.md +40 -0
- package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +54 -0
- package/templates/agent-kit/skills/aifabrix-connected-system/references/production-safety.md +35 -0
- package/templates/agent-kit/skills/aifabrix-connected-system/scripts/reserved-system-key.js +19 -0
- package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +52 -0
- package/templates/agent-kit/skills/aifabrix-plan/references/challenge-checklist.md +108 -0
- package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +34 -0
- package/templates/agent-kit/skills/aifabrix-plan/references/plan-template.md +182 -0
- package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +51 -0
- package/templates/agent-kit/skills/aifabrix-prove/references/demo-template.md +65 -0
- package/templates/agent-kit/skills/aifabrix-prove/references/evidence-lifecycle.md +68 -0
- package/templates/agent-kit/skills/aifabrix-prove/references/learning-template.md +66 -0
- package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +43 -0
- package/templates/agent-kit/skills/aifabrix-role-assistant/references/package-boundary.md +21 -0
- package/templates/agent-kit/skills/shared/hosts.md +65 -0
- package/templates/agent-kit/skills/shared/login.md +16 -0
- package/templates/agent-kit/workspace/.cursor/plans/README.md +22 -0
- package/templates/agent-kit/workspace/LICENSE.mit +21 -0
- package/templates/agent-kit/workspace/README.md +89 -0
- package/templates/applications/builder-api/Dockerfile +3 -1
- package/templates/applications/dataplane/application.yaml +2 -3
- package/templates/applications/dataplane/env.template +5 -5
- package/templates/applications/dataplane/rbac.yaml +28 -0
- package/templates/applications/keycloak/env.template +3 -3
- package/templates/applications/miso-controller/application.yaml +21 -2
- package/templates/applications/miso-controller/env.template +96 -25
- package/templates/applications/miso-controller/rbac.yaml +5 -0
- package/templates/channels/claude/TROUBLESHOOTING.md +3 -0
- package/templates/channels/claude/channel.json +1 -1
- package/templates/channels/claude/install.template.md.hbs +1 -0
- package/templates/channels/claude/integration/claude-system.json +1 -1
- package/templates/channels/slack/channel.json +11 -0
- package/templates/channels/slack/install.template.md.hbs +9 -0
- package/templates/channels/slack/integration/slack-system.json +11 -0
- package/templates/channels/slack/manifest.template.json.hbs +6 -3
- package/templates/channels/teams-bot/channel.json +1 -0
- package/templates/python/docker-compose.hbs +14 -0
- 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
|
|
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 };
|
package/lib/utils/cli-utils.js
CHANGED
|
@@ -83,9 +83,11 @@ function isPortConflictError(errorMsg) {
|
|
|
83
83
|
* @returns {boolean} True if permission denied error
|
|
84
84
|
*/
|
|
85
85
|
function isPermissionDeniedError(errorMsg) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
'
|
|
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
|
-
*
|
|
136
|
-
*
|
|
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
|
|
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(
|
|
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
|
|
package/lib/utils/device-code.js
CHANGED
|
@@ -128,25 +128,45 @@ function buildVerificationUrlWithUserCode(verificationUri, userCode) {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
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 =
|
|
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('
|
|
147
|
-
logger.log(chalk.
|
|
148
|
-
logger.log(chalk.gray('
|
|
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
|
|
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
|
-
|
|
115
|
-
addPermissionList(lines,
|
|
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';
|