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:
parent
fc464cb68b
commit
4826b9c2b5
6 changed files with 75 additions and 66 deletions
16
README.md
16
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
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
12
server.js
12
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.' });
|
||||
|
|
|
|||
59
worker.mjs
59
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" }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
"compatibility_date": "2024-09-23",
|
||||
"main": "worker.mjs",
|
||||
"assets": {
|
||||
"directory": "./public"
|
||||
"directory": "./public",
|
||||
"binding": "ASSETS"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue