The cookie approach seems like the more secure aproach because it ties the authentication or registration attempt to the user's browser session, but it doesn't work reliably on Chrome for Windows. Also, a simila problem pops up on Chrome for Linux if the session is used instead of a separate cookie. It looks like the browser doesn't propagate the state change through fast enough which results in some requests contaiining the new/updated cookie, and others don't, which results in sporadic failures. Since we use a signed and expiring challange we still get protection from replay attacks and tampering which enables us to omit the cookie entierly and rely on the challange's signature to prove expiration and authenticity. The only thing we lose is the ability to tie and attemp to a single browser session.
See: https://github.com/w3c/webauthn/wiki/Explainer:-WebAuthn-challengeURL which proposes to add the same challange fetching logic as part of the standard
See: https://github.com/w3c/webauthn/issues/1856 which discusses issues that arise from having expiring challanges (which the spec recommends)
See: https://github.com/OneUptime/oneuptime/security/advisories/GHSA-gjjc-pcwp-c74m which is an explot that can happen if the server isn't able to verify the authenticity of challanges that are sent outside of a cookie
This simplifies the loading and cleanup logic, while providing ubiquitous support across JS frameworks. Buttons can now be added in any way imaginable and still work without requiring additional initialization. The upside of this aproach is that it doesn't require a mutation observer nor a global click listener, and is supported by all browsers that also support passkeys.
This is a guess based on a few articles I read where Windows Hello doesn't play nice with Same-Site: Strict. I have no credible evidence to back this up.
No longer needed since the scrubber is not memoized across calls and the
text node length guard prevents the performance issue.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
A large comment body was causing Regexp::TimeoutError in production.
The URI regexp isn't catastrophically backtracking — it's linear — but
with a long enough string it exceeds the 1s Regexp.timeout set by
Rails.
Skip scanning text nodes over 10KB, which is well beyond any
reasonable content for auto-linking and still keeps us many orders of
magnitued under the timeout on an unloaded machine.
Fixes FIZZY-Q4
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix indentation
- Split awkward initializer into separate methods
- Rename credentials to passkeys
- Simplify authenticator registry loading
- Flatten nested errors under ActionPack::WebAuthn
- Set passkey current params only requests that use it
- Inline anemic methods
- Replace custom validation with ActiveModel::Validation
- Rename identity to holder in Passkey
- Rename credentials to passkeys in JS
- Extract framework library out of controllers
- Pass params hashes down to ActionPack
- Attempt to simplify public interface
- Push data decoding down to the classes representing the data
- Introduce has_passkeys
- Add CBOR bigint support
- Rename ActionPack::WebAuthn::Passkey to ActionPack::Passkey
- Add create_passkey_button helper
- Rename public-key to creation-options
- Add sign_in_with_passkey_button helper
- Dispatch events for the whole Passkey lifecycle
- Add ED25519 support
- Prevent crash on missing meta tag
- Validate resident key options
- Don't clobber existing ActionPack config options
- Validate cryptographic params
- Move CurrentWebAuthnRequest into ActionPack::Passkey
- Use ActiveModel::Attributes for Options objects
- Implement expiring challanges
- Add lifecycle events
- Extract param helpers into Request
- Add passkey_creation_options and passkey_request_options helpers
- Add create_passkey_challenge to make it easier to override the create method if needed
- Prefix all view helpers with passkey_
- Auto-include ActionPack::Passkey::Holder
- Make the passkey challange url configurable
- Add a reminder about Passkeys to the magic link email
- Use plain client data json everywhere
- Expose AAGUID and backed_up on Credentials
- Make attestation verifiers configurable
- Auto-suggest names for passkeys and show icons
Separate the attachment existence check from the broadcast suppression
override so each file has a single responsibility. The guard now lives
in ActiveStorageAnalyzeJobSkipDetached with its own documentation
explaining the upload-then-delete race condition.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Upload-then-delete races cause AnalyzeJob to hit S3 after PurgeJob has
already removed the object, producing Aws::S3::Errors::NoSuchKey noise
in solid_queue_failed_executions. Since PurgeOnLastAttachment destroys
the attachment row before enqueueing PurgeJob, checking
blob.attachments.exists? catches this — a fast DB query that avoids the
S3 round-trip entirely.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Old service workers cached in browsers still read `data.path` from the
push notification payload. Include it alongside `data.url` so notification
clicks keep working until those service workers update.
Currently we only register the service worker in the notification
settings screen, so this won't happen very often. Once we introduce
offline mode, we'll register the service worker in more places, so we
should be able to remove this.
Instead, let's use a Loofah scrubber which will create DOM nodes
directly. This should be faster and is a tiny bit simpler, as well as
removing a potential HTML injection vector.
Also, add "noreferrer" to all `mailto:` links (already present on URLs).
replacing normalize_base36. This should make it easier to convert
UUIDs between hex and base36 formats, useful when debugging production
logs that contain hex UUIDs.
For example, if in a log you see:
User Load (0.3ms) SELECT `users`.* FROM `users` WHERE
`users`.`account_id` = x'019afab815897a4f920f3a24fab75400' AND
`users`.`id` = x'019afab815a2790688c58c7f8326e700' LIMIT 1
and you want to find that account or that user in the console, you can
now do:
id = ActiveRecord::Type::Uuid.hex_to_base36("019afab815897a4f920f3a24fab75400")
=> "03f6bilrvt3oghzhurll4pp8g"
An append-only storage ledger replaces deadlock-prone synchronous
counter updates and drift-prone async updates.
All content storage (card images, card/comment/board description embeds)
is tracked. Account exports (which expire) and avatars are not tracked.
Storage is accounted for by Account and Board. Both consume the same
event stream independently: no bubble-up storage bumps triggering
deadlocks due to lock sequencing. Each calculates its own total from
the underlying ledger with independent cursors and materialization.
* Storage::Entry: Append-only ledger recording attach/detach/transfer
events with delta bytes. Single event stream indexed for both
Account and Board cursor queries.
* Storage::Total: Polymorphic snapshot cache with cursor (last_entry_id)
tracking which entries have been materialized.
* Storage::Totaled concern: Provides bytes_used (fast snapshot) and
bytes_used_exact (snapshot + pending) query modes, plus
materialize_storage! to roll up pending entries.
* Storage::Tracked concern: For models owning attachments (Card,
Comment, Board). Provides board_for_storage_tracking for models
where board is determined differently (Board returns self).
Handles board transfers by recording transfer_out/transfer_in entries.
* Storage::AttachmentTracking: Hooks ActiveStorage::Attachment lifecycle
to record attach/detach entries. Handles ActionText::RichText embeds
by traversing to the actual model. Snapshots context in before_destroy
to handle cascading deletes where parent record may be gone by
after_destroy_commit.
* MaterializeJob: Rolls up pending entries into snapshot. Concurrency
limited per owner to prevent duplicate work.
* ReconcileJob: On-demand reconciliation against actual attachment
storage for support/debugging. Compares ledger total to real bytes
from card images, card embeds, comment embeds, and board embeds.
Usage:
* account.bytes_used / board.bytes_used: fast, slightly stale bytesize
* account.bytes_used_exact / board.bytes_used_exact: real-time bytesize
* Storage::Entry: audit trail for debugging and point-in-time queries
Add SSRF protection for web push endpoints:
- Resolve endpoint IP once and pin it for connection
- Validate endpoints resolve to public IPs
- Whitelist permitted push service hosts
Add missing IP ranges to SsrfProtection:
- 100.64.0.0/10 (Carrier-grade NAT, RFC6598)
- 198.18.0.0/15 (Benchmark testing, RFC2544)
Note: link-local (169.254.0.0/16) is already covered by ip.link_local?
* main: (116 commits)
Ensure avatar thumbnails are square
Update useragent to recognize twitterbot/facebot
Add defensive styles for non-square avatar images
Update test for copy changes
Missed commit
AI: standardize on https://agents.md
Make it clear this is just notifications, not comprehensive activity
AI: configure MCP servers for Chrome, Grafana, and Sentry (#1727)
Allow requests from Google Image Proxy
Update to basecamp's useragent fork
Clean up a little bit the CSRF reporting code
Claude: production observability guidance (#1725)
Prevent autoscroll to the root columns container to prevent jump on page load
Include full name string so you can type your name to filter
Prioritize current user and assigned users in assignment dropdown
Check and report on Sec-Fetch-Site header for forgery protection
bundle update
Bump bootsnap from 1.18.6 to 1.19.0
Bump rails from `077c3ad` to `17f6e00`
Fix cards getting stuck in edit mode
...
It doesn't actually work, and even if we could make it work reliably
we are better off if the records always know to go to the right shard.
It does make the interface a bit more complicated as we need to select
the right shard class with `for(account_id)`.
Instead we'll compute the table name dynamically based on
Current.account where needed. Also we'll prevent searchable records
from being saved if Current.account is not set, otherwise the after
commit callbacks will fail.