Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
51 changes: 33 additions & 18 deletions api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
Expand Down Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions terraform/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ locals {
"ANTHROPIC_API_KEY",
"PSA_AUTH_TOKEN",
"CASECOMP_API_KEY",
"CASECOMP_SANDBOX_KEY",
]
}

Expand Down