Входящие, история и зашифрованное хранение
Руководство по интеграции и справочник API: Входящие, история и зашифрованное хранение.
Этот подробный справочник API поддерживается на английском языке и соответствует указанному коду SDK. Примеры с переменными приложения требуют ваших ключей, средства подписи, хранилища или интерфейса.
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.