Autumn Cleaning

It's been two weeks since the last update, and this one is a bit different. After Journal landed, I had an AI-assisted review go through the whole app, hunting for ways things could go wrong. It found quite a few. 😬

Most of this post is about that, because you deserve to know what was wrong, what is fixed, and what changes for you.

Security fixes

I checked the logs and found no signs of any of this being abused. Still, I'm sorry these were there in the first place. The silver lining is that they're fixed now, and I know a lot more about where the sharp edges are.

Some of it was about things being visible to the wrong people. Hidden shot times, for example, could leak through related shots and pagination links. Then there was the account side: resetting your password now also revokes OAuth access, passkeys, and push subscriptions, so nobody who got in earlier keeps a back door. Passkey sign-in now asks for your PIN or biometric and is rate limited. OAuth secrets are properly hashed, Airtable tokens are encrypted, and Airtable sync and custom fields now check for Premium on the server, not just in the UI.

The scariest category was keeping the site up. A single crafted upload could have kept workers busy long enough to take the whole site down. Parsing is bounded now, files over 10 MB are rejected, and web push endpoints and webhook events are validated too.

Many other small things were fixed along the way, and you can find them all in the full changelog.

What might break for you

Tightening things up means a few API behaviors changed, so if you build on it, please check these:

  1. Access tokens in query or body params no longer work. Send them in the Authorization: Bearer header.
  2. Tokens without the read scope see public data only. Shot reads treat them as anonymous, with no error.
  3. Unconfirmed and disabled accounts get a 403 from the API.
  4. Premium and authorization failures are always a 403 JSON error.
  5. After a password reset, integrations must be authorized again.
  6. Old OAuth app secrets can't be recovered. They're only stored hashed now, so if you never saved one, you'll need a new app.

Sign-ups

I noticed a lot of bots and fake accounts when I sent the newsletter and got a bunch of bounced emails and spam reports. Not a great look. 😅

So new accounts now need to confirm their email, sign-ups from domains without MX records are rejected, and sign-up attempts are rate limited. All existing users were marked as confirmed. Accounts left unconfirmed for 7 days are deleted, or disabled if they have shots.

Uploads, API, and behind the scenes

Invalid files now get a clear message (a 422 in the API) instead of a 500, and dropping lots of files uploads them in batches. Smart Espresso Profiler CSVs parse faster, and Decent .tcl files about 9x faster with a new hand-written parser. I also fixed a .tcl export bug that produced files the Decent app couldn't load.

On the API side, archiving coffee bags was requested, so integrations can now do exactly that. Roaster and coffee bag images can be uploaded too, and the documented upload scope, which was actually being refused as invalid, works now. The OpenAPI spec got a full rewrite while I was at it.

Images are now decoded and resized in a locked-down sidecar container called Hot Cell, introduced by 37signals, instead of inside the Rails app, so a malicious image file stays contained there.

Grind suggestions

Last time I said I was experimenting with Jev for grind suggestions. I removed it, since it was only enabled for me and wasn't as useful as I hoped it would be. I even tried replacing it with Cloudflare's Clef, but it was even worse. 🤦‍♂️

If something looks off after all these changes, especially in the API, please let me know through GitHub issues or email.

Thanks for reading, and enjoy your weekend! ☕