Add self-service transitions, authorized by item possession not group membership
Deploy / deploy (push) Successful in 33s

New Alternative.self_transition: like ResourceSpec.transitions, but for
an anonymous visitor holding one specific item's own chain hash (from
a ?chain= link, the same reference /subscribed?chain=... already
carries) rather than a signed-in owner browsing a whole bucket. email
is a second factor checked against the stored item, not the lookup
key - defense in depth against a leaked/guessed chain hash alone.

AlternativeCard renders it as a single confirm button when both
?chain= and ?email= are present; QuestionView hides the alternative
entirely otherwise, rather than showing a dead card with nothing to
click.

Powers /subscribed's new "Unsubscribe" alternative, which the
newsletter's own recipient links now carry - no email lookup needed,
just the same chain_hash the subscription itself already produced.
This commit is contained in:
Bendik Aagaard Lynghaug
2026-08-05 18:44:28 +02:00
parent 6df895b1d3
commit 213b1130bb
3 changed files with 181 additions and 2 deletions
+20
View File
@@ -58,6 +58,26 @@ pub struct Alternative {
/// silently break if the wording changes later.
#[serde(default)]
pub record_as: Option<String>,
/// A transition anyone holding the *right item's own reference* can
/// fire - not gated by `requires_group` like `ResourceSpec.transitions`,
/// which is for signed-in owners browsing a whole bucket. This is
/// the opposite shape: an anonymous visitor who already holds one
/// specific chain hash (from a `?chain=` link - see `chain.rs`,
/// the same reference `/subscribed?chain=...` already carries) may
/// transition *that one item*, and only that one, without ever
/// being able to enumerate or touch anyone else's. `email` is a
/// second factor checked against the stored item's own `email`
/// response field - not the lookup key, just cheap defense in depth
/// against a leaked/guessed chain hash alone being sufficient.
#[serde(default)]
pub self_transition: Option<SelfTransition>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SelfTransition {
pub bucket: String,
pub to: String,
pub label: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]