Boîte de réception, historique et stockage chiffré
Guide d’intégration et référence API : Boîte de réception, historique et stockage chiffré.
Cette référence détaillée de l’API est maintenue en anglais et suit le code SDK lié. Les exemples avec des variables applicatives nécessitent votre signataire, vos clés, votre stockage ou votre interface.
Layer 2 — Cache (/cache)
A complete inbox subsystem: subscribes to inbound events, auto-decrypts via your signer, dedupes, ingests into in-memory observables, and (optionally) persists to a DMStorage backend. Built on nostr-wot-sdk/data's sharedCoalescer, so DM subscriptions piggyback on the same connection pool used by profile/note fetchers.
Bootstrap a session
import {
initDMSession,
subscribeInbox,
sendDM,
localStorageDMStorage,
} from "@nostr-wot/dm/cache";
const session = await initDMSession({
myPubkey,
signer,
relays: ["wss://relay.damus.io", "wss://nos.lol"],
storage: localStorageDMStorage(), // optional persistence
discoverInboxRelays: true, // auto-resolve kind 10050
});
const stop = subscribeInbox(session); // live; returns teardown
await sendDM(session, partnerPubkey, "hi", { scheme: "nip17" }); // defaultsubscribeInbox opens three coalesced subscriptions: NIP-04 inbound (#p == me), NIP-04 outbound echo (authors == me), and NIP-17 gift wraps (#p == me). Every event is decrypted and ingested through the same ingestMessage pipeline — UI subscribers see one consistent stream.
Inbox-relay discovery + publishing (kind 10050)
import { fetchInboxRelays, publishInboxRelays } from "@nostr-wot/dm/cache";
// Where do my DMs go?
const inbox = await fetchInboxRelays(myPubkey, defaultRelays);
// Tell other clients where to send my DMs
await publishInboxRelays(signer, /* publish to */ relays, /* inbox = */ inbox);Without a kind-10050, your NIP-17 DMs only arrive from senders who happen to share a relay with you.
Historical backfill
subscribeInbox only catches new events. To populate threads on first login (or after a long offline period), walk past kind-1059 wraps:
import { backfillInbox } from "@nostr-wot/dm/cache";
const { partners, ingested, windowsWalked } = await backfillInbox(session, {
windows: 12, // 12 × 30-day = ≈1 year
limit: 200, // events per window per filter
minPartners: 50, // stop once enough partners discovered
timeoutMs: 30_000, // hard wallclock budget
});Read cursors (device-local)
Read state is intentionally never synced to relays — no server should know who you're DMing. Read cursors live in localStorage, keyed per account.
import {
markRead,
setReadCursor,
getReadCursor,
getUnreadCount,
getUnreadCounts,
detectScheme,
} from "@nostr-wot/dm/cache";
markRead(myPubkey, partnerPubkey);
const unread = getUnreadCount(myPubkey, partnerPubkey);
const allUnreads = getUnreadCounts(myPubkey); // { partnerHex: n, ... }
// Predict whether to send NIP-04 or NIP-17 to a partner based on
// the most recent cached messages.
const scheme = detectScheme(messages); // "nip04" | "nip17" | nullEncrypted-at-rest storage
Wrap any DMStorage with AES-GCM encryption keyed by a NIP-44-self-encrypted KEK. The raw key is imported as a non-extractable WebCrypto key — XSS can call your encrypt/decrypt helpers but cannot exfiltrate the bytes.
import {
initDMSession,
localStorageDMStorage,
getOrCreateCacheKey,
wrapStorageWithEncryption,
} from "@nostr-wot/dm/cache";
const key = await getOrCreateCacheKey(myPubkey, signer); // one-time signer prompt
const storage = wrapStorageWithEncryption(localStorageDMStorage()!, key);
await initDMSession({ myPubkey, signer, relays, storage });Custom storage backends
import type { DMStorage } from "@nostr-wot/dm/cache";
const indexedDBStorage: DMStorage = {
async load(myPubkey) { /* ... */ },
async save(myPubkey, conversations) { /* ... */ },
};Auto-persist + eviction
When you pass a storage to initDMSession, the session auto-persists on a debounce (default 500ms) after every cache mutation. Set autoPersist: false for manual control, or tune the debounce with autoPersistDebounceMs.
After each mutation the session also calls evictIfNeeded(myPubkey, evictionCap) (default cap 2000). Eviction is a no-op until you register a follow set — without one, every partner is treated as protected:
import { setFollowSet } from "@nostr-wot/dm/cache";
import { fetchFollows } from "nostr-wot-sdk/data";
const follows = await fetchFollows(myPubkey);
setFollowSet(myPubkey, new Set(follows.pubkeys)); // enables eviction
// or pass null to explicitly mark "follows un-hydrated → protect everything"
setFollowSet(myPubkey, null);Followed partners are never evicted; oldest non-followed messages drop first once the cap is exceeded.
Logout / account switch
import { closeDMSession, clearDMSession } from "@nostr-wot/dm/cache";
closeDMSession(session); // stop subscriptions
await clearDMSession(myPubkey, { storage, clearStorage: true }); // wipe stateOther signer backends
An existing signer integration can implement NostrSigner from nostr-wot-sdk/signers and pass it to initDMSession. Supply the NIP-44 methods needed for NIP-17; the package does not export an NDK adapter.