From 848cdf763e34034df3315544915f914e1daa282d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Lohrer?= Date: Tue, 28 Apr 2026 08:19:20 +0200 Subject: [PATCH] fix(snapshot): NIP-09-filter beachtet zeitliche reihenfolge Per NIP-09 darf ein deletion nur events mit created_at <= deletion.created_at loeschen. Vorher wurde ein re-publizierter post nach geloeschtem vorgaenger stumm wegfiltern. Code-review-feedback aus etappe 2. Co-Authored-By: Claude Opus 4.7 (1M context) --- snapshot/src/core/nip09-filter.ts | 11 ++++++++--- snapshot/tests/nip09-filter.test.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/snapshot/src/core/nip09-filter.ts b/snapshot/src/core/nip09-filter.ts index cd2d602..0caaf7e 100644 --- a/snapshot/src/core/nip09-filter.ts +++ b/snapshot/src/core/nip09-filter.ts @@ -5,18 +5,23 @@ export function filterDeleted( deletions: SignedEvent[], authorPubkey: string, ): SignedEvent[] { - const deletedCoords = new Set() + const deletedAtByCoord = new Map() for (const del of deletions) { if (del.kind !== 5) continue if (del.pubkey !== authorPubkey) continue for (const tag of del.tags) { - if (tag[0] === 'a' && tag[1]) deletedCoords.add(tag[1]) + if (tag[0] !== 'a' || !tag[1]) continue + const previous = deletedAtByCoord.get(tag[1]) + if (previous === undefined || del.created_at > previous) { + deletedAtByCoord.set(tag[1], del.created_at) + } } } return events.filter((ev) => { const d = ev.tags.find((t) => t[0] === 'd')?.[1] if (!d) return true const coord = `${ev.kind}:${ev.pubkey}:${d}` - return !deletedCoords.has(coord) + const deletedAt = deletedAtByCoord.get(coord) + return deletedAt === undefined || ev.created_at > deletedAt }) } diff --git a/snapshot/tests/nip09-filter.test.ts b/snapshot/tests/nip09-filter.test.ts index 0eabf6b..2112709 100644 --- a/snapshot/tests/nip09-filter.test.ts +++ b/snapshot/tests/nip09-filter.test.ts @@ -28,3 +28,30 @@ Deno.test('filterDeleted ignoriert kind:5 fremder pubkeys', () => { const out = filterDeleted([post('alive', 'a')], [fremde], 'P') assertEquals(out.length, 1) }) + +Deno.test('filterDeleted: re-publizierter post (post.created_at > deletion.created_at) bleibt erhalten', () => { + const oldDelete: SignedEvent = { + id: 'del', pubkey: 'P', created_at: 100, kind: 5, sig: 's', content: '', + tags: [['a', '30023:P:resurrected']], + } + const newPost: SignedEvent = { + id: 'new', pubkey: 'P', created_at: 200, kind: 30023, sig: 's', content: '', + tags: [['d', 'resurrected']], + } + const out = filterDeleted([newPost], [oldDelete], 'P') + assertEquals(out.length, 1) + assertEquals(out[0].id, 'new') +}) + +Deno.test('filterDeleted: post mit created_at <= deletion.created_at wird entfernt', () => { + const newDelete: SignedEvent = { + id: 'del', pubkey: 'P', created_at: 200, kind: 5, sig: 's', content: '', + tags: [['a', '30023:P:dead']], + } + const oldPost: SignedEvent = { + id: 'old', pubkey: 'P', created_at: 100, kind: 30023, sig: 's', content: '', + tags: [['d', 'dead']], + } + const out = filterDeleted([oldPost], [newDelete], 'P') + assertEquals(out.length, 0) +})