Asymmetric + unsafe cancel: loadCancelableExecution (internal/purchase/approvals.go:191-193, token/email path) only rejects completed/cancelled, so it ALLOWS cancelling running/approved/paused/failed/expired. The session path cancelPurchaseViaSession (handler_purchases.go:512-514) rejects everything except pending/notified. So an email-link holder can cancel an in-flight/approved execution that the dashboard user cannot, and there's no guard that the AWS purchase isn't already mid-flight (cancelling a row whose commitment is being created leaves DB and cloud out of sync).
Fix: align the token path with the session path (only pending/notified cancelable), and add an explicit guard against cancelling a row that is mid-execution. Tests for both paths.
Dedup: not #621/#632. No existing issue. New. Surfaced in the purchase-workflow trace.
Asymmetric + unsafe cancel:
loadCancelableExecution(internal/purchase/approvals.go:191-193, token/email path) only rejectscompleted/cancelled, so it ALLOWS cancellingrunning/approved/paused/failed/expired. The session pathcancelPurchaseViaSession(handler_purchases.go:512-514) rejects everything exceptpending/notified. So an email-link holder can cancel an in-flight/approved execution that the dashboard user cannot, and there's no guard that the AWS purchase isn't already mid-flight (cancelling a row whose commitment is being created leaves DB and cloud out of sync).Fix: align the token path with the session path (only pending/notified cancelable), and add an explicit guard against cancelling a row that is mid-execution. Tests for both paths.
Dedup: not #621/#632. No existing issue. New. Surfaced in the purchase-workflow trace.