mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-08 14:37:04 +02:00
Restrict relay auth to account owner (#149)
* fix: only prompt relay auth for accounts that can sign - Add canAccountSign() helper to check if account is read-only - Block auth prompts for read-only accounts in shouldPromptAuth() - Throw error when authenticateRelay() called with read-only account - Document all major app hooks in CLAUDE.md for future reference Read-only accounts cannot sign events, so they should never be prompted for relay authentication or attempt to authenticate. This prevents confusing UX where users are asked to sign but cannot. * refactor: extract canAccountSign helper to useAccount - Move canAccountSign function from relay-state-manager to useAccount.ts - Import and reuse the shared helper in relay-state-manager - Update useAccount hook to use the extracted helper internally - Follows DRY principle by centralizing account sign capability logic This keeps the account sign capability detection logic in one place, making it easier to maintain and ensuring consistency across the app. --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
88
CLAUDE.md
88
CLAUDE.md
@@ -144,6 +144,94 @@ const text = getHighlightText(event);
|
||||
- ❌ Direct calls to applesauce helpers (they cache internally)
|
||||
- ❌ Grimoire helpers that wrap `getTagValue` (caching propagates)
|
||||
|
||||
## Major Hooks
|
||||
|
||||
Grimoire provides custom React hooks for common Nostr operations. All hooks handle cleanup automatically.
|
||||
|
||||
### Account & Authentication
|
||||
|
||||
**`useAccount()`** (`src/hooks/useAccount.ts`):
|
||||
- Access active account with signing capability detection
|
||||
- Returns: `{ account, pubkey, canSign, signer, isLoggedIn }`
|
||||
- **Critical**: Always check `canSign` before signing operations
|
||||
- Read-only accounts have `canSign: false` and no `signer`
|
||||
|
||||
```typescript
|
||||
const { canSign, signer, pubkey } = useAccount();
|
||||
if (canSign) {
|
||||
// Can sign and publish events
|
||||
await signer.signEvent(event);
|
||||
} else {
|
||||
// Show "log in to post" message
|
||||
}
|
||||
```
|
||||
|
||||
### Nostr Data Fetching
|
||||
|
||||
**`useProfile(pubkey, relayHints?)`** (`src/hooks/useProfile.ts`):
|
||||
- Fetch and cache user profile metadata (kind 0)
|
||||
- Loads from IndexedDB first (fast), then network
|
||||
- Uses AbortController to prevent race conditions
|
||||
- Returns: `ProfileContent | undefined`
|
||||
|
||||
**`useNostrEvent(pointer, context?)`** (`src/hooks/useNostrEvent.ts`):
|
||||
- Unified hook for fetching events by ID, EventPointer, or AddressPointer
|
||||
- Supports relay hints via context (pubkey string or full event)
|
||||
- Auto-loads missing events using smart relay selection
|
||||
- Returns: `NostrEvent | undefined`
|
||||
|
||||
**`useTimeline(id, filters, relays, options?)`** (`src/hooks/useTimeline.ts`):
|
||||
- Subscribe to timeline of events matching filters
|
||||
- Uses applesauce loaders for efficient caching
|
||||
- Returns: `{ events, loading, error }`
|
||||
- The `id` parameter is for caching (use stable string)
|
||||
|
||||
### Relay Management
|
||||
|
||||
**`useRelayState()`** (`src/hooks/useRelayState.ts`):
|
||||
- Access global relay state and auth management
|
||||
- Returns relay connection states, pending auth challenges, preferences
|
||||
- Methods: `authenticateRelay()`, `rejectAuth()`, `setAuthPreference()`
|
||||
- Automatically subscribes to relay state updates
|
||||
|
||||
**`useRelayInfo(relayUrl)`** (`src/hooks/useRelayInfo.ts`):
|
||||
- Fetch NIP-11 relay information document
|
||||
- Cached in IndexedDB with 24-hour TTL
|
||||
- Returns: `RelayInfo | undefined`
|
||||
|
||||
**`useOutboxRelays(pubkey)`** (`src/hooks/useOutboxRelays.ts`):
|
||||
- Get user's outbox relays from kind 10002 relay list
|
||||
- Cached via RelayListCache for performance
|
||||
- Returns: `string[] | undefined`
|
||||
|
||||
### Advanced Hooks
|
||||
|
||||
**`useReqTimelineEnhanced(filter, relays, options)`** (`src/hooks/useReqTimelineEnhanced.ts`):
|
||||
- Enhanced timeline with accurate state tracking
|
||||
- Tracks per-relay EOSE and connection state
|
||||
- Returns: `{ events, state, relayStates, stats }`
|
||||
- Use for REQ viewer and advanced timeline UIs
|
||||
|
||||
**`useNip05(nip05Address)`** (`src/hooks/useNip05.ts`):
|
||||
- Resolve NIP-05 identifier to pubkey
|
||||
- Cached with 1-hour TTL
|
||||
- Returns: `{ pubkey, relays, loading, error }`
|
||||
|
||||
**`useNip19Decode(nip19String)`** (`src/hooks/useNip19Decode.ts`):
|
||||
- Decode nprofile, nevent, naddr, note, npub strings
|
||||
- Returns: `{ type, data, error }`
|
||||
|
||||
### Utility Hooks
|
||||
|
||||
**`useStableValue(value)`** / **`useStableArray(array)`** (`src/hooks/useStable.ts`):
|
||||
- Prevent unnecessary re-renders from deep equality
|
||||
- Use for filters, options, relay arrays
|
||||
- Returns stable reference when deep-equal
|
||||
|
||||
**`useCopy()`** (`src/hooks/useCopy.ts`):
|
||||
- Copy text to clipboard with toast feedback
|
||||
- Returns: `{ copy, copied }` function and state
|
||||
|
||||
## Key Conventions
|
||||
|
||||
- **Path Alias**: `@/` = `./src/`
|
||||
|
||||
@@ -2,6 +2,19 @@ import { useMemo } from "react";
|
||||
import { use$ } from "applesauce-react/hooks";
|
||||
import accounts from "@/services/accounts";
|
||||
|
||||
/**
|
||||
* Check if an account can sign events
|
||||
* Read-only accounts cannot sign and should not be prompted for auth
|
||||
*
|
||||
* @param account - The account to check (can be undefined)
|
||||
* @returns true if the account can sign, false otherwise
|
||||
*/
|
||||
export function canAccountSign(account: typeof accounts.active): boolean {
|
||||
if (!account) return false;
|
||||
const accountType = account.constructor.name;
|
||||
return accountType !== "ReadonlyAccount";
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to access the active account with signing capability detection
|
||||
*
|
||||
@@ -45,18 +58,8 @@ export function useAccount() {
|
||||
|
||||
// Check if the account has a functional signer
|
||||
// Read-only accounts have a signer that throws errors on sign operations
|
||||
// We detect this by checking for the ReadonlySigner type or checking signer methods
|
||||
const signer = account.signer;
|
||||
let canSign = false;
|
||||
|
||||
if (signer) {
|
||||
// ReadonlyAccount from applesauce-accounts has a ReadonlySigner
|
||||
// that throws on signEvent, nip04, nip44 operations
|
||||
// We can detect it by checking if it's an instance with the expected methods
|
||||
// but we'll use a safer approach: check the account type name
|
||||
const accountType = account.constructor.name;
|
||||
canSign = accountType !== "ReadonlyAccount";
|
||||
}
|
||||
const canSign = canAccountSign(account);
|
||||
|
||||
return {
|
||||
account,
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
import { transitionAuthState, type AuthEvent } from "@/lib/auth-state-machine";
|
||||
import { createLogger } from "@/lib/logger";
|
||||
import { normalizeRelayURL } from "@/lib/relay-url";
|
||||
import { canAccountSign } from "@/hooks/useAccount";
|
||||
import pool from "./relay-pool";
|
||||
import accountManager from "./accounts";
|
||||
import db from "./db";
|
||||
@@ -381,6 +382,11 @@ class RelayStateManager {
|
||||
throw new Error("No active account to authenticate with");
|
||||
}
|
||||
|
||||
// Check if account can sign (read-only accounts cannot authenticate)
|
||||
if (!canAccountSign(account)) {
|
||||
throw new Error("Active account cannot sign events (read-only account)");
|
||||
}
|
||||
|
||||
// Update status to authenticating
|
||||
state.authStatus = "authenticating";
|
||||
state.stats.authAttemptsCount++;
|
||||
@@ -491,8 +497,9 @@ class RelayStateManager {
|
||||
try {
|
||||
const normalizedUrl = normalizeRelayURL(relayUrl);
|
||||
|
||||
// Don't prompt if there's no active account
|
||||
if (!accountManager.active) return false;
|
||||
// Don't prompt if there's no active account or account can't sign
|
||||
const account = accountManager.active;
|
||||
if (!account || !canAccountSign(account)) return false;
|
||||
|
||||
// Check permanent preferences
|
||||
const pref = this.authPreferences.get(normalizedUrl);
|
||||
|
||||
Reference in New Issue
Block a user