@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
@@ -19,6 +19,12 @@ const {
19
19
  COPILOT_RUNTIME_TRANSPORT,
20
20
  COPILOT_RUNTIME_AUTH_MODE
21
21
  } = require('./chatgpt-runtime-contract');
22
+ const { applyMicrosoftCopilotDisplayLimits } = require('./microsoft-copilot-limits');
23
+ const {
24
+ resolveCopilotEntraScopes,
25
+ evaluateCopilotEntraSsoReadiness
26
+ } = require('./microsoft-copilot-entra-sso');
27
+ const { applyCopilotEntraSsoFormFields } = require('./microsoft-copilot-entra-sso-form');
22
28
 
23
29
  /** Default Slack slash prefix — matches dataplane `templates/slack/command_catalog.yaml`. */
24
30
  const DEFAULT_SLACK_COMMAND_PREFIX = 'af';
@@ -152,15 +158,62 @@ function buildDeterministicGuid(seed) {
152
158
  * @param {Object} resolved
153
159
  * @param {Object} params
154
160
  */
155
- function applyMicrosoftCopilotRuntimeValues(resolved, params) {
161
+ /**
162
+ * Apply discovery-driven package context (plan 167.0).
163
+ * @param {Object} resolved
164
+ * @param {Object} [packageContext]
165
+ */
166
+ function applyDiscoveryPackageContext(resolved, packageContext) {
167
+ if (!packageContext || typeof packageContext !== 'object') {
168
+ resolved.installReady = false;
169
+ resolved.discoveryBlocker = resolved.discoveryBlocker || 'discoveryUnavailable';
170
+ return;
171
+ }
172
+ if (Array.isArray(packageContext.governedToolNames) && packageContext.governedToolNames.length > 0) {
173
+ resolved.layerBToolNames = [...packageContext.governedToolNames];
174
+ }
175
+ if (packageContext.mcpServerUrl) {
176
+ resolved.mcpUrl = packageContext.mcpServerUrl;
177
+ }
178
+ if (packageContext.displayName && !resolved.brandingNameOverride) {
179
+ // Keep CLI --name as appName; discovery fills defaults only when requested upstream.
180
+ }
181
+ if (packageContext.description && !resolved.appDescription) {
182
+ resolved.appDescription = packageContext.description;
183
+ }
184
+ resolved.installReady = packageContext.installReady === true;
185
+ resolved.discoveryBlocker = packageContext.blocker || null;
186
+ resolved.discoveryAuthentication = packageContext.authentication || null;
187
+ resolved.discoveryAuthenticationCapabilities = packageContext.authenticationCapabilities || [];
188
+ }
189
+
190
+ /**
191
+ * @param {Object} resolved
192
+ * @returns {void}
193
+ */
194
+ function applyCopilotRuntimeDomain(resolved) {
195
+ resolved.runtimeDomain = '';
196
+ try {
197
+ resolved.runtimeDomain = new URL(resolved.mcpUrl).hostname;
198
+ } catch {
199
+ try {
200
+ resolved.runtimeDomain = new URL(resolved.dataplaneBaseUrl).hostname;
201
+ } catch {
202
+ resolved.runtimeDomain = '';
203
+ }
204
+ }
205
+ }
206
+
207
+ /**
208
+ * @param {Object} resolved
209
+ * @param {Object} params
210
+ * @returns {void}
211
+ */
212
+ function applyCopilotPackageIdentity(resolved, params) {
156
213
  resolved.runtimeSystemKey = COPILOT_RUNTIME_SYSTEM_KEY;
157
214
  resolved.runtimeTransport = COPILOT_RUNTIME_TRANSPORT;
158
215
  resolved.runtimeAuthMode = COPILOT_RUNTIME_AUTH_MODE;
159
216
  resolved.layerBToolNames = Array.isArray(params.layerBToolNames) ? [...params.layerBToolNames] : [];
160
- resolved.scopes = Array.isArray(params.scopes)
161
- ? [...params.scopes]
162
- : ['openid', 'profile', 'offline_access'];
163
- resolved.scopesJson = JSON.stringify(resolved.scopes);
164
217
  resolved.packageVersion = typeof params.packageVersion === 'string' ? params.packageVersion : '1.0.0';
165
218
  resolved.packageAppId = buildDeterministicGuid(
166
219
  `${resolved.channelTarget}:${resolved.baseName}:${resolved.environment}:app`
@@ -168,16 +221,45 @@ function applyMicrosoftCopilotRuntimeValues(resolved, params) {
168
221
  resolved.pluginId = buildDeterministicGuid(
169
222
  `${resolved.channelTarget}:${resolved.baseName}:${resolved.environment}:plugin`
170
223
  );
171
- resolved.runtimeDomain = '';
172
- try {
173
- resolved.runtimeDomain = new URL(resolved.mcpUrl).hostname;
174
- } catch {
175
- try {
176
- resolved.runtimeDomain = new URL(resolved.dataplaneBaseUrl).hostname;
177
- } catch {
178
- resolved.runtimeDomain = '';
179
- }
224
+ const referenceId =
225
+ typeof params.oauthPluginVaultReferenceId === 'string' &&
226
+ params.oauthPluginVaultReferenceId.trim()
227
+ ? params.oauthPluginVaultReferenceId.trim()
228
+ : '';
229
+ resolved.oauthPluginVaultReferenceId = referenceId || null;
230
+ }
231
+
232
+ /**
233
+ * @param {Object} resolved
234
+ * @param {Object} params
235
+ * @returns {void}
236
+ */
237
+ function applyMicrosoftCopilotRuntimeValues(resolved, params) {
238
+ applyCopilotPackageIdentity(resolved, params);
239
+ applyDiscoveryPackageContext(resolved, params.packageContext);
240
+ resolved.scopes = resolveCopilotEntraScopes({
241
+ explicitScopes: params.scopes,
242
+ packageContext: params.packageContext,
243
+ entraProvider: params.entraProvider
244
+ });
245
+ resolved.scopesJson = JSON.stringify(resolved.scopes);
246
+ const readiness = evaluateCopilotEntraSsoReadiness({
247
+ packageContext: params.packageContext,
248
+ oauthPluginVaultReferenceId: resolved.oauthPluginVaultReferenceId,
249
+ scopes: resolved.scopes,
250
+ entraProvider: params.entraProvider || null
251
+ });
252
+ resolved.installReady = readiness.installReady;
253
+ resolved.discoveryBlocker = readiness.blocker;
254
+ resolved.discoveryAuthentication = readiness.packageAuthentication;
255
+ if (params.entraProvider?.issuer) {
256
+ resolved.entraIssuer = params.entraProvider.issuer;
180
257
  }
258
+ if (params.iconColorPath) resolved.iconColorPath = params.iconColorPath;
259
+ if (params.iconOutlinePath) resolved.iconOutlinePath = params.iconOutlinePath;
260
+ applyCopilotRuntimeDomain(resolved);
261
+ applyCopilotEntraSsoFormFields(resolved, params);
262
+ applyMicrosoftCopilotDisplayLimits(resolved);
181
263
  }
182
264
 
183
265
  /**
@@ -254,6 +336,7 @@ function resolveChannelRuntimeValues(params) {
254
336
 
255
337
  if (params.channelTarget === 'slack') {
256
338
  applySlackRuntimeValues(resolved, params);
339
+ applyDiscoveryPackageContext(resolved, params.packageContext);
257
340
  }
258
341
 
259
342
  if (params.channelTarget === 'microsoft-copilot') {
@@ -262,6 +345,11 @@ function resolveChannelRuntimeValues(params) {
262
345
 
263
346
  if (params.channelTarget === 'chatgpt') {
264
347
  applyChatGptRuntimeValues(resolved, params);
348
+ applyDiscoveryPackageContext(resolved, params.packageContext);
349
+ }
350
+
351
+ if (params.channelTarget === 'teams-bot') {
352
+ applyDiscoveryPackageContext(resolved, params.packageContext);
265
353
  }
266
354
 
267
355
  return resolved;
@@ -277,6 +365,7 @@ module.exports = {
277
365
  applySlackRuntimeValues,
278
366
  applyMicrosoftCopilotRuntimeValues,
279
367
  applyChatGptRuntimeValues,
368
+ applyDiscoveryPackageContext,
280
369
  resolveSlackOAuthManifestFields,
281
370
  resolveChannelRuntimeValues,
282
371
  DEFAULT_SLACK_COMMAND_PREFIX
@@ -125,7 +125,13 @@ function displayChannelCoreFields(result) {
125
125
  logger.log(metadata(`Artifact: ${result.artifactPath}`));
126
126
  }
127
127
  if (result.installGuidePath) {
128
- logger.log(metadata(`Install guide: ${result.installGuidePath}`));
128
+ logger.log(metadata(`Install guide (README): ${result.installGuidePath}`));
129
+ }
130
+ if (result.zipPath) {
131
+ logger.log(metadata(`Package ZIP: ${result.zipPath}`));
132
+ }
133
+ if (result.installReady === false && result.blocker) {
134
+ logger.log(metadata(`Install ready: no (${result.blocker})`));
129
135
  }
130
136
  }
131
137
 
@@ -0,0 +1,138 @@
1
+ /**
2
+ * microsoft-copilot channel-add helpers (Entra SSO form context for operator errors).
3
+ *
4
+ * @fileoverview Keep add-channel.js under size limits
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const { resolveChannelAuthContext } = require('./channel-auth-context');
12
+ const {
13
+ normalizeBaseChannelName,
14
+ resolveChannelDisplayName
15
+ } = require('./channel-display-name');
16
+ const { getDefaultChannelDescription } = require('./channel-targets');
17
+ const { resolveAppDescription } = require('./channel-description');
18
+ const { loadChannelPackageContext } = require('./channel-discovery');
19
+ const { getInboundMicrosoftEntraIdProvider } = require('../api/token-providers.api');
20
+ const { getKeycloakEntraIdentityProvider } = require('../api/keycloak.api');
21
+ const { resolveChannelRuntimeValues } = require('./channel-runtime-resolver');
22
+ const { assertOauthPluginVaultReferenceId } = require('./microsoft-copilot-entra-sso');
23
+
24
+ /**
25
+ * @param {Object} options
26
+ * @returns {boolean}
27
+ */
28
+ function hasOauthPluginVaultReference(options) {
29
+ return (
30
+ typeof options.oauthPluginVaultReferenceId === 'string' &&
31
+ Boolean(options.oauthPluginVaultReferenceId.trim())
32
+ );
33
+ }
34
+
35
+ /**
36
+ * Load Trust source + Keycloak Entra IdP for Copilot packaging (null on miss/error).
37
+ * @param {Object} ctx
38
+ * @returns {Promise<{ entraProvider: Object|null, entraIdentityProvider: Object|null }>}
39
+ */
40
+ async function loadMicrosoftCopilotEntraContext(ctx) {
41
+ let entraProvider = null;
42
+ let entraIdentityProvider = null;
43
+ if (!ctx?.controllerUrl) {
44
+ return { entraProvider, entraIdentityProvider };
45
+ }
46
+ try {
47
+ entraProvider = await getInboundMicrosoftEntraIdProvider(ctx.controllerUrl, ctx.authConfig);
48
+ } catch {
49
+ entraProvider = null;
50
+ }
51
+ try {
52
+ entraIdentityProvider = await getKeycloakEntraIdentityProvider(
53
+ ctx.controllerUrl,
54
+ ctx.authConfig
55
+ );
56
+ } catch {
57
+ entraIdentityProvider = null;
58
+ }
59
+ return { entraProvider, entraIdentityProvider };
60
+ }
61
+
62
+ /**
63
+ * @param {Object} params
64
+ * @returns {Promise<Object>}
65
+ */
66
+ async function resolveCopilotFormContextForError(params) {
67
+ const { options, ctx, name, baseName, description } = params;
68
+ let packageContext = null;
69
+ try {
70
+ packageContext = await loadChannelPackageContext({
71
+ dataplaneBaseUrl: ctx.dataplaneUrl,
72
+ authConfig: ctx.authConfig,
73
+ channelTarget: 'microsoft-copilot',
74
+ branding: { displayName: name, description }
75
+ });
76
+ } catch {
77
+ packageContext = null;
78
+ }
79
+ const { entraProvider, entraIdentityProvider } = await loadMicrosoftCopilotEntraContext(ctx);
80
+ return resolveChannelRuntimeValues({
81
+ dataplaneBaseUrl: ctx.dataplaneUrl,
82
+ channelTarget: 'microsoft-copilot',
83
+ appName: name,
84
+ baseName,
85
+ environment: ctx.environment,
86
+ appDescription: description,
87
+ packageContext,
88
+ entraProvider,
89
+ entraIdentityProvider,
90
+ oauthPluginVaultReferenceId: options.oauthPluginVaultReferenceId
91
+ });
92
+ }
93
+
94
+ /**
95
+ * @param {Object} params
96
+ * @throws {Error}
97
+ */
98
+ async function assertMicrosoftCopilotOauthReference(params) {
99
+ const { options } = params;
100
+ if (hasOauthPluginVaultReference(options)) {
101
+ return;
102
+ }
103
+ const formContext = await resolveCopilotFormContextForError(params);
104
+ assertOauthPluginVaultReferenceId({ ...options, formContext });
105
+ }
106
+
107
+ /**
108
+ * Fail before force upload when Teams Entra SSO auth-config id is missing.
109
+ * @param {Object} options
110
+ * @throws {Error}
111
+ */
112
+ async function assertMicrosoftCopilotOauthReferenceBeforeForce(options) {
113
+ if (hasOauthPluginVaultReference(options)) {
114
+ return;
115
+ }
116
+ const ctx = await resolveChannelAuthContext('microsoft-copilot', { silent: options.quiet });
117
+ const baseName = normalizeBaseChannelName(options.name);
118
+ const name = resolveChannelDisplayName(baseName, ctx.environment);
119
+ const rawDescription =
120
+ options.description && String(options.description).trim()
121
+ ? String(options.description).trim()
122
+ : getDefaultChannelDescription();
123
+ await assertMicrosoftCopilotOauthReference({
124
+ options,
125
+ ctx,
126
+ name,
127
+ baseName,
128
+ description: resolveAppDescription('microsoft-copilot', rawDescription)
129
+ });
130
+ }
131
+
132
+ module.exports = {
133
+ hasOauthPluginVaultReference,
134
+ loadMicrosoftCopilotEntraContext,
135
+ resolveCopilotFormContextForError,
136
+ assertMicrosoftCopilotOauthReference,
137
+ assertMicrosoftCopilotOauthReferenceBeforeForce
138
+ };
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Microsoft Copilot Entra SSO form field resolution + template rendering.
3
+ *
4
+ * @fileoverview Template-backed Teams Entra SSO registration guidance
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
+ const Handlebars = require('handlebars');
14
+ const { BUILDER_ROOT } = require('./channel-integration-template');
15
+
16
+ const ENTRA_SSO_REGISTRATION_URL =
17
+ 'https://dev.teams.microsoft.com/tools/entra-configuration';
18
+ const TEMPLATES_DIR = path.join(BUILDER_ROOT, 'templates', 'channels', 'microsoft-copilot');
19
+
20
+ /**
21
+ * @param {string} issuer
22
+ * @returns {string|null}
23
+ */
24
+ function extractTenantIdFromIssuer(issuer) {
25
+ if (typeof issuer !== 'string' || !issuer.trim()) {
26
+ return null;
27
+ }
28
+ const match = issuer.match(
29
+ /login\.microsoftonline\.com\/([0-9a-fA-F-]{36})(?:\/|$)/i
30
+ );
31
+ return match ? match[1] : null;
32
+ }
33
+
34
+ /**
35
+ * @param {string} [url]
36
+ * @returns {string|null}
37
+ */
38
+ function originFromUrl(url) {
39
+ if (typeof url !== 'string' || !url.trim()) {
40
+ return null;
41
+ }
42
+ try {
43
+ const parsed = new URL(url.trim());
44
+ return `${parsed.protocol}//${parsed.host}`;
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Prefer Keycloak status enterpriseAppId (Entra SSO application/client id).
52
+ * @param {Object|null} entraIdentityProvider
53
+ * @param {Object|null} entraProvider
54
+ * @param {Object|null} auth
55
+ * @returns {string|null}
56
+ */
57
+ function resolveEntraSsoClientId(entraIdentityProvider, entraProvider, auth) {
58
+ if (
59
+ entraIdentityProvider &&
60
+ typeof entraIdentityProvider.enterpriseAppId === 'string' &&
61
+ entraIdentityProvider.enterpriseAppId.trim()
62
+ ) {
63
+ return entraIdentityProvider.enterpriseAppId.trim();
64
+ }
65
+ if (entraProvider && typeof entraProvider.clientId === 'string' && entraProvider.clientId.trim()) {
66
+ return entraProvider.clientId.trim();
67
+ }
68
+ if (auth && typeof auth.clientId === 'string' && auth.clientId.trim()) {
69
+ return auth.clientId.trim();
70
+ }
71
+ return null;
72
+ }
73
+
74
+ /**
75
+ * @param {Object} resolved
76
+ * @param {Object|null} entraIdentityProvider
77
+ * @param {Object|null} entraProvider
78
+ * @param {Object|null} auth
79
+ * @returns {string|null}
80
+ */
81
+ function resolveEntraTenantId(resolved, entraIdentityProvider, entraProvider, auth) {
82
+ if (
83
+ entraIdentityProvider &&
84
+ typeof entraIdentityProvider.tenantId === 'string' &&
85
+ entraIdentityProvider.tenantId.trim()
86
+ ) {
87
+ return entraIdentityProvider.tenantId.trim();
88
+ }
89
+ return extractTenantIdFromIssuer(
90
+ resolved.entraIssuer || entraProvider?.issuer || auth?.issuer || null
91
+ );
92
+ }
93
+
94
+ /**
95
+ * @param {Object} resolved
96
+ * @param {Object} [params]
97
+ * @returns {void}
98
+ */
99
+ function applyCopilotEntraSsoFormFields(resolved, params = {}) {
100
+ const entraProvider = params.entraProvider || null;
101
+ const entraIdentityProvider = params.entraIdentityProvider || null;
102
+ const auth = resolved.discoveryAuthentication || null;
103
+ resolved.entraSsoRegistrationUrl = ENTRA_SSO_REGISTRATION_URL;
104
+ resolved.entraSsoBaseUrl =
105
+ originFromUrl(resolved.dataplaneBaseUrl) || originFromUrl(resolved.mcpUrl) || null;
106
+ resolved.entraSsoClientId = resolveEntraSsoClientId(
107
+ entraIdentityProvider,
108
+ entraProvider,
109
+ auth
110
+ );
111
+ const scopes = Array.isArray(resolved.scopes) ? resolved.scopes : [];
112
+ resolved.entraSsoScope = scopes.length > 0 ? scopes.join(', ') : null;
113
+ resolved.entraTenantId = resolveEntraTenantId(
114
+ resolved,
115
+ entraIdentityProvider,
116
+ entraProvider,
117
+ auth
118
+ );
119
+ if (entraIdentityProvider?.enterpriseAppId) {
120
+ resolved.entraEnterpriseAppId = String(entraIdentityProvider.enterpriseAppId).trim();
121
+ }
122
+ }
123
+
124
+ /**
125
+ * @param {string} fileName
126
+ * @returns {string}
127
+ */
128
+ function readCopilotTemplate(fileName) {
129
+ const templatePath = path.join(TEMPLATES_DIR, fileName);
130
+ if (!fs.existsSync(templatePath)) {
131
+ throw new Error(`Microsoft Copilot template not found: ${templatePath}`);
132
+ }
133
+ return fs.readFileSync(templatePath, 'utf8');
134
+ }
135
+
136
+ /**
137
+ * @param {string} fileName
138
+ * @param {Object} context
139
+ * @returns {string}
140
+ */
141
+ function renderCopilotChannelTemplate(fileName, context) {
142
+ const hb = Handlebars.create();
143
+ hb.registerPartial('entraSsoRegistration', readCopilotTemplate('entra-sso-registration.template.md.hbs'));
144
+ const template = hb.compile(readCopilotTemplate(fileName));
145
+ return template(context).trim();
146
+ }
147
+
148
+ /**
149
+ * Operator-facing error when Teams Entra SSO auth-config id is missing.
150
+ * Prose lives in templates — not inline CLI copy.
151
+ * @param {Object|string} [context]
152
+ * @returns {string}
153
+ */
154
+ function formatOauthPluginVaultReferenceRequiredError(context = {}) {
155
+ const values = typeof context === 'string' ? { appName: context, baseName: context } : context || {};
156
+ const appName =
157
+ (typeof values.appName === 'string' && values.appName.trim()) ||
158
+ (typeof values.baseName === 'string' && values.baseName.trim()) ||
159
+ 'Your Assistant';
160
+ const baseName =
161
+ (typeof values.baseName === 'string' && values.baseName.trim()) || appName;
162
+ return renderCopilotChannelTemplate('oauth-plugin-vault-required.template.md.hbs', {
163
+ ...values,
164
+ appName,
165
+ baseName,
166
+ channelTarget: 'microsoft-copilot',
167
+ entraSsoRegistrationUrl: values.entraSsoRegistrationUrl || ENTRA_SSO_REGISTRATION_URL
168
+ });
169
+ }
170
+
171
+ module.exports = {
172
+ ENTRA_SSO_REGISTRATION_URL,
173
+ extractTenantIdFromIssuer,
174
+ originFromUrl,
175
+ resolveEntraSsoClientId,
176
+ resolveEntraTenantId,
177
+ applyCopilotEntraSsoFormFields,
178
+ renderCopilotChannelTemplate,
179
+ formatOauthPluginVaultReferenceRequiredError
180
+ };