clone

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

main
bindws / src / marmot.ts
marmot.ts 3.8K
1// Marmot's relay-facing events: exact transport shapes that a relay can2// check without understanding MLS, and the admission identity for opaque3// group envelopes.4import type { ConnState, Relay } from "./relay.ts";5import { KIND_MARMOT_GROUP, KIND_MARMOT_KEY_PACKAGE } from "./kinds.ts";6import { featureOn } from "./settings.ts";7import type { Event } from "./event.ts";89const HEX64 = /^[0-9a-f]{64}$/;10const ID = /^0x[0-9a-f]{4}$/;11const LIST_TAGS = ["mls_ciphersuite", "mls_extensions", "mls_proposals", "app_components"];1213const singleton = (e: Event, name: string, test: (v: string) => boolean): boolean => {14 const tags = e.tags.filter((t) => t[0] === name);15 return tags.length === 1 && tags[0].length === 2 && test(tags[0][1]);16};1718const idList = (e: Event, name: string): boolean => {19 const tags = e.tags.filter((t) => t[0] === name);20 if (tags.length !== 1 || tags[0].length < 2) return false;21 const values = tags[0].slice(1);22 return values.every((v) => ID.test(v)) && new Set(values).size === values.length;23};2425const base64 = (value: string): boolean => {26 if (value === "" || value.length % 4 !== 0 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) return false;27 try {28 return atob(value).length > 0;29 } catch {30 return false;31 }32};3334const decodedBytes = (value: string): number => {35 try {36 return atob(value).length;37 } catch {38 return 0;39 }40};4142// marmotShape checks only the signed envelope and transport tags. MLS bytes43// stay opaque here, so clients remain responsible for decoding and verifying44// the KeyPackage or group message inside the base64 content.45export const marmotShape = (e: Event): string => {46 if (e.kind === KIND_MARMOT_GROUP) {47 if (!singleton(e, "h", (v) => HEX64.test(v))) return "invalid: kind 445 needs one lowercase 32-byte h tag";48 if (!base64(e.content) || decodedBytes(e.content) < 28) return "invalid: kind 445 content must be base64 containing a nonce and authentication tag";49 if (e.tags.some((t) => t[0] !== "h" && !(t[0] === "expiration" && t.length === 2 && /^\d+$/.test(t[1])))) return "invalid: kind 445 has an unsupported tag";50 if (e.tags.filter((t) => t[0] === "expiration").length > 1) return "invalid: kind 445 has repeated expiration";51 return "";52 }53 if (e.kind !== KIND_MARMOT_KEY_PACKAGE) return "";54 if (!singleton(e, "d", (v) => HEX64.test(v)) || !singleton(e, "mls_protocol_version", (v) => v === "1.0") || !singleton(e, "i", (v) => /^[0-9a-f]+$/.test(v))) return "invalid: kind 30443 has a malformed singleton tag";55 if (LIST_TAGS.some((name) => !idList(e, name))) return "invalid: kind 30443 has a malformed id-list tag";56 if (!e.tags.some((t) => t[0] === "app_components" && t.slice(1).includes("0x8009"))) return "invalid: kind 30443 must advertise account identity proof";57 if (!base64(e.content)) return "invalid: kind 30443 content must be padded base64";58 const allowed = new Set(["d", "mls_protocol_version", "i", ...LIST_TAGS]);59 if (e.tags.some((t) => !allowed.has(t[0]))) return "invalid: kind 30443 has an unsupported tag";60 return "";61};6263// marmotPrincipal returns the authenticated account whose policy and member64// limits apply to an opaque group envelope. An open relay may accept one65// without a principal, because the ephemeral author cannot identify an account.66export const marmotPrincipal = (relay: Relay, e: Event, conn: ConnState | null): string => {67 if (e.kind !== KIND_MARMOT_GROUP) return e.pubkey;68 if (relay.settings.policy.writes === "open") return conn?.authed[0] ?? e.pubkey;69 return conn?.authed.find((pk) => relay.settings.mayWrite(pk) === "") ?? "";70};7172export const isMarmotEvent = (e: Event): boolean => e.kind === KIND_MARMOT_GROUP || e.kind === KIND_MARMOT_KEY_PACKAGE;7374export const marmotEnabled = (relay: Relay): boolean => featureOn(relay.settings.policy, "marmot");