@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,240 @@
1
+ /**
2
+ * CRM-style fieldMappings + properties query for OpenAPI object APIs.
3
+ * @fileoverview Keep BID propertyKeys durable across force-apply
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const RESERVED_ATTR = new Set(['id', 'externalId', 'associations']);
9
+
10
+ /**
11
+ * Seed metadataSchema + fieldMappings.attributes from BID propertyKeys.
12
+ * Does not write dimensions[] (repair / enrich owns dimensions alignment).
13
+ * @param {Record<string, unknown>} body
14
+ * @param {unknown} propertyKeys
15
+ * @returns {void}
16
+ */
17
+ function seedPropertyKeysOnDatasource(body, propertyKeys) {
18
+ const keys = Array.isArray(propertyKeys)
19
+ ? propertyKeys.map((row) => String(row || '').trim()).filter(Boolean)
20
+ : [];
21
+ if (keys.length === 0) {
22
+ return;
23
+ }
24
+ if (!body.metadataSchema || typeof body.metadataSchema !== 'object') {
25
+ body.metadataSchema = { type: 'object', properties: {} };
26
+ }
27
+ const schema = /** @type {any} */ (body.metadataSchema);
28
+ if (!schema.properties || typeof schema.properties !== 'object') {
29
+ schema.properties = {};
30
+ }
31
+ if (!body.fieldMappings || typeof body.fieldMappings !== 'object') {
32
+ body.fieldMappings = { attributes: {} };
33
+ }
34
+ const mappings = /** @type {any} */ (body.fieldMappings);
35
+ if (!mappings.attributes || typeof mappings.attributes !== 'object') {
36
+ mappings.attributes = {};
37
+ }
38
+ for (const key of keys) {
39
+ if (!schema.properties[key]) {
40
+ schema.properties[key] = {
41
+ type: 'string',
42
+ index: true,
43
+ filter: true,
44
+ description: `Synced property ${key}`
45
+ };
46
+ }
47
+ if (!mappings.attributes[key]) {
48
+ mappings.attributes[key] = {
49
+ expression: `{{raw.${key}}}`,
50
+ description: `Synced property ${key}`
51
+ };
52
+ }
53
+ }
54
+ }
55
+
56
+ function mappingAttributes(body) {
57
+ const mappings = /** @type {any} */ (body.fieldMappings);
58
+ if (mappings && mappings.attributes && typeof mappings.attributes === 'object') {
59
+ return mappings.attributes;
60
+ }
61
+ return {};
62
+ }
63
+
64
+ function propertyNameFromAttr(key, row) {
65
+ const expr =
66
+ row && typeof row === 'object' ? String(/** @type {any} */ (row).expression || '') : '';
67
+ const match = expr.match(/\{\{\s*raw\.properties\.([a-zA-Z0-9_]+)\s*\}\}/);
68
+ if (match) {
69
+ return match[1];
70
+ }
71
+ if (expr === `{{raw.${key}}}` || expr.includes(`raw.properties.${key}`)) {
72
+ return key;
73
+ }
74
+ return null;
75
+ }
76
+
77
+ /**
78
+ * Collect vendor property names from CRM-style attribute expressions + labelKey.
79
+ * @param {Record<string, unknown>} body
80
+ * @returns {string[]}
81
+ */
82
+ function collectCrmPropertyNames(body) {
83
+ const names = new Set();
84
+ const labelKeys = Array.isArray(body.labelKey) ? body.labelKey : [];
85
+ for (const label of labelKeys) {
86
+ const key = String(label || '').trim();
87
+ if (key) {
88
+ names.add(key);
89
+ }
90
+ }
91
+ for (const [key, row] of Object.entries(mappingAttributes(body))) {
92
+ if (RESERVED_ATTR.has(key)) {
93
+ continue;
94
+ }
95
+ const name = propertyNameFromAttr(key, row);
96
+ if (name) {
97
+ names.add(name);
98
+ }
99
+ }
100
+ return [...names].sort();
101
+ }
102
+
103
+ function ensureCrmPropertiesBag(schema) {
104
+ if (!schema || !schema.properties || typeof schema.properties !== 'object') {
105
+ return;
106
+ }
107
+ schema.properties.properties = {
108
+ type: 'object',
109
+ additionalProperties: true,
110
+ description: 'Vendor nested properties bag (CRM object APIs)'
111
+ };
112
+ schema.properties.associations = {
113
+ type: 'object',
114
+ additionalProperties: true,
115
+ description: 'Associated records bag from vendor list/get'
116
+ };
117
+ }
118
+
119
+ function rewriteCrmLabelExpressions(mappings, labelKeys) {
120
+ for (const label of labelKeys) {
121
+ if (mappings.attributes[label]) {
122
+ mappings.attributes[label].expression = `{{raw.properties.${label}}}`;
123
+ mappings.attributes[label].description =
124
+ 'Display label from vendor properties object';
125
+ }
126
+ }
127
+ }
128
+
129
+ function rewriteCrmAttributeExpressions(mappings, labelKeys) {
130
+ for (const [key, row] of Object.entries(mappings.attributes)) {
131
+ if (RESERVED_ATTR.has(key) || labelKeys.includes(key) || !row || typeof row !== 'object') {
132
+ continue;
133
+ }
134
+ const expr = String(/** @type {any} */ (row).expression || '');
135
+ if (expr === `{{raw.${key}}}` || !expr.includes('raw.properties.')) {
136
+ /** @type {any} */ (row).expression = `{{raw.properties.${key}}}`;
137
+ if (!/** @type {any} */ (row).description) {
138
+ /** @type {any} */ (row).description = `Synced property ${key}`;
139
+ }
140
+ }
141
+ }
142
+ }
143
+
144
+ function ensureCrmIdentityMappings(mappings) {
145
+ if (mappings.attributes.id) {
146
+ mappings.attributes.id.expression = '{{raw.id}}';
147
+ }
148
+ if (mappings.attributes.externalId) {
149
+ mappings.attributes.externalId.expression = '{{raw.id}}';
150
+ }
151
+ if (!mappings.attributes.associations) {
152
+ mappings.attributes.associations = {
153
+ expression: '{{raw.associations}}',
154
+ description: 'Associated records returned when OpenAPI associations query is set'
155
+ };
156
+ }
157
+ }
158
+
159
+ /**
160
+ * When collection paths look like CRM object APIs, prefer nested properties for labels
161
+ * and request those properties on list/get CIP queries.
162
+ * @param {Record<string, unknown>} body
163
+ * @param {string} samplePath
164
+ * @returns {void}
165
+ */
166
+ function maybeEnrichCrmStyleFieldMappings(body, samplePath) {
167
+ if (!/\/objects\//i.test(samplePath)) {
168
+ return;
169
+ }
170
+ const mappings = /** @type {any} */ (body.fieldMappings);
171
+ if (!mappings || !mappings.attributes || typeof mappings.attributes !== 'object') {
172
+ return;
173
+ }
174
+ const labelKeys = Array.isArray(body.labelKey) ? body.labelKey : ['name'];
175
+ ensureCrmPropertiesBag(/** @type {any} */ (body.metadataSchema));
176
+ rewriteCrmLabelExpressions(mappings, labelKeys);
177
+ rewriteCrmAttributeExpressions(mappings, labelKeys);
178
+ ensureCrmIdentityMappings(mappings);
179
+ applyCrmPropertiesQuery(body);
180
+ }
181
+
182
+ function resolveCipOperations(body) {
183
+ if (
184
+ body.execution &&
185
+ typeof body.execution === 'object' &&
186
+ /** @type {any} */ (body.execution).cip &&
187
+ typeof /** @type {any} */ (body.execution).cip === 'object'
188
+ ) {
189
+ return /** @type {any} */ (body.execution).cip.operations;
190
+ }
191
+ return null;
192
+ }
193
+
194
+ function applyPropertiesToFetchSteps(op, joined) {
195
+ if (!op || typeof op !== 'object' || !Array.isArray(/** @type {any} */ (op).steps)) {
196
+ return;
197
+ }
198
+ for (const step of /** @type {any} */ (op).steps) {
199
+ if (!step || !step.fetch || typeof step.fetch !== 'object') {
200
+ continue;
201
+ }
202
+ if (!step.fetch.query || typeof step.fetch.query !== 'object') {
203
+ step.fetch.query = {};
204
+ }
205
+ step.fetch.query.properties = joined;
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Set CIP list/get query.properties from mapped CRM property names.
211
+ * @param {Record<string, unknown>} body
212
+ * @returns {void}
213
+ */
214
+ function applyCrmPropertiesQuery(body) {
215
+ const propertyNames = collectCrmPropertyNames(body);
216
+ if (propertyNames.length === 0) {
217
+ return;
218
+ }
219
+ const cipOps = resolveCipOperations(body);
220
+ if (!cipOps || typeof cipOps !== 'object') {
221
+ return;
222
+ }
223
+ const joined = propertyNames.join(',');
224
+ for (const [capKey, op] of Object.entries(cipOps)) {
225
+ const leaf = String(capKey || '')
226
+ .split('.')
227
+ .pop()
228
+ .toLowerCase();
229
+ if (leaf === 'list' || leaf === 'get') {
230
+ applyPropertiesToFetchSteps(op, joined);
231
+ }
232
+ }
233
+ }
234
+
235
+ module.exports = {
236
+ seedPropertyKeysOnDatasource,
237
+ collectCrmPropertyNames,
238
+ maybeEnrichCrmStyleFieldMappings,
239
+ applyCrmPropertiesQuery
240
+ };
@@ -0,0 +1,434 @@
1
+ /**
2
+ * Bind BID capability keys to OpenAPI method/path (vendor-agnostic heuristics).
3
+ * @fileoverview BID apply OpenAPI enrichment
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const fs = require('../internal/fs-real-sync');
9
+ const path = require('path');
10
+ const {
11
+ maybeEnrichCrmStyleFieldMappings
12
+ } = require('./enrich-crm-field-mappings');
13
+
14
+ const PATH_PARAM_RE = /\{(\w+)\}/g;
15
+
16
+ /**
17
+ * @param {string} ref
18
+ * @param {string} [baseDir]
19
+ * @returns {string}
20
+ */
21
+ function resolveOpenApiRefPath(ref, baseDir) {
22
+ const trimmed = String(ref || '').trim();
23
+ if (!trimmed) {
24
+ throw new Error('openapiSourceRef is empty');
25
+ }
26
+ if (path.isAbsolute(trimmed)) {
27
+ return trimmed;
28
+ }
29
+ const moduleEnrichmentDir = path.join(__dirname, 'openapi-enrichment');
30
+ // Prefer caller baseDir, then bundled openapi-enrichment, then cwd.
31
+ // Cwd last avoids false misses when Jest mocks fs.existsSync but readFileSync
32
+ // remains real (fallback would otherwise point at <cwd>/<ref>).
33
+ const candidates = [
34
+ baseDir ? path.resolve(baseDir, trimmed) : null,
35
+ path.join(moduleEnrichmentDir, trimmed),
36
+ path.join(moduleEnrichmentDir, path.basename(trimmed)),
37
+ path.resolve(process.cwd(), trimmed)
38
+ ].filter(Boolean);
39
+ for (const candidate of candidates) {
40
+ if (fs.existsSync(candidate)) {
41
+ return candidate;
42
+ }
43
+ }
44
+ return candidates[0];
45
+ }
46
+
47
+ /**
48
+ * @param {string} ref
49
+ * @param {string} [baseDir]
50
+ * @returns {Record<string, unknown>}
51
+ */
52
+ function loadOpenApiDocument(ref, baseDir) {
53
+ const abs = resolveOpenApiRefPath(ref, baseDir);
54
+ const raw = fs.readFileSync(abs, 'utf8');
55
+ const doc = JSON.parse(raw);
56
+ if (!doc || typeof doc !== 'object' || !doc.paths || typeof doc.paths !== 'object') {
57
+ throw new Error(`OpenAPI document missing paths: ${abs}`);
58
+ }
59
+ return doc;
60
+ }
61
+
62
+ /**
63
+ * Object-collection segment from paths like `/crm/v3/objects/companies`.
64
+ * @param {string} opPath
65
+ * @returns {string | null}
66
+ */
67
+ function objectTypeFromOpenApiPath(opPath) {
68
+ const match = String(opPath || '').match(/\/objects\/([^/{?]+)/i);
69
+ const value = match && match[1] ? String(match[1]).trim() : '';
70
+ if (!value) {
71
+ return null;
72
+ }
73
+ // Skip opaque numeric type ids (e.g. HubSpot `0-3`); associations query
74
+ // expects named collection segments when available.
75
+ if (/^\d/.test(value)) {
76
+ return null;
77
+ }
78
+ return value;
79
+ }
80
+
81
+ /**
82
+ * @param {Record<string, unknown>} doc
83
+ * @param {string} method
84
+ * @param {string} opPath
85
+ * @returns {boolean}
86
+ */
87
+ /**
88
+ * @param {Record<string, unknown>} doc
89
+ * @param {string} method
90
+ * @param {string} opPath
91
+ * @returns {any | null}
92
+ */
93
+ function operationFromDoc(doc, method, opPath) {
94
+ const pathItem = /** @type {any} */ (doc && doc.paths ? doc.paths[opPath] : null);
95
+ if (!pathItem || typeof pathItem !== 'object') {
96
+ return null;
97
+ }
98
+ return (
99
+ pathItem[String(method || '').toLowerCase()] ||
100
+ pathItem[String(method || '').toUpperCase()] ||
101
+ null
102
+ );
103
+ }
104
+
105
+ /**
106
+ * @param {Record<string, unknown>} doc
107
+ * @param {string} method
108
+ * @param {string} opPath
109
+ * @returns {any[]}
110
+ */
111
+ function operationParameters(doc, method, opPath) {
112
+ const pathItem = /** @type {any} */ (doc && doc.paths ? doc.paths[opPath] : null);
113
+ const op = operationFromDoc(doc, method, opPath);
114
+ return [
115
+ ...(pathItem && Array.isArray(pathItem.parameters) ? pathItem.parameters : []),
116
+ ...(op && Array.isArray(op.parameters) ? op.parameters : [])
117
+ ];
118
+ }
119
+
120
+ function operationDeclaresAssociationsQuery(doc, method, opPath) {
121
+ return operationParameters(doc, method, opPath).some(
122
+ (row) =>
123
+ row &&
124
+ typeof row === 'object' &&
125
+ row.in === 'query' &&
126
+ String(row.name || '').toLowerCase() === 'associations'
127
+ );
128
+ }
129
+
130
+ /**
131
+ * Detect cursor list pagination from OpenAPI query params + response schema/docs.
132
+ * Vendor-agnostic: only emits when both cursor and page-size query params exist and
133
+ * response/docs mention a paging.next.after style cursor field.
134
+ * @param {Record<string, unknown>} doc
135
+ * @param {string} method
136
+ * @param {string} opPath
137
+ * @returns {{ cursorParam: string, pageSizeParam: string, cursorField: string } | null}
138
+ */
139
+ function operationCursorPagination(doc, method, opPath) {
140
+ const params = operationParameters(doc, method, opPath).filter(
141
+ (row) => row && typeof row === 'object' && row.in === 'query'
142
+ );
143
+ const names = new Set(
144
+ params.map((row) => String(row.name || '').toLowerCase()).filter(Boolean)
145
+ );
146
+ if (!names.has('after') || !names.has('limit')) {
147
+ return null;
148
+ }
149
+ const blob = JSON.stringify({
150
+ params,
151
+ op: operationFromDoc(doc, method, opPath)
152
+ });
153
+ if (!/paging\.next\.after/i.test(blob)) {
154
+ return null;
155
+ }
156
+ return {
157
+ cursorParam: 'after',
158
+ pageSizeParam: 'limit',
159
+ cursorField: '$.paging.next.after'
160
+ };
161
+ }
162
+
163
+ /**
164
+ * @param {string} method
165
+ * @param {string} opPath
166
+ * @param {Record<string, unknown>} op
167
+ * @returns {{ method: string, path: string, operationId: string, pathParamCount: number }}
168
+ */
169
+ function normalizeOp(method, opPath, op) {
170
+ const params = [...String(opPath).matchAll(PATH_PARAM_RE)].map((m) => m[1]);
171
+ return {
172
+ method: String(method || '').toUpperCase(),
173
+ path: String(opPath),
174
+ operationId: typeof op.operationId === 'string' ? op.operationId : '',
175
+ pathParamCount: params.length
176
+ };
177
+ }
178
+
179
+ /**
180
+ * @param {Record<string, unknown>} doc
181
+ * @returns {Array<{ method: string, path: string, operationId: string, pathParamCount: number }>}
182
+ */
183
+ function indexOpenApiOperations(doc) {
184
+ const out = [];
185
+ for (const [opPath, methods] of Object.entries(doc.paths || {})) {
186
+ if (!methods || typeof methods !== 'object') {
187
+ continue;
188
+ }
189
+ for (const [method, op] of Object.entries(methods)) {
190
+ if (!op || typeof op !== 'object' || method.startsWith('x-')) {
191
+ continue;
192
+ }
193
+ if (!['get', 'post', 'put', 'patch', 'delete'].includes(method.toLowerCase())) {
194
+ continue;
195
+ }
196
+ out.push(normalizeOp(method, opPath, /** @type {Record<string, unknown>} */ (op)));
197
+ }
198
+ }
199
+ return out;
200
+ }
201
+
202
+ /**
203
+ * @param {string} text
204
+ * @returns {boolean}
205
+ */
206
+ function isBatchOrSearch(text) {
207
+ // Keep DELETE archive ops; exclude batch/search helpers only.
208
+ return /\/batch\/|\bbatch\b|\/search|merge|upsert/i.test(text);
209
+ }
210
+
211
+ /**
212
+ * @param {string} capabilityKey
213
+ * @param {Array<{ method: string, path: string, operationId: string, pathParamCount: number }>} ops
214
+ * @returns {{ method: string, path: string, operationId: string } | null}
215
+ */
216
+ function pickFirstMatchingOp(candidates, predicate, idPattern) {
217
+ const hits = candidates.filter(predicate);
218
+ return hits.find((op) => idPattern.test(op.operationId)) || hits[0] || null;
219
+ }
220
+
221
+ function pickOperationForCapability(capabilityKey, ops) {
222
+ const key = String(capabilityKey || '').toLowerCase();
223
+ const candidates = ops.filter((op) => !isBatchOrSearch(`${op.path} ${op.operationId}`));
224
+ const rules = {
225
+ list: {
226
+ predicate: (op) => op.method === 'GET' && op.pathParamCount === 0,
227
+ idPattern: /getpage|list|getall/i
228
+ },
229
+ get: {
230
+ predicate: (op) => op.method === 'GET' && op.pathParamCount >= 1,
231
+ idPattern: /getbyid|get.*id|read/i
232
+ },
233
+ create: {
234
+ predicate: (op) => op.method === 'POST' && op.pathParamCount === 0,
235
+ idPattern: /create$|_create$/i
236
+ },
237
+ createbasic: {
238
+ predicate: (op) => op.method === 'POST' && op.pathParamCount === 0,
239
+ idPattern: /create$|_create$/i
240
+ },
241
+ update: {
242
+ predicate: (op) =>
243
+ (op.method === 'PATCH' || op.method === 'PUT') && op.pathParamCount >= 1,
244
+ idPattern: /update$|_update$/i
245
+ },
246
+ updatebasic: {
247
+ predicate: (op) =>
248
+ (op.method === 'PATCH' || op.method === 'PUT') && op.pathParamCount >= 1,
249
+ idPattern: /update$|_update$/i
250
+ },
251
+ delete: {
252
+ predicate: (op) => op.method === 'DELETE' && op.pathParamCount >= 1,
253
+ idPattern: /archive|delete/i
254
+ }
255
+ };
256
+ const rule = rules[key];
257
+ if (!rule) {
258
+ return null;
259
+ }
260
+ return pickFirstMatchingOp(candidates, rule.predicate, rule.idPattern);
261
+ }
262
+
263
+ /**
264
+ * @param {Record<string, unknown>} body
265
+ * @param {{ method: string, path: string, operationId: string }} binding
266
+ * @param {string} capabilityKey
267
+ * @param {{
268
+ * hasAssociationsQuery?: boolean,
269
+ * associationTargets?: string[],
270
+ * cursorPagination?: { cursorParam: string, pageSizeParam: string, cursorField: string } | null
271
+ * }} [bindOptions]
272
+ * @returns {void}
273
+ */
274
+ function bindOpenApiOperation(body, binding, capabilityKey) {
275
+ if (!body.openapi || typeof body.openapi !== 'object') {
276
+ body.openapi = {};
277
+ }
278
+ const openapi = /** @type {Record<string, unknown>} */ (body.openapi);
279
+ openapi.enabled = true;
280
+ if (!openapi.operations || typeof openapi.operations !== 'object') {
281
+ openapi.operations = {};
282
+ }
283
+ const ops = /** @type {Record<string, unknown>} */ (openapi.operations);
284
+ const prev = ops[capabilityKey] && typeof ops[capabilityKey] === 'object'
285
+ ? /** @type {Record<string, unknown>} */ (ops[capabilityKey])
286
+ : {};
287
+ ops[capabilityKey] = {
288
+ ...prev,
289
+ operationId: binding.operationId || prev.operationId || capabilityKey,
290
+ method: binding.method,
291
+ path: binding.path
292
+ };
293
+ }
294
+
295
+ function createCipQuery(capabilityKey, bindOptions) {
296
+ const cap = String(capabilityKey || '').toLowerCase();
297
+ const query = {};
298
+ const targets = Array.isArray(bindOptions.associationTargets)
299
+ ? bindOptions.associationTargets.map((row) => String(row || '').trim()).filter(Boolean)
300
+ : [];
301
+ if (targets.length > 0 && bindOptions.hasAssociationsQuery && (cap === 'list' || cap === 'get')) {
302
+ query.associations = targets.join(',');
303
+ }
304
+ const cursorPagination =
305
+ bindOptions.cursorPagination && typeof bindOptions.cursorPagination === 'object'
306
+ ? bindOptions.cursorPagination
307
+ : null;
308
+ if (cap === 'list' && cursorPagination) {
309
+ query[cursorPagination.pageSizeParam] = `{{${cursorPagination.pageSizeParam}}}`;
310
+ query[cursorPagination.cursorParam] = `{{${cursorPagination.cursorParam}}}`;
311
+ }
312
+ return { cap, query, cursorPagination };
313
+ }
314
+
315
+ function createCipSteps(capabilityKey, binding, cipQuery) {
316
+ const { cap, query, cursorPagination } = cipQuery;
317
+ const steps = [{ fetch: { source: 'openapi', openapiRef: capabilityKey, query } }];
318
+ if (cap === 'list' && /\/objects\//i.test(binding.path)) {
319
+ if (cursorPagination) {
320
+ steps.push({
321
+ paginate: {
322
+ strategy: 'cursor',
323
+ cursorField: cursorPagination.cursorField,
324
+ cursorParam: cursorPagination.cursorParam,
325
+ pageSizeParam: cursorPagination.pageSizeParam,
326
+ pageSize: 100,
327
+ maxPages: 100
328
+ }
329
+ });
330
+ }
331
+ steps.push({ map: { useFieldMappings: true, inputPath: '$.results[*]' } });
332
+ } else if (cap === 'get' || /^create/.test(cap) || /^update/.test(cap) || cap === 'delete' || cap === 'archive') {
333
+ steps.push({ map: { useFieldMappings: true, inputPath: '$' } });
334
+ }
335
+ steps.push({ output: { mode: 'records' } });
336
+ return steps;
337
+ }
338
+
339
+ function bindCipOperation(body, binding, capabilityKey, bindOptions) {
340
+ if (!body.execution || typeof body.execution !== 'object') {
341
+ body.execution = { engine: 'cip' };
342
+ }
343
+ const execution = /** @type {Record<string, unknown>} */ (body.execution);
344
+ if (!execution.cip || typeof execution.cip !== 'object') {
345
+ execution.cip = { version: '1.0', operations: {} };
346
+ }
347
+ const cip = /** @type {Record<string, unknown>} */ (execution.cip);
348
+ if (!cip.operations || typeof cip.operations !== 'object') {
349
+ cip.operations = {};
350
+ }
351
+ const cipOps = /** @type {Record<string, unknown>} */ (cip.operations);
352
+ const existing = cipOps[capabilityKey];
353
+ const steps = createCipSteps(capabilityKey, binding, createCipQuery(capabilityKey, bindOptions));
354
+ cipOps[capabilityKey] = {
355
+ ...(existing && typeof existing === 'object' ? existing : {}),
356
+ enabled: true,
357
+ description:
358
+ (existing &&
359
+ typeof existing === 'object' &&
360
+ /** @type {any} */ (existing).description) ||
361
+ `Execute ${capabilityKey} via OpenAPI`,
362
+ steps
363
+ };
364
+ }
365
+
366
+ function applyBindingToDatasource(body, binding, capabilityKey, bindOptions = {}) {
367
+ bindOpenApiOperation(body, binding, capabilityKey);
368
+ bindCipOperation(body, binding, capabilityKey, bindOptions);
369
+ }
370
+
371
+ /**
372
+ * Enrich datasource openapi.operations + CIP steps from an OpenAPI document.
373
+ * @param {Record<string, unknown>} body - datasource body (mutated)
374
+ * @param {string} openapiSourceRef
375
+ * @param {{ baseDir?: string, associationTargets?: string[] }} [options]
376
+ * @returns {string[]} capability keys that received method/path bindings
377
+ */
378
+ function enrichDatasourceFromOpenApi(body, openapiSourceRef, options = {}) {
379
+ const doc = loadOpenApiDocument(openapiSourceRef, options.baseDir);
380
+ const indexed = indexOpenApiOperations(doc);
381
+ const caps = Array.isArray(body.capabilities)
382
+ ? body.capabilities
383
+ .map((row) => (row && typeof row === 'object' ? row.key : row))
384
+ .filter((k) => typeof k === 'string' && k.trim())
385
+ : [];
386
+ const selfTypeCandidates = indexed
387
+ .map((op) => objectTypeFromOpenApiPath(op.path))
388
+ .filter(Boolean);
389
+ // Opaque OpenAPI paths (e.g. /objects/0-3) yield no segment — fall back to BID ds key.
390
+ const selfType =
391
+ selfTypeCandidates[0] ||
392
+ (typeof body.key === 'string' && body.key.trim() ? body.key.trim() : null);
393
+ const associationTargets = (
394
+ Array.isArray(options.associationTargets) ? options.associationTargets : []
395
+ )
396
+ .map((row) => String(row || '').trim())
397
+ .filter((row) => row && row !== selfType);
398
+ const bound = [];
399
+ let samplePath = '';
400
+ for (const capabilityKey of caps) {
401
+ const picked = pickOperationForCapability(capabilityKey, indexed);
402
+ if (!picked) {
403
+ continue;
404
+ }
405
+ applyBindingToDatasource(body, picked, capabilityKey, {
406
+ hasAssociationsQuery: operationDeclaresAssociationsQuery(
407
+ doc,
408
+ picked.method,
409
+ picked.path
410
+ ),
411
+ associationTargets,
412
+ cursorPagination: operationCursorPagination(doc, picked.method, picked.path)
413
+ });
414
+ bound.push(capabilityKey);
415
+ if (!samplePath) {
416
+ samplePath = picked.path;
417
+ }
418
+ }
419
+ if (samplePath) {
420
+ maybeEnrichCrmStyleFieldMappings(body, samplePath);
421
+ }
422
+ return bound;
423
+ }
424
+
425
+ module.exports = {
426
+ resolveOpenApiRefPath,
427
+ loadOpenApiDocument,
428
+ indexOpenApiOperations,
429
+ pickOperationForCapability,
430
+ enrichDatasourceFromOpenApi,
431
+ objectTypeFromOpenApiPath,
432
+ operationDeclaresAssociationsQuery,
433
+ operationCursorPagination
434
+ };