@aifabrix/builder 2.46.0 → 2.47.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 (191) hide show
  1. package/.npmrc.token +1 -0
  2. package/.temp/jest-protection-convert/run-IFN2nv/.protection/hubspot-companies.json +1 -0
  3. package/.temp/jest-protection-convert/run-IvmKLO/.protection/hubspot-companies.yaml +17 -0
  4. package/.temp/jest-protection-convert/run-SdvblF/.protection/hubspot-companies.json +1 -0
  5. package/.temp/jest-protection-convert/run-b51TsW/.protection/hubspot-companies.json +1 -0
  6. package/.temp/jest-protection-convert/run-fiLmb1/.protection/hubspot-companies.json +1 -0
  7. package/.temp/jest-protection-convert/run-qYGHMU/.protection/hubspot-companies.yaml +17 -0
  8. package/.temp/jest-protection-fixtures/hubspot-Mq5re8/hubspot-companies.yaml +17 -0
  9. package/.temp/jest-protection-fixtures/hubspot-f3u4YL/hubspot-companies.yaml +17 -0
  10. package/.temp/jest-protection-fixtures/hubspot-iQtd1O/hubspot-companies.yaml +17 -0
  11. package/.temp/jest-protection-fixtures/hubspot-mdmz5M/hubspot-companies.yaml +17 -0
  12. package/.temp/jest-protection-fixtures/hubspot-qbKYxQ/hubspot-companies.yaml +17 -0
  13. package/.temp/jest-protection-fixtures/hubspot-yzlJy6/hubspot-companies.yaml +17 -0
  14. package/.temp/validation/summary.txt +5 -0
  15. package/jest.isolated-projects.js +18 -2
  16. package/jest.projects.js +24 -0
  17. package/lib/api/dataplane-health.api.js +19 -7
  18. package/lib/api/lifecycle.api.js +22 -1
  19. package/lib/api/resource-types.api.js +107 -0
  20. package/lib/api/runtime-configuration.api.js +72 -0
  21. package/lib/api/types/resource-types.types.js +45 -0
  22. package/lib/api/user-groups.api.js +9 -9
  23. package/lib/api/users.api.js +8 -8
  24. package/lib/api/wizard.api.js +10 -2
  25. package/lib/app/down.js +58 -26
  26. package/lib/app/index.js +2 -2
  27. package/lib/app/run-resolve-image.js +2 -1
  28. package/lib/app/run-stop-containers.js +113 -0
  29. package/lib/app/run.js +1 -31
  30. package/lib/app/show-display.js +35 -0
  31. package/lib/app/show.js +32 -1
  32. package/lib/build/index.js +17 -5
  33. package/lib/build/miso-controller-npm-sdk-guard.js +25 -0
  34. package/lib/channels/add-channel.js +348 -0
  35. package/lib/channels/channel-artifact-generator.js +141 -0
  36. package/lib/channels/channel-audit.js +101 -0
  37. package/lib/channels/channel-auth-context.js +37 -0
  38. package/lib/channels/channel-credential.js +200 -0
  39. package/lib/channels/channel-display-name.js +64 -0
  40. package/lib/channels/channel-external-system.js +106 -0
  41. package/lib/channels/channel-integration-generator.js +274 -0
  42. package/lib/channels/channel-probe.js +89 -0
  43. package/lib/channels/channel-runtime-resolver.js +152 -0
  44. package/lib/channels/channel-status-display.js +141 -0
  45. package/lib/channels/channel-targets.js +115 -0
  46. package/lib/cli/index.js +18 -4
  47. package/lib/cli/infra-guided-auth.js +36 -12
  48. package/lib/cli/setup-app.js +54 -3
  49. package/lib/cli/setup-infra-up-dataplane-action.js +2 -2
  50. package/lib/cli/setup-infra-up-platform-action.js +2 -1
  51. package/lib/cli/setup-npm.js +59 -0
  52. package/lib/cli/setup-platform.js +1 -1
  53. package/lib/cli/setup-secrets.js +1 -1
  54. package/lib/cli/setup-utility-help-after.js +4 -1
  55. package/lib/cli/setup-utility-resolve.js +8 -5
  56. package/lib/commands/app-logs.js +54 -26
  57. package/lib/commands/app.js +41 -0
  58. package/lib/commands/auth-status-dataplane-version.js +97 -61
  59. package/lib/commands/auth-status.js +2 -7
  60. package/lib/commands/channel.js +58 -0
  61. package/lib/commands/datasource.js +1 -1
  62. package/lib/commands/identity-apply-sync.js +49 -8
  63. package/lib/commands/identity-group-cli.js +66 -26
  64. package/lib/commands/identity-membership-role-cache.js +58 -37
  65. package/lib/commands/identity-shared.js +22 -1
  66. package/lib/commands/identity-user-cli.js +80 -34
  67. package/lib/commands/login-device.js +11 -3
  68. package/lib/commands/login.js +8 -3
  69. package/lib/commands/npm-config.js +103 -0
  70. package/lib/commands/repair-auth-apply.js +30 -4
  71. package/lib/commands/repair-auth-config.js +8 -13
  72. package/lib/commands/repair-datasource-com-v2.js +15 -0
  73. package/lib/commands/repair-datasource-service-contract.js +206 -0
  74. package/lib/commands/repair-datasource.js +53 -18
  75. package/lib/commands/repair-env-template.js +12 -12
  76. package/lib/commands/repair-openapi-sync.js +26 -4
  77. package/lib/commands/repair-steps.js +28 -13
  78. package/lib/commands/repair-system-alignment.js +7 -4
  79. package/lib/commands/resource-type.js +371 -0
  80. package/lib/commands/setup-modes.js +1 -1
  81. package/lib/commands/setup-prompts-platform-mode.js +24 -21
  82. package/lib/commands/test-trust-external.js +39 -90
  83. package/lib/commands/up-common.js +33 -1
  84. package/lib/commands/up-dataplane.js +8 -4
  85. package/lib/commands/upload.js +24 -3
  86. package/lib/commands/verify-trust-command-action.js +4 -1
  87. package/lib/commands/verify-trust-external.js +12 -20
  88. package/lib/core/config-setup-platform-mode.js +3 -3
  89. package/lib/core/setup-platform-mode.js +41 -6
  90. package/lib/datasource/capability/capability-diff-slice.js +2 -2
  91. package/lib/datasource/capability/capability-manifest.js +254 -0
  92. package/lib/datasource/capability/capability-resolve.js +3 -4
  93. package/lib/datasource/capability/copy-operations.js +5 -3
  94. package/lib/datasource/capability/remove-operations.js +2 -1
  95. package/lib/datasource/capability/run-capability-copy.js +32 -4
  96. package/lib/datasource/capability/run-capability-diff.js +3 -3
  97. package/lib/datasource/capability/run-capability-dimension.js +2 -4
  98. package/lib/datasource/capability/run-capability-edit.js +4 -7
  99. package/lib/datasource/capability/run-capability-relate.js +2 -4
  100. package/lib/datasource/capability/run-capability-remove.js +2 -4
  101. package/lib/datasource/capability/validate-capability-slice.js +2 -1
  102. package/lib/datasource/datasource-identifier.js +2 -1
  103. package/lib/datasource/datasource-validate-display.js +1 -1
  104. package/lib/datasource/field-reference-validator.js +24 -3
  105. package/lib/datasource/orphan-profiles-validator.js +98 -0
  106. package/lib/datasource/validate.js +41 -23
  107. package/lib/external-system/download-helpers.js +87 -0
  108. package/lib/external-system/download.js +61 -122
  109. package/lib/external-system/generator.js +1 -59
  110. package/lib/external-system/test.js +10 -15
  111. package/lib/generator/builders.js +11 -1
  112. package/lib/generator/external-controller-manifest.js +5 -2
  113. package/lib/generator/split.js +3 -1
  114. package/lib/generator/wizard.js +1 -28
  115. package/lib/identity/fk-reference.js +65 -0
  116. package/lib/identity/identity-apply-core.js +13 -58
  117. package/lib/identity/identity-apply-phases.js +5 -1
  118. package/lib/identity/identity-display.js +243 -0
  119. package/lib/identity/identity-response-helpers.js +52 -0
  120. package/lib/identity/identity-sync-errors.js +62 -0
  121. package/lib/identity/resolve-user-id.js +129 -0
  122. package/lib/identity/username-default.js +31 -0
  123. package/lib/lifecycle/certification-progress.js +2 -2
  124. package/lib/lifecycle/report-display-trust.js +70 -0
  125. package/lib/lifecycle/report-display.js +14 -13
  126. package/lib/resolvers/datasource-resolver.js +2 -3
  127. package/lib/resolvers/resource-type-file.js +159 -0
  128. package/lib/schema/application-schema.json +16 -0
  129. package/lib/schema/deployment-rules.yaml +2 -1
  130. package/lib/schema/external-datasource.schema.json +372 -58
  131. package/lib/schema/external-system.schema.json +331 -2
  132. package/lib/schema/type/document-storage.json +9 -5
  133. package/lib/utils/api.js +2 -1
  134. package/lib/utils/app-run-containers.js +25 -2
  135. package/lib/utils/authentication-shape.js +224 -0
  136. package/lib/utils/build-copy.js +36 -5
  137. package/lib/utils/build-resolve-image.js +66 -12
  138. package/lib/utils/celery-companion-apps.js +53 -0
  139. package/lib/utils/cli-utils.js +4 -0
  140. package/lib/utils/compose-celery-worker.js +23 -0
  141. package/lib/utils/compose-generate-docker-compose.js +9 -1
  142. package/lib/utils/configuration-env-resolver.js +36 -0
  143. package/lib/utils/controller-url.js +4 -1
  144. package/lib/utils/credential-secrets-env-kv.js +216 -0
  145. package/lib/utils/credential-secrets-env.js +52 -262
  146. package/lib/utils/dataplane-resolver.js +4 -115
  147. package/lib/utils/declarative-url-ports.js +24 -1
  148. package/lib/utils/dev-platform-tls.js +148 -0
  149. package/lib/utils/docker-build.js +1 -4
  150. package/lib/utils/external-env-template.js +10 -3
  151. package/lib/utils/external-system-validators.js +8 -2
  152. package/lib/utils/health-check-request.js +114 -0
  153. package/lib/utils/health-check-url.js +3 -1
  154. package/lib/utils/health-check.js +1 -113
  155. package/lib/utils/help-builder.js +3 -0
  156. package/lib/utils/keycloak-realm-ready.js +5 -1
  157. package/lib/utils/localhost-public-host.js +31 -0
  158. package/lib/utils/npm-user-config.js +145 -0
  159. package/lib/utils/paths.js +25 -0
  160. package/lib/utils/platform-controller-url.js +58 -4
  161. package/lib/utils/platform-kv-ref.js +57 -0
  162. package/lib/utils/runtime-configuration-download.js +189 -0
  163. package/lib/utils/runtime-configuration-values.js +119 -0
  164. package/lib/utils/schema-loader.js +19 -4
  165. package/lib/utils/url-declarative-public-base.js +33 -25
  166. package/lib/utils/worker-sidecars.js +142 -0
  167. package/lib/validation/external-manifest-validator.js +2 -0
  168. package/lib/validation/runtime-configuration-rules.js +98 -0
  169. package/lib/validation/validate-display-dimensions.js +154 -0
  170. package/lib/validation/validate-display-sections.js +276 -0
  171. package/lib/validation/validate-display-steps.js +67 -0
  172. package/lib/validation/validate-display.js +24 -402
  173. package/package.json +1 -1
  174. package/templates/applications/dataplane/application.yaml +5 -0
  175. package/templates/applications/dataplane/env.template +26 -13
  176. package/templates/applications/dataplane/rbac.yaml +108 -1
  177. package/templates/applications/miso-controller/application.yaml +2 -2
  178. package/templates/applications/miso-controller/env.template +37 -122
  179. package/templates/applications/miso-controller/rbac.yaml +28 -13
  180. package/templates/channels/chatgpt/install.template.md.hbs +49 -0
  181. package/templates/channels/chatgpt/manifest.template.json.hbs +9 -0
  182. package/templates/channels/microsoft-copilot/install.template.md.hbs +49 -0
  183. package/templates/channels/microsoft-copilot/manifest.template.json.hbs +8 -0
  184. package/templates/channels/slack/install.template.md.hbs +49 -0
  185. package/templates/channels/slack/manifest.template.json.hbs +66 -0
  186. package/templates/channels/teams-bot/install.template.md.hbs +49 -0
  187. package/templates/channels/teams-bot/manifest.template.json.hbs +13 -0
  188. package/templates/channels/web-embed/install.template.md.hbs +49 -0
  189. package/templates/channels/web-embed/manifest.template.json.hbs +8 -0
  190. package/templates/python/docker-compose.hbs +56 -0
  191. package/templates/typescript/docker-compose.hbs +52 -0
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Channel credential matrix and provisioning (plan 152).
3
+ *
4
+ * @fileoverview Ensure channel credentials exist; push secrets from integration env
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const path = require('path');
12
+ const { listCredentials } = require('../api/credentials.api');
13
+ const { credentialSelection } = require('../api/wizard.api');
14
+ const { pushCredentialSecrets } = require('../utils/credential-secrets-env');
15
+ const { getIntegrationPath } = require('../utils/paths');
16
+ const {
17
+ auditCredentialCreated,
18
+ auditCredentialUpdated,
19
+ auditFailed
20
+ } = require('./channel-audit');
21
+
22
+ /** @type {Record<string, { method: string, displayName: string, credentialConfig: Object }>} */
23
+ const CREDENTIAL_SPECS = {
24
+ slack: {
25
+ method: 'hmac',
26
+ credentialConfig: {
27
+ type: 'hmac',
28
+ signingSecret: 'pending-channel-install',
29
+ algorithm: 'sha256',
30
+ signatureHeader: 'X-Slack-Signature',
31
+ timestampHeader: 'X-Slack-Request-Timestamp',
32
+ signaturePrefix: 'v0=',
33
+ timestampMaxAge: 300
34
+ }
35
+ },
36
+ 'teams-bot': {
37
+ method: 'oidc',
38
+ credentialConfig: {
39
+ type: 'oidc',
40
+ openIdConfigUrl: 'https://login.botframework.com/v1/.well-known/openid_configuration',
41
+ clientId: 'pending-channel-install',
42
+ validateSignature: true,
43
+ algorithms: ['RS256'],
44
+ clockSkewSeconds: 300
45
+ }
46
+ },
47
+ 'microsoft-copilot': {
48
+ method: 'oauth2',
49
+ credentialConfig: {
50
+ type: 'oauth2',
51
+ grantType: 'client_credentials',
52
+ tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
53
+ clientId: 'pending-channel-install',
54
+ clientSecret: 'pending-channel-install'
55
+ }
56
+ },
57
+ chatgpt: {
58
+ method: 'apiKey',
59
+ credentialConfig: {
60
+ type: 'apiKey',
61
+ apiKey: 'pending-channel-install',
62
+ headerName: 'Authorization',
63
+ prefix: 'Bearer'
64
+ }
65
+ },
66
+ 'web-embed': {
67
+ method: 'apiKey',
68
+ credentialConfig: {
69
+ type: 'apiKey',
70
+ apiKey: 'pending-channel-install',
71
+ headerName: 'Authorization',
72
+ prefix: 'Bearer'
73
+ }
74
+ }
75
+ };
76
+
77
+ /**
78
+ * @param {Object} response
79
+ * @returns {Object[]}
80
+ */
81
+ function extractCredentials(response) {
82
+ const body = response?.data ?? response;
83
+ const data = body?.data ?? body;
84
+ const items = data?.credentials ?? data?.items ?? (Array.isArray(data) ? data : []);
85
+ return Array.isArray(items) ? items : [];
86
+ }
87
+
88
+ /**
89
+ * @param {string} channelTarget
90
+ * @param {typeof import('./channel-targets').CHANNEL_TARGETS[string]} target
91
+ * @returns {{ method: ChannelCredentialMethod, displayName: string, credentialConfig: Object }}
92
+ */
93
+ function buildCredentialSpec(channelTarget, target) {
94
+ const base = CREDENTIAL_SPECS[channelTarget];
95
+ if (!base) {
96
+ throw new Error(`No credential spec for channel target "${channelTarget}"`);
97
+ }
98
+ return {
99
+ method: base.method,
100
+ displayName: `${target.key} assistant channel`,
101
+ credentialConfig: base.credentialConfig
102
+ };
103
+ }
104
+
105
+ /**
106
+ * @async
107
+ * @param {Object} ctx
108
+ * @param {string} credentialKey
109
+ * @returns {Promise<Object|null>}
110
+ */
111
+ async function findCredentialByKey(ctx, credentialKey) {
112
+ const response = await listCredentials(ctx.dataplaneUrl, ctx.authConfig, { pageSize: 200 });
113
+ const items = extractCredentials(response);
114
+ return items.find((item) => item.key === credentialKey || item.credentialKey === credentialKey) || null;
115
+ }
116
+
117
+ /**
118
+ * @async
119
+ * @param {Object} ctx
120
+ * @param {string} channelTarget
121
+ * @param {Object} target
122
+ * @returns {Promise<Object>}
123
+ */
124
+ async function createChannelCredential(ctx, channelTarget, target) {
125
+ const spec = buildCredentialSpec(channelTarget, target);
126
+ const payload = {
127
+ action: 'create',
128
+ credentialConfig: {
129
+ key: target.credentialKey,
130
+ displayName: spec.displayName,
131
+ description: 'Assistant channel credential (secrets via kv:// env.template)',
132
+ credentialConfig: spec.credentialConfig,
133
+ isActive: true
134
+ }
135
+ };
136
+ const response = await credentialSelection(ctx.dataplaneUrl, ctx.authConfig, payload);
137
+ auditCredentialCreated(channelTarget, { credentialKey: target.credentialKey, method: spec.method });
138
+ return { created: true, credentialIdOrKey: response?.data?.credentialIdOrKey || target.credentialKey };
139
+ }
140
+
141
+ /**
142
+ * @async
143
+ * @param {Object} ctx
144
+ * @param {string} channelTarget
145
+ * @returns {Promise<{ pushed: number }>}
146
+ */
147
+ async function pushIntegrationSecrets(ctx, channelTarget) {
148
+ const integrationDir = getIntegrationPath(channelTarget);
149
+ const envPath = path.join(integrationDir, '.env');
150
+ const result = await pushCredentialSecrets(ctx.dataplaneUrl, ctx.authConfig, {
151
+ envFilePath: envPath,
152
+ appName: channelTarget,
153
+ quiet: true
154
+ });
155
+ return { pushed: result?.pushed ?? 0, skipped: result?.skipped === true };
156
+ }
157
+
158
+ /**
159
+ * Ensure channel credential record exists; optionally push secrets from integration/.env.
160
+ * Does not rotate secrets when force is set (plan 152 V1).
161
+ *
162
+ * @async
163
+ * @param {Object} params
164
+ * @param {Object} params.ctx
165
+ * @param {string} params.channelTarget
166
+ * @param {Object} params.target
167
+ * @param {boolean} [params.force]
168
+ * @returns {Promise<Object>}
169
+ */
170
+ async function ensureChannelCredential(params) {
171
+ const { ctx, channelTarget, target, force = false } = params;
172
+ const existing = await findCredentialByKey(ctx, target.credentialKey);
173
+ if (existing) {
174
+ auditCredentialUpdated(channelTarget, {
175
+ credentialKey: target.credentialKey,
176
+ rotated: false,
177
+ force: force === true
178
+ });
179
+ const secretPush = await pushIntegrationSecrets(ctx, channelTarget);
180
+ return { existed: true, credentialKey: target.credentialKey, secretPush };
181
+ }
182
+
183
+ try {
184
+ const created = await createChannelCredential(ctx, channelTarget, target);
185
+ const secretPush = await pushIntegrationSecrets(ctx, channelTarget);
186
+ return { ...created, credentialKey: target.credentialKey, secretPush };
187
+ } catch (err) {
188
+ auditFailed(channelTarget, { phase: 'credential', error: err.message });
189
+ throw err;
190
+ }
191
+ }
192
+
193
+ module.exports = {
194
+ buildCredentialSpec,
195
+ extractCredentials,
196
+ findCredentialByKey,
197
+ createChannelCredential,
198
+ pushIntegrationSecrets,
199
+ ensureChannelCredential
200
+ };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Environment-scoped assistant channel display names (plan 152).
3
+ *
4
+ * @fileoverview Base name (e.g. Elsa) → Elsa (dev) | Elsa (tst) | Elsa in production
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const ENV_SUFFIX_PATTERN = /\s+\((dev|tst|test|pro|prod|production)\)$/i;
12
+
13
+ /**
14
+ * Strip a trailing environment suffix so --name can be re-run safely.
15
+ * @param {string} name
16
+ * @returns {string}
17
+ */
18
+ function normalizeBaseChannelName(name) {
19
+ if (!name || typeof name !== 'string') {
20
+ throw new Error('Channel base name is required');
21
+ }
22
+ const trimmed = name.trim();
23
+ if (!trimmed) {
24
+ throw new Error('Channel base name is required');
25
+ }
26
+ return trimmed.replace(ENV_SUFFIX_PATTERN, '').trim();
27
+ }
28
+
29
+ /**
30
+ * @param {string} environment
31
+ * @returns {boolean}
32
+ */
33
+ function isProductionEnvironment(environment) {
34
+ const env = String(environment || '').toLowerCase();
35
+ return env === 'pro' || env === 'prod' || env === 'production';
36
+ }
37
+
38
+ /**
39
+ * Resolve customer-visible channel name for the active environment.
40
+ * @param {string} baseName - Canonical product name (e.g. Elsa)
41
+ * @param {string} environment - dev | tst | pro
42
+ * @returns {string}
43
+ */
44
+ function resolveChannelDisplayName(baseName, environment) {
45
+ const base = normalizeBaseChannelName(baseName);
46
+ const env = String(environment || 'dev').toLowerCase();
47
+
48
+ if (isProductionEnvironment(env)) {
49
+ return base;
50
+ }
51
+ if (env === 'dev') {
52
+ return `${base} (dev)`;
53
+ }
54
+ if (env === 'tst' || env === 'test') {
55
+ return `${base} (tst)`;
56
+ }
57
+ return `${base} (${env})`;
58
+ }
59
+
60
+ module.exports = {
61
+ normalizeBaseChannelName,
62
+ isProductionEnvironment,
63
+ resolveChannelDisplayName
64
+ };
@@ -0,0 +1,106 @@
1
+ /**
2
+ * External system channel config helpers (plan 152).
3
+ *
4
+ * @fileoverview Patch config.extensions.channel on deployed communication systems
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const { getExternalSystem, updateExternalSystem } = require('../api/external-systems.api');
12
+ const { unwrapApiData } = require('../utils/external-system-readiness-core');
13
+
14
+ /**
15
+ * @param {Object|null} system
16
+ * @returns {Object|null}
17
+ */
18
+ function getExistingChannelExtension(system) {
19
+ if (!system || typeof system !== 'object') return null;
20
+ const channel = system.config?.extensions?.channel;
21
+ return channel && typeof channel === 'object' ? channel : null;
22
+ }
23
+
24
+ /**
25
+ * @param {Object} params
26
+ * @param {typeof import('./channel-targets').CHANNEL_TARGETS[string]} target
27
+ * @param {string} params.name
28
+ * @param {string} params.baseName
29
+ * @param {string} params.description
30
+ * @param {string} params.artifactHash
31
+ * @param {string} params.status
32
+ * @returns {Object}
33
+ */
34
+ function buildChannelExtensionBlock(params, target) {
35
+ return {
36
+ type: target.channelType,
37
+ name: params.name,
38
+ baseName: params.baseName,
39
+ description: params.description,
40
+ capabilities: [...target.capabilities],
41
+ status: params.status,
42
+ artifactHash: params.artifactHash,
43
+ credentialKey: target.credentialKey
44
+ };
45
+ }
46
+
47
+ /**
48
+ * @param {Object} system
49
+ * @param {Object} channelBlock
50
+ * @returns {Object}
51
+ */
52
+ function mergeChannelIntoSystem(system, channelBlock) {
53
+ const config = system.config && typeof system.config === 'object' ? { ...system.config } : {};
54
+ const extensions =
55
+ config.extensions && typeof config.extensions === 'object' ? { ...config.extensions } : {};
56
+ extensions.channel = channelBlock;
57
+ config.extensions = extensions;
58
+ return { ...system, config, displayName: channelBlock.name };
59
+ }
60
+
61
+ /**
62
+ * @async
63
+ * @param {Object} ctx
64
+ * @param {string} ctx.dataplaneUrl
65
+ * @param {Object} ctx.authConfig
66
+ * @param {string} ctx.channelTarget
67
+ * @returns {Promise<Object>}
68
+ */
69
+ async function fetchCommunicationSystem(ctx) {
70
+ const res = await getExternalSystem(ctx.dataplaneUrl, ctx.channelTarget, ctx.authConfig);
71
+ const system = unwrapApiData(res);
72
+ if (!system || typeof system !== 'object') {
73
+ throw new Error(
74
+ `Communication external system "${ctx.channelTarget}" was not found. ` +
75
+ `Run: aifabrix deploy ${ctx.channelTarget}`
76
+ );
77
+ }
78
+ const kind = system.systemKind || system.systemKindKey;
79
+ if (kind && kind !== 'communication') {
80
+ throw new Error(
81
+ `External system "${ctx.channelTarget}" is not systemKind communication (found: ${kind}).`
82
+ );
83
+ }
84
+ return system;
85
+ }
86
+
87
+ /**
88
+ * @async
89
+ * @param {Object} ctx
90
+ * @param {Object} channelBlock
91
+ * @returns {Promise<Object>}
92
+ */
93
+ async function patchChannelExtension(ctx, channelBlock) {
94
+ const system = await fetchCommunicationSystem(ctx);
95
+ const merged = mergeChannelIntoSystem(system, channelBlock);
96
+ const res = await updateExternalSystem(ctx.dataplaneUrl, ctx.channelTarget, ctx.authConfig, merged);
97
+ return unwrapApiData(res) || merged;
98
+ }
99
+
100
+ module.exports = {
101
+ getExistingChannelExtension,
102
+ buildChannelExtensionBlock,
103
+ mergeChannelIntoSystem,
104
+ fetchCommunicationSystem,
105
+ patchChannelExtension
106
+ };
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Generate integration/<channelTarget>/ for communication external systems (plan 152).
3
+ *
4
+ * @fileoverview Builder generator for channel deploy step 1 scaffolds
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 { getChannelTarget } = require('./channel-targets');
14
+ const { getIntegrationPath } = require('../utils/paths');
15
+
16
+ /** @type {Record<string, Object>} */
17
+ const AUTH_BLOCKS = {
18
+ slack: {
19
+ authType: 'credential',
20
+ credential: {
21
+ credentialKey: 'slack-app',
22
+ displayName: 'Slack assistant channel',
23
+ method: 'hmac',
24
+ variables: {},
25
+ security: {
26
+ signingSecret: 'kv://slack/signingSecret',
27
+ clientId: 'kv://slack/clientId',
28
+ clientSecret: 'kv://slack/clientSecret'
29
+ },
30
+ writeOnce: true
31
+ }
32
+ },
33
+ 'teams-bot': {
34
+ authType: 'credential',
35
+ credential: {
36
+ credentialKey: 'teams-bot',
37
+ displayName: 'Microsoft Teams assistant bot',
38
+ method: 'oidc',
39
+ variables: {
40
+ openIdConfigUrl: 'https://login.botframework.com/v1/.well-known/openid_configuration',
41
+ clientId: 'kv://teams-bot/botAppId',
42
+ expectedIssuer: 'https://api.botframework.com'
43
+ },
44
+ security: {},
45
+ writeOnce: true
46
+ }
47
+ },
48
+ 'microsoft-copilot': {
49
+ authType: 'credential',
50
+ credential: {
51
+ credentialKey: 'microsoft-copilot-channel',
52
+ displayName: 'Microsoft Copilot assistant channel',
53
+ method: 'oauth2',
54
+ variables: {
55
+ tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
56
+ grantType: 'client_credentials'
57
+ },
58
+ security: {
59
+ clientId: 'kv://microsoft-copilot/clientId',
60
+ clientSecret: 'kv://microsoft-copilot/clientSecret'
61
+ },
62
+ writeOnce: true
63
+ }
64
+ },
65
+ chatgpt: {
66
+ authType: 'credential',
67
+ credential: {
68
+ credentialKey: 'chatgpt-channel',
69
+ displayName: 'ChatGPT assistant channel',
70
+ method: 'apikey',
71
+ variables: {},
72
+ security: {
73
+ apiKey: 'kv://chatgpt/apiKey'
74
+ },
75
+ writeOnce: true
76
+ }
77
+ },
78
+ 'web-embed': {
79
+ authType: 'credential',
80
+ credential: {
81
+ credentialKey: 'web-embed-channel',
82
+ displayName: 'Web embed assistant channel',
83
+ method: 'apikey',
84
+ variables: {},
85
+ security: {
86
+ apiKey: 'kv://web-embed/apiKey'
87
+ },
88
+ writeOnce: true
89
+ }
90
+ }
91
+ };
92
+
93
+ /**
94
+ * @param {string} channelTarget
95
+ * @param {typeof import('./channel-targets').CHANNEL_TARGETS[string]} target
96
+ * @returns {Object}
97
+ */
98
+ function buildAuthenticationBlock(channelTarget, target) {
99
+ const block = AUTH_BLOCKS[channelTarget];
100
+ if (!block) {
101
+ throw new Error(`No authentication block for channel target "${channelTarget}"`);
102
+ }
103
+ const auth = JSON.parse(JSON.stringify(block));
104
+ auth.credential.credentialKey = target.credentialKey;
105
+ return auth;
106
+ }
107
+
108
+ /**
109
+ * @param {string} channelTarget
110
+ * @returns {Object}
111
+ */
112
+ function buildSystemJson(channelTarget) {
113
+ const target = getChannelTarget(channelTarget);
114
+ const system = {
115
+ key: channelTarget,
116
+ displayName: 'Microsoft Teams assistant bot',
117
+ description: 'Bot Framework assistant channel (not full Teams / Graph integration)',
118
+ type: 'custom',
119
+ systemKind: 'communication',
120
+ enabled: true,
121
+ authentication: buildAuthenticationBlock(channelTarget, target),
122
+ configuration: [],
123
+ dataSources: []
124
+ };
125
+
126
+ if (channelTarget === 'teams-bot') {
127
+ system.displayName = 'Microsoft Teams assistant bot';
128
+ system.endpoints = [
129
+ {
130
+ type: 'messages',
131
+ path: '/messages',
132
+ router: 'teams_bot_router',
133
+ description: 'Bot Framework messaging endpoint',
134
+ active: true
135
+ },
136
+ {
137
+ type: 'notifications',
138
+ path: '/notifications',
139
+ router: 'teams_notifications_router',
140
+ description: 'Proactive notification endpoint',
141
+ active: true
142
+ },
143
+ {
144
+ type: 'config',
145
+ path: '/bot-config',
146
+ router: 'teams_config_router',
147
+ description: 'Bot configuration endpoint',
148
+ active: true
149
+ }
150
+ ];
151
+ system.endpointsActive = true;
152
+ } else {
153
+ system.displayName = `${target.key} assistant channel`;
154
+ system.description = 'Communication external system for assistant channel install (plan 152)';
155
+ }
156
+
157
+ return system;
158
+ }
159
+
160
+ /**
161
+ * @param {string} channelTarget
162
+ * @returns {Object}
163
+ */
164
+ function buildApplicationJson(channelTarget) {
165
+ const target = getChannelTarget(channelTarget);
166
+ const systemFile = `${channelTarget}-system.json`;
167
+ return {
168
+ app: {
169
+ key: channelTarget,
170
+ displayName: `${target.key} assistant channel`,
171
+ description: 'Assistant communication channel — deploy then app add-channel',
172
+ type: 'external'
173
+ },
174
+ externalIntegration: {
175
+ schemaBasePath: './',
176
+ systems: [systemFile],
177
+ dataSources: [],
178
+ autopublish: true,
179
+ version: '1.0.0'
180
+ }
181
+ };
182
+ }
183
+
184
+ /**
185
+ * @param {string} channelTarget
186
+ * @returns {string}
187
+ */
188
+ function buildEnvTemplate(channelTarget) {
189
+ const kvPrefix = channelTarget === 'web-embed' ? 'web-embed' : channelTarget;
190
+ const lines = [
191
+ `# ${channelTarget} assistant channel — resolve secrets before deploy/add-channel`,
192
+ '# kv:// paths are pushed via aifabrix credential push or add-channel secret sync',
193
+ ''
194
+ ];
195
+ if (channelTarget === 'slack') {
196
+ lines.push('KV_SLACK_SIGNINGSECRET=kv://slack/signingSecret');
197
+ lines.push('KV_SLACK_CLIENTID=kv://slack/clientId');
198
+ lines.push('KV_SLACK_CLIENTSECRET=kv://slack/clientSecret');
199
+ } else if (channelTarget === 'teams-bot') {
200
+ lines.push('KV_TEAMS_BOT_BOTAPPID=kv://teams-bot/botAppId');
201
+ } else if (channelTarget === 'microsoft-copilot') {
202
+ lines.push('KV_MICROSOFT_COPILOT_CLIENTID=kv://microsoft-copilot/clientId');
203
+ lines.push('KV_MICROSOFT_COPILOT_CLIENTSECRET=kv://microsoft-copilot/clientSecret');
204
+ } else {
205
+ lines.push(`KV_${kvPrefix.toUpperCase().replace(/-/g, '_')}_APIKEY=kv://${kvPrefix}/apiKey`);
206
+ }
207
+ lines.push('');
208
+ return lines.join('\n');
209
+ }
210
+
211
+ /**
212
+ * @param {string} channelTarget
213
+ * @param {Object} [options]
214
+ * @param {boolean} [options.force]
215
+ * @param {boolean} [options.allowExperimental]
216
+ * @param {string} [options.projectRoot]
217
+ * @returns {{ integrationDir: string, files: string[] }}
218
+ */
219
+ function scaffoldChannelIntegration(channelTarget, options = {}) {
220
+ const target = getChannelTarget(channelTarget);
221
+ if (target.experimental && !options.allowExperimental) {
222
+ throw new Error(
223
+ `Channel target "${channelTarget}" is experimental. Pass --allow-experimental to scaffold.`
224
+ );
225
+ }
226
+
227
+ const integrationDir = options.projectRoot
228
+ ? path.join(path.resolve(options.projectRoot), 'integration', channelTarget)
229
+ : getIntegrationPath(channelTarget);
230
+ if (fs.existsSync(integrationDir) && !options.force) {
231
+ throw new Error(
232
+ `Integration folder already exists: ${integrationDir}. Use --force to overwrite generated files.`
233
+ );
234
+ }
235
+
236
+ fs.mkdirSync(integrationDir, { recursive: true });
237
+ const systemPath = path.join(integrationDir, `${channelTarget}-system.json`);
238
+ const appPath = path.join(integrationDir, 'application.json');
239
+ const envPath = path.join(integrationDir, 'env.template');
240
+ const readmePath = path.join(integrationDir, 'README.md');
241
+
242
+ fs.writeFileSync(systemPath, `${JSON.stringify(buildSystemJson(channelTarget), null, 2)}\n`, 'utf8');
243
+ fs.writeFileSync(appPath, `${JSON.stringify(buildApplicationJson(channelTarget), null, 2)}\n`, 'utf8');
244
+ fs.writeFileSync(envPath, buildEnvTemplate(channelTarget), 'utf8');
245
+ fs.writeFileSync(
246
+ readmePath,
247
+ [
248
+ `# ${channelTarget} assistant channel`,
249
+ '',
250
+ '## Two-step install',
251
+ '',
252
+ '```bash',
253
+ `aifabrix deploy ${channelTarget}`,
254
+ `aifabrix app add-channel ${channelTarget} --name "Your Assistant"`,
255
+ '```',
256
+ '',
257
+ 'Generated by `aifabrix channel scaffold`. Secrets belong in kv:// env.template only.',
258
+ ''
259
+ ].join('\n'),
260
+ 'utf8'
261
+ );
262
+
263
+ return {
264
+ integrationDir,
265
+ files: [systemPath, appPath, envPath, readmePath]
266
+ };
267
+ }
268
+
269
+ module.exports = {
270
+ buildSystemJson,
271
+ buildApplicationJson,
272
+ buildEnvTemplate,
273
+ scaffoldChannelIntegration
274
+ };