github ovumcy/ovumcy-web v1.0.0

5 hours ago

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-facing docs/gdpr.md, and a repo-visible mirror of the security invariants in docs/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 new consent field on the registration payload; the backend refuses any registration where consent is not truthy and surfaces auth.error.consent_required through the same flash/JSON channel used by other auth errors. Localized labels and error copy are added across en, ru, es, fr, and de.
  • Three new privacy-page sectionsdata-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 through app.GetRoutes(), filters out the public auth endpoints, and asserts each remaining route rejects an unsupported-role auth cookie with 403 + a cleared ovumcy_auth cookie. New state-mutating endpoints inherit this coverage automatically.
  • docs/gdpr.md operator-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_KEY separation, 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-link href only) 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_ENABLED env var gates per-action security-event logging. LogSecurityEvent is a no-op by default; operators flip the flag to true only when investigating an incident. Startup banner reports the effective setting.
  • CSRF header support: csrf middleware now accepts both the existing csrf_token form field and X-CSRF-Token header. The header is the canonical path for non-HTMX /api/v1/* JSON clients.
  • error_detail on 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 returns ErrOIDCLinkRequiresConfirmation and 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. ConfirmAndLinkIdentity fails 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, and POST /api/v1/onboarding/complete were previously only guarded by handler.AuthRequired, which transitively rejects unsupported roles through ErrAuthUnsupportedRole. The explicit OwnerOnly middleware closes the defense-in-depth gap so any future regression of AuthRequired cannot 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 via equalizeRegistrationTiming, matching the existing equalizeAuthCredentialsTiming pattern used in login.
  • Dead non-revoking password / recovery update methods removed. UserRepository.UpdatePassword and UpdateRecoveryCodeHash never bumped auth_session_version and were not called from production code; the AndRevokeSessions variants 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 expose data-explainer-key="<i18n-key>" (and data-explainer-primary-key/data-explainer-secondary-key for the multi-line calendar variant) on their prediction-explainer containers; stats empty-state coverage moves to data-stats-empty-state + data-stats-completed-cycles; dashboard stale-cycle coverage moves to data-dashboard-cycle-warnings/data-dashboard-stale-warning/data-dashboard-phase; privacy sections expose data-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-key attributes. 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.json are now exact-pinned (htmx.org, tailwindcss, @playwright/test, eslint, jsdom, otplib, globals, @eslint/js). Matches the existing go.mod exact-pinning baseline and removes the residual supply-chain surface where an npm install outside npm ci could 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.sql shipped 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 with local_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, set AUDIT_LOG_ENABLED=true in 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

Don't miss a new ovumcy-web release

NewReleases is sending notifications on new releases.