[plus] discord role verifier to grant RR plus

This commit is contained in:
Devin Zuczek
2026-08-31 15:44:33 -04:00
parent 740e9efa09
commit 8260c5abcd
23 changed files with 1746 additions and 98 deletions
+24
View File
@@ -101,6 +101,30 @@ export interface Account {
* `runx admin grant-moderator`. Absent/false means no role.
*/
isModerator?: boolean
/**
* Whether this account has Rec Room Plus — the paid tier the client's API calls a
* `CampusCard`. Nothing SELLS one here. Absent/false means no Plus.
*
* This flag ALONE is what confers it, and it stands on its own: two things set it, and
* neither is a precondition of the other.
*
* - the website's benefits claim (`www` `POST /api/benefits/claim`), where a player
* proves a qualifying role in the community Discord. That path also links their
* Discord identity into `platform_account` as a `PlatformType.Discord` row — but the
* link exists to keep the CLAIM once-only per Discord user, not to justify the flag.
* - an operator, via `runx admin grant-plus`, with no Discord anywhere in sight.
*
* So never read a Discord link as a precondition for Plus, and never revoke one because
* the other is missing: a manually granted account has `hasPlus` and no link at all, and
* that is a normal, supported state.
*
* Nothing reads this per request. `auth` stamps it into every token it mints as the
* `rn.plus` claim, and `econ` decides the CampusCard and the subscriber discount from
* that claim alone — so setting it takes effect on the account's NEXT login, not
* immediately. Tokens last a day and the client never refreshes them, so that lag is
* real: the website's claim page warns about it, and so does `grant-plus`.
*/
hasPlus?: boolean
}
interface AccountRow {
+33
View File
@@ -5,6 +5,39 @@
* the tsconfig sets `isolatedModules` (which disallows `const enum` across files).
*/
/**
* PlatformType, the client's platform enum. Declaration order is wire order. The
* `platform` form field is posted as the integer; a token's `platform` claim carries it
* too. `auth` re-exports this as the source for its OpenAPI schema and description.
*
* A plain `as const` object rather than an `enum` like its neighbours, and deliberately
* so: `auth` builds `PlatformTypeSchema`'s description by walking `Object.entries`, and a
* numeric TS enum also emits a REVERSE mapping (`{ '0': 'Steam', Steam: 0, … }`), which
* would double every member in the generated spec.
*
* Everything from `Steam` to `Pico` is a real Rec Room client platform, numbered by the
* client. `Discord` is OURS — it is not a platform anyone signs in from, and the client
* never sends it. It exists so a verified Discord identity can be stored as an account
* link like any other external identity (see `auth`'s platform-db and the website's
* benefits claim); it sits at 101, well clear of the client's range, so a future client
* platform can be added without colliding with it.
*/
export const PlatformType = {
All: -1,
Steam: 0,
Oculus: 1,
PlayStation: 2,
Xbox: 3,
RecNet: 4,
IOS: 5,
GooglePlay: 6,
Standalone: 7,
Pico: 8,
Discord: 101,
} as const
export type PlatformType = (typeof PlatformType)[keyof typeof PlatformType]
/** The kind of a room instance (live session), matching the client's `RoomInstanceType`. */
export enum RoomInstanceType {
Public = 0,
+14 -2
View File
@@ -12,9 +12,21 @@
const ITERATIONS = 100_000
const b64 = (bytes: Uint8Array): string => btoa(String.fromCharCode(...bytes))
const fromB64 = (s: string): Uint8Array => Uint8Array.from(atob(s), (ch) => ch.charCodeAt(0))
const fromB64 = (s: string): Uint8Array<ArrayBuffer> =>
Uint8Array.from(atob(s), (ch) => ch.charCodeAt(0))
async function deriveBits(password: string, salt: Uint8Array): Promise<Uint8Array> {
/**
* The salt is `Uint8Array<ArrayBuffer>` rather than a bare `Uint8Array` because the latter
* is `Uint8Array<ArrayBufferLike>`, which admits a `SharedArrayBuffer` — and the DOM lib's
* `BufferSource` does not. Both callers already produce a plain-ArrayBuffer view
* (`getRandomValues` and `fromB64`), so this only writes down what was always true; without
* it, any worker whose tsconfig includes the DOM lib (`www`, for its React client) fails to
* compile on the `deriveBits` call below.
*/
async function deriveBits(
password: string,
salt: Uint8Array<ArrayBuffer>
): Promise<Uint8Array<ArrayBuffer>> {
const keyMaterial = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(password),