From 71ffba4494d485cf4214809e7b8d6185896284fe Mon Sep 17 00:00:00 2001 From: karim Hassan Date: Sat, 30 Aug 2025 16:18:34 +0300 Subject: [PATCH 01/10] Add NIP-XX: Time Capsules specification This NIP defines time-locked capsules: encrypted Nostr events that become readable only at/after a target timestamp or when a threshold of designated witnesses publish unlock shares. It includes specifications for event kinds, unlock modes, protocol flow, client behavior, security considerations, and examples. --- xx.md | 317 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 xx.md diff --git a/xx.md b/xx.md new file mode 100644 index 0000000000..5eef1ba1ce --- /dev/null +++ b/xx.md @@ -0,0 +1,317 @@ +# NIP-XX: Time Capsules + +`draft` `optional` + +This NIP defines time-locked capsules: encrypted Nostr events that become readable only at/after a target timestamp or when a threshold of designated witnesses publish unlock shares. This enables delayed revelation, threshold cryptography, digital inheritance, and whistleblowing protection. + +Time-locked capsules allow content to be: + +- Released automatically after a specific timestamp +- Unlocked when multiple witnesses collaborate +- Made accessible after long periods for digital inheritance +- Protected with built-in delays for sensitive material + +## Event Kinds +Permalink: Event Kinds + +- `1990`: Time Capsule (regular) +- `30095`: Time Capsule (parameterized replaceable; keyed by `d` tag) +- `1991`: Time Capsule Unlock Share +- `1992`: Time Capsule Share Distribution + +## Specification +Permalink: Specification + +### Time Capsule Events (kinds `1990` and `30095`) +Permalink: Time Capsule Events + +A time capsule event contains encrypted content and unlock conditions. + +#### Required tags + +- `u`: Unlock configuration in format `["u","","","",...]` +- `p`: Witness pubkeys (one or more) - `["p",""]` +- `w-commit`: Merkle root commitment - `["w-commit",""]` +- `enc`: Encryption method - `["enc","nip44:v2"]` +- `loc`: Storage location - `["loc","inline"|"https"|"blossom"|"ipfs"]` + +#### Optional tags + +- `d`: Identifier (required for kind `30095`) - `["d",""]` +- `uri`: External content URI (required when `loc != "inline"`) - `["uri",""]` +- `sha256`: Content integrity hash - `["sha256",""]` +- `expiration`: Expiration timestamp per NIP-40 - `["expiration",""]` +- `alt`: Human-readable description - `["alt",""]` + +#### Content + +The `content` field MUST contain a base64-encoded NIP-44 v2 encrypted payload. When `loc` is `"inline"`, the entire encrypted content is in this field. When `loc` is external, this field MAY be empty and the `uri` tag points to the encrypted content. + +### Unlock Modes +Permalink: Unlock Modes + +#### Threshold Mode + +```plaintext +["u","threshold","t","","n","","T",""] +``` + +- **t**-of-**n** witnesses must provide shares at/after timestamp `T` +- Prevents unilateral early disclosure but not collusion of any `t` witnesses + +#### Scheduled Mode + +```plaintext +["u","scheduled","T",""] +``` + +- Indicates time-based operational release where witnesses or services intend to post shares after `T` +- This mode is not a cryptographic timelock; a future revision may define a VDF-based trustless mode + +Implementations MUST parse unknown `u` modes conservatively and treat them as unsupported. + +### Unlock Share Events (kind `1991`) +Permalink: Unlock Share Events + +A witness posts one share after the unlock timestamp (with optional skew tolerance). + +#### Required tags + +- `e`: Capsule event reference - `["e",""]` +- `a`: Addressable reference (if capsule is parameterized replaceable) - `["a","30095::"]` +- `p`: Witness pubkey - `["p",""]` +- `T`: Unlock time from capsule - `["T",""]` + +#### Content + +- Base64 Shamir share for threshold mode +- MAY be gift-wrapped (per NIP-59) to reduce metadata leakage +- Clients MUST access the plaintext share after timestamp `T` + +### Share Distribution Events (kind `1992`) +Permalink: Share Distribution Events + +Automates delivery of per-witness shares immediately after capsule creation. + +#### Required tags + +- `e`: Capsule event reference - `["e",""]` +- `a`: Addressable reference (if capsule is parameterized replaceable) - `["a","30095::"]` +- `p`: Recipient witness - `["p",""]` +- `share-idx`: Share index - `["share-idx","<0..n-1>"]` +- `enc`: Encryption method - `["enc","nip44:v2"]` + +#### Content + +NIP-44 v2 ciphertext containing the Shamir share destined for the witness. Only the intended witness can decrypt. + +#### Validation Rules + +- Event MUST be authored by the same pubkey as the capsule +- The target `p` MUST appear in the capsule's witness list +- `share-idx` MUST be within `[0, n-1]` + +## Protocol Flow +Permalink: Protocol Flow + +1. **Create Capsule** (kind `1990` or `30095`) + - Author generates random key `K` and encrypts payload with NIP-44 v2 → `C` + - Selects witnesses (p tags), sets threshold `t`, witness count `n`, unlock time `T` + - Computes `w-commit` over ordered witnesses + - Publishes capsule with `content=C`, unlock config, witness list, commitment, storage location + +2. **Distribute Shares** (kind `1992`) *(recommended)* + - Split `K` using Shamir's Secret Sharing (t, n) + - For each witness, publish `1992` with NIP-44 encrypted share for that witness + - Include `share-idx` to maintain ordering + +3. **Unlock** (kind `1991`) + - At/after timestamp `T` (± skew tolerance), witnesses publish `1991` with plaintext shares + - Clients collect any `t` valid shares, reconstruct `K`, and decrypt `C` + +## Relay Behavior +Permalink: Relay Behavior + +### Validation + +Relays MUST: + +- Ensure required tags exist and are well-formed +- For `1991`, reject shares where `now < T - skew` (recommended skew = 300 seconds) +- For `1992`, validate author matches capsule author and recipient witness is in capsule's witness list + +### Indexing + +Relays SHOULD: + +- Index `p` tags (witnesses) and `e` tags (capsule references) for discovery +- Not rely on custom tag filters beyond NIP-01 + +### NIP-11 Capability Advertisement +Permalink: NIP-11 Capability Advertisement + +Relays implementing this NIP SHOULD advertise their support in their NIP-11 document: + +```json +{ + "supported_nips": [1, 11, ...], + "software": "...", + "version": "...", + "capsules": { + "v": "1", + "modes": ["threshold","scheduled"], + "max_inline_bytes": 131072 + } +} +``` + +### Error Handling + +Early share rejection SHOULD use clear error messages per NIP-01 (e.g., `["OK", , false, "invalid: too early"]`). + +## Client Behavior +Permalink: Client Behavior + +- **Creation**: Generate `K`, encrypt payload with NIP-44 v2, produce capsule event, compute `w-commit`, publish +- **Distribution**: Publish `1992` per witness with NIP-44 encrypted share; store local copy +- **Monitoring**: Track timestamp `T`, watch for `1991` from witnesses; tolerate skew ±300s +- **Reconstruction**: Verify witness membership via `w-commit`, collect any `t` valid shares, reconstruct `K`, decrypt content +- **Integrity**: When `loc != inline`, fetch `uri`, verify `sha256` hash before decryption +- **Discovery**: Use standard filters, e.g., witnesses look up: + +```json +{ "kinds": [1992], "#p": [""] } +``` + +## Security Considerations +Permalink: Security Considerations + +- **Witness Collusion**: Threshold prevents unilateral early disclosure but not collusion of any `t` witnesses. Choose diverse witnesses and set `t` accordingly. +- **Early Disclosure**: Enforce timestamp `T` at relays (reject pre-`T - skew`) and at clients (ignore early shares). +- **Time Manipulation**: Use trusted time sources where possible; keep small skew windows. +- **External Storage Integrity**: Include `sha256` for any `uri` content. +- **Spam/DoS**: Rate-limit `1991/1992` per capsule and per witness. + +## Examples +Permalink: Examples + +### Time Capsule (kind 1990, threshold 2/3) + +```json +{ + "kind": 1990, + "pubkey": "a2b3c4d5...", + "created_at": 1735689600, + "content": "base64_encoded_nip44v2_ciphertext", + "tags": [ + ["u","threshold","t","2","n","3","T","1735776000"], + ["p","f7234bd4..."], + ["p","a1a2a3a4..."], + ["p","b1b2b3b4..."], + ["w-commit","3a5f...c9"], + ["enc","nip44:v2"], + ["loc","inline"], + ["alt","Secret message requiring 2 of 3 witnesses"] + ] +} +``` + +### Time Capsule (kind 30095, external storage) + +```json +{ + "kind": 30095, + "pubkey": "a2b3c4d5...", + "created_at": 1735689600, + "content": "", + "tags": [ + ["d","capsule-2025-07"], + ["u","threshold","t","3","n","5","T","1736000000"], + ["p","w1..."], + ["p","w2..."], + ["p","w3..."], + ["p","w4..."], + ["p","w5..."], + ["w-commit","9c01...ab"], + ["enc","nip44:v2"], + ["loc","https"], + ["uri","https://media.example/caps/abc"], + ["sha256","c0ffee..."], + ["alt","External ciphertext with integrity hash"] + ] +} +``` + +### Unlock Share (kind 1991) + +```json +{ + "kind": 1991, + "pubkey": "a1a2a3a4...", + "created_at": 1735776100, + "content": "base64_shamir_share", + "tags": [ + ["e","...capsule_event_id..."], + ["a","30095:a2b3c4d5...:capsule-2025-07"], + ["p","a1a2a3a4..."], + ["T","1735776000"] + ] +} +``` + +### Share Distribution (kind 1992) + +```json +{ + "kind": 1992, + "pubkey": "a2b3c4d5...", + "created_at": 1735689700, + "content": "base64_nip44v2_encrypted_share_for_witness", + "tags": [ + ["e","...capsule_event_id..."], + ["a","30095:a2b3c4d5...:capsule-2025-07"], + ["p","a1a2a3a4..."], + ["share-idx","1"], + ["enc","nip44:v2"] + ] +} +``` + +## Test Vectors +Permalink: Test Vectors + +### Test Vector A: Threshold 2-of-3 + +- Witnesses (ordered pubkeys): `hex_pubkey_A`, `hex_pubkey_B`, `hex_pubkey_C` +- `w-commit` = MerkleRoot([(0, `hex_pubkey_A`), (1, `hex_pubkey_B`), (2, `hex_pubkey_C`)]) +- `T` = `1735776000` +- Shares: `S0,S1,S2`; any two reconstruct `K` +- Ciphertext: `C = NIP44v2_Encrypt(K, "hello world")` → `content = base64(C)` + +Expected flow: + +- `1990` event as shown above +- `1992` to `hex_pubkey_B` with `share-idx=1` (content = NIP-44 encrypted `S1` to `hex_pubkey_B`) +- `1991` from `hex_pubkey_B` and `hex_pubkey_C` after `T` (plaintext shares) +- Client reconstructs `K` and decrypts `C` → `"hello world"` + +## Rationale +Permalink: Rationale + +- Uses new kinds to avoid overloading existing semantics; unaware nodes ignore unknown kinds +- Leverages standard `p`/`e` tags for discovery; avoids non-standard tag filtering +- `w-commit` binds the witness set to prevent tampering +- Parameterized replaceable variant (`30095`) supports pre-`T` fixes via the `d` tag and `a` addressing + +## Backwards Compatibility +Permalink: Backwards Compatibility + +New kinds are ignored by unaware relays/clients. The `alt` tag provides a human-readable hint for unknown kinds. Use of standard `p` and `e` tags preserves discoverability via existing filters. + +## Reference Implementation +Permalink: Reference Implementation + +A reference implementation is provided in [Shugur Relay](https://github.com/Shugur-Network/relay) project: + +- Relay validation: `internal/relay/nips/nip_time_capsules.go` +- Test suite: `tests/nips/test_time_capsules_comprehensive.sh` From 2166c5f9c835a2b423e1483d282f50bff5adaf6b Mon Sep 17 00:00:00 2001 From: karim Hassan Date: Fri, 12 Sep 2025 21:55:58 +0300 Subject: [PATCH 02/10] Refactor: Redefine Time Capsule --- xx.md | 358 ++++++++++++++++++---------------------------------------- 1 file changed, 110 insertions(+), 248 deletions(-) diff --git a/xx.md b/xx.md index 5eef1ba1ce..d7590ae358 100644 --- a/xx.md +++ b/xx.md @@ -1,317 +1,179 @@ -# NIP-XX: Time Capsules +# **NIP-XX — Time-Lock Encrypted Messages (Time Capsules)** -`draft` `optional` +`draft` `optional` -This NIP defines time-locked capsules: encrypted Nostr events that become readable only at/after a target timestamp or when a threshold of designated witnesses publish unlock shares. This enables delayed revelation, threshold cryptography, digital inheritance, and whistleblowing protection. +This NIP defines **time capsules**: Nostr events whose plaintext becomes readable **at/after a target time** using a drand time-lock (tlock). Capsules can be broadcast publicly or delivered privately with [**NIP-59**](https://github.com/nostr-protocol/nips/blob/master/59.md) gift wrapping; encryption for sealing/wrapping uses [**NIP-44 v2**](https://github.com/nostr-protocol/nips/blob/master/44.md). -Time-locked capsules allow content to be: +> Encoding note: All Base64 in this NIP is RFC 4648 padded and MUST NOT contain line breaks. +> +> **Hex note:** All hex strings in this NIP are **lowercase**. -- Released automatically after a specific timestamp -- Unlocked when multiple witnesses collaborate -- Made accessible after long periods for digital inheritance -- Protected with built-in delays for sensitive material +--- -## Event Kinds -Permalink: Event Kinds +## Event kinds -- `1990`: Time Capsule (regular) -- `30095`: Time Capsule (parameterized replaceable; keyed by `d` tag) -- `1991`: Time Capsule Unlock Share -- `1992`: Time Capsule Share Distribution +- **1041** — Time Capsule. -## Specification -Permalink: Specification +--- -### Time Capsule Events (kinds `1990` and `30095`) -Permalink: Time Capsule Events +## Time capsule (kind: 1041) -A time capsule event contains encrypted content and unlock conditions. +A public capsule is a signed `kind:1041` event. Its `content` is a **Base64 of the binary (non-armored) age v1 ciphertext** with **exactly one `tlock` recipient stanza** (no other recipient types). -#### Required tags - -- `u`: Unlock configuration in format `["u","","","",...]` -- `p`: Witness pubkeys (one or more) - `["p",""]` -- `w-commit`: Merkle root commitment - `["w-commit",""]` -- `enc`: Encryption method - `["enc","nip44:v2"]` -- `loc`: Storage location - `["loc","inline"|"https"|"blossom"|"ipfs"]` - -#### Optional tags - -- `d`: Identifier (required for kind `30095`) - `["d",""]` -- `uri`: External content URI (required when `loc != "inline"`) - `["uri",""]` -- `sha256`: Content integrity hash - `["sha256",""]` -- `expiration`: Expiration timestamp per NIP-40 - `["expiration",""]` -- `alt`: Human-readable description - `["alt",""]` - -#### Content - -The `content` field MUST contain a base64-encoded NIP-44 v2 encrypted payload. When `loc` is `"inline"`, the entire encrypted content is in this field. When `loc` is external, this field MAY be empty and the `uri` tag points to the encrypted content. - -### Unlock Modes -Permalink: Unlock Modes - -#### Threshold Mode - -```plaintext -["u","threshold","t","","n","","T",""] -``` - -- **t**-of-**n** witnesses must provide shares at/after timestamp `T` -- Prevents unilateral early disclosure but not collusion of any `t` witnesses - -#### Scheduled Mode - -```plaintext -["u","scheduled","T",""] +```json +{ + "id": "<32-byte lowercase hex sha256 of serialized event>", + "pubkey": "<32-byte lowercase hex pubkey of the author>", + "created_at": "", + "kind": 1041, + "tags": [ + ["tlock", "", ""], + ["alt", ""] + ], + "content": "", + "sig": "<64-byte lowercase hex signature of the event hash>" +} ``` -- Indicates time-based operational release where witnesses or services intend to post shares after `T` -- This mode is not a cryptographic timelock; a future revision may define a VDF-based trustless mode - -Implementations MUST parse unknown `u` modes conservatively and treat them as unsupported. - -### Unlock Share Events (kind `1991`) -Permalink: Unlock Share Events - -A witness posts one share after the unlock timestamp (with optional skew tolerance). - -#### Required tags - -- `e`: Capsule event reference - `["e",""]` -- `a`: Addressable reference (if capsule is parameterized replaceable) - `["a","30095::"]` -- `p`: Witness pubkey - `["p",""]` -- `T`: Unlock time from capsule - `["T",""]` - -#### Content - -- Base64 Shamir share for threshold mode -- MAY be gift-wrapped (per NIP-59) to reduce metadata leakage -- Clients MUST access the plaintext share after timestamp `T` +**Rules (public 1041):** -### Share Distribution Events (kind `1992`) -Permalink: Share Distribution Events +- Exactly **one** `tlock` tag (see below). +- `content` **MUST** be Base64 of **binary** age v1 with a **single `tlock` recipient stanza** and **no other** recipient stanzas (e.g., **no** `X25519`, `scrypt`). ASCII-armored age is **invalid**. +- **clients** enforce unlock by verifying drand beacons; relays **do not** enforce time. -Automates delivery of per-witness shares immediately after capsule creation. +--- -#### Required tags +## Tags -- `e`: Capsule event reference - `["e",""]` -- `a`: Addressable reference (if capsule is parameterized replaceable) - `["a","30095::"]` -- `p`: Recipient witness - `["p",""]` -- `share-idx`: Share index - `["share-idx","<0..n-1>"]` -- `enc`: Encryption method - `["enc","nip44:v2"]` +### `tlock` (required on 1041) -#### Content +**Single, preferred format (normative):** -NIP-44 v2 ciphertext containing the Shamir share destined for the witness. Only the intended witness can decrypt. - -#### Validation Rules - -- Event MUST be authored by the same pubkey as the capsule -- The target `p` MUST appear in the capsule's witness list -- `share-idx` MUST be within `[0, n-1]` - -## Protocol Flow -Permalink: Protocol Flow - -1. **Create Capsule** (kind `1990` or `30095`) - - Author generates random key `K` and encrypts payload with NIP-44 v2 → `C` - - Selects witnesses (p tags), sets threshold `t`, witness count `n`, unlock time `T` - - Computes `w-commit` over ordered witnesses - - Publishes capsule with `content=C`, unlock config, witness list, commitment, storage location - -2. **Distribute Shares** (kind `1992`) *(recommended)* - - Split `K` using Shamir's Secret Sharing (t, n) - - For each witness, publish `1992` with NIP-44 encrypted share for that witness - - Include `share-idx` to maintain ordering - -3. **Unlock** (kind `1991`) - - At/after timestamp `T` (± skew tolerance), witnesses publish `1991` with plaintext shares - - Clients collect any `t` valid shares, reconstruct `K`, and decrypt `C` +```json +["tlock", "", ""] +``` -## Relay Behavior -Permalink: Relay Behavior +**Validation:** -### Validation +- `drand_chain_hex64` matches `^[0-9a-f]{64}$` (lowercase). +- `drand_round_uint` matches `^[1-9][0-9]{0,18}$` (positive, 64-bit safe). +- The age ciphertext **MUST** contain **exactly one** recipient stanza of type `tlock` whose **chain and round equal** the tag values; any mismatch **MUST** be rejected. -Relays MUST: +### `p` (routing) — **only valid on outer 1059** -- Ensure required tags exist and are well-formed -- For `1991`, reject shares where `now < T - skew` (recommended skew = 300 seconds) -- For `1992`, validate author matches capsule author and recipient witness is in capsule's witness list +- The **inner** `kind:1041` **MUST NOT** contain `p` tags. Clients **MUST** reject any private capsule whose inner 1041 includes a `p` tag. +- On the **outer** `kind:1059` (gift wrap), include at least one `["p","",""]` per recipient for routing. -### Indexing +### `alt` (optional on 1041) -Relays SHOULD: +- Human-readable description for UX. -- Index `p` tags (witnesses) and `e` tags (capsule references) for discovery -- Not rely on custom tag filters beyond NIP-01 +--- -### NIP-11 Capability Advertisement -Permalink: NIP-11 Capability Advertisement +## Private capsule (sealed & wrapped per NIP-59) -Relays implementing this NIP SHOULD advertise their support in their NIP-11 document: +A private capsule is delivered via the NIP-59 pipeline: -```json -{ - "supported_nips": [1, 11, ...], - "software": "...", - "version": "...", - "capsules": { - "v": "1", - "modes": ["threshold","scheduled"], - "max_inline_bytes": 131072 - } -} -``` +1. **Create the rumor (kind:1041, unsigned).** -### Error Handling + Same schema as public, but **do not sign**. `content` is Base64(binary age v1 `tlock` ciphertext) and the `tlock` tag is present. **Omit `p`**. -Early share rejection SHOULD use clear error messages per NIP-01 (e.g., `["OK", , false, "invalid: too early"]`). + **Rumor MUST NOT include `sig`.** **`id` MAY be present**; if present, clients **MUST** recompute it after recovery and reject on mismatch. -## Client Behavior -Permalink: Client Behavior +2. **Seal (kind:13).** -- **Creation**: Generate `K`, encrypt payload with NIP-44 v2, produce capsule event, compute `w-commit`, publish -- **Distribution**: Publish `1992` per witness with NIP-44 encrypted share; store local copy -- **Monitoring**: Track timestamp `T`, watch for `1991` from witnesses; tolerate skew ±300s -- **Reconstruction**: Verify witness membership via `w-commit`, collect any `t` valid shares, reconstruct `K`, decrypt content -- **Integrity**: When `loc != inline`, fetch `uri`, verify `sha256` hash before decryption -- **Discovery**: Use standard filters, e.g., witnesses look up: + JSON-serialize the rumor and encrypt it to the **recipient** using **NIP-44 v2**; put the ciphertext in `.content`. **`tags` MUST be `[]`**. **Sign with the author’s real key.** -```json -{ "kinds": [1992], "#p": [""] } -``` +3. **Gift wrap (kind:1059).** -## Security Considerations -Permalink: Security Considerations + JSON-serialize the **seal** and encrypt it to the **recipient** using **NIP-44 v2** with a **one-time ephemeral** key; put the ciphertext in `.content`. Add at least one `["p","",""]` (one 1059 per recipient is best practice). **Sign with the ephemeral key.** -- **Witness Collusion**: Threshold prevents unilateral early disclosure but not collusion of any `t` witnesses. Choose diverse witnesses and set `t` accordingly. -- **Early Disclosure**: Enforce timestamp `T` at relays (reject pre-`T - skew`) and at clients (ignore early shares). -- **Time Manipulation**: Use trusted time sources where possible; keep small skew windows. -- **External Storage Integrity**: Include `sha256` for any `uri` content. -- **Spam/DoS**: Rate-limit `1991/1992` per capsule and per witness. + Broadcast only to the recipient’s **DM relays** as advertised by their relay list metadata (per the relevant NIP). -## Examples -Permalink: Examples +### Minimal examples (structure only) -### Time Capsule (kind 1990, threshold 2/3) +**Rumor (kind:1041, unsigned):** ```json { - "kind": 1990, - "pubkey": "a2b3c4d5...", - "created_at": 1735689600, - "content": "base64_encoded_nip44v2_ciphertext", + "id": "<32-byte lowercase hex sha256 of serialized event>", + "pubkey": "", + "created_at": 1234567890, + "kind": 1041, "tags": [ - ["u","threshold","t","2","n","3","T","1735776000"], - ["p","f7234bd4..."], - ["p","a1a2a3a4..."], - ["p","b1b2b3b4..."], - ["w-commit","3a5f...c9"], - ["enc","nip44:v2"], - ["loc","inline"], - ["alt","Secret message requiring 2 of 3 witnesses"] - ] + ["tlock", "", ""], + ["alt", ""] + ], + "content": "" } ``` -### Time Capsule (kind 30095, external storage) +**Seal (kind:13, signed by author; `tags = []`):** ```json { - "kind": 30095, - "pubkey": "a2b3c4d5...", - "created_at": 1735689600, - "content": "", - "tags": [ - ["d","capsule-2025-07"], - ["u","threshold","t","3","n","5","T","1736000000"], - ["p","w1..."], - ["p","w2..."], - ["p","w3..."], - ["p","w4..."], - ["p","w5..."], - ["w-commit","9c01...ab"], - ["enc","nip44:v2"], - ["loc","https"], - ["uri","https://media.example/caps/abc"], - ["sha256","c0ffee..."], - ["alt","External ciphertext with integrity hash"] - ] + "id": "<32-byte lowercase hex sha256 of serialized event>", + "pubkey": "", + "created_at": 1234567890, + "kind": 13, + "tags": [], + "content": "", + "sig": "" } ``` -### Unlock Share (kind 1991) +**Gift wrap (kind:1059, signed by ephemeral; includes `p`):** ```json { - "kind": 1991, - "pubkey": "a1a2a3a4...", - "created_at": 1735776100, - "content": "base64_shamir_share", - "tags": [ - ["e","...capsule_event_id..."], - ["a","30095:a2b3c4d5...:capsule-2025-07"], - ["p","a1a2a3a4..."], - ["T","1735776000"] - ] + "id": "<32-byte lowercase hex sha256 of serialized event>", + "pubkey": "", + "created_at": 1234567890, + "kind": 1059, + "tags": [["p", "", ""]], + "content": "", + "sig": "" } ``` -### Share Distribution (kind 1992) +--- -```json -{ - "kind": 1992, - "pubkey": "a2b3c4d5...", - "created_at": 1735689700, - "content": "base64_nip44v2_encrypted_share_for_witness", - "tags": [ - ["e","...capsule_event_id..."], - ["a","30095:a2b3c4d5...:capsule-2025-07"], - ["p","a1a2a3a4..."], - ["share-idx","1"], - ["enc","nip44:v2"] - ] -} -``` +## Decryption & validation (client-side) + +### Public 1041 -## Test Vectors -Permalink: Test Vectors +1. Verify **NIP-01** signature; check **exactly one** `tlock` tag; Base64-decode `content`. +2. Fetch the drand beacon for `drand_round_uint` and **verify** it against the chain’s BLS public key derived from `drand_chain_hex64`. +3. Parse the **binary** age v1 ciphertext; ensure **exactly one** recipient stanza of type `tlock` whose chain/round **match the tag**; reject ASCII armor or extra recipient types. +4. Decrypt with the verified beacon; the result is the plaintext. -### Test Vector A: Threshold 2-of-3 +### Private (1059 → 13 → 1041) -- Witnesses (ordered pubkeys): `hex_pubkey_A`, `hex_pubkey_B`, `hex_pubkey_C` -- `w-commit` = MerkleRoot([(0, `hex_pubkey_A`), (1, `hex_pubkey_B`), (2, `hex_pubkey_C`)]) -- `T` = `1735776000` -- Shares: `S0,S1,S2`; any two reconstruct `K` -- Ciphertext: `C = NIP44v2_Encrypt(K, "hello world")` → `content = base64(C)` +1. Validate outer **1059** (ephemeral **NIP-01** signature); **NIP-44 v2** decrypt `.content` with your key. +2. Parse inner **kind:13**; **`tags` MUST be empty**; verify **author** signature; **NIP-44 v2** decrypt `.content` using the author↔recipient conversation key. +3. Parse recovered **unsigned kind:1041 rumor**. **Verify** `lower(seal.pubkey) == lower(rumor.pubkey)` (both 32-byte lowercase hex). If `rumor.id` is present, **recompute** and reject on mismatch. For display and ordering, **use `rumor.created_at`**; the `created_at` of the seal and wrap are transport metadata and **MUST NOT** replace the rumor’s timestamp in UX. +4. Fetch & verify drand beacon as above; ensure `tlock` tag ↔ age stanza chain/round match; then age-decrypt to recover the plaintext. -Expected flow: +--- -- `1990` event as shown above -- `1992` to `hex_pubkey_B` with `share-idx=1` (content = NIP-44 encrypted `S1` to `hex_pubkey_B`) -- `1991` from `hex_pubkey_B` and `hex_pubkey_C` after `T` (plaintext shares) -- Client reconstructs `K` and decrypts `C` → `"hello world"` +## Relay semantics -## Rationale -Permalink: Rationale +- Relays **MUST NOT** attempt to decrypt or enforce unlock times. +- Clients **MUST** enforce unlock using **verified** drand beacons, **not** local clocks. -- Uses new kinds to avoid overloading existing semantics; unaware nodes ignore unknown kinds -- Leverages standard `p`/`e` tags for discovery; avoids non-standard tag filtering -- `w-commit` binds the witness set to prevent tampering -- Parameterized replaceable variant (`30095`) supports pre-`T` fixes via the `d` tag and `a` addressing +--- -## Backwards Compatibility -Permalink: Backwards Compatibility +## Security considerations -New kinds are ignored by unaware relays/clients. The `alt` tag provides a human-readable hint for unknown kinds. Use of standard `p` and `e` tags preserves discoverability via existing filters. +- **Beacon verification:** Always verify drand beacons against the chain’s BLS public key (derived from `drand_chain_hex64`) before age decryption. Do **not** trust local time or unsigned beacons; accept the first BLS-verified beacon from any endpoint. +- **Ciphertext format:** Accept **only** binary age v1 `tlock` with **exactly one** recipient stanza; **reject** ASCII-armored inputs and stanza multiplicity or other stanza types. +- **Bounds & DoS:** Before allocation, clients **SHOULD** enforce `tlock_blob ≤ 4096 bytes` and **SHOULD** reject 1041 whose **decoded** `content` exceeds **64 KiB**. Relays **MAY** drop 1041 exceeding **256 KiB** decoded. +- **Sealing/wrapping crypto:** Use **NIP-44 v2** (ECDH → HKDF, ChaCha20, HMAC, padded Base64). Validate MAC in constant time **before** attempting decryption. +- **Timestamps & privacy:** Randomize seal/wrap `created_at` slightly (e.g., jitter/backdate) for metadata privacy; the rumor’s `created_at` is canonical for UX. -## Reference Implementation -Permalink: Reference Implementation +--- -A reference implementation is provided in [Shugur Relay](https://github.com/Shugur-Network/relay) project: +## Implementations -- Relay validation: `internal/relay/nips/nip_time_capsules.go` -- Test suite: `tests/nips/test_time_capsules_comprehensive.sh` +- **Relay** [**Shugur Relay**] () +- **Client** [**Shugur Time Capsules**] () From 0d0b2547a1db5c5caa5eb003fd6e6096cc8672ce Mon Sep 17 00:00:00 2001 From: karim Hassan Date: Wed, 22 Oct 2025 23:27:39 +0300 Subject: [PATCH 03/10] Add support for nostr web --- NIP-YY.md | 270 ++++++++++++++++++++++++++++++++++ NIP-ZZ.md | 427 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 697 insertions(+) create mode 100644 NIP-YY.md create mode 100644 NIP-ZZ.md diff --git a/NIP-YY.md b/NIP-YY.md new file mode 100644 index 0000000000..d9a3e976c3 --- /dev/null +++ b/NIP-YY.md @@ -0,0 +1,270 @@ +# 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`) +- `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 +- `route` - Page route/path for reference (e.g., `/`, `/about`) +- `csp` - Content Security Policy directives to override the default CSP for this specific page + +**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"], + ["route", "/"], + ["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 uses a truncated hash (like Git short hashes) for content-addressed versioning. + +**Required tags:** + +- `d` - First 7-12 characters of the SHA-256 hash of the `content` field (e.g., `"a1b2c3d"`, `"a1b2c3d4e5f6"`) +- `x` - Full hex-encoded SHA-256 hash of the `content` field (to verify the `d` tag is correctly derived) + +**Optional tags:** + +- `alt` - Human-readable identifier (e.g., `"main"`, `"staging"`, `"v1.2.3"`) for convenience + +**Content:** JSON object with route mappings and optional metadata + +**Required fields:** + +- `routes` - Object mapping route paths to page manifest event IDs (kind 1126) + +**Optional fields:** + +- `version` - Semantic version string (e.g., `"1.2.3"`) for tracking site versions +- `defaultRoute` - Default route to display when no specific route is requested (e.g., `"/"`) +- `notFoundRoute` - Route to display for 404 errors (e.g., `"/404"`) or `null` if not specified + +**Example:** + +```json +{ + "kind": 31126, + "pubkey": "", + "created_at": 1234567890, + "tags": [ + ["d", "a1b2c3d"], + ["x", "a1b2c3d4e5f6789...full-hash..."], + ["alt", "main"] + ], + "content": "{ + \"routes\": { + \"/\": \"\", + \"/about\": \"\", + \"/blog/post-1\": \"\" + }, + \"version\": \"1.2.3\", + \"defaultRoute\": \"/\", + \"notFoundRoute\": \"/404\" + }", + "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 truncated hash used in the site index's `d` tag + +**Example:** + +```json +{ + "kind": 11126, + "pubkey": "", + "created_at": 1234567890, + "tags": [["a", "31126::a1b2c3d", "wss://relay.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 +5. Update entrypoint (11126) to point to the current site index +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 content to extract `routes`, `version`, `defaultRoute`, and `notFoundRoute` fields +6. Get page manifest ID for requested route from `content.routes` +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 with CSP enforcement + +### 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) MUST include `x` tag with SHA-256 hash of the content + - The `x` tag is used to verify that the `d` tag (truncated hash) is correctly derived from the full hash + - Clients SHOULD verify that the first 7-12 characters of the `x` tag match the `d` tag + +**Content Security Policy:** + +- Default CSP: `default-src 'self'; script-src 'sha256-'` +- Per-page CSP can be specified via the `csp` tag in Page Manifest (1126) +- Custom CSP allows pages to: + - Allow specific external API connections (`connect-src`) + - Permit inline styles if needed (`style-src`) + - Control frame embedding (`frame-ancestors`) +- Clients SHOULD enforce CSP to prevent code injection +- If a page has a `csp` tag, it overrides the default CSP for that page only + +**Sandboxing:** + +- Content SHOULD be rendered in isolated environment (iframe sandbox) +- Network requests outside Nostr/Blossom 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..18d7014e30 --- /dev/null +++ b/NIP-ZZ.md @@ -0,0 +1,427 @@ +# 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 author's public key (for event verification) +- Relay URLs where site events are published + +The DNS record does NOT contain the site index event ID. Instead, clients query relays for the most recent 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 single-line JSON string with the following fields: + +| Field | Type | Required | Description | +| -------- | ------- | -------- | -------------------------------------- | +| `v` | integer | Yes | Schema version (currently `1`) | +| `pk` | string | Yes | Site pubkey (npub or hex) | +| `relays` | array | Yes | WSS relay URLs (at least one) | +| `policy` | object | No | Client hints (reserved for future use) | + +### Example Records + +**Minimal record:** + +```json +{ + "v": 1, + "pk": "npub1abc123...", + "relays": ["wss://relay.damus.io"] +} +``` + +**Complete record:** + +```json +{ + "v": 1, + "pk": "5e56a2e48c4c5eb902e062bc30f92eabcf2e2fb96b5e7...", + "relays": ["wss://relay.damus.io", "wss://nos.lol", "wss://relay.nostr.band"], + "policy": { + "min_relays": 2 + } +} +``` + +## Public Key Format + +The `pk` field accepts two formats: + +**npub (bech32):** + +```json +"pk": "npub1abc123def456..." +``` + +**Hex:** + +```json +"pk": "5e56a2e48c4c5eb902e062bc30f92eabcf2e2fb96b5e7..." +``` + +Clients MUST support both formats. + +## Relay URLs + +**Format:** + +- MUST use `wss://` scheme (WebSocket Secure) +- MUST be valid WebSocket URLs +- SHOULD be publicly accessible + +**Examples:** + +```json +"relays": [ + "wss://relay.damus.io", + "wss://relay.nostr.band", + "wss://nos.lol" +] +``` + +**Recommendations:** + +- Include at least 2-3 relays for redundancy +- Use well-known public relays +- Ensure relays support NIP-YY event kinds (1125, 1126, 31126, 11126) + +## Policy Object + +Reserved for future client hints: + +```json +"policy": { + "min_relays": 2, + "ttl": 3600, + "cache_strategy": "aggressive" +} +``` + +Current version ignores this field. Future NIPs may define standard policy fields. + +## Why No Site Index in DNS? + +**Design Decision:** The DNS record intentionally omits the site index event ID. This is a key architectural choice with significant benefits: + +### Benefits of Query-Based Discovery + +1. **Zero DNS Updates After Initial Setup** + + - DNS is configured once with pubkey and relays + - All content updates happen via event publishing only + - No waiting for DNS propagation (which can take hours) + - No risk of DNS misconfiguration breaking updates + +2. **Instant Content Updates** + + - Publishers republish by creating new events with fresh timestamps + - Clients query for latest event by `created_at` + - Updates visible immediately (relay propagation is seconds, not hours) + +3. **Supports Multiple Sites Per Pubkey** + + - Multiple domains can point to the same pubkey + - Each site uses different `d` tags for site indices + - Clients distinguish sites by domain name, not just pubkey + +4. **Resilient to DNS Poisoning** + + - Attacker can't point DNS to old/malicious site version + - DNS only contains pubkey (identity), not content pointer + - Latest content is always determined by on-chain timestamps + +5. **Simpler Publisher Workflow** + - No need to update DNS TXT record on every publish + - No need to wait for propagation to verify changes + - Reduces human error in DNS management + +### How It Works + +``` +┌─────────────┐ +│ Browser │ +└──────┬──────┘ + │ + │ 1. User visits example.com + ▼ +┌─────────────────┐ +│ DNS Lookup │ ◄─── Query _nweb.example.com +│ (ONE TIME) │ Returns: {pk, relays} +└────────┬────────┘ + │ + │ 2. Connect to relays + ▼ +┌──────────────────┐ +│ Query Entrypoint │ ◄─── Filter: kind 11126, author=pk +│ (EVERY VISIT) │ Get latest replaceable event +└────────┬─────────┘ + │ + │ 3. Extract site index address from 'a' tag + ▼ +┌──────────────────┐ +│ Fetch Site Index │ ◄─── Get addressable event: 31126:pk:d-hash +│ (kind 31126) │ Contains route mappings +└────────┬─────────┘ + │ + │ 4. Got latest site index! + ▼ +┌──────────────────┐ +│ Load Site │ +│ Content │ +└──────────────────┘ +``` + +Publishers republish → New entrypoint with newer timestamp → Points to new site index → Clients automatically fetch the new version. + +## Client Behavior + +### DNS Lookup + +1. User navigates to `example.com` +2. Client checks for `_nweb.example.com` TXT record +3. If found, parse JSON and validate schema +4. If not found or invalid, handle as regular HTTP navigation + +### Record Validation + +**Required checks:** + +- `v` field equals `1` +- `pk` field is valid npub or hex pubkey +- `relays` array has at least one valid `wss://` URL + +**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 !== dnsRecord.pk) { + throw new Error("Event author does not match DNS pubkey"); +} +``` + +This prevents relay impersonation attacks. + +## DNS Provider Considerations + +### JSON Formatting + +Some DNS providers require specific formatting: + +**Single-line JSON:** + +``` +{"v":1,"pk":"npub1...","relays":["wss://relay.damus.io"]} +``` + +**Escaped quotes (automatic by some providers):** + +``` +{\"v\":1,\"pk\":\"npub1...\",\"relays\":[\"wss://relay.damus.io\"]} +``` + +Verify the actual TXT record value resolves to valid JSON. + +### 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 (shorter) +- Limiting relay array length +- Omitting optional fields + +## 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.damus.io"]} +TTL: Auto or 3600 (DNS rarely needs updating) +``` + +**Route 53:** + +``` +Type: TXT +Name: _nweb.example.com +Value: "{"v":1,"pk":"npub1...","relays":["wss://relay.damus.io"]}" +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 +const txtRecord = data.Answer?.[0]?.data; +const nwebConfig = JSON.parse(txtRecord.replace(/^"|"$/g, "")); + +// Validate +if (nwebConfig.v !== 1) throw new Error("Unsupported version"); +if (!nwebConfig.pk || !nwebConfig.relays) throw new Error("Invalid record"); + +// Use config +const pubkey = parseNpub(nwebConfig.pk); +const relays = nwebConfig.relays; +``` + +## 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 + +**Key Insight:** Separating DNS bootstrap (pubkey + relays) from content discovery (query latest entrypoint → site index) means: + +- DNS is configured once during initial setup +- All subsequent site updates are instant (no DNS changes needed) +- Clients automatically fetch the newest version from relays From a2fa618a47068f949ebf47d26e027593ed87f20b Mon Sep 17 00:00:00 2001 From: karim Hassan Date: Wed, 22 Oct 2025 23:36:04 +0300 Subject: [PATCH 04/10] Remove NIP-XX specification for Time-Lock Encrypted Messages (Time Capsules) --- xx.md | 179 ---------------------------------------------------------- 1 file changed, 179 deletions(-) delete mode 100644 xx.md diff --git a/xx.md b/xx.md deleted file mode 100644 index d7590ae358..0000000000 --- a/xx.md +++ /dev/null @@ -1,179 +0,0 @@ -# **NIP-XX — Time-Lock Encrypted Messages (Time Capsules)** - -`draft` `optional` - -This NIP defines **time capsules**: Nostr events whose plaintext becomes readable **at/after a target time** using a drand time-lock (tlock). Capsules can be broadcast publicly or delivered privately with [**NIP-59**](https://github.com/nostr-protocol/nips/blob/master/59.md) gift wrapping; encryption for sealing/wrapping uses [**NIP-44 v2**](https://github.com/nostr-protocol/nips/blob/master/44.md). - -> Encoding note: All Base64 in this NIP is RFC 4648 padded and MUST NOT contain line breaks. -> -> **Hex note:** All hex strings in this NIP are **lowercase**. - ---- - -## Event kinds - -- **1041** — Time Capsule. - ---- - -## Time capsule (kind: 1041) - -A public capsule is a signed `kind:1041` event. Its `content` is a **Base64 of the binary (non-armored) age v1 ciphertext** with **exactly one `tlock` recipient stanza** (no other recipient types). - -```json -{ - "id": "<32-byte lowercase hex sha256 of serialized event>", - "pubkey": "<32-byte lowercase hex pubkey of the author>", - "created_at": "", - "kind": 1041, - "tags": [ - ["tlock", "", ""], - ["alt", ""] - ], - "content": "", - "sig": "<64-byte lowercase hex signature of the event hash>" -} -``` - -**Rules (public 1041):** - -- Exactly **one** `tlock` tag (see below). -- `content` **MUST** be Base64 of **binary** age v1 with a **single `tlock` recipient stanza** and **no other** recipient stanzas (e.g., **no** `X25519`, `scrypt`). ASCII-armored age is **invalid**. -- **clients** enforce unlock by verifying drand beacons; relays **do not** enforce time. - ---- - -## Tags - -### `tlock` (required on 1041) - -**Single, preferred format (normative):** - -```json -["tlock", "", ""] -``` - -**Validation:** - -- `drand_chain_hex64` matches `^[0-9a-f]{64}$` (lowercase). -- `drand_round_uint` matches `^[1-9][0-9]{0,18}$` (positive, 64-bit safe). -- The age ciphertext **MUST** contain **exactly one** recipient stanza of type `tlock` whose **chain and round equal** the tag values; any mismatch **MUST** be rejected. - -### `p` (routing) — **only valid on outer 1059** - -- The **inner** `kind:1041` **MUST NOT** contain `p` tags. Clients **MUST** reject any private capsule whose inner 1041 includes a `p` tag. -- On the **outer** `kind:1059` (gift wrap), include at least one `["p","",""]` per recipient for routing. - -### `alt` (optional on 1041) - -- Human-readable description for UX. - ---- - -## Private capsule (sealed & wrapped per NIP-59) - -A private capsule is delivered via the NIP-59 pipeline: - -1. **Create the rumor (kind:1041, unsigned).** - - Same schema as public, but **do not sign**. `content` is Base64(binary age v1 `tlock` ciphertext) and the `tlock` tag is present. **Omit `p`**. - - **Rumor MUST NOT include `sig`.** **`id` MAY be present**; if present, clients **MUST** recompute it after recovery and reject on mismatch. - -2. **Seal (kind:13).** - - JSON-serialize the rumor and encrypt it to the **recipient** using **NIP-44 v2**; put the ciphertext in `.content`. **`tags` MUST be `[]`**. **Sign with the author’s real key.** - -3. **Gift wrap (kind:1059).** - - JSON-serialize the **seal** and encrypt it to the **recipient** using **NIP-44 v2** with a **one-time ephemeral** key; put the ciphertext in `.content`. Add at least one `["p","",""]` (one 1059 per recipient is best practice). **Sign with the ephemeral key.** - - Broadcast only to the recipient’s **DM relays** as advertised by their relay list metadata (per the relevant NIP). - -### Minimal examples (structure only) - -**Rumor (kind:1041, unsigned):** - -```json -{ - "id": "<32-byte lowercase hex sha256 of serialized event>", - "pubkey": "", - "created_at": 1234567890, - "kind": 1041, - "tags": [ - ["tlock", "", ""], - ["alt", ""] - ], - "content": "" -} -``` - -**Seal (kind:13, signed by author; `tags = []`):** - -```json -{ - "id": "<32-byte lowercase hex sha256 of serialized event>", - "pubkey": "", - "created_at": 1234567890, - "kind": 13, - "tags": [], - "content": "", - "sig": "" -} -``` - -**Gift wrap (kind:1059, signed by ephemeral; includes `p`):** - -```json -{ - "id": "<32-byte lowercase hex sha256 of serialized event>", - "pubkey": "", - "created_at": 1234567890, - "kind": 1059, - "tags": [["p", "", ""]], - "content": "", - "sig": "" -} -``` - ---- - -## Decryption & validation (client-side) - -### Public 1041 - -1. Verify **NIP-01** signature; check **exactly one** `tlock` tag; Base64-decode `content`. -2. Fetch the drand beacon for `drand_round_uint` and **verify** it against the chain’s BLS public key derived from `drand_chain_hex64`. -3. Parse the **binary** age v1 ciphertext; ensure **exactly one** recipient stanza of type `tlock` whose chain/round **match the tag**; reject ASCII armor or extra recipient types. -4. Decrypt with the verified beacon; the result is the plaintext. - -### Private (1059 → 13 → 1041) - -1. Validate outer **1059** (ephemeral **NIP-01** signature); **NIP-44 v2** decrypt `.content` with your key. -2. Parse inner **kind:13**; **`tags` MUST be empty**; verify **author** signature; **NIP-44 v2** decrypt `.content` using the author↔recipient conversation key. -3. Parse recovered **unsigned kind:1041 rumor**. **Verify** `lower(seal.pubkey) == lower(rumor.pubkey)` (both 32-byte lowercase hex). If `rumor.id` is present, **recompute** and reject on mismatch. For display and ordering, **use `rumor.created_at`**; the `created_at` of the seal and wrap are transport metadata and **MUST NOT** replace the rumor’s timestamp in UX. -4. Fetch & verify drand beacon as above; ensure `tlock` tag ↔ age stanza chain/round match; then age-decrypt to recover the plaintext. - ---- - -## Relay semantics - -- Relays **MUST NOT** attempt to decrypt or enforce unlock times. -- Clients **MUST** enforce unlock using **verified** drand beacons, **not** local clocks. - ---- - -## Security considerations - -- **Beacon verification:** Always verify drand beacons against the chain’s BLS public key (derived from `drand_chain_hex64`) before age decryption. Do **not** trust local time or unsigned beacons; accept the first BLS-verified beacon from any endpoint. -- **Ciphertext format:** Accept **only** binary age v1 `tlock` with **exactly one** recipient stanza; **reject** ASCII-armored inputs and stanza multiplicity or other stanza types. -- **Bounds & DoS:** Before allocation, clients **SHOULD** enforce `tlock_blob ≤ 4096 bytes` and **SHOULD** reject 1041 whose **decoded** `content` exceeds **64 KiB**. Relays **MAY** drop 1041 exceeding **256 KiB** decoded. -- **Sealing/wrapping crypto:** Use **NIP-44 v2** (ECDH → HKDF, ChaCha20, HMAC, padded Base64). Validate MAC in constant time **before** attempting decryption. -- **Timestamps & privacy:** Randomize seal/wrap `created_at` slightly (e.g., jitter/backdate) for metadata privacy; the rumor’s `created_at` is canonical for UX. - ---- - -## Implementations - -- **Relay** [**Shugur Relay**] () -- **Client** [**Shugur Time Capsules**] () From 3792e229fb5473037742c9a36b44dced72c0b6c1 Mon Sep 17 00:00:00 2001 From: karim Hassan Date: Mon, 27 Oct 2025 12:48:31 +0300 Subject: [PATCH 05/10] Refactor NIP-ZZ: Update DNS TXT record format and improve clarity in documentation --- NIP-ZZ.md | 235 +++++++++++++++++------------------------------------- 1 file changed, 73 insertions(+), 162 deletions(-) diff --git a/NIP-ZZ.md b/NIP-ZZ.md index 18d7014e30..6b0770583e 100644 --- a/NIP-ZZ.md +++ b/NIP-ZZ.md @@ -10,10 +10,10 @@ This NIP standardizes DNS TXT record format for bootstrapping Nostr Web Pages cl To make Nostr Web Pages accessible via traditional domain names, this NIP defines a DNS TXT record format at `_nweb.` that provides: -- Site author's public key (for event verification) +- Site public key (for event verification) - Relay URLs where site events are published -The DNS record does NOT contain the site index event ID. Instead, clients query relays for the most recent entrypoint (kind 11126) from the specified pubkey, which points to the current site index (kind 31126), enabling automatic updates without DNS changes. +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 @@ -40,54 +40,46 @@ Examples: ### Record Value -A single-line JSON string with the following fields: +A space-separated key-value token string with the following format: -| Field | Type | Required | Description | -| -------- | ------- | -------- | -------------------------------------- | -| `v` | integer | Yes | Schema version (currently `1`) | -| `pk` | string | Yes | Site pubkey (npub or hex) | -| `relays` | array | Yes | WSS relay URLs (at least one) | -| `policy` | object | No | Client hints (reserved for future use) | +``` +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:** -```json -{ - "v": 1, - "pk": "npub1abc123...", - "relays": ["wss://relay.damus.io"] -} +``` +v=1 pk=npub1abc123... relays=wss://relay.example.com ``` -**Complete record:** +**Multiple relays:** -```json -{ - "v": 1, - "pk": "5e56a2e48c4c5eb902e062bc30f92eabcf2e2fb96b5e7...", - "relays": ["wss://relay.damus.io", "wss://nos.lol", "wss://relay.nostr.band"], - "policy": { - "min_relays": 2 - } -} +``` +v=1 pk=5e56a2e48c4c5eb902e062bc30f92eabcf2e2fb96b5e7... relays=wss://relay.example.com,wss://relay.example2.com,wss://relay.example3.com ``` ## Public Key Format -The `pk` field accepts two formats: +The `pk=` value accepts two formats: **npub (bech32):** -```json -"pk": "npub1abc123def456..." +``` +v=1 pk=npub1abc123def456... relays=wss://relay.example.com ``` **Hex:** -```json -"pk": "5e56a2e48c4c5eb902e062bc30f92eabcf2e2fb96b5e7..." +``` +v=1 pk=5e56a2e48c4c5eb902e062bc30f92eabcf2e2fb96b5e7... relays=wss://relay.example.com ``` Clients MUST support both formats. @@ -99,110 +91,20 @@ Clients MUST support both formats. - MUST use `wss://` scheme (WebSocket Secure) - MUST be valid WebSocket URLs - SHOULD be publicly accessible +- Comma-separated in the `relays=` value -**Examples:** +**Example:** -```json -"relays": [ - "wss://relay.damus.io", - "wss://relay.nostr.band", - "wss://nos.lol" -] +``` +v=1 pk=npub1... relays=wss://relay.example.com,wss://relay.example3.com,wss://relay.example2.com ``` **Recommendations:** -- Include at least 2-3 relays for redundancy +- 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) - -## Policy Object - -Reserved for future client hints: - -```json -"policy": { - "min_relays": 2, - "ttl": 3600, - "cache_strategy": "aggressive" -} -``` - -Current version ignores this field. Future NIPs may define standard policy fields. - -## Why No Site Index in DNS? - -**Design Decision:** The DNS record intentionally omits the site index event ID. This is a key architectural choice with significant benefits: - -### Benefits of Query-Based Discovery - -1. **Zero DNS Updates After Initial Setup** - - - DNS is configured once with pubkey and relays - - All content updates happen via event publishing only - - No waiting for DNS propagation (which can take hours) - - No risk of DNS misconfiguration breaking updates - -2. **Instant Content Updates** - - - Publishers republish by creating new events with fresh timestamps - - Clients query for latest event by `created_at` - - Updates visible immediately (relay propagation is seconds, not hours) - -3. **Supports Multiple Sites Per Pubkey** - - - Multiple domains can point to the same pubkey - - Each site uses different `d` tags for site indices - - Clients distinguish sites by domain name, not just pubkey - -4. **Resilient to DNS Poisoning** - - - Attacker can't point DNS to old/malicious site version - - DNS only contains pubkey (identity), not content pointer - - Latest content is always determined by on-chain timestamps - -5. **Simpler Publisher Workflow** - - No need to update DNS TXT record on every publish - - No need to wait for propagation to verify changes - - Reduces human error in DNS management - -### How It Works - -``` -┌─────────────┐ -│ Browser │ -└──────┬──────┘ - │ - │ 1. User visits example.com - ▼ -┌─────────────────┐ -│ DNS Lookup │ ◄─── Query _nweb.example.com -│ (ONE TIME) │ Returns: {pk, relays} -└────────┬────────┘ - │ - │ 2. Connect to relays - ▼ -┌──────────────────┐ -│ Query Entrypoint │ ◄─── Filter: kind 11126, author=pk -│ (EVERY VISIT) │ Get latest replaceable event -└────────┬─────────┘ - │ - │ 3. Extract site index address from 'a' tag - ▼ -┌──────────────────┐ -│ Fetch Site Index │ ◄─── Get addressable event: 31126:pk:d-hash -│ (kind 31126) │ Contains route mappings -└────────┬─────────┘ - │ - │ 4. Got latest site index! - ▼ -┌──────────────────┐ -│ Load Site │ -│ Content │ -└──────────────────┘ -``` - -Publishers republish → New entrypoint with newer timestamp → Points to new site index → Clients automatically fetch the new version. +- No spaces allowed in relay URLs (or use percent-encoding) ## Client Behavior @@ -210,16 +112,17 @@ Publishers republish → New entrypoint with newer timestamp → Points to new s 1. User navigates to `example.com` 2. Client checks for `_nweb.example.com` TXT record -3. If found, parse JSON and validate schema +3. If found, parse the tokenized string and validate 4. If not found or invalid, handle as regular HTTP navigation ### Record Validation **Required checks:** -- `v` field equals `1` -- `pk` field is valid npub or hex pubkey -- `relays` array has at least one valid `wss://` URL +- 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:** @@ -275,7 +178,7 @@ This two-step approach (Entrypoint → Site Index) ensures clients always load t All fetched events MUST be authored by the pubkey from DNS record: ```javascript -if (event.pubkey !== dnsRecord.pk) { +if (event.pubkey !== parsedDnsRecord.pubkey) { throw new Error("Event author does not match DNS pubkey"); } ``` @@ -284,24 +187,16 @@ This prevents relay impersonation attacks. ## DNS Provider Considerations -### JSON Formatting - -Some DNS providers require specific formatting: +### Token Formatting -**Single-line JSON:** - -``` -{"v":1,"pk":"npub1...","relays":["wss://relay.damus.io"]} -``` +The tokenized format is simpler than JSON and works universally across DNS providers: -**Escaped quotes (automatic by some providers):** +**Standard format:** ``` -{\"v\":1,\"pk\":\"npub1...\",\"relays\":[\"wss://relay.damus.io\"]} +v=1 pk=npub1... relays=wss://relay.example.com,wss://relay.example2.com ``` -Verify the actual TXT record value resolves to valid JSON. - ### Record Length Limits TXT records have size limits: @@ -311,9 +206,9 @@ TXT records have size limits: For large records, consider: -- Using hex pubkey instead of npub (shorter) -- Limiting relay array length -- Omitting optional fields +- Using hex pubkey instead of npub (slightly shorter) +- Limiting the number of relay URLs +- Prioritizing the most reliable relays ## DNSSEC @@ -335,7 +230,7 @@ Clients SHOULD verify DNSSEC signatures when available. ``` Type: TXT Name: _nweb -Content: {"v":1,"pk":"npub1...","relays":["wss://relay.damus.io"]} +Content: v=1 pk=npub1... relays=wss://relay.example.com TTL: Auto or 3600 (DNS rarely needs updating) ``` @@ -344,7 +239,7 @@ TTL: Auto or 3600 (DNS rarely needs updating) ``` Type: TXT Name: _nweb.example.com -Value: "{"v":1,"pk":"npub1...","relays":["wss://relay.damus.io"]}" +Value: "v=1 pk=npub1... relays=wss://relay.example.com" TTL: 3600 (DNS rarely needs updating) ``` @@ -357,17 +252,39 @@ const response = await fetch( ); const data = await response.json(); -// Parse TXT record -const txtRecord = data.Answer?.[0]?.data; -const nwebConfig = JSON.parse(txtRecord.replace(/^"|"$/g, "")); +// Parse TXT record (remove quotes if present) +const txtRecord = data.Answer?.[0]?.data.replace(/^"|"$/g, ""); + +// Parse key-value tokens +const tokens = txtRecord.split(/\s+/); +const params = {}; -// Validate -if (nwebConfig.v !== 1) throw new Error("Unsupported version"); -if (!nwebConfig.pk || !nwebConfig.relays) throw new Error("Invalid record"); +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(",").filter((r) => r.startsWith("wss://")); + +if (relays.length === 0) { + throw new Error("Invalid record: no valid relay URLs"); +} // Use config -const pubkey = parseNpub(nwebConfig.pk); -const relays = nwebConfig.relays; +const parsedPubkey = parseNpub(pubkey); // handles both npub and hex ``` ## Security Considerations @@ -419,9 +336,3 @@ If all listed relays censor content: - 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 - -**Key Insight:** Separating DNS bootstrap (pubkey + relays) from content discovery (query latest entrypoint → site index) means: - -- DNS is configured once during initial setup -- All subsequent site updates are instant (no DNS changes needed) -- Clients automatically fetch the newest version from relays From 1e53eb79fcf9e3b9442c8df1a83972e8fc1db034 Mon Sep 17 00:00:00 2001 From: karim Hassan Date: Mon, 27 Oct 2025 12:54:24 +0300 Subject: [PATCH 06/10] Fix relay URLs in NIP-ZZ examples for consistency and clarity --- NIP-ZZ.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/NIP-ZZ.md b/NIP-ZZ.md index 6b0770583e..bcb85d1489 100644 --- a/NIP-ZZ.md +++ b/NIP-ZZ.md @@ -63,7 +63,7 @@ v=1 pk=npub1abc123... relays=wss://relay.example.com **Multiple relays:** ``` -v=1 pk=5e56a2e48c4c5eb902e062bc30f92eabcf2e2fb96b5e7... relays=wss://relay.example.com,wss://relay.example2.com,wss://relay.example3.com +v=1 pk=5e56a2e48c4c5eb902e062bc30f92eabcf2e2fb96b5e7... relays=wss://relay1.example.com,wss://relay2.example.com,wss://relay3.example.com ``` ## Public Key Format @@ -96,7 +96,7 @@ Clients MUST support both formats. **Example:** ``` -v=1 pk=npub1... relays=wss://relay.example.com,wss://relay.example3.com,wss://relay.example2.com +v=1 pk=npub1... relays=wss://relay1.example.com,wss://relay2.example.com,wss://relay3.example.com ``` **Recommendations:** @@ -194,7 +194,7 @@ The tokenized format is simpler than JSON and works universally across DNS provi **Standard format:** ``` -v=1 pk=npub1... relays=wss://relay.example.com,wss://relay.example2.com +v=1 pk=npub1... relays=wss://relay1.example.com,wss://relay2.example.com ``` ### Record Length Limits From 0ad82be2e748e7180f2da0c9acab6dc38e843038 Mon Sep 17 00:00:00 2001 From: karim Hassan Date: Mon, 27 Oct 2025 12:54:43 +0300 Subject: [PATCH 07/10] Update MIME type examples and add image asset reference in NIP-YY documentation --- NIP-YY.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NIP-YY.md b/NIP-YY.md index d9a3e976c3..6b332e3348 100644 --- a/NIP-YY.md +++ b/NIP-YY.md @@ -36,7 +36,7 @@ Content-addressed assets for web pages. All web assets (HTML, CSS, JavaScript, f **Required tags:** -- `m` - MIME type (e.g., `text/html`, `text/css`, `text/javascript`, `application/wasm`, `font/woff2`) +- `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:** @@ -89,6 +89,7 @@ Regular event that links assets for a specific page. Each page version is a sepa ["e", "", "wss://relay.example.com"], ["e", "", "wss://relay.example.com"], ["e", "", "wss://relay.example.com"], + ["e","", "wss://relay.example.com"] ["route", "/"], ["title", "Home"], ["description", "Welcome to my Nostr Web site"] From e7aa560f2bc2a946c3c67821179ae46b72a49cc2 Mon Sep 17 00:00:00 2001 From: karim Hassan Date: Mon, 27 Oct 2025 13:26:30 +0300 Subject: [PATCH 08/10] Refactor NIP-YY: Update site index versioning and route mapping for improved clarity and functionality --- NIP-YY.md | 85 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 46 insertions(+), 39 deletions(-) diff --git a/NIP-YY.md b/NIP-YY.md index 6b332e3348..95a809a175 100644 --- a/NIP-YY.md +++ b/NIP-YY.md @@ -102,28 +102,24 @@ Regular event that links assets for a specific page. Each page version is a sepa ### Site Index (31126) -Addressable event that maps routes to their current page manifest IDs. The `d` tag uses a truncated hash (like Git short hashes) for content-addressed versioning. +Addressable event that maps routes to their current page manifest IDs. The `d` tag is used for versioning. **Required tags:** -- `d` - First 7-12 characters of the SHA-256 hash of the `content` field (e.g., `"a1b2c3d"`, `"a1b2c3d4e5f6"`) -- `x` - Full hex-encoded SHA-256 hash of the `content` field (to verify the `d` tag is correctly derived) +- `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:** -- `alt` - Human-readable identifier (e.g., `"main"`, `"staging"`, `"v1.2.3"`) for convenience - -**Content:** JSON object with route mappings and optional metadata - -**Required fields:** - -- `routes` - Object mapping route paths to page manifest event IDs (kind 1126) - -**Optional fields:** - -- `version` - Semantic version string (e.g., `"1.2.3"`) for tracking site versions -- `defaultRoute` - Default route to display when no specific route is requested (e.g., `"/"`) -- `notFoundRoute` - Route to display for 404 errors (e.g., `"/404"`) or `null` if not specified +- `title` - Site title +- `description` - Site description **Example:** @@ -133,20 +129,26 @@ Addressable event that maps routes to their current page manifest IDs. The `d` t "pubkey": "", "created_at": 1234567890, "tags": [ - ["d", "a1b2c3d"], - ["x", "a1b2c3d4e5f6789...full-hash..."], - ["alt", "main"] + ["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": "{ - \"routes\": { - \"/\": \"\", - \"/about\": \"\", - \"/blog/post-1\": \"\" - }, - \"version\": \"1.2.3\", - \"defaultRoute\": \"/\", - \"notFoundRoute\": \"/404\" - }", + "content": "", "id": "", "sig": "" } @@ -158,8 +160,8 @@ Replaceable event that points to the current site index. Only the latest event p **Required tags:** -- `a` - Address coordinates to the current site index: `["a", "31126::", ""]` - - The `` is the truncated hash used in the site index's `d` tag +- `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:** @@ -168,7 +170,7 @@ Replaceable event that points to the current site index. Only the latest event p "kind": 11126, "pubkey": "", "created_at": 1234567890, - "tags": [["a", "31126::a1b2c3d", "wss://relay.example.com"]], + "tags": [["a", "31126::v1.2.3", "wss://relay1.example.com"]], "content": "", "id": "", "sig": "" @@ -182,8 +184,8 @@ Replaceable event that points to the current site index. Only the latest event p 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 -5. Update entrypoint (11126) to point to the current site index +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 @@ -192,8 +194,13 @@ Replaceable event that points to the current site index. Only the latest event p 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 content to extract `routes`, `version`, `defaultRoute`, and `notFoundRoute` fields -6. Get page manifest ID for requested route from `content.routes` +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.) @@ -212,9 +219,9 @@ Replaceable event that points to the current site index. Only the latest event p - 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) MUST include `x` tag with SHA-256 hash of the content - - The `x` tag is used to verify that the `d` tag (truncated hash) is correctly derived from the full hash - - Clients SHOULD verify that the first 7-12 characters of the `x` tag match the `d` tag +- 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 **Content Security Policy:** From 0351518c3a962b8641d9fac9e801f98d6c847d27 Mon Sep 17 00:00:00 2001 From: karim Hassan Date: Mon, 27 Oct 2025 13:26:55 +0300 Subject: [PATCH 09/10] Update NIP-ZZ: Change DNS TXT record format to semicolon-separated key-value pairs for improved clarity --- NIP-ZZ.md | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/NIP-ZZ.md b/NIP-ZZ.md index bcb85d1489..0700ca5100 100644 --- a/NIP-ZZ.md +++ b/NIP-ZZ.md @@ -40,10 +40,10 @@ Examples: ### Record Value -A space-separated key-value token string with the following format: +A semicolon-separated key-value token string with the following format: ``` -v= pk= relays=[,,...] +v=;pk=;relays=[,,...] ``` | Component | Required | Description | @@ -57,13 +57,13 @@ v= pk= relays=[,,...] **Minimal record:** ``` -v=1 pk=npub1abc123... relays=wss://relay.example.com +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 +v=1;pk=5e56a2e48c4c5eb902e062bc30f92eabcf2e2fb96b5e7...;relays=wss://relay1.example.com,wss://relay2.example.com,wss://relay3.example.com ``` ## Public Key Format @@ -73,13 +73,13 @@ The `pk=` value accepts two formats: **npub (bech32):** ``` -v=1 pk=npub1abc123def456... relays=wss://relay.example.com +v=1;pk=npub1abc123def456...;relays=wss://relay.example.com ``` **Hex:** ``` -v=1 pk=5e56a2e48c4c5eb902e062bc30f92eabcf2e2fb96b5e7... relays=wss://relay.example.com +v=1;pk=5e56a2e48c4c5eb902e062bc30f92eabcf2e2fb96b5e7...;relays=wss://relay.example.com ``` Clients MUST support both formats. @@ -96,7 +96,7 @@ Clients MUST support both formats. **Example:** ``` -v=1 pk=npub1... relays=wss://relay1.example.com,wss://relay2.example.com,wss://relay3.example.com +v=1;pk=npub1...;relays=wss://relay1.example.com,wss://relay2.example.com,wss://relay3.example.com ``` **Recommendations:** @@ -104,7 +104,7 @@ v=1 pk=npub1... relays=wss://relay1.example.com,wss://relay2.example.com,wss://r - 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) -- No spaces allowed in relay URLs (or use percent-encoding) +- Spaces after commas in relay list are allowed and will be trimmed during parsing ## Client Behavior @@ -194,9 +194,11 @@ The tokenized format is simpler than JSON and works universally across DNS provi **Standard format:** ``` -v=1 pk=npub1... relays=wss://relay1.example.com,wss://relay2.example.com +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: @@ -230,7 +232,7 @@ Clients SHOULD verify DNSSEC signatures when available. ``` Type: TXT Name: _nweb -Content: v=1 pk=npub1... relays=wss://relay.example.com +Content: v=1;pk=npub1...;relays=wss://relay.example.com TTL: Auto or 3600 (DNS rarely needs updating) ``` @@ -239,7 +241,7 @@ TTL: Auto or 3600 (DNS rarely needs updating) ``` Type: TXT Name: _nweb.example.com -Value: "v=1 pk=npub1... relays=wss://relay.example.com" +Value: "v=1;pk=npub1...;relays=wss://relay.example.com" TTL: 3600 (DNS rarely needs updating) ``` @@ -255,8 +257,8 @@ const data = await response.json(); // Parse TXT record (remove quotes if present) const txtRecord = data.Answer?.[0]?.data.replace(/^"|"$/g, ""); -// Parse key-value tokens -const tokens = txtRecord.split(/\s+/); +// Parse key-value tokens (semicolon-separated) +const tokens = txtRecord.split(";"); const params = {}; for (const token of tokens) { @@ -277,7 +279,10 @@ if (!params.pk || !params.relays) { // Extract pubkey and relays const pubkey = params.pk; -const relays = params.relays.split(",").filter((r) => r.startsWith("wss://")); +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"); From 19ea627736ad23d553af0ddbff94eea33e054039 Mon Sep 17 00:00:00 2001 From: karim Hassan Date: Mon, 27 Oct 2025 13:40:28 +0300 Subject: [PATCH 10/10] Refactor NIP-YY: Remove route from Page Manifest for clarity --- NIP-YY.md | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/NIP-YY.md b/NIP-YY.md index 95a809a175..627dbdd12a 100644 --- a/NIP-YY.md +++ b/NIP-YY.md @@ -74,8 +74,6 @@ Regular event that links assets for a specific page. Each page version is a sepa - `title` - Page title - `description` - Page description -- `route` - Page route/path for reference (e.g., `/`, `/about`) -- `csp` - Content Security Policy directives to override the default CSP for this specific page **Example:** @@ -89,8 +87,7 @@ Regular event that links assets for a specific page. Each page version is a sepa ["e", "", "wss://relay.example.com"], ["e", "", "wss://relay.example.com"], ["e", "", "wss://relay.example.com"], - ["e","", "wss://relay.example.com"] - ["route", "/"], + ["e", "", "wss://relay.example.com"], ["title", "Home"], ["description", "Welcome to my Nostr Web site"] ], @@ -205,7 +202,7 @@ Replaceable event that points to the current site index. Only the latest event p 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 with CSP enforcement +11. Render in sandboxed environment ### Security Considerations @@ -223,21 +220,10 @@ Replaceable event that points to the current site index. Only the latest event p - 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 -**Content Security Policy:** - -- Default CSP: `default-src 'self'; script-src 'sha256-'` -- Per-page CSP can be specified via the `csp` tag in Page Manifest (1126) -- Custom CSP allows pages to: - - Allow specific external API connections (`connect-src`) - - Permit inline styles if needed (`style-src`) - - Control frame embedding (`frame-ancestors`) -- Clients SHOULD enforce CSP to prevent code injection -- If a page has a `csp` tag, it overrides the default CSP for that page only - **Sandboxing:** - Content SHOULD be rendered in isolated environment (iframe sandbox) -- Network requests outside Nostr/Blossom SHOULD be blocked +- Network requests outside Nostr SHOULD be blocked ## Caching