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

function forgetWrappedDeviceSecret<OnlyPending extends boolean = false>(
  appName: string & Brand<"UrlSafeString"> & Brand<"AppName">,
  __namedParameters?: {
    onlyPending?: OnlyPending;
  },
): Task<
  void,
  UnknownError | OnlyPending extends true
    ? WrappedDeviceSecretStoredError
    : never,
  CreateDbWorkerDep &
    CreateBroadcastChannelDep &
    CreateMessageChannelDep &
    DevicePersistenceDep &
    LockManagerDep &
    ReloadAppDep &
    Partial<ConsoleDep> &
    Partial<CreateMemoryInstanceWorkerDep> &
    Partial<FlushSyncDep> &
    Partial<RequestPersistentStorageDep> &
    StoredWorkerDep
>;

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

Forgets the app's stored WrappedDeviceSecret, or a setup's pending key, so the next unlockWrappedDeviceSecret is a first use, with another key and another DeviceSecret. It's how an app starts over when the key is gone for good, such as a deleted passkey, which the provider reports like a dismissed prompt, or after a DeviceSecretUnwrapError. Evolu never calls it on its own.

A pending key wrapped no secret, so forgetting it loses nothing, and with onlyPending, it forgets only that: when it finds a stored secret, it forgets nothing and fails with WrappedDeviceSecretStoredError. It checks under the lock setups take, so a setup in another tab can't store a secret between the check and the forget, as it can after hasWrappedDeviceSecret. An app that offers to start over calls it with onlyPending first, without a warning, and on that error asks the user to confirm before calling it without onlyPending. Forgetting a stored secret loses its device database: nothing opens it again, so its AppOwners return only from their backups, and its device tables and the AppOwners' names are gone. An AppOwner restored from its backup reopens its own database with its local data, unsynced changes included. With EvoluAppConfig.createFirstAppOwner, the new device database creates a new, empty AppOwner, so a user who restores one from a backup has two; after the restore, remove the empty one with EvoluApp.remove.

A tab that is already unlocked stays on the old device database until it reloads. AppOwners added there are lost at the next start, and it's the last place where the user can still back up the old ones.

When it finds a pending key or a stored secret, it waits for a setup running in another tab, but not for an unlock of a stored secret, whose prompt can stay open. Where the platform keeps nothing, there's nothing to forget. 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.

Example

import {
  AppName,
  assertEqual,
  forgetWrappedDeviceSecret,
  ok,
} from "@evolu/common";
import { createEvoluDeps, createRun } from "@evolu/web";

const run = createRun(createEvoluDeps());
const appName = AppName.orThrow("StartOver");

// After an unlock failed, without a warning.
const forgotten = await run(
  forgetWrappedDeviceSecret(appName, { onlyPending: true }),
);
if (
  !forgotten.ok &&
  forgotten.error.type === "WrappedDeviceSecretStoredError"
) {
  // Once the user confirms losing the device database.
  await run.orThrow(forgetWrappedDeviceSecret(appName));
}
// Nothing was stored, so there was nothing to forget.
assertEqual(forgotten, ok());

After a stored secret was forgotten:

import {
  AppName,
  assertEqual,
  assertOk,
  createDeviceSecret,
  createEvoluApp,
  createRandomBytes,
  id,
  mnemonicToOwnerSecret,
  NonEmptyTrimmedString100,
  testAppOwner,
} from "@evolu/common";
import { createEvoluDeps, createRun } from "@evolu/web";

const Schema = {
  todo: { id: id("Todo"), title: NonEmptyTrimmedString100 },
};
const run = createRun(createEvoluDeps());
// The DeviceSecret of the first unlock after forgetWrappedDeviceSecret.
const secret = createDeviceSecret({ randomBytes: createRandomBytes() });

await using app = await run.orThrow(
  createEvoluApp(Schema, {
    appName: AppName.orThrow("ForgetWrappedDeviceSecret"),
    auth: { type: "DeviceSecret", secret },
    createFirstAppOwner: true,
    transports: [],
  }),
);
// The new device database created an empty AppOwner.
const [empty] = app.appOwners.get();

// Restore the user's AppOwner from its backup, then remove the empty one.
assertOk(await app.add(mnemonicToOwnerSecret(testAppOwner.mnemonic)));
assertOk(await app.remove(empty.id));
assertEqual(
  app.appOwners.get().map(({ id }) => id),
  [testAppOwner.id],
);