[READ ONLY MIRROR] Open Source TikTok alternative built on AT Protocol github.com/sprksocial/client
flutter atproto video dart
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: prepare for auth scopes

+138 -21
+4
lib/src/core/auth/data/models/auth_snapshot.dart
··· 89 89 this.clientSecret, 90 90 this.registrationAccessToken, 91 91 this.clientSecretExpiresAt, 92 + this.scope, 92 93 }); 93 94 94 95 factory AipClientRegistration.fromJson(Map<String, dynamic> json) { ··· 97 98 clientSecret: json['clientSecret'] as String?, 98 99 registrationAccessToken: json['registrationAccessToken'] as String?, 99 100 clientSecretExpiresAt: json['clientSecretExpiresAt'] as String?, 101 + scope: json['scope'] as String?, 100 102 ); 101 103 } 102 104 ··· 104 106 final String? clientSecret; 105 107 final String? registrationAccessToken; 106 108 final String? clientSecretExpiresAt; 109 + final String? scope; 107 110 108 111 DateTime? get clientSecretExpiresAtDateTime { 109 112 final value = clientSecretExpiresAt; ··· 117 120 'clientSecret': clientSecret, 118 121 'registrationAccessToken': registrationAccessToken, 119 122 'clientSecretExpiresAt': clientSecretExpiresAt, 123 + 'scope': scope, 120 124 }; 121 125 } 122 126 }
+20 -7
lib/src/core/auth/data/repositories/auth_repository_impl.dart
··· 20 20 Future<({String did, String handle})> Function(ATProto atproto); 21 21 22 22 const String _redirectUriValue = 'sprk://oauth-callback'; 23 - const String _aipScope = 'atproto transition:generic'; 24 23 const String _clientName = 'Spark Mobile App'; 25 24 const String _clientUri = 'https://sprk.so'; 26 25 const String _softwareId = 'spark-mobile'; ··· 28 27 const Duration _refreshLeeway = Duration(minutes: 5); 29 28 const String _randomCharset = 30 29 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; 30 + 31 + List<String> _buildAipScopes() { 32 + return const <String>['atproto', 'transition:generic']; 33 + } 34 + 35 + String _buildAipScope() => _buildAipScopes().join(' '); 36 + 37 + bool _registrationScopeMatches(AipClientRegistration registration) { 38 + return registration.scope == _buildAipScope(); 39 + } 31 40 32 41 class AuthRepositoryImpl implements AuthRepository { 33 42 AuthRepositoryImpl({ ··· 51 60 final DateTime Function() _now; 52 61 final AtprotoSessionFetcher _fetchSessionInfo; 53 62 final Uri _aipBaseUri; 63 + final List<String> _aipScopes = _buildAipScopes(); 54 64 final Completer<void> _initCompleter = Completer<void>(); 55 65 56 66 Future<bool>? _refreshInFlight; ··· 336 346 _AipOAuthMetadata metadata, 337 347 ) async { 338 348 final existing = _snapshot?.aipClientRegistration; 339 - if (existing != null && !_registrationNeedsRefresh(existing)) { 349 + if (existing != null && 350 + !_registrationNeedsRefresh(existing) && 351 + _registrationScopeMatches(existing)) { 340 352 return existing; 341 353 } 342 354 ··· 350 362 'response_types': ['code'], 351 363 'grant_types': ['authorization_code', 'refresh_token'], 352 364 'token_endpoint_auth_method': 'client_secret_post', 353 - 'scope': _aipScope, 365 + 'scope': _buildAipScope(), 354 366 'software_id': _softwareId, 355 367 'software_version': _softwareVersion, 356 368 }), ··· 364 376 365 377 final registration = _AipClientRegistrationResponse.fromJson( 366 378 _decodeJsonObject(response.body), 367 - ).toStoredRegistration(); 379 + ).toStoredRegistration(scope: _buildAipScope()); 368 380 369 381 final previousClientId = existing?.clientId; 370 382 _snapshot = (_snapshot ?? const AuthSnapshot()).copyWith( ··· 448 460 449 461 var authorizationUri = grant.getAuthorizationUrl( 450 462 redirectUri, 451 - scopes: _aipScope.split(' '), 463 + scopes: _aipScopes, 452 464 state: state, 453 465 ); 454 466 ··· 510 522 511 523 grant.getAuthorizationUrl( 512 524 Uri.parse(context.redirectUri), 513 - scopes: _aipScope.split(' '), 525 + scopes: _aipScopes, 514 526 state: context.state, 515 527 ); 516 528 ··· 903 915 final String? registrationAccessToken; 904 916 final int? clientSecretExpiresAt; 905 917 906 - AipClientRegistration toStoredRegistration() { 918 + AipClientRegistration toStoredRegistration({required String scope}) { 907 919 final secretExpiry = clientSecretExpiresAt; 908 920 final expiryDateTime = secretExpiry == null || secretExpiry <= 0 909 921 ? null ··· 914 926 clientSecret: clientSecret, 915 927 registrationAccessToken: registrationAccessToken, 916 928 clientSecretExpiresAt: expiryDateTime?.toIso8601String(), 929 + scope: scope, 917 930 ); 918 931 } 919 932 }
+21 -7
lib/src/core/config/app_config.dart
··· 8 8 class AppConfig { 9 9 /// Base URL for the video processing service. 10 10 static String get videoServiceUrl => 11 - _getStringValue('VIDEO_SERVICE_URL', 'http://localhost:3000'); 11 + _getStringValue('VIDEO_SERVICE_URL', 'https://video.sprk.so'); 12 12 13 13 /// License key for the img.ly editor. 14 14 static String get license => _getStringValue('SHOWCASES_LICENSE_FLUTTER', ''); 15 15 16 16 /// URL for the app view (web view display). 17 17 static String get appViewUrl => 18 - _getStringValue('SPRK_APPVIEW_URL', 'http://localhost:3000'); 18 + _getStringValue('SPRK_APPVIEW_URL', 'https://api.sprk.so'); 19 + 20 + /// Base URL for the Bluesky appview. 21 + static String get bskyAppViewUrl => 22 + _getStringValue('BSKY_APPVIEW_URL', 'https://api.bsky.app'); 23 + 24 + /// DID for the Spark moderation service. 25 + static String get modDid => _getStringValue( 26 + 'MOD_DID', 27 + 'did:plc:pbgyr67hftvpoqtvaurpsctc#atproto_labeler', 28 + ); 29 + 30 + /// DID for the Bluesky moderation service. 31 + static String get bskyModDid => _getStringValue( 32 + 'BSKY_MOD_DID', 33 + 'did:plc:ar7c4by46qjdydhdevvrndac#atproto_labeler', 34 + ); 19 35 20 36 /// Base URL for the messages service (chat service). 21 37 static String get messagesServiceUrl => 22 - _getStringValue('MESSAGES_SERVICE_URL', 'http://localhost:3000'); 38 + _getStringValue('MESSAGES_SERVICE_URL', 'https://chat.sprk.so'); 23 39 24 40 /// Base URL for the AIP OAuth server. 25 - static String get aipBaseUrl => _getStringValue( 26 - 'AIP_BASE_URL', 27 - _getStringValue('OAUTH_ISSUER_URL', 'https://auth.sprk.so'), 28 - ); 41 + static String get aipBaseUrl => 42 + _getStringValue('AIP_BASE_URL', 'https://auth.sprk.so'); 29 43 30 44 /// Service DID for the chat service (used for service auth). 31 45 static String get chatServiceDid =>
+3 -3
lib/src/core/network/atproto/data/repositories/sprk_repository.dart
··· 21 21 22 22 /// Get the Sprk DID 23 23 String get sprkDid; 24 - String get bskyDid => 'did:web:api.bsky.app#bsky_appview'; 25 - String get modDid => 'did:plc:pbgyr67hftvpoqtvaurpsctc#atproto_labeler'; 26 - String get bskyModDid => 'did:plc:ar7c4by46qjdydhdevvrndac#atproto_labeler'; 24 + String get bskyDid; 25 + String get modDid; 26 + String get bskyModDid; 27 27 28 28 ActorRepository get actor; 29 29 RepoRepository get repo;
+12 -4
lib/src/core/network/atproto/data/repositories/sprk_repository_impl.dart
··· 24 24 25 25 /// Client for interacting with Spark API endpoints 26 26 class SprkRepositoryImpl implements SprkRepository { 27 - SprkRepositoryImpl(this._authRepository) : _sprkDid = _getSprkDid() { 27 + SprkRepositoryImpl(this._authRepository) 28 + : _sprkDid = _getSprkDid(), 29 + _bskyDid = _getBskyDid() { 28 30 _logger.d('SprkRepository initialized with DID: $_sprkDid'); 29 31 } 30 32 final AuthRepository _authRepository; 31 33 final String _sprkDid; 34 + final String _bskyDid; 32 35 final SparkLogger _logger = GetIt.instance<LogService>().getLogger( 33 36 'SprkRepository', 34 37 ); ··· 52 55 String get sprkDid => _sprkDid; 53 56 54 57 @override 55 - String get bskyDid => 'did:web:api.bsky.app#bsky_appview'; 58 + String get bskyDid => _bskyDid; 56 59 57 60 @override 58 - String get modDid => 'did:plc:pbgyr67hftvpoqtvaurpsctc#atproto_labeler'; 61 + String get modDid => AppConfig.modDid; 59 62 60 63 @override 61 - String get bskyModDid => 'did:plc:ar7c4by46qjdydhdevvrndac#atproto_labeler'; 64 + String get bskyModDid => AppConfig.bskyModDid; 62 65 63 66 static String _getSprkDid() { 64 67 final sprkAppView = Uri.parse(AppConfig.appViewUrl); 65 68 return 'did:web:${sprkAppView.host}#sprk_appview'; 69 + } 70 + 71 + static String _getBskyDid() { 72 + final bskyAppView = Uri.parse(AppConfig.bskyAppViewUrl); 73 + return 'did:web:${bskyAppView.host}#bsky_appview'; 66 74 } 67 75 68 76 /// Execute API request with token expiration handling
+78
test/src/core/auth/data/repositories/auth_repository_impl_test.dart
··· 519 519 registrationBody['grant_types'], 520 520 containsAll(<String>['authorization_code', 'refresh_token']), 521 521 ); 522 + expect( 523 + registrationBody['scope'] as String, 524 + 'atproto transition:generic', 525 + ); 522 526 return http.Response( 523 527 json.encode({ 524 528 'client_id': 'client-1', ··· 565 569 expect(authUri.queryParameters['login_hint'], 'alice.sprk.so'); 566 570 expect(authUri.queryParameters['code_challenge'], isNotEmpty); 567 571 expect(authUri.queryParameters['state'], isNotEmpty); 572 + expect(authUri.queryParameters['scope'], 'atproto transition:generic'); 568 573 569 574 final callbackUrl = Uri.parse(_redirectUri) 570 575 .replace( ··· 581 586 expect(registrationCalls, 1); 582 587 expect(tokenCalls, 1); 583 588 expect(sessionCalls, 1); 589 + }, 590 + ); 591 + 592 + test( 593 + 'initiateOAuth re-registers when the cached AIP client scope is stale', 594 + () async { 595 + final storage = _InMemoryStorage(); 596 + await _storeSnapshot( 597 + storage, 598 + AuthSnapshot( 599 + aipClientRegistration: const AipClientRegistration( 600 + clientId: 'stale-client', 601 + clientSecret: 'secret-1', 602 + scope: 'atproto', 603 + ), 604 + ), 605 + ); 606 + 607 + var registrationCalls = 0; 608 + final client = MockClient((request) async { 609 + switch (request.url.path) { 610 + case '/.well-known/oauth-authorization-server': 611 + return http.Response( 612 + json.encode({ 613 + 'authorization_endpoint': 614 + 'https://auth.sprk.so/oauth/authorize', 615 + 'token_endpoint': 'https://auth.sprk.so/oauth/token', 616 + 'registration_endpoint': 617 + 'https://auth.sprk.so/oauth/clients/register', 618 + }), 619 + 200, 620 + ); 621 + case '/oauth/clients/register': 622 + registrationCalls += 1; 623 + final registrationBody = 624 + json.decode(request.body) as Map<String, dynamic>; 625 + expect( 626 + registrationBody['scope'] as String, 627 + 'atproto transition:generic', 628 + ); 629 + return http.Response( 630 + json.encode({ 631 + 'client_id': 'client-2', 632 + 'client_secret': 'secret-2', 633 + }), 634 + 201, 635 + ); 636 + default: 637 + return http.Response('unexpected request', 500); 638 + } 639 + }); 640 + 641 + final repository = AuthRepositoryImpl( 642 + secureStorage: storage, 643 + httpClient: client, 644 + logger: SparkLogger(name: 'AuthRepositoryTest'), 645 + ); 646 + 647 + await repository.initializationComplete; 648 + final authUrl = await repository.initiateOAuth('alice.sprk.so'); 649 + final authUri = Uri.parse(authUrl); 650 + 651 + expect(registrationCalls, 1); 652 + expect(authUri.queryParameters['scope'], 'atproto transition:generic'); 653 + 654 + final savedSnapshot = AuthSnapshot.fromJsonString( 655 + (await storage.getString(StorageKeys.account))!, 656 + ); 657 + expect(savedSnapshot.aipClientRegistration?.clientId, 'client-2'); 658 + expect( 659 + savedSnapshot.aipClientRegistration?.scope, 660 + 'atproto transition:generic', 661 + ); 584 662 }, 585 663 ); 586 664