Murasaki
Guides

WebView Content Features

Downloads, file drag-and-drop, init scripts, zoom, print, and cookies for the native WebView.

Beyond session/network settings (User-Agent, incognito, proxy — see Configuration), Murasaki exposes six WebView content features: downloads, file drag-and-drop, trusted init scripts, page zoom, printing, and cookies. All but init scripts are gated by their own deny-by-default webview:* capability.

Downloads

Grant webview:download and a native with_download_started_handler is installed; without it, every download is denied (unchanged default). The suggested filename is sanitized down to a portable basename (directory components, controls, leading dots, Windows-invalid characters/device names, and trailing dots/spaces are handled; UTF-8 names are capped at 240 bytes and an empty name becomes download), and the final path is confined inside the configured directory — a name collision gets (n) appended before the extension, matching Wry's own behavior.

murasaki.config.ts
export default defineConfig({
  // ...
  webview: {
    downloads: { directory: '/Users/example/Documents/MyApp Downloads' },
  },
  capabilities: ['webview:download'],
})

downloads.directory is optional and must be an absolute path; omitted, it resolves to the OS user Downloads folder. Subscribe to both lifecycle events with one typed helper:

import { subscribeDownloads } from 'murasaki'

const unsubscribe = subscribeDownloads((event) => {
  if (event.type === 'started') console.log('downloading', event.url, event.path)
  if (event.type === 'completed') console.log('done', event.success, event.path)
})

There is no reliable id linking a started event to the completed event that follows it — Wry's completed handler only reports url/path/success, not the id — so concurrent downloads of the same URL can be ambiguous to tell apart from completed alone. On macOS, completed's path is always null (an upstream WebKit API limitation).

File drag-and-drop

Grant webview:dragDrop to receive drag/drop events for files dragged onto the window. The native handler always lets the OS default proceed — it never blocks — so <input type="file"> keeps working whether or not this capability is granted.

import { useFileDrop } from 'murasaki'

function Dropzone() {
  useFileDrop(({ paths }) => importFiles(paths))
  return <div>Drop files here</div>
}

For enter/over/leave as well as drop, use subscribeFileDrops:

import { subscribeFileDrops } from 'murasaki'

const unsubscribe = subscribeFileDrops((event) => {
  if (event.type === 'over') setHighlighted(true)
  if (event.type === 'leave') setHighlighted(false)
})

over is throttled to at most 20 dispatched events per second — the OS reports drag-move far more often than a page needs to reposition a drop-target highlight.

murasaki.config.ts
export default defineConfig({
  // ...
  capabilities: ['webview:dragDrop'],
})

Trusted init scripts

webview.initScripts runs project-authored JavaScript before every page load — config-owned and trusted, so it needs no capability (unlike everything else on this page). Paths are project-root-relative; contents are read and embedded at dev/bundle time, applied in declaration order.

murasaki.config.ts
export default defineConfig({
  // ...
  webview: {
    initScripts: ['scripts/polyfills.js', 'scripts/telemetry-bootstrap.js'],
  },
})

Each file is bounded to 256 KiB and the combined total to 1 MiB, enforced when the project loads (a missing file or an oversized script fails the murasaki dev/murasaki bundle command with a clear error, not silently).

Page zoom

webview.setZoom({ factor }) sets the page zoom level (0.25 to 5.0 inclusive); webview:zoom gates the call.

import { webview } from 'murasaki/native'

await webview.setZoom(1.25)

Zoom is available on macOS 11+ and iOS 14+ only (older macOS silently fails the call, which surfaces as a rejected Promise); Android is unsupported. Separately, webview.hotkeysZoom (config, not a capability) enables OS zoom hotkeys/gestures — effective on Windows (WebView2) only; a no-op on macOS/Linux.

murasaki.config.ts
export default defineConfig({
  // ...
  webview: { hotkeysZoom: true },
  capabilities: ['webview:zoom'],
})

Print

webview.print() opens the platform's native print dialog for the current page. Requires webview:print.

import { webview } from 'murasaki/native'

await webview.print()

There is no find-in-page API — Wry has none to expose. This is out of scope until an upstream API exists.

Cookies

webview:readCookies gates webview.getCookies(); webview:writeCookies gates both webview.setCookie() and webview.deleteCookie().

import { webview } from 'murasaki/native'

const { cookies } = await webview.getCookies({ url: 'https://example.com/' })
await webview.setCookie({
  url: 'https://example.com/',
  name: 'theme',
  value: 'dark',
  secure: true,
})
await webview.deleteCookie({ url: 'https://example.com/', name: 'theme' })

getCookies() returns at most 1000 entries, each value truncated at 4 KiB. setCookie/deleteCookie validate the cookie name against the RFC 6265 token charset, bound value to 4 KiB, and require an http/https url. deleteCookie matches by name, the URL's host as domain, and the default / path — a cookie set with a non-default path is not addressable through deleteCookie.

Security: Runtime authentication no longer uses a cookie. The reserved legacy murasaki_runtime name remains invisible and immutable through this API as defense in depth: reads filter it and writes/deletes reject it, regardless of granted capabilities.

Prefer a structured URL scope for cookie access. A scoped read must pass an explicit url; an unscoped getCookies() is available only to a legacy string grant. Writes are checked against the effective cookie path, and a renderer cannot set a parent or sibling domain through domain:

murasaki.config.ts
capabilities: [
  { permission: 'webview:readCookies', allow: { urls: ['https://app.example.com/**'] } },
  { permission: 'webview:writeCookies', allow: { urls: ['https://app.example.com/account/**'] } },
]
murasaki.config.ts
export default defineConfig({
  // ...
  capabilities: ['webview:readCookies', 'webview:writeCookies'],
})

In-page permission requests

getUserMedia()/geolocation calls from page JavaScript are not intercepted by Murasaki today — see Security for the full explanation and macOS TCC interaction.

Next

Improve this page on GitHub

On this page