- commit
- 05788ff3b9677949578c0ec39971aa7c2a85dbea
- parent
- 3e88db02da1f060d25a747a6493533429c3b0249
- author
- Dami <dami.etoile@gmail.com>
| M | README.md | 2 | +- |
| M | docs/ | 2 | +- |
| M | docs/ | 1 | + |
| M | docs/ | 19 | +++++++++++++++--- |
| M | relay-templates/ | 5 | ++++- |
| A | relay-templates/ | 24 | +++++++++++++++++++++++ |
| M | relay-templates/ | 3 | ++- |
| M | src/ | 31 | +++++++++++++++++++++++++++++- |
| M | test/ | 15 | ++++++++++++--- |
| M | test/ | 2 | +- |
@@ -21,7 +21,7 @@ That answers with a relay at a memorable name, open to anyone for 14 days. Claim
2121 - **Encrypted group transport.** Opt-in Marmot KeyPackages and opaque MLS group messages, with account admission and storage caps for ephemeral authors. Clients manage encryption and group membership.2222 - **A Git host.** Opt-in GRASP-01 Smart HTTP, backed by [ntig](https://github.com/FelineStateMachine/ntig). Signed NIP-34 state authorizes branch and tag changes; events wait for their Git objects before publication. Hosting has [explicit storage and transaction limits](docs/22-grasp-01-git-hosting.md#git-http).2323 - **A file host.** Blossom and NIP-96 on the same bucket, with mirroring, pre-flight checks and file reports.24-- **A console.** Nine tabs on the relay's own page, signed by a browser extension or a remote signer app on a phone. Presets set up an outbox, an inbox, a private relay, a chat, a search replica, a media host or a quiet relay with every costly feature off, in one click. The template collection also includes static sites, Marmot transport and Git repositories.24+- **A console.** Nine tabs on the relay's own page, signed by a browser extension or a remote signer app on a phone. Presets set up an outbox, an inbox, a private relay, a chat, a search replica, a media host or a quiet relay with every costly feature off, in one click. The template collection also includes static sites, public or member-published Marmot transport and Git repositories.2525 - **A file.** Everything the console sets, as one document with a schema: check it offline, see what applying would change, apply it, pull it back. Keep your relay in your repository. The presets are the same files, in `relay-templates/`.2626 - **Jobs.** Pull another relay in, keep a standing mirror, fetch your own history from your relay list, rebroadcast to other relays, and dump everything to a file on a schedule.2727 - **Your own domain.** `wss://relay.example.com` onto your relay with one CNAME, or a custom hostname targeting one of your static sites.@@ -69,7 +69,7 @@ The per-address limit is what stops a client from opening many sockets to multip
6969 7070 **Features** are the doors and the costs an owner may do without. Search indexes the prose kinds by default (notes, threads, comments, highlights, articles, wiki pages); *full* indexes every public kind with content, and *off* refuses search filters. A change applies to events from then on; the index is not rebuilt. Sync (NIP-77) reads the whole matching set per reconciliation, counts (NIP-45) keep sketches, the discovery record (NIP-66) is what crawlers find, names (NIP-05) publish members under the domain, files are the Blossom and NIP-96 doors, pages are notes and articles as web pages with the feed, and signer traffic is NIP-46 carried for anyone. What is off leaves `supported_nips`, answers 404 at its door, and is refused at the socket with an `unsupported:` reason. The bridge, groups, the roster, reports and management are not features: the console runs on them.7171 72-Three features add bounded services beside the relay feed. **Sites** is on by default and **Mirror site files** is on by default inside it: members can publish NIP-5A manifests, and the relay can verify and retain their files or fetch a verified cache miss from the manifest's sources. A site hostname is isolated from the relay's event and management doors. The [NIP-5A static websites](20-nip-5a-static-websites.md) guide describes the origins and custom-domain choices. **Marmot** is off by default and carries signed KeyPackages and opaque MLS group envelopes, with the ordinary write, read, ban, rate and fuel rules still applied. **GRASP Git hosting** is off by default, needs open reads, and admits repository announcements, state and related NIP-34 events before exposing Git Smart HTTP; the [GRASP-01 Git hosting](22-grasp-01-git-hosting.md) guide lists its bounded repository and object rules. The `marmot` and `grasp` templates select these features for you.72+Three features add bounded services beside the relay feed. **Sites** is on by default and **Mirror site files** is on by default inside it: members can publish NIP-5A manifests, and the relay can verify and retain their files or fetch a verified cache miss from the manifest's sources. A site hostname is isolated from the relay's event and management doors. The [NIP-5A static websites](20-nip-5a-static-websites.md) guide describes the origins and custom-domain choices. **Marmot** is off by default and carries signed KeyPackages and opaque MLS group envelopes, with the ordinary write, read, ban, rate and fuel rules still applied. **GRASP Git hosting** is off by default, needs open reads, and admits repository announcements, state and related NIP-34 events before exposing Git Smart HTTP; the [GRASP-01 Git hosting](22-grasp-01-git-hosting.md) guide lists its bounded repository and object rules. The `marmot`, `marmot-members` and `grasp` templates select these features for you. **Marmot** accepts public writes; **Marmot members** admits member publishers and requires account authentication for group envelopes. Both keep encrypted messages publicly readable, with recipient checks on gift wraps.7373 7474 ### Presets7575 @@ -50,6 +50,7 @@ Names are cheap, so a relay does not have to do everything. On the Rules tab, **
5050 | site | members | anyone | every kind, forever; site hosting, mirroring and files enabled |5151 | marmot | anyone | anyone | KeyPackages, encrypted group envelopes, private welcomes and relay lists; Marmot enabled |5252 | grasp | members publish events | anyone | every kind, forever; GRASP Git hosting enabled |53+| marmot-members | members; group writes authenticate | anyone | KeyPackages, encrypted group envelopes, private welcomes and relay lists; Marmot enabled |5354 5455 Each preset is a file in the repository's `relay-templates/` folder: a relay configuration with the rules sections only, which is also what a file of your own looks like ([Scripts and agents](13-scripts-and-agents.md#the-configuration-file)). A preset that names features sets them too.5556 @@ -13,11 +13,24 @@ encrypted group message and routes it with one `h` tag containing the random
1313 32-byte group id. The relay checks the signed envelope and leaves MLS1414 verification to clients.1515 16-The transport is off on a fresh relay. An owner enables it with the Marmot17-template or `setpolicy` with `{"features":{"marmot":true}}`. The template18-also allows kinds `10002`, `10050`, `1059`, `30443` and `445`, which are the16+The transport is off on a fresh relay. You enable it with **Marmot**,17+**Marmot members** or `setpolicy` with `{"features":{"marmot":true}}`.18+Both templates allow kinds `10002`, `10050`, `1059`, `30443` and `445`, which are the1919 account relay list, inbox list, gift wrap, KeyPackage and group message paths.2020 21+Choose **Marmot** for public opaque transport: anyone can publish, including22+group envelopes without account authentication. Choose **Marmot members**23+when you admit the publishers. Add their account npubs on the People tab;24+their clients authenticate with NIP-42 before publishing group envelopes.25+Both templates keep encrypted messages publicly readable and retain the26+recipient checks on gift wraps. Relay membership controls who writes; your27+clients control who belongs to an MLS group and holds its decryption keys.28+29+**Marmot members** charges each envelope against its authenticated account's30+member limits. Both templates use the relay owner's existing fuel allowance31+and balance for storage and metered work; neither sets a new price or cap.32+Other feature settings stay as they are.33+2134 Kind `445` uses a fresh ephemeral author, so that author is not a membership2235 identity. An open relay may accept it anonymously. A relay with an allowlist,2336 web of trust or owner-only write rule requires the socket to authenticate an@@ -1,9 +1,12 @@
1+// Public opaque transport: anyone can publish KeyPackages and group envelopes.2+// Ephemeral envelope authors need no account authentication on open writes.3+// Clients hold the MLS keys; gift wraps keep their recipient checks.14 {25 "$schema": "https://bind.ws/relay-config.schema.json",36 "format": "bind.ws/relay-config/2",47 "template": {58 "title": "Marmot",6- "about": "A relay for Marmot KeyPackages, opaque group messages and private welcomes. Group envelopes stay subject to the relay's write limits."9+ "about": "Public Marmot transport. Anyone publishes KeyPackages and opaque group messages; gift wraps keep their recipient checks. Relay write limits still apply."710 },811 "policy": {912 "writes": "open",@@ -0,0 +1,24 @@
1+// Accountable Marmot transport: members publish KeyPackages and authenticate2+// their sockets before publishing envelopes signed by fresh ephemeral keys.3+// The admitted account's limits apply; the relay owner's fuel pays for storage.4+// Reads stay open for encrypted transport, with recipient checks on gift wraps.5+// Relay membership admits publishers; clients manage MLS group membership.6+{7+ "$schema": "https://bind.ws/relay-config.schema.json",8+ "format": "bind.ws/relay-config/2",9+ "template": {10+ "title": "Marmot members",11+ "about": "Members publish Marmot KeyPackages and authenticate group message writes. Encrypted transport stays publicly readable; member limits apply and gift wraps keep their recipient checks."12+ },13+ "policy": {14+ "writes": "allowlist",15+ "reads": "open",16+ "directoryPublic": true,17+ "features": { "marmot": true }18+ },19+ "kinds": {20+ "allow": [10002, 10050, 1059, 30443, 445],21+ "block": []22+ },23+ "retention": []24+}@@ -18,8 +18,9 @@ copy as the start of your own.
1818 | [dm](09-dm.jsonc) | it is a drop box for private messages: anyone leaves a gift wrap, only members read. |1919 | [quiet](10-quiet.jsonc) | it should cost nothing and announce nothing: members only, every optional feature off, switched on one at a time as needed. |2020 | [site](11-site.jsonc) | it hosts NIP-5A static websites: members publish manifests and files, with mirroring on. |21-| [marmot](12-marmot.jsonc) | it carries Marmot KeyPackages, opaque group messages and private welcomes. |21+| [marmot](12-marmot.jsonc) | anyone can publish Marmot KeyPackages and opaque group messages; gift wraps keep their recipient checks. |2222 | [grasp](13-grasp.jsonc) | it hosts GRASP Git repositories: admitted members publish repository state and related events, with reads open. |23+| [marmot-members](14-marmot-members.jsonc) | members publish Marmot transport, with authenticated group writes and member limits; encrypted messages stay publicly readable. |2324 2425 Each file says more in its comment: what the template does, which Haven relay2526 it mirrors, and why its kinds are what they are.@@ -292,7 +292,7 @@ export const TEMPLATES: { name: string; document: unknown }[] = [
292292 "format": "bind.ws/relay-config/2",293293 "template": {294294 "title": "Marmot",295- "about": "A relay for Marmot KeyPackages, opaque group messages and private welcomes. Group envelopes stay subject to the relay's write limits."295+ "about": "Public Marmot transport. Anyone publishes KeyPackages and opaque group messages; gift wraps keep their recipient checks. Relay write limits still apply."296296 },297297 "policy": {298298 "writes": "open",@@ -337,5 +337,34 @@ export const TEMPLATES: { name: string; document: unknown }[] = [
337337 },338338 "retention": []339339 }340+ },341+ {342+ "name": "marmot-members",343+ "document": {344+ "format": "bind.ws/relay-config/2",345+ "template": {346+ "title": "Marmot members",347+ "about": "Members publish Marmot KeyPackages and authenticate group message writes. Encrypted transport stays publicly readable; member limits apply and gift wraps keep their recipient checks."348+ },349+ "policy": {350+ "writes": "allowlist",351+ "reads": "open",352+ "directoryPublic": true,353+ "features": {354+ "marmot": true355+ }356+ },357+ "kinds": {358+ "allow": [359+ 10002,360+ 10050,361+ 1059,362+ 30443,363+ 445364+ ],365+ "block": []366+ },367+ "retention": []368+ }340369 }341370 ];@@ -45,19 +45,28 @@ describe("marmot", () => {
4545 await new Promise((resolve) => setTimeout(resolve, 100));4646 });4747 48- it("requires an authenticated admitted account for group envelopes on limited relays", async () => {48+ it("the members template requires admitted account authentication while encrypted messages stay publicly readable", async () => {4949 const host = "marmot-auth.bind.ws";5050 const owner = generateSecretKey();5151 const member = generateSecretKey();5252 const ephemeral = generateSecretKey();5353 await rpc(host, owner, "claim");5454 await rpc(host, owner, "setmember", pk(member));55- await rpc(host, owner, "setpolicy", { features: { marmot: true }, writes: "allowlist" });55+ await rpc(host, owner, "applypreset", "marmot-members");5656 const c = await WS.connect(host);5757 const group = () => ev(ephemeral, KIND_MARMOT_GROUP, GROUP_CONTENT, [["h", "cd".repeat(32)]]);5858 expect((await c.ok(group())).msg).toMatch(/^auth-required:/);5959 await c.auth(member, host);6060 expect((await c.ok(group())).ok).toBe(true);61+ const stranger = await WS.connect(host);62+ await stranger.auth(generateSecretKey(), host);63+ const denied = ev(generateSecretKey(), KIND_MARMOT_GROUP, GROUP_CONTENT, [["h", "ee".repeat(32)]]);64+ expect((await stranger.ok(denied)).msg).toMatch(/^auth-required:/);65+ stranger.ws.close();66+ const reader = await WS.connect(host);67+ const messages = await reader.query({ kinds: [KIND_MARMOT_GROUP], "#h": ["cd".repeat(32)] });68+ expect(messages.events.map((e) => e.pubkey)).toEqual([pk(ephemeral)]);69+ reader.ws.close();6170 const reused = ev(ephemeral, KIND_MARMOT_GROUP, GROUP_CONTENT, [["h", "de".repeat(32)]]);6271 expect((await c.ok(reused)).msg).toMatch(/fresh ephemeral author|already used/);6372 c.ws.close();@@ -70,7 +79,7 @@ describe("marmot", () => {
7079 const member = generateSecretKey();7180 await rpc(host, owner, "claim");7281 await rpc(host, owner, "setmember", pk(member), { maxBytes: 700 });73- await rpc(host, owner, "setpolicy", { features: { marmot: true }, writes: "allowlist" });82+ await rpc(host, owner, "applypreset", "marmot-members");7483 const c = await WS.connect(host);7584 await c.auth(member, host);7685 const first = ev(generateSecretKey(), KIND_MARMOT_GROUP, GROUP_CONTENT, [["h", "ef".repeat(32)]]);@@ -11,7 +11,7 @@ describe("presets", () => {
1111 const owner = generateSecretKey();1212 await rpc(host, owner, "claim");1313 const list = (await rpc(host, owner, "listpresets")).result;14- expect(list.map((p: any) => p.name)).toEqual(["default", "outbox", "inbox", "private", "chat", "media", "search", "articles", "dm", "quiet", "site", "marmot", "grasp"]);14+ expect(list.map((p: any) => p.name)).toEqual(["default", "outbox", "inbox", "private", "chat", "media", "search", "articles", "dm", "quiet", "site", "marmot", "grasp", "marmot-members"]);1515 for (const p of list) expect(p.about.length).toBeGreaterThan(10);1616 expect(list.find((p: any) => p.name === "search").source).toBe("required");1717 expect(list.find((p: any) => p.name === "articles").source).toBe("optional");