diff --git a/crdt_sync_dart/lib/src/firebase_settings.dart b/crdt_sync_dart/lib/src/firebase_settings.dart index 3bbe65a..1bcfc49 100644 --- a/crdt_sync_dart/lib/src/firebase_settings.dart +++ b/crdt_sync_dart/lib/src/firebase_settings.dart @@ -80,7 +80,17 @@ class FirebaseAccount { final email = decoded['email']; final password = decoded['password']; if (email is! String || password is! String) return null; - if (email.isEmpty || password.isEmpty) return null; + // An empty password is legal, and rejecting it was a real bug: both the + // Google one-tap path and the seeded-session path deliberately store + // `password: ''` -- the refresh token, not an address or a password, is + // the credential on those devices. Refusing to parse the blob they had + // just written made every such device read back as "not configured" on + // the next launch, so a signed-in phone fell through to the mirror + // forever. Callers already handle it: each guards `password.isEmpty` and + // passes null, meaning "no password on this device". + // + // An empty *email* stays rejected: it carries no identity at all. + if (email.isEmpty) return null; return FirebaseAccount(email: email, password: password); } diff --git a/crdt_sync_dart/pubspec.yaml b/crdt_sync_dart/pubspec.yaml index 3374161..55c2944 100644 --- a/crdt_sync_dart/pubspec.yaml +++ b/crdt_sync_dart/pubspec.yaml @@ -2,7 +2,7 @@ name: crdt_sync description: >- Shared CRDT merge scheme + GitHub-Contents-API sync transport for cross-device apps. Pure Dart, no Flutter dependency. -version: 0.10.0 +version: 0.11.0 publish_to: 'none' environment: diff --git a/crdt_sync_dart/test/firebase_settings_test.dart b/crdt_sync_dart/test/firebase_settings_test.dart index 386a92b..972cccf 100644 --- a/crdt_sync_dart/test/firebase_settings_test.dart +++ b/crdt_sync_dart/test/firebase_settings_test.dart @@ -67,11 +67,25 @@ void main() { ('a missing password', '{"email":"a@b.c"}'), ('a non-string email', '{"email":1,"password":"p"}'), ('an empty email', '{"email":"","password":"p"}'), - ('an empty password', '{"email":"a@b.c","password":""}'), ]) { test('treats $name as not configured', () { expect(FirebaseAccount.tryParse(raw), isNull); }); } + + test('accepts an empty password, which Google sign-in writes', () { + // Regression: this used to return null, so a device that signed in with + // Google -- or adopted a seeded session -- wrote an account marker it + // could never read back. Every later launch reported "not configured" + // and fell through to the mirror, while a perfectly good refresh token + // sat unused in the keystore beside it. + final parsed = FirebaseAccount.tryParse( + const FirebaseAccount(email: 'a@b.c', password: '').toJsonString(), + ); + + expect(parsed, isNotNull); + expect(parsed!.email, 'a@b.c'); + expect(parsed.password, isEmpty); + }); }); }