Commit Graph

6660 Commits

Author SHA1 Message Date
Stanko K.R. da69039476 Implement Passkey authentication 2026-03-18 11:49:23 +01:00
Andy Smith 70c19e4881 Style polish 2026-03-18 11:49:23 +01:00
Stanko K.R. 6c58fd9bdd Implement Passkey registration
- Create Identity credentials
- Add management UI and registration flow
2026-03-18 11:48:52 +01:00
Adrien Maston 768e7a3a6e Merge pull request #2689 from basecamp/mobile/tweak-card-perma-footer
Mobile / Tweak card perma footer
2026-03-17 12:05:47 +01:00
Jorge Manrubia 964915bc66 Remove payment/subscription system and card/storage limits
Fizzy is now free. Remove the entire Stripe billing system,
subscription management, and card/storage limit enforcement.

Removes from saas/: Plan model, Account::Billing, Account::Subscription,
Account::Limited, Account::OverriddenLimits, Account::BillingWaiver,
all subscription/billing controllers and views, Stripe webhook handler,
card creation/publishing limit enforcement, admin account override UI,
usage report rake task, and all related tests.

Removes from main app: Fizzy.saas? guards for subscription panel,
SaaS card footer override, near-limit notices, and saas.css stylesheet.

Adds migration to drop billing tables from the SaaS database.

Non-billing SaaS features (push notifications, signup, authorization,
telemetry, console1984/audits1984) are preserved.
2026-03-17 09:22:04 +01:00
Stanko Krtalić bead275d27 Merge pull request #2710 from basecamp/conditionally_disable_peer_verification_for_zip_streaming
Conditionally disable peer verification for ZIP streaming
2026-03-16 18:16:31 +01:00
Stanko K.R. ddc6ce020e Conditionally disable peer verification for ZIP streaming 2026-03-16 17:57:26 +01:00
Stanko K.R. 8f6fca94fa Prevent HTML injection through filenames 2026-03-16 17:50:31 +01:00
Mike Dalessio 3959d91148 Revert "Configure Lexxy to add extra spacing between block elements"
This reverts commit d6bf103efd.
2026-03-12 12:24:35 -04:00
Mike Dalessio d6bf103efd Configure Lexxy to add extra spacing between block elements
Updating Lexxy to v0.8.0 for the insert-markdown event.
2026-03-11 22:17:07 -04:00
Adrien Maston 31975be1fa Update app/models/user/named.rb
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-11 13:36:09 +01:00
Adrien Maston 7a6c905a32 Prevent line breaks in the middle of familiar_name 2026-03-11 13:20:40 +01:00
Adrien Maston 99e5af6f95 Tweak the layout
... left aligning
2026-03-11 13:19:48 +01:00
Jeffrey Hardy 801da17cfa Merge pull request #2687 from basecamp/fix-card-filing-keyboard-shortcuts
Fix that the "maybe" stream wouldn't be replaced after triaging a card
2026-03-10 16:21:10 -04:00
Mike Dalessio 1e40457418 Merge pull request #2686 from basecamp/flavorjones/fix-path-traversal
Fix path traversal in ActiveStorage blob key import
2026-03-10 15:40:48 -04:00
Stanko K.R. b4fd22644c Set the list of importable models based on the record sets in the manifest 2026-03-10 15:33:24 -04:00
Mike Dalessio 8fb57ace01 Validate polymorphic types against importable models allowlist
Add IMPORTABLE_MODEL_NAMES to RecordSet and verify polymorphic type
columns during import check against this allowlist before calling
constantize. This prevents arbitrary class instantiation from
untrusted import ZIP data.
2026-03-10 15:33:24 -04:00
Jeffrey Hardy 0f5216f433 Fix that the "maybe" stream wouldn't be replaced after triaging a card
The element was originally named "the-stream" and later renamed to "maybe",
but the original references weren't updated.

Refs:
- https://github.com/basecamp/fizzy/pull/2211
- https://github.com/basecamp/fizzy/commit/b3cfae35
2026-03-10 14:50:32 -04:00
Rosa Gutierrez 66039c92f3 Use x_user_agent cookie for platform detection in Hotwire Native
Turbo's offline/service worker sets an x_user_agent cookie with the
original browser user agent. This ensures platform detection works
correctly for Hotwire Native apps where service worker requests may
not carry the overridden user agent header.
2026-03-10 15:34:05 +00:00
Rob Zolkos 8c2318e267 Add JSON response format to entropy endpoints (#2673)
* Add JSON response format to entropy endpoints

Add account settings JSON endpoint returning account info including auto_postpone_period. Add auto_postpone_period to board JSON responses. Add JSON update support to both account and board entropy controllers.

* Add auto_postpone_period to all board response examples in API docs

* Use auto_postpone_period_in_days in JSON responses and API docs

* Use valid period values in permission tests
2026-03-10 11:31:00 -04:00
Mike Dalessio 71b99c1e18 Fix path traversal vulnerability in ActiveStorage blob key import
BlobRecordSet#import_batch preserved blob keys from exported ZIP files
verbatim. A crafted ZIP with a traversal key like "../../config/deploy.yml"
would create a blob whose key resolves to an arbitrary filesystem path
when served by ActiveStorage's DiskService, allowing an attacker to read
local files.

Fix: generate fresh blob keys on import, discarding whatever key was in
the ZIP. This is safe because nothing else in the import pipeline
references blobs by key — attachments use blob_id, ActionText uses GIDs
based on record IDs, and only FileRecordSet used the old key for file
data lookup.

Update FileRecordSet to build an old_key→blob_id mapping from the blob
JSON metadata in the ZIP, then look up blobs by ID instead of by key.
Both check_record and import_batch are now fail-closed: they raise
IntegrityError for unmapped storage files, missing blobs, and duplicate
keys in the export.

Backfill test coverage for BlobRecordSet and FileRecordSet import, and
add a round-trip test verifying blob data survives export/import with
regenerated keys.
2026-03-10 11:03:53 -04:00
Rosa Gutierrez 49a86c7cbf Enable CORS fetch mode for storage requests
The load balancer now returns a specific `Access-Control-Allow-Origin`
instead of a wildcard, so credentials can be included by default.
This enables `maxEntrySize` enforcement for storage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 10:20:54 +01:00
Rosa Gutierrez 1855490f80 Revert CORS fetch mode for Active Storage
CORS mode doesn't work with redirect-based Active Storage URLs
when the storage bucket uses wildcard Access-Control-Allow-Origin.
Relying on maxEntries alone until specific origin CORS is available.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 10:20:54 +01:00
Rosa Gutierrez 67d629f722 Use CORS fetch mode for Active Storage to enable maxEntrySize checks
Otherwise, for resources like images loaded via <img> tags, the browser
sets `mode: "no-cors"` (as these aren't CORS requests), so the service
worker gets an opaque response even though the server sends CORS
headers.

We could upgrade all no-cors requests to cors mode, sending a `mode:
"cors"` request to a server that doesn't send CORS headers will fail
entirely: the `fetch` call throws a `TypeError` (network error), and the
browser blocks the response. So the resource wouldn't load at all, not
even as opaque. We wouldn't be able to cache it at all.

By opting in via `fetchOptions`, we can do it only for rules where we
know the server will send CORS headers.
2026-03-10 10:20:54 +01:00
Rosa Gutierrez 99382db720 Only start offline mode when signed in
Gate Turbo.offline.start() behind Current.user so the service worker
is only registered for authenticated users. This avoids errors on
unauthenticated pages where /service-worker.js requires auth.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 10:20:54 +01:00
Rosa Gutierrez 0482670fec Exclude service worker and edit/pin/watch/new pages from main cache
We don't need the service worker to cache itself, or pages that won't
work offline anyway.
2026-03-10 10:20:54 +01:00
Rosa Gutierrez de6d70cc2e Bump turbo-rails for Turbo.offline.clearCache()
Simplify the clear-offline-cache controller to use the new
Turbo.offline.clearCache() API instead of messaging the service
worker directly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 10:20:54 +01:00
Rosa Gutierrez 1c2de6ca79 Clear offline cache on sign-in as well as sign-out
Rename logout controller to clear-offline-cache and attach it to the
magic link verification form so the service worker cache is cleared
when a different user signs in.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 10:20:54 +01:00
Rosa Gutierrez 089fcf94f8 Preload resources after service worker installation
When the service worker is registered for the first time, resources loaded
before it becomes active won't go through the service worker. These resources
may be served from the browser's HTTP cache on subsequent requests, bypassing
the service worker cache entirely.

The new `preload` option accepts a regex pattern. On first visit (when no
controller exists), it waits for the service worker to take control, then
sends a message with URLs from `performance.getEntriesByType("resource")`
that match the pattern. The service worker fetches and caches these resources.
2026-03-10 10:20:54 +01:00
Rosa Gutierrez b93808235c Limit cached entry sizes
1MB and no limit for number of entries except for attachments, where we
limit individual entries to 2MB and total of entries to 500. The number
is based on the following percentiles for Active Storage blobs:

```
p50: 97.1044921875 KB
p75: 236.9140625 KB
p90: 917.7548828125 KB
```
2026-03-10 10:20:54 +01:00
Rosa Gutierrez cf8d92afea Enable offline caching for all web users
Previously, offline caching was conditionally enabled only for Hotwire
Native apps. This removes that restriction and enables offline support
for all users, including PWAs and regular browsers.

This Uses the new `fetchOptions` support in `TurboOffline` handlers
to pass `cache: "no-cache"` for document fetches, which we need
to work around a quite annoying Safari PWA bug
(see https://github.com/basecamp/fizzy/pull/1014).

Also, simplify a bit the cache names and remove the `misc` one.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-10 10:20:54 +01:00
Rosa Gutierrez a949f1e3ae Clear offline cache when logging out
Adds a logout Stimulus controller that sends a message to the service
worker to clear all cached content when the user logs out. This ensures
that cached data from one user isn't accessible after logout.

The implementation:
- Adds logout_controller.js that posts { action: "clearCache" } to the
  service worker via postMessage
- Updates logout buttons to use the controller on form submission

Also fixes data-turbo placement: moved from button to form element where
it actually takes effect for disabling Turbo Drive form submissions.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-10 10:20:54 +01:00
Rosa Gutierrez 975968b50f Only enable offline caching for Hotwire Native apps
This change conditionally renders the TurboOffline caching rules based on
whether the request comes from a Hotwire Native app. Web browsers get a
minimal service worker that only handles push notifications and a simple
document fetch fallback.

Why separate behavior for native vs web?
-----------------------------------------
We want offline caching for Hotwire Native apps (where users expect
app-like offline behavior) but not for regular web browsers.

Why use ERB conditional rendering?
----------------------------------
We explored several approaches to detect Hotwire Native requests in the
service worker:

1. User-Agent detection in fetch handler: Would be ideal, but Android
   WebViews override the User-Agent header with the system default when
   requests are intercepted by service workers, stripping the custom
   "Hotwire Native" identifier.

2. Custom header (X-Hotwire-Native) from native apps: Would require
   intercepting ALL requests at the native level using both
   WebViewClient.shouldInterceptRequest() and ServiceWorkerClient.
   Complex to implement and still incomplete coverage for all request
   types (navigation, resources loaded by HTML).

3. In-memory flag with IndexedDB persistence: Service workers can be
   terminated when idle and restart with fresh state. Reading from
   IndexedDB is async, but the decision to call respondWith() in a
   fetch handler must be synchronous.

4. Separate service worker URLs: Same theoretical churn problem as ERB
   rendering, with more complexity.

Why ERB conditional rendering works in practice
-----------------------------------------------
The main concern with conditional ERB rendering was "churn" — the service
worker constantly updating as different client types fetch it. However,
this only happens when web and native share storage on the same device.

In practice, this is rare because:
- Android native apps use isolated WebView storage
- iOS doesn't support service workers in WebViews (yet)
- Web browsers have completely separate storage

So each context gets its own stable service worker without churn.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-10 10:20:54 +01:00
Rosa Gutierrez 9fc724572e Use new except option for offline rules 2026-03-10 10:20:54 +01:00
Rosa Gutierrez ada4688c4e Take a first stab at offline mode support 2026-03-10 10:20:54 +01:00
Jeremy Daer d33aa8b757 Wrap params coverage nits (#2681)
* Add missing assert_response to signup API test

* Add JSON response format to joins#create
2026-03-09 21:53:43 -07:00
Jeremy Daer a52b6f1c87 Add explicit wrap_parameters to all controllers (#2680)
* Add explicit wrap_parameters to controllers for flat JSON API support

Virtual attributes (has_rich_text, has_one_attached, delegated setters,
ActiveModel attrs) are not in Model.attribute_names, so wrap_parameters
auto-detection silently drops them from flat JSON requests. Add explicit
include: lists matching each controller's permitted params.

* Convert short-form wrap_parameters to explicit include: lists

Defense-in-depth: these controllers only have real-column params today,
so auto-detection works, but explicit lists prevent future regressions
if virtual attributes are added.

* Add flat JSON param tests for all wrap_parameters controllers

17 new tests covering every controller with wrap_parameters, verifying
that flat (unwrapped) JSON payloads are correctly wrapped and processed.
Focuses on virtual attributes that would be silently dropped without
explicit include: lists.
2026-03-09 21:47:05 -07:00
Jeremy Daer 71a1ee7031 Wrap comment params for flat JSON payloads (#2679)
* Wrap comment params for flat JSON payloads

The SDK sends flat JSON ({"body": "..."}) but Rails wrap_parameters
auto-detection misses :body since it's a virtual ActionText attribute,
not in Comment.attribute_names. Explicitly declare wrap_parameters so
params.expect(comment: [...]) works with both wrapped and flat payloads.

* Test comment create and update with flat JSON params
2026-03-09 17:02:08 -07:00
Rob Zolkos 23ac76555b Validate and normalize auto-postpone period to days (#2672)
* Validate and normalize auto-postpone period to days

- Add Entropy::AUTO_POSTPONE_PERIODS and validate auto_postpone_period against the allowed set
- Introduce auto_postpone_period_in_days for forms and entropy update endpoints
- Fall back to index 0 in knob partial when current value isn't in options
- Remove entropy_auto_close_options helper in favor of model constant
- Update tests to use allowed period values

* Use auto_postpone_period_in_days for JSON entropy updates

The JSON API was accepting auto_postpone_period (seconds) which
bypassed the days normalization and validation. Switch to
auto_postpone_period_in_days consistently and add explicit
wrap_parameters so flat JSON params are wrapped correctly for
the virtual attribute.

* Default to account or 30-day fallback when knob value is invalid

* Address PR review feedback for entropy validation

- Fall back to first knob option (index 0) when persisted value isn't in
  the allowed set, preventing nil index errors for legacy values
- Use integer division (1.day.to_i) for consistent day calculations

* Default to account entropy period instead of first option for knob fallback

* Test that default auto-postpone period is in the allowed periods

* Return 422 instead of 500 for invalid entropy auto-postpone values

Rescue ActiveRecord::RecordInvalid in entropy controllers so invalid
auto_postpone_period_in_days values return 422 Unprocessable Entity
instead of raising a 500.

* Fix NoMethodError when board entropy falls back to account default

Use container.account.entropy.auto_postpone_period_in_days instead of
container.account.auto_postpone_period_in_days since Account doesn't
delegate that method.

* Test board entropy fallback to account default for invalid periods
2026-03-09 17:37:56 -04:00
Rob Zolkos 0435db30ba Add JSON response format to webhooks (#2671)
* Add JSON response format to webhooks

Support JSON responses for all webhook endpoints: index, show, create,
update, destroy, and activation. Adds jbuilder views, updates controllers
to use respond_to, and documents the API in docs/API.md.

* Address PR review feedback

- Use distinct board ID in API docs webhook example
- Use render "webhooks/show" in activations controller for consistency

* Address PR review feedback from flavorjones

- Include webhook.board in cache key to avoid stale board data
- Use _url instead of _path for location header consistency

* Fix webhook create test to match Location header format

Use board_webhook_url instead of board_webhook_path since the controller
returns a full URL in the Location header.

* Fix board ID in Location header example in API docs

Use distinct board ID in the Location URL to match the JSON body example.
2026-03-09 17:33:30 -04:00
Jeremy Daer 723c8180b4 Add JSON response format for full SDK/CLI coverage (#2675)
* Add JSON response format to void-response controller actions

Add respond_to blocks with JSON format to 14 controllers that
previously only rendered HTML or Turbo Stream responses. JSON
callers get head :no_content (or :created/:ok for push
subscriptions, distinguishing new vs existing).

Covers: board involvements, board/account entropies, column
reordering, card readings, card publishing, user roles, user
avatars, account settings, notification settings, join codes
(with 422 error branch), and push subscriptions.

* Add steps index endpoint with JSON view

Add index action to Cards::StepsController so SDK/CLI callers
can list all steps for a card. Reuses the existing _step.json
partial.

* Add JSON views for paginated card list endpoints

Add show.json.jbuilder for stream, not-now, and closed column
endpoints. No controller changes needed — set_page_and_extract_
portion_from and fresh_when already work format-agnostically.

* Add JSON search endpoint with distinct-card pagination

JSON search paginates distinct Card records (via the existing
Card.mentioning scope with .distinct) rather than Search::Record
objects. This ensures page boundaries, Link headers, and counts
reflect unique cards — no short pages from post-pagination dedup.

ID-match searches return a uniform single-element Card[] array
through the same pagination path.

* Add JSON views for settings and configuration endpoints

Add show.json.jbuilder for account settings (name), join codes
(code, usage_count, usage_limit, url, active), and notification
settings (bundle_email_frequency). Tests for show and update
were added in the void-response commit.

* Add JSON response format to data exports

Create returns 201 with export id, status, and created_at so
callers can poll. Show returns any-status exports for JSON (not
just completed) with a download_url when ready, or 404 for
missing/other-user exports.

* Extend access token JSON API

Add index.json and _access_token.json partial for listing tokens.
Add JSON format to destroy. Include id and created_at in the
create response alongside the existing token/description/permission
fields.

* Fix ambiguous precedence warning in export JSON view

* Assert X-Total-Count pagination header in streams JSON test

* Address review feedback

- Guard steps index against HTML requests (redirect to card)
- Normalize created_at to UTC in access token and export JSON
- Add deterministic ordering (.latest) to search JSON pagination

* Return 406 for HTML requests to steps index instead of redirect

* Add wrap_parameters for flat SDK JSON payload compatibility

Five controllers infer the wrong wrapper key from their class
name, so flat JSON bodies like {"role":"admin"} fail with 400.
Add explicit wrap_parameters declarations:

- Users::RolesController → :user
- Notifications::SettingsController → :user_settings
- Account::JoinCodesController → :account_join_code
- Account::SettingsController → :account
- Boards::EntropiesController → :board

Add integration tests that send flat (unwrapped) JSON payloads
for all seven param-bearing endpoints to prove SDK compatibility.

* Address PR review feedback

- Use 201 Created (not 204) for all create actions: publishes, readings,
  left/right positions, push subscriptions
- Simplify push subscription to always return :created (no 200/201 variance)
- Remove superfluous respond_to block from steps#index
- Merge exports#show into single respond_to block
- Move export download_url conditional out of inline args
- Ensure join code active is explicitly boolean
- Remove extra parens from search controller assignment
- Add blank lines between format blocks for readability

* Return JSON bodies on create actions

Create actions for boards, cards, columns, comments, and steps now
render the resource as JSON (via their show views) instead of returning
empty 201 responses with only a Location header. SDKs expect a JSON
body they can deserialize into the created resource.

* Return JSON bodies on reaction create actions

Card and comment reaction creates now render the reaction as JSON
instead of returning empty 201 responses, consistent with the other
resource-creating endpoints.

* Allow access tokens API without account scope

Access tokens are identity-level (not account-scoped), so the
controller needs to work with bearer token auth and no account slug
in the URL. Skip require_account so the bearer token auth path
can proceed.

Also fix involvement test to send params in JSON body (not query
string) to match real SDK usage.
2026-03-09 11:39:49 -07:00
Rob Zolkos a896b0a9e6 Touch board when publication is created or destroyed (#2674)
Publishing or unpublishing a board didn't bust the `json.cache! board`
fragment cache because `Board::Publication` didn't touch its parent
board. The board's `updated_at` stayed stale, so subsequent GET requests
continued serving cached JSON without `public_url`.
2026-03-09 13:59:57 -04:00
Rob Zolkos 854c0b85bc Update magic link sign-up email copy to be surface-agnostic (#2670)
Remove "on the Fizzy sign-up page" from the verification code email since users can now sign up via CLI, not just the web.
2026-03-06 16:47:15 -05:00
Rob Zolkos e47f2d93eb Add JSON response format to board publication toggle (#2663) 2026-03-05 20:48:03 -05:00
Mike Dalessio e27c0c5fa1 Filter draft cards from public closed column (#2667)
The public closed column was showing draft cards because it queried
`@board.cards.closed` without a `.published` filter. This adds the
missing `.published` scope to match the pattern used elsewhere in the
public controllers.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 20:31:41 -05:00
Rob Zolkos 8ee2d7d65f Add JSON response format to access token creation (#2662)
* Add JSON response format to access token creation

Allows programmatic clients (e.g., CLI) to create access tokens
via the API by returning the token, description, and permission
as JSON instead of redirecting to the HTML show page.

* Add JSON response format to access token creation

Allows programmatic clients (e.g., CLI) to create access tokens
via the API by returning the token, description, and permission
as JSON instead of redirecting to the HTML show page.

* Style

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-03-05 13:48:51 -04:00
Adrien Maston 734549a4cf Merge pull request #2657 from basecamp/mobile/fix-clipped-column-menu
Mobile / Fix clipped column menu
2026-03-05 17:39:21 +01:00
Adrien Maston deb31ddc40 Merge pull request #2655 from basecamp/mobile/fix-search
Mobile / Fix search
2026-03-04 17:15:05 +01:00
Adrien Maston 62d4bab5a8 Merge pull request #2660 from basecamp/mobile/tweak-activity-headers
Mobile / Tweak activity headers
2026-03-04 17:03:30 +01:00
Adrien Maston 98d019d47b Make it a fitted pill shape on iOS 2026-03-04 15:36:07 +01:00