@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.
- package/CHANGELOG.md +29 -0
- package/README.md +10 -4
- package/dist/index.cjs +8641 -6289
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2227 -779
- package/dist/index.d.ts +2227 -779
- package/dist/index.js +8640 -6287
- package/dist/index.js.map +1 -1
- package/dist/node.cjs +1089 -746
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +102 -32
- package/dist/node.d.ts +102 -32
- package/dist/node.js +1088 -744
- package/dist/node.js.map +1 -1
- package/package.json +8 -7
- package/dist/destination-joCBtgvY.d.cts +0 -1444
- package/dist/destination-joCBtgvY.d.ts +0 -1444
|
@@ -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 };
|