A series of tightly related fixes hardened the E2EE implementation after the initial ship.
Cortez Gets Its Own Keypair
The AI assistant Cortez was given a persistent RSA-OAEP keypair stored server-side (CORTEZ_PRIVATE_KEY env var). When a client initializes an E2EE room, it now wraps the room key for Cortez's public key in addition to the human members. This lets Cortez actually read and respond to encrypted messages — previously it was either replying blindly to ciphertext or staying silent.
Skip AI on Decrypt Failure
When Cortez's key bundle hasn't been set up yet, decryptForCortez returns null. The handler now returns early in that case rather than passing the raw base64 ciphertext string to OpenRouter — which was causing Cortez to generate bizarre replies "analyzing" the encrypted blob. A e2ee_key_needed event prompts the client to re-wrap the key.
Exclude Encrypted Messages from History
Ciphertext was being added to Cortez's conversation history, polluting the context it used to generate subsequent replies. Encrypted messages are now filtered out of the history array before it's passed to the AI.
Fix for Username Save
PATCH /auth/me was returning { user } without a success: true field, causing the frontend to treat a successful update as an error. Fixed with a one-line addition.
E2EE History Recovery
After a page refresh, the frontend was losing the decrypted history because it was fetching room_history before the locally-stored room key had been loaded from session storage. The load order was corrected so decryption happens before the history render.
E2EE Health Endpoint
GET /api/admin/e2ee-health returns Cortez's public key JWK and whether a private key is configured — useful for diagnosing E2EE issues in production.
User Profile Retheme
The user profile panel was restyled to match the room UI's purple palette.
Why it matters
Without these fixes, E2EE rooms were either non-functional or actively confusing. This sprint got the feature to a state where it worked correctly end-to-end in production.