From 43961efef03321d3862ef5948f019ea95452507b Mon Sep 17 00:00:00 2001 From: yuvanvk Date: Wed, 5 Aug 2026 10:00:29 +0530 Subject: [PATCH 1/4] fix(twitterapiio): fail closed when webhook secret is missing --- packages/twitterapiio/index.ts | 7 ++++++- packages/twitterapiio/webhooks/types.ts | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/twitterapiio/index.ts b/packages/twitterapiio/index.ts index 49225d56a..89b32f2ca 100644 --- a/packages/twitterapiio/index.ts +++ b/packages/twitterapiio/index.ts @@ -723,7 +723,12 @@ export function twitterapiio( if (source === 'webhook') { const res = await ctx.keys.get_webhook_signature(); - return res ?? ''; + if (!res) { + throw new Error( + '[auth-missing:twitterapiio:webhook_signature]: TwitterApiIO webhook signature is missing', + ); + } + return res; } if (source === 'endpoint' && options.key) { diff --git a/packages/twitterapiio/webhooks/types.ts b/packages/twitterapiio/webhooks/types.ts index 64d8fb803..552eb0c03 100644 --- a/packages/twitterapiio/webhooks/types.ts +++ b/packages/twitterapiio/webhooks/types.ts @@ -85,8 +85,8 @@ export function verifyTwitterApiIOWebhookSignature( | undefined; if (!signature) { - // If no secret is configured, treat as valid - if (!secret) return { valid: true }; + // If no secret is configured, treat as invalid + if (!secret) return { valid: false, error: 'Missing webhook secret' }; return { valid: false, error: 'Missing x-twitterapiio-signature header' }; } From 0bb8391933897de495d91c0f3804c480e152299a Mon Sep 17 00:00:00 2001 From: yuvanvk Date: Wed, 5 Aug 2026 10:01:12 +0530 Subject: [PATCH 2/4] test(twitterapiio): add webhook tests for signature verification --- packages/twitterapiio/webhook.test.ts | 58 +++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 packages/twitterapiio/webhook.test.ts diff --git a/packages/twitterapiio/webhook.test.ts b/packages/twitterapiio/webhook.test.ts new file mode 100644 index 000000000..bf0807c1e --- /dev/null +++ b/packages/twitterapiio/webhook.test.ts @@ -0,0 +1,58 @@ +import { verifyHmacSignature } from 'corsair/http'; +import { verifyTwitterApiIOWebhookSignature } from './webhooks/types'; + +jest.mock('corsair/http', () => ({ + verifyHmacSignature: jest.fn(), +})); + +const mockedVerify = verifyHmacSignature as jest.Mock; + +describe('TwitterApiIO Webhooks', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('Signature Verification', () => { + it('Should verify correct signature via HMAC SHA256', async () => { + mockedVerify.mockReturnValue(true); + + const mockRequest: any = { + rawBody: 'mock-body-string', + headers: { + 'x-twitterapiio-signature': 'sha256=valid-signature', + }, + }; + + const result = verifyTwitterApiIOWebhookSignature( + mockRequest, + 'my-app-secret', + ); + + expect(mockedVerify).toHaveBeenCalledWith( + 'mock-body-string', + 'my-app-secret', + 'sha256=valid-signature', + 'sha256', + ); + expect(result.valid).toBe(true); + }); + + it('should reject when verification fails', async () => { + mockedVerify.mockReturnValue(false); + + const mockRequest: any = { + rawBody: 'mock-body-string', + headers: { + 'x-twitterapiio-signature': 'sha256=invalid-signature', + }, + }; + + const result = verifyTwitterApiIOWebhookSignature( + mockRequest, + 'my-app-secret', + ); + + expect(result.valid).toBe(false); + }); + }); +}); From d0efd0699faafac451b5b89201827a5f83e18f73 Mon Sep 17 00:00:00 2001 From: yuvanvk Date: Wed, 5 Aug 2026 10:16:21 +0530 Subject: [PATCH 3/4] test(twitterapiio): add tests cover more cases --- packages/twitterapiio/webhook.test.ts | 31 +++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/twitterapiio/webhook.test.ts b/packages/twitterapiio/webhook.test.ts index bf0807c1e..f788ae757 100644 --- a/packages/twitterapiio/webhook.test.ts +++ b/packages/twitterapiio/webhook.test.ts @@ -54,5 +54,36 @@ describe('TwitterApiIO Webhooks', () => { expect(result.valid).toBe(false); }); + + it('Should reject when webhook secret is missing', () => { + mockedVerify.mockReturnValue(false); + + const mockRequest: any = { + rawBody: 'mock-body-string', + headers: {}, + }; + + const result = verifyTwitterApiIOWebhookSignature(mockRequest, ''); + + expect(result.valid).toBe(false); + expect(result.error).toMatch('Missing webhook secret'); + }); + + it('Should reject when signature is missing with configured secret', () => { + mockedVerify.mockReturnValue(false); + + const mockRequest: any = { + rawBody: 'mock-body-string', + headers: {}, + }; + + const result = verifyTwitterApiIOWebhookSignature( + mockRequest, + 'my-app-secret', + ); + + expect(result.valid).toBe(false); + expect(result.error).toMatch('Missing x-twitterapiio-signature header'); + }); }); }); From aa1c16aa2843811702fc0eec93cfa19526f9f878 Mon Sep 17 00:00:00 2001 From: yuvanvk Date: Wed, 5 Aug 2026 10:42:43 +0530 Subject: [PATCH 4/4] refactor(type): use WebhookRequest type instead of any --- packages/twitterapiio/webhook.test.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/twitterapiio/webhook.test.ts b/packages/twitterapiio/webhook.test.ts index f788ae757..60fb2d955 100644 --- a/packages/twitterapiio/webhook.test.ts +++ b/packages/twitterapiio/webhook.test.ts @@ -1,3 +1,4 @@ +import type { WebhookRequest } from 'corsair/core'; import { verifyHmacSignature } from 'corsair/http'; import { verifyTwitterApiIOWebhookSignature } from './webhooks/types'; @@ -16,7 +17,8 @@ describe('TwitterApiIO Webhooks', () => { it('Should verify correct signature via HMAC SHA256', async () => { mockedVerify.mockReturnValue(true); - const mockRequest: any = { + const mockRequest: WebhookRequest = { + payload: {}, rawBody: 'mock-body-string', headers: { 'x-twitterapiio-signature': 'sha256=valid-signature', @@ -40,7 +42,8 @@ describe('TwitterApiIO Webhooks', () => { it('should reject when verification fails', async () => { mockedVerify.mockReturnValue(false); - const mockRequest: any = { + const mockRequest: WebhookRequest = { + payload: {}, rawBody: 'mock-body-string', headers: { 'x-twitterapiio-signature': 'sha256=invalid-signature', @@ -58,7 +61,8 @@ describe('TwitterApiIO Webhooks', () => { it('Should reject when webhook secret is missing', () => { mockedVerify.mockReturnValue(false); - const mockRequest: any = { + const mockRequest: WebhookRequest = { + payload: {}, rawBody: 'mock-body-string', headers: {}, }; @@ -72,7 +76,8 @@ describe('TwitterApiIO Webhooks', () => { it('Should reject when signature is missing with configured secret', () => { mockedVerify.mockReturnValue(false); - const mockRequest: any = { + const mockRequest: WebhookRequest = { + payload: {}, rawBody: 'mock-body-string', headers: {}, };