Commit Graph
9450 Commits
Author SHA1 Message Date
Marc-André Bombeck 44d8181cb4 docs(openapi): publish the sleep score value
The derived envelope types `value` as an open record, so the sleep
score's `rhythmNights` and per-sub-score `gap` were on the wire but in
no schema. `SleepScoreValue` joins `WellnessScoreValue` in the forced
components slot, the envelope's `value` description names it, and a
contract test holds the published field set to the one the engine
emits, for a scored rhythm and for one withheld for too few nights.

Refs #1204
2026-10-19 18:56:06 +02:00
Marc-André Bombeck d08e2b4e6d fix(observability): keep heap samples out of the admin log ring and harden the snapshot directory
runtime.heap.sampled is written every minute while the heap is above
70 % of its limit, which turned the 500-entry ring behind the admin log
page over in hours and pushed out the events an operator opens it for.
emitEvent takes a skipMemoryRing option; the sample sets it and still
reaches stdout and Loki, while the pressure and snapshot lines go
everywhere as before.

The snapshot directory was /tmp/healthlog-heap-<pid>, a predictable name
(pid 1 in most containers) that mkdirSync accepted when it already
existed or was a symlink. It now comes from mkdtempSync and is checked
with lstat to be a real directory owned by the process user and closed
to others before the snapshot is written.
2026-10-19 18:46:43 +02:00
Dareeon 50e901f807 i18n(pt-BR): add initial translation (common, apiErrors) 2026-10-19 13:46:00 -03:00
Marc-André Bombeck 3bdd53ab76 fix(mcp): answer GET and DELETE with 405 so clients stop reconnecting
The route is stateless and returns one buffered JSON response per call,
so it opens no server stream and holds no session. GET let the transport
open an event stream that the handler closed again at once; a client
read that as a dropped connection and reconnected about once a second,
each attempt costing a rate-limit slot and a module lookup. GET and
DELETE now answer 405 with Allow: POST after authentication, which is
what the transport specification asks of a server without that stream,
and an unauthenticated GET still gets the 401 challenge.
2026-10-19 18:34:37 +02:00
Marc-André Bombeck c016ec8aec fix(recovery): claim no Oura permission the server cannot prove
The empty recovery note told every connected Oura account without a
recorded permission gap that its permission covers resilience. The
server cannot know that: the scope note is cleared on each reconnect
and written only by a sync that reached Oura, and a missing health
block says nothing either way. The note now asks for a reconnect only
when the gap is recorded and otherwise says only that no level has
arrived, pointing at the Oura card. It also waits for the connection
status instead of showing the general note first and swapping it.

Refs #1204
2026-10-19 18:31:45 +02:00
Marc-André Bombeck 8c745fca8d fix(sleep): treat a short night as the night, only a daytime nap leaves the clock
Keeping a nap-only day off the sleep rhythm used three hours asleep as
the line, which also took every shorter night off the clock: someone
who sleeps two and a half hours a night (insomnia, a newborn, split
sleep) lost consistency, timing, chronotype and the health score's
regularity altogether. A session now leaves the clock only when it is
short AND centred in the daytime, outside 20:00 to 10:00 local time.
A short night keeps its place, a long daytime sleep after a night
shift stays a main sleep, and the withheld-timing note now reads that
the latest day had only a nap.

Refs #1204
2026-10-19 18:30:06 +02:00
Marc-André Bombeck 3a9f619001 chore(release): v1.43.6 2026-10-19 18:18:12 +02:00
Marc-André Bombeck 9d6bfe2624 Merge branch 'fix/v1436-gh' into release/v1.43.6 2026-10-19 17:26:51 +02:00
Marc-André Bombeck 8ce8065372 test(integration): size the multi-year backfill run from the environment
The multi-year Google Health backfill run walked three and four years of
heart rate on every pull request, some 3.7 million readings in over twenty
minutes, which filled most of its shard's time budget. What a run costs is
its pages, so a shorter history only helps with shorter slices.

HEALTHLOG_BACKFILL_TEST_DAYS now sets the size. Unset, the runs walk 120
and 160 days in 50-page slices with the faults at the same places within
them and the light collections already walked, about a tenth of the
pages. The heap growth bound applies to the full run only. The full three
and four years run nightly and on demand in integration-long.yml.

Refs #1220
2026-10-19 17:26:04 +02:00
Marc-André Bombeck f6b09f07cd feat(integrations): say where an imported Google Health history ends
The two status routes publish backfillEnded: each data type whose
backfill stopped short of the start of its history, with the instant it
reaches back to and whether the horizon or a walk that stopped moving
ended it. The integration card lists them in place of an import that
would otherwise read as still loading, in all seven locales.

Refs #1220
2026-10-19 17:25:57 +02:00
Marc-André Bombeck 72c259d85c fix(google-health): bound the backfill by progress and a horizon, not pages
A resumable collection counted the pages it had written across every
backfill attempt and stopped at its ceiling (10,000 for heart rate). A
watch that records heart rate every few seconds reaches that count two to
three years back, and since it never reset, every later slice stopped at
the same place, failed as stuck, and the backfill was never stamped
complete.

A walk is now stopped only when it stops moving: 20 pages in a row that
reach no further back than the pages before them end it. A history
backfill reads back at most GOOGLE_HEALTH_BACKFILL_HORIZON_YEARS (20 when
unset). A data type that ends either way counts as finished, and where and
why it ended is kept in backfillProgress past the completion stamp. An
oldest-first list is read whole, as before. The slice chain's ceiling is
derived from sixty days of back-to-back slices instead of the old page
count. An account left at the old ceiling continues from its edge with
the next slice.

Refs #1220
2026-10-19 17:25:48 +02:00
Marc-André Bombeck 0e6e50fc7b Merge branch 'fix/v1436-ui' into release/v1.43.6 2026-10-19 17:04:31 +02:00
Marc-André Bombeck 362afe2333 Merge branch 'fix/v1436-heap' into release/v1.43.6 2026-10-19 17:04:30 +02:00
Marc-André Bombeck 09c93cd547 Merge branch 'fix/v1436-oura' into release/v1.43.6 2026-10-19 17:04:29 +02:00
Marc-André Bombeck 378de45f88 Merge branch 'fix/v1436-locks' into release/v1.43.6 2026-10-19 17:04:29 +02:00
Marc-André Bombeck d953789ecc chore: spell the value-range separator as an escape
The template key in the measurement list carried a literal NUL byte,
which made tools treat the file as binary.
2026-10-19 16:43:54 +02:00
Marc-André Bombeck 38c106e358 fix(ui): draw no empty frame and say when a read failed
"Since the last visit" was wrapped in a card by the visits section, so
the card stayed on screen empty while the window loaded, when it held
nothing and when the read failed. The component now owns its card and
draws it only with a row; a failed read shows a compact error row.

The same shapes elsewhere: the caffeine card and the mood forecast no
longer open a loading card that most often collapses, and a failed read
on them, the mood tag catalogue, the usual-range strip, the cycle ring
and the measurement drill-down now says so with a retry instead of
passing for no data. The scores strip keeps the ring's cell on a failed
calendar read. The mental-health card no longer draws an empty action
slot for a viewer without the right to start.

The frame guard now also flags a frame whose only content sits behind a
condition.

Refs #1241
2026-10-19 16:43:52 +02:00
Marc-André Bombeck d00281f35a test: count neither a comment nor a zero offset as a pager
The client fetch-limit guard took any `offset` within three lines as a
pager. The day-grouped measurement list passed it with `limit` 5000
between a comment about pagination and `offset` pinned to "0", while it
drew a whole history on one page. Comment lines, trailing comments and
inline block comments no longer count, nor does an offset pinned to a
literal zero in any of the three spellings. The old list shape is a
test case and fails the guard.

Refs #1245
2026-10-19 16:43:50 +02:00
Marc-André Bombeck cfd6219981 fix(measurements): page day sums and sleep nights in SQL
The day-grouped list read the newest `limit` raw rows (the web asked
for 5000), folded them into days and returned every day at once: one
page of thousands of rows, and every day older than the 5000th raw row
missing without a word. The sleep list did the same at a year.

Both now pick the page in Postgres. The day mode selects the page's
local days with the distinct-day total from `COUNT(*) OVER ()` in the
same query, then folds only those days into per-source groups in SQL
and lets the source ladder choose among the groups, so no raw row
reaches the process. The sleep mode cuts sessions with the same gap
rule as the reconstruction, pages wake days the same way and reads only
the page's stage rows; the 365-day cut and `meta.windowFrom` are gone.
A sleep page holds at most 1000 nights, echoed in `meta.limit`.

The web list pages both modes at 25 like the per-sample list. The
`partial` day marker is dropped from the contract; no day is cut any
more.

Refs #1245
2026-10-19 16:43:39 +02:00
Marc-André Bombeck 16024397c8 feat(observability): log heap samples and a one-time pressure report
runtime.heap.sampled records heap, old space, RSS, external memory and
the work in flight every five minutes, every minute at or above 70 % of
the heap limit. At 85 % runtime.heap.pressure lists the oldest requests
and jobs in flight and the routes most of them share, once, re-armed
only after the heap falls below 70 % and thirty minutes pass. A
heap-limit crash leaves no other record of what the process was doing.

HEALTHLOG_HEAP_SNAPSHOT=1 adds one heap snapshot at that moment into
HEALTHLOG_DIAG_DIR (0700 directory, 0600 file), off by default.
docs/ops/heap-diagnostics.md warns that a snapshot holds decrypted
health data and keys and says how to delete it.
2026-10-19 16:00:53 +02:00
Marc-André Bombeck 5923dac8aa feat(observability): keep a bounded register of requests and jobs in flight
Every HTTP request the server accepts is registered through Node's
http.server.request.start diagnostics channel and removed when its
response closes, finished or aborted, so pages, RSC payloads and server
actions are covered alongside API routes. Jobs register around their
handler in observeJob. The register holds strings only, cuts the query
string on entry, caps at 2000 entries and counts what it refuses; route
labels are scrubbed and collapsed only when a report is written.
2026-10-19 16:00:45 +02:00
Marc-André Bombeck d38ceb4beb fix(sleep): say why consistency or timing has no value
A rhythm sub-score below three nights with a main sleep was dropped from
the blend and its row read "no data", although the nights were there;
since a nap-only day no longer has a place on the clock, a ring worn
for only a few nights reaches that state more often. Each withheld
consistency or timing sub-score now carries the reason
(`few_nights`, `no_main_sleep`) and the score value carries how many
nights the rhythm is built from, so the row reads "not enough nights
yet (2 of 3)" or "latest night had no main sleep" instead. Neither is
ever shown as 0.

Refs #1204
2026-10-19 15:15:46 +02:00
Marc-André Bombeck 1606099005 build(deps): keep the Next.js packages out of the weekly groups
A red Next bump inside the production group held the twelve harmless
updates next to it red as well. The three Next packages open as single
pull requests, so each is judged on its own checks.
2026-10-19 15:09:50 +02:00
Marc-André Bombeck 7b17ec819a chore: drop personal names from comments and test fixtures
Comments referred to the maintainer by name, and a test fixture carried a
full real name. Comments describe the behaviour; fixtures use placeholders.
2026-10-19 14:57:00 +02:00
Marc-André Bombeck 146fcac079 chore: write NUL as an escape instead of a raw byte
Raw NUL bytes in source files make grep and ripgrep treat the file as
binary and skip it, so sweeps silently miss it. The affected string and
regex literals use the escape now; behaviour is unchanged.
2026-10-19 14:57:00 +02:00
Marc-André Bombeck ff3559c169 fix(workouts): render workout dates in the profile timezone
The workout list, the workout header and the dashboard tile formatted
instants through the shared formatter cache without a timeZone, so they
followed the browser's zone. The year comparison in the list used the
browser's calendar as well. All three now take the profile zone, and the
timezone guard also matches getDateTimeFormat(locale, options) calls that
leave the zone out.
2026-10-19 14:56:53 +02:00
Rob de Graaf c6f61fad33 feat(i18n): add Dutch as an eighth end-to-end locale
Adds messages/nl.json (AI-initial, informal je/jij register, full key
parity with en.json) and registers nl everywhere the locale list lives:
config, loaders, Intl/date-fns mapping, zod enums, OpenAPI spec, scripts,
the managed-profile picker and the notification locale guards.

Dutch follows the Korean pattern for the AI surfaces (English body plus a
Dutch reply-language footer, no safety-contract matrix) and gets its own
banks in the outbound safety screen (dose, risk, causal, continuation),
the prose-grounding and citation-coverage tables, the reference-range
prose and the no-key fallback copy, each with Dutch test fixtures.

PHQ-9 / GAD-7 / WHO-5 are not marked validated for nl yet: the Dutch
wording has not been read against the published forms, so Dutch readers
get the English items plus the existing note until it has.
2026-10-19 11:06:55 +02:00
Marc-André Bombeck 40b1389970 fix(recovery): name what is missing on an Oura account's empty recovery page
An Oura ring fills the recovery page through its resilience level alone.
The empty note listed every source that could fill the page, so a
connected ring with no level read the same as no wearable at all. The
note now follows the server's view of the Oura connection: when the
grant leaves out the heart-health data the level sits behind, it asks
for one reconnect and links to the integrations settings; when the grant
covers it, it says Oura has not sent a level for this account yet.

Refs #1204
2026-10-19 10:53:59 +02:00
Marc-André Bombeck 7f2f473a58 fix(sleep): keep a nap-only day off the sleep rhythm
A day whose ring or watch recorded no night has a nap as its largest
session, and its afternoon midpoint sat hours from every other night.
One such day in a month pushed the midpoint spread past the 90-minute
full scale and read a regular rhythm as Consistency 0. A day's midpoint
now needs a main session of at least three hours asleep, the line
wearables draw between a nap and a main sleep. The day keeps its
minutes; it only has no place on the clock for consistency, timing,
chronotype and the health score's regularity.

Refs #1204
2026-10-19 10:25:30 +02:00
dependabot[bot] 01bf8fbb64 build(deps): bump the codeql-action group with 3 updates (#1242)
Bumps the codeql-action group with 3 updates: [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/analyze](https://github.com/github/codeql-action) and [github/codeql-action/upload-sarif](https://github.com/github/codeql-action).


Updates `github/codeql-action/init` from 4.38.3 to 4.39.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/24c54180a607b1449ed407dd24f251e4e9147c8d...6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee)

Updates `github/codeql-action/analyze` from 4.38.3 to 4.39.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/24c54180a607b1449ed407dd24f251e4e9147c8d...6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee)

Updates `github/codeql-action/upload-sarif` from 4.38.3 to 4.39.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/24c54180a607b1449ed407dd24f251e4e9147c8d...6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee)

---
updated-dependencies:
- dependency-name: github/codeql-action/init
  dependency-version: 4.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: codeql-action
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: codeql-action
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 4.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: codeql-action
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-18 16:35:04 +00:00
Marc-André Bombeck dac778dac7 Merge pull request #1240
Release v1.43.5
v1.43.5
2026-10-18 13:51:27 +02:00
Marc-André Bombeck ade7a252f0 chore(release): v1.43.5 2026-10-18 13:11:47 +02:00
Marc-André Bombeck 947551cd8e fix(jobs): name the stage and condition when the tombstone purge fails
A failed run threw the bare Prisma error, which the support report showed
as "Error". The reason now carries the stage and the database condition
(statement_timeout, lock_timeout, pool_timeout, ...), and the report's
error class reads the SQLSTATE out of a Prisma message.

Refs #1239
2026-10-18 13:10:21 +02:00
Marc-André Bombeck 1d7077286f feat(admin): flag a heap limit below 700 MB with the setting to raise
The support report and the system status now say in one sentence when the
heap is the 1 GB-container size and point at APP_MEMORY_LIMIT.

Refs #1239
2026-10-18 13:10:20 +02:00
Marc-André Bombeck ac047854ba fix(backup): stream documents and ECG traces through backup and restore
The weekly copy read every stored document and ECG trace whole, then held
their base64, the section JSON and the preview copies beside them. A vault
of 190 MB peaked at 930 MB of heap and took a 524 MB process down on every
run. Documents now leave the database one at a time in 1.5 MB slices, the
traces a page at a time, every other section is written an element at a
time, and a sealed piece never exceeds its chunk size. The restore keeps
document bytes out of its first read and writes each document from a
second one. Same file format, byte for byte.

Refs #1239
2026-10-18 13:10:19 +02:00
Marc-André Bombeck 9db0f09ba5 Merge pull request #1238
Release v1.43.4
v1.43.4
2026-10-17 12:07:08 +02:00
Marc-André Bombeck 5afa22b382 chore(release): v1.43.4 2026-10-17 11:37:20 +02:00
Marc-André Bombeck e8e3cf4cd3 Merge remote-tracking branch 'origin/main' into fix/v1434-illness-symptoms 2026-10-17 11:35:55 +02:00
Marc-André Bombeck 271e8d6ea8 Merge pull request #1236
Release v1.43.3
v1.43.3
2026-10-17 11:09:58 +02:00
Marc-André Bombeck c1df60693b fix(symptoms): assign an occurrence to an illness by its link, not its time
The episode detail listed every symptom occurrence that fell inside the
episode's onset-to-recovery span as well as the ones filed against it, so a
long-running condition showed the symptoms of every illness overlapping it,
and picking an illness in the entry sheet changed nothing on the page. The
occurrence edit sheet carried no illness link at all, so once logged the
assignment could be neither seen nor changed.

- The episode card reads only the occurrences filed against the episode
  (`SymptomEvent.episodeId`), with a guard against any other.
- The edit sheet loads the link and edits it like the time and intensity;
  an unchanged link is not re-sent, so a link the list cannot show stays.
- One rule decides which episodes may take an occurrence
  (`isEpisodeLinkableAt`): an open one, or a resolved one whose span holds
  the occurrence's time. The server checks it against the time after the
  edit, and the entry and edit sheets offer exactly what it admits, so a
  backdated occurrence can be filed with an illness that has since ended.
- The illness page names the illness on each occurrence row.
- The day view opens a symptom's illness only while that episode exists.

The stored links were always written correctly, so no data repair is needed.

Refs #1237
2026-10-17 11:00:02 +02:00
Marc-André Bombeck d67032db96 test(e2e): wait for the document title before scanning the handover screens
The decision screen is reached by a client navigation, during which the old
route's title unmounts and the new one mounts from a suspended metadata
boundary that can reveal after the body has painted. Under load the scan ran
in that stretch and reported document-title. The helper now asserts the
title first, so a screen that never gets one still fails.

Refs #959
2026-10-17 10:43:59 +02:00
Marc-André Bombeck b254814bae test(e2e): walk the re-proof dialog when the rotation's step-up has aged out
An aged-out step-up no longer ends in a refusal sentence on the card: the
refusal names the proofs it takes and the shared re-proof dialog opens above
the pending confirmation. The journey now asserts that dialog, that
cancelling it sends nothing more and leaves the codes as they were, and that
proving the factor there runs the rotation once more and issues a fresh
batch. The proof's code waits for the step after the enrolment code, which
the replay floor would refuse.

Refs #959
2026-10-17 10:43:57 +02:00
Marc-André Bombeck 792fc89a93 fix(auth): count a passkey as the second factor on the managed-profile acts
An account whose only strong credential is a primary passkey was refused
auth.stepup.mfa_not_enrolled on every action behind requireFreshMfa, so it
could not create, edit, hand over or delete a managed profile or manage its
guardians, although a passkey sign-in or re-proof already stamps the
session the gate reads.

requireFreshMfa now defaults to the any-possession proof source: a passkey
makes the gate reachable, and a passkey re-proof through the dialog clears
it. The refusal offers passkey in meta.methods, never the password.

hasSecondFactorEnrolled keeps its narrow meaning on purpose. It decides
whether the conditional gates apply at all (account deletion, the data
reset, the password change, the export, offering manage access, the
recent-proof branches and the native Bearer rules), so none of those
change, and password sign-in still challenges only TOTP and security keys.
The MFA-management routes keep the second-factor source. The decision and
its limits are written beside FreshFactorProofSource.

Copy, the OpenAPI description and the sharing and key-rotation docs say
"second factor or passkey" where this gate applies.

Refs #959
2026-10-17 09:56:30 +02:00
Marc-André Bombeck d9b6061d28 chore(bundle): raise the aggregate budget for the re-proof dialog callers
The re-proof dialog now opens from every fresh-factor refusal, so the hook
and the dialog are imported by more on-demand chunks, which the aggregate
counts once each. The build measured 4977 KB gz against the 4975 cap; every
watched route stays inside its allowance. Loading the dialog through
next/dynamic measured 4979 and grew every watched route, so it is not kept.

Refs #959
2026-10-17 09:56:30 +02:00
dependabot[bot] 048ef0020e build(deps-dev): bump the development-weekly group across 1 directory with 8 updates (#1235)
Bumps the development-weekly group with 8 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@testcontainers/postgresql](https://github.com/testcontainers/testcontainers-node) | `12.2.0` | `12.2.1` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.6.4` | `26.6.5` |
| [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) | `5.0.3` | `5.0.4` |
| [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip) | `6.40.0` | `6.41.0` |
| [lefthook](https://github.com/evilmartians/lefthook) | `2.2.1` | `2.2.2` |
| [prettier](https://github.com/prettier/prettier) | `3.9.9` | `3.9.10` |
| [testcontainers](https://github.com/testcontainers/testcontainers-node) | `12.2.0` | `12.2.1` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `5.0.3` | `5.0.4` |



Updates `@testcontainers/postgresql` from 12.2.0 to 12.2.1
- [Release notes](https://github.com/testcontainers/testcontainers-node/releases)
- [Commits](https://github.com/testcontainers/testcontainers-node/compare/v12.2.0...v12.2.1)

Updates `@types/node` from 26.6.4 to 26.6.5
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@vitest/coverage-v8` from 5.0.3 to 5.0.4
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v5.0.4/packages/coverage-v8)

Updates `knip` from 6.40.0 to 6.41.0
- [Release notes](https://github.com/webpro-nl/knip/releases)
- [Commits](https://github.com/webpro-nl/knip/commits/knip@6.41.0/packages/knip)

Updates `lefthook` from 2.2.1 to 2.2.2
- [Release notes](https://github.com/evilmartians/lefthook/releases)
- [Changelog](https://github.com/evilmartians/lefthook/blob/master/CHANGELOG.md)
- [Commits](https://github.com/evilmartians/lefthook/compare/v2.2.1...v2.2.2)

Updates `prettier` from 3.9.9 to 3.9.10
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.9.9...3.9.10)

Updates `testcontainers` from 12.2.0 to 12.2.1
- [Release notes](https://github.com/testcontainers/testcontainers-node/releases)
- [Commits](https://github.com/testcontainers/testcontainers-node/compare/v12.2.0...v12.2.1)

Updates `vitest` from 5.0.3 to 5.0.4
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v5.0.4/packages/vitest)

---
updated-dependencies:
- dependency-name: "@testcontainers/postgresql"
  dependency-version: 12.2.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-weekly
- dependency-name: "@types/node"
  dependency-version: 26.6.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-weekly
- dependency-name: "@vitest/coverage-v8"
  dependency-version: 5.0.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-weekly
- dependency-name: knip
  dependency-version: 6.41.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-weekly
- dependency-name: lefthook
  dependency-version: 2.2.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-weekly
- dependency-name: prettier
  dependency-version: 3.9.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-weekly
- dependency-name: testcontainers
  dependency-version: 12.2.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-weekly
- dependency-name: vitest
  dependency-version: 5.0.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-weekly
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-17 07:28:11 +00:00
Marc-André Bombeck bb53c7484f chore(release): v1.43.3 2026-10-17 09:17:59 +02:00
Marc-André Bombeck 99f0607a69 fix(auth): let every fresh-factor refusal open the re-proof dialog
A Guardian signed in through single sign-on with a second factor enrolled
could not hand a managed profile over once five minutes had passed since
sign-in: the handover answered 401 auth.stepup.required, and the panel
printed "Confirm your second factor, then try again" with no way to do so.

The same held for every action behind requireFreshMfa: creating, editing
and ending a managed profile, adding and removing a Guardian, disabling
the second factor, rotating recovery codes, removing a security key or a
passkey, changing the password, the data wipe, account deletion and the
key rotation. The recent-proof routes had the dialog; these had text.

- requireFreshMfa names the proofs that clear a stale stamp in
  meta.methods (the possession proofs the account holds, never a
  password, which stamps reproofAt only).
- The re-proof flow treats that refusal like auth.reproof.required:
  open the dialog, post the proof to /api/auth/reproof, send the request
  once more. The flow moves into a React-free module so it can be driven
  in tests; the hook binds it to the dialog.
- The managed-profile mutation hooks take a required proof guard, and
  every caller passes recentProof.run. The other surfaces wrap their
  requests in recentProof.run; disabling the second factor asks before
  its confirmation opens, so the code typed there is not spent twice.
- An account with nothing to re-prove in place (single sign-on only, no
  factor) is told to sign in again when adding a factor, instead of being
  offered a password it does not have.
- step-up-web-caller-guard freezes the gated route methods and fails when
  a web request to one of them runs outside the flow.

An account whose only second factor is the identity provider's is still
asked for an app factor on the managed-profile acts: the app cannot see
the provider's factor. Enrolling right after a sign-in needs no further
proof, and the confirmation stamps the session, so the act follows.

Refs #959
2026-10-17 09:17:03 +02:00
Marc-André Bombeck 59f910df5e Merge pull request #1234 from MBombeck/fix/v1432-consent
Release v1.43.2
v1.43.2
2026-10-16 17:51:43 +02:00
Marc-André Bombeck 1a2853395b chore(release): v1.43.2 2026-10-16 17:11:58 +02:00
Marc-André Bombeck e2b7e7350e fix(consent): mint AI consent receipts only on an explicit agreement
The AI settings page posted to /api/consent/ai/web on every mount and the
server minted an ai_full receipt for it, so an account that had agreed to
nothing sent its health data to the operator's provider from background
jobs. Native builds synced a receipt the same way on every launch.

- Both grant routes now require `confirm: true`; anything else writes
  nothing and answers 400 consent.ai.confirmRequired.
- Remove the mount-time heal. A consent card names the operator's service
  and the data that would go there, with one Agree button, in Settings > AI
  and on the Insights overview while AI is closed by the missing consent
  alone.
- Publish the standing consent as `ai.consent` on /api/auth/me (status,
  since, recipients); the settings card renders active-since and the
  withdraw control from it.
- Existing receipts stay valid.
- Guard that no client grants outside a click handler, route tests for the
  confirmation, and an integration test that a fresh operator-key account
  gets no background AI text until it agrees.
2026-10-16 17:11:04 +02:00