feat(rust): site-owned permissions — the site is the author, the game is the cache
R2, and the first phase where this module WRITES to a game. Groups and grants are authored on the website and pushed into each server's own permission store, so every plugin that already calls `UserHasPermission` honours them with no adapter, and a wipe stops being a data-loss event. **Seven org-lead decisions (D28-D34).** A grant is keyed to the website USER and resolved to every Steam id they have linked at push time (D28); every authored row carries a scope — a server or `*` (D29); groups are mirrored as real groups rather than flattened (D30); a holder the site did not author is REPORTED, never undone, with adopt and revoke offered (D31); one verb, with the plugin diffing locally (D32); a permission no server has registered is reported unresolved and never self-registered (D33); authoring is people and groups by hand, with rules deferred (D34). **Three sets, and every interesting question is a difference between two.** `desired − pushed` is what to apply; `pushed − desired` is what to RETIRE, because the site put it there and has since withdrawn it; `present − desired` is drift. The middle one is why `rust_perm_pushed` exists: a name in the store that is not in the desired set is either something the site retired or something a human granted, and those two have opposite correct answers. **What lands is not what was sent.** A grant naming a permission the server has not registered did not land — `GrantUserPermission` no-ops silently — and a member the store has never seen could not be placed. Neither is recorded as pushed, so the site never believes it gave a privilege it did not. The loop asks a cheap question every thirty seconds — does the digest of the desired set still equal what this server last confirmed — and syncs on a change, a restart, a wipe, a drift hook, a failed attempt past its backoff, or the fifteen-minute audit that finds drift on a server nobody has touched. **This module's first admin page**, because a permission model is the first thing here that has to be composed rather than configured. What is on it is decided by what an operator can get wrong: four states are invisible from the game and from a list of grants, and each is a sentence rather than a number. Walked end to end against a real core at the pinned ref, the real sidecar, and a stand-in speaking protocol 4 — including a restart that emptied the store and was fully re-pushed. Four defects the browser found that 133 green tests did not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMH6bw1jXMgbyF3ZWGEzSM
This commit is contained in:
@@ -52,9 +52,11 @@ const TIMEOUT_MS = 12000
|
||||
* here, `PROTOCOL_VERSION` in the sidecar, `ProtocolVersion` in the bridge
|
||||
* plugin, and `protocol` in its `overlay.toml`.
|
||||
*
|
||||
* **3 — identity.** Protocol 2 was the read path; 3 adds the first message the
|
||||
* WEBSITE originates (`link.confirm`) and the two account frames the plugin
|
||||
* emits beside it. The bump lands here in the same change as the emitters,
|
||||
* **4 — the permission mirror.** Protocol 2 was the read path, 3 the first
|
||||
* message the WEBSITE originates (`link.confirm`); 4 is the first that WRITES
|
||||
* to the game — the whole permission set the site authors for one server, and
|
||||
* the report the plugin sends back. The bump lands here in the same change as
|
||||
* the emitters,
|
||||
* because the sidecar refuses a client declaring a different version with a
|
||||
* `409`: a module left on 2 would stop being able to read the server board it
|
||||
* has been reading all along. A constant that lags the deployment is not a safe
|
||||
@@ -64,7 +66,7 @@ const TIMEOUT_MS = 12000
|
||||
* deployment into a `409` naming both numbers instead of a parse failure three
|
||||
* layers further in.
|
||||
*/
|
||||
const PROTOCOL_VERSION = 3
|
||||
const PROTOCOL_VERSION = 4
|
||||
|
||||
/** What a caller gets back. Shaped once so every call site reads the same. */
|
||||
function reply(ok, status, data = null) {
|
||||
@@ -210,6 +212,33 @@ const feedTail = (server) => request(server, '/feed')
|
||||
const confirmLink = (server, code) =>
|
||||
request(server, '/link/confirm', { method: 'POST', body: { code } })
|
||||
|
||||
/**
|
||||
* What one server's loaded plugins have registered, and the groups its store
|
||||
* holds (protocol 4).
|
||||
*
|
||||
* The option source behind the authoring form (D33). It is a live read through
|
||||
* to the game rather than anything cached at the sidecar, because the answer
|
||||
* changes when an operator loads a plugin — and the whole reason to ask is to
|
||||
* offer names that will actually resolve. It therefore fails when the game is
|
||||
* down, like `/status` and unlike every store-backed read.
|
||||
*/
|
||||
const permCatalogue = (server) => request(server, '/permissions/catalogue')
|
||||
|
||||
/**
|
||||
* Push the whole permission set this site authors for one server (protocol 4).
|
||||
*
|
||||
* **The second call in this file that is not a GET, and the first that changes
|
||||
* the game.** The body is the desired set plus what the site has withdrawn; the
|
||||
* plugin diffs it against the live store, applies the difference and answers
|
||||
* with a report — counts, the names it could not resolve, the memberships that
|
||||
* are waiting on a first connection, and every holder the site did not author.
|
||||
*
|
||||
* **A refusal comes back `{ ok: true }`**, like a refused link code: `perm.error`
|
||||
* and `perm.report` are both answers, and the sidecar keeps its own status codes
|
||||
* for the transport. The caller discriminates on `data.kind`.
|
||||
*/
|
||||
const permSync = (server, set) => request(server, '/permissions/sync', { method: 'POST', body: set })
|
||||
|
||||
module.exports = {
|
||||
TIMEOUT_MS,
|
||||
PROTOCOL_VERSION,
|
||||
@@ -221,5 +250,7 @@ module.exports = {
|
||||
feed,
|
||||
feedTail,
|
||||
confirmLink,
|
||||
permCatalogue,
|
||||
permSync,
|
||||
joinUrl,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user