fix(server): own game-account signup, and repair the gate slice 1 broke

`POST /player/shard/account` and its staff twin have answered 500 for every
caller since slice 1: the ported controller called
`settings.isGameAccountSignupEnabled()`, which is a member of core's settings
model and not of `ctx.settings` — three functions, deliberately. The call was
`undefined(...)`, the TypeError landed in the catch, and no test reached the
branch.

The gate now lives on the side that uses it (`utils/gameSignup.js`), which is
also where the policy belongs: the setting's own help text names Bridge.cfg and
says the shard's SignupMode must agree, and core cannot own a sentence about a
UO shard. The admin field moves to this module's Shard page and the derived
flag onto `/public/shard/features`, beside the visibility flags the same
callers already read.

The setting KEY is unchanged. Renaming `game_account_signup` would silently
reset every configured instance to `disabled` on upgrade, with players
reporting broken signup as the only clue — the same grandfathering as
`spawn_atlas_servuo_path` and the seven stream ids.

Both regression tests were shown to fail against the bug before it was fixed.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-11 18:00:37 -05:00
parent 28f4b9afe2
commit 493cf296ab
5 changed files with 305 additions and 2 deletions

View File

@@ -0,0 +1,63 @@
// ── Whether this site creates game accounts, and in which direction ────────
//
// This policy was core's until slice 3 of the Phase 3 extraction, and it should
// never have been: the setting's own help text names *Bridge.cfg* and says the
// game server's `SignupMode` must agree with it. That is a sentence about a UO
// shard, and core cannot own a sentence about a UO shard.
//
// **The setting key is unchanged.** `game_account_signup` keeps its name and its
// row in core's `settings` table, read and written through `ctx.settings`. The
// key is not prefixed because renaming it would silently reset every existing
// instance's configured mode to the default — the same reasoning that
// grandfathered `spawn_atlas_servuo_path`, `cliloc_client_path` and the seven
// stream ids (MODULE_API.md §6.5). A module owning an unprefixed settings key is
// a grandfathering, not a pattern to copy.
//
// **It was also broken.** Slice 1 ported the call site
// (`router/player/shard.controller.js`) still calling
// `settings.isGameAccountSignupEnabled()`, which `ctx.settings` does not expose —
// it is three functions, not the model. So `POST /player/shard/account` threw a
// TypeError and answered 500 for every caller, and no test saw it because the
// module's suite never reached that branch. This file is where that function now
// lives, on the side that actually uses it.
const { settings } = require('../core')
const KEY = 'game_account_signup'
/**
* The four modes, and what each means.
*
* `website` and `hybrid` are the two that accept a site-created account; `game`
* means accounts are made in the client and only linked here. The shard's own
* `SignupMode` still has the final say when the call is actually made — this is
* the site half of an agreement between two systems, which is exactly why it
* reads as UO policy rather than as site configuration.
*/
const MODES = ['disabled', 'website', 'hybrid', 'game']
const OFFERS_SIGNUP = ['website', 'hybrid']
/** The configured mode, or `disabled` for anything unset or unrecognised. */
async function getMode() {
const value = await settings.get(KEY)
return MODES.includes(value) ? value : 'disabled'
}
/**
* Does this site offer game-account creation right now?
*
* Fails CLOSED on an unreadable setting, because `getMode` resolves an unknown
* value to `disabled`. Offering a form that the shard will refuse is a dead end
* a player cannot distinguish from a bug.
*/
async function isEnabled() {
return OFFERS_SIGNUP.includes(await getMode())
}
/** @throws if `mode` is not one of MODES — the caller validates first. */
async function setMode(mode, updatedBy) {
if (!MODES.includes(mode)) throw new Error(`unknown game-signup mode "${mode}"`)
return settings.set(KEY, mode, updatedBy)
}
module.exports = { KEY, MODES, OFFERS_SIGNUP, getMode, isEnabled, setMode }