diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..2774531 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,32 @@ +module.exports = { + root: true, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:svelte/recommended', + 'prettier' + ], + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + parserOptions: { + sourceType: 'module', + ecmaVersion: 2022 + }, + env: { + browser: true, + es2022: true, + node: true + }, + overrides: [ + { + files: ['*.svelte'], + parser: 'svelte-eslint-parser', + parserOptions: { + parser: '@typescript-eslint/parser' + } + } + ], + rules: { + '@typescript-eslint/no-explicit-any': 'warn' + } +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6635cf5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..77d9794 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,17 @@ +{ + "useTabs": false, + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte"], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f11e7f8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +# Multi-stage build +FROM node:20-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +ARG VITE_DEFAULT_RELAYS +ARG VITE_ZAP_THRESHOLD +ARG VITE_THREAD_TIMEOUT_DAYS +ARG VITE_PWA_ENABLED +ENV VITE_DEFAULT_RELAYS=${VITE_DEFAULT_RELAYS} +ENV VITE_ZAP_THRESHOLD=${VITE_ZAP_THRESHOLD} +ENV VITE_THREAD_TIMEOUT_DAYS=${VITE_THREAD_TIMEOUT_DAYS} +ENV VITE_PWA_ENABLED=${VITE_PWA_ENABLED} +RUN npm run build + +FROM httpd:alpine +COPY --from=builder /app/build /usr/local/apache2/htdocs/ +COPY httpd.conf.template /usr/local/apache2/conf/httpd.conf.template +COPY docker-entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/docker-entrypoint.sh +ARG PORT=9876 +ENV PORT=${PORT} +EXPOSE ${PORT} +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] diff --git a/README.md b/README.md index d9d9eb0..5bc4303 100644 --- a/README.md +++ b/README.md @@ -1,108 +1,535 @@ -# Aitherboard +# Aitherboard Specification -A dockerized 4chan-style imageboard built on Nostr protocol. Aitherboard is a single-page application (SPA) that provides a decentralized discussion board experience with real-time updates, multiple authentication methods, and full Nostr protocol integration. +A decentralized messageboard built on the Nostr protocol. This document defines the complete specification for implementation. + +This is a client from [silberengel@gitcitadel.com](https://jumble.imwald.eu/users/npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z) ## Table of Contents -- [Overview](#overview) - [Technology Stack](#technology-stack) -- [Architecture](#architecture) -- [Features](#features) +- [Project Structure](#project-structure) +- [Event Types](#event-types) +- [Relay Management](#relay-management) - [Authentication](#authentication) -- [Content Types](#content-types) +- [Content Modules](#content-modules) +- [User Interface](#user-interface) +- [Performance Requirements](#performance-requirements) +- [Accessibility Requirements](#accessibility-requirements) +- [Security Requirements](#security-requirements) - [Configuration](#configuration) -- [Docker Deployment](#docker-deployment) -- [Health Check](#health-check) -- [Development](#development) -- [Implementation Phases](#implementation-phases) +- [Deployment](#deployment) -## Overview +## Technology Stack -Aitherboard is a decentralized imageboard that uses the Nostr protocol for content distribution. It supports: +**REQUIRED**: +- Frontend: Svelte 5 (with runes: `$state`, `$derived`, `$effect`) + TypeScript + Vite +- Styling: Tailwind CSS (4chan-style minimal design) +- Nostr Library: [`applesauce-core`](https://github.com/hzrd149/applesauce) +- Markdown: `marked` + `DOMPurify` for sanitization +- Storage: IndexedDB (via `idb` or `localforage`) +- Deployment: Docker + Apache httpd (static serving on port 9876) +- PWA: Progressive Web App support -- **Kind 11 threads** (NIP-7D) - Discussion threads with topics -- **Kind 1111 comments** (NIP-22) - Threaded comments -- **Kind 9735 zap receipts** (NIP-57) - Lightning payments displayed as comments -- **Kind 1 feed** - Twitter-like feed with replies and zaps -- **Kind 7 reactions** - Upvote/downvote system -- **Real-time updates** - Live feed of new content -- **Multiple auth methods** - NIP-07, nsec, NIP-46 bunker, anonymous -- **Full markdown rendering** - All content supports markdown -- **NIP-21 link parsing** - Clickable profile badges and event cards +## Project Structure -## Technology Stack +``` +aitherboard/ +├── src/ +│ ├── lib/ +│ │ ├── services/ +│ │ │ ├── nostr/ +│ │ │ │ ├── applesauce-client.ts +│ │ │ │ ├── relay-pool.ts +│ │ │ │ ├── event-store.ts +│ │ │ │ ├── subscription-manager.ts +│ │ │ │ ├── auth-handler.ts +│ │ │ │ └── config.ts +│ │ │ ├── auth/ +│ │ │ │ ├── nip07-signer.ts +│ │ │ │ ├── nsec-signer.ts +│ │ │ │ ├── bunker-signer.ts +│ │ │ │ ├── anonymous-signer.ts +│ │ │ │ ├── session-manager.ts +│ │ │ │ ├── profile-fetcher.ts +│ │ │ │ ├── relay-list-fetcher.ts +│ │ │ │ ├── user-preferences-fetcher.ts +│ │ │ │ ├── user-status-fetcher.ts +│ │ │ │ └── activity-tracker.ts +│ │ │ ├── cache/ +│ │ │ │ ├── indexeddb-store.ts +│ │ │ │ ├── event-cache.ts +│ │ │ │ ├── profile-cache.ts +│ │ │ │ ├── search-index.ts +│ │ │ │ └── anonymous-key-store.ts +│ │ │ └── security/ +│ │ │ ├── key-management.ts +│ │ │ ├── bech32-utils.ts +│ │ │ ├── event-validator.ts +│ │ │ └── sanitizer.ts +│ │ ├── modules/ +│ │ │ ├── threads/ +│ │ │ ├── comments/ +│ │ │ ├── zaps/ +│ │ │ ├── reactions/ +│ │ │ ├── profiles/ +│ │ │ └── feed/ +│ │ └── components/ +│ │ ├── content/ +│ │ ├── interactions/ +│ │ ├── layout/ +│ │ ├── modals/ +│ │ └── preferences/ +│ └── routes/ +├── public/ +├── scripts/ +├── Dockerfile +├── docker-compose.yml +├── httpd.conf.template +└── docker-entrypoint.sh +``` + +## Event Types + +### Supported Event Kinds + +| Kind | Name | NIP | Purpose | +|------|------|-----|---------| +| 0 | Metadata/Profile | 1 | User profile information | +| 1 | Note/Feed Post | 10 | Twitter-like feed posts | +| 7 | Reaction | 25 | Upvote/downvote/like | +| 11 | Thread | 7D | Discussion threads | +| 1111 | Comment | 22 | Threaded comments | +| 9734 | Zap Request | 57 | Lightning payment request (not published to relays) | +| 9735 | Zap Receipt | 57 | Lightning payment confirmation | +| 10000 | Mute List | 51 | User mute preferences | +| 10002 | Relay List | 65 | User relay preferences (inbox/outbox) | +| 10006 | Blocked Relays | 51 | User blocked relay list | +| 10432 | Local Relays | - | Local relay preferences (used alongside 10002) | +| 10133 | Payment Targets | A3 | Payment address information (RFC-8905) | +| 30315 | General User Status | 38 | General user status (NIP-38) | + +**Note**: Media attachments use tags defined in NIP-92 (imeta), NIP-94 (file attachments), and NIP-23 (image tags). + +### Event Structure Examples + +#### Kind 0 (Metadata/Profile) + +```json +{ + "kind": 0, + "content": "{\"name\":\"Alice\",\"about\":\"Developer\",\"picture\":\"https://example.com/alice.jpg\",\"website\":\"https://alice.example.com\",\"lud16\":\"alice@wallet.example.com\",\"nip05\":\"alice@example.com\"}", + "tags": [ + ["name", "Alice"], + ["about", "Developer"], + ["picture", "https://example.com/alice.jpg"], + ["website", "https://alice.example.com"], + ["lud16", "alice@wallet.example.com"], + ["nip05", "alice@example.com"], + ["nip05", "alice@otherexample.com"] + ], + "pubkey": "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", + "created_at": 1679673265, + "id": "67b48a14fb66c60c8f9070bdeb37afdfcc3d08ad01989460448e4081eddda446", + "sig": "f2cb581a84ed10e4dc84937bd98e27acac71ab057255f6aa8dfa561808c981fe8870f4a03c1e3666784d82a9c802d3704e174371aa13d63e2aeaf24ff5374d9d" +} +``` + +**REQUIREMENTS**: +- Parse tags (preferred) OR fallback to stringified JSON content +- Support multiple tags for: `lud16`, `website`, `nip05` +- Extract `lud16` tags for payment address display + +#### Kind 1 (Note/Feed Post) + +```json +{ + "kind": 1, + "content": "Hello nostr!", + "tags": [ + ["e", "67b48a14fb66c60c8f9070bdeb37afdfcc3d08ad01989460448e4081eddda446", "wss://relay.example.com", "reply", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"], + ["p", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"], + ["root", "67b48a14fb66c60c8f9070bdeb37afdfcc3d08ad01989460448e4081eddda446"], + ["client", "Aitherboard"] + ], + "pubkey": "a55c15f5e41d5aebd236eca5e0142789c5385703f1a7485aa4b38d94fd18dcc4", + "created_at": 1679673300, + "id": "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36", + "sig": "77127f636577e9029276be060332ea565deaf89ff215a494ccff16ae3f757065e2bc59b2e8c113dd407917a010b3abd36c8d7ad84c0e3ab7dab3a0b0caa9835d" +} +``` + +**REQUIREMENTS**: +- Support NIP-10 threading (`e`, `p`, `root`, `reply` tags) +- Render markdown content +- Flat threading display (no indentation) + +#### Kind 7 (Reaction) + +```json +{ + "kind": 7, + "content": "+", + "tags": [ + ["e", "67b48a14fb66c60c8f9070bdeb37afdfcc3d08ad01989460448e4081eddda446", "wss://relay.example.com", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"], + ["p", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", "wss://relay.example.com"], + ["k", "1"], + ["client", "Aitherboard"] + ], + "pubkey": "a55c15f5e41d5aebd236eca5e0142789c5385703f1a7485aa4b38d94fd18dcc4", + "created_at": 1679673300, + "id": "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36", + "sig": "77127f636577e9029276be060332ea565deaf89ff215a494ccff16ae3f757065e2bc59b2e8c113dd407917a010b3abd36c8d7ad84c0e3ab7dab3a0b0caa9835d" +} +``` + +**REQUIREMENTS**: See [Reaction Module](#reaction-module-srclibmodulesreactions) for full requirements. + +#### Kind 11 (Thread) + +```json +{ + "kind": 11, + "content": "Good morning everyone!", + "tags": [ + ["title", "GM"], + ["t", "tech"], + ["image", "https://example.com/cover.jpg"], + ["imeta", "url https://example.com/image1.jpg", "m image/jpeg", "x 1920", "y 1080"], + ["imeta", "url https://example.com/video1.mp4", "m video/mp4", "x 1920", "y 1080", "dim 1920x1080x30"], + ["file", "https://example.com/document.pdf", "application/pdf", "size 1048576"], + ["client", "Aitherboard"] + ], + "pubkey": "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", + "created_at": 1679673265, + "id": "67b48a14fb66c60c8f9070bdeb37afdfcc3d08ad01989460448e4081eddda446", + "sig": "f2cb581a84ed10e4dc84937bd98e27acac71ab057255f6aa8dfa561808c981fe8870f4a03c1e3666784d82a9c802d3704e174371aa13d63e2aeaf24ff5374d9d" +} +``` + +**REQUIREMENTS**: +- Max 3 `t` tags (topics) +- Threads without topics appear under "General" +- 30-day timeout (configurable) +- Render markdown content +- **Cover image**: Support `image` tag (NIP-23) for thread cover image +- **Media attachments**: Support NIP-92 (imeta) and NIP-94 (file attachments) tags + +#### Kind 1111 (Comment) + +```json +{ + "kind": 1111, + "content": "Cool beans", + "tags": [ + ["K", "11"], + ["E", "67b48a14fb66c60c8f9070bdeb37afdfcc3d08ad01989460448e4081eddda446", "wss://relay.example.com", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"], + ["P", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"], + ["e", "67b48a14fb66c60c8f9070bdeb37afdfcc3d08ad01989460448e4081eddda446", "wss://relay.example.com", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"], + ["k", "11"], + ["p", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"], + ["client", "Aitherboard"] + ], + "pubkey": "a55c15f5e41d5aebd236eca5e0142789c5385703f1a7485aa4b38d94fd18dcc4", + "created_at": 1679673300, + "id": "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36", + "sig": "77127f636577e9029276be060332ea565deaf89ff215a494ccff16ae3f757065e2bc59b2e8c113dd407917a010b3abd36c8d7ad84c0e3ab7dab3a0b0caa9835d" +} +``` + +**REQUIREMENTS**: +- Parse NIP-22 tags: `K`, `E`, `e`, `k`, `p`, `P` +- Flat list display (no indentation) +- Gray blurb showing parent preview +- Click blurb to highlight/scroll to parent +- Render markdown content + +#### Kind 9734 (Zap Request) + +```json +{ + "kind": 9734, + "content": "Zap!", + "tags": [ + ["relays", "wss://nostr-pub.wellorder.com", "wss://anotherrelay.example.com"], + ["amount", "21000"], + ["lnurl", "lnurl1dp68gurn8ghj7um5v93kketj9ehx2amn9uh8wetvdskkkmn0wahz7mrww4excup0dajx2mrv92x9xp"], + ["p", "04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9"], + ["e", "9ae37aa68f48645127299e9453eb5d908a0cbb6058ff340d528ed4d37c8994fb"], + ["k", "1"], + ["client", "Aitherboard"] + ], + "pubkey": "97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322", + "created_at": 1679673265, + "id": "30efed56a035b2549fcaeec0bf2c1595f9a9b3bb4b1a38abaf8ee9041c4b7d93", + "sig": "f2cb581a84ed10e4dc84937bd98e27acac71ab057255f6aa8dfa561808c981fe8870f4a03c1e3666784d82a9c802d3704e174371aa13d63e2aeaf24ff5374d9d" +} +``` + +**REQUIREMENTS**: +- **NOT published to relays** - sent directly to wallet lnurl callback +- **Primary method**: Send via `lightning:` URI to external wallet +- **Fallback method**: Fetch invoice, display QR code + copyable text +- User pays with external wallet (no in-app wallet) + +#### Kind 9735 (Zap Receipt) + +```json +{ + "id": "67b48a14fb66c60c8f9070bdeb37afdfcc3d08ad01989460448e4081eddda446", + "pubkey": "9630f464cca6a5147aa8a35f0bcdd3ce485324e732fd39e09233b1d848238f31", + "created_at": 1674164545, + "kind": 9735, + "tags": [ + ["p", "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"], + ["P", "97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322"], + ["e", "3624762a1274dd9636e0c552b53086d70bc88c165bc4dc0f9e836a1eaf86c3b8"], + ["k", "1"], + ["bolt11", "lnbc10u1p3unwfusp5t9r3yymhpfqculx78u027lxspgxcr2n2987mx2j55nnfs95nxnzqpp5jmrh92pfld78spqs78v9euf2385t83uvpwk9ldrlvf6ch7tpascqhp5zvkrmemgth3tufcvflmzjzfvjt023nazlhljz2n9hattj4f8jq8qxqyjw5qcqpjrzjqtc4fc44feggv7065fqe5m4ytjarg3repr5j9el35xhmtfexc42yczarjuqqfzqqqqqqqqlgqqqqqqgq9q9qxpqysgq079nkq507a5tw7xgttmj4u990j7wfggtrasah5gd4ywfr2pjcn29383tphp4t48gquelz9z78p4cq7ml3nrrphw5w6eckhjwmhezhnqpy6gyf0"], + ["description", "{\"pubkey\":\"97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322\",\"content\":\"\",\"id\":\"d9cc14d50fcb8c27539aacf776882942c1a11ea4472f8cdec1dea82fab66279d\",\"created_at\":1674164539,\"sig\":\"77127f636577e9029276be060332ea565deaf89ff215a494ccff16ae3f757065e2bc59b2e8c113dd407917a010b3abd36c8d7ad84c0e3ab7dab3a0b0caa9835d\",\"kind\":9734,\"tags\":[[\"e\",\"3624762a1274dd9636e0c552b53086d70bc88c165bc4dc0f9e836a1eaf86c3b8\"],[\"p\",\"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245\"],[\"relays\",\"wss://relay.damus.io\"]]}"], + ["preimage", "5d006d2cf1e73c7148e7519a4c68adc81642ce0e25a432b2434c99f97344c15f"] + ], + "content": "", + "sig": "f2cb581a84ed10e4dc84937bd98e27acac71ab057255f6aa8dfa561808c981fe8870f4a03c1e3666784d82a9c802d3704e174371aa13d63e2aeaf24ff5374d9d" +} +``` + +**REQUIREMENTS**: +- Display with ⚡ emoji +- Only display if amount ≥ configured threshold (default: 1 sat) +- Published by wallet, not client + +#### Kind 10002 (Relay List) + +```json +{ + "kind": 10002, + "tags": [ + ["r", "wss://alicerelay.example.com"], + ["r", "wss://brando-relay.com"], + ["r", "wss://expensive-relay.example2.com", "write"], + ["r", "wss://nostr-relay.example.com", "read"] + ], + "content": "", + "pubkey": "854043ae8f1f97430ca8c1f1a090bdde6488bd5115c7a45307a2a212750ae4cb", + "created_at": 1699597889, + "id": "a92a316b75e44cfdc19986c634049158d4206fcc0b7b9c7ccbcdabe28beebcd0", + "sig": "1173822c53261f8cffe7efbf43ba4a97a9198b3e402c2a1df130f42a8985a2d0d3430f4de350db184141e45ca844ab4e5364ea80f11d720e36357e1853dba6ca" +} +``` + +**REQUIREMENTS**: +- Replaceable event (one per user) +- `r` tags with relay URLs +- Optional `read` or `write` marker (if omitted, relay is both) +- **Inbox relays** (marked `read` or unmarked): Added to reading operations +- **Outbox relays** (marked `write` or unmarked): Added to publishing operations +- Used alongside kind 10432 (local relays) - both kinds combined for relay selection + +#### Kind 10432 (Local Relays) + +```json +{ + "kind": 10432, + "tags": [ + ["r", "ws://localhost:7777"], + ["r", "ws://local-relay.example.com", "write"], + ["r", "ws://localhost:8080", "read"] + ], + "content": "", + "pubkey": "854043ae8f1f97430ca8c1f1a090bdde6488bd5115c7a45307a2a212750ae4cb", + "created_at": 1699597889, + "id": "a92a316b75e44cfdc19986c634049158d4206fcc0b7b9c7ccbcdabe28beebcd0", + "sig": "1173822c53261f8cffe7efbf43ba4a97a9198b3e402c2a1df130f42a8985a2d0d3430f4de350db184141e45ca844ab4e5364ea80f11d720e36357e1853dba6ca" +} +``` + +**REQUIREMENTS**: +- Replaceable event (one per user) +- `r` tags with relay URLs (typically local network relays) +- Optional `read` or `write` marker (if omitted, relay is both) +- **Inbox relays** (marked `read` or unmarked): Added to reading operations +- **Outbox relays** (marked `write` or unmarked): Added to publishing operations +- Used alongside kind 10002 - both kinds combined for relay selection +- **NOTE**: Kind number 10432 should be verified against current Nostr specifications -- **Frontend**: Svelte 5 (with runes: `$state`, `$derived`, `$effect`) + TypeScript + Vite -- **Styling**: Tailwind CSS (4chan-style minimal design) -- **Nostr Library**: [`applesauce-core`](https://github.com/hzrd149/applesauce) -- **Markdown**: `marked` + `DOMPurify` for sanitization -- **Storage**: IndexedDB (via `idb` or `localforage`) -- **Deployment**: Docker + Apache httpd (static serving on port 9876) -- **PWA**: Progressive Web App support +#### Kind 10000 (Mute List) -## Architecture +```json +{ + "id": "a92a316b75e44cfdc19986c634049158d4206fcc0b7b9c7ccbcdabe28beebcd0", + "pubkey": "854043ae8f1f97430ca8c1f1a090bdde6488bd5115c7a45307a2a212750ae4cb", + "created_at": 1699597889, + "kind": 10000, + "tags": [ + ["p", "07caba282f76441955b695551c3c5c742e5b9202a3784780f8086fdcdc1da3a9"], + ["p", "a55c15f5e41d5aebd236eca5e0142789c5385703f1a7485aa4b38d94fd18dcc4"] + ], + "content": "", + "sig": "1173822c53261f8cffe7efbf43ba4a97a9198b3e402c2a1df130f42a8985a2d0d3430f4de350db184141e45ca844ab4e5364ea80f11d720e36357e1853dba6ca" +} +``` -### Architecture Layers +**REQUIREMENTS**: +- Replaceable event (one per user) +- Filter out all events from pubkeys in `p` tags +- Applied automatically when user is logged in +#### Kind 10006 (Blocked Relays) + +```json +{ + "kind": 10006, + "content": "", + "tags": [ + ["relay", "wss://blocked-relay1.com"], + ["relay", "wss://blocked-relay2.com"] + ], + "pubkey": "854043ae8f1f97430ca8c1f1a090bdde6488bd5115c7a45307a2a212750ae4cb", + "created_at": 1699597889, + "id": "a92a316b75e44cfdc19986c634049158d4206fcc0b7b9c7ccbcdabe28beebcd0", + "sig": "1173822c53261f8cffe7efbf43ba4a97a9198b3e402c2a1df130f42a8985a2d0d3430f4de350db184141e45ca844ab4e5364ea80f11d720e36357e1853dba6ca" +} ``` -┌─────────────────────────────────────────────────────────┐ -│ UI Components Layer │ -│ (LandingPage, ThreadList, ThreadView, Profile, Feed) │ -└───────────────────────┬─────────────────────────────────┘ - │ -┌───────────────────────▼─────────────────────────────────┐ -│ Feature Modules Layer │ -│ (Threads, Comments, Zaps, Reactions, Profiles, Feed) │ -└───────────────────────┬─────────────────────────────────┘ - │ -┌───────────────────────▼─────────────────────────────────┐ -│ Core Services Layer │ -│ (Nostr Client, Relay Pool, Auth, Cache, Security) │ -└───────────────────────┬─────────────────────────────────┘ - │ -┌───────────────────────▼─────────────────────────────────┐ -│ Applesauce Library │ -│ (Nostr Protocol Implementation) │ -└─────────────────────────────────────────────────────────┘ + +**REQUIREMENTS**: +- Replaceable event (one per user) +- Remove relays in `relay` tags from all operations (read and write) +- Applied automatically when user is logged in +- Filtered before establishing connections + +#### Kind 10133 (Payment Targets, NIP-A3) + +```json +{ + "kind": 10133, + "content": "", + "tags": [ + ["payto", "bitcoin", "bc1qxq66e0t8d7ugdecwnmv58e90tpry23nc84pg9k"], + ["payto", "nano", "nano_1dctqbmqxfppo9pswbm6kg9d4s4mbraqn8i4m7ob9gnzz91aurmuho48jx3c"], + ["payto", "lightning", "user@wallet.example.com"] + ], + "pubkey": "afc93622eb4d79c0fb75e56e0c14553f7214b0a466abeba14cb38968c6755e6a", + "created_at": 1679673265, + "id": "67b48a14fb66c60c8f9070bdeb37afdfcc3d08ad01989460448e4081eddda446", + "sig": "f2cb581a84ed10e4dc84937bd98e27acac71ab057255f6aa8dfa561808c981fe8870f4a03c1e3666784d82a9c802d3704e174371aa13d63e2aeaf24ff5374d9d" +} +``` + +**REQUIREMENTS**: +- Replaceable event (one per user) +- Extract `payto` tags for payment address display +- Combine with kind 0 `lud16` tags (deduplicated) +- All addresses copyable +- Lightning addresses zappable +- Recognized types: bitcoin, lightning, ethereum, nano, monero, cashme, revolut, venmo +- Unrecognized types displayed generically + +#### Kind 30315 (General User Status, NIP-38) + +```json +{ + "kind": 30315, + "content": "Working on a new project", + "tags": [ + ["d", "general"], + ["client", "Aitherboard"] + ], + "pubkey": "854043ae8f1f97430ca8c1f1a090bdde6488bd5115c7a45307a2a212750ae4cb", + "created_at": 1699597889, + "id": "a92a316b75e44cfdc19986c634049158d4206fcc0b7b9c7ccbcdabe28beebcd0", + "sig": "1173822c53261f8cffe7efbf43ba4a97a9198b3e402c2a1df130f42a8985a2d0d3430f4de350db184141e45ca844ab4e5364ea80f11d720e36357e1853dba6ca" +} ``` -### Core Services Layer +**REQUIREMENTS**: +- Replaceable event (one per user) +- Display status content on profile page +- Fetch from same relays as kind 0 profiles +- Defined in NIP-38 + +## Relay Management + +### Base Relay Lists + +| Category | Relays | Purpose | +|----------|--------|---------| +| **Default Relays** | `wss://theforest.nostr1.com`
`wss://nostr21.com`
`wss://nostr.land`
`wss://nostr.wine`
`wss://nostr.sovbit.host` | Base relays for all operations | +| **Profile Relays** | `wss://relay.damus.io`
`wss://aggr.nostr.land`
`wss://profiles.nostr1.com` | Additional relays for profile/kind 1 content | + +### Relay Selection by Operation + +| Operation | Relays Used | Notes | +|-----------|------------|-------| +| **Reading: Threads (kind 11)** | Default + User inbox (if logged in) | Blocked relays removed | +| **Reading: Comments (kind 1111)** | Default + User inbox (if logged in) | Blocked relays removed | +| **Reading: Kind 1 feed** | Default + User inbox (if logged in) | Blocked relays removed | +| **Reading: Kind 1 responses** | Default + User inbox + `wss://aggr.nostr.land` | For threads beneath kind 1 OP events | +| **Reading: Zap receipts (kind 9735)** | Default + User inbox (if logged in) | Blocked relays removed | +| **Reading: Profiles (kind 0)** | Default + Profile + User inbox (if logged in) | Blocked relays removed | +| **Reading: Payment targets (kind 10133)** | Default + Profile + User inbox (if logged in) | Blocked relays removed | +| **Reading: User status (kind 30315)** | Default + Profile + User inbox (if logged in) | Blocked relays removed | +| **Publishing: Threads (kind 11)** | Default + `wss://thecitadel.nostr1.com` + User outbox | Blocked relays removed | +| **Publishing: Comments (kind 1111)** | Default + User outbox + Reply target's inbox | If replying, include target's inbox | +| **Publishing: Kind 1 posts** | Default + User outbox + Reply target's inbox | If replying, include target's inbox | +| **Publishing: Reactions (kind 7)** | Default + User outbox | Blocked relays removed | +| **Publishing: Zap requests (kind 9734)** | Not published to relays | Sent to wallet lnurl callback | +| **Publishing: Zap receipts (kind 9735)** | Relays from zap request | Published by wallet, not client | + +### Dynamic Relay Management + +| Source | Kind | When Applied | Effect | +|--------|------|--------------|--------| +| **User Inbox Relays** | 10002 + 10432 (marked `read` or unmarked) | When user logs in | Added to reading operations | +| **User Outbox Relays** | 10002 + 10432 (marked `write` or unmarked) | When user logs in | Added to publishing operations | +| **Blocked Relays** | 10006 | When user logs in | Removed from all operations | + +**REQUIREMENTS**: +- Relays combined, normalized, and deduplicated automatically +- Relay list updates dynamically when user logs in/out +- Blocked relays filtered before establishing connections +- All operations respect user's kind 10002, kind 10432, and kind 10006 preferences +- Kind 10002 and kind 10432 relays are combined (deduplicated) for each operation + +## Authentication -#### 1. Nostr Client Service (`src/lib/services/nostr/`) +### Authentication Methods -- `applesauce-client.ts` - Applesauce client initialization -- `relay-pool.ts` - Relay connection management -- `event-store.ts` - In-memory event cache with deduplication -- `subscription-manager.ts` - Active subscription tracking -- `auth-handler.ts` - NIP-42 AUTH challenge handling -- `config.ts` - Centralized configuration with env var validation +| Method | Implementation | Key Storage | +|--------|----------------|------------| +| **NIP-07** | Browser extension (Alby, nos2x, etc.) | No storage (extension manages) | +| **Nsec** | Direct bech32 nsec or hex private key, stored in the in-browser cache | **REQUIRED**: NIP-49 encrypted in localStorage | +| **NIP-46 Bunker** | Remote signer via `bunker://` URI | No local storage | +| **Anonymous** | Generated on the fly when publishing | **REQUIRED**: NIP-49 encrypted in IndexedDB | -#### 2. Authentication Service (`src/lib/services/auth/`) +### Key Storage & Encryption -- `nip07-signer.ts` - NIP-07 browser extension signer -- `nsec-signer.ts` - Direct nsec/hex key signer (with NIP-49 encryption) -- `bunker-signer.ts` - NIP-46 remote signer (bunker) -- `anonymous-signer.ts` - Anonymous session key generation -- `session-manager.ts` - Session persistence and key management -- `profile-fetcher.ts` - Kind 0 metadata fetching +**CRITICAL**: NO SECRET KEYS STORED ON THE SERVER +- All keys stored client-side only (IndexedDB/localStorage) +- Server only serves static files +- All key management in browser +- **REQUIRED**: All nsec keys (including anonymous) MUST be encrypted with NIP-49 (password-based) before storage +- Store as ncryptsec format (never plaintext nsec) +- Anonymous keys persist in IndexedDB across sessions +- Users can provide their own anonymous key (must be encrypted) -#### 3. Cache Service (`src/lib/services/cache/`) +### Anonymous User Behavior -- `indexeddb-store.ts` - IndexedDB wrapper -- `event-cache.ts` - Event caching and retrieval -- `profile-cache.ts` - Profile metadata caching -- `search-index.ts` - Full-text search index +- Pattern-based avatar +- Handle: `Aitherite{random}` +- Display with this format, whenever a kind 0 cannot be found, for the npub. -#### 4. Security Utilities (`src/lib/services/security/`) +## Content Modules -- `key-management.ts` - Key encryption/decryption (NIP-49) -- `bech32-utils.ts` - NIP-19 bech32 encoding/decoding -- `event-validator.ts` - Event signature and structure validation -- `sanitizer.ts` - HTML/content sanitization +### Event Publishing -### Feature Modules Layer +**REQUIREMENTS** (applies to all published events): +- **NIP-89 client tag**: Add `["client", "Aitherboard"]` tag to all published events when checkbox is selected +- Checkbox "Include client tag." displayed in all publish forms +- Checkbox selected by default +- Only include client tag if checkbox is selected (not deselected) +- Applies to: kind 1, kind 7, kind 11, kind 1111 (all user-published events) -#### 5. Thread Module (`src/lib/modules/threads/`) +### Thread Module (`src/lib/modules/threads/`) **Components**: - `ThreadList.svelte` - Main thread listing @@ -110,75 +537,99 @@ Aitherboard is a decentralized imageboard that uses the Nostr protocol for conte - `ThreadView.svelte` - Full thread display - `CreateThreadForm.svelte` - Thread creation -**Features**: -- Topic organization (max 3 `t` tags per thread) -- "General" category for threads without topics -- 30-day timeout (configurable) -- Thread statistics (comments, zaps, activity) +**REQUIREMENTS**: +- Max 3 `t` tags (topics) per thread +- Threads without topics under "General" +- 30-day timeout (configurable, checkbox to show older) - Sorting: newest, most active, most upvoted -- Plaintext preview extraction for landing page (no markdown/images) - -**Landing Page Thread Display**: -- Title -- Relative time of thread creation -- Relative time of latest response -- Profile badge -- Vote stats -- First 250 chars of plaintext (no markdown/images) +- Landing page: Title, relative times, profile badge, vote stats, first 250 chars plaintext (no markdown/images) +- **Client badge**: Display client name from `client` tag (NIP-89) on thread cards - subtle, muted color +- Render markdown in full thread view +- **Cover image**: Display image from `image` tag (NIP-23) prominently on thread view +- **Media attachments**: Handle NIP-92 (imeta) and NIP-94 (file) tags (see [Media Attachments](#media-attachments)) +- Display all target relays with deselection option +- Publication status modal: Success/failure per relay, error messages (hyperlink URLs), auto-close 30s or manual +- Include NIP-89 client tag (see [Event Publishing](#event-publishing)) -#### 6. Comment Module (`src/lib/modules/comments/`) +### Comment Module (`src/lib/modules/comments/`) **Components**: - `Comment.svelte` - Individual comment - `CommentThread.svelte` - Threaded comment list - `CommentForm.svelte` - Reply form -**Threading Style**: +**REQUIREMENTS**: - Flat list (no indentation) - Gray blurb showing parent preview - Click blurb to highlight/scroll to parent - Parse NIP-22 tags: `K`, `E`, `e`, `k`, `p`, `P` +- Render markdown content +- **Client badge**: Display client name from `client` tag (NIP-89) on comment cards - subtle, muted color +- Include NIP-89 client tag (see [Event Publishing](#event-publishing)) -#### 7. Zap Module (`src/lib/modules/zaps/`) +### Zap Module (`src/lib/modules/zaps/`) **Components**: - `ZapButton.svelte` - Zap action button - `ZapReceipt.svelte` - Zap receipt display +- `ZapInvoiceModal.svelte` - Invoice display (fallback) -**Features**: -- Create kind 9734 zap requests -- Send to recipient's lnurl callback (wallet) -- Display kind 9735 zap receipts (≥ configured threshold) -- Zap receipts displayed with ⚡ emoji -- Configurable minimum sat threshold (default: 1, must be 0 or positive) +**REQUIREMENTS**: +- **Primary**: Send zap request (kind 9734) via `lightning:` URI to external wallet +- **Fallback**: Fetch invoice from lnurl callback, display QR code + copyable text +- No in-app wallet connections +- Display kind 9735 receipts (≥ configured threshold, default: 1 sat) +- Display with ⚡ emoji +- Zap requests NOT published to relays (sent to wallet) -**Important**: Zap requests (9734) go to wallets, NOT relays. Only receipts (9735) are published to relays. - -#### 8. Reaction Module (`src/lib/modules/reactions/`) +### Reaction Module (`src/lib/modules/reactions/`) **Components**: -- `ReactionButtons.svelte` - Upvote/downvote buttons - -**Rules**: -- Only `+` and `-` content allowed -- One vote per user per event -- Clicking same button twice deletes the reaction - -#### 9. Profile Module (`src/lib/modules/profiles/`) +- `ReactionButtons.svelte` - For threads/comments (kind 11/1111) +- `Kind1ReactionButtons.svelte` - For kind 1 feed + +**REQUIREMENTS**: +- **Kind 11/1111**: Only `+` and `-` allowed + - `+` renders as upvote + - `-` renders as downvote + - **Backward compatibility**: `⬆️` (up arrow) counted as upvote, `⬇️` (down arrow) counted as downvote +- **Kind 1**: All reactions allowed + - `+` renders as ❤️ + - Default suggested: `+` + - Other reactions in submenu +- One reaction per user per event +- Clicking same reaction twice deletes it +- Display reaction counts +- Include NIP-89 client tag (see [Event Publishing](#event-publishing)) + +### Profile Module (`src/lib/modules/profiles/`) **Components**: - `ProfilePage.svelte` - User profile display -- `ProfileBadge.svelte` - Clickable profile badge -- `RecentlyActive.svelte` - Recently active users list - -**Features**: -- Display kind 0 metadata (name, about, picture, NIP-05) -- Show kind 1 feed from default relays + `wss://relay.damus.io` + `wss://aggr.nostr.land` +- `ProfileBadge.svelte` - Clickable profile badge with activity indicator and user status +- `PaymentAddresses.svelte` - Unified payment addresses + +**REQUIREMENTS**: +- **Kind 0**: Parse tags (preferred) OR fallback to JSON + - Support multiple: `lud16`, `website`, `nip05` + - Display: name, about, picture, NIP-05, website, lightning addresses +- **Kind 30315 (NIP-38)**: Fetch and display general user status + - Display status content on profile page + - Display status in profile badges on cards (thread cards, comment cards, etc.) + - Replaceable event (one per user) +- **Payment addresses**: Fetch kind 10133, combine with kind 0 `lud16` + - Deduplicate addresses + - All addresses copyable + - Lightning addresses zappable + - Support recognized types: bitcoin, lightning, ethereum, nano, monero, cashme, revolut, venmo + - Display unrecognized types generically +- **Profile badge activity indicator**: See [Content Rendering](#content-rendering) section +- Show kind 1 feed (see relay selection) +- Show kind 1 responses (see relay selection) - Allow zapping profile owner -- Allow replying to kind 1 notes with kind 1 replies (NIP-10) or zaps -- Recently active users tracking (15min/1hr/24hr windows) +- Allow replying to kind 1 with kind 1 or zap -#### 10. Kind 1 Feed Module (`src/lib/modules/feed/`) +### Kind 1 Feed Module (`src/lib/modules/feed/`) **Components**: - `Kind1FeedPage.svelte` - Main feed page @@ -188,157 +639,234 @@ Aitherboard is a decentralized imageboard that uses the Nostr protocol for conte - `CreateKind1Form.svelte` - Create new kind 1 events - `ReplyToKind1Form.svelte` - Reply to kind 1 events -**Features**: -- **"View feed" button** on landing page opens feed page -- Fetch kind 1 events from default relays -- **Create new kind 1 events** with markdown editor -- **Reply to kind 1 with kind 1** (NIP-10 threading) -- **Reply to kind 1 with zap** (display receipt as reply with ⚡ emoji) -- **Reply to kind 1 replies** with their own kind 1 -- **Reply to zap receipts** with: - - Zap receipts (zap the zapper) - - Kind 1111 comments -- All content rendered as markdown -- Flat threading display (no indentation, similar to comment threading) +**REQUIREMENTS**: +- "View feed" button on landing page opens `/feed` +- Fetch kind 1 events (see relay selection) +- Create new kind 1 events with markdown editor +- Reply to kind 1 with kind 1 (NIP-10 threading) +- Reply to kind 1 with zap (see zap module) +- Reply to kind 1 replies with kind 1 +- Reply to zap receipts with zap receipts or kind 1111 comments +- Display all reactions (not just upvotes) + - Default suggested: `+` (rendered as ❤️) + - Other reactions in submenu +- **Client badge**: Display client name from `client` tag (NIP-89) on kind 1 post cards - subtle, muted color +- Render all content as markdown +- **Media attachments**: Handle NIP-92 (imeta) and NIP-94 (file) tags (see [Media Attachments](#media-attachments)) +- Flat threading display (no indentation) - Real-time updates - Infinite scroll - -**Interaction Flow**: -1. User clicks "View feed" → Opens feed page at `/feed` -2. Feed displays kind 1 events from default relays -3. User can create new kind 1 events -4. User can reply to kind 1 events with kind 1 or zap -5. Zap receipts displayed as replies with ⚡ emoji -6. User can reply to kind 1 replies with kind 1 -7. User can reply to zap receipts with zap receipts or kind 1111 comments - -## Features - -### Authentication Methods - -1. **NIP-07 Extension**: Browser extension (e.g., Alby, nos2x) -2. **Nsec Login**: Direct bech32 nsec or hex private key - - Optional NIP-49 encryption (ncryptsec) with password - - Stored encrypted in localStorage -3. **NIP-46 Bunker**: Remote signer via bunker connection string - - `bunker://` URI scheme - - WebSocket connection to remote signer -4. **Anonymous**: Temporary nsec for session - - Generated on first visit - - Pattern-based avatar - - Handle: `Aitherite{random}` - - Only allows zap requests (no comments) - -### Content Display - -- **Threads (Kind 11)**: Discussion threads with topics -- **Comments (Kind 1111)**: Threaded comments with NIP-22 tags -- **Zap Receipts (Kind 9735)**: Lightning payments displayed as comments with ⚡ -- **Kind 1 Feed**: Twitter-like feed with replies and zaps -- **Reactions (Kind 7)**: Upvote/downvote system -- **Markdown Rendering**: All content supports full markdown -- **NIP-21 Links**: - - `nostr:npub...` → Clickable profile badge +- Include NIP-89 client tag (see [Event Publishing](#event-publishing)) + +## User Interface + +### User Preferences & Readability + +**REQUIREMENTS**: +- **Text size control**: Simple control (small/medium/large) to adjust base font size + - Small: ~14px base + - Medium: ~16px base (default) + - Large: ~18px base + - Preference stored in localStorage + - Applied via CSS custom properties or data attributes +- **Line spacing**: Adjustable line height for better readability + - Tight: 1.4 + - Normal: 1.6 (default) + - Loose: 1.8 +- **Content width**: Maximum content width control + - Narrow: ~600px + - Medium: ~800px (default) + - Wide: ~1200px +- **Theme**: Light/dark mode toggle + - Support system preference detection (`prefers-color-scheme`) + - Manual toggle to override system preference + - Preference stored in localStorage + - Smooth transition between modes +- All preferences persist in localStorage +- Preferences accessible from main navigation/settings menu +- Clear visual feedback when preferences change + +### Content Rendering + +**REQUIREMENTS**: +- All content supports full markdown rendering +- Videos MUST NOT autoplay (require user interaction) +- Images lazy-loaded for performance +- NIP-21 link parsing: + - `nostr:npub...` → Clickable profile badge (with activity indicator and user status) - `nostr:nevent/note/naddr...` → Event card (250 char preview) → Event viewer +- **Client badges**: Display client name from `client` tag (NIP-89) on all event cards + - Display on: thread cards, comment cards, kind 1 post cards + - Subtle, muted color styling + - Small badge/chip format +- **Profile badge**: + - **Activity indicator** (custom feature, separate from kind 30315): + - Track last activity from all events in relay pool per pubkey + - **Red**: ≥168 hours (7 days) since last interaction + - **Yellow**: ≥48 hours (2 days) but <168 hours since last interaction + - **Green**: <48 hours since last interaction + - Display indicator on all profile badges + - **User status (kind 30315, NIP-38)**: Display user status text in profile badges on cards +- **Content filtering**: Hide content with sensitive tags or #NSFW hashtag + - Hide events with `content-warning` tag or `sensitive` tag + - Hide events with `#NSFW` hashtag in content or tags + - Content is completely hidden (not displayed with warning) +- **Media handling**: See [Media Attachments](#media-attachments) section + +### Media Attachments + +**REQUIREMENTS**: +- **Support NIP-92 (imeta tags)**: Media metadata for images, videos, audio + - Format: `["imeta", "url ", "m ", "x ", "y ", ...]` + - Additional fields: `dim` (dimensions for video), `blurhash`, `thumb`, etc. +- **Support NIP-94 (file tags)**: File attachments + - Format: `["file", "", "", "size ", ...]` +- **Support NIP-23 image tags**: Cover images for threads/articles + - Format: `["image", ""]` +- **Media URL normalization and deduplication**: + - Extract media URLs from: + - `imeta` tags (url field) + - `file` tags (first element after "file") + - `image` tags (second element) + - Markdown image syntax in content: `![alt](url)` + - HTML ``, `