From 10ccc95dd262ab07441cdaeb0b4fddaf51f0c9e0 Mon Sep 17 00:00:00 2001 From: Takuya Kosugiyama Date: Tue, 9 Sep 2025 21:27:08 +0900 Subject: [PATCH] Support options request for CORS pre-flight --- src/server/server.test.ts | 57 +++++++++++++++++++++++++++++++++++++-- src/server/server.ts | 7 +++-- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/src/server/server.test.ts b/src/server/server.test.ts index e2d2737..10dea70 100644 --- a/src/server/server.test.ts +++ b/src/server/server.test.ts @@ -38,8 +38,8 @@ describe('server', () => { }); }); - describe('POST /hook/*', () => { - it('should return 404 when no client is connected', async () => { + describe('HTTP /hook/*', () => { + it('should return 404 when no client is connected (POST)', async () => { const response = await request(server) .post('/hook/test-path') .send({ test: 'data' }) @@ -48,6 +48,14 @@ describe('server', () => { expect(response.text).toContain('No matching client found'); }); + it('should return 404 when no client is connected (OPTIONS)', async () => { + const response = await request(server) + .options('/hook/test-path') + .expect(404); + + expect(response.text).toContain('No matching client found'); + }); + it('should relay request to connected client', (done) => { const ws = new WebSocket(`ws://localhost:${port}?clientId=test-client`); @@ -94,6 +102,51 @@ describe('server', () => { }); }); + it('should relay OPTIONS request to connected client', (done) => { + const ws = new WebSocket(`ws://localhost:${port}?clientId=test-client`); + + ws.on('message', (data) => { + const message = JSON.parse(data.toString()); + + if (message.kind === 'challenge') { + const challengeMessage = message as WebSocketChallengeMessage; + const hmac = createHmac('sha256', challengePassphrase) + .update(challengeMessage.nonce) + .digest('hex'); + + const response: WebSocketChallengeResponse = { + kind: 'challenge-response', + clientId: 'test-client', + hmac, + }; + ws.send(JSON.stringify(response)); + } else if (message.kind === 'challenge-result' && message.success) { + request(server) + .options('/hook/test-path') + .end(() => {}); + } else if (message.kind === 'http') { + const httpMessage = message as WebSocketHTTPMessage; + expect(httpMessage.path).toBe('/test-path'); + expect(httpMessage.method).toBe('OPTIONS'); + + const response: WebSocketHTTPResponse = { + kind: 'http', + clientId: 'test-client', + messageId: httpMessage.messageId, + headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS' }, + status: 200, + body: '', + }; + ws.send(JSON.stringify(response)); + + setTimeout(() => { + ws.close(); + done(); + }, 100); + } + }); + }); + it('should handle wildcard client', (done) => { const ws = new WebSocket(`ws://localhost:${port}?clientId=test-client`); diff --git a/src/server/server.ts b/src/server/server.ts index acf2d31..26f1455 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -234,7 +234,7 @@ export function createServer(logger: Logger, challengePassphrase: string): { }); }); - app.post('/hook/*', async ( + const hookHandler = async ( req: Request, res: Response, ) => { @@ -315,7 +315,10 @@ export function createServer(logger: Logger, challengePassphrase: string): { logger.error(err, 'Error while receiving data'); res.status(500).send('Error receiving data'); }); - }); + }; + + app.post('/hook/*', hookHandler); + app.options('/hook/*', hookHandler); const CALLBACK_REGISTRATION_EXPIRATION_MS = 30 * 1000; // 30 seconds app.post('/callback/oneshot/register', express.json(), async (req: Request, res: Response) => {