1- import * as crypto from 'crypto'
1+ import { randomUUID } from 'crypto'
22import { db } from '@sim/db'
33import { idempotencyKey } from '@sim/db/schema'
44import { and , eq } from 'drizzle-orm'
@@ -451,110 +451,26 @@ export class IdempotencyService {
451451
452452 /**
453453 * Create an idempotency key from a webhook payload following RFC best practices
454- * Priority order:
455- * 1. Standard webhook headers (webhook-id, x-webhook-id, etc.)
456- * 2. Event/message IDs from payload
457- * 3. Deterministic hash of stable payload fields (excluding timestamps)
454+ * Standard webhook headers (webhook-id, x-webhook-id, etc.)
458455 */
459- static createWebhookIdempotencyKey (
460- webhookId : string ,
461- payload : any ,
462- headers ?: Record < string , string >
463- ) : string {
464- // 1. Check for standard webhook headers (RFC compliant)
456+ static createWebhookIdempotencyKey ( webhookId : string , headers ?: Record < string , string > ) : string {
457+ const normalizedHeaders = headers
458+ ? Object . fromEntries ( Object . entries ( headers ) . map ( ( [ k , v ] ) => [ k . toLowerCase ( ) , v ] ) )
459+ : undefined
460+
465461 const webhookIdHeader =
466- headers ?. [ 'webhook-id' ] || // Standard Webhooks spec
467- headers ?. [ 'x-webhook-id' ] || // Legacy standard
468- headers ?. [ 'x-shopify-webhook-id' ] ||
469- headers ?. [ 'x-github-delivery' ] ||
470- headers ?. [ 'x-event-id' ] // Generic event ID header
462+ normalizedHeaders ?. [ 'webhook-id' ] ||
463+ normalizedHeaders ?. [ 'x-webhook-id' ] ||
464+ normalizedHeaders ?. [ 'x-shopify-webhook-id' ] ||
465+ normalizedHeaders ?. [ 'x-github-delivery' ] ||
466+ normalizedHeaders ?. [ 'x-event-id' ]
471467
472468 if ( webhookIdHeader ) {
473469 return `${ webhookId } :${ webhookIdHeader } `
474470 }
475471
476- // 2. Extract event/message IDs from payload (most reliable)
477- const payloadId =
478- payload ?. id ||
479- payload ?. event_id ||
480- payload ?. eventId ||
481- payload ?. message ?. id ||
482- payload ?. data ?. id ||
483- payload ?. object ?. id ||
484- payload ?. event ?. id
485-
486- if ( payloadId ) {
487- return `${ webhookId } :${ payloadId } `
488- }
489-
490- // 3. Create deterministic hash from stable payload fields (excluding timestamps)
491- const stablePayload = IdempotencyService . createStablePayloadForHashing ( payload )
492- const payloadHash = crypto
493- . createHash ( 'sha256' )
494- . update ( JSON . stringify ( stablePayload ) )
495- . digest ( 'hex' )
496- . substring ( 0 , 16 )
497-
498- return `${ webhookId } :${ payloadHash } `
499- }
500-
501- /**
502- * Create a stable representation of the payload for hashing by removing
503- * timestamp and other volatile fields that change between requests
504- */
505- private static createStablePayloadForHashing ( payload : any ) : any {
506- if ( ! payload || typeof payload !== 'object' ) {
507- return payload
508- }
509-
510- const volatileFields = [
511- 'timestamp' ,
512- 'created_at' ,
513- 'updated_at' ,
514- 'sent_at' ,
515- 'received_at' ,
516- 'processed_at' ,
517- 'delivered_at' ,
518- 'attempt' ,
519- 'retry_count' ,
520- 'request_id' ,
521- 'trace_id' ,
522- 'span_id' ,
523- 'delivery_id' ,
524- 'webhook_timestamp' ,
525- ]
526-
527- const cleanPayload = { ...payload }
528-
529- const removeVolatileFields = ( obj : any ) : any => {
530- if ( ! obj || typeof obj !== 'object' ) return obj
531-
532- if ( Array . isArray ( obj ) ) {
533- return obj . map ( removeVolatileFields )
534- }
535-
536- const cleaned : any = { }
537- for ( const [ key , value ] of Object . entries ( obj ) ) {
538- const lowerKey = key . toLowerCase ( )
539-
540- if ( volatileFields . some ( ( field ) => lowerKey . includes ( field ) ) ) {
541- continue
542- }
543-
544- if ( typeof value === 'string' && / ^ \d { 4 } - \d { 2 } - \d { 2 } T \d { 2 } : \d { 2 } : \d { 2 } / . test ( value ) ) {
545- continue
546- }
547- if ( typeof value === 'number' && value > 1000000000 && value < 9999999999 ) {
548- continue
549- }
550-
551- cleaned [ key ] = removeVolatileFields ( value )
552- }
553-
554- return cleaned
555- }
556-
557- return removeVolatileFields ( cleanPayload )
472+ const uniqueId = randomUUID ( )
473+ return `${ webhookId } :${ uniqueId } `
558474 }
559475}
560476
@@ -567,8 +483,3 @@ export const pollingIdempotency = new IdempotencyService({
567483 namespace : 'polling' ,
568484 ttlSeconds : 60 * 60 * 24 * 3 , // 3 days
569485} )
570-
571- export const triggerIdempotency = new IdempotencyService ( {
572- namespace : 'trigger' ,
573- ttlSeconds : 60 * 60 * 24 * 1 , // 1 day
574- } )
0 commit comments