From 360049f48248ff964fc3cf780cd004b2f8f0d4cf Mon Sep 17 00:00:00 2001 From: ZDDC Date: Thu, 21 May 2026 09:00:12 -0500 Subject: [PATCH] fix(browse): preserve undefined verbs to distinguish Caddy/FS-API from zddc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three modes again behave consistently after Part 3's per-entry gating: 1. file:// (FS Access API picker) — fromHandle leaves verbs unset (now undefined, not ""). The events.js Rename/Delete gates skip the cap.has cascade check when typeof node.verbs is not 'string', so the items stay enabled per the original canMutate contract. 2. Caddy file-server — fromServerEntry sees no verbs in the listing and preserves undefined. Same skip applies; Rename / Delete stay enabled but the underlying server will 405 the POST/DELETE (same pre-Part-3 behavior). Markdown/yaml editors still mount read-only via cap.has's writable fallback. 3. zddc-server — verbs is always emitted (possibly as "" for an explicit zero grant). cap.has interprets the string and the gates apply. The previous "verbs ?? ''" normalisation collapsed (1)+(2) into the explicit-zero case, which incorrectly disabled Rename/Delete in offline mode. Tri-state verbs (string non-empty / string empty / undefined) restores the intent. Co-Authored-By: Claude Opus 4.7 (1M context) --- browse/js/events.js | 36 ++++++++++++++++++++++++------------ browse/js/loader.js | 18 ++++++++++++++---- browse/js/tree.js | 14 ++++++++------ 3 files changed, 46 insertions(+), 22 deletions(-) diff --git a/browse/js/events.js b/browse/js/events.js index 495f704..bf85130 100644 --- a/browse/js/events.js +++ b/browse/js/events.js @@ -898,22 +898,31 @@ // // Two gates compose: canMutate() rules out un-writable // sources (offline FS-API without a handle, zip members, - // virtual placeholders) and zddc.cap.has(node, verb) - // applies the server-computed cascade verbs. If either - // bars the action, the item disables with a tooltip - // explaining the reason — server-side ACL still has the - // final say on the actual PUT/DELETE if a stale client - // somehow tries. + // virtual placeholders) and — when the listing carries + // server-cascade verbs — zddc.cap.has(node, verb) applies + // the per-entry ACL. The verbs gate is server-mode only; + // file:// FS-API and plain Caddy listings have no verbs + // field, so we fall back to canMutate alone (FS-API + // enforces locally; Caddy has no PUT/DELETE either way). + // Server-side ACL still has the final say on the actual + // PUT/DELETE if a stale client tries the action. { label: 'Rename…', disabled: function (c) { if (!canMutate(c)) return true; - if (!window.zddc.cap) return false; + if (!serverMode || !window.zddc.cap) return false; + // verbs===undefined → Caddy or other non-zddc + // server, no cascade signal to gate on. verbs==="" + // is zddc-server's explicit zero grant; still + // gate (disable). verbs==="rw…" → check the bit. + if (typeof c.node.verbs !== 'string') return false; return !window.zddc.cap.has(c.node, 'w'); }, tooltip: function (c) { - if (!canMutate(c)) return ''; - if (!window.zddc.cap || window.zddc.cap.has(c.node, 'w')) return ''; + if (!serverMode || !canMutate(c)) return ''; + if (!window.zddc.cap) return ''; + if (typeof c.node.verbs !== 'string') return ''; + if (window.zddc.cap.has(c.node, 'w')) return ''; return "You don't have write access to this item."; }, action: function (c) { renameNode(c.node); } @@ -924,12 +933,15 @@ danger: true, disabled: function (c) { if (!canMutate(c)) return true; - if (!window.zddc.cap) return false; + if (!serverMode || !window.zddc.cap) return false; + if (typeof c.node.verbs !== 'string') return false; return !window.zddc.cap.has(c.node, 'd'); }, tooltip: function (c) { - if (!canMutate(c)) return ''; - if (!window.zddc.cap || window.zddc.cap.has(c.node, 'd')) return ''; + if (!serverMode || !canMutate(c)) return ''; + if (!window.zddc.cap) return ''; + if (typeof c.node.verbs !== 'string') return ''; + if (window.zddc.cap.has(c.node, 'd')) return ''; return "You don't have delete access to this item."; }, action: function (c) { deleteNode(c.node); } diff --git a/browse/js/loader.js b/browse/js/loader.js index 04b6e00..18d7045 100644 --- a/browse/js/loader.js +++ b/browse/js/loader.js @@ -46,10 +46,20 @@ // Server-computed verb set: canonical "rwcda" subset the // calling principal holds at this entry's URL. Per-entry // gating in the context menu (Rename/Delete) reads this - // through zddc.cap.has(node, 'w'|'d'). Empty string is the - // explicit-deny case; absence (offline FS-API mode) makes - // zddc.cap.has fall back to the writable bit for 'w'. - verbs: typeof e.verbs === 'string' ? e.verbs : '', + // through zddc.cap.has(node, 'w'|'d'). + // + // "rw…" — zddc-server emitted explicit grant. + // "" — zddc-server emitted explicit zero grant + // (rare; usually the entry would have been + // filtered before reaching the client). + // undefined — the server didn't emit a verbs field at + // all (Caddy or any non-zddc backend). + // cap.has and the events.js gates treat + // this as "verbs unknown" and skip the + // per-entry cascade gate; canMutate + + // whatever the server enforces on the + // actual PUT/DELETE still apply. + verbs: typeof e.verbs === 'string' ? e.verbs : undefined, // FS-API specific (null in server mode): handle: null }; diff --git a/browse/js/tree.js b/browse/js/tree.js index bdeeaf1..0e30fbc 100644 --- a/browse/js/tree.js +++ b/browse/js/tree.js @@ -51,12 +51,14 @@ // cause behind "I'm admin but the editor says read-only". writable: !!raw.writable, // Server-computed verb set (canonical "rwcda" subset). - // Per-entry permission gating in the context menu reads - // this via zddc.cap.has(node, verb). Empty string = no - // verbs known / explicit deny; offline FS-API listings - // leave it empty and gating falls back to the writable - // bit through cap.has's transition shim. - verbs: typeof raw.verbs === 'string' ? raw.verbs : '' + // Per-entry permission gating reads this via + // zddc.cap.has(node, verb). Three states: + // "rw…" — zddc-server explicit grant + // "" — zddc-server explicit zero grant + // undefined — Caddy / FS-API listings (no verbs field). + // Per-entry gates skip the cascade check + // and fall back to canMutate / writable. + verbs: typeof raw.verbs === 'string' ? raw.verbs : undefined }; state.nodes.set(id, node); return node;