Murasaki
ガイド

ウィンドウと権限

複数のネイティブウィンドウを宣言し、各レンダラーへ必要最小限のコマンドだけを許可する。

Murasaki のウィンドウは murasaki.config.ts で宣言します。既存の window がプライマリウィンドウで、ラベルは常に main です。追加のウィンドウは、安定したラベルをキーとして windows に記述します。

murasaki.config.ts
import { defineConfig } from 'murasaki'

export default defineConfig({
  appId: 'com.example.notes',
  productName: 'Notes',

  capabilities: ['clipboard:readText'], // main だけのfallback
  window: {
    route: '/',
    width: 1100,
    height: 760,
    capabilities: ['window:getLabel', 'window:open', 'window:list', 'window:manage'],
  },

  windows: {
    settings: {
      route: '/settings',
      title: 'Settings',
      width: 720,
      height: 560,
      // secondaryのvisibleは既定false
      capabilities: [],
    },
    preview: {
      route: '/preview',
      visible: false,
      capabilities: ['clipboard:readText'],
    },
    report: {
      route: '/report',
      createOnLaunch: false,
      capabilities: [],
    },
  },
})

宣言されたウィンドウの createOnLaunch はデフォルトで true です。起動時に作成されるセカンダリウィンドウは、visible: true を指定しない限り非表示で開始します。createOnLaunch: false にすると、信頼された Node Main が windows.create(label) を呼ぶまで、その宣言を休止状態のままにできます。セカンダリウィンドウで capabilities を省略すると、すべての権限が拒否された状態になり、トップレベルのリストは継承されません。プライマリウィンドウは常に起動時に作成され、window.capabilities ?? capabilities ?? [] の順に権限を解決します。

非表示のセカンダリウィンドウは、通常 main のような表示中のレンダラーから開いてください。WebKit や WebView2 は、非表示のままレンダラーを実行する場合もあれば、表示されるまで JavaScript の実行を遅延させる場合もあります。どちらのタイミングにも依存せず、非表示のレンダラー自身に windows.open() で自分自身を開かせる設計にはしないでください。ネイティブの可視性は OS のイベントループを通じて反映されるため、windows.list() でオープン後の状態を確認する必要がある場合は、最初の読み取りがコンポジターと同期していると仮定せず、visibletrue になるまで待ってください。

Windows バックエンドの console オプションは、アプリケーションバックエンドコンソール全体を制御するため、プライマリウィンドウ専用です。セカンダリウィンドウの宣言に console を書くと、設定解決時に拒否されます。

ラベルは 1〜64 文字で、先頭は英数字、残りは英数字・._- だけを利用できます。main は予約済みです。route/ で始まる同一オリジンのパスだけを受け付け、完全な URL、プロトコル相対 URL、バックスラッシュは設定解決時に拒否されます。

ウィンドウを識別・操作する

レンダラー自身のウィンドウには appWindow を、ラベルで指定する宣言済みのウィンドウには windows を使います。

'use client'

import { appWindow, windows } from 'murasaki/native'

const current = await appWindow.getLabel()
await windows.open('settings')
await windows.focus('settings')

const all = await windows.list()
const settings = all.find((item) => item.label === 'settings')
console.log({ current, settings })

await windows.hide('settings')

windows.list()WindowInfo[] を返します。

interface WindowInfo {
  label: string
  primary: boolean
  visible: boolean
  focused: boolean
  minimized: boolean
  maximized: boolean
}
APIケイパビリティ動作
appWindow.getLabel()window:getLabel現在のレンダラーのラベルを返す。
windows.open(label)window:open生存中の宣言済みウィンドウを表示・復元してフォーカスする。
windows.list()window:list生存中の宣言済みウィンドウすべての状態を返す。
windows.show(label) / hide(label) / focus(label) / close(label)window:manage別の宣言済みウィンドウを操作する。

設定にないラベルは利用できません。任意のルートや実行時のウィンドウを動的に生成する API ではありません。windows.open(label) は表示のみを行う API であり、休止状態または破棄済みの宣言に対しては拒否されます。先に Node Main のウィンドウマネージャーでその宣言を作成してください。

OS の close コントロールと appWindow.close() はセカンダリウィンドウを非表示にするため、windows.open(label) で同じ宣言済みウィンドウを再表示できます。一方、windows.close(label) はセカンダリウィンドウのターゲットを明示的に破棄します。破棄したセカンダリウィンドウは、信頼された Node Main からだけ再生成できます。main のクローズは、引き続きアプリの終了リクエストとして扱われます。Node Mainも参照してください。

フレームレスウィンドウとカスタムタイトルバー

decorations: false を指定すると、すべてのプラットフォームで OS のウィンドウ枠を取り除けます。macOS の titleBarStyle: 'hidden' は、信号機ボタンを残したままタイトルテキストだけを隠し、WebView をその下まで広げます。

murasaki.config.ts
export default defineConfig({
  appId: 'com.example.notes',
  productName: 'Notes',
  window: {
    decorations: false, // 全platformでframeless
    // titleBarStyle: 'hidden', // macOS専用。Windows/Linuxではconfig warning
    //                          // 付きで無視されます
  },
})

ネイティブのタイトルバーがないと、OS 側にウィンドウをドラッグするための領域がなくなります。useWindowDrag() で、任意の領域をドラッグ可能にしてください。

'use client'

import { appWindow } from 'murasaki/native'
import { useWindowDrag } from 'murasaki'

function Titlebar() {
  const drag = useWindowDrag()
  return (
    <header {...drag} style={{ WebkitUserSelect: 'none' }}>
      <span>My App</span>
      <button data-murasaki-no-drag onClick={() => appWindow.close()}>
        ×
      </button>
    </header>
  )
}

useWindowDrag() は、プライマリボタンの pointerdown でネイティブドラッグを開始します。インタラクティブなターゲット(buttoninputaselecttextarea、または data-murasaki-no-drag が付いた要素)はスキップするため、ドラッグ可能な領域の内側にあるタイトルバーのコントロールも問題なく動作します。ネイティブレンダラーの外や、OS がドラッグを拒否した場合(例: マウスボタンが押されていない場合)は何もしません。

フルスクリーン、最大サイズ、モニター

APIケイパビリティ動作
appWindow.startDragging()window:manageOS のウィンドウドラッグを開始する。上記の useWindowDrag() を参照。
appWindow.setFullscreen(bool) / isFullscreen()window:manageウィンドウの現在のモニターでボーダーレスフルスクリーンを開始・終了する。専用ビデオモードによる排他的フルスクリーンには未対応。
appWindow.setMaxSize({ width?, height? })window:manage最大の内側サイズを設定する。両方省略、または null を指定するとクリアされる。片方だけの指定は拒否され、両方指定するか、どちらも指定しない必要がある。
appWindow.getMonitors()window:manageウィンドウから見えるすべての OS ディスプレイを物理ピクセルで返す。

これらは呼び出したレンダラー自身のウィンドウを操作するため、window:manage は許可の有無だけで判定します。上記の windows.show/hide/focus/close(label) と異なり、スコープ付きの許可が適用対象とする別ウィンドウの label 引数はありません。

window.fullscreenwindows.<label> 配下も同様)は、起動時の初期状態を設定します。window.maxWidth / maxHeight は初期の最大サイズを設定し、両方指定した場合は minWidth / minHeight 以上である必要があります。

murasaki.config.ts
export default defineConfig({
  appId: 'com.example.notes',
  productName: 'Notes',
  window: { maxWidth: 1600, maxHeight: 1200 },
})

getMonitors(){ monitors: WindowMonitorInfo[] } を resolve します。

interface WindowMonitorInfo {
  name: string | null
  isPrimary: boolean
  isCurrent: boolean
  x: number
  y: number
  width: number
  height: number
  scaleFactor: number
}

プライマリウィンドウのクローズとアプリのライフサイクル

セカンダリウィンドウで OS のクローズコントロールまたは appWindow.close() を使うと、そのウィンドウだけが非表示になります。main のクローズはアプリの終了リクエストであり、beforeQuit() が実行され、その中でキャンセルできます。その後、時間上限付きの shutdown() を経て、ネイティブホストが終了します。ウィンドウ別のクローズフックは、今のところ用意されていません。

セッション分離

WebContext / プロファイルはウィンドウのラベルごとに分離されるため、起動時に作成されたラベルと動的に作成されたラベルの間で、cookie、Web Storage、Service Worker、SharedWorker が共有されることはありません。同じラベルをプロセス内で再作成した場合は、コンテキストが再利用されます。Windows では、永続的なラベルごとのプロファイルを、アプリ専用の WebView2 データディレクトリ以下に保存します。webview.incognito: true では、すべてのラベルが非永続的なコンテキストを使用します。

ウィンドウ別の最小権限

権限は呼び出しを行うレンダラーごとに評価されます。React の状態だけを編集する設定用のウィンドウには capabilities: [] を、クリップボードを読み取るプレビュー用のウィンドウには clipboard:readText だけを付与できます。ウィンドウ管理権限は、通常、信頼するプライマリレンダラーだけに置きます。

murasaki.config.ts
export default defineConfig({
  appId: 'com.example.notes',
  productName: 'Notes',
  window: {
    route: '/',
    capabilities: [
      'window:list',
      { permission: 'window:open', allow: { windows: ['settings'] } },
      { permission: 'window:manage', allow: { windows: ['settings'] } },
    ],
    backendCapabilities: ['api:GET:/api/account'],
  },
  windows: {
    settings: { route: '/settings', capabilities: [], backendCapabilities: ['api:GET:/api/settings'] },
    importer: { route: '/import', capabilities: ['dialog:openFile'], backendCapabilities: [] },
  },
})

ウィンドウ別のケイパビリティ一覧は、侵害されたレンダラーが呼び出せるネイティブコマンド名を制限します。構造化された window:open / window:manage の許可では、完全一致するターゲットラベルも限定でき、denyallow より優先されます。ダイアログのデフォルト値など、他のコマンド引数はまだスコープできません。権限を持つレンダラーにリモートコンテンツや実行可能なユーザー作成コンテンツを読み込ませず、広範な操作についてはアプリケーションレベルの意図も検証してください。

ネイティブの許可リストとバックエンドの許可リストは、それぞれ別の境界を保護します。各ネイティブウィンドウはラベルに紐づく HMAC アイデンティティを受け取り、backendCapabilities が Server Actions、'use main'、API Routes、アップデーターのルート、イベント、診断を制限します。セカンダリウィンドウは、デフォルトでバックエンドの許可を持ちません。ルートは同じ HTTP オリジンを使いますが、ブラウザプロファイルはウィンドウごとに分離されるため、あるウィンドウの Service Worker、SharedWorker、cookie、ストレージが、別のウィンドウのバックエンド権限を引き継ぐことはありません。プライマリウィンドウは、従来のアプリケーションプロファイルを維持します。セカンダリウィンドウのプロファイルは Windows / Linux と macOS 14+ で永続化され、カスタムの永続的な WebKit ストアを使えない macOS 11–13 では、ウィンドウ別の非永続的なストアへ安全側に倒れます。ただし、これは完全なレンダラーサンドボックスではなく、XSS は自身のウィンドウに許可されたリソースをすべて利用できます。ハンドラー内でもユーザー / セッション / オブジェクト単位の認証・認可を行い、信頼できない実行可能なコンテンツをアプリケーションウィンドウへ読み込まないでください。複数のウィンドウで共有する永続的な状態は、ブラウザストレージではなく Main / API ハンドラーを経由させてください。

次へ

GitHub でこのページを改善

On this page