@fabricorg/webhook-delivery 0.1.1 → 0.1.3

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.
@@ -1,1444 +0,0 @@
1
- import { EventPayloadClassification, EnterpriseEventEnvelope } from '@fabricorg/platform-host';
2
-
3
- /**
4
- * Fixed code sets. Stored diagnostics, denial reasons and attempt outcomes are
5
- * typed as one of these codes rather than a free string or a port's error
6
- * message (ADR 0018 decision 2), so a thrown error has no field through which
7
- * to carry payload, secret or response-body text into a durable record. The
8
- * arrays are frozen, so no caller can widen a set at runtime.
9
- */
10
- /** Reasons an admission port may give for `deny`. */
11
- declare const WEBHOOK_ADMISSION_DENIAL_CODES: readonly ["actor_not_permitted", "scope_not_permitted", "event_type_not_permitted", "classification_not_permitted", "policy_denied"];
12
- /** A reason code carried by an admission `deny` decision and stored on its denial record. */
13
- type WebhookAdmissionDenialCode = (typeof WEBHOOK_ADMISSION_DENIAL_CODES)[number];
14
- /** Reasons a delivery authorization port may give for `revoked`. */
15
- declare const WEBHOOK_REVOCATION_CODES: readonly ["grant_expired", "scope_revoked", "registration_inactive"];
16
- /** A reason code carried by a delivery authorization `revoked` decision. */
17
- type WebhookRevocationCode = (typeof WEBHOOK_REVOCATION_CODES)[number];
18
- /**
19
- * Diagnostics stored on a fan-out job or delivery intent when a port failed,
20
- * timed out or a coordination check refused the work. A port exception is
21
- * stored as the code for that port, not as its message.
22
- *
23
- * Three are written only by the fan-out cycle: `eligibility_unsettled` when
24
- * it defers a job until its event's eligibility is settled (README,
25
- * "Fan-out"), `intent_build_failed` when it could not render a body or
26
- * create a delivery id for an admitted subscription, and `job_unreadable`
27
- * when it dead-letters a claimed job whose identity and lease it could read
28
- * but whose body it could not.
29
- *
30
- * Three are written only by the delivery cycle (README, "Delivery"):
31
- * `intent_unreadable` when it dead-letters a claimed intent whose identity
32
- * and lease it could read but whose body it could not,
33
- * `subscription_not_in_scope` when the intent's subscription does not exist
34
- * in the intent's tenant and space, or the store's answer names another
35
- * one, and `transport_not_sent` when the transport returned `not_sent` for
36
- * a request it never started.
37
- */
38
- declare const WEBHOOK_DIAGNOSTIC_CODES: readonly ["admission_unavailable", "authorization_unavailable", "subscription_store_unavailable", "delivery_store_unavailable", "signer_unavailable", "resolver_unavailable", "port_timeout", "scope_predicate_failed", "configuration_conflict", "lease_lost", "intent_not_pending", "processing_deadline_exceeded", "eligibility_unsettled", "intent_build_failed", "job_unreadable", "intent_unreadable", "subscription_not_in_scope", "transport_not_sent"];
39
- /** A stored diagnostic code for a port failure or refused coordination check. */
40
- type WebhookDiagnosticCode = (typeof WEBHOOK_DIAGNOSTIC_CODES)[number];
41
- /**
42
- * What a delivery attempt's observation means for its intent (ADR 0018
43
- * decision 10). `not_sent` is never a classification: the delivery cycle
44
- * records it, with the reason `not_sent`, on an attempt it began under the
45
- * intent's lease and handed to the transport, when the transport answered
46
- * that it never started the request (its deadline had passed or its signal
47
- * had aborted while the request was queued). Such an attempt spends no budget.
48
- */
49
- declare const WEBHOOK_ATTEMPT_OUTCOMES: readonly ["delivered", "retryable", "dead_letter", "cancelled", "not_sent"];
50
- /**
51
- * The classified result of one attempt. A 401 or 403 is `retryable` like any
52
- * other row of that kind; its `http_401` / `http_403` reason and the
53
- * classification's `receiverActionLikely` flag let the caller surface it to
54
- * operators before the retry budget is exhausted.
55
- */
56
- type WebhookAttemptOutcome = (typeof WEBHOOK_ATTEMPT_OUTCOMES)[number];
57
- /** The row of decision 10's table an observation matched. */
58
- declare const WEBHOOK_ATTEMPT_REASON_CODES: readonly ["http_2xx", "network_error", "timeout", "dns_failure", "http_408", "http_425", "http_429", "http_5xx", "signer_unavailable", "http_401", "http_403", "http_1xx", "http_3xx", "http_410", "http_4xx", "invalid_url", "private_address", "authorization_revoked", "subscription_paused", "subscription_deleted", "not_sent"];
59
- /** The reason code recorded with an attempt outcome. */
60
- type WebhookAttemptReasonCode = (typeof WEBHOOK_ATTEMPT_REASON_CODES)[number];
61
- /** Codes on {@link WebhookDeliveryConfigError}. */
62
- declare const WEBHOOK_CONFIG_ERROR_CODES: readonly ["limit_not_positive_finite", "limit_not_integer", "limit_out_of_range", "retry_schedule_empty", "limit_unreadable"];
63
- /** Why a configured limit was rejected. */
64
- type WebhookConfigErrorCode = (typeof WEBHOOK_CONFIG_ERROR_CODES)[number];
65
- /** Codes on {@link WebhookProjectionError}. */
66
- declare const WEBHOOK_PROJECTION_ERROR_CODES: readonly ["invalid_envelope", "payload_field_not_json", "fact_field_not_json", "accessor_property", "inspection_failed", "scope_mismatch"];
67
- /** Why an envelope could not be projected. */
68
- type WebhookProjectionErrorCode = (typeof WEBHOOK_PROJECTION_ERROR_CODES)[number];
69
- /**
70
- * Codes on the `TypeError` and `RangeError` instances this package throws
71
- * for malformed call input (headers, classification, retry, admission).
72
- * Each code has one constant message; neither repeats the input.
73
- */
74
- declare const WEBHOOK_INPUT_ERROR_CODES: readonly ["input_unreadable", "observation_unknown", "status_out_of_range", "signature_malformed", "timestamp_invalid", "event_id_invalid", "delivery_id_invalid", "attempt_invalid", "raw_body_not_string", "random_source_missing", "random_sample_invalid", "random_source_failed", "denial_code_unknown"];
75
- /** Why call input was refused. */
76
- type WebhookInputErrorCode = (typeof WEBHOOK_INPUT_ERROR_CODES)[number];
77
- /**
78
- * Codes on {@link WebhookStoreError}: a store call that was malformed, broke
79
- * a store invariant, or was no longer wanted when the store reached its
80
- * write. Refusals the caller is expected to handle (a lost lease, an
81
- * existing key, a replay past the horizon, a requeue of a record that is not
82
- * dead-lettered) are results, not errors.
83
- */
84
- declare const WEBHOOK_STORE_ERROR_CODES: readonly ["input_invalid", "record_mismatch", "delivery_id_conflict", "call_aborted", "deadline_passed", "clock_invalid", "store_failed"];
85
- /** Why a store call was refused. */
86
- type WebhookStoreErrorCode = (typeof WEBHOOK_STORE_ERROR_CODES)[number];
87
- /**
88
- * Codes on {@link WebhookFanOutError}: why the fan-out publisher, the
89
- * composite publisher or the fan-out cycle refused or failed. A publisher
90
- * rejection makes the Host relay retry and, once its budget is spent,
91
- * dead-letter the outbox record, so a failed publish stays visible there.
92
- *
93
- * - `event_past_replay_horizon`: `insertFanOutJob` answered `past_horizon`.
94
- * The event is older than the replay horizon (R5), so no job can be stored
95
- * and the publish fails rather than counting as delivered.
96
- * - `fan_out_store_failed` / `fan_out_store_timeout`: the publisher's job
97
- * insert or the cycle's claim threw, or did not settle within
98
- * `portTimeoutMs`. Store failures later in a cycle reschedule the job
99
- * with a diagnostic instead.
100
- * - `publish_aborted`: the caller's signal aborted before the store answered.
101
- * - `sink_failed`: a required sink of a composite publisher rejected with an
102
- * error this package did not create.
103
- * - `sink_timeout`: a required sink of a composite publisher did not settle
104
- * within the composite's `sinkTimeoutMs`.
105
- * - `clock_invalid`: the injected clock threw or returned an invalid time.
106
- * - `options_invalid`: an option is missing or has the wrong shape.
107
- */
108
- declare const WEBHOOK_FAN_OUT_ERROR_CODES: readonly ["event_past_replay_horizon", "fan_out_store_failed", "fan_out_store_timeout", "publish_aborted", "sink_failed", "sink_timeout", "clock_invalid", "options_invalid"];
109
- /** Why a fan-out publish or cycle failed. */
110
- type WebhookFanOutErrorCode = (typeof WEBHOOK_FAN_OUT_ERROR_CODES)[number];
111
- /**
112
- * Codes on {@link WebhookDeliveryError}: why the delivery cycle could not
113
- * run. Everything that goes wrong while one claimed intent is processed is
114
- * settled on that intent with a fixed code instead (README, "Delivery").
115
- *
116
- * - `delivery_store_failed` / `delivery_store_timeout`: the claim threw or
117
- * answered something unreadable, or did not settle within `portTimeoutMs`.
118
- * - `clock_invalid`: the injected clock threw or returned an invalid time.
119
- * - `options_invalid`: an option or port is missing or has the wrong shape.
120
- */
121
- declare const WEBHOOK_DELIVERY_ERROR_CODES: readonly ["delivery_store_failed", "delivery_store_timeout", "clock_invalid", "options_invalid"];
122
- /** Why a delivery cycle failed. */
123
- type WebhookDeliveryErrorCode = (typeof WEBHOOK_DELIVERY_ERROR_CODES)[number];
124
- /**
125
- * Why a governed action (PR 9; README, "Governed actions") refused. At
126
- * submission, the action's admission check refuses with
127
- * `webhook_action_refused:<code>` (or `webhook_parameters_refused:<code>:<field>`
128
- * with a code from `WEBHOOK_ACTION_PARAMETER_CODES`) and Host records nothing.
129
- * At execution, the action fails with Host's `error` set to
130
- * `webhook_action_refused:<code>`, writes nothing more and emits no event.
131
- *
132
- * - `admission_not_enforced`: the handler ran without Host's record that the
133
- * submission was admitted (`ActionContext.admittedAt`), as under a Host
134
- * older than 7.3.0, which ignores `admitSubmission`. Nothing is written.
135
- * - `scope_invalid` / `actor_invalid`: Host's trusted context names a tenant,
136
- * space or actor that breaks the store identifier rule.
137
- * - `secret_ref_invalid`: the application's secret store says the reference
138
- * names no secret of this tenant. `secret_ref_unverified`: the check threw,
139
- * timed out or answered something unreadable.
140
- * - `secret_ref_unchanged`: a rotation named the reference already in use.
141
- * - `destination_refused`: the resolver refused the URL (`invalid_url`,
142
- * `private_address`). `destination_unresolved`: it answered `dns_failure`,
143
- * threw or timed out. Neither writes anything.
144
- * - `subscription_not_found`, `subscription_exists`, `subscription_deleted`,
145
- * `invalid_transition`, `revision_conflict`, `version_conflict`: the
146
- * subscription store refused the write, or the action's own read found
147
- * the same. `subscription_not_active`: an intent requeue for a paused or
148
- * deleted subscription. `paused_since_intent`: an intent requeue for an
149
- * active subscription that was paused and resumed after the intent's
150
- * eligibility revision; dispatch would cancel it (decision 3).
151
- * - `not_found`, `stale`, `not_dead_letter`, `past_replay_horizon`: the
152
- * delivery store refused a requeue or enrichment (decision 10, R5).
153
- * - `not_enrichable`, `actor_present`, `actor_suppressed`: enrichment of a
154
- * completed job, or of a job whose actor is not `legacy` (decision 5).
155
- * `actor_withheld`, `actor_not_found`, `actor_unavailable`: the
156
- * application's actor reader withheld the actor, found no such event, or
157
- * threw, timed out or answered something unreadable.
158
- * - `store_unavailable`: a read threw, timed out or answered something
159
- * unreadable; nothing was written. `store_outcome_unknown`: a write threw,
160
- * timed out or answered something unreadable, so it may have applied
161
- * (README, "A store write that timed out is unknown"); also a re-run of an
162
- * intent requeue whose evidence predates `subscriptionId` and whose intent
163
- * can no longer be read, so the applied requeue has no event (README,
164
- * "Atomicity and re-runs").
165
- * - `action_failed`: anything else, such as a failing clock; nothing it
166
- * threw is read.
167
- */
168
- declare const WEBHOOK_ACTION_REFUSAL_CODES: readonly ["admission_not_enforced", "scope_invalid", "actor_invalid", "secret_ref_invalid", "secret_ref_unverified", "secret_ref_unchanged", "destination_refused", "destination_unresolved", "subscription_not_found", "subscription_exists", "subscription_deleted", "subscription_not_active", "paused_since_intent", "invalid_transition", "revision_conflict", "version_conflict", "not_found", "stale", "not_dead_letter", "past_replay_horizon", "not_enrichable", "actor_present", "actor_suppressed", "actor_withheld", "actor_not_found", "actor_unavailable", "store_unavailable", "store_outcome_unknown", "action_failed"];
169
- /** Why a governed action refused. */
170
- type WebhookActionRefusalCode = (typeof WEBHOOK_ACTION_REFUSAL_CODES)[number];
171
- /**
172
- * Codes on {@link WebhookGovernedActionError}: `options_invalid`, a module
173
- * option or port is missing or has the wrong shape.
174
- */
175
- declare const WEBHOOK_GOVERNED_ACTION_ERROR_CODES: readonly ["options_invalid"];
176
- /** Why the governed-action module could not be built. */
177
- type WebhookGovernedActionErrorCode = (typeof WEBHOOK_GOVERNED_ACTION_ERROR_CODES)[number];
178
-
179
- /** A JSON value. Every record this package defines is built from these. */
180
- type WebhookJsonValue = null | boolean | number | string | readonly WebhookJsonValue[] | {
181
- readonly [key: string]: WebhookJsonValue;
182
- };
183
- /** A JSON object. */
184
- type WebhookJsonObject = {
185
- readonly [key: string]: WebhookJsonValue;
186
- };
187
- /** The Host envelope this package consumes, re-exported for adapters. */
188
- type WebhookSourceEnvelope = EnterpriseEventEnvelope;
189
- /**
190
- * The envelope metadata a fan-out job keeps: everything that identifies,
191
- * orders and classifies the event, and nothing from its payload, trace
192
- * context or provenance. Timestamps are ISO-8601 strings so the record is
193
- * serializable.
194
- */
195
- interface WebhookEventMetadata {
196
- readonly eventId: string;
197
- readonly eventType: string;
198
- readonly eventSchemaVersion: number;
199
- readonly tenantId: string;
200
- readonly spaceId: string;
201
- readonly subjectType: string;
202
- readonly subjectId: string;
203
- /** The tenant/space ledger cursor. Delivery order is not guaranteed. */
204
- readonly sequence: number;
205
- readonly actionInvocationId?: string;
206
- readonly correlationId: string;
207
- readonly causationId?: string;
208
- readonly occurredAt: string;
209
- /** Stamped by Host's clock; selects the eligibility revision (decision 3). */
210
- readonly recordedAt: string;
211
- readonly producerModuleVersion: string;
212
- readonly payloadClassification: EventPayloadClassification;
213
- /** Present only when the producing action declared it. */
214
- readonly consistency?: "authoritative-now" | "provisional-until-reconciled";
215
- }
216
- /**
217
- * The actor a job carries for admission, or why it carries none (decision 5).
218
- * `suppressed`: Host's `includeActor` hook excluded it. `legacy`: the envelope
219
- * predates the actor field. Admission that needs an actor treats both
220
- * absences as `unavailable`. The actor is never rendered into a v1 body.
221
- */
222
- type WebhookJobActor = {
223
- readonly state: "present";
224
- readonly actorId: string;
225
- readonly actorType: string;
226
- } | {
227
- readonly state: "absent";
228
- readonly reason: "suppressed" | "legacy";
229
- };
230
- /**
231
- * What the application allows to leave for one event type (decision 9).
232
- * Field names are top-level payload keys. An event type with no entry goes
233
- * out metadata-only.
234
- */
235
- interface WebhookEventTypePolicy {
236
- /** Payload fields that may be persisted on the job and rendered into the body. */
237
- readonly fields: readonly string[];
238
- /**
239
- * Payload fields admission and scope predicates need. They are persisted on
240
- * the job as facts and are never rendered into the body unless also listed
241
- * in `fields`.
242
- */
243
- readonly facts?: readonly string[];
244
- /** A `restricted` event is metadata-only, and keeps no payload or facts, unless this is `true`. */
245
- readonly includeRestricted?: boolean;
246
- /** A scoped type egresses its payload only when the injected scope predicate returns `true`. */
247
- readonly scoped?: boolean;
248
- }
249
- /** Egress policy keyed by event type. Only own properties are consulted. */
250
- type WebhookPayloadPolicy = {
251
- readonly [eventType: string]: WebhookEventTypePolicy;
252
- };
253
- /**
254
- * The persistence projection of one envelope (decision 2): what the fan-out
255
- * publisher inserts, keyed by `eventId`. It holds metadata, the allowlisted
256
- * payload fields and the declared facts only.
257
- */
258
- interface WebhookFanOutJobDraft {
259
- readonly eventId: string;
260
- readonly tenantId: string;
261
- readonly spaceId: string;
262
- readonly event: WebhookEventMetadata;
263
- /** Allowlisted payload fields, or `null` when the event is metadata-only. */
264
- readonly payload: WebhookJsonObject | null;
265
- /** `true` exactly when `payload` is not `null`. */
266
- readonly payloadProjected: boolean;
267
- /** Declared admission and scope facts; empty when none are declared or allowed. */
268
- readonly facts: WebhookJsonObject;
269
- readonly actor: WebhookJobActor;
270
- }
271
- /** A fenced lease. A write carrying an older `fence` than the record's is refused. */
272
- interface WebhookLease {
273
- readonly owner: string;
274
- readonly fence: number;
275
- readonly expiresAt: string;
276
- }
277
- /** Fan-out job lifecycle. */
278
- type WebhookFanOutJobStatus = "pending" | "completed" | "dead_letter";
279
- /** A stored fan-out job. */
280
- interface WebhookFanOutJob extends WebhookFanOutJobDraft {
281
- readonly status: WebhookFanOutJobStatus;
282
- readonly attemptCount: number;
283
- readonly availableAt: string;
284
- /** Persisted at creation; deferrals and reclaims count toward it (R4). */
285
- readonly processingDeadline: string;
286
- readonly createdAt: string;
287
- readonly completedAt?: string;
288
- readonly lastDiagnostic?: WebhookDiagnosticCode;
289
- /**
290
- * The record's fence: the dead-letter generation a requeue must name.
291
- * Each claim and each applied requeue advances it, so a requeue built
292
- * from an earlier read is refused as `stale` (decision 10).
293
- */
294
- readonly fence: number;
295
- readonly lease?: WebhookLease;
296
- }
297
- /** Subscription lifecycle state recorded on each eligibility revision and configuration read. */
298
- type WebhookSubscriptionStatus = "active" | "paused" | "deleted";
299
- /**
300
- * An immutable eligibility revision (decision 3). Create, pattern change,
301
- * pause and resume each write one. The interval is half-open:
302
- * `effectiveFrom <= recordedAt < effectiveUntil`, with `null` meaning open.
303
- */
304
- interface WebhookEligibilityRevision {
305
- readonly subscriptionId: string;
306
- readonly revision: number;
307
- readonly tenantId: string;
308
- readonly spaceId: string;
309
- readonly status: WebhookSubscriptionStatus;
310
- readonly eventTypePatterns: readonly string[];
311
- readonly effectiveFrom: string;
312
- readonly effectiveUntil: string | null;
313
- }
314
- /** The only signature scheme in v1 (decision 7). */
315
- type WebhookSignatureScheme = "v1";
316
- /**
317
- * One version of a subscription's delivery configuration. Intents store no
318
- * destination; attempts record the version they used. The secret is a
319
- * reference resolved by the signer within the subscription's tenant, never
320
- * key material (decision 8).
321
- */
322
- interface WebhookDeliveryConfiguration {
323
- readonly subscriptionId: string;
324
- readonly tenantId: string;
325
- readonly spaceId: string;
326
- readonly version: number;
327
- readonly destinationUrl: string;
328
- readonly signatureScheme: WebhookSignatureScheme;
329
- readonly secretRef: string;
330
- }
331
- /** What the dispatch check reads for one subscription. */
332
- interface WebhookSubscriptionSnapshot {
333
- readonly subscriptionId: string;
334
- readonly tenantId: string;
335
- readonly spaceId: string;
336
- readonly status: WebhookSubscriptionStatus;
337
- /**
338
- * The eligibility revision at which the subscription entered `status`: 1
339
- * when it has never been paused, otherwise its latest pause, resume or
340
- * delete revision (a pattern change keeps the status, so it does not
341
- * move this). Read in the same atomic step as `status`. While the
342
- * subscription is active, an intent bound to a lower revision belongs to
343
- * an interval a pause ended, and dispatch cancels it (decision 3;
344
- * README, "Pause and resume are enforced at dispatch").
345
- */
346
- readonly statusSinceRevision: number;
347
- readonly configuration: WebhookDeliveryConfiguration;
348
- }
349
- /** Delivery intent lifecycle. */
350
- type WebhookDeliveryIntentStatus = "pending" | "delivered" | "dead_letter" | "cancelled";
351
- /** One intent per (subscriptionId, eventId), holding the rendered body (decision 9). */
352
- interface WebhookDeliveryIntent {
353
- readonly deliveryId: string;
354
- readonly subscriptionId: string;
355
- readonly eventId: string;
356
- readonly tenantId: string;
357
- readonly spaceId: string;
358
- readonly eligibilityRevision: number;
359
- /**
360
- * The event's `recordedAt`, copied by the store from the parent fan-out
361
- * job when the intent is inserted. It anchors the replay horizon (R5).
362
- */
363
- readonly eventRecordedAt: string;
364
- /** The rendered `fabric.webhook-delivery.v1` body, signed byte-for-byte per attempt. */
365
- readonly body: string;
366
- readonly status: WebhookDeliveryIntentStatus;
367
- readonly attemptCount: number;
368
- readonly availableAt: string;
369
- readonly processingDeadline: string;
370
- readonly createdAt: string;
371
- readonly lastOutcome?: {
372
- readonly outcome: WebhookAttemptOutcome;
373
- readonly reason: WebhookAttemptReasonCode;
374
- };
375
- readonly lastDiagnostic?: WebhookDiagnosticCode;
376
- /**
377
- * The record's fence: the dead-letter generation a requeue must name.
378
- * Each claim and each applied requeue advances it, so a requeue built
379
- * from an earlier read is refused as `stale` (decision 10).
380
- */
381
- readonly fence: number;
382
- readonly lease?: WebhookLease;
383
- }
384
- /** A recorded admission denial; it satisfies completeness for its (subscription, event). */
385
- interface WebhookAdmissionDenialRecord {
386
- readonly subscriptionId: string;
387
- readonly eventId: string;
388
- readonly tenantId: string;
389
- readonly spaceId: string;
390
- readonly eligibilityRevision: number;
391
- readonly code: WebhookAdmissionDenialCode;
392
- readonly recordedAt: string;
393
- }
394
- /**
395
- * An attempt log entry with its outcome. Response bodies are never
396
- * persisted. An entry appended whole by `appendAttempt` has no `fence`; one
397
- * begun by `beginAttempt` and finished by `finishAttempt` carries the
398
- * intent's lease fence it was begun under.
399
- */
400
- interface WebhookDeliveryAttemptRecord {
401
- readonly deliveryId: string;
402
- readonly subscriptionId: string;
403
- readonly eventId: string;
404
- readonly attempt: number;
405
- readonly configurationVersion: number;
406
- readonly startedAt: string;
407
- readonly finishedAt: string;
408
- readonly outcome: WebhookAttemptOutcome;
409
- readonly reason: WebhookAttemptReasonCode;
410
- readonly httpStatus?: number;
411
- /** The intent lease fence the attempt was begun under; absent for an entry appended whole. */
412
- readonly fence?: number;
413
- }
414
- /**
415
- * What `beginAttempt` records, under the intent's lease, immediately before
416
- * the request is handed to the transport (README, "Delivery": the
417
- * irrevocable point). The configuration version is the one the request is
418
- * signed and addressed with.
419
- */
420
- interface WebhookDeliveryAttemptStart {
421
- readonly deliveryId: string;
422
- readonly subscriptionId: string;
423
- readonly eventId: string;
424
- readonly attempt: number;
425
- readonly configurationVersion: number;
426
- readonly startedAt: string;
427
- }
428
- /** What `finishAttempt` appends to a begun attempt once the transport answered. */
429
- interface WebhookDeliveryAttemptFinish {
430
- readonly finishedAt: string;
431
- readonly outcome: WebhookAttemptOutcome;
432
- readonly reason: WebhookAttemptReasonCode;
433
- readonly httpStatus?: number;
434
- }
435
- /**
436
- * A begun attempt with no outcome recorded: it was handed to the transport,
437
- * or its worker stopped between `beginAttempt` and the handoff, and no
438
- * outcome has been appended since. The request may have reached the
439
- * receiver.
440
- */
441
- interface WebhookDeliveryAttemptInFlight extends WebhookDeliveryAttemptStart {
442
- readonly fence: number;
443
- readonly outcome: "in_flight";
444
- }
445
- /** One entry of an intent's attempt log, in the order its first write was made. */
446
- type WebhookDeliveryAttemptLogEntry = WebhookDeliveryAttemptRecord | WebhookDeliveryAttemptInFlight;
447
- /**
448
- * The uniqueness marker for one (subscriptionId, eventId). The store writes
449
- * it in the same step as the intent or denial it stands for and removes it
450
- * with that record, when a purge finds the event older than the replay
451
- * horizon (R5). `retainUntil` is the event's `recordedAt` plus the store's
452
- * horizon: until then any insert for the pair is `already_present`, and
453
- * after it every insert for the pair is `past_horizon`, so a replay cannot
454
- * re-deliver whether or not a purge has run (the "cleanup racing" cases in
455
- * `tests/store-contract.ts`).
456
- */
457
- interface WebhookTombstone {
458
- readonly kind: "intent" | "denial";
459
- readonly subscriptionId: string;
460
- readonly eventId: string;
461
- readonly tenantId: string;
462
- readonly spaceId: string;
463
- readonly retainUntil: string;
464
- }
465
- /** A governed compare-and-set requeue from dead letter back to pending (decision 10). */
466
- interface WebhookRequeueRequest {
467
- readonly expectedStatus: "dead_letter";
468
- /**
469
- * The `fence` of the dead-lettered record as the operator read it. The
470
- * requeue applies only while the record still has that fence; otherwise
471
- * it is refused as `stale`. Two requeues built from one read therefore
472
- * apply at most once, and a request read before the record was requeued,
473
- * re-run and dead-lettered again does not apply to the new dead letter.
474
- */
475
- readonly expectedFence: number;
476
- readonly requestedBy: string;
477
- readonly requestedAt: Date;
478
- /**
479
- * The requeued record's new processing deadline (R4). The record becomes
480
- * available at `requestedAt` with its attempt budget restored; without a
481
- * new deadline a requeued record past its old one could not be retried.
482
- */
483
- readonly processingDeadline: Date;
484
- /**
485
- * The governed invocation that authorized the requeue, for audit. It is
486
- * also the request's identity: a later request under the same id, for the
487
- * same target and record in the same scope, is a re-run of this one, and
488
- * the store answers it with this request's evidence and `replayed: true`
489
- * instead of applying it (ports, "Re-runs of one invocation").
490
- */
491
- readonly actionInvocationId?: string;
492
- }
493
- /**
494
- * A governed actor enrichment of a `legacy` fan-out job (decision 5, PR 9).
495
- * Like {@link WebhookRequeueRequest} it is a compare-and-set on the job's
496
- * fence as the operator read it, and it restores the budget with a new
497
- * processing deadline. `actor` is what the application's reader found on the
498
- * canonical event for the job's tenant, space and `eventId`.
499
- */
500
- interface WebhookActorEnrichmentRequest {
501
- /** The job's `fence` as the operator read it; a mismatch is refused as `stale`. */
502
- readonly expectedFence: number;
503
- readonly actor: {
504
- readonly actorId: string;
505
- readonly actorType: string;
506
- };
507
- readonly requestedBy: string;
508
- readonly requestedAt: Date;
509
- /** The repaired job's new processing deadline (R4). It becomes available at `requestedAt`. */
510
- readonly processingDeadline: Date;
511
- /** The governed invocation that authorized the enrichment, for audit, and the request's identity, as for {@link WebhookRequeueRequest.actionInvocationId}. */
512
- readonly actionInvocationId?: string;
513
- }
514
- /**
515
- * Append-only evidence of one applied requeue or actor enrichment
516
- * (decisions 5 and 10). The store writes it in the same step as the
517
- * transition, and no store method updates or removes it, a purge included
518
- * (the memory store keeps it for its lifetime, the PostgreSQL store until
519
- * someone deletes rows from `webhook_requeue_evidence` directly);
520
- * the `prior*` fields keep the state the requeue replaced, so a later
521
- * requeue cannot erase an earlier failure. Contract cases: "job requeue is a
522
- * compare-and-set …", "actor enrichment repairs a legacy job …" and "purge
523
- * removes only terminal records …".
524
- */
525
- interface WebhookRequeueEvidence {
526
- /** `fan_out_job_actor`: an actor enrichment (PR 9), which also requeues the job. */
527
- readonly target: "fan_out_job" | "delivery_intent" | "fan_out_job_actor";
528
- readonly eventId: string;
529
- /** Present when `target` is `delivery_intent`. */
530
- readonly deliveryId?: string;
531
- /**
532
- * Present when `target` is `delivery_intent`: the intent's subscription,
533
- * so a re-run of the requeue can report it after the intent is purged.
534
- * Evidence written before this field existed lacks it.
535
- */
536
- readonly subscriptionId?: string;
537
- readonly tenantId: string;
538
- readonly spaceId: string;
539
- readonly requestedBy: string;
540
- readonly requestedAt: string;
541
- readonly actionInvocationId?: string;
542
- /** The store's clock when the requeue was applied. */
543
- readonly appliedAt: string;
544
- readonly priorAttemptCount: number;
545
- readonly priorAvailableAt: string;
546
- readonly priorProcessingDeadline: string;
547
- /** The record's fence when it was requeued (the request's `expectedFence`); the requeue advances it by one. */
548
- readonly priorFence: number;
549
- readonly priorLastDiagnostic?: WebhookDiagnosticCode;
550
- readonly priorLastOutcome?: {
551
- readonly outcome: WebhookAttemptOutcome;
552
- readonly reason: WebhookAttemptReasonCode;
553
- };
554
- readonly processingDeadline: string;
555
- /** Present when `target` is `fan_out_job_actor`: the actor the enrichment set. The prior actor was `legacy`. */
556
- readonly actor?: {
557
- readonly actorId: string;
558
- readonly actorType: string;
559
- };
560
- }
561
- /** A delivery configuration as a writer supplies it; the store assigns `version`. */
562
- interface WebhookDeliveryConfigurationInput {
563
- readonly destinationUrl: string;
564
- readonly signatureScheme: WebhookSignatureScheme;
565
- /** A reference the signer resolves within the subscription's tenant, never key material (decision 8). */
566
- readonly secretRef: string;
567
- }
568
- /** The v1 wire body (decision 7). Field order is the serialized order. */
569
- interface WebhookDeliveryBodyV1 {
570
- readonly format: "fabric.webhook-delivery.v1";
571
- readonly eventId: string;
572
- readonly type: string;
573
- readonly tenantId: string;
574
- readonly spaceId: string;
575
- readonly correlationId: string;
576
- readonly occurredAt: string;
577
- readonly event: {
578
- readonly type: string;
579
- readonly schemaVersion: number;
580
- readonly subjectType: string;
581
- readonly subjectId: string;
582
- readonly sequence: number;
583
- readonly recordedAt: string;
584
- readonly actionInvocationId?: string;
585
- readonly consistency?: "provisional-until-reconciled";
586
- readonly payload: WebhookJsonObject | null;
587
- readonly payloadProjected: boolean;
588
- };
589
- readonly delivery: {
590
- readonly deliveryId: string;
591
- readonly subscriptionId: string;
592
- };
593
- }
594
-
595
- /**
596
- * Port interfaces (ADR 0018 decision 6). An application passes
597
- * implementations to an instance built at its composition root; this package
598
- * keeps no module-scope registry of them (ADR 0008). Every awaited call takes
599
- * an absolute `deadline` and an `AbortSignal` so a caller can bound it (R4).
600
- *
601
- * Ports are trusted application code, like the fan-out sinks (package README,
602
- * "Trust model"). An async call returns an ordinary promise or thenable. A
603
- * promise whose `constructor` access throws is outside this contract: no
604
- * JavaScript code can attach a handler to it (`then`, `await` and
605
- * `Promise.resolve` all read `constructor` first), so a rejection it carries
606
- * can surface as unhandled.
607
- */
608
-
609
- /** Bounds every awaited port call. */
610
- interface WebhookCallOptions {
611
- /** The absolute time after which the call's result is ignored. */
612
- readonly deadline: Date;
613
- /** Aborted when the caller stops waiting; implementations should stop work. */
614
- readonly signal: AbortSignal;
615
- }
616
- /** Tenant and space scope every store read is confined to (decision 9). */
617
- interface WebhookScope {
618
- readonly tenantId: string;
619
- readonly spaceId: string;
620
- }
621
- /**
622
- * Tenant- and space-scoped reads of eligibility revisions and delivery
623
- * configuration (decision 3). The fan-out and delivery cycles depend on this
624
- * read port only; writes are on {@link WebhookSubscriptionWriter}.
625
- */
626
- interface WebhookSubscriptionStore {
627
- /**
628
- * Eligibility revisions in the scope whose half-open interval contains
629
- * `recordedAt`, at most one per subscription, in `subscriptionId` order.
630
- * Paused and deleted revisions are returned with their status; the caller
631
- * decides eligibility.
632
- */
633
- listEligibilityRevisions(input: WebhookScope & {
634
- readonly recordedAt: string;
635
- }, options: WebhookCallOptions): Promise<readonly WebhookEligibilityRevision[]>;
636
- /**
637
- * The subscription's current status, the revision that status began at
638
- * (`statusSinceRevision`, read in the same atomic step as the status) and
639
- * its current delivery configuration, or `null` when it does not exist in
640
- * this scope. The dispatch check calls it as close to transport handoff
641
- * as it can.
642
- */
643
- readSubscription(input: WebhookScope & {
644
- readonly subscriptionId: string;
645
- }, options: WebhookCallOptions): Promise<WebhookSubscriptionSnapshot | null>;
646
- /**
647
- * Every eligibility revision of one subscription, oldest first, or an
648
- * empty list when it does not exist in this scope. Revisions are never
649
- * removed (R5: eligibility history outlives the replay horizon).
650
- */
651
- listEligibilityHistory(input: WebhookScope & {
652
- readonly subscriptionId: string;
653
- }, options: WebhookCallOptions): Promise<readonly WebhookEligibilityRevision[]>;
654
- }
655
- /**
656
- * Why a subscription write was refused. `not_found`: no such subscription in
657
- * this scope. `already_exists`: create named an existing one. `deleted`: the
658
- * subscription is deleted, which is terminal. `invalid_transition`: pause of
659
- * a paused subscription, or resume of an active one. `revision_conflict` /
660
- * `version_conflict`: the caller's expected revision or configuration
661
- * version is not the current one.
662
- */
663
- type WebhookSubscriptionWriteRefusal = "not_found" | "already_exists" | "deleted" | "invalid_transition" | "revision_conflict";
664
- /** The result of a subscription write that appends an eligibility revision. */
665
- type WebhookSubscriptionWriteResult = {
666
- readonly applied: true;
667
- readonly revision: WebhookEligibilityRevision;
668
- readonly configuration: WebhookDeliveryConfiguration;
669
- } | {
670
- readonly applied: false;
671
- readonly refusal: WebhookSubscriptionWriteRefusal;
672
- readonly currentRevision?: number;
673
- };
674
- /** The result of a delivery configuration replacement. */
675
- type WebhookConfigurationWriteResult = {
676
- readonly applied: true;
677
- readonly configuration: WebhookDeliveryConfiguration;
678
- } | {
679
- readonly applied: false;
680
- readonly refusal: "not_found" | "deleted" | "version_conflict";
681
- readonly currentVersion?: number;
682
- };
683
- /** Names one subscription for a transition, optionally guarded by its current revision. */
684
- type WebhookSubscriptionTransitionInput = WebhookScope & {
685
- readonly subscriptionId: string;
686
- /** When set, the write is refused as `revision_conflict` unless this is the current revision. */
687
- readonly expectedRevision?: number;
688
- };
689
- /**
690
- * Subscription writes (decision 3), for the governed actions (PR 9). Create,
691
- * pattern change, pause, resume and delete each append one immutable
692
- * eligibility revision, effective from the store's clock; configuration
693
- * replacement appends one delivery configuration version. The store checks
694
- * each transition against its own current state, not the caller's.
695
- *
696
- * Pause and delete do not touch the delivery store; the two stores need not
697
- * share a database, so nothing here is atomic with intent state. The
698
- * governed action calls {@link WebhookDeliveryStore.cancelPendingIntents}
699
- * with the same scope and `subscriptionId` after the revision is written,
700
- * passing that revision's number as `beforeRevision`, so a cancel delayed
701
- * past a later resume cannot cancel intents bound to the resumed revision;
702
- * resume makes the same call with its own revision. These calls are
703
- * cleanups. What stops delivery, whether they failed, timed out or raced a
704
- * late fan-out insert, is the delivery cycle's pre-handoff
705
- * `readSubscription` check: it cancels the intent unsent while the
706
- * subscription is paused or deleted, and while it is active with a
707
- * {@link WebhookSubscriptionSnapshot.statusSinceRevision} above the
708
- * intent's `eligibilityRevision`, i.e. resumed after a pause that ended the
709
- * intent's interval (decision 3, R2; README, "Pause and resume are enforced
710
- * at dispatch"). So resume never resurrects an intent of an interval a
711
- * pause ended: a cancelled intent is terminal, and one still pending, or
712
- * requeued from dead letter, is cancelled at dispatch.
713
- */
714
- interface WebhookSubscriptionWriter {
715
- /** Create an active subscription with revision 1 and configuration version 1. */
716
- createSubscription(input: WebhookScope & {
717
- readonly subscriptionId: string;
718
- readonly eventTypePatterns: readonly string[];
719
- readonly configuration: WebhookDeliveryConfigurationInput;
720
- }, options: WebhookCallOptions): Promise<WebhookSubscriptionWriteResult>;
721
- /** Append a revision with new patterns and the current status. Refused once deleted. */
722
- changeEventTypePatterns(input: WebhookSubscriptionTransitionInput & {
723
- readonly eventTypePatterns: readonly string[];
724
- }, options: WebhookCallOptions): Promise<WebhookSubscriptionWriteResult>;
725
- /** Active to paused. See the interface note for cancelling pending intents. */
726
- pauseSubscription(input: WebhookSubscriptionTransitionInput, options: WebhookCallOptions): Promise<WebhookSubscriptionWriteResult>;
727
- /** Paused to active. Cancelled intents stay cancelled, and intents bound to revisions before the resume are not delivered (see the interface note). */
728
- resumeSubscription(input: WebhookSubscriptionTransitionInput, options: WebhookCallOptions): Promise<WebhookSubscriptionWriteResult>;
729
- /** Active or paused to deleted, which is terminal. History is kept. */
730
- deleteSubscription(input: WebhookSubscriptionTransitionInput, options: WebhookCallOptions): Promise<WebhookSubscriptionWriteResult>;
731
- /** Append a delivery configuration version. Refused once deleted. */
732
- replaceDeliveryConfiguration(input: WebhookScope & {
733
- readonly subscriptionId: string;
734
- readonly configuration: WebhookDeliveryConfigurationInput;
735
- /** When set, the write is refused as `version_conflict` unless this is the current version. */
736
- readonly expectedVersion?: number;
737
- }, options: WebhookCallOptions): Promise<WebhookConfigurationWriteResult>;
738
- }
739
- /** A claim request for either queue. */
740
- interface WebhookClaimRequest {
741
- readonly workerId: string;
742
- readonly leaseDurationMs: number;
743
- readonly limit: number;
744
- readonly now: Date;
745
- }
746
- /** Identifies a leased record; a write whose fence is stale is refused. */
747
- interface WebhookFencedWrite {
748
- readonly owner: string;
749
- readonly fence: number;
750
- readonly now: Date;
751
- }
752
- /**
753
- * The result of an insert-if-absent. `already_present`: a job for the
754
- * `eventId` exists. `past_horizon`: the event's `recordedAt` is older than
755
- * the replay horizon (R5), so nothing was written, whether or not a job for
756
- * it was ever stored or has been purged.
757
- */
758
- interface WebhookInsertResult {
759
- readonly outcome: "inserted" | "already_present" | "past_horizon";
760
- }
761
- /**
762
- * The fan-out job lease an intent or denial insert is made under. The job is
763
- * keyed by `eventId`; `fence` is the job's lease token, which a reclaim and
764
- * actor enrichment both advance.
765
- */
766
- interface WebhookFanOutJobFence {
767
- readonly eventId: string;
768
- readonly owner: string;
769
- readonly fence: number;
770
- }
771
- /**
772
- * The result of an insert made under a fan-out job's lease. `lease_lost`:
773
- * the job's current owner or fence no longer matches, and nothing was
774
- * written. `past_horizon`: the event is older than the replay horizon (R5),
775
- * and nothing was written. `already_present`: an intent, denial or tombstone
776
- * already exists for (subscriptionId, eventId). Checked in that order.
777
- */
778
- interface WebhookFencedInsertResult {
779
- readonly outcome: "inserted" | "already_present" | "lease_lost" | "past_horizon";
780
- }
781
- /**
782
- * The result of a compare-and-set requeue, checked in this order.
783
- * `not_found`: no such record in this scope. `stale`: the record's fence is
784
- * no longer the request's `expectedFence`, because it was claimed or
785
- * requeued since the operator read it (including when it has since been
786
- * dead-lettered again). `not_dead_letter`: the record is not dead-lettered
787
- * (a pending, completed, delivered or cancelled record is never requeued).
788
- * `past_horizon`: the event is older than the replay horizon (decision 10).
789
- */
790
- type WebhookRequeueResult = {
791
- readonly applied: true;
792
- readonly evidence: WebhookRequeueEvidence;
793
- /** Present when the request's `actionInvocationId` had already applied it: `evidence` is that application's, and nothing changed now. */
794
- readonly replayed?: true;
795
- } | {
796
- readonly applied: false;
797
- readonly refusal: "not_found" | "stale" | "not_dead_letter" | "past_horizon";
798
- };
799
- /**
800
- * The result of an actor enrichment (decision 5), checked in this order.
801
- * `not_found`: no such job in this scope. `stale`: the job's fence is no
802
- * longer the request's `expectedFence`. `not_enrichable`: the job is
803
- * `completed`, which is final (README, "Completeness boundary").
804
- * `actor_present`: the job already carries an actor. `actor_suppressed`:
805
- * Host's `includeActor` hook withheld the actor, which enrichment never
806
- * overrides. `past_horizon`: the event is older than the replay horizon.
807
- */
808
- type WebhookActorEnrichmentResult = {
809
- readonly applied: true;
810
- readonly evidence: WebhookRequeueEvidence;
811
- /** As on {@link WebhookRequeueResult}. */
812
- readonly replayed?: true;
813
- } | {
814
- readonly applied: false;
815
- readonly refusal: "not_found" | "stale" | "not_enrichable" | "actor_present" | "actor_suppressed" | "past_horizon";
816
- };
817
- /**
818
- * What a purge removed. `cutoff` is the store's clock minus the horizon;
819
- * only records whose event `recordedAt` is earlier were removed. `more` is
820
- * `true` when the limit stopped the purge before every such record went.
821
- */
822
- interface WebhookPurgeResult {
823
- readonly cutoff: string;
824
- readonly removed: {
825
- readonly fanOutJobs: number;
826
- readonly deliveryIntents: number;
827
- readonly admissionDenials: number;
828
- };
829
- readonly more: boolean;
830
- }
831
- /**
832
- * What `beginAttempt` answered. `receipt` is the only proof that the caller
833
- * began the attempt; it is needed to record the attempt's outcome.
834
- */
835
- type WebhookAttemptBeginResult = {
836
- readonly begun: true;
837
- readonly receipt: string;
838
- } | {
839
- readonly begun: false;
840
- };
841
- /**
842
- * What a pre-handoff lease check found. `valid` only when the intent is
843
- * still `pending`, leased to this owner at this fence, in this scope, and
844
- * the lease has not expired at `now`.
845
- */
846
- type WebhookDeliveryLeaseCheck = {
847
- readonly valid: true;
848
- } | {
849
- readonly valid: false;
850
- readonly reason: "lease_lost" | "intent_not_pending" | "not_found";
851
- };
852
- /**
853
- * Durable state for both stages (decisions 2, 3 and 10): fan-out jobs keyed
854
- * by `eventId`, intents keyed by (subscriptionId, eventId), fenced leases,
855
- * retry scheduling, dead letter, compare-and-set requeue, an append-only
856
- * attempt log and tombstones. Fenced writes (job completion, reschedule,
857
- * dead letter and lease renewal; intent and denial inserts under a parent
858
- * job's fence; `appendAttempt`, `beginAttempt`, `settleDeliveryIntent` and
859
- * `renewDeliveryIntentLease`) resolve
860
- * `false`, or `lease_lost` for an insert, without changing anything, when
861
- * the caller no longer holds the lease: the record is not `pending`, its
862
- * lease owner or fence differs, or its lease has expired. Expiry is judged
863
- * on the store's own clock, read inside the atomic step (after every lock
864
- * is held, in PostgreSQL), never on the caller's `now`, so a write delayed
865
- * past its lease is refused even before another worker reclaims the record
866
- * (contract case "every fenced write decides lease expiry on the store's
867
- * clock …"; changed in PR 8, before which the fence alone decided).
868
- * `finishAttempt` is not fenced. Memory and PostgreSQL implementations
869
- * share one contract suite (`tests/store-contract.ts`).
870
- *
871
- * **Replay horizon (R5).** The store is built with a horizon `H` and reads
872
- * its own clock. Every record keyed by an event is anchored at that event's
873
- * `recordedAt`. Let `floor` be the later of `now - H` and the highest
874
- * `cutoff` any purge has used. A fan-out job, intent or denial insert, a
875
- * job or intent requeue, and an actor enrichment (PR 9), is refused as
876
- * `past_horizon` when its anchor is
877
- * earlier than `floor`; {@link WebhookDeliveryStore.purgeExpiredRecords}
878
- * removes only terminal records whose anchor is earlier than its `cutoff`.
879
- * The comparison and the write are one atomic step (one transaction, or one
880
- * synchronous step in memory), so a purge and a replay of the same key
881
- * cannot interleave between them. Whichever runs first, a replay of a
882
- * purged key is refused and a record inside the horizon is never removed.
883
- * The floor can fall when the store's clock steps backwards, but never below
884
- * the highest cutoff a purge has used, which is all a purged key needs: it
885
- * stays refused even if the clock steps backwards after the purge. The
886
- * race and floor cases in `tests/store-contract.ts` check the outcome of
887
- * each whole-operation order, forced through the `beforeAtomicStep` seam;
888
- * they cannot interpose inside the atomic step, so each implementation
889
- * proves its check-to-write atomicity separately (README, "Contract suite").
890
- */
891
- interface WebhookDeliveryStore {
892
- /**
893
- * Insert a pending job, available at `createdAt`, unless one exists for
894
- * `eventId` or the event is past the replay horizon.
895
- */
896
- insertFanOutJob(job: WebhookFanOutJobDraft & {
897
- readonly processingDeadline: string;
898
- readonly createdAt: string;
899
- }, options: WebhookCallOptions): Promise<WebhookInsertResult>;
900
- /**
901
- * Lease up to `limit` pending jobs whose `availableAt` has passed and that
902
- * are unleased or whose lease has expired, oldest `availableAt` first.
903
- * Each claim advances the job's fence. Claiming spends no attempt.
904
- */
905
- claimFanOutJobs(request: WebhookClaimRequest, options: WebhookCallOptions): Promise<readonly WebhookFanOutJob[]>;
906
- /** Mark a job complete once each eligible revision has an intent or recorded denial. */
907
- completeFanOutJob(input: WebhookFencedWrite & {
908
- readonly eventId: string;
909
- }, options: WebhookCallOptions): Promise<boolean>;
910
- /** Return a job to pending at `availableAt`, spending one attempt. */
911
- rescheduleFanOutJob(input: WebhookFencedWrite & {
912
- readonly eventId: string;
913
- readonly availableAt: Date;
914
- readonly diagnostic: WebhookDiagnosticCode;
915
- }, options: WebhookCallOptions): Promise<boolean>;
916
- /**
917
- * Extend the lease the caller holds on a job, so a worker that is still
918
- * making progress keeps it (PR 8). Fenced like the other job writes:
919
- * resolves `true` only when the job is `pending`, leased to `owner` at
920
- * `fence`, and the lease has not expired on the store's clock read after
921
- * its locks; otherwise `false`, writing nothing. On `true` the lease's
922
- * expiry becomes the later of its current expiry and the earlier of
923
- * `now + leaseDurationMs` and the job's processing deadline: a renewal
924
- * never shortens a lease and never extends it past the processing
925
- * deadline (contract case "a lease renewal extends only a lease the
926
- * caller holds …"). The fence, attempt count and availability are
927
- * unchanged.
928
- */
929
- renewFanOutJobLease(input: WebhookFencedWrite & {
930
- readonly eventId: string;
931
- readonly leaseDurationMs: number;
932
- }, options: WebhookCallOptions): Promise<boolean>;
933
- /** Move an exhausted job to dead letter. */
934
- deadLetterFanOutJob(input: WebhookFencedWrite & {
935
- readonly eventId: string;
936
- readonly diagnostic: WebhookDiagnosticCode;
937
- }, options: WebhookCallOptions): Promise<boolean>;
938
- /**
939
- * Compare-and-set a dead-lettered job back to pending with a restored
940
- * budget and a new processing deadline, appending requeue evidence in the
941
- * same step. The compare is on the fence (`request.expectedFence`) as
942
- * well as the status, and the requeue advances the fence, so one read
943
- * yields at most one applied requeue. The fence check, the status and
944
- * horizon checks, the transition and the evidence are one atomic step.
945
- * Refused past the replay horizon.
946
- *
947
- * **Re-runs of one invocation.** When the request carries an
948
- * `actionInvocationId`, the atomic step first looks for evidence this
949
- * scope holds under that id for the same target and record. If there is
950
- * some, the call answers `{ applied: true, replayed: true, evidence }`
951
- * with that evidence and changes nothing, before any other check: the
952
- * record may since have moved on, been claimed or been purged. So
953
- * requests of one governed invocation that reach this step, overlapping
954
- * or one after the other, apply once and each reports that application,
955
- * while a request under another id is checked as usual. An attempt that
956
- * a check before this step refused never reaches it (README, "Atomicity
957
- * and re-runs"). Contract cases "a request repeated
958
- * under its actionInvocationId …" (three).
959
- */
960
- requeueFanOutJob(input: WebhookScope & {
961
- readonly eventId: string;
962
- readonly request: WebhookRequeueRequest;
963
- }, options: WebhookCallOptions): Promise<WebhookRequeueResult>;
964
- /**
965
- * Repair a `legacy` job's actor (decision 5, PR 9): set the actor the
966
- * application read from the canonical event, return the job to pending
967
- * with a restored budget and a new processing deadline, advance its fence
968
- * and clear its lease, and append evidence with `target:
969
- * "fan_out_job_actor"`. The job may be `pending` (leased or not) or
970
- * `dead_letter`; a `completed` job is refused. Advancing the fence is what
971
- * stops a worker that leased the job before the repair: its later fenced
972
- * writes (intent and denial inserts, completion, reschedule, dead letter,
973
- * renewal) answer `lease_lost` or `false`. The scope, fence, status,
974
- * actor-state and horizon checks, the transition and the evidence are one
975
- * atomic step, so a `suppressed` or `present` actor is refused by the store
976
- * itself, whatever the caller read before. A request of an invocation that
977
- * already applied is answered as for {@link WebhookDeliveryStore.requeueFanOutJob}
978
- * ("Re-runs of one invocation").
979
- */
980
- enrichFanOutJobActor(input: WebhookScope & {
981
- readonly eventId: string;
982
- readonly request: WebhookActorEnrichmentRequest;
983
- }, options: WebhookCallOptions): Promise<WebhookActorEnrichmentResult>;
984
- /** The job for `eventId` in this scope, or `null`. */
985
- readFanOutJob(input: WebhookScope & {
986
- readonly eventId: string;
987
- }, options: WebhookCallOptions): Promise<WebhookFanOutJob | null>;
988
- /**
989
- * Insert an intent unless an intent, denial or tombstone exists for
990
- * (subscriptionId, eventId), on behalf of the fan-out job `parent`. The
991
- * store copies the parent job's event `recordedAt` onto the intent.
992
- *
993
- * Atomicity requirement: the check that `parent.eventId`'s job is still
994
- * leased to `parent.owner` at `parent.fence`, the replay-horizon check,
995
- * the uniqueness check and the insert are one atomic step (one
996
- * transaction, or one conditional write). A worker whose lease was
997
- * reclaimed, or whose job fence actor enrichment advanced, gets
998
- * `lease_lost` and writes nothing, even if it lost the lease between its
999
- * last read and this call. The intent's `eventId`, `tenantId` and
1000
- * `spaceId` must match the job's (`record_mismatch` otherwise), and its
1001
- * `deliveryId` must not belong to another pair (`delivery_id_conflict`).
1002
- */
1003
- insertDeliveryIntent(intent: Omit<WebhookDeliveryIntent, "status" | "attemptCount" | "lastOutcome" | "lastDiagnostic" | "lease" | "eventRecordedAt" | "fence">, parent: WebhookFanOutJobFence, options: WebhookCallOptions): Promise<WebhookFencedInsertResult>;
1004
- /**
1005
- * Record an admission denial unless an intent, denial or tombstone exists
1006
- * for (subscriptionId, eventId), on behalf of the fan-out job `parent`.
1007
- * Same atomicity requirement as {@link WebhookDeliveryStore.insertDeliveryIntent}:
1008
- * the parent fence check, the horizon check and the insert are one atomic step.
1009
- */
1010
- recordAdmissionDenial(record: WebhookAdmissionDenialRecord, parent: WebhookFanOutJobFence, options: WebhookCallOptions): Promise<WebhookFencedInsertResult>;
1011
- /**
1012
- * Lease up to `limit` pending intents whose `availableAt` has passed and
1013
- * that are unleased or whose lease has expired, oldest `availableAt` first.
1014
- * Each claim advances the intent's fence. Claiming spends no attempt.
1015
- */
1016
- claimDeliveryIntents(request: WebhookClaimRequest, options: WebhookCallOptions): Promise<readonly WebhookDeliveryIntent[]>;
1017
- /**
1018
- * Extend the lease the caller holds on an intent (PR 8), with the rule
1019
- * of {@link WebhookDeliveryStore.renewFanOutJobLease}: fenced, decided
1020
- * on the store's clock after its locks, never shortening the lease and
1021
- * never extending it past the intent's processing deadline. The fence,
1022
- * attempt count and availability are unchanged.
1023
- */
1024
- renewDeliveryIntentLease(input: WebhookFencedWrite & {
1025
- readonly deliveryId: string;
1026
- readonly leaseDurationMs: number;
1027
- }, options: WebhookCallOptions): Promise<boolean>;
1028
- /**
1029
- * Read-only check that the intent is still `pending` and leased to
1030
- * `owner` at `fence` in this tenant and space, with a lease that has not
1031
- * expired at `now`, so a caller learns that its intent was cancelled,
1032
- * settled or re-leased even when the subscription itself is active
1033
- * again. The delivery cycle does not call it: its last check before the
1034
- * handoff is {@link WebhookDeliveryStore.beginAttempt}, which makes the
1035
- * same check and records the attempt in one atomic step.
1036
- *
1037
- * Requirements: it changes nothing (no settlement, no lease renewal); it
1038
- * reads the committed current row, not a cached or replica copy, so it
1039
- * observes every settle, cancel, requeue or reclaim that committed before
1040
- * it ran; it is bounded by `options`. A change committed after it returns
1041
- * may still race with the handoff (R2).
1042
- */
1043
- checkDeliveryLease(input: WebhookScope & {
1044
- readonly deliveryId: string;
1045
- readonly owner: string;
1046
- readonly fence: number;
1047
- readonly now: Date;
1048
- }, options: WebhookCallOptions): Promise<WebhookDeliveryLeaseCheck>;
1049
- /**
1050
- * Append one complete attempt to the log, fenced by the intent's lease.
1051
- * The delivery cycle uses it for attempts that end before the transport
1052
- * is reached (a signer failure, a refused destination). Never updates or
1053
- * removes an earlier entry; a purge keeps the log. The attempt's
1054
- * `subscriptionId` and `eventId` must match the intent's.
1055
- */
1056
- appendAttempt(input: WebhookFencedWrite & {
1057
- readonly attempt: WebhookDeliveryAttemptRecord;
1058
- }, options: WebhookCallOptions): Promise<boolean>;
1059
- /**
1060
- * Record, as one atomic step, that the lease holder is about to hand
1061
- * `attempt` to the transport: the delivery cycle's last check before the
1062
- * irrevocable point (R2). Resolves `{ begun: true, receipt }` and appends
1063
- * an in-flight entry keyed by (`deliveryId`, `fence`) only when the
1064
- * intent is `pending`, leased to `owner` at `fence`, its lease has not
1065
- * expired on the store's clock, and no attempt has been begun under this
1066
- * fence yet. Otherwise it resolves `{ begun: false }` and writes nothing.
1067
- * It does not change the intent. The attempt's `subscriptionId` and
1068
- * `eventId` must match the intent's (`record_mismatch` otherwise).
1069
- *
1070
- * `receipt` is an unguessable token (32 random bytes) created for this
1071
- * attempt and returned only here. The store keeps its SHA-256 digest,
1072
- * never the token, and no read returns either; only the worker that
1073
- * began the attempt can record its outcome ({@link WebhookDeliveryStore.finishAttempt}).
1074
- * The delivery cycle keeps it in a local variable only.
1075
- *
1076
- * The delivery cycle calls it before every handoff and hands off only on
1077
- * a well-formed `begun: true` answer (src/delivery-cycle.ts), so every
1078
- * request it hands to the transport has an attempt entry, even when its
1079
- * worker loses the lease before it can settle the intent (README, "Delivery").
1080
- */
1081
- beginAttempt(input: WebhookFencedWrite & {
1082
- readonly attempt: WebhookDeliveryAttemptStart;
1083
- }, options: WebhookCallOptions): Promise<WebhookAttemptBeginResult>;
1084
- /**
1085
- * Append the outcome of the attempt begun under (`deliveryId`, `fence`)
1086
- * in this scope, presenting the `receipt` its `beginAttempt` returned.
1087
- * Not fenced: a worker whose lease was reclaimed after the handoff can
1088
- * still record what its request observed. Bound to the receipt instead:
1089
- * a worker that did not begin that attempt cannot record its outcome,
1090
- * whether or not it holds the lease now. It reads and changes nothing of
1091
- * the intent, so no worker can move the intent's state through it
1092
- * (contract cases "a worker reclaimed after its handoff still finishes
1093
- * its attempt …" and "only the worker that began an attempt can record
1094
- * its outcome …"). Resolves `true` when it appended the outcome; `false`,
1095
- * writing nothing, when no attempt was begun under that key in this
1096
- * scope, the receipt does not match, or its outcome is already recorded
1097
- * (the first outcome stands).
1098
- */
1099
- finishAttempt(input: WebhookScope & {
1100
- readonly deliveryId: string;
1101
- readonly fence: number;
1102
- readonly receipt: string;
1103
- readonly finish: WebhookDeliveryAttemptFinish;
1104
- }, options: WebhookCallOptions): Promise<boolean>;
1105
- /**
1106
- * Settle an intent after an attempt or a refused check, clearing its
1107
- * lease. `release` returns it to pending unsent without spending budget;
1108
- * `retry` spends one attempt; `delivered`, `dead_letter` and `cancelled`
1109
- * are terminal and leave the attempt count as it was.
1110
- */
1111
- settleDeliveryIntent(input: WebhookFencedWrite & {
1112
- readonly deliveryId: string;
1113
- readonly settlement: {
1114
- readonly kind: "delivered";
1115
- } | {
1116
- readonly kind: "retry";
1117
- readonly availableAt: Date;
1118
- readonly outcome: WebhookAttemptOutcome;
1119
- readonly reason: WebhookAttemptReasonCode;
1120
- } | {
1121
- readonly kind: "release";
1122
- readonly availableAt: Date;
1123
- readonly diagnostic: WebhookDiagnosticCode;
1124
- } | {
1125
- readonly kind: "dead_letter";
1126
- readonly reason: WebhookAttemptReasonCode | WebhookDiagnosticCode;
1127
- } | {
1128
- readonly kind: "cancelled";
1129
- readonly reason: WebhookAttemptReasonCode;
1130
- };
1131
- }, options: WebhookCallOptions): Promise<boolean>;
1132
- /**
1133
- * Cancel the pending intents of a subscription in this scope, leased or
1134
- * not, that are bound to an eligibility revision earlier than
1135
- * `beforeRevision`, as a pause or delete does. Returns the count. Pass the
1136
- * revision the pause or delete wrote: then only intents for events the
1137
- * pause ended are cancelled, and an intent bound to a later revision (one
1138
- * written after a resume) is left alone even when this call is delayed
1139
- * past the resume. A cancelled intent is terminal; its lease holder's
1140
- * later fenced writes and lease checks fail. The governed actions call
1141
- * it as a cleanup; dispatch does not depend on it (see
1142
- * {@link WebhookSubscriptionWriter}).
1143
- */
1144
- cancelPendingIntents(input: WebhookScope & {
1145
- readonly subscriptionId: string;
1146
- readonly reason: "subscription_paused" | "subscription_deleted";
1147
- /** Only intents whose `eligibilityRevision` is lower than this are cancelled. */
1148
- readonly beforeRevision: number;
1149
- readonly now: Date;
1150
- }, options: WebhookCallOptions): Promise<number>;
1151
- /**
1152
- * Compare-and-set a dead-lettered intent back to pending, with the same
1153
- * fence compare-and-set, atomicity and re-run recognition as
1154
- * {@link WebhookDeliveryStore.requeueFanOutJob}; the evidence names the
1155
- * intent's `subscriptionId`. Refused past the replay horizon.
1156
- */
1157
- requeueDeliveryIntent(input: WebhookScope & {
1158
- readonly deliveryId: string;
1159
- readonly request: WebhookRequeueRequest;
1160
- }, options: WebhookCallOptions): Promise<WebhookRequeueResult>;
1161
- /** The intent in this scope, or `null`. */
1162
- readDeliveryIntent(input: WebhookScope & {
1163
- readonly deliveryId: string;
1164
- }, options: WebhookCallOptions): Promise<WebhookDeliveryIntent | null>;
1165
- /**
1166
- * The attempt log of one intent in this scope, in the order each entry
1167
- * was first written: entries appended whole, begun attempts with their
1168
- * outcome once one is recorded, and begun attempts still `in_flight`.
1169
- */
1170
- listDeliveryAttempts(input: WebhookScope & {
1171
- readonly deliveryId: string;
1172
- }, options: WebhookCallOptions): Promise<readonly WebhookDeliveryAttemptLogEntry[]>;
1173
- /** Requeue evidence for one event's job and intents in this scope, in append order. */
1174
- listRequeueEvidence(input: WebhookScope & {
1175
- readonly eventId: string;
1176
- }, options: WebhookCallOptions): Promise<readonly WebhookRequeueEvidence[]>;
1177
- /**
1178
- * Requeue and enrichment evidence appended under one governed
1179
- * `actionInvocationId` in this scope, in append order, whether or not the
1180
- * records it names still exist. The governed actions read it before they
1181
- * read the record, so a re-run reports what its invocation applied.
1182
- */
1183
- listInvocationRequeueEvidence(input: WebhookScope & {
1184
- readonly actionInvocationId: string;
1185
- }, options: WebhookCallOptions): Promise<readonly WebhookRequeueEvidence[]>;
1186
- /** Read the tombstone for (subscriptionId, eventId), if one is retained. */
1187
- readTombstone(input: WebhookScope & {
1188
- readonly subscriptionId: string;
1189
- readonly eventId: string;
1190
- }, options: WebhookCallOptions): Promise<WebhookTombstone | null>;
1191
- /**
1192
- * Remove up to `limit` terminal records whose event is older than the
1193
- * replay horizon at the store's clock: completed and dead-lettered jobs,
1194
- * delivered, dead-lettered and cancelled intents, and denials, each with
1195
- * its tombstone. Pending records are never removed, so live work is not
1196
- * lost; the cycles are to move them to a terminal state through their
1197
- * processing deadlines (R4). The attempt log and requeue evidence are audit records
1198
- * and are kept. The purge's cutoff raises the store's horizon floor in
1199
- * the same step, so no later insert or requeue can recreate what it
1200
- * removed. Oldest anchor first.
1201
- */
1202
- purgeExpiredRecords(input: {
1203
- readonly limit: number;
1204
- }, options: WebhookCallOptions): Promise<WebhookPurgeResult>;
1205
- }
1206
- /** What a signer signs: the timestamp and the exact body bytes (decision 7). */
1207
- interface WebhookSignRequest {
1208
- readonly scheme: WebhookSignatureScheme;
1209
- /** Resolved by the signer within `tenantId`; the package never holds key material. */
1210
- readonly secretRef: string;
1211
- readonly tenantId: string;
1212
- readonly spaceId: string;
1213
- readonly timestampSeconds: number;
1214
- readonly rawBody: string;
1215
- }
1216
- /**
1217
- * Signs one attempt at send time. For `v1` it returns the `X-Fabric-Signature`
1218
- * value `v1=<hex hmac-sha256(secret, "<timestampSeconds>.<rawBody>")>`. A
1219
- * throw is classified `signer_unavailable` and retried.
1220
- */
1221
- interface WebhookSigner {
1222
- sign(request: WebhookSignRequest, options: WebhookCallOptions): Promise<string>;
1223
- }
1224
- /** One resolved address of a destination host. */
1225
- interface WebhookResolvedAddress {
1226
- readonly address: string;
1227
- readonly family: 4 | 6;
1228
- }
1229
- /** A destination that passed the URL rules and whose every resolved answer is public. */
1230
- interface WebhookVettedDestination {
1231
- readonly url: string;
1232
- /** Kept for SNI and certificate hostname verification. */
1233
- readonly hostname: string;
1234
- readonly port: 443;
1235
- /** The vetted addresses the transport posts to without re-resolving. */
1236
- readonly addresses: readonly WebhookResolvedAddress[];
1237
- }
1238
- /** The outcome of resolving a destination. Rejections carry a fixed code, never the URL's parts. */
1239
- type WebhookDestinationResolution = {
1240
- readonly ok: true;
1241
- readonly destination: WebhookVettedDestination;
1242
- } | {
1243
- readonly ok: false;
1244
- readonly code: "invalid_url" | "private_address" | "dns_failure";
1245
- };
1246
- /**
1247
- * Validates a destination (`https:`, port 443 only, no userinfo) and resolves
1248
- * it, requiring every answer to be public. Called at configuration change,
1249
- * requeue and dispatch.
1250
- */
1251
- interface WebhookDestinationResolver {
1252
- resolve(url: string, options: WebhookCallOptions): Promise<WebhookDestinationResolution>;
1253
- }
1254
- /** One HTTP request handed to the transport. */
1255
- interface WebhookTransportRequest {
1256
- readonly destination: WebhookVettedDestination;
1257
- readonly method: "POST";
1258
- readonly headers: Readonly<Record<string, string>>;
1259
- readonly body: string;
1260
- }
1261
- /**
1262
- * What the transport observed. `not_sent` means the request never started
1263
- * (deadline already passed, or the signal aborted while queued) and the
1264
- * dispatcher releases the intent without spending budget. `timeout` covers a
1265
- * deadline or abort after the request started. Response bodies are not
1266
- * returned.
1267
- */
1268
- type WebhookTransportResult = {
1269
- readonly kind: "response";
1270
- readonly status: number;
1271
- readonly retryAfter?: string | null;
1272
- } | {
1273
- readonly kind: "network_error" | "timeout" | "dns_failure" | "invalid_url" | "private_address";
1274
- } | {
1275
- readonly kind: "not_sent";
1276
- readonly reason: "deadline_expired" | "aborted";
1277
- };
1278
- /**
1279
- * Posts to the vetted address without re-resolving, verifies certificate and
1280
- * hostname, refuses redirects and bounds the response. It must reject a
1281
- * handoff whose deadline has passed and never start a request whose signal
1282
- * aborted while queued (R3).
1283
- */
1284
- interface WebhookTransport {
1285
- send(request: WebhookTransportRequest, options: WebhookCallOptions): Promise<WebhookTransportResult>;
1286
- }
1287
- /** What admission decides about one (subscription revision, event). */
1288
- interface WebhookAdmissionInput {
1289
- readonly subscriptionId: string;
1290
- readonly eligibilityRevision: number;
1291
- readonly event: WebhookEventMetadata;
1292
- readonly facts: WebhookJsonObject;
1293
- readonly actor: WebhookJobActor;
1294
- }
1295
- /**
1296
- * Tri-state admission (decision 4). `unavailable` retries the job without
1297
- * writing an intent; so does a throw or a timeout.
1298
- */
1299
- type WebhookAdmissionDecision = {
1300
- readonly decision: "allow";
1301
- } | {
1302
- readonly decision: "deny";
1303
- readonly code: WebhookAdmissionDenialCode;
1304
- } | {
1305
- readonly decision: "unavailable";
1306
- };
1307
- /** Decides whether an event may be delivered to a subscription at fan-out. */
1308
- interface WebhookEventAdmission {
1309
- admit(input: WebhookAdmissionInput, options: WebhookCallOptions): Promise<WebhookAdmissionDecision>;
1310
- }
1311
- /** The attempt a delivery authorization check is about. */
1312
- interface WebhookDeliveryAuthorizationInput {
1313
- readonly subscriptionId: string;
1314
- readonly tenantId: string;
1315
- readonly spaceId: string;
1316
- readonly eventId: string;
1317
- readonly deliveryId: string;
1318
- readonly configurationVersion: number;
1319
- readonly attempt: number;
1320
- }
1321
- /**
1322
- * Per-attempt authorization (decision 4). `revoked` cancels the intent;
1323
- * `unavailable`, a throw or a timeout defers it without spending budget.
1324
- */
1325
- type WebhookDeliveryAuthorizationDecision = {
1326
- readonly decision: "authorized";
1327
- } | {
1328
- readonly decision: "revoked";
1329
- readonly code: WebhookRevocationCode;
1330
- } | {
1331
- readonly decision: "unavailable";
1332
- };
1333
- /** Re-checks grant expiry, scope revocation and registration status before each attempt. */
1334
- interface WebhookDeliveryAuthorization {
1335
- authorize(input: WebhookDeliveryAuthorizationInput, options: WebhookCallOptions): Promise<WebhookDeliveryAuthorizationDecision>;
1336
- }
1337
- /** The injected clock. */
1338
- interface WebhookClock {
1339
- now(): Date;
1340
- }
1341
- /**
1342
- * Delay in milliseconds before the next attempt, given the 1-based number of
1343
- * the attempt that just failed. See `createWebhookRetryDelay`.
1344
- */
1345
- type WebhookRetryDelay = (attempt: number) => number;
1346
- /** A random source returning a number in `[0, 1)`, injected so jitter is testable. */
1347
- type WebhookRandomSource = () => number;
1348
- /** What a scope predicate sees when deciding whether a scoped type may egress its payload. */
1349
- interface WebhookScopeInput {
1350
- readonly subscriptionId: string;
1351
- readonly tenantId: string;
1352
- readonly spaceId: string;
1353
- readonly event: WebhookEventMetadata;
1354
- readonly facts: WebhookJsonObject;
1355
- }
1356
- /**
1357
- * Synchronous scope predicate for scoped event types (decision 9). Only a
1358
- * literal `true` lets the payload out; `false`, any other value, a throw or a
1359
- * missing predicate renders metadata-only.
1360
- */
1361
- type WebhookScopePredicate = (input: WebhookScopeInput) => boolean;
1362
-
1363
- /** The longest destination URL accepted, in UTF-16 code units. */
1364
- declare const WEBHOOK_DESTINATION_MAX_URL_LENGTH = 2048;
1365
- /** The most addresses one lookup may return; a longer answer is refused as `dns_failure`. */
1366
- declare const WEBHOOK_DESTINATION_MAX_ADDRESSES = 32;
1367
- /** The default bound on one DNS lookup. */
1368
- declare const DEFAULT_WEBHOOK_LOOKUP_TIMEOUT_MS = 5000;
1369
- /** The result of checking a destination URL's rules, before any DNS lookup. */
1370
- type WebhookDestinationUrlCheck = {
1371
- readonly ok: true;
1372
- /** `https://<host><path><query>`: normalized by WHATWG URL parsing, fragment dropped. */
1373
- readonly url: string;
1374
- /** The host without IPv6 brackets: the SNI name and certificate identity. */
1375
- readonly hostname: string;
1376
- /** Set when the host is an IP literal; it is the only address the destination may use. */
1377
- readonly literal?: WebhookResolvedAddress;
1378
- } | {
1379
- readonly ok: false;
1380
- readonly code: "invalid_url" | "private_address";
1381
- };
1382
- /**
1383
- * Check a destination URL against decision 6's rules without resolving it.
1384
- * `invalid_url` when it is not a string of at most
1385
- * {@link WEBHOOK_DESTINATION_MAX_URL_LENGTH} characters with no ASCII
1386
- * control character or space, does not start with `https://` and a host
1387
- * character (WHATWG would find a host in `https:host` or `https:///host`),
1388
- * does not parse, is not `https:`, names a port
1389
- * other than 443 (an explicit `:443` is the default and accepted), carries
1390
- * userinfo, or has an empty host or one ending in `.`. `private_address` when
1391
- * the host is `localhost` or a `.localhost` name, or an IP literal that is not
1392
- * public ({@link isPublicWebhookAddress}). A public IP literal passes and is
1393
- * returned as `literal`. IPv4 literals in decimal, hex, octal or shorthand
1394
- * form are normalized to dotted-quad by URL parsing before they are checked.
1395
- * Never throws.
1396
- */
1397
- declare function checkWebhookDestinationUrl(url: string): WebhookDestinationUrlCheck;
1398
- /**
1399
- * Looks up a hostname's addresses. It is passed `{ deadline, signal }`; the
1400
- * resolver aborts `signal` when it stops waiting, and ignores any answer that
1401
- * arrives after that. Each answer is `{ address, family }` with a textual IPv4
1402
- * or IPv6 address. `createNodeWebhookDnsLookup()` at
1403
- * `@fabricorg/webhook-delivery/node` is one implementation.
1404
- */
1405
- type WebhookDnsLookup = (hostname: string, options: WebhookCallOptions) => Promise<readonly WebhookResolvedAddress[]>;
1406
- /** Options for {@link createWebhookDestinationResolver}. */
1407
- interface WebhookDestinationResolverOptions {
1408
- readonly lookup: WebhookDnsLookup;
1409
- /** The longest one lookup may take; default {@link DEFAULT_WEBHOOK_LOOKUP_TIMEOUT_MS}. The call's own deadline also applies. */
1410
- readonly lookupTimeoutMs?: number;
1411
- /** The clock the call's deadline is compared with; the system clock by default. */
1412
- readonly clock?: WebhookClock;
1413
- }
1414
- /**
1415
- * A {@link WebhookDestinationResolver} that applies
1416
- * {@link checkWebhookDestinationUrl}, then resolves the host through the
1417
- * injected lookup and requires every answer to be public. The vetted
1418
- * destination it returns carries the addresses the transport must connect
1419
- * to; the transport never resolves the name again, so a later answer (a DNS
1420
- * rebinding) cannot change where the request goes.
1421
- *
1422
- * A URL that passes the rules resolves to `dns_failure`, never a pass, when
1423
- * the call's signal has aborted, its deadline has passed or the clock cannot
1424
- * be read (no lookup is made; this applies to IP literals too), when the
1425
- * lookup throws, rejects, or has not answered within `lookupTimeoutMs` or by
1426
- * the deadline (its signal is then aborted and a late answer ignored), when
1427
- * an answer arrives but, once read, the signal has aborted, the clock cannot
1428
- * be read or that lookup deadline has passed, or when its answer is empty,
1429
- * longer than {@link WEBHOOK_DESTINATION_MAX_ADDRESSES}, or not a list of
1430
- * `{ address, family }` records whose family matches the address. It is
1431
- * `private_address` when any answer is a well-formed address that is not
1432
- * public, whatever the others are. A public IP-literal host is returned
1433
- * without a lookup.
1434
- *
1435
- * @throws {TypeError} `input_unreadable` at construction when `lookup` is not
1436
- * a function or `clock` has no `now` function, and from `resolve` (as a
1437
- * rejected promise) when `{ deadline, signal }` is not a real `Date` and
1438
- * `AbortSignal`.
1439
- * @throws {WebhookDeliveryConfigError} when `lookupTimeoutMs` is not positive,
1440
- * finite and at most `WEBHOOK_MAX_TIMER_MS`.
1441
- */
1442
- declare function createWebhookDestinationResolver(options: WebhookDestinationResolverOptions): WebhookDestinationResolver;
1443
-
1444
- export { type WebhookAdmissionDecision as $, type WebhookAttemptBeginResult as A, type WebhookDeliveryAttemptFinish as B, type WebhookDeliveryAttemptLogEntry as C, type WebhookRequeueEvidence as D, type WebhookTombstone as E, type WebhookPurgeResult as F, type WebhookSubscriptionStore as G, type WebhookSubscriptionWriter as H, type WebhookEligibilityRevision as I, type WebhookSubscriptionSnapshot as J, type WebhookDeliveryConfigurationInput as K, type WebhookSubscriptionWriteResult as L, type WebhookSubscriptionTransitionInput as M, type WebhookConfigurationWriteResult as N, type WebhookRandomSource as O, type WebhookRetryDelay as P, type WebhookAttemptOutcome as Q, type WebhookAttemptReasonCode as R, type WebhookTransportResult as S, type WebhookPayloadPolicy as T, type WebhookJobActor as U, type WebhookEventMetadata as V, type WebhookConfigErrorCode as W, type WebhookSourceEnvelope as X, type WebhookJsonObject as Y, type WebhookScopePredicate as Z, type WebhookDeliveryBodyV1 as _, type WebhookDeliveryErrorCode as a, type WebhookDeliveryAuthorizationDecision as a0, type WebhookAdmissionDenialCode as a1, type WebhookEventAdmission as a2, type WebhookDeliveryAuthorization as a3, type WebhookSigner as a4, type WebhookDestinationResolver as a5, type WebhookTransport as a6, DEFAULT_WEBHOOK_LOOKUP_TIMEOUT_MS as a7, WEBHOOK_ACTION_REFUSAL_CODES as a8, WEBHOOK_ADMISSION_DENIAL_CODES as a9, type WebhookJsonValue as aA, type WebhookLease as aB, type WebhookResolvedAddress as aC, type WebhookRevocationCode as aD, type WebhookScopeInput as aE, type WebhookSignRequest as aF, type WebhookSignatureScheme as aG, type WebhookSubscriptionStatus as aH, type WebhookSubscriptionWriteRefusal as aI, type WebhookTransportRequest as aJ, type WebhookVettedDestination as aK, checkWebhookDestinationUrl as aL, createWebhookDestinationResolver as aM, WEBHOOK_ATTEMPT_OUTCOMES as aa, WEBHOOK_ATTEMPT_REASON_CODES as ab, WEBHOOK_CONFIG_ERROR_CODES as ac, WEBHOOK_DELIVERY_ERROR_CODES as ad, WEBHOOK_DESTINATION_MAX_ADDRESSES as ae, WEBHOOK_DESTINATION_MAX_URL_LENGTH as af, WEBHOOK_DIAGNOSTIC_CODES as ag, WEBHOOK_FAN_OUT_ERROR_CODES as ah, WEBHOOK_GOVERNED_ACTION_ERROR_CODES as ai, WEBHOOK_INPUT_ERROR_CODES as aj, WEBHOOK_PROJECTION_ERROR_CODES as ak, WEBHOOK_REVOCATION_CODES as al, WEBHOOK_STORE_ERROR_CODES as am, type WebhookActionRefusalCode as an, type WebhookAdmissionInput as ao, type WebhookDeliveryAttemptInFlight as ap, type WebhookDeliveryAuthorizationInput as aq, type WebhookDeliveryConfiguration as ar, type WebhookDeliveryIntentStatus as as, type WebhookDestinationResolution as at, type WebhookDestinationResolverOptions as au, type WebhookDestinationUrlCheck as av, type WebhookDnsLookup as aw, type WebhookEventTypePolicy as ax, type WebhookFanOutJobStatus as ay, type WebhookInputErrorCode as az, type WebhookFanOutErrorCode as b, type WebhookGovernedActionErrorCode as c, type WebhookProjectionErrorCode as d, type WebhookStoreErrorCode as e, type WebhookDeliveryStore as f, type WebhookClock as g, type WebhookFanOutJobDraft as h, type WebhookCallOptions as i, type WebhookInsertResult as j, type WebhookClaimRequest as k, type WebhookFanOutJob as l, type WebhookFencedWrite as m, type WebhookDiagnosticCode as n, type WebhookScope as o, type WebhookRequeueRequest as p, type WebhookRequeueResult as q, type WebhookActorEnrichmentRequest as r, type WebhookActorEnrichmentResult as s, type WebhookDeliveryIntent as t, type WebhookFanOutJobFence as u, type WebhookFencedInsertResult as v, type WebhookAdmissionDenialRecord as w, type WebhookDeliveryLeaseCheck as x, type WebhookDeliveryAttemptRecord as y, type WebhookDeliveryAttemptStart as z };