* feat(workers): auto-dismiss consent dialogs with autoconsent
Add @duckduckgo/autoconsent CMP auto-opt-out to the browser crawl. The bundle
+ rules load once at browser init (browser.ts); each page installs the content
script via addInitScript over an exposeFunction bridge (crawler/autoconsent.ts),
after the SSRF router and redirect guard. When a CMP is detected we wait up to
3s (abort-aware) for opt-out before capturing HTML/screenshot/PDF; pages with
no CMP add zero latency. Gated by CRAWLER_ENABLE_AUTOCONSENT (default on).
The dummy comparison meant to equalise the timing of a login against a
non-existent account passed a 40 character string as the hash. bcrypt parses
the hash to recover its cost factor and salt, so an argument that isn't a valid
60 character hash makes it bail out immediately without deriving anything --
the branch cost 0.1ms against 53ms for a real account, a 500x gap that
identifies registered emails from a single request. Compare against a real
hash of a random secret at the same cost factor instead. Measured after the
change: 54.9ms, 55.1ms and 54.1ms for the real, unknown and password-less
paths.
The password-less branch returned with no comparison at all, which likewise
made OAuth-only accounts distinguishable from password accounts. Give it the
same treatment.
forgotPassword awaited the delivery of the reset email, which only real
accounts reach, leaking existence through response latency. Worse, a failure
there was rethrown as a 500, so an unhealthy SMTP server turned the endpoint
into an exact oracle: 500 for a registered address, 200 for an unregistered
one. Dispatch it without awaiting and log failures instead, so the response is
identical either way. Token creation failures still surface, since they don't
depend on whether the account exists.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q1xMm8rJm4riqQgyy9Eydu
Wiring up onlineManager means react-query's default "online" network mode now
takes effect for mutations, which pauses them instead of running them: onMutate
still applies its optimistic update, isPending stays true forever, and onError
never fires. shouldDehydrateMutation is false, so nothing is persisted either
-- the write is simply lost when the app is killed, with the UI having shown it
as successful. Favouriting, archiving or tagging offline all hit this.
Default mutations to "always" so they fail fast against the existing onError
handlers, restoring the pre-1.10 behaviour of surfacing a real error. Proper
offline queuing needs persisted mutations plus setMutationDefaults and
resumePausedMutations, which is a bigger change than this deserves.
The share sheet had a second problem: its idle branch rendered only a spinner,
with no dismiss affordance and no auto-close timer (the existing one returns
early for "idle"). An empty share intent never calls mutate at all, so nothing
can move it out of that state and the sheet has to be force-quit. Show a
dismiss escape hatch once a save has been idle for five seconds.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q1xMm8rJm4riqQgyy9Eydu
getClient() memoised the in-flight init promise but only cleared it on the
success path. If initClient() rejected -- meilisearch restarting, or the
explicit throw on an unsupported v0.x server -- the rejected promise stayed in
initPromise, and the early-return guard replayed that same rejection on every
subsequent call for the lifetime of the process. A transient blip therefore
disabled search or the vector store until the container was restarted, with
nothing in the logs to suggest a restart was needed.
Clear it in a finally instead, in both providers.
Also stop the admin connection check from resolving the clients outside its
try blocks. getClient() connects lazily, so it can throw, and one unreachable
backend was taking down the entire Server Stats page instead of reporting
itself as disconnected alongside the healthy ones. A throw means a provider is
registered but unreachable, so report it as configured with the error attached.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q1xMm8rJm4riqQgyy9Eydu
Tag names were concatenated straight into the TAGS="..." attribute of the
exported HTML with no escaping, while the URL and title on the same line were
both escaped. A tag containing a quote breaks out of the attribute, and one
containing `">` breaks out of the <A> element entirely.
Tag names are attacker-influenced: the AI tagger writes them from raw model
output, which is parsed as z.array(z.string()) with no character validation,
so a crawled page can steer the tagger into emitting a payload. The export is
served as text/html, so the script runs when the user opens the downloaded
file. The vector is also reachable cross-user, since a crafted netscape import
plants tags that the victim later re-exports.
Escape the joined tag names with the existing helper, and cover it with a test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q1xMm8rJm4riqQgyy9Eydu