1// The two gates every client passes. The write gate is what a published2// event must clear whoever sends it: shape, bans, blocked words, the3// relay's state, fuel, the one-group rule. The read gate is what a4// subscription's filters must clear for the socket that opened it: the5// read rule, and the private kinds only their parties may see. Both answer6// "" to let through, or the reason in NIP-01's prefix: form.7import { checkPush } from "./push.ts";8import { graspGate } from "./grasp-state.ts";9import { checkSite } from "./sites.ts";10import type { Relay, ConnState } from "./relay.ts";11import { expiration, hasTag, isPrivate, tagValues, type Event } from "./event.ts";12import type { Filter } from "./filter.ts";13import { KIND_AUTH, KIND_MARMOT_GROUP, KIND_MARMOT_KEY_PACKAGE, KIND_NOSTR_CONNECT } from "./kinds.ts";14import { isGroupState } from "./groups.ts";15import { featureOn } from "./settings.ts";16import { marmotPrincipal, marmotShape } from "./marmot.ts";1718// writeGate is what every write must pass, whoever sends it: shape, bans, the19// relay's state, fuel, and the one-group rule. "" lets it through.20export function writeGate(relay: Relay, e: Event, conn: ConnState | null, t: number): string {21 if (relay.repositoryAccess.blocked) return "restricted: relay operation in progress; retry the event";22 const pushError = checkPush(relay, e, conn);23 if (pushError) return pushError;24 const graspError = graspGate(relay, e, conn?.host ?? "");25 if (graspError) return graspError;26 const siteError = checkSite(e);27 if (siteError) return siteError;28 if (e.kind === KIND_MARMOT_GROUP || e.kind === KIND_MARMOT_KEY_PACKAGE) {29 if (!featureOn(relay.settings.policy, "marmot")) return "restricted: Marmot transport is switched off on this relay";30 const shape = marmotShape(e);31 if (shape) return shape;32 if (e.kind === KIND_MARMOT_GROUP) {33 // A group envelope's transport key is one-use and must not double as34 // the authenticated account identity.35 if (conn?.authed.includes(e.pubkey)) return "invalid: kind 445 must use a fresh ephemeral author";36 if (relay.sql.exec(`SELECT 1 FROM events WHERE kind=? AND pubkey=? LIMIT 1`, KIND_MARMOT_GROUP, e.pubkey).toArray().length) return "invalid: kind 445 ephemeral author was already used";37 const principal = marmotPrincipal(relay, e, conn);38 if (principal && relay.settings.isBanned(principal)) return "blocked: this Marmot account is banned from this relay";39 if (relay.settings.policy.writes !== "open" && !principal) return "auth-required: Marmot group envelopes need an authenticated account allowed to write here";40 }41 }42 const p = relay.settings.policy;43 if (e.kind === KIND_AUTH) return "blocked: kind 22242 is only accepted inside an AUTH message";44 if (p.maxFuture > 0 && e.created_at > t + p.maxFuture) return "invalid: event creation date is too far off from the current time";45 const exp = expiration(e);46 if (exp > 0 && exp <= t) return "invalid: event has already expired";47 if (relay.settings.isBanned(e.pubkey)) return "blocked: this pubkey is banned from this relay";48 if (relay.settings.isEventBanned(e.id)) return "blocked: this event is banned from this relay";49 // Blocked words: the owner and moderators may say anything, nobody else may say these.50 const where = relay.settings.hasBlockedWord(e.content, e.tags);51 if (where) {52 const role = relay.settings.roleOf(e.pubkey);53 if (role !== "owner" && role !== "moderator") return where === "tags" ? "blocked: a tag contains a blocked word" : "blocked: content contains a blocked word";54 }55 // NIP-46 traffic passes the ownership, fuel and write gates: it is56 // ephemeral, never stored, and readable only by its two parties, and57 // letting it through means this relay can carry a remote signer's58 // session, even for the person about to claim it from a phone. Bans59 // and the per-connection rate limit still apply.60 if (e.kind === KIND_NOSTR_CONNECT && featureOn(p, "signer")) return "";61 const h = tagValues(e, "h")[0];62 if (e.kind !== KIND_MARMOT_GROUP && h !== undefined && h !== relay.slug) return "blocked: this relay hosts one group: " + relay.slug;63 if (conn) {64 if (isGroupState(e.kind)) return "blocked: group metadata is written by the relay";65 if (relay.settings.isUnclaimed()) return "restricted: this relay is unclaimed; open https://" + conn.host + "/ to claim it";66 if (relay.settings.leaseExpired(t)) return "restricted: this temporary relay has expired";67 const f = relay.fuelStatus();68 if (f.outOfFuel) {69 return f.enabled ? "restricted: this relay is out of fuel; zap it at https://" + conn.host + "/ to top up" : "restricted: this relay has reached its storage or traffic limit";70 }71 if (hasTag(e, "-") && !conn.authed.includes(e.pubkey)) return "auth-required: this event may only be published by its author";72 }73 return "";74}7576// guestPass lets a stranger through a limited write rule: a kind the owner77// opened to anyone, or, when replies are open, a note or comment that78// answers something a member or the owner wrote here.79export function guestPass(relay: Relay, e: Event): boolean {80 const p = relay.settings.policy;81 if (p.openKinds.includes(e.kind)) return true;82 if (!p.guestReplies || (e.kind !== 1 && e.kind !== 1111)) return false;83 const parents = [...tagValues(e, "e"), ...tagValues(e, "E")].filter((id) => /^[0-9a-f]{64}$/.test(id)).slice(0, 5);84 for (const id of parents) {85 const row = relay.sql.exec<{ pubkey: string }>(`SELECT pubkey FROM events WHERE id=?`, id).toArray()[0];86 if (row && relay.settings.isAllowed(row.pubkey)) return true;87 }88 return false;89}9091// readGate returns a CLOSED reason, or "" plus whether an EOSE "auth"92// hint applies because private kinds were silently filtered out.93export function readGate(relay: Relay, s: ConnState, filters: Filter[]): { reason: string; authHint: boolean } {94 // A subscription to NIP-46 traffic alone may be opened under any read95 // policy, with or without AUTH; the traffic itself is a private kind,96 // delivered only to its parties (canSee).97 const p = relay.settings.policy;98 if (!featureOn(p, "search") && filters.some((f) => f.search !== undefined)) return { reason: "unsupported: search is switched off on this relay", authHint: false };99 if (filters.some((f) => f.kinds?.some((k) => k === KIND_MARMOT_GROUP || k === KIND_MARMOT_KEY_PACKAGE)) && !featureOn(p, "marmot")) return { reason: "unsupported: Marmot transport is switched off on this relay", authHint: false };100 if (featureOn(p, "signer") && filters.length > 0 && filters.every((f) => f.kinds?.length === 1 && f.kinds[0] === KIND_NOSTR_CONNECT)) return { reason: "", authHint: false };101 const authed = s.authed.length > 0;102 const gate = relay.settings.mayRead(s.authed);103 if (gate) return { reason: gate, authHint: false };104 if (authed) return { reason: "", authHint: false };105 let authHint = false;106 for (const f of filters) {107 if (!f.kinds || f.kinds.length === 0) {108 authHint = true;109 continue;110 }111 const priv = f.kinds.filter(isPrivate).length;112 if (priv === f.kinds.length) return { reason: "auth-required: private kinds are only served to their recipients", authHint: false };113 if (priv > 0) authHint = true;114 }115 return { reason: "", authHint };116}