@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
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Validate Microsoft Copilot package source files before ZIP.
3
+ *
4
+ * @fileoverview Schema-oriented checks for Teams + declarative agent + AI plugin manifests
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const {
12
+ TEAMS_NAME_SHORT_MAX,
13
+ TEAMS_NAME_FULL_MAX,
14
+ TEAMS_DESCRIPTION_SHORT_MAX,
15
+ TEAMS_DESCRIPTION_FULL_MAX,
16
+ PLUGIN_NAME_FOR_HUMAN_MAX,
17
+ PLUGIN_DESCRIPTION_FOR_HUMAN_MAX
18
+ } = require('./microsoft-copilot-limits');
19
+ const { readPngDimensions } = require('./microsoft-copilot-icons');
20
+
21
+ const ALLOWED_AUTH_TYPES = new Set(['None', 'OAuthPluginVault', 'ApiKeyPluginVault']);
22
+
23
+ /**
24
+ * @param {unknown} value
25
+ * @param {string} label
26
+ * @param {number} max
27
+ * @param {string[]} errors
28
+ */
29
+ function assertMaxLength(value, label, max, errors) {
30
+ if (typeof value !== 'string') {
31
+ errors.push(`${label} must be a string`);
32
+ return;
33
+ }
34
+ if (value.length > max) {
35
+ errors.push(`${label} exceeds ${max} characters (got ${value.length})`);
36
+ }
37
+ }
38
+
39
+ /**
40
+ * @param {Record<string, string>} sourceFiles
41
+ * @returns {{ manifest: Object|null, agent: Object|null, plugin: Object|null, errors: string[] }}
42
+ */
43
+ function parsePackageJsonFiles(sourceFiles) {
44
+ const errors = [];
45
+ const parsed = { manifest: null, agent: null, plugin: null };
46
+ try {
47
+ parsed.manifest = JSON.parse(sourceFiles['manifest.json'] || '');
48
+ } catch {
49
+ errors.push('manifest.json is not valid JSON');
50
+ }
51
+ try {
52
+ parsed.agent = JSON.parse(sourceFiles['declarativeAgent.json'] || '');
53
+ } catch {
54
+ errors.push('declarativeAgent.json is not valid JSON');
55
+ }
56
+ try {
57
+ parsed.plugin = JSON.parse(sourceFiles['ai-plugin.json'] || '');
58
+ } catch {
59
+ errors.push('ai-plugin.json is not valid JSON');
60
+ }
61
+ return { ...parsed, errors };
62
+ }
63
+
64
+ /**
65
+ * @param {Object|null} manifest
66
+ * @param {string[]} errors
67
+ */
68
+ function validateTeamsManifest(manifest, errors) {
69
+ if (!manifest) return;
70
+ assertMaxLength(manifest?.name?.short, 'manifest.name.short', TEAMS_NAME_SHORT_MAX, errors);
71
+ assertMaxLength(manifest?.name?.full, 'manifest.name.full', TEAMS_NAME_FULL_MAX, errors);
72
+ assertMaxLength(
73
+ manifest?.description?.short,
74
+ 'manifest.description.short',
75
+ TEAMS_DESCRIPTION_SHORT_MAX,
76
+ errors
77
+ );
78
+ assertMaxLength(
79
+ manifest?.description?.full,
80
+ 'manifest.description.full',
81
+ TEAMS_DESCRIPTION_FULL_MAX,
82
+ errors
83
+ );
84
+ if (!manifest.copilotAgents?.declarativeAgents?.length) {
85
+ errors.push('manifest.copilotAgents.declarativeAgents is required');
86
+ }
87
+ }
88
+
89
+ /**
90
+ * @param {Object|null} agent
91
+ * @param {string[]} errors
92
+ */
93
+ function validateDeclarativeAgent(agent, errors) {
94
+ if (!agent) return;
95
+ if (agent.version !== 'v1.4') {
96
+ errors.push('declarativeAgent.version must be "v1.4"');
97
+ }
98
+ if (!agent.id || !agent.name || !agent.description || !agent.instructions) {
99
+ errors.push('declarativeAgent requires id, name, description, and instructions');
100
+ }
101
+ }
102
+
103
+ /**
104
+ * @param {Object|null} plugin
105
+ * @param {string[]} errors
106
+ */
107
+ function validateAiPlugin(plugin, errors) {
108
+ if (!plugin) return;
109
+ if (plugin.schema_version !== 'v2.4') {
110
+ errors.push('ai-plugin.schema_version must be "v2.4"');
111
+ }
112
+ assertMaxLength(plugin.name_for_human, 'ai-plugin.name_for_human', PLUGIN_NAME_FOR_HUMAN_MAX, errors);
113
+ assertMaxLength(
114
+ plugin.description_for_human,
115
+ 'ai-plugin.description_for_human',
116
+ PLUGIN_DESCRIPTION_FOR_HUMAN_MAX,
117
+ errors
118
+ );
119
+ if (!plugin.namespace || !/^[A-Za-z0-9-]+$/.test(plugin.namespace)) {
120
+ errors.push('ai-plugin.namespace is required (alphanumeric and hyphen only)');
121
+ }
122
+ validateAiPluginRuntime(plugin, errors);
123
+ }
124
+
125
+ /**
126
+ * @param {Object} plugin
127
+ * @param {string[]} errors
128
+ */
129
+ function validateAiPluginRuntime(plugin, errors) {
130
+ const runtime = Array.isArray(plugin.runtimes) ? plugin.runtimes[0] : null;
131
+ if (!runtime || runtime.type !== 'RemoteMCPServer') {
132
+ errors.push('ai-plugin.runtimes[0].type must be RemoteMCPServer');
133
+ return;
134
+ }
135
+ if (!ALLOWED_AUTH_TYPES.has(runtime.auth?.type)) {
136
+ errors.push(
137
+ `ai-plugin.runtimes[0].auth.type must be one of ${[...ALLOWED_AUTH_TYPES].join(', ')}`
138
+ );
139
+ }
140
+ if (runtime.auth?.type === 'OAuthPluginVault') {
141
+ const ref = runtime.auth.reference_id;
142
+ if (typeof ref !== 'string' || !ref.trim()) {
143
+ errors.push(
144
+ 'OAuthPluginVault.reference_id is required — pass --oauth-plugin-vault-reference-id ' +
145
+ 'with the Teams Developer Portal Entra SSO auth-config id'
146
+ );
147
+ }
148
+ }
149
+ if (runtime.auth?.type === 'None') {
150
+ errors.push(
151
+ 'ai-plugin.runtimes[0].auth.type None is not allowed; use OAuthPluginVault (Microsoft token / Entra)'
152
+ );
153
+ }
154
+ const desc = runtime.spec?.mcp_tool_description;
155
+ if (desc && (desc.mode || desc.layerBToolNames)) {
156
+ errors.push(
157
+ 'ai-plugin mcp_tool_description must not use mode/layerBToolNames; omit for dynamic discovery or use tools/file'
158
+ );
159
+ }
160
+ }
161
+
162
+ /**
163
+ * @param {{ color: Buffer, outline: Buffer }} icons
164
+ * @param {string[]} errors
165
+ */
166
+ function validatePackageIcons(icons, errors) {
167
+ const colorDims = readPngDimensions(icons.color);
168
+ const outlineDims = readPngDimensions(icons.outline);
169
+ if (!colorDims || colorDims.width !== 192 || colorDims.height !== 192) {
170
+ errors.push('color.png must be a 192x192 PNG');
171
+ }
172
+ if (!outlineDims || outlineDims.width !== 32 || outlineDims.height !== 32) {
173
+ errors.push('outline.png must be a 32x32 PNG');
174
+ }
175
+ }
176
+
177
+ /**
178
+ * @param {Record<string, string>} sourceFiles
179
+ * @param {{ color: Buffer, outline: Buffer }} icons
180
+ * @throws {Error}
181
+ */
182
+ function assertValidMicrosoftCopilotPackage(sourceFiles, icons) {
183
+ const parsed = parsePackageJsonFiles(sourceFiles);
184
+ const errors = [...parsed.errors];
185
+ validateTeamsManifest(parsed.manifest, errors);
186
+ validateDeclarativeAgent(parsed.agent, errors);
187
+ validateAiPlugin(parsed.plugin, errors);
188
+ validatePackageIcons(icons, errors);
189
+ if (errors.length) {
190
+ throw new Error(`Microsoft Copilot package validation failed:\n- ${errors.join('\n- ')}`);
191
+ }
192
+ }
193
+
194
+ module.exports = {
195
+ assertValidMicrosoftCopilotPackage,
196
+ ALLOWED_AUTH_TYPES
197
+ };
package/lib/cli/index.js CHANGED
@@ -67,6 +67,8 @@ function setupCoreCliCommands(program) {
67
67
  setupChannelCommands(program);
68
68
  setupDatasourceCommands(program);
69
69
  setupManifestCommands(program);
70
+ const { setupIntegrationDefinitionCommands } = require('../commands/integration-definition');
71
+ setupIntegrationDefinitionCommands(program);
70
72
  }
71
73
 
72
74
  /**
@@ -331,23 +331,30 @@ async function runGuidedDownInfra(appName, options, infraMod, appLib) {
331
331
  logger.log(SEPARATOR);
332
332
  logger.log('');
333
333
 
334
+ const targetDeveloperId = options && options.developerId;
334
335
  const spin = startSpinner('Stopping infrastructure...');
335
336
  await withMutedLogger(async() => {
336
337
  if (typeof appName === 'string' && appName.trim().length > 0) {
337
- await appLib.downApp(appName, { volumes: !!options.volumes });
338
+ await appLib.downApp(appName, {
339
+ volumes: !!options.volumes,
340
+ developerId: targetDeveloperId
341
+ });
338
342
  return;
339
343
  }
340
- if (options.volumes) await infraMod.stopInfraWithVolumes();
341
- else await infraMod.stopInfra();
344
+ if (options.volumes) await infraMod.stopInfraWithVolumes(targetDeveloperId);
345
+ else await infraMod.stopInfra(targetDeveloperId);
342
346
  });
343
347
  stopSpinnerSuccess(spin, 'Infrastructure stopped');
344
348
 
345
- const developerId = await config.getDeveloperId();
346
- const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
349
+ const developerId = String(
350
+ targetDeveloperId !== undefined && targetDeveloperId !== null && targetDeveloperId !== ''
351
+ ? targetDeveloperId
352
+ : await config.getDeveloperId()
353
+ );
347
354
  const cfg = await config.getConfig();
348
355
 
349
356
  logFooterStart('Stopped');
350
- logSection('Developer', `dev${String(idNum).padStart(2, '0')} (id: ${idNum})`);
357
+ logSection('Developer', `dev${developerId.padStart(2, '0')} (id: ${developerId})`);
351
358
  logStoppedServices(cfg);
352
359
  if (options.volumes) {
353
360
  logger.log(chalk.yellow('⚠ Volumes removed: all local data deleted'));
@@ -31,6 +31,9 @@ const {
31
31
  getRestartableInfraServiceNames,
32
32
  buildRestartInfraHelpLines
33
33
  } = require('../constants/infra-compose-service-names');
34
+ const {
35
+ resolveTransientDeveloperIdFromOptions
36
+ } = require('../utils/transient-developer-id');
34
37
 
35
38
  const {
36
39
  computeEffectiveInfraOptionalFlags,
@@ -97,6 +100,18 @@ async function applyDeveloperIdFromUpInfra(options) {
97
100
  return id;
98
101
  }
99
102
 
103
+ const DOWN_INFRA_HELP_AFTER = `
104
+ Examples:
105
+ $ aifabrix down-infra
106
+ $ aifabrix down-infra --volumes
107
+ $ aifabrix down-infra --developer-id 02
108
+ $ aifabrix down-infra -d 02 --volumes
109
+ $ aifabrix down-infra myapp --developer-id 02
110
+
111
+ --developer-id / -d target another developer's stack for this command only.
112
+ They do not change developer-id in config.
113
+ `;
114
+
100
115
  /**
101
116
  * @param {Object} options
102
117
  * @param {Object} cfg - Mutable config from getConfig()
@@ -302,20 +317,32 @@ function setupUpBuilderApiCommand(program) {
302
317
  function setupDownInfraCommand(program) {
303
318
  program.command('down-infra [service|app]')
304
319
  .description('Stop all infra, or stop one app; use -v to remove volumes')
320
+ .addHelpText('after', DOWN_INFRA_HELP_AFTER)
305
321
  .option('-v, --volumes', 'Remove volumes (deletes all data)')
306
322
  .option('--verbose', 'Show full orchestration output (default is guided summary)')
323
+ .option(
324
+ '-d, --developer <id>',
325
+ 'Target developer ID for this stop only (does not change config)'
326
+ )
327
+ .option(
328
+ '--developer-id <id>',
329
+ 'Same as --developer; target another developer without changing config'
330
+ )
307
331
  .action(async(appName, options) => {
308
332
  try {
333
+ const developerId = resolveTransientDeveloperIdFromOptions(options);
334
+ const opts = { ...options, developerId };
309
335
  if (!options.verbose) {
310
- await runGuidedDownInfra(appName, options, infra, appLib);
336
+ await runGuidedDownInfra(appName, opts, infra, appLib);
311
337
  return;
312
338
  }
313
339
 
314
340
  if (typeof appName === 'string' && appName.trim().length > 0) {
315
- await appLib.downApp(appName, { volumes: !!options.volumes });
341
+ await appLib.downApp(appName, { volumes: !!options.volumes, developerId });
342
+ } else if (options.volumes) {
343
+ await infra.stopInfraWithVolumes(developerId);
316
344
  } else {
317
- if (options.volumes) await infra.stopInfraWithVolumes();
318
- else await infra.stopInfra();
345
+ await infra.stopInfra(developerId);
319
346
  }
320
347
  } catch (error) {
321
348
  handleCommandError(error, 'down-infra');
@@ -18,26 +18,36 @@ const { addChannelCommand } = require('../channels/add-channel');
18
18
  const CHANNEL_ADD_HELP_AFTER = `
19
19
  ${formatSupportedChannelTargetsHelp()}
20
20
 
21
+ One command covers scaffold → upload → install:
22
+ $ aifabrix channel add slack --name "Elsa" --probe
23
+
21
24
  Examples:
22
- # After upload/deploy of the communication external system
23
25
  $ aifabrix channel add slack --name "Elsa"
24
26
  $ aifabrix channel add teams-bot --name "Elsa" --description "Enterprise AI assistant for employees"
25
27
  $ aifabrix channel add chatgpt --name "Your Assistant"
26
- $ aifabrix channel add microsoft-copilot --name "Your Assistant"
28
+ $ aifabrix channel add microsoft-copilot --name "Your Assistant" \\
29
+ --oauth-plugin-vault-reference-id "<Teams-Entra-SSO-auth-config-id>"
27
30
 
28
31
  # Slack slash-command prefix (registers /{prefix}-help in manifest)
29
32
  $ aifabrix channel add slack --name "Elsa" --command-prefix af
30
33
 
31
- # Generate artifacts only; no dataplane patch
34
+ # Generate artifacts only; no dataplane patch / upload
32
35
  $ aifabrix channel add slack --name "Elsa" --dry-run
33
36
 
34
- # Regenerate artifacts and patch config (does not rotate secrets)
37
+ # Re-upload communication system + regenerate channel config (does not rotate secrets)
35
38
  $ aifabrix channel add slack --name "Elsa" --force
36
39
 
37
40
  # Validate callbacks and assistant catalog after install
38
41
  $ aifabrix channel add slack --name "Elsa" --probe
39
42
 
40
- Prerequisites: aifabrix upload <target> (or deploy) and aifabrix login
43
+ Behavior:
44
+ - Scaffolds integration/<target>/ when missing (does not overwrite existing files)
45
+ - Uploads when the system is not on the dataplane; with --force uploads every time
46
+ - Then configures the channel install (artifacts, credentials, admin guide)
47
+ - microsoft-copilot uses Entra SSO (trustedIssuer); pass --oauth-plugin-vault-reference-id
48
+ from Teams Developer Portal. Does not create Fabrix oauth2 channel credentials.
49
+
50
+ Requires: aifabrix login
41
51
  `;
42
52
 
43
53
  /**
@@ -64,6 +74,7 @@ async function runChannelAddAction(channelTarget, options, meta = {}) {
64
74
  icon: options.icon,
65
75
  template: options.template,
66
76
  commandPrefix: options.commandPrefix,
77
+ oauthPluginVaultReferenceId: options.oauthPluginVaultReferenceId,
67
78
  dryRun: options.dryRun === true,
68
79
  force: options.force === true,
69
80
  probe: options.probe === true
@@ -77,7 +88,7 @@ async function runChannelAddAction(channelTarget, options, meta = {}) {
77
88
  */
78
89
  function registerChannelAddCommand(parent, meta = {}) {
79
90
  const description =
80
- 'Configure channel install (manifest, credentials, admin guide) after upload/deploy';
91
+ 'Scaffold (if needed), upload (if needed/--force), then configure channel install';
81
92
  const commandName = meta.commandName || 'add <channelTarget>';
82
93
  const cmd = meta.hidden
83
94
  ? parent.command(commandName, { hidden: true })
@@ -88,12 +99,19 @@ function registerChannelAddCommand(parent, meta = {}) {
88
99
  .option('-d, --description <text>', 'Channel description (default generated)')
89
100
  .option('--icon <path>', 'Path to channel icon asset')
90
101
  .option('--template <name>', 'Template override (default from channel target)')
91
- .option('--dry-run', 'Generate artifacts only; do not patch external system or credentials')
92
- .option('--force', 'Regenerate artifacts and patch config (does not rotate secrets)')
102
+ .option('--dry-run', 'Generate artifacts only; do not upload, patch, or set credentials')
103
+ .option(
104
+ '--force',
105
+ 'Re-upload communication system and regenerate channel config (does not rotate secrets)'
106
+ )
93
107
  .option(
94
108
  '--command-prefix <key>',
95
109
  'Slack slash alias prefix (default af) — registers /{prefix}-help etc. in manifest'
96
110
  )
111
+ .option(
112
+ '--oauth-plugin-vault-reference-id <id>',
113
+ 'Teams Developer Portal Entra SSO OAuth auth-config id (microsoft-copilot)'
114
+ )
97
115
  .option('--probe', 'After install, validate callbacks and assistant catalog')
98
116
  .addHelpText('after', CHANNEL_ADD_HELP_AFTER)
99
117
  .action(async(channelTarget, options) => {
@@ -101,7 +119,8 @@ function registerChannelAddCommand(parent, meta = {}) {
101
119
  await runChannelAddAction(channelTarget, options, meta);
102
120
  } catch (error) {
103
121
  if (!error.message?.includes('Authentication required')) {
104
- logger.error(formatBlockingError('Channel install failed:'), error.message);
122
+ const detail = error.message || String(error);
123
+ logger.error(formatBlockingError(`Channel install failed:\n${detail}`));
105
124
  }
106
125
  process.exit(1);
107
126
  }
@@ -25,15 +25,20 @@ const { registerChannelAddCommand } = require('./channel-add-setup');
25
25
 
26
26
  const CHANNEL_HELP_AFTER = `
27
27
  Subcommands:
28
- scaffold <target> Create integration/<target>/ files locally (step 1)
29
- add <target> Channel install guide + manifest/credential patch (step 3)
28
+ add <target> One command: scaffold (if needed) → upload (if needed/--force) → install
29
+ scaffold <target> Optional: create integration/<target>/ files only (local)
30
30
 
31
31
  ${formatSupportedChannelTargetsHelp()}
32
32
 
33
- Typical flow (channels are connected systems, not controller apps):
33
+ Typical flow:
34
+ aifabrix channel add slack --name "Elsa" --probe
35
+
36
+ # Force re-upload of the communication system, then reinstall channel
37
+ aifabrix channel add slack --name "Elsa" --force --probe
38
+
39
+ Optional low-level steps (usually unnecessary):
34
40
  aifabrix channel scaffold slack
35
41
  aifabrix upload slack --force
36
- aifabrix channel add slack --name "Elsa" --probe
37
42
 
38
43
  Controller pipeline apps only: aifabrix app register · aifabrix app list
39
44
 
@@ -43,10 +48,10 @@ Requires: aifabrix login (for add)
43
48
  const CHANNEL_SCAFFOLD_HELP_AFTER = `
44
49
  ${formatSupportedChannelTargetsHelp()}
45
50
 
46
- Typical flow:
47
- aifabrix channel scaffold slack
48
- aifabrix upload slack --force
51
+ Prefer one command (scaffold + upload + install):
49
52
  aifabrix channel add slack --name "Your Assistant" --probe
53
+
54
+ scaffold alone only writes local files under integration/<target>/.
50
55
  `;
51
56
 
52
57
  /**
@@ -56,7 +61,7 @@ function setupChannelCommands(program) {
56
61
  const channel = program
57
62
  .command('channel')
58
63
  .description(
59
- 'Assistant channels (Slack, Teams, Copilot, ChatGPT) — scaffold integration and install'
64
+ 'Assistant channels — one-command add (scaffold → upload → install), plus optional scaffold'
60
65
  )
61
66
  .addHelpText('after', CHANNEL_HELP_AFTER);
62
67
 
@@ -86,8 +91,7 @@ function setupChannelCommands(program) {
86
91
  logger.log(metadata(file));
87
92
  }
88
93
  logger.log('');
89
- logger.log(chalk.white('Next steps:'));
90
- logger.log(chalk.gray(` aifabrix upload ${channelTarget} --force`));
94
+ logger.log(chalk.white('Next step (recommended):'));
91
95
  logger.log(
92
96
  chalk.gray(` aifabrix channel add ${channelTarget} --name "Your Assistant" --probe`)
93
97
  );
@@ -0,0 +1,158 @@
1
+ /**
2
+ * CLI: integration-definition validate | apply
3
+ * @fileoverview BT → Builder BID commands (no section PATCH orchestration)
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ /* eslint-disable no-console */
9
+
10
+ const chalk = require('chalk');
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const {
14
+ applyIntegrationDefinition,
15
+ validateIntegrationDefinition,
16
+ withDefinitionHash
17
+ } = require('../integration-definition');
18
+ const { handleCommandError } = require('../utils/cli-utils');
19
+ const {
20
+ formatSuccessLine,
21
+ formatBlockingError,
22
+ failureGlyph
23
+ } = require('../utils/cli-layout-chalk');
24
+
25
+ const PARENT_HELP_AFTER = `
26
+
27
+ Examples:
28
+ $ aifabrix integration-definition validate ./bid.json
29
+ $ aifabrix integration-definition apply ./bid.json --dry-run
30
+
31
+ BID is the minimal business create language (systemKind, resourceType, propertyKeys).
32
+ Apply materializes integration/<systemKey>/ package files (EDS/system).
33
+ AI catalog: GET /help?topic=resourceTypeCatalog on builder-api.
34
+ `;
35
+
36
+ const VALIDATE_HELP_AFTER = `
37
+
38
+ Examples:
39
+ $ aifabrix integration-definition validate ./bid.json
40
+ $ aifabrix integration-definition validate ./bid.json --fix-hash
41
+
42
+ Checks JSON Schema, definitionHash, product rules, and active resourceType catalog keys.
43
+ `;
44
+
45
+ const APPLY_HELP_AFTER = `
46
+
47
+ Examples:
48
+ $ aifabrix integration-definition apply ./bid.json
49
+ $ aifabrix integration-definition apply ./bid.json --dry-run
50
+ $ aifabrix integration-definition apply ./bid.json --force
51
+ $ aifabrix integration-definition apply ./bid.json --output-dir /tmp/out
52
+ `;
53
+
54
+ /**
55
+ * @param {string} filePath
56
+ * @returns {Record<string, unknown>}
57
+ */
58
+ function loadBidFile(filePath) {
59
+ const abs = path.resolve(filePath);
60
+ if (!fs.existsSync(abs)) {
61
+ throw new Error(`BID file not found: ${abs}`);
62
+ }
63
+ return JSON.parse(fs.readFileSync(abs, 'utf8'));
64
+ }
65
+
66
+ /**
67
+ * @param {string} file
68
+ * @param {{ fixHash?: boolean }} options
69
+ * @returns {Promise<void>}
70
+ */
71
+ async function runValidateCommand(file, options) {
72
+ let bid = loadBidFile(file);
73
+ if (options.fixHash) {
74
+ bid = withDefinitionHash(bid);
75
+ fs.writeFileSync(path.resolve(file), `${JSON.stringify(bid, null, 2)}\n`, 'utf8');
76
+ console.log(formatSuccessLine(`Wrote definitionHash to ${file}`));
77
+ }
78
+ const result = validateIntegrationDefinition(bid);
79
+ if (!result.valid) {
80
+ console.error(formatBlockingError('BID invalid'));
81
+ for (const err of result.errors) {
82
+ console.error(chalk.red(` ${failureGlyph()} ${err}`));
83
+ }
84
+ process.exit(1);
85
+ }
86
+ console.log(formatSuccessLine('BID valid'));
87
+ console.log(` definitionId: ${result.bid.definitionId}`);
88
+ console.log(` definitionHash: ${result.bid.definitionHash}`);
89
+ }
90
+
91
+ /**
92
+ * @param {string} file
93
+ * @param {{ outputDir?: string, dryRun?: boolean, force?: boolean }} options
94
+ * @returns {Promise<void>}
95
+ */
96
+ async function runApplyCommand(file, options) {
97
+ const bid = loadBidFile(file);
98
+ const out = await applyIntegrationDefinition(bid, {
99
+ outputDir: options.outputDir,
100
+ dryRun: Boolean(options.dryRun),
101
+ force: Boolean(options.force)
102
+ });
103
+ console.log(formatSuccessLine(`BID apply ${out.status}`));
104
+ console.log(` systemKey: ${out.systemKey}`);
105
+ console.log(` outputDir: ${out.outputDir}`);
106
+ console.log(` definitionHash: ${out.definitionHash}`);
107
+ if (out.files) {
108
+ console.log(` files: ${out.files.join(', ')}`);
109
+ }
110
+ }
111
+
112
+ /**
113
+ * @param {import('commander').Command} program
114
+ */
115
+ function setupIntegrationDefinitionCommands(program) {
116
+ const cmd = program
117
+ .command('integration-definition')
118
+ .description(
119
+ 'Validate or apply a Builder Integration Definition (BID) — minimal business create language for integration packages'
120
+ )
121
+ .addHelpText('after', PARENT_HELP_AFTER);
122
+
123
+ cmd
124
+ .command('validate <file>')
125
+ .description(
126
+ 'Validate BID JSON (schema, definitionHash, product rules, resourceType catalog)'
127
+ )
128
+ .option('--fix-hash', 'Rewrite file with computed definitionHash')
129
+ .addHelpText('after', VALIDATE_HELP_AFTER)
130
+ .action(async(file, options) => {
131
+ try {
132
+ await runValidateCommand(file, options);
133
+ } catch (error) {
134
+ handleCommandError(error, 'integration-definition validate');
135
+ }
136
+ });
137
+
138
+ cmd
139
+ .command('apply <file>')
140
+ .description(
141
+ 'Idempotently apply BID to integration/<systemKey>/ (Builder-owned EDS/system writes)'
142
+ )
143
+ .option('--output-dir <dir>', 'Override output directory')
144
+ .option('--force', 'Rewrite package even when definitionHash matches stamp')
145
+ .option('--dry-run', 'Show planned files without writing')
146
+ .addHelpText('after', APPLY_HELP_AFTER)
147
+ .action(async(file, options) => {
148
+ try {
149
+ await runApplyCommand(file, options);
150
+ } catch (error) {
151
+ handleCommandError(error, 'integration-definition apply');
152
+ }
153
+ });
154
+ }
155
+
156
+ module.exports = {
157
+ setupIntegrationDefinitionCommands
158
+ };
@@ -28,6 +28,7 @@ const ALLOWED_REPAIR_AUTH = [
28
28
  'oidc',
29
29
  'hmac',
30
30
  'none',
31
+ 'system',
31
32
  'bearerforward'
32
33
  ];
33
34
 
@@ -57,8 +58,12 @@ function isAllowedRepairAuth(authOption) {
57
58
  */
58
59
  function buildAuthenticationForRepair(systemKey, authOption) {
59
60
  const normalized = normalizeRepairAuthOption(authOption);
61
+ if (normalized === 'system') {
62
+ return buildSystemAuthentication();
63
+ }
60
64
  if (normalized === 'bearerforward') {
61
- return buildBearerForwardAuthentication();
65
+ // Legacy alias: prefer authType system for platform facades.
66
+ return buildSystemAuthentication();
62
67
  }
63
68
  const schemaMethod =
64
69
  normalized === 'queryparam'
@@ -70,11 +75,19 @@ function buildAuthenticationForRepair(systemKey, authOption) {
70
75
  }
71
76
 
72
77
  /**
73
- * Facade APIs that forward the caller Bearer token — no workspace kv secret.
74
- * @returns {{ authType: 'none' }}
78
+ * Platform-managed downstream authentication (no workspace kv secret).
79
+ * @returns {{ authType: 'system' }}
80
+ */
81
+ function buildSystemAuthentication() {
82
+ return { authType: 'system' };
83
+ }
84
+
85
+ /**
86
+ * @deprecated Use buildSystemAuthentication — bearerForward is not a public authType.
87
+ * @returns {{ authType: 'system' }}
75
88
  */
76
89
  function buildBearerForwardAuthentication() {
77
- return { authType: 'none' };
90
+ return buildSystemAuthentication();
78
91
  }
79
92
 
80
93
  /**
@@ -99,7 +112,7 @@ function mergePreservedAuthVariables(mergedVariables, existingVars) {
99
112
  function repairAuthChangeLabel(authOption) {
100
113
  const normalized = normalizeRepairAuthOption(authOption);
101
114
  if (normalized === 'bearertoken' || normalized === 'bearerkey') return 'bearerToken';
102
- if (normalized === 'bearerforward') return 'bearerForward';
115
+ if (normalized === 'bearerforward') return 'system';
103
116
  return normalized;
104
117
  }
105
118
 
@@ -157,6 +170,7 @@ module.exports = {
157
170
  normalizeRepairAuthOption,
158
171
  isAllowedRepairAuth,
159
172
  buildAuthenticationForRepair,
173
+ buildSystemAuthentication,
160
174
  buildBearerForwardAuthentication,
161
175
  mergePreservedAuthVariables,
162
176
  repairAuthChangeLabel,