From defed434cc66acaa3a03563ad061e003474a5a17 Mon Sep 17 00:00:00 2001 From: ZDDC Date: Thu, 21 May 2026 08:50:49 -0500 Subject: [PATCH] feat(form): pre-flight Submit gate + cap-toast on 403 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes to the form tool's submit path: - Submit button hides when /.profile/access?path= reports no 'c' verb. The form-status line surfaces a short explanation so the user knows why the button disappeared. - 403 on POST routes through zddc.cap.handleForbidden, which renders an error toast naming the missing verb and offers Elevate when the path-scoped view reports an elevation grant covering it. The existing "You are not allowed to submit here" status line still appears as the in-form indicator. Also guards shared/cap.js's fetchAccess against file:// URLs — calling fetch() on a file:// page logs a browser-level error that shows up as test-runner noise. Short-circuiting to null lets offline tools (browse on a picked folder, form opened standalone from a file URL) silently degrade to "no path-scoped info" and fall back to whatever existing gate they had. Co-Authored-By: Claude Opus 4.7 (1M context) --- form/js/main.js | 23 +++++++++++++++++++++++ form/js/post.js | 6 ++++++ shared/cap.js | 9 +++++++++ 3 files changed, 38 insertions(+) diff --git a/form/js/main.js b/form/js/main.js index ca1fbaf..0923197 100644 --- a/form/js/main.js +++ b/form/js/main.js @@ -79,6 +79,29 @@ const submitBtn = document.getElementById('submit-btn'); if (submitBtn) { submitBtn.addEventListener('click', app.modules.post.submit); + // Pre-flight gate: hide Submit when the cascade denies + // create at the submission directory. Server still + // enforces on POST — this just avoids dangling an + // affordance that would 403. Submission directory is the + // parent of submitUrl; fall back to the page URL when + // submitUrl is absent (file:// / no-context mode). + if (window.zddc && window.zddc.cap && app.context && app.context.submitUrl) { + const subUrl = app.context.submitUrl; + const dir = subUrl.replace(/\/[^\/]*$/, '/') || subUrl; + window.zddc.cap.at(dir).then(function (view) { + if (!view) return; + const verbs = view.path_verbs || ''; + if (verbs.indexOf('c') === -1) { + submitBtn.hidden = true; + const status = document.getElementById('form-status'); + if (status) { + status.textContent = "You don't have permission to submit here."; + status.hidden = false; + status.classList.add('is-error'); + } + } + }); + } } } diff --git a/form/js/post.js b/form/js/post.js index c061089..5a39360 100644 --- a/form/js/post.js +++ b/form/js/post.js @@ -56,6 +56,12 @@ showStatus('Please correct the errors below.', 'error'); } else if (res.status === 403) { showStatus('You are not allowed to submit here.', 'error'); + if (window.zddc && window.zddc.cap) { + window.zddc.cap.handleForbidden(res, { + context: 'Submit', + path: app.context.submitUrl + }); + } } else if (res.status === 409) { showStatus('A submission with this filename already exists.', 'error'); } else { diff --git a/shared/cap.js b/shared/cap.js index 369b960..0d53d2f 100644 --- a/shared/cap.js +++ b/shared/cap.js @@ -34,6 +34,15 @@ var pathCache = new Map(); // path → AccessView (or null sentinel) async function fetchAccess(path) { + // file:// pages have no server to fetch /.profile/access from; + // calling fetch() there logs a browser-level error before our + // catch even runs. Short-circuit so offline tools (browse on + // a picked folder, form opened from a file URL) silently + // degrade to "no path-scoped info, fall back to existing + // gating signals". + if (location.protocol !== 'http:' && location.protocol !== 'https:') { + return null; + } try { var url = '/.profile/access'; if (path) url += '?path=' + encodeURIComponent(path);