more stubs

This commit is contained in:
Devin Zuczek
2026-08-15 13:54:19 -04:00
parent 8ea0caa1e5
commit 11b037a2f1
67 changed files with 38566 additions and 2652 deletions
+15 -1
View File
@@ -662,6 +662,19 @@ const app = new Hono<App>()
typeof body.device_class === 'string' ? Number.parseInt(body.device_class, 10) : NaN
const deviceClass = Number.isNaN(deviceClassInt) ? 0 : deviceClassInt
// 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).
//
// 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
// treated as absent for the same reason: presence must never carry an empty
// version, which breaks the client's handling of it.
const version = typeof body.ver === 'string' && body.ver !== '' ? body.ver : undefined
// The client's real IP, per Cloudflare (the client can't spoof CF-Connecting-IP —
// the edge sets it — unlike X-Forwarded-For, which is why we don't read that).
// Recorded as the immutable `signupIp` at creation and as `lastLoginIp` on every
@@ -1033,7 +1046,8 @@ const app = new Hono<App>()
platform,
jwtSecret,
accountRoles(roleAccount),
accountPrivileges(roleAccount)
accountPrivileges(roleAccount),
version
)
// Issue a fresh, persisted refresh token (single-use; the client redeems it via
// grant_type=refresh_token). A refresh grant thus rotates its token.
+8
View File
@@ -170,6 +170,14 @@ export const TokenRequest = z.object({
.optional()
.describe('Client-chosen, unverified. Recorded on the account, never trusted'),
device_class: z.string().optional().describe('Integer string; defaults to 0'),
ver: z
.string()
.optional()
.describe(
'The clients build, e.g. `20250718.01`. Stamped into the tokens `rn.ver` claim and ' +
'read back by `match` when it writes presence, so a player reports the build they ' +
'are running. Absent (or empty) falls back to the servers GAME_VERSION'
),
})
/** `POST /account/me/changepassword` form body. */
@@ -5,6 +5,7 @@ import { beforeAll, describe, expect, test } from 'vitest'
import '../../auth.app'
import {
GAME_VERSION,
getAccountsByDeviceId,
hashPassword,
PRESENCE_SCHEMA_DDL,
@@ -579,6 +580,25 @@ describe('auth worker routes', () => {
expect(payload.scope).toContain('rn.api')
})
// `rn.ver` is the CLIENT's build, from the `ver` it posts here — presence in `match`
// reads it back off the token, so this is what a player is reported as running.
test('POST /connect/token stamps the posted ver into rn.ver', async () => {
const payload = await tokenFor(`account_id=42&password=${LOGIN_PASSWORD}&ver=20250718.01`)
expect(payload['rn.ver']).toBe('20250718.01')
})
// A grant that names no build — a refresh, or a caller that isn't the game — falls
// back to the server's GAME_VERSION rather than stamping an empty claim, which would
// leave presence carrying an empty version.
test('POST /connect/token falls back to GAME_VERSION with no ver', async () => {
expect((await tokenFor(`account_id=42&password=${LOGIN_PASSWORD}`))['rn.ver']).toBe(
GAME_VERSION
)
expect((await tokenFor(`account_id=42&password=${LOGIN_PASSWORD}&ver=`))['rn.ver']).toBe(
GAME_VERSION
)
})
test('POST /connect/token stamps developer/moderator roles into the token', async () => {
await env.DB.prepare('INSERT OR IGNORE INTO account (data) VALUES (?1)')
.bind(