Skip to content

Commit 01ac230

Browse files
committed
fix(triggers): close two Greptile-flagged crash/collision gaps
instantly: the timestamp-present branch was fixed for the email_id collision risk, but the no-timestamp fallback still keyed on bare email_id — same collision risk, unfixed. Return null instead so dedup is skipped rather than risking a false collision between repeat opens/ clicks/replies on the same email. linear: extractIdempotencyId cast `body` straight to a Record without checking it was actually an object first, so a JSON `null` body (no Linear-Delivery header available) would throw on `b.type` and turn a malformed payload into a webhook 500 instead of a clean skip.
1 parent 77dd673 commit 01ac230

4 files changed

Lines changed: 18 additions & 6 deletions

File tree

apps/sim/lib/webhooks/providers/instantly.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,12 +176,12 @@ describe('Instantly webhook provider', () => {
176176
expect(id).toBe('instantly:email_sent:email-123:2026-07-08T12:00:00.000Z')
177177
})
178178

179-
it('falls back to the bare email_id when timestamp is missing', () => {
179+
it('returns null when email_id is present but timestamp is missing, rather than risk a false collision', () => {
180180
const id = instantlyHandler.extractIdempotencyId!({
181181
event_type: 'email_sent',
182182
email_id: 'email-123',
183183
})
184-
expect(id).toBe('instantly:email_sent:email-123')
184+
expect(id).toBeNull()
185185
})
186186

187187
it('falls back to a content-based key without an email_id', () => {

apps/sim/lib/webhooks/providers/instantly.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@ export const instantlyHandler: WebhookProviderHandler = {
6464
* a thread), so email_id alone would collapse those distinct, legitimate
6565
* deliveries into one idempotency slot. `timestamp` is fixed per event
6666
* occurrence (not regenerated on retry), so appending it keeps the key
67-
* both retry-stable and unique per occurrence.
67+
* both retry-stable and unique per occurrence — without it there's no way
68+
* to distinguish occurrences, so dedup is skipped rather than risking a
69+
* false collision.
6870
*/
6971
extractIdempotencyId(body: unknown): string | null {
7072
if (!isRecordLike(body)) return null
@@ -75,9 +77,7 @@ export const instantlyHandler: WebhookProviderHandler = {
7577
const timestamp = typeof body.timestamp === 'string' ? body.timestamp : undefined
7678
const emailId = typeof body.email_id === 'string' ? body.email_id : undefined
7779
if (emailId) {
78-
return timestamp
79-
? `instantly:${eventType}:${emailId}:${timestamp}`
80-
: `instantly:${eventType}:${emailId}`
80+
return timestamp ? `instantly:${eventType}:${emailId}:${timestamp}` : null
8181
}
8282

8383
const campaignId = typeof body.campaign_id === 'string' ? body.campaign_id : undefined

apps/sim/lib/webhooks/providers/linear.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,5 +141,15 @@ describe('Linear webhook provider', () => {
141141
const key = linearHandler.extractIdempotencyId!({ action: 'create', data: { id: 'x' } })
142142
expect(key).toBeNull()
143143
})
144+
145+
it('returns null instead of throwing when the body is null', () => {
146+
expect(linearHandler.extractIdempotencyId!(null)).toBeNull()
147+
})
148+
149+
it('returns null instead of throwing when the body is a non-object', () => {
150+
expect(linearHandler.extractIdempotencyId!('not an object')).toBeNull()
151+
expect(linearHandler.extractIdempotencyId!(42)).toBeNull()
152+
expect(linearHandler.extractIdempotencyId!(['array'])).toBeNull()
153+
})
144154
})
145155
})

apps/sim/lib/webhooks/providers/linear.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ export const linearHandler: WebhookProviderHandler = {
157157
* not a request-time timestamp, so retried deliveries of the same event still collapse.
158158
*/
159159
extractIdempotencyId(body: unknown): string | null {
160+
if (!body || typeof body !== 'object' || Array.isArray(body)) return null
161+
160162
const b = body as Record<string, unknown>
161163
const type = typeof b.type === 'string' ? b.type : undefined
162164
const action = typeof b.action === 'string' ? b.action : undefined

0 commit comments

Comments
 (0)