Compare commits

..

11 Commits

Author SHA1 Message Date
Patrick
6e53f9082e Initial commit: MKStack template with Welshman pool manager
Some checks failed
Deploy to GitHub Pages / deploy (push) Has been cancelled
Test / test (push) Has been cancelled
- Replaced NPool with NWelshman for intelligent relay management
- Added router configuration for specialized relay selection
- Implemented quality scoring for relay prioritization
- Added comprehensive unit and integration tests
- Updated README with detailed documentation
- Configured redundancy and connection limits for better reliability
2025-08-21 15:12:01 -04:00
Alex Gleason
df80653a83
Update mcp servers 2025-08-18 21:55:35 -05:00
Alex Gleason
a862d30e8e
Remove sonner 2025-08-09 19:45:54 -08:00
Alex Gleason
5eeb424d43
Revert "Add .ai-history/ to .gitignore"
This reverts commit b6d31cdd9d10161879215930eae8060af122531c.
2025-07-30 00:11:29 -05:00
Alex Gleason
b6d31cdd9d
Add .ai-history/ to .gitignore 2025-07-28 23:55:20 -05:00
Chad Curtis
7473c794cc Merge branch 'zapSupport' into 'main'
Implement Zap + NWC Wallet support

See merge request soapbox-pub/mkstack!11
2025-07-26 00:12:25 +00:00
Chad Curtis
0c039bfbcf revert gitlab-ci to upstream version for now 2025-07-26 00:09:58 +00:00
Chad Curtis
36237fd5b6 attempt to isolate gitlab logic to mainline mkstack 2025-07-25 23:51:13 +00:00
Chad Curtis
1de668785f CONTEXT update per review feedback 2025-07-25 23:47:46 +00:00
Chad Curtis
c231ee40ac Merge branch 'main' of gitlab.com:soapbox-pub/mkstack into zapSupport 2025-07-25 23:45:15 +00:00
Alex Gleason
fbbf7f8c39
npm audit fix 2025-07-23 16:30:18 -05:00
12 changed files with 1021 additions and 232 deletions

View File

@ -11,26 +11,7 @@ stages:
test: test:
stage: test stage: test
script: script:
- | - npm run test
# Run npm test and capture output and exit code
set +e
test_output=$(npm run test 2>&1)
test_exit_code=$?
set -e
echo "$test_output"
# Check if the output contains exactly 6 linting problems
if echo "$test_output" | grep -q "✖ 6 problems (6 errors, 0 warnings)"; then
echo "✅ Expected 6 linting errors found - template is correctly configured"
exit 0
elif [ $test_exit_code -eq 0 ]; then
echo "✅ Tests passed with no linting errors"
exit 0
else
echo "❌ Unexpected test failure or wrong number of linting errors"
exit 1
fi
pages: pages:
stage: deploy stage: deploy

View File

@ -1,9 +1,14 @@
{ {
"mcpServers": { "mcpServers": {
"js-dev": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@soapbox.pub/js-dev-mcp@latest"]
},
"nostr": { "nostr": {
"type": "stdio", "type": "stdio",
"command": "npx", "command": "npx",
"args": ["-y", "xjsr", "@nostrbook/mcp"] "args": ["-y", "@nostrbook/mcp@latest"]
} }
} }
} }

7
.vscode/mcp.json vendored
View File

@ -1,9 +1,14 @@
{ {
"servers": { "servers": {
"js-dev": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@soapbox.pub/js-dev-mcp@latest"]
},
"nostr": { "nostr": {
"type": "stdio", "type": "stdio",
"command": "npx", "command": "npx",
"args": ["-y", "xjsr", "@nostrbook/mcp"] "args": ["-y", "@nostrbook/mcp@latest"]
} }
} }
} }

View File

@ -86,7 +86,6 @@ The project uses shadcn/ui components located in `@/components/ui`. These are un
- **Sidebar**: Navigation sidebar component - **Sidebar**: Navigation sidebar component
- **Skeleton**: Loading placeholder - **Skeleton**: Loading placeholder
- **Slider**: Input for selecting a value from a range - **Slider**: Input for selecting a value from a range
- **Sonner**: Toast notification manager
- **Switch**: Toggle switch control - **Switch**: Toggle switch control
- **Table**: Data table with headers and rows - **Table**: Data table with headers and rows
- **Tabs**: Tabbed interface component - **Tabs**: Tabbed interface component
@ -720,60 +719,6 @@ export function Post(/* ...props */) {
} }
``` ```
### Lightning Zaps (NIP-57)
Implement zaps with a payment method fallback chain: **NWC → WebLN → Manual**. Always validate recipient lightning addresses (`lud16`/`lud06`) before creating zap requests.
**⚠️ CRITICAL**: The `NWCProvider` must be included in the app's provider hierarchy for zap functionality to work. It should be placed inside `NostrProvider` but outside other UI providers:
```tsx
// In App.tsx and TestApp.tsx
import { NWCProvider } from '@/contexts/NWCContext';
<NostrProvider>
<NWCProvider>
{/* other providers and app content */}
</NWCProvider>
</NostrProvider>
```
#### useZaps Hook API
**The `useZaps` hook accepts flexible input types - DO NOT create duplicate hooks:**
```tsx
// Single event
const { zap, totalSats, isLoading } = useZaps(event, webln, activeNWC, onSuccess);
// Multiple events (for bulk fetching zap data)
const { zapData, isLoading } = useZaps(eventArray, webln, activeNWC);
// Disable fetching
const { zap } = useZaps([], webln, activeNWC, onSuccess);
```
```tsx
// Use unified wallet detection and zap components
const { webln, activeNWC, preferredMethod } = useWallet();
// Pre-built components available
import { ZapButton } from '@/components/ZapButton';
import { ZapDialog } from '@/components/ZapDialog';
import { WalletModal } from '@/components/WalletModal';
// Validate recipient can receive zaps
if (!author.metadata?.lud16 && !author.metadata?.lud06) {
return null; // Hide zap button
}
```
**Critical patterns:**
- **Include NWCProvider** in the provider tree before using any zap functionality
- **Use existing `useZaps` hook** - it handles both single events and arrays
- **Avoid duplicate zap displays** - ZapButton already includes count display, don't add separate badges
- Detect WebLN only when needed (dialog open)
- Show payment method indicator to users
- Handle errors gracefully with specific messaging
### Adding Comments Sections ### Adding Comments Sections
The project includes a complete commenting system using NIP-22 (kind 1111) comments that can be added to any Nostr event or URL. The `CommentsSection` component provides a full-featured commenting interface with threaded replies, user authentication, and real-time updates. The project includes a complete commenting system using NIP-22 (kind 1111) comments that can be added to any Nostr event or URL. The `CommentsSection` component provides a full-featured commenting interface with threaded replies, user authentication, and real-time updates.

155
README.md
View File

@ -1,3 +1,154 @@
# MKStack # MKStack with Welshman
Template for building Nostr client application with React 18.x, TailwindCSS 3.x, Vite, shadcn/ui, and Nostrify. Template for building Nostr client applications with React 18.x, TailwindCSS 3.x, Vite, shadcn/ui, and Nostrify - enhanced with Welshman for intelligent relay management.
## Overview
This is an enhanced version of the MKStack template that replaces the basic `NPool` implementation with `NWelshman` - a more sophisticated relay pool manager that provides intelligent routing, quality scoring, and specialized relay selection.
## Key Differences from Base MKStack
### 1. Enhanced Relay Management
**Base MKStack (NPool):**
- Simple relay connection using `NRelay1`
- All queries go to a single configured relay
- Basic round-robin publishing to preset relays
- No distinction between relay types or capabilities
**This Template (NWelshman):**
- Intelligent relay routing with quality scoring
- Specialized relay selection based on query type:
- **Indexer relays** for metadata (kinds 0, 3, 10002)
- **Search relays** for NIP-50 full-text search
- **Static relays** for general queries
- Quality-based relay prioritization
- Configurable redundancy and connection limits
### 2. Router Configuration
The `NostrProvider` now implements a sophisticated Router class that provides:
```typescript
// Relay selection based on capability
getIndexerRelays() // Returns relays optimized for metadata
getSearchRelays() // Returns NIP-50 compatible search relays
getStaticRelays() // Returns general-purpose relays
// Quality assessment
getRelayQuality(url) // Returns 0-1 quality score per relay
// Connection management
getRedundancy() // Number of relays to query (default: 2)
getLimit() // Maximum relay connections (default: 5)
```
### 3. Performance Benefits
- **Reduced latency**: Queries are sent to relays best suited for the task
- **Better reliability**: Redundancy ensures queries succeed even if one relay fails
- **Optimized bandwidth**: Intelligent routing reduces unnecessary connections
- **Faster metadata lookups**: Dedicated indexer relays for profile data
- **Enhanced search**: NIP-50 aware relay selection for text search
## Dependencies
This template adds the following dependencies to base MKStack:
```json
{
"@nostrify/welshman": "^0.35.0",
"@welshman/util": "^0.4.2"
}
```
## Usage
The template maintains full compatibility with the base MKStack API. All existing hooks and components work without modification:
```tsx
// Works exactly the same as base MKStack
const { nostr } = useNostr();
const events = await nostr.query([{ kinds: [1], limit: 20 }]);
```
## Relay Configuration
The Router automatically categorizes relays based on their known capabilities:
### Known Indexer Relays
- `wss://relay.nostr.band`
- `wss://relay.damus.io`
- `wss://purplepag.es`
### Known Search Relays (NIP-50)
- `wss://relay.nostr.band`
- `wss://nostr.wine`
- `wss://search.nos.today`
### Quality Scoring
Relays are scored from 0.0 to 1.0 based on:
- User's configured relay: 1.0 (highest priority)
- Known high-quality relays: 0.8-0.9
- Preset relays: 0.7
- Unknown relays: 0.5 (lowest priority)
## Testing
The template includes comprehensive test coverage:
- **Unit tests** (`NostrProvider.test.tsx`): Mock-based testing of router configuration
- **Integration tests** (`NostrProvider.integration.test.tsx`): Real relay connection tests
Run tests with:
```bash
npm test
```
## Migration from Base MKStack
To migrate an existing MKStack project to use Welshman:
1. Install the new dependencies:
```bash
npm install @nostrify/welshman @welshman/util
```
2. Replace the `NostrProvider` component with the Welshman version
3. No other code changes required - the API remains the same
## When to Use This Template
Choose this Welshman-enhanced template when you need:
- **High-performance Nostr apps** that benefit from intelligent relay routing
- **Apps with heavy metadata queries** (profile lookups, contact lists)
- **Search functionality** using NIP-50
- **Better reliability** through redundancy
- **Production-ready** relay management
Use the base MKStack template for:
- Simple prototypes or learning projects
- Apps that only need basic relay connectivity
- Minimal dependency requirements
## Development
```bash
# Install dependencies
npm install
# Start development server
npm run dev
# Build for production
npm run build
# Run tests
npm test
```
## License
Same as MKStack base template.

403
package-lock.json generated
View File

@ -13,6 +13,7 @@
"@hookform/resolvers": "^3.9.0", "@hookform/resolvers": "^3.9.0",
"@nostrify/nostrify": "npm:@jsr/nostrify__nostrify@^0.46.4", "@nostrify/nostrify": "npm:@jsr/nostrify__nostrify@^0.46.4",
"@nostrify/react": "npm:@jsr/nostrify__react@^0.2.8", "@nostrify/react": "npm:@jsr/nostrify__react@^0.2.8",
"@nostrify/welshman": "npm:@jsr/nostrify__welshman@^0.35.0",
"@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-aspect-ratio": "^1.1.0", "@radix-ui/react-aspect-ratio": "^1.1.0",
@ -43,6 +44,7 @@
"@tanstack/react-query": "^5.56.2", "@tanstack/react-query": "^5.56.2",
"@unhead/addons": "^2.0.10", "@unhead/addons": "^2.0.10",
"@unhead/react": "^2.0.10", "@unhead/react": "^2.0.10",
"@welshman/util": "^0.4.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.0.0", "cmdk": "^1.0.0",
@ -50,7 +52,6 @@
"embla-carousel-react": "^8.3.0", "embla-carousel-react": "^8.3.0",
"input-otp": "^1.2.4", "input-otp": "^1.2.4",
"lucide-react": "^0.462.0", "lucide-react": "^0.462.0",
"next-themes": "^0.3.0",
"nostr-tools": "^2.13.0", "nostr-tools": "^2.13.0",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"react": "^18.3.1", "react": "^18.3.1",
@ -60,7 +61,6 @@
"react-resizable-panels": "^2.1.3", "react-resizable-panels": "^2.1.3",
"react-router-dom": "^6.26.2", "react-router-dom": "^6.26.2",
"recharts": "^2.12.7", "recharts": "^2.12.7",
"sonner": "^1.5.0",
"tailwind-merge": "^2.5.2", "tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.3", "vaul": "^0.9.3",
@ -849,9 +849,9 @@
} }
}, },
"node_modules/@eslint/config-array": { "node_modules/@eslint/config-array": {
"version": "0.20.0", "version": "0.21.0",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
"integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@ -864,9 +864,9 @@
} }
}, },
"node_modules/@eslint/config-helpers": { "node_modules/@eslint/config-helpers": {
"version": "0.2.2", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz",
"integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
@ -874,9 +874,9 @@
} }
}, },
"node_modules/@eslint/core": { "node_modules/@eslint/core": {
"version": "0.13.0", "version": "0.15.1",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz",
"integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@ -924,13 +924,16 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "9.25.1", "version": "9.31.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.1.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz",
"integrity": "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==", "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://eslint.org/donate"
} }
}, },
"node_modules/@eslint/object-schema": { "node_modules/@eslint/object-schema": {
@ -944,13 +947,13 @@
} }
}, },
"node_modules/@eslint/plugin-kit": { "node_modules/@eslint/plugin-kit": {
"version": "0.2.8", "version": "0.3.4",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz",
"integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@eslint/core": "^0.13.0", "@eslint/core": "^0.15.1",
"levn": "^0.4.1" "levn": "^0.4.1"
}, },
"engines": { "engines": {
@ -1062,33 +1065,6 @@
"eslint": "^8.0.0 || ^9.0.0" "eslint": "^8.0.0 || ^9.0.0"
} }
}, },
"node_modules/@html-eslint/eslint-plugin/node_modules/@eslint/core": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz",
"integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@html-eslint/eslint-plugin/node_modules/@eslint/plugin-kit": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz",
"integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^0.14.0",
"levn": "^0.4.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@html-eslint/parser": { "node_modules/@html-eslint/parser": {
"version": "0.41.0", "version": "0.41.0",
"resolved": "https://registry.npmjs.org/@html-eslint/parser/-/parser-0.41.0.tgz", "resolved": "https://registry.npmjs.org/@html-eslint/parser/-/parser-0.41.0.tgz",
@ -1272,11 +1248,42 @@
"resolved": "https://npm.jsr.io/~/11/@jsr/scure__base/1.2.5.tgz", "resolved": "https://npm.jsr.io/~/11/@jsr/scure__base/1.2.5.tgz",
"integrity": "sha512-ino21k5s2kamz+uhAL/N3iI5vJ7x3zTtrzhM52iXBth41KxkFSIDiOlQNbMj4/jTih27jmS6Mi4Xke5vKTdNIA==" "integrity": "sha512-ino21k5s2kamz+uhAL/N3iI5vJ7x3zTtrzhM52iXBth41KxkFSIDiOlQNbMj4/jTih27jmS6Mi4Xke5vKTdNIA=="
}, },
"node_modules/@jsr/std__assert": {
"version": "0.224.0",
"resolved": "https://npm.jsr.io/~/11/@jsr/std__assert/0.224.0.tgz",
"integrity": "sha512-RB0p0ydybgKSfTba6kHWytfpEJ0CBPi+byxZikLYa51L9uLINW52/j6n4KuiLFoh2cdFfpNZSNMY/dzQPW90DQ==",
"dependencies": {
"@jsr/std__fmt": "^0.224.0",
"@jsr/std__internal": "^0.224.0"
}
},
"node_modules/@jsr/std__crypto": {
"version": "0.224.0",
"resolved": "https://npm.jsr.io/~/11/@jsr/std__crypto/0.224.0.tgz",
"integrity": "sha512-qzZWI8VnH215FS7hmQsAeNafjLMkmSl1OOvexorVUEf1Zl9omHSN87MwIjtmyVXGLtpGRLzIhKXbeup1xO69Zw==",
"dependencies": {
"@jsr/std__assert": "^0.224.0",
"@jsr/std__encoding": "^0.224.0"
}
},
"node_modules/@jsr/std__encoding": { "node_modules/@jsr/std__encoding": {
"version": "0.224.3", "version": "0.224.3",
"resolved": "https://npm.jsr.io/~/11/@jsr/std__encoding/0.224.3.tgz", "resolved": "https://npm.jsr.io/~/11/@jsr/std__encoding/0.224.3.tgz",
"integrity": "sha512-zAuX2QV1zwJ5RSmrnDGVerAtN3pBXpYYNlGzhERW9AiQ1UJd2/xruyB3i5NdTWy2OK2pjETswOj+0+prYTPlxQ==" "integrity": "sha512-zAuX2QV1zwJ5RSmrnDGVerAtN3pBXpYYNlGzhERW9AiQ1UJd2/xruyB3i5NdTWy2OK2pjETswOj+0+prYTPlxQ=="
}, },
"node_modules/@jsr/std__fmt": {
"version": "0.224.0",
"resolved": "https://npm.jsr.io/~/11/@jsr/std__fmt/0.224.0.tgz",
"integrity": "sha512-lyrH5LesMB897QW0NIbZlGp72Ucopj2hMZW2wqB0NyZhuXfLH2sPBIUpCSf87kRKTGnx90JV905w4iTp0TD+Sg=="
},
"node_modules/@jsr/std__internal": {
"version": "0.224.0",
"resolved": "https://npm.jsr.io/~/11/@jsr/std__internal/0.224.0.tgz",
"integrity": "sha512-inYzKOGAFK2tyy1D4NfwlbPiqEcSaXfOms3Tm4Y+1LmKSYOeB9wjqWHF4y/BJuYj8XUv61F7eaHaIw6NIlhBWg==",
"dependencies": {
"@jsr/std__fmt": "^0.224.0"
}
},
"node_modules/@noble/ciphers": { "node_modules/@noble/ciphers": {
"version": "0.5.3", "version": "0.5.3",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz", "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz",
@ -1383,6 +1390,113 @@
"react": "^18.0.0" "react": "^18.0.0"
} }
}, },
"node_modules/@nostrify/welshman": {
"name": "@jsr/nostrify__welshman",
"version": "0.35.0",
"resolved": "https://npm.jsr.io/~/11/@jsr/nostrify__welshman/0.35.0.tgz",
"integrity": "sha512-4pnfEB6MACMFJ8spxw7TE2qqCWNldG/RxWVpf5HYS4bTE+dI4bJYUPI3+38VL6t0JMEW+0QlbM6VFa5uSdHJdw==",
"dependencies": {
"@jsr/nostrify__nostrify": "^0.35.0",
"@welshman/lib": "^0.0.2",
"@welshman/net": "^0.0.2",
"@welshman/util": "^0.0.2"
}
},
"node_modules/@nostrify/welshman/node_modules/@jsr/nostrify__nostrify": {
"version": "0.35.0",
"resolved": "https://npm.jsr.io/~/11/@jsr/nostrify__nostrify/0.35.0.tgz",
"integrity": "sha512-jCUA0SgU1e7ltkHVo6CoUfPuCIgBwttErJcjR4cCPba6g7YjlCS2pJ75HIEG1vWr+zyMIpO4Qa3csRhJ/gAOow==",
"dependencies": {
"@jsr/nostrify__types": "^0.35.0",
"@jsr/std__crypto": "^0.224.0",
"@jsr/std__encoding": "^0.224.1",
"@scure/base": "^1.1.6",
"@scure/bip32": "^1.4.0",
"@scure/bip39": "^1.3.0",
"lru-cache": "^10.2.0",
"nostr-tools": "^2.7.0",
"websocket-ts": "^2.1.5",
"zod": "^3.23.8"
}
},
"node_modules/@nostrify/welshman/node_modules/@jsr/nostrify__types": {
"version": "0.35.0",
"resolved": "https://npm.jsr.io/~/11/@jsr/nostrify__types/0.35.0.tgz",
"integrity": "sha512-dukOLFxyF7JwDvORLvb3PwMFs1HOBIkTyiewkujiGPaQ7FjvvGMiqY/QxbG0qZX5AmTtS65c/lLlg/ni1flrGQ=="
},
"node_modules/@nostrify/welshman/node_modules/@noble/curves": {
"version": "1.9.7",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz",
"integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.8.0"
},
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nostrify/welshman/node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nostrify/welshman/node_modules/@scure/base": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz",
"integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==",
"license": "MIT",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nostrify/welshman/node_modules/@scure/bip32": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz",
"integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==",
"license": "MIT",
"dependencies": {
"@noble/curves": "~1.9.0",
"@noble/hashes": "~1.8.0",
"@scure/base": "~1.2.5"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nostrify/welshman/node_modules/@scure/bip39": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz",
"integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "~1.8.0",
"@scure/base": "~1.2.5"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nostrify/welshman/node_modules/@welshman/util": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@welshman/util/-/util-0.0.2.tgz",
"integrity": "sha512-z/v7zf+Z0xl0AKDE5X/FBvRvVIR3Afm/j00yVj4g8JbFPbpeb8f472ArM37YGRKOjnsg9sHXXsQnzjKrE0PLyw==",
"license": "MIT",
"dependencies": {
"@welshman/lib": "0.0.2",
"nostr-tools": "^2.3.2"
}
},
"node_modules/@pkgjs/parseargs": { "node_modules/@pkgjs/parseargs": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@ -3594,6 +3708,12 @@
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/events": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.3.tgz",
"integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==",
"license": "MIT"
},
"node_modules/@types/filesystem": { "node_modules/@types/filesystem": {
"version": "0.0.36", "version": "0.0.36",
"resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz", "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz",
@ -3620,7 +3740,6 @@
"version": "22.15.3", "version": "22.15.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz",
"integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==", "integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
@ -3664,6 +3783,15 @@
"@types/react": "^18.0.0" "@types/react": "^18.0.0"
} }
}, },
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.31.1", "version": "8.31.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.1.tgz",
@ -4029,10 +4157,88 @@
"url": "https://opencollective.com/vitest" "url": "https://opencollective.com/vitest"
} }
}, },
"node_modules/@welshman/lib": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@welshman/lib/-/lib-0.0.2.tgz",
"integrity": "sha512-ZQPHF87MIDH+ngL8PQ6eMXzs1nYOFQSq7Aw1P+FUZNzrvu6PR/hXr+XQRBWK1pISbyOS/jtbHqsNTPE6SjAQOg==",
"license": "MIT",
"dependencies": {
"@scure/base": "^1.1.6",
"events": "^3.3.0",
"throttle-debounce": "^5.0.0"
}
},
"node_modules/@welshman/lib/node_modules/@scure/base": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz",
"integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==",
"license": "MIT",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@welshman/net": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@welshman/net/-/net-0.0.2.tgz",
"integrity": "sha512-vJspb28zvrQxI9uYCyzNF20JmMICtaqFLMGJSy7IJWn+vLFMp/6rbEOcFNbsvRLWjeR0Xjt48jxGx9q9TdE44w==",
"license": "MIT",
"dependencies": {
"@welshman/lib": "0.0.2",
"@welshman/util": "0.0.2",
"isomorphic-ws": "^5.0.0",
"ws": "^8.16.0"
}
},
"node_modules/@welshman/net/node_modules/@welshman/util": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@welshman/util/-/util-0.0.2.tgz",
"integrity": "sha512-z/v7zf+Z0xl0AKDE5X/FBvRvVIR3Afm/j00yVj4g8JbFPbpeb8f472ArM37YGRKOjnsg9sHXXsQnzjKrE0PLyw==",
"license": "MIT",
"dependencies": {
"@welshman/lib": "0.0.2",
"nostr-tools": "^2.3.2"
}
},
"node_modules/@welshman/util": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@welshman/util/-/util-0.4.2.tgz",
"integrity": "sha512-uhMViulhQFxfIZb1MQ4lx9rxJMPKvJYaMxv4tPL4WlYlNgnAAJdcwLJk52xsm7/FeOVuCKtrvyuBBLDLaKCg8w==",
"license": "MIT",
"dependencies": {
"@types/ws": "^8.5.13",
"@welshman/lib": "0.4.2",
"js-base64": "^3.7.7",
"nostr-tools": "^2.14.2",
"nostr-wasm": "^0.1.0"
}
},
"node_modules/@welshman/util/node_modules/@scure/base": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz",
"integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==",
"license": "MIT",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@welshman/util/node_modules/@welshman/lib": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@welshman/lib/-/lib-0.4.2.tgz",
"integrity": "sha512-1QAEwrmp5IN3k1olO/LKKwS/6+5kgvX5JYW1WpkKCmHB4r3vroUP2DiHf4ah946bixqUX3XLiJfLZmD9TB98zg==",
"license": "MIT",
"dependencies": {
"@scure/base": "^1.1.6",
"@types/events": "^3.0.3",
"events": "^3.3.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.14.1", "version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
@ -5058,20 +5264,20 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "9.25.1", "version": "9.31.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.25.1.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz",
"integrity": "sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==", "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.20.0", "@eslint/config-array": "^0.21.0",
"@eslint/config-helpers": "^0.2.1", "@eslint/config-helpers": "^0.3.0",
"@eslint/core": "^0.13.0", "@eslint/core": "^0.15.0",
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.25.1", "@eslint/js": "9.31.0",
"@eslint/plugin-kit": "^0.2.8", "@eslint/plugin-kit": "^0.3.1",
"@humanfs/node": "^0.16.6", "@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2", "@humanwhocodes/retry": "^0.4.2",
@ -5082,9 +5288,9 @@
"cross-spawn": "^7.0.6", "cross-spawn": "^7.0.6",
"debug": "^4.3.2", "debug": "^4.3.2",
"escape-string-regexp": "^4.0.0", "escape-string-regexp": "^4.0.0",
"eslint-scope": "^8.3.0", "eslint-scope": "^8.4.0",
"eslint-visitor-keys": "^4.2.0", "eslint-visitor-keys": "^4.2.1",
"espree": "^10.3.0", "espree": "^10.4.0",
"esquery": "^1.5.0", "esquery": "^1.5.0",
"esutils": "^2.0.2", "esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
@ -5142,9 +5348,9 @@
} }
}, },
"node_modules/eslint-scope": { "node_modules/eslint-scope": {
"version": "8.3.0", "version": "8.4.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
"integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
@ -5159,9 +5365,9 @@
} }
}, },
"node_modules/eslint-visitor-keys": { "node_modules/eslint-visitor-keys": {
"version": "4.2.0", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
@ -5172,15 +5378,15 @@
} }
}, },
"node_modules/espree": { "node_modules/espree": {
"version": "10.3.0", "version": "10.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
"integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"acorn": "^8.14.0", "acorn": "^8.15.0",
"acorn-jsx": "^5.3.2", "acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^4.2.0" "eslint-visitor-keys": "^4.2.1"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -5250,6 +5456,15 @@
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"license": "MIT",
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/expect-type": { "node_modules/expect-type": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz",
@ -5764,6 +5979,15 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/isomorphic-ws": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz",
"integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==",
"license": "MIT",
"peerDependencies": {
"ws": "*"
}
},
"node_modules/jackspeak": { "node_modules/jackspeak": {
"version": "3.4.3", "version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
@ -5788,6 +6012,12 @@
"jiti": "bin/jiti.js" "jiti": "bin/jiti.js"
} }
}, },
"node_modules/js-base64": {
"version": "3.7.8",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz",
"integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==",
"license": "BSD-3-Clause"
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -6151,16 +6381,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/next-themes": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz",
"integrity": "sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8 || ^17 || ^18",
"react-dom": "^16.8 || ^17 || ^18"
}
},
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.19", "version": "2.0.19",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
@ -7207,16 +7427,6 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/sonner": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz",
"integrity": "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==",
"license": "MIT",
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@ -7506,6 +7716,15 @@
"node": ">=0.8" "node": ">=0.8"
} }
}, },
"node_modules/throttle-debounce": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
"integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==",
"license": "MIT",
"engines": {
"node": ">=12.22"
}
},
"node_modules/tiny-invariant": { "node_modules/tiny-invariant": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@ -7797,7 +8016,6 @@
"version": "6.21.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/unhead": { "node_modules/unhead": {
@ -8416,7 +8634,6 @@
"version": "8.18.2", "version": "8.18.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"

View File

@ -15,6 +15,7 @@
"@hookform/resolvers": "^3.9.0", "@hookform/resolvers": "^3.9.0",
"@nostrify/nostrify": "npm:@jsr/nostrify__nostrify@^0.46.4", "@nostrify/nostrify": "npm:@jsr/nostrify__nostrify@^0.46.4",
"@nostrify/react": "npm:@jsr/nostrify__react@^0.2.8", "@nostrify/react": "npm:@jsr/nostrify__react@^0.2.8",
"@nostrify/welshman": "npm:@jsr/nostrify__welshman@^0.35.0",
"@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-aspect-ratio": "^1.1.0", "@radix-ui/react-aspect-ratio": "^1.1.0",
@ -45,6 +46,7 @@
"@tanstack/react-query": "^5.56.2", "@tanstack/react-query": "^5.56.2",
"@unhead/addons": "^2.0.10", "@unhead/addons": "^2.0.10",
"@unhead/react": "^2.0.10", "@unhead/react": "^2.0.10",
"@welshman/util": "^0.4.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.0.0", "cmdk": "^1.0.0",
@ -52,7 +54,6 @@
"embla-carousel-react": "^8.3.0", "embla-carousel-react": "^8.3.0",
"input-otp": "^1.2.4", "input-otp": "^1.2.4",
"lucide-react": "^0.462.0", "lucide-react": "^0.462.0",
"next-themes": "^0.3.0",
"nostr-tools": "^2.13.0", "nostr-tools": "^2.13.0",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"react": "^18.3.1", "react": "^18.3.1",
@ -62,7 +63,6 @@
"react-resizable-panels": "^2.1.3", "react-resizable-panels": "^2.1.3",
"react-router-dom": "^6.26.2", "react-router-dom": "^6.26.2",
"recharts": "^2.12.7", "recharts": "^2.12.7",
"sonner": "^1.5.0",
"tailwind-merge": "^2.5.2", "tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.3", "vaul": "^0.9.3",

View File

@ -7,7 +7,6 @@ import { InferSeoMetaPlugin } from '@unhead/addons';
import { Suspense } from 'react'; import { Suspense } from 'react';
import NostrProvider from '@/components/NostrProvider'; import NostrProvider from '@/components/NostrProvider';
import { Toaster } from "@/components/ui/toaster"; import { Toaster } from "@/components/ui/toaster";
import { Toaster as Sonner } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
import { NostrLoginProvider } from '@nostrify/react/login'; import { NostrLoginProvider } from '@nostrify/react/login';
import { AppProvider } from '@/components/AppProvider'; import { AppProvider } from '@/components/AppProvider';
@ -53,7 +52,6 @@ export function App() {
<NWCProvider> <NWCProvider>
<TooltipProvider> <TooltipProvider>
<Toaster /> <Toaster />
<Sonner />
<Suspense> <Suspense>
<AppRouter /> <AppRouter />
</Suspense> </Suspense>

View File

@ -0,0 +1,281 @@
import { describe, it, expect, beforeAll } from 'vitest';
import { NWelshman } from '@nostrify/welshman';
import type { NostrEvent } from '@nostrify/nostrify';
// Use Nostrify's event creation instead of nostr-tools
function createTestEvent(): NostrEvent {
return {
id: 'test-' + Date.now(),
pubkey: '0'.repeat(64),
created_at: Math.floor(Date.now() / 1000),
kind: 1,
tags: [['client', 'nostrify-welshman-test']],
content: `Integration test from Nostrify NWelshman - ${Date.now()}`,
sig: '0'.repeat(128),
};
}
// Router configuration to match what we use in NostrProvider
interface RouterOptions {
getUserPubkey: () => string | null;
getGroupRelays: (address: string) => string[];
getCommunityRelays: (address: string) => string[];
getPubkeyRelays: (pubkey: string, mode?: unknown) => string[];
getStaticRelays: () => string[];
getIndexerRelays: () => string[];
getSearchRelays: () => string[];
getRelayQuality: (url: string) => number;
getRedundancy: () => number;
getLimit: () => number;
}
class Router {
constructor(public options: RouterOptions) {}
// Add the methods that NWelshman expects based on the source code
User() {
return {
policy: (fn: any) => ({
getUrls: () => this.options.getStaticRelays()
}),
getUrls: () => this.options.getStaticRelays()
};
}
FromPubkeys(pubkeys: string[]) {
return {
policy: (fn: any) => ({
getSelections: () => this.options.getStaticRelays().map(relay => ({
relay,
values: pubkeys
}))
})
};
}
WithinMultipleContexts(contexts: string[]) {
return {
policy: (fn: any) => ({
getSelections: () => this.options.getStaticRelays().map(relay => ({
relay,
values: contexts
}))
})
};
}
PublishEvent(event: any) {
return {
getUrls: () => this.options.getStaticRelays()
};
}
product(filters: string[], relays: string[]) {
return {
getSelections: () => relays.map(relay => ({
relay,
values: filters
}))
};
}
merge(scenarios: any[]) {
return {
getSelections: () => this.options.getStaticRelays().map(relay => ({
relay,
values: ['test']
}))
};
}
addMinimalFallbacks = (count: number, limit: number) => {
return Math.min(count + 1, limit);
};
}
const testRelays = [
'wss://relay.nostr.band',
'wss://relay.damus.io',
];
describe('NWelshman Integration Tests', () => {
let pool: NWelshman;
let testRouter: Router;
beforeAll(() => {
testRouter = new Router({
getUserPubkey: () => null,
getGroupRelays: () => [],
getCommunityRelays: () => [],
getPubkeyRelays: () => testRelays,
getStaticRelays: () => testRelays,
getIndexerRelays: () => testRelays,
getSearchRelays: () => testRelays,
getRelayQuality: (url: string) => {
if (testRelays.includes(url)) return 1.0;
return 0.5;
},
getRedundancy: () => 2,
getLimit: () => 5,
});
pool = new NWelshman(testRouter as unknown as import('@welshman/util').Router);
});
it('should query events from real relays', async () => {
// Query for some recent kind 1 events (text notes)
const events = await pool.query([
{
kinds: [1],
limit: 3,
}
], {
signal: AbortSignal.timeout(10000), // 10 second timeout
});
expect(events).toBeDefined();
expect(Array.isArray(events)).toBe(true);
// We might get 0 events if relays are slow, but the structure should be correct
if (events.length > 0) {
const event = events[0];
expect(event).toHaveProperty('id');
expect(event).toHaveProperty('pubkey');
expect(event).toHaveProperty('created_at');
expect(event).toHaveProperty('kind');
expect(event).toHaveProperty('tags');
expect(event).toHaveProperty('content');
expect(event).toHaveProperty('sig');
expect(event.kind).toBe(1);
}
}, 15000); // 15 second timeout for this test
it('should handle req stream from real relays', async () => {
const events: NostrEvent[] = [];
let receivedEOSE = false;
const stream = pool.req([
{
kinds: [1],
limit: 2,
}
], {
signal: AbortSignal.timeout(10000),
});
for await (const msg of stream) {
if (msg[0] === 'EVENT') {
events.push(msg[2]);
} else if (msg[0] === 'EOSE') {
receivedEOSE = true;
break;
} else if (msg[0] === 'CLOSED') {
break;
}
// Break early if we get enough events
if (events.length >= 2) {
break;
}
}
expect(receivedEOSE).toBe(true);
if (events.length > 0) {
const event = events[0];
expect(event).toHaveProperty('id');
expect(event).toHaveProperty('kind');
expect(event.kind).toBe(1);
}
}, 15000);
it('should publish event to real relays', async () => {
// Generate a test event (note: this won't actually publish since sig is fake)
const testEvent = createTestEvent();
// This should not throw an error (though relay may reject due to invalid sig)
await expect(pool.event(testEvent)).resolves.not.toThrow();
}, 15000);
it('should handle metadata queries to indexer relays', async () => {
// Query for some profile metadata (kind 0)
const events = await pool.query([
{
kinds: [0],
limit: 2,
}
], {
signal: AbortSignal.timeout(10000),
});
expect(events).toBeDefined();
expect(Array.isArray(events)).toBe(true);
if (events.length > 0) {
const event = events[0];
expect(event.kind).toBe(0);
expect(typeof event.content).toBe('string');
// Try to parse the content as JSON (kind 0 should be JSON)
try {
const profile = JSON.parse(event.content);
expect(typeof profile).toBe('object');
} catch {
// Some kind 0 events might have invalid JSON, that's ok
}
}
}, 15000);
it('should handle connection errors gracefully', async () => {
// Create a router with an invalid relay URL
const badRouter = new Router({
getUserPubkey: () => null,
getGroupRelays: () => [],
getCommunityRelays: () => [],
getPubkeyRelays: () => [],
getStaticRelays: () => ['wss://invalid.relay.that.does.not.exist'],
getIndexerRelays: () => ['wss://invalid.relay.that.does.not.exist'],
getSearchRelays: () => ['wss://invalid.relay.that.does.not.exist'],
getRelayQuality: () => 0.1,
getRedundancy: () => 1,
getLimit: () => 1,
});
const badPool = new NWelshman(badRouter as unknown as import('@welshman/util').Router);
// Query should not crash but may return empty results
const events = await badPool.query([
{
kinds: [1],
limit: 1,
}
], {
signal: AbortSignal.timeout(5000),
});
expect(events).toBeDefined();
expect(Array.isArray(events)).toBe(true);
// We expect empty results due to connection failure
expect(events.length).toBe(0);
}, 10000);
it('should respect query timeouts', async () => {
const startTime = Date.now();
try {
await pool.query([
{
kinds: [1],
limit: 100,
}
], {
signal: AbortSignal.timeout(1000), // Very short timeout
});
} catch (error) {
// Timeout is expected, check that it happened reasonably quickly
const elapsed = Date.now() - startTime;
expect(elapsed).toBeLessThan(3000); // Should timeout within 3 seconds
expect(error).toBeDefined();
}
});
});

View File

@ -0,0 +1,132 @@
import { describe, it, expect, vi } from 'vitest';
import { render } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import NostrProvider from './NostrProvider';
import { AppProvider } from '@/components/AppProvider';
import { AppConfig } from '@/contexts/AppContext';
// Mock NWelshman since it requires network access
const mockNWelshman = vi.fn().mockImplementation(() => ({
query: vi.fn().mockResolvedValue([]),
event: vi.fn().mockResolvedValue(undefined),
req: vi.fn().mockImplementation(async function* () {
yield ['EOSE', ''];
}),
close: vi.fn().mockResolvedValue(undefined),
}));
vi.mock('@nostrify/welshman', () => ({
NWelshman: mockNWelshman,
}));
const defaultConfig: AppConfig = {
theme: "light",
relayUrl: "wss://relay.primal.net",
};
const presetRelays = [
{ url: 'wss://relay.nostr.band', name: 'Nostr.Band' },
{ url: 'wss://relay.damus.io', name: 'Damus' },
];
describe('NostrProvider with NWelshman', () => {
it('renders children without errors', () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
const TestComponent = () => <div data-testid="test-child">Test content</div>;
const { getByTestId } = render(
<QueryClientProvider client={queryClient}>
<AppProvider storageKey="test:app-config" defaultConfig={defaultConfig} presetRelays={presetRelays}>
<NostrProvider>
<TestComponent />
</NostrProvider>
</AppProvider>
</QueryClientProvider>
);
expect(getByTestId('test-child')).toBeInTheDocument();
});
it('creates NWelshman with router configuration', async () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
const TestComponent = () => <div>Test</div>;
render(
<QueryClientProvider client={queryClient}>
<AppProvider storageKey="test:app-config-2" defaultConfig={defaultConfig} presetRelays={presetRelays}>
<NostrProvider>
<TestComponent />
</NostrProvider>
</AppProvider>
</QueryClientProvider>
);
// Verify NWelshman was instantiated
expect(mockNWelshman).toHaveBeenCalled();
// Verify router was passed with correct configuration
const routerArg = mockNWelshman.mock.calls[0][0];
expect(routerArg).toBeDefined();
expect(typeof routerArg.options.getUserPubkey).toBe('function');
expect(typeof routerArg.options.getStaticRelays).toBe('function');
expect(typeof routerArg.options.getIndexerRelays).toBe('function');
expect(typeof routerArg.options.getSearchRelays).toBe('function');
expect(typeof routerArg.options.getRelayQuality).toBe('function');
// Test router methods return expected values
const staticRelays = routerArg.options.getStaticRelays();
expect(staticRelays).toContain('wss://relay.primal.net');
expect(staticRelays).toContain('wss://relay.nostr.band');
const indexerRelays = routerArg.options.getIndexerRelays();
expect(indexerRelays).toContain('wss://relay.nostr.band');
const searchRelays = routerArg.options.getSearchRelays();
expect(searchRelays).toContain('wss://relay.nostr.band');
expect(routerArg.options.getRedundancy()).toBe(2);
expect(routerArg.options.getLimit()).toBe(5);
});
it('handles relay quality scoring', async () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
const TestComponent = () => <div>Test</div>;
render(
<QueryClientProvider client={queryClient}>
<AppProvider storageKey="test:app-config-3" defaultConfig={defaultConfig} presetRelays={presetRelays}>
<NostrProvider>
<TestComponent />
</NostrProvider>
</AppProvider>
</QueryClientProvider>
);
const routerArg = mockNWelshman.mock.calls[mockNWelshman.mock.calls.length - 1][0];
const getRelayQuality = routerArg.options.getRelayQuality;
// Test quality scoring
expect(getRelayQuality('wss://relay.primal.net')).toBe(1.0); // User's configured relay
expect(getRelayQuality('wss://relay.damus.io')).toBe(0.9); // High quality relay
expect(getRelayQuality('wss://nostr.band')).toBe(0.9); // High quality relay
expect(getRelayQuality('wss://unknown.relay')).toBe(0.5); // Unknown relay
});
});

View File

@ -1,9 +1,28 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useMemo, useRef } from 'react';
import { NostrEvent, NPool, NRelay1 } from '@nostrify/nostrify'; import { NWelshman } from '@nostrify/welshman';
import { NostrContext } from '@nostrify/react'; import { NostrContext } from '@nostrify/react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { useAppContext } from '@/hooks/useAppContext'; import { useAppContext } from '@/hooks/useAppContext';
// Type definitions to match what NWelshman expects
interface RouterOptions {
getUserPubkey: () => string | null;
getGroupRelays: (address: string) => string[];
getCommunityRelays: (address: string) => string[];
getPubkeyRelays: (pubkey: string, mode?: unknown) => string[];
getStaticRelays: () => string[];
getIndexerRelays: () => string[];
getSearchRelays: () => string[];
getRelayQuality: (url: string) => number;
getRedundancy: () => number;
getLimit: () => number;
}
// Mock Router class that satisfies NWelshman requirements
class Router {
constructor(public options: RouterOptions) {}
}
interface NostrProviderProps { interface NostrProviderProps {
children: React.ReactNode; children: React.ReactNode;
} }
@ -14,10 +33,7 @@ const NostrProvider: React.FC<NostrProviderProps> = (props) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
// Create NPool instance only once // Use refs so the router always has the latest data
const pool = useRef<NPool | undefined>(undefined);
// Use refs so the pool always has the latest data
const relayUrl = useRef<string>(config.relayUrl); const relayUrl = useRef<string>(config.relayUrl);
// Update refs when config changes // Update refs when config changes
@ -26,35 +42,122 @@ const NostrProvider: React.FC<NostrProviderProps> = (props) => {
queryClient.resetQueries(); queryClient.resetQueries();
}, [config.relayUrl, queryClient]); }, [config.relayUrl, queryClient]);
// Initialize NPool only once // Create router configuration
if (!pool.current) { const router = useMemo(() => {
pool.current = new NPool({ return new Router({
open(url: string) { // Return the current user's pubkey if logged in
return new NRelay1(url); // This will be set by child components that have access to the login context
getUserPubkey: () => {
// Try to get pubkey from localStorage (where login info is stored)
try {
const loginData = localStorage.getItem('nostr:login');
if (loginData) {
const parsed = JSON.parse(loginData);
return parsed?.pubkey ?? null;
}
} catch {
// Ignore parse errors
}
return null;
}, },
reqRouter(filters) {
return new Map([[relayUrl.current, filters]]); // Group relays for NIP-29 groups (not used in this app yet)
getGroupRelays: (_address: string) => [],
// Community relays for NIP-72 communities (not used in this app yet)
getCommunityRelays: (_address: string) => [],
// Get relays for a specific pubkey - for now, use configured relay
getPubkeyRelays: (_pubkey: string, _mode?: unknown) => {
return [relayUrl.current];
}, },
eventRouter(_event: NostrEvent) {
// Publish to the selected relay // Static fallback relays - use configured relay plus presets
const allRelays = new Set<string>([relayUrl.current]); getStaticRelays: () => {
const relays = [relayUrl.current];
// Also publish to the preset relays, capped to 5 // Add up to 4 preset relays as fallbacks
for (const { url } of (presetRelays ?? [])) { for (const { url } of (presetRelays ?? []).slice(0, 4)) {
allRelays.add(url); if (!relays.includes(url)) {
relays.push(url);
if (allRelays.size >= 5) {
break;
} }
} }
return relays;
return [...allRelays];
}, },
// Indexer relays for metadata lookups (kinds 0, 3, 10002)
getIndexerRelays: (): string[] => {
// Use configured relay if it's an indexer, otherwise use known indexers
const indexers: string[] = [];
const currentRelay = relayUrl.current;
// Known good indexer relays
if (currentRelay.includes('nostr.band') ||
currentRelay.includes('relay.damus.io') ||
currentRelay.includes('purplepag.es')) {
indexers.push(currentRelay);
}
// Add wss://relay.nostr.band as a fallback indexer
if (!indexers.includes('wss://relay.nostr.band')) {
indexers.push('wss://relay.nostr.band');
}
return indexers;
},
// Search relays that support NIP-50
getSearchRelays: (): string[] => {
const searchRelays: string[] = [];
const currentRelay = relayUrl.current;
// Known NIP-50 supporting relays
if (currentRelay.includes('nostr.band') ||
currentRelay.includes('nostr.wine') ||
currentRelay.includes('search.nos.today')) {
searchRelays.push(currentRelay);
}
// Add search fallback
if (!searchRelays.includes('wss://relay.nostr.band')) {
searchRelays.push('wss://relay.nostr.band');
}
return searchRelays;
},
// Relay quality assessment (0-1 scale)
getRelayQuality: (url: string) => {
// Prefer the user's configured relay
if (url === relayUrl.current) return 1.0;
// Known high-quality relays
if (url.includes('relay.damus.io')) return 0.9;
if (url.includes('nostr.band')) return 0.9;
if (url.includes('relay.primal.net')) return 0.85;
if (url.includes('nos.lol')) return 0.8;
// Preset relays are trusted
if (presetRelays?.some(r => r.url === url)) return 0.7;
// Unknown relays get lower quality
return 0.5;
},
// Number of relays to use per selection (redundancy)
getRedundancy: () => 2,
// Maximum number of relays to return
getLimit: () => 5,
}); });
} }, [relayUrl, presetRelays]);
// Create NWelshman pool with the router
const pool = useMemo(() => {
return new NWelshman(router as any);
}, [router]);
return ( return (
<NostrContext.Provider value={{ nostr: pool.current }}> <NostrContext.Provider value={{ nostr: pool as any }}>
{children} {children}
</NostrContext.Provider> </NostrContext.Provider>
); );

View File

@ -1,29 +0,0 @@
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
)
}
export { Toaster }