tane/apps/app_seeds/lib/services/device_id_store.dart
vjrj d0dbed9bc2 feat(sync): device-to-device inventory sync, end to end
Wires the encrypted sync transport into a working replication between one
identity's own devices, reusing the CRDT-ready model (HLC + tombstones +
LWW) the data model was built for.

- SocialSession gains a 5th interface, sync, over the shared connection,
  built with a per-install deviceId + the app's inventory namespace.
- DeviceIdStore: a stable random per-install id in the keystore — distinct
  from the seed-derived CRDT node id, which is IDENTICAL across a user's
  devices (so it couldn't tell them apart / they'd clobber each other's
  replaceable record).
- InventorySnapshotIO (implemented by ExportImportService): build/apply the
  inventory as the SAME interchange JSON as backups but UNSEALED — the sync
  transport does the encryption, so it must not double-seal. Apply = the
  existing idempotent LWW importInventory.
- SyncService: on (re)connect publishes this device's snapshot + subscribes
  to the others'; a local edit (debounced, off tableUpdates) republishes;
  incoming snapshots merge idempotently; skips its own echo; a byte check
  skips re-publishing an unchanged snapshot so the ping-pong converges.
  Foreground only, like the inbox.
- DI/Bootstrap/switchSocialAccount wire and lifecycle it per identity.

Tests: DeviceIdStore, ExportImportService snapshot round-trip (unsealed +
idempotent), SyncService.handleRemote (applies others', skips own).

Follow-ups: deltas vs full-snapshot pushes; background sync; conflict UI.
2026-07-10 23:45:40 +02:00

34 lines
1.2 KiB
Dart

import 'dart:math';
import '../security/secret_store.dart';
/// A stable per-INSTALL device id, kept in the keystore. Distinct from the
/// seed-derived CRDT node id (`rootSeedHex.substring(0,16)`), which is IDENTICAL
/// across a user's devices — so it can't tell them apart. Device-to-device sync
/// needs each device to own a separate record, hence a random per-install id.
class DeviceIdStore {
DeviceIdStore(this._store, {Random? random}) : _random = random ?? Random.secure();
final SecretStore _store;
final Random _random;
static const _key = 'tane.device.id';
Future<String>? _cached;
/// This install's device id, generated and persisted on first use.
Future<String> deviceId() => _cached ??= _readOrCreate();
Future<String> _readOrCreate() async {
final existing = await _store.read(_key);
if (existing != null && existing.isNotEmpty) return existing;
final id = _randomHex(8); // 64 bits — plenty to avoid device collisions
await _store.write(_key, id);
return id;
}
String _randomHex(int bytes) => [
for (var i = 0; i < bytes; i++)
_random.nextInt(256).toRadixString(16).padLeft(2, '0'),
].join();
}