@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
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestrate aifabrix search and aifabrix search skills.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Search command run path (retrieve only)
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const fs = require('../internal/fs-real-sync');
|
|
12
|
+
const logger = require('../utils/logger');
|
|
13
|
+
const { getWorkRoles, getWorkSkills, searchWork } = require('../api/work-search.api');
|
|
14
|
+
const { resolveSearchDataplaneContext } = require('./dataplane-context');
|
|
15
|
+
const { SearchCliError, authRequiredError } = require('./search-errors');
|
|
16
|
+
const { buildSearchBody, roleRefFromOptions, resourceTypeFromOptions } = require('./search-args');
|
|
17
|
+
const {
|
|
18
|
+
skillsEnvelope,
|
|
19
|
+
resourceTypesMap,
|
|
20
|
+
typeSpec,
|
|
21
|
+
isOperationRequired,
|
|
22
|
+
assertTypePublished,
|
|
23
|
+
assertOperationPublished
|
|
24
|
+
} = require('./search-skills');
|
|
25
|
+
const {
|
|
26
|
+
isInteractive,
|
|
27
|
+
missingFlagsError,
|
|
28
|
+
promptRoleAssistant,
|
|
29
|
+
promptResourceType,
|
|
30
|
+
promptOperation
|
|
31
|
+
} = require('./search-prompts');
|
|
32
|
+
const { emitSearchSuccess, emitSkillsSuccess } = require('./search-display');
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {Object} [overrides]
|
|
36
|
+
* @returns {Object}
|
|
37
|
+
*/
|
|
38
|
+
function defaultDeps(overrides = {}) {
|
|
39
|
+
return {
|
|
40
|
+
resolveContext: resolveSearchDataplaneContext,
|
|
41
|
+
getWorkRoles,
|
|
42
|
+
getWorkSkills,
|
|
43
|
+
searchWork,
|
|
44
|
+
prompt: (qs) => require('inquirer').prompt(qs),
|
|
45
|
+
logger,
|
|
46
|
+
isTty: () => Boolean(process.stdin && process.stdin.isTTY),
|
|
47
|
+
fsApi: fs,
|
|
48
|
+
...overrides
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {Object} options
|
|
54
|
+
* @param {Function} resolveContext
|
|
55
|
+
* @returns {Promise<Object>}
|
|
56
|
+
*/
|
|
57
|
+
async function resolveContextOrThrow(options, resolveContext) {
|
|
58
|
+
try {
|
|
59
|
+
return await resolveContext(options);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
const message = err.message || String(err);
|
|
62
|
+
if (/login|auth|token|controller url/i.test(message)) {
|
|
63
|
+
throw authRequiredError(message);
|
|
64
|
+
}
|
|
65
|
+
throw new SearchCliError(message);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {Object} options
|
|
71
|
+
*/
|
|
72
|
+
function assertScriptSearchFlags(options) {
|
|
73
|
+
const missing = [];
|
|
74
|
+
if (!roleRefFromOptions(options)) missing.push('--role');
|
|
75
|
+
if (!resourceTypeFromOptions(options)) missing.push('--resource-type');
|
|
76
|
+
if (missing.length) {
|
|
77
|
+
throw missingFlagsError(missing);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {Object} res
|
|
83
|
+
* @param {string} fallback
|
|
84
|
+
* @returns {Object}
|
|
85
|
+
*/
|
|
86
|
+
function payloadOrThrow(res, fallback) {
|
|
87
|
+
if (res && Number(res.status) === 200) {
|
|
88
|
+
const payload = skillsEnvelope(res.payload);
|
|
89
|
+
return payload && typeof payload === 'object' ? payload : {};
|
|
90
|
+
}
|
|
91
|
+
const text = String((res && res.errorText) || '').trim() || fallback;
|
|
92
|
+
throw new SearchCliError(text);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {Object} options
|
|
97
|
+
* @param {Object} ctx
|
|
98
|
+
* @param {Object} deps
|
|
99
|
+
* @returns {Promise<string>}
|
|
100
|
+
*/
|
|
101
|
+
async function resolveRoleRef(options, ctx, deps) {
|
|
102
|
+
const existing = roleRefFromOptions(options);
|
|
103
|
+
if (existing) {
|
|
104
|
+
return existing;
|
|
105
|
+
}
|
|
106
|
+
return promptRoleAssistant(ctx, deps);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {string} explicitType
|
|
111
|
+
* @param {string[]} typeKeys
|
|
112
|
+
* @param {Object} typeMap
|
|
113
|
+
* @param {boolean} interactive
|
|
114
|
+
* @param {Object} deps
|
|
115
|
+
* @returns {Promise<string>}
|
|
116
|
+
*/
|
|
117
|
+
async function resolveResourceType(explicitType, typeKeys, typeMap, interactive, deps) {
|
|
118
|
+
if (!typeKeys.length) {
|
|
119
|
+
throw new SearchCliError('No searchable resource types for this Role Assistant.');
|
|
120
|
+
}
|
|
121
|
+
if (explicitType) {
|
|
122
|
+
assertTypePublished(explicitType, typeMap);
|
|
123
|
+
return explicitType;
|
|
124
|
+
}
|
|
125
|
+
if (!interactive) {
|
|
126
|
+
throw missingFlagsError(['--resource-type']);
|
|
127
|
+
}
|
|
128
|
+
return promptResourceType(typeKeys, deps);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {string} explicitOp
|
|
133
|
+
* @param {Object} spec
|
|
134
|
+
* @param {boolean} interactive
|
|
135
|
+
* @param {Object} deps
|
|
136
|
+
* @returns {Promise<string>}
|
|
137
|
+
*/
|
|
138
|
+
async function resolveOperation(explicitOp, spec, interactive, deps) {
|
|
139
|
+
if (explicitOp) {
|
|
140
|
+
assertOperationPublished(explicitOp, spec);
|
|
141
|
+
return explicitOp;
|
|
142
|
+
}
|
|
143
|
+
if (isOperationRequired(spec) && !interactive) {
|
|
144
|
+
throw new SearchCliError(
|
|
145
|
+
'Operation is required for this resource type. Pass --operation.'
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
if (!interactive) {
|
|
149
|
+
return '';
|
|
150
|
+
}
|
|
151
|
+
return promptOperation(spec, deps);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @param {Object} body
|
|
156
|
+
* @param {Object} skillsPayload
|
|
157
|
+
* @param {Object} options
|
|
158
|
+
* @param {Object} deps
|
|
159
|
+
* @returns {Promise<Object>}
|
|
160
|
+
*/
|
|
161
|
+
async function fillTypeAndOperation(body, skillsPayload, options, deps) {
|
|
162
|
+
const typeMap = resourceTypesMap(skillsPayload);
|
|
163
|
+
const typeKeys = Object.keys(typeMap);
|
|
164
|
+
const interactive = isInteractive(options, deps.isTty);
|
|
165
|
+
const resourceType = await resolveResourceType(
|
|
166
|
+
body.resourceType || '',
|
|
167
|
+
typeKeys,
|
|
168
|
+
typeMap,
|
|
169
|
+
interactive,
|
|
170
|
+
deps
|
|
171
|
+
);
|
|
172
|
+
body.resourceType = resourceType;
|
|
173
|
+
const spec = typeSpec(typeMap, resourceType);
|
|
174
|
+
const operation = await resolveOperation(body.operation || '', spec, interactive, deps);
|
|
175
|
+
if (operation) {
|
|
176
|
+
body.operation = operation;
|
|
177
|
+
} else {
|
|
178
|
+
delete body.operation;
|
|
179
|
+
}
|
|
180
|
+
if (isOperationRequired(spec) && !body.operation) {
|
|
181
|
+
throw new SearchCliError(
|
|
182
|
+
'Operation is required for this resource type. Pass --operation.'
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
return body;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @param {Object} options
|
|
190
|
+
* @param {Object} [depOverrides]
|
|
191
|
+
* @returns {Promise<number>}
|
|
192
|
+
*/
|
|
193
|
+
async function runSearch(options, depOverrides) {
|
|
194
|
+
const deps = defaultDeps(depOverrides);
|
|
195
|
+
const body = buildSearchBody(options, deps.fsApi);
|
|
196
|
+
const interactive = isInteractive(options, deps.isTty);
|
|
197
|
+
if (!interactive) {
|
|
198
|
+
assertScriptSearchFlags(options);
|
|
199
|
+
}
|
|
200
|
+
const ctx = await resolveContextOrThrow(options, deps.resolveContext);
|
|
201
|
+
if (!body.roleRef) {
|
|
202
|
+
body.roleRef = await resolveRoleRef(options, ctx, deps);
|
|
203
|
+
}
|
|
204
|
+
const skillsRes = await deps.getWorkSkills(ctx.dataplaneUrl, ctx.authConfig, body.roleRef);
|
|
205
|
+
const skillsPayload = payloadOrThrow(skillsRes, 'Unable to load search skills.');
|
|
206
|
+
await fillTypeAndOperation(body, skillsPayload, options, deps);
|
|
207
|
+
const searchRes = await deps.searchWork(ctx.dataplaneUrl, ctx.authConfig, body);
|
|
208
|
+
const payload = payloadOrThrow(searchRes, 'Search request failed.');
|
|
209
|
+
emitSearchSuccess(options, body, payload, deps.logger, deps.fsApi);
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* @param {Object} options
|
|
215
|
+
* @param {Object} [depOverrides]
|
|
216
|
+
* @returns {Promise<number>}
|
|
217
|
+
*/
|
|
218
|
+
async function runSearchSkills(options, depOverrides) {
|
|
219
|
+
const deps = defaultDeps(depOverrides);
|
|
220
|
+
const interactive = isInteractive(options, deps.isTty);
|
|
221
|
+
if (!interactive && !roleRefFromOptions(options)) {
|
|
222
|
+
throw missingFlagsError(['--role']);
|
|
223
|
+
}
|
|
224
|
+
const ctx = await resolveContextOrThrow(options, deps.resolveContext);
|
|
225
|
+
const roleRef = await resolveRoleRef(options, ctx, deps);
|
|
226
|
+
const skillsRes = await deps.getWorkSkills(ctx.dataplaneUrl, ctx.authConfig, roleRef);
|
|
227
|
+
const payload = payloadOrThrow(skillsRes, 'Unable to load search skills.');
|
|
228
|
+
if (!payload.roleRef) {
|
|
229
|
+
payload.roleRef = roleRef;
|
|
230
|
+
}
|
|
231
|
+
emitSkillsSuccess(options, payload, deps.logger, deps.fsApi);
|
|
232
|
+
return 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = {
|
|
236
|
+
defaultDeps,
|
|
237
|
+
runSearch,
|
|
238
|
+
runSearchSkills,
|
|
239
|
+
fillTypeAndOperation,
|
|
240
|
+
resolveRoleRef
|
|
241
|
+
};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAML-free copies of work.skills helpers (do not import the test runner).
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Resource type / operation keys from a skills payload
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const { SearchCliError } = require('./search-errors');
|
|
12
|
+
|
|
13
|
+
/** Table retrieve keys stamped by the search grammar (list/search page, get one). */
|
|
14
|
+
const RETRIEVE_TABLE_KEYS = new Set(['list', 'get', 'search']);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {*} item
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
function operationKeyFromItem(item) {
|
|
21
|
+
if (typeof item === 'string' && item.trim()) {
|
|
22
|
+
return item.trim();
|
|
23
|
+
}
|
|
24
|
+
if (item && typeof item === 'object') {
|
|
25
|
+
return String(item.key || '').trim();
|
|
26
|
+
}
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {*} item
|
|
32
|
+
* @returns {boolean}
|
|
33
|
+
*/
|
|
34
|
+
function hasResultMode(item) {
|
|
35
|
+
return Boolean(item && typeof item === 'object' && String(item.resultMode || '').trim());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Retrieve/analyze only. Write-shaped keys stay off search.
|
|
40
|
+
* Keep table retrieve keys (list/get/search) and named views (source viewpoint).
|
|
41
|
+
*
|
|
42
|
+
* @param {*} item
|
|
43
|
+
* @param {boolean} grammarPresent
|
|
44
|
+
* @param {boolean} tableRetrievePresent
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
function isRetrieveOperation(item, grammarPresent, tableRetrievePresent) {
|
|
48
|
+
const key = operationKeyFromItem(item);
|
|
49
|
+
if (!key) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
if (RETRIEVE_TABLE_KEYS.has(key)) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (typeof item === 'string') {
|
|
56
|
+
return !tableRetrievePresent;
|
|
57
|
+
}
|
|
58
|
+
const source = String((item && item.source) || '')
|
|
59
|
+
.trim()
|
|
60
|
+
.toLowerCase();
|
|
61
|
+
if (source === 'viewpoint') {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
if (!source && !grammarPresent && !tableRetrievePresent) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {Object|null|undefined} spec
|
|
72
|
+
* @returns {*[]}
|
|
73
|
+
*/
|
|
74
|
+
function operationItems(spec) {
|
|
75
|
+
return Array.isArray(spec && spec.operations) ? spec.operations : [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {Object|null|undefined} spec
|
|
80
|
+
* @returns {string[]}
|
|
81
|
+
*/
|
|
82
|
+
function operationKeys(spec) {
|
|
83
|
+
const raw = operationItems(spec);
|
|
84
|
+
const grammarPresent = raw.some(hasResultMode);
|
|
85
|
+
const tableRetrievePresent = raw.some((item) =>
|
|
86
|
+
RETRIEVE_TABLE_KEYS.has(operationKeyFromItem(item))
|
|
87
|
+
);
|
|
88
|
+
const keys = [];
|
|
89
|
+
for (const item of raw) {
|
|
90
|
+
if (!isRetrieveOperation(item, grammarPresent, tableRetrievePresent)) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const key = operationKeyFromItem(item);
|
|
94
|
+
if (key) {
|
|
95
|
+
keys.push(key);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return keys;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {Object|null|undefined} spec
|
|
103
|
+
* @returns {string[]}
|
|
104
|
+
*/
|
|
105
|
+
function allOperationKeys(spec) {
|
|
106
|
+
return operationItems(spec).map(operationKeyFromItem).filter(Boolean);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* work.skills is an object. MCP execute may wrap it as a one-element array.
|
|
111
|
+
* @param {*} skillsPayload
|
|
112
|
+
* @returns {Object|null}
|
|
113
|
+
*/
|
|
114
|
+
function skillsEnvelope(skillsPayload) {
|
|
115
|
+
let cur = skillsPayload;
|
|
116
|
+
if (Array.isArray(cur) && cur.length === 1 && cur[0] && typeof cur[0] === 'object') {
|
|
117
|
+
cur = cur[0];
|
|
118
|
+
}
|
|
119
|
+
if (!cur || typeof cur !== 'object' || Array.isArray(cur)) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
if (cur.search && typeof cur.search === 'object') {
|
|
123
|
+
return cur;
|
|
124
|
+
}
|
|
125
|
+
if (cur.data && typeof cur.data === 'object' && !Array.isArray(cur.data) && cur.data.search) {
|
|
126
|
+
return cur.data;
|
|
127
|
+
}
|
|
128
|
+
return cur;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {Object|null|undefined} skillsPayload
|
|
133
|
+
* @returns {Object}
|
|
134
|
+
*/
|
|
135
|
+
function resourceTypesMap(skillsPayload) {
|
|
136
|
+
const root = skillsEnvelope(skillsPayload);
|
|
137
|
+
const rts = root && root.search && root.search.resourceTypes;
|
|
138
|
+
return rts && typeof rts === 'object' && !Array.isArray(rts) ? rts : {};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @param {Object} typeMap
|
|
143
|
+
* @param {string} resourceType
|
|
144
|
+
* @returns {Object}
|
|
145
|
+
*/
|
|
146
|
+
function typeSpec(typeMap, resourceType) {
|
|
147
|
+
const spec = typeMap && typeMap[resourceType];
|
|
148
|
+
return spec && typeof spec === 'object' ? spec : {};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {Object} spec
|
|
153
|
+
* @returns {boolean}
|
|
154
|
+
*/
|
|
155
|
+
function isOperationRequired(spec) {
|
|
156
|
+
return spec && spec.operationRequired === true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @param {string} resourceType
|
|
161
|
+
* @param {Object} typeMap
|
|
162
|
+
*/
|
|
163
|
+
function assertTypePublished(resourceType, typeMap) {
|
|
164
|
+
if (!Object.prototype.hasOwnProperty.call(typeMap, resourceType)) {
|
|
165
|
+
throw new SearchCliError(
|
|
166
|
+
`Resource type "${resourceType}" is not published for this Role Assistant.`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* @param {string} operation
|
|
173
|
+
* @param {Object} spec
|
|
174
|
+
*/
|
|
175
|
+
function assertOperationPublished(operation, spec) {
|
|
176
|
+
const keys = operationKeys(spec);
|
|
177
|
+
if (keys.includes(operation)) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (allOperationKeys(spec).includes(operation)) {
|
|
181
|
+
throw new SearchCliError(
|
|
182
|
+
`Operation "${operation}" is not a retrieve operation for this resource type.`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
throw new SearchCliError(
|
|
186
|
+
`Operation "${operation}" is not published for this resource type.`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = {
|
|
191
|
+
operationKeys,
|
|
192
|
+
skillsEnvelope,
|
|
193
|
+
resourceTypesMap,
|
|
194
|
+
typeSpec,
|
|
195
|
+
isOperationRequired,
|
|
196
|
+
assertTypePublished,
|
|
197
|
+
assertOperationPublished
|
|
198
|
+
};
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve required RBAC scopes for online API calls.
|
|
3
|
+
*
|
|
4
|
+
* Catalog is built from `@requiresPermission` plus the HTTP path in `lib/api/*.api.js`.
|
|
5
|
+
* Used when a 403 body does not list missing/required permissions.
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Fallback required-permission lookup for 403 errors
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const fsRealSync = require('../internal/fs-real-sync');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
const SCOPE_IN_TEXT_RE = /\b[a-z][a-z0-9_-]*:[a-z][a-z0-9_-]*(?::[a-z][a-z0-9_-]*)*\b/gi;
|
|
18
|
+
const SKIP_SCOPE_SCHEMES = new Set(['http', 'https', 'mailto', 'ws', 'wss']);
|
|
19
|
+
|
|
20
|
+
/** @type {Array<{ method: string, regex: RegExp, service: string, permissions: string[] }>|null} */
|
|
21
|
+
let cachedCatalog = null;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* CLI command name (handleCommandError second arg) → required permission fallback.
|
|
25
|
+
* URL catalog is preferred; this is used when the 403 has no request URL.
|
|
26
|
+
*/
|
|
27
|
+
const COMMAND_REQUIRED_PERMISSIONS = Object.freeze({
|
|
28
|
+
deploy: { service: 'Controller', permissions: ['applications:deploy'] },
|
|
29
|
+
'env deploy': { service: 'Controller', permissions: ['controller:deploy'] },
|
|
30
|
+
upload: { service: 'Dataplane', permissions: ['external-system:publish'] },
|
|
31
|
+
download: { service: 'Dataplane', permissions: ['external-system:read'] },
|
|
32
|
+
delete: { service: 'Dataplane', permissions: ['external-system:delete'] },
|
|
33
|
+
wizard: { service: 'Dataplane', permissions: ['external-system:create'] },
|
|
34
|
+
'credential list': { service: 'Dataplane', permissions: ['credential:read'] },
|
|
35
|
+
'deployment list': { service: 'Controller', permissions: ['deployments:read'] },
|
|
36
|
+
'app register': { service: 'Controller', permissions: ['environments-applications:create'] },
|
|
37
|
+
'app list': { service: 'Controller', permissions: ['environments-applications:read'] }
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} spec
|
|
42
|
+
* @returns {boolean}
|
|
43
|
+
*/
|
|
44
|
+
function looksLikeRbacSpec(spec) {
|
|
45
|
+
if (!spec || typeof spec !== 'string') return false;
|
|
46
|
+
const trimmed = spec.trim();
|
|
47
|
+
if (/^(none|public)\b/i.test(trimmed) && !trimmed.includes(':')) return false;
|
|
48
|
+
if (/authenticated\s*\(/i.test(trimmed)) return false;
|
|
49
|
+
if (/client credentials/i.test(trimmed) && !/[a-z0-9_-]+:[a-z0-9_-]+/i.test(trimmed)) return false;
|
|
50
|
+
return /[a-z0-9_-]+:[a-z0-9_-]+/i.test(trimmed);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {string} spec
|
|
55
|
+
* @returns {string[]}
|
|
56
|
+
*/
|
|
57
|
+
function permissionListFromSpec(spec) {
|
|
58
|
+
const main = String(spec).split('(')[0].trim();
|
|
59
|
+
if (!main) return [];
|
|
60
|
+
if (/\bor\b/i.test(main)) return [main];
|
|
61
|
+
return main.split(/\s*\+\s*/).map((part) => part.trim()).filter(Boolean);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {string} template
|
|
66
|
+
* @returns {RegExp}
|
|
67
|
+
*/
|
|
68
|
+
function pathTemplateToRegex(template) {
|
|
69
|
+
const parts = String(template).split('/');
|
|
70
|
+
const escaped = parts.map((seg) => {
|
|
71
|
+
if (/^\{[^}]+\}$/.test(seg)) return '[^/]+';
|
|
72
|
+
return seg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
73
|
+
}).join('/');
|
|
74
|
+
return new RegExp(`^${escaped}$`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {string} block
|
|
79
|
+
* @returns {{ method: string, path: string, service: string, permissions: string[] }|null}
|
|
80
|
+
*/
|
|
81
|
+
function parsePermissionCommentBlock(block) {
|
|
82
|
+
const text = String(block).replace(/^\s*\*\s?/gm, '');
|
|
83
|
+
const methodPath = text.match(/\b(GET|POST|PUT|PATCH|DELETE)\s+(\/api\/[^\s*]+)/);
|
|
84
|
+
const perm = text.match(/@requiresPermission\s+\{([^}]+)\}\s+([^\n*]+)/);
|
|
85
|
+
if (!methodPath || !perm) return null;
|
|
86
|
+
const spec = String(perm[2] || '').trim();
|
|
87
|
+
if (!looksLikeRbacSpec(spec)) return null;
|
|
88
|
+
const permissions = permissionListFromSpec(spec);
|
|
89
|
+
if (permissions.length === 0) return null;
|
|
90
|
+
return {
|
|
91
|
+
method: methodPath[1].toUpperCase(),
|
|
92
|
+
path: methodPath[2].replace(/\/+$/, ''),
|
|
93
|
+
service: String(perm[1] || '').trim(),
|
|
94
|
+
permissions
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param {string} source
|
|
100
|
+
* @returns {Array<{ method: string, regex: RegExp, service: string, permissions: string[], pathLength: number }>}
|
|
101
|
+
*/
|
|
102
|
+
function parsePermissionEntriesFromSource(source) {
|
|
103
|
+
const entries = [];
|
|
104
|
+
const blocks = String(source).match(/\/\*\*[\s\S]*?\*\//g) || [];
|
|
105
|
+
for (const block of blocks) {
|
|
106
|
+
const parsed = parsePermissionCommentBlock(block);
|
|
107
|
+
if (!parsed) continue;
|
|
108
|
+
entries.push({
|
|
109
|
+
method: parsed.method,
|
|
110
|
+
regex: pathTemplateToRegex(parsed.path),
|
|
111
|
+
service: parsed.service,
|
|
112
|
+
permissions: parsed.permissions,
|
|
113
|
+
pathLength: parsed.path.length
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return entries;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @returns {Array<{ method: string, regex: RegExp, service: string, permissions: string[], pathLength: number }>}
|
|
121
|
+
*/
|
|
122
|
+
function loadApiPermissionCatalog() {
|
|
123
|
+
if (cachedCatalog) return cachedCatalog;
|
|
124
|
+
const apiDir = path.join(__dirname, '..', 'api');
|
|
125
|
+
const entries = [];
|
|
126
|
+
let names = [];
|
|
127
|
+
try {
|
|
128
|
+
names = fsRealSync.readdirSync(apiDir);
|
|
129
|
+
} catch {
|
|
130
|
+
cachedCatalog = [];
|
|
131
|
+
return cachedCatalog;
|
|
132
|
+
}
|
|
133
|
+
for (const name of names) {
|
|
134
|
+
if (!name.endsWith('.api.js')) continue;
|
|
135
|
+
const source = fsRealSync.readFileSync(path.join(apiDir, name), 'utf8');
|
|
136
|
+
entries.push(...parsePermissionEntriesFromSource(source));
|
|
137
|
+
}
|
|
138
|
+
entries.sort((a, b) => b.pathLength - a.pathLength);
|
|
139
|
+
cachedCatalog = entries;
|
|
140
|
+
return cachedCatalog;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {string} [url]
|
|
145
|
+
* @returns {string}
|
|
146
|
+
*/
|
|
147
|
+
function apiPathFromUrl(url) {
|
|
148
|
+
const raw = String(url || '');
|
|
149
|
+
try {
|
|
150
|
+
const parsed = new URL(raw);
|
|
151
|
+
const idx = parsed.pathname.indexOf('/api/');
|
|
152
|
+
return idx >= 0 ? parsed.pathname.slice(idx).replace(/\/+$/, '') : parsed.pathname;
|
|
153
|
+
} catch {
|
|
154
|
+
const idx = raw.indexOf('/api/');
|
|
155
|
+
const pathOnly = idx >= 0 ? raw.slice(idx).split('?')[0] : raw.split('?')[0];
|
|
156
|
+
return pathOnly.replace(/\/+$/, '');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @param {string} [method]
|
|
162
|
+
* @param {string} [url]
|
|
163
|
+
* @returns {{ service: string, permissions: string[] }|null}
|
|
164
|
+
*/
|
|
165
|
+
function lookupRequiredPermission(method, url) {
|
|
166
|
+
const verb = String(method || '').toUpperCase();
|
|
167
|
+
const apiPath = apiPathFromUrl(url);
|
|
168
|
+
if (!verb || !apiPath.startsWith('/api/')) return null;
|
|
169
|
+
const catalog = loadApiPermissionCatalog();
|
|
170
|
+
for (const entry of catalog) {
|
|
171
|
+
if (entry.method === verb && entry.regex.test(apiPath)) {
|
|
172
|
+
return { service: entry.service, permissions: entry.permissions };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @param {string} [command]
|
|
180
|
+
* @returns {{ service: string, permissions: string[] }|null}
|
|
181
|
+
*/
|
|
182
|
+
function lookupCommandRequiredPermission(command) {
|
|
183
|
+
if (!command || typeof command !== 'string') return null;
|
|
184
|
+
return COMMAND_REQUIRED_PERMISSIONS[command] || null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @param {string} text
|
|
189
|
+
* @returns {string[]}
|
|
190
|
+
*/
|
|
191
|
+
function extractPermissionTokensFromText(text) {
|
|
192
|
+
if (!text || typeof text !== 'string') return [];
|
|
193
|
+
const found = text.match(SCOPE_IN_TEXT_RE) || [];
|
|
194
|
+
const unique = [];
|
|
195
|
+
for (const token of found) {
|
|
196
|
+
const scheme = token.split(':')[0].toLowerCase();
|
|
197
|
+
if (SKIP_SCOPE_SCHEMES.has(scheme)) continue;
|
|
198
|
+
if (!unique.includes(token)) unique.push(token);
|
|
199
|
+
}
|
|
200
|
+
return unique;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @param {Object} data
|
|
205
|
+
* @returns {boolean}
|
|
206
|
+
*/
|
|
207
|
+
function hasBodyPermissionLists(data) {
|
|
208
|
+
if (!data || typeof data !== 'object') return false;
|
|
209
|
+
const lists = [
|
|
210
|
+
data.missingPermissions,
|
|
211
|
+
data.requiredPermissions,
|
|
212
|
+
data.permissions,
|
|
213
|
+
data.missing && data.missing.permissions,
|
|
214
|
+
data.required && data.required.permissions,
|
|
215
|
+
data.data && data.data.missing && data.data.missing.permissions,
|
|
216
|
+
data.data && data.data.required && data.data.required.permissions
|
|
217
|
+
];
|
|
218
|
+
return lists.some((list) => Array.isArray(list) && list.length > 0);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* @param {*} errorData
|
|
223
|
+
* @returns {Object}
|
|
224
|
+
*/
|
|
225
|
+
function asErrorObject(errorData) {
|
|
226
|
+
if (errorData && typeof errorData === 'object' && !Array.isArray(errorData)) {
|
|
227
|
+
return { ...errorData };
|
|
228
|
+
}
|
|
229
|
+
const text = errorData === undefined || errorData === null ? 'Permission denied' : String(errorData);
|
|
230
|
+
return { detail: text, message: text };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* When a 403 body has no permission list, fill requiredPermissions from the request catalog.
|
|
235
|
+
*
|
|
236
|
+
* @param {*} errorData
|
|
237
|
+
* @param {{ status?: number, method?: string, url?: string }} [ctx]
|
|
238
|
+
* @returns {*}
|
|
239
|
+
*/
|
|
240
|
+
function enrichForbiddenErrorData(errorData, ctx = {}) {
|
|
241
|
+
if (ctx.status !== 403) return errorData;
|
|
242
|
+
const data = asErrorObject(errorData);
|
|
243
|
+
if (ctx.method && !data.method) data.method = ctx.method;
|
|
244
|
+
if (ctx.url && !data.instance && !data.url) data.url = ctx.url;
|
|
245
|
+
if (hasBodyPermissionLists(data)) {
|
|
246
|
+
return data;
|
|
247
|
+
}
|
|
248
|
+
const fromText = extractPermissionTokensFromText(
|
|
249
|
+
[data.detail, data.message, data.error, data.title, data.errorDescription].filter(Boolean).join(' ')
|
|
250
|
+
);
|
|
251
|
+
if (fromText.length > 0) {
|
|
252
|
+
data.requiredPermissions = fromText;
|
|
253
|
+
return data;
|
|
254
|
+
}
|
|
255
|
+
const hit = lookupRequiredPermission(ctx.method, ctx.url);
|
|
256
|
+
if (!hit) return data;
|
|
257
|
+
data.cliPermissionService = hit.service;
|
|
258
|
+
data.requiredPermissions = hit.permissions;
|
|
259
|
+
return data;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
module.exports = {
|
|
263
|
+
lookupRequiredPermission,
|
|
264
|
+
lookupCommandRequiredPermission,
|
|
265
|
+
enrichForbiddenErrorData,
|
|
266
|
+
extractPermissionTokensFromText,
|
|
267
|
+
parsePermissionEntriesFromSource,
|
|
268
|
+
parsePermissionCommentBlock,
|
|
269
|
+
apiPathFromUrl,
|
|
270
|
+
COMMAND_REQUIRED_PERMISSIONS
|
|
271
|
+
};
|