Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26c0a71292 | ||
|
|
41fcbfd9ca | ||
|
|
6d5079561a | ||
|
|
11f24766e5 | ||
|
|
f10ee66972 | ||
|
|
6f46fce625 | ||
|
|
9dc7cdacec | ||
|
|
e9c8fccc9f | ||
|
|
6cf5d6c50e | ||
|
|
ecf248dfc2 | ||
|
|
1b28f78f44 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1 +1,3 @@
|
||||
Nostr_NIPs/
|
||||
Nostr_NIPs/
|
||||
nostr_login_lite/
|
||||
style_guide/
|
||||
|
||||
6
.gitmodules
vendored
Normal file
6
.gitmodules
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
[submodule "style_guide"]
|
||||
path = style_guide
|
||||
url = ssh://git@git.laantungir.net:222/laantungir/style_guide.git
|
||||
[submodule "nostr_login_lite"]
|
||||
path = nostr_login_lite
|
||||
url = ssh://git@git.laantungir.net:222/laantungir/nostr_login_lite.git
|
||||
153
DAEMON.md
153
DAEMON.md
@@ -1,153 +0,0 @@
|
||||
# Superball Daemon Rules
|
||||
|
||||
## What I Am
|
||||
I am Superball - an anonymizing node that provides location privacy for Nostr users by forwarding their encrypted events with timing delays and size obfuscation.
|
||||
|
||||
## What I Look For
|
||||
|
||||
### 1. Routing Events (Kind 30000)
|
||||
- Monitor all relays I'm connected to
|
||||
- Look for events with `kind: 30000`
|
||||
- Check if `tags` contains `["p", "<my_pubkey>"]`
|
||||
- These are events meant for me to process
|
||||
|
||||
### 2. Event Structure I Expect
|
||||
```json
|
||||
{
|
||||
"kind": 30000,
|
||||
"pubkey": "<some_ephemeral_key>", // Not important to me
|
||||
"content": "<nip44_encrypted_payload>", // This is what I need
|
||||
"tags": [["p", "<my_pubkey>"]],
|
||||
"created_at": <timestamp>,
|
||||
"id": "<event_id>",
|
||||
"sig": "<signature>"
|
||||
}
|
||||
```
|
||||
|
||||
## What I Do When I Receive An Event
|
||||
|
||||
### 1. Validate
|
||||
- Verify the event signature is valid
|
||||
- Confirm the `p` tag contains my pubkey
|
||||
- Ensure it's kind 30000
|
||||
|
||||
### 2. Decrypt
|
||||
- Use my private key with NIP-44 to decrypt the content
|
||||
- Extract the payload which contains:
|
||||
```json
|
||||
{
|
||||
"event": { /* The event to forward */ },
|
||||
"routing": {
|
||||
"relays": ["wss://relay1.com", "wss://relay2.com"],
|
||||
"delay": 30,
|
||||
"pad": "+150", // or "-50"
|
||||
"p": "next_superball_pubkey", // Optional - missing means final posting
|
||||
"audit": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456", // Required audit tag
|
||||
"payment": "eCash_token" // Optional
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Process Routing Instructions
|
||||
|
||||
#### Delay
|
||||
- Wait the specified number of seconds before forwarding
|
||||
- Add random jitter (±10%) to prevent timing analysis
|
||||
- Queue the event for delayed processing
|
||||
|
||||
#### Padding
|
||||
- **Remove padding (`"pad": "-N"`)**: Delete N bytes worth of padding tags from the event
|
||||
- **Add padding (`"pad": "+N"`)**: Create new routing wrapper with N bytes of padding tags
|
||||
|
||||
#### Relays
|
||||
- Post to ALL relays in the `relays` array
|
||||
- Validate all relay URLs are properly formatted
|
||||
- Provides redundancy and availability
|
||||
|
||||
#### Next Hop Logic
|
||||
- **`p` field present**: Create routing event for specified next Superball (can apply padding)
|
||||
- **`p` field missing**: Extract inner event and post directly to relays (end chain, no padding changes)
|
||||
|
||||
#### Padding Logic
|
||||
- **`p` field present + `pad` field**: Apply padding changes when creating routing wrapper
|
||||
- **`p` field missing**: Ignore any `pad` field - cannot modify signed event
|
||||
- **Final hop rule**: Never modify signed events, post exactly as received
|
||||
|
||||
#### Audit Tag Processing
|
||||
- **`audit` field**: Always present - include as `["p", "<audit_tag>"]` in routing event
|
||||
- **Camouflage**: Audit tag looks identical to real next-hop pubkeys
|
||||
- **Security**: Enables user detection of dropped/delayed/modified events
|
||||
|
||||
#### Payment Processing
|
||||
- **`payment` field present**: Process eCash token for service payment
|
||||
- **`payment` field missing**: Process for free (if daemon allows)
|
||||
|
||||
### 4. Forward Event
|
||||
|
||||
#### Always Rewrap (Important for Privacy)
|
||||
**ALWAYS** create a new routing event to hide whether padding was added or removed:
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": 30000, // Always use routing event
|
||||
"pubkey": "<my_ephemeral_key>", // Generate fresh ephemeral key
|
||||
"content": "<encrypted_inner_event>", // Re-encrypt with my key
|
||||
"tags": [
|
||||
["p", "<next_hop_or_final_destination>"],
|
||||
["p", "<audit_tag_from_routing>"], // Always include audit tag as p tag
|
||||
["padding", "<random_data_1>"], // Adjusted padding
|
||||
["padding", "<random_data_2>"] // May be more or less than before
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Next Hop Handling
|
||||
- **If `p` field in routing**: Create routing event with that pubkey in p tag
|
||||
- **If no `p` field in routing**: Extract inner event and post directly to all relays
|
||||
- **Multi-relay posting**: Post to every relay in the `relays` array
|
||||
- **End of chain**: When no `p` field, I am the final hop
|
||||
|
||||
## My Rules
|
||||
|
||||
### Security Rules
|
||||
1. **Never log sensitive data** - Don't store decrypted content or routing info
|
||||
2. **Generate new keys** - Use fresh ephemeral keys for each forward
|
||||
3. **Validate everything** - Check signatures, event structure, relay URLs
|
||||
4. **Rate limiting** - Don't process more than X events per minute from same source
|
||||
|
||||
### Privacy Rules
|
||||
1. **No correlation** - Don't link input events to output events in logs
|
||||
2. **Clear memory** - Immediately clear decrypted data after processing
|
||||
3. **Random timing** - Add jitter to specified delays
|
||||
4. **Mix traffic** - Send decoy traffic when idle (optional)
|
||||
|
||||
### Processing Rules
|
||||
1. **First come, first served** - Process events in order received
|
||||
2. **Fail silently** - Drop invalid events without response
|
||||
3. **Retry logic** - Attempt to post 3 times before giving up
|
||||
4. **Resource limits** - Drop oldest queued events if memory/queue full
|
||||
|
||||
### Network Rules
|
||||
1. **Multiple relays** - Connect to diverse set of relays
|
||||
2. **Separate connections** - Use different connections for input/output
|
||||
3. **AUTH support** - Prefer relays that support AUTH for privacy
|
||||
4. **Rotate connections** - Periodically reconnect to prevent fingerprinting
|
||||
|
||||
## What I Never Do
|
||||
|
||||
1. **Never modify final signatures** - Only remove padding tags, never add to signed events
|
||||
2. **Never store routing paths** - Process and forget
|
||||
3. **Never respond to clients** - Silent operation only
|
||||
4. **Never correlate users** - Each event is independent
|
||||
5. **Never log destinations** - Only log operational metrics
|
||||
|
||||
## Example Processing Flow
|
||||
|
||||
1. **Receive**: Kind 30000 event with my pubkey in p tag
|
||||
2. **Decrypt**: Extract inner event + routing instructions
|
||||
3. **Queue**: Schedule for delayed processing (e.g., 30 seconds + jitter)
|
||||
4. **Process**: Apply padding changes and prepare for forwarding
|
||||
5. **Forward**: Post to target relay(s)
|
||||
6. **Clean**: Clear all decrypted data from memory
|
||||
|
||||
I am a privacy-preserving relay that helps users post content while hiding their location. I ask no questions, store no logs, and remember nothing about the events that pass through me.
|
||||
90
EXAMPLE.md
90
EXAMPLE.md
@@ -5,8 +5,8 @@ Alice wants to post a message under her real identity while hiding her location
|
||||
|
||||
### Participants
|
||||
- **Alice**: Original sender (pubkey: `alice123...`)
|
||||
- **Superball A**: First hop (pubkey: `sball_a789...`)
|
||||
- **Superball B**: Second hop (pubkey: `sball_b012...`)
|
||||
- **Thrower A**: First hop (pubkey: `thrower_a789...`)
|
||||
- **Thrower B**: Second hop (pubkey: `thrower_b012...`)
|
||||
- **Relay1**: `wss://relay1.com` (where Alice posts)
|
||||
- **Relay2**: `wss://relay2.com` (intermediate relay)
|
||||
- **Relay3**: `wss://relay3.com` (where final message appears)
|
||||
@@ -20,23 +20,23 @@ Alice wants to post a message under her real identity while hiding her location
|
||||
"pubkey": "alice123...",
|
||||
"content": "The government is lying about inflation statistics",
|
||||
"tags": [],
|
||||
"created_at": 1703000000,
|
||||
"created_at": 1702222200,
|
||||
"id": "alice_event_id",
|
||||
"sig": "alice_signature"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Alice Encrypts Instructions for Superball B (Final Hop)
|
||||
Payload for Superball B (final hop - no `p` field):
|
||||
### 2. Alice Encrypts Instructions for Thrower B (Final Hop)
|
||||
Payload for Thrower B (final hop - no `p` field):
|
||||
```json
|
||||
{
|
||||
"event": { /* Alice's signed event above */ },
|
||||
"routing": {
|
||||
"relays": ["wss://relay3.com", "wss://relay4.com"],
|
||||
"delay": 15,
|
||||
"audit": "9f8e7d6c5b4a39281726354019283746502918374650283746501928374650"
|
||||
"audit": "audit_tag_b_456def",
|
||||
"payment": "eCash_ZYX321..." // Optional payment
|
||||
// No "p" field - this means final posting
|
||||
// No "pad" field - can't modify signed event
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -44,26 +44,26 @@ Payload for Superball B (final hop - no `p` field):
|
||||
Creates routing event:
|
||||
```json
|
||||
{
|
||||
"kind": 30000,
|
||||
"kind": 22222,
|
||||
"pubkey": "ephemeral_key_2",
|
||||
"content": "<encrypted_payload_for_superball_b>",
|
||||
"tags": [["p", "sball_b012..."]],
|
||||
"content": "<encrypted_payload_for_thrower_b>",
|
||||
"tags": [["p", "thrower_b012..."]],
|
||||
"created_at": 1703000100,
|
||||
"id": "routing_for_b",
|
||||
"sig": "ephemeral_signature_2"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Alice Encrypts Instructions for Superball A (First Hop)
|
||||
Payload for Superball A (continuing chain):
|
||||
### 3. Alice Encrypts Instructions for Thrower A (First Hop)
|
||||
Payload for Thrower A (continuing chain):
|
||||
```json
|
||||
{
|
||||
"event": { /* routing event for Superball B above */ },
|
||||
"event": { /* routing event for Thrower B above */ },
|
||||
"routing": {
|
||||
"relays": ["wss://relay2.com"],
|
||||
"delay": 45,
|
||||
"pad": "+200",
|
||||
"p": "sball_b012...", // Next Superball in chain
|
||||
"add_padding_bytes": 200,
|
||||
"p": "thrower_b012...", // Next Thrower in chain
|
||||
"audit": "1a2b3c4d5e6f7890abcdef1234567890abcdef1234567890abcdef1234567890",
|
||||
"payment": "eCash_A1B2C3..." // Optional payment
|
||||
}
|
||||
@@ -73,10 +73,10 @@ Payload for Superball A (continuing chain):
|
||||
Alice posts this to Relay1:
|
||||
```json
|
||||
{
|
||||
"kind": 30000,
|
||||
"kind": 22222,
|
||||
"pubkey": "ephemeral_key_1",
|
||||
"content": "<encrypted_payload_for_superball_a>",
|
||||
"tags": [["p", "sball_a789..."]],
|
||||
"content": "<encrypted_payload_for_thrower_a>",
|
||||
"tags": [["p", "thrower_a789..."]],
|
||||
"created_at": 1703000200,
|
||||
"id": "routing_for_a",
|
||||
"sig": "ephemeral_signature_1"
|
||||
@@ -87,42 +87,46 @@ Alice posts this to Relay1:
|
||||
|
||||
**T+0**: Alice posts routing event to Relay1
|
||||
```
|
||||
Relay1: kind 30000 event (p tag = sball_a789...)
|
||||
Relay1: kind 22222 event (p tag = thrower_a789...)
|
||||
```
|
||||
|
||||
**T+5**: Superball A processes
|
||||
**T+5**: Thrower A processes
|
||||
- Decrypts payload
|
||||
- Sees: relay2.com, delay 45s, pad +200
|
||||
- Needs to ADD padding, so creates new wrapper
|
||||
- Sees: relay2.com, delay 45s, add_padding_bytes 200, next hop thrower_b012...
|
||||
- Creates padding-wrapper payload around the inner encrypted event
|
||||
- Queues for 45-second delay
|
||||
|
||||
**T+50**: Superball A always rewraps (consistent behavior)
|
||||
**T+50**: Thrower A forwards with padding wrapper
|
||||
```
|
||||
Relay2: NEW routing event (always looks the same)
|
||||
Relay2: NEW routing event with padding wrapper
|
||||
{
|
||||
"kind": 30000,
|
||||
"pubkey": "superball_a_ephemeral_key", // Fresh key
|
||||
"content": "<newly_encrypted_payload>", // Re-encrypted
|
||||
"kind": 22222,
|
||||
"pubkey": "thrower_a_ephemeral_key", // Fresh key
|
||||
"content": "<padding_wrapper_payload>", // Contains inner event + padding
|
||||
"tags": [
|
||||
["p", "sball_b012..."], // Real next hop
|
||||
["p", "1a2b3c4d5e6f7890abcdef1234567890abcdef1234567890abcdef1234567890"], // Audit tag
|
||||
["padding", "random_data_1..."], // Adjusted padding
|
||||
["padding", "random_data_2..."], // (+200 bytes added)
|
||||
["padding", "random_data_3..."]
|
||||
["p", "thrower_b012..."], // Real next hop
|
||||
["p", "1a2b3c4d5e6f7890abcdef1234567890abcdef1234567890abcdef1234567890"] // Audit tag
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Alice monitors relay2.com and sees her audit tag `1a2b3c4d5e6f...` appear at T+50 with correct +200 byte padding, confirming Superball A is honest.
|
||||
Where the padding_wrapper_payload contains:
|
||||
```json
|
||||
{
|
||||
"event": { /* The still-encrypted inner event for Thrower B */ },
|
||||
"padding": "random_padding_data_200_bytes_worth"
|
||||
}
|
||||
```
|
||||
|
||||
**T+55**: Superball B processes
|
||||
- Decrypts payload
|
||||
- Sees: Alice's event + instructions (relays=[relay3.com, relay4.com], delay 15s)
|
||||
Alice monitors relay2.com and sees her audit tag `1a2b3c4d5e6f...` appear at T+50, confirming Thrower A is honest.
|
||||
|
||||
**T+55**: Thrower B processes
|
||||
- First decrypt: Gets padding wrapper payload - discards padding
|
||||
- Second decrypt: Gets Alice's event + routing instructions (relays=[relay3.com, relay4.com], delay 15s)
|
||||
- NO `p` field - this means final posting, extract and post Alice's event exactly as-is
|
||||
- Cannot modify padding on signed event
|
||||
- Queues for 15-second delay
|
||||
|
||||
**T+70**: Superball B posts Alice's final event (end of chain)
|
||||
**T+70**: Thrower B posts Alice's final event (end of chain)
|
||||
```
|
||||
Relay3 AND Relay4: Alice's original signed event appears exactly as she created it
|
||||
{
|
||||
@@ -130,7 +134,7 @@ Relay3 AND Relay4: Alice's original signed event appears exactly as she created
|
||||
"pubkey": "alice123...",
|
||||
"content": "The government is lying about inflation statistics",
|
||||
"tags": [], // Original tags preserved
|
||||
"created_at": 1703000000,
|
||||
"created_at": 1702222200,
|
||||
"id": "alice_event_id",
|
||||
"sig": "alice_signature" // Original signature preserved
|
||||
}
|
||||
@@ -141,16 +145,16 @@ Alice's message now appears on both relay3.com and relay4.com for redundancy.
|
||||
## Privacy and Security Achieved
|
||||
|
||||
- **Alice's location**: Completely hidden from surveillance
|
||||
- **Message origin**: Appears to come from Superball B's location
|
||||
- **Message origin**: Appears to come from Thrower B's location
|
||||
- **Traffic analysis**: 65-second delay + size changes prevent correlation
|
||||
- **Identity preserved**: Alice's real pubkey and signature maintained
|
||||
- **Plausible deniability**: No proof Alice initiated the posting
|
||||
- **Malicious node detection**: Audit tags allow Alice to verify proper forwarding
|
||||
- **Accountability**: Bad Superballs can be identified and avoided
|
||||
- **Accountability**: Bad Throwers can be identified and avoided
|
||||
|
||||
### Audit Trail for Alice
|
||||
- **T+50**: Audit tag `1a2b3c4d5e6f...` appears on relay2.com (✓ Superball A honest)
|
||||
- **T+70**: Final message appears on relay3.com and relay4.com (✓ Superball B honest)
|
||||
- **T+50**: Audit tag `1a2b3c4d5e6f...` appears on relay2.com (✓ Thrower A honest)
|
||||
- **T+70**: Final message appears on relay3.com and relay4.com (✓ Thrower B honest)
|
||||
- **Size verification**: Event sizes match expected padding operations
|
||||
- **Timing verification**: Delays match requested timeouts
|
||||
|
||||
|
||||
22
README.md
22
README.md
@@ -1,29 +1,33 @@
|
||||
# Superball
|
||||

|
||||
|
||||
Superball provides Tor-like location privacy for Nostr users. It's a daemon that bounces encrypted events between relays, allowing users to post content under their real identity while completely hiding their network location.
|
||||
Superball provides Tor-like location privacy for Nostr users. A **Superball** is a wrapped nostr event, similar to a TOR onion packet, that bounces between nostr relays and **Throwers**.
|
||||
|
||||
A **Thrower** is a nostr-capable daemon that monitors nostr nodes, looks for superballs posted for them, grabs them, cryptographically unwraps them, rewraps them potentially with padding, then throws the superball to relay(s) that it is instructed to, when it is instructed to.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **User creates content** - Normal signed Nostr event with their real pubkey
|
||||
2. **Encrypt with routing** - Bundle event + routing instructions, encrypt to Superball daemon
|
||||
3. **Anonymous forwarding** - Event bounces through multiple daemons with delays and padding
|
||||
4. **Final posting** - Original event appears on target relay with user's identity but from daemon's location
|
||||
2. **Wrap as Superball** - Bundle event + routing instructions, encrypt to Thrower daemon
|
||||
3. **Anonymous bouncing** - Superball bounces through multiple Throwers with delays and padding
|
||||
4. **Final posting** - Original event appears on target relay with user's identity but from Thrower's location
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Location Privacy**: Hide your network location while preserving your identity
|
||||
- **Traffic Analysis Resistance**: Random delays and size padding prevent correlation
|
||||
- **Simple Protocol**: Uses NIP-44 encryption with new kind 30000 for routing
|
||||
- **Flexible Routing**: Support for multi-hop paths through multiple daemons
|
||||
- **Simple Protocol**: Uses NIP-44 encryption with new kind 22222 for routing
|
||||
- **Flexible Routing**: Support for multi-hop paths through multiple Throwers
|
||||
- **Signature Preservation**: Original event signatures maintained for authenticity
|
||||
- **Audit Security**: Detect and avoid malicious Superballs through cryptographic verification
|
||||
- **Audit Security**: Detect and avoid malicious Throwers through cryptographic verification
|
||||
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
- [`PROTOCOL.md`](PROTOCOL.md) - Technical protocol specification
|
||||
- [`THROWER.md`](THROWER.md) - Rules and behavior for Thrower operators
|
||||
- [`EXAMPLE.md`](EXAMPLE.md) - Complete walkthrough example
|
||||
- [`DAEMON.md`](DAEMON.md) - Rules and behavior for Superball daemon operators
|
||||
- [`SUPs.md`](SUPs.md) - Superball Upgrade Proposals
|
||||
|
||||
Perfect for journalists, activists, or anyone who needs to protect their physical location while maintaining accountability for their public statements.
|
||||
|
||||
|
||||
350
SUPs.md
Normal file
350
SUPs.md
Normal file
@@ -0,0 +1,350 @@
|
||||
# SUPs - Superball Upgrade Possibilities
|
||||
|
||||
SUPs (Superball Upgrade Possibilities) describe standards for the Superball anonymity protocol, including core protocol rules, Thrower behavior specifications, routing algorithms, and security mechanisms.
|
||||
|
||||
## Terminology
|
||||
- **Superball**: A wrapped nostr event (like a TOR onion packet) that bounces between relays and Throwers
|
||||
- **Thrower**: A nostr-capable daemon that monitors nostr nodes, looks for superballs posted for them, grabs them, cryptographically unwraps them, rewraps them potentially with padding, then throws the superball to relay(s) when instructed
|
||||
|
||||
|
||||
---
|
||||
|
||||
## SUP-1: Core Protocol Specification
|
||||
|
||||
### Abstract
|
||||
|
||||
This SUP defines the core Superball protocol for providing location privacy in Nostr through encrypted event routing via Thrower nodes. Users can post content under their real identity while completely hiding their network location.
|
||||
|
||||
### Motivation
|
||||
|
||||
Current Nostr implementations reveal users' network locations through direct relay connections, enabling surveillance and censorship. Superball provides Tor-like anonymity while preserving message authenticity through cryptographic signatures.
|
||||
|
||||
### Specification
|
||||
|
||||
#### Event Kind
|
||||
- **Kind 22222**: Superball routing event
|
||||
|
||||
#### Routing Event Structure
|
||||
```json
|
||||
{
|
||||
"kind": 22222,
|
||||
"pubkey": "<ephemeral_key>",
|
||||
"content": "<nip44_encrypted_payload>",
|
||||
"tags": [
|
||||
["p", "<thrower_pubkey>"],
|
||||
["p", "<audit_pubkey>"],
|
||||
["padding", "<random_data>"]
|
||||
],
|
||||
"created_at": "<timestamp>",
|
||||
"id": "<event_id>",
|
||||
"sig": "<ephemeral_signature>"
|
||||
}
|
||||
```
|
||||
|
||||
#### Encrypted Payload Format
|
||||
```json
|
||||
{
|
||||
"event": { /* Original signed event or next routing event */ },
|
||||
"routing": {
|
||||
"relays": ["wss://relay1.com", "wss://relay2.com"],
|
||||
"delay": 30,
|
||||
"padding": "+150",
|
||||
"p": "next_thrower_pubkey", // Optional
|
||||
"audit": "audit_verification_pubkey", // Required
|
||||
"payment": "ecash_token" // Optional
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Processing Rules
|
||||
1. **Always Rewrap**: Create new routing event for every forward
|
||||
2. **Audit Verification**: Include audit tag as p tag in routing event
|
||||
3. **Delay Compliance**: Wait specified time plus random jitter
|
||||
4. **Padding Operations**: Apply size modifications as instructed
|
||||
5. **Multi-Relay Posting**: Post to all specified relays
|
||||
6. **Relay Authentication Constraint**: Throwers can only write to relays that do not require authentication (AUTH)
|
||||
|
||||
#### Relay Authentication Requirements
|
||||
|
||||
**Critical Constraint**: Throwers MUST be able to post events signed by other users without possessing their private keys. This creates a fundamental limitation:
|
||||
|
||||
- **Read Capability**: Throwers can monitor any relay (AUTH or non-AUTH) for incoming Superballs
|
||||
- **Write Capability**: Throwers can ONLY post to relays that do not require NIP-42 authentication
|
||||
- **NIP-65 Compliance**: Throwers must maintain accurate relay lists distinguishing read vs write capabilities
|
||||
|
||||
**NIP-65 Relay List Format for Throwers**:
|
||||
```json
|
||||
{
|
||||
"kind": 10002,
|
||||
"tags": [
|
||||
["r", "wss://noauth.relay.com"], // Read+Write (no AUTH required)
|
||||
["r", "wss://auth-required.relay.com", "read"], // Read only (AUTH required)
|
||||
["r", "wss://write-only.relay.com", "write"] // Write only (no AUTH required)
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Authentication Testing**: Throwers should automatically test relay authentication requirements by attempting anonymous event publication and classify relays accordingly.
|
||||
|
||||
### Rationale
|
||||
|
||||
The protocol uses NIP-44 encryption for simplicity over NIP-59 gift wrapping, includes mandatory audit mechanisms for security, and employs consistent rewrapping to prevent traffic analysis.
|
||||
|
||||
---
|
||||
|
||||
## SUP-2: Audit Security Mechanism
|
||||
|
||||
### Abstract
|
||||
|
||||
This SUP defines the audit mechanism that allows users to detect malicious Throwers through cryptographic verification of proper event forwarding.
|
||||
|
||||
### Motivation
|
||||
|
||||
Users need the ability to verify that Throwers are honestly forwarding Superballs according to instructions, including proper delays, padding operations, and relay posting.
|
||||
|
||||
### Specification
|
||||
|
||||
#### Audit Tag Format
|
||||
- **Length**: 64 hexadecimal characters
|
||||
- **Format**: Appears as valid Nostr pubkey
|
||||
- **Generation**: Cryptographically secure random per-hop
|
||||
- **Posting**: Always included as `["p", "<audit_tag>"]` in routing events
|
||||
|
||||
#### User Monitoring
|
||||
1. Generate unique audit tag for each routing hop
|
||||
2. Monitor specified relays for audit tag appearance
|
||||
3. Verify timing matches delay instructions
|
||||
4. Verify event size matches padding operations
|
||||
5. Build reputation scores for Throwers based on compliance
|
||||
|
||||
#### Privacy Properties
|
||||
- Audit tags indistinguishable from real next-hop pubkeys
|
||||
- Only originating user knows which tags are audit verification
|
||||
- No correlation possible between different users' audit tags
|
||||
|
||||
### Implementation
|
||||
|
||||
```javascript
|
||||
// Generate audit tag
|
||||
const auditTag = bytesToHex(randomBytes(32));
|
||||
|
||||
// Monitor relay for audit appearance
|
||||
const monitorAudit = async (relay, auditTag, expectedDelay, expectedSize) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
relay.subscribe([{
|
||||
kinds: [22222],
|
||||
"#p": [auditTag]
|
||||
}], {
|
||||
onevent: (event) => {
|
||||
const actualDelay = (event.created_at * 1000) - startTime;
|
||||
const actualSize = JSON.stringify(event).length;
|
||||
|
||||
// Verify compliance
|
||||
const delayCompliant = Math.abs(actualDelay - expectedDelay) < TOLERANCE;
|
||||
const sizeCompliant = Math.abs(actualSize - expectedSize) < PADDING_TOLERANCE;
|
||||
|
||||
recordThrowerReputation(event.pubkey, delayCompliant && sizeCompliant);
|
||||
}
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SUP-3: Daemon Operational Rules
|
||||
|
||||
### Abstract
|
||||
|
||||
This SUP provides operational guidelines for Thrower operators, including security practices, resource management, and privacy protection protocols.
|
||||
|
||||
### Security Rules
|
||||
1. **Never log sensitive data** - Don't store decrypted content or routing information
|
||||
2. **Generate fresh keys** - Use new ephemeral keys for each forward operation
|
||||
3. **Validate everything** - Check signatures, event structure, and relay URLs
|
||||
4. **Rate limiting** - Prevent abuse through request throttling
|
||||
5. **Memory clearing** - Immediately clear decrypted data after processing
|
||||
|
||||
### Privacy Rules
|
||||
1. **No correlation** - Don't link input events to output events in logs
|
||||
2. **Random timing** - Add jitter to specified delays
|
||||
3. **Traffic mixing** - Send decoy traffic when idle (optional enhancement)
|
||||
4. **Connection rotation** - Periodically reconnect to prevent fingerprinting
|
||||
|
||||
### Processing Rules
|
||||
1. **First come, first served** - Process events in arrival order
|
||||
2. **Fail silently** - Drop invalid events without response
|
||||
3. **Retry logic** - Attempt relay posting 3 times before giving up
|
||||
4. **Resource limits** - Drop oldest queued events if memory/queue full
|
||||
|
||||
---
|
||||
|
||||
## SUP-4: Multi-Path Routing Enhancement
|
||||
|
||||
### Abstract
|
||||
|
||||
This SUP proposes an enhancement allowing users to send the same event through multiple independent Thrower chains simultaneously for increased security against coordinated attacks.
|
||||
|
||||
### Motivation
|
||||
|
||||
Single routing chains are vulnerable to adversaries who control multiple Throwers in the path. Multi-path routing increases security by requiring adversaries to control Throwers across multiple independent chains.
|
||||
|
||||
### Specification
|
||||
|
||||
#### Multi-Path Event Structure
|
||||
```json
|
||||
{
|
||||
"paths": [
|
||||
{
|
||||
"chain_id": "random_identifier_1",
|
||||
"routing": { /* Standard routing instructions */ }
|
||||
},
|
||||
{
|
||||
"chain_id": "random_identifier_2",
|
||||
"routing": { /* Different Throwers and relays */ }
|
||||
}
|
||||
],
|
||||
"threshold": 1 // Minimum successful deliveries required
|
||||
}
|
||||
```
|
||||
|
||||
#### Implementation Requirements
|
||||
- Generate independent routing chains with no overlapping Throwers
|
||||
- Use different target relays for each path
|
||||
- Include chain_id in audit mechanism for path-specific monitoring
|
||||
- Consider delivery successful when threshold number of paths complete
|
||||
|
||||
### Security Analysis
|
||||
|
||||
- **Resistance**: Requires adversary control of nodes across multiple chains
|
||||
- **Redundancy**: Event delivery succeeds even if some paths are compromised
|
||||
- **Cost**: Increases bandwidth and processing requirements
|
||||
- **Detection**: Path-specific audit tags enable per-chain monitoring
|
||||
|
||||
---
|
||||
|
||||
## SUP-5: Payment Integration Specification
|
||||
|
||||
### Abstract
|
||||
|
||||
This SUP defines the integration of eCash payment tokens for monetized Thrower services, enabling sustainable economic models for privacy infrastructure.
|
||||
|
||||
### Specification
|
||||
|
||||
#### Payment Field Format
|
||||
```json
|
||||
{
|
||||
"payment": {
|
||||
"type": "ecash",
|
||||
"token": "base64_encoded_ecash_token",
|
||||
"amount": 100, // satoshis
|
||||
"mint": "https://mint.example.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Processing Rules
|
||||
1. **Validate token** - Verify eCash token authenticity with mint
|
||||
2. **Check amount** - Ensure payment meets minimum service fee
|
||||
3. **Redeem atomically** - Claim payment only after successful forwarding
|
||||
4. **Rate limits** - Higher processing priority for paid requests
|
||||
|
||||
#### Economic Considerations
|
||||
- Free tier with limited throughput for accessibility
|
||||
- Premium tiers with guaranteed processing times
|
||||
- Dynamic pricing based on network congestion
|
||||
- Reputation bonuses for consistent payment
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## SUP-6: Thrower Information Document
|
||||
|
||||
### Abstract
|
||||
|
||||
This SUP defines a standardized announcement mechanism for Throwers to advertise their services and signal their online status through replaceable events.
|
||||
|
||||
### Motivation
|
||||
|
||||
Users need a reliable way to discover available Throwers and verify their operational status. This SUP provides a standardized format for Throwers to announce their services and capabilities. By specifying a refresh rate, one can see if the document has been created within that refresh rate, and if so, you know that the Thrower is online.
|
||||
|
||||
### Specification
|
||||
|
||||
#### Event Kind
|
||||
- **Kind 12222**: Thrower Information Document (Replaceable Event)
|
||||
|
||||
#### Event Structure
|
||||
```json
|
||||
{
|
||||
"id": "<32-bytes lowercase hex-encoded sha256 of the serialized event data>",
|
||||
"pubkey": "<32-bytes lowercase hex-encoded public key of the Thrower>",
|
||||
"created_at": "<unix timestamp in seconds>",
|
||||
"kind": 12222,
|
||||
"tags": [
|
||||
["name", "<string identifying Thrower>"],
|
||||
["description", "<string with detailed information>"],
|
||||
["banner", "<a link to an image (e.g. in .jpg, or .png format)>"],
|
||||
["icon", "<a link to an icon (e.g. in .jpg, or .png format>)"],
|
||||
["pubkey", "<administrative contact pubkey>"],
|
||||
["contact", "<administrative alternate contact>"],
|
||||
["supported_sups", "<a list of SUP numbers supported by the Thrower>"],
|
||||
["software", "<string identifying Thrower software URL>"],
|
||||
["version", "<string version identifier>"],
|
||||
["privacy_policy", "<a link to a text file describing the Thrower's privacy policy>"],
|
||||
["terms_of_service", "<a link to a text file describing the Thrower's terms of service>"],
|
||||
["refresh_rate", "<if the Thrower is online, it will refresh this document within this many seconds>"]
|
||||
],
|
||||
"content": "<arbitrary string>",
|
||||
"sig": "<64-bytes lowercase hex of the signature>"
|
||||
}
|
||||
```
|
||||
|
||||
#### Operational Requirements
|
||||
|
||||
1. **Service Announcement**: Inform users on a relay of the Thrower's presence
|
||||
2. **Liveness Signal**: By updating the document within its refresh rate, signals that the Thrower is currently online
|
||||
3. **Discovery**: Enable automated discovery of available Throwers by clients
|
||||
|
||||
#### Implementation Guidelines
|
||||
|
||||
- Update frequency should indicate operational status
|
||||
- Include comprehensive service information in tags
|
||||
- Use standard URLs for policy documents
|
||||
- Maintain consistent pubkey identity across announcements
|
||||
|
||||
### Rationale
|
||||
|
||||
Standardized service announcements enable better user experience through automated Thrower discovery and verification of operational status through regular updates.
|
||||
|
||||
---
|
||||
|
||||
## Future SUP Topics
|
||||
|
||||
### Proposed Enhancements
|
||||
- **SUP-7**: Zero-Knowledge Proof Integration for Verifiable Delays
|
||||
- **SUP-8**: Decentralized Thrower Discovery Protocol Enhancement
|
||||
- **SUP-9**: Quantum-Resistant Encryption Migration
|
||||
- **SUP-10**: Mobile Client Optimizations
|
||||
- **SUP-11**: Governance and Protocol Upgrade Mechanisms
|
||||
|
||||
### Research Areas
|
||||
- Traffic analysis resistance measurements
|
||||
- Economic attack vectors and mitigations
|
||||
- Integration with other privacy protocols
|
||||
- Scalability optimizations
|
||||
- Censorship resistance enhancements
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
SUPs follow the collaborative development process:
|
||||
|
||||
1. **Draft**: Create initial specification
|
||||
2. **Discussion**: Community review and feedback
|
||||
3. **Implementation**: Reference implementations
|
||||
4. **Testing**: Security and compatibility testing
|
||||
5. **Final**: Adoption by Superball ecosystem
|
||||
|
||||
Submit SUP proposals through the project repository with detailed technical specifications, security analysis, and implementation considerations.
|
||||
184
THROWER.md
Normal file
184
THROWER.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# Thrower Rules
|
||||
|
||||
## What I Am
|
||||
I am a Thrower - an anonymizing node that provides location privacy for Nostr users by catching, unwrapping, rewrapping and throwing Superballs (wrapped encrypted events) with timing delays and size obfuscation.
|
||||
|
||||
|
||||
## What I Look For
|
||||
|
||||
### 1. Routing Events (Kind 22222)
|
||||
- Monitor all relays I'm connected to
|
||||
- Look for events with `kind: 22222`
|
||||
- Check if `tags` contains `["p", "<my_pubkey>"]`
|
||||
- These are events meant for me to process
|
||||
|
||||
### 2. Event Structure I Expect
|
||||
```json
|
||||
{
|
||||
"kind": 22222,
|
||||
"pubkey": "<some_ephemeral_key>", // Not important to me
|
||||
"content": "<nip44_encrypted_payload>", // This is what I need
|
||||
"tags": [["p", "<my_pubkey>"]],
|
||||
"created_at": <timestamp>,
|
||||
"id": "<event_id>",
|
||||
"sig": "<signature>"
|
||||
}
|
||||
```
|
||||
|
||||
## What I Do When I Receive An Event
|
||||
|
||||
### 1. Validate
|
||||
- Verify the event signature is valid
|
||||
- Confirm the `p` tag contains my pubkey
|
||||
- Ensure it's kind 22222
|
||||
|
||||
### 2. Decrypt and Identify Payload Type
|
||||
- Use my private key with NIP-44 to decrypt the content
|
||||
- Check payload structure to determine type:
|
||||
|
||||
#### Type 1: Routing Payload (Created by Builder)
|
||||
```json
|
||||
{
|
||||
"event": { /* Final event or inner wrapped event */ },
|
||||
"routing": { /* My routing instructions from builder */
|
||||
"relays": ["wss://relay1.com", "wss://relay2.com"],
|
||||
"delay": 30,
|
||||
"p": "next_thrower_pubkey", // Optional - missing means final posting
|
||||
"audit": "audit_tag", // Required audit tag
|
||||
"payment": "eCash_token", // Optional
|
||||
"add_padding_bytes": 256 // Optional
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Type 2: Padding Payload (Created by Previous Thrower)
|
||||
```json
|
||||
{
|
||||
"event": { /* Still-encrypted inner event */ },
|
||||
"padding": "01234567890123" // Padding data to discard
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Handle Payload Type
|
||||
|
||||
#### If Padding Payload:
|
||||
1. **Discard padding** - Ignore padding field completely
|
||||
2. **Decrypt again** - The "event" field contains another encrypted payload
|
||||
3. **Process the inner payload** - This will be a routing payload meant for me
|
||||
|
||||
#### If Routing Payload:
|
||||
1. **Process routing instructions** - These were created by the builder specifically for me
|
||||
2. **Continue with normal processing**
|
||||
|
||||
### 4. Process Routing Instructions
|
||||
|
||||
#### Delay
|
||||
- Wait the specified number of seconds before forwarding
|
||||
- Add random jitter (±10%) to prevent timing analysis
|
||||
- Queue the event for delayed processing
|
||||
|
||||
#### Relays
|
||||
- Post to ALL relays in the `relays` array that don't require AUTH
|
||||
- Skip AUTH-required relays when posting final events (can't authenticate as original author)
|
||||
- Validate all relay URLs are properly formatted
|
||||
- Provides redundancy and availability within AUTH constraints
|
||||
|
||||
#### Next Hop Logic
|
||||
- **`p` field present**: Forward to next Thrower with padding-only wrapper
|
||||
- **`p` field missing**: Post inner event directly to relays (end chain)
|
||||
|
||||
#### Audit Tag Processing
|
||||
- **`audit` field**: Always present - include as `["p", "<audit_tag>"]` in routing event
|
||||
- **Camouflage**: Audit tag looks identical to real next-hop pubkeys
|
||||
- **Security**: Enables user detection of dropped/delayed/modified events
|
||||
|
||||
#### Payment Processing
|
||||
- **`payment` field present**: Process eCash token for service payment
|
||||
- **`payment` field missing**: Process for free (if thrower allows)
|
||||
|
||||
### 5. Forward Event
|
||||
|
||||
#### Two-Path Processing
|
||||
|
||||
**Path 1: Forward to Next Thrower (`p` field present)**
|
||||
- Create padding-only wrapper (never create routing instructions)
|
||||
- Generate fresh ephemeral keypair
|
||||
- Create padding payload:
|
||||
```json
|
||||
{
|
||||
"event": { /* The still-encrypted inner event */ },
|
||||
"padding": "random_padding_data_123456789"
|
||||
}
|
||||
```
|
||||
- Encrypt to next Thrower's pubkey
|
||||
- Create routing event with next hop's pubkey in p tag
|
||||
|
||||
**Path 2: Final Posting (`p` field missing)**
|
||||
- Extract inner event from payload
|
||||
- Post directly to all relays in routing.relays array
|
||||
- No wrapping or encryption needed
|
||||
- End of chain
|
||||
|
||||
#### Critical Rules
|
||||
1. **Throwers NEVER create routing instructions** - Only padding
|
||||
2. **Routing instructions come ONLY from the builder** - Pre-encrypted for each hop
|
||||
3. **Always use fresh ephemeral keys** when forwarding
|
||||
4. **Include audit tag** in routing event p tags for camouflage
|
||||
|
||||
## My Rules
|
||||
|
||||
### Security Rules
|
||||
1. **Never log sensitive data** - Don't store decrypted content or routing info
|
||||
2. **Generate new keys** - Use fresh ephemeral keys for each forward
|
||||
3. **Validate everything** - Check signatures, event structure, relay URLs
|
||||
4. **Rate limiting** - Don't process more than X events per minute from same source
|
||||
|
||||
### Privacy Rules
|
||||
1. **No correlation** - Don't link input events to output events in logs
|
||||
2. **Clear memory** - Immediately clear decrypted data after processing
|
||||
3. **Random timing** - Add jitter to specified delays
|
||||
4. **Mix traffic** - Send decoy traffic when idle (optional)
|
||||
|
||||
### Processing Rules
|
||||
1. **First come, first served** - Process events in order received
|
||||
2. **Fail silently** - Drop invalid events without response
|
||||
3. **Retry logic** - Attempt to post 3 times before giving up
|
||||
4. **Resource limits** - Drop oldest queued events if memory/queue full
|
||||
|
||||
### Network Rules
|
||||
1. **Multiple relays** - Connect to diverse set of relays
|
||||
2. **Separate connections** - Use different connections for input/output
|
||||
3. **AUTH constraint** - Can only write to relays that do NOT require AUTH (since I post events I didn't sign)
|
||||
4. **Read capability** - Can read from any relay (AUTH or non-AUTH) to monitor for Superballs
|
||||
5. **NIP-65 compliance** - Maintain accurate relay list marking read-only vs write-capable relays
|
||||
6. **Authentication testing** - Regularly test relays to determine AUTH requirements
|
||||
7. **Rotate connections** - Periodically reconnect to prevent fingerprinting
|
||||
|
||||
## What I Never Do
|
||||
|
||||
1. **Never modify final signatures** - Only remove padding tags, never add to signed events
|
||||
2. **Never store routing paths** - Process and forget
|
||||
3. **Never respond to clients** - Silent operation only
|
||||
4. **Never correlate users** - Each Superball is independent
|
||||
5. **Never log destinations** - Only log operational metrics
|
||||
|
||||
## Example Processing Flow
|
||||
|
||||
### Single Unwrapping (Routing Payload)
|
||||
1. **Receive**: Kind 22222 event with my pubkey in p tag
|
||||
2. **Decrypt**: Get payload with routing instructions
|
||||
3. **Process**: These routing instructions were created for me by builder
|
||||
4. **Forward or Post**: Based on routing.p field
|
||||
|
||||
### Double Unwrapping (Padding Payload)
|
||||
1. **Receive**: Kind 22222 event with my pubkey in p tag
|
||||
2. **First Decrypt**: Get padding payload - discard padding
|
||||
3. **Second Decrypt**: Decrypt the inner event to get my routing instructions
|
||||
4. **Process**: These routing instructions were created for me by builder
|
||||
5. **Forward or Post**: Based on routing.p field
|
||||
|
||||
### Clean Up
|
||||
- **Queue**: Schedule for delayed processing (e.g., 30 seconds + jitter)
|
||||
- **Clean**: Clear all decrypted data from memory after processing
|
||||
|
||||
I am a privacy-preserving Thrower that helps users post content while hiding their location. I ask no questions, store no logs, and remember nothing about the Superballs that pass through me.
|
||||
@@ -14,7 +14,7 @@ User creates and signs their normal Nostr event:
|
||||
"pubkey": "user_pubkey",
|
||||
"content": "Message content",
|
||||
"tags": [],
|
||||
"created_at": 1703000000,
|
||||
"created_at": 1702222200,
|
||||
"id": "event_id",
|
||||
"sig": "user_signature"
|
||||
}
|
||||
@@ -30,7 +30,7 @@ User creates routing instructions:
|
||||
"wss://target-relay3.com"
|
||||
],
|
||||
"delay": 30,
|
||||
"pad": "+150",
|
||||
"padding": "+150",
|
||||
"p": "superball_b_pubkey", // Next superball (optional - if missing, final posting)
|
||||
"audit": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456", // Audit pubkey (always required)
|
||||
"payment": "eCash_token_here" // Optional payment for processing
|
||||
@@ -41,7 +41,7 @@ User creates routing instructions:
|
||||
User creates a routing event with NIP-44 encryption:
|
||||
```json
|
||||
{
|
||||
"kind": 30000, // Superball routing event
|
||||
"kind": 22222, // Superball routing event
|
||||
"pubkey": "ephemeral_key",
|
||||
"content": "<nip44_encrypted_payload>",
|
||||
"tags": [
|
||||
@@ -61,14 +61,14 @@ The encrypted payload contains:
|
||||
"pubkey": "user_pubkey",
|
||||
"content": "Message content",
|
||||
"tags": [],
|
||||
"created_at": 1703000000,
|
||||
"created_at": 1702222200,
|
||||
"id": "event_id",
|
||||
"sig": "user_signature"
|
||||
},
|
||||
"routing": {
|
||||
"relays": ["wss://target-relay1.com", "wss://target-relay2.com"],
|
||||
"delay": 30,
|
||||
"pad": "+150",
|
||||
"padding": "+150",
|
||||
"p": "next_superball_pubkey", // Optional - if missing, final posting
|
||||
"audit": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456", // Required audit pubkey
|
||||
"payment": "eCash_token" // Optional payment
|
||||
@@ -78,7 +78,7 @@ The encrypted payload contains:
|
||||
|
||||
## Superball Processing
|
||||
|
||||
1. **Receive**: Monitor for kind 30000 events with p tag = own pubkey
|
||||
1. **Receive**: Monitor for kind 22222 events with p tag = own pubkey
|
||||
2. **Decrypt**: Use NIP-44 to decrypt the content
|
||||
3. **Parse**: Extract the event and routing instructions
|
||||
4. **Apply Padding**: Modify padding according to instructions
|
||||
@@ -96,10 +96,10 @@ Superballs **ALWAYS** create a new routing wrapper to prevent analysis of paddin
|
||||
- **Metadata mixing**: Padding levels change unpredictably at each hop
|
||||
|
||||
### Routing Event Structure
|
||||
Every forward creates a new kind 30000 event:
|
||||
Every forward creates a new kind 22222 event:
|
||||
```json
|
||||
{
|
||||
"kind": 30000,
|
||||
"kind": 22222,
|
||||
"pubkey": "<fresh_ephemeral_key>",
|
||||
"content": "<newly_encrypted_payload>",
|
||||
"tags": [
|
||||
@@ -133,14 +133,14 @@ The audit mechanism allows users to detect malicious Superballs that drop events
|
||||
{
|
||||
"relays": ["wss://relay2.com"],
|
||||
"delay": 45,
|
||||
"pad": "+200",
|
||||
"padding": "+200",
|
||||
"p": "sball_b_real_pubkey",
|
||||
"audit": "a1b2c3...fake_pubkey_for_audit"
|
||||
}
|
||||
|
||||
// Superball A should post this after 45s with +200 bytes:
|
||||
{
|
||||
"kind": 30000,
|
||||
"kind": 22222,
|
||||
"tags": [
|
||||
["p", "sball_b_real_pubkey"], // Real next hop
|
||||
["p", "a1b2c3...fake_pubkey"], // Audit tag (looks identical)
|
||||
3
deploy.sh
Executable file
3
deploy.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
rsync -avz --chmod=644 --progress web/{superball.html,thrower.html,superball-shared.css} ubuntu@laantungir.net:WWW/superball/
|
||||
1
style_guide
Submodule
1
style_guide
Submodule
Submodule style_guide added at 111a0631f2
190
web/login-and-profile.html
Normal file
190
web/login-and-profile.html
Normal file
@@ -0,0 +1,190 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>🔐 NOSTR_LOGIN_LITE - All Login Methods Test</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: #ffffff;
|
||||
min-height: 100vh;
|
||||
color: #000000;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="login-section">
|
||||
<!-- Login UI if needed -->
|
||||
</div>
|
||||
<div id="profile-section">
|
||||
<img id="profile-picture">
|
||||
<div id="profile-pubkey"></div>
|
||||
<div id="profile-name"></div>
|
||||
<div id="profile-about"></div>
|
||||
</div>
|
||||
|
||||
<!-- Load the official nostr-tools bundle first -->
|
||||
<script src="../lite/nostr.bundle.js"></script>
|
||||
|
||||
<!-- Load NOSTR_LOGIN_LITE main library (now includes NIP-46 extension) -->
|
||||
<script src="../lite/nostr-lite.js"></script>
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
// Global variables
|
||||
let nlLite = null;
|
||||
let userPubkey = null;
|
||||
let relayUrl = 'wss://relay.laantungir.net';
|
||||
|
||||
// Initialize NOSTR_LOGIN_LITE
|
||||
async function initializeApp() {
|
||||
// console.log('INFO', 'Initializing NOSTR_LOGIN_LITE...');
|
||||
|
||||
try {
|
||||
await window.NOSTR_LOGIN_LITE.init({
|
||||
theme: 'default',
|
||||
darkMode: false,
|
||||
methods: {
|
||||
extension: true,
|
||||
local: true,
|
||||
seedphrase: true,
|
||||
connect: true, // Enables "Nostr Connect" (NIP-46)
|
||||
remote: true, // Also needed for "Nostr Connect" compatibility
|
||||
otp: true // Enables "DM/OTP"
|
||||
},
|
||||
floatingTab: {
|
||||
enabled: true,
|
||||
hPosition: .98, // 95% from left
|
||||
vPosition: 0, // 50% from top (center)
|
||||
getUserInfo: true, // Fetch user profiles
|
||||
getUserRelay: ['wss://relay.laantungir.net'], // Custom relays for profiles
|
||||
appearance: {
|
||||
style: 'minimal',
|
||||
theme: 'auto',
|
||||
icon: '',
|
||||
text: 'Login',
|
||||
iconOnly: false
|
||||
},
|
||||
behavior: {
|
||||
hideWhenAuthenticated: false,
|
||||
showUserInfo: true,
|
||||
autoSlide: false,
|
||||
persistent: false
|
||||
},
|
||||
animation: {
|
||||
slideDirection: 'right' // Slide to the right when hiding
|
||||
}
|
||||
},
|
||||
debug: true
|
||||
});
|
||||
|
||||
nlLite = window.NOSTR_LOGIN_LITE;
|
||||
console.log('SUCCESS', 'NOSTR_LOGIN_LITE initialized successfully');
|
||||
|
||||
window.addEventListener('nlMethodSelected', handleAuthEvent);
|
||||
|
||||
} catch (error) {
|
||||
console.log('ERROR', `Initialization failed: ${error.message}`);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function handleAuthEvent(event) {
|
||||
const { pubkey, method, error } = event.detail;
|
||||
console.log('INFO', `Auth event received: method=${method}`);
|
||||
|
||||
if (method && pubkey) {
|
||||
userPubkey = pubkey;
|
||||
console.log('SUCCESS', `Login successful! Method: ${method}, Pubkey: ${pubkey}`);
|
||||
|
||||
loadUserProfile();
|
||||
|
||||
} else if (error) {
|
||||
console.log('ERROR', `Authentication error: ${error}`);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Load user profile using nostr-tools pool
|
||||
async function loadUserProfile() {
|
||||
if (!userPubkey) return;
|
||||
|
||||
console.log('INFO', `Loading profile for: ${userPubkey}`);
|
||||
document.getElementById('profile-name').textContent = 'Loading profile...';
|
||||
document.getElementById('profile-pubkey').textContent = userPubkey;
|
||||
|
||||
try {
|
||||
// Create a SimplePool instance
|
||||
const pool = new window.NostrTools.SimplePool();
|
||||
const relays = [relayUrl, 'wss://relay.laantungir.net'];
|
||||
|
||||
// Get profile event (kind 0) for the user using querySync
|
||||
const events = await pool.querySync(relays, {
|
||||
kinds: [0],
|
||||
authors: [userPubkey],
|
||||
limit: 1
|
||||
});
|
||||
|
||||
pool.close(relays); // Clean up connections
|
||||
|
||||
if (events.length > 0) {
|
||||
console.log('SUCCESS', 'Profile event received');
|
||||
const profile = JSON.parse(events[0].content);
|
||||
displayProfile(profile);
|
||||
} else {
|
||||
console.log('INFO', 'No profile found');
|
||||
document.getElementById('profile-name').textContent = 'No profile found';
|
||||
document.getElementById('profile-about').textContent = 'User has not set up a profile yet.';
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log('ERROR', `Profile loading failed: ${error.message}`);
|
||||
document.getElementById('profile-name').textContent = 'Error loading profile';
|
||||
document.getElementById('profile-about').textContent = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
// Display profile data
|
||||
function displayProfile(profile) {
|
||||
const name = profile.name || profile.display_name || profile.displayName || 'Anonymous User';
|
||||
const about = profile.about || '';
|
||||
const picture = profile.picture || '';
|
||||
|
||||
document.getElementById('profile-name').textContent = name;
|
||||
document.getElementById('profile-about').textContent = about;
|
||||
|
||||
if (picture) {
|
||||
document.getElementById('profile-picture').src = picture;
|
||||
}
|
||||
|
||||
console.log('SUCCESS', `Profile displayed: ${name}`);
|
||||
}
|
||||
|
||||
// Logout function
|
||||
async function logout() {
|
||||
console.log('INFO', 'Logging out...');
|
||||
try {
|
||||
await nlLite.logout();
|
||||
console.log('SUCCESS', 'Logged out successfully');
|
||||
} catch (error) {
|
||||
console.log('ERROR', `Logout failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Initialize the app
|
||||
setTimeout(initializeApp, 100);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
4355
web/nostr-lite.js
Normal file
4355
web/nostr-lite.js
Normal file
File diff suppressed because it is too large
Load Diff
11534
web/nostr.bundle.js
Normal file
11534
web/nostr.bundle.js
Normal file
File diff suppressed because it is too large
Load Diff
184
web/sign.html
Normal file
184
web/sign.html
Normal file
@@ -0,0 +1,184 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NIP-07 Signing Test</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<div id="status"></div>
|
||||
|
||||
<div id="test-section" style="display:none;">
|
||||
<button id="sign-button">Sign Event</button>
|
||||
<button id="encrypt-button">Test NIP-04 Encrypt</button>
|
||||
<button id="decrypt-button">Test NIP-04 Decrypt</button>
|
||||
<div id="results"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="../lite/nostr.bundle.js"></script>
|
||||
<script src="../lite/nostr-lite.js"></script>
|
||||
|
||||
<script>
|
||||
let testPubkey = 'npub1damus9dqe7g7jqn45kjcjgsv0vxjqnk8cxjkf8gqjwm8t8qjm7cqm3z7l';
|
||||
let ciphertext = '';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await window.NOSTR_LOGIN_LITE.init({
|
||||
theme: 'default',
|
||||
methods: {
|
||||
extension: true,
|
||||
local: true,
|
||||
readonly: true,
|
||||
connect: true,
|
||||
remote: true,
|
||||
otp: true
|
||||
},
|
||||
floatingTab: {
|
||||
enabled: true,
|
||||
hPosition: 1, // 0.0-1.0 or '95%' from left
|
||||
vPosition: 0, // 0.0-1.0 or '50%' from top
|
||||
appearance: {
|
||||
style: 'pill', // 'pill', 'square', 'circle', 'minimal'
|
||||
icon: '', // Clean display without icon placeholders
|
||||
text: 'Login'
|
||||
},
|
||||
behavior: {
|
||||
hideWhenAuthenticated: false,
|
||||
showUserInfo: true,
|
||||
autoSlide: true
|
||||
},
|
||||
getUserInfo: true, // Enable profile fetching
|
||||
getUserRelay: [ // Specific relays for profile fetching
|
||||
'wss://relay.laantungir.net'
|
||||
]
|
||||
}});
|
||||
|
||||
|
||||
// document.getElementById('login-button').addEventListener('click', () => {
|
||||
// window.NOSTR_LOGIN_LITE.launch('login');
|
||||
// });
|
||||
|
||||
window.addEventListener('nlMethodSelected', (event) => {
|
||||
document.getElementById('status').textContent = `Authenticated with: ${event.detail.method}`;
|
||||
document.getElementById('test-section').style.display = 'block';
|
||||
});
|
||||
|
||||
document.getElementById('sign-button').addEventListener('click', testSigning);
|
||||
document.getElementById('encrypt-button').addEventListener('click', testEncryption);
|
||||
document.getElementById('decrypt-button').addEventListener('click', testDecryption);
|
||||
});
|
||||
|
||||
async function testSigning() {
|
||||
try {
|
||||
console.log('=== DEBUGGING SIGN EVENT START ===');
|
||||
console.log('testSigning: window.nostr is:', window.nostr);
|
||||
console.log('testSigning: window.nostr constructor:', window.nostr?.constructor?.name);
|
||||
console.log('testSigning: window.nostr === our facade?', window.nostr?.constructor?.name === 'WindowNostr');
|
||||
|
||||
// Get user public key for comparison
|
||||
const userPubkey = await window.nostr.getPublicKey();
|
||||
console.log('User public key:', userPubkey);
|
||||
|
||||
// Check auth state if our facade
|
||||
if (window.nostr?.constructor?.name === 'WindowNostr') {
|
||||
console.log('WindowNostr authState:', window.nostr.authState);
|
||||
console.log('WindowNostr authenticatedExtension:', window.nostr.authenticatedExtension);
|
||||
console.log('WindowNostr existingNostr:', window.nostr.existingNostr);
|
||||
}
|
||||
|
||||
const event = {
|
||||
kind: 1,
|
||||
content: 'Hello from NIP-07!',
|
||||
tags: [],
|
||||
created_at: Math.floor(Date.now() / 1000)
|
||||
};
|
||||
|
||||
console.log('=== EVENT BEING SENT TO EXTENSION ===');
|
||||
console.log('Event object:', JSON.stringify(event, null, 2));
|
||||
console.log('Event keys:', Object.keys(event));
|
||||
console.log('Event kind type:', typeof event.kind, event.kind);
|
||||
console.log('Event content type:', typeof event.content, event.content);
|
||||
console.log('Event tags type:', typeof event.tags, event.tags);
|
||||
console.log('Event created_at type:', typeof event.created_at, event.created_at);
|
||||
console.log('Event created_at value:', event.created_at);
|
||||
|
||||
// Check if created_at is within reasonable bounds
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const timeDiff = Math.abs(event.created_at - now);
|
||||
console.log('Time difference from now (seconds):', timeDiff);
|
||||
console.log('Event timestamp as Date:', new Date(event.created_at * 1000));
|
||||
|
||||
// Additional debugging for user-specific issues
|
||||
console.log('=== USER-SPECIFIC DEBUG INFO ===');
|
||||
console.log('User pubkey length:', userPubkey?.length);
|
||||
console.log('User pubkey format check (hex):', /^[a-fA-F0-9]{64}$/.test(userPubkey));
|
||||
|
||||
// Try to get user profile info if available
|
||||
try {
|
||||
const profileEvent = {
|
||||
kinds: [0],
|
||||
authors: [userPubkey],
|
||||
limit: 1
|
||||
};
|
||||
console.log('Would query profile with filter:', profileEvent);
|
||||
} catch (profileErr) {
|
||||
console.log('Profile query setup failed:', profileErr);
|
||||
}
|
||||
|
||||
console.log('=== ABOUT TO CALL EXTENSION SIGN EVENT ===');
|
||||
const signedEvent = await window.nostr.signEvent(event);
|
||||
|
||||
console.log('=== SIGN EVENT SUCCESSFUL ===');
|
||||
console.log('Signed event:', JSON.stringify(signedEvent, null, 2));
|
||||
console.log('Signed event keys:', Object.keys(signedEvent));
|
||||
console.log('Signature present:', !!signedEvent.sig);
|
||||
console.log('ID present:', !!signedEvent.id);
|
||||
console.log('Pubkey matches user:', signedEvent.pubkey === userPubkey);
|
||||
|
||||
document.getElementById('results').innerHTML = `<h3>Signed Event:</h3><pre>${JSON.stringify(signedEvent, null, 2)}</pre>`;
|
||||
|
||||
console.log('=== DEBUGGING SIGN EVENT END ===');
|
||||
} catch (error) {
|
||||
console.error('=== SIGN EVENT ERROR ===');
|
||||
console.error('Error message:', error.message);
|
||||
console.error('Error stack:', error.stack);
|
||||
console.error('Error object:', error);
|
||||
|
||||
document.getElementById('results').innerHTML = `<h3>Sign Error:</h3><pre>${error.message}</pre><pre>${error.stack}</pre>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function testEncryption() {
|
||||
try {
|
||||
const plaintext = 'Secret message for testing';
|
||||
const pubkey = await window.nostr.getPublicKey();
|
||||
|
||||
ciphertext = await window.nostr.nip04.encrypt(pubkey, plaintext);
|
||||
document.getElementById('results').innerHTML = `<h3>Encrypted:</h3><pre>${ciphertext}</pre>`;
|
||||
} catch (error) {
|
||||
document.getElementById('results').innerHTML = `<h3>Encrypt Error:</h3><pre>${error.message}</pre>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function testDecryption() {
|
||||
try {
|
||||
if (!ciphertext) {
|
||||
document.getElementById('results').innerHTML = `<h3>Decrypt Error:</h3><pre>No ciphertext available. Run encrypt first.</pre>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const pubkey = await window.nostr.getPublicKey();
|
||||
const decrypted = await window.nostr.nip04.decrypt(pubkey, ciphertext);
|
||||
document.getElementById('results').innerHTML = `<h3>Decrypted:</h3><pre>${decrypted}</pre>`;
|
||||
} catch (error) {
|
||||
document.getElementById('results').innerHTML = `<h3>Decrypt Error:</h3><pre>${error.message}</pre>`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
740
web/superball-shared.css
Normal file
740
web/superball-shared.css
Normal file
@@ -0,0 +1,740 @@
|
||||
/* Superball Shared Styles */
|
||||
|
||||
/* Apply theme variables and classes */
|
||||
:root {
|
||||
/* Core Variables (6) */
|
||||
--primary-color: #000000;
|
||||
--secondary-color: #ffffff;
|
||||
--accent-color: #ff0000;
|
||||
--muted-color: #666666;
|
||||
--font-family: "Courier New", Courier, monospace;
|
||||
--border-radius: 15px;
|
||||
--border-width: 3px;
|
||||
|
||||
/* Floating Tab Variables (8) - from theme.css */
|
||||
--tab-bg-logged-out: #ffffff;
|
||||
--tab-bg-logged-in: #ffffff;
|
||||
--tab-bg-opacity-logged-out: 0.9;
|
||||
--tab-bg-opacity-logged-in: 0.2;
|
||||
--tab-color-logged-out: #000000;
|
||||
--tab-color-logged-in: #ffffff;
|
||||
--tab-border-logged-out: #000000;
|
||||
--tab-border-logged-in: #ff0000;
|
||||
--tab-border-opacity-logged-out: 1.0;
|
||||
--tab-border-opacity-logged-in: 0.1;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family);
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: var(--secondary-color);
|
||||
min-height: 100vh;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.section {
|
||||
margin: 20px 0;
|
||||
border: var(--border-width) solid var(--primary-color);
|
||||
padding: 15px;
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--secondary-color);
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 18px;
|
||||
font-family: var(--font-family);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
margin: 5px 0;
|
||||
border: var(--border-width) solid var(--primary-color);
|
||||
border-radius: var(--border-radius);
|
||||
font-family: var(--font-family);
|
||||
background: var(--secondary-color);
|
||||
color: var(--primary-color);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
border-color: var(--accent-color);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button {
|
||||
background: var(--secondary-color);
|
||||
color: var(--primary-color);
|
||||
border: var(--border-width) solid var(--primary-color);
|
||||
border-radius: var(--border-radius);
|
||||
font-family: var(--font-family);
|
||||
cursor: pointer;
|
||||
width: auto;
|
||||
padding: 10px 20px;
|
||||
margin: 5px 0;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
button:active {
|
||||
background: var(--accent-color);
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
#main-content button {
|
||||
background: var(--secondary-color);
|
||||
color: var(--primary-color);
|
||||
border: var(--border-width) solid var(--primary-color);
|
||||
border-radius: var(--border-radius);
|
||||
font-family: var(--font-family);
|
||||
cursor: pointer;
|
||||
width: auto;
|
||||
padding: 10px 20px;
|
||||
margin: 5px 0;
|
||||
transition: border-color 0.2s ease;
|
||||
height: 40px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#main-content button:hover {
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
#main-content button:active {
|
||||
background: var(--accent-color);
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
#main-content .button-primary {
|
||||
background: var(--secondary-color);
|
||||
color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
#main-content .button-primary:hover {
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
#main-content .button-danger {
|
||||
background: var(--accent-color);
|
||||
color: var(--secondary-color);
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
#main-content .button-danger:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
margin-bottom: 3px;
|
||||
font-family: var(--font-family);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.relay-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 5px 0;
|
||||
padding: 8px;
|
||||
border: var(--border-width) solid var(--primary-color);
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--secondary-color);
|
||||
}
|
||||
|
||||
.relay-url {
|
||||
flex: 1;
|
||||
font-family: var(--font-family);
|
||||
font-size: 12px;
|
||||
word-break: break-all;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.relay-type {
|
||||
min-width: 80px;
|
||||
font-size: 12px;
|
||||
color: var(--muted-color);
|
||||
text-align: center;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.relay-auth-status {
|
||||
min-width: 100px;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
font-family: var(--font-family);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.auth-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.auth-indicator.read-write {
|
||||
background-color: #28a745;
|
||||
/* Green - fully compatible */
|
||||
}
|
||||
|
||||
.auth-indicator.read-only {
|
||||
background-color: #ffc107;
|
||||
/* Yellow - read only */
|
||||
}
|
||||
|
||||
.auth-indicator.error {
|
||||
background-color: #dc3545;
|
||||
/* Red - connection error */
|
||||
}
|
||||
|
||||
.auth-indicator.testing {
|
||||
background-color: #6c757d;
|
||||
/* Gray - testing in progress */
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-status-text {
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.relay-actions {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
#main-content .relay-actions button {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
width: auto;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.status-message {
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-radius: var(--border-radius);
|
||||
border: var(--border-width) solid var(--primary-color);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.success {
|
||||
background: var(--secondary-color);
|
||||
color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.error {
|
||||
background: var(--secondary-color);
|
||||
color: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.info {
|
||||
background: var(--secondary-color);
|
||||
color: var(--primary-color);
|
||||
border-color: var(--muted-color);
|
||||
}
|
||||
|
||||
.add-relay-form {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.add-relay-form input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.add-relay-form select {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.add-relay-form button {
|
||||
width: auto;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.pubkey-display {
|
||||
font-family: var(--font-family);
|
||||
font-size: 12px;
|
||||
word-break: break-all;
|
||||
background: var(--secondary-color);
|
||||
color: var(--muted-color);
|
||||
padding: 5px;
|
||||
border: var(--border-width) solid var(--muted-color);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.daemon-control {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
#main-content #daemon-toggle {
|
||||
font-size: 16px;
|
||||
padding: 10px 20px;
|
||||
margin-bottom: 15px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
#main-content #daemon-toggle.running {
|
||||
background: var(--accent-color);
|
||||
color: var(--secondary-color);
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
#main-content #daemon-toggle.running:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
#daemon-status {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
background: var(--secondary-color);
|
||||
border: var(--border-width) solid var(--primary-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
font-family: var(--font-family);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.event-queue-item {
|
||||
background: var(--secondary-color);
|
||||
border: var(--border-width) solid var(--muted-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 10px;
|
||||
margin: 5px 0;
|
||||
font-family: var(--font-family);
|
||||
font-size: 12px;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.event-queue-item.processing {
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
padding: 8px;
|
||||
margin: 2px 0;
|
||||
border-left: var(--border-width) solid var(--muted-color);
|
||||
background: var(--secondary-color);
|
||||
font-family: var(--font-family);
|
||||
font-size: 12px;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.log-entry.success {
|
||||
border-left-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.log-entry.error {
|
||||
border-left-color: var(--accent-color);
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.log-entry.info {
|
||||
border-left-color: var(--muted-color);
|
||||
}
|
||||
|
||||
.log-timestamp {
|
||||
color: var(--muted-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#processing-log {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: var(--border-width) solid var(--primary-color);
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--secondary-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-family);
|
||||
color: var(--primary-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: var(--font-family);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
strong {
|
||||
font-family: var(--font-family);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
small {
|
||||
font-family: var(--font-family);
|
||||
color: var(--muted-color);
|
||||
}
|
||||
|
||||
#thrower-banner {
|
||||
border: var(--border-width) solid var(--primary-color);
|
||||
border-radius: var(--border-radius);
|
||||
filter: grayscale(100%);
|
||||
transition: filter 0.3s ease;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
#thrower-banner:hover {
|
||||
filter: grayscale(0%) saturate(50%);
|
||||
}
|
||||
|
||||
#thrower-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: var(--border-radius);
|
||||
border: var(--border-width) solid var(--primary-color);
|
||||
filter: grayscale(100%);
|
||||
transition: filter 0.3s ease;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
#thrower-icon:hover {
|
||||
filter: grayscale(0%) saturate(50%);
|
||||
}
|
||||
|
||||
#login-container {
|
||||
margin: 20px auto;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
#profile-picture {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: var(--border-radius);
|
||||
border: var(--border-width) solid var(--primary-color);
|
||||
filter: grayscale(100%);
|
||||
transition: filter 0.3s ease;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
#profile-picture:hover {
|
||||
filter: grayscale(0%) saturate(50%);
|
||||
}
|
||||
|
||||
#profile-name,
|
||||
#profile-pubkey {
|
||||
font-family: var(--font-family);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
#profile-pubkey {
|
||||
color: var(--muted-color);
|
||||
font-size: 12px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Superball Builder Specific Styles */
|
||||
.json-display {
|
||||
background: var(--secondary-color);
|
||||
border: var(--border-width) solid var(--muted-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 10px;
|
||||
font-family: var(--font-family);
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
margin: 10px 0;
|
||||
color: var(--muted-color);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.json-display:not(:empty) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bounce-section {
|
||||
background: var(--secondary-color);
|
||||
border: var(--border-width) solid var(--muted-color);
|
||||
}
|
||||
|
||||
.small-input {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
/* Visualization Styles */
|
||||
.visualization {
|
||||
background: var(--secondary-color);
|
||||
border: var(--border-width) solid var(--accent-color);
|
||||
padding: 15px;
|
||||
margin: 15px 0;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.timeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.timeline-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border: var(--border-width) solid var(--primary-color);
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--secondary-color);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.step-time {
|
||||
min-width: 80px;
|
||||
font-weight: bold;
|
||||
color: var(--muted-color);
|
||||
font-size: 12px;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.step-actor {
|
||||
min-width: 100px;
|
||||
font-weight: bold;
|
||||
color: var(--primary-color);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.step-action {
|
||||
flex: 1;
|
||||
margin: 0 10px;
|
||||
font-family: var(--font-family);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.step-size {
|
||||
min-width: 80px;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
color: var(--muted-color);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.step-relays {
|
||||
font-size: 11px;
|
||||
color: var(--muted-color);
|
||||
font-style: italic;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
/* Throw button hover effect */
|
||||
.throw-button:hover {
|
||||
border-color: var(--accent-color) !important;
|
||||
}
|
||||
|
||||
#profile-about {
|
||||
font-family: var(--font-family);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Thrower Discovery Styles */
|
||||
.thrower-item {
|
||||
background: var(--secondary-color);
|
||||
border: var(--border-width) solid var(--muted-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 8px 15px;
|
||||
margin: 5px 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.thrower-item.online {
|
||||
border-color: #28a745;
|
||||
}
|
||||
|
||||
.thrower-item.offline {
|
||||
border-color: #dc3545;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.thrower-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.thrower-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.thrower-details-section {
|
||||
margin-top: 10px;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease, opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.thrower-details-section.collapsed {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.thrower-details-section.expanded {
|
||||
max-height: 1000px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.expand-triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 8px solid var(--muted-color);
|
||||
transition: transform 0.3s ease;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.expand-triangle.expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.thrower-condensed-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.thrower-status {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.thrower-status.online {
|
||||
background-color: #28a745;
|
||||
}
|
||||
|
||||
.thrower-status.offline {
|
||||
background-color: #dc3545;
|
||||
}
|
||||
|
||||
.thrower-name {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.thrower-pubkey {
|
||||
font-family: var(--font-family);
|
||||
font-size: 10px;
|
||||
color: var(--muted-color);
|
||||
word-break: break-all;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.thrower-description {
|
||||
color: var(--primary-color);
|
||||
margin: 8px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.thrower-details {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--muted-color);
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.thrower-detail {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#discovery-status {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.loading {
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
/* Additional theme styles from theme.css */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.floating-tab {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 10px 15px;
|
||||
border-radius: var(--border-radius);
|
||||
border: var(--border-width) solid;
|
||||
font-family: var(--font-family);
|
||||
font-size: 14px;
|
||||
z-index: 1000;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.floating-tab.logged-out {
|
||||
background: rgba(255, 255, 255, var(--tab-bg-opacity-logged-out));
|
||||
color: var(--tab-color-logged-out);
|
||||
border-color: rgba(0, 0, 0, var(--tab-border-opacity-logged-out));
|
||||
}
|
||||
|
||||
.floating-tab.logged-in {
|
||||
background: rgba(255, 255, 255, var(--tab-bg-opacity-logged-in));
|
||||
color: var(--tab-color-logged-in);
|
||||
border-color: rgba(255, 0, 0, var(--tab-border-opacity-logged-in));
|
||||
}
|
||||
|
||||
.floating-tab:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
1921
web/superball.html
Normal file
1921
web/superball.html
Normal file
File diff suppressed because it is too large
Load Diff
2187
web/thrower.html
Normal file
2187
web/thrower.html
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user