diff --git a/README.md b/README.md index 5023fcb..57208e8 100644 --- a/README.md +++ b/README.md @@ -79,14 +79,17 @@ accounts has ever been able to offer. Every entry in a post is validated before any of them is written: a party that half-posts is worse than one that does not post at all. -### The missing-game shim +### Every request names its game -A request with no `game` is treated as `lemonade`. That is a compatibility -shim, not a default worth keeping — the deployed lemonade bundle posts no game -at all, because when it was built there was only one board, and copies of it -are sitting in people's browsers. It can go once that bundle has been rebuilt -and redeployed, and not before, or every score set from a cached page lands -nowhere. +There is no default. A request that does not say which board it wants is +refused with the list of boards there are. + +There was briefly a shim that read a missing `game` as `lemonade`, because the +deployed lemonade bundle predated this service and posted no game at all. It +was removed once that bundle had been rebuilt and was what the site served — +verified by reading the asset the public URL actually points at, not the one +the origin holds, because there is a CDN in between and the two are not the +same claim. ## The migration diff --git a/src/index.ts b/src/index.ts index 5a03a5a..2c0d578 100644 --- a/src/index.ts +++ b/src/index.ts @@ -113,10 +113,10 @@ function readBody(req: IncomingMessage): Promise { } /** - * Rows go back in the game's own field names, flattened, exactly as its - * client already expects them. That is not politeness -- the lemonade stand's - * bundle is sitting in people's browsers right now expecting `assets` and - * `days`, and it will keep expecting them until it is rebuilt. + * Rows go back in the game's own field names, flattened, exactly as its client + * expects them. That is the bargain that makes a shared service invisible: a + * game asks for its board and gets `assets` and `days`, or `bagged` and + * `arrows`, and never has to know it is sharing a table with anybody. */ const shape = (rows: Row[]) => rows.map((r) => ({ id: r.id, name: r.name, at: r.at, ...r.fields })) @@ -124,14 +124,18 @@ const shape = (rows: Row[]) => /** * Which game a request is about. * - * A missing game means lemonade, and that is a compatibility shim rather than - * a default worth keeping: the deployed lemonade client posts no game at all, - * because when it was built there was only one board. It can go once that - * bundle has been rebuilt and redeployed -- and not before, or every score set - * from a cached page lands nowhere. + * There was a shim here that read a missing game as lemonade, because the + * deployed lemonade bundle predated this service and posted no game at all. + * That bundle has been rebuilt and is what the site serves now -- both games + * name themselves on every call -- so the shim has been removed and a request + * that does not say which board it wants is refused rather than guessed at. + * + * Guessing was the right thing while there was something to guess for. It is + * the wrong thing now: silently filing an unlabelled score under whichever + * game happened to be first is the sort of default that is invisible until it + * is wrong, and the caller always knows which game it is. */ function gameFor(explicit: unknown): Game | undefined { - if (explicit === undefined || explicit === null || explicit === '') return lookup('lemonade') return lookup(explicit) } @@ -153,7 +157,7 @@ const server = createServer(async (req, res) => { if (path === '/api/scores' && req.method === 'GET') { const game = gameFor(url.searchParams.get('game') ?? undefined) - if (!game) return send(res, 404, { error: 'unknown game' }) + if (!game) return send(res, 404, { error: `unknown game; try one of: ${known().join(', ')}` }) return send(res, 200, { game: game.id, scores: shape(store.board(game, BOARD_LIMIT)) }) } @@ -170,7 +174,7 @@ const server = createServer(async (req, res) => { const body = (parsed ?? {}) as { game?: unknown; entries?: unknown } const game = gameFor(body.game) - if (!game) return send(res, 404, { error: 'unknown game' }) + if (!game) return send(res, 404, { error: `unknown game; try one of: ${known().join(', ')}` }) const list = Array.isArray(parsed) ? parsed : body.entries if (!Array.isArray(list)) return send(res, 400, { error: 'expected an array of entries' }) diff --git a/src/store.test.ts b/src/store.test.ts index 8f6f437..2e27e9a 100644 --- a/src/store.test.ts +++ b/src/store.test.ts @@ -15,6 +15,21 @@ describe('the registry', () => { expect(lookup('doom')).toBeUndefined() expect(lookup(42)).toBeUndefined() }) + + /** + * There is no default game, and there must not be one. + * + * A shim used to read a missing game as lemonade, for bundles that predated + * this service. Now that nothing posts unlabelled, guessing would be worse + * than refusing: it would file somebody's score under whichever game + * happened to be first, silently, and nobody would find out until the board + * looked wrong. These are the shapes a missing game arrives in. + */ + it('refuses to guess when nothing says which board', () => { + for (const missing of [undefined, null, '', 0, false, {}, []]) { + expect(lookup(missing)).toBeUndefined() + } + }) }) /**