@hazem3500/palette 0.9.111 → 0.9.112
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/NODE.md +65 -0
- package/dist/chunk-CF4GJN3B.js +1 -0
- package/dist/index.js +1 -1
- package/dist/node.mjs +147 -77
- package/dist/postgres/008_webhook_outbox.sql +14 -0
- package/dist/react.js +1 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.js +141 -77
- package/dist/setup.js +1 -1
- package/dist/{snapshot-3PSQKXUV.js → snapshot-YYM5GW6I.js} +1 -1
- package/dist/snapshot.d.ts +3 -2
- package/package.json +1 -1
- package/server/index.ts +70 -71
- package/server/migrations/0022_webhook_outbox.sql +14 -0
- package/server/node-webhooks.ts +5 -0
- package/server/setup-client.txt +52 -52
- package/server/webhooks.ts +98 -13
- package/dist/chunk-DDYHX4RZ.js +0 -1
package/server/webhooks.ts
CHANGED
|
@@ -19,16 +19,11 @@ export type WebhookEvent = {
|
|
|
19
19
|
selector: string;
|
|
20
20
|
author: string;
|
|
21
21
|
body: string | null;
|
|
22
|
+
snapshotStatus?: "pending" | "skipped";
|
|
22
23
|
};
|
|
23
24
|
|
|
24
|
-
type WebhookEnv = {
|
|
25
|
-
DB:
|
|
26
|
-
prepare(sql: string): {
|
|
27
|
-
bind(...values: unknown[]): {
|
|
28
|
-
first<T>(): Promise<T | null>;
|
|
29
|
-
};
|
|
30
|
-
};
|
|
31
|
-
};
|
|
25
|
+
export type WebhookEnv = {
|
|
26
|
+
DB: D1Database;
|
|
32
27
|
PALETTE_WEBHOOK_KEYS?: string;
|
|
33
28
|
PALETTE_WEBHOOK_SEND?: (
|
|
34
29
|
url: string,
|
|
@@ -163,9 +158,11 @@ export async function signWebhook(secret: string, timestamp: number, body: strin
|
|
|
163
158
|
export async function dispatchWebhook(
|
|
164
159
|
env: WebhookEnv,
|
|
165
160
|
project: string,
|
|
166
|
-
event: WebhookEvent
|
|
161
|
+
event: WebhookEvent,
|
|
162
|
+
destination?: { webhook_url: string; webhook_secret: string; webhook_events: string; webhook_enabled: number },
|
|
163
|
+
storedBody?: string
|
|
167
164
|
) {
|
|
168
|
-
const row = await env.DB.prepare(
|
|
165
|
+
const row = destination ?? await env.DB.prepare(
|
|
169
166
|
"SELECT webhook_url,webhook_secret,webhook_events,webhook_enabled FROM project_settings WHERE project=?"
|
|
170
167
|
)
|
|
171
168
|
.bind(project)
|
|
@@ -179,8 +176,8 @@ export async function dispatchWebhook(
|
|
|
179
176
|
const events = JSON.parse(row.webhook_events ?? "[]") as unknown;
|
|
180
177
|
if (!Array.isArray(events) || !events.includes(event.event)) return;
|
|
181
178
|
if (!env.PALETTE_WEBHOOK_SEND) return;
|
|
182
|
-
const body = JSON.stringify(event);
|
|
183
|
-
if (encoder.encode(body).length > 16 * 1024)
|
|
179
|
+
const body = storedBody ?? JSON.stringify(event);
|
|
180
|
+
if (encoder.encode(body).length > 16 * 1024) throw Error("Webhook payload exceeds delivery limit");
|
|
184
181
|
const timestamp = Math.floor(Date.now() / 1000);
|
|
185
182
|
const secret = await decryptWebhookSecret(
|
|
186
183
|
env.PALETTE_WEBHOOK_KEYS,
|
|
@@ -188,7 +185,7 @@ export async function dispatchWebhook(
|
|
|
188
185
|
row.webhook_secret
|
|
189
186
|
);
|
|
190
187
|
const signature = await signWebhook(secret, timestamp, body);
|
|
191
|
-
await env.PALETTE_WEBHOOK_SEND(row.webhook_url, body, {
|
|
188
|
+
await env.PALETTE_WEBHOOK_SEND(validateWebhookUrl(row.webhook_url), body, {
|
|
192
189
|
"X-Palette-Delivery": event.id,
|
|
193
190
|
"X-Palette-Event": event.event,
|
|
194
191
|
"X-Palette-Signature": signature,
|
|
@@ -208,3 +205,91 @@ function fromBase64(value: string) {
|
|
|
208
205
|
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
209
206
|
return Uint8Array.from(atob(normalized), (char) => char.charCodeAt(0));
|
|
210
207
|
}
|
|
208
|
+
|
|
209
|
+
export function enqueueWebhook(
|
|
210
|
+
db: D1Database,
|
|
211
|
+
project: string,
|
|
212
|
+
event: WebhookEvent,
|
|
213
|
+
commentId: string,
|
|
214
|
+
) {
|
|
215
|
+
return db
|
|
216
|
+
.prepare(
|
|
217
|
+
`INSERT INTO webhook_outbox(id,project,body,attempts,next_attempt,claim_until,claim_token,state,url,secret) SELECT ?,?,?,0,?,0,'','pending',webhook_url,webhook_secret FROM project_settings WHERE EXISTS (SELECT 1 FROM comments WHERE id=?) AND project=? AND webhook_enabled=1 AND webhook_url IS NOT NULL AND webhook_secret IS NOT NULL AND webhook_events LIKE ?`,
|
|
218
|
+
)
|
|
219
|
+
.bind(
|
|
220
|
+
event.id,
|
|
221
|
+
project,
|
|
222
|
+
JSON.stringify(event),
|
|
223
|
+
Date.now(),
|
|
224
|
+
commentId,
|
|
225
|
+
project,
|
|
226
|
+
`%"${event.event}"%`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** A bounded drain; run periodically even when no requests arrive. */
|
|
231
|
+
export async function drainWebhooks(env: WebhookEnv) {
|
|
232
|
+
if (!env.PALETTE_WEBHOOK_SEND) return;
|
|
233
|
+
for (let count = 0; count < 20; count++) {
|
|
234
|
+
const now = Date.now();
|
|
235
|
+
const token = crypto.randomUUID();
|
|
236
|
+
const row = await env.DB.prepare(
|
|
237
|
+
`UPDATE webhook_outbox SET claim_token=?,claim_until=?,attempts=attempts+1 WHERE id=(SELECT id FROM webhook_outbox WHERE state='pending' AND next_attempt<=? AND claim_until<=? AND attempts<6 ORDER BY next_attempt,id LIMIT 1) AND state='pending' AND claim_until<=? AND attempts<6 RETURNING id,project,body,attempts,url,secret`,
|
|
238
|
+
)
|
|
239
|
+
.bind(token, now + 30000, now, now, now)
|
|
240
|
+
.first<{
|
|
241
|
+
id: string;
|
|
242
|
+
project: string;
|
|
243
|
+
body: string;
|
|
244
|
+
attempts: number;
|
|
245
|
+
url: string;
|
|
246
|
+
secret: string;
|
|
247
|
+
}>();
|
|
248
|
+
if (!row) break;
|
|
249
|
+
let state = "delivered";
|
|
250
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
251
|
+
try {
|
|
252
|
+
await Promise.race([
|
|
253
|
+
dispatchWebhook(
|
|
254
|
+
env,
|
|
255
|
+
row.project,
|
|
256
|
+
JSON.parse(row.body) as WebhookEvent,
|
|
257
|
+
{
|
|
258
|
+
webhook_url: row.url,
|
|
259
|
+
webhook_secret: row.secret,
|
|
260
|
+
webhook_events: JSON.stringify([
|
|
261
|
+
(JSON.parse(row.body) as WebhookEvent).event,
|
|
262
|
+
]),
|
|
263
|
+
webhook_enabled: 1,
|
|
264
|
+
},
|
|
265
|
+
row.body,
|
|
266
|
+
),
|
|
267
|
+
new Promise<never>((_, reject) => {
|
|
268
|
+
timer = setTimeout(
|
|
269
|
+
() => reject(Error("Webhook send timed out")),
|
|
270
|
+
5000,
|
|
271
|
+
);
|
|
272
|
+
}),
|
|
273
|
+
]);
|
|
274
|
+
} catch {
|
|
275
|
+
state = row.attempts >= 6 ? "exhausted" : "pending";
|
|
276
|
+
} finally {
|
|
277
|
+
clearTimeout(timer);
|
|
278
|
+
}
|
|
279
|
+
await env.DB.prepare(
|
|
280
|
+
`UPDATE webhook_outbox SET state=?,next_attempt=?,claim_until=0,claim_token='' WHERE id=? AND claim_token=?`,
|
|
281
|
+
)
|
|
282
|
+
.bind(
|
|
283
|
+
state,
|
|
284
|
+
Date.now() + 10000 * 2 ** (row.attempts - 1),
|
|
285
|
+
row.id,
|
|
286
|
+
token,
|
|
287
|
+
)
|
|
288
|
+
.run();
|
|
289
|
+
}
|
|
290
|
+
await env.DB.prepare(
|
|
291
|
+
`UPDATE webhook_outbox SET state='exhausted' WHERE state='pending' AND attempts>=6 AND claim_until<=?`,
|
|
292
|
+
)
|
|
293
|
+
.bind(Date.now())
|
|
294
|
+
.run();
|
|
295
|
+
}
|