mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
fix(push): honor legacy relay channel grammar before publication
Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
@@ -30,7 +30,11 @@ filters match event tags exactly. Array and string limits, community scoping,
|
||||
and channel membership checks still apply.
|
||||
|
||||
Mobile accepts both this descriptor and the older `uuid-v4-lowercase`
|
||||
descriptor. Older relays continue to reject non-v4 channel filters. Deploy
|
||||
descriptor. Mobile checks both positive and ignore channel filters against
|
||||
the advertised grammar before enrollment or publication. A lease requiring
|
||||
non-v4 IDs on an older relay remains unavailable until that relay upgrades;
|
||||
mobile preserves all desired filters and rechecks capabilities every five
|
||||
minutes instead of sending an invalid lease. Deploy
|
||||
mobile support for `uuid-lowercase` before changing the relay advertisement:
|
||||
older mobile clients reject that descriptor and cannot renew their push leases.
|
||||
The new grammar does not alter existing UUIDv4 subscriptions.
|
||||
|
||||
@@ -19,6 +19,9 @@ const _installationIdPattern = r'^[0-9a-f]{32}$';
|
||||
|
||||
class BuzzPushLeaseDescriptor {
|
||||
final String origin;
|
||||
|
||||
/// Canonical channel-ID grammar advertised by this relay.
|
||||
final String hGrammar;
|
||||
final String executorKeyId;
|
||||
final String executorPubkey;
|
||||
final String transport;
|
||||
@@ -30,6 +33,7 @@ class BuzzPushLeaseDescriptor {
|
||||
|
||||
const BuzzPushLeaseDescriptor({
|
||||
required this.origin,
|
||||
this.hGrammar = 'uuid-v4-lowercase',
|
||||
required this.executorKeyId,
|
||||
required this.executorPubkey,
|
||||
required this.transport,
|
||||
@@ -115,8 +119,7 @@ class BuzzPushLeaseDescriptor {
|
||||
);
|
||||
}
|
||||
final hGrammar = _nonEmptyString(push['h_grammar'], name: 'h_grammar');
|
||||
// Older relays advertise the narrower grammar. Keep UUIDv4 leases working
|
||||
// during rollout; those relays still reject non-v4 channels themselves.
|
||||
// Retain the advertised grammar so publication can enforce legacy limits.
|
||||
if (hGrammar != 'uuid-lowercase' && hGrammar != 'uuid-v4-lowercase') {
|
||||
throw const FormatException('Unsupported push h_grammar');
|
||||
}
|
||||
@@ -187,6 +190,7 @@ class BuzzPushLeaseDescriptor {
|
||||
|
||||
return BuzzPushLeaseDescriptor(
|
||||
origin: origin,
|
||||
hGrammar: hGrammar,
|
||||
executorKeyId: currentKey['id'] as String,
|
||||
executorPubkey: currentKey['pubkey'] as String,
|
||||
transport: transport,
|
||||
@@ -199,6 +203,33 @@ class BuzzPushLeaseDescriptor {
|
||||
}
|
||||
}
|
||||
|
||||
/// The desired lease requires channel IDs that this relay cannot accept yet.
|
||||
class BuzzPushChannelGrammarException implements Exception {
|
||||
const BuzzPushChannelGrammarException();
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'Push lease requires uuid-lowercase support from the relay';
|
||||
}
|
||||
|
||||
/// Validate both positive and ignore filters without weakening mute semantics.
|
||||
void validateBuzzPushChannelGrammar(
|
||||
BuzzPushLeaseDescriptor descriptor,
|
||||
List<BuzzPushSubscription> subscriptions,
|
||||
) {
|
||||
if (descriptor.hGrammar == 'uuid-lowercase') return;
|
||||
for (final subscription in subscriptions) {
|
||||
for (final filter in [subscription.filter, ...subscription.ignore]) {
|
||||
for (final channel in filter.hTags ?? const <String>[]) {
|
||||
// The legacy Rust validator checked version 4, not the UUID variant.
|
||||
if (channel[14] != '4') {
|
||||
throw const BuzzPushChannelGrammarException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<BuzzPushLeaseDescriptor> fetchBuzzPushLeaseDescriptor(
|
||||
String relayBaseUrl, {
|
||||
http.Client? client,
|
||||
@@ -262,6 +293,7 @@ Future<BuzzPushLeasePublication> publishBuzzDevPushLease({
|
||||
required BuzzPushLeaseSubmit submit,
|
||||
DateTime Function() now = DateTime.now,
|
||||
}) async {
|
||||
validateBuzzPushChannelGrammar(descriptor, subscriptions);
|
||||
_validateGrant(grant, descriptor);
|
||||
final effectiveLeaseInstallationId =
|
||||
leaseInstallationId ?? grant.installationId;
|
||||
|
||||
@@ -82,6 +82,7 @@ String buzzPushPublicationAttemptKey({
|
||||
token,
|
||||
descriptor.executorKeyId,
|
||||
descriptor.executorPubkey,
|
||||
descriptor.hGrammar,
|
||||
buzzPushSubscriptionsFingerprint(subscriptions),
|
||||
].join('|');
|
||||
|
||||
@@ -318,8 +319,13 @@ class BuzzPushBootstrap extends HookConsumerWidget {
|
||||
},
|
||||
);
|
||||
} catch (error, stack) {
|
||||
publicationAttempt.failed(
|
||||
publicationAttempt.retryAfter(
|
||||
attempt,
|
||||
// Re-fetch capabilities after a relay upgrade without repeatedly
|
||||
// enrolling or publishing a lease the old relay cannot accept.
|
||||
delay: error is BuzzPushChannelGrammarException
|
||||
? const Duration(minutes: 5)
|
||||
: _pushBootstrapRetryDelay,
|
||||
retry: () {
|
||||
if (context.mounted) publicationRetry.value += 1;
|
||||
},
|
||||
@@ -380,6 +386,7 @@ class BuzzPushBootstrap extends HookConsumerWidget {
|
||||
final descriptor = await ref.read(buzzPushDescriptorFetcherProvider)(
|
||||
config.baseUrl,
|
||||
);
|
||||
validateBuzzPushChannelGrammar(descriptor, desired);
|
||||
final grant = await enrollBuzzPush(
|
||||
config.wsUrl,
|
||||
Env.pushGatewayUrl,
|
||||
|
||||
@@ -33,6 +33,74 @@ void main() {
|
||||
});
|
||||
}
|
||||
|
||||
for (final ignored in [false, true]) {
|
||||
test(
|
||||
'legacy relay rejects non-v4 ${ignored ? 'ignore' : 'positive'} filters before submission',
|
||||
() async {
|
||||
final information = _descriptorJson(relay.public);
|
||||
(information['push'] as Map<String, dynamic>)['h_grammar'] =
|
||||
'uuid-v4-lowercase';
|
||||
final legacy = BuzzPushLeaseDescriptor.fromRelayInformation(
|
||||
information,
|
||||
);
|
||||
final channelFilter = BuzzPushFilter(
|
||||
kinds: const [9],
|
||||
hTags: const ['fee88e92-da9f-5958-a910-48db3264ad38'],
|
||||
);
|
||||
var submissions = 0;
|
||||
await expectLater(
|
||||
publishBuzzDevPushLease(
|
||||
grant: grant,
|
||||
descriptor: legacy,
|
||||
nsec: signer.nsec,
|
||||
memberPubkey: signer.public,
|
||||
subscriptions: [
|
||||
BuzzPushSubscription(
|
||||
filter: ignored
|
||||
? BuzzPushFilter(kinds: const [9], pTags: [signer.public])
|
||||
: channelFilter,
|
||||
notificationClass: 'default',
|
||||
ignore: ignored ? [channelFilter] : const [],
|
||||
),
|
||||
],
|
||||
now: () => now,
|
||||
submit:
|
||||
({
|
||||
required kind,
|
||||
required content,
|
||||
required tags,
|
||||
createdAt,
|
||||
}) async {
|
||||
submissions++;
|
||||
throw StateError('incompatible lease must not be sent');
|
||||
},
|
||||
),
|
||||
throwsA(isA<BuzzPushChannelGrammarException>()),
|
||||
);
|
||||
expect(submissions, 0);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
test('legacy relay keeps compatible subscriptions and ignores intact', () {
|
||||
final information = _descriptorJson(relay.public);
|
||||
(information['push'] as Map<String, dynamic>)['h_grammar'] =
|
||||
'uuid-v4-lowercase';
|
||||
final legacy = BuzzPushLeaseDescriptor.fromRelayInformation(information);
|
||||
final subscriptions = buildDesiredBuzzPushSubscriptions(
|
||||
myPubkey: signer.public,
|
||||
channelIds: const [
|
||||
'11111111-1111-4111-8111-111111111111',
|
||||
'22222222-2222-4222-8222-222222222222',
|
||||
],
|
||||
mutedChannelIds: const ['22222222-2222-4222-8222-222222222222'],
|
||||
);
|
||||
final before = buzzPushSubscriptionsFingerprint(subscriptions);
|
||||
validateBuzzPushChannelGrammar(legacy, subscriptions);
|
||||
expect(buzzPushSubscriptionsFingerprint(subscriptions), before);
|
||||
expect(legacy.hGrammar, 'uuid-v4-lowercase');
|
||||
});
|
||||
|
||||
test('legacy capability does not enable Buzz push', () {
|
||||
final information = _descriptorJson(relay.public);
|
||||
information['supported_extensions'] = ['nip-pl'];
|
||||
|
||||
@@ -168,6 +168,21 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('relay grammar upgrade changes publication attempt identity', () {
|
||||
String key(String grammar) => buzzPushPublicationAttemptKey(
|
||||
communityId: 'community',
|
||||
relayBaseUrl: 'https://relay.example',
|
||||
token: 'token',
|
||||
descriptor: _descriptor(
|
||||
keyId: 'relay-v1',
|
||||
pubkey: _hex('b'),
|
||||
hGrammar: grammar,
|
||||
),
|
||||
subscriptions: const [],
|
||||
);
|
||||
expect(key('uuid-lowercase'), isNot(key('uuid-v4-lowercase')));
|
||||
});
|
||||
|
||||
test('default push requires relay capability and respects opt-out', () {
|
||||
final enabled = Community.create(
|
||||
name: 'Team',
|
||||
@@ -269,8 +284,10 @@ void main() {
|
||||
BuzzPushLeaseDescriptor _descriptor({
|
||||
required String keyId,
|
||||
required String pubkey,
|
||||
String hGrammar = 'uuid-v4-lowercase',
|
||||
}) => BuzzPushLeaseDescriptor(
|
||||
origin: 'wss://relay.example',
|
||||
hGrammar: hGrammar,
|
||||
executorKeyId: keyId,
|
||||
executorPubkey: pubkey,
|
||||
transport: 'apns',
|
||||
|
||||
Reference in New Issue
Block a user