Files
scan-receipts/scripts/verify_sanitize_orchestrator.mjs
Timo 84b9987c49 Add full application: receipt scanning, auth, billing, and account deletion
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>
2026-08-19 20:59:04 +02:00

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