feat(rust): the permission manager — the site owns the whole store (D160-D163, D188-D198)
PLAN_REDESIGNS section 1. - Every sync reads the store (perm.inventory), reconciles it against the site's record and its ledger, and pushes. A change made in the game is settled by the server's policy (D161): auto-adopt (default), adopt, or revoke. The first read of a server imports everything (D198). - Groups belong to one server unless an admin shares them (D189), in new id-keyed tables; the old ones are copied once at boot and left unread. Holders may be a Steam account nobody linked (D188). - An in-game change affects that server only (D190): a grant that reaches further gains an exception, a shared group is split. - Never judged: a permission the server does not register right now (an unloaded plugin is not a revocation), and a pair an event lease holds. - A new admin API (server view, grant/revoke with everywhere-or-here, groups by id, share/split, members, drift answers) and a screen on PermissionsManager's flow with a state on every toggle (D162, D163, U-1). - The announcement voice names a group by id; old name settings still read. Walked on both rigs against the walk core: import on an existing install, auto-adopt of a grant and a revoke, a fleet grant's exception, Kits unloaded without loss, a shared group split, adopt and revoke policies. Server 420/420, client 58/58, swagger, imports and route manifest current. Refs #21 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -254,6 +254,70 @@ const permCatalogue = (server) => request(server, '/permissions/catalogue')
|
||||
*/
|
||||
const permSync = (server, set) => request(server, '/permissions/sync', { method: 'POST', body: set })
|
||||
|
||||
/**
|
||||
* One page of the game's whole permission store (protocol 13, PLAN_REDESIGNS
|
||||
* §1.2): every permission with the plugin that registered it, every group, every
|
||||
* holder, and what event leases hold. `{ page: 0 }` starts a fresh read; later
|
||||
* pages name the `snapshotId` page 0 answered with.
|
||||
*
|
||||
* Like the sync, a refusal (`perm.error`: `busy`, `stale`, `too-large`) comes
|
||||
* back `{ ok: true }` and is told apart by `data.kind`.
|
||||
*/
|
||||
const permInventory = (server, { snapshotId = null, page = 0 } = {}) =>
|
||||
request(server, '/permissions/inventory', {
|
||||
method: 'POST',
|
||||
body: snapshotId ? { snapshotId, page } : { page },
|
||||
})
|
||||
|
||||
/**
|
||||
* The whole inventory, every page, or `{ ok: false, error }`. A snapshot that
|
||||
* goes stale mid-read is started again once from page 0: a page from a different
|
||||
* snapshot would tear the answer.
|
||||
*/
|
||||
async function readInventory(server) {
|
||||
for (let attempt = 0; attempt < 2; attempt++) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const first = await permInventory(server, { page: 0 })
|
||||
if (!first.ok) return { ok: false, error: first.status || 'unreachable' }
|
||||
|
||||
const head = first.data || {}
|
||||
if (head.kind === 'perm.error') return { ok: false, error: `the game refused the read: ${head.reason || 'unknown'}` }
|
||||
if (head.kind !== 'perm.inventory') return { ok: false, error: 'the game does not know perm.inventory (protocol 13)' }
|
||||
|
||||
let users = Array.isArray(head.users) ? head.users : []
|
||||
let stale = false
|
||||
|
||||
for (let page = 1; page < Number(head.pages || 1); page++) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const next = await permInventory(server, { snapshotId: head.snapshotId, page })
|
||||
const data = (next.ok && next.data) || {}
|
||||
|
||||
if (data.kind !== 'perm.inventory') {
|
||||
stale = true
|
||||
break
|
||||
}
|
||||
|
||||
users = users.concat(Array.isArray(data.users) ? data.users : [])
|
||||
}
|
||||
|
||||
if (stale) continue
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
inventory: {
|
||||
snapshotId: head.snapshotId,
|
||||
permissions: Array.isArray(head.permissions) ? head.permissions : [],
|
||||
groups: Array.isArray(head.groups) ? head.groups : [],
|
||||
leased: Array.isArray(head.leased) ? head.leased : [],
|
||||
users,
|
||||
stats: head.stats || null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: false, error: 'the inventory changed while it was being read, twice' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Every settings file on one game host, and every plugin loaded to reload one
|
||||
* (protocol 5, R18).
|
||||
@@ -441,6 +505,8 @@ module.exports = {
|
||||
confirmLink,
|
||||
permCatalogue,
|
||||
permSync,
|
||||
permInventory,
|
||||
readInventory,
|
||||
configFiles,
|
||||
configFile,
|
||||
configWrite,
|
||||
|
||||
Reference in New Issue
Block a user