Skip to content

API Reference

Full reference for all classes, functions, and types in tsunagiya v0.3.0.

Table of Contents


Main Module

typescript
import {
  AuthState,
  classifyEvent,
  createLogger,
  filterEvents,
  generateChallenge,
  getParameterizedId,
  isEphemeral,
  isParameterizedReplaceable,
  isReplaceable,
  Logger,
  matchFilter,
  matchFilters,
  MockPool,
  MockRelay,
} from "@ikuradon/tsunagiya";

MockPool constructor:

typescript
new MockPool();

relay(url, options) method:

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

// with options
const relay = pool.relay("wss://relay.example.com", {
  latency: 100,
  errorRate: 0.1,
});

install() / uninstall():

typescript
pool.install();
// ...
pool.uninstall();

reset():

typescript
pool.reset();

connections:

typescript
console.log(pool.connections); // Map { "wss://relay.example.com" => 2 }

store(event):

typescript
relay.store({
  id: "abc123",
  pubkey: "pubkey1",
  kind: 1,
  content: "hello",
  created_at: 1700000000,
  tags: [],
  sig: "sig1",
}); // → true

// Ephemeral events are not added to the store
relay.store(EventBuilder.kind(20001).build()); // → false

onREQ(handler):

typescript
relay.onREQ((subId, filters) => {
  return [customEvent];
});

// async is also supported
relay.onREQ(async (subId, filters) => {
  return await fetchEvents(filters);
});

onEVENT(handler):

typescript
relay.onEVENT((event) => {
  return ["OK", event.id, true, ""];
});

// to reject
relay.onEVENT((event) => {
  return ["OK", event.id, false, "blocked: spam"];
});

onCOUNT(handler):

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

Error cases:

typescript
relay.refuse();
relay.disconnect(); // code: 1000
relay.disconnect(1006); // abnormal disconnect
relay.disconnectAfter(3000); // disconnect after 3 seconds
relay.close(1006);
relay.sendRaw("not json");
relay.sendNotice("rate-limited");

NIP-42 AUTH:

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

Verification helpers:

typescript
const req = relay.findREQ("sub1");
const count = relay.findCOUNT("count1");
const snap = relay.snapshot();
relay.restore(snap);

Filter functions:

typescript
const matches = matchFilter(event, { kinds: [1], authors: ["pubkey1"] });

const matches = matchFilters(event, [
  { kinds: [1] },
  { kinds: [0], authors: ["pubkey1"] },
]);

const results = filterEvents(allEvents, { kinds: [1], limit: 10 });

Event type functions:

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

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

getParameterizedId(event); // "30023:author-pubkey:my-article"
getParameterizedId(EventBuilder.kind1().build()); // null

MockPool Method Reference

Method/PropertyDescription
relay(url, options?): MockRelayRegister and retrieve a MockRelay
install(): voidReplace globalThis.WebSocket with MockWebSocket
uninstall(): voidRestore the original WebSocket
reset(): voidReset the state of all relays
connections: Map<string, number> (readonly)Current active connections
installed: boolean (readonly)Whether install has been called
[Symbol.dispose](): voidFor using syntax. Calls uninstall() if installed
[Symbol.asyncDispose](): Promise<void>For await using syntax. Same as above

MockRelay Property Reference

PropertyTypeDescription
urlstring (readonly)Relay URL
optionsMockRelayOptions (readonly)Relay options
receivedClientMessage[] (readonly)All received messages
connectionCountnumber (readonly)Number of active connections
errorsReadonlyArray<string> (readonly)Log of error responses that occurred
deletedIdsReadonlySet<string> (readonly)Deleted event IDs (NIP-09)
loggerLogger | null (readonly)Logger instance
authResultsReadonlyArray<{ eventId: string; accepted: boolean; message: string }> (readonly)AUTH authentication result log

Subscription Management

MethodReturn TypeDescription
getSubscriptions()ReadonlyMap<string, ReadonlyArray<NostrFilter>>List of active subscriptions
clearOlderThan(timestamp: number)numberDelete events older than the specified timestamp
broadcast(event: NostrEvent)voidDeliver event to active subscriptions

NIP-11 Relay Information

MethodSignatureDescription
setInfosetInfo(info: Partial<RelayInformation>): voidSet relay information
getInfogetInfo(): RelayInformationGet relay information

Testing Module

typescript
import {
  assertAuthCompleted,
  assertClosed,
  assertEventPublished,
  assertNoErrors,
  assertReceived,
  assertReceivedREQ,
  EventBuilder,
  FilterBuilder,
  restore,
  snapshot,
  startStream,
  streamEvents,
} from "@ikuradon/tsunagiya/testing";

EventBuilder static helpers:

typescript
const events = EventBuilder.bulk(100, { kind: 1 });

const events = EventBuilder.timeline(50, {
  kind: 1,
  interval: 60,
  startTime: 1700000000,
});

const thread = EventBuilder.thread(5);
// thread[0]: root, thread[1]: reply1, ...

const [post, reactions] = EventBuilder.withReactions(5);

EventBuilder extended helpers:

ts
// clone and modify existing event
const original = EventBuilder.kind1().content("hello").build();
const modified = EventBuilder.from(original).content("world").build();

// auto-generate event matching a filter
const filter = { kinds: [1], authors: ["abc123"] };
const event = EventBuilder.matchFilter(filter);

// NIP-17 private DM in one call
const dm = EventBuilder.privateDM({
  recipientPubkey: "recipient-pubkey",
  content: "secret message",
});

// NIP-40 expiring event
const expiring = EventBuilder.kind1()
  .content("temporary")
  .withExpiration(Math.floor(Date.now() / 1000) + 3600)
  .build();

// deterministic bulk generation with seed
const events = EventBuilder.bulk(5, { seed: "test-seed" });

NIP-09 deletion request:

typescript
const deletion = EventBuilder.deletion(["event-id-1", "event-id-2"])
  .pubkey(authorPubkey)
  .build();
// → kind: 5, tags: [["e", "event-id-1"], ["e", "event-id-2"]]

const deletion = EventBuilder.deletionByAddress([
  "30023:pubkey:article-slug",
])
  .pubkey(authorPubkey)
  .build();
// → kind: 5, tags: [["a", "30023:pubkey:article-slug"]]

NIP-specific templates:

typescript
EventBuilder.metadata({ name: "Alice", about: "Nostr user", picture: "..." });
EventBuilder.contacts(["pubkey1", "pubkey2"]);
EventBuilder.dm("recipient-pubkey", "message").build();
EventBuilder.groupMessage("group-id").content("hello group").build();

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

const event = EventBuilder.nip07Request();
// → kind: 24133, content: "mock-nip07-request"

CorruptOptions:

typescript
const broken = EventBuilder.kind1()
  .corrupt({
    id: true, // set id to invalid value
    pubkey: true, // set pubkey to invalid value
    sig: true, // set signature to invalid value
    created_at: true, // set created_at to -1
  })
  .build();

FilterBuilder:

typescript
// generic filters
const authorFilter = FilterBuilder.author("pubkey123");
const kindFilter = FilterBuilder.kind(7);
const sinceFilter = FilterBuilder.since(1700000000);
const tagFilter = FilterBuilder.tagged("e", ["event1"]);

// combine filters
const combined = FilterBuilder.combine(
  { kinds: [1], since: 100 },
  { kinds: [7], since: 200 },
);
// → { kinds: [1, 7], since: 200 }

FilterBuilder.timeline({ limit: 20 });
// → { kinds: [1], limit: 20 }

FilterBuilder.profile("pubkey1");
// → { kinds: [0], authors: ["pubkey1"] }

FilterBuilder.mentions("pubkey1");
// → { kinds: [1], "#p": ["pubkey1"] }

FilterBuilder.reactions("event1");
// → { kinds: [7], "#e": ["event1"] }

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

// NIP-17: Private Direct Messages
FilterBuilder.giftWraps("recipient-pubkey");
// → { kinds: [1059], "#p": ["recipient-pubkey"] }

FilterBuilder.dmRelayList("pubkey1");
// → { kinds: [10050], authors: ["pubkey1"] }

// NIP-18: Reposts
FilterBuilder.reposts("event-id");
// → { kinds: [6], "#e": ["event-id"] }

FilterBuilder.allReposts("event-id");
// → { kinds: [6, 16], "#e": ["event-id"] }

// NIP-23: Long-form Content
FilterBuilder.longFormContent("pubkey1");
// → { kinds: [30023], authors: ["pubkey1"] }

FilterBuilder.longFormByTag("nostr");
// → { kinds: [30023], "#t": ["nostr"] }

// NIP-25: Reactions (address-based)
FilterBuilder.reactionsTo("30023:pubkey1:article-slug");
// → { kinds: [7], "#a": ["30023:pubkey1:article-slug"] }

// NIP-51: Lists
FilterBuilder.muteList("pubkey1");
// → { kinds: [10000], authors: ["pubkey1"] }

FilterBuilder.pinList("pubkey1");
// → { kinds: [10001], authors: ["pubkey1"] }

FilterBuilder.bookmarks("pubkey1");
// → { kinds: [10003], authors: ["pubkey1"] }

FilterBuilder.followSets("pubkey1");
// → { kinds: [30000], authors: ["pubkey1"] }

// NIP-52: Calendar Events
FilterBuilder.calendarDateEvents();
// → { kinds: [31922] }

FilterBuilder.calendarTimeEvents();
// → { kinds: [31923] }

FilterBuilder.calendarEvents();
// → { kinds: [31922, 31923] }

FilterBuilder.calendarCollections();
// → { kinds: [31924] }

FilterBuilder.rsvps("31922:pubkey1:event-slug");
// → { kinds: [31925], "#a": ["31922:pubkey1:event-slug"] }

// NIP-65: Relay List Metadata
FilterBuilder.relayList("pubkey1");
// → { kinds: [10002], authors: ["pubkey1"] }

Assertion functions:

typescript
assertReceivedREQ(relay, { kinds: [1] });
assertEventPublished(relay, "event-id");
assertNoErrors(relay);
assertAuthCompleted(relay);
assertClosed(relay, "sub1");
assertReceived(relay, (messages) => messages.some((m) => m[0] === "REQ"));

Stream functions:

typescript
const handle = streamEvents(relay, events, {
  interval: 100, // send interval (ms)
  jitter: 50, // jitter range (±ms)
});
handle.stop();

const stream = startStream(relay, {
  eventGenerator: () => EventBuilder.random({ kind: 1 }),
  interval: 1000,
  count: 10,
});
stream.stop();

Snapshot functions:

typescript
const snap = snapshot(relay);
// ... operations ...
restore(relay, snap);

EventBuilder Factory Methods

MethodDescription
EventBuilder.kind0()kind:0 (Metadata) builder
EventBuilder.kind1()kind:1 (Short Text Note) builder
EventBuilder.kind3()kind:3 (Contacts) builder
EventBuilder.kind4()kind:4 (Encrypted DM) builder
EventBuilder.kind7()kind:7 (Reaction) builder
EventBuilder.kind(k: number)Any kind
EventBuilder.from(event: NostrEvent)Restore builder from existing event
EventBuilder.matchFilter(filter: NostrFilter)Auto-generate event matching a filter
EventBuilder.privateDM(options: ChatMessageOptions)NIP-17 private DM in one call (kind:1059)

EventBuilder Builder Methods

All builder methods return EventBuilder (chainable).

MethodDescription
content(text: string)Set content
tag(key: string, ...values: string[])Add tag
pubkey(pubkey: string)Set public key
id(id: string)Set ID
createdAt(timestamp: number)Set created_at
sign(privateKey?: string)Generate mock signature (not cryptographically valid)
corrupt(options: CorruptOptions)Replace fields with invalid values
geohash(hash: string)Add geohash tag (NIP-52)
emoji(name: string, url: string)Add emoji tag (NIP-30)
withExpiration(timestamp: number)Add NIP-40 expiration tag
build()Build and return NostrEvent

Logger

Method/PropertyTypeDescription
levelLogLevel (readonly)Current log level
entriesReadonlyArray<LogEntry> (readonly)Accumulated log entries
setLevel(level)voidChange log level
setHandler(handler)voidSet custom handler
clear()voidClear log entries
log(entry, level?)voidRecord a log entry

Type Definitions

NostrEvent:

typescript
interface NostrEvent {
  id: string; // event ID (64-char hex)
  pubkey: string; // public key (64-char hex)
  created_at: number; // UNIX timestamp (seconds)
  kind: number; // event kind
  tags: string[][]; // tag array
  content: string; // content string
  sig: string; // signature (128-char hex)
}

NostrFilter:

typescript
interface NostrFilter {
  ids?: string[]; // ID prefix match
  authors?: string[]; // public key prefix match
  kinds?: number[]; // exact kind match
  since?: number; // created_at lower bound (inclusive)
  until?: number; // created_at upper bound (inclusive)
  limit?: number; // maximum results
  search?: string; // NIP-50: search keyword
  [key: `#${string}`]: string[] | undefined; // tag filter
}

ClientMessage:

typescript
type ClientMessage =
  | ["EVENT", NostrEvent]
  | ["REQ", string, ...NostrFilter[]]
  | ["CLOSE", string]
  | ["AUTH", NostrEvent]
  | ["COUNT", string, ...NostrFilter[]];

RelayMessage:

typescript
type RelayMessage =
  | ["EVENT", string, NostrEvent]
  | ["OK", string, boolean, string]
  | ["EOSE", string]
  | ["CLOSED", string, string]
  | ["NOTICE", string]
  | ["AUTH", string]
  | ["COUNT", string, { count: number }];

MockRelayOptions:

typescript
interface MockRelayOptions {
  latency?: { min: number; max: number } | number;
  errorRate?: number; // 0.0 - 1.0
  disconnectRate?: number; // 0.0 - 1.0
  connectionTimeout?: number; // ms
  connectionDelay?: number; // ms (simulate connection delay)
  requiresAuth?: boolean;
  logging?: boolean | LogHandler;
  verifier?: EventVerifier; // event signature verification
}

LogEntry:

typescript
interface LogEntry {
  timestamp: number; // ms
  relay: string; // relay URL
  direction: "send" | "receive";
  data: unknown;
}

RelaySnapshot:

typescript
interface RelaySnapshot {
  timestamp: number; // save time (ms)
  store: NostrEvent[]; // events in store
  received: ClientMessage[]; // received message log
  deletedIds?: string[]; // deleted event IDs (NIP-09)
  info?: RelayInformation; // relay information (NIP-11)
  metadata?: {
    subscriptionCount: number; // number of subscriptions
    connectionCount: number; // number of connections
    eventCount: number; // number of events
  };
}

EventKind:

typescript
type EventKind =
  | "regular"
  | "replaceable"
  | "ephemeral"
  | "parameterized_replaceable";

COUNTHandler:

typescript
type COUNTHandler = (
  subId: string,
  filters: NostrFilter[],
) => { count: number } | Promise<{ count: number }>;

LogLevel:

typescript
type LogLevel = "silent" | "error" | "info" | "debug" | "trace";

UnsignedEvent:

typescript
interface UnsignedEvent {
  pubkey: string; // public key (64-char hex)
  created_at: number; // UNIX timestamp (seconds)
  kind: number; // event kind
  tags: string[][]; // tag array
  content: string; // content string
}

EventSigner:

typescript
interface EventSigner {
  getPublicKey(): string | Promise<string>;
  signEvent(
    event: UnsignedEvent,
  ): { id: string; sig: string } | Promise<{ id: string; sig: string }>;
}

EventVerifier:

typescript
interface EventVerifier {
  verifyEvent(event: NostrEvent): boolean | Promise<boolean>;
}

MIT License