Extract eleven topic areas into loadable skills so AGENTS.md can serve as a scannable overview instead of a specification dump. The file shrinks from 1480 to 358 lines (~76%) while keeping every concrete rule, critical code pattern, and pointer that an agent needs on first read. New Ditto-specific skills: - nostr-kinds: NIP-vs-custom-kind decision framework, kind ranges, tag design, content-vs-tags, NIP.md update rule, and Ditto's seven-location UI registration checklist for new kinds (NoteCard, PostDetailPage, extraKinds.ts, KIND_LABELS/KIND_ICONS in CommentContext, WELL_KNOWN_KIND_LABELS in ExternalContentHeader, EmbeddedNote/EmbeddedNaddr, ReplyComposeModal). - nostr-publishing: useNostrPublish, the read-modify-write pattern via fetchFreshEvent + prev for replaceable/addressable events, published_at contract, and d-tag collision prevention. - nostr-queries: the standard useNostr + useQuery pattern, combining kinds into one filter to avoid rate limits, and the NIP-52 validator walkthrough. - theming: @fontsource install flow, the Ditto runtime font-loader path (sanitizeUrl + sanitizeCssString), color scheme variables, useTheme toggle, and the isolate + negative-z-index gotcha. - ci-cd-publishing: Zapstore NIP-46 bunker auth (zsp + nip46-auth.mjs), nsite deploys (nsyte nbunksec + configured relays/servers), and Google Play AAB uploads via fastlane supply (service-account JSON base64 encoding and rotation). - capacitor-compat: WKWebView/WebView limitations, the downloadTextFile / openUrl helpers in src/lib/downloadFile.ts, platform detection, and the full plugin list. - git-workflow: pre-commit validation order and the Regression-of: trailer convention used by the release skill's changelog generator. Ported from mkstack, lightly adapted where needed: - nip19-routing: root-level /:nip19 routing and filter construction patterns (adapted to reference Ditto's existing NIP19Page). - nostr-relay-pools: nostr.relay() and nostr.group() for targeted queries. - nostr-encryption: NIP-44 / NIP-04 via the user's signer. - file-uploads: useUploadFile + Blossom + NIP-94 imeta tag construction. AGENTS.md itself now follows mkstack's density — concrete rules inline, one code example per section, pointer to the matching skill for details. The enumerations that previously bloated it (every shadcn primitive, every hook, every Capacitor plugin, the full NostrMetadata type dump, the NIP-19 prefix reference table, etc.) are either removed in favor of "ls the directory" or moved into their skill.
3.3 KiB
name, description
| name | description |
|---|---|
| nostr-encryption | Encrypt and decrypt content for Nostr direct messages, gift wraps, or any feature that needs NIP-44 (or legacy NIP-04) ciphertext, using the logged-in user's signer. |
Nostr Encryption and Decryption
The logged-in user exposes a signer object that matches the NIP-07 signer interface. The signer handles all cryptographic operations internally — including ECDH, conversation-key derivation, and AEAD — so your code never touches a private key.
Always use the signer interface for encryption. Never ask the user for their private key, and never derive a shared secret yourself.
NIP-44 (preferred)
NIP-44 is the modern, authenticated encryption scheme used for DMs (NIP-17), gift wraps (NIP-59), and most new encrypted payloads.
import { useCurrentUser } from "@/hooks/useCurrentUser";
function useEncryptedNote() {
const { user } = useCurrentUser();
if (!user) throw new Error("Must be logged in");
// Guard: older signers may not support NIP-44 yet.
if (!user.signer.nip44) {
throw new Error(
"Please upgrade your signer extension to a version that supports NIP-44 encryption",
);
}
// Encrypt a message to a recipient (use your own pubkey to encrypt to self).
const ciphertext = await user.signer.nip44.encrypt(
recipientPubkey,
"hello world",
);
// Decrypt a message from a sender (use the *other party's* pubkey).
const plaintext = await user.signer.nip44.decrypt(senderPubkey, ciphertext);
return plaintext;
}
Key points
encrypt(peerPubkey, plaintext)—peerPubkeyis the other party's hex public key. For self-encryption (notes, backups), passuser.pubkey.decrypt(peerPubkey, ciphertext)—peerPubkeyis the author of the ciphertext you're decrypting (for an incoming DM, this is the sender's pubkey).- Both methods are async and may throw if the signer rejects the request or the ciphertext is malformed. Wrap calls in
try/catch. - The signer handles conversation-key caching; repeated calls for the same peer are cheap.
NIP-04 (legacy)
NIP-04 is only needed when interacting with older clients that haven't adopted NIP-44. The API mirrors NIP-44:
if (!user.signer.nip04) {
throw new Error("Signer does not support NIP-04");
}
const ciphertext = await user.signer.nip04.encrypt(peerPubkey, plaintext);
const plaintext = await user.signer.nip04.decrypt(peerPubkey, ciphertext);
Prefer NIP-44 for anything new. Only fall back to NIP-04 when a spec or peer explicitly requires it.
Patterns
Encrypt-to-self (drafts, private notes)
const ciphertext = await user.signer.nip44.encrypt(user.pubkey, draft);
createEvent({ kind: 30078, content: ciphertext, tags: [["d", "my-draft"]] });
Decrypt an incoming DM (NIP-17 / NIP-59)
For gift-wrapped DMs, you'll typically decrypt the outer wrap, then the inner seal, then read the rumor's content. Each decryption uses the sender of that specific layer as the peer pubkey.
Guarding the UI
Always check user.signer.nip44 (or nip04) before calling encryption methods. Remote signers and older browser extensions may not implement every interface, and catching the missing-capability case lets you show a useful message ("Please upgrade your signer") instead of an unhandled promise rejection.