Repository navigation
·
106 commits
to develop
since this release
v1.1.0
A hardening release. Session revocation for SuperAdmins, real per-client rate limiting, a Content-Security-Policy and security headers, a stricter password floor, a validated peer-chat gateway, and a lint rule that turns the "no raw HTML" convention into a build failure. It also brings the frontend onto the React Compiler, moves every form onto react-hook-form, and switches production to tag-based releases.
97 commits since v1.0.0. Full diff: v1.0.0...v1.1.0.
Read this first
- Database. Two migrations were applied to production on 2026-09-19, before this tag:
20260913021830_add_followup_counters_drop_simulated(addssignals.followUpSentandsignals.followUpAnswered, drops thesimulated_follow_upstable) and20260919115622_add_super_admin_is_active(addssuper_admins.isActive, defaulttrue). The first one belongs to v1.0.0 and had never been applied. Confirm withprisma migrate statusbefore rolling back. Migrations are forward-only, so a rollback does not undo them. - New release process. Production is now deployed from a
vMAJOR.MINOR.PATCHtag byrelease.yml: the API first (behind theproductionenvironment approval), then theproductionbranch that Vercel builds the web app from. Merging intomaindeploys nothing. Seedocs/releasing.md. - Passwords. New and reset passwords need at least 10 characters (was 8). Existing passwords keep working until their next reset.
- Sessions. No forced logout. Tokens still live in
sessionStorage; the move toHttpOnlycookies is planned separately (priorities #30). - Peer chat. The client event is now
request_peer. The oldrequest-peeris still accepted, so installed PWAs and Android APKs keep working.
Security
- SuperAdmin sessions can be revoked (#66).
super_admins.isActiveis new,AdminAuthGuardre-reads the row on every request, and a deleted or deactivated admin's token stops working immediately instead of living out its 8 hours. Login rejects a deactivated admin with the same non-disclosing error. - Rate limiting keys on the real client (#70). Behind Fly's proxy,
req.ipwas the proxy's address, so the "per-IP" limit was one bucket shared by every user. Limits now key onFly-Client-IP. The three login routes add 20 attempts per 15 minutes per address (bounds scrypt CPU per source) and 5 per 15 minutes per address and e-mail (bounds guesses per account without locking out colleagues behind one hospital NAT). - Content-Security-Policy and security headers (#72). The site sends a CSP (no
unsafe-eval, no wildcard host,connect-srclimited to Zelo's own API origins, the one inline theme script allowed by hash),X-Frame-Options,nosniff,Referrer-Policyand aPermissions-Policythat keeps the camera for the QR scanner. The API now runshelmetand no longer sendsX-Powered-By. The Docker nginx sends the same site headers. Checked in headless Chrome against the production build and on the dev deployment. - No raw HTML, enforced (#72). ESLint now fails the build on
dangerouslySetInnerHTML,innerHTML/outerHTMLassignment,insertAdjacentHTML,document.write,eval,new Function, string timers andjavascript:URLs. - Peer-chat gateway validates input (#69). Every event payload is schema-checked, a peer partner's own socket cannot issue a request, one socket holds one open request, and open requests are capped per address.
- Password floor of 10 characters (#71), from one shared schema used by both set-password endpoints, the web form and the
admin:createscript, which used to hash whatever it was given. - Seed guard (#55). The seed refuses to run when
DATABASE_URLandDIRECT_DATABASE_URLdisagree about local versus remote.
Features
- A
SECTOR_MANAGERcan be created without a sector and stays in pending registration (#38). - Every remaining form now uses react-hook-form with zod (#39, #40).
- The frontend is compiled with the React Compiler (#65), with
eslint-plugin-react-hooksenforced (#62). - The 18 staff routes load lazily; the entry chunk is 21.7% smaller (#54).
- The QueryClient has a 30 second default
staleTime(#59).
Build, test and tooling
prisma generateis part of Turborepo's build and test graph, so a stale client can no longer pass locally (#58).- The dependency-cruiser rule that keeps Prisma out of
application/now actually fires (#56). - Mock isolation is restored between tests (#57).
- Capacitor live-reload and LAN/emulator access for development (#37).
- Production releases run from
release.yml; a manual run with a tag redeploys that version (#73).
Documentation
- Every environment variable is documented in the three
.env.examplefiles (#67). - The July build-plan documents and 47 executed plans were removed, the READMEs and the pt-BR architecture reference were corrected, and the dead
VITE_BASE_PATHplumbing was deleted (#68). - Conventions, AI skills and the design and plan documents for the newer work (#42, #43, #53, #60 to #64).
docs/releasing.mddescribes the release process, including the manual production migration.
Known limitations
- Session tokens are still readable by script in
sessionStorage. The CSP and the lint rule lower the likelihood and the blast radius; theHttpOnlycookie migration is the real fix (priorities #30). - The Android APK loads its assets locally and does not receive the site's headers.
- Rate-limit counters are in memory per process, so a second Fly machine would count separately.
- The dev API is still served from
zelo-api-dev.fly.dev;api-dev.zelohealth.appdoes not exist yet.