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
|
// Two gates compose: canMutate() rules out un-writable
|
||||||
// sources (offline FS-API without a handle, zip members,
|
// sources (offline FS-API without a handle, zip members,
|
||||||
// virtual placeholders) and zddc.cap.has(node, verb)
|
// virtual placeholders) and — when the listing carries
|
||||||
// applies the server-computed cascade verbs. If either
|
// server-cascade verbs — zddc.cap.has(node, verb) applies
|
||||||
// bars the action, the item disables with a tooltip
|
// the per-entry ACL. The verbs gate is server-mode only;
|
||||||
// explaining the reason — server-side ACL still has the
|
// file:// FS-API and plain Caddy listings have no verbs
|
||||||
// final say on the actual PUT/DELETE if a stale client
|
// field, so we fall back to canMutate alone (FS-API
|
||||||
// somehow tries.
|
// 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…',
|
label: 'Rename…',
|
||||||
disabled: function (c) {
|
disabled: function (c) {
|
||||||
if (!canMutate(c)) return true;
|
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');
|
return !window.zddc.cap.has(c.node, 'w');
|
||||||
},
|
},
|
||||||
tooltip: function (c) {
|
tooltip: function (c) {
|
||||||
if (!canMutate(c)) return '';
|
if (!serverMode || !canMutate(c)) return '';
|
||||||
if (!window.zddc.cap || window.zddc.cap.has(c.node, 'w')) 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.";
|
return "You don't have write access to this item.";
|
||||||
},
|
},
|
||||||
action: function (c) { renameNode(c.node); }
|
action: function (c) { renameNode(c.node); }
|
||||||
|
|
@ -924,12 +933,15 @@
|
||||||
danger: true,
|
danger: true,
|
||||||
disabled: function (c) {
|
disabled: function (c) {
|
||||||
if (!canMutate(c)) return true;
|
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');
|
return !window.zddc.cap.has(c.node, 'd');
|
||||||
},
|
},
|
||||||
tooltip: function (c) {
|
tooltip: function (c) {
|
||||||
if (!canMutate(c)) return '';
|
if (!serverMode || !canMutate(c)) return '';
|
||||||
if (!window.zddc.cap || window.zddc.cap.has(c.node, 'd')) 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.";
|
return "You don't have delete access to this item.";
|
||||||
},
|
},
|
||||||
action: function (c) { deleteNode(c.node); }
|
action: function (c) { deleteNode(c.node); }
|
||||||
|
|
|
||||||
|
|
@ -46,10 +46,20 @@
|
||||||
// Server-computed verb set: canonical "rwcda" subset the
|
// Server-computed verb set: canonical "rwcda" subset the
|
||||||
// calling principal holds at this entry's URL. Per-entry
|
// calling principal holds at this entry's URL. Per-entry
|
||||||
// gating in the context menu (Rename/Delete) reads this
|
// gating in the context menu (Rename/Delete) reads this
|
||||||
// through zddc.cap.has(node, 'w'|'d'). Empty string is the
|
// through zddc.cap.has(node, 'w'|'d').
|
||||||
// explicit-deny case; absence (offline FS-API mode) makes
|
//
|
||||||
// zddc.cap.has fall back to the writable bit for 'w'.
|
// "rw…" — zddc-server emitted explicit grant.
|
||||||
verbs: typeof e.verbs === 'string' ? e.verbs : '',
|
// "" — 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):
|
// FS-API specific (null in server mode):
|
||||||
handle: null
|
handle: null
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -51,12 +51,14 @@
|
||||||
// cause behind "I'm admin but the editor says read-only".
|
// cause behind "I'm admin but the editor says read-only".
|
||||||
writable: !!raw.writable,
|
writable: !!raw.writable,
|
||||||
// Server-computed verb set (canonical "rwcda" subset).
|
// Server-computed verb set (canonical "rwcda" subset).
|
||||||
// Per-entry permission gating in the context menu reads
|
// Per-entry permission gating reads this via
|
||||||
// this via zddc.cap.has(node, verb). Empty string = no
|
// zddc.cap.has(node, verb). Three states:
|
||||||
// verbs known / explicit deny; offline FS-API listings
|
// "rw…" — zddc-server explicit grant
|
||||||
// leave it empty and gating falls back to the writable
|
// "" — zddc-server explicit zero grant
|
||||||
// bit through cap.has's transition shim.
|
// undefined — Caddy / FS-API listings (no verbs field).
|
||||||
verbs: typeof raw.verbs === 'string' ? raw.verbs : ''
|
// 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);
|
state.nodes.set(id, node);
|
||||||
return node;
|
return node;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue