@memberjunction/connector-elevate 0.2.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.
@@ -0,0 +1,458 @@
1
+ import { type UserInfo } from '@memberjunction/core';
2
+ import type { MJCompanyIntegrationEntity, MJIntegrationObjectEntity, MJIntegrationObjectFieldEntity } from '@memberjunction/core-entities';
3
+ import { BaseRESTIntegrationConnector, type RESTAuthContext, type RESTResponse, type PaginationState, type PaginationType, type RateLimitPolicy, type ConnectionTestResult, type ExternalObjectSchema, type ExternalFieldSchema, type FetchContext, type FetchBatchResult, type SourceSchemaInfo, type SyncErrorCode, type ErrorSeverity, type UpdateRecordContext, type DeleteRecordContext, type CRUDResult } from '@memberjunction/integration-engine';
4
+ /** Resolved per-connection auth + routing context. */
5
+ interface ElevateAuthContext extends RESTAuthContext {
6
+ /** Per-connection base URL derived from the connection's `siteUrl` — never a vendor default. */
7
+ SiteUrl: string;
8
+ /** The api_key injected into every request body. NEVER logged, NEVER put in an error message. */
9
+ ApiKey: string;
10
+ /** The IntegrationID this context was resolved for (metadata lookups off the request path). */
11
+ IntegrationID: string;
12
+ }
13
+ /** A classified Elevate failure, derived from the observed error ENVELOPE rather than the status alone. */
14
+ export interface ElevateErrorClassification {
15
+ /** Whether this response is a FAILURE at all. A 2xx carrying an error envelope is one. */
16
+ IsError: boolean;
17
+ /** Engine-level sync error code the run artifact reports. */
18
+ Code: SyncErrorCode;
19
+ /** Engine-level severity. */
20
+ Severity: ErrorSeverity;
21
+ /** Whether the engine may retry. A 500 from the read door is a CLIENT error → never retryable. */
22
+ Retryable: boolean;
23
+ /** Short machine-ish reason naming WHY this classification was chosen. */
24
+ Reason: string;
25
+ /** The unrecognised field name when the door rejected the `fields` allow-list, else null. */
26
+ UnknownField: string | null;
27
+ }
28
+ /**
29
+ * Typed transport failure. Carries the response headers so `ExtractRetryAfterMs` can honour
30
+ * `Retry-After` off the error the engine sees, and the classification so callers do not re-parse.
31
+ */
32
+ export declare class ElevateAPIError extends Error {
33
+ readonly Status: number;
34
+ readonly Headers: Record<string, string>;
35
+ readonly Classification: ElevateErrorClassification;
36
+ constructor(message: string, Status: number, Headers: Record<string, string>, Classification: ElevateErrorClassification);
37
+ }
38
+ /**
39
+ * Elevate LMS (Cadmium) connector.
40
+ *
41
+ * Reads ride the Report API's single POST door with a JSON envelope; writes ride the base class's
42
+ * generic per-operation CRUD slots against the Registration API. Auth is a per-client `api_key`
43
+ * carried in the request BODY.
44
+ */
45
+ export declare class ElevateConnector extends BaseRESTIntegrationConnector {
46
+ /** Resolved auth per CompanyIntegration.ID. The api_key is static and has no refresh endpoint. */
47
+ private authCache;
48
+ /** Field names the door's own `response.labels` dictionary has revealed, per connection+object. */
49
+ private discoveredFieldNames;
50
+ /**
51
+ * Learned field names this connection's door has PROVEN it accepts on a read, per connection+object.
52
+ * Only these are ever allowed to join a DATA read's `fields` allow-list — see {@link VerifyLearnedFields}.
53
+ */
54
+ private verifiedFieldNames;
55
+ /** One sampled value per discovered field name, used ONLY for runtime type inference. */
56
+ private discoveredSamples;
57
+ /** Field names this connection's door REJECTED — never requested again for that object. */
58
+ private rejectedFieldNames;
59
+ /** Declared resources this connection accepted a probe query for. Absence NEVER deactivates. */
60
+ private validatedResources;
61
+ /**
62
+ * `product_url` returned alongside a created registration, keyed by the new registration_id — the
63
+ * learner's link, which the generic `CRUDResult` has no slot for. Bounded (oldest evicted) so a long
64
+ * push cannot grow it without limit.
65
+ */
66
+ readonly LastCreatedProductURLs: Map<string, string>;
67
+ /** Warnings already emitted, so the log stays honest rather than noisy. */
68
+ private warnedOnce;
69
+ /** Verbatim `MJ: Integrations.Name`. Load-bearing: T1 compares this === the metadata Name. */
70
+ get IntegrationName(): string;
71
+ /** POST /api/registrations exists (productRegistration only) — `CreateAPIPath`/`CreateMethod` are populated. */
72
+ get SupportsCreate(): boolean;
73
+ /**
74
+ * FALSE. `Configuration.WriteCapability.update` (metadata): "No update endpoint is documented for
75
+ * any resource anywhere in the corpus." No IO carries `UpdateAPIPath`. `UpdateRecord` below fails
76
+ * loudly rather than no-opping or degrading into a create.
77
+ */
78
+ get SupportsUpdate(): boolean;
79
+ /**
80
+ * TRUE only in the sense the metadata declares: `POST /registrations/cancel` is a domain-specific
81
+ * CANCEL on one productRegistration, not a generic hard DELETE, and it is NOT reflected on the
82
+ * read side (`Configuration.DeleteSemantics` = 'none'). Deletion RECONCILIATION therefore needs a
83
+ * periodic key sweep — this connector never claims a delete FEED.
84
+ */
85
+ get SupportsDelete(): boolean;
86
+ /**
87
+ * FALSE, permanently. `Configuration.DiscoveryAuthoritativeness` records `no-describe-endpoint` at
88
+ * BOTH object and field level with `deactivationPermitted: false`. With no describe endpoint,
89
+ * absence at runtime proves nothing: a thin query result must never deactivate a persisted object
90
+ * or field, which would be tenant-visible data loss.
91
+ */
92
+ get DiscoveryIsAuthoritative(): boolean;
93
+ /**
94
+ * Rate limiting provably EXISTS (Elevate Release 2025.02, verbatim: "Introduced rate limiting for
95
+ * Report API requests") but is UNQUANTIFIED — no ceiling, window, burst or `Retry-After` format is
96
+ * documented anywhere. The only empirical evidence is the RealityProbe's own pacing result recorded
97
+ * in `Configuration.PaginationDefaultsNote`: a pass at ~1.1s between requests was rate-limited
98
+ * (429s on 4 windows); the re-paced pass at ~3.4s completed 15 windows all HTTP 200. That is an
99
+ * OBSERVATION, not a vendor commitment, so the sustained rate published here is deliberately below
100
+ * the slower of the two (≈0.29/s) and the engine's AIMD limiter does the pacing — this connector
101
+ * never sleeps on its own. Burst 1: the door serves one query per request, there is nothing to batch.
102
+ */
103
+ get RateLimitPolicy(): RateLimitPolicy;
104
+ /** One in flight. The limit exists, its numbers do not, and the door is a reporting engine. */
105
+ get MaxConcurrencyHint(): number;
106
+ /**
107
+ * Honours `Retry-After` (delta-seconds AND the HTTP-date form) off the typed error the transport
108
+ * threw, so the engine's AIMD bucket backs off by the vendor's own instruction. Returns undefined
109
+ * when the response carried no header — the metadata records `retryAfterHeaderDocumented: false`,
110
+ * so the header's absence is expected and must not be papered over with an invented number.
111
+ */
112
+ ExtractRetryAfterMs(error: unknown): number | undefined;
113
+ /**
114
+ * Strictly whatever the metadata declares. Only `Product` still carries one (`id`, probe-confirmed
115
+ * populated 1985/1985); `User.member_id` and `AccountingCode.id` were WITHDRAWN by the RealityProbe
116
+ * because they are not populated on every row, and a null-bearing column is not a resume cursor.
117
+ * Never synthesised here.
118
+ */
119
+ StableOrderingKey(objectName: string): string | null;
120
+ /**
121
+ * The declared catalog is the FLOOR, not the ceiling and not a code constant: it is seeded into the
122
+ * engine cache from `metadata/integrations/elevate/.elevate.integration.json` and read back here via
123
+ * the base implementation. On top of that floor this method VALIDATES each declared resource against
124
+ * THIS connection with a minimal probe query — accepted ⇒ present.
125
+ *
126
+ * A probe that fails NEVER removes an object. `DiscoveryIsAuthoritative` is false and the metadata
127
+ * records `deactivationPermitted: false`: with no describe endpoint, a rejection can mean a
128
+ * per-tenant permission, a transient fault, or a genuinely absent resource, and those are not
129
+ * distinguishable. The rejection is surfaced as a loud, once-per-connection warning instead.
130
+ */
131
+ DiscoverObjects(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<ExternalObjectSchema[]>;
132
+ /**
133
+ * Two-stage, additive. Stage 1 is the declared floor from the engine cache (the base implementation).
134
+ * Stage 2 probes THIS connection: every Report API response carries a `response.labels` dictionary
135
+ * keying the resource's FULL retrievable field set to its display label — returned irrespective of
136
+ * which columns the request asked for — so a site's configured custom/profile fields are reachable
137
+ * per tenant without any describe endpoint. Discovered-only fields are appended, never substituted,
138
+ * and a declared field is never dropped because a probe did not see it.
139
+ */
140
+ DiscoverFields(companyIntegration: MJCompanyIntegrationEntity, objectName: string, contextUser: UserInfo): Promise<ExternalFieldSchema[]>;
141
+ /**
142
+ * Declared ∪ runtime-discovered, so a tenant's own columns reach the schema builder. Delegates the
143
+ * union to the shared `mergeDeclaredWithSampledFields` helper (never-shrink by field name); the
144
+ * connector supplies no merge logic of its own.
145
+ */
146
+ IntrospectSchema(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<SourceSchemaInfo>;
147
+ /**
148
+ * Runs the cheapest real read the door supports: a minimal single-column query against the first
149
+ * declared resource. A 200 with the documented envelope proves the site URL, the api_key and the
150
+ * door are all good together. The message never carries credential bytes.
151
+ */
152
+ TestConnection(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<ConnectionTestResult>;
153
+ /**
154
+ * OVERRIDDEN because Elevate's reads are a POST-with-a-JSON-envelope to a SINGLE endpoint with the
155
+ * object chosen by a body field — the base class's generic per-operation GET-list CRUD would 404/405
156
+ * on every object. Everything that varies per object (door path, `resource` wire value, response
157
+ * keys, watermark field, window field) is routed FROM METADATA; nothing is a string guess in code.
158
+ */
159
+ FetchChanges(ctx: FetchContext): Promise<FetchBatchResult>;
160
+ /**
161
+ * Resolves the per-connection credential. Elevate's api_key is a single static per-client string
162
+ * with no authorize/token endpoint, no scopes and no documented expiry, so there is nothing to
163
+ * refresh and the resolved context is cached per CompanyIntegration. The credential is read through
164
+ * the standard `MJ: Credentials` record when the connection carries one, with the connection's own
165
+ * `Configuration` JSON as the fallback — no inline crypto anywhere.
166
+ */
167
+ protected Authenticate(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<ElevateAuthContext>;
168
+ /**
169
+ * Transport headers ONLY. Elevate's credential does NOT travel in a header or a query string on any
170
+ * of its three POST operations — `Configuration.AuthHeaderPattern` is deliberately null and
171
+ * `AuthCredentialParamLocation` is 'body'. Reaching for the generic Bearer/Basic header path for
172
+ * this vendor is wrong, and no credential byte is ever placed here.
173
+ */
174
+ protected BuildHeaders(_auth: RESTAuthContext): Record<string, string>;
175
+ /**
176
+ * The single wire choke point. It owns four vendor-specific concerns:
177
+ * 1. Injecting `api_key` into the request BODY (never a header, never a query param, never logged).
178
+ * 2. Treating a VENDOR ERROR ENVELOPE as a failure even on a 2xx — the read door has been observed
179
+ * answering `{ error: { message } }` and the write endpoints document `{ error_messages: {...} }`
180
+ * with no status code shown, so a body-blind success check would sync zero rows silently.
181
+ * 3. Honouring 429 (and 503 carrying `Retry-After`) with bounded adaptive backoff.
182
+ * 4. NEVER retrying a 500 — this door returns 500 for CLIENT errors (wrong resource name,
183
+ * non-existent field), and blind-retrying burns the unquantified rate-limit budget replaying a
184
+ * request that can never succeed.
185
+ */
186
+ protected MakeHTTPRequest(auth: RESTAuthContext, url: string, method: string, headers: Record<string, string>, body?: unknown): Promise<RESTResponse>;
187
+ /**
188
+ * Strips the Report API envelope. The shape was OBSERVED by the RealityProbe (key names only):
189
+ * `{ response: { labels: {...}, items: [...], count: N } }`, so the rows are the array at the
190
+ * metadata-declared `ResponseDataKey` = `response.items`. A dotted key is walked, never split on the
191
+ * first segment only — declaring `response.items` and reading `response` would return zero rows.
192
+ */
193
+ protected NormalizeResponse(rawBody: unknown, responseDataKey: string | null): Record<string, unknown>[];
194
+ /**
195
+ * ALWAYS `HasMore: false`. Pagination was PROBED AND DECIDED NEGATIVE at volume: 29,003 unfiltered
196
+ * rows equalled the sum of 15 all-HTTP-200 yearly windows, and none of limit/offset/page/per_page/
197
+ * page_size changed the row count. There is no scheme to extract, and inventing one would silently
198
+ * truncate. Bulk beyond the probed ceiling is bounded by DATE WINDOWS in `FetchChanges`, not pages.
199
+ */
200
+ protected ExtractPaginationInfo(_rawBody: unknown, _paginationType: PaginationType, _currentPage: number, _currentOffset: number, _pageSize: number): PaginationState;
201
+ /** The connection's OWN Elevate site root. There is no vendor host and no fallback default. */
202
+ protected GetBaseURL(_companyIntegration: MJCompanyIntegrationEntity, auth: RESTAuthContext): string;
203
+ /**
204
+ * Projects each declared dot-path column onto its flat MJ column name (`product.title` lands at
205
+ * `raw.product.title`, the IOF is named `product_title`) while PRESERVING the complete source row:
206
+ * the spread keeps every key the door returned so the framework's custom-column capture can still
207
+ * see a per-tenant column this build never declared.
208
+ */
209
+ protected TransformRecord(raw: Record<string, unknown>, _obj: MJIntegrationObjectEntity, fields: MJIntegrationObjectFieldEntity[]): Record<string, unknown>;
210
+ /**
211
+ * Reads the created record's id from a DOTTED body location. The base helper understands only
212
+ * `body` / `header`; Elevate declares `body.registration_id`, and `registration_id` is the ONLY
213
+ * handle the Cancellation API accepts, so losing it here would make every cancel impossible.
214
+ * `product_url` from the same body is stashed on {@link LastCreatedProductURLs}.
215
+ */
216
+ protected ExtractIDFromResponse(response: RESTResponse, idLocation: string | null): string | undefined;
217
+ /** Reads the vendor's message out of either observed envelope: `{error:{message}}` / `{error_messages:{}}`. */
218
+ protected ExtractErrorMessage(response: RESTResponse): string | undefined;
219
+ /**
220
+ * ALWAYS fails, explicitly. No update endpoint is documented for ANY Elevate resource, so there is
221
+ * nothing to call: no IO carries `UpdateAPIPath`. This method exists so the failure is a classified,
222
+ * visible NOT-SUPPORTED rather than a silent no-op — and it must never degrade into a create, which
223
+ * would mint a second registration for the same learner.
224
+ */
225
+ UpdateRecord(ctx: UpdateRecordContext): Promise<CRUDResult>;
226
+ /**
227
+ * OVERRIDDEN for one reason: the id location. Elevate's cancel is a POST whose `registration_id`
228
+ * travels in the BODY (`DeleteIDLocation` = `body.registration_id`), and the base class's generic
229
+ * delete sends NO body at all for a non-path id location — the call would arrive without the id and
230
+ * cancel nothing. Everything else is still read from metadata: the verb comes from `DeleteMethod`
231
+ * (POST, never assumed DELETE) and the path is used EXACTLY as declared — `/registrations/cancel`
232
+ * genuinely has no `/api/` prefix, and "fixing" it would 404.
233
+ */
234
+ DeleteRecord(ctx: DeleteRecordContext): Promise<CRUDResult>;
235
+ /**
236
+ * Classifies from the observed error ENVELOPE, not the status alone. Two shapes exist and they are
237
+ * NOT shared between surfaces (`Configuration.ErrorResponseShape.surfaceSplit`): the read door
238
+ * answers `{ error: { message } }` (observed at HTTP 500) and the write endpoints document
239
+ * `{ error_messages: { field: message } }` with NO status code ever shown. A 2xx carrying either is
240
+ * a FAILURE — treating it as a successful empty read is exactly how a sync reports zero rows and
241
+ * green at the same time.
242
+ */
243
+ ClassifyElevateResponse(status: number, body: unknown): ElevateErrorClassification;
244
+ /** The raw HTTP call. Isolated so a mocked subclass can capture the wire without losing the behaviour above. */
245
+ protected rawRequest(url: string, method: string, headers: Record<string, string>, body?: unknown): Promise<RESTResponse>;
246
+ /** All ACTIVE IntegrationObjects for this integration; `[]` when the engine cache is unavailable. */
247
+ protected getCachedObjects(integrationID: string): MJIntegrationObjectEntity[];
248
+ /** The IntegrationID for this integration, or null when the engine cache is unavailable. */
249
+ protected tryGetIntegrationID(): string | null;
250
+ /**
251
+ * Runs ONE report query and returns its rows. Two safety behaviours ride here because both are
252
+ * per-query facts, not per-object ones:
253
+ * • THE COMPLETENESS TRIPWIRE — `response.count` is the door's own total for the query; when it
254
+ * exceeds `response.items.length` the read was silently truncated, which is the only protection
255
+ * that survives above the 29,003 rows the probe actually measured.
256
+ * • THE ALLOW-LIST REPAIR — a SAFETY NET for DECLARED columns (a wrong `wireSelector` in metadata):
257
+ * the door rejects the WHOLE query for one unrecognised column and names the offender, so that name
258
+ * is dropped, remembered and the query retried. Runtime-discovered names never rely on it — they
259
+ * are proven out of band by {@link VerifyLearnedFields} before they may enter a data read — and the
260
+ * repair refuses to act when the named column was not in the request it just sent, because dropping
261
+ * it changes nothing and the retry would replay an identical, already-failed call.
262
+ */
263
+ private RunReportQuery;
264
+ /**
265
+ * Runs one date window, halving it when the door reports truncation. That is the ADAPTIVE sizing the
266
+ * probe's evidence calls for: it does not assume a chunk size is small enough, it verifies each chunk
267
+ * against the door's own count and splits until every chunk is provably complete.
268
+ */
269
+ private RunWindow;
270
+ /**
271
+ * Builds the request envelope. Key order is deliberate and stable — `resource` sits next to
272
+ * `format` so a captured request is self-describing. `api_key` is NOT added here; it is injected at
273
+ * the transport choke point so no caller can accidentally log or persist an envelope carrying it.
274
+ */
275
+ private BuildEnvelope;
276
+ /**
277
+ * json unless the connection explicitly asks for csv AND every selector is flat. The vendor is
278
+ * explicit that CSV "is limited to only the values in the top-most layer of the API response", so a
279
+ * dot-path selection silently loses its columns in csv — the request is forced back to json and the
280
+ * downgrade is announced rather than quietly honoured.
281
+ */
282
+ private ResolveFormat;
283
+ /**
284
+ * The `fields` allow-list for one DATA read: the DECLARED read-surface wire selectors UNIONED with the
285
+ * per-tenant column names this connection's door has PROVEN it accepts (see {@link VerifyLearnedFields}),
286
+ * minus anything the door has already rejected. A build-time-only list would truncate every record to
287
+ * what this build thought to ask for; the union is what makes a site's configured custom/profile
288
+ * columns reachable.
289
+ *
290
+ * The union is over VERIFIED names, never over freshly-learned ones. Elevate's allow-list is
291
+ * ALL-OR-NOTHING — one unrecognised name fails the WHOLE query — so folding an unproven label name
292
+ * into a data read makes every row of that object hostage to a guess: the read has to fail at least
293
+ * once, and it zeroes the object outright if the door's message does not NAME the offender (the
294
+ * repair below can only act on a named column). Unproven names are therefore proven OUT OF BAND
295
+ * first; a rejection there costs one probe and never a row.
296
+ * `FetchContext.RequestedSourceFields`, when the engine supplies it, narrows the union to the columns
297
+ * actually mapped.
298
+ */
299
+ private SelectorsFor;
300
+ /**
301
+ * Proves — OUT OF BAND, before any data read — which of the names runtime discovery has learned for
302
+ * this connection+object the door will actually accept in a `fields` allow-list. This is the whole
303
+ * reason a learned label can no longer zero an object's sync:
304
+ *
305
+ * • the DATA read only ever asks for DECLARED columns ∪ names proven here, so an unrecognised
306
+ * label can never fail it — named in the door's message or not;
307
+ * • a rejection costs exactly this probe (never a row), the offending name is remembered as
308
+ * rejected for the connection and is never asked for again;
309
+ * • a probe that fails for ANY OTHER reason leaves the names UNVERIFIED rather than rejected —
310
+ * absence of proof is not proof of absence, and the next sync re-attempts them.
311
+ *
312
+ * Zero-cost when there is nothing new to prove (the overwhelmingly common case): with no unproven
313
+ * name the method makes no request at all. The probe is scoped by the SAME filter the imminent read
314
+ * uses, so proving a column on a watermarked object does not drag the whole resource across the wire.
315
+ */
316
+ private VerifyLearnedFields;
317
+ /** Learned names not yet proven, not already rejected, and not already covered by a declared column. */
318
+ private PendingLearnedFields;
319
+ /** Records the learned names this connection's door answered a read for. */
320
+ private MarkVerified;
321
+ /**
322
+ * Records the per-resource field dictionary the door returns on EVERY call (`response.labels`) as
323
+ * runtime-discovered column names for this connection, plus one sampled value each for type
324
+ * inference. This is discovery from a real runtime surface, not a build-time sample: nothing here is
325
+ * written back to the declared metadata.
326
+ */
327
+ private LearnLabels;
328
+ /** Remembers a column this connection's door refused, so it is never requested for that object again. */
329
+ private RememberRejected;
330
+ /**
331
+ * The silent-truncation tripwire. `response.count` is the door's OWN total for the query; when it
332
+ * disagrees with the number of rows actually returned, the read was capped. Raised as a FetchWarning
333
+ * so the engine surfaces it in the structured run artifact instead of it being a swallowed console line.
334
+ */
335
+ private CheckCompleteness;
336
+ /**
337
+ * The date column a bulk pull is chunked on — strictly the object's DECLARED
338
+ * `IncrementalWatermarkField`, and nothing else. For productRegistration that is `modified_at`: the
339
+ * probe-proven UPDATE watermark, deliberately not `transaction_at`, which is insert time and cannot
340
+ * see an edit. `null` for every other object, and then NO window is ever synthesised.
341
+ *
342
+ * A date-shaped column is NOT enough to justify a filter here. `EarnedCredit.updated_at` looks like a
343
+ * watermark and the metadata explicitly WITHHOLDS incremental capability for it: filters-envelope
344
+ * reachability for that resource is unproven, so chunking on it would apply an unevidenced filter and
345
+ * would silently drop every row whose column is null. Honour what the probe proved; do not re-derive it.
346
+ */
347
+ private WindowFieldFor;
348
+ /**
349
+ * Consecutive, NON-OVERLAPPING windows over `[start, end]`. The start is the sync watermark when the
350
+ * engine supplied one (so a delta pass re-reads only what changed), the resume cursor when a prior
351
+ * batch stopped mid-plan, or the connection's declared `elevateWindowStart`. With no lower bound at
352
+ * all the plan is EMPTY and the object is read in one query — verified by the completeness tripwire
353
+ * rather than assumed complete.
354
+ */
355
+ private BuildWindowPlan;
356
+ /** Halves a window, or null when it is already a single day and cannot be narrowed further. */
357
+ private SplitWindow;
358
+ /**
359
+ * The delta filter for an UNCHUNKED incremental read. Only ever built from the object's own declared
360
+ * watermark; an object whose metadata declares none runs a FULL SCAN, and no delta path is invented
361
+ * for it.
362
+ */
363
+ private WatermarkFilter;
364
+ /** Whether the connection explicitly asked for a bounded/chunked pull rather than the default. */
365
+ private HasExplicitWindowConfig;
366
+ /** Max-SEEN watermark across the batch (never "most recent row"), or null when the object has none. */
367
+ private MaxWatermark;
368
+ /**
369
+ * Builds one ExternalRecord. Identity is STABLE ACROSS PASSES by construction:
370
+ * • when the object's DECLARED primary key is fully populated, the ExternalID is that key;
371
+ * • otherwise — every Elevate object except Product, whose keys the RealityProbe demoted or
372
+ * falsified — the identity is a content hash over the DECLARED READ PROJECTION only, never over
373
+ * the whole raw row. That distinction is the point: hashing the raw row makes identity a
374
+ * function of any volatile or per-tenant byte the door happens to add, which is the drift class
375
+ * the two-pass idempotency rung exists to catch.
376
+ * `Fields` still carries the COMPLETE source row (plus the flattened projections) so the framework's
377
+ * custom-column capture sees everything the door returned.
378
+ */
379
+ private ToElevateRecord;
380
+ /** The stable projection a keyless record's identity hashes over: declared columns only, by MJ name. */
381
+ private IdentityBasis;
382
+ /** Declared PK names in Sequence order, mirroring the base class's `['ID']` synthetic fallback. */
383
+ private PrimaryKeyNames;
384
+ /** The read route for one object, entirely from metadata. Throws rather than guessing a wire value. */
385
+ private ReadRouteFor;
386
+ /** Fallback resource resolution: the depth-0 access path's own body selector. Still metadata, not code. */
387
+ private AccessPathResource;
388
+ /**
389
+ * The object's READ-surface columns. A field is excluded when the metadata marks it write-only or
390
+ * explicitly excludes it from the read selector — `registration_id` is exactly that case: the probe
391
+ * FALSIFIED it as a read column (`Field registration_id doesn't exist`) while it remains the only
392
+ * handle the cancel API accepts. Sending it would fail the WHOLE query for the object.
393
+ */
394
+ private ReadColumnsFor;
395
+ /** Parsed `Configuration` JSON for one IntegrationObject. */
396
+ private ObjectConfig;
397
+ /**
398
+ * Probes one declared resource against THIS connection with the cheapest possible query. Accepted ⇒
399
+ * present. A rejection is remembered and warned about but NEVER removes the object — with no describe
400
+ * endpoint, absence proves nothing, and deactivating on a thin result is tenant-visible data loss.
401
+ */
402
+ private ValidateResource;
403
+ /** Runs one read so the door's `response.labels` dictionary can be harvested for this connection. */
404
+ private LearnFieldsFromSource;
405
+ /** An `ExternalFieldSchema` for a column only runtime discovery has seen. Types inferred, never asserted. */
406
+ private SchemaForDiscoveredField;
407
+ /** Conservative runtime type inference from one observed value. Unknown ⇒ String, never a guessed width. */
408
+ private InferType;
409
+ /** Credential record first, connection Configuration second. No inline crypto; nothing is logged. */
410
+ private LoadCredentials;
411
+ /** Extracts the two Elevate credential fields from a credential/Configuration JSON string. */
412
+ private ParseCredentialJSON;
413
+ /** Merges the credential into the request body. The ONLY place the api_key ever touches the wire. */
414
+ private WithCredential;
415
+ /** Removes any occurrence of the credential from a string before it reaches a log or an error. */
416
+ private Redact;
417
+ /** The vendor's message from either observed envelope, or undefined when the body carries no error. */
418
+ private VendorMessage;
419
+ /** Pulls the offending column out of the door's own `Field <name> doesn't exist` message. */
420
+ private UnknownFieldFrom;
421
+ /** Walks a dotted path into a parsed body. Returns undefined at the first missing/non-object segment. */
422
+ private ReadPath;
423
+ /** Joins a base URL with an API path exactly as declared — no path is invented or normalised away. */
424
+ private JoinURL;
425
+ /** Path-only view of a URL, for messages that must never carry a query string or a credential. */
426
+ private PathOf;
427
+ /** Cache key scoping a per-tenant discovery to one connection + object. */
428
+ private CacheKey;
429
+ /** Deterministic ordering so discovery output is byte-stable across passes. */
430
+ private SortedNames;
431
+ /** A narrowing cast to a plain object, or null. */
432
+ private AsObject;
433
+ /** First non-empty string value among `keys` on a parsed object. */
434
+ private FirstString;
435
+ /** A trimmed string from the connection Configuration JSON. */
436
+ private ConfigString;
437
+ /** A finite number from the connection Configuration JSON. */
438
+ private ConfigNumber;
439
+ /** Tolerant JSON-object parse; malformed configuration degrades to "absent" rather than crashing a sync. */
440
+ private ParseJSONObject;
441
+ /** `YYYY-MM-DD` for today, in UTC — the granularity the probe partitioned with. */
442
+ private Today;
443
+ /** Normalises a watermark/config value to `YYYY-MM-DD`, or null when it is not a usable date. */
444
+ private ToDayString;
445
+ /** `YYYY-MM-DD` + n days, UTC. */
446
+ private AddDays;
447
+ /** Whole days between two `YYYY-MM-DD` values. */
448
+ private DayDiff;
449
+ /** The earlier of two `YYYY-MM-DD` values (lexicographic order is chronological for this format). */
450
+ private MinDay;
451
+ /** Sleeps, bounded. Only ever reached on a 429/503 with a honoured `Retry-After`. */
452
+ private Sleep;
453
+ /** An error message safe to log: never carries credential bytes. */
454
+ private SafeMessage;
455
+ /** Emits a warning at most once per connector lifetime, so the log stays honest rather than noisy. */
456
+ private WarnOnce;
457
+ }
458
+ export {};