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 &#91; 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.
nccommitted parent b8229226dbe7c93ddd2923cadfbf3c185685b6ec754e02cBrowse files
17 files changed+639−39

Rendering diff…