From c87dccdb23c6ece5b635a4b3eade651f50df99b7 Mon Sep 17 00:00:00 2001 From: ZDDC Date: Thu, 21 May 2026 08:51:50 -0500 Subject: [PATCH] docs: client-side capability gating model Brief subsection under "Permission model" explaining the three server surfaces that feed front-end gating (verbs in listings, /.profile/access?path=, missing_verb in 403 bodies) and the shared client helpers in shared/cap.js. Records the hide/disable philosophy and notes that transmittal + classifier are FS-API-only so server-side gating doesn't apply to their UI controls. Co-Authored-By: Claude Opus 4.7 (1M context) --- ARCHITECTURE.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 8920dd1..9a9a204 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -721,6 +721,23 @@ The schema keys that drive built-in behavior: The role invariants (verb sets at each canonical path, subtree-admin scope) are locked down in `zddc/internal/zddc/standardroles_test.go`. New roles, when added, should ship with a parallel test in that file. +#### Client-side capability gating + +Three server surfaces feed the front-end's hide/disable model: + +- **Per-entry `verbs` on every directory listing item** (`zddc/internal/listing/types.go`). Canonical `"rwcda"` subset granted to the calling principal at that entry's URL. For files it reflects the parent dir's chain (matches Writable's gate); for directories it reflects the subdir's OWN chain. `Writable` stays in lockstep during the transition window; new clients should read `verbs` and let `writable` wither. +- **`GET /.profile/access?path=`** returns the global view (Email, IsSuperAdmin, CanElevate, …) plus three path-scoped fields: `path_verbs` (verbs at the requested path under the caller's CURRENT elevation), `path_is_admin` (subtree-admin authority at that path under current elevation), and `path_can_elevate_grant` (verbs the caller WOULD hold at that path if they elevated, empty when elevation wouldn't change anything). Each tool fetches its current directory once on load to gate top-of-page affordances. +- **403 ACL-deny responses carry a JSON body** `{"error": "Forbidden", "missing_verb": ""}` (`zddc/internal/handler/errors.go writeForbidden`). Other 403 conditions (no authenticated principal, existence-leak guards) keep plain-text bodies — `missing_verb` only applies to ACL denies. + +Client side, `shared/cap.js` consumes all three: `zddc.cap.has(node, verb)` reads the listing's verbs string (falling back to `node.writable` for `w` on offline FS-API listings); `zddc.cap.at(path)` memo-fetches the path-scoped profile view; `zddc.cap.handleForbidden(resp, opts)` renders an error toast naming the missing verb and offers an Elevate button when `path_can_elevate_grant` covers it. + +Each tool gates per the hide/disable rules: +- **Hide** admin-only actions (`a`), WORM-zone destructive items, and flow-terminal steps (Publish, advance state) when the verb is unattainable. +- **Disable + tooltip** everyday write affordances (Rename/Delete in the context menu, Save in editors, `+ Add row`, `+ New folder`, `Submit`) so the user discovers what permission is missing and can elevate if applicable. +- **Optimistic** for bulk / cross-directory operations — let the server return 403 and surface it via `cap.handleForbidden`. + +Browse implements the per-entry gating (rename/delete + editor save); tables and form pre-flight their primary writes via `cap.at` + route 403s through `cap.handleForbidden`. Transmittal and classifier write through the FS Access API rather than the server, so server-side gating doesn't apply to their UI controls. + ### File API (authenticated CRUD) zddc-server exposes write methods on the same URL space as GET. Each method maps to a specific verb and is gated against the cascade-derived verb set: