feat: import Chinese-localized Buzz source snapshot
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled

Signed-off-by: cls_宁波本机 <908705107@qq.com>
This commit is contained in:
2026-08-13 18:34:25 +08:00
parent 61c3fa1df9
commit 9dfa06ffee
3785 changed files with 1085458 additions and 2 deletions
@@ -0,0 +1,234 @@
import 'dart:async';
import 'dart:convert';
import 'package:nostr/nostr.dart' as nostr;
import 'package:web_socket_channel/web_socket_channel.dart';
import '../../shared/relay/nostr_models.dart';
const _desktopPairingAuthChallengeGrace = Duration(seconds: 3);
const _pairingAuthOkTimeout = Duration(seconds: 8);
/// Ephemeral WebSocket connection for NIP-AB pairing.
///
/// Uses ephemeral keys for NIP-42 auth (not the stored user keys).
/// Single-use — disposed after the pairing session completes.
class PairingAuthException implements Exception {
final String message;
const PairingAuthException(this.message);
@override
String toString() => 'PairingAuthException: $message';
}
class PairingSocket {
final String _wsUrl;
final String _ephemeralPrivkey;
final void Function(List<dynamic> message) _onMessage;
final void Function(Object? error) _onDisconnected;
final Duration _authChallengeTimeout;
final Duration _authResponseTimeout;
final WebSocketChannel Function(Uri uri) _channelFactory;
WebSocketChannel? _channel;
StreamSubscription<dynamic>? _subscription;
Completer<void>? _authCompleter;
Timer? _authChallengeTimer;
Timer? _authResponseTimer;
String? _pendingAuthEventId;
bool _connected = false;
PairingSocket({
required String wsUrl,
required String ephemeralPrivkey,
required void Function(List<dynamic> message) onMessage,
required void Function(Object? error) onDisconnected,
Duration authChallengeTimeout = _desktopPairingAuthChallengeGrace,
Duration authResponseTimeout = _pairingAuthOkTimeout,
WebSocketChannel Function(Uri uri) channelFactory =
WebSocketChannel.connect,
}) : _wsUrl = wsUrl,
_ephemeralPrivkey = ephemeralPrivkey,
_onMessage = onMessage,
_onDisconnected = onDisconnected,
_authChallengeTimeout = authChallengeTimeout,
_authResponseTimeout = authResponseTimeout,
_channelFactory = channelFactory;
bool get isConnected => _connected;
/// Connect and answer a NIP-42 challenge when the relay requires one.
Future<void> connect() async {
_channel = _channelFactory(Uri.parse(_wsUrl));
await _channel!.ready;
_authCompleter = Completer<void>();
_subscription = _channel!.stream.listen(
_handleRawMessage,
onError: _failAuth,
onDone: () => _failAuth(null),
);
// Dedicated pairing relays may be open and send no NIP-42 challenge.
_authChallengeTimer = Timer(_authChallengeTimeout, () {
if (_pendingAuthEventId == null &&
_authCompleter != null &&
!_authCompleter!.isCompleted) {
_authCompleter!.complete();
}
});
try {
await _authCompleter!.future;
_connected = true;
} catch (error) {
await disconnect();
_onDisconnected(error);
rethrow;
} finally {
_authChallengeTimer?.cancel();
_authResponseTimer?.cancel();
}
}
/// Send a raw JSON array.
void send(List<dynamic> payload) {
_channel?.sink.add(jsonEncode(payload));
}
/// Send a subscribe request.
void subscribe(String subId, int kind, String pubkeyHex) {
send([
'REQ',
subId,
{
'kinds': [kind],
'#p': [pubkeyHex],
},
]);
}
/// Publish a Nostr event (already JSON-encoded map).
void publishEvent(Map<String, dynamic> event) {
send(['EVENT', event]);
}
Future<void> disconnect() async {
_connected = false;
_subscription?.cancel();
_subscription = null;
_authChallengeTimer?.cancel();
_authResponseTimer?.cancel();
final channel = _channel;
_channel = null;
if (channel != null) {
await channel.sink.close();
}
}
void dispose() {
_connected = false;
_subscription?.cancel();
_channel?.sink.close();
_channel = null;
_authChallengeTimer?.cancel();
_authResponseTimer?.cancel();
}
void _failAuth(Object? error) {
final authError = error ?? Exception('Connection closed');
if (_authCompleter != null && !_authCompleter!.isCompleted) {
_authCompleter!.completeError(authError);
return;
}
if (_connected) {
unawaited(disconnect());
_onDisconnected(authError);
}
}
void _handleRawMessage(dynamic raw) {
if (raw is! String) return;
final List<dynamic> data;
try {
data = jsonDecode(raw) as List<dynamic>;
} catch (_) {
return;
}
if (data.isEmpty) return;
final type = data[0] as String;
switch (type) {
case 'AUTH':
_handleAuthChallenge(data);
case 'OK':
_handleOk(data);
default:
// Pass EVENT, EOSE, NOTICE upstream.
_onMessage(data);
}
}
void _handleAuthChallenge(List<dynamic> data) {
if (data.length < 2) return;
final challenge = data[1] as String;
_authChallengeTimer?.cancel();
_authResponseTimer?.cancel();
_authResponseTimer = Timer(_authResponseTimeout, () {
_failAuth(
const PairingAuthException('Relay did not confirm authentication'),
);
});
try {
// Build NIP-42 auth event (kind:22242) with ephemeral keys.
final tags = <List<String>>[
['relay', _wsUrl],
['challenge', challenge],
];
final event = nostr.Event.from(
kind: EventKind.auth,
content: '',
tags: tags,
secretKey: _ephemeralPrivkey,
createdAt: DateTime.now().millisecondsSinceEpoch ~/ 1000,
);
_pendingAuthEventId = event.id;
send(['AUTH', event.toMap()]);
} catch (e) {
_failAuth(e);
}
}
void _handleOk(List<dynamic> data) {
if (data.length < 3) return;
final eventId = data[1] as String;
final accepted = data[2] as bool;
if (_pendingAuthEventId != null && eventId == _pendingAuthEventId) {
_pendingAuthEventId = null;
_authResponseTimer?.cancel();
if (accepted) {
if (_authCompleter != null && !_authCompleter!.isCompleted) {
_authCompleter!.complete();
}
} else {
final message = data.length > 3
? data[3] as String
: 'Auth rejected by relay';
_failAuth(PairingAuthException(message));
}
return;
}
// Pass non-auth OK upstream.
_onMessage(data);
}
}