@orthacms/webhooks-server 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/lib/application/dto/list-deliveries-query.dto.d.ts +8 -0
- package/dist/lib/application/dto/list-deliveries-query.dto.d.ts.map +1 -0
- package/dist/lib/application/dto/list-deliveries-query.dto.js +64 -0
- package/dist/lib/application/dto/save-webhook-endpoint.dto.d.ts +23 -0
- package/dist/lib/application/dto/save-webhook-endpoint.dto.d.ts.map +1 -0
- package/dist/lib/application/dto/save-webhook-endpoint.dto.js +127 -0
- package/dist/lib/application/webhook-endpoints.service.d.ts +82 -0
- package/dist/lib/application/webhook-endpoints.service.d.ts.map +1 -0
- package/dist/lib/application/webhook-endpoints.service.js +213 -0
- package/dist/lib/docs/describe-webhooks-api.d.ts +16 -0
- package/dist/lib/docs/describe-webhooks-api.d.ts.map +1 -0
- package/dist/lib/docs/describe-webhooks-api.js +170 -0
- package/dist/lib/docs/webhook-schemas.d.ts +21 -0
- package/dist/lib/docs/webhook-schemas.d.ts.map +1 -0
- package/dist/lib/docs/webhook-schemas.js +360 -0
- package/dist/lib/domain/errors/index.d.ts +3 -0
- package/dist/lib/domain/errors/index.d.ts.map +1 -0
- package/dist/lib/domain/errors/index.js +7 -0
- package/dist/lib/domain/errors/webhook-delivery-not-found.error.d.ts +12 -0
- package/dist/lib/domain/errors/webhook-delivery-not-found.error.d.ts.map +1 -0
- package/dist/lib/domain/errors/webhook-delivery-not-found.error.js +19 -0
- package/dist/lib/domain/errors/webhook-endpoint-not-found.error.d.ts +6 -0
- package/dist/lib/domain/errors/webhook-endpoint-not-found.error.d.ts.map +1 -0
- package/dist/lib/domain/errors/webhook-endpoint-not-found.error.js +13 -0
- package/dist/lib/domain/webhook-secret.d.ts +29 -0
- package/dist/lib/domain/webhook-secret.d.ts.map +1 -0
- package/dist/lib/domain/webhook-secret.js +34 -0
- package/dist/lib/domain/webhook-views.d.ts +85 -0
- package/dist/lib/domain/webhook-views.d.ts.map +1 -0
- package/dist/lib/domain/webhook-views.js +2 -0
- package/dist/lib/http/controllers/webhook-deliveries.controller.d.ts +28 -0
- package/dist/lib/http/controllers/webhook-deliveries.controller.d.ts.map +1 -0
- package/dist/lib/http/controllers/webhook-deliveries.controller.js +110 -0
- package/dist/lib/http/controllers/webhook-endpoints.controller.d.ts +61 -0
- package/dist/lib/http/controllers/webhook-endpoints.controller.d.ts.map +1 -0
- package/dist/lib/http/controllers/webhook-endpoints.controller.js +213 -0
- package/dist/lib/http/controllers/webhook-events.controller.d.ts +13 -0
- package/dist/lib/http/controllers/webhook-events.controller.d.ts.map +1 -0
- package/dist/lib/http/controllers/webhook-events.controller.js +38 -0
- package/dist/lib/infrastructure/event-mapping.d.ts +13 -0
- package/dist/lib/infrastructure/event-mapping.d.ts.map +1 -0
- package/dist/lib/infrastructure/event-mapping.js +69 -0
- package/dist/lib/infrastructure/purge/webhook-workspaces.purger.d.ts +42 -0
- package/dist/lib/infrastructure/purge/webhook-workspaces.purger.d.ts.map +1 -0
- package/dist/lib/infrastructure/purge/webhook-workspaces.purger.js +66 -0
- package/dist/lib/infrastructure/schema/index.d.ts +3 -0
- package/dist/lib/infrastructure/schema/index.d.ts.map +1 -0
- package/dist/lib/infrastructure/schema/index.js +8 -0
- package/dist/lib/infrastructure/schema/webhook-deliveries.d.ts +332 -0
- package/dist/lib/infrastructure/schema/webhook-deliveries.d.ts.map +1 -0
- package/dist/lib/infrastructure/schema/webhook-deliveries.js +92 -0
- package/dist/lib/infrastructure/schema/webhook-endpoints.d.ts +330 -0
- package/dist/lib/infrastructure/schema/webhook-endpoints.d.ts.map +1 -0
- package/dist/lib/infrastructure/schema/webhook-endpoints.js +94 -0
- package/dist/lib/infrastructure/webhook-delivery.repository.d.ts +99 -0
- package/dist/lib/infrastructure/webhook-delivery.repository.d.ts.map +1 -0
- package/dist/lib/infrastructure/webhook-delivery.repository.js +255 -0
- package/dist/lib/infrastructure/webhook-delivery.worker.d.ts +72 -0
- package/dist/lib/infrastructure/webhook-delivery.worker.d.ts.map +1 -0
- package/dist/lib/infrastructure/webhook-delivery.worker.js +216 -0
- package/dist/lib/infrastructure/webhook-endpoint.repository.d.ts +88 -0
- package/dist/lib/infrastructure/webhook-endpoint.repository.d.ts.map +1 -0
- package/dist/lib/infrastructure/webhook-endpoint.repository.js +306 -0
- package/dist/lib/infrastructure/webhook-fanout.subscriber.d.ts +36 -0
- package/dist/lib/infrastructure/webhook-fanout.subscriber.d.ts.map +1 -0
- package/dist/lib/infrastructure/webhook-fanout.subscriber.js +83 -0
- package/dist/lib/infrastructure/webhook-http.client.d.ts +76 -0
- package/dist/lib/infrastructure/webhook-http.client.d.ts.map +1 -0
- package/dist/lib/infrastructure/webhook-http.client.js +225 -0
- package/dist/lib/types/webhooks-config.d.ts +64 -0
- package/dist/lib/types/webhooks-config.d.ts.map +1 -0
- package/dist/lib/types/webhooks-config.js +24 -0
- package/dist/lib/utils/webhooks-plugin.d.ts +35 -0
- package/dist/lib/utils/webhooks-plugin.d.ts.map +1 -0
- package/dist/lib/utils/webhooks-plugin.js +90 -0
- package/dist/lib/webhooks.module.d.ts +18 -0
- package/dist/lib/webhooks.module.d.ts.map +1 -0
- package/dist/lib/webhooks.module.js +72 -0
- package/dist/lib/webhooks.tokens.d.ts +5 -0
- package/dist/lib/webhooks.tokens.d.ts.map +1 -0
- package/dist/lib/webhooks.tokens.js +9 -0
- package/migrations/0000_init_webhooks.sql +53 -0
- package/migrations/meta/0000_snapshot.json +424 -0
- package/migrations/meta/_journal.json +13 -0
- package/package.json +45 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var WebhookHttpClient_1;
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.WebhookHttpClient = void 0;
|
|
5
|
+
const tslib_1 = require("tslib");
|
|
6
|
+
const common_1 = require("@nestjs/common");
|
|
7
|
+
const node_dns_1 = require("node:dns");
|
|
8
|
+
const webhooks_domain_1 = require("@orthacms/webhooks-domain");
|
|
9
|
+
const undici_1 = require("undici");
|
|
10
|
+
const webhooks_tokens_1 = require("../webhooks.tokens");
|
|
11
|
+
/**
|
|
12
|
+
* Posts one delivery, under the policy that decides where this server may be
|
|
13
|
+
* talked into connecting.
|
|
14
|
+
*
|
|
15
|
+
* **Why an `undici.Agent` and not `fetch`.** The address has to be checked
|
|
16
|
+
* *after* the hostname resolves and the connection has to go to that same
|
|
17
|
+
* address. Global `fetch` gives no hook between the two, so a name that passes
|
|
18
|
+
* a pre-flight check and then resolves to `169.254.169.254` — DNS rebinding —
|
|
19
|
+
* connects anyway. A custom `lookup` is the seam where the resolved address is
|
|
20
|
+
* judged and either handed to the connector or refused.
|
|
21
|
+
*
|
|
22
|
+
* Redirects are not followed. A `3xx` is reported as a failed delivery rather
|
|
23
|
+
* than an invitation to make a second request, because following one is the
|
|
24
|
+
* simplest way to be walked to an internal address after the first hop passed.
|
|
25
|
+
*/
|
|
26
|
+
let WebhookHttpClient = WebhookHttpClient_1 = class WebhookHttpClient {
|
|
27
|
+
config;
|
|
28
|
+
logger = new common_1.Logger(WebhookHttpClient_1.name);
|
|
29
|
+
/**
|
|
30
|
+
* One agent for every delivery, so connections are pooled across them.
|
|
31
|
+
* Built lazily because it captures the policy from injected config.
|
|
32
|
+
*/
|
|
33
|
+
agent = null;
|
|
34
|
+
constructor(config) {
|
|
35
|
+
this.config = config;
|
|
36
|
+
}
|
|
37
|
+
/** Sends one delivery and reports what happened. Never throws. */
|
|
38
|
+
async send(delivery) {
|
|
39
|
+
const startedAt = Date.now();
|
|
40
|
+
const body = JSON.stringify(delivery.body);
|
|
41
|
+
const timestamp = Math.floor(startedAt / 1000);
|
|
42
|
+
try {
|
|
43
|
+
// Re-checked on every send, not only when the endpoint was saved:
|
|
44
|
+
// a hostname re-pointed at a private address afterwards must be
|
|
45
|
+
// refused here, and the URL policy may have been tightened since.
|
|
46
|
+
const url = (0, webhooks_domain_1.assertUrlShape)(delivery.url, this.policy());
|
|
47
|
+
const response = await (0, undici_1.request)(url, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
body,
|
|
50
|
+
headers: this.headersFor(delivery, body, timestamp),
|
|
51
|
+
dispatcher: this.dispatcher(),
|
|
52
|
+
// `request` does not follow redirects unless a redirect
|
|
53
|
+
// interceptor is added, and none is: a 3xx is reported as a
|
|
54
|
+
// failed delivery rather than followed, because following one
|
|
55
|
+
// is the simplest way to be walked to an internal address
|
|
56
|
+
// after the first hop already passed the address check.
|
|
57
|
+
headersTimeout: this.config.timeoutMs,
|
|
58
|
+
bodyTimeout: this.config.timeoutMs
|
|
59
|
+
});
|
|
60
|
+
const snippet = await this.readSnippet(response.body);
|
|
61
|
+
return {
|
|
62
|
+
statusCode: response.statusCode,
|
|
63
|
+
retryAfterMs: retryAfterOf(response.headers),
|
|
64
|
+
error: null,
|
|
65
|
+
responseSnippet: snippet,
|
|
66
|
+
durationMs: Date.now() - startedAt
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
return {
|
|
71
|
+
statusCode: null,
|
|
72
|
+
error: describeFailure(error),
|
|
73
|
+
responseSnippet: null,
|
|
74
|
+
durationMs: Date.now() - startedAt
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** The URL policy this deployment configured. */
|
|
79
|
+
policy() {
|
|
80
|
+
return {
|
|
81
|
+
allowInsecureUrls: this.config.allowInsecureUrls,
|
|
82
|
+
allowPrivateNetworks: this.config.allowPrivateNetworks
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The agent, with the address check wired into its DNS lookup.
|
|
87
|
+
*
|
|
88
|
+
* `lookup` is called by the connector with the hostname it is about to
|
|
89
|
+
* connect to; refusing here means the socket is never opened, and the
|
|
90
|
+
* address that was judged is the address that would have been used.
|
|
91
|
+
*/
|
|
92
|
+
dispatcher() {
|
|
93
|
+
if (this.agent)
|
|
94
|
+
return this.agent;
|
|
95
|
+
const policy = this.policy();
|
|
96
|
+
this.agent = new undici_1.Agent({
|
|
97
|
+
connect: {
|
|
98
|
+
timeout: this.config.timeoutMs,
|
|
99
|
+
lookup: (hostname, options, callback) => {
|
|
100
|
+
(0, node_dns_1.lookup)(hostname, options, (error, address, family) => {
|
|
101
|
+
if (error) {
|
|
102
|
+
callback(error, address, family);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
for (const resolved of toAddressList(address)) {
|
|
107
|
+
(0, webhooks_domain_1.assertAddressAllowed)(resolved, policy);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (rejection) {
|
|
111
|
+
callback(rejection, address, family);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
callback(null, address, family);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
return this.agent;
|
|
120
|
+
}
|
|
121
|
+
/** The headers one delivery travels with. */
|
|
122
|
+
headersFor(delivery, body, timestamp) {
|
|
123
|
+
const headers = {};
|
|
124
|
+
// The endpoint's own headers go first, so the reserved ones below can
|
|
125
|
+
// never be overwritten by configuration. `isAllowedCustomHeader` is
|
|
126
|
+
// also enforced on write; this is the belt to that braces, because a
|
|
127
|
+
// row could predate the check.
|
|
128
|
+
for (const [name, value] of Object.entries(delivery.headers)) {
|
|
129
|
+
if ((0, webhooks_domain_1.isAllowedCustomHeader)(name))
|
|
130
|
+
headers[name] = value;
|
|
131
|
+
}
|
|
132
|
+
headers['content-type'] = 'application/json';
|
|
133
|
+
headers['user-agent'] = webhooks_domain_1.DELIVERY_USER_AGENT;
|
|
134
|
+
headers[webhooks_domain_1.DELIVERY_HEADERS.EVENT] = delivery.eventKind;
|
|
135
|
+
headers[webhooks_domain_1.DELIVERY_HEADERS.DELIVERY] = delivery.deliveryId;
|
|
136
|
+
headers[webhooks_domain_1.DELIVERY_HEADERS.EVENT_ID] = delivery.eventId;
|
|
137
|
+
headers[webhooks_domain_1.DELIVERY_HEADERS.ATTEMPT] = String(delivery.attempt);
|
|
138
|
+
headers[webhooks_domain_1.DELIVERY_HEADERS.SIGNATURE] = (0, webhooks_domain_1.signatureHeader)(delivery.secret, timestamp, body);
|
|
139
|
+
if (delivery.workspaceId) {
|
|
140
|
+
headers[webhooks_domain_1.DELIVERY_HEADERS.WORKSPACE] = delivery.workspaceId;
|
|
141
|
+
}
|
|
142
|
+
return headers;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Reads at most `responseSnippetBytes` of the body, then abandons the rest.
|
|
146
|
+
*
|
|
147
|
+
* A receiver that answers with a gigabyte would otherwise be able to
|
|
148
|
+
* exhaust this process's memory from the far end of a connection we
|
|
149
|
+
* initiated.
|
|
150
|
+
*/
|
|
151
|
+
async readSnippet(body) {
|
|
152
|
+
const chunks = [];
|
|
153
|
+
let size = 0;
|
|
154
|
+
try {
|
|
155
|
+
for await (const chunk of body) {
|
|
156
|
+
const buffer = Buffer.isBuffer(chunk)
|
|
157
|
+
? chunk
|
|
158
|
+
: Buffer.from(chunk);
|
|
159
|
+
chunks.push(buffer);
|
|
160
|
+
size += buffer.length;
|
|
161
|
+
if (size >= this.config.responseSnippetBytes)
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
this.logger.debug(`Could not read the response body: ${describeFailure(error)}`);
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
// Whether we stopped early or the stream errored, the connection
|
|
170
|
+
// must not be left half-read or the pooled socket is unusable.
|
|
171
|
+
body.resume?.();
|
|
172
|
+
}
|
|
173
|
+
if (chunks.length === 0)
|
|
174
|
+
return null;
|
|
175
|
+
return Buffer.concat(chunks)
|
|
176
|
+
.subarray(0, this.config.responseSnippetBytes)
|
|
177
|
+
.toString('utf8');
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
exports.WebhookHttpClient = WebhookHttpClient;
|
|
181
|
+
exports.WebhookHttpClient = WebhookHttpClient = WebhookHttpClient_1 = tslib_1.__decorate([
|
|
182
|
+
(0, common_1.Injectable)(),
|
|
183
|
+
tslib_1.__param(0, (0, webhooks_tokens_1.InjectWebhooksConfig)()),
|
|
184
|
+
tslib_1.__metadata("design:paramtypes", [Object])
|
|
185
|
+
], WebhookHttpClient);
|
|
186
|
+
/**
|
|
187
|
+
* A `Retry-After` from the response headers, in milliseconds.
|
|
188
|
+
*
|
|
189
|
+
* Parsed in the domain so the header's two legal forms live in one place;
|
|
190
|
+
* `undefined` simply falls back to our own schedule.
|
|
191
|
+
*/
|
|
192
|
+
function retryAfterOf(headers) {
|
|
193
|
+
const raw = headers['retry-after'];
|
|
194
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
195
|
+
return value ? (0, webhooks_domain_1.parseRetryAfter)(value) : undefined;
|
|
196
|
+
}
|
|
197
|
+
/** `lookup` may hand back one address or a list; normalise to a list. */
|
|
198
|
+
function toAddressList(address) {
|
|
199
|
+
return typeof address === 'string'
|
|
200
|
+
? [address]
|
|
201
|
+
: address.map((entry) => entry.address);
|
|
202
|
+
}
|
|
203
|
+
/** A failure, phrased for the delivery log rather than for a stack trace. */
|
|
204
|
+
function describeFailure(error) {
|
|
205
|
+
if (error instanceof webhooks_domain_1.WebhookUrlRejectedError)
|
|
206
|
+
return error.message;
|
|
207
|
+
if (error instanceof Error) {
|
|
208
|
+
const code = error.code;
|
|
209
|
+
if (code === 'UND_ERR_HEADERS_TIMEOUT' ||
|
|
210
|
+
code === 'UND_ERR_BODY_TIMEOUT') {
|
|
211
|
+
return 'The receiver did not respond in time.';
|
|
212
|
+
}
|
|
213
|
+
if (code === 'UND_ERR_CONNECT_TIMEOUT') {
|
|
214
|
+
return 'Could not connect to the receiver in time.';
|
|
215
|
+
}
|
|
216
|
+
if (code === 'ENOTFOUND') {
|
|
217
|
+
return 'The host name could not be resolved.';
|
|
218
|
+
}
|
|
219
|
+
if (code === 'ECONNREFUSED') {
|
|
220
|
+
return 'The receiver refused the connection.';
|
|
221
|
+
}
|
|
222
|
+
return error.message;
|
|
223
|
+
}
|
|
224
|
+
return String(error);
|
|
225
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for the webhooks plugin.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is a deployment concern — how hard the worker is allowed to
|
|
5
|
+
* push, how long it waits, what it may reach. The endpoints themselves live in
|
|
6
|
+
* the database, because which systems a CMS notifies is operational data that
|
|
7
|
+
* changes without a redeploy.
|
|
8
|
+
*/
|
|
9
|
+
export interface WebhooksPluginConfig {
|
|
10
|
+
/**
|
|
11
|
+
* How often, in milliseconds, the worker looks for claimable deliveries.
|
|
12
|
+
* `0` turns delivery off entirely — rows still queue, so a deployment can
|
|
13
|
+
* run the API without a sender and let one process do the sending.
|
|
14
|
+
*/
|
|
15
|
+
deliveryIntervalMs?: number;
|
|
16
|
+
/**
|
|
17
|
+
* How many deliveries one tick claims. Bounds the burst a single process
|
|
18
|
+
* aims at receivers, and the memory one tick holds.
|
|
19
|
+
*/
|
|
20
|
+
batchSize?: number;
|
|
21
|
+
/** Per-request timeout in milliseconds. */
|
|
22
|
+
timeoutMs?: number;
|
|
23
|
+
/** How many attempts a delivery gets before it is given up on. */
|
|
24
|
+
maxAttempts?: number;
|
|
25
|
+
/**
|
|
26
|
+
* How long a completed delivery stays in the log, in days. `0` keeps them
|
|
27
|
+
* forever — which is a real choice for a low-volume install, and a table
|
|
28
|
+
* that grows without bound for any other.
|
|
29
|
+
*/
|
|
30
|
+
retentionDays?: number;
|
|
31
|
+
/**
|
|
32
|
+
* How many consecutive dead deliveries switch an endpoint off. Without a
|
|
33
|
+
* ceiling, a staging URL that was torn down months ago keeps generating six
|
|
34
|
+
* requests per save for the life of the deployment.
|
|
35
|
+
*/
|
|
36
|
+
autoDisableAfter?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Permit `http://` destinations. Off by default — a signature over
|
|
39
|
+
* plaintext still leaks the payload to the network path.
|
|
40
|
+
*/
|
|
41
|
+
allowInsecureUrls?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Permit loopback, link-local and RFC 1918 destinations. Off by default;
|
|
44
|
+
* a self-hosted install whose receiver shares the cluster turns it on
|
|
45
|
+
* deliberately.
|
|
46
|
+
*/
|
|
47
|
+
allowPrivateNetworks?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* How long a row may sit in `delivering` before the reaper assumes the
|
|
50
|
+
* worker that claimed it died, in milliseconds. Must comfortably exceed
|
|
51
|
+
* {@link timeoutMs}, or a slow-but-live request gets reclaimed and sent
|
|
52
|
+
* twice.
|
|
53
|
+
*/
|
|
54
|
+
claimTimeoutMs?: number;
|
|
55
|
+
/** Bytes of the response body kept for the delivery log. */
|
|
56
|
+
responseSnippetBytes?: number;
|
|
57
|
+
}
|
|
58
|
+
/** Config with every optional filled in — what the services actually read. */
|
|
59
|
+
export type ResolvedWebhooksConfig = Required<WebhooksPluginConfig>;
|
|
60
|
+
/** Defaults chosen to be safe next to someone else's server, not fast. */
|
|
61
|
+
export declare const WEBHOOKS_DEFAULTS: ResolvedWebhooksConfig;
|
|
62
|
+
/** Fills the defaults in, so no service has to repeat a `??`. */
|
|
63
|
+
export declare function resolveWebhooksConfig(config?: WebhooksPluginConfig): ResolvedWebhooksConfig;
|
|
64
|
+
//# sourceMappingURL=webhooks-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhooks-config.d.ts","sourceRoot":"","sources":["../../../src/lib/types/webhooks-config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,WAAW,oBAAoB;IACjC;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAE5B;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAE/B;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,4DAA4D;IAC5D,oBAAoB,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,8EAA8E;AAC9E,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC,oBAAoB,CAAC,CAAC;AAEpE,0EAA0E;AAC1E,eAAO,MAAM,iBAAiB,EAAE,sBAc/B,CAAC;AAEF,iEAAiE;AACjE,wBAAgB,qBAAqB,CACjC,MAAM,GAAE,oBAAyB,GAClC,sBAAsB,CAExB"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WEBHOOKS_DEFAULTS = void 0;
|
|
4
|
+
exports.resolveWebhooksConfig = resolveWebhooksConfig;
|
|
5
|
+
/** Defaults chosen to be safe next to someone else's server, not fast. */
|
|
6
|
+
exports.WEBHOOKS_DEFAULTS = {
|
|
7
|
+
deliveryIntervalMs: 2_000,
|
|
8
|
+
batchSize: 20,
|
|
9
|
+
timeoutMs: 10_000,
|
|
10
|
+
maxAttempts: 6,
|
|
11
|
+
retentionDays: 30,
|
|
12
|
+
autoDisableAfter: 20,
|
|
13
|
+
allowInsecureUrls: false,
|
|
14
|
+
allowPrivateNetworks: false,
|
|
15
|
+
// Three times the request timeout: long enough that a slow receiver is
|
|
16
|
+
// never reclaimed underneath a live request, short enough that a crashed
|
|
17
|
+
// worker's rows move again within a minute.
|
|
18
|
+
claimTimeoutMs: 30_000,
|
|
19
|
+
responseSnippetBytes: 2_048
|
|
20
|
+
};
|
|
21
|
+
/** Fills the defaults in, so no service has to repeat a `??`. */
|
|
22
|
+
function resolveWebhooksConfig(config = {}) {
|
|
23
|
+
return { ...exports.WEBHOOKS_DEFAULTS, ...config };
|
|
24
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ServerPlugin } from '@orthacms/bootstrap-server';
|
|
2
|
+
import type { WebhooksPluginConfig } from '../types/webhooks-config';
|
|
3
|
+
/** The webhooks plugin, carrying its config alongside the standard shape. */
|
|
4
|
+
export interface WebhooksServerPluginType extends ServerPlugin {
|
|
5
|
+
/** The configuration this plugin was constructed with. */
|
|
6
|
+
webhooksConfig: WebhooksPluginConfig;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Creates the webhooks plugin — outgoing HTTP notifications about content
|
|
10
|
+
* changes.
|
|
11
|
+
*
|
|
12
|
+
* Register it **after** `DatabasePlugin` (the outbox it subscribes to and the
|
|
13
|
+
* client it injects) and after `IdentityPlugin` (the permissions its routes are
|
|
14
|
+
* gated on). It owns three tables — `webhook_endpoints`,
|
|
15
|
+
* `webhook_endpoint_workspaces` and `webhook_deliveries` — and ships their
|
|
16
|
+
* migrations under its own tracking table.
|
|
17
|
+
*
|
|
18
|
+
* It never delivers from inside the outbox subscriber: the subscriber queues
|
|
19
|
+
* rows, and a worker sends them with no transaction open. See ADR-0016 for why
|
|
20
|
+
* that separation is a rule rather than an implementation detail.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* createServer({
|
|
25
|
+
* plugins: [
|
|
26
|
+
* DatabasePlugin({ connectionString: config.database.url }),
|
|
27
|
+
* IdentityPlugin(config.plugins.identity),
|
|
28
|
+
* ContentPlugin({ types: contentTypes }),
|
|
29
|
+
* WebhooksPlugin({ retentionDays: 30 })
|
|
30
|
+
* ]
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare function WebhooksPlugin(config?: WebhooksPluginConfig): WebhooksServerPluginType;
|
|
35
|
+
//# sourceMappingURL=webhooks-plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhooks-plugin.d.ts","sourceRoot":"","sources":["../../../src/lib/utils/webhooks-plugin.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAG/D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAErE,6EAA6E;AAC7E,MAAM,WAAW,wBAAyB,SAAQ,YAAY;IAC1D,0DAA0D;IAC1D,cAAc,EAAE,oBAAoB,CAAC;CACxC;AAqDD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,cAAc,CAC1B,MAAM,GAAE,oBAAyB,GAClC,wBAAwB,CAgB1B"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WebhooksPlugin = WebhooksPlugin;
|
|
4
|
+
const node_path_1 = require("node:path");
|
|
5
|
+
const describe_webhooks_api_1 = require("../docs/describe-webhooks-api");
|
|
6
|
+
const webhooks_module_1 = require("../webhooks.module");
|
|
7
|
+
/**
|
|
8
|
+
* Rejects a configuration that would misbehave rather than fail loudly.
|
|
9
|
+
*
|
|
10
|
+
* The bounds here are not style: a zero `batchSize` is a worker that claims
|
|
11
|
+
* nothing and looks exactly like a worker with nothing to do, and a
|
|
12
|
+
* `claimTimeoutMs` under the request timeout reclaims deliveries out from under
|
|
13
|
+
* live requests and sends every slow one twice. Both are far cheaper to catch
|
|
14
|
+
* at construction than to diagnose from a delivery log.
|
|
15
|
+
*/
|
|
16
|
+
function assertConfig(config) {
|
|
17
|
+
const positive = [
|
|
18
|
+
['batchSize', config.batchSize],
|
|
19
|
+
['timeoutMs', config.timeoutMs],
|
|
20
|
+
['maxAttempts', config.maxAttempts],
|
|
21
|
+
['autoDisableAfter', config.autoDisableAfter],
|
|
22
|
+
['claimTimeoutMs', config.claimTimeoutMs],
|
|
23
|
+
['responseSnippetBytes', config.responseSnippetBytes]
|
|
24
|
+
];
|
|
25
|
+
for (const [key, value] of positive) {
|
|
26
|
+
if (value !== undefined && (!Number.isInteger(value) || value < 1)) {
|
|
27
|
+
throw new Error(`WebhooksPlugin: ${String(key)} must be a positive integer; got ${value}.`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Zero is a real choice for both of these — "do not send from this process"
|
|
31
|
+
// and "keep the log forever" — so only a negative value is refused.
|
|
32
|
+
const nonNegative = [
|
|
33
|
+
['deliveryIntervalMs', config.deliveryIntervalMs],
|
|
34
|
+
['retentionDays', config.retentionDays]
|
|
35
|
+
];
|
|
36
|
+
for (const [key, value] of nonNegative) {
|
|
37
|
+
if (value !== undefined && (!Number.isInteger(value) || value < 0)) {
|
|
38
|
+
throw new Error(`WebhooksPlugin: ${String(key)} must be a non-negative integer (0 disables it); got ${value}.`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const timeout = config.timeoutMs ?? 10_000;
|
|
42
|
+
const claim = config.claimTimeoutMs ?? 30_000;
|
|
43
|
+
if (claim <= timeout) {
|
|
44
|
+
throw new Error(`WebhooksPlugin: claimTimeoutMs (${claim}) must exceed timeoutMs (${timeout}), ` +
|
|
45
|
+
'or a delivery still in flight is reclaimed by another worker and sent twice.');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Creates the webhooks plugin — outgoing HTTP notifications about content
|
|
50
|
+
* changes.
|
|
51
|
+
*
|
|
52
|
+
* Register it **after** `DatabasePlugin` (the outbox it subscribes to and the
|
|
53
|
+
* client it injects) and after `IdentityPlugin` (the permissions its routes are
|
|
54
|
+
* gated on). It owns three tables — `webhook_endpoints`,
|
|
55
|
+
* `webhook_endpoint_workspaces` and `webhook_deliveries` — and ships their
|
|
56
|
+
* migrations under its own tracking table.
|
|
57
|
+
*
|
|
58
|
+
* It never delivers from inside the outbox subscriber: the subscriber queues
|
|
59
|
+
* rows, and a worker sends them with no transaction open. See ADR-0016 for why
|
|
60
|
+
* that separation is a rule rather than an implementation detail.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* createServer({
|
|
65
|
+
* plugins: [
|
|
66
|
+
* DatabasePlugin({ connectionString: config.database.url }),
|
|
67
|
+
* IdentityPlugin(config.plugins.identity),
|
|
68
|
+
* ContentPlugin({ types: contentTypes }),
|
|
69
|
+
* WebhooksPlugin({ retentionDays: 30 })
|
|
70
|
+
* ]
|
|
71
|
+
* });
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
function WebhooksPlugin(config = {}) {
|
|
75
|
+
assertConfig(config);
|
|
76
|
+
return {
|
|
77
|
+
name: 'webhooks',
|
|
78
|
+
module: webhooks_module_1.WebhooksModule.forRoot(config),
|
|
79
|
+
webhooksConfig: config,
|
|
80
|
+
migrations: {
|
|
81
|
+
dir: () => (0, node_path_1.join)(__dirname, '../../../migrations'),
|
|
82
|
+
table: '__drizzle_migrations_webhooks'
|
|
83
|
+
},
|
|
84
|
+
// Every response view here is an `interface`, which `@nestjs/swagger`
|
|
85
|
+
// cannot see — so the scanner emits an empty 200 for each route. This
|
|
86
|
+
// hook is where the reference learns what a delivery, an endpoint and
|
|
87
|
+
// a test result actually look like.
|
|
88
|
+
docs: { decorate: describe_webhooks_api_1.describeWebhooksApi }
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type DynamicModule } from '@nestjs/common';
|
|
2
|
+
import { type WebhooksPluginConfig } from './types/webhooks-config';
|
|
3
|
+
/**
|
|
4
|
+
* The webhooks plugin's one dynamic module.
|
|
5
|
+
*
|
|
6
|
+
* `global: true` like every other plugin here, so a future surface — an MCP
|
|
7
|
+
* tool that lists endpoints, a copilot read tool — injects these services
|
|
8
|
+
* rather than growing its own queries over the same three tables.
|
|
9
|
+
*
|
|
10
|
+
* Route order matters: `webhooks/:id/deliveries` is registered **before**
|
|
11
|
+
* `webhooks/:id`, so the literal segment is matched before the parameter that
|
|
12
|
+
* would otherwise swallow it. The same reason content registers `bulk` ahead of
|
|
13
|
+
* `:id`.
|
|
14
|
+
*/
|
|
15
|
+
export declare class WebhooksModule {
|
|
16
|
+
static forRoot(config?: WebhooksPluginConfig): DynamicModule;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=webhooks.module.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhooks.module.d.ts","sourceRoot":"","sources":["../../src/lib/webhooks.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAY5D,OAAO,EAEH,KAAK,oBAAoB,EAC5B,MAAM,yBAAyB,CAAC;AAEjC;;;;;;;;;;;GAWG;AACH,qBACa,cAAc;IACvB,MAAM,CAAC,OAAO,CAAC,MAAM,GAAE,oBAAyB,GAAG,aAAa;CAoCnE"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var WebhooksModule_1;
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.WebhooksModule = void 0;
|
|
5
|
+
const tslib_1 = require("tslib");
|
|
6
|
+
const common_1 = require("@nestjs/common");
|
|
7
|
+
const webhooks_tokens_1 = require("./webhooks.tokens");
|
|
8
|
+
const webhook_endpoints_service_1 = require("./application/webhook-endpoints.service");
|
|
9
|
+
const webhook_delivery_repository_1 = require("./infrastructure/webhook-delivery.repository");
|
|
10
|
+
const webhook_delivery_worker_1 = require("./infrastructure/webhook-delivery.worker");
|
|
11
|
+
const webhook_endpoint_repository_1 = require("./infrastructure/webhook-endpoint.repository");
|
|
12
|
+
const webhook_fanout_subscriber_1 = require("./infrastructure/webhook-fanout.subscriber");
|
|
13
|
+
const webhook_http_client_1 = require("./infrastructure/webhook-http.client");
|
|
14
|
+
const webhook_workspaces_purger_1 = require("./infrastructure/purge/webhook-workspaces.purger");
|
|
15
|
+
const webhook_deliveries_controller_1 = require("./http/controllers/webhook-deliveries.controller");
|
|
16
|
+
const webhook_endpoints_controller_1 = require("./http/controllers/webhook-endpoints.controller");
|
|
17
|
+
const webhook_events_controller_1 = require("./http/controllers/webhook-events.controller");
|
|
18
|
+
const webhooks_config_1 = require("./types/webhooks-config");
|
|
19
|
+
/**
|
|
20
|
+
* The webhooks plugin's one dynamic module.
|
|
21
|
+
*
|
|
22
|
+
* `global: true` like every other plugin here, so a future surface — an MCP
|
|
23
|
+
* tool that lists endpoints, a copilot read tool — injects these services
|
|
24
|
+
* rather than growing its own queries over the same three tables.
|
|
25
|
+
*
|
|
26
|
+
* Route order matters: `webhooks/:id/deliveries` is registered **before**
|
|
27
|
+
* `webhooks/:id`, so the literal segment is matched before the parameter that
|
|
28
|
+
* would otherwise swallow it. The same reason content registers `bulk` ahead of
|
|
29
|
+
* `:id`.
|
|
30
|
+
*/
|
|
31
|
+
let WebhooksModule = WebhooksModule_1 = class WebhooksModule {
|
|
32
|
+
static forRoot(config = {}) {
|
|
33
|
+
return {
|
|
34
|
+
module: WebhooksModule_1,
|
|
35
|
+
global: true,
|
|
36
|
+
controllers: [
|
|
37
|
+
webhook_events_controller_1.WebhookEventsController,
|
|
38
|
+
webhook_deliveries_controller_1.WebhookDeliveriesController,
|
|
39
|
+
webhook_endpoints_controller_1.WebhookEndpointsController
|
|
40
|
+
],
|
|
41
|
+
providers: [
|
|
42
|
+
{
|
|
43
|
+
provide: webhooks_tokens_1.WEBHOOKS_CONFIG,
|
|
44
|
+
useValue: (0, webhooks_config_1.resolveWebhooksConfig)(config)
|
|
45
|
+
},
|
|
46
|
+
webhook_endpoint_repository_1.WebhookEndpointRepository,
|
|
47
|
+
webhook_delivery_repository_1.WebhookDeliveryRepository,
|
|
48
|
+
webhook_http_client_1.WebhookHttpClient,
|
|
49
|
+
webhook_endpoints_service_1.WebhookEndpointsService,
|
|
50
|
+
// Registers itself with the outbox dispatcher on bootstrap and
|
|
51
|
+
// does nothing but queue rows.
|
|
52
|
+
webhook_fanout_subscriber_1.WebhookFanoutSubscriber,
|
|
53
|
+
// Arms the sender. The only place this plugin touches the
|
|
54
|
+
// network, and it never holds a transaction while it does.
|
|
55
|
+
webhook_delivery_worker_1.WebhookDeliveryWorker,
|
|
56
|
+
// Registers itself with the workspace-delete registry, if the
|
|
57
|
+
// host mounted workspaces at all.
|
|
58
|
+
webhook_workspaces_purger_1.WebhookWorkspacesPurger
|
|
59
|
+
],
|
|
60
|
+
exports: [
|
|
61
|
+
webhooks_tokens_1.WEBHOOKS_CONFIG,
|
|
62
|
+
webhook_endpoints_service_1.WebhookEndpointsService,
|
|
63
|
+
webhook_endpoint_repository_1.WebhookEndpointRepository,
|
|
64
|
+
webhook_delivery_repository_1.WebhookDeliveryRepository
|
|
65
|
+
]
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
exports.WebhooksModule = WebhooksModule;
|
|
70
|
+
exports.WebhooksModule = WebhooksModule = WebhooksModule_1 = tslib_1.__decorate([
|
|
71
|
+
(0, common_1.Module)({})
|
|
72
|
+
], WebhooksModule);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** DI token for the plugin's resolved configuration. */
|
|
2
|
+
export declare const WEBHOOKS_CONFIG: unique symbol;
|
|
3
|
+
/** Injects the resolved webhooks configuration. */
|
|
4
|
+
export declare const InjectWebhooksConfig: () => ParameterDecorator;
|
|
5
|
+
//# sourceMappingURL=webhooks.tokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhooks.tokens.d.ts","sourceRoot":"","sources":["../../src/lib/webhooks.tokens.ts"],"names":[],"mappings":"AAEA,wDAAwD;AACxD,eAAO,MAAM,eAAe,eAA4B,CAAC;AAEzD,mDAAmD;AACnD,eAAO,MAAM,oBAAoB,QAAO,kBACb,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InjectWebhooksConfig = exports.WEBHOOKS_CONFIG = void 0;
|
|
4
|
+
const common_1 = require("@nestjs/common");
|
|
5
|
+
/** DI token for the plugin's resolved configuration. */
|
|
6
|
+
exports.WEBHOOKS_CONFIG = Symbol('WEBHOOKS_CONFIG');
|
|
7
|
+
/** Injects the resolved webhooks configuration. */
|
|
8
|
+
const InjectWebhooksConfig = () => (0, common_1.Inject)(exports.WEBHOOKS_CONFIG);
|
|
9
|
+
exports.InjectWebhooksConfig = InjectWebhooksConfig;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
CREATE TABLE "webhook_deliveries" (
|
|
2
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
3
|
+
"endpoint_id" uuid NOT NULL,
|
|
4
|
+
"event_id" uuid NOT NULL,
|
|
5
|
+
"event_kind" text NOT NULL,
|
|
6
|
+
"workspace_id" uuid,
|
|
7
|
+
"content_type" text,
|
|
8
|
+
"payload" jsonb NOT NULL,
|
|
9
|
+
"status" text DEFAULT 'pending' NOT NULL,
|
|
10
|
+
"attempts" integer DEFAULT 0 NOT NULL,
|
|
11
|
+
"next_attempt_at" timestamp with time zone,
|
|
12
|
+
"claimed_at" timestamp with time zone,
|
|
13
|
+
"last_status_code" integer,
|
|
14
|
+
"last_error" text,
|
|
15
|
+
"response_snippet" text,
|
|
16
|
+
"duration_ms" integer,
|
|
17
|
+
"redelivery_of" uuid,
|
|
18
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
19
|
+
"completed_at" timestamp with time zone
|
|
20
|
+
);
|
|
21
|
+
--> statement-breakpoint
|
|
22
|
+
CREATE TABLE "webhook_endpoint_workspaces" (
|
|
23
|
+
"endpoint_id" uuid NOT NULL,
|
|
24
|
+
"workspace_id" uuid NOT NULL,
|
|
25
|
+
CONSTRAINT "webhook_endpoint_workspaces_endpoint_id_workspace_id_pk" PRIMARY KEY("endpoint_id","workspace_id")
|
|
26
|
+
);
|
|
27
|
+
--> statement-breakpoint
|
|
28
|
+
CREATE TABLE "webhook_endpoints" (
|
|
29
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
30
|
+
"name" text NOT NULL,
|
|
31
|
+
"url" text NOT NULL,
|
|
32
|
+
"secret" text NOT NULL,
|
|
33
|
+
"secret_hint" text NOT NULL,
|
|
34
|
+
"enabled" boolean DEFAULT true NOT NULL,
|
|
35
|
+
"event_kinds" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
|
36
|
+
"content_types" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
|
37
|
+
"all_workspaces" boolean DEFAULT false NOT NULL,
|
|
38
|
+
"headers" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
39
|
+
"disabled_reason" text,
|
|
40
|
+
"consecutive_failures" integer DEFAULT 0 NOT NULL,
|
|
41
|
+
"created_by" uuid,
|
|
42
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
43
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
44
|
+
);
|
|
45
|
+
--> statement-breakpoint
|
|
46
|
+
ALTER TABLE "webhook_deliveries" ADD CONSTRAINT "webhook_deliveries_endpoint_id_webhook_endpoints_id_fk" FOREIGN KEY ("endpoint_id") REFERENCES "public"."webhook_endpoints"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
47
|
+
ALTER TABLE "webhook_endpoint_workspaces" ADD CONSTRAINT "webhook_endpoint_workspaces_endpoint_id_webhook_endpoints_id_fk" FOREIGN KEY ("endpoint_id") REFERENCES "public"."webhook_endpoints"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
48
|
+
CREATE UNIQUE INDEX "webhook_deliveries_endpoint_event_unique" ON "webhook_deliveries" USING btree ("endpoint_id","event_id") WHERE "webhook_deliveries"."redelivery_of" is null;--> statement-breakpoint
|
|
49
|
+
CREATE INDEX "webhook_deliveries_claimable_idx" ON "webhook_deliveries" USING btree ("next_attempt_at") WHERE "webhook_deliveries"."status" in ('pending', 'failed');--> statement-breakpoint
|
|
50
|
+
CREATE INDEX "webhook_deliveries_endpoint_idx" ON "webhook_deliveries" USING btree ("endpoint_id","created_at");--> statement-breakpoint
|
|
51
|
+
CREATE INDEX "webhook_deliveries_completed_idx" ON "webhook_deliveries" USING btree ("completed_at");--> statement-breakpoint
|
|
52
|
+
CREATE INDEX "webhook_endpoint_workspaces_workspace_idx" ON "webhook_endpoint_workspaces" USING btree ("workspace_id");--> statement-breakpoint
|
|
53
|
+
CREATE INDEX "webhook_endpoints_enabled_idx" ON "webhook_endpoints" USING btree ("enabled");
|