fix(auth): a hold-out moves a reset on only by its own time, and a reset lives 30 days at most

The reading of the full reading's fixes found that every refusal at the
address's ceiling moved a reset to 72 hours after it (A1): someone who
knows only the address kept the owner's own reset from ever taking effect
with one burst every three days, and kept a forgotten one alive (A2).

Now a hold-out that begins before a reset takes effect moves it on only by
the time it holds the owner out and no hold-out counted before did
(account_reset.heldOutUntil, under a row lock), so the owner gets 72 hours
outside hold-outs in total; and every reset not carried out lapses 30 days
after it was asked (resetLivesAtMost), whatever moved it, besides a week
after it took effect.

Migration (module, Learn's and Todo's servers, Learn's device schema):
ALTER TABLE "account_reset" ADD COLUMN "heldOutUntil" timestamp without
time zone; nullable, existing rows counted from now on.

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 15:50:29 +02:00
commit 274b796faa
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
28 changed files with 41748 additions and 65 deletions

View file

@ -0,0 +1,33 @@
{
"__className__": "serverpod.DatabaseMigration",
"actions": [
{
"__className__": "serverpod.DatabaseMigrationAction",
"type": "alterTable",
"alterTable": {
"__className__": "serverpod.TableMigration",
"name": "account_reset",
"schema": "public",
"addColumns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "heldOutUntil",
"fieldName": "heldOutUntil",
"columnType": 4,
"isNullable": true,
"dartType": "DateTime?"
}
],
"deleteColumns": [],
"modifyColumns": [],
"addIndexes": [],
"deleteIndexes": [],
"addForeignKeys": [],
"deleteForeignKeys": [],
"warnings": []
}
}
],
"warnings": [],
"migrationApiVersion": 1
}

View file

@ -26,6 +26,7 @@ part '20261020204619104-session-code-tries/migration.dart';
part '20261020211224692-email-code-purpose/migration.dart'; part '20261020211224692-email-code-purpose/migration.dart';
part '20261021100422048-session-ended-warning/migration.dart'; part '20261021100422048-session-ended-warning/migration.dart';
part '20261021110159867-second-factor-paths/migration.dart'; part '20261021110159867-second-factor-paths/migration.dart';
part '20261021134803749-reset-hold-outs/migration.dart';
/// Migration registry for the client-side database. /// Migration registry for the client-side database.
class MigrationRegistry { class MigrationRegistry {
@ -54,6 +55,7 @@ class MigrationRegistry {
_Migration20261020211224692_email_code_purpose(), _Migration20261020211224692_email_code_purpose(),
_Migration20261021100422048_session_ended_warning(), _Migration20261021100422048_session_ended_warning(),
_Migration20261021110159867_second_factor_paths(), _Migration20261021110159867_second_factor_paths(),
_Migration20261021134803749_reset_hold_outs(),
]; ];
/// List of all client-side database migration versions. /// List of all client-side database migration versions.

View file

@ -0,0 +1,33 @@
{
"__className__": "serverpod.DatabaseMigration",
"actions": [
{
"__className__": "serverpod.DatabaseMigrationAction",
"type": "alterTable",
"alterTable": {
"__className__": "serverpod.TableMigration",
"name": "account_reset",
"schema": "public",
"addColumns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "heldOutUntil",
"fieldName": "heldOutUntil",
"columnType": 4,
"isNullable": true,
"dartType": "DateTime?"
}
],
"deleteColumns": [],
"modifyColumns": [],
"addIndexes": [],
"deleteIndexes": [],
"addForeignKeys": [],
"deleteForeignKeys": [],
"warnings": []
}
}
],
"warnings": [],
"migrationApiVersion": 1
}

View file

@ -0,0 +1,57 @@
BEGIN;
--
-- ACTION ALTER TABLE
--
ALTER TABLE "account_reset" ADD COLUMN "heldOutUntil" timestamp without time zone;
--
-- MIGRATION VERSION FOR halkyon_learn
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('halkyon_learn', '20261021134803749-reset-hold-outs', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20261021134803749-reset-hold-outs', "timestamp" = now();
--
-- MIGRATION VERSION FOR halkyon_base
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('halkyon_base', '20261021134802955-reset-hold-outs', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20261021134802955-reset-hold-outs', "timestamp" = now();
--
-- MIGRATION VERSION FOR serverpod
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('serverpod', '20260824182259319', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20260824182259319', "timestamp" = now();
--
-- MIGRATION VERSION FOR serverpod_auth_idp
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('serverpod_auth_idp', '20260924105404509', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20260924105404509', "timestamp" = now();
--
-- MIGRATION VERSION FOR serverpod_offline_sync
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('serverpod_offline_sync', '20260914143806119', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20260914143806119', "timestamp" = now();
--
-- MIGRATION VERSION FOR serverpod_auth_core
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('serverpod_auth_core', '20260924105232991', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20260924105232991', "timestamp" = now();
COMMIT;

View file

@ -43,3 +43,4 @@
20261020211224692-email-code-purpose 20261020211224692-email-code-purpose
20261021100422048-session-ended-warning 20261021100422048-session-ended-warning
20261021110159867-second-factor-paths 20261021110159867-second-factor-paths
20261021134803749-reset-hold-outs

View file

@ -0,0 +1,29 @@
{
"__className__": "serverpod.DatabaseDefinition",
"schemaVersion": 2,
"moduleName": "halkyon_todo",
"tables": [],
"installedModules": [
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "halkyon_base",
"version": "20261021134802955-reset-hold-outs"
},
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "serverpod",
"version": "20260824182259319"
},
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "serverpod_auth_idp",
"version": "20260924105404509"
},
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "serverpod_auth_core",
"version": "20260924105232991"
}
],
"migrationApiVersion": 1
}

View file

@ -0,0 +1,33 @@
{
"__className__": "serverpod.DatabaseMigration",
"actions": [
{
"__className__": "serverpod.DatabaseMigrationAction",
"type": "alterTable",
"alterTable": {
"__className__": "serverpod.TableMigration",
"name": "account_reset",
"schema": "public",
"addColumns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "heldOutUntil",
"fieldName": "heldOutUntil",
"columnType": 4,
"isNullable": true,
"dartType": "DateTime?"
}
],
"deleteColumns": [],
"modifyColumns": [],
"addIndexes": [],
"deleteIndexes": [],
"addForeignKeys": [],
"deleteForeignKeys": [],
"warnings": []
}
}
],
"warnings": [],
"migrationApiVersion": 1
}

View file

@ -0,0 +1,49 @@
BEGIN;
--
-- ACTION ALTER TABLE
--
ALTER TABLE "account_reset" ADD COLUMN "heldOutUntil" timestamp without time zone;
--
-- MIGRATION VERSION FOR halkyon_todo
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('halkyon_todo', '20261021134804476-reset-hold-outs', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20261021134804476-reset-hold-outs', "timestamp" = now();
--
-- MIGRATION VERSION FOR halkyon_base
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('halkyon_base', '20261021134802955-reset-hold-outs', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20261021134802955-reset-hold-outs', "timestamp" = now();
--
-- MIGRATION VERSION FOR serverpod
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('serverpod', '20260824182259319', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20260824182259319', "timestamp" = now();
--
-- MIGRATION VERSION FOR serverpod_auth_idp
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('serverpod_auth_idp', '20260924105404509', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20260924105404509', "timestamp" = now();
--
-- MIGRATION VERSION FOR serverpod_auth_core
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('serverpod_auth_core', '20260924105232991', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20260924105232991', "timestamp" = now();
COMMIT;

View file

@ -15,3 +15,4 @@
20261020211225101-email-code-purpose 20261020211225101-email-code-purpose
20261021100422779-session-ended-warning 20261021100422779-session-ended-warning
20261021110200318-second-factor-paths 20261021110200318-second-factor-paths
20261021134804476-reset-hold-outs

View file

@ -23,6 +23,7 @@ abstract class AccountReset
required this.authUserId, required this.authUserId,
required this.kind, required this.kind,
required this.takesEffectAt, required this.takesEffectAt,
this.heldOutUntil,
required this.createdAt, required this.createdAt,
}); });
@ -31,6 +32,7 @@ abstract class AccountReset
required _is.UuidValue authUserId, required _is.UuidValue authUserId,
required _itrk9fro.AccountResetKind kind, required _itrk9fro.AccountResetKind kind,
required DateTime takesEffectAt, required DateTime takesEffectAt,
DateTime? heldOutUntil,
required DateTime createdAt, required DateTime createdAt,
}) = _AccountResetImpl; }) = _AccountResetImpl;
@ -46,6 +48,11 @@ abstract class AccountReset
takesEffectAt: _is.DateTimeJsonExtension.fromJson( takesEffectAt: _is.DateTimeJsonExtension.fromJson(
jsonSerialization['takesEffectAt'], jsonSerialization['takesEffectAt'],
), ),
heldOutUntil: jsonSerialization['heldOutUntil'] == null
? null
: _is.DateTimeJsonExtension.fromJson(
jsonSerialization['heldOutUntil'],
),
createdAt: _is.DateTimeJsonExtension.fromJson( createdAt: _is.DateTimeJsonExtension.fromJson(
jsonSerialization['createdAt'], jsonSerialization['createdAt'],
), ),
@ -68,6 +75,10 @@ abstract class AccountReset
/// When it may be carried out. /// When it may be carried out.
DateTime takesEffectAt; DateTime takesEffectAt;
/// Until when its owner's hold-outs were counted (`pushOutResets`), so
/// a hold-out moves it on only by the time no earlier one covered.
DateTime? heldOutUntil;
DateTime createdAt; DateTime createdAt;
@override @override
@ -81,6 +92,7 @@ abstract class AccountReset
_is.UuidValue? authUserId, _is.UuidValue? authUserId,
_itrk9fro.AccountResetKind? kind, _itrk9fro.AccountResetKind? kind,
DateTime? takesEffectAt, DateTime? takesEffectAt,
DateTime? heldOutUntil,
DateTime? createdAt, DateTime? createdAt,
}); });
@override @override
@ -91,6 +103,7 @@ abstract class AccountReset
'authUserId': authUserId.toJson(), 'authUserId': authUserId.toJson(),
'kind': kind.toJson(), 'kind': kind.toJson(),
'takesEffectAt': takesEffectAt.toJson(), 'takesEffectAt': takesEffectAt.toJson(),
if (heldOutUntil != null) 'heldOutUntil': heldOutUntil?.toJson(),
'createdAt': createdAt.toJson(), 'createdAt': createdAt.toJson(),
}; };
} }
@ -136,12 +149,14 @@ class _AccountResetImpl extends AccountReset {
required _is.UuidValue authUserId, required _is.UuidValue authUserId,
required _itrk9fro.AccountResetKind kind, required _itrk9fro.AccountResetKind kind,
required DateTime takesEffectAt, required DateTime takesEffectAt,
DateTime? heldOutUntil,
required DateTime createdAt, required DateTime createdAt,
}) : super._( }) : super._(
id: id, id: id,
authUserId: authUserId, authUserId: authUserId,
kind: kind, kind: kind,
takesEffectAt: takesEffectAt, takesEffectAt: takesEffectAt,
heldOutUntil: heldOutUntil,
createdAt: createdAt, createdAt: createdAt,
); );
@ -154,6 +169,7 @@ class _AccountResetImpl extends AccountReset {
_is.UuidValue? authUserId, _is.UuidValue? authUserId,
_itrk9fro.AccountResetKind? kind, _itrk9fro.AccountResetKind? kind,
DateTime? takesEffectAt, DateTime? takesEffectAt,
Object? heldOutUntil = _Undefined,
DateTime? createdAt, DateTime? createdAt,
}) { }) {
return AccountReset( return AccountReset(
@ -161,6 +177,9 @@ class _AccountResetImpl extends AccountReset {
authUserId: authUserId ?? this.authUserId, authUserId: authUserId ?? this.authUserId,
kind: kind ?? this.kind, kind: kind ?? this.kind,
takesEffectAt: takesEffectAt ?? this.takesEffectAt, takesEffectAt: takesEffectAt ?? this.takesEffectAt,
heldOutUntil: heldOutUntil is DateTime?
? heldOutUntil
: this.heldOutUntil,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
); );
} }
@ -189,6 +208,12 @@ class AccountResetUpdateTable extends _is.UpdateTable<AccountResetTable> {
value, value,
); );
_is.ColumnValue<DateTime, DateTime> heldOutUntil(DateTime? value) =>
_is.ColumnValue(
table.heldOutUntil,
value,
);
_is.ColumnValue<DateTime, DateTime> createdAt(DateTime value) => _is.ColumnValue<DateTime, DateTime> createdAt(DateTime value) =>
_is.ColumnValue( _is.ColumnValue(
table.createdAt, table.createdAt,
@ -212,6 +237,10 @@ class AccountResetTable extends _is.Table<int?> {
'takesEffectAt', 'takesEffectAt',
this, this,
); );
heldOutUntil = _is.ColumnDateTime(
'heldOutUntil',
this,
);
createdAt = _is.ColumnDateTime( createdAt = _is.ColumnDateTime(
'createdAt', 'createdAt',
this, this,
@ -229,6 +258,10 @@ class AccountResetTable extends _is.Table<int?> {
/// When it may be carried out. /// When it may be carried out.
late final _is.ColumnDateTime takesEffectAt; late final _is.ColumnDateTime takesEffectAt;
/// Until when its owner's hold-outs were counted (`pushOutResets`), so
/// a hold-out moves it on only by the time no earlier one covered.
late final _is.ColumnDateTime heldOutUntil;
late final _is.ColumnDateTime createdAt; late final _is.ColumnDateTime createdAt;
@override @override
@ -237,6 +270,7 @@ class AccountResetTable extends _is.Table<int?> {
authUserId, authUserId,
kind, kind,
takesEffectAt, takesEffectAt,
heldOutUntil,
createdAt, createdAt,
]; ];
} }

View file

@ -337,6 +337,12 @@ class Protocol extends _is.DatabaseSerializationManager {
isNullable: false, isNullable: false,
dartType: 'DateTime', dartType: 'DateTime',
), ),
_isp.ColumnDefinition(
name: 'heldOutUntil',
columnType: _isp.ColumnType.timestampWithoutTimeZone,
isNullable: true,
dartType: 'DateTime?',
),
_isp.ColumnDefinition( _isp.ColumnDefinition(
name: 'createdAt', name: 'createdAt',
columnType: _isp.ColumnType.timestampWithoutTimeZone, columnType: _isp.ColumnType.timestampWithoutTimeZone,

View file

@ -11,6 +11,9 @@ fields:
kind: AccountResetKind kind: AccountResetKind
### When it may be carried out. ### When it may be carried out.
takesEffectAt: DateTime takesEffectAt: DateTime
### Until when its owner's hold-outs were counted (`pushOutResets`), so
### a hold-out moves it on only by the time no earlier one covered.
heldOutUntil: DateTime?
createdAt: DateTime createdAt: DateTime
indexes: indexes:
### One of each kind per account. ### One of each kind per account.

Some files were not shown because too many files have changed in this diff Show more