Murasaki
ガイド

自動更新

GitHub Releases、またはあなた自身のサーバーからの、署名付き・自己検証型のアプリ更新 — IPC もサードパーティの更新サービスも不要です。

Murasaki アプリは、更新の確認・ダウンロード・インストールを自分自身で行えます。専用の更新サービスを立てる必要はありません — マニフェストはリリース成果物の隣に置かれる単なる JSON ファイル(デフォルトでは GitHub Releases、またはあなたが管理する任意の静的ホスティング)であり、信頼モデル全体は一度だけ生成する Ed25519 の鍵ペア1つに集約されます。

macOS、Windows、そして Linux の AppImage ビルドに対応しています。.deb でインストールした場合や、手動展開しただけの AppDir には置き換え可能な自己完結型のファイルが存在しないため、check() はマニフェストを確認する代わりに「アップデートはシステムのパッケージマネージャーが管理しています」という構造化された結果を返します — エラーにはなりません。Linux パッケージングの詳細は配布を参照してください。

クイックスタート

  1. プロジェクトルートで、一度だけ署名鍵を生成します:

    murasaki release --keygen

    これにより .murasaki/update-key.pub(コミットしてください)と .murasaki/update-key(モード 0600、自動的に gitignore されます)が書き出されます。秘密鍵は表示されません。コマンド出力の通り、stdin 経由で MURASAKI_UPDATE_KEY という GitHub シークレットへ登録してください:

    gh secret set MURASAKI_UPDATE_KEY < .murasaki/update-key
  2. アップデーターを有効化します:

    murasaki.config.ts
    export default defineConfig({
      // ...
      updater: true,
    })

    true は完全な設定です: GitHub リポジトリは package.jsonrepository フィールドから、公開鍵は .murasaki/update-key.pub から推論されます。アップデーターを有効にすると、プライマリウィンドウに内部の app:quit 権限も自動的に付与されます。これは、検証済みのインストールが正常に再起動を完了できるようにするためです。更新用の UI をセカンダリウィンドウに置く場合は、そのウィンドウへ app:quit を明示的に付与してください。

  3. ボタンを配置します:

    import { UpdateButton } from 'murasaki'
    
    export default function Settings() {
      return <UpdateButton />
    }

アプリ側はこれで完了です。署名済みリリースの公開については、下記を参照してください。

Playground

useUpdate() が返す各状態と、<UpdateButton /> が示す更新フローを、実際に操作しながら確認できます。このプレイグラウンドは更新サーバーへの接続、ファイルのダウンロード、アプリの終了、再起動のいずれも行いません。

Murasaki Updater
署名済みアップデートをダウンロードできます。
available
現在
0.55.6
最新版
0.56.0
リリースノート

起動の高速化、Windowsパッケージングの改善、アップデーターの信頼性修正。

フックの状態
useUpdate()
{
  "status": "available",
  "current": "0.55.6",
  "latest": "0.56.0",
  "notes": "起動の高速化、Windowsパッケージングの改善、アップデーターの信頼性修正。",
  "mandatory": false
}

仕組み

useUpdate() の確認/ダウンロード/検証ロジックは Node 上で動作し、コンテキストメニューやアプリメニューが使うネイティブの IPC ブリッジではなく、アプリの他の部分をすでに配信しているのと同じローカル HTTP サーバー(Server Actions や API Routes と同じ仕組み)経由でページから到達します。最後の「終了してから適用する」ステップだけがネイティブランチャーに触れます — アプリのプロセスが終了したあとも動き続ける必要があるためです。

import { useUpdate } from 'murasaki'

const { status, latest, notes, progress, check, download, install, dismiss } = useUpdate()

statusidle → checking → available → downloading → ready(または not-available / error)と遷移します:

  • check() — マニフェストを取得し、署名を検証し、バージョンを比較します。
  • download() — プラットフォームに一致するアセットをディスクにストリーミングし、その SHA-256 を検証します。
  • install() — 検証済みのペイロードをネイティブランチャーに渡し、アプリを終了します。ランチャーが更新を適用して再起動します。

useUpdate() はヘッドレスです — 描画もスタイリングの意見も持ちません。<UpdateButton />(こちらも murasaki から、@murasakijs/ui でスタイリング)は、それをラップしたすぐに使える表示レイヤーです:

  • idle / checking / not-available / error の間は何も描画しません。
  • available になると 「Update to vX」 — クリックでダウンロードを開始します。
  • downloading の間はプログレスバーを表示します。
  • ready になると 「Restart to update」 — クリックでインストールして再起動します。
  • マウント時に、自分自身で一度だけチェックを行います。

<UpdateButton />error のとき何も描画しません — 失敗を自分の UI に表示したい場合は、useUpdate() から update.error を自分で読み取ってください。同様に、マニフェストの mandatory フラグは useUpdate() の状態として渡されますが、<UpdateButton /> はそれを特別扱いしません(強制的で閉じられないフローは用意されていません)— そうしたものが必要な場合は、update.mandatory を使って自分で UI を組み立ててください。

channel は、どのマニフェスト URL が解決されるかを変更します(マニフェストのセルフホスティングを参照)。checkOnStartcheckInterval は、アップデーターエンジンのスケジューラーを制御します。デフォルトでは起動時に 1 回、その後 6 時間ごとにチェックし、重複するチェックは 1 つにまとめられます。<UpdateButton /> もマウント時にチェックを行いますが、実行中のスケジュールされたチェックがある場合は新しい処理を開始せず、それに合流します。完全に手動でチェックしたい場合は checkOnStart: falsecheckInterval: false を設定してください。

開発時には check() は動作します — フルバンドルなしでマニフェストと鍵の組を検証できます — が、download() / install()status: 'error'error: 'Updates only apply to a bundled app. Run \murasaki bundle` first.'` で即座に失敗します: 更新を適用する対象の、パッケージ化されたアプリやランチャーバイナリがまだ存在しないためです。

マニフェスト

murasaki release --manifestdist/latest.json を書き出し、常に分離署名 dist/latest.json.sig と一緒に公開されます:

latest.json
{
  "version": "1.2.0",
  "publishedAt": "2026-07-12T09:00:00.000Z",
  "generatedAt": "2026-07-12T09:00:00.000Z",
  "notes": "markdown release notes",
  "mandatory": false,
  "rollout": 25,
  "keyId": "a1b2c3d4e5f60718",
  "assets": {
    "darwin-arm64": { "url": "https://.../App-1.2.0-darwin-arm64.app.zip", "sha256": "<hex>" },
    "darwin-x64":   { "url": "https://.../App-1.2.0-darwin-x64.app.zip",   "sha256": "<hex>" },
    "win32-x64":    { "url": "https://.../App-1.2.0-setup-x64.exe",       "sha256": "<hex>" },
    "win32-arm64":  { "url": "https://.../App-1.2.0-setup-arm64.exe",     "sha256": "<hex>" }
  }
}

assets のキーは <platform>-<arch> で、実行中のアプリの process.platform / process.arch に一致します。実行中のプラットフォームに対応するキーが無い場合は、エラーではなく「あなた向けの更新はありません」を意味します — アプリは、マニフェストが本来カバーしうる範囲より少ないプラットフォーム向けに出荷されることもあります。

latest.json.sig は、latest.jsonそのままの生バイト列に対する分離 Ed25519 署名の base64 です。クライアントは、それを JSON としてパースする前に、必ずそのバイト列を検証します — 逆ではありません。そのため JSON の正規化に関する曖昧さを心配する必要はありません。

クライアント側では generatedAt が必須で、rolloutkeyId は任意です。いずれも署名されたバイト列に含まれているため、同じ署名でカバーされます:

  • generatedAt — マニフェストの固定化やリプレイ攻撃を防ぐガードです。下記のマニフェストの鮮度を参照してください。
  • rollout — 0〜100 の段階的ロールアウト率です。下記の段階的ロールアウトを参照してください。
  • keyId — 鍵のローテーションのヒントです。下記の鍵のローテーションを参照してください。

リリースの公開

murasaki release --keygen [--force]
murasaki release --manifest --base-url <url> --version <v> [--notes <md>] [--mandatory] [--rollout <0-100>]
murasaki release --sign
  • --keygen — Ed25519 の鍵ペアを生成します(クイックスタート参照)。--force を指定しない限り既存の鍵を上書きしません — 鍵をローテーションすると、古い公開鍵をまだ持っている既存のアプリからの信頼が失われます(先に両方の鍵をピン留めしておけば失われません — 鍵のローテーション参照)。
  • --manifestdist/ 配下からこのバージョンの成果物(murasaki bundle が生成する macOS の .app.zipmurasaki installer が生成する Windows の -setup-<arch>.exe。アーキテクチャのサフィックスが付く前に公開された win32-x64 の資産のために、サフィックスなしの旧 -setup.exe という名前も引き続き認識します)を探してハッシュ化し、generatedAt タイムスタンプ付きで dist/latest.json を書き出します。見つからないターゲットはエラーではなくスキップされます — 何も見つからなかった場合のみエラーになります。--rollout <0-100> は、任意の段階的ロールアウト率(%)を書き込みます(段階的ロールアウト参照)。
  • --signdist/latest.json を署名して dist/latest.json.sig を生成します。秘密鍵は $MURASAKI_UPDATE_KEY から、なければ .murasaki/update-key から読み込まれます。.murasaki/update-key.pub が存在する場合、署名前にマニフェストへ keyId ヒントも書き込みます(鍵のローテーション参照)。

--generate-manifest は、--manifest の非推奨エイリアスとして引き続き動作します。)

GitHub Actions

リリースワークフローがすべきことは、各プラットフォーム向けの更新ペイロードをビルドし、マニフェストを生成・署名し、それらすべてを同じ GitHub Release にアップロードすることです — そうすることで、デフォルトの updater: true が使う URL(releases/latest/download/latest.json)が実際にそれらを解決できます:

.github/workflows/release.yml
name: Release
on:
  push:
    tags: ['v*']

jobs:
  macos:
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with: { node-version: 24 }
      - run: pnpm install
      - run: pnpm exec murasaki bundle            # darwin-arm64(ホストアーキテクチャ)
      - run: pnpm exec murasaki bundle --arch x64  # darwin-x64(クロスアーキテクチャ)
      - uses: actions/upload-artifact@v4
        with:
          name: macos-payloads
          path: dist/bundle/*.app.zip

  windows:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with: { node-version: 24 }
      - run: pnpm install
      - name: Install NSIS
        shell: pwsh
        run: |
          choco install nsis -y
          echo "C:\Program Files (x86)\NSIS" >> $env:GITHUB_PATH
      - run: pnpm exec murasaki installer
      - uses: actions/upload-artifact@v4
        with:
          name: windows-payload
          path: dist/*-setup-*.exe

  publish:
    needs: [macos, windows]
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with: { node-version: 24 }
      - run: pnpm install
      - uses: actions/download-artifact@v4
        with: { name: macos-payloads, path: dist/bundle }
      - uses: actions/download-artifact@v4
        with: { name: windows-payload, path: dist }
      - name: Build + sign the manifest
        env:
          MURASAKI_UPDATE_KEY: ${{ secrets.MURASAKI_UPDATE_KEY }}
        run: |
          VERSION="${GITHUB_REF_NAME#v}"
          BASE_URL="https://github.com/${{ github.repository }}/releases/download/${GITHUB_REF_NAME}"
          pnpm exec murasaki release --manifest --base-url "$BASE_URL" --version "$VERSION"
          pnpm exec murasaki release --sign
      - uses: softprops/action-gh-release@v2
        with:
          files: |
            dist/bundle/*.app.zip
            dist/*-setup-*.exe
            dist/latest.json
            dist/latest.json.sig

MURASAKI_UPDATE_KEY を秘密鍵ファイルからリポジトリシークレットとして追加してください。シェルの引数に鍵を直接貼り付けないでください。これはコード署名とは別の話です — Developer ID で署名・公証された .dmg も必要な場合は配布を参照してください。同じ macos ジョブにそのステップを追加しても構いません。

この例は win32-x64 のみを公開しますが、win32-arm64 にも対応しています(下記のプラットフォーム対応状況参照)— windows ジョブをもう1つ(またはビルドマトリクスの1エントリ)追加し、murasaki bundle --target win32-arm64 / murasaki installer --target win32-arm64 を実行して、その -setup-arm64.exe を x64 のものと一緒にアップロードしてください。アーキテクチャのサフィックス付きファイル名なので、同じ dist/ に置いても衝突しません。

適用トランザクションと初回起動時のロールバック

ネイティブランチャーは、新しいインストールが正しく動作することを期待して、現在のインストールを先に削除するようなことはしません。インストール済みアプリの隣に所有者専用のジャーナルをアトミックに書き込み、現在のインストールを同一ボリューム上のバックアップへリネームしたうえで、検証済みのペイロードをインストールし、そのバックアップは再起動後も保持し続けます。

新しいバージョンが正常性を確認済みと判断されるのは、パッケージ化された Node サーバーがリッスンを開始し、プライマリインスタンスのエンドポイントが公開され、createOnLaunch: true のネイティブウィンドウ / WebView がすべて作成された後です。休止状態のテンプレートはメタデータとして検証されますが、この初回起動チェックポイントの対象には含まれません。入れ替え処理の途中でアップデートヘルパーが停止した場合や、新しいランチャーの初回起動がこのチェックポイントに到達する前に終了した場合、次回の起動時にインストール先の外へリカバリーヘルパーがコピーされ、旧バージョンを復元して再起動します。PID の所有権確認、排他的なジャーナルロック、アプリスコープの単一コンポーネントバックアップ名により、別プロセスやパストラバーサルによってこのトランザクションが乗っ取られることを防いでいます。

ロールバックの対象は、初回起動チェックポイントまでのアプリケーションファイルです。それ以降のクラッシュや、NSIS が変更する Windows のレジストリ / ショートカットは、このファイルトランザクションの対象外です。リカバリーのリネーム処理そのものの最中に電源が失われた場合は、手動での修復が必要になることもあります。新しいバージョン側で独自の永続的なマイグレーションチェックポイントが確立されるまでは、アプリのデータマイグレーションを旧バージョンと後方互換に保ってください。

セキュリティモデル

署名検証は必須です — それを無効化する設定オプションはありません。 すべての check()latest.jsonlatest.json.sig を取得し、マニフェストの生バイト列に対する Ed25519 署名をアプリの公開鍵で検証し、失敗した場合はそのマニフェストを信頼することを拒否します。すべての download() は、(すでに検証済みの)マニフェスト内のハッシュに対して、ペイロードの SHA-256 を個別に再検証します。

この組み合わせにより、ファイルホストだけを制御できる攻撃者(侵害された CDN、MITM されたミラー、セルフホストの dist/ バケットへの悪意ある PR など)は、秘密鍵も同時に握っていない限り、偽の更新を送り込むことはできません — 秘密鍵は .murasaki/update-key / あなたの CI シークレットの外に出ることはありません。

マニフェストの署名に加えて、murasaki installer --target win32-x64 --sign で Authenticode を使用してください — 配布を参照してください。--sign がなければ、Windows における更新の真正性保証は Ed25519 だけになります。

.murasaki/update-key はバージョン管理の外に置いてください(--keygen が自動的に gitignore します)。MURASAKI_UPDATE_KEY は、他のリリース署名用シークレットと同様に扱ってください。

セルフホストの updater.endpointhttps: である必要があります — http: はループバックホスト(127.0.0.1localhost[::1])に対してのみ、ローカルテスト用に許可されます。これは設定の読み込み時と取得時の両方で強制されるため、何らかの理由で検証を迂回した設定であっても、平文のエンドポイントを指すことはできません。GitHub ホストのマニフェストは常に https: で取得されるため、これは endpoint を設定した場合にのみ関係します(マニフェストのセルフホスティング参照)。

マニフェストの鮮度

すべてのマニフェストには generatedAt タイムスタンプ(murasaki release --manifest が自動生成)が必要で、他の何よりも先にこの値で鮮度を確認します。updater.maxManifestAgeDays(デフォルト 90、最小 1)より古いマニフェストは、固定化されたか、あるいはリプレイされた可能性があるものとして拒否されます。この仕組みがないと、一度正しく署名された古いマニフェストを手に入れた攻撃者が、それを永遠に配信し続けることで、既知の脆弱なバージョンに配布済みのアプリ全体を固定してしまうおそれがあります。24 時間より未来の日付が付いたマニフェストも、時刻のずれや改ざんの兆候として拒否されます(この許容範囲は固定で、設定はできません)。generatedAt が無いマニフェストは、安全側に倒して拒否されます。移行期間中に限り allowLegacyManifestsWithoutGeneratedAt: true で一時的に許可できますが、警告がログに記録され、リプレイ保護も弱まるため、可能な限りマニフェストを再生成・再署名してください。パッケージ化されたアプリは、チャンネルごとに認証済みの最新の generatedAt とバージョンを OS のアプリデータディレクトリへ永続化します。後から届いたマニフェストがそのどちらかを巻き戻す場合、署名と経過時間が有効であっても拒否されます。

murasaki.config.ts
export default defineConfig({
  // ...
  updater: {
    maxManifestAgeDays: 30, // 30日より古いものを拒否
    // allowLegacyManifestsWithoutGeneratedAt: true, // migration時のみ
  },
})

鍵のローテーション

publicKeys で複数の Ed25519 公開鍵をピン留めする(publicKey を含め合計最大 4 つ)ことで、既に出荷済みのアプリを壊さずに鍵をローテーションできます。検証はピン留めされたすべての鍵を成功するまで順に試すため、旧鍵と新鍵のどちらを持つアプリでも、いずれの鍵で署名されたマニフェストも信頼し続けます。

murasaki.config.ts
export default defineConfig({
  // ...
  updater: {
    publicKey: 'OLD_KEY_BASE64...',      // ローテーション前の .murasaki/update-key.pub
    publicKeys: ['NEW_KEY_BASE64...'],   // ローテーションに先立って追加する新しい鍵
  },
})

murasaki release --sign は、.murasaki/update-key.pub が利用可能な場合、署名の隣に keyId(生の公開鍵の sha256 の最初の8バイト、hex)もマニフェストへ書き込みます。クライアントは keyId を、ピン留めされた鍵のうちどれを最初に試すかのヒントとしてのみ使用します — 常にピン留めされたすべての鍵を試すフォールバックを行うため、ヒントが欠けていたり古かったりしても、誤って拒否されることはありません。

ローテーションの手順(3回のリリースにわたって):

  1. バージョン N[旧鍵, 新鍵] の両方をピン留めして出荷します。署名はまだ旧鍵で行います。N をインストールまたは更新したすべてのアプリは、これで両方の鍵を信頼するようになります。
  2. バージョン N+1murasaki release --keygen --force(または別の方法で新しい鍵に差し替え)を実行し、新しい鍵で署名します。N(またはそれ以降)にいるアプリは既にそれを信頼しています。まだ N より前のバージョンのアプリは、まず N に更新するまでその鍵を拒否します。
  3. バージョン N+2publicKeys / publicKey から旧鍵を外し、新しい鍵だけをピン留めします。この時点で、追随しているすべてのアプリは、旧鍵しか知らなかったバージョンをとうに通り過ぎています。

段階的ロールアウト

murasaki release --manifest --rollout <0-100> は、任意の rollout 率(%)をマニフェストへ書き込みます。未設定(または 100)の場合、今まで通りすべてのクライアントが即座に更新を確認できます。

100 未満の場合、インストール済みの各アプリは、最初にチェックした際に永続化されたランダムな ID(update-client-id。Main の OS 標準 context.paths.data ディレクトリに保存)から安定したバケットを計算します。可変なロールアウトの状態を、署名済み・読み取り専用のアプリリソースへ書き込むことはなく、そこに置かれるのは 1 回限りの .murasaki-apply.json というランチャーへの引き渡しデータだけです。バケットは sha256(id) の最初のバイトを 100 で割った余りです。バケットが >= rollout のクライアントは、そのチェックで「対応プラットフォーム向けの更新はありません」の場合と全く同じ not-available を受け取ります。エラーにもリトライの嵐にもならず、次回の定期チェックでまた試すだけです — その頃には割合を上げているかもしれません。これは配布を調整するためのノブであり、セキュリティ境界ではありません — クライアントが何をインストールできるかは変わらず、いつ提供されるかだけが変わります。

プラットフォーム対応状況

プラットフォーム更新ペイロード状態
macOS (arm64)<productName>-darwin-arm64.app.zip対応
macOS (x64)<productName>-darwin-x64.app.zip対応
Windows (x64)<productName>-<version>-setup-x64.exe対応
Windows (arm64)<productName>-<version>-setup-arm64.exe対応
Linux AppImage (x64 / arm64)<productName>-<version>-linux-{x64,arm64}.AppImage対応
Linux .deb / 手動展開の AppDir対象外 — check() は「システムのパッケージマネージャーが管理」と返します

Windows の自己更新は、意図的に ユーザー単位の NSIS インストールと結び付けられています。updater を設定すると、murasaki installer は NSIS を必須とし、MSI をスキップし、installer.windows.installMode: 'perMachine' を拒否します。MSI は、組み込みのアップデーターを無効にした場合にのみ使える、システム管理型のデプロイ方式です。この場合は Murasaki のアップデートエンジンではなく、MSI のメジャーアップグレードか、組織のソフトウェア管理システムで更新してください。

murasaki installer は NSIS インストーラーの名前にアーキテクチャのサフィックスを付けるため、x64 と arm64 のビルドが同じ dist/ に置かれても衝突しません。murasaki release --manifest は、サフィックスなしの旧 -setup.exe という名前も引き続き認識するため、この変更より前に公開された資産も win32-x64 として解決され続けます。

マニフェストのセルフホスティング

GitHub Releases を使わない場合は、repo の代わりに endpoint を、latest.json を配信する任意の URL に向けてください(latest.json.sig も同じ場所に置きます):

murasaki.config.ts
export default defineConfig({
  // ...
  updater: {
    endpoint: 'https://updates.example.com/latest.json',
  },
})

repoendpoint は互いに排他的です — 両方が設定されていると、murasaki はビルド時/開発時にエラーになります。endpointhttps: である必要があります(ローカルテスト用のループバック例外については、上記のセキュリティモデルを参照してください)。murasaki release --manifest --base-url https://updates.example.com を実行し、dist/latest.json + dist/latest.json.sig + 各ペイロードを、その URL が解決する場所にアップロードしてください。

GitHub 上で stable 以外のチャンネルを使う場合、Murasaki は releases/latest/download/… の代わりに releases/download/<channel>/latest.json を参照します — そのチャンネル向けにリリースのたびにプッシュし直す、移動するタグです(例: ベータチャンネル向けの beta タグ)。

次へ

GitHub でこのページを改善

On this page