fix(mobile): bound and isolate Solo discovery failures

Skip invalid inventory events without losing valid agents, and cancel metadata body reads after a total deadline.

Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
kenny lopez
2026-10-24 11:38:30 -07:00
parent b2282c11ea
commit 2187953458
4 changed files with 64 additions and 6 deletions

View File

@@ -63,7 +63,12 @@ Map<String, String> _inventoryNames(List<NostrEvent> events, String owner) {
final id = event.getTagValue('d');
if (id == null || !RegExp(r'^[0-9a-f]{64}$').hasMatch(id)) continue;
// Verify before allowing a newer tombstone or record to replace a head.
nostr.Event.fromJson(jsonEncode(event.toJson()));
try {
nostr.Event.fromJson(jsonEncode(event.toJson()));
} catch (_) {
// A corrupt record must not hide unrelated verified agents.
continue;
}
final previous = heads[id];
if (previous == null || meEventIsNewer(event, previous)) heads[id] = event;
}

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'dart:isolate';
@@ -28,12 +29,24 @@ final meArchivedAgentsProvider = FutureProvider<Set<String>>((ref) async {
)
.timeout(const Duration(seconds: 3));
if (response.statusCode != 200) return {};
final chunks = StreamIterator(
response.stream.timeout(const Duration(seconds: 3)),
);
final bytes = <int>[];
await for (final chunk in response.stream.timeout(
const Duration(seconds: 3),
)) {
if (bytes.length + chunk.length > 256 * 1024) return {};
bytes.addAll(chunk);
try {
await (() async {
while (await chunks.moveNext()) {
final chunk = chunks.current;
if (bytes.length + chunk.length > 256 * 1024) {
throw const FormatException(
'Relay information exceeds size limit.',
);
}
bytes.addAll(chunk);
}
})().timeout(const Duration(seconds: 3));
} finally {
await chunks.cancel();
}
sync.check();
final info = jsonDecode(utf8.decode(bytes));

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'package:buzz/shared/me/me_archived_agents.dart';
@@ -68,6 +69,40 @@ void main() {
},
);
test('slow metadata has a total deadline and cancels its stream', () async {
final owner = nostr.Keys.generate();
final relay = _ArchiveRelay(null);
var cancelled = false;
final body = StreamController<List<int>>(onCancel: () => cancelled = true);
final client = MockClient.streaming(
(_, _) async => http.StreamedResponse(body.stream, 200),
);
final container = ProviderContainer(
overrides: [
meSyncProvider.overrideWithValue(
MeSync(
relay,
RelayConfig(baseUrl: 'https://relay.test', nsec: owner.nsec),
),
),
meRelayInformationClientProvider.overrideWithValue(client),
],
);
addTearDown(container.dispose);
addTearDown(client.close);
addTearDown(body.close);
final trickle = Timer.periodic(const Duration(milliseconds: 100), (_) {
body.add([32]);
});
addTearDown(trickle.cancel);
final result = await container
.read(meArchivedAgentsProvider.future)
.timeout(const Duration(seconds: 5));
expect(result, isEmpty);
expect(cancelled, isTrue);
expect(relay.author, isNull);
});
test(
'contact keys and unavailable metadata are not archive authority',
() async {

View File

@@ -237,6 +237,11 @@ void main() {
],
inventory: [
listing(agent.public),
for (final field in ['id', 'sig'])
NostrEvent.fromJson(
listing(agent.public, time: 3, content: '{}').toJson()
..[field] = '0' * (field == 'id' ? 64 : 128),
),
listing(archivedAgent.public),
listing(removed.public),
listing(removed.public, time: 2, content: '{}'),