[2026] rollout support, to be short lived I suppose

This commit is contained in:
Devin Zuczek
2026-10-19 22:54:18 -04:00
parent a3291bc9ce
commit 05d7fe4c32
13 changed files with 462 additions and 56 deletions
+16
View File
@@ -17,6 +17,22 @@ export type Env = SharedHonoEnv & {
DOMAIN: string
/** Maximum accepted size of each API-owned image upload, in bytes. */
RECFLARE_MAX_API_UPLOAD_BYTES?: string
/**
* The client builds `/api/versioncheck/v4` answers "current" for, as a comma-separated
* list of builds (`20230414,20250718`), or `off` for none. Unset means the default
* `SUPPORTED_GAME_VERSIONS` in @repo/domain. Set for every worker from the root `.env`
* (`RECFLARE_SUPPORTED_GAME_VERSIONS`), so `auth` refuses a token to exactly the builds
* this flags for an update. Read through `supportedGameBuilds`, which also folds in
* every build under rollout (below).
*/
SUPPORTED_GAME_VERSIONS?: string
/**
* The builds under staged rollout (`RECFLARE_ROLLOUT_GAME_VERSIONS`), same format;
* `off` ends the rollout. Unset means the default `ROLLOUT_GAME_VERSIONS` in
* @repo/domain. A rollout build is always "current" here — it is `auth` that decides who
* may sign in on it — so the version check never sends a supporter to update.
*/
ROLLOUT_GAME_VERSIONS?: string
// Shared rooms database (schema/migrations owned by the `rooms` worker). Used
// read-only here to resolve room roles for `/api/rooms/v1/verifyRole`.
DB: D1Database
+8 -4
View File
@@ -1,7 +1,7 @@
import { Hono } from 'hono'
import { describeRoute } from 'hono-openapi'
import { GAME_VERSION, isSupportedGameVersion } from '@repo/domain'
import { GAME_VERSION, isSupportedGameVersion, supportedGameBuilds } from '@repo/domain'
import { validateAndGetBuild } from '@repo/jwt'
import apiConfigV2 from '../../static/api-config-v2.json'
@@ -106,13 +106,17 @@ export const configRoutes = new Hono<App>({ strict: false })
summary: 'Client version check',
description:
'Whether the client build is current. Compares the client’s `?v=` build against ' +
'the builds we serve (`SUPPORTED_GAME_VERSIONS`): `VersionStatus` is 0 when the ' +
'client is on one of them, 1 when it is on some other build.',
'the builds we serve — the operator’s `SUPPORTED_GAME_VERSIONS` var (or the ' +
'default list in `@repo/domain`) plus every build under rollout ' +
'(`ROLLOUT_GAME_VERSIONS`): `VersionStatus` is 0 when the client is on one of ' +
'them, 1 when it is on some other build. The same lists decide which builds ' +
'`auth` hands a token to, so a build dropped from them is told to update here ' +
'and refused there.',
responses: { 200: json(VersionCheck, 'Version status') },
}),
(c) =>
c.json({
VersionStatus: isSupportedGameVersion(c.req.query('v')) ? 0 : 1,
VersionStatus: isSupportedGameVersion(c.req.query('v'), supportedGameBuilds(c.env)) ? 0 : 1,
UpdateNotificationStage: 0,
IsVersionIslanded: false,
IsCrossPlayDisabled: false,
+50
View File
@@ -323,6 +323,56 @@ describe('public endpoints', () => {
expect(await res.json()).toMatchObject({ VersionStatus: 1 })
})
// The list is the operator's SUPPORTED_GAME_VERSIONS var when set, plus every build
// under rollout (ROLLOUT_GAME_VERSIONS) — the same lists `auth` gates tokens on, so a
// build told to update here is refused there too.
test('GET /api/versioncheck/v4 reads the lists from vars', async () => {
const status = async (version: string) => {
const res = await exports.default.fetch(`${ORIGIN}/api/versioncheck/v4?v=${version}`)
return ((await res.json()) as { VersionStatus: number }).VersionStatus
}
const originals = {
SUPPORTED_GAME_VERSIONS: env.SUPPORTED_GAME_VERSIONS,
ROLLOUT_GAME_VERSIONS: env.ROLLOUT_GAME_VERSIONS,
}
try {
// Forced upgrade: the 2025 build drops out, every point release of it.
env.SUPPORTED_GAME_VERSIONS = '20230414,20260323'
expect(await status('20250718.01')).toBe(1)
expect(await status('20250718')).toBe(1)
expect(await status(GAME_VERSION)).toBe(0)
expect(await status('20260323.14')).toBe(0)
// Alphas not named in the var are gone too: the var replaces the default list.
expect(await status('20231207')).toBe(1)
// A build under rollout is current even when the var omits it: who may sign in on
// it is `auth`'s call, and the version check must not send a supporter to update.
env.SUPPORTED_GAME_VERSIONS = '20230414'
expect(await status('20260323.14')).toBe(0)
env.ROLLOUT_GAME_VERSIONS = 'off'
expect(await status('20260323.14')).toBe(1)
env.ROLLOUT_GAME_VERSIONS = '20250718'
expect(await status('20250718.01')).toBe(0)
// Entries are builds: a copied point release matches, and `off` empties the list.
env.ROLLOUT_GAME_VERSIONS = undefined
env.SUPPORTED_GAME_VERSIONS = '20250718.01'
expect(await status('20250718.2')).toBe(0)
expect(await status(GAME_VERSION)).toBe(1)
env.SUPPORTED_GAME_VERSIONS = 'off'
env.ROLLOUT_GAME_VERSIONS = 'off'
for (const version of SUPPORTED_GAME_VERSIONS) expect(await status(version), version).toBe(1)
// Blank is unset: the default list.
env.SUPPORTED_GAME_VERSIONS = ''
env.ROLLOUT_GAME_VERSIONS = ''
for (const version of SUPPORTED_GAME_VERSIONS) expect(await status(version), version).toBe(0)
} finally {
env.SUPPORTED_GAME_VERSIONS = originals.SUPPORTED_GAME_VERSIONS
env.ROLLOUT_GAME_VERSIONS = originals.ROLLOUT_GAME_VERSIONS
}
})
// Two catalogs, picked off the token's `rn.ver`: a build newer than GAME_VERSION gets
// the 2025 one. `Avatars.AdvancedFaceCustomizationEnabled` is a key only that catalog
// has, so its presence identifies which body was served.
+60 -23
View File
@@ -15,12 +15,15 @@ import {
getRoomById,
hashPassword,
isRolloutGameVersion,
isSupportedGameVersion,
rolloutGameBuilds,
RoomInstanceType,
setLastLoginTime,
setLoginContext,
setPasswordHash,
setPresence,
subRoomDataBlob,
supportedGameBuilds,
updateAccount,
verifyPassword,
writeAuditLog,
@@ -175,25 +178,34 @@ const DEFAULT_PASSWORD_SIGNUP = false
const PASSWORD_SIGNUP_DISABLED_DESCRIPTION = 'password signup is disabled'
/**
* The rollout gate. A `/connect/token` grant posting a build under staged rollout as `ver`
* (`ROLLOUT_GAME_VERSIONS` in @repo/domain — shared with `match`, which serves those builds
* the sandbox Tachyon pool) is refused unless the account is STAFF (developer or moderator)
* or its Discord link holds a role named in `DISCORD_ROLLOUT_ROLE_IDS` (see context.ts and
* `accountMayUseRolloutBuild`). Every other build, and a grant posting no `ver` (a refresh,
* the website), is untouched.
* The build gates on `/connect/token`, both keyed on the grant's posted `ver` and both
* driven by the operator's game-version vars (`SUPPORTED_GAME_VERSIONS`,
* `ROLLOUT_GAME_VERSIONS` — see context.ts and `supportedGameBuilds`/`rolloutGameBuilds`
* in @repo/domain, which `api` and `match` read the same way). A grant posting no `ver`
* (a refresh, the website) is untouched by either.
*
* 1. The SUPPORT gate: a build outside `supportedGameBuilds` is refused as `invalid_grant`
* with UNSUPPORTED_BUILD_DESCRIPTION, for every grant and every account — staff
* included. `api`'s `/api/versioncheck/v4` tells the same builds to update; this is what
* makes the update prompt stick, since a client that got a token anyway would play on.
* Dropping a build from the list is how its players are made to upgrade.
* 2. The ROLLOUT gate: a build under staged rollout is refused unless the account is STAFF
* (developer or moderator) or its Discord link holds a role named in
* `DISCORD_ROLLOUT_ROLE_IDS` (see `accountMayUseRolloutBuild`), as `invalid_grant` with
* ROLLOUT_GATED_DESCRIPTION. `create_account` is refused before anything is minted: a
* brand-new account has no Discord link to hold a role, so the player makes their
* account on a released build, links Discord on the website, and comes back. Ending the
* rollout (`ROLLOUT_GAME_VERSIONS=off`) opens the build to everyone.
*
* `ver` is client-reported and unverified, like everything else about the build. That is
* fine for what this is: a rollout gate, not a security boundary. A player who lies about
* their build gets a token the 2023 server surfaces would have handed them anyway, and
* `api`'s `/api/versioncheck/v4` is still what decides which builds are supported at all.
*
* Refused as `invalid_grant` with ROLLOUT_GATED_DESCRIPTION. `create_account` is refused
* before anything is minted: a brand-new account has no Discord link to hold a role, so
* the player makes their account on a released build, links Discord on the website, and
* comes back.
* fine for what these are: rollout and upgrade gates, not a security boundary. A player
* who lies about their build gets a token the surfaces of the build they claim would have
* handed them anyway, and then runs a client the server isn't answering for.
*/
const ROLLOUT_GATED_DESCRIPTION =
'this build is limited to Discord supporters; link your Discord on the website first'
const UNSUPPORTED_BUILD_DESCRIPTION =
'this game version is not supported by this server; update your game'
/**
* `DISCORD_ROLLOUT_ROLE_IDS` as a list of role ids: snowflakes separated by commas and/or
@@ -988,8 +1000,16 @@ const app = new Hono<App>()
'act from signing into one. The description is deliberately vague — the signup',
'refused may belong to a housemate of the banned player rather than to them.',
'',
'**Support gate.** A grant whose `ver` names a build this server does not serve —',
'outside the operator’s `SUPPORTED_GAME_VERSIONS` (default: the list in `@repo/domain`)',
'and not under rollout — is refused as `invalid_grant` "this game version is not',
'supported by this server; update your game", for every grant and every account.',
'`api`’s `/api/versioncheck/v4` flags the same builds, so dropping a build from the',
'list is how its players are made to upgrade. Grants posting no `ver` are untouched.',
'',
'**Rollout gate.** A grant whose `ver` names a build under staged rollout',
'(`ROLLOUT_GAME_VERSIONS`, currently `20260323` — every point release of it) is refused as `invalid_grant` "this build is limited to',
'(`ROLLOUT_GAME_VERSIONS`, by default `20260323` — every point release of it; `off` ends',
'the rollout and opens the build to everyone) is refused as `invalid_grant` "this build is limited to',
'Discord supporters" unless the account is staff (developer or moderator) or its',
'Discord link (made by the website’s benefits claim) holds one of the roles in the',
'operator’s `DISCORD_ROLLOUT_ROLE_IDS`. With none configured the gate is closed to',
@@ -1075,9 +1095,9 @@ const app = new Hono<App>()
// The client's own build (`ver`, e.g. `20250718.01`), stamped into the token's
// `rn.ver` claim so everything downstream reports the build the player is ACTUALLY
// running rather than this server's GAME_VERSION — `match` reads it back off the
// token when it writes presence. Unverified like the device fields, and only ever
// echoed, never trusted for a decision (the version CHECK is `api`'s
// `/api/versioncheck/v4`, against its own list).
// token when it writes presence. Unverified like the device fields: echoed, and
// gated (below, and the rollout gate further down), but never trusted as a
// credential.
//
// A grant that posts none — a refresh, or a caller that isn't the game — leaves it
// undefined and generateToken falls back to GAME_VERSION. An empty string is
@@ -1091,6 +1111,23 @@ const app = new Hono<App>()
// login; both feed the per-IP signup cap. Absent (empty) outside the CF edge.
const clientIp = c.req.header('cf-connecting-ip') ?? ''
// The support gate (see UNSUPPORTED_BUILD_DESCRIPTION): a posted build this server
// doesn't serve gets no token, whatever the grant and whoever the account. Decided
// before any credential is read — it depends on nothing but the operator's list, so
// there is nothing about an account to probe here, and a closed door costs no D1
// round trip. The same list `api`'s `/api/versioncheck/v4` flags for an update.
if (version !== undefined && !isSupportedGameVersion(version, supportedGameBuilds(c.env))) {
logger.info('token refused: game version is not supported', {
grantType,
version,
ip: clientIp,
})
return c.json(
{ error: 'invalid_grant', error_description: UNSUPPORTED_BUILD_DESCRIPTION },
400
)
}
// A platform-authenticated login proves who you are with the platform itself, and
// we can verify exactly two: Steam (0), from its Steam-signed platform_auth ticket,
// and Meta/Oculus (1), by asking Meta to validate the nonce in platform_auth (see
@@ -1194,7 +1231,7 @@ const app = new Hono<App>()
// link, so it could never hold the role the gate wants. Refused before anything
// is read or minted, like the switch below — no row left behind, no cap slot
// burned.
if (isRolloutGameVersion(version)) {
if (isRolloutGameVersion(version, rolloutGameBuilds(c.env))) {
logger.info('signup refused: build is limited to Discord supporters', {
version,
ip: clientIp,
@@ -1432,15 +1469,15 @@ const app = new Hono<App>()
await setLoginContext(c.env.DB, resolvedId, { deviceId, deviceClass, ip: clientIp })
}
// The rollout gate: a build in ROLLOUT_GAME_VERSIONS is for staff and Discord
// supporters only, so the account signing in on it must be staff or hold one of the
// configured roles.
// The rollout gate: a build under staged rollout (`rolloutGameBuilds`) is for staff
// and Discord supporters only, so the account signing in on it must be staff or hold
// one of the configured roles.
// Deliberately AFTER the credential checks, like the ban lookup below — a wrong
// password on a gated build is still "invalid account_id or password", so the
// gate can't be used to probe whether an account exists. Unlike a ban, this IS a
// refusal: there is no block screen to reach, and the client surfaces the
// description on its sign-in failure.
if (isRolloutGameVersion(version)) {
if (isRolloutGameVersion(version, rolloutGameBuilds(c.env))) {
const roleIds = rolloutRoleIds(c.env.DISCORD_ROLLOUT_ROLE_IDS)
if (!(await accountMayUseRolloutBuild(c.env.DB, Number(accountId), roleIds))) {
logger.info('login refused: build is limited to Discord supporters', {
+19
View File
@@ -64,6 +64,25 @@ export type Env = SharedHonoEnv & {
* Logins on every other build are never touched by this.
*/
DISCORD_ROLLOUT_ROLE_IDS?: string
/**
* The client builds a `/connect/token` grant may post as `ver` and still get a token:
* a comma-separated list of builds (`20230414,20250718`), or `off` for none. Unset means
* the default `SUPPORTED_GAME_VERSIONS` in @repo/domain. Set for every worker from the
* root `.env` (`RECFLARE_SUPPORTED_GAME_VERSIONS`), so this refuses exactly the builds
* `api`'s `/api/versioncheck/v4` tells to update — dropping a build from the list is how
* its players are made to upgrade. Read through `supportedGameBuilds`, which also folds
* in every build under rollout (below). A grant posting no `ver` is never gated.
*/
SUPPORTED_GAME_VERSIONS?: string
/**
* The builds under staged rollout — the ones the Discord-role gate above applies to —
* overriding `ROLLOUT_GAME_VERSIONS` in @repo/domain: same format, or `off` to end the
* rollout and open those builds to every account. Set for every worker
* (`RECFLARE_ROLLOUT_GAME_VERSIONS`), so `match`'s rollout voice pool follows the same
* list. A build here is served whether or not `SUPPORTED_GAME_VERSIONS` names it. Read
* through `rolloutGameBuilds`.
*/
ROLLOUT_GAME_VERSIONS?: string
/**
* Base domain, e.g. `recflare.net`. Injected for every worker at deploy time
* (`--var DOMAIN`). Studio's device-login response points the browser at
+134
View File
@@ -1415,6 +1415,140 @@ describe('auth worker routes', () => {
expect(after?.n).toBe(before?.n)
})
})
// The two lists behind the gates are vars (`SUPPORTED_GAME_VERSIONS`,
// `ROLLOUT_GAME_VERSIONS`), so an operator can end the rollout or drop a build without
// a deploy of new code. Same env-override discipline as withRoleIds.
describe('the lists come from vars', () => {
const UNSUPPORTED = 'this game version is not supported by this server; update your game'
async function withVars<T>(
vars: { SUPPORTED_GAME_VERSIONS?: string; ROLLOUT_GAME_VERSIONS?: string },
run: () => Promise<T>
): Promise<T> {
const originals = {
SUPPORTED_GAME_VERSIONS: env.SUPPORTED_GAME_VERSIONS,
ROLLOUT_GAME_VERSIONS: env.ROLLOUT_GAME_VERSIONS,
}
const apply = (values: typeof originals) => {
for (const key of ['SUPPORTED_GAME_VERSIONS', 'ROLLOUT_GAME_VERSIONS'] as const) {
if (values[key] === undefined) delete env[key]
else env[key] = values[key]
}
}
apply({ ...originals, ...vars })
try {
return await run()
} finally {
apply(originals)
}
}
test('a build dropped from SUPPORTED_GAME_VERSIONS is refused a token, staff included', async () => {
await withVars({ SUPPORTED_GAME_VERSIONS: '20230414,20260323' }, async () => {
await withRoleIds(undefined, async () => {
// The 2025 build, every point release of it: this is the forced upgrade.
for (const version of ['20250718', '20250718.01', '20250718.2']) {
const res = await login('gate_outsider', version)
expect(res.status, version).toBe(400)
expect(res.json, version).toEqual({
error: 'invalid_grant',
error_description: UNSUPPORTED,
})
}
// Staff get no exception here — the build is not served, full stop.
expect((await login('gate_developer', '20250718.01')).status).toBe(400)
// The builds that ARE listed, and a grant posting no `ver`, are untouched;
// the rollout gate still applies to the one under rollout.
expect((await login('gate_outsider', GAME_VERSION)).status).toBe(200)
expect((await login('gate_outsider')).status).toBe(200)
expect((await login('gate_developer', GATED)).status).toBe(200)
expect((await login('gate_outsider', GATED)).json.error_description).toBe(REFUSED)
})
})
})
test('create_account on an unsupported build is refused before anything is minted', async () => {
await withVars({ SUPPORTED_GAME_VERSIONS: '20230414' }, async () => {
const before = await env.DB.prepare('SELECT COUNT(*) AS n FROM account').first<{
n: number
}>()
const res = await postToken(
'grant_type=create_account&platform_id=steam-unsupported&password=hunter2&ver=20250718.01'
)
expect(res.status).toBe(400)
expect(res.json).toEqual({ error: 'invalid_grant', error_description: UNSUPPORTED })
const after = await env.DB.prepare('SELECT COUNT(*) AS n FROM account').first<{
n: number
}>()
expect(after?.n).toBe(before?.n)
})
})
test('ROLLOUT_GAME_VERSIONS=off ends the rollout: the build opens to everyone', async () => {
await withVars({ ROLLOUT_GAME_VERSIONS: 'off' }, async () => {
await withRoleIds(undefined, async () => {
const res = await login('gate_outsider', GATED)
expect(res.status).toBe(200)
expect(decodePayload(res.json.access_token as string)['rn.ver']).toBe(GATED)
expect((await login('gate_unlinked', GATED)).status).toBe(200)
// ...and accounts can be created on it, since nothing needs a Discord link.
const created = await postToken(
`grant_type=create_account&platform_id=steam-rollout-over&password=hunter2&ver=${GATED}`
)
expect(created.status).toBe(200)
})
})
})
test('ROLLOUT_GAME_VERSIONS names which builds are gated', async () => {
// Move the gate onto the beta build: it is now supporters-only and the 2026
// build is open. Both stay served.
await withVars({ ROLLOUT_GAME_VERSIONS: '20250718' }, async () => {
await withRoleIds(supporter, async () => {
const outsider = await login('gate_outsider', '20250718.01')
expect(outsider.status).toBe(400)
expect(outsider.json.error_description).toBe(REFUSED)
expect((await login('gate_supporter', '20250718.01')).status).toBe(200)
expect((await login('gate_outsider', GATED)).status).toBe(200)
})
})
})
test('a build under rollout is served even when SUPPORTED_GAME_VERSIONS omits it', async () => {
await withVars({ SUPPORTED_GAME_VERSIONS: '20230414' }, async () => {
await withRoleIds(supporter, async () => {
// Rolling out, so the ROLLOUT gate answers, not the support one.
expect((await login('gate_supporter', GATED)).status).toBe(200)
expect((await login('gate_outsider', GATED)).json.error_description).toBe(REFUSED)
})
})
// In neither list, nobody reaches it — a rollout pulled back entirely.
await withVars(
{ SUPPORTED_GAME_VERSIONS: '20230414', ROLLOUT_GAME_VERSIONS: 'off' },
async () => {
await withRoleIds(supporter, async () => {
for (const username of ['gate_supporter', 'gate_developer']) {
const res = await login(username, GATED)
expect(res.status, username).toBe(400)
expect(res.json.error_description, username).toBe(UNSUPPORTED)
}
})
}
)
})
test('entries are read as builds, so a copied point release still matches', async () => {
await withVars({ SUPPORTED_GAME_VERSIONS: ' 20230414, 20250718.01 ' }, async () => {
expect((await login('gate_outsider', '20250718.2')).status).toBe(200)
expect((await login('gate_outsider', '20250718')).status).toBe(200)
})
// Blank is unset: the default list applies, including the alpha builds.
await withVars({ SUPPORTED_GAME_VERSIONS: ' ' }, async () => {
expect((await login('gate_outsider', '20231207')).status).toBe(200)
})
})
})
})
test('the signup caps come from vars, and 0 disables an arm', async () => {
+9
View File
@@ -83,6 +83,15 @@ export type Env = SharedHonoEnv & {
* means rollout callers use the live pool. See `tachyonPoolFor` in match.app.ts.
*/
TACHYON_HOST_PORT_ROLLOUT?: string
/**
* The builds under staged rollout, overriding `ROLLOUT_GAME_VERSIONS` in @repo/domain:
* a comma-separated list of builds, or `off` for none. Set for every worker from the
* root `.env` (`RECFLARE_ROLLOUT_GAME_VERSIONS`), so this agrees with the gate `auth`
* applies by construction. Read through `rolloutGameBuilds`; decides who is served the
* rollout Tachyon pool above. Ending the rollout (`off`) moves those callers to the
* live pool.
*/
ROLLOUT_GAME_VERSIONS?: string
/**
* The Photon region every session is pinned to — both the region named in the connection
* info and the one stamped on every room instance, which must agree. Optional; unlike the
+4 -3
View File
@@ -49,6 +49,7 @@ import {
recordStat,
refreshInstanceFullness,
Role,
rolloutGameBuilds,
RoomInstanceType,
roomRoles,
setPresence,
@@ -253,7 +254,7 @@ const ROLLOUT_TACHYON_SERVER_ID_PREFIX = 'rollout-'
interface TachyonCaller {
/** The account's `isSandbox` flag. */
isSandbox: boolean
/** Running a build under staged rollout (`ROLLOUT_GAME_VERSIONS`, off the token's `rn.ver`). */
/** Running a build under staged rollout (`rolloutGameBuilds`, off the token's `rn.ver`). */
isRollout: boolean
}
@@ -3216,7 +3217,7 @@ const app = new Hono<App>()
'…). Both are empty when the pool is unset or the caller is in no instance. A',
'sandbox account is picked out of the `TACHYON_HOST_PORT_SANDBOX` pool instead, the',
'same way, with ids `dev-1`, `dev-2`, …; a caller whose token’s `rn.ver` is a build',
'under staged rollout (`ROLLOUT_GAME_VERSIONS`, currently `20260323`) out of',
'under staged rollout (`ROLLOUT_GAME_VERSIONS`, by default `20260323`) out of',
'`TACHYON_HOST_PORT_ROLLOUT`, with ids `rollout-1`, `rollout-2`, …. Sandbox wins when',
'both apply, and either special pool left unset falls back to the live one.',
'`experiments` carries the client’s networking flags.',
@@ -3264,7 +3265,7 @@ const app = new Hono<App>()
const tachyon = tachyonServerFor(
tachyonPoolFor(c.env, {
isSandbox: account?.isSandbox === true,
isRollout: isRolloutGameVersion(await callerVersion(c)),
isRollout: isRolloutGameVersion(await callerVersion(c), rolloutGameBuilds(c.env)),
}),
roomInstanceId
)
@@ -1814,6 +1814,7 @@ describe('auth-gated endpoints', () => {
const original = env.TACHYON_HOST_PORT
const originalSandbox = env.TACHYON_HOST_PORT_SANDBOX
const originalRollout = env.TACHYON_HOST_PORT_ROLLOUT
const originalRolloutVersions = env.ROLLOUT_GAME_VERSIONS
try {
env.TACHYON_HOST_PORT = '198.51.100.12:7777'
env.TACHYON_HOST_PORT_SANDBOX = '203.0.113.30:7777, 203.0.113.30:7778'
@@ -1905,6 +1906,30 @@ describe('auth-gated endpoints', () => {
})
}
// WHICH builds are under rollout is the ROLLOUT_GAME_VERSIONS var, the same one
// `auth` gates on. `off` ends the rollout and its callers join the live pool; naming
// another build moves the pool to that build's players instead.
const movedRollout = await createRoomInstance(env.DB, {
ownerAccountId: 976,
roomId: 2,
photonRoomId: 'tachyon-rollout-moved',
maxCapacity: 12,
})
env.ROLLOUT_GAME_VERSIONS = 'off'
expect(await rolloutVoiceFor('976', '20260323.14', movedRollout.roomInstanceId)).toEqual({
voiceConnectionInfo: '198.51.100.12:7777',
voiceServerId: 'tachyon-1',
})
env.ROLLOUT_GAME_VERSIONS = '20250718'
expect(await rolloutVoiceFor('976', '20250718.01', movedRollout.roomInstanceId)).toEqual(
rolloutPool[movedRollout.roomInstanceId % rolloutPool.length]
)
expect(await rolloutVoiceFor('976', '20260323.14', movedRollout.roomInstanceId)).toEqual({
voiceConnectionInfo: '198.51.100.12:7777',
voiceServerId: 'tachyon-1',
})
env.ROLLOUT_GAME_VERSIONS = originalRolloutVersions
// With no rollout pool configured, a rollout build falls back to the live one, not
// the sandbox one.
env.TACHYON_HOST_PORT_ROLLOUT = undefined
@@ -1935,6 +1960,7 @@ describe('auth-gated endpoints', () => {
env.TACHYON_HOST_PORT = original
env.TACHYON_HOST_PORT_SANDBOX = originalSandbox
env.TACHYON_HOST_PORT_ROLLOUT = originalRollout
env.ROLLOUT_GAME_VERSIONS = originalRolloutVersions
}
})