Ovumcy v1.0.0
Release date: 2026-05-19
Highlights
- First stable public API contract. The entire HTTP surface moves under
/api/v1/*and is now the published, stable third-party contract. Breaking changes from here go in/api/v2/*. - OIDC account-linking hardening: first-time linking of a fresh
(issuer, subject)to a pre-existing local-auth account now requires explicit password confirmation through/auth/oidc/link-confirm, closing a malicious / sloppy upstream IdP account-takeover vector. - GDPR groundwork: explicit consent control on
/register, three new privacy-page sections covering Art. 13/15-22 disclosures, operator-facingdocs/gdpr.md, and a repo-visible mirror of the security invariants indocs/SECURITY_INVARIANTS.md. - Defense-in-depth: onboarding endpoints now explicitly require
OwnerOnly, and a forward-looking role-boundary discovery matrix asserts the contract for every authenticated/api/v1/*route automatically. - Backend HTML regressions switch from exact UI copy to stable
data-*hooks (data-explainer-key,data-flash-key,data-error-key,data-stats-empty-state,data-dashboard-cycle-warnings, …), so copy edits no longer trigger backend churn while i18n keys remain pinned.
Changed (BREAKING)
The entire HTTP surface moves under /api/v1/* and ships as the stable third-party contract. The legacy /api/* (non-v1) and the page-route mutators at /settings/cycle and /onboarding/* are removed.
Full mapping (canonical REST verbs):
| Legacy | Canonical /api/v1/*
|
|---|---|
POST /api/auth/register
| POST /api/v1/users
|
POST /api/auth/login
| POST /api/v1/sessions
|
POST /api/auth/logout
| DELETE /api/v1/sessions/current
|
POST /api/auth/2fa
| POST /api/v1/sessions/2fa-challenge
|
POST /api/auth/forgot-password
| POST /api/v1/password-resets
|
POST /api/auth/reset-password
| POST /api/v1/password-resets/redeem
|
GET /api/days
| GET /api/v1/days
|
GET /api/days/{date}
| GET /api/v1/days/{date}
|
GET /api/days/{date}/exists
| HEAD /api/v1/days/{date}
|
POST /api/days/{date} (upsert)
| PUT /api/v1/days/{date}
|
DELETE /api/days/{date}
| DELETE /api/v1/days/{date}
|
POST /api/days/{date}/cycle-start
| POST /api/v1/days/{date}/cycle-start
|
DELETE /api/log/delete?date=
| DELETE /api/v1/days?date=
|
GET /api/symptoms
| GET /api/v1/symptoms
|
POST /api/symptoms
| POST /api/v1/symptoms
|
POST /api/symptoms/{id} (update)
| PATCH /api/v1/symptoms/{id}
|
POST /api/symptoms/{id}/archive & DELETE /api/symptoms/{id}
| DELETE /api/v1/symptoms/{id} (single canonical path)
|
POST /api/symptoms/{id}/restore
| POST /api/v1/symptoms/{id}/restore
|
GET /api/stats/overview
| GET /api/v1/stats/overview
|
POST /api/export/{summary,csv,json}
| GET /api/v1/exports/{summary,csv,json}?from=&to=
|
POST /api/settings/profile
| PATCH /api/v1/users/current/profile
|
POST /api/settings/interface
| PATCH /api/v1/users/current/interface
|
POST /api/settings/tracking
| PATCH /api/v1/users/current/tracking
|
POST /settings/cycle (page route)
| PATCH /api/v1/users/current/cycle
|
POST /api/settings/change-password
| PUT /api/v1/users/current/password
|
POST /api/settings/start-local-password-setup
| POST /api/v1/users/current/password/step-up
|
POST /api/settings/regenerate-recovery-code
| POST /api/v1/users/current/recovery-code
|
POST /api/settings/2fa/verify
| PUT /api/v1/users/current/2fa
|
POST /api/settings/2fa/disable
| DELETE /api/v1/users/current/2fa
|
POST /api/settings/clear-data/validate
| POST /api/v1/users/current/data-wipe/validate
|
POST /api/settings/clear-data
| POST /api/v1/users/current/data-wipe
|
DELETE /api/settings/delete-account
| DELETE /api/v1/users/current
|
POST /onboarding/step1 (page route)
| POST /api/v1/onboarding/steps/1
|
POST /onboarding/step2 (page route)
| POST /api/v1/onboarding/steps/2
|
POST /onboarding/complete (page route)
| POST /api/v1/onboarding/complete
|
Auth model for /api/v1/*: cookies + X-CSRF-Token header (the csrf_token form field continues to work for HTMX). Bearer tokens remain a future phase.
Added
GET /api/v1/users/current("whoami"): returns the minimum representation of the session subject —id,email,display_name,role, and lifecycle flags (onboarding_completed,local_auth_enabled,must_change_password). Sensitive fields (password / recovery hashes, TOTP secret) are never included.docs/openapi.yaml(OpenAPI 3.1) describing the full/api/v1/*surface plus meta routes (/healthz,/lang). The yaml is the wire-contract source of truth; HTML/HTMX response variants are intentionally not described.- GDPR consent control on the public registration form (
/register). The browser checkbox is wired to a newconsentfield on the registration payload; the backend refuses any registration whereconsentis not truthy and surfacesauth.error.consent_requiredthrough the same flash/JSON channel used by other auth errors. Localized labels and error copy are added acrossen,ru,es,fr, andde. - Three new privacy-page sections —
data-privacy-section="your-rights","retention","predictions"— render the GDPR Art. 13/15-22 disclosures alongside the existing data-protection summary, with full translations across all five UI locales. - Role-boundary coverage matrix (
TestUnsupportedRoleRejectedAcrossEveryAuthedV1Route). The test discovers every registered/api/v1/*route throughapp.GetRoutes(), filters out the public auth endpoints, and asserts each remaining route rejects an unsupported-role auth cookie with403+ a clearedovumcy_authcookie. New state-mutating endpoints inherit this coverage automatically. docs/gdpr.mdoperator-facing GDPR compliance guide. Maps each in-scope GDPR obligation (Art. 6, 9, 13, 15-22, 30, 32, 33) onto the technical control plus operator action (encryption at rest via LUKS/BitLocker, backup hygiene,SECRET_KEYseparation, DSAR fulfilment through export, audit log retention, breach notification runbook).docs/SECURITY_INVARIANTS.md— repo-visible mirror of the security-critical invariants previously documented only in agent-only context files. Contributors who only see the public repository can read the layering, role-boundary, AEAD, CSP, GDPR, and CI rules without depending on gitignored agent context.- Browser regression for the privacy page (
e2e/privacy.spec.ts) that asserts the rendered copy of every privacy section plus the authenticated breadcrumb/back-link contract. The matching backend regression is reduced to structural smoke (section presence and back-linkhrefonly) so copy edits no longer trigger backend churn. - Security Claim Test Matrix in
SECURITY.md. Every test-enforceable claim is referenced from the matrix at the bottom of the document, pointing at the specific Go test (function name + file path) that guards it. AUDIT_LOG_ENABLEDenv var gates per-action security-event logging.LogSecurityEventis a no-op by default; operators flip the flag totrueonly when investigating an incident. Startup banner reports the effective setting.- CSRF header support:
csrfmiddleware now accepts both the existingcsrf_tokenform field andX-CSRF-Tokenheader. The header is the canonical path for non-HTMX/api/v1/*JSON clients. error_detailon JSON error responses (additive). Existing error keys are preserved; clients can opt into the richer field when present.
Security
- OIDC first-time link to existing email is gated behind password confirmation. The OIDC callback path does not auto-link a fresh
(issuer, subject)to a pre-existing local account by trusting the asserted email. When the OIDC exchange resolves to an existing local user by verified email, the service layer returnsErrOIDCLinkRequiresConfirmationand the handler gates the link through/auth/oidc/link-confirm. The pending-link cookie (ovumcy_oidc_link_pending) is sealed (AEAD AAD-by-name),HttpOnly, path-scoped to/auth/oidc/link-confirm, and payload-TTL-bounded to 5 minutes.ConfirmAndLinkIdentityfails closed if the(issuer, subject)was claimed by a different user between the OIDC callback and the confirmation submission. - Onboarding endpoints now require
handler.OwnerOnly.POST /api/v1/onboarding/steps/1,POST /api/v1/onboarding/steps/2, andPOST /api/v1/onboarding/completewere previously only guarded byhandler.AuthRequired, which transitively rejects unsupported roles throughErrAuthUnsupportedRole. The explicitOwnerOnlymiddleware closes the defense-in-depth gap so any future regression ofAuthRequiredcannot expose onboarding mutations. - Register timing equalization on duplicate email.
POST /api/v1/users's duplicate-email branch previously returned in ~5-20ms while the new-email branch ran two bcrypt cost-10 operations (~150-300ms), leaking account existence through response latency on a single request. The duplicate branch now mirrors the bcrypt work viaequalizeRegistrationTiming, matching the existingequalizeAuthCredentialsTimingpattern used in login. - Dead non-revoking password / recovery update methods removed.
UserRepository.UpdatePasswordandUpdateRecoveryCodeHashnever bumpedauth_session_versionand were not called from production code; theAndRevokeSessionsvariants are the canonical paths. Keeping the silent non-revoking versions around was a foot-gun where an accidental call would rotate a credential without invalidating active sessions.
Changed
- Backend HTML regressions for shared explainer / empty-state / warning blocks now assert stable
data-*hooks instead of exact UI copy. The dashboard, calendar, and stats templates exposedata-explainer-key="<i18n-key>"(anddata-explainer-primary-key/data-explainer-secondary-keyfor the multi-line calendar variant) on their prediction-explainer containers; stats empty-state coverage moves todata-stats-empty-state+data-stats-completed-cycles; dashboard stale-cycle coverage moves todata-dashboard-cycle-warnings/data-dashboard-stale-warning/data-dashboard-phase; privacy sections exposedata-privacy-section="..."anchors. - Flash / error / subtitle regressions on auth and settings pages now assert i18n keys through stable
data-flash-key/data-flash-status/data-error-key/data-subtitle-keyattributes. The shared HTMX status wrapper (httpx.StatusErrorMarkup) accepts an optional error key and emits the same attributes so HTMX inline errors share the same contract. - Frontend npm devDependencies in
package.jsonare now exact-pinned (htmx.org,tailwindcss,@playwright/test,eslint,jsdom,otplib,globals,@eslint/js). Matches the existinggo.modexact-pinning baseline and removes the residual supply-chain surface where annpm installoutsidenpm cicould pull a minor/patch update without a corresponding lockfile bump. - Cycle prediction range and age hints aligned with variability evidence. Range/age explainers now switch on observed cycle-length variability rather than just count, so onboarding baselines and sparse irregular states render the right copy.
Fixed
days: clearing the anchor period day now also clears bare auto-filled neighbors that were seeded only because of that anchor.- Dashboard autosave reads the verb from the
hx-*attribute (PUT for the v1 day upsert) so the client and server agree on the canonical method.
Upgrade notes
- Breaking client contract. Any third-party client or wrapper that hit
/api/auth/*,/api/days/*,/api/symptoms/*,/api/stats/*,/api/export/*,/api/settings/*,/settings/cycle, or/onboarding/*must move to the canonical/api/v1/*paths above. HTMX/page routes that the browser uses internally are unchanged (/login,/register,/dashboard,/calendar,/settings,/auth/oidc/*, …). - No new schema migrations in this release. Migration
022_register_pickup_tokens.sqlshipped in v0.9.5; v1.0.0 only changes routing, behavior, and validation, so the migration history is identical between v0.9.5 and v1.0.0. - OIDC operators: if you have OIDC users whose verified email matches an existing local account, the first-time linking path now requires the user to enter their existing local password through
/auth/oidc/link-confirm. The endpoint refuses accounts withlocal_auth_enabled=false— multi-provider linking onto OIDC-only accounts is intentionally a future Settings flow. - Audit logging is off by default. If you relied on stderr
security event:lines, setAUDIT_LOG_ENABLED=truein the environment. The startup banner reports the effective setting. - Tagged images publish under
ghcr.io/ovumcy/ovumcy-web:v1.0.0. - Existing deployments can upgrade in place by pulling the new image or by setting
OVUMCY_IMAGE=ghcr.io/ovumcy/ovumcy-web:v1.0.0.
Full changelog
- Compare: v0.9.5...v1.0.0
- Changelog entry:
CHANGELOG.mdsection1.0.0.