diff --git a/NIP-YY.md b/NIP-YY.md new file mode 100644 index 0000000000..627dbdd12a --- /dev/null +++ b/NIP-YY.md @@ -0,0 +1,264 @@ +# NIP-YY + +## Nostr Web Pages + +`draft` `optional` + +This NIP defines a set of event kinds for hosting static websites on Nostr, enabling censorship-resistant web publishing through the Nostr protocol. + +## Abstract + +Nostr Web Pages (NWP) allows publishing static websites as Nostr events. Web assets (HTML, CSS, JavaScript, fonts, etc.) are stored as regular events (kind 1125), page manifests (1126) define routes, the entrypoint (11126) provides the current entry to a site, and the site index (31126) maps routes using addressable events. This enables decentralized, verifiable, and censorship-resistant websites. + +## Motivation + +Traditional web hosting relies on centralized servers that can be censored, taken down, or compromised. By publishing websites as Nostr events: + +- Content becomes censorship-resistant through relay replication +- Sites are independently verifiable via cryptographic signatures +- No central origin servers are required +- Publishing works with existing Nostr infrastructure + +## Event Kinds + +This NIP defines the following event kinds: + +| Kind | Description | Type | +| ------- | ------------- | ----------- | +| `1125` | Asset | Regular | +| `1126` | Page Manifest | Regular | +| `31126` | Site Index | Addressable | +| `11126` | Entrypoint | Replaceable | + +### Regular Assets (1125) + +Content-addressed assets for web pages. All web assets (HTML, CSS, JavaScript, fonts, etc.) use kind `1125` with MIME type specified in the `m` tag. + +**Required tags:** + +- `m` - MIME type (e.g., `text/html`, `text/css`, `text/javascript`, `application/wasm`, `font/woff2`, 'image/png', etc.) +- `x` - Hex-encoded SHA-256 hash of the `content` field (for content deduplication) + +**Optional tags:** + +- `alt` - Alternative text or description for the asset + +**Example HTML asset:** + +```json +{ + "kind": 1125, + "pubkey": "", + "created_at": 1234567890, + "tags": [ + ["m", "text/html"], + ["x", "a1b2c3..."], + ["alt", "Home Page"] + ], + "content": "...", + "id": "", + "sig": "" +} +``` + +### Page Manifest (1126) + +Regular event that links assets for a specific page. Each page version is a separate event. + +**Required tags:** + +- `e` - Asset event IDs (kind 1125): `["e", "", ""]` + - Assets are identified by their event ID; MIME type is specified in the asset's `m` tag + +**Optional tags:** + +- `title` - Page title +- `description` - Page description + +**Example:** + +```json +{ + "kind": 1126, + "pubkey": "", + "created_at": 1234567890, + "tags": [ + ["e", "", "wss://relay.example.com"], + ["e", "", "wss://relay.example.com"], + ["e", "", "wss://relay.example.com"], + ["e", "", "wss://relay.example.com"], + ["e", "", "wss://relay.example.com"], + ["title", "Home"], + ["description", "Welcome to my Nostr Web site"] + ], + "content": "", + "id": "", + "sig": "" +} +``` + +### Site Index (31126) + +Addressable event that maps routes to their current page manifest IDs. The `d` tag is used for versioning. + +**Required tags:** + +- `d` - Version identifier (e.g., `"v1.2.3"`, `"v1.0.0"`, `"main"`, `"staging"`) +- `rt` - Route mapping: `["rt", "", "", "", ""]` + - One `rt` tag per route + - Route path (e.g., `"/"`, `"/about"`, `"/blog/post-1"`) + - Page manifest event ID (kind 1126) + - Optional relay URL hint + - Optional marker: `"default"` or `"404"` to indicate special routes + - If no route is marked as `"default"`, clients SHOULD use the `"/"` route as default + - If no route is marked as `"404"`, clients SHOULD display a generic error page + +**Optional tags:** + +- `title` - Site title +- `description` - Site description + +**Example:** + +```json +{ + "kind": 31126, + "pubkey": "", + "created_at": 1234567890, + "tags": [ + ["d", "v1.2.3"], + [ + "rt", + "/", + "", + "wss://relay1.example.com", + "default" + ], + ["rt", "/about", "", "wss://relay1.example.com"], + [ + "rt", + "/blog/post-1", + "", + "wss://relay2.example.com" + ], + ["rt", "/404", "", "", "404"], + ["title", "My Nostr Website"], + ["description", "A decentralized website on Nostr"] + ], + "content": "", + "id": "", + "sig": "" +} +``` + +### Entrypoint (11126) + +Replaceable event that points to the current site index. Only the latest event per author is stored by relays. + +**Required tags:** + +- `a` - Address coordinates to the current site index: `["a", "31126::", ""]` + - The `` is the version identifier used in the site index's `d` tag (e.g., `"v1.2.3"`) + +**Example:** + +```json +{ + "kind": 11126, + "pubkey": "", + "created_at": 1234567890, + "tags": [["a", "31126::v1.2.3", "wss://relay1.example.com"]], + "content": "", + "id": "", + "sig": "" +} +``` + +## Client Behavior + +### Publishing + +1. Generate asset events (kind 1125) with SHA-256 hashes and MIME types +2. Publish assets to relays +3. Create page manifest (1126) for each page, referencing asset event IDs +4. Create/update site index (31126) with route-to-manifest mapping using `rt` tags +5. Update entrypoint (11126) to point to the current site index version +6. Generate DNS TXT record (see NIP-ZZ) + +### Fetching and Rendering + +1. Query DNS for `_nweb.` TXT record to get site pubkey and relays +2. Fetch entrypoint (11126) from relays: `{"kinds": [11126], "authors": [""]}` +3. Extract site index address from the `a` tag in entrypoint +4. Fetch site index (31126) using the address coordinates +5. Parse site index tags to extract routes (`rt`), identifying which route is marked as `"default"` and which is marked as `"404"` +6. Get page manifest ID for requested route from the `rt` tags + - If route not found and a `"default"` route exists, use the default route + - If route not found and no `"default"` route exists, use the `"/"` route + - If the `"/"` route doesn't exist, display a 404 error + - If a `"404"` route exists, display the 404 page + - If no `"404"` route exists, display a generic client-generated error page +7. Fetch page manifest (1126): `{"ids": [""]}` +8. Fetch all referenced assets (kind 1125) by event ID +9. Parse each asset's `m` tag to determine MIME type (HTML, CSS, JavaScript, etc.) +10. Assemble HTML with CSS and JS references +11. Render in sandboxed environment + +### Security Considerations + +**Author Verification:** + +- All events MUST be authored by the pubkey specified in DNS TXT record +- Clients MUST reject events from other pubkeys + +**Content Addressing:** + +- All assets (kind 1125) MUST include `x` tag with SHA-256 hash of the content + - Enables content deduplication: relays MAY use the `x` tag to identify and deduplicate identical content + - Allows content sharing: multiple sites can reference the same asset by its hash +- Site indexes (31126) use the `d` tag for versioning + - The `d` tag SHOULD follow semantic versioning (e.g., `"v1.2.3"`) or use environment identifiers (e.g., `"main"`, `"staging"`) + - Allows publishers to reference specific versions via the entrypoint + +**Sandboxing:** + +- Content SHOULD be rendered in isolated environment (iframe sandbox) +- Network requests outside Nostr SHOULD be blocked + +## Caching + +**DNS Records:** + +- Cache only as offline fallback +- Always attempt fresh DNS lookup + +**Entrypoint (11126):** + +- Always fetch fresh (TTL = 0) +- Required to get current site index + +**Site Index (31126):** + +- Cache with short TTL (30-60 seconds) +- Required to detect site updates + +**Page Manifests (1126):** + +- Cache with reasonable TTL (content-addressed by ID) +- Validate against current site index + +**Regular Assets (1125):** + +- Cache with reasonable TTL (content-addressed) +- Use SHA-256 hash (from `x` tag) for cache key validation +- Can be indexed by MIME type (from `m` tag) for efficient queries + +## Reference Implementation + +- Website: nweb.shugur.com +- Browser extension: + - Chrome Web Store: [nostr-web-browser](https://chromewebstore.google.com/detail/nostr-web-browser/hhdngjdmlabdachflbdfapkogadodkif) + - Firefox Add-on: [nostr-web-browser on AMO](https://addons.mozilla.org/en-US/firefox/addon/nostr-web-browser/) +- Publisher CLI: + - [nw-publisher on npm](https://www.npmjs.com/package/nw-publisher) (Recommended) + - [publisher](https://github.com/Shugur-Network/nw-nips/tree/main/publisher) diff --git a/NIP-ZZ.md b/NIP-ZZ.md new file mode 100644 index 0000000000..0700ca5100 --- /dev/null +++ b/NIP-ZZ.md @@ -0,0 +1,343 @@ +# NIP-ZZ + +## DNS Bootstrap for Nostr Web Pages + +`draft` `optional` + +This NIP standardizes DNS TXT record format for bootstrapping Nostr Web Pages clients with site metadata including pubkey, relay URLs for discovery. + +## Abstract + +To make Nostr Web Pages accessible via traditional domain names, this NIP defines a DNS TXT record format at `_nweb.` that provides: + +- Site public key (for event verification) +- Relay URLs where site events are published + +Clients query relays for the entrypoint (kind 11126) from the specified pubkey, which points to the current site index (kind 31126), enabling automatic updates without DNS changes. + +## Motivation + +While Nostr events are identified by pubkey and event IDs, users expect to navigate via domain names (e.g., `example.com`). DNS bootstrap provides: + +- **Human-readable addresses** - Users type domains, not npubs +- **Author pinning** - DNS record pins the canonical site pubkey, preventing impersonation +- **Relay discovery** - Clients know where to fetch events +- **Automatic updates** - DNS is set once; clients query for latest events by pubkey, eliminating DNS updates on republish + +## DNS TXT Record Format + +### Record Name + +``` +_nweb. +``` + +Examples: + +- `_nweb.example.com` +- `_nweb.blog.example.com` +- `_nweb.mysite.org` + +### Record Value + +A semicolon-separated key-value token string with the following format: + +``` +v=;pk=;relays=[,,...] +``` + +| Component | Required | Description | +| --------- | -------- | --------------------------------------------- | +| `v=` | Yes | Schema version (currently `1`) | +| `pk=` | Yes | Site pubkey (npub or hex) | +| `relays=` | Yes | Comma-separated WSS relay URLs (at least one) | + +### Example Records + +**Minimal record:** + +``` +v=1;pk=npub1abc123...;relays=wss://relay.example.com +``` + +**Multiple relays:** + +``` +v=1;pk=5e56a2e48c4c5eb902e062bc30f92eabcf2e2fb96b5e7...;relays=wss://relay1.example.com,wss://relay2.example.com,wss://relay3.example.com +``` + +## Public Key Format + +The `pk=` value accepts two formats: + +**npub (bech32):** + +``` +v=1;pk=npub1abc123def456...;relays=wss://relay.example.com +``` + +**Hex:** + +``` +v=1;pk=5e56a2e48c4c5eb902e062bc30f92eabcf2e2fb96b5e7...;relays=wss://relay.example.com +``` + +Clients MUST support both formats. + +## Relay URLs + +**Format:** + +- MUST use `wss://` scheme (WebSocket Secure) +- MUST be valid WebSocket URLs +- SHOULD be publicly accessible +- Comma-separated in the `relays=` value + +**Example:** + +``` +v=1;pk=npub1...;relays=wss://relay1.example.com,wss://relay2.example.com,wss://relay3.example.com +``` + +**Recommendations:** + +- Include at least 3 relays for redundancy (don't include many relays to avoid bloat) +- Use well-known public relays +- Ensure relays support NIP-YY event kinds (1125, 1126, 31126, 11126) +- Spaces after commas in relay list are allowed and will be trimmed during parsing + +## Client Behavior + +### DNS Lookup + +1. User navigates to `example.com` +2. Client checks for `_nweb.example.com` TXT record +3. If found, parse the tokenized string and validate +4. If not found or invalid, handle as regular HTTP navigation + +### Record Validation + +**Required checks:** + +- Record contains `v=1` token (version check) +- Record contains `pk=` token with valid npub or hex pubkey +- Record contains `relays=` token with at least one `wss://` URL +- Unknown parameters MUST be ignored for forward compatibility + +**If validation fails:** + +- Ignore record and handle as regular navigation +- Log warning for debugging + +### Relay Connection and Site Discovery + +1. Connect to all relays in parallel +2. Query for the most recent entrypoint event (kind 11126) from the pubkey: + ```javascript + { + kinds: [11126], + authors: [pubkey], + limit: 1 + } + ``` +3. Extract the site index address from the `a` tag in the entrypoint event: + - The `a` tag format is: `["a", "31126::", ""]` + - Parse to get: `kind`, `pubkey`, and `d` tag value +4. Query for the site index (kind 31126) using the extracted address coordinates: + ```javascript + { + kinds: [31126], + authors: [pubkey], + "#d": [""] + } + ``` +5. Aggregate results from multiple relays +6. Use the event with the most recent `created_at` timestamp + +**Alternative Query Method:** + +If you want to discover all available site index versions (for version history or debugging), you can query without the `#d` filter: + +```javascript +{ + kinds: [31126], + authors: [pubkey] +} +``` + +Then sort by `created_at` to get the latest version, or filter by specific `d` tags to get particular versions. + +**Note:** The entrypoint (kind 11126) is a replaceable event, so relays only store the latest one. This ensures clients always discover the current site index through the entrypoint's `a` tag reference. + +This two-step approach (Entrypoint → Site Index) ensures clients always load the latest published version without requiring DNS updates. + +### Author Verification + +**Critical security requirement:** + +All fetched events MUST be authored by the pubkey from DNS record: + +```javascript +if (event.pubkey !== parsedDnsRecord.pubkey) { + throw new Error("Event author does not match DNS pubkey"); +} +``` + +This prevents relay impersonation attacks. + +## DNS Provider Considerations + +### Token Formatting + +The tokenized format is simpler than JSON and works universally across DNS providers: + +**Standard format:** + +``` +v=1;pk=npub1...;relays=wss://relay1.example.com,wss://relay2.example.com +``` + +**No escaping required** - Semicolon-separated key-value tokens with comma-separated relay values are naturally supported by DNS TXT records. + +### Record Length Limits + +TXT records have size limits: + +- Single string: 255 characters +- Multiple strings: Concatenated to ~4KB + +For large records, consider: + +- Using hex pubkey instead of npub (slightly shorter) +- Limiting the number of relay URLs +- Prioritizing the most reliable relays + +## DNSSEC + +DNSSEC is **strongly recommended** to prevent DNS spoofing: + +```bash +# Enable DNSSEC for your domain +# (Provider-specific configuration) +``` + +Clients SHOULD verify DNSSEC signatures when available. + +## Example Implementation + +### Setting DNS Record + +**Cloudflare:** + +``` +Type: TXT +Name: _nweb +Content: v=1;pk=npub1...;relays=wss://relay.example.com +TTL: Auto or 3600 (DNS rarely needs updating) +``` + +**Route 53:** + +``` +Type: TXT +Name: _nweb.example.com +Value: "v=1;pk=npub1...;relays=wss://relay.example.com" +TTL: 3600 (DNS rarely needs updating) +``` + +### Client Lookup + +```javascript +// DNS-over-HTTPS lookup +const response = await fetch( + `https://dns.google/resolve?name=_nweb.example.com&type=TXT` +); +const data = await response.json(); + +// Parse TXT record (remove quotes if present) +const txtRecord = data.Answer?.[0]?.data.replace(/^"|"$/g, ""); + +// Parse key-value tokens (semicolon-separated) +const tokens = txtRecord.split(";"); +const params = {}; + +for (const token of tokens) { + const [key, value] = token.split("="); + if (key && value) { + params[key] = value; + } +} + +// Validate format +if (params.v !== "1") { + throw new Error("Unsupported version or invalid format"); +} + +if (!params.pk || !params.relays) { + throw new Error("Invalid record: missing pk or relays"); +} + +// Extract pubkey and relays +const pubkey = params.pk; +const relays = params.relays + .split(",") + .map((r) => r.trim()) + .filter((r) => r.startsWith("wss://")); + +if (relays.length === 0) { + throw new Error("Invalid record: no valid relay URLs"); +} + +// Use config +const parsedPubkey = parseNpub(pubkey); // handles both npub and hex +``` + +## Security Considerations + +### Author Pinning + +The DNS record acts as a trust anchor: + +- **MUST** verify all events match DNS pubkey +- **MUST** reject events from other authors +- Prevents relay injection attacks + +### DNS Spoofing + +Without DNSSEC: + +- Attacker could serve fake DNS record +- Points to malicious pubkey +- User sees impersonated site + +**Mitigation:** + +- Enable DNSSEC +- Use DNS-over-HTTPS (DoH) for client lookups +- Cache validated records + +### Relay Censorship + +If all listed relays censor content: + +- Site becomes inaccessible via DNS +- Users can manually specify alternative relays +- Consider including diverse relay set + +## Caching Strategy + +**DNS TTL:** + +- DNS TTL can be set high (3600+ seconds) since the record rarely changes +- The DNS record only contains pubkey and relay information +- Site content updates happen via new events, not DNS changes +- Re-query DNS only on TTL expiration or manual refresh +- Cache DNS record for offline fallback + +**Site Content Updates:** + +- Clients query relays for the latest entrypoint (kind 11126) on each visit +- The entrypoint points to the current site index (kind 31126) via the `a` tag +- Use `created_at` timestamp to determine the most recent version +- No DNS propagation delay for content updates +- Publishers can update sites instantly by publishing new entrypoint events