Skip to content

[pull] main from tldraw:main#474

Merged
pull[bot] merged 13 commits intocode:mainfrom
tldraw:main
Apr 1, 2026
Merged

[pull] main from tldraw:main#474
pull[bot] merged 13 commits intocode:mainfrom
tldraw:main

Conversation

@pull
Copy link
Copy Markdown

@pull pull Bot commented Apr 1, 2026

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 : )

MitjaBezensek and others added 12 commits April 1, 2026 09:13
…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&amp;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    |
@pull pull Bot locked and limited conversation to collaborators Apr 1, 2026
@pull pull Bot added the ⤵️ pull label Apr 1, 2026
@pull pull Bot had a problem deploying to bemo-canary April 1, 2026 15:13 Failure
… 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)
@pull pull Bot merged commit 742cbd5 into code:main Apr 1, 2026
@pull pull Bot had a problem deploying to deploy-production April 1, 2026 21:13 Failure
@pull pull Bot had a problem deploying to deploy-staging April 1, 2026 21:13 Failure
@pull pull Bot had a problem deploying to vsce publish April 1, 2026 21:13 Failure
@pull pull Bot had a problem deploying to bemo-canary April 1, 2026 21:13 Failure
@pull pull Bot had a problem deploying to deploy-staging April 1, 2026 21:13 Error
@pull pull Bot had a problem deploying to deploy-staging April 2, 2026 00:32 Error
@pull pull Bot temporarily deployed to e2e-dotcom April 2, 2026 02:36 Inactive
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants