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],
);