feat: Add NIP-44 validation and better gift wrap error diagnostics

- 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.)
This commit is contained in:
Claude
2026-01-16 08:01:43 +00:00
parent da72ce67ec
commit 23e8b20fb7
2 changed files with 64 additions and 0 deletions

View File

@@ -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",

View File

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