Skip to content

Paykit: recover stale sessions and harden coroutine error handling #1093

Description

@jvsena42

Follow-up to #1092, which stops the crash but not its cause.

1. Stale Paykit session is never recovered

A stored Pubky session whose capabilities no longer satisfy requiredSessionCapabilities(paykitSdkConfig()) makes PaykitSdkService.initialize() throw:

code=identity_error, context=Pubky session is missing required Paykit capabilities: /pub/paykit/:rw,/pub/bitkit.to:rw

Nothing recovers from this:

  • PubkyRepo.initialize() logs the failure and returns early before the block that sets _sessionRestorationFailed, so the "session expired" toast in AppViewModel never fires. The user gets no signal and no re-auth prompt.
  • Nothing clears the stale session, so it is reloaded and rejected on every launch, forever. startAuth() already requests the correct capabilities, so a fresh authorization would succeed — the session just never gets cleared.
  • Toggling Paykit off in dev settings is not a workaround: updatePaykitEnabled(false) clears endpoints and publication state but leaves PAYKIT_SESSION/PUBKY_SECRET_KEY in the keychain.

Suggested: detect the identity error during init, clear the session access, and set _sessionRestorationFailed so the existing toast and re-auth flow trigger.

With #1092 merged, affected wallets no longer crash but their wallet backup fails permanently until this is addressed.

2. A failed init poisons every later Paykit call

PaykitSdkService.initialize() stores the failure via isSetup.completeExceptionally(t). Every method starting with isSetup.await() (currentPublicKey(), importSession(), signIn(), signUp(), identityStatus()) then rethrows that same exception for the rest of the process lifetime, since nothing re-runs initialize() after the first failure.

This is what spread a Paykit init failure into the wallet backup path.

3. No CoroutineExceptionHandler anywhere

PubkyRepo and BackupRepo both use CoroutineScope(ioDispatcher + SupervisorJob()) with no CoroutineExceptionHandler. A SupervisorJob does not handle exceptions — it only stops them propagating between siblings — so any uncaught throw inside scope.launch reaches the default handler and kills the process. There is no CoroutineExceptionHandler in the codebase at all.

Note on blast radius

The Paykit UI is gated behind a dev setting, and a session can only be created through that gated flow, so the observed trigger does not reach users who never enabled Paykit. But the crash path is not gated: BackupRepo injects PubkyRepo directly (so initialize() runs at startup for everyone) and getWalletBackupDataBytes calls backupSnapshot() unconditionally. Any Paykit init failure — corrupted state blob, native init error — would crash a normal user the same way.

Also worth checking with the Paykit SDK owners whether rc29–rc31 tightened the required capability set. If so, sessions granted before that bump break identically on other clients, which is a migration concern beyond this app.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions