Skip to content

Cookie attributes, create-race fixes, and authored HTML at a real origin (for FlutterFlow desktop) - #35

Merged
wenkaifan0720 merged 5 commits into
mainfrom
feat/cookie-samesite
Sep 22, 2026
Merged

wenkaifan0720 merged 5 commits into
mainfrom
feat/cookie-samesite

Conversation

@wenkaifan0720

@wenkaifan0720 wenkaifan0720 commented Sep 21, 2026 •

Copy link
Copy Markdown
Collaborator

What FlutterFlow's desktop app needed to render its Test Mode preview and Monaco editors through flutter_cef.

1. setCookie carries secure / httpOnly / sameSite (macOS + Windows)

FlutterFlow's Test Mode worker authenticates the preview with a session_jwt cookie that must be stored SameSite=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.sameSite is reported back by getCookies. SameSite=None forces Secure (Chromium drops it otherwise).

2. 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) 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 is what wedged FlutterFlow's preview on about:blank.
  • evalReturning resolves by wire id on TID_UI and always replies ({ok:false,"no browser"} instead of silence).
  • dispose resolves 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 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 a data: 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 plain navigate() or another load ends it. Elsewhere (Windows for now) it falls back to data: 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 and loadHtmlString; reload keeps serving; no-baseUrl stays opaque; navigate() ends it. The hosts used don't resolve, which proves the bytes came from the host, not the network.
  • End to end in the FlutterFlow macOS app: Test Mode preview loads authenticated.
  • The Windows host changes are mechanical mirrors of the macOS ones and have not been compiled here.

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_host now reads the same source AppKit does (the system StandardKeyBinding.dict, then the user's DefaultKeyBinding.dict) and delivers a matching keydown through DevTools Input.dispatchKeyEvent with its commands — 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_probe is 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.sh reads releases/download/… anonymously; make publish-cef-host now needs gh with push instead of gsutil. Idempotency and the Developer-ID signature gates are unchanged. FLUTTER_CEF_GCS_BASE → FLUTTER_CEF_PREBUILT_BASE.

🤖 Generated with Claude Code

wenkaifan0720 and others added 5 commits August 25, 2026 14:37
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>
@wenkaifan0720
wenkaifan0720 merged commit 81b7fcd into main Sep 22, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant