Fix audit findings: markdown injection, address normalisation, no tests
Masked-link phishing. Anyone who learns an address can send to it, and Discord renders markdown in bot messages, so a body containing `[www.yourbank.com](https://evil.example)` arrived as a clickable link whose visible text lied about its destination. Subjects and sender addresses went in raw too. htmlToText already defused this for HTML mail by rendering links as `label (<url>)`; that protection now applies everywhere, including anchor labels, which are just as attacker-controlled as the body. URLs are held out of the text while escaping runs and spliced back afterward, so real destinations are never mangled. Escaping runs after entity decoding so [ can't smuggle a live bracket through. allowed_mentions stops an @everyone in an email from resolving. Address normalisation. /torch, /extend and /note compared user input against stored addresses verbatim, so an uppercased or space-padded one came back as "Not found or not yours", which reads like an ownership problem rather than a typo. Phone keyboards autocapitalise the first letter, so this was easy to hit. Inbound mail already lowercased its recipient; commands now do the same. Tests. CI ran typecheck and nothing else, on logic that had just been rewritten. 60 tests now cover markdown escaping, expiry and note semantics, counter behaviour, permanent addresses, inbound filtering, and that schema.sql still agrees with the migration chain. Node's own runner against real in-memory SQLite, so no new dependencies and no mocks standing in for the SQL. Includes a regression test for the bug where a missing counters table silently dropped inbound mail. Also: /counters now sends a cache header, since it's public, unauthenticated and does a full scan for the user count; that count keys on (owner_type, owner_id) so a second adapter can't merge two people who share an id; the expiry and note bounds live in one place instead of being duplicated in the command registration; and register-commands explains where to get the secrets, which now live only in Cloudflare and can't be read back.
17 files changed+639−39
Rendering diff…