Brings the working codebase (Next.js app, auth system, Stripe billing, Docker/deploy config, tests, docs) into version control on top of the placeholder initial commit, and adds account self-deletion (Danger Zone in Settings, password + typed-email confirmation, cascading DB cleanup, Stripe cancellation) per GDPR right-to-erasure. Excludes local build caches, node_modules, and internal agent scratch files; .gitignore hardened to keep those out going forward. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
96 lines
5.2 KiB
JavaScript
96 lines
5.2 KiB
JavaScript
/**
|
|
* Orchestrator-level verification for Task 1 (Sanitize before storing).
|
|
* Tests the real implementation in src/lib/ingest/sanitize.ts (zod schemas +
|
|
* sanitizers) and the CSV formula-injection guard in
|
|
* src/lib/export/csvGenerator.ts (neutralizeFormulaPrefix).
|
|
*
|
|
* Run: node --experimental-loader ./scripts/cors-resolve-hook.mjs scripts/verify_sanitize_orchestrator.mjs
|
|
*/
|
|
import { sanitizeText, sanitizeMultilineText, sanitizeReceiptBatch, sanitizeReceipt, StoredReceiptSchema } from "../src/lib/ingest/sanitize.ts";
|
|
import { neutralizeFormulaPrefix } from "../src/lib/export/csvGenerator.ts";
|
|
|
|
let failed = 0;
|
|
let checks = 0;
|
|
function expect(cond, label) {
|
|
checks++;
|
|
if (!cond) { failed++; console.error(" FAIL:", label); }
|
|
else console.log(" ok ", label);
|
|
}
|
|
|
|
console.log("[1] sanitizeText strips markup + control chars");
|
|
const dirty = `<script>alert(1)</script>Händler <b>GmbH</b>\u0000\u0007`;
|
|
const clean = sanitizeText(dirty, 200);
|
|
expect(!clean.includes("<") && !clean.includes(">"), "no < > remain");
|
|
expect(!clean.includes("\u0000") && !clean.includes("\u0007"), "control chars stripped");
|
|
expect(clean.includes("Händler GmbH"), "text preserved");
|
|
expect(sanitizeText("a\u00a0b c", 100) === "a b c", "whitespace collapsed");
|
|
|
|
console.log("[2] multiline OCR keeps line layout, strips markup");
|
|
const ocr = "Zeile1\nZeile2<img onerror=alert(1)>\tZeile3";
|
|
const ocrClean = sanitizeMultilineText(ocr, 50_000);
|
|
expect(ocrClean.includes("\n"), "newlines survive");
|
|
expect(!ocrClean.includes("<"), "markup stripped");
|
|
expect(ocrClean.includes("Zeile3"), "tab survived");
|
|
|
|
console.log("[3] length caps");
|
|
expect(sanitizeText("x".repeat(500), 64).length === 64, "capped at limit");
|
|
|
|
console.log("[4] sanitizeReceiptBatch — malicious receipt payload");
|
|
const evil = {
|
|
id: "r1<script>",
|
|
merchant: { name: '=HYPERLINK("http://evil","x")', address: null, taxId: null, confidence: 1 },
|
|
date: { isoDate: "2026-08-15", time: null, confidence: 1 },
|
|
documentType: "KASSENBON",
|
|
receiptNumber: "+cmd|' /C calc'!A0",
|
|
currency: "EUR",
|
|
totalAmount: { value: 12.5, confidence: 1 },
|
|
netAmount: 10.5,
|
|
taxBreakdown: [{ ratePercent: 19, taxAmount: 2, netAmount: 10.5 }],
|
|
lineItems: [{ description: "<b>Cola</b>", quantity: 2, price: 2.5, unitPrice: 1.25, taxRate: 19 }],
|
|
suggestedCategory: "Sonstiges<script>",
|
|
validation: { isMathValid: true, isDuplicateSuspected: false, needsUserReview: false, reviewField: "none", reviewReason: null },
|
|
rawText: "OCR <img onerror=1> line\n2",
|
|
paymentMethod: "CASH",
|
|
createdAt: "2026-08-15T10:00:00.000Z",
|
|
};
|
|
const batch = sanitizeReceiptBatch([evil]);
|
|
expect(batch.ok === true, "malicious-but-structurally-valid payload accepted (sanitized)");
|
|
if (batch.ok && batch.receipts) {
|
|
const r = batch.receipts[0];
|
|
expect(!r.merchant.name.includes("<") && !r.merchant.name.includes(">"), "merchant markup stripped");
|
|
expect(!r.suggestedCategory.includes("<script>"), "category markup stripped");
|
|
expect(!r.rawText.includes("<"), "rawText markup stripped");
|
|
expect(r.currency === "EUR", "currency normalized");
|
|
expect(r.id === "r1", "id sanitized (markup tag removed)");
|
|
}
|
|
|
|
console.log("[5] sanitizeReceiptBatch — structural rejection");
|
|
expect(sanitizeReceiptBatch(null).ok === false, "null payload rejected");
|
|
expect(sanitizeReceiptBatch({}).ok === false, "empty object rejected");
|
|
expect(sanitizeReceiptBatch([]).ok === false, "empty array rejected");
|
|
expect(sanitizeReceiptBatch([{ id: 42 }]).ok === false, "non-string id rejected");
|
|
expect(sanitizeReceiptBatch([{ id: "x", merchant: { name: 5 } }]).ok === false, "non-string merchant name rejected");
|
|
expect(sanitizeReceiptBatch([{ id: "x", currency: "EURO" }]).ok === false, "bad currency rejected");
|
|
expect(sanitizeReceiptBatch([{ id: "x", totalAmount: { value: NaN } }]).ok === false, "NaN amount rejected");
|
|
const tooMany = Array.from({ length: 501 }, (_, i) => ({ id: `r${i}`, currency: "EUR", totalAmount: { value: 1 } }));
|
|
expect(sanitizeReceiptBatch(tooMany).ok === false, "501 receipts rejected");
|
|
expect(sanitizeReceiptBatch(tooMany).error.includes("Too many"), "batch-size error message");
|
|
|
|
console.log("[6] sanitizeReceipt single variant");
|
|
expect(sanitizeReceipt(evil) !== null, "valid single receipt sanitized");
|
|
expect(sanitizeReceipt({ id: "x" }) === null, "invalid single receipt → null");
|
|
|
|
console.log("[7] CSV formula injection — neutralizeFormulaPrefix");
|
|
expect(neutralizeFormulaPrefix('=HYPERLINK("http://evil","x")') === "'=HYPERLINK(\"http://evil\",\"x\")", "= prefix neutralized");
|
|
expect(neutralizeFormulaPrefix('+cmd|\' /C calc\'!A0') === "'+cmd|' /C calc'!A0", "+ non-number neutralized");
|
|
expect(neutralizeFormulaPrefix('@SUM(1+1)') === "'@SUM(1+1)", "@ neutralized");
|
|
expect(neutralizeFormulaPrefix('\t=1') === "'\t=1", "tab neutralized");
|
|
expect(neutralizeFormulaPrefix('-5,00') === "-5,00", "negative number literal untouched (credit note)");
|
|
expect(neutralizeFormulaPrefix('+1,25') === "+1,25", "positive number literal untouched");
|
|
expect(neutralizeFormulaPrefix('-SUM(1+1)') === "'-SUM(1+1)", "- non-number neutralized");
|
|
expect(neutralizeFormulaPrefix('Rewe Markt') === "Rewe Markt", "normal text untouched");
|
|
|
|
console.log(`\n${checks} checks, ${failed} failure(s)`);
|
|
if (failed > 0) process.exit(1);
|
|
console.log("SANITIZE VERIFICATION PASSED");
|