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/webhook.test.ts b/packages/twitterapiio/webhook.test.ts new file mode 100644 index 000000000..60fb2d955 --- /dev/null +++ b/packages/twitterapiio/webhook.test.ts @@ -0,0 +1,94 @@ +import type { WebhookRequest } from 'corsair/core'; +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: WebhookRequest = { + payload: {}, + 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: WebhookRequest = { + payload: {}, + rawBody: 'mock-body-string', + headers: { + 'x-twitterapiio-signature': 'sha256=invalid-signature', + }, + }; + + const result = verifyTwitterApiIOWebhookSignature( + mockRequest, + 'my-app-secret', + ); + + expect(result.valid).toBe(false); + }); + + it('Should reject when webhook secret is missing', () => { + mockedVerify.mockReturnValue(false); + + const mockRequest: WebhookRequest = { + payload: {}, + 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: WebhookRequest = { + payload: {}, + 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'); + }); + }); +}); 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' }; }