From 23e8b20fb78643f33ff22df4390e4aa6313991ac Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Jan 2026 08:01:43 +0000 Subject: [PATCH] feat: Add NIP-44 validation and better gift wrap error diagnostics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Validate signer has NIP-44 support before attempting decryption - NIP-59 gift wraps REQUIRE NIP-44 encryption - Fail early with helpful message if signer lacks nip44 - Suggests using compatible extensions (Alby, nos2x-fox, etc.) - Add specific error messages for common decryption failures - JSON parse errors → "Decrypted content is not valid JSON" - Missing event properties → "Seal is missing required properties" - Author mismatch → "Seal and rumor authors don't match" - Includes event ID in error messages for debugging - Add test for signer without NIP-44 support This helps diagnose the two production errors: 1. "Unexpected token 'T', "This is to"..." → JSON parse failure 2. "can't serialize event" → malformed seal event Users will now see clear error messages explaining the issue (incompatible extension, malformed gift wrap, etc.) --- src/services/gift-wrap.test.ts | 26 +++++++++++++++++++++++ src/services/gift-wrap.ts | 38 ++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/services/gift-wrap.test.ts b/src/services/gift-wrap.test.ts index d009ba6..eddd317 100644 --- a/src/services/gift-wrap.test.ts +++ b/src/services/gift-wrap.test.ts @@ -53,6 +53,32 @@ describe("unwrapAndUnseal", () => { }); describe("validation", () => { + it("should reject signer without NIP-44 support", async () => { + const giftWrap: NostrEvent = { + id: "test-id", + pubkey: "ephemeral-key", + created_at: 1234567890, + kind: 1059, + tags: [], + content: "encrypted-content", + sig: "signature", + }; + + // Signer without nip44 + const signerWithoutNip44: ISigner = { + getPublicKey: vi.fn().mockResolvedValue("mock-pubkey"), + signEvent: vi.fn(), + }; + + await expect( + unwrapAndUnseal(giftWrap, signerWithoutNip44), + ).rejects.toThrow(GiftWrapError); + + await expect( + unwrapAndUnseal(giftWrap, signerWithoutNip44), + ).rejects.toThrow("does not support NIP-44"); + }); + it("should reject non-1059 events", async () => { const invalidGiftWrap: NostrEvent = { id: "test-id", diff --git a/src/services/gift-wrap.ts b/src/services/gift-wrap.ts index 2e2c910..8768643 100644 --- a/src/services/gift-wrap.ts +++ b/src/services/gift-wrap.ts @@ -89,6 +89,16 @@ export async function unwrapAndUnseal( throw new GiftWrapError("Gift wrap content is empty", "MISSING_CONTENT"); } + // CRITICAL: Validate signer has NIP-44 support + // NIP-59 gift wraps REQUIRE NIP-44 encryption + if (!signer.nip44) { + throw new GiftWrapError( + "Signer does not support NIP-44 encryption (required for NIP-59 gift wraps). " + + "Please use a browser extension that supports NIP-44 (e.g., Alby, nos2x-fox, Nostr-Connect).", + "NO_SIGNER", + ); + } + // CRITICAL: Add to event store before unlocking // Applesauce requires the event to be in the store for caching to work // We wrap in try-catch because the event might not be valid for the event store @@ -147,6 +157,34 @@ export async function unwrapAndUnseal( const message = error instanceof Error ? error.message : String(error); + // Provide helpful error messages for common issues + if (message.includes("Unexpected token") || message.includes("JSON")) { + throw new GiftWrapError( + `Decrypted content is not valid JSON. This gift wrap may be malformed or use incompatible encryption. ` + + `Event ID: ${giftWrap.id.slice(0, 16)}... (${message})`, + "INVALID_SEAL", + ); + } + + if ( + message.includes("can't serialize") || + message.includes("wrong or missing properties") + ) { + throw new GiftWrapError( + `Decrypted seal is missing required event properties. This gift wrap may be malformed. ` + + `Event ID: ${giftWrap.id.slice(0, 16)}... (${message})`, + "INVALID_SEAL", + ); + } + + if (message.includes("Seal author does not match rumor author")) { + throw new GiftWrapError( + `Seal and rumor authors don't match - possible tampering. ` + + `Event ID: ${giftWrap.id.slice(0, 16)}...`, + "INVALID_SEAL", + ); + } + // Map decryption errors if ( message.includes("decrypt") ||