clone

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

main
bindws / src / nip11.ts
nip11.ts 7.7K
1// The NIP-11 information document: what the relay says about itself to a2// client that asks with accept: application/nostr+json. The rules, the3// limits, the policy links and lists the owner set, retention, fuel, the4// lease, succession, and the views, from the settings and the relay's state.5import type { Relay } from "./relay.ts";6import { now } from "./event.ts";7import { SUCCESSION_WARN_DAYS, featureOn, FEATURE_NAMES, type Feature } from "./settings.ts";8import { SITE_KINDS } from "./sites.ts";9import { MiB, GiB } from "./git-limits.ts";10import { KIND_REPO } from "./kinds.ts";11import { nip11Views } from "./views.ts";1213// The less obvious numbers: 43 is added once the relay has an identity; 6214// is request-to-vanish (store.vanish); 67 is the EOSE hint array at the end15// of the subscription handler; 70 is the "-" tag check in the write gate16// (gates.ts); 66 is the discovery record the relay signs about itself17// (publishDiscovery); 77 is negentropy sync (handleSync).18export const SUPPORTED_NIPS = [1, 5, 9, 11, 13, 17, 29, 40, 42, 45, 46, 50, 56, 59, 62, 66, 67, 70, 77, 86, 98];19// The numbers a switched-off feature takes with it (settings.ts, features).20export const FEATURE_NIPS: Record<Feature, number[]> = { search: [50], sync: [77], count: [45], discovery: [66], names: [5], files: [], pages: [], signer: [46], sites: [], marmot: [], grasp: [], grasp02: [], grasp03: [], grasp05: [], grasp06: [], push: [] };2122// Lettered identifiers retain their protocol spelling, never decimal values.23// New draft capabilities belong here only alongside their implementation.24export const LETTERED_NIPS: { id: string; enabled: (relay: Relay) => boolean }[] = [25 { id: "AD", enabled: () => true },26 { id: "5A", enabled: (relay) => featureOn(relay.settings.policy, "sites") },27 { id: "9a", enabled: (relay) => featureOn(relay.settings.policy, "push") },28];29export type NipIdentifier = number | string;3031// supportedNips is the list as it stands with the features that are on.32export function supportedNips(relay: Relay): NipIdentifier[] {33 const off = new Set(FEATURE_NAMES.filter((f) => !featureOn(relay.settings.policy, f)).flatMap((f) => FEATURE_NIPS[f]));34 const numeric = SUPPORTED_NIPS.filter((n) => !off.has(n));35 const mixed = relay.settings.policy.letteredNips || featureOn(relay.settings.policy, "push");36 return mixed ? [...numeric, ...LETTERED_NIPS.filter((n) => n.enabled(relay)).map((n) => n.id)] : numeric;37}38export const SOFTWARE = "https://bind.ws";39export const VERSION = "0.1.0";4041const capacity = (n: number) => n % GiB === 0 ? `${n / GiB} GiB` : n % MiB === 0 ? `${n / MiB} MiB` : `${n} bytes`;4243export function nip11(relay: Relay, host: string) {44 const p = relay.settings.policy;45 const doc: Record<string, unknown> = {46 name: p.name || relay.slug,47 description: p.description,48 supported_nips: supportedNips(relay),49 software: SOFTWARE,50 version: VERSION,51 limitation: {52 max_message_length: relay.maxMessage(),53 max_subscriptions: p.maxSubs,54 max_limit: p.maxLimit,55 default_limit: p.maxLimit,56 max_subid_length: 64,57 // True whenever a fresh socket cannot REQ: reads for the authenticated or for members.58 auth_required: p.reads !== "open",59 payment_required: false,60 restricted_writes: p.writes !== "open" || relay.settings.isUnclaimed(),61 created_at_upper_limit: p.maxFuture || undefined,62 min_pow_difficulty: p.minPow || undefined,63 },64 };65 if (p.icon) doc.icon = p.icon;66 if (p.banner) doc.banner = p.banner;67 if (p.joinTerms && host) doc.terms_of_service = relay.webURL(host) + "/terms";68 if (p.postingPolicy) doc.posting_policy = p.postingPolicy;69 if (p.privacyPolicy) doc.privacy_policy = p.privacyPolicy;70 if (p.tags.length) doc.tags = p.tags;71 if (p.languageTags.length) doc.language_tags = p.languageTags;72 if (p.relayCountries.length) doc.relay_countries = p.relayCountries;73 if (p.contact) doc.contact = p.contact;74 const retention = relay.settings.listRetention().map((r) => (r.kind === null ? { time: r.days * 86400 } : { kinds: [r.kind], time: r.days * 86400 }));75 if (retention.length) doc.retention = retention;76 if (relay.fuel.cfg.lightningAddress && relay.fuel.cfg.servicePubkey && host) {77 const f = relay.fuel.status(now(), relay.eventBytes(), relay.mediaBytes());78 (doc.limitation as Record<string, unknown>).payment_required = f.outOfFuel;79 doc.payments_url = "https://" + host + "/";80 }81 if (p.owner) doc.pubkey = p.owner;82 if (p.succession && relay.succession.warn) doc.succession_pending = new Date((relay.succession.warn.since + SUCCESSION_WARN_DAYS * 86400) * 1000).toISOString().slice(0, 10);83 if (relay.settings.isLeased() && p.lease) doc.lease = { expires_at: p.lease.until, holder: p.lease.holder || undefined, claim_url: host ? "https://" + host + "/" : undefined };84 if (featureOn(p, "grasp") && p.reads === "open") {85 // The bounded sync previews remain unadvertised pending interoperability audit.86 doc.supported_grasps = ["GRASP-01", ...(featureOn(p, "grasp06") ? ["GRASP-06"] : [])];87 const access = {88 open: "Anyone may announce a repository.",89 allowlist: "Allowlist hosting: only the relay owner and members may announce repositories.",90 owner: "Curated hosting: only the relay owner may announce repositories.",91 wot: "Curated hosting: the relay owner, members and people they follow may announce repositories.",92 };93 const eligibility = relay.settings.isUnclaimed() ? "Repository hosting is unavailable until this relay is claimed."94 : relay.settings.leaseExpired(now()) ? "Repository hosting is unavailable because this relay's lease has expired."95 : !relay.settings.kindAllowed(KIND_REPO) ? "Curated hosting: kind 30617 is restricted; only the relay owner's repository announcements pass the kind rule."96 : p.writes !== "open" && p.openKinds.includes(KIND_REPO) ? "Anyone may announce a repository through the guest exception for kind 30617."97 : access[p.writes];98 const guests = p.writes !== "open" && p.openKinds.length ? ` Guest write exceptions apply to kinds ${p.openKinds.join(", ")}.` : "";99 const listing = featureOn(p, "grasp05") ? "Archive preview accepts repository announcements without naming this service in clone and relays." : "Repository announcements name this service in clone and relays.";100 const git = p.git;101 doc.git_limits = { ...git };102 doc.repo_acceptance_criteria = `${eligibility} ${listing} State and collaboration events follow the relay write rule (${p.writes}) and kind rules; bans, proof of work and fuel apply.${guests} At most ${git.maxRepositories} repositories, ${capacity(git.maxPackBytes)} per incoming pack, ${capacity(git.maxObjectBytes)} per new object, and ${capacity(git.maxTransactionRawBytes)} decoded per write operation. Each repository may retain ${git.maxObjects} objects, ${capacity(git.maxRawBytes)} raw content, ${capacity(git.maxCompressedBytes)} compressed objects and ${capacity(git.maxMetadataBytes)} metadata. Total Git storage is limited to ${capacity(git.maxRelayBytes)} and shares the relay SQLite allowance. Limits are owner-configurable; storage and fuel charges still apply.`;103 if (p.writes !== "open" || p.blockedWords.length) doc.curation = "The relay's configured write rule and blocked topics apply to Nostr repository and collaboration events.";104 }105 if (featureOn(p, "sites")) doc.nsites = {106 host: relay.domain,107 kinds: SITE_KINDS,108 root: "https://<npub>." + relay.domain,109 named: "https://<pubkeyB36><dTag>." + relay.domain,110 snapshot: "https://v<snapshotIdB36>." + relay.domain,111 };112 if (host) doc.self_url = relay.relayURL(host);113 if (relay.identity.pubkey) {114 doc.self = relay.identity.pubkey;115 doc.supported_nips = [...supportedNips(relay), 43];116 doc.views = nip11Views(relay);117 }118 return doc;119}