fix(browse): preserve undefined verbs to distinguish Caddy/FS-API from zddc
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) <noreply@anthropic.com>
This commit is contained in:
parent
c87dccdb23
commit
360049f482
3 changed files with 46 additions and 22 deletions
|
|
@ -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); }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue