4 Demos in this repo - This implementation provides a modular, reusable system for NIP-42 authentication and publishing kind 30078 (Parameterized Replaceable Events) in Astro applications using nostr-tools.
sequenceDiagram
participant Client as Client App<br/>(Astro Page)
participant Extension as NIP-07<br/>Extension
participant Relay as Nostr Relay<br/>(ws://localhost:3334)
Note over Client, Relay: Phase 1: Initial Connection & Challenge
Client->>Relay: 1. Connect WebSocket
activate Relay
Client->>Relay: 2. Send REQ (trigger AUTH)
Note right of Client: ["REQ", "auth-trigger", {"limit": 1}]
Relay->>Client: 3. AUTH Challenge
Note left of Relay: ["AUTH", "<random-challenge-string>"]
Note over Client, Relay: Phase 2: Authentication Response
Client->>Extension: 4. Get Public Key
activate Extension
Extension-->>Client: pubkey
deactivate Extension
Client->>Client: 5. Create AUTH Event
Note right of Client: kind: 22242<br/>tags: [["relay", "ws://..."], ["challenge", "..."]]<br/>content: ""
Client->>Extension: 6. Sign AUTH Event
activate Extension
Extension-->>Client: Signed AUTH Event
deactivate Extension
Client->>Relay: 7. Send Signed AUTH
Note right of Client: ["AUTH", signed-auth-event]
Relay->>Client: 8. AUTH Response (OK/FAIL)
Note left of Relay: ["OK", event-id, true/false, message]
alt Authentication Successful
Relay-->>Client: ✅ Connection Authenticated
Note over Client, Relay: Same connection remains open
Note over Client, Relay: Phase 3: Publish Kind 30078 Event
Client->>Client: 9. Create Kind 30078 Event
Note right of Client: kind: 30078<br/>tags: [["d", "unique-tag"], ...]<br/>content: "event content"
Client->>Extension: 10. Sign Kind 30078 Event
activate Extension
Extension-->>Client: Signed Event
deactivate Extension
Client->>Relay: 11. Publish Event
Note right of Client: ["EVENT", signed-30078-event]
Relay->>Client: 12. Publish Response (OK/FAIL)
Note left of Relay: ["OK", event-id, true/false, message]
alt Publish Successful
Relay-->>Client: ✅ Event Published
Note right of Client: Event ID returned
else Publish Failed
Relay-->>Client: ❌ Publish Error
Note right of Client: Error message provided
end
else Authentication Failed
Relay-->>Client: ❌ AUTH Failed
Note right of Client: Connection not authenticated<br/>Cannot publish events
end
Note over Client, Relay: Phase 4: Connection Management
opt Later Operations
Client->>Relay: Additional Events
Note right of Client: Can publish more events<br/>using same authenticated connection
end
opt Disconnect
Client->>Relay: Close Connection
deactivate Relay
Note over Client, Relay: Connection closed<br/>Must re-authenticate for new session
end
// Client connects to relay
const ws = new WebSocket('ws://localhost:3334')
["REQ", "auth-trigger", {"limit": 1}]
- Purpose: Many relays only send AUTH challenges when a restricted operation is attempted
- Effect: Prompts relay to send AUTH challenge if required
["AUTH", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"]
- Format:
["AUTH", "<challenge-string>"]
- Challenge: Random string that must be included in AUTH response
- Security: Prevents replay attacks
const pubkey = await window.nostr.getPublicKey()
- Returns: User's public key (hex format)
- Permission: May trigger extension permission prompt
{
"kind": 22242,
"created_at": 1699123456,
"tags": [
["relay", "ws://localhost:3334"],
["challenge", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"]
],
"content": "",
"pubkey": "user-pubkey-hex"
}
- Kind 22242: Special event type for NIP-42 authentication
- Required Tags:
relay
: The relay URL being authenticated tochallenge
: The exact challenge string received
- Content: Always empty for AUTH events
const signedAuthEvent = await window.nostr.signEvent(authEvent)
- Adds:
id
(event hash) andsig
(signature) fields - Security: Cryptographically proves ownership of private key
["AUTH", {
"kind": 22242,
"created_at": 1699123456,
"tags": [["relay", "ws://localhost:3334"], ["challenge", "..."]],
"content": "",
"pubkey": "...",
"id": "event-hash",
"sig": "signature"
}]
["OK", "event-id", true, ""]
- Format:
["OK", event-id, success, message]
- Success:
true
= authenticated,false
= failed - Message: Error description if failed
{
"kind": 30078,
"created_at": 1699123456,
"tags": [
["d", "unique-identifier"],
["t", "example"],
["client", "astro-app"]
],
"content": "Hello, authenticated Nostr!",
"pubkey": "user-pubkey-hex"
}
- Kind 30078: Parameterized Replaceable Event
- Required Tag:
["d", "identifier"]
- makes event replaceable - Additional Tags: Custom tags for categorization, client info, etc.
const signedEvent = await window.nostr.signEvent(event)
["EVENT", {
"kind": 30078,
"created_at": 1699123456,
"tags": [["d", "unique-id"], ["t", "example"]],
"content": "Hello, authenticated Nostr!",
"pubkey": "...",
"id": "event-hash",
"sig": "signature"
}]
["OK", "event-id", true, ""]
- Unique Challenge: Each AUTH attempt gets a unique challenge
- Replay Protection: Old AUTH events cannot be reused
- Time Sensitivity: AUTH events typically have short validity windows
- Digital Signatures: All events are cryptographically signed
- Public Key Verification: Relay verifies signature matches claimed pubkey
- Event Integrity: Event ID is hash of event content, preventing tampering
- Persistent Auth: Authentication persists for the WebSocket connection lifetime
- Per-Connection: Each new connection requires fresh authentication
- Selective Access: Relays can require AUTH for specific operations only
// ✅ CORRECT: Use same connection for AUTH and publishing
const relay = await pool.ensureRelay(url)
await authenticate(relay) // AUTH on this connection
await publishEvent(relay) // Publish on SAME connection
// ❌ WRONG: New connection loses authentication
const relay1 = await pool.ensureRelay(url)
await authenticate(relay1)
relay1.close()
const relay2 = await pool.ensureRelay(url) // New connection!
await publishEvent(relay2) // Will fail - not authenticated
// ✅ CORRECT: Use exact challenge from relay
relay.on('auth', (challenge) => {
const authEvent = {
tags: [['challenge', challenge]] // Exact challenge
}
})
// ❌ WRONG: Modified or old challenge
const authEvent = {
tags: [['challenge', 'old-challenge']] // Will be rejected
}
// ✅ CORRECT: Proper kind 30078 with d-tag
const event = {
kind: 30078,
tags: [['d', 'unique-id']] // Required for parameterized replaceable
}
// ❌ WRONG: Missing d-tag
const event = {
kind: 30078,
tags: [['t', 'topic']] // Missing required d-tag
}
- Always handle AUTH challenges immediately
- Keep the same WebSocket connection alive
- Implement proper timeout handling
- Validate all event structures before signing
- Handle extension permission requests gracefully
- Provide clear error messages to users
- Test with different relay implementations
- NIP-42 Authentication: Complete relay authentication flow
- Kind 30078 Events: Publish parameterized replaceable events after auth
- NIP-07 Integration: Works with browser extension signers (Alby, nos2x, etc.)
- Persistent Connection: Maintains same connection for AUTH and event publishing
- Modular Design: Reusable across multiple Astro pages
- Environment Configuration: Configurable relay URLs via environment variables
- TypeScript Support: Full type safety and IntelliSense
- Error Handling: Comprehensive error handling and user feedback
src/
├── lib/
│ └── nostr-auth.ts # Core authentication service
├── composables/
│ └── useNostrAuth.ts # Reusable composable with UI helpers
├── pages/
│ ├── nostr-auth-example.astro # Complete example page
│ └── simple-nostr-example.astro # Simple usage example
└── env.d.ts # Environment type definitions
- Install dependencies:
npm install nostr-tools
npm install -D @types/node typescript
- Set up environment variables:
# .env
HIVETALK_RELAYS=ws://localhost:3334
- Configure Astro (astro.config.mjs):
import { defineConfig } from 'astro/config';
export default defineConfig({
vite: {
define: {
'process.env.HIVETALK_RELAYS': JSON.stringify(process.env.HIVETALK_RELAYS || 'ws://localhost:3334')
}
}
});
The main authentication service that handles:
- NIP-42 authentication flow
- WebSocket connection management
- Event signing and publishing
- Error handling and timeouts
Key Methods:
authenticate()
: Perform NIP-42 AUTH with relaypublishKind30078Event()
: Publish parameterized replaceable eventsdisconnect()
: Clean up connectionsgetAuthStatus()
: Check authentication state
A higher-level composable that provides:
- State management with reactive updates
- UI helper utilities
- Simplified API for common operations
- Event subscription system
Key Features:
subscribe()
: Listen to auth state changescreateUIManager()
: Automatic DOM updates- Environment variable integration
- Error state management
Kind 30078 events are Parameterized Replaceable Events that:
- Require a "d" tag as unique identifier
- Can be updated/replaced by publishing new events with same d-tag
- Support additional custom tags
- Must be published on authenticated connections (if relay requires AUTH)
Event Structure:
{
"kind": 30078,
"created_at": 1699123456,
"tags": [
["d", "unique-identifier"],
["t", "custom-tag"],
["client", "my-app"]
],
"content": "Event content here",
"pubkey": "...",
"id": "...",
"sig": "..."
}
interface AuthConfig {
relayUrl: string // WebSocket relay URL
timeout?: number // Operation timeout in ms (default: 10000)
}
interface UseNostrAuthOptions {
relayUrl?: string // Override relay URL
timeout?: number // Operation timeout
autoConnect?: boolean // Auto-authenticate when extension available
}
- Extension Security: Always verify NIP-07 extension availability
- Connection Persistence: Maintain same connection for AUTH and publishing
- Event Validation: Validate all event data before signing
- Error Handling: Never expose sensitive information in error messages
- Timeout Management: Use appropriate timeouts to prevent hanging
"NIP-07 extension not found"
- Install a Nostr extension (Alby, nos2x, etc.)
- Refresh the page after installation
"Authentication timeout"
- Check relay URL and availability
- Increase timeout in configuration
- Verify relay supports NIP-42
"Event publish failed"
- Ensure authentication completed successfully
- Check d-tag uniqueness for replaceable events
- Verify relay accepts kind 30078 events
"Connection lost"
- Don't disconnect between AUTH and publishing
- Handle WebSocket connection errors gracefully
- Implement reconnection logic if needed
- Enable Console Logging: All operations log detailed information
- Check Network Tab: Verify WebSocket messages in browser dev tools
- Test Relay: Use a simple WebSocket client to test relay connectivity
- Validate Environment: Ensure HIVETALK_RELAYS is properly set
- NIP-42: Authentication of clients to relays
- NIP-01: Basic protocol flow
- NIP-07: Browser extension interface
- Nostr Tools Documentation
This implementation is designed to be modular and extensible. Feel free to:
- Add support for additional event kinds
- Implement reconnection logic
- Add more sophisticated error handling
- Create additional UI components
This code is provided as-is for educational and development purposes. Please review and test thoroughly before using in production applications.