@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.
- package/jest.isolated-projects.js +25 -2
- package/jest.projects.js +4 -1
- package/lib/api/datasources-extended.api.js +8 -8
- package/lib/api/keycloak.api.js +92 -0
- package/lib/api/token-providers.api.js +77 -0
- package/lib/api/types/keycloak.types.js +23 -0
- package/lib/api/types/token-providers.types.js +65 -0
- package/lib/app/down.js +16 -1
- package/lib/channels/add-channel.js +126 -19
- package/lib/channels/approval-guides/chatgpt.js +1 -1
- package/lib/channels/approval-guides/microsoft-copilot.js +16 -12
- package/lib/channels/channel-add-ensure.js +128 -0
- package/lib/channels/channel-artifact-generator.js +11 -1
- package/lib/channels/channel-branding.js +94 -0
- package/lib/channels/channel-credential.js +8 -0
- package/lib/channels/channel-discovery.js +330 -0
- package/lib/channels/channel-external-system.js +1 -1
- package/lib/channels/channel-runtime-resolver.js +103 -14
- package/lib/channels/channel-status-display.js +7 -1
- package/lib/channels/microsoft-copilot-channel-add.js +138 -0
- package/lib/channels/microsoft-copilot-entra-sso-form.js +180 -0
- package/lib/channels/microsoft-copilot-entra-sso.js +264 -0
- package/lib/channels/microsoft-copilot-icons.js +74 -0
- package/lib/channels/microsoft-copilot-limits.js +84 -0
- package/lib/channels/microsoft-copilot-package.js +130 -49
- package/lib/channels/microsoft-copilot-validate.js +197 -0
- package/lib/cli/index.js +2 -0
- package/lib/cli/infra-guided.js +13 -6
- package/lib/cli/setup-infra.js +31 -4
- package/lib/commands/channel-add-setup.js +28 -9
- package/lib/commands/channel.js +14 -10
- package/lib/commands/integration-definition.js +158 -0
- package/lib/commands/repair-auth-apply.js +19 -5
- package/lib/commands/repair-datasource-sync-operations.js +4 -1
- package/lib/commands/role-assistant-test-output.js +200 -0
- package/lib/commands/role-assistant.js +125 -166
- package/lib/datasource/capability/capability-resolve.js +15 -5
- package/lib/datasource/capability/copy-operations.js +14 -4
- package/lib/infrastructure/index.js +19 -4
- package/lib/integration-definition/apply.js +478 -0
- package/lib/integration-definition/canonical-json.js +51 -0
- package/lib/integration-definition/emit-suite-bid.js +94 -0
- package/lib/integration-definition/enrich-crm-field-mappings.js +240 -0
- package/lib/integration-definition/enrich-openapi-operations.js +434 -0
- package/lib/integration-definition/from-discovery.js +339 -0
- package/lib/integration-definition/hash.js +37 -0
- package/lib/integration-definition/index.js +37 -0
- package/lib/integration-definition/openapi-enrichment/hubspot-crm-companies.json +205 -0
- package/lib/integration-definition/resource-type-catalog.js +59 -0
- package/lib/integration-definition/system-kind-enum.js +40 -0
- package/lib/integration-definition/validate.js +148 -0
- package/lib/programmatic/builder-api-request.js +5 -1
- package/lib/programmatic/builder-help-governance.js +3 -2
- package/lib/programmatic/capability-registry.js +19 -0
- package/lib/programmatic/openapi-component-schemas.js +6 -1
- package/lib/programmatic/openapi-descriptions.js +40 -0
- package/lib/programmatic/openapi-envelope-examples.js +269 -0
- package/lib/programmatic/openapi-operation-bindings.js +45 -0
- package/lib/programmatic/openapi-request-schemas.js +14 -0
- package/lib/programmatic/openapi-result-schemas-overflow.js +101 -0
- package/lib/programmatic/openapi-result-schemas.js +46 -99
- package/lib/programmatic/openapi-schema-descriptions-core.js +7 -4
- package/lib/programmatic/openapi-schema-descriptions-help.js +1 -1
- package/lib/programmatic/openapi-schema-descriptions-operations.js +24 -0
- package/lib/programmatic/openapi-schema-helpers.js +9 -9
- package/lib/programmatic/openapi-spec.js +11 -1
- package/lib/programmatic/orchestration-openapi-meta.js +10 -0
- package/lib/programmatic/route-handler-map.js +29 -1
- package/lib/programmatic/run-channel-package.js +236 -0
- package/lib/programmatic/run-integration-definition.js +69 -0
- package/lib/role-assistant/knowledge-sync-logging.js +58 -0
- package/lib/role-assistant/knowledge-sync.js +88 -93
- package/lib/role-assistant/test-cases-bid.js +155 -0
- package/lib/role-assistant/test-cases-evaluate.js +9 -1
- package/lib/role-assistant/test-cases-honesty.js +24 -10
- package/lib/role-assistant/test-cases.js +28 -0
- package/lib/role-assistant/test-runner-bid-apply.js +122 -0
- package/lib/role-assistant/test-runner-bid-document.js +278 -0
- package/lib/role-assistant/test-runner-workhub-answers.js +245 -20
- package/lib/role-assistant/test-runner-workhub.js +100 -18
- package/lib/role-assistant/test-suite.js +119 -0
- package/lib/schema/application-schema.json +56 -6
- package/lib/schema/external-datasource.schema.json +103 -17
- package/lib/schema/external-system.schema.json +54 -3
- package/lib/schema/integration-definition.schema.json +195 -0
- package/lib/utils/authentication-shape.js +6 -0
- package/lib/utils/docker-build.js +3 -3
- package/lib/utils/help-builder.js +1 -0
- package/lib/utils/promote-dev-docker-image.js +97 -18
- package/lib/utils/transient-developer-id.js +28 -0
- package/package.json +1 -1
- package/scripts/pnpm/af.mjs +24 -9
- package/scripts/pnpm/help.mjs +1 -1
- package/templates/applications/dataplane/application.yaml +3 -2
- package/templates/applications/dataplane/env.template +7 -6
- package/templates/applications/miso-controller/application.yaml +23 -9
- package/templates/applications/miso-controller/env.template +8 -2
- package/templates/channels/README.md +11 -1
- package/templates/channels/brand/color-192.png +0 -0
- package/templates/channels/brand/dark/128.png +0 -0
- package/templates/channels/brand/dark/192.png +0 -0
- package/templates/channels/brand/dark/256.png +0 -0
- package/templates/channels/brand/dark/32.png +0 -0
- package/templates/channels/brand/dark/512.png +0 -0
- package/templates/channels/brand/dark/64.png +0 -0
- package/templates/channels/brand/light/128.png +0 -0
- package/templates/channels/brand/light/192.png +0 -0
- package/templates/channels/brand/light/256.png +0 -0
- package/templates/channels/brand/light/32.png +0 -0
- package/templates/channels/brand/light/512.png +0 -0
- package/templates/channels/brand/light/64.png +0 -0
- package/templates/channels/brand/mono-white/128.png +0 -0
- package/templates/channels/brand/mono-white/192.png +0 -0
- package/templates/channels/brand/mono-white/256.png +0 -0
- package/templates/channels/brand/mono-white/32.png +0 -0
- package/templates/channels/brand/mono-white/512.png +0 -0
- package/templates/channels/brand/mono-white/64.png +0 -0
- package/templates/channels/brand/outline-32.png +0 -0
- package/templates/channels/chatgpt/install.template.md.hbs +6 -4
- package/templates/channels/chatgpt/integration/README.md +4 -5
- package/templates/channels/chatgpt/integration/chatgpt-system.json +1 -16
- package/templates/channels/microsoft-copilot/ai-plugin.template.json.hbs +10 -16
- package/templates/channels/microsoft-copilot/declarative-agent.template.json.hbs +5 -3
- package/templates/channels/microsoft-copilot/entra-sso-registration.template.md.hbs +27 -0
- package/templates/channels/microsoft-copilot/install.template.md.hbs +39 -20
- package/templates/channels/microsoft-copilot/integration/README.md +2 -3
- package/templates/channels/microsoft-copilot/integration/microsoft-copilot-system.json +1 -22
- package/templates/channels/microsoft-copilot/manifest.template.json.hbs +7 -7
- package/templates/channels/microsoft-copilot/oauth-plugin-vault-required.template.md.hbs +11 -0
- package/templates/channels/slack/integration/README.md +2 -4
- package/templates/channels/teams-bot/install.template.md.hbs +4 -0
- package/templates/channels/teams-bot/integration/README.md +3 -5
- package/templates/channels/web-embed/integration/web-embed-system.json +1 -16
- /package/.cursor/plans/{Done → Archive}/157.1-security-quality-violations-by-type.csv +0 -0
|
@@ -203,12 +203,26 @@ async function removeAppVolumes(appNames, devId) {
|
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Resolve developer id for stop paths. Optional override is transient (not config).
|
|
208
|
+
* @param {string|number|null|undefined} developerId
|
|
209
|
+
* @returns {Promise<string>}
|
|
210
|
+
*/
|
|
211
|
+
async function resolveStopDeveloperId(developerId) {
|
|
212
|
+
if (developerId === undefined || developerId === null || developerId === '') {
|
|
213
|
+
return config.getDeveloperId();
|
|
214
|
+
}
|
|
215
|
+
const { validateAndNormalizeDeveloperId } = require('../core/config-normalize');
|
|
216
|
+
return validateAndNormalizeDeveloperId(developerId);
|
|
217
|
+
}
|
|
218
|
+
|
|
206
219
|
/**
|
|
207
220
|
* Stops and removes local infrastructure services and all application containers
|
|
208
221
|
* on the same network. Cleanly shuts down infra and app containers.
|
|
209
222
|
*
|
|
210
223
|
* @async
|
|
211
224
|
* @function stopInfra
|
|
225
|
+
* @param {string|number|null|undefined} [developerId] - Optional target developer (does not change config)
|
|
212
226
|
* @returns {Promise<void>} Resolves when infrastructure is stopped
|
|
213
227
|
* @throws {Error} If Docker compose fails
|
|
214
228
|
*
|
|
@@ -216,8 +230,8 @@ async function removeAppVolumes(appNames, devId) {
|
|
|
216
230
|
* await stopInfra();
|
|
217
231
|
* // All infrastructure and app containers on the same network are stopped and removed
|
|
218
232
|
*/
|
|
219
|
-
async function stopInfra() {
|
|
220
|
-
const devId = await
|
|
233
|
+
async function stopInfra(developerId) {
|
|
234
|
+
const devId = await resolveStopDeveloperId(developerId);
|
|
221
235
|
const { infraDir, adminSecretsPath } = resolveInfraStatePaths(devId);
|
|
222
236
|
const composePath = path.join(infraDir, 'compose.yaml');
|
|
223
237
|
const hasCompose = fs.existsSync(composePath);
|
|
@@ -279,6 +293,7 @@ async function stopAllAppContainersAndVolumes(devId) {
|
|
|
279
293
|
*
|
|
280
294
|
* @async
|
|
281
295
|
* @function stopInfraWithVolumes
|
|
296
|
+
* @param {string|number|null|undefined} [developerId] - Optional target developer (does not change config)
|
|
282
297
|
* @returns {Promise<void>} Resolves when infrastructure is stopped and volumes removed
|
|
283
298
|
* @throws {Error} If Docker compose fails
|
|
284
299
|
*
|
|
@@ -286,8 +301,8 @@ async function stopAllAppContainersAndVolumes(devId) {
|
|
|
286
301
|
* await stopInfraWithVolumes();
|
|
287
302
|
* // All infrastructure and app containers and volumes are removed
|
|
288
303
|
*/
|
|
289
|
-
async function stopInfraWithVolumes() {
|
|
290
|
-
const devId = await
|
|
304
|
+
async function stopInfraWithVolumes(developerId) {
|
|
305
|
+
const devId = await resolveStopDeveloperId(developerId);
|
|
291
306
|
const { infraDir, adminSecretsPath } = resolveInfraStatePaths(devId);
|
|
292
307
|
const composePath = path.join(infraDir, 'compose.yaml');
|
|
293
308
|
const hasCompose = fs.existsSync(composePath);
|
|
@@ -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
|
+
};
|