@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
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Save wizard artifacts and update their README and RBAC files.
|
|
3
|
+
* @author AI Fabrix Team
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
const ora = require('ora');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const fs = require('fs').promises;
|
|
10
|
+
const logger = require('../utils/logger');
|
|
11
|
+
const { formatSuccessParagraph } = require('../utils/cli-layout-chalk');
|
|
12
|
+
const { getDeploymentDocs, postDeploymentDocs } = require('../api/wizard.api');
|
|
13
|
+
const { generateWizardFiles } = require('../generator/wizard');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Fetches deployment docs and writes README.md when application config and deploy JSON are available.
|
|
17
|
+
* @async
|
|
18
|
+
* @param {string} appPath - Application path
|
|
19
|
+
* @param {string} appName - Application name
|
|
20
|
+
* @param {string} dataplaneUrl - Dataplane URL
|
|
21
|
+
* @param {Object} authConfig - Authentication configuration
|
|
22
|
+
* @param {string} systemKey - System key
|
|
23
|
+
*/
|
|
24
|
+
async function tryUpdateReadmeFromDeploymentDocs(appPath, appName, dataplaneUrl, authConfig, systemKey) {
|
|
25
|
+
const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
|
|
26
|
+
const deployPath = path.join(appPath, `${appName}-deploy.json`);
|
|
27
|
+
let variablesYaml = null;
|
|
28
|
+
let deployJson = null;
|
|
29
|
+
try {
|
|
30
|
+
const configPath = resolveApplicationConfigPath(appPath);
|
|
31
|
+
variablesYaml = await fs.readFile(configPath, 'utf8');
|
|
32
|
+
} catch {
|
|
33
|
+
// optional
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const deployContent = await fs.readFile(deployPath, 'utf8');
|
|
37
|
+
deployJson = JSON.parse(deployContent);
|
|
38
|
+
} catch {
|
|
39
|
+
// optional
|
|
40
|
+
}
|
|
41
|
+
const hasBody = variablesYaml !== null || deployJson !== null;
|
|
42
|
+
const body = hasBody ? { variablesYaml: variablesYaml || null, deployJson: deployJson || null } : null;
|
|
43
|
+
const docsResponse = body
|
|
44
|
+
? await postDeploymentDocs(dataplaneUrl, authConfig, systemKey, body)
|
|
45
|
+
: await getDeploymentDocs(dataplaneUrl, authConfig, systemKey);
|
|
46
|
+
const content = docsResponse?.data?.content ?? docsResponse?.content;
|
|
47
|
+
// Only overwrite README when the API returns substantial content; otherwise keep the template README
|
|
48
|
+
const MIN_README_LENGTH = 400;
|
|
49
|
+
if (content && typeof content === 'string' && content.trim().length >= MIN_README_LENGTH) {
|
|
50
|
+
const readmePath = path.join(appPath, 'README.md');
|
|
51
|
+
await fs.writeFile(readmePath, content, 'utf8');
|
|
52
|
+
logger.log(chalk.gray(' Updated README.md from deployment-docs API (application config + deploy JSON).'));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Writes rbac.yaml / rbac.json from datasource resourceType + capabilities (same as `af repair --rbac`).
|
|
58
|
+
* @param {Object} generatedFiles - Result of generateWizardFiles (appPath, systemFilePath, datasourceFilePaths)
|
|
59
|
+
* @param {string} format - Project format: yaml | json
|
|
60
|
+
*/
|
|
61
|
+
function logWizardFileSaveFooter(appName, generatedFiles) {
|
|
62
|
+
logger.log(formatSuccessParagraph('Wizard completed successfully!'));
|
|
63
|
+
logger.log(chalk.green(`\nFiles created in: ${generatedFiles.appPath}`));
|
|
64
|
+
logger.log(chalk.blue('\nNext steps:'));
|
|
65
|
+
logger.log(chalk.gray(` 1. Review the generated files in integration/${appName}/`));
|
|
66
|
+
logger.log(chalk.gray(' 2. Update env.template with your authentication details'));
|
|
67
|
+
logger.log(chalk.gray(` 3. Deploy using: node deploy.js or aifabrix deploy ${appName}`));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function mergeRbacAfterWizardFilesWritten(generatedFiles, format) {
|
|
71
|
+
const { mergeRbacFromDatasources, extractRbacFromSystem } = require('./repair-rbac');
|
|
72
|
+
const { loadConfigFile } = require('../utils/config-format');
|
|
73
|
+
const systemParsedForRbac = loadConfigFile(generatedFiles.systemFilePath);
|
|
74
|
+
const datasourceFileNames = (generatedFiles.datasourceFilePaths || []).map((p) => path.basename(p));
|
|
75
|
+
const rbacChanges = [];
|
|
76
|
+
const rbacUpdated = mergeRbacFromDatasources(
|
|
77
|
+
generatedFiles.appPath,
|
|
78
|
+
systemParsedForRbac,
|
|
79
|
+
datasourceFileNames,
|
|
80
|
+
extractRbacFromSystem,
|
|
81
|
+
{ format: format === 'json' ? 'json' : 'yaml', dryRun: false, changes: rbacChanges }
|
|
82
|
+
);
|
|
83
|
+
if (rbacUpdated && rbacChanges.length) {
|
|
84
|
+
rbacChanges.forEach((c) => logger.log(chalk.gray(` RBAC: ${c}`)));
|
|
85
|
+
logger.log(chalk.green(' RBAC file updated from datasource capabilities (enableRBAC).'));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Handle file saving step
|
|
91
|
+
* @async
|
|
92
|
+
* @function handleFileSaving
|
|
93
|
+
* @param {string} appName - Application name
|
|
94
|
+
* @param {Object} systemConfig - System configuration
|
|
95
|
+
* @param {Object[]} datasourceConfigs - Datasource configurations
|
|
96
|
+
* @param {string} systemKey - System key
|
|
97
|
+
* @param {{ dataplaneUrl: string, authConfig: Object, enableRBAC?: boolean }} ctx - Dataplane auth + optional RBAC generation
|
|
98
|
+
* @returns {Promise<Object>} Generated files information
|
|
99
|
+
*/
|
|
100
|
+
async function handleFileSaving(appName, systemConfig, datasourceConfigs, systemKey, ctx) {
|
|
101
|
+
const { dataplaneUrl, authConfig, enableRBAC = false } = ctx || {};
|
|
102
|
+
logger.log(chalk.blue('\n\uD83D\uDCCB Step 7: Save Files'));
|
|
103
|
+
const spinner = ora('Saving files...').start();
|
|
104
|
+
try {
|
|
105
|
+
const config = require('../core/config');
|
|
106
|
+
const format = ctx?.format || (await config.getFormat()) || 'yaml';
|
|
107
|
+
const generatedFiles = await generateWizardFiles(appName, systemConfig, datasourceConfigs, systemKey, { aiGeneratedReadme: null, format });
|
|
108
|
+
if (enableRBAC && generatedFiles.appPath && generatedFiles.systemFilePath) {
|
|
109
|
+
try {
|
|
110
|
+
mergeRbacAfterWizardFilesWritten(generatedFiles, format);
|
|
111
|
+
} catch (e) {
|
|
112
|
+
logger.log(chalk.yellow(` Could not generate RBAC file: ${e.message}`));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (systemKey && dataplaneUrl && authConfig && generatedFiles.appPath) {
|
|
116
|
+
try {
|
|
117
|
+
await tryUpdateReadmeFromDeploymentDocs(generatedFiles.appPath, appName, dataplaneUrl, authConfig, systemKey);
|
|
118
|
+
} catch (e) {
|
|
119
|
+
logger.log(chalk.gray(` Could not fetch AI-generated README: ${e.message}`));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
spinner.stop();
|
|
123
|
+
logWizardFileSaveFooter(appName, generatedFiles);
|
|
124
|
+
return generatedFiles;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
spinner.stop();
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Validate app name and check if directory exists
|
|
133
|
+
* @async
|
|
134
|
+
* @function validateAndCheckAppDirectory
|
|
135
|
+
* @param {string} appName - Application name
|
|
136
|
+
* @param {boolean} [interactive=true] - Whether to prompt for confirmation
|
|
137
|
+
* @returns {Promise<boolean>} True if should continue, false if cancelled
|
|
138
|
+
*/
|
|
139
|
+
async function validateAndCheckAppDirectory(appName, interactive = true) {
|
|
140
|
+
if (!/^[a-z0-9-_]+$/.test(appName)) {
|
|
141
|
+
throw new Error('Application name must contain only lowercase letters, numbers, hyphens, and underscores');
|
|
142
|
+
}
|
|
143
|
+
const appPath = path.join(process.cwd(), 'integration', appName);
|
|
144
|
+
try {
|
|
145
|
+
await fs.access(appPath);
|
|
146
|
+
if (interactive) {
|
|
147
|
+
const { overwrite } = await require('inquirer').prompt([{
|
|
148
|
+
type: 'confirm', name: 'overwrite',
|
|
149
|
+
message: `Directory ${appPath} already exists. Overwrite?`, default: false
|
|
150
|
+
}]);
|
|
151
|
+
if (!overwrite) {
|
|
152
|
+
logger.log(chalk.yellow('Wizard cancelled.')); return false;
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
logger.log(chalk.yellow(`Warning: Directory ${appPath} exists. Overwriting...`));
|
|
156
|
+
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (error.code !== 'ENOENT') throw error;
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = { handleFileSaving, validateAndCheckAppDirectory };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `{NAME}` expansion in env content (plan 200.0).
|
|
3
|
+
*
|
|
4
|
+
* Values resolved from the manifest `platforms` / `environments` sections are substituted into
|
|
5
|
+
* `env.template` values. Runs after `kv://` and before `url://`, so a resolved value may itself
|
|
6
|
+
* carry a declarative URL.
|
|
7
|
+
*
|
|
8
|
+
* Only the value side of `KEY=value` is touched, and only `{UPPER_SNAKE}` tokens that are not
|
|
9
|
+
* preceded by `$`. That leaves Builder's existing `${VAR}` expansion and `{lowercase}` text in
|
|
10
|
+
* comments alone — both appear in shipped templates.
|
|
11
|
+
*
|
|
12
|
+
* @fileoverview Platform/environment name expansion for env.template
|
|
13
|
+
* @author AI Fabrix Team
|
|
14
|
+
* @version 1.0.0
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const { resolveConfiguration } = require('../utils/platform-resolution');
|
|
20
|
+
|
|
21
|
+
/** `{NAME}` not preceded by `$`; uppercase only, matching the configuration name pattern. */
|
|
22
|
+
const NAME_TOKEN = /(?<!\$)\{([A-Z_][A-Z0-9_]*)\}/g;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {Object} [manifest] - Application manifest
|
|
26
|
+
* @returns {boolean} True when the manifest declares either section
|
|
27
|
+
*/
|
|
28
|
+
function declaresPlatformSections(manifest) {
|
|
29
|
+
if (!manifest || typeof manifest !== 'object') return false;
|
|
30
|
+
return Array.isArray(manifest.platforms) || Array.isArray(manifest.environments);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function expandValue(value, values, key, strict, context = {}) {
|
|
34
|
+
return value.replace(NAME_TOKEN, (match, name) => {
|
|
35
|
+
if (Object.prototype.hasOwnProperty.call(values, name)) return values[name];
|
|
36
|
+
if (!strict) return match;
|
|
37
|
+
// Without a resource group no platform matched, so only environments and built-ins resolved.
|
|
38
|
+
// Saying so turns "why is this missing" into "declare it here".
|
|
39
|
+
const where = context.resourceGroup
|
|
40
|
+
? `resource group "${context.resourceGroup}" and environment "${context.environment || ''}"`
|
|
41
|
+
: `environment "${context.environment || ''}" (no resource group, so platform values are unavailable)`;
|
|
42
|
+
const hint = context.resourceGroup
|
|
43
|
+
? ''
|
|
44
|
+
: ' The environment key comes from --env, or from the current CLI environment' +
|
|
45
|
+
' (aifabrix auth set-environment <key>).';
|
|
46
|
+
throw new Error(
|
|
47
|
+
`${key} references {${name}}, which no platform or environment configuration resolves for ` +
|
|
48
|
+
`${where}. Declare it under environments, or run against a resource group that supplies it.${hint}`
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Expands `{NAME}` on the value side of every assignment line.
|
|
55
|
+
* @param {string} content - Env content
|
|
56
|
+
* @param {Object} values - Resolved names
|
|
57
|
+
* @param {Object} [opts] - `strict` (default true) errors on an unresolved name; false leaves the
|
|
58
|
+
* token in place for a later resolver (the deploy manifest, resolved by Miso per installation)
|
|
59
|
+
* @returns {string}
|
|
60
|
+
* @throws {Error} When strict and a referenced name is not resolved
|
|
61
|
+
*/
|
|
62
|
+
function expandNamesInEnvContent(content, values, opts = {}) {
|
|
63
|
+
const strict = opts.strict !== false;
|
|
64
|
+
const context = { environment: opts.environment, resourceGroup: opts.resourceGroup };
|
|
65
|
+
const lines = String(content || '').split('\n');
|
|
66
|
+
const out = lines.map(line => {
|
|
67
|
+
const trimmed = line.trimStart();
|
|
68
|
+
if (!trimmed || trimmed.startsWith('#')) return line;
|
|
69
|
+
const separator = line.indexOf('=');
|
|
70
|
+
if (separator < 0) return line;
|
|
71
|
+
const key = line.slice(0, separator);
|
|
72
|
+
const value = line.slice(separator + 1);
|
|
73
|
+
return `${key}=${expandValue(value, values, key.trim(), strict, context)}`;
|
|
74
|
+
});
|
|
75
|
+
return out.join('\n');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Resolves the manifest sections and expands `{NAME}` when either section is declared.
|
|
80
|
+
* A manifest without them is returned untouched, so existing applications are unaffected.
|
|
81
|
+
* @param {string} content - Env content after kv:// resolution
|
|
82
|
+
* @param {Object} params - `manifest`, `environment` (deployment env key), `resourceGroup`, `strict`
|
|
83
|
+
* @returns {string}
|
|
84
|
+
*/
|
|
85
|
+
function expandPlatformNamesIfPresent(content, params = {}) {
|
|
86
|
+
const { manifest, environment, resourceGroup, strict } = params;
|
|
87
|
+
if (!declaresPlatformSections(manifest)) return content;
|
|
88
|
+
const { values } = resolveConfiguration(manifest, { environment, resourceGroup });
|
|
89
|
+
return expandNamesInEnvContent(content, values, { strict, environment, resourceGroup });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = {
|
|
93
|
+
NAME_TOKEN,
|
|
94
|
+
declaresPlatformSections,
|
|
95
|
+
expandNamesInEnvContent,
|
|
96
|
+
expandPlatformNamesIfPresent
|
|
97
|
+
};
|
|
@@ -44,6 +44,7 @@ const { computeEffectiveEnvironmentScopedResources, redisDbIndexForScopedRunEnv
|
|
|
44
44
|
const { applyRedisDbIndexToEnvContent } = require('../utils/redis-env-scope');
|
|
45
45
|
const { expandDeclarativeUrlsIfPresent } = require('./secrets-env-declarative-expand');
|
|
46
46
|
const {
|
|
47
|
+
mergeAdditionalPublishedPorts,
|
|
47
48
|
mergeDockerManifestPublishedPort,
|
|
48
49
|
rewriteDockerManifestPublicPortEnvLine
|
|
49
50
|
} = require('../utils/docker-manifest-public-port');
|
|
@@ -154,6 +155,12 @@ async function getDockerRedisDbEndpoints() {
|
|
|
154
155
|
return { redisHost, redisPort, dbHost, dbPort };
|
|
155
156
|
}
|
|
156
157
|
|
|
158
|
+
/** Host ports clients connect to: the manifest's own, plus any additionalPorts. */
|
|
159
|
+
async function mergePublishedPorts(envVars, appDoc) {
|
|
160
|
+
await mergeDockerManifestPublishedPort(envVars, appDoc);
|
|
161
|
+
await mergeAdditionalPublishedPorts(envVars, appDoc);
|
|
162
|
+
}
|
|
163
|
+
|
|
157
164
|
/** Docker env transformations: ports, infra endpoints, PORT. */
|
|
158
165
|
async function applyDockerTransformations(resolved, variablesPath) {
|
|
159
166
|
resolved = await resolveServicePortsInEnvContent(resolved, 'docker');
|
|
@@ -164,7 +171,7 @@ async function applyDockerTransformations(resolved, variablesPath) {
|
|
|
164
171
|
const containerPort = getContainerPortFromPath(variablesPath) ?? getContainerPortFromDockerEnv(dockerEnv) ?? 3000;
|
|
165
172
|
const envVars = await buildEnvVarMap('docker', null, null, { appPort: containerPort });
|
|
166
173
|
const appDoc = loadVariablesFromPath(variablesPath);
|
|
167
|
-
await
|
|
174
|
+
await mergePublishedPorts(envVars, appDoc);
|
|
168
175
|
envVars.REDIS_HOST = redisHost;
|
|
169
176
|
envVars.REDIS_PORT = String(redisPort);
|
|
170
177
|
envVars.DB_HOST = dbHost;
|
|
@@ -187,6 +194,32 @@ async function applyEnvironmentTransformations(resolved, environment, variablesP
|
|
|
187
194
|
return resolved;
|
|
188
195
|
}
|
|
189
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Expands manifest `{NAME}` values when the application declares platforms/environments.
|
|
199
|
+
* Local Builder runs pass no resource group, so they resolve as the local platform.
|
|
200
|
+
* @param {string} content - Env content after kv:// resolution
|
|
201
|
+
* @param {string|null} variablesPath - Application manifest path (null in env-only mode)
|
|
202
|
+
* @param {string} runEnvKey - Deployment environment key
|
|
203
|
+
* @param {Object} [options] - `resourceGroup` when a deployment target is known
|
|
204
|
+
* @returns {string}
|
|
205
|
+
*/
|
|
206
|
+
function expandPlatformNamesForApp(content, variablesPath, runEnvKey, options = {}) {
|
|
207
|
+
if (!variablesPath) return content;
|
|
208
|
+
const { expandPlatformNamesIfPresent } = require('./env-platform-expand');
|
|
209
|
+
const { loadConfigFile } = require('../utils/config-format');
|
|
210
|
+
let manifest = null;
|
|
211
|
+
try {
|
|
212
|
+
manifest = loadConfigFile(variablesPath);
|
|
213
|
+
} catch {
|
|
214
|
+
return content;
|
|
215
|
+
}
|
|
216
|
+
return expandPlatformNamesIfPresent(content, {
|
|
217
|
+
manifest,
|
|
218
|
+
environment: runEnvKey,
|
|
219
|
+
resourceGroup: options.resourceGroup
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
190
223
|
/**
|
|
191
224
|
* Generate .env content from template and secrets (no disk write).
|
|
192
225
|
* When options.envOnly is true, variablesPath is null (no application config).
|
|
@@ -210,15 +243,16 @@ async function generateEnvContent(appName, secretsPath, environment = 'local', f
|
|
|
210
243
|
}
|
|
211
244
|
const secrets = await loadSecrets(secretsPath, appName);
|
|
212
245
|
const { runEnvKey, effective } = await buildScopedKvContext(appPath, options);
|
|
213
|
-
const scopedKv = { envKey: runEnvKey, effective };
|
|
214
246
|
let resolved = await resolveKvReferences(template, secrets, environment, secretsPaths, appName, {
|
|
215
247
|
envKey: runEnvKey,
|
|
216
248
|
effective,
|
|
217
249
|
envTemplatePath: templatePath
|
|
218
250
|
});
|
|
219
251
|
if (!secretsPath) {
|
|
220
|
-
await materializeResolvedKvSecretsToUserLocal(template, secrets,
|
|
252
|
+
await materializeResolvedKvSecretsToUserLocal(template, secrets, { envKey: runEnvKey, effective }, options);
|
|
221
253
|
}
|
|
254
|
+
// Platform/environment {NAME} before url://, so a resolved value may carry a declarative URL.
|
|
255
|
+
resolved = expandPlatformNamesForApp(resolved, variablesPath, runEnvKey, options);
|
|
222
256
|
resolved = await expandDeclarativeUrlsIfPresent(
|
|
223
257
|
resolved,
|
|
224
258
|
appName,
|
|
@@ -436,7 +470,11 @@ async function generateEnvFile(appName, secretsPath, environment = 'local', forc
|
|
|
436
470
|
}
|
|
437
471
|
|
|
438
472
|
// Always resolve so missing-secret / kv:// errors still surface in noWrite mode.
|
|
439
|
-
const resolved = await generateEnvContent(appName, secretsPath, environment, force, {
|
|
473
|
+
const resolved = await generateEnvContent(appName, secretsPath, environment, force, {
|
|
474
|
+
appPath,
|
|
475
|
+
envOnly,
|
|
476
|
+
runEnvKey: opts.runEnvKey
|
|
477
|
+
});
|
|
440
478
|
|
|
441
479
|
if (noWrite) {
|
|
442
480
|
return null;
|
|
@@ -157,8 +157,11 @@ async function resolveAndWriteEnvFile(appName, options = {}) {
|
|
|
157
157
|
const environment = options.environment || 'docker';
|
|
158
158
|
const secretsPath = options.secretsPath || null;
|
|
159
159
|
const force = options.force === true;
|
|
160
|
+
const genOpts = options.runEnvKey ? { runEnvKey: String(options.runEnvKey) } : undefined;
|
|
160
161
|
|
|
161
|
-
let resolved =
|
|
162
|
+
let resolved = genOpts
|
|
163
|
+
? await secrets.generateEnvContent(appName, secretsPath, environment, force, genOpts)
|
|
164
|
+
: await secrets.generateEnvContent(appName, secretsPath, environment, force);
|
|
162
165
|
resolved = await injectRegistryTokens(resolved, secretsPath, appName);
|
|
163
166
|
resolved = await injectBashPrefixedExportLines(resolved, secretsPath, appName);
|
|
164
167
|
|
|
@@ -185,7 +188,8 @@ async function resolveAndWriteEnvFile(appName, options = {}) {
|
|
|
185
188
|
* @async
|
|
186
189
|
* @function resolveAndGetEnvMap
|
|
187
190
|
* @param {string} appName - Application name
|
|
188
|
-
* @param {Object} [options] - Options (same as resolveAndWriteEnvFile)
|
|
191
|
+
* @param {Object} [options] - Options (same as resolveAndWriteEnvFile); `runEnvKey` selects the
|
|
192
|
+
* manifest environment for {NAME} resolution
|
|
189
193
|
* @returns {Promise<Object.<string, string>>} Map of variable name to value
|
|
190
194
|
*/
|
|
191
195
|
async function resolveAndGetEnvMap(appName, options = {}) {
|
|
@@ -193,7 +197,10 @@ async function resolveAndGetEnvMap(appName, options = {}) {
|
|
|
193
197
|
const environment = options.environment || 'docker';
|
|
194
198
|
const secretsPath = options.secretsPath || null;
|
|
195
199
|
const force = options.force === true;
|
|
196
|
-
|
|
200
|
+
const genOpts = options.runEnvKey ? { runEnvKey: String(options.runEnvKey) } : undefined;
|
|
201
|
+
let content = genOpts
|
|
202
|
+
? await secrets.generateEnvContent(appName, secretsPath, environment, force, genOpts)
|
|
203
|
+
: await secrets.generateEnvContent(appName, secretsPath, environment, force);
|
|
197
204
|
content = await injectRegistryTokens(content, secretsPath, appName);
|
|
198
205
|
content = await injectBashPrefixedExportLines(content, secretsPath, appName);
|
|
199
206
|
const map = parseEnvContentToMap(content);
|
package/lib/core/secrets-load.js
CHANGED
|
@@ -167,9 +167,11 @@ async function mergeFromConfiguredSecretsPath(configSecretsPath, userSecrets, he
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
async function loadMergedConfigAndUserSecrets() {
|
|
170
|
-
const userSecrets = loadPrimaryUserSecrets();
|
|
171
|
-
const helpers = createMergeHelpers(userSecrets);
|
|
172
170
|
const userPath = pathsUtil.getPrimaryUserSecretsLocalPath();
|
|
171
|
+
const userSecrets = await decryptSecretsObject(loadPrimaryUserSecrets(), {
|
|
172
|
+
defaultSourceLabel: userPath
|
|
173
|
+
});
|
|
174
|
+
const helpers = createMergeHelpers(userSecrets);
|
|
173
175
|
/** @type {Record<string, string>} */
|
|
174
176
|
const keySources = {};
|
|
175
177
|
for (const k of Object.keys(userSecrets || {})) {
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline semantic checks for the binary/documents pairing (dataplane schema 2.9.13).
|
|
3
|
+
*
|
|
4
|
+
* The schema alone cannot express these: `output.mode: documents` and
|
|
5
|
+
* `fetch.responseType: binary` are individually valid anywhere, but only mean something
|
|
6
|
+
* when paired with a documentStorage entity and a binary-capable fetch source. The
|
|
7
|
+
* dataplane rejects the bad combinations at upload; these checks give the same answer
|
|
8
|
+
* offline, under the same error codes, so an author is not told "valid" by the CLI and
|
|
9
|
+
* "invalid" by the server.
|
|
10
|
+
*
|
|
11
|
+
* Codes mirror the dataplane validator and must not be renamed here.
|
|
12
|
+
*
|
|
13
|
+
* @fileoverview Semantic validation for binary responses and document output
|
|
14
|
+
* @author AI Fabrix Team
|
|
15
|
+
* @version 1.0.0
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
/** Sources whose response body the runtime can read as bytes. */
|
|
21
|
+
const BINARY_FETCH_SOURCES = new Set(['openapi', 'http']);
|
|
22
|
+
|
|
23
|
+
/** documentStorage.binaryOperationRef default, per type/document-storage.json. */
|
|
24
|
+
const DEFAULT_BINARY_OPERATION = 'get';
|
|
25
|
+
|
|
26
|
+
const CODES = Object.freeze({
|
|
27
|
+
entityType: 'cip_documents_output_entity_type',
|
|
28
|
+
storageDisabled: 'cip_documents_output_storage_disabled',
|
|
29
|
+
binarySource: 'cip_fetch_binary_source_invalid',
|
|
30
|
+
withoutBinaryFetch: 'cip_documents_output_without_binary_fetch'
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {Object} datasource - Datasource definition
|
|
35
|
+
* @returns {Array<[string, Object]>} CIP operation entries, empty when not a CIP datasource
|
|
36
|
+
*/
|
|
37
|
+
function cipOperations(datasource) {
|
|
38
|
+
const cip = datasource?.execution?.engine === 'cip' ? datasource.execution.cip : null;
|
|
39
|
+
const operations = cip && cip.operations;
|
|
40
|
+
if (!operations || typeof operations !== 'object') {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
return Object.entries(operations).filter(([, operation]) => operation && typeof operation === 'object');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {Object} operation - CIP operation
|
|
48
|
+
* @returns {Array<Object>} Its steps
|
|
49
|
+
*/
|
|
50
|
+
function stepsOf(operation) {
|
|
51
|
+
return Array.isArray(operation.steps) ? operation.steps : [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A step declares a binary body. The schema also promotes a binary Content-Type when
|
|
56
|
+
* responseType is omitted, but that is a runtime decision we cannot make offline, so only
|
|
57
|
+
* the explicit declaration counts here.
|
|
58
|
+
* @param {Object} step - CIP step
|
|
59
|
+
* @returns {boolean} True when the step fetches a binary body
|
|
60
|
+
*/
|
|
61
|
+
function isBinaryFetch(step) {
|
|
62
|
+
return Boolean(step && step.fetch && String(step.fetch.responseType || '').trim().toLowerCase() === 'binary');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {Object} step - CIP step
|
|
67
|
+
* @returns {boolean} True when the step finalizes as documents
|
|
68
|
+
*/
|
|
69
|
+
function isDocumentsOutput(step) {
|
|
70
|
+
return Boolean(step && step.output && step.output.mode === 'documents');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* `binary` is a body contract, so it only applies where the runtime holds the response
|
|
75
|
+
* bytes. A datasource, viewpoint or record source produces rows, never a file body.
|
|
76
|
+
* @param {string} operationName - Operation key
|
|
77
|
+
* @param {Array<Object>} steps - Operation steps
|
|
78
|
+
* @param {Array<Object>} errors - Collected errors
|
|
79
|
+
* @returns {void}
|
|
80
|
+
*/
|
|
81
|
+
function checkBinaryFetchSources(operationName, steps, errors) {
|
|
82
|
+
steps.forEach((step, index) => {
|
|
83
|
+
if (!isBinaryFetch(step)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const source = String(step.fetch.source || 'openapi').trim().toLowerCase();
|
|
87
|
+
if (BINARY_FETCH_SOURCES.has(source)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
errors.push({
|
|
91
|
+
code: CODES.binarySource,
|
|
92
|
+
message:
|
|
93
|
+
`operations.${operationName}.steps[${index}].fetch.responseType is "binary" but ` +
|
|
94
|
+
`fetch.source is "${source || 'unset'}"; binary bodies come from openapi or http sources only`
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {Object} datasource - Datasource definition
|
|
101
|
+
* @param {string} operationName - Operation key that emits documents
|
|
102
|
+
* @param {number} stepIndex - Index of the output step
|
|
103
|
+
* @param {Array<Object>} errors - Collected errors
|
|
104
|
+
* @returns {void}
|
|
105
|
+
*/
|
|
106
|
+
function checkDocumentsEntity(datasource, operationName, stepIndex, errors) {
|
|
107
|
+
const where = `operations.${operationName}.steps[${stepIndex}].output.mode is "documents"`;
|
|
108
|
+
if (!['documentstorage', 'document-storage'].includes(String(datasource.entityType || '').trim().toLowerCase().replace(/_/g, '-'))) {
|
|
109
|
+
errors.push({
|
|
110
|
+
code: CODES.entityType,
|
|
111
|
+
message: `${where} but entityType is "${datasource.entityType || 'unset'}"; documents output requires entityType documentStorage`
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const storage = datasource.documentStorage;
|
|
116
|
+
if (!storage || storage.enabled === false) {
|
|
117
|
+
errors.push({
|
|
118
|
+
code: CODES.storageDisabled,
|
|
119
|
+
message: `${where} but documentStorage is ${storage ? 'not enabled' : 'missing'}; set documentStorage.enabled true`
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The operation named by binaryOperationRef must actually retrieve bytes, otherwise the
|
|
126
|
+
* document pipeline has nothing to store. A missing reference and a reference to a
|
|
127
|
+
* JSON-only operation are the same defect from the author's point of view.
|
|
128
|
+
* @param {Object} datasource - Datasource definition
|
|
129
|
+
* @param {string} operationName - Operation key that emits documents
|
|
130
|
+
* @param {Array<Object>} warnings - Collected warnings
|
|
131
|
+
* @returns {void}
|
|
132
|
+
*/
|
|
133
|
+
function checkBinaryOperationRef(datasource, operationName, warnings) {
|
|
134
|
+
const storage = datasource.documentStorage || {};
|
|
135
|
+
const ref = String(storage.binaryOperationRef || DEFAULT_BINARY_OPERATION);
|
|
136
|
+
const operations = new Map(cipOperations(datasource));
|
|
137
|
+
const referenced = operations.get(ref);
|
|
138
|
+
if (ref === operationName && referenced && stepsOf(referenced).some(isBinaryFetch)) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const reason = ref !== operationName && referenced
|
|
142
|
+
? `binaryOperationRef "${ref}" does not name this operation "${operationName}"`
|
|
143
|
+
: referenced
|
|
144
|
+
? `operation "${ref}" has no step with fetch.responseType "binary"`
|
|
145
|
+
: `operation "${ref}" does not exist in execution.cip.operations`;
|
|
146
|
+
warnings.push({
|
|
147
|
+
code: CODES.withoutBinaryFetch,
|
|
148
|
+
message:
|
|
149
|
+
`operations.${operationName} emits documents but ${reason}; ` +
|
|
150
|
+
'set documentStorage.binaryOperationRef to the operation that fetches the file'
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Semantic checks for the binary/documents pairing.
|
|
156
|
+
* @param {Object} datasource - Datasource definition
|
|
157
|
+
* @returns {{ errors: Array<Object>, warnings: Array<Object> }} Findings with dataplane codes
|
|
158
|
+
*/
|
|
159
|
+
function validateBinaryDocumentsContract(datasource) {
|
|
160
|
+
const errors = [];
|
|
161
|
+
const warnings = [];
|
|
162
|
+
if (!datasource || typeof datasource !== 'object') {
|
|
163
|
+
return { errors, warnings };
|
|
164
|
+
}
|
|
165
|
+
for (const [operationName, operation] of cipOperations(datasource)) {
|
|
166
|
+
if (operation.enabled === false) continue;
|
|
167
|
+
const steps = stepsOf(operation);
|
|
168
|
+
checkBinaryFetchSources(operationName, steps, errors);
|
|
169
|
+
steps.forEach((step, index) => {
|
|
170
|
+
if (!isDocumentsOutput(step)) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
checkDocumentsEntity(datasource, operationName, index, errors);
|
|
174
|
+
const preceding = steps.slice(0, index + 1).some(candidate =>
|
|
175
|
+
isBinaryFetch(candidate) && BINARY_FETCH_SOURCES.has(String(candidate.fetch.source || 'openapi').trim().toLowerCase()));
|
|
176
|
+
if (!preceding) {
|
|
177
|
+
warnings.push({ code: CODES.withoutBinaryFetch,
|
|
178
|
+
message: `operations.${operationName} has no step with fetch.responseType "binary" preceding documents output` });
|
|
179
|
+
} else {
|
|
180
|
+
checkBinaryOperationRef(datasource, operationName, warnings);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return { errors, warnings };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = {
|
|
188
|
+
CODES,
|
|
189
|
+
validateBinaryDocumentsContract
|
|
190
|
+
};
|