Skip to main content

max / makenotwork

Run item-cover idempotency check before the storage-cap check item_image_confirm ran check_upload_allowed before the cover_s3_key idempotency short-circuit, inverted from project_image_confirm. A creator at their storage cap who legitimately retried a confirm for a cover they already own got a spurious "cap exceeded" error instead of the idempotent success the project path returns. Move the idempotency short-circuit ahead of the cap check to match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-06-25 05:06 UTC
Commit: 0994faa5aee72d8f1333a284479ee104ae659899
Parent: fed7af7
1 file changed, +13 insertions, -11 deletions
@@ -399,17 +399,10 @@ pub(super) async fn item_image_confirm(
399 399 )));
400 400 }
401 401
402 - // Enforce tier-based limits
403 - let max_storage = match db::creator_tiers::check_upload_allowed(&state.db, user.id, FileType::Cover, file_size_bytes).await {
404 - Ok(max) => max,
405 - Err(e) => {
406 - super::enqueue_s3_orphan(&state.db, &req.s3_key, "image_upload_rejected").await;
407 - return Err(e);
408 - }
409 - };
410 -
411 - // Idempotency: if cover_s3_key already matches, return success (no-op).
412 - // Otherwise capture the existing cover for atomic replacement below.
402 + // Idempotency: if cover_s3_key already matches, return success (no-op) BEFORE
403 + // the tier/cap check — a creator at their storage cap must still be able to
404 + // re-confirm a cover they already own (Run 9; matches project_image_confirm's
405 + // ordering). Otherwise capture the existing cover for atomic replacement below.
413 406 let existing_item = db::items::get_item_by_id(&state.db, req.item_id).await?;
414 407 if let Some(ref item) = existing_item
415 408 && item.cover_s3_key.as_deref() == Some(&req.s3_key)
@@ -425,6 +418,15 @@ pub(super) async fn item_image_confirm(
425 418 }));
426 419 }
427 420
421 + // Enforce tier-based limits
422 + let max_storage = match db::creator_tiers::check_upload_allowed(&state.db, user.id, FileType::Cover, file_size_bytes).await {
423 + Ok(max) => max,
424 + Err(e) => {
425 + super::enqueue_s3_orphan(&state.db, &req.s3_key, "image_upload_rejected").await;
426 + return Err(e);
427 + }
428 + };
429 +
428 430 // The key the CAS must still see on the row for this confirm to win — the
429 431 // raw current cover key, independent of its recorded size (a zero/NULL-size
430 432 // old cover is still a key the column holds, and the CAS must match it or a