Files
runicgateway.com/PLAY_DATA_SAFETY.md
wtclaude dff4232491
Some checks failed
PR checks / checks (pull_request) Failing after 3m21s
fix(legal): declare report, block and hide in the app's data safety
The app now sends reports, blocks and hides (USER_SAFETY.md step 4,
Android-app#64). A new app row, app-safety, declares them: App activity ->
Other user-generated content (the report's note) and Other actions (the
report, block and hide themselves), collected, not shared, required. Other
actions moves from "not collected" to collected in PLAY_DATA_SAFETY.md.

The deployment row for posted content now says a report keeps a copy of
what it reported, and that blocks and hides are kept on the account and
deleted with it (user_blocks and user_hidden_items cascade).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-30 11:02:56 -05:00

273 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!--
GENERATED FILE — do not edit.
Source: src/data/collection.mjs (scope "app", and the deletion row) + src/data/legal.mjs
Generator: scripts/playDataSafety.mjs
Edit the data file and run `npm run play:datasafety`. CI runs the same
generator with --check, so a hand edit here fails the build rather than
quietly disagreeing with the published privacy policy.
-->
# Google Play Data Safety — the answers, and what they are based on
The Play Console asks, for every category of data, whether the app **collects** it, whether it is **shared**, whether collection is **required or optional**, and *why*. This file holds the answers for the Runic Gateway Android app, generated from the same inventory the published privacy policy renders — see `/privacy`, section 2.
> **This is not a filled-in form.** Play’s definitions change and no check here can read them. Every answer below is a fact about the code with the reasoning attached; read the console’s current wording against them when you fill the form. What this file exists to prevent is somebody answering from memory about what the app stores.
## The premise every answer rests on
We operate **no server the app talks to.** The app ships pointed at nothing: its first screen asks for the address of a Runic Gateway deployment and validates it before anything else in the app runs. That deployment belongs to whoever runs that community. Data therefore travels from the device to *their* server, and there is no endpoint of ours anywhere in the path — not for content, not for telemetry, and not for crash reports, of which there are none.
Play still counts what the app sends to that server as **collected**: the data leaves the device, and the form does not ask whose server it lands on. So everything the app sends is declared collected, not shared, and required, which is the conservative answer and the accurate one (org lead, 2026-10-29). What stays true, and what `/privacy` says, is that none of it reaches us.
## Data collection and security
- **Does the app collect or share any of the required user data types?** Yes, it collects them (below). It shares none.
- **Is all of the user data collected by the app encrypted in transit?** Yes. The release build’s network security config sets `cleartextTrafficPermitted="false"` for every connection, and the app refuses an `http` deployment address outside debug builds. Read from: `app/src/main/res/xml/network_security_config.xml`, `core/net/ServerUrl.kt`.
- **Is any of it processed ephemerally?** No. The deployment keeps what it receives, under its operator’s policy, and we cannot promise otherwise on its behalf.
- **Free text on security practices**, where the console offers it: credentials are held in Android’s encrypted storage (AES-256-GCM via Jetpack Security), and push notifications carry **no content**: a relay receives a stream name and a reference, and the app fetches the actual message over its own authenticated connection.
## Data types the app collects
| Category | Data type | Collected | Shared | Required | Purposes | From |
|---|---|---|---|---|---|---|
| Personal info | Name | Yes | No | Yes | App functionality | Your name, email address and message, if you use Contact |
| Personal info | Email address | Yes | No | Yes | App functionality | Your name, email address and message, if you use Contact |
| Personal info | User IDs | Yes | No | Yes | App functionality; Fraud prevention, security, and compliance; Account management | Your sign-in, and the tokens it issues; Linking a game account |
| Messages | Other in-app messages | Yes | No | Yes | App functionality | Your name, email address and message, if you use Contact; What you post and send from the app |
| App activity | App interactions | Yes | No | Yes | App functionality; Personalization | Which notifications you have read, and which you want |
| App activity | In-app search history | Yes | No | Yes | App functionality | What you search for |
| App activity | Other user-generated content | Yes | No | Yes | App functionality; Fraud prevention, security, and compliance; Personalization | What you post and send from the app; What you report, block or hide |
| App activity | Other actions | Yes | No | Yes | App functionality; Fraud prevention, security, and compliance; Personalization | What you report, block or hide |
| App info and performance | Diagnostics | Yes | No | Yes | App functionality; Fraud prevention, security, and compliance | The app’s version and your Android version |
| Device or other IDs | Device or other IDs | Yes | No | Yes | App functionality; Fraud prevention, security, and compliance; Account management | The trusted-device token and the device’s name, if you asked for one; Push registration, if you turn notifications on |
## Data types the app does not collect
The form lists every type, so every one is answered. Vocabulary read from Play Console Help on 2026-10-29; check it against the console’s current list.
- **Location:** none collected.
- **Personal info:** Address, Phone number, Race and ethnicity, Political or religious beliefs, Sexual orientation, Other info: not collected.
- **Financial info:** none collected.
- **Health and fitness:** none collected.
- **Messages:** Emails, SMS or MMS: not collected.
- **Photos and videos:** none collected.
- **Audio files:** none collected.
- **Files and docs:** none collected.
- **Calendar:** none collected.
- **Contacts:** none collected.
- **App activity:** Installed apps: not collected.
- **Web browsing:** none collected.
- **App info and performance:** Crash logs, Other app performance data: not collected.
## Held on the device only
Not collected, because none of it leaves the phone; listed so nobody declares it by mistake.
- **The address of the deployment you chose.** It never leaves the phone. It is the destination of requests, not the contents of one.
- **A snapshot of your notifications, so the inbox opens without a signal.** The snapshot is written on the phone from data the deployment had already delivered. It is not uploaded anywhere.
- **No analytics, no crash reporting, no advertising.** No crash reporter, no advertising ID, no analytics identifier, and no library that would generate one is linked into the build.
## Each answer, and why it is the truthful one
### Your sign-in, and the tokens it issues
**Personal info → User IDs.** Collected, not shared, required.
Purposes: App functionality; Account management; Fraud prevention, security, and compliance.
Signing in sends your username and password to the deployment, or with single sign-on a one-time code from the provider, plus a two-factor code when the account has one. The deployment answers with an access and a refresh token and the username, role and account id they belong to. The tokens are held in encrypted storage on the device (AES-256-GCM through Jetpack Security) and are sent to exactly one place: the deployment that issued them, with every request. Changing your username or password, setting up two-factor sign-in and generating recovery codes from the app go to the same place. So does deleting the account, which sends your username, your password and, if the account has one, a two-factor code.
- **Why that answer:** The username, password and tokens leave the phone for the deployment the user chose, and Play counts that as collection whoever runs the server. None of it reaches us. Security is a purpose because the deployment checks two-factor codes and trusted devices with it.
- **Retention:** On the device until you sign out or delete the account
- **In detail:** Signing out clears them, and so does deleting the account; uninstalling the app removes them with it.
- **Read from:** `data/api/AuthApi.kt, data/api/SsoApi.kt, data/api/MeApi.kt, core/auth/EncryptedTokenStore.kt`
### The trusted-device token and the device’s name, if you asked for one
**Device or other IDs → Device or other IDs.** Collected, not shared, required.
Purposes: Account management; Fraud prevention, security, and compliance.
Ticking “trust this device” during two-factor sign-in stores an opaque token so the deployment can skip the second factor next time. It lives in its own encrypted store, deliberately separate from the session, because it has to outlive a sign-out to be worth anything — and the deployment holds only a hash of it, so the copy on your phone is the only usable one. The phone’s make and model, such as “Samsung SM-S918B”, goes with the request as the device’s label, so you can tell it apart in the deployment’s list of trusted devices. It is a model name, not a hardware identifier.
- **Why that answer:** The token identifies this device to the deployment, and the label is sent with it. Both are presented to the deployment that minted the token and to nothing else.
- **Retention:** On the device until it expires or you revoke it
- **In detail:** Thirty days, and revocable at any time from the deployment’s Trusted Devices screen, which is also where it can be revoked if the phone is lost. Deleting the account from the app drops it from the phone too, since there is no next sign-in for it to skip.
- **Read from:** `core/auth/EncryptedTrustTokenStore.kt, core/auth/DeviceNameProvider.kt`
### The app’s version and your Android version
**App info and performance → Diagnostics.** Collected, not shared, required.
Purposes: App functionality; Fraud prevention, security, and compliance.
Every request carries a User-Agent such as “RunicGatewayApp/0.6.0 (Android 15)”. It is there because a deployment turns away traffic that looks like a scanner, and it names nothing about you or the phone beyond those two version numbers.
- **Why that answer:** It is technical information about the app and the device, and it is sent with every request. The deployment uses it to tell the app from a scanner.
- **Retention:** Under the deployment operator’s policy
- **In detail:** A deployment’s access log may keep it, as any web server keeps the user agent of a browser.
- **Read from:** `di/NetworkModule.kt, core/net/UserAgentInterceptor.kt`
### The address of the deployment you chose
**No console type.** Not collected. Stored on the device only.
The app ships pointed at nothing and asks for an address on first run. That address is stored in ordinary preferences rather than encrypted storage — it is not a secret, it is the equivalent of a bookmark — and it is what every other screen in the app talks to.
- **Why that answer:** It never leaves the phone. It is the destination of requests, not the contents of one.
- **Retention:** On the device until you change it or uninstall
- **Read from:** `core/prefs/ServerPreferences.kt`
### Push registration, if you turn notifications on
**Device or other IDs → Device or other IDs.** Collected, not shared, required. Mention the relay in the free-text security section if the console asks.
Purposes: App functionality.
Push is off until you enable it. When you do, the app mints a random, unguessable topic name on the notification relay the deployment nominates, and registers that topic’s URL with the deployment so it has somewhere to send a nudge. What actually travels through the relay is content-free — a stream name and a reference, never the message — and the app then fetches the real content over its authenticated connection to the deployment. A leaked topic name therefore reveals nothing, which is the reason the relay needs no account and holds nothing about you.
- **Why that answer:** The topic URL is an address for this device, and the app sends it to the deployment. The relay is the one the deployment nominates to deliver its own notifications, so it carries them on the deployment’s behalf rather than receiving a share. It only ever sees a topic name and a tickle; the app pulls the content itself, authenticated. No hop reaches a server we operate.
- **Retention:** Until you turn push off, sign out, delete the account, or uninstall
- **In detail:** Signing out or disabling push unregisters the device with the deployment and discards the topic; deleting the account drops the registration on the deployment with everything else. The relay retains whatever its own operator configures it to; if the deployment points at a relay it does not run, that relay is a third party to both of us, and it still only ever sees a tickle.
- **Read from:** `core/push/NtfyTopic.kt, core/push/PushPreferences.kt, data/api/NotificationsApi.kt`
### Linking a game account
**Personal info → User IDs.** Collected, not shared, required.
Purposes: App functionality; Account management.
Linking your site account to a game sends the one-time code the game gave you. Where a game server offers it, the app can also create a new game account, which sends the account name and password you chose. Unlinking sends the id being unlinked, such as a Steam id. All of it goes to the deployment, which keeps the link against your site account.
- **Why that answer:** Game account names, Steam ids and link codes identify a player, and the app sends them to the deployment.
- **Retention:** Held by the deployment until you unlink, under its operator’s policy
- **Read from:** `data/api/PlayerShardApi.kt, data/api/PlayerRustApi.kt, data/api/MeApi.kt`
### Your name, email address and message, if you use Contact
**Personal info → Name; Personal info → Email address; Messages → Other in-app messages.** Collected, not shared, required.
Purposes: App functionality.
The Contact screen sends the name, email address and message you type to the deployment, which passes them on to its operator by email so they can reply. Nothing is sent until you press send.
- **Why that answer:** The form’s three fields leave the phone for the deployment, whose operator receives them.
- **Retention:** Held by the deployment’s operator, under their policy
- **In detail:** The message is delivered to the operator’s mailbox, and kept as long as they keep it.
- **Read from:** `data/api/PublicApi.kt, data/api/dto/ContactDto.kt`
### What you post and send from the app
**Messages → Other in-app messages; App activity → Other user-generated content.** Collected, not shared, required.
Purposes: App functionality.
Most of the app is reading. What it sends is what you write, and most of that is for staff: news posts, wiki categories, a broadcast to the game, the answer to a player’s help page, and the account and reason on a kick or a ban. All of it is a live write to the deployment. Apart from the notification snapshot described below, nothing is cached for offline use and nothing is duplicated anywhere else — the app with no signal is an app with almost no content, which is a limitation and also an accurate description of where the data lives.
- **Why that answer:** Posts, broadcasts and replies are written to the community’s own installation. We have no copy, no access and no way to obtain one.
- **Retention:** Held by the deployment, under its operator’s policy
- **Read from:** `data/api/AdminApi.kt`
### What you report, block or hide
**App activity → Other user-generated content; App activity → Other actions.** Collected, not shared, required.
Purposes: App functionality; Fraud prevention, security, and compliance; Personalization.
Long-pressing a chat line offers Report, Hide for me and Block player. A report sends which line it is, the reason you picked and, if you wrote one, a note of up to 500 characters, so the deployment’s staff can review it. Hide for me and Block player send which line it is; the deployment works out who wrote it and keeps the hide or the block on your account, so it applies on the website and on every device. Your list of blocks, and unblocking someone, come from and go to the same place. For staff, Hide for everyone and Show to everyone again send the line too. Nothing is sent until you choose one.
- **Why that answer:** The report’s note is text the user writes, and reporting, blocking and hiding are actions the app sends to the deployment, which keeps them. Reports exist so staff can act on abuse; blocks and hides change what this one reader is shown.
- **Retention:** Held by the deployment, under its operator’s policy
- **In detail:** A block or a hide lasts until you remove it or delete the account, which deletes them. A report stays as a moderation record, with a copy of what the line said, until the operator removes it.
- **Read from:** `data/api/SafetyApi.kt, data/api/dto/SafetyDto.kt, data/api/RustApi.kt`
### Which notifications you have read, and which you want
**App activity → App interactions.** Collected, not shared, required.
Purposes: App functionality; Personalization.
Opening a notification marks it read on the deployment, so the badge agrees on every device you use. Your choice of notification streams, and of which arrive by push or email, is saved there too.
- **Why that answer:** Read state and preferences are actions in the app that are sent to, and kept by, the deployment.
- **Retention:** Held by the deployment, under its operator’s policy
- **Read from:** `data/api/NotificationsApi.kt`
### What you search for
**App activity → In-app search history.** Collected, not shared, required.
Purposes: App functionality.
Searching the wiki, a game’s market or its creature atlas sends what you typed to the deployment, because the deployment does the searching. The app keeps no history of searches.
- **Why that answer:** Search text leaves the phone in the request to the deployment.
- **Retention:** Under the deployment operator’s policy
- **In detail:** The query is part of the request’s address, so a deployment’s access log may record it. That is why it is declared rather than called ephemeral.
- **Read from:** `data/api/PublicApi.kt`
### A snapshot of your notifications, so the inbox opens without a signal
**No console type.** Not collected. Stored on the device only.
The app keeps the most recent notifications it has already fetched — at most thirty, and only the first page — on the device, so opening the inbox shows you what you had rather than a spinner. It is a copy of what the deployment already sent you and it is refreshed from there; nothing is written here that was not read from your own account. It is scoped to the account that fetched it, so a second person signing in on the same phone is never shown the first one’s messages.
- **Why that answer:** The snapshot is written on the phone from data the deployment had already delivered. It is not uploaded anywhere.
- **Retention:** Until you sign out or delete the account, or the thirty are pushed out by newer ones
- **In detail:** Signing out, or deleting the account, deletes the snapshot outright. It lives in the app’s ordinary preference store rather than the encrypted one — sign-in tokens are the thing that store is for — which is worth stating plainly: on a device where someone has root, these are readable, and they are notification bodies rather than credentials.
- **Read from:** `core/inbox/DataStoreInboxCache.kt, data/repository/AuthRepository.kt`
### No analytics, no crash reporting, no advertising
**App info and performance → Crash logs.** Not collected. The advertising ID declaration is "no".
There is no third-party SDK in the app at all — no Firebase, no Crashlytics, no advertising identifier, no measurement library. That is checkable rather than claimed: it is what the dependency list and the manifest say, and a build that gained one would gain permissions with it.
- **Why that answer:** No crash reporter, no advertising ID, no analytics identifier, and no library that would generate one is linked into the build.
- **Retention:** Nothing to retain
- **Read from:** `app/build.gradle.kts, app/src/main/AndroidManifest.xml`
## The rest of the listing
- **Privacy policy URL:** `/privacy` on this site. It is the URL Play is given, and section 2 of it is about the app specifically.
- **Target audience:** adults. The beta is stated as **18 or older** (D31); the app contains no content directed at children and no age verification.
- **Accounts the app can start:** the sign-in screen links to the deployment’s own sign-up page, which opens in the browser, and where a game server offers it the app creates a game account on it. Read from: `ui/auth/LoginViewModel.kt`, `data/api/PlayerShardApi.kt`.
- **Can users delete their account? Yes,** in the app (*My account* → *Delete my account…*) and on the deployment’s website (*Account* → *Delete my account*). It takes the password, and a two-factor code where the account has one, and the account is deleted at once. The account belongs to the deployment the user chose, so it is deleted there; we hold none. Read from: `ui/auth/DeleteAccountScreen.kt`, `data/api/MeApi.kt`.
- **Delete account URL:** `/delete-account/` on this site (D85). It names the app and us, gives the steps in the app and on the website, says what is deleted and what is kept and for how long, and tells someone who cannot sign in to ask their community’s operator, who can delete any account from the admin panel (D86).
- **Deleting some data without deleting the account:** the deployment holds it, so that is a request to its operator. The one list we hold, the beta signup, is erased on request; `/privacy` section 4 says how to ask.
## What an account deletion deletes and keeps
The console asks what is deleted and what is kept. These are the same words `/delete-account/` renders, from the same row of `src/data/collection.mjs`.
**Deleted at once:**
- your sign-in, password, email address, two-factor settings and recovery codes
- every signed-in device, trusted device and push registration
- your linked sign-ins, such as Google, Discord or Steam
- your game-account links, and anything this site granted in the game through them
- your notification settings and inbox
**Kept:**
- What you wrote where others read it, such as forum posts, stays. Other members see it as written by “[deleted player]”. *Until a moderator or the operator removes it, as with any post.*
- Reports you filed and appeals you made stay as moderation records. Staff can still see who it was, and the activity log keeps its IP addresses. *Until the operator removes them.*
- Event results stay with the score and the in-game name, with no site account behind them. *Until the operator removes them.*
- A game account stays on the game server. Only its link to this site goes. *For as long as the game server keeps it, which is up to whoever runs that server.*
- The record of notifications sent to you keeps a one-way hash of your email address, never the address, and no longer names your account. An address that bounced or was reported as spam stays on the suppression list, so it is never mailed again. *The record is swept after a period the operator sets, 180 days by default. The suppression list does not expire.*
- If you were staff, your username can never be used again, by anyone.
## What the website collects, for the same reviewer
Not part of the Data Safety form — that form is about the app — but a reviewer who follows the privacy policy URL lands on a page covering three things, so it is worth knowing which of them the site itself is responsible for:
- **Your email address** — Until the beta ends, or until you ask.
- **The wording you agreed to, and when** — For the life of the row.
- **A one-way hash of your IP address — never the address** — With the row; the rate-limit log is pruned after 48 hours.
- **Your browser’s user-agent string, truncated** — With the row; blanked on removal.
- **The web server’s access log** — Short-term operational retention, then rotated away.
Last generated from data dated 2026-10-30. Regenerate with `npm run play:datasafety` after any change to what the app stores.