@hookflo/tern 4.0.2 → 4.0.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/dist/index.js +48 -50
- package/dist/upstash/queue.js +70 -12
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -48,7 +48,7 @@ class WebhookVerificationService {
|
|
|
48
48
|
// Ensure the platform is set correctly in the result
|
|
49
49
|
if (result.isValid) {
|
|
50
50
|
result.platform = config.platform;
|
|
51
|
-
result.eventId = this.resolveCanonicalEventId(config.platform, result.metadata, result.payload);
|
|
51
|
+
result.eventId = this.resolveCanonicalEventId(config.platform, result.metadata, result.payload) ?? undefined;
|
|
52
52
|
if (config.normalize) {
|
|
53
53
|
result.payload = (0, simple_1.normalizePayload)(config.platform, result.payload, config.normalize);
|
|
54
54
|
}
|
|
@@ -134,6 +134,8 @@ class WebhookVerificationService {
|
|
|
134
134
|
}
|
|
135
135
|
static resolveCanonicalEventId(platform, metadata, payload) {
|
|
136
136
|
const rawId = this.resolveRawEventId(platform, metadata, payload);
|
|
137
|
+
if (!rawId)
|
|
138
|
+
return null;
|
|
137
139
|
return `${platform}_${rawId}`;
|
|
138
140
|
}
|
|
139
141
|
static pickString(...candidates) {
|
|
@@ -149,56 +151,52 @@ class WebhookVerificationService {
|
|
|
149
151
|
return undefined;
|
|
150
152
|
}
|
|
151
153
|
static resolveRawEventId(platform, metadata, payload) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return this.pickString(payload?.request_id, metadata?.id);
|
|
189
|
-
case 'grafana':
|
|
190
|
-
return this.pickString(payload?.groupKey, payload?.alerts?.[0]?.fingerprint, metadata?.id);
|
|
191
|
-
case 'doppler':
|
|
192
|
-
return this.pickString(payload?.event?.id, metadata?.id);
|
|
193
|
-
case 'sanity':
|
|
194
|
-
return this.pickString(payload?.transactionId, payload?._id, metadata?.id);
|
|
195
|
-
default:
|
|
196
|
-
return this.pickString(payload?.idempotency_key, payload?.event_id, payload?.webhook_id, payload?.request_id, payload?.id, payload?.data?.id, metadata?.id, metadata?.delivery, metadata?.requestId);
|
|
154
|
+
switch (platform) {
|
|
155
|
+
case 'stripe':
|
|
156
|
+
return this.pickString(payload?.request?.idempotency_key, payload?.id) || null;
|
|
157
|
+
case 'github':
|
|
158
|
+
return this.pickString(metadata?.delivery) || null;
|
|
159
|
+
case 'clerk':
|
|
160
|
+
return this.pickString(metadata?.id) || null;
|
|
161
|
+
case 'shopify':
|
|
162
|
+
return this.pickString(metadata?.id, payload?.id) || null;
|
|
163
|
+
case 'paddle':
|
|
164
|
+
return this.pickString(payload?.event_id, payload?.data?.id) || null;
|
|
165
|
+
case 'polar':
|
|
166
|
+
return this.pickString(payload?.data?.id, payload?.id) || null;
|
|
167
|
+
case 'dodopayments':
|
|
168
|
+
return this.pickString(payload?.data?.payment_id, payload?.data?.subscription_id, payload?.data?.id) || null;
|
|
169
|
+
case 'falai':
|
|
170
|
+
return this.pickString(payload?.request_id) || null;
|
|
171
|
+
case 'replicateai':
|
|
172
|
+
case 'replicate':
|
|
173
|
+
return this.pickString(payload?.id) || null;
|
|
174
|
+
case 'workos':
|
|
175
|
+
case 'sentry':
|
|
176
|
+
case 'vercel':
|
|
177
|
+
return this.pickString(payload?.id) || null;
|
|
178
|
+
case 'doppler':
|
|
179
|
+
return this.pickString(payload?.event?.id, metadata?.id) || null;
|
|
180
|
+
case 'sanity':
|
|
181
|
+
return this.pickString(payload?.transactionId, payload?._id) || null;
|
|
182
|
+
case 'razorpay':
|
|
183
|
+
return this.pickString(payload?.payload?.payment?.entity?.id, payload?.payload?.order?.entity?.id, payload?.payload?.subscription?.entity?.id) || null;
|
|
184
|
+
case 'lemonsqueezy': {
|
|
185
|
+
const dataId = this.pickString(payload?.data?.id);
|
|
186
|
+
if (!dataId)
|
|
187
|
+
return null;
|
|
188
|
+
const eventName = this.pickString(payload?.meta?.event_name);
|
|
189
|
+
return eventName ? `${eventName}_${dataId}` : null;
|
|
197
190
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
191
|
+
case 'gitlab':
|
|
192
|
+
return this.pickString(payload?.object_attributes?.id?.toString()) || null;
|
|
193
|
+
case 'grafana':
|
|
194
|
+
return this.pickString(payload?.groupKey, payload?.alerts?.[0]?.fingerprint) || null;
|
|
195
|
+
case 'woocommerce':
|
|
196
|
+
return this.pickString(payload?.id?.toString()) || null;
|
|
197
|
+
default:
|
|
198
|
+
return this.pickString(payload?.idempotency_key, payload?.event_id, payload?.webhook_id, payload?.id, metadata?.id, metadata?.delivery) || null;
|
|
199
|
+
}
|
|
202
200
|
}
|
|
203
201
|
static detectPlatform(request) {
|
|
204
202
|
const headers = request.headers;
|
package/dist/upstash/queue.js
CHANGED
|
@@ -157,9 +157,7 @@ async function resolveDeduplicationId(request, verificationResult) {
|
|
|
157
157
|
headers.get('x-github-delivery') ||
|
|
158
158
|
headers.get('idempotency-key') ||
|
|
159
159
|
headers.get('upstash-deduplication-id') ||
|
|
160
|
-
|
|
161
|
-
? verificationResult.eventId
|
|
162
|
-
: null) ||
|
|
160
|
+
verificationResult.eventId ||
|
|
163
161
|
(typeof verificationResult.metadata?.id === 'string' ? verificationResult.metadata.id : null) ||
|
|
164
162
|
payloadId ||
|
|
165
163
|
payloadRequestId ||
|
|
@@ -185,7 +183,7 @@ async function handleReceive(request, platform, secret, queueConfig, toleranceIn
|
|
|
185
183
|
metadata: verificationResult.metadata || {},
|
|
186
184
|
};
|
|
187
185
|
const deduplicationId = await resolveDeduplicationId(request, verificationResult);
|
|
188
|
-
if (process.env.NODE_ENV
|
|
186
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
189
187
|
console.log(`[tern] deduplication-id: ${deduplicationId} (platform: ${platform})`);
|
|
190
188
|
}
|
|
191
189
|
const publishPayload = {
|
|
@@ -199,11 +197,60 @@ async function handleReceive(request, platform, secret, queueConfig, toleranceIn
|
|
|
199
197
|
if (queueConfig.retries !== undefined) {
|
|
200
198
|
publishPayload.retries = queueConfig.retries;
|
|
201
199
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
200
|
+
try {
|
|
201
|
+
await client.publishJSON(publishPayload);
|
|
202
|
+
return new Response(JSON.stringify({ queued: true }), {
|
|
203
|
+
status: 200,
|
|
204
|
+
headers: { 'Content-Type': 'application/json' },
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
const status = error?.status ?? 0;
|
|
209
|
+
const errorBody = error?.body
|
|
210
|
+
? (() => {
|
|
211
|
+
try {
|
|
212
|
+
return JSON.parse(error.body);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return {};
|
|
216
|
+
}
|
|
217
|
+
})()
|
|
218
|
+
: {};
|
|
219
|
+
console.error('[tern] QStash publish failed:', {
|
|
220
|
+
status,
|
|
221
|
+
message: String(error),
|
|
222
|
+
});
|
|
223
|
+
if (status === 400) {
|
|
224
|
+
return new Response(JSON.stringify({
|
|
225
|
+
error: 'Invalid payload — could not enqueue',
|
|
226
|
+
detail: errorBody?.error,
|
|
227
|
+
}), {
|
|
228
|
+
status: 489,
|
|
229
|
+
headers: {
|
|
230
|
+
'Content-Type': 'application/json',
|
|
231
|
+
'Upstash-NonRetryable-Error': 'true',
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
if (status === 401) {
|
|
236
|
+
return new Response(JSON.stringify({
|
|
237
|
+
error: '[tern] QStash token invalid — check QSTASH_TOKEN in env',
|
|
238
|
+
}), {
|
|
239
|
+
status: 503,
|
|
240
|
+
headers: { 'Content-Type': 'application/json' },
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
if (status === 429) {
|
|
244
|
+
return new Response(JSON.stringify({ error: 'QStash rate limited — retry shortly' }), {
|
|
245
|
+
status: 503,
|
|
246
|
+
headers: { 'Content-Type': 'application/json' },
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return new Response(JSON.stringify({ error: 'Queue temporarily unavailable' }), {
|
|
250
|
+
status: 503,
|
|
251
|
+
headers: { 'Content-Type': 'application/json' },
|
|
252
|
+
});
|
|
253
|
+
}
|
|
207
254
|
}
|
|
208
255
|
async function handleProcess(request, handler, queueConfig) {
|
|
209
256
|
const signature = request.headers.get('upstash-signature');
|
|
@@ -219,11 +266,22 @@ async function handleProcess(request, handler, queueConfig) {
|
|
|
219
266
|
url: request.url,
|
|
220
267
|
});
|
|
221
268
|
if (verification === false) {
|
|
222
|
-
return
|
|
269
|
+
return nonRetryableResponse('Invalid QStash signature', 401);
|
|
223
270
|
}
|
|
224
271
|
}
|
|
225
|
-
catch {
|
|
226
|
-
|
|
272
|
+
catch (error) {
|
|
273
|
+
const message = String(error).toLowerCase();
|
|
274
|
+
if (message.includes('invalid signature') ||
|
|
275
|
+
message.includes('signature') ||
|
|
276
|
+
message.includes('unauthorized') ||
|
|
277
|
+
message.includes('forbidden')) {
|
|
278
|
+
return nonRetryableResponse('Invalid QStash signature', 401);
|
|
279
|
+
}
|
|
280
|
+
console.error('[tern] QStash signature verification error:', String(error));
|
|
281
|
+
return new Response(JSON.stringify({ error: 'Signature verification temporarily unavailable' }), {
|
|
282
|
+
status: 503,
|
|
283
|
+
headers: { 'Content-Type': 'application/json' },
|
|
284
|
+
});
|
|
227
285
|
}
|
|
228
286
|
if (!handler) {
|
|
229
287
|
return nonRetryableResponse('Queue processing requires a handler function');
|
package/package.json
CHANGED