mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 22:51:30 -07:00
add version check for 20230414 client
This commit is contained in:
@@ -135,9 +135,9 @@ export const ApiConfigV2 = JsonObject.describe(
|
|||||||
'The static client config, plus a ShareBaseUrl templated from the deploy domain'
|
'The static client config, plus a ShareBaseUrl templated from the deploy domain'
|
||||||
)
|
)
|
||||||
|
|
||||||
/** `GET /api/versioncheck/v4` — always the "you are up to date" answer. */
|
/** `GET /api/versioncheck/v4` — whether the client's `?v=` build matches GAME_VERSION. */
|
||||||
export const VersionCheck = z.object({
|
export const VersionCheck = z.object({
|
||||||
VersionStatus: z.int().describe('0 = current'),
|
VersionStatus: z.int().describe('0 = current, 1 = client on a different build'),
|
||||||
UpdateNotificationStage: z.int(),
|
UpdateNotificationStage: z.int(),
|
||||||
IsVersionIslanded: z.boolean(),
|
IsVersionIslanded: z.boolean(),
|
||||||
IsCrossPlayDisabled: z.boolean(),
|
IsCrossPlayDisabled: z.boolean(),
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Hono } from 'hono'
|
import { Hono } from 'hono'
|
||||||
import { describeRoute } from 'hono-openapi'
|
import { describeRoute } from 'hono-openapi'
|
||||||
|
|
||||||
|
import { GAME_VERSION } from '@repo/domain'
|
||||||
|
|
||||||
import apiConfigV2 from '../../static/api-config-v2.json'
|
import apiConfigV2 from '../../static/api-config-v2.json'
|
||||||
import gameConfigsV1All from '../../static/gameconfigs-v1-all.json'
|
import gameConfigsV1All from '../../static/gameconfigs-v1-all.json'
|
||||||
import {
|
import {
|
||||||
@@ -97,13 +99,14 @@ export const configRoutes = new Hono<App>({ strict: false })
|
|||||||
tags: ['Config'],
|
tags: ['Config'],
|
||||||
summary: 'Client version check',
|
summary: 'Client version check',
|
||||||
description:
|
description:
|
||||||
'Whether the client build is current. Always the “up to date, nothing islanded” ' +
|
'Whether the client build is current. Compares the client’s `?v=` build against ' +
|
||||||
'answer — this server does not gate on client version.',
|
'our target `GAME_VERSION`: `VersionStatus` is 0 when they match, 1 when the ' +
|
||||||
responses: { 200: json(VersionCheck, 'Always current') },
|
'client is on a different build.',
|
||||||
|
responses: { 200: json(VersionCheck, 'Version status') },
|
||||||
}),
|
}),
|
||||||
(c) =>
|
(c) =>
|
||||||
c.json({
|
c.json({
|
||||||
VersionStatus: 0,
|
VersionStatus: c.req.query('v') === GAME_VERSION ? 0 : 1,
|
||||||
UpdateNotificationStage: 0,
|
UpdateNotificationStage: 0,
|
||||||
IsVersionIslanded: false,
|
IsVersionIslanded: false,
|
||||||
IsCrossPlayDisabled: false,
|
IsCrossPlayDisabled: false,
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { adminSecretsStore, env } from 'cloudflare:test'
|
|||||||
import { exports } from 'cloudflare:workers'
|
import { exports } from 'cloudflare:workers'
|
||||||
import { beforeAll, describe, expect, test } from 'vitest'
|
import { beforeAll, describe, expect, test } from 'vitest'
|
||||||
|
|
||||||
|
import { GAME_VERSION } from '@repo/domain'
|
||||||
|
|
||||||
import '../../api.app'
|
import '../../api.app'
|
||||||
|
|
||||||
import { createImage, getImageByName, SCHEMA_DDL as IMAGES_SCHEMA_DDL } from '../../images-db'
|
import { createImage, getImageByName, SCHEMA_DDL as IMAGES_SCHEMA_DDL } from '../../images-db'
|
||||||
@@ -135,11 +137,16 @@ describe('public endpoints', () => {
|
|||||||
expect(body).toMatchObject({ ReportBudget: 125, VersionRegex: '.*' })
|
expect(body).toMatchObject({ ReportBudget: 125, VersionRegex: '.*' })
|
||||||
})
|
})
|
||||||
|
|
||||||
test('GET /api/versioncheck/v4', async () => {
|
test('GET /api/versioncheck/v4 reports current for the matching build', async () => {
|
||||||
const res = await exports.default.fetch(`${ORIGIN}/api/versioncheck/v4`)
|
const res = await exports.default.fetch(`${ORIGIN}/api/versioncheck/v4?v=${GAME_VERSION}`)
|
||||||
expect(await res.json()).toMatchObject({ VersionStatus: 0 })
|
expect(await res.json()).toMatchObject({ VersionStatus: 0 })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('GET /api/versioncheck/v4 flags a mismatched build', async () => {
|
||||||
|
const res = await exports.default.fetch(`${ORIGIN}/api/versioncheck/v4?v=19990101`)
|
||||||
|
expect(await res.json()).toMatchObject({ VersionStatus: 1 })
|
||||||
|
})
|
||||||
|
|
||||||
test('GET /api/relationships/v2/get returns empty array for a player with none', async () => {
|
test('GET /api/relationships/v2/get returns empty array for a player with none', async () => {
|
||||||
const res = await exports.default.fetch(`${ORIGIN}/api/relationships/v2/get`, {
|
const res = await exports.default.fetch(`${ORIGIN}/api/relationships/v2/get`, {
|
||||||
headers: await bearer('99999'),
|
headers: await bearer('99999'),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
countAccountsByPlatformId,
|
countAccountsByPlatformId,
|
||||||
countAccountsBySignupIp,
|
countAccountsBySignupIp,
|
||||||
createAccount,
|
createAccount,
|
||||||
|
GAME_VERSION,
|
||||||
getAccount,
|
getAccount,
|
||||||
getAccountByUsername,
|
getAccountByUsername,
|
||||||
getAccountsByPlatformId,
|
getAccountsByPlatformId,
|
||||||
@@ -139,7 +140,7 @@ async function placeNewPlayerInOrientation(
|
|||||||
deviceClass,
|
deviceClass,
|
||||||
vrMovementMode: 1,
|
vrMovementMode: 1,
|
||||||
platform: 0,
|
platform: 0,
|
||||||
appVersion: '20230302',
|
appVersion: GAME_VERSION,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
createRoomInstance,
|
createRoomInstance,
|
||||||
deleteExpiredPresence,
|
deleteExpiredPresence,
|
||||||
deletePresence,
|
deletePresence,
|
||||||
|
GAME_VERSION,
|
||||||
getAccount,
|
getAccount,
|
||||||
getClubSummary,
|
getClubSummary,
|
||||||
getExpiredPresenceInstanceIds,
|
getExpiredPresenceInstanceIds,
|
||||||
@@ -140,13 +141,6 @@ type Presence = StoredPresence<RoomInstance>
|
|||||||
*/
|
*/
|
||||||
const PRESENCE_REFRESH_THRESHOLD = 300
|
const PRESENCE_REFRESH_THRESHOLD = 300
|
||||||
|
|
||||||
/**
|
|
||||||
* Game build version reported in presence. This is a server-side constant — the
|
|
||||||
* client doesn't supply it, and an empty value breaks the client's
|
|
||||||
* presence/version handling. Matches our target 2023 client build.
|
|
||||||
*/
|
|
||||||
const GAME_VERSION = '20230302'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default `/player` payload, served whenever the `id` is missing/invalid or the
|
* Default `/player` payload, served whenever the `id` is missing/invalid or the
|
||||||
* account isn't found. Inlined here (Workers have no filesystem). The stub player
|
* account isn't found. Inlined here (Workers have no filesystem). The stub player
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { beforeAll, describe, expect, test } from 'vitest'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
countPlayersInInstance,
|
countPlayersInInstance,
|
||||||
|
GAME_VERSION,
|
||||||
getRoomInstance,
|
getRoomInstance,
|
||||||
PRESENCE_SCHEMA_DDL,
|
PRESENCE_SCHEMA_DDL,
|
||||||
ROOM_INSTANCE_SCHEMA_DDL,
|
ROOM_INSTANCE_SCHEMA_DDL,
|
||||||
@@ -200,7 +201,7 @@ describe('public endpoints', () => {
|
|||||||
// fields, which only ever carry values in a matchmaking response.
|
// fields, which only ever carry values in a matchmaking response.
|
||||||
expect(await res.json()).toEqual([
|
expect(await res.json()).toEqual([
|
||||||
{
|
{
|
||||||
appVersion: '20230302',
|
appVersion: GAME_VERSION,
|
||||||
deviceClass: 0,
|
deviceClass: 0,
|
||||||
errorCode: 0,
|
errorCode: 0,
|
||||||
isOnline: false,
|
isOnline: false,
|
||||||
@@ -234,7 +235,7 @@ describe('public endpoints', () => {
|
|||||||
const res = await exports.default.fetch(`${ORIGIN}/player`)
|
const res = await exports.default.fetch(`${ORIGIN}/player`)
|
||||||
expect(res.status).toBe(200)
|
expect(res.status).toBe(200)
|
||||||
const players = (await res.json()) as Array<{ playerId: number; isOnline: boolean }>
|
const players = (await res.json()) as Array<{ playerId: number; isOnline: boolean }>
|
||||||
expect(players[0]).toMatchObject({ playerId: 1, isOnline: true, appVersion: '20230302' })
|
expect(players[0]).toMatchObject({ playerId: 1, isOnline: true, appVersion: GAME_VERSION })
|
||||||
})
|
})
|
||||||
|
|
||||||
test('POST /goto/none returns the offline dorm', async () => {
|
test('POST /goto/none returns the offline dorm', async () => {
|
||||||
@@ -702,7 +703,7 @@ describe('auth-gated endpoints', () => {
|
|||||||
deviceClass: 0,
|
deviceClass: 0,
|
||||||
vrMovementMode: 1,
|
vrMovementMode: 1,
|
||||||
platform: 0,
|
platform: 0,
|
||||||
appVersion: '20230302',
|
appVersion: GAME_VERSION,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -761,7 +762,7 @@ describe('auth-gated endpoints', () => {
|
|||||||
deviceClass: 0,
|
deviceClass: 0,
|
||||||
vrMovementMode: 1,
|
vrMovementMode: 1,
|
||||||
platform: 0,
|
platform: 0,
|
||||||
appVersion: '20230302',
|
appVersion: GAME_VERSION,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -879,7 +880,7 @@ describe('auth-gated endpoints', () => {
|
|||||||
deviceClass: 0,
|
deviceClass: 0,
|
||||||
vrMovementMode: 1,
|
vrMovementMode: 1,
|
||||||
platform: 0,
|
platform: 0,
|
||||||
appVersion: '20230302',
|
appVersion: GAME_VERSION,
|
||||||
expiresAt: nowSeconds() + 800,
|
expiresAt: nowSeconds() + 800,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,6 +24,14 @@
|
|||||||
/** Presence is kept this long (s) after the last matchmake/heartbeat refresh. */
|
/** Presence is kept this long (s) after the last matchmake/heartbeat refresh. */
|
||||||
export const PRESENCE_TTL_SECONDS = 900
|
export const PRESENCE_TTL_SECONDS = 900
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Game build version reported in presence (and echoed in the auth token's `rn.ver`
|
||||||
|
* claim). This is a server-side constant — the client doesn't supply it, and an
|
||||||
|
* empty value breaks the client's presence/version handling. Matches our target
|
||||||
|
* 2023 client build.
|
||||||
|
*/
|
||||||
|
export const GAME_VERSION = '20230414'
|
||||||
|
|
||||||
/** Schema DDL (mirror of migrations/0006_presence.sql). */
|
/** Schema DDL (mirror of migrations/0006_presence.sql). */
|
||||||
export const PRESENCE_SCHEMA_DDL: string[] = [
|
export const PRESENCE_SCHEMA_DDL: string[] = [
|
||||||
`CREATE TABLE IF NOT EXISTS presence (
|
`CREATE TABLE IF NOT EXISTS presence (
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"check:types": "run-tsc"
|
"check:types": "run-tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@repo/domain": "workspace:*",
|
||||||
"hono": "4.12.27"
|
"hono": "4.12.27"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
|
|
||||||
import { sign, verify } from 'hono/jwt'
|
import { sign, verify } from 'hono/jwt'
|
||||||
|
|
||||||
|
import { GAME_VERSION } from '@repo/domain'
|
||||||
|
|
||||||
/** Token lifetime in seconds (mirrored in the `expires_in` response field). */
|
/** Token lifetime in seconds (mirrored in the `expires_in` response field). */
|
||||||
export const TOKEN_TTL_SECONDS = 3600
|
export const TOKEN_TTL_SECONDS = 3600
|
||||||
|
|
||||||
@@ -127,8 +129,7 @@ export async function generateToken(
|
|||||||
idp: 'local',
|
idp: 'local',
|
||||||
platform,
|
platform,
|
||||||
platform_id: platformId,
|
platform_id: platformId,
|
||||||
'rn.ver': '20230302',
|
'rn.ver': GAME_VERSION,
|
||||||
// Same PlatformType int as the `platform` claim — it was pinned to Steam.
|
|
||||||
'rn.plat': platform,
|
'rn.plat': platform,
|
||||||
role: [...BASE_ROLES, ...extraRoles],
|
role: [...BASE_ROLES, ...extraRoles],
|
||||||
scope: TOKEN_SCOPES,
|
scope: TOKEN_SCOPES,
|
||||||
|
|||||||
Generated
+3
@@ -881,6 +881,9 @@ importers:
|
|||||||
|
|
||||||
packages/jwt:
|
packages/jwt:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@repo/domain':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../domain
|
||||||
hono:
|
hono:
|
||||||
specifier: 4.12.27
|
specifier: 4.12.27
|
||||||
version: 4.12.27
|
version: 4.12.27
|
||||||
|
|||||||
Reference in New Issue
Block a user