{"id":"695275c47b4757eff2434d65360eab27eecb78a70e2d06c143a4e9a493e2e817","pubkey":"da19f1cd34beca44be74da4b306d9d1dd86b6343cef94ce22c49c6f59816e5bd","created_at":1786099188,"kind":30817,"tags":[["d","gopherkind-documents"],["title","Gopherkind documents"],["k","31436"]],"content":"# Gopherkind documents\n\n`draft` `optional`\n\nKind `31436`, numbered after RFC 1436, defines one text document at one\nabsolute path. A collection of these addressable events under one pubkey can\nbe presented as a gopherhole, but this NIP defines the events rather than any\nparticular bridge or frontend.\n\nThe key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are used as defined in\nRFC 2119.\n\n## Motivation\n\nA gopherhole is a directory on a host. It lasts as long as that host does, at a\nname its author does not own, and once the host is gone a reader who wants the\ndocuments has nobody left to ask. Nothing about the documents themselves\nrequires that arrangement: they are small, they are text, and they are already\naddressed by path.\n\nThis kind puts each document in a signed event instead. A hole becomes a set of\naddressable events under one pubkey, readable from any relay that carries a\ncopy and servable by any bridge that can fetch one, with authorship settled by\nthe signature rather than by whoever currently answers on port 70. The text is\ninline, so nothing else has to be alive at read time either.\n\n## Event format\n\n```json\n{\n  \"kind\": 31436,\n  \"tags\": [\n    [\"d\", \"/phlog/2026-08-02.txt\"],\n    [\"type\", \"0\"],\n    [\"title\", \"First post\"]\n  ],\n  \"content\": \"...\"\n}\n```\n\nA valid event has:\n\n- exactly one `d` tag, whose second element is a valid path;\n- exactly one `type` tag, whose second element is `0` or `1`;\n- zero or one `title` tag. Its second element MUST consist of well-formed\n  Unicode scalar values and MUST NOT contain a control character. A missing\n  title means the path is used as the display name.\n\nFurther elements in those tags and unrecognised tags are ignored. An event\nthat does not meet these requirements is not a gopherkind document and MUST\nNOT be served or listed. In particular, a missing `d` MUST NOT be interpreted\nas the root document.\n\nFor type `0`, `content` is plain UTF-8 text. For type `1`, `content` is a\nkindmap as defined below. Publishers SHOULD keep each event within the size\naccepted by their relays.\n\n## Paths\n\nA valid path is `/`, or one or more segments each preceded by `/`. Each\nsegment:\n\n- is non-empty;\n- is not `.` or `..`;\n- contains no `/`;\n- consists of well-formed Unicode scalar values (no unpaired surrogate);\n- contains no Unicode control character in U+0000-U+001F or\n  U+007F-U+009F.\n\nOnly the root path ends in `/`. Paths are compared as their exact UTF-8 byte\nsequences. Consumers MUST NOT case-fold, Unicode-normalise, percent-decode or\notherwise rewrite the `d` value before comparison. Publishers SHOULD produce\nNFC paths, but consumers do not enforce that recommendation. There is no\nlower-case recommendation because paths are case-sensitive.\n\nThe `d` tag stores the raw path, not a URL-encoded form. Consequently `/a b`\nand `/a%20b` are different documents. A URL frontend encodes each path\nsegment once, so their URL paths are `/a%20b` and `/a%2520b` respectively,\nand decodes each received segment once.\n\nAn RFC 1436 selector is commonly limited to 255 bytes. When the selector also\ncontains an `npub`, publishers SHOULD keep the path at or below 190 UTF-8\nbytes so it remains usable through a gopher bridge.\n\n## Fetching documents\n\nBecause the path is the `d` tag, an entire hole is one filter and one document\nis that filter narrowed by path:\n\n```json\n{\"kinds\": [31436], \"authors\": [\"<pubkey>\"]}\n{\"kinds\": [31436], \"authors\": [\"<pubkey>\"], \"#d\": [\"/about.txt\"]}\n```\n\nThe root document is the one whose `d` is `/`. There is no index event and none\nis needed: a hole's shape is whatever its type `1` documents link to.\n\n## Replacement, expiry and deletion\n\nKind `31436` is addressable. For a given `(pubkey, d)`, consumers first select\nfrom all kind `31436` events they received for that NIP-01 coordinate the event\nwith the greatest `created_at`; on a tie the event with the lowest id wins.\nOnly then is the winning event validated as a gopherkind document. If it is\ninvalid, the path is absent. A consumer MUST NOT reveal an older valid revision\nafter a malformed winner, because a relay may already have discarded that\nolder event under NIP-01.\n\nExpiry is likewise applied only after that winner has been selected and\nvalidated. If the winning event has a NIP-40 `expiration` at or before the\ncurrent time, the path is absent. A consumer MUST NOT fall back to an older\nrevision: relays may already have discarded it, and fallback would make the\nresult depend on the relays queried.\n\nNIP-40 permits a relay to delete an expired winner. If that relay retained an\nolder revision, a later consumer cannot infer that an unseen replacement once\nexisted. Expiration therefore does not provide durable tombstone semantics for\nan addressable coordinate. Publishers that require a path to remain absent\nmust arrange deletion of the coordinate's earlier revisions rather than rely\non a single expiring replacement, and must retain the usual caveat that NIP-09\ndeletion requests can be ignored by relays.\n\nA NIP-09 deletion request for a document SHOULD include:\n\n- an `a` tag containing `31436:<pubkey>:<path>`;\n- a `k` tag containing `31436`;\n- an `e` tag for a known current event MAY also be included.\n\nDeletion remains a request to relays and is not guaranteed.\n\n## Kindmap\n\nType `1` content is a **kindmap**, a host-independent menu source. Records are\nseparated by LF; a CR immediately before an LF is removed. One final empty\nrecord caused by a terminating LF is discarded, while deliberate blank\nrecords are retained. Empty content contains no records.\n\nA record without a tab is information text. A leading literal `i` is removed\nfrom such a record; it is otherwise displayed in full.\n\nA record containing a tab has this form:\n\n```text\n<item-type><display><TAB><link>\n```\n\nThe first tab separates the heading from the link. The first character of the\nheading is the item type and the rest is the display text. The link ends at\nthe next tab, if present; further fields are ignored so a pasted RFC 1436\ngophermap has deterministic behaviour.\n\nThe item type MUST be one printable ASCII character from `!` through `~`.\nUnknown printable item types are retained. A type `i` record or a record with\nan empty link is information text and its link is ignored.\n\nDisplay text MAY contain SGR sequences, that is `ESC` `[`, zero or more\ncharacters from `0123456789;:`, then `m`. These carry colour and style only:\nthey move no cursor, address no device, set no window property and cannot be\nmade actionable, and gopherspace has long used them for menu art. A consumer\nthat cannot render them MUST strip the sequences rather than reject the record.\n\nNo other control character may appear in a display, and no control character of\nany kind may appear in a link. A link is parsed and acted upon, so it never\ncarries an escape; display text is only shown, so it may carry an inert one.\n\nA record whose link contains a control character, or whose display contains one\nthat is not part of an SGR sequence, MUST be rendered as information text using\nonly its heading, with the offending characters replaced by spaces and its SGR\nsequences left intact; it MUST NOT produce a link. Consumers MUST likewise\nneutralise tabs and CR/LF in every field emitted to a line-oriented frontend.\n\nThe defined link forms are:\n\n| Link form | Meaning |\n|---|---|\n| `/path` | A valid path in the same author's document set |\n| `naddr1...` for kind `31436` | A document in another author's set |\n| `npub1...` or `nprofile1...` | Another author's root document |\n| `gopher://host[:port]/T/selector` | An RFC 1436 resource |\n| `http://...`, `https://...` or `gemini://...` | An external URL |\n\nNostr entity links MAY have a `nostr:` prefix. NIP-19 relay hints SHOULD be\nused when resolving an `nprofile` or `naddr`. A same-author path or decoded\nkind `31436` identifier that is not a valid path is an invalid link.\n\nOnly the forms above produce links. In particular, a consumer MUST NOT turn\nan unrecognised scheme such as `javascript:` or `data:` into a clickable or\notherwise actionable target. Authors SHOULD use the target's conventional\ngopher item type: `0` for text, `1` for a menu, and `h` for an external URL.\nA gopher renderer emits an external URL as an `h` item with a `URL:` selector.\n\n## Security considerations\n\nPaths, titles, kindmaps and linked relay hints are attacker-controlled.\nConsumers must apply the validation above before producing gophermap or\ngemtext lines. Networked readers should bound relay hints and reject relay or\nproxy targets that resolve to loopback, private or link-local addresses.\n\nGopher is plaintext and unauthenticated. A bridge MUST NOT accept credentials\nor treat a remote gopher connection as an authenticated user.\n\n## Rationale\n\nEach document is signed and addressed by an author's pubkey and a path rather\nthan by one server. The content remains inline on relays, so a text-only hole\ndoes not depend on an HTTP origin or a separate blob store.\n\nNIP-5A (static websites, kinds `15128`, `35128` and `5128`, formerly the nsite\nkind `34128`) also describes pubkey-owned paths, and is the closest existing\nwork. It differs in three ways that matter here.\n\nContent location. A NIP-5A `path` tag carries the SHA-256 of a Blossom blob,\nso a site is retrievable only where both its relays and its blob servers are.\nKind `31436` carries UTF-8 text inline, so a hole is readable from relays\nalone. That is the whole claim about outliving a host, and a blob dependency\nwould weaken it.\n\nGranularity. NIP-5A publishes a manifest listing many paths, so any edit\nrewrites the manifest. Here one event is one document, addressed by its `d`\ntag, so revising a page replaces that page and nothing else.\n\nTarget. NIP-5A serves HTML over HTTP, with an `index.html` fallback. This kind\ncarries gopher item types and a menu grammar, because the receiving client may\nbe a 1991 gopher client with no HTML parser and no notion of a hostname it did\nnot dial itself.\n\nKind `30023` is for long-form Markdown articles rather than arbitrary\npath-addressed text and menus.\n\nVirtual documents, social views, search, pagination, relay policy, Gemini and\nHTTP URL spaces, and signer-backed account features are application behaviour\nand are deliberately outside this NIP.\n\n## Test vectors\n\nPath identity:\n\n| `d` value | URL path segment | Result |\n|---|---|---|\n| `/a b` | `/a%20b` | valid document |\n| `/a%20b` | `/a%2520b` | different valid document |\n| `/a//b` | n/a | invalid event |\n| `/a/../b` | n/a | invalid event |\n| `/a/` | n/a | invalid event |\n\nKindmap parsing:\n\n| Input record | Parsed result |\n|---|---|\n| `0About<TAB>/about.txt` | type `0`, display `About`, link `/about.txt` |\n| `hello` | information text `hello` |\n| `ihello` | information text `hello` |\n| `1Home<TAB>/<TAB>old.example<TAB>70` | type `1`, display `Home`, link `/` |\n| `1Broken<TAB>` | information text `Broken` |\n\nA type `0` body `\"hello\\n.hidden\\n\"` is rendered over gopher as\n`hello\\r\\n..hidden\\r\\n.\\r\\n`.\n","sig":"57d2ee5ececab0022480e52c35e9925cfa340b9a6d55d844557973006c962464ff66ed3b7fd36d594f7ddb29442a82068d40e40c46a4d4bd374e793d1f25eff1"}