Commit Graph
9450 Commits
Author SHA1 Message Date
Marc-André Bombeck 40c310f0ad fix(account-deletion): hold frozen accounts out of the work the guard missed
The enumeration guard read any query that named userId as scoped, including
an orderBy tiebreak, so the nightly recovery, strain and stress score jobs
grouped every account's measurements, wrote scores into frozen records and
spent cap slots on them. It also looked only at findMany and groupBy under
lib/jobs.

The guard now reads a scope from a query's filter only (orderBy, by,
distinct, select and include are dropped first), covers count, aggregate,
updateMany, deleteMany and executeRaw, and walks the boot discoveries and
sweeps that live beside their domain: the rollup and mood rollup discovery,
the Fitbit jobs, the cumulative record rederivation, the intake slot dedup,
the inventory expiry and the climb backfill. What it then found is filtered
(the score cohorts through excludePendingOwners, which filters before the
cap; Fitbit sync, backfill and sleep repair; coach proposal lapsing and
reminder nagging; expired coach reminders; snooze clearing; inventory
expiry) or says why it needn't be (retention and expiry sweeps over rows,
writes scoped by id).

Jobs queued before a deletion ran anyway, since handlers act on the userId
they carry without asking again. The binding of every queue now completes a
job whose payload names an account pending deletion without loading its
handler (pending-deletion-gate). The silent medication intake push, which
does not go through the dispatcher, refuses a pending account itself.
2026-10-25 17:13:09 +01:00
Marc-André Bombeck 8783182b1e build(deps): keep @aws-sdk/client-s3 on the same release as lib-storage 2026-10-25 17:06:17 +01:00
Marc-André Bombeck 328268d894 build(deps): bump the production-weekly group and codeql-action
@aws-sdk/client-s3 and lib-storage 3.1155.0, @napi-rs/canvas 1.0.13,
@tanstack/react-virtual 3.14.22, jose 6.2.13, lucide-react 1.61.0,
nodemailer 10.2.1, and github/codeql-action 4.39.1.
2026-10-25 17:05:54 +01:00
Marc-André Bombeck 763cb48273 fix(recovery): readiness leaves accounts pending deletion out
The weekly and off-host passes skip an account pending deletion on purpose,
and the readiness read counted it anyway: within days of any deletion the
weekly copy read failing and the off-host copies stale, every admin was
pushed a problem and then a recovery after the purge, and a real failure
hid behind a signal already red. The read now filters with ACTIVE_ACCOUNT,
and the enumeration guard covers the file.
2026-10-25 17:03:10 +01:00
Marc-André Bombeck be78517500 fix(managed-profiles): pending deletion closes handovers and the Guardian count
A handover link stayed claimable after its profile was deleted: the claim
gave the profile credentials that every sign-in then refused, and the purge
erased the account someone had just taken over. Locking an account for
deletion now revokes the open links for that profile and the links that
account minted, and creating, previewing and claiming a link refuse a profile
pending deletion, the claim under the record lock.

The last-Guardian rule counted Guardians pending deletion and profiles
pending deletion. A Guardian who had deleted their only profile could not
delete their own account; two Guardians sharing a profile could both leave,
and the second one's erasure was then refused on every run. The rule now
counts only Guardians whose account is not pending deletion, ignores profiles
pending deletion, and is decided under the record lock for account deletion,
data wipe, revoking and renouncing a Guardian. An admin's undo of a managed
profile with no such Guardian left is refused (409
account.deletion.no_guardian).
2026-10-25 17:02:32 +01:00
Marc-André Bombeck b37e4f5df7 fix(admin): the last-admin rule counts only admins who can sign in
Demoting an admin counted every admin, pending deletion included, so after
one admin deleted their account the other could step down and leave the
instance with no admin who can sign in or undo the deletion. Both the
demotion and an admin's own deletion now count the other admins not pending
deletion, inside the transaction that makes the change, under one roster
lock, so two at the same moment cannot each count the other.
2026-10-25 17:02:32 +01:00
Marc-André Bombeck 4137aa546e fix(account-deletion): one failing erasure no longer holds up the others
The hourly purge rethrew any error that was not the Guardian refusal, so the
oldest due account, failing on every run, stopped every later erasure and the
sweep of expired pre-wipe copies for good.

Each account is now its own try: a failure is counted, reported through the
worker error path with its first error, and told to the admins once per
request (user.account.erasure_failed), and the run goes on. The pre-wipe
sweep and the off-host kick run in a finally. Every account the run starts is
marked in backup_pass_attempts under the purge's queue name and the mark
closes only on erasure or when the account is no longer due, so an account
that failed or was refused goes behind every other due account next time.
The run stops starting accounts once its job budget is spent, since fifty
erasures of up to two minutes each did not fit the queue's expiry.
2026-10-25 16:56:57 +01:00
Marc-André Bombeck 933be858c5 fix(images): size a GIF by its largest frame, not only its logical screen
The header check before a thumbnail decode read only the GIF logical screen.
A decoder sizes its buffer from the frame's image descriptor, so a file with a
small screen and a huge frame passed the pixel cap and allocated the full
frame. Walk every block, read each image descriptor, size the image by the
largest of screen and frames, and refuse a file that ends inside a header or
descriptor, carries an unknown block, or has no frame.
2026-10-25 16:53:31 +01:00
Marc-André Bombeck 664b46d2c6 fix(db-backup): read the off-host leg of the dump on its own
A run that kept its dump on the volume and failed the upload is the newest
good dump and the newest failure at the same instant. Compared with ">", the
card showed no failure and readiness said fresh, night after night, while no
dump left the host.

The health read now carries the off-host leg separately: when the newest dump
reached the bucket, and the newest one that did not. One pure reading of it
(databaseDumpProblems) feeds both the backups card and the readiness model.
A configured bucket that missed the newest dump is attention (offhost_failed)
with the upload's error, and failing once the newest dump there is older than
36 hours (offhost_stale) or none ever arrived (offhost_never). The card shows
the refused upload and when the bucket last got a dump. A failure at the
instant of the newest dump for another reason, retention throwing, is now
shown too.
2026-10-25 16:53:24 +01:00
Marc-André Bombeck 0085915a56 Merge remote-tracking branch 'origin/release/v1.46.0' into fix/v146-move-safety 2026-10-25 16:49:06 +01:00
Marc-André Bombeck 4887ed8071 fix(db-backup): name the archive to pg_restore --list instead of streaming it
pg_restore reads the table of contents and exits; the read stream piped into
it stayed open on the dump. The long-lived worker checks one dump a night, so
every dump retention deleted stayed allocated until the worker restarted and
the backups volume filled. runTool no longer takes a stdin stream at all.
2026-10-25 16:47:26 +01:00
Marc-André Bombeck e8c96eea88 docs: say what deletion keeps and for how long, and drop stale claims
The reproductive-health page promised that deleting all data or the
account erases at once. Both wait for the grace window now, the nightly
dumps keep a record until they age out, and the old pgdata volume is a
full copy until it is removed; the page says so and how to make deletion
final. The README, the getting-started note on majors, the move guide and
the Unraid and Portainer templates catch up with Postgres 18, the backups
volume and the recovery kit. The demo seed's spread bound leaves room for
the seed's own one-in-150 spread.
2026-10-25 16:47:00 +01:00
Marc-André Bombeck 41f2f5ac6c fix(db): leave a Postgres 16 volume alone while the database runs on it
Going back to Postgres 16 after a move that stopped half-way left the
moving marker, and the next start emptied the live 16 database and
started a second server on its data directory. db-upgrade now waits for
the database service and does nothing unless it runs Postgres 18.

Using 16 again after a finished move and then updating started the app
on the move-day copy without a word. The marker now records the old
database's last checkpoint, and a later start refuses when it moved on;
the guide says how to move the current database instead. The disk check
leaves room for the first pre-migration dump, and the move check seeds
the old database with v1.45's own migrations.
2026-10-25 16:45:19 +01:00
Marc-André Bombeck 104bb35919 test(dr): read the no-volume start log from a file
grep -q stops at the first match, and with pipefail the SIGPIPE that then
ends docker logs read as a miss, so the drill failed on a line that was
there. The Dockerfile notes that the dump directory keeps nothing without
a mount.
2026-10-25 16:35:01 +01:00
Marc-André Bombeck 6b9b9a3f8d test(documents): type the index fixtures instead of casting them away
The resource-limit cases and two older mocks now pass typed values, so the
file carries fewer as-never casts than before; the outcome list's comment
is a line shorter to keep its file at its length. Both ratchet baselines
are written down, the source size one also for offhost-backup.ts, which
had shrunk without its number following.
2026-10-25 16:24:15 +01:00
Marc-André Bombeck 430468932c docs(ops): rollback list, how admins hear about backups, GlitchTip's place
The fourth rollback step had been joined onto the paragraph after the
list; it is a list item again, with the way back to the data after it.
The disaster recovery page now says how the server tells admins about
its copies: the recovery readiness card, the recovery kit and the hourly
Backup health notification. The support page put GlitchTip under Admin,
General; the card is under Admin, Integrations, as the deploy runbook
says. The pre-migration dump notes say that without the backups volume
no dump is taken, also where the image's own directory is writable.
2026-10-25 16:01:46 +01:00
Marc-André Bombeck c7040ccdaf docs(backup): a personal export is refused when it lists a document
The restore refuses a file that carries any document as metadata only
(findIncompleteDocument), whatever the account holds; the page said it
refused when the account held a document.
2026-10-25 16:00:49 +01:00
Marc-André Bombeck aebd7ea612 docs(deploy): describe the deploy webhook's signature and strict mode
The compose comment still called DEPLOY_WEBHOOK_REQUIRE_TIMESTAMP a
requirement for a fresh timestamp header, which the route stopped
accepting in strict mode once requests could be signed, and the env
manifest named only the secret header and lacked the strict switch. Both
now say what the route checks: the secret header, which has no replay
protection, or X-Deploy-Webhook-Signature over the timestamp and body,
and that the strict setting takes signed requests only. The development
example says the same and points at the deploy runbook instead of a page
that no longer exists.
2026-10-25 16:00:35 +01:00
Marc-André Bombeck 877db9a5b7 docs(proxy): say TRUST_CF_CONNECTING_IP needs a Cloudflare-only origin
The flag makes the app believe CF-Connecting-IP from every connection,
before the peer check and whatever TRUST_PROXY_HOPS says, which is safe
only when nothing but Cloudflare can reach the origin. The reverse-proxy
page now says so with the ways to get there, the compose comment and both
example files carry the same condition, and the variable is in the env
manifest. The page's list of peers whose forwarding headers are read now
matches the code: loopback, 0.0.0.0/8, private, CGNAT, link-local,
unique-local and site-local, the reserved blocks and multicast. Those last
two stay trusted, since no internet host can connect from them; a test
pins every listed range and the Cloudflare precedence.
2026-10-25 16:00:35 +01:00
Marc-André Bombeck 8471e0edc9 fix(documents): say a PDF past the reader's limits is too large to read
A text read that crossed the isolated reader's memory or time bound came
back as a plain read error, which the index job records as decrypt-error,
so the document said its stored file could not be opened. The local
extractor now answers resource-limit for PdfResourceLimitError, the index
records it under that name, and the detail view says the PDF is too large
or complex to read here, in every locale. Like the thumbnail's refusal of
the same bytes it is final: nothing retries it on its own.
2026-10-25 16:00:35 +01:00
Marc-André Bombeck 66e92f36fe feat(recovery): judge the bucket's rule for database dumps in readiness
Only the backups page said when the bucket's lifecycle rules left
database/ out, so every dump, and every deleted account inside it, stayed
in the bucket forever without anyone being told. Readiness gains a
dump_retention signal that reads the same cached lifecycle probe: attention
when dumps go off-host and no rule covers database/, ok with the expiry
when one does, quiet when the bucket will not say, inactive without a
bucket or without off-host dumps. The hourly evaluation pushes it to the
admins like every other signal.
2026-10-25 16:00:35 +01:00
Marc-André Bombeck 351e7d1fe0 fix(backup): keep a dump locally only where it survives the container
The image creates DB_BACKUP_DIR owned by the app user, so an install that
runs it without the backups volume could write there, and every nightly and
pre-migration dump landed in the container's own layer. The next update
threw them away while the admin page, the boot line and the readiness card
called them kept on this server.

A dump directory now counts as kept only when it survives the container:
inside a container (Docker, Podman, Kubernetes, a container cgroup or an
overlay root) the directory must sit on a mount of its own that is not a
tmpfs, read from /proc/self/mountinfo with a device-number fallback;
outside one, a writable directory is a disk. Otherwise no dump is written
there: the nightly run uploads off-host only and records not_persistent,
the pre-migration dump is skipped with a warning, the boot line says the
directory is inside the container, the backups page stops promising local
dumps and ignores local-only ledger rows, and readiness fails with no
bucket and notes the bucket-only setup otherwise.

The Unraid template gains a path for the dumps, the Unraid and TrueNAS
pages say why it is needed, and the restore drill first runs the stack
without the volume and checks that nothing is kept or claimed.
2026-10-25 16:00:34 +01:00
Marc-André Bombeck fb4ab29aad Merge remote-tracking branch 'origin/release/v1.46.0' into feat/v146-grace 2026-10-25 15:28:23 +01:00
Marc-André Bombeck 1b39d93a8a docs(auth): say that credentials from before a deletion answer 401
The deletion revokes every session, refresh token and API token, so they
answer like any revoked credential; only new sign-ins get the 403
auth.account.deletion_pending, through the shared login response.
2026-10-25 15:21:10 +01:00
Marc-André Bombeck 9db5e755e6 Merge the release into the deletion grace work
The translation files merge key by key (no key changed on both sides), the
backups list carries both the database dump and the pre-wipe copy window,
and the backup-restore doc keeps the shorter rewrite plus the dump in the
bucket and a lifecycle rule that covers database/.
2026-10-25 15:18:46 +01:00
Marc-André Bombeck 8678a3edc4 test(recovery): keep the weekly-copy push test to its one signal
With the real dump health wired in, an empty test database also reports
a missing dump. The file follows the weekly copy alone; the dump signal
is judged in the readiness unit tests.
2026-10-25 15:16:14 +01:00
Marc-André Bombeck da9d941a24 Merge the release with the security fixes into the backup health work 2026-10-25 15:03:17 +01:00
Marc-André Bombeck 13e62bbb92 docs(coach): keep the labs block note to its old length 2026-10-25 14:50:05 +01:00
Marc-André Bombeck e88dcd2c53 Merge the release into the backup health work and read the real dump health
The readiness model now takes the database dump's state from
getDatabaseBackupHealth, and the dump test reaches the off-host key id
where it moved.
2026-10-25 14:40:31 +01:00
Marc-André Bombeck c25e9fa4e9 refactor: drop an unused test reset for the public-peer warning 2026-10-25 14:28:32 +01:00
Marc-André Bombeck d902125371 feat(account): tell admins about a refused erasure, and what undo leaves revoked
When the grace window ends for an account that has become the last
active guardian of a managed profile, the purge job refuses to erase it
and the account stays locked. Until now only the worker error log said
so. Every admin now gets one localized notification naming the account
and the ways out (another guardian, deleting the profile, or undoing
the deletion). An audit row on the account records that it was reported,
so the hourly retries stay quiet; a deletion undone and asked for again
is reported afresh.

The undo confirmation in the admin users list now says that sign-in
works again but API tokens, share links and trusted devices stay revoked
and have to be set up again (for a managed profile: its share links and
tokens).
2026-10-25 14:28:15 +01:00
Marc-André Bombeck 694befd635 fix(admin): say what actually ages a stored account copy out
The backups page stated "Retention: 90 days", from a constant whose
comment said it matched a backup-prune job. There is no such job: nothing
deletes a stored copy by age. The weekly copy is replaced in place, one
per account; the copy taken before "Delete all data" is deleted by the
account-deletion purge when the grace window ends; an uploaded copy stays
until its account is erased.

The list now carries preWipeCopyDays (the grace window, 0 when no such
copy is taken) instead of retentionDays, and the page says exactly the
three facts above, in every language.

Also drops a re-export of the JSON import row limit nothing imports.
2026-10-25 14:28:07 +01:00
Marc-André Bombeck 459b5baa9d fix(backup): read ECG traces a strip at a time
The shared backup reader kept documents without their stored bytes but
kept every ECG strip whole, trace included. A long watch history is
hundreds of strips of tens to hundreds of kilobytes each, so an upload,
preview or restore of such a copy held the whole history in memory.

The first read now keeps each strip without its trace (a short stand-in,
after the key it was written under is recorded and its form checked),
and forEachEcgTrace reads the traces again one strip at a time. The ECG
restore writes each strip from that second read, as the document restore
already did, and refuses a file whose two reads disagree on the count.

The upload memory test gains an ECG-heavy file: 150 strips, about 200 MB.
The old reader held 197 MB of it and failed the 48 MB bound; the new one
holds about 1 MB, also under a 524 MB heap.
2026-10-25 14:27:56 +01:00
Marc-André Bombeck 0a053d6884 feat(admin): name a switched-off dump and a newly listed kit key plainly
A switched-off scheduled dump is attention now, sent once and never
reminded, with how to turn it back on (DB_BACKUP_ENABLED). Dumps are on
by default, so off is a choice that leaves no copy able to rebuild the
host. An install that never wrote a dump and has nowhere to write one
stays quiet.

The out-of-date kit notice says what changed: the kit now also lists a
key the last confirmation did not cover, for example API_TOKEN_HMAC_KEY
on an instance upgraded from the one-key step, and that nothing broke.

The French kit and readiness strings use vous, the form the rest of the
French bundle uses.
2026-10-25 14:09:41 +01:00
Marc-André Bombeck e79ea08b13 Merge remote-tracking branch 'origin/release/v1.46.0' into fix/v146-security 2026-10-25 14:07:01 +01:00
Marc-André Bombeck 21aa791c3a Merge remote-tracking branch 'origin/release/v1.46.0' into fix/v146-security
# Conflicts:
#	src/lib/ai/coach/labs-snapshot.ts
#	src/lib/ai/coach/snapshot.ts
2026-10-25 14:05:23 +01:00
Marc-André Bombeck 2f19ca1f73 test(ratchet): write back the smaller sizes the fixes left
apiHandler's request wrapper fell under the 200-line mark, and the
reminder-check route and the Coach snapshot shrank by a line or two.
2026-10-25 13:52:04 +01:00
Marc-André Bombeck f7030feba7 docs(ops): say that the pre-upgrade volume keeps data erasure never reaches
The Postgres major-upgrade guide suggested removing the old pgdata volume
"when you are sure". That volume is a full copy of the database as it stood
before the move: account deletions, trash purges and key rotations after it
never reach it. The guide now says so and makes removing it part of the
move.
2026-10-25 13:52:04 +01:00
Marc-André Bombeck a20897b305 fix(monitoring): sign the GlitchTip relay in and strip Prisma call arguments
The browser error relay forwarded anything posted to it into the operator's
GlitchTip without a session, limited only per address, so anyone could
fill the incident list. Separately, a Prisma client error prints the
failed call into its message and stack, with the arguments object (a
reading's value, a note's text) when no source map is at hand, and the
server sent that to GlitchTip and the logs after a credential scrub that
does not look for it.

The relay now needs a signed-in session and is limited per account; errors
on pages before sign-in go unreported. The central redaction cuts a Prisma
call's printed arguments, keeping the call, a reason line that names a
field, and the stack frames, and the GlitchTip sender applies the
redaction to every event at the egress boundary, whichever caller built it.
2026-10-25 13:52:04 +01:00
Marc-André Bombeck f2a9b0fc0e fix(deploy-webhook): accept signed requests and stop calling a bare timestamp replay protection
The deploy-status webhook authenticated with a static secret in a header,
and the timestamp the docs called replay protection was covered by nothing:
whoever captures one request holds the secret and can send a fresh request
with any content. The strict setting, documented as closing the replay
window completely, only made that timestamp mandatory.

A sender can now sign instead: X-Deploy-Webhook-Signature is the
HMAC-SHA256 of the timestamp and the exact body under the shared secret,
with the timestamp within five minutes, and the secret never travels. The
strict setting accepts only signed requests. The secret header still works
by default for senders that cannot sign, and the docs describe its
timestamp as the staleness hint it is.
2026-10-25 13:52:04 +01:00
Marc-André Bombeck 807d483b89 fix(egress): check every redirect hop and close the reserved-range gaps
safeFetch with requirePublicHost vetted the first URL and pinned DNS
answers in the connector, but Node never looks up an IP literal, so a
followed redirect to a literal private or metadata address was dialled
unchecked. No caller follows redirects to a user-chosen host today; the
floor did not hold for the next one that would.

With requirePublicHost, redirects are now followed one hop at a time and
every location goes through isPublicUrl before it is dialled, at most five
hops. isPublicIp also refuses the benchmarking, protocol-assignment,
multicast, reserved and broadcast IPv4 ranges, site-local and multicast
IPv6 and the IPv4-translated spelling of a private address, and isPublicUrl
strips a trailing dot before the hostname denylist.
2026-10-25 13:52:04 +01:00
Marc-André Bombeck e021adde40 fix(notifications): limit quick answers per account, junk in a bucket of its own
The quick-answer route charged one per-address bucket before it read the
token. Where the client address cannot be read, every caller shares that
bucket, so two junk requests a minute switched off every person's
notification quick answers.

The token is an HMAC and is now checked first, which costs one hash and no
database read. A genuine token is limited per account; anything else is
limited per address in a separate bucket that genuine answers never draw
from.
2026-10-25 13:52:04 +01:00
Marc-André Bombeck cd96cb3e60 fix(proxy): keep API paths ending in a file extension behind the proxy
The proxy matcher left out every path ending in an image or text extension
so static files skip it. The exclusion was not anchored, so an API path
whose last segment the caller chooses (a custom tag name ending in .png)
skipped the proxy too, and with it the demo-mode write refusal, the
worker-only refusal and the security headers. Authentication was unaffected.

The extension exclusion now applies outside /api/ only.
2026-10-25 13:52:04 +01:00
Marc-André Bombeck 0039e112dd fix(mcp): fence medication and lab names in tool results
The MCP server instructions promise that the person's free text comes back
wrapped in USER_TEXT markers, but get_medication_schedule returned
medication names and doses as plain strings, and get_labs returned analyte,
panel and unit names that can come straight out of an uploaded document.
Text written to read like an instruction reached the connecting client's
model unmarked. The marker scrub also removed only the exact uppercase
spelling, so a lower-case or spaced marker survived inside fenced text.

Both tools now fence those fields, and the scrub removes a marker in any
case and with any spacing inside the brackets.
2026-10-25 13:52:04 +01:00
Marc-André Bombeck e3598985f5 fix(ingest): answer a scoped token the same for unknown and out-of-scope names
A token scoped to one medication got 404 for a name that matched none of
the owner's medications and 403 for one that matched outside its scope, so
it could confirm which medications exist by name. Its idempotency replay
also ran before the scope check and returned any of the owner's doses
stored under the key.

A token without the wildcard now gets the same 403 for both, the replay is
looked up only after the scope check, and a key already spent on another
medication answers 409 without that dose. The wildcard token keeps its 404.
2026-10-25 13:52:04 +01:00
Marc-André Bombeck 989381b3f6 fix(share): stop serving shared documents once the documents module is off
A share link gated its report on the doctorReport module but listed and
served its attached documents unconditionally, although the comment beside
the gate said the documents kept a gate of their own. An owner who switched
the documents module off still had every attached file decrypted for anyone
holding the link and its passphrase.

The share page lists no document while the module is off, and the document
route answers 404, the same as for an unknown document, before it looks the
membership up.
2026-10-25 13:52:04 +01:00
Marc-André Bombeck d1fcbca63b fix(mcp): name the host an authorization goes to on the consent page
Dynamic client registration is open by specification and the client names
itself, so anyone could register under a well-known assistant's name with a
callback on a host of their own and send a signed-in person the authorize
link. The consent page showed the borrowed name and "Unverified
application", never where the code would be sent.

Every consent page now says where the access goes: the callback's host, or
an application on this device for a loopback callback, and the Allow button
names the host. For a self-registered client whose callback leaves the
device, a warning that names the host leads the page, above the
self-reported name. Registration stays open: the hosted assistants register
this way with their own https callback.
2026-10-25 13:52:04 +01:00
Marc-André Bombeck 77ace106b3 fix(http): refuse state changes a browser sends from another site
The session cookie is SameSite=Lax, and SameSite compares the registrable
domain, so a page on a sibling subdomain of the instance could send a
CORS-simple POST with the person's cookie attached. safeJson accepted any
Content-Type containing "application/json", so a text/plain body that
mentions it in a parameter, which needs no preflight, was parsed as the
person's own request: readings added, entries deleted, integrations
disconnected, a delegate invited within the re-proof window.

apiHandler now refuses a POST, PUT, PATCH or DELETE before authentication
when the browser marks it Sec-Fetch-Site same-site or cross-site, or, from
a browser that sends no fetch metadata, when its Origin names a host other
than the one the request went to or APP_URL. A request with an
Authorization header, or with neither header (the phone app, scripts,
webhook senders), is unaffected, as are the webhook and URL-credential
routes that authenticate with their own secret. The JSON media type is
compared exactly, with parameters allowed, in safeJson and in every route
that branched on a substring of it.
2026-10-25 13:52:04 +01:00
Marc-André Bombeck fa50bcd1cb fix(ai): keep lab values out of every model prompt while Labs is off
The Coach snapshot attached its labs block regardless of the module, a
decision from before Labs could be switched off, while the snapshot's own
prelude says a disabled domain never reaches the model. With Labs off, the
Coach prompt, its get_labs tool, the daily briefing's flagged-marker block,
the biomarker status note, the reaction line to a new panel and the MCP
lab_trend_brief prompt all still read lab values for a model.

Each of those reads now asks the labs module first and reads nothing when it
is off: the Coach tool answers module_disabled, the briefing omits the
block, the status note neither writes nor serves text, the reaction line
gets no value, and the MCP prompt refuses like get_labs and the doctor-visit
prompt already did. The data routes stay as they are. A structural guard
holds every lab-table reader under the model-input paths to the gate or to
a listed reason its rows never reach a model.
2026-10-25 13:52:04 +01:00
Marc-André Bombeck 6fbfe71955 fix(auth): stop exempting a previously used address from the sign-in throttle
The per-account password throttle skipped every attempt that came from an
address one of the account's sessions or device logins had used in the last
thirty days. An address is the easiest thing about a request to share or
claim: in a household or an office the guesser has the owner's address, and
a direct caller could name it in a forwarding header. Either way the
throttle switched off and guessing was bounded only by the hash cost.

A remembered browser (the trusted-device cookie) and a phone presenting the
device id of one of the account's logins still pass; everything else gets
the growing wait.
2026-10-25 13:52:04 +01:00