Skip to content
Merged
7 changes: 6 additions & 1 deletion packages/twitterapiio/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,12 @@ export function twitterapiio<const T extends TwitterApiIOPluginOptions>(

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) {
Expand Down
94 changes: 94 additions & 0 deletions packages/twitterapiio/webhook.test.ts
Original file line number Diff line number Diff line change
@@ -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<unknown> = {
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<unknown> = {
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<unknown> = {
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<unknown> = {
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');
});
});
});
4 changes: 2 additions & 2 deletions packages/twitterapiio/webhooks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' };
}

Expand Down
Loading