Two replaceable-event (NIP-33/kind 30078) lifecycle fixes found auditing
edit/delete/tombstone:
1. created_at monotonicity. Edits and delete tombstones used bare
Date.now(), with no guarantee of being newer than the version they replace.
NIP-01 keeps the highest created_at (ties -> lowest id), so a device clock
that's behind — or a second edit within the same second — left relays
serving the OLD version: edits silently didn't apply, and (worse) deletions
didn't actually remove the video for other clients (the local in-memory
tombstone masked the failure locally). The edit and revert/delete builders
now use max(now, baseEvent.created_at + 1). created_at is threaded through
revertVideo and _softDeleteVersions so the bulk-delete path keeps it too.
2. Legacy tombstone key mismatch. The deletion builder synthesizes
videoRootId = "LEGACY:<pubkey>:<dTag>" for legacy videos, so its tombstone
keyed as "ROOT:LEGACY:..." while the original legacy event (no videoRootId)
keyed as "<pubkey>:<dTag>" — the tombstone never matched the zombie it was
meant to suppress. getActiveKey now treats a "LEGACY:"-prefixed root as
non-distinct and falls through to the pubkey:dTag identity, so both match.
Verified sound and unchanged: d-tag + videoRootId preservation on edit,
ownership/version checks, dual delete (NIP-09 kind-5 + per-d-tag soft-delete),
and the tombstone guard wired across all ingestion paths.
Tests: edit/tombstone created_at is bumped above a future-dated base
(clock-skew) and uses wall-clock for a past base; a legacy deletion shares the
original event's key. Both fixes mutation-verified.
Co-Authored-By: Claude Opus 4.8 <[email protected]>