Skip to content

NIP Support Status

NIP (Nostr Implementation Possibilities) support status for tsunagiya v0.3.0.


Supported NIPs (v0.3.0)

NIPDescriptionSupport Status
NIP-01Basic ProtocolEVENT, REQ, CLOSE, EOSE, OK, CLOSED, NOTICE + Event Treatment + Addressable Events
NIP-04Encrypted DM ⚠️ deprecated (→ NIP-17)EventBuilder template (migration to NIP-17 recommended)
NIP-09Event Deletionkind:5 deletion request handling
NIP-10Reply ThreadingEventBuilder e/p tags
NIP-11Relay InformationsetInfo/getInfo + fetch interception
NIP-17Private Direct MessagesEventBuilder templates (chatMessage/seal/giftWrap/dmRelayList)
NIP-18RepostsEventBuilder templates (repost/genericRepost)
NIP-23Long-form ContentEventBuilder templates (longFormContent/longFormDraft)
NIP-25ReactionsEventBuilder withReactions / externalReaction
NIP-29Relay-based GroupsEventBuilder templates
NIP-30Custom EmojiEventBuilder emoji tag
NIP-40Expiration TimestampEventBuilder withExpiration()
NIP-42AUTHChallenge/response
NIP-45COUNTCOUNT message support
NIP-50SearchContent partial-match search
NIP-51ListsEventBuilder templates (muteList/pinList/bookmarks/followSet, etc.)
NIP-52Calendar EventsEventBuilder templates (all 4 types: Date/Time/Collection/RSVP)
NIP-57Lightning ZapsEventBuilder templates
NIP-65Relay List MetadataEventBuilder relayList (kind:10002)

Note: The former NIP-16 (Event Treatment) and NIP-33 (Parameterized Replaceable Events) have been merged into NIP-01. Regular/Replaceable/Ephemeral/Addressable event handling in this library is part of NIP-01 support.


Details and Examples for Each NIP

NIP-01 filter usage:

typescript
// All filter conditions supported
const filter: NostrFilter = {
  ids: ["prefix..."], // ID prefix match
  authors: ["prefix..."], // public key prefix match
  kinds: [1], // exact kind match
  since: 1700000000, // created_at lower bound
  until: 1700100000, // created_at upper bound
  limit: 20, // maximum results
  "#e": ["eventId"], // tag filter
  "#p": ["pubkey"], // tag filter
};

NIP-01 usage:

typescript
const pool = new MockPool();
const relay = pool.relay("wss://relay.example.com");

relay.store(EventBuilder.kind1().content("hello").build());

pool.install();
try {
  const ws = new WebSocket("wss://relay.example.com");
  ws.onopen = () => ws.send(JSON.stringify(["REQ", "sub1", { kinds: [1] }]));
  // → returns EVENT, EOSE
} finally {
  pool.uninstall();
}

NIP-04 DM template (⚠️ deprecated — migration to NIP-17 recommended):

typescript
const dm = EventBuilder.dm("recipient-pubkey", "hello").build();
// → kind: 4, content: "mock-encrypted:hello", tags: [["p", "recipient-pubkey"]]

NIP-09 deletion request:

typescript
const deletion = EventBuilder.deletion(["target-event-id1", "target-event-id2"])
  .pubkey(authorPubkey)
  .build();
relay.store(deletion);

const addrDeletion = EventBuilder.deletionByAddress([
  "30023:pubkey:article-slug",
])
  .pubkey(authorPubkey)
  .build();
relay.store(addrDeletion);

console.log(relay.deletedIds); // Set { "target-event-id1", "target-event-id2" }

NIP-10 reply threading:

typescript
const thread = EventBuilder.thread(5);
// thread[0]: root (no tags)
// thread[1]: reply (["e", root.id, "", "root"], ["p", root.pubkey])
// thread[2]: reply (["e", root.id, "", "root"], ["e", thread[1].id, "", "reply"], ["p", thread[1].pubkey])

NIP-01 event types (formerly NIP-16 — now merged into NIP-01):

typescript
import { classifyEvent, isEphemeral, isReplaceable } from "@ikuradon/tsunagiya";

classifyEvent(1); // "regular"
classifyEvent(10002); // "replaceable"
classifyEvent(20001); // "ephemeral"
classifyEvent(30023); // "parameterized_replaceable"

isReplaceable(10002); // true
isEphemeral(20001); // true

NIP-01 Addressable Events (formerly NIP-33 — now merged into NIP-01):

typescript
import {
  getParameterizedId,
  isParameterizedReplaceable,
} from "@ikuradon/tsunagiya";

isParameterizedReplaceable(30023); // true

const article = EventBuilder.kind(30023)
  .tag("d", "my-article")
  .pubkey("author-pubkey")
  .content("article content")
  .build();

getParameterizedId(article); // "30023:author-pubkey:my-article"

relay.store(article);
const updated = EventBuilder.kind(30023)
  .tag("d", "my-article")
  .pubkey("author-pubkey")
  .createdAt(article.created_at + 60)
  .content("updated content")
  .build();
relay.store(updated); // true (old version is replaced)

NIP-17 Private Direct Messages:

typescript
import { EventBuilder, FilterBuilder } from "@ikuradon/tsunagiya/testing";

// Chat Message (kind:14)
const chatMsg = EventBuilder.chatMessage({
  recipientPubkey: "recipient-pubkey",
  content: "hello!",
  subject: "greeting",
}).build();

// Seal (kind:13) — wraps chat message
const seal = EventBuilder.seal(chatMsg).build();

// Gift Wrap (kind:1059) — wraps seal with random pubkey
const wrapped = EventBuilder.giftWrap({
  recipientPubkey: "recipient-pubkey",
  innerEvent: seal,
}).build();

// DM Relay List (kind:10050)
const dmList = EventBuilder.dmRelayList([
  "wss://dm.relay1.test",
  "wss://dm.relay2.test",
]).build();

// NIP-17: privateDM (generates chatMessage → seal → giftWrap in one call)
const dm = EventBuilder.privateDM({
  recipientPubkey: "recipient-pubkey",
  content: "secret message",
});
// dm.kind === 1059 (Gift Wrap)

// Filters
FilterBuilder.giftWraps("recipient-pubkey"); // { kinds: [1059], "#p": [...] }
FilterBuilder.dmRelayList("pubkey"); // { kinds: [10050], authors: [...] }

NIP-18 Reposts:

typescript
import { EventBuilder, FilterBuilder } from "@ikuradon/tsunagiya/testing";

const original = EventBuilder.kind1().content("original post").build();

// Repost (kind:6)
const repost = EventBuilder.repost(original, "wss://relay.example.com").build();
// → kind: 6, content: JSON.stringify(original), tags: [["e", ...], ["p", ...]]

// Generic Repost (kind:16) — for non-kind:1 events
const article = EventBuilder.kind(30023).tag("d", "my-article").build();
const genericRepost = EventBuilder.genericRepost(article).build();
// → kind: 16, tags: [["e", ...], ["p", ...], ["k", "30023"]]

// Filters
FilterBuilder.reposts(original.id); // { kinds: [6], "#e": [...] }
FilterBuilder.allReposts(original.id); // { kinds: [6, 16], "#e": [...] }

NIP-23 Long-form Content:

typescript
import { EventBuilder, FilterBuilder } from "@ikuradon/tsunagiya/testing";

// Long-form Content (kind:30023)
const article = EventBuilder.longFormContent({
  identifier: "my-article-2026",
  title: "Getting Started with Nostr",
  content: "## Introduction\nNostr is...",
  summary: "A beginner's guide to Nostr",
  publishedAt: 1740000000,
  hashtags: ["nostr", "tutorial"],
}).build();

// Long-form Draft (kind:30024)
const draft = EventBuilder.longFormDraft({
  identifier: "draft-article",
  title: "Draft Article",
  content: "Work in progress...",
}).build();

// Filters
FilterBuilder.longFormContent(); // { kinds: [30023] }
FilterBuilder.longFormContent("author-pk"); // { kinds: [30023], authors: [...] }
FilterBuilder.longFormByTag("nostr"); // { kinds: [30023], "#t": ["nostr"] }

NIP-25 Reactions:

typescript
const [post, reactions] = EventBuilder.withReactions(5);
// reactions[n]: kind: 7, content: "+", tags: [["e", post.id], ["p", post.pubkey]]

// with options (content, targetKind)
const [post2, reactions2] = EventBuilder.withReactions(3, {
  content: "🤙",
  targetKind: 1,
});
// reactions2[n]: kind: 7, content: "🤙", tags: [..., ["k", "1"]]

// External content reaction (kind:17, NIP-25)
const externalReaction = EventBuilder.externalReaction(
  "https://example.com/article",
  "text/html",
).build();
// → kind: 17, content: "+", tags: [["i", url], ["k", contentType]]

// Filter (address-based)
FilterBuilder.reactionsTo("30023:pubkey:my-article"); // { kinds: [7], "#a": [...] }

NIP-29 Group chat:

typescript
const msg = EventBuilder.groupMessage("group-id").content("hello group")
  .build();
// → kind: 9, tags: [["h", "group-id"]]

NIP-30 Custom emoji:

typescript
const event = EventBuilder.kind1()
  .emoji("sushi", "https://example.com/sushi.png")
  .build();
// → tags: [["emoji", "sushi", "https://example.com/sushi.png"]]

NIP-42 AUTH:

typescript
const relay = pool.relay("wss://auth.relay.test", { requiresAuth: true });

// Default validation (no validator set): kind:22242 + challenge + relay URL match
// Custom validator: access relayUrl / challenge from context
relay.requireAuth((authEvent, context) => {
  return authEvent.tags.some(
    (t) => t[0] === "relay" && t[1] === context.relayUrl,
  );
});

NIP-45 COUNT:

typescript
const relay = pool.relay("wss://relay.example.com");

for (const event of EventBuilder.bulk(50, { kind: 1 })) {
  relay.store(event);
}

pool.install();
try {
  const ws = new WebSocket("wss://relay.example.com");
  ws.onopen = () => {
    ws.send(JSON.stringify(["COUNT", "count1", { kinds: [1] }]));
  };
  ws.onmessage = (e) => {
    const msg = JSON.parse(e.data);
    // → ["COUNT", "count1", { count: 50 }]
  };
} finally {
  pool.uninstall();
}

relay.onCOUNT((subId, filters) => {
  return { count: 42 };
});

NIP-50 Search:

typescript
import { FilterBuilder } from "@ikuradon/tsunagiya/testing";

relay.store(EventBuilder.kind1().content("Hello Nostr World").build());
relay.store(EventBuilder.kind1().content("goodbye").build());

pool.install();
try {
  const ws = new WebSocket("wss://relay.example.com");
  ws.onopen = () => {
    ws.send(JSON.stringify(["REQ", "search1", { search: "nostr" }]));
    // → only "Hello Nostr World" matches
  };
} finally {
  pool.uninstall();
}

const filter = FilterBuilder.search("nostr");
// → { search: "nostr" }

NIP-52 Calendar Events (all 4 types supported):

typescript
import { EventBuilder, FilterBuilder } from "@ikuradon/tsunagiya/testing";

// Date-based Calendar Event (kind:31922)
const dateEvent = EventBuilder.calendarDateEvent({
  title: "Nostr Meetup",
  startDate: "2026-03-01",
  endDate: "2026-03-01",
  location: "Tokyo",
  geohash: "xn76g",
}).build();

// Time-based Calendar Event (kind:31923)
const timeEvent = EventBuilder.calendarTimeEvent({
  title: "Online Seminar",
  start: 1740000000,
  end: 1740003600,
  startTzid: "Asia/Tokyo",
}).build();

// Calendar Collection (kind:31924)
const collection = EventBuilder.calendarCollection({
  title: "Tech Events 2026",
  events: ["31922:pubkey:meetup", "31923:pubkey:seminar"],
}).build();

// RSVP (kind:31925)
const rsvp = EventBuilder.calendarRsvp({
  eventAddress: "31922:pubkey:meetup",
  status: "accepted",
}).build();

// Geohash tag (still available)
const event = EventBuilder.kind1().geohash("u4pruydqqvj").build();

// Filters
FilterBuilder.calendarDateEvents(); // { kinds: [31922] }
FilterBuilder.calendarTimeEvents(); // { kinds: [31923] }
FilterBuilder.calendarEvents(); // { kinds: [31922, 31923] }
FilterBuilder.calendarCollections(); // { kinds: [31924] }
FilterBuilder.rsvps("31922:pubkey:meetup"); // { kinds: [31925], "#a": [...] }

NIP-51 Lists:

typescript
import { EventBuilder, FilterBuilder } from "@ikuradon/tsunagiya/testing";

// Mute List (kind:10000)
const mute = EventBuilder.muteList({
  pubkeys: ["muted-pubkey-1", "muted-pubkey-2"],
  hashtags: ["spam"],
  words: ["badword"],
}).build();

// Pin List (kind:10001)
const pins = EventBuilder.pinList(["event-id-1", "event-id-2"]).build();

// Bookmarks (kind:10003)
const bookmarks = EventBuilder.bookmarks({
  eventIds: ["event-id-1"],
  addresses: ["30023:pubkey:article-slug"],
}).build();

// Follow Set (kind:30000)
const followSet = EventBuilder.followSet("my-friends", [
  "pubkey-alice",
  "pubkey-bob",
]).build();

// Relay Set (kind:30002)
const relaySet = EventBuilder.relaySet("my-relays", [
  "wss://relay1.example.com",
  "wss://relay2.example.com",
]).build();

// Emoji Set (kind:30030)
const emojiSet = EventBuilder.emojiSet("my-emojis", [
  ["sushi", "https://example.com/sushi.png"],
  ["nostr", "https://example.com/nostr.png"],
]).build();

// Filters
FilterBuilder.muteList("pubkey"); // { kinds: [10000], authors: [...] }
FilterBuilder.pinList("pubkey"); // { kinds: [10001], authors: [...] }
FilterBuilder.bookmarks("pubkey"); // { kinds: [10003], authors: [...] }
FilterBuilder.followSets("pubkey"); // { kinds: [30000], authors: [...] }

NIP-65 Relay List Metadata:

typescript
import { EventBuilder, FilterBuilder } from "@ikuradon/tsunagiya/testing";

// Relay List Metadata (kind:10002)
const relayList = EventBuilder.relayList([
  { url: "wss://relay1.example.com" }, // read/write
  { url: "wss://relay2.example.com", marker: "read" }, // read-only
  { url: "wss://relay3.example.com", marker: "write" }, // write-only
]).build();
// → kind: 10002, tags: [["r", url], ["r", url, "read"], ["r", url, "write"]]

// Filter
FilterBuilder.relayList("pubkey"); // { kinds: [10002], authors: ["pubkey"] }

NIP-57 Lightning Zaps:

typescript
const zap = EventBuilder.zapRequest({
  amount: 1000,
  relays: ["wss://relay.example.com"],
  lnurl: "lnurl1...",
  eventId: "target-event",
  recipientPubkey: "recipient-pub",
});
// → kind: 9734

NIP-40 Expiration Timestamp:

typescript
// NIP-40: Expiration Timestamp
const event = EventBuilder.kind1()
  .content("temporary message")
  .withExpiration(Math.floor(Date.now() / 1000) + 3600) // expires in 1 hour
  .build();

NIP-01: Basic Protocol Message Reference

Supported Messages

MessageDirectionNIPSupport
EVENTclient → relayNIP-01✅ Receive, store, OK response
REQclient → relayNIP-01✅ Filtering, EVENT/EOSE response
CLOSEclient → relayNIP-01✅ Subscription cancellation
AUTHclient → relayNIP-42✅ AUTH response
COUNTclient → relayNIP-45✅ Count query
EVENTrelay → clientNIP-01✅ Subscription delivery
OKrelay → clientNIP-01✅ EVENT accept/reject
EOSErelay → clientNIP-01✅ End of stored events
CLOSEDrelay → clientNIP-01✅ Subscription terminated
NOTICErelay → clientNIP-01sendNotice()
AUTHrelay → clientNIP-42✅ Challenge
COUNTrelay → clientNIP-45✅ Count result response

NIP-01: Event Type Store Behavior (formerly NIP-16/NIP-33)

NIP-16 (Event Treatment) and NIP-33 (Parameterized Replaceable Events → Addressable Events) have been merged into NIP-01.

TypeNIP-01 Defined kind RangeStore Behavior
Regular1, 2, 4-44, 1000-9999Added normally
Replaceable0, 3, 10000-19999Old events with same kind+pubkey are deleted before adding
Ephemeral20000-29999Not stored
Addressable (formerly Parameterized Replaceable)30000-39999Old events with same kind+pubkey+d-tag are deleted before adding

Note: Kinds not classified by NIP-01 (45-999, 40000+, etc.) are treated as Regular by this library and stored normally.


Planned NIPs (v0.3.0 and later)

NIPDescriptionTarget VersionOverview
NIP-94File Metadatav0.3.0Template for kind:1063

Unsupported NIPs (No Plans)

NIPDescriptionReason for Non-support
NIP-05DNS IdentifierDNS resolution is outside the scope of a mock library
NIP-07Browser ExtensionBrowser API mocking should be handled by a separate library (※ kind:24133 test events can be generated via EventBuilder.nip07Request())
NIP-19bech32 EncodingEncoding is client-side processing
NIP-46Nostr ConnectRemote signing is outside the scope of a mock relay

MIT License