From cc1cb0d2a75785c94437f5c48718af58f15edc13 Mon Sep 17 00:00:00 2001 From: siribeesu Date: Thu, 23 Jul 2026 17:23:34 +0530 Subject: [PATCH] Add extraction input size validation --- backend/utils/extractValidation.js | 38 ++++++++++++++++++++++++++++++ server.js | 15 ++++++++---- tests/extractValidation.test.js | 23 ++++++++++++++++++ 3 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 backend/utils/extractValidation.js create mode 100644 tests/extractValidation.test.js diff --git a/backend/utils/extractValidation.js b/backend/utils/extractValidation.js new file mode 100644 index 00000000..b357aeef --- /dev/null +++ b/backend/utils/extractValidation.js @@ -0,0 +1,38 @@ +const MAX_EXTRACTION_TEXT_LENGTH = 12000; + +function validateExtractionText(text) { + if (typeof text !== 'string') { + return { + ok: false, + statusCode: 400, + error: 'Text must be provided as a string.', + }; + } + + const trimmedText = text.trim(); + if (!trimmedText) { + return { + ok: false, + statusCode: 400, + error: 'Text is required', + }; + } + + if (trimmedText.length > MAX_EXTRACTION_TEXT_LENGTH) { + return { + ok: false, + statusCode: 413, + error: `Text exceeds the maximum allowed length of ${MAX_EXTRACTION_TEXT_LENGTH} characters. Please provide a smaller excerpt for AI extraction.`, + }; + } + + return { + ok: true, + text: trimmedText, + }; +} + +module.exports = { + MAX_EXTRACTION_TEXT_LENGTH, + validateExtractionText, +}; diff --git a/server.js b/server.js index b5267cb0..98d72f34 100644 --- a/server.js +++ b/server.js @@ -5,10 +5,11 @@ const { db, initDb } = require('./database'); const { GoogleGenAI } = require('@google/genai'); const path = require('path'); const csvDownloadRouter = require('./backend/routers/csvDownload.router.js'); +const { validateExtractionText } = require('./backend/utils/extractValidation.js'); const app = express(); app.use(cors()); -app.use(express.json()); +app.use(express.json({ limit: '1mb' })); const page404Path = path.join(__dirname, '404.html'); const page500Path = path.join(__dirname, 'error.html'); @@ -530,7 +531,13 @@ app.delete('/api/tasks/:id', (req, res) => { // ================= AI EXTRACTION ================= app.post('/api/extract', async (req, res) => { const { text } = req.body; - if (!text) return res.status(400).json({ error: 'Text is required' }); + const validation = validateExtractionText(text); + + if (!validation.ok) { + return res.status(validation.statusCode).json({ error: validation.error }); + } + + const safeText = validation.text; if (ai) { try { @@ -540,7 +547,7 @@ Return ONLY a raw JSON array (no markdown, no backticks, no explanation). Each object must have: title (string), subject_name (string), due_at (ISO 8601 datetime), notes (string), confidence_score (number 0-100), priority ("low"|"medium"|"high"), icon (emoji). IMPORTANT: Do not strip hashtags from the task description! If the original text contains hashtag labels (e.g. #urgent, #Group), you MUST include them at the end of the 'title' field (e.g. 'Read chapter 1 #urgent'). -Text: "${text}" +Text: "${safeText}" `; const response = await ai.models.generateContent({ model: 'gemini-2.5-flash', @@ -559,7 +566,7 @@ Text: "${text}" } // NLP heuristic fallback (no API key, or Gemini failed) - const tasks = nlpExtractTasksFromText(text); + const tasks = nlpExtractTasksFromText(safeText); return res.json(tasks); }); // ================= AUTH ================= diff --git a/tests/extractValidation.test.js b/tests/extractValidation.test.js new file mode 100644 index 00000000..7cb4d3fd --- /dev/null +++ b/tests/extractValidation.test.js @@ -0,0 +1,23 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const { + MAX_EXTRACTION_TEXT_LENGTH, + validateExtractionText, +} = require('../backend/utils/extractValidation.js'); + +test('validateExtractionText accepts a normal-sized prompt', () => { + const result = validateExtractionText('Submit math homework by tomorrow.'); + + assert.equal(result.ok, true); + assert.equal(result.text, 'Submit math homework by tomorrow.'); +}); + +test('validateExtractionText rejects oversized input with a helpful error', () => { + const longText = 'a'.repeat(MAX_EXTRACTION_TEXT_LENGTH + 1); + const result = validateExtractionText(longText); + + assert.equal(result.ok, false); + assert.equal(result.statusCode, 413); + assert.match(result.error, /maximum allowed length/i); +});