diff --git a/README.md b/README.md index 7ddffc3a..2bc8d6a9 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,8 @@ The code follows a feature-first clean architecture with BLoC; [CLAUDE.md](CLAUD | [docs/security.md](docs/security.md) | Token storage, certificate pinning, screen capture protection and the inactivity sign-out | | [docs/crash-reporting.md](docs/crash-reporting.md) | Sentry and what it masks before an event leaves the device | | [docs/development.md](docs/development.md) | Toolchain, test slices, the feature generator and CI | -| [docs/release.md](docs/release.md) | Release signing, the changelog and the upload to Play internal testing | +| [docs/release.md](docs/release.md) | Release signing, the changelog, the upload to Play internal testing, and the store listing answers | +| [docs/privacy-policy.md](docs/privacy-policy.md) | The app's privacy policy, also in Turkish ([docs/privacy-policy.tr.md](docs/privacy-policy.tr.md)) | | [docs/testing-architecture.md](docs/testing-architecture.md) | Test layers, doubles and guard tests | | [docs/mock-architecture.md](docs/mock-architecture.md) | How recorded answers in `assets/mock/` are served | | [docs/feature-first-clean-boundaries.md](docs/feature-first-clean-boundaries.md) | Layer boundaries and dependency rules | diff --git a/android/fastlane/metadata/android/en-US/full_description.txt b/android/fastlane/metadata/android/en-US/full_description.txt new file mode 100644 index 00000000..75c0ecb1 --- /dev/null +++ b/android/fastlane/metadata/android/en-US/full_description.txt @@ -0,0 +1,31 @@ +LibreDB Mobile is the Android client of LibreDB Studio. It lets you work from your phone with the databases that Studio offers you. + +The app signs in to the LibreDB Studio and Keycloak deployment it was built for, so you need an account there from the organisation that runs it. The app never connects to a database itself and holds no database password: Studio keeps every database credential, opens every connection and decides which connections you may use. + +Connections +• See the connections Studio offers you, grouped by environment, and search them. +• Star your favourites and find the ones you opened recently. +• Check whether a connection answers, and switch the open connection from the top bar. + +Browse +• Look inside a connection without changing it: its databases and schemas, the kinds of objects with their counts, and the objects of each kind. +• Open an object's columns, indexes, foreign keys and source, and search the objects by name. +• Browse the keys of a Redis database and open a key's value. + +Rows +• Preview the rows of a table as cards or as a compact table, load more, and open a row's detail. +• Columns whose names look sensitive, such as email, password or card number, are masked on screen with the same default rules as Studio's web grid. + +SQL editor +• Type and run a statement, a script or a selection, in the database you pick, and cancel a statement that is running. +• Your draft and your statement history stay on the phone, and a statement from the history or from Browse opens in the editor. +• Before a statement that may change data runs, the app shows it in full and asks you to confirm; on a production connection you type the connection's name first. + +Privacy and security +• Single sign-on through your organisation's Keycloak; the tokens are kept in the device's secure storage. +• Drafts and history are encrypted on the device, separately for each account. +• Screens that show results are kept out of screenshots and the recent apps view. +• The app signs you out after 15 minutes without input. +• Remove my data, in Settings, deletes what the app keeps for your account. + +The app is available in English and Turkish. diff --git a/android/fastlane/metadata/android/en-US/short_description.txt b/android/fastlane/metadata/android/en-US/short_description.txt new file mode 100644 index 00000000..42182c0d --- /dev/null +++ b/android/fastlane/metadata/android/en-US/short_description.txt @@ -0,0 +1 @@ +Browse your databases and run SQL from your phone, through LibreDB Studio. diff --git a/android/fastlane/metadata/android/en-US/title.txt b/android/fastlane/metadata/android/en-US/title.txt new file mode 100644 index 00000000..a7e7f2b1 --- /dev/null +++ b/android/fastlane/metadata/android/en-US/title.txt @@ -0,0 +1 @@ +LibreDB Mobile diff --git a/android/fastlane/metadata/android/tr-TR/full_description.txt b/android/fastlane/metadata/android/tr-TR/full_description.txt new file mode 100644 index 00000000..dfa6eab1 --- /dev/null +++ b/android/fastlane/metadata/android/tr-TR/full_description.txt @@ -0,0 +1,31 @@ +LibreDB Mobile, LibreDB Studio'nun Android istemcisidir. Studio'nun size sunduğu veritabanlarıyla telefonunuzdan çalışmanızı sağlar. + +Uygulama, derlenirken belirlenen LibreDB Studio ve Keycloak kurulumunda oturum açar; bu yüzden o kurulumu işleten kuruluştan alınmış bir hesabınız olmalıdır. Uygulama hiçbir veritabanına kendisi bağlanmaz ve hiçbir veritabanı parolası tutmaz: her veritabanı kimlik bilgisini Studio saklar, her bağlantıyı Studio açar ve hangi bağlantıları kullanabileceğinize Studio karar verir. + +Bağlantılar +• Studio'nun size sunduğu bağlantıları ortamlarına göre gruplanmış olarak görün ve aralarında arayın. +• Sık kullandıklarınızı yıldızlayın, son açtıklarınızı bulun. +• Bir bağlantının yanıt verip vermediğini sınayın, açık bağlantıyı üst çubuktan değiştirin. + +Gözat +• Bir bağlantının içine hiçbir şeyi değiştirmeden bakın: veritabanları ve şemaları, nesne türleri ve sayıları, her türün nesneleri. +• Bir nesnenin sütunlarını, dizinlerini, yabancı anahtarlarını ve kaynağını açın, nesneleri adlarıyla arayın. +• Bir Redis veritabanının anahtarlarına göz atın ve bir anahtarın değerini açın. + +Satırlar +• Bir tablonun satırlarını kartlar ya da sıkışık bir tablo olarak önizleyin, daha fazlasını yükleyin, bir satırın ayrıntısını açın. +• Adı hassas görünen sütunlar, örneğin e-posta, parola ya da kart numarası, ekranda Studio'nun web tablosundaki varsayılan kurallarla maskelenir. + +SQL düzenleyici +• Seçtiğiniz veritabanında bir deyimi, bir betiği ya da bir seçimi yazıp çalıştırın, çalışan bir deyimi iptal edin. +• Taslağınız ve deyim geçmişiniz telefonda kalır; geçmişteki ya da Gözat'taki bir deyim düzenleyicide açılır. +• Veriyi değiştirebilecek bir deyim çalışmadan önce uygulama onu eksiksiz gösterir ve onayınızı ister; üretim bağlantısında önce bağlantının adını yazarsınız. + +Gizlilik ve güvenlik +• Kuruluşunuzun Keycloak'u üzerinden tek oturum açma; belirteçler cihazın güvenli depolamasında tutulur. +• Taslaklar ve geçmiş cihazda, her hesap için ayrı şifrelenir. +• Sonuç gösteren ekranlar ekran görüntülerinin ve son uygulamalar görünümünün dışında tutulur. +• Uygulama 15 dakika işlem yapılmadığında oturumu kapatır. +• Ayarlar'daki Verilerimi sil, uygulamanın hesabınız için tuttuklarını siler. + +Uygulama İngilizce ve Türkçe kullanılabilir. diff --git a/android/fastlane/metadata/android/tr-TR/short_description.txt b/android/fastlane/metadata/android/tr-TR/short_description.txt new file mode 100644 index 00000000..f37f1f63 --- /dev/null +++ b/android/fastlane/metadata/android/tr-TR/short_description.txt @@ -0,0 +1 @@ +LibreDB Studio üzerinden veritabanlarınıza göz atın, telefondan SQL çalıştırın. diff --git a/android/fastlane/metadata/android/tr-TR/title.txt b/android/fastlane/metadata/android/tr-TR/title.txt new file mode 100644 index 00000000..a7e7f2b1 --- /dev/null +++ b/android/fastlane/metadata/android/tr-TR/title.txt @@ -0,0 +1 @@ +LibreDB Mobile diff --git a/docs/privacy-policy.md b/docs/privacy-policy.md new file mode 100644 index 00000000..627f7585 --- /dev/null +++ b/docs/privacy-policy.md @@ -0,0 +1,89 @@ +# LibreDB Mobile privacy policy + +Last updated: October 24, 2026. + +This policy describes what LibreDB Mobile for Android does with your data. +LibreDB Mobile is a client of LibreDB Studio: it signs you in through Keycloak and shows the databases that Studio offers you. +Questions about this policy go to [info@libredb.org](mailto:info@libredb.org). + +## The servers the app talks to + +The app talks to three kinds of servers, and to no others on its own: + +- **Keycloak**, the sign-in server of the LibreDB deployment the app was built for. + You sign in on Keycloak's own page, in the device's browser, so the app never sees your password. + Keycloak gives the app tokens that identify you. +- **LibreDB Studio** of the same deployment. + The app sends Studio your Keycloak access token with its requests, so Studio knows who you are and what you may use. + The access token carries what Keycloak puts into it, such as your username, your e-mail address and your name. + The app also sends Studio what you ask it to do: the connection you open, the objects you look at and the statements you run. +- **Sentry**, a crash reporting service, but only when the build was made with a Sentry project key. + See the Crash reports section below. + +The organisation that runs the deployment operates Keycloak and Studio, and decides what they record. +For example, Studio can keep an audit log of sign-ins and sign-outs and, depending on its settings, a record of the statements you run. +That organisation's own privacy terms cover what those servers keep. + +The app reaches Keycloak, Studio and Sentry over HTTPS, which encrypts the traffic. + +## What the app does not do + +- The app holds no database password or other database credential: Studio keeps them and opens every database connection. +- The app shows no advertising, contains no advertising, third-party analytics or tracking SDK, and sends nothing about how you use it beyond the crash reports, session records and timing information described in the Crash reports section below. +- The app does not read your contacts, location, photos, files or other apps' data. +- The app does not sell your data or share it with anyone for advertising. + +## What the app keeps on the device + +- **Your session tokens**, in Android's secure storage, encrypted with a key held by the Android Keystore. + Signing out deletes them. +- **Your drafts and statement history**: the editor's draft for each connection and the statements you ran, encrypted on the device with a key for your account alone. + They stay after you sign out, so you find them again when you sign in with the same account, and another account on the device cannot read them. +- **Your connection choices**: the connections you starred or opened recently, the connection you had open last and the database you last used on it, and the sections of the connection list you collapsed. +- **Device preferences**: the language, the theme and whether results show as cards or as a table. + +The app keeps no rows of query results after it closes. +While it runs, it keeps the lists, counts and descriptions of a connection's objects in memory for up to 10 minutes, so going back does not ask Studio again. +What you type into the object search goes only to Studio, and the app keeps no search history. +Android's cloud backup is turned off for the app, and its preferences, drafts and history are left out of device-to-device transfers. + +## Sharing and copying + +The app passes data to another app only when you ask it to. +When you copy a value of a result, the app copies what the screen shows, so a masked value is copied as its mask. +When you share an object's source, such as a view's definition, the system's share sheet sends it to the app you choose. +A source longer than 100,000 characters is shared as a file, and that file can stay in the app's temporary storage until Android clears it or you uninstall the app. + +## Crash reports + +A build made with a Sentry project key sends a report to Sentry when the app runs into an error. +A report holds the error and where in the app it happened, the app's version, technical details of the device such as its model, Android version, language, free memory and battery level, a random identifier of the installation, and a trail of the events before the error, such as the screens you opened and the app going to the background. +A build with a Sentry project key also sends a session record each time you use the app: when the session started and ended, how long it lasted, whether it ended in a crash, and the installation identifier. +It also sends timing information for about one in five app starts and screen changes, to show where the app is slow. +Before a report leaves the device, the app removes e-mail addresses, tokens, passwords and other credentials from it. +A report carries no screenshot, no layout of the screen, no request body and no screen recording of the session. +Sentry receives your device's IP address with each report, as with any connection to a server. +Sentry derives an approximate location, such as a city and a country, from that address, unless the Sentry project is set not to store IP addresses. +A build made without a Sentry project key sends no reports. + +## Protections on the device + +- The screens that show query results, a row's detail, a key's value or an object's source are kept out of screenshots, screen recordings and the recent apps view. +- Columns whose names look sensitive, such as e-mail, password or card number, are masked on screen. +- The app signs you out after 15 minutes without input. + +## Deleting your data + +- **Remove my data**, in the Privacy section of Settings, deletes what the app keeps for the signed-in account (the drafts, the history and the connection choices) and then signs you out. + It keeps the language, the theme and the result view preference, which belong to the device. +- **Uninstalling the app** deletes everything it keeps on the device. +- Crash reports already sent stay in Sentry until they expire; write to [info@libredb.org](mailto:info@libredb.org) to have them deleted sooner. +- Data that Keycloak or Studio keep about you is held by the organisation that runs them; ask that organisation to delete it. + +## Children + +The app is a tool for people who work with databases and is not directed at children. + +## Changes to this policy + +When the app starts handling data in a new way, this policy is updated before that version is released, and the date at the top changes. diff --git a/docs/privacy-policy.tr.md b/docs/privacy-policy.tr.md new file mode 100644 index 00000000..911ebc40 --- /dev/null +++ b/docs/privacy-policy.tr.md @@ -0,0 +1,89 @@ +# LibreDB Mobile gizlilik politikası + +Son güncelleme: 24 Ekim 2026. + +Bu politika, Android için LibreDB Mobile'ın verilerinizle ne yaptığını anlatır. +LibreDB Mobile, LibreDB Studio'nun bir istemcisidir: Keycloak üzerinden oturumunuzu açar ve Studio'nun size sunduğu veritabanlarını gösterir. +Bu politikayla ilgili sorularınızı [info@libredb.org](mailto:info@libredb.org) adresine yazabilirsiniz. + +## Uygulamanın konuştuğu sunucular + +Uygulama üç tür sunucuyla konuşur, kendiliğinden başka hiçbir sunucuyla konuşmaz: + +- **Keycloak**, uygulamanın derlenirken bağlandığı LibreDB kurulumunun oturum açma sunucusu. + Oturumu Keycloak'un kendi sayfasında, cihazın tarayıcısında açarsınız; bu yüzden uygulama parolanızı hiç görmez. + Keycloak uygulamaya sizi tanıtan belirteçler verir. +- Aynı kurulumun **LibreDB Studio**'su. + Uygulama isteklerinde Studio'ya Keycloak erişim belirtecinizi gönderir; böylece Studio kim olduğunuzu ve neyi kullanabileceğinizi bilir. + Erişim belirteci, Keycloak'un içine koyduklarını taşır: örneğin kullanıcı adınızı, e-posta adresinizi ve adınızı. + Uygulama Studio'ya ondan istediklerinizi de gönderir: açtığınız bağlantıyı, baktığınız nesneleri ve çalıştırdığınız deyimleri. +- **Sentry**, bir çökme raporlama hizmeti; ancak yalnızca uygulama bir Sentry proje anahtarıyla derlendiyse. + Ayrıntılar aşağıdaki Çökme raporları bölümünde. + +Keycloak ile Studio'yu kurulumu işleten kuruluş çalıştırır ve bu sunucuların neyi kaydedeceğine o kuruluş karar verir. +Örneğin Studio, oturum açma ve kapatmaların bir denetim kaydını ve ayarlarına göre çalıştırdığınız deyimlerin bir kaydını tutabilir. +Bu sunucuların tuttuklarına o kuruluşun kendi gizlilik koşulları uygulanır. + +Uygulama Keycloak'a, Studio'ya ve Sentry'ye trafiği şifreleyen HTTPS üzerinden ulaşır. + +## Uygulamanın yapmadıkları + +- Uygulama hiçbir veritabanı parolası ya da başka bir veritabanı kimlik bilgisi tutmaz: bunları Studio saklar ve her veritabanı bağlantısını Studio açar. +- Uygulama reklam göstermez, hiçbir reklam, üçüncü taraf analiz ya da izleme SDK'sı içermez ve uygulamayı nasıl kullandığınıza dair aşağıdaki Çökme raporları bölümünde anlatılan çökme raporları, oturum kayıtları ve süre bilgisinin ötesinde hiçbir şey göndermez. +- Uygulama kişilerinizi, konumunuzu, fotoğraflarınızı, dosyalarınızı ya da başka uygulamaların verilerini okumaz. +- Uygulama verilerinizi satmaz ve reklam amacıyla kimseyle paylaşmaz. + +## Uygulamanın cihazda tuttukları + +- **Oturum belirteçleriniz**, Android'in güvenli depolamasında, Android Keystore'un tuttuğu bir anahtarla şifrelenmiş olarak. + Oturumu kapatmak onları siler. +- **Taslaklarınız ve deyim geçmişiniz**: düzenleyicinin her bağlantı için taslağı ve çalıştırdığınız deyimler, cihazda yalnızca hesabınıza ait bir anahtarla şifrelenmiş olarak. + Oturumu kapattıktan sonra da kalırlar; böylece aynı hesapla yeniden oturum açtığınızda onları bulursunuz ve cihazdaki başka bir hesap onları okuyamaz. +- **Bağlantı seçimleriniz**: yıldızladığınız ya da son açtığınız bağlantılar, en son açık olan bağlantı ve onda en son kullandığınız veritabanı, bağlantı listesinde daralttığınız bölümler. +- **Cihaz tercihleri**: dil, tema ve sonuçların kart mı tablo mu gösterileceği. + +Uygulama kapandıktan sonra sorgu sonuçlarının hiçbir satırını tutmaz. +Uygulama çalışırken bir bağlantının nesnelerinin listelerini, sayılarını ve tanımlarını, geri dönüldüğünde Studio'ya yeniden sorulmasın diye en çok 10 dakika bellekte tutar. +Nesne aramasına yazdıklarınız yalnızca Studio'ya gider ve uygulama arama geçmişi tutmaz. +Uygulamanın Android bulut yedeklemesi kapalıdır; tercihleri, taslakları ve geçmişi cihazdan cihaza aktarımların dışında bırakılır. + +## Paylaşma ve kopyalama + +Uygulama veriyi başka bir uygulamaya yalnızca siz istediğinizde verir. +Bir sonucun bir değerini kopyaladığınızda uygulama ekranda görüneni kopyalar; maskelenmiş bir değer maskesiyle kopyalanır. +Bir nesnenin kaynağını, örneğin bir görünümün tanımını paylaştığınızda sistemin paylaşım penceresi onu seçtiğiniz uygulamaya gönderir. +100.000 karakterden uzun bir kaynak dosya olarak paylaşılır ve bu dosya, Android temizleyene ya da uygulamayı kaldırana kadar uygulamanın geçici depolamasında kalabilir. + +## Çökme raporları + +Bir Sentry proje anahtarıyla derlenmiş uygulama, bir hatayla karşılaştığında Sentry'ye bir rapor gönderir. +Bir rapor şunları içerir: hatayı ve uygulamanın neresinde olduğunu, uygulamanın sürümünü, cihazın modeli, Android sürümü, dili, boş belleği ve pil düzeyi gibi teknik ayrıntılarını, kuruluma ait rastgele bir tanımlayıcıyı ve hatadan önceki olayların bir izini, örneğin açtığınız ekranları ve uygulamanın arka plana geçişini. +Bir Sentry proje anahtarıyla derlenmiş uygulama ayrıca uygulamayı her kullanışınızda bir oturum kaydı gönderir: oturumun ne zaman başlayıp bittiği, ne kadar sürdüğü, bir çökmeyle bitip bitmediği ve kurulum tanımlayıcısı. +Ayrıca, uygulamanın nerede yavaş olduğunu göstermek için uygulama açılışlarının ve ekran değişikliklerinin yaklaşık beşte biri için süre bilgisi gönderir. +Bir rapor cihazdan çıkmadan önce uygulama içinden e-posta adreslerini, belirteçleri, parolaları ve diğer kimlik bilgilerini çıkarır. +Bir rapor ekran görüntüsü, ekran düzeni, istek gövdesi ya da oturumun ekran kaydını (görüntü kaydı) taşımaz. +Sentry, her sunucu bağlantısında olduğu gibi, her raporla birlikte cihazınızın IP adresini alır. +Sentry projesi IP adreslerini saklamayacak şekilde ayarlanmadıkça Sentry bu adresten şehir ve ülke gibi yaklaşık bir konum çıkarır. +Sentry proje anahtarı olmadan derlenmiş bir uygulama rapor göndermez. + +## Cihazdaki korumalar + +- Sorgu sonuçlarını, bir satırın ayrıntısını, bir anahtarın değerini ya da bir nesnenin kaynağını gösteren ekranlar ekran görüntülerinin, ekran kayıtlarının ve son uygulamalar görünümünün dışında tutulur. +- Adı hassas görünen sütunlar, örneğin e-posta, parola ya da kart numarası, ekranda maskelenir. +- Uygulama 15 dakika işlem yapılmadığında oturumunuzu kapatır. + +## Verilerinizi silmek + +- Ayarlar'ın Gizlilik bölümündeki **Verilerimi sil**, uygulamanın oturum açmış hesap için tuttuklarını (taslakları, geçmişi ve bağlantı seçimlerini) siler, ardından oturumunuzu kapatır. + Cihaza ait olan dili, temayı ve sonuç görünümü tercihini korur. +- **Uygulamayı kaldırmak** cihazda tuttuğu her şeyi siler. +- Gönderilmiş çökme raporları süreleri dolana kadar Sentry'de kalır; daha önce silinmeleri için [info@libredb.org](mailto:info@libredb.org) adresine yazın. +- Keycloak'un ya da Studio'nun sizinle ilgili tuttuğu verileri onları işleten kuruluş tutar; silinmeleri için o kuruluşa başvurun. + +## Çocuklar + +Uygulama veritabanlarıyla çalışan kişiler için bir araçtır ve çocuklara yönelik değildir. + +## Bu politikadaki değişiklikler + +Uygulama veriyi yeni bir şekilde işlemeye başladığında bu politika o sürüm yayımlanmadan önce güncellenir ve en üstteki tarih değişir. diff --git a/docs/release.md b/docs/release.md index 930f1832..7568ebb5 100644 --- a/docs/release.md +++ b/docs/release.md @@ -145,6 +145,91 @@ The `release-bundle` artifact of each run keeps the exact bundle and mapping for Play requires new apps and app updates to target Android 16 (API level 36) from August 31, 2026 ([Play Console Help, target API level requirements](https://support.google.com/googleplay/android-developer/answer/11926878)). The app's `targetSdk` is `flutter.targetSdkVersion`, which is 36 in Flutter 3.47.7, so it meets the requirement; check it again with each Flutter upgrade. +## Store listing + +The texts the Play Console asks for live in the repository, in English and Turkish, so the maintainer copies them instead of writing them. +The App Store listing reuses them (#199). + +### Listing texts + +`android/fastlane/metadata/android/en-US/` and `tr-TR/` hold `title.txt`, `short_description.txt` and `full_description.txt` in the fastlane metadata layout, so a later pipeline step can upload them. +`test/architecture/play_listing_metadata_test.dart` fails when a text is missing, empty or longer than Play allows (30, 80 and 4000 characters), or when the two locales do not hold the same files. +The texts say only what the app does today, and that it signs in to the deployment it was built for; change them in the same pull request as a feature they name. +The release notes of each version come from the generated `CHANGELOG.md` and are never written by hand. +The screenshots, the 512 px icon and the 1024 by 500 feature graphic are added during the phone walkthrough (#196), from the tool/studio-dev sample data only. + +### Privacy policy + +[privacy-policy.md](privacy-policy.md) and [privacy-policy.tr.md](privacy-policy.tr.md) are the policy; the maintainer publishes them at a URL of their choice and enters that URL in the Play Console. +Each sentence states something the app does, with its source in [security.md](security.md), [crash-reporting.md](crash-reporting.md), [architecture.md](architecture.md), `lib/features/object_browser/data/repositories/cached_object_browser_repository.dart`, `NativeSentry.kt` with the sentry-android defaults it keeps, or `android/app/src/main/AndroidManifest.xml` and its backup rules. +Update both files and their date in the pull request that changes what the app keeps or sends, and publish them again before that version ships, for example: + +- when #202 removes the temporary file of a shared source, drop the sentence that says it can stay; +- when the iOS build ships (#199), the policy has to cover iOS, whose backups and crash reports differ. + +### Data safety + +The answers below are for a store build with `SENTRY_DSN` set; without it, every row marked "Sentry" is answered No. +The store build signs in to the deployment the maintainer runs, so what the app sends to that Keycloak and Studio counts as collected by the developer. +Sentry processes crash reports on the developer's behalf, which Play does not count as sharing. + +Overview: + +| Question | Answer | Why | +| --- | --- | --- | +| Does the app collect or share any of the required user data types? | Yes | The access token, the statements sent to Studio, and the Sentry reports | +| Is all user data encrypted in transit? | Yes | `OIDC_ISSUER` must be https (`lib/infrastructure/config/auth_config.dart`), `PROD_API_URL` is the deployment's https URL, and the Sentry DSN is https | +| Account creation | The app does not allow users to create an account | Accounts are made in the deployment's Keycloak by the organisation; the app only signs in | +| Can users request that their data is deleted? | Yes | Remove my data in Settings, uninstalling, and info@libredb.org for crash reports | + +The app does not check that `PROD_API_URL` is https yet ([#218](https://github.com/libredb/libredb-mobile/issues/218), before 1.0.0), so the maintainer keeps the release variable on https; the encryption answer depends on it. + +Data types (none is shared, none is processed ephemerally, and none is optional): + +| Category | Data type | Collected | Purposes | Source | +| --- | --- | --- | --- | --- | +| Personal info | Name | Yes | App functionality, Account management | The access token Studio receives carries what the `profile` scope gives, `OIDC_SCOPE` defaulting to `openid profile email` | +| Personal info | Email address | Yes | App functionality, Account management | The same token; Studio names the user by `email` or `preferred_username` ([architecture.md](architecture.md)) | +| Personal info | User IDs | Yes | App functionality, Account management | The same token's subject and username | +| App activity | Other user-generated content | Yes | App functionality | The statements sent to Studio, which Studio may record in its audit and query logs | +| App activity | App interactions | Yes (Sentry) | Analytics | The navigation breadcrumbs of a crash report ([crash-reporting.md](crash-reporting.md)), and the session record sent for each use, since sentry-android 8.61.0 keeps `enableAutoSessionTracking` on and `NativeSentry.kt` does not change it | +| App info and performance | Crash logs | Yes (Sentry) | Analytics | The crash reports | +| App info and performance | Diagnostics | Yes (Sentry) | Analytics | The device context of a report, the session records (start, end, duration, crashed or not) and the traces of `tracesSampleRate = 0.2` (`lib/app/bootstrap/sentry_start.dart`) | +| Device or other IDs | Device or other IDs | Yes (Sentry) | Analytics | sentry-android 8.61.0 sets the user id of every event to a random installation id when the app sets none (`DefaultAndroidEventProcessor.mergeUser`), and the app sets none | +| Location | Approximate location | Yes (Sentry), until the setting below is on | Analytics | Sentry derives a place from the client IP at ingestion ([crash-reporting.md](crash-reporting.md)) | + +Approximate location becomes No once 'Prevent Storing of IP Addresses' is on in the Sentry project ([crash-reporting.md](crash-reporting.md), the user's steps). +Every other data type is No: precise location, financial info, health and fitness, messages, photos and videos, audio, files and docs, calendar, contacts, web browsing, and the other app activity and performance types. +The app keeps its drafts and history on the device, which Play does not count as collection. +Object search terms go only to the deployment's Studio, as part of the request, and the app keeps no search history. + +### Content rating + +The questionnaire's answers, for the category of utility and productivity apps: + +- No violence, fear, sexual content, crude humour, profanity, drugs, alcohol, tobacco or gambling. +- Users cannot talk to each other or exchange content through the app, and the app shares no user's location with other users. +- No digital purchases and no unrestricted web browsing. +- The app shows the content of the organisation's own databases; it supplies no content of its own, and the review deployment holds sample data only. + +Other declarations: the app contains no ads, its target audience is 18 and over, it is not a news or government app, and it has no financial or health features. + +### App access + +Every screen needs a sign-in, so the app access entry is "All or some functionality is restricted". +The maintainer creates the demo account in the review deployment's Keycloak, with a Studio role that reaches the sample connections only, and enters its username and password in that entry of the Play Console; neither is ever written into the repository, an issue or a pull request. +The instructions for the entry, which name no host and no password: + +```text +LibreDB Mobile is a client of LibreDB Studio, so every screen needs a sign-in. +This build signs in to the publisher's own LibreDB deployment, which holds sample databases only; no VPN, code or other setup is needed. +1. Open the app and tap "Sign in with SSO". +2. On the sign-in page that opens in the browser, enter the username and password given in this entry. +3. The Connections tab lists the sample connections; tap one to open it. +4. The Browse tab shows its objects; open a table and tap "View rows" to see its rows. Columns with sensitive names, such as email, are masked. +5. The SQL tab runs statements on a SQL connection, for example SELECT 1. A statement that may change data asks for confirmation before it runs. +``` + ## Actions Every action in `release.yml` and `release-please.yml` is pinned to a full commit SHA with its version in a comment, and `test/architecture/android_release_setup_test.dart` fails on a tag or branch reference. diff --git a/test/architecture/play_listing_metadata_test.dart b/test/architecture/play_listing_metadata_test.dart new file mode 100644 index 00000000..164d0e7c --- /dev/null +++ b/test/architecture/play_listing_metadata_test.dart @@ -0,0 +1,115 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; + +/// Guards the Play store listing texts in `android/fastlane/metadata/android/` (issue #197). +/// +/// The texts sit in the fastlane metadata layout, so a later pipeline step can upload them: one directory per locale, +/// each with `title.txt`, `short_description.txt` and `full_description.txt`. Play refuses a listing whose title is +/// longer than 30 characters, whose short description is longer than 80 or whose full description is longer than +/// 4000, so this test fails before the Console would. Play counts characters, not bytes, so a Turkish `ş` counts as +/// one. +/// +/// The English and the Turkish listing must hold the same files, so a text added to one locale is not forgotten in the +/// other. +void main() { + test('each locale has the three listing texts within Play\'s lengths', () { + final problems = [for (final locale in _locales) ...listingProblems(Directory('$_metadataRoot/$locale'))]; + + expect(problems, isEmpty, reason: problems.join('\n')); + }); + + test('the listing has exactly the en-US and tr-TR locales, holding the same files', () { + final root = Directory(_metadataRoot); + expect(root.existsSync(), isTrue, reason: '$_metadataRoot does not exist'); + + final locales = [for (final entry in root.listSync().whereType()) _name(entry.path)]..sort(); + expect(locales, _locales); + + final english = relativeFiles(Directory('$_metadataRoot/en-US')); + final turkish = relativeFiles(Directory('$_metadataRoot/tr-TR')); + expect(english, isNotEmpty); + expect(turkish, english, reason: 'en-US and tr-TR must hold the same files'); + }); + + group('listingProblems', () { + late Directory temp; + + setUp(() => temp = Directory.systemTemp.createTempSync('play_listing_')); + tearDown(() => temp.deleteSync(recursive: true)); + + void write(String name, String text) => File('${temp.path}/$name').writeAsStringSync(text); + + test('accepts texts at the limits, counting characters rather than bytes', () { + write('title.txt', 'ş' * 30); + write('short_description.txt', 'ğ' * 80); + write('full_description.txt', 'ü' * 4000); + + expect(listingProblems(temp), isEmpty); + }); + + test('names a text one character past its limit', () { + write('title.txt', 'a' * 31); + write('short_description.txt', 'b' * 81); + write('full_description.txt', 'c' * 4001); + + expect(listingProblems(temp), [ + '${temp.path}/title.txt has 31 characters, Play allows 30', + '${temp.path}/short_description.txt has 81 characters, Play allows 80', + '${temp.path}/full_description.txt has 4001 characters, Play allows 4000', + ]); + }); + + test('names a missing or empty text', () { + write('title.txt', ' \n'); + + expect(listingProblems(temp), [ + '${temp.path}/title.txt is empty', + '${temp.path}/short_description.txt is missing', + '${temp.path}/full_description.txt is missing', + ]); + }); + + test('does not count the final line break of a file', () { + write('title.txt', '${'a' * 30}\n'); + write('short_description.txt', '${'b' * 80}\r\n'); + write('full_description.txt', 'c'); + + expect(listingProblems(temp), isEmpty); + }); + }); +} + +const _metadataRoot = 'android/fastlane/metadata/android'; +const _locales = ['en-US', 'tr-TR']; + +/// Play's limit for each listing text, in characters. +const listingLimits = {'title.txt': 30, 'short_description.txt': 80, 'full_description.txt': 4000}; + +/// The problems of the listing texts in [locale]: a text that is missing, empty, or longer than Play allows. +List listingProblems(Directory locale) => [ + for (final MapEntry(key: name, value: limit) in listingLimits.entries) + ...?_textProblem('${locale.path}/$name', limit), +]; + +List? _textProblem(String path, int limit) { + final file = File(path); + if (!file.existsSync()) return ['$path is missing']; + final text = file.readAsStringSync().replaceFirst(RegExp(r'\r?\n$'), ''); + if (text.trim().isEmpty) return ['$path is empty']; + final length = text.runes.length; + if (length > limit) return ['$path has $length characters, Play allows $limit']; + return null; +} + +/// Every file under [locale], as a sorted list of paths relative to it. +List relativeFiles(Directory locale) { + if (!locale.existsSync()) return const []; + final prefix = '${locale.path.replaceAll('\\', '/')}/'; + return [ + for (final file in locale.listSync(recursive: true).whereType()) + file.path.replaceAll('\\', '/').substring(prefix.length), + ]..sort(); +} + +String _name(String path) => path.replaceAll('\\', '/').split('/').last;