fix(dashboard): F-S-014 warn on any permissiveness increase in topic edit modal

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) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-07 20:44:12 +02:00
parent fbbe7676ae
commit e9f1b835f8

View File

@@ -412,10 +412,24 @@
let editTopicAuthMode = $state<TopicAuthMode>('public');
let savingTopic = $state(false);
let editTopicError = $state<string | null>(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<Topic | null>(null);
let removingTopic = $state(false);