feat(base,app,learn): a shared space's key rolls once a member goes
What is written in a shared space after a member went takes a key they
never get (Halkyon's ADR 0044, D22). The instance marks the space as one
of its members goes, removed, leaving or deleted (`keyRollDue` on
`shared_space`, set in `removeMember`, through which each of them
passes); an invitation taken back never had a key and marks nothing, nor
does a new role. The Owner's or an Editor's device that next keeps its
keys up after a sync is told so (`sharedSpaceKeys.rollDue`, never true
for a Viewer, who seals no key) and makes a new key, sealed to those who
stay (D33). The first seal bringing a key ID the space never had clears
the mark in the same transaction (D34); members the rolling device
missed get it later as newcomers do. Earlier rows keep the key they had,
which whoever went had read already.
The migration, the same in the module and in each server including it
(Learn, Meditate, Todo, Train):
ALTER TABLE "shared_space"
ADD COLUMN "keyRollDue" boolean NOT NULL DEFAULT false;
It adds the column with false for every space there is, so no space is
due a roll from it; PostgreSQL adds a column with a constant default
without rewriting the table. The server before the switch reads and
writes the table naming its own columns, so it keeps working.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
b45cb0f3cb
commit
7f1f07883a
13 changed files with 100883 additions and 6 deletions
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"__className__": "serverpod.DatabaseMigration",
|
||||
"actions": [
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationAction",
|
||||
"type": "alterTable",
|
||||
"alterTable": {
|
||||
"__className__": "serverpod.TableMigration",
|
||||
"name": "shared_space",
|
||||
"schema": "public",
|
||||
"addColumns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "keyRollDue",
|
||||
"fieldName": "keyRollDue",
|
||||
"columnType": 1,
|
||||
"isNullable": false,
|
||||
"columnDefault": "false",
|
||||
"dartType": "bool"
|
||||
}
|
||||
],
|
||||
"deleteColumns": [],
|
||||
"modifyColumns": [],
|
||||
"addIndexes": [],
|
||||
"deleteIndexes": [],
|
||||
"addForeignKeys": [],
|
||||
"deleteForeignKeys": [],
|
||||
"warnings": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"warnings": [],
|
||||
"migrationApiVersion": 1
|
||||
}
|
||||
|
|
@ -39,6 +39,7 @@ part '20261110225358224-file-store-in-halkyon-base/migration.dart';
|
|||
part '20261111091720823-shared-spaces/migration.dart';
|
||||
part '20261111100124819-shared-space-keys/migration.dart';
|
||||
part '20261111110923694-own-rows-by-id/migration.dart';
|
||||
part '20261111145608988-shared-space-key-roll/migration.dart';
|
||||
|
||||
/// Migration registry for the client-side database.
|
||||
class MigrationRegistry {
|
||||
|
|
@ -80,6 +81,7 @@ class MigrationRegistry {
|
|||
_Migration20261111091720823_shared_spaces(),
|
||||
_Migration20261111100124819_shared_space_keys(),
|
||||
_Migration20261111110923694_own_rows_by_id(),
|
||||
_Migration20261111145608988_shared_space_key_roll(),
|
||||
];
|
||||
|
||||
/// List of all client-side database migration versions.
|
||||
|
|
|
|||
|
|
@ -35,9 +35,9 @@ class SharedSpaceKeys(
|
|||
/// The key new writes in [space] take; null while none is open here.
|
||||
SpaceKeyId? latestOf(String space) => _latest[space];
|
||||
|
||||
/// Opens the keys sealed to the person in each of their shared spaces and
|
||||
/// seals those this device holds to members who lack them: at the data's
|
||||
/// opening and after each sync.
|
||||
/// Opens the keys sealed to the person in each of their shared spaces,
|
||||
/// seals those this device holds to members who lack them, and makes a
|
||||
/// new key where one is due: at the data's opening and after each sync.
|
||||
Future<void> upkeep() => _upkeep.run();
|
||||
|
||||
Future<void> _keepUp() async {
|
||||
|
|
@ -50,6 +50,12 @@ class SharedSpaceKeys(
|
|||
_learn(space, keyId);
|
||||
}
|
||||
await _keys.sealForNewcomers(space);
|
||||
// A member went: what is written from now on takes a key they
|
||||
// never get, which this device makes as one that writes (D22,
|
||||
// D33). The instance asks a Viewer's device for none.
|
||||
if (await _keys.rollDue(space)) {
|
||||
await newKey(space);
|
||||
}
|
||||
} on Object catch (error, st) {
|
||||
log(
|
||||
"Keeping a shared space's keys failed",
|
||||
|
|
|
|||
|
|
@ -115,4 +115,41 @@ void main() {
|
|||
await bob.keys.upkeep();
|
||||
expect(bob.keys.latestOf(space), key);
|
||||
});
|
||||
|
||||
test("a member going makes the next writer's upkeep roll the key, once, "
|
||||
'sealed to those who stay (D22, D33)', () async {
|
||||
server.addMember(space, 'carol', keys: keysOf(carolAccount));
|
||||
final alice = await device('alice', aliceAccount);
|
||||
final bob = await device('bob', bobAccount);
|
||||
final carol = await device('carol', carolAccount);
|
||||
final first = await alice.keys.newKey(space);
|
||||
await carol.keys.upkeep();
|
||||
expect(carol.keys.latestOf(space), first);
|
||||
|
||||
server.removeMember(space, 'carol');
|
||||
await bob.keys.upkeep();
|
||||
final rolled = bob.keys.latestOf(space)!;
|
||||
expect(rolled, isNot(first));
|
||||
expect(rolled.madeAt, greaterThan(first.madeAt));
|
||||
expect(carol.cipher.has(rolled.value), isFalse);
|
||||
expect(await server.as('bob').rollDue(space), isFalse);
|
||||
|
||||
await alice.keys.upkeep();
|
||||
expect(alice.keys.latestOf(space), rolled, reason: 'rolled once');
|
||||
});
|
||||
|
||||
test("a Viewer's device never rolls the key", () async {
|
||||
server
|
||||
..addMember(space, 'carol', keys: keysOf(carolAccount), writes: false)
|
||||
..addMember(space, 'dave', keys: keysOf(crypto.keysNumbered(4)));
|
||||
final alice = await device('alice', aliceAccount);
|
||||
final carol = await device('carol', carolAccount);
|
||||
final first = await alice.keys.newKey(space);
|
||||
await carol.keys.upkeep();
|
||||
|
||||
server.removeMember(space, 'dave');
|
||||
await carol.keys.upkeep();
|
||||
expect(carol.keys.latestOf(space), first);
|
||||
expect(await server.as('alice').rollDue(space), isTrue);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"__className__": "serverpod.DatabaseMigration",
|
||||
"actions": [
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationAction",
|
||||
"type": "alterTable",
|
||||
"alterTable": {
|
||||
"__className__": "serverpod.TableMigration",
|
||||
"name": "shared_space",
|
||||
"schema": "public",
|
||||
"addColumns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "keyRollDue",
|
||||
"fieldName": "keyRollDue",
|
||||
"columnType": 1,
|
||||
"isNullable": false,
|
||||
"columnDefault": "false",
|
||||
"dartType": "bool"
|
||||
}
|
||||
],
|
||||
"deleteColumns": [],
|
||||
"modifyColumns": [],
|
||||
"addIndexes": [],
|
||||
"deleteIndexes": [],
|
||||
"addForeignKeys": [],
|
||||
"deleteForeignKeys": [],
|
||||
"warnings": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"warnings": [],
|
||||
"migrationApiVersion": 1
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
BEGIN;
|
||||
|
||||
--
|
||||
-- ACTION ALTER TABLE
|
||||
--
|
||||
ALTER TABLE "shared_space" ADD COLUMN "keyRollDue" boolean NOT NULL DEFAULT false;
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR halkyon_learn
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('halkyon_learn', '20261111145608988-shared-space-key-roll', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20261111145608988-shared-space-key-roll', "timestamp" = now();
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR halkyon_base
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('halkyon_base', '20261111145608468-shared-space-key-roll', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20261111145608468-shared-space-key-roll', "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 halkyon_oidc
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('halkyon_oidc', '20261023180644988', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20261023180644988', "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;
|
||||
|
|
@ -56,3 +56,4 @@
|
|||
20261111091720823-shared-spaces
|
||||
20261111100124819-shared-space-keys
|
||||
20261111110923694-own-rows-by-id
|
||||
20261111145608988-shared-space-key-roll
|
||||
|
|
|
|||
Loading…
Reference in a new issue