Files
amethyst/quartz/plans/2026-06-12-giftwrap-deletion-requests.md
Claude f72388a559 refactor: rename packages, helpers and app code after the NIP event renames
Follow-up to the event class renames: the names around those classes now
match too.

Quartz (published API, every old name kept as a @Deprecated alias/forwarder):
- nip51Lists packages moved: followList -> starterPack, hashtagList -> interestList,
  peopleList -> followSet, labeledBookmarkList -> bookmarkSet. The old packages
  keep a Deprecated.kt with typealiases and forwarding extension functions
  (ReplaceWith points at the new package).
- LnZapPrivateEvent -> PrivateZapEvent, LnZapReceiptValidator -> ZapReceiptValidator,
  ChannelListDiff -> PublicChatListDiff, HashtagListDiff -> InterestListDiff.
- Kind 30063 now indexes NIP-82 release notes for search (never NIP-51 content,
  which can hold encrypted private items). searchable-events docs updated.

App code (amethyst, commons, desktopApp, cli; no aliases needed):
- model packages nip51Lists.{hashtagLists,peopleList,labeledBookmarkLists,relayFeeds}
  -> {interestLists,followSets,bookmarkSets,favoriteRelays}.
- State/cache/UI names derived from the old event names: HashtagListState ->
  InterestListState, PeopleListsState -> FollowSetsState, FollowListsState ->
  StarterPacksState, LabeledBookmarkList -> BookmarkSet, RelayFeedListState ->
  FavoriteRelayListState, ContactCardsState -> UserAssertionsState, NIP90* view
  models/renderers -> Dvm*, LnZap* handlers -> Zap*, SealedRumor* -> Seal*, and
  Account.peopleLists/followLists/hashtagList -> followSets/starterPacks/interestList.
- String literals were left untouched, so preference keys and @SerialName values
  stored on disk are unchanged.
- The generic PeopleList UI model (shared by follow sets and starter packs) and the
  EmojiPackSelection route (which shows a kind 30030 pack) keep their names.

Docs: plans, brainstorms, changelogs, skills and READMEs use the new names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015VvJ8XpeYqBe8bYT3bJ7UD
2026-09-26 20:56:07 +00:00

65 lines
3.4 KiB
Markdown

# 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(DeletionRequestEvent)`):** 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.