From 7b59d82cdb3e2fc5c01b4f5850332c89db39b1cb Mon Sep 17 00:00:00 2001 From: ZDDC Date: Fri, 5 Jun 2026 14:30:48 -0500 Subject: [PATCH] feat(browse): edit .zddc.zip bundle members in-place (elevated admin) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The .zddc/markdown editors marked every zip member read-only. Add util.isEditableZipMember (member of a .zddc.zip + session elevated) and let those through canSave in both editors — so an elevated admin can open a bundle's policy .zddc (or any member) and save it, which PUTs to the member URL where the new server-side ServeZipWrite handles the in-place rewrite + in-zip history. The server (bundle gate + active-admin) is the real authority; this just drives the editor UX (mount editable, label "config bundle" instead of "read-only (zip)"). Content-archive members stay read-only. Co-Authored-By: Claude Opus 4.8 (1M context) --- browse/js/preview-markdown.js | 7 +++++-- browse/js/preview-yaml.js | 8 ++++++-- browse/js/util.js | 12 ++++++++++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/browse/js/preview-markdown.js b/browse/js/preview-markdown.js index f4abafa..a9b20cf 100644 --- a/browse/js/preview-markdown.js +++ b/browse/js/preview-markdown.js @@ -315,9 +315,12 @@ } var isZipMemberNode = util.isZipMemberNode; + var isEditableZipMember = util.isEditableZipMember; function canSave(node) { - if (isZipMemberNode(node)) return false; + // A .zddc.zip bundle member is saveable iff editable (elevated admin) — + // the server's ServeZipWrite is the gate; other zip members read-only. + if (isZipMemberNode(node)) return isEditableZipMember(node); // Server-computed authority gate. The listing's verbs string // tells us whether a PUT to this entry would be allowed — // false here means the file API would 403, so we mount in @@ -486,7 +489,7 @@ var sourceEl = document.createElement('span'); sourceEl.className = 'md-shell__source'; if (isZipMemberNode(node)) { - sourceEl.textContent = 'read-only (zip)'; + sourceEl.textContent = isEditableZipMember(node) ? 'config bundle' : 'read-only (zip)'; } else if (node.handle) { sourceEl.textContent = 'local'; } else if (node.url) { diff --git a/browse/js/preview-yaml.js b/browse/js/preview-yaml.js index 22f3903..47aad2e 100644 --- a/browse/js/preview-yaml.js +++ b/browse/js/preview-yaml.js @@ -53,9 +53,13 @@ } var isZipMemberNode = util.isZipMemberNode; + var isEditableZipMember = util.isEditableZipMember; function canSave(node) { - if (isZipMemberNode(node)) return false; + // A .zddc.zip bundle member is saveable iff editable (elevated admin); + // the server's ServeZipWrite is the real gate. Other zip members are + // read-only. + if (isZipMemberNode(node)) return isEditableZipMember(node); // Virtual .zddc placeholders are designed to be saved — a PUT // materializes the file from the synthetic body and the next // listing serves a real entry. Every other virtual node (per- @@ -444,7 +448,7 @@ var sourceEl = document.createElement('span'); sourceEl.className = 'md-shell__source'; - if (isZipMemberNode(node)) sourceEl.textContent = 'read-only (zip)'; + if (isZipMemberNode(node)) sourceEl.textContent = isEditableZipMember(node) ? 'config bundle' : 'read-only (zip)'; else if (node.handle) sourceEl.textContent = 'local'; else if (node.url) sourceEl.textContent = 'server'; diff --git a/browse/js/util.js b/browse/js/util.js index 4b2dbdd..526004b 100644 --- a/browse/js/util.js +++ b/browse/js/util.js @@ -90,6 +90,17 @@ return false; } + // isEditableZipMember reports whether node is a member of the .zddc.zip + // config bundle AND the session is elevated — the one case where the server + // accepts a write into a zip (ServeZipWrite, admin-gated). Every other zip + // member (content archives, or the bundle when not elevated) stays + // read-only. The server is the real gate; this just drives editor UX. + function isEditableZipMember(node) { + if (!node || !node.url || window.app.state.source !== 'server') return false; + if (!/\.zddc\.zip\//i.test(node.url)) return false; + return !!(window.zddc && window.zddc.elevation && window.zddc.elevation.isElevated()); + } + // Thrown by saveFile when the server rejects a write with 412 // Precondition Failed — the file changed under us since we loaded it. // Callers branch on `.status === 412` to open the conflict UI instead @@ -193,6 +204,7 @@ fetchAccessEmails: fetchAccessEmails, fmtSize: fmtSize, isZipMemberNode: isZipMemberNode, + isEditableZipMember: isEditableZipMember, saveFile: saveFile, saveCopy: saveCopy, ConflictError: ConflictError