Conversation
…ploy (#8336) Zero 1.0.0 is the first stable release — symbolic version bump from 0.26.1 with no breaking changes. This PR upgrades the dependency and enables zero-cache deployment for production (previously `DEPLOY_ZERO=false`). **Version bump:** `@rocicorp/zero` 0.26.1 → 1.0.0 in all four packages (zero-cache, dotcom-shared, sync-worker, client). **Production deploy:** CI now sets `DEPLOY_ZERO=flyio-multinode` for the production branch, same as staging. The deploy script's `getZeroUrl()` returns the Fly.io URL when multinode is active, with a fallback to `production.zero.tldraw.com`. **Per-environment VM sizing:** Fly.io toml templates now use `__VM_CPUS` / `__VM_MEMORY` placeholders filled by the deploy script per environment, instead of hardcoded values. Production gets max shared-cpu (VS: 8 CPUs / 16gb, RM: 4 CPUs / 8gb), staging bumped to (VS: 2 CPUs / 4gb, RM: 1 / 2gb). **Required setup before merging to production:** - Set these secrets in the `deploy-production` GitHub environment (copy from `deploy-staging` — same R2 bucket, backups are namespaced by `production/` folder): - `ZERO_R2_ENDPOINT` - `ZERO_R2_BUCKET_NAME` - `ZERO_R2_ACCESS_KEY_ID` - `ZERO_R2_SECRET_ACCESS_KEY` - `FLY_API_TOKEN` and `ZERO_ADMIN_PASSWORD` are already set globally. - No domains or Fly apps to pre-create — auto-provisioned by the deploy script. ### Change type - [x] `improvement` ### Test plan 1. Deploy to staging, verify zero-cache starts and serves queries 2. Deploy to production, verify multinode zero-cache (RM + VS) starts with correct VM sizes ### Code changes | Section | LOC change | | -------------- | ----------- | | Apps | +8 / -6 | | Config/tooling | +46 / -17 |
This PR allows pasting any `<iframe>` embed code onto the canvas to create an embed shape. Previously, only iframe URLs that matched a known embed definition (YouTube, Google Maps, etc.) would work, and the iframe had to be the only element in the pasted content. Now any valid iframe with an HTTP(S) src creates an embed directly. <img width="1456" height="712" alt="Screenshot 2026-03-20 at 18 11 27" src="https://github.com/user-attachments/assets/df1cc534-04d7-4cd2-babf-6664c540a360" /> Examples that now work: - OpenStreetMap: `<iframe width="425" height="350" src="https://www.openstreetmap.org/export/embed.html?bbox=-0.1258492469787598%2C51.558009663698876%2C-0.09986400604248047%2C51.57165515830595&layer=mapnik" style="border: 1px solid black"></iframe>` - SoundCloud: `<iframe src="https://w.soundcloud.com/player/?visual=true&url=https%3A%2F%2Fapi.soundcloud.com%2Ftracks%2F2184622839&show_artwork=true" style="top: 0; left: 0; width: 100%; height: 100%; position: absolute; border: 0;" allowfullscreen></iframe>` - Loom: `<iframe src="https://www.loom.com/embed/e5b8c04bca094dd8a5507925ab887002?hideEmbedTopBar=true&t=20s" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></iframe>` (screenshot coming) ### Change type - [x] `feature` ### Test plan 1. Copy an iframe embed code from any service (OpenStreetMap, SoundCloud, Loom, etc.) 2. Paste onto the tldraw canvas 3. An embed shape should be created with the iframe content rendered inside 4. The embed should be resizable - [x] Unit tests ### Code changes | Section | LOC change | | ---------- | ---------- | | Core code | +55 / -39 | | Tests | +211 / -0 | ### Release notes - Add support for pasting arbitrary `<iframe>` embed codes onto the canvas. Any iframe from services like OpenStreetMap, SoundCloud, Loom, and others can now be pasted directly to create an embed shape. Made with [Cursor](https://cursor.com)
We were missing the `sandbox` attribute on Gists. ### Change type - [x] `bugfix` - [ ] `improvement` - [ ] `feature` - [ ] `api` - [ ] `other` ### Release notes - embed: add sandbox attr to gist
…ling (#8392) In order to remove a stale, unmaintained dependency and eliminate workarounds for its behavior, this PR replaces `@use-gesture/react` with custom event handlers for wheel and pinch gesture handling on the canvas. Closes #8391. The library was only used in a single file (`useGestureEvents.ts`) for wheel and pinch actions. The custom implementation: - Listens for `wheel` events directly, removing the library's 140ms synthetic wheel end event that required a timing workaround - Handles touch pinch via `pointerdown`/`pointermove`/`pointerup` with multi-touch tracking - Handles Safari trackpad pinch via `gesturestart`/`gesturechange`/`gestureend` events - Computes zoom scale and origin internally with the same `scaleBounds` and `from` clamping behavior - Preserves the existing pinch state machine (zooming/panning/not sure) exactly as-is ### Change type - [x] `improvement` ### Test plan 1. Open the examples app (`yarn dev`) 2. Test wheel scrolling on the canvas — should pan smoothly 3. Test Ctrl/Cmd+wheel — should zoom in/out 4. Test trackpad pinch-to-zoom on macOS (Safari and Chrome) 5. Test two-finger panning on a touch device 6. Test two-finger pinch-to-zoom on a touch device 7. Verify zooming respects min/max zoom bounds 8. Test wheel scrolling over a scrollable editing shape (e.g., text shape being edited) — wheel events should pass through - [x] Unit tests ### Release notes - Remove `@use-gesture/react` dependency in favor of custom gesture handling, reducing bundle size and eliminating a stale dependency. ### Code changes | Section | LOC change | | -------------- | ------------ | | Core code | +239 / -177 | | Config/tooling | +0 / -20 | --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
#8404) In order to mitigate security risks introduced by #8306 (arbitrary iframe embed support), this PR restricts the iframe sandbox permissions for unknown/arbitrary embeds that don't match a curated embed definition. Previously, all embeds (known and unknown) received the same sandbox permissions including `allow-same-origin`, `allow-forms`, and `allow-popups`. This combination is dangerous for untrusted content because: - **Same-origin sandbox escape**: `allow-scripts` + `allow-same-origin` lets an iframe on the same origin as the host page access `window.parent`, read/write cookies and localStorage, and effectively remove its own sandbox - **Phishing**: `allow-forms` lets an attacker embed a convincing login form inside the canvas - **Popup attacks**: `allow-popups` enables social engineering via opened windows Now, embeds that match a curated definition (YouTube, Figma, Google Maps, etc.) continue to receive the full default permissions plus their specific overrides. Unknown/arbitrary embeds get a restricted set with `allow-same-origin`, `allow-forms`, and `allow-popups` disabled. `allow-scripts` remains enabled since most embeds need JS to render. ### Change type - [x] `bugfix` ### Test plan 1. Paste a known embed (e.g. YouTube URL) — should render normally with full permissions 2. Paste an arbitrary `<iframe>` embed code (e.g. OpenStreetMap, SoundCloud) — should render but with restricted sandbox (no same-origin, no forms, no popups) 3. Verify the restricted embed still loads and runs JS correctly - [x] Unit tests ### API changes - Added `unknownEmbedShapePermissionOverrides` — sandbox permission overrides applied to unknown/arbitrary embeds ### Release notes - Restrict iframe sandbox permissions for unknown/arbitrary embeds to prevent same-origin escape, phishing via forms, and popup-based attacks. Curated embed definitions are unaffected. ### Code changes | Section | LOC change | | --------------- | ---------- | | Core code | +19 / -1 | | Tests | +51 / -1 | | Automated files | +3 / -0 | Made with [Cursor](https://cursor.com)
…ment TTS (#8399) In order to eliminate the fragile chunking, alignment, and splitting pipeline in the PR walkthrough audio generator, this PR switches `generate-audio.sh` to make one TTS call per segment instead of generating a single monolithic audio file and then splitting it via Gemini alignment. The previous approach generated all narration as one TTS call, uploaded the WAV to the Gemini Files API, used a separate Gemini model to detect segment boundaries, then split at those timestamps. This was complex and prone to alignment errors, truncation, and off-by-one clipping issues. The new approach generates each segment's audio independently with automatic retry and duration validation, then trims silence — no upload, alignment, or splitting needed. https://github.com/user-attachments/assets/e2933171-1c6e-429a-843c-a41d549ecf4f ### Change type - [x] `improvement` ### Test plan 1. Run `generate-audio.sh` with a multi-segment `narration.json` and verify individual `audio-XX.wav` clips are produced 2. Verify `durations.json` is written with correct entries 3. Confirm retry logic works by inspecting logs when a segment produces unexpected duration ### Release notes - Internal tooling change, no user-facing impact ### Code changes | Section | LOC change | | -------------- | ----------- | | Config/tooling | +85 / -307 |
Just an idea, would be nice to have all support in one spot 🤷♂️ This PR adds Plain thread creation when users submit feedback on tldraw.com. Feedback is posted to Plain in addition to the existing Discord webhook, with a link back to the Plain thread included in the Discord embed. ### Change type - [x] `improvement` ### Test plan 1. Submit feedback on tldraw.com 2. Verify a Plain thread is created 3. Verify Discord embed includes the Plain thread link 4. Verify feedback still posts to Discord if Plain API is unavailable
…#8405) The KV error fallback test in `featureFlags.test.ts` intentionally throws to verify graceful degradation, but the `console.error` from the catch block was showing up in CI output and looked like a real failure: ``` Failed to get feature flag zero_kill_switch: Error: KV down at Object.<anonymous> (.../featureFlags.test.ts:170:10) ``` This suppresses the expected `console.error` during the test. Example run [here](https://github.com/tldraw/tldraw/actions/runs/23846844691/job/69516189133?pr=8031#step:15:894). ### Change type - [x] `improvement` ### Test plan - [x] Unit tests ### Code changes | Section | LOC change | | ------- | ---------- | | Tests | +2 / -0 |
Increases production view syncer `ZERO_UPSTREAM_MAX_CONNS` from 5 to 8 to match the 8-CPU VM. Without this, zero-cache crashes on startup: `Insufficient upstream connections (5) for 7 syncers`. ### Change type - [x] `bugfix` ### Test plan - Deploy to production and verify view syncer starts without connection errors ### Code changes | Section | LOC change | | -------------- | ---------- | | Apps | +1 / -1 |
…eload (#8407) Production zero-cache (replication manager) was crash-looping due to `SQLITE_FULL` — the 1GB default Fly.io volume filled up when the backup replicator tried to write. This PR sets explicit volume sizes in the Fly.io deploy templates (8GB for production, 1GB for staging/preview) and narrows the zero kill switch reload to only affect users actually running proper Zero. ### Change type - [x] `bugfix` ### Test plan 1. Deploy to staging and verify zero-cache volumes are created with correct `initial_size` 2. Enable `zero_kill_switch` flag and verify only users with `zero_enabled` or localStorage override get reloaded ### Release notes - Fix zero-cache crash loop caused by undersized Fly.io volumes in production ### Code changes | Section | LOC change | | -------------- | ---------- | | Apps | +14 / -4 | | Config/tooling | +5 / -1 |
… URLs (#8412) In order to prevent leaking document paths (e.g. room IDs, query params) to third-party embed providers, this PR switches the `referrerPolicy` on embed iframes from `no-referrer-when-downgrade` to `strict-origin-when-cross-origin`. Despite its name, `no-referrer-when-downgrade` sends the **full URL** (including path and query string) to any HTTPS destination. `strict-origin-when-cross-origin` sends only the **origin** (e.g. `https://tldraw.com`) for cross-origin requests, which is all embed providers need for domain allowlisting and analytics. This is also the modern browser default. Relates to #8306, #8404 ### Change type - [x] `improvement` ### Test plan 1. Paste a known embed (YouTube, Figma, Google Maps, etc.) — should render and function normally 2. Paste an arbitrary `<iframe>` embed code — should render normally 3. Verify in DevTools Network tab that the `Referer` header sent to embed hosts contains only the origin, not the full path ### Release notes - Tighten iframe referrer policy for embeds to avoid leaking document URLs to third-party embed providers. ### Code changes | Section | LOC change | | --------- | ---------- | | Core code | +2 / -2 | Made with [Cursor](https://cursor.com)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )