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.
This commit is contained in:
katR 2026-09-20 01:47:51 +05:30
parent fc464cb68b
commit 4826b9c2b5
6 changed files with 75 additions and 66 deletions

View file

@ -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

View file

@ -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" }
});
}
}

View file

@ -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: {

View file

@ -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.' });

View file

@ -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" }
});
}
}

View file

@ -3,6 +3,7 @@
"compatibility_date": "2024-09-23",
"main": "worker.mjs",
"assets": {
"directory": "./public"
"directory": "./public",
"binding": "ASSETS"
}
}