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") ||