@memberjunction/integration-connectors 5.35.0 → 5.37.0

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 (81) hide show
  1. package/dist/AptifyConnector.d.ts +88 -0
  2. package/dist/AptifyConnector.d.ts.map +1 -0
  3. package/dist/AptifyConnector.js +737 -0
  4. package/dist/AptifyConnector.js.map +1 -0
  5. package/dist/BettyConnector.d.ts +288 -0
  6. package/dist/BettyConnector.d.ts.map +1 -0
  7. package/dist/BettyConnector.js +909 -0
  8. package/dist/BettyConnector.js.map +1 -0
  9. package/dist/BlackbaudConnector.d.ts +64 -0
  10. package/dist/BlackbaudConnector.d.ts.map +1 -0
  11. package/dist/BlackbaudConnector.js +916 -0
  12. package/dist/BlackbaudConnector.js.map +1 -0
  13. package/dist/ConstantContactConnector.d.ts +126 -0
  14. package/dist/ConstantContactConnector.d.ts.map +1 -0
  15. package/dist/ConstantContactConnector.js +636 -0
  16. package/dist/ConstantContactConnector.js.map +1 -0
  17. package/dist/GrowthZoneConnector.d.ts +185 -0
  18. package/dist/GrowthZoneConnector.d.ts.map +1 -0
  19. package/dist/GrowthZoneConnector.js +745 -0
  20. package/dist/GrowthZoneConnector.js.map +1 -0
  21. package/dist/HubSpotConnector.d.ts +2 -2
  22. package/dist/HubSpotConnector.d.ts.map +1 -1
  23. package/dist/HubSpotConnector.js +3 -3
  24. package/dist/HubSpotConnector.js.map +1 -1
  25. package/dist/IMISConnector.d.ts +155 -0
  26. package/dist/IMISConnector.d.ts.map +1 -0
  27. package/dist/IMISConnector.js +686 -0
  28. package/dist/IMISConnector.js.map +1 -0
  29. package/dist/MJToMJConnector.d.ts +279 -0
  30. package/dist/MJToMJConnector.d.ts.map +1 -0
  31. package/dist/MJToMJConnector.js +893 -0
  32. package/dist/MJToMJConnector.js.map +1 -0
  33. package/dist/MagnetMailConnector.d.ts +163 -0
  34. package/dist/MagnetMailConnector.d.ts.map +1 -0
  35. package/dist/MagnetMailConnector.js +816 -0
  36. package/dist/MagnetMailConnector.js.map +1 -0
  37. package/dist/MailchimpConnector.d.ts +188 -0
  38. package/dist/MailchimpConnector.d.ts.map +1 -0
  39. package/dist/MailchimpConnector.js +753 -0
  40. package/dist/MailchimpConnector.js.map +1 -0
  41. package/dist/NetForumConnector.d.ts +51 -0
  42. package/dist/NetForumConnector.d.ts.map +1 -0
  43. package/dist/NetForumConnector.js +419 -0
  44. package/dist/NetForumConnector.js.map +1 -0
  45. package/dist/NetSuiteConnector.d.ts +70 -0
  46. package/dist/NetSuiteConnector.d.ts.map +1 -0
  47. package/dist/NetSuiteConnector.js +605 -0
  48. package/dist/NetSuiteConnector.js.map +1 -0
  49. package/dist/NimbleAMSConnector.d.ts +49 -0
  50. package/dist/NimbleAMSConnector.d.ts.map +1 -0
  51. package/dist/NimbleAMSConnector.js +408 -0
  52. package/dist/NimbleAMSConnector.js.map +1 -0
  53. package/dist/PropFuelConnector.d.ts +68 -0
  54. package/dist/PropFuelConnector.d.ts.map +1 -0
  55. package/dist/PropFuelConnector.js +514 -0
  56. package/dist/PropFuelConnector.js.map +1 -0
  57. package/dist/RasaConnector.d.ts +26 -0
  58. package/dist/RasaConnector.d.ts.map +1 -1
  59. package/dist/RasaConnector.js +233 -1
  60. package/dist/RasaConnector.js.map +1 -1
  61. package/dist/Reach360Connector.d.ts +165 -0
  62. package/dist/Reach360Connector.d.ts.map +1 -0
  63. package/dist/Reach360Connector.js +710 -0
  64. package/dist/Reach360Connector.js.map +1 -0
  65. package/dist/SharePointConnector.d.ts +181 -0
  66. package/dist/SharePointConnector.d.ts.map +1 -0
  67. package/dist/SharePointConnector.js +796 -0
  68. package/dist/SharePointConnector.js.map +1 -0
  69. package/dist/WicketConnector.d.ts +1 -0
  70. package/dist/WicketConnector.d.ts.map +1 -1
  71. package/dist/WicketConnector.js +6 -0
  72. package/dist/WicketConnector.js.map +1 -1
  73. package/dist/WildApricotConnector.d.ts +188 -0
  74. package/dist/WildApricotConnector.d.ts.map +1 -0
  75. package/dist/WildApricotConnector.js +1092 -0
  76. package/dist/WildApricotConnector.js.map +1 -0
  77. package/dist/index.d.ts +16 -0
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.js +16 -0
  80. package/dist/index.js.map +1 -1
  81. package/package.json +6 -6
@@ -0,0 +1,909 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { RegisterClass } from '@memberjunction/global';
8
+ import { Metadata } from '@memberjunction/core';
9
+ import { BaseIntegrationConnector, BaseRESTIntegrationConnector, } from '@memberjunction/integration-engine';
10
+ // ─── Constants ──────────────────────────────────────────────────────
11
+ /** Default HTTP timeout in milliseconds. */
12
+ const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
13
+ /**
14
+ * Default minimum interval between requests. Betty rate limits are
15
+ * undocumented; 500ms = 2 req/sec is conservative.
16
+ */
17
+ const DEFAULT_MIN_REQUEST_INTERVAL_MS = 500;
18
+ /** Max retries on 429/503 before giving up. */
19
+ const MAX_RETRIES = 4;
20
+ /** Base backoff in ms for exponential backoff on 429/503. */
21
+ const BASE_BACKOFF_MS = 1000;
22
+ /** Candidate paths where a tenant might expose an OpenAPI / Swagger spec. */
23
+ const OPENAPI_CANDIDATE_PATHS = [
24
+ '/openapi.json',
25
+ '/swagger.json',
26
+ '/v1/openapi.json',
27
+ '/api/openapi.json',
28
+ '/api/swagger.json',
29
+ '/swagger/v1/swagger.json',
30
+ ];
31
+ /** Candidate ResponseDataKey field names when the shape is unknown. */
32
+ const CANDIDATE_DATA_KEYS = ['items', 'data', 'results', 'records'];
33
+ /**
34
+ * Static seed objects returned by DiscoverObjects when runtime discovery
35
+ * finds no additional objects, or to merge with discovered ones. Mirrors
36
+ * the entities in the research metadata JSON.
37
+ */
38
+ const BETTY_STATIC_OBJECTS = [
39
+ {
40
+ Name: 'Conversations',
41
+ Label: 'Conversations',
42
+ Description: 'Chat session metadata — maps to MJ: Conversations.',
43
+ SupportsIncrementalSync: true,
44
+ SupportsWrite: false,
45
+ },
46
+ {
47
+ Name: 'Messages',
48
+ Label: 'Messages',
49
+ Description: 'Individual conversation turns — maps to MJ: Conversation Details. Parent-child of Conversations.',
50
+ SupportsIncrementalSync: true,
51
+ SupportsWrite: false,
52
+ },
53
+ {
54
+ Name: 'KnowledgeSources',
55
+ Label: 'Knowledge Sources',
56
+ Description: 'Documents Betty has been trained on (per-tenant).',
57
+ SupportsIncrementalSync: true,
58
+ SupportsWrite: false,
59
+ },
60
+ {
61
+ Name: 'Users',
62
+ Label: 'Users',
63
+ Description: 'Betty-tracked identities used for conversation ownership.',
64
+ SupportsIncrementalSync: true,
65
+ SupportsWrite: false,
66
+ },
67
+ {
68
+ Name: 'Feedback',
69
+ Label: 'Feedback',
70
+ Description: 'User ratings/corrections on Betty responses. Parent-child of Messages.',
71
+ SupportsIncrementalSync: true,
72
+ SupportsWrite: false,
73
+ },
74
+ {
75
+ Name: 'ContentGaps',
76
+ Label: 'Content Gaps',
77
+ Description: 'Aggregate view of questions Betty could not answer confidently.',
78
+ SupportsIncrementalSync: true,
79
+ SupportsWrite: false,
80
+ },
81
+ ];
82
+ // ─── Connector Implementation ───────────────────────────────────────
83
+ /**
84
+ * Read-only connector for Betty AI (Brightfind/Tasio) — pulls conversational
85
+ * data from a customer's Betty tenant into MJ's native conversation entities.
86
+ *
87
+ * Destination entities in MJ:
88
+ * - Conversations -> `MJ: Conversations` (MJConversationEntity)
89
+ * - Messages -> `MJ: Conversation Details` (MJConversationDetailEntity)
90
+ * - Feedback -> linked to `MJ: Conversation Details` via message ID
91
+ * - KnowledgeSources / Users / ContentGaps -> reference/analytics data
92
+ * (no built-in MJ target; consumers route to custom tables if desired)
93
+ *
94
+ * Betty has **no public developer portal**. The tenant URL and API key must be
95
+ * provisioned by the Betty/Brightfind team. This connector:
96
+ * - attempts runtime OpenAPI discovery at common paths but tolerates absence
97
+ * - drives everything else from `MJ: Integration Objects` / `Integration Object Fields` metadata
98
+ * - is strictly READ-ONLY (CreateRecord/UpdateRecord/DeleteRecord throw)
99
+ *
100
+ * Configuration shape (stored on `MJ: Credentials.Values` or
101
+ * `MJ: Company Integrations.Configuration` as JSON):
102
+ * ```json
103
+ * {
104
+ * "TenantEndpoint": "https://customer.betty.example.com",
105
+ * "ApiKey": "<provisioned key>",
106
+ * "AuthHeader": "Authorization: Bearer {key}",
107
+ * "RequestTimeoutMs": 30000,
108
+ * "MinRequestIntervalMs": 500
109
+ * }
110
+ * ```
111
+ *
112
+ * The MJ field-mapping hint (`DefaultQueryParams.mj_field_map` on each
113
+ * IntegrationObject) documents how Betty fields should map to MJ
114
+ * Conversation / Conversation Detail columns at sync time — it is
115
+ * documentation only and not enforced by the connector.
116
+ */
117
+ let BettyConnector = class BettyConnector extends BaseRESTIntegrationConnector {
118
+ constructor() {
119
+ // ── Instance state ───────────────────────────────────────────────
120
+ super(...arguments);
121
+ /** Resolved config for the current run (populated on Authenticate). */
122
+ this.resolvedConfig = null;
123
+ /** Timestamp of the last HTTP request — used for throttling. */
124
+ this.lastRequestTime = 0;
125
+ /**
126
+ * Per-object index of the successful watermark field. The configured
127
+ * candidates list (`watermark_field_candidates` in DefaultQueryParams)
128
+ * is tried in order; once one succeeds we cache which one worked so we
129
+ * stop re-probing failed candidates for the rest of the run.
130
+ */
131
+ this.watermarkFieldIndexByObject = new Map();
132
+ }
133
+ // ── Capability getters (all default to false except Get) ─────────
134
+ get SupportsCreate() { return false; }
135
+ get SupportsUpdate() { return false; }
136
+ get SupportsDelete() { return false; }
137
+ get SupportsSearch() { return false; }
138
+ get SupportsListing() { return false; }
139
+ get IntegrationName() { return 'Betty AI'; }
140
+ // ── Abstract method implementations (BaseRESTIntegrationConnector) ──
141
+ /**
142
+ * Authenticates against a Betty tenant. There's no OAuth or session
143
+ * exchange — we just validate that a usable API key + endpoint exist
144
+ * and stash them onto the auth context so downstream calls can use them.
145
+ */
146
+ async Authenticate(companyIntegration, contextUser) {
147
+ const config = await this.ParseConfig(companyIntegration, contextUser);
148
+ this.resolvedConfig = config;
149
+ const auth = {
150
+ Token: config.ApiKey,
151
+ Config: config,
152
+ };
153
+ return auth;
154
+ }
155
+ /**
156
+ * Builds HTTP headers for every Betty request. The exact auth header is
157
+ * controlled by `BettyConnectionConfig.AuthHeader` so per-tenant
158
+ * conventions (`Authorization: Bearer …` vs `x-api-key: …`) can be
159
+ * switched without touching code.
160
+ */
161
+ BuildHeaders(auth) {
162
+ const bettyAuth = auth;
163
+ const style = bettyAuth.Config.AuthHeader ?? 'Authorization: Bearer {key}';
164
+ const headers = { 'Accept': 'application/json' };
165
+ const apiKey = bettyAuth.Config.ApiKey;
166
+ if (style === 'x-api-key: {key}') {
167
+ headers['x-api-key'] = apiKey;
168
+ }
169
+ else {
170
+ // Default to Bearer token.
171
+ headers['Authorization'] = `Bearer ${apiKey}`;
172
+ }
173
+ return headers;
174
+ }
175
+ /**
176
+ * Executes an HTTP request against the Betty tenant with throttling,
177
+ * timeout, and exponential backoff on 429/503.
178
+ */
179
+ async MakeHTTPRequest(auth, url, method, headers, body) {
180
+ const bettyAuth = auth;
181
+ const currentHeaders = { ...headers };
182
+ if (body !== undefined && !currentHeaders['Content-Type']) {
183
+ currentHeaders['Content-Type'] = 'application/json';
184
+ }
185
+ await this.applyThrottle(bettyAuth);
186
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
187
+ const response = await this.fetchRaw(bettyAuth, url, method, currentHeaders, body);
188
+ if (response.status === 429 || response.status === 503) {
189
+ if (attempt === MAX_RETRIES) {
190
+ return this.buildBettyResponse(response, await this.readResponseBodySafe(response));
191
+ }
192
+ await this.sleep(this.computeBackoffMs(response, attempt));
193
+ continue;
194
+ }
195
+ this.lastRequestTime = Date.now();
196
+ const parsedBody = await this.readResponseBodySafe(response);
197
+ return this.buildBettyResponse(response, parsedBody);
198
+ }
199
+ throw new Error(`Betty API request failed after ${MAX_RETRIES + 1} attempts: ${url}`);
200
+ }
201
+ /**
202
+ * Extracts the data array from a Betty response envelope using the
203
+ * IntegrationObject's ResponseDataKey. Falls back to a short list of
204
+ * common keys if none is configured.
205
+ */
206
+ NormalizeResponse(rawBody, responseDataKey) {
207
+ if (rawBody == null)
208
+ return [];
209
+ // Explicit data key from metadata takes priority.
210
+ if (responseDataKey != null) {
211
+ const extracted = this.extractArrayByKey(rawBody, responseDataKey);
212
+ if (extracted != null)
213
+ return extracted;
214
+ }
215
+ // Root-level array response.
216
+ if (Array.isArray(rawBody)) {
217
+ return rawBody;
218
+ }
219
+ // Fallback: try common candidate keys.
220
+ if (typeof rawBody === 'object') {
221
+ for (const key of CANDIDATE_DATA_KEYS) {
222
+ const extracted = this.extractArrayByKey(rawBody, key);
223
+ if (extracted != null)
224
+ return extracted;
225
+ }
226
+ // Single-record response — wrap in an array.
227
+ return [rawBody];
228
+ }
229
+ return [];
230
+ }
231
+ /**
232
+ * Extracts pagination state from a Betty response. Betty's per-tenant
233
+ * conventions vary, so we inspect a short list of common envelope keys
234
+ * (`nextCursor`, `next`, `next_page_token`) before falling back to
235
+ * inferring `HasMore` from record counts.
236
+ */
237
+ ExtractPaginationInfo(rawBody, paginationType, currentPage, currentOffset, pageSize) {
238
+ if (paginationType === 'None') {
239
+ return { HasMore: false };
240
+ }
241
+ const cursor = this.extractCursorFromBody(rawBody);
242
+ const recordCount = this.countRecordsInBody(rawBody);
243
+ if (paginationType === 'Cursor') {
244
+ return {
245
+ HasMore: cursor != null,
246
+ NextCursor: cursor ?? undefined,
247
+ };
248
+ }
249
+ // Offset / PageNumber: infer HasMore from record count vs pageSize.
250
+ const hasMore = recordCount >= pageSize;
251
+ return {
252
+ HasMore: hasMore,
253
+ NextPage: paginationType === 'PageNumber' ? currentPage + 1 : undefined,
254
+ NextOffset: paginationType === 'Offset' ? currentOffset + recordCount : undefined,
255
+ };
256
+ }
257
+ /** Base URL for all Betty API requests — just the tenant endpoint. */
258
+ GetBaseURL(companyIntegration, auth) {
259
+ const bettyAuth = auth;
260
+ if (bettyAuth.Config?.TenantEndpoint) {
261
+ return this.stripTrailingSlash(bettyAuth.Config.TenantEndpoint);
262
+ }
263
+ // Fallback: best-effort parse from CompanyIntegration.Configuration.
264
+ const configJson = companyIntegration.Configuration;
265
+ if (configJson) {
266
+ const parsed = this.safeParseJson(configJson);
267
+ if (parsed) {
268
+ const candidate = String(parsed['TenantEndpoint'] ?? parsed['tenantEndpoint'] ?? '').trim();
269
+ if (candidate)
270
+ return this.stripTrailingSlash(candidate);
271
+ }
272
+ }
273
+ throw new Error('BettyConnector: Cannot determine tenant base URL — no TenantEndpoint in configuration.');
274
+ }
275
+ // ─── TestConnection ──────────────────────────────────────────────
276
+ /**
277
+ * Tests connectivity to a Betty tenant. Because the health endpoint is not
278
+ * publicly documented we probe a few common paths and treat any
279
+ * authenticated response (200 or 401) as a sign that the tenant is
280
+ * reachable and the API key was at least accepted for evaluation.
281
+ *
282
+ * - 200 -> full success
283
+ * - 401 -> tenant reachable but the supplied key was rejected
284
+ * - 403 -> tenant reachable, key accepted, but path restricted
285
+ * - otherwise -> reported failure with the HTTP status for debugging
286
+ */
287
+ async TestConnection(companyIntegration, contextUser) {
288
+ try {
289
+ const auth = await this.Authenticate(companyIntegration, contextUser);
290
+ const baseURL = this.GetBaseURL(companyIntegration, auth);
291
+ const probeResult = await this.probeTenantReachable(auth, baseURL);
292
+ if (probeResult.success) {
293
+ return {
294
+ Success: true,
295
+ Message: probeResult.message,
296
+ ServerVersion: probeResult.serverVersion ?? 'Betty AI (version unreported)',
297
+ };
298
+ }
299
+ return { Success: false, Message: probeResult.message };
300
+ }
301
+ catch (err) {
302
+ const message = err instanceof Error ? err.message : String(err);
303
+ return {
304
+ Success: false,
305
+ Message: `Betty connection test failed: ${message}. Confirm TenantEndpoint, ApiKey, and AuthHeader style with the customer's Betty/Brightfind admin.`,
306
+ };
307
+ }
308
+ }
309
+ // ─── Discovery (static + runtime OpenAPI merge) ──────────────────
310
+ /**
311
+ * Combines the static IntegrationObject list with any additional objects
312
+ * surfaced by runtime OpenAPI discovery. Discovery failures are logged
313
+ * and tolerated — customer tenants without an OpenAPI spec still work
314
+ * from the static metadata alone.
315
+ */
316
+ async DiscoverObjects(companyIntegration, contextUser) {
317
+ const staticObjects = BETTY_STATIC_OBJECTS.map(o => ({
318
+ Name: o.Name,
319
+ Label: o.Label,
320
+ Description: o.Description,
321
+ SupportsIncrementalSync: o.SupportsIncrementalSync,
322
+ SupportsWrite: o.SupportsWrite,
323
+ }));
324
+ const staticNames = new Set(staticObjects.map(o => o.Name.toLowerCase()));
325
+ try {
326
+ const auth = await this.Authenticate(companyIntegration, contextUser);
327
+ const openapi = await this.attemptOpenAPIDiscovery(auth);
328
+ if (!openapi)
329
+ return staticObjects;
330
+ const discovered = this.extractObjectsFromOpenAPI(openapi, staticNames);
331
+ return [...staticObjects, ...discovered];
332
+ }
333
+ catch (err) {
334
+ const message = err instanceof Error ? err.message : String(err);
335
+ console.warn(`[Betty] OpenAPI discovery failed (${message}) — returning static objects only.`);
336
+ return staticObjects;
337
+ }
338
+ }
339
+ /**
340
+ * Fetches a sample record for the object and enumerates its keys. Any key
341
+ * absent from the static field metadata is flagged `IsCustom: true`.
342
+ */
343
+ async DiscoverFields(companyIntegration, objectName, contextUser) {
344
+ const staticFields = await super.DiscoverFields(companyIntegration, objectName, contextUser);
345
+ const staticNames = new Set(staticFields.map(f => f.Name.toLowerCase()));
346
+ try {
347
+ const auth = await this.Authenticate(companyIntegration, contextUser);
348
+ const sampleRecord = await this.fetchSampleRecord(auth, companyIntegration, objectName);
349
+ if (!sampleRecord)
350
+ return staticFields;
351
+ const discovered = this.buildCustomFieldSchemas(sampleRecord, staticNames);
352
+ return [...staticFields, ...discovered];
353
+ }
354
+ catch (err) {
355
+ const message = err instanceof Error ? err.message : String(err);
356
+ console.warn(`[Betty] DiscoverFields for "${objectName}" failed (${message}) — returning static fields only.`);
357
+ return staticFields;
358
+ }
359
+ }
360
+ // ─── Read-only guards ────────────────────────────────────────────
361
+ async CreateRecord() {
362
+ throw new Error('Betty connector is read-only — CreateRecord is not supported.');
363
+ }
364
+ async UpdateRecord() {
365
+ throw new Error('Betty connector is read-only — UpdateRecord is not supported.');
366
+ }
367
+ async DeleteRecord() {
368
+ throw new Error('Betty connector is read-only — DeleteRecord is not supported.');
369
+ }
370
+ // ─── Watermark-aware query param injection ───────────────────────
371
+ /**
372
+ * Extends base AppendDefaultQueryParams to translate the
373
+ * `watermark_field_candidates` hint into a real query param when a
374
+ * watermark is active. The connector tries candidates in order; once
375
+ * one returns without HTTP 400 we cache the winning index.
376
+ *
377
+ * MJ-mapping hints (`mj_target_entity`, `mj_field_map`) are stripped so
378
+ * they never leak onto an actual HTTP call.
379
+ */
380
+ AppendDefaultQueryParams(url, obj) {
381
+ const params = this.parseDefaultQueryParams(obj);
382
+ if (!params)
383
+ return url;
384
+ // Strip MJ-mapping-only hints so they aren't sent on the wire.
385
+ const cleaned = this.stripMjMappingHints(params);
386
+ // Build a temporary object wrapping the cleaned params as a JSON string
387
+ // so we can delegate to the base class's logic unchanged.
388
+ const synthetic = this.cloneWithQueryParams(obj, cleaned);
389
+ return super.AppendDefaultQueryParams(url, synthetic);
390
+ }
391
+ // ─── Runtime OpenAPI discovery helpers ───────────────────────────
392
+ /**
393
+ * Probes each OPENAPI_CANDIDATE_PATHS path. Returns the first valid
394
+ * OpenAPI document it can parse, or null if none succeed.
395
+ */
396
+ async attemptOpenAPIDiscovery(auth) {
397
+ const baseURL = this.stripTrailingSlash(auth.Config.TenantEndpoint);
398
+ for (const path of OPENAPI_CANDIDATE_PATHS) {
399
+ const url = `${baseURL}${path}`;
400
+ try {
401
+ const headers = this.BuildHeaders(auth);
402
+ const response = await this.MakeHTTPRequest(auth, url, 'GET', headers);
403
+ if (response.Status >= 200 && response.Status < 300 && response.Body && typeof response.Body === 'object') {
404
+ const doc = response.Body;
405
+ if (doc.paths && typeof doc.paths === 'object') {
406
+ return doc;
407
+ }
408
+ }
409
+ }
410
+ catch {
411
+ // Continue to next candidate path.
412
+ }
413
+ }
414
+ return null;
415
+ }
416
+ /**
417
+ * Extracts candidate integration objects from an OpenAPI document's
418
+ * `paths` map. Skips any object whose name (case-insensitive) is
419
+ * already in the static set. Everything returned is flagged as
420
+ * custom by the caller via a "Custom:" description prefix.
421
+ */
422
+ extractObjectsFromOpenAPI(doc, staticNames) {
423
+ const discovered = [];
424
+ const seen = new Set();
425
+ const paths = doc.paths ?? {};
426
+ for (const rawPath of Object.keys(paths)) {
427
+ const segment = this.firstPathSegment(rawPath);
428
+ if (!segment)
429
+ continue;
430
+ const normalized = this.toPascalCase(segment);
431
+ const lc = normalized.toLowerCase();
432
+ if (staticNames.has(lc) || seen.has(lc))
433
+ continue;
434
+ seen.add(lc);
435
+ discovered.push({
436
+ Name: normalized,
437
+ Label: normalized,
438
+ Description: `Custom: discovered via OpenAPI at ${rawPath}`,
439
+ SupportsIncrementalSync: false,
440
+ SupportsWrite: false,
441
+ });
442
+ }
443
+ return discovered;
444
+ }
445
+ /**
446
+ * Returns the first meaningful path segment from a path like
447
+ * `/conversations/{id}/messages` → `conversations`.
448
+ */
449
+ firstPathSegment(path) {
450
+ const parts = path.split('/').filter(p => p.length > 0 && !p.startsWith('{'));
451
+ return parts[0] ?? null;
452
+ }
453
+ /** Converts `conversations` / `knowledge-sources` to `Conversations` / `KnowledgeSources`. */
454
+ toPascalCase(input) {
455
+ return input
456
+ .split(/[-_\s]+/)
457
+ .filter(p => p.length > 0)
458
+ .map(p => p.charAt(0).toUpperCase() + p.slice(1))
459
+ .join('');
460
+ }
461
+ // ─── Field-discovery helpers ─────────────────────────────────────
462
+ /**
463
+ * Fetches a single sample record from the object's configured endpoint
464
+ * with `limit=1`. Returns null if no records are available or if the
465
+ * endpoint uses parent-child templating (we don't probe parents here).
466
+ */
467
+ async fetchSampleRecord(auth, companyIntegration, objectName) {
468
+ const obj = this.tryGetCachedObject(companyIntegration.IntegrationID, objectName);
469
+ if (!obj)
470
+ return null;
471
+ if (obj.APIPath.includes('{'))
472
+ return null; // Skip parent-scoped endpoints.
473
+ const baseURL = this.GetBaseURL(companyIntegration, auth);
474
+ const url = this.buildSampleURL(baseURL, obj.APIPath);
475
+ const headers = this.BuildHeaders(auth);
476
+ const response = await this.MakeHTTPRequest(auth, url, 'GET', headers);
477
+ if (response.Status < 200 || response.Status >= 300)
478
+ return null;
479
+ const records = this.NormalizeResponse(response.Body, obj.ResponseDataKey);
480
+ return records.length > 0 ? records[0] : null;
481
+ }
482
+ /**
483
+ * Builds ExternalFieldSchema entries for keys present in a sample
484
+ * record but absent from the configured static metadata. Flagged as
485
+ * custom via a "Custom:" description prefix.
486
+ */
487
+ buildCustomFieldSchemas(sample, staticNames) {
488
+ const out = [];
489
+ for (const key of Object.keys(sample)) {
490
+ if (staticNames.has(key.toLowerCase()))
491
+ continue;
492
+ out.push({
493
+ Name: key,
494
+ Label: key,
495
+ Description: 'Custom: discovered via runtime sample',
496
+ DataType: this.inferDataType(sample[key]),
497
+ IsRequired: false,
498
+ IsUniqueKey: false,
499
+ IsReadOnly: true,
500
+ IsForeignKey: false,
501
+ ForeignKeyTarget: null,
502
+ });
503
+ }
504
+ return out;
505
+ }
506
+ /** Infers a coarse data-type label from a JS value. */
507
+ inferDataType(value) {
508
+ if (value === null || value === undefined)
509
+ return 'nvarchar';
510
+ if (typeof value === 'number')
511
+ return Number.isInteger(value) ? 'int' : 'decimal';
512
+ if (typeof value === 'boolean')
513
+ return 'bit';
514
+ if (value instanceof Date)
515
+ return 'datetimeoffset';
516
+ if (Array.isArray(value) || typeof value === 'object')
517
+ return 'nvarchar'; // JSON-serialized
518
+ return 'nvarchar';
519
+ }
520
+ // ─── URL / param helpers ─────────────────────────────────────────
521
+ /** Builds a URL for a sample fetch, appending `limit=1`. */
522
+ buildSampleURL(baseURL, apiPath) {
523
+ const base = this.stripTrailingSlash(baseURL);
524
+ const path = apiPath.startsWith('/') ? apiPath : `/${apiPath}`;
525
+ const sep = path.includes('?') ? '&' : '?';
526
+ return `${base}${path}${sep}limit=1`;
527
+ }
528
+ /** Attempts to pull the cached IntegrationObject; returns null if it doesn't exist. */
529
+ tryGetCachedObject(integrationID, objectName) {
530
+ try {
531
+ return this.GetCachedObject(integrationID, objectName);
532
+ }
533
+ catch {
534
+ return null;
535
+ }
536
+ }
537
+ /**
538
+ * Parses DefaultQueryParams JSON into an object. Returns null if the
539
+ * field is missing or malformed.
540
+ */
541
+ parseDefaultQueryParams(obj) {
542
+ if (!obj.DefaultQueryParams)
543
+ return null;
544
+ const parsed = this.safeParseJson(obj.DefaultQueryParams);
545
+ if (!parsed || typeof parsed !== 'object')
546
+ return null;
547
+ const out = {};
548
+ for (const [k, v] of Object.entries(parsed)) {
549
+ if (v == null)
550
+ continue;
551
+ out[k] = String(v);
552
+ }
553
+ return out;
554
+ }
555
+ /**
556
+ * Removes MJ-mapping-only hints so they are never sent on the wire.
557
+ * These are documentation hints for field-mapping setup at sync time.
558
+ */
559
+ stripMjMappingHints(params) {
560
+ const dropKeys = new Set(['mj_target_entity', 'mj_field_map', 'watermark_field_candidates']);
561
+ const out = {};
562
+ for (const [k, v] of Object.entries(params)) {
563
+ if (!dropKeys.has(k))
564
+ out[k] = v;
565
+ }
566
+ return out;
567
+ }
568
+ /**
569
+ * Returns a shallow clone of the IntegrationObject with its
570
+ * DefaultQueryParams replaced by the given map. Used when we need the
571
+ * base class to process filtered params without mutating the cache.
572
+ */
573
+ cloneWithQueryParams(obj, params) {
574
+ // Shallow object-prototype clone is sufficient for use by
575
+ // AppendDefaultQueryParams which only reads DefaultQueryParams.
576
+ const cloned = Object.create(Object.getPrototypeOf(obj));
577
+ Object.assign(cloned, obj);
578
+ const json = Object.keys(params).length === 0 ? null : JSON.stringify(params);
579
+ // DefaultQueryParams is `string | null` on MJIntegrationObjectEntity.
580
+ cloned.DefaultQueryParams = json;
581
+ return cloned;
582
+ }
583
+ // ─── Response-parsing helpers ────────────────────────────────────
584
+ /**
585
+ * Extracts an array from a response envelope by key. Supports dotted
586
+ * paths (e.g., `data.items`). Returns null if the key is not present or
587
+ * the value at the key is not an array.
588
+ */
589
+ extractArrayByKey(body, key) {
590
+ if (typeof body !== 'object' || body == null)
591
+ return null;
592
+ let cursor = body;
593
+ for (const segment of key.split('.')) {
594
+ if (typeof cursor !== 'object' || cursor == null)
595
+ return null;
596
+ cursor = cursor[segment];
597
+ }
598
+ return Array.isArray(cursor) ? cursor : null;
599
+ }
600
+ /** Looks for common cursor-style keys at the response envelope's root. */
601
+ extractCursorFromBody(body) {
602
+ if (typeof body !== 'object' || body == null)
603
+ return null;
604
+ const obj = body;
605
+ const candidates = ['nextCursor', 'next_cursor', 'next', 'next_page_token', 'nextPageToken'];
606
+ for (const key of candidates) {
607
+ const value = obj[key];
608
+ if (typeof value === 'string' && value.length > 0)
609
+ return value;
610
+ }
611
+ // Also check nested `pagination`/`meta` envelopes.
612
+ for (const env of ['pagination', 'meta']) {
613
+ const inner = obj[env];
614
+ if (inner && typeof inner === 'object') {
615
+ const found = this.extractCursorFromBody(inner);
616
+ if (found)
617
+ return found;
618
+ }
619
+ }
620
+ return null;
621
+ }
622
+ /** Counts records in a response without knowing the exact data key. */
623
+ countRecordsInBody(body) {
624
+ if (Array.isArray(body))
625
+ return body.length;
626
+ if (typeof body !== 'object' || body == null)
627
+ return 0;
628
+ for (const key of CANDIDATE_DATA_KEYS) {
629
+ const arr = this.extractArrayByKey(body, key);
630
+ if (arr != null)
631
+ return arr.length;
632
+ }
633
+ return 0;
634
+ }
635
+ // ─── Auth/config resolution ──────────────────────────────────────
636
+ /**
637
+ * Resolves a BettyConnectionConfig from either the CompanyIntegration's
638
+ * linked credential record or its legacy `Configuration` JSON field.
639
+ */
640
+ async ParseConfig(companyIntegration, contextUser) {
641
+ const credentialID = companyIntegration.CredentialID;
642
+ if (credentialID) {
643
+ const fromCred = await this.loadConfigFromCredential(credentialID, contextUser);
644
+ if (fromCred)
645
+ return fromCred;
646
+ }
647
+ const configJson = companyIntegration.Configuration;
648
+ if (configJson) {
649
+ return this.parseConfigJson(configJson);
650
+ }
651
+ throw new Error('BettyConnector: No credentials or configuration found. Attach a Betty API credential ' +
652
+ '(TenantEndpoint + ApiKey) or set Configuration JSON on the CompanyIntegration.');
653
+ }
654
+ /**
655
+ * Loads a Betty config from a `MJ: Credentials` record. Returns null if
656
+ * the credential is missing, empty, or fails to parse.
657
+ */
658
+ async loadConfigFromCredential(credentialID, contextUser, provider) {
659
+ const md = provider ?? new Metadata();
660
+ const credential = await md.GetEntityObject('MJ: Credentials', contextUser);
661
+ const loaded = await credential.Load(credentialID);
662
+ if (!loaded || !credential.Values)
663
+ return null;
664
+ try {
665
+ return this.parseConfigJson(credential.Values);
666
+ }
667
+ catch {
668
+ return null;
669
+ }
670
+ }
671
+ /**
672
+ * Parses a JSON configuration payload into BettyConnectionConfig.
673
+ * Accepts PascalCase and camelCase keys for convenience.
674
+ */
675
+ parseConfigJson(json) {
676
+ const parsed = this.safeParseJson(json);
677
+ if (!parsed) {
678
+ throw new Error('BettyConnector: Configuration JSON is malformed.');
679
+ }
680
+ const tenantEndpoint = this.pickString(parsed, 'TenantEndpoint', 'tenantEndpoint', 'endpoint', 'Endpoint');
681
+ const apiKey = this.pickString(parsed, 'ApiKey', 'apiKey', 'APIKey', 'api_key');
682
+ if (!tenantEndpoint || !apiKey) {
683
+ throw new Error('BettyConnector: Configuration must include TenantEndpoint and ApiKey.');
684
+ }
685
+ const authHeaderRaw = this.pickString(parsed, 'AuthHeader', 'authHeader');
686
+ const authHeader = this.normalizeAuthHeader(authHeaderRaw);
687
+ const requestTimeoutMs = this.pickPositiveInt(parsed, 'RequestTimeoutMs', 'requestTimeoutMs');
688
+ const minRequestIntervalMs = this.pickPositiveInt(parsed, 'MinRequestIntervalMs', 'minRequestIntervalMs');
689
+ return {
690
+ TenantEndpoint: tenantEndpoint,
691
+ ApiKey: apiKey,
692
+ AuthHeader: authHeader,
693
+ RequestTimeoutMs: requestTimeoutMs,
694
+ MinRequestIntervalMs: minRequestIntervalMs,
695
+ };
696
+ }
697
+ /**
698
+ * Safely parses a JSON string, returning null (instead of throwing) on
699
+ * malformed input.
700
+ */
701
+ safeParseJson(json) {
702
+ try {
703
+ const parsed = JSON.parse(json);
704
+ return (typeof parsed === 'object' && parsed != null) ? parsed : null;
705
+ }
706
+ catch {
707
+ return null;
708
+ }
709
+ }
710
+ /** Picks the first defined string value among the given keys. */
711
+ pickString(source, ...keys) {
712
+ for (const key of keys) {
713
+ const value = source[key];
714
+ if (typeof value === 'string' && value.trim().length > 0)
715
+ return value.trim();
716
+ }
717
+ return undefined;
718
+ }
719
+ /** Picks the first positive integer value among the given keys. */
720
+ pickPositiveInt(source, ...keys) {
721
+ for (const key of keys) {
722
+ const value = source[key];
723
+ if (value == null)
724
+ continue;
725
+ const n = Number(value);
726
+ if (!Number.isNaN(n) && Number.isFinite(n) && n > 0)
727
+ return Math.floor(n);
728
+ }
729
+ return undefined;
730
+ }
731
+ /**
732
+ * Normalizes an AuthHeader string to the known literal values. Falls
733
+ * back to the bearer default if the input is unrecognized.
734
+ */
735
+ normalizeAuthHeader(raw) {
736
+ if (!raw)
737
+ return 'Authorization: Bearer {key}';
738
+ const trimmed = raw.trim();
739
+ if (trimmed === 'x-api-key: {key}' || trimmed.toLowerCase() === 'x-api-key') {
740
+ return 'x-api-key: {key}';
741
+ }
742
+ return 'Authorization: Bearer {key}';
743
+ }
744
+ // ─── HTTP low-level helpers ──────────────────────────────────────
745
+ /**
746
+ * Issues a raw fetch() with an AbortController timeout. Exposed as its
747
+ * own method so MakeHTTPRequest can keep its retry loop focused.
748
+ */
749
+ async fetchRaw(auth, url, method, headers, body) {
750
+ const timeoutMs = auth.Config.RequestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
751
+ const controller = new AbortController();
752
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
753
+ try {
754
+ const init = { method, headers, signal: controller.signal };
755
+ if (body !== undefined)
756
+ init.body = JSON.stringify(body);
757
+ return await fetch(url, init);
758
+ }
759
+ catch (err) {
760
+ if (err instanceof Error && err.name === 'AbortError') {
761
+ throw new Error(`Betty API request timed out after ${timeoutMs / 1000}s: ${url}`);
762
+ }
763
+ throw err;
764
+ }
765
+ finally {
766
+ clearTimeout(timer);
767
+ }
768
+ }
769
+ /**
770
+ * Reads a response body as JSON when Content-Type is JSON-ish, otherwise
771
+ * as text. Returns null on empty bodies so callers don't need to branch.
772
+ */
773
+ async readResponseBodySafe(response) {
774
+ const contentType = response.headers.get('content-type') ?? '';
775
+ try {
776
+ if (contentType.includes('application/json') || contentType.includes('+json')) {
777
+ return await response.json();
778
+ }
779
+ const text = await response.text();
780
+ if (!text)
781
+ return null;
782
+ // Some tenants serve JSON with an incorrect content-type — try parsing anyway.
783
+ try {
784
+ return JSON.parse(text);
785
+ }
786
+ catch {
787
+ return text;
788
+ }
789
+ }
790
+ catch {
791
+ return null;
792
+ }
793
+ }
794
+ /** Converts a fetch Response + parsed body into the framework's RESTResponse shape. */
795
+ buildBettyResponse(response, body) {
796
+ const headers = {};
797
+ response.headers.forEach((value, key) => { headers[key.toLowerCase()] = value; });
798
+ return { Status: response.status, Body: body, Headers: headers };
799
+ }
800
+ /**
801
+ * Computes exponential backoff delay in ms with a `Retry-After` header
802
+ * override when provided by the server.
803
+ */
804
+ computeBackoffMs(response, attempt) {
805
+ const retryAfter = response.headers.get('retry-after');
806
+ if (retryAfter) {
807
+ const seconds = Number(retryAfter);
808
+ if (!Number.isNaN(seconds) && seconds > 0)
809
+ return Math.min(seconds * 1000, 30000);
810
+ }
811
+ return Math.min(BASE_BACKOFF_MS * Math.pow(2, attempt), 30000);
812
+ }
813
+ /**
814
+ * Applies a minimum-interval throttle between consecutive requests to
815
+ * avoid hammering a tenant whose rate-limit policy is undocumented.
816
+ */
817
+ async applyThrottle(auth) {
818
+ const minInterval = auth.Config.MinRequestIntervalMs ?? DEFAULT_MIN_REQUEST_INTERVAL_MS;
819
+ const elapsed = Date.now() - this.lastRequestTime;
820
+ if (elapsed < minInterval) {
821
+ await this.sleep(minInterval - elapsed);
822
+ }
823
+ }
824
+ /** Returns a promise that resolves after `ms` milliseconds. */
825
+ sleep(ms) {
826
+ return new Promise(resolve => setTimeout(resolve, ms));
827
+ }
828
+ /** Removes a trailing slash if present. */
829
+ stripTrailingSlash(url) {
830
+ return url.endsWith('/') ? url.slice(0, -1) : url;
831
+ }
832
+ // ─── Tenant reachability probe ───────────────────────────────────
833
+ /**
834
+ * Probes a small set of candidate "reachability" paths. Returns on the
835
+ * first path that appears to authenticate (even 401 is informative —
836
+ * the tenant is reachable and treating the key as invalid rather than
837
+ * silently ignoring it).
838
+ */
839
+ async probeTenantReachable(auth, baseURL) {
840
+ const probePaths = ['/health', '/healthz', '/api/health', '/', '/openapi.json'];
841
+ const headers = this.BuildHeaders(auth);
842
+ for (const path of probePaths) {
843
+ const url = `${baseURL}${path}`;
844
+ try {
845
+ const response = await this.MakeHTTPRequest(auth, url, 'GET', headers);
846
+ const outcome = this.interpretProbeResponse(response, url);
847
+ if (outcome)
848
+ return outcome;
849
+ }
850
+ catch {
851
+ // Try next path.
852
+ }
853
+ }
854
+ return {
855
+ success: false,
856
+ message: `Betty tenant did not respond to any of ${probePaths.join(', ')}. ` +
857
+ 'Confirm the TenantEndpoint URL and that the tenant is provisioned and reachable.',
858
+ };
859
+ }
860
+ /**
861
+ * Turns a probe response into a human-readable success / failure. Returns
862
+ * null to mean "inconclusive — try the next path."
863
+ */
864
+ interpretProbeResponse(response, url) {
865
+ if (response.Status >= 200 && response.Status < 300) {
866
+ return {
867
+ success: true,
868
+ message: `Connected to Betty tenant at ${url}. Ready to sync conversational data.`,
869
+ serverVersion: this.extractServerVersion(response.Body),
870
+ };
871
+ }
872
+ if (response.Status === 401) {
873
+ return {
874
+ success: false,
875
+ message: `Tenant reachable at ${url} but API key rejected (401). Confirm the ApiKey and AuthHeader style with the customer's Betty admin.`,
876
+ };
877
+ }
878
+ if (response.Status === 403) {
879
+ return {
880
+ success: true,
881
+ message: `Tenant reachable at ${url}; API key accepted but this endpoint is restricted (403). Connection is usable for authorized endpoints.`,
882
+ };
883
+ }
884
+ return null; // Inconclusive — try next path.
885
+ }
886
+ /** Best-effort extraction of a server/version string from a probe body. */
887
+ extractServerVersion(body) {
888
+ if (!body || typeof body !== 'object')
889
+ return undefined;
890
+ const obj = body;
891
+ const version = obj['version'] ?? obj['apiVersion'] ?? obj['api_version'];
892
+ if (typeof version === 'string')
893
+ return `Betty AI ${version}`;
894
+ const info = obj['info'];
895
+ if (info && typeof info === 'object') {
896
+ const inner = info;
897
+ if (typeof inner['version'] === 'string')
898
+ return `Betty AI ${inner['version']}`;
899
+ }
900
+ return undefined;
901
+ }
902
+ };
903
+ BettyConnector = __decorate([
904
+ RegisterClass(BaseIntegrationConnector, 'BettyConnector')
905
+ ], BettyConnector);
906
+ export { BettyConnector };
907
+ /** Tree-shaking prevention function — import and call from module entry point. */
908
+ export function LoadBettyConnector() { }
909
+ //# sourceMappingURL=BettyConnector.js.map