Nostr WoT

Posteingang, Verlauf und verschlüsselte Speicherung

Integrationsanleitung und API-Referenz: Posteingang, Verlauf und verschlüsselte Speicherung.

Diese ausführliche API-Referenz wird auf Englisch gepflegt und folgt dem verlinkten SDK-Quellcode. Beispiele mit Anwendungsvariablen benötigen Ihren Signer, Ihre Schlüssel, Ihren Speicher oder Ihre Oberfläche.

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

ts
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" }); // default

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

ts
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:

ts
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.

ts
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" | null

Encrypted-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.

ts
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

ts
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:

ts
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

ts
import { closeDMSession, clearDMSession } from "@nostr-wot/dm/cache";

closeDMSession(session);                               // stop subscriptions
await clearDMSession(myPubkey, { storage, clearStorage: true }); // wipe state

Other 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.


API-Referenz