From be4d3b1b88f2c545df0dbd015d2d07f302f413fd Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Mon, 27 Jul 2026 18:49:45 -0400 Subject: [PATCH 1/4] Draft CLINK Enroll spec (kind 21004) and README links. Co-authored-by: Cursor --- README.md | 7 +- specs/clink-enroll.md | 148 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 specs/clink-enroll.md diff --git a/README.md b/README.md index b3e0a07..d084960 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ Where NWC is deferential to LNURL and scoped for a specific task, **CLINK is fun - [CLINK Offers](specs/clink-offers.md): Static payment codes (`noffer1...`) analogous to LNURL-Pay but entirely Nostr-native. Enables invoice generation via Nostr direct messages without a publicly accessible HTTPS endpoint. Services like Lightning.Pub can trigger webhooks on offer events for easy integration while maintaining self-custody. - [CLINK Debits](specs/clink-debits.md): Static authorization pointers (`ndebit1...`) for direct, secure payment requests between parties via key-based identity and event-based authorization flows. - [CLINK Manage](specs/clink-manage.md): Delegated management (`nmanage1...`), e.g., external apps managing offers for a user. +- [CLINK Enroll](specs/clink-enroll.md): Bootstrap an account on a node service for a Nostr key and receive default `noffer` / `ndebit` / `nmanage` pointers (kind `21004`). Direct-use only — does not mint third-party grants. ## Event Kinds @@ -52,16 +53,18 @@ Where NWC is deferential to LNURL and scoped for a specific task, **CLINK is fun | 21001 | Offer Request/Response | [CLINK Offers](specs/clink-offers.md) | | 21002 | Debit Request/Response | [CLINK Debits](specs/clink-debits.md) | | 21003 | Management Delegation | [CLINK Manage](specs/clink-manage.md) | +| 21004 | Enroll / account bootstrap | [CLINK Enroll](specs/clink-enroll.md) | ## Ecosystem | Project | Type | Supports | Features / Notes | |--------------|---------|----------------|------------------| -| [Lightning.Pub](https://lightning.pub) | Server | Offers, Debits | Reference server for wallets. | +| [Lightning.Pub](https://lightning.pub) | Server | Offers, Debits, Manage, Enroll | Reference server for wallets. | | [ShockWallet](https://shockwallet.app) | Wallet | Offers, Debits | Pay offers and manage your offers and requests via Lightning.Pub. | | [Zeus Wallet](https://zeusln.com) | Wallet | Offers | Pay offers, ZEUS Pay users get an offer by default. | | [Bridgelet](https://github.com/shocknet/bridgelet) | Bridge | Offers | Simple NIP-05, LNURL and Lightning Address bridge for your custom domain, uses Offers to fetch invoices from your node. | -| [CLINK SDK](https://www.npmjs.com/package/@shocknet/clink-sdk) | SDK | Offers, Debits | JS/TS library for CLINK integration. | +| [CLINK SDK](https://www.npmjs.com/package/@shocknet/clink-sdk) | SDK | Offers, Debits, Manage | JS/TS library for CLINK integration. | +| [clink-go](https://github.com/shocknet/clink-go) | SDK + CLI | Offers, Debits, Manage, Enroll | Go SDK and `clinkctl` CLI. | | [Stacker.News](https://stacker.news) | Message Board | Offers, Debits | Attach a wallet via CLINK to send and receive zaps. | | [clinkme.dev](https://clinkme.dev) | Web Demo | Offers, Debits | Demo of a static website using CLINK Offers. | | [bxrd.app](https://bxrd.app) | Nostr Client | Offers, Debits | A graph-based Nostr Client with Debit integration for Zaps. | diff --git a/specs/clink-enroll.md b/specs/clink-enroll.md new file mode 100644 index 0000000..0d56889 --- /dev/null +++ b/specs/clink-enroll.md @@ -0,0 +1,148 @@ +# CLINK Enroll Specification + +## Overview + +**CLINK Enroll** binds a Nostr key to an account (pointer) on a node service and returns that account’s default static CLINK pointers (`noffer1…`, `ndebit1…`, `nmanage1…`). + +It is the bootstrap step for headless clients, CLIs, and agents that act **as** the user (direct-use principal). It does **not** grant third parties spend or manage rights. + +## Motivation + +Offers, Debits, and Manage all require a service pubkey, relay, and account pointer. Without Enroll, clients must use proprietary wallet RPC (e.g. Lightning.Pub `GetUserInfo`) to create an account and learn those pointers. + +Enroll makes account provisioning a portable CLINK protocol so a client can: + +1. Talk to a service (`nprofile` / pubkey + relay) +2. Ensure an account exists for its signing key +3. Receive default `noffer` / `ndebit` / `nmanage` strings +4. Proceed with kinds 21001–21003 only + +## Non-goals + +Enroll MUST NOT: + +- Create debit authorizations for other npubs +- Create manage authorizations for other npubs +- Expose balance, history, on-chain, or general wallet RPC +- Replace [CLINK Debits](clink-debits.md) or [CLINK Manage](clink-manage.md) + +Third-party spend/manage remains Debits / Manage with explicit grants. Owner self-use is defined under **Owner policy** below (and MAY be implemented as server policy on 21002 / 21003 without a separate Enroll action). + +## Target + +Input is the **node service** identity only: + +- Service pubkey (32-byte) +- Relay URL(s) where the service listens + +There is no separate “app” pubkey in the pointer model. Account identity on the service is an opaque **pointer** (user id) chosen by the service. + +## Nostr Events + +- **Kind:** `21004` (CLINK Enroll) +- **Tags (request):** + - `["p", ""]` + - `["clink_version", "1"]` +- **Tags (response):** + - `["p", ""]` + - `["e", ""]` + - `["clink_version", "1"]` +- **Content:** NIP-44 encrypted JSON (same pattern as Offers / Debits / Manage) + +## Request + +Empty object or optional fields: + +```json +{} +``` + +Optional: + +```json +{ + "preferred_pointer": "" +} +``` + +Services MAY ignore `preferred_pointer`. Services MUST associate the resulting account with the **request event’s pubkey**. + +## Response (success) + +```json +{ + "pointer": "", + "noffer": "noffer1...", + "ndebit": "ndebit1...", + "nmanage": "nmanage1..." +} +``` + +| Field | Requirement | +|-------|-------------| +| `pointer` | Opaque account id used as TLV `2` in the returned bech32s | +| `noffer` | Default spontaneous (or service-default) offer for this account | +| `ndebit` | Static debit pointer for this account | +| `nmanage` | Manage pointer for this account | + +Returned bech32s MUST use: + +- TLV `0` = **this service’s** pubkey +- TLV `1` = a relay the service listens on (typically the relay used for the request) +- TLV `2` = `pointer` (where the format includes a pointer) + +Idempotency: repeating Enroll with the same key MUST return the same `pointer` and equivalent pointers (bech32 strings MAY differ only if relay preference changes). + +## Response (error) + +```json +{ + "code": 1, + "error": "human readable reason" +} +``` + +Suggested codes (align with other CLINK specs where practical): + +| Code | Meaning | +|------|---------| +| 1 | Denied / not allowed | +| 2 | Rate limited | +| 3 | Service unavailable | + +## Owner policy (normative for reference servers) + +When the signer of a kind **21002** (Debit) or **21003** (Manage) request is the Nostr key that **owns** the account identified by the pointer (the same key that Enrolled): + +- The service MUST allow the operation without a prior third-party debit/manage authorization grant. +- This does **not** authorize any other pubkey. + +Marketplace / agent keys that are **not** the account owner still require normal Debit / Manage authorization flows. + +## Client flow + +``` +nprofile (service) + user key + │ + ▼ + kind 21004 Enroll + │ + ▼ + noffer / ndebit / nmanage + │ + ├── 21001 pay-into offer (others → you) + ├── 21002 pay-out via ndebit (you → invoice) [owner auto-allow] + └── 21003 create offers via nmanage [owner auto-allow] +``` + +## Security considerations + +- Enroll proves control of a key and creates (or resumes) a custodial-or-self-hosted account on the service; services SHOULD rate-limit and MAY require invites. +- Returned `ndebit` is powerful for the **owner key** under owner policy; clients MUST treat the secret key as a full account credential. +- Do not overload Enroll with grant minting — that recreates ambient authority and breaks Manage’s delegation model. + +## Reference implementation + +- Spec repo: this document +- Go CLI/SDK: [clink-go](https://github.com/shocknet/clink-go) (`clinkctl enroll`) +- Reference server: [Lightning.Pub](https://github.com/shocknet/Lightning.Pub) From 373ece780a941b954624120a46b9e83e8f3b8424 Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Tue, 28 Jul 2026 10:35:54 -0400 Subject: [PATCH 2/4] Enroll: require NIP-13 PoW (default 18 bits) against spam. Co-authored-by: Cursor --- specs/clink-enroll.md | 58 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/specs/clink-enroll.md b/specs/clink-enroll.md index 0d56889..5e6581f 100644 --- a/specs/clink-enroll.md +++ b/specs/clink-enroll.md @@ -43,12 +43,55 @@ There is no separate “app” pubkey in the pointer model. Account identity on - **Tags (request):** - `["p", ""]` - `["clink_version", "1"]` + - `["nonce", "", ""]` — [NIP-13](https://github.com/nostr-protocol/nips/blob/master/13.md) proof of work (see **Proof of work**) - **Tags (response):** - `["p", ""]` - `["e", ""]` - `["clink_version", "1"]` - **Content:** NIP-44 encrypted JSON (same pattern as Offers / Debits / Manage) +## Proof of work + +Enroll creates (or resumes) an account. Unbounded free enroll is a DoS / spam vector. Request events MUST carry [NIP-13](https://github.com/nostr-protocol/nips/blob/master/13.md) proof of work so mass scripted enrollment is expensive, while a single enroll on a phone, browser tab, or low-resource agent stays interactive. + +### Rules + +1. The kind `21004` **request** event MUST include a NIP-13 `nonce` tag: `["nonce", "", ""]`. +2. The event id MUST have at least `target_difficulty` leading zero bits. +3. `target_difficulty` in the tag MUST be ≥ the service’s required difficulty (committed target — services MUST reject “lucky” high-difficulty ids that commit to a lower target). +4. Services MUST reject requests that fail (1)–(3), preferably with error code `4` and `required_difficulty` so clients can remine once. + +### Recommended difficulty + +| Setting | Bits | Intent | +|---------|------|--------| +| **Minimum allowed** | 16 | Floor for public enroll (~65k hashes expected) | +| **Recommended default** | **18** | Deterrent for naive scripts; typically well under ~1s on a mid-range phone / browser WASM / small VPS agent | +| **Stronger public** | 20 | ~1M hashes expected; still usually a few seconds on a phone, not “forever” | +| **Avoid by default** | ≥ 22 | Fine for invite-gated or desktop-only services; too slow for shitty phones / casual web tabs | + +Expected work scales as `2^bits` SHA-256 event-id trials. One extra bit ≈ 2× wall time. + +Reference servers SHOULD default to **18** and MAY raise toward **20** under load. They MUST NOT require ≥ 22 for open public enroll unless they document that low-power clients are unsupported or offer an invite / lower-difficulty path. + +### Existing accounts + +If the requestor pubkey already owns an account on the service, the service MAY: + +- accept the same PoW requirement, or +- accept a lower difficulty (including `0`) for idempotent “return my pointers” re-enroll + +New account creation MUST still meet the full required difficulty. + +### Advertising difficulty + +Services SHOULD advertise `required_difficulty` so clients mine once: + +- in error responses when PoW is insufficient (see below), and/or +- out of band (e.g. service kind `0` / documentation) + +Clients SHOULD mine at the advertised value (or the recommended default **18** if unknown), and on code `4` remine at `required_difficulty`. + ## Request Empty object or optional fields: @@ -97,11 +140,14 @@ Idempotency: repeating Enroll with the same key MUST return the same `pointer` a ```json { - "code": 1, - "error": "human readable reason" + "code": 4, + "error": "insufficient proof of work", + "required_difficulty": 18 } ``` +`required_difficulty` MUST be present when `code` is `4`. + Suggested codes (align with other CLINK specs where practical): | Code | Meaning | @@ -109,6 +155,7 @@ Suggested codes (align with other CLINK specs where practical): | 1 | Denied / not allowed | | 2 | Rate limited | | 3 | Service unavailable | +| 4 | Insufficient NIP-13 proof of work (see `required_difficulty`) | ## Owner policy (normative for reference servers) @@ -125,8 +172,12 @@ Marketplace / agent keys that are **not** the account owner still require normal nprofile (service) + user key │ ▼ + mine NIP-13 PoW (default 18 bits) on kind 21004 + │ + ▼ kind 21004 Enroll │ + ├─ code 4 → remine at required_difficulty → retry ▼ noffer / ndebit / nmanage │ @@ -137,7 +188,8 @@ nprofile (service) + user key ## Security considerations -- Enroll proves control of a key and creates (or resumes) a custodial-or-self-hosted account on the service; services SHOULD rate-limit and MAY require invites. +- Enroll proves control of a key and creates (or resumes) a custodial-or-self-hosted account on the service; services SHOULD rate-limit **and** require NIP-13 PoW for new accounts. PoW alone is not enough under a well-funded attacker — combine with rate limits / invites. +- Recommended **18-bit** PoW aims for “annoying to spray thousands of accounts, fine for one human or agent on a phone.” Do not push public defaults into “wait forever on a weak device” territory (≥ 22). - Returned `ndebit` is powerful for the **owner key** under owner policy; clients MUST treat the secret key as a full account credential. - Do not overload Enroll with grant minting — that recreates ambient authority and breaks Manage’s delegation model. From 4b3964373ee5d29b80a4e40af9450b74f2daade7 Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Wed, 29 Jul 2026 00:47:03 -0400 Subject: [PATCH 3/4] beacon --- README.md | 7 +- specs/clink-beacon.md | 277 ++++++++++++++++++++++++++++++++++++++++++ specs/clink-enroll.md | 58 ++++----- 3 files changed, 311 insertions(+), 31 deletions(-) create mode 100644 specs/clink-beacon.md diff --git a/README.md b/README.md index d084960..bf0d2fd 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,8 @@ Where NWC is deferential to LNURL and scoped for a specific task, **CLINK is fun - [CLINK Offers](specs/clink-offers.md): Static payment codes (`noffer1...`) analogous to LNURL-Pay but entirely Nostr-native. Enables invoice generation via Nostr direct messages without a publicly accessible HTTPS endpoint. Services like Lightning.Pub can trigger webhooks on offer events for easy integration while maintaining self-custody. - [CLINK Debits](specs/clink-debits.md): Static authorization pointers (`ndebit1...`) for direct, secure payment requests between parties via key-based identity and event-based authorization flows. - [CLINK Manage](specs/clink-manage.md): Delegated management (`nmanage1...`), e.g., external apps managing offers for a user. -- [CLINK Enroll](specs/clink-enroll.md): Bootstrap an account on a node service for a Nostr key and receive default `noffer` / `ndebit` / `nmanage` pointers (kind `21004`). Direct-use only — does not mint third-party grants. +- [CLINK Enroll](specs/clink-enroll.md): Kind `21004` Enroll request/response — provision a Nostr key on a node service and receive default `noffer` / `ndebit` / `nmanage` pointers. Direct-use only; does not mint third-party grants. +- [CLINK Beacon](specs/clink-beacon.md): Optional kind `30078` heartbeat for service liveness, persona, fee disclosure, and Enroll PoW fast-path. ## Event Kinds @@ -53,7 +54,8 @@ Where NWC is deferential to LNURL and scoped for a specific task, **CLINK is fun | 21001 | Offer Request/Response | [CLINK Offers](specs/clink-offers.md) | | 21002 | Debit Request/Response | [CLINK Debits](specs/clink-debits.md) | | 21003 | Management Delegation | [CLINK Manage](specs/clink-manage.md) | -| 21004 | Enroll / account bootstrap | [CLINK Enroll](specs/clink-enroll.md) | +| 21004 | Enroll (Account Request) | [CLINK Enroll](specs/clink-enroll.md) | +| 30078 | Service beacon (NIP-78) | [CLINK Beacon](specs/clink-beacon.md) | ## Ecosystem @@ -64,7 +66,6 @@ Where NWC is deferential to LNURL and scoped for a specific task, **CLINK is fun | [Zeus Wallet](https://zeusln.com) | Wallet | Offers | Pay offers, ZEUS Pay users get an offer by default. | | [Bridgelet](https://github.com/shocknet/bridgelet) | Bridge | Offers | Simple NIP-05, LNURL and Lightning Address bridge for your custom domain, uses Offers to fetch invoices from your node. | | [CLINK SDK](https://www.npmjs.com/package/@shocknet/clink-sdk) | SDK | Offers, Debits, Manage | JS/TS library for CLINK integration. | -| [clink-go](https://github.com/shocknet/clink-go) | SDK + CLI | Offers, Debits, Manage, Enroll | Go SDK and `clinkctl` CLI. | | [Stacker.News](https://stacker.news) | Message Board | Offers, Debits | Attach a wallet via CLINK to send and receive zaps. | | [clinkme.dev](https://clinkme.dev) | Web Demo | Offers, Debits | Demo of a static website using CLINK Offers. | | [bxrd.app](https://bxrd.app) | Nostr Client | Offers, Debits | A graph-based Nostr Client with Debit integration for Zaps. | diff --git a/specs/clink-beacon.md b/specs/clink-beacon.md new file mode 100644 index 0000000..7beaf31 --- /dev/null +++ b/specs/clink-beacon.md @@ -0,0 +1,277 @@ +# CLINK Beacon Specification + +## Overview + +**CLINK Beacon** is a replaceable Nostr event that a node service publishes to signal **liveness**, **persona** (display metadata), and optional **protocol parameters** to clients that already know the service pubkey and relay. + +It complements interactive CLINK protocols (kinds `21001`–`21004`) with a lightweight, subscription-friendly heartbeat clients can watch before opening encrypted round-trips. + +## Motivation + +Wallet and CLI clients need to know whether a node is reachable before Enroll, payment, or management flows. Polling proprietary HTTP health endpoints reintroduces web infrastructure CLINK avoids. + +A periodic beacon on the same relay the service already uses gives clients: + +1. **Onlineness** — fresh `created_at` ⇒ service is likely up; stale/missing ⇒ warn or defer work +2. **Persona** — human-readable name, avatar, and related display fields for source lists and UIs +3. **Fee disclosure** — service fee floor and basis points before a pay flow +4. **Enroll fast-path** — optional `enroll_difficulty` so clients can skip an Enroll probe when mining PoW (see [CLINK Enroll](clink-enroll.md)) +5. **Capability hints** — optional `supported_kinds` so clients know which CLINK kinds the node advertises +6. **Operator linkage** — optional operator social pubkey so clients can discover or label a node by a known Nostr identity (see **Discovery by operator**). **Requires operator attestation** before trusted display (see **Operator attestation**). + +Beacon is **optional for servers** and **optional for clients** as an optimization. Portable behavior always remains available via Enroll probe and normal CLINK request/response flows. Beacon carries **service-level** hints only — not per-account pointers, balances, or wallet RPC (see [CLINK Enroll](clink-enroll.md)). + +## Nostr event + +Beacon uses [NIP-78](https://github.com/nostr-protocol/nips/blob/master/78.md) **addressable** kind `30078`. + +| Field | Value | +|-------|--------| +| **Kind** | `30078` | +| **Author** | Service pubkey (same pubkey as in the service `nprofile` / TLV `0` of CLINK bech32 strings) | +| **`d` tag** | `clink-node` | +| **Tags** | `["operator", ""]` — optional; required when `operator_npub` is present in content (see below) | +| **Content** | UTF-8 JSON object (schema below) | + +**Subscription filter (typical):** + +```json +{ + "kinds": [30078], + "authors": [""], + "#d": ["clink-node"] +} +``` + +Clients SHOULD subscribe on a relay the service is known to use (from `nprofile`, prior Enroll, or beacon `relays`). + +To **discover nodes by operator** (“what does this `npub` run?”), clients SHOULD start from the **operator attestation** (author = operator pubkey), not `#operator` on service beacons. Spoofed services can claim any `operator_npub`; the operator-signed attestation is the authoritative service list. + +```json +{ + "kinds": [30078], + "authors": [""], + "#d": ["clink-node-operator"] +} +``` + +Read `service` tags (and check `clink-node-operator-revoke`). For each attested service pubkey `S`, fetch the `clink-node` beacon from author `S` and confirm it claims the same operator (`operator_npub` / `operator` tag). Only then treat linkage as **verified**. + +Clients MAY also query `#operator` on `clink-node` beacons to find **unverified** candidates, but MUST not treat matches as trusted without the operator-first path above. + +### Publication cadence + +Services that publish beacons SHOULD republish at least every **60 seconds** while online so subscribers can detect staleness without long gaps. + +### Client staleness + +Clients SHOULD treat a beacon as **stale** when no event with `created_at` within the last **180 seconds** has been seen for that service pubkey and `d` tag. Stale beacons MUST NOT be used for `enroll_difficulty` fast-path; clients fall back to Enroll probe or user warning. + +These intervals are recommendations; deployments MAY tune locally but SHOULD stay within similar bounds for interoperable UX. + +## Content schema + +All fields except `clink_version` are optional. Unknown fields MUST be ignored by clients. + +```json +{ + "clink_version": "1", + "name": "My Node", + "avatarUrl": "https://example.com/avatar.png", + "website": "https://example.com", + "nip05": "bob@example.com", + "description": "Short human-readable blurb.", + "operator_npub": "npub1…", + "relays": ["wss://relay.example.com"], + "fees": { + "serviceFeeFloor": 0, + "serviceFeeBps": 100 + }, + "enroll_difficulty": 18, + "supported_kinds": [21001, 21002, 21003, 21004] +} +``` + +### Field definitions + +| Field | Type | Requirement | Description | +|-------|------|-------------|-------------| +| `clink_version` | string | RECOMMENDED | CLINK beacon schema version. Implementations SHOULD use `"1"`. | +| `name` | string | RECOMMENDED when publishing | Display name for the node service. | +| `avatarUrl` | string | optional | HTTPS URL for an avatar image. | +| `website` | string | optional | Service website URL. | +| `nip05` | string | optional | NIP-05 identifier for the service (verification is out of band). | +| `description` | string | optional | Short description for UIs. | +| `operator_npub` | string | optional | Node **operator** social Nostr identity ([NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md) `npub…` bech32). Not the service CLINK pubkey (event author). When present, the event MUST include tag `["operator", ""]` where hex is the decoded `operator_npub`. Clients MUST NOT treat this as verified without **operator attestation** (below). | +| `relays` | string[] | optional | Relay URL(s) where the service listens for CLINK traffic. First entry MAY be treated as preferred. | +| `fees` | object | optional | Service fee disclosure for pay flows. | +| `fees.serviceFeeFloor` | integer | optional | Minimum service fee in **satoshis**. | +| `fees.serviceFeeBps` | integer | optional | Service fee in basis points (100 = 1%). | +| `enroll_difficulty` | integer | optional | NIP-13 bits the service requires for **new** Enroll when PoW is enabled. MUST **equal** the enforced required difficulty (same value as `required_difficulty` on Enroll code `4`). Not a minimum or maximum — an accurate advertisement. | +| `supported_kinds` | integer[] | optional | CLINK event kinds this service supports (e.g. `21001` Offers, `21002` Debits, `21003` Manage, `21004` Enroll). | + +Monetary amounts use **satoshis**, consistent with other CLINK specs. + +### Discovery by operator + +The **service pubkey** (beacon event author) is the CLINK backend identity. The **operator pubkey** is the human operator’s everyday Nostr key. + +**Recommended flow** (operator-first): + +1. Fetch current `clink-node-operator` from the operator pubkey → attested service pubkeys (`service` tags). +2. Subtract any pubkeys on current `clink-node-operator-revoke`. +3. For each remaining `S`, fetch `clink-node` from author `S` and confirm `operator_npub` / `operator` tag matches the operator. + +Verified “operated by” UI requires steps 1–3. Starting from `#operator` on service beacons invites affinity scams — many spoofed nodes can claim the same famous `npub`. + +Operator linkage does **not** grant [Enroll](clink-enroll.md) owner policy on kind `21002` / `21003` for that key unless that key is also the enrolled account owner on the service. + +### Operator attestation + +A service beacon’s `operator_npub` is a **one-way claim**. Anyone can set `operator_npub` to a famous key. Clients need a **bidirectional proof**: the claimed operator key must also attest that it operates this service pubkey. + +The operator publishes a separate replaceable event: + +| Field | Value | +|-------|--------| +| **Kind** | `30078` | +| **Author** | Operator pubkey (decoded `operator_npub`) | +| **`d` tag** | `clink-node-operator` | +| **Tags** | `["service", ""]` — one tag per attested service (the list lives in tags only) | +| **Content** | `{"clink_version": "1"}` | + +Service pubkeys are carried **only** in `service` tags (enables `#service` relay filters). Clients MUST NOT duplicate them in `content`. + +**Subscription filter (does operator attest to service `S`?):** + +```json +{ + "kinds": [30078], + "authors": [""], + "#d": ["clink-node-operator"], + "#service": [""] +} +``` + +**Verification** (operator pubkey `O`, service pubkey `S`): + +1. Service beacon (author `S`) includes `operator_npub` / `operator` tag for `O`. +2. Current operator attestation (`d=clink-node-operator`, author `O`) includes tag `["service", "S"]`. +3. Current revocation replaceable state does not include tag `["service", "S"]` — see **Operator attestation revocation**. +4. Steps 1–3 satisfied ⇒ **verified** operator linkage. +5. Beacon claims `O` but attestation missing, no `service` tag for `S`, or `S` is revoked ⇒ **unverified** — clients MUST NOT present as “operated by” without a warning; treat as possible affinity scam. +6. Attestation tags `S` but beacon does not claim `O` ⇒ operator attestation only; no “operated by” UI from beacon alone. + +Clients MUST determine attestation and revocation from **current replaceable state** (latest event per `d` tag). Clients MUST NOT order these documents by `created_at` — timestamps are self-reported and not a reliable chain. + +**Subscription filter (full attestation document):** + +```json +{ + "kinds": [30078], + "authors": [""], + "#d": ["clink-node-operator"] +} +``` + +### Operator attestation revocation + +An operator MAY revoke a service without editing the attestation list by publishing a separate **replaceable** revocation document: + +| Field | Value | +|-------|--------| +| **Kind** | `30078` | +| **Author** | Operator pubkey | +| **`d` tag** | `clink-node-operator-revoke` | +| **Tags** | `["service", ""]` — one tag per revoked service | +| **Content** | `{"clink_version": "1"}` | + +**Subscription filter (revocation document):** + +```json +{ + "kinds": [30078], + "authors": [""], + "#d": ["clink-node-operator-revoke"] +} +``` + +**Subscription filter (is service `S` revoked?):** + +```json +{ + "kinds": [30078], + "authors": [""], + "#d": ["clink-node-operator-revoke"], + "#service": [""] +} +``` + +**Verification with revocation** (operator `O`, service `S`): fetch the **current** `clink-node-operator` and `clink-node-operator-revoke` replaceable events from `O`. Linkage is **verified** only if attestation includes `["service", "S"]` and revocation does **not**. If both include a `service` tag for `S`, **revocation wins**. To attest again after revoke, the operator MUST republish revocation without the `S` tag (and SHOULD keep attestation consistent). + +Clients SHOULD fetch or subscribe to both `clink-node-operator` and `clink-node-operator-revoke` when verifying operator linkage for display. + +## Server requirements + +Services MAY publish a CLINK beacon. If they publish: + +- The event MUST match kind `30078`, author pubkey, and `d` tag `clink-node`. +- `clink_version` SHOULD be `"1"`. +- If `enroll_difficulty` is present, it MUST **equal** the required PoW difficulty enforced on kind `21004` for new enrolls (same value as `required_difficulty` when code is `4`). Services MUST NOT publish a lower or higher value than they enforce. +- If `supported_kinds` is present, listed kinds MUST be actually supported. +- If `fees` is present, values MUST reflect current service fee policy. +- If `operator_npub` is present, the event MUST include `["operator", ""]` and hex MUST match the decoded `operator_npub`. Verified display of operator identity requires matching **operator attestation** from that pubkey. + +Services MUST NOT rely on beacon alone for security decisions on kind `21004`; Enroll PoW validation remains on the Enroll request event. + +## Client requirements + +Clients MAY subscribe to CLINK beacons when they know service pubkey and relay. + +When using a beacon: + +1. **Onlineness** — use event `created_at` (and subscription freshness) per **Client staleness** above. +2. **Enroll difficulty** — if `enroll_difficulty` is present and the beacon is not stale, clients MAY mine at that value before Enroll instead of probing. The Enroll request still MUST meet the service’s required difficulty (mining **at or above** required satisfies NIP-13; see [CLINK Enroll](clink-enroll.md)). On code `4`, mine at `required_difficulty` and retry once. If absent, stale, or untrusted, clients MUST use the portable Enroll probe path. +3. **Persona / fees** — clients MAY display `name`, `avatarUrl`, etc., and show `fees` before payment; display is advisory unless cross-checked in a pay response. +4. **Relays** — clients MAY update preferred relay hints from `relays` when reconnecting or building filters. +5. **Operator** — for verified linkage, follow **Discovery by operator** (attestation first, then confirm `operator_npub` on each service beacon). Unverified `#operator` beacon matches alone MUST NOT show trusted “operated by” UI. + +Clients MUST support Enroll probe (code `4` + `required_difficulty`) regardless of beacon support. + +## Relationship to Enroll + +``` +nprofile (service pubkey + relay) + │ + ▼ + optional: subscribe kind 30078, d=clink-node + • stale/missing → treat as unknown / offline warning + • fresh + enroll_difficulty → MAY skip probe and mine + │ + ▼ + portable fallback: Enroll probe (kind 21004, 0 PoW → code 4) + │ + ▼ + kind 21004 Enroll → noffer / ndebit / nmanage +``` + +Beacon does not create accounts or return pointers. It only advertises parameters and liveness before Enroll. + +See [CLINK Enroll](clink-enroll.md) for PoW rules, probe semantics, and pointer delivery. + +## Security considerations + +- The beacon is a **signed** Nostr event from the service pubkey. The signature proves the service published this persona and hints — not that the service is trustworthy, that quoted `fees` apply to a specific invoice, or that any party is authorized to spend. Clients MUST NOT treat the beacon alone as proof of good behavior, invoice-level fee correctness, or spend authorization. +- **Affinity scam:** a malicious service can publish any `operator_npub`. Clients MUST use bidirectional **operator attestation** and check **revocation** before trusted “operated by” display. +- `nip05` in beacon is not verified by this spec; use NIP-05 lookup when verification matters. +- `enroll_difficulty` fast-path is safe only when the beacon is fresh and from the expected author pubkey; otherwise probe. +- Rate of beacon publication is a operational choice; very sparse beacons weaken onlineness signals. + +## Reference implementation + +- Spec repo: this document +- **SDK:** [CLINK SDK](https://github.com/shocknet/ClinkSDK) ([`@shocknet/clink-sdk`](https://www.npmjs.com/package/@shocknet/clink-sdk) on npm) +- Reference server: [Lightning.Pub](https://github.com/shocknet/Lightning.Pub) (publishes kind `30078` beacons today; migration to `d=clink-node` and `enroll_difficulty` is pending) +- Reference wallet: [ShockWallet](https://shockwallet.app) (subscribes to service beacons for onlineness and display name) + +**Backward compatibility (non-normative):** some deployments historically used `d=Lightning.Pub` with a similar JSON shape. New implementations SHOULD use `d=clink-node`. The `clink-*` prefix reserves the CLINK namespace on kind `30078` (`clink-node`, `clink-node-operator`, `clink-node-operator-revoke`, …). Clients MAY accept legacy `d` tags only when explicitly targeting those deployments during a transition period. diff --git a/specs/clink-enroll.md b/specs/clink-enroll.md index 5e6581f..5453ed2 100644 --- a/specs/clink-enroll.md +++ b/specs/clink-enroll.md @@ -4,7 +4,9 @@ **CLINK Enroll** binds a Nostr key to an account (pointer) on a node service and returns that account’s default static CLINK pointers (`noffer1…`, `ndebit1…`, `nmanage1…`). -It is the bootstrap step for headless clients, CLIs, and agents that act **as** the user (direct-use principal). It does **not** grant third parties spend or manage rights. +It is how headless clients, CLIs, and agents that act **as** the user (direct-use principal) send an **Enroll request** to provision an account on a node. It does **not** grant third parties spend or manage rights. + +**Terminology:** An **Enroll request** is a kind `21004` event (same request/response pattern as Offers and Debits). Successful enrollment **provisions** an account for the requestor’s signing key. This is not wallet “bootstrap node”, Pub bootstrap liquidity, or application first-run initialization. ## Motivation @@ -43,7 +45,7 @@ There is no separate “app” pubkey in the pointer model. Account identity on - **Tags (request):** - `["p", ""]` - `["clink_version", "1"]` - - `["nonce", "", ""]` — [NIP-13](https://github.com/nostr-protocol/nips/blob/master/13.md) proof of work (see **Proof of work**) + - `["nonce", "", ""]` — [NIP-13](https://github.com/nostr-protocol/nips/blob/master/13.md) proof of work when the service requires it (see **Proof of work**) - **Tags (response):** - `["p", ""]` - `["e", ""]` @@ -52,49 +54,48 @@ There is no separate “app” pubkey in the pointer model. Account identity on ## Proof of work -Enroll creates (or resumes) an account. Unbounded free enroll is a DoS / spam vector. Request events MUST carry [NIP-13](https://github.com/nostr-protocol/nips/blob/master/13.md) proof of work so mass scripted enrollment is expensive, while a single enroll on a phone, browser tab, or low-resource agent stays interactive. +Enroll creates an account for a new key, or returns the existing account if that key is already enrolled. Unbounded free enroll can be abused. Services **MAY** require [NIP-13](https://github.com/nostr-protocol/nips/blob/master/13.md) proof of work on the kind `21004` request to make mass scripted enrollment expensive, while a single enroll on a phone, browser tab, or low-resource agent stays interactive. + +This spec does **not** mandate that every deployment enforce PoW. Operators choose based on their threat model (open public node vs invite-only vs already rate-limited). When a service *does* require PoW, the rules below apply. -### Rules +### When PoW is required -1. The kind `21004` **request** event MUST include a NIP-13 `nonce` tag: `["nonce", "", ""]`. +1. The request event MUST include a NIP-13 `nonce` tag: `["nonce", "", ""]`. 2. The event id MUST have at least `target_difficulty` leading zero bits. -3. `target_difficulty` in the tag MUST be ≥ the service’s required difficulty (committed target — services MUST reject “lucky” high-difficulty ids that commit to a lower target). -4. Services MUST reject requests that fail (1)–(3), preferably with error code `4` and `required_difficulty` so clients can remine once. +3. `target_difficulty` in the tag MUST be ≥ the service’s required difficulty (committed target — reject “lucky” high-difficulty ids that commit to a lower target). +4. If PoW is insufficient, the service SHOULD respond with error code `4` and `required_difficulty` so the client can remine once without guessing. ### Recommended difficulty | Setting | Bits | Intent | |---------|------|--------| -| **Minimum allowed** | 16 | Floor for public enroll (~65k hashes expected) | -| **Recommended default** | **18** | Deterrent for naive scripts; typically well under ~1s on a mid-range phone / browser WASM / small VPS agent | -| **Stronger public** | 20 | ~1M hashes expected; still usually a few seconds on a phone, not “forever” | -| **Avoid by default** | ≥ 22 | Fine for invite-gated or desktop-only services; too slow for shitty phones / casual web tabs | +| **Recommended** | **18** | Deters scripts; typically well under ~1s on a mid-range phone / browser tab / small agent harness | +| **Stronger** | 20 | ~1M hashes; usually a few seconds on a phone | +| **Heavy** | ≥ 22 | Avoid for open public enroll aimed at low-power clients | Expected work scales as `2^bits` SHA-256 event-id trials. One extra bit ≈ 2× wall time. -Reference servers SHOULD default to **18** and MAY raise toward **20** under load. They MUST NOT require ≥ 22 for open public enroll unless they document that low-power clients are unsupported or offer an invite / lower-difficulty path. +### Discover difficulty before mining -### Existing accounts +Blindly hashing at 18 and then discovering the service wants 20 wastes a full mine on weak devices. -If the requestor pubkey already owns an account on the service, the service MAY: +**Beacon fast-path (optional):** see [CLINK Beacon](clink-beacon.md). A fresh kind `30078` beacon with `enroll_difficulty` lets clients skip the probe before mining. -- accept the same PoW requirement, or -- accept a lower difficulty (including `0`) for idempotent “return my pointers” re-enroll +**Portable discovery (normative):** clients learn difficulty by **probing** — send Enroll with no PoW (or difficulty `0`). If the service requires PoW, it responds with code `4` and `required_difficulty`; the client mines once at that value and retries. Every CLINK Enroll implementation MUST support this path. Portable clients MUST still probe when beacon is missing, stale, or not implemented. -New account creation MUST still meet the full required difficulty. +If the probe is ignored or the service does not require PoW, the client MAY enroll with no PoW or with the recommended **18** bits as a local default. On code `4`, mine at `required_difficulty` and retry **once**. -### Advertising difficulty +Services that require PoW SHOULD return code `4` + `required_difficulty` on insufficient work so the probe path works. -Services SHOULD advertise `required_difficulty` so clients mine once: +Services that publish a CLINK beacon with `enroll_difficulty` MUST keep it in sync with what they enforce on kind `21004` (see [CLINK Beacon](clink-beacon.md)). -- in error responses when PoW is insufficient (see below), and/or -- out of band (e.g. service kind `0` / documentation) +### Existing accounts -Clients SHOULD mine at the advertised value (or the recommended default **18** if unknown), and on code `4` remine at `required_difficulty`. +If the requestor pubkey already owns an account, a service that normally requires PoW MAY accept lower difficulty (including none) for an idempotent “return my pointers”. New account creation, when PoW is enabled, SHOULD still meet the full requirement. ## Request -Empty object or optional fields: +Kind `21004` **Enroll request** event. Empty object or optional fields: ```json {} @@ -172,12 +173,13 @@ Marketplace / agent keys that are **not** the account owner still require normal nprofile (service) + user key │ ▼ - mine NIP-13 PoW (default 18 bits) on kind 21004 + learn difficulty: + optional: kind 30078 beacon → enroll_difficulty (see CLINK Beacon) + portable: Enroll with 0 PoW → code 4 + required_difficulty │ ▼ - kind 21004 Enroll + mine if required, then kind 21004 Enroll │ - ├─ code 4 → remine at required_difficulty → retry ▼ noffer / ndebit / nmanage │ @@ -188,8 +190,8 @@ nprofile (service) + user key ## Security considerations -- Enroll proves control of a key and creates (or resumes) a custodial-or-self-hosted account on the service; services SHOULD rate-limit **and** require NIP-13 PoW for new accounts. PoW alone is not enough under a well-funded attacker — combine with rate limits / invites. -- Recommended **18-bit** PoW aims for “annoying to spray thousands of accounts, fine for one human or agent on a phone.” Do not push public defaults into “wait forever on a weak device” territory (≥ 22). +- Enroll proves control of a key and creates an account on the service (or returns pointers for an account that key already has). Services SHOULD rate-limit new enrolls and MAY require PoW and/or invites. +- When PoW is used, ~**18 bits** is a sensible default tradeoff. Clients SHOULD probe (code `4`) before mining so they do not double-hash on a phone. - Returned `ndebit` is powerful for the **owner key** under owner policy; clients MUST treat the secret key as a full account credential. - Do not overload Enroll with grant minting — that recreates ambient authority and breaks Manage’s delegation model. From 5919994c033d70e950035d667e6db33aba11e29b Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Wed, 29 Jul 2026 12:24:23 -0400 Subject: [PATCH 4/4] version req --- specs/clink-beacon.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/specs/clink-beacon.md b/specs/clink-beacon.md index 7beaf31..3898968 100644 --- a/specs/clink-beacon.md +++ b/specs/clink-beacon.md @@ -71,7 +71,7 @@ These intervals are recommendations; deployments MAY tune locally but SHOULD sta ## Content schema -All fields except `clink_version` are optional. Unknown fields MUST be ignored by clients. +`clink_version` is required; all other fields are optional. Unknown fields MUST be ignored by clients. ```json { @@ -96,8 +96,8 @@ All fields except `clink_version` are optional. Unknown fields MUST be ignored b | Field | Type | Requirement | Description | |-------|------|-------------|-------------| -| `clink_version` | string | RECOMMENDED | CLINK beacon schema version. Implementations SHOULD use `"1"`. | -| `name` | string | RECOMMENDED when publishing | Display name for the node service. | +| `clink_version` | string | required | `"1"` | +| `name` | string | optional | Display name for the node service. | | `avatarUrl` | string | optional | HTTPS URL for an avatar image. | | `website` | string | optional | Service website URL. | | `nip05` | string | optional | NIP-05 identifier for the service (verification is out of band). | @@ -215,8 +215,7 @@ Clients SHOULD fetch or subscribe to both `clink-node-operator` and `clink-node- Services MAY publish a CLINK beacon. If they publish: -- The event MUST match kind `30078`, author pubkey, and `d` tag `clink-node`. -- `clink_version` SHOULD be `"1"`. +- The event MUST match kind `30078`, author pubkey, `d` tag `clink-node`, and `content` with `clink_version: "1"`. - If `enroll_difficulty` is present, it MUST **equal** the required PoW difficulty enforced on kind `21004` for new enrolls (same value as `required_difficulty` when code is `4`). Services MUST NOT publish a lower or higher value than they enforce. - If `supported_kinds` is present, listed kinds MUST be actually supported. - If `fees` is present, values MUST reflect current service fee policy.