diff --git a/.env.local.example b/.env.local.example index 5694add..431fc8f 100644 --- a/.env.local.example +++ b/.env.local.example @@ -33,3 +33,8 @@ INVOICE_CONTRACT_ID=C... SIGNER_SECRET_KEY=S... NETWORK_PASSPHRASE=Standalone Network ; February 2025 SOROBAN_RPC_URL=http://localhost:8000/soroban/rpc + +# CORS — comma-separated allowlist of browser origins allowed to call the API. +# Each entry is a bare origin (scheme://host[:port]); no paths, trailing slash or "*". +# Requests from any other origin are rejected with 403. +CORS_ORIGINS=http://localhost:5173 diff --git a/.env.mainnet.example b/.env.mainnet.example index 4b0297f..bb55ddd 100644 --- a/.env.mainnet.example +++ b/.env.mainnet.example @@ -12,3 +12,8 @@ USDC_CONTRACT_ID=C... REDIS_URL=redis://localhost:6379 RATE_LIMIT_POINTS=60 RATE_LIMIT_DURATION=60 + +# CORS — comma-separated allowlist of browser origins allowed to call the API. +# Each entry is a bare origin (scheme://host[:port]); no paths, trailing slash or "*". +# List only production frontends here; never use a wildcard on mainnet. +CORS_ORIGINS=https://app.your-frontend.example diff --git a/.env.testnet.example b/.env.testnet.example index 567d715..92e10d5 100644 --- a/.env.testnet.example +++ b/.env.testnet.example @@ -12,3 +12,7 @@ USDC_CONTRACT_ID=C... REDIS_URL=redis://localhost:6379 RATE_LIMIT_POINTS=60 RATE_LIMIT_DURATION=60 + +# CORS — comma-separated allowlist of browser origins allowed to call the API. +# Each entry is a bare origin (scheme://host[:port]); no paths, trailing slash or "*". +CORS_ORIGINS=https://testnet.your-frontend.example diff --git a/comebackhere-backend/package.json b/comebackhere-backend/package.json index c4564cf..1e952e2 100644 --- a/comebackhere-backend/package.json +++ b/comebackhere-backend/package.json @@ -21,6 +21,7 @@ "zod": "^4.4.3" }, "devDependencies": { + "@types/cors": "^2.8.19", "@types/express": "^4.17.21", "@types/ioredis": "^5.0.0", "@types/node": "^20.11.0", diff --git a/comebackhere-backend/src/app.ts b/comebackhere-backend/src/app.ts index 08e9df3..71fb22a 100644 --- a/comebackhere-backend/src/app.ts +++ b/comebackhere-backend/src/app.ts @@ -1,4 +1,5 @@ import express from "express" +import helmet from "helmet" import swaggerUi from "swagger-ui-express" import invoicesRouter from "./routes/invoices.js" import complianceRouter from "./routes/compliance.js" @@ -11,15 +12,69 @@ import analyticsRouter from "./routes/analytics.js" import { startComplianceIndexer } from "./services/compliance-indexer.js" import { rateLimitMiddleware } from "./middleware/rateLimiter.js" import { correlationIdMiddleware } from "./middleware/correlationId.js" +import { errorHandler, notFoundHandler } from "./middleware/errorHandler.js" +import { createCorsMiddleware } from "./middleware/cors.js" +import { parseCorsOrigins } from "./lib/env.js" import { openapiSpec } from "./openapi.js" import { renderMetrics } from "./lib/metrics.js" -export function createApp() { +/** Maximum accepted JSON body size; larger requests get a 413 envelope. */ +export const JSON_BODY_LIMIT = "100kb" + +// This is a JSON API, so by default nothing may be loaded, framed or executed. +const apiHelmet = helmet({ + contentSecurityPolicy: { + useDefaults: false, + directives: { + defaultSrc: ["'none'"], + frameAncestors: ["'none'"], + baseUri: ["'none'"], + formAction: ["'none'"], + }, + }, +}) + +// Swagger UI serves its JS/CSS from same-origin files but also uses inline +//