@aifabrix/builder 2.51.1 → 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 (187) 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/app/run-container-start.js +4 -0
  10. package/lib/app/run-deploy-job.js +135 -27
  11. package/lib/app/run-env-compose.js +32 -0
  12. package/lib/app/run-env-recovery.js +35 -0
  13. package/lib/app/run-helpers.js +5 -6
  14. package/lib/app/run.js +11 -3
  15. package/lib/channels/add-channel.js +126 -19
  16. package/lib/channels/approval-guides/chatgpt.js +12 -9
  17. package/lib/channels/approval-guides/microsoft-copilot.js +16 -12
  18. package/lib/channels/channel-add-ensure.js +128 -0
  19. package/lib/channels/channel-artifact-generator.js +11 -1
  20. package/lib/channels/channel-branding.js +94 -0
  21. package/lib/channels/channel-credential.js +8 -0
  22. package/lib/channels/channel-discovery.js +330 -0
  23. package/lib/channels/channel-external-system.js +1 -1
  24. package/lib/channels/channel-runtime-resolver.js +103 -14
  25. package/lib/channels/channel-status-display.js +7 -1
  26. package/lib/channels/channel-targets.js +18 -1
  27. package/lib/channels/chatgpt-runtime-contract.js +1 -1
  28. package/lib/channels/microsoft-copilot-channel-add.js +138 -0
  29. package/lib/channels/microsoft-copilot-entra-sso-form.js +180 -0
  30. package/lib/channels/microsoft-copilot-entra-sso.js +264 -0
  31. package/lib/channels/microsoft-copilot-icons.js +74 -0
  32. package/lib/channels/microsoft-copilot-limits.js +84 -0
  33. package/lib/channels/microsoft-copilot-package.js +130 -49
  34. package/lib/channels/microsoft-copilot-validate.js +197 -0
  35. package/lib/cli/index.js +2 -0
  36. package/lib/cli/infra-guided-footers.js +1 -1
  37. package/lib/cli/infra-guided-public-url-warn.js +55 -0
  38. package/lib/cli/infra-guided.js +78 -62
  39. package/lib/cli/setup-app.help.js +4 -0
  40. package/lib/cli/setup-app.test-commands.js +59 -28
  41. package/lib/cli/setup-external-system.js +50 -12
  42. package/lib/cli/setup-infra.js +31 -4
  43. package/lib/commands/channel-add-setup.js +35 -10
  44. package/lib/commands/channel.js +27 -12
  45. package/lib/commands/deploy-gate-orchestrator.js +3 -2
  46. package/lib/commands/governance-verify-external.js +2 -5
  47. package/lib/commands/integration-definition.js +158 -0
  48. package/lib/commands/lifecycle-external.js +14 -7
  49. package/lib/commands/repair-auth-apply.js +19 -5
  50. package/lib/commands/repair-datasource-sync-operations.js +4 -1
  51. package/lib/commands/role-assistant-test-output.js +200 -0
  52. package/lib/commands/role-assistant.js +131 -114
  53. package/lib/commands/test-governance-external.js +2 -5
  54. package/lib/commands/up-miso.js +61 -24
  55. package/lib/commands/upload-data-sync-finalize.js +20 -3
  56. package/lib/commands/verify-governance-external.js +2 -5
  57. package/lib/commands/verify-operations-external.js +2 -5
  58. package/lib/commands/verify-operations-step-plan.js +31 -0
  59. package/lib/commands/verify-trust-command-action.js +2 -5
  60. package/lib/datasource/capability/capability-resolve.js +15 -5
  61. package/lib/datasource/capability/copy-operations.js +14 -4
  62. package/lib/datasource/datasource-identifier.js +4 -3
  63. package/lib/datasource/integration-context.js +3 -3
  64. package/lib/datasource/resolve-app.js +90 -8
  65. package/lib/datasource/test-e2e.js +2 -2
  66. package/lib/datasource/unified-validation-run-body.js +15 -1
  67. package/lib/datasource/unified-validation-run-resolve.js +2 -2
  68. package/lib/external-system/deploy-helpers.js +9 -5
  69. package/lib/external-system/deploy.js +2 -1
  70. package/lib/external-system/test-system-level.js +21 -3
  71. package/lib/external-system/test.js +4 -4
  72. package/lib/generator/builders.js +9 -15
  73. package/lib/infrastructure/index.js +19 -4
  74. package/lib/integration-definition/apply.js +478 -0
  75. package/lib/integration-definition/canonical-json.js +51 -0
  76. package/lib/integration-definition/emit-suite-bid.js +94 -0
  77. package/lib/integration-definition/enrich-crm-field-mappings.js +240 -0
  78. package/lib/integration-definition/enrich-openapi-operations.js +434 -0
  79. package/lib/integration-definition/from-discovery.js +339 -0
  80. package/lib/integration-definition/hash.js +37 -0
  81. package/lib/integration-definition/index.js +37 -0
  82. package/lib/integration-definition/openapi-enrichment/hubspot-crm-companies.json +205 -0
  83. package/lib/integration-definition/resource-type-catalog.js +59 -0
  84. package/lib/integration-definition/system-kind-enum.js +40 -0
  85. package/lib/integration-definition/validate.js +148 -0
  86. package/lib/programmatic/builder-api-request.js +5 -1
  87. package/lib/programmatic/builder-help-governance.js +3 -2
  88. package/lib/programmatic/capability-registry.js +19 -0
  89. package/lib/programmatic/openapi-component-schemas.js +6 -1
  90. package/lib/programmatic/openapi-descriptions.js +40 -0
  91. package/lib/programmatic/openapi-envelope-examples.js +269 -0
  92. package/lib/programmatic/openapi-operation-bindings.js +45 -0
  93. package/lib/programmatic/openapi-request-schemas.js +14 -0
  94. package/lib/programmatic/openapi-result-schemas-overflow.js +101 -0
  95. package/lib/programmatic/openapi-result-schemas.js +46 -99
  96. package/lib/programmatic/openapi-schema-descriptions-core.js +7 -4
  97. package/lib/programmatic/openapi-schema-descriptions-help.js +1 -1
  98. package/lib/programmatic/openapi-schema-descriptions-operations.js +24 -0
  99. package/lib/programmatic/openapi-schema-helpers.js +9 -9
  100. package/lib/programmatic/openapi-spec.js +11 -1
  101. package/lib/programmatic/orchestration-openapi-meta.js +10 -0
  102. package/lib/programmatic/route-handler-map.js +29 -1
  103. package/lib/programmatic/run-channel-package.js +236 -0
  104. package/lib/programmatic/run-integration-definition.js +69 -0
  105. package/lib/role-assistant/knowledge-markdown-frontmatter.js +75 -0
  106. package/lib/role-assistant/knowledge-sync-logging.js +58 -0
  107. package/lib/role-assistant/knowledge-sync.js +104 -89
  108. package/lib/role-assistant/test-cases-bid.js +155 -0
  109. package/lib/role-assistant/test-cases-evaluate.js +9 -1
  110. package/lib/role-assistant/test-cases-honesty.js +24 -10
  111. package/lib/role-assistant/test-cases.js +28 -0
  112. package/lib/role-assistant/test-runner-bid-apply.js +122 -0
  113. package/lib/role-assistant/test-runner-bid-document.js +278 -0
  114. package/lib/role-assistant/test-runner-workhub-answers.js +289 -46
  115. package/lib/role-assistant/test-runner-workhub.js +121 -25
  116. package/lib/role-assistant/test-suite.js +119 -0
  117. package/lib/schema/application-schema.json +64 -32
  118. package/lib/schema/external-datasource.schema.json +103 -17
  119. package/lib/schema/external-system.schema.json +54 -3
  120. package/lib/schema/infra.parameter.yaml +0 -20
  121. package/lib/schema/integration-definition.schema.json +195 -0
  122. package/lib/utils/authentication-shape.js +6 -0
  123. package/lib/utils/certify-external-workspace.js +40 -0
  124. package/lib/utils/docker-build.js +3 -3
  125. package/lib/utils/docker-run-ephemeral.js +77 -17
  126. package/lib/utils/health-check-public-warn.js +31 -13
  127. package/lib/utils/health-check.js +11 -1
  128. package/lib/utils/help-builder.js +1 -0
  129. package/lib/utils/paths.js +13 -15
  130. package/lib/utils/promote-dev-docker-image.js +97 -18
  131. package/lib/utils/role-assistant-paths.js +76 -14
  132. package/lib/utils/transient-developer-id.js +28 -0
  133. package/lib/utils/upload-sync-options.js +7 -4
  134. package/lib/validation/reserved-system-key-prefixes.js +56 -0
  135. package/lib/validation/validate.js +54 -19
  136. package/package.json +1 -1
  137. package/scripts/pnpm/af.mjs +24 -9
  138. package/scripts/pnpm/help.mjs +1 -1
  139. package/templates/applications/builder-api/application.yaml +3 -3
  140. package/templates/applications/dataplane/application.yaml +5 -5
  141. package/templates/applications/dataplane/env.template +8 -7
  142. package/templates/applications/dataplane/rbac.yaml +10 -10
  143. package/templates/applications/keycloak/application.yaml +2 -2
  144. package/templates/applications/keycloak/env.template +5 -2
  145. package/templates/applications/miso-controller/application.yaml +25 -11
  146. package/templates/applications/miso-controller/env.template +24 -5
  147. package/templates/channels/README.md +11 -1
  148. package/templates/channels/brand/color-192.png +0 -0
  149. package/templates/channels/brand/dark/128.png +0 -0
  150. package/templates/channels/brand/dark/192.png +0 -0
  151. package/templates/channels/brand/dark/256.png +0 -0
  152. package/templates/channels/brand/dark/32.png +0 -0
  153. package/templates/channels/brand/dark/512.png +0 -0
  154. package/templates/channels/brand/dark/64.png +0 -0
  155. package/templates/channels/brand/light/128.png +0 -0
  156. package/templates/channels/brand/light/192.png +0 -0
  157. package/templates/channels/brand/light/256.png +0 -0
  158. package/templates/channels/brand/light/32.png +0 -0
  159. package/templates/channels/brand/light/512.png +0 -0
  160. package/templates/channels/brand/light/64.png +0 -0
  161. package/templates/channels/brand/mono-white/128.png +0 -0
  162. package/templates/channels/brand/mono-white/192.png +0 -0
  163. package/templates/channels/brand/mono-white/256.png +0 -0
  164. package/templates/channels/brand/mono-white/32.png +0 -0
  165. package/templates/channels/brand/mono-white/512.png +0 -0
  166. package/templates/channels/brand/mono-white/64.png +0 -0
  167. package/templates/channels/brand/outline-32.png +0 -0
  168. package/templates/channels/chatgpt/install.template.md.hbs +76 -36
  169. package/templates/channels/chatgpt/integration/README.md +4 -4
  170. package/templates/channels/chatgpt/integration/chatgpt-system.json +1 -16
  171. package/templates/channels/chatgpt/manifest.template.json.hbs +3 -3
  172. package/templates/channels/microsoft-copilot/ai-plugin.template.json.hbs +10 -16
  173. package/templates/channels/microsoft-copilot/declarative-agent.template.json.hbs +5 -3
  174. package/templates/channels/microsoft-copilot/entra-sso-registration.template.md.hbs +27 -0
  175. package/templates/channels/microsoft-copilot/install.template.md.hbs +39 -20
  176. package/templates/channels/microsoft-copilot/integration/README.md +2 -3
  177. package/templates/channels/microsoft-copilot/integration/microsoft-copilot-system.json +1 -22
  178. package/templates/channels/microsoft-copilot/manifest.template.json.hbs +7 -7
  179. package/templates/channels/microsoft-copilot/oauth-plugin-vault-required.template.md.hbs +11 -0
  180. package/templates/channels/slack/install.template.md.hbs +6 -1
  181. package/templates/channels/slack/integration/README.md +2 -4
  182. package/templates/channels/teams-bot/install.template.md.hbs +4 -0
  183. package/templates/channels/teams-bot/integration/README.md +3 -5
  184. package/templates/channels/web-embed/integration/web-embed-system.json +1 -16
  185. package/templates/python/docker-compose.hbs +10 -0
  186. package/templates/typescript/docker-compose.hbs +10 -0
  187. /package/.cursor/plans/{Done → Archive}/157.1-security-quality-violations-by-type.csv +0 -0
@@ -0,0 +1,478 @@
1
+ /**
2
+ * Apply Builder Integration Definition to integration/<systemKey>/.
3
+ * @fileoverview Idempotent BID → package materialization (no AI)
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const fs = require('../internal/fs-real-sync');
9
+ const fsp = require('fs').promises;
10
+ const path = require('path');
11
+ const { getIntegrationPath } = require('../utils/paths');
12
+ const { validateIntegrationDefinition } = require('./validate');
13
+
14
+ const STAMP_FILE = '.aifabrix-integration-definition.json';
15
+
16
+ /**
17
+ * @param {string} dir
18
+ * @returns {Promise<Record<string, unknown> | null>}
19
+ */
20
+ async function readStamp(dir) {
21
+ const stampPath = path.join(dir, STAMP_FILE);
22
+ try {
23
+ const raw = await fsp.readFile(stampPath, 'utf8');
24
+ return JSON.parse(raw);
25
+ } catch (err) {
26
+ if (err && err.code === 'ENOENT') {
27
+ return null;
28
+ }
29
+ throw err;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * @param {Record<string, unknown>} bid
35
+ * @param {Record<string, unknown>} stamp
36
+ * @returns {void}
37
+ */
38
+ function assertApplyAllowed(bid, stamp) {
39
+ if (!stamp) {
40
+ return;
41
+ }
42
+ const meta = /** @type {Record<string, unknown>} */ (bid.meta || {});
43
+ if (stamp.definitionId === bid.definitionId && stamp.definitionHash === bid.definitionHash) {
44
+ return;
45
+ }
46
+ if (stamp.definitionId === bid.definitionId) {
47
+ return;
48
+ }
49
+ if (stamp.systemKey === /** @type {any} */ (bid.packageIdentity).systemKey && !meta.replaceSystemKey) {
50
+ const err = new Error(
51
+ `systemKey ${stamp.systemKey} already bound to definitionId ${stamp.definitionId}; ` +
52
+ 'set meta.replaceSystemKey true to replace'
53
+ );
54
+ err.code = 'BID_SYSTEM_KEY_CONFLICT';
55
+ throw err;
56
+ }
57
+ }
58
+
59
+ function buildDatasourceCapabilities(caps, key) {
60
+ return caps.map((capability) => ({
61
+ key: capability,
62
+ description: `${capability} ${key}`,
63
+ riskLevel:
64
+ capability === 'delete'
65
+ ? 'high'
66
+ : capability.startsWith('create') || capability.startsWith('update')
67
+ ? 'medium'
68
+ : 'low',
69
+ approvalRequired: false
70
+ }));
71
+ }
72
+
73
+ function buildDatasourceSchema(labelField) {
74
+ return {
75
+ metadataSchema: {
76
+ type: 'object',
77
+ properties: {
78
+ externalId: { type: 'string', index: true, description: 'Stable external join identity' },
79
+ id: { type: 'string', index: true, description: 'Vendor record id' },
80
+ [labelField]: { type: 'string', index: true, filter: true, description: 'Display label' }
81
+ },
82
+ required: ['externalId', 'id']
83
+ },
84
+ fieldMappings: {
85
+ attributes: {
86
+ id: { expression: '{{raw.id}}', description: 'Vendor record id' },
87
+ externalId: {
88
+ expression: '{{raw.id}}',
89
+ description: 'Stable join identity (defaults to vendor id until enrichment)'
90
+ },
91
+ [labelField]: {
92
+ expression: `{{raw.${labelField}}}`,
93
+ description: 'Display label (enrich from OpenAPI raw paths as needed)'
94
+ }
95
+ }
96
+ }
97
+ };
98
+ }
99
+
100
+ function enrichDatasourceFile(body, ds, enrichOptions) {
101
+ const { enrichDatasourceFromOpenApi } = require('./enrich-openapi-operations');
102
+ const openapiSourceRef =
103
+ (typeof ds.openapiSourceRef === 'string' && ds.openapiSourceRef.trim()) ||
104
+ (typeof enrichOptions.systemOpenapiSourceRef === 'string' &&
105
+ enrichOptions.systemOpenapiSourceRef.trim()) ||
106
+ '';
107
+ if (openapiSourceRef) {
108
+ enrichDatasourceFromOpenApi(body, openapiSourceRef, {
109
+ baseDir: enrichOptions.baseDir,
110
+ associationTargets: enrichOptions.associationTargets
111
+ });
112
+ }
113
+ }
114
+
115
+ /**
116
+ * @param {Record<string, unknown>} ds
117
+ * @param {string} systemKey
118
+ * @param {Record<string, string[]>} capabilities
119
+ * @param {Record<string, unknown>} sync
120
+ * @returns {Record<string, unknown>}
121
+ */
122
+ function buildDatasourceFile(ds, systemKey, capabilities, sync, enrichOptions = {}) {
123
+ const key = String(ds.key);
124
+ const caps = capabilities[key] || ds.capabilityKeys || ['list', 'get'];
125
+ const labelField = ds.labelField || 'name';
126
+ const { repairSyncOperationsFromCapabilities } = require('../commands/repair-datasource-sync-operations');
127
+ const { seedPropertyKeysOnDatasource } = require('./enrich-crm-field-mappings');
128
+ const body = {
129
+ key,
130
+ displayName: ds.displayName || key,
131
+ description: ds.description || `${ds.displayName || key} datasource`,
132
+ enabled: true,
133
+ systemKey,
134
+ entityType: ds.entityType || 'recordStorage',
135
+ resourceType: ds.resourceType,
136
+ version: '1.0.0',
137
+ capabilities: buildDatasourceCapabilities(caps, key),
138
+ // Schema sync is mode/schedule/batchSize only; BID sync.mode=incremental is operational intent.
139
+ sync: { mode: 'pull', schedule: /** @type {any} */ (sync).schedule || '0 */6 * * *', batchSize: 100 },
140
+ primaryKey: ['id'],
141
+ labelKey: [labelField],
142
+ ...buildDatasourceSchema(labelField)
143
+ };
144
+ // Mirror capabilities → openapi.operations + execution.cip.operations stubs (154.7).
145
+ repairSyncOperationsFromCapabilities(body, []);
146
+ seedPropertyKeysOnDatasource(body, ds.propertyKeys);
147
+ enrichDatasourceFile(body, ds, enrichOptions);
148
+ return body;
149
+ }
150
+
151
+ /**
152
+ * Collect vendor object-type segments from each datasource OpenAPI ref.
153
+ * Used so list/get can request associations to sibling collections.
154
+ * @param {any[]} datasources
155
+ * @param {string} systemOpenapiSourceRef
156
+ * @param {{ baseDir?: string }} enrichOptions
157
+ * @returns {string[]}
158
+ */
159
+ function collectAssociationObjectTypes(datasources, systemOpenapiSourceRef, enrichOptions = {}) {
160
+ const {
161
+ loadOpenApiDocument,
162
+ indexOpenApiOperations,
163
+ objectTypeFromOpenApiPath
164
+ } = require('./enrich-openapi-operations');
165
+ const types = new Set();
166
+ for (const ds of datasources || []) {
167
+ // BID datasource keys are the named collection segments CRM association
168
+ // queries expect (opaque OpenAPI paths like /objects/0-3 omit these).
169
+ const dsKey = String(ds.key || '').trim();
170
+ if (dsKey) {
171
+ types.add(dsKey);
172
+ }
173
+ const ref =
174
+ (typeof ds.openapiSourceRef === 'string' && ds.openapiSourceRef.trim()) ||
175
+ systemOpenapiSourceRef ||
176
+ '';
177
+ if (!ref) {
178
+ continue;
179
+ }
180
+ try {
181
+ const doc = loadOpenApiDocument(ref, enrichOptions.baseDir);
182
+ const ops = indexOpenApiOperations(doc);
183
+ for (const op of ops) {
184
+ const objectType = objectTypeFromOpenApiPath(op.path);
185
+ if (objectType) {
186
+ types.add(objectType);
187
+ }
188
+ }
189
+ } catch {
190
+ // Skip unreadable refs; enrichment still binds per-datasource.
191
+ }
192
+ }
193
+ return [...types];
194
+ }
195
+
196
+ /**
197
+ * @param {Record<string, unknown>} bid
198
+ * @returns {Record<string, unknown>}
199
+ */
200
+ function buildSystemFile(bid) {
201
+ const system = /** @type {Record<string, unknown>} */ (bid.system);
202
+ const identity = /** @type {Record<string, unknown>} */ (bid.packageIdentity);
203
+ const refs = /** @type {Record<string, string>} */ (system.connectionRefs || {});
204
+ const tokenRef = refs.token || refs.apiKey || Object.values(refs)[0] || `kv://${identity.systemKey}/token`;
205
+ return {
206
+ key: system.key,
207
+ displayName: system.displayName || identity.displayName,
208
+ description: system.description || identity.displayName,
209
+ type: system.type || 'openapi',
210
+ enabled: true,
211
+ systemKind: system.systemKind || 'crm',
212
+ authentication: {
213
+ authType: 'credential',
214
+ credential: {
215
+ credentialKey: `${identity.systemKey}-cred`,
216
+ method: 'bearerToken',
217
+ variables: {
218
+ baseUrl: refs.baseUrl || 'https://example.invalid',
219
+ headerName: 'Authorization',
220
+ prefix: 'Bearer'
221
+ },
222
+ security: {
223
+ token: tokenRef
224
+ },
225
+ writeOnce: true
226
+ }
227
+ },
228
+ dataSources: (/** @type {any[]} */ (bid.datasources) || []).map((d) => d.key)
229
+ };
230
+ }
231
+
232
+ /**
233
+ * @param {Record<string, unknown>} bid
234
+ * @param {string[]} datasourceFiles
235
+ * @param {string} systemFile
236
+ * @returns {Record<string, unknown>}
237
+ */
238
+ function buildApplicationFile(bid, datasourceFiles, systemFile) {
239
+ const identity = /** @type {Record<string, unknown>} */ (bid.packageIdentity);
240
+ return {
241
+ app: {
242
+ key: identity.systemKey,
243
+ displayName: identity.displayName,
244
+ description: `${identity.displayName} (BT BID-applied)`,
245
+ type: 'external'
246
+ },
247
+ externalIntegration: {
248
+ schemaBasePath: './',
249
+ systems: [systemFile],
250
+ dataSources: datasourceFiles,
251
+ autopublish: true,
252
+ version: identity.packageVersion
253
+ },
254
+ integrationDefinition: {
255
+ schemaName: bid.schemaName,
256
+ schemaVersion: bid.schemaVersion,
257
+ definitionId: bid.definitionId,
258
+ definitionHash: bid.definitionHash,
259
+ suiteRunId: /** @type {any} */ (bid.meta).suiteRunId
260
+ }
261
+ };
262
+ }
263
+
264
+ /**
265
+ * @param {Record<string, unknown>} bid
266
+ * @returns {string}
267
+ */
268
+ function buildEnvTemplate(bid) {
269
+ const identity = /** @type {Record<string, unknown>} */ (bid.packageIdentity);
270
+ const systemKey = String(identity.systemKey);
271
+ const system = /** @type {Record<string, unknown>} */ (bid.system || {});
272
+ const refs = /** @type {Record<string, string>} */ (system.connectionRefs || {});
273
+ const tokenRef = refs.token || refs.apiKey || `kv://${systemKey}/token`;
274
+ return [
275
+ `# Environment for ${systemKey} (BID-applied). No plaintext secrets.`,
276
+ `# Resolve with aifabrix secret set ${systemKey}/token "<value>" --shared`,
277
+ `KV_TOKEN=${tokenRef}`,
278
+ ''
279
+ ].join('\n');
280
+ }
281
+
282
+ /**
283
+ * @param {Record<string, unknown>} bid
284
+ * @param {string} systemKey
285
+ * @returns {{
286
+ * systemFile: string,
287
+ * datasourceFiles: string[],
288
+ * application: Record<string, unknown>,
289
+ * system: Record<string, unknown>,
290
+ * dsFiles: Array<{ name: string, body: Record<string, unknown> }>,
291
+ * nextStamp: Record<string, unknown>
292
+ * }}
293
+ */
294
+ function buildPackageArtifacts(bid, systemKey, enrichOptions = {}) {
295
+ const identity = /** @type {Record<string, unknown>} */ (bid.packageIdentity);
296
+ const datasources = /** @type {Record<string, unknown>[]} */ (bid.datasources || []);
297
+ const capabilities = /** @type {Record<string, string[]>} */ (bid.capabilities || {});
298
+ const sync = /** @type {Record<string, unknown>} */ (bid.sync || {});
299
+ const system = /** @type {Record<string, unknown>} */ (bid.system || {});
300
+ const systemOpenapiSourceRef =
301
+ typeof system.openapiSourceRef === 'string' ? system.openapiSourceRef : '';
302
+ const associationTargets = collectAssociationObjectTypes(
303
+ datasources,
304
+ systemOpenapiSourceRef,
305
+ enrichOptions
306
+ );
307
+ const systemFile = `${systemKey}-system.json`;
308
+ const datasourceFiles = datasources.map((ds) => `${systemKey}-datasource-${ds.key}.json`);
309
+ return {
310
+ systemFile,
311
+ datasourceFiles,
312
+ application: buildApplicationFile(bid, datasourceFiles, systemFile),
313
+ system: buildSystemFile(bid),
314
+ dsFiles: datasources.map((ds) => ({
315
+ name: `${systemKey}-datasource-${ds.key}.json`,
316
+ body: buildDatasourceFile(ds, systemKey, capabilities, sync, {
317
+ ...enrichOptions,
318
+ systemOpenapiSourceRef,
319
+ associationTargets
320
+ })
321
+ })),
322
+ envTemplate: buildEnvTemplate(bid),
323
+ nextStamp: {
324
+ schemaName: bid.schemaName,
325
+ schemaVersion: bid.schemaVersion,
326
+ definitionId: bid.definitionId,
327
+ definitionHash: bid.definitionHash,
328
+ suiteRunId: /** @type {any} */ (bid.meta).suiteRunId,
329
+ systemKey,
330
+ packageVersion: identity.packageVersion,
331
+ appliedAt: new Date().toISOString()
332
+ }
333
+ };
334
+ }
335
+
336
+ /**
337
+ * @param {string} outDir
338
+ * @param {ReturnType<typeof buildPackageArtifacts>} artifacts
339
+ * @returns {Promise<void>}
340
+ */
341
+ async function writePackageArtifacts(outDir, artifacts) {
342
+ await fsp.mkdir(outDir, { recursive: true });
343
+ await fsp.writeFile(
344
+ path.join(outDir, 'application.json'),
345
+ JSON.stringify(artifacts.application, null, 2),
346
+ 'utf8'
347
+ );
348
+ await fsp.writeFile(
349
+ path.join(outDir, artifacts.systemFile),
350
+ JSON.stringify(artifacts.system, null, 2),
351
+ 'utf8'
352
+ );
353
+ for (const file of artifacts.dsFiles) {
354
+ await fsp.writeFile(path.join(outDir, file.name), JSON.stringify(file.body, null, 2), 'utf8');
355
+ }
356
+ await fsp.writeFile(path.join(outDir, 'env.template'), artifacts.envTemplate, 'utf8');
357
+ await fsp.writeFile(
358
+ path.join(outDir, STAMP_FILE),
359
+ JSON.stringify(artifacts.nextStamp, null, 2),
360
+ 'utf8'
361
+ );
362
+ }
363
+
364
+ /**
365
+ * @param {unknown} bidInput
366
+ * @returns {Record<string, unknown>}
367
+ */
368
+ function requireValidBid(bidInput) {
369
+ const result = validateIntegrationDefinition(bidInput);
370
+ if (!result.valid) {
371
+ const err = new Error(`Invalid BID: ${result.errors.join('; ')}`);
372
+ err.code = 'BID_INVALID';
373
+ err.errors = result.errors;
374
+ throw err;
375
+ }
376
+ return result.bid;
377
+ }
378
+
379
+ /**
380
+ * @param {Record<string, unknown> | null} stamp
381
+ * @param {Record<string, unknown>} bid
382
+ * @param {string} outDir
383
+ * @param {string} systemKey
384
+ * @returns {Record<string, unknown> | null}
385
+ */
386
+ function idempotentResult(stamp, bid, outDir, systemKey) {
387
+ const identity = /** @type {Record<string, unknown>} */ (bid.packageIdentity);
388
+ if (
389
+ !(
390
+ stamp &&
391
+ stamp.definitionId === bid.definitionId &&
392
+ stamp.definitionHash === bid.definitionHash &&
393
+ fs.existsSync(path.join(outDir, 'application.json'))
394
+ )
395
+ ) {
396
+ return null;
397
+ }
398
+ return {
399
+ status: 'idempotent',
400
+ systemKey,
401
+ outputDir: outDir,
402
+ definitionId: bid.definitionId,
403
+ definitionHash: bid.definitionHash,
404
+ packageVersion: identity.packageVersion
405
+ };
406
+ }
407
+
408
+ /**
409
+ * Apply a validated BID to the integration workspace.
410
+ * @async
411
+ * @param {Record<string, unknown>} bidInput
412
+ * @param {{
413
+ * outputDir?: string,
414
+ * workspaceRoot?: string,
415
+ * dryRun?: boolean,
416
+ * force?: boolean,
417
+ * openapiBaseDir?: string,
418
+ * baseDir?: string
419
+ * }} [options]
420
+ * @returns {Promise<Record<string, unknown>>}
421
+ */
422
+ async function applyIntegrationDefinition(bidInput, options = {}) {
423
+ const bid = requireValidBid(bidInput);
424
+ const identity = /** @type {Record<string, unknown>} */ (bid.packageIdentity);
425
+ const systemKey = String(identity.systemKey);
426
+ const outDir =
427
+ options.outputDir ||
428
+ (options.workspaceRoot
429
+ ? path.join(options.workspaceRoot, 'integration', systemKey)
430
+ : getIntegrationPath(systemKey));
431
+ const stamp = await readStamp(outDir);
432
+ const early = options.force ? null : idempotentResult(stamp, bid, outDir, systemKey);
433
+ if (early) {
434
+ return early;
435
+ }
436
+ assertApplyAllowed(bid, stamp);
437
+ // Prefer explicit openapiBaseDir; else ship-local enrichment specs (not caller cwd).
438
+ // Suite/builder-api apply runs with workspace cwd and must still resolve openapiSourceRef.
439
+ const defaultOpenapiBaseDir = path.join(__dirname, 'openapi-enrichment');
440
+ const artifacts = buildPackageArtifacts(bid, systemKey, {
441
+ baseDir: options.openapiBaseDir || options.baseDir || defaultOpenapiBaseDir
442
+ });
443
+ const fileList = [
444
+ 'application.json',
445
+ artifacts.systemFile,
446
+ ...artifacts.datasourceFiles,
447
+ 'env.template',
448
+ STAMP_FILE
449
+ ];
450
+ if (options.dryRun) {
451
+ return {
452
+ status: 'dryRun',
453
+ systemKey,
454
+ outputDir: outDir,
455
+ files: fileList,
456
+ stamp: artifacts.nextStamp
457
+ };
458
+ }
459
+ await writePackageArtifacts(outDir, artifacts);
460
+ return {
461
+ status: stamp ? 'updated' : 'created',
462
+ systemKey,
463
+ outputDir: outDir,
464
+ definitionId: bid.definitionId,
465
+ definitionHash: bid.definitionHash,
466
+ packageVersion: identity.packageVersion,
467
+ files: fileList
468
+ };
469
+ }
470
+
471
+ module.exports = {
472
+ applyIntegrationDefinition,
473
+ STAMP_FILE,
474
+ buildApplicationFile,
475
+ buildSystemFile,
476
+ buildDatasourceFile,
477
+ buildEnvTemplate
478
+ };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Canonical JSON helpers for Builder Integration Definition hashing.
3
+ * @fileoverview Stable stringify for BID definitionHash
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ /**
9
+ * Recursively sort object keys for canonical JSON.
10
+ * @param {unknown} value
11
+ * @returns {unknown}
12
+ */
13
+ function sortValue(value) {
14
+ if (Array.isArray(value)) {
15
+ return value.map(sortValue);
16
+ }
17
+ if (value && typeof value === 'object' && !(value instanceof Date)) {
18
+ const out = {};
19
+ for (const key of Object.keys(value).sort()) {
20
+ out[key] = sortValue(value[key]);
21
+ }
22
+ return out;
23
+ }
24
+ return value;
25
+ }
26
+
27
+ /**
28
+ * Canonical JSON string (sorted keys, no whitespace variance).
29
+ * @param {unknown} value
30
+ * @returns {string}
31
+ */
32
+ function canonicalJsonString(value) {
33
+ return JSON.stringify(sortValue(value));
34
+ }
35
+
36
+ /**
37
+ * Clone BID without definitionHash for hashing.
38
+ * @param {Record<string, unknown>} bid
39
+ * @returns {Record<string, unknown>}
40
+ */
41
+ function bidBodyWithoutHash(bid) {
42
+ const clone = { ...bid };
43
+ delete clone.definitionHash;
44
+ return clone;
45
+ }
46
+
47
+ module.exports = {
48
+ sortValue,
49
+ canonicalJsonString,
50
+ bidBodyWithoutHash
51
+ };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Stamp suite provenance onto a BID template for hard GP acceptance.
3
+ * @fileoverview Suite-emitted BID: suiteRunId + answerFingerprint + definitionHash
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const crypto = require('crypto');
9
+ const { withDefinitionHash } = require('./hash');
10
+ const { canonicalJsonString } = require('./canonical-json');
11
+
12
+ /**
13
+ * Fingerprint discovery / case answers for BID meta.answerFingerprint.
14
+ * @param {unknown} answers
15
+ * @returns {string}
16
+ */
17
+ function fingerprintAnswers(answers) {
18
+ const digest = crypto
19
+ .createHash('sha256')
20
+ .update(canonicalJsonString(answers === null || answers === undefined ? null : answers), 'utf8')
21
+ .digest('hex');
22
+ return `sha256:${digest}`;
23
+ }
24
+
25
+ function resolvePriorMeta(baseBid) {
26
+ return baseBid.meta && typeof baseBid.meta === 'object' && !Array.isArray(baseBid.meta)
27
+ ? baseBid.meta
28
+ : {};
29
+ }
30
+
31
+ function resolveAnswerFingerprint(options) {
32
+ if (typeof options.answerFingerprint === 'string' && options.answerFingerprint.trim()) {
33
+ return options.answerFingerprint.trim();
34
+ }
35
+ return fingerprintAnswers(options.answers);
36
+ }
37
+
38
+ function resolveProducedAt(options) {
39
+ if (typeof options.producedAt === 'string' && options.producedAt.trim()) {
40
+ return options.producedAt.trim();
41
+ }
42
+ return new Date().toISOString();
43
+ }
44
+
45
+ function resolveBtAssistantKey(options, priorMeta) {
46
+ if (typeof options.btAssistantKey === 'string' && options.btAssistantKey.trim()) {
47
+ return options.btAssistantKey.trim();
48
+ }
49
+ if (typeof priorMeta.btAssistantKey === 'string' && priorMeta.btAssistantKey.trim()) {
50
+ return priorMeta.btAssistantKey.trim();
51
+ }
52
+ return 'ra-business-transformation';
53
+ }
54
+
55
+ /**
56
+ * Clone a BID template and stamp suite provenance (hard GP).
57
+ * Does not invent datasources/capabilities — only meta + hash refresh.
58
+ *
59
+ * @param {Record<string, unknown>} baseBid - Valid BID template (e.g. example-bid.json)
60
+ * @param {Object} options
61
+ * @param {string} options.suiteRunId - Suite / processExecution id (required for hard GP)
62
+ * @param {string} [options.producedAt] - ISO timestamp
63
+ * @param {string} [options.btAssistantKey]
64
+ * @param {unknown} [options.answers] - Case answers for answerFingerprint
65
+ * @param {string} [options.answerFingerprint] - Explicit fingerprint override
66
+ * @returns {Record<string, unknown>}
67
+ */
68
+ function emitSuiteBid(baseBid, options = {}) {
69
+ if (!baseBid || typeof baseBid !== 'object' || Array.isArray(baseBid)) {
70
+ throw new Error('emitSuiteBid requires a BID object');
71
+ }
72
+ const suiteRunId = String(options.suiteRunId || '').trim();
73
+ if (!suiteRunId) {
74
+ throw new Error('emitSuiteBid requires options.suiteRunId');
75
+ }
76
+ const priorMeta = resolvePriorMeta(baseBid);
77
+ const next = {
78
+ ...baseBid,
79
+ meta: {
80
+ ...priorMeta,
81
+ suiteRunId,
82
+ producedAt: resolveProducedAt(options),
83
+ btAssistantKey: resolveBtAssistantKey(options, priorMeta),
84
+ answerFingerprint: resolveAnswerFingerprint(options)
85
+ }
86
+ };
87
+ delete next.definitionHash;
88
+ return withDefinitionHash(next);
89
+ }
90
+
91
+ module.exports = {
92
+ emitSuiteBid,
93
+ fingerprintAnswers
94
+ };