Security Changelog.
Every hardening we ship, in chronological order. Buyers, auditors and peer engineers use this page to gauge how quickly we address findings and how the surface evolves.
Cells starting with =, +, -, @, tab or CR are prefixed with a single quote so spreadsheet formulas cannot execute on auditors’ machines.
Accounts with no passwordHash now return HTTP 409 with code:'recovery_required' and are routed through /forgot-password to prove ownership before binding a password.
POST /api/auth/recover returns an identical JSON shape whether the email exists or not; unknown emails receive a decoy code that never validates.
Both endpoints now use the shared requireSession helper so expired sessions are rejected and cleaned up, matching every other API route.
Client pages no longer store the bearer token in localStorage; auth flows through the HttpOnly junix_token cookie. Bearer remains supported server-side for integrations.
POST /api/auth/signin now requires an 8+ char password meeting the shared policy (mixed classes, no long runs, blocklist). Timing-safe scrypt compare, 30-day session TTL, expired-session cleanup, 8/min rate limit.
/api/chat, /api/compliance-chat, /api/playground, /api/deal-room, /api/briefing all funnel through per-IP token buckets (30 or 10/min) plus 20-message / 4000-char caps so no anonymous request can drain the billing key.
Sales tools now require an authenticated session, capture the caller as createdByEmail, and refuse contact.email input without an explicit consent:true flag. The /deal/[id] viewer enforces creator-or-buyer ownership via the HttpOnly cookie.
next.config.js now emits X-Frame-Options: SAMEORIGIN, CSP frame-ancestors 'self', X-Content-Type-Options: nosniff and Referrer-Policy; CORS Allow-Headers is Content-Type, Authorization instead of *.
Responsible disclosure is welcomed. Please email security@junix.id or open a private issue via the Junix Trust Center. We acknowledge within one business day and publish resolved findings on this page.