@opencxh/domain 1.162.0 → 1.165.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,91 @@
1
+ /**
2
+ * Laag 1 van de integratielagen: **Account** — het credential.
3
+ *
4
+ * Vier lagen, en dit is de onderste:
5
+ * | Laag | Vraag | Woord |
6
+ * |---|---|---|
7
+ * | 1 Credential | mag ik erin? | **Account** (`ManagedAccount`) |
8
+ * | 2 Connector | waar is het, welk protocol? | **Connector** (`McpServerEntity`) |
9
+ * | 3 Capability | wat kan ik ermee? | **Source** (`sync-source`, `memory-source`, …) |
10
+ * | 4 Usage | staat het aan, met welke instellingen? | **Connection** (`SyncConnection`, `Channel`) |
11
+ *
12
+ * `ManagedAccount` is een **platform**-primitive, niet een comms-entiteit — dat het type
13
+ * historisch in `entities/communication/types.ts` stond kwam alleen doordat de *store* daar
14
+ * woont. De store blijft in comms (daar zitten de scope-guard en de OAuth-orchestrator);
15
+ * het type hoort hier, want ai, mail, ftp, s3, google en microsoft hangen er allemaal aan.
16
+ *
17
+ * ## Niet verwarren met een Registration
18
+ *
19
+ * `google_account`, `microsoft_account`, `shopify_account`, `slack_account` en `meta_app` zijn
20
+ * GEEN accounts in deze zin. Dat zijn **registraties**: de clientId/clientSecret waarmee de
21
+ * organisatie zichzelf bij de vendor aanmeldt. Eén registratie draagt N accounts. Ze delen
22
+ * alleen het woord.
23
+ *
24
+ * ## Scope: `userId` is de enige eigendom-as
25
+ *
26
+ * `userId` afwezig/`null` = org-breed, gedeeld. Gezet = persoonlijk, van die ene gebruiker.
27
+ * Er is bewust **geen** `ownerScope` en **geen** `createdBy` op een credential:
28
+ * - `ownerScope` (`personal`/`team`/`org`) is de as van een *resource* — met wie deel ik dit
29
+ * contact, werkitem of geheugen. Een token heeft geen teamvariant.
30
+ * - "wat eist de vendor" (één gedeeld token vs. iedereen zijn eigen) is een eigenschap van de
31
+ * **connector**, niet van de accountrij: zie `credentialScope` in `entities/mcp/types.ts`.
32
+ */
33
+ export type AuthState = {
34
+ type: "oauth2";
35
+ accessToken: string;
36
+ refreshToken?: string;
37
+ /** Epoch ms */
38
+ expiresAt: number;
39
+ scopes: string[];
40
+ idToken?: string;
41
+ } | {
42
+ type: "password";
43
+ username: string;
44
+ /** Rauw wachtwoord (geen secret-store in repo). Alleen via internal-path. */
45
+ password: string;
46
+ /** Toekomstige secret-store-referentie i.p.v. raw `password`. */
47
+ passwordRef?: string;
48
+ } | {
49
+ type: "certificate";
50
+ certRef: string;
51
+ } | {
52
+ type: "apikey";
53
+ /** Rauwe API-key (geen secret-store in repo). Alleen via internal-path. */
54
+ apiKey: string;
55
+ } | {
56
+ type: "static";
57
+ };
58
+ export type ManagedAccountStatus = "connected" | "disconnected" | "error" | "expired";
59
+ export interface ManagedAccount {
60
+ id: string;
61
+ organizationId: string;
62
+ /**
63
+ * Afwezig/`null` = org-breed gedeeld account. Gezet = persoonlijk account van die gebruiker.
64
+ * De enige eigendom-as op dit niveau — zie de moduledoc.
65
+ */
66
+ userId?: string;
67
+ providerId: string;
68
+ /** "oauth2" | "imap" | "sip" | "webhook" | "apikey" | provider-eigen string */
69
+ protocol: string;
70
+ displayName: string;
71
+ status: ManagedAccountStatus;
72
+ auth: AuthState;
73
+ /** Provider-specifieke metadata (tenantId, externalUserId, scope-flags). */
74
+ metadata: Record<string, unknown>;
75
+ createdAt?: number;
76
+ updatedAt?: number;
77
+ }
78
+ /**
79
+ * Public-view zonder tokens — wat `account/list` en `account/get` aan UI/comm-server
80
+ * retourneren. `auth` is gestript tot alleen het type.
81
+ *
82
+ * **Leesasymmetrie, en dit is de valkuil:** de publieke `account/list` filtert op de
83
+ * *huidige* gebruiker, dus een job of cron (die geen sessie heeft) ziet daar niets — ook
84
+ * geen org-brede rijen. Server-side code die accounts moet zien gebruikt altijd de
85
+ * `account-internal/`-route (`listManagedAccountsInternal` / `getManagedAccountInternal`).
86
+ */
87
+ export type ManagedAccountPublic = Omit<ManagedAccount, "auth"> & {
88
+ authType: AuthState["type"];
89
+ scopes?: string[];
90
+ expiresAt?: number;
91
+ };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Aanwijsbare resources, over app-grenzen heen.
3
+ *
4
+ * Eén vraag die in dit platform steeds terugkwam en tot nu toe per geval werd opgelost: *welke
5
+ * dingen kan ik hier aanwijzen, en hoe heten ze?* Een werkitem koppelen aan een gesprek, een
6
+ * `resource_ref`-veld invullen, straks een bestand aanhaken — drie keer dezelfde vraag aan drie
7
+ * verschillende apps.
8
+ *
9
+ * Het antwoord is een **contract op de servicebus**, niet een HTTP-providerrol: beide vragen
10
+ * worden op de client gesteld door een gebruiker die de bron-app toch al open heeft, en die app
11
+ * heeft zijn gesprekken en bedrijven al in een store staan. Een providerrol zou daar een
12
+ * round-trip van maken voor gegevens die in het geheugen liggen.
13
+ *
14
+ * Drie sleutels, alle drie via `sdk.services.execute` (die de antwoorden van elke geïnstalleerde
15
+ * app als array teruggeeft):
16
+ *
17
+ * | Sleutel | Params | Antwoord |
18
+ * |---|---|---|
19
+ * | `resources.describe` | — | {@link ResourceSourceDescribe} |
20
+ * | `resources.search` | {@link ResourceSearchParams} | {@link ResourceSummary}`[]` |
21
+ * | `resources.resolve` | {@link ResourceResolveParams} | {@link ResourceSummary}`[]` |
22
+ *
23
+ * `describe` is niet cosmetisch: een kiezer tekent zijn soort-filters uit wat er daadwerkelijk
24
+ * antwoordt. Zonder die call zou hij een vaste lijst tonen met tabbladen die leeg blijven zodra
25
+ * een app niet geïnstalleerd is — precies de per-provider aanname die dit contract vermijdt.
26
+ */
27
+ /** De servicebus-sleutels, als constante zodat een typefout niet stil een lege lijst oplevert. */
28
+ export declare const RESOURCE_DESCRIBE_SERVICE = "resources.describe";
29
+ export declare const RESOURCE_SEARCH_SERVICE = "resources.search";
30
+ export declare const RESOURCE_RESOLVE_SERVICE = "resources.resolve";
31
+ /** Eén aanwijsbare resource, zoals de app die hem bezit hem toont. */
32
+ export interface ResourceSummary {
33
+ /**
34
+ * `<kind>:<ref>` — dezelfde vorm die `ScopeAuth` autoriseert en `WorkLink.toKey` draagt.
35
+ *
36
+ * Dit is de identiteit: twee aanbieders die dezelfde resource kennen (comms federeert
37
+ * adresboeken, crm heeft eigen contacten) leveren dezelfde sleutel, en een consument
38
+ * ontdubbelt daarop.
39
+ */
40
+ scopeKey: string;
41
+ /** Het voorvoegsel van {@link ResourceSummary.scopeKey}, apart zodat filteren geen split vraagt. */
42
+ kind: string;
43
+ title: string;
44
+ /** Tweede regel: afzender, e-mailadres, bedrijfsnaam, statuslabel. */
45
+ subtitle?: string;
46
+ /** Lucide-icoonnaam, zoals `TimeTarget.icon`. Afwezig = de consument kiest er zelf één bij de soort. */
47
+ icon?: string;
48
+ /**
49
+ * Pad binnen de shell om deze resource te openen.
50
+ *
51
+ * Afwezig = de regel is niet aanklikbaar. Dat is bewust een aparte staat en geen gok: een
52
+ * verzonnen pad levert een doodlopende navigatie op, en dat is erger dan geen link.
53
+ */
54
+ href?: string;
55
+ /** Sorteerhint; recenter staat hoger. Afwezig telt als oudst. */
56
+ updatedAt?: number;
57
+ /**
58
+ * Andere scopeKeys die bij deze resource horen — het contact en het bedrijf achter een
59
+ * gesprek, de klant achter een zaak.
60
+ *
61
+ * **De bron zegt het, de consument raadt het niet.** Comms weet dat een interactie
62
+ * `partyKeys` heeft; `apps/work` weet dat niet en hoort dat ook niet te weten. Zonder dit
63
+ * veld zou "neem het contact mee" in de koppel-flow een `if (kind === "interaction")` met
64
+ * een comms-specifieke veldnaam worden, en dat breekt bij de volgende aanbieder.
65
+ *
66
+ * Alleen één hop, en alleen wat de bron zelf al in handen heeft: dit is een hint voor een
67
+ * kiezer, geen graaf.
68
+ */
69
+ related?: string[];
70
+ }
71
+ /** Antwoord op `resources.describe`: welke soorten deze app kan zoeken en oplossen. */
72
+ export interface ResourceSourceDescribe {
73
+ kinds: string[];
74
+ }
75
+ /** Params voor `resources.search`. */
76
+ export interface ResourceSearchParams {
77
+ /** Vrije tekst. Leeg = "wat is er recent", niet "alles". */
78
+ query: string;
79
+ /** Beperk tot deze soorten. Leeg/afwezig = alles wat de aanbieder heeft. */
80
+ kinds?: string[];
81
+ /** Maximum per aanbieder, niet in totaal — de consument kapt zelf af na het ontdubbelen. */
82
+ limit?: number;
83
+ }
84
+ /** Params voor `resources.resolve`: van sleutel naar leesbare rij. */
85
+ export interface ResourceResolveParams {
86
+ scopeKeys: string[];
87
+ }
88
+ /**
89
+ * De soort uit een scopeKey, zonder de rest te hoeven splitsen.
90
+ *
91
+ * Losse helper en geen `splitScopeKey`-hergebruik: die zit in `platform-api` (server), en dit
92
+ * contract wordt vooral op de client gelezen.
93
+ */
94
+ export declare function resourceKindOf(scopeKey: string): string;
@@ -32,6 +32,20 @@ export interface InternalSDK<LocalServices extends ServiceMap = {}, TranslationK
32
32
  * that does not exist.
33
33
  */
34
34
  navigateAbsolute(path: string, options?: any): void;
35
+ /**
36
+ * Terug naar de vorige pagina — de echte, inclusief die van een andere app.
37
+ *
38
+ * Waarom dit hier hoort en niet per app: elk detailscherm heeft een terugknop, en zonder
39
+ * gedeelde vorm wordt dat overal een vaste route ("terug" = altijd de lijst) die liegt zodra
40
+ * je van een bord of uit een gesprek komt.
41
+ *
42
+ * `fallback` is app-relatief, net als {@link navigate}, en geldt alleen als er geen vorige
43
+ * pagina ín deze sessie is — dat is precies het geval bij een koud geopende deeplink. Geef je
44
+ * er geen, dan gebeurt er niets: uit het product lopen is geen taak van een terugknop.
45
+ */
46
+ back(fallback?: string): void;
47
+ /** Is er een vorige pagina in deze sessie? Voor een knop die zichzelf wil verbergen. */
48
+ canGoBack(): boolean;
35
49
  currentPath(): string;
36
50
  path$: Observable<string>;
37
51
  };
@@ -0,0 +1,442 @@
1
+ /**
2
+ * Data van buiten naar binnen halen, één keer geregeld.
3
+ *
4
+ * Het platform ingest vandaag op vier onafhankelijke manieren — een cron-sync per provider-app,
5
+ * webhooks, een event-driven verrijkingsjob en losse backfills — en elk van die vier herbouwt
6
+ * zelfstandig dezelfde zes dingen: cursor, batching, locking, idempotentie, foutafhandeling en
7
+ * observability. Dat is te zien: `cursorKey()` staat drie keer los in de repo,
8
+ * `microsoft_sync_state` groeit per sub-sync een kolom, en een sync die stilvalt is een
9
+ * `console.error` die niemand ziet.
10
+ *
11
+ * De splitsing die dat oplost is de industriestandaard (Singer/Airbyte, Nango, Fivetran): **een
12
+ * smal bron-contract met een dikke generieke runtime.** De bron weet hoe je één pagina uit
13
+ * Microsoft Graph of Asana haalt; de runtime weet hoe je cursors checkpoint, batches
14
+ * doorschakelt, backoff doet en er een run-log van bijhoudt. Dit bestand is dat smalle contract.
15
+ *
16
+ * Drie endpoints, in de vorm van `memory-source` en `analytics-source` — dus een providerrol en
17
+ * geen servicebus-sleutel, want dit draait server-side zonder gebruiker erbij:
18
+ *
19
+ * | Rol | Endpoint | Antwoord |
20
+ * |---|---|---|
21
+ * | `sync-source` | `GET /provider/sync/describe` | {@link SyncSourceDescribe} |
22
+ * | `sync-source` | `POST /provider/sync/pull` | {@link SyncPullResponse} |
23
+ * | `sync-target` | `POST /provider/sync/land` | {@link SyncLandResponse} |
24
+ *
25
+ * Bron en doel zijn **losse rollen**: `apps/microsoft` levert records en landt niets,
26
+ * `apps/work` landt records en levert niets. Een app mag beide zijn, maar hoeft dat niet.
27
+ */
28
+ /** De providerrol-groepen, als constante zodat een typefout niet stil een lege lijst oplevert. */
29
+ export declare const SYNC_SOURCE_PROVIDER_GROUP = "sync-source";
30
+ export declare const SYNC_TARGET_PROVIDER_GROUP = "sync-target";
31
+ /**
32
+ * Continu of eenmalig — en dat is een echt verschil, geen label.
33
+ *
34
+ * `continuous` draait op een tik en moet dus goedkoop kunnen niksdoen (een cursor die zegt
35
+ * "niets nieuws"). `once` wordt door een mens gestart, mag duur zijn, en moet **uitputtend**
36
+ * zijn: een migratie die 95% ophaalt is geen migratie. Die eis is precies waarom niet elke bron
37
+ * beide kan — zie {@link SyncSourceDefinition.transport}.
38
+ */
39
+ export type SyncMode = "continuous" | "once";
40
+ /**
41
+ * Hoe de records feitelijk binnenkomen.
42
+ *
43
+ * Dit staat in het contract omdat het de verwachtingen bepaalt die de runtime en de UI mogen
44
+ * hebben, niet als documentatie:
45
+ *
46
+ * - **`native`** — de app praat zelf met de API van de leverancier. Kent delta-tokens en
47
+ * pagination, dus geschikt voor `continuous` én `once`.
48
+ * - **`tool`** — de app gaat via de AI-tool/MCP-laag. Prima voor de lange staart aan systemen
49
+ * waarvoor een eigen connector niet loont, maar een MCP-zoektool antwoordt "top-N relevant"
50
+ * en niet "alles sinds X", en het protocol kent geen change-token. Een `tool`-bron die
51
+ * `once` claimt belooft volledigheid die hij niet kan leveren.
52
+ * - **`file`** — een geüpload bestand (CSV). Per definitie `once`, en per definitie volledig.
53
+ */
54
+ export type SyncTransport = "native" | "tool" | "file";
55
+ /** Eén ding dat een app van buiten kan halen. */
56
+ /**
57
+ * Eén instelling van een koppeling, zoals de bron hem declareert.
58
+ *
59
+ * Bewust geen ui-kit-types hier: `packages/domain` heeft geen UI-afhankelijkheden, en het
60
+ * formulier mapt deze declaratie zelf op zijn eigen velden.
61
+ */
62
+ export interface SyncSettingsField {
63
+ /** Sleutel in het settings-object dat de bron bij `pull` terugkrijgt. */
64
+ key: string;
65
+ label: string;
66
+ type: "text" | "number" | "boolean" | "select";
67
+ required?: boolean;
68
+ /** Alleen bij `type: "select"`. */
69
+ options?: {
70
+ value: string;
71
+ label: string;
72
+ }[];
73
+ /** Uitleg onder het veld. Voor "leeg = ..."-gevallen, die anders raadwerk zijn. */
74
+ help?: string;
75
+ /**
76
+ * Hint dat deze waarde een resource in een andere app aanwijst (`"work_project"`).
77
+ *
78
+ * Vandaag rendert het formulier hem als tekstveld; de hint staat er zodat een picker later
79
+ * toegevoegd kan worden zonder dat de bron verandert. Een hint en geen belofte.
80
+ */
81
+ resource?: string;
82
+ }
83
+ export interface SyncSourceDefinition {
84
+ /**
85
+ * Stabiel en app-genamespaced: `microsoft.todo`, `asana.tasks`, `csv.contacts`.
86
+ *
87
+ * Dit is een **opgeslagen verwijzing** — `SyncConnection.sourceId` bewaart hem — dus
88
+ * hernoemen breekt bestaande koppelingen. Punt als scheidingsteken, zoals `MemoryKindId`
89
+ * voor app-eigen kinds.
90
+ */
91
+ id: string;
92
+ label: string;
93
+ description?: string;
94
+ /**
95
+ * De scope-soorten die deze bron landt: `work_item`, `interaction`, `company`, `contact`.
96
+ *
97
+ * Hetzelfde vocabulaire als `ScopeDescribe.kinds`, en dat is geen cosmetische keuze — de
98
+ * runtime routeert een record naar de bezittende app met `findScopeOwner(kind)`. Een soort
99
+ * die geen scope-eigenaar heeft, kan dus niet geland worden, en dat merk je bij het
100
+ * registreren in plaats van halverwege een run.
101
+ */
102
+ kinds: string[];
103
+ /** Welke modi deze bron ondersteunt. Leeg is zinloos; minstens één. */
104
+ modes: SyncMode[];
105
+ transport: SyncTransport;
106
+ /**
107
+ * De `providerId` van de {@link ManagedAccount} die deze bron nodig heeft.
108
+ *
109
+ * Afwezig = geen credential nodig (een bestand-upload). Aanwezig betekent dat een koppeling
110
+ * niet werkt zonder gekozen account, en dat de UI dat vóór de eerste run kan zeggen in
111
+ * plaats van een mislukte run te laten zien.
112
+ */
113
+ accountProviderId?: string;
114
+ /**
115
+ * Instellingen die per koppeling verschillen en dus niet in code horen.
116
+ *
117
+ * Declaratief, zodat een connector-app **geen frontend nodig heeft** om configureerbaar te
118
+ * zijn: het Koppelingen-formulier rendert deze velden. Dat is wat `apps/asana` (nul
119
+ * frontend-bestanden) mogelijk houdt.
120
+ *
121
+ * Plat gehouden met opzet. Een genest schema zou een mapping-DSL worden, en die is eerder
122
+ * afgewezen: datumformaten en enums verschillen per bron, en fouten verschuiven dan van `tsc`
123
+ * naar runtime.
124
+ */
125
+ settingsSchema?: SyncSettingsField[];
126
+ /**
127
+ * Alleen bij `transport: "tool"`: de catalogus-sleutel waarop de connector-picker
128
+ * voorfiltert (`"asana"`, `"linear"`).
129
+ *
130
+ * Een **hint**, geen resolutie: hij bepaalt welke van de MCP-verbindingen van de
131
+ * organisatie als kandidaat worden voorgesteld. Wat de koppeling daarna bewaart is het
132
+ * connector-**id** ({@link SyncConnection.connectorId}). Zonder hint krijgt de gebruiker
133
+ * simpelweg de volledige lijst.
134
+ */
135
+ connectorHint?: string;
136
+ /**
137
+ * Alleen bij `transport: "tool"`: deze bron kan alleen draaien op een gedeeld
138
+ * organisatie-credential.
139
+ *
140
+ * Waarom dit bestaat: een onbemande ronde (cron, sync) heeft geen handelende gebruiker, dus
141
+ * een connector met `credentialScope: "per-user"` levert daar per definitie nul tools — en dat
142
+ * zag je vroeger alleen als "de sync doet niets". Met deze vlag weigert het opslaan van een
143
+ * `continuous`-koppeling meteen, met een uitleg.
144
+ *
145
+ * Alleen voor `continuous`: een `once`-koppeling start een mens, en dan is er wél een
146
+ * handelende gebruiker wiens token gebruikt kan worden.
147
+ */
148
+ requiresCredentialScope?: "shared";
149
+ /**
150
+ * Aanbevolen minimale tijd tussen twee rondes, in ms. De organisatie mag hem overschrijven.
151
+ *
152
+ * Een **interval** en geen cron-expressie, omdat dat is wat er daadwerkelijk uitvoerbaar is:
153
+ * `JobOptions.schedule` staat vast bij registratie, dus een cron-string per koppeling zou
154
+ * nooit geëvalueerd worden — een veld dat belooft wat het niet doet. De runtime tikt op een
155
+ * grof raster en poort daarop.
156
+ *
157
+ * Bij de bron en niet in de runtime, omdat alleen de bron weet wat zijn API verdraagt: een
158
+ * delta-feed mag elke minuut, een lijst-endpoint dat elke keer alles teruggeeft niet.
159
+ */
160
+ defaultIntervalMs?: number;
161
+ /**
162
+ * Hoe vaak de bron zijn cursor moet negeren en alles opnieuw lezen (ms).
163
+ *
164
+ * De reconcile-sweep uit het Stripe/Shopify-patroon, en in deze codebase al bewezen als
165
+ * `CHAT_FULL_SWEEP_INTERVAL_MS`: cursors rusten op de aanname dat de leverancier een
166
+ * `updatedAt` bumpt voor alles wat telt, en dat houdt voor nieuwe records maar is
167
+ * onbetrouwbaar voor edits en verwijderingen. Afwezig = nooit vegen (juist voor een
168
+ * delta-feed die verwijderingen zelf meldt).
169
+ */
170
+ fullSweepIntervalMs?: number;
171
+ }
172
+ /**
173
+ * Bare payload van `GET /provider/sync/describe` — **niet** in `ResponseFactory` verpakt
174
+ * (mal: {@link MemorySourceDescription} en `AnalyticsSourceDescription`).
175
+ */
176
+ export interface SyncSourceDescribe {
177
+ /** De declarerende app (== `manifest.name` == `req.source.app`). */
178
+ source: string;
179
+ sources: SyncSourceDefinition[];
180
+ }
181
+ /**
182
+ * Eén record uit een bronsysteem, klaar om geland te worden.
183
+ *
184
+ * De bron doet de mapping naar de vorm die de doel-app verwacht. Dat is bewust: de runtime kent
185
+ * geen enkel veld van geen enkele leverancier, en zodra hij dat wél zou doen is hij niet meer
186
+ * generiek. Zie {@link SyncRecord.data}.
187
+ */
188
+ export interface SyncRecord {
189
+ /**
190
+ * Het stabiele id in het bronsysteem, genamespaced door de bron zelf
191
+ * (`asana_task:12345`, `graph-todo:AAMk…`).
192
+ *
193
+ * Dit is de dedupe-as: de doel-app upsert hierop via zijn
194
+ * `upsert_by_external_provider_id`-route, dus dezelfde run twee keer draaien schrijft één
195
+ * rij. "Stabiel" is de hele eis — een id dat per pagina verandert maakt van elke sync een
196
+ * duplicaat-fabriek.
197
+ */
198
+ externalId: string;
199
+ /**
200
+ * Ids die de **container** aanwijzen en niet deze rij: de lijst, het project, het board.
201
+ *
202
+ * Ze worden wél bewaard (uitgaande dispatch heeft ze nodig) maar mogen nooit meedoen in de
203
+ * dedupe. Dit veld bestaat omdat het al één keer fout is gegaan: Microsoft Graph stuurt
204
+ * naast het taak-id ook `graph-todolist:<id>`, dat voor élke taak in die lijst hetzelfde is,
205
+ * en zoeken daarop klapte de hele lijst op één rij (`task/external-ids.ts`). Daar is het
206
+ * met een prefix-lijst gerepareerd; hier zegt de bron het zelf, zodat die lijst niet hoeft
207
+ * te groeien.
208
+ */
209
+ containerExternalIds?: string[];
210
+ /** De scope-soort, uit {@link SyncSourceDefinition.kinds}. Bepaalt naar welke app dit gaat. */
211
+ kind: string;
212
+ /**
213
+ * De velden voor de doel-app.
214
+ *
215
+ * **Vaste veldnamen, nooit een map met een extern id als sleutel.** De SDK-client mangelt
216
+ * object-sleutels tussen camel- en snake-case, dus data die ín een sleutel zit komt
217
+ * verminkt aan. Dezelfde les die `chatCursors` een lijst maakte en `ContactSource.sourceId`
218
+ * een veldwaarde.
219
+ */
220
+ data: Record<string, unknown>;
221
+ /**
222
+ * Dit record is bij de bron verwijderd.
223
+ *
224
+ * Alleen zinvol als de bron dat kán weten: een delta-feed meldt verwijderingen, een
225
+ * lijst-endpoint laat ze simpelweg weg. Afwezigheid uit een lijst is géén verwijdering —
226
+ * dat verschil niet respecteren betekent dat een gefilterde of gepagineerde respons je
227
+ * halve administratie opruimt.
228
+ */
229
+ deleted?: boolean;
230
+ }
231
+ /** `POST /provider/sync/pull` — haal één pagina op. */
232
+ export interface SyncPullRequest {
233
+ sourceId: string;
234
+ /** Zodat de bron kan loggen en zijn eigen per-koppeling state kan vinden als hij die heeft. */
235
+ connectionId: string;
236
+ /** De {@link ManagedAccount} om mee te praten. Afwezig bij `transport: "file"`. */
237
+ accountId?: string;
238
+ /**
239
+ * Bij `transport: "tool"`: het id van de MCP-connector die deze koppeling mag gebruiken,
240
+ * gekozen tóen de koppeling werd geconfigureerd.
241
+ *
242
+ * Hiermee is de toolnaam een pure string — `mcpToolPrefix(connectorId) + toolnaam` — en is
243
+ * er per pagina géén resolutie-RPC nodig. Eerder zocht een bron zijn connector op
244
+ * catalogus-sleutel op, en dat kon bij twee kandidaten (een org-rij naast een persoonlijke,
245
+ * of twee gebruikers met elk hun eigen) geen winnaar kiezen; een app kon zelfs het prefix
246
+ * van iemands persoonlijke connector krijgen.
247
+ */
248
+ connectorId?: string;
249
+ /**
250
+ * De instellingen van deze koppeling, volgens {@link SyncSourceDefinition.settingsSchema}.
251
+ *
252
+ * Een **object** hier en een JSON-string in de opslag. Let op: dat is NIET omdat de
253
+ * SDK-client uitgaande sleutels zou mangelen — dat doet hij niet, de verzendkant is
254
+ * `removeUndefined` + `JSON.stringify`. Alleen *antwoorden* worden gecamelCase't. De string in
255
+ * de opslag is er om dezelfde reden als de cursor: één schemaveld dat elke vorm moet kunnen
256
+ * dragen.
257
+ */
258
+ settings?: Record<string, unknown>;
259
+ /**
260
+ * Opaak, exact zoals de bron hem vorige keer teruggaf. `undefined` = vanaf het begin.
261
+ *
262
+ * **De runtime kijkt er nooit in.** Een Graph `deltaLink`, een tijdstempel, een
263
+ * pagina-token — het maakt niet uit, en juist daarom hoeft er geen kolom per sub-sync te
264
+ * bestaan. Dat `microsoft_sync_state` vandaag `todoDeltaLink` náást `chatCursors` náást
265
+ * `chatFullSweepAt` heeft, is precies wat een opaak veld voorkomt.
266
+ */
267
+ cursor?: unknown;
268
+ /** Negeer de cursor en lees alles opnieuw — de reconcile-sweep. */
269
+ fullSweep?: boolean;
270
+ /** Maximum aantal records in dit antwoord. De bron mag minder geven, nooit meer. */
271
+ limit: number;
272
+ }
273
+ /** Antwoord op `POST /provider/sync/pull`. Wél in `ResponseFactory` verpakt. */
274
+ export interface SyncPullResponse {
275
+ records: SyncRecord[];
276
+ /**
277
+ * De cursor ná deze pagina. De runtime bewaart hem **pas nadat de records geland zijn**.
278
+ *
279
+ * Dat is de Singer/Airbyte-regel en de reden dat crash-herstel werkt: valt de boel om
280
+ * tussen pull en land, dan wordt de batch herhaald (idempotent, want upsert op
281
+ * `externalId`) in plaats van overgeslagen. Andersom — cursor eerst — verlies je records
282
+ * stil, en stil is hier het probleem.
283
+ */
284
+ cursor?: unknown;
285
+ /** Geen data meer na deze pagina. De runtime schakelt dan niet door. */
286
+ done: boolean;
287
+ /**
288
+ * De bron is gerate-limit; wacht minstens zo lang voor de volgende poging.
289
+ *
290
+ * In milliseconden, zoals elke andere duur in dit platform. Een bron die dit vult in
291
+ * plaats van te gooien, houdt de run `partial` in plaats van `failed` — het is uitstel,
292
+ * geen fout.
293
+ */
294
+ retryAfterMs?: number;
295
+ }
296
+ /** `POST /provider/sync/land` — schrijf deze records weg. Alleen app-callers. */
297
+ export interface SyncLandRequest {
298
+ /** Waar ze vandaan komen, voor `source`/attributie op de gelande rij. */
299
+ sourceId: string;
300
+ records: SyncRecord[];
301
+ }
302
+ /** Wat er met één record gebeurde. */
303
+ export interface SyncLandOutcome {
304
+ externalId: string;
305
+ /**
306
+ * `created` en `updated` zijn beide "geschreven"; `unchanged` bestaat apart omdat een
307
+ * idempotente resync die niets veranderde géén SSE-ruis mag maken en niet als werk mag
308
+ * tellen (het patroon uit `lib/upsert-dedup.ts`). `failed` is één rij, niet de batch.
309
+ */
310
+ result: "created" | "updated" | "unchanged" | "deleted" | "failed";
311
+ /** De scopeKey van de gelande rij, zodat de run-log naar het resultaat kan linken. */
312
+ scopeKey?: string;
313
+ /** Alleen bij `failed`. Kort genoeg voor een tabelregel. */
314
+ error?: string;
315
+ }
316
+ /** Antwoord op `POST /provider/sync/land`. Wél in `ResponseFactory` verpakt. */
317
+ export interface SyncLandResponse {
318
+ outcomes: SyncLandOutcome[];
319
+ }
320
+ /**
321
+ * Bare payload van `GET /provider/sync/land-describe`: welke soorten deze app kan landen.
322
+ *
323
+ * Apart van `ScopeDescribe`, ook al overlappen de soorten meestal: een app kan een soort
324
+ * autoriseren zonder er een ingest-route voor te hebben. Dat verschil zichtbaar maken is
325
+ * goedkoper dan een run die halverwege een 404 vindt.
326
+ */
327
+ export interface SyncTargetDescribe {
328
+ source: string;
329
+ kinds: string[];
330
+ }
331
+ /** Hoe gezond een koppeling is. Wat de lijst als badge toont. */
332
+ export type SyncHealth =
333
+ /** Laatste run geslaagd. */
334
+ "ok"
335
+ /** Laatste run deels geslaagd, of gerate-limit. Loopt nog, maar niet schoon. */
336
+ | "degraded"
337
+ /** Laatste run gefaald — meestal een verlopen credential. Vraagt een mens. */
338
+ | "broken"
339
+ /** Nooit gedraaid, of een verplicht account ontbreekt. Nog geen fout. */
340
+ | "unconfigured";
341
+ /**
342
+ * Eén geconfigureerde koppeling: deze bron, met dit account, in deze organisatie.
343
+ *
344
+ * De credential zit hier **niet** in — `accountId` verwijst naar de canonieke
345
+ * {@link ManagedAccount}-store. Een tweede plek waar tokens leven is een tweede plek waar ze
346
+ * verlopen zonder dat iemand het weet.
347
+ */
348
+ export interface SyncConnection {
349
+ id: string;
350
+ organizationId: string;
351
+ /** → {@link SyncSourceDefinition.id} */
352
+ sourceId: string;
353
+ /** → {@link ManagedAccount.id}. Afwezig bij een bron zonder credential. */
354
+ accountId?: string;
355
+ /**
356
+ * Bij `transport: "tool"`: de gekozen MCP-connector. Expliciet, niet run-time geraden —
357
+ * zo is in de UI te zien welke verbinding deze koppeling gebruikt, en verandert het antwoord
358
+ * niet doordat iemand anders dezelfde catalogus-entry toevoegt.
359
+ *
360
+ * Is de rij weg of uitgezet, dan gaat de koppeling naar `health: "unconfigured"` en faalt de
361
+ * run luid — nooit een stille lege pagina waarop de cursor doorschuift.
362
+ */
363
+ connectorId?: string;
364
+ /**
365
+ * De ingevulde instellingen, geparsed. In de opslag staat hij als JSON-string, om dezelfde
366
+ * reden als de cursor: één schemaveld dat elke vorm moet kunnen dragen. **Niet** vanwege
367
+ * sleutel-mangeling — de client transformeert uitgaande sleutels niet.
368
+ */
369
+ settings?: Record<string, unknown>;
370
+ /** Vrije naam; afwezig = het label van de bron. */
371
+ label?: string;
372
+ mode: SyncMode;
373
+ enabled: boolean;
374
+ /**
375
+ * Minimale tijd tussen twee rondes, in ms.
376
+ *
377
+ * Bij het aanmaken gekopieerd uit {@link SyncSourceDefinition.defaultIntervalMs}, zodat de
378
+ * tik hem kan lezen zonder per koppeling de bron-catalogus te bevragen — en zodat het
379
+ * effectieve interval te zien en te wijzigen is in plaats van ergens in code te zitten.
380
+ */
381
+ intervalMs?: number;
382
+ /** Opaak, van de bron. Zie {@link SyncPullRequest.cursor}. */
383
+ cursor?: unknown;
384
+ /** Epoch ms van de laatste volledige veegronde. */
385
+ lastFullSweepAt?: number;
386
+ health: SyncHealth;
387
+ lastRunAt?: number;
388
+ /** De fout van de laatste gefaalde run, zodat de lijst hem kan tonen zonder een run te lezen. */
389
+ lastError?: string;
390
+ createdBy?: string;
391
+ createdAt?: number;
392
+ updatedAt?: number;
393
+ }
394
+ /** Waardoor een run begon. */
395
+ export type SyncRunTrigger = "cron" | "manual" | "webhook";
396
+ /**
397
+ * `partial` bestaat naast `done` en `failed` omdat "12 van de 500 rijen deden het niet" geen
398
+ * van beide is: de koppeling werkt, de cursor mag opschuiven, en er is toch iets om te melden.
399
+ * Zonder die derde staat wordt het óf een fout die de sync stilzet óf een succes dat de fouten
400
+ * verzwijgt.
401
+ */
402
+ export type SyncRunStatus = "running" | "done" | "partial" | "failed";
403
+ /**
404
+ * Wat er tijdens één ronde gebeurde.
405
+ *
406
+ * Bewust dezelfde vorm als `PlaybookRun`: het is dezelfde vraag ("wat deed het systeem
407
+ * ongevraagd, en ging het goed?"), dus dezelfde velden en dezelfde schermen.
408
+ */
409
+ export interface SyncRun {
410
+ id: string;
411
+ organizationId: string;
412
+ connectionId: string;
413
+ sourceId: string;
414
+ trigger: SyncRunTrigger;
415
+ status: SyncRunStatus;
416
+ startedAt: number;
417
+ finishedAt?: number;
418
+ /** Hoeveel pagina's deze run gelezen heeft. Verraadt een bron die niet opschiet. */
419
+ pages: number;
420
+ scanned: number;
421
+ /** `created` + `updated`. Expliciet niet `unchanged`, anders lijkt een lege resync werk. */
422
+ written: number;
423
+ /** `unchanged` — de gezonde uitkomst van een idempotente resync. */
424
+ skipped: number;
425
+ failed: number;
426
+ /** Was dit een veegronde? Verklaart waarom `scanned` ineens veel hoger is. */
427
+ fullSweep?: boolean;
428
+ /** De fout die de hele run stopte. Bij `partial` leeg — die staan in {@link SyncRun.errors}. */
429
+ error?: string;
430
+ /**
431
+ * De eerste N mislukte rijen, met hun `externalId`.
432
+ *
433
+ * Begrensd en niet volledig: een bron waarvan élke rij faalt zou anders een run-rij van
434
+ * megabytes opleveren. Het doel is debuggen ("welke rij, en waarom"), niet boekhouden.
435
+ */
436
+ errors?: {
437
+ externalId: string;
438
+ message: string;
439
+ }[];
440
+ }
441
+ /** Hoeveel mislukte rijen een {@link SyncRun} onthoudt. */
442
+ export declare const SYNC_RUN_MAX_ERRORS = 20;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencxh/domain",
3
- "version": "1.162.0",
3
+ "version": "1.165.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",