@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.
Files changed (145) hide show
  1. package/README.md +14 -11
  2. package/docs/README.md +80 -0
  3. package/docs/builder-help/evidence-patterns.json +155 -0
  4. package/docs/builder-help/golden-examples/crm-company.json +30 -0
  5. package/docs/builder-help/golden-examples/crm-deal.json +29 -0
  6. package/docs/builder-help/golden-examples/document-storage-keyed-get.json +85 -0
  7. package/docs/builder-help/golden-examples/document-storage.json +30 -0
  8. package/docs/builder-help/golden-examples/meeting-transcript.json +29 -0
  9. package/docs/builder-help/golden-examples/repository-template.json +29 -0
  10. package/docs/builder-help/golden-examples/service-ticket.json +29 -0
  11. package/docs/builder-help/platform-roles.json +98 -0
  12. package/docs/builder-help/resource-type-catalog.json +402 -0
  13. package/lib/agent-kit/git-identity.js +180 -0
  14. package/lib/agent-kit/setup.js +3 -0
  15. package/lib/agent-kit/start.js +45 -1
  16. package/lib/api/configuration.api.js +131 -0
  17. package/lib/api/role-assistant-test-job.api.js +60 -0
  18. package/lib/api/system-secrets.api.js +72 -0
  19. package/lib/api/work-search.api.js +13 -6
  20. package/lib/app/deploy.js +8 -1
  21. package/lib/app/run-docker-fallback.js +6 -1
  22. package/lib/app/run-helpers.js +2 -1
  23. package/lib/app/run-parameter-sync.js +142 -0
  24. package/lib/app/show-display.js +1 -0
  25. package/lib/app/show-online.js +15 -0
  26. package/lib/build/docker-build-args.js +5 -2
  27. package/lib/build/index.js +3 -2
  28. package/lib/build/standard-docker-build.js +6 -3
  29. package/lib/cli/setup-app.js +19 -1
  30. package/lib/cli/setup-environment.js +156 -0
  31. package/lib/cli/setup-utility.js +24 -1
  32. package/lib/commands/datasource-capability-upsert-cli.js +112 -0
  33. package/lib/commands/datasource-capability.js +4 -2
  34. package/lib/commands/env-secret-context.js +113 -0
  35. package/lib/commands/env-secret-list.js +200 -0
  36. package/lib/commands/env-secret-push-confirm.js +54 -0
  37. package/lib/commands/env-secret-push-run.js +227 -0
  38. package/lib/commands/env-secret-push.js +168 -0
  39. package/lib/commands/repair-datasource-apply.js +2 -0
  40. package/lib/commands/repair-datasource-keyed-document.js +122 -0
  41. package/lib/commands/repair-datasource-run.js +1 -0
  42. package/lib/commands/role-assistant.js +7 -0
  43. package/lib/commands/setup-modes.js +1 -8
  44. package/lib/commands/setup-prompts.js +2 -180
  45. package/lib/commands/verify-operations-skip-e2e.js +25 -1
  46. package/lib/commands/verify-operations-steps.js +13 -1
  47. package/lib/commands/wizard-config-normalizer.js +7 -4
  48. package/lib/commands/wizard-core.js +5 -157
  49. package/lib/commands/wizard-file-saving.js +163 -0
  50. package/lib/core/env-platform-expand.js +97 -0
  51. package/lib/core/secrets-env-content.js +42 -4
  52. package/lib/core/secrets-env-write.js +10 -3
  53. package/lib/core/secrets-load.js +4 -2
  54. package/lib/datasource/binary-documents-validator.js +190 -0
  55. package/lib/datasource/capability/run-capability-upsert.js +202 -0
  56. package/lib/datasource/capability/upsert-ingredients.js +291 -0
  57. package/lib/datasource/capability/upsert-operations.js +138 -0
  58. package/lib/datasource/capability/upsert-test-scaffold.js +189 -0
  59. package/lib/datasource/validate.js +12 -5
  60. package/lib/deployment/installation/azure-infra-stage.js +3 -1
  61. package/lib/deployment/installation/infra-catalog.js +2 -5
  62. package/lib/generator/builders.js +17 -0
  63. package/lib/generator/helpers.js +23 -2
  64. package/lib/generator/index.js +13 -6
  65. package/lib/lifecycle/product-model.js +4 -3
  66. package/lib/lifecycle/report-display.js +3 -2
  67. package/lib/parameters/infra-parameter-catalog.js +1 -1
  68. package/lib/programmatic/builder-help-enterprise-sync-fabrix.js +1 -1
  69. package/lib/programmatic/builder-help-governance.js +1 -1
  70. package/lib/programmatic/builder-help.js +1 -1
  71. package/lib/role-assistant/test-cases-search.js +3 -0
  72. package/lib/role-assistant/test-job-runner.js +125 -0
  73. package/lib/role-assistant/test-runner-search.js +44 -2
  74. package/lib/role-assistant/test-runner-workhub-answers.js +4 -1
  75. package/lib/role-assistant/test-runner-workhub-missing-fields.js +42 -0
  76. package/lib/role-assistant/test-runner-workhub-wait-stop.js +94 -0
  77. package/lib/role-assistant/test-runner-workhub.js +28 -19
  78. package/lib/schema/application-schema.json +205 -2
  79. package/lib/schema/external-datasource.schema.json +23 -3
  80. package/lib/schema/infra-parameter.schema.json +139 -33
  81. package/lib/schema/infra.parameter.yaml +416 -66
  82. package/lib/schema/infrastructure-schema.json +10 -35
  83. package/lib/utils/compose-generate-docker-compose.js +16 -9
  84. package/lib/utils/datasource-binary-evidence.js +92 -0
  85. package/lib/utils/datasource-test-run-capability-scope.js +44 -1
  86. package/lib/utils/datasource-test-run-debug-display.js +2 -0
  87. package/lib/utils/datasource-test-run-display.js +8 -2
  88. package/lib/utils/datasource-test-run-issue-guidance.js +176 -0
  89. package/lib/utils/datasource-test-run-tty-log.js +2 -0
  90. package/lib/utils/docker-build.js +29 -8
  91. package/lib/utils/docker-manifest-public-port.js +36 -0
  92. package/lib/utils/env-copy.js +11 -10
  93. package/lib/utils/external-system-system-test-tty.js +3 -2
  94. package/lib/utils/image-tags.js +2 -2
  95. package/lib/utils/platform-kv-ref.js +1 -1
  96. package/lib/utils/platform-resolution.js +226 -0
  97. package/lib/utils/prepare-local-data-mount.js +58 -0
  98. package/lib/utils/resolve-docker-image-ref.js +8 -3
  99. package/lib/utils/secrets-helpers.js +0 -1
  100. package/lib/utils/system-secret-mapping.js +125 -0
  101. package/lib/utils/test-log-writer.js +2 -1
  102. package/lib/validation/external-manifest-validator.js +5 -0
  103. package/lib/validation/openapi-contract-surface-validator.js +3 -1
  104. package/lib/validation/validate-external-file.js +5 -1
  105. package/package.json +5 -4
  106. package/templates/README.md +2 -1
  107. package/templates/agent-kit/agent-kit.yaml +4 -1
  108. package/templates/agent-kit/instructions/AGENTKIT.md +1 -1
  109. package/templates/agent-kit/instructions/root.AGENTS.md +2 -0
  110. package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +35 -2
  111. package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +15 -0
  112. package/templates/agent-kit/skills/aifabrix-connected-system/scripts/delivery-verdict.js +176 -0
  113. package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +6 -2
  114. package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +28 -2
  115. package/templates/agent-kit/skills/aifabrix-prove/references/evidence-lifecycle.md +22 -0
  116. package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +28 -1
  117. package/templates/agent-kit/skills/aifabrix-role-assistant/references/testing-playbook.md +117 -0
  118. package/templates/agent-kit/skills/shared/feedback.md +31 -0
  119. package/templates/agent-kit/skills/shared/hosts.md +13 -3
  120. package/templates/agent-kit/skills/shared/interaction.md +83 -0
  121. package/templates/agent-kit/skills/shared/status.md +76 -0
  122. package/templates/agent-kit/workspace/BUILDER_IMPROVEMENT_FINDINGS.md +27 -0
  123. package/templates/applications/builder-api/application.yaml +1 -1
  124. package/templates/applications/builder-api/env.template +5 -1
  125. package/templates/applications/dataplane/application.yaml +30 -2
  126. package/templates/applications/dataplane/env.template +36 -5
  127. package/templates/applications/keycloak/application.yaml +6 -1
  128. package/templates/applications/miso-controller/application.yaml +96 -1
  129. package/templates/applications/miso-controller/env.template +63 -37
  130. package/templates/applications/miso-controller/rbac.yaml +17 -0
  131. package/templates/external-system/external-datasource.yaml.hbs +7 -1
  132. package/templates/marketplace/main.json +73 -225
  133. package/templates/python/Dockerfile.hbs +2 -0
  134. package/templates/python/docker-compose.hbs +1 -1
  135. package/templates/typescript/Dockerfile.hbs +2 -0
  136. package/templates/typescript/docker-compose.hbs +12 -0
  137. package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +0 -34
  138. /package/{lib/programmatic/help-content → docs/builder-help/content}/channel-onboarding.md +0 -0
  139. /package/{lib/programmatic/help-content → docs/builder-help/content}/cip-overview.md +0 -0
  140. /package/{lib/programmatic/help-content → docs/builder-help/content}/connected-system-ui.md +0 -0
  141. /package/{lib/programmatic/help-content → docs/builder-help/content}/dimensions-guide.md +0 -0
  142. /package/{lib/programmatic/help-content → docs/builder-help/content}/enterprise-sync-fabrix.md +0 -0
  143. /package/{lib/programmatic/help-content → docs/builder-help/content}/overview.md +0 -0
  144. /package/{lib/programmatic/help-content → docs/builder-help/content}/subscription-guide.md +0 -0
  145. /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 mergeDockerManifestPublishedPort(envVars, appDoc);
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, scopedKv, options);
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, { appPath, envOnly });
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 = await secrets.generateEnvContent(appName, secretsPath, environment, force);
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
- let content = await secrets.generateEnvContent(appName, secretsPath, environment, force);
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);
@@ -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
+ };