From 977d71dc6081312fcc6e7d1e0367cb93d31a8489 Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Thu, 3 Sep 2026 21:56:26 +0700 Subject: [PATCH] =?UTF-8?q?Fix=20admin=20delete=20hanging=20on=20"Deleting?= =?UTF-8?q?=E2=80=A6"=20with=20Amber=20signer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compute event id locally in CapacitorAmberSigner.signEvent instead of relying on Amber to echo it back. Amber (NIP-55) commonly returns only a bare `signature` for kind:5 deletions and kind:1985 moderation labels, leaving id empty — which failed the malformed-event check or emitted a bad event that stalled the relay forever (the "stuck on Deleting…" symptom). The id is a deterministic NIP-01 hash of the signed fields, so we derive it ourselves, making signature-only responses work for every kind including admin takedowns. Co-authored-by: shakespeare.diy --- src/hooks/useDeleteEntity.ts | 9 ++++++++- src/lib/capacitorAmberSigner.ts | 23 +++++++++++++++++++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/hooks/useDeleteEntity.ts b/src/hooks/useDeleteEntity.ts index a2c8068..c2492d8 100644 --- a/src/hooks/useDeleteEntity.ts +++ b/src/hooks/useDeleteEntity.ts @@ -72,7 +72,14 @@ export function useDeleteEntity() { // Admin takedown of someone else's content: also publish a client-side // moderation label so the app hides it even if relays won't honor a - // cross-author NIP-09 deletion. + // cross-author NIP-09 deletion. This label is what actually hides the + // content across SkateSpots (see useAdminModeration), so it MUST succeed — + // we await it and let a failure surface to the caller as an error. + // + // NOTE: this requires a SECOND Amber approval. Both signatures now compute + // their event id locally (see CapacitorAmberSigner.signEvent), so a + // signature-only Amber response no longer produces an empty id that stalls + // the publish forever — the previous "stuck on Deleting…" cause. if (isAdmin && !isOwner) { const labelTags: string[][] = [ ['L', MODERATION_LABEL_NAMESPACE], diff --git a/src/lib/capacitorAmberSigner.ts b/src/lib/capacitorAmberSigner.ts index b54d0c9..bccd699 100644 --- a/src/lib/capacitorAmberSigner.ts +++ b/src/lib/capacitorAmberSigner.ts @@ -29,7 +29,7 @@ import type { NostrEvent, NostrSigner } from '@nostrify/nostrify'; import { Capacitor, registerPlugin } from '@capacitor/core'; -import { nip19 } from 'nostr-tools'; +import { nip19, getEventHash } from 'nostr-tools'; const AMBER_PACKAGE = 'com.greenart7c3.nostrsigner'; @@ -242,13 +242,28 @@ export class CapacitorAmberSigner implements NostrSigner { // Merge Amber's response over the original event so required fields are // never lost, then verify the essentials are present before returning. - const merged: NostrEvent = { + const pubkey = signed.pubkey ?? this.pubkey; + const base = { kind: signed.kind ?? event.kind, content: signed.content ?? event.content, tags: signed.tags ?? event.tags, created_at: signed.created_at ?? event.created_at, - pubkey: signed.pubkey ?? this.pubkey, - id: signed.id ?? '', + pubkey, + }; + + // CRITICAL: Amber (NIP-55) commonly returns ONLY the `signature` for events + // like kind:5 deletions and kind:1985 labels — no `id` and no full event + // JSON. Previously we left `id: ''`, which failed the malformed-event check + // below (or emitted a bad event that stalled the relay forever). The event + // id is a deterministic NIP-01 hash of these fields, so we compute it + // ourselves instead of depending on Amber to echo it back. This makes + // signature-only responses work for EVERY kind — including admin takedowns. + const id = + signed.id && signed.id.length === 64 ? signed.id : getEventHash(base); + + const merged: NostrEvent = { + ...base, + id, sig: signed.sig, };