feat(sidecar): POST /permissions/inventory forwards the plugin's store read #20

Merged
whitlocktech merged 1 commits from feat/perm-inventory into edge 2026-09-28 16:38:45 +00:00
2 changed files with 29 additions and 0 deletions

View File

@@ -171,6 +171,10 @@ use tracing_subscriber::EnvFilter;
/// whatever its kind — so what changes here is what no longer has to fit: the reload window that
/// had to sit inside [`rpc::REPLY_TIMEOUT`] is gone.
///
/// The permission manager's redesign (the module's PLAN_REDESIGNS.md §1, D160) adds one route,
/// `POST /permissions/inventory`: the plugin's whole store, in pages it sizes to the game link's
/// line cap. Forwarded like every other object; the pages are never kept here.
///
/// `docs/rust-link/PROTOCOL.md` is the specification — §8 the read path, §9 identity, §10 the
/// mirror, §11 configuration, §12 clans, §13 the raid frame, §14 the leases, §15 the world verbs,
/// §16 the rewards, §17 the map, §18 the optional mods, §19 the walk's fixes; this constant is one

View File

@@ -95,6 +95,10 @@ pub async fn serve(addr: &str, state: AppState) -> anyhow::Result<()> {
// plugin will do with any of it. It puts `cmd` and `reqId` on the object and forwards it,
// exactly as it forwards a link code.
.route("/permissions/sync", post(perm_sync))
// Protocol 13 (D160): the whole store, owners included, in pages. The body is
// `{snapshotId?, page?}` and is forwarded as it is; paging is the plugin's and the
// website's business, never this process's, which keeps no copy of any page.
.route("/permissions/inventory", post(perm_inventory))
// Protocol 5 (R18): the plugin's own view of the game host's configuration tree. All
// three are correlated round trips and all three fail when the game is down, because
// "what is on that host's disk" has no stale answer worth giving — and, unlike a board,
@@ -435,6 +439,13 @@ async fn perm_sync(State(st): State<AppState>, Json(body): Json<Value>) -> Respo
respond(st.rpc.call(&st.game, command, &req_id).await)
}
/// One page of the plugin's permission inventory (protocol 13). Page 0 without a `snapshotId`
/// starts a fresh read; any other page names the snapshot page 0 answered with. A `perm.error`
/// (`busy`, `stale`, `too-large`) is an answer, and arrives as a `200` like the sync's.
async fn perm_inventory(State(st): State<AppState>, Json(body): Json<Value>) -> Response {
forward_object(&st, body, "perm.inventory", "an inventory request").await
}
#[derive(Debug, Deserialize)]
struct ConfigPathQuery {
path: String,
@@ -1081,6 +1092,20 @@ mod tests {
assert!(stamp(json!(["9"]), "tally.close", "r-4").is_none());
}
/// Protocol 13: an inventory page request keeps its paging fields and cannot become a sync.
#[test]
fn an_inventory_request_cannot_choose_its_own_command() {
let body =
json!({ "cmd": "perm.sync", "reqId": "theirs", "snapshotId": "abc123", "page": 2 });
let stamped = stamp(body, "perm.inventory", "r-7").expect("an object is stamped");
assert_eq!(stamped["cmd"], "perm.inventory");
assert_eq!(stamped["reqId"], "r-7");
assert_eq!(stamped["snapshotId"], "abc123");
assert_eq!(stamped["page"], 2);
assert!(stamp(json!([0]), "perm.inventory", "r-8").is_none());
}
/// Protocol 12: a title set is forwarded whole. The markup inside each `text` is the module's
/// and the plugin's business, so it must arrive byte for byte as the website composed it.
#[test]