hypercal 2.1.0 A new name, a from-source build, and a server that is markedly harder to attack. **`WEBAUTHN_ORIGIN` is now required in production.** A server started with `NODE_ENV=production` and no `WEBAUTHN_ORIGIN` refuses to boot. Set it to the public origin people reach the instance on, scheme included — e.g. `https://calendar.example.com`. It is not a convenience setting: without it a passkey assertion is checked against the caller's own `Origin` header, which is a check that can only pass. `WEBAUTHN_RP_ID` remains optional and defaults to that origin's hostname. `AUTH_DEV_AUTOLOGIN=1` together with `NODE_ENV=production` is now fatal rather than a warning — it treats every session-less request as a seeded admin, which is a complete authentication bypass. The Playwright suites run as the new `NODE_ENV=e2e`, which is production behaviour in every respect that matters and tolerates the bypass the harness needs. Nothing else needs attention. The schema migrates itself and the Android package identifiers are unchanged, so this is an ordinary in-place update. The name is shorter everywhere a person reads it — launcher, PWA manifest, browser tab, F-Droid listing — with a new icon and a refreshed set of screenshots. `dev.hypercalendar.app` is untouched, and so is the CalDAV realm string: clients cache credentials against (host, realm), so renaming it would make every configured device re-prompt for its app password in exchange for a name nobody ever sees. The SQLite engine the on-device backend runs on is now compiled from source during the build — emscripten, in CI — instead of arriving as the prebuilt `.wasm` inside `@sqlite.org/sqlite-wasm`. F-Droid builds every APK from source on their own infrastructure and their scanner rejects a prebuilt binary outright, so this is what makes a from-source build possible at all. Doing it in our pipeline too, rather than only inside the F-Droid recipe, is the point: the published APK and the one F-Droid builds now run the same engine, and the standalone test suite exercises it on every tag instead of leaving it a path only their reviewer discovers is broken. - **Per-username login throttling.** The existing per-IP limiter cannot help with the case that actually matters for a password: many hosts making a few attempts each against one account, which stays under every per-IP ceiling indefinitely. Failures are now counted against the account itself, wherever they come from. It is a throttle and not a lockout — each failure past a small free allowance imposes a doubling delay, so knowing a family member's username is not a way to lock them out of their own calendar. Failed CalDAV Basic auth feeds the same counter. - **Origin checking on state-changing `/api` requests.** `SameSite=Lax` cookies already stop a cross-site POST, but Lax treats sibling subdomains as same-site, so an untrusted neighbour under the same registrable domain was not covered. Requests carrying an `Origin` must now match this instance's; requests with no `Origin` are unaffected, because they did not come from a browser being tricked. `CSRF_ALLOWED_ORIGINS` adds deliberate extras. - **Passkey sign-in no longer skips 2FA when it proves only possession.** User verification is requested as "preferred", so an authenticator that performs none still returns a valid assertion. For an account that has deliberately enabled TOTP, that assertion now falls through to the same second step the password flow uses. - **A real Content-Security-Policy for the SPA shell.** The one inline script — the pre-paint theme read — is allowed by a hash computed at startup from the file the server is about to serve, rather than by `'unsafe-inline'` or by a constant that drifts the next time someone edits the shell. - **SSRF hardening** on the outbound fetches behind ICS subscriptions and remote CalDAV links, with the same guard shimmed into the on-device backend. - **Signing out wipes the device.** The offline event cache, the pending write queue, the conflict list and the service worker's cached `/api/events` responses are all cleared, so the next account on a shared device starts clean. - **A leaner, pinned container.** Base images are pinned by digest, and the native build toolchain `better-sqlite3` needs is confined to a build stage instead of shipping to production. `docker-compose.yml` is now a hardened reference deployment — read-only root filesystem, no new privileges. The security model, security policy, deployment and self-hosting pages have all been reworked around the above, including what the optional per-calendar encryption does and does not defend against.