@aifabrix/builder 2.50.0 → 2.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/plans/157.1-security-quality-violations-by-type.csv +374 -0
- package/.cursor/rules/assistant-driven-integration-build.mdc +40 -0
- package/integration/hubspot-test/create-hubspot.js +1 -16
- package/lib/api/evidence-kinds.api.js +128 -0
- package/lib/api/pipeline.api.js +5 -1
- package/lib/api/resource-types.api.js +4 -4
- package/lib/api/roles.api.js +121 -0
- package/lib/api/types/evidence-kinds.types.js +42 -0
- package/lib/api/types/roles.types.js +36 -0
- package/lib/app/certification-verify-rows.js +1 -1
- package/lib/app/show.js +2 -2
- package/lib/certification/merge-certification-from-artifact.js +1 -1
- package/lib/channels/channel-artifact-generator.js +2 -1
- package/lib/cli/index.js +4 -0
- package/lib/cli/setup-app.js +22 -5
- package/lib/cli/setup-dev.js +23 -7
- package/lib/cli/setup-utility-help-after.js +12 -0
- package/lib/cli/setup-utility-validate.js +41 -2
- package/lib/commands/deploy-gate-failure-details.js +21 -0
- package/lib/commands/deploy-gate-orchestrator.js +64 -18
- package/lib/commands/dev-cli-handlers.js +6 -3
- package/lib/commands/dev-init.js +7 -109
- package/lib/commands/dev-refresh.js +197 -0
- package/lib/commands/evidence-kind.js +428 -0
- package/lib/commands/governance-command-action.js +4 -2
- package/lib/commands/repair-openapi-export.js +1 -1
- package/lib/commands/repair-openapi-sync.js +14 -6
- package/lib/commands/resource-type.js +101 -18
- package/lib/commands/role.js +452 -0
- package/lib/commands/setup.js +1 -1
- package/lib/commands/validate-json.js +230 -0
- package/lib/commands/verify-operations-step-plan.js +78 -40
- package/lib/core/audit-logger.js +1 -1
- package/lib/core/secrets-load.js +1 -3
- package/lib/datasource/capability/relate-operations.js +1 -1
- package/lib/generator/helpers.js +13 -5
- package/lib/generator/split-variables.js +9 -3
- package/lib/infrastructure/helpers-docker-check.js +6 -0
- package/lib/infrastructure/helpers.js +1 -1
- package/lib/programmatic/auth-config.js +3 -2
- package/lib/programmatic/bearer-auth.js +1 -1
- package/lib/programmatic/enterprise-mcp-client.js +18 -2
- package/lib/programmatic/manifest-section-registry.js +18 -1
- package/lib/programmatic/openapi-result-schemas.js +28 -11
- package/lib/programmatic/openapi-schema-descriptions-core.js +5 -2
- package/lib/programmatic/openapi-schema-helpers.js +16 -1
- package/lib/programmatic/openapi-spec.js +1 -1
- package/lib/programmatic/run-repository-emcp.js +11 -3
- package/lib/programmatic/run-template-workspace.js +14 -1
- package/lib/resolvers/evidence-kind-file.js +230 -0
- package/lib/resolvers/resource-type-file.js +36 -1
- package/lib/resolvers/role-file.js +227 -0
- package/lib/schema/application-schema.json +22 -2
- package/lib/schema/external-datasource.schema.json +85 -2
- package/lib/schema/external-system.schema.json +16 -2
- package/lib/schema/infra.parameter.yaml +25 -1
- package/lib/schema/type/credential-field-registry.json +19 -3
- package/lib/utils/api.js +8 -2
- package/lib/utils/app-register-config.js +4 -3
- package/lib/utils/cli-sync-options.js +59 -3
- package/lib/utils/cli-utils.js +11 -3
- package/lib/utils/dev-ca-install.js +5 -6
- package/lib/utils/dev-config.js +2 -2
- package/lib/utils/dev-list-sort.js +145 -0
- package/lib/utils/dev-platform-tls.js +0 -63
- package/lib/utils/docker-build.js +46 -27
- package/lib/utils/docker-daemon-tls-ca.js +1 -1
- package/lib/utils/docker-exec.js +91 -6
- package/lib/utils/env-copy.js +1 -3
- package/lib/utils/error-formatters/error-parser.js +0 -3
- package/lib/utils/external-env-template.js +1 -1
- package/lib/utils/external-system-readiness-compact-blocks.js +28 -1
- package/lib/utils/health-check-request.js +2 -3
- package/lib/utils/help-builder.js +3 -0
- package/lib/utils/image-name.js +22 -4
- package/lib/utils/logger.js +37 -4
- package/lib/utils/manifest-source-emit.js +1 -1
- package/lib/utils/paths.js +32 -4
- package/lib/utils/pipeline-upload-auth-retry.js +4 -0
- package/lib/utils/postgres-platform-bootstrap.js +24 -17
- package/lib/utils/postgres-wipe.js +5 -2
- package/lib/utils/remote-dev-auth.js +39 -4
- package/lib/utils/resolve-docker-image-ref.js +36 -6
- package/lib/utils/token-manager.js +16 -0
- package/lib/utils/upload-sync-options.js +17 -3
- package/lib/validation/openapi-contract-surface-validator.js +4 -0
- package/lib/validation/validator.js +6 -5
- package/package.json +34 -12
- package/scripts/export-builder-api-evidence.js +2 -2
- package/scripts/lib/plan-actor.js +2 -2
- package/scripts/lib/plan-meta.js +1 -1
- package/scripts/lib/plan-parse.js +1 -1
- package/scripts/lib/plan-prevalidate-gates.js +12 -1
- package/scripts/lib/plan-run-report.js +13 -2
- package/scripts/pnpm/lib/resolve-aifabrix-bin.mjs +10 -10
- package/templates/applications/dataplane/application.yaml +26 -4
- package/templates/applications/dataplane/env.template +28 -9
- package/templates/applications/dataplane/rbac.yaml +64 -2
- package/templates/applications/miso-controller/application.yaml +10 -1
- package/templates/applications/miso-controller/env.template +10 -7
- package/templates/applications/miso-controller/rbac.yaml +72 -0
- package/templates/channels/slack/integration/slack-system.json +60 -2
- package/templates/python/requirements.txt +1 -1
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline validate then write *-deploy.json (same quality as validate + same write as json).
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview aifabrix validate-json command
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const chalk = require('chalk');
|
|
13
|
+
const logger = require('../utils/logger');
|
|
14
|
+
const validate = require('../validation/validate');
|
|
15
|
+
const generator = require('../generator');
|
|
16
|
+
const { listIntegrationAppNames, listBuilderAppNames } = require('../utils/paths');
|
|
17
|
+
const {
|
|
18
|
+
formatSuccessLine,
|
|
19
|
+
formatBlockingError,
|
|
20
|
+
formatWarningLine
|
|
21
|
+
} = require('../utils/cli-test-layout-chalk');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} appKey
|
|
25
|
+
* @returns {boolean}
|
|
26
|
+
*/
|
|
27
|
+
function looksLikeExistingFilePath(appKey) {
|
|
28
|
+
try {
|
|
29
|
+
return Boolean(appKey && fs.existsSync(appKey) && fs.statSync(appKey).isFile());
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {object} opts
|
|
37
|
+
* @returns {string[]}
|
|
38
|
+
*/
|
|
39
|
+
function collectBatchAppNames(opts) {
|
|
40
|
+
const names = [];
|
|
41
|
+
if (opts.integration === true) {
|
|
42
|
+
names.push(...listIntegrationAppNames());
|
|
43
|
+
}
|
|
44
|
+
if (opts.builder === true) {
|
|
45
|
+
names.push(...listBuilderAppNames());
|
|
46
|
+
}
|
|
47
|
+
return [...new Set(names)];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} appKey
|
|
52
|
+
* @param {object} result
|
|
53
|
+
* @param {string} outFormat
|
|
54
|
+
*/
|
|
55
|
+
function emitManifestLine(appKey, result, outFormat) {
|
|
56
|
+
if (outFormat === 'json' || !result?.appPath) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const { emitManifestMetadataLineIfTTY } = require('../utils/manifest-source-emit');
|
|
61
|
+
emitManifestMetadataLineIfTTY(logger, {
|
|
62
|
+
appKey,
|
|
63
|
+
appPath: result.appPath,
|
|
64
|
+
envOnly: false,
|
|
65
|
+
json: false
|
|
66
|
+
});
|
|
67
|
+
} catch {
|
|
68
|
+
/* ignore */
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Validate one app/systemKey; write deploy JSON only when local validate succeeds.
|
|
74
|
+
* @param {string} appKey
|
|
75
|
+
* @param {object} [options]
|
|
76
|
+
* @returns {Promise<{ appKey: string, valid: boolean, validation: object, deploy?: object|null, error?: string }>}
|
|
77
|
+
*/
|
|
78
|
+
async function validateThenWriteDeploy(appKey, options = {}) {
|
|
79
|
+
let validation;
|
|
80
|
+
try {
|
|
81
|
+
validation = await validate.validateAppOrFile(appKey, { ...options, certSync: false });
|
|
82
|
+
} catch (err) {
|
|
83
|
+
return {
|
|
84
|
+
appKey,
|
|
85
|
+
valid: false,
|
|
86
|
+
validation: { valid: false, errors: [err.message || String(err)] },
|
|
87
|
+
deploy: null,
|
|
88
|
+
error: err.message || String(err)
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (!validation.valid) {
|
|
92
|
+
return { appKey, valid: false, validation, deploy: null };
|
|
93
|
+
}
|
|
94
|
+
const deploy = await generator.generateDeployJsonWithValidation(appKey, options);
|
|
95
|
+
const ok = deploy.success === true;
|
|
96
|
+
return {
|
|
97
|
+
appKey,
|
|
98
|
+
valid: ok,
|
|
99
|
+
validation,
|
|
100
|
+
deploy,
|
|
101
|
+
error: ok ? undefined : (deploy.validation?.errors || []).join('; ') || 'deploy JSON validation failed'
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {object} item
|
|
107
|
+
* @param {string} outFormat
|
|
108
|
+
*/
|
|
109
|
+
function displayOneHumanResult(item, outFormat) {
|
|
110
|
+
emitManifestLine(item.appKey, item.validation, outFormat);
|
|
111
|
+
if (outFormat === 'json') {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
validate.displayValidationResults(item.validation);
|
|
115
|
+
if (!item.validation.valid) {
|
|
116
|
+
logger.log(formatBlockingError(`Skipped deploy JSON for ${item.appKey} (validation failed)`));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (item.deploy?.success && item.deploy.path) {
|
|
120
|
+
logger.log(formatSuccessLine(`Generated deployment JSON: ${item.deploy.path}`));
|
|
121
|
+
const warnings = item.deploy.validation?.warnings || [];
|
|
122
|
+
if (warnings.length > 0) {
|
|
123
|
+
warnings.forEach((w) => logger.log(formatWarningLine(w)));
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
logger.log(formatBlockingError(`Deploy JSON write failed for ${item.appKey}`));
|
|
128
|
+
(item.deploy?.validation?.errors || []).forEach((e) => logger.log(` • ${e}`));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {Array<object>} items
|
|
133
|
+
* @param {string} outFormat
|
|
134
|
+
*/
|
|
135
|
+
function displayBatchSummary(items, outFormat) {
|
|
136
|
+
if (outFormat === 'json') {
|
|
137
|
+
logger.log(JSON.stringify({
|
|
138
|
+
batch: true,
|
|
139
|
+
valid: items.every((i) => i.valid),
|
|
140
|
+
results: items.map((i) => ({
|
|
141
|
+
appKey: i.appKey,
|
|
142
|
+
valid: i.valid,
|
|
143
|
+
path: i.deploy?.path || null,
|
|
144
|
+
error: i.error || null
|
|
145
|
+
}))
|
|
146
|
+
}, null, 2));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const passed = items.filter((i) => i.valid);
|
|
150
|
+
const failed = items.filter((i) => !i.valid);
|
|
151
|
+
logger.log('');
|
|
152
|
+
logger.log(chalk.bold('validate-json summary'));
|
|
153
|
+
logger.log(` Passed: ${passed.length}`);
|
|
154
|
+
logger.log(` Failed: ${failed.length}`);
|
|
155
|
+
failed.forEach((i) => {
|
|
156
|
+
logger.log(formatBlockingError(`${i.appKey}${i.error ? `: ${i.error}` : ''}`));
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @param {object} opts
|
|
162
|
+
* @param {string} outFormat
|
|
163
|
+
* @returns {Promise<{ exitCode: number }>}
|
|
164
|
+
*/
|
|
165
|
+
async function runValidateJsonBatch(opts, outFormat) {
|
|
166
|
+
const names = collectBatchAppNames(opts);
|
|
167
|
+
const items = [];
|
|
168
|
+
for (const name of names) {
|
|
169
|
+
const item = await validateThenWriteDeploy(name, opts);
|
|
170
|
+
displayOneHumanResult(item, outFormat);
|
|
171
|
+
items.push(item);
|
|
172
|
+
}
|
|
173
|
+
displayBatchSummary(items, outFormat);
|
|
174
|
+
return { exitCode: items.every((i) => i.valid) ? 0 : 1 };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @param {string} appKey
|
|
179
|
+
* @param {object} opts
|
|
180
|
+
* @param {string} outFormat
|
|
181
|
+
* @returns {Promise<{ exitCode: number }>}
|
|
182
|
+
*/
|
|
183
|
+
async function runValidateJsonSingle(appKey, opts, outFormat) {
|
|
184
|
+
if (looksLikeExistingFilePath(appKey)) {
|
|
185
|
+
logger.log(
|
|
186
|
+
chalk.red(
|
|
187
|
+
'validate-json requires an appKey or systemKey (not a file path). Use aifabrix validate <file> for files.'
|
|
188
|
+
)
|
|
189
|
+
);
|
|
190
|
+
return { exitCode: 1 };
|
|
191
|
+
}
|
|
192
|
+
const item = await validateThenWriteDeploy(appKey, opts);
|
|
193
|
+
if (outFormat === 'json') {
|
|
194
|
+
logger.log(JSON.stringify({
|
|
195
|
+
appKey: item.appKey,
|
|
196
|
+
valid: item.valid,
|
|
197
|
+
path: item.deploy?.path || null,
|
|
198
|
+
validation: item.validation,
|
|
199
|
+
error: item.error || null
|
|
200
|
+
}, null, 2));
|
|
201
|
+
} else {
|
|
202
|
+
displayOneHumanResult(item, outFormat);
|
|
203
|
+
}
|
|
204
|
+
return { exitCode: item.valid ? 0 : 1 };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* @param {string|undefined} appKey
|
|
209
|
+
* @param {object} options - Commander options or opts()
|
|
210
|
+
* @returns {Promise<{ exitCode: number }>}
|
|
211
|
+
*/
|
|
212
|
+
async function runValidateJsonCommand(appKey, options) {
|
|
213
|
+
const opts = options.opts ? options.opts() : options;
|
|
214
|
+
const outFormat = (opts.format || 'default').toLowerCase();
|
|
215
|
+
if (opts.integration === true || opts.builder === true) {
|
|
216
|
+
return runValidateJsonBatch(opts, outFormat);
|
|
217
|
+
}
|
|
218
|
+
if (!appKey || typeof appKey !== 'string') {
|
|
219
|
+
logger.log(chalk.red('App or system key is required, or use --integration / --builder'));
|
|
220
|
+
return { exitCode: 1 };
|
|
221
|
+
}
|
|
222
|
+
return runValidateJsonSingle(appKey, opts, outFormat);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
module.exports = {
|
|
226
|
+
runValidateJsonCommand,
|
|
227
|
+
validateThenWriteDeploy,
|
|
228
|
+
collectBatchAppNames,
|
|
229
|
+
looksLikeExistingFilePath
|
|
230
|
+
};
|
|
@@ -8,58 +8,96 @@ const { validateExternalSystemComplete } = require('../validation/validate');
|
|
|
8
8
|
const { testExternalSystem, testExternalSystemIntegration } = require('../external-system/test');
|
|
9
9
|
const { runTestE2EForExternalSystem } = require('./test-e2e-external');
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} systemKey
|
|
13
|
+
* @returns {Array}
|
|
14
|
+
*/
|
|
15
|
+
function buildBeforeSyncSteps(systemKey) {
|
|
16
|
+
return [
|
|
17
|
+
{
|
|
18
|
+
key: 'validation',
|
|
19
|
+
run: () => validateExternalSystemComplete(systemKey, { type: 'external' }),
|
|
20
|
+
ok: (result) => result.valid === true
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
key: 'unit',
|
|
24
|
+
run: () => testExternalSystem(systemKey, { verbose: false }),
|
|
25
|
+
ok: (result) => result.valid === true
|
|
26
|
+
}
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
|
|
11
30
|
/**
|
|
12
31
|
* @param {string} systemKey
|
|
13
32
|
* @param {Object} options
|
|
14
|
-
* @returns {
|
|
33
|
+
* @returns {Array}
|
|
15
34
|
*/
|
|
16
|
-
function
|
|
17
|
-
return
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
key: 'unit',
|
|
26
|
-
run: () => testExternalSystem(systemKey, { verbose: false }),
|
|
27
|
-
ok: (result) => result.valid === true
|
|
28
|
-
}
|
|
29
|
-
],
|
|
30
|
-
afterSync: [
|
|
31
|
-
{
|
|
32
|
-
key: 'integration',
|
|
33
|
-
run: () =>
|
|
34
|
-
testExternalSystemIntegration(systemKey, {
|
|
35
|
-
environment: options.env,
|
|
36
|
-
noPublish: true,
|
|
37
|
-
verbose: false,
|
|
38
|
-
debug: options.debug === true
|
|
39
|
-
}),
|
|
40
|
-
ok: (result) => result.success === true
|
|
41
|
-
}
|
|
42
|
-
],
|
|
43
|
-
e2e: {
|
|
44
|
-
key: 'e2e',
|
|
45
|
-
run: (progress) =>
|
|
46
|
-
runTestE2EForExternalSystem(systemKey, {
|
|
47
|
-
env: options.env,
|
|
35
|
+
function buildAfterSyncSteps(systemKey, options) {
|
|
36
|
+
return [
|
|
37
|
+
{
|
|
38
|
+
key: 'integration',
|
|
39
|
+
run: () =>
|
|
40
|
+
testExternalSystemIntegration(systemKey, {
|
|
41
|
+
environment: options.env || options.environment,
|
|
48
42
|
noPublish: true,
|
|
49
43
|
verbose: false,
|
|
50
44
|
debug: options.debug === true,
|
|
51
|
-
|
|
45
|
+
// Token-forward (builder-api certify): same auth as E2E step — do not
|
|
46
|
+
// fall back to getDeploymentAuth inside the container.
|
|
52
47
|
authConfig: options.authConfig,
|
|
53
48
|
dataplaneUrl: options.dataplaneUrl,
|
|
54
|
-
silentResolve: options.silentResolve === true
|
|
55
|
-
quietPollUi: true,
|
|
56
|
-
onDatasourceProgress: progress?.setLabel
|
|
57
|
-
? ({ key, index, total }) =>
|
|
58
|
-
progress.setLabel(`Running E2E: ${key} (${index + 1}/${total})`)
|
|
59
|
-
: undefined
|
|
49
|
+
silentResolve: options.silentResolve === true
|
|
60
50
|
}),
|
|
61
51
|
ok: (result) => result.success === true
|
|
62
52
|
}
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {string} systemKey
|
|
58
|
+
* @param {Object} options
|
|
59
|
+
* @returns {Object}
|
|
60
|
+
*/
|
|
61
|
+
function buildE2eStep(systemKey, options) {
|
|
62
|
+
if (options.skipE2e === true || options.minimal === true) {
|
|
63
|
+
return {
|
|
64
|
+
key: 'e2e',
|
|
65
|
+
run: async() => ({ success: true, skipped: true, reason: 'minimal' }),
|
|
66
|
+
ok: () => true
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
key: 'e2e',
|
|
71
|
+
run: (progress) =>
|
|
72
|
+
runTestE2EForExternalSystem(systemKey, {
|
|
73
|
+
env: options.env,
|
|
74
|
+
noPublish: true,
|
|
75
|
+
verbose: false,
|
|
76
|
+
debug: options.debug === true,
|
|
77
|
+
async: options.async !== false,
|
|
78
|
+
authConfig: options.authConfig,
|
|
79
|
+
dataplaneUrl: options.dataplaneUrl,
|
|
80
|
+
silentResolve: options.silentResolve === true,
|
|
81
|
+
quietPollUi: true,
|
|
82
|
+
onDatasourceProgress: progress?.setLabel
|
|
83
|
+
? ({ key, index, total }) =>
|
|
84
|
+
progress.setLabel(`Running E2E: ${key} (${index + 1}/${total})`)
|
|
85
|
+
: undefined
|
|
86
|
+
}),
|
|
87
|
+
ok: (result) => result.success === true
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {string} systemKey
|
|
93
|
+
* @param {Object} options
|
|
94
|
+
* @returns {{ beforeSync: Array, afterSync: Array, e2e: Object }}
|
|
95
|
+
*/
|
|
96
|
+
function buildOperationsVerifyStepPlan(systemKey, options) {
|
|
97
|
+
return {
|
|
98
|
+
beforeSync: buildBeforeSyncSteps(systemKey),
|
|
99
|
+
afterSync: buildAfterSyncSteps(systemKey, options),
|
|
100
|
+
e2e: buildE2eStep(systemKey, options)
|
|
63
101
|
};
|
|
64
102
|
}
|
|
65
103
|
|
package/lib/core/audit-logger.js
CHANGED
|
@@ -238,7 +238,7 @@ async function logApiCall({ url, options, statusCode, duration, success, errorIn
|
|
|
238
238
|
const path = extractPathFromUrl(url);
|
|
239
239
|
|
|
240
240
|
// Extract controller URL from full URL
|
|
241
|
-
let controllerUrl
|
|
241
|
+
let controllerUrl;
|
|
242
242
|
try {
|
|
243
243
|
const urlObj = new URL(url);
|
|
244
244
|
controllerUrl = `${urlObj.protocol}//${urlObj.host}`;
|
package/lib/core/secrets-load.js
CHANGED
|
@@ -133,9 +133,7 @@ function mergeUserWithConfigFile(userSecrets, resolvedConfigPath, keySources) {
|
|
|
133
133
|
for (const key of Object.keys(configSecrets)) {
|
|
134
134
|
if (!(key in merged) || merged[key] === undefined || merged[key] === null || merged[key] === '') {
|
|
135
135
|
merged[key] = configSecrets[key];
|
|
136
|
-
|
|
137
|
-
keySources[key] = resolvedConfigPath;
|
|
138
|
-
}
|
|
136
|
+
keySources[key] = resolvedConfigPath;
|
|
139
137
|
}
|
|
140
138
|
}
|
|
141
139
|
return merged;
|
package/lib/generator/helpers.js
CHANGED
|
@@ -156,10 +156,10 @@ function validatePortalInput(portalInput, variableName) {
|
|
|
156
156
|
* @throws {Error} If portalInput structure is invalid
|
|
157
157
|
*/
|
|
158
158
|
/**
|
|
159
|
-
* Creates a map of
|
|
159
|
+
* Creates a map of portal overlay metadata from variables config
|
|
160
160
|
* @function createPortalInputMap
|
|
161
161
|
* @param {Object|null} variablesConfig - Configuration from application.yaml
|
|
162
|
-
* @returns {Map} Map of variable names to portalInput
|
|
162
|
+
* @returns {Map} Map of variable names to { portalInput, category? }
|
|
163
163
|
*/
|
|
164
164
|
function createPortalInputMap(variablesConfig) {
|
|
165
165
|
const portalInputMap = new Map();
|
|
@@ -168,7 +168,11 @@ function createPortalInputMap(variablesConfig) {
|
|
|
168
168
|
if (configItem.name && configItem.portalInput) {
|
|
169
169
|
// Validate portalInput before adding to map
|
|
170
170
|
validatePortalInput(configItem.portalInput, configItem.name);
|
|
171
|
-
|
|
171
|
+
const overlay = { portalInput: configItem.portalInput };
|
|
172
|
+
if (typeof configItem.category === 'string' && configItem.category.trim().length > 0) {
|
|
173
|
+
overlay.category = configItem.category.trim();
|
|
174
|
+
}
|
|
175
|
+
portalInputMap.set(configItem.name, overlay);
|
|
172
176
|
}
|
|
173
177
|
}
|
|
174
178
|
}
|
|
@@ -291,9 +295,13 @@ function createConfigItem(key, value, location, required, portalInputMap, appKey
|
|
|
291
295
|
required
|
|
292
296
|
};
|
|
293
297
|
|
|
294
|
-
// Merge portalInput if
|
|
298
|
+
// Merge portalInput / category if present in application config
|
|
295
299
|
if (portalInputMap.has(key)) {
|
|
296
|
-
|
|
300
|
+
const overlay = portalInputMap.get(key);
|
|
301
|
+
configItem.portalInput = overlay.portalInput;
|
|
302
|
+
if (typeof overlay.category === 'string' && overlay.category.length > 0) {
|
|
303
|
+
configItem.category = overlay.category;
|
|
304
|
+
}
|
|
297
305
|
}
|
|
298
306
|
|
|
299
307
|
return configItem;
|
|
@@ -66,15 +66,21 @@ function extractOptionalSections(deployment) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
* Portal-only configuration for application.yaml (name +
|
|
69
|
+
* Portal-only configuration for application.yaml (name + optional category + portalInput).
|
|
70
70
|
* @param {Array} configuration - Configuration array from deployment JSON
|
|
71
|
-
* @returns {Array<{ name: string, portalInput: Object }>}
|
|
71
|
+
* @returns {Array<{ name: string, category?: string, portalInput: Object }>}
|
|
72
72
|
*/
|
|
73
73
|
function extractPortalInputConfiguration(configuration) {
|
|
74
74
|
if (!Array.isArray(configuration) || configuration.length === 0) return [];
|
|
75
75
|
return configuration
|
|
76
76
|
.filter(item => item && item.portalInput && typeof item.name === 'string')
|
|
77
|
-
.map(item =>
|
|
77
|
+
.map(item => {
|
|
78
|
+
const entry = { name: item.name, portalInput: item.portalInput };
|
|
79
|
+
if (typeof item.category === 'string' && item.category.trim().length > 0) {
|
|
80
|
+
entry.category = item.category.trim();
|
|
81
|
+
}
|
|
82
|
+
return entry;
|
|
83
|
+
});
|
|
78
84
|
}
|
|
79
85
|
|
|
80
86
|
/**
|
|
@@ -53,6 +53,12 @@ function formatDockerInfrastructureFailure(detail) {
|
|
|
53
53
|
*/
|
|
54
54
|
async function checkDockerAvailability() {
|
|
55
55
|
await ensureDevCertsIfNeededForRemoteDocker();
|
|
56
|
+
try {
|
|
57
|
+
const { ensureDevCertFreshIfDue } = require('../commands/dev-refresh');
|
|
58
|
+
await ensureDevCertFreshIfDue({ quiet: true });
|
|
59
|
+
} catch {
|
|
60
|
+
// Best-effort renew for remote Docker TLS; docker check reports real failures
|
|
61
|
+
}
|
|
56
62
|
try {
|
|
57
63
|
await dockerUtils.ensureDockerAndCompose();
|
|
58
64
|
} catch (error) {
|
|
@@ -181,7 +181,7 @@ function generatePgAdminConfig(infraDir, postgresPassword) {
|
|
|
181
181
|
* @returns {string} Escaped string
|
|
182
182
|
*/
|
|
183
183
|
function escapePgString(s) {
|
|
184
|
-
if (
|
|
184
|
+
if (typeof s !== 'string') return '';
|
|
185
185
|
return s.replace(/'/g, '\'\'');
|
|
186
186
|
}
|
|
187
187
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* @param {string} bearerToken
|
|
13
|
-
* @returns {{ type: 'bearer', token: string }}
|
|
13
|
+
* @returns {{ type: 'bearer', token: string, preserveBearer: true }}
|
|
14
14
|
*/
|
|
15
15
|
function buildBearerAuthConfig(bearerToken) {
|
|
16
16
|
if (!bearerToken || typeof bearerToken !== 'string') {
|
|
@@ -20,7 +20,8 @@ function buildBearerAuthConfig(bearerToken) {
|
|
|
20
20
|
if (!token) {
|
|
21
21
|
throw new Error('Bearer token is required');
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
// Token-forward: caller JWT must reach dataplane CIP/pipeline unchanged (no API_KEY swap).
|
|
24
|
+
return { type: 'bearer', token, preserveBearer: true };
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
/**
|
|
@@ -19,6 +19,21 @@ function resolveEnterpriseMcpSystemKey() {
|
|
|
19
19
|
return (process.env.ENTERPRISE_MCP_SYSTEM_KEY || 'enterprise-integration-lifecycle').trim();
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* COM-generated Enterprise MCP tools sometimes use OpenAPI operationId forms
|
|
24
|
+
* (e.g. repository.getrepositorystatus) instead of canonical capability keys
|
|
25
|
+
* (repository.status). Map canonical → published name when present.
|
|
26
|
+
*
|
|
27
|
+
* @type {Record<string, string>}
|
|
28
|
+
*/
|
|
29
|
+
const ENTERPRISE_MCP_TOOL_ALIASES = {
|
|
30
|
+
'repository.status': 'repository.getrepositorystatus',
|
|
31
|
+
'repository.sync': 'repository.runrepositorysync',
|
|
32
|
+
'repository.createReview': 'repository.runrepositorycreatereview',
|
|
33
|
+
'repository.dispatchPipeline': 'repository.runrepositorydispatchpipeline',
|
|
34
|
+
'template.materializeWorkspace': 'template.runwizard'
|
|
35
|
+
};
|
|
36
|
+
|
|
22
37
|
/**
|
|
23
38
|
* @param {string} capabilityKey
|
|
24
39
|
* @returns {{ resourceType: string, toolName: string }}
|
|
@@ -29,9 +44,10 @@ function parseCapabilityKey(capabilityKey) {
|
|
|
29
44
|
if (dotIndex <= 0) {
|
|
30
45
|
throw new Error('capabilityKey must be resourceType.operation form');
|
|
31
46
|
}
|
|
47
|
+
const toolName = ENTERPRISE_MCP_TOOL_ALIASES[key] || key;
|
|
32
48
|
return {
|
|
33
49
|
resourceType: key.slice(0, dotIndex),
|
|
34
|
-
toolName
|
|
50
|
+
toolName
|
|
35
51
|
};
|
|
36
52
|
}
|
|
37
53
|
|
|
@@ -42,7 +58,7 @@ function parseCapabilityKey(capabilityKey) {
|
|
|
42
58
|
* @returns {string}
|
|
43
59
|
*/
|
|
44
60
|
function buildMcpExecutePath(systemKey, resourceType, toolName) {
|
|
45
|
-
return `/api/v1/mcp/${encodeURIComponent(systemKey)}/${encodeURIComponent(resourceType)}/tools/${encodeURIComponent(toolName)}/execute`;
|
|
61
|
+
return `/api/v1/mcp/servers/${encodeURIComponent(systemKey)}/${encodeURIComponent(resourceType)}/tools/${encodeURIComponent(toolName)}/execute`;
|
|
46
62
|
}
|
|
47
63
|
|
|
48
64
|
/**
|
|
@@ -22,6 +22,15 @@ const SCHEMA_DIR = path.join(__dirname, '..', 'schema');
|
|
|
22
22
|
let systemSchemaDoc = null;
|
|
23
23
|
/** @type {Object|null} */
|
|
24
24
|
let datasourceSchemaDoc = null;
|
|
25
|
+
const PROTOTYPE_POLLUTION_KEYS = new Set(['__proto__', 'prototype', 'constructor']);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} part
|
|
29
|
+
* @returns {boolean}
|
|
30
|
+
*/
|
|
31
|
+
function isSafePathPart(part) {
|
|
32
|
+
return typeof part === 'string' && part.length > 0 && !PROTOTYPE_POLLUTION_KEYS.has(part);
|
|
33
|
+
}
|
|
25
34
|
|
|
26
35
|
function getSystemSchemaDoc() {
|
|
27
36
|
if (!systemSchemaDoc) {
|
|
@@ -90,10 +99,18 @@ function getPathValue(doc, dottedPath) {
|
|
|
90
99
|
*/
|
|
91
100
|
function setPathValue(doc, dottedPath, value) {
|
|
92
101
|
const parts = dottedPath.split('.');
|
|
102
|
+
for (const part of parts) {
|
|
103
|
+
if (!isSafePathPart(part)) {
|
|
104
|
+
const err = new Error(`Unsafe path segment: ${part}`);
|
|
105
|
+
err.statusCode = 400;
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
93
109
|
let current = doc;
|
|
94
110
|
for (let i = 0; i < parts.length - 1; i += 1) {
|
|
95
111
|
const part = parts[i];
|
|
96
|
-
|
|
112
|
+
const next = Object.prototype.hasOwnProperty.call(current, part) ? current[part] : undefined;
|
|
113
|
+
if (next === null || next === undefined || typeof next !== 'object' || Array.isArray(next)) {
|
|
97
114
|
current[part] = {};
|
|
98
115
|
}
|
|
99
116
|
current = current[part];
|
|
@@ -12,18 +12,35 @@ const { frozenObject, schemaRef, stringArraySchema } = require('./openapi-schema
|
|
|
12
12
|
|
|
13
13
|
/** @type {Record<string, Object>} */
|
|
14
14
|
const OPENAPI_RESULT_SCHEMAS = {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
15
|
+
ProblemDetail: Object.assign(
|
|
16
|
+
frozenObject(
|
|
17
|
+
{
|
|
18
|
+
type: { type: 'string', format: 'uri-reference' },
|
|
19
|
+
title: { type: 'string' },
|
|
20
|
+
status: { type: 'integer', minimum: 100, maximum: 599 },
|
|
21
|
+
detail: { type: 'string' },
|
|
22
|
+
instance: { type: 'string' },
|
|
23
|
+
correlationId: { type: 'string' },
|
|
24
|
+
errors: { type: 'array', items: schemaRef('ProblemValidationError') }
|
|
25
|
+
},
|
|
26
|
+
['type', 'title', 'status', 'detail', 'correlationId', 'instance', 'errors']
|
|
27
|
+
),
|
|
28
|
+
{
|
|
29
|
+
example: {
|
|
30
|
+
type: '/Errors/Unauthorized',
|
|
31
|
+
title: 'Unauthorized',
|
|
32
|
+
status: 401,
|
|
33
|
+
detail: 'Missing or invalid Bearer token.',
|
|
34
|
+
instance: '/api/v1/builder/workspaces/me/systems',
|
|
35
|
+
correlationId: '00000000-0000-4000-8000-000000000003',
|
|
36
|
+
errors: []
|
|
37
|
+
}
|
|
38
|
+
}
|
|
26
39
|
),
|
|
40
|
+
/** Prefer ProblemDetail; allOf alias for published REST lint (OAPI-148). */
|
|
41
|
+
ProblemDetails: {
|
|
42
|
+
allOf: [schemaRef('ProblemDetail')]
|
|
43
|
+
},
|
|
27
44
|
ProblemValidationError: frozenObject(
|
|
28
45
|
{
|
|
29
46
|
field: { type: 'string' },
|
|
@@ -22,7 +22,7 @@ const CORE_SCHEMA_DESCRIPTIONS = {
|
|
|
22
22
|
RepositorySyncDirection: {
|
|
23
23
|
_schema: 'Repository sync direction: pull remote into workspace or push workspace to remote.'
|
|
24
24
|
},
|
|
25
|
-
|
|
25
|
+
ProblemDetail: {
|
|
26
26
|
_schema: 'RFC 7807 problem response for builder-api errors (application/problem+json).',
|
|
27
27
|
type: 'Problem type URI or identifier.',
|
|
28
28
|
title: 'Short human-readable error title.',
|
|
@@ -32,8 +32,11 @@ const CORE_SCHEMA_DESCRIPTIONS = {
|
|
|
32
32
|
correlationId: 'Correlation id for log lookup across services.',
|
|
33
33
|
errors: 'Field-level validation errors when status is 400.'
|
|
34
34
|
},
|
|
35
|
+
ProblemDetails: {
|
|
36
|
+
_schema: 'Alias of ProblemDetail (allOf) for backward-compatible $refs.'
|
|
37
|
+
},
|
|
35
38
|
ProblemValidationError: {
|
|
36
|
-
_schema: 'Single field validation error inside
|
|
39
|
+
_schema: 'Single field validation error inside ProblemDetail.errors.',
|
|
37
40
|
field: 'JSON pointer or field name that failed validation.',
|
|
38
41
|
message: 'Validation failure message.',
|
|
39
42
|
value: 'Rejected value when safe to echo (may be omitted for secrets).'
|