Adds a regression suite for #3843 so the leak cannot return silently. The
client had no test runner, so this introduces Vitest, which reuses the
existing Vite resolution and the `@/` alias rather than needing a separate
transform pipeline. jsdom is required because the logger reads
window.location and navigator.userAgent.
The tests drive the exported `logger` rather than the private redaction
helpers, so they cover the wiring as well as the logic. That distinction
matters here: the first attempt at this fix redacted the JSON branch but
left the debug branch printing raw objects, a gap that testing the helper
in isolation would not have caught. Each case runs at both error and debug
level for the same reason.
Covered: the reported login payload, credentials carried on an AxiosError
(config.data and the Authorization header), non-JSON request bodies,
passwords on class instances, key-matching variants, and circular payloads.
One asserts non-sensitive context survives, so redaction cannot regress
into blanket suppression.
Verified by reverting the fix: 16 of 23 tests fail against the original
behaviour, including a pre-existing crash on circular payloads.
Follow-up to the initial redaction pass, addressing four defects:
Class instances bypassed redaction entirely. The prototype guard was meant
to skip FormData/Blob but caught every non-literal object, returning it by
reference — so a credential on a DTO or form-state instance was logged
verbatim, and the deep-copy contract was violated. The check is now an
explicit deny-list of opaque types; everything else is walked.
The raw Error was still logged by the debug branch. An AxiosError carries
config.data (the serialized request body) and config.headers.Authorization
(set for every request in ApiClient), so the password redacted from the
context was printed straight back out via the error object. Errors now go
through toSafeError(). Because Axios serializes the body to a string, which
key-based redaction cannot see into, it is parsed before redaction; a body
that is not JSON is replaced wholesale rather than logged raw.
The Blob guard was gated on `typeof FormData !== "undefined"`, so in an
environment with FormData but no Blob the instanceof check threw a
ReferenceError — from inside a catch block, turning a handled API error
into an unhandled crash. Each constructor is now resolved defensively.
Also widened the key list (cookie, jwt, privateKey, sessionId, csrf, pwd)
and dropped a redundant type annotation.
Verified with 18 assertions covering the reported payload, class instances,
cycles, shared siblings, axios error shapes, and a missing Blob global.
Failed requests logged the full request body to the browser console, so a
failing POST to /auth/login wrote the user's plaintext password where it
could be captured by error-reporting tools, screen shares, or support
sessions.
Redaction is applied centrally in the logger rather than at the call sites,
so every current and future caller is covered:
- createLogData() redacts context before it is serialized, covering the
JSON branches of error() and warn().
- The debug-level branches of error() and warn() printed the raw context
object directly, bypassing createLogData(); they now print the redacted
copy.
- debug() and info() redact their free-form data argument.
Keys are matched case-insensitively on a substring after stripping dashes
and underscores, so password, newPassword, X-Api-Key and Authorization are
all caught. The walk copies rather than mutates the caller's object, cuts
genuine cycles without mislabelling repeated siblings, and renders
FormData/Blob as a type marker instead of walking them.
Affected paths were POST (/auth/login, /auth/register) and PATCH/PUT, which
carry monitor secrets. Error-level logging is on by default, so this was
not limited to debug builds.
Fixes#3843
Matrix notifications failed for anyone who entered a room alias. The send
endpoint only accepts a room ID, and an unresolved alias broke the request
outright: the leading "#" opens a URI fragment, so the PUT landed on
/_matrix/client/v3/rooms/ instead of the send endpoint. Element shows users
the alias, so this is the value most people paste in.
- Resolve "#alias:server" through GET /_matrix/client/v3/directory/room/{alias}
before sending, per the client-server spec
- Percent-encode the room ID in the send path; ":" was previously sent raw
- Strip trailing slashes from the homeserver URL to avoid "//_matrix/..."
Alias resolution failures return false and log, matching how the provider
already handles send failures.
Closes#3478
sendEmail caught transport failures internally and resolved to false or
undefined, so every caller's .catch() was dead code and send failures were
silently suppressed. Password recovery in particular reported "email sent
successfully" while the user received nothing.
sendEmail now throws an AppError carrying the underlying cause, and returns
Promise<string> instead of Promise<string | false | undefined>.
Call sites updated for the new contract:
- UserService.registerUser: the existing .catch() now actually fires
- UserService.requestRecovery: the failure reaches the client instead of a
misleading success response
- InviteService and the test-email endpoint: keep their user-facing messages
and attach the SMTP cause
- EmailProvider: catches and returns false, because NotificationsService fans
providers out through Promise.all and a rejection there would abort the
other channels' alerts
Closes#3757
ntfy servers that require auth rejected every Checkmate alert because the
provider posted without an Authorization header, so alerts to protected
topics failed silently.
Adds ntfyAuthType ("none" | "token" | "basic"):
- token: Authorization: Bearer <accessToken>
- basic: Authorization: Basic base64(ntfyUsername:accessToken)
Both types store their secret in the existing accessToken field rather than
introducing a new secret, so credential handling stays in one place.
Incomplete credentials fail the send instead of falling back to an anonymous
post, and validation rejects partial auth config up front so a channel can't
be saved looking authenticated while posting anonymously.
Backend only; frontend follows once this lands.
Closes#3717
Build title_link from the monitor type (hardware -> infrastructure,
pagespeed -> pagespeed, uptime types -> uptime) and omit it when
clientHost is not configured, instead of always linking to the
infrastructure page.
- Bare IP addresses and non-domain hosts (localhost, IPv6) now return a
null expiry instead of throwing, so they no longer spam error logs.
- Extract the registrable domain with tldts instead of walking up
hostname labels, so a subdomain can never accidentally return its
parent domain's expiry date.
- Cache WHOIS lookups per registrable domain (1h positive, 10min
negative) so visiting details pages does not re-query the registry
every time.
Addresses review feedback on #3834.
Adds a GET /monitors/domain/:monitorId endpoint that looks up the
registrable domain's WHOIS expiry date (walking up from subdomains),
and displays it next to the certificate expiry on the uptime details
page.
Fixes#2094