From e9f1b835f834b9995b6620da1481b2902920d12e Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sun, 7 Jun 2026 20:44:12 +0200 Subject: [PATCH] fix(dashboard): F-S-014 warn on any permissiveness increase in topic edit modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit editFlipToExternal warned only on the internal → external transition. Switching `token → public` or `session → public` while already external is equally risky (opens topic to anonymous subscribers) but produced no warning. Replace with editPermissivenessChange that ranks the (external, auth_mode) pair on a 0..3 scale and fires the warning whenever the resulting rank strictly exceeds the current one: 0 internal (any auth) 1 external + session 2 external + token 3 external + public Keep `editFlipToExternal` as an alias pointing at the new derived so nothing downstream breaks. AUDIT.md anchor: F-S-014. Co-Authored-By: Claude Opus 4.7 (1M context) --- dashboard/src/routes/apps/[slug]/+page.svelte | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/dashboard/src/routes/apps/[slug]/+page.svelte b/dashboard/src/routes/apps/[slug]/+page.svelte index 6021538..46c1fd4 100644 --- a/dashboard/src/routes/apps/[slug]/+page.svelte +++ b/dashboard/src/routes/apps/[slug]/+page.svelte @@ -412,10 +412,24 @@ let editTopicAuthMode = $state('public'); let savingTopic = $state(false); let editTopicError = $state(null); - // Flipping internal → external is the security-sensitive change. - const editFlipToExternal = $derived( - !!topicToEdit && !topicToEdit.external_subscribable && editTopicExternal - ); + // F-S-014: warn whenever the resulting (external, auth_mode) pair is + // strictly more permissive than the current one — not just on the + // internal → external flip. The order least → most permissive is: + // internal (any auth) < external+session < external+token < external+public + const editPermissivenessChange = $derived.by(() => { + if (!topicToEdit) return false; + const rank = (external: boolean, mode: TopicAuthMode) => { + if (!external) return 0; + if (mode === 'session') return 1; + if (mode === 'token') return 2; + return 3; // public + }; + const before = rank(topicToEdit.external_subscribable, topicToEdit.auth_mode); + const after = rank(editTopicExternal, editTopicAuthMode); + return after > before; + }); + // Kept for narrow callers; semantics now covered by editPermissivenessChange. + const editFlipToExternal = $derived(editPermissivenessChange); // Delete confirm. let topicToRemove = $state(null); let removingTopic = $state(false);