mihomo-party-org/clash-party · error · VaultUnavailableError

Plugin vault is temporarily unavailable

Error message

Plugin vault is temporarily unavailable

What it means

runLogin reads the plugin's credential vault before deciding whether to reuse an existing device. If readVault returns kind 'unavailable' (e.g. the OS keychain/safeStorage backend cannot be accessed), VaultUnavailableError with message 'Plugin vault is temporarily unavailable' is thrown. Login is intentionally aborted rather than proceeding without credentials.

Source

Thrown at src/main/resolve/plugin/index.ts:152

export async function loginPlugin(id: string): Promise<void> {
  try {
    await runLogin(id)
  } catch (e) {
    throw sanitizeLoginError(e)
  }
}

// 设备复用仅限「needs-login 且已有 vault」这一种情形:上次 enroll 成功但首份订阅拉取失败留下的
// “孤儿设备”,重拉即可,避免每次重试都 enroll 新设备、消耗服务端设备数上限。
// 其它情形——needs-reauth(显式重新登录)、active(刷新)、无 vault(首装/换机/Linux 无 safeStorage)——
// 一律走全新浏览器登录 + 新设备,与 spec §9「reauth = 再走一次 login 流程、新设备密钥」一致。
async function runLogin(id: string): Promise<void> {
  const record = await getPluginItem(id)
  if (!record) throw new Error('Plugin not found')
  const net = await netOpts(record)

  const existingResult = await readVault(id)
  if (existingResult.kind === 'unavailable') throw new VaultUnavailableError()
  const existing = existingResult.kind === 'ok' ? existingResult.vault : undefined
  if (existing && record.status === 'needs-login') {
    try {
      const content = await fetchWithRediscovery(id, record, existing, net)
      await finishLogin(id, record, content)
      return
    } catch (e) {
      // 孤儿设备已被吊销 → 丢弃旧 vault,落到下面的全新浏览器登录 + 新设备
      if (!(e instanceof GatewayError && e.kind === 'revoked')) throw e
      await removeVault(id)
    }
  }

  // 先确认 Keychain/secret store 可以实际加密,再打开 OAuth 和 enroll,避免用户完成
  // 浏览器登录后才发现私钥无法持久化。旧 Electron 兼容包会在这里走同步探测。
  await ensureVaultWritable()

  const wk = await discoverGateway(record.loginUrl, net)

View on GitHub (pinned to 911e090537)

Solutions

  1. Ensure a keyring/secret service is running (on Linux install/start gnome-keyring or kwallet) and unlock it, then retry.
  2. Retry after the keychain is unlocked — the error is explicitly 'temporarily unavailable'.
  3. Verify Electron safeStorage is usable in the current environment before initiating login.
  4. If the platform genuinely has no safeStorage, expect the full browser-login path (spec §9) rather than vault reuse.

Example fix

// before
await loginPlugin(id) // fails while keychain is locked
// after
try {
  await loginPlugin(id)
} catch (e) {
  if (e instanceof VaultUnavailableError) {
    await promptUnlockKeychain()
    await loginPlugin(id) // retry once unlocked
  } else throw e
}
Defensive patterns

Strategy: retry

Validate before calling

// probe vault availability before login
const probe = await readVault(id)
if (probe.kind === 'unavailable') throw new Error('unlock the system keychain before logging in')

Try / catch

try {
  await loginPlugin(id)
} catch (e) {
  if (e instanceof VaultUnavailableError) {
    await promptUnlockKeychain()
    await loginPlugin(id) // bounded retry after unlock
  } else throw e
}

Prevention

When it happens

Trigger: Calling loginPlugin when the system credential store is locked or unreachable — Linux without a working safeStorage/keyring backend, keychain locked after reboot, or the vault service temporarily down.

Common situations: Running on Linux in a headless/CI session with no secret-service (gnome-keyring/kwallet) available; macOS Keychain access denied; keychain still locked at login time.

Related errors


AI-assisted analysis of mihomo-party-org/clash-party@911e090537 (2026-08-30). Data as JSON: /api/errors/3182de464dcd90fd. Report an issue: GitHub.