fix(accounting): PR #622 blockers — CSV formula injection + IMAP double-ingest race
Blocker 1 — CSV/Banana formula injection. Neither csvEscape (ledgerService) nor the tax-report CSV escape nor the unquoted tab-separated Banana cell formatter prefixed risky leading chars, so an admin-/sender-controlled cell beginning with = + - @ TAB CR executes as a formula when the Treuhänder opens the export. New shared util neutralizeSpreadsheetFormula() prepends a single quote; wired into all three sinks (quoted CSV + unquoted Banana). Unit test pins one of each char. Blocker 2 — IMAP intake double-ingest race. received_emails.message_id was INDEX, not UNIQUE, and the poller ingested attachments BEFORE writing the audit row, so a second replica / rolling-deploy overlap double-ingested the same mail. Migration 128 makes message_id UNIQUE (nulls stay distinct); the intake now CLAIMS the message row (status='processing') BEFORE ingesting — a concurrent claim hits the unique constraint and skips cleanly (shared isUniqueViolation helper). Stale 'processing' rows (worker crashed mid-ingest) are reclaimed after 10 min so no attachment is orphaned. NOT done (deliberate): the suggested UNIQUE on inbound_documents.file_sha256 — that column is a SOFT dedup key by design (manual re-uploads are kept as flagged 'duplicate' rows + duplicate_of_id for the Duplikat disposition); a unique index would break that feature. The file race only yields an extra 'unsorted' row (a data-quality nit, caught by the existing manual Duplikat backstop), not a double-count. Rationale to be added to the PR reply.
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Cross-driver detector for a unique-constraint violation. The error shape
|
||||
* varies by driver: Postgres → SQLSTATE `23505`; better-sqlite3 →
|
||||
* "UNIQUE constraint failed"; node-sqlite3 → `SQLITE_CONSTRAINT`. Used by the
|
||||
* claim-then-work concurrency patterns (document_sequences, monthly-draft, the
|
||||
* IMAP intake claim) to converge cleanly when a concurrent writer wins the race.
|
||||
*/
|
||||
function isUniqueViolation(err) {
|
||||
if (!err) return false;
|
||||
if (err.code === '23505' || err.code === 'SQLITE_CONSTRAINT') return true;
|
||||
const msg = String(err.message || '');
|
||||
return /unique/i.test(msg) || /sqlite_constraint/i.test(msg);
|
||||
}
|
||||
|
||||
module.exports = { isUniqueViolation };
|
||||
Reference in New Issue
Block a user