NIP Support Status
NIP (Nostr Implementation Possibilities) support status for tsunagiya v0.3.0.
Supported NIPs (v0.3.0)
| NIP | Description | Support Status |
|---|---|---|
| NIP-01 | Basic Protocol | EVENT, REQ, CLOSE, EOSE, OK, CLOSED, NOTICE + Event Treatment + Addressable Events |
| NIP-04 | Encrypted DM ⚠️ deprecated (→ NIP-17) | EventBuilder template (migration to NIP-17 recommended) |
| NIP-09 | Event Deletion | kind:5 deletion request handling |
| NIP-10 | Reply Threading | EventBuilder e/p tags |
| NIP-11 | Relay Information | setInfo/getInfo + fetch interception |
| NIP-17 | Private Direct Messages | EventBuilder templates (chatMessage/seal/giftWrap/dmRelayList) |
| NIP-18 | Reposts | EventBuilder templates (repost/genericRepost) |
| NIP-23 | Long-form Content | EventBuilder templates (longFormContent/longFormDraft) |
| NIP-25 | Reactions | EventBuilder withReactions / externalReaction |
| NIP-29 | Relay-based Groups | EventBuilder templates |
| NIP-30 | Custom Emoji | EventBuilder emoji tag |
| NIP-40 | Expiration Timestamp | EventBuilder withExpiration() |
| NIP-42 | AUTH | Challenge/response |
| NIP-45 | COUNT | COUNT message support |
| NIP-50 | Search | Content partial-match search |
| NIP-51 | Lists | EventBuilder templates (muteList/pinList/bookmarks/followSet, etc.) |
| NIP-52 | Calendar Events | EventBuilder templates (all 4 types: Date/Time/Collection/RSVP) |
| NIP-57 | Lightning Zaps | EventBuilder templates |
| NIP-65 | Relay List Metadata | EventBuilder 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:
// 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:
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):
const dm = EventBuilder.dm("recipient-pubkey", "hello").build();
// → kind: 4, content: "mock-encrypted:hello", tags: [["p", "recipient-pubkey"]]NIP-09 deletion request:
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:
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):
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); // trueNIP-01 Addressable Events (formerly NIP-33 — now merged into NIP-01):
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:
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:
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:
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:
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:
const msg = EventBuilder.groupMessage("group-id").content("hello group")
.build();
// → kind: 9, tags: [["h", "group-id"]]NIP-30 Custom emoji:
const event = EventBuilder.kind1()
.emoji("sushi", "https://example.com/sushi.png")
.build();
// → tags: [["emoji", "sushi", "https://example.com/sushi.png"]]NIP-42 AUTH:
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:
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:
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):
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:
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:
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:
const zap = EventBuilder.zapRequest({
amount: 1000,
relays: ["wss://relay.example.com"],
lnurl: "lnurl1...",
eventId: "target-event",
recipientPubkey: "recipient-pub",
});
// → kind: 9734NIP-40 Expiration Timestamp:
// 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
| Message | Direction | NIP | Support |
|---|---|---|---|
EVENT | client → relay | NIP-01 | ✅ Receive, store, OK response |
REQ | client → relay | NIP-01 | ✅ Filtering, EVENT/EOSE response |
CLOSE | client → relay | NIP-01 | ✅ Subscription cancellation |
AUTH | client → relay | NIP-42 | ✅ AUTH response |
COUNT | client → relay | NIP-45 | ✅ Count query |
EVENT | relay → client | NIP-01 | ✅ Subscription delivery |
OK | relay → client | NIP-01 | ✅ EVENT accept/reject |
EOSE | relay → client | NIP-01 | ✅ End of stored events |
CLOSED | relay → client | NIP-01 | ✅ Subscription terminated |
NOTICE | relay → client | NIP-01 | ✅ sendNotice() |
AUTH | relay → client | NIP-42 | ✅ Challenge |
COUNT | relay → client | NIP-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.
| Type | NIP-01 Defined kind Range | Store Behavior |
|---|---|---|
| Regular | 1, 2, 4-44, 1000-9999 | Added normally |
| Replaceable | 0, 3, 10000-19999 | Old events with same kind+pubkey are deleted before adding |
| Ephemeral | 20000-29999 | Not stored |
| Addressable (formerly Parameterized Replaceable) | 30000-39999 | Old 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)
| NIP | Description | Target Version | Overview |
|---|---|---|---|
| NIP-94 | File Metadata | v0.3.0 | Template for kind:1063 |
Unsupported NIPs (No Plans)
| NIP | Description | Reason for Non-support |
|---|---|---|
| NIP-05 | DNS Identifier | DNS resolution is outside the scope of a mock library |
| NIP-07 | Browser Extension | Browser API mocking should be handled by a separate library (※ kind:24133 test events can be generated via EventBuilder.nip07Request()) |
| NIP-19 | bech32 Encoding | Encoding is client-side processing |
| NIP-46 | Nostr Connect | Remote signing is outside the scope of a mock relay |
Related Documentation
- API Reference — API details
- Examples — Usage examples
- Tutorial — Tutorial