clone

nostr relays on demand at <name>.bind.ws: claim one with your key, pay in zaps past the free allowance

main
bindws / src / event.ts
event.ts 3.4K
1// Event validation and helpers, mirroring event.go.2import { verifyEvent, type Event } from "nostr-tools/pure";3import { KIND_PUSH_REGISTRATION } from "./kinds.ts";45export type { Event };67export const isEphemeral = (k: number) => k >= 20000 && k < 30000;8export const isReplaceable = (k: number) => k === 0 || k === 3 || (k >= 10000 && k < 20000);9export const isAddressable = (k: number) => k >= 30000 && k < 40000;10// Only served to the parties involved: NIP-04 DMs and NIP-59 gift wraps per11// NIP-17, NIP-46 signer traffic, which is ephemeral and addressed by p, and12// NIP-9a registrations, which are readable only by their author.13export const isPrivate = (k: number) => k === 4 || k === 1059 || k === 21059 || k === 24133 || k === KIND_PUSH_REGISTRATION;1415export const now = () => Math.floor(Date.now() / 1000);1617const HEX64 = /^[0-9a-f]{64}$/;18const HEX128 = /^[0-9a-f]{128}$/;1920// validate returns "" when the event is well formed and signed, else the21// NIP-01 reason ("invalid: ...").22export function validate(e: unknown): string {23 const ev = e as Event;24 if (!ev || typeof ev !== "object") return "invalid: not an object";25 if (typeof ev.id !== "string" || !HEX64.test(ev.id)) return "invalid: bad id";26 if (typeof ev.pubkey !== "string" || !HEX64.test(ev.pubkey)) return "invalid: bad pubkey";27 if (typeof ev.sig !== "string" || !HEX128.test(ev.sig)) return "invalid: bad sig";28 if (!Number.isInteger(ev.kind) || ev.kind < 0 || ev.kind > 65535) return "invalid: kind out of range";29 if (!Number.isInteger(ev.created_at) || ev.created_at < 0) return "invalid: bad created_at";30 if (typeof ev.content !== "string") return "invalid: content must be a string";31 if (!Array.isArray(ev.tags)) return "invalid: tags must be a list";32 for (const t of ev.tags) {33 if (!Array.isArray(t) || t.length === 0 || t.some((x) => typeof x !== "string")) return "invalid: bad tag";34 }35 if (!verifyEvent(ev)) return "invalid: id or signature does not match content";36 return "";37}3839// canonical re-serializes an event with a fixed key order and nothing else,40// so storage and fan-out use one exact form.41export function canonical(e: Event): string {42 return JSON.stringify({ id: e.id, pubkey: e.pubkey, created_at: e.created_at, kind: e.kind, tags: e.tags, content: e.content, sig: e.sig });43}4445export function tag(e: Event, name: string): string {46 for (const t of e.tags) if (t[0] === name) return t[1] ?? "";47 return "";48}4950export function hasTag(e: Event, name: string): boolean {51 return e.tags.some((t) => t[0] === name);52}5354export function tagValues(e: Event, name: string): string[] {55 return e.tags.filter((t) => t[0] === name && t.length > 1).map((t) => t[1]);56}5758// expiration is the NIP-40 timestamp, or 0.59export function expiration(e: Event): number {60 const n = parseInt(tag(e, "expiration"), 10);61 return Number.isFinite(n) && n > 0 ? n : 0;62}6364// difficulty is the NIP-13 leading zero bits of the id; target is the65// committed difficulty from the nonce tag (0 if absent).66export function difficulty(e: Event): { difficulty: number; target: number } {67 let bits = 0;68 for (const ch of e.id) {69 const n = parseInt(ch, 16);70 if (n === 0) {71 bits += 4;72 continue;73 }74 bits += Math.clz32(n) - 28;75 break;76 }77 let target = 0;78 for (const t of e.tags) {79 if (t[0] === "nonce" && t.length > 2) {80 target = parseInt(t[2], 10) || 0;81 break;82 }83 }84 return { difficulty: bits, target };85}