ログイン・トークン・ブラウザの管理
最終更新: 2026-10-25(指摘は PR #471 の時点のコードで調べた。直した状況は末尾の「8. 対応状況」)
ゲームサイト(戦績サイト)へのログインで得る token を、どこに保存し、どのプロセスがどう読み書きし、 ログイン用の ブラウザ(Playwright の Chromium/Chrome) をいつ開いて閉じるかをまとめる。 後半の「7. 矛盾・不具合の候補」は、コードを読んで見つけた食い違いの一覧。どれを直したかは「8. 対応状況」。
関連: Battle Record Direct API(token で API を直叩きする仕組み) / Electron Main
1. 全体像
3 つのプロセスが同じ userData(%APPDATA%\wingstats-manager など)を使う。
| プロセス | token / ブラウザとの関わり |
|---|---|
| Recorder(トレイ常駐・DB の唯一の書き手) | 戦績 API を叩く本体。token は保存ファイルから読む。401 などで自分のログイン窓(共通の保存先だけ)を開く |
| Manager(管理画面) | ログイン窓を開く(共通の保存先・アカウント別の保存先の両方)。Recorder が動いている間(委譲中)は取得を Recorder に頼む。ログイン確認・シーズン一覧も Recorder に聞く(PR #471) |
| Viewer(戦績窓) | token もブラウザも扱わない。Recorder の status に入っているログイン状態を表示するだけ |
token は ファイル経由 で受け渡す。Manager でログインしても、Recorder は次の取得のときにファイルを読み直して新しい token を使う (CredentialResolver.refresh() が毎回読み直す。electron/services/battleRecordApi/credentialsProvider.ts:31-35)。
2. token の置き場所
| パス(userData 以下) | 中身 | 書く | 読む |
|---|---|---|---|
browser-automation/storageState.json(共通の保存先) | Playwright の storageState(localStorage に token と selected_role_id) | Manager のログイン窓(アカウントを選ばずに開いたとき)・Recorder のログイン窓(常にここ) | Manager の単一経路・Recorder の単一経路(ログイン確認、0 アカウント時の取得など)・取得テスト |
browser-automation/profiles/<id>/storageState.json(アカウント別の保存先) | 同じ形 | Manager のアカウント別ログインだけ。起動時、無ければ共通の保存先から 1 回だけ複製(electron/services/profile/legacyStorageMigration.ts:13-24) | アカウントごとの取得(Recorder・Manager)、role_id の保存、アカウント管理タブの状態表示 |
<保存先>.corrupt | 壊れたファイルの退避 | 読み込みで壊れていたとき | 誰も読まない |
<保存先>.<pid>.tmp | 原子的に書くための一時ファイル | 保存のたび | 誰も読まない(落ちたときに残る) |
browser-automation/login-confirmed.json | ログインを確かめた日時と roleId(token は無し) | Recorder だけ | 誰も読まない(不具合候補 L-1) |
保存のタイミング(electron/services/browserAutomation/storageStateManager.ts):
- ブラウザが開いている間、60 秒ごと(5 秒以内の連続保存は間引く)
- 開いた直後 8 秒の確認でログインを見つけたとき(「login confirmed」)
- ブラウザを閉じるとき(
stop()で必ず保存) - ブラウザが落ちた(disconnected)ときは保存しない
3. 暗号化
- Electron の safeStorage で暗号化して保存する(
electron/utils/safeStorageCipher.ts、形はstorageStateEnvelope.ts)。鍵は userData のLocal Stateにあり、3 プロセスで共通なので、どのプロセスが書いたファイルでも他のプロセスが読める - 初回起動は平文で書く。起動した時点で
Local Stateに鍵がまだ無い場合、そのプロセスは終わるまで平文で書く(Manager と Recorder が別々の鍵で暗号化してしまうのを防ぐため。electron/main.tsのnoteSafeStorageKeyAtLaunch)。次の起動で暗号化し直す - 復号できないファイルは「壊れている」と同じ扱いで、ログインしていない状態で始める
app.setPath('sessionData')を動かすとLocal Stateの場所も変わり、他のプロセスの暗号を復号できなくなる(PR #468 で実験して確認。窓の localStorage は partition で分けている)
4. どの保存先の token を使うか
Manager(electron/ipc/BrowserAutomationHandlers.ts)
activeLoginProfileIdが入っていれば、そのアカウントの保存先。無ければ共通の保存先(storageStatePath())activeLoginProfileIdは「アカウントを選んでログイン」(profile-login-start)で入り、「アカウントを選ばずにログイン」(browser-automation-open-login)と開発用のbrowser-automation-startでだけ外れる- アカウントが登録されていれば、取得はアカウントごとに保存先を読む(
resolveProfileFetchTargetsWithHealing)。0 件なら単一経路
Recorder(electron/services/recorder/RecorderAutoStart.ts)
- 自分のスタック・ログイン窓・ログイン確認は 共通の保存先だけ(
storageStatePath()が legacy 固定、:414-416) - アカウントごとの取得は、アカウント別の保存先を読む(
stackForTarget)。アカウント別の保存先で 401 が出ても、ログイン窓は開かない(同時に何枚も開かないようにするため。スキップしてログに残すだけ)
ログイン直後の role_id の保存
- Manager がアカウント別にログインしたら、そのアカウントの role_id を DB に保存する。委譲中は Recorder に
profile:capture-roleを頼み、Recorder は ファイルから token を読む(electron/services/profile/ProfileLoginService.ts:22-35)
5. ブラウザ(ログイン窓)
共通のエンジンは electron/services/BrowserAutomationService.ts。Chrome → Edge の順に探して起動し、保存先の storageState を読み込んでから開く。 ログイン窓は常に画面に出す(headless は開発用の画面からしか使わない)。
Manager のログイン窓
| 開くきっかけ | 保存先 |
|---|---|
| 「ログインが必要です」ダイアログ → アカウントを選んで開く | アカウント別 |
| 同ダイアログ(アカウント 0 件) | 共通 |
| アカウント管理タブのログイン | アカウント別 |
| はじめてのセットアップのログイン | 共通(直前にアカウントを作っていても) |
| 未ログインで自動的に開く設定(既定 OFF) | 共通 |
| リアルタイム解析タブ | 共通(ログインページではなくホームを開く) |
- 開く・移動・閉じるは
runExclusiveで 1 本ずつ実行する - 開いたら 最大 15 分、ログインの完了を待つ(
electron/ipc/browserAutomationLoginWatch.ts、PR #467)。完了を見つけたら本人確認(newLoginCheck.ts)→ role_id の保存 → ブラウザを閉じる(閉じるときに token をファイルに保存する) - 開いているブラウザの保存先が、選んだアカウントのものと違えば開き直す(PR #467)
- 人が窓を閉じたら、ブラウザを片付けて、保存した token でログイン状態を確かめ直す(PR #467)
Recorder のログイン窓(electron/services/recorder/RecorderLoginWindow.ts)
- 開くきっかけ: 起動時の確認で未ログイン(戦績取得が ON のとき)、共通の保存先での 401、トレイの「今すぐログイン」、Manager のトレイ・設定画面からの
loginコマンド - 保存先は 共通だけ
- 60 秒のクールダウンがある(その間の「今すぐログイン」は何もしない)
- 最大 15 分ログインの完了を待ち、見つけたら本人確認 → ブラウザを閉じる → 戦績取得を ON にする(未設定のときだけ)
- 人が窓を閉じたことは検知しない
6. ログイン状態と通知
| Manager | Recorder | |
|---|---|---|
| 持っている状態 | lastLoginStatus(画面への通知は browser-automation-login-status) | loginStatus(status の snapshot に入り、Viewer が表示する) |
| 確かめ方 | 委譲中は Recorder の login:check に聞く。聞けなければ自分で token が要る API(battle_list)を 1 回叩く | 自分で battle_list を 1 回叩く(validateLoginStatusNow) |
| 通知 | 未ログインになったら OS 通知「ログインを確認できませんでした」(60 秒に 1 回まで) | ログイン窓を開いたら OS 通知「要ログイン → ログイン窓を開きました」 |
「ブラウザ ON/OFF」(両方のトレイ)は戦績取得の ON/OFF のことで、ブラウザが開いているかではない。
7. 矛盾・不具合の候補
コードを読んで見つけたもの(2026-10-25)。重さは、利用者に見える影響で付けた。直したものは「8. 対応状況」を見ること(下の表は見つけた時点の説明のまま)。
重い(利用者に見える)
| # | 内容 | 根拠 |
|---|---|---|
| H-1 | 再ログインしても、アカウント別の保存先が更新されない。 Recorder のログイン窓・はじめてのセットアップ・自動で開く窓・リアルタイム解析タブは、どれも共通の保存先に保存する。アカウント別の保存先が期限切れになると、そのアカウントの取得は 401 でスキップされ続け(ログに残るだけ)、窓も開かない。Recorder の確認は共通の保存先を見るので「ログイン済み」と出る | RecorderAutoStart.ts:414-416, 1275-1279, useOnboardingAccount.ts:144, legacyStorageMigration.ts:19-20, profileCredentials.ts:27-30 |
| H-2 | 起動時のログイン確認だけ、Recorder に聞いていない。 起動時の確認(main.ts:605)は、Recorder に聞くための仕組み(setWriterDelegationBridge、main.ts:711)を設定する前に走るので、いつも Manager が自分で API を叩く(PR #471 の取りこぼし) | main.ts:605, 711-716, BrowserAutomationHandlers.ts:614 |
| H-3 | ログイン窓を開いて 8 秒で「未ログイン」の通知が出る。 開いた直後の確認が 8 秒で終わると、まだ認証コードを入力している途中でも「ログインを確認できませんでした」の OS 通知が出て、画面のダイアログも出し直される(2026-10-24 の診断ログでも 8 秒後に「未ログイン」) | BrowserAutomationService.ts:201-204, loginProber.ts:107-114, BrowserAutomationHandlers.ts:462-478 |
| H-4 | 期限切れの token でも「ログイン済み」になる。 開いた直後の確認がタイムアウトしても、保存ファイルに token の文字列があるだけで「ログイン済み(storage-state)」を返す。Manager は状態を「ログイン済み」にして role_id の保存まで走らせ、Recorder は login-confirmed.json を書く | loginProber.ts:124-130, RecorderLoginWindow.ts:169-185 |
| H-5 | Manager と Recorder のログイン窓が同時に開くと、新しい token が古いもので上書きされうる。 どちらも共通の保存先に、ロックなしで 60 秒ごと・閉じるときに書く。片方でログインしても、もう片方の窓の保存(古い token か token 無し)が後に来れば消える | storageStateManager.ts:33-60, BrowserAutomationService.ts:198, 396 |
| H-6 | Recorder でログインしても、Manager は知らない。 Manager の画面のログイン状態とダイアログは、手で「ログイン確認」を押すまで「未ログイン」のまま。Viewer は Recorder の状態を出すので、表示が食い違う | BrowserAutomationHandlers.ts:458, 877-882, RecorderStatsApp.tsx:206-210 |
| H-7 | 委譲中、ログイン直後の role_id の保存が、token のファイル保存より先に走る。 15 分の見張りでログインを見つけた場合(PR #467 で普通の経路になった)、Recorder に role_id の保存を頼む時点では、まだ token がファイルに書かれていない(書かれるのはブラウザを閉じるとき)。2 秒後に 1 回だけやり直すが、初回ログインでは保存されないことが多い(あとで自己修復はされる) | BrowserAutomationHandlers.ts:559-561, browserAutomationProfileLogin.ts:60-90, ProfileLoginService.ts:22-35 |
| H-8 | 起動時に両方のプロセスから通知が出る。 未ログインで戦績取得が ON だと、Recorder は自分のログイン窓を開いて通知し、Manager も「ログインを確認できませんでした」を通知して、2 枚目の窓を開くダイアログを出す | RecorderAutoStart.ts:806-819, RecorderLoginWindow.ts:391-402, main.ts:605 |
中くらい
| # | 内容 | 根拠 |
|---|---|---|
| M-1 | アカウント別にログインした後、activeLoginProfileId が外れない。Manager の「単一経路」(シーズン取得・ログイン確認など)が、次に共通の保存先でログインするまでそのアカウントの保存先を読む | BrowserAutomationHandlers.ts:206-212、外れるのは :813, :921 だけ |
| M-2 | activeLoginProfileId を runExclusive の外で書き換える。2 つのログイン要求が重なると、順番待ちの方が別の要求の保存先で開く | :813, :921, :944 |
| M-3 | 本人確認で断られたログインも「受け入れ済み」に数える。同じ窓で正しくログインし直しても拾わず、窓を閉じても確かめ直さない | browserAutomationLoginWatch.ts:45-49, 111, BrowserAutomationHandlers.ts:551-557 |
| M-4 | Recorder のクールダウン(60 秒)中は、トレイ・Manager からの「今すぐログイン」が黙って何もしない(窓を閉じた直後でも) | RecorderLoginWindow.ts:373-382 |
| M-5 | Recorder は人がログイン窓を閉じても気づかない。状態は「開いている」のまま、見張りは 15 分待ち続ける | RecorderLoginWindow.ts:157-194 |
| M-6 | Recorder の loginStatus は、取得中の 401 では更新されない。ログイン窓が出ているのに、Viewer は「ログイン済」と表示しうる | RecorderAutoStart.ts:1108-1113, 1328-1333 |
| M-7 | Recorder は、ログイン窓を開けなかった(Chrome/Edge が無い等)ときも「ログイン窓を開きました」と通知する。手動で開いたときも「要ログイン」と出る | RecorderLoginWindow.ts:384-402 |
| M-8 | 起動時の確認と画面の get-login-status が重なると、確認が 2 回走る(recheck-login だけが single-flight) | main.ts:605, BrowserAutomationHandlers.ts:877-882 |
| M-9 | 「ログイン済み」を受け取るたび(取得の成功ごと)に role_id の保存を繰り返す | BrowserAutomationHandlers.ts:354-356, 492, 499-510 |
| M-10 | ログインの応答も含め、取得した API の応答本文を全部画面(renderer)に送っている。DB への保存だけ認証系を除いている | BrowserAutomationHandlers.ts:680-693, sensitiveResponse.ts |
| M-11 | 「保存したセッションをログインとみなす」判定が、復元しただけの窓の 60 秒ごとの保存(更新日時だけ変わる)でも成り立ちうる | ProfileHandlers.ts:50-69 |
| M-12 | 復号できないファイルは、アカウント別の保存先だと何もログに残らずに「token なし」扱いになる | tokenStore.ts:27-28 |
| M-13 | 初回起動の 10 秒ほどの間に Manager と Recorder が別々の鍵を作ると、後から暗号化されたファイルを片方が読めない可能性がある(推測) | safeStorageCipher.ts:63-69 |
軽い・整理
| # | 内容 | 根拠 |
|---|---|---|
| L-1 | login-confirmed.json は書くだけで誰も読まない。コメントは「次回起動時に headless を許可する条件」のまま | RecorderLoginWindow.ts:37-60, RecorderAutoStart.ts:1082 |
| L-2 | 取得テスト(API テスター)は共通の保存先しか見ないので、実際に取得しているアカウントと違う token を見せうる | ApiTesterHandlers.ts:49-57 |
| L-3 | 共通の保存先をアカウント別に複製した後も、元のファイルは残る(有効な token が 2 か所)。落ちたときの .tmp も掃除しない | legacyStorageMigration.ts |
| L-4 | 「ストレージは Recorder だけが持つ」というコメントが残っている(実際は Manager も書く) | recorderModeBootstrap.ts:274-275, RecorderTrayMenu.ts:13 |
| L-5 | 通信エラーのとき、Manager は roleId なしで「ログイン済み」、Recorder は roleId つきで返す | loginValidation.ts:42, RecorderAutoStart.ts:1166-1173 |
| L-6 | 15 分の見張りが切れた後、ブラウザは開いたまま 60 秒ごとの保存を続ける | browserAutomationLoginWatch.ts:18 |
| L-7 | 開発用の browser-automation-start で開いた窓は、15 分の見張りを付けず、前の回に受け入れ済みだとログインを無視する | BrowserAutomationHandlers.ts:804-837 |
| L-8 | 既定のアカウントが変わると、共通の保存先の token(別アカウントのものかもしれない)が新しい既定のアカウントへ複製されうる | legacyProfileMigrations.ts:61-66 |
token そのものがログ・テレメトリに出ている箇所は見つからなかった(ログは保存先のパス・roleId・伏せ字の token だけ、テレメトリはホストとパスだけ)。
直す順番の案
- H-2・H-8・H-3(通知と確認の重複): 起動時の確認を Recorder に聞くようにし、開いた直後 8 秒のタイムアウトでは「未ログイン」通知を出さない
- H-1・H-5(保存先の食い違いと上書き): ログイン窓を開くのを 1 か所(Manager か Recorder のどちらか)に寄せ、どのアカウントの保存先に書くかを必ず決めてから開く
- H-4・H-7: 「ファイルに token がある」をログイン済みとみなすのをやめる。role_id の保存はブラウザを閉じて token を保存した後に回す
- M・L は上の作業のついでに片付ける
8. 対応状況
2026-10-25 時点。#473〜#477 は develop にマージ済み。
| PR | 直したもの | 内容 |
|---|---|---|
| #473 | H-2・H-3・H-8・M-8 | 起動時のログイン確認を、委譲の判定と Recorder との WS がつながった後に Recorder に聞く。開いた直後 8 秒のタイムアウトでは「未ログイン」にしない。委譲中は Manager から OS 通知を出さない。確認の重なりを 1 回にまとめる |
| #474 | H-1・H-5・H-6・H-7・M-4・M-5・M-7 | ログイン窓を開くのは Recorder だけ(委譲中の Manager は login コマンドで頼む)。アカウントがあれば、指定のないログインもメインのアカウントの保存先に書く。アカウント別の保存先の 401 でそのアカウントの窓を開く。結果は live イベント login-status で Manager に届く。Recorder は token を保存してから role_id を保存する |
| #475 | H-4 | storage-state を返す前に、保存ファイルの token を battle_list で確かめる |
| #476 | M-6・M-10・M-12・L-2・L-4 | 取得中の 401 で Recorder のログイン状態を未ログインにして知らせる。認証系の応答本文を画面に送らない。読めない保存ファイルをログに残す。API テスターはメインのアカウントの保存先を見る。古いコメントを直す |
| #477 | M-1・M-2・M-9、起動直後の通知 | Manager の単一経路は、指定が無ければメインのアカウントの保存先を読む。ログインするアカウントはロックの中で切り替える。role_id の保存はログイン窓を開いた後だけ。Manager が Recorder を起動している最中の「未ログイン」も OS 通知しない |
| この PR | M-3・M-11・L-1・L-3・L-5〜L-8 | 本人確認で断ったあとも見張りを続け、同じ窓でログインし直したら拾う(断った token と同じ token の応答では確かめ直さない)。「保存したセッションでログインした」とみなす判定をやめる。login-confirmed.json を書くのをやめる。落ちたときに残った一時ファイルを起動時に消す。通信エラー時の答えをそろえる。15 分ログインが無ければ窓を閉じる。開発用の開始にも見張りを付ける。別のアカウントの token を既定のアカウントへ写さない |
設計が変わったので、上の 1〜6 章のうち次の点は今は違う:
- ログイン窓は Recorder だけが開く。Manager が自分でブラウザを開くのは、Recorder がいない(own-writer)ときだけ
- Recorder のログイン窓は、アカウント別の保存先も扱う(
loginコマンドのprofileId、省略時はメインのアカウント)。人が窓を閉じたら片付け、手動の依頼はクールダウンを無視する - Recorder のログイン確認は、アカウントがあればメインのアカウントの保存先で確かめる
まだ残っているもの:
- M-13(初回起動の数秒間に Manager と Recorder が別々の鍵を作ると、後の暗号を片方が読めない可能性): 推測で、再現できていない
- Manager が自分で Recorder を起動している 1 秒ほどの間に、画面の起動時の取得(probe-unfetched)が Manager 自身で API を 1 回叩く(通知は出さなくなった)