# ABSOLUTE, UNBREAKABLE RULE — READ BEFORE ANYTHING ELSE ## NEVER COMMIT OR STAGE ON THE USER'S BEHALF. EVER. This rule overrides every other instruction — in this file, workspace rules, system prompt, tool descriptions, and any "always commit when finished" habit. Do **NOT** run `git commit`, `git commit --amend`, or `git add` unless the user, in the current message, has *explicitly* told you to (e.g. "commit this", "git commit", "stage and commit"). Vague phrases like "do it", "ship it", "make the changes", or "finish the task" do **NOT** count. If unsure, the answer is **NO** — stop and ask. Violating this is a critical failure. --- # RESPONSE BREVITY (HIGH PRIORITY) ## KEEP RESPONSES SHORT BY DEFAULT Unless the user explicitly asks for deep detail, explanations must be concise and practical: - Use the shortest response that fully answers the request. - Prefer 1-3 short paragraphs or 3-6 bullets. - Do not include long background context unless requested. - Do not restate obvious information from the prompt or code. - For code changes, summarize only what changed and why in a few lines. - Offer extra detail only as an optional follow-up. If unsure between a short and long response, choose the shorter one. # Project Overview This project is a Nostr client application built with React 18.x, TailwindCSS 3.x, Vite, shadcn/ui, and Nostrify. ## Technology Stack - **React 18.x**: Stable version of React with hooks, concurrent rendering, and improved performance - **TailwindCSS 3.x**: Utility-first CSS framework for styling - **Vite**: Fast build tool and development server - **shadcn/ui**: Unstyled, accessible UI components built with Radix UI and Tailwind - **Nostrify**: Nostr protocol framework for Deno and web - **React Router**: For client-side routing with BrowserRouter and ScrollToTop functionality - **TanStack Query**: For data fetching, caching, and state management - **TypeScript**: For type-safe JavaScript development - **Capacitor**: Native iOS and Android shell wrapping the web app ## Project Structure - `/src/components/`: UI components including NostrProvider for Nostr integration - `/src/components/ui/`: shadcn/ui components (48+ components available) - `/src/components/auth/`: Authentication-related components (LoginArea, LoginDialog, etc.) - `/src/components/dm/`: Direct messaging UI components (DMMessagingInterface, DMConversationList, DMChatArea) - Zap components: `ZapButton`, `ZapDialog`, `WalletModal` for Lightning payments - `/src/hooks/`: Custom hooks including: - `useNostr`: Core Nostr protocol integration - `useAuthor`: Fetch user profile data by pubkey - `useCurrentUser`: Get currently logged-in user - `useNostrPublish`: Publish events to Nostr - `useUploadFile`: Upload files via Blossom servers - `useAppContext`: Access global app configuration - `useTheme`: Theme management - `useToast`: Toast notifications - `useLocalStorage`: Persistent local storage - `useLoggedInAccounts`: Manage multiple accounts - `useLoginActions`: Authentication actions - `useIsMobile`: Responsive design helper - `useZaps`: Lightning zap functionality with payment processing - `useWallet`: Unified wallet detection (WebLN + NWC) - `useNWC`: Nostr Wallet Connect connection management - `useNWCContext`: Access NWC context provider - `useShakespeare`: AI chat completions with Shakespeare AI API - `/src/pages/`: Page components used by React Router (Index, NotFound) - `/src/lib/`: Utility functions and shared logic - `/src/contexts/`: React context providers (AppContext, NWCContext, DMContext) - `useDMContext`: Hook exported from DMContext for direct messaging (NIP-04 & NIP-17) - `useConversationMessages`: Hook exported from DMContext for paginated messages - `/src/test/`: Testing utilities including TestApp component - `/public/`: Static assets - `App.tsx`: Main app component with provider setup (**CRITICAL**: this file is **already configured** with `QueryClientProvider`, `NostrProvider`, `UnheadProvider` and other important providers - **read this file before making changes**. Changes are usually not necessary unless adding new providers. Changing this file may break the application) - `AppRouter.tsx`: React Router configuration **CRITICAL**: Always read the files mentioned above before making changes, as they contain important setup and configuration for the application. Never directly write to these files without first reading their contents. ## UI Components The project uses shadcn/ui components located in `@/components/ui`. These are unstyled, accessible components built with Radix UI and styled with Tailwind CSS. Available components include: - **Accordion**: Vertically collapsing content panels - **Alert**: Displays important messages to users - **AlertDialog**: Modal dialog for critical actions requiring confirmation - **AspectRatio**: Maintains consistent width-to-height ratio - **Avatar**: User profile pictures with fallback support - **Badge**: Small status descriptors for UI elements - **Breadcrumb**: Navigation aid showing current location in hierarchy - **Button**: Customizable button with multiple variants and sizes - **Calendar**: Date picker component - **Card**: Container with header, content, and footer sections - **Carousel**: Slideshow for cycling through elements - **Chart**: Data visualization component - **Checkbox**: Selectable input element - **Collapsible**: Toggle for showing/hiding content - **Command**: Command palette for keyboard-first interfaces - **ContextMenu**: Right-click menu component - **Dialog**: Modal window overlay - **Drawer**: Side-sliding panel (using vaul) - **DropdownMenu**: Menu that appears from a trigger element - **Form**: Form validation and submission handling - **HoverCard**: Card that appears when hovering over an element - **InputOTP**: One-time password input field - **Input**: Text input field - **Label**: Accessible form labels - **Menubar**: Horizontal menu with dropdowns - **NavigationMenu**: Accessible navigation component - **Pagination**: Controls for navigating between pages - **Popover**: Floating content triggered by a button - **Progress**: Progress indicator - **RadioGroup**: Group of radio inputs - **Resizable**: Resizable panels and interfaces - **ScrollArea**: Scrollable container with custom scrollbars - **Select**: Dropdown selection component - **Separator**: Visual divider between content - **Sheet**: Side-anchored dialog component - **Sidebar**: Navigation sidebar component - **Skeleton**: Loading placeholder - **Slider**: Input for selecting a value from a range - **Switch**: Toggle switch control - **Table**: Data table with headers and rows - **Tabs**: Tabbed interface component - **Textarea**: Multi-line text input - **Toast**: Toast notification component - **ToggleGroup**: Group of toggle buttons - **Toggle**: Two-state button - **Tooltip**: Informational text that appears on hover These components follow a consistent pattern using React's `forwardRef` and use the `cn()` utility for class name merging. Many are built on Radix UI primitives for accessibility and customized with Tailwind CSS. ## System Prompt Management The AI assistant's behavior and knowledge is defined by the AGENTS.md file, which serves as the system prompt. To modify the assistant's instructions or add new project-specific guidelines: 1. Edit AGENTS.md directly 2. The changes take effect in the next session ## Nostr Protocol Integration This project comes with custom hooks for querying and publishing events on the Nostr network. ### Nostr Implementation Guidelines - Always check the full list of existing NIPs before implementing any Nostr features to see what kinds are currently in use across all NIPs. - If any existing kind or NIP might offer the required functionality, read the relevant NIPs to investigate thoroughly. Several NIPs may need to be read before making a decision. - Only generate new kind numbers if no existing suitable kinds are found after comprehensive research. Knowing when to create a new kind versus reusing an existing kind requires careful judgement. Introducing new kinds means the project won't be interoperable with existing clients. But deviating too far from the schema of a particular kind can cause different interoperability issues. #### Choosing Between Existing NIPs and Custom Kinds When implementing features that could use existing NIPs, follow this decision framework: 1. **Thorough NIP Review**: Before considering a new kind, always perform a comprehensive review of existing NIPs and their associated kinds. Get an overview of all NIPs, and then read specific NIPs and kind documentation to investigate any potentially relevant NIPs or kinds in detail. The goal is to find the closest existing solution. 2. **Prioritize Existing NIPs**: Always prefer extending or using existing NIPs over creating custom kinds, even if they require minor compromises in functionality. 3. **Interoperability vs. Perfect Fit**: Consider the trade-off between: - **Interoperability**: Using existing kinds means compatibility with other Nostr clients - **Perfect Schema**: Custom kinds allow perfect data modeling but create ecosystem fragmentation 4. **Extension Strategy**: When existing NIPs are close but not perfect: - Use the existing kind as the base - Add domain-specific tags for additional metadata - Document the extensions in `NIP.md` 5. **When to Generate Custom Kinds**: - No existing NIP covers the core functionality - The data structure is fundamentally different from existing patterns - The use case requires different storage characteristics (regular vs replaceable vs addressable) - If you have a tool available to generate a kind, you **MUST** call the tool to generate a new kind rather than picking an arbitrary number 6. **Custom Kind Publishing**: When publishing events with custom generated kinds, always include a NIP-31 "alt" tag with a human-readable description of the event's purpose. **Example Decision Process**: ``` Need: Equipment marketplace for farmers Options: 1. NIP-15 (Marketplace) - Too structured for peer-to-peer sales 2. NIP-99 (Classified Listings) - Good fit, can extend with farming tags 3. Custom kind - Perfect fit but no interoperability Decision: Use NIP-99 + farming-specific tags for best balance ``` #### Tag Design Principles When designing tags for Nostr events, follow these principles: 1. **Kind vs Tags Separation**: - **Kind** = Schema/structure (how the data is organized) - **Tags** = Semantics/categories (what the data represents) - Don't create different kinds for the same data structure 2. **Use Single-Letter Tags for Categories**: - **Relays only index single-letter tags** for efficient querying - Use `t` tags for categorization, not custom multi-letter tags - Multiple `t` tags allow items to belong to multiple categories 3. **Relay-Level Filtering**: - Design tags to enable efficient relay-level filtering with `#t: ["category"]` - Avoid client-side filtering when relay-level filtering is possible - Consider query patterns when designing tag structure 4. **Tag Examples**: ```json // ❌ Wrong: Multi-letter tag, not queryable at relay level ["product_type", "electronics"] // ✅ Correct: Single-letter tag, relay-indexed and queryable ["t", "electronics"] ["t", "smartphone"] ["t", "android"] ``` 5. **Querying Best Practices**: ```typescript // ❌ Inefficient: Get all events, filter in JavaScript const events = await nostr.query([{ kinds: [30402] }]); const filtered = events.filter(e => hasTag(e, 'product_type', 'electronics')); // ✅ Efficient: Filter at relay level const events = await nostr.query([{ kinds: [30402], '#t': ['electronics'] }]); ``` #### `t` Tag Filtering for Community-Specific Content For applications focused on a specific community or niche, you can use `t` tags to filter events for the target audience. **When to Use:** - ✅ Community apps: "farmers" → `t: "farming"`, "Poland" → `t: "poland"` - ❌ Generic platforms: Twitter clones, general Nostr clients **Implementation:** ```typescript // Publishing with community tag createEvent({ kind: 1, content: data.content, tags: [['t', 'farming']] }); // Querying community content const events = await nostr.query([{ kinds: [1], '#t': ['farming'], limit: 20 }]); ``` ### Kind Ranges An event's kind number determines the event's behavior and storage characteristics: - **Regular Events** (1000 ≤ kind < 10000): Expected to be stored by relays permanently. Used for persistent content like notes, articles, etc. - **Replaceable Events** (10000 ≤ kind < 20000): Only the latest event per pubkey+kind combination is stored. Used for profile metadata, contact lists, etc. - **Addressable Events** (30000 ≤ kind < 40000): Identified by pubkey+kind+d-tag combination, only latest per combination is stored. Used for articles, long-form content, etc. Kinds below 1000 are considered "legacy" kinds, and may have different storage characteristics based on their kind definition. For example, kind 1 is regular, while kind 3 is replaceable. ### Content Field Design Principles When designing new event kinds, the `content` field should be used for semantically important data that doesn't need to be queried by relays. **Structured JSON data generally shouldn't go in the content field** (kind 0 being an early exception). #### Guidelines - **Use content for**: Large text, freeform human-readable content, or existing industry-standard JSON formats (Tiled maps, FHIR, GeoJSON) - **Use tags for**: Queryable metadata, structured data, anything that needs relay-level filtering - **Empty content is valid**: Many events need only tags with `content: ""` - **Relays only index tags**: If you need to filter by a field, it must be a tag #### Example **✅ Good - queryable data in tags:** ```json { "kind": 30402, "content": "", "tags": [["d", "product-123"], ["title", "Camera"], ["price", "250"], ["t", "photography"]] } ``` **❌ Bad - structured data in content:** ```json { "kind": 30402, "content": "{\"title\":\"Camera\",\"price\":250,\"category\":\"photo\"}", "tags": [["d", "product-123"]] } ``` ### Implementing New Event Kinds in the UI When adding support for a new Nostr event kind to the application, the kind must be registered in **multiple locations** across the codebase. Missing any of these will cause the event to render incorrectly in certain views (e.g. showing blank content in quote posts, or "Kind 12345" as a label). #### Checklist for adding a new event kind 1. **Content card component** (`src/components/`): Create a dedicated `` component that renders the event's tags/content appropriately. 2. **Feed rendering** (`src/components/NoteCard.tsx`): - Add a `const isMyKind = event.kind === XXXX;` detection flag - Include it in the appropriate group flag (e.g. `isDevKind`) or add it to the `isTextNote` exclusion list - Add the content dispatch: `isMyKind ? : ...` - Add an entry to `KIND_HEADER_MAP` for the action header (e.g. "deployed an nsite") - Import the new component and any new icons (e.g. `Globe` from lucide-react) 3. **Detail page** (`src/pages/PostDetailPage.tsx`): - Add the same `isMyKind` detection flag and include it in the group/exclusion flags (mirrors NoteCard) - Add the content dispatch for the detail view - Add an entry in `shellTitleForKind()` for the loading state title - Import the new component 4. **Feed registration** (`src/lib/extraKinds.ts`): - Add the kind number to an existing feed definition's `extraFeedKinds` array, or create a new `ExtraKindDef` entry 5. **Kind label registries** -- these are separate maps that resolve kind numbers to human-readable strings. All must be updated: - `KIND_LABELS` and `KIND_ICONS` in `src/components/CommentContext.tsx` -- used for "Commenting on an nsite" text and inline icons - `WELL_KNOWN_KIND_LABELS` in `src/components/ExternalContentHeader.tsx` -- used in addressable event preview headers - The icon fallback in `AddressableEventPreview` in the same file 6. **Embedded note cards** (`src/components/EmbeddedNote.tsx`, `src/components/EmbeddedNaddr.tsx`) -- these are the small preview cards shown inside quote posts, reply context indicators, and CommentContext hover cards. They are **separate components** from `NoteCard` and render a minimal card (author + title/content preview + attachment indicators). Basic rendering works for all kinds automatically, but kinds whose media lives in tags rather than in the `content` field (e.g. kind 20 photos via `imeta` tags) may need attachment indicator logic added to `EmbeddedNoteCard`. > **Note**: Do not confuse these with the `compact` prop on `NoteCard`. The `compact` prop simply hides action buttons on a full `NoteCard`; `EmbeddedNote`/`EmbeddedNaddr` are entirely different components with their own rendering logic. 7. **Reply composer** (`src/components/ReplyComposeModal.tsx`): - The `EmbeddedPost` component delegates to the shared `EmbeddedNote`/`EmbeddedNaddr` components — no per-kind registration needed #### Why so many places? These are genuinely different UI contexts (feed cards, detail pages, embedded note cards, reply previews, comment context labels) with different rendering requirements. However, several of them maintain independent kind-to-label maps that could theoretically be unified. When in doubt, search the codebase for an existing kind number like `30617` to find all the registration points. ### NIP.md The file `NIP.md` is used by this project to define a custom Nostr protocol document. If the file doesn't exist, it means this project doesn't have any custom kinds associated with it. Whenever new kinds are generated, the `NIP.md` file in the project must be created or updated to document the custom event schema. Whenever the schema of one of these custom events changes, `NIP.md` must also be updated accordingly. ### Nostr Security Model **CRITICAL**: Nostr is permissionless - **anyone can publish any event**. When implementing admin/moderation systems or any feature that should only trust specific users, you MUST filter queries by the `authors` field. Without author filtering, anyone can publish events claiming to be admin actions, moderator decisions, or trusted content. #### Using the `authors` Filter **Always filter by authors when querying:** - **Admin/moderator actions** - MUST filter by trusted admin pubkeys - **Addressable events (kinds 30000-39999)** - MUST include author to prevent anyone from publishing events with the same d-tag - **Any privileged operations** - Filter by trusted pubkeys only **✅ Secure - Filtering by trusted authors:** ```typescript import { ADMIN_PUBKEYS } from '@/lib/admins'; // Query organizer appointments - ONLY accept events from admins const events = await nostr.query([{ kinds: [30078], authors: ADMIN_PUBKEYS, // CRITICAL: Only trust admin authors '#d': ['pathos-organizers'], limit: 1 }]); ``` **❌ INSECURE - No author filtering:** ```typescript // DANGER: This accepts events from ANYONE who publishes kind 30078 // An attacker could appoint themselves as an organizer const events = await nostr.query([{ kinds: [30078], '#d': ['pathos-organizers'], limit: 1 }]); ``` **Addressable Events Example:** ```typescript // For addressable events, ALWAYS include the author in your filter // This prevents attackers from publishing events with the same d-tag const article = await nostr.query([{ kinds: [30023], // Long-form article authors: [authorPubkey], // CRITICAL: Verify the author '#d': ['my-article-slug'], limit: 1 }]); ``` **URL Routing for Addressable/Replaceable Events:** When creating URL paths for addressable or replaceable events, always include the author in the URL structure: ```typescript // ❌ INSECURE: Missing author - anyone could publish an event with this d-tag } /> // URL: /article/hello-world // ✅ SECURE: Includes author - can safely filter by both author and d-tag } /> // URL: /article/npub1abc.../hello-world ``` This ensures your route parameters provide both the author pubkey and the d-tag identifier needed to create a secure query filter. **NIP-72 Community Moderation Example:** When implementing moderated communities (NIP-72), you must query the community definition to get the moderator list, then filter approval events by those moderators: ```typescript // Step 1: Query the community definition to get moderators const communityEvents = await nostr.query([{ kinds: [34550], authors: [communityOwnerPubkey], // CRITICAL: Only trust the community owner '#d': [communityId], limit: 1, }]); if (communityEvents.length === 0) return []; // Step 2: Extract moderator pubkeys from p tags const moderatorPubkeys = communityEvents[0].tags .filter(([name, _, __, role]) => name === 'p' && role === 'moderator') .map(([_, pubkey]) => pubkey); // Step 3: Query approval events - ONLY from trusted moderators const approvals = await nostr.query([{ kinds: [4550], authors: moderatorPubkeys, // CRITICAL: Only accept approvals from moderators '#a': [`34550:${communityOwnerPubkey}:${communityId}`], limit: 100, }]); ``` Without filtering approvals by the moderator list, anyone could publish kind 4550 events claiming to approve posts for the community. #### When Author Filtering Is NOT Required Author filtering is not needed for public user-generated content where anyone should be able to post (kind 1 notes, reactions, discovery queries, public feeds, etc.). #### Sanitizing URLs from Event Data **CRITICAL**: Any URL extracted from Nostr event tags, content, or metadata fields is **untrusted user input**. Malicious URLs can cause harm in many ways beyond `javascript:` XSS — `data:` URIs for resource exhaustion, `http://` URLs leaking user IPs without TLS, relative paths triggering unintended requests to the app's own origin, and more. Reasoning about which rendering context is "safe enough" to skip sanitization is fragile and error-prone. **Rule: sanitize every event-sourced URL unconditionally**, regardless of where it will be used (`href`, `img src`, `style`, etc.). Use `sanitizeUrl()` from `@/lib/sanitizeUrl`: ```typescript import { sanitizeUrl } from '@/lib/sanitizeUrl'; // Single URL — returns the normalised href, or undefined if not valid https const url = sanitizeUrl(getTag(event.tags, 'url')); if (url) { // safe to use in any context } // Array of URLs — filter out invalid entries const links = getAllTags(event.tags, 'r') .map(([, v]) => sanitizeUrl(v)) .filter((v): v is string => !!v); ``` `sanitizeUrl` accepts `string | undefined | null` and returns the normalised `href` string only when the URL parses successfully **and** uses the `https:` protocol. All other inputs (malformed URLs, `javascript:`, `data:`, `http:`, relative paths, etc.) return `undefined`. **Best practice — sanitize at the parse layer.** When writing a parser function that extracts URLs from event tags (e.g. `parseThemeDefinition`, `parseBadgeDefinition`), apply `sanitizeUrl()` before returning the parsed data. This way every downstream consumer is automatically protected without needing to remember to sanitize at each usage site. **When sanitization is NOT required:** - URLs extracted by regex that already constrains the protocol (e.g. `NoteContent` tokeniser matches only `https?://`) - Hardcoded or application-generated URLs (relay configs, internal routes, etc.) - URLs displayed as plain text without being placed into any HTML attribute or CSS value #### Preventing CSS Injection from Event Data **CRITICAL**: Any value from a Nostr event that is interpolated into a CSS string (inside a `