Second-pass fixes caught by a self-review after the initial commit — each
one would have undermined a fix from the previous commit.
- mfaPolicy now goes through `verifyJwtAndLoadUser` too. Without this,
a JWT stolen before a password reset still satisfied `require_mfa`
until its natural 24h expiry, defeating the whole point of the
password_version bump.
- Drop the `?? keys[0]` fallback in OIDC JWKS key selection. When the
token carries a `kid` that is not in the current JWKS, refuse
outright instead of picking an arbitrary key and letting the
signature check produce a generic failure — the real failure mode
deserves a specific error code.
- Tighten OAuth DCR custom-scheme rule so `javascript:`, `data:`,
`vbscript:`, `file:`, `blob:`, `about:`, `chrome:` are all rejected.
Previously the catch-all "not http/https" check admitted them; the
authorize flow later 302s the browser to whatever is registered,
which with a `javascript:` URI would execute attacker script on
redirect. Also require the private-use scheme body to be reverse-DNS
(contain a dot), matching RFC 8252 §7.1.
- permanentDeleteFile / emptyTrash only delete the trip_files row when
the on-disk unlink actually succeeded. Previously Promise.all
swallowed individual unlink failures and DELETE ran unconditionally,
so a permission / ENOSPC failure would orphan bytes on disk.
- restoreFromZip also invalidates the permissions cache in the outer
catch. If extraction threw before the DB swap even started, the
cache wasn't stale, but belt-and-braces is cheap and guarantees no
failed-restore path leaves stale cache behind.