11import crypto from 'crypto'
22import { createLogger } from '@sim/logger'
3- import { NextResponse } from 'next/server'
43import { safeCompare } from '@/lib/core/security/encryption'
54import { generateId } from '@/lib/core/utils/uuid'
65import { getNotificationUrl , getProviderConfig } from '@/lib/webhooks/provider-subscription-utils'
76import type {
8- AuthContext ,
97 DeleteSubscriptionContext ,
108 EventMatchContext ,
119 FormatInputContext ,
@@ -14,6 +12,7 @@ import type {
1412 SubscriptionResult ,
1513 WebhookProviderHandler ,
1614} from '@/lib/webhooks/providers/types'
15+ import { createHmacVerifier } from '@/lib/webhooks/providers/utils'
1716
1817const logger = createLogger ( 'WebhookProvider:Linear' )
1918
@@ -42,73 +41,16 @@ function validateLinearSignature(secret: string, signature: string, body: string
4241 }
4342}
4443
45- const LINEAR_WEBHOOK_TIMESTAMP_SKEW_MS = 5 * 60 * 1000
46-
4744export const linearHandler : WebhookProviderHandler = {
48- async verifyAuth ( {
49- request,
50- rawBody,
51- requestId,
52- providerConfig,
53- } : AuthContext ) : Promise < NextResponse | null > {
54- const secret = providerConfig . webhookSecret as string | undefined
55- if ( ! secret ) {
56- return null
57- }
58-
59- const signature = request . headers . get ( 'Linear-Signature' )
60- if ( ! signature ) {
61- logger . warn ( `[${ requestId } ] Linear webhook missing signature header` )
62- return new NextResponse ( 'Unauthorized - Missing Linear signature' , { status : 401 } )
63- }
64-
65- if ( ! validateLinearSignature ( secret , signature , rawBody ) ) {
66- logger . warn ( `[${ requestId } ] Linear signature verification failed` )
67- return new NextResponse ( 'Unauthorized - Invalid Linear signature' , { status : 401 } )
68- }
69-
70- try {
71- const parsed = JSON . parse ( rawBody ) as Record < string , unknown >
72- const ts = parsed . webhookTimestamp
73- if ( typeof ts !== 'number' || ! Number . isFinite ( ts ) ) {
74- logger . warn ( `[${ requestId } ] Linear webhookTimestamp missing or invalid` )
75- return new NextResponse ( 'Unauthorized - Invalid webhook timestamp' , {
76- status : 401 ,
77- } )
78- }
79-
80- if ( Math . abs ( Date . now ( ) - ts ) > LINEAR_WEBHOOK_TIMESTAMP_SKEW_MS ) {
81- logger . warn (
82- `[${ requestId } ] Linear webhookTimestamp outside allowed skew (${ LINEAR_WEBHOOK_TIMESTAMP_SKEW_MS } ms)`
83- )
84- return new NextResponse ( 'Unauthorized - Webhook timestamp skew too large' , {
85- status : 401 ,
86- } )
87- }
88- } catch ( error ) {
89- logger . warn (
90- `[${ requestId } ] Linear webhook body parse failed after signature verification` ,
91- error
92- )
93- return new NextResponse ( 'Unauthorized - Invalid webhook body' , { status : 401 } )
94- }
95-
96- return null
97- } ,
45+ verifyAuth : createHmacVerifier ( {
46+ configKey : 'webhookSecret' ,
47+ headerName : 'Linear-Signature' ,
48+ validateFn : validateLinearSignature ,
49+ providerLabel : 'Linear' ,
50+ } ) ,
9851
9952 async formatInput ( { body } : FormatInputContext ) : Promise < FormatInputResult > {
10053 const b = body as Record < string , unknown >
101- const rawActor = b . actor
102- let actor : unknown = null
103- if ( rawActor && typeof rawActor === 'object' && ! Array . isArray ( rawActor ) ) {
104- const a = rawActor as Record < string , unknown >
105- const { type : linearActorType , ...rest } = a
106- actor = {
107- ...rest ,
108- actorType : typeof linearActorType === 'string' ? linearActorType : null ,
109- }
110- }
111-
11254 return {
11355 input : {
11456 action : b . action || '' ,
@@ -117,8 +59,7 @@ export const linearHandler: WebhookProviderHandler = {
11759 webhookTimestamp : b . webhookTimestamp || 0 ,
11860 organizationId : b . organizationId || '' ,
11961 createdAt : b . createdAt || '' ,
120- url : typeof b . url === 'string' ? b . url : '' ,
121- actor,
62+ actor : b . actor || null ,
12263 data : b . data || null ,
12364 updatedFrom : b . updatedFrom || null ,
12465 } ,
@@ -167,20 +108,6 @@ export const linearHandler: WebhookProviderHandler = {
167108
168109 const notificationUrl = getNotificationUrl ( ctx . webhook )
169110 const webhookSecret = generateId ( )
170- const teamId = config . teamId as string | undefined
171-
172- const input : Record < string , unknown > = {
173- url : notificationUrl ,
174- resourceTypes,
175- secret : webhookSecret ,
176- enabled : true ,
177- }
178-
179- if ( teamId ) {
180- input . teamId = teamId
181- } else {
182- input . allPublicTeams = true
183- }
184111
185112 try {
186113 const response = await fetch ( 'https://api.linear.app/graphql' , {
@@ -196,7 +123,14 @@ export const linearHandler: WebhookProviderHandler = {
196123 webhook { id enabled }
197124 }
198125 }` ,
199- variables : { input } ,
126+ variables : {
127+ input : {
128+ url : notificationUrl ,
129+ resourceTypes,
130+ secret : webhookSecret ,
131+ enabled : true ,
132+ } ,
133+ } ,
200134 } ) ,
201135 } )
202136
@@ -219,12 +153,6 @@ export const linearHandler: WebhookProviderHandler = {
219153 }
220154
221155 const externalId = result . webhook ?. id
222- if ( typeof externalId !== 'string' || ! externalId . trim ( ) ) {
223- throw new Error (
224- 'Linear webhook was created but the API response did not include a webhook id.'
225- )
226- }
227-
228156 logger . info (
229157 `[${ ctx . requestId } ] Created Linear webhook ${ externalId } for webhook ${ ctx . webhook . id } `
230158 )
@@ -293,4 +221,13 @@ export const linearHandler: WebhookProviderHandler = {
293221 } )
294222 }
295223 } ,
224+
225+ extractIdempotencyId ( body : unknown ) {
226+ const obj = body as Record < string , unknown >
227+ const data = obj . data as Record < string , unknown > | undefined
228+ if ( obj . action && data ?. id ) {
229+ return `${ obj . action } :${ data . id } `
230+ }
231+ return null
232+ } ,
296233}
0 commit comments