Commit Graph

378 Commits

Author SHA1 Message Date
GPT on behalf of DHH 008ea1ab2a Update verified benchmarks for final upstream Rails revision 2026-10-08 00:09:25 +02:00
GPT on behalf of DHH 8de3e22ab4 Clarify database task repair hook scope 2026-10-07 23:51:12 +02:00
GPT on behalf of DHH f792e16e64 Repair message counters within database task pools 2026-10-07 23:50:37 +02:00
GPT on behalf of DHH fd49099729 Merge SQLite-maintained bot pagination counts from PR #337 2026-10-07 23:28:53 +02:00
GPT on behalf of DHH d9c3207886 Merge upstream banned-user push notification fix 2026-10-07 23:19:33 +02:00
GPT on behalf of DHH cd0414ef8f Publish verified architecture-transfer benchmarks 2026-10-07 23:15:30 +02:00
Cursor Agent b4ab2df20f Repair messages_count triggers in one SQLite write transaction
ensure! now rechecks trigger presence under BEGIN IMMEDIATE, backfills
drifted rooms.messages_count, and reinstalls all three triggers before
commit so concurrent writers and concurrent boot repairs cannot observe
a partial install or keep a wrong total. install! uses the same
immediate write lock for drop/recreate. Lifecycle regressions cover
missing/partial trigger drift, concurrent ensure!, and writes racing
repair.

Co-authored-by: Thomas Klemm <github@tklemm.eu>
2026-10-07 20:58:32 +00:00
Cursor Agent 32748e7fd6 Keep fixtures :all; isolate trigger lifecycle tests
Drop the explicit fixture list and prepend module. Override load_fixtures
only long enough to ensure! + backfill after alphabetical fixture load.
Move destructive trigger DDL into its own test file so parallel CI workers
do not strip triggers from the counter examples.

Co-authored-by: Thomas Klemm <github@tklemm.eu>
2026-10-07 20:14:13 +00:00
GPT on behalf of DHH 7271eb59f4 Wait for persisted messages before leaving system job helpers 2026-10-07 21:56:09 +02:00
Cursor Agent 9fd1563c9b Simplify messages_count trigger install and harden ensure!
Require all three SQLite triggers before treating the counter as installed,
drop the schema.rb / dump-rewrite install paths in favor of rake ensure after
schema load, isolate destructive trigger tests, and cover foreign room moves
plus fixture baseline counts.

Co-authored-by: Thomas Klemm <github@tklemm.eu>
2026-10-07 19:46:57 +00:00
GPT on behalf of DHH 105f4b8fd1 Digest session credentials in instrumented fragment keys 2026-10-07 21:45:31 +02:00
GPT on behalf of DHH a95361dd15 Recheck captured snapshot before native fragment lookup 2026-10-07 21:43:26 +02:00
GPT on behalf of DHH 8d02540e31 Bound native fragment caching and collapse concurrent page renders 2026-10-07 21:38:36 +02:00
Cursor Agent 1f5858098b Keep bot page totals on SQLite-maintained rooms.messages_count
X-Total-Count on GET /rooms/:id/:bot_key/messages was COUNT(*) of the
room on every page. Serve it from rooms.messages_count updated by SQLite
triggers so Rails, bulk SQL, and foreign writers stay in step — without
ActiveRecord counter_cache callbacks those paths skip.

Fixes #309.

Co-authored-by: Thomas Klemm <github@tklemm.eu>
2026-10-07 19:36:01 +00:00
GPT on behalf of DHH b220486c16 Admit paginated HTML and hydrate CSRF tokens outside presentation keys 2026-10-07 20:58:54 +02:00
Jeremy Daer 6e312c6028 Stop sending push notifications to banned users (#338)
Banning a user deletes their sessions and closes their connections but
keeps their push subscriptions, and Room::MessagePusher chose recipients
by membership alone. A banned user's browser or phone therefore went on
receiving the room name, sender and text of new direct messages,
mentions, and messages in rooms they had set to everything.

Choose subscriptions from active users only. The subscriptions are kept,
so unbanning brings notifications back without the user having to
subscribe again (the client doesn't resubscribe while the browser still
holds a subscription).

Co-authored-by: Marcello Costagliola <176920116+namespaceMarcello@users.noreply.github.com>
v1.5.2
2026-10-07 11:25:16 -07:00
GPT on behalf of DHH ac73267b07 Reuse authorized read pages without stale presentation or CSRF masks
Transfer the C completed-response cache lesson into Rails, keeping authentication, room checks and cookies per request. A persistent read-only SQLite observer detects local and foreign commits and rejects racing admission. Whole-page misses render fresh to avoid stale nested fragments; message ETags reflect token-neutral presentation.
2026-10-07 20:02:20 +02:00
GPT on behalf of DHH ee5fe3717a Update benchmarks from the shared response-verified comparison 2026-10-07 17:53:35 +02:00
GPT on behalf of DHH 27f5461067 Render a complete auto-submit transfer form 2026-10-07 17:08:48 +02:00
GPT on behalf of DHH 8bbe129030 Preserve active ping editors during sidebar refreshes 2026-10-07 16:55:08 +02:00
GPT on behalf of DHH 765711f27d Preserve request ports in message copy links 2026-10-07 16:37:53 +02:00
GPT on behalf of DHH 3c022a1641 Preserve timestamp ordering of Rails unread notices 2026-10-07 16:19:10 +02:00
GPT on behalf of DHH 2b9685c35c Wait for sidebar frames before reloading on Cable connections 2026-10-07 16:18:39 +02:00
GPT on behalf of DHH dbc7620a76 Bound accessible search probes and reject invalid benchmark responses 2026-10-07 14:22:07 +02:00
GPT on behalf of DHH c49f53d8f8 Refresh Go measurements after final sidebar correction 2026-10-07 12:22:17 +02:00
GPT on behalf of DHH 6288430f37 Update shared benchmarks after independent performance review 2026-10-07 12:17:01 +02:00
GPT on behalf of DHH 7df98ac883 Merge current Rails main during performance review
# Conflicts:
#	app/views/messages/_message.html.erb
2026-10-07 11:34:17 +02:00
GPT on behalf of DHH 27065ef489 Keep same-second unread events and bound idle push connections 2026-10-07 11:00:47 +02:00
Donal McBreen 8a6e4290d8 Merge commit from fork
* Derive message DOM ids from the server id, not client_message_id

A message's DOM id was derived from the browser-chosen client_message_id
via a Message#to_key override, so dom_id(message) was
"message_<client_message_id>". Turbo's append de-dups by DOM id, so a room
member who posted a message reusing a victim's client_message_id displaced
the victim's message element in every connected member's live view; editing
the attacker's own message then broadcast onto the victim's presentation id.

Drop the to_key override so every message DOM id and broadcast target derives
from the record's primary key. Two distinct records can no longer share a DOM
id regardless of stored client_message_id, so the collision is impossible with
no data migration and no uniqueness constraint. to_param and the fragment
cache key already used the primary key, so message URLs and per-message cache
entries are unchanged.

The composer's optimistic pending message still uses client_message_id as its
placeholder DOM id, which no longer matches the server broadcast's PK-based id.
Reconcile instead by rendering data-client-message-id on the real message and
having the messages controller drop the matching pending placeholder on
connect. Only client-side placeholders (data-pending-message) are removed, so a
message another member posts reusing the same client_message_id can never
displace a real one through the reconciliation path either.

Also point the boost broadcast target at the PK-based dom_id(message, :boosts)
to match the rebuilt container id.

GHSA-3v99-4vxh-xg84

* Bust cached message fragments rendered with client_message_id DOM ids

The message fragment cache keys on the record and the template digest, and
removing the to_key override changes neither. Fragments cached by an earlier
release would keep their message_<client_message_id> ids, so edit, delete and
boost broadcasts, which now target primary-key ids, would miss those messages
in other members' live views until the cache entry expired.

* Locate messages by record id in the client_message_id collision tests

Assert on data-message-id rather than the new primary-key DOM ids, so the tests
describe the behavior instead of the fix and fail on the vulnerable code for the
real reason. Edit the attacker's message to new text and wait for it to arrive,
so the edit path is exercised rather than passing vacuously. Add a request test
that two messages sharing a client_message_id render as distinct elements.

---------

Co-authored-by: Jeremy Daer <jeremy@37signals.com>
2026-10-07 01:41:12 -07:00
GPT on behalf of DHH 14a2f8f550 Merge pull request #329: Delete a room's messages in a job, one transaction each
Reviewed and merged by GPT on behalf of DHH.
2026-10-07 10:33:57 +02:00
GPT on behalf of DHH 4ceb92ea04 Merge pull request #326: Reuse push connections pinned to the address the guard just approved
Reviewed and merged by GPT on behalf of DHH.
2026-10-07 10:33:57 +02:00
GPT on behalf of DHH 72648a8f41 Merge pull request #296: Fan the unread room notice out from a job
Reviewed and merged by GPT on behalf of DHH.
2026-10-07 10:33:57 +02:00
GPT on behalf of DHH b409b9998f Merge pull request #328: Bound how long link unfurling can hold a request
Reviewed and merged by GPT on behalf of DHH.
2026-10-07 10:33:57 +02:00
GPT on behalf of DHH 3005154114 Merge pull request #331: avoid preview generation for rich-text file embeds
Reviewed and merged by GPT on behalf of DHH. Retain all media regressions and invalidate presentation caches again.
2026-10-07 10:33:57 +02:00
GPT on behalf of DHH a991adb19d Merge pull request #330: bound attachment preview work
Reviewed and merged by GPT on behalf of DHH. Keep both boost-cache and preview-loading regressions.
2026-10-07 10:33:02 +02:00
GPT on behalf of DHH cbf52805bd Merge pull request #311: Post a message whose attachment can't be previewed instead of failing
Reviewed and merged by GPT on behalf of DHH.
2026-10-07 10:32:12 +02:00
GPT on behalf of DHH 0f2a1da560 Merge pull request #316: List one page of account members at a time
Reviewed and merged by GPT on behalf of DHH.
2026-10-07 10:32:12 +02:00
GPT on behalf of DHH 6ad4735b19 Merge pull request #324: Rewrite a message's search entry only when its body or attachment changes
Reviewed and merged by GPT on behalf of DHH.
2026-10-07 10:32:12 +02:00
GPT on behalf of DHH 819b3896fb Merge pull request #323: Count unread rooms for push badges once per batch
Reviewed and merged by GPT on behalf of DHH.
2026-10-07 10:32:12 +02:00
GPT on behalf of DHH b6d307537e Merge pull request #325: Render the boosts a message has preloaded instead of querying them again
Reviewed and merged by GPT on behalf of DHH.
2026-10-07 10:32:12 +02:00
GPT on behalf of DHH fdec7dfc9f Merge pull request #322: Cache boosts with their message instead of one by one
Reviewed and merged by GPT on behalf of DHH.
2026-10-07 10:32:12 +02:00
GPT on behalf of DHH 0d5998f057 Merge pull request #318: Query sidebar directs and shared rooms separately
Reviewed and merged by GPT on behalf of DHH.
2026-10-07 10:32:12 +02:00
GPT on behalf of DHH 87eafc025f Merge pull request #310: Find a direct room with one query instead of checking every one
Reviewed and merged by GPT on behalf of DHH.
2026-10-07 10:32:12 +02:00
GPT on behalf of DHH 1e0d353c60 Merge pull request #312: Find the messages a refresh replaces through an index
Reviewed and merged by GPT on behalf of DHH.
2026-10-07 10:32:12 +02:00
GPT on behalf of DHH cf63b61361 Merge pull request #334: Create the room and creation time index only where it is missing
Reviewed and merged by GPT on behalf of DHH.
2026-10-07 10:32:12 +02:00
Marcello Costagliola bf5dc0d74d Create the room and creation time index only where it is missing
The Rust port creates index_messages_on_room_id_and_created_at on boot,
under the same name and on the same columns, with CREATE INDEX IF NOT
EXISTS. On a database the port opened before this migration ran,
db:prepare stopped with "index ... already exists" and the app didn't
start. With if_not_exists the migration skips an index that is already
there and creates it everywhere else.

Databases that already ran the migration don't run it again, and the
dumped schema is the same, so db/schema.rb doesn't change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwoX7uRy5vFZSNKJhSqMSG
2026-10-06 22:00:50 +02:00
Marcello Costagliola 839ea34c29 Create the room and update time index only where it is missing
The Rust port creates index_messages_on_room_id_and_updated_at on boot,
under the same name, with CREATE INDEX IF NOT EXISTS
(basecamp/once-campfire-rust#45). On a database the port has opened,
this migration stopped db:prepare with "index ... already exists" and
the app didn't start. With if_not_exists it skips an index that is
already there and creates it everywhere else, and the dumped schema
stays the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwoX7uRy5vFZSNKJhSqMSG
2026-10-06 21:55:54 +02:00
Marcello Costagliola c48083dcfe Show a file in a message's rich text by its name, whatever its variants
Showing an image's preview when its variant had already been made tied the cached message to state
that changes without touching it: a variant made after the message was cached would never show. It also
cost a query per embedded image, since Action Text loads the embeds without their variant records.
Nothing in the app makes those variants, so the partial no longer looks at them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bj8KnxpTf9sj2Ysa8aLAVa
2026-10-06 16:02:35 +02:00
Marcello Costagliola 15f56134e7 Show a file in a message's rich text without making its preview on view
Files are posted as a message's own attachment, and the composer puts none in the rich text: it permits
only mentions and link embeds. The server still keeps any attachment whose signed id resolves, a blob
included, and Action Text's blob partial links to a representation URL that makes the preview on the
first request, without the limits the POST has, and on every request while it fails.

The app's own partial shows an image's preview only if it was already made, and otherwise the file's
name and size, as it does for a file that isn't an image. The cached presentation's version goes up,
since Action Text renders the partial by name and the template digest doesn't see it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bj8KnxpTf9sj2Ysa8aLAVa
2026-10-06 15:41:34 +02:00
Marcello Costagliola 5f198146e8 Give a video a poster only once its poster variant is made
ActiveStorage::Preview#processed? is true as soon as ffmpeg's frame is attached, but the poster is a
variant of that frame and can fail on its own: the POST rescues a Vips::Error and leaves the frame
attached. The view then emitted the poster's URL, and every view retried the resize. It now checks the
variant too, from the variant records with_attached_attachment already preloads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bj8KnxpTf9sj2Ysa8aLAVa
2026-10-06 15:34:46 +02:00