Fix admin delete hanging on "Deleting…" with Amber signer

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 <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-09-03 21:56:26 +07:00
parent 26245f3d05
commit 977d71dc60
2 changed files with 27 additions and 5 deletions

View file

@ -72,7 +72,14 @@ export function useDeleteEntity() {
// Admin takedown of someone else's content: also publish a client-side // 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 // 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) { if (isAdmin && !isOwner) {
const labelTags: string[][] = [ const labelTags: string[][] = [
['L', MODERATION_LABEL_NAMESPACE], ['L', MODERATION_LABEL_NAMESPACE],

View file

@ -29,7 +29,7 @@
import type { NostrEvent, NostrSigner } from '@nostrify/nostrify'; import type { NostrEvent, NostrSigner } from '@nostrify/nostrify';
import { Capacitor, registerPlugin } from '@capacitor/core'; import { Capacitor, registerPlugin } from '@capacitor/core';
import { nip19 } from 'nostr-tools'; import { nip19, getEventHash } from 'nostr-tools';
const AMBER_PACKAGE = 'com.greenart7c3.nostrsigner'; 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 // Merge Amber's response over the original event so required fields are
// never lost, then verify the essentials are present before returning. // 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, kind: signed.kind ?? event.kind,
content: signed.content ?? event.content, content: signed.content ?? event.content,
tags: signed.tags ?? event.tags, tags: signed.tags ?? event.tags,
created_at: signed.created_at ?? event.created_at, created_at: signed.created_at ?? event.created_at,
pubkey: signed.pubkey ?? this.pubkey, pubkey,
id: signed.id ?? '', };
// 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, sig: signed.sig,
}; };