From 7d24336380630d72725f699f602f5e458537238d Mon Sep 17 00:00:00 2001 From: Pyronewbic Date: Mon, 11 May 2026 14:58:31 +0530 Subject: [PATCH] feat: sandbox API key with 5 req/min rate limit for developer playground - CC_LIVE_SANDBOX_ key with aggressive rate limit (5/min per IP) - Sandbox requests use third-party cache isolation - Moved getRequestToken/isOwnerKey/cachePrefix before rate limit middleware - Secret Manager: CASECOMP_SANDBOX_KEY added --- .env.example | 3 ++- api.js | 51 ++++++++++++++++++++++++++++++----------------- terraform/main.tf | 1 + 3 files changed, 36 insertions(+), 19 deletions(-) diff --git a/.env.example b/.env.example index 609fc08..d10c67b 100644 --- a/.env.example +++ b/.env.example @@ -68,7 +68,8 @@ LOCAL_GRADER_URL= # # API key for v1 endpoints (Bearer auth). If unset, v1 endpoints are open. # Generate with: node -e "console.log('cc_live_' + require('crypto').randomBytes(24).toString('hex'))" -#CASECOMP_API_KEY=cc_live_ +#CASECOMP_API_KEY=CC_LIVE_ +#CASECOMP_SANDBOX_KEY=CC_LIVE_SANDBOX_ # # Firestore for persistent storage (grades, drops, webhooks). # On Cloud Run, auto-authenticates via the service account — no config needed. diff --git a/api.js b/api.js index e63c9ca..917b9eb 100644 --- a/api.js +++ b/api.js @@ -49,6 +49,24 @@ app.use("/logos", express.static(path.join(__dirname, "logos"))); app.get("/docs/spec.json", (req, res) => res.json(swaggerSpec)); app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec)); +function getRequestToken(req) { + const auth = req.headers.authorization; + const query = req.query.key; + return auth?.startsWith("Bearer ") ? auth.slice(7) : query || ""; +} + +function isOwnerKey(req) { + const key = process.env.CASECOMP_API_KEY; + if (!key) return true; + return getRequestToken(req) === key; +} + +function cachePrefix(req) { + if (isOwnerKey(req)) return ""; + const token = getRequestToken(req); + return token.slice(0, 16) + "_"; +} + const apiLimiter = rateLimit({ windowMs: 60_000, max: 60, @@ -66,9 +84,24 @@ const demoLimiter = rateLimit({ message: { error: "Too many requests, please try again later" }, }); +const sandboxLimiter = rateLimit({ + windowMs: 60_000, + max: 5, + standardHeaders: true, + legacyHeaders: false, + keyGenerator: (req) => req.ip, + message: { error: "Sandbox rate limit: 5 requests per minute" }, +}); + +function isSandboxKey(req) { + const token = getRequestToken(req); + return token && token === process.env.CASECOMP_SANDBOX_KEY; +} + app.use("/api", (req, res, next) => { if (req.path === "/health") return next(); if (req.query.demo === "true") return demoLimiter(req, res, next); + if (isSandboxKey(req)) return sandboxLimiter(req, res, next); return apiLimiter(req, res, next); }); app.use("/v1", apiLimiter); @@ -370,24 +403,6 @@ app.get("/api/health", async (req, res) => { // ============ V1 API — Drop Intelligence ============ -function getRequestToken(req) { - const auth = req.headers.authorization; - const query = req.query.key; - return auth?.startsWith("Bearer ") ? auth.slice(7) : query || ""; -} - -function isOwnerKey(req) { - const key = process.env.CASECOMP_API_KEY; - if (!key) return true; - return getRequestToken(req) === key; -} - -function cachePrefix(req) { - if (isOwnerKey(req)) return ""; - const token = getRequestToken(req); - return token.slice(0, 16) + "_"; -} - function authMiddleware(req, res, next) { const key = process.env.CASECOMP_API_KEY; if (!key) return next(); diff --git a/terraform/main.tf b/terraform/main.tf index 44d271e..d787ebb 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -65,6 +65,7 @@ locals { "ANTHROPIC_API_KEY", "PSA_AUTH_TOKEN", "CASECOMP_API_KEY", + "CASECOMP_SANDBOX_KEY", ] }