API reference › @evolu/common › local‑first/Evolu › unlockWrappedDeviceSecret

function unlockWrappedDeviceSecret(__namedParameters: {
  appName: string & Brand<"UrlSafeString"> & Brand<"AppName">;
  keyProvider: DeviceSecretKeyProvider;
}): Task<
  Uint8Array<ArrayBufferLike> &
    Brand<"Entropy"> &
    Brand<"Length32"> &
    Brand<"DeviceSecret">,
  | DeviceSecretKeyError
  | DeviceSecretUnwrapError
  | WrappedDeviceSecretNotPersistedError,
  CreateDbWorkerDep &
    CreateBroadcastChannelDep &
    CreateMessageChannelDep &
    DevicePersistenceDep &
    LockManagerDep &
    ReloadAppDep &
    Partial<ConsoleDep> &
    Partial<CreateMemoryInstanceWorkerDep> &
    Partial<FlushSyncDep> &
    Partial<RequestPersistentStorageDep> &
    StoredWorkerDep
>;

Defined in: packages/common/src/local-first/Evolu.ts:2980

Unlocks the app's DeviceSecret with a key from the DeviceSecretKeyProvider, and on the first use creates them: a key, a DeviceSecret, and its WrappedDeviceSecret, which Evolu stores in a database of its own whose key is public. Pass the DeviceSecret to createEvoluApp as DeviceSecretAuth.

On the first use, it runs under a lock per app, so a second tab waits and then unlocks what the first one created. Once a wrapped secret is stored, it only unlocks, without the lock, so no tab waits for another tab's prompt, and an error, such as a dismissed prompt, never starts another setup, which would orphan the device database. A setup that stopped after creating its key resumes with that key, also after the provider refused it, so the user gets no second passkey; only when the provider can't provide it does the next setup create another one. When its database is refused, as evoluError reports, it fails with an UnknownError, the refusal itself or one wrapping it. Like createEvoluApp, it stays pending while another build of the app keeps it waiting or the shared worker fails to load. Some browsers allow a prompt only in a user gesture, so call it from one, such as a button click, and use hasWrappedDeviceSecret to tell which button to show. After an error, keep the app usable and let the user try again, rather than blocking it on the prompt.

WebAuthn reports a deleted passkey like a dismissed prompt, and a stored secret whose key is gone for good fails every unlock, so after any refusal an app can offer to start over with forgetWrappedDeviceSecret. Its onlyPending mode forgets a setup's pending key, which loses nothing and needs no warning, and fails when it finds a stored secret, whose loss the user confirms first. When unlocking still fails right after a start over, the provider likely refuses its PRF output, so stop offering to forget a stored secret, but keep offering onlyPending, which forgets only the pending key, loses nothing, and lets the user try another provider. Where the platform keeps nothing, it fails with WrappedDeviceSecretNotPersistedError before anything else.