- Shipped
- September 17, 2026 at 3:33 AM UTC
- Author
- Kamo
- Commit
- cb7395f
GET /api/ai/providers and GET /api/ai/providers/{id} answered with each provider's DECRYPTED API key. The list is what every member's AI chat model picker loads (and kamo-js / the mobile app), so any member of an organization reorder) needed only a session, so any member could also point a provider's base URL at their own server and press Test, which sends the decrypted key there. The settings screen already demanded MANAGE_AI_SETTINGS; the server did not. - Both reads now answer hasApiKey and maskedApiKey (the last four characters; a short or undecryptable key shows none of itself). The unused maskApiKey, which masked the ciphertext, is replaced. - Create, update, delete, test and reorder answer 403 without MANAGE_AI_SETTINGS (God bypasses, as everywhere). - Update keeps the stored key unless a non-blank one is sent. The edit form was filled from the returned key, so a row seeded in plaintext (decrypts to "") had its key overwritten with an encrypted blank on any save. AiProviderKeyExposureTest drives the controller through MockMvc with the real SessionHelper: no key in either read, the key's state reported, blank keeps / typed replaces, and all five changes refused for a member without the right with nothing saved and no adapter called. Five of its six tests fail against the previous controller. The kamo-internal settings form stops expecting the key in the same change set.