refactor!: packages, paths and app IDs under Halkyon Days' names (ADR 0045, step 3)
Learn's folder is apps/halkyon, Todo's apps/myrmex; every package of
ours takes the prefix halkyone_ (halkyone_halkyon_*, halkyone_myrmex_*,
halkyone_train_*, halkyone_meditate_*, halkyone_app, halkyone_base_* and
the other shared ones; halkyon_oidc_* keeps its name, D11). Halkyon and
Myrmex take the app IDs one.halky.halkyon and one.halky.myrmex (D10);
Train and Meditate keep theirs, while every app's Android namespace,
Kotlin package and method channels move to one.halky. The snaps are
halkyon and myrmex (D17), each built in a clone with its recipe in
place of snap/, as Train's and Todo's were.
Serverpod finds a database's migrations by its module's name, which
changed with the packages':
- each server's new, empty migration ("rename") records itself under
the new names and deletes the rows under the old ones;
- deploy/redeploy.sh copies each database's record to the new name
before the migrations (carry_module), so the renamed servers find
their databases migrated; a copy, never a move, so a deploy failing
before the migration commits leaves the old rows in place;
- Train and Meditate keep their devices' databases: halkyone_app's
carryMigrationRecord copies their record before the sync engine
opens them (openPersonDatabase);
- the client migrations' generated moduleName getters, one line each,
say the new names: Serverpod's client asserts in debug builds that
every migration names the module, and create-migration never writes
a migration's files again.
Train's WorkManager work is enqueued under new names, the former ones
cancelled: the work kept names the workers' former classes.
The release tool: an app's tags of its earlier names count as its
history (legacyTagPrefixes), the shared packages' former paths count
for its version, and every snap is built in a clone (atRoot gone).
The databases, Docker images and Compose project keep their names
until step 5.
BREAKING CHANGE: Halkyon (Learn) and Myrmex (Todo) are new apps to
Android and the Snap Store: installs of app.halkyon.learn and
app.halkyon.todo don't update to them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
016f9b7e26
commit
b4290f71c8
1 changed files with 118206 additions and 13301 deletions
|
|
@ -1,186 +0,0 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_content/halkyon_content.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
/// Content sealed with a data key (ADR 0019), with libsodium as its build
|
||||
/// hook compiles it.
|
||||
void main() {
|
||||
late UnlockedAccountKeys alice;
|
||||
late UnlockedAccountKeys bob;
|
||||
late SodiumContentCipher device;
|
||||
|
||||
final context = utf8.encode('card_revision/0192/payload');
|
||||
final plain = Uint8List.fromList(utf8.encode('{"front":"火"}'));
|
||||
|
||||
setUpAll(() async {
|
||||
alice = await newAccountKeys();
|
||||
bob = await newAccountKeys();
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
device = SodiumContentCipher();
|
||||
await device.unlock(alice);
|
||||
});
|
||||
|
||||
tearDown(() => device.dispose());
|
||||
|
||||
test("a key made on one device opens on another of the account's, and "
|
||||
'what it seals opens there', () async {
|
||||
final sealedKey = device.newKey('studyset-1');
|
||||
final sealed = device.seal(plain, keyId: 'studyset-1', context: context);
|
||||
|
||||
final other = SodiumContentCipher();
|
||||
await other.unlock(alice);
|
||||
addTearDown(other.dispose);
|
||||
expect(other.has('studyset-1'), isFalse);
|
||||
expect(other.openKey('studyset-1', sealedKey), isTrue);
|
||||
|
||||
expect(other.has('studyset-1'), isTrue);
|
||||
expect(other.unseal(sealed, context: context), plain);
|
||||
expect(sealed.keyId, 'studyset-1');
|
||||
expect(String.fromCharCodes(sealed.payload), isNot(contains('front')));
|
||||
});
|
||||
|
||||
test("another account's device opens neither the key nor what it "
|
||||
'sealed', () async {
|
||||
final sealedKey = device.newKey('studyset-1');
|
||||
final sealed = device.seal(plain, keyId: 'studyset-1', context: context);
|
||||
|
||||
final bobs = SodiumContentCipher();
|
||||
await bobs.unlock(bob);
|
||||
addTearDown(bobs.dispose);
|
||||
|
||||
expect(bobs.openKey('studyset-1', sealedKey), isFalse);
|
||||
expect(bobs.unseal(sealed, context: context), isNull);
|
||||
});
|
||||
|
||||
test('a key opens only under the ID it was sealed with, and only once '
|
||||
'the account is unlocked', () async {
|
||||
final sealedKey = device.newKey('studyset-1');
|
||||
final other = SodiumContentCipher();
|
||||
addTearDown(other.dispose);
|
||||
|
||||
expect(() => other.openKey('studyset-1', sealedKey), throwsStateError);
|
||||
await other.unlock(alice);
|
||||
expect(other.openKey('studyset-2', sealedKey), isFalse);
|
||||
expect(other.openKey('studyset-1', sealedKey), isTrue);
|
||||
});
|
||||
|
||||
test('a payload moved to another row, or changed, opens no more', () {
|
||||
device.newKey('studyset-1');
|
||||
final sealed = device.seal(plain, keyId: 'studyset-1', context: context);
|
||||
final changed = Uint8List.fromList(sealed.payload)..last ^= 1;
|
||||
|
||||
expect(
|
||||
device.unseal(sealed, context: utf8.encode('card_revision/0193/payload')),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
device.unseal(
|
||||
SealedPayload(
|
||||
keyId: sealed.keyId,
|
||||
nonce: sealed.nonce,
|
||||
payload: changed,
|
||||
),
|
||||
context: context,
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
expect(device.unseal(sealed, context: context), plain);
|
||||
});
|
||||
|
||||
test(
|
||||
"a row's keyed ID (ADR 0030, D57) is the same on every device of the "
|
||||
"account, another on another account's, and none before unlocking",
|
||||
() async {
|
||||
final other = SodiumContentCipher();
|
||||
await other.unlock(alice);
|
||||
addTearDown(other.dispose);
|
||||
final bobs = SodiumContentCipher();
|
||||
await bobs.unlock(bob);
|
||||
addTearDown(bobs.dispose);
|
||||
final locked = SodiumContentCipher();
|
||||
|
||||
final id = device.rowId('train/day/2026-10-30');
|
||||
expect(other.rowId('train/day/2026-10-30'), id);
|
||||
expect(bobs.rowId('train/day/2026-10-30'), isNot(id));
|
||||
expect(device.rowId('train/day/2026-10-31'), isNot(id));
|
||||
expect(() => locked.rowId('train/day/2026-10-30'), throwsStateError);
|
||||
},
|
||||
);
|
||||
|
||||
test('without its key nothing opens, nor is sealed', () {
|
||||
final sealed = SealedPayload(
|
||||
keyId: 'unknown',
|
||||
nonce: Uint8List(24),
|
||||
payload: Uint8List(100),
|
||||
);
|
||||
|
||||
expect(device.unseal(sealed, context: context), isNull);
|
||||
expect(
|
||||
() => device.seal(plain, keyId: 'unknown', context: context),
|
||||
throwsStateError,
|
||||
);
|
||||
});
|
||||
|
||||
test("a shared key's content opens when a member it trusts signed it, "
|
||||
"and only for that key (Halkyon's ADR 0044, D20)", () async {
|
||||
final carol = await newAccountKeys();
|
||||
PublicKeys keysOf(UnlockedAccountKeys account) => (
|
||||
encryption: account.encryptionPublicKey,
|
||||
signing: account.signingPublicKey,
|
||||
);
|
||||
Future<SodiumContentCipher> deviceOf(UnlockedAccountKeys account) async {
|
||||
final cipher = SodiumContentCipher();
|
||||
await cipher.unlock(account);
|
||||
addTearDown(cipher.dispose);
|
||||
return cipher;
|
||||
}
|
||||
|
||||
final bobs = await deviceOf(bob);
|
||||
final carols = await deviceOf(carol);
|
||||
for (final keyId in ['shared', 'other']) {
|
||||
device.newKey(keyId);
|
||||
for (final (cipher, keys) in [(bobs, bob), (carols, carol)]) {
|
||||
expect(
|
||||
cipher.openSharedKey(
|
||||
keyId,
|
||||
device.sealKeyTo(keyId, keysOf(keys)),
|
||||
sealedBy: alice.signingPublicKey,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
}
|
||||
}
|
||||
final byBob = bobs.seal(plain, keyId: 'shared', context: context);
|
||||
final byCarol = carols.seal(plain, keyId: 'shared', context: context);
|
||||
final byAlice = device.seal(plain, keyId: 'shared', context: context);
|
||||
|
||||
// Only the account's own, until told whom to trust.
|
||||
expect(device.unseal(byBob, context: context), isNull);
|
||||
expect(device.unseal(byAlice, context: context), plain);
|
||||
|
||||
device.trustSigners('shared', [
|
||||
alice.signingPublicKey,
|
||||
bob.signingPublicKey,
|
||||
]);
|
||||
expect(device.unseal(byBob, context: context), plain);
|
||||
expect(device.unseal(byAlice, context: context), plain);
|
||||
expect(device.unseal(byCarol, context: context), isNull);
|
||||
expect(
|
||||
device.unseal(
|
||||
bobs.seal(plain, keyId: 'other', context: context),
|
||||
context: context,
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
|
||||
// What it trusts is replaced, not added to.
|
||||
device.trustSigners('shared', [carol.signingPublicKey]);
|
||||
expect(device.unseal(byCarol, context: context), plain);
|
||||
expect(device.unseal(byBob, context: context), isNull);
|
||||
expect(device.unseal(byAlice, context: context), isNull);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue