@aifabrix/builder 2.52.0 → 2.52.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 (134) hide show
  1. package/jest.isolated-projects.js +25 -2
  2. package/jest.projects.js +4 -1
  3. package/lib/api/datasources-extended.api.js +8 -8
  4. package/lib/api/keycloak.api.js +92 -0
  5. package/lib/api/token-providers.api.js +77 -0
  6. package/lib/api/types/keycloak.types.js +23 -0
  7. package/lib/api/types/token-providers.types.js +65 -0
  8. package/lib/app/down.js +16 -1
  9. package/lib/channels/add-channel.js +126 -19
  10. package/lib/channels/approval-guides/chatgpt.js +1 -1
  11. package/lib/channels/approval-guides/microsoft-copilot.js +16 -12
  12. package/lib/channels/channel-add-ensure.js +128 -0
  13. package/lib/channels/channel-artifact-generator.js +11 -1
  14. package/lib/channels/channel-branding.js +94 -0
  15. package/lib/channels/channel-credential.js +8 -0
  16. package/lib/channels/channel-discovery.js +330 -0
  17. package/lib/channels/channel-external-system.js +1 -1
  18. package/lib/channels/channel-runtime-resolver.js +103 -14
  19. package/lib/channels/channel-status-display.js +7 -1
  20. package/lib/channels/microsoft-copilot-channel-add.js +138 -0
  21. package/lib/channels/microsoft-copilot-entra-sso-form.js +180 -0
  22. package/lib/channels/microsoft-copilot-entra-sso.js +264 -0
  23. package/lib/channels/microsoft-copilot-icons.js +74 -0
  24. package/lib/channels/microsoft-copilot-limits.js +84 -0
  25. package/lib/channels/microsoft-copilot-package.js +130 -49
  26. package/lib/channels/microsoft-copilot-validate.js +197 -0
  27. package/lib/cli/index.js +2 -0
  28. package/lib/cli/infra-guided.js +13 -6
  29. package/lib/cli/setup-infra.js +31 -4
  30. package/lib/commands/channel-add-setup.js +28 -9
  31. package/lib/commands/channel.js +14 -10
  32. package/lib/commands/integration-definition.js +158 -0
  33. package/lib/commands/repair-auth-apply.js +19 -5
  34. package/lib/commands/repair-datasource-sync-operations.js +4 -1
  35. package/lib/commands/role-assistant-test-output.js +200 -0
  36. package/lib/commands/role-assistant.js +125 -166
  37. package/lib/datasource/capability/capability-resolve.js +15 -5
  38. package/lib/datasource/capability/copy-operations.js +14 -4
  39. package/lib/infrastructure/index.js +19 -4
  40. package/lib/integration-definition/apply.js +478 -0
  41. package/lib/integration-definition/canonical-json.js +51 -0
  42. package/lib/integration-definition/emit-suite-bid.js +94 -0
  43. package/lib/integration-definition/enrich-crm-field-mappings.js +240 -0
  44. package/lib/integration-definition/enrich-openapi-operations.js +434 -0
  45. package/lib/integration-definition/from-discovery.js +339 -0
  46. package/lib/integration-definition/hash.js +37 -0
  47. package/lib/integration-definition/index.js +37 -0
  48. package/lib/integration-definition/openapi-enrichment/hubspot-crm-companies.json +205 -0
  49. package/lib/integration-definition/resource-type-catalog.js +59 -0
  50. package/lib/integration-definition/system-kind-enum.js +40 -0
  51. package/lib/integration-definition/validate.js +148 -0
  52. package/lib/programmatic/builder-api-request.js +5 -1
  53. package/lib/programmatic/builder-help-governance.js +3 -2
  54. package/lib/programmatic/capability-registry.js +19 -0
  55. package/lib/programmatic/openapi-component-schemas.js +6 -1
  56. package/lib/programmatic/openapi-descriptions.js +40 -0
  57. package/lib/programmatic/openapi-envelope-examples.js +269 -0
  58. package/lib/programmatic/openapi-operation-bindings.js +45 -0
  59. package/lib/programmatic/openapi-request-schemas.js +14 -0
  60. package/lib/programmatic/openapi-result-schemas-overflow.js +101 -0
  61. package/lib/programmatic/openapi-result-schemas.js +46 -99
  62. package/lib/programmatic/openapi-schema-descriptions-core.js +7 -4
  63. package/lib/programmatic/openapi-schema-descriptions-help.js +1 -1
  64. package/lib/programmatic/openapi-schema-descriptions-operations.js +24 -0
  65. package/lib/programmatic/openapi-schema-helpers.js +9 -9
  66. package/lib/programmatic/openapi-spec.js +11 -1
  67. package/lib/programmatic/orchestration-openapi-meta.js +10 -0
  68. package/lib/programmatic/route-handler-map.js +29 -1
  69. package/lib/programmatic/run-channel-package.js +236 -0
  70. package/lib/programmatic/run-integration-definition.js +69 -0
  71. package/lib/role-assistant/knowledge-sync-logging.js +58 -0
  72. package/lib/role-assistant/knowledge-sync.js +88 -93
  73. package/lib/role-assistant/test-cases-bid.js +155 -0
  74. package/lib/role-assistant/test-cases-evaluate.js +9 -1
  75. package/lib/role-assistant/test-cases-honesty.js +24 -10
  76. package/lib/role-assistant/test-cases.js +28 -0
  77. package/lib/role-assistant/test-runner-bid-apply.js +122 -0
  78. package/lib/role-assistant/test-runner-bid-document.js +278 -0
  79. package/lib/role-assistant/test-runner-workhub-answers.js +245 -20
  80. package/lib/role-assistant/test-runner-workhub.js +100 -18
  81. package/lib/role-assistant/test-suite.js +119 -0
  82. package/lib/schema/application-schema.json +56 -6
  83. package/lib/schema/external-datasource.schema.json +103 -17
  84. package/lib/schema/external-system.schema.json +54 -3
  85. package/lib/schema/integration-definition.schema.json +195 -0
  86. package/lib/utils/authentication-shape.js +6 -0
  87. package/lib/utils/docker-build.js +3 -3
  88. package/lib/utils/help-builder.js +1 -0
  89. package/lib/utils/promote-dev-docker-image.js +97 -18
  90. package/lib/utils/transient-developer-id.js +28 -0
  91. package/package.json +1 -1
  92. package/scripts/pnpm/af.mjs +24 -9
  93. package/scripts/pnpm/help.mjs +1 -1
  94. package/templates/applications/dataplane/application.yaml +3 -2
  95. package/templates/applications/dataplane/env.template +7 -6
  96. package/templates/applications/miso-controller/application.yaml +23 -9
  97. package/templates/applications/miso-controller/env.template +8 -2
  98. package/templates/channels/README.md +11 -1
  99. package/templates/channels/brand/color-192.png +0 -0
  100. package/templates/channels/brand/dark/128.png +0 -0
  101. package/templates/channels/brand/dark/192.png +0 -0
  102. package/templates/channels/brand/dark/256.png +0 -0
  103. package/templates/channels/brand/dark/32.png +0 -0
  104. package/templates/channels/brand/dark/512.png +0 -0
  105. package/templates/channels/brand/dark/64.png +0 -0
  106. package/templates/channels/brand/light/128.png +0 -0
  107. package/templates/channels/brand/light/192.png +0 -0
  108. package/templates/channels/brand/light/256.png +0 -0
  109. package/templates/channels/brand/light/32.png +0 -0
  110. package/templates/channels/brand/light/512.png +0 -0
  111. package/templates/channels/brand/light/64.png +0 -0
  112. package/templates/channels/brand/mono-white/128.png +0 -0
  113. package/templates/channels/brand/mono-white/192.png +0 -0
  114. package/templates/channels/brand/mono-white/256.png +0 -0
  115. package/templates/channels/brand/mono-white/32.png +0 -0
  116. package/templates/channels/brand/mono-white/512.png +0 -0
  117. package/templates/channels/brand/mono-white/64.png +0 -0
  118. package/templates/channels/brand/outline-32.png +0 -0
  119. package/templates/channels/chatgpt/install.template.md.hbs +6 -4
  120. package/templates/channels/chatgpt/integration/README.md +4 -5
  121. package/templates/channels/chatgpt/integration/chatgpt-system.json +1 -16
  122. package/templates/channels/microsoft-copilot/ai-plugin.template.json.hbs +10 -16
  123. package/templates/channels/microsoft-copilot/declarative-agent.template.json.hbs +5 -3
  124. package/templates/channels/microsoft-copilot/entra-sso-registration.template.md.hbs +27 -0
  125. package/templates/channels/microsoft-copilot/install.template.md.hbs +39 -20
  126. package/templates/channels/microsoft-copilot/integration/README.md +2 -3
  127. package/templates/channels/microsoft-copilot/integration/microsoft-copilot-system.json +1 -22
  128. package/templates/channels/microsoft-copilot/manifest.template.json.hbs +7 -7
  129. package/templates/channels/microsoft-copilot/oauth-plugin-vault-required.template.md.hbs +11 -0
  130. package/templates/channels/slack/integration/README.md +2 -4
  131. package/templates/channels/teams-bot/install.template.md.hbs +4 -0
  132. package/templates/channels/teams-bot/integration/README.md +3 -5
  133. package/templates/channels/web-embed/integration/web-embed-system.json +1 -16
  134. /package/.cursor/plans/{Done → Archive}/157.1-security-quality-violations-by-type.csv +0 -0
@@ -1,14 +1,16 @@
1
1
  /**
2
- * Microsoft Copilot channel admin approval steps (plan 152).
2
+ * Microsoft Copilot channel install / approval steps.
3
3
  *
4
- * @fileoverview Copilot Studio / M365 admin connector guide
4
+ * @fileoverview Teams custom-app upload guide aligned with install templates
5
5
  * @author AI Fabrix Team
6
6
  * @version 1.0.0
7
7
  */
8
8
 
9
9
  'use strict';
10
10
 
11
- const { formatCredentialPortalStep } = require('./credential-portal');
11
+ const {
12
+ ENTRA_SSO_REGISTRATION_URL
13
+ } = require('../microsoft-copilot-entra-sso-form');
12
14
 
13
15
  /**
14
16
  * @param {Object} ctx
@@ -16,16 +18,18 @@ const { formatCredentialPortalStep } = require('./credential-portal');
16
18
  */
17
19
  function buildMicrosoftCopilotApprovalGuide(ctx) {
18
20
  return {
19
- portalUrl: 'https://admin.cloud.microsoft.com/',
21
+ portalUrl: ENTRA_SSO_REGISTRATION_URL,
20
22
  steps: [
21
- 'Sign in to Microsoft 365 admin (https://admin.cloud.microsoft.com/) or Copilot Studio as a tenant admin.',
22
- 'Upload or sideload the generated appPackage.<env>.zip package artifact.',
23
- 'Review declarative agent + Remote MCP plugin details and approve tenant distribution.',
24
- 'Configure Entra/OAuth consent and user assignment for target groups.',
25
- formatCredentialPortalStep(ctx, 'Copilot channel credential references (no secrets in package files)'),
26
- 'Verify Runtime MCP endpoint and Layer B tool visibility using INSTALL.md guidance.',
27
- `After Copilot shows the package as available, re-run: ${ctx.resumeCommand}`,
28
- 'Optional: use --probe for AI Fabrix-side checks; tenant upload/approval remains Microsoft-admin responsibility.'
23
+ 'Open Microsoft Teams with your target Microsoft 365 account.',
24
+ 'Go to Apps → Manage your apps → Upload an app → Upload a custom app.',
25
+ 'Upload only appPackage.<env>.zip (do not upload individual manifest files or channel-microsoft-copilot.json).',
26
+ 'Review permissions, select Add, and open the installed app. Users stay on the Microsoft 365 Entra session — no separate AI Fabrix sign-in.',
27
+ 'If prompted, complete Entra admin consent for the Runtime MCP delegated scope.',
28
+ 'If Upload a custom app is unavailable, ask a Teams admin to enable custom app uploads or deploy the package centrally.',
29
+ `Register Entra SSO first at ${ENTRA_SSO_REGISTRATION_URL} using the Base URL / Client ID / Scope values printed by the CLI (or INSTALL.md). Pass the registration id as --oauth-plugin-vault-reference-id.`,
30
+ 'Do not configure AI Fabrix OAuth client secrets under Connected Systems → Authentication for this Entra SSO flow.',
31
+ 'Confirm Runtime MCP and governed tools using INSTALL.md, then re-run with --probe after Teams install.',
32
+ `After the app is available in Teams, re-run: ${ctx.resumeCommand}`
29
33
  ]
30
34
  };
31
35
  }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Ensure local scaffold + dataplane upload before channel add install.
3
+ *
4
+ * @fileoverview One-command channel add prelude (scaffold → upload if needed)
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const chalk = require('chalk');
14
+ const logger = require('../utils/logger');
15
+ const { formatSuccessLine, metadata } = require('../utils/cli-test-layout-chalk');
16
+ const { getIntegrationPath } = require('../utils/paths');
17
+ const { getExternalSystem } = require('../api/external-systems.api');
18
+ const { unwrapApiData } = require('../utils/external-system-readiness-core');
19
+ const { scaffoldChannelIntegration } = require('./channel-integration-generator');
20
+ const { resolveChannelAuthContext } = require('./channel-auth-context');
21
+ const { uploadExternalSystem } = require('../commands/upload');
22
+
23
+ /**
24
+ * @param {string} channelTarget
25
+ * @param {string} [projectRoot]
26
+ * @returns {string}
27
+ */
28
+ function resolveIntegrationDir(channelTarget, projectRoot) {
29
+ if (projectRoot) {
30
+ return path.join(path.resolve(projectRoot), 'integration', channelTarget);
31
+ }
32
+ return getIntegrationPath(channelTarget);
33
+ }
34
+
35
+ /**
36
+ * @param {Object} ctx
37
+ * @param {string} channelTarget
38
+ * @returns {Promise<boolean>}
39
+ */
40
+ async function isCommunicationSystemUploaded(ctx, channelTarget) {
41
+ const res = await getExternalSystem(ctx.dataplaneUrl, channelTarget, ctx.authConfig);
42
+ const system = unwrapApiData(res);
43
+ return !!(system && typeof system === 'object');
44
+ }
45
+
46
+ /**
47
+ * Scaffold integration/<target>/ when missing (does not overwrite).
48
+ *
49
+ * @param {string} channelTarget
50
+ * @param {Object} options
51
+ * @returns {Promise<{ scaffolded: boolean, integrationDir: string }>}
52
+ */
53
+ async function ensureChannelScaffold(channelTarget, options = {}) {
54
+ const integrationDir = resolveIntegrationDir(channelTarget, options.projectRoot);
55
+ if (fs.existsSync(integrationDir)) {
56
+ return { scaffolded: false, integrationDir };
57
+ }
58
+ const result = await scaffoldChannelIntegration(channelTarget, {
59
+ projectRoot: options.projectRoot,
60
+ allowExperimental: options.allowExperimental === true
61
+ });
62
+ if (options.quiet !== true) {
63
+ logger.log(formatSuccessLine(`Scaffolded integration at ${result.integrationDir}`));
64
+ for (const file of result.files) {
65
+ logger.log(metadata(file));
66
+ }
67
+ }
68
+ return { scaffolded: true, integrationDir: result.integrationDir };
69
+ }
70
+
71
+ /**
72
+ * Upload when missing on dataplane; with --force upload every time.
73
+ *
74
+ * @param {string} channelTarget
75
+ * @param {Object} options
76
+ * @returns {Promise<{ uploaded: boolean, skipped: boolean, ctx: Object|null }>}
77
+ */
78
+ async function ensureChannelUploaded(channelTarget, options = {}) {
79
+ if (options.dryRun === true) {
80
+ return { uploaded: false, skipped: true, ctx: null };
81
+ }
82
+ const ctx = await resolveChannelAuthContext(channelTarget, { silent: options.quiet === true });
83
+ const force = options.force === true;
84
+ const alreadyUploaded = force ? false : await isCommunicationSystemUploaded(ctx, channelTarget);
85
+ if (alreadyUploaded) {
86
+ if (options.quiet !== true) {
87
+ logger.log(chalk.gray(` Communication system "${channelTarget}" already on dataplane`));
88
+ }
89
+ return { uploaded: false, skipped: true, ctx };
90
+ }
91
+ if (options.quiet !== true) {
92
+ const reason = force ? 'force upload' : 'not found on dataplane';
93
+ logger.log(chalk.white(`Uploading ${channelTarget} (${reason})…`));
94
+ }
95
+ await uploadExternalSystem(channelTarget, {
96
+ force: true,
97
+ minimal: options.quiet === true || options.minimal !== false
98
+ });
99
+ if (options.quiet !== true) {
100
+ logger.log(formatSuccessLine(`Uploaded ${channelTarget}`));
101
+ }
102
+ return { uploaded: true, skipped: false, ctx };
103
+ }
104
+
105
+ /**
106
+ * Scaffold if needed, then upload if missing (or always when --force).
107
+ *
108
+ * @param {string} channelTarget
109
+ * @param {Object} options
110
+ * @returns {Promise<{ scaffolded: boolean, uploaded: boolean, integrationDir: string }>}
111
+ */
112
+ async function ensureChannelReadyForAdd(channelTarget, options = {}) {
113
+ const scaffold = await ensureChannelScaffold(channelTarget, options);
114
+ const upload = await ensureChannelUploaded(channelTarget, options);
115
+ return {
116
+ scaffolded: scaffold.scaffolded,
117
+ uploaded: upload.uploaded,
118
+ integrationDir: scaffold.integrationDir
119
+ };
120
+ }
121
+
122
+ module.exports = {
123
+ resolveIntegrationDir,
124
+ isCommunicationSystemUploaded,
125
+ ensureChannelScaffold,
126
+ ensureChannelUploaded,
127
+ ensureChannelReadyForAdd
128
+ };
@@ -147,11 +147,21 @@ function generateChannelArtifact(params) {
147
147
  params.dryRun === true
148
148
  );
149
149
 
150
+ const installReady = params.runtimeValues?.installReady === true;
151
+ const blocker = params.runtimeValues?.discoveryBlocker || null;
150
152
  return {
151
153
  outputPath,
152
154
  artifactHash,
153
155
  content,
154
- installPath: install?.installPath
156
+ installPath: install?.installPath,
157
+ zipPath: null,
158
+ links: {
159
+ readme: install?.installPath || null,
160
+ zip: null,
161
+ filesRoot: outputDir
162
+ },
163
+ installReady,
164
+ blocker
155
165
  };
156
166
  }
157
167
 
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Optional channel package branding in Builder workspace (plan 167.0).
3
+ *
4
+ * @fileoverview Read/write channel-branding.json under integration/<systemKey>/
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('../internal/fs-real-sync');
12
+ const path = require('path');
13
+
14
+ const BRANDING_FILENAME = 'channel-branding.json';
15
+
16
+ /**
17
+ * @param {string} workspaceSystemDir
18
+ * @returns {string}
19
+ */
20
+ function brandingFilePath(workspaceSystemDir) {
21
+ return path.join(workspaceSystemDir, BRANDING_FILENAME);
22
+ }
23
+
24
+ /**
25
+ * @param {string} workspaceSystemDir
26
+ * @returns {{ displayName?: string, description?: string, iconColorPath?: string, iconOutlinePath?: string }}
27
+ */
28
+ function readChannelBranding(workspaceSystemDir) {
29
+ const filePath = brandingFilePath(workspaceSystemDir);
30
+ if (!fs.existsSync(filePath)) {
31
+ return {};
32
+ }
33
+ const raw = JSON.parse(fs.readFileSync(filePath, 'utf8'));
34
+ if (!raw || typeof raw !== 'object') {
35
+ return {};
36
+ }
37
+ /** @type {{ displayName?: string, description?: string, iconColorPath?: string, iconOutlinePath?: string }} */
38
+ const out = {};
39
+ if (typeof raw.displayName === 'string' && raw.displayName.trim()) {
40
+ out.displayName = raw.displayName.trim();
41
+ }
42
+ if (typeof raw.description === 'string' && raw.description.trim()) {
43
+ out.description = raw.description.trim();
44
+ }
45
+ if (typeof raw.iconColorPath === 'string' && raw.iconColorPath.trim()) {
46
+ out.iconColorPath = raw.iconColorPath.trim();
47
+ }
48
+ if (typeof raw.iconOutlinePath === 'string' && raw.iconOutlinePath.trim()) {
49
+ out.iconOutlinePath = raw.iconOutlinePath.trim();
50
+ }
51
+ return out;
52
+ }
53
+
54
+ /**
55
+ * @param {string} workspaceSystemDir
56
+ * @param {Object} branding
57
+ * @returns {Object}
58
+ */
59
+ function writeChannelBranding(workspaceSystemDir, branding) {
60
+ fs.mkdirSync(workspaceSystemDir, { recursive: true });
61
+ const next = {
62
+ ...readChannelBranding(workspaceSystemDir),
63
+ ...(branding && typeof branding === 'object' ? branding : {})
64
+ };
65
+ const cleaned = {};
66
+ for (const key of ['displayName', 'description', 'iconColorPath', 'iconOutlinePath']) {
67
+ if (typeof next[key] === 'string' && next[key].trim()) {
68
+ cleaned[key] = next[key].trim();
69
+ }
70
+ }
71
+ fs.writeFileSync(brandingFilePath(workspaceSystemDir), `${JSON.stringify(cleaned, null, 2)}\n`, 'utf8');
72
+ return cleaned;
73
+ }
74
+
75
+ /**
76
+ * Merge CLI/API overrides over workspace branding.
77
+ * @param {Object} [workspaceBranding]
78
+ * @param {Object} [overrides]
79
+ * @returns {Object}
80
+ */
81
+ function mergeChannelBranding(workspaceBranding, overrides) {
82
+ return {
83
+ ...(workspaceBranding && typeof workspaceBranding === 'object' ? workspaceBranding : {}),
84
+ ...(overrides && typeof overrides === 'object' ? overrides : {})
85
+ };
86
+ }
87
+
88
+ module.exports = {
89
+ BRANDING_FILENAME,
90
+ brandingFilePath,
91
+ readChannelBranding,
92
+ writeChannelBranding,
93
+ mergeChannelBranding
94
+ };
@@ -18,6 +18,7 @@ const {
18
18
  auditCredentialUpdated,
19
19
  auditFailed
20
20
  } = require('./channel-audit');
21
+ const { skipsChannelOauthCredential } = require('./microsoft-copilot-entra-sso');
21
22
 
22
23
  /** @type {Record<string, { method: string, displayName: string, credentialConfig: Object }>} */
23
24
  const CREDENTIAL_SPECS = {
@@ -171,6 +172,13 @@ async function pushIntegrationSecrets(ctx, channelTarget) {
171
172
  */
172
173
  async function ensureChannelCredential(params) {
173
174
  const { ctx, channelTarget, target, force = false } = params;
175
+ if (skipsChannelOauthCredential(channelTarget)) {
176
+ return {
177
+ skipped: true,
178
+ reason: 'entraSsoTrustedIssuer',
179
+ credentialKey: target.credentialKey
180
+ };
181
+ }
174
182
  const existing = await findCredentialByKey(ctx, target.credentialKey);
175
183
  if (existing) {
176
184
  auditCredentialUpdated(channelTarget, {
@@ -0,0 +1,330 @@
1
+ /**
2
+ * External AI Discovery mapping for channel packaging (plan 167.0).
3
+ *
4
+ * @fileoverview Pure discovery → package context; optional Dataplane fetch
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const { ApiClient } = require('../api');
12
+ const { RUNTIME_ORCHESTRATION_SYSTEM_KEY } = require('./chatgpt-runtime-contract');
13
+ const { normalizeBaseUrl, joinUrl } = require('./channel-runtime-resolver');
14
+
15
+ const GATEWAY_TOOLS_CATALOG_MARKER = '/api/v1/mcp/servers/tools';
16
+
17
+ /**
18
+ * @param {string} pathOrUrl
19
+ * @returns {boolean}
20
+ */
21
+ function isGatewayToolsCatalogUrl(pathOrUrl) {
22
+ const value = String(pathOrUrl || '');
23
+ return value.includes(GATEWAY_TOOLS_CATALOG_MARKER);
24
+ }
25
+
26
+ /**
27
+ * Normalize discovery paths that include a public `/data` prefix.
28
+ * @param {string} dataplaneBaseUrl
29
+ * @param {string|null|undefined} pathOrUrl
30
+ * @returns {string|null}
31
+ */
32
+ function resolveDiscoveryUrl(dataplaneBaseUrl, pathOrUrl) {
33
+ if (!pathOrUrl || typeof pathOrUrl !== 'string') {
34
+ return null;
35
+ }
36
+ const trimmed = pathOrUrl.trim();
37
+ if (!trimmed) {
38
+ return null;
39
+ }
40
+ if (isGatewayToolsCatalogUrl(trimmed)) {
41
+ throw new Error(
42
+ 'Gateway tools catalog must not be used as MCP server URL for channel packages'
43
+ );
44
+ }
45
+ if (/^https?:\/\//i.test(trimmed)) {
46
+ return trimmed;
47
+ }
48
+ let relative = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
49
+ if (relative.startsWith('/data/api/')) {
50
+ relative = relative.slice('/data'.length);
51
+ }
52
+ return joinUrl(normalizeBaseUrl(dataplaneBaseUrl), relative);
53
+ }
54
+
55
+ /**
56
+ * @param {Object} discovery
57
+ * @param {string} key
58
+ * @returns {Object|null}
59
+ */
60
+ function findEntryPoint(discovery, key) {
61
+ const entries = Array.isArray(discovery?.entryPoints) ? discovery.entryPoints : [];
62
+ return entries.find((ep) => ep && ep.key === key) || null;
63
+ }
64
+
65
+ /**
66
+ * @param {Object} discovery
67
+ * @returns {string[]}
68
+ */
69
+ function extractGovernedToolNames(discovery) {
70
+ const fromRuntime = discovery?.runtimeDiscovery?.governedToolNames;
71
+ if (Array.isArray(fromRuntime) && fromRuntime.length > 0) {
72
+ return fromRuntime.map((name) => String(name)).filter(Boolean);
73
+ }
74
+ const tools = Array.isArray(discovery?.tools) ? discovery.tools : [];
75
+ return tools.map((tool) => (tool && tool.name ? String(tool.name) : '')).filter(Boolean);
76
+ }
77
+
78
+ /**
79
+ * @param {Object} discovery
80
+ * @param {Object|null} adapter
81
+ * @param {string} dataplaneBaseUrl
82
+ * @returns {{ mcpServerUrl: string|null, installReady: boolean, blocker: string|null }}
83
+ */
84
+ function resolveMcpConnection(discovery, adapter, dataplaneBaseUrl) {
85
+ const adapterUrl = adapter?.connection?.mcpServerUrl || null;
86
+ const remote = findEntryPoint(discovery, 'remoteMcp');
87
+ const fromEntry =
88
+ resolveDiscoveryUrl(dataplaneBaseUrl, remote?.absoluteUrl) ||
89
+ resolveDiscoveryUrl(dataplaneBaseUrl, remote?.path);
90
+ const mcpServerUrl = resolveDiscoveryUrl(dataplaneBaseUrl, adapterUrl) || fromEntry;
91
+ if (mcpServerUrl && isGatewayToolsCatalogUrl(mcpServerUrl)) {
92
+ throw new Error(
93
+ 'Gateway tools catalog must not be used as MCP server URL for channel packages'
94
+ );
95
+ }
96
+ const mcpReady = discovery?.publication?.mcpReady === true;
97
+ const connectable = adapter ? adapter.connectable === true : mcpReady;
98
+ const remoteAvailable = discovery?.transport?.remoteMcp?.status === 'available';
99
+ const installReady = Boolean(mcpReady && connectable && remoteAvailable && mcpServerUrl);
100
+ const blocker =
101
+ adapter?.blocker ||
102
+ (!installReady ? 'remoteMcpUnavailable' : null);
103
+ return { mcpServerUrl, installReady, blocker };
104
+ }
105
+
106
+ /**
107
+ * @param {Object} branding
108
+ * @param {Object} identity
109
+ * @param {Object|null} adapter
110
+ * @returns {string}
111
+ */
112
+ function resolvePackageDisplayName(branding, identity, adapter) {
113
+ if (typeof branding.displayName === 'string' && branding.displayName.trim()) {
114
+ return branding.displayName.trim();
115
+ }
116
+ return (
117
+ identity.displayName ||
118
+ adapter?.nameForHuman ||
119
+ adapter?.displayName ||
120
+ identity.systemKey ||
121
+ RUNTIME_ORCHESTRATION_SYSTEM_KEY
122
+ );
123
+ }
124
+
125
+ /**
126
+ * @param {Object} branding
127
+ * @param {Object} identity
128
+ * @param {Object|null} adapter
129
+ * @param {Object} discovery
130
+ * @returns {string}
131
+ */
132
+ function resolvePackageDescription(branding, identity, adapter, discovery) {
133
+ if (typeof branding.description === 'string' && branding.description.trim()) {
134
+ return branding.description.trim();
135
+ }
136
+ return (
137
+ identity.description ||
138
+ adapter?.descriptionForHuman ||
139
+ adapter?.description ||
140
+ discovery.operatingModel?.summary ||
141
+ ''
142
+ );
143
+ }
144
+
145
+ /**
146
+ * @param {Object|null} auth
147
+ * @returns {Object|null}
148
+ */
149
+ function mapAdapterAuthentication(auth) {
150
+ if (!auth) return null;
151
+ return {
152
+ recommendedCapability: auth.recommendedCapability || null,
153
+ issuer: auth.issuer || null,
154
+ openidConfigurationUrl: auth.openidConfigurationUrl || null,
155
+ authorizationServerMetadataUrl: auth.authorizationServerMetadataUrl || null,
156
+ protectedResourceMetadataUrl: auth.protectedResourceMetadataUrl || null,
157
+ resource: auth.resource || auth.apiResource || null,
158
+ audience: auth.audience !== undefined && auth.audience !== null ? auth.audience : null,
159
+ scopes: Array.isArray(auth.scopes)
160
+ ? [...auth.scopes]
161
+ : Array.isArray(auth.allowedScopes)
162
+ ? [...auth.allowedScopes]
163
+ : [],
164
+ operatorNote: auth.operatorNote || null,
165
+ pkceRequired: auth.pkceRequired === true,
166
+ clientRegistrationModes: Array.isArray(auth.clientRegistrationModes)
167
+ ? [...auth.clientRegistrationModes]
168
+ : []
169
+ };
170
+ }
171
+
172
+ /**
173
+ * @param {Object} branding
174
+ * @returns {boolean}
175
+ */
176
+ function isBrandingApplied(branding) {
177
+ return Boolean(
178
+ (branding.displayName && String(branding.displayName).trim()) ||
179
+ (branding.description && String(branding.description).trim())
180
+ );
181
+ }
182
+
183
+ /**
184
+ * Map discovery (+ optional adapter) into packaging fields.
185
+ *
186
+ * @param {Object} params
187
+ * @param {Object} params.discovery
188
+ * @param {Object} [params.adapter]
189
+ * @param {string} params.dataplaneBaseUrl
190
+ * @param {Object} [params.branding]
191
+ * @returns {Object}
192
+ */
193
+ function mapDiscoveryToPackageContext(params) {
194
+ const discovery = params.discovery;
195
+ if (!discovery || typeof discovery !== 'object') {
196
+ throw new Error('discovery document is required');
197
+ }
198
+ const identity = discovery.identity || {};
199
+ const branding = params.branding && typeof params.branding === 'object' ? params.branding : {};
200
+ const adapter = params.adapter || null;
201
+ const connection = resolveMcpConnection(discovery, adapter, params.dataplaneBaseUrl);
202
+ const authCaps = Array.isArray(discovery.authenticationCapabilities)
203
+ ? discovery.authenticationCapabilities.map((c) => c.capability || c).filter(Boolean)
204
+ : [];
205
+ return {
206
+ systemKey: identity.systemKey || RUNTIME_ORCHESTRATION_SYSTEM_KEY,
207
+ displayName: resolvePackageDisplayName(branding, identity, adapter),
208
+ description: resolvePackageDescription(branding, identity, adapter, discovery),
209
+ governedToolNames: extractGovernedToolNames(discovery),
210
+ mcpServerUrl: connection.mcpServerUrl,
211
+ installReady: connection.installReady,
212
+ blocker: connection.blocker,
213
+ authentication: mapAdapterAuthentication(adapter?.authentication || null),
214
+ authenticationCapabilities: authCaps,
215
+ publication: {
216
+ mcpReady: discovery.publication?.mcpReady === true,
217
+ environment: discovery.publication?.environment || null
218
+ },
219
+ brandingApplied: isBrandingApplied(branding)
220
+ };
221
+ }
222
+
223
+ /**
224
+ * @param {Object} response
225
+ * @returns {Object}
226
+ */
227
+ function unwrapApiData(response) {
228
+ if (response && typeof response === 'object' && response.data && typeof response.data === 'object') {
229
+ if (response.data.kind || response.data.identity || response.data.adapter) {
230
+ return response.data;
231
+ }
232
+ }
233
+ return response;
234
+ }
235
+
236
+ /**
237
+ * @param {string} dataplaneBaseUrl
238
+ * @param {Object} authConfig
239
+ * @param {string} [systemKey]
240
+ * @returns {Promise<Object>}
241
+ */
242
+ async function fetchExternalAiDiscovery(
243
+ dataplaneBaseUrl,
244
+ authConfig,
245
+ systemKey = RUNTIME_ORCHESTRATION_SYSTEM_KEY
246
+ ) {
247
+ const client = new ApiClient(normalizeBaseUrl(dataplaneBaseUrl), authConfig || {});
248
+ const response = await client.get(`/api/v1/external/systems/${systemKey}/discovery`);
249
+ const discovery = unwrapApiData(response);
250
+ if (!discovery || !discovery.kind) {
251
+ throw new Error(`Discovery document missing for systemKey=${systemKey}`);
252
+ }
253
+ return discovery;
254
+ }
255
+
256
+ /**
257
+ * @param {string} dataplaneBaseUrl
258
+ * @param {Object} authConfig
259
+ * @param {string} adapterKey microsoft-plugin | chatgpt-setup
260
+ * @param {string} [systemKey]
261
+ * @returns {Promise<Object>}
262
+ */
263
+ async function fetchExternalAiDiscoveryAdapter(
264
+ dataplaneBaseUrl,
265
+ authConfig,
266
+ adapterKey,
267
+ systemKey = RUNTIME_ORCHESTRATION_SYSTEM_KEY
268
+ ) {
269
+ const client = new ApiClient(normalizeBaseUrl(dataplaneBaseUrl), authConfig || {});
270
+ const response = await client.get(
271
+ `/api/v1/external/systems/${systemKey}/discovery/adapters/${adapterKey}`
272
+ );
273
+ return unwrapApiData(response);
274
+ }
275
+
276
+ /**
277
+ * @param {string} channelTarget
278
+ * @returns {string|null}
279
+ */
280
+ function adapterKeyForChannel(channelTarget) {
281
+ if (channelTarget === 'microsoft-copilot') return 'microsoft-plugin';
282
+ if (channelTarget === 'chatgpt') return 'chatgpt-setup';
283
+ return null;
284
+ }
285
+
286
+ /**
287
+ * Fetch discovery (+ adapter when known) and map package context.
288
+ *
289
+ * @param {Object} params
290
+ * @returns {Promise<Object>}
291
+ */
292
+ async function loadChannelPackageContext(params) {
293
+ const systemKey = params.runtimeSystemKey || RUNTIME_ORCHESTRATION_SYSTEM_KEY;
294
+ const discovery = await fetchExternalAiDiscovery(
295
+ params.dataplaneBaseUrl,
296
+ params.authConfig,
297
+ systemKey
298
+ );
299
+ const adapterKey = adapterKeyForChannel(params.channelTarget);
300
+ let adapter = null;
301
+ if (adapterKey) {
302
+ try {
303
+ adapter = await fetchExternalAiDiscoveryAdapter(
304
+ params.dataplaneBaseUrl,
305
+ params.authConfig,
306
+ adapterKey,
307
+ systemKey
308
+ );
309
+ } catch {
310
+ adapter = null;
311
+ }
312
+ }
313
+ return mapDiscoveryToPackageContext({
314
+ discovery,
315
+ adapter,
316
+ dataplaneBaseUrl: params.dataplaneBaseUrl,
317
+ branding: params.branding
318
+ });
319
+ }
320
+
321
+ module.exports = {
322
+ GATEWAY_TOOLS_CATALOG_MARKER,
323
+ isGatewayToolsCatalogUrl,
324
+ resolveDiscoveryUrl,
325
+ mapDiscoveryToPackageContext,
326
+ fetchExternalAiDiscovery,
327
+ fetchExternalAiDiscoveryAdapter,
328
+ adapterKeyForChannel,
329
+ loadChannelPackageContext
330
+ };
@@ -72,7 +72,7 @@ async function fetchCommunicationSystem(ctx) {
72
72
  if (!system || typeof system !== 'object') {
73
73
  throw new Error(
74
74
  `Communication external system "${ctx.channelTarget}" was not found. ` +
75
- `Run: aifabrix deploy ${ctx.channelTarget}`
75
+ `Run: aifabrix channel add ${ctx.channelTarget} --name "…" --force`
76
76
  );
77
77
  }
78
78
  const kind = system.systemKind || system.systemKindKey;