[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.

fix: don't assume client_id on dpop

+66 -8
+12 -3
lib/src/core/auth/data/models/aip_session_response.dart
··· 14 14 required this.scopes, 15 15 required this.pdsEndpoint, 16 16 required this.expiresAt, 17 + this.clientId, 17 18 this.dpopKey, 18 19 this.dpopJwk, 19 20 }); ··· 32 33 (json['expires_at'] as int) * 1000, 33 34 isUtc: true, 34 35 ), 36 + clientId: json['client_id'] as String?, 35 37 dpopKey: json['dpop_key'] as String?, 36 38 dpopJwk: json['dpop_jwk'] == null 37 39 ? null ··· 46 48 final List<String> scopes; 47 49 final String pdsEndpoint; 48 50 final DateTime expiresAt; 51 + final String? clientId; 49 52 final String? dpopKey; 50 53 final AipDpopJwk? dpopJwk; 51 54 } ··· 88 91 PdsSessionCache buildPdsSessionCacheFromAipResponse( 89 92 AipAtprotocolSessionResponse response, { 90 93 String dpopNonce = '', 94 + String? clientId, 91 95 }) { 92 96 final dpopJwk = response.dpopJwk; 93 97 if (dpopJwk == null || dpopJwk.d == null || dpopJwk.d!.isEmpty) { ··· 96 100 ); 97 101 } 98 102 99 - final clientId = extractClientIdFromAccessToken(response.accessToken); 100 - if (clientId == null || clientId.isEmpty) { 103 + final resolvedClientId = 104 + response.clientId ?? 105 + clientId ?? 106 + extractClientIdFromAccessToken(response.accessToken); 107 + if (resolvedClientId == null || resolvedClientId.isEmpty) { 101 108 throw const AipExportedSessionException( 102 109 'AIP-exported token is incompatible with direct-PDS mode: missing client_id.', 103 110 ); ··· 111 118 pdsEndpoint: response.pdsEndpoint, 112 119 scope: response.scopes.join(' '), 113 120 dpopNonce: dpopNonce, 121 + clientId: resolvedClientId, 114 122 publicKey: encodeDpopPublicKey(dpopJwk.x, dpopJwk.y), 115 123 privateKey: encodeDpopPrivateKey(dpopJwk.d!), 116 124 ); 117 125 } 118 126 119 127 OAuthSession restorePdsOAuthSessionFromCache(PdsSessionCache cache) { 120 - final clientId = extractClientIdFromAccessToken(cache.accessToken); 128 + final clientId = 129 + cache.clientId ?? extractClientIdFromAccessToken(cache.accessToken); 121 130 if (clientId == null || clientId.isEmpty) { 122 131 throw const AipExportedSessionException( 123 132 'AIP-exported token is incompatible with direct-PDS mode: missing client_id.',
+10 -1
lib/src/core/auth/data/models/auth_snapshot.dart
··· 148 148 required this.pdsEndpoint, 149 149 required this.scope, 150 150 required this.dpopNonce, 151 + this.clientId, 151 152 required this.publicKey, 152 153 required this.privateKey, 153 154 }); 154 155 155 156 factory PdsSessionCache.fromJson(Map<String, dynamic> json) { 157 + final accessToken = json['accessToken'] as String; 156 158 return PdsSessionCache( 157 - accessToken: json['accessToken'] as String, 159 + accessToken: accessToken, 158 160 expiresAt: json['expiresAt'] as String, 159 161 did: json['did'] as String, 160 162 handle: json['handle'] as String, 161 163 pdsEndpoint: json['pdsEndpoint'] as String, 162 164 scope: json['scope'] as String, 163 165 dpopNonce: json['dpopNonce'] as String? ?? '', 166 + clientId: json['clientId'] as String?, 164 167 publicKey: json['publicKey'] as String, 165 168 privateKey: json['privateKey'] as String, 166 169 ); ··· 173 176 final String pdsEndpoint; 174 177 final String scope; 175 178 final String dpopNonce; 179 + final String? clientId; 176 180 final String publicKey; 177 181 final String privateKey; 178 182 ··· 187 191 'pdsEndpoint': pdsEndpoint, 188 192 'scope': scope, 189 193 'dpopNonce': dpopNonce, 194 + 'clientId': clientId, 190 195 'publicKey': publicKey, 191 196 'privateKey': privateKey, 192 197 }; ··· 200 205 String? pdsEndpoint, 201 206 String? scope, 202 207 String? dpopNonce, 208 + Object? clientId = _missingValue, 203 209 String? publicKey, 204 210 String? privateKey, 205 211 }) { ··· 211 217 pdsEndpoint: pdsEndpoint ?? this.pdsEndpoint, 212 218 scope: scope ?? this.scope, 213 219 dpopNonce: dpopNonce ?? this.dpopNonce, 220 + clientId: identical(clientId, _missingValue) 221 + ? this.clientId 222 + : clientId as String?, 214 223 publicKey: publicKey ?? this.publicKey, 215 224 privateKey: privateKey ?? this.privateKey, 216 225 );
+7
lib/src/core/auth/data/repositories/auth_repository_impl.dart
··· 238 238 pdsEndpoint: pdsEndpoint, 239 239 scope: oauthSession.scope, 240 240 dpopNonce: oauthSession.$dPoPNonce, 241 + clientId: 242 + oauthSession.$clientId ?? 243 + extractClientIdFromAccessToken(oauthSession.accessToken), 241 244 publicKey: oauthSession.$publicKey, 242 245 privateKey: oauthSession.$privateKey, 243 246 ), ··· 661 664 final pdsSession = buildPdsSessionCacheFromAipResponse( 662 665 sessionResponse, 663 666 dpopNonce: existingNonce, 667 + clientId: _aipAtprotocolClientId, 664 668 ); 665 669 666 670 _snapshot = (_snapshot ?? const AuthSnapshot()).copyWith( ··· 682 686 return false; 683 687 } 684 688 } 689 + 690 + String get _aipAtprotocolClientId => 691 + _aipBaseUri.resolve('/oauth-client-metadata.json').toString(); 685 692 686 693 Future<bool> _refreshAuthState() async { 687 694 final inFlight = _refreshInFlight;
+35
test/src/core/auth/data/models/aip_session_response_test.dart
··· 51 51 expect(restored.$privateKey, normalizedCache.privateKey); 52 52 }); 53 53 54 + test('restores with exported client_id when token omits the claim', () { 55 + final cache = buildPdsSessionCacheFromAipResponse( 56 + _sessionResponse( 57 + accessToken: _jwt(clientId: null), 58 + clientId: 'https://auth.sprk.so/oauth-client-metadata.json', 59 + ), 60 + ); 61 + 62 + final restored = restorePdsOAuthSessionFromCache(cache); 63 + 64 + expect( 65 + restored.$clientId, 66 + 'https://auth.sprk.so/oauth-client-metadata.json', 67 + ); 68 + }); 69 + 70 + test( 71 + 'restores with caller-provided client_id when token omits the claim', 72 + () { 73 + final cache = buildPdsSessionCacheFromAipResponse( 74 + _sessionResponse(accessToken: _jwt(clientId: null)), 75 + clientId: 'https://auth.sprk.so/oauth-client-metadata.json', 76 + ); 77 + 78 + final restored = restorePdsOAuthSessionFromCache(cache); 79 + 80 + expect( 81 + restored.$clientId, 82 + 'https://auth.sprk.so/oauth-client-metadata.json', 83 + ); 84 + }, 85 + ); 86 + 54 87 test('rejects responses without private DPoP key material', () { 55 88 final response = _sessionResponse( 56 89 accessToken: _jwt(clientId: 'spark-client'), ··· 88 121 89 122 AipAtprotocolSessionResponse _sessionResponse({ 90 123 required String accessToken, 124 + String? clientId, 91 125 String? d = 'AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI', 92 126 String x = 'AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE', 93 127 String y = 'AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM', ··· 100 134 scopes: const ['atproto'], 101 135 pdsEndpoint: 'https://pds.sprk.so', 102 136 expiresAt: DateTime.utc(2030, 1, 1), 137 + clientId: clientId, 103 138 dpopKey: 'did:key:test', 104 139 dpopJwk: AipDpopJwk(kty: 'EC', crv: 'P-256', x: x, y: y, d: d), 105 140 );
+2 -4
test/src/core/auth/data/repositories/auth_repository_impl_test.dart
··· 688 688 case '/api/atprotocol/session': 689 689 sessionCalls += 1; 690 690 return http.Response( 691 - json.encode( 692 - _sessionResponseBody(_pdsJwt(clientId: 'client-1')), 693 - ), 691 + json.encode(_sessionResponseBody(_pdsJwt(clientId: null))), 694 692 200, 695 693 ); 696 694 default: ··· 884 882 }; 885 883 } 886 884 887 - String _pdsJwt({required String clientId, DateTime? exp}) { 885 + String _pdsJwt({required String? clientId, DateTime? exp}) { 888 886 final payload = <String, Object?>{ 889 887 'sub': 'did:plc:test', 890 888 'exp': (exp ?? DateTime.utc(2030, 1, 1)).millisecondsSinceEpoch ~/ 1000,