Bots can only create. A lifecycle notification — an alert that fires and then
resolves, a deploy that starts and finishes, a backup that runs — therefore has
to post a second message, and the room becomes an append-only log of states
rather than a view of the current one.
Adds PATCH and DELETE inside the existing bot_key scope, routed to
Messages::ByBotsController. The body is read the way create reads it, so
updating a message is the same request shape as posting one.
No new authorization: both actions already run through ensure_can_administer,
and can_administer? grants access only to a record the user created, so a bot
key reaches that bot's own messages and no others. set_room narrows it again by
looking the room up through the bot's own memberships. A leaked bot key gains
what it could already do by posting: write to rooms that bot belongs to.
update answers head :ok rather than the redirect, which meant extracting the
update and its broadcast into update_message — calling super and then head
would double render, since the parent redirects inside the action. destroy
needs no split, because the parent renders implicitly like create does.
- Use jbuilder instead of hashes
- Use resource instead of direct HTTP verbs
Verbs only make sense if you have one or two routes, if there are
multiple that emulate what resource does then it's better to use resource.
- Paginate using link headers
- Cache responses
Message content is delivered over turbo streams, which ran on the stock
Turbo::StreamsChannel. That channel verifies the signature on the stream name and
nothing else. The name carries no expiry and no binding to a user, so one read off
the page while a member kept working after the membership was revoked.
Revocation made this worse rather than better. Membership#after_destroy_commit
disconnects the user with reconnect: true, and the client replays its subscriptions
on the new socket: RoomChannel re-checks membership and rejects, while the turbo
subscription re-verified only the signature and was accepted.
RoomMessagesChannel re-checks membership on every subscribe, deriving the room from
the verified stream name so there is no parameter to point elsewhere. Since the
subscriber names the channel it wants, the stock channel would otherwise be a way
around that check, so it now turns these stream names away and this is the only door.
and add test coverage for (un)supported file types.
The avatar and logo variants move into the models and return nil for content
types that are no longer variable, so the controllers fall back to the initials
avatar and stock logo icon instead of raising `ActiveStorage::InvariableError`.
Security fix for 3 stored/DOM XSS advisories in Trix:
GHSA-g9jg-w8vm-g96v (attachment attribute), GHSA-qmpg-8xg6-ph5q
(serialized attributes), GHSA-53p3-c7vp-4mcc (JSON deserialization
bypass in drag-and-drop).
EXPOSED: campfire's composer/editor use Trix for message bodies, and the
browser actually ran the hand-vendored vendor/javascript/trix.esm.min.js
pinned at 2.0.10 — which lacks the DOMPurify/isValidAttribute sanitizers.
The gem bump alone is a no-op for the browser; the real fix is
re-vendoring the ESM build:
- bump gem to 2.1.19 (Gemfile.lock)
- re-vendor vendor/javascript/trix.esm.min.js from Trix 2.1.19
(2.0.10 → 2.1.19; bundles DOMPurify 3.4.2, rangy 1.3.2)
- update importmap pin comment @2.0.10 → @2.1.19
89 commits analyzed. Verified green via full unit + system suites
(system tests drive the Trix composer via fill_in_rich_text_area).
https://github.com/basecamp/37signals-hq/blob/main/upgrade-analysis/campfire-20260609-action_text-trix_v2.1.15..v2.1.19.md🤖 Assisted by Claude
Adds a new endpoint that allows bots to add emoji reactions (boosts) to messages:
POST /rooms/:room_id/:bot_key/messages/:message_id/boosts
This enables bots to acknowledge messages with reactions like 👀 (eyes) when
mentioned, providing immediate feedback to users before generating a full response.
The endpoint:
- Validates the bot is a member of the room
- Validates the message exists in the room
- Broadcasts the boost to connected clients via Turbo Streams
- Returns 201 Created on success, 404 if room/message not found
Co-authored-by: openhands <openhands@all-hands.dev>
Adds a new GET endpoint at /rooms/:room_id/:bot_key/messages that allows
bots to read messages from rooms they are members of.
The endpoint returns JSON with:
- Room info (id, name)
- Messages array with body (plain/html), created_at, and creator info
- Pagination info (oldest_id, newest_id, has_more)
Supports pagination via ?before=:id and ?after=:id query parameters,
consistent with the existing pagination in the messages controller.
This enables AI bots and other automated agents to understand conversation
context when responding to messages, rather than only receiving the single
message that triggered the webhook.
Co-authored-by: openhands <openhands@all-hands.dev>
* Add GitHub Actions audit job (actionlint + zizmor) to CI
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Configure dependabot for GitHub Actions, bundler, and Docker
Batches all action updates into a single weekly PR. Adds cooldown
periods to all ecosystems.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add local GitHub Actions linting (actionlint + zizmor) to bin/setup and bin/ci
Install actionlint, shellcheck, and zizmor in bin/setup. Run both
linters as CI steps in config/ci.rb alongside existing style checks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Pin all GitHub Actions to SHA hashes
Run pinact to pin action versions to specific commit SHAs,
preventing supply chain attacks from tag mutation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix high severity zizmor findings
- Suppress unpinned-images for redis service containers (digest
pinning is nontrivial for service containers)
- Move workflow-level permissions to job-level in publish-image.yml
(build gets full set, manifest gets only what it needs)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix medium severity zizmor findings
- Add persist-credentials: false to all checkout steps
- Add permissions: {} at workflow level in ci.yml
- Add job-level permissions (contents: read) to all CI jobs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix informational template-injection findings in publish-image.yml
Move steps.meta.outputs.tags from inline ${{ }} expressions to env
vars in both the manifest creation and cosign signing steps.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update brakeman to 8.0.4
bin/brakeman uses --ensure-latest which fails if not on the newest version.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously we had to use structure.sql since schema.rb didn't have support for virtual tables that we needed for search. Since Campfire's release virtuals tables have been added to Rails, so there is no need to use structure.sql anymore
This adds the ability to ban a user by their IP address.
When an admin is viewing a user profile, a new "Ban user" button is
present. Clicking on that will:
- Create a ban on the IP addresses that were tracked for that user's
sessions
- Remove all the messages authored by that user
- Log the user out immediately
In addition, that user will no longer be shown in most user lists in the
app. They are still shown to admins, in account settings. Viewing their
profile from there will now show a "Remove ban" button which can be used
to restore their access (it doesn't restore their messages though --
those are already gone -- it just removes the blocks so they can log in
again).
* Bump Ruby to 3.4.5
* Update dependencies
* Adjust for Rails 8 and Ruby 3.5 API changes
* Mark params strings as mutable in prepapration for frozen strings in Ruby 3.5
* Update test for HTML5 sanitizer
With Rails 7.1 the HTML5 sanitizer became the default, this breakts this test because the old sanitizer used to delete unpermitted nodes, while the new one returns their content
The final string is safe, but different then it used to be in Rails 7.0
* Remove direct Turbo tesh helpers require & parallelize tests
* Fix Zeitwerk issues with rails extensions
* Update Resque setup for Redis 5+
* Remove unused views
* Remove GID v1 handler