Cookie attributes, create-race fixes, and authored HTML at a real origin (for FlutterFlow desktop) - #35
Merged
Conversation
The FlutterFlow Test Mode worker sets its session_jwt cookie SameSite=None;
Secure so it rides the preview app's cross-site subresource requests (assets,
the DWDS websocket). setCookie could only carry name/value/domain/path, so a
mirrored cookie was stored SameSite-unspecified (treated Lax): the top-level
/preview/ load authenticated but every subresource went out cookie-less and
the preview rendered blank.
- Dart: setCookie({secure, httpOnly, sameSite}) + CefCookieSameSite enum;
CefCookie gains sameSite, parsed from the host JSON (absent -> unspecified).
- Wire: kOpSetCookie grows three NUL-separated fields
(secure(0|1), httpOnly(0|1), sameSite token). Old hosts read only the first
five fields; new hosts pad missing ones — skew-tolerant both ways.
- Hosts (macOS main.mm + Windows cef_host_win.cc): map the token onto
CefCookie.same_site, set secure/httponly, and force Secure on for
SameSite=None (Chromium drops None-without-Secure at SetCookie time).
CookieToJson reports sameSite back for getCookies.
- example/samesite_probe.dart: auto-running jar round-trip probe (macOS run:
10/10 PASS against a freshly built host).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… at a real origin
Two things the FlutterFlow desktop app needs (Test Mode preview, Monaco editors).
1. Ops that race the create are no longer dropped.
create() returns before cef_host has a slot for the browser: the slot is
registered by a later TID_UI task, and on a shared host the create FRAME is
itself paced behind earlier establishments while every other op is sent
immediately. The reader thread's `if (!slot) break` silently dropped whatever
arrived in that window:
- cookie verbs (macOS + Windows): now take the wire id, not a slot — the jar is
process-global, the id only routes the reply. A dropped setCookie meant an
unauthenticated first load; a dropped getCookies never replied, so the
caller's future hung forever (this wedged FlutterFlow's preview on a blank
page).
- evalReturning: resolved by wire id on TID_UI and ALWAYS replies
({ok:false,"no browser"} instead of silence).
- dispose: resolved on TID_UI, so a dispose racing its own create no longer
leaks the browser.
- navigate / loadTrusted that beat a paced create frame are parked
(g_early_nav, ids are monotonic) and become that create's URL.
2. loadHtmlString(html, baseUrl:) / CefWebView(html:, htmlBaseUrl:) honour an
http(s) baseUrl (macOS). New op kOpSetAuthoredHtml (0x3f, {url}\0{html}): the
host answers the main-frame request for exactly that URL with the HTML, so the
document has the URL's real origin — relative URLs, fetches, workers and
origin-keyed storage work, which a data: URL's opaque origin cannot give.
Stored by wire id on the reader thread and written in ONE write ahead of the
create frame, so it is in place before the first request; kept on the Swift
session so a re-home re-sends it. Sticky across reload; a plain navigate or
another load ends it. Elsewhere (Windows for now) it falls back to data: with
an injected <base href>.
example: authored_origin_probe (11/11 on macOS); deployment target 12.0 (Xcode 27
refuses 10.15).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…(macOS) Two bugs that made a code editor (Monaco) unusable in a CefWebView: 1. A modifier pressed on its own went out as keycode 0. Modifier keys were in neither key table, so both windows_key_code and native_key_code fell back to 0 — and macOS keycode 0 IS the `A` key. With the ⌘ flag set, the page saw a bare Command press as ⌘A (keydown code=KeyA, metaKey) and selected everything; reaching for ⌘C therefore replaced the selection before copying. A bare ⌃ arrived as ⌃A. Modifiers now carry their kVK_* / VK_* codes. 2. ⌘C/X/V/A/Z never reached the page. CefWebView swallowed them and ran the browser's edit command instead, which bypasses an editor that owns its undo stack and selection: ⌘Z did nothing in Monaco, ⌘A selected the wrong thing. A real browser gives the PAGE the keydown first and only falls back to undo:/selectAll:/copy: if it was left unhandled. Do the same: on macOS the raw combo is forwarded, and cef_host implements CefKeyboardHandler::OnKeyEvent (called exactly when the renderer did not consume the key) to run the edit command. A ⌘ combo is also never treated as text, so it stays off the IME and the app's Edit menu. Windows keeps its explicit Ctrl+ mapping for now. example/lib/keyboard_shortcut_probe.dart drives the real host: bare ⌘ = Meta and selects nothing (with the old encoding shown as a control: the page sees `M-…/KeyA`), unhandled ⌘A/⌘Z/⌘⇧Z fall back to the browser, and a page that preventDefaults them gets them without the browser command also running. 9/9. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
In a windowed Chrome, AppKit's key-binding manager turns ⌘←, ⌘⌫, ⌃K, ⌃A … into edit selectors that Chromium attaches to the keydown; Blink runs them as the key's default action. An off-screen browser has no NSView in the responder chain, so every binding AppKit owns was dead in plain inputs, textareas and contenteditable (and so in Flutter web text fields). Only the ⌥ bindings worked, because Blink carries those itself. cef_host now reads the same source AppKit does — the system StandardKeyBinding.dict, then the user's DefaultKeyBinding.dict — and sends a matching keydown through DevTools Input.dispatchKeyEvent, whose `commands` field is that edit-command channel. No shortcut list to maintain, and the page still sees an ordinary keydown first, so an editor that owns the key wins. Unmodified and shift-only keys stay on SendKeyEvent. DevTools message ids now come from one per-browser counter, so the target-id probe and key dispatch can't renumber each other. keyboard_shortcut_probe: 23/23 (⌘←/→/↑/↓, ⇧⌘→, ⌘⌫, ⌃A, ⌃K, page-first ⌘←). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The gs://flutterflow-downloads bucket is no longer writable from a developer account, and the plugin repo is public anyway, so the prebuilt now lives on the repo itself: one GitHub Release per content hash (tag cef-host-<hash>), targeted at the publishing commit for provenance, assets = the tarball and its .sha256. fetch_cef_host.sh reads from releases/download; publish uses `gh` (needs push on the repo). Same idempotency and signature checks as before. FLUTTER_CEF_GCS_BASE becomes FLUTTER_CEF_PREBUILT_BASE. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.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 join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
What FlutterFlow's desktop app needed to render its Test Mode preview and Monaco editors through flutter_cef.
1.
setCookiecarriessecure/httpOnly/sameSite(macOS + Windows)FlutterFlow's Test Mode worker authenticates the preview with a
session_jwtcookie that must be storedSameSite=None; Secure. Without the attributes CEF sent the top-level load authenticated but every cross-site subresource cookie-less, and the preview rendered blank.CefCookie.sameSiteis reported back bygetCookies.SameSite=NoneforcesSecure(Chromium drops it otherwise).2. Ops that race the create are no longer dropped
create()returns beforecef_hosthas a slot for the browser: the slot is registered by a laterTID_UItask, and on a shared host the create frame is itself paced behind earlier establishments while every other op is sent immediately. The reader thread'sif (!slot) breaksilently dropped whatever arrived in that window.setCookiemeant an unauthenticated first load; a droppedgetCookiesnever replied, so the caller's future hung forever. This is what wedged FlutterFlow's preview onabout:blank.evalReturningresolves by wire id onTID_UIand always replies ({ok:false,"no browser"}instead of silence).disposeresolves onTID_UI, so a dispose racing its own create no longer leaks the browser.navigate/loadTrustedthat beat a paced create frame are parked and become that create's URL.3.
loadHtmlString(html, baseUrl:)/CefWebView(html:, htmlBaseUrl:)honour an http(s) base URL (macOS)New op
kOpSetAuthoredHtml(0x3f,{url}\0{html}): the host answers the main-frame request for exactly that URL with the HTML, so the document has the URL's real origin — relative URLs, fetches, workers and origin-keyed storage work, which adata:URL's opaque origin cannot give. (FlutterFlow's custom-code Monaco bundle loads its workers from the app origin.)Stored by wire id on the reader thread and written in one write ahead of the create frame, so it is in place before the first request; kept on the Swift session so a re-home re-sends it. Sticky across
reload(); a plainnavigate()or another load ends it. Elsewhere (Windows for now) it falls back todata:with an injected<base href>— Windows parity is a follow-up.Testing
flutter test: 181 pass (new: authored-document channel contract,<base href>fallback, cookie attributes).example/lib/samesite_probe.dart: PASS.example/lib/authored_origin_probe.dart: 11/11 on macOS — early cookie verbs answer first try with no retry; origin / href / relative URLs / per-origin storage for both create-with-html andloadHtmlString; reload keeps serving; no-baseUrlstays opaque;navigate()ends it. The hosts used don't resolve, which proves the bytes came from the host, not the network.Also: the example's macOS deployment target moves to 12.0 (Xcode 27 refuses 10.15).
macOS text key bindings (caa7a26)
A windowless browser has no NSView in the responder chain, so AppKit's key-binding manager never runs and every binding it owns — ⌘←/→/↑/↓, ⌘⌫, ⌃A/⌃E/⌃K, … — did nothing in plain inputs, textareas and contenteditable (so also in Flutter web text fields).
cef_hostnow reads the same source AppKit does (the systemStandardKeyBinding.dict, then the user'sDefaultKeyBinding.dict) and delivers a matching keydown through DevToolsInput.dispatchKeyEventwith itscommands— the edit-command channel a windowed Chrome uses. The page still sees a normal keydown first, so an editor that owns the key wins. No shortcut list to maintain.keyboard_shortcut_probeis 23/23.Prebuilt cef_host on GitHub Releases (0848932)
The GCS bucket is no longer developer-writable, so the prebuilt moves onto this (public) repo: one release per content hash, tag
cef-host-<hash>, targeted at the publishing commit, assets = tarball +.sha256.fetch_cef_host.shreadsreleases/download/…anonymously;make publish-cef-hostnow needsghwith push instead ofgsutil. Idempotency and the Developer-ID signature gates are unchanged.FLUTTER_CEF_GCS_BASE→FLUTTER_CEF_PREBUILT_BASE.🤖 Generated with Claude Code