@aifabrix/builder 2.50.0 → 2.51.1

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 (236) hide show
  1. package/.cursor/plans/Done/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/jest.isolated-projects.js +77 -1
  5. package/jest.projects.js +5 -426
  6. package/lib/api/assistants.api.js +67 -0
  7. package/lib/api/datasources-extended.api.js +6 -6
  8. package/lib/api/evidence-authoring-helpers.js +267 -0
  9. package/lib/api/evidence-authoring.api.js +337 -0
  10. package/lib/api/evidence-kinds.api.js +128 -0
  11. package/lib/api/evidence.api.js +57 -1
  12. package/lib/api/index.js +24 -7
  13. package/lib/api/pipeline.api.js +47 -3
  14. package/lib/api/records-bulk.api.js +28 -3
  15. package/lib/api/resource-types.api.js +4 -4
  16. package/lib/api/roles.api.js +121 -0
  17. package/lib/api/types/evidence-kinds.types.js +42 -0
  18. package/lib/api/types/evidence.types.js +156 -8
  19. package/lib/api/types/roles.types.js +36 -0
  20. package/lib/api/work.api.js +283 -0
  21. package/lib/app/certification-verify-rows.js +1 -1
  22. package/lib/app/run-deploy-job.js +108 -0
  23. package/lib/app/run-helpers.js +10 -3
  24. package/lib/app/run.js +8 -0
  25. package/lib/app/show.js +2 -2
  26. package/lib/certification/merge-certification-from-artifact.js +1 -1
  27. package/lib/channels/add-channel.js +54 -23
  28. package/lib/channels/approval-guides/chatgpt.js +8 -7
  29. package/lib/channels/approval-guides/microsoft-copilot.js +7 -8
  30. package/lib/channels/channel-artifact-generator.js +16 -3
  31. package/lib/channels/channel-credential.js +3 -1
  32. package/lib/channels/channel-probe-evidence.js +79 -0
  33. package/lib/channels/channel-probe.js +265 -13
  34. package/lib/channels/channel-runtime-resolver.js +120 -29
  35. package/lib/channels/channel-status-display.js +44 -0
  36. package/lib/channels/channel-targets.js +7 -0
  37. package/lib/channels/chatgpt-runtime-contract.js +29 -0
  38. package/lib/channels/microsoft-copilot-package.js +280 -0
  39. package/lib/channels/runtime-tool-catalog.js +165 -0
  40. package/lib/cli/index.js +6 -0
  41. package/lib/cli/setup-app.js +26 -5
  42. package/lib/cli/setup-dev.js +23 -7
  43. package/lib/cli/setup-external-system-help.js +8 -2
  44. package/lib/cli/setup-external-system.js +12 -0
  45. package/lib/cli/setup-utility-help-after.js +12 -0
  46. package/lib/cli/setup-utility-validate.js +41 -2
  47. package/lib/commands/app-test.js +2 -14
  48. package/lib/commands/datasource-load-export-cli.js +6 -1
  49. package/lib/commands/deploy-gate-failure-details.js +23 -0
  50. package/lib/commands/deploy-gate-orchestrator.js +64 -18
  51. package/lib/commands/dev-cli-handlers.js +6 -3
  52. package/lib/commands/dev-down.js +2 -6
  53. package/lib/commands/dev-init.js +7 -109
  54. package/lib/commands/dev-refresh.js +197 -0
  55. package/lib/commands/evidence-kind.js +478 -0
  56. package/lib/commands/governance-command-action.js +4 -2
  57. package/lib/commands/governance-generate-external.js +56 -21
  58. package/lib/commands/lifecycle-external.js +10 -4
  59. package/lib/commands/repair-datasource-apply.js +10 -1
  60. package/lib/commands/repair-datasource-run.js +4 -2
  61. package/lib/commands/repair-datasource-test-payload.js +80 -19
  62. package/lib/commands/repair-datasource-webhooks-sync-bridge.js +42 -3
  63. package/lib/commands/repair-internal.js +9 -1
  64. package/lib/commands/repair-openapi-export.js +1 -1
  65. package/lib/commands/repair-openapi-sync.js +43 -18
  66. package/lib/commands/resource-type.js +101 -18
  67. package/lib/commands/role-assistant.js +313 -0
  68. package/lib/commands/role.js +452 -0
  69. package/lib/commands/setup.js +1 -1
  70. package/lib/commands/upload-data-sync-finalize.js +5 -4
  71. package/lib/commands/upload-sidecar-followups.js +144 -47
  72. package/lib/commands/upload.js +43 -6
  73. package/lib/commands/validate-json.js +230 -0
  74. package/lib/commands/verify-governance-combined-external.js +73 -9
  75. package/lib/commands/verify-governance-db.js +80 -25
  76. package/lib/commands/verify-governance-external.js +29 -5
  77. package/lib/commands/verify-operations-step-plan.js +78 -40
  78. package/lib/commands/wizard-core-helpers.js +7 -3
  79. package/lib/commands/wizard-core.js +78 -9
  80. package/lib/core/audit-logger.js +1 -1
  81. package/lib/core/secrets-load.js +1 -3
  82. package/lib/datasource/bulk-loader-run-batches.js +4 -2
  83. package/lib/datasource/bulk-loader-service.js +5 -1
  84. package/lib/datasource/capability/relate-operations.js +1 -1
  85. package/lib/datasource/datasource-validate-summary.js +2 -2
  86. package/lib/datasource/validate.js +13 -6
  87. package/lib/external-system/deploy-helpers.js +35 -1
  88. package/lib/external-system/deploy.js +23 -10
  89. package/lib/external-system/download-helpers.js +111 -2
  90. package/lib/external-system/download-role-assistant.js +55 -0
  91. package/lib/external-system/download.js +45 -26
  92. package/lib/external-system/test-helpers.js +1 -1
  93. package/lib/external-system/test.js +39 -3
  94. package/lib/generator/builders.js +3 -0
  95. package/lib/generator/helpers.js +13 -5
  96. package/lib/generator/split-variables.js +9 -3
  97. package/lib/generator/wizard.js +51 -4
  98. package/lib/infrastructure/helpers-docker-check.js +6 -0
  99. package/lib/infrastructure/helpers.js +1 -1
  100. package/lib/lifecycle/access-control-abac.js +121 -0
  101. package/lib/lifecycle/lifecycle-governance-options.js +4 -1
  102. package/lib/lifecycle/product-model.js +3 -2
  103. package/lib/lifecycle/report-display.js +14 -2
  104. package/lib/programmatic/auth-config.js +3 -2
  105. package/lib/programmatic/bearer-auth.js +1 -1
  106. package/lib/programmatic/capability-registry.js +20 -1
  107. package/lib/programmatic/command-options.js +1 -0
  108. package/lib/programmatic/enterprise-mcp-client.js +18 -2
  109. package/lib/programmatic/index.js +4 -0
  110. package/lib/programmatic/manifest-patch-section.js +48 -9
  111. package/lib/programmatic/manifest-section-registry-data.js +3 -2
  112. package/lib/programmatic/manifest-section-registry.js +18 -1
  113. package/lib/programmatic/openapi-apply-result-schemas.js +44 -0
  114. package/lib/programmatic/openapi-component-schemas.js +16 -1
  115. package/lib/programmatic/openapi-description-enricher.js +3 -1
  116. package/lib/programmatic/openapi-descriptions.js +55 -5
  117. package/lib/programmatic/openapi-evidence-schemas.js +161 -0
  118. package/lib/programmatic/openapi-manifest-schemas.js +6 -2
  119. package/lib/programmatic/openapi-operation-bindings.js +99 -0
  120. package/lib/programmatic/openapi-request-schemas.js +36 -1
  121. package/lib/programmatic/openapi-result-schemas.js +29 -12
  122. package/lib/programmatic/openapi-schema-descriptions-certify.js +5 -1
  123. package/lib/programmatic/openapi-schema-descriptions-core.js +6 -3
  124. package/lib/programmatic/openapi-schema-descriptions-evidence.js +156 -0
  125. package/lib/programmatic/openapi-schema-descriptions-manifest.js +4 -3
  126. package/lib/programmatic/openapi-schema-descriptions-operations.js +70 -0
  127. package/lib/programmatic/openapi-schema-helpers.js +16 -1
  128. package/lib/programmatic/openapi-spec.js +48 -29
  129. package/lib/programmatic/orchestration-openapi-meta.js +5 -0
  130. package/lib/programmatic/request-timeout.js +3 -0
  131. package/lib/programmatic/route-handler-map.js +89 -2
  132. package/lib/programmatic/run-certify.js +15 -2
  133. package/lib/programmatic/run-deploy.js +74 -0
  134. package/lib/programmatic/run-evidence-authoring.js +80 -0
  135. package/lib/programmatic/run-protection.js +124 -0
  136. package/lib/programmatic/run-repository-emcp.js +11 -3
  137. package/lib/programmatic/run-template-workspace.js +42 -2
  138. package/lib/programmatic/workspace-context.js +6 -0
  139. package/lib/protection/auth-context.js +48 -6
  140. package/lib/protection/paths.js +22 -0
  141. package/lib/protection/run-protection-create-helpers.js +22 -1
  142. package/lib/protection/run-protection-create.js +1 -1
  143. package/lib/protection/sync-after-upload.js +1 -1
  144. package/lib/resolvers/evidence-kind-file.js +230 -0
  145. package/lib/resolvers/resource-type-file.js +36 -1
  146. package/lib/resolvers/role-file.js +227 -0
  147. package/lib/role-assistant/evidence-sync.js +450 -0
  148. package/lib/role-assistant/github-workflow.js +96 -0
  149. package/lib/role-assistant/knowledge-sync.js +465 -0
  150. package/lib/role-assistant/settings.js +126 -0
  151. package/lib/role-assistant/test-cases-evaluate.js +170 -0
  152. package/lib/role-assistant/test-cases-honesty.js +184 -0
  153. package/lib/role-assistant/test-cases.js +364 -0
  154. package/lib/role-assistant/test-runner-workhub-answers.js +203 -0
  155. package/lib/role-assistant/test-runner-workhub.js +385 -0
  156. package/lib/role-assistant/test-runner.js +500 -0
  157. package/lib/schema/application-schema.json +36 -2
  158. package/lib/schema/deployment-rules.yaml +1 -0
  159. package/lib/schema/external-datasource.schema.json +121 -4
  160. package/lib/schema/external-system.schema.json +20 -6
  161. package/lib/schema/infra.parameter.yaml +45 -1
  162. package/lib/schema/role-assistant-settings.schema.json +108 -0
  163. package/lib/schema/type/credential-field-registry.json +19 -3
  164. package/lib/utils/api.js +52 -21
  165. package/lib/utils/app-path-resolve.js +72 -24
  166. package/lib/utils/app-register-config.js +4 -3
  167. package/lib/utils/authentication-shape.js +3 -1
  168. package/lib/utils/cli-sync-options.js +59 -3
  169. package/lib/utils/cli-utils.js +11 -3
  170. package/lib/utils/deployment-validation.js +28 -3
  171. package/lib/utils/dev-ca-install.js +5 -6
  172. package/lib/utils/dev-config.js +2 -2
  173. package/lib/utils/dev-list-sort.js +145 -0
  174. package/lib/utils/dev-platform-tls.js +0 -63
  175. package/lib/utils/docker-build.js +46 -27
  176. package/lib/utils/docker-daemon-tls-ca.js +1 -1
  177. package/lib/utils/docker-exec.js +91 -6
  178. package/lib/utils/docker-run-ephemeral.js +55 -0
  179. package/lib/utils/enterprise-knowledge-paths.js +29 -12
  180. package/lib/utils/env-copy-keycloak.js +196 -0
  181. package/lib/utils/env-copy.js +35 -4
  182. package/lib/utils/error-formatters/error-parser.js +0 -3
  183. package/lib/utils/external-env-template.js +1 -1
  184. package/lib/utils/external-system-readiness-compact-blocks.js +28 -1
  185. package/lib/utils/external-system-readiness-compact-display.js +3 -0
  186. package/lib/utils/health-check-request.js +2 -3
  187. package/lib/utils/help-builder.js +4 -0
  188. package/lib/utils/image-name.js +22 -4
  189. package/lib/utils/logger.js +37 -4
  190. package/lib/utils/manifest-source-emit.js +1 -1
  191. package/lib/utils/paths.js +98 -26
  192. package/lib/utils/pipeline-upload-auth-retry.js +19 -3
  193. package/lib/utils/postgres-platform-bootstrap.js +24 -17
  194. package/lib/utils/postgres-wipe.js +5 -2
  195. package/lib/utils/remote-dev-auth.js +39 -4
  196. package/lib/utils/resolve-docker-image-ref.js +36 -6
  197. package/lib/utils/role-assistant-paths.js +98 -0
  198. package/lib/utils/schema-loader.js +41 -0
  199. package/lib/utils/schema-resolver.js +36 -4
  200. package/lib/utils/token-manager.js +47 -22
  201. package/lib/utils/upload-sync-options.js +17 -3
  202. package/lib/utils/url-declarative-resolve-load-doc.js +38 -1
  203. package/lib/validation/external-manifest-validator.js +31 -8
  204. package/lib/validation/openapi-contract-surface-validator.js +4 -0
  205. package/lib/validation/validate-external-file.js +8 -1
  206. package/lib/validation/validator.js +6 -5
  207. package/package.json +35 -12
  208. package/scripts/export-builder-api-evidence.js +3 -3
  209. package/scripts/lib/plan-actor.js +2 -2
  210. package/scripts/lib/plan-meta.js +1 -1
  211. package/scripts/lib/plan-parse.js +1 -1
  212. package/scripts/lib/plan-prevalidate-gates.js +12 -1
  213. package/scripts/lib/plan-run-report.js +13 -2
  214. package/scripts/pnpm/lib/resolve-aifabrix-bin.mjs +10 -10
  215. package/templates/applications/dataplane/application.yaml +47 -17
  216. package/templates/applications/dataplane/env.template +28 -9
  217. package/templates/applications/dataplane/rbac.yaml +78 -2
  218. package/templates/applications/miso-controller/application.yaml +10 -1
  219. package/templates/applications/miso-controller/env.template +13 -8
  220. package/templates/applications/miso-controller/rbac.yaml +72 -0
  221. package/templates/channels/chatgpt/install.template.md.hbs +42 -24
  222. package/templates/channels/chatgpt/integration/README.md +10 -7
  223. package/templates/channels/chatgpt/integration/application.json +2 -2
  224. package/templates/channels/chatgpt/integration/chatgpt-system.json +3 -3
  225. package/templates/channels/chatgpt/integration/env.template +1 -1
  226. package/templates/channels/chatgpt/manifest.template.json.hbs +36 -5
  227. package/templates/channels/microsoft-copilot/ai-plugin.template.json.hbs +28 -0
  228. package/templates/channels/microsoft-copilot/declarative-agent.template.json.hbs +22 -0
  229. package/templates/channels/microsoft-copilot/install.template.md.hbs +32 -22
  230. package/templates/channels/microsoft-copilot/integration/README.md +9 -7
  231. package/templates/channels/microsoft-copilot/integration/application.json +2 -2
  232. package/templates/channels/microsoft-copilot/integration/env.template +1 -1
  233. package/templates/channels/microsoft-copilot/integration/microsoft-copilot-system.json +7 -6
  234. package/templates/channels/microsoft-copilot/manifest.template.json.hbs +34 -6
  235. package/templates/channels/slack/integration/slack-system.json +60 -2
  236. package/templates/python/requirements.txt +1 -1
@@ -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,9 @@ 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: 'role-assistant' },
83
+ { name: 'evidence-kind' },
81
84
  { name: 'enterprise-knowledge' },
82
85
  { name: 'identity' },
83
86
  { name: 'protection' },
@@ -93,6 +96,7 @@ const CATEGORIES = [
93
96
  { name: 'convert', term: 'convert <appKey|systemKey>' },
94
97
  { name: 'show', term: 'show <appKey|systemKey>' },
95
98
  { name: 'validate', term: 'validate [appKey|systemKey|file]' },
99
+ { name: 'validate-json', term: 'validate-json [appKey|systemKey]' },
96
100
  { name: 'parameters', term: 'parameters validate' },
97
101
  { name: 'diff', term: 'diff <file1> <file2>' }
98
102
  ]
@@ -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 {
@@ -382,15 +382,41 @@ function getIntegrationBuilderBaseDir() {
382
382
  }
383
383
 
384
384
  /**
385
- * Returns the default integration root under {@link getAppsMaterializationParent} (listing may also
386
- * scan {@link getCwdIntegrationRoot}; see {@link listIntegrationAppNames}).
385
+ * Returns the default integration root: {@link getCwdIntegrationRoot} when present, else
386
+ * under {@link getAppsMaterializationParent}. Listing may still scan both trees
387
+ * (see {@link listIntegrationAppNames}).
387
388
  *
388
389
  * @returns {string} Absolute path to integration/ directory
389
390
  */
390
391
  function getIntegrationRoot() {
392
+ const cwdRoot = getCwdIntegrationRoot();
393
+ if (cwdRoot) {
394
+ return cwdRoot;
395
+ }
391
396
  return path.join(getAppsMaterializationParent(), 'integration');
392
397
  }
393
398
 
399
+ /**
400
+ * True when path exists and is a directory (best-effort; false on error).
401
+ * @param {string} dirPath
402
+ * @returns {boolean}
403
+ */
404
+ function isExistingDirectoryPath(dirPath) {
405
+ if (!dirPath || typeof dirPath !== 'string') {
406
+ return false;
407
+ }
408
+ try {
409
+ const disk = nodeFs();
410
+ if (!disk.existsSync(dirPath)) {
411
+ return false;
412
+ }
413
+ const st = disk.statSync(dirPath);
414
+ return Boolean(st && typeof st.isDirectory === 'function' && st.isDirectory());
415
+ } catch {
416
+ return false;
417
+ }
418
+ }
419
+
394
420
  /**
395
421
  * Absolute `integration/` next to {@link process.cwd} when that directory exists.
396
422
  *
@@ -537,19 +563,47 @@ function isAppSubdirSync(root, name) {
537
563
  }
538
564
 
539
565
  /**
540
- * Lists app names (directories) under integration root. Excludes dot-prefixed entries.
566
+ * True when an integration/builder app directory has application config (yaml/json or legacy variables.yaml).
567
+ * Skips helper folders such as fixtures/ and wizard-configs/.
568
+ * @param {string} appDir - Absolute path to candidate app directory
569
+ * @returns {boolean}
570
+ */
571
+ function hasApplicationConfigSync(appDir) {
572
+ if (!appDir || typeof appDir !== 'string') return false;
573
+ const disk = nodeFs();
574
+ const names = [
575
+ 'application.yaml',
576
+ 'application.yml',
577
+ 'application.json',
578
+ 'variables.yaml'
579
+ ];
580
+ return names.some((name) => {
581
+ try {
582
+ return disk.existsSync(path.join(appDir, name));
583
+ } catch {
584
+ return false;
585
+ }
586
+ });
587
+ }
588
+
589
+ /**
590
+ * Lists app names (directories) under integration root. Excludes dot-prefixed entries
591
+ * and directories without application config (e.g. fixtures/, wizard-configs/).
541
592
  * Returns [] if root does not exist.
542
593
  * @returns {string[]} Sorted list of app directory names
543
594
  */
544
595
  function listIntegrationAppNames() {
545
596
  const disk = nodeFs();
546
597
  const names = new Set();
598
+ const withAppConfig = (root) => (name) => hasApplicationConfigSync(path.join(root, name));
547
599
  const cwdRoot = getCwdIntegrationRoot();
548
600
  if (cwdRoot) {
549
- addBuilderSubdirNamesToSet(disk, cwdRoot, names, null);
601
+ addBuilderSubdirNamesToSet(disk, cwdRoot, names, withAppConfig(cwdRoot));
602
+ }
603
+ const matInt = path.join(getAppsMaterializationParent(), 'integration');
604
+ if (!cwdRoot || path.resolve(matInt) !== path.resolve(cwdRoot)) {
605
+ addBuilderSubdirNamesToSet(disk, matInt, names, withAppConfig(matInt));
550
606
  }
551
- const matInt = getIntegrationRoot();
552
- addBuilderSubdirNamesToSet(disk, matInt, names, null);
553
607
  return [...names].sort();
554
608
  }
555
609
 
@@ -595,14 +649,19 @@ function listBuilderAppNames() {
595
649
  }
596
650
  roots.add(path.resolve(getSystemBuilderRoot()));
597
651
  for (const r of roots) {
598
- addBuilderSubdirNamesToSet(disk, r, names, null);
652
+ addBuilderSubdirNamesToSet(disk, r, names, (name) =>
653
+ hasApplicationConfigSync(path.join(r, name))
654
+ );
599
655
  }
600
656
  return [...names].sort();
601
657
  }
602
658
 
603
659
  /**
604
- * Gets the integration folder path: **`cwd/integration/<appName>`** when that directory exists, else
605
- * **`aifabrix-work` or `aifabrix-home` + `/integration/<appName>`**.
660
+ * Gets the integration folder path.
661
+ * Resolution: existing `cwd/integration/<app>` → existing materialization copy →
662
+ * `cwd/integration/<app>` when `cwd/integration/` exists (new download/create) →
663
+ * materialization parent. Download should use {@link getAppCreatePath} when it must
664
+ * always land under cwd even if a materialization orphan exists.
606
665
  *
607
666
  * @param {string} appName - Application name
608
667
  * @returns {string} Absolute path to integration directory
@@ -620,18 +679,21 @@ function getIntegrationPath(appName) {
620
679
  } catch {
621
680
  // programmatic module optional for CLI-only usage
622
681
  }
623
- const cwdInt = path.join(path.resolve(process.cwd()), 'integration', appName);
624
- try {
625
- if (nodeFs().existsSync(cwdInt)) {
626
- const st = nodeFs().statSync(cwdInt);
627
- if (st && typeof st.isDirectory === 'function' && st.isDirectory()) {
628
- return cwdInt;
629
- }
682
+ const cwdRoot = getCwdIntegrationRoot();
683
+ const matApp = path.join(getAppsMaterializationParent(), 'integration', appName);
684
+ if (cwdRoot) {
685
+ const cwdApp = path.join(cwdRoot, appName);
686
+ if (isExistingDirectoryPath(cwdApp)) {
687
+ return cwdApp;
630
688
  }
631
- } catch {
632
- // ignore
633
689
  }
634
- return path.join(getAppsMaterializationParent(), 'integration', appName);
690
+ if (isExistingDirectoryPath(matApp)) {
691
+ return matApp;
692
+ }
693
+ if (cwdRoot) {
694
+ return path.join(cwdRoot, appName);
695
+ }
696
+ return matApp;
635
697
  }
636
698
 
637
699
  /**
@@ -769,13 +831,15 @@ function resolveExistingWorkspacePath(appName, options) {
769
831
  if (isExistingAppDirectory(appPath)) {
770
832
  return { appPath, baseDir };
771
833
  }
772
- const altWorkspace =
773
- baseDir === 'enterprise-knowledge' ? 'integration' : 'enterprise-knowledge';
774
- const alt = resolveAppWorkspace(appName, { ...options, workspace: altWorkspace });
775
- if (!isExistingAppDirectory(alt.appPath)) {
776
- return null;
834
+ const alternates = ['integration', 'enterprise-knowledge', 'role-assistant']
835
+ .filter((workspaceName) => workspaceName !== baseDir);
836
+ for (const workspaceName of alternates) {
837
+ const alt = resolveAppWorkspace(appName, { ...options, workspace: workspaceName });
838
+ if (isExistingAppDirectory(alt.appPath)) {
839
+ return { appPath: alt.appPath, baseDir: alt.baseDir };
840
+ }
777
841
  }
778
- return { appPath: alt.appPath, baseDir: alt.baseDir };
842
+ return null;
779
843
  }
780
844
 
781
845
  function externalWorkspaceFolderFromConfig(appPath, baseDir, scopedWorkspace) {
@@ -898,7 +962,7 @@ async function detectAppType(appName, options = {}) {
898
962
  const builderAfterTemplate = checkBuilderFolder(appName);
899
963
  if (builderAfterTemplate) return builderAfterTemplate;
900
964
  throw new Error(
901
- `App '${appName}' not found in integration/${appName}, enterprise-knowledge/${appName}, or builder/${appName}`
965
+ `App '${appName}' not found in integration/${appName}, enterprise-knowledge/${appName}, role-assistant/${appName}, or builder/${appName}`
902
966
  );
903
967
  }
904
968
 
@@ -936,6 +1000,11 @@ async function getResolveAppPath(appName, options = {}) {
936
1000
  if (workspaceHit) {
937
1001
  return workspaceHit;
938
1002
  }
1003
+ const { getRoleAssistantPath } = require('./role-assistant-paths');
1004
+ const roleAssistantHit = tryResolveEnvOnlyPath(getRoleAssistantPath(appName));
1005
+ if (roleAssistantHit) {
1006
+ return roleAssistantHit;
1007
+ }
939
1008
  const integrationHit = tryResolveEnvOnlyPath(getIntegrationPath(appName));
940
1009
  if (integrationHit) {
941
1010
  return integrationHit;
@@ -1045,6 +1114,9 @@ module.exports = {
1045
1114
  getEnterpriseKnowledgePath: require('./enterprise-knowledge-paths').getEnterpriseKnowledgePath,
1046
1115
  getEnterpriseKnowledgeRoot: require('./enterprise-knowledge-paths').getEnterpriseKnowledgeRoot,
1047
1116
  listEnterpriseKnowledgeAnchors: require('./enterprise-knowledge-paths').listEnterpriseKnowledgeAnchors,
1117
+ getRoleAssistantPath: require('./role-assistant-paths').getRoleAssistantPath,
1118
+ getRoleAssistantRoot: require('./role-assistant-paths').getRoleAssistantRoot,
1119
+ getCwdRoleAssistantRoot: require('./role-assistant-paths').getCwdRoleAssistantRoot,
1048
1120
  resolveAppWorkspace: require('./app-path-resolve').resolveAppWorkspace,
1049
1121
  resolveExternalSystemPath: require('./app-path-resolve').resolveExternalSystemPath
1050
1122
  };
@@ -57,13 +57,29 @@ async function resolveClientTokenAuthForPipelineRetry(systemKey) {
57
57
  * @param {Object} authConfig
58
58
  * @param {Object} payload
59
59
  * @param {string} systemKey - External system key for credential lookup on retry
60
+ * @param {Object} [options] - Forwarded to uploadApplicationViaPipeline (e.g. timeoutMs)
60
61
  * @returns {Promise<Object>}
61
62
  */
62
- async function uploadApplicationViaPipelineWithAuthRetry(dataplaneUrl, authConfig, payload, systemKey) {
63
- const first = await uploadApplicationViaPipeline(dataplaneUrl, authConfig, payload);
63
+ async function uploadApplicationViaPipelineWithAuthRetry(
64
+ dataplaneUrl,
65
+ authConfig,
66
+ payload,
67
+ systemKey,
68
+ options = {}
69
+ ) {
70
+ const first = await uploadApplicationViaPipeline(
71
+ dataplaneUrl,
72
+ authConfig,
73
+ payload,
74
+ options
75
+ );
64
76
  if (!isPipelineAuthFailure(first) || authConfig.type !== 'bearer') {
65
77
  return first;
66
78
  }
79
+ // Token-forward: do not swap the caller JWT for application client-token.
80
+ if (authConfig.preserveBearer) {
81
+ return first;
82
+ }
67
83
  const clientAuth = await resolveClientTokenAuthForPipelineRetry(systemKey);
68
84
  if (!clientAuth || clientAuth.type === 'bearer') {
69
85
  return first;
@@ -73,7 +89,7 @@ async function uploadApplicationViaPipelineWithAuthRetry(dataplaneUrl, authConfi
73
89
  'Device login token was rejected for pipeline publish; retrying with application client token…'
74
90
  )
75
91
  );
76
- return uploadApplicationViaPipeline(dataplaneUrl, clientAuth, payload);
92
+ return uploadApplicationViaPipeline(dataplaneUrl, clientAuth, payload, options);
77
93
  }
78
94
 
79
95
  module.exports = {
@@ -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
  }
@@ -24,6 +24,17 @@ function expandConfiguredSecretsTilde(filePath) {
24
24
  return filePath;
25
25
  }
26
26
 
27
+ /** One auto-renew attempt per CLI process (shared by concurrent getRemoteDevAuth callers). */
28
+ let autoRenewPromise = null;
29
+
30
+ /**
31
+ * Reset process-level auto-renew gate (tests only).
32
+ * @returns {void}
33
+ */
34
+ function resetDevCertAutoRenewGateForTests() {
35
+ autoRenewPromise = null;
36
+ }
37
+
27
38
  /**
28
39
  * Single API object so resolveSharedSecretsEndpoint and callers share one getRemoteDevAuth
29
40
  * (Jest spies and partial mocks work reliably).
@@ -39,12 +50,11 @@ const remoteDevAuth = {
39
50
  },
40
51
 
41
52
  /**
42
- * Get Builder Server URL, client cert PEM, and optional dev root CA when remote is configured; otherwise null.
43
- * serverCaPem comes from ca.pem (saved at dev init); required for Node TLS to private-CA servers.
44
- * Use for cert-authenticated dev API calls (settings, users, ssh-keys, secrets).
53
+ * Read remote server URL + client cert from disk without auto-renew.
54
+ * Use when renewing (avoids recursion) or when only a local presence check is needed.
45
55
  * @returns {Promise<{ serverUrl: string, clientCertPem: string, serverCaPem: string|null }|null>}
46
56
  */
47
- async getRemoteDevAuth() {
57
+ async readRemoteDevAuth() {
48
58
  const serverUrl = await config.getRemoteServer();
49
59
  if (!serverUrl) return null;
50
60
  const devId = await config.getDeveloperId();
@@ -55,6 +65,30 @@ const remoteDevAuth = {
55
65
  return { serverUrl, clientCertPem, serverCaPem };
56
66
  },
57
67
 
68
+ /**
69
+ * Get Builder Server URL, client cert PEM, and optional dev root CA when remote is configured; otherwise null.
70
+ * When the client cert is still valid but expires within 14 days, renews it once per process before returning.
71
+ * serverCaPem comes from ca.pem (saved at dev init); required for Node TLS to private-CA servers.
72
+ * Use for cert-authenticated dev API calls (settings, users, ssh-keys, secrets).
73
+ * @returns {Promise<{ serverUrl: string, clientCertPem: string, serverCaPem: string|null }|null>}
74
+ */
75
+ async getRemoteDevAuth() {
76
+ const existing = await remoteDevAuth.readRemoteDevAuth();
77
+ if (!existing) return null;
78
+ if (!autoRenewPromise) {
79
+ autoRenewPromise = (async() => {
80
+ const { ensureDevCertFreshIfDue } = require('../commands/dev-refresh');
81
+ return ensureDevCertFreshIfDue({ quiet: false });
82
+ })();
83
+ }
84
+ try {
85
+ await autoRenewPromise;
86
+ } catch {
87
+ // ensureDevCertFreshIfDue already handles failures; keep using current cert
88
+ }
89
+ return remoteDevAuth.readRemoteDevAuth();
90
+ },
91
+
58
92
  /**
59
93
  * Resolve where shared secrets read/write should go. After dev init, config may still hold a
60
94
  * server-side filesystem path from GET /api/dev/settings (not an http URL) if merge did not rewrite it.
@@ -148,3 +182,4 @@ function normalizeSharedSecretsFilePath(configured) {
148
182
  }
149
183
 
150
184
  module.exports = remoteDevAuth;
185
+ module.exports.resetDevCertAutoRenewGateForTests = resetDevCertAutoRenewGateForTests;
@@ -12,6 +12,32 @@
12
12
  'use strict';
13
13
 
14
14
  const { parseImageOverride } = require('./parse-image-ref');
15
+ const DOCKER_REPOSITORY_RE = /^[a-z0-9][a-z0-9._/-]*$/i;
16
+ const DOCKER_TAG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
17
+
18
+ /**
19
+ * @param {string} repository
20
+ * @returns {string}
21
+ */
22
+ function assertSafeRepository(repository) {
23
+ const trimmed = String(repository || '').trim();
24
+ if (!trimmed || !DOCKER_REPOSITORY_RE.test(trimmed)) {
25
+ throw new Error(`Invalid Docker repository path: ${repository}`);
26
+ }
27
+ return trimmed;
28
+ }
29
+
30
+ /**
31
+ * @param {string} tag
32
+ * @returns {string}
33
+ */
34
+ function assertSafeTag(tag) {
35
+ const trimmed = String(tag || '').trim();
36
+ if (!trimmed || !DOCKER_TAG_RE.test(trimmed)) {
37
+ throw new Error(`Invalid Docker image tag: ${tag}`);
38
+ }
39
+ return trimmed;
40
+ }
15
41
 
16
42
  /**
17
43
  * Repository path without host (same rules as compose-generator getImageName).
@@ -59,7 +85,11 @@ function normalizeDockerRegistryPrefix(registry) {
59
85
  if (!t) {
60
86
  return '';
61
87
  }
62
- return t.replace(/\/+$/, '');
88
+ let end = t.length;
89
+ while (end > 0 && t[end - 1] === '/') {
90
+ end -= 1;
91
+ }
92
+ return t.slice(0, end);
63
93
  }
64
94
 
65
95
  /**
@@ -81,18 +111,18 @@ function resolveDockerImageRef(appName, appConfig, runOptions = {}) {
81
111
  if (opts.image) {
82
112
  const parsed = parseImageOverride(opts.image);
83
113
  return {
84
- imageName: parsed ? parsed.name : getRepositoryPathFromConfig(appConfig, appName),
85
- imageTag: parsed ? parsed.tag : tagFromOpts || imageTagFromConfig(appConfig)
114
+ imageName: assertSafeRepository(parsed ? parsed.name : getRepositoryPathFromConfig(appConfig, appName)),
115
+ imageTag: assertSafeTag(parsed ? parsed.tag : tagFromOpts || imageTagFromConfig(appConfig))
86
116
  };
87
117
  }
88
118
 
89
- const baseRepo = getRepositoryPathFromConfig(appConfig, appName);
90
- const imageTag = tagFromOpts || imageTagFromConfig(appConfig);
119
+ const baseRepo = assertSafeRepository(getRepositoryPathFromConfig(appConfig, appName));
120
+ const imageTag = assertSafeTag(tagFromOpts || imageTagFromConfig(appConfig));
91
121
  const prefix =
92
122
  normalizeDockerRegistryPrefix(opts.registry) ||
93
123
  normalizeDockerRegistryPrefix(appConfig?.image?.registry ?? '');
94
124
  if (prefix) {
95
- return { imageName: `${prefix}/${baseRepo}`, imageTag };
125
+ return { imageName: assertSafeRepository(`${prefix}/${baseRepo}`), imageTag };
96
126
  }
97
127
  return { imageName: baseRepo, imageTag };
98
128
  }