mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
fix(mobile): tolerate unrelated NIP-11 relay metadata (#8160)
Relay capability additions such as `read_state_snapshot` currently make mobile push discovery reject otherwise valid NIP-11 documents. This blocks descriptor-dependent enrollment, lease updates, and remote revocation. Read unrelated top-level metadata permissively while preserving validation of the advertised `nip-pl` extension and the complete push descriptor. Each returned descriptor value is covered by compatibility tests; missing or malformed required push fields still fail. ### Validation - [Codex code review](https://github.com/block/buzz/pull/8160#issuecomment-6049355700) and [security review](https://github.com/block/buzz/pull/8160#issuecomment-6049486587) completed without findings on the final code. - Added 24 regression cases covering read-state metadata, arbitrary future metadata, HTTP discovery, missing/malformed push fields, and invalid extension advertisements. The six compatibility/discovery cases fail on unchanged main and pass with the fix. - All 55 focused descriptor, bootstrap, capability, and revocation tests pass. Mobile formatting and analysis pass. - Full mobile gate passes: 3,063 tests passed and four skipped with a configured gateway, plus all three tests for an unconfigured gateway build and the gateway recipe checks. - [Current-head CI](https://github.com/block/buzz/actions/runs/37706738403) passes, including Flutter tests, Android debug APK compilation, the complete iOS simulator app, and the unsigned iOS release build. - Production HTTP descriptor discovery against live personal-relay metadata passes without enrollment or relay changes. - Device enrollment, delivery, notification tap, and opt-out remain to be exercised in the updated iOS candidate. No relay or device state was changed. buzz-review-completed --------- Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
@@ -44,33 +44,6 @@ class BuzzPushLeaseDescriptor {
|
||||
factory BuzzPushLeaseDescriptor.fromRelayInformation(
|
||||
Map<String, dynamic> information,
|
||||
) {
|
||||
_requireExactKeys(
|
||||
information,
|
||||
required: const {},
|
||||
allowed: const {
|
||||
'name',
|
||||
'description',
|
||||
'pubkey',
|
||||
'contact',
|
||||
'supported_nips',
|
||||
'supported_extensions',
|
||||
'software',
|
||||
'version',
|
||||
'limitation',
|
||||
'retention',
|
||||
'relay_countries',
|
||||
'language_tags',
|
||||
'tags',
|
||||
'posting_policy',
|
||||
'payments_url',
|
||||
'fees',
|
||||
'icon',
|
||||
'self',
|
||||
'pairing_relay_url',
|
||||
'push',
|
||||
},
|
||||
name: 'NIP-11 document',
|
||||
);
|
||||
final extensions = _stringList(
|
||||
information['supported_extensions'],
|
||||
name: 'supported_extensions',
|
||||
|
||||
@@ -11,6 +11,8 @@ import 'package:buzz/shared/relay/relay_socket.dart';
|
||||
import 'package:buzz/shared/relay/signed_event_relay.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:nostr/nostr.dart' as nostr;
|
||||
|
||||
void main() {
|
||||
@@ -298,6 +300,111 @@ void main() {
|
||||
expect(jsonDecode(plaintext), jsonDecode(publication.plaintext));
|
||||
});
|
||||
|
||||
group('NIP-11 forward compatibility', () {
|
||||
final metadata = <String, Object?>{
|
||||
'read_state_snapshot': {
|
||||
'version': 1,
|
||||
'community_id': '00000000-0000-4000-8000-000000000001',
|
||||
'max_events': 4096,
|
||||
'max_event_array_bytes': 8388608,
|
||||
},
|
||||
'future_object': {
|
||||
'nested': [1, true, null],
|
||||
},
|
||||
'future_array': [1, 'two'],
|
||||
'future_scalar': false,
|
||||
'future_null': null,
|
||||
};
|
||||
|
||||
for (final entry in metadata.entries) {
|
||||
test('ignores unrelated top-level ${entry.key}', () {
|
||||
final information = _descriptorJson(relay.public)
|
||||
..[entry.key] = entry.value;
|
||||
final parsed = BuzzPushLeaseDescriptor.fromRelayInformation(
|
||||
information,
|
||||
);
|
||||
expect(parsed.origin, descriptor.origin);
|
||||
expect(parsed.executorPubkey, descriptor.executorPubkey);
|
||||
expect(parsed.executorKeyId, descriptor.executorKeyId);
|
||||
expect(parsed.transport, descriptor.transport);
|
||||
expect(parsed.maxLeaseTtlSeconds, descriptor.maxLeaseTtlSeconds);
|
||||
expect(parsed.maxContentLength, descriptor.maxContentLength);
|
||||
expect(parsed.maxPlaintextLength, descriptor.maxPlaintextLength);
|
||||
expect(parsed.maxEndpointLength, descriptor.maxEndpointLength);
|
||||
expect(parsed.maxStringLength, descriptor.maxStringLength);
|
||||
});
|
||||
}
|
||||
|
||||
test('HTTP discovery accepts unrelated metadata', () async {
|
||||
final information = _descriptorJson(relay.public)..addAll(metadata);
|
||||
final client = MockClient((request) async {
|
||||
expect(request.url, Uri.parse('https://tenant.example:8443/'));
|
||||
expect(request.headers['Accept'], 'application/nostr+json');
|
||||
return http.Response(jsonEncode(information), 200);
|
||||
});
|
||||
addTearDown(client.close);
|
||||
|
||||
final parsed = await fetchBuzzPushLeaseDescriptor(
|
||||
'https://tenant.example:8443',
|
||||
client: client,
|
||||
);
|
||||
expect(parsed.executorPubkey, relay.public);
|
||||
expect(parsed.origin, descriptor.origin);
|
||||
});
|
||||
|
||||
for (final field in [
|
||||
'origin',
|
||||
'keys',
|
||||
'app_profiles',
|
||||
'push_kinds',
|
||||
'h_grammar',
|
||||
'class_support',
|
||||
'limitation',
|
||||
]) {
|
||||
for (final missing in [true, false]) {
|
||||
test(
|
||||
'${missing ? 'missing' : 'malformed'} push.$field fails with future metadata',
|
||||
() {
|
||||
final information = _descriptorJson(relay.public)..addAll(metadata);
|
||||
final push = information['push'] as Map<String, dynamic>;
|
||||
if (missing) {
|
||||
push.remove(field);
|
||||
} else {
|
||||
push[field] = false;
|
||||
}
|
||||
expect(
|
||||
() => BuzzPushLeaseDescriptor.fromRelayInformation(information),
|
||||
throwsA(
|
||||
isA<FormatException>().having(
|
||||
(error) => error.message,
|
||||
'rejected field',
|
||||
contains(field),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (final extensions in [
|
||||
null,
|
||||
false,
|
||||
<String>[],
|
||||
['nip-er'],
|
||||
]) {
|
||||
test('invalid extension advertisement $extensions fails', () {
|
||||
final information = _descriptorJson(relay.public)
|
||||
..addAll(metadata)
|
||||
..['supported_extensions'] = extensions;
|
||||
expect(
|
||||
() => BuzzPushLeaseDescriptor.fromRelayInformation(information),
|
||||
throwsA(isA<FormatException>()),
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('descriptor rejects canonical origin with a trailing slash', () {
|
||||
final information = _descriptorJson(relay.public);
|
||||
(information['push'] as Map<String, dynamic>)['origin'] =
|
||||
|
||||
Reference in New Issue
Block a user