@aifabrix/builder 2.50.0 → 2.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/.cursor/plans/157.1-security-quality-violations-by-type.csv +374 -0
  2. package/.cursor/rules/assistant-driven-integration-build.mdc +40 -0
  3. package/integration/hubspot-test/create-hubspot.js +1 -16
  4. package/lib/api/evidence-kinds.api.js +128 -0
  5. package/lib/api/pipeline.api.js +5 -1
  6. package/lib/api/resource-types.api.js +4 -4
  7. package/lib/api/roles.api.js +121 -0
  8. package/lib/api/types/evidence-kinds.types.js +42 -0
  9. package/lib/api/types/roles.types.js +36 -0
  10. package/lib/app/certification-verify-rows.js +1 -1
  11. package/lib/app/show.js +2 -2
  12. package/lib/certification/merge-certification-from-artifact.js +1 -1
  13. package/lib/channels/channel-artifact-generator.js +2 -1
  14. package/lib/cli/index.js +4 -0
  15. package/lib/cli/setup-app.js +22 -5
  16. package/lib/cli/setup-dev.js +23 -7
  17. package/lib/cli/setup-utility-help-after.js +12 -0
  18. package/lib/cli/setup-utility-validate.js +41 -2
  19. package/lib/commands/deploy-gate-failure-details.js +21 -0
  20. package/lib/commands/deploy-gate-orchestrator.js +64 -18
  21. package/lib/commands/dev-cli-handlers.js +6 -3
  22. package/lib/commands/dev-init.js +7 -109
  23. package/lib/commands/dev-refresh.js +197 -0
  24. package/lib/commands/evidence-kind.js +428 -0
  25. package/lib/commands/governance-command-action.js +4 -2
  26. package/lib/commands/repair-openapi-export.js +1 -1
  27. package/lib/commands/repair-openapi-sync.js +14 -6
  28. package/lib/commands/resource-type.js +101 -18
  29. package/lib/commands/role.js +452 -0
  30. package/lib/commands/setup.js +1 -1
  31. package/lib/commands/validate-json.js +230 -0
  32. package/lib/commands/verify-operations-step-plan.js +78 -40
  33. package/lib/core/audit-logger.js +1 -1
  34. package/lib/core/secrets-load.js +1 -3
  35. package/lib/datasource/capability/relate-operations.js +1 -1
  36. package/lib/generator/helpers.js +13 -5
  37. package/lib/generator/split-variables.js +9 -3
  38. package/lib/infrastructure/helpers-docker-check.js +6 -0
  39. package/lib/infrastructure/helpers.js +1 -1
  40. package/lib/programmatic/auth-config.js +3 -2
  41. package/lib/programmatic/bearer-auth.js +1 -1
  42. package/lib/programmatic/enterprise-mcp-client.js +18 -2
  43. package/lib/programmatic/manifest-section-registry.js +18 -1
  44. package/lib/programmatic/openapi-result-schemas.js +28 -11
  45. package/lib/programmatic/openapi-schema-descriptions-core.js +5 -2
  46. package/lib/programmatic/openapi-schema-helpers.js +16 -1
  47. package/lib/programmatic/openapi-spec.js +1 -1
  48. package/lib/programmatic/run-repository-emcp.js +11 -3
  49. package/lib/programmatic/run-template-workspace.js +14 -1
  50. package/lib/resolvers/evidence-kind-file.js +230 -0
  51. package/lib/resolvers/resource-type-file.js +36 -1
  52. package/lib/resolvers/role-file.js +227 -0
  53. package/lib/schema/application-schema.json +22 -2
  54. package/lib/schema/external-datasource.schema.json +85 -2
  55. package/lib/schema/external-system.schema.json +16 -2
  56. package/lib/schema/infra.parameter.yaml +25 -1
  57. package/lib/schema/type/credential-field-registry.json +19 -3
  58. package/lib/utils/api.js +8 -2
  59. package/lib/utils/app-register-config.js +4 -3
  60. package/lib/utils/cli-sync-options.js +59 -3
  61. package/lib/utils/cli-utils.js +11 -3
  62. package/lib/utils/dev-ca-install.js +5 -6
  63. package/lib/utils/dev-config.js +2 -2
  64. package/lib/utils/dev-list-sort.js +145 -0
  65. package/lib/utils/dev-platform-tls.js +0 -63
  66. package/lib/utils/docker-build.js +46 -27
  67. package/lib/utils/docker-daemon-tls-ca.js +1 -1
  68. package/lib/utils/docker-exec.js +91 -6
  69. package/lib/utils/env-copy.js +1 -3
  70. package/lib/utils/error-formatters/error-parser.js +0 -3
  71. package/lib/utils/external-env-template.js +1 -1
  72. package/lib/utils/external-system-readiness-compact-blocks.js +28 -1
  73. package/lib/utils/health-check-request.js +2 -3
  74. package/lib/utils/help-builder.js +3 -0
  75. package/lib/utils/image-name.js +22 -4
  76. package/lib/utils/logger.js +37 -4
  77. package/lib/utils/manifest-source-emit.js +1 -1
  78. package/lib/utils/paths.js +32 -4
  79. package/lib/utils/pipeline-upload-auth-retry.js +4 -0
  80. package/lib/utils/postgres-platform-bootstrap.js +24 -17
  81. package/lib/utils/postgres-wipe.js +5 -2
  82. package/lib/utils/remote-dev-auth.js +39 -4
  83. package/lib/utils/resolve-docker-image-ref.js +36 -6
  84. package/lib/utils/token-manager.js +16 -0
  85. package/lib/utils/upload-sync-options.js +17 -3
  86. package/lib/validation/openapi-contract-surface-validator.js +4 -0
  87. package/lib/validation/validator.js +6 -5
  88. package/package.json +34 -12
  89. package/scripts/export-builder-api-evidence.js +2 -2
  90. package/scripts/lib/plan-actor.js +2 -2
  91. package/scripts/lib/plan-meta.js +1 -1
  92. package/scripts/lib/plan-parse.js +1 -1
  93. package/scripts/lib/plan-prevalidate-gates.js +12 -1
  94. package/scripts/lib/plan-run-report.js +13 -2
  95. package/scripts/pnpm/lib/resolve-aifabrix-bin.mjs +10 -10
  96. package/templates/applications/dataplane/application.yaml +26 -4
  97. package/templates/applications/dataplane/env.template +28 -9
  98. package/templates/applications/dataplane/rbac.yaml +64 -2
  99. package/templates/applications/miso-controller/application.yaml +10 -1
  100. package/templates/applications/miso-controller/env.template +10 -7
  101. package/templates/applications/miso-controller/rbac.yaml +72 -0
  102. package/templates/channels/slack/integration/slack-system.json +60 -2
  103. package/templates/python/requirements.txt +1 -1
@@ -110,25 +110,15 @@ function buildDockerCliArgs(imageName, tag, dockerfilePath, contextPath, buildAr
110
110
  return args;
111
111
  }
112
112
 
113
- function runDockerBuildProcess(buildOpts) {
114
- const {
115
- imageName,
116
- tag,
117
- dockerfilePath,
118
- contextPath,
119
- spinner,
120
- resolve,
121
- reject,
122
- env = {},
123
- buildArgs = {},
124
- noCache = false
125
- } = buildOpts;
126
- const spawnEnv = { ...process.env, ...env };
127
- const args = buildDockerCliArgs(imageName, tag, dockerfilePath, contextPath, buildArgs, noCache);
128
- const dockerProcess = spawn('docker', args, {
129
- shell: process.platform === 'win32',
130
- env: spawnEnv
131
- });
113
+ function createDockerSpawnParams(options) {
114
+ const { imageName, tag, dockerfilePath, contextPath, buildArgs, noCache, env } = options;
115
+ const spawnEnv = { ...process.env, ...(env || {}) };
116
+ const args = buildDockerCliArgs(imageName, tag, dockerfilePath, contextPath, buildArgs || {}, noCache);
117
+ return { spawnEnv, args };
118
+ }
119
+
120
+ function attachDockerBuildListeners(dockerProcess, context) {
121
+ const { imageName, tag, spinner, resolve, reject } = context;
132
122
  let stdoutBuffer = '';
133
123
  let stderrBuffer = '';
134
124
  const lastProgressUpdateRef = { current: Date.now() };
@@ -162,6 +152,35 @@ function runDockerBuildProcess(buildOpts) {
162
152
  });
163
153
  }
164
154
 
155
+ function runDockerBuildProcess(buildOpts) {
156
+ const {
157
+ imageName,
158
+ tag,
159
+ dockerfilePath,
160
+ contextPath,
161
+ spinner,
162
+ resolve,
163
+ reject,
164
+ env = {},
165
+ buildArgs = {},
166
+ noCache = false
167
+ } = buildOpts;
168
+ const { spawnEnv, args } = createDockerSpawnParams({
169
+ imageName,
170
+ tag,
171
+ dockerfilePath,
172
+ contextPath,
173
+ buildArgs,
174
+ noCache,
175
+ env
176
+ });
177
+ const dockerProcess = spawn('docker', args, {
178
+ shell: process.platform === 'win32',
179
+ env: spawnEnv
180
+ });
181
+ attachDockerBuildListeners(dockerProcess, { imageName, tag, spinner, resolve, reject });
182
+ }
183
+
165
184
  /**
166
185
  * Executes Docker build command with progress indicator
167
186
  * @param {string} imageName - Image name to build
@@ -236,12 +255,10 @@ async function executeBuild(imageName, dockerfilePath, contextPath, tag, options
236
255
 
237
256
  // Tag image if additional tag provided
238
257
  if (options && options.tag && options.tag !== 'latest') {
239
- const { promisify } = require('util');
240
- const { exec } = require('child_process');
241
- const run = promisify(exec);
258
+ const { execFileWithDockerEnv } = require('./docker-exec');
242
259
  const { getDockerExecEnv } = require('./remote-docker-env');
243
260
  const env = await getDockerExecEnv();
244
- await run(`docker tag ${imageName}:${tag} ${imageName}:latest`, { env });
261
+ await execFileWithDockerEnv('docker', ['tag', `${imageName}:${tag}`, `${imageName}:latest`], { env });
245
262
  }
246
263
  }
247
264
 
@@ -270,12 +287,14 @@ async function executeDockerBuildWithTag(effectiveImageName, imageName, dockerfi
270
287
  }
271
288
 
272
289
  try {
273
- const { promisify } = require('util');
274
- const { exec } = require('child_process');
275
- const run = promisify(exec);
290
+ const { execFileWithDockerEnv } = require('./docker-exec');
276
291
  const { getDockerExecEnv } = require('./remote-docker-env');
277
292
  const env = await getDockerExecEnv();
278
- await run(`docker tag ${effectiveImageName}:${tag} ${imageName}:${tag}`, { env });
293
+ await execFileWithDockerEnv(
294
+ 'docker',
295
+ ['tag', `${effectiveImageName}:${tag}`, `${imageName}:${tag}`],
296
+ { env }
297
+ );
279
298
  logger.log(formatSuccessLine(`Tagged image: ${imageName}:${tag}`));
280
299
  } catch (err) {
281
300
  logger.log(
@@ -183,7 +183,7 @@ function fetchDockerDaemonTlsCaPem(dockerEndpoint, opts) {
183
183
  host: parsed.host,
184
184
  port: parsed.port,
185
185
  servername: parsed.servername,
186
- rejectUnauthorized: false
186
+ rejectUnauthorized: true
187
187
  },
188
188
  () => {
189
189
  try {
@@ -9,9 +9,85 @@
9
9
  'use strict';
10
10
 
11
11
  const { promisify } = require('util');
12
- const { exec } = require('child_process');
12
+ const childProcess = require('child_process');
13
+ const execFileAsync = childProcess.execFile ? promisify(childProcess.execFile) : null;
13
14
 
14
- const execAsync = promisify(exec);
15
+ /**
16
+ * Parse a command string into argv tokens without invoking a shell.
17
+ * Supports simple single/double quoted segments and escaping quotes within double quotes.
18
+ * @param {string} command
19
+ * @returns {string[]}
20
+ */
21
+ // eslint-disable-next-line max-statements, complexity -- tokenizer intentionally handles quoted token edge-cases in one place
22
+ function parseCommandTokens(command) {
23
+ const input = String(command || '');
24
+ if (!input.trim()) {
25
+ throw new Error('Command must be a non-empty string');
26
+ }
27
+
28
+ /** @type {string[]} */
29
+ const tokens = [];
30
+ let current = '';
31
+ let quote = null;
32
+
33
+ for (let i = 0; i < input.length; i += 1) {
34
+ const ch = input[i];
35
+ if (quote === null && (ch === '"' || ch === '\'')) {
36
+ quote = ch;
37
+ continue;
38
+ }
39
+ if (quote !== null && ch === quote) {
40
+ quote = null;
41
+ continue;
42
+ }
43
+ // Bash double-quote escapes: \", \\, \$, \`, and escaped newlines.
44
+ if (quote === '"' && ch === '\\') {
45
+ const next = input[i + 1];
46
+ if (next === '"' || next === '\\' || next === '$' || next === '`' || next === '\n') {
47
+ current += next;
48
+ i += 1;
49
+ continue;
50
+ }
51
+ }
52
+ if (quote === null && /\s/.test(ch)) {
53
+ if (current) {
54
+ tokens.push(current);
55
+ current = '';
56
+ }
57
+ continue;
58
+ }
59
+ current += ch;
60
+ }
61
+
62
+ if (quote !== null) {
63
+ throw new Error('Unterminated quote in command');
64
+ }
65
+ if (current) {
66
+ tokens.push(current);
67
+ }
68
+ if (!tokens.length) {
69
+ throw new Error('Command produced no executable token');
70
+ }
71
+ return tokens;
72
+ }
73
+
74
+ /**
75
+ * Run an executable with argv and remote Docker env settings.
76
+ * @param {string} file
77
+ * @param {string[]} [args]
78
+ * @param {import('child_process').ExecFileOptions} [options]
79
+ * @returns {Promise<{ stdout: string, stderr: string }>}
80
+ */
81
+ async function execFileWithDockerEnv(file, args = [], options = {}) {
82
+ const { getDockerExecEnv } = require('./remote-docker-env');
83
+ const env = { ...(await getDockerExecEnv()), ...(options.env || {}) };
84
+ if (execFileAsync) {
85
+ return execFileAsync(file, args, { ...options, env });
86
+ }
87
+ const execAsync = promisify(childProcess.exec);
88
+ const command = [file, ...args.map((arg) => String(arg))].join(' ');
89
+ return execAsync(command, { ...options, env });
90
+ }
15
91
 
16
92
  /**
17
93
  * Run a shell command with process.env merged with remote Docker client settings when docker-endpoint is set.
@@ -20,9 +96,18 @@ const execAsync = promisify(exec);
20
96
  * @returns {Promise<{ stdout: string, stderr: string }>}
21
97
  */
22
98
  async function execWithDockerEnv(command, options = {}) {
23
- const { getDockerExecEnv } = require('./remote-docker-env');
24
- const env = { ...(await getDockerExecEnv()), ...(options.env || {}) };
25
- return execAsync(command, { ...options, env });
99
+ if (!execFileAsync || process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined) {
100
+ const { getDockerExecEnv } = require('./remote-docker-env');
101
+ const env = { ...(await getDockerExecEnv()), ...(options.env || {}) };
102
+ const execAsync = promisify(childProcess.exec);
103
+ return execAsync(command, { ...options, env });
104
+ }
105
+ const [file, ...args] = parseCommandTokens(command);
106
+ return execFileWithDockerEnv(file, args, options);
26
107
  }
27
108
 
28
- module.exports = { execWithDockerEnv };
109
+ module.exports = {
110
+ execWithDockerEnv,
111
+ execFileWithDockerEnv,
112
+ parseCommandTokens
113
+ };
@@ -385,9 +385,7 @@ async function processEnvVariables(envPath, variablesPath, appName, secretsPath,
385
385
  }
386
386
 
387
387
  const label = variables.build.envOutputPath;
388
- const appPathForLocal =
389
- (copyOptions && copyOptions.appPath) ||
390
- (variablesPath ? path.dirname(variablesPath) : null);
388
+ const appPathForLocal = (copyOptions && copyOptions.appPath) || path.dirname(variablesPath);
391
389
  const preferLocal =
392
390
  copyOptions && typeof copyOptions === 'object' && copyOptions.preferLocalEnvOutputPath === true;
393
391
  const freshEnv = copyOptions && typeof copyOptions === 'object' && copyOptions.freshEnv === true;
@@ -25,9 +25,6 @@ const { formatNetworkError } = require('./network-errors');
25
25
  * @returns {Object} Parsed error data object
26
26
  */
27
27
  function parseErrorData(errorResponse) {
28
- if (errorResponse === undefined || errorResponse === null) {
29
- return { message: 'Unknown error occurred' };
30
- }
31
28
  if (typeof errorResponse === 'string') {
32
29
  try {
33
30
  return JSON.parse(errorResponse);
@@ -142,7 +142,7 @@ function buildConfigurationEntries(system) {
142
142
  function buildExternalEnvTemplateContext(system) {
143
143
  const authBlock = system?.authentication || system?.auth;
144
144
  const credential = getAuthCredentialTemplate(authBlock);
145
- let authMethod = 'apikey';
145
+ let authMethod;
146
146
  if (authBlock?.authType === 'apiProvider') {
147
147
  authMethod = 'apiprovider';
148
148
  } else if (authBlock?.authType === 'none') {
@@ -215,7 +215,34 @@ function logDeployReadinessSummaryCompact(ctx) {
215
215
  logCompactTip({ systemKey, command: 'deploy', probed: !!ctx.probeData });
216
216
  }
217
217
 
218
+ /**
219
+ * Compact abort card when a deploy --gate pillar fails before controller deploy.
220
+ * @param {Object} ctx
221
+ */
222
+ function logDeployGateAbortCompact(ctx) {
223
+ const systemKey = ctx.systemKey || 'system';
224
+ logCompactHeader({
225
+ command: 'deploy',
226
+ systemKey,
227
+ environment: ctx.environment || 'dev',
228
+ dataplaneUrl: ctx.dataplaneUrl
229
+ });
230
+ logger.log(` ${formatSuccessLine('Local validation')}`);
231
+ const pf = formatPreflightLine(ctx.baseline || { unitPassed: true, integrationPassed: true });
232
+ if (pf) logger.log(` ${pf}`);
233
+ for (const gate of ctx.gatesSoFar || []) {
234
+ logger.log(` ${formatGatePillarLine(gate)}`);
235
+ }
236
+ const pillar = ctx.failedPillar || 'operations';
237
+ const detail = ctx.failedDetail || 'gate failed';
238
+ const label = PILLAR_LABELS[pillar] || pillar;
239
+ logger.log(` ${formatBlockingError(`${label} · FAILED — ${detail}`)}`);
240
+ logger.log('');
241
+ logCompactTip({ systemKey, command: 'deploy', probed: false });
242
+ }
243
+
218
244
  module.exports = {
219
245
  logUploadReadinessSummaryCompact,
220
- logDeployReadinessSummaryCompact
246
+ logDeployReadinessSummaryCompact,
247
+ logDeployGateAbortCompact
221
248
  };
@@ -50,7 +50,7 @@ function handleHealthResponse(res, data, debug, resolve) {
50
50
  resolve(isHealthy);
51
51
  }
52
52
 
53
- function doHealthCheckRequest(healthCheckUrl, debug, resolve, reject, allowInsecureTls = false) {
53
+ function doHealthCheckRequest(healthCheckUrl, debug, resolve, reject, _allowInsecureTls = false) {
54
54
  try {
55
55
  const urlObj = new URL(healthCheckUrl);
56
56
  const transport = urlObj.protocol === 'https:' ? https : http;
@@ -58,8 +58,7 @@ function doHealthCheckRequest(healthCheckUrl, debug, resolve, reject, allowInsec
58
58
  hostname: urlObj.hostname,
59
59
  port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
60
60
  path: urlObj.pathname + urlObj.search,
61
- method: 'GET',
62
- ...(allowInsecureTls ? { rejectUnauthorized: false } : {})
61
+ method: 'GET'
63
62
  };
64
63
  if (debug) {
65
64
  logger.log(chalk.gray(`[DEBUG] Health check request: ${healthCheckUrl}`));
@@ -78,6 +78,8 @@ const CATEGORIES = [
78
78
  { name: 'dimension' },
79
79
  { name: 'dimension value', term: 'dimension value <command>' },
80
80
  { name: 'resource-type' },
81
+ { name: 'role' },
82
+ { name: 'evidence-kind' },
81
83
  { name: 'enterprise-knowledge' },
82
84
  { name: 'identity' },
83
85
  { name: 'protection' },
@@ -93,6 +95,7 @@ const CATEGORIES = [
93
95
  { name: 'convert', term: 'convert <appKey|systemKey>' },
94
96
  { name: 'show', term: 'show <appKey|systemKey>' },
95
97
  { name: 'validate', term: 'validate [appKey|systemKey|file]' },
98
+ { name: 'validate-json', term: 'validate-json [appKey|systemKey]' },
96
99
  { name: 'parameters', term: 'parameters validate' },
97
100
  { name: 'diff', term: 'diff <file1> <file2>' }
98
101
  ]
@@ -9,6 +9,23 @@
9
9
 
10
10
  'use strict';
11
11
 
12
+ const DOCKER_REPOSITORY_PATH_RE = /^[a-z0-9][a-z0-9._/-]*$/i;
13
+
14
+ /**
15
+ * @param {string} repositoryPath
16
+ * @returns {string}
17
+ */
18
+ function assertSafeRepositoryPath(repositoryPath) {
19
+ const normalized = String(repositoryPath || '').trim();
20
+ if (!normalized) {
21
+ throw new Error('Repository path is required');
22
+ }
23
+ if (!DOCKER_REPOSITORY_PATH_RE.test(normalized)) {
24
+ throw new Error(`Repository path contains unsafe characters: ${repositoryPath}`);
25
+ }
26
+ return normalized;
27
+ }
28
+
12
29
  /**
13
30
  * Builds a developer-scoped image name for local Docker builds.
14
31
  * Format: "<base>-dev<developerId>" when developerId is a positive integer.
@@ -55,18 +72,19 @@ function buildDevImageRepositoryPath(repositoryPath, developerId) {
55
72
  if (!repositoryPath || typeof repositoryPath !== 'string') {
56
73
  throw new Error('Repository path is required and must be a string');
57
74
  }
75
+ const safeRepositoryPath = assertSafeRepositoryPath(repositoryPath);
58
76
  const idNum =
59
77
  typeof developerId === 'number' ? developerId : parseInt(String(developerId), 10);
60
78
  if (!Number.isFinite(idNum) || idNum === 0) {
61
- return repositoryPath;
79
+ return safeRepositoryPath;
62
80
  }
63
- const idx = repositoryPath.lastIndexOf('/');
64
- const tail = idx === -1 ? repositoryPath : repositoryPath.slice(idx + 1);
81
+ const idx = safeRepositoryPath.lastIndexOf('/');
82
+ const tail = idx === -1 ? safeRepositoryPath : safeRepositoryPath.slice(idx + 1);
65
83
  const scopedTail = buildDevImageName(tail, developerId);
66
84
  if (idx === -1) {
67
85
  return scopedTail;
68
86
  }
69
- return `${repositoryPath.slice(0, idx)}/${scopedTail}`;
87
+ return `${safeRepositoryPath.slice(0, idx)}/${scopedTail}`;
70
88
  }
71
89
 
72
90
  module.exports = {
@@ -11,6 +11,39 @@
11
11
 
12
12
  /* eslint-disable no-console */
13
13
 
14
+ const SENSITIVE_KEY_RE = /(password|secret|token|apikey|apiKey|authorization|clientSecret|x-client-token)/i;
15
+ const BEARER_TOKEN_RE = /(Bearer\s+)[A-Za-z0-9._~-]+/gi;
16
+
17
+ function maskString(value) {
18
+ const asString = String(value);
19
+ return asString.replace(BEARER_TOKEN_RE, '$1[REDACTED]');
20
+ }
21
+
22
+ function sanitizeValue(value) {
23
+ if (value instanceof Error) {
24
+ return value;
25
+ }
26
+ if (typeof value === 'string') {
27
+ return maskString(value);
28
+ }
29
+ if (Array.isArray(value)) {
30
+ return value.map((item) => sanitizeValue(item));
31
+ }
32
+ if (value && typeof value === 'object') {
33
+ /** @type {Record<string, any>} */
34
+ const out = {};
35
+ for (const [key, entry] of Object.entries(value)) {
36
+ if (SENSITIVE_KEY_RE.test(key)) {
37
+ out[key] = '[REDACTED]';
38
+ } else {
39
+ out[key] = sanitizeValue(entry);
40
+ }
41
+ }
42
+ return out;
43
+ }
44
+ return value;
45
+ }
46
+
14
47
  /**
15
48
  * Logger utility that wraps console methods
16
49
  * All console statements should use this logger to avoid eslint warnings
@@ -21,7 +54,7 @@ const logger = {
21
54
  * @param {...any} args - Arguments to log
22
55
  */
23
56
  log: (...args) => {
24
- console.log(...args);
57
+ console.log(...args.map((arg) => sanitizeValue(arg)));
25
58
  },
26
59
 
27
60
  /**
@@ -29,7 +62,7 @@ const logger = {
29
62
  * @param {...any} args - Arguments to log
30
63
  */
31
64
  error: (...args) => {
32
- console.error(...args);
65
+ console.error(...args.map((arg) => sanitizeValue(arg)));
33
66
  },
34
67
 
35
68
  /**
@@ -37,7 +70,7 @@ const logger = {
37
70
  * @param {...any} args - Arguments to log
38
71
  */
39
72
  warn: (...args) => {
40
- console.warn(...args);
73
+ console.warn(...args.map((arg) => sanitizeValue(arg)));
41
74
  },
42
75
 
43
76
  /**
@@ -45,7 +78,7 @@ const logger = {
45
78
  * @param {...any} args - Arguments to log
46
79
  */
47
80
  info: (...args) => {
48
- console.log(...args);
81
+ console.log(...args.map((arg) => sanitizeValue(arg)));
49
82
  }
50
83
  };
51
84
 
@@ -74,7 +74,7 @@ function getManifestSourcePayload(appKey, appPath, opts = {}) {
74
74
  } catch {
75
75
  configPath = '';
76
76
  }
77
- let tier = 'resolved';
77
+ let tier;
78
78
  try {
79
79
  tier = computeTierForAppPath(key, dir, opts.cwd);
80
80
  } catch {
@@ -537,19 +537,45 @@ function isAppSubdirSync(root, name) {
537
537
  }
538
538
 
539
539
  /**
540
- * Lists app names (directories) under integration root. Excludes dot-prefixed entries.
540
+ * True when an integration/builder app directory has application config (yaml/json or legacy variables.yaml).
541
+ * Skips helper folders such as fixtures/ and wizard-configs/.
542
+ * @param {string} appDir - Absolute path to candidate app directory
543
+ * @returns {boolean}
544
+ */
545
+ function hasApplicationConfigSync(appDir) {
546
+ if (!appDir || typeof appDir !== 'string') return false;
547
+ const disk = nodeFs();
548
+ const names = [
549
+ 'application.yaml',
550
+ 'application.yml',
551
+ 'application.json',
552
+ 'variables.yaml'
553
+ ];
554
+ return names.some((name) => {
555
+ try {
556
+ return disk.existsSync(path.join(appDir, name));
557
+ } catch {
558
+ return false;
559
+ }
560
+ });
561
+ }
562
+
563
+ /**
564
+ * Lists app names (directories) under integration root. Excludes dot-prefixed entries
565
+ * and directories without application config (e.g. fixtures/, wizard-configs/).
541
566
  * Returns [] if root does not exist.
542
567
  * @returns {string[]} Sorted list of app directory names
543
568
  */
544
569
  function listIntegrationAppNames() {
545
570
  const disk = nodeFs();
546
571
  const names = new Set();
572
+ const withAppConfig = (root) => (name) => hasApplicationConfigSync(path.join(root, name));
547
573
  const cwdRoot = getCwdIntegrationRoot();
548
574
  if (cwdRoot) {
549
- addBuilderSubdirNamesToSet(disk, cwdRoot, names, null);
575
+ addBuilderSubdirNamesToSet(disk, cwdRoot, names, withAppConfig(cwdRoot));
550
576
  }
551
577
  const matInt = getIntegrationRoot();
552
- addBuilderSubdirNamesToSet(disk, matInt, names, null);
578
+ addBuilderSubdirNamesToSet(disk, matInt, names, withAppConfig(matInt));
553
579
  return [...names].sort();
554
580
  }
555
581
 
@@ -595,7 +621,9 @@ function listBuilderAppNames() {
595
621
  }
596
622
  roots.add(path.resolve(getSystemBuilderRoot()));
597
623
  for (const r of roots) {
598
- addBuilderSubdirNamesToSet(disk, r, names, null);
624
+ addBuilderSubdirNamesToSet(disk, r, names, (name) =>
625
+ hasApplicationConfigSync(path.join(r, name))
626
+ );
599
627
  }
600
628
  return [...names].sort();
601
629
  }
@@ -64,6 +64,10 @@ async function uploadApplicationViaPipelineWithAuthRetry(dataplaneUrl, authConfi
64
64
  if (!isPipelineAuthFailure(first) || authConfig.type !== 'bearer') {
65
65
  return first;
66
66
  }
67
+ // Token-forward: do not swap the caller JWT for application client-token.
68
+ if (authConfig.preserveBearer) {
69
+ return first;
70
+ }
67
71
  const clientAuth = await resolveClientTokenAuthForPipelineRetry(systemKey);
68
72
  if (!clientAuth || clientAuth.type === 'bearer') {
69
73
  return first;
@@ -68,29 +68,37 @@ function extractPasswordFromUrlOrValue(urlOrPassword) {
68
68
  }
69
69
 
70
70
  /**
71
- * Escape SQL for embedding in a double-quoted shell argument (-c "...").
72
- * Bash expands `$` and `$$` (PID) inside double quotes; backslash-escape every `$`.
71
+ * Run psql -c inside the Postgres container without a shell.
72
+ * Passes SQL as an argv element so dollar-quoting (DO $$ … $$) is not mangled.
73
73
  *
74
- * @param {string} s
75
- * @returns {string}
76
- */
77
- function escapeShellDoubleQuoted(s) {
78
- return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$');
79
- }
80
-
81
- /**
82
74
  * @async
83
75
  * @param {string} container
84
76
  * @param {string} sql
85
77
  * @param {string} adminPassword
78
+ * @param {string} [database='postgres']
86
79
  * @returns {Promise<void>}
87
80
  */
88
81
  async function runPsqlCommand(container, sql, adminPassword, database = 'postgres') {
89
- const escapedSql = escapeShellDoubleQuoted(sql);
90
- const cmd =
91
- `docker exec -e PGPASSWORD -i ${container} psql -U ${SUPERUSER_ROLE} -d ${database} ` +
92
- `-v ON_ERROR_STOP=1 -c "${escapedSql}"`;
93
- await dockerExec.execWithDockerEnv(cmd, { env: { PGPASSWORD: adminPassword } });
82
+ await dockerExec.execFileWithDockerEnv(
83
+ 'docker',
84
+ [
85
+ 'exec',
86
+ '-e',
87
+ 'PGPASSWORD',
88
+ '-i',
89
+ container,
90
+ 'psql',
91
+ '-U',
92
+ SUPERUSER_ROLE,
93
+ '-d',
94
+ database,
95
+ '-v',
96
+ 'ON_ERROR_STOP=1',
97
+ '-c',
98
+ sql
99
+ ],
100
+ { env: { PGPASSWORD: adminPassword } }
101
+ );
94
102
  }
95
103
 
96
104
  /**
@@ -194,6 +202,5 @@ module.exports = {
194
202
  PLATFORM_DATABASE_SLOTS,
195
203
  bootstrapPlatformPostgresDatabases,
196
204
  pgRoleName,
197
- extractPasswordFromUrlOrValue,
198
- escapeShellDoubleQuoted
205
+ extractPasswordFromUrlOrValue
199
206
  };
@@ -86,9 +86,12 @@ async function runPsql(container, sql, adminPassword) {
86
86
  if (!adminPassword || typeof adminPassword !== 'string') {
87
87
  throw new Error('Admin password is required');
88
88
  }
89
- const escapedSql = sql.replace(/"/g, '\\"');
90
- const cmd = `docker exec -e PGPASSWORD -i ${container} psql -U ${SUPERUSER_ROLE} -d postgres -tAc "${escapedSql}"`;
89
+ if (!/^[a-z0-9][a-z0-9._-]*$/i.test(container)) {
90
+ throw new Error(`Unsafe Postgres container name: ${container}`);
91
+ }
91
92
  const env = { PGPASSWORD: adminPassword };
93
+ const safeSqlToken = JSON.stringify(String(sql));
94
+ const cmd = `docker exec -e PGPASSWORD -i ${container} psql -U ${SUPERUSER_ROLE} -d postgres -tAc ${safeSqlToken}`;
92
95
  const result = (await dockerExec.execWithDockerEnv(cmd, { env })) || {};
93
96
  return String(result.stdout || '').trim();
94
97
  }