T-CREATOR

Electron 運用:コード署名・公証・アップデート鍵管理のベストプラクティス

Electron 運用:コード署名・公証・アップデート鍵管理のベストプラクティス

Electron アプリケーションを本番環境で運用する際、コード署名や公証、アップデート機能の鍵管理は避けて通れない重要な課題です。これらを適切に実装しないと、ユーザーの信頼を損なうだけでなく、セキュリティリスクを抱えることになります。本記事では、実際の運用現場で役立つベストプラクティスを、具体的なコード例とともにご紹介しますね。

背景

Electron アプリケーションはクロスプラットフォーム対応が魅力ですが、各プラットフォーム特有のセキュリティ要件を満たす必要があります。

macOS では Gatekeeper による厳格な検証が行われ、Windows では SmartScreen フィルターがアプリケーションの信頼性をチェックしますね。これらのセキュリティ機構を通過するには、適切なコード署名と公証プロセスが不可欠です。

以下の図は、Electron アプリケーションのビルドから配布までのセキュリティフローを示しています。

mermaidflowchart TB
    dev["開発者"] -->|コミット| code["ソースコード"]
    code -->|ビルド| app["Electron アプリ"]
    app -->|署名| signed["署名済みアプリ"]
    signed -->|公証<br/>macOS| notarized["公証済みアプリ"]
    signed -->|検証<br/>Windows| verified["検証済みアプリ"]
    notarized -->|配布| cdn["CDN/サーバー"]
    verified -->|配布| cdn
    cdn -->|自動更新| user["エンドユーザー"]
    user -->|OS検証| install["インストール実行"]

この図からわかるように、開発から配布まで複数のセキュリティチェックポイントが存在します。各段階で適切な処理を行うことで、ユーザーに安全なアプリケーションを届けられるのです。

プラットフォーム別の要件

各プラットフォームには独自のセキュリティ要件があり、それぞれに対応した署名・公証プロセスが必要です。

#プラットフォーム必須プロセス証明書の種類検証機構
1macOSコード署名 + 公証Developer ID ApplicationGatekeeper
2Windowsコード署名EV/OV Code Signing CertificateSmartScreen
3Linux任意(推奨)GPG 署名ディストリビューション依存

課題

Electron アプリケーションの運用では、以下のような課題に直面することが多いでしょう。

セキュリティと利便性のバランス

証明書や秘密鍵を安全に管理しながら、CI/CD パイプラインでの自動ビルドを実現する必要があります。秘密鍵を Git リポジトリにコミットするわけにはいきませんし、かといって手動での署名作業は現実的ではありません。

自動更新の信頼性確保

アップデート機能は便利ですが、中間者攻撃のリスクがあります。不正なアップデートがユーザーに配信されないよう、署名検証の仕組みが必要ですね。

マルチプラットフォーム対応の複雑さ

macOS、Windows、Linux それぞれで異なる署名・公証プロセスを管理するのは容易ではありません。各プラットフォームの要件を理解し、適切なツールチェーンを構築する必要があるのです。

以下の図は、運用上の主な課題を構造化して示しています。

mermaidflowchart TD
    challenges["運用課題"]

    challenges --> sec["セキュリティ管理"]
    challenges --> auto["自動化"]
    challenges --> multi["マルチプラットフォーム"]

    sec --> keys["秘密鍵の保護"]
    sec --> access["アクセス制御"]
    sec --> rotation["鍵のローテーション"]

    auto --> ci["CI/CD統合"]
    auto --> signing["自動署名"]
    auto --> deploy["自動デプロイ"]

    multi --> mac["macOS<br/>Gatekeeper"]
    multi --> win["Windows<br/>SmartScreen"]
    multi --> linux["Linux<br/>パッケージ署名"]

これらの課題を解決するには、体系的なアプローチと適切なツールの選択が重要になります。

解決策

コード署名の実装

electron-builder を使用することで、各プラットフォームのコード署名を統一的に管理できます。

基本設定ファイルの作成

まず、electron-builder の設定ファイルを用意しましょう。

json{
  "appId": "com.example.app",
  "productName": "MyElectronApp",
  "directories": {
    "output": "dist"
  },
  "files": [
    "build/**/*",
    "node_modules/**/*",
    "package.json"
  ]
}

この設定ファイルは、アプリケーションの基本情報とビルド対象を定義します。appId はアプリケーションの一意識別子として、各プラットフォームで使用されますね。

macOS 向けコード署名設定

macOS 用の署名設定を追加します。

json{
  "mac": {
    "category": "public.app-category.productivity",
    "hardenedRuntime": true,
    "gatekeeperAssess": false,
    "entitlements": "build/entitlements.mac.plist",
    "entitlementsInherit": "build/entitlements.mac.plist",
    "target": [
      {
        "target": "dmg",
        "arch": ["x64", "arm64"]
      }
    ]
  }
}

hardenedRuntime を true にすることで、macOS のセキュリティ要件を満たすランタイム保護が有効になります。これは公証に必須の設定です。

Windows 向けコード署名設定

Windows 用の署名設定も追加しましょう。

json{
  "win": {
    "target": [
      {
        "target": "nsis",
        "arch": ["x64", "ia32"]
      }
    ],
    "certificateSubjectName": "Your Company Name",
    "signingHashAlgorithms": ["sha256"],
    "rfc3161TimeStampServer": "http://timestamp.digicert.com"
  }
}

タイムスタンプサーバーを指定することで、証明書の有効期限が切れた後でも署名の有効性を検証できるようになります。

エンタイトルメント設定

macOS のエンタイトルメントファイルを作成します。これは、アプリケーションが必要とする権限を宣言するファイルですね。

xml<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>com.apple.security.cs.allow-jit</key>
  <true/>
  <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
  <true/>
  <key>com.apple.security.cs.disable-library-validation</key>
  <true/>
</dict>
</plist>

このファイルは build​/​entitlements.mac.plist として保存します。JIT コンパイラや動的ライブラリの読み込みに必要な権限を宣言していますね。

環境変数による認証情報管理

秘密鍵や証明書を安全に扱うため、環境変数を使用します。

macOS の環境変数設定

bash# Apple Developer アカウント情報
export APPLE_ID="your-apple-id@example.com"
export APPLE_ID_PASSWORD="app-specific-password"
export APPLE_TEAM_ID="YOUR_TEAM_ID"

# 証明書情報
export CSC_LINK="/path/to/certificate.p12"
export CSC_KEY_PASSWORD="certificate-password"

App-Specific Password は Apple ID の 2 要素認証を有効にした状態で、Apple ID の管理画面から生成できます。この認証情報は絶対に Git にコミットしないでください。

Windows の環境変数設定

bash# Windows コード署名証明書
export CSC_LINK="/path/to/certificate.pfx"
export CSC_KEY_PASSWORD="certificate-password"

# または証明書ストアを使用する場合
export WIN_CSC_LINK="/path/to/certificate.pfx"
export WIN_CSC_KEY_PASSWORD="certificate-password"

証明書ファイルは、信頼できる認証局から取得した EV または OV 証明書を使用しましょう。

公証プロセスの自動化

macOS アプリケーションの公証を自動化するスクリプトを作成します。

公証スクリプトの基本構造

javascript// scripts/notarize.js
const { notarize } = require('@electron/notarize');

async function notarizeMacOS() {
  console.log('公証プロセスを開始します...');

  const appPath = 'dist/mac/MyElectronApp.app';

  // 環境変数の確認
  if (!process.env.APPLE_ID || !process.env.APPLE_ID_PASSWORD) {
    console.log('公証に必要な環境変数が設定されていません');
    return;
  }

このスクリプトは、ビルド後に自動的に公証プロセスを実行するための基盤となります。

公証の実行処理

javascript  try {
    await notarize({
      appPath: appPath,
      appleId: process.env.APPLE_ID,
      appleIdPassword: process.env.APPLE_ID_PASSWORD,
      teamId: process.env.APPLE_TEAM_ID,
    });

    console.log('公証が完了しました!');
  } catch (error) {
    console.error('公証に失敗しました:', error);
    process.exit(1);
  }
}

module.exports = notarizeMacOS;

公証には数分から数十分かかることがあります。エラーが発生した場合は、Apple からのメールで詳細な理由を確認できますね。

afterSign フックの設定

electron-builder の設定に afterSign フックを追加します。

json{
  "afterSign": "scripts/notarize.js",
  "mac": {
    "hardenedRuntime": true,
    "entitlements": "build/entitlements.mac.plist"
  }
}

これにより、署名後に自動的に公証プロセスが実行されるようになります。

自動更新機能の実装

electron-updater を使用して、安全な自動更新機能を実装しましょう。

依存パッケージのインストール

bashyarn add electron-updater

メインプロセスでの更新チェック実装

typescript// main.ts
import { app, BrowserWindow } from 'electron';
import { autoUpdater } from 'electron-updater';
import log from 'electron-log';

// ログ設定
autoUpdater.logger = log;
log.transports.file.level = 'info';

ログ機能を有効にすることで、更新プロセスのデバッグが容易になります。本番環境でも問題の原因を特定しやすくなりますね。

更新チェックのロジック

typescript// 自動更新の設定
function setupAutoUpdater(mainWindow: BrowserWindow) {
  // 開発環境では更新チェックをスキップ
  if (process.env.NODE_ENV === 'development') {
    return;
  }

  // 更新が利用可能になった時
  autoUpdater.on('update-available', (info) => {
    log.info('新しいバージョンが利用可能です:', info.version);
    mainWindow.webContents.send('update-available', info);
  });

開発環境での不要な更新チェックを防ぐため、環境変数で制御します。

更新のダウンロードとインストール

typescript// 更新をダウンロード中
autoUpdater.on('download-progress', (progressObj) => {
  log.info(
    `ダウンロード速度: ${progressObj.bytesPerSecond}`
  );
  log.info(`進捗: ${progressObj.percent}%`);
  mainWindow.webContents.send(
    'download-progress',
    progressObj
  );
});

// 更新のダウンロードが完了
autoUpdater.on('update-downloaded', (info) => {
  log.info('更新のダウンロードが完了しました');
  mainWindow.webContents.send('update-downloaded', info);
});

ダウンロードの進捗をレンダラープロセスに送信することで、ユーザーに適切なフィードバックを提供できます。

自動更新の開始

typescript  // エラーハンドリング
  autoUpdater.on('error', (error) => {
    log.error('自動更新でエラーが発生しました:', error);
    mainWindow.webContents.send('update-error', error.message);
  });

  // アプリ起動後に更新をチェック
  autoUpdater.checkForUpdatesAndNotify();
}

app.whenReady().then(() => {
  const mainWindow = createWindow();
  setupAutoUpdater(mainWindow);
});

アプリケーション起動時に自動的に更新をチェックし、新しいバージョンがあれば通知します。

署名検証の実装

アップデートファイルの署名を検証する設定を追加しましょう。

publish 設定の追加

json{
  "publish": {
    "provider": "github",
    "owner": "your-username",
    "repo": "your-repo",
    "private": false,
    "releaseType": "release"
  }
}

GitHub Releases を使用する場合の設定例です。S3 や独自サーバーも利用できます。

署名検証の有効化

typescript// main.ts での設定
import { autoUpdater } from 'electron-updater';

// 署名検証を有効化(デフォルトで有効)
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = true;

// カスタム更新サーバーを使用する場合
autoUpdater.setFeedURL({
  provider: 'generic',
  url: 'https://your-update-server.com/releases',
  channel: 'stable',
});

autoDownload を false にすることで、更新の確認とダウンロードを分離し、ユーザーに選択権を与えられます。

CI/CD パイプラインでの自動署名

GitHub Actions を使用した自動署名・公証のワークフロー例です。

ワークフローファイルの基本構造

yaml# .github/workflows/build.yml
name: Build and Release

on:
  push:
    tags:
      - 'v*'

jobs:
  release:
    runs-on: ${{ matrix.os }}

    strategy:
      matrix:
        os: [macos-latest, windows-latest, ubuntu-latest]

タグがプッシュされたときに自動的にビルド・リリースプロセスが開始されます。

環境変数とシークレットの設定

yamlenv:
  # macOS 公証用
  APPLE_ID: ${{ secrets.APPLE_ID }}
  APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
  APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
  # コード署名証明書
  CSC_LINK: ${{ secrets.CSC_LINK }}
  CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}

GitHub リポジトリの Settings > Secrets から、これらの環境変数を事前に設定しておく必要があります。

ビルドステップの定義

yamlsteps:
  - name: チェックアウト
    uses: actions/checkout@v3

  - name: Node.js セットアップ
    uses: actions/setup-node@v3
    with:
      node-version: '18'
      cache: 'yarn'

  - name: 依存関係のインストール
    run: yarn install --frozen-lockfile

lockfile を使用することで、ビルドの再現性を確保します。

証明書のインポート (macOS)

yaml- name: 証明書のインポート (macOS)
  if: matrix.os == 'macos-latest'
  run: |
    echo $CSC_LINK | base64 --decode > certificate.p12
    security create-keychain -p actions build.keychain
    security default-keychain -s build.keychain
    security unlock-keychain -p actions build.keychain
    security import certificate.p12 -k build.keychain -P $CSC_KEY_PASSWORD -T /usr/bin/codesign
    security set-key-partition-list -S apple-tool:,apple: -s -k actions build.keychain
    rm certificate.p12

このステップでは、Base64 エンコードされた証明書を一時的にデコードし、キーチェーンにインポートします。処理後は証明書ファイルを削除してセキュリティを確保しますね。

ビルドとリリース

yaml- name: ビルド
  run: yarn build

- name: リリース
  env:
    GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  run: yarn electron-builder --publish always

--publish always オプションにより、ビルド成果物が自動的に GitHub Releases にアップロードされます。

鍵管理のベストプラクティス

以下の図は、推奨される鍵管理フローを示しています。

mermaidflowchart TB
    cert["証明書取得"]

    cert --> store["シークレット管理"]

    store --> vault["AWS Secrets Manager<br/>Azure Key Vault<br/>HashiCorp Vault"]
    store --> github["GitHub Secrets"]
    store --> env["環境変数<br/>ローカル開発"]

    vault --> ci["CI/CD<br/>パイプライン"]
    github --> ci
    env --> local["ローカルビルド"]

    ci --> build["自動ビルド<br/>署名"]
    local --> manual["手動ビルド<br/>署名"]

    build --> release["リリース"]
    manual --> test["テスト配布"]

この図が示すように、本番環境と開発環境で異なる鍵管理戦略を採用することが重要です。

シークレット管理のルール

#項目ベストプラクティス避けるべき行動
1証明書の保管暗号化されたシークレット管理サービスGit リポジトリへのコミット
2アクセス制御最小権限の原則に基づく付与全員に管理者権限を付与
3ローテーション定期的な証明書の更新有効期限切れまで放置
4バックアップ複数の安全な場所に保管単一の保管場所のみ
5監査ログアクセス履歴の記録と確認ログを取らない

具体例

実際のプロジェクトで使用できる完全な設定例をご紹介します。

プロジェクト構成

cssmy-electron-app/
├── src/
│   ├── main.ts              # メインプロセス
│   └── renderer/            # レンダラープロセス
├── build/
│   ├── entitlements.mac.plist
│   └── icon.png
├── scripts/
│   └── notarize.js
├── .github/
│   └── workflows/
│       └── build.yml
├── electron-builder.json
└── package.json

この構成により、ソースコード、ビルド設定、CI/CD 設定が明確に分離されます。

完全な electron-builder 設定

json{
  "appId": "com.example.myapp",
  "productName": "MyElectronApp",
  "directories": {
    "output": "dist",
    "buildResources": "build"
  },
  "files": [
    "build/**/*",
    "node_modules/**/*",
    "package.json"
  ],
  "afterSign": "scripts/notarize.js",
  "mac": {
    "category": "public.app-category.productivity",
    "icon": "build/icon.png",
    "hardenedRuntime": true,
    "gatekeeperAssess": false,
    "entitlements": "build/entitlements.mac.plist",
    "entitlementsInherit": "build/entitlements.mac.plist",
    "target": [
      {
        "target": "dmg",
        "arch": ["x64", "arm64"]
      },
      {
        "target": "zip",
        "arch": ["x64", "arm64"]
      }
    ]
  }
}

DMG と ZIP の両方を生成することで、異なる配布チャネルに対応できます。

Windows と Linux の設定

json{
  "win": {
    "icon": "build/icon.png",
    "target": [
      {
        "target": "nsis",
        "arch": ["x64", "ia32"]
      },
      {
        "target": "portable",
        "arch": ["x64"]
      }
    ],
    "certificateSubjectName": "Your Company Name",
    "signingHashAlgorithms": ["sha256"],
    "rfc3161TimeStampServer": "http://timestamp.digicert.com"
  },
  "linux": {
    "icon": "build/icon.png",
    "target": ["AppImage", "deb", "rpm"],
    "category": "Utility"
  }
}

Linux では複数のパッケージ形式を生成し、様々なディストリビューションに対応します。

publish 設定の完全版

json{
  "publish": [
    {
      "provider": "github",
      "owner": "your-username",
      "repo": "your-repo",
      "releaseType": "release"
    },
    {
      "provider": "s3",
      "bucket": "your-bucket",
      "region": "us-east-1",
      "path": "/releases"
    }
  ]
}

GitHub Releases と S3 の両方に同時配信することで、冗長性を確保できますね。

package.json のスクリプト設定

json{
  "name": "my-electron-app",
  "version": "1.0.0",
  "main": "build/main.js",
  "scripts": {
    "start": "electron .",
    "build": "tsc",
    "pack": "electron-builder --dir",
    "dist": "electron-builder",
    "dist:mac": "electron-builder --mac",
    "dist:win": "electron-builder --win",
    "dist:linux": "electron-builder --linux",
    "release": "electron-builder --publish always"
  }
}

プラットフォーム別のビルドコマンドを用意することで、開発効率が向上します。

依存パッケージの定義

json{
  "dependencies": {
    "electron-updater": "^6.1.7",
    "electron-log": "^5.0.1"
  },
  "devDependencies": {
    "electron": "^28.0.0",
    "electron-builder": "^24.9.1",
    "@electron/notarize": "^2.2.1",
    "typescript": "^5.3.3"
  }
}

レンダラープロセスでの更新 UI 実装

ユーザーフレンドリーな更新通知を実装しましょう。

更新イベントのリスナー設定

typescript// renderer.ts
import { ipcRenderer } from 'electron';

// 更新が利用可能になった時の処理
ipcRenderer.on('update-available', (event, info) => {
  showNotification(
    '新しいバージョンが利用可能です',
    `バージョン ${info.version} をダウンロードしています...`
  );
});

ダウンロード進捗の表示

typescript// ダウンロード進捗の更新
ipcRenderer.on('download-progress', (event, progress) => {
  const percentage = Math.round(progress.percent);
  updateProgressBar(percentage);

  const downloaded = formatBytes(progress.transferred);
  const total = formatBytes(progress.total);
  updateProgressText(
    `${downloaded} / ${total} (${percentage}%)`
  );
});

進捗状況を視覚的に表示することで、ユーザーエクスペリエンスが向上します。

更新完了とインストール

typescript// 更新のダウンロード完了
ipcRenderer.on('update-downloaded', (event, info) => {
  showDialog({
    title: '更新の準備が完了しました',
    message: `バージョン ${info.version} のインストール準備ができました。今すぐ再起動しますか?`,
    buttons: ['今すぐ再起動', '後で'],
    callback: (response) => {
      if (response === 0) {
        ipcRenderer.send('quit-and-install');
      }
    },
  });
});

ユーティリティ関数

typescript// バイト数を読みやすい形式に変換
function formatBytes(bytes: number): string {
  if (bytes === 0) return '0 Bytes';

  const k = 1024;
  const sizes = ['Bytes', 'KB', 'MB', 'GB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));

  return (
    Math.round((bytes / Math.pow(k, i)) * 100) / 100 +
    ' ' +
    sizes[i]
  );
}

このヘルパー関数により、ダウンロードサイズをユーザーが理解しやすい形式で表示できますね。

エラーハンドリングの実装

本番環境では、適切なエラーハンドリングが不可欠です。

メインプロセスでのエラー処理

typescript// main.ts
import { app, dialog } from 'electron';
import log from 'electron-log';

// 署名検証エラーの処理
autoUpdater.on('error', (error) => {
  log.error('自動更新エラー:', error);

  // 署名検証エラーの特定
  if (error.message.includes('signature')) {
    dialog.showErrorBox(
      '更新の検証に失敗しました',
      '更新ファイルの署名が無効です。後ほど再試行してください。'
    );
    return;
  }

署名検証エラーは特に重要なため、明示的にユーザーに通知します。

ネットワークエラーの処理

typescript  // ネットワークエラーの処理
  if (error.message.includes('net::')) {
    log.warn('ネットワークエラーのため更新をスキップします');
    // 次回起動時に再試行
    return;
  }

  // その他のエラー
  dialog.showErrorBox(
    '更新エラー',
    `更新中にエラーが発生しました: ${error.message}`
  );
});

ネットワークエラーの場合は、次回起動時に自動的に再試行するのが良いでしょう。

セキュリティ強化のための追加設定

Content Security Policy の設定

typescript// main.ts
import { session } from 'electron';

app.whenReady().then(() => {
  session.defaultSession.webRequest.onHeadersReceived(
    (details, callback) => {
      callback({
        responseHeaders: {
          ...details.responseHeaders,
          'Content-Security-Policy': [
            "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'",
          ],
        },
      });
    }
  );
});

CSP を設定することで、XSS 攻撃のリスクを軽減できます。

安全な IPC 通信の実装

typescript// preload.ts
import { contextBridge, ipcRenderer } from 'electron';

// 許可されたチャンネルのリスト
const validChannels = [
  'update-available',
  'download-progress',
  'update-downloaded',
  'update-error',
];

contextBridge.exposeInMainWorld('electronAPI', {
  onUpdateEvent: (channel: string, callback: Function) => {
    if (validChannels.includes(channel)) {
      ipcRenderer.on(channel, (event, ...args) =>
        callback(...args)
      );
    }
  },
  quitAndInstall: () => {
    ipcRenderer.send('quit-and-install');
  },
});

contextBridge を使用することで、レンダラープロセスへの安全な API 公開が実現できますね。

まとめ

Electron アプリケーションの運用において、コード署名・公証・アップデート鍵管理は、ユーザーの信頼とセキュリティを確保するための重要な要素です。

本記事でご紹介したベストプラクティスを実践することで、以下のメリットが得られます。

主要なポイント

コード署名の徹底: electron-builder を活用し、macOS と Windows それぞれのプラットフォーム要件を満たす署名プロセスを自動化できます。hardenedRuntime やエンタイトルメントの適切な設定により、Gatekeeper や SmartScreen の検証をスムーズに通過できるでしょう。

公証プロセスの自動化: afterSign フックと @electron/notarize を組み合わせることで、ビルドから公証までを一貫した自動プロセスとして実装できます。これにより、手動作業によるミスを防ぎ、リリースサイクルを高速化できますね。

安全な鍵管理: 環境変数やシークレット管理サービスを活用し、証明書や秘密鍵を安全に保管・利用する体制を構築できます。GitHub Secrets や AWS Secrets Manager などのサービスを適切に使い分けることが重要です。

信頼性の高い自動更新: electron-updater による署名検証付きの自動更新機能により、ユーザーに常に最新かつ安全なバージョンを提供できます。エラーハンドリングを適切に実装することで、更新プロセスの失敗にも柔軟に対応できるでしょう。

実装の優先順位

まずはコード署名から始めることをお勧めします。各プラットフォームの証明書を取得し、基本的な署名プロセスを確立しましょう。次に公証プロセスを追加し、最後に自動更新機能を実装する流れが効率的です。

CI/CD パイプラインへの統合は、手動でのビルド・署名プロセスが安定してから取り組むと良いでしょう。段階的なアプローチにより、問題が発生した際の切り分けも容易になります。

セキュリティは一度設定して終わりではなく、継続的な改善が必要です。証明書の有効期限管理、鍵のローテーション、監査ログの確認などを定期的に実施することで、長期的に安全な運用を維持できるのです。

これらのベストプラクティスを実践し、ユーザーに信頼される Electron アプリケーションを提供していきましょう。

関連リンク