From d5c2da18509908e206a1712a5c59e56826fc9d89 Mon Sep 17 00:00:00 2001 From: DevSolex Date: Fri, 24 Jul 2026 12:37:33 +0100 Subject: [PATCH 1/2] feat(backend): add 403 response to OpenAPI spec for GET /api/tasks/{id} (#161) The ownership check (walletpublickey header required, must match task owner) was already implemented in the route handler. This commit adds the missing HTTP 403 response to the JSDoc annotation so the generated OpenAPI spec accurately documents the auth behaviour for consumers. Closes #161 --- backend/src/api/routes/tasks.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/src/api/routes/tasks.ts b/backend/src/api/routes/tasks.ts index a04eb46..f852435 100644 --- a/backend/src/api/routes/tasks.ts +++ b/backend/src/api/routes/tasks.ts @@ -261,6 +261,12 @@ export function createTasksRouter(dispatch: DispatchFn, releasePayment: PaymentR * application/json: * schema: * $ref: '#/components/schemas/Task' + * 403: + * description: Access denied — walletpublickey header is missing or does not match the task owner + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' * 404: * description: Task not found * content: From efa9268f420b270eab4da9193ad9a71ad6896b54 Mon Sep 17 00:00:00 2001 From: DevSolex Date: Fri, 24 Jul 2026 12:45:06 +0100 Subject: [PATCH 2/2] feat: add CORS, HSTS enforcement and fix WS URL (#160) - backend/src/api/middleware/cors.ts: CORS factory with configurable ALLOWED_ORIGINS, credentials, and custom headers (walletpublickey, x-challenge, x-signature) - backend/src/api/app.ts: mount CORS middleware and HSTS header middleware (production-only, max-age=31536000 + includeSubDomains + preload) - backend/src/config/index.ts: ALLOWED_ORIGINS Zod field with default http://localhost:3000 - backend/.env.example: document ALLOWED_ORIGINS under Security section - frontend/src/hooks/useTaskMonitor.ts: replace hardcoded ws://localhost:3001 with dynamic protocol/host (wss: on HTTPS, VITE_WS_HOST override) - backend/tests/cors.test.ts: add tests for allowed origin, blocked origin, no-origin passthrough, credentials header, preflight OPTIONS, default origin fallback, HSTS in production, and HSTS absent in development --- backend/tests/cors.test.ts | 69 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/backend/tests/cors.test.ts b/backend/tests/cors.test.ts index f2e4247..62c8e4d 100644 --- a/backend/tests/cors.test.ts +++ b/backend/tests/cors.test.ts @@ -36,4 +36,73 @@ describe('CORS Middleware', () => { expect(res.status).toBe(500); // cors module calls next(new Error('Not allowed by CORS')) which causes 500 in express default error handler expect(res.text).toContain('Not allowed by CORS'); }); + + it('allows requests with no Origin header (e.g. server-to-server)', async () => { + const res = await request(app).get('/test'); + expect(res.status).toBe(200); + }); + + it('sets credentials header when origin is allowed', async () => { + const res = await request(app) + .get('/test') + .set('Origin', 'https://app.trusted.com'); + + expect(res.headers['access-control-allow-credentials']).toBe('true'); + }); + + it('responds to preflight OPTIONS with allowed methods', async () => { + const res = await request(app) + .options('/test') + .set('Origin', 'http://trusted.com') + .set('Access-Control-Request-Method', 'POST'); + + expect(res.status).toBeLessThan(300); + const methods = res.headers['access-control-allow-methods']; + expect(methods).toContain('POST'); + expect(methods).toContain('GET'); + }); + + it('falls back to http://localhost:3000 when ALLOWED_ORIGINS is unset', async () => { + jest.resetModules(); + delete process.env.ALLOWED_ORIGINS; + const corsMiddleware = require('../src/api/middleware/cors').createCorsMiddleware(); + const localApp = express(); + localApp.use(corsMiddleware); + localApp.get('/test', (_req, res) => res.json({ ok: true })); + + const res = await request(localApp) + .get('/test') + .set('Origin', 'http://localhost:3000'); + + expect(res.status).toBe(200); + expect(res.headers['access-control-allow-origin']).toBe('http://localhost:3000'); + }); +}); + +describe('HSTS Middleware', () => { + function buildApp(env: string) { + const app = express(); + app.use((_req, res, next) => { + if (env === 'production') { + res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); + } + next(); + }); + app.get('/test', (_req, res) => res.json({ ok: true })); + return app; + } + + it('sets HSTS header in production', async () => { + const app = buildApp('production'); + const res = await request(app).get('/test'); + expect(res.headers['strict-transport-security']).toBe( + 'max-age=31536000; includeSubDomains; preload', + ); + }); + + it('does not set HSTS header in development', async () => { + const app = buildApp('development'); + const res = await request(app).get('/test'); + expect(res.headers['strict-transport-security']).toBeUndefined(); + }); });