Files
amethyst/quartz/plans/2026-06-12-giftwrap-deletion-requests.md
Claude 6579b5f656 docs: audit, status-stamp, and index all module plans
Audited all 143 plan files across the 10 plans/ folders. Each plan now
carries a Status header (shipped | in-progress | queued | abandoned)
backed by codebase evidence, and every folder has a README.md index
grouping plans by status.

Shipped plans were moved into a per-folder plans/archive/ (via git mv,
history preserved) so each plans/ folder surfaces only live work:

  shipped (archived): 122   in-progress: 8   queued: 7   abandoned: 4

docs/plans/ is the frozen legacy folder; its plans were stamped and
indexed in place (48 of 52 archived) but it remains closed to new plans.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hpUivtmq4pgzqRbY6MYrA
2026-06-30 15:35:38 +00:00

3.4 KiB

TODO: Deletion Requests (kind 5) for Gift Wraps

Status: queued — none of the four steps landed: HostStub still lacks a recipient field and DeletionIndex has no gift-wrap recipient-authored handling; header itself reads "Open — not implemented". Audited 2026-06-30.

Date: 2026-06-12 Status: Open — design agreed, not implemented Modules: quartz (DeletionIndex), amethyst (LocalCache)

The special case

Gift wraps (kind 1059) are signed by a discarded throwaway key, so the normal NIP-09 rule — a deletion only applies when its author equals the target's author — can never match a wrap. The intended rule: a kind-5 authored by the wrap's p tag (the recipient) may delete it. A recipient deleting their own received wrap is also the only deletion a client can express without leaking the private rumor id (the rumor id must never appear in a public kind-5).

Current behavior (verified 2026-06-12)

DeletionIndex is strictly author-keyed (DeletionRequest(targetId, deleterPubkey)), the live-delete path requires deleteNote.author == deletion.pubKey, and no downward cascade (wrap → seal → rumor) exists — deleteEnvelopes only walks upward via Note.rumorHost.

Deletion e-tags authored by live message deleted? blocks later insert?
wrap id recipient (p tag) no (key mismatch + no cascade + wrap note usually GC'd) no (tombstone keyed (wrapId, recipient), check uses (wrapId, throwawayKey))
seal id sender seal note only; message survives (no cascade) yes — accidental: seals are sender-signed, and GiftWrapEventHandler gates the unwrap on justConsume(seal)
rumor id sender yes (private un-react path; deleteEnvelopes cascades upward) yes

Only the rumor-id direction works; the wrap-id direction — the one the special case describes — does nothing.

Implementation sketch

  1. Insertion blocking (quartz, small): in DeletionIndex.hasBeenDeleted, when the event is a GiftWrapEvent, additionally check DeletionRequest(event.id, event.recipientPubKey()). A recipient-authored tombstone then blocks the wrap in justConsume before it is ever unwrapped, which blocks the message.

  2. Recipient on the stub (commons, tiny): add recipient: HexKey? to HostStub (one shared-string reference per rumor), populated from GiftWrapEvent.recipientPubKey() at unseal time, so the validation below works after the wrap note is GC'd.

  3. Live cascade (amethyst LocalCache.consume(DeletionEvent)): the wrap note that knew its innerEventId is GC'd by the time a deletion arrives, so find the rumor by reverse lookup: scan notes for note.rumorHost?.id == deletedId (precedent: the addressable pass in the same function already does a full notes.forEach; deletions are rare). Validate deletion.pubKey == note.rumorHost.recipient, then deleteNote(rumor) — envelope cleanup and chatroom removal already follow from the existing deleted-notes pipeline.

  4. Tests: block-before-unwrap (tombstone first, wrap second → message never materializes) and delete-after-unwrap (message in a chatroom, recipient-authored kind-5 for the wrap id → rumor and envelopes gone).

Note: only wraps addressed to the local user are ever in the cache, so the live-cascade case in practice means "another of my devices retracted a DM" — rare, not a hot path.