From 4826b9c2b5df6859b319ac9d968c863fec6066f5 Mon Sep 17 00:00:00 2001 From: oiupoyt Date: Sun, 20 Sep 2026 01:47:51 +0530 Subject: [PATCH] fix: remove the hardcoded OpenRouter key and always-true hasKey reporting The key was committed to three backends behind `|| DEFAULT_KEY` fallbacks, which also meant a misconfigured deploy silently billed the bundled key instead of failing. It is now read from the environment only: /api/ask answers 503 and /api/status reports hasKey:false when it is absent. hasKey was unreachable-false as well, via `|| true` in the Pages function and `|| DEFAULT_KEY` in the worker, so it could never signal a missing key. All responses now go through a shared json() helper, so error paths carry the same CORS headers and content type as success paths, and wrangler.json declares the ASSETS binding the worker's static fallback already referenced. --- README.md | 16 ++++++++++- functions/api/ask.js | 48 +++++++++++++++------------------ functions/api/status.js | 3 +-- server.js | 12 ++++++++- worker.mjs | 59 +++++++++++++++++------------------------ wrangler.json | 3 ++- 6 files changed, 75 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 33ba19f..ce5cbb7 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,25 @@ PORT=3000 OPENROUTER_API_KEY=sk-or-v1-... ``` +The key is **required** — there is no bundled fallback. Without it `POST /api/ask` +answers `503` and `GET /api/status` reports `"hasKey": false`. For Cloudflare, store +it as a secret instead of a plain variable: + +```bash +wrangler secret put OPENROUTER_API_KEY +``` + ## endpoints -- `GET /api/status` - model status and latency check +- `GET /api/status` - model status and whether the API key is configured - `POST /api/ask` - submit query to Jev decisions pipeline +## tests + +```bash +npm test +``` + ## license MIT diff --git a/functions/api/ask.js b/functions/api/ask.js index 5877c60..d552f32 100644 --- a/functions/api/ask.js +++ b/functions/api/ask.js @@ -1,7 +1,17 @@ -const DEFAULT_KEY = "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"; const JEV_DECISIONS_URL = "https://openrouter.ai/api/alpha/decisions"; const MODEL = "typesafe/jev-1.13"; +const CORS_HEADERS = { "Access-Control-Allow-Origin": "*" }; + +// One response helper so error paths cannot forget the JSON content type or the CORS +// headers, and so the API key never has a hardcoded fallback. +function json(body, status = 200) { + return new Response(JSON.stringify(body), { + status, + headers: { "Content-Type": "application/json", ...CORS_HEADERS } + }); +} + const APHORISMS = { certain: { text: "It is certain.", sentiment: "affirmative" }, decidedly: { text: "It is decidedly so.", sentiment: "affirmative" }, @@ -29,24 +39,21 @@ const APHORISMS = { export async function onRequestPost(context) { const { request, env } = context; - const apiKey = env.OPENROUTER_API_KEY || DEFAULT_KEY; + const apiKey = env.OPENROUTER_API_KEY; + if (!apiKey) { + return json({ error: "Oracle is not configured. Set the OPENROUTER_API_KEY secret." }, 503); + } let body; try { body = await request.json(); } catch (e) { - return new Response(JSON.stringify({ error: "Invalid JSON input." }), { - status: 400, - headers: { "Content-Type": "application/json" } - }); + return json({ error: "Invalid JSON input." }, 400); } const question = body.question; if (!question || typeof question !== "string" || !question.trim()) { - return new Response(JSON.stringify({ error: "Question is required." }), { - status: 400, - headers: { "Content-Type": "application/json" } - }); + return json({ error: "Question is required." }, 400); } const cleanQuestion = question.trim().slice(0, 300); @@ -111,14 +118,11 @@ export async function onRequestPost(context) { if (!response.ok) { const errText = await response.text(); - return new Response(JSON.stringify({ + return json({ error: "OpenRouter decision error", details: errText, fallback: fallbackDecision(cleanQuestion) - }), { - status: response.status, - headers: { "Content-Type": "application/json" } - }); + }, response.status); } const data = await response.json(); @@ -166,24 +170,16 @@ export async function onRequestPost(context) { raw: data }; - return new Response(JSON.stringify(result), { - status: 200, - headers: { - "Content-Type": "application/json", - "Access-Control-Allow-Origin": "*" - } - }); + return json(result); } catch (err) { + console.error("Jev decision request failed:", err); const latency = Date.now() - startTime; - return new Response(JSON.stringify({ + return json({ question: cleanQuestion, ...fallbackDecision(cleanQuestion), latency, offline: true - }), { - status: 200, - headers: { "Content-Type": "application/json" } }); } } diff --git a/functions/api/status.js b/functions/api/status.js index 675cc1f..aa46771 100644 --- a/functions/api/status.js +++ b/functions/api/status.js @@ -1,11 +1,10 @@ export async function onRequestGet(context) { const { env } = context; - const hasKey = Boolean(env.OPENROUTER_API_KEY || true); return new Response(JSON.stringify({ status: "online", model: "typesafe/jev-1.13", engine: "TypeSafe Jev (Cloudflare Pages Edge)", - hasKey + hasKey: Boolean(env.OPENROUTER_API_KEY) }), { status: 200, headers: { diff --git a/server.js b/server.js index 59f4ff1..5d39ed3 100644 --- a/server.js +++ b/server.js @@ -7,10 +7,16 @@ const app = express(); app.disable('x-powered-by'); const PORT = parseInt(process.env.PORT, 10) || 3000; -const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY || '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'; +const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY; const JEV_DECISIONS_URL = 'https://openrouter.ai/api/alpha/decisions'; const MODEL = 'typesafe/jev-1.13'; +if (!OPENROUTER_API_KEY) { + console.warn( + '[jev-8ball] OPENROUTER_API_KEY is not set — /api/ask will answer 503. Add it to .env (see .env.example).' + ); +} + app.use(cors()); app.use(express.json()); app.use(express.static(path.join(__dirname, 'public'))); @@ -53,6 +59,10 @@ app.get('/api/status', (req, res) => { // POST /api/ask app.post('/api/ask', async (req, res) => { + if (!OPENROUTER_API_KEY) { + return res.status(503).json({ error: 'Oracle is not configured. Set OPENROUTER_API_KEY.' }); + } + const { question } = req.body; if (!question || typeof question !== 'string' || !question.trim()) { return res.status(400).json({ error: 'Question is required.' }); diff --git a/worker.mjs b/worker.mjs index 8ea7eab..9a26cd8 100644 --- a/worker.mjs +++ b/worker.mjs @@ -1,7 +1,17 @@ -const DEFAULT_KEY = "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"; const JEV_DECISIONS_URL = "https://openrouter.ai/api/alpha/decisions"; const MODEL = "typesafe/jev-1.13"; +const CORS_HEADERS = { "Access-Control-Allow-Origin": "*" }; + +// Route every response through one helper so error paths cannot forget the JSON content +// type or the CORS headers, and so the API key never has a hardcoded fallback. +function json(body, status = 200) { + return new Response(JSON.stringify(body), { + status, + headers: { "Content-Type": "application/json", ...CORS_HEADERS } + }); +} + const APHORISMS = { certain: { text: "It is certain.", sentiment: "affirmative" }, decidedly: { text: "It is decidedly so.", sentiment: "affirmative" }, @@ -45,41 +55,31 @@ export default { // GET /api/status if (url.pathname === "/api/status") { - const hasKey = Boolean(env.OPENROUTER_API_KEY || DEFAULT_KEY); - return new Response(JSON.stringify({ + return json({ status: "online", model: MODEL, engine: "TypeSafe Jev (Cloudflare Edge Worker)", - hasKey - }), { - status: 200, - headers: { - "Content-Type": "application/json", - "Access-Control-Allow-Origin": "*" - } + hasKey: Boolean(env.OPENROUTER_API_KEY) }); } // POST /api/ask if (url.pathname === "/api/ask" && request.method === "POST") { - const apiKey = env.OPENROUTER_API_KEY || DEFAULT_KEY; + const apiKey = env.OPENROUTER_API_KEY; + if (!apiKey) { + return json({ error: "Oracle is not configured. Set the OPENROUTER_API_KEY secret." }, 503); + } let body; try { body = await request.json(); } catch (e) { - return new Response(JSON.stringify({ error: "Invalid JSON input." }), { - status: 400, - headers: { "Content-Type": "application/json" } - }); + return json({ error: "Invalid JSON input." }, 400); } const question = body.question; if (!question || typeof question !== "string" || !question.trim()) { - return new Response(JSON.stringify({ error: "Question is required." }), { - status: 400, - headers: { "Content-Type": "application/json" } - }); + return json({ error: "Question is required." }, 400); } const cleanQuestion = question.trim().slice(0, 300); @@ -144,14 +144,11 @@ export default { if (!response.ok) { const errText = await response.text(); - return new Response(JSON.stringify({ + return json({ error: "OpenRouter decision error", details: errText, fallback: fallbackDecision(cleanQuestion) - }), { - status: response.status, - headers: { "Content-Type": "application/json" } - }); + }, response.status); } const data = await response.json(); @@ -204,24 +201,16 @@ export default { raw: data }; - return new Response(JSON.stringify(result), { - status: 200, - headers: { - "Content-Type": "application/json", - "Access-Control-Allow-Origin": "*" - } - }); + return json(result); } catch (err) { + console.error("Jev decision request failed:", err); const latency = Date.now() - startTime; - return new Response(JSON.stringify({ + return json({ question: cleanQuestion, ...fallbackDecision(cleanQuestion), latency, offline: true - }), { - status: 200, - headers: { "Content-Type": "application/json" } }); } } diff --git a/wrangler.json b/wrangler.json index e238e37..13a7c61 100644 --- a/wrangler.json +++ b/wrangler.json @@ -3,6 +3,7 @@ "compatibility_date": "2024-09-23", "main": "worker.mjs", "assets": { - "directory": "./public" + "directory": "./public", + "binding": "ASSETS" } }