feat(auth): a session ended for its wrong proofs warns its owner

The tenth wrong owner proof in a row ends a session (the third
reading); someone holding that session tried the password or its
codes. The register now keeps the moment with the account's password
(`account_password.sessionEndedForProofsAt`): the account's sign-in
ways and its next password sign-in carry it, until the owner dismisses
it (`password.dismissSessionEndedWarning`, their session alone: it
hides a warning and secures nothing) or sets a new password. A member
ending its own session tells the register
(`noteSessionEndedForProofs`); a register out of reach leaves the
owner unwarned, the session ended all the same.

Migration (module, Learn's and Todo's servers, Learn's device schema):
ALTER TABLE "account_password" ADD COLUMN "sessionEndedForProofsAt"
timestamp without time zone; nullable, so every row keeps its values
and has no warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-21 12:05:18 +02:00
commit 5e7d7f4e36
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -562,6 +562,18 @@ void main() {
);
final ways = await remote.signInWaysOf(session, annaOnTodo);
expect(ways.password && ways.authenticator, isTrue);
expect(ways.sessionEndedForProofsAt, isNull);
// A member's session ended for its wrong proofs: the owner's
// warning, kept here, read and dismissed through the member.
await remote.noteSessionEndedForProofs(session, annaOnTodo);
final warned = now;
expect(
(await remote.signInWaysOf(
session,
annaOnTodo,
)).sessionEndedForProofsAt,
warned,
);
now = now.add(totpPeriod);
final challenge = await remote.beginPasswordSignIn(
@ -583,6 +595,15 @@ void main() {
expect(proof.account, annaOnTodo);
expect(answer.verifiesServer(proof.serverProof), isTrue);
expect(proof.passwordKeys.toJson(), device.passwordKeys.toJson());
expect(proof.sessionEndedForProofsAt, warned);
await remote.dismissSessionEndedWarning(session, annaOnTodo);
expect(
(await remote.signInWaysOf(
session,
annaOnTodo,
)).sessionEndedForProofsAt,
isNull,
);
final again = await remote.beginPasswordSignIn(
session,
address: 'anna@example.org',