fix(api): say what the library check actually proves

A library entry records the person, not the account the games were
synced from, and `POST /library/sync` is not even told which account a
list came from. So the ownership check added for session requests asks
"has somebody this person linked got this game?" and not "does the
account about to play own it?" — for the one Steam account most people
have those are the same sentence, and for two they are not.

Confirmed rather than reasoned about: a person with two Steam links, a
game synced at person level, and a request naming the second account is
accepted today.

The check stays, because it still turns a box that boots, tries to
launch and fails minutes later into an immediate refusal, and it never
refuses on account grounds that the data cannot support. What changes is
the comment, which claimed the stronger property, and a test that pins
the gap so it is found deliberately rather than by surprise.

Closing it properly means recording the linked account on a library
entry: a column, a sync contract that says which account a list belongs
to, a uniqueness rule per account rather than per person, and a backfill
with no correct answer for rows already written. That is a decision about
what a library is, and inferring it here would be the kind of modelling
taken by accident that this branch refuses elsewhere.
This commit is contained in:
Wanjohi
2026-09-04 22:27:20 +03:00
parent 51dabddbd8
commit 7bdca1240f
2 changed files with 51 additions and 8 deletions

View File

@@ -9,6 +9,7 @@ import { Identifier } from '@nestri/core/id';
import { Machine } from '@nestri/core/machine/index';
import { Session } from '@nestri/core/session/index';
import { Library } from '@nestri/core/user/library';
import { LinkedAccount } from '@nestri/core/user/linked-account';
import { app } from '../app/index';
import './setup';
@@ -265,6 +266,39 @@ describe('POST /session', () => {
expect(await Session.listByBox(s.box.id)).toHaveLength(0);
});
test('the library check is per person, not per account it plays as', async () => {
const s = await scene('route-multilink', 5562);
// A second Steam account on the same person. The unique index is on
// (provider, providerAccountId) and is global rather than per user, so
// nothing stops this — but a fixed id here would collide with its own
// previous run, hence one shaped like a SteamID64 and unique per run.
const second = await LinkedAccount.create({
id: Identifier.ascending('linkedAccount'),
userId: s.owner.userId,
provider: 'steam',
providerAccountId: `7656119${Date.now()}`.slice(0, 17),
profile: null
});
const res = await app.request('/session', {
method: 'POST',
headers: s.user,
body: JSON.stringify({
boxId: s.box.id,
gameId: s.gameId,
linkedAccountId: second
})
});
// Accepted, and this pins a known gap rather than asserting it is
// right: a library entry records the person and not the account the
// games came from, so "the account playing owns this" cannot be asked.
// The run will fail at launch exactly as it did before the check
// existed. Closing it means recording the account on the library
// entry, which changes what a library is and what the sync must send.
expect(res.status).toBe(201);
});
test('an unknown game is a 404 and not a foreign key crash', async () => {
const s = await scene('route-nogame', 5507);
const res = await app.request('/session', {