feat(#174): promote name-only block to pubkey (advert/DM) + 30d age-out

BlockService.maybePromote(name,key): when a name-only block links to a pubkey,
add the key to blockedKeys and drop the name entry. Connector calls it on every
contact advert AND incoming DM (the two ways we learn a name<->key link).
_pruneExpiredNames on load drops name-only blocks older than 30 days.

Epic A #165 / A7 #174. Design: docs/architecture/block-contract-as-built.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pull/209/head
Strycher 1 week ago
parent c3a47559f2
commit ff255d366f

@ -4797,6 +4797,12 @@ class MeshCoreConnector extends ChangeNotifier {
return;
}
// Learned name<->key from this advert: promote a matching name-only block.
final blockService = _blockService;
if (blockService != null) {
unawaited(blockService.maybePromote(contact.name, contact.publicKeyHex));
}
if (contact.type == advTypeRepeater) {
final removedCount = _contactUnreadCount[contact.publicKeyHex] ?? 0;
_cachedContactsUnreadTotal = (_cachedContactsUnreadTotal - removedCount)
@ -5051,6 +5057,15 @@ class MeshCoreConnector extends ChangeNotifier {
if (contact != null) {
_updateContactLastMessageAt(contact.publicKeyHex, message.timestamp);
}
if (contact != null) {
// Learned name<->key from this DM: promote a matching name-only block.
final blockService = _blockService;
if (blockService != null) {
unawaited(
blockService.maybePromote(contact.name, contact.publicKeyHex),
);
}
}
if (!message.isOutgoing) {
final existing = _conversations[message.senderKeyHex];
final incomingTimestamp = message.timestamp.millisecondsSinceEpoch;

@ -24,6 +24,7 @@ class BlockService extends ChangeNotifier {
_blockedNames
..clear()
..addAll(await _store.loadNames());
await _pruneExpiredNames();
notifyListeners();
}
@ -61,4 +62,35 @@ class BlockService extends ChangeNotifier {
await _store.saveNames(_blockedNames);
notifyListeners();
}
/// Name-only blocks older than this are pruned on load (self-cleaning).
static const Duration _nameBlockTtl = Duration(days: 30);
/// Promote a name-only block to a durable pubkey block once we learn the
/// identity behind it (observed via an advert or a DM). No-op if the name
/// isn't name-blocked.
Future<void> maybePromote(String name, String publicKeyHex) async {
final n = name.trim().toLowerCase();
if (n.isEmpty || !_blockedNames.containsKey(n)) return;
_blockedNames.remove(n);
_blockedKeys.add(publicKeyHex.toLowerCase());
await _store.saveNames(_blockedNames);
await _store.saveKeys(_blockedKeys);
notifyListeners();
}
/// Drop name-only blocks that never linked to a pubkey within [_nameBlockTtl].
Future<void> _pruneExpiredNames() async {
final cutoff =
DateTime.now().millisecondsSinceEpoch - _nameBlockTtl.inMilliseconds;
final expired = _blockedNames.entries
.where((e) => e.value < cutoff)
.map((e) => e.key)
.toList();
if (expired.isEmpty) return;
for (final n in expired) {
_blockedNames.remove(n);
}
await _store.saveNames(_blockedNames);
}
}

Loading…
Cancel
Save

Powered by TurnKey Linux.