@aifabrix/builder 2.59.0 → 2.61.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 +14 -11
- package/docs/README.md +80 -0
- package/docs/builder-help/evidence-patterns.json +155 -0
- package/docs/builder-help/golden-examples/crm-company.json +30 -0
- package/docs/builder-help/golden-examples/crm-deal.json +29 -0
- package/docs/builder-help/golden-examples/document-storage-keyed-get.json +85 -0
- package/docs/builder-help/golden-examples/document-storage.json +30 -0
- package/docs/builder-help/golden-examples/meeting-transcript.json +29 -0
- package/docs/builder-help/golden-examples/repository-template.json +29 -0
- package/docs/builder-help/golden-examples/service-ticket.json +29 -0
- package/docs/builder-help/platform-roles.json +98 -0
- package/docs/builder-help/resource-type-catalog.json +402 -0
- package/lib/agent-kit/git-identity.js +180 -0
- package/lib/agent-kit/setup.js +3 -0
- package/lib/agent-kit/start.js +45 -1
- package/lib/api/configuration.api.js +131 -0
- package/lib/api/role-assistant-test-job.api.js +60 -0
- package/lib/api/system-secrets.api.js +72 -0
- package/lib/api/work-search.api.js +13 -6
- package/lib/app/deploy.js +8 -1
- package/lib/app/run-docker-fallback.js +6 -1
- package/lib/app/run-helpers.js +2 -1
- package/lib/app/run-parameter-sync.js +142 -0
- package/lib/app/show-display.js +1 -0
- package/lib/app/show-online.js +15 -0
- package/lib/build/docker-build-args.js +5 -2
- package/lib/build/index.js +3 -2
- package/lib/build/standard-docker-build.js +6 -3
- package/lib/cli/setup-app.js +19 -1
- package/lib/cli/setup-environment.js +156 -0
- package/lib/cli/setup-utility.js +24 -1
- package/lib/commands/datasource-capability-upsert-cli.js +112 -0
- package/lib/commands/datasource-capability.js +4 -2
- package/lib/commands/env-secret-context.js +113 -0
- package/lib/commands/env-secret-list.js +200 -0
- package/lib/commands/env-secret-push-confirm.js +54 -0
- package/lib/commands/env-secret-push-run.js +227 -0
- package/lib/commands/env-secret-push.js +168 -0
- package/lib/commands/repair-datasource-apply.js +2 -0
- package/lib/commands/repair-datasource-keyed-document.js +122 -0
- package/lib/commands/repair-datasource-run.js +1 -0
- package/lib/commands/role-assistant.js +7 -0
- package/lib/commands/setup-modes.js +1 -8
- package/lib/commands/setup-prompts.js +2 -180
- package/lib/commands/verify-operations-skip-e2e.js +25 -1
- package/lib/commands/verify-operations-steps.js +13 -1
- package/lib/commands/wizard-config-normalizer.js +7 -4
- package/lib/commands/wizard-core.js +5 -157
- package/lib/commands/wizard-file-saving.js +163 -0
- package/lib/core/env-platform-expand.js +97 -0
- package/lib/core/secrets-env-content.js +42 -4
- package/lib/core/secrets-env-write.js +10 -3
- package/lib/core/secrets-load.js +4 -2
- package/lib/datasource/binary-documents-validator.js +190 -0
- package/lib/datasource/capability/run-capability-upsert.js +202 -0
- package/lib/datasource/capability/upsert-ingredients.js +291 -0
- package/lib/datasource/capability/upsert-operations.js +138 -0
- package/lib/datasource/capability/upsert-test-scaffold.js +189 -0
- package/lib/datasource/validate.js +12 -5
- package/lib/deployment/installation/azure-infra-stage.js +3 -1
- package/lib/deployment/installation/infra-catalog.js +2 -5
- package/lib/generator/builders.js +17 -0
- package/lib/generator/helpers.js +23 -2
- package/lib/generator/index.js +13 -6
- package/lib/lifecycle/product-model.js +4 -3
- package/lib/lifecycle/report-display.js +3 -2
- package/lib/parameters/infra-parameter-catalog.js +1 -1
- package/lib/programmatic/builder-help-enterprise-sync-fabrix.js +1 -1
- package/lib/programmatic/builder-help-governance.js +1 -1
- package/lib/programmatic/builder-help.js +1 -1
- package/lib/role-assistant/test-cases-search.js +3 -0
- package/lib/role-assistant/test-job-runner.js +125 -0
- package/lib/role-assistant/test-runner-search.js +44 -2
- package/lib/role-assistant/test-runner-workhub-answers.js +4 -1
- package/lib/role-assistant/test-runner-workhub-missing-fields.js +42 -0
- package/lib/role-assistant/test-runner-workhub-wait-stop.js +94 -0
- package/lib/role-assistant/test-runner-workhub.js +28 -19
- package/lib/schema/application-schema.json +205 -2
- package/lib/schema/external-datasource.schema.json +23 -3
- package/lib/schema/infra-parameter.schema.json +139 -33
- package/lib/schema/infra.parameter.yaml +416 -66
- package/lib/schema/infrastructure-schema.json +10 -35
- package/lib/utils/compose-generate-docker-compose.js +16 -9
- package/lib/utils/datasource-binary-evidence.js +92 -0
- package/lib/utils/datasource-test-run-capability-scope.js +44 -1
- package/lib/utils/datasource-test-run-debug-display.js +2 -0
- package/lib/utils/datasource-test-run-display.js +8 -2
- package/lib/utils/datasource-test-run-issue-guidance.js +176 -0
- package/lib/utils/datasource-test-run-tty-log.js +2 -0
- package/lib/utils/docker-build.js +29 -8
- package/lib/utils/docker-manifest-public-port.js +36 -0
- package/lib/utils/env-copy.js +11 -10
- package/lib/utils/external-system-system-test-tty.js +3 -2
- package/lib/utils/image-tags.js +2 -2
- package/lib/utils/platform-kv-ref.js +1 -1
- package/lib/utils/platform-resolution.js +226 -0
- package/lib/utils/prepare-local-data-mount.js +58 -0
- package/lib/utils/resolve-docker-image-ref.js +8 -3
- package/lib/utils/secrets-helpers.js +0 -1
- package/lib/utils/system-secret-mapping.js +125 -0
- package/lib/utils/test-log-writer.js +2 -1
- package/lib/validation/external-manifest-validator.js +5 -0
- package/lib/validation/openapi-contract-surface-validator.js +3 -1
- package/lib/validation/validate-external-file.js +5 -1
- package/package.json +5 -4
- package/templates/README.md +2 -1
- package/templates/agent-kit/agent-kit.yaml +4 -1
- package/templates/agent-kit/instructions/AGENTKIT.md +1 -1
- package/templates/agent-kit/instructions/root.AGENTS.md +2 -0
- package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +35 -2
- package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +15 -0
- package/templates/agent-kit/skills/aifabrix-connected-system/scripts/delivery-verdict.js +176 -0
- package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +6 -2
- package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +28 -2
- package/templates/agent-kit/skills/aifabrix-prove/references/evidence-lifecycle.md +22 -0
- package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +28 -1
- package/templates/agent-kit/skills/aifabrix-role-assistant/references/testing-playbook.md +117 -0
- package/templates/agent-kit/skills/shared/feedback.md +31 -0
- package/templates/agent-kit/skills/shared/hosts.md +13 -3
- package/templates/agent-kit/skills/shared/interaction.md +83 -0
- package/templates/agent-kit/skills/shared/status.md +76 -0
- package/templates/agent-kit/workspace/BUILDER_IMPROVEMENT_FINDINGS.md +27 -0
- package/templates/applications/builder-api/application.yaml +1 -1
- package/templates/applications/builder-api/env.template +5 -1
- package/templates/applications/dataplane/application.yaml +30 -2
- package/templates/applications/dataplane/env.template +36 -5
- package/templates/applications/keycloak/application.yaml +6 -1
- package/templates/applications/miso-controller/application.yaml +96 -1
- package/templates/applications/miso-controller/env.template +63 -37
- package/templates/applications/miso-controller/rbac.yaml +17 -0
- package/templates/external-system/external-datasource.yaml.hbs +7 -1
- package/templates/marketplace/main.json +73 -225
- package/templates/python/Dockerfile.hbs +2 -0
- package/templates/python/docker-compose.hbs +1 -1
- package/templates/typescript/Dockerfile.hbs +2 -0
- package/templates/typescript/docker-compose.hbs +12 -0
- package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +0 -34
- /package/{lib/programmatic/help-content → docs/builder-help/content}/channel-onboarding.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/cip-overview.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/connected-system-ui.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/dimensions-guide.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/enterprise-sync-fabrix.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/overview.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/subscription-guide.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/workflow.md +0 -0
package/lib/utils/env-copy.js
CHANGED
|
@@ -56,16 +56,17 @@ function readDeveloperIdFromConfig(config) {
|
|
|
56
56
|
* Creates the mount folder on the local filesystem (next to the .env file) when it does not exist.
|
|
57
57
|
* @param {string} content - Env file content
|
|
58
58
|
* @param {string} outputPath - Resolved path of the .env file being written
|
|
59
|
+
* @param {string} [appName] - System key (defaults to output directory name)
|
|
59
60
|
* @returns {string} Content with /mnt/data replaced by path to mount directory
|
|
60
61
|
*/
|
|
61
|
-
function substituteMntDataForLocal(content, outputPath) {
|
|
62
|
+
function substituteMntDataForLocal(content, outputPath, appName) {
|
|
62
63
|
const outputDir = path.dirname(outputPath);
|
|
63
64
|
// Avoid introducing a drive letter for root-relative paths (e.g. "\tmp\out"),
|
|
64
65
|
// which are common in unit tests and some Windows path constructions.
|
|
65
66
|
const isWinRootRelative = process.platform === 'win32' && /^\\(?!\\)/.test(outputDir);
|
|
66
67
|
const localMountPath = isWinRootRelative
|
|
67
|
-
? path.win32.normalize(path.win32.join(outputDir, 'mount'))
|
|
68
|
-
: path.resolve(outputDir, 'mount');
|
|
68
|
+
? path.win32.normalize(path.win32.join(outputDir, 'mount', 'data', appName || path.basename(outputDir)))
|
|
69
|
+
: path.resolve(outputDir, 'mount', 'data', appName || path.basename(outputDir));
|
|
69
70
|
if (!fs.existsSync(localMountPath)) {
|
|
70
71
|
fs.mkdirSync(localMountPath, { recursive: true });
|
|
71
72
|
}
|
|
@@ -140,12 +141,12 @@ async function writeEnvOutputForReload(outputPath, runEnvPath, appName) {
|
|
|
140
141
|
baseContent = await fsp.readFile(outputPath, 'utf8');
|
|
141
142
|
} else if (appName) {
|
|
142
143
|
baseContent = await generateEnvContent(appName, null, 'local', false);
|
|
143
|
-
baseContent = substituteMntDataForLocal(baseContent, outputPath);
|
|
144
|
+
baseContent = substituteMntDataForLocal(baseContent, outputPath, appName);
|
|
144
145
|
} else {
|
|
145
146
|
baseContent = runContent;
|
|
146
147
|
}
|
|
147
148
|
let toWrite = mergeEnvMapIntoContent(baseContent, runMap, { appendMissingFromNewMap: false });
|
|
148
|
-
toWrite = substituteMntDataForLocal(toWrite, outputPath);
|
|
149
|
+
toWrite = substituteMntDataForLocal(toWrite, outputPath, appName);
|
|
149
150
|
await fsp.writeFile(outputPath, toWrite, { mode: 0o600 });
|
|
150
151
|
logger.log(
|
|
151
152
|
formatSuccessLine(
|
|
@@ -163,7 +164,7 @@ async function writeEnvOutputForReload(outputPath, runEnvPath, appName) {
|
|
|
163
164
|
async function writeEnvOutputForLocal(appName, outputPath) {
|
|
164
165
|
const { generateEnvContent, parseEnvContentToMap, mergeEnvMapIntoContent } = require('../core/secrets');
|
|
165
166
|
let localContent = await generateEnvContent(appName, null, 'local', false);
|
|
166
|
-
localContent = substituteMntDataForLocal(localContent, outputPath);
|
|
167
|
+
localContent = substituteMntDataForLocal(localContent, outputPath, appName);
|
|
167
168
|
let toWrite = localContent;
|
|
168
169
|
if (fs.existsSync(outputPath)) {
|
|
169
170
|
const existingContent = await fsp.readFile(outputPath, 'utf8');
|
|
@@ -319,7 +320,7 @@ async function patchEnvContentForLocal(envContent, variables) {
|
|
|
319
320
|
async function syncWrittenBuilderEnvToOutputPath(envPath, outputPath, envOutputPathLabel, copyOptions = {}) {
|
|
320
321
|
const { parseEnvContentToMap, mergeEnvMapIntoContent } = require('../core/secrets');
|
|
321
322
|
const raw = fs.readFileSync(envPath, 'utf8');
|
|
322
|
-
const synced = substituteMntDataForLocal(raw, outputPath);
|
|
323
|
+
const synced = substituteMntDataForLocal(raw, outputPath, copyOptions.appName || path.basename(path.dirname(envPath)));
|
|
323
324
|
let toWrite = synced;
|
|
324
325
|
if (!copyOptions.freshEnv && fs.existsSync(outputPath)) {
|
|
325
326
|
const existingContent = fs.readFileSync(outputPath, 'utf8');
|
|
@@ -344,7 +345,7 @@ async function writeLocalEnvToOutputPath(outputPath, appName, secretsPath, envOu
|
|
|
344
345
|
const { generateEnvContent, parseEnvContentToMap, mergeEnvMapIntoContent } = require('../core/secrets');
|
|
345
346
|
const genOpts = extraOpts.appPath ? { appPath: extraOpts.appPath } : {};
|
|
346
347
|
let localEnvContent = await generateEnvContent(appName, secretsPath, 'local', false, genOpts);
|
|
347
|
-
localEnvContent = substituteMntDataForLocal(localEnvContent, outputPath);
|
|
348
|
+
localEnvContent = substituteMntDataForLocal(localEnvContent, outputPath, appName);
|
|
348
349
|
let toWrite = localEnvContent;
|
|
349
350
|
if (!extraOpts.freshEnv && fs.existsSync(outputPath)) {
|
|
350
351
|
const existingContent = fs.readFileSync(outputPath, 'utf8');
|
|
@@ -366,7 +367,7 @@ async function writeLocalEnvToOutputPath(outputPath, appName, secretsPath, envOu
|
|
|
366
367
|
async function writePatchedEnvToOutputPath(envPath, outputPath, variables, envOutputPathLabel) {
|
|
367
368
|
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
368
369
|
let patchedContent = await patchEnvContentForLocal(envContent, variables);
|
|
369
|
-
patchedContent = substituteMntDataForLocal(patchedContent, outputPath);
|
|
370
|
+
patchedContent = substituteMntDataForLocal(patchedContent, outputPath, variables.app?.key || path.basename(path.dirname(envPath)));
|
|
370
371
|
fs.writeFileSync(outputPath, patchedContent, { mode: 0o600 });
|
|
371
372
|
logger.log(formatSuccessLine(`Copied .env to: ${envOutputPathLabel}`));
|
|
372
373
|
}
|
|
@@ -405,7 +406,7 @@ async function copyOrRegenerateEnvForNamedApp(opts) {
|
|
|
405
406
|
return;
|
|
406
407
|
}
|
|
407
408
|
if (fs.existsSync(envPath)) {
|
|
408
|
-
await syncWrittenBuilderEnvToOutputPath(envPath, outputPath, label, { freshEnv: freshEnv === true });
|
|
409
|
+
await syncWrittenBuilderEnvToOutputPath(envPath, outputPath, label, { freshEnv: freshEnv === true, appName });
|
|
409
410
|
return;
|
|
410
411
|
}
|
|
411
412
|
await writeLocalEnvToOutputPath(outputPath, appName, secretsPath, label, localOpts);
|
|
@@ -182,7 +182,9 @@ function pickBlockingDatasourceKey(rows) {
|
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
function issueKey(issue) {
|
|
185
|
-
const code = issue && issue.
|
|
185
|
+
const code = issue && (issue.stableCode || issue.code)
|
|
186
|
+
? String(issue.stableCode || issue.code)
|
|
187
|
+
: '';
|
|
186
188
|
const msg = issue && issue.message ? String(issue.message) : '';
|
|
187
189
|
return `${code}::${msg}`.toLowerCase();
|
|
188
190
|
}
|
|
@@ -402,4 +404,3 @@ module.exports = {
|
|
|
402
404
|
systemCertStatus,
|
|
403
405
|
drillDownCommand
|
|
404
406
|
};
|
|
405
|
-
|
package/lib/utils/image-tags.js
CHANGED
|
@@ -48,7 +48,7 @@ function expandTag(entry, version) {
|
|
|
48
48
|
* @throws {Error} On blank entries, unsupported placeholders, missing version or invalid tags
|
|
49
49
|
*/
|
|
50
50
|
function resolveImageTags(tagInput, appVersion) {
|
|
51
|
-
if (tagInput === undefined || tagInput === null
|
|
51
|
+
if (tagInput === undefined || tagInput === null) {
|
|
52
52
|
return [DEFAULT_TAG];
|
|
53
53
|
}
|
|
54
54
|
const entries = String(tagInput).split(',').map((entry) => entry.trim());
|
|
@@ -72,7 +72,7 @@ function resolveImageTags(tagInput, appVersion) {
|
|
|
72
72
|
*/
|
|
73
73
|
function resolveBuildTagPlan(tagInput, appVersion) {
|
|
74
74
|
const tags = resolveImageTags(tagInput, appVersion);
|
|
75
|
-
const explicitSingle =
|
|
75
|
+
const explicitSingle = tagInput !== undefined && tagInput !== null && !String(tagInput).includes(',');
|
|
76
76
|
if (explicitSingle && !FLOATING_TAGS.includes(tags[0])) {
|
|
77
77
|
tags.push(DEFAULT_TAG);
|
|
78
78
|
}
|
|
@@ -42,7 +42,7 @@ function isPlatformCatalogKvRef(val) {
|
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* True when repair should normalize a kv value to path-style kv://systemKey/variable.
|
|
45
|
-
* Platform catalog refs (e.g. kv://
|
|
45
|
+
* Platform catalog refs (e.g. kv://redis-passwordKeyVault) are never legacy.
|
|
46
46
|
*
|
|
47
47
|
* @param {string} val - Value from authentication.security or configuration
|
|
48
48
|
* @returns {boolean}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform and environment configuration resolution (plan 200.0).
|
|
3
|
+
*
|
|
4
|
+
* The deployment's Azure resource group is matched against the manifest `platforms` list, yielding
|
|
5
|
+
* `{platform}` and `{platform.name}`. Platform values expand from those, then the matching
|
|
6
|
+
* `environments` entry overrides by name. The result is one flat set of names that `env.template`
|
|
7
|
+
* references as `{NAME}`.
|
|
8
|
+
*
|
|
9
|
+
* Pure by contract: manifest + resource group + environment key in, resolved set out. No
|
|
10
|
+
* filesystem, no Azure, no network — the same function must be implementable in Miso and produce
|
|
11
|
+
* identical output for the shared test vectors.
|
|
12
|
+
*
|
|
13
|
+
* @fileoverview Resource-group driven configuration resolution
|
|
14
|
+
* @author AI Fabrix Team
|
|
15
|
+
* @version 1.0.0
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
/** Local Docker has no resource group; every local run resolves as this platform. */
|
|
21
|
+
const LOCAL_PLATFORM = 'dev';
|
|
22
|
+
/** Capture for `*` in a resource pattern: one group-name segment. */
|
|
23
|
+
const NAME_CAPTURE = '[A-Za-z0-9][A-Za-z0-9-]*';
|
|
24
|
+
const NAME_WILDCARD = '*';
|
|
25
|
+
|
|
26
|
+
function escapeLiteral(text) {
|
|
27
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Compiles a `resource` pattern into a matcher.
|
|
32
|
+
* `{platform}` matches one of `options`; `*` captures a name segment.
|
|
33
|
+
* @param {Object} entry - Platform entry
|
|
34
|
+
* @returns {{ regex: RegExp, order: string[] }} Matcher and the capture order
|
|
35
|
+
*/
|
|
36
|
+
function compileResourcePattern(entry) {
|
|
37
|
+
const order = [];
|
|
38
|
+
let source = '';
|
|
39
|
+
const pattern = String(entry.resource || '');
|
|
40
|
+
const tokens = pattern.split(/(\{platform\}|\*)/);
|
|
41
|
+
for (const token of tokens) {
|
|
42
|
+
if (token === '{platform}') {
|
|
43
|
+
const options = Array.isArray(entry.options) ? entry.options : [];
|
|
44
|
+
source += `(${options.map(escapeLiteral).join('|')})`;
|
|
45
|
+
order.push('platform');
|
|
46
|
+
} else if (token === NAME_WILDCARD) {
|
|
47
|
+
source += `(${NAME_CAPTURE})`;
|
|
48
|
+
order.push('name');
|
|
49
|
+
} else if (token) {
|
|
50
|
+
source += escapeLiteral(token);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { regex: new RegExp(`^${source}$`), order };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {Object} entry - Platform entry
|
|
58
|
+
* @throws {Error} When the entry cannot yield a platform
|
|
59
|
+
*/
|
|
60
|
+
function assertPlatformDeclaration(entry) {
|
|
61
|
+
const hasToken = String(entry.resource || '').includes('{platform}');
|
|
62
|
+
const hasOptions = Array.isArray(entry.options) && entry.options.length > 0;
|
|
63
|
+
if (hasToken && !hasOptions) {
|
|
64
|
+
throw new Error(`Platform "${entry.resource}" uses {platform} but declares no options`);
|
|
65
|
+
}
|
|
66
|
+
if (!hasToken && !entry.platform) {
|
|
67
|
+
throw new Error(`Platform "${entry.resource}" has no {platform} token, so it must declare platform:`);
|
|
68
|
+
}
|
|
69
|
+
if (hasToken && entry.platform) {
|
|
70
|
+
throw new Error(`Platform "${entry.resource}" declares both {platform} and platform:; use one`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function matchEntry(entry, resourceGroup) {
|
|
75
|
+
assertPlatformDeclaration(entry);
|
|
76
|
+
const { regex, order } = compileResourcePattern(entry);
|
|
77
|
+
const found = regex.exec(resourceGroup);
|
|
78
|
+
if (!found) return null;
|
|
79
|
+
const captured = {};
|
|
80
|
+
order.forEach((key, index) => {
|
|
81
|
+
captured[key] = found[index + 1];
|
|
82
|
+
});
|
|
83
|
+
const platform = captured.platform || entry.platform;
|
|
84
|
+
const name = entry.name === NAME_WILDCARD ? captured.name : entry.name;
|
|
85
|
+
if (!name) {
|
|
86
|
+
throw new Error(`Platform "${entry.resource}" uses name: * but the pattern captures no name segment`);
|
|
87
|
+
}
|
|
88
|
+
return { entry, platform, name };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Matches a resource group against the manifest platforms.
|
|
93
|
+
* @param {Array} platforms - Manifest `platforms`
|
|
94
|
+
* @param {string} resourceGroup - Target Azure resource group
|
|
95
|
+
* @returns {{ entry: Object, platform: string, name: string }} The single match
|
|
96
|
+
* @throws {Error} On no match or more than one match
|
|
97
|
+
*/
|
|
98
|
+
function matchPlatform(platforms, resourceGroup) {
|
|
99
|
+
const entries = Array.isArray(platforms) ? platforms : [];
|
|
100
|
+
const group = String(resourceGroup || '').trim();
|
|
101
|
+
if (!group) {
|
|
102
|
+
throw new Error('A resource group is required to resolve a platform');
|
|
103
|
+
}
|
|
104
|
+
const matches = entries.map(entry => matchEntry(entry, group)).filter(Boolean);
|
|
105
|
+
if (matches.length === 0) {
|
|
106
|
+
const tried = entries.map(entry => `"${entry.resource}"`).join(', ') || '(none declared)';
|
|
107
|
+
throw new Error(`Resource group "${group}" matches no platform. Patterns tried: ${tried}`);
|
|
108
|
+
}
|
|
109
|
+
if (matches.length > 1) {
|
|
110
|
+
const ambiguous = matches.map(match => `"${match.entry.resource}"`).join(', ');
|
|
111
|
+
throw new Error(`Resource group "${group}" matches more than one platform: ${ambiguous}. Make the patterns exclusive.`);
|
|
112
|
+
}
|
|
113
|
+
return matches[0];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Expands `{platform}`, `{platform.name}` and `{resourceGroup}` in a value.
|
|
118
|
+
* @param {string} value - Raw value
|
|
119
|
+
* @param {Object} context - `platform`, `name`, `resourceGroup`
|
|
120
|
+
* @returns {string}
|
|
121
|
+
* @throws {Error} On an unknown token
|
|
122
|
+
*/
|
|
123
|
+
function expandPlatformTokens(value, context) {
|
|
124
|
+
const text = value === undefined || value === null ? '' : String(value);
|
|
125
|
+
return text.replace(/\{([a-zA-Z][a-zA-Z.]*)\}/g, (match, token) => {
|
|
126
|
+
if (token === 'platform') return context.platform;
|
|
127
|
+
if (token === 'platform.name') return context.name;
|
|
128
|
+
if (token === 'resourceGroup') return context.resourceGroup;
|
|
129
|
+
throw new Error(`Unsupported token ${match}; use {platform}, {platform.name} or {resourceGroup}`);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function applyItems(target, items, context) {
|
|
134
|
+
for (const item of Array.isArray(items) ? items : []) {
|
|
135
|
+
target.set(item.name, expandPlatformTokens(item.value, context));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Local runs have no resource group, but still need platform values. The first entry that can
|
|
141
|
+
* produce the local platform supplies them; an `environments` entry still overrides by name.
|
|
142
|
+
* A `*` name cannot be captured without a group, so it resolves to ''.
|
|
143
|
+
* @param {Array} platforms - Manifest platforms
|
|
144
|
+
* @returns {{ entry: Object, platform: string, name: string }|null}
|
|
145
|
+
*/
|
|
146
|
+
function matchLocalPlatform(platforms) {
|
|
147
|
+
const entry = platforms.find(item =>
|
|
148
|
+
item.platform === LOCAL_PLATFORM ||
|
|
149
|
+
(Array.isArray(item.options) && item.options.includes(LOCAL_PLATFORM)));
|
|
150
|
+
if (!entry) return null;
|
|
151
|
+
return { entry, platform: LOCAL_PLATFORM, name: entry.name === NAME_WILDCARD ? '' : entry.name };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @param {Object} manifest - Application manifest
|
|
156
|
+
* @param {string} resourceGroup - Target resource group ('' for local Docker)
|
|
157
|
+
* @returns {{ entry: Object, platform: string, name: string }|null}
|
|
158
|
+
*/
|
|
159
|
+
function matchIfDeclared(manifest, resourceGroup, allowLocalPlatform) {
|
|
160
|
+
const platforms = manifest && Array.isArray(manifest.platforms) ? manifest.platforms : [];
|
|
161
|
+
if (platforms.length === 0) return null;
|
|
162
|
+
if (!resourceGroup) return allowLocalPlatform ? matchLocalPlatform(platforms) : null;
|
|
163
|
+
return matchPlatform(platforms, resourceGroup);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function environmentEntry(manifest, environment) {
|
|
167
|
+
const entries = manifest && Array.isArray(manifest.environments) ? manifest.environments : [];
|
|
168
|
+
return entries.find(item => item.environment === environment) || null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function applyBuiltIns(values, context, environment) {
|
|
172
|
+
const builtIns = [
|
|
173
|
+
// PLATFORM is omitted when nothing determined it, so a consumer fails closed on {PLATFORM}
|
|
174
|
+
// rather than receiving a guess.
|
|
175
|
+
...(context.platform ? [['PLATFORM', context.platform]] : []),
|
|
176
|
+
['PLATFORM_NAME', context.name],
|
|
177
|
+
['RESOURCE_GROUP', context.resourceGroup],
|
|
178
|
+
['ENVIRONMENT', environment]
|
|
179
|
+
];
|
|
180
|
+
// Built-ins never overwrite an authored name of the same spelling.
|
|
181
|
+
for (const [key, value] of builtIns) {
|
|
182
|
+
if (!values.has(key)) values.set(key, value);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Resolves the configuration set for one deployment.
|
|
188
|
+
* @param {Object} manifest - Application manifest with optional `platforms` / `environments`
|
|
189
|
+
* @param {Object} target - `resourceGroup` (omitted for local Docker), `environment`, and
|
|
190
|
+
* `allowLocalPlatform` (default true). A caller that is not a local run — the Controller, which
|
|
191
|
+
* knows the environment but may not know the group — passes false so a missing group resolves no
|
|
192
|
+
* platform at all instead of silently claiming `dev`.
|
|
193
|
+
* @returns {{ platform: string|null, name: string|null, resourceGroup: string|null, values: Object }}
|
|
194
|
+
*/
|
|
195
|
+
function resolveConfiguration(manifest, target = {}) {
|
|
196
|
+
const resourceGroup = target.resourceGroup ? String(target.resourceGroup).trim() : '';
|
|
197
|
+
const environment = target.environment ? String(target.environment).trim() : '';
|
|
198
|
+
const allowLocalPlatform = target.allowLocalPlatform !== false;
|
|
199
|
+
const matched = matchIfDeclared(manifest, resourceGroup, allowLocalPlatform);
|
|
200
|
+
const localPlatform = allowLocalPlatform ? LOCAL_PLATFORM : '';
|
|
201
|
+
const context = {
|
|
202
|
+
platform: matched ? matched.platform : localPlatform,
|
|
203
|
+
name: matched ? matched.name : '',
|
|
204
|
+
resourceGroup
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const values = new Map();
|
|
208
|
+
if (matched) applyItems(values, matched.entry.configuration, context);
|
|
209
|
+
const envEntry = environmentEntry(manifest, environment);
|
|
210
|
+
if (envEntry) applyItems(values, envEntry.configuration, context);
|
|
211
|
+
applyBuiltIns(values, context, environment);
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
platform: context.platform || null,
|
|
215
|
+
name: matched && matched.name ? matched.name : null,
|
|
216
|
+
resourceGroup: resourceGroup || null,
|
|
217
|
+
values: Object.fromEntries(values)
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = {
|
|
222
|
+
LOCAL_PLATFORM,
|
|
223
|
+
matchPlatform,
|
|
224
|
+
expandPlatformTokens,
|
|
225
|
+
resolveConfiguration
|
|
226
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prepare persistent storage before starting a local application container.
|
|
3
|
+
* @fileoverview Local storage mount and configured DATA_DIR initialization
|
|
4
|
+
*/
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const dotenv = require('dotenv');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Read only a DATA_DIR beneath the container's storage mount.
|
|
13
|
+
* @param {string|undefined} envFile - Resolved application env file
|
|
14
|
+
* @returns {string[]} Relative directory components
|
|
15
|
+
*/
|
|
16
|
+
function dataDirectoryParts(envFile) {
|
|
17
|
+
if (!envFile || !fs.existsSync(envFile)) return [];
|
|
18
|
+
const { DATA_DIR: dataDir } = dotenv.parse(fs.readFileSync(envFile, 'utf8'));
|
|
19
|
+
if (!dataDir || !dataDir.startsWith('/mnt/data/')) return [];
|
|
20
|
+
const parts = dataDir.slice('/mnt/data/'.length).split('/').filter(part => part && part !== '.');
|
|
21
|
+
if (parts.some(part => part === '..' || part.includes('\\') || part.includes('\0'))) {
|
|
22
|
+
throw new Error('DATA_DIR must remain inside the /mnt/data storage mount');
|
|
23
|
+
}
|
|
24
|
+
return parts;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create the mount root and any configured DATA_DIR inside it, preserving state.
|
|
29
|
+
* Refuse child symlinks so initialization cannot write outside the storage root.
|
|
30
|
+
* @param {string} localPath - Host directory mounted at /mnt/data
|
|
31
|
+
* @param {string|undefined} envFile - Resolved application env file
|
|
32
|
+
* @returns {void}
|
|
33
|
+
*/
|
|
34
|
+
function prepareLocalDataMount(localPath, envFile) {
|
|
35
|
+
try {
|
|
36
|
+
const parts = dataDirectoryParts(envFile);
|
|
37
|
+
fs.mkdirSync(localPath, { recursive: true });
|
|
38
|
+
let directory = localPath;
|
|
39
|
+
for (const part of parts) {
|
|
40
|
+
directory = path.join(directory, part);
|
|
41
|
+
try {
|
|
42
|
+
const stat = fs.lstatSync(directory);
|
|
43
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
44
|
+
throw new Error('DATA_DIR components must be directories, not symlinks or files');
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error.code !== 'ENOENT') throw error;
|
|
48
|
+
fs.mkdirSync(directory);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
// Filesystem errors can include env content or paths; report only their code.
|
|
53
|
+
const reason = error.code || error.message;
|
|
54
|
+
throw new Error(`Unable to prepare local storage before container startup: ${reason}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { prepareLocalDataMount };
|
|
@@ -148,16 +148,21 @@ function qualifyRepository(registry, repository) {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
/**
|
|
151
|
-
* Deployment/registration image reference (`registry/name:tag`)
|
|
151
|
+
* Deployment/registration image reference (`registry/name:tag`).
|
|
152
|
+
*
|
|
153
|
+
* Deliberately ignores the installation registry (`dev set-registry`): a generated manifest is a
|
|
154
|
+
* committed artifact shared by the team, so it must carry the application's own `image.registry`,
|
|
155
|
+
* not one developer's local default. `push` applies the installation registry when it publishes,
|
|
156
|
+
* and an explicit `options.registry` still wins here for CI that targets another registry.
|
|
152
157
|
* @param {Object} variables - Application manifest
|
|
153
158
|
* @param {string} fallbackName - Name when the manifest has no image name or app key
|
|
154
|
-
* @param {Object} [options] -
|
|
159
|
+
* @param {Object} [options] - `registry` (explicit override only)
|
|
155
160
|
* @returns {string}
|
|
156
161
|
*/
|
|
157
162
|
function buildManifestImageReference(variables, fallbackName, options = {}) {
|
|
158
163
|
const imageName = variables?.image?.name || variables?.app?.key || fallbackName;
|
|
159
164
|
const tag = variables?.image?.tag || 'latest';
|
|
160
|
-
const { registry } = resolveEffectiveRegistry(variables, options);
|
|
165
|
+
const { registry } = resolveEffectiveRegistry(variables, { ...options, installationRegistry: '' });
|
|
161
166
|
return `${qualifyRepository(registry, imageName)}:${tag}`;
|
|
162
167
|
}
|
|
163
168
|
|
|
@@ -55,7 +55,6 @@ const { resolveBashKvFromProcessEnv } = require('./secrets-bash-kv');
|
|
|
55
55
|
const EMPTY_ALLOWED_KV_FALLBACK = new Set([
|
|
56
56
|
'redis-passwordKeyVault',
|
|
57
57
|
'azure-openaiapi-urlKeyVault',
|
|
58
|
-
'secrets-azureOpenaiApiKeyVault',
|
|
59
58
|
'miso-controller-secrets-apiKeyVault'
|
|
60
59
|
]);
|
|
61
60
|
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map an application's `.env` variables onto Miso catalog keys.
|
|
3
|
+
*
|
|
4
|
+
* The `.env` is keyed by environment variable (`MORI_POLICY_PRIVATE_KEY`) while the system
|
|
5
|
+
* secrets API is keyed by catalog key (`secrets-policyPrivateKeyVault`). The mapping is not
|
|
6
|
+
* invented here: it is read from the application's own `env.template`, because the
|
|
7
|
+
* `NAME=kv://<catalogKey>` line is already the declaration that decides where the value is
|
|
8
|
+
* read from at runtime. A second table in the CLI would be free to drift from it.
|
|
9
|
+
*
|
|
10
|
+
* A variable whose template line is not a `kv://` reference is ordinary configuration, not a
|
|
11
|
+
* secret, and is skipped rather than pushed.
|
|
12
|
+
*
|
|
13
|
+
* Note this is the flat `kv://<catalogKey>` form used by the Controller's system secrets.
|
|
14
|
+
* The Dataplane's credential store uses `kv://<systemKey>/<path>`; see
|
|
15
|
+
* `credential-secrets-env-kv.js` for that one. They are not interchangeable.
|
|
16
|
+
*
|
|
17
|
+
* @fileoverview env.template kv:// declarations → catalog keys
|
|
18
|
+
* @author AI Fabrix Team
|
|
19
|
+
* @version 1.0.0
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
/** `NAME=value`, ignoring blank lines, comments and `export ` prefixes. */
|
|
25
|
+
const ASSIGNMENT = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Strip surrounding quotes and trailing whitespace from an env value.
|
|
29
|
+
* @param {string} raw - Raw right-hand side
|
|
30
|
+
* @returns {string} Cleaned value
|
|
31
|
+
*/
|
|
32
|
+
function cleanValue(raw) {
|
|
33
|
+
const trimmed = String(raw === undefined || raw === null ? '' : raw).trim();
|
|
34
|
+
if (trimmed.length >= 2) {
|
|
35
|
+
const first = trimmed[0];
|
|
36
|
+
const last = trimmed[trimmed.length - 1];
|
|
37
|
+
if ((first === '"' && last === '"') || (first === '\'' && last === '\'')) {
|
|
38
|
+
return trimmed.slice(1, -1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return trimmed;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Parse `NAME=value` lines into a Map, last assignment winning.
|
|
46
|
+
* @param {string} content - File content
|
|
47
|
+
* @returns {Map<string, string>} Variable name → value
|
|
48
|
+
*/
|
|
49
|
+
function parseEnvAssignments(content) {
|
|
50
|
+
const out = new Map();
|
|
51
|
+
for (const line of String(content || '').split(/\r?\n/)) {
|
|
52
|
+
if (!line || /^\s*#/.test(line)) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const match = line.match(ASSIGNMENT);
|
|
56
|
+
if (match) {
|
|
57
|
+
out.set(match[1], cleanValue(match[2]));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Catalog keys declared by an `env.template`, keyed by environment variable name.
|
|
65
|
+
* Only `kv://` lines are secrets; everything else is skipped.
|
|
66
|
+
* @param {string} templateContent - env.template content
|
|
67
|
+
* @returns {Map<string, string>} Variable name → catalog key
|
|
68
|
+
*/
|
|
69
|
+
function catalogKeysFromTemplate(templateContent) {
|
|
70
|
+
const out = new Map();
|
|
71
|
+
for (const [name, value] of parseEnvAssignments(templateContent)) {
|
|
72
|
+
if (!value.startsWith('kv://')) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const catalogKey = value.slice('kv://'.length).trim();
|
|
76
|
+
if (catalogKey && !catalogKey.includes('/')) {
|
|
77
|
+
out.set(name, catalogKey);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Pair the values in a `.env` with the catalog keys its `env.template` declares.
|
|
85
|
+
*
|
|
86
|
+
* Skipped rows are returned rather than dropped, so the command can say why a variable was
|
|
87
|
+
* not pushed instead of silently ignoring it.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} envContent - Operator's .env content
|
|
90
|
+
* @param {string} templateContent - The application's env.template content
|
|
91
|
+
* @param {Object} [options] - `only` restricts to one variable name
|
|
92
|
+
* @returns {{ pushable: Array<Object>, skipped: Array<Object> }} Rows carrying value and catalog key
|
|
93
|
+
*/
|
|
94
|
+
function mapEnvToCatalogKeys(envContent, templateContent, options = {}) {
|
|
95
|
+
const declared = catalogKeysFromTemplate(templateContent);
|
|
96
|
+
const values = parseEnvAssignments(envContent);
|
|
97
|
+
const only = options.only ? String(options.only).trim() : '';
|
|
98
|
+
const pushable = [];
|
|
99
|
+
const skipped = [];
|
|
100
|
+
for (const [name, value] of values) {
|
|
101
|
+
if (only && name !== only) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const catalogKey = declared.get(name);
|
|
105
|
+
if (!catalogKey) {
|
|
106
|
+
skipped.push({ name, reason: 'not a kv:// reference in env.template' });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (!value) {
|
|
110
|
+
skipped.push({ name, catalogKey, reason: 'no value in the env file' });
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
pushable.push({ name, catalogKey, value });
|
|
114
|
+
}
|
|
115
|
+
if (only && pushable.length === 0 && skipped.length === 0) {
|
|
116
|
+
skipped.push({ name: only, reason: 'not present in the env file' });
|
|
117
|
+
}
|
|
118
|
+
return { pushable, skipped };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = {
|
|
122
|
+
parseEnvAssignments,
|
|
123
|
+
catalogKeysFromTemplate,
|
|
124
|
+
mapEnvToCatalogKeys
|
|
125
|
+
};
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
const fs = require('fs').promises;
|
|
11
11
|
const path = require('path');
|
|
12
|
+
const { sanitizeBinaryEvidence } = require('./datasource-binary-evidence');
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Prepare object for JSON serialization (handles circular refs)
|
|
@@ -45,7 +46,7 @@ async function writeTestLog(appKey, data, logType = 'test-integration', integrat
|
|
|
45
46
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
46
47
|
const filename = `${logType}-${timestamp}.json`;
|
|
47
48
|
const filePath = path.join(logsDir, filename);
|
|
48
|
-
const sanitized = sanitizeForLog(data);
|
|
49
|
+
const sanitized = sanitizeForLog(sanitizeBinaryEvidence(data));
|
|
49
50
|
await fs.writeFile(filePath, JSON.stringify(sanitized, null, 2), 'utf8');
|
|
50
51
|
return filePath;
|
|
51
52
|
}
|
|
@@ -16,6 +16,7 @@ const path = require('path');
|
|
|
16
16
|
const { formatValidationErrors } = require('../utils/error-formatter');
|
|
17
17
|
const { validateFieldReferences } = require('../datasource/field-reference-validator');
|
|
18
18
|
const { validateAbac } = require('../datasource/abac-validator');
|
|
19
|
+
const { validateBinaryDocumentsContract } = require('../datasource/binary-documents-validator');
|
|
19
20
|
const { validateManifestRuntimeConfiguration } = require('./runtime-configuration-rules');
|
|
20
21
|
const { validateAppContractSurface } = require('./openapi-contract-surface-validator');
|
|
21
22
|
|
|
@@ -147,9 +148,13 @@ function validateDatasources(manifest, ajv, externalDatasourceSchema, errors, wa
|
|
|
147
148
|
} else {
|
|
148
149
|
const fieldRefErrors = validateFieldReferences(datasource);
|
|
149
150
|
const abacErrors = validateAbac(datasource);
|
|
151
|
+
const binaryDocuments = validateBinaryDocumentsContract(datasource);
|
|
150
152
|
const prefix = `Datasource ${index + 1} (${datasource.key || 'unknown'}): `;
|
|
151
153
|
fieldRefErrors.forEach(e => errors.push(prefix + e));
|
|
152
154
|
abacErrors.forEach(e => errors.push(prefix + e));
|
|
155
|
+
// Codes are the dataplane's, so the offline answer matches the upload answer.
|
|
156
|
+
binaryDocuments.errors.forEach(e => errors.push(`${prefix}${e.code}: ${e.message}`));
|
|
157
|
+
binaryDocuments.warnings.forEach(w => warnings.push(`${prefix}${w.code}: ${w.message}`));
|
|
153
158
|
}
|
|
154
159
|
});
|
|
155
160
|
}
|
|
@@ -426,5 +426,7 @@ module.exports = {
|
|
|
426
426
|
validateDatasourceFileContractSurface,
|
|
427
427
|
validateVendorOperationContract,
|
|
428
428
|
validateServiceContractDatasourceContracts,
|
|
429
|
-
findVendorOperation
|
|
429
|
+
findVendorOperation,
|
|
430
|
+
resolveVendorOpenApiSpec,
|
|
431
|
+
inferIntegrationContextFromPath
|
|
430
432
|
};
|
|
@@ -111,11 +111,15 @@ function validateDatasourceFileExtras(parsed, filePath) {
|
|
|
111
111
|
const { collectExternalDatasourceWarnings } = require('./datasource-warnings');
|
|
112
112
|
const { validateDatasourceFileContractSurface } = require('./openapi-contract-surface-validator');
|
|
113
113
|
const errors = [...validateFieldReferences(parsed), ...validateAbac(parsed)];
|
|
114
|
+
const { validateBinaryDocumentsContract } = require('../datasource/binary-documents-validator');
|
|
115
|
+
const binary = validateBinaryDocumentsContract(parsed);
|
|
116
|
+
errors.push(...binary.errors.map(issue => `${issue.code}: ${issue.message}`));
|
|
114
117
|
const contractSurface = validateDatasourceFileContractSurface(parsed, filePath);
|
|
115
118
|
errors.push(...contractSurface.errors);
|
|
116
119
|
const warnings = [
|
|
117
120
|
...collectExternalDatasourceWarnings(parsed),
|
|
118
|
-
...contractSurface.warnings
|
|
121
|
+
...contractSurface.warnings,
|
|
122
|
+
...binary.warnings.map(issue => `${issue.code}: ${issue.message}`)
|
|
119
123
|
];
|
|
120
124
|
return { errors, warnings };
|
|
121
125
|
}
|