|
10 | 10 | * - Accepts text in body and model as query parameter |
11 | 11 | * - Returns binary audio data (application/octet-stream) |
12 | 12 | * - CORS enabled for frontend communication |
13 | | - * - JWT session auth with page nonce (production only) |
| 13 | + * - JWT session auth with rate limiting (production only) |
14 | 14 | * - Pure API server (frontend served separately) |
15 | 15 | */ |
16 | 16 |
|
@@ -44,70 +44,18 @@ const CONFIG = { |
44 | 44 | }; |
45 | 45 |
|
46 | 46 | // ============================================================================ |
47 | | -// SESSION AUTH - JWT tokens with page nonce for production security |
| 47 | +// SESSION AUTH - JWT tokens for production security |
48 | 48 | // ============================================================================ |
49 | 49 |
|
50 | 50 | /** |
51 | | - * Session secret for signing JWTs. When set (production/Fly.io), nonce |
52 | | - * validation is enforced. When unset (local dev), tokens are issued freely. |
| 51 | + * Session secret for signing JWTs. |
53 | 52 | */ |
54 | 53 | const SESSION_SECRET = |
55 | 54 | process.env.SESSION_SECRET || crypto.randomBytes(32).toString("hex"); |
56 | | -const REQUIRE_NONCE = !!process.env.SESSION_SECRET; |
57 | | - |
58 | | -/** In-memory nonce store: nonce → expiry timestamp */ |
59 | | -const sessionNonces = new Map(); |
60 | | - |
61 | | -/** Nonce expiry time (5 minutes) */ |
62 | | -const NONCE_TTL_MS = 5 * 60 * 1000; |
63 | 55 |
|
64 | 56 | /** JWT expiry time (1 hour) */ |
65 | 57 | const JWT_EXPIRY = "1h"; |
66 | 58 |
|
67 | | -/** |
68 | | - * Generates a single-use nonce and stores it with an expiry |
69 | | - * @returns {string} The generated nonce |
70 | | - */ |
71 | | -function generateNonce() { |
72 | | - const nonce = crypto.randomBytes(16).toString("hex"); |
73 | | - sessionNonces.set(nonce, Date.now() + NONCE_TTL_MS); |
74 | | - return nonce; |
75 | | -} |
76 | | - |
77 | | -/** |
78 | | - * Validates and consumes a nonce (single-use) |
79 | | - * @param {string} nonce - The nonce to validate |
80 | | - * @returns {boolean} True if the nonce was valid and consumed |
81 | | - */ |
82 | | -function consumeNonce(nonce) { |
83 | | - const expiry = sessionNonces.get(nonce); |
84 | | - if (!expiry) return false; |
85 | | - sessionNonces.delete(nonce); |
86 | | - return Date.now() < expiry; |
87 | | -} |
88 | | - |
89 | | -/** Periodically clean up expired nonces (every 60 seconds) */ |
90 | | -setInterval(() => { |
91 | | - const now = Date.now(); |
92 | | - for (const [nonce, expiry] of sessionNonces) { |
93 | | - if (now >= expiry) sessionNonces.delete(nonce); |
94 | | - } |
95 | | -}, 60_000); |
96 | | - |
97 | | -/** |
98 | | - * Reads frontend/dist/index.html and injects a session nonce meta tag. |
99 | | - * Returns null in dev mode (no built frontend). |
100 | | - */ |
101 | | -let indexHtmlTemplate = null; |
102 | | -try { |
103 | | - indexHtmlTemplate = fs.readFileSync( |
104 | | - path.join(__dirname, "frontend", "dist", "index.html"), |
105 | | - "utf-8" |
106 | | - ); |
107 | | -} catch { |
108 | | - // No built frontend (dev mode) — index.html served by Vite |
109 | | -} |
110 | | - |
111 | 59 | /** |
112 | 60 | * Express middleware that validates JWT from Authorization header. |
113 | 61 | * Returns 401 with JSON error if token is missing or invalid. |
@@ -305,42 +253,14 @@ function formatErrorResponse(error, statusCode = 500, errorCode = null) { |
305 | 253 | // ============================================================================ |
306 | 254 |
|
307 | 255 | /** |
308 | | - * GET / — Serve index.html with injected session nonce (production only). |
309 | | - * In dev mode, Vite serves the frontend directly. |
310 | | - */ |
311 | | -app.get("/", (req, res) => { |
312 | | - if (!indexHtmlTemplate) { |
313 | | - return res.status(404).send("Frontend not built. Run make build first."); |
314 | | - } |
315 | | - const nonce = generateNonce(); |
316 | | - const html = indexHtmlTemplate.replace( |
317 | | - "</head>", |
318 | | - `<meta name="session-nonce" content="${nonce}">\n</head>` |
319 | | - ); |
320 | | - res.type("html").send(html); |
321 | | -}); |
322 | | - |
323 | | -/** |
324 | | - * GET /api/session — Issues a JWT. In production (SESSION_SECRET set), |
325 | | - * requires a valid single-use nonce via X-Session-Nonce header. |
| 256 | + * GET /api/session — Issues a signed JWT for session authentication. |
326 | 257 | */ |
327 | 258 | app.get("/api/session", (req, res) => { |
328 | | - if (REQUIRE_NONCE) { |
329 | | - const nonce = req.headers["x-session-nonce"]; |
330 | | - if (!nonce || !consumeNonce(nonce)) { |
331 | | - return res.status(403).json({ |
332 | | - error: { |
333 | | - type: "AuthenticationError", |
334 | | - code: "INVALID_NONCE", |
335 | | - message: "Valid session nonce required. Please refresh the page.", |
336 | | - }, |
337 | | - }); |
338 | | - } |
339 | | - } |
340 | | - |
341 | | - const token = jwt.sign({ iat: Math.floor(Date.now() / 1000) }, SESSION_SECRET, { |
342 | | - expiresIn: JWT_EXPIRY, |
343 | | - }); |
| 259 | + const token = jwt.sign( |
| 260 | + { iat: Math.floor(Date.now() / 1000) }, |
| 261 | + SESSION_SECRET, |
| 262 | + { expiresIn: JWT_EXPIRY } |
| 263 | + ); |
344 | 264 | res.json({ token }); |
345 | 265 | }); |
346 | 266 |
|
@@ -478,7 +398,7 @@ app.get("/api/metadata", (req, res) => { |
478 | 398 | app.listen(CONFIG.port, CONFIG.host, () => { |
479 | 399 | console.log("\n" + "=".repeat(70)); |
480 | 400 | console.log(`🚀 Backend API running at http://localhost:${CONFIG.port}`); |
481 | | - console.log(`📡 GET /api/session${REQUIRE_NONCE ? " (nonce required)" : ""}`); |
| 401 | + console.log(`📡 GET /api/session`); |
482 | 402 | console.log(`📡 POST /api/text-to-speech (auth required)`); |
483 | 403 | console.log(`📡 GET /api/metadata`); |
484 | 404 | console.log("=".repeat(70) + "\n"); |
|
0 commit comments