@opencxh/domain 1.164.0 → 1.166.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.
@@ -28,12 +28,15 @@ export interface AIProfile<T extends Record<string, any> = Record<string, any>>
28
28
  /** Namespaced tool names this profile may use (default: none enabled). */
29
29
  enabledTools?: string[];
30
30
  /**
31
- * Per-tool confirmation policy. Stored as a LIST (not a keyed map) on purpose:
32
- * namespaced tool names contain "__", and the SDK client transforms every
33
- * object KEY between camelCase/snake_case — which mangles map keys (e.g.
34
- * `communication__compose_email` -> `communication_ComposeEmail`) so they no
35
- * longer match `enabledTools`. As a list the tool name lives in a string VALUE,
36
- * which the transform leaves untouched.
31
+ * Per-tool confirmation policy, stored as a LIST (not a keyed map).
32
+ *
33
+ * De oorspronkelijke reden was dwingend: de SDK-client transformeerde élke object-key
34
+ * camel↔snake, en dat verminkte een genamespacede toolnaam (`communication__compose_email` →
35
+ * `communication_ComposeEmail`) zodat hij niet meer matchte met `enabledTools`. **Die transform
36
+ * is 17-08-2026 verwijderd**, dus een map zou nu werken.
37
+ *
38
+ * De lijst blijft staan: hij zit op bestaande profielrijen met een formulier eroverheen, en een
39
+ * migratie zou alleen een prettigere vorm opleveren. `enabledTools` ernaast is ook een lijst.
37
40
  *
38
41
  * Under autonomy "suggest", write tools are held for approval; reads run freely.
39
42
  * A tool absent from this list defaults to "write" (safe).
@@ -42,9 +42,11 @@ export interface DashboardDef {
42
42
  source?: string;
43
43
  }
44
44
  /**
45
- * A single localized string. Kept as a flat LIST (not a lang->key->value map) because the app-sdk http
46
- * client mangles object KEYS (snake<->camel) — i18n keys with underscores/dots as map keys get corrupted
47
- * (e.g. "comms.dash.resolution_over_time"). As list VALUES they pass through untouched.
45
+ * A single localized string. Kept as a flat LIST (not a lang->key->value map).
46
+ *
47
+ * The original reason (the http client mangled object KEYS, so "comms.dash.resolution_over_time" as a
48
+ * key got corrupted) no longer applies — that transform was removed on 2026-08-17. The flat list is
49
+ * kept because every consumer already iterates it and a nested map would be a migration for looks.
48
50
  */
49
51
  export interface LocaleString {
50
52
  lang: string;
@@ -12,10 +12,12 @@ import { MetricDefinition } from './metric';
12
12
  */
13
13
  export type WidgetType = "table" | "counter";
14
14
  /**
15
- * One filter clause. Filters (and cells/totals below) are LISTS, not maps — the app-sdk http client
16
- * deep-mangles every object KEY (camel<->snake), which would corrupt dimension ids / metric ids used as
17
- * keys (they contain dots and mixed case). As list VALUES they pass through untouched. This mirrors the
18
- * platform's existing {name,value}[] pattern (see AIProfile.toolPolicy).
15
+ * One filter clause. Filters (and cells/totals below) are LISTS, not maps.
16
+ *
17
+ * The original reason was that the http client mangled object keys; that transform was removed on
18
+ * 2026-08-17 and keys now travel untouched. The shape stays, for a reason that always held and is
19
+ * the stronger one: **a filter list can carry the same dimension twice** (`status=open` OR
20
+ * `status=pending`), and a map cannot. Same for cells, which mirror a table row.
19
21
  */
20
22
  export interface FilterClause {
21
23
  dimensionId: DimensionId;
@@ -60,54 +60,3 @@ export interface Transcription {
60
60
  export interface SessionMetadata {
61
61
  [custom: string]: unknown;
62
62
  }
63
- export type AuthState = {
64
- type: "oauth2";
65
- accessToken: string;
66
- refreshToken?: string;
67
- /** Epoch ms */
68
- expiresAt: number;
69
- scopes: string[];
70
- idToken?: string;
71
- } | {
72
- type: "password";
73
- username: string;
74
- /** Rauw wachtwoord (geen secret-store in repo). Alleen via internal-path. */
75
- password: string;
76
- /** Toekomstige secret-store-referentie i.p.v. raw `password`. */
77
- passwordRef?: string;
78
- } | {
79
- type: "certificate";
80
- certRef: string;
81
- } | {
82
- type: "apikey";
83
- /** Rauwe API-key (geen secret-store in repo). Alleen via internal-path. */
84
- apiKey: string;
85
- } | {
86
- type: "static";
87
- };
88
- export type ManagedAccountStatus = "connected" | "disconnected" | "error" | "expired";
89
- export interface ManagedAccount {
90
- id: string;
91
- organizationId: string;
92
- /** null = org-level account (geen specifieke user). */
93
- userId?: string;
94
- providerId: string;
95
- /** "oauth2" | "imap" | "sip" | "webhook" | "apikey" | provider-eigen string */
96
- protocol: string;
97
- displayName: string;
98
- status: ManagedAccountStatus;
99
- auth: AuthState;
100
- /** Provider-specifieke metadata (tenantId, externalUserId, scope-flags). */
101
- metadata: Record<string, unknown>;
102
- createdAt?: number;
103
- updatedAt?: number;
104
- }
105
- /**
106
- * Public-view zonder tokens — wat `account/list` en `account/get` aan
107
- * UI/comm-server retourneren. `auth` is gestripped tot alleen het type.
108
- */
109
- export type ManagedAccountPublic = Omit<ManagedAccount, "auth"> & {
110
- authType: AuthState["type"];
111
- scopes?: string[];
112
- expiresAt?: number;
113
- };
@@ -20,8 +20,11 @@ export interface ContactSourceConfig {
20
20
  * Either a built-in source id (`microsoft-directory`, `google`, `eylo-voip`, `local`)
21
21
  * or an AI tool name (`mcp__<id>__search_companies`).
22
22
  *
23
- * A field VALUE, never an object key: the SDK client deep-mangles object keys between
24
- * camel and snake case, which would corrupt any namespaced name used as a key.
23
+ * A field VALUE, never an object key.
24
+ *
25
+ * The original reason (the SDK client mangled object keys) is gone as of 2026-08-17. It stays a
26
+ * value because this is a table row: it carries `order`, `search` and `enrich` alongside, and a
27
+ * map has no ordering.
25
28
  */
26
29
  sourceId: string;
27
30
  kind: "internal" | "mcp";
@@ -0,0 +1 @@
1
+ export * from './types';
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Wie is "Asana-gebruiker 1203847" hier?
3
+ *
4
+ * Deze laag bestond niet, en dat is precies waarom `apps/asana` zijn `assignees` leeg laat: een
5
+ * Asana-assignee is een gid, en die één-op-één overnemen zou werk toewijzen aan een id dat in dit
6
+ * platform niemand is — werk op naam van een spook. De enige twee opties waren "veld weglaten" of
7
+ * "per app een hack", en elke volgende connector die assignees, auteurs, reviewers of commenters
8
+ * synct loopt er opnieuw tegenaan.
9
+ *
10
+ * ## Waarom dit een eigen store is en geen veld
11
+ *
12
+ * Een externe identiteit is **niet** hetzelfde als de vier dingen die er op lijken:
13
+ *
14
+ * | Lijkt erop | Is | Waarom niet hetzelfde |
15
+ * |---|---|---|
16
+ * | `externalId` op een record | dedupe-as voor één rij | zegt niets over personen |
17
+ * | `ManagedAccount.metadata.externalUserId` | de identiteit van een *verbonden account* | één per account, en alleen van wie zelf verbond |
18
+ * | `ManagedAccount.userId` | van wie is dit credential | eigendom, geen mapping |
19
+ * | `Contact` / shadow contact | een persoon *buiten* de organisatie | een assignee is iemand *binnen* de organisatie, in een extern systeem |
20
+ *
21
+ * De vraag hier is de vijfde: welke platform-gebruiker hóórt bij dit externe account. Die heeft
22
+ * een eigen levensduur (je koppelt hem één keer, hij geldt voor elke connector-ronde daarna) en
23
+ * een eigen beheer-oppervlak ("3 van de 12 Asana-gebruikers gekoppeld").
24
+ *
25
+ * ## Waarom in `apps/user`
26
+ *
27
+ * Bij de app die gebruikers bezit. De e-mail-bootstrap heeft de tenant-roster nodig en die staat
28
+ * daar; en niet-sync-consumenten (AI-tools, webhooks, companion-apps) kunnen hem net zo goed
29
+ * gebruiken. In de sync-pijplijn zetten zou hem onbruikbaar maken buiten sync.
30
+ */
31
+ /**
32
+ * Hoe zeker weten we dat deze koppeling klopt?
33
+ *
34
+ * De drie waarden zijn geen kwaliteitslabel maar een **beslissingsas**: alleen bij `confirmed` en
35
+ * `email-match` mag er een `userId` aan hangen, en een `unmatched`-rij bestaat juist om zichtbaar
36
+ * te maken dat er iets te koppelen valt.
37
+ */
38
+ export type ExternalIdentityConfidence =
39
+ /** Een mens heeft dit gekoppeld. Wordt nooit door een bootstrap overschreven. */
40
+ "confirmed"
41
+ /** Het externe adres matchte exact op het adres van een actieve gebruiker. */
42
+ | "email-match"
43
+ /** Bekend extern account, geen platform-gebruiker gevonden. `userId` is leeg. */
44
+ | "unmatched";
45
+ export interface ExternalIdentity {
46
+ id: string;
47
+ organizationId: string;
48
+ /**
49
+ * Welk extern systeem. De catalogus-sleutel van de connector (`"asana"`) of de app-namespace
50
+ * (`"microsoft"`) — hetzelfde vocabulaire als `SyncSourceDefinition.connectorHint`.
51
+ *
52
+ * Bewust niet het connector-**id**: twee organisatie-verbindingen naar hetzelfde product delen
53
+ * dezelfde gebruikers, en dan zou je de mapping twee keer moeten doen.
54
+ */
55
+ connectorKey: string;
56
+ /** Het id van deze persoon in dat systeem: een Asana-gid, een Graph-`oid`, een Slack-user-id. */
57
+ externalId: string;
58
+ /** De platform-gebruiker. Leeg bij `confidence: "unmatched"`. */
59
+ userId?: string;
60
+ /** Wat het externe systeem als adres gaf. Bewaard zodat een latere bootstrap kan matchen. */
61
+ externalEmail?: string;
62
+ /** Wat het externe systeem als naam gaf. Alleen om de beheerlijst leesbaar te maken. */
63
+ externalName?: string;
64
+ confidence: ExternalIdentityConfidence;
65
+ createdAt?: number;
66
+ updatedAt?: number;
67
+ }
68
+ /** Eén extern account waarover een connector een vraag stelt. */
69
+ export interface ExternalIdentityQuery {
70
+ externalId: string;
71
+ /** Als het externe systeem een adres geeft. Zonder dit kan de bootstrap niets matchen. */
72
+ email?: string;
73
+ name?: string;
74
+ }
75
+ /**
76
+ * `POST /external-identity/resolve` — **batch**, want één sync-pagina draagt er tientallen.
77
+ *
78
+ * Per record vragen zou per pagina tientallen app-grenzen kosten, en dat is precies de fout die
79
+ * `landRecords` al vermijdt door per soort te groeperen.
80
+ */
81
+ export interface ResolveExternalIdentitiesRequest {
82
+ connectorKey: string;
83
+ externals: ExternalIdentityQuery[];
84
+ }
85
+ /** Wat de resolve teruggeeft per extern id. `userId` afwezig = niets om aan toe te wijzen. */
86
+ export interface ResolvedExternalIdentity {
87
+ userId?: string;
88
+ confidence: ExternalIdentityConfidence;
89
+ }
90
+ export interface ResolveExternalIdentitiesResponse {
91
+ /**
92
+ * Op `externalId`. Elk gevraagd id staat erin, ook de niet-gematchte — een ontbrekende sleutel
93
+ * en "geen match" zijn twee verschillende dingen, en de beller mag ze niet hoeven raden.
94
+ *
95
+ * Een gewone map is hier veilig: sinds de camelCase-transform op antwoorden verwijderd is
96
+ * (17-08-2026) reizen sleutels ongeschonden, dus een gid met een punt of een streep erin komt
97
+ * heel aan.
98
+ */
99
+ resolved: Record<string, ResolvedExternalIdentity>;
100
+ }
101
+ /** `POST /external-identity/link` — een mens wijst de koppeling aan. */
102
+ export interface LinkExternalIdentityRequest {
103
+ connectorKey: string;
104
+ externalId: string;
105
+ /** Leeg = ontkoppelen; de rij blijft bestaan als `unmatched`. */
106
+ userId?: string;
107
+ }
@@ -22,8 +22,11 @@ export interface KnowledgeBase {
22
22
  * `defaultLocale`; kept so the backfill can still read it. */
23
23
  locale?: KbLocale;
24
24
  }
25
- /** A name and blurb for one language. Never a map keyed by locale: the http
26
- * client mangles object keys, so `{ "nl-NL": … }` does not survive the wire. */
25
+ /** A name and blurb for one language.
26
+ *
27
+ * A list, not a map keyed by locale. The original reason (the http client mangled object keys) is
28
+ * gone as of 2026-08-17 — a map would now survive the wire. Kept as a list because it is stored
29
+ * data with a form and a backfill behind it, and a migration would buy nothing but a nicer shape. */
27
30
  export interface KbTranslation {
28
31
  locale: KbLocale;
29
32
  name: string;
@@ -1,7 +1,57 @@
1
1
  /** How the AI server authenticates to an MCP server. */
2
2
  export type McpAuthMode = "header" | "oauth";
3
- /** Whether an OAuth-authenticated MCP server uses one shared org account or one per user. */
3
+ /**
4
+ * @deprecated Gebruik {@link McpCredentialScope}. Blijft één release staan omdat bestaande
5
+ * rijen `oauth.scope` dragen; `mcpCredentialScope()` leest beide.
6
+ */
4
7
  export type McpOAuthAccountScope = "user" | "org";
8
+ /**
9
+ * **Hoe deze connector aan zijn credential komt:** één gedeeld organisatie-credential, of
10
+ * iedereen brengt zijn eigen mee.
11
+ *
12
+ * Dit is de tweede van de drie scope-vragen, en de enige die op de connector hoort:
13
+ *
14
+ * | Vraag | Waar |
15
+ * |---|---|
16
+ * | Van wie is deze *rij*? | `ownerUserId` / `ManagedAccount.userId` (afwezig = gedeeld) |
17
+ * | Hoe komt hij aan een credential? | **`credentialScope`** — hier |
18
+ * | Met wie is de *resource* gedeeld? | `ownerScope` op contacten, werkitems, geheugens |
19
+ *
20
+ * Het is een **configuratiekeuze van de organisatie**, geen eigenschap van de vendor: dezelfde
21
+ * MCP-server kan met één service-account of met een token per gebruiker draaien. Hij hoort dus op
22
+ * de connectorrij, want daar woont configuratie.
23
+ *
24
+ * De consequentie die hem belangrijk maakt: **een onbemande run (sync, cron, playbook) heeft geen
25
+ * handelende gebruiker en kan dus nooit bij een `"per-user"`-credential.** Dat is vooraf te
26
+ * controleren en uit te leggen, in plaats van te eindigen als een ronde die nul tools zag.
27
+ *
28
+ * `personalConnect` is hier expliciet géén onderdeel van: dat is een *permissie* (mag een lid zijn
29
+ * eigen token aan een beheerder-gedefinieerde server hangen), niet een scope. Alleen zinvol samen
30
+ * met `"per-user"`.
31
+ */
32
+ export type McpCredentialScope = "shared" | "per-user";
33
+ /**
34
+ * De credential-scope van een server, met terugval op de oude velden.
35
+ *
36
+ * Bestaande rijen dragen `oauth.scope`; nieuwe dragen `credentialScope`. Eén functie zodat
37
+ * "wie brengt het token mee" nergens twee keer wordt uitgerekend.
38
+ */
39
+ export declare function mcpCredentialScope(server: {
40
+ credentialScope?: McpCredentialScope;
41
+ authMode?: McpAuthMode;
42
+ oauth?: {
43
+ scope?: McpOAuthAccountScope;
44
+ };
45
+ }): McpCredentialScope;
46
+ /**
47
+ * De naamconventie van een MCP-tool, op één plek.
48
+ *
49
+ * Een tool heet `mcp__<connectorId>__<toolnaam>`. Het connector-id is per organisatie
50
+ * willekeurig, dus een app kan de naam niet hardcoderen — maar zodra hij het id *heeft*
51
+ * (bijvoorbeeld omdat de koppeling het expliciet bewaart) is dit een pure string en is er
52
+ * geen enkele lookup nodig.
53
+ */
54
+ export declare function mcpToolPrefix(connectorId: string): string;
5
55
  /**
6
56
  * Client-facing OAuth view for an MCP server. Never carries the client secret
7
57
  * or tokens — those live server-side (entity `oauth.client` / ManagedAccount).
@@ -24,6 +74,14 @@ export interface McpServerConfig {
24
74
  id: string;
25
75
  organizationId: string;
26
76
  name: string;
77
+ /**
78
+ * De catalogus-sleutel waar deze verbinding uit komt, als hint voor pickers.
79
+ *
80
+ * Stond wél op de write-body maar niet op de read view, en dáárom moest het "welke verbinding is
81
+ * Asana"-antwoord server-side verzonnen worden: een client kón het niet zien. Nu wel, en dan is
82
+ * voorfilteren gewoon een `filter` in de UI.
83
+ */
84
+ catalogKey?: string;
27
85
  /** Only Streamable HTTP transport is supported for server-side connections. */
28
86
  transport: "http";
29
87
  url: string;
@@ -35,14 +93,21 @@ export interface McpServerConfig {
35
93
  /** OAuth settings, present when `authMode === "oauth"`. */
36
94
  oauth?: McpOAuthClientView;
37
95
  /**
38
- * Owner of this server DEFINITION: absent/null = org-defined (admin-managed),
39
- * set = a user's own custom personal server. Governs who may edit/delete it and
40
- * where it is visible; orthogonal to `oauth.scope` (which is about credentials).
96
+ * Hoe deze connector aan zijn credential komt. Afwezig op oude rijen — lees hem altijd via
97
+ * {@link mcpCredentialScope}, nooit rechtstreeks.
98
+ */
99
+ credentialScope?: McpCredentialScope;
100
+ /**
101
+ * Van wie is deze RIJ: afwezig = door de organisatie gedefinieerd (beheerder), gezet = de
102
+ * eigen custom server van die gebruiker. Bepaalt wie hem mag wijzigen en waar hij zichtbaar
103
+ * is — los van {@link McpCredentialScope}, dat over het credential gaat.
41
104
  */
42
105
  ownerUserId?: string;
43
106
  /**
44
- * Org-defined servers only: users may connect this personally (the "catalog").
45
- * Only meaningful together with `oauth.scope === "user"`.
107
+ * PERMISSIE, geen scope: mag een lid zijn eigen credential hangen aan deze door de
108
+ * organisatie gedefinieerde server (de "catalogus")? Alleen zinvol bij
109
+ * `credentialScope === "per-user"`. Bewust niet afgeleid — dat zou het recht stil
110
+ * verruimen voor servers waar de beheerder het juist niet toestond.
46
111
  */
47
112
  personalConnect?: boolean;
48
113
  }
@@ -75,16 +140,55 @@ export interface McpCatalogEntry {
75
140
  scope?: McpOAuthAccountScope;
76
141
  scopes?: string[];
77
142
  };
143
+ /** De voorgestelde default bij het toevoegen. Afwezig = afleiden uit `oauth.scope`. */
144
+ credentialScope?: McpCredentialScope;
145
+ }
146
+ /**
147
+ * Wat een app over een **gekozen** connector mag weten.
148
+ *
149
+ * Dit is een preflight, geen zoekopdracht: je komt hier met een id dat iemand bij het
150
+ * configureren heeft vastgelegd, en de vraag is alleen "kan ik hem nog gebruiken". Een lookup
151
+ * op primary key, dus deterministisch van constructie — anders dan de eerdere zoekopdracht op
152
+ * catalogus-sleutel, die bij twee kandidaten een willekeurige winnaar koos.
153
+ */
154
+ export interface McpConnectorDescribe {
155
+ id: string;
156
+ name: string;
157
+ /** Alleen als hij uit de catalogus komt. */
158
+ catalogKey?: string;
159
+ enabled: boolean;
160
+ credentialScope: McpCredentialScope;
161
+ /**
162
+ * Er bestaat een gedeeld organisatie-credential.
163
+ *
164
+ * De enige vorm die een onbemande run kan gebruiken: die heeft geen handelende gebruiker en
165
+ * kan dus niet bij een persoonlijk token. `false` bij een `"per-user"`-connector, ook als
166
+ * tien mensen persoonlijk verbonden zijn.
167
+ */
168
+ sharedCredential: boolean;
169
+ /** `mcp__<id>__`, hetzelfde als {@link mcpToolPrefix} — meegegeven zodat de beller niets rekent. */
170
+ toolPrefix: string;
78
171
  }
79
172
  /** Body accepted by the create/update endpoints (write shape, distinct from the read view). */
80
173
  export interface McpServerInput {
81
174
  name?: string;
82
175
  url?: string;
176
+ /**
177
+ * De catalogus-sleutel waar dit uit komt ({@link McpCatalogEntry.key}).
178
+ *
179
+ * **Een hint, geen resolutie.** Hij filtert de connector-picker ("welke van jouw
180
+ * verbindingen is Asana") en voedt de "al toegevoegd"-status in de catalogus. Wie een
181
+ * connector nodig heeft bewaart het connector-**id** expliciet en gebruikt
182
+ * {@link mcpToolPrefix} — een zoekopdracht op key kan bij twee kandidaten geen winnaar
183
+ * kiezen en heeft dat eerder ook niet gedaan.
184
+ */
185
+ catalogKey?: string;
83
186
  enabled?: boolean;
84
187
  /** Header-mode secrets; masked values mean "unchanged". */
85
188
  headers?: Record<string, string>;
86
189
  authMode?: McpAuthMode;
87
190
  oauth?: McpServerOAuthInput;
191
+ credentialScope?: McpCredentialScope;
88
192
  /** Org-defined servers: mark as personally connectable (catalog membership). */
89
193
  personalConnect?: boolean;
90
194
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -13,9 +13,11 @@ export type MemoryKindId = string;
13
13
  * Een verwijzing naar iets anders — intern (`interaction:i_2`) of extern
14
14
  * (`hubspot:5591`, `clickup:86ab2`).
15
15
  *
16
- * Een **lijst**, nooit een object met de relatie als key: de app-sdk http-client
17
- * mangelt elke object-key camel<->snake, dus `{ "crm.company": "..." }` komt verminkt
18
- * aan de andere kant aan.
16
+ * Een **lijst**, nooit een object met de relatie als key.
17
+ *
18
+ * De oorspronkelijke reden (de http-client mangelde object-keys) geldt niet meer sinds 17-08-2026.
19
+ * De lijst blijft om een reden die altijd al sterker was: **dezelfde `rel` mag twee keer voorkomen**
20
+ * — twee gekoppelde bedrijven, drie betrokken interacties — en een map gooit er dan één weg.
19
21
  */
20
22
  export interface MemoryLink {
21
23
  rel: string;
@@ -48,7 +48,7 @@ export interface MemorySourceDescription {
48
48
  /** De declarerende app (== `manifest.name` == `req.source.app`). */
49
49
  source: string;
50
50
  kinds: MemoryKindDefinition[];
51
- /** Vlakke LIJST: keys met een punt worden gemangeld. */
51
+ /** Vlakke LIJST, zoals {@link LocaleBundle}; zie daar waarom die vorm blijft. */
52
52
  locales?: LocaleBundle;
53
53
  /** Fase 3: welke subjects/kinds deze bron live kan leveren. */
54
54
  live?: {
@@ -61,7 +61,7 @@ export interface AssignmentSourceDescription {
61
61
  /** De declarerende app (== `manifest.name` == `req.source.app`). */
62
62
  source: string;
63
63
  targets: AssignmentTargetKind[];
64
- /** Vlakke LIJST: keys met een punt worden gemangeld. */
64
+ /** Vlakke LIJST, zoals {@link LocaleBundle}; zie daar waarom die vorm blijft. */
65
65
  locales?: LocaleBundle;
66
66
  }
67
67
  /** De samengestelde catalogus die de hub aan de builder levert. */
@@ -18,4 +18,17 @@ export interface Webhook {
18
18
  triggers: WebhookTrigger[];
19
19
  enabled: boolean;
20
20
  createdBy: string;
21
+ /**
22
+ * Hoe de laatste aflevering ging.
23
+ *
24
+ * Vier velden en geen afleverhistorie: de vraag die een beheerder stelt is "komt dit aan?", en
25
+ * die is met de laatste poging plus een teller beantwoord. Een volledige historie is een eigen
26
+ * scherm en een eigen entiteit.
27
+ */
28
+ lastDeliveryAt?: number;
29
+ /** HTTP-status van de laatste poging. Afwezig = de call kwam niet tot een antwoord. */
30
+ lastStatus?: number;
31
+ lastError?: string;
32
+ /** Opeenvolgende mislukkingen, nul zodra er één aankomt — niet een totaalteller. */
33
+ failureCount?: number;
21
34
  }