mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
Foreground reconnects rebuild preference managers and repeatedly derive the same NIP-44 keys on the UI isolate. Retain the six crypto objects in providers scoped to relay configuration and identity while preserving manager refresh, subscription, and publication behavior. Community changes, key rotation, and sign-out invalidate the retained objects. RelayConfig normalizes the signing key once at its shared accessor, so identity, theme encryption, event signing, and NIP-42 authentication receive the same value, including when an active configuration key has surrounding whitespace. The identity-export authorization snapshot uses the same normalized key for the active-identity check and the outgoing recovery payload. The branch incorporates main's relay-scoped preference managers. ### Validation - Regression exercises six production crypto providers across reconnects, configuration/key changes, and sign-out. Reintroducing a session dependency makes it fail. - Added a loopback WebSocket regression that exercises actual RelaySocket NIP-42 authentication with a padded key and verifies its challenge, relay, pubkey and signature. It fails before shared key normalization. - Added a connected-theme regression for whitespace-padded keys, encryption round-trip, and a valid published event signature. It fails before the normalization fix. - Added padded-key identity export coverage through authorization, SAS confirmation, exactly one normalized payload, and successful completion. Separate relay-change and key-rotation tests verify both fresh and expired grants fail closed. The padded-key regression failed before the fix. - Current head: all 48 focused pairing/crypto/auth tests and mobile analysis passed. All [CI checks](https://github.com/block/buzz/actions/runs/39497098787) passed, including mobile test shards, Android, native Swift tests and release builds. Codex feedback is addressed; Jude approved this exact head after independent regression and mutation checks. Fresh [Codex security review](https://github.com/block/buzz/pull/8442#issuecomment-6300273942) found no concrete issues. - Previous head: 122 config/socket/crypto/theme tests, 44 read-state tests, and all [GitHub CI checks](https://github.com/block/buzz/actions/runs/39491550425) passed, including mobile test shards, Android, native Swift tests and release builds. - Original implementation: native iOS Debug build and full local repository checks passed. A paired simulator background/resume capture had no samples in the targeted crypto constructors or key derivation; an 82 ms UI frame remained. Debug/JIT, profiler overhead and shared-host activity prevent an overall speedup claim. The later normalization change is covered by the production-provider regression above. - Two image decode timing failures occurred in the initial local full suite; direct baseline/patch runs, the subsequent full repository run, and the latest full mobile suite passed. Cold-start credential validation is unchanged: padded persisted keys are rejected by AuthNotifier before RelayConfig is constructed. The normalization regressions cover active configurations and identity export, not broader persisted-key acceptance. <!-- buzz-review-completed --> --------- Signed-off-by: Tom Brow <tomb@block.xyz>
146 lines
5.2 KiB
Dart
146 lines
5.2 KiB
Dart
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
import 'package:nostr/nostr.dart' as nostr;
|
|
|
|
import '../hosted/hosted_relay_auth.dart';
|
|
import '../community/community_provider.dart';
|
|
import 'relay_client.dart';
|
|
|
|
/// Relay connection configuration.
|
|
///
|
|
/// In the pure-nostr world the only secrets the app cares about are:
|
|
/// - `baseUrl` — where the relay lives (used for WS + media upload)
|
|
/// - `nsec` — the user's signing key (drives NIP-42 AUTH and event sigs)
|
|
class RelayConfig {
|
|
const RelayConfig({required String baseUrl, String? nsec})
|
|
: _baseUrl = baseUrl,
|
|
_nsec = nsec;
|
|
|
|
/// Relay origin exactly as the active community stored it.
|
|
final String _baseUrl;
|
|
|
|
/// Normalized Nostr secret key shared by identity, signing, and NIP-42 AUTH.
|
|
String? get nsec => _nsec?.trim();
|
|
|
|
final String? _nsec;
|
|
|
|
/// The origin as persisted, before scheme canonicalization.
|
|
///
|
|
/// Exists solely so identity-scoped storage keys written before [baseUrl]
|
|
/// was canonicalized stay reachable — see [readMigratedPref]. Never use it
|
|
/// for network I/O; [baseUrl] and [wsUrl] are the addresses to connect to.
|
|
String get storedOrigin => _baseUrl;
|
|
|
|
/// Relay origin as an HTTP(S) URL.
|
|
///
|
|
/// Communities are persisted with whichever scheme their onboarding flow
|
|
/// used: device pairing stores `https://` (it rejects anything else), while
|
|
/// an invite join stores the `wss://` relay URL carried by the invite link.
|
|
/// Every consumer treats this as an HTTP origin — [wsUrl], the `/query`
|
|
/// endpoint, media upload and Blossom auth — so a `wss://` base silently
|
|
/// degrades all of them. Folding the websocket schemes back here keeps both
|
|
/// onboarding paths equivalent, including for already-persisted communities.
|
|
///
|
|
/// Derived rather than normalized in the constructor so that the constructor
|
|
/// stays `const`: the compile-time fallback below relies on canonicalization
|
|
/// to keep its identity stable across rebuilds, and Riverpod's default
|
|
/// `updateShouldNotify` is `previous != next`, which falls back to identity
|
|
/// here. A fresh instance per rebuild would resubscribe every listener.
|
|
String get baseUrl {
|
|
final uri = Uri.tryParse(_baseUrl);
|
|
if (uri == null) return _baseUrl;
|
|
final scheme = switch (uri.scheme) {
|
|
'wss' => 'https',
|
|
'ws' => 'http',
|
|
_ => null,
|
|
};
|
|
return scheme == null ? _baseUrl : uri.replace(scheme: scheme).toString();
|
|
}
|
|
|
|
/// Derive the websocket URL from the HTTP base URL.
|
|
String get wsUrl {
|
|
final uri = Uri.parse(baseUrl);
|
|
final scheme = uri.scheme == 'https' ? 'wss' : 'ws';
|
|
return uri.replace(scheme: scheme).toString();
|
|
}
|
|
}
|
|
|
|
/// Compile-time environment config via --dart-define.
|
|
///
|
|
/// Run with:
|
|
/// flutter run --dart-define=BUZZ_RELAY_URL=http://localhost:3000 \
|
|
/// --dart-define=BUZZ_PUSH_GATEWAY_URL=http://localhost:8080
|
|
///
|
|
/// Or create a `.env.json` and use --dart-define-from-file=.env.json
|
|
class Env {
|
|
static const relayUrl = String.fromEnvironment(
|
|
'BUZZ_RELAY_URL',
|
|
defaultValue: 'http://localhost:3000',
|
|
);
|
|
|
|
/// Optional gateway origin. Without it this artifact has no push capability.
|
|
static const pushGatewayUrl = String.fromEnvironment('BUZZ_PUSH_GATEWAY_URL');
|
|
|
|
/// Whether this artifact can offer push notification enrollment.
|
|
static const pushGatewayConfigured = pushGatewayUrl != '';
|
|
}
|
|
|
|
class RelayConfigNotifier extends Notifier<RelayConfig> {
|
|
@override
|
|
RelayConfig build() {
|
|
// Push lease bookkeeping updates the community while using its live
|
|
// session. Only connection-context changes may restart that session.
|
|
final context = ref.watch(
|
|
activeCommunityProvider.select((value) {
|
|
final active = value.value;
|
|
return (id: active?.id, relayUrl: active?.relayUrl, nsec: active?.nsec);
|
|
}),
|
|
);
|
|
if (context.relayUrl != null) {
|
|
return RelayConfig(baseUrl: context.relayUrl!, nsec: context.nsec);
|
|
}
|
|
|
|
// Fallback to compile-time env config (dev mode).
|
|
return const RelayConfig(baseUrl: Env.relayUrl);
|
|
}
|
|
|
|
void update({required String baseUrl, String? nsec}) {
|
|
state = RelayConfig(baseUrl: baseUrl, nsec: nsec);
|
|
}
|
|
}
|
|
|
|
final relayConfigProvider = NotifierProvider<RelayConfigNotifier, RelayConfig>(
|
|
RelayConfigNotifier.new,
|
|
);
|
|
|
|
/// Derive the hex pubkey from a bech32 nsec, or null on any failure.
|
|
String? pubkeyFromNsec(String? nsec) {
|
|
if (nsec == null || nsec.isEmpty) return null;
|
|
try {
|
|
final privkeyHex = nostr.Nip19.decode(payload: nsec).data;
|
|
if (privkeyHex.isEmpty) return null;
|
|
return nostr.Keys(privkeyHex).public;
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// The current user's hex pubkey, derived from the active community nsec.
|
|
final myPubkeyProvider = Provider<String?>((ref) {
|
|
final config = ref.watch(relayConfigProvider);
|
|
return pubkeyFromNsec(config.nsec);
|
|
});
|
|
|
|
/// Provides a [RelayClient] that reacts to config changes.
|
|
///
|
|
/// Only used for the media upload HTTP endpoint now — all data flow goes
|
|
/// through the relay WebSocket session.
|
|
final relayClientProvider = Provider<RelayClient>((ref) {
|
|
final config = ref.watch(relayConfigProvider);
|
|
final client = RelayClient(
|
|
baseUrl: config.baseUrl,
|
|
httpClient: ref.watch(hostedHttpClientFactoryProvider)(),
|
|
);
|
|
ref.onDispose(client.dispose);
|
|
return client;
|
|
});
|