mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 20:48:24 +00:00
- Implement NIP-59 gift wrap protocol (rumor → seal → gift wrap) - Add NIP-44 encrypted message delivery with ephemeral keys - Support NIP-40 expiration tags for auto-deletion - Update error_reporting_service to use gift wrap for cached crash reports Part of #268 Signed-off-by: alltheseas <alltheseas@users.noreply.github.com> Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
143 lines
4.7 KiB
Dart
143 lines
4.7 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
import 'package:models/models.dart';
|
|
import 'package:zapstore/constants/app_constants.dart';
|
|
import 'package:zapstore/services/crash_report_cache_service.dart';
|
|
import 'package:zapstore/services/nip17_gift_wrap_service.dart';
|
|
import 'package:zapstore/utils/extensions.dart';
|
|
|
|
/// Service for reporting errors via NIP-17 gift-wrapped messages to Zapstore team.
|
|
///
|
|
/// Generates an ephemeral signer per report so errors can be sent even when
|
|
/// the user is not signed in.
|
|
class ErrorReportingService {
|
|
ErrorReportingService(this.ref);
|
|
|
|
final Ref ref;
|
|
|
|
/// Track reported errors to avoid spam (error hash -> timestamp)
|
|
final Map<int, DateTime> _reportedErrors = {};
|
|
|
|
/// Rate limit: one report per error type per session
|
|
static const _rateLimitDuration = Duration(minutes: 30);
|
|
|
|
/// Maximum reports per session to prevent abuse
|
|
static const _maxReportsPerSession = 10;
|
|
int _sessionReportCount = 0;
|
|
|
|
/// Report an error to the Zapstore team via encrypted DM.
|
|
///
|
|
/// Rate-limited to prevent spam. Only sends one report per unique
|
|
/// error type per session.
|
|
Future<void> reportError(Object exception, StackTrace? stackTrace) async {
|
|
// Don't report in debug mode
|
|
if (kDebugMode) {
|
|
return;
|
|
}
|
|
|
|
// Check session limit
|
|
if (_sessionReportCount >= _maxReportsPerSession) {
|
|
return;
|
|
}
|
|
|
|
// Rate limit by error type
|
|
final errorHash = exception.toString().hashCode;
|
|
final lastReport = _reportedErrors[errorHash];
|
|
if (lastReport != null &&
|
|
DateTime.now().difference(lastReport) < _rateLimitDuration) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Create ephemeral signer for error reporting
|
|
final signer = Bip340PrivateKeySigner(Utils.generateRandomHex64(), ref);
|
|
await signer.signIn(setAsActive: false, registerSigner: false);
|
|
|
|
// Format error report
|
|
final report = _formatErrorReport(exception, stackTrace);
|
|
|
|
// Create encrypted DM to Zapstore pubkey (uses NIP-44 by default)
|
|
final dm = PartialDirectMessage(
|
|
content: report,
|
|
receiver: kZapstorePubkey,
|
|
);
|
|
|
|
// Sign and publish
|
|
final signedDm = await dm.signWith(signer);
|
|
await ref.read(storageNotifierProvider.notifier).publish({
|
|
signedDm,
|
|
}, source: const RemoteSource(relays: 'social'));
|
|
|
|
// Update rate limiting
|
|
_reportedErrors[errorHash] = DateTime.now();
|
|
_sessionReportCount++;
|
|
} catch (e) {
|
|
// Silently fail - we don't want error reporting to cause more errors
|
|
}
|
|
}
|
|
|
|
String _formatErrorReport(Object exception, StackTrace? stackTrace) {
|
|
final buffer = StringBuffer();
|
|
|
|
buffer.writeln('=== ZAPSTORE ERROR REPORT ===');
|
|
buffer.writeln('Timestamp: ${DateTime.now().toUtc().toIso8601String()}');
|
|
buffer.writeln('Platform: ${Platform.operatingSystem}');
|
|
buffer.writeln('OS Version: ${Platform.operatingSystemVersion}');
|
|
buffer.writeln('');
|
|
buffer.writeln('Exception:');
|
|
buffer.writeln(exception.toString());
|
|
buffer.writeln('');
|
|
|
|
if (stackTrace != null) {
|
|
buffer.writeln('Stack Trace:');
|
|
// Limit stack trace length to avoid huge messages
|
|
final stackLines = stackTrace.toString().split('\n');
|
|
final limitedStack = stackLines.take(30).join('\n');
|
|
buffer.writeln(limitedStack);
|
|
if (stackLines.length > 30) {
|
|
buffer.writeln('... (${stackLines.length - 30} more lines)');
|
|
}
|
|
}
|
|
|
|
return buffer.toString();
|
|
}
|
|
|
|
/// Send cached crash reports with user consent.
|
|
///
|
|
/// Uses NIP-17 gift wrap for enhanced privacy:
|
|
/// - Ephemeral keys hide sender identity on relays
|
|
/// - NIP-44 encryption protects message content
|
|
/// - NIP-40 expiration tags ensure auto-deletion after 30 days
|
|
///
|
|
/// [userComment] is an optional message from the user describing what happened.
|
|
Future<void> sendCachedCrashReports(
|
|
List<CrashReport> crashes, {
|
|
String? userComment,
|
|
}) async {
|
|
final giftWrapService = ref.read(nip17GiftWrapServiceProvider);
|
|
|
|
for (final crash in crashes) {
|
|
try {
|
|
// Format crash report with optional user comment
|
|
final report = crash.toReportString(userComment: userComment);
|
|
|
|
// Send using NIP-17 gift wrap with 30-day expiration
|
|
await giftWrapService.sendGiftWrappedMessage(
|
|
content: report,
|
|
recipientPubkey: kCrashReportPubkey,
|
|
expirationDays: 30,
|
|
);
|
|
} catch (e) {
|
|
// Silently fail individual reports - continue with others
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Provider for the error reporting service
|
|
final errorReportingServiceProvider = Provider<ErrorReportingService>(
|
|
ErrorReportingService.new,
|
|
);
|