@aifabrix/builder 2.60.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 (112) 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/api/configuration.api.js +131 -0
  14. package/lib/api/system-secrets.api.js +72 -0
  15. package/lib/app/run-docker-fallback.js +6 -1
  16. package/lib/app/run-helpers.js +2 -1
  17. package/lib/app/run-parameter-sync.js +142 -0
  18. package/lib/build/docker-build-args.js +5 -2
  19. package/lib/build/index.js +3 -2
  20. package/lib/build/standard-docker-build.js +2 -2
  21. package/lib/cli/setup-app.js +5 -0
  22. package/lib/cli/setup-environment.js +156 -0
  23. package/lib/commands/datasource-capability-upsert-cli.js +112 -0
  24. package/lib/commands/datasource-capability.js +4 -2
  25. package/lib/commands/env-secret-context.js +113 -0
  26. package/lib/commands/env-secret-list.js +200 -0
  27. package/lib/commands/env-secret-push-confirm.js +54 -0
  28. package/lib/commands/env-secret-push-run.js +227 -0
  29. package/lib/commands/env-secret-push.js +168 -0
  30. package/lib/commands/repair-datasource-apply.js +2 -0
  31. package/lib/commands/repair-datasource-keyed-document.js +122 -0
  32. package/lib/commands/repair-datasource-run.js +1 -0
  33. package/lib/commands/setup-modes.js +1 -8
  34. package/lib/commands/setup-prompts.js +2 -180
  35. package/lib/commands/verify-operations-skip-e2e.js +25 -1
  36. package/lib/commands/verify-operations-steps.js +13 -1
  37. package/lib/commands/wizard-config-normalizer.js +7 -4
  38. package/lib/commands/wizard-core.js +5 -157
  39. package/lib/commands/wizard-file-saving.js +163 -0
  40. package/lib/core/env-platform-expand.js +5 -1
  41. package/lib/core/secrets-env-content.js +5 -1
  42. package/lib/core/secrets-env-write.js +10 -3
  43. package/lib/core/secrets-load.js +4 -2
  44. package/lib/datasource/binary-documents-validator.js +190 -0
  45. package/lib/datasource/capability/run-capability-upsert.js +202 -0
  46. package/lib/datasource/capability/upsert-ingredients.js +291 -0
  47. package/lib/datasource/capability/upsert-operations.js +138 -0
  48. package/lib/datasource/capability/upsert-test-scaffold.js +189 -0
  49. package/lib/datasource/validate.js +12 -5
  50. package/lib/generator/index.js +3 -0
  51. package/lib/lifecycle/product-model.js +4 -3
  52. package/lib/lifecycle/report-display.js +3 -2
  53. package/lib/parameters/infra-parameter-catalog.js +1 -1
  54. package/lib/programmatic/builder-help-enterprise-sync-fabrix.js +1 -1
  55. package/lib/programmatic/builder-help-governance.js +1 -1
  56. package/lib/programmatic/builder-help.js +1 -1
  57. package/lib/role-assistant/test-runner-workhub-answers.js +4 -1
  58. package/lib/role-assistant/test-runner-workhub-missing-fields.js +42 -0
  59. package/lib/role-assistant/test-runner-workhub-wait-stop.js +94 -0
  60. package/lib/role-assistant/test-runner-workhub.js +28 -19
  61. package/lib/schema/application-schema.json +14 -2
  62. package/lib/schema/external-datasource.schema.json +23 -3
  63. package/lib/schema/infra.parameter.yaml +388 -38
  64. package/lib/utils/compose-generate-docker-compose.js +7 -12
  65. package/lib/utils/datasource-binary-evidence.js +92 -0
  66. package/lib/utils/datasource-test-run-capability-scope.js +44 -1
  67. package/lib/utils/datasource-test-run-debug-display.js +2 -0
  68. package/lib/utils/datasource-test-run-display.js +8 -2
  69. package/lib/utils/datasource-test-run-issue-guidance.js +176 -0
  70. package/lib/utils/datasource-test-run-tty-log.js +2 -0
  71. package/lib/utils/env-copy.js +11 -10
  72. package/lib/utils/external-system-system-test-tty.js +3 -2
  73. package/lib/utils/image-tags.js +2 -2
  74. package/lib/utils/platform-kv-ref.js +1 -1
  75. package/lib/utils/prepare-local-data-mount.js +58 -0
  76. package/lib/utils/secrets-helpers.js +0 -1
  77. package/lib/utils/system-secret-mapping.js +125 -0
  78. package/lib/utils/test-log-writer.js +2 -1
  79. package/lib/validation/external-manifest-validator.js +5 -0
  80. package/lib/validation/openapi-contract-surface-validator.js +3 -1
  81. package/lib/validation/validate-external-file.js +5 -1
  82. package/package.json +4 -3
  83. package/templates/agent-kit/agent-kit.yaml +1 -1
  84. package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +2 -1
  85. package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +15 -0
  86. package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +3 -2
  87. package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +6 -2
  88. package/templates/agent-kit/skills/aifabrix-prove/references/evidence-lifecycle.md +22 -0
  89. package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +7 -1
  90. package/templates/agent-kit/skills/aifabrix-role-assistant/references/testing-playbook.md +117 -0
  91. package/templates/agent-kit/skills/shared/interaction.md +83 -0
  92. package/templates/agent-kit/skills/shared/status.md +3 -12
  93. package/templates/applications/builder-api/application.yaml +1 -1
  94. package/templates/applications/builder-api/env.template +5 -1
  95. package/templates/applications/dataplane/application.yaml +1 -1
  96. package/templates/applications/dataplane/env.template +10 -12
  97. package/templates/applications/keycloak/application.yaml +6 -1
  98. package/templates/applications/miso-controller/application.yaml +74 -2
  99. package/templates/applications/miso-controller/env.template +42 -3
  100. package/templates/applications/miso-controller/rbac.yaml +17 -0
  101. package/templates/external-system/external-datasource.yaml.hbs +7 -1
  102. package/templates/marketplace/main.json +4 -4
  103. package/templates/python/docker-compose.hbs +1 -1
  104. package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +0 -41
  105. /package/{lib/programmatic/help-content → docs/builder-help/content}/channel-onboarding.md +0 -0
  106. /package/{lib/programmatic/help-content → docs/builder-help/content}/cip-overview.md +0 -0
  107. /package/{lib/programmatic/help-content → docs/builder-help/content}/connected-system-ui.md +0 -0
  108. /package/{lib/programmatic/help-content → docs/builder-help/content}/dimensions-guide.md +0 -0
  109. /package/{lib/programmatic/help-content → docs/builder-help/content}/enterprise-sync-fabrix.md +0 -0
  110. /package/{lib/programmatic/help-content → docs/builder-help/content}/overview.md +0 -0
  111. /package/{lib/programmatic/help-content → docs/builder-help/content}/subscription-guide.md +0 -0
  112. /package/{lib/programmatic/help-content → docs/builder-help/content}/workflow.md +0 -0
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Show compact configuration status without exposing sensitive values.
3
+ *
4
+ * @fileoverview aifabrix env secret list
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const logger = require('../utils/logger');
12
+ const { listEnvironmentConfiguration } = require('../api/configuration.api');
13
+ const {
14
+ colorAggregateGlyph,
15
+ metadata,
16
+ sectionTitle
17
+ } = require('../utils/cli-layout-chalk');
18
+
19
+ const FORBIDDEN_FIELDS = ['data', 'ciphertext', 'reference', 'vaultName', 'secretValue'];
20
+ const WIDTHS = Object.freeze({ status: 6, application: 20, name: 28, type: 8, value: 48 });
21
+ const GLYPHS = Object.freeze({ ok: '✔', warn: '⚠', skipped: '⏭' });
22
+
23
+ /**
24
+ * @param {Object} row - Configuration metadata row
25
+ * @param {string} application - Optional expected owner
26
+ * @returns {void} Throws when unsafe or contradictory
27
+ */
28
+ function assertSafeRow(row, application) {
29
+ if (!row || typeof row !== 'object') {
30
+ throw new Error('Configuration list returned invalid or unsafe metadata');
31
+ }
32
+ const owner = String(row.application || '').trim();
33
+ const name = String(row.name || row.key || '').trim();
34
+ const hasValue = Object.prototype.hasOwnProperty.call(row, 'value');
35
+ const safeValue = [!hasValue, row.sensitive === false && typeof row.value === 'string']
36
+ .some(Boolean);
37
+ const forbidden = FORBIDDEN_FIELDS.some(field =>
38
+ Object.prototype.hasOwnProperty.call(row, field)
39
+ );
40
+ const exactOwner = [!application, owner === application].some(Boolean);
41
+ const booleans = ['sensitive', 'configured', 'required']
42
+ .every(field => typeof row[field] === 'boolean');
43
+ const valid = [owner, name, exactOwner, booleans, safeValue, !forbidden].every(Boolean);
44
+ if (!valid) {
45
+ throw new Error('Configuration list returned invalid or unsafe metadata');
46
+ }
47
+ }
48
+
49
+ /**
50
+ * @param {Object} row - Configuration metadata row
51
+ * @param {string} application - Optional expected exact owner
52
+ * @returns {Object} Safe normalized row
53
+ */
54
+ function normalizeRow(row, application) {
55
+ assertSafeRow(row, application);
56
+ const owner = String(row.application).trim();
57
+ const name = String(row.name || row.key).trim();
58
+ const hasValue = Object.prototype.hasOwnProperty.call(row, 'value');
59
+ const state = row.configured ? 'ok' : (row.required ? 'warn' : 'skipped');
60
+ const normalized = {
61
+ application: owner,
62
+ name,
63
+ type: row.sensitive ? 'secret' : 'variable',
64
+ configured: row.configured,
65
+ required: row.required,
66
+ state
67
+ };
68
+ if (!row.sensitive && hasValue) normalized.value = row.value;
69
+ return normalized;
70
+ }
71
+
72
+ /**
73
+ * @param {unknown} value - Cell value
74
+ * @returns {string} Safe single-line cell value
75
+ */
76
+ function cleanCell(value) {
77
+ const noControls = [...String(value ?? '')]
78
+ .map(char => char.charCodeAt(0) <= 31 || char.charCodeAt(0) === 127 ? ' ' : char)
79
+ .join('');
80
+ return noControls.replace(/\s+/g, ' ').trim();
81
+ }
82
+
83
+ /**
84
+ * @param {unknown} value - Cell value
85
+ * @param {number} width - Visible cell width
86
+ * @returns {string} Single-line padded or truncated cell
87
+ */
88
+ function formatCell(value, width) {
89
+ const clean = cleanCell(value);
90
+ const visible = clean.length > width ? `${clean.slice(0, width - 1)}…` : clean;
91
+ return visible.padEnd(width);
92
+ }
93
+
94
+ /**
95
+ * Keep identifiers complete while retaining compact minimum widths.
96
+ * @param {Array<Object>} rows - Safe normalized rows
97
+ * @returns {Object} Widths for this result set
98
+ */
99
+ function tableWidths(rows) {
100
+ return {
101
+ ...WIDTHS,
102
+ application: Math.max(WIDTHS.application, ...rows.map(row => cleanCell(row.application).length)),
103
+ name: Math.max(WIDTHS.name, ...rows.map(row => cleanCell(row.name).length))
104
+ };
105
+ }
106
+
107
+ /**
108
+ * @param {Object} row - Safe normalized row
109
+ * @param {boolean} includeApplication - Include owner column
110
+ * @returns {string} One compact table row
111
+ */
112
+ function formatRow(row, includeApplication, widths = WIDTHS) {
113
+ const glyph = colorAggregateGlyph(row.state, GLYPHS[row.state]);
114
+ const value = row.type === 'variable'
115
+ ? (row.value === '' ? '""' : (row.value ?? ''))
116
+ : '';
117
+ const cells = [glyph + ' '.repeat(widths.status - 1)];
118
+ if (includeApplication) cells.push(formatCell(row.application, widths.application));
119
+ cells.push(formatCell(row.name, widths.name));
120
+ cells.push(formatCell(row.type, widths.type));
121
+ cells.push(formatCell(value, widths.value).trimEnd());
122
+ return ` ${cells.join(' ')}`;
123
+ }
124
+
125
+ /**
126
+ * @param {boolean} includeApplication - Include owner column
127
+ * @returns {string[]} Header and divider
128
+ */
129
+ function tableHeader(includeApplication, widths = WIDTHS) {
130
+ const headers = [formatCell('Status', widths.status)];
131
+ const dividers = ['-'.repeat(widths.status)];
132
+ if (includeApplication) {
133
+ headers.push(formatCell('Application', widths.application));
134
+ dividers.push('-'.repeat(widths.application));
135
+ }
136
+ headers.push(formatCell('Name', widths.name), formatCell('Type', widths.type), 'Value');
137
+ dividers.push('-'.repeat(widths.name), '-'.repeat(widths.type), '-'.repeat(12));
138
+ return [` ${headers.join(' ')}`, ` ${dividers.join(' ')}`];
139
+ }
140
+
141
+ /**
142
+ * @param {Array<Object>} rows - Safe rows
143
+ * @returns {string} Compact totals
144
+ */
145
+ function formatSummary(rows) {
146
+ const configured = rows.filter(row => row.configured).length;
147
+ const requiredMissing = rows.filter(row => !row.configured && row.required).length;
148
+ const optionalUnset = rows.length - configured - requiredMissing;
149
+ const ok = colorAggregateGlyph('ok', GLYPHS.ok);
150
+ const warn = colorAggregateGlyph('warn', GLYPHS.warn);
151
+ const skipped = colorAggregateGlyph('skipped', GLYPHS.skipped);
152
+ const label = rows.length === 1 ? 'entry' : 'entries';
153
+ return `${rows.length} ${label} · ${ok} ${configured} configured · ` +
154
+ `${warn} ${requiredMissing} required missing · ${skipped} ${optionalUnset} optional unset`;
155
+ }
156
+
157
+ /**
158
+ * List Parameter declarations for an environment or exact application owner.
159
+ * @param {Object} ctx - controllerUrl, authConfig, envKey
160
+ * @param {Object} [options] - `api` for injection, application and key search
161
+ * @returns {Promise<{ exitCode: number, rows: Array<Object> }>} Safe normalized rows
162
+ */
163
+ async function runEnvSecretList(ctx, options = {}) {
164
+ const application = String(options.application || '').trim();
165
+ const api = options.api || { listEnvironmentConfiguration };
166
+ const raw = await api.listEnvironmentConfiguration(
167
+ ctx.controllerUrl, ctx.authConfig, ctx.envKey, application || undefined,
168
+ options.key ? { search: String(options.key).trim() } : {}
169
+ );
170
+ if (!Array.isArray(raw)) throw new Error('Configuration list response is not an array');
171
+ const rows = raw.map(row => normalizeRow(row, application)).sort((left, right) =>
172
+ left.application.localeCompare(right.application) || left.name.localeCompare(right.name)
173
+ );
174
+ const title = application ? `${application} (${ctx.envKey})` : ctx.envKey;
175
+ logger.log(sectionTitle(`Configuration: ${title}`));
176
+ logger.log(metadata(`Controller: ${ctx.controllerUrl}`));
177
+ logger.log('');
178
+ if (rows.length === 0) {
179
+ const suffix = options.key ? ` matching "${options.key}"` : '';
180
+ logger.log(metadata(` No configuration entries${suffix}.`));
181
+ return { exitCode: 0, rows };
182
+ }
183
+ const widths = tableWidths(rows);
184
+ tableHeader(!application, widths).forEach(line => logger.log(line));
185
+ rows.forEach(row => logger.log(formatRow(row, !application, widths)));
186
+ logger.log('');
187
+ logger.log(metadata(formatSummary(rows)));
188
+ return { exitCode: 0, rows };
189
+ }
190
+
191
+ module.exports = {
192
+ normalizeRow,
193
+ cleanCell,
194
+ formatCell,
195
+ tableWidths,
196
+ formatRow,
197
+ tableHeader,
198
+ formatSummary,
199
+ runEnvSecretList
200
+ };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Confirmation gate for bulk installation-secret writes.
3
+ *
4
+ * @fileoverview Prevent accidental env secret bulk pushes
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const readline = require('readline');
10
+ const logger = require('../utils/logger');
11
+ const { formatWarningLine } = require('../utils/cli-test-layout-chalk');
12
+
13
+ /**
14
+ * @param {string} question - Prompt text
15
+ * @param {Object} [streams] - Injectable input/output streams
16
+ * @returns {Promise<string>} User response
17
+ */
18
+ function promptLine(question, streams = {}) {
19
+ const input = streams.input || process.stdin;
20
+ const output = streams.output || process.stdout;
21
+ const rl = readline.createInterface({ input, output });
22
+ return new Promise(resolve => {
23
+ rl.question(question, answer => {
24
+ rl.close();
25
+ resolve(String(answer || '').trim());
26
+ });
27
+ });
28
+ }
29
+
30
+ /**
31
+ * Require explicit approval only when every mapped secret would be written.
32
+ * @param {Object} ctx - Resolved environment context
33
+ * @param {Object} options - CLI options
34
+ * @param {Object} [deps] - Injectable prompt and TTY state
35
+ * @returns {Promise<boolean>} True when execution may continue
36
+ */
37
+ async function confirmBulkSecretPush(ctx, options = {}, deps = {}) {
38
+ if (options.key || options.dryRun || options.yes) return true;
39
+ const isTTY = deps.isTTY === undefined
40
+ ? Boolean(process.stdin.isTTY && process.stdout.isTTY) : deps.isTTY;
41
+ if (!isTTY) {
42
+ throw new Error('Bulk configuration push requires --yes in non-interactive or AI usage');
43
+ }
44
+ logger.log(formatWarningLine(
45
+ `Push all declared configuration from the .env for ${options.app} to ${ctx.envKey}?`
46
+ ));
47
+ const prompt = deps.prompt || promptLine;
48
+ const answer = await prompt('Type "yes" to confirm (anything else cancels): ');
49
+ if (String(answer).trim().toLowerCase() === 'yes') return true;
50
+ logger.log('Cancelled. No configuration values were pushed.');
51
+ return false;
52
+ }
53
+
54
+ module.exports = { promptLine, confirmBulkSecretPush };
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Orchestration and reporting for `aifabrix env secret push`.
3
+ *
4
+ * Reporting is the security-sensitive half: every line names a variable, a catalog key and
5
+ * an outcome, and there is no branch that can print a value. Kept separate from the row
6
+ * mechanics so the redaction rule is easy to read in one place.
7
+ *
8
+ * @fileoverview env secret push: run, report, exit code
9
+ * @author AI Fabrix Team
10
+ * @version 1.0.0
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const chalk = require('chalk');
16
+ const logger = require('../utils/logger');
17
+ const {
18
+ OUTCOME, readEnvFile, pushRow
19
+ } = require('./env-secret-push');
20
+ const { parseEnvAssignments } = require('../utils/system-secret-mapping');
21
+ const {
22
+ listEnvironmentConfiguration,
23
+ putEnvironmentConfiguration
24
+ } = require('../api/configuration.api');
25
+ const { putSystemSecret } = require('../api/system-secrets.api');
26
+
27
+ const GLYPH = Object.freeze({ ok: '✔', fail: '✖', skip: '·' });
28
+
29
+ /**
30
+ * @param {Object} row - Reported row
31
+ * @returns {string} One aligned line naming the variable, key and outcome
32
+ */
33
+ function formatRow(row, widths = { name: 34, key: 44 }) {
34
+ const glyph = row.outcome === OUTCOME.updated ? chalk.green(GLYPH.ok)
35
+ : row.outcome === OUTCOME.skipped ? chalk.gray(GLYPH.skip)
36
+ : chalk.red(GLYPH.fail);
37
+ const name = String(row.name).padEnd(widths.name);
38
+ const key = String(row.catalogKey || '—').padEnd(widths.key);
39
+ const detail = row.reason ? `${row.outcome} (${row.reason})` : row.outcome;
40
+ return ` ${name}-> ${key}${glyph} ${detail}`;
41
+ }
42
+
43
+ /**
44
+ * Keep variable and declaration names complete and aligned.
45
+ * @param {Array<Object>} rows - Report rows
46
+ * @returns {{ name: number, key: number }} Result-specific widths
47
+ */
48
+ function reportWidths(rows) {
49
+ return {
50
+ name: Math.max(34, ...rows.map(row => String(row.name).length)),
51
+ key: Math.max(44, ...rows.map(row => String(row.catalogKey || '—').length))
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Print the installation before the first write. Pushing into the wrong installation is
57
+ * silent and unrecoverable, because no value can be read back to show what was overwritten.
58
+ * @param {Object} ctx - controllerUrl, envKey
59
+ * @returns {void}
60
+ */
61
+ function reportTarget(ctx) {
62
+ logger.log(`Controller: ${ctx.controllerUrl} environment: ${ctx.envKey}`);
63
+ logger.log('');
64
+ }
65
+
66
+ /**
67
+ * @param {Array<Object>} results - Reported rows
68
+ * @param {boolean} dryRun - Whether anything was written
69
+ * @returns {number} Exit code: non-zero when any row failed
70
+ */
71
+ function reportSummary(results, dryRun) {
72
+ const failed = results.filter(row => row.outcome !== OUTCOME.updated && row.outcome !== OUTCOME.skipped);
73
+ const updated = results.filter(row => row.outcome === OUTCOME.updated);
74
+ const skipped = results.filter(row => row.outcome === OUTCOME.skipped);
75
+ // Count only the rows that would actually be written. Counting skipped rows as "would be
76
+ // pushed" overstates what the command does, which is the one thing a dry run must not do.
77
+ const considered = results.length - skipped.length;
78
+ logger.log('');
79
+ if (dryRun) {
80
+ const tail = skipped.length > 0 ? ` ${skipped.length} skipped.` : '';
81
+ logger.log(`Dry run: ${updated.length} of ${considered} configuration value(s) would be pushed.${tail} Nothing was written.`);
82
+ return 0;
83
+ }
84
+ if (failed.length === 0) {
85
+ logger.log(`${updated.length} of ${considered} pushed.`);
86
+ return 0;
87
+ }
88
+ logger.log(`${updated.length} of ${considered} pushed; ${failed.length} failed.`);
89
+ reportRemediation(failed);
90
+ return 1;
91
+ }
92
+
93
+ /**
94
+ * One hint per failure class, because each has a different owner.
95
+ * @param {Array<Object>} failed - Failing rows
96
+ * @returns {void}
97
+ */
98
+ function reportRemediation(failed) {
99
+ if (failed.some(row => row.outcome === OUTCOME.notInCatalog)) {
100
+ logger.log('The controller no longer recognizes those configuration declarations.');
101
+ logger.log('Refresh with aifabrix env secret list, then retry using the current declaration.');
102
+ }
103
+ if (failed.some(row => row.outcome === OUTCOME.forbidden)) {
104
+ logger.log('Setting a system secret requires the controller:admin scope. Run: aifabrix login');
105
+ }
106
+ if (failed.some(row => row.outcome === OUTCOME.unreachable)) {
107
+ logger.log('Nothing was written for those keys. Check the controller URL above and retry.');
108
+ }
109
+ if (failed.some(row => row.outcome === OUTCOME.conflict)) {
110
+ logger.log('The configuration changed before the write. Run the push again to use a fresh revision.');
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Map .env variable names to authoritative Controller declarations.
116
+ * @param {string} envContent - Application .env content
117
+ * @param {Array<Object>} declarations - Controller configuration rows
118
+ * @param {string} [only] - Optional exact .env variable name
119
+ * @param {string} [application] - Expected exact application owner
120
+ * @returns {{ pushable: Array<Object>, skipped: Array<Object> }} Revision-bound rows
121
+ */
122
+ function mapEnvConfiguration(envContent, declarations, only, application) {
123
+ const pushable = [];
124
+ const skipped = [];
125
+ const values = parseEnvAssignments(envContent);
126
+ for (const [name, value] of values) {
127
+ if (only && name !== only) continue;
128
+ const matches = declarations.filter(item => item && item.name === name &&
129
+ (!application || item.application === application));
130
+ if (matches.length !== 1) {
131
+ skipped.push({ name, reason: matches.length ? 'ambiguous declaration' : 'not declared' });
132
+ continue;
133
+ }
134
+ const match = matches[0];
135
+ if (typeof match.sensitive !== 'boolean' || !match.key || !match.application || !match.revision) {
136
+ skipped.push({ name, catalogKey: match.key, reason: 'incomplete declaration metadata' });
137
+ continue;
138
+ }
139
+ if (match.sensitive && !value) {
140
+ skipped.push({ name, catalogKey: match.key, reason: 'no secret value in the env file' });
141
+ continue;
142
+ }
143
+ pushable.push({
144
+ name, value, catalogKey: match.key, application: match.application,
145
+ revision: match.revision, sensitive: match.sensitive
146
+ });
147
+ }
148
+ if (only && !values.has(only)) skipped.push({ name: only, reason: 'not present in the env file' });
149
+ return { pushable, skipped };
150
+ }
151
+
152
+ /**
153
+ * @param {Object} deps - Injected api
154
+ * @param {Object} ctx - controllerUrl, authConfig, envKey
155
+ * @param {Array<Object>} ready - Rows to push
156
+ * @returns {Promise<Array<Object>>} Reported rows
157
+ */
158
+ async function pushAll(deps, ctx, ready) {
159
+ const results = [];
160
+ for (const row of ready) {
161
+ const result = await pushRow(deps, ctx, row);
162
+ results.push(result);
163
+ if (result.fatal) {
164
+ // 403 fails identically for every remaining key; report them rather than retry.
165
+ for (const remaining of ready.slice(results.length)) {
166
+ results.push({ name: remaining.name, catalogKey: remaining.catalogKey, outcome: result.outcome });
167
+ }
168
+ break;
169
+ }
170
+ }
171
+ return results;
172
+ }
173
+
174
+ /**
175
+ * Run the push.
176
+ * @param {Object} ctx - controllerUrl, authConfig, envKey, envFilePath
177
+ * @param {Object} [options] - `key`, `dryRun`, `api`
178
+ * @returns {Promise<{ exitCode: number, results: Array<Object> }>} Outcome per row
179
+ */
180
+ async function runEnvSecretPush(ctx, options = {}) {
181
+ const envContent = readEnvFile(ctx.envFilePath, options);
182
+ const api = options.api || {
183
+ listEnvironmentConfiguration, putSystemSecret, putEnvironmentConfiguration
184
+ };
185
+ const declarations = await api.listEnvironmentConfiguration(
186
+ ctx.controllerUrl, ctx.authConfig, ctx.envKey, options.application
187
+ );
188
+ const { pushable, skipped } = mapEnvConfiguration(
189
+ envContent, declarations, options.key, options.application
190
+ );
191
+ reportTarget(ctx);
192
+ // Every mapped row is attempted. Whether an unlisted key can be written differs by
193
+ // installation, so only the server can answer it.
194
+ const results = options.dryRun
195
+ ? dryRunRows(pushable)
196
+ : await pushAll(options, ctx, pushable);
197
+ const reported = results.concat(skipped.map(row => ({ ...row, outcome: OUTCOME.skipped })));
198
+ const widths = reportWidths(reported);
199
+ for (const row of reported) {
200
+ logger.log(formatRow(row, widths));
201
+ }
202
+ return { exitCode: reportSummary(reported, options.dryRun === true), results: reported };
203
+ }
204
+
205
+ /**
206
+ * Dry-run rows: what would be attempted. Catalog validity is decided by PUT on a real run.
207
+ * @param {Array<Object>} pushable - Mapped rows
208
+ * @returns {Array<Object>} Reportable rows that write nothing
209
+ */
210
+ function dryRunRows(pushable) {
211
+ return pushable.map(row => ({
212
+ name: row.name,
213
+ catalogKey: row.catalogKey,
214
+ outcome: OUTCOME.updated
215
+ }));
216
+ }
217
+
218
+ module.exports = {
219
+ formatRow,
220
+ reportWidths,
221
+ reportTarget,
222
+ reportSummary,
223
+ dryRunRows,
224
+ pushAll,
225
+ mapEnvConfiguration,
226
+ runEnvSecretPush
227
+ };
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Push declared configuration values from an operator's env file into an installation.
3
+ *
4
+ * Every value in this module comes from a file and goes to one API call. It is never an
5
+ * argument (so it cannot reach shell history or a process list), never logged, and never
6
+ * placed in a message, a progress line or a JSON envelope — on success or on failure.
7
+ * Rows are reported by variable name, declaration key and outcome only.
8
+ *
9
+ * The command updates Controller declarations and cannot create a new declaration.
10
+ *
11
+ * @fileoverview aifabrix env secret push
12
+ * @author AI Fabrix Team
13
+ * @version 1.0.0
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const { execFileSync } = require('child_process');
21
+ const { putSystemSecret } = require('../api/system-secrets.api');
22
+ const {
23
+ listEnvironmentConfiguration,
24
+ putEnvironmentConfiguration
25
+ } = require('../api/configuration.api');
26
+
27
+ /** Outcome codes reported per row; none of them can carry a value. */
28
+ const OUTCOME = Object.freeze({
29
+ updated: 'updated',
30
+ notInCatalog: 'not declared',
31
+ forbidden: 'not permitted',
32
+ conflict: 'configuration changed; retry',
33
+ unreachable: 'controller unreachable',
34
+ skipped: 'skipped'
35
+ });
36
+
37
+ /**
38
+ * HTTP status from an API client error, when it carries one.
39
+ * @param {Error} error - Thrown error
40
+ * @returns {number} Status code, or 0 when the failure was not an HTTP response
41
+ */
42
+ function statusOf(error) {
43
+ const status = error && (error.status || error.statusCode ||
44
+ (error.response && error.response.status));
45
+ return Number.isInteger(status) ? status : 0;
46
+ }
47
+
48
+ /**
49
+ * The API client resolves non-2xx responses as `{ success: false, status }` rather than
50
+ * throwing, so a failure that is only caught in a try/catch is reported as success. Verified
51
+ * against the live controller: a PUT to an undefined catalog key resolves with status 404.
52
+ * @param {Object} response - Resolved API response
53
+ * @returns {boolean} True when the call did not succeed
54
+ */
55
+ function isFailedResponse(response) {
56
+ if (!response || typeof response !== 'object') {
57
+ return false;
58
+ }
59
+ if (response.success === false) {
60
+ return true;
61
+ }
62
+ const status = statusOf(response);
63
+ return status >= 400;
64
+ }
65
+
66
+ /**
67
+ * Classify a failure by owner. 403, 404 and transport failures need different messages
68
+ * because different people fix them; collapsing them to "failed to push" helps nobody.
69
+ * @param {Error} error - Thrown error
70
+ * @returns {{ outcome: string, fatal: boolean }} Outcome and whether remaining keys are pointless
71
+ */
72
+ function classifyFailure(error) {
73
+ const status = statusOf(error);
74
+ if (status === 403 || status === 401) {
75
+ // Every remaining key fails the same way, so stop rather than repeat it.
76
+ return { outcome: OUTCOME.forbidden, fatal: true };
77
+ }
78
+ if (status === 404) {
79
+ return { outcome: OUTCOME.notInCatalog, fatal: false };
80
+ }
81
+ if (status === 409) {
82
+ return { outcome: OUTCOME.conflict, fatal: true };
83
+ }
84
+ return { outcome: OUTCOME.unreachable, fatal: false };
85
+ }
86
+
87
+ /**
88
+ * A secrets file that git does not ignore is one `git add .` away from being committed, and
89
+ * a committed secret is disclosed no matter what happens afterwards. Refuse rather than
90
+ * read it. An untracked path outside any repository is fine — there is nothing to commit to.
91
+ *
92
+ * @param {string} filePath - Env file path
93
+ * @param {Object} [deps] - `execFileSync` for tests
94
+ * @returns {boolean} True when git ignores the file, or the path is not in a repository
95
+ */
96
+ function isIgnoredByGit(filePath, deps = {}) {
97
+ const exec = deps.execFileSync || execFileSync;
98
+ const cwd = path.dirname(filePath);
99
+ try {
100
+ exec('git', ['check-ignore', '-q', '--', filePath], { cwd, stdio: 'ignore' });
101
+ return true;
102
+ } catch (error) {
103
+ // check-ignore exits 1 when the path is NOT ignored, and 128 outside a repository.
104
+ return statusOf(error) === 128 || (error && error.status === 128);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * @param {string} filePath - Env file path
110
+ * @param {Object} [deps] - `execFileSync` for tests
111
+ * @returns {string} File content
112
+ * @throws {Error} When the file is missing or git does not ignore it
113
+ */
114
+ function readEnvFile(filePath, deps = {}) {
115
+ if (!filePath || !fs.existsSync(filePath)) {
116
+ throw new Error(`Env file not found: ${filePath || '(no path resolved)'}`);
117
+ }
118
+ if (!isIgnoredByGit(filePath, deps)) {
119
+ throw new Error(
120
+ `Refusing to read ${filePath}: git does not ignore it. ` +
121
+ 'A secrets file inside a tracked path can be committed by accident. ' +
122
+ 'Add it to .gitignore, or pass --file with a path outside the repository.'
123
+ );
124
+ }
125
+ return fs.readFileSync(filePath, 'utf8');
126
+ }
127
+
128
+ /**
129
+ * Push one row. Returns an outcome; never returns or throws the value.
130
+ * @param {Object} deps - Injected api
131
+ * @param {Object} ctx - controllerUrl, authConfig, envKey
132
+ * @param {Object} row - name, catalogKey, value
133
+ * @returns {Promise<{ name: string, catalogKey: string, outcome: string, fatal?: boolean }>} Result
134
+ */
135
+ async function pushRow(deps, ctx, row) {
136
+ const api = deps.api || {
137
+ putSystemSecret, listEnvironmentConfiguration, putEnvironmentConfiguration
138
+ };
139
+ try {
140
+ const common = [ctx.controllerUrl, ctx.authConfig, ctx.envKey];
141
+ const response = row.sensitive
142
+ ? await api.putSystemSecret(
143
+ ...common, row.catalogKey, row.value, row.revision
144
+ )
145
+ : await api.putEnvironmentConfiguration(
146
+ ...common, row.application, row.catalogKey,
147
+ { value: row.value, revision: row.revision }
148
+ );
149
+ if (isFailedResponse(response)) {
150
+ const { outcome, fatal } = classifyFailure(response);
151
+ return { name: row.name, catalogKey: row.catalogKey, outcome, fatal };
152
+ }
153
+ return { name: row.name, catalogKey: row.catalogKey, outcome: OUTCOME.updated };
154
+ } catch (error) {
155
+ const { outcome, fatal } = classifyFailure(error);
156
+ return { name: row.name, catalogKey: row.catalogKey, outcome, fatal };
157
+ }
158
+ }
159
+
160
+ module.exports = {
161
+ OUTCOME,
162
+ classifyFailure,
163
+ statusOf,
164
+ isIgnoredByGit,
165
+ isFailedResponse,
166
+ readEnvFile,
167
+ pushRow
168
+ };
@@ -8,6 +8,7 @@
8
8
 
9
9
  'use strict';
10
10
 
11
+ const { repairKeyedDocument } = require('./repair-datasource-keyed-document');
11
12
  const { repairOpenapiSection } = require('./repair-datasource-openapi');
12
13
  const { repairSyncOperationsFromCapabilities } = require('./repair-datasource-sync-operations');
13
14
  const { normalizeAutoRbacOperationKeys } = require('./repair-datasource-auto-rbac');
@@ -132,6 +133,7 @@ function runRepairDatasourceFile(parsed, options, changes, helpers) {
132
133
 
133
134
  const none = helpers.isNoneEntityType(parsed?.entityType);
134
135
 
136
+ updated = repairKeyedDocument(parsed, options, out) || updated;
135
137
  updated = applyBaseDatasourceRepairs(parsed, none, out, options, helpers) || updated;
136
138
  updated = applyOpenapiDatasourceRepairs(parsed, out) || updated;
137
139
  updated = applyOptionalDatasourceRepairs(parsed, options, none, out, helpers) || updated;