fix(legal): declare what the app sends as collected, not shared (D84)
All checks were successful
PR checks / checks (pull_request) Successful in 2m33s
All checks were successful
PR checks / checks (pull_request) Successful in 2m33s
Play counts data that leaves the device as collected, whoever runs the server it lands on. The generated Data Safety notes answered "not collected" because we run no server, which is true of /privacy and the wrong answer to the form (org lead, 2026-10-29, Android PLAN.md M8). An audit of the app's edge (every body, query and header in data/api/) found more leaving the phone than section 2 listed. New rows: the User-Agent, game-account links and creation, the Contact form, staff posts and moderation, notification read state and preferences, search text. The sign-in and trusted-device rows now say what is sent (the credentials, the device's make and model). - PlayMapping: a list of console types, plus required and purposes, checked against PLAY_DATA_TYPES / PLAY_PURPOSES (the first version filed "Other user-generated content" under Messages). - The generator answers per data type, lists every type not collected, and answers the form's security questions. - The premise test now asserts what is still true: nothing is shared. - legal.lastUpdated moves to 2026-10-29 with /privacy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -24,8 +24,9 @@
|
||||
* `site` This website. We are the data controller. It is one form.
|
||||
* `app` The Android app. We operate NO server it talks to — every byte goes to
|
||||
* a deployment the user typed the address of, run by whoever runs that
|
||||
* community. What is listed here is therefore mostly what the device
|
||||
* HOLDS, not what we receive, because we receive nothing.
|
||||
* community. What is listed here is what the device holds and what it
|
||||
* sends to that deployment. None of it reaches us, because we receive
|
||||
* nothing; Play still counts what is sent as collected (see `PlayMapping`).
|
||||
* `deployment` A self-hosted install of the platform. The operator is the controller,
|
||||
* not us. This scope exists so an operator sees the responsibility they
|
||||
* are taking on, and so a player never mistakes this policy for the one
|
||||
@@ -46,13 +47,23 @@
|
||||
* @property {string} summary Short enough to sit in a table cell.
|
||||
* @property {string} [detail] The mechanism, where the summary alone would be a promise.
|
||||
*
|
||||
* @typedef {object} PlayType
|
||||
* @property {string} category The console's grouping, a key of `PLAY_DATA_TYPES`.
|
||||
* @property {string} type The console's data type within that grouping.
|
||||
*
|
||||
* @typedef {object} PlayMapping
|
||||
* How this row answers Google Play's Data Safety form. `app` scope only — see
|
||||
* `scripts/playDataSafety.mjs`, which is the only reader.
|
||||
* @property {string} category The console's grouping, e.g. "Personal info".
|
||||
* @property {string} type The console's data type within that grouping.
|
||||
* @property {boolean} collected Does it leave the device to a server WE operate?
|
||||
* @property {boolean} shared Do we pass it to a third party?
|
||||
* @property {PlayType[]} types The console data types this row answers.
|
||||
* @property {boolean} collected Does it leave the device? Play counts data the app sends
|
||||
* off the phone as collected, whoever runs the server it
|
||||
* goes to. Ours receives none of it, because we run none;
|
||||
* the deployment the user chose receives it (org lead,
|
||||
* 2026-10-29: the conservative answers).
|
||||
* @property {boolean} shared Is it passed to a third party? Never, for this app.
|
||||
* @property {boolean} [required] Collected rows only. Declared required throughout (same
|
||||
* decision), rather than arguing feature by feature.
|
||||
* @property {string[]} [purposes] Collected rows only, from `PLAY_PURPOSES`.
|
||||
* @property {string} answer The recommended console answer, in one line.
|
||||
* @property {string} because Why that answer is the truthful one.
|
||||
*
|
||||
@@ -64,9 +75,64 @@
|
||||
* @property {Retention} retention
|
||||
* @property {string} source The file this was read out of, repo-relative.
|
||||
* @property {PlayMapping} [play]
|
||||
*
|
||||
* @type {Collected[]}
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Data Safety form's own vocabulary: every category and the data types under it, and
|
||||
* the purposes a collected type can be given. Read from Play Console Help ("Provide
|
||||
* information for Google Play's Data safety section") on `PLAY_VOCABULARY_READ`.
|
||||
*
|
||||
* Here so a typo or a misfiled type fails a test instead of reaching the console. The
|
||||
* first version of this file filed "Other user-generated content" under Messages; it
|
||||
* lives under App activity. The full list is also what lets the generator say, for every
|
||||
* type the app does NOT collect, that it does not — the form asks about all of them.
|
||||
*/
|
||||
export const PLAY_VOCABULARY_READ = '2026-10-29';
|
||||
|
||||
export const PLAY_DATA_TYPES = {
|
||||
Location: ['Approximate location', 'Precise location'],
|
||||
'Personal info': [
|
||||
'Name',
|
||||
'Email address',
|
||||
'User IDs',
|
||||
'Address',
|
||||
'Phone number',
|
||||
'Race and ethnicity',
|
||||
'Political or religious beliefs',
|
||||
'Sexual orientation',
|
||||
'Other info',
|
||||
],
|
||||
'Financial info': ['User payment info', 'Purchase history', 'Credit score', 'Other financial info'],
|
||||
'Health and fitness': ['Health info', 'Fitness info'],
|
||||
Messages: ['Emails', 'SMS or MMS', 'Other in-app messages'],
|
||||
'Photos and videos': ['Photos', 'Videos'],
|
||||
'Audio files': ['Voice or sound recordings', 'Music files', 'Other audio files'],
|
||||
'Files and docs': ['Files and docs'],
|
||||
Calendar: ['Calendar events'],
|
||||
Contacts: ['Contacts'],
|
||||
'App activity': [
|
||||
'App interactions',
|
||||
'In-app search history',
|
||||
'Installed apps',
|
||||
'Other user-generated content',
|
||||
'Other actions',
|
||||
],
|
||||
'Web browsing': ['Web browsing history'],
|
||||
'App info and performance': ['Crash logs', 'Diagnostics', 'Other app performance data'],
|
||||
'Device or other IDs': ['Device or other IDs'],
|
||||
};
|
||||
|
||||
export const PLAY_PURPOSES = [
|
||||
'App functionality',
|
||||
'Analytics',
|
||||
'Developer communications',
|
||||
'Advertising or marketing',
|
||||
'Fraud prevention, security, and compliance',
|
||||
'Personalization',
|
||||
'Account management',
|
||||
];
|
||||
|
||||
/** @type {Collected[]} */
|
||||
export const collected = [
|
||||
/* =====================================================================================
|
||||
1. THIS WEBSITE
|
||||
@@ -163,8 +229,14 @@ export const collected = [
|
||||
the app talks to. `ConnectScreen.kt` gates the entire app on an address the user
|
||||
enters and the app validates; everything below either stays on the phone or goes to
|
||||
that address. There is no telemetry SDK, no crash reporter and no analytics in the
|
||||
build — the manifest asks for INTERNET, network state, notifications and a data-sync
|
||||
foreground service, and nothing else.
|
||||
build — the manifest asks for INTERNET, network state, notifications, a special-use
|
||||
foreground service for push and the boot broadcast that restarts it, and nothing else.
|
||||
|
||||
What goes to that address is still COLLECTED in Play's sense, because it leaves the
|
||||
phone, and it is declared that way (org lead, 2026-10-29). Read out of the Retrofit
|
||||
interfaces in `data/api/` on the Android app's `edge` (19c2f77): every request body,
|
||||
query and header the app sends is in one of the rows below. Rows that stay on the
|
||||
phone say so, and carry no console type.
|
||||
|
||||
`play` is filled in on every row here, because a row in this scope with no mapping is
|
||||
a question on the console form that somebody will answer from memory (D33).
|
||||
@@ -172,54 +244,98 @@ export const collected = [
|
||||
{
|
||||
id: 'app-session-tokens',
|
||||
scope: 'app',
|
||||
title: 'Your sign-in tokens',
|
||||
title: 'Your sign-in, and the tokens it issues',
|
||||
body:
|
||||
'When you sign in to a deployment, the app keeps the access and refresh tokens it ' +
|
||||
'was issued, plus the username, role and account id they belong to. They 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.',
|
||||
'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.',
|
||||
retention: {
|
||||
summary: 'On the device until you sign out',
|
||||
detail: 'Signing out clears them; uninstalling the app removes them with it.',
|
||||
},
|
||||
source: 'core/auth/EncryptedTokenStore.kt',
|
||||
source:
|
||||
'data/api/AuthApi.kt, data/api/SsoApi.kt, data/api/MeApi.kt, core/auth/EncryptedTokenStore.kt',
|
||||
play: {
|
||||
category: 'Personal info',
|
||||
type: 'User IDs',
|
||||
collected: false,
|
||||
types: [{ category: 'Personal info', type: 'User IDs' }],
|
||||
collected: true,
|
||||
shared: false,
|
||||
answer: 'Not collected by us.',
|
||||
required: true,
|
||||
purposes: [
|
||||
'App functionality',
|
||||
'Account management',
|
||||
'Fraud prevention, security, and compliance',
|
||||
],
|
||||
answer: 'Collected, not shared, required.',
|
||||
because:
|
||||
'The credentials are issued by, and returned to, a server the user nominated. ' +
|
||||
'Nothing reaches an endpoint under our control, because we run none.',
|
||||
'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.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'app-trust-token',
|
||||
scope: 'app',
|
||||
title: 'The trusted-device token, if you asked for one',
|
||||
title: 'The trusted-device token and the device’s name, if you asked for one',
|
||||
body:
|
||||
'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.',
|
||||
'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.',
|
||||
retention: {
|
||||
summary: 'On the device until it expires or you revoke it',
|
||||
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.',
|
||||
},
|
||||
source: 'core/auth/EncryptedTrustTokenStore.kt',
|
||||
source: 'core/auth/EncryptedTrustTokenStore.kt, core/auth/DeviceNameProvider.kt',
|
||||
play: {
|
||||
category: 'Personal info',
|
||||
type: 'User IDs',
|
||||
collected: false,
|
||||
types: [{ category: 'Device or other IDs', type: 'Device or other IDs' }],
|
||||
collected: true,
|
||||
shared: false,
|
||||
answer: 'Not collected by us.',
|
||||
required: true,
|
||||
purposes: ['Account management', 'Fraud prevention, security, and compliance'],
|
||||
answer: 'Collected, not shared, required.',
|
||||
because:
|
||||
'Same as the session tokens: minted by the user’s deployment, stored on the ' +
|
||||
'device, presented back to that same deployment.',
|
||||
'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.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'app-user-agent',
|
||||
scope: 'app',
|
||||
title: 'The app’s version and your Android version',
|
||||
body:
|
||||
'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.',
|
||||
retention: {
|
||||
summary: 'Under the deployment operator’s policy',
|
||||
detail:
|
||||
'A deployment’s access log may keep it, as any web server keeps the user agent of ' +
|
||||
'a browser.',
|
||||
},
|
||||
source: 'di/NetworkModule.kt, core/net/UserAgentInterceptor.kt',
|
||||
play: {
|
||||
types: [{ category: 'App info and performance', type: 'Diagnostics' }],
|
||||
collected: true,
|
||||
shared: false,
|
||||
required: true,
|
||||
purposes: ['App functionality', 'Fraud prevention, security, and compliance'],
|
||||
answer: 'Collected, not shared, required.',
|
||||
because:
|
||||
'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.',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -234,11 +350,10 @@ export const collected = [
|
||||
retention: { summary: 'On the device until you change it or uninstall' },
|
||||
source: 'core/prefs/ServerPreferences.kt',
|
||||
play: {
|
||||
category: 'App info and performance',
|
||||
type: 'Other app data',
|
||||
types: [],
|
||||
collected: false,
|
||||
shared: false,
|
||||
answer: 'Not collected by us. Stored on the device only.',
|
||||
answer: 'Not collected. Stored on the device only.',
|
||||
because:
|
||||
'It never leaves the phone. It is the destination of requests, not the contents ' +
|
||||
'of one.',
|
||||
@@ -264,45 +379,157 @@ export const collected = [
|
||||
'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.',
|
||||
},
|
||||
source: 'core/push/NtfyTopic.kt, core/push/PushPreferences.kt',
|
||||
source: 'core/push/NtfyTopic.kt, core/push/PushPreferences.kt, data/api/NotificationsApi.kt',
|
||||
play: {
|
||||
category: 'Messages',
|
||||
type: 'Other in-app messages',
|
||||
collected: false,
|
||||
types: [{ category: 'Device or other IDs', type: 'Device or other IDs' }],
|
||||
collected: true,
|
||||
shared: false,
|
||||
required: true,
|
||||
purposes: ['App functionality'],
|
||||
answer:
|
||||
'Not collected by us. Declare the relay hop in the console’s free-text ' +
|
||||
'security section if it asks.',
|
||||
'Collected, not shared, required. Mention the relay in the free-text security ' +
|
||||
'section if the console asks.',
|
||||
because:
|
||||
'The notification passes through a relay chosen by the deployment, and it carries ' +
|
||||
'no content — the app pulls the content itself, authenticated. Neither hop reaches ' +
|
||||
'a server we operate.',
|
||||
'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.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'app-game-accounts',
|
||||
scope: 'app',
|
||||
title: 'Linking a game account',
|
||||
body:
|
||||
'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.',
|
||||
retention: {
|
||||
summary: 'Held by the deployment until you unlink, under its operator’s policy',
|
||||
},
|
||||
source: 'data/api/PlayerShardApi.kt, data/api/PlayerRustApi.kt, data/api/MeApi.kt',
|
||||
play: {
|
||||
types: [{ category: 'Personal info', type: 'User IDs' }],
|
||||
collected: true,
|
||||
shared: false,
|
||||
required: true,
|
||||
purposes: ['App functionality', 'Account management'],
|
||||
answer: 'Collected, not shared, required.',
|
||||
because:
|
||||
'Game account names, Steam ids and link codes identify a player, and the app ' +
|
||||
'sends them to the deployment.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'app-contact',
|
||||
scope: 'app',
|
||||
title: 'Your name, email address and message, if you use Contact',
|
||||
body:
|
||||
'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.',
|
||||
retention: {
|
||||
summary: 'Held by the deployment’s operator, under their policy',
|
||||
detail: 'The message is delivered to the operator’s mailbox, and kept as long as they keep it.',
|
||||
},
|
||||
source: 'data/api/PublicApi.kt, data/api/dto/ContactDto.kt',
|
||||
play: {
|
||||
types: [
|
||||
{ category: 'Personal info', type: 'Name' },
|
||||
{ category: 'Personal info', type: 'Email address' },
|
||||
{ category: 'Messages', type: 'Other in-app messages' },
|
||||
],
|
||||
collected: true,
|
||||
shared: false,
|
||||
required: true,
|
||||
purposes: ['App functionality'],
|
||||
answer: 'Collected, not shared, required.',
|
||||
because:
|
||||
'The form’s three fields leave the phone for the deployment, whose operator ' +
|
||||
'receives them.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'app-content',
|
||||
scope: 'app',
|
||||
title: 'Everything you read and post in the app',
|
||||
title: 'What you post and send from the app',
|
||||
body:
|
||||
'Forum posts, Team activity, what the game module shows you, notification ' +
|
||||
'preferences: all of it is a live read or write against the deployment. Apart from ' +
|
||||
'the notification snapshot described in the next entry, 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.',
|
||||
'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.',
|
||||
retention: {
|
||||
summary: 'Held by the deployment, under its operator’s policy',
|
||||
},
|
||||
source: 'PLAN.md §9 section 2',
|
||||
source: 'data/api/AdminApi.kt',
|
||||
play: {
|
||||
category: 'Messages',
|
||||
type: 'Other user-generated content',
|
||||
collected: false,
|
||||
types: [
|
||||
{ category: 'Messages', type: 'Other in-app messages' },
|
||||
{ category: 'App activity', type: 'Other user-generated content' },
|
||||
],
|
||||
collected: true,
|
||||
shared: false,
|
||||
answer: 'Not collected by us.',
|
||||
required: true,
|
||||
purposes: ['App functionality'],
|
||||
answer: 'Collected, not shared, required.',
|
||||
because:
|
||||
'Content is written to the community’s own installation. We have no copy, no ' +
|
||||
'access and no way to obtain one.',
|
||||
'Posts, broadcasts and replies are written to the community’s own installation. ' +
|
||||
'We have no copy, no access and no way to obtain one.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'app-interactions',
|
||||
scope: 'app',
|
||||
title: 'Which notifications you have read, and which you want',
|
||||
body:
|
||||
'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.',
|
||||
retention: {
|
||||
summary: 'Held by the deployment, under its operator’s policy',
|
||||
},
|
||||
source: 'data/api/NotificationsApi.kt',
|
||||
play: {
|
||||
types: [{ category: 'App activity', type: 'App interactions' }],
|
||||
collected: true,
|
||||
shared: false,
|
||||
required: true,
|
||||
purposes: ['App functionality', 'Personalization'],
|
||||
answer: 'Collected, not shared, required.',
|
||||
because:
|
||||
'Read state and preferences are actions in the app that are sent to, and kept ' +
|
||||
'by, the deployment.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'app-search',
|
||||
scope: 'app',
|
||||
title: 'What you search for',
|
||||
body:
|
||||
'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.',
|
||||
retention: {
|
||||
summary: 'Under the deployment operator’s policy',
|
||||
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.',
|
||||
},
|
||||
source: 'data/api/PublicApi.kt',
|
||||
play: {
|
||||
types: [{ category: 'App activity', type: 'In-app search history' }],
|
||||
collected: true,
|
||||
shared: false,
|
||||
required: true,
|
||||
purposes: ['App functionality'],
|
||||
answer: 'Collected, not shared, required.',
|
||||
because: 'Search text leaves the phone in the request to the deployment.',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -328,15 +555,13 @@ export const collected = [
|
||||
},
|
||||
source: 'core/inbox/DataStoreInboxCache.kt, data/repository/AuthRepository.kt',
|
||||
play: {
|
||||
category: 'Messages',
|
||||
type: 'Other in-app messages',
|
||||
types: [],
|
||||
collected: false,
|
||||
shared: false,
|
||||
answer: 'Not collected by us. Stored on the device only.',
|
||||
answer: 'Not collected. Stored on the device only.',
|
||||
because:
|
||||
'The snapshot is written on the phone from data the deployment had already ' +
|
||||
'delivered. It is not uploaded anywhere, and no server we operate is on either ' +
|
||||
'end of it.',
|
||||
'delivered. It is not uploaded anywhere.',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -351,14 +576,13 @@ export const collected = [
|
||||
retention: { summary: 'Nothing to retain' },
|
||||
source: 'app/build.gradle.kts, app/src/main/AndroidManifest.xml',
|
||||
play: {
|
||||
category: 'Device or other IDs',
|
||||
type: 'Device or other IDs',
|
||||
types: [{ category: 'App info and performance', type: 'Crash logs' }],
|
||||
collected: false,
|
||||
shared: false,
|
||||
answer: 'Not collected.',
|
||||
answer: 'Not collected. The advertising ID declaration is "no".',
|
||||
because:
|
||||
'No advertising ID, no analytics identifier, and no library that would generate ' +
|
||||
'one is linked into the build.',
|
||||
'No crash reporter, no advertising ID, no analytics identifier, and no library ' +
|
||||
'that would generate one is linked into the build.',
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ export const legal = {
|
||||
* 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.
|
||||
*/
|
||||
lastUpdated: '2026-09-01',
|
||||
lastUpdated: '2026-10-29',
|
||||
|
||||
/**
|
||||
* The minimum age to sign up for the beta. The org lead's decision, 2026-08-24 (D31).
|
||||
|
||||
Reference in New Issue
Block a user