Compare commits
13 Commits
338cc6f3de
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f38dec2ac4 | |||
| 080f9827b8 | |||
| 7ac19a791c | |||
| b8306dcdaf | |||
| 08a4973bde | |||
| 8b8896f54b | |||
| e5ec81b172 | |||
| e0b2885cd7 | |||
| c1b38081f5 | |||
| dff4232491 | |||
| caab3476eb | |||
| 70b31302b7 | |||
| 094c606238 |
@@ -157,6 +157,21 @@ jobs:
|
|||||||
docker push "${IMAGE}:latest"
|
docker push "${IMAGE}:latest"
|
||||||
docker push "${IMAGE}:${TAG}"
|
docker push "${IMAGE}:${TAG}"
|
||||||
|
|
||||||
|
- name: Remove this run's image and cap the build cache
|
||||||
|
# The build runs on the HOST daemon, so whatever it leaves stays on the
|
||||||
|
# runner host for good: every run used to leave two tagged images, and
|
||||||
|
# each newer `latest` turned the previous one into a permanent `sha-` image
|
||||||
|
# until the host ran out of disk. Both tags are in the registry by now, so
|
||||||
|
# the local copies are dead weight. BuildKit keeps its layer cache apart
|
||||||
|
# from the images, so the next build stays fast; the cache is pruned to a
|
||||||
|
# cap instead of growing forever (`--reserved-space` is the same flag's
|
||||||
|
# name on newer Docker). Runs even after a failure, and never fails the job.
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
docker image rm "${IMAGE}:latest" "${IMAGE}:${TAG}" 2>/dev/null || true
|
||||||
|
docker builder prune -f --keep-storage 5GB \
|
||||||
|
|| docker builder prune -f --reserved-space 5GB || true
|
||||||
|
|
||||||
- name: Log out
|
- name: Log out
|
||||||
if: always()
|
if: always()
|
||||||
run: docker logout "${REGISTRY}" || true
|
run: docker logout "${REGISTRY}" || true
|
||||||
@@ -207,3 +222,11 @@ jobs:
|
|||||||
echo "::error::The site did not become healthy within 150s."
|
echo "::error::The site did not become healthy within 150s."
|
||||||
docker compose logs --tail 100 site
|
docker compose logs --tail 100 site
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
|
- name: Remove the image this deploy replaced
|
||||||
|
# The pull moves `latest` to the new image and leaves the old one untagged
|
||||||
|
# on the host. Untagged images are never used again (a rollback pulls a
|
||||||
|
# `sha-` tag from the registry), so drop them. Dangling images only: nothing
|
||||||
|
# tagged and nothing a container is using is touched.
|
||||||
|
if: always()
|
||||||
|
run: docker image prune -f || true
|
||||||
|
|||||||
32
PLAN.md
32
PLAN.md
@@ -264,6 +264,7 @@ somewhere other than the thing it decided. The count of record is **eighty-four*
|
|||||||
| D80–D81 | §16, "How phase 18 built it" | A name in a screenshot is a role; the Rust captures' players are mock data |
|
| D80–D81 | §16, "How phase 18 built it" | A name in a screenshot is a role; the Rust captures' players are mock data |
|
||||||
| D82–D83 | §15, "How the site reaches it" | `/beta` lists the demo's sign-ins as the demo publishes them; a demo deep link names its game and shows only for the games the mounted `demoGames` lists |
|
| D82–D83 | §15, "How the site reaches it" | `/beta` lists the demo's sign-ins as the demo publishes them; a demo deep link names its game and shows only for the games the mounted `demoGames` lists |
|
||||||
| D84 | §9, "How phase 6 built the legal pages" | The Data Safety answers say "collected, not shared" for everything the app sends, amending D33 |
|
| D84 | §9, "How phase 6 built the legal pages" | The Data Safety answers say "collected, not shared" for everything the app sends, amending D33 |
|
||||||
|
| D85–D86 | §9, "`/delete-account/`" | The deletion page is linked from the footer, `/privacy` and `/app/`; someone who cannot sign in is sent to their community's operator, never to us |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -955,6 +956,8 @@ deliberately so — no fetch can tell whether an APK runs. `checkFacts.mjs` asse
|
|||||||
still exist and that `minSdk` still says what "Android 10 or newer" claims, so the link is correct
|
still exist and that `minSdk` still says what "Android 10 or newer" claims, so the link is correct
|
||||||
the moment a working build flips the boolean. The panel is not removed while the link is off: a page
|
the moment a working build flips the boolean. The panel is not removed while the link is off: a page
|
||||||
that omitted sideloading would read, to somebody who knows the APK exists, as a page hiding it.
|
that omitted sideloading would read, to somebody who knows the APK exists, as a page hiding it.
|
||||||
|
*(2026-10-31: the link is back on. Android `v0.7.0`'s APK was installed on an Android 16 emulator,
|
||||||
|
connected to the public demo and read its Rust servers, so `serviceable` is `true`.)*
|
||||||
|
|
||||||
**Three mechanisms this phase added that the plan did not anticipate.**
|
**Three mechanisms this phase added that the plan did not anticipate.**
|
||||||
|
|
||||||
@@ -1086,6 +1089,34 @@ form's security questions. D33's test now asserts the part of the premise that i
|
|||||||
**nothing is shared**. `/privacy` section 2 keeps saying none of it reaches us, and `lastUpdated`
|
**nothing is shared**. `/privacy` section 2 keeps saying none of it reaches us, and `lastUpdated`
|
||||||
moved with it. Report, block and hide (Android M8 step 3) adds its rows when it lands.
|
moved with it. Report, block and hide (Android M8 step 3) adds its rows when it lands.
|
||||||
|
|
||||||
|
### `/delete-account/` (2026-10-30)
|
||||||
|
|
||||||
|
Play needs one account-deletion address from whoever publishes the app. The platform gained
|
||||||
|
self-service deletion on the website and in the app (`docs/website/ACCOUNT_DELETION.md`, AD1), and
|
||||||
|
this page is the address Play is given (AD2, §4.5 there). It cannot delete anything: we hold no
|
||||||
|
account, so it explains where the button is in the app and on the website, what goes and what stays
|
||||||
|
and for how long, and who to ask when you cannot sign in.
|
||||||
|
|
||||||
|
**The facts are one row.** What a deletion takes is the `deploy-account-deletion` row in
|
||||||
|
`src/data/collection.mjs`, whose `deletion` field holds the website's own words for what goes and
|
||||||
|
what stays (`client/src/lib/accountDeletion.js`, which the app's strings repeat) plus how long each
|
||||||
|
kept thing is kept, which Play asks and the screens do not say. `/privacy` section 3 renders the row
|
||||||
|
as prose, the page renders its lists, and `PLAY_DATA_SAFETY.md` prints them under the console's
|
||||||
|
deletion questions, so the three cannot disagree. `deletionRow()` throws when the row is missing,
|
||||||
|
and `test/legal.test.mjs` fails a kept entry with no retention. The app rows that deletion touches
|
||||||
|
(the session, the trust token, push and the inbox snapshot) say so, and `lastUpdated` moved.
|
||||||
|
|
||||||
|
**D85 — The page is linked from the footer, `/privacy` and `/app/`** (org lead, 2026-10-30). The
|
||||||
|
footer's legal bar reads Privacy · Terms · Delete account, because a player looking for it looks
|
||||||
|
where the privacy link is; `/privacy` sections 2 and 3 and the bottom of `/app/`'s "Two ways to get
|
||||||
|
it" link to it as well.
|
||||||
|
|
||||||
|
**D86 — Someone who cannot sign in is sent to their community's operator, never to us** (org lead,
|
||||||
|
2026-10-30). The page offers "Forgot your password?" first, which works when the account has an
|
||||||
|
email address and the site can send mail, and then the operator, who can delete any account from the
|
||||||
|
admin panel. It says plainly that we cannot do it: we hold no account and have no access to any
|
||||||
|
community's site. Offering our contact address to help find an operator was considered and declined.
|
||||||
|
|
||||||
**What phase 6 also closed.** `/privacy/` and `/terms/` were the last two entries in
|
**What phase 6 also closed.** `/privacy/` and `/terms/` were the last two entries in
|
||||||
`checkLinks.mjs`'s `PLANNED_ROUTES`; building them emptied the list, and its reverse check is what
|
`checkLinks.mjs`'s `PLANNED_ROUTES`; building them emptied the list, and its reverse check is what
|
||||||
forced the deletion. The list itself stays, because §10's documentation routes land in phases 7 and 8
|
forced the deletion. The list itself stays, because §10's documentation routes land in phases 7 and 8
|
||||||
@@ -1111,6 +1142,7 @@ Organised by what a reader is trying to do. A reader should never need to know t
|
|||||||
| `/beta/` | The closed-beta signup (§8). The one page that handles its own POST (D28) |
|
| `/beta/` | The closed-beta signup (§8). The one page that handles its own POST (D28) |
|
||||||
| `/community/` | Discord (`discord.gg/t2Jav8yT4g`) as the front door, the Gitea org for code and contributions, the `brand.json` contact address for vulnerabilities (D13) — the split in §14 N3 |
|
| `/community/` | Discord (`discord.gg/t2Jav8yT4g`) as the front door, the Gitea org for code and contributions, the `brand.json` contact address for vulnerabilities (D13) — the split in §14 N3 |
|
||||||
| `/privacy/`, `/terms/` | §9 |
|
| `/privacy/`, `/terms/` | §9 |
|
||||||
|
| `/delete-account/` | §9: how to delete an account you use in the app, and what it keeps. The address Play is given (D85, D86) |
|
||||||
|
|
||||||
**Feature grouping**, using project terminology:
|
**Feature grouping**, using project terminology:
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<!--
|
<!--
|
||||||
GENERATED FILE — do not edit.
|
GENERATED FILE — do not edit.
|
||||||
|
|
||||||
Source: src/data/collection.mjs (scope "app") + src/data/legal.mjs
|
Source: src/data/collection.mjs (scope "app", and the deletion row) + src/data/legal.mjs
|
||||||
Generator: scripts/playDataSafety.mjs
|
Generator: scripts/playDataSafety.mjs
|
||||||
|
|
||||||
Edit the data file and run `npm run play:datasafety`. CI runs the same
|
Edit the data file and run `npm run play:datasafety`. CI runs the same
|
||||||
@@ -38,7 +38,8 @@ Play still counts what the app sends to that server as **collected**: the data l
|
|||||||
| 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 |
|
| 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 | 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 | In-app search history | Yes | No | Yes | App functionality | What you search for |
|
||||||
| App activity | Other user-generated content | Yes | No | Yes | App functionality | What you post and send from the app |
|
| 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 |
|
| 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 |
|
| 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 |
|
||||||
|
|
||||||
@@ -56,7 +57,7 @@ The form lists every type, so every one is answered. Vocabulary read from Play C
|
|||||||
- **Files and docs:** none collected.
|
- **Files and docs:** none collected.
|
||||||
- **Calendar:** none collected.
|
- **Calendar:** none collected.
|
||||||
- **Contacts:** none collected.
|
- **Contacts:** none collected.
|
||||||
- **App activity:** Installed apps, Other actions: not collected.
|
- **App activity:** Installed apps: not collected.
|
||||||
- **Web browsing:** none collected.
|
- **Web browsing:** none collected.
|
||||||
- **App info and performance:** Crash logs, Other app performance data: not collected.
|
- **App info and performance:** Crash logs, Other app performance data: not collected.
|
||||||
|
|
||||||
@@ -76,11 +77,11 @@ Not collected, because none of it leaves the phone; listed so nobody declares it
|
|||||||
|
|
||||||
Purposes: App functionality; Account management; Fraud prevention, security, and compliance.
|
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.
|
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.
|
- **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
|
- **Retention:** On the device until you sign out or delete the account
|
||||||
- **In detail:** Signing out clears them; uninstalling the app removes them with it.
|
- **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`
|
- **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
|
### The trusted-device token and the device’s name, if you asked for one
|
||||||
@@ -93,7 +94,7 @@ Ticking “trust this device” during two-factor sign-in stores an opaque token
|
|||||||
|
|
||||||
- **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.
|
- **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
|
- **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.
|
- **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`
|
- **Read from:** `core/auth/EncryptedTrustTokenStore.kt, core/auth/DeviceNameProvider.kt`
|
||||||
|
|
||||||
### The app’s version and your Android version
|
### The app’s version and your Android version
|
||||||
@@ -128,8 +129,8 @@ 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.
|
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.
|
- **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, or uninstall
|
- **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. 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.
|
- **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`
|
- **Read from:** `core/push/NtfyTopic.kt, core/push/PushPreferences.kt, data/api/NotificationsApi.kt`
|
||||||
|
|
||||||
### Linking a game account
|
### Linking a game account
|
||||||
@@ -169,6 +170,19 @@ Most of the app is reading. What it sends is what you write, and most of that is
|
|||||||
- **Retention:** Held by the deployment, under its operator’s policy
|
- **Retention:** Held by the deployment, under its operator’s policy
|
||||||
- **Read from:** `data/api/AdminApi.kt`
|
- **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
|
### Which notifications you have read, and which you want
|
||||||
|
|
||||||
**App activity → App interactions.** Collected, not shared, required.
|
**App activity → App interactions.** Collected, not shared, required.
|
||||||
@@ -201,8 +215,8 @@ Searching the wiki, a game’s market or its creature atlas sends what you typed
|
|||||||
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.
|
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.
|
- **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 the thirty are pushed out by newer ones
|
- **Retention:** Until you sign out or delete the account, or the thirty are pushed out by newer ones
|
||||||
- **In detail:** Signing out 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.
|
- **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`
|
- **Read from:** `core/inbox/DataStoreInboxCache.kt, data/repository/AuthRepository.kt`
|
||||||
|
|
||||||
### No analytics, no crash reporting, no advertising
|
### No analytics, no crash reporting, no advertising
|
||||||
@@ -219,9 +233,31 @@ There is no third-party SDK in the app at all — no Firebase, no Crashlytics, n
|
|||||||
|
|
||||||
- **Privacy policy URL:** `/privacy` on this site. It is the URL Play is given, and section 2 of it is about the app specifically.
|
- **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.
|
- **Target audience:** adults. The beta is stated as **18 or older** (D31); the app contains no content directed at children and no age verification.
|
||||||
- **Account deletion:** the app creates no account with us — an account belongs to the deployment the user chose, and is deleted there. The only list we hold is the beta signup, which is erased on request; `/privacy` section 4 says how to ask.
|
- **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`.
|
||||||
- **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. The platform has no self-service account deletion today; a deployment’s admin deletes accounts from its admin panel. 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`.
|
||||||
- **Data deletion request URL:** the contact address published on `/privacy`, which is read from the mounted `brand.json` rather than typed anywhere in the source (D13).
|
- **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
|
## What the website collects, for the same reviewer
|
||||||
|
|
||||||
@@ -233,4 +269,4 @@ Not part of the Data Safety form — that form is about the app — but a review
|
|||||||
- **Your browser’s user-agent string, truncated** — With the row; blanked on removal.
|
- **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.
|
- **The web server’s access log** — Short-term operational retention, then rotated away.
|
||||||
|
|
||||||
Last generated from data dated 2026-10-29. Regenerate with `npm run play:datasafety` after any change to what the app stores.
|
Last generated from data dated 2026-10-30. Regenerate with `npm run play:datasafety` after any change to what the app stores.
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import path from 'node:path';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
collectedIn,
|
collectedIn,
|
||||||
|
deletionRow,
|
||||||
PLAY_DATA_TYPES,
|
PLAY_DATA_TYPES,
|
||||||
PLAY_PURPOSES,
|
PLAY_PURPOSES,
|
||||||
PLAY_VOCABULARY_READ,
|
PLAY_VOCABULARY_READ,
|
||||||
@@ -94,7 +95,9 @@ function render() {
|
|||||||
lines.push('<!--');
|
lines.push('<!--');
|
||||||
lines.push(' GENERATED FILE — do not edit.');
|
lines.push(' GENERATED FILE — do not edit.');
|
||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push(` Source: src/data/collection.mjs (scope "app") + src/data/legal.mjs`);
|
lines.push(
|
||||||
|
` Source: src/data/collection.mjs (scope "app", and the deletion row) + src/data/legal.mjs`
|
||||||
|
);
|
||||||
lines.push(` Generator: ${GENERATOR}`);
|
lines.push(` Generator: ${GENERATOR}`);
|
||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push(' Edit the data file and run `npm run play:datasafety`. CI runs the same');
|
lines.push(' Edit the data file and run `npm run play:datasafety`. CI runs the same');
|
||||||
@@ -235,22 +238,50 @@ function render() {
|
|||||||
`- **Target audience:** adults. The beta is stated as **${legal.minimumAge} or older** ` +
|
`- **Target audience:** adults. The beta is stated as **${legal.minimumAge} or older** ` +
|
||||||
'(D31); the app contains no content directed at children and no age verification.'
|
'(D31); the app contains no content directed at children and no age verification.'
|
||||||
);
|
);
|
||||||
lines.push(
|
|
||||||
'- **Account deletion:** the app creates no account with us — an account belongs to ' +
|
|
||||||
'the deployment the user chose, and is deleted there. The only list we hold is the ' +
|
|
||||||
'beta signup, which is erased on request; `/privacy` section 4 says how to ask.'
|
|
||||||
);
|
|
||||||
lines.push(
|
lines.push(
|
||||||
'- **Accounts the app can start:** the sign-in screen links to the deployment’s own ' +
|
'- **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 ' +
|
'sign-up page, which opens in the browser, and where a game server offers it the app ' +
|
||||||
'creates a game account on it. The platform has no self-service account ' +
|
'creates a game account on it. Read from: `ui/auth/LoginViewModel.kt`, ' +
|
||||||
'deletion today; a deployment’s admin deletes accounts from its admin panel. Read ' +
|
'`data/api/PlayerShardApi.kt`.'
|
||||||
'from: `ui/auth/LoginViewModel.kt`, `data/api/PlayerShardApi.kt`.'
|
|
||||||
);
|
);
|
||||||
lines.push(
|
lines.push(
|
||||||
'- **Data deletion request URL:** the contact address published on `/privacy`, which ' +
|
'- **Can users delete their account? Yes,** in the app (*My account* → *Delete my ' +
|
||||||
'is read from the mounted `brand.json` rather than typed anywhere in the source (D13).'
|
'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`.'
|
||||||
);
|
);
|
||||||
|
lines.push(
|
||||||
|
'- **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).'
|
||||||
|
);
|
||||||
|
lines.push(
|
||||||
|
'- **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.'
|
||||||
|
);
|
||||||
|
lines.push('');
|
||||||
|
|
||||||
|
/* --------------------------------------------------------------------------------- */
|
||||||
|
const deletion = deletionRow().deletion;
|
||||||
|
lines.push('## What an account deletion deletes and keeps');
|
||||||
|
lines.push('');
|
||||||
|
lines.push(
|
||||||
|
'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`.'
|
||||||
|
);
|
||||||
|
lines.push('');
|
||||||
|
lines.push('**Deleted at once:**');
|
||||||
|
lines.push('');
|
||||||
|
for (const item of deletion.goes) lines.push(`- ${cell(item)}`);
|
||||||
|
lines.push('');
|
||||||
|
lines.push('**Kept:**');
|
||||||
|
lines.push('');
|
||||||
|
for (const entry of deletion.stays) lines.push(`- ${cell(entry.what)} *${cell(entry.keep)}*`);
|
||||||
|
lines.push(`- ${cell(deletion.staff)}`);
|
||||||
lines.push('');
|
lines.push('');
|
||||||
|
|
||||||
lines.push('## What the website collects, for the same reviewer');
|
lines.push('## What the website collects, for the same reviewer');
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import platform from '../data/platform.json';
|
|||||||
* in the legal bar at the foot rather than in the columns: a legal link is not a thing a
|
* in the legal bar at the foot rather than in the columns: a legal link is not a thing a
|
||||||
* reader browses to alongside Features, it is a thing they go looking for, and the line
|
* reader browses to alongside Features, it is a thing they go looking for, and the line
|
||||||
* that already carries the licence and the copyright is where people look.
|
* that already carries the licence and the copyright is where people look.
|
||||||
|
* `/delete-account/` joined them for the same reason (D85): it is the address Google Play
|
||||||
|
* is given, and a player looking for it looks where the privacy link is.
|
||||||
*/
|
*/
|
||||||
// `/beta` renders per request, so the footer it gets must read the mount rather than the
|
// `/beta` renders per request, so the footer it gets must read the mount rather than the
|
||||||
// value baked at build time. See `renderBrand` in src/lib/brand.mjs (phase 11).
|
// value baked at build time. See `renderBrand` in src/lib/brand.mjs (phase 11).
|
||||||
@@ -59,6 +61,7 @@ const isExternal = (href: string) => href.startsWith('http');
|
|||||||
<span class="site-footer__links">
|
<span class="site-footer__links">
|
||||||
<a href="/privacy/">Privacy</a>
|
<a href="/privacy/">Privacy</a>
|
||||||
<a href="/terms/">Terms</a>
|
<a href="/terms/">Terms</a>
|
||||||
|
<a href="/delete-account/">Delete account</a>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p class="site-footer__meta">
|
<p class="site-footer__meta">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: Moderation
|
title: Moderation
|
||||||
description: Three screens that do three different jobs — Discord moderation, content reports, and appeals against a sanction.
|
description: Three screens that do three different jobs — Discord moderation, content reports, and appeals against a sanction — and what members can do for themselves.
|
||||||
---
|
---
|
||||||
|
|
||||||
import { Aside } from '@astrojs/starlight/components';
|
import { Aside } from '@astrojs/starlight/components';
|
||||||
@@ -11,7 +11,7 @@ not overlap.
|
|||||||
| Screen | Is about | Comes from |
|
| Screen | Is about | Comes from |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **Moderation** | Your **Discord** server — bans, kicks, mutes, warnings, joins, leaves, filter and spam hits | the bot, captured live |
|
| **Moderation** | Your **Discord** server — bans, kicks, mutes, warnings, joins, leaves, filter and spam hits | the bot, captured live |
|
||||||
| **Reports** | **Team forum content** members have reported | the site |
|
| **Reports** | **Content** members have reported — Team forum threads and posts, and a game's chat lines where its module offers them | the site |
|
||||||
| **Appeals** | Sanctions people are asking you to reverse | the site |
|
| **Appeals** | Sanctions people are asking you to reverse | the site |
|
||||||
|
|
||||||
## Moderation (Discord)
|
## Moderation (Discord)
|
||||||
@@ -29,13 +29,20 @@ browsing the window.
|
|||||||
|
|
||||||
## Reports
|
## Reports
|
||||||
|
|
||||||
Reports raised by members about Team forum content. Two design decisions show through in
|
Reports raised by members about Team forum threads and posts and, where a game module offers
|
||||||
how this screen behaves:
|
it, the game's chat lines — the Rust module does, from the `⋯` menu on a chat line on the
|
||||||
|
site and a long press in the [app](/app/). Each report keeps a copy of what the item said when it was
|
||||||
|
reported, so an author editing it afterwards does not change what staff read. Three design
|
||||||
|
decisions show through in how this screen behaves:
|
||||||
|
|
||||||
- **They come to site staff, and a Team's own leaders never see them.** A leader moderates
|
- **They come to site staff, and a Team's own leaders never see them.** A leader moderates
|
||||||
their own forum, so a report *about a leader* has to reach someone above them.
|
their own forum, so a report *about a leader* has to reach someone above them.
|
||||||
- **Handling a report records a decision about the report.** It does not touch the content:
|
- **A report changes nothing on its own.** Filing one must never be a way to hide
|
||||||
hiding or removing a post is done in the forum, or as a sanction against the account.
|
something, so the reported item stays where it is until staff act.
|
||||||
|
- **Acting is a separate, deliberate step.** *Hide for everyone* on a report hides the item
|
||||||
|
from everybody except staff, who still see it marked *Hidden by staff* and can show it
|
||||||
|
again, and it records the report as actioned. Removing content outright, or sanctioning the
|
||||||
|
account, is still done in the forum or against the account.
|
||||||
|
|
||||||
The filters are *Open*, *Reviewing*, *Actioned*, *Dismissed* and *All*, and the count of
|
The filters are *Open*, *Reviewing*, *Actioned*, *Dismissed* and *All*, and the count of
|
||||||
open reports sits at the top so the screen is glanceable.
|
open reports sits at the top so the screen is glanceable.
|
||||||
@@ -46,6 +53,19 @@ that never empties stops being read, and the reporter's next report is the one t
|
|||||||
matters.
|
matters.
|
||||||
</Aside>
|
</Aside>
|
||||||
|
|
||||||
|
## What members do for themselves
|
||||||
|
|
||||||
|
Two tools need no staff at all, and nothing about them reaches the queue:
|
||||||
|
|
||||||
|
- **Hide for me** takes one item out of that member's own view, on every device.
|
||||||
|
- **Block** takes everything a person writes — chat lines and forum content — out of the
|
||||||
|
blocker's view, on the site and in the app. The blocked person is not told. Staff cannot be
|
||||||
|
blocked, and a block whose target later becomes staff stops applying and says so. A game
|
||||||
|
player who has not linked a site account is blocked by their game identity, which turns
|
||||||
|
into a block on their account if they link one later.
|
||||||
|
|
||||||
|
A member undoes either from **Account → Blocked**.
|
||||||
|
|
||||||
## Appeals
|
## Appeals
|
||||||
|
|
||||||
An appeal is a request to reverse a sanction, filtered by *Open*, *Pending*, *Under
|
An appeal is a request to reverse a sanction, filtered by *Open*, *Pending*, *Under
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ with six tabs.
|
|||||||
|
|
||||||
| Tab | What it shows |
|
| Tab | What it shows |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Feed** | Kills, chat, comings and goings, the server's own story, RunicNPC bosses, and Rust's world events — cargo, helicopters, airdrops — as they happen |
|
| **Feed** | Kills, chat, comings and goings, the server's own story, RunicNPC bosses, and Rust's world events — cargo, helicopters, airdrops — as they happen. A signed-in member can report a chat line, hide it for themself or block the player from its `⋯` menu, and staff can hide a line for everyone ([Moderation](/docs/administration/moderation/)) |
|
||||||
| **Leaderboard** | Per wipe: kills, deaths, NPC kills, structures, time played, and the chat titles players have earned |
|
| **Leaderboard** | Per wipe: kills, deaths, NPC kills, structures, time played, and the chat titles players have earned |
|
||||||
| **Online** | Who is on right now |
|
| **Online** | Who is on right now |
|
||||||
| **Map** | The [live map](/docs/rust/the-live-map/): the server's own map picture, with layers |
|
| **Map** | The [live map](/docs/rust/the-live-map/): the server's own map picture, with layers |
|
||||||
|
|||||||
@@ -128,6 +128,22 @@ export const appFeatures = [
|
|||||||
'Visible to the account they belong to and to nobody else.',
|
'Visible to the account they belong to and to nobody else.',
|
||||||
gate: 'A game module',
|
gate: 'A game module',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Report, block and hide',
|
||||||
|
body:
|
||||||
|
'Long-press a chat line to report it to the site’s staff, hide it for yourself, or ' +
|
||||||
|
'block the player. A block is kept on your account, so it applies on the website and ' +
|
||||||
|
'on every device too; staff cannot be blocked, and Account → Blocked undoes any of ' +
|
||||||
|
'it. Staff get Hide for everyone on the same menu.',
|
||||||
|
gate: 'A game module whose chat lines can be reported (Rust)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Deleting your account',
|
||||||
|
body:
|
||||||
|
'From My account, with your password and, where the account has one, a two-factor ' +
|
||||||
|
'code. The account is deleted on the deployment at once — see what that removes and ' +
|
||||||
|
'what it keeps on the delete-account page.',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -75,6 +75,16 @@
|
|||||||
* @property {Retention} retention
|
* @property {Retention} retention
|
||||||
* @property {string} source The file this was read out of, repo-relative.
|
* @property {string} source The file this was read out of, repo-relative.
|
||||||
* @property {PlayMapping} [play]
|
* @property {PlayMapping} [play]
|
||||||
|
* @property {Deletion} [deletion] The one row `/delete-account/` renders. See that row.
|
||||||
|
*
|
||||||
|
* @typedef {object} Kept
|
||||||
|
* @property {string} what What stays, and who sees what of it.
|
||||||
|
* @property {string} keep For how long. Play asks for the retention of what stays.
|
||||||
|
*
|
||||||
|
* @typedef {object} Deletion
|
||||||
|
* @property {string[]} goes What goes with the account, in the deployment's own words.
|
||||||
|
* @property {Kept[]} stays
|
||||||
|
* @property {string} staff Said of a staff account only (AD9).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -253,10 +263,13 @@ export const collected = [
|
|||||||
'storage on the device (AES-256-GCM through Jetpack Security) and are sent to ' +
|
'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 ' +
|
'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 ' +
|
'your username or password, setting up two-factor sign-in and generating recovery ' +
|
||||||
'codes from the app go to the same place.',
|
'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.',
|
||||||
retention: {
|
retention: {
|
||||||
summary: 'On the device until you sign out',
|
summary: 'On the device until you sign out or delete the account',
|
||||||
detail: 'Signing out clears them; uninstalling the app removes them with it.',
|
detail:
|
||||||
|
'Signing out clears them, and so does deleting the account; uninstalling the app ' +
|
||||||
|
'removes them with it.',
|
||||||
},
|
},
|
||||||
source:
|
source:
|
||||||
'data/api/AuthApi.kt, data/api/SsoApi.kt, data/api/MeApi.kt, core/auth/EncryptedTokenStore.kt',
|
'data/api/AuthApi.kt, data/api/SsoApi.kt, data/api/MeApi.kt, core/auth/EncryptedTokenStore.kt',
|
||||||
@@ -295,7 +308,9 @@ export const collected = [
|
|||||||
summary: 'On the device until it expires or you revoke it',
|
summary: 'On the device until it expires or you revoke it',
|
||||||
detail:
|
detail:
|
||||||
'Thirty days, and revocable at any time from the deployment’s Trusted Devices ' +
|
'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.',
|
'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.',
|
||||||
},
|
},
|
||||||
source: 'core/auth/EncryptedTrustTokenStore.kt, core/auth/DeviceNameProvider.kt',
|
source: 'core/auth/EncryptedTrustTokenStore.kt, core/auth/DeviceNameProvider.kt',
|
||||||
play: {
|
play: {
|
||||||
@@ -372,10 +387,11 @@ export const collected = [
|
|||||||
'authenticated connection to the deployment. A leaked topic name therefore reveals ' +
|
'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.',
|
'nothing, which is the reason the relay needs no account and holds nothing about you.',
|
||||||
retention: {
|
retention: {
|
||||||
summary: 'Until you turn push off, sign out, or uninstall',
|
summary: 'Until you turn push off, sign out, delete the account, or uninstall',
|
||||||
detail:
|
detail:
|
||||||
'Signing out or disabling push unregisters the device with the deployment and ' +
|
'Signing out or disabling push unregisters the device with the deployment and ' +
|
||||||
'discards the topic. The relay retains whatever its own operator configures it to; ' +
|
'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 ' +
|
'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.',
|
'to both of us, and it still only ever sees a tickle.',
|
||||||
},
|
},
|
||||||
@@ -483,6 +499,51 @@ export const collected = [
|
|||||||
'We have no copy, no access and no way to obtain one.',
|
'We have no copy, no access and no way to obtain one.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
/*
|
||||||
|
* Report, block and hide (USER_SAFETY.md §4.3), on Android-app#64 rather than the edge
|
||||||
|
* commit the section header names. Read out of `data/api/SafetyApi.kt` and
|
||||||
|
* `data/api/dto/SafetyDto.kt`: a report sends the item's type and id, a reason and an
|
||||||
|
* optional note; a block and a hide send the item's type and id; unblocking sends the
|
||||||
|
* block's id. No request names the person being blocked — the deployment works out who
|
||||||
|
* wrote the line — so none of it is a User ID.
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
id: 'app-safety',
|
||||||
|
scope: 'app',
|
||||||
|
title: 'What you report, block or hide',
|
||||||
|
body:
|
||||||
|
'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.',
|
||||||
|
retention: {
|
||||||
|
summary: 'Held by the deployment, under its operator’s policy',
|
||||||
|
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.',
|
||||||
|
},
|
||||||
|
source: 'data/api/SafetyApi.kt, data/api/dto/SafetyDto.kt, data/api/RustApi.kt',
|
||||||
|
play: {
|
||||||
|
types: [
|
||||||
|
{ category: 'App activity', type: 'Other user-generated content' },
|
||||||
|
{ category: 'App activity', type: 'Other actions' },
|
||||||
|
],
|
||||||
|
collected: true,
|
||||||
|
shared: false,
|
||||||
|
required: true,
|
||||||
|
purposes: ['App functionality', 'Fraud prevention, security, and compliance', 'Personalization'],
|
||||||
|
answer: 'Collected, not shared, required.',
|
||||||
|
because:
|
||||||
|
'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.',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'app-interactions',
|
id: 'app-interactions',
|
||||||
scope: 'app',
|
scope: 'app',
|
||||||
@@ -545,9 +606,9 @@ export const collected = [
|
|||||||
'second person signing in on the same phone is never shown the first one’s ' +
|
'second person signing in on the same phone is never shown the first one’s ' +
|
||||||
'messages.',
|
'messages.',
|
||||||
retention: {
|
retention: {
|
||||||
summary: 'Until you sign out, or the thirty are pushed out by newer ones',
|
summary: 'Until you sign out or delete the account, or the thirty are pushed out by newer ones',
|
||||||
detail:
|
detail:
|
||||||
'Signing out deletes the snapshot outright. It lives in the app’s ordinary ' +
|
'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 ' +
|
'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 ' +
|
'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 ' +
|
'has root, these are readable, and they are notification bodies rather than ' +
|
||||||
@@ -607,6 +668,84 @@ export const collected = [
|
|||||||
retention: { summary: 'Set by the operator; nothing expires on its own' },
|
retention: { summary: 'Set by the operator; nothing expires on its own' },
|
||||||
source: 'website server/db/schema.sql — users, user_identities',
|
source: 'website server/db/schema.sql — users, user_identities',
|
||||||
},
|
},
|
||||||
|
/*
|
||||||
|
* The facts `/delete-account/` is built from (ACCOUNT_DELETION.md §4.5), which is the page
|
||||||
|
* Google Play is given. A row here, rather than a list on that page, so `/privacy` and the
|
||||||
|
* deletion page cannot disagree about what a deletion takes.
|
||||||
|
*
|
||||||
|
* `goes` and the first four `stays` are the website's own words, from
|
||||||
|
* `client/src/lib/accountDeletion.js` (WHAT_GOES, WHAT_STAYS), which the app's strings
|
||||||
|
* repeat: a member who reads this page and then the confirm step must recognise the list.
|
||||||
|
* Play also asks how long what stays is kept, which the screens do not say, so each
|
||||||
|
* `stays` entry carries a `keep`. The last entry, the delivery log, is on BACKEND_DESIGN.md
|
||||||
|
* §6.6's list but not on the screens; it is here because Play asks about everything kept.
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
id: 'deploy-account-deletion',
|
||||||
|
scope: 'deployment',
|
||||||
|
title: 'Deleting an account',
|
||||||
|
body:
|
||||||
|
'You can delete your own account, on the site from the Account page, or in the app ' +
|
||||||
|
'from My account. It takes your password, and a two-factor code if the account has ' +
|
||||||
|
'one, and the account goes at once: there is no waiting period and nothing to ' +
|
||||||
|
'cancel. Your sign-in, email address, devices, linked sign-ins, game-account links ' +
|
||||||
|
'and inbox go with it. What you wrote where others read it stays, and other members ' +
|
||||||
|
'see it as written by “[deleted player]”; staff still see who it was, for ' +
|
||||||
|
'moderation. An operator can delete any account from the admin panel in the same way.',
|
||||||
|
retention: {
|
||||||
|
summary: 'Deleted at once; what stays, and for how long, is on the account-deletion page',
|
||||||
|
detail:
|
||||||
|
'Where the site can send email and the account had an address, one last message ' +
|
||||||
|
'goes to it, saying the account was deleted.',
|
||||||
|
},
|
||||||
|
source:
|
||||||
|
'website server/src/auth/accountDeletion.js, client/src/lib/accountDeletion.js; ' +
|
||||||
|
'docs/website/ACCOUNT_DELETION.md',
|
||||||
|
deletion: {
|
||||||
|
goes: [
|
||||||
|
'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',
|
||||||
|
],
|
||||||
|
stays: [
|
||||||
|
{
|
||||||
|
what:
|
||||||
|
'What you wrote where others read it, such as forum posts, stays. Other members ' +
|
||||||
|
'see it as written by “[deleted player]”.',
|
||||||
|
keep: 'Until a moderator or the operator removes it, as with any post.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
what:
|
||||||
|
'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.',
|
||||||
|
keep: 'Until the operator removes them.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
what:
|
||||||
|
'Event results stay with the score and the in-game name, with no site account ' +
|
||||||
|
'behind them.',
|
||||||
|
keep: 'Until the operator removes them.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
what: 'A game account stays on the game server. Only its link to this site goes.',
|
||||||
|
keep: 'For as long as the game server keeps it, which is up to whoever runs that server.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
what:
|
||||||
|
'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.',
|
||||||
|
keep:
|
||||||
|
'The record is swept after a period the operator sets, 180 days by default. The ' +
|
||||||
|
'suppression list does not expire.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
staff: 'If you were staff, your username can never be used again, by anyone.',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'deploy-sessions',
|
id: 'deploy-sessions',
|
||||||
scope: 'deployment',
|
scope: 'deployment',
|
||||||
@@ -652,7 +791,10 @@ export const collected = [
|
|||||||
title: 'Everything posted on the site',
|
title: 'Everything posted on the site',
|
||||||
body:
|
body:
|
||||||
'Forum threads and replies, uploads, wiki revisions, moderation actions, warnings, ' +
|
'Forum threads and replies, uploads, wiki revisions, moderation actions, warnings, ' +
|
||||||
'reports and appeals — with the account that made each one. Where the operator has ' +
|
'reports and appeals — with the account that made each one. A report keeps a copy ' +
|
||||||
|
'of what the reported post or chat line said, so staff read what was reported even ' +
|
||||||
|
'after it is edited or pruned. Each member’s blocks and the items they hid for ' +
|
||||||
|
'themselves are kept against their account, and deleted with it. Where the operator has ' +
|
||||||
'connected Discord, some of that crosses into Discord and is then also subject to ' +
|
'connected Discord, some of that crosses into Discord and is then also subject to ' +
|
||||||
'Discord’s own terms.',
|
'Discord’s own terms.',
|
||||||
retention: {
|
retention: {
|
||||||
@@ -661,7 +803,9 @@ export const collected = [
|
|||||||
'Soft-deleted forum uploads and Team activity have retention windows an admin ' +
|
'Soft-deleted forum uploads and Team activity have retention windows an admin ' +
|
||||||
'sets; most other content is kept until somebody removes it.',
|
'sets; most other content is kept until somebody removes it.',
|
||||||
},
|
},
|
||||||
source: 'website server/db/schema.sql — team_forum_*, mod_actions, content_reports',
|
source:
|
||||||
|
'website server/db/schema.sql — team_forum_*, mod_actions, content_reports, ' +
|
||||||
|
'user_blocks, user_hidden_items',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'deploy-engagement',
|
id: 'deploy-engagement',
|
||||||
@@ -757,6 +901,24 @@ export function collectedIn(scope) {
|
|||||||
return collected.filter((entry) => entry.scope === scope);
|
return collected.filter((entry) => entry.scope === scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The row `/delete-account/` renders. It throws rather than returning nothing, because
|
||||||
|
* that page is the address Google Play is given, and with no row it would render as a
|
||||||
|
* deletion page that names nothing deleted.
|
||||||
|
*
|
||||||
|
* @returns {Collected & { deletion: Deletion }}
|
||||||
|
*/
|
||||||
|
export function deletionRow() {
|
||||||
|
const rows = collected.filter((entry) => entry.deletion);
|
||||||
|
if (rows.length !== 1) {
|
||||||
|
throw new Error(
|
||||||
|
`src/data/collection.mjs: expected exactly one row with \`deletion\`, found ${rows.length}.\n` +
|
||||||
|
'\n/delete-account/ renders that row (ACCOUNT_DELETION.md §4.5).\n'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return /** @type {Collected & { deletion: Deletion }} */ (rows[0]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `app` rows that carry a Play mapping — the Data Safety generator's input.
|
* The `app` rows that carry a Play mapping — the Data Safety generator's input.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const legal = {
|
|||||||
* from git: a build timestamp would move on every rebuild and tell a reader nothing,
|
* from git: a build timestamp would move on every rebuild and tell a reader nothing,
|
||||||
* and a commit date would move when a stylesheet changed.
|
* and a commit date would move when a stylesheet changed.
|
||||||
*/
|
*/
|
||||||
lastUpdated: '2026-10-29',
|
lastUpdated: '2026-10-30',
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The minimum age to sign up for the beta. The org lead's decision, 2026-08-24 (D31).
|
* The minimum age to sign up for the beta. The org lead's decision, 2026-08-24 (D31).
|
||||||
|
|||||||
@@ -12,14 +12,14 @@
|
|||||||
"wrong protocol number in the first place."
|
"wrong protocol number in the first place."
|
||||||
],
|
],
|
||||||
|
|
||||||
"verifiedOn": "2026-10-29",
|
"verifiedOn": "2026-10-30",
|
||||||
|
|
||||||
"$comment_moduleApi": [
|
"$comment_moduleApi": [
|
||||||
"The one version every deployment shares, whichever game it runs. D66: it is the only",
|
"The one version every deployment shares, whichever game it runs. D66: it is the only",
|
||||||
"version the homepage hero and the footer show — a protocol and a bundle belong to a game,",
|
"version the homepage hero and the footer show — a protocol and a bundle belong to a game,",
|
||||||
"so they appear on that game's own pages."
|
"so they appear on that game's own pages."
|
||||||
],
|
],
|
||||||
"moduleApi": "1.19.0",
|
"moduleApi": "1.21.0",
|
||||||
|
|
||||||
"$comment_releases": [
|
"$comment_releases": [
|
||||||
"Every repository whose release the site quotes, flat, so checkFacts.mjs reads them all",
|
"Every repository whose release the site quotes, flat, so checkFacts.mjs reads them all",
|
||||||
@@ -27,12 +27,12 @@
|
|||||||
],
|
],
|
||||||
"releases": {
|
"releases": {
|
||||||
"installer": "v0.8.1",
|
"installer": "v0.8.1",
|
||||||
"Android-app": "v0.6.0",
|
"Android-app": "v0.7.0",
|
||||||
"link": "v2.3.0",
|
"link": "v2.3.0",
|
||||||
"Module-uo": "v1.3.0",
|
"Module-uo": "v1.4.1",
|
||||||
"Rust-Link": "v0.3.0",
|
"Rust-Link": "v0.3.0",
|
||||||
"Rust-Plugins": "v0.3.0",
|
"Rust-Plugins": "v0.3.1",
|
||||||
"Module-Rust": "v0.6.3",
|
"Module-Rust": "v0.7.0",
|
||||||
"runicnpc-rust": "v1.0.0"
|
"runicnpc-rust": "v1.0.0"
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
"protocol": 8,
|
"protocol": 8,
|
||||||
"module": {
|
"module": {
|
||||||
"repo": "Module-uo",
|
"repo": "Module-uo",
|
||||||
"coreApi": "^1.10.0",
|
"coreApi": "^1.20.0",
|
||||||
"capabilities": ["shard", "atlas", "market", "governors", "guilds", "houses", "champs", "cliloc"]
|
"capabilities": ["shard", "atlas", "market", "governors", "guilds", "houses", "champs", "cliloc"]
|
||||||
},
|
},
|
||||||
"sidecarRepo": "link",
|
"sidecarRepo": "link",
|
||||||
@@ -79,15 +79,15 @@
|
|||||||
"protocol": 14,
|
"protocol": 14,
|
||||||
"module": {
|
"module": {
|
||||||
"repo": "Module-Rust",
|
"repo": "Module-Rust",
|
||||||
"coreApi": "^1.19.0",
|
"coreApi": "^1.21.0",
|
||||||
"capabilities": ["rust", "servers", "killfeed", "leaderboard", "presence", "wipes", "identity", "map"]
|
"capabilities": ["rust", "servers", "killfeed", "leaderboard", "presence", "wipes", "identity", "map"]
|
||||||
},
|
},
|
||||||
"sidecarRepo": "Rust-Link",
|
"sidecarRepo": "Rust-Link",
|
||||||
"pluginRepo": "Rust-Plugins",
|
"pluginRepo": "Rust-Plugins",
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"tag": "2026.10.22",
|
"tag": "2026.10.30",
|
||||||
"sidecar": "v0.3.0",
|
"sidecar": "v0.3.0",
|
||||||
"plugin": "v0.3.0",
|
"plugin": "v0.3.1",
|
||||||
"oxideMin": "2.0.7585",
|
"oxideMin": "2.0.7585",
|
||||||
"carbonMin": "2.0.259",
|
"carbonMin": "2.0.259",
|
||||||
"requiresPlugins": ["Kits", "ZoneManager"],
|
"requiresPlugins": ["Kits", "ZoneManager"],
|
||||||
@@ -120,17 +120,18 @@
|
|||||||
"",
|
"",
|
||||||
"`serviceable` is the one value here with no authority to check it against, and it is",
|
"`serviceable` is the one value here with no authority to check it against, and it is",
|
||||||
"deliberately manual. It answers a question no API can: does the published build",
|
"deliberately manual. It answers a question no API can: does the published build",
|
||||||
"actually work. The org lead reports v0.5.0's does not, so the download block renders a",
|
"actually work. v0.5.0's did not, and while false the download block renders a 'being",
|
||||||
"'being replaced' state instead of a link, and flipping this to true is the single edit",
|
"replaced' state instead of a link. A check cannot run an APK; a person can, and this",
|
||||||
"that turns the link back on once a working build is released. A check cannot run an",
|
"is where they record that they did. v0.7.0's was installed on an Android 16 emulator",
|
||||||
"APK; a person can, and this is where they record that they did."
|
"on 2026-10-31, connected to the public demo and read its Rust servers: true.",
|
||||||
|
"When `asset` moves to a new release, set this back to false until someone has run it."
|
||||||
],
|
],
|
||||||
"androidApk": {
|
"androidApk": {
|
||||||
"asset": "runic-gateway-0.6.0.apk",
|
"asset": "runic-gateway-0.7.0.apk",
|
||||||
"checksums": "SHA256SUMS",
|
"checksums": "SHA256SUMS",
|
||||||
"minSdk": 29,
|
"minSdk": 29,
|
||||||
"minAndroid": "10",
|
"minAndroid": "10",
|
||||||
"serviceable": false
|
"serviceable": true
|
||||||
},
|
},
|
||||||
|
|
||||||
"gitea": {
|
"gitea": {
|
||||||
|
|||||||
@@ -196,6 +196,12 @@ const downloadBase = `${releasePage}/download/${release}`;
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- D85: the deletion page Play is given, beside the two ways in. -->
|
||||||
|
<p class="app-delete">
|
||||||
|
Done with it? <a href="/delete-account/">How to delete the account you use in the
|
||||||
|
app</a>, and what deleting it keeps.
|
||||||
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<NotBuilt scope="app" title="What the app does not do" />
|
<NotBuilt scope="app" title="What the app does not do" />
|
||||||
@@ -335,6 +341,12 @@ const downloadBase = `${releasePage}/download/${release}`;
|
|||||||
padding-top: 0.4rem;
|
padding-top: 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-delete {
|
||||||
|
margin: 1.25rem 0 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
.app-get__foot {
|
.app-get__foot {
|
||||||
margin: 1rem 0 0;
|
margin: 1rem 0 0;
|
||||||
padding-top: 0.85rem;
|
padding-top: 0.85rem;
|
||||||
|
|||||||
327
src/pages/delete-account.astro
Normal file
327
src/pages/delete-account.astro
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
---
|
||||||
|
import Base from '../layouts/Base.astro';
|
||||||
|
import PageHeader from '../components/PageHeader.astro';
|
||||||
|
|
||||||
|
import { deletionRow } from '../data/collection.mjs';
|
||||||
|
import { legal } from '../data/legal.mjs';
|
||||||
|
import { brand } from '../lib/brand.mjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `/delete-account/` — the page Google Play is given for account deletion
|
||||||
|
* (docs/website/ACCOUNT_DELETION.md §4.5, AD2).
|
||||||
|
*
|
||||||
|
* ---------------------------------------------------------------------------------------
|
||||||
|
* IT EXPLAINS; IT CANNOT PERFORM
|
||||||
|
* ---------------------------------------------------------------------------------------
|
||||||
|
* Play needs one deletion address from whoever publishes the app, and that is us. But we
|
||||||
|
* hold no account: every community runs its own site, and the account lives there. So this
|
||||||
|
* page says where the button is on the website and in the app, what goes and what stays,
|
||||||
|
* and who to ask when you cannot sign in, which is that community's operator and never us
|
||||||
|
* (D86). We have no access to any community's site, so offering to help would promise
|
||||||
|
* something we cannot do.
|
||||||
|
*
|
||||||
|
* What goes and what stays is not written here. It is the `deletion` row in
|
||||||
|
* `src/data/collection.mjs`, the same row `/privacy` section 3 renders, so the two pages
|
||||||
|
* cannot disagree about what a deletion takes.
|
||||||
|
*/
|
||||||
|
const title = 'Delete your account';
|
||||||
|
const description =
|
||||||
|
`How to delete an account you use in the ${brand.siteName} Android app or on a ` +
|
||||||
|
`${brand.siteName} community site, what is deleted, and what is kept.`;
|
||||||
|
|
||||||
|
const row = deletionRow();
|
||||||
|
const { goes, stays, staff } = row.deletion;
|
||||||
|
|
||||||
|
const surfaces = [
|
||||||
|
{
|
||||||
|
id: 'on-the-website',
|
||||||
|
eyebrow: 'On the website',
|
||||||
|
heading: 'Your community’s site',
|
||||||
|
steps: [
|
||||||
|
'Sign in to the site.',
|
||||||
|
'Open “Account”. Staff open “Account” in the admin panel instead.',
|
||||||
|
'Scroll to the bottom, to “Delete my account”, and press “Delete my account…”.',
|
||||||
|
'Type your username, then your password, and a two-factor code if the account has two-factor sign-in on.',
|
||||||
|
'Press “Delete my account”. You are signed out everywhere, and the site says the account has been deleted.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'in-the-app',
|
||||||
|
eyebrow: 'In the app',
|
||||||
|
heading: `The ${brand.siteName} app`,
|
||||||
|
steps: [
|
||||||
|
'Sign in, if you are not already.',
|
||||||
|
'Open the menu and choose “My account”.',
|
||||||
|
'Scroll to the bottom and tap “Delete my account…”.',
|
||||||
|
'Type your username, then your password, and a two-factor code if the account has two-factor sign-in on.',
|
||||||
|
'Tap “Delete my account”. The app forgets the account and returns to sign-in, saying it has been deleted.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
---
|
||||||
|
|
||||||
|
<Base title={title} description={description}>
|
||||||
|
<PageHeader eyebrow="Account deletion" title="Deleting your account">
|
||||||
|
<p>
|
||||||
|
For the {brand.siteName} Android app, and for the community sites it connects to. We
|
||||||
|
are {brand.siteName}, the project that makes both.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You can delete your account yourself, on the website or in the app. It is deleted at
|
||||||
|
once: there is no waiting period and nothing to cancel.
|
||||||
|
</p>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<section class="page section legal-meta">
|
||||||
|
<p class="legal-meta__line">
|
||||||
|
<span class="chip chip--version">Last updated {legal.lastUpdated}</span>
|
||||||
|
<a class="legal-meta__link" href="/privacy/#deployments">The same list is on the privacy page</a>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="page section">
|
||||||
|
<div class="panel del-who">
|
||||||
|
<h2>Your account is with your community, not with us</h2>
|
||||||
|
<p>
|
||||||
|
The app ships pointed at nothing. On first run it asks for the address of a
|
||||||
|
{' '}{brand.siteName} site, and that site is run by whoever runs that community. Your
|
||||||
|
account lives there, under their control. We hold no account for you and have no
|
||||||
|
copy of one, so the deletion happens on that site, and you are the one who does it.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="page section" id="how">
|
||||||
|
<h2 class="del-h2">How to delete it</h2>
|
||||||
|
<p class="prose del-lede">
|
||||||
|
The same steps on both, and either one deletes the account everywhere: deleting it in
|
||||||
|
the app also deletes it on the website, and the other way round.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="del-grid">
|
||||||
|
{
|
||||||
|
surfaces.map((surface) => (
|
||||||
|
<div class="panel del-surface" id={surface.id}>
|
||||||
|
<p class="eyebrow">{surface.eyebrow}</p>
|
||||||
|
<h3>{surface.heading}</h3>
|
||||||
|
<ol class="del-steps">
|
||||||
|
{surface.steps.map((step) => (
|
||||||
|
<li>{step}</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="del-notes">
|
||||||
|
<li>
|
||||||
|
<strong>A password is required.</strong> If you only ever signed in with Google,
|
||||||
|
Discord or Steam and never set a password, set one in the Password section of the
|
||||||
|
same page first.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>The only admin cannot delete themselves.</strong> Make someone else an admin
|
||||||
|
first, so the site is never left with nobody to run it.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="page section" id="what-goes">
|
||||||
|
<div class="del-cols">
|
||||||
|
<div class="panel del-list">
|
||||||
|
<p class="eyebrow">Deleted</p>
|
||||||
|
<h2>What goes with the account</h2>
|
||||||
|
<ul>
|
||||||
|
{goes.map((item) => <li>{item[0].toUpperCase() + item.slice(1)}.</li>)}
|
||||||
|
</ul>
|
||||||
|
<p class="del-list__foot">{row.retention.detail}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel del-list">
|
||||||
|
<p class="eyebrow">Kept</p>
|
||||||
|
<h2>What stays, and for how long</h2>
|
||||||
|
<ul class="del-kept">
|
||||||
|
{
|
||||||
|
stays.map((entry) => (
|
||||||
|
<li>
|
||||||
|
{entry.what}
|
||||||
|
<span class="del-kept__keep">
|
||||||
|
<span class="del-kept__label">How long</span>
|
||||||
|
{entry.keep}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
<p class="del-list__foot">{staff}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="page section" id="cannot-sign-in">
|
||||||
|
<div class="panel del-who">
|
||||||
|
<h2>If you cannot sign in</h2>
|
||||||
|
<ol class="del-steps">
|
||||||
|
<li>
|
||||||
|
Use “Forgot your password?” on the sign-in page, on the website or in the app. It
|
||||||
|
works if your account has an email address and the site can send email.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Otherwise, ask the community that runs the site. Its operator can delete any
|
||||||
|
account from the admin panel, and the result is the same as deleting it yourself.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<p class="del-who__note">
|
||||||
|
We cannot do it for you. We hold no account, and we have no access to any
|
||||||
|
community’s site.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</Base>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* The header strip, as /privacy and /terms render it. Astro scopes component styles, so
|
||||||
|
each legal page carries its own copy (see the note in terms.astro). */
|
||||||
|
/* Unlike /privacy, a panel follows the strip directly, so it keeps some room below. */
|
||||||
|
.legal-meta {
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legal-meta__line {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legal-meta__link {
|
||||||
|
color: var(--dim);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.del-h2 {
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
font-size: clamp(1.6rem, 3.2vw, 2.1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.del-lede {
|
||||||
|
margin: 0 0 1.75rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The two panels that frame the page: who holds the account, and who to ask. Gold edge,
|
||||||
|
as /app's prerequisite panel, because each is a fact about the product rather than a
|
||||||
|
warning. */
|
||||||
|
.del-who {
|
||||||
|
padding: clamp(1.25rem, 4vw, 2.25rem);
|
||||||
|
border-color: var(--gold-deep);
|
||||||
|
}
|
||||||
|
|
||||||
|
.del-who h2 {
|
||||||
|
margin: 0 0 0.7rem;
|
||||||
|
font-size: clamp(1.3rem, 3vw, 1.7rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.del-who p,
|
||||||
|
.del-who li {
|
||||||
|
max-width: var(--measure);
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.del-who p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The one sentence a reader who skims must not miss, so it is brighter than the rest. */
|
||||||
|
.del-who .del-who__note {
|
||||||
|
margin-top: 1rem;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.del-grid,
|
||||||
|
.del-cols {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 21rem), 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.del-surface,
|
||||||
|
.del-list {
|
||||||
|
padding: clamp(1.1rem, 3vw, 1.75rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.del-surface h3 {
|
||||||
|
margin: 0 0 0.8rem;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.del-list h2 {
|
||||||
|
margin: 0 0 0.8rem;
|
||||||
|
font-size: clamp(1.2rem, 2.6vw, 1.45rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.del-steps {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.3rem;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.97rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.del-steps li + li {
|
||||||
|
margin-top: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.del-notes {
|
||||||
|
margin: 1.25rem 0 0;
|
||||||
|
padding-left: 1.2rem;
|
||||||
|
max-width: var(--measure);
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.del-notes li + li {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.del-list ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.2rem;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.del-list li + li {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.del-kept li + li {
|
||||||
|
margin-top: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.del-kept__keep {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
color: var(--dim);
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.del-kept__label {
|
||||||
|
display: block;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.11em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.del-list__foot {
|
||||||
|
margin: 1.1rem 0 0;
|
||||||
|
padding-top: 0.9rem;
|
||||||
|
border-top: 1px solid var(--line-soft);
|
||||||
|
color: var(--dim);
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -66,6 +66,8 @@ const sections = [
|
|||||||
'the app happens between your phone and their server, and there is no account with ' +
|
'the app happens between your phone and their server, and there is no account with ' +
|
||||||
'us, no service of ours in the middle, and no copy of anything on our side — because ' +
|
'us, no service of ours in the middle, and no copy of anything on our side — because ' +
|
||||||
'we do not operate one.',
|
'we do not operate one.',
|
||||||
|
/* The deletion page Play is given (D85). The app is where most people will look. */
|
||||||
|
link: { href: '/delete-account/', text: 'How to delete the account you use in the app' },
|
||||||
rows: collectedIn('app'),
|
rows: collectedIn('app'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -79,6 +81,7 @@ const sections = [
|
|||||||
'and their policy — this page is not it. What follows is an inventory of what the ' +
|
'and their policy — this page is not it. What follows is an inventory of what the ' +
|
||||||
'software collects, so that an operator can see plainly what they are taking on, ' +
|
'software collects, so that an operator can see plainly what they are taking on, ' +
|
||||||
'and a player can see what to ask their operator about.',
|
'and a player can see what to ask their operator about.',
|
||||||
|
link: { href: '/delete-account/', text: 'What deleting an account takes, and what it keeps' },
|
||||||
rows: collectedIn('deployment'),
|
rows: collectedIn('deployment'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -131,6 +134,11 @@ const sections = [
|
|||||||
<h2>{section.heading}</h2>
|
<h2>{section.heading}</h2>
|
||||||
<p class="legal-sec__who">{section.controller}</p>
|
<p class="legal-sec__who">{section.controller}</p>
|
||||||
<p class="prose legal-sec__lede">{section.lede}</p>
|
<p class="prose legal-sec__lede">{section.lede}</p>
|
||||||
|
{section.link && (
|
||||||
|
<p class="legal-sec__link">
|
||||||
|
<a href={section.link.href}>{section.link.text}</a>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="legal-rows">
|
<ul class="legal-rows">
|
||||||
@@ -304,6 +312,11 @@ const sections = [
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.legal-sec__link {
|
||||||
|
margin: 0.75rem 0 0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
.legal-rows {
|
.legal-rows {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
assertScopeNonEmpty,
|
assertScopeNonEmpty,
|
||||||
collected,
|
collected,
|
||||||
collectedIn,
|
collectedIn,
|
||||||
|
deletionRow,
|
||||||
PLAY_DATA_TYPES,
|
PLAY_DATA_TYPES,
|
||||||
PLAY_PURPOSES,
|
PLAY_PURPOSES,
|
||||||
playRows,
|
playRows,
|
||||||
@@ -139,6 +140,40 @@ describe('the Play Data Safety mapping', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* `/delete-account/` is the address Google Play is given (ACCOUNT_DELETION.md §4.5), and
|
||||||
|
* Play asks two things of it that render fine when missing: what is deleted, and how long
|
||||||
|
* what stays is kept. A `stays` entry with no `keep` is a card with no answer to the
|
||||||
|
* second question.
|
||||||
|
*/
|
||||||
|
describe('the account-deletion row', () => {
|
||||||
|
it('exists exactly once, in the deployment scope', () => {
|
||||||
|
const row = deletionRow();
|
||||||
|
assert.equal(row.scope, 'deployment', 'the deletion row is about a deployment, not about us');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('says what goes and what stays, with how long for each', () => {
|
||||||
|
const { goes, stays, staff } = deletionRow().deletion;
|
||||||
|
assert.ok(goes.length, 'the deletion row names nothing deleted');
|
||||||
|
assert.ok(stays.length, 'the deletion row names nothing kept');
|
||||||
|
for (const entry of stays) {
|
||||||
|
assert.ok(entry.what?.trim(), 'a kept entry says nothing about what is kept');
|
||||||
|
assert.ok(entry.keep?.trim(), `"${entry.what}" does not say how long it is kept`);
|
||||||
|
}
|
||||||
|
assert.ok(staff?.trim(), 'the staff note (AD9) is missing');
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* AD5 in test form: members see "[deleted player]", the wording the website and the app
|
||||||
|
* render. A page that named a different placeholder would describe a product that does
|
||||||
|
* not exist.
|
||||||
|
*/
|
||||||
|
it('names the placeholder members see', () => {
|
||||||
|
const text = deletionRow().deletion.stays.map((entry) => entry.what).join(' ');
|
||||||
|
assert.match(text, /“\[deleted player\]”/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('the minimum age', () => {
|
describe('the minimum age', () => {
|
||||||
it('is stated in the consent sentence the row records', () => {
|
it('is stated in the consent sentence the row records', () => {
|
||||||
assert.match(
|
assert.match(
|
||||||
|
|||||||
Reference in New Issue
Block a user