From 5e8544316b393913da346aa057d38da6b96ef41a Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Thu, 3 Sep 2026 20:20:08 +0700 Subject: [PATCH] Audit fixes: harden Amber signEvent response handling, fix web AmberSigner signEvent naming, add Blossom upload fallbacks, sanitize untrusted URLs in FoodPage/CreateTourPage Co-authored-by: shakespeare.diy --- src/hooks/useUploadFile.ts | 5 ++++ src/lib/amber.ts | 10 +++++-- src/lib/capacitorAmberSigner.ts | 51 +++++++++++++++++++++++++++++++-- src/pages/CreateTourPage.tsx | 5 ++-- src/pages/FoodPage.tsx | 9 +++--- 5 files changed, 70 insertions(+), 10 deletions(-) diff --git a/src/hooks/useUploadFile.ts b/src/hooks/useUploadFile.ts index a3c9793..6738164 100644 --- a/src/hooks/useUploadFile.ts +++ b/src/hooks/useUploadFile.ts @@ -13,8 +13,13 @@ export function useUploadFile() { } const uploader = new BlossomUploader({ + // Multiple servers so a single slow/down Blossom host doesn't hang the + // upload (crew logo uploads were stalling when primal.net was slow). + // The uploader tries them in order and falls back on failure. servers: [ 'https://blossom.primal.net/', + 'https://blossom.band/', + 'https://cdn.satellite.earth/', ], signer: user.signer, }); diff --git a/src/lib/amber.ts b/src/lib/amber.ts index ae4184f..dd1a51b 100644 --- a/src/lib/amber.ts +++ b/src/lib/amber.ts @@ -604,11 +604,17 @@ export class AmberSigner implements NostrSigner { return this.pubkey; } - async sign(event: Omit): Promise { + // NOTE: Nostrify's NostrSigner interface requires `signEvent` (not `sign`). + // The old method was named `sign`, so every `user.signer.signEvent(...)` call + // on a web/PWA Amber login threw "signer.signEvent is not a function". This + // now correctly implements the interface. On web, signing genuinely requires + // a redirect round-trip to Amber, so this navigates away and the result is + // processed on return by AmberCallbackPage. + async signEvent(event: Omit): Promise { // Store the current state and redirect to Amber // The result will be processed on page reload openAmberForSignEvent(event); - + // This promise will never resolve in the current page context // because we're redirecting to Amber return new Promise(() => { diff --git a/src/lib/capacitorAmberSigner.ts b/src/lib/capacitorAmberSigner.ts index 8533870..b54d0c9 100644 --- a/src/lib/capacitorAmberSigner.ts +++ b/src/lib/capacitorAmberSigner.ts @@ -202,7 +202,7 @@ export class CapacitorAmberSigner implements NostrSigner { async signEvent(event: Omit): Promise { const plugin = await getPlugin(); - const { event: signedJson } = await withTimeout( + const res = await withTimeout( plugin.signEvent({ packageName: AMBER_PACKAGE, eventJson: JSON.stringify(event), @@ -211,7 +211,54 @@ export class CapacitorAmberSigner implements NostrSigner { }), 'signature', ); - return JSON.parse(signedJson) as NostrEvent; + + // Amber's response shape varies across builds: some return the full signed + // event JSON in `event`, others return only `signature` (expecting the caller + // to attach it to the original event). If we blindly `JSON.parse` an empty + // `event`, or return an event with no `sig`, the resulting object silently + // publishes as a malformed event and stalls the relay forever — the exact + // "publish hangs" symptom. Handle every shape defensively and always fail + // loud rather than emitting a bad event. + let signed: Partial | null = null; + + if (res.event && res.event.trim()) { + try { + signed = JSON.parse(res.event) as Partial; + } catch { + signed = null; + } + } + + // Fall back to reconstructing the event from a bare signature. + if ((!signed || !signed.sig) && res.signature) { + signed = { ...(signed ?? {}), sig: res.signature }; + } + + if (!signed || !signed.sig) { + throw new Error( + 'Amber returned an incomplete signature. Open Amber, approve the request, then try again.', + ); + } + + // 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 = { + 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 ?? '', + sig: signed.sig, + }; + + if (!merged.id || !merged.sig || !merged.pubkey) { + throw new Error( + 'Amber returned a malformed event. Please retry, or restart Amber if it persists.', + ); + } + + return merged; } readonly nip04 = { diff --git a/src/pages/CreateTourPage.tsx b/src/pages/CreateTourPage.tsx index 7b431a0..60091f3 100644 --- a/src/pages/CreateTourPage.tsx +++ b/src/pages/CreateTourPage.tsx @@ -14,6 +14,7 @@ import { useNostrPublish } from '@/hooks/useNostrPublish'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useUploadFile } from '@/hooks/useUploadFile'; import { useShops } from '@/hooks/useShops'; +import { sanitizeUrl } from '@/lib/sanitizeUrl'; import { useToast } from '@/hooks/useToast'; import { LoginArea } from '@/components/auth/LoginArea'; import { FeaturedUpgrade } from '@/components/FeaturedEventUpgrade'; @@ -1811,9 +1812,9 @@ export default function CreateTourPage() {

)}