From f2024a5ac57a91d1911a9aca3beac43476970bbb Mon Sep 17 00:00:00 2001 From: Bradley Axen Date: Mon, 2 Nov 2026 10:15:39 -0800 Subject: [PATCH] fix(mobile): let the issuer choose relay assertion lifetime Mobile rejected any NIP-FI relay assertion expiring more than 300s out, so once the issuer moves to longer lifetimes, org sign-in fails on every fetch and the Block relay can't connect. Keep the expired check and drop the cap, matching desktop (block/buzz-app#2171). The token cache already refreshes from expires_at. Co-authored-by: claude00 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Bradley Axen --- .../lib/shared/hosted/hosted_relay_auth.dart | 2 +- .../hosted/organization_relay_auth_test.dart | 29 ++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/mobile/lib/shared/hosted/hosted_relay_auth.dart b/mobile/lib/shared/hosted/hosted_relay_auth.dart index 5507a75683..9e6893b67e 100644 --- a/mobile/lib/shared/hosted/hosted_relay_auth.dart +++ b/mobile/lib/shared/hosted/hosted_relay_auth.dart @@ -223,7 +223,7 @@ class HostedRelayAuth extends ChangeNotifier { throw AccountFailure.invalidResponse; } final now = _now().millisecondsSinceEpoch ~/ 1000; - if (expiry <= now || expiry > now + 300) { + if (expiry <= now) { throw AccountFailure.invalidResponse; } final expires = DateTime.fromMillisecondsSinceEpoch( diff --git a/mobile/test/shared/hosted/organization_relay_auth_test.dart b/mobile/test/shared/hosted/organization_relay_auth_test.dart index 3515256885..58fc27b3fe 100644 --- a/mobile/test/shared/hosted/organization_relay_auth_test.dart +++ b/mobile/test/shared/hosted/organization_relay_auth_test.dart @@ -87,6 +87,7 @@ void main() { group('organization relay assertions', () { late List requests; late Object? boundIdentity; + late int assertionLifetime; BuilderlabAccountClient organizationClient() => BuilderlabAccountClient( accountOrigin: organization, @@ -110,7 +111,9 @@ void main() { jsonEncode({ 'assertion': 'org-token', 'nostr_pubkey': keys.public, - 'expires_at': DateTime.now().millisecondsSinceEpoch ~/ 1000 + 120, + 'expires_at': + DateTime.now().millisecondsSinceEpoch ~/ 1000 + + assertionLifetime, }), 200, ), @@ -145,6 +148,7 @@ void main() { setUp(() { requests = []; boundIdentity = null; + assertionLifetime = 120; }); test( @@ -173,6 +177,29 @@ void main() { }, ); + test('the issuer chooses the lifetime of an unexpired assertion', () async { + await saveSession(); + for (final (lifetime, accepted) in [ + (300, true), + (86400, true), + (0, false), + (-60, false), + ]) { + assertionLifetime = lifetime; + final auth = relayAuth(organizationClient()); + if (accepted) { + expect(await auth.headers(relay), { + 'Nostr-Federated-Identity': 'Bearer org-token', + }); + } else { + await expectLater( + auth.headers(relay), + throwsA(AccountFailure.invalidResponse), + ); + } + } + }); + test('a key bound to the same member is accepted', () async { await saveSession(); boundIdentity = {'pubkey_hex': keys.public};